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