diff --git a/matsim/src/main/java/org/matsim/core/controler/corelisteners/PlansReplanningImpl.java b/matsim/src/main/java/org/matsim/core/controler/corelisteners/PlansReplanningImpl.java index 7dd5d99e824..733087d70df 100644 --- a/matsim/src/main/java/org/matsim/core/controler/corelisteners/PlansReplanningImpl.java +++ b/matsim/src/main/java/org/matsim/core/controler/corelisteners/PlansReplanningImpl.java @@ -24,7 +24,9 @@ import org.matsim.core.controler.events.ReplanningEvent; import org.matsim.core.controler.listener.ReplanningListener; import org.matsim.core.replanning.ReplanningContext; +import org.matsim.core.replanning.ReplanningUtils; import org.matsim.core.replanning.StrategyManager; +import org.matsim.core.replanning.conflicts.ConflictManager; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -41,21 +43,24 @@ */ @Singleton final class PlansReplanningImpl implements PlansReplanning, ReplanningListener { - private final Provider replanningContextProvider; private final Population population; private final StrategyManager strategyManager; + private final ConflictManager conflictManager; @Inject - PlansReplanningImpl(StrategyManager strategyManager, Population pop, Provider replanningContextProvider) { + PlansReplanningImpl(StrategyManager strategyManager, ConflictManager conflictManager, Population pop, + Provider replanningContextProvider) { this.population = pop; this.strategyManager = strategyManager; + this.conflictManager = conflictManager; this.replanningContextProvider = replanningContextProvider; } @Override public void notifyReplanning(final ReplanningEvent event) { + conflictManager.initializeReplanning(population); strategyManager.run(population, event.getIteration(), replanningContextProvider.get()); + conflictManager.run(population, event.getIteration()); } - } diff --git a/matsim/src/main/java/org/matsim/core/replanning/ReplanningUtils.java b/matsim/src/main/java/org/matsim/core/replanning/ReplanningUtils.java index 100345bded7..426c3c6309d 100644 --- a/matsim/src/main/java/org/matsim/core/replanning/ReplanningUtils.java +++ b/matsim/src/main/java/org/matsim/core/replanning/ReplanningUtils.java @@ -22,13 +22,40 @@ package org.matsim.core.replanning; +import javax.annotation.Nullable; + import org.matsim.api.core.v01.population.BasicPlan; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; public final class ReplanningUtils { + static public final String INITIAl_PLAN_ATTRIBUTE = "isInitialPlan"; + + public static boolean isInitialPlan(Plan plan) { + Boolean isInitialPlan = (Boolean) plan.getAttributes().getAttribute(INITIAl_PLAN_ATTRIBUTE); + return isInitialPlan != null && isInitialPlan; + } + + @Nullable + public static Plan getInitialPlan(Person person) { + for (Plan plan : person.getPlans()) { + if (isInitialPlan(plan)) { + return plan; + } + } + + return null; + } + + public static void setInitialPlan(Person person) { + person.getPlans().forEach(plan -> plan.getAttributes().removeAttribute(INITIAl_PLAN_ATTRIBUTE)); + person.getSelectedPlan().getAttributes().putAttribute(INITIAl_PLAN_ATTRIBUTE, true); + } + /** * Return whether a strategy is innovative, i.e. is producing new plans. - * */ + */ public static

boolean isInnovativeStrategy(GenericPlanStrategy planStrategy) { return !isOnlySelector(planStrategy); } diff --git a/matsim/src/main/java/org/matsim/core/replanning/StrategyManagerModule.java b/matsim/src/main/java/org/matsim/core/replanning/StrategyManagerModule.java index d42fde9fabe..ef2daff09cc 100644 --- a/matsim/src/main/java/org/matsim/core/replanning/StrategyManagerModule.java +++ b/matsim/src/main/java/org/matsim/core/replanning/StrategyManagerModule.java @@ -35,6 +35,7 @@ import org.matsim.core.controler.OutputDirectoryHierarchy; import org.matsim.core.replanning.choosers.StrategyChooser; import org.matsim.core.replanning.choosers.WeightedStrategyChooser; +import org.matsim.core.replanning.conflicts.ConflictModule; import org.matsim.core.replanning.modules.ExternalModule; import org.matsim.core.replanning.selectors.RandomPlanSelector; import org.matsim.core.replanning.strategies.DefaultPlanStrategiesModule; @@ -92,6 +93,8 @@ public void install() { // (settings is the key ... ok. The Key.get(...) returns the PlanStrategy that was registered under its name at (*) above.) } } + + install(new ConflictModule()); } /** diff --git a/matsim/src/main/java/org/matsim/core/replanning/conflicts/ConflictManager.java b/matsim/src/main/java/org/matsim/core/replanning/conflicts/ConflictManager.java new file mode 100644 index 00000000000..16a3bcc02d2 --- /dev/null +++ b/matsim/src/main/java/org/matsim/core/replanning/conflicts/ConflictManager.java @@ -0,0 +1,117 @@ +package org.matsim.core.replanning.conflicts; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdSet; +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.core.replanning.ReplanningUtils; + +import com.google.common.base.Preconditions; + +/** + * This class handles conflicts during replanning. ConflictResolvers are used to + * identify agents whose plans are conflicting with others and need to be + * "rejected" in order to resolve these conflicts. "Rejecting" means to switch + * those agents back to a plan in their memory that does not cause any + * conflicts. Those are plans that are not "potentially conflicting", i.e., + * could interfere in any way with another agent. Those are usually plans that + * don't contain a certain restricted/limited/capacitated mode or resource. The + * logic of conflicts is defined using the ConflictResolver interface. + */ +public class ConflictManager { + private final static Logger logger = LogManager.getLogger(ConflictManager.class); + + private final Set resolvers; + private final ConflictWriter writer; + private final Random random; + + public ConflictManager(Set resolvers, ConflictWriter writer, Random random) { + this.resolvers = resolvers; + this.random = random; + this.writer = writer; + } + + public void initializeReplanning(Population population) { + if (resolvers.size() > 0) { // only require if active + population.getPersons().values().forEach(ReplanningUtils::setInitialPlan); + } + } + + public void run(Population population, int iteration) { + if (resolvers.size() == 0) { + return; + } + + logger.info("Resolving conflicts ..."); + + Map conflictCounts = new HashMap<>(); + IdSet conflictingIds = new IdSet<>(Person.class); + + for (ConflictResolver resolver : resolvers) { + IdSet resolverConflictingIds = resolver.resolve(population, iteration); + conflictCounts.put(resolver.getName(), resolverConflictingIds.size()); + conflictingIds.addAll(resolverConflictingIds); + } + + logger.info(" Conflicts: " + conflictCounts.entrySet().stream() + .map(entry -> String.format("%s=%d", entry.getKey(), entry.getValue())) + .collect(Collectors.joining(", "))); + + int switchedToInitialCount = 0; + int switchedToRandomCount = 0; + + for (Id personId : conflictingIds) { + Person person = population.getPersons().get(personId); + + // If the initial plan is non-conflicting, switch back to it + Plan initialPlan = ReplanningUtils.getInitialPlan(person); + + if (initialPlan != null && !isPotentiallyConflicting(initialPlan)) { + person.setSelectedPlan(initialPlan); + switchedToInitialCount++; + } else { + // Select a random non-conflicting plan + List candidates = person.getPlans().stream().filter(p -> !isPotentiallyConflicting(p)) + .collect(Collectors.toList()); + Preconditions.checkState(candidates.size() > 0, + String.format("Agent %s has no non-conflicting plan", personId)); + + // Shuffle, and select the first + Collections.shuffle(candidates, random); + person.setSelectedPlan(candidates.get(0)); + + switchedToRandomCount++; + } + } + + logger.info(String.format(" %d (%.2f%%) switched to initial", switchedToInitialCount, + (double) switchedToInitialCount / population.getPersons().size())); + logger.info(String.format(" %d (%.2f%%) switched to random", switchedToRandomCount, + (double) switchedToRandomCount / population.getPersons().size())); + + writer.write(iteration, switchedToInitialCount, switchedToRandomCount, conflictCounts); + + logger.info(" Done resolving conflicts!"); + } + + public boolean isPotentiallyConflicting(Plan plan) { + for (ConflictResolver resolver : resolvers) { + if (resolver.isPotentiallyConflicting(plan)) { + return true; + } + } + + return false; + } +} diff --git a/matsim/src/main/java/org/matsim/core/replanning/conflicts/ConflictModule.java b/matsim/src/main/java/org/matsim/core/replanning/conflicts/ConflictModule.java new file mode 100644 index 00000000000..4795c0568b5 --- /dev/null +++ b/matsim/src/main/java/org/matsim/core/replanning/conflicts/ConflictModule.java @@ -0,0 +1,72 @@ +package org.matsim.core.replanning.conflicts; + +import java.io.File; +import java.util.Random; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.matsim.core.controler.AbstractModule; +import org.matsim.core.controler.OutputDirectoryHierarchy; +import org.matsim.core.gbl.MatsimRandom; + +import com.google.inject.Binder; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.multibindings.Multibinder; + +/** + * Prepares injection of the conflict resolution logic during replanning + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ConflictModule extends AbstractModule { + private final static Logger logger = LogManager.getLogger(ConflictModule.class); + + private final static String OUTPUT_FILE = "conflicts.csv"; + + @Override + public void install() { + // initialize the builder + getMultibinder(binder()); + } + + @Provides + @Singleton + ConflictWriter provideConflictWriter(OutputDirectoryHierarchy outputDirectoryHierarchy) { + File outputPath = new File(outputDirectoryHierarchy.getOutputFilename(OUTPUT_FILE)); + return new ConflictWriter(outputPath); + } + + @Provides + @Singleton + ConflictManager provideConflictManager(Set resolvers, ConflictWriter writer) { + if (!getConfig().replanning().getPlanSelectorForRemoval() + .equals(WorstPlanForRemovalSelectorWithConflicts.SELECTOR_NAME)) { + logger.warn("The replanning.planSelectorForRemoval is not set to " + + WorstPlanForRemovalSelectorWithConflicts.SELECTOR_NAME + + ". This will likely cause problems with the conflict logic if you are not sure what you are doing."); + } + + Random random = MatsimRandom.getRandom(); // no need for local instance, not parallel! + return new ConflictManager(resolvers, writer, random); + } + + static Multibinder getMultibinder(Binder binder) { + return Multibinder.newSetBinder(binder, ConflictResolver.class); + } + + /** + * Allows to bind a conflict resolver in an AbstractModule, for instance: + * + * + * ConflictModule.bindResolver(binder()).toInstance(new ConflictResolver() { + * // ... + * }); + * + */ + static public LinkedBindingBuilder bindResolver(Binder binder) { + return getMultibinder(binder).addBinding(); + } +} diff --git a/matsim/src/main/java/org/matsim/core/replanning/conflicts/ConflictResolver.java b/matsim/src/main/java/org/matsim/core/replanning/conflicts/ConflictResolver.java new file mode 100644 index 00000000000..9595d073e7b --- /dev/null +++ b/matsim/src/main/java/org/matsim/core/replanning/conflicts/ConflictResolver.java @@ -0,0 +1,23 @@ +package org.matsim.core.replanning.conflicts; + +import org.matsim.api.core.v01.IdSet; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.api.core.v01.population.Population; + +/** + * This interface is called after standard replanning. Its purpose is to check + * the population and detect any conflicts between agents. The interface must + * then return a list of agents that should be reset to a non-conflicting plan + * in order to resolve all conflicts. Plans that are not conflicting are + * identified as such using the isPotentiallyConflicting method. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public interface ConflictResolver { + IdSet resolve(Population population, int iteration); + + boolean isPotentiallyConflicting(Plan plan); + + String getName(); +} diff --git a/matsim/src/main/java/org/matsim/core/replanning/conflicts/ConflictWriter.java b/matsim/src/main/java/org/matsim/core/replanning/conflicts/ConflictWriter.java new file mode 100644 index 00000000000..77604d2b870 --- /dev/null +++ b/matsim/src/main/java/org/matsim/core/replanning/conflicts/ConflictWriter.java @@ -0,0 +1,61 @@ +package org.matsim.core.replanning.conflicts; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.matsim.core.utils.io.IOUtils; + +/** + * Writes high-level statistics on the conflict resolution process per iteration + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ConflictWriter { + private final File outputPath; + + public ConflictWriter(File outputPath) { + this.outputPath = outputPath; + } + + public void write(int iteration, int rejectedToInitial, int rejectedToRandom, Map conflictCounts) { + boolean writeHeader = !outputPath.exists(); + + List resolvers = new ArrayList<>(conflictCounts.keySet()); + Collections.sort(resolvers); + + try { + BufferedWriter writer = IOUtils.getAppendingBufferedWriter(outputPath.getPath()); + + if (writeHeader) { + List header = new ArrayList<>( + Arrays.asList("iteration", "rejected_total", "switched_to_initial", "switched_to_random")); + resolvers.stream().map(r -> "resolver:" + r).forEach(header::add); + + writer.write(String.join(";", header) + "\n"); + } + + List row = new ArrayList<>(Arrays.asList(String.valueOf(iteration), // + String.valueOf(rejectedToInitial + rejectedToRandom), // + String.valueOf(rejectedToInitial), // + String.valueOf(rejectedToRandom) // + )); + + for (String resolver : resolvers) { + row.add(String.valueOf(conflictCounts.get(resolver))); + } + + writer.write(String.join(";", row) + "\n"); + + writer.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/matsim/src/main/java/org/matsim/core/replanning/conflicts/WorstPlanForRemovalSelectorWithConflicts.java b/matsim/src/main/java/org/matsim/core/replanning/conflicts/WorstPlanForRemovalSelectorWithConflicts.java new file mode 100644 index 00000000000..22e4947e9d2 --- /dev/null +++ b/matsim/src/main/java/org/matsim/core/replanning/conflicts/WorstPlanForRemovalSelectorWithConflicts.java @@ -0,0 +1,90 @@ +/* *********************************************************************** * + * project: org.matsim.* + * WorstPlanSelector.java + * * + * *********************************************************************** * + * * + * copyright : (C) 2009 by the members listed in the COPYING, * + * LICENSE and WARRANTY file. * + * email : info at matsim dot org * + * * + * *********************************************************************** * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * See also COPYING, LICENSE and WARRANTY file * + * * + * *********************************************************************** */ + +package org.matsim.core.replanning.conflicts; + +import java.util.Collections; +import java.util.LinkedList; + +import org.apache.commons.lang3.tuple.Pair; +import org.jboss.logging.Logger; +import org.matsim.api.core.v01.population.HasPlansAndId; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.core.replanning.selectors.PlanSelector; + +import com.google.inject.Inject; + +/** + * This selector is used like the standard WorstPlanForRemovalSelector to reduce + * plans from an agent's memory. However, adhering to the conflict resolution + * logic, the selector will make sure that a non-conflicting plan is never + * selected for removal if it is the last remaining non-conflicting one. This + * way, we make sure that every agent always keeps one plan that is not + * conflicting. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class WorstPlanForRemovalSelectorWithConflicts implements PlanSelector { + public static final String SELECTOR_NAME = "WorstPlanForRemovalSelectorWithConflicts"; + + private final Logger logger = Logger.getLogger(WorstPlanForRemovalSelectorWithConflicts.class); + + private final ConflictManager conflictManager; + + @Inject + public WorstPlanForRemovalSelectorWithConflicts(ConflictManager conflictManager) { + this.conflictManager = conflictManager; + } + + @Override + public Plan selectPlan(HasPlansAndId person) { + LinkedList> sorter = new LinkedList<>(); + int nonConflictingCount = 0; + + for (Plan plan : person.getPlans()) { + double score = plan.getScore() == null ? Double.NEGATIVE_INFINITY : plan.getScore(); + sorter.add(Pair.of(plan, score)); + + if (!conflictManager.isPotentiallyConflicting(plan)) { + nonConflictingCount++; + } + } + + if (nonConflictingCount == 0) { + logger.error(String.format("No non-conflicting plan found for agent %s", person.getId())); + } + + Collections.sort(sorter, (a, b) -> Double.compare(a.getRight(), b.getRight())); + + if (nonConflictingCount == 1 && !conflictManager.isPotentiallyConflicting(sorter.getFirst().getLeft())) { + // Remove the first from the removable candidates if it is the only + // non-conflicting one + sorter.removeFirst(); + } + + if (sorter.size() > 0) { + return sorter.getFirst().getLeft(); + } + + return null; + } + +} diff --git a/matsim/src/main/java/org/matsim/core/replanning/strategies/DefaultPlanStrategiesModule.java b/matsim/src/main/java/org/matsim/core/replanning/strategies/DefaultPlanStrategiesModule.java index 18ff8e31436..a0dd6d9c423 100644 --- a/matsim/src/main/java/org/matsim/core/replanning/strategies/DefaultPlanStrategiesModule.java +++ b/matsim/src/main/java/org/matsim/core/replanning/strategies/DefaultPlanStrategiesModule.java @@ -37,6 +37,7 @@ import org.matsim.core.controler.AbstractModule; import org.matsim.core.population.algorithms.PermissibleModesCalculator; import org.matsim.core.population.algorithms.PermissibleModesCalculatorImpl; +import org.matsim.core.replanning.conflicts.WorstPlanForRemovalSelectorWithConflicts; import org.matsim.core.replanning.selectors.ExpBetaPlanChanger; import org.matsim.core.replanning.selectors.ExpBetaPlanSelector; import org.matsim.core.replanning.selectors.PathSizeLogitSelector; @@ -54,6 +55,9 @@ public void install() { if (getConfig().replanning().getPlanSelectorForRemoval().equals(DefaultPlansRemover.WorstPlanSelector.toString())) { bindPlanSelectorForRemoval().to(WorstPlanForRemovalSelector.class); } + if (getConfig().replanning().getPlanSelectorForRemoval().equals(WorstPlanForRemovalSelectorWithConflicts.SELECTOR_NAME)) { + bindPlanSelectorForRemoval().to(WorstPlanForRemovalSelectorWithConflicts.class); + } if (getConfig().replanning().getPlanSelectorForRemoval().equals(DefaultPlansRemover.SelectRandom.toString())) { bindPlanSelectorForRemoval().to(new TypeLiteral>(){}); } diff --git a/matsim/src/test/java/org/matsim/core/replanning/conflicts/ReplanningWithConflictsTest.java b/matsim/src/test/java/org/matsim/core/replanning/conflicts/ReplanningWithConflictsTest.java new file mode 100644 index 00000000000..d7dcef44001 --- /dev/null +++ b/matsim/src/test/java/org/matsim/core/replanning/conflicts/ReplanningWithConflictsTest.java @@ -0,0 +1,254 @@ +package org.matsim.core.replanning.conflicts; + +import static org.junit.Assert.assertEquals; + +import org.junit.Rule; +import org.junit.Test; +import org.matsim.api.core.v01.Coord; +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.events.PersonDepartureEvent; +import org.matsim.api.core.v01.events.handler.PersonDepartureEventHandler; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.network.NetworkFactory; +import org.matsim.api.core.v01.network.Node; +import org.matsim.api.core.v01.population.Activity; +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.api.core.v01.population.PopulationFactory; +import org.matsim.core.config.Config; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.config.groups.ReplanningConfigGroup.StrategySettings; +import org.matsim.core.config.groups.RoutingConfigGroup.TeleportedModeParams; +import org.matsim.core.config.groups.ScoringConfigGroup.ActivityParams; +import org.matsim.core.config.groups.ScoringConfigGroup.ModeParams; +import org.matsim.core.controler.AbstractModule; +import org.matsim.core.controler.Controler; +import org.matsim.core.replanning.strategies.DefaultPlanStrategiesModule.DefaultSelector; +import org.matsim.core.replanning.strategies.DefaultPlanStrategiesModule.DefaultStrategy; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.core.scenario.ScenarioUtils; +import org.matsim.testcases.MatsimTestUtils; + +/** + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ReplanningWithConflictsTest { + @Rule + public MatsimTestUtils utils = new MatsimTestUtils(); + + @Test + public void testModeRestriction() { + /* + * This is the possibly simplest use of the the conflict resolution logic. We + * have 100 agents and two modes, one is "unrestricted", the other one is + * "restricted". Each agent has one leg with either mode, all start with an + * "unrestricted" leg. Having an "unrestrited" leg does not give any score, + * having a "restricted" leg gives a score of 1.0. The replanning logic is + * BestScore, so if selection is chosen, a plan with "restricted" will be chosen + * if it exists in the agent memory. For innovation, we use ChangeTripMode, so + * we choose a new random mode. + * + * Without conflicts, this will lead to all agents choosing the "restricted" + * mode eventually, only 10% will choose "unrestricted" because they innovate. + * + * However, we introduce a conflict logic that ensures that we never have more + * than 40 agents using the "restricted" mode. In our simple logic, we iterate + * through the agents and "accept" plans as long as we don't go over this + * threshold. As soon as we hit the limit, we note down the "conflicting" agents + * and let our ConflictResolver return their IDs. The conflict logic will then + * *reject* the plans generated by replanning and switch the agents back to a + * non-conflicting plan (in that case the ones with the "unrestricted" mode). + * + * To make sure that every agent always has a non-conflicting plan in memory, we + * use WorstPlanForRemovalSelectorWithConflicts as the planRemovalSelector. + * + * Running the simulation will make the agents replan and soon we hit the mark + * of 40 agents using the "restricted" mode. After that, the conflict logic will + * start rejecting plans. Note that in this particular logic as a side effect, + * we will also slowly sort out agents so that the 40 first in the population + * will eventually be the ones using the restricted mode. In a more elaborate + * logic we would make sure that, for instance, agents that didn't change their + * plan or were already using initially the restricted mode are treated with + * priority. + * + * This is the most simple set-up of a conflict resolution process. It can be + * adapted to all kind of use cases, for instance, managing parking space, + * matching agents for car-pooling or implementing peer-to-peer car-sharing. + */ + Config config = ConfigUtils.createConfig(); + + config.controller().setOutputDirectory(utils.getOutputDirectory()); + config.controller().setLastIteration(10); + + ActivityParams genericActivityParams = new ActivityParams("generic"); + genericActivityParams.setScoringThisActivityAtAll(false); + config.scoring().addActivityParams(genericActivityParams); + + ModeParams unrestrictedModeParams = new ModeParams("unrestricted_mode"); + unrestrictedModeParams.setConstant(0.0); + config.scoring().addModeParams(unrestrictedModeParams); + + ModeParams restrictedModeParams = new ModeParams("restricted_mode"); + restrictedModeParams.setConstant(1.0); + config.scoring().addModeParams(restrictedModeParams); + + TeleportedModeParams unrestrictedRoutingParams = new TeleportedModeParams("unrestricted_mode"); + unrestrictedRoutingParams.setTeleportedModeSpeed(1.0); + config.routing().addTeleportedModeParams(unrestrictedRoutingParams); + + TeleportedModeParams restrictedRoutingParams = new TeleportedModeParams("restricted_mode"); + restrictedRoutingParams.setTeleportedModeSpeed(1.0); + config.routing().addTeleportedModeParams(restrictedRoutingParams); + + TeleportedModeParams walkRoutingParams = new TeleportedModeParams("walk"); + walkRoutingParams.setTeleportedModeSpeed(1.0); + config.routing().addTeleportedModeParams(walkRoutingParams); + + config.replanning().clearStrategySettings(); + + StrategySettings selectionStrategy = new StrategySettings(); + selectionStrategy.setStrategyName(DefaultSelector.BestScore); + selectionStrategy.setWeight(0.9); + config.replanning().addStrategySettings(selectionStrategy); + + StrategySettings innovationStrategy = new StrategySettings(); + innovationStrategy.setStrategyName(DefaultStrategy.ChangeTripMode); + innovationStrategy.setWeight(0.1); + config.replanning().addStrategySettings(innovationStrategy); + + config.replanning().setPlanSelectorForRemoval(WorstPlanForRemovalSelectorWithConflicts.SELECTOR_NAME); + + config.changeMode().setModes(new String[] { "restricted_mode", "unrestricted_mode" }); + + Scenario scenario = ScenarioUtils.createScenario(config); + + Population population = scenario.getPopulation(); + PopulationFactory populationFactory = population.getFactory(); + + for (int i = 0; i < 100; i++) { + Person person = populationFactory.createPerson(Id.createPersonId("p" + i)); + population.addPerson(person); + + Plan plan = populationFactory.createPlan(); + person.addPlan(plan); + + Activity firstActivity = populationFactory.createActivityFromCoord("generic", new Coord(0.0, 0.0)); + firstActivity.setEndTime(0.0); + plan.addActivity(firstActivity); + + Leg leg = populationFactory.createLeg("unrestricted_mode"); + plan.addLeg(leg); + + Activity secondActivity = populationFactory.createActivityFromCoord("generic", new Coord(0.0, 0.0)); + plan.addActivity(secondActivity); + } + + Network network = scenario.getNetwork(); + NetworkFactory networkFactory = network.getFactory(); + + Node node = networkFactory.createNode(Id.createNodeId("node"), new Coord(0.0, 0.0)); + network.addNode(node); + + Link link = networkFactory.createLink(Id.createLinkId("link"), node, node); + network.addLink(link); + + Controler controller = new Controler(scenario); + + controller.addOverridingModule(new AbstractModule() { + @Override + public void install() { + ConflictModule.bindResolver(binder()).toInstance(new ConflictResolver() { + @Override + public IdSet resolve(Population population, int iteration) { + IdSet conflictingPersonIds = new IdSet<>(Person.class); + + int maximumRestricted = 40; + int restrictedCount = 0; + + for (Person person : population.getPersons().values()) { + Plan plan = person.getSelectedPlan(); + boolean usesRestrictedMode = false; + + for (Leg leg : TripStructureUtils.getLegs(plan)) { + if (leg.getMode().equals("restricted_mode")) { + usesRestrictedMode = true; + break; + } + } + + if (usesRestrictedMode) { + restrictedCount++; + + if (restrictedCount > maximumRestricted) { + conflictingPersonIds.add(person.getId()); + } + } + } + + return conflictingPersonIds; + } + + @Override + public boolean isPotentiallyConflicting(Plan plan) { + for (Leg leg : TripStructureUtils.getLegs(plan)) { + if (leg.getMode().equals("restricted_mode")) { + return true; + } + } + + return false; + } + + @Override + public String getName() { + return "restriction"; + } + }); + } + }); + + LegCounter counter = new LegCounter(); + counter.install(controller); + + controller.run(); + + assertEquals(40, counter.restricted); + assertEquals(60, counter.unrestricted); + } + + private class LegCounter implements PersonDepartureEventHandler { + int restricted; + int unrestricted; + + @Override + public void handleEvent(PersonDepartureEvent event) { + if (event.getLegMode().equals("unrestricted_mode")) { + unrestricted++; + } else if (event.getLegMode().equals("restricted_mode")) { + restricted++; + } + } + + @Override + public void reset(int iteration) { + restricted = 0; + unrestricted = 0; + } + + void install(Controler controller) { + LegCounter self = this; + + controller.addOverridingModule(new AbstractModule() { + @Override + public void install() { + addEventHandlerBinding().toInstance(self); + } + }); + } + } +}