Skip to content

Commit

Permalink
IMC contrib maintenance (#3648)
Browse files Browse the repository at this point in the history
* reduce memory usage of mode choice search

* add balanced innovation strategy chooser

* support plans with huge number of combinations

* improve balanced innovation

* improve tests

* more even innovation

* improve tests

* add non normalized mnl selector, update tests

* add additional convenience method

* small improvements for pruning and docs

* check if modes is supposed to be switched

* topk optional in select subtour mode

* remove reference to plan, make select subtour mode consistent with random subtour mode

* fix tests

* don't try to anneal infinity

* improve start time calculation

* filter modes without real usage in single trip generator
  • Loading branch information
rakow authored Dec 30, 2024
1 parent f217386 commit 74ac44f
Show file tree
Hide file tree
Showing 38 changed files with 1,812 additions and 448 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package org.matsim.modechoice;

import com.google.inject.Inject;
import org.matsim.api.core.v01.TransportMode;
import org.matsim.api.core.v01.population.Leg;
import org.matsim.api.core.v01.population.PlanElement;
import org.matsim.core.config.groups.PlansConfigGroup;
import org.matsim.core.router.AnalysisMainModeIdentifier;
import org.matsim.core.router.TripRouter;
import org.matsim.core.router.TripStructureUtils;
import org.matsim.core.utils.timing.TimeInterpretation;
Expand All @@ -23,14 +24,18 @@ public final class EstimateRouter {

private final TripRouter tripRouter;
private final ActivityFacilities facilities;
private final AnalysisMainModeIdentifier mmi;
private final TimeInterpretation timeInterpretation;

@Inject
public EstimateRouter(TripRouter tripRouter, ActivityFacilities facilities,
TimeInterpretation timeInterpretation) {
AnalysisMainModeIdentifier mmi) {
this.tripRouter = tripRouter;
this.facilities = facilities;
this.timeInterpretation = timeInterpretation;
this.mmi = mmi;
// ignore the travel times of individual legs
this.timeInterpretation = TimeInterpretation.create(PlansConfigGroup.ActivityDurationInterpretation.tryEndTimeThenDuration,
PlansConfigGroup.TripDurationHandling.ignoreDelays);
}

/**
Expand Down Expand Up @@ -73,14 +78,14 @@ public void routeModes(PlanModel model, Collection<String> modes) {
}
*/

// Use the end-time of an activity or the time tracker if not available
final List<? extends PlanElement> newTrip = tripRouter.calcRoute(
mode, from, to,
oldTrip.getOriginActivity().getEndTime().orElse(timeTracker.getTime().seconds()),
model.getPerson(),
oldTrip.getTripAttributes()
);

// update time tracker, however it will be updated before each iteration
timeTracker.addElements(newTrip);

// store and increment
Expand All @@ -89,21 +94,6 @@ public void routeModes(PlanModel model, Collection<String> modes) {
.map(el -> (Leg) el)
.collect(Collectors.toList());

// The PT router can return walk only trips that don't actually use pt
// this one special case is handled here, it is unclear if similar behaviour might be present in other modes
if (mode.equals(TransportMode.pt) && ll.stream().noneMatch(l -> l.getMode().equals(TransportMode.pt))) {
legs[i++] = null;
continue;
}

// TODO: might consider access agress walk modes

// Filters all kind of modes that did return only walk legs when they could not be used (e.g. drt)
if (!mode.equals(TransportMode.walk) && ll.stream().allMatch(l -> l.getMode().equals(TransportMode.walk))) {
legs[i++] = null;
continue;
}

legs[i++] = ll;

}
Expand Down Expand Up @@ -146,12 +136,6 @@ public void routeSingleTrip(PlanModel model, Collection<String> modes, int idx)
.map(el -> (Leg) el)
.collect(Collectors.toList());

// not a real pt trip, see reasoning above
if (mode.equals(TransportMode.pt) && ll.stream().noneMatch(l -> l.getMode().equals(TransportMode.pt))) {
model.setLegs(mode, new List[model.trips()]);
continue;
}

List<Leg>[] legs = new List[model.trips()];
legs[idx] = ll;

Expand Down Expand Up @@ -183,10 +167,13 @@ private double advanceTimetracker(TimeTracker timeTracker, TripStructureUtils.Tr
List<Leg> oldLegs = oldTrip.getLegsOnly();
boolean undefined = oldLegs.stream().anyMatch(l -> timeInterpretation.decideOnLegTravelTime(l).isUndefined());

// If no time is known the previous trips need to be routed
// If no time is known the previous trips needs to be routed
if (undefined) {
String routingMode = TripStructureUtils.getRoutingMode(oldLegs.get(0));
List<? extends PlanElement> legs = routeTrip(oldTrip, plan, routingMode != null ? routingMode : oldLegs.get(0).getMode(), timeTracker);
String routingMode = TripStructureUtils.getRoutingMode(oldLegs.getFirst());
if (routingMode == null)
routingMode = mmi.identifyMainMode(oldLegs);

List<? extends PlanElement> legs = routeTrip(oldTrip, plan, routingMode, timeTracker);
timeTracker.addElements(legs);

} else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ public class InformedModeChoiceConfigGroup extends ReflectiveConfigGroup {
" POSITIVE_INFINITY will select randomly from the best k.")
private double invBeta = Double.POSITIVE_INFINITY;

@Parameter
@Comment("Normalize utility values when selecting")
private boolean normalizeUtility = false;

@Parameter
@Comment("Name of the candidate pruner to apply, needs to be bound with guice.")
private String pruning = null;
Expand Down Expand Up @@ -142,6 +146,14 @@ public Map<String, String> getComments() {
return comments;
}

public boolean isNormalizeUtility() {
return normalizeUtility;
}

public void setNormalizeUtility(boolean normalizeUtility) {
this.normalizeUtility = normalizeUtility;
}

public enum Schedule {
off,
linear,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,31 @@ public static void replaceReplanningStrategy(Config config, String subpopulation
// Copy list because it is unmodifiable
List<ReplanningConfigGroup.StrategySettings> strategies = new ArrayList<>(config.replanning().getStrategySettings());
List<ReplanningConfigGroup.StrategySettings> found = strategies.stream()
.filter(s -> s.getSubpopulation().equals(subpopulation))
.filter(s -> subpopulation == null || Objects.equals(s.getSubpopulation(), subpopulation))
.filter(s -> s.getStrategyName().equals(existing))
.toList();

if (found.isEmpty())
throw new IllegalArgumentException("No strategy %s found for subpopulation %s".formatted(existing, subpopulation));

if (found.size() > 1)
if (subpopulation != null && found.size() > 1)
throw new IllegalArgumentException("Multiple strategies %s found for subpopulation %s".formatted(existing, subpopulation));

ReplanningConfigGroup.StrategySettings old = found.getFirst();
old.setStrategyName(replacement);
found.forEach(s -> s.setStrategyName(replacement));

// reset und set new strategies
config.replanning().clearStrategySettings();
strategies.forEach(s -> config.replanning().addStrategySettings(s));
}

/**
* Replace a strategy in the config for all subpoopulations.
* @see #replaceReplanningStrategy(Config, String, String, String)
*/
public static void replaceReplanningStrategy(Config config, String existing, String replacement) {
replaceReplanningStrategy(config, null, existing, replacement);
}

public static Builder newBuilder() {
return new Builder();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.matsim.modechoice;

import com.google.inject.Inject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.matsim.core.config.Config;
Expand Down Expand Up @@ -30,14 +31,16 @@ public final class ModeChoiceWeightScheduler implements StartupListener, Iterati

private InformedModeChoiceConfigGroup.Schedule anneal;

@Override
public void notifyStartup(StartupEvent event) {

Config config = event.getServices().getConfig();
@Inject
public ModeChoiceWeightScheduler(Config config) {
InformedModeChoiceConfigGroup imc = ConfigUtils.addOrGetModule(config, InformedModeChoiceConfigGroup.class);

startBeta = currentBeta = imc.getInvBeta();
anneal = imc.getAnneal();
}

@Override
public void notifyStartup(StartupEvent event) {
Config config = event.getServices().getConfig();

// The first iteration does not do any replanning
n = config.controller().getLastIteration() - 1;
Expand All @@ -52,7 +55,7 @@ public void notifyStartup(StartupEvent event) {
@Override
public void notifyIterationStarts(IterationStartsEvent event) {

if (anneal == InformedModeChoiceConfigGroup.Schedule.off || event.getIteration() == 0)
if (anneal == InformedModeChoiceConfigGroup.Schedule.off || event.getIteration() == 0 || currentBeta == Double.POSITIVE_INFINITY)
return;

// anneal target is 0, iterations are offset by 1 because first iteration does not do replanning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ public final class ModeEstimate {
private final double[] est;
private final double[] tripEst;

/**
* Mark trips with no real usage. E.g pt trips that consist only of walk legs.
* These trips will not be considered during estimation.
*/
private final boolean[] noRealUsage;

/**
* Whether this should be for a minimum estimate. Otherwise, maximum is assumed.
*/
Expand All @@ -42,6 +48,7 @@ public final class ModeEstimate {
this.usable = isUsable;
this.est = usable ? new double[n] : null;
this.tripEst = storeTripEst ? new double[n] : null;
this.noRealUsage = usable ? new boolean[n] : null;
}

public String getMode() {
Expand All @@ -68,6 +75,10 @@ public double[] getTripEstimates() {
return tripEst;
}

public boolean[] getNoRealUsage() {
return noRealUsage;
}

@Override
public String toString() {
return mode + "=" + option + (min ? " (min) " : "");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,6 @@ public final class PlanModel implements Iterable<TripStructureUtils.Trip>, HasPe
*/
private boolean fullyRouted;

/**
* Original plan.
*/
private Plan plan;

/**
* Create a new plan model instance from an existing plan.
*/
Expand All @@ -66,7 +61,6 @@ private PlanModel(Plan plan) {
List<TripStructureUtils.Trip> tripList = TripStructureUtils.getTrips(plan);

this.trips = tripList.toArray(new TripStructureUtils.Trip[0]);
this.plan = plan;
this.legs = new HashMap<>();
this.estimates = new HashMap<>();
this.currentModes = new String[trips.length];
Expand All @@ -80,11 +74,6 @@ public Person getPerson() {
return person;
}

public Plan getPlan() {
// TODO: This should better be removed, memory usage by keeping these plans is increased
return plan;
}

public int trips() {
return trips.length;
}
Expand Down Expand Up @@ -123,8 +112,6 @@ public double[] getStartTimes() {
* Update current plan an underlying modes.
*/
public void setPlan(Plan plan) {
this.plan = plan;

List<TripStructureUtils.Trip> newTrips = TripStructureUtils.getTrips(plan);

if (newTrips.size() != this.trips.length)
Expand Down Expand Up @@ -223,6 +210,13 @@ public TripStructureUtils.Trip getTrip(int i) {
return trips[i];
}

/**
* Get all trips of the day.
*/
public List<TripStructureUtils.Trip> getTrips() {
return Arrays.asList(trips);
}

void setLegs(String mode, List<Leg>[] legs) {
mode = mode.intern();

Expand Down Expand Up @@ -257,6 +251,9 @@ public Map<String, List<ModeEstimate>> getEstimates() {
return estimates;
}

/**
* Iterate over estimates and collect modes that match the predicate.
*/
public Set<String> filterModes(Predicate<? super ModeEstimate> predicate) {
Set<String> modes = new HashSet<>();
for (Map.Entry<String, List<ModeEstimate>> e : estimates.entrySet()) {
Expand Down Expand Up @@ -300,6 +297,20 @@ public List<Leg> getLegs(String mode, int i) {
return legs[i];
}

/**
* Check whether a mode is available for a trip.
* If for instance not pt option is found in the legs this will return false.
*/
public boolean hasModeForTrip(String mode, int i) {

List<Leg>[] legs = this.legs.get(mode);
if (legs == null)
return false;

List<Leg> ll = legs[i];
return ll.stream().anyMatch(l -> l.getMode().equals(mode));
}

/**
* Delete stored routes and estimates.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,10 @@ public void calculateEstimates(EstimatorContext context, PlanModel planModel) {
if (!c.isUsable())
continue;

// All estimates are stored within the objects and modified directly here
double[] values = c.getEstimates();
double[] tValues = c.getTripEstimates();
boolean[] noUsage = c.getNoRealUsage();

// Collect all estimates
for (int i = 0; i < planModel.trips(); i++) {
Expand All @@ -181,13 +183,15 @@ public void calculateEstimates(EstimatorContext context, PlanModel planModel) {
continue;
}

TripEstimator tripEst = tripEstimator.get(c.getMode());
TripEstimator tripEst = tripEstimator.get(c.getMode());

// some options may produce equivalent results, but are re-estimated
// however, the more expensive computation is routing and only done once
boolean realUsage = planModel.hasModeForTrip(c.getMode(), i);
noUsage[i] = !realUsage;

double estimate = 0;
if (tripEst != null) {
if (tripEst != null && realUsage) {
MinMaxEstimate minMax = tripEst.estimate(context, c.getMode(), planModel, legs, c.getOption());
double tripEstimate = c.isMin() ? minMax.getMin() : minMax.getMax();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public RelaxedMassConservationConstraint(SubtourModeChoiceConfigGroup config) {
@Override
public Context getContext(EstimatorContext context, PlanModel model) {

Collection<TripStructureUtils.Subtour> subtours = TripStructureUtils.getSubtours(model.getPlan(), coordDistance);
Collection<TripStructureUtils.Subtour> subtours = TripStructureUtils.getSubtoursFromTrips(model.getTrips(), coordDistance);

Object2IntMap<Object> facilities = new Object2IntArrayMap<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public RelaxedSubtourConstraint(SubtourModeChoiceConfigGroup config) {
@Override
public int[] getContext(EstimatorContext context, PlanModel model) {

Collection<TripStructureUtils.Subtour> subtours = TripStructureUtils.getSubtours(model.getPlan(), coordDistance);
Collection<TripStructureUtils.Subtour> subtours = TripStructureUtils.getSubtoursFromTrips(model.getTrips(), coordDistance);

// ids will contain unique identifier to which subtour a trip belongs.
int[] ids = new int[model.trips()];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,12 @@ default double planThreshold(PlanModel planModel) {
return -1;
}


/**
* Calculate threshold to be applied on a single trip. Modes worse than this threshold on this trip will be discarded.
*
* @return positive threshold, if negative it will not be applied
*/
default double tripThreshold(PlanModel planModel, int idx) {
return -1;
return planThreshold(planModel);
}
}
Loading

0 comments on commit 74ac44f

Please sign in to comment.