diff --git a/contribs/application/src/main/java/org/matsim/application/Category.java b/contribs/application/src/main/java/org/matsim/application/Category.java new file mode 100644 index 00000000000..c3f437d86a4 --- /dev/null +++ b/contribs/application/src/main/java/org/matsim/application/Category.java @@ -0,0 +1,180 @@ +package org.matsim.application; + +import org.matsim.core.config.ReflectiveConfigGroup; + +import java.util.*; +import java.util.regex.Pattern; + +/** + * Categorize values into groups. + */ +public final class Category { + + private static final Set TRUE = Set.of("true", "yes", "1", "on", "y", "j", "ja"); + private static final Set FALSE = Set.of("false", "no", "0", "off", "n", "nein"); + + /** + * Unique values of the category. + */ + private final Set values; + + /** + * Groups of values that have been subsumed under a single category. + * These are values separated by , + */ + private final Map grouped; + + + /** + * Regular expressions for each category. + */ + private final Map regex; + + /** + * Range categories. + */ + private final List ranges; + + public Category(Set values) { + this.values = values; + this.grouped = new HashMap<>(); + this.regex = new HashMap<>(); + for (String v : values) { + if (v.contains(",")) { + String[] grouped = v.split(","); + for (String g : grouped) { + this.grouped.put(g, v); + } + } + + if (v.startsWith("/") && v.endsWith("/")) { + this.regex.put(v, Pattern.compile(v.substring(1, v.length() - 1), Pattern.CASE_INSENSITIVE)); + } + } + + boolean range = this.values.stream().allMatch(v -> v.contains("-") || v.contains("+")); + if (range) { + ranges = new ArrayList<>(); + for (String value : this.values) { + if (value.contains("-")) { + String[] parts = value.split("-"); + ranges.add(new Range(Double.parseDouble(parts[0]), Double.parseDouble(parts[1]), value)); + } else if (value.contains("+")) { + ranges.add(new Range(Double.parseDouble(value.replace("+", "")), Double.POSITIVE_INFINITY, value)); + } + } + + ranges.sort(Comparator.comparingDouble(r -> r.left)); + } else + ranges = null; + + + // Check if all values are boolean + if (values.stream().allMatch(v -> TRUE.contains(v.toLowerCase()) || FALSE.contains(v.toLowerCase()))) { + for (String value : values) { + Set group = TRUE.contains(value.toLowerCase()) ? TRUE : FALSE; + for (String g : group) { + this.grouped.put(g, value); + } + } + } + } + + /** + * Create categories from config parameters. + */ + public static Map fromConfigParams(Collection params) { + + Map> categories = new HashMap<>(); + + // Collect all values + for (ReflectiveConfigGroup parameter : params) { + for (Map.Entry kv : parameter.getParams().entrySet()) { + categories.computeIfAbsent(kv.getKey(), k -> new HashSet<>()).add(kv.getValue()); + } + } + + return categories.entrySet().stream() + .collect(HashMap::new, (m, e) -> m.put(e.getKey(), new Category(e.getValue())), HashMap::putAll); + } + + /** + * Categorize a single value. + */ + public String categorize(Object value) { + + if (value == null) + return null; + + if (value instanceof Boolean) { + // Booleans and synonyms are in the group map + return categorize(((Boolean) value).toString().toLowerCase()); + } else if (value instanceof Number) { + return categorizeNumber((Number) value); + } else { + String v = value.toString(); + if (values.contains(v)) + return v; + else if (grouped.containsKey(v)) + return grouped.get(v); + else { + for (Map.Entry kv : regex.entrySet()) { + if (kv.getValue().matcher(v).matches()) + return kv.getKey(); + } + } + + try { + double d = Double.parseDouble(v); + return categorizeNumber(d); + } catch (NumberFormatException e) { + return null; + } + } + } + + private String categorizeNumber(Number value) { + + if (ranges != null) { + for (Range r : ranges) { + if (value.doubleValue() >= r.left && value.doubleValue() < r.right) + return r.label; + } + } + + // Match string representation + String v = value.toString(); + if (values.contains(v)) + return v; + else if (grouped.containsKey(v)) + return grouped.get(v); + + + // Convert the number to a whole number, which will have a different string representation + if (value instanceof Float || value instanceof Double) { + return categorizeNumber(value.longValue()); + } + + return null; + } + + @Override + public String toString() { + return "Category{" + + "values=" + values + + (grouped != null && !grouped.isEmpty() ? ", grouped=" + grouped : "") + + (regex != null && !regex.isEmpty() ? ", regex=" + regex : "") + + (ranges != null && !ranges.isEmpty() ? ", ranges=" + ranges : "") + + '}'; + } + + /** + * Number range. + * + * @param left Left bound of the range. + * @param right Right bound of the range. (exclusive) + * @param label Label of this group. + */ + private record Range(double left, double right, String label) { + } +} diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ModeTargetParameters.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ModeTargetParameters.java new file mode 100644 index 00000000000..bd31c21966f --- /dev/null +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ModeTargetParameters.java @@ -0,0 +1,94 @@ +package org.matsim.modechoice; + +import com.google.common.base.Joiner; +import it.unimi.dsi.fastutil.objects.Object2DoubleMap; +import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap; +import jakarta.validation.constraints.Positive; +import org.matsim.application.Category; +import org.matsim.core.config.ReflectiveConfigGroup; +import org.matsim.utils.objectattributes.attributable.Attributes; + +import java.util.Map; +import java.util.Objects; + +/** + * Target parameters for mode shares. + * This group allows arbitrary attributes to be defined, which are matched against person attributes. + */ +public final class ModeTargetParameters extends ReflectiveConfigGroup { + + public static final String GROUP_NAME = "modeTargetParameters"; + + private static final String SHARES = "shares"; + + private static Joiner.MapJoiner JOINER = Joiner.on(",").withKeyValueSeparator("="); + + @Parameter + @Positive + @Comment("Allowed tolerance for mode share.") + public double tolerance = 0.01; + + private Object2DoubleMap shares = new Object2DoubleOpenHashMap<>(); + + public ModeTargetParameters() { + super(GROUP_NAME, true); + } + + public ModeTargetParameters(String subpopulation, Map shares) { + super(GROUP_NAME, true); + this.shares.putAll(shares); + this.addParam("subpopulation", subpopulation); + } + + /** + * Name of the config group. + */ + public String getGroupName() { + Map params = getParams(); + params.remove("shares"); + params.remove("tolerance"); + return JOINER.join(params); + } + + public Object2DoubleMap getShares() { + return shares; + } + + @StringSetter(SHARES) + void setShares(String shares) { + this.shares = new Object2DoubleOpenHashMap<>(); + String[] parts = shares.split(","); + for (String part : parts) { + String[] kv = part.split("="); + this.shares.put(kv[0], Double.parseDouble(kv[1])); + } + } + + @StringGetter(SHARES) + String getSharesString() { + return JOINER.join(shares); + } + + + /** + * Match attributes from an object with parameters defined in config. + */ + public boolean matchPerson(Attributes attr, Map categories) { + + for (Map.Entry e : getParams().entrySet()) { + if (e.getKey().equals(SHARES) || e.getKey().equals("tolerance")) + continue; + + // might be null if not defined + Object objValue = attr.getAttribute(e.getKey()); + String category = categories.get(e.getKey()).categorize(objValue); + + // compare as string + if (!Objects.toString(category).equals(e.getValue())) + return false; + } + + return true; + } + +} diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ScheduledModeChoiceConfigGroup.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ScheduledModeChoiceConfigGroup.java index b16be68d637..46d5aadf6a6 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ScheduledModeChoiceConfigGroup.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ScheduledModeChoiceConfigGroup.java @@ -128,22 +128,4 @@ public void addParameterSet(ConfigGroup set) { } } - /** - * Target parameters for mode shares. - * This group allows arbitrary attributes to be defined, which are matched against person attributes. - */ - public static final class ModeTargetParameters extends ReflectiveConfigGroup { - - private static final String GROUP_NAME = "modeTargetParameters"; - - @Parameter - @Comment("Target mode shares. Should only contain relevant modes.") - public Map shares = new HashMap<>(); - - public ModeTargetParameters() { - super(GROUP_NAME, true); - } - - } - } diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ScheduledModeChoiceModule.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ScheduledModeChoiceModule.java index 7fc006af4ec..deb8ae81f6e 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ScheduledModeChoiceModule.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/ScheduledModeChoiceModule.java @@ -14,8 +14,12 @@ import org.matsim.modechoice.replanning.scheduled.AllBestPlansStrategyProvider; import org.matsim.modechoice.replanning.scheduled.ReRouteSelectedStrategyProvider; import org.matsim.modechoice.replanning.scheduled.ScheduledStrategyChooser; +import org.matsim.modechoice.replanning.scheduled.TimeMutateSelectedStrategyProvider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.OptionalDouble; +import java.util.Set; /** * Module for to enable scheduled mode choice. @@ -42,6 +46,9 @@ public class ScheduledModeChoiceModule extends AbstractModule { ALL_BEST_K_PLAN_MODES_STRATEGY ); public static String REROUTE_SELECTED = "ReRouteSelected"; + public static String TIME_MUTATE_SELECTED = "TimeAllocationMutatorSelected"; + + private final Builder builder; private ScheduledModeChoiceModule(Builder builder) { @@ -60,9 +67,9 @@ public void install() { addPlanStrategyBinding(ALL_BEST_K_PLAN_MODES_STRATEGY).toProvider(AllBestPlansStrategyProvider.class); addPlanStrategyBinding(REROUTE_SELECTED).toProvider(ReRouteSelectedStrategyProvider.class); + addPlanStrategyBinding(TIME_MUTATE_SELECTED).toProvider(TimeMutateSelectedStrategyProvider.class); - bind(new TypeLiteral>() { - }).to(ScheduledStrategyChooser.class); + bind(new TypeLiteral>() {}).to(ScheduledStrategyChooser.class); ScheduledModeChoiceConfigGroup config = ConfigUtils.addOrGetModule(getConfig(), ScheduledModeChoiceConfigGroup.class); @@ -76,6 +83,13 @@ public void install() { .mapToDouble(ReplanningConfigGroup.StrategySettings::getWeight) .findFirst(); + OptionalDouble timeMutate = strategies.stream().filter(s -> + (s.getStrategyName().equals(DefaultPlanStrategiesModule.DefaultStrategy.TimeAllocationMutator) || + s.getStrategyName().equals(DefaultPlanStrategiesModule.DefaultStrategy.TimeAllocationMutator_ReRoute)) && + s.getSubpopulation().equals(subpopulation)) + .mapToDouble(ReplanningConfigGroup.StrategySettings::getWeight) + .findFirst(); + strategies.removeIf(s -> s.getSubpopulation().equals(subpopulation) && EXCLUDED.contains(s.getStrategyName())); ReplanningConfigGroup.StrategySettings strategy = new ReplanningConfigGroup.StrategySettings(); @@ -91,6 +105,14 @@ public void install() { s.setWeight(reroute.getAsDouble()); strategies.add(s); } + + if (timeMutate.isPresent()) { + ReplanningConfigGroup.StrategySettings s = new ReplanningConfigGroup.StrategySettings(); + s.setStrategyName(TIME_MUTATE_SELECTED); + s.setSubpopulation(subpopulation); + s.setWeight(timeMutate.getAsDouble()); + strategies.add(s); + } } strategies.forEach(s -> getConfig().replanning().addStrategySettings(s)); diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/AllBestPlansStrategy.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/AllBestPlansStrategy.java index 7c9292e5bbd..b61856b96dd 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/AllBestPlansStrategy.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/AllBestPlansStrategy.java @@ -7,22 +7,24 @@ import org.matsim.api.core.v01.Scenario; import org.matsim.api.core.v01.population.Person; import org.matsim.api.core.v01.population.Plan; +import org.matsim.application.Category; import org.matsim.core.config.Config; import org.matsim.core.config.ConfigUtils; +import org.matsim.core.population.PopulationUtils; import org.matsim.core.population.algorithms.ParallelPersonAlgorithmUtils; import org.matsim.core.population.algorithms.PersonAlgorithm; import org.matsim.core.population.algorithms.PlanAlgorithm; import org.matsim.core.replanning.ReplanningContext; import org.matsim.core.replanning.modules.AbstractMultithreadedModule; -import org.matsim.modechoice.InformedModeChoiceConfigGroup; -import org.matsim.modechoice.PlanCandidate; -import org.matsim.modechoice.PlanModel; -import org.matsim.modechoice.ScheduledModeChoiceConfigGroup; +import org.matsim.modechoice.*; import org.matsim.modechoice.replanning.GeneratorContext; import org.matsim.modechoice.replanning.scheduled.solver.ModeSchedulingSolver; +import org.matsim.modechoice.replanning.scheduled.solver.ModeTarget; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; /** @@ -47,6 +49,8 @@ public class AllBestPlansStrategy extends AbstractMultithreadedModule implements */ private int applyIdx = -1; + private ModeTarget target; + public AllBestPlansStrategy(Config config, Scenario scenario, Provider generator) { super(config.global()); @@ -67,24 +71,58 @@ protected void beforePrepareReplanningHook(ReplanningContext replanningContextTm if (!plans.isEmpty()) return; + Map params = scheduleConfig.getModeTargetParameters().stream().collect( + LinkedHashMap::new, + (map, p) -> map.put(p.getGroupName(), p), + LinkedHashMap::putAll + ); + + target = new ModeTarget( + Category.fromConfigParams(scheduleConfig.getModeTargetParameters()), params, + params.entrySet().stream().collect( + LinkedHashMap::new, + (map, p) -> map.put(p.getKey(), p.getValue().getShares()), + LinkedHashMap::putAll + ), + new ConcurrentHashMap<>() + ); + log.info("Creating plan candidates."); ParallelPersonAlgorithmUtils.run(scenario.getPopulation(), config.global().getNumberOfThreads(), this); - log.info("Creating plan schedule."); + log.info("Creating plan schedule for {} agents.", target.mapping().size()); InformedModeChoiceConfigGroup imc = ConfigUtils.addOrGetModule(config, InformedModeChoiceConfigGroup.class); ModeSchedulingSolver solver = new ModeSchedulingSolver(scheduleConfig.getScheduleIterations(), imc.getTopK(), - scheduleConfig.getModeTargetParameters(), scheduleConfig.getTargetSwitchShare()); + target, scheduleConfig.getTargetSwitchShare()); - plans = solver.solve(plans); + // make sure plans always have the same order + plans = solver.solve(new TreeMap<>(plans)); } @Override public void run(Person person) { + // Not configured subpopulations are ignored + if (!scheduleConfig.getSubpopulations().contains(PopulationUtils.getSubpopulation(person))) + return; + + String category = null; + for (Map.Entry kv : this.target.params().entrySet()) { + if (kv.getValue().matchPerson(person.getAttributes(), target.categories())) { + category = kv.getKey(); + break; + } + } + PlanModel model = PlanModel.newInstance(person.getSelectedPlan()); List candidates = ctx.get().generator.generate(model); plans.put(person.getId(), candidates); + + // Only relevant are put in the mapping, others will be ignored + if (category != null) + target.mapping().put(person.getId(), category); + } @Override @@ -99,6 +137,7 @@ private final class Algorithm implements PlanAlgorithm { public void run(Plan plan) { // Does nothing if schedule is not advancing + // this strategy also serves the purpose of keeping the selected plans for a part of the agents if (applyIdx < 0) { return; } @@ -114,6 +153,4 @@ public void run(Plan plan) { } } } - - } diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/ReRouteSelectedStrategyProvider.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/ReRouteSelectedStrategyProvider.java index 13dd22e4c7c..412ad96144e 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/ReRouteSelectedStrategyProvider.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/ReRouteSelectedStrategyProvider.java @@ -29,8 +29,6 @@ public class ReRouteSelectedStrategyProvider implements Provider { @Override public PlanStrategy get() { - // TODO: selected plan might be removed after iteration - // would need a custom removal selector that does not remove the selected plan PlanStrategyImpl.Builder builder = new PlanStrategyImpl.Builder(new KeepSelected<>()); diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/TimeMutateSelectedStrategyProvider.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/TimeMutateSelectedStrategyProvider.java new file mode 100644 index 00000000000..add33d11372 --- /dev/null +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/TimeMutateSelectedStrategyProvider.java @@ -0,0 +1,37 @@ +package org.matsim.modechoice.replanning.scheduled; + +import com.google.inject.Provider; +import jakarta.inject.Inject; +import org.matsim.core.config.groups.GlobalConfigGroup; +import org.matsim.core.config.groups.TimeAllocationMutatorConfigGroup; +import org.matsim.core.replanning.PlanStrategy; +import org.matsim.core.replanning.PlanStrategyImpl; +import org.matsim.core.replanning.modules.ReRoute; +import org.matsim.core.replanning.selectors.KeepSelected; +import org.matsim.core.replanning.strategies.TimeAllocationMutatorModule; +import org.matsim.core.router.TripRouter; +import org.matsim.core.utils.timing.TimeInterpretation; +import org.matsim.facilities.ActivityFacilities; + +/** + * Same as re-route but keeps the selected plan. + */ +public class TimeMutateSelectedStrategyProvider implements Provider { + @Inject private jakarta.inject.Provider tripRouterProvider; + @Inject private GlobalConfigGroup globalConfigGroup; + @Inject private TimeAllocationMutatorConfigGroup timeAllocationMutatorConfigGroup; + @Inject private ActivityFacilities activityFacilities; + @Inject private TimeInterpretation timeInterpretation; + + @Override + public PlanStrategy get() { + + PlanStrategyImpl.Builder builder = new PlanStrategyImpl.Builder(new KeepSelected<>()); + + builder.addStrategyModule(new TimeAllocationMutatorModule(this.timeAllocationMutatorConfigGroup, this.globalConfigGroup) ); + builder.addStrategyModule(new ReRoute(this.activityFacilities, this.tripRouterProvider, this.globalConfigGroup, this.timeInterpretation)); + + return builder.build(); + } + +} diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/solver/ModeSchedulingSolver.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/solver/ModeSchedulingSolver.java index f4d1fb6ff8c..43f283ec8f5 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/solver/ModeSchedulingSolver.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/solver/ModeSchedulingSolver.java @@ -3,15 +3,15 @@ import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntIntPair; import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.objects.Object2DoubleMap; import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.matsim.api.core.v01.Id; -import org.matsim.api.core.v01.TransportMode; import org.matsim.api.core.v01.population.Person; +import org.matsim.modechoice.ModeTargetParameters; import org.matsim.modechoice.PlanCandidate; -import org.matsim.modechoice.ScheduledModeChoiceConfigGroup; import org.optaplanner.core.api.solver.Solver; import org.optaplanner.core.api.solver.SolverFactory; @@ -33,14 +33,16 @@ public final class ModeSchedulingSolver { private final int scheduleLength; private final int topK; + private final ModeTarget target; private final double targetSwitchShare; public ModeSchedulingSolver(int scheduleLength, int topK, - List modeTargetParameters, + ModeTarget target, double targetSwitchShare) { this.scheduleLength = scheduleLength; // topK must be divisible by window size this.topK = topK + Math.floorMod(-topK, WINDOW_SIZE); + this.target = target; this.targetSwitchShare = targetSwitchShare; } @@ -49,6 +51,9 @@ public Map, List> solve(Map, List topK) throw new IllegalArgumentException("Schedule length must be less than or equal to topK"); + if (plans.isEmpty()) + throw new IllegalArgumentException("No plans to optimize"); + List agents = new ArrayList<>(); Reference2ObjectMap target = createTarget(plans); @@ -62,6 +67,10 @@ public Map, List> solve(Map, List, List> solve(Map, List Objects.equals(p, planCategories[0]))) @@ -86,10 +95,12 @@ public Map, List> solve(Map, List a.length).sum()); - Map, List> result = new HashMap<>(); + Map, List> result = new LinkedHashMap<>(); for (int k = 0; k < scheduleLength; k += WINDOW_SIZE) { @@ -97,6 +108,9 @@ public Map, List> solve(Map, List, List> solve(Map, List createTarget(Map, List> plans) { - int trips = plans.values().stream() - .mapToInt(p -> p.get(0).size()) - .sum(); - Reference2ObjectMap target = new Reference2ObjectLinkedOpenHashMap<>(); - // TODO: fixed car share of 50%# - // fixed deviation - target.put(TransportMode.car, IntIntPair.of((int) (trips * 0.495), (int) (trips * 0.505))); + for (Map.Entry> kv : this.target.targets().entrySet()) { + + String key = kv.getKey(); + ModeTargetParameters params = this.target.params().get(key); + + // number of trips for persons belonging to the target + int trips = plans.entrySet().stream() + .filter(p -> this.target.mapping().get(p.getKey()).equals(key)) + .mapToInt(p -> p.getValue().get(0).size()) + .sum(); + + for (Object2DoubleMap.Entry e : kv.getValue().object2DoubleEntrySet()) { + int lower = (int) (trips * (e.getDoubleValue() - params.tolerance / 2)); + int upper = (int) (trips * (e.getDoubleValue() + params.tolerance / 2)); + + String t = key + "[" + e.getKey() + "]"; + IntIntPair bounds = IntIntPair.of(lower, upper); + target.put(t.intern(), bounds); + + log.info("Target {} {} with {} trips.", t, bounds, trips); + } + } return target; } @@ -147,37 +176,78 @@ private Reference2ObjectMap createTarget(Map, Lis */ private void initialize(int k, List agents) { - // TODO: could choose plans such that error is minimized - List copy = new ArrayList<>(agents); Random rnd = new Random(k); Collections.shuffle(copy, rnd); + IntList avail = new IntArrayList(topK); + for (AgentSchedule agent : copy) { agent.indices.clear(); - IntList avail = new IntArrayList(agent.availablePlans); - Collections.shuffle(avail, rnd); + avail.clear(); + Collections.shuffle(agent.availablePlans, rnd); + avail.addAll(agent.availablePlans); for (int i = 0; i < WINDOW_SIZE; i++) { agent.indices.add(avail.getInt(i)); } } + } + /** + * Tries to construct an initial solution. + */ + private void preSolve(int k, ModeSchedulingProblem problem) { + + ScoreCalculator score = new ScoreCalculator(); + score.resetWorkingSolution(problem); + + int[] targets = new int[problem.getTargets().size()]; + int i = 0; + for (Map.Entry kv : problem.getTargets().entrySet()) { + targets[i++] = (kv.getValue().leftInt() + kv.getValue().rightInt()) / 2; + } + + List copy = new ArrayList<>(problem.getAgents()); + + Random rnd = new Random(k); + Collections.shuffle(copy, rnd); + + IntList avail = new IntArrayList(topK); + + for (AgentSchedule agent : copy) { + + // Avail stores other available plans for initialisation + avail.clear(); + avail.addAll(agent.availablePlans); + avail.removeAll(agent.indices); + + for (int idx = 0; idx < WINDOW_SIZE; idx++) { + score.applyImprovement(agent, idx, avail, targets); + } + } } - private String[] categorizePlans(List candidates) { + private String[] categorizePlans(Id id, List candidates) { int trips = candidates.get(0).size(); String[] categories = new String[candidates.size() * trips]; + String key = target.mapping().get(id); + Object2DoubleMap t = target.targets().get(key); + for (int i = 0; i < candidates.size(); i++) { PlanCandidate candidate = candidates.get(i); - // TODO: hard coded to car mode for (int j = 0; j < trips; j++) { - categories[i * trips + j] = Objects.equals(candidate.getMode(j), TransportMode.car) ? TransportMode.car : null; + String mode = candidate.getMode(j); + String category = null; + if (t.containsKey(mode)) { + category = key + "[" + mode + "]"; + } + categories[i * trips + j] = category != null ? category.intern() : null; } } diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/solver/ModeTarget.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/solver/ModeTarget.java new file mode 100644 index 00000000000..49c57448704 --- /dev/null +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/solver/ModeTarget.java @@ -0,0 +1,23 @@ +package org.matsim.modechoice.replanning.scheduled.solver; + +import it.unimi.dsi.fastutil.objects.Object2DoubleMap; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.population.Person; +import org.matsim.application.Category; +import org.matsim.modechoice.ModeTargetParameters; + +import java.util.Map; + +/** + * Holds all needed information for all mode share target values + * + * @param categories categories needed for matching + * @param params maps name to params + * @param targets Group name to respective target values. + * @param mapping Mapping from person id to the mode choice target. + */ +public record ModeTarget(Map categories, + Map params, + Map> targets, + Map, String> mapping) { +} diff --git a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/solver/ScoreCalculator.java b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/solver/ScoreCalculator.java index d22d1ef1b27..5a5ee250f0d 100644 --- a/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/solver/ScoreCalculator.java +++ b/contribs/informed-mode-choice/src/main/java/org/matsim/modechoice/replanning/scheduled/solver/ScoreCalculator.java @@ -2,6 +2,7 @@ import it.unimi.dsi.fastutil.ints.IntIntPair; +import it.unimi.dsi.fastutil.ints.IntList; import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore; import org.optaplanner.core.api.score.calculator.IncrementalScoreCalculator; @@ -66,6 +67,59 @@ private void updateAgentPlan(AgentSchedule agent, int diff) { } } + /** + * Improve an agent schedule and update the internal state. + */ + public void applyImprovement(AgentSchedule agent, int idx, IntList avail, int[] targets) { + + // new plan index + int bestK = agent.indices.getInt(idx); + int[] bestDiff = new int[targets.length]; + + // Weight of the current chosen plan + int[] weight = new int[targets.length]; + + // positive diff means there are too many trips (per target) + int[] diff = new int[targets.length]; + + int bestError = 0; + for (int i = 0; i < targets.length; i++) { + weight[i] = agent.weights[bestK * targets.length + i]; + diff[i] = observed[idx * targets.length + i] - targets[i]; + bestError += Math.abs(diff[i]); + } + + for (int j = 0; j < avail.size(); j++) { + + int k = avail.getInt(j); + + // calc the improvement for each plan + int[] d = new int[targets.length]; + int sum = 0; + for (int i = 0; i < targets.length; i++) { + byte w = agent.weights[k * targets.length + i]; + d[i] = w - weight[i]; + sum += Math.abs(diff[i] + d[i]); + } + + if (sum < bestError) { + bestError = sum; + bestDiff = d; + bestK = k; + } + } + + // update if index has changed + if (bestK != agent.indices.getInt(idx)) { + avail.rem(bestK); + agent.indices.set(idx, bestK); + + for (int i = 0; i < targets.length; i++) { + observed[idx * targets.length + i] += bestDiff[i]; + } + } + } + @Override public void beforeEntityAdded(Object entity) { } diff --git a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/scheduled/AllBestPlansStrategyTest.java b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/scheduled/AllBestPlansStrategyTest.java index c279d3d7681..c87883094fa 100644 --- a/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/scheduled/AllBestPlansStrategyTest.java +++ b/contribs/informed-mode-choice/src/test/java/org/matsim/modechoice/replanning/scheduled/AllBestPlansStrategyTest.java @@ -2,13 +2,14 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.matsim.api.core.v01.TransportMode; import org.matsim.core.config.Config; import org.matsim.core.config.ConfigUtils; import org.matsim.core.config.groups.ReplanningConfigGroup; import org.matsim.core.controler.Controler; import org.matsim.modechoice.*; -import static org.junit.jupiter.api.Assertions.*; +import java.util.Map; class AllBestPlansStrategyTest extends ScenarioTest { @@ -30,6 +31,12 @@ protected void prepareConfig(Config config) { smc.setScheduleIterations(15); smc.setSubpopulations("person"); + ModeTargetParameters target = new ModeTargetParameters( + "person", Map.of(TransportMode.car, 0.55) + ); + + smc.addParameterSet(target); + } @Override diff --git a/matsim/src/main/java/org/matsim/core/replanning/strategies/TimeAllocationMutatorModule.java b/matsim/src/main/java/org/matsim/core/replanning/strategies/TimeAllocationMutatorModule.java index 485089a53ee..1711d042697 100644 --- a/matsim/src/main/java/org/matsim/core/replanning/strategies/TimeAllocationMutatorModule.java +++ b/matsim/src/main/java/org/matsim/core/replanning/strategies/TimeAllocationMutatorModule.java @@ -38,7 +38,7 @@ * @author mrieser * @see org.matsim.core.population.algorithms.TripPlanMutateTimeAllocation */ -class TimeAllocationMutatorModule extends AbstractMultithreadedModule{ +public class TimeAllocationMutatorModule extends AbstractMultithreadedModule{ private static final Logger log = LogManager.getLogger( TimeAllocationMutatorModule.class ); @@ -47,7 +47,7 @@ class TimeAllocationMutatorModule extends AbstractMultithreadedModule{ private final TimeAllocationMutatorConfigGroup timeAllocationMutatorConfigGroup; - TimeAllocationMutatorModule( TimeAllocationMutatorConfigGroup timeAllocationMutatorConfigGroup, GlobalConfigGroup globalConfigGroup) { + public TimeAllocationMutatorModule( TimeAllocationMutatorConfigGroup timeAllocationMutatorConfigGroup, GlobalConfigGroup globalConfigGroup) { super(globalConfigGroup); this.mutationRange = timeAllocationMutatorConfigGroup.getMutationRange(); this.affectingDuration = timeAllocationMutatorConfigGroup.isAffectingDuration();