From 44b4da19e4f4dbfaa5ca043e33fd8f5944dbf4eb Mon Sep 17 00:00:00 2001 From: nkuehnel Date: Wed, 12 Jun 2024 15:42:38 +0200 Subject: [PATCH] use globally defined csv delimiter more consistently --- ...ftEfficiencyAnalysisControlerListener.java | 7 ++- .../h3/RunDrtWithH3ZonalSystemIT.java | 3 +- .../zonal/DrtModeZonalSystemModule.java | 2 +- .../zonal/DrtZonalWaitTimesAnalyzer.java | 53 +++++++++---------- .../DvrpBenchmarkTravelTimeModule.java | 3 +- .../DvrpOfflineTravelTimeEstimator.java | 20 ++++--- .../DvrpOfflineTravelTimes.java | 27 +++++----- .../DvrpTravelTimeModule.java | 3 +- .../DvrpOfflineTravelTimeEstimatorTest.java | 7 +-- .../DvrpOfflineTravelTimesTest.java | 6 ++- 10 files changed, 71 insertions(+), 60 deletions(-) diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/analysis/efficiency/ShiftEfficiencyAnalysisControlerListener.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/analysis/efficiency/ShiftEfficiencyAnalysisControlerListener.java index 57e451f31b0..9ea39e1a75f 100644 --- a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/analysis/efficiency/ShiftEfficiencyAnalysisControlerListener.java +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/analysis/efficiency/ShiftEfficiencyAnalysisControlerListener.java @@ -43,6 +43,8 @@ public final class ShiftEfficiencyAnalysisControlerListener implements Iteration private final DrtConfigGroup drtConfigGroup; private final ShiftEfficiencyTracker shiftEfficiencyTracker; + private final String delimiter; + @Inject public ShiftEfficiencyAnalysisControlerListener(DrtConfigGroup drtConfigGroup, ShiftEfficiencyTracker shiftEfficiencyTracker, @@ -52,6 +54,7 @@ public ShiftEfficiencyAnalysisControlerListener(DrtConfigGroup drtConfigGroup, this.shiftEfficiencyTracker = shiftEfficiencyTracker; this.drtShiftsSpecification = drtShiftsSpecification; this.matsimServices = matsimServices; + this.delimiter = matsimServices.getConfig().global().getDefaultDelimiter(); } @Override @@ -131,7 +134,7 @@ private String filename(IterationEndsEvent event, String prefix, String extensio .getIterationFilename(event.getIteration(), prefix + "_" + drtConfigGroup.getMode() + extension); } - private static String line(Object... cells) { - return Arrays.stream(cells).map(Object::toString).collect(Collectors.joining(";", "", "\n")); + private String line(Object... cells) { + return Arrays.stream(cells).map(Object::toString).collect(Collectors.joining(delimiter, "", "\n")); } } diff --git a/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/h3/RunDrtWithH3ZonalSystemIT.java b/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/h3/RunDrtWithH3ZonalSystemIT.java index d1ef4e2fea7..c79a868144f 100644 --- a/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/h3/RunDrtWithH3ZonalSystemIT.java +++ b/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/h3/RunDrtWithH3ZonalSystemIT.java @@ -61,13 +61,12 @@ public void install() { public void install() { bindModal(DrtZonalWaitTimesAnalyzer.class).toProvider(modalProvider( getter -> new DrtZonalWaitTimesAnalyzer(drtConfig, getter.getModal(DrtEventSequenceCollector.class), - getter.getModal(ZoneSystem.class)))).asEagerSingleton(); + getter.getModal(ZoneSystem.class), config.global().getDefaultDelimiter()))).asEagerSingleton(); addControlerListenerBinding().to(modalKey(DrtZonalWaitTimesAnalyzer.class)); } }); } }); - } } diff --git a/contribs/drt/src/main/java/org/matsim/contrib/drt/analysis/zonal/DrtModeZonalSystemModule.java b/contribs/drt/src/main/java/org/matsim/contrib/drt/analysis/zonal/DrtModeZonalSystemModule.java index 05129ba7faf..aebb8b44686 100644 --- a/contribs/drt/src/main/java/org/matsim/contrib/drt/analysis/zonal/DrtModeZonalSystemModule.java +++ b/contribs/drt/src/main/java/org/matsim/contrib/drt/analysis/zonal/DrtModeZonalSystemModule.java @@ -90,7 +90,7 @@ public void install() { bindModal(DrtZonalWaitTimesAnalyzer.class).toProvider(modalProvider( getter -> new DrtZonalWaitTimesAnalyzer(drtCfg, getter.getModal(DrtEventSequenceCollector.class), - getter.getModal(ZoneSystem.class)))).asEagerSingleton(); + getter.getModal(ZoneSystem.class), getConfig().global().getDefaultDelimiter()))).asEagerSingleton(); addControlerListenerBinding().to(modalKey(DrtZonalWaitTimesAnalyzer.class)); } } diff --git a/contribs/drt/src/main/java/org/matsim/contrib/drt/analysis/zonal/DrtZonalWaitTimesAnalyzer.java b/contribs/drt/src/main/java/org/matsim/contrib/drt/analysis/zonal/DrtZonalWaitTimesAnalyzer.java index d16a0cd8e0a..ce140ce1772 100644 --- a/contribs/drt/src/main/java/org/matsim/contrib/drt/analysis/zonal/DrtZonalWaitTimesAnalyzer.java +++ b/contribs/drt/src/main/java/org/matsim/contrib/drt/analysis/zonal/DrtZonalWaitTimesAnalyzer.java @@ -58,13 +58,16 @@ public final class DrtZonalWaitTimesAnalyzer implements IterationEndsListener, S private final ZoneSystem zones; private static final Id zoneIdForOutsideOfZonalSystem = Id.create("outsideOfDrtZonalSystem", Zone.class); private static final String notAvailableString = "NaN"; + + private final String delimiter; private static final Logger log = LogManager.getLogger(DrtZonalWaitTimesAnalyzer.class); public DrtZonalWaitTimesAnalyzer(DrtConfigGroup configGroup, DrtEventSequenceCollector requestAnalyzer, - ZoneSystem zones) { + ZoneSystem zones, String delimiter) { this.drtCfg = configGroup; this.requestAnalyzer = requestAnalyzer; this.zones = zones; + this.delimiter = delimiter; } @Override @@ -76,7 +79,6 @@ public void notifyIterationEnds(IterationEndsEvent event) { } public void write(String fileName) { - String delimiter = ";"; Map, DescriptiveStatistics> zoneStats = createZonalStats(); BufferedWriter bw = IOUtils.getBufferedWriter(fileName); try { @@ -85,7 +87,11 @@ public void write(String fileName) { format.setMinimumIntegerDigits(1); format.setMaximumFractionDigits(2); format.setGroupingUsed(false); - bw.append("zone;centerX;centerY;nRequests;sumWaitTime;meanWaitTime;min;max;p95;p90;p80;p75;p50"); + String header = new StringJoiner(delimiter) + .add("zone").add("centerX").add("centerY").add("nRequests") + .add("sumWaitTime").add("meanWaitTime").add("min").add("max") + .add("p95").add("p90").add("p80").add("p75").add("p50").toString(); + bw.append(header); // sorted output SortedSet> zoneIdsAndOutside = new TreeSet<>(zones.getZones().keySet()); zoneIdsAndOutside.add(zoneIdForOutsideOfZonalSystem); @@ -96,31 +102,22 @@ public void write(String fileName) { String centerY = drtZone != null ? String.valueOf(drtZone.getCentroid().getY()) : notAvailableString; DescriptiveStatistics stats = zoneStats.get(zoneId); bw.newLine(); - bw.append(zoneId.toString()) - .append(delimiter) - .append(centerX) - .append(delimiter) - .append(centerY) - .append(delimiter) - .append(format.format(stats.getN())) - .append(delimiter) - .append(format.format(stats.getSum())) - .append(delimiter) - .append(String.valueOf(stats.getMean())) - .append(delimiter) - .append(String.valueOf(stats.getMin())) - .append(delimiter) - .append(String.valueOf(stats.getMax())) - .append(delimiter) - .append(String.valueOf(stats.getPercentile(95))) - .append(delimiter) - .append(String.valueOf(stats.getPercentile(90))) - .append(delimiter) - .append(String.valueOf(stats.getPercentile(80))) - .append(delimiter) - .append(String.valueOf(stats.getPercentile(75))) - .append(delimiter) - .append(String.valueOf(stats.getPercentile(50))); + bw.append( + new StringJoiner(delimiter) + .add(zoneId.toString()) + .add(centerX) + .add(centerY) + .add(format.format(stats.getN())) + .add(format.format(stats.getSum())) + .add(String.valueOf(stats.getMean())) + .add(String.valueOf(stats.getMin())) + .add(String.valueOf(stats.getMax())) + .add(String.valueOf(stats.getPercentile(95))) + .add(String.valueOf(stats.getPercentile(90))) + .add(String.valueOf(stats.getPercentile(80))) + .add(String.valueOf(stats.getPercentile(75))) + .add(String.valueOf(stats.getPercentile(50))).toString() + ); } bw.flush(); bw.close(); diff --git a/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/benchmark/DvrpBenchmarkTravelTimeModule.java b/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/benchmark/DvrpBenchmarkTravelTimeModule.java index 7dad30dbd34..f46302a0d40 100644 --- a/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/benchmark/DvrpBenchmarkTravelTimeModule.java +++ b/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/benchmark/DvrpBenchmarkTravelTimeModule.java @@ -47,7 +47,8 @@ public void install() { addTravelTimeBinding(DvrpTravelTimeModule.DVRP_ESTIMATED).toProvider(() -> { URL url = ConfigGroup.getInputFileURL(getConfig().getContext(), dvrpCfg.initialTravelTimesFile); var timeDiscretizer = new TimeDiscretizer(getConfig().travelTimeCalculator()); - var linkTravelTimes = DvrpOfflineTravelTimes.loadLinkTravelTimes(timeDiscretizer, url); + var linkTravelTimes = DvrpOfflineTravelTimes.loadLinkTravelTimes(timeDiscretizer, url, + getConfig().global().getDefaultDelimiter()); return DvrpOfflineTravelTimes.asTravelTime(timeDiscretizer, linkTravelTimes); }).asEagerSingleton(); } else { diff --git a/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimeEstimator.java b/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimeEstimator.java index 33b22634347..5d80e161462 100644 --- a/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimeEstimator.java +++ b/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimeEstimator.java @@ -28,6 +28,7 @@ import org.matsim.contrib.dvrp.router.DvrpGlobalRoutingNetworkProvider; import org.matsim.contrib.dvrp.run.DvrpConfigGroup; import org.matsim.contrib.common.timeprofile.TimeDiscretizer; +import org.matsim.core.config.groups.GlobalConfigGroup; import org.matsim.core.config.groups.TravelTimeCalculatorConfigGroup; import org.matsim.core.controler.OutputDirectoryHierarchy; import org.matsim.core.controler.events.AfterMobsimEvent; @@ -63,18 +64,21 @@ public class DvrpOfflineTravelTimeEstimator private final double[][] linkTravelTimes; private final double alpha; + private final String delimiter; + @Inject public DvrpOfflineTravelTimeEstimator(@Named(DvrpTravelTimeModule.DVRP_INITIAL) TravelTime initialTT, - @Named(DvrpTravelTimeModule.DVRP_OBSERVED) TravelTime observedTT, - @Named(DvrpGlobalRoutingNetworkProvider.DVRP_ROUTING) Network network, - TravelTimeCalculatorConfigGroup ttCalcConfig, DvrpConfigGroup dvrpConfig, - OutputDirectoryHierarchy outputDirectoryHierarchy) { - this(initialTT, observedTT, network, new TimeDiscretizer(ttCalcConfig), dvrpConfig.travelTimeEstimationAlpha, outputDirectoryHierarchy); + @Named(DvrpTravelTimeModule.DVRP_OBSERVED) TravelTime observedTT, + @Named(DvrpGlobalRoutingNetworkProvider.DVRP_ROUTING) Network network, + TravelTimeCalculatorConfigGroup ttCalcConfig, DvrpConfigGroup dvrpConfig, + OutputDirectoryHierarchy outputDirectoryHierarchy, GlobalConfigGroup globalConfig) { + this(initialTT, observedTT, network, new TimeDiscretizer(ttCalcConfig), dvrpConfig.travelTimeEstimationAlpha, + outputDirectoryHierarchy, globalConfig.getDefaultDelimiter()); } public DvrpOfflineTravelTimeEstimator(TravelTime initialTT, TravelTime observedTT, Network network, TimeDiscretizer timeDiscretizer, double travelTimeEstimationAlpha, - OutputDirectoryHierarchy outputDirectoryHierarchy) { + OutputDirectoryHierarchy outputDirectoryHierarchy, String delimiter) { this.observedTT = observedTT; this.network = network; @@ -83,6 +87,7 @@ public DvrpOfflineTravelTimeEstimator(TravelTime initialTT, TravelTime observedT this.timeInterval = timeDiscretizer.getTimeInterval(); this.outputDirectoryHierarchy = outputDirectoryHierarchy; + this.delimiter = delimiter; alpha = travelTimeEstimationAlpha; checkArgument(alpha > 0 && alpha <= 1, "travelTimeEstimationAlpha must be in (0,1]"); @@ -115,7 +120,8 @@ public void notifyMobsimBeforeCleanup(@SuppressWarnings("rawtypes") MobsimBefore @Override public void notifyAfterMobsim(AfterMobsimEvent event) { DvrpOfflineTravelTimes.saveLinkTravelTimes(timeDiscretizer, linkTravelTimes, - outputDirectoryHierarchy.getIterationFilename(event.getIteration(), "dvrp_travel_times.csv.gz")); + outputDirectoryHierarchy.getIterationFilename(event.getIteration(), + "dvrp_travel_times.csv.gz"), delimiter); } private void updateTTs(TravelTime travelTime, double alpha) { diff --git a/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimes.java b/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimes.java index 67882267db2..d8bc99c89b1 100644 --- a/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimes.java +++ b/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimes.java @@ -41,25 +41,25 @@ * @author Michal Maciejewski (michalm) */ public class DvrpOfflineTravelTimes { - private static final String DELIMITER = ";"; public static void saveLinkTravelTimes(TimeDiscretizer timeDiscretizer, double[][] linkTravelTimes, - String filename) { + String filename, String delimiter) { try (Writer writer = IOUtils.getBufferedWriter(filename)) { - saveLinkTravelTimes(timeDiscretizer, linkTravelTimes, writer); + saveLinkTravelTimes(timeDiscretizer, linkTravelTimes, writer, delimiter); } catch (IOException e) { throw new RuntimeException(e); } } - public static void saveLinkTravelTimes(TimeDiscretizer timeDiscretizer, double[][] linkTravelTimes, Writer writer) + public static void saveLinkTravelTimes(TimeDiscretizer timeDiscretizer, double[][] linkTravelTimes, + Writer writer, String delimiter) throws IOException { int intervalCount = timeDiscretizer.getIntervalCount(); //header row - writer.append("linkId" + DELIMITER); + writer.append("linkId").append(delimiter); for (int i = 0; i < intervalCount; i++) { double time = i * timeDiscretizer.getTimeInterval(); - writer.append(time + DELIMITER); + writer.append(String.valueOf(time)).append(delimiter); } writer.append('\n'); @@ -71,12 +71,12 @@ public static void saveLinkTravelTimes(TimeDiscretizer timeDiscretizer, double[] if (ttRow != null) { checkArgument(ttRow.length == intervalCount); - writer.append(Id.get(idx, Link.class) + DELIMITER); + writer.append(String.valueOf(Id.get(idx, Link.class))).append(delimiter); for (int t = 0; t < intervalCount; t++) { // rounding up to full seconds, otherwise the output files are sometimes huge (even when gzipped) // consider having a switch for enabling/disabling rounding int tt = (int)Math.ceil(ttRow[t]);//rounding up to avoid zeros; also QSim rounds up - writer.append(tt + DELIMITER); + writer.append(String.valueOf(tt)).append(delimiter); } writer.append('\n'); } @@ -101,27 +101,28 @@ public static TravelTime asTravelTime(TimeDiscretizer timeDiscretizer, double[][ }; } - public static double[][] loadLinkTravelTimes(TimeDiscretizer timeDiscretizer, URL url) { + public static double[][] loadLinkTravelTimes(TimeDiscretizer timeDiscretizer, URL url, String delimiter) { try (BufferedReader reader = IOUtils.getBufferedReader(url)) { - return loadLinkTravelTimes(timeDiscretizer, reader); + return loadLinkTravelTimes(timeDiscretizer, reader, delimiter); } catch (IOException e) { throw new RuntimeException(e); } } - public static double[][] loadLinkTravelTimes(TimeDiscretizer timeDiscretizer, BufferedReader reader) + public static double[][] loadLinkTravelTimes(TimeDiscretizer timeDiscretizer, BufferedReader reader, + String delimiter) throws IOException { //start with IdMap and then convert to array (to avoid index out of bounds) IdMap linkTravelTimes = new IdMap<>(Link.class); //header row - String[] headerLine = reader.readLine().split(";"); + String[] headerLine = reader.readLine().split(delimiter); verify(timeDiscretizer.getIntervalCount() == headerLine.length - 1); verify(headerLine[0].equals("linkId")); timeDiscretizer.forEach((bin, time) -> verify(Double.parseDouble(headerLine[bin + 1]) == time)); //regular rows - reader.lines().map(line -> line.split(DELIMITER)).forEach(cells -> { + reader.lines().map(line -> line.split(delimiter)).forEach(cells -> { verify(timeDiscretizer.getIntervalCount() == cells.length - 1); double[] row = new double[timeDiscretizer.getIntervalCount()]; diff --git a/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpTravelTimeModule.java b/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpTravelTimeModule.java index 798ac1166c1..c295c70ac6a 100644 --- a/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpTravelTimeModule.java +++ b/contribs/dvrp/src/main/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpTravelTimeModule.java @@ -48,7 +48,8 @@ public void install() { addTravelTimeBinding(DvrpTravelTimeModule.DVRP_INITIAL).toProvider(() -> { URL url = ConfigGroup.getInputFileURL(getConfig().getContext(), dvrpCfg.initialTravelTimesFile); var timeDiscretizer = new TimeDiscretizer(getConfig().travelTimeCalculator()); - var linkTravelTimes = DvrpOfflineTravelTimes.loadLinkTravelTimes(timeDiscretizer, url); + var linkTravelTimes = DvrpOfflineTravelTimes.loadLinkTravelTimes(timeDiscretizer, url, + getConfig().global().getDefaultDelimiter()); return DvrpOfflineTravelTimes.asTravelTime(timeDiscretizer, linkTravelTimes); }).asEagerSingleton(); } else { diff --git a/contribs/dvrp/src/test/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimeEstimatorTest.java b/contribs/dvrp/src/test/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimeEstimatorTest.java index 808df1ff835..2260a2f0979 100644 --- a/contribs/dvrp/src/test/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimeEstimatorTest.java +++ b/contribs/dvrp/src/test/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimeEstimatorTest.java @@ -61,7 +61,7 @@ public class DvrpOfflineTravelTimeEstimatorTest { @Test void getLinkTravelTime_timeAreCorrectlyBinned() { - var estimator = new DvrpOfflineTravelTimeEstimator(initialTT, null, network, timeDiscretizer, 0.25, null); + var estimator = new DvrpOfflineTravelTimeEstimator(initialTT, null, network, timeDiscretizer, 0.25, null, ";"); //bin 0 assertThat(linkTravelTime(estimator, linkAB, -1)).isEqualTo(1); @@ -98,7 +98,7 @@ void getLinkTravelTime_exponentialAveragingOverIterations() { }; var estimator = new DvrpOfflineTravelTimeEstimator(initialTT, observedTT, network, timeDiscretizer, alpha, - null); + null,";"); //expected TTs for each time bin double expectedTT_0 = 1; @@ -125,7 +125,8 @@ void getLinkTravelTime_exponentialAveragingOverIterations() { @Test void getLinkTravelTime_linkOutsideNetwork_fail() { var linkOutsideNetwork = new FakeLink(Id.createLinkId("some-link")); - var estimator = new DvrpOfflineTravelTimeEstimator(initialTT, null, network, timeDiscretizer, 0.25, null); + var estimator = new DvrpOfflineTravelTimeEstimator(initialTT, null, network, timeDiscretizer, + 0.25, null, ";"); assertThatThrownBy(() -> linkTravelTime(estimator, linkOutsideNetwork, 0)).isExactlyInstanceOf( NullPointerException.class) diff --git a/contribs/dvrp/src/test/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimesTest.java b/contribs/dvrp/src/test/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimesTest.java index e3b904d4084..f8270e2f8b3 100644 --- a/contribs/dvrp/src/test/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimesTest.java +++ b/contribs/dvrp/src/test/java/org/matsim/contrib/dvrp/trafficmonitoring/DvrpOfflineTravelTimesTest.java @@ -59,7 +59,8 @@ void saveLinkTravelTimes() throws IOException { linkTTs[linkIdB.index()] = new double[] { 5.5, 6.6, 7.7, 8.8, 9.9 }; var stringWriter = new StringWriter(); - DvrpOfflineTravelTimes.saveLinkTravelTimes(new TimeDiscretizer(3600, 900), linkTTs, stringWriter); + DvrpOfflineTravelTimes.saveLinkTravelTimes(new TimeDiscretizer(3600, 900), + linkTTs, stringWriter, ";"); var lines = stringWriter.toString().split("\n"); assertThat(lines).hasSize(3); @@ -76,7 +77,8 @@ void loadLinkTravelTimes() throws IOException { var lines = String.join("\n", line0, line1, line2); var stringReader = new BufferedReader(new StringReader(lines)); - var linkTTs = DvrpOfflineTravelTimes.loadLinkTravelTimes(new TimeDiscretizer(3600, 900), stringReader); + var linkTTs = DvrpOfflineTravelTimes.loadLinkTravelTimes(new TimeDiscretizer(3600, 900), + stringReader, ";"); //the matrix may have more than 2 rows (depends on how many link ids are cached) //all rows are null (except for links A and B)