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/2] 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/2] 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())); } }