View Javadoc
1   package org.opentrafficsim.road.od;
2   
3   import java.util.ArrayList;
4   import java.util.Arrays;
5   import java.util.Comparator;
6   import java.util.LinkedHashMap;
7   import java.util.LinkedHashSet;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Map.Entry;
11  import java.util.Set;
12  import java.util.stream.Collectors;
13  
14  import org.djunits.unit.FrequencyUnit;
15  import org.djunits.value.vdouble.scalar.Duration;
16  import org.djunits.value.vdouble.scalar.Frequency;
17  import org.djunits.value.vdouble.scalar.Length;
18  import org.djunits.value.vdouble.scalar.Time;
19  import org.djutils.exceptions.Throw;
20  import org.opentrafficsim.base.parameters.ParameterException;
21  import org.opentrafficsim.core.distributions.Generator;
22  import org.opentrafficsim.core.distributions.ProbabilityException;
23  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
24  import org.opentrafficsim.core.gtu.GtuException;
25  import org.opentrafficsim.core.gtu.GtuType;
26  import org.opentrafficsim.core.idgenerator.IdGenerator;
27  import org.opentrafficsim.core.math.Draw;
28  import org.opentrafficsim.core.network.Connector;
29  import org.opentrafficsim.core.network.Link;
30  import org.opentrafficsim.core.network.LinkType;
31  import org.opentrafficsim.core.network.NetworkException;
32  import org.opentrafficsim.core.network.Node;
33  import org.opentrafficsim.road.gtu.generator.GeneratorPositions;
34  import org.opentrafficsim.road.gtu.generator.GeneratorPositions.LaneBiases;
35  import org.opentrafficsim.road.gtu.generator.LaneBasedGtuGenerator;
36  import org.opentrafficsim.road.gtu.generator.LaneBasedGtuGenerator.RoomChecker;
37  import org.opentrafficsim.road.gtu.generator.MarkovCorrelation;
38  import org.opentrafficsim.road.gtu.generator.characteristics.LaneBasedGtuCharacteristics;
39  import org.opentrafficsim.road.gtu.generator.characteristics.LaneBasedGtuCharacteristicsGenerator;
40  import org.opentrafficsim.road.gtu.generator.characteristics.LaneBasedGtuCharacteristicsGeneratorOd;
41  import org.opentrafficsim.road.gtu.generator.headway.Arrivals;
42  import org.opentrafficsim.road.gtu.generator.headway.ArrivalsHeadwayGenerator;
43  import org.opentrafficsim.road.gtu.generator.headway.ArrivalsHeadwayGenerator.HeadwayDistribution;
44  import org.opentrafficsim.road.gtu.generator.headway.DemandPattern;
45  import org.opentrafficsim.road.network.RoadNetwork;
46  import org.opentrafficsim.road.network.lane.CrossSectionLink;
47  import org.opentrafficsim.road.network.lane.Lane;
48  import org.opentrafficsim.road.network.lane.LanePosition;
49  import org.opentrafficsim.road.network.lane.object.detector.DetectorType;
50  import org.opentrafficsim.road.network.lane.object.detector.LaneDetector;
51  import org.opentrafficsim.road.network.lane.object.detector.SinkDetector;
52  
53  import nl.tudelft.simulation.dsol.SimRuntimeException;
54  import nl.tudelft.simulation.jstats.streams.MersenneTwister;
55  import nl.tudelft.simulation.jstats.streams.StreamInterface;
56  
57  /**
58   * Utility to create vehicle generators on a network from an OD.
59   * <p>
60   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
61   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
62   * </p>
63   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
64   * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
65   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
66   */
67  public final class OdApplier
68  {
69  
70      /**
71       * Utility class.
72       */
73      private OdApplier()
74      {
75          //
76      }
77  
78      /**
79       * Applies the OD to the network by creating vehicle generators. The map returned contains objects created for vehicle
80       * generation. These are bundled in a {@code GeneratorObjects} and mapped to the vehicle generator id. Vehicle generator id
81       * is equal to the origin node id. For lane-based generators the id's are appended with an ordered number (e.g. A1), where
82       * the ordering is first by link id, and then right to left concerning the lateral lane position at the start of the lane.
83       * For node "A" this would for example be:<br>
84       * <table >
85       * <caption>&nbsp;</caption>
86       * <tr>
87       * <th>Generator id</th>
88       * <th>Link</th>
89       * <th>Lateral start offset</th>
90       * </tr>
91       * <tr>
92       * <th>A1</th>
93       * <th>AB</th>
94       * <th>-1.75m</th>
95       * </tr>
96       * <tr>
97       * <th>A2</th>
98       * <th>AB</th>
99       * <th>1.75m</th>
100      * </tr>
101      * <tr>
102      * <th>A3</th>
103      * <th>AC</th>
104      * <th>-3.5m</th>
105      * </tr>
106      * <tr>
107      * <th>A4</th>
108      * <th>AC</th>
109      * <th>0.0m</th>
110      * </tr>
111      * </table>
112      * If the GTU generation is lane-based (i.e. {@code Lane} in the {@code Categorization}) this method creates a
113      * {@code LaneBasedGtuGenerator} per lane. It will have a single source of demand data, specifying demand towards all
114      * relevant destinations, and with a unique {@code MarkovChain} for the GTU type if {@code MarkovCorrelation} is defined.
115      * For zone GTU generation one {@code LaneBasedGtuGenerator} is created, with one single source of demand data, specifying
116      * demand towards all destinations. A single {@code MarkovChain} may be used. Traffic is distributed over possible
117      * {@code Connectors} based on their link-weight, or the number of lanes of the connected links if no weight is given.
118      * @param network RoadNetwork; network
119      * @param od OdMatrix; OD matrix
120      * @param odOptions OdOptions; options for vehicle generation
121      * @param detectorType DetectorType; detector type.
122      * @return Map&lt;String, GeneratorObjects&gt; map of generator id's and created generator objects mainly for testing
123      * @throws ParameterException if a parameter is missing
124      * @throws SimRuntimeException if this method is called after simulation time 0
125      */
126     @SuppressWarnings("checkstyle:methodlength")
127     public static Map<String, GeneratorObjects> applyOd(final RoadNetwork network, final OdMatrix od, final OdOptions odOptions,
128             final DetectorType detectorType) throws ParameterException, SimRuntimeException
129     {
130         Throw.whenNull(network, "Network may not be null.");
131         Throw.whenNull(od, "OD matrix may not be null.");
132         Throw.whenNull(odOptions, "OD options may not be null.");
133         OtsSimulatorInterface simulator = network.getSimulator();
134         Throw.when(!simulator.getSimulatorTime().eq0(), SimRuntimeException.class,
135                 "Method OdApplier.applyOd() should be invoked at simulation time 0.");
136 
137         // TODO sinks? white extension links?
138         for (Node destination : od.getDestinations())
139         {
140             createSensorsAtDestination(destination, simulator, detectorType);
141         }
142 
143         // TODO clean up stream acquiring code after task OTS-315 has been completed
144         StreamInterface stream = getStream(simulator);
145 
146         boolean laneBased = od.getCategorization().entails(Lane.class);
147         Map<String, GeneratorObjects> output = new LinkedHashMap<>();
148         for (Node origin : od.getOrigins())
149         {
150             Map<Lane, DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>>> originNodePerLane = new LinkedHashMap<>();
151             DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>> originNodeZone =
152                     buildDemandNodeTree(od, odOptions, stream, origin, originNodePerLane);
153             Map<DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>>, Set<LanePosition>> initialPositions =
154                     new LinkedHashMap<>();
155             Map<CrossSectionLink, Double> linkWeights = new LinkedHashMap<>();
156             Map<CrossSectionLink, Node> viaNodes = new LinkedHashMap<>();
157             if (laneBased)
158             {
159                 gatherPositionsLaneBased(originNodePerLane, initialPositions);
160             }
161             else
162             {
163                 initialPositions.put(originNodeZone, gatherPositionsZone(origin, linkWeights, viaNodes));
164             }
165             if (linkWeights.isEmpty())
166             {
167                 linkWeights = null;
168                 viaNodes = null;
169             }
170             initialPositions = sortByValue(initialPositions); // sorts by lateral position at link start
171             createGenerators(network, odOptions, simulator, laneBased, stream, output, initialPositions, linkWeights, viaNodes);
172         }
173         return output;
174     }
175 
176     /**
177      * Builds nested demand node structure (i.e. tree) for demand and GTU characteristics generation. If
178      * {@code MarkovCorrelation} is specified, in case of zone GTU generation, a single {@code MarkovChain} is used for the
179      * selection of GTU type and the relevant lane. In case of lane-based GTU generation, one {@code MarkovChain} is used for
180      * each lane, even when multiple {@code Category}'s contain the same lane. This method loops over all destinations for the
181      * given origin, and then over all categories. For lane-based GTU generation, at that level the appropriate origin node is
182      * taken from the input map, or it is created in to it, and the destination demand-node coupled to that for the looped
183      * destination is obtained or created, with possible {@code MarkovChain}. For zone GTU generation, the looping is a more
184      * straight-forward creation of nodes from origin, and for destination and category. The result of lane-based GTU generation
185      * is given in the input map, for zone GTU generation the single origin demand node is returned by the method.
186      * @param od OdMatrix; OD matrix.
187      * @param odOptions OdOptions; OD options.
188      * @param stream StreamInterface; random number stream.
189      * @param origin Node; origin node.
190      * @param originNodePerLane Map&lt;Lane, DemandNode&lt;Node, DemandNode&lt;Node, DemandNode&lt;Category, ?&gt;&gt;&gt;&gt;;
191      *            map of origin demand node per lane, populated for lane-based GTU generation.
192      * @return DemandNode&lt;Node, DemandNode&lt;Node, DemandNode&lt;Category, ?&gt;&gt;&gt;; demand node structure for the
193      *         entire generator in case of zone GTU generation.
194      */
195     private static DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>> buildDemandNodeTree(final OdMatrix od,
196             final OdOptions odOptions, final StreamInterface stream, final Node origin,
197             final Map<Lane, DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>>> originNodePerLane)
198     {
199         boolean laneBased = od.getCategorization().entails(Lane.class);
200         boolean markovian = od.getCategorization().entails(GtuType.class);
201         DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>> demandNode = null; // for each generator, flexibly used
202         MarkovChain markovChain = null;
203         if (!laneBased)
204         {
205             demandNode = new DemandNode<>(origin, stream, null);
206             LinkType linkType = getLinkTypeFromNode(origin);
207             if (markovian)
208             {
209                 MarkovCorrelation<GtuType, Frequency> correlation = odOptions.get(OdOptions.MARKOV, null, origin, linkType);
210                 if (correlation != null)
211                 {
212                     Throw.when(!od.getCategorization().entails(GtuType.class), IllegalArgumentException.class,
213                             "Markov correlation can only be used on OD categorization entailing GTU type.");
214                     markovChain = new MarkovChain(correlation);
215                 }
216             }
217         }
218         for (Node destination : od.getDestinations())
219         {
220             Set<Category> categories = od.getCategories(origin, destination);
221             if (!categories.isEmpty())
222             {
223                 DemandNode<Node, DemandNode<Category, ?>> destinationNode = null;
224                 if (!laneBased)
225                 {
226                     destinationNode = new DemandNode<>(destination, stream, markovChain);
227                     demandNode.addChild(destinationNode);
228                 }
229                 for (Category category : categories)
230                 {
231                     if (laneBased)
232                     {
233                         // obtain or create root and destination nodes
234                         Lane lane = category.get(Lane.class);
235                         demandNode = originNodePerLane.get(lane);
236                         if (demandNode == null)
237                         {
238                             demandNode = new DemandNode<>(origin, stream, null);
239                             originNodePerLane.put(lane, demandNode);
240                         }
241                         destinationNode = demandNode.getChild(destination);
242                         if (destinationNode == null)
243                         {
244                             markovChain = null;
245                             if (markovian)
246                             {
247                                 MarkovCorrelation<GtuType, Frequency> correlation =
248                                         odOptions.get(OdOptions.MARKOV, lane, origin, lane.getLink().getType());
249                                 if (correlation != null)
250                                 {
251                                     Throw.when(!od.getCategorization().entails(GtuType.class), IllegalArgumentException.class,
252                                             "Markov correlation can only be used on OD categorization entailing GTU type.");
253                                     markovChain = new MarkovChain(correlation); // 1 for each generator per lane
254                                 }
255                             }
256                             destinationNode = new DemandNode<>(destination, stream, markovChain);
257                             demandNode.addChild(destinationNode);
258                         }
259                     }
260                     DemandNode<Category, ?> categoryNode =
261                             new DemandNode<>(category, od.getDemandPattern(origin, destination, category));
262                     if (markovian)
263                     {
264                         destinationNode.addLeaf(categoryNode, category.get(GtuType.class));
265                     }
266                     else
267                     {
268                         destinationNode.addChild(categoryNode);
269                     }
270                 }
271             }
272         }
273         return demandNode;
274     }
275 
276     /**
277      * Returns a set of positions for GTU generation from each lane defined in demand. Stores the positions with the coupled
278      * demand node.
279      * @param originNodePerLane Map&lt;Lane, DemandNode&lt;Node, DemandNode&lt;Node, DemandNode&lt;Category, ?&gt;&gt;&gt;&gt;;
280      *            map with a demand node per lane.
281      * @param initialPositions Map&lt;DemandNode&lt;Node, DemandNode&lt;Node, DemandNode&lt;Category, ?&gt;&gt;&gt;,
282      *            Set&lt;LanePosition&gt;&gt;; map with positions per demand node.
283      */
284     private static void gatherPositionsLaneBased(
285             final Map<Lane, DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>>> originNodePerLane,
286             final Map<DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>>, Set<LanePosition>> initialPositions)
287     {
288         for (Lane lane : originNodePerLane.keySet())
289         {
290             DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>> demandNode = originNodePerLane.get(lane);
291             Set<LanePosition> initialPosition = new LinkedHashSet<>();
292             initialPosition.add(lane.getLink().getStartNode().equals(demandNode.getObject())
293                     ? new LanePosition(lane, Length.ZERO) : new LanePosition(lane, lane.getLength()));
294             initialPositions.put(demandNode, initialPosition);
295         }
296     }
297 
298     /**
299      * Returns a set of positions for GTU generation from a zone. All links connected to the origin node are considered. In case
300      * a link is a {@code Connector}, a link weight and via-node over that link are stored in the provided maps, for later use
301      * in constructing weighted generator positions. For each {@code CrossSectionLink} attached to the via-node, or to the first
302      * link if there was no {@code Connector}, positions are generated.
303      * @param origin Node; origin node for the zone.
304      * @param linkWeights Map&lt;CrossSectionLink, Double&gt;; link weight map to place link weights in.
305      * @param viaNodes Map&lt;CrossSectionLink, Node&gt;; via node map to place via nodes in.
306      * @return Set&lt;LanePosition&gt;; gathered lane positions.
307      */
308     private static Set<LanePosition> gatherPositionsZone(final Node origin, final Map<CrossSectionLink, Double> linkWeights,
309             final Map<CrossSectionLink, Node> viaNodes)
310     {
311         Set<LanePosition> positionSet = new LinkedHashSet<>();
312         for (Link link : origin.getLinks())
313         {
314             if (link instanceof Connector)
315             {
316                 Connector connector = (Connector) link;
317                 if (connector.getStartNode().equals(origin))
318                 {
319                     Node connectedNode = connector.getEndNode();
320                     // count number of served links
321                     int served = 0;
322                     for (Link connectedLink : connectedNode.getLinks())
323                     {
324                         if (connectedLink instanceof CrossSectionLink)
325                         {
326                             served++;
327                         }
328                     }
329                     for (Link connectedLink : connectedNode.getLinks())
330                     {
331                         if (connectedLink instanceof CrossSectionLink)
332                         {
333                             if (connector.getDemandWeight() > 0.0)
334                             {
335                                 // store weight under connected link, as this
336                                 linkWeights.put(((CrossSectionLink) connectedLink), connector.getDemandWeight() / served);
337                             }
338                             else
339                             {
340                                 // negative weight results in number of lanes being used
341                                 linkWeights.put(((CrossSectionLink) connectedLink), -1.0);
342                             }
343                             viaNodes.put((CrossSectionLink) connectedLink, connectedNode);
344                             setLanePosition((CrossSectionLink) connectedLink, connectedNode, positionSet);
345                         }
346                     }
347                 }
348             }
349             else if (link instanceof CrossSectionLink)
350             {
351                 setLanePosition((CrossSectionLink) link, origin, positionSet);
352             }
353         }
354         return positionSet;
355     }
356 
357     /**
358      * Creates GTU generators. For lane-based GTU generation (i.e. {@code Lane} in the {@code Categorization}), the generators
359      * will obtain an ID with the node id plus a counter. For this the initial positions need to be sorted. The link weights and
360      * via nodes should be {@code null} for lane-based GTU generation. Furthermore, the lane is then used to obtain OD option
361      * values possibly specified at the lane level. Other than that, for either lane-based or zone GTU generation, a
362      * {@code LaneBasedGtuGenerator} is created for each initial position given.
363      * @param network RoadNetwork; network.
364      * @param odOptions OdOptions; od options.
365      * @param simulator OtsSimulatorInterface; simulator.
366      * @param laneBased boolean; lane in category.
367      * @param stream StreamInterface; random number stream.
368      * @param output Map&lt;String, GeneratorObjects&gt;; map that output elements will be stored in.
369      * @param initialPositions Map&lt;DemandNode&lt;Node, DemandNode&lt;Node, DemandNode&lt;Category, ?&gt;&gt;&gt;,
370      *            Set&lt;LanePosition&gt;&gt;; sorted initial positions.
371      * @param linkWeights Map&lt;CrossSectionLink, Double&gt;; weights per link, may be {@code null}.
372      * @param viaNodes Map&lt;CrossSectionLink, Node&gt;; nodes to select from for zone, may be {@code null}.
373      * @throws ParameterException if drawing from the inter-arrival generator fails
374      */
375     @SuppressWarnings("checkstyle:parameternumber")
376     private static void createGenerators(final RoadNetwork network, final OdOptions odOptions,
377             final OtsSimulatorInterface simulator, final boolean laneBased, final StreamInterface stream,
378             final Map<String, GeneratorObjects> output,
379             final Map<DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>>, Set<LanePosition>> initialPositions,
380             final Map<CrossSectionLink, Double> linkWeights, final Map<CrossSectionLink, Node> viaNodes)
381             throws ParameterException
382     {
383         Map<Node, Integer> laneGeneratorCounterForUniqueId = new LinkedHashMap<>();
384         for (DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>> root : initialPositions.keySet())
385         {
386             Set<LanePosition> initialPosition = initialPositions.get(root);
387             // id
388             Node o = root.getObject();
389             String id = o.getId();
390             if (laneBased)
391             {
392                 Integer count = laneGeneratorCounterForUniqueId.get(o);
393                 if (count == null)
394                 {
395                     count = 0;
396                 }
397                 count++;
398                 id += count;
399                 laneGeneratorCounterForUniqueId.put(o, count);
400             }
401             // functional generation elements
402             Lane lane;
403             LinkType linkType;
404             if (laneBased)
405             {
406                 lane = initialPosition.iterator().next().lane();
407                 linkType = lane.getLink().getType();
408             }
409             else
410             {
411                 lane = null;
412                 linkType = getLinkTypeFromNode(o);
413             }
414             HeadwayDistribution randomization = odOptions.get(OdOptions.HEADWAY_DIST, lane, o, linkType);
415             ArrivalsHeadwayGenerator headwayGenerator = new ArrivalsHeadwayGenerator(root, simulator, stream, randomization);
416             LaneBasedGtuCharacteristicsGeneratorOd characteristicsGeneratorOd =
417                     odOptions.get(OdOptions.GTU_TYPE, lane, o, linkType);
418             LaneBasedGtuCharacteristicsGenerator characteristicsGenerator = new LaneBasedGtuCharacteristicsGenerator()
419             {
420                 /** {@inheritDoc} */
421                 @Override
422                 public LaneBasedGtuCharacteristics draw() throws ProbabilityException, ParameterException, GtuException
423                 {
424                     Time time = simulator.getSimulatorAbsTime();
425                     Node origin = root.getObject();
426                     DemandNode<Node, DemandNode<Category, ?>> destinationNode = root.draw(time);
427                     Node destination = destinationNode.getObject();
428                     Category category = destinationNode.draw(time).getObject();
429                     return characteristicsGeneratorOd.draw(origin, destination, category, stream);
430                 }
431             };
432 
433             RoomChecker roomChecker = odOptions.get(OdOptions.ROOM_CHECKER, lane, o, linkType);
434             IdGenerator idGenerator = odOptions.get(OdOptions.GTU_ID, lane, o, linkType);
435             LaneBiases biases = odOptions.get(OdOptions.LANE_BIAS, lane, o, linkType);
436             // and finally, the generator
437             try
438             {
439                 LaneBasedGtuGenerator generator = new LaneBasedGtuGenerator(id, headwayGenerator, characteristicsGenerator,
440                         GeneratorPositions.create(initialPosition, stream, biases, linkWeights, viaNodes), network, simulator,
441                         roomChecker, idGenerator);
442                 generator.setNoLaneChangeDistance(odOptions.get(OdOptions.NO_LC_DIST, lane, o, linkType));
443                 generator.setInstantaneousLaneChange(odOptions.get(OdOptions.INSTANT_LC, lane, o, linkType));
444                 generator.setErrorHandler(odOptions.get(OdOptions.ERROR_HANDLER, lane, o, linkType));
445                 output.put(id, new GeneratorObjects(generator, headwayGenerator, characteristicsGenerator));
446             }
447             catch (SimRuntimeException exception)
448             {
449                 // should not happen, we check that time is 0
450                 simulator.getLogger().always().error(exception);
451                 throw new RuntimeException(exception);
452             }
453             catch (ProbabilityException exception)
454             {
455                 // should not happen, as we define probabilities in the headwayGenerator
456                 simulator.getLogger().always().error(exception);
457                 throw new RuntimeException(exception);
458             }
459             catch (NetworkException exception)
460             {
461                 // should not happen, as unique ids are guaranteed by UUID
462                 simulator.getLogger().always().error(exception);
463                 throw new RuntimeException(exception);
464             }
465         }
466     }
467 
468     /**
469      * Obtains a stream for vehicle generation.
470      * @param simulator OtsSimulatorInterface; simulator.
471      * @return StreamInterface; stream for vehicle generation.
472      */
473     private static StreamInterface getStream(final OtsSimulatorInterface simulator)
474     {
475         StreamInterface stream = simulator.getModel().getStream("generation");
476         if (stream == null)
477         {
478             stream = simulator.getModel().getStream("default");
479             if (stream == null)
480             {
481                 System.out
482                         .println("Using locally created stream (not from the simulator) for vehicle generation, with seed 1.");
483                 stream = new MersenneTwister(1L);
484             }
485             else
486             {
487                 System.out.println("Using stream 'default' for vehicle generation.");
488             }
489         }
490         return stream;
491     }
492 
493     /**
494      * Create destination sensors at all lanes connected to a destination node. This method considers connectors too.
495      * @param destination Node; destination node
496      * @param simulator OtsSimulatorInterface; simulator
497      * @param detectorType DetectorType; detector type.
498      */
499     private static void createSensorsAtDestination(final Node destination, final OtsSimulatorInterface simulator,
500             final DetectorType detectorType)
501     {
502         for (Link link : destination.getLinks())
503         {
504             if (link.isConnector() && !link.getStartNode().equals(destination))
505             {
506                 createSensorsAtDestinationNode(link.getStartNode(), simulator, detectorType);
507             }
508             else
509             {
510                 createSensorsAtDestinationNode(destination, simulator, detectorType);
511             }
512         }
513     }
514 
515     /**
516      * Create sensors at all lanes connected to this node. This method does not handle connectors.
517      * @param destination Node; the destination node
518      * @param simulator OtsSimulatorInterface; simulator
519      * @param detectorType DetectorType; detector type.
520      */
521     private static void createSensorsAtDestinationNode(final Node destination, final OtsSimulatorInterface simulator,
522             final DetectorType detectorType)
523     {
524         for (Link link : destination.getLinks())
525         {
526             if (link instanceof CrossSectionLink)
527             {
528                 for (Lane lane : ((CrossSectionLink) link).getLanes())
529                 {
530                     try
531                     {
532                         // if the lane already contains a SinkDetector, skip creating a new one
533                         boolean destinationDetectorExists = false;
534                         for (LaneDetector detector : lane.getDetectors())
535                         {
536                             if (detector instanceof SinkDetector)
537                             {
538                                 destinationDetectorExists = true;
539                             }
540                         }
541                         if (!destinationDetectorExists)
542                         {
543                             new SinkDetector(lane, lane.getLength(), simulator, detectorType, SinkDetector.DESTINATION);
544                         }
545                     }
546                     catch (NetworkException exception)
547                     {
548                         // can not happen, we use Length.ZERO and lane.getLength()
549                         simulator.getLogger().always().error(exception);
550                         throw new RuntimeException(exception);
551                     }
552                 }
553             }
554         }
555     }
556 
557     /**
558      * Returns the common ancestor {@code LinkType} of all links connected to the node, moving through connectors.
559      * @param node Node; origin node
560      * @return common ancestor {@code LinkType} of all links connected to the node, moving through connectors
561      */
562     private static LinkType getLinkTypeFromNode(final Node node)
563     {
564         return getLinkTypeFromNode0(node, false);
565     }
566 
567     /**
568      * Returns the common ancestor {@code LinkType} of all links connected to the node, moving through connectors.
569      * @param node Node; origin node
570      * @param ignoreConnectors boolean; ignore connectors
571      * @return common ancestor {@code LinkType} of all links connected to the node, moving through connectors
572      */
573     private static LinkType getLinkTypeFromNode0(final Node node, final boolean ignoreConnectors)
574     {
575         LinkType linkType = null;
576         for (Link link : node.getLinks())
577         {
578             LinkType next = link.getType();
579             if (!ignoreConnectors && link.isConnector())
580             {
581                 Node otherNode = link.getStartNode().equals(node) ? link.getEndNode() : link.getStartNode();
582                 next = getLinkTypeFromNode0(otherNode, true);
583             }
584             if (next != null && !link.isConnector())
585             {
586                 if (linkType == null)
587                 {
588                     linkType = next;
589                 }
590                 else
591                 {
592                     linkType = linkType.commonAncestor(next);
593                     if (linkType == null)
594                     {
595                         // incompatible link types
596                         return null;
597                     }
598                 }
599             }
600         }
601         return linkType;
602     }
603 
604     /**
605      * Returns a sorted map.
606      * @param map Map&lt;K, V&gt;; input map
607      * @param <K> key type (implemented for cleaner code only)
608      * @param <V> value type (implemented for cleaner code only)
609      * @return Map; sorted map
610      */
611     private static <K, V extends Set<LanePosition>> Map<K, V> sortByValue(final Map<K, V> map)
612     {
613         return map.entrySet().stream().sorted(new Comparator<Map.Entry<K, V>>()
614         {
615             @Override
616             public int compare(final Entry<K, V> o1, final Entry<K, V> o2)
617             {
618                 LanePosition lanePos1 = o1.getValue().iterator().next();
619                 String linkId1 = lanePos1.lane().getLink().getId();
620                 LanePosition lanePos2 = o2.getValue().iterator().next();
621                 String linkId2 = lanePos2.lane().getLink().getId();
622                 int c = linkId1.compareToIgnoreCase(linkId2);
623                 if (c == 0)
624                 {
625                     Length pos1 = Length.ZERO;
626                     Length lat1 = lanePos1.lane().getLateralCenterPosition(pos1);
627                     Length pos2 = Length.ZERO;
628                     Length lat2 = lanePos2.lane().getLateralCenterPosition(pos2);
629                     return lat1.compareTo(lat2);
630                 }
631                 return c;
632             }
633         }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
634     }
635 
636     /**
637      * Adds {@code LanePosition}s to the input set, for {@code Lane}s on the given link, starting at the given {@code Node}.
638      * @param link CrossSectionLink; link with lanes to add positions for
639      * @param node Node; node on the side where positions should be placed
640      * @param positionSet Set&lt;LanePosition&gt;; set to add position to
641      */
642     private static void setLanePosition(final CrossSectionLink link, final Node node, final Set<LanePosition> positionSet)
643     {
644         for (Lane lane : link.getLanes())
645         {
646             // TODO should be GTU type dependent.
647             if (lane.getLink().getStartNode().equals(node))
648             {
649                 positionSet.add(new LanePosition(lane, Length.ZERO));
650             }
651         }
652     }
653 
654     /**
655      * Node for demand tree. Based on two constructors there are 2 types of nodes:<br>
656      * <ul>
657      * <li>Branch nodes; with an object and a stream for randomly drawing a child node.</li>
658      * <li>Leaf nodes; with an object and demand data (time, frequency, interpolation).</li>
659      * </ul>
660      * To accomplish a branching of Node (origin) &gt; Node (destination) &gt; Category, the following generics types can be
661      * used:<br>
662      * <br>
663      * {@code DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>>}
664      * <p>
665      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
666      * <br>
667      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
668      * </p>
669      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
670      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
671      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
672      * @param <T> type of contained object
673      * @param <K> type of child nodes
674      */
675     private static class DemandNode<T, K extends DemandNode<?, ?>> implements Arrivals
676     {
677 
678         /** Node object. */
679         private final T object;
680 
681         /** Random stream to draw child node. */
682         private final StreamInterface stream;
683 
684         /** Children. */
685         private final List<K> children = new ArrayList<>();
686 
687         /** Demand data. */
688         private final DemandPattern demandPattern;
689 
690         /** Unique GTU types of leaf nodes. */
691         private final List<GtuType> gtuTypes = new ArrayList<>();
692 
693         /** Number of leaf nodes for the unique GTU types. */
694         private final List<Integer> gtuTypeCounts = new ArrayList<>();
695 
696         /** GTU type of leaf nodes. */
697         private final Map<K, GtuType> gtuTypesPerChild = new LinkedHashMap<>();
698 
699         /** Markov chain for GTU type selection. */
700         private final MarkovChain markov;
701 
702         /**
703          * Constructor for branching node, with Markov selection.
704          * @param object T; node object
705          * @param stream StreamInterface; random stream to draw child node
706          * @param markov MarkovChain; Markov chain
707          */
708         DemandNode(final T object, final StreamInterface stream, final MarkovChain markov)
709         {
710             this.object = object;
711             this.stream = stream;
712             this.demandPattern = null;
713             this.markov = markov;
714         }
715 
716         /**
717          * Constructor for leaf node, without Markov selection.
718          * @param object T; node object
719          * @param demandPattern DemandPattern; demand data
720          */
721         DemandNode(final T object, final DemandPattern demandPattern)
722         {
723             this.object = object;
724             this.stream = null;
725             this.demandPattern = demandPattern;
726             this.markov = null;
727         }
728 
729         /**
730          * Adds child to a branching node.
731          * @param child K; child node
732          */
733         public void addChild(final K child)
734         {
735             this.children.add(child);
736         }
737 
738         /**
739          * Adds child to a branching node.
740          * @param child K; child node
741          * @param gtuType GtuType; gtu type for Markov chain
742          */
743         public void addLeaf(final K child, final GtuType gtuType)
744         {
745             Throw.when(this.gtuTypes == null, IllegalStateException.class,
746                     "Adding leaf with GtuType in not possible on a non-Markov node.");
747             addChild(child);
748             this.gtuTypesPerChild.put(child, gtuType);
749             if (!this.gtuTypes.contains(gtuType))
750             {
751                 this.gtuTypes.add(gtuType);
752                 this.gtuTypeCounts.add(1);
753             }
754             else
755             {
756                 int index = this.gtuTypes.indexOf(gtuType);
757                 this.gtuTypeCounts.set(index, this.gtuTypeCounts.get(index) + 1);
758             }
759         }
760 
761         /**
762          * Randomly draws a child node.
763          * @param time Time; simulation time
764          * @return K; randomly drawn child node
765          */
766         public K draw(final Time time)
767         {
768             Throw.when(this.children.isEmpty(), RuntimeException.class, "Calling draw on a leaf node in the demand tree.");
769             Map<K, Double> weightMap = new LinkedHashMap<>();
770             if (this.markov == null)
771             {
772                 // regular draw, loop children and collect their frequencies
773                 for (K child : this.children)
774                 {
775                     double f = child.getFrequency(time, true).si; // sliceStart = true is arbitrary
776                     weightMap.put(child, f);
777                 }
778             }
779             else
780             {
781                 // markov chain draw, the markov chain only selects a GTU type, not a child node
782                 GtuType[] gtuTypeArray = new GtuType[this.gtuTypes.size()];
783                 gtuTypeArray = this.gtuTypes.toArray(gtuTypeArray);
784                 Frequency[] steadyState = new Frequency[this.gtuTypes.size()];
785                 Arrays.fill(steadyState, Frequency.ZERO);
786                 Map<K, Frequency> frequencies = new LinkedHashMap<>(); // stored, saves us from calculating them twice
787                 for (K child : this.children)
788                 {
789                     GtuType gtuType = this.gtuTypesPerChild.get(child);
790                     int index = this.gtuTypes.indexOf(gtuType);
791                     Frequency f = child.getFrequency(time, true); // sliceStart = true is arbitrary
792                     frequencies.put(child, f);
793                     steadyState[index] = steadyState[index].plus(f);
794                 }
795                 GtuType nextGtuType = this.markov.draw(gtuTypeArray, steadyState, this.stream);
796                 // select only child nodes registered to the next GTU type
797                 for (K child : this.children)
798                 {
799                     if (this.gtuTypesPerChild.get(child).equals(nextGtuType))
800                     {
801                         double f = frequencies.get(child).si;
802                         weightMap.put(child, f);
803                     }
804                 }
805             }
806             return Draw.drawWeighted(weightMap, this.stream);
807         }
808 
809         /**
810          * Returns the node object.
811          * @return T; node object
812          */
813         public T getObject()
814         {
815             return this.object;
816         }
817 
818         /**
819          * Returns the child that pertains to specified object or {@code null} if no such child is present.
820          * @param obj Object; child object
821          * @return child that pertains to specified object or {@code null} if no such child is present
822          */
823         public K getChild(final Object obj)
824         {
825             for (K child : this.children)
826             {
827                 if (child.getObject().equals(obj))
828                 {
829                     return child;
830                 }
831             }
832             return null;
833         }
834 
835         /** {@inheritDoc} */
836         @Override
837         public Frequency getFrequency(final Time time, final boolean sliceStart)
838         {
839             if (this.demandPattern != null)
840             {
841                 return this.demandPattern.getFrequency(time, sliceStart);
842             }
843             Frequency f = new Frequency(0.0, FrequencyUnit.PER_HOUR);
844             for (K child : this.children)
845             {
846                 f = f.plus(child.getFrequency(time, sliceStart));
847             }
848             return f;
849         }
850 
851         /** {@inheritDoc} */
852         @Override
853         public Time nextTimeSlice(final Time time)
854         {
855             if (this.demandPattern != null)
856             {
857                 return this.demandPattern.nextTimeSlice(time);
858             }
859             Time out = null;
860             for (K child : this.children)
861             {
862                 Time childSlice = child.nextTimeSlice(time);
863                 out = out == null || (childSlice != null && childSlice.lt(out)) ? childSlice : out;
864             }
865             return out;
866         }
867 
868         /** {@inheritDoc} */
869         @Override
870         public String toString()
871         {
872             return "DemandNode [object=" + this.object + ", stream=" + this.stream + ", children=" + this.children
873                     + ", demandPattern=" + this.demandPattern + ", gtuTypes=" + this.gtuTypes + ", gtuTypeCounts="
874                     + this.gtuTypeCounts + ", gtuTypesPerChild=" + this.gtuTypesPerChild + ", markov=" + this.markov + "]";
875         }
876 
877     }
878 
879     /**
880      * Wrapper class around a {@code MarkovCorrelation}, including the last type. One of these should be used for each vehicle
881      * generator.
882      */
883     private static class MarkovChain
884     {
885         /** Markov correlation for GTU type selection. */
886         private final MarkovCorrelation<GtuType, Frequency> markov;
887 
888         /** Previously returned GTU type. */
889         private GtuType previousGtuType = null;
890 
891         /**
892          * Constructor.
893          * @param markov MarkovCorrelation&lt;GtuType, Frequency&gt;; Markov correlation for GTU type selection
894          */
895         MarkovChain(final MarkovCorrelation<GtuType, Frequency> markov)
896         {
897             this.markov = markov;
898         }
899 
900         /**
901          * Returns a next GTU type drawn using a Markov chain.
902          * @param gtuTypes GtuType[]; GtuTypes to consider
903          * @param intensities Frequency[]; frequency for each GTU type, i.e. the steady-state
904          * @param stream StreamInterface; stream for random numbers
905          * @return next GTU type drawn using a Markov chain
906          */
907         public GtuType draw(final GtuType[] gtuTypes, final Frequency[] intensities, final StreamInterface stream)
908         {
909             this.previousGtuType = this.markov.drawState(this.previousGtuType, gtuTypes, intensities, stream);
910             return this.previousGtuType;
911         }
912     }
913 
914     /**
915      * Class to contain created generator objects.
916      * <p>
917      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
918      * <br>
919      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
920      * </p>
921      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
922      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
923      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
924      * @param generator LaneBasedGtuGenerator; main generator for GTU's
925      * @param headwayGenerator Generator&lt;Duration&gt;; generator of headways
926      * @param characteristicsGenerator LaneBasedGtuCharacteristicsGenerator; generator of GTU characteristics
927      */
928     public static record GeneratorObjects(LaneBasedGtuGenerator generator, Generator<Duration> headwayGenerator,
929             LaneBasedGtuCharacteristicsGenerator characteristicsGenerator)
930     {
931     }
932 
933 }