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