View Javadoc
1   package org.opentrafficsim.road.od;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
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.NoSuchElementException;
11  import java.util.Optional;
12  import java.util.Set;
13  import java.util.TreeMap;
14  
15  import org.djunits.unit.DurationUnit;
16  import org.djunits.unit.FrequencyUnit;
17  import org.djunits.value.ValueRuntimeException;
18  import org.djunits.value.vdouble.scalar.Duration;
19  import org.djunits.value.vdouble.scalar.Frequency;
20  import org.djunits.value.vdouble.scalar.Time;
21  import org.djunits.value.vdouble.vector.DurationVector;
22  import org.djunits.value.vdouble.vector.FrequencyVector;
23  import org.djutils.base.Identifiable;
24  import org.djutils.exceptions.Throw;
25  import org.opentrafficsim.base.OtsRuntimeException;
26  import org.opentrafficsim.core.network.NetworkException;
27  import org.opentrafficsim.core.network.Node;
28  import org.opentrafficsim.core.network.route.Route;
29  import org.opentrafficsim.road.gtu.generator.headway.DemandPattern;
30  
31  /**
32   * The minimal OD matrix has 1 origin, 1 destination and 1 time period. More of each can be used. Further categorization of data
33   * is possible, i.e. for origin O to destination D, <i>for lane L, for route R and for vehicle class C</i>, the demand at time T
34   * is D. The further categorization is defined by an array of {@code Class}'s that define the categorization.
35   * <p>
36   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
37   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
38   * </p>
39   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
40   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
41   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
42   */
43  public class OdMatrix implements Identifiable
44  {
45  
46      /** Id. */
47      private final String id;
48  
49      /** Origin nodes. */
50      private final List<Node> origins;
51  
52      /** Destination nodes. */
53      private final List<Node> destinations;
54  
55      /** Categorization of demand data. */
56      private final Categorization categorization;
57  
58      /** Global time vector. */
59      private final DurationVector globalTimeVector;
60  
61      /** Global interpolation of the data. */
62      private final Interpolation globalInterpolation;
63  
64      /** Demand data per origin and destination, and possibly further categorization. */
65      private final Map<Node, Map<Node, Map<Category, DemandPattern>>> demandData = new LinkedHashMap<>();
66  
67      /** Node comparator. */
68      private static final Comparator<Node> COMPARATOR = new Comparator<Node>()
69      {
70          @Override
71          public int compare(final Node o1, final Node o2)
72          {
73              return o1.getId().compareTo(o2.getId());
74          }
75      };
76  
77      /**
78       * Constructs an OD matrix.
79       * @param id id
80       * @param origins origin nodes
81       * @param destinations destination nodes
82       * @param categorization categorization of data
83       * @param globalDurationVector default time
84       * @param globalInterpolation interpolation of demand data
85       * @throws NullPointerException if any input is null
86       */
87      public OdMatrix(final String id, final List<? extends Node> origins, final List<? extends Node> destinations,
88              final Categorization categorization, final DurationVector globalDurationVector,
89              final Interpolation globalInterpolation)
90      {
91          Throw.whenNull(id, "Id may not be null.");
92          Throw.whenNull(origins, "Origins may not be null.");
93          Throw.when(origins.contains(null), NullPointerException.class, "Origin may not contain null.");
94          Throw.whenNull(destinations, "Destination may not be null.");
95          Throw.when(destinations.contains(null), NullPointerException.class, "Destination may not contain null.");
96          Throw.whenNull(categorization, "Categorization may not be null.");
97          // Throw.whenNull(globalDurationVector, "Global time vector may not be null.");
98          // Throw.whenNull(globalInterpolation, "Global interpolation may not be null.");
99          this.id = id;
100         this.origins = new ArrayList<>(origins);
101         this.destinations = new ArrayList<>(destinations);
102         Collections.sort(this.origins, COMPARATOR);
103         Collections.sort(this.destinations, COMPARATOR);
104         this.categorization = categorization;
105         this.globalTimeVector = globalDurationVector;
106         this.globalInterpolation = globalInterpolation;
107         // build empty OD
108         for (Node origin : origins)
109         {
110             Map<Node, Map<Category, DemandPattern>> map = new LinkedHashMap<>();
111             for (Node destination : destinations)
112             {
113                 map.put(destination, new TreeMap<>(new Comparator<Category>()
114                 {
115                     @Override
116                     public int compare(final Category o1, final Category o2)
117                     {
118                         for (int i = 0; i < o1.getCategorization().size(); i++)
119                         {
120                             int order = Integer.compare(o1.get(i).hashCode(), o2.get(i).hashCode());
121                             if (order != 0)
122                             {
123                                 return order;
124                             }
125                         }
126                         return 0;
127                     }
128                 }));
129             }
130             this.demandData.put(origin, map);
131         }
132     }
133 
134     @Override
135     public final String getId()
136     {
137         return this.id;
138     }
139 
140     /**
141      * Returns origins.
142      * @return origins
143      */
144     public final List<Node> getOrigins()
145     {
146         return new ArrayList<>(this.origins);
147     }
148 
149     /**
150      * Returns destinations.
151      * @return destinations
152      */
153     public final List<Node> getDestinations()
154     {
155         return new ArrayList<>(this.destinations);
156     }
157 
158     /**
159      * Returns categorization.
160      * @return categorization
161      */
162     public final Categorization getCategorization()
163     {
164         return this.categorization;
165     }
166 
167     /**
168      * Returns global time vector.
169      * @return global time vector
170      */
171     public final DurationVector getGlobalTimeVector()
172     {
173         return this.globalTimeVector;
174     }
175 
176     /**
177      * Returns global interpolation.
178      * @return global interpolation
179      */
180     public final Interpolation getGlobalInterpolation()
181     {
182         return this.globalInterpolation;
183     }
184 
185     /**
186      * Add a demand vector to OD.
187      * @param origin origin
188      * @param destination destination
189      * @param category category
190      * @param demand demand data, length has to be equal to the global time vector
191      * @param fraction fraction of demand for this category
192      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
193      * @throws IllegalArgumentException if the category does not belong to the categorization
194      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
195      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
196      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
197      * @throws NullPointerException if an input is null
198      */
199     public final void putDemandVector(final Node origin, final Node destination, final Category category,
200             final FrequencyVector demand, final double fraction)
201     {
202         putDemandVector(origin, destination, category, demand, this.globalTimeVector, this.globalInterpolation, fraction);
203     }
204 
205     /**
206      * Add a demand vector to OD.
207      * @param origin origin
208      * @param destination destination
209      * @param category category
210      * @param demand demand data, length has to be equal to the global time vector
211      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
212      * @throws IllegalArgumentException if the category does not belong to the categorization
213      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
214      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
215      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
216      * @throws NullPointerException if an input is null
217      */
218     public final void putDemandVector(final Node origin, final Node destination, final Category category,
219             final FrequencyVector demand)
220     {
221         putDemandVector(origin, destination, category, demand, this.globalTimeVector, this.globalInterpolation);
222     }
223 
224     /**
225      * Add a demand vector to OD. In this method, which all other methods that add or put demand indirectly refer to, many
226      * consistency and validity checks are performed. These do not include checks on network connectivity, since the network may
227      * be subject to change during simulation.
228      * @param origin origin
229      * @param destination destination
230      * @param category category
231      * @param demand demand data, length has to be equal to the time vector
232      * @param timeVector time vector
233      * @param interpolation interpolation
234      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
235      * @throws IllegalArgumentException if the category does not belong to the categorization
236      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
237      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
238      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
239      * @throws NullPointerException if an input is null
240      */
241     public final void putDemandVector(final Node origin, final Node destination, final Category category,
242             final FrequencyVector demand, final DurationVector timeVector, final Interpolation interpolation)
243     {
244         Throw.whenNull(origin, "Origin may not be null.");
245         Throw.whenNull(destination, "Destination may not be null.");
246         Throw.whenNull(category, "Category may not be null.");
247         Throw.whenNull(demand, "Demand data may not be null.");
248         Throw.whenNull(timeVector, "Time vector may not be null.");
249         Throw.whenNull(interpolation, "Interpolation may not be null.");
250         Throw.when(!this.origins.contains(origin), IllegalArgumentException.class, "Origin '%s' is not part of the OD matrix.",
251                 origin);
252         Throw.when(!this.destinations.contains(destination), IllegalArgumentException.class,
253                 "Destination '%s' is not part of the OD matrix.", destination);
254         Throw.when(!this.categorization.equals(category.getCategorization()), IllegalArgumentException.class,
255                 "Provided category %s does not belong to the categorization %s.", category, this.categorization);
256         Throw.when(demand.size() != timeVector.size() || demand.size() < 2, IllegalArgumentException.class,
257                 "Demand data has different length than time vector, or has less than 2 values.");
258         for (Frequency q : demand)
259         {
260             Throw.when(q.lt0(), IllegalArgumentException.class, "Demand contains negative value(s).");
261         }
262         Duration prevTime;
263         try
264         {
265             prevTime = timeVector.get(0).eq0() ? Duration.ofSI(-1.0) : Duration.ZERO;
266         }
267         catch (ValueRuntimeException exception)
268         {
269             // verified to be > 1, so no empty vector
270             throw new OtsRuntimeException("Unexpected exception while checking time vector.", exception);
271         }
272         for (Duration time : timeVector)
273         {
274             Throw.when(prevTime.ge(time), IllegalArgumentException.class,
275                     "Time vector is not strictly increasing, or contains negative time.");
276             prevTime = time;
277         }
278         if (this.categorization.entails(Route.class))
279         {
280             Route route = category.get(Route.class);
281             try
282             {
283                 Throw.when(!route.originNode().equals(origin) || !route.destinationNode().equals(destination),
284                         IllegalArgumentException.class,
285                         "Route from %s to %s does not comply with origin %s and destination %s.", route.originNode(),
286                         route.destinationNode(), origin, destination);
287             }
288             catch (NetworkException exception)
289             {
290                 throw new IllegalArgumentException("Route in OD has no nodes.", exception);
291             }
292         }
293         DurationVector durationVector = new DurationVector(timeVector.getValuesInUnit(), timeVector.getDisplayUnit());
294         DemandPattern demandPattern = new DemandPattern(demand, durationVector, interpolation);
295         this.demandData.get(origin).get(destination).put(category, demandPattern);
296     }
297 
298     /**
299      * Add a demand vector to OD, by a fraction of total demand.
300      * @param origin origin
301      * @param destination destination
302      * @param category category
303      * @param demand demand data, length has to be equal to the time vector
304      * @param timeVector time vector
305      * @param interpolation interpolation
306      * @param fraction fraction of demand for this category
307      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
308      * @throws IllegalArgumentException if the category does not belong to the categorization
309      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
310      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
311      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
312      * @throws NullPointerException if an input is null
313      */
314     public final void putDemandVector(final Node origin, final Node destination, final Category category,
315             final FrequencyVector demand, final DurationVector timeVector, final Interpolation interpolation,
316             final double fraction)
317     {
318         Throw.whenNull(demand, "Demand data may not be null.");
319         FrequencyVector demandScaled;
320         if (fraction == 1.0)
321         {
322             demandScaled = demand;
323         }
324         else
325         {
326             double[] in = demand.getValuesInUnit();
327             double[] scaled = new double[in.length];
328             for (int i = 0; i < in.length; i++)
329             {
330                 scaled[i] = in[i] * fraction;
331             }
332             try
333             {
334                 demandScaled = new FrequencyVector(scaled, demand.getDisplayUnit(), demand.getStorageType());
335             }
336             catch (ValueRuntimeException exception)
337             {
338                 // cannot happen, we use an existing vector
339                 throw new OtsRuntimeException("An object was null.", exception);
340             }
341         }
342         putDemandVector(origin, destination, category, demandScaled, timeVector, interpolation);
343     }
344 
345     /**
346      * Add a demand vector to OD, by a fraction per time period of total demand.
347      * @param origin origin
348      * @param destination destination
349      * @param category category
350      * @param demand demand data, length has to be equal to the time vector
351      * @param timeVector time vector
352      * @param interpolation interpolation
353      * @param fraction fraction of demand for this category
354      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
355      * @throws IllegalArgumentException if the category does not belong to the categorization
356      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
357      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
358      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
359      * @throws NullPointerException if an input is null
360      */
361     public final void putDemandVector(final Node origin, final Node destination, final Category category,
362             final FrequencyVector demand, final DurationVector timeVector, final Interpolation interpolation,
363             final double[] fraction)
364     {
365         Throw.whenNull(demand, "Demand data may not be null.");
366         Throw.whenNull(fraction, "Fraction data may not be null.");
367         Throw.whenNull(timeVector, "Time vector may not be null.");
368         Throw.when(demand.size() != timeVector.size() || timeVector.size() != fraction.length, IllegalArgumentException.class,
369                 "Arrays are of unequal length: demand=%d, DurationVector=%d, fraction=%d", demand.size(), timeVector.size(),
370                 fraction.length);
371         double[] in = demand.getValuesInUnit();
372         double[] scaled = new double[in.length];
373         for (int i = 0; i < in.length; i++)
374         {
375             scaled[i] = in[i] * fraction[i];
376         }
377         FrequencyVector demandScaled;
378         try
379         {
380             demandScaled = new FrequencyVector(scaled, demand.getDisplayUnit(), demand.getStorageType());
381         }
382         catch (ValueRuntimeException exception)
383         {
384             // cannot happen, we use an existing vector
385             throw new OtsRuntimeException("An object was null.", exception);
386         }
387         putDemandVector(origin, destination, category, demandScaled, timeVector, interpolation);
388     }
389 
390     /**
391      * Returns demand data for given origin, destination and categorization.
392      * @param origin origin
393      * @param destination destination
394      * @param category category
395      * @return demand data for given origin, destination and categorization, empty if no data is given
396      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
397      * @throws IllegalArgumentException if the category does not belong to the categorization
398      * @throws NullPointerException if an input is null
399      */
400     public final Optional<FrequencyVector> getDemandVector(final Node origin, final Node destination, final Category category)
401     {
402         Optional<DemandPattern> demandPattern = getDemandPattern(origin, destination, category);
403         if (demandPattern.isEmpty())
404         {
405             return Optional.empty();
406         }
407         return Optional.of(demandPattern.get().demandVector());
408     }
409 
410     /**
411      * Returns interpolation for given origin, destination and categorization.
412      * @param origin origin
413      * @param destination destination
414      * @param category category
415      * @return interpolation for given origin, destination and categorization, empty if no data is given
416      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
417      * @throws IllegalArgumentException if the category does not belong to the categorization
418      * @throws NullPointerException if an input is null
419      */
420     public final Optional<DurationVector> getDurationVector(final Node origin, final Node destination, final Category category)
421     {
422         Optional<DemandPattern> demandPattern = getDemandPattern(origin, destination, category);
423         if (demandPattern.isEmpty())
424         {
425             return Optional.empty();
426         }
427         return Optional.of(demandPattern.get().timeVector());
428     }
429 
430     /**
431      * Returns interpolation for given origin, destination and categorization.
432      * @param origin origin
433      * @param destination destination
434      * @param category category
435      * @return interpolation for given origin, destination and categorization, empty if no data is given
436      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
437      * @throws IllegalArgumentException if the category does not belong to the categorization
438      * @throws NullPointerException if an input is null
439      */
440     public final Optional<Interpolation> getInterpolation(final Node origin, final Node destination, final Category category)
441     {
442         Optional<DemandPattern> demandPattern = getDemandPattern(origin, destination, category);
443         if (demandPattern.isEmpty())
444         {
445             return Optional.empty();
446         }
447         return Optional.of(demandPattern.get().interpolation());
448     }
449 
450     /**
451      * Returns the demand at given time. If given time is before the first time slice or after the last time slice, 0 demand is
452      * returned.
453      * @param origin origin
454      * @param destination destination
455      * @param category category
456      * @param time time
457      * @param sliceStart whether the time is at the start of an arbitrary time slice
458      * @return demand for given origin, destination and categorization, at given time
459      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
460      * @throws IllegalArgumentException if the category does not belong to the categorization
461      * @throws NullPointerException if an input is null
462      */
463     public final Frequency getDemand(final Node origin, final Node destination, final Category category, final Time time,
464             final boolean sliceStart)
465     {
466         Throw.whenNull(time, "Time may not be null.");
467         Optional<DemandPattern> demandPattern = getDemandPattern(origin, destination, category);
468         if (demandPattern.isEmpty())
469         {
470             return new Frequency(0.0, FrequencyUnit.PER_HOUR); // Frequency.ZERO gives "Hz" which is not nice for flow
471         }
472         return demandPattern.get().getFrequency(Duration.ofSI(time.si), sliceStart);
473     }
474 
475     /**
476      * Returns OD entry for given origin, destination and categorization.
477      * @param origin origin
478      * @param destination destination
479      * @param category category
480      * @return OD entry for given origin, destination and categorization.
481      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
482      * @throws IllegalArgumentException if the category does not belong to the categorization
483      * @throws NullPointerException if an input is null
484      */
485     public Optional<DemandPattern> getDemandPattern(final Node origin, final Node destination, final Category category)
486     {
487         Throw.whenNull(origin, "Origin may not be null.");
488         Throw.whenNull(destination, "Destination may not be null.");
489         Throw.whenNull(category, "Category may not be null.");
490         Throw.when(!this.origins.contains(origin), IllegalArgumentException.class, "Origin '%s' is not part of the OD matrix",
491                 origin);
492         Throw.when(!this.destinations.contains(destination), IllegalArgumentException.class,
493                 "Destination '%s' is not part of the OD matrix.", destination);
494         Throw.when(!this.categorization.equals(category.getCategorization()), IllegalArgumentException.class,
495                 "Provided category %s does not belong to the categorization %s.", category, this.categorization);
496         return Optional.ofNullable(this.demandData.get(origin).get(destination).get(category));
497     }
498 
499     /**
500      * Returns whether there is data for the specified origin, destination and category.
501      * @param origin origin
502      * @param destination destination
503      * @param category category
504      * @return whether there is data for the specified origin, destination and category
505      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
506      * @throws IllegalArgumentException if the category does not belong to the categorization
507      * @throws NullPointerException if an input is null
508      */
509     public final boolean contains(final Node origin, final Node destination, final Category category)
510     {
511         return getDemandPattern(origin, destination, category) != null;
512     }
513 
514     /**
515      * Returns the categories specified for given origin-destination combination.
516      * @param origin origin
517      * @param destination destination
518      * @return categories specified for given origin-destination combination
519      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
520      * @throws NullPointerException if an input is null
521      */
522     public final Set<Category> getCategories(final Node origin, final Node destination)
523     {
524         Throw.whenNull(origin, "Origin may not be null.");
525         Throw.whenNull(destination, "Destination may not be null.");
526         Throw.when(!this.origins.contains(origin), IllegalArgumentException.class, "Origin '%s' is not part of the OD matrix",
527                 origin);
528         Throw.when(!this.destinations.contains(destination), IllegalArgumentException.class,
529                 "Destination '%s' is not part of the OD matrix.", destination);
530         return new LinkedHashSet<>(this.demandData.get(origin).get(destination).keySet());
531     }
532 
533     /******************************************************************************************************/
534     /****************************************** TRIP METHODS **********************************************/
535     /******************************************************************************************************/
536 
537     /**
538      * Put trip vector.
539      * @param origin origin
540      * @param destination destination
541      * @param category category
542      * @param trips trip data, length has to be equal to the global time vector - 1, each value is the number of trips during a
543      *            period
544      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
545      * @throws IllegalArgumentException if the category does not belong to the categorization
546      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
547      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
548      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
549      * @throws NullPointerException if an input is null
550      */
551     public final void putTripsVector(final Node origin, final Node destination, final Category category, final int[] trips)
552     {
553         putTripsVector(origin, destination, category, trips, getGlobalTimeVector());
554     }
555 
556     /**
557      * Sets demand data by number of trips. Interpolation over time is stepwise.
558      * @param origin origin
559      * @param destination destination
560      * @param category category
561      * @param trips trip data, length has to be equal to the time vector - 1, each value is the number of trips during a period
562      * @param timeVector time vector
563      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
564      * @throws IllegalArgumentException if the category does not belong to the categorization
565      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
566      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
567      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
568      * @throws NullPointerException if an input is null
569      */
570     public final void putTripsVector(final Node origin, final Node destination, final Category category, final int[] trips,
571             final DurationVector timeVector)
572     {
573         // this is what we need here, other checks in putDemandVector
574         Throw.whenNull(trips, "Demand data may not be null.");
575         Throw.whenNull(timeVector, "Time vector may not be null.");
576         Throw.when(trips.length != timeVector.size() - 1, IllegalArgumentException.class,
577                 "Trip data and time data have wrong lengths. Trip data should be 1 shorter than time data.");
578         // convert to flow
579         double[] flow = new double[timeVector.size()];
580         try
581         {
582             for (int i = 0; i < trips.length; i++)
583             {
584                 flow[i] = trips[i]
585                         / (timeVector.get(i + 1).getInUnit(DurationUnit.HOUR) - timeVector.get(i).getInUnit(DurationUnit.HOUR));
586             }
587             // last value can remain zero as initialized
588             putDemandVector(origin, destination, category, new FrequencyVector(flow, FrequencyUnit.PER_HOUR), timeVector,
589                     Interpolation.STEPWISE);
590         }
591         catch (ValueRuntimeException exception)
592         {
593             // should not happen as we check and then loop over the array length
594             throw new OtsRuntimeException("Could not translate trip vector into demand vector.", exception);
595         }
596     }
597 
598     /**
599      * Get trip vector.
600      * @param origin origin
601      * @param destination destination
602      * @param category category
603      * @return trip data for given origin, destination and categorization, {@code null} if no data is given
604      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
605      * @throws IllegalArgumentException if the category does not belong to the categorization
606      * @throws NullPointerException if an input is null
607      */
608     public final int[] getTripsVector(final Node origin, final Node destination, final Category category)
609     {
610         Optional<FrequencyVector> demand = getDemandVector(origin, destination, category);
611         if (demand.isEmpty())
612         {
613             return null;
614         }
615         int[] trips = new int[demand.get().size() - 1];
616         DurationVector time = getDurationVector(origin, destination, category).get();
617         Interpolation interpolation = getInterpolation(origin, destination, category).get();
618         for (int i = 0; i < trips.length; i++)
619         {
620             try
621             {
622                 trips[i] = interpolation.integrate(demand.get().get(i), time.get(i), demand.get().get(i + 1), time.get(i + 1));
623             }
624             catch (ValueRuntimeException exception)
625             {
626                 // should not happen as we loop over the array length
627                 throw new OtsRuntimeException("Could not translate demand vector into trip vector.", exception);
628             }
629         }
630         return trips;
631     }
632 
633     /**
634      * Returns the number of trips in the given time period.
635      * @param origin origin
636      * @param destination destination
637      * @param category category
638      * @param periodIndex index of time period
639      * @return demand for given origin, destination and categorization, at given time
640      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
641      * @throws IllegalArgumentException if the category does not belong to the categorization
642      * @throws IndexOutOfBoundsException if the period is outside of the specified range
643      * @throws NullPointerException if an input is null
644      */
645     public final int getTrips(final Node origin, final Node destination, final Category category, final int periodIndex)
646     {
647         Optional<DurationVector> time = getDurationVector(origin, destination, category);
648         if (time.isEmpty())
649         {
650             return 0;
651         }
652         Throw.when(periodIndex < 0 || periodIndex >= time.get().size() - 1, IndexOutOfBoundsException.class,
653                 "Period index out of range.");
654         FrequencyVector demand = getDemandVector(origin, destination, category).get();
655         Interpolation interpolation = getInterpolation(origin, destination, category).get();
656         try
657         {
658             return interpolation.integrate(demand.get(periodIndex), time.get().get(periodIndex), demand.get(periodIndex + 1),
659                     time.get().get(periodIndex + 1));
660         }
661         catch (ValueRuntimeException exception)
662         {
663             // should not happen as the index was checked
664             throw new OtsRuntimeException("Could not get number of trips.", exception);
665         }
666     }
667 
668     /**
669      * Adds a number of trips to given origin-destination combination, category and time period. This can only be done for data
670      * with stepwise interpolation.
671      * @param origin origin
672      * @param destination destination
673      * @param category category
674      * @param periodIndex index of time period
675      * @param trips trips to add (may be negative)
676      * @throws NoSuchElementException if there is no demand data to increase
677      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
678      * @throws IllegalArgumentException if the category does not belong to the categorization
679      * @throws IndexOutOfBoundsException if the period is outside of the specified range
680      * @throws UnsupportedOperationException if the interpolation of the data is not stepwise, or demand becomes negative
681      * @throws NullPointerException if an input is null
682      */
683     public final void increaseTrips(final Node origin, final Node destination, final Category category, final int periodIndex,
684             final int trips)
685     {
686         Interpolation interpolation = getInterpolation(origin, destination, category)
687                 .orElseThrow(() -> new NoSuchElementException("No data to increase for OD " + origin.getId() + ", "
688                         + destination.getId() + ", category " + category));
689         Throw.when(!interpolation.equals(Interpolation.STEPWISE), UnsupportedOperationException.class,
690                 "Can only increase the number of trips for data with stepwise interpolation.");
691         DurationVector time = getDurationVector(origin, destination, category).get();
692         Throw.when(periodIndex < 0 || periodIndex >= time.size() - 1, IndexOutOfBoundsException.class,
693                 "Period index out of range.");
694         FrequencyVector demand = getDemandVector(origin, destination, category).get();
695         try
696         {
697             double additionalDemand = trips / (time.get(periodIndex + 1).getInUnit(DurationUnit.HOUR)
698                     - time.get(periodIndex).getInUnit(DurationUnit.HOUR));
699             double[] dem = demand.getValuesInUnit(FrequencyUnit.PER_HOUR);
700             Throw.when(dem[periodIndex] < -additionalDemand, UnsupportedOperationException.class,
701                     "Demand may not become negative.");
702             dem[periodIndex] += additionalDemand;
703             DurationVector timeVector = new DurationVector(time.getValuesInUnit());
704             putDemandVector(origin, destination, category, new FrequencyVector(dem, FrequencyUnit.PER_HOUR), timeVector,
705                     Interpolation.STEPWISE);
706         }
707         catch (ValueRuntimeException exception)
708         {
709             // should not happen as the index was checked
710             throw new OtsRuntimeException("Unexpected exception while getting number of trips.", exception);
711         }
712     }
713 
714     /**
715      * Calculates total number of trips over time for given origin.
716      * @param origin origin
717      * @return total number of trips over time for given origin
718      * @throws IllegalArgumentException if origin is not part of the OD matrix
719      * @throws NullPointerException if origin is null
720      */
721     public final int originTotal(final Node origin)
722     {
723         int sum = 0;
724         for (Node destination : getDestinations())
725         {
726             sum += originDestinationTotal(origin, destination);
727         }
728         return sum;
729     }
730 
731     /**
732      * Calculates total number of trips over time for given destination.
733      * @param destination destination
734      * @return total number of trips over time for given destination
735      * @throws IllegalArgumentException if destination is not part of the OD matrix
736      * @throws NullPointerException if destination is null
737      */
738     public final int destinationTotal(final Node destination)
739     {
740         int sum = 0;
741         for (Node origin : getOrigins())
742         {
743             sum += originDestinationTotal(origin, destination);
744         }
745         return sum;
746     }
747 
748     /**
749      * Calculates total number of trips over time for the complete matrix.
750      * @return total number of trips over time for the complete matrix
751      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
752      * @throws NullPointerException if an input is null
753      */
754     public final int matrixTotal()
755     {
756         int sum = 0;
757         for (Node origin : getOrigins())
758         {
759             for (Node destination : getDestinations())
760             {
761                 sum += originDestinationTotal(origin, destination);
762             }
763         }
764         return sum;
765     }
766 
767     /**
768      * Calculates total number of trips over time for given origin-destination combination.
769      * @param origin origin
770      * @param destination destination
771      * @return total number of trips over time for given origin-destination combination
772      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
773      * @throws NullPointerException if an input is null
774      */
775     public final int originDestinationTotal(final Node origin, final Node destination)
776     {
777         int sum = 0;
778         for (Category category : getCategories(origin, destination))
779         {
780             Optional<Interpolation> interpolation = getInterpolation(origin, destination, category);
781             if (interpolation.isPresent())
782             {
783                 DurationVector time = getDurationVector(origin, destination, category).get();
784                 FrequencyVector demand = getDemandVector(origin, destination, category).get();
785                 for (int i = 0; i < time.size() - 1; i++)
786                 {
787                     try
788                     {
789                         sum += interpolation.get().integrate(demand.get(i), time.get(i), demand.get(i + 1), time.get(i + 1));
790                     }
791                     catch (ValueRuntimeException exception)
792                     {
793                         // should not happen as we loop over the array length
794                         throw new OtsRuntimeException("Unexcepted exception while determining total trips over time.",
795                                 exception);
796                     }
797                 }
798             }
799         }
800         return sum;
801     }
802 
803     /******************************************************************************************************/
804     /****************************************** OTHER METHODS *********************************************/
805     /******************************************************************************************************/
806 
807     @Override
808     @SuppressWarnings("checkstyle:designforextension")
809     public String toString()
810     {
811         return "OdMatrix [" + this.id + ", " + this.origins.size() + " origins, " + this.destinations.size() + " destinations, "
812                 + this.categorization + " ]";
813     }
814 
815     /**
816      * Prints the complete OD matrix with each demand data on a single line.
817      */
818     public final void print()
819     {
820         int originLength = 0;
821         for (Node origin : this.origins)
822         {
823             originLength = originLength >= origin.getId().length() ? originLength : origin.getId().length();
824         }
825         int destinLength = 0;
826         for (Node destination : this.destinations)
827         {
828             destinLength = destinLength >= destination.getId().length() ? destinLength : destination.getId().length();
829         }
830         String format = "%-" + Math.max(originLength, 1) + "s -> %-" + Math.max(destinLength, 1) + "s | ";
831         for (Node origin : this.origins)
832         {
833             Map<Node, Map<Category, DemandPattern>> destinationMap = this.demandData.get(origin);
834             for (Node destination : this.destinations)
835             {
836                 Map<Category, DemandPattern> categoryMap = destinationMap.get(destination);
837                 if (categoryMap.isEmpty())
838                 {
839                     System.out.println(String.format(format, origin.getId(), destination.getId()) + "-no data-");
840                 }
841                 else
842                 {
843                     for (Category category : categoryMap.keySet())
844                     {
845                         StringBuilder catStr = new StringBuilder("[");
846                         String sep = "";
847                         for (int i = 0; i < category.getCategorization().size(); i++)
848                         {
849                             catStr.append(sep);
850                             Object obj = category.get(i);
851                             if (obj instanceof Route)
852                             {
853                                 catStr.append("Route: " + ((Route) obj).getId());
854                             }
855                             else
856                             {
857                                 catStr.append(obj);
858                             }
859                             sep = ", ";
860                         }
861                         catStr.append("]");
862                         System.out.println(String.format(format, origin.getId(), destination.getId()) + catStr + " | "
863                                 + categoryMap.get(category).demandVector());
864                     }
865                 }
866             }
867         }
868     }
869 
870     @Override
871     public final int hashCode()
872     {
873         final int prime = 31;
874         int result = 1;
875         result = prime * result + ((this.categorization == null) ? 0 : this.categorization.hashCode());
876         result = prime * result + ((this.demandData == null) ? 0 : this.demandData.hashCode());
877         result = prime * result + ((this.destinations == null) ? 0 : this.destinations.hashCode());
878         result = prime * result + ((this.globalInterpolation == null) ? 0 : this.globalInterpolation.hashCode());
879         result = prime * result + ((this.globalTimeVector == null) ? 0 : this.globalTimeVector.hashCode());
880         result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
881         result = prime * result + ((this.origins == null) ? 0 : this.origins.hashCode());
882         return result;
883     }
884 
885     @Override
886     public final boolean equals(final Object obj)
887     {
888         if (this == obj)
889         {
890             return true;
891         }
892         if (obj == null)
893         {
894             return false;
895         }
896         if (getClass() != obj.getClass())
897         {
898             return false;
899         }
900         OdMatrix other = (OdMatrix) obj;
901         if (this.categorization == null)
902         {
903             if (other.categorization != null)
904             {
905                 return false;
906             }
907         }
908         else if (!this.categorization.equals(other.categorization))
909         {
910             return false;
911         }
912         if (this.demandData == null)
913         {
914             if (other.demandData != null)
915             {
916                 return false;
917             }
918         }
919         else if (!this.demandData.equals(other.demandData))
920         {
921             return false;
922         }
923         if (this.destinations == null)
924         {
925             if (other.destinations != null)
926             {
927                 return false;
928             }
929         }
930         else if (!this.destinations.equals(other.destinations))
931         {
932             return false;
933         }
934         if (this.globalInterpolation != other.globalInterpolation)
935         {
936             return false;
937         }
938         if (this.globalTimeVector == null)
939         {
940             if (other.globalTimeVector != null)
941             {
942                 return false;
943             }
944         }
945         else if (!this.globalTimeVector.equals(other.globalTimeVector))
946         {
947             return false;
948         }
949         if (this.id == null)
950         {
951             if (other.id != null)
952             {
953                 return false;
954             }
955         }
956         else if (!this.id.equals(other.id))
957         {
958             return false;
959         }
960         if (this.origins == null)
961         {
962             if (other.origins != null)
963             {
964                 return false;
965             }
966         }
967         else if (!this.origins.equals(other.origins))
968         {
969             return false;
970         }
971         return true;
972     }
973 
974 }