View Javadoc
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-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://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
21   * @author <a href="https://github.com/wjschakel">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}: &#931;{@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} &lt; 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-2024 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://github.com/wjschakel">Wouter Schakel</a>
181      */
182     public interface HeadwayDistribution
183     {
184 
185         /** Constant headway. */
186         HeadwayDistribution CONSTANT = new HeadwayDistribution()
187         {
188             /** {@inheritDoc} */
189             @Override
190             public double draw(final StreamInterface randomStream)
191             {
192                 return 1.0;
193             }
194 
195             /** {@inheritDoc} */
196             @Override
197             public String getName()
198             {
199                 return "CONSTANT";
200             }
201         };
202 
203         /** Exponential headway distribution. */
204         HeadwayDistribution EXPONENTIAL = new HeadwayDistribution()
205         {
206             /** {@inheritDoc} */
207             @Override
208             public double draw(final StreamInterface randomStream)
209             {
210                 return -Math.log(randomStream.nextDouble());
211             }
212 
213             /** {@inheritDoc} */
214             @Override
215             public String getName()
216             {
217                 return "EXPONENTIAL";
218             }
219         };
220 
221         /** Uniform headway distribution. */
222         HeadwayDistribution UNIFORM = new HeadwayDistribution()
223         {
224             /** {@inheritDoc} */
225             @Override
226             public double draw(final StreamInterface randomStream)
227             {
228                 return 2.0 * randomStream.nextDouble();
229             }
230 
231             /** {@inheritDoc} */
232             @Override
233             public String getName()
234             {
235                 return "UNIFORM";
236             }
237         };
238 
239         /** Triangular headway distribution. */
240         HeadwayDistribution TRIANGULAR = new HeadwayDistribution()
241         {
242             /** {@inheritDoc} */
243             @Override
244             public double draw(final StreamInterface randomStream)
245             {
246                 double r = randomStream.nextDouble();
247                 if (r < .5)
248                 {
249                     return Math.sqrt(r * 2.0);
250                 }
251                 return 2.0 - Math.sqrt((1.0 - r) * 2.0);
252             }
253 
254             /** {@inheritDoc} */
255             @Override
256             public String getName()
257             {
258                 return "TRIANGULAR";
259             }
260         };
261 
262         /** Triangular (left side, mean 2/3) and exponential (right side, mean 4/3) headway distribution. */
263         HeadwayDistribution TRI_EXP = new HeadwayDistribution()
264         {
265             /** {@inheritDoc} */
266             @Override
267             public double draw(final StreamInterface randomStream)
268             {
269                 double r = randomStream.nextDouble();
270                 if (r < .5)
271                 {
272                     return Math.sqrt(r * 2.0); // left-hand side of triangular distribution, with mean 2/3
273                 }
274                 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
275                 // note: 50% with mean 2/3 and 50% with mean 1 + 1/3 gives a mean of 1
276             }
277 
278             /** {@inheritDoc} */
279             @Override
280             public String getName()
281             {
282                 return "TRI_EXP";
283             }
284         };
285 
286         /** Log-normal headway distribution (variance = 1.0). */
287         HeadwayDistribution LOGNORMAL = new HeadwayDistribution()
288         {
289             /** Mu. */
290             private final double mu = Math.log(1.0 / Math.sqrt(2.0));
291 
292             /** Sigma. */
293             private final double sigma = Math.sqrt(Math.log(2.0));
294 
295             /** {@inheritDoc} */
296             @Override
297             public double draw(final StreamInterface randomStream)
298             {
299                 return Math.exp(new DistNormal(randomStream, this.mu, this.sigma).draw());
300             }
301 
302             /** {@inheritDoc} */
303             @Override
304             public String getName()
305             {
306                 return "LOGNORMAL";
307             }
308         };
309 
310         /**
311          * Draws a randomized headway factor. The average value returned is always 1.0. The returned value is applied on the
312          * demand pattern by (reversed) integration to derive actual headways.
313          * @param randomStream StreamInterface; random number stream
314          * @return randomized headway factor
315          */
316         double draw(StreamInterface randomStream);
317 
318         /**
319          * Returns the distribution name.
320          * @return distribution name
321          */
322         String getName();
323 
324     }
325 
326 }