Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Non-deterministic behaviour of SwissRailRaptor #3568

Merged
merged 6 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.matsim.contrib.discrete_mode_choice;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.Test;
import org.matsim.api.core.v01.Scenario;
import org.matsim.contribs.discrete_mode_choice.modules.DiscreteModeChoiceModule;
import org.matsim.contribs.discrete_mode_choice.modules.ModeAvailabilityModule;
import org.matsim.contribs.discrete_mode_choice.modules.config.DiscreteModeChoiceConfigGroup;
import org.matsim.core.config.Config;
import org.matsim.core.config.ConfigUtils;
import org.matsim.core.controler.Controler;
import org.matsim.core.controler.OutputDirectoryHierarchy;
import org.matsim.core.gbl.MatsimRandom;
import org.matsim.core.scenario.ScenarioUtils;
import org.matsim.core.utils.io.IOUtils;
import org.matsim.core.utils.misc.CRCChecksum;
import org.matsim.examples.ExamplesUtils;

import java.net.URL;

public class DeterminismTest {
private static void runConfig(URL configUrl, String outputDirectory) {
Config config = ConfigUtils.loadConfig(configUrl, new DiscreteModeChoiceConfigGroup());
config.controller().setLastIteration(2);
config.controller().setOutputDirectory(outputDirectory);
config.controller().setOverwriteFileSetting(OutputDirectoryHierarchy.OverwriteFileSetting.deleteDirectoryIfExists);
Scenario scenario = ScenarioUtils.createScenario(config);
ScenarioUtils.loadScenario(scenario);
Controler controler = new Controler(scenario);
controler.addOverridingModule(new DiscreteModeChoiceModule());
controler.addOverridingModule(new ModeAvailabilityModule());
controler.run();
}


@Test
public void testSimulationDeterminism() {
Logger logger = LogManager.getLogger(DeterminismTest.class);
logger.info("Testing simulation determinism");
URL configUrl = IOUtils.extendUrl(ExamplesUtils.getTestScenarioURL("siouxfalls-2014"), "config_default.xml");
int samples = 10;
for(int i=0; i<samples; i++) {
String outputDirectory = "test_determinism/output_"+i;
MatsimRandom.reset();
runConfig(configUrl, outputDirectory);
String referencePlans = "test_determinism/output_0/output_plans.xml.gz";
String comparedPlans = outputDirectory + "/output_plans.xml.gz";
String referenceEvents = "test_determinism/output_0/output_events.xml.gz";
String comparedEvents = outputDirectory + "/output_events.xml.gz";
assert i == 0 || CRCChecksum.getCRCFromFile(referencePlans) == CRCChecksum.getCRCFromFile(comparedPlans);
assert i == 0 || CRCChecksum.getCRCFromFile(referenceEvents) == CRCChecksum.getCRCFromFile(comparedEvents);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,7 @@
import org.matsim.pt.transitSchedule.api.TransitStopFacility;
import org.matsim.vehicles.Vehicle;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.*;

/**
* The actual RAPTOR implementation, based on Delling et al, Round-Based Public Transit Routing.
Expand Down Expand Up @@ -118,7 +110,8 @@ public RaptorRoute calcLeastCostRoute(double depTime, Facility fromFacility, Fac
reset();
CachingTransferProvider transferProvider = this.data.new CachingTransferProvider();

Map<TransitStopFacility, InitialStop> destinationStops = new HashMap<>();
// Using a LinkedHashMap instead of a regular HashMap here is necessary to have a deterministic behaviour
Map<TransitStopFacility, InitialStop> destinationStops = new LinkedHashMap<>();

// go through all egressStops; check if already in destinationStops; if so, check if current cost is smaller; if so, then replace. This can
// presumably happen when the same stop can be reached at lower cost by a different egress mode. (*)
Expand All @@ -141,7 +134,8 @@ public RaptorRoute calcLeastCostRoute(double depTime, Facility fromFacility, Fac
}

// same as (*) for access stops:
Map<TransitStopFacility, InitialStop> initialStops = new HashMap<>();
// Also, using a LinkedHashMap instead of a regular HashMap here is necessary to have a deterministic behaviour
Map<TransitStopFacility, InitialStop> initialStops = new LinkedHashMap<>();
for (InitialStop accessStop : accessStops) {
InitialStop alternative = initialStops.get(accessStop.stop);
if (alternative == null || accessStop.accessCost < alternative.accessCost) {
Expand Down Expand Up @@ -833,7 +827,7 @@ private void handleTransfers(boolean strict, RaptorParameters raptorParams, Cach
final int firstTransferIndex;
final int lastTransferIndex;
final RTransfer[] transfers;

if (!useAdaptiveTransferCalculation) {
// efficient lookup from the precomputed transfer candidates
transfers = this.data.transfers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,7 @@

package ch.sbb.matsim.routing.pt.raptor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.function.Supplier;

import javax.annotation.Nullable;
Expand Down Expand Up @@ -148,7 +138,8 @@ public static SwissRailRaptorData create(TransitSchedule schedule, @Nullable Veh
// enumerate TransitStopFacilities along their usage in transit routes to (hopefully) achieve a better memory locality
// well, I'm not even sure how often we'll need the transit stop facilities, likely we'll use RouteStops more often
Map<TransitStopFacility, Integer> stopFacilityIndices = new HashMap<>((int) (schedule.getFacilities().size() * 1.5));
Map<TransitStopFacility, int[]> routeStopsPerStopFacility = new HashMap<>();
// Using a LinkedHashMap instead of a regular HashMap here is necessary to have a deterministic behaviour
Map<TransitStopFacility, int[]> routeStopsPerStopFacility = new LinkedHashMap<>();

boolean useModeMapping = staticConfig.isUseModeMappingForPassengers();
for (TransitLine line : schedule.getTransitLines().values()) {
Expand Down
207 changes: 207 additions & 0 deletions matsim/src/test/java/ch/sbb/matsim/RaptorDeterminismTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package ch.sbb.matsim;

import ch.sbb.matsim.routing.pt.raptor.SwissRailRaptorData;
import ch.sbb.matsim.routing.pt.raptor.RaptorStopFinder;
import ch.sbb.matsim.routing.pt.raptor.RaptorParametersForPerson;
import ch.sbb.matsim.routing.pt.raptor.SwissRailRaptor;
import ch.sbb.matsim.routing.pt.raptor.RaptorParameters;
import ch.sbb.matsim.routing.pt.raptor.InitialStop;
import com.google.inject.Injector;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.Test;
import org.matsim.api.core.v01.Scenario;
import org.matsim.api.core.v01.population.*;
import org.matsim.core.config.Config;
import org.matsim.core.config.ConfigUtils;
import org.matsim.core.controler.AbstractModule;
import org.matsim.core.events.EventsManagerModule;
import org.matsim.core.router.TripRouter;
import org.matsim.core.router.TripRouterModule;
import org.matsim.core.router.TripStructureUtils;
import org.matsim.core.router.costcalculators.TravelDisutilityModule;
import org.matsim.core.scenario.ScenarioByInstanceModule;
import org.matsim.core.scenario.ScenarioUtils;
import org.matsim.core.trafficmonitoring.TravelTimeCalculatorModule;
import org.matsim.core.utils.io.IOUtils;
import org.matsim.core.utils.timing.TimeInterpretationModule;
import org.matsim.examples.ExamplesUtils;
import org.matsim.facilities.ActivityFacilities;
import org.matsim.facilities.FacilitiesUtils;
import org.matsim.facilities.Facility;
import org.matsim.pt.routes.DefaultTransitPassengerRoute;
import org.matsim.pt.transitSchedule.api.TransitStopFacility;


import java.net.URL;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

public class RaptorDeterminismTest {

public static boolean comparePlan(List<? extends PlanElement> left, List<? extends PlanElement> right) {
if(left.size() != right.size()) {
return false;
}
for(int i=0; i<left.size(); i++) {
PlanElement leftElement = left.get(i);
PlanElement rightElement = right.get(i);
if(!leftElement.getClass().equals(rightElement.getClass())) {
return false;
}
if(leftElement instanceof Activity leftActivity && rightElement instanceof Activity rightActivity) {
if(!leftActivity.getEndTime().equals(rightActivity.getEndTime())) {
return false;
}
if(!leftActivity.getType().equals(rightActivity.getType())) {
return false;
}
if(!Optional.ofNullable(leftActivity.getLinkId()).equals(Optional.ofNullable(rightActivity.getLinkId()))) {
return false;
}
if(!leftActivity.getStartTime().equals(rightActivity.getStartTime())) {
return false;
}
if(!leftActivity.getMaximumDuration().equals(rightActivity.getMaximumDuration())) {
return false;
}
} else if (leftElement instanceof Leg leftLeg && rightElement instanceof Leg rightLeg) {
if(!leftLeg.getMode().equals(rightLeg.getMode())) {
return false;
}
if(!leftLeg.getTravelTime().equals(rightLeg.getTravelTime())) {
return false;
}
if(!leftLeg.getMode().equals("pt")) {
continue;
}
Route leftRoute = leftLeg.getRoute();
Route rightRoute = leftLeg.getRoute();
if(leftRoute instanceof DefaultTransitPassengerRoute leftTransitPassengerRoute && rightRoute instanceof DefaultTransitPassengerRoute rightTransitPassengerRoute) {
if(!leftTransitPassengerRoute.toString().equals(rightTransitPassengerRoute.toString())) {
return false;
}
if(!leftTransitPassengerRoute.getRouteId().equals(rightTransitPassengerRoute.getRouteId())) {
return false;
}
if(!leftTransitPassengerRoute.getAccessStopId().equals(rightTransitPassengerRoute.getAccessStopId())) {
return false;
}
if(!leftTransitPassengerRoute.getEgressStopId().equals(rightTransitPassengerRoute.getEgressStopId())) {
return false;
}
} else {
throw new IllegalStateException();
}
}
}
return true;
}


@Test
public void testRaptorDeterminism() {
Logger logger = LogManager.getLogger(RaptorDeterminismTest.class);
logger.info("Testing raptor determinism");
URL configUrl = IOUtils.extendUrl(ExamplesUtils.getTestScenarioURL("siouxfalls-2014"), "config_default.xml");
Config config = ConfigUtils.loadConfig(configUrl);
int scenarioSamples = 10;
Scenario[] scenarios = new Scenario[scenarioSamples];
RaptorStopFinder[] raptorStopFinders = new RaptorStopFinder[scenarioSamples];
ActivityFacilities[] activityFacilities = new ActivityFacilities[scenarioSamples];
RaptorParametersForPerson[] raptorParametersForPerson = new RaptorParametersForPerson[scenarioSamples];
SwissRailRaptor[] swissRailRaptors = new SwissRailRaptor[scenarioSamples];
SwissRailRaptorData[] swissRailRaptorData = new SwissRailRaptorData[scenarioSamples];
List<TransitStopFacility>[] transitStopFacilitiesByScenario = new List[scenarioSamples];
List<? extends Person>[] personLists = new List[scenarioSamples];
TripRouter[] tripRouters = new TripRouter[scenarioSamples];

logger.info(String.format("Loading scenario %d times", scenarioSamples));
for(int scenarioIndex=0; scenarioIndex<scenarioSamples; scenarioIndex++) {
Scenario scenario = ScenarioUtils.createScenario(config);
ScenarioUtils.loadScenario(scenario);

// SwissRailRaptorModule is installed by TransitRouterModule which installed by TripRouterModule
AbstractModule mainModule = AbstractModule.override(List.of(new TripRouterModule(), new EventsManagerModule(), new TimeInterpretationModule(), new TravelDisutilityModule(), new TravelTimeCalculatorModule()), new ScenarioByInstanceModule(scenario));

Injector injector = org.matsim.core.controler.Injector.createInjector(config, mainModule);

raptorStopFinders[scenarioIndex] = injector.getInstance(RaptorStopFinder.class);
activityFacilities[scenarioIndex] = injector.getInstance(ActivityFacilities.class);
raptorParametersForPerson[scenarioIndex] = injector.getInstance(RaptorParametersForPerson.class);
swissRailRaptors[scenarioIndex] = injector.getInstance(SwissRailRaptor.class);
scenarios[scenarioIndex] = scenario;
transitStopFacilitiesByScenario[scenarioIndex] = new ArrayList<>(scenario.getTransitSchedule().getFacilities().values());
swissRailRaptorData[scenarioIndex] = swissRailRaptors[scenarioIndex].getUnderlyingData();
personLists[scenarioIndex] = scenario.getPopulation().getPersons().values().stream().toList();

tripRouters[scenarioIndex] = injector.getInstance(TripRouter.class);
}

logger.info(String.format("Comparing stop facilities order %d", scenarioSamples));

for(int scenarioIndex=1; scenarioIndex<scenarioSamples; scenarioIndex++) {
for(int i=0; i<transitStopFacilitiesByScenario[0].size(); i++) {
assert transitStopFacilitiesByScenario[0].get(i).getId().equals(transitStopFacilitiesByScenario[scenarioIndex].get(i).getId());
}
}

logger.info("Comparing stop RaptorStopFinder.findStops alongSide ptRouter.calcRoute(pt, ...)");

for(int personIndex=0; personIndex<personLists[0].size(); personIndex++) {
if(personIndex % 1000 == 0) {
logger.info(String.format("Person %d", personIndex));
}
Person referencePerson = personLists[0].get(personIndex);
RaptorParameters referenceRaptorParameters = raptorParametersForPerson[0].getRaptorParameters(referencePerson);

for(TripStructureUtils.Trip referenceTrip: TripStructureUtils.getTrips(referencePerson.getSelectedPlan())) {
Facility fromFacility = FacilitiesUtils.toFacility(referenceTrip.getOriginActivity(), activityFacilities[0]);
Facility toFacility = FacilitiesUtils.toFacility(referenceTrip.getDestinationActivity(), activityFacilities[0]);

List<? extends PlanElement> referenceElements = tripRouters[0].calcRoute("pt", fromFacility, toFacility, referenceTrip.getOriginActivity().getEndTime().seconds(), referencePerson, referenceTrip.getTripAttributes());

for(int scenarioIndex=1; scenarioIndex<scenarioSamples; scenarioIndex++) {

assert personLists[scenarioIndex].get(personIndex).getId().equals(referencePerson.getId());
Person otherPerson = scenarios[scenarioIndex].getPopulation().getPersons().get(referencePerson.getId());

RaptorParameters otherRaptorParameters = raptorParametersForPerson[scenarioIndex].getRaptorParameters(referencePerson);
Facility otherFromFacility = FacilitiesUtils.toFacility(referenceTrip.getOriginActivity(), activityFacilities[scenarioIndex]);
Facility otherToFacility = FacilitiesUtils.toFacility(referenceTrip.getDestinationActivity(), activityFacilities[scenarioIndex]);

assert otherFromFacility.getCoord().equals(fromFacility.getCoord());
assert otherToFacility.getCoord().equals(toFacility.getCoord());

// We specifically test the RaptorStopFinder
for(RaptorStopFinder.Direction direction: RaptorStopFinder.Direction.values()) {
List<InitialStop> referenceInitialStops = raptorStopFinders[0].findStops(fromFacility, toFacility, referencePerson, referenceTrip.getOriginActivity().getEndTime().seconds(), referenceTrip.getTripAttributes(), referenceRaptorParameters, swissRailRaptorData[0], direction);
List<InitialStop> sortedReferenceInitialStops = new ArrayList<>(referenceInitialStops);
sortedReferenceInitialStops.sort(Comparator.comparing(InitialStop::toString));

List<InitialStop> comparedInitialStops = raptorStopFinders[scenarioIndex].findStops(otherFromFacility, otherToFacility, referencePerson, referenceTrip.getOriginActivity().getEndTime().seconds(), referenceTrip.getTripAttributes(), otherRaptorParameters, swissRailRaptorData[scenarioIndex], direction);

assert referenceInitialStops.size() == comparedInitialStops.size();

List<InitialStop> sortedComparedInitialStops = new ArrayList<>(comparedInitialStops);
sortedComparedInitialStops.sort(Comparator.comparing(InitialStop::toString));
for(int j=0; j<referenceInitialStops.size(); j++) {
assert sortedReferenceInitialStops.get(j).toString().equals(sortedComparedInitialStops.get(j).toString());
}
for(int j=0; j<referenceInitialStops.size(); j++) {
assert referenceInitialStops.get(j).toString().equals(comparedInitialStops.get(j).toString());
}
}

// Then we test the elements return by raptor

List<? extends PlanElement> comparedElements = tripRouters[scenarioIndex].calcRoute("pt", otherFromFacility, otherToFacility, referenceTrip.getOriginActivity().getEndTime().seconds(), otherPerson, referenceTrip.getTripAttributes());
assert comparePlan(referenceElements, comparedElements);
}
}
}
}

}
Loading