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-2020 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 /**
165 * Headway distribution.
166 * <p>
167 * Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
168 * <br>
169 * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
170 * <p>
171 * @version $Revision$, $LastChangedDate$, by $Author$, initial version 5 dec. 2017 <br>
172 * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
173 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
174 * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
175 */
176 public interface HeadwayDistribution
177 {
178
179 /** Constant headway. */
180 HeadwayDistribution CONSTANT = new HeadwayDistribution()
181 {
182 @Override
183 public double draw(final StreamInterface randomStream)
184 {
185 return 1.0;
186 }
187
188 @Override
189 public String getName()
190 {
191 return "CONSTANT";
192 }
193 };
194
195 /** Exponential headway distribution. */
196 HeadwayDistribution EXPONENTIAL = new HeadwayDistribution()
197 {
198 @Override
199 public double draw(final StreamInterface randomStream)
200 {
201 return -Math.log(randomStream.nextDouble());
202 }
203
204 @Override
205 public String getName()
206 {
207 return "EXPONENTIAL";
208 }
209 };
210
211 /** Uniform headway distribution. */
212 HeadwayDistribution UNIFORM = new HeadwayDistribution()
213 {
214 @Override
215 public double draw(final StreamInterface randomStream)
216 {
217 return 2.0 * randomStream.nextDouble();
218 }
219
220 @Override
221 public String getName()
222 {
223 return "UNIFORM";
224 }
225 };
226
227 /** Triangular headway distribution. */
228 HeadwayDistribution TRIANGULAR = new HeadwayDistribution()
229 {
230 @Override
231 public double draw(final StreamInterface randomStream)
232 {
233 double r = randomStream.nextDouble();
234 if (r < .5)
235 {
236 return Math.sqrt(r * 2.0);
237 }
238 return 2.0 - Math.sqrt((1.0 - r) * 2.0);
239 }
240
241 @Override
242 public String getName()
243 {
244 return "TRIANGULAR";
245 }
246 };
247
248 /** Triangular (left side, mean 2/3) and exponential (right side, mean 4/3) headway distribution. */
249 HeadwayDistribution TRI_EXP = new HeadwayDistribution()
250 {
251 @Override
252 public double draw(final StreamInterface randomStream)
253 {
254 double r = randomStream.nextDouble();
255 if (r < .5)
256 {
257 return Math.sqrt(r * 2.0); // left-hand side of triangular distribution, with mean 2/3
258 }
259 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
260 // note: 50% with mean 2/3 and 50% with mean 1 + 1/3 gives a mean of 1
261 }
262
263 @Override
264 public String getName()
265 {
266 return "TRI_EXP";
267 }
268 };
269
270 /** Log-normal headway distribution (variance = 1.0). */
271 HeadwayDistribution LOGNORMAL = new HeadwayDistribution()
272 {
273 /** Mu. */
274 private final double mu = Math.log(1.0 / Math.sqrt(2.0));
275
276 /** Sigma. */
277 private final double sigma = Math.sqrt(Math.log(2.0));
278
279 @Override
280 public double draw(final StreamInterface randomStream)
281 {
282 return Math.exp(new DistNormal(randomStream, this.mu, this.sigma).draw());
283 }
284
285 @Override
286 public String getName()
287 {
288 return "LOGNORMAL";
289 }
290 };
291
292 /**
293 * Draws a randomized headway factor. The average value returned is always 1.0. The returned value is applied on the
294 * demand pattern by (reversed) integration to derive actual headways.
295 * @param randomStream StreamInterface; random number stream
296 * @return randomized headway factor
297 */
298 double draw(StreamInterface randomStream);
299
300 /**
301 * Returns the distribution name.
302 * @return distribution name
303 */
304 String getName();
305
306 }
307
308 }