Skip to content

Commit bf364e0

Browse files
Merge pull request opentripplanner#6656 from Skanetrafiken/fix-negative-cost-in-filter-2
Fix negative cost in filter
2 parents 87baa1a + 11d6892 commit bf364e0

File tree

3 files changed

+113
-15
lines changed

3 files changed

+113
-15
lines changed

application/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/transit/group/RemoveOtherThanSameLegsMaxGeneralizedCost.java

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@
1717
* itineraries have a much higher cost for the other legs. This is similar to {@link
1818
* org.opentripplanner.routing.algorithm.filterchain.filters.transit.TransitGeneralizedCostFilter},
1919
* but is used together with {@link GroupByFilter} to filter within the groups.
20+
*
21+
* <h3>Example</h3>
22+
*
23+
* Lets give 5% slack: f=1.05
24+
*
25+
* <pre>
26+
* Itin A: | ### cost of legs common trips - $42 ### | ########## other legs - $41 ########## | (Total: $83)
27+
* Itin B: | ######## cost of legs common trips - $52 ######## | ### other legs - $27 ### | (Total: $79)
28+
* </pre>
29+
*
30+
* <ul>
31+
* <li>Min cost common legs: a=$42</li>
32+
* <li>Min cost all itineraries: b=$79</li>
33+
* <li>maxLimit = a + (b - a) * f = 42 + 37 * 1.05 = 81</li>
34+
* <li><b>Result:</b> Keep itinerary A, and drop B ($83 > limit $81)</li>
35+
* </ul>
2036
*/
2137
public class RemoveOtherThanSameLegsMaxGeneralizedCost implements RemoveItineraryFlagger {
2238

@@ -26,6 +42,9 @@ public class RemoveOtherThanSameLegsMaxGeneralizedCost implements RemoveItinerar
2642
private final double maxCostOtherLegsFactor;
2743

2844
public RemoveOtherThanSameLegsMaxGeneralizedCost(double maxCostOtherLegsFactor) {
45+
if (maxCostOtherLegsFactor < 1.0) {
46+
throw new IllegalArgumentException("maxCostOtherLegsFactor must be >= 1.0");
47+
}
2948
this.maxCostOtherLegsFactor = maxCostOtherLegsFactor;
3049
}
3150

@@ -59,35 +78,39 @@ public List<Itinerary> flagForRemoval(List<Itinerary> itineraries) {
5978
})
6079
.get();
6180

81+
if (commonTrips.isEmpty()) {
82+
return List.of();
83+
}
84+
6285
// Find the lowest cost of the common legs
63-
OptionalInt commonCost = itineraries
86+
int commonLegsCost = itineraries
6487
.stream()
6588
.mapToInt(itinerary ->
6689
itinerary
6790
.legs()
6891
.stream()
6992
.filter(Leg::isTransitLeg)
7093
.filter(leg -> commonTrips.contains(leg.trip()))
71-
.mapToInt(leg -> leg.generalizedCost())
94+
.mapToInt(leg -> Integer.max(0, leg.generalizedCost()))
7295
.sum()
7396
)
74-
.min();
75-
76-
if (commonCost.isEmpty()) {
77-
return List.of();
78-
}
97+
.min()
98+
.orElseThrow();
7999

80100
// Find the lowest cost for any itinerary
81-
int minimumCost = itineraries
101+
int minimumItineraryCost = itineraries
82102
.stream()
83103
.mapToInt(it -> it.generalizedCostIncludingPenalty().toSeconds())
84104
.min()
85105
.orElseThrow();
86106

107+
// Leg costs in otp are not guaranteed to be accurate and it is possible that the sum of the
108+
// commonLegCosts are higher than the minimumItineraryCost. So we need to make sure this does
109+
// not go negative.
110+
int otherLegsCost = Integer.max(0, minimumItineraryCost - commonLegsCost);
111+
87112
// Calculate the maximum limit allowed for itinerary cost
88-
Cost maxLimit = Cost.costOfSeconds(
89-
((minimumCost - commonCost.getAsInt()) * maxCostOtherLegsFactor) + commonCost.getAsInt()
90-
);
113+
Cost maxLimit = Cost.costOfSeconds(otherLegsCost * maxCostOtherLegsFactor + commonLegsCost);
91114

92115
return itineraries
93116
.stream()

