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 9ca90df98..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 @@ -26,8 +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 TRAVEL_TIME_ANALYSIS_INTERVAL = "travelTimeAnalysisInterval"; private final static String USE_SCHEDULE_BASED_TRANSPORT = "useScheduleBasedTransport"; @@ -45,8 +46,9 @@ public class EqasimConfigGroup extends ReflectiveConfigGroup { private int analysisInterval = 0; private DistanceUnit analysisDistanceUnit = DistanceUnit.meter; - + private int travelTimeRecordingInterval = 0; + private int travelTimeAnalysisInterval = 10; private boolean useScheduleBasedTransport = true; @@ -57,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; } @@ -97,15 +99,25 @@ public void setUsePseudoRandomErrors(boolean usePseudoRandomErrors) { this.usePseudoRandomErrors = usePseudoRandomErrors; } + @StringGetter(TRAVEL_TIME_ANALYSIS_INTERVAL) + public int getTravelTimeAnalysisInterval() { + return travelTimeAnalysisInterval; + } + + @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 71b9cea09..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 @@ -1,21 +1,31 @@ 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.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; 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; 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 +47,9 @@ public void install() { } install(new StuckAnalysisModule()); - + bind(AnalysisMainModeIdentifier.class).toInstance(new RoutingModeMainModeIdentifier()); + addControlerListenerBinding().to(TravelTimeComparisonListener.class); } @Provides @@ -59,7 +70,7 @@ public PublicTransportLegListener providePublicTransportListener(Network network PersonAnalysisFilter personFilter) { return new PublicTransportLegListener(schedule); } - + @Provides @Singleton public ActivityListener provideActivityListener(PersonAnalysisFilter personFilter) { @@ -77,4 +88,15 @@ public TravelTimeRecorder travelTimeRecorder(Network network, Config config) { } return new TravelTimeRecorder(new RoadNetwork(network), startTime, stopTime, 600); } + + @Provides + @Singleton + public TravelTimeComparisonListener provideTravelTimeComparisionListener(Population population, + TimeInterpretation timeInterpretation, + OutputDirectoryHierarchy outputDirectoryHierarchy, EventsManager eventsManager, + EqasimConfigGroup eqasimConfig, RoutingConfigGroup routingConfig, ControllerConfigGroup controllerConfig) { + return new TravelTimeComparisonListener(population, timeInterpretation, outputDirectoryHierarchy, + eventsManager, eqasimConfig.getTravelTimeAnalysisInterval(), eqasimConfig.getAnalysisInterval(), + new HashSet<>(routingConfig.getNetworkModes())); + } } 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 new file mode 100644 index 000000000..8308b2deb --- /dev/null +++ b/core/src/main/java/org/eqasim/core/simulation/analysis/travel_time/TravelTimeComparisonListener.java @@ -0,0 +1,360 @@ +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.HashSet; +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.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; +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; +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 TravelTimeComparisonListener + 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"; + static public final String CAR_OUTPUT_NAME = "car_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 TravelTimeComparisonListener(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)); + } + } + + 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); + + 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)) + : 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", "planned_age" + }) + "\n"); + } + + for (Person person : population.getPersons().values()) { + TimeTracker timeTracker = new TimeTracker(timeInterpretation); + int legIndex = 0; + + Plan plan = person.getSelectedPlan(); + + int age = event.getIteration() - planHistory.get(plan).modifiedIteration; + 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()); + + 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), // + String.valueOf(age) // + }) + "\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); + + 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); + } + } + } + + 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", "is_recent" + }) + "\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()), // + "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"); + } + } + + 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", "observations", "mean", "median", "q10", "q90", "std", "is_recent" + }) + "\n"); + } + + for (String mode : modes) { + DescriptiveStatistics summary = overallSummary.get(mode); + + overallWriter.write(String.join(";", new String[] { // + String.valueOf(event.getIteration()), // + 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()), // + "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"); + } + + overallWriter.close(); + + if (modes.contains(TransportMode.car)) { + writeHeader = !new File(outputHierarchy.getOutputFilename(CAR_OUTPUT_NAME)).exists(); + BufferedWriter carWriter = IOUtils + .getAppendingBufferedWriter(outputHierarchy.getOutputFilename(CAR_OUTPUT_NAME)); + + if (writeHeader) { + carWriter.write(String.join(";", new String[] { + "iteration", "observations", "mean", "median", "q10", "q90", "std" + }) + "\n"); + } + + DescriptiveStatistics carSummary = overallSummary.get(TransportMode.car); + + 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); + } + } +}