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