Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Take rental vehicle battery level into account #5960

Draft
wants to merge 7 commits into
base: dev-2.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.opentripplanner.astar.strategy;

import java.util.function.Predicate;
import org.opentripplanner.astar.spi.AStarEdge;
import org.opentripplanner.astar.spi.AStarState;
import org.opentripplanner.astar.spi.SkipEdgeStrategy;

/**
* Skips Edges when the available battery distance of a vehicle is less than the accumulated driving
* distance of the same vehicle
*/
public class BatteryDistanceSkipEdgeStrategy<
State extends AStarState<State, Edge, ?>, Edge extends AStarEdge<State, Edge, ?>
>
implements SkipEdgeStrategy<State, Edge> {

private final Predicate<State> shouldSkip;

public BatteryDistanceSkipEdgeStrategy(Predicate<State> shouldSkip) {
this.shouldSkip = shouldSkip;
}

@Override
public boolean shouldSkipEdge(State current, Edge edge) {
return shouldSkip.test(current);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.opentripplanner.routing.impl;

import org.opentripplanner.street.search.state.State;

public class BatteryValidator {

public static boolean wouldBatteryRunOut(Object current) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way you can get away without the cast?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not find any way, without breaking the AStarArchitectureTest.
If I got it right, we would have to use a concrete State as Generic in our Strategy, which would break the AStarArchitectureTest.

State state = (State) current;
double traversedBatteryMeters = state.traversedBatteryMeters;
double currentRangeMeters = state.currentRangeMeters;
if (currentRangeMeters == Double.POSITIVE_INFINITY) {
return false;
}
return currentRangeMeters < traversedBatteryMeters;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import java.util.Set;
import javax.annotation.Nullable;
import org.opentripplanner.astar.model.GraphPath;
import org.opentripplanner.astar.spi.SkipEdgeStrategy;
import org.opentripplanner.astar.spi.TraverseVisitor;
import org.opentripplanner.astar.strategy.BatteryDistanceSkipEdgeStrategy;
import org.opentripplanner.astar.strategy.ComposingSkipEdgeStrategy;
import org.opentripplanner.astar.strategy.DurationSkipEdgeStrategy;
import org.opentripplanner.astar.strategy.PathComparator;
import org.opentripplanner.ext.dataoverlay.routing.DataOverlayContext;
Expand Down Expand Up @@ -88,9 +91,7 @@ public List<GraphPath<State, Edge, Vertex>> getPaths(
.of()
.setHeuristic(new EuclideanRemainingWeightHeuristic(maxCarSpeed))
.setSkipEdgeStrategy(
new DurationSkipEdgeStrategy(
preferences.maxDirectDuration().valueOf(request.journey().direct().mode())
)
getSkipEdgeStrategy(request, preferences)
)
// FORCING the dominance function to weight only
.setDominanceFunction(new DominanceFunctions.MinimumWeight())
Expand Down Expand Up @@ -119,6 +120,24 @@ public List<GraphPath<State, Edge, Vertex>> getPaths(
return paths;
}

/**
* chooses which SkipEdge Strategies to use, based on if the Mode includes rental
* @return a ComposingSkipEdgeStrategy if mode includes rental, else selects the DurationSkipStrategy
*/
private static SkipEdgeStrategy getSkipEdgeStrategy(RouteRequest request, StreetPreferences preferences) {
if(request.journey().direct().mode().includesRenting()){
return new ComposingSkipEdgeStrategy<>(
new DurationSkipEdgeStrategy(
preferences.maxDirectDuration().valueOf(request.journey().direct().mode())
),
new BatteryDistanceSkipEdgeStrategy(BatteryValidator::wouldBatteryRunOut)
);
}
return new DurationSkipEdgeStrategy(
preferences.maxDirectDuration().valueOf(request.journey().direct().mode())
);
}

/**
* Try to find N paths through the Graph
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,6 @@ default boolean networkIsNotAllowed(VehicleRentalPreferences preferences) {

return !preferences.allowedNetworks().contains(getNetwork());
}

double getCurrentRangeMeters();
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ public VehicleRentalSystem getVehicleRentalSystem() {
return system;
}

@Override
public double getCurrentRangeMeters() {
return Double.POSITIVE_INFINITY;
}

@Override
public String toString() {
return String.format(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.opentripplanner.service.vehiclerental.model;

import java.time.Instant;
import java.util.Optional;
import java.util.Set;
import org.opentripplanner.framework.i18n.I18NString;
import org.opentripplanner.street.model.RentalFormFactor;
Expand Down Expand Up @@ -133,4 +134,12 @@ public VehicleRentalStationUris getRentalUris() {
public VehicleRentalSystem getVehicleRentalSystem() {
return system;
}

@Override
public double getCurrentRangeMeters() {
if (currentRangeMeters == null) {
return Double.POSITIVE_INFINITY;
}
return currentRangeMeters;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package org.opentripplanner.service.vehiclerental.street;

import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nonnull;
import org.opentripplanner.framework.i18n.I18NString;
import org.opentripplanner.routing.api.request.StreetMode;
import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace;
import org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle;
import org.opentripplanner.street.model.RentalFormFactor;
import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.search.state.State;
Expand All @@ -19,10 +21,12 @@
public class VehicleRentalEdge extends Edge {

public final RentalFormFactor formFactor;
private final VehicleRentalPlaceVertex vertex;

private VehicleRentalEdge(VehicleRentalPlaceVertex vertex, RentalFormFactor formFactor) {
super(vertex, vertex);
this.formFactor = formFactor;
this.vertex = vertex;
}

public static VehicleRentalEdge createVehicleRentalEdge(
Expand Down Expand Up @@ -169,6 +173,7 @@ public State[] traverse(State s0) {
? (int) preferences.pickupTime().toSeconds()
: (int) preferences.dropOffTime().toSeconds()
);
s1.setCurrentRangeMeters(getCurrentRangeMeters());
s1.setBackMode(null);
return s1.makeStateArray();
}
Expand Down Expand Up @@ -206,4 +211,8 @@ private static Set<RentalFormFactor> allowedModes(StreetMode streetMode) {
default -> Set.of();
};
}

public double getCurrentRangeMeters() {
return vertex.getStation().getCurrentRangeMeters();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,10 @@ private StateEditor doTraverse(State s0, TraverseMode traverseMode, boolean walk
s1.incrementWalkDistance(getDistanceWithElevation());
}

if (s1.isRentingVehicle()) {
s1.incrementTraversedBatteryMeters(getDistanceWithElevation());
}

if (costExtension != null) {
weight += costExtension.calculateExtraCost(s0, length_mm, traverseMode);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ public class State implements AStarState<State, Edge, Vertex>, Cloneable {
// we should DEFINITELY rename this variable and the associated methods.
public double walkDistance;

// how far a sharing vehicle powered by battery has driven
public double traversedBatteryMeters;

//the available battery distance of a currently selected sharing vehicle
//setting a magic value of 0.0 to make sure
public double currentRangeMeters;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As said in the developer meeting, I agree that the algorithm itself has to be evaluated firstmost.

With that said, you can save a considerable amount of memory by selecting different data types for the fields.
By decreasing the granularity with three bits (8 meters) you can represent values up to 2040 meters using a single byte. The values are still comparable, it's just that the unit is octa-meters.

The value has to be mangled (shifted and casted) to fit but those operations are basically free from a CPU perspective.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice it is not so easy, but lets not go into discussion on HW and Java architecture before we know how to solve this. I have a strong feeling that we do not need to add anything to the state to solve this.

/* CONSTRUCTORS */

/**
Expand Down Expand Up @@ -82,6 +89,8 @@ public State(
vertex.rentalRestrictions().noDropOffNetworks();
}
this.walkDistance = 0;
this.traversedBatteryMeters = 0;
this.currentRangeMeters = Double.POSITIVE_INFINITY;
this.time = startTime.getEpochSecond();
}

Expand Down Expand Up @@ -360,6 +369,8 @@ public State reverse() {
editor.incrementTimeInSeconds(orig.getAbsTimeDeltaSeconds());
editor.incrementWeight(orig.getWeightDelta());
editor.incrementWalkDistance(orig.getWalkDistanceDelta());
editor.incrementTraversedBatteryMeters(orig.getBatteryDistanceDelta());
editor.setCurrentRangeMeters(orig.getCurrentRangeMeters());

// propagate the modes through to the reversed edge
editor.setBackMode(orig.getBackMode());
Expand Down Expand Up @@ -503,6 +514,22 @@ private double getWalkDistanceDelta() {
}
}

private double getBatteryDistanceDelta() {
if (backState != null) {
return Math.abs(this.traversedBatteryMeters - backState.traversedBatteryMeters);
} else {
return 0.0;
}
}

private double getCurrentRangeMeters() {
if (backState != null) {
return backState.currentRangeMeters;
} else {
return Double.POSITIVE_INFINITY;
}
}

private State reversedClone() {
StreetSearchRequest reversedRequest = request
.copyOfReversed(getTime())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,19 @@ public void incrementWalkDistance(double length) {
child.walkDistance += length;
}

public void incrementTraversedBatteryMeters(double length) {
if (length < 0) {
LOG.warn("A state's battery distance is being incremented by a negative amount.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will have to check with the other developers but I would be totally unforgiving and throw an exception in this case. This indicates a bug elsewhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The incrementWalkDistance, incrementTimeInSeconds, incrementWeight methods in the StateEditor class do it the same way(thwowing no exception. So probably, if we change this, we should change those methods too.

We could add exceptions like NegativeBatteryMeterIncrement, NegativeWalkDistanceIncrement...

But we don' t know really good the exception handling in OTP.
So at least, I would prefer to create a new PR for that, if we should change that.

defectiveTraversal = true;
return;
}
child.traversedBatteryMeters += length;
}

public void setCurrentRangeMeters(double currentRangeMeters) {
child.currentRangeMeters = currentRangeMeters;
}

/* Basic Setters */

public void resetEnteredNoThroughTrafficArea() {
Expand Down Expand Up @@ -389,4 +402,8 @@ private void cloneStateDataAsNeeded() {
if (child.backState != null && child.stateData == child.backState.stateData) child.stateData =
child.stateData.clone();
}

public boolean isRentingVehicle() {
return child.isRentingVehicle();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.opentripplanner.astar.strategy;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;
import org.opentripplanner.routing.algorithm.GraphRoutingTest;
import org.opentripplanner.routing.impl.BatteryValidator;
import org.opentripplanner.street.search.state.State;
import org.opentripplanner.street.search.state.TestStateBuilder;

public class BatteryDistanceSkipEdgeStrategyTest extends GraphRoutingTest {

/**
* battery is not enough for driven distance-> skips Edge
* battery is 0m, driven meters = 100 => true
*/
@Test
void batteryIsNotEnough() {
var state = getState(100.0);

state.currentRangeMeters = 0.0;

var strategy = new BatteryDistanceSkipEdgeStrategy(BatteryValidator::wouldBatteryRunOut);

assertTrue(strategy.shouldSkipEdge(state, null));
}

/**
* battery is enough for driven distance-> does not skip Edge
* battery is 4000m, driven meters = 100 => false
*/
@Test
void batteryIsEnough() {
var state = getState(100.0);
state.currentRangeMeters = 4000.0;

var strategy = new BatteryDistanceSkipEdgeStrategy(BatteryValidator::wouldBatteryRunOut);
assertFalse(strategy.shouldSkipEdge(state, null));
}

/**
* battery dies at exact time location is reached -> does not skip edge
* battery is 100m, driven meters = 100 => false
*/
@Test
void batteryDiesAtFinalLocation() {
var state = getState(100.0);

state.currentRangeMeters = 100.0;

var strategy = new BatteryDistanceSkipEdgeStrategy(BatteryValidator::wouldBatteryRunOut);
assertFalse(strategy.shouldSkipEdge(state, null));
}

/**
* battery has remaining Energy, no distance was driven so far -> does not skip edge
* battery is 100m, driven meters = 0 => false
*/
@Test
void noDrivenMeters() {
var state = getState(0.0);
state.currentRangeMeters = 100.0;

var strategy = new BatteryDistanceSkipEdgeStrategy(BatteryValidator::wouldBatteryRunOut);
assertFalse(strategy.shouldSkipEdge(state, null));
}

/**
* vehicle.currentRangeMeters (battery) is Null or of Empty Value -> does not skip Edge
* Battery is Optional.Empty() => false
*/
@Test
void batteryHasNoValue() {
var state = TestStateBuilder.ofScooterRental().build();
state.currentRangeMeters = Double.POSITIVE_INFINITY;

var strategy = new BatteryDistanceSkipEdgeStrategy(BatteryValidator::wouldBatteryRunOut);
assertFalse(strategy.shouldSkipEdge(state, null));
}

private static State getState(double traversedBatteryMeters) {
var state = TestStateBuilder.ofScooterRental().build();
state.traversedBatteryMeters = traversedBatteryMeters;
return state;
}
}
Loading