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.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}: &#931;{@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} &lt; 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 }