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
9 import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
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-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
17 * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
18 * <p>
19 * @version $Revision$, $LastChangedDate$, by $Author$, initial version 13 dec. 2017 <br>
20 * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
21 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
22 * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
23 */
24 public class ArrivalsHeadwayGenerator implements Generator<Duration>
25 {
26
27 /** Arrivals. */
28 private final Arrivals arrivals;
29
30 /** Simulator. */
31 private final SimulatorInterface.TimeDoubleUnit simulator;
32
33 /** Random stream to draw headway. */
34 private final StreamInterface stream;
35
36 /** Random headway generator. */
37 private final HeadwayDistribution distribution;
38
39 /** First GTU. */
40 private boolean first = true;
41
42 /**
43 * @param arrivals Arrivals; arrivals
44 * @param simulator SimulatorInterface.TimeDoubleUnit; simulator
45 * @param stream StreamInterface; random stream to draw headway
46 * @param distribution HeadwayDistribution; random headway distribution
47 */
48 public ArrivalsHeadwayGenerator(final Arrivals arrivals, final SimulatorInterface.TimeDoubleUnit 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 Duration; new headway
87 * @throws ProbabilityException if the stored collection is empty
88 * @throws ParameterException in case of a parameter exception
89 */
90 @Override
91 public Duration draw() throws ProbabilityException, ParameterException
92 {
93 Time now = this.simulator.getSimulatorTime();
94 // initial slice times and frequencies
95 Time t1 = now;
96 double f1 = this.arrivals.getFrequency(t1, true).si;
97 Time t2 = this.arrivals.nextTimeSlice(t1);
98 if (t2 == null)
99 {
100 return null; // no new vehicle
101 }
102 double f2 = this.arrivals.getFrequency(t2, false).si;
103 // next vehicle's random factor
104 double rem = this.distribution.draw(this.stream);
105 if (this.first)
106 {
107 // first headway may be partially in the past, take a random factor
108 rem *= this.stream.nextDouble();
109 this.first = false;
110 }
111 // integrate until rem (by reducing it to 0.0, possibly in steps per slice)
112 while (rem > 0.0)
113 {
114 // extrapolate to find 'integration = rem' in this slice giving demand slope, this may beyond the slice length
115 double dt = t2.si - t1.si;
116 double t;
117 double slope = (f2 - f1) / dt;
118 if (Math.abs(slope) < 1e-12) // no slope
119 {
120 if (f1 > 0.0)
121 {
122 t = rem / f1; // rem = t * f1, t = rem / f1
123 }
124 else
125 {
126 t = Double.POSITIVE_INFINITY; // no demand in this slice
127 }
128 }
129 else
130 {
131 // reverse of trapezoidal rule: rem = t * (f1 + (f1 + t * slope)) / 2
132 double sqrt = 2 * slope * rem + f1 * f1;
133 if (sqrt >= 0.0)
134 {
135 t = (-f1 + Math.sqrt(sqrt)) / slope;
136 }
137 else
138 {
139 t = Double.POSITIVE_INFINITY; // not sufficient demand in this slice, with negative slope
140 }
141 }
142 if (t > dt)
143 {
144 // next slice
145 rem -= dt * (f1 + f2) / 2; // subtract integral of this slice using trapezoidal rule
146 t1 = t2;
147 t2 = this.arrivals.nextTimeSlice(t1);
148 if (t2 == null)
149 {
150 return null; // no new vehicle
151 }
152 f1 = this.arrivals.getFrequency(t1, true).si; // we can't use f1 = f2 due to possible steps in demand
153 f2 = this.arrivals.getFrequency(t2, false).si;
154 }
155 else
156 {
157 // return resulting integration times
158 return Duration.instantiateSI(t1.si + t - now.si);
159 }
160 }
161 throw new RuntimeException("Exception while determining headway from Arrivals.");
162 }
163
164 @Override
165 public String toString()
166 {
167 return "ArrivalsHeadwayGenerator [arrivals=" + arrivals + ", simulator=" + simulator + ", stream=" + stream
168 + ", distribution=" + distribution + ", first=" + first + "]";
169 }
170
171 /**
172 * Headway distribution.
173 * <p>
174 * Copyright (c) 2013-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
175 * <br>
176 * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
177 * <p>
178 * @version $Revision$, $LastChangedDate$, by $Author$, initial version 5 dec. 2017 <br>
179 * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
180 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
181 * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
182 */
183 public interface HeadwayDistribution
184 {
185
186 /** Constant headway. */
187 HeadwayDistribution CONSTANT = new HeadwayDistribution()
188 {
189 @Override
190 public double draw(final StreamInterface randomStream)
191 {
192 return 1.0;
193 }
194
195 @Override
196 public String getName()
197 {
198 return "CONSTANT";
199 }
200 };
201
202 /** Exponential headway distribution. */
203 HeadwayDistribution EXPONENTIAL = new HeadwayDistribution()
204 {
205 @Override
206 public double draw(final StreamInterface randomStream)
207 {
208 return -Math.log(randomStream.nextDouble());
209 }
210
211 @Override
212 public String getName()
213 {
214 return "EXPONENTIAL";
215 }
216 };
217
218 /** Uniform headway distribution. */
219 HeadwayDistribution UNIFORM = new HeadwayDistribution()
220 {
221 @Override
222 public double draw(final StreamInterface randomStream)
223 {
224 return 2.0 * randomStream.nextDouble();
225 }
226
227 @Override
228 public String getName()
229 {
230 return "UNIFORM";
231 }
232 };
233
234 /** Triangular headway distribution. */
235 HeadwayDistribution TRIANGULAR = new HeadwayDistribution()
236 {
237 @Override
238 public double draw(final StreamInterface randomStream)
239 {
240 double r = randomStream.nextDouble();
241 if (r < .5)
242 {
243 return Math.sqrt(r * 2.0);
244 }
245 return 2.0 - Math.sqrt((1.0 - r) * 2.0);
246 }
247
248 @Override
249 public String getName()
250 {
251 return "TRIANGULAR";
252 }
253 };
254
255 /** Triangular (left side, mean 2/3) and exponential (right side, mean 4/3) headway distribution. */
256 HeadwayDistribution TRI_EXP = new HeadwayDistribution()
257 {
258 @Override
259 public double draw(final StreamInterface randomStream)
260 {
261 double r = randomStream.nextDouble();
262 if (r < .5)
263 {
264 return Math.sqrt(r * 2.0); // left-hand side of triangular distribution, with mean 2/3
265 }
266 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
267 // note: 50% with mean 2/3 and 50% with mean 1 + 1/3 gives a mean of 1
268 }
269
270 @Override
271 public String getName()
272 {
273 return "TRI_EXP";
274 }
275 };
276
277 /** Log-normal headway distribution (variance = 1.0). */
278 HeadwayDistribution LOGNORMAL = new HeadwayDistribution()
279 {
280 /** Mu. */
281 private final double mu = Math.log(1.0 / Math.sqrt(2.0));
282
283 /** Sigma. */
284 private final double sigma = Math.sqrt(Math.log(2.0));
285
286 @Override
287 public double draw(final StreamInterface randomStream)
288 {
289 return Math.exp(new DistNormal(randomStream, this.mu, this.sigma).draw());
290 }
291
292 @Override
293 public String getName()
294 {
295 return "LOGNORMAL";
296 }
297 };
298
299 /**
300 * Draws a randomized headway factor. The average value returned is always 1.0. The returned value is applied on the
301 * demand pattern by (reversed) integration to derive actual headways.
302 * @param randomStream StreamInterface; random number stream
303 * @return randomized headway factor
304 */
305 double draw(StreamInterface randomStream);
306
307 /**
308 * Returns the distribution name.
309 * @return distribution name
310 */
311 String getName();
312
313 }
314
315 }