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