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