1 package org.opentrafficsim.road.gtu.generator.headway;
2
3 import org.djunits.value.vdouble.scalar.Duration;
4 import org.djunits.value.vdouble.scalar.Time;
5 import org.opentrafficsim.base.parameters.ParameterException;
6 import org.opentrafficsim.core.distributions.Generator;
7 import org.opentrafficsim.core.distributions.ProbabilityException;
8 import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
9
10 import nl.tudelft.simulation.jstats.distributions.DistNormal;
11 import nl.tudelft.simulation.jstats.streams.StreamInterface;
12
13 /**
14 * Headway generation based on {@code Arrivals}.
15 * <p>
16 * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
17 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
18 * </p>
19 * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
20 * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
21 * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
22 */
23 public class ArrivalsHeadwayGenerator implements Generator<Duration>
24 {
25
26 /** Arrivals. */
27 private final Arrivals arrivals;
28
29 /** Simulator. */
30 private final OtsSimulatorInterface simulator;
31
32 /** Random stream to draw headway. */
33 private final StreamInterface stream;
34
35 /** Random headway generator. */
36 private final HeadwayDistribution distribution;
37
38 /** First GTU. */
39 private boolean first = true;
40
41 /**
42 * @param arrivals Arrivals; arrivals
43 * @param simulator OtsSimulatorInterface; simulator
44 * @param stream StreamInterface; random stream to draw headway
45 * @param distribution HeadwayDistribution; random headway distribution
46 */
47 public ArrivalsHeadwayGenerator(final Arrivals arrivals, final OtsSimulatorInterface simulator,
48 final StreamInterface stream, final HeadwayDistribution distribution)
49 {
50 this.arrivals = arrivals;
51 this.simulator = simulator;
52 this.stream = stream;
53 this.distribution = distribution;
54 }
55
56 /**
57 * Returns a new headway {@code h} assuming that the previous vehicle arrived at the current time {@code t0}. The vehicle
58 * thus arrives at {@code t1 = t0 + h}. This method guarantees that no vehicle arrives during periods where demand is zero,
59 * while maintaining random headways based on average demand over a certain time period.<br>
60 * <br>
61 * The general method is to find {@code h} such that the integral of the demand pattern {@code D} from {@code t0} until
62 * {@code t1} equals {@code r}: Σ{@code D(t0 > t1) = r}. One can think of {@code r} as being 1 and representing an
63 * additional vehicle to arrive. The headway {@code h} that results correlates directly to the mean demand between
64 * {@code t0} and {@code t1}.<br>
65 * <br>
66 * The value of {@code r} always has a mean of 1, but may vary between specific vehicle arrivals depending on the headway
67 * distribution. When assuming constant headways for any given demand level, {@code r} always equals 1. For exponentially
68 * distributed headways {@code r} may range anywhere between 0 and infinity.<br>
69 * <br>
70 * This usage of {@code r} guarantees that no vehicles arrive during periods with 0 demand. For example:
71 * <ul>
72 * <li>Suppose we have 0 demand between 300s and 400s.</li>
73 * <li>The previous vehicle was generated at 299s.</li>
74 * <li>The demand at 299s equals 1800veh/h (1 veh per 2s).</li>
75 * <li>For both constant and exponentially distributed headways, the expected next vehicle arrival based on this demand
76 * value alone would be 299 + 2 = 301s. This is within the 0-demand period and should not happen. It's also not
77 * theoretically sound, as the demand from 299s until 301s is not 1800veh/h on average.</li>
78 * <li>Using integration we find that the surface of demand from 299s until 300s equals 0.5 veh for stepwise demand, and
79 * 0.25 veh for linear demand. Consequently, the vehicle will not arrive until later slices integrate to an additional 0.5
80 * veh or 0.75 veh respectively. This additional surface under the demand curve is only found after 400s.</li>
81 * <li>In case the exponential headway distribution would have resulted in {@code r} < 0.5 (stepwise demand) or 0.25
82 * (linear demand), a vehicle will simply arrive between 299s and 300s.</li>
83 * </ul>
84 * <br>
85 * @return Duration; new headway
86 * @throws ProbabilityException if the stored collection is empty
87 * @throws ParameterException in case of a parameter exception
88 */
89 @Override
90 public Duration draw() throws ProbabilityException, ParameterException
91 {
92 Time now = this.simulator.getSimulatorAbsTime();
93 // initial slice times and frequencies
94 Time t1 = now;
95 double f1 = this.arrivals.getFrequency(t1, true).si;
96 Time t2 = this.arrivals.nextTimeSlice(t1);
97 if (t2 == null)
98 {
99 return null; // no new vehicle
100 }
101 double f2 = this.arrivals.getFrequency(t2, false).si;
102 // next vehicle's random factor
103 double rem = this.distribution.draw(this.stream);
104 if (this.first)
105 {
106 // first headway may be partially in the past, take a random factor
107 rem *= this.stream.nextDouble();
108 this.first = false;
109 }
110 // integrate until rem (by reducing it to 0.0, possibly in steps per slice)
111 while (rem > 0.0)
112 {
113 // extrapolate to find 'integration = rem' in this slice giving demand slope, this may be beyond the slice length
114 double dt = t2.si - t1.si;
115 double t;
116 double slope = (f2 - f1) / dt;
117 if (Math.abs(slope) < 1e-12) // no slope
118 {
119 if (f1 > 0.0)
120 {
121 t = rem / f1; // rem = t * f1, t = rem / f1
122 }
123 else
124 {
125 t = Double.POSITIVE_INFINITY; // no demand in this slice
126 }
127 }
128 else
129 {
130 // reverse of trapezoidal rule: rem = t * (f1 + (f1 + t * slope)) / 2
131 double sqrt = 2 * slope * rem + f1 * f1;
132 if (sqrt >= 0.0)
133 {
134 t = (-f1 + Math.sqrt(sqrt)) / slope;
135 }
136 else
137 {
138 t = Double.POSITIVE_INFINITY; // not sufficient demand in this slice, with negative slope
139 }
140 }
141 if (t > dt)
142 {
143 // next slice
144 rem -= dt * (f1 + f2) / 2; // subtract integral of this slice using trapezoidal rule
145 t1 = t2;
146 t2 = this.arrivals.nextTimeSlice(t1);
147 if (t2 == null)
148 {
149 return null; // no new vehicle
150 }
151 f1 = this.arrivals.getFrequency(t1, true).si; // we can't use f1 = f2 due to possible steps in demand
152 f2 = this.arrivals.getFrequency(t2, false).si;
153 }
154 else
155 {
156 // return resulting integration times
157 return Duration.instantiateSI(t1.si + t - now.si);
158 }
159 }
160 throw new RuntimeException("Exception while determining headway from Arrivals.");
161 }
162
163 /** {@inheritDoc} */
164 @Override
165 public String toString()
166 {
167 return "ArrivalsHeadwayGenerator [arrivals=" + this.arrivals + ", simulator=" + this.simulator + ", stream="
168 + this.stream + ", distribution=" + this.distribution + ", first=" + this.first + "]";
169 }
170
171 /**
172 * Headway distribution.
173 * <p>
174 * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
175 * <br>
176 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
177 * </p>
178 * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
179 * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
180 * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
181 */
182 public interface HeadwayDistribution
183 {
184
185 /** Constant headway. */
186 HeadwayDistribution CONSTANT = new HeadwayDistribution()
187 {
188 @Override
189 public double draw(final StreamInterface randomStream)
190 {
191 return 1.0;
192 }
193
194 @Override
195 public String getName()
196 {
197 return "CONSTANT";
198 }
199 };
200
201 /** Exponential headway distribution. */
202 HeadwayDistribution EXPONENTIAL = new HeadwayDistribution()
203 {
204 @Override
205 public double draw(final StreamInterface randomStream)
206 {
207 return -Math.log(randomStream.nextDouble());
208 }
209
210 @Override
211 public String getName()
212 {
213 return "EXPONENTIAL";
214 }
215 };
216
217 /** Uniform headway distribution. */
218 HeadwayDistribution UNIFORM = new HeadwayDistribution()
219 {
220 @Override
221 public double draw(final StreamInterface randomStream)
222 {
223 return 2.0 * randomStream.nextDouble();
224 }
225
226 @Override
227 public String getName()
228 {
229 return "UNIFORM";
230 }
231 };
232
233 /** Triangular headway distribution. */
234 HeadwayDistribution TRIANGULAR = new HeadwayDistribution()
235 {
236 @Override
237 public double draw(final StreamInterface randomStream)
238 {
239 double r = randomStream.nextDouble();
240 if (r < .5)
241 {
242 return Math.sqrt(r * 2.0);
243 }
244 return 2.0 - Math.sqrt((1.0 - r) * 2.0);
245 }
246
247 @Override
248 public String getName()
249 {
250 return "TRIANGULAR";
251 }
252 };
253
254 /** Triangular (left side, mean 2/3) and exponential (right side, mean 4/3) headway distribution. */
255 HeadwayDistribution TRI_EXP = new HeadwayDistribution()
256 {
257 @Override
258 public double draw(final StreamInterface randomStream)
259 {
260 double r = randomStream.nextDouble();
261 if (r < .5)
262 {
263 return Math.sqrt(r * 2.0); // left-hand side of triangular distribution, with mean 2/3
264 }
265 return 1.0 - Math.log((1.0 - r) * 2.0) / 3.0; // 1 + 1/3, where 1/3 is mean of exponential right-hand side
266 // note: 50% with mean 2/3 and 50% with mean 1 + 1/3 gives a mean of 1
267 }
268
269 @Override
270 public String getName()
271 {
272 return "TRI_EXP";
273 }
274 };
275
276 /** Log-normal headway distribution (variance = 1.0). */
277 HeadwayDistribution LOGNORMAL = new HeadwayDistribution()
278 {
279 /** Mu. */
280 private final double mu = Math.log(1.0 / Math.sqrt(2.0));
281
282 /** Sigma. */
283 private final double sigma = Math.sqrt(Math.log(2.0));
284
285 @Override
286 public double draw(final StreamInterface randomStream)
287 {
288 return Math.exp(new DistNormal(randomStream, this.mu, this.sigma).draw());
289 }
290
291 @Override
292 public String getName()
293 {
294 return "LOGNORMAL";
295 }
296 };
297
298 /**
299 * Draws a randomized headway factor. The average value returned is always 1.0. The returned value is applied on the
300 * demand pattern by (reversed) integration to derive actual headways.
301 * @param randomStream StreamInterface; random number stream
302 * @return randomized headway factor
303 */
304 double draw(StreamInterface randomStream);
305
306 /**
307 * Returns the distribution name.
308 * @return distribution name
309 */
310 String getName();
311
312 }
313
314 }