From 68942a6af280554d09300715053d7180837a20aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=B6rl?= Date: Mon, 17 Mar 2025 17:31:41 +0100 Subject: [PATCH 1/8] feat: travel time comparison --- .../analysis/EqasimAnalysisModule.java | 25 +- .../TravelTimeComparisionListener.java | 246 ++++++++++++++++++ 2 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java diff --git a/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java b/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java index 71b9cea09..3243ff6ac 100644 --- a/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java +++ b/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java @@ -1,21 +1,30 @@ package org.eqasim.core.simulation.analysis; +import java.util.HashSet; + import org.eqasim.core.analysis.DefaultPersonAnalysisFilter; import org.eqasim.core.analysis.PersonAnalysisFilter; import org.eqasim.core.analysis.activities.ActivityListener; import org.eqasim.core.analysis.legs.LegListener; import org.eqasim.core.analysis.pt.PublicTransportLegListener; import org.eqasim.core.analysis.trips.TripListener; +import org.eqasim.core.components.config.EqasimConfigGroup; import org.eqasim.core.components.travel_time.TravelTimeRecorder; import org.eqasim.core.scenario.cutter.network.RoadNetwork; import org.eqasim.core.simulation.analysis.stuck.StuckAnalysisModule; +import org.eqasim.core.simulation.analysis.travel_time.TravelTimeComparisionListener; import org.eqasim.core.simulation.modes.drt.analysis.DrtAnalysisModule; import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.population.Population; import org.matsim.contrib.drt.run.MultiModeDrtConfigGroup; +import org.matsim.core.api.experimental.events.EventsManager; import org.matsim.core.config.Config; +import org.matsim.core.config.groups.RoutingConfigGroup; import org.matsim.core.controler.AbstractModule; +import org.matsim.core.controler.OutputDirectoryHierarchy; import org.matsim.core.router.AnalysisMainModeIdentifier; import org.matsim.core.router.RoutingModeMainModeIdentifier; +import org.matsim.core.utils.timing.TimeInterpretation; import org.matsim.pt.transitSchedule.api.TransitSchedule; import com.google.inject.Provides; @@ -37,8 +46,9 @@ public void install() { } install(new StuckAnalysisModule()); - + bind(AnalysisMainModeIdentifier.class).toInstance(new RoutingModeMainModeIdentifier()); + addControlerListenerBinding().to(TravelTimeComparisionListener.class); } @Provides @@ -59,7 +69,7 @@ public PublicTransportLegListener providePublicTransportListener(Network network PersonAnalysisFilter personFilter) { return new PublicTransportLegListener(schedule); } - + @Provides @Singleton public ActivityListener provideActivityListener(PersonAnalysisFilter personFilter) { @@ -77,4 +87,15 @@ public TravelTimeRecorder travelTimeRecorder(Network network, Config config) { } return new TravelTimeRecorder(new RoadNetwork(network), startTime, stopTime, 600); } + + @Provides + @Singleton + public TravelTimeComparisionListener provideTravelTimeComparisionListener(Population population, + TimeInterpretation timeInterpretation, + OutputDirectoryHierarchy outputDirectoryHierarchy, EventsManager eventsManager, + EqasimConfigGroup eqasimConfig, + int detailedAnalysisInterval, RoutingConfigGroup routingConfig) { + return new TravelTimeComparisionListener(population, timeInterpretation, outputDirectoryHierarchy, + eventsManager, eqasimConfig.getAnalysisInterval(), 0, new HashSet<>(routingConfig.getNetworkModes())); + } } diff --git a/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java b/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java new file mode 100644 index 000000000..52ff2c7a3 --- /dev/null +++ b/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java @@ -0,0 +1,246 @@ +package org.eqasim.core.simulation.analysis.travel_time; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.events.PersonArrivalEvent; +import org.matsim.api.core.v01.events.PersonDepartureEvent; +import org.matsim.api.core.v01.events.handler.PersonArrivalEventHandler; +import org.matsim.api.core.v01.events.handler.PersonDepartureEventHandler; +import org.matsim.api.core.v01.population.Leg; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.PlanElement; +import org.matsim.api.core.v01.population.Population; +import org.matsim.core.api.experimental.events.EventsManager; +import org.matsim.core.controler.OutputDirectoryHierarchy; +import org.matsim.core.controler.events.IterationEndsEvent; +import org.matsim.core.controler.events.IterationStartsEvent; +import org.matsim.core.controler.listener.IterationEndsListener; +import org.matsim.core.controler.listener.IterationStartsListener; +import org.matsim.core.utils.io.IOUtils; +import org.matsim.core.utils.timing.TimeInterpretation; +import org.matsim.core.utils.timing.TimeTracker; + +public class TravelTimeComparisionListener + implements IterationStartsListener, IterationEndsListener, PersonDepartureEventHandler, + PersonArrivalEventHandler { + static public final String DETAILED_OUTPUT_NAME = "detailed_travel_time_comparison.csv"; + static public final String HOURLY_OUTPUT_NAME = "hourly_travel_time_comparison.csv"; + static public final String OVERALL_OUTPUT_NAME = "travel_time_comparison.csv"; + + private final Population population; + private final TimeInterpretation timeInterpretation; + private final OutputDirectoryHierarchy outputHierarchy; + private final EventsManager eventsManager; + + private final int analysisInterval; + private final int detailedAnalysisInterval; + + private final Set modes; + + private final Map>> trackedTimes = new HashMap<>(); + private final IdMap ongoing = new IdMap<>(Person.class); + + public TravelTimeComparisionListener(Population population, TimeInterpretation timeInterpretation, + OutputDirectoryHierarchy outputDirectoryHierarchy, EventsManager eventsManager, int analysisInterval, + int detailedAnalysisInterval, Set modes) { + this.population = population; + this.timeInterpretation = timeInterpretation; + this.outputHierarchy = outputDirectoryHierarchy; + this.eventsManager = eventsManager; + this.analysisInterval = analysisInterval; + this.detailedAnalysisInterval = detailedAnalysisInterval; + this.modes = modes; + } + + private record OngoingLegItem(String mode, double departureTime) { + } + + private record FinishedLegItem(double departureTime, double travelTime) { + } + + private List getList(String mode, Id personId) { + return trackedTimes.computeIfAbsent(mode, m -> new IdMap<>(Person.class)).computeIfAbsent(personId, + p -> new LinkedList<>()); + } + + @Override + public void notifyIterationStarts(IterationStartsEvent event) { + trackedTimes.clear(); + ongoing.clear(); + + if (analysisInterval > 0 && (event.getIteration() % analysisInterval == 0 || event.isLastIteration())) { + eventsManager.addHandler(this); + } + } + + @Override + public void handleEvent(PersonDepartureEvent event) { + if (modes.contains(event.getLegMode())) { + ongoing.put(event.getPersonId(), new OngoingLegItem(event.getLegMode(), event.getTime())); + } + } + + @Override + public void handleEvent(PersonArrivalEvent event) { + if (modes.contains(event.getLegMode())) { + OngoingLegItem item = ongoing.remove(event.getPersonId()); + getList(item.mode, event.getPersonId()) + .add(new FinishedLegItem(item.departureTime, event.getTime() - item.departureTime)); + } + } + + @Override + public void notifyIterationEnds(IterationEndsEvent event) { + try { + if (analysisInterval > 0 && (event.getIteration() % analysisInterval == 0 || event.isLastIteration())) { + eventsManager.removeHandler(this); + + Map overallSummary = new HashMap<>(); + Map> hourlySummary = new HashMap<>(); + + for (String mode : modes) { + overallSummary.put(mode, new DescriptiveStatistics()); + hourlySummary.put(mode, new LinkedList<>()); + + for (int hour = 0; hour < 24; hour++) { + hourlySummary.get(mode).add(new DescriptiveStatistics()); + } + } + + boolean writeDetails = detailedAnalysisInterval > 0 + && (event.getIteration() % detailedAnalysisInterval == 0 || event.isLastIteration()); + + BufferedWriter detailsWriter = writeDetails ? IOUtils + .getBufferedWriter( + outputHierarchy.getIterationFilename(event.getIteration(), DETAILED_OUTPUT_NAME)) + : null; + + if (detailsWriter != null) { + detailsWriter.write(String.join(";", new String[] { // + "person_id", "leg_index", "mode", "planned_departure_time", "planned_travel_time", + "tracked_departure_time", "tracked_travel_time" + }) + "\n"); + } + + for (Person person : population.getPersons().values()) { + TimeTracker timeTracker = new TimeTracker(timeInterpretation); + int legIndex = 0; + + for (PlanElement element : person.getSelectedPlan().getPlanElements()) { + if (element instanceof Leg leg) { + if (modes.contains(leg.getMode())) { + List finished = getList(leg.getMode(), person.getId()); + + double trackedDepartureTime = Double.NaN; + double trackedTravelTime = Double.NaN; + + if (legIndex < finished.size()) { + FinishedLegItem item = finished.get(legIndex); + trackedDepartureTime = item.departureTime; + trackedTravelTime = item.travelTime; + } + + double plannedDepartureTime = timeTracker.getTime().seconds(); + double plannedTravelTime = leg.getTravelTime().seconds(); + + if (detailsWriter != null) { + detailsWriter.write(String.join(";", new String[] { // + person.getId().toString(), // + String.valueOf(legIndex), // + leg.getMode(), // + String.valueOf(plannedDepartureTime), // + String.valueOf(plannedTravelTime), // + String.valueOf(trackedDepartureTime), // + String.valueOf(trackedTravelTime) // + }) + "\n"); + } + + if (plannedTravelTime > 0.0 || trackedTravelTime > 0.0) { + int hour = (int) Math.floor(plannedDepartureTime / 3600.0); + if (hour < 24 && hour >= 0) { + hourlySummary.get(leg.getMode()).get(hour) + .addValue(plannedTravelTime - trackedTravelTime); + } + + overallSummary.get(leg.getMode()).addValue(plannedTravelTime - trackedTravelTime); + } + } + + legIndex++; + } + + timeTracker.addElement(element); + } + } + + if (detailsWriter != null) { + detailsWriter.close(); + } + + BufferedWriter hourlyWriter = IOUtils.getAppendingBufferedWriter( + outputHierarchy.getIterationFilename(event.getIteration(), HOURLY_OUTPUT_NAME)); + + hourlyWriter.write(String.join(";", new String[] { + "hour", "mode", "obs", "mean", "median", "q10", "q90", "std" + }) + "\n"); + + for (String mode : modes) { + for (int hour = 0; hour < 24; hour++) { + DescriptiveStatistics summary = hourlySummary.get(mode).get(hour); + + hourlyWriter.write(String.join(";", new String[] { + String.valueOf(hour), mode, // + String.valueOf(summary.getN()), // + String.valueOf(summary.getMean()), // + String.valueOf(summary.getPercentile(50)), // + String.valueOf(summary.getPercentile(10)), // + String.valueOf(summary.getPercentile(90)), // + String.valueOf(summary.getStandardDeviation()) // + }) + "\n"); + } + } + + hourlyWriter.close(); + + boolean writeHeader = !new File(outputHierarchy.getOutputFilename(OVERALL_OUTPUT_NAME)).exists(); + BufferedWriter overallWriter = IOUtils + .getAppendingBufferedWriter(outputHierarchy.getOutputFilename(OVERALL_OUTPUT_NAME)); + + if (writeHeader) { + overallWriter.write(String.join(";", new String[] { + "iteration", "mode", "mean", "median", "q10", "q90", "std" + }) + "\n"); + } + + for (String mode : modes) { + DescriptiveStatistics summary = overallSummary.get(mode); + + overallWriter.write(String.join(";", new String[] { // + String.valueOf(event.getIteration()), // + mode, // + String.valueOf(summary.getMean()), // + String.valueOf(summary.getMean()), // + String.valueOf(summary.getPercentile(50)), // + String.valueOf(summary.getPercentile(10)), // + String.valueOf(summary.getPercentile(90)), // + String.valueOf(summary.getStandardDeviation()), // + }) + "\n"); + } + + overallWriter.close(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} From 42349b480aeb77527f3b4e37fc6dc83f5a2cf700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=B6rl?= Date: Fri, 13 Jun 2025 09:56:13 +0200 Subject: [PATCH 2/8] bugfix --- .../core/components/config/EqasimConfigGroup.java | 12 ++++++++++++ .../simulation/analysis/EqasimAnalysisModule.java | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/eqasim/core/components/config/EqasimConfigGroup.java b/core/src/main/java/org/eqasim/core/components/config/EqasimConfigGroup.java index e61777734..e0c6a722c 100644 --- a/core/src/main/java/org/eqasim/core/components/config/EqasimConfigGroup.java +++ b/core/src/main/java/org/eqasim/core/components/config/EqasimConfigGroup.java @@ -24,6 +24,7 @@ public class EqasimConfigGroup extends ReflectiveConfigGroup { private final static String ANALYSIS_DISTANCE_UNIT = "analysisDistanceUnit"; private final static String TRAVEL_TIME_RECORDING_INTERVAL = "travelTimeRecordingInterval"; + private final static String DETAILED_TRAVEL_TIME_ANALYSIS_INTERVAL = "detailedTravelTimeAnalysisInterval"; private final static String USE_SCHEDULE_BASED_TRANSPORT = "useScheduleBasedTransport"; @@ -41,6 +42,7 @@ public class EqasimConfigGroup extends ReflectiveConfigGroup { private DistanceUnit analysisDistanceUnit = DistanceUnit.meter; private int travelTimeRecordingInterval = 0; + private int detailedTravelTimeAnalysisInterval = 0; private boolean useScheduleBasedTransport = true; @@ -89,6 +91,16 @@ public void setUsePseudoRandomErrors(boolean usePseudoRandomErrors) { this.usePseudoRandomErrors = usePseudoRandomErrors; } + @StringGetter(DETAILED_TRAVEL_TIME_ANALYSIS_INTERVAL) + public int getDetailedTravelTimeAnalysisInterval() { + return detailedTravelTimeAnalysisInterval; + } + + @StringSetter(DETAILED_TRAVEL_TIME_ANALYSIS_INTERVAL) + public void setDetailedTravelTimeAnalysisInterval(int detailedTravelTimeAnalysisInterval) { + this.detailedTravelTimeAnalysisInterval = detailedTravelTimeAnalysisInterval; + } + @Override public ConfigGroup createParameterSet(String type) { switch (type) { diff --git a/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java b/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java index 3243ff6ac..aecde96a2 100644 --- a/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java +++ b/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java @@ -93,9 +93,9 @@ public TravelTimeRecorder travelTimeRecorder(Network network, Config config) { public TravelTimeComparisionListener provideTravelTimeComparisionListener(Population population, TimeInterpretation timeInterpretation, OutputDirectoryHierarchy outputDirectoryHierarchy, EventsManager eventsManager, - EqasimConfigGroup eqasimConfig, - int detailedAnalysisInterval, RoutingConfigGroup routingConfig) { + EqasimConfigGroup eqasimConfig, RoutingConfigGroup routingConfig) { return new TravelTimeComparisionListener(population, timeInterpretation, outputDirectoryHierarchy, - eventsManager, eqasimConfig.getAnalysisInterval(), 0, new HashSet<>(routingConfig.getNetworkModes())); + eventsManager, eqasimConfig.getAnalysisInterval(), eqasimConfig.getDetailedTravelTimeAnalysisInterval(), + new HashSet<>(routingConfig.getNetworkModes())); } } From 5ba20c354bb3ee211db02289edf5132dcb2c3891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=B6rl?= Date: Mon, 2 Mar 2026 12:31:59 +0100 Subject: [PATCH 3/8] update --- .../components/config/EqasimConfigGroup.java | 36 ++++---- .../analysis/EqasimAnalysisModule.java | 5 +- .../TravelTimeComparisionListener.java | 85 +++++++++++++++++-- 3 files changed, 99 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/org/eqasim/core/components/config/EqasimConfigGroup.java b/core/src/main/java/org/eqasim/core/components/config/EqasimConfigGroup.java index 1470993f2..ee207e435 100644 --- a/core/src/main/java/org/eqasim/core/components/config/EqasimConfigGroup.java +++ b/core/src/main/java/org/eqasim/core/components/config/EqasimConfigGroup.java @@ -26,9 +26,9 @@ public class EqasimConfigGroup extends ReflectiveConfigGroup { private final static String ANALYSIS_INTERVAL = "analysisInterval"; private final static String ANALYSIS_DISTANCE_UNIT = "analysisDistanceUnit"; - + private final static String TRAVEL_TIME_RECORDING_INTERVAL = "travelTimeRecordingInterval"; - private final static String DETAILED_TRAVEL_TIME_ANALYSIS_INTERVAL = "detailedTravelTimeAnalysisInterval"; + private final static String TRAVEL_TIME_ANALYSIS_INTERVAL = "travelTimeAnalysisInterval"; private final static String USE_SCHEDULE_BASED_TRANSPORT = "useScheduleBasedTransport"; @@ -46,9 +46,9 @@ public class EqasimConfigGroup extends ReflectiveConfigGroup { private int analysisInterval = 0; private DistanceUnit analysisDistanceUnit = DistanceUnit.meter; - + private int travelTimeRecordingInterval = 0; - private int detailedTravelTimeAnalysisInterval = 0; + private int TravelTimeAnalysisInterval = 0; private boolean useScheduleBasedTransport = true; @@ -59,13 +59,13 @@ public class EqasimConfigGroup extends ReflectiveConfigGroup { public EqasimConfigGroup() { super(GROUP_NAME); } - + @Override public final Map getComments() { Map map = super.getComments(); map.put(SAMPLE_SIZE, "The sample size of the population you are simulating. This is normally set by the synthesis pipeline."); - + return map; } @@ -99,25 +99,25 @@ public void setUsePseudoRandomErrors(boolean usePseudoRandomErrors) { this.usePseudoRandomErrors = usePseudoRandomErrors; } - @StringGetter(DETAILED_TRAVEL_TIME_ANALYSIS_INTERVAL) - public int getDetailedTravelTimeAnalysisInterval() { - return detailedTravelTimeAnalysisInterval; + @StringGetter(TRAVEL_TIME_ANALYSIS_INTERVAL) + public int getTravelTimeAnalysisInterval() { + return TravelTimeAnalysisInterval; } - @StringSetter(DETAILED_TRAVEL_TIME_ANALYSIS_INTERVAL) - public void setDetailedTravelTimeAnalysisInterval(int detailedTravelTimeAnalysisInterval) { - this.detailedTravelTimeAnalysisInterval = detailedTravelTimeAnalysisInterval; + @StringSetter(TRAVEL_TIME_ANALYSIS_INTERVAL) + public void setTravelTimeAnalysisInterval(int TravelTimeAnalysisInterval) { + this.TravelTimeAnalysisInterval = TravelTimeAnalysisInterval; } @Override public ConfigGroup createParameterSet(String type) { switch (type) { - case EstimatorParameterSet.GROUP_NAME: - return new EstimatorParameterSet(); - case CostModelParameterSet.GROUP_NAME: - return new CostModelParameterSet(); - default: - throw new IllegalArgumentException("Unknown parameter set type: " + type); + case EstimatorParameterSet.GROUP_NAME: + return new EstimatorParameterSet(); + case CostModelParameterSet.GROUP_NAME: + return new CostModelParameterSet(); + default: + throw new IllegalArgumentException("Unknown parameter set type: " + type); } } diff --git a/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java b/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java index aecde96a2..923839255 100644 --- a/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java +++ b/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java @@ -19,6 +19,7 @@ import org.matsim.contrib.drt.run.MultiModeDrtConfigGroup; import org.matsim.core.api.experimental.events.EventsManager; import org.matsim.core.config.Config; +import org.matsim.core.config.groups.ControllerConfigGroup; import org.matsim.core.config.groups.RoutingConfigGroup; import org.matsim.core.controler.AbstractModule; import org.matsim.core.controler.OutputDirectoryHierarchy; @@ -93,9 +94,9 @@ public TravelTimeRecorder travelTimeRecorder(Network network, Config config) { public TravelTimeComparisionListener provideTravelTimeComparisionListener(Population population, TimeInterpretation timeInterpretation, OutputDirectoryHierarchy outputDirectoryHierarchy, EventsManager eventsManager, - EqasimConfigGroup eqasimConfig, RoutingConfigGroup routingConfig) { + EqasimConfigGroup eqasimConfig, RoutingConfigGroup routingConfig, ControllerConfigGroup controllerConfig) { return new TravelTimeComparisionListener(population, timeInterpretation, outputDirectoryHierarchy, - eventsManager, eqasimConfig.getAnalysisInterval(), eqasimConfig.getDetailedTravelTimeAnalysisInterval(), + eventsManager, eqasimConfig.getTravelTimeAnalysisInterval(), eqasimConfig.getAnalysisInterval(), new HashSet<>(routingConfig.getNetworkModes())); } } diff --git a/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java b/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java index 52ff2c7a3..0f80aa137 100644 --- a/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java +++ b/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java @@ -18,6 +18,7 @@ import org.matsim.api.core.v01.events.handler.PersonDepartureEventHandler; import org.matsim.api.core.v01.population.Leg; import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; import org.matsim.api.core.v01.population.PlanElement; import org.matsim.api.core.v01.population.Population; import org.matsim.core.api.experimental.events.EventsManager; @@ -106,20 +107,29 @@ public void notifyIterationEnds(IterationEndsEvent event) { eventsManager.removeHandler(this); Map overallSummary = new HashMap<>(); + Map recentOverallSummary = new HashMap<>(); + Map> hourlySummary = new HashMap<>(); + Map> recentHourlySummary = new HashMap<>(); for (String mode : modes) { overallSummary.put(mode, new DescriptiveStatistics()); + recentOverallSummary.put(mode, new DescriptiveStatistics()); + hourlySummary.put(mode, new LinkedList<>()); + recentHourlySummary.put(mode, new LinkedList<>()); for (int hour = 0; hour < 24; hour++) { hourlySummary.get(mode).add(new DescriptiveStatistics()); + recentHourlySummary.get(mode).add(new DescriptiveStatistics()); } } boolean writeDetails = detailedAnalysisInterval > 0 && (event.getIteration() % detailedAnalysisInterval == 0 || event.isLastIteration()); + writeDetails = true; + BufferedWriter detailsWriter = writeDetails ? IOUtils .getBufferedWriter( outputHierarchy.getIterationFilename(event.getIteration(), DETAILED_OUTPUT_NAME)) @@ -128,7 +138,7 @@ public void notifyIterationEnds(IterationEndsEvent event) { if (detailsWriter != null) { detailsWriter.write(String.join(";", new String[] { // "person_id", "leg_index", "mode", "planned_departure_time", "planned_travel_time", - "tracked_departure_time", "tracked_travel_time" + "tracked_departure_time", "tracked_travel_time", "planned_age" }) + "\n"); } @@ -136,7 +146,28 @@ public void notifyIterationEnds(IterationEndsEvent event) { TimeTracker timeTracker = new TimeTracker(timeInterpretation); int legIndex = 0; - for (PlanElement element : person.getSelectedPlan().getPlanElements()) { + Plan plan = person.getSelectedPlan(); + + // START TODO: This can be much simplified if Plan.getIterationCreated works by + // default: https://github.com/matsim-org/matsim-libs/issues/4762 + + Integer planHash = (Integer) plan.getAttributes().getAttribute("travelTimeHash"); + Integer planIteration = (Integer) plan.getAttributes().getAttribute("travelTimeIteration"); + + final int age; + if (planHash == null || planHash != plan.hashCode()) { + age = 0; + plan.getAttributes().putAttribute("travelTimeHash", plan.hashCode()); + plan.getAttributes().putAttribute("travelTimeIteration", event.getIteration()); + } else { + age = event.getIteration() - planIteration; + } + + // END TODO + + boolean isRecent = age == 0; + + for (PlanElement element : plan.getPlanElements()) { if (element instanceof Leg leg) { if (modes.contains(leg.getMode())) { List finished = getList(leg.getMode(), person.getId()); @@ -161,7 +192,8 @@ public void notifyIterationEnds(IterationEndsEvent event) { String.valueOf(plannedDepartureTime), // String.valueOf(plannedTravelTime), // String.valueOf(trackedDepartureTime), // - String.valueOf(trackedTravelTime) // + String.valueOf(trackedTravelTime), // + String.valueOf(age) // }) + "\n"); } @@ -170,9 +202,19 @@ public void notifyIterationEnds(IterationEndsEvent event) { if (hour < 24 && hour >= 0) { hourlySummary.get(leg.getMode()).get(hour) .addValue(plannedTravelTime - trackedTravelTime); + + if (isRecent) { + recentHourlySummary.get(leg.getMode()).get(hour) + .addValue(plannedTravelTime - trackedTravelTime); + } } overallSummary.get(leg.getMode()).addValue(plannedTravelTime - trackedTravelTime); + + if (isRecent) { + recentOverallSummary.get(leg.getMode()) + .addValue(plannedTravelTime - trackedTravelTime); + } } } @@ -191,7 +233,7 @@ public void notifyIterationEnds(IterationEndsEvent event) { outputHierarchy.getIterationFilename(event.getIteration(), HOURLY_OUTPUT_NAME)); hourlyWriter.write(String.join(";", new String[] { - "hour", "mode", "obs", "mean", "median", "q10", "q90", "std" + "hour", "mode", "obs", "mean", "median", "q10", "q90", "std", "is_recent" }) + "\n"); for (String mode : modes) { @@ -205,7 +247,21 @@ public void notifyIterationEnds(IterationEndsEvent event) { String.valueOf(summary.getPercentile(50)), // String.valueOf(summary.getPercentile(10)), // String.valueOf(summary.getPercentile(90)), // - String.valueOf(summary.getStandardDeviation()) // + String.valueOf(summary.getStandardDeviation()), // + "false" // + }) + "\n"); + + DescriptiveStatistics recentSummary = recentHourlySummary.get(mode).get(hour); + + hourlyWriter.write(String.join(";", new String[] { + String.valueOf(hour), mode, // + String.valueOf(recentSummary.getN()), // + String.valueOf(recentSummary.getMean()), // + String.valueOf(recentSummary.getPercentile(50)), // + String.valueOf(recentSummary.getPercentile(10)), // + String.valueOf(recentSummary.getPercentile(90)), // + String.valueOf(recentSummary.getStandardDeviation()), // + "true" // }) + "\n"); } } @@ -218,7 +274,7 @@ public void notifyIterationEnds(IterationEndsEvent event) { if (writeHeader) { overallWriter.write(String.join(";", new String[] { - "iteration", "mode", "mean", "median", "q10", "q90", "std" + "iteration", "mode", "observations", "mean", "median", "q10", "q90", "std", "is_recent" }) + "\n"); } @@ -228,12 +284,27 @@ public void notifyIterationEnds(IterationEndsEvent event) { overallWriter.write(String.join(";", new String[] { // String.valueOf(event.getIteration()), // mode, // - String.valueOf(summary.getMean()), // + String.valueOf(summary.getN()), // String.valueOf(summary.getMean()), // String.valueOf(summary.getPercentile(50)), // String.valueOf(summary.getPercentile(10)), // String.valueOf(summary.getPercentile(90)), // String.valueOf(summary.getStandardDeviation()), // + "false" // + }) + "\n"); + + DescriptiveStatistics recentSummary = recentOverallSummary.get(mode); + + overallWriter.write(String.join(";", new String[] { // + String.valueOf(event.getIteration()), // + mode, // + String.valueOf(recentSummary.getN()), // + String.valueOf(recentSummary.getMean()), // + String.valueOf(recentSummary.getPercentile(50)), // + String.valueOf(recentSummary.getPercentile(10)), // + String.valueOf(recentSummary.getPercentile(90)), // + String.valueOf(recentSummary.getStandardDeviation()), // + "true" // }) + "\n"); } From c75879a441b0c0eb143c2f09e00e3ab3b178aa3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=B6rl?= Date: Mon, 2 Mar 2026 12:35:18 +0100 Subject: [PATCH 4/8] defaults --- .../eqasim/core/components/config/EqasimConfigGroup.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/eqasim/core/components/config/EqasimConfigGroup.java b/core/src/main/java/org/eqasim/core/components/config/EqasimConfigGroup.java index ee207e435..2e23811f3 100644 --- a/core/src/main/java/org/eqasim/core/components/config/EqasimConfigGroup.java +++ b/core/src/main/java/org/eqasim/core/components/config/EqasimConfigGroup.java @@ -48,7 +48,7 @@ public class EqasimConfigGroup extends ReflectiveConfigGroup { private DistanceUnit analysisDistanceUnit = DistanceUnit.meter; private int travelTimeRecordingInterval = 0; - private int TravelTimeAnalysisInterval = 0; + private int travelTimeAnalysisInterval = 10; private boolean useScheduleBasedTransport = true; @@ -101,12 +101,12 @@ public void setUsePseudoRandomErrors(boolean usePseudoRandomErrors) { @StringGetter(TRAVEL_TIME_ANALYSIS_INTERVAL) public int getTravelTimeAnalysisInterval() { - return TravelTimeAnalysisInterval; + return travelTimeAnalysisInterval; } @StringSetter(TRAVEL_TIME_ANALYSIS_INTERVAL) public void setTravelTimeAnalysisInterval(int TravelTimeAnalysisInterval) { - this.TravelTimeAnalysisInterval = TravelTimeAnalysisInterval; + this.travelTimeAnalysisInterval = TravelTimeAnalysisInterval; } @Override From 3b37c8f310f3f357dc336f5bcf035392f6d1d42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=B6rl?= Date: Mon, 2 Mar 2026 13:56:38 +0100 Subject: [PATCH 5/8] fix tests --- .../TravelTimeComparisionListener.java | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java b/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java index 0f80aa137..5ff8e58d7 100644 --- a/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java +++ b/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.IOException; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -100,6 +101,11 @@ public void handleEvent(PersonArrivalEvent event) { } } + private record PlanHistoryEntry(int hash, int modifiedIteration) { + } + + private final Map planHistory = new HashMap<>(); + @Override public void notifyIterationEnds(IterationEndsEvent event) { try { @@ -151,18 +157,14 @@ public void notifyIterationEnds(IterationEndsEvent event) { // START TODO: This can be much simplified if Plan.getIterationCreated works by // default: https://github.com/matsim-org/matsim-libs/issues/4762 - Integer planHash = (Integer) plan.getAttributes().getAttribute("travelTimeHash"); - Integer planIteration = (Integer) plan.getAttributes().getAttribute("travelTimeIteration"); - - final int age; - if (planHash == null || planHash != plan.hashCode()) { - age = 0; - plan.getAttributes().putAttribute("travelTimeHash", plan.hashCode()); - plan.getAttributes().putAttribute("travelTimeIteration", event.getIteration()); - } else { - age = event.getIteration() - planIteration; + PlanHistoryEntry planHistoryEntry = planHistory.get(plan); + if (planHistoryEntry == null || planHistoryEntry.hash != plan.hashCode()) { + planHistoryEntry = new PlanHistoryEntry(plan.hashCode(), event.getIteration()); + planHistory.put(plan, planHistoryEntry); } + int age = event.getIteration() - planHistoryEntry.modifiedIteration; + // END TODO boolean isRecent = age == 0; @@ -313,5 +315,21 @@ public void notifyIterationEnds(IterationEndsEvent event) { } catch (IOException e) { throw new RuntimeException(e); } + + // TODO: Clean-up plan history + // Not required if code is updated (see above) + // (we do this cleanup to free the backreferences for GC) + + Set existingPlans = new HashSet<>(); + for (Person person : population.getPersons().values()) { + for (Plan plan : person.getPlans()) { + existingPlans.add(plan); + } + } + + Set removePlans = new HashSet<>(planHistory.keySet()); + removePlans.removeAll(existingPlans); + + removePlans.forEach(planHistory::remove); } } From c79588bd3eead4e0501dee4eb262fd2403d51246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=B6rl?= Date: Mon, 2 Mar 2026 13:56:57 +0100 Subject: [PATCH 6/8] update --- ...arisionListener.java => TravelTimeComparisonListener.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/{TravelTimeComparisionListener.java => TravelTimeComparisonListener.java} (99%) diff --git a/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java b/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisonListener.java similarity index 99% rename from core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java rename to core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisonListener.java index 5ff8e58d7..2780cedb9 100644 --- a/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisionListener.java +++ b/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisonListener.java @@ -32,7 +32,7 @@ import org.matsim.core.utils.timing.TimeInterpretation; import org.matsim.core.utils.timing.TimeTracker; -public class TravelTimeComparisionListener +public class TravelTimeComparisonListener implements IterationStartsListener, IterationEndsListener, PersonDepartureEventHandler, PersonArrivalEventHandler { static public final String DETAILED_OUTPUT_NAME = "detailed_travel_time_comparison.csv"; @@ -52,7 +52,7 @@ public class TravelTimeComparisionListener private final Map>> trackedTimes = new HashMap<>(); private final IdMap ongoing = new IdMap<>(Person.class); - public TravelTimeComparisionListener(Population population, TimeInterpretation timeInterpretation, + public TravelTimeComparisonListener(Population population, TimeInterpretation timeInterpretation, OutputDirectoryHierarchy outputDirectoryHierarchy, EventsManager eventsManager, int analysisInterval, int detailedAnalysisInterval, Set modes) { this.population = population; From 6fa61f7eb7b427c3080ceffaf252b5770b091ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=B6rl?= Date: Mon, 2 Mar 2026 13:57:32 +0100 Subject: [PATCH 7/8] update --- .../core/simulation/analysis/EqasimAnalysisModule.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java b/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java index 923839255..0450fad7f 100644 --- a/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java +++ b/core/src/main/java/org/eqasim/core/simulation/analysis/EqasimAnalysisModule.java @@ -12,7 +12,7 @@ import org.eqasim.core.components.travel_time.TravelTimeRecorder; import org.eqasim.core.scenario.cutter.network.RoadNetwork; import org.eqasim.core.simulation.analysis.stuck.StuckAnalysisModule; -import org.eqasim.core.simulation.analysis.travel_time.TravelTimeComparisionListener; +import org.eqasim.core.simulation.analysis.travel_time.TravelTimeComparisonListener; import org.eqasim.core.simulation.modes.drt.analysis.DrtAnalysisModule; import org.matsim.api.core.v01.network.Network; import org.matsim.api.core.v01.population.Population; @@ -49,7 +49,7 @@ public void install() { install(new StuckAnalysisModule()); bind(AnalysisMainModeIdentifier.class).toInstance(new RoutingModeMainModeIdentifier()); - addControlerListenerBinding().to(TravelTimeComparisionListener.class); + addControlerListenerBinding().to(TravelTimeComparisonListener.class); } @Provides @@ -91,11 +91,11 @@ public TravelTimeRecorder travelTimeRecorder(Network network, Config config) { @Provides @Singleton - public TravelTimeComparisionListener provideTravelTimeComparisionListener(Population population, + public TravelTimeComparisonListener provideTravelTimeComparisionListener(Population population, TimeInterpretation timeInterpretation, OutputDirectoryHierarchy outputDirectoryHierarchy, EventsManager eventsManager, EqasimConfigGroup eqasimConfig, RoutingConfigGroup routingConfig, ControllerConfigGroup controllerConfig) { - return new TravelTimeComparisionListener(population, timeInterpretation, outputDirectoryHierarchy, + return new TravelTimeComparisonListener(population, timeInterpretation, outputDirectoryHierarchy, eventsManager, eqasimConfig.getTravelTimeAnalysisInterval(), eqasimConfig.getAnalysisInterval(), new HashSet<>(routingConfig.getNetworkModes())); } From 9ba9cfe063a7d6982f784bdb019f750611b2ba5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=B6rl?= Date: Mon, 2 Mar 2026 14:28:40 +0100 Subject: [PATCH 8/8] simplify car analysis --- .../TravelTimeComparisonListener.java | 83 ++++++++++++------- 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisonListener.java b/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisonListener.java index 2780cedb9..8308b2deb 100644 --- a/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisonListener.java +++ b/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisonListener.java @@ -13,6 +13,7 @@ import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; import org.matsim.api.core.v01.Id; import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.TransportMode; import org.matsim.api.core.v01.events.PersonArrivalEvent; import org.matsim.api.core.v01.events.PersonDepartureEvent; import org.matsim.api.core.v01.events.handler.PersonArrivalEventHandler; @@ -38,6 +39,7 @@ public class TravelTimeComparisonListener static public final String DETAILED_OUTPUT_NAME = "detailed_travel_time_comparison.csv"; static public final String HOURLY_OUTPUT_NAME = "hourly_travel_time_comparison.csv"; static public final String OVERALL_OUTPUT_NAME = "travel_time_comparison.csv"; + static public final String CAR_OUTPUT_NAME = "car_travel_time_comparison.csv"; private final Population population; private final TimeInterpretation timeInterpretation; @@ -106,8 +108,33 @@ private record PlanHistoryEntry(int hash, int modifiedIteration) { private final Map planHistory = new HashMap<>(); + private void updatePlanHistory(int iteration) { + // TODO: This can be much simplified if Plan.getIterationCreated works by + // default: https://github.com/matsim-org/matsim-libs/issues/4762 + + Set existingPlans = new HashSet<>(); + for (Person person : population.getPersons().values()) { + for (Plan plan : person.getPlans()) { + existingPlans.add(plan); + + PlanHistoryEntry planHistoryEntry = planHistory.get(plan); + if (planHistoryEntry == null || planHistoryEntry.hash != plan.hashCode()) { + planHistoryEntry = new PlanHistoryEntry(plan.hashCode(), iteration); + planHistory.put(plan, planHistoryEntry); + } + } + } + + Set removePlans = new HashSet<>(planHistory.keySet()); + removePlans.removeAll(existingPlans); + + removePlans.forEach(planHistory::remove); + } + @Override public void notifyIterationEnds(IterationEndsEvent event) { + updatePlanHistory(event.getIteration()); + try { if (analysisInterval > 0 && (event.getIteration() % analysisInterval == 0 || event.isLastIteration())) { eventsManager.removeHandler(this); @@ -154,19 +181,7 @@ public void notifyIterationEnds(IterationEndsEvent event) { Plan plan = person.getSelectedPlan(); - // START TODO: This can be much simplified if Plan.getIterationCreated works by - // default: https://github.com/matsim-org/matsim-libs/issues/4762 - - PlanHistoryEntry planHistoryEntry = planHistory.get(plan); - if (planHistoryEntry == null || planHistoryEntry.hash != plan.hashCode()) { - planHistoryEntry = new PlanHistoryEntry(plan.hashCode(), event.getIteration()); - planHistory.put(plan, planHistoryEntry); - } - - int age = event.getIteration() - planHistoryEntry.modifiedIteration; - - // END TODO - + int age = event.getIteration() - planHistory.get(plan).modifiedIteration; boolean isRecent = age == 0; for (PlanElement element : plan.getPlanElements()) { @@ -311,25 +326,35 @@ public void notifyIterationEnds(IterationEndsEvent event) { } overallWriter.close(); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - // TODO: Clean-up plan history - // Not required if code is updated (see above) - // (we do this cleanup to free the backreferences for GC) + if (modes.contains(TransportMode.car)) { + writeHeader = !new File(outputHierarchy.getOutputFilename(CAR_OUTPUT_NAME)).exists(); + BufferedWriter carWriter = IOUtils + .getAppendingBufferedWriter(outputHierarchy.getOutputFilename(CAR_OUTPUT_NAME)); - Set existingPlans = new HashSet<>(); - for (Person person : population.getPersons().values()) { - for (Plan plan : person.getPlans()) { - existingPlans.add(plan); - } - } + if (writeHeader) { + carWriter.write(String.join(";", new String[] { + "iteration", "observations", "mean", "median", "q10", "q90", "std" + }) + "\n"); + } - Set removePlans = new HashSet<>(planHistory.keySet()); - removePlans.removeAll(existingPlans); + DescriptiveStatistics carSummary = overallSummary.get(TransportMode.car); - removePlans.forEach(planHistory::remove); + carWriter.write(String.join(";", new String[] { // + String.valueOf(event.getIteration()), // + String.valueOf(carSummary.getN()), // + String.valueOf(carSummary.getMean()), // + String.valueOf(carSummary.getPercentile(50)), // + String.valueOf(carSummary.getPercentile(10)), // + String.valueOf(carSummary.getPercentile(90)), // + String.valueOf(carSummary.getStandardDeviation()), // + }) + "\n"); + + carWriter.close(); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } } }