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
99
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
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
207
208
209
210
211
212
213
214
215
216
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241 public final void putDemandVector(final Node origin, final Node destination, final Category category,
242 final FrequencyVector demand, final TimeVector 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 Time prevTime;
263 try
264 {
265 prevTime = timeVector.get(0).eq0() ? Time.createSI(-1.0) : Time.ZERO;
266 }
267 catch (ValueException exception)
268 {
269
270 throw new RuntimeException("Unexpected exception while checking time vector.", exception);
271 }
272 for (Time 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 DemandPattern demandPattern = new DemandPattern(demand, timeVector, interpolation);
294 this.demandData.get(origin).get(destination).put(category, demandPattern);
295 }
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313 public final void putDemandVector(final Node origin, final Node destination, final Category category,
314 final FrequencyVector demand, final TimeVector timeVector, final Interpolation interpolation, final double fraction)
315 {
316 Throw.whenNull(demand, "Demand data may not be null.");
317 FrequencyVector demandScaled;
318 if (fraction == 1.0)
319 {
320 demandScaled = demand;
321 }
322 else
323 {
324 double[] in = demand.getValuesInUnit();
325 double[] scaled = new double[in.length];
326 for (int i = 0; i < in.length; i++)
327 {
328 scaled[i] = in[i] * fraction;
329 }
330 try
331 {
332 demandScaled = new FrequencyVector(scaled, demand.getUnit(), demand.getStorageType());
333 }
334 catch (ValueException exception)
335 {
336
337 throw new RuntimeException("An object was null.", exception);
338 }
339 }
340 putDemandVector(origin, destination, category, demandScaled, timeVector, interpolation);
341 }
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359 public final void putDemandVector(final Node origin, final Node destination, final Category category,
360 final FrequencyVector demand, final TimeVector timeVector, final Interpolation interpolation,
361 final double[] fraction)
362 {
363 Throw.whenNull(demand, "Demand data may not be null.");
364 Throw.whenNull(fraction, "Fraction data may not be null.");
365 Throw.whenNull(timeVector, "Time vector may not be null.");
366 Throw.when(demand.size() != timeVector.size() || timeVector.size() != fraction.length, IllegalArgumentException.class,
367 "Arrays are of unequal length: demand=%d, timeVector=%d, fraction=%d", demand.size(), timeVector.size(),
368 fraction.length);
369 double[] in = demand.getValuesInUnit();
370 double[] scaled = new double[in.length];
371 for (int i = 0; i < in.length; i++)
372 {
373 scaled[i] = in[i] * fraction[i];
374 }
375 FrequencyVector demandScaled;
376 try
377 {
378 demandScaled = new FrequencyVector(scaled, demand.getUnit(), demand.getStorageType());
379 }
380 catch (ValueException exception)
381 {
382
383 throw new RuntimeException("An object was null.", exception);
384 }
385 putDemandVector(origin, destination, category, demandScaled, timeVector, interpolation);
386 }
387
388
389
390
391
392
393
394
395
396
397 public final FrequencyVector getDemandVector(final Node origin, final Node destination, final Category category)
398 {
399 DemandPattern demandPattern = getDemandPattern(origin, destination, category);
400 if (demandPattern == null)
401 {
402 return null;
403 }
404 return demandPattern.getDemandVector();
405 }
406
407
408
409
410
411
412
413
414
415
416 public final TimeVector getTimeVector(final Node origin, final Node destination, final Category category)
417 {
418 DemandPattern demandPattern = getDemandPattern(origin, destination, category);
419 if (demandPattern == null)
420 {
421 return null;
422 }
423 return demandPattern.getTimeVector();
424 }
425
426
427
428
429
430
431
432
433
434
435 public final Interpolation getInterpolation(final Node origin, final Node destination, final Category category)
436 {
437 DemandPattern demandPattern = getDemandPattern(origin, destination, category);
438 if (demandPattern == null)
439 {
440 return null;
441 }
442 return demandPattern.getInterpolation();
443 }
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458 public final Frequency getDemand(final Node origin, final Node destination, final Category category, final Time time,
459 final boolean sliceStart)
460 {
461 Throw.whenNull(time, "Time may not be null.");
462 DemandPattern demandPattern = getDemandPattern(origin, destination, category);
463 if (demandPattern == null)
464 {
465 return new Frequency(0.0, FrequencyUnit.PER_HOUR);
466 }
467 return demandPattern.getFrequency(time, sliceStart);
468 }
469
470
471
472
473
474
475
476
477
478
479 public DemandPattern getDemandPattern(final Node origin, final Node destination, final Category category)
480 {
481 Throw.whenNull(origin, "Origin may not be null.");
482 Throw.whenNull(destination, "Destination may not be null.");
483 Throw.whenNull(category, "Category may not be null.");
484 Throw.when(!this.origins.contains(origin), IllegalArgumentException.class, "Origin '%s' is not part of the OD matrix",
485 origin);
486 Throw.when(!this.destinations.contains(destination), IllegalArgumentException.class,
487 "Destination '%s' is not part of the OD matrix.", destination);
488 Throw.when(!this.categorization.equals(category.getCategorization()), IllegalArgumentException.class,
489 "Provided category %s does not belong to the categorization %s.", category, this.categorization);
490 return this.demandData.get(origin).get(destination).get(category);
491 }
492
493
494
495
496
497
498
499
500
501
502 public final boolean contains(final Node origin, final Node destination, final Category category)
503 {
504 return getDemandPattern(origin, destination, category) != null;
505 }
506
507
508
509
510
511
512
513
514
515 public final Set<Category> getCategories(final Node origin, final Node destination)
516 {
517 Throw.whenNull(origin, "Origin may not be null.");
518 Throw.whenNull(destination, "Destination may not be null.");
519 Throw.when(!this.origins.contains(origin), IllegalArgumentException.class, "Origin '%s' is not part of the OD matrix",
520 origin);
521 Throw.when(!this.destinations.contains(destination), IllegalArgumentException.class,
522 "Destination '%s' is not part of the OD matrix.", destination);
523 return new LinkedHashSet<>(this.demandData.get(origin).get(destination).keySet());
524 }
525
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 {
545 putTripsVector(origin, destination, category, trips, getGlobalTimeVector());
546 }
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563 public final void putTripsVector(final Node origin, final Node destination, final Category category, final int[] trips,
564 final TimeVector timeVector)
565 {
566
567 Throw.whenNull(trips, "Demand data may not be null.");
568 Throw.whenNull(timeVector, "Time vector may not be null.");
569 Throw.when(trips.length != timeVector.size() - 1, IllegalArgumentException.class,
570 "Trip data and time data have wrong lengths. Trip data should be 1 shorter than time data.");
571
572 double[] flow = new double[timeVector.size()];
573 try
574 {
575 for (int i = 0; i < trips.length; i++)
576 {
577 flow[i] = trips[i] / (timeVector.get(i + 1).getInUnit(TimeUnit.BASE_HOUR)
578 - timeVector.get(i).getInUnit(TimeUnit.BASE_HOUR));
579 }
580
581 putDemandVector(origin, destination, category, new FrequencyVector(flow, FrequencyUnit.PER_HOUR, StorageType.DENSE),
582 timeVector, Interpolation.STEPWISE);
583 }
584 catch (ValueException exception)
585 {
586
587 throw new RuntimeException("Could not translate trip vector into demand vector.", exception);
588 }
589 }
590
591
592
593
594
595
596
597
598
599
600 public final int[] getTripsVector(final Node origin, final Node destination, final Category category)
601 {
602 FrequencyVector demand = getDemandVector(origin, destination, category);
603 if (demand == null)
604 {
605 return null;
606 }
607 int[] trips = new int[demand.size() - 1];
608 TimeVector time = getTimeVector(origin, destination, category);
609 Interpolation interpolation = getInterpolation(origin, destination, category);
610 for (int i = 0; i < trips.length; i++)
611 {
612 try
613 {
614 trips[i] = interpolation.integrate(demand.get(i), time.get(i), demand.get(i + 1), time.get(i + 1));
615 }
616 catch (ValueException exception)
617 {
618
619 throw new RuntimeException("Could not translate demand vector into trip vector.", exception);
620 }
621 }
622 return trips;
623 }
624
625
626
627
628
629
630
631
632
633
634
635
636
637 public final int getTrips(final Node origin, final Node destination, final Category category, final int periodIndex)
638 {
639 TimeVector time = getTimeVector(origin, destination, category);
640 if (time == null)
641 {
642 return 0;
643 }
644 Throw.when(periodIndex < 0 || periodIndex >= time.size() - 1, IndexOutOfBoundsException.class,
645 "Period index out of range.");
646 FrequencyVector demand = getDemandVector(origin, destination, category);
647 Interpolation interpolation = getInterpolation(origin, destination, category);
648 try
649 {
650 return interpolation.integrate(demand.get(periodIndex), time.get(periodIndex), demand.get(periodIndex + 1),
651 time.get(periodIndex + 1));
652 }
653 catch (ValueException exception)
654 {
655
656 throw new RuntimeException("Could not get number of trips.", exception);
657 }
658 }
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674 public final void increaseTrips(final Node origin, final Node destination, final Category category, final int periodIndex,
675 final int trips)
676 {
677 Interpolation interpolation = getInterpolation(origin, destination, category);
678 Throw.when(!interpolation.equals(Interpolation.STEPWISE), UnsupportedOperationException.class,
679 "Can only increase the number of trips for data with stepwise interpolation.");
680 TimeVector time = getTimeVector(origin, destination, category);
681 Throw.when(periodIndex < 0 || periodIndex >= time.size() - 1, IndexOutOfBoundsException.class,
682 "Period index out of range.");
683 FrequencyVector demand = getDemandVector(origin, destination, category);
684 try
685 {
686 double additionalDemand = trips / (time.get(periodIndex + 1).getInUnit(TimeUnit.BASE_HOUR)
687 - time.get(periodIndex).getInUnit(TimeUnit.BASE_HOUR));
688 double[] dem = demand.getValuesInUnit(FrequencyUnit.PER_HOUR);
689 Throw.when(dem[periodIndex] < -additionalDemand, UnsupportedOperationException.class,
690 "Demand may not become negative.");
691 dem[periodIndex] += additionalDemand;
692 putDemandVector(origin, destination, category, new FrequencyVector(dem, FrequencyUnit.PER_HOUR, StorageType.DENSE),
693 time, Interpolation.STEPWISE);
694 }
695 catch (ValueException exception)
696 {
697
698 throw new RuntimeException("Unexpected exception while getting number of trips.", exception);
699 }
700 }
701
702
703
704
705
706
707
708
709 public final int originTotal(final Node origin)
710 {
711 int sum = 0;
712 for (Node destination : getDestinations())
713 {
714 sum += originDestinationTotal(origin, destination);
715 }
716 return sum;
717 }
718
719
720
721
722
723
724
725
726 public final int destinationTotal(final Node destination)
727 {
728 int sum = 0;
729 for (Node origin : getOrigins())
730 {
731 sum += originDestinationTotal(origin, destination);
732 }
733 return sum;
734 }
735
736
737
738
739
740
741
742 public final int matrixTotal()
743 {
744 int sum = 0;
745 for (Node origin : getOrigins())
746 {
747 for (Node destination : getDestinations())
748 {
749 sum += originDestinationTotal(origin, destination);
750 }
751 }
752 return sum;
753 }
754
755
756
757
758
759
760
761
762
763 public final int originDestinationTotal(final Node origin, final Node destination)
764 {
765 int sum = 0;
766 for (Category category : getCategories(origin, destination))
767 {
768 TimeVector time = getTimeVector(origin, destination, category);
769 FrequencyVector demand = getDemandVector(origin, destination, category);
770 Interpolation interpolation = getInterpolation(origin, destination, category);
771 for (int i = 0; i < time.size() - 1; i++)
772 {
773 try
774 {
775 sum += interpolation.integrate(demand.get(i), time.get(i), demand.get(i + 1), time.get(i + 1));
776 }
777 catch (ValueException exception)
778 {
779
780 throw new RuntimeException("Unexcepted exception while determining total trips over time.", exception);
781 }
782 }
783 }
784 return sum;
785 }
786
787
788
789
790
791
792 @Override
793 @SuppressWarnings("checkstyle:designforextension")
794 public String toString()
795 {
796 return "ODMatrix [" + this.id + ", " + this.origins.size() + " origins, " + this.destinations.size() + " destinations, "
797 + this.categorization + " ]";
798 }
799
800
801
802
803 public final void print()
804 {
805 int originLength = 0;
806 for (Node origin : this.origins)
807 {
808 originLength = originLength >= origin.getId().length() ? originLength : origin.getId().length();
809 }
810 int destinLength = 0;
811 for (Node destination : this.destinations)
812 {
813 destinLength = destinLength >= destination.getId().length() ? destinLength : destination.getId().length();
814 }
815 String format = "%-" + Math.max(originLength, 1) + "s -> %-" + Math.max(destinLength, 1) + "s | ";
816 for (Node origin : this.origins)
817 {
818 Map<Node, Map<Category, DemandPattern>> destinationMap = this.demandData.get(origin);
819 for (Node destination : this.destinations)
820 {
821 Map<Category, DemandPattern> categoryMap = destinationMap.get(destination);
822 if (categoryMap.isEmpty())
823 {
824 System.out.println(String.format(format, origin.getId(), destination.getId()) + "-no data-");
825 }
826 else
827 {
828 for (Category category : categoryMap.keySet())
829 {
830 StringBuilder catStr = new StringBuilder("[");
831 String sep = "";
832 for (int i = 0; i < category.getCategorization().size(); i++)
833 {
834 catStr.append(sep);
835 Object obj = category.get(i);
836 if (obj instanceof Route)
837 {
838 catStr.append("Route: " + ((Route) obj).getId());
839 }
840 else
841 {
842 catStr.append(obj);
843 }
844 sep = ", ";
845 }
846 catStr.append("]");
847
848 System.out.println(String.format(format, origin.getId(), destination.getId()) + catStr + " | "
849 + categoryMap.get(category).getDemandVector());
850 }
851 }
852 }
853 }
854 }
855
856
857 @Override
858 public final int hashCode()
859 {
860 final int prime = 31;
861 int result = 1;
862 result = prime * result + ((this.categorization == null) ? 0 : this.categorization.hashCode());
863 result = prime * result + ((this.demandData == null) ? 0 : this.demandData.hashCode());
864 result = prime * result + ((this.destinations == null) ? 0 : this.destinations.hashCode());
865 result = prime * result + ((this.globalInterpolation == null) ? 0 : this.globalInterpolation.hashCode());
866 result = prime * result + ((this.globalTimeVector == null) ? 0 : this.globalTimeVector.hashCode());
867 result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
868 result = prime * result + ((this.origins == null) ? 0 : this.origins.hashCode());
869 return result;
870 }
871
872
873 @Override
874 public final boolean equals(final Object obj)
875 {
876 if (this == obj)
877 {
878 return true;
879 }
880 if (obj == null)
881 {
882 return false;
883 }
884 if (getClass() != obj.getClass())
885 {
886 return false;
887 }
888 ODMatrix other = (ODMatrix) obj;
889 if (this.categorization == null)
890 {
891 if (other.categorization != null)
892 {
893 return false;
894 }
895 }
896 else if (!this.categorization.equals(other.categorization))
897 {
898 return false;
899 }
900 if (this.demandData == null)
901 {
902 if (other.demandData != null)
903 {
904 return false;
905 }
906 }
907 else if (!this.demandData.equals(other.demandData))
908 {
909 return false;
910 }
911 if (this.destinations == null)
912 {
913 if (other.destinations != null)
914 {
915 return false;
916 }
917 }
918 else if (!this.destinations.equals(other.destinations))
919 {
920 return false;
921 }
922 if (this.globalInterpolation != other.globalInterpolation)
923 {
924 return false;
925 }
926 if (this.globalTimeVector == null)
927 {
928 if (other.globalTimeVector != null)
929 {
930 return false;
931 }
932 }
933 else if (!this.globalTimeVector.equals(other.globalTimeVector))
934 {
935 return false;
936 }
937 if (this.id == null)
938 {
939 if (other.id != null)
940 {
941 return false;
942 }
943 }
944 else if (!this.id.equals(other.id))
945 {
946 return false;
947 }
948 if (this.origins == null)
949 {
950 if (other.origins != null)
951 {
952 return false;
953 }
954 }
955 else if (!this.origins.equals(other.origins))
956 {
957 return false;
958 }
959 return true;
960 }
961
962 }