forked from HSLdevcom/OpenTripPlanner
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'otp2_transit_priority_in_group_filter' into otp2_entur_…
…develop
- Loading branch information
Showing
13 changed files
with
949 additions
and
50 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
...opentripplanner/routing/algorithm/filterchain/filters/system/SingeCriteriaComparator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package org.opentripplanner.routing.algorithm.filterchain.filters.system; | ||
|
||
import java.util.Comparator; | ||
import java.util.function.ToIntFunction; | ||
import org.opentripplanner.model.plan.Itinerary; | ||
import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.grouppriority.TransitGroupPriority32n; | ||
|
||
/** | ||
* Comparator used to compare a SINGE criteria for dominance. The difference between this and the | ||
* {@link org.opentripplanner.raptor.util.paretoset.ParetoComparator} is that: | ||
* <ol> | ||
* <li>This applies to one criteria, not multiple.</li> | ||
* <li>This interface apply to itineraries; It is not generic.</li> | ||
* </ol> | ||
* A set of instances of this interface can be used to create a pareto-set. See | ||
* {@link org.opentripplanner.raptor.util.paretoset.ParetoSet} and | ||
* {@link org.opentripplanner.raptor.util.paretoset.ParetoComparator}. | ||
* <p/> | ||
* This interface extends {@link Comparator} so elements can be sorted as well. Not all criteria | ||
* can be sorted, if so the {@link #strictOrder()} should return false (this is the default). | ||
*/ | ||
@FunctionalInterface | ||
public interface SingeCriteriaComparator { | ||
/** | ||
* The left criteria dominates the right criteria. Note! The right criteria my dominate | ||
* the left criteria if there is no {@link #strictOrder()}. If left and right are equals, then | ||
* there is no dominance. | ||
*/ | ||
boolean leftDominanceExist(Itinerary left, Itinerary right); | ||
|
||
/** | ||
* Return true if the criteria can be deterministically sorted. | ||
*/ | ||
default boolean strictOrder() { | ||
return false; | ||
} | ||
|
||
static SingeCriteriaComparator compareNumTransfers() { | ||
return compareLessThan(Itinerary::getNumberOfTransfers); | ||
} | ||
|
||
static SingeCriteriaComparator compareGeneralizedCost() { | ||
return compareLessThan(Itinerary::getGeneralizedCost); | ||
} | ||
|
||
@SuppressWarnings("OptionalGetWithoutIsPresent") | ||
static SingeCriteriaComparator compareTransitGroupsPriority() { | ||
return (left, right) -> | ||
TransitGroupPriority32n.dominate( | ||
left.getGeneralizedCost2().get(), | ||
right.getGeneralizedCost2().get() | ||
); | ||
} | ||
|
||
static SingeCriteriaComparator compareLessThan(final ToIntFunction<Itinerary> op) { | ||
return new SingeCriteriaComparator() { | ||
@Override | ||
public boolean leftDominanceExist(Itinerary left, Itinerary right) { | ||
return op.applyAsInt(left) < op.applyAsInt(right); | ||
} | ||
|
||
@Override | ||
public boolean strictOrder() { | ||
return true; | ||
} | ||
}; | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
...in/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/Group.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package org.opentripplanner.routing.algorithm.filterchain.filters.system.mcmax; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
|
||
/** | ||
* The purpose of a group is to maintain a list of items, all optimal for a single | ||
* criteria/comparator. After the group is created, then the criteria is no longer needed, so we do | ||
* not keep a reference to the original criteria. | ||
*/ | ||
class Group implements Iterable<Item> { | ||
|
||
private final List<Item> items = new ArrayList<>(); | ||
|
||
public Group(Item firstItem) { | ||
add(firstItem); | ||
} | ||
|
||
Item first() { | ||
return items.getFirst(); | ||
} | ||
|
||
boolean isEmpty() { | ||
return items.isEmpty(); | ||
} | ||
|
||
boolean isSingleItemGroup() { | ||
return items.size() == 1; | ||
} | ||
|
||
void add(Item item) { | ||
item.incGroupCount(); | ||
items.add(item); | ||
} | ||
|
||
void removeAllItems() { | ||
items.forEach(Item::decGroupCount); | ||
items.clear(); | ||
} | ||
|
||
void addNewDominantItem(Item item) { | ||
removeAllItems(); | ||
add(item); | ||
} | ||
|
||
boolean contains(Item item) { | ||
return this.items.contains(item); | ||
} | ||
|
||
@Override | ||
public Iterator<Item> iterator() { | ||
return items.iterator(); | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
...ain/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/Item.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package org.opentripplanner.routing.algorithm.filterchain.filters.system.mcmax; | ||
|
||
import org.opentripplanner.model.plan.Itinerary; | ||
|
||
/** | ||
* An item is a decorated itinerary. The extra information added is the index in the input list | ||
* (sort order) and a groupCount. The sort order is used to break ties, while the group-count is | ||
* used to select the itinerary witch exist in the highest number of groups. The group dynamically | ||
* updates the group-count; The count is incremented when an item is added to a group, and | ||
* decremented when the group is removed from the State. | ||
*/ | ||
class Item { | ||
|
||
private final Itinerary item; | ||
private final int index; | ||
private int groupCount = 0; | ||
|
||
Item(Itinerary item, int index) { | ||
this.item = item; | ||
this.index = index; | ||
} | ||
|
||
/** | ||
* An item is better than another if the groupCount is higher, and in case of a tie, if the sort | ||
* index is lower. | ||
*/ | ||
public boolean betterThan(Item o) { | ||
return groupCount != o.groupCount ? groupCount > o.groupCount : index < o.index; | ||
} | ||
|
||
Itinerary item() { | ||
return item; | ||
} | ||
|
||
void incGroupCount() { | ||
++this.groupCount; | ||
} | ||
|
||
void decGroupCount() { | ||
--this.groupCount; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return "Item #%d {count:%d, %s}".formatted(index, groupCount, item.toStr()); | ||
} | ||
} |
105 changes: 105 additions & 0 deletions
105
.../opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/McMaxLimitFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package org.opentripplanner.routing.algorithm.filterchain.filters.system.mcmax; | ||
|
||
import java.util.List; | ||
import java.util.function.Predicate; | ||
import org.opentripplanner.model.plan.Itinerary; | ||
import org.opentripplanner.routing.algorithm.filterchain.filters.system.SingeCriteriaComparator; | ||
import org.opentripplanner.routing.algorithm.filterchain.framework.spi.RemoveItineraryFlagger; | ||
|
||
/** | ||
* This filter is used to reduce a set of itineraries down to the specified limit, if possible. | ||
* The filter is guaranteed to keep at least the given {@code minNumItineraries} and/or the best | ||
* itinerary for each criterion. The criterion is defined using the list of {@code comparators}. | ||
* <p> | ||
* The main usage of this filter is to combine it with a transit grouping filter and for each group | ||
* make sure there is at least {@code minNumItineraries} and that the best itinerary with respect | ||
* to each criterion is kept. So, if the grouping is based on time and riding common trips, then | ||
* this filter will use the reminding criterion (transfers, generalized-cost, | ||
* [transit-group-priority]) to filter the grouped set of itineraries. DO NOT INCLUDE CRITERIA | ||
* USED TO GROUP THE ITINERARIES, ONLY THE REMINDING CRITERION USED IN THE RAPTOR SEARCH. | ||
* <p> | ||
* <b>IMPLEMENTATION DETAILS</b> | ||
* <p> | ||
* This is not a trivial problem. In most cases, the best itinerary for a given criteria is unique, | ||
* but there might be ties - same number of transfers, same cost, and/or different priority groups. | ||
* In case of a tie, we will look if an itinerary is "best-in-group" for more than one criterion, | ||
* if so we pick the one witch is best in the highest number of groups. Again, if there is a tie | ||
* (best in the same number of groups), then we fall back to the given itinerary sorting order. | ||
* <p> | ||
* This filter will use the order of the input itineraries to break ties. So, make sure to call the | ||
* appropriate sort function before this filter is invoked. | ||
* <p> | ||
* Note! For a criteria like num-of-transfers or generalized-cost, there is only one set of "best" | ||
* itineraries, and usually there are only one or a few itineraries. In case there is more than one, | ||
* picking just one is fine. But, for transit-group-priority there might be more than one optimal | ||
* set of itineraries. For each set, we need to pick one itinerary for the final result. Each of | ||
* these sets may or may not have more than one itinerary. If you group by agency, then there will | ||
* be at least one itinerary for each agency present in the result (simplified, an itinerary may | ||
* consist of legs with different agencies). The transit-group-priority pareto-function used by | ||
* Raptor is reused, so we do not need to worry about the logic here. | ||
* <p> | ||
* Let's discuss an example (this example also exists as a unit-test case): | ||
* <pre> | ||
* minNumItineraries = 4 | ||
* comparators = [ generalized-cost, min-num-transfers, transit-group-priority ] | ||
* itineraries: [ | ||
* #0 : [ 1000, 2, (a) ] | ||
* #1 : [ 1000, 3, (a,b) ] | ||
* #2 : [ 1000, 3, (b) ] | ||
* #3 : [ 1200, 1, (a,b) ] | ||
* #4 : [ 1200, 1, (a) ] | ||
* #5 : [ 1300, 2, (c) ] | ||
* #6 : [ 1300, 3, (c) ] | ||
* ] | ||
* </pre> | ||
* The best itineraries by generalized-cost are (#0, #1, #2). The best itineraries by | ||
* min-num-transfers are (#3, #4). The best itineraries by transit-group-priority are | ||
* (a:(#0, #4), b:(#2), c:(#5, #6)). | ||
* <p> | ||
* So we need to pick one from each group (#0, #1, #2), (#3, #4), (#0, #4), (#2), and (#5, #6). | ||
* Since #2 is a single, we pick it first. Itinerary #2 is also one of the best | ||
* generalized-cost itineraries - so we are done with generalized-cost itineraries as well. The two | ||
* groups left are (#3, #4), (#0, #4), and (#5, #6). #4 exists in 2 groups, so we pick it next. Now | ||
* we are left with (#5, #6). To break the tie, we look at the sort-order. We pick | ||
* itinerary #5. Result: #2, #4, and #5. | ||
* <p> | ||
* The `minNumItineraries` limit is not met, so we need to pick another itinerary, we use the | ||
* sort-order again and add itinerary #0. The result returned is: [#0, #2, #4, #5] | ||
*/ | ||
public class McMaxLimitFilter implements RemoveItineraryFlagger { | ||
|
||
private final String name; | ||
private final int minNumItineraries; | ||
private final List<SingeCriteriaComparator> comparators; | ||
|
||
public McMaxLimitFilter( | ||
String name, | ||
int minNumItineraries, | ||
List<SingeCriteriaComparator> comparators | ||
) { | ||
this.name = name; | ||
this.minNumItineraries = minNumItineraries; | ||
this.comparators = comparators; | ||
} | ||
|
||
@Override | ||
public String name() { | ||
return name; | ||
} | ||
|
||
@Override | ||
public List<Itinerary> flagForRemoval(List<Itinerary> itineraries) { | ||
if (itineraries.size() <= minNumItineraries) { | ||
return List.of(); | ||
} | ||
var state = new State(itineraries, comparators); | ||
state.findAllSingleItemGroupsAndAddTheItemToTheResult(); | ||
state.findTheBestItemsUntilAllGroupsAreRepresentedInTheResult(); | ||
state.fillUpTheResultWithMinimumNumberOfItineraries(minNumItineraries); | ||
|
||
// We now have the itineraries we want, but we must invert this and return the | ||
// list of itineraries to drop - keeping the original order | ||
var ok = state.getResult(); | ||
return itineraries.stream().filter(Predicate.not(ok::contains)).toList(); | ||
} | ||
} |
Oops, something went wrong.