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