application/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,13 @@ public TestItineraryBuilder bus(
303303
/**
304304
* Add a rail/train leg to the itinerary
305305
*/
306-
public TestItineraryBuilder rail(int tripId, int startTime, int endTime, Place to) {
306+
public TestItineraryBuilder rail(
307+
int tripId,
308+
int startTime,
309+
int endTime,
310+
Place to,
311+
@Nullable Integer cost
312+
) {
307313
return transit(
308314
RAIL_ROUTE,
309315
Integer.toString(tripId),
@@ -314,10 +320,15 @@ public TestItineraryBuilder rail(int tripId, int startTime, int endTime, Place t
314320
to,
315321
null,
316322
null,
317-
null
323+
null,
324+
cost
318325
);
319326
}
320327

328+
public TestItineraryBuilder rail(int tripId, int startTime, int endTime, Place to) {
329+
return rail(tripId, startTime, endTime, to, null);
330+
}
331+
321332
public TestItineraryBuilder faresV2Rail(
322333
int tripId,
323334
int startTime,
@@ -464,14 +475,46 @@ public TestItineraryBuilder transit(
464475
LocalDate serviceDate,
465476
Integer headwaySecs,
466477
ConstrainedTransfer transferFromPreviousLeg
478+
) {
479+
return transit(
480+
route,
481+
tripId,
482+
start,
483+
end,
484+
fromStopIndex,
485+
toStopIndex,
486+
to,
487+
serviceDate,
488+
headwaySecs,
489+
transferFromPreviousLeg,
490+
null
491+
);
492+
}
493+
494+
public TestItineraryBuilder transit(
495+
Route route,
496+
String tripId,
497+
int start,
498+
int end,
499+
int fromStopIndex,
500+
int toStopIndex,
501+
Place to,
502+
LocalDate serviceDate,
503+
Integer headwaySecs,
504+
ConstrainedTransfer transferFromPreviousLeg,
505+
@Nullable Integer cost
467506
) {
468507
if (lastPlace == null) {
469508
throw new IllegalStateException("Trip from place is unknown!");
470509
}
471510
int waitTime = start - lastEndTime(start);
472511
int legCost = 0;
473-
legCost += cost(WAIT_RELUCTANCE_FACTOR, waitTime);
474-
legCost += cost(1.0f, end - start) + BOARD_COST;
512+
if (cost != null) {
513+
legCost = cost;
514+
} else {
515+
legCost += cost(WAIT_RELUCTANCE_FACTOR, waitTime);
516+
legCost += cost(1.0f, end - start) + BOARD_COST;
517+
}
475518

476519
Trip trip = trip(tripId, route);
477520

application/src/test/java/org/opentripplanner/routing/algorithm/filterchain/filters/transit/group/RemoveOtherThanSameLegsMaxGeneralizedCostTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,36 @@ public void testFilter() {
2222
var subject = new RemoveOtherThanSameLegsMaxGeneralizedCost(2.0);
2323
assertEquals(List.of(second), subject.flagForRemoval(List.of(first, second)));
2424
}
25+
26+
@Test
27+
public void testFilterNothingInCommon() {
28+
Itinerary first = newItinerary(A)
29+
.rail(20, T11_05, T11_14, B)
30+
.bus(30, T11_16, T11_20, C)
31+
.build();
32+
33+
Itinerary second = newItinerary(A).rail(40, T11_05, T11_14, B).walk(D10m, C).build();
34+
35+
var subject = new RemoveOtherThanSameLegsMaxGeneralizedCost(2.0);
36+
assertEquals(List.of(), subject.flagForRemoval(List.of(first, second)));
37+
}
38+
39+
@Test
40+
public void testFilterInaccurateLegCosts() {
41+
// Regression test that verifies that we don't crash in the case where the sum of the leg costs
42+
// is larger than the itinerary cost.
43+
int itineraryCost = 100;
44+
Itinerary first = newItinerary(A)
45+
.rail(20, T11_05, T11_14, B, 400)
46+
.walk(60 * 4, C)
47+
.build(itineraryCost);
48+
49+
Itinerary second = newItinerary(A)
50+
.rail(20, T11_05, T11_14, B, 400)
51+
.walk(D10m, C)
52+
.build(itineraryCost);
53+
54+
var subject = new RemoveOtherThanSameLegsMaxGeneralizedCost(2.0);
55+
assertEquals(List.of(), subject.flagForRemoval(List.of(first, second)));
56+
}
2557
}

0 commit comments

Comments
 (0)