diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/EstimateRouter.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/EstimateRouter.java index ea57746fcb7..27e68b0c2f7 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/EstimateRouter.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/EstimateRouter.java @@ -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; @@ -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); } /** @@ -73,7 +78,6 @@ public void routeModes(PlanModel model, Collection modes) { } */ - // Use the end-time of an activity or the time tracker if not available final List newTrip = tripRouter.calcRoute( mode, from, to, oldTrip.getOriginActivity().getEndTime().orElse(timeTracker.getTime().seconds()), @@ -81,6 +85,7 @@ public void routeModes(PlanModel model, Collection modes) { oldTrip.getTripAttributes() ); + // update time tracker, however it will be updated before each iteration timeTracker.addElements(newTrip); // store and increment @@ -89,21 +94,6 @@ public void routeModes(PlanModel model, Collection 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; } @@ -146,12 +136,6 @@ public void routeSingleTrip(PlanModel model, Collection 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[] legs = new List[model.trips()]; legs[idx] = ll; @@ -183,10 +167,13 @@ private double advanceTimetracker(TimeTracker timeTracker, TripStructureUtils.Tr List 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 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 legs = routeTrip(oldTrip, plan, routingMode, timeTracker); timeTracker.addElements(legs); } else diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/InformedModeChoiceConfigGroup.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/InformedModeChoiceConfigGroup.java index 76c9e429e0a..822d78b1c12 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/InformedModeChoiceConfigGroup.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/InformedModeChoiceConfigGroup.java @@ -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; @@ -142,6 +146,14 @@ public Map getComments() { return comments; } + public boolean isNormalizeUtility() { + return normalizeUtility; + } + + public void setNormalizeUtility(boolean normalizeUtility) { + this.normalizeUtility = normalizeUtility; + } + public enum Schedule { off, linear, diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/InformedModeChoiceModule.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/InformedModeChoiceModule.java index 6c422644a14..cad6cb65579 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/InformedModeChoiceModule.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/InformedModeChoiceModule.java @@ -54,24 +54,31 @@ public static void replaceReplanningStrategy(Config config, String subpopulation // Copy list because it is unmodifiable List strategies = new ArrayList<>(config.replanning().getStrategySettings()); List 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(); } diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ModeChoiceWeightScheduler.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ModeChoiceWeightScheduler.java index 5d0eed4bb41..e286ab4deda 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ModeChoiceWeightScheduler.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ModeChoiceWeightScheduler.java @@ -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; @@ -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; @@ -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 diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ModeEstimate.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ModeEstimate.java index 6b08caba569..078b649985f 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ModeEstimate.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ModeEstimate.java @@ -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. */ @@ -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() { @@ -68,6 +75,10 @@ public double[] getTripEstimates() { return tripEst; } + public boolean[] getNoRealUsage() { + return noRealUsage; + } + @Override public String toString() { return mode + "=" + option + (min ? " (min) " : ""); diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/PlanModel.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/PlanModel.java index cfb4c3f7966..6d85a756d99 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/PlanModel.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/PlanModel.java @@ -48,11 +48,6 @@ public final class PlanModel implements Iterable, HasPe */ private boolean fullyRouted; - /** - * Original plan. - */ - private Plan plan; - /** * Create a new plan model instance from an existing plan. */ @@ -66,7 +61,6 @@ private PlanModel(Plan plan) { List 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]; @@ -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; } @@ -123,8 +112,6 @@ public double[] getStartTimes() { * Update current plan an underlying modes. */ public void setPlan(Plan plan) { - this.plan = plan; - List newTrips = TripStructureUtils.getTrips(plan); if (newTrips.size() != this.trips.length) @@ -223,6 +210,13 @@ public TripStructureUtils.Trip getTrip(int i) { return trips[i]; } + /** + * Get all trips of the day. + */ + public List getTrips() { + return Arrays.asList(trips); + } + void setLegs(String mode, List[] legs) { mode = mode.intern(); @@ -257,6 +251,9 @@ public Map> getEstimates() { return estimates; } + /** + * Iterate over estimates and collect modes that match the predicate. + */ public Set filterModes(Predicate predicate) { Set modes = new HashSet<>(); for (Map.Entry> e : estimates.entrySet()) { @@ -300,6 +297,20 @@ public List 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[] legs = this.legs.get(mode); + if (legs == null) + return false; + + List ll = legs[i]; + return ll.stream().anyMatch(l -> l.getMode().equals(mode)); + } + /** * Delete stored routes and estimates. */ diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/PlanModelService.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/PlanModelService.java index 93e0fb86c67..96ad71e0af8 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/PlanModelService.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/PlanModelService.java @@ -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++) { @@ -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(); diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/constraints/RelaxedMassConservationConstraint.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/constraints/RelaxedMassConservationConstraint.java index 5432f3b1649..6e2583e7072 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/constraints/RelaxedMassConservationConstraint.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/constraints/RelaxedMassConservationConstraint.java @@ -37,7 +37,7 @@ public RelaxedMassConservationConstraint(SubtourModeChoiceConfigGroup config) { @Override public Context getContext(EstimatorContext context, PlanModel model) { - Collection subtours = TripStructureUtils.getSubtours(model.getPlan(), coordDistance); + Collection subtours = TripStructureUtils.getSubtoursFromTrips(model.getTrips(), coordDistance); Object2IntMap facilities = new Object2IntArrayMap<>(); diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/constraints/RelaxedSubtourConstraint.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/constraints/RelaxedSubtourConstraint.java index aa8c03fbd8e..de3f54e7dcc 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/constraints/RelaxedSubtourConstraint.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/constraints/RelaxedSubtourConstraint.java @@ -33,7 +33,7 @@ public RelaxedSubtourConstraint(SubtourModeChoiceConfigGroup config) { @Override public int[] getContext(EstimatorContext context, PlanModel model) { - Collection subtours = TripStructureUtils.getSubtours(model.getPlan(), coordDistance); + Collection subtours = TripStructureUtils.getSubtoursFromTrips(model.getTrips(), coordDistance); // ids will contain unique identifier to which subtour a trip belongs. int[] ids = new int[model.trips()]; diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/pruning/CandidatePruner.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/pruning/CandidatePruner.java index ef2cc7af044..e785177ab8a 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/pruning/CandidatePruner.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/pruning/CandidatePruner.java @@ -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); } } diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/MultinomialLogitSelector.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/MultinomialLogitSelector.java index 6546714116f..d58ec06825d 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/MultinomialLogitSelector.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/MultinomialLogitSelector.java @@ -34,7 +34,6 @@ public class MultinomialLogitSelector implements PlanSelector { - private final static Logger log = LogManager.getLogger(MultinomialLogitSelector.class); private final double scale; private final Random rnd; @@ -61,42 +60,32 @@ public PlanCandidate select(Collection candidates) { return candidates.stream().skip(rnd.nextInt(candidates.size())).findFirst().orElse(null); } - // if two option are exactly the same this will be incorrect - // for very small scales, exp overflows, this function needs to return best solution at this point - if (scale <= 1 / 700d) { + // for very small scales, avoid numerical issues by using the best candidate + if (scale <= 1e-8) { return candidates.stream().sorted().findFirst().orElse(null); } - // III) Create a probability distribution over candidates - DoubleList density = new DoubleArrayList(candidates.size()); - List pcs = new ArrayList<>(candidates); + // Subtract the maximum utility to avoid overflow + double max = candidates.stream().mapToDouble(PlanCandidate::getUtility).max().orElseThrow() / scale; - double min = candidates.stream().mapToDouble(PlanCandidate::getUtility).min().orElseThrow(); - double scale = candidates.stream().mapToDouble(PlanCandidate::getUtility).max().orElseThrow() - min; - - // For very small differences the small is ignored - if (scale < 1e-6) - scale = 1; - - for (PlanCandidate candidate : pcs) { - double utility = (candidate.getUtility() - min) / scale; - density.add(Math.exp(utility / this.scale)); - } - - // IV) Build a cumulative density of the distribution - DoubleList cumulativeDensity = new DoubleArrayList(density.size()); + // III) Build a cumulative density of the distribution + double[] cumulativeDensity = new double[candidates.size()]; + int i = 0; double totalDensity = 0.0; - for (int i = 0; i < density.size(); i++) { - totalDensity += density.getDouble(i); - cumulativeDensity.add(totalDensity); + for (PlanCandidate candidate : candidates) { + double utility = (candidate.getUtility() / scale) - max; + double exp = Math.exp(utility); + + totalDensity += exp; + cumulativeDensity[i++] = totalDensity; } // V) Perform a selection using the CDF double pointer = rnd.nextDouble() * totalDensity; - int selection = (int) cumulativeDensity.doubleStream().filter(f -> f < pointer).count(); - return pcs.get(selection); + int selection = (int) Arrays.stream(cumulativeDensity).filter(f -> f < pointer).count(); + return candidates.stream().skip(selection).findFirst().orElse(null); } @@ -132,7 +121,7 @@ public static void main(String[] args) throws IOException { printer.println(); - double start = 2.5; + double start = 10; int n = 100; for (int i = 0; i <= n; i++) { diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/MultinomialLogitSelectorProvider.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/MultinomialLogitSelectorProvider.java index d0490415d4c..d8fa0040cef 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/MultinomialLogitSelectorProvider.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/MultinomialLogitSelectorProvider.java @@ -1,7 +1,10 @@ package org.matsim.modechoice.replanning; import com.google.inject.Provider; +import org.matsim.core.config.Config; +import org.matsim.core.config.ConfigUtils; import org.matsim.core.gbl.MatsimRandom; +import org.matsim.modechoice.InformedModeChoiceConfigGroup; import org.matsim.modechoice.ModeChoiceWeightScheduler; import jakarta.inject.Inject; @@ -14,8 +17,17 @@ public class MultinomialLogitSelectorProvider implements Provider @Inject private ModeChoiceWeightScheduler weights; + @Inject + private Config config; + @Override public PlanSelector get() { + + InformedModeChoiceConfigGroup c = ConfigUtils.addOrGetModule(config, InformedModeChoiceConfigGroup.class); + if (c.isNormalizeUtility()) { + return new NormalizedMultinomialLogitSelector(weights.getInvBeta(), MatsimRandom.getLocalInstance()); + } + return new MultinomialLogitSelector(weights.getInvBeta(), MatsimRandom.getLocalInstance()); } } diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/NormalizedMultinomialLogitSelector.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/NormalizedMultinomialLogitSelector.java new file mode 100644 index 00000000000..a261950830a --- /dev/null +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/NormalizedMultinomialLogitSelector.java @@ -0,0 +1,147 @@ +package org.matsim.modechoice.replanning; + +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.matsim.modechoice.InformedModeChoiceConfigGroup; +import org.matsim.modechoice.ModeChoiceWeightScheduler; +import org.matsim.modechoice.PlanCandidate; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +; + +/** + * Multinomial logit selector with normalized utilities. + * + * @see MultinomialLogitSelector + */ +public class NormalizedMultinomialLogitSelector implements PlanSelector { + + private final double scale; + private final Random rnd; + + /** + * @param scale if scale is 0, always the best option will be picked. + */ + public NormalizedMultinomialLogitSelector(double scale, Random rnd) { + this.scale = scale; + this.rnd = rnd; + } + + @Nullable + @Override + public PlanCandidate select(Collection candidates) { + + if (candidates.isEmpty()) + return null; + + if (candidates.size() == 1) + return candidates.iterator().next(); + + // Short-path to do completely random selection + if (scale == Double.POSITIVE_INFINITY) { + return candidates.stream().skip(rnd.nextInt(candidates.size())).findFirst().orElse(null); + } + + // if two option are exactly the same this will be incorrect + // for very small scales, exp overflows, this function needs to return best solution at this point + if (scale <= 1 / 700d) { + return candidates.stream().sorted().findFirst().orElse(null); + } + + // III) Create a probability distribution over candidates + DoubleList density = new DoubleArrayList(candidates.size()); + List pcs = new ArrayList<>(candidates); + + double min = candidates.stream().mapToDouble(PlanCandidate::getUtility).min().orElseThrow(); + double scale = candidates.stream().mapToDouble(PlanCandidate::getUtility).max().orElseThrow() - min; + + // For very small differences the small is ignored + if (scale < 1e-6) + scale = 1; + + for (PlanCandidate candidate : pcs) { + double utility = (candidate.getUtility() - min) / scale; + density.add(Math.exp(utility / this.scale)); + } + + // IV) Build a cumulative density of the distribution + DoubleList cumulativeDensity = new DoubleArrayList(density.size()); + double totalDensity = 0.0; + + for (int i = 0; i < density.size(); i++) { + totalDensity += density.getDouble(i); + cumulativeDensity.add(totalDensity); + } + + // V) Perform a selection using the CDF + double pointer = rnd.nextDouble() * totalDensity; + + int selection = (int) cumulativeDensity.doubleStream().filter(f -> f < pointer).count(); + return pcs.get(selection); + } + + + /** + * Write debug information regarding choice probability. + */ + public static void main(String[] args) throws IOException { + + if (args.length < 2) { + System.out.println("Provide at least two choices."); + return; + } + + for (InformedModeChoiceConfigGroup.Schedule schedule : List.of(InformedModeChoiceConfigGroup.Schedule.linear, InformedModeChoiceConfigGroup.Schedule.quadratic, InformedModeChoiceConfigGroup.Schedule.cubic, + InformedModeChoiceConfigGroup.Schedule.exponential, InformedModeChoiceConfigGroup.Schedule.trigonometric)) { + + List candidates = new ArrayList<>(); + + for (String arg : args) { + candidates.add(new PlanCandidate(new String[]{arg}, Double.parseDouble(arg))); + } + + Path p = Path.of("choices-" + args.length + "-" + schedule + ".tsv"); + + System.out.println("Writing to " + p); + + CSVPrinter printer = new CSVPrinter(Files.newBufferedWriter(p), CSVFormat.MONGODB_TSV); + + printer.print("x"); + printer.print("weight"); + for (int i = 0; i < candidates.size(); i++) + printer.print(String.format(Locale.US, "choice %d = %.2f", i, candidates.get(i).getUtility())); + + printer.println(); + + double start = 2.5; + int n = 100; + + for (int i = 0; i <= n; i++) { + + double weight = ModeChoiceWeightScheduler.anneal(schedule, start, n, i); + + System.out.println("Sampling weight " + weight + " @ iteration " + i); + + NormalizedMultinomialLogitSelector selector = new NormalizedMultinomialLogitSelector(weight, new Random()); + double[] prob = selector.sample(100_000, candidates); + + printer.print(i); + printer.print(weight); + for (double v : prob) { + printer.print(v); + } + printer.println(); + } + + printer.close(); + } + } + +} diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/RandomSubtourModeStrategy.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/RandomSubtourModeStrategy.java index 85c1cbce7a9..9e0f9e4af22 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/RandomSubtourModeStrategy.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/RandomSubtourModeStrategy.java @@ -81,14 +81,15 @@ public void run(Plan plan) { // only select trips that are allowed to change for (int i = 0; i < model.trips(); i++) { - if (nonChainBasedModes.contains(model.getTripMode(i))) { + String m = model.getTripMode(i); + if (nonChainBasedModes.contains(m) && switchModes.contains(m)) { options.add(i); } } - if (!options.isEmpty()) { + while (!options.isEmpty()) { - int idx = options.getInt(rnd.nextInt(options.size())); + int idx = options.removeInt(rnd.nextInt(options.size())); String[] current = model.getCurrentModes(); @@ -97,9 +98,14 @@ public void run(Plan plan) { if (config.isRequireDifferentModes()) candidates.remove(current[idx]); - if (!candidates.isEmpty()) { + while (!candidates.isEmpty()) { + + current[idx] = candidates.remove(rnd.nextInt(candidates.size())); + + // Check further constraints + if (!planModelService.isValidOption(model, current)) + continue; - current[idx] = candidates.get(rnd.nextInt(candidates.size())); new PlanCandidate(current, -1).applyTo(plan); return; } @@ -141,7 +147,6 @@ public void run(Plan plan) { } - if (!candidates.isEmpty()) { PlanCandidate select = candidates.get(rnd.nextInt(candidates.size())); select.applyTo(plan); diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/SelectSingleTripModeStrategy.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/SelectSingleTripModeStrategy.java index 89b50019936..eceec8767c8 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/SelectSingleTripModeStrategy.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/SelectSingleTripModeStrategy.java @@ -31,13 +31,13 @@ public class SelectSingleTripModeStrategy extends AbstractMultithreadedModule { private final Provider generator; private final Provider selector; - private final List modes; + private final Set modes; private final Provider pruner; private final boolean requireDifferentModes; public SelectSingleTripModeStrategy(GlobalConfigGroup globalConfigGroup, - List modes, + Set modes, Provider generator, Provider selector, Provider pruner, boolean requireDifferentModes) { @@ -81,7 +81,7 @@ public Algorithm(SingleTripChoicesGenerator generator, PlanSelector selector, Ca public void run(Plan plan) { PlanModel model = PlanModel.newInstance(plan); - PlanCandidate c = chooseCandidate(model, null); + PlanCandidate c = chooseCandidate(model); if (c != null) { if (pruner != null) { @@ -96,11 +96,10 @@ public void run(Plan plan) { /** * Choose one candidate with one single trip changed. * - * @param avoidList combinations to avoid, can be null - * @return true if a candidate was selected + * @return selected candidate or null if no candidate is found */ @Nullable - public PlanCandidate chooseCandidate(PlanModel model, @Nullable Collection avoidList) { + public PlanCandidate chooseCandidate(PlanModel model) { // empty plan if (model.trips() == 0) @@ -117,33 +116,12 @@ public PlanCandidate chooseCandidate(PlanModel model, @Nullable Collection candidates = generator.generate(model, modes, mask); @@ -166,16 +144,6 @@ public PlanCandidate chooseCandidate(PlanModel model, @Nullable Collection Objects.equals(c.getMode(idx), model.getTripMode(idx))); - // Remove avoided combinations - if (avoidList != null) { - String[] current = model.getCurrentModes(); - - candidates.removeIf(c -> { - current[idx] = c.getMode(idx); - return avoidList.contains(current); - }); - } - PlanCandidate selected = selector.select(candidates); log.debug("Candidates for person {} at trip {}: {} | selected {}", model.getPerson(), idx, candidates, selected); diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/SelectSingleTripModeStrategyProvider.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/SelectSingleTripModeStrategyProvider.java index dd9cea36045..87970ec9e8d 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/SelectSingleTripModeStrategyProvider.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/SelectSingleTripModeStrategyProvider.java @@ -15,6 +15,8 @@ import jakarta.inject.Inject; import jakarta.inject.Provider; +import java.util.HashSet; + /** * Provider for {@link SelectFromGeneratorStrategy}. */ @@ -47,7 +49,7 @@ public PlanStrategy get() { PlanStrategyImpl.Builder builder = new PlanStrategyImpl.Builder(new RandomPlanSelector<>()); - builder.addStrategyModule(new SelectSingleTripModeStrategy(globalConfigGroup, config.getModes(), generator, selector, pruner, config.isRequireDifferentModes())); + builder.addStrategyModule(new SelectSingleTripModeStrategy(globalConfigGroup, new HashSet<>(config.getModes()), generator, selector, pruner, config.isRequireDifferentModes())); builder.addStrategyModule(new ReRoute(facilities, tripRouterProvider, globalConfigGroup, timeInterpretation)); diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/SelectSubtourModeStrategy.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/SelectSubtourModeStrategy.java index 21863fe785e..3c6b74665a5 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/SelectSubtourModeStrategy.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/SelectSubtourModeStrategy.java @@ -104,7 +104,7 @@ public void run(Plan plan) { // Do change single trip on non-chain based modes with certain probability if (rnd.nextDouble() < smc.getProbaForRandomSingleTripMode() && hasSingleTripChoice(model, nonChainBasedModes)) { - PlanCandidate c = singleTrip.chooseCandidate(model, null); + PlanCandidate c = singleTrip.chooseCandidate(model); if (c != null) { c.applyTo(plan); return; @@ -129,9 +129,6 @@ public void run(Plan plan) { List options = new ArrayList<>(); - // current mode for comparison - options.add(model.getCurrentModesMutable()); - // generate all single mode options for (String m : config.getModes()) { String[] option = model.getCurrentModes(); @@ -140,11 +137,9 @@ public void run(Plan plan) { for (int i = 0; i < mask.length; i++) { if (mask[i]) option[i] = m; - } - // Current option is not added twice - if (!Arrays.equals(model.getCurrentModesMutable(), option)) options.add(option); + } } List singleModeCandidates = ctx.generator.generatePredefined(model, options); @@ -156,16 +151,16 @@ public void run(Plan plan) { continue; } - Set candidateSet = new LinkedHashSet<>(); - // Single modes are also added - candidateSet.addAll(singleModeCandidates); + Set candidateSet = new LinkedHashSet<>(singleModeCandidates); // one could either allow all modes here or only non chain based // config switch might be useful to investigate which option is better - // execute best k modes - candidateSet.addAll(ctx.generator.generate(model, nonChainBasedModes, mask)); + // execute best k modes, setting k to 0 disables this, then this strategy is similar to random subtour + if (config.getTopK() > 0) { + candidateSet.addAll(ctx.generator.generate(model, nonChainBasedModes, mask)); + } // candidates are unique after this List candidates = new ArrayList<>(candidateSet); @@ -176,9 +171,13 @@ public void run(Plan plan) { // Pruning is applied based on current plan estimate // best k generator applied pruning already, but the single trip options need to be checked again if (ctx.pruner != null) { - double threshold = ctx.pruner.planThreshold(model); - if (!Double.isNaN(threshold) && threshold > 0) { - candidates.removeIf(c -> c.getUtility() < singleModeCandidates.get(0).getUtility() - threshold); + + OptionalDouble max = candidates.stream().mapToDouble(PlanCandidate::getUtility).max(); + double t = ctx.pruner.planThreshold(model); + + if (max.isPresent() && t >= 0) { + double threshold = max.getAsDouble() - t; + candidates.removeIf(c -> c.getUtility() < threshold); } // Only applied at the end diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeArrayIterator.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeArrayIterator.java new file mode 100644 index 00000000000..288fc008902 --- /dev/null +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeArrayIterator.java @@ -0,0 +1,130 @@ +package org.matsim.modechoice.search; + +import it.unimi.dsi.fastutil.objects.ObjectHeapPriorityQueue; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Searches all possible combination by using a heap. Uses arrays to store combinations of unlimited size. + */ +final class ModeArrayIterator implements ModeIterator { + + private final String[] result; + private final double[] max; + private final double best; + private final double[][] estimates; + private final ModeChoiceSearch search; + private final ObjectHeapPriorityQueue heap; + private final Set seen; + + + public ModeArrayIterator(String[] result, double[] max, Entry shortest, double best, ModeChoiceSearch search) { + this.result = result; + this.max = max; + this.best = best; + this.estimates = search.estimates; + this.search = search; + this.heap = new ObjectHeapPriorityQueue<>(); + this.seen = new HashSet<>(); + + heap.enqueue(shortest); + seen.add(shortest); + } + + @Override + public double nextDouble() { + + Entry entry = heap.dequeue(); + + for (int i = 0; i < result.length; i++) { + + byte mode = -1; + byte originalMode = entry.modes[i]; + + // This mode had no options + if (originalMode == -1) + continue; + + double min = estimates[i][originalMode]; + double max = Double.NEGATIVE_INFINITY; + + // search for a deviation that is worse than the current mode + + for (byte j = 0; j < estimates[i].length; j++) { + if (estimates[i][j] <= min && j != originalMode && estimates[i][j] > max) { + max = estimates[i][j]; + mode = j; + } + } + + if (mode != -1) { + byte[] path = Arrays.copyOf(entry.modes, entry.modes.length); + path[i] = mode; + + // recompute the deviation from the maximum + // there might be a way to store and update this, without recomputing + + double dev = 0; + for (int j = 0; j < result.length; j++) { + byte legMode = path[j]; + if (legMode == -1) + continue; + + dev += this.max[j] - estimates[j][legMode]; + } + + Entry e = new Entry(path, dev); + + if (!seen.contains(e)) { + heap.enqueue(e); + seen.add(e); + } + } + } + + + search.convert(entry.modes, result); + return best - entry.deviation; + } + + @Override + public boolean hasNext() { + return !heap.isEmpty(); + } + + @Override + public int maxIters() { + return 1_000_000; + } + + record Entry(byte[] modes, double deviation) implements Comparable { + + @Override + public int compareTo(Entry o) { + return Double.compare(deviation, o.deviation); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Entry entry = (Entry) o; + return Double.compare(entry.deviation, deviation) == 0 && Arrays.equals(modes, entry.modes); + } + + @Override + public int hashCode() { + int result = Objects.hash(deviation); + result = 31 * result + Arrays.hashCode(modes); + return result; + } + + @Override + public String toString() { + return Arrays.toString(modes) + " = " + deviation; + } + } +} diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeChoiceSearch.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeChoiceSearch.java index 755fb3d006c..75162604f81 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeChoiceSearch.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeChoiceSearch.java @@ -2,13 +2,10 @@ import it.unimi.dsi.fastutil.bytes.Byte2ObjectMap; import it.unimi.dsi.fastutil.bytes.Byte2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.doubles.DoubleIterator; -import it.unimi.dsi.fastutil.objects.*; +import it.unimi.dsi.fastutil.objects.Object2ByteMap; +import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; import java.util.Arrays; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; /** * This class finds the best solutions by doing an exhaustive search over the best possible combinations of modes. @@ -18,13 +15,16 @@ final class ModeChoiceSearch { /** * Stores the estimates for all modes by trip x mode */ - private final double[][] estimates; + final double[][] estimates; /** * Mode to index mapping. */ + final Byte2ObjectMap inv; + final int depth; + private final Object2ByteMap mapping; - private final Byte2ObjectMap inv; + private final double maxSize; /** * Constructor @@ -32,12 +32,17 @@ final class ModeChoiceSearch { * @param trips number of trips * @param modes max number of modes per trip */ - @SuppressWarnings("unchecked") ModeChoiceSearch(int trips, int modes) { - estimates = new double[trips][modes]; - mapping = new Object2ByteOpenHashMap<>(); - inv = new Byte2ObjectOpenHashMap<>(); + this.estimates = new double[trips][modes]; + this.mapping = new Object2ByteOpenHashMap<>(); + this.inv = new Byte2ObjectOpenHashMap<>(); + // Number of options per trip, +1 to encode null value + this.depth = modes + 1; + + maxSize = Math.pow(depth, trips); + if (!Double.isFinite(maxSize)) + throw new IllegalArgumentException("Too many mode combinations: %s ^ %s".formatted(modes, trips)); clear(); } @@ -49,7 +54,7 @@ final class ModeChoiceSearch { * @param result mode assignment will be written into this array, no memory is allocated during search. * @return iterator returning the summed utility. */ - public DoubleIterator iter(String[] result) { + public ModeIterator iter(String[] result) { assert result.length == estimates.length; @@ -79,25 +84,44 @@ public DoubleIterator iter(String[] result) { total += max; } + long b = 1; + for (int i = 0; i < this.estimates.length - 1; i++) { + b *= depth; + } + + if (maxSize < Integer.MAX_VALUE) { + return new ModeIntIterator(result, maxs, new ModeIntIterator.Entry(path, depth, 0), total, (int) b, this); + } + + if (maxSize < Long.MAX_VALUE) { + return new ModeLongIterator(result, maxs, new ModeLongIterator.Entry(path, depth, 0), total, b, this); + } - return new Iterator(result, maxs, new Entry(path, 0), total); + return new ModeArrayIterator(result, maxs, new ModeArrayIterator.Entry(path, 0), total, this); } /** * Copy estimates into the internal map. */ public void addEstimates(String mode, double[] values) { - addEstimates(mode, values, null); + addEstimates(mode, values, null, null); } - public void addEstimates(String mode, double[] values, boolean[] mask) { + /** + * Copy estimates into the internal map. + * @param mode added mode + * @param values utility estimates + * @param mask only use estimates where the mask is true + * @param filter only use estimates where the filter is false + */ + public void addEstimates(String mode, double[] values, boolean[] mask, boolean[] filter) { byte idx = mapping.computeIfAbsent(mode, k -> (byte) mapping.size()); inv.putIfAbsent(idx, mode); // estimates needs to be accessed by each trip index first and then by mode for (int i = 0; i < values.length; i++) { - if (mask == null || mask[i]) + if ( (mask == null || mask[i]) && (filter == null || !filter[i]) ) estimates[i][idx] = values[i]; } } @@ -147,7 +171,7 @@ public String toString() { /** * Byte representation as string array. */ - private void convert(byte[] path, String[] modes) { + void convert(byte[] path, String[] modes) { for (int i = 0; i < path.length; i++) { String m = inv.get(path[i]); // Pre-defined entries are not touched @@ -156,125 +180,4 @@ private void convert(byte[] path, String[] modes) { } } - - /** - * Searches all possible combination by using a heap. - */ - final class Iterator implements DoubleIterator { - - // This implementation uses a heap and stores all seen combinations - // Because the graph structure is fixed, it does not need to be as complicated as other k shortest path algorithms - // Two major improvements may be investigated: - // Could this be implemented with pre allocated memory and without objects ? - // Do all seen combinations need to be stored ? - // This is the case in Yens algorithms and other and will consume a lot of memory, when many top k paths are generated and a lot are thrown away. - - private final String[] result; - private final double[] max; - private final double best; - private final ObjectHeapPriorityQueue heap; - private final Set seen; - - - public Iterator(String[] result, double[] max, Entry shortest, double best) { - this.result = result; - this.max = max; - this.best = best; - this.heap = new ObjectHeapPriorityQueue<>(); - this.seen = new HashSet<>(); - - heap.enqueue(shortest); - seen.add(shortest); - } - - @Override - public double nextDouble() { - - Entry entry = heap.dequeue(); - - for (int i = 0; i < result.length; i++) { - - byte mode = -1; - byte originalMode = entry.modes[i]; - - // This mode had no options - if (originalMode == -1) - continue; - - double min = estimates[i][originalMode]; - double max = Double.NEGATIVE_INFINITY; - - // search for a deviation that is worse than the current mode - - for (byte j = 0; j < estimates[i].length; j++) { - if (estimates[i][j] <= min && j != originalMode && estimates[i][j] > max) { - max = estimates[i][j]; - mode = j; - } - } - - if (mode != -1) { - byte[] path = Arrays.copyOf(entry.modes, entry.modes.length); - path[i] = mode; - - // recompute the deviation from the maximum - // there might be a way to store and update this, without recomputing - - double dev = 0; - for (int j = 0; j < result.length; j++) { - byte legMode = path[j]; - if (legMode == -1) - continue; - - dev += this.max[j] - estimates[j][legMode]; - } - - Entry e = new Entry(path, dev); - - if (!seen.contains(e)) { - heap.enqueue(e); - seen.add(e); - } - } - } - - - convert(entry.modes, result); - return best - entry.deviation; - } - - @Override - public boolean hasNext() { - return !heap.isEmpty(); - } - } - - private record Entry(byte[] modes, double deviation) implements Comparable { - - @Override - public int compareTo(Entry o) { - return Double.compare(deviation, o.deviation); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Entry entry = (Entry) o; - return Double.compare(entry.deviation, deviation) == 0 && Arrays.equals(modes, entry.modes); - } - - @Override - public int hashCode() { - int result = Objects.hash(deviation); - result = 31 * result + Arrays.hashCode(modes); - return result; - } - - @Override - public String toString() { - return Arrays.toString(modes) + " = " + deviation; - } - } - } diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeIntIterator.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeIntIterator.java new file mode 100644 index 00000000000..37ba4e2310b --- /dev/null +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeIntIterator.java @@ -0,0 +1,189 @@ +package org.matsim.modechoice.search; + +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.objects.ObjectHeapPriorityQueue; + +import java.util.Objects; + +/** + * Copy of the {@link ModeLongIterator} that uses an int instead of a long to store the index. + * @see ModeLongIterator + */ +final class ModeIntIterator implements ModeIterator { + + + private final String[] result; + private final byte[] modes; + private final double[] max; + private final double best; + private final double[][] estimates; + private final int base; + private final int depth; + private final ModeChoiceSearch search; + private final ObjectHeapPriorityQueue heap; + private final IntSet seen; + + + ModeIntIterator(String[] result, double[] max, Entry shortest, double best, int base, ModeChoiceSearch search) { + + this.result = result; + this.modes = new byte[result.length]; + this.max = max; + this.best = best; + this.estimates = search.estimates; + this.base = base; + this.depth = search.depth; + this.search = search; + this.heap = new ObjectHeapPriorityQueue<>(); + this.seen = new IntOpenHashSet(); + + heap.enqueue(shortest); + seen.add(shortest.index); + } + + @Override + public double nextDouble() { + + Entry entry = heap.dequeue(); + // Create the array of indices from the entry + entry.toArray(modes, base, depth); + + for (int i = 0; i < result.length; i++) { + + byte mode = -1; + byte originalMode = modes[i]; + + // This mode had no options + if (originalMode == -1) + continue; + + double min = estimates[i][originalMode]; + double max = Double.NEGATIVE_INFINITY; + + // search for a deviation that is worse than the current mode + + for (byte j = 0; j < estimates[i].length; j++) { + if (estimates[i][j] <= min && j != originalMode && estimates[i][j] > max) { + max = estimates[i][j]; + mode = j; + } + } + + if (mode != -1) { + int newIdx = Entry.toIndex(modes, depth, i, mode); + + // recompute the deviation from the maximum + // there might be a way to store and update this, without recomputing + double dev = 0; + for (int j = 0; j < result.length; j++) { + // Use either the replaced mode or the original mode + byte legMode = j == i ? mode : modes[j]; + + if (legMode == -1) + continue; + + dev += this.max[j] - estimates[j][legMode]; + } + + Entry e = new Entry(newIdx, dev); + + if (!seen.contains(e.index)) { + heap.enqueue(e); + seen.add(e.index); + } + } + } + + search.convert(modes, result); + return best - entry.deviation; + } + + @Override + public boolean hasNext() { + return !heap.isEmpty(); + } + + static final class Entry implements Comparable { + private final int index; + private final double deviation; + + Entry(byte[] modes, int depth, double deviation) { + this.index = toIndex(modes, depth); + this.deviation = deviation; + } + + Entry(int index, double deviation) { + this.index = index; + this.deviation = deviation; + } + + static int toIndex(byte[] modes, int depth) { + int result = modes[0] + 1; + int base = depth; + for (int i = 1; i < modes.length; i++) { + result += (modes[i] + 1) * base; + base *= depth; + } + + return result; + } + + /** + * Convert the mode array to an index, where one mode at index {@code idx} is replaced. + */ + static int toIndex(byte[] modes, int depth, int idx, byte mode) { + int result = (idx == 0 ? mode : modes[0]) + 1; + int base = depth; + for (int i = 1; i < modes.length; i++) { + result += ((idx == i ? mode : modes[i]) + 1) * base; + base *= depth; + } + + return result; + } + + @Override + public int compareTo(Entry o) { + return Double.compare(deviation, o.deviation); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Entry entry = (Entry) o; + return Double.compare(entry.deviation, deviation) == 0 && index == entry.index; + } + + @Override + public int hashCode() { + int result = Objects.hash(deviation); + result = 31 * result + Long.hashCode(index); + return result; + } + + @Override + public String toString() { + return "idx: " + index + " = " + deviation; + } + + public long getIndex() { + return index; + } + + void toArray(byte[] array, int base, int depth) { + int idx = index; + for (int i = array.length - 1; i > 0; i--) { + + int v = idx / base; + array[i] = (byte) (v - 1); + + idx -= v * base; + base /= depth; + } + + array[0] = (byte) (idx - 1); + } + } +} diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeIterator.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeIterator.java new file mode 100644 index 00000000000..604bfea3d4e --- /dev/null +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeIterator.java @@ -0,0 +1,16 @@ +package org.matsim.modechoice.search; + +import it.unimi.dsi.fastutil.doubles.DoubleIterator; + +sealed interface ModeIterator extends DoubleIterator permits ModeArrayIterator, ModeLongIterator, ModeIntIterator { + + /** + * Maximum number of iterations. Memory usage will increase the more iterations are done. + */ + int MAX_ITER = 10_000_000; + + default int maxIters() { + return MAX_ITER; + } + +} diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeLongIterator.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeLongIterator.java new file mode 100644 index 00000000000..01818db4c44 --- /dev/null +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/ModeLongIterator.java @@ -0,0 +1,194 @@ +package org.matsim.modechoice.search; + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import it.unimi.dsi.fastutil.objects.ObjectHeapPriorityQueue; + +import java.util.Objects; + +/** + * Searches all possible combination by using a heap. + */ +final class ModeLongIterator implements ModeIterator { + + // This implementation uses a heap and stores all seen combinations + // Because the graph structure is fixed, it does not need to be as complicated as other k shortest path algorithms + // Two major improvements may be investigated: + // Could this be implemented with pre allocated memory and without objects ? + // Do all seen combinations need to be stored ? + // This is the case in Yens algorithms and other and will consume a lot of memory, when many top k paths are generated and a lot are thrown away. + + private final String[] result; + private final byte[] modes; + private final double[] max; + private final double best; + private final double[][] estimates; + private final long base; + private final int depth; + private final ModeChoiceSearch search; + private final ObjectHeapPriorityQueue heap; + private final LongSet seen; + + + ModeLongIterator(String[] result, double[] max, Entry shortest, double best, long base, ModeChoiceSearch search) { + + this.result = result; + this.modes = new byte[result.length]; + this.max = max; + this.best = best; + this.estimates = search.estimates; + this.base = base; + this.depth = search.depth; + this.search = search; + this.heap = new ObjectHeapPriorityQueue<>(); + this.seen = new LongOpenHashSet(); + + heap.enqueue(shortest); + seen.add(shortest.index); + } + + @Override + public double nextDouble() { + + Entry entry = heap.dequeue(); + // Create the array of indices from the entry + entry.toArray(modes, base, depth); + + for (int i = 0; i < result.length; i++) { + + byte mode = -1; + byte originalMode = modes[i]; + + // This mode had no options + if (originalMode == -1) + continue; + + double min = estimates[i][originalMode]; + double max = Double.NEGATIVE_INFINITY; + + // search for a deviation that is worse than the current mode + + for (byte j = 0; j < estimates[i].length; j++) { + if (estimates[i][j] <= min && j != originalMode && estimates[i][j] > max) { + max = estimates[i][j]; + mode = j; + } + } + + if (mode != -1) { + long newIdx = Entry.toIndex(modes, depth, i, mode); + + // recompute the deviation from the maximum + // there might be a way to store and update this, without recomputing + double dev = 0; + for (int j = 0; j < result.length; j++) { + // Use either the replaced mode or the original mode + byte legMode = j == i ? mode : modes[j]; + + if (legMode == -1) + continue; + + dev += this.max[j] - estimates[j][legMode]; + } + + Entry e = new Entry(newIdx, dev); + + if (!seen.contains(e.index)) { + heap.enqueue(e); + seen.add(e.index); + } + } + } + + search.convert(modes, result); + return best - entry.deviation; + } + + @Override + public boolean hasNext() { + return !heap.isEmpty(); + } + + static final class Entry implements Comparable { + private final long index; + private final double deviation; + + Entry(byte[] modes, int depth, double deviation) { + this.index = toIndex(modes, depth); + this.deviation = deviation; + } + + Entry(long index, double deviation) { + this.index = index; + this.deviation = deviation; + } + + static long toIndex(byte[] modes, int depth) { + long result = modes[0] + 1; + long base = depth; + for (int i = 1; i < modes.length; i++) { + result += (modes[i] + 1) * base; + base *= depth; + } + + return result; + } + + /** + * Convert the mode array to an index, where one mode at index {@code idx} is replaced. + */ + static long toIndex(byte[] modes, int depth, int idx, byte mode) { + long result = (idx == 0 ? mode : modes[0]) + 1; + long base = depth; + for (int i = 1; i < modes.length; i++) { + result += ((idx == i ? mode : modes[i]) + 1) * base; + base *= depth; + } + + return result; + } + + @Override + public int compareTo(Entry o) { + return Double.compare(deviation, o.deviation); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Entry entry = (Entry) o; + return Double.compare(entry.deviation, deviation) == 0 && index == entry.index; + } + + @Override + public int hashCode() { + int result = Objects.hash(deviation); + result = 31 * result + Long.hashCode(index); + return result; + } + + @Override + public String toString() { + return "idx: " + index + " = " + deviation; + } + + public long getIndex() { + return index; + } + + void toArray(byte[] array, long base, int depth) { + long idx = index; + for (int i = array.length - 1; i > 0; i--) { + + long v = idx / base; + array[i] = (byte) (v - 1); + + idx -= v * base; + base /= depth; + } + + array[0] = (byte) (idx - 1); + } + } +} diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/SingleTripChoicesGenerator.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/SingleTripChoicesGenerator.java index 5ba80a11740..7f5afad42f4 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/SingleTripChoicesGenerator.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/SingleTripChoicesGenerator.java @@ -61,6 +61,10 @@ public List generate(PlanModel planModel, @Nullable Set c ModeEstimate est = opt.get(); + // Not actual used modes are not generated here + if (est.getNoRealUsage()[idx]) + continue; + String[] modes = planModel.getCurrentModes(); modes[idx] = est.getMode(); diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/TopKChoicesGenerator.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/TopKChoicesGenerator.java index 27ed135b3b2..e3791702020 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/TopKChoicesGenerator.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/search/TopKChoicesGenerator.java @@ -22,11 +22,6 @@ @SuppressWarnings("unchecked") public class TopKChoicesGenerator extends AbstractCandidateGenerator { - /** - * Maximum number of iterations. Memory usage will increase the more iterations are done. - */ - private static final int MAX_ITER = 10_000_000; - private static final Logger log = LogManager.getLogger(TopKChoicesGenerator.class); @@ -116,7 +111,8 @@ private List generateCandidate(EstimatorContext context, PlanMode continue m; } - search.addEstimates(mode.getMode(), mode.getEstimates(), mask); + // Only add estimates for desired modes and those that have actual usage + search.addEstimates(mode.getMode(), mode.getEstimates(), mask, mode.getNoRealUsage()); } if (search.isEmpty()) @@ -167,14 +163,14 @@ private List generateCandidate(EstimatorContext context, PlanMode int k = 0; int n = 0; - DoubleIterator it = search.iter(result); + ModeIterator it = search.iter(result); double best = Double.NEGATIVE_INFINITY; outer: while (it.hasNext() && k < topK) { double estimate = preDeterminedEstimate + it.nextDouble(); - if (n++ > MAX_ITER) { + if (n++ > it.maxIters()) { log.warn("Maximum number of iterations reached for person {}", context.person.getId()); break; } diff --git a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/ModeChoiceWeightSchedulerTest.java b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/ModeChoiceWeightSchedulerTest.java index 4d9910b96bf..103ce6175a3 100644 --- a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/ModeChoiceWeightSchedulerTest.java +++ b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/ModeChoiceWeightSchedulerTest.java @@ -20,7 +20,7 @@ void linear() { imc.setInvBeta(1); imc.setAnneal(InformedModeChoiceConfigGroup.Schedule.linear); - ModeChoiceWeightScheduler scheduler = injector.getInstance(ModeChoiceWeightScheduler.class); + ModeChoiceWeightScheduler scheduler = new ModeChoiceWeightScheduler(controler.getConfig()); MatsimServices services = injector.getInstance(MatsimServices.class); scheduler.notifyStartup(new StartupEvent(services)); @@ -46,7 +46,7 @@ void quadratic() { imc.setInvBeta(1); imc.setAnneal(InformedModeChoiceConfigGroup.Schedule.quadratic); - ModeChoiceWeightScheduler scheduler = injector.getInstance(ModeChoiceWeightScheduler.class); + ModeChoiceWeightScheduler scheduler = new ModeChoiceWeightScheduler(controler.getConfig()); MatsimServices services = injector.getInstance(MatsimServices.class); scheduler.notifyStartup(new StartupEvent(services)); diff --git a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/MultinomialLogitSelectorTest.java b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/MultinomialLogitSelectorTest.java index 851404656f9..144ff9fb899 100644 --- a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/MultinomialLogitSelectorTest.java +++ b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/MultinomialLogitSelectorTest.java @@ -5,200 +5,113 @@ import org.junit.jupiter.api.Test; import org.matsim.modechoice.PlanCandidate; +import java.util.ArrayList; import java.util.List; import java.util.Random; import static org.assertj.core.api.Assertions.assertThat; -public class MultinomialLogitSelectorTest { - - private MultinomialLogitSelector selector; +class MultinomialLogitSelectorTest { private static final int N = 500_000; + private final Offset offset = Offset.offset(0.01); + private MultinomialLogitSelector selector; @BeforeEach public void setUp() throws Exception { selector = new MultinomialLogitSelector(1, new Random(0)); } - @Test - void selection() { - - List candidates = List.of( - new PlanCandidate(new String[]{"car"}, -1), - new PlanCandidate(new String[]{"pt"}, -1.5), - new PlanCandidate(new String[]{"walk"}, -2) - ); - - - double[] sample = selector.sample(1000, candidates); - - assertThat(sample) - .containsExactly(0.51, 0.306, 0.184); - - candidates = List.of( - new PlanCandidate(new String[]{"car"}, -50), - new PlanCandidate(new String[]{"pt"}, -60), - new PlanCandidate(new String[]{"walk"}, -70) - ); - - sample = selector.sample(1000, candidates); - - assertThat(sample) - .containsExactly(0.491, 0.323, 0.186); + private double[] sample(double... utils) { + List candidates = new ArrayList<>(); + for (int i = 0; i < utils.length; i++) { + candidates.add(new PlanCandidate(new String[]{"" + i}, utils[i])); + } - candidates = List.of( - new PlanCandidate(new String[]{"car"}, 7), - new PlanCandidate(new String[]{"pt"}, 7), - new PlanCandidate(new String[]{"walk"}, 5) - ); - - sample = selector.sample(N, candidates); - - assertThat(sample) - .containsExactly(0.42231, 0.42174, 0.15595); - + return selector.sample(N, candidates); } @Test - void invariance() { - - List candidates = List.of( - new PlanCandidate(new String[]{"car"}, -1), - new PlanCandidate(new String[]{"pt"}, -2), - new PlanCandidate(new String[]{"walk"}, -3) - ); + void probabilities() { + // checked with https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.softmax.html - double[] sample1 = selector.sample(N, candidates); + assertThat(sample(-1, -2, -3)) + .containsExactly(new double[]{0.66524096, 0.24472847, 0.09003057}, offset); - candidates = List.of( - new PlanCandidate(new String[]{"car"}, -10), - new PlanCandidate(new String[]{"pt"}, -20), - new PlanCandidate(new String[]{"walk"}, -30) - ); + assertThat(sample(-3, -1, -2)) + .containsExactly(new double[]{0.09003057, 0.66524096, 0.24472847}, offset); - double[] sample2 = selector.sample(N, candidates); + assertThat(sample(-2, -4, -6)) + .containsExactly(new double[]{0.86681333, 0.11731043, 0.01587624}, offset); - candidates = List.of( - new PlanCandidate(new String[]{"car"}, 10), - new PlanCandidate(new String[]{"pt"}, 0), - new PlanCandidate(new String[]{"walk"}, -10) - ); + assertThat(sample(-0.5, -1, -1.5)) + .containsExactly(new double[]{0.50648039, 0.30719589, 0.18632372}, offset); - double[] sample3 = selector.sample(N, candidates); + selector = new MultinomialLogitSelector(2, new Random(0)); - assertThat(sample1) - .containsExactly(0.50625, 0.306986, 0.186764); - assertThat(sample2) - .containsExactly(0.506194, 0.307304, 0.186502); - assertThat(sample3) - .containsExactly(0.506376, 0.30703, 0.186594); + assertThat(sample(-1, -2, -3)) + .containsExactly(new double[]{0.50648039, 0.30719589, 0.18632372}, offset); - } - - @Test - void best() { - - selector = new MultinomialLogitSelector(0, new Random(0)); + selector = new MultinomialLogitSelector(0.5, new Random(0)); - List candidates = List.of( - new PlanCandidate(new String[]{"car"}, 1.04), - new PlanCandidate(new String[]{"pt"}, 1.06), - new PlanCandidate(new String[]{"walk"}, 1.05) - ); + assertThat(sample(-1, -2, -3)) + .containsExactly(new double[]{0.86681333, 0.11731043, 0.01587624}, offset); - double[] sample = selector.sample(N, candidates); + selector = new MultinomialLogitSelector(10, new Random(0)); - assertThat(sample).containsExactly(0, 1, 0); + assertThat(sample(-1, -2, -3)) + .containsExactly(new double[]{0.3671654 , 0.33222499, 0.30060961}, offset); } @Test - void sameScore() { - - selector = new MultinomialLogitSelector(0.01, new Random(0)); + void best() { - List candidates = List.of( - new PlanCandidate(new String[]{"car"}, 1.), - new PlanCandidate(new String[]{"walk"}, 1.) - ); + selector = new MultinomialLogitSelector(0, new Random(0)); - double[] sample = selector.sample(N, candidates); + assertThat(sample(1.04, 1.06, 1.05)) + .containsExactly(0, 1, 0); - assertThat(sample[0]).isCloseTo(0.5, Offset.offset(0.01)); - assertThat(sample[1]).isCloseTo(0.5, Offset.offset(0.01)); } @Test - void single() { - - - selector = new MultinomialLogitSelector(1, new Random(0)); - - List candidates = List.of( - new PlanCandidate(new String[]{"car"}, 1.) - ); - - double[] sample = selector.sample(N, candidates); + void random() { - assertThat(sample[0]).isEqualTo(1); + selector = new MultinomialLogitSelector(Double.POSITIVE_INFINITY, new Random(0)); + assertThat(sample(100, 3, 1)) + .containsExactly(new double[]{0.333, 0.333, 0.333}, offset); } @Test - void precision() { - - selector = new MultinomialLogitSelector(0.01, new Random(0)); - - List candidates = List.of( - new PlanCandidate(new String[]{"car1"}, 2), - new PlanCandidate(new String[]{"car2"}, 10000), - new PlanCandidate(new String[]{"car3"}, 1) - ); + void invariance() { - double[] sample = selector.sample(N, candidates); + // Selector is invariant to shifting, but not to scale of utilities + assertThat(sample(-1, -2, -3)) + .containsExactly(new double[]{0.664678, 0.244972, 0.09035}, offset); - assertThat(sample[1]).isEqualTo(1); + assertThat(sample(1, 0, -1)) + .containsExactly(new double[]{0.665126, 0.244834, 0.09004}, offset); } @Test - void inf() { - - selector = new MultinomialLogitSelector(10000, new Random(0)); + void smallScale() { - List candidates = List.of( - new PlanCandidate(new String[]{"car1"}, 2), - new PlanCandidate(new String[]{"car2"}, 10000), - new PlanCandidate(new String[]{"car3"}, 1) - ); + selector = new MultinomialLogitSelector(1 / 10000d, new Random(0)); - double[] sample = selector.sample(N, candidates); - - assertThat(sample[0]).isEqualTo(0.333, Offset.offset(0.001)); - assertThat(sample[1]).isEqualTo(0.333, Offset.offset(0.001)); - assertThat(sample[2]).isEqualTo(0.333, Offset.offset(0.001)); - - } - - @Test - void random() { - - selector = new MultinomialLogitSelector(Double.POSITIVE_INFINITY, new Random(0)); + assertThat(sample(-1e-4, -1e-4, -2e-4)) + .containsExactly(new double[]{0.4223188, 0.4223188, 0.1553624}, offset); - List candidates = List.of( - new PlanCandidate(new String[]{"car1"}, 100), - new PlanCandidate(new String[]{"car2"}, 3), - new PlanCandidate(new String[]{"car3"}, 1) - ); + assertThat(sample(-1, -1, -2)) + .containsExactly(new double[]{1/2d, 1/2d, 0}, offset); - double[] sample = selector.sample(N, candidates); + selector = new MultinomialLogitSelector(2e-8, new Random(0)); - assertThat(sample[0]).isEqualTo(0.333, Offset.offset(0.001)); - assertThat(sample[1]).isEqualTo(0.333, Offset.offset(0.001)); - assertThat(sample[2]).isEqualTo(0.333, Offset.offset(0.001)); + assertThat(sample(-1, -1.0001, -2)) + .containsExactly(new double[]{1, 0, 0}, offset); } } diff --git a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/NormalizedMultinomialLogitSelectorTest.java b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/NormalizedMultinomialLogitSelectorTest.java new file mode 100644 index 00000000000..c0beaba8703 --- /dev/null +++ b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/NormalizedMultinomialLogitSelectorTest.java @@ -0,0 +1,204 @@ +package org.matsim.modechoice.replanning; + +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.matsim.modechoice.PlanCandidate; + +import java.util.List; +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NormalizedMultinomialLogitSelectorTest { + + private NormalizedMultinomialLogitSelector selector; + + private static final int N = 500_000; + + @BeforeEach + public void setUp() throws Exception { + selector = new NormalizedMultinomialLogitSelector(1, new Random(0)); + } + + @Test + void selection() { + + List candidates = List.of( + new PlanCandidate(new String[]{"car"}, -1), + new PlanCandidate(new String[]{"pt"}, -1.5), + new PlanCandidate(new String[]{"walk"}, -2) + ); + + + double[] sample = selector.sample(1000, candidates); + + assertThat(sample) + .containsExactly(0.51, 0.306, 0.184); + + candidates = List.of( + new PlanCandidate(new String[]{"car"}, -50), + new PlanCandidate(new String[]{"pt"}, -60), + new PlanCandidate(new String[]{"walk"}, -70) + ); + + sample = selector.sample(1000, candidates); + + assertThat(sample) + .containsExactly(0.491, 0.323, 0.186); + + + candidates = List.of( + new PlanCandidate(new String[]{"car"}, 7), + new PlanCandidate(new String[]{"pt"}, 7), + new PlanCandidate(new String[]{"walk"}, 5) + ); + + sample = selector.sample(N, candidates); + + assertThat(sample) + .containsExactly(0.42231, 0.42174, 0.15595); + + } + + @Test + void invariance() { + + List candidates = List.of( + new PlanCandidate(new String[]{"car"}, -1), + new PlanCandidate(new String[]{"pt"}, -2), + new PlanCandidate(new String[]{"walk"}, -3) + ); + + + double[] sample1 = selector.sample(N, candidates); + + candidates = List.of( + new PlanCandidate(new String[]{"car"}, -10), + new PlanCandidate(new String[]{"pt"}, -20), + new PlanCandidate(new String[]{"walk"}, -30) + ); + + double[] sample2 = selector.sample(N, candidates); + + candidates = List.of( + new PlanCandidate(new String[]{"car"}, 10), + new PlanCandidate(new String[]{"pt"}, 0), + new PlanCandidate(new String[]{"walk"}, -10) + ); + + double[] sample3 = selector.sample(N, candidates); + + assertThat(sample1) + .containsExactly(0.50625, 0.306986, 0.186764); + assertThat(sample2) + .containsExactly(0.506194, 0.307304, 0.186502); + assertThat(sample3) + .containsExactly(0.506376, 0.30703, 0.186594); + + } + + @Test + void best() { + + selector = new NormalizedMultinomialLogitSelector(0, new Random(0)); + + List candidates = List.of( + new PlanCandidate(new String[]{"car"}, 1.04), + new PlanCandidate(new String[]{"pt"}, 1.06), + new PlanCandidate(new String[]{"walk"}, 1.05) + ); + + double[] sample = selector.sample(N, candidates); + + assertThat(sample).containsExactly(0, 1, 0); + + } + + @Test + void sameScore() { + + selector = new NormalizedMultinomialLogitSelector(0.01, new Random(0)); + + List candidates = List.of( + new PlanCandidate(new String[]{"car"}, 1.), + new PlanCandidate(new String[]{"walk"}, 1.) + ); + + double[] sample = selector.sample(N, candidates); + + assertThat(sample[0]).isCloseTo(0.5, Offset.offset(0.01)); + assertThat(sample[1]).isCloseTo(0.5, Offset.offset(0.01)); + } + + @Test + void single() { + + + selector = new NormalizedMultinomialLogitSelector(1, new Random(0)); + + List candidates = List.of( + new PlanCandidate(new String[]{"car"}, 1.) + ); + + double[] sample = selector.sample(N, candidates); + + assertThat(sample[0]).isEqualTo(1); + + } + + @Test + void precision() { + + selector = new NormalizedMultinomialLogitSelector(0.01, new Random(0)); + + List candidates = List.of( + new PlanCandidate(new String[]{"car1"}, 2), + new PlanCandidate(new String[]{"car2"}, 10000), + new PlanCandidate(new String[]{"car3"}, 1) + ); + + double[] sample = selector.sample(N, candidates); + + assertThat(sample[1]).isEqualTo(1); + + } + + @Test + void inf() { + + selector = new NormalizedMultinomialLogitSelector(10000, new Random(0)); + + List candidates = List.of( + new PlanCandidate(new String[]{"car1"}, 2), + new PlanCandidate(new String[]{"car2"}, 10000), + new PlanCandidate(new String[]{"car3"}, 1) + ); + + double[] sample = selector.sample(N, candidates); + + assertThat(sample[0]).isEqualTo(0.333, Offset.offset(0.001)); + assertThat(sample[1]).isEqualTo(0.333, Offset.offset(0.001)); + assertThat(sample[2]).isEqualTo(0.333, Offset.offset(0.001)); + + } + + @Test + void random() { + + selector = new NormalizedMultinomialLogitSelector(Double.POSITIVE_INFINITY, new Random(0)); + + List candidates = List.of( + new PlanCandidate(new String[]{"car1"}, 100), + new PlanCandidate(new String[]{"car2"}, 3), + new PlanCandidate(new String[]{"car3"}, 1) + ); + + double[] sample = selector.sample(N, candidates); + + assertThat(sample[0]).isEqualTo(0.333, Offset.offset(0.001)); + assertThat(sample[1]).isEqualTo(0.333, Offset.offset(0.001)); + assertThat(sample[2]).isEqualTo(0.333, Offset.offset(0.001)); + + } +} diff --git a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/SelectSubtourModeStrategyTest.java b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/SelectSubtourModeStrategyTest.java index 7536fe5f8de..5e18df71dd4 100644 --- a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/SelectSubtourModeStrategyTest.java +++ b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/SelectSubtourModeStrategyTest.java @@ -30,7 +30,6 @@ void person() { PrepareForMobsim prepare = injector.getInstance(PrepareForMobsim.class); prepare.run(); - PlanStrategy strategy = injector.getInstance(Key.get(PlanStrategy.class, Names.named(InformedModeChoiceModule.SELECT_SUBTOUR_MODE_STRATEGY))); Person person = controler.getScenario().getPopulation().getPersons().get(TestScenario.Agents.get(5)); @@ -70,7 +69,6 @@ void constraint() { @Test void allowedModes() { - TopKChoicesGenerator generator = injector.getInstance(TopKChoicesGenerator.class); // This agent is not allowed to use car @@ -101,4 +99,4 @@ private void run(PlanStrategy strategy, Person person) { strategy.finish(); } -} \ No newline at end of file +} diff --git a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/SelectVsRandomSubtourTest.java b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/SelectVsRandomSubtourTest.java new file mode 100644 index 00000000000..249dc433365 --- /dev/null +++ b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/SelectVsRandomSubtourTest.java @@ -0,0 +1,107 @@ +package org.matsim.modechoice.replanning; + +import com.google.inject.Key; +import com.google.inject.name.Names; +import it.unimi.dsi.fastutil.objects.Object2DoubleAVLTreeMap; +import it.unimi.dsi.fastutil.objects.Object2DoubleSortedMap; +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.Test; +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.core.config.Config; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.controler.PrepareForMobsim; +import org.matsim.core.replanning.PlanStrategy; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.modechoice.InformedModeChoiceConfigGroup; +import org.matsim.modechoice.InformedModeChoiceModule; +import org.matsim.modechoice.ScenarioTest; +import org.matsim.modechoice.TestScenario; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Checks if {@link RandomSubtourModeStrategy} and {@link SelectSubtourModeStrategy} are equivalent under specific configuration. + */ +public class SelectVsRandomSubtourTest extends ScenarioTest { + + @Override + protected String[] getArgs() { + return new String[]{"--mc"}; + } + + + @Override + protected void prepareConfig(Config config) { + InformedModeChoiceConfigGroup imc = ConfigUtils.addOrGetModule(config, InformedModeChoiceConfigGroup.class); + imc.setTopK(0); + imc.setInvBeta(Double.POSITIVE_INFINITY); + } + + + @Test + void replanning() { + + PrepareForMobsim prepare = injector.getInstance(PrepareForMobsim.class); + prepare.run(); + + PlanStrategy select = injector.getInstance(Key.get(PlanStrategy.class, Names.named(InformedModeChoiceModule.SELECT_SUBTOUR_MODE_STRATEGY))); + PlanStrategy random = injector.getInstance(Key.get(PlanStrategy.class, Names.named(InformedModeChoiceModule.RANDOM_SUBTOUR_MODE_STRATEGY))); + + List plans = TestScenario.Agents.stream() + .map(agent -> controler.getScenario().getPopulation().getPersons().get(agent)) + .map(Person::getSelectedPlan) + .toList(); + + double[] randomModes = sampleModes(plans, random); + double[] selectModes = sampleModes(plans, select); + + assertThat(selectModes) + .containsExactly(randomModes, Offset.offset(1200d)); + + } + + /** + * Run the strategy and count obtained modes. + */ + private double[] sampleModes(List plans, PlanStrategy strategy) { + + Object2DoubleSortedMap modes = new Object2DoubleAVLTreeMap<>(); + + for (Plan plan : plans) { + + Person person = plan.getPerson(); + person.getPlans().clear(); + + person.addPlan(plan); + + } + + for (int i = 0; i < 500; i++) { + int finalI = i; + strategy.init(() -> finalI); + + for (Plan plan : plans) + strategy.run(plan.getPerson()); + + strategy.finish(); + + for (Plan plan : plans) { + + Plan selected = plan.getPerson().getSelectedPlan(); + plan.getPerson().getPlans().removeIf(p -> selected != p); + for (Leg leg : TripStructureUtils.getLegs(selected)) { + modes.mergeDouble(leg.getMode(), 1, Double::sum); + } + + } + } + + System.out.println("Modes: " + modes.keySet()); + + return modes.values().toDoubleArray(); + } +} diff --git a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/search/ModeChoiceSearchTest.java b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/search/ModeChoiceSearchTest.java index 6c1ab6e7a7e..c8c8af0f00c 100644 --- a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/search/ModeChoiceSearchTest.java +++ b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/search/ModeChoiceSearchTest.java @@ -3,6 +3,8 @@ import it.unimi.dsi.fastutil.doubles.DoubleIterator; import org.junit.jupiter.api.Test; +import java.util.Arrays; + import static org.assertj.core.api.Assertions.assertThat; public class ModeChoiceSearchTest { @@ -97,4 +99,163 @@ void nullValues() { } -} \ No newline at end of file + @Test + void huge() { + + ModeChoiceSearch search = new ModeChoiceSearch(25, 5); + + for (int i = 0; i < 5; i++) { + double[] values = new double[25]; + + Arrays.fill(values, i); + + search.addEstimates(String.valueOf(i), values); + } + + String[] result = new String[25]; + + ModeIterator it = search.iter(result); + + double v = it.nextDouble(); + + assertThat(v) + .isEqualTo(100); + + assertThat(result) + .containsOnly("4"); + + // All combinations with 99 util + for (int i = 0; i < 25; i++) { + assertThat(it.nextDouble()).isEqualTo(99); + } + + assertThat(it.nextDouble()).isEqualTo(98); + + } + + @Test + void longEntry() { + + int depth = 6; + long base = (long) Math.pow(depth, 9); + + long b = 1; + for (int i = 0; i < 9; i++) { + b *= depth; + } + + assertThat(b) + .isEqualTo(base); + + byte[] modes = new byte[10]; + Arrays.fill(modes, (byte) -1); + + assertThat(ModeLongIterator.Entry.toIndex(modes, depth)) + .isEqualTo(0); + + assertThat(ModeLongIterator.Entry.toIndex(modes, depth, 0, (byte) 0)) + .isEqualTo(1); + + modes[0] = 0; + + assertThat(ModeLongIterator.Entry.toIndex(modes, depth)) + .isEqualTo(1); + + assertThat(ModeLongIterator.Entry.toIndex(modes, depth, 1, (byte) 0)) + .isEqualTo(depth + 1); + + modes[1] = 1; + modes[2] = 2; + modes[3] = 3; + modes[4] = 4; + + ModeLongIterator.Entry e = new ModeLongIterator.Entry(modes, depth, 0); + + assertThat(e.getIndex()) + .isEqualTo(7465); + + byte[] result = new byte[10]; + Arrays.fill(result, (byte) -1); + + e.toArray(result, base, depth); + + assertThat(result) + .isEqualTo(modes); + + Arrays.fill(modes, (byte) 4); + + e = new ModeLongIterator.Entry(modes, depth, 0); + + e.toArray(result, base, depth); + + assertThat(result) + .isEqualTo(modes); + + assertThat(e.getIndex()) + .isEqualTo(base * depth - 1); + + } + + @Test + void intEntry() { + + int depth = 6; + int base = (int) Math.pow(depth, 5); + + int b = 1; + for (int i = 0; i < 5; i++) { + b *= depth; + } + + assertThat(b) + .isEqualTo(base); + + byte[] modes = new byte[6]; + Arrays.fill(modes, (byte) -1); + + assertThat(ModeIntIterator.Entry.toIndex(modes, depth)) + .isEqualTo(0); + + modes[0] = 0; + + assertThat(ModeIntIterator.Entry.toIndex(modes, depth)) + .isEqualTo(1); + + assertThat(ModeLongIterator.Entry.toIndex(modes, depth, 0, (byte) 0)) + .isEqualTo(1); + + assertThat(ModeLongIterator.Entry.toIndex(modes, depth, 1, (byte) 0)) + .isEqualTo(depth + 1); + + modes[1] = 1; + modes[2] = 2; + modes[3] = 3; + modes[4] = 4; + + ModeIntIterator.Entry e = new ModeIntIterator.Entry(modes, depth, 0); + + assertThat(e.getIndex()) + .isEqualTo(7465); + + byte[] result = new byte[6]; + Arrays.fill(result, (byte) -1); + + e.toArray(result, base, depth); + + assertThat(result) + .isEqualTo(modes); + + Arrays.fill(modes, (byte) 4); + + e = new ModeIntIterator.Entry(modes, depth, 0); + + e.toArray(result, base, depth); + + assertThat(result) + .isEqualTo(modes); + + assertThat(e.getIndex()) + .isEqualTo(base * depth - 1); + + } +} diff --git a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/search/TopKMinMaxTest.java b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/search/TopKMinMaxTest.java index 5490b076de8..4ee5e1b2562 100644 --- a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/search/TopKMinMaxTest.java +++ b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/search/TopKMinMaxTest.java @@ -19,6 +19,7 @@ import org.matsim.core.config.groups.PlansConfigGroup; import org.matsim.core.controler.ControlerListenerManager; import org.matsim.core.population.PopulationUtils; +import org.matsim.core.router.DefaultAnalysisMainModeIdentifier; import org.matsim.core.router.TripRouter; import org.matsim.core.scoring.functions.ScoringParameters; import org.matsim.core.scoring.functions.ScoringParametersForPerson; @@ -207,7 +208,8 @@ else if (invocationOnMock.getArgument(0).equals(TransportMode.walk)) { bind(EstimateRouter.class).toInstance(new EstimateRouter(router, FacilitiesUtils.createActivityFacilities(), - TimeInterpretation.create(PlansConfigGroup.ActivityDurationInterpretation.minOfDurationAndEndTime, PlansConfigGroup.TripDurationHandling.shiftActivityEndTimes))); + new DefaultAnalysisMainModeIdentifier() + )); MapBinder optionBinder = MapBinder.newMapBinder(binder(), new TypeLiteral<>() {}, new TypeLiteral<>(){}); optionBinder.addBinding(TransportMode.car).toInstance(new ModeOptions.AlwaysAvailable()); diff --git a/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/PtTripFareEstimatorTest.java b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/PtTripFareEstimatorTest.java index 791931b89a2..b74f341e4de 100644 --- a/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/PtTripFareEstimatorTest.java +++ b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/PtTripFareEstimatorTest.java @@ -103,7 +103,7 @@ private List estimateAgent(Id personId) { List trip = model.getLegs(TransportMode.pt, i); - if (trip == null) { + if (trip == null || !model.hasModeForTrip(TransportMode.pt, i)) { continue; } diff --git a/matsim/src/main/java/org/matsim/core/replanning/GenericStrategyManagerImpl.java b/matsim/src/main/java/org/matsim/core/replanning/GenericStrategyManagerImpl.java index ba4cc972533..2befea6c2c5 100644 --- a/matsim/src/main/java/org/matsim/core/replanning/GenericStrategyManagerImpl.java +++ b/matsim/src/main/java/org/matsim/core/replanning/GenericStrategyManagerImpl.java @@ -51,7 +51,7 @@ */ public class GenericStrategyManagerImpl> implements GenericStrategyManager{ // the "I extends ... <, I>" is correct, although it feels odd. kai, nov'15 - + private static final Logger log = LogManager.getLogger( GenericStrategyManagerImpl.class ); @@ -90,7 +90,7 @@ static class StrategyWeights implements StrategyChooser. // private String subpopulationAttributeName = null; - + public GenericStrategyManagerImpl() { this(new WeightedStrategyChooser<>()); } @@ -210,6 +210,8 @@ final void run( final Iterable> persons, final ReplanningContext replanningContext ) { + this.strategyChooser.beforeReplanning(replanningContext); + // initialize all strategies for (GenericPlanStrategy strategy : distinctStrategies()) { strategy.init(replanningContext); @@ -234,7 +236,7 @@ final void run( if (strategy==null) { throw new RuntimeException("No strategy found! Have you defined at least one replanning strategy per subpopulation? Current subpopulation = " + subpopName); } - + // ... and run the strategy: strategy.run(person); } diff --git a/matsim/src/main/java/org/matsim/core/replanning/choosers/BalancedInnovationStrategyChooser.java b/matsim/src/main/java/org/matsim/core/replanning/choosers/BalancedInnovationStrategyChooser.java new file mode 100644 index 00000000000..4e4bbe296f0 --- /dev/null +++ b/matsim/src/main/java/org/matsim/core/replanning/choosers/BalancedInnovationStrategyChooser.java @@ -0,0 +1,207 @@ +package org.matsim.core.replanning.choosers; + +import com.google.inject.Binder; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.TypeLiteral; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import org.matsim.api.core.v01.population.*; +import org.matsim.core.controler.AbstractModule; +import org.matsim.core.controler.Controller; +import org.matsim.core.gbl.MatsimRandom; +import org.matsim.core.population.PopulationUtils; +import org.matsim.core.replanning.GenericPlanStrategy; +import org.matsim.core.replanning.ReplanningContext; +import org.matsim.core.replanning.ReplanningUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * Strategy chooser which ensures that all agents perform innovative strategies in a balanced manner. + * That is at any time all agents have either performed n, n+1 or n+2 times an innovative strategy. This difference is never larger than 2. + * The class also tries to balance the number of innovations in a single iteration. + */ +public class BalancedInnovationStrategyChooser> implements StrategyChooser { + + /** + * The total number of agents per subpopulation. + */ + private final Object2IntMap total = new Object2IntOpenHashMap<>(); + + /** + * Number of agents already processed this iteration. + */ + private final Object2IntMap seen = new Object2IntOpenHashMap<>(); + + /** + * Number of agents already decided to innovate this iteration. + */ + private final Object2IntMap innovated = new Object2IntOpenHashMap<>(); + + /** + * Agents that are forced to innovate (per subpopulation). + */ + private final Object2IntMap carryOver = new Object2IntOpenHashMap<>(); + + /** + * The set of indices of agents that have performed an innovative strategy. + */ + private final Map oneAhead = new HashMap<>(); + + /** + * The set of indices of agents that have performed an innovative strategy two times. + */ + private final Map twoAhead = new HashMap<>(); + + @Inject + public BalancedInnovationStrategyChooser(Population population) { + + for (Person person : population.getPersons().values()) { + String key = PopulationUtils.getSubpopulation(person); + total.mergeInt(key != null ? key : "__none__", 1, Integer::sum); + } + + for (String s : total.keySet()) { + oneAhead.put(s, new IntOpenHashSet()); + twoAhead.put(s, new IntOpenHashSet()); + carryOver.put(s, 0); + } + } + + /** + * Convenience method to bind this strategy chooser to a Guice binder. + * + * @param binder Guice binder + */ + public static void bind(Binder binder) { + binder.bind(new TypeLiteral>() { + }).to(new TypeLiteral>() { + }).in(Singleton.class); + } + + + /** + * Install this strategy chooser in the given controller. + */ + public static void install(Controller controller) { + controller.addOverridingModule(new AbstractModule() { + @Override + public void install() { + BalancedInnovationStrategyChooser.bind(binder()); + } + }); + } + + @Override + public void beforeReplanning(ReplanningContext replanningContext) { + innovated.clear(); + seen.clear(); + } + + @Override + public GenericPlanStrategy chooseStrategy(HasPlansAndId person, String subpopulation, ReplanningContext replanningContext, StrategyChooser.Weights weights) { + + // Two separate arrays for innovation and selection strategies weights + double[] wInno = new double[weights.size()]; + double totalInno = 0; + + double[] wSel = new double[weights.size()]; + double totalSel = 0; + + for (int i = 0; i < weights.size(); i++) { + if (ReplanningUtils.isOnlySelector(weights.getStrategy(i))) { + wSel[i] = weights.getWeight(i); + totalSel += wSel[i]; + } else { + wInno[i] = weights.getWeight(i); + totalInno += wInno[i]; + } + } + + double rnd = MatsimRandom.getRandom().nextDouble() * (totalInno + totalSel); + + // Subpopulation can not be null + if (subpopulation == null) { + subpopulation = "__none__"; + } + + IntSet oneAhead = this.oneAhead.get(subpopulation); + IntSet twoAhead = this.twoAhead.get(subpopulation); + + int id = person.getId().index(); + + // Expected number of innovations (this iteration) + double expected = (seen.getInt(subpopulation) * totalInno) / (totalInno + totalSel); + double diff = expected - innovated.getInt(subpopulation); + + seen.mergeInt(subpopulation, 1, Integer::sum); + + if (rnd < totalInno) { + // Agent would innovate + + // Agent has already innovated, force selection and increase carry over + if (oneAhead.contains(id)) { + carryOver.mergeInt(subpopulation, 1, Integer::sum); + return chooseStrategy(totalSel, wSel, weights); + } + + oneAhead.add(id); + innovated.mergeInt(subpopulation, 1, Integer::sum); + advanceStep(subpopulation); + + return chooseStrategy(totalInno, wInno, weights); + } else { + // Agent would select + + // Force to innovate if there is carry over + if (carryOver.getInt(subpopulation) > 0 && !oneAhead.contains(id)) { + carryOver.mergeInt(subpopulation, -1, Integer::sum); + + oneAhead.add(id); + innovated.mergeInt(subpopulation, 1, Integer::sum); + advanceStep(subpopulation); + return chooseStrategy(totalInno, wInno, weights); + } + + // If the difference becomes too large, agents are allowed to innovate again + // some carry overs are always reserved for agent that need to innovate one step + if (carryOver.getInt(subpopulation) > 20 && diff > 50 && !twoAhead.contains(id)) { + carryOver.mergeInt(subpopulation, -1, Integer::sum); + + twoAhead.add(id); + innovated.mergeInt(subpopulation, 1, Integer::sum); + return chooseStrategy(totalInno, wInno, weights); + } + + return chooseStrategy(totalSel, wSel, weights); + } + } + + private void advanceStep(String subpopulation) { + IntSet oneAhead = this.oneAhead.get(subpopulation); + + if (oneAhead.size() == total.getInt(subpopulation)) { + IntSet twoAhead = this.twoAhead.get(subpopulation); + + oneAhead.clear(); + oneAhead.addAll(twoAhead); + twoAhead.clear(); + } + } + + private GenericPlanStrategy chooseStrategy(double total, double[] w, StrategyChooser.Weights weights) { + double rnd = MatsimRandom.getRandom().nextDouble() * total; + double sum = 0.0; + for (int i = 0, max = w.length; i < max; i++) { + sum += w[i]; + if (rnd <= sum) { + return weights.getStrategy(i); + } + } + return null; + } +} diff --git a/matsim/src/main/java/org/matsim/core/replanning/choosers/ForceInnovationStrategyChooser.java b/matsim/src/main/java/org/matsim/core/replanning/choosers/ForceInnovationStrategyChooser.java index 193e36eda43..dbd6b1407b8 100644 --- a/matsim/src/main/java/org/matsim/core/replanning/choosers/ForceInnovationStrategyChooser.java +++ b/matsim/src/main/java/org/matsim/core/replanning/choosers/ForceInnovationStrategyChooser.java @@ -9,6 +9,9 @@ /** * This chooser forces to select an innovative strategy every X iteration for every X person in the population. + * This chooser forces innovation regardless of the weights. + *

