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