Skip to content

Commit

Permalink
introduce conflict-based replanning
Browse files Browse the repository at this point in the history
  • Loading branch information
sebhoerl committed Nov 13, 2023
1 parent 48949d4 commit b2cd761
Show file tree
Hide file tree
Showing 10 changed files with 652 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,21 +43,24 @@
*/
@Singleton
final class PlansReplanningImpl implements PlansReplanning, ReplanningListener {

private final Provider<ReplanningContext> replanningContextProvider;
private final Population population;
private final StrategyManager strategyManager;
private final ConflictManager conflictManager;

@Inject
PlansReplanningImpl(StrategyManager strategyManager, Population pop, Provider<ReplanningContext> replanningContextProvider) {
PlansReplanningImpl(StrategyManager strategyManager, ConflictManager conflictManager, Population pop,
Provider<ReplanningContext> replanningContextProvider) {
this.population = pop;
this.strategyManager = strategyManager;
this.conflictManager = conflictManager;
this.replanningContextProvider = replanningContextProvider;
}

@Override
public void notifyReplanning(final ReplanningEvent event) {
population.getPersons().values().forEach(ReplanningUtils::setInitialPlan);
strategyManager.run(population, event.getIteration(), replanningContextProvider.get());
conflictManager.run(population, event.getIteration());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <P extends BasicPlan, R> boolean isInnovativeStrategy(GenericPlanStrategy<P, R> planStrategy) {
return !isOnlySelector(planStrategy);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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<ConflictResolver> resolvers;
private final ConflictWriter writer;
private final Random random;

public ConflictManager(Set<ConflictResolver> resolvers, ConflictWriter writer, Random random) {
this.resolvers = resolvers;
this.random = random;
this.writer = writer;
}

public void run(Population population, int iteration) {
if (resolvers.size() == 0) {
return;
}

logger.info("Resolving conflicts ...");

Map<String, Integer> conflictCounts = new HashMap<>();
IdSet<Person> conflictingIds = new IdSet<>(Person.class);

for (ConflictResolver resolver : resolvers) {
IdSet<Person> 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<Person> 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<Plan> 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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() {
}

@Provides
@Singleton
ConflictWriter provideConflictWriter(OutputDirectoryHierarchy outputDirectoryHierarchy) {
File outputPath = new File(outputDirectoryHierarchy.getOutputFilename(OUTPUT_FILE));
return new ConflictWriter(outputPath);
}

@Provides
@Singleton
ConflictManager provideConflictManager(Set<ConflictResolver> 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.getLocalInstance();
return new ConflictManager(resolvers, writer, random);
}

static Multibinder<ConflictResolver> getMultibinder(Binder binder) {
return Multibinder.newSetBinder(binder, ConflictResolver.class);
}

/**
* Allows to bind a conflict resolver in an AbstractModule, for instance:
*
* <code>
* ConflictModule.bindResolver(binder()).toInstance(new ConflictResolver() {
* // ...
* });
* </code>
*/
static public LinkedBindingBuilder<ConflictResolver> bindResolver(Binder binder) {
return getMultibinder(binder).addBinding();
}
}
Original file line number Diff line number Diff line change
@@ -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<Person> resolve(Population population, int iteration);

boolean isPotentiallyConflicting(Plan plan);

String getName();
}
Original file line number Diff line number Diff line change
@@ -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<String, Integer> conflictCounts) {
boolean writeHeader = !outputPath.exists();

List<String> resolvers = new ArrayList<>(conflictCounts.keySet());
Collections.sort(resolvers);

try {
BufferedWriter writer = IOUtils.getAppendingBufferedWriter(outputPath.getPath());

if (writeHeader) {
List<String> 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<String> 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);
}
}
}
Loading

0 comments on commit b2cd761

Please sign in to comment.