+ * For a more consistent innovation rate use {@link BalancedInnovationStrategyChooser}. */ public class ForceInnovationStrategyChooser> implements StrategyChooser { diff --git a/matsim/src/main/java/org/matsim/core/replanning/choosers/StrategyChooser.java b/matsim/src/main/java/org/matsim/core/replanning/choosers/StrategyChooser.java index 59a2f64d52a..198a05736a8 100644 --- a/matsim/src/main/java/org/matsim/core/replanning/choosers/StrategyChooser.java +++ b/matsim/src/main/java/org/matsim/core/replanning/choosers/StrategyChooser.java @@ -9,6 +9,10 @@ * Interface for choosing a strategy for each person, each iteration. */ public interface StrategyChooser> { + + default void beforeReplanning(ReplanningContext replanningContext) { + } + GenericPlanStrategy chooseStrategy(HasPlansAndId person, final String subpopulation, ReplanningContext replanningContext, Weights weights); diff --git a/matsim/src/main/java/org/matsim/core/router/TripStructureUtils.java b/matsim/src/main/java/org/matsim/core/router/TripStructureUtils.java index 69ac6c3d0c3..9c17a8b91c8 100644 --- a/matsim/src/main/java/org/matsim/core/router/TripStructureUtils.java +++ b/matsim/src/main/java/org/matsim/core/router/TripStructureUtils.java @@ -234,19 +234,19 @@ public static Collection getSubtours( final Plan plan, final Predicate< return getSubtours( plan.getPlanElements(), isStageActivity, 0); } - // for contrib socnetsim only - // I think now that we should actually keep this. kai, jan'20 - @Deprecated public static Collection getSubtours( - final List planElements, - final Predicate isStageActivity, double coordDistance) { - final List subtours = new ArrayList<>(); + final List planElements, + final Predicate isStageActivity, double coordDistance) { + return getSubtoursFromTrips(getTrips(planElements, isStageActivity), coordDistance); + } + public static Collection getSubtoursFromTrips(List trips, double coordDistance) { + + final List subtours = new ArrayList<>(); Object destinationId = null; // can be either id or coordinate final List originIds = new ArrayList<>(); - final List trips = getTrips( planElements, isStageActivity ); final List nonAllocatedTrips = new ArrayList<>( trips ); for (Trip trip : trips) { diff --git a/matsim/src/test/java/org/matsim/core/replanning/choosers/BalancedInnovationStrategyChooserTest.java b/matsim/src/test/java/org/matsim/core/replanning/choosers/BalancedInnovationStrategyChooserTest.java new file mode 100644 index 00000000000..6eefa355b4d --- /dev/null +++ b/matsim/src/test/java/org/matsim/core/replanning/choosers/BalancedInnovationStrategyChooserTest.java @@ -0,0 +1,175 @@ +package org.matsim.core.replanning.choosers; + +import it.unimi.dsi.fastutil.ints.Int2IntMap; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.matsim.api.core.v01.Id; +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.replanning.PlanStrategyModule; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.gbl.MatsimRandom; +import org.matsim.core.population.PopulationUtils; +import org.matsim.core.replanning.GenericPlanStrategy; +import org.matsim.core.replanning.PlanStrategy; +import org.matsim.core.replanning.PlanStrategyImpl; +import org.matsim.core.replanning.ReplanningContext; +import org.matsim.core.replanning.selectors.RandomPlanSelector; + +import java.util.IntSummaryStatistics; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("rawtypes") +class BalancedInnovationStrategyChooserTest { + + private Population population; + private BalancedInnovationStrategyChooser chooser; + private StrategyChooser.Weights weights; + private InnovationCounting count; + + @BeforeEach + void setUp() { + + population = PopulationUtils.createPopulation(ConfigUtils.createConfig()); + for (int i = 0; i < 10000; i++) { + Person person = population.getFactory().createPerson(Id.createPersonId(Integer.toString(i))); + person.addPlan(population.getFactory().createPlan()); + population.addPerson(person); + } + + chooser = new BalancedInnovationStrategyChooser(population); + count = new InnovationCounting(); + weights = new StrategyChooser.Weights<>() { + + private final PlanStrategy sel = new PlanStrategyImpl.Builder(new RandomPlanSelector<>()).build(); + private final PlanStrategy inno = new PlanStrategyImpl.Builder(new RandomPlanSelector<>()).addStrategyModule(count).build(); + + @Override + public int size() { + return 3; + } + + @Override + public double getWeight(int i) { + return i == 0 ? 0.7 : 0.15; + } + + @Override + public double getTotalWeights() { + return 3 * 0.15; + } + + @Override + public PlanStrategy getStrategy(int i) { + return switch (i) { + case 0 -> sel; + case 1, 2 -> inno; + default -> throw new IllegalStateException("Unexpected value: " + i); + }; + } + }; + } + + @Test + void innovationRates() { + + assertThat(count.getSum()).isEqualTo(0); + + runReplanning(); + assertThat(count.getSum()).isCloseTo(3000, Offset.offset(70)); + + runReplanning(); + assertThat(count.getSum()).isCloseTo(3000 * 2, Offset.offset(70)); + + runReplanning(); + assertThat(count.getSum()).isCloseTo(3000 * 3, Offset.offset(140)); + + assertThat(count.getDifference()).isLessThanOrEqualTo(2); + + for (int i = 0; i < 600 - 3; i++) { + int before = count.getSum(); + + runReplanning(); + + // Check the number of innovations per iteration + int diff = count.getSum() - before; + + assertThat(diff) + .isCloseTo(3000, Offset.offset(300)); + + } + + assertThat(count.getSum()).isCloseTo(3000 * 600, Offset.offset(300)); + assertThat(count.getDifference()).isLessThanOrEqualTo(2); + } + + @Test + void baseline() { + + // Compare how balanced baseline iterative algorithm is per iteration + for (int i = 0; i < 600; i++) { + int innovation = 0; + for (Person person : population.getPersons().values()) { + double rnd = MatsimRandom.getRandom().nextDouble(); + if (rnd < 0.3) { + innovation++; + } + } + + assertThat(innovation).isCloseTo(3000, Offset.offset(300)); + } + + } + + private void runReplanning() { + + chooser.beforeReplanning(null); + + for (Person person : population.getPersons().values()) { + + GenericPlanStrategy strategy = chooser.chooseStrategy(person, PopulationUtils.getSubpopulation(person), null, weights); + strategy.run(person); + + // Always remove old plan + person.getPlans().removeIf(p -> p != person.getSelectedPlan()); + } + } + + private final static class InnovationCounting implements PlanStrategyModule { + + private final Int2IntMap count; + + private InnovationCounting() { + count = new Int2IntOpenHashMap(); + } + + @Override + public void prepareReplanning(ReplanningContext replanningContext) { + + } + + @Override + public void handlePlan(Plan plan) { + count.mergeInt(plan.getPerson().getId().index(), 1, Integer::sum); + } + + @Override + public void finishReplanning() { + } + + int getSum() { + return count.values().intStream().sum(); + } + + int getDifference() { + IntSummaryStatistics stats = count.values().intStream().summaryStatistics(); + return stats.getMax() - stats.getMin(); + } + + } + +}