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}: Σ{@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} < 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 }