Record throughput through a traffic signal by direction, expose through

the API (#245), and beef up the Python example.

Impact to prebaked file size is tiny -- for lakeslice, the original
intersection_thruput is 2MB and the new traffic_signal_thruput is 435KB.

[rebuild]
This commit is contained in:
Dustin Carlino 2020-08-22 09:12:18 -07:00
parent 5b2a548c93
commit 11832aa606
6 changed files with 112 additions and 39 deletions

View File

@ -34,6 +34,9 @@ are missing, etc. A summary of the commands available so far:
- **GET /traffic-signals/get-delays?id=42&t1=03:00:00&t2=03:30:00**: Returns
the delay experienced by every agent passing through intersection #42 from
3am to 3:30, grouped by direction of travel.
- **GET /traffic-signals/get-cumulative-thruput?id=42**: Returns the number of
agents passing through intersection #42 since midnight, grouped by direction
of travel.
- **/data**
- **GET /data/get-finished-trips**: Returns a JSON list of all finished trips.
Each tuple is (time the trip finished in seconds after midnight, trip ID,

View File

@ -73,9 +73,9 @@ data/system/maps/south_seattle.bin,87815aaddcc294c0cd30f0b7db97d0d4,https://www.
data/system/maps/udistrict.bin,ae9163526dcd1b4f15d4cee2b3a2f91f,https://www.dropbox.com/s/hb6yxpvkums7mtt/udistrict.bin.zip?dl=0
data/system/maps/west_seattle.bin,d91214290adac1f8d8db63f6d680f2d1,https://www.dropbox.com/s/qk0kxreqjpoghwh/west_seattle.bin.zip?dl=0
data/system/maps/xian.bin,dcb681858c2f4723a4c09b0ebafcc38f,https://www.dropbox.com/s/qxtwswze0o93w3p/xian.bin.zip?dl=0
data/system/prebaked_results/lakeslice/weekday.bin,364abba838c51f1ecc94738878c6ec9c,https://www.dropbox.com/s/jwy9gnak0g81qoz/weekday.bin.zip?dl=0
data/system/prebaked_results/montlake/car vs bike contention.bin,faa041af70a41bb8fcacd3dfd7a7d467,https://www.dropbox.com/s/jefg0ikjy9dsrdd/car%20vs%20bike%20contention.bin.zip?dl=0
data/system/prebaked_results/montlake/weekday.bin,3d2e502d13b995ebd3de4682ae41207a,https://www.dropbox.com/s/wxi268ft3iuhco4/weekday.bin.zip?dl=0
data/system/prebaked_results/lakeslice/weekday.bin,6f5a5b9076fb4f47c3e2d9870c020167,https://www.dropbox.com/s/tzzxxu4575wnapl/weekday.bin.zip?dl=0
data/system/prebaked_results/montlake/car vs bike contention.bin,8002ad9207bda6a2fc20594f413a8c2e,https://www.dropbox.com/s/jefg0ikjy9dsrdd/car%20vs%20bike%20contention.bin.zip?dl=0
data/system/prebaked_results/montlake/weekday.bin,c483c6734087c76630cdf652882dc440,https://www.dropbox.com/s/rvhek1pbwnf7t2e/weekday.bin.zip?dl=0
data/system/scenarios/ballard/weekday.bin,8fba0bf4e623552d75c27e9c210ce469,https://www.dropbox.com/s/62pwtz3pn098v84/weekday.bin.zip?dl=0
data/system/scenarios/downtown/weekday.bin,d789b5b7fe2a536f65ae330abb575823,https://www.dropbox.com/s/2ia9ssy9gza86sw/weekday.bin.zip?dl=0
data/system/scenarios/huge_seattle/weekday.bin,9892f03c2998b3661384f086dbc6c4bb,https://www.dropbox.com/s/hk3sr90owfr3hzc/weekday.bin.zip?dl=0

View File

@ -105,6 +105,10 @@ impl App {
"- intersection_thruput: {} bytes",
prettyprint_usize(serialized_size_bytes(&a.intersection_thruput))
);
println!(
"- traffic_signal_thruput: {} bytes",
prettyprint_usize(serialized_size_bytes(&a.traffic_signal_thruput))
);
println!(
"- demand : {} bytes",
prettyprint_usize(serialized_size_bytes(&a.demand))

View File

@ -9,10 +9,11 @@ import json
import requests
def main():
api = 'http://localhost:1234'
hours_to_sim = '12:00:00'
api = 'http://localhost:1234'
hours_to_sim = '12:00:00'
def main():
# Make sure to start the simulation from the beginning
print('Did you just start the simulation? Time is currently', requests.get(api + '/sim/get-time').text)
print('Reset the simulation:', requests.get(api + '/sim/reset').text)
@ -20,13 +21,12 @@ def main():
# Run a few hours to get a baseline
print('Simulating before any edits')
print(requests.get(api + '/sim/goto-time', params={'t': hours_to_sim}).text)
baseline_trips = process_trips(requests.get(api + '/data/get-finished-trips').json()['trips'])
baseline_delays = requests.get(api + '/traffic-signals/get-delays', params={'id': 67, 't1': '00:00:00', 't2': hours_to_sim}).json()
print('Baseline: {} finished trips, total of {} seconds'.format(len(baseline_trips), sum(baseline_trips.values())))
trips1, delays1, thruput1 = run_experiment()
print('Baseline: {} finished trips, total of {} seconds'.format(len(trips1), sum(trips1.values())))
print()
# Modify one traffic signal, doubling the duration of its second phase
print('Modify a traffic signal')
ts = requests.get(api + '/traffic-signals/get', params={'id': 67}).json()
ts['phases'][1]['phase_type']['Fixed'] *= 2
# Reset the simulation before applying the edit, since reset also clears edits.
@ -36,42 +36,61 @@ def main():
# Repeat the experiment
print('Simulating after the edits')
print(requests.get(api + '/sim/goto-time', params={'t': hours_to_sim}).text)
experimental_trips = process_trips(requests.get(api + '/data/get-finished-trips').json()['trips'])
experimental_delays = requests.get(api + '/traffic-signals/get-delays', params={'id': 67, 't1': '00:00:00', 't2': hours_to_sim}).json()
print('Experiment: {} finished trips, total of {} seconds'.format(len(experimental_trips), sum(experimental_trips.values())))
trips2, delays2, thruput2 = run_experiment()
print('Experiment: {} finished trips, total of {} seconds'.format(len(trips2), sum(trips2.values())))
print()
# Compare -- did this help or not?
print('{} more trips finished after the edits (higher is better)'.format(len(experimental_trips) - len(baseline_trips)))
print('Experiment was {} seconds faster, over all trips'.format(sum(baseline_trips.values()) - sum(experimental_trips.values())))
print('{} more trips finished after the edits (higher is better)'.format(len(trips2) - len(trips1)))
print('Experiment was {} seconds faster, over all trips'.format(sum(trips1.values()) - sum(trips2.values())))
print()
# How much delay difference was there for each direction of travel?
print('Average delay per direction of travel at this intersection BEFORE edits:')
for direction, delays1 in baseline_delays['per_direction']:
# Skip empty cases and crosswalks
if delays1 and not direction['crosswalk']:
print('- {} -> {}: {:.1f} seconds'.format(print_road(direction['from']), print_road(direction['to']), sum(delays1) / len(delays1)))
# TODO Hard to group these together to compare, because Python can't handle dicts as keys. Stringify it first.
print('Average delay per direction of travel at this intersection AFTER edits:')
for direction, delays2 in experimental_delays['per_direction']:
# Skip empty cases and crosswalks
if delays2 and not direction['crosswalk']:
print('- {} -> {}: {:.1f} seconds'.format(print_road(direction['from']), print_road(direction['to']), sum(delays2) / len(delays2)))
# Now we'll print some before/after stats per direction of travel through
# the intersection
col = '{:<40} {:>20} {:>20} {:>17} {:>17}'
print(col.format('Direction', 'avg delay before', 'avg delay after', 'thruput before', 'thruput after'))
for k in delays1.keys():
print(col.format(k, delays1[k], delays2[k], thruput1[k], thruput2[k]))
# Return a map from trip ID to the duration (in seconds) of the trip. Filter
# out aborted (failed) trips.
def process_trips(trips):
results = {}
for (_, trip, mode, duration) in trips:
# Returns (trips, delay, throughput)
def run_experiment():
print(requests.get(api + '/sim/goto-time', params={'t': hours_to_sim}).text)
raw_trips = requests.get(api + '/data/get-finished-trips').json()['trips']
raw_delays = requests.get(api + '/traffic-signals/get-delays', params={'id': 67, 't1': '00:00:00', 't2': hours_to_sim}).json()
raw_thruput = requests.get(api + '/traffic-signals/get-cumulative-thruput', params={'id': 67}).json()
# Map trip ID to the duration (in seconds) of the trip. Filter out aborted
# (failed) trips.
trips = {}
for (_, trip, mode, duration) in raw_trips:
if mode is not None:
results[trip] = duration
return results
trips[trip] = duration
# The direction is a dict, but Python can't handle dicts as keys. Stringify
# the keys, also filtering out crosswalks and empty directions.
delays = {}
for k, v in raw_delays['per_direction']:
k = stringify_direction(k)
if k and v:
delays[k] = '{:.1f}'.format(sum(v) / len(v))
thruput = {}
for k, v in raw_thruput['per_direction']:
k = stringify_direction(k)
if k:
thruput[k] = v
return (trips, delays, thruput)
def print_road(directed_road):
def stringify_direction(direxn):
if direxn['crosswalk']:
return None
return '{} -> {}'.format(stringify_road(direxn['from']), stringify_road(direxn['to']))
def stringify_road(directed_road):
if directed_road['forwards']:
direxn = 'fwd'
else:

View File

@ -12,10 +12,11 @@
use abstutil::{serialize_btreemap, CmdArgs, Timer};
use geom::{Duration, Time};
use hyper::{Body, Request, Response, Server};
use map_model::{ControlTrafficSignal, IntersectionID, Map, TurnGroupID};
use map_model::{CompressedTurnGroupID, ControlTrafficSignal, IntersectionID, Map, TurnGroupID};
use serde::Serialize;
use sim::{AlertHandler, Sim, SimFlags, SimOptions, TripID, TripMode};
use std::collections::{BTreeMap, HashMap};
use std::convert::TryFrom;
use std::error::Error;
use std::sync::RwLock;
@ -147,6 +148,30 @@ fn handle_command(
}
Ok(abstutil::to_json(&delays))
}
"/traffic-signals/get-cumulative-thruput" => {
let i = IntersectionID(params["id"].parse::<usize>()?);
let ts = if let Some(ts) = map.maybe_get_traffic_signal(i) {
ts
} else {
return Err(format!("{} isn't a traffic signal", i).into());
};
let mut thruput = Throughput {
per_direction: BTreeMap::new(),
};
for (idx, tg) in ts.turn_groups.keys().enumerate() {
thruput.per_direction.insert(
tg.clone(),
sim.get_analytics()
.traffic_signal_thruput
.total_for(CompressedTurnGroupID {
i,
idx: u8::try_from(idx).unwrap(),
}),
);
}
Ok(abstutil::to_json(&thruput))
}
// Querying data
"/data/get-finished-trips" => Ok(abstutil::to_json(&FinishedTrips {
trips: sim.get_analytics().finished_trips.clone(),
@ -169,3 +194,9 @@ struct Delays {
#[serde(serialize_with = "serialize_btreemap")]
per_direction: BTreeMap<TurnGroupID, Vec<Duration>>,
}
#[derive(Serialize)]
struct Throughput {
#[serde(serialize_with = "serialize_btreemap")]
per_direction: BTreeMap<TurnGroupID, usize>,
}

View File

@ -2,8 +2,8 @@ use crate::{AgentType, AlertLocation, CarID, Event, ParkingSpot, TripID, TripMod
use abstutil::Counter;
use geom::{Distance, Duration, Time};
use map_model::{
BusRouteID, BusStopID, IntersectionID, LaneID, Map, ParkingLotID, Path, PathRequest, RoadID,
Traversable, TurnGroupID,
BusRouteID, BusStopID, CompressedTurnGroupID, IntersectionID, LaneID, Map, ParkingLotID, Path,
PathRequest, RoadID, Traversable, TurnGroupID,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, VecDeque};
@ -12,6 +12,10 @@ use std::collections::{BTreeMap, VecDeque};
pub struct Analytics {
pub road_thruput: TimeSeriesCount<RoadID>,
pub intersection_thruput: TimeSeriesCount<IntersectionID>,
// TODO For traffic signals, intersection_thruput could theoretically use this. But that
// requires occasionally expensive or complicated summing or merging over all directions of an
// intersection. So for now, eat the file size cost.
pub traffic_signal_thruput: TimeSeriesCount<CompressedTurnGroupID>,
// Unlike everything else in Analytics, this is just for a moment in time.
pub demand: BTreeMap<TurnGroupID, usize>,
@ -50,6 +54,7 @@ impl Analytics {
Analytics {
road_thruput: TimeSeriesCount::new(),
intersection_thruput: TimeSeriesCount::new(),
traffic_signal_thruput: TimeSeriesCount::new(),
demand: BTreeMap::new(),
bus_arrivals: Vec::new(),
passengers_boarding: BTreeMap::new(),
@ -99,6 +104,17 @@ impl Analytics {
if let Some(id) = map.get_turn_group(t) {
*self.demand.entry(id).or_insert(0) -= 1;
let tg = map.get_traffic_signal(t.parent).compressed_id(t);
self.traffic_signal_thruput.record(time, tg, a.to_type(), 1);
if let Some(n) = passengers {
self.traffic_signal_thruput.record(
time,
tg,
AgentType::TransitRider,
n,
);
}
}
}
};