From 81a0bb100e438139fe8a6f8d4d91bb52921779b1 Mon Sep 17 00:00:00 2001 From: tkchouaki Date: Tue, 5 Mar 2024 08:59:16 +0100 Subject: [PATCH] Feat: A more advanced standalone mode choice (#195) * feat: RunStandaloneModeChoice * feat: PublicTransportLegReaderFromPopulation * fix: input csv file names --------- Co-authored-by: Tarek Chouaki --- ...ublicTransportLegReaderFromPopulation.java | 77 +++++ .../routing/RunPopulationRouting.java | 4 +- .../eqasim/ile_de_france/RunModeChoice.java | 218 -------------- .../RunStandaloneModeChoice.java | 277 ++++++++++++++++++ .../StandaloneModeChoiceConfigGroup.java | 40 +++ .../StandaloneModeChoiceModule.java | 101 +++++++ .../StandaloneModeChoicePerformer.java | 182 ++++++++++++ .../eqasim/ile_de_france/TestCorisica.java | 12 + 8 files changed, 691 insertions(+), 220 deletions(-) create mode 100644 core/src/main/java/org/eqasim/core/analysis/pt/PublicTransportLegReaderFromPopulation.java delete mode 100644 ile_de_france/src/main/java/org/eqasim/ile_de_france/RunModeChoice.java create mode 100644 ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/RunStandaloneModeChoice.java create mode 100644 ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/StandaloneModeChoiceConfigGroup.java create mode 100644 ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/StandaloneModeChoiceModule.java create mode 100644 ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/StandaloneModeChoicePerformer.java diff --git a/core/src/main/java/org/eqasim/core/analysis/pt/PublicTransportLegReaderFromPopulation.java b/core/src/main/java/org/eqasim/core/analysis/pt/PublicTransportLegReaderFromPopulation.java new file mode 100644 index 000000000..e20e37a4c --- /dev/null +++ b/core/src/main/java/org/eqasim/core/analysis/pt/PublicTransportLegReaderFromPopulation.java @@ -0,0 +1,77 @@ +package org.eqasim.core.analysis.pt; + +import org.eqasim.core.analysis.PersonAnalysisFilter; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.TransportMode; +import org.matsim.api.core.v01.population.*; +import org.matsim.core.config.Config; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.population.io.PopulationReader; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.core.scenario.ScenarioUtils; +import org.matsim.pt.routes.DefaultTransitPassengerRoute; +import org.matsim.pt.transitSchedule.api.TransitSchedule; +import org.matsim.pt.transitSchedule.api.TransitStopArea; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class PublicTransportLegReaderFromPopulation { + + final private PersonAnalysisFilter personAnalysisFilter; + final private TransitSchedule transitSchedule; + + public PublicTransportLegReaderFromPopulation(TransitSchedule transitSchedule, PersonAnalysisFilter personAnalysisFilter) { + this.personAnalysisFilter = personAnalysisFilter; + this.transitSchedule = transitSchedule; + + } + + public Collection readPublicTransportLegs(String populationFilePath) { + Config config = ConfigUtils.createConfig(); + Scenario scenario = ScenarioUtils.createScenario(config); + new PopulationReader(scenario).readFile(populationFilePath); + return this.readPublicTransportLegs(scenario.getPopulation()); + } + + public Collection readPublicTransportLegs(Population population) { + return population.getPersons().values().stream().flatMap(person -> getPublicTransportLegs(person).stream()).collect(Collectors.toList()); + } + + public Collection getPublicTransportLegs(Person person) { + List legItems = new ArrayList<>(); + Plan plan = person.getSelectedPlan(); + int tripIndex = -1; + int legIndex = -1; + for(PlanElement planElement: plan.getPlanElements()) { + if(planElement instanceof Activity) { + Activity activity = (Activity) planElement; + if(!TripStructureUtils.isStageActivityType(activity.getType())) { + tripIndex +=1; + } + continue; + } + Leg leg = (Leg) planElement; + legIndex=+1; + if(!leg.getMode().equals(TransportMode.pt)) { + continue; + } + if(! (leg.getRoute() instanceof DefaultTransitPassengerRoute)) { + throw new IllegalStateException("PT leg has invalid route type"); + } + DefaultTransitPassengerRoute transitPassengerRoute = (DefaultTransitPassengerRoute) leg.getRoute(); + + Id accessStopAreaId = this.transitSchedule.getFacilities().get(transitPassengerRoute.getAccessStopId()).getStopAreaId(); + Id egressStopAreaId = this.transitSchedule.getFacilities().get(transitPassengerRoute.getEgressStopId()).getStopAreaId(); + String mode = this.transitSchedule.getTransitLines().get(transitPassengerRoute.getLineId()).getRoutes().get(transitPassengerRoute.getRouteId()).getTransportMode(); + + PublicTransportLegItem item = new PublicTransportLegItem(person.getId(), tripIndex, legIndex, transitPassengerRoute.getAccessStopId(), transitPassengerRoute.getEgressStopId(), transitPassengerRoute.getLineId(), transitPassengerRoute.getRouteId(), accessStopAreaId, egressStopAreaId, mode); + legItems.add(item); + } + return legItems; + } + +} diff --git a/core/src/main/java/org/eqasim/core/scenario/routing/RunPopulationRouting.java b/core/src/main/java/org/eqasim/core/scenario/routing/RunPopulationRouting.java index ffca00864..dafae98ff 100644 --- a/core/src/main/java/org/eqasim/core/scenario/routing/RunPopulationRouting.java +++ b/core/src/main/java/org/eqasim/core/scenario/routing/RunPopulationRouting.java @@ -77,7 +77,7 @@ static public void main(String[] args) throws ConfigurationException, Interrupte new PopulationWriter(scenario.getPopulation()).write(cmd.getOptionStrict("output-path")); } - static private void insertVehicles(Config config, Scenario scenario) { + static public void insertVehicles(Config config, Scenario scenario) { if (config.qsim().getVehiclesSource().equals(VehiclesSource.defaultVehicle)) { Vehicles vehicles = scenario.getVehicles(); VehiclesFactory factory = vehicles.getFactory(); @@ -100,7 +100,7 @@ static private void insertVehicles(Config config, Scenario scenario) { } } - static private void clearVehicles(Config config, Scenario scenario) { + static public void clearVehicles(Config config, Scenario scenario) { if (config.qsim().getVehiclesSource().equals(VehiclesSource.defaultVehicle)) { for (Person person : scenario.getPopulation().getPersons().values()) { person.getAttributes().removeAttribute("vehicles"); diff --git a/ile_de_france/src/main/java/org/eqasim/ile_de_france/RunModeChoice.java b/ile_de_france/src/main/java/org/eqasim/ile_de_france/RunModeChoice.java deleted file mode 100644 index 9c97acb12..000000000 --- a/ile_de_france/src/main/java/org/eqasim/ile_de_france/RunModeChoice.java +++ /dev/null @@ -1,218 +0,0 @@ -package org.eqasim.ile_de_france; - -import com.google.inject.Injector; -import com.google.inject.Key; -import com.google.inject.name.Names; -import org.eqasim.core.analysis.DistanceUnit; -import org.eqasim.core.analysis.PersonAnalysisFilter; -import org.eqasim.core.analysis.trips.TripItem; -import org.eqasim.core.analysis.trips.TripReaderFromPopulation; -import org.eqasim.core.analysis.trips.TripWriter; -import org.eqasim.core.misc.InjectorBuilder; -import org.eqasim.core.scenario.validation.ScenarioValidator; -import org.eqasim.core.simulation.analysis.EqasimAnalysisModule; -import org.eqasim.core.simulation.mode_choice.EqasimModeChoiceModule; -import org.eqasim.ile_de_france.mode_choice.IDFModeChoiceModule; -import org.matsim.api.core.v01.Scenario; -import org.matsim.api.core.v01.network.Link; -import org.matsim.api.core.v01.population.Person; -import org.matsim.api.core.v01.population.Population; -import org.matsim.api.core.v01.population.PopulationWriter; -import org.matsim.contribs.discrete_mode_choice.modules.DiscreteModeChoiceModule; -import org.matsim.contribs.discrete_mode_choice.modules.ModelModule; -import org.matsim.core.config.CommandLine; -import org.matsim.core.config.Config; -import org.matsim.core.config.ConfigGroup; -import org.matsim.core.config.ConfigUtils; -import org.matsim.core.config.groups.StrategyConfigGroup; -import org.matsim.core.controler.AbstractModule; -import org.matsim.core.controler.ControlerDefaultsModule; -import org.matsim.core.controler.NewControlerModule; -import org.matsim.core.controler.corelisteners.ControlerDefaultCoreListenersModule; -import org.matsim.core.replanning.PlanStrategy; -import org.matsim.core.replanning.ReplanningContext; -import org.matsim.core.router.MainModeIdentifier; -import org.matsim.core.router.util.TravelTime; -import org.matsim.core.scenario.ScenarioUtils; -import org.matsim.core.trafficmonitoring.FreeSpeedTravelTime; -import org.matsim.vehicles.Vehicle; - -import java.io.*; -import java.util.*; - - -/** - * This class isolates the discrete mode choice model component of Eqasim and performs the mode choice on all the agents of a provided population - * It is meant to be ran from the command line with the following arguments - * - config-path: mandatory, the path of the MATSim config file indicating the population on which the mode choice model should be performed. This configuration also specifies the parameters of the mode choice model and other parameters. - * - output-plans-path: optional, when used, writes a plans file containing the new agent plans resulting from the mode choice - * - output-csv-path: optional, when used, writes a csv file with the header personId;tripId;mode indicating the new modes of agent trips - * - base-csv-path: optional, when used, writes a csv file containing the base modes of agent trips before performing the mode choice - * At least one of the arguments output-plans-path and output-csv-path should be used. - * Parameters in the configuration file can be overridden in the command line by using an argument of the form config:arg=value. - * E.g. --config-path=config.xml --output-plans-path=plans_out.xml.gz --output-csv-path=trip_modes_out.csv --base-csv-path=trip_modes_in.csv --config:global.numberOfThreads=10 - */ -public class RunModeChoice { - - public static class TravelTimeFactors implements TravelTime { - - private final String filePath; - private final FreeSpeedTravelTime freeSpeedTravelTime; - private List congestionSlotUpperBounds; - private List congestionSlotSpeedFactor; - private static final String CSV_SEPARATOR = ";"; - private static final String TIME_UPPER_BOUND_COLUMN = "timeUpperBound"; - private static final String CONGESTION_FACTOR_COLUMN = "travelTimeFactor"; - - public TravelTimeFactors(String filePath) { - this.filePath = filePath; - this.freeSpeedTravelTime = new FreeSpeedTravelTime(); - try { - this.readFile(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private void readFile() throws IOException { - this.congestionSlotSpeedFactor = new ArrayList<>(); - this.congestionSlotUpperBounds = new ArrayList<>(); - BufferedReader reader = new BufferedReader( - new InputStreamReader(new FileInputStream(this.filePath))); - String line; - List header = null; - while ((line = reader.readLine()) != null) { - List row = Arrays.asList(line.split(CSV_SEPARATOR)); - - if (header == null) { - header = row; - } else { - double timeUpperBound = Double.parseDouble(row.get(header.indexOf(TIME_UPPER_BOUND_COLUMN))); - double speedFactor = Double.parseDouble(row.get(header.indexOf(CONGESTION_FACTOR_COLUMN))); - if(this.congestionSlotUpperBounds.size() > 0 && this.congestionSlotUpperBounds.get(this.congestionSlotUpperBounds.size()-1) >= timeUpperBound) { - throw new IllegalStateException(); - } - this.congestionSlotUpperBounds.add(timeUpperBound); - this.congestionSlotSpeedFactor.add(speedFactor); - } - } - reader.close(); - } - - @Override - public double getLinkTravelTime(Link link, double time, Person person, Vehicle vehicle) { - int slotIndex; - for(slotIndex=this.congestionSlotSpeedFactor.size()-1; slotIndex>0 && congestionSlotUpperBounds.get(slotIndex)>time; slotIndex--); - if(slotIndex < 0) { - slotIndex=0; - } - return this.freeSpeedTravelTime.getLinkTravelTime(link, time, person, vehicle) * congestionSlotSpeedFactor.get(slotIndex); - } - } - - public static void main(String[] args) throws CommandLine.ConfigurationException { - CommandLine cmd = new CommandLine.Builder(args) // - .requireOptions("config-path") - .allowOptions("output-plans-path", "output-csv-path", "base-csv-path") - .allowOptions("travel-times-factors-path") - .build(); - - Config config = ConfigUtils.loadConfig(cmd.getOptionStrict("config-path")); - Optional outputPlansPath = cmd.getOption("output-plans-path"); - Optional outputCsvPath = cmd.getOption("output-csv-path"); - - if(outputPlansPath.isEmpty() && outputCsvPath.isEmpty()) { - throw new IllegalStateException("At least one of output-plans-path and output-csv-path should be provided"); - } - - IDFConfigurator configurator = new IDFConfigurator(); - for(ConfigGroup configGroup : configurator.getConfigGroups()) { - config.addModule(configGroup); - } - cmd.applyConfiguration(config); - - // We make sure the config is set to use DiscreteModeChoice, i.e. contains a DiscreteModeChoice module and a DiscreteModeChoice strategy settings - boolean containsDiscreteModeChoiceStrategy = false; - for(StrategyConfigGroup.StrategySettings strategySettings: config.strategy().getStrategySettings()) { - if(strategySettings.getStrategyName().equals("DiscreteModeChoice")) { - containsDiscreteModeChoiceStrategy = true; - break; - } - } - if(!containsDiscreteModeChoiceStrategy || !config.getModules().containsKey("DiscreteModeChoice")) { - throw new IllegalStateException("The config file is not set to use DiscreteModeChoice"); - } - - Scenario scenario = ScenarioUtils.createScenario(config); - ScenarioUtils.loadScenario(scenario); - - ScenarioValidator scenarioValidator = new ScenarioValidator(); - scenarioValidator.checkScenario(scenario); - - InjectorBuilder injectorBuilder = new InjectorBuilder(scenario) - .addOverridingModule(new NewControlerModule()) - .addOverridingModule(new ControlerDefaultCoreListenersModule()) - .addOverridingModule(new ControlerDefaultsModule()) - .addOverridingModule(new IDFModeChoiceModule(cmd)) - .addOverridingModule(new EqasimModeChoiceModule()) - .addOverridingModule(new EqasimAnalysisModule()) - .addOverridingModule(new ModelModule()) - .addOverridingModule(new DiscreteModeChoiceModule()); - if(cmd.hasOption("travel-times-factors-path")) { - String travelTimesFactorsPath = cmd.getOptionStrict("travel-times-factors-path"); - injectorBuilder.addOverridingModule(new AbstractModule() { - @Override - public void install() { - addTravelTimeBinding("car").toInstance(new TravelTimeFactors(travelTimesFactorsPath)); - } - }); - } - - for(AbstractModule module: configurator.getModules()) { - injectorBuilder.addOverridingModule(module); - } - Injector injector = injectorBuilder.build(); - - - Population population = injector.getInstance(Population.class); - // We init the TripReaderFromPopulation here as we might need it just below - // We retrieve the DiscreteModeChoice Strategy here - /* - * Depending on the configuration, the strategy can be set to use multiple threads or not. - * In the former case, the threads need to be created before running the strategy. - */ - - TripReaderFromPopulation tripReader = new TripReaderFromPopulation(Arrays.asList("car,pt".split(",")), injector.getInstance(MainModeIdentifier.class), injector.getInstance(PersonAnalysisFilter.class), Optional.empty(), Optional.empty()); - cmd.getOption("base-csv-path").ifPresent(s -> { - //We write the initial trip modes - Collection trips = tripReader.readTrips(population); - try { - new TripWriter(trips, DistanceUnit.meter, DistanceUnit.meter).write(s); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - - - PlanStrategy strategy = injector.getInstance(Key.get(PlanStrategy.class, Names.named("DiscreteModeChoice"))); - - strategy.init(injector.getInstance(ReplanningContext.class)); - for(Person person: population.getPersons().values()) { - strategy.run(person); - } - /* - * In the multithreaded case, the run method only adds the person to the queue of a given thread. - * We need to call the finish method to actually perform the mode choice. - */ - strategy.finish(); - outputPlansPath.ifPresent(s -> new PopulationWriter(population).write(s)); - outputCsvPath.ifPresent(s -> { - Collection trips = tripReader.readTrips(population); - try { - new TripWriter(trips, DistanceUnit.meter, DistanceUnit.meter).write(s); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } -} diff --git a/ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/RunStandaloneModeChoice.java b/ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/RunStandaloneModeChoice.java new file mode 100644 index 000000000..fffb86de9 --- /dev/null +++ b/ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/RunStandaloneModeChoice.java @@ -0,0 +1,277 @@ +package org.eqasim.ile_de_france.standalone_mode_choice; + + +import com.google.inject.Key; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Names; +import org.eqasim.core.analysis.DefaultPersonAnalysisFilter; +import org.eqasim.core.analysis.DistanceUnit; +import org.eqasim.core.analysis.PersonAnalysisFilter; +import org.eqasim.core.analysis.pt.PublicTransportLegItem; +import org.eqasim.core.analysis.pt.PublicTransportLegReaderFromPopulation; +import org.eqasim.core.analysis.pt.PublicTransportLegWriter; +import org.eqasim.core.analysis.trips.TripItem; +import org.eqasim.core.analysis.trips.TripReaderFromPopulation; +import org.eqasim.core.analysis.trips.TripWriter; +import org.eqasim.core.components.travel_time.RecordedTravelTime; +import org.eqasim.core.misc.InjectorBuilder; +import org.eqasim.core.scenario.routing.RunPopulationRouting; +import org.eqasim.core.scenario.validation.ScenarioValidator; +import org.eqasim.core.simulation.mode_choice.EqasimModeChoiceModule; +import org.eqasim.ile_de_france.IDFConfigurator; +import org.eqasim.ile_de_france.RunSimulation; +import org.eqasim.ile_de_france.mode_choice.IDFModeChoiceModule; +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Population; +import org.matsim.core.config.CommandLine; +import org.matsim.core.config.Config; +import org.matsim.core.config.ConfigGroup; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.controler.AbstractModule; +import org.matsim.core.controler.Controler; +import org.matsim.core.controler.OutputDirectoryHierarchy; +import org.matsim.core.router.MainModeIdentifier; +import org.matsim.core.router.util.TravelTime; +import org.matsim.core.scenario.ScenarioUtils; +import org.matsim.core.trafficmonitoring.FreeSpeedTravelTime; +import org.matsim.core.utils.timing.TimeInterpretationModule; +import org.matsim.pt.transitSchedule.api.TransitSchedule; +import org.matsim.vehicles.Vehicle; + +import java.io.*; +import java.nio.file.Paths; +import java.util.*; + +/** + * This class offers the functionality of running the discrete mode choice model on the whole population without having to go through the whole iterative MATSim process. It is also possible to filter-out the persons that do not have a valid alternative. + * The class requires one parameter: + * - config-path: a path to a MATSim config file + * The mode choice is performed via a StandaloneModeChoice module which is configurable via a config group. + * The StandaloneModeChoiceConfigGroup can be included in the supplied config file, if not one with the default settings is added and these settings can be set via the commandline using the config: prefix. Below the list of supported parameters: + * - outputDirectory: The directory in which the resulting plans will as well as the logfiles be written + * - removePersonsWithNoValidAlternatives: if set to true, persons with no valid alternative for at least one tour or trip will be removed in the resulting population + * More parameters can be supplied via the command line + * - write-input-csv-trips: if specified, writes out the base trips and pt legs into a csv file called input_trips.csv and input_pt_legs.csv before performing the mode choice + * - write-output-csv-trips: writes out the trips resulting from the mode choice, as well as pt legs, into csv files called output_trips.csv and output_pt_legs.csv in addition to the plans file + * - travel-times-factors-path: if provided, should point out to a csv file specifying the congestion levels on the network during the day as factors by which the free speed is divided. The file in question is a csv With a header timeUpperBound;travelTimeFactor in which the timeUpperBound should be ordered incrementally. + * - recorded-travel-times-path: mutually exclusive with the travel-times-factors-path. Points to a RecordedTravelTime file. + * - simulate-after: if set, a single-iteration simulation using the resulting population will be performed, allowing to generate the regular MATSim output files. + */ +public class RunStandaloneModeChoice { + public static class TravelTimeFactors implements TravelTime { + + private final String filePath; + private final FreeSpeedTravelTime freeSpeedTravelTime; + private List congestionSlotUpperBounds; + private List congestionSlotSpeedFactor; + private static final String CSV_SEPARATOR = ";"; + private static final String TIME_UPPER_BOUND_COLUMN = "timeUpperBound"; + private static final String CONGESTION_FACTOR_COLUMN = "travelTimeFactor"; + + public TravelTimeFactors(String filePath) { + this.filePath = filePath; + this.freeSpeedTravelTime = new FreeSpeedTravelTime(); + try { + this.readFile(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void readFile() throws IOException { + this.congestionSlotSpeedFactor = new ArrayList<>(); + this.congestionSlotUpperBounds = new ArrayList<>(); + BufferedReader reader = new BufferedReader( + new InputStreamReader(new FileInputStream(this.filePath))); + String line; + List header = null; + while ((line = reader.readLine()) != null) { + List row = Arrays.asList(line.split(CSV_SEPARATOR)); + + if (header == null) { + header = row; + } else { + double timeUpperBound = Double.parseDouble(row.get(header.indexOf(TIME_UPPER_BOUND_COLUMN))); + double speedFactor = Double.parseDouble(row.get(header.indexOf(CONGESTION_FACTOR_COLUMN))); + if(this.congestionSlotUpperBounds.size() > 0 && this.congestionSlotUpperBounds.get(this.congestionSlotUpperBounds.size()-1) >= timeUpperBound) { + throw new IllegalStateException(); + } + this.congestionSlotUpperBounds.add(timeUpperBound); + this.congestionSlotSpeedFactor.add(speedFactor); + } + } + reader.close(); + } + + @Override + public double getLinkTravelTime(Link link, double time, Person person, Vehicle vehicle) { + int slotIndex; + for(slotIndex=this.congestionSlotSpeedFactor.size()-1; slotIndex>0 && congestionSlotUpperBounds.get(slotIndex)>time; slotIndex--); + if(slotIndex < 0) { + slotIndex=0; + } + return this.freeSpeedTravelTime.getLinkTravelTime(link, time, person, vehicle) * congestionSlotSpeedFactor.get(slotIndex); + } + } + + + public static final String CMD_WRITE_INPUT_CSV = "write-input-csv-trips"; + public static final String CMD_WRITE_OUTPUT_CSV = "write-output-csv-trips"; + public static final String CMD_SIMULATE_AFTER = "simulate-after"; + public static final String CMD_CONFIG_PATH = "config-path"; + public static final String CMD_TRAVEL_TIMES_FACTORS_PATH = "travel-times-factors-path"; + public static final String CMD_RECORDED_TRAVEL_TIMES_PATH = "recorded-travel-times-path"; + + + public static void main(String[] args) throws CommandLine.ConfigurationException, InterruptedException, IOException { + CommandLine cmd = new CommandLine.Builder(args) // + .requireOptions(CMD_CONFIG_PATH) + .allowOptions(CMD_WRITE_INPUT_CSV, CMD_WRITE_OUTPUT_CSV) + .allowOptions(CMD_TRAVEL_TIMES_FACTORS_PATH, CMD_RECORDED_TRAVEL_TIMES_PATH) + .allowOptions(CMD_SIMULATE_AFTER) + .build(); + + // Loading the config + IDFConfigurator configurator = new IDFConfigurator(); + ConfigGroup[] configGroups = new ConfigGroup[configurator.getConfigGroups().length+1]; + int i=0; + for(ConfigGroup configGroup: configurator.getConfigGroups()) { + configGroups[i] = configGroup; + i++; + } + // We should add this module now so that parameters can be overridden by the commandline + configGroups[i] = new StandaloneModeChoiceConfigGroup(); + + Config config = ConfigUtils.loadConfig(cmd.getOptionStrict(CMD_CONFIG_PATH), configGroups); + configurator.addOptionalConfigGroups(config); + cmd.applyConfiguration(config); + + Optional travelTimesFactorsPath = cmd.getOption(CMD_TRAVEL_TIMES_FACTORS_PATH); + Optional recordedTravelTimesPath = cmd.getOption(CMD_RECORDED_TRAVEL_TIMES_PATH); + + + if(travelTimesFactorsPath.isPresent() && recordedTravelTimesPath.isPresent()) { + throw new IllegalStateException(String.format("Can't use the two options '%s' and '%s' simultaneously", CMD_TRAVEL_TIMES_FACTORS_PATH, CMD_RECORDED_TRAVEL_TIMES_PATH)); + } + + // We make sure the config is set to use DiscreteModeChoice, i.e. contains a DiscreteModeChoice module + if(!config.getModules().containsKey("DiscreteModeChoice")) { + throw new IllegalStateException("The config file is not set to use DiscreteModeChoice"); + } + + Scenario scenario = ScenarioUtils.createScenario(config); + ScenarioUtils.loadScenario(scenario); + + ScenarioValidator scenarioValidator = new ScenarioValidator(); + scenarioValidator.checkScenario(scenario); + configurator.adjustScenario(scenario); + //The line below has to be done here right after scenario loading and not in the StandaloneModeChoicePerformer + RunPopulationRouting.insertVehicles(config, scenario); + + InjectorBuilder injectorBuilder = new InjectorBuilder(scenario) + // We add a module that just binds the PersonAnalysisFilter without having to add the whole EqasimAnalysisModule + // This bind is required for building the TripReaderFromPopulation object + .addOverridingModule(new AbstractModule() { + @Override + public void install() { + bind(PersonAnalysisFilter.class).to(DefaultPersonAnalysisFilter.class); + } + }) + .addOverridingModule(new TimeInterpretationModule()) + .addOverridingModule(new EqasimModeChoiceModule()) + .addOverridingModule(new IDFModeChoiceModule(cmd)) + .addOverridingModule(new StandaloneModeChoiceModule(config)); + + + travelTimesFactorsPath.ifPresent(path -> { + injectorBuilder.addOverridingModule(new AbstractModule() { + @Override + public void install() { + addTravelTimeBinding("car").toInstance(new TravelTimeFactors(path)); + } + }); + }); + + recordedTravelTimesPath.ifPresent(path -> { + injectorBuilder.addOverridingModule(new AbstractModule() { + @Override + public void install() { + addTravelTimeBinding("car").to(RecordedTravelTime.class); + } + + @Provides + @Singleton + RecordedTravelTime provideRecordedTravelTime() { + try { + InputStream inputStream = new FileInputStream(path); + RecordedTravelTime recordedTravelTime = RecordedTravelTime.readBinary(inputStream); + inputStream.close(); + return recordedTravelTime; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + }); + + for(AbstractModule module: configurator.getModules()) { + injectorBuilder.addOverridingModule(module); + } + + com.google.inject.Injector injector = injectorBuilder.build(); + + + Population population = injector.getInstance(Population.class); + // We initialize the TripReaderFromPopulation here as we might need it just below + TripReaderFromPopulation tripReader = new TripReaderFromPopulation(Arrays.asList("car,pt".split(",")), injector.getInstance(MainModeIdentifier.class), injector.getInstance(PersonAnalysisFilter.class), Optional.empty(), Optional.empty()); + PublicTransportLegReaderFromPopulation ptLegReader = new PublicTransportLegReaderFromPopulation(injector.getInstance(TransitSchedule.class), injector.getInstance(PersonAnalysisFilter.class)); + OutputDirectoryHierarchy outputDirectoryHierarchy = injector.getInstance(Key.get(OutputDirectoryHierarchy.class, Names.named("StandaloneModeChoice"))); + + cmd.getOption(CMD_WRITE_INPUT_CSV).ifPresent(s -> { + if(Boolean.parseBoolean(s)) { + writeTripsCsv(population, outputDirectoryHierarchy.getOutputFilename("input_trips.csv"), tripReader); + writePtLegsCsv(population, outputDirectoryHierarchy.getOutputFilename("input_pt_legs.csv"), ptLegReader); + } + }); + + StandaloneModeChoicePerformer modeChoicePerformer = injector.getInstance(StandaloneModeChoicePerformer.class); + + modeChoicePerformer.run(); + + cmd.getOption(CMD_WRITE_OUTPUT_CSV).ifPresent(s -> { + if(Boolean.parseBoolean(s)) { + writeTripsCsv(population, outputDirectoryHierarchy.getOutputFilename("output_trips.csv"), tripReader); + writePtLegsCsv(population, outputDirectoryHierarchy.getOutputFilename("output_pt_legs.csv"), ptLegReader); + } + }); + if(cmd.getOption(CMD_SIMULATE_AFTER).isPresent()) { + RunSimulation.main(new String[]{ + "--config-path", cmd.getOptionStrict(CMD_CONFIG_PATH), + "--config:plans.inputPlansFile", Paths.get(outputDirectoryHierarchy.getOutputFilename("output_plans.xml.gz")).toAbsolutePath().toString(), + "--config:controler.outputDirectory", outputDirectoryHierarchy.getOutputFilename("sim"), + "--config:controler.lastIteration", "0"}); + } + } + + private static void writeTripsCsv(Population population, String filePath, TripReaderFromPopulation tripReader) { + //We write the initial trip modes + Collection trips = tripReader.readTrips(population); + try { + new TripWriter(trips, DistanceUnit.meter, DistanceUnit.meter).write(filePath); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void writePtLegsCsv(Population population, String filePath, PublicTransportLegReaderFromPopulation legsReader) { + Collection legs = legsReader.readPublicTransportLegs(population); + try { + new PublicTransportLegWriter(legs).write(filePath); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/StandaloneModeChoiceConfigGroup.java b/ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/StandaloneModeChoiceConfigGroup.java new file mode 100644 index 000000000..c0158926c --- /dev/null +++ b/ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/StandaloneModeChoiceConfigGroup.java @@ -0,0 +1,40 @@ +package org.eqasim.ile_de_france.standalone_mode_choice; + +import jakarta.validation.constraints.NotNull; +import org.matsim.core.config.ReflectiveConfigGroup; + +public class StandaloneModeChoiceConfigGroup extends ReflectiveConfigGroup { + + public static final String GROUP_NAME = "standaloneModeChoice"; + public static final String REMOVE_PERSON_WITH_NO_VALID_ALTERNATIVES = "removePersonsWithNoValidAlternatives"; + public static final String OUTPUT_DIRECTORY = "outputDirectory"; + + private boolean removePersonsWithNoValidAlternative = false; + + @NotNull + private String outputDirectory = "output_mode_choice"; + + public StandaloneModeChoiceConfigGroup() { + super(GROUP_NAME); + } + + @StringGetter(REMOVE_PERSON_WITH_NO_VALID_ALTERNATIVES) + public boolean isRemovePersonsWithNoValidAlternative() { + return this.removePersonsWithNoValidAlternative; + } + + @StringSetter(REMOVE_PERSON_WITH_NO_VALID_ALTERNATIVES) + public void setRemovePersonsWithNoValidAlternative(boolean removePersonsWithNoValidAlternative) { + this.removePersonsWithNoValidAlternative = removePersonsWithNoValidAlternative; + } + + @StringGetter(OUTPUT_DIRECTORY) + public String getOutputDirectory() { + return this.outputDirectory; + } + + @StringSetter(OUTPUT_DIRECTORY) + public void setOutputDirectory(String outputDirectory) { + this.outputDirectory = outputDirectory; + } +} diff --git a/ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/StandaloneModeChoiceModule.java b/ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/StandaloneModeChoiceModule.java new file mode 100644 index 000000000..147c001b9 --- /dev/null +++ b/ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/StandaloneModeChoiceModule.java @@ -0,0 +1,101 @@ +package org.eqasim.ile_de_france.standalone_mode_choice; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import org.apache.log4j.Logger; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.Scenario; +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.Population; +import org.matsim.contribs.discrete_mode_choice.model.DiscreteModeChoiceModel; +import org.matsim.contribs.discrete_mode_choice.modules.config.DiscreteModeChoiceConfigGroup; +import org.matsim.core.config.Config; +import org.matsim.core.config.groups.ControlerConfigGroup; +import org.matsim.core.config.groups.QSimConfigGroup; +import org.matsim.core.controler.AbstractModule; +import org.matsim.core.controler.OutputDirectoryHierarchy; +import org.matsim.core.controler.OutputDirectoryLogging; +import org.matsim.core.population.routes.NetworkRoute; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.core.utils.io.IOUtils; +import org.matsim.vehicles.Vehicle; +import org.matsim.vehicles.VehicleUtils; +import org.matsim.vehicles.Vehicles; +import org.matsim.vehicles.VehiclesFactory; + +import javax.inject.Named; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + + +public class StandaloneModeChoiceModule extends AbstractModule { + private static final Logger log = Logger.getLogger(StandaloneModeChoiceModule.class); + + private final StandaloneModeChoiceConfigGroup configGroup; + private final int numberOfThreads; + private final long randomSeed; + + @Inject + public StandaloneModeChoiceModule(Config config) { + try { + this.configGroup = (StandaloneModeChoiceConfigGroup) config.getModules().get(StandaloneModeChoiceConfigGroup.GROUP_NAME); + } catch (NullPointerException e) { + throw new IllegalStateException(String.format("%s module is required in the config", StandaloneModeChoiceConfigGroup.GROUP_NAME), e); + } catch (ClassCastException e) { + throw new IllegalStateException(String.format("%s module is present in the config but its corresponding configGroup wasn't set while loading it", StandaloneModeChoiceConfigGroup.GROUP_NAME), e); + } + try { + DiscreteModeChoiceConfigGroup discreteModeChoiceConfigGroup = (DiscreteModeChoiceConfigGroup) config.getModules().get(DiscreteModeChoiceConfigGroup.GROUP_NAME); + if (!discreteModeChoiceConfigGroup.getFallbackBehaviour().equals(DiscreteModeChoiceModel.FallbackBehaviour.EXCEPTION) && this.configGroup.isRemovePersonsWithNoValidAlternative()) { + throw new IllegalStateException(String.format("The %s module relies on the exceptions thrown by the %s module to filter out persons with no alternatives. " + + "The %s attribute of the latter needs to be set to %s when the %s attribute of the former is set to true", StandaloneModeChoiceConfigGroup.GROUP_NAME, DiscreteModeChoiceConfigGroup.GROUP_NAME, DiscreteModeChoiceConfigGroup.FALLBACK_BEHAVIOUR, DiscreteModeChoiceModel.FallbackBehaviour.EXCEPTION, StandaloneModeChoiceConfigGroup.REMOVE_PERSON_WITH_NO_VALID_ALTERNATIVES)); + } + } catch (NullPointerException e) { + throw new IllegalStateException(String.format("%s module is required in the config", DiscreteModeChoiceConfigGroup.GROUP_NAME), e); + } catch (ClassCastException e) { + throw new IllegalStateException(String.format("%s module is present in the config but its corresponding configGroup wasn't set while loading it", DiscreteModeChoiceConfigGroup.GROUP_NAME), e); + } + this.numberOfThreads = config.global().getNumberOfThreads(); + this.randomSeed = config.global().getRandomSeed(); + } + + @Override + public void install() { + + } + + @Provides + public StandaloneModeChoicePerformer provideBadPlansFilter(Provider discreteModeChoiceModelProvider, Population population, @Named("StandaloneModeChoice") OutputDirectoryHierarchy outputDirectoryHierarchy, Scenario scenario) { + return new StandaloneModeChoicePerformer(discreteModeChoiceModelProvider, configGroup, population, this.numberOfThreads, this.randomSeed, outputDirectoryHierarchy, scenario); + } + + @Provides + @Named("StandaloneModeChoice") + @Singleton + public OutputDirectoryHierarchy provideOutputDirectoryHierarchy() { + OutputDirectoryHierarchy outputDirectoryHierarchy = new OutputDirectoryHierarchy(this.configGroup.getOutputDirectory(), null, OutputDirectoryHierarchy.OverwriteFileSetting.overwriteExistingFiles, false, ControlerConfigGroup.CompressionType.gzip); + File outputDir = new File(outputDirectoryHierarchy.getOutputPath()); + if (outputDir.exists()) { + if (outputDir.isFile()) { + throw new RuntimeException("Cannot create output directory. " + + outputDirectoryHierarchy.getOutputPath() + " is a file and cannot be replaced by a directory."); + } + if (Objects.requireNonNull(outputDir.list()).length > 0) { + IOUtils.deleteDirectoryRecursively(outputDir.toPath()); + } + } + try { + OutputDirectoryLogging.initLoggingWithOutputDirectory(this.configGroup.getOutputDirectory()); + } catch (IOException e) { + throw new RuntimeException(e); + } + return outputDirectoryHierarchy; + } +} diff --git a/ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/StandaloneModeChoicePerformer.java b/ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/StandaloneModeChoicePerformer.java new file mode 100644 index 000000000..dab5afda2 --- /dev/null +++ b/ile_de_france/src/main/java/org/eqasim/ile_de_france/standalone_mode_choice/StandaloneModeChoicePerformer.java @@ -0,0 +1,182 @@ +package org.eqasim.ile_de_france.standalone_mode_choice; + + +import com.google.inject.Provider; +import org.apache.log4j.Logger; +import org.eqasim.core.scenario.routing.RunPopulationRouting; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdSet; +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.population.*; +import org.matsim.contribs.discrete_mode_choice.model.DiscreteModeChoiceModel; +import org.matsim.contribs.discrete_mode_choice.replanning.DiscreteModeChoiceAlgorithm; +import org.matsim.contribs.discrete_mode_choice.replanning.TripListConverter; +import org.matsim.core.config.Config; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.config.groups.QSimConfigGroup; +import org.matsim.core.controler.OutputDirectoryHierarchy; +import org.matsim.core.population.routes.NetworkRoute; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.core.utils.misc.Counter; +import org.matsim.vehicles.Vehicle; +import org.matsim.vehicles.VehicleUtils; +import org.matsim.vehicles.Vehicles; +import org.matsim.vehicles.VehiclesFactory; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +public class StandaloneModeChoicePerformer { + + private static final Logger logger = Logger.getLogger(StandaloneModeChoicePerformer.class); + + private final Provider discreteModeChoiceModelProvider; + private final boolean removePersonsWithBadPlans; + private final Population population; + private final int numberOfThreads; + private final long seed; + private final OutputDirectoryHierarchy outputDirectoryHierarchy; + private final Scenario scenario; + + public StandaloneModeChoicePerformer(Provider discreteModeChoiceModelProvider, StandaloneModeChoiceConfigGroup configGroup, Population population, int numberOfThreads, long seed, OutputDirectoryHierarchy outputDirectoryHierarchy, Scenario scenario) { + this.discreteModeChoiceModelProvider = discreteModeChoiceModelProvider; + this.removePersonsWithBadPlans = configGroup.isRemovePersonsWithNoValidAlternative(); + this.numberOfThreads = numberOfThreads; + this.population = population; + this.seed = seed; + this.outputDirectoryHierarchy = outputDirectoryHierarchy; + this.scenario = scenario; + } + + public void run() throws InterruptedException { + + Counter counter = new Counter("handled plan #"); + + if(numberOfThreads > 0) { + List threads = new LinkedList<>(); + + final AtomicBoolean errorOccurred = new AtomicBoolean(false); + + + PlanAlgoThread[] planAlgoThreads = new PlanAlgoThread[this.numberOfThreads]; + + for (int i = 0; i < numberOfThreads; i++) { + Random random = new Random(this.seed); + planAlgoThreads[i] = new PlanAlgoThread(new DiscreteModeChoiceAlgorithm(random, this.discreteModeChoiceModelProvider.get(), this.population.getFactory(), new TripListConverter()), counter, this.removePersonsWithBadPlans); + Thread thread = new Thread(planAlgoThreads[i]); + thread.setUncaughtExceptionHandler((t, e) -> { + e.printStackTrace(); + errorOccurred.set(true); + }); + threads.add(thread); + } + + int personsCount = 0; + logger.info(String.format("Distributing %d persons on %d threads", population.getPersons().size(), this.numberOfThreads)); + for(Person person: population.getPersons().values()) { + List unselectedPlans = new ArrayList<>(); + for(Plan plan: person.getPlans()) { + if(plan != person.getSelectedPlan()) { + unselectedPlans.add(plan); + } + } + unselectedPlans.forEach(person::removePlan); + planAlgoThreads[personsCount % this.numberOfThreads].addPlanToThread(person.getSelectedPlan()); + personsCount+=1; + } + logger.info(String.format("Starting %d threads, handling in %d plans", this.numberOfThreads, population.getPersons().size())); + + threads.forEach(Thread::start); + + for (Thread thread: threads) { + thread.join(); + } + + if (errorOccurred.get()) { + throw new RuntimeException("Found errors in mode choice threads threads"); + } + + if(this.removePersonsWithBadPlans) { + IdSet personsToRemove = new IdSet<>(Person.class); + for(PlanAlgoThread planAlgoThread: planAlgoThreads) { + personsToRemove.addAll(planAlgoThread.getPersonsWithNoAlternative()); + } + double percentage = ((double) personsToRemove.size()) * 100 / population.getPersons().size(); + logger.info(String.format("Removing %d persons with no valid alternative out of %d (%f %%)", personsToRemove.size(), population.getPersons().size(), percentage)); + for(Id personId: personsToRemove) { + population.removePerson(personId); + } + } + } else { + Random random = new Random(this.seed); + PlanAlgoThread planAlgoThread = new PlanAlgoThread(new DiscreteModeChoiceAlgorithm(random, this.discreteModeChoiceModelProvider.get(), this.population.getFactory(), new TripListConverter()), counter, this.removePersonsWithBadPlans); + for(Person person: population.getPersons().values()) { + List unselectedPlans = new ArrayList<>(); + for(Plan plan: person.getPlans()) { + if(plan != person.getSelectedPlan()) { + unselectedPlans.add(plan); + } + } + unselectedPlans.forEach(person::removePlan); + planAlgoThread.addPlanToThread(person.getSelectedPlan()); + } + planAlgoThread.run(); + if(this.removePersonsWithBadPlans) { + IdSet personsToRemove = new IdSet<>(Person.class); + personsToRemove.addAll(planAlgoThread.getPersonsWithNoAlternative()); + double percentage = ((double) personsToRemove.size()) * 100 / population.getPersons().size(); + logger.info(String.format("Removing %d persons with no valid alternative out of %d (%f %%)", personsToRemove.size(), population.getPersons().size(), percentage)); + personsToRemove.forEach(population::removePerson); + } + } + + String outputPlansName = outputDirectoryHierarchy.getOutputFilename("output_plans.xml.gz"); + // We do this right here before writing the population file so that following simulations using it work well + RunPopulationRouting.clearVehicles(this.scenario.getConfig(), this.scenario); + new PopulationWriter(population).write(outputPlansName); + ConfigUtils.writeConfig(scenario.getConfig(), this.outputDirectoryHierarchy.getOutputFilename("output_config.xml")); + } + + + private final static class PlanAlgoThread implements Runnable { + + private final DiscreteModeChoiceAlgorithm planAlgo; + private final List plans = new LinkedList<>(); + private final Counter counter; + private final IdSet personsWithNoAlternative; + private final boolean reportPersonsWithNoAlternative; + + public PlanAlgoThread(final DiscreteModeChoiceAlgorithm algo, final Counter counter, boolean reportPersonsWithNoAlternative) { + this.planAlgo = algo; + this.counter = counter; + this.personsWithNoAlternative = new IdSet<>(Person.class); + this.reportPersonsWithNoAlternative = reportPersonsWithNoAlternative; + } + + public void addPlanToThread(final Plan plan) { + this.plans.add(plan); + } + + @Override + public void run() { + for (Plan plan : this.plans) { + try { + this.planAlgo.run(plan); + } catch (IllegalStateException e) { + if(e.getCause() instanceof DiscreteModeChoiceModel.NoFeasibleChoiceException) { + if(this.reportPersonsWithNoAlternative) { + this.personsWithNoAlternative.add(plan.getPerson().getId()); + } + } else { + throw e; + } + } + this.counter.incCounter(); + } + } + + public IdSet getPersonsWithNoAlternative() { + return this.personsWithNoAlternative; + } + } +} diff --git a/ile_de_france/src/test/java/org/eqasim/ile_de_france/TestCorisica.java b/ile_de_france/src/test/java/org/eqasim/ile_de_france/TestCorisica.java index 47465565f..2b66a9854 100644 --- a/ile_de_france/src/test/java/org/eqasim/ile_de_france/TestCorisica.java +++ b/ile_de_france/src/test/java/org/eqasim/ile_de_france/TestCorisica.java @@ -9,6 +9,7 @@ import org.apache.commons.io.FileUtils; import org.eqasim.core.scenario.cutter.RunScenarioCutter; +import org.eqasim.ile_de_france.standalone_mode_choice.RunStandaloneModeChoice; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -59,6 +60,17 @@ public void testCorsicaPipeline() Assert.assertEquals(47, (long) counts.get("pt")); } + // Run the mode choice + following simulation + { + RunStandaloneModeChoice.main(new String[]{ + "--config-path", "corsica_test/corsica_config.xml", + "--write-input-csv-trips", "true", + "--write-output-csv-trips", "true", + "--simulate-after", "true", + "--config:standaloneModeChoice.outputDirectory", "corsica_test/mode_choice_output" + }); + } + // Cut the scenario based on output plans { RunScenarioCutter.main(new String[] { //