Skip to content

Commit

Permalink
DRT Leg Estimators (#2865)
Browse files Browse the repository at this point in the history
* Put changes for DRT estimator into separate PR

* add todos from discussion

* small improvements with config and API

* integrate fare params into initial estimate

* revert change to generics

* improve how guice bindings are used

* update default decay

* controler -> controller

* update to new config api

* small change in initial estimate

* simplified injection

* Avoid storing speedUpParams

* bind estimators as map as well
  • Loading branch information
rakow authored Oct 18, 2023
1 parent 1542546 commit a9efdbc
Show file tree
Hide file tree
Showing 13 changed files with 745 additions and 2 deletions.
7 changes: 7 additions & 0 deletions contribs/drt-extensions/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
<version>16.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>org.matsim.contrib</groupId>
<artifactId>informed-mode-choice</artifactId>
<version>16.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>org.matsim.contrib</groupId>
<artifactId>simwrapper</artifactId>
Expand All @@ -33,6 +39,7 @@
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Scenario parameters only used in testing -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package org.matsim.contrib.drt.extension.estimator;

import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
import org.apache.commons.math3.stat.regression.RegressionResults;
import org.apache.commons.math3.stat.regression.SimpleRegression;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.matsim.api.core.v01.events.PersonMoneyEvent;
import org.matsim.contrib.drt.analysis.DrtEventSequenceCollector;
import org.matsim.contrib.drt.extension.estimator.run.DrtEstimatorConfigGroup;
import org.matsim.contrib.drt.fare.DrtFareParams;
import org.matsim.contrib.drt.routing.DrtRoute;
import org.matsim.contrib.drt.run.DrtConfigGroup;
import org.matsim.contrib.drt.speedup.DrtSpeedUp;
import org.matsim.core.controler.events.IterationEndsEvent;
import org.matsim.core.controler.listener.IterationEndsListener;
import org.matsim.core.utils.misc.OptionalTime;

import java.util.SplittableRandom;

/**
* Estimates drt trips based only daily averages. No spatial or temporal differentiation is taken into account for the estimate.
* This estimator is suited for small scenarios with few vehicles and trips and consequently few data points.
*/
public class BasicDrtEstimator implements DrtEstimator, IterationEndsListener {

private static final Logger log = LogManager.getLogger(BasicDrtEstimator.class);

private final DrtEventSequenceCollector collector;
private final DrtEstimatorConfigGroup config;
private final DrtConfigGroup drtConfig;

private final SplittableRandom rnd = new SplittableRandom();
/**
* Currently valid estimates.
*/
private GlobalEstimate currentEst;
private RegressionResults fare;

public BasicDrtEstimator(DrtEventSequenceCollector collector, DrtEstimatorConfigGroup config,
DrtConfigGroup drtConfig) {
//zones = injector.getModal(DrtZonalSystem.class);
this.collector = collector;
this.config = config;
this.drtConfig = drtConfig;
}

@Override
public void notifyIterationEnds(IterationEndsEvent event) {

// Speed-up iteration need to be ignored for the estimates
if (drtConfig.getDrtSpeedUpParams().isPresent() &&
DrtSpeedUp.isTeleportDrtUsers(drtConfig.getDrtSpeedUpParams().get(),
event.getServices().getConfig().controller(), event.getIteration())) {
return;
}

GlobalEstimate est = new GlobalEstimate();

int n = 0;

int nRejections = collector.getRejectedRequestSequences().size();
int nSubmitted = collector.getRequestSubmissions().size();

for (DrtEventSequenceCollector.EventSequence seq : collector.getPerformedRequestSequences().values()) {

if (seq.getPickedUp().isPresent() && seq.getDroppedOff().isPresent()) {

double waitTime = seq.getPickedUp().get().getTime() - seq.getSubmitted().getTime();
est.waitTime.addValue(waitTime);

double unsharedTime = seq.getSubmitted().getUnsharedRideTime();
double travelTime = seq.getDroppedOff().get().getTime() - seq.getPickedUp().get().getTime();

est.detour.addValue(travelTime / unsharedTime);

double fare = seq.getDrtFares().stream().mapToDouble(PersonMoneyEvent::getAmount).sum();
est.fare.addData(seq.getSubmitted().getUnsharedRideDistance(), fare);
n++;
}
}

// At least some data points are required
if (n <= 3)
return;

fare = est.fare.regress();

double rejectionRate = (double) nRejections / nSubmitted;

if (currentEst == null) {
est.meanWait = est.waitTime.getMean();
est.stdWait = est.waitTime.getStandardDeviation();
est.meanDetour = est.detour.getMean();
est.stdDetour = est.detour.getStandardDeviation();
est.rejectionRate = rejectionRate;
} else {
est.meanWait = config.decayFactor * est.waitTime.getMean() + (1 - config.decayFactor) * currentEst.waitTime.getMean();
est.stdWait = config.decayFactor * est.waitTime.getStandardDeviation() + (1 - config.decayFactor) * currentEst.waitTime.getStandardDeviation();
est.meanDetour = config.decayFactor * est.detour.getMean() + (1 - config.decayFactor) * currentEst.detour.getMean();
est.stdDetour = config.decayFactor * est.detour.getStandardDeviation() + (1 - config.decayFactor) * currentEst.detour.getStandardDeviation();
est.rejectionRate = config.decayFactor * rejectionRate + (1 - config.decayFactor) * currentEst.rejectionRate;
}

log.info("Calculated {}", est);
currentEst = est;
}

@Override
public Estimate estimate(DrtRoute route, OptionalTime departureTime) {

if (currentEst == null) {
// If not estimates are present, use travel time alpha as detour
// beta is not used, because estimates are supposed to be minimums and not worst cases
double travelTime = Math.min(route.getDirectRideTime() + drtConfig.maxAbsoluteDetour,
route.getDirectRideTime() * drtConfig.maxTravelTimeAlpha);

double fare = 0;
if (drtConfig.getDrtFareParams().isPresent()) {
DrtFareParams fareParams = drtConfig.getDrtFareParams().get();
fare = fareParams.distanceFare_m * route.getDistance()
+ fareParams.timeFare_h * route.getDirectRideTime() / 3600.0
+ fareParams.baseFare;

fare = Math.max(fare, fareParams.minFarePerTrip);
}

// for distance, also use the max travel time alpha
return new Estimate(route.getDistance() * drtConfig.maxTravelTimeAlpha, travelTime, drtConfig.maxWaitTime, fare, 0);
}

double fare = 0;
if (this.fare != null)
fare = this.fare.getParameterEstimate(0) + this.fare.getParameterEstimate(1) * route.getDistance();

if (drtConfig.getDrtFareParams().isPresent()) {
fare = Math.max(fare, drtConfig.getDrtFareParams().get().minFarePerTrip);
}

double detour = Math.max(1, rnd.nextGaussian(currentEst.meanDetour, config.randomization * currentEst.stdDetour));
double waitTime = Math.max(0, rnd.nextGaussian(currentEst.meanWait, config.randomization * currentEst.stdWait));

return new Estimate(route.getDistance() * detour, route.getDirectRideTime() * detour, waitTime, fare, currentEst.rejectionRate);
}

/**
* Helper class to hold statistics.
*/
private static final class GlobalEstimate {

private final SummaryStatistics waitTime = new SummaryStatistics();
private final SummaryStatistics detour = new SummaryStatistics();
private final SimpleRegression fare = new SimpleRegression(true);

private double meanWait;
private double stdWait;
private double meanDetour;
private double stdDetour;
private double rejectionRate;

@Override
public String toString() {
return "GlobalEstimate{" +
"meanWait=" + meanWait +
", stdWait=" + stdWait +
", meanDetour=" + meanDetour +
", stdDetour=" + stdDetour +
", rejectionRate=" + rejectionRate +
'}';
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package org.matsim.contrib.drt.extension.estimator;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.matsim.api.core.v01.events.PersonMoneyEvent;
import org.matsim.contrib.drt.analysis.DrtEventSequenceCollector;
import org.matsim.contrib.drt.extension.estimator.run.DrtEstimatorConfigGroup;
import org.matsim.contrib.drt.routing.DrtRoute;
import org.matsim.core.controler.events.AfterMobsimEvent;
import org.matsim.core.controler.events.ShutdownEvent;
import org.matsim.core.controler.events.StartupEvent;
import org.matsim.core.controler.listener.AfterMobsimListener;
import org.matsim.core.controler.listener.ShutdownListener;
import org.matsim.core.controler.listener.StartupListener;
import org.matsim.core.utils.misc.OptionalTime;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

/**
* Analyzes and outputs drt estimates errors metrics based on daily requests.
*/
public final class DrtEstimateAnalyzer implements StartupListener, ShutdownListener, AfterMobsimListener {

private static final Logger log = LogManager.getLogger(DrtEstimateAnalyzer.class);

// Might be useful but not needed currently
//private final DefaultMainLegRouter.RouteCreator creator;
private final DrtEstimator estimator;
private final DrtEventSequenceCollector collector;
private final DrtEstimatorConfigGroup config;

private CSVPrinter csv;

public DrtEstimateAnalyzer(DrtEstimator estimator, DrtEventSequenceCollector collector, DrtEstimatorConfigGroup config) {
this.estimator = estimator;
this.collector = collector;
this.config = config;
}

@Override
public void notifyStartup(StartupEvent event) {

String filename = event.getServices().getControlerIO().getOutputFilename("drt_estimates_" + config.getMode() + ".csv");

try {
csv = new CSVPrinter(Files.newBufferedWriter(Path.of(filename), StandardCharsets.UTF_8), CSVFormat.DEFAULT);
csv.printRecord("iteration",
"wait_time_mae", "wait_time_err_q5", "wait_time_err_q50", "wait_time_err_q95",
"travel_time_mae", "travel_time_err_q5", "travel_time_err_q50", "travel_time_err_q95",
"fare_mae", "fare_err_q5", "fare_err_q50", "fare_err_q95"
);

} catch (IOException e) {
throw new UncheckedIOException("Could not open output file for estimates.", e);
}
}

@Override
public void notifyShutdown(ShutdownEvent event) {
try {
csv.close();
} catch (IOException e) {
log.warn("Could not close drt estimate file", e);
}
}

/**
* Needs to run before any estimators updates.
*/
@Override
public void notifyAfterMobsim(AfterMobsimEvent event) {

try {
csv.printRecord(calcMetrics(event.getIteration()));
csv.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

/**
* Return row of metrics for the csv file.
*/
private Iterable<Number> calcMetrics(int iteration) {

DescriptiveStatistics waitTime = new DescriptiveStatistics();
DescriptiveStatistics travelTime = new DescriptiveStatistics();
DescriptiveStatistics fare = new DescriptiveStatistics();

for (DrtEventSequenceCollector.EventSequence seq : collector.getPerformedRequestSequences().values()) {
if (seq.getPickedUp().isPresent() && seq.getDroppedOff().isPresent()) {

// many attributes are not filled, when using the constructor
DrtRoute route = new DrtRoute(seq.getSubmitted().getFromLinkId(), seq.getSubmitted().getToLinkId());
route.setDirectRideTime(seq.getSubmitted().getUnsharedRideTime());
route.setDistance(seq.getSubmitted().getUnsharedRideDistance());

double valWaitTime = seq.getPickedUp().get().getTime() - seq.getSubmitted().getTime();
double valTravelTime = seq.getDroppedOff().get().getTime() - seq.getPickedUp().get().getTime();
double valFare = seq.getDrtFares().stream().mapToDouble(PersonMoneyEvent::getAmount).sum();

DrtEstimator.Estimate estimate = estimator.estimate(route, OptionalTime.defined(seq.getSubmitted().getTime()));

waitTime.addValue(Math.abs(estimate.waitingTime() - valWaitTime));
travelTime.addValue(Math.abs(estimate.travelTime() - valTravelTime));
fare.addValue(Math.abs(estimate.fare() - valFare));
}
}

return List.of(
iteration,
waitTime.getMean(), waitTime.getPercentile(5), waitTime.getPercentile(50), waitTime.getPercentile(95),
travelTime.getMean(), travelTime.getPercentile(5), travelTime.getPercentile(50), travelTime.getPercentile(95),
fare.getMean(), fare.getPercentile(5), fare.getPercentile(50), fare.getPercentile(95)
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.matsim.contrib.drt.extension.estimator;

import org.matsim.contrib.drt.routing.DrtRoute;
import org.matsim.core.controler.listener.ControlerListener;
import org.matsim.core.utils.misc.OptionalTime;

/**
* Interface to estimate a DRT service's detour, waiting time and costs.
*/
public interface DrtEstimator extends ControlerListener {

/**
* Provide an estimate for a drt route with specific pickup and dropoff point.
*
* @param route drt route
* @param departureTime estimated departure time
* @return An {@link Estimate} instance
*/
Estimate estimate(DrtRoute route, OptionalTime departureTime);


/**
* Estimate for various attributes for a drt trip.
*
* @param distance travel distance in meter
* @param travelTime travel time in seconds
* @param waitingTime waiting time in seconds
* @param fare money, which is negative if the customer needs to pay it
* @param rejectionRate probability of a trip being rejected
*/
record Estimate(double distance, double travelTime, double waitingTime, double fare, double rejectionRate) {

}

}
Loading

0 comments on commit a9efdbc

Please sign in to comment.