diff --git a/contribs/application/src/main/java/org/matsim/application/ApplicationUtils.java b/contribs/application/src/main/java/org/matsim/application/ApplicationUtils.java index 083a315ecb5..1bd76f4d445 100644 --- a/contribs/application/src/main/java/org/matsim/application/ApplicationUtils.java +++ b/contribs/application/src/main/java/org/matsim/application/ApplicationUtils.java @@ -440,8 +440,13 @@ public static Path matchInput(String name, Path dir) { if (path.isPresent()) return path.get(); - // Match more general pattern at last - path = matchPattern(".+\\.[a-zA-Z0-9]*_" + name + "\\..+", dir); + // Match more general pattern + path = matchPattern(".+\\.[a-zA-Z0-9\\-]*_" + name + "\\..+", dir); + if (path.isPresent()) + return path.get(); + + // Even more permissive pattern + path = matchPattern(".+\\.[a-zA-Z0-9_.\\-]*(_|\\.)" + name + "\\..+", dir); if (path.isPresent()) return path.get(); diff --git a/contribs/application/src/main/java/org/matsim/application/analysis/population/TripAnalysis.java b/contribs/application/src/main/java/org/matsim/application/analysis/population/TripAnalysis.java index 11c75f21b1a..893c05cf9fd 100644 --- a/contribs/application/src/main/java/org/matsim/application/analysis/population/TripAnalysis.java +++ b/contribs/application/src/main/java/org/matsim/application/analysis/population/TripAnalysis.java @@ -17,6 +17,7 @@ import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; +import org.matsim.application.ApplicationUtils; import org.matsim.application.CommandSpec; import org.matsim.application.MATSimAppCommand; import org.matsim.application.options.CsvOptions; @@ -34,6 +35,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.stream.IntStream; @@ -45,15 +47,13 @@ produces = { "mode_share.csv", "mode_share_per_dist.csv", "mode_users.csv", "trip_stats.csv", "mode_share_per_%s.csv", "population_trip_stats.csv", "trip_purposes_by_hour.csv", - "mode_share_distance_distribution.csv", + "mode_share_distance_distribution.csv", "mode_shift.csv", "mode_choices.csv", "mode_choice_evaluation.csv", "mode_choice_evaluation_per_mode.csv", "mode_confusion_matrix.csv", "mode_prediction_error.csv" } ) public class TripAnalysis implements MATSimAppCommand { - private static final Logger log = LogManager.getLogger(TripAnalysis.class); - /** * Attributes which relates this person to a reference person. */ @@ -66,31 +66,24 @@ public class TripAnalysis implements MATSimAppCommand { * Person attribute containing its weight for analysis purposes. */ public static final String ATTR_REF_WEIGHT = "ref_weight"; - + private static final Logger log = LogManager.getLogger(TripAnalysis.class); + @CommandLine.Option(names = "--person-filter", description = "Define which persons should be included into trip analysis. Map like: Attribute name (key), attribute value (value). " + + "The attribute needs to be contained by output_persons.csv. Persons who do not match all filters are filtered out.", split = ",") + private final Map personFilters = new HashMap<>(); @CommandLine.Mixin private InputOptions input = InputOptions.ofCommand(TripAnalysis.class); @CommandLine.Mixin private OutputOptions output = OutputOptions.ofCommand(TripAnalysis.class); - @CommandLine.Option(names = "--input-ref-data", description = "Optional path to reference data", required = false) private String refData; - @CommandLine.Option(names = "--match-id", description = "Pattern to filter agents by id") private String matchId; - @CommandLine.Option(names = "--dist-groups", split = ",", description = "List of distances for binning", defaultValue = "0,1000,2000,5000,10000,20000") private List distGroups; - @CommandLine.Option(names = "--modes", split = ",", description = "List of considered modes, if not set all will be used") private List modeOrder; - @CommandLine.Option(names = "--shp-filter", description = "Define how the shp file filtering should work", defaultValue = "home") private LocationFilter filter; - - @CommandLine.Option(names = "--person-filter", description = "Define which persons should be included into trip analysis. Map like: Attribute name (key), attribute value (value). " + - "The attribute needs to be contained by output_persons.csv. Persons who do not match all filters are filtered out.", split = ",") - private final Map personFilters = new HashMap<>(); - @CommandLine.Mixin private ShpOptions shp; @@ -137,6 +130,20 @@ private static double[] calcHistogram(double[] data, double[] bins) { return hist; } + private static Map getColumnTypes() { + Map columnTypes = new HashMap<>(Map.of("person", ColumnType.TEXT, + "trav_time", ColumnType.STRING, "wait_time", ColumnType.STRING, "dep_time", ColumnType.STRING, + "longest_distance_mode", ColumnType.STRING, "main_mode", ColumnType.STRING, + "start_activity_type", ColumnType.TEXT, "end_activity_type", ColumnType.TEXT, + "first_pt_boarding_stop", ColumnType.TEXT, "last_pt_egress_stop", ColumnType.TEXT)); + + // Map.of only has 10 argument max + columnTypes.put("traveled_distance", ColumnType.LONG); + columnTypes.put("euclidean_distance", ColumnType.LONG); + + return columnTypes; + } + @Override public Integer call() throws Exception { @@ -209,18 +216,8 @@ public Integer call() throws Exception { log.info("Filtered {} out of {} persons", persons.rowCount(), total); - Map columnTypes = new HashMap<>(Map.of("person", ColumnType.TEXT, - "trav_time", ColumnType.STRING, "wait_time", ColumnType.STRING, "dep_time", ColumnType.STRING, - "longest_distance_mode", ColumnType.STRING, "main_mode", ColumnType.STRING, - "start_activity_type", ColumnType.TEXT, "end_activity_type", ColumnType.TEXT, - "first_pt_boarding_stop", ColumnType.TEXT, "last_pt_egress_stop", ColumnType.TEXT)); - - // Map.of only has 10 argument max - columnTypes.put("traveled_distance", ColumnType.LONG); - columnTypes.put("euclidean_distance", ColumnType.LONG); - Table trips = Table.read().csv(CsvReadOptions.builder(IOUtils.getBufferedReader(input.getPath("trips.csv"))) - .columnTypesPartial(columnTypes) + .columnTypesPartial(getColumnTypes()) .sample(false) .separator(CsvOptions.detectDelimiter(input.getPath("trips.csv"))).build()); @@ -314,6 +311,8 @@ public Integer call() throws Exception { writeTripDistribution(joined); + writeModeShift(joined); + return 0; } @@ -583,6 +582,34 @@ private void writeTripDistribution(Table trips) throws IOException { } } + private void writeModeShift(Table trips) throws IOException { + Path path; + try { + Path dir = Path.of(input.getPath("trips.csv")).getParent().resolve("ITERS").resolve("it.0"); + path = ApplicationUtils.matchInput("trips.csv", dir); + } catch (Exception e) { + log.error("Could not find trips from 0th iteration.", e); + return; + } + + Table originalTrips = Table.read().csv(CsvReadOptions.builder(IOUtils.getBufferedReader(path.toString())) + .columnTypesPartial(getColumnTypes()) + .sample(false) + .separator(CsvOptions.detectDelimiter(path.toString())).build()); + + // Use longest_distance_mode where main_mode is not present + originalTrips.stringColumn("main_mode") + .set(originalTrips.stringColumn("main_mode").isMissing(), + originalTrips.stringColumn("longest_distance_mode")); + + originalTrips.column("main_mode").setName("original_mode"); + + Table joined = new DataFrameJoiner(trips, "trip_id").inner(true, originalTrips); + Table aggr = joined.summarize("trip_id", count).by("original_mode", "main_mode"); + + aggr.write().csv(output.getPath("mode_shift.csv").toFile()); + } + /** * How shape file filtering should be applied. */ diff --git a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/dashboard/TripDashboard.java b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/dashboard/TripDashboard.java index 7603da2b791..34c5f1ba1c3 100644 --- a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/dashboard/TripDashboard.java +++ b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/dashboard/TripDashboard.java @@ -247,7 +247,7 @@ public void configure(Header header, Layout layout) { .el(Plotly.class, (viz, data) -> { viz.title = "Mode usage"; - viz.description = "Share of persons using a main mode at least once per day."; + viz.description = "Share of persons using a main mode at least once per day"; viz.width = 2d; Plotly.DataSet ds = viz.addDataset(data.compute(TripAnalysis.class, "mode_users.csv")); @@ -267,6 +267,11 @@ public void configure(Header header, Layout layout) { viz.mergeDatasets = true; } + }).el(Sankey.class, (viz, data) -> { + viz.title = "Mode shift"; + viz.width = 1.5d; + viz.description = "by main mode. Compares initial input with output after the last iteration"; + viz.csv = data.compute(TripAnalysis.class, "mode_shift.csv", args); }); createDistancePlot(layout, args, tab); diff --git a/contribs/simwrapper/src/test/java/org/matsim/simwrapper/dashboard/DashboardTests.java b/contribs/simwrapper/src/test/java/org/matsim/simwrapper/dashboard/DashboardTests.java index 7818cd4c190..baf1b5b2b8d 100644 --- a/contribs/simwrapper/src/test/java/org/matsim/simwrapper/dashboard/DashboardTests.java +++ b/contribs/simwrapper/src/test/java/org/matsim/simwrapper/dashboard/DashboardTests.java @@ -74,7 +74,8 @@ void trip() { run(new TripDashboard()); Assertions.assertThat(out) .isDirectoryContaining("glob:**trip_stats.csv") - .isDirectoryContaining("glob:**mode_share.csv"); + .isDirectoryContaining("glob:**mode_share.csv") + .isDirectoryContaining("glob:**mode_shift.csv"); } @Test diff --git a/matsim/src/main/java/org/matsim/analysis/IterationTravelStatsControlerListener.java b/matsim/src/main/java/org/matsim/analysis/IterationTravelStatsControlerListener.java index 483f282480c..f0807fbe07e 100644 --- a/matsim/src/main/java/org/matsim/analysis/IterationTravelStatsControlerListener.java +++ b/matsim/src/main/java/org/matsim/analysis/IterationTravelStatsControlerListener.java @@ -94,15 +94,17 @@ private boolean isWriteGraph(IterationEndsEvent event){ return config.controller().getCreateGraphsInterval() > 0 && event.getIteration() % config.controller().getCreateGraphsInterval() == 0; } - private boolean isWriteTripsAndLegs(IterationEndsEvent event){ - if (config.controller().getWriteTripsInterval() <= 0) { - return false; - } + private boolean isWriteTripsAndLegs(IterationEndsEvent event) { - if (event.getIteration() == 0 || event.isLastIteration()){ - return true; + // This uses the same logic as in PlansDumpingImpl + int writeTripsInterval = config.controller().getWriteTripsInterval(); + final boolean writingTripsAtAll = writeTripsInterval > 0; + final boolean earlyIteration = event.getIteration() <= config.controller().getWritePlansInterval() ; + + if (!writingTripsAtAll) { + return false; } - return event.getIteration() % config.controller().getWriteTripsInterval() == 0; + return earlyIteration || event.isLastIteration() || event.getIteration() % writeTripsInterval == 0; } } diff --git a/matsim/src/main/java/org/matsim/core/config/consistency/VspConfigConsistencyCheckerImpl.java b/matsim/src/main/java/org/matsim/core/config/consistency/VspConfigConsistencyCheckerImpl.java index 901d5da4b5c..9fd9e5045b7 100644 --- a/matsim/src/main/java/org/matsim/core/config/consistency/VspConfigConsistencyCheckerImpl.java +++ b/matsim/src/main/java/org/matsim/core/config/consistency/VspConfigConsistencyCheckerImpl.java @@ -495,6 +495,19 @@ private static boolean checkControlerConfigGroup( Config config, Level lvl, bool case SpeedyALT: break; } + + if ( config.controller().getWritePlansInterval() <= 0 ) { + problem = true ; + System.out.flush() ; + log.log( lvl, "found writePlansInterval==0. vsp default is to write plans at least once (for simwrapper).") ; + } + + if ( config.controller().getWriteTripsInterval() <= 0 ) { + problem = true ; + System.out.flush() ; + log.log( lvl, "found writeTripsInterval==0. vsp default is to write trips at least once (for simwrapper).") ; + } + return problem; }