diff --git a/pom.xml b/pom.xml index 31d77d0..166a2d0 100644 --- a/pom.xml +++ b/pom.xml @@ -157,6 +157,22 @@ 5.3.0 + + + org.apache.commons + commons-math3 + + + org.apache.commons + commons-statistics-distribution + 1.0 + + + org.apache.commons + commons-rng-simple + 1.5 + + com.github.matsim-vsp pt-extensions diff --git a/src/main/java/org/matsim/run/MetropoleRuhrScenario.java b/src/main/java/org/matsim/run/MetropoleRuhrScenario.java index f6a8b7b..b1e7296 100644 --- a/src/main/java/org/matsim/run/MetropoleRuhrScenario.java +++ b/src/main/java/org/matsim/run/MetropoleRuhrScenario.java @@ -27,6 +27,7 @@ import org.apache.logging.log4j.Logger; import org.matsim.analysis.ModeChoiceCoverageControlerListener; import org.matsim.analysis.TripMatrix; +import org.matsim.analysis.linkpaxvolumes.LinkPaxVolumesAnalysisModule; import org.matsim.analysis.personMoney.PersonMoneyEventsAnalysisModule; import org.matsim.analysis.pt.stop2stop.PtStop2StopAnalysisModule; import org.matsim.api.core.v01.Id; @@ -58,6 +59,8 @@ import org.matsim.extensions.pt.routing.ptRoutingModes.PtIntermodalRoutingModesModule; import org.matsim.prepare.AdjustDemand; import org.matsim.prepare.RuhrUtils; +import org.matsim.run.scoring.AdvancedScoringConfigGroup; +import org.matsim.run.scoring.AdvancedScoringModule; import org.matsim.simwrapper.SimWrapperConfigGroup; import org.matsim.simwrapper.SimWrapperModule; import org.matsim.vehicles.VehicleType; @@ -345,11 +348,27 @@ protected void prepareControler(Controler controler) { controler.addOverridingModule(new IntermodalTripFareCompensatorsModule()); // additional analysis output - //controler.addOverridingModule(new LinkPaxVolumesAnalysisModule()); + controler.addOverridingModule(new LinkPaxVolumesAnalysisModule()); controler.addOverridingModule(new PtStop2StopAnalysisModule()); controler.addOverridingModule(new PtFareModule()); + // AdvancedScoring as for matsim-berlin! + if (ConfigUtils.hasModule(controler.getConfig(), AdvancedScoringConfigGroup.class)) { + controler.addOverridingModule(new AdvancedScoringModule()); + } else { + // if the above config group is not present we still need income dependent scoring + // this implementation also allows for person specific asc + + // for income dependent scoring --> this works with the bicycle contrib as we don´t use the scoring in the bicycle contrib + controler.addOverridingModule(new AbstractModule() { + @Override + public void install() { + bind(ScoringParametersForPerson.class).to(IncomeDependentUtilityOfMoneyPersonScoringParameters.class).in(Singleton.class); + } + }); + } + controler.addOverridingModule(new AbstractModule() { @Override public void install() { @@ -364,10 +383,6 @@ public void install() { bind(RaptorIntermodalAccessEgress.class).to(EnhancedRaptorIntermodalAccessEgress.class); // separate pure walk+pt from intermodal pt in mode stats etc. bind(AnalysisMainModeIdentifier.class).to(IntermodalPtAnalysisModeIdentifier.class); - - // for income dependent scoring --> this works with the bicycle contrib as we don´t use the scoring in the bicycle contrib - bind(ScoringParametersForPerson.class).to(IncomeDependentUtilityOfMoneyPersonScoringParameters.class).in(Singleton.class); - } }); diff --git a/src/main/java/org/matsim/run/scoring/AdvancedScoringConfigGroup.java b/src/main/java/org/matsim/run/scoring/AdvancedScoringConfigGroup.java new file mode 100644 index 0000000..1cad0b1 --- /dev/null +++ b/src/main/java/org/matsim/run/scoring/AdvancedScoringConfigGroup.java @@ -0,0 +1,182 @@ +package org.matsim.run.scoring; + +import org.matsim.core.config.ConfigGroup; +import org.matsim.core.config.ReflectiveConfigGroup; + +import java.util.*; + +/** + * COPIED FROM BERLIN - DO NOT DEVELOP HERE. WILL BE DELETED AS SOON AS THIS FUNCTIONALITY IS MOVED THE MATSIM CORE. + * + * Stores scoring parameters for {@link AdvancedScoringModule}. + */ +@SuppressWarnings("checkstyle:VisibilityModifier") +public final class AdvancedScoringConfigGroup extends ReflectiveConfigGroup { + + private static final String GROUP_NAME = "advancedScoring"; + + @Parameter + @Comment("The distance groups if marginal utility of distance is adjusted. In meters.") + public List distGroups; + + @Parameter + @Comment("Enable income dependent marginal utility of money.") + public IncomeDependentScoring incomeDependent = IncomeDependentScoring.avgByPersonalIncome; + + @Parameter + @Comment("Exponent for (global_income / personal_income) ** x.") + public double incomeExponent = 1; + + @Parameter + @Comment("Define how to load existing preferences.") + public LoadPreferences loadPreferences = LoadPreferences.none; + + private final List scoringParameters = new ArrayList<>(); + + public AdvancedScoringConfigGroup() { + super(GROUP_NAME); + } + + /** + * Return the defined scoring parameters. + */ + public List getScoringParameters() { + return Collections.unmodifiableList(scoringParameters); + } + + @Override + public ConfigGroup createParameterSet(String type) { + if (type.equals(ScoringParameters.GROUP_NAME)) { + return new ScoringParameters(); + } else { + throw new IllegalArgumentException("Unsupported parameter set type: " + type); + } + } + + @Override + public void addParameterSet(ConfigGroup set) { + if (set instanceof ScoringParameters p) { + super.addParameterSet(set); + scoringParameters.add(p); + } else { + throw new IllegalArgumentException("Unsupported parameter set class: " + set); + } + } + + /** + * Different options for income dependent scoring. + */ + public enum IncomeDependentScoring { + none, + avgByPersonalIncome + } + + /** + * Define how existing preferences are loaded. + */ + public enum LoadPreferences { + none, + requireAttribute, + skipMissing, + skipRefPersons + } + + /** + * Variate values with random draw from specific distribution. + */ + public enum VariationType { + fixed, normal, truncatedNormal + } + + /** + * Scoring parameters for a specific group of agents. + * This group allows arbitrary attributes to be defined, which are matched against person attributes. + */ + public static final class ScoringParameters extends ReflectiveConfigGroup { + + private static final String GROUP_NAME = "scoringParameters"; + + /** + * Params per mode. + */ + private final Map modeParams = new HashMap<>(); + + public ScoringParameters() { + super(GROUP_NAME, true); + } + + public Map getModeParams() { + return modeParams; + } + + @Override + public ConfigGroup createParameterSet(final String type) { + return switch (type) { + case ModeParams.GROUP_NAME -> new ModeParams(); + default -> throw new IllegalArgumentException(type); + }; + } + + @Override + public void addParameterSet(ConfigGroup set) { + if (set instanceof ModeParams p) { + super.addParameterSet(set); + modeParams.put(p.mode, p); + } else { + throw new IllegalArgumentException("Unsupported parameter set class: " + set); + } + } + + /** + * Retrieve mode parameters. + */ + public ModeParams getOrCreateModeParams(String mode) { + if (!modeParams.containsKey(mode)) { + ModeParams p = new ModeParams(); + p.mode = mode; + + addParameterSet(p); + return p; + } + + return modeParams.get(mode); + } + + } + + /** + * Stores mode specific parameters and also attributes to whom to apply this specification. + */ + public static final class ModeParams extends ReflectiveConfigGroup { + + private static final String GROUP_NAME = "modeParams"; + + @Parameter + @Comment("The mode for which the parameters are defined.") + public String mode; + + @Parameter + @Comment("[utils/leg] alternative-specific constant.") + public double deltaConstant; + + @Parameter + @Comment("Variation of the constant across individuals.") + public VariationType varConstant = VariationType.fixed; + + @Parameter + @Comment("[utils/day] if the mode is used at least once.") + public double deltaDailyConstant; + + @Parameter + @Comment("Variation of the daily constant across individuals.") + public VariationType varDailyConstant = VariationType.fixed; + + @Parameter + @Comment("total delta utility per dist group.") + public List deltaPerDistGroup; + + public ModeParams() { + super(GROUP_NAME); + } + } +} diff --git a/src/main/java/org/matsim/run/scoring/AdvancedScoringFunctionFactory.java b/src/main/java/org/matsim/run/scoring/AdvancedScoringFunctionFactory.java new file mode 100644 index 0000000..1e4ffa0 --- /dev/null +++ b/src/main/java/org/matsim/run/scoring/AdvancedScoringFunctionFactory.java @@ -0,0 +1,42 @@ +package org.matsim.run.scoring; + +import com.google.inject.Inject; +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.population.Person; +import org.matsim.core.config.Config; +import org.matsim.core.scoring.ScoringFunction; +import org.matsim.core.scoring.ScoringFunctionFactory; +import org.matsim.core.scoring.SumScoringFunction; +import org.matsim.core.scoring.functions.*; + +/** + * COPIED FROM BERLIN - DO NOT DEVELOP HERE. WILL BE DELETED AS SOON AS THIS FUNCTIONALITY IS MOVED THE MATSIM CORE. + * + * Same as {@link CharyparNagelScoringFunctionFactory} but with {@link PiecewiseLinearlLegScoring}. + */ +public class AdvancedScoringFunctionFactory implements ScoringFunctionFactory { + + @Inject + private Config config; + + @Inject + private ScoringParametersForPerson params; + + @Inject + private Network network; + + @Override + public ScoringFunction createNewScoringFunction(Person person) { + final ScoringParameters parameters = params.getScoringParameters(person); + + SumScoringFunction sumScoringFunction = new SumScoringFunction(); + sumScoringFunction.addScoringFunction(new CharyparNagelActivityScoring(parameters)); + // replaced original leg scoring + sumScoringFunction.addScoringFunction(new PiecewiseLinearlLegScoring(parameters, this.network, config.transit().getTransitModes())); + sumScoringFunction.addScoringFunction(new CharyparNagelMoneyScoring(parameters)); + sumScoringFunction.addScoringFunction(new CharyparNagelAgentStuckScoring(parameters)); + sumScoringFunction.addScoringFunction(new ScoreEventScoring()); + return sumScoringFunction; + } + +} diff --git a/src/main/java/org/matsim/run/scoring/AdvancedScoringModule.java b/src/main/java/org/matsim/run/scoring/AdvancedScoringModule.java new file mode 100644 index 0000000..a89b3fb --- /dev/null +++ b/src/main/java/org/matsim/run/scoring/AdvancedScoringModule.java @@ -0,0 +1,26 @@ +package org.matsim.run.scoring; + +import jakarta.inject.Singleton; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.controler.AbstractModule; +import org.matsim.core.scoring.functions.ScoringParametersForPerson; + +/** + * COPIED FROM BERLIN - DO NOT DEVELOP HERE. WILL BE DELETED AS SOON AS THIS FUNCTIONALITY IS MOVED THE MATSIM CORE. + * + * Module to bind components needed for advanced scoring functionality configured by {@link AdvancedScoringConfigGroup}. + */ +public class AdvancedScoringModule extends AbstractModule { + + @Override + public void install() { + + ConfigUtils.addOrGetModule(getConfig(), AdvancedScoringConfigGroup.class); + + bind(ScoringParametersForPerson.class).to(IndividualPersonScoringParameters.class).in(Singleton.class); + + addControlerListenerBinding().to(AdvancedScoringOutputWriter.class).in(Singleton.class); + + bindScoringFunctionFactory().to(AdvancedScoringFunctionFactory.class).in(Singleton.class); + } +} diff --git a/src/main/java/org/matsim/run/scoring/AdvancedScoringOutputWriter.java b/src/main/java/org/matsim/run/scoring/AdvancedScoringOutputWriter.java new file mode 100644 index 0000000..894c33b --- /dev/null +++ b/src/main/java/org/matsim/run/scoring/AdvancedScoringOutputWriter.java @@ -0,0 +1,72 @@ +package org.matsim.run.scoring; + +import com.google.inject.Inject; +import it.unimi.dsi.fastutil.objects.Object2DoubleMap; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.matsim.api.core.v01.population.Person; +import org.matsim.core.controler.OutputDirectoryHierarchy; +import org.matsim.core.controler.events.IterationEndsEvent; +import org.matsim.core.controler.listener.IterationEndsListener; +import org.matsim.core.scoring.functions.ScoringParametersForPerson; +import org.matsim.core.utils.io.IOUtils; + +import java.io.IOException; +import java.io.UncheckedIOException; + +/** + * COPIED FROM BERLIN - DO NOT DEVELOP HERE. WILL BE DELETED AS SOON AS THIS FUNCTIONALITY IS MOVED THE MATSIM CORE. + * + * This class writes person specific information from {@link IndividualPersonScoringParameters} to the output. + */ +public class AdvancedScoringOutputWriter implements IterationEndsListener { + + + @Inject + private ScoringParametersForPerson scoring; + + private boolean outputWritten = false; + + @Override + public void notifyIterationEnds(IterationEndsEvent event) { + + if (outputWritten) + return; + + if (!(scoring instanceof IndividualPersonScoringParameters params)) + return; + + OutputDirectoryHierarchy io = event.getServices().getControlerIO(); + + String output = io.getOutputFilename("person_util_variations.csv"); + + // Write scoring information for each person + try (CSVPrinter csv = new CSVPrinter(IOUtils.getBufferedWriter(output), CSVFormat.DEFAULT)) { + + csv.print("person"); + csv.printRecord(params.header); + + for (Person person : event.getServices().getScenario().getPopulation().getPersons().values()) { + + Object2DoubleMap values = params.info.get(person.getId()); + if (values == null) { + continue; + } + + csv.print(person.getId()); + for (String s : params.header) { + csv.print(values.getDouble(s)); + } + csv.println(); + } + + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + params.header.clear(); + params.info.clear(); + + outputWritten = true; + } +} diff --git a/src/main/java/org/matsim/run/scoring/DistanceGroup.java b/src/main/java/org/matsim/run/scoring/DistanceGroup.java new file mode 100644 index 0000000..5a52f74 --- /dev/null +++ b/src/main/java/org/matsim/run/scoring/DistanceGroup.java @@ -0,0 +1,17 @@ +package org.matsim.run.scoring; + +/** + * COPIED FROM BERLIN - DO NOT DEVELOP HERE. WILL BE DELETED AS SOON AS THIS FUNCTIONALITY IS MOVED THE MATSIM CORE. + * + * Store distance group. + * + * @param dist lower bound for distance group + * @param constant added constant + * @param utilPerM utility per meter, i.e. slope of linear function + */ +record DistanceGroup(double dist, double constant, double utilPerM) implements Comparable { + @Override + public int compareTo(DistanceGroup o) { + return Double.compare(dist, o.dist); + } +} diff --git a/src/main/java/org/matsim/run/scoring/DistanceGroupModeUtilityParameters.java b/src/main/java/org/matsim/run/scoring/DistanceGroupModeUtilityParameters.java new file mode 100644 index 0000000..f533f32 --- /dev/null +++ b/src/main/java/org/matsim/run/scoring/DistanceGroupModeUtilityParameters.java @@ -0,0 +1,79 @@ +package org.matsim.run.scoring; + +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import org.matsim.core.scoring.functions.ModeUtilityParameters; + +/** + * COPIED FROM BERLIN - DO NOT DEVELOP HERE. WILL BE DELETED AS SOON AS THIS FUNCTIONALITY IS MOVED THE MATSIM CORE. + * + * Mode utility with separate marginalUtilityOfDistance_m per distance group. + */ +public class DistanceGroupModeUtilityParameters extends ModeUtilityParameters { + + private final DistanceGroup[] groups; + + /** + * Constructor which copies the base params from given modeParams. + */ + DistanceGroupModeUtilityParameters(ModeUtilityParameters modeParams, DeltaBuilder delta, DistanceGroup[] groups) { + super(modeParams.marginalUtilityOfTraveling_s, + modeParams.marginalUtilityOfDistance_m, + modeParams.monetaryDistanceCostRate, + modeParams.constant + delta.constant, + modeParams.dailyMoneyConstant, + modeParams.dailyUtilityConstant + delta.dailyUtilityConstant); + + this.groups = groups; + } + + /** + * Calculate the utility for given distance. + */ + public double calcUtilityDistDelta(double dist) { + + if (groups == null) + return 0; + + DistanceGroup group = groups[0]; + for (int i = 1; i < groups.length; i++) { + if (groups[i].dist() > dist) + break; + + group = groups[i]; + } + + return group.constant() + group.utilPerM() * (dist - group.dist()); + } + + static final class DeltaBuilder { + + private final DoubleList perDistGroup = new DoubleArrayList(); + double constant; + double dailyUtilityConstant; + + public DoubleList getPerDistGroup() { + return perDistGroup; + } + + /** + * Sum delta of distance utilities. + */ + public void addUtilsDistance(AdvancedScoringConfigGroup.ModeParams params) { + if (params.deltaPerDistGroup != null && !params.deltaPerDistGroup.isEmpty()) { + if (perDistGroup.isEmpty()) { + perDistGroup.addAll(params.deltaPerDistGroup); + return; + } + + if (perDistGroup.size() != params.deltaPerDistGroup.size()) { + throw new IllegalArgumentException("Distance utility parameters do not match"); + } + + for (int i = 0; i < perDistGroup.size(); i++) { + perDistGroup.set(i, perDistGroup.getDouble(i) + params.deltaPerDistGroup.get(i)); + } + } + } + } +} diff --git a/src/main/java/org/matsim/run/scoring/IndividualPersonScoringParameters.java b/src/main/java/org/matsim/run/scoring/IndividualPersonScoringParameters.java new file mode 100644 index 0000000..de326d2 --- /dev/null +++ b/src/main/java/org/matsim/run/scoring/IndividualPersonScoringParameters.java @@ -0,0 +1,368 @@ +package org.matsim.run.scoring; + +import com.google.common.base.Joiner; +import com.google.common.primitives.Longs; +import com.google.inject.Inject; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import it.unimi.dsi.fastutil.objects.Object2DoubleMap; +import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.rng.RestorableUniformRandomProvider; +import org.apache.commons.rng.core.RandomProviderDefaultState; +import org.apache.commons.rng.simple.RandomSource; +import org.apache.commons.statistics.distribution.ContinuousDistribution; +import org.apache.commons.statistics.distribution.NormalDistribution; +import org.apache.commons.statistics.distribution.TruncatedNormalDistribution; +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.IdMap; +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Population; +import org.matsim.application.analysis.population.Category; +import org.matsim.application.analysis.population.TripAnalysis; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.config.groups.ScoringConfigGroup; +import org.matsim.core.population.PersonUtils; +import org.matsim.core.population.PopulationUtils; +import org.matsim.core.scoring.functions.ActivityUtilityParameters; +import org.matsim.core.scoring.functions.ModeUtilityParameters; +import org.matsim.core.scoring.functions.ScoringParameters; +import org.matsim.core.scoring.functions.ScoringParametersForPerson; +import org.matsim.pt.PtConstants; +import org.matsim.pt.config.TransitConfigGroup; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * COPIED FROM BERLIN - DO NOT DEVELOP HERE. WILL BE DELETED AS SOON AS THIS FUNCTIONALITY IS MOVED THE MATSIM CORE. + * + * Scoring parameters for {@link AdvancedScoringConfigGroup}. This makes use of the standard scoring and adds persons specific differences. + */ +public class IndividualPersonScoringParameters implements ScoringParametersForPerson { + + private static final Logger log = LogManager.getLogger(IndividualPersonScoringParameters.class); + /** + * Header for info file. + */ + final Set header = new LinkedHashSet<>(); + /** + * This map contains debug information to be written out after first iteration. + */ + final Map, Object2DoubleMap> info = new ConcurrentHashMap<>(); + /** + * Cache instances of {@link ActivityUtilityParameters} for each subpopulation. + */ + private final Map> actUtils = new ConcurrentHashMap<>(); + /** + * Cache instances of {@link ScoringParameters} for each person. + */ + private final IdMap cache; + /** + * Cache and reuse distance group arrays. + */ + private final Map distGroups = new ConcurrentHashMap<>(); + + /** + * Categories from config group. + */ + private final Map categories; + + /** + * Thread-local random number generator. + */ + private final ThreadLocal rnd; + private final Scenario scenario; + private final ScoringConfigGroup basicScoring; + private final TransitConfigGroup transitConfig; + private final AdvancedScoringConfigGroup scoring; + /** + * Average income of all agents with this attribute. Can be NaN if not used. + */ + private final double globalAvgIncome; + + @Inject + public IndividualPersonScoringParameters(Scenario scenario) { + this.scenario = scenario; + this.basicScoring = scenario.getConfig().scoring(); + this.scoring = ConfigUtils.addOrGetModule(scenario.getConfig(), AdvancedScoringConfigGroup.class); + this.transitConfig = scenario.getConfig().transit(); + this.globalAvgIncome = computeAvgIncome(scenario.getPopulation()); + this.categories = Category.fromConfigParams(this.scoring.getScoringParameters()); + this.cache = new IdMap<>(Person.class, scenario.getPopulation().getPersons().size()); + this.rnd = ThreadLocal.withInitial(() -> new Context(scenario.getConfig().global().getRandomSeed())); + } + + static DistanceGroup[] calcDistanceGroups(List dists, DoubleList distUtils) { + + // Nothing to do if no distance groups are defined. + if (dists == null || dists.isEmpty() || distUtils.isEmpty()) { + return null; + } + + List copy = new ArrayList<>(dists); + + if (copy.get(0) != 0) + copy.add(0, 0); + + // Effectively no distance groups present + if (copy.size() <= 1) { + return null; + } + + // No delta for distance groups + if (distUtils.doubleStream().allMatch(d -> d == 0)) + return null; + + DistanceGroup[] groups = new DistanceGroup[copy.size()]; + + if (groups.length - 1 != distUtils.size()) { + log.error("Distance groups: {}, utils: {}", dists, distUtils); + throw new IllegalArgumentException("Distance groups and distance utilities must have the same size."); + } + + + for (int i = 0; i < copy.size() - 1; i++) { + + double dist = copy.get(i); + double nextDist = copy.get(i + 1); + + double constant = i == 0 ? 0 : distUtils.getDouble(i - 1); + double next = distUtils.getDouble(i); + + + groups[i] = new DistanceGroup(dist, constant, (next - constant) / (nextDist - dist)); + } + + // Last open ended dist group + int lastDist = copy.get(copy.size() - 1); + double lastUtil = distUtils.getDouble(distUtils.size() - 1); + groups[copy.size() - 1] = new DistanceGroup(lastDist, lastUtil, lastUtil / lastDist); + + return groups; + } + + private double computeAvgIncome(Population population) { + if (scoring.incomeDependent == AdvancedScoringConfigGroup.IncomeDependentScoring.none) + return Double.NaN; + + log.info("reading income attribute using " + PersonUtils.class + " of all agents and compute global average.\n" + + "Make sure to set this attribute only to appropriate agents (i.e. true 'persons' and not freight agents) \n" + + "Income values <= 0 are ignored. Agents that have negative or 0 income will use the marginalUtilityOfMoney in their subpopulation's scoring params.."); + OptionalDouble averageIncome = population.getPersons().values().stream() + //consider only agents that have a specific income provided + .filter(person -> PersonUtils.getIncome(person) != null) + .mapToDouble(PersonUtils::getIncome) + .filter(dd -> dd > 0) + .average(); + + if (averageIncome.isEmpty()) { + throw new RuntimeException("you have enabled income dependent scoring but there is not a single income attribute in the population! " + + "If you are not aiming for person-specific marginalUtilityOfMoney, better use other PersonScoringParams, e.g. SubpopulationPersonScoringParams, which have higher performance." + + "Otherwise, please provide income attributes in the population..."); + } else { + log.info("global average income is " + averageIncome); + return averageIncome.getAsDouble(); + } + } + + @Override + public ScoringParameters getScoringParameters(Person person) { + + return this.cache.computeIfAbsent(person.getId(), id -> { + + String subpopulation = PopulationUtils.getSubpopulation(person); + ScoringConfigGroup.ScoringParameterSet scoringParameters = basicScoring.getScoringParameters(subpopulation); + + // Activity params can be reused per subpopulation + Map activityParams = actUtils.computeIfAbsent(subpopulation, k -> { + Map ap = new TreeMap<>(); + for (ScoringConfigGroup.ActivityParams params : scoringParameters.getActivityParams()) { + ActivityUtilityParameters.Builder factory = new ActivityUtilityParameters.Builder(params); + ap.put(params.getActivityType(), factory.build()); + } + + // The code to add this activity type is always copied between different scoring implementations + // it might not be actually needed anymore (because default staging activities are also added elsewhere) + // but it's not clear if it's safe to remove it. + if (transitConfig.isUseTransit()) { + ScoringConfigGroup.ActivityParams transitActivityParams = new ScoringConfigGroup.ActivityParams(PtConstants.TRANSIT_ACTIVITY_TYPE); + transitActivityParams.setTypicalDuration(120.0); + transitActivityParams.setOpeningTime(0.); + transitActivityParams.setClosingTime(0.); + ActivityUtilityParameters.Builder modeParamsBuilder = new ActivityUtilityParameters.Builder(transitActivityParams); + modeParamsBuilder.setScoreAtAll(false); + ap.put(PtConstants.TRANSIT_ACTIVITY_TYPE, modeParamsBuilder.build()); + } + + return ap; + }); + + ScoringParameters.Builder builder = new ScoringParameters.Builder(basicScoring, + scoringParameters, activityParams, scenario.getConfig().scenario()); + + Double personalIncome = PersonUtils.getIncome(person); + // Income dependent scoring might be disabled + if (!Double.isNaN(globalAvgIncome) && personalIncome != null) { + if (personalIncome != 0) { + builder.setMarginalUtilityOfMoney(scoringParameters.getMarginalUtilityOfMoney() * + Math.pow(globalAvgIncome / personalIncome, this.scoring.incomeExponent)); + } else { + log.warn("You have set income to {} for person {}. This is invalid and gets ignored.Instead, the marginalUtilityOfMoney is derived from the subpopulation's scoring parameters.", personalIncome, person); + } + } + + Map deltaParams = new HashMap<>(); + + this.rnd.get().setSeed(person); + + for (AdvancedScoringConfigGroup.ScoringParameters parameter : scoring.getScoringParameters()) { + + if (Category.matchAttributesWithConfig(person.getAttributes(), parameter, categories)) { + for (Map.Entry mode : parameter.getModeParams().entrySet()) { + + DistanceGroupModeUtilityParameters.DeltaBuilder b = + deltaParams.computeIfAbsent(mode.getKey(), k -> new DistanceGroupModeUtilityParameters.DeltaBuilder()); + + b.addUtilsDistance(mode.getValue()); + addDeltaParams(this.rnd.get(), b, mode.getValue()); + } + } + } + + Object attr = person.getAttributes().getAttribute("utilDelta"); + Object2DoubleMap existing = new Object2DoubleOpenHashMap<>(); + if (attr instanceof String s) { + String[] split = s.split("\\|"); + for (String s1 : split) { + String[] split1 = s1.split("="); + existing.put(split1[0], Double.parseDouble(split1[1])); + } + } + + for (Map.Entry mode : deltaParams.entrySet()) { + ModeUtilityParameters params = builder.getModeParameters(mode.getKey()); + DistanceGroupModeUtilityParameters.DeltaBuilder delta = mode.getValue(); + + // These arrays are re-used if possible + DistanceGroup[] groups = distGroups.computeIfAbsent(delta.getPerDistGroup(), k -> calcDistanceGroups(scoring.distGroups, k)); + + // This may overwrite the preferences with the one stored + loadPreferences(mode.getKey(), delta, person, existing); + + DistanceGroupModeUtilityParameters p = new DistanceGroupModeUtilityParameters(params, delta, groups); + builder.setModeParameters(mode.getKey(), p); + + // Collect final adjustments information + Object2DoubleMap values = info.computeIfAbsent(person.getId(), k -> new Object2DoubleOpenHashMap<>()); + + // Write the overall constants, but only if they are different to the base values + if (delta.constant != 0) { + values.put(mode.getKey() + "_constant", p.constant); + existing.put(mode.getKey() + "_constant", delta.constant); + } + + if (delta.dailyUtilityConstant != 0) { + values.put(mode.getKey() + "_dailyConstant", p.dailyUtilityConstant); + existing.put(mode.getKey() + "_dailyConstant", delta.dailyUtilityConstant); + } + + if (groups != null) { + for (DistanceGroup group : groups) { + values.put("%s_dist_%.0f".formatted(mode.getKey(), group.dist()), group.utilPerM()); + } + } + + header.addAll(values.keySet()); + } + + if (!existing.isEmpty()) { + Joiner.MapJoiner mapJoiner = Joiner.on("|").withKeyValueSeparator("="); + person.getAttributes().putAttribute("utilDelta", mapJoiner.join(existing)); + } + + return builder.build(); + }); + } + + private void loadPreferences(String mode, DistanceGroupModeUtilityParameters.DeltaBuilder delta, Person person, Object2DoubleMap existing) { + + boolean isRefPerson = person.getAttributes().getAttribute(TripAnalysis.ATTR_REF_ID) != null; + + if (scoring.loadPreferences == AdvancedScoringConfigGroup.LoadPreferences.none || + (isRefPerson && scoring.loadPreferences == AdvancedScoringConfigGroup.LoadPreferences.skipRefPersons)) { + return; + } + + // Else, require that the attributes are present + if (!existing.containsKey(mode + "_constant") && scoring.loadPreferences == AdvancedScoringConfigGroup.LoadPreferences.requireAttribute) { + throw new IllegalArgumentException("Person " + person.getId() + " does not have attribute " + mode + "_constant"); + } + if (!existing.containsKey(mode + "_dailyConstant") && scoring.loadPreferences == AdvancedScoringConfigGroup.LoadPreferences.requireAttribute) { + throw new IllegalArgumentException("Person " + person.getId() + " does not have attribute " + mode + "_dailyConstant"); + } + + // Use attributes if they are present + if (existing.containsKey(mode + "_constant")) + delta.constant = existing.getDouble(mode + "_constant") ; + + if (existing.containsKey(mode + "_dailyConstant")) + delta.dailyUtilityConstant = existing.getDouble(mode + "_dailyConstant"); + } + + /** + * Compute or retrieve delta params for person. + */ + private void addDeltaParams(Context ctx, DistanceGroupModeUtilityParameters.DeltaBuilder delta, AdvancedScoringConfigGroup.ModeParams params) { + + ContinuousDistribution.Sampler normal = ctx.normal.createSampler(ctx.rnd()); + ContinuousDistribution.Sampler tn = ctx.tn.createSampler(ctx.rnd()); + + switch (params.varConstant) { + case fixed -> delta.constant += params.deltaConstant; + case normal -> delta.constant += normal.sample() * params.deltaConstant; + case truncatedNormal -> delta.constant += tn.sample() * params.deltaConstant; + default -> throw new IllegalArgumentException("Unsupported varConstant: " + params.varConstant); + } + + switch (params.varDailyConstant) { + case fixed -> delta.dailyUtilityConstant += params.deltaDailyConstant; + case normal -> delta.dailyUtilityConstant += normal.sample() * params.deltaDailyConstant; + case truncatedNormal -> delta.dailyUtilityConstant += tn.sample() * params.deltaDailyConstant; + default -> throw new IllegalArgumentException("Unsupported varDailyConstant: " + params.varDailyConstant); + } + } + + /** + * Thread-local context for random number generation. This makes generation thread-safe and consistent independently of threads and order of persons. + */ + private record Context(NormalDistribution normal, TruncatedNormalDistribution tn, byte[] seed, RestorableUniformRandomProvider rnd) { + + Context(long seed) { + this(NormalDistribution.of(0, 1), + TruncatedNormalDistribution.of(0, 1, 0, Double.POSITIVE_INFINITY), + // Feed seed into random number generator + Longs.toByteArray(new SplittableRandom(seed).nextLong()), + RandomSource.KISS.create()); + } + + /** + * Set the state of rnd specific to person and configured global seed. + */ + void setSeed(Person p) { + + byte[] state = new byte[20]; + byte[] person = p.getId().toString().getBytes(); + + // Reverse, because the more significant bytes are at the end + ArrayUtils.reverse(person); + + System.arraycopy(seed, 0, state, 0, 8); + System.arraycopy(person, 0, state, 8, Math.min(person.length, 12)); + + rnd.restoreState(new RandomProviderDefaultState(state)); + } + } +} diff --git a/src/main/java/org/matsim/run/scoring/PiecewiseLinearlLegScoring.java b/src/main/java/org/matsim/run/scoring/PiecewiseLinearlLegScoring.java new file mode 100644 index 0000000..c7616d3 --- /dev/null +++ b/src/main/java/org/matsim/run/scoring/PiecewiseLinearlLegScoring.java @@ -0,0 +1,225 @@ +/* *********************************************************************** * + * project: org.matsim.* + * CharyparNagelOpenTimesScoringFunctionFactory.java + * * + * *********************************************************************** * + * * + * copyright : (C) 2007 by the members listed in the COPYING, * + * LICENSE and WARRANTY file. * + * email : info at matsim dot org * + * * + * *********************************************************************** * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * See also COPYING, LICENSE and WARRANTY file * + * * + * *********************************************************************** */ + +package org.matsim.run.scoring; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.matsim.api.core.v01.TransportMode; +import org.matsim.api.core.v01.events.ActivityEndEvent; +import org.matsim.api.core.v01.events.Event; +import org.matsim.api.core.v01.events.PersonDepartureEvent; +import org.matsim.api.core.v01.events.PersonEntersVehicleEvent; +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.population.Leg; +import org.matsim.api.core.v01.population.Route; +import org.matsim.core.gbl.Gbl; +import org.matsim.core.scoring.functions.ModeUtilityParameters; +import org.matsim.core.scoring.functions.ScoringParameters; +import org.matsim.pt.PtConstants; + +import java.util.HashSet; +import java.util.Set; + +/** + * COPIED FROM BERLIN - DO NOT DEVELOP HERE. WILL BE DELETED AS SOON AS THIS FUNCTIONALITY IS MOVED THE MATSIM CORE. + * + * This is a copy of {@link org.matsim.core.scoring.functions.CharyparNagelLegScoring}. + * Distance utilities are scored with different linear functions per distance group. + */ +@SuppressWarnings("checkstyle") +public final class PiecewiseLinearlLegScoring implements org.matsim.core.scoring.SumScoringFunction.LegScoring, org.matsim.core.scoring.SumScoringFunction.ArbitraryEventScoring { + // yyyy URL in above javadoc is broken. kai, feb'17 + + private static final Logger log = LogManager.getLogger(PiecewiseLinearlLegScoring.class); + private static int ccc = 0; + /** + * The parameters used for scoring. + */ + private final ScoringParameters params; + private final Set ptModes; + private final double marginalUtilityOfMoney; + private final Set modesAlreadyConsideredForDailyConstants; + private double score; + private Network network; + private boolean nextEnterVehicleIsFirstOfTrip = true; + private boolean nextStartPtLegIsFirstOfTrip = true; + private boolean currentLegIsPtLeg = false; + private double lastActivityEndTime = Double.NaN; + + public PiecewiseLinearlLegScoring(final ScoringParameters params, Network network, Set ptModes) { + this.params = params; + this.network = network; + this.ptModes = ptModes; + this.modesAlreadyConsideredForDailyConstants = new HashSet<>(); + this.marginalUtilityOfMoney = this.params.marginalUtilityOfMoney; + } + + @Override + public void finish() { + + } + + @Override + public double getScore() { + return this.score; + } + + /** + * Calculate the score for a leg. + */ + private double calcLegScore(final double departureTime, final double arrivalTime, final Leg leg) { + double tmpScore = 0.0; + // travel time in seconds + double travelTime = arrivalTime - departureTime; + ModeUtilityParameters modeParams = this.params.modeParams.get(leg.getMode()); + + if (modeParams == null) { + if (leg.getMode().equals(TransportMode.transit_walk) || leg.getMode().equals(TransportMode.non_network_walk)) { + modeParams = this.params.modeParams.get(TransportMode.walk); + } else { +// modeParams = this.params.modeParams.get(TransportMode.other); + throw new RuntimeException("just encountered mode for which no scoring parameters are defined: " + leg.getMode()); + } + } + + tmpScore += travelTime * modeParams.marginalUtilityOfTraveling_s; + + if (modeParams instanceof DistanceGroupModeUtilityParameters distParams) { + + if (modeParams.monetaryDistanceCostRate != 0.0) { + Route route = leg.getRoute(); + // distance in meters + double dist = route.getDistance(); + if (Double.isNaN(dist)) { + if (ccc < 10) { + ccc++; + LogManager.getLogger(this.getClass()).warn("distance is NaN. Will make score of this plan NaN. Possible reason: Simulation does not report " + + "a distance for this trip. Possible reason for that: mode is teleported and router does not " + + "write distance into plan. Needs to be fixed or these plans will die out."); + if (ccc == 10) { + LogManager.getLogger(this.getClass()).warn(Gbl.FUTURE_SUPPRESSED); + } + } + } + tmpScore += modeParams.monetaryDistanceCostRate * this.marginalUtilityOfMoney * dist; + } + + Route route = leg.getRoute(); + double dist = route.getDistance(); + + // Apply the default distance scoring parameter. + if (modeParams.marginalUtilityOfDistance_m != 0) { + tmpScore += modeParams.marginalUtilityOfDistance_m * dist; + } + + tmpScore += distParams.calcUtilityDistDelta(dist); + + } else { + + // standard leg scoring + if (modeParams.marginalUtilityOfDistance_m != 0.0 + || modeParams.monetaryDistanceCostRate != 0.0) { + Route route = leg.getRoute(); + // distance in meters + double dist = route.getDistance(); + if (Double.isNaN(dist)) { + if (ccc < 10) { + ccc++; + LogManager.getLogger(this.getClass()).warn("distance is NaN. Will make score of this plan NaN. Possible reason: Simulation does not report " + + "a distance for this trip. Possible reason for that: mode is teleported and router does not " + + "write distance into plan. Needs to be fixed or these plans will die out."); + if (ccc == 10) { + LogManager.getLogger(this.getClass()).warn(Gbl.FUTURE_SUPPRESSED); + } + } + } + tmpScore += modeParams.marginalUtilityOfDistance_m * dist; + tmpScore += modeParams.monetaryDistanceCostRate * this.marginalUtilityOfMoney * dist; + } + } + + tmpScore += modeParams.constant; + // (yyyy once we have multiple legs without "real" activities in between, this will produce wrong results. kai, dec'12) + // (yy NOTE: the constant is added for _every_ pt leg. This is not how such models are estimated. kai, nov'12) + + // account for the daily constants + if (!modesAlreadyConsideredForDailyConstants.contains(leg.getMode())) { + tmpScore += modeParams.dailyUtilityConstant + modeParams.dailyMoneyConstant * this.marginalUtilityOfMoney; + modesAlreadyConsideredForDailyConstants.add(leg.getMode()); + } + // yyyy the above will cause problems if we ever decide to differentiate pt mode into bus, tram, train, ... + // Might have to move the MainModeIdentifier then. kai, sep'18 + + return tmpScore; + } + + @Override + public void handleEvent(Event event) { + if (event instanceof ActivityEndEvent) { + // When there is a "real" activity, flags are reset: + if (!PtConstants.TRANSIT_ACTIVITY_TYPE.equals(((ActivityEndEvent) event).getActType())) { + this.nextEnterVehicleIsFirstOfTrip = true; + this.nextStartPtLegIsFirstOfTrip = true; + } + this.lastActivityEndTime = event.getTime(); + } + + if (event instanceof PersonEntersVehicleEvent && currentLegIsPtLeg) { + if (!this.nextEnterVehicleIsFirstOfTrip) { + // all vehicle entering after the first triggers the disutility of line switch: + this.score += params.utilityOfLineSwitch; + } + this.nextEnterVehicleIsFirstOfTrip = false; + // add score of waiting, _minus_ score of travelling (since it is added in the legscoring above): + this.score += (event.getTime() - this.lastActivityEndTime) * (this.params.marginalUtilityOfWaitingPt_s - this.params.modeParams.get(TransportMode.pt).marginalUtilityOfTraveling_s); + } + + if (event instanceof PersonDepartureEvent) { + String mode = ((PersonDepartureEvent) event).getLegMode(); + + this.currentLegIsPtLeg = this.ptModes.contains(mode); + if (currentLegIsPtLeg) { + if (!this.nextStartPtLegIsFirstOfTrip) { + this.score -= params.modeParams.get(mode).constant; + // (yyyy deducting this again, since is it wrongly added above. should be consolidated; this is so the code + // modification is minimally invasive. kai, dec'12) + } + this.nextStartPtLegIsFirstOfTrip = false; + } + } + } + + @Override + public void handleLeg(Leg leg) { + Gbl.assertIf(leg.getDepartureTime().isDefined()); + Gbl.assertIf(leg.getTravelTime().isDefined()); + + double legScore = calcLegScore( + leg.getDepartureTime().seconds(), leg.getDepartureTime().seconds() + leg.getTravelTime() + .seconds(), leg); + if (Double.isNaN(legScore)) { + log.error("dpTime=" + leg.getDepartureTime().seconds() + + "; ttime=" + leg.getTravelTime().seconds() + "; leg=" + leg); + throw new RuntimeException("score is NaN"); + } + this.score += legScore; + } +} diff --git a/src/main/java/org/matsim/run/scoring/readme.txt b/src/main/java/org/matsim/run/scoring/readme.txt new file mode 100644 index 0000000..f59417d --- /dev/null +++ b/src/main/java/org/matsim/run/scoring/readme.txt @@ -0,0 +1 @@ +This package is copied from berlin scenario because the code is not yet in the matsim libs. However, if you want to touch the code, please do it in the berlin repository. \ No newline at end of file