View Javadoc
1   package org.opentrafficsim.road.gtu.generator;
2   
3   import java.util.LinkedHashMap;
4   import java.util.Map;
5   import java.util.PriorityQueue;
6   import java.util.Queue;
7   import java.util.SortedMap;
8   import java.util.TreeMap;
9   
10  import javax.naming.NamingException;
11  
12  import org.djunits.value.vdouble.scalar.Duration;
13  import org.djunits.value.vdouble.scalar.Frequency;
14  import org.djunits.value.vdouble.vector.DurationVector;
15  import org.djunits.value.vdouble.vector.FrequencyVector;
16  import org.djutils.exceptions.Throw;
17  import org.djutils.exceptions.Try;
18  import org.djutils.math.means.ArithmeticMean;
19  import org.opentrafficsim.base.geometry.OtsGeometryException;
20  import org.opentrafficsim.base.logger.Logger;
21  import org.opentrafficsim.base.parameters.ParameterException;
22  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
23  import org.opentrafficsim.core.gtu.GtuException;
24  import org.opentrafficsim.core.gtu.GtuType;
25  import org.opentrafficsim.core.network.NetworkException;
26  import org.opentrafficsim.core.network.Node;
27  import org.opentrafficsim.road.gtu.generator.characteristics.LaneBasedGtuCharacteristicsGenerator;
28  import org.opentrafficsim.road.gtu.generator.characteristics.LaneBasedGtuCharacteristicsGeneratorOd;
29  import org.opentrafficsim.road.network.lane.Lane;
30  import org.opentrafficsim.road.od.Category;
31  import org.opentrafficsim.road.od.Interpolation;
32  
33  import nl.tudelft.simulation.dsol.SimRuntimeException;
34  import nl.tudelft.simulation.jstats.streams.StreamInterface;
35  
36  /**
37   * Connects with a lane-based GTU generator to disable it over some time and generate a platoon instead. Platoons are added as:
38   * {@code Platoons.ofCategory(...).addPlatoon(...).addGtu(...).addGtu(...).addPlatoon(...).addGtu(...).addGtu(...).start();}.
39   * Method {@code addGtu(...)} may only determine a generation time if other info is set by {@code fixInfo(...)}.<br>
40   * <br>
41   * This class may be used with a {@code LaneBasedGtuCharacteristicsGenerator} or {@code GtuCharacteristicsGeneratorOD}. Use
42   * {@code ofGtuType()} or {@code ofCategory()} respectively.
43   * <p>
44   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
45   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
46   * </p>
47   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
48   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
49   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
50   * @param <T> type of demand category, typically a Category in an OdMatrix or a GtuType
51   */
52  public abstract class Platoons<T>
53  {
54  
55      /** GTU generator to disable. */
56      private LaneBasedGtuGenerator gen;
57  
58      /** Simulator. */
59      private final OtsSimulatorInterface simulator;
60  
61      /** Position to generate the GTU's at. */
62      private final Lane position;
63  
64      /** Queue of GTU information to generate GTU's with. */
65      private final Queue<PlatoonGtu<T>> queue = new PriorityQueue<>();
66  
67      /** Map of platoon start and end times. */
68      private final SortedMap<Duration, Duration> periods = new TreeMap<>();
69  
70      /** Start time of current platoon. */
71      private Duration startTime;
72  
73      /** End time of current platoon. */
74      private Duration endTime;
75  
76      /** Origin to use on added GTU. */
77      private Node fixedOrigin;
78  
79      /** Destination to use on added GTU. */
80      private Node fixedDestination;
81  
82      /** Category to use on added GTU. */
83      private T fixedCategory;
84  
85      /** Number of GTUs that will be generated. */
86      private Map<T, Integer> numberOfGtus = new LinkedHashMap<>();
87  
88      /** Whether the Platoons was started, after which nothing may be added. */
89      private boolean started = false;
90  
91      /**
92       * Constructor.
93       * @param simulator simulator
94       * @param position position
95       */
96      private Platoons(final OtsSimulatorInterface simulator, final Lane position)
97      {
98          this.simulator = simulator;
99          this.position = position;
100     }
101 
102     /**
103      * Creates a {@code Platoon&lt;Category&gt;} instance for platoons.
104      * @param characteristics characteristics generator
105      * @param simulator simulator
106      * @param stream random number stream
107      * @param position position
108      * @return platoons based on OD
109      */
110     public static Platoons<Category> ofCategory(final LaneBasedGtuCharacteristicsGeneratorOd characteristics,
111             final OtsSimulatorInterface simulator, final StreamInterface stream, final Lane position)
112     {
113         return new Platoons<Category>(simulator, position)
114         {
115             /** Characteristics generator OD based. */
116             private final LaneBasedGtuCharacteristicsGeneratorOd characteristicsOD = characteristics;
117 
118             /** Random number stream. */
119             private final StreamInterface strm = stream;
120 
121             @Override
122             protected void placeGtu(final PlatoonGtu<Category> platoonGtu) throws SimRuntimeException, NamingException,
123                     GtuException, NetworkException, OtsGeometryException, ParameterException
124             {
125                 getGenerator().queueGtu(this.characteristicsOD.draw(platoonGtu.origin(), platoonGtu.destination(),
126                         platoonGtu.category(), this.strm), getPosition());
127                 start();
128             }
129         };
130     }
131 
132     /**
133      * Creates a {@code Platoon&lt;GtuType&gt;} instance for platoons.
134      * @param characteristics characteristics generator
135      * @param simulator simulator
136      * @param stream random number stream
137      * @param position position
138      * @return platoons based on OD
139      */
140     @SuppressWarnings("synthetic-access")
141     public static Platoons<GtuType> ofGtuType(final LaneBasedGtuCharacteristicsGenerator characteristics,
142             final OtsSimulatorInterface simulator, final StreamInterface stream, final Lane position)
143     {
144         return new Platoons<GtuType>(simulator, position)
145         {
146             /** Characteristics generator. */
147             private final LaneBasedGtuCharacteristicsGenerator chrctrstcs = characteristics;
148 
149             @Override
150             protected void placeGtu(final PlatoonGtu<GtuType> platoonGtu) throws SimRuntimeException, NamingException,
151                     GtuException, NetworkException, OtsGeometryException, ParameterException
152             {
153                 // we actually do nothing with the platoonGtu here
154                 getGenerator().queueGtu(this.chrctrstcs.draw(), getPosition());
155                 start();
156             }
157         };
158     }
159 
160     /**
161      * Add a platoon. The generator is disabled during the provided time frame. Individual GTU's should be added using
162      * {@code addGtu}.
163      * @param start start time
164      * @param end end time
165      * @return for method chaining
166      * @throws SimRuntimeException on exception
167      */
168     public Platoons<T> addPlatoon(final Duration start, final Duration end) throws SimRuntimeException
169     {
170         Throw.when(this.started, IllegalStateException.class, "Cannot add a platoon after the Platoons was started.");
171         Throw.whenNull(start, "Start may not be null.");
172         Throw.whenNull(end, "End may not be null.");
173         this.startTime = start;
174         this.endTime = end;
175         this.periods.put(start, end);
176         return this;
177     }
178 
179     /**
180      * Fix all info except time for GTU's added hereafter.
181      * @param origin origin
182      * @param destination destination
183      * @param category category
184      * @return for method chaining
185      */
186     public Platoons<T> fixInfo(final Node origin, final Node destination, final T category)
187     {
188         this.fixedOrigin = origin;
189         this.fixedDestination = destination;
190         this.fixedCategory = category;
191         return this;
192     }
193 
194     /**
195      * Add GTU to the current platoon. Per platoon, GTU may be given in any order. This method uses info set with
196      * {@code fixInfo}.
197      * @param time time of generation
198      * @return for method chaining
199      * @throws IllegalStateException if no fixed info was set using {@code fixInfo}
200      */
201     public Platoons<T> addGtu(final Duration time)
202     {
203         Throw.when(this.fixedOrigin == null || this.fixedDestination == null || this.fixedCategory == null,
204                 IllegalStateException.class, "When using addGtu(Time), used fixInfo(...) before to set other info.");
205         return addGtu(time, this.fixedOrigin, this.fixedDestination, this.fixedCategory);
206     }
207 
208     /**
209      * Add GTU to the current platoon. Per platoon, GTU may be given in any order.
210      * @param time time of generation
211      * @param origin origin
212      * @param destination destination
213      * @param category category
214      * @return for method chaining
215      * @throws IllegalStateException if no platoon was started or time is outside of the platoon time range
216      */
217     public Platoons<T> addGtu(final Duration time, final Node origin, final Node destination, final T category)
218     {
219         Throw.when(this.started, IllegalStateException.class, "Cannot add a GTU after the Platoons was started.");
220         Throw.when(this.startTime == null || this.endTime == null, IllegalStateException.class,
221                 "First call addPlatoon() before calling addGtu()");
222         Throw.when(time.gt(this.endTime) || time.lt(this.startTime), IllegalArgumentException.class,
223                 "Time %s is not between %s and %s", time, this.startTime, this.endTime);
224         this.queue.add(new PlatoonGtu<>(time, origin, destination, category));
225         this.numberOfGtus.put(category, this.numberOfGtus.getOrDefault(category, 0) + 1);
226         return this;
227     }
228 
229     /**
230      * Sets the generator and starts the events.
231      * @param generator GTU generator
232      * @throws SimRuntimeException if start of first platoon is in the past
233      */
234     public void start(final LaneBasedGtuGenerator generator) throws SimRuntimeException
235     {
236         Throw.when(this.started, IllegalStateException.class, "Cannot start the Platoons, it was already started.");
237         this.gen = generator;
238         // check platoon overlap
239         Duration prevEnd = null;
240         for (Map.Entry<Duration, Duration> entry : this.periods.entrySet())
241         {
242             Duration start = entry.getKey();
243             Throw.when(prevEnd != null && start.le(prevEnd), IllegalStateException.class, "Platoons are overlapping.");
244             prevEnd = entry.getValue();
245             this.gen.disable(start, prevEnd, this.position);
246         }
247         this.started = true;
248         start();
249     }
250 
251     /**
252      * Returns the vehicle generator for sub classes.
253      * @return vehicle generator for sub classes
254      */
255     protected LaneBasedGtuGenerator getGenerator()
256     {
257         return this.gen;
258     }
259 
260     /**
261      * Returns the position for sub classes.
262      * @return position for sub classes
263      */
264     protected Lane getPosition()
265     {
266         return this.position;
267     }
268 
269     /**
270      * Starts the events.
271      * @throws SimRuntimeException if start of first platoon is in the past
272      */
273     protected void start() throws SimRuntimeException
274     {
275         if (!this.queue.isEmpty())
276         {
277             this.simulator.scheduleEventAbs(this.queue.peek().time(),
278                     () -> Try.execute(() -> placeGtu(this.queue.poll()), "Exception while placing platoon GTU."));
279         }
280     }
281 
282     /**
283      * Creates a demand vector in which the platoon demand has been compensated from the input demand vector. Only demand
284      * pertaining to the location where the platoons are generated should be compensated.
285      * @param category category
286      * @param demand demand vector
287      * @param time time vector
288      * @param interpolation interpolation
289      * @return demand vector in which the platoon demand has been compensated from the input demand vector
290      */
291     public FrequencyVector compensate(final T category, final FrequencyVector demand, final DurationVector time,
292             final Interpolation interpolation)
293     {
294         Throw.whenNull(category, "Category may not be null.");
295         Throw.whenNull(demand, "Demand may not be null.");
296         Throw.whenNull(time, "Time may not be null.");
297         Throw.whenNull(interpolation, "Interpolation may not be null.");
298         Throw.when(demand.size() != time.size(), IllegalArgumentException.class, "Demand and time have unequal length.");
299         ArithmeticMean<Double, Double> weightedSumLost = new ArithmeticMean<>();
300         for (Map.Entry<Duration, Duration> entry : this.periods.entrySet())
301         {
302             Duration start = entry.getKey();
303             Duration end = entry.getValue();
304             for (int i = 0; i < demand.size() - 1; i++)
305             {
306                 Duration s = Duration.max(start, time.get(i));
307                 Duration e = Duration.min(end, time.get(i + 1));
308                 if (s.lt(e))
309                 {
310                     Frequency fStart = interpolation.interpolateVector(s, demand, time, true);
311                     // TODO: end time of platoon may be in next demand period, which makes the demand non-linear
312                     Frequency fEnd = interpolation.interpolateVector(e, demand, time, false);
313                     weightedSumLost.add((fStart.si + fEnd.si) / 2, e.si - s.si);
314                 }
315             }
316         }
317         ArithmeticMean<Double, Double> weightedSumTotal = new ArithmeticMean<>();
318         for (int i = 0; i < demand.size() - 1; i++)
319         {
320             Frequency fStart = interpolation.interpolateVector(time.get(i), demand, time, true);
321             Frequency fEnd = interpolation.interpolateVector(time.get(i + 1), demand, time, false);
322             weightedSumTotal.add((fStart.si + fEnd.si) / 2, time.getSI(i + 1) - time.getSI(i));
323 
324         }
325         // calculate factor
326         double lost = weightedSumLost.getSum();
327         double total = weightedSumTotal.getSum();
328         int platooning = this.numberOfGtus.getOrDefault(category, 0);
329         double factor = (total - platooning) / (total - lost);
330         if (factor < 0.0)
331         {
332             Logger.ots().warn("Reducing demand of {} by {}, demand is set to 0.", total, total - factor * total);
333             factor = 0.0;
334         }
335         // create and return factor copy
336         double[] array = new double[demand.size()];
337         for (int i = 0; i < array.length - 1; i++)
338         {
339             array[i] = demand.getInUnit(i) * factor;
340         }
341         return new FrequencyVector(array, demand.getDisplayUnit());
342     }
343 
344     /**
345      * Places the next platoon GTU and schedules the next one.
346      * @param platoonGtu info of GTU to generate
347      * @throws SimRuntimeException on exception
348      * @throws NamingException on exception
349      * @throws GtuException on exception
350      * @throws NetworkException on exception
351      * @throws OtsGeometryException on exception
352      * @throws ParameterException on exception
353      */
354     protected abstract void placeGtu(PlatoonGtu<T> platoonGtu) throws SimRuntimeException, NamingException, GtuException,
355             NetworkException, OtsGeometryException, ParameterException;
356 
357     /**
358      * Class containing info of a GTU to generate.
359      * <p>
360      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
361      * <br>
362      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
363      * </p>
364      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
365      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
366      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
367      * @param time time to generate
368      * @param origin origin
369      * @param destination destination
370      * @param category category
371      * @param <K> type of demand category, typically a Category in an OdMatrix or a GtuType
372      */
373     private record PlatoonGtu<K>(Duration time, Node origin, Node destination, K category) implements Comparable<PlatoonGtu<K>>
374     {
375         @Override
376         public int compareTo(final PlatoonGtu<K> o)
377         {
378             if (o == null)
379             {
380                 return 1;
381             }
382             return this.time.compareTo(o.time);
383         }
384     }
385 }