MATSIM
ChooseRandomLegModeForSubtour.java
Go to the documentation of this file.
1 /* *********************************************************************** *
2  * project: org.matsim.*
3  * ChangeLegMode.java
4  * *
5  * *********************************************************************** *
6  * *
7  * copyright : (C) 2008 by the members listed in the COPYING, *
8  * LICENSE and WARRANTY file. *
9  * email : info at matsim dot org *
10  * *
11  * *********************************************************************** *
12  * *
13  * This program is free software; you can redistribute it and/or modify *
14  * it under the terms of the GNU General Public License as published by *
15  * the Free Software Foundation; either version 2 of the License, or *
16  * (at your option) any later version. *
17  * See also COPYING, LICENSE and WARRANTY file *
18  * *
19  * *********************************************************************** */
20 
21 package org.matsim.core.population.algorithms;
22 
23 import java.util.*;
24 import java.util.stream.Collectors;
25 
26 import org.apache.logging.log4j.LogManager;
27 import org.apache.logging.log4j.Logger;
29 import org.matsim.api.core.v01.Id;
40 
49 public final class ChooseRandomLegModeForSubtour implements PlanAlgorithm {
50 
51  private static final Logger logger = LogManager.getLogger(ChooseRandomLegModeForSubtour.class);
52 
56  public static class Candidate {
57  private final List<Trip> plan;
58  private final Subtour subtour;
59  private final String newTransportMode;
60 
61  public Candidate(List<Trip> plan, final Subtour subtour, final String newTransportMode) {
62  this.plan = plan;
63  this.subtour = subtour;
64  this.newTransportMode = newTransportMode;
65  }
66 
67  @Override
68  public boolean equals(Object o) {
69  if (this == o) return true;
70  if (o == null || getClass() != o.getClass()) return false;
71  Candidate candidate = (Candidate) o;
72  return subtour.equals(candidate.subtour) && newTransportMode.equals(candidate.newTransportMode);
73  }
74 
75  @Override
76  public int hashCode() {
77  return Objects.hash(subtour, newTransportMode);
78  }
79 
80  @Override
81  public String toString() {
82  String trips = subtour.getTrips().stream().map(plan::indexOf).map(String::valueOf).collect(Collectors.joining(","));
83  return "Candidate[Trips: " + trips + " -> " + newTransportMode + "}";
84  }
85 
86  public Subtour getSubtour() {
87  return subtour;
88  }
89 
90  public String getNewTransportMode() {
91  return newTransportMode;
92  }
93  }
94 
95  private Collection<String> modes;
96  private final Collection<String> chainBasedModes;
98  private Collection<String> singleTripSubtourModes;
99 
101 
102  private final Random rng;
103 
105 
106  private final double probaForChangeSingleTripMode;
109 
110  private double coordDist = 0;
111 
113  final MainModeIdentifier mainModeIdentifier,
114  final PermissibleModesCalculator permissibleModesCalculator,
115  final String[] modes,
116  final String[] chainBasedModes,
117  final Random rng, SubtourModeChoice.Behavior behavior, double probaForChooseRandomSingleTripMode) {
118  this(mainModeIdentifier, permissibleModesCalculator, modes, chainBasedModes, rng, behavior, probaForChooseRandomSingleTripMode, 0);
119  }
120 
122  final MainModeIdentifier mainModeIdentifier,
123  final PermissibleModesCalculator permissibleModesCalculator,
124  final String[] modes,
125  final String[] chainBasedModes,
126  final Random rng, SubtourModeChoice.Behavior behavior, double probaForChooseRandomSingleTripMode, double coordDist) {
127  this.mainModeIdentifier = mainModeIdentifier;
128  this.permissibleModesCalculator = permissibleModesCalculator;
129  this.modes = Arrays.asList(modes);
130  this.chainBasedModes = Arrays.asList(chainBasedModes);
131  this.behavior = behavior;
132  this.probaForChangeSingleTripMode = probaForChooseRandomSingleTripMode;
133  this.singleTripSubtourModes = this.chainBasedModes;
134  this.coordDist = coordDist;
135 
136  this.rng = rng;
137  logger.info("Chain based modes: " + this.chainBasedModes.toString());
138 
139  // also set up the standard change single leg mode, in order to alternatively randomize modes in
140  // subtours. kai, may'18
141  if (this.probaForChangeSingleTripMode > 0.) {
142  Collection<String> notChainBasedModes = new ArrayList<>(this.modes);
143  notChainBasedModes.removeAll(this.chainBasedModes);
144  final String[] possibleModes = notChainBasedModes.toArray(new String[]{});
145  if (possibleModes.length >= 2) {
146  // nothing to choose if there is only one mode! I also don't think that we can base this on size, since
147  // mode strings might be registered multiple times (these are not sets). kai, may'18
148 
149  this.tripsToLegs = new TripsToLegsAlgorithm(this.mainModeIdentifier);
150 
151  // change single leg would not respect car availability, if car is not a chain based mode -- cr, aug'22
152  this.changeSingleLegMode = new ChooseRandomSingleLegMode(possibleModes, rng, true);
153  }
154  }
155  }
156 
166  public void setSingleTripSubtourModes(final String[] singleTripSubtourModes) {
167  this.singleTripSubtourModes = Arrays.asList(singleTripSubtourModes);
168  }
169 
173  public List<Candidate> determineChoiceSet(final Plan plan) {
174 
175  final Id<? extends BasicLocation> homeLocation = ((Activity) plan.getPlanElements().get(0)).getFacilityId() != null ?
176  ((Activity) plan.getPlanElements().get(0)).getFacilityId() :
177  ((Activity) plan.getPlanElements().get(0)).getLinkId();
178  Collection<String> permissibleModesForThisPlan = permissibleModesCalculator.getPermissibleModes(plan);
179 
180  return determineChoiceSet(
181  homeLocation,
182  plan,
184  permissibleModesForThisPlan);
185  }
186 
190  public boolean isMassConserving(final Subtour subtour) {
191 
192  for (String mode : chainBasedModes) {
193  if (!isMassConserving(subtour, mode)) {
194  return false;
195  }
196  }
197  return true;
198  }
199 
200  @Override
201  public void run(final Plan plan) {
202  if (plan.getPlanElements().size() <= 1) {
203  return;
204  }
205 
206  // trips are used multiple times
207  List<Trip> trips = TripStructureUtils.getTrips(plan);
208 
209  // with certain proba, do standard single leg mode choice for not-chain-based modes:
210  // if a plan consist of only chain based modes, change single trip won't be able to do anything, in this case it will be ignored
211  if (this.changeSingleLegMode != null && rng.nextDouble() < this.probaForChangeSingleTripMode && hasSingleTripChoice(trips)) {
212  // (the null check is for computational efficiency)
213 
214  this.tripsToLegs.run(plan);
215  this.changeSingleLegMode.run(plan);
216  return;
217  }
218 
219  final Id<? extends BasicLocation> homeLocation = ((Activity) plan.getPlanElements().get(0)).getFacilityId() != null ?
220  ((Activity) plan.getPlanElements().get(0)).getFacilityId() :
221  ((Activity) plan.getPlanElements().get(0)).getLinkId();
222 
223  Collection<String> permissibleModesForThisPlan = permissibleModesCalculator.getPermissibleModes(plan);
224  // (modes that agent can in principle use; e.g. cannot use car sharing if not member)
225 
226 
227  List<Candidate> choiceSet = determineChoiceSet(homeLocation, plan, trips, permissibleModesForThisPlan);
228 
229  if (!choiceSet.isEmpty()) {
230  Candidate whatToDo = choiceSet.get(rng.nextInt(choiceSet.size()));
231  // (means that in the end we are changing modes only for one subtour)
232 
233  applyChange(whatToDo, plan);
234  }
235  }
236 
240  private boolean hasSingleTripChoice(List<Trip> trips) {
241  return !trips.stream().map(
242  t -> mainModeIdentifier.identifyMainMode(t.getTripElements())
243  ).allMatch(chainBasedModes::contains);
244  }
245 
246  private List<Candidate> determineChoiceSet(
247  final Id<? extends BasicLocation> homeLocation,
248  final Plan plan,
249  final List<Trip> trips,
250  final Collection<String> permissibleModesForThisPerson) {
251 
252  final List<Subtour> subtours = new ArrayList<>(TripStructureUtils.getSubtours(plan, coordDist));
253  final ArrayList<Candidate> choiceSet = new ArrayList<>();
254 
255  // there is no subtour containing all trips, so it will be added
256  if (behavior == SubtourModeChoice.Behavior.betweenAllAndFewerConstraints && trips.size() > 0 && subtours.stream().noneMatch(s -> s.getTrips().equals(trips))) {
257  subtours.add(0, TripStructureUtils.getUnclosedRootSubtour(plan));
258  }
259 
260  for (Subtour subtour : subtours) {
261 
262  // If subtour is not closed, it will still be considered if constrains are disabled
264  continue;
265  }
266 
268  continue;
269  }
270 
271  final Id<? extends BasicLocation> subtourStartLocation =
272 // anchorAtFacilities ?
273  subtour.getTrips().get(0).getOriginActivity().getFacilityId() != null ?
274  subtour.getTrips().get(0).getOriginActivity().getFacilityId() :
275  subtour.getTrips().get(0).getOriginActivity().getLinkId();
276 
277  final Collection<String> testingModes =
278  subtour.getTrips().size() == 1 ?
281  // I am not sure what the singleTripSubtourModes thing means. But apart from that ...
282 
283  // ... test whether a vehicle was brought to the subtourStartLocation:
284  final Set<String> usableChainBasedModes = new LinkedHashSet<>();
285  for (String mode : testingModes) {
286  Id<? extends BasicLocation> vehicleLocation = homeLocation;
287  Activity lastDestination =
289  trips.subList(
290  0,
291  trips.indexOf(subtour.getTrips().get(0))),
292  mode);
293  if (lastDestination != null) {
294  vehicleLocation = getLocationId(lastDestination);
295  }
296  if (vehicleLocation.equals(subtourStartLocation)) {
297  usableChainBasedModes.add(mode);
298  // can have more than one mode here when subtour starts at home.
299  }
300  }
301 
302  // yy My intuition is that with the above condition, a switch of an all-car plan with at least
303  // one explicit sub-tour could switch to an all-bicycle plan only via first changing the
304  // explicit sub-tour first to a non-chain-based mode. kai, jul'18
305 
306  // The top-level subtour may use all chain-based modes freely for the whole plan
307 
308  Set<String> usableModes = new LinkedHashSet<>();
309  if (isMassConserving(subtour) || (behavior == SubtourModeChoice.Behavior.betweenAllAndFewerConstraints && subtour.getParent() == null)) { // We can only replace a subtour if it doesn't itself move a vehicle from one place to another
310  for (String candidate : permissibleModesForThisPerson) {
311  if (chainBasedModes.contains(candidate)) {
312  // for chain-based modes, only add if vehicle is available:
313  if (usableChainBasedModes.contains(candidate)) {
314  usableModes.add(candidate);
315  }
316  } else {
317  // for non-chain-based modes, always add:
318  usableModes.add(candidate);
319  }
320  }
321  }
322 
324 
325  // only remove candidates if the whole trip uses the same mode
326  usableModes.removeIf(mode -> subtour.getTrips().stream().allMatch(
327  trip-> mode.equals(mainModeIdentifier.identifyMainMode(trip.getTripElements()))
328  ));
329 
330  } else {
331  // The old behaviour removes candidates that could change trips to a different mode
332  usableModes.remove(getTransportMode(subtour));
333  }
334 
335 
336  // (remove current mode so we don't get it again; note that the parent plan is kept anyways)
337  for (String transportMode : usableModes) {
338  choiceSet.add(
339  new Candidate(trips,
340  subtour,
341  transportMode));
342  }
343  }
344  return choiceSet;
345  }
346 
347  private boolean containsUnknownMode(final Subtour subtour) {
348  for (Trip trip : subtour.getTrips()) {
349  if (!modes.contains(mainModeIdentifier.identifyMainMode(trip.getTripElements()))) {
350  return true;
351  }
352  }
353  return false;
354  }
355 
356  private boolean isMassConserving(
357  final Subtour subtour,
358  final String mode) {
359  final Activity firstOrigin =
361  subtour.getTrips(),
362  mode);
363 
364  if (firstOrigin == null) {
365  return true;
366  }
367 
368  final Activity lastDestination =
370  subtour.getTrips(),
371  mode);
372 
373  return atSameLocation(firstOrigin, lastDestination);
374  }
375 
377  return activity.getFacilityId() != null ?
378  activity.getFacilityId() :
379  activity.getLinkId();
380  }
381 
382  private boolean atSameLocation(Activity firstLegUsingMode,
383  Activity lastLegUsingMode) {
384  if (firstLegUsingMode.getFacilityId() != null)
385  return firstLegUsingMode.getFacilityId().equals(lastLegUsingMode.getFacilityId());
386  else if ( coordDist > 0 && firstLegUsingMode.getCoord() != null && lastLegUsingMode.getCoord() != null)
387  return CoordUtils.calcEuclideanDistance(firstLegUsingMode.getCoord(), lastLegUsingMode.getCoord()) <= coordDist;
388  else
389  return firstLegUsingMode.getLinkId().equals(lastLegUsingMode.getLinkId());
390  }
391 
393  final List<Trip> tripsToSearch,
394  final String mode) {
395  final List<Trip> reversed = new ArrayList<>(tripsToSearch);
396  Collections.reverse(reversed);
397  for (Trip trip : reversed) {
398  if (mode.equals(mainModeIdentifier.identifyMainMode(trip.getTripElements()))) {
399  return trip.getDestinationActivity();
400  }
401  }
402  return null;
403  }
404 
406  final List<Trip> tripsToSearch,
407  final String mode) {
408  for (Trip trip : tripsToSearch) {
409  if (mode.equals(mainModeIdentifier.identifyMainMode(trip.getTripElements()))) {
410  return trip.getOriginActivity();
411  }
412  }
413  return null;
414  }
415 
416  private String getTransportMode(final Subtour subtour) {
417  return mainModeIdentifier.identifyMainMode(
418  subtour.getTrips().get(0).getTripElements());
419  }
420 
421  private void applyChange(
422  final Candidate whatToDo,
423  final Plan plan) {
424 
425  boolean containsChainBasedMode = false;
426  Set<String> originalModes = new HashSet<>();
427 
428  // Trips that are not part of any child subtour
429  Set<Trip> childTrips = new HashSet<>(whatToDo.subtour.getTripsWithoutSubSubtours());
430 
431  for (Trip trip : whatToDo.subtour.getTrips()) {
432 
433  String mainMode = mainModeIdentifier.identifyMainMode(trip.getTripElements());
434 
435  if (childTrips.contains(trip) && chainBasedModes.contains(mainMode))
436  containsChainBasedMode = true;
437 
438  if (childTrips.contains(trip))
439  originalModes.add(mainMode);
440 
442  if (!modes.contains(mainMode)) {
443  // (ignore trips with modes that are not in "modes". MATSIM-809)
444  continue;
445  }
446  }
447 
449  plan,
450  trip.getOriginActivity(),
451  Collections.singletonList(PopulationUtils.createLeg(whatToDo.newTransportMode)),
452  trip.getDestinationActivity());
453  }
454 
455 
456  // If this is a subtour covering all trips, this constraint will be ignored
457  // this is because the whole plan is always added as a whole even if not a subtour
459  && containsChainBasedMode && originalModes.size() > 1
460  && TripStructureUtils.getTrips(plan).size() != whatToDo.subtour.getTrips().size()
461  ) {
462  logger.error("Subtour of person {} contains a mix of chain- and non-chainbased modes: {}. " +
463  "Subtour mode-choice won't be able to go back to this state. You need to adjust the input beforehand or disable 'betweenAllAndFewerConstraints'.", plan.getPerson().getId(), originalModes);
464  throw new IllegalStateException("Subtour contains a mix of chain- and non-chainbased modes.");
465  }
466 
467  }
468 
469 }
static Subtour getUnclosedRootSubtour(final Plan plan)
boolean atSameLocation(Activity firstLegUsingMode, Activity lastLegUsingMode)
Activity findFirstOriginOfMode(final List< Trip > tripsToSearch, final String mode)
ChooseRandomLegModeForSubtour(final MainModeIdentifier mainModeIdentifier, final PermissibleModesCalculator permissibleModesCalculator, final String[] modes, final String[] chainBasedModes, final Random rng, SubtourModeChoice.Behavior behavior, double probaForChooseRandomSingleTripMode, double coordDist)
static double calcEuclideanDistance(Coord coord, Coord other)
static List< PlanElement > insertTrip(final Plan plan, final Activity origin, final List<? extends PlanElement > trip, final Activity destination)
String identifyMainMode(List<? extends PlanElement > tripElements)
ChooseRandomLegModeForSubtour(final MainModeIdentifier mainModeIdentifier, final PermissibleModesCalculator permissibleModesCalculator, final String[] modes, final String[] chainBasedModes, final Random rng, SubtourModeChoice.Behavior behavior, double probaForChooseRandomSingleTripMode)
Activity findLastDestinationOfMode(final List< Trip > tripsToSearch, final String mode)
static Leg createLeg(String transportMode)
static List< Trip > getTrips(final Plan plan)
List< PlanElement > getPlanElements()
static Collection< Subtour > getSubtours(final Plan plan)
Id< ActivityFacility > getFacilityId()
Candidate(List< Trip > plan, final Subtour subtour, final String newTransportMode)
boolean equals(Object obj)
Definition: Id.java:139
List< Candidate > determineChoiceSet(final Id<? extends BasicLocation > homeLocation, final Plan plan, final List< Trip > trips, final Collection< String > permissibleModesForThisPerson)