diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 63f249a..1f0aae5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -11,6 +11,7 @@ train command and its TEST lines. Otherwise, delete this section. --> ## Checklist - [ ] `cargo test --locked --release` passes (zero vehicle overlaps) +- [ ] The code is formatted with `cargo fmt --all` - [ ] `cargo clippy --locked --release --all-targets -- -D warnings` is clean (and for `wasm32-unknown-unknown` if the app changed) - [ ] New functionality has tests, and a bug fix has a test that fails without it (see [CONTRIBUTING.md](https://github.com/schreck61/intersection-lab/blob/main/CONTRIBUTING.md#guidelines)) - [ ] Runs stay deterministic for a given seed diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 239a1fc..e41d602 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,9 @@ # Copyright (c) 2026 Jim Schreckengast # SPDX-License-Identifier: MIT OR Apache-2.0 # -# Build and test on every platform, build the browser version, lint with clippy, build -# the crate from the files crates.io would get, check licenses, advisories and -# attributions, and fuzz the loading of weights files. +# Build and test on every platform, build the browser version, check formatting and lint +# with clippy, build the crate from the files crates.io would get, check licenses, +# advisories and attributions, and fuzz the loading of weights files. name: CI on: @@ -56,8 +56,12 @@ jobs: persist-credentials: false - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable with: - components: clippy + components: clippy, rustfmt targets: wasm32-unknown-unknown + # Formatting (see rustfmt.toml): the crate, then the fuzz target, which is a + # workspace of its own. + - run: cargo fmt --all --check + - run: cargo fmt --manifest-path fuzz/Cargo.toml --check - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 - run: cargo clippy --locked --release --all-targets -- -D warnings - run: cargo clippy --locked --release --target wasm32-unknown-unknown --bin intersection-lab -- -D warnings diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c4caed..9155cf6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,6 +48,7 @@ trunk serve --release --open CI runs these on every pull request. Running them locally first saves a round trip: ```bash +cargo fmt --all --check cargo build --locked --release --all-targets cargo test --locked --release cargo clippy --locked --release --all-targets -- -D warnings @@ -106,9 +107,14 @@ pass that file's path in place of the two folders. * **Few dependencies.** The project builds for desktop and the browser from one codebase and implements its own neural network and simulation, so new crates need a good reason. They must use a license allowed in [deny.toml](deny.toml). -* **Style:** match the surrounding code; the project doesn't run `rustfmt`, so please - don't reformat code you aren't changing. New source files start with the same - copyright and `SPDX-License-Identifier: MIT OR Apache-2.0` header as the others. +* **Style:** the code follows the + [Rust Style Guide](https://doc.rust-lang.org/style-guide/) as rustfmt applies it + (with the settings in [rustfmt.toml](rustfmt.toml)) and passes Clippy's default + lints. Run `cargo fmt --all` before you commit, and + `cargo fmt --manifest-path fuzz/Cargo.toml` if you changed the fuzz target. CI fails + a pull request that isn't formatted or that Clippy warns about. New source files + start with the same copyright and `SPDX-License-Identifier: MIT OR Apache-2.0` header + as the others. * **Spelling:** use American spelling in code, comments, documentation and UI text. * **Pull requests:** keep each one focused on a single change, explain what it does and why, and note anything you couldn't test (for example a platform you don't have). diff --git a/GOVERNANCE.md b/GOVERNANCE.md index beb9538..0726f81 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -19,8 +19,8 @@ This document describes how decisions are made and who does what. A change that lets vehicles collide isn't accepted, however much it helps delay. * **Through pull requests.** Every change to `main`, including the maintainer's own, goes through a pull request whose required checks pass: the tests on Linux, macOS and - Windows, the browser build, Clippy, the crate package, the license and advisory - checks, CodeQL, and DCO sign-off. Nobody can push to `main` directly. + Windows, the browser build, rustfmt and Clippy, the crate package, the license and + advisory checks, CodeQL, and DCO sign-off. Nobody can push to `main` directly. * **With reasons.** The maintainer reviews contributions and decides whether to merge them, and explains the reasons when declining a change or asking for a different approach. If you disagree with a decision, say so in the issue or pull request. diff --git a/docs/architecture.md b/docs/architecture.md index 571897d..65ac683 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -3143,7 +3143,7 @@ date: |---|---|---| | Test (ubuntu-22.04), Test (macos-15), Test (windows-latest) | `ci.yml` | `cargo build --locked --release --all-targets`, then `cargo test --locked --release` | | Web (wasm32) | `ci.yml` | Builds the app for `wasm32-unknown-unknown` | -| Clippy | `ci.yml` | `cargo clippy` with warnings as errors, for the native targets and the browser app | +| Clippy | `ci.yml` | `cargo fmt --check` on the crate and the fuzz target, then `cargo clippy` with warnings as errors, for the native targets and the browser app | | Crate package | `ci.yml` | `cargo package --locked`, which builds the crate from the files crates.io would get | | Licenses and advisories | `ci.yml` | `cargo deny check` (licenses, advisories, bans and sources), and that `THIRD_PARTY_LICENSES.md` is up to date | | Analyze (rust), Analyze (actions) | `codeql.yml` | CodeQL on the Rust code and on the workflows; also every Monday | diff --git a/examples/cooperative.rs b/examples/cooperative.rs index ee8aab4..89f191c 100644 --- a/examples/cooperative.rs +++ b/examples/cooperative.rs @@ -56,7 +56,18 @@ fn main() { ); println!( "{:>9} {:>9} {:>9} {:>8} {:>8} {:>8} {:>7} {:>8} {:>8} {:>6} {:>6} {:>6}", - "demand", "arrived", "served", "box/h", "delay", "p95", "maxwait", "queued", "PETmin", "viol", "dev", "overlap" + "demand", + "arrived", + "served", + "box/h", + "delay", + "p95", + "maxwait", + "queued", + "PETmin", + "viol", + "dev", + "overlap" ); let demands: Vec = match arg(6) { a if !a.is_empty() => a.split(',').filter_map(|x| x.parse().ok()).collect(), @@ -73,7 +84,8 @@ fn main() { if cars { demand.mix = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]; } - let mut sim = Sim::with_demand(net, ControlMode::Cooperative, CtrlKind::Fixed, demand, 7000 + seed); + let mut sim = + Sim::with_demand(net, ControlMode::Cooperative, CtrlKind::Fixed, demand, 7000 + seed); sim.set_gap_policy(policy); sim.set_scheduling(scheduling); sim.check_safety = true; diff --git a/examples/corridor.rs b/examples/corridor.rs index 384a82e..f23b3f2 100644 --- a/examples/corridor.rs +++ b/examples/corridor.rs @@ -8,14 +8,17 @@ //! //! usage: corridor [layout] [n] [minutes] [seeds] [demand-scale] use intersection_lab::control::CtrlKind; +use intersection_lab::sim::ControlMode; use intersection_lab::sim::demand::DemandConfig; use intersection_lab::sim::network::{LayoutKind, Network}; use intersection_lab::sim::world::World; -use intersection_lab::sim::ControlMode; fn main() { let args: Vec = std::env::args().collect(); - let kind = args.get(1).and_then(|k| LayoutKind::ALL.into_iter().find(|x| x.key() == k)).unwrap_or(LayoutKind::Arterial4); + let kind = args + .get(1) + .and_then(|k| LayoutKind::ALL.into_iter().find(|x| x.key() == k)) + .unwrap_or(LayoutKind::Arterial4); let n: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(3); let minutes: f64 = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(20.0); let seeds: u64 = args.get(4).and_then(|s| s.parse().ok()).unwrap_or(4); @@ -33,7 +36,17 @@ fn main() { ]; println!( "=== Corridor: {} x {} | {:.0} min x {} seeds ===\n{:<32} {:>10} {:>9} {:>10} {:>10} {:>10} {:>6}", - n, kind.name(), minutes, seeds, "control", "persDly*", "thru n", "thru time", "thru stops", "thru dly", "coll" + n, + kind.name(), + minutes, + seeds, + "control", + "persDly*", + "thru n", + "thru time", + "thru stops", + "thru dly", + "coll" ); let handles: Vec<_> = runs .into_iter() @@ -66,6 +79,9 @@ fn main() { .collect(); for h in handles { let (name, a) = h.join().unwrap(); - println!("{:<32} {:>9.1}s {:>9.0} {:>9.1}s {:>10.2} {:>9.1}s {:>6.0}", name, a[0], a[1], a[2], a[3], a[4], a[5]); + println!( + "{:<32} {:>9.1}s {:>9.0} {:>9.1}s {:>10.2} {:>9.1}s {:>6.0}", + name, a[0], a[1], a[2], a[3], a[4], a[5] + ); } } diff --git a/examples/fya.rs b/examples/fya.rs index b97a517..ba583e0 100644 --- a/examples/fya.rs +++ b/examples/fya.rs @@ -9,7 +9,10 @@ use intersection_lab::sim::{ControlMode, LeftMode, Sim}; fn main() { let seeds: u64 = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(6); - println!("{:<11} {:<11} {:>5} {:<30}| {:>12} {:>10} {:>9}", "layout", "controller", "scale", "left turns", "person delay", "left delay", "protected"); + println!( + "{:<11} {:<11} {:>5} {:<30}| {:>12} {:>10} {:>9}", + "layout", "controller", "scale", "left turns", "person delay", "left delay", "protected" + ); for kind in [LayoutKind::Arterial4, LayoutKind::MajorMinor4] { for ctrl in [CtrlKind::Fixed, CtrlKind::Actuated, CtrlKind::Predictive] { for scale in [0.7, 1.0, 1.3] { @@ -24,8 +27,12 @@ fn main() { let t0 = sim.t; sim.run_for(1800.0); let s = sim.stats.summary(t0, sim.t, &sim.vehicles, &sim.queues); - let lefts: Vec = (0..sim.net.movements.len()).filter(|&m| sim.net.movements[m].turn == Turn::L).collect(); - let (ld, ln) = lefts.iter().fold((0.0, 0), |a, &m| (a.0 + s.by_move[m].delay, a.1 + s.by_move[m].n)); + let lefts: Vec = (0..sim.net.movements.len()) + .filter(|&m| sim.net.movements[m].turn == Turn::L) + .collect(); + let (ld, ln) = lefts + .iter() + .fold((0.0, 0), |a, &m| (a.0 + s.by_move[m].delay, a.1 + s.by_move[m].n)); let prot = sim.left_protected.iter().filter(|&&p| p).count(); (s.person_delay_all, ld / ln.max(1) as f64, prot as f64) }) @@ -34,7 +41,16 @@ fn main() { let r: Vec<(f64, f64, f64)> = jobs.into_iter().map(|j| j.join().unwrap()).collect(); let n = seeds as f64; let m = |f: fn(&(f64, f64, f64)) -> f64| r.iter().map(f).sum::() / n; - println!("{:<11} {:<11} {:>5} {:<30}| {:>11.1}s {:>9.1}s {:>9.1}", kind.key(), ctrl.short(), scale, mode.name(), m(|x| x.0), m(|x| x.1), m(|x| x.2)); + println!( + "{:<11} {:<11} {:>5} {:<30}| {:>11.1}s {:>9.1}s {:>9.1}", + kind.key(), + ctrl.short(), + scale, + mode.name(), + m(|x| x.0), + m(|x| x.1), + m(|x| x.2) + ); } } } diff --git a/examples/lpi.rs b/examples/lpi.rs index 40c8b1a..94e4471 100644 --- a/examples/lpi.rs +++ b/examples/lpi.rs @@ -10,11 +10,19 @@ use intersection_lab::sim::{ControlMode, Sim}; fn main() { let seeds: u64 = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(8); - println!("{:<11} {:<11} {:>6} {:<14}| {:>11} {:>9} {:>12}", "layout", "controller", "peds/h", "treatment", "near-miss/h", "ped wait", "person delay"); + println!( + "{:<11} {:<11} {:>6} {:<14}| {:>11} {:>9} {:>12}", + "layout", "controller", "peds/h", "treatment", "near-miss/h", "ped wait", "person delay" + ); for kind in [LayoutKind::Arterial4, LayoutKind::Town4] { for ctrl in [CtrlKind::Fixed, CtrlKind::Predictive] { for ped in [60.0, 240.0] { - for (name, lpi, ntor) in [("none", 0.0, false), ("LPI 4 s", 4.0, false), ("NTOR w/ peds", 0.0, true), ("both", 4.0, true)] { + for (name, lpi, ntor) in [ + ("none", 0.0, false), + ("LPI 4 s", 4.0, false), + ("NTOR w/ peds", 0.0, true), + ("both", 4.0, true), + ] { let jobs: Vec<_> = (0..seeds) .map(|seed| { std::thread::spawn(move || { @@ -33,7 +41,16 @@ fn main() { let r: Vec<(f64, f64, f64)> = jobs.into_iter().map(|j| j.join().unwrap()).collect(); let n = seeds as f64; let m = |f: fn(&(f64, f64, f64)) -> f64| r.iter().map(f).sum::() / n; - println!("{:<11} {:<11} {:>6} {:<14}| {:>11.1} {:>8.1}s {:>11.1}s", kind.key(), ctrl.short(), ped, name, m(|x| x.0), m(|x| x.1), m(|x| x.2)); + println!( + "{:<11} {:<11} {:>6} {:<14}| {:>11.1} {:>8.1}s {:>11.1}s", + kind.key(), + ctrl.short(), + ped, + name, + m(|x| x.0), + m(|x| x.1), + m(|x| x.2) + ); } } } diff --git a/examples/make_icons.rs b/examples/make_icons.rs index ab72625..cdf1678 100644 --- a/examples/make_icons.rs +++ b/examples/make_icons.rs @@ -38,7 +38,8 @@ fn shade(x: f32, y: f32) -> Rgba { let road_w = 0.115; let in_v = (x - 0.5).abs() < road_w; let in_h = (y - 0.5).abs() < road_w; - let curb = ((x - 0.5).abs() - road_w).abs() < 0.012 && !in_h || ((y - 0.5).abs() - road_w).abs() < 0.012 && !in_v; + let curb = ((x - 0.5).abs() - road_w).abs() < 0.012 && !in_h + || ((y - 0.5).abs() - road_w).abs() < 0.012 && !in_v; if curb { c = rgb(214, 214, 206); } diff --git a/examples/satflow.rs b/examples/satflow.rs index a490652..2a51165 100644 --- a/examples/satflow.rs +++ b/examples/satflow.rs @@ -10,7 +10,9 @@ use intersection_lab::sim::{ControlMode, Sim}; use std::collections::HashMap; fn main() { - for (label, mix) in [("cars only", [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]), ("default mix", [0.62, 0.24, 0.05, 0.03, 0.04, 0.0])] { + for (label, mix) in + [("cars only", [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]), ("default mix", [0.62, 0.24, 0.05, 0.03, 0.04, 0.0])] + { let net = Network::build(LayoutKind::Arterial4); let mut d = DemandConfig::default_for(&net); // Saturate the eastbound through lanes; nothing else. @@ -19,7 +21,10 @@ fn main() { d.mix = mix; d.ped_rate = 0.0; let mut sim = Sim::with_demand(net, ControlMode::Signal, CtrlKind::Fixed, d, 1); - let m = sim.net.movement_for(sim.net.leg_index(intersection_lab::sim::network::Compass::W).unwrap(), Turn::T).unwrap(); + let m = sim + .net + .movement_for(sim.net.leg_index(intersection_lab::sim::network::Compass::W).unwrap(), Turn::T) + .unwrap(); let mut prev_ri: HashMap = HashMap::new(); let mut green_start = None; // (lane, position in queue discharge, time since green) @@ -40,13 +45,19 @@ fn main() { } for v in &sim.vehicles { let p = prev_ri.insert(v.id, v.ri).unwrap_or(0); - if p == 0 && v.ri >= 1 && v.movement == m && let Some(g) = green_start { + if p == 0 + && v.ri >= 1 + && v.movement == m + && let Some(g) = green_start + { let lane = v.route[0]; let n = count.entry(lane).or_insert(0); *n += 1; let since = sim.t - g; if *n > 4 { - if let Some(&t0) = last_cross.get(&lane) && (v.queue_since.is_some() || v.stops > 0) { + if let Some(&t0) = last_cross.get(&lane) + && (v.queue_since.is_some() || v.stops > 0) + { headways.push(sim.t - t0); } } else if *n == 4 { @@ -69,5 +80,7 @@ fn main() { headways.len() ); } - println!("HCM reference: base saturation flow 1,900 pc/h/ln (headway 1.89 s), start-up lost time ~2 s; heavy vehicles reduce flow ~1-2% per 1% trucks."); + println!( + "HCM reference: base saturation flow 1,900 pc/h/ln (headway 1.89 s), start-up lost time ~2 s; heavy vehicles reduce flow ~1-2% per 1% trucks." + ); } diff --git a/examples/sweep.rs b/examples/sweep.rs index abbb716..c295057 100644 --- a/examples/sweep.rs +++ b/examples/sweep.rs @@ -22,10 +22,12 @@ fn main() { let mut jobs = vec![]; for kind in LayoutKind::ALL { for mode in ControlMode::options(kind) { - let ctrls: Vec = if mode == ControlMode::Signal { CtrlKind::ALL[..4].to_vec() } else { vec![CtrlKind::Fixed] }; + let ctrls: Vec = + if mode == ControlMode::Signal { CtrlKind::ALL[..4].to_vec() } else { vec![CtrlKind::Fixed] }; for c in ctrls { jobs.push(std::thread::spawn(move || { - let mut total = 0; let mut first = String::new(); + let mut total = 0; + let mut first = String::new(); for seed in 0..seeds { let mut sim = Sim::new(kind, mode, c, 2000 + seed); sim.check_safety = true; @@ -38,7 +40,11 @@ fn main() { } sim.run_for(1500.0); total += sim.collisions; - if first.is_empty() && let Some(l) = sim.collision_log.first() { first = l.clone(); } + if first.is_empty() + && let Some(l) = sim.collision_log.first() + { + first = l.clone(); + } } (format!("{:<12} {:<28} {:<12}", kind.key(), mode.name(), c.short()), total, first) })); @@ -46,6 +52,12 @@ fn main() { } } let mut sum = 0; - for j in jobs { let (n, t, f) = j.join().unwrap(); sum += t; if t > 0 { println!("{n} {t:>4} {f}"); } } + for j in jobs { + let (n, t, f) = j.join().unwrap(); + sum += t; + if t > 0 { + println!("{n} {t:>4} {f}"); + } + } println!("TOTAL overlap ticks: {sum}"); } diff --git a/fuzz/fuzz_targets/weights.rs b/fuzz/fuzz_targets/weights.rs index c0470c1..a0eefe4 100644 --- a/fuzz/fuzz_targets/weights.rs +++ b/fuzz/fuzz_targets/weights.rs @@ -36,7 +36,14 @@ fuzz_target!(|data: &[u8]| { /// One training update, as online learning makes once its replay memory fills. fn train(agent: &mut Dqn) { let (n_in, n_out) = (agent.online.n_in(), agent.online.n_out()); - let t = Transition { s: vec![1.0; n_in], a: n_out - 1, r: -1.0, s2: vec![0.5; n_in], disc: 0.9, mask2: vec![true; n_out] }; + let t = Transition { + s: vec![1.0; n_in], + a: n_out - 1, + r: -1.0, + s2: vec![0.5; n_in], + disc: 0.9, + mask2: vec![true; n_out], + }; for _ in 0..1000 { agent.push(t.clone()); } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..943f275 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,6 @@ +# Formatting for `cargo fmt`, using only settings stable rustfmt supports. Lines may +# run to 110 characters, and "Max" keeps a short expression on one line whenever it +# fits, which stays close to the compact layout the code had before rustfmt. +edition = "2024" +max_width = 110 +use_small_heuristics = "Max" diff --git a/src/bin/bench.rs b/src/bin/bench.rs index e315f7a..2fc6dc2 100644 --- a/src/bin/bench.rs +++ b/src/bin/bench.rs @@ -6,13 +6,13 @@ //! usage: bench [layout|all] [minutes] [seeds] [demand-scale] [pattern] //! bench sensors [minutes] [seeds] (sensor-package study) -use std::time::Instant; use intersection_lab::control::CtrlKind; -use intersection_lab::{run_scenario, scenario}; +use intersection_lab::sim::ControlMode; use intersection_lab::sim::demand::{DemandConfig, Pattern}; use intersection_lab::sim::network::{LayoutKind, Network}; use intersection_lab::sim::sensors::{Conditions, SensorConfig}; -use intersection_lab::sim::ControlMode; +use intersection_lab::{run_scenario, scenario}; +use std::time::Instant; /// How much does each sensor package help each sensor-driven controller, with ideal /// and field-realistic sensors, across weather, and with failed loop detectors? @@ -32,32 +32,71 @@ fn sensor_study(kind: LayoutKind, minutes: f64, seeds: u64) { ("100% CV only".into(), cv_only), ("+radar IDEAL".into(), rad.clone().ideal()), ]; - table(kind, "Sensor packages (realistic errors, clear day; last column ideal sensors)", &packages, &demand, minutes, seeds); + table( + kind, + "Sensor packages (realistic errors, clear day; last column ideal sensors)", + &packages, + &demand, + minutes, + seeds, + ); let mut conds: Vec<(String, SensorConfig)> = Conditions::ALL .iter() .map(|&c| (c.name().to_string(), SensorConfig { conditions: c, ..rad.clone() })) .collect(); conds.push(("loops, 25% faulty".into(), SensorConfig { loop_fault: 0.25, ..loops })); - conds.push(("fog + 25% faulty".into(), SensorConfig { conditions: Conditions::Fog, loop_fault: 0.25, ..rad })); - table(kind, "Weather and detector faults (loops + camera + radar unless noted)", &conds, &demand, minutes, seeds); + conds.push(( + "fog + 25% faulty".into(), + SensorConfig { conditions: Conditions::Fog, loop_fault: 0.25, ..rad }, + )); + table( + kind, + "Weather and detector faults (loops + camera + radar unless noted)", + &conds, + &demand, + minutes, + seeds, + ); } -fn table(kind: LayoutKind, title: &str, packages: &[(String, SensorConfig)], demand: &DemandConfig, minutes: f64, seeds: u64) { - println!("\n=== {} | {title} | person delay incl. unserved (s), {:.0} min x {} seeds ===", kind.name(), minutes, seeds); +fn table( + kind: LayoutKind, + title: &str, + packages: &[(String, SensorConfig)], + demand: &DemandConfig, + minutes: f64, + seeds: u64, +) { + println!( + "\n=== {} | {title} | person delay incl. unserved (s), {:.0} min x {} seeds ===", + kind.name(), + minutes, + seeds + ); print!("{:<14}", "controller"); for (n, _) in packages { print!(" {:>17}", n); } println!(); - for c in [CtrlKind::Actuated, CtrlKind::MaxPressure, CtrlKind::Predictive, CtrlKind::DeepRl, CtrlKind::Hybrid] { + for c in + [CtrlKind::Actuated, CtrlKind::MaxPressure, CtrlKind::Predictive, CtrlKind::DeepRl, CtrlKind::Hybrid] + { let hs: Vec<_> = packages .iter() .map(|(_, s)| { let (d, s) = (demand.clone(), s.clone()); std::thread::spawn(move || { (0..seeds) - .map(|k| run_scenario(scenario(kind, ControlMode::Signal, c, &d, &s, 1000 + k), 300.0, minutes * 60.0).0.person_delay_all) + .map(|k| { + run_scenario( + scenario(kind, ControlMode::Signal, c, &d, &s, 1000 + k), + 300.0, + minutes * 60.0, + ) + .0 + .person_delay_all + }) .sum::() / seeds as f64 }) @@ -77,7 +116,9 @@ fn main() { let which = args.get(2).map(|s| s.as_str()).unwrap_or("arterial4"); let minutes: f64 = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(20.0); let seeds: u64 = args.get(4).and_then(|s| s.parse().ok()).unwrap_or(3); - for k in LayoutKind::ALL.iter().copied().filter(|k| k.signalized() && (which == "all" || k.key() == which)) { + for k in + LayoutKind::ALL.iter().copied().filter(|k| k.signalized() && (which == "all" || k.key() == which)) + { sensor_study(k, minutes, seeds); } return; @@ -132,7 +173,14 @@ fn main() { let t0 = Instant::now(); let mut acc = [0.0f64; 8]; for seed in 0..seeds { - let sim = scenario(kind, mode, c.unwrap_or(CtrlKind::Fixed), &demand, &sensors, 1000 + seed); + let sim = scenario( + kind, + mode, + c.unwrap_or(CtrlKind::Fixed), + &demand, + &sensors, + 1000 + seed, + ); let (s, coll) = run_scenario(sim, 300.0, minutes * 60.0); acc[0] += s.avg_delay; acc[1] += s.p95_delay; diff --git a/src/bin/train.rs b/src/bin/train.rs index 0d28be6..e0047e3 100644 --- a/src/bin/train.rs +++ b/src/bin/train.rs @@ -16,7 +16,6 @@ //! Environment: `RESUME=1` fine-tunes the saved weights, `SEED=n` changes the random //! seed, and `HIDDEN=128,128` overrides the hidden layer sizes of a new network. -use std::time::Instant; use intersection_lab::control::hybrid::{self, Hybrid}; use intersection_lab::control::rl::{self, RlController}; use intersection_lab::control::{Controller, CtrlKind, make}; @@ -25,6 +24,7 @@ use intersection_lab::sim::network::{LayoutKind, Network}; use intersection_lab::sim::rng::Rng; use intersection_lab::sim::sensors::{Conditions, SensorConfig}; use intersection_lab::sim::{ControlMode, Sim}; +use std::time::Instant; /// A checkpoint is accepted only if its mean pedestrian wait is within this many /// seconds of the predictive controller's on the same scenarios. @@ -179,7 +179,8 @@ fn evaluate(net: &Network, agent: Agent, set: &[Scenario], secs: f64) -> ((f64, let mut agent = agent; let saved = agent.schedule(); agent.set(false, 0.0, 0.0); - let had_model = if let Agent::Hybrid(h) = &mut agent { std::mem::replace(&mut h.has_model, true) } else { false }; + let had_model = + if let Agent::Hybrid(h) = &mut agent { std::mem::replace(&mut h.has_model, true) } else { false }; let (mut pd, mut pw) = (0.0, 0.0); for sc in set { agent.new_episode(net); @@ -237,8 +238,9 @@ fn train(kind: LayoutKind, episodes: u64, secs: f64, which: &str) { let mut base = (f64::INFINITY, hybrid::DEFAULT_ACTION); if hybrid { // The bar the agent must clear to add anything: the best single tuning. - let scores: Vec<((f64, f64), usize)> = - (0..hybrid::N_ACTIONS).map(|a| (evaluate(&net, static_hybrid(&net, a), &val, secs).0, a)).collect(); + let scores: Vec<((f64, f64), usize)> = (0..hybrid::N_ACTIONS) + .map(|a| (evaluate(&net, static_hybrid(&net, a), &val, secs).0, a)) + .collect(); base = scores .iter() .filter(|(s, _)| s.1 <= ped_limit) @@ -251,12 +253,15 @@ fn train(kind: LayoutKind, episodes: u64, secs: f64, which: &str) { scores[hybrid::DEFAULT_ACTION].0.0 ); } - let mut rng = Rng::new(kind as u64 * 7777 + 1 + resume_salt() + if hybrid { 31 } else { 0 } + seed() * 1000); + let mut rng = + Rng::new(kind as u64 * 7777 + 1 + resume_salt() + if hybrid { 31 } else { 0 } + seed() * 1000); let mut best = f64::INFINITY; let resume = std::env::var_os("RESUME").is_some(); let hidden: Vec = std::env::var("HIDDEN") .ok() - .map(|h| h.split(',').map(|x| x.trim().parse().expect("HIDDEN: comma-separated layer sizes")).collect()) + .map(|h| { + h.split(',').map(|x| x.trim().parse().expect("HIDDEN: comma-separated layer sizes")).collect() + }) .unwrap_or_else(|| (if hybrid { hybrid::HIDDEN } else { rl::HIDDEN }).to_vec()); let mut agent = match (hybrid, resume) { (false, true) => Agent::Dqn(Box::new(RlController::load_or_new(&net))), @@ -362,7 +367,14 @@ fn train(kind: LayoutKind, episodes: u64, secs: f64, which: &str) { } /// Score the saved weights, and the baselines, on the held-out test set. -fn report_test(kind: LayoutKind, net: &Network, hybrid: bool, base_action: usize, saved_any: bool, secs: f64) { +fn report_test( + kind: LayoutKind, + net: &Network, + hybrid: bool, + base_action: usize, + saved_any: bool, + secs: f64, +) { let test = test_set(net); let fmt = |r: (f64, f64)| format!("{:.1} / {:.1}", r.0, r.1); let mut line = format!( diff --git a/src/control/actuated.rs b/src/control/actuated.rs index 3c72a98..d2b75a4 100644 --- a/src/control/actuated.rs +++ b/src/control/actuated.rs @@ -76,7 +76,8 @@ impl Controller for Actuated { for leg in &ctx.net.legs { for &lane in &leg.in_lanes { let moves = &ctx.net.links[lane].lane_moves; - let all = moves.iter().all(|&m| matches!(stat(ctx.net, cur, m), Some(MStat::G) | Some(MStat::Rt))); + let all = + moves.iter().all(|&m| matches!(stat(ctx.net, cur, m), Some(MStat::G) | Some(MStat::Rt))); if all { // A faulty station is on max recall: it always extends. let lp = if ctx.view.lanes[lane].fault { ctx.t } else { ctx.view.lanes[lane].last_pulse }; @@ -90,7 +91,9 @@ impl Controller for Actuated { return None; } let i = self.seq.iter().position(|&s| s == cur).unwrap_or(0); - let next = (1..=self.seq.len()).map(|k| self.seq[(i + k) % self.seq.len()]).find(|&j| j != cur && self.calls[j]); + let next = (1..=self.seq.len()) + .map(|k| self.seq[(i + k) % self.seq.len()]) + .find(|&j| j != cur && self.calls[j]); let Some(next) = next else { self.status = "resting (no conflicting calls)".into(); return None; @@ -99,7 +102,10 @@ impl Controller for Actuated { // Transit priority: hold green for an approaching bus. let bus_coming = (0..ctx.net.movements.len()).any(|m| { matches!(stat(ctx.net, cur, m), Some(MStat::G) | Some(MStat::Rt)) - && ctx.view.moves[m].vehicles.iter().any(|o| o.kind == Some(VKind::Bus) && o.eta > 0.0 && o.eta < 8.0) + && ctx.view.moves[m] + .vehicles + .iter() + .any(|o| o.kind == Some(VKind::Bus) && o.eta > 0.0 && o.eta < 8.0) }); if bus_coming && ctx.sig.green_elapsed < maxg + 10.0 { self.status = "transit priority extension".into(); @@ -127,7 +133,13 @@ impl Controller for Actuated { lower_is_better: false, notes: vec![format!( "stages with calls: {}", - self.calls.iter().enumerate().filter(|c| *c.1).map(|c| c.0.to_string()).collect::>().join(", ") + self.calls + .iter() + .enumerate() + .filter(|c| *c.1) + .map(|c| c.0.to_string()) + .collect::>() + .join(", ") )], } } diff --git a/src/control/fixed.rs b/src/control/fixed.rs index e6996f6..09f664b 100644 --- a/src/control/fixed.rs +++ b/src/control/fixed.rs @@ -93,8 +93,10 @@ pub fn webster(net: &Network, demand: &DemandConfig, seq: &[usize]) -> (f64, Vec let moves = &net.links[lane].lane_moves; // A shared lane only discharges when every movement in it is served. let all_served = moves.iter().all(|&m| st.stat[m].is_some() || flows[m] <= 0.0); - let served_prot = moves.iter().any(|&m| matches!(st.stat[m], Some(MStat::G) | Some(MStat::Rt))); - let only_perm = moves.iter().all(|&m| st.stat[m] == Some(MStat::P) && !protected_somewhere(m)); + let served_prot = + moves.iter().any(|&m| matches!(st.stat[m], Some(MStat::G) | Some(MStat::Rt))); + let only_perm = + moves.iter().all(|&m| st.stat[m] == Some(MStat::P) && !protected_somewhere(m)); let has_perm = moves.iter().any(|&m| st.stat[m] == Some(MStat::P) && flows[m] > 0.0); if all_served && (served_prot || only_perm) { let sat = if has_perm { SAT_FLOW * 0.7 } else { SAT_FLOW }; diff --git a/src/control/hybrid.rs b/src/control/hybrid.rs index 2c6bd9b..f15dba3 100644 --- a/src/control/hybrid.rs +++ b/src/control/hybrid.rs @@ -14,9 +14,9 @@ use super::predictive::Predictive; use super::rl::{GAMMA_PER_2S, REWARD_SCALE, RlController}; use super::{Brain, Controller, Ctx}; use crate::rl::dqn::{Dqn, SavedModel, Transition, argmax}; -use serde::{Deserialize, Serialize}; use crate::sim::network::Network; use crate::sim::rng::Rng; +use serde::{Deserialize, Serialize}; use std::path::PathBuf; const FAIRNESS: [f64; 3] = [0.0, 0.004, 0.012]; @@ -127,7 +127,9 @@ impl Hybrid { /// keeps the planner's default tuning (it behaves exactly like the predictive /// controller). pub fn load_or_new(net: &Network) -> Self { - if let Some((mut h, source)) = super::weights::load(&weights_file(net.kind.key()), |txt| Self::from_saved(net, txt)) { + if let Some((mut h, source)) = + super::weights::load(&weights_file(net.kind.key()), |txt| Self::from_saved(net, txt)) + { h.loaded_from = Some(source); return h; } @@ -155,7 +157,12 @@ impl Hybrid { /// Save the base tuning, its score if known, and the network if `with_model`. A /// score that isn't finite is saved as unknown, since JSON has no number for it. - pub fn save(&self, path: &std::path::Path, base_score: Option, with_model: bool) -> std::io::Result<()> { + pub fn save( + &self, + path: &std::path::Path, + base_score: Option, + with_model: bool, + ) -> std::io::Result<()> { let model = with_model.then(|| SavedModel { layout: self.layout.clone(), n_in: self.agent.online.n_in(), @@ -163,7 +170,11 @@ impl Hybrid { episodes: self.episodes, net: self.agent.online.clone(), }); - let sv = SavedHybrid { base_action: self.base_action, base_score: base_score.filter(|s| s.is_finite()), model }; + let sv = SavedHybrid { + base_action: self.base_action, + base_score: base_score.filter(|s| s.is_finite()), + model, + }; std::fs::write(path, serde_json::to_string(&sv).unwrap()) } @@ -202,10 +213,19 @@ impl Controller for Hybrid { if ctx.t >= self.next_decision { self.next_decision = ctx.t + DECISION_DT; let s = RlController::features(ctx); - if let Some((ps, pa, pt)) = self.prev.take() && self.learning { + if let Some((ps, pa, pt)) = self.prev.take() + && self.learning + { let disc = GAMMA_PER_2S.powf((ctx.t - pt) / 2.0) as f32; let mask2 = vec![true; N_ACTIONS]; - self.agent.push(Transition { s: ps, a: pa, r: self.r_acc as f32, s2: s.clone(), disc, mask2 }); + self.agent.push(Transition { + s: ps, + a: pa, + r: self.r_acc as f32, + s2: s.clone(), + disc, + mask2, + }); for _ in 0..4 { self.agent.train_step(); } @@ -235,9 +255,18 @@ impl Controller for Hybrid { let inner = self.planner.brain(); let mut notes = vec![format!("RL tuning now: {}", preset_name(self.action))]; notes.push(match (&self.loaded_from, self.has_model) { - (Some(_), true) => format!("trained network ({} episodes) over base tuning {}", self.episodes, preset_name(self.base_action)), - (Some(_), false) if self.model_rejected => format!("the saved network doesn't fit this layout; running {}", preset_name(self.base_action)), - (Some(_), false) => format!("no network beat the best static tuning; running {}", preset_name(self.base_action)), + (Some(_), true) => format!( + "trained network ({} episodes) over base tuning {}", + self.episodes, + preset_name(self.base_action) + ), + (Some(_), false) if self.model_rejected => format!( + "the saved network doesn't fit this layout; running {}", + preset_name(self.base_action) + ), + (Some(_), false) => { + format!("no network beat the best static tuning; running {}", preset_name(self.base_action)) + } (None, _) if self.learning => "learning online from scratch".into(), (None, _) => "no trained weights: running the planner's default tuning".into(), }); @@ -245,10 +274,19 @@ impl Controller for Hybrid { let mut order: Vec = (0..N_ACTIONS).collect(); // A network can overflow to NaN even with finite weights, so no partial_cmp. order.sort_by(|&a, &b| self.last_q[b].total_cmp(&self.last_q[a])); - let top: Vec = order.iter().take(3).map(|&a| format!("{} ({:.2})", preset_name(a), self.last_q[a])).collect(); + let top: Vec = order + .iter() + .take(3) + .map(|&a| format!("{} ({:.2})", preset_name(a), self.last_q[a])) + .collect(); notes.push(format!("best tunings by Q: {}", top.join("; "))); } notes.extend(inner.notes); - Brain { title: format!("RL-tuned planner · {}", inner.title), bars: inner.bars, lower_is_better: true, notes } + Brain { + title: format!("RL-tuned planner · {}", inner.title), + bars: inner.bars, + lower_is_better: true, + notes, + } } } diff --git a/src/control/maxpressure.rs b/src/control/maxpressure.rs index d6c3db8..231c510 100644 --- a/src/control/maxpressure.rs +++ b/src/control/maxpressure.rs @@ -39,8 +39,14 @@ pub fn stage_pressures(ctx: &Ctx) -> Vec { }; let mo = &ctx.view.moves[m]; // Queue plus vehicles arriving within ~8 s. - let up: f64 = mo.queue + mo.vehicles.iter().filter(|o| !o.queued && o.eta < 8.0).map(|o| o.weight * 0.5).sum::(); - let down = ctx.view.out_queue[net.movements[m].to_leg] / net.legs[net.movements[m].to_leg].n_out as f64; + let up: f64 = mo.queue + + mo.vehicles + .iter() + .filter(|o| !o.queued && o.eta < 8.0) + .map(|o| o.weight * 0.5) + .sum::(); + let down = + ctx.view.out_queue[net.movements[m].to_leg] / net.legs[net.movements[m].to_leg].n_out as f64; *pj += w * (up / lanes_for(net, m).sqrt() - down).max(0.0) * lanes_for(net, m).sqrt(); } for &cw in &net.stages[j].walk { @@ -78,7 +84,13 @@ impl Controller for MaxPressure { fn brain(&self) -> Brain { Brain { title: "Stage pressure (queue upstream − downstream)".into(), - bars: self.names.iter().zip(&self.pressures).enumerate().map(|(i, (n, p))| (n.clone(), *p, i == self.cur)).collect(), + bars: self + .names + .iter() + .zip(&self.pressures) + .enumerate() + .map(|(i, (n, p))| (n.clone(), *p, i == self.cur)) + .collect(), lower_is_better: false, notes: vec![], } diff --git a/src/control/mod.rs b/src/control/mod.rs index e211bff..4661e4d 100644 --- a/src/control/mod.rs +++ b/src/control/mod.rs @@ -77,8 +77,14 @@ pub enum CtrlKind { } impl CtrlKind { - pub const ALL: [CtrlKind; 6] = - [CtrlKind::Fixed, CtrlKind::Actuated, CtrlKind::MaxPressure, CtrlKind::Predictive, CtrlKind::DeepRl, CtrlKind::Hybrid]; + pub const ALL: [CtrlKind; 6] = [ + CtrlKind::Fixed, + CtrlKind::Actuated, + CtrlKind::MaxPressure, + CtrlKind::Predictive, + CtrlKind::DeepRl, + CtrlKind::Hybrid, + ]; pub fn name(self) -> &'static str { match self { CtrlKind::Fixed => "Fixed-time (Webster)", @@ -104,12 +110,24 @@ impl CtrlKind { } pub fn description(self) -> &'static str { match self { - CtrlKind::Fixed => "Pre-timed cycle. Cycle length and splits from Webster's formula using historical demand. Uses no sensors.", - CtrlKind::Actuated => "Fully actuated: min green, extends on loop actuations within the passage time, gaps out or maxes out, skips stages with no calls. Bus priority extension when a camera/CV sees a bus.", - CtrlKind::MaxPressure => "Varaiya's throughput-optimal max-pressure: serves the stage with the largest upstream-minus-downstream queue pressure. Needs queue counts (camera).", - CtrlKind::Predictive => "Rolling-horizon, schedule-driven control in the spirit of SURTRAC: predicts every detected vehicle's arrival, simulates candidate signal schedules 90 s ahead and picks the one minimizing person-delay. Benefits most from long-range radar / connected vehicles.", - CtrlKind::DeepRl => "Double DQN neural network trained by reinforcement learning (reward = negative person delay) on randomized demand. Observes queues, predicted arrivals, waits and pedestrian calls. Can keep learning online.", - CtrlKind::Hybrid => "The predictive planner makes every signal decision; a reinforcement-learning agent re-tunes it every 10 s (fairness weight, hold/switch bias, max green) from the traffic it sees. Keeps the planner's safety; learns when to deviate from its defaults.", + CtrlKind::Fixed => { + "Pre-timed cycle. Cycle length and splits from Webster's formula using historical demand. Uses no sensors." + } + CtrlKind::Actuated => { + "Fully actuated: min green, extends on loop actuations within the passage time, gaps out or maxes out, skips stages with no calls. Bus priority extension when a camera/CV sees a bus." + } + CtrlKind::MaxPressure => { + "Varaiya's throughput-optimal max-pressure: serves the stage with the largest upstream-minus-downstream queue pressure. Needs queue counts (camera)." + } + CtrlKind::Predictive => { + "Rolling-horizon, schedule-driven control in the spirit of SURTRAC: predicts every detected vehicle's arrival, simulates candidate signal schedules 90 s ahead and picks the one minimizing person-delay. Benefits most from long-range radar / connected vehicles." + } + CtrlKind::DeepRl => { + "Double DQN neural network trained by reinforcement learning (reward = negative person delay) on randomized demand. Observes queues, predicted arrivals, waits and pedestrian calls. Can keep learning online." + } + CtrlKind::Hybrid => { + "The predictive planner makes every signal decision; a reinforcement-learning agent re-tunes it every 10 s (fairness weight, hold/switch bias, max green) from the traffic it sees. Keeps the planner's safety; learns when to deviate from its defaults." + } } } } diff --git a/src/control/predictive.rs b/src/control/predictive.rs index 2086509..445e922 100644 --- a/src/control/predictive.rs +++ b/src/control/predictive.rs @@ -76,7 +76,8 @@ impl Predictive { let continuing = if bi == 0 { ctx.sig.green_elapsed > 2.0 } else { - stat(net, blocks[bi - 1].stage, m).is_some() && (blocks[bi - 1].end - b.start).abs() < 1e-6 + stat(net, blocks[bi - 1].stage, m).is_some() + && (blocks[bi - 1].end - b.start).abs() < 1e-6 }; let start = b.start + if continuing { 0.0 } else { 2.0 }; let leaving = blocks.get(bi + 1).is_some_and(|n| stat(net, n.stage, m).is_none()); @@ -236,7 +237,11 @@ impl Controller for Predictive { } } let h_eff: Vec> = (0..ns) - .map(|k| (0..nm).map(|m| stat(net, k, m).map(|s| Self::headway(s) / lanes_for(net, m)).unwrap_or(2.0)).collect()) + .map(|k| { + (0..nm) + .map(|m| stat(net, k, m).map(|s| Self::headway(s) / lanes_for(net, m)).unwrap_or(2.0)) + .collect() + }) .collect(); let min_g = ctx.sig.min_green; let xs = [0.0, 2.0, 4.0, 7.0, 10.0, 15.0, 20.0, 30.0]; diff --git a/src/control/rl.rs b/src/control/rl.rs index ac59465..2f68a27 100644 --- a/src/control/rl.rs +++ b/src/control/rl.rs @@ -105,7 +105,9 @@ impl RlController { } pub fn load_or_new(net: &Network) -> Self { - if let Some((mut c, source)) = super::weights::load(&weights_file(net.kind.key()), |txt| Self::from_saved(net, txt)) { + if let Some((mut c, source)) = + super::weights::load(&weights_file(net.kind.key()), |txt| Self::from_saved(net, txt)) + { c.loaded_from = Some(source); return c; } @@ -120,7 +122,9 @@ impl RlController { /// doesn't parse or its network doesn't fit this layout. Weights files can be /// damaged or edited by hand, so a network is checked before it runs. pub fn from_saved(net: &Network, txt: &str) -> Option { - let m = serde_json::from_str::(txt).ok().filter(|m| m.fits(n_inputs(net), net.stages.len()))?; + let m = serde_json::from_str::(txt) + .ok() + .filter(|m| m.fits(n_inputs(net), net.stages.len()))?; let mut c = Self::with_agent(net, Dqn::from_net(m.net, Rng::new(7))); c.episodes = m.episodes; Some(c) @@ -178,7 +182,12 @@ impl RlController { for m in 0..net.movements.len() { if stat(net, j, m).is_some() { let mo = &ctx.view.moves[m]; - d += mo.queue + mo.vehicles.iter().filter(|o| !o.queued && o.eta < 12.0).map(|o| o.weight).sum::(); + d += mo.queue + + mo.vehicles + .iter() + .filter(|o| !o.queued && o.eta < 12.0) + .map(|o| o.weight) + .sum::(); } } d + net.stages[j].walk.iter().map(|&cw| ctx.view.ped_waiting[cw] as f64).sum::() @@ -250,7 +259,9 @@ impl Controller for RlController { fn step(&mut self, ctx: &Ctx) -> Option { self.r_acc -= ctx.reward_delay / REWARD_SCALE; - if let Some(e) = self.expert.as_mut() && let Some(j) = e.step(ctx) { + if let Some(e) = self.expert.as_mut() + && let Some(j) = e.step(ctx) + { self.expert_wish = Some((j, ctx.t)); } if ctx.sig.phase != Phase::Green || !ctx.can_switch || ctx.t < self.next_decision { @@ -260,9 +271,18 @@ impl Controller for RlController { self.cur = ctx.sig.stage; let s = Self::features(ctx); let mask = Self::shield(ctx); - if let Some((ps, pa, pt)) = self.prev.take() && self.learning { + if let Some((ps, pa, pt)) = self.prev.take() + && self.learning + { let disc = GAMMA_PER_2S.powf((ctx.t - pt) / 2.0) as f32; - self.agent.push(Transition { s: ps, a: pa, r: self.r_acc as f32, s2: s.clone(), disc, mask2: mask.clone() }); + self.agent.push(Transition { + s: ps, + a: pa, + r: self.r_acc as f32, + s2: s.clone(), + disc, + mask2: mask.clone(), + }); self.since_train += 1; if self.since_train >= self.train_every { self.since_train = 0; @@ -282,7 +302,11 @@ impl Controller for RlController { } else if self.learning && self.rng.chance(self.epsilon) { // Exploration over shield-allowed actions, biased toward holding. let allowed: Vec = (0..mask.len()).filter(|&i| mask[i]).collect(); - if mask[self.cur] && self.rng.chance(0.5) { self.cur } else { allowed[self.rng.below(allowed.len())] } + if mask[self.cur] && self.rng.chance(0.5) { + self.cur + } else { + allowed[self.rng.below(allowed.len())] + } } else { greedy }; @@ -293,7 +317,8 @@ impl Controller for RlController { } fn brain(&self) -> Brain { - let best = if self.last_q.is_empty() { usize::MAX } else { argmax_masked(&self.last_q, &self.last_mask) }; + let best = + if self.last_q.is_empty() { usize::MAX } else { argmax_masked(&self.last_q, &self.last_mask) }; let mut notes = vec![match &self.loaded_from { Some(p) => format!("pretrained weights: {} episodes ({})", self.episodes, short_path(p)), None => "no pretrained weights found: learning from scratch".into(), @@ -315,7 +340,11 @@ impl Controller for RlController { .iter() .enumerate() .map(|(i, q)| { - let mut label = if i == self.cur { format!("hold {}", self.names[i]) } else { format!("→ {}", self.names[i]) }; + let mut label = if i == self.cur { + format!("hold {}", self.names[i]) + } else { + format!("→ {}", self.names[i]) + }; if self.last_mask.get(i) == Some(&false) { label.push_str(" (shielded)"); } diff --git a/src/control/weights.rs b/src/control/weights.rs index 41927d4..9e0c8b7 100644 --- a/src/control/weights.rs +++ b/src/control/weights.rs @@ -42,12 +42,17 @@ pub fn user_dir() -> Option { /// Weights file `file` as `parse` loads it from the first copy it accepts, and where /// that copy came from. pub fn load(file: &str, parse: impl FnMut(&str) -> Option) -> Option<(T, String)> { - let dirs: Vec = std::iter::once(PathBuf::from("weights")).chain(user_dir().map(|d| d.join("weights"))).collect(); + let dirs: Vec = + std::iter::once(PathBuf::from("weights")).chain(user_dir().map(|d| d.join("weights"))).collect(); load_from(&dirs, file, parse) } /// Like [`load`], but looking in `dirs`, in order, before the built-in copy. -pub fn load_from(dirs: &[impl AsRef], file: &str, mut parse: impl FnMut(&str) -> Option) -> Option<(T, String)> { +pub fn load_from( + dirs: &[impl AsRef], + file: &str, + mut parse: impl FnMut(&str) -> Option, +) -> Option<(T, String)> { for p in dirs.iter().map(|d| d.as_ref().join(file)) { if let Ok(txt) = std::fs::read_to_string(&p) && let Some(v) = parse(&txt) diff --git a/src/gui/bench.rs b/src/gui/bench.rs index 33b2f81..6d6e945 100644 --- a/src/gui/bench.rs +++ b/src/gui/bench.rs @@ -8,11 +8,11 @@ use crate::control::CtrlKind; use crate::sim::coop::{GapPolicy, Scheduling}; -use crate::sim::{ControlMode, Sim}; use crate::sim::demand::DemandConfig; use crate::sim::network::LayoutKind; use crate::sim::sensors::SensorConfig; use crate::sim::stats::Summary; +use crate::sim::{ControlMode, Sim}; /// Arrival streams (seeds) per strategy, and the length of each run. const SEEDS: u64 = 3; @@ -150,7 +150,12 @@ impl Bench { pub fn start(title: String, kind: LayoutKind, demand: &DemandConfig, jobs: Vec) -> Self { let total = jobs.len(); - let queue = web::Queue { kind, demand: demand.clone(), jobs: jobs.into_iter().enumerate().collect(), current: None }; + let queue = web::Queue { + kind, + demand: demand.clone(), + jobs: jobs.into_iter().enumerate().collect(), + current: None, + }; Bench { title, total, rows: (0..total).map(|_| None).collect(), queue } } diff --git a/src/gui/charts.rs b/src/gui/charts.rs index 6f4469e..d2860e2 100644 --- a/src/gui/charts.rs +++ b/src/gui/charts.rs @@ -35,13 +35,26 @@ fn nice_max(v: f64) -> f64 { } /// Line chart of one or more series sharing the x axis. -pub fn line_chart(ui: &mut Ui, title: &str, unit: &str, xs: &[f64], series: &[(&str, Vec, Color32)], height: f32) { +pub fn line_chart( + ui: &mut Ui, + title: &str, + unit: &str, + xs: &[f64], + series: &[(&str, Vec, Color32)], + height: f32, +) { ui.label(egui::RichText::new(title).strong().size(12.5)); let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), height), Sense::hover()); let p = ui.painter_at(rect); p.rect_filled(rect, 4.0, Color32::from_rgb(24, 26, 31)); if xs.len() < 2 { - p.text(rect.center(), Align2::CENTER_CENTER, "collecting data…", FontId::proportional(11.0), AXIS_TXT); + p.text( + rect.center(), + Align2::CENTER_CENTER, + "collecting data…", + FontId::proportional(11.0), + AXIS_TXT, + ); return; } let plot = Rect::from_min_max(rect.min + vec2(38.0, 6.0), rect.max - vec2(6.0, 17.0)); @@ -53,11 +66,29 @@ pub fn line_chart(ui: &mut Ui, title: &str, unit: &str, xs: &[f64], series: &[(& let y = ymax * k as f64 / 4.0; let yy = ty(y); p.line_segment([pos2(plot.left(), yy), pos2(plot.right(), yy)], Stroke::new(1.0, GRID)); - p.text(pos2(plot.left() - 4.0, yy), Align2::RIGHT_CENTER, fmt_num(y), FontId::proportional(TXT), AXIS_TXT); + p.text( + pos2(plot.left() - 4.0, yy), + Align2::RIGHT_CENTER, + fmt_num(y), + FontId::proportional(TXT), + AXIS_TXT, + ); } let mins = |x: f64| format!("{:.0}m", x / 60.0); - p.text(pos2(plot.left(), plot.bottom() + 2.0), Align2::LEFT_TOP, mins(x0), FontId::proportional(TXT), AXIS_TXT); - p.text(pos2(plot.right(), plot.bottom() + 2.0), Align2::RIGHT_TOP, mins(x1), FontId::proportional(TXT), AXIS_TXT); + p.text( + pos2(plot.left(), plot.bottom() + 2.0), + Align2::LEFT_TOP, + mins(x0), + FontId::proportional(TXT), + AXIS_TXT, + ); + p.text( + pos2(plot.right(), plot.bottom() + 2.0), + Align2::RIGHT_TOP, + mins(x1), + FontId::proportional(TXT), + AXIS_TXT, + ); for (_, ys, col) in series { let pts: Vec = xs.iter().zip(ys).map(|(x, y)| pos2(tx(*x), ty(*y))).collect(); p.add(egui::Shape::line(pts, Stroke::new(1.6, *col))); @@ -66,20 +97,35 @@ pub fn line_chart(ui: &mut Ui, title: &str, unit: &str, xs: &[f64], series: &[(& let mut lx = plot.left() + 4.0; for (name, ys, col) in series { let last = ys.last().copied().unwrap_or(0.0); - let r = p.text(pos2(lx, plot.top()), Align2::LEFT_TOP, format!("● {name} {}{unit}", fmt_num(last)), FontId::proportional(TXT), *col); + let r = p.text( + pos2(lx, plot.top()), + Align2::LEFT_TOP, + format!("● {name} {}{unit}", fmt_num(last)), + FontId::proportional(TXT), + *col, + ); lx = r.right() + 10.0; } - if let Some(h) = resp.hover_pos() && plot.contains(h) { + if let Some(h) = resp.hover_pos() + && plot.contains(h) + { let xv = x0 + ((h.x - plot.left()) / plot.width()) as f64 * (x1 - x0); let i = xs.partition_point(|&x| x < xv).min(xs.len() - 1); - p.line_segment([pos2(tx(xs[i]), plot.top()), pos2(tx(xs[i]), plot.bottom())], Stroke::new(1.0, Color32::from_white_alpha(40))); + p.line_segment( + [pos2(tx(xs[i]), plot.top()), pos2(tx(xs[i]), plot.bottom())], + Stroke::new(1.0, Color32::from_white_alpha(40)), + ); let mut txt = format!("t={:.1} min", xs[i] / 60.0); for (name, ys, _) in series { txt.push_str(&format!("\n{name}: {}{unit}", fmt_num(ys[i]))); } let pos = pos2((tx(xs[i]) + 8.0).min(plot.right() - 90.0), plot.top() + 14.0); let g = p.layout_no_wrap(txt, FontId::proportional(TXT), Color32::WHITE); - p.rect_filled(Rect::from_min_size(pos - vec2(3.0, 2.0), g.size() + vec2(6.0, 4.0)), 3.0, Color32::from_black_alpha(200)); + p.rect_filled( + Rect::from_min_size(pos - vec2(3.0, 2.0), g.size() + vec2(6.0, 4.0)), + 3.0, + Color32::from_black_alpha(200), + ); p.galley(pos, g, Color32::WHITE); } } @@ -104,7 +150,8 @@ pub fn hbars(ui: &mut Ui, rows: &[(String, f64, bool)], unit: &str, lower_is_bet let vmin = finite.iter().cloned().fold(f64::INFINITY, f64::min); let neg = vmin < 0.0; let row_h = 17.0; - let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), rows.len() as f32 * row_h + 2.0), Sense::hover()); + let (rect, _) = + ui.allocate_exact_size(vec2(ui.available_width(), rows.len() as f32 * row_h + 2.0), Sense::hover()); let p = ui.painter_at(rect); let label_w = (rect.width() * 0.42).min(170.0); let best = if lower_is_better { @@ -114,15 +161,20 @@ pub fn hbars(ui: &mut Ui, rows: &[(String, f64, bool)], unit: &str, lower_is_bet }; for (i, (name, v, hl)) in rows.iter().enumerate() { let y = rect.top() + i as f32 * row_h; - let bar_area = Rect::from_min_max(pos2(rect.left() + label_w, y + 2.0), pos2(rect.right() - 44.0, y + row_h - 2.0)); - p.text(pos2(rect.left(), y + row_h / 2.0), Align2::LEFT_CENTER, name, FontId::proportional(TXT), if *hl { Color32::WHITE } else { AXIS_TXT }); + let bar_area = Rect::from_min_max( + pos2(rect.left() + label_w, y + 2.0), + pos2(rect.right() - 44.0, y + row_h - 2.0), + ); + p.text( + pos2(rect.left(), y + row_h / 2.0), + Align2::LEFT_CENTER, + name, + FontId::proportional(TXT), + if *hl { Color32::WHITE } else { AXIS_TXT }, + ); p.rect_filled(bar_area, 2.0, Color32::from_rgb(30, 33, 39)); if v.is_finite() { - let frac = if neg { - ((v - vmin) / (vmax - vmin).max(1e-9)) as f32 - } else { - (v / vmax) as f32 - }; + let frac = if neg { ((v - vmin) / (vmax - vmin).max(1e-9)) as f32 } else { (v / vmax) as f32 }; let w = bar_area.width() * frac.clamp(0.0, 1.0); let col = if *hl { Color32::from_rgb(255, 200, 70) @@ -132,9 +184,21 @@ pub fn hbars(ui: &mut Ui, rows: &[(String, f64, bool)], unit: &str, lower_is_bet Color32::from_rgb(90, 140, 220) }; p.rect_filled(Rect::from_min_size(bar_area.min, vec2(w.max(1.0), bar_area.height())), 2.0, col); - p.text(pos2(rect.right(), y + row_h / 2.0), Align2::RIGHT_CENTER, format!("{}{unit}", fmt_num(*v)), FontId::proportional(TXT), Color32::WHITE); + p.text( + pos2(rect.right(), y + row_h / 2.0), + Align2::RIGHT_CENTER, + format!("{}{unit}", fmt_num(*v)), + FontId::proportional(TXT), + Color32::WHITE, + ); } else { - p.text(pos2(rect.right(), y + row_h / 2.0), Align2::RIGHT_CENTER, "—", FontId::proportional(TXT), AXIS_TXT); + p.text( + pos2(rect.right(), y + row_h / 2.0), + Align2::RIGHT_CENTER, + "—", + FontId::proportional(TXT), + AXIS_TXT, + ); } } } @@ -151,16 +215,38 @@ pub fn histogram(ui: &mut Ui, bins: &[u32], bin_w: f64, height: f32) { let h = plot.height() * b as f32 / max.max(1.0); let x = plot.left() + i as f32 * bw; let col = super::render::delay_color(i as f64 * bin_w + bin_w / 2.0); - p.rect_filled(Rect::from_min_max(pos2(x + 1.0, plot.bottom() - h), pos2(x + bw - 1.0, plot.bottom())), 2.0, col); + p.rect_filled( + Rect::from_min_max(pos2(x + 1.0, plot.bottom() - h), pos2(x + bw - 1.0, plot.bottom())), + 2.0, + col, + ); if i % 2 == 0 { - let label = if i == bins.len() - 1 { format!("{:.0}+", i as f64 * bin_w) } else { format!("{:.0}", i as f64 * bin_w) }; - p.text(pos2(x + bw / 2.0, plot.bottom() + 2.0), Align2::CENTER_TOP, label, FontId::proportional(TXT), AXIS_TXT); + let label = if i == bins.len() - 1 { + format!("{:.0}+", i as f64 * bin_w) + } else { + format!("{:.0}", i as f64 * bin_w) + }; + p.text( + pos2(x + bw / 2.0, plot.bottom() + 2.0), + Align2::CENTER_TOP, + label, + FontId::proportional(TXT), + AXIS_TXT, + ); } } - if let Some(h) = resp.hover_pos() && plot.contains(h) { + if let Some(h) = resp.hover_pos() + && plot.contains(h) + { let i = (((h.x - plot.left()) / bw) as usize).min(bins.len() - 1); let pct = if total > 0 { 100.0 * bins[i] as f32 / total as f32 } else { 0.0 }; - p.text(pos2(plot.right(), plot.top()), Align2::RIGHT_TOP, format!("{:.0}–{:.0}s: {} veh ({:.0}%)", i as f64 * bin_w, (i + 1) as f64 * bin_w, bins[i], pct), FontId::proportional(TXT), Color32::WHITE); + p.text( + pos2(plot.right(), plot.top()), + Align2::RIGHT_TOP, + format!("{:.0}–{:.0}s: {} veh ({:.0}%)", i as f64 * bin_w, (i + 1) as f64 * bin_w, bins[i], pct), + FontId::proportional(TXT), + Color32::WHITE, + ); } } @@ -184,21 +270,49 @@ pub fn pet_histogram(ui: &mut Ui, bins: &[u32], bin_w: f64, height: f32) { let h = (plot.height() * b as f32 / max).min(plot.height()); let x = plot.left() + i as f32 * bw; if i == last && b as f32 > max { - p.text(pos2(x + bw / 2.0, plot.top() + 2.0), Align2::CENTER_TOP, "▲", FontId::proportional(TXT), AXIS_TXT); + p.text( + pos2(x + bw / 2.0, plot.top() + 2.0), + Align2::CENTER_TOP, + "▲", + FontId::proportional(TXT), + AXIS_TXT, + ); } let mid = (i as f64 + 0.5) * bin_w; let col = super::render::delay_color(90.0 * (1.0 - (mid / 2.0).min(1.0))); - p.rect_filled(Rect::from_min_max(pos2(x + 1.0, plot.bottom() - h), pos2(x + bw - 1.0, plot.bottom())), 2.0, col); + p.rect_filled( + Rect::from_min_max(pos2(x + 1.0, plot.bottom() - h), pos2(x + bw - 1.0, plot.bottom())), + 2.0, + col, + ); let at = i as f64 * bin_w; if (at - at.round()).abs() < 1e-9 { - p.text(pos2(x, plot.bottom() + 2.0), Align2::CENTER_TOP, format!("{at:.0} s"), FontId::proportional(TXT), AXIS_TXT); + p.text( + pos2(x, plot.bottom() + 2.0), + Align2::CENTER_TOP, + format!("{at:.0} s"), + FontId::proportional(TXT), + AXIS_TXT, + ); } } - if let Some(h) = resp.hover_pos() && plot.contains(h) { + if let Some(h) = resp.hover_pos() + && plot.contains(h) + { let i = (((h.x - plot.left()) / bw) as usize).min(bins.len() - 1); let pct = 100.0 * bins[i] as f32 / total as f32; - let range = if i + 1 == bins.len() { format!("{:.2} s or more", i as f64 * bin_w) } else { format!("{:.2}–{:.2} s", i as f64 * bin_w, (i + 1) as f64 * bin_w) }; - p.text(pos2(plot.right(), plot.top()), Align2::RIGHT_TOP, format!("{range}: {} ({pct:.1}%)", bins[i]), FontId::proportional(TXT), Color32::WHITE); + let range = if i + 1 == bins.len() { + format!("{:.2} s or more", i as f64 * bin_w) + } else { + format!("{:.2}–{:.2} s", i as f64 * bin_w, (i + 1) as f64 * bin_w) + }; + p.text( + pos2(plot.right(), plot.top()), + Align2::RIGHT_TOP, + format!("{range}: {} ({pct:.1}%)", bins[i]), + FontId::proportional(TXT), + Color32::WHITE, + ); } } @@ -211,7 +325,10 @@ pub fn crossing_timeline(ui: &mut Ui, sim: &crate::sim::Sim, past: f64, ahead: f let net = &sim.net; let rows: Vec = c.ring_order().concat(); let row_h = 14.0; - let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), rows.len().max(1) as f32 * row_h + 21.0), Sense::hover()); + let (rect, _) = ui.allocate_exact_size( + vec2(ui.available_width(), rows.len().max(1) as f32 * row_h + 21.0), + Sense::hover(), + ); let p = ui.painter_at(rect); p.rect_filled(rect, 4.0, Color32::from_rgb(22, 24, 28)); let label_w = 150.0; @@ -220,21 +337,40 @@ pub fn crossing_timeline(ui: &mut Ui, sim: &crate::sim::Sim, past: f64, ahead: f let (t0, t1) = (now - past, now + ahead); let tx = |t: f64| plot.left() + ((t - t0) / (t1 - t0)) as f32 * plot.width(); let row_of = |m: usize| rows.iter().position(|&x| x == m); - let color = |m: usize| if net.movements[m].turn == crate::sim::network::Turn::L { SERIES[1] } else { SERIES[0] }; + let color = + |m: usize| if net.movements[m].turn == crate::sim::network::Turn::L { SERIES[1] } else { SERIES[0] }; let open = c.open_movements(); for (i, &m) in rows.iter().enumerate() { let y = plot.top() + i as f32 * row_h; let mv = &net.movements[m]; let name = format!("{} {}", net.legs[mv.leg].compass.bound(), mv.turn.name().to_lowercase()); let col = if open.contains(&m) { Color32::WHITE } else { AXIS_TXT }; - p.text(pos2(rect.left() + 6.0, y + row_h / 2.0), Align2::LEFT_CENTER, name, FontId::proportional(TXT), col); - p.line_segment([pos2(plot.left(), y + row_h - 0.5), pos2(plot.right(), y + row_h - 0.5)], Stroke::new(1.0, Color32::from_rgb(34, 37, 43))); + p.text( + pos2(rect.left() + 6.0, y + row_h / 2.0), + Align2::LEFT_CENTER, + name, + FontId::proportional(TXT), + col, + ); + p.line_segment( + [pos2(plot.left(), y + row_h - 0.5), pos2(plot.right(), y + row_h - 0.5)], + Stroke::new(1.0, Color32::from_rgb(34, 37, 43)), + ); } let mut t = (t1 / 30.0).floor() * 30.0; while t >= t0 { if t >= 0.0 { - p.line_segment([pos2(tx(t), plot.top()), pos2(tx(t), plot.bottom())], Stroke::new(1.0, Color32::from_rgb(40, 44, 50))); - p.text(pos2(tx(t), plot.bottom() + 1.0), Align2::CENTER_TOP, format!("{:.0}:{:02.0}", (t / 60.0).floor(), t % 60.0), FontId::proportional(TXT), AXIS_TXT); + p.line_segment( + [pos2(tx(t), plot.top()), pos2(tx(t), plot.bottom())], + Stroke::new(1.0, Color32::from_rgb(40, 44, 50)), + ); + p.text( + pos2(tx(t), plot.bottom() + 1.0), + Align2::CENTER_TOP, + format!("{:.0}:{:02.0}", (t / 60.0).floor(), t % 60.0), + FontId::proportional(TXT), + AXIS_TXT, + ); } t -= 30.0; } @@ -246,7 +382,11 @@ pub fn crossing_timeline(ui: &mut Ui, sim: &crate::sim::Sim, past: f64, ahead: f let Some(i) = row_of(b.movement) else { continue }; let y = plot.top() + i as f32 * row_h; let (a, e) = (tx(b.start.max(t0)), tx(b.end.min(t1))); - p.rect_filled(Rect::from_min_max(pos2(a - 1.0, y + 1.5), pos2(e.max(a) + 1.0, y + row_h - 2.5)), 2.0, color(b.movement).gamma_multiply(0.45)); + p.rect_filled( + Rect::from_min_max(pos2(a - 1.0, y + 1.5), pos2(e.max(a) + 1.0, y + row_h - 2.5)), + 2.0, + color(b.movement).gamma_multiply(0.45), + ); } for &(t, m) in &c.crossings { if t < t0 || t > t1 { @@ -260,7 +400,13 @@ pub fn crossing_timeline(ui: &mut Ui, sim: &crate::sim::Sim, past: f64, ahead: f } let x = tx(now); p.line_segment([pos2(x, plot.top()), pos2(x, plot.bottom())], Stroke::new(1.5, super::render::SCHEDULED)); - p.text(pos2(x + 3.0, plot.top()), Align2::LEFT_TOP, "now", FontId::proportional(TXT), super::render::SCHEDULED); + p.text( + pos2(x + 3.0, plot.top()), + Align2::LEFT_TOP, + "now", + FontId::proportional(TXT), + super::render::SCHEDULED, + ); } /// Signal timing diagram: one row per stage over the last `window` seconds. @@ -268,11 +414,18 @@ pub fn timeline(ui: &mut Ui, sim: &crate::sim::Sim, window: f64) { let net = &sim.net; let n = net.stages.len().max(1); let row_h = 14.0; - let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), n as f32 * row_h + 21.0), Sense::hover()); + let (rect, _) = + ui.allocate_exact_size(vec2(ui.available_width(), n as f32 * row_h + 21.0), Sense::hover()); let p = ui.painter_at(rect); p.rect_filled(rect, 4.0, Color32::from_rgb(22, 24, 28)); if !sim.signalized() { - p.text(rect.center(), Align2::CENTER_CENTER, format!("{} — no signal timing", sim.mode.name()), FontId::proportional(12.0), AXIS_TXT); + p.text( + rect.center(), + Align2::CENTER_CENTER, + format!("{} — no signal timing", sim.mode.name()), + FontId::proportional(12.0), + AXIS_TXT, + ); return; } let label_w = 150.0; @@ -283,15 +436,33 @@ pub fn timeline(ui: &mut Ui, sim: &crate::sim::Sim, window: f64) { for (i, st) in net.stages.iter().enumerate() { let y = plot.top() + i as f32 * row_h; let cur = i == sim.signal.stage; - p.text(pos2(rect.left() + 6.0, y + row_h / 2.0), Align2::LEFT_CENTER, &st.name, FontId::proportional(TXT), if cur { Color32::WHITE } else { AXIS_TXT }); - p.line_segment([pos2(plot.left(), y + row_h - 0.5), pos2(plot.right(), y + row_h - 0.5)], Stroke::new(1.0, Color32::from_rgb(34, 37, 43))); + p.text( + pos2(rect.left() + 6.0, y + row_h / 2.0), + Align2::LEFT_CENTER, + &st.name, + FontId::proportional(TXT), + if cur { Color32::WHITE } else { AXIS_TXT }, + ); + p.line_segment( + [pos2(plot.left(), y + row_h - 0.5), pos2(plot.right(), y + row_h - 0.5)], + Stroke::new(1.0, Color32::from_rgb(34, 37, 43)), + ); } let k = (window / 30.0) as i64; for j in 0..=k { let t = (t1 / 30.0).floor() * 30.0 - j as f64 * 30.0; if t >= t0 { - p.line_segment([pos2(tx(t), plot.top()), pos2(tx(t), plot.bottom())], Stroke::new(1.0, Color32::from_rgb(40, 44, 50))); - p.text(pos2(tx(t), plot.bottom() + 1.0), Align2::CENTER_TOP, format!("{:.0}:{:02.0}", (t / 60.0).floor(), t % 60.0), FontId::proportional(TXT), AXIS_TXT); + p.line_segment( + [pos2(tx(t), plot.top()), pos2(tx(t), plot.bottom())], + Stroke::new(1.0, Color32::from_rgb(40, 44, 50)), + ); + p.text( + pos2(tx(t), plot.bottom() + 1.0), + Align2::CENTER_TOP, + format!("{:.0}:{:02.0}", (t / 60.0).floor(), t % 60.0), + FontId::proportional(TXT), + AXIS_TXT, + ); } } // Each stage's green in its own color, then the yellow change and all-red clearance @@ -308,7 +479,11 @@ pub fn timeline(ui: &mut Ui, sim: &crate::sim::Sim, window: f64) { Phase::Yellow => (Color32::from_rgb(230, 190, 40), 1.0), Phase::AllRed => (Color32::from_rgb(220, 60, 50), 1.0), }; - p.rect_filled(Rect::from_min_max(pos2(a, y + 1.5), pos2(b.max(a + 1.0), y + row_h - 2.5)), round, col); + p.rect_filled( + Rect::from_min_max(pos2(a, y + 1.5), pos2(b.max(a + 1.0), y + row_h - 2.5)), + round, + col, + ); } } @@ -324,7 +499,12 @@ pub fn sparkline(ui: &mut Ui, ys: &[f32], height: f32, col: Color32) { let pts: Vec = ys .iter() .enumerate() - .map(|(i, y)| pos2(rect.left() + i as f32 / (ys.len() - 1) as f32 * rect.width(), rect.bottom() - y / max * (rect.height() - 4.0) - 2.0)) + .map(|(i, y)| { + pos2( + rect.left() + i as f32 / (ys.len() - 1) as f32 * rect.width(), + rect.bottom() - y / max * (rect.height() - 4.0) - 2.0, + ) + }) .collect(); p.add(egui::Shape::line(pts, Stroke::new(1.2, col))); } diff --git a/src/gui/mod.rs b/src/gui/mod.rs index d39622f..7db00b1 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -18,8 +18,8 @@ use crate::sim::stats::{HIST_BIN, Summary, los}; use crate::sim::vtypes::{ALL_KINDS, VKind}; use crate::sim::world::{CorridorSummary, SPACING, World}; use crate::sim::{ControlMode, LeftMode, Sim}; -use egui::{Color32, RichText, Sense, Ui, vec2}; use bench::{Bench, BenchRow, Job}; +use egui::{Color32, RichText, Sense, Ui, vec2}; use render::{Camera, ColorBy, DrawCtx, Overlays, Scene}; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; @@ -203,7 +203,10 @@ impl App { self.twin_sched = Scheduling::Batches; } (None, None) if b == "fcfs" => self.twin_sched = Scheduling::FirstCome, - (None, None) => self.twin_mode = if b == "allway" { ControlMode::AllWayStop } else { ControlMode::TwoWayStop }, + (None, None) => { + self.twin_mode = + if b == "allway" { ControlMode::AllWayStop } else { ControlMode::TwoWayStop } + } } } self.rebuild(false); @@ -220,7 +223,9 @@ impl App { } self.ov.night = var("TS_NIGHT").is_some(); self.show_about = var("TS_ABOUT").is_some(); - if let Some(c) = var("TS_COND").and_then(|v| Conditions::ALL.into_iter().find(|c| format!("{c:?}").eq_ignore_ascii_case(&v))) { + if let Some(c) = var("TS_COND") + .and_then(|v| Conditions::ALL.into_iter().find(|c| format!("{c:?}").eq_ignore_ascii_case(&v))) + { self.world.sims[self.focus].sensors.conditions = c; } if let Some(f) = var("TS_LOOP_FAULT").and_then(|v| v.parse().ok()) { @@ -255,7 +260,11 @@ impl App { } let old = &self.world.sims[self.focus]; let net = Network::build(self.layout); - let demand = if keep_demand && old.net.kind == self.layout { old.demand.clone() } else { DemandConfig::default_for(&net) }; + let demand = if keep_demand && old.net.kind == self.layout { + old.demand.clone() + } else { + DemandConfig::default_for(&net) + }; let template = Self::settings_of(old); // Under the green wave, the controller to give back when it ends. let ctrl = self.world.controller_before_wave(self.focus).unwrap_or(self.ctrl); @@ -287,7 +296,13 @@ impl App { let demand = src.demand.clone(); // B runs one controller everywhere; under the green wave, the one it ran before. let ctrl = self.twin.as_ref().and_then(|t| t.controller_before_wave(0)).unwrap_or(self.twin_ctrl); - self.twin = Some(self.make_world(self.twin_mode, ctrl, (self.twin_policy, self.twin_sched), demand, &template)); + self.twin = Some(self.make_world( + self.twin_mode, + ctrl, + (self.twin_policy, self.twin_sched), + demand, + &template, + )); self.twin_summary = Summary::default(); } @@ -318,7 +333,14 @@ impl App { /// One intersection, or a corridor of `corridor_n` along the main road. The /// cooperative crossing always stands alone: its manager schedules every vehicle from /// the edge of its own map. - fn make_world(&self, mode: ControlMode, ctrl: CtrlKind, (policy, sched): (GapPolicy, Scheduling), demand: DemandConfig, settings: &Sim) -> World { + fn make_world( + &self, + mode: ControlMode, + ctrl: CtrlKind, + (policy, sched): (GapPolicy, Scheduling), + demand: DemandConfig, + settings: &Sim, + ) -> World { let n = if self.is_coop() { 1 } else { self.corridor_n }; let mut w = World::corridor(n, self.layout, mode, ctrl, &demand, self.seed); for sim in &mut w.sims { @@ -457,7 +479,11 @@ impl App { VKind::Emergency => "🚑", }; let sel = self.tool == Tool::Vehicle && self.spawn_kind == k; - if ui.selectable_label(sel, format!("{icon} {}", k.name())).on_hover_text("Then click an approach road (or use the N/E/S/W buttons)").clicked() { + if ui + .selectable_label(sel, format!("{icon} {}", k.name())) + .on_hover_text("Then click an approach road (or use the N/E/S/W buttons)") + .clicked() + { self.tool = if sel { Tool::Select } else { Tool::Vehicle }; self.spawn_kind = k; } @@ -466,11 +492,15 @@ impl App { let walk = !net.crosswalks.is_empty(); let no_walk = match net.kind { LayoutKind::Cooperative => "No crosswalks here: pedestrians use underground passages", - LayoutKind::Roundabout => "No crosswalks here: the roundabout is simulated without pedestrians", + LayoutKind::Roundabout => { + "No crosswalks here: the roundabout is simulated without pedestrians" + } _ => "No crosswalks here", }; let ped = ui - .add_enabled_ui(walk, |ui| ui.selectable_label(self.tool == Tool::Pedestrian, "🚶 Pedestrian")) + .add_enabled_ui(walk, |ui| { + ui.selectable_label(self.tool == Tool::Pedestrian, "🚶 Pedestrian") + }) .inner .on_hover_text("Click near a crosswalk corner") .on_disabled_hover_text(no_walk); @@ -495,7 +525,8 @@ impl App { (leg.compass.letter(), leg.compass.bound()) }; if ui.button(format!("+ {letter}")).on_hover_text(format!("{bound} approach")).clicked() { - let ok = self.world.sims[self.focus].spawn_manual(li, self.spawn_kind, self.spawn_turn); + let ok = + self.world.sims[self.focus].spawn_manual(li, self.spawn_kind, self.spawn_turn); if let Some(t) = self.twin.as_mut() { t.sims[self.focus].spawn_manual(li, self.spawn_kind, self.spawn_turn); } @@ -909,11 +940,20 @@ impl App { }); } - fn strategy_name(&self, mode: ControlMode, ctrl: CtrlKind, (policy, sched): (GapPolicy, Scheduling)) -> String { + fn strategy_name( + &self, + mode: ControlMode, + ctrl: CtrlKind, + (policy, sched): (GapPolicy, Scheduling), + ) -> String { match (mode, sched) { (ControlMode::Signal, _) if self.layout.signalized() => ctrl.short().to_string(), - (ControlMode::Cooperative, Scheduling::Batches) => format!("Intersection manager · {} gaps", policy.label()), - (ControlMode::Cooperative, Scheduling::FirstCome) => format!("First come, first served · {} gaps", policy.label()), + (ControlMode::Cooperative, Scheduling::Batches) => { + format!("Intersection manager · {} gaps", policy.label()) + } + (ControlMode::Cooperative, Scheduling::FirstCome) => { + format!("First come, first served · {} gaps", policy.label()) + } _ => mode.name().to_string(), } } @@ -962,7 +1002,9 @@ impl App { }) .response .labelled_by(label.id) - .on_hover_text("How long after a crossing vehicle leaves a conflict zone the next one may enter it"); + .on_hover_text( + "How long after a crossing vehicle leaves a conflict zone the next one may enter it", + ); if p != self.coop_policy { // Live: trajectories already granted keep their gaps. self.coop_policy = p; @@ -1234,7 +1276,13 @@ impl App { let cyan = Color32::from_rgb(90, 220, 240); ui.heading("Intersection manager"); if c.scheduling == Scheduling::FirstCome { - ui.label(RichText::new("First come, first served: each vehicle gets the earliest gap that fits as it arrives").strong().color(cyan)); + ui.label( + RichText::new( + "First come, first served: each vehicle gets the earliest gap that fits as it arrives", + ) + .strong() + .color(cyan), + ); egui::Grid::new("coop_kpis").num_columns(3).spacing(vec2(16.0, 6.0)).show(ui, |ui| { Self::kpi(ui, "vehicles scheduled", c.plans.len().to_string(), Color32::WHITE); Self::kpi(ui, "zone reservations", c.reservations().to_string(), Color32::WHITE); @@ -1249,7 +1297,9 @@ impl App { } else { self.phase_table(ui, c, sim); } - ui.label(RichText::new("Right turns use the slip lanes and never enter the box.").small().color(gray)); + ui.label( + RichText::new("Right turns use the slip lanes and never enter the box.").small().color(gray), + ); ui.add_space(4.0); self.safety_monitors(ui, c); } @@ -1272,14 +1322,23 @@ impl App { egui::Grid::new("coop_kpis").num_columns(3).spacing(vec2(16.0, 6.0)).show(ui, |ui| { Self::kpi(ui, "vehicles scheduled", c.plans.len().to_string(), Color32::WHITE); Self::kpi(ui, "zone reservations", c.reservations().to_string(), Color32::WHITE); - Self::kpi(ui, "average cycle", if cycles >= 1.0 { format!("{:.0} s", sim.t / cycles) } else { "–".into() }, Color32::WHITE); + Self::kpi( + ui, + "average cycle", + if cycles >= 1.0 { format!("{:.0} s", sim.t / cycles) } else { "–".into() }, + Color32::WHITE, + ); ui.end_row(); Self::kpi(ui, "switching lost / cycle", format!("{:.1} s", c.lost_time()), Color32::WHITE); Self::kpi(ui, "phases served", c.stats.batches.to_string(), Color32::WHITE); Self::kpi(ui, "emergency priority", c.stats.preempted.to_string(), Color32::WHITE); ui.end_row(); }); - ui.label(RichText::new("Phases in service order (two rings, barrier between north–south and east–west)").small().color(gray)); + ui.label( + RichText::new("Phases in service order (two rings, barrier between north–south and east–west)") + .small() + .color(gray), + ); egui::Grid::new("coop_moves").striped(true).num_columns(4).show(ui, |ui| { for h in ["movement", "demand veh/h", "max phase", ""] { ui.label(RichText::new(h).small().color(gray)); @@ -1291,7 +1350,11 @@ impl App { ui.label(format!("{} · {}", r + 1, name(m))); ui.label(format!("{:.0}", c.arrival_rate(m))); ui.label(format!("{:.0} s", c.max_green(m))); - ui.label(if open.contains(&m) { RichText::new("serving").color(cyan) } else { RichText::new("") }); + ui.label(if open.contains(&m) { + RichText::new("serving").color(cyan) + } else { + RichText::new("") + }); ui.end_row(); } } @@ -1307,8 +1370,18 @@ impl App { let flag = |n: u64| if n == 0 { ok } else { Color32::from_rgb(240, 90, 90) }; let (margin, margin_ok) = margin_kpi(st.margin_min); egui::Grid::new("coop_safety").num_columns(3).spacing(vec2(16.0, 6.0)).show(ui, |ui| { - Self::kpi(ui, "closest crossing (PET)", if st.pet_min.is_finite() { format!("{:.2} s", st.pet_min) } else { "–".into() }, Color32::WHITE); - Self::kpi(ui, "margin over gap", margin, if margin_ok { ok } else { Color32::from_rgb(240, 90, 90) }); + Self::kpi( + ui, + "closest crossing (PET)", + if st.pet_min.is_finite() { format!("{:.2} s", st.pet_min) } else { "–".into() }, + Color32::WHITE, + ); + Self::kpi( + ui, + "margin over gap", + margin, + if margin_ok { ok } else { Color32::from_rgb(240, 90, 90) }, + ); Self::kpi(ui, "zone overlaps", st.zone_violations.to_string(), flag(st.zone_violations)); ui.end_row(); Self::kpi(ui, "entries without slot", st.unauthorized.to_string(), flag(st.unauthorized)); @@ -1361,7 +1434,11 @@ impl App { match &sim.coop { Some(c) => { let slot = match c.plans.get(&v.id) { - Some(p) if p.t_line > sim.t => format!("crosses the line in {:.1} s at {:.0} km/h", p.t_line - sim.t, p.v_line * 3.6), + Some(p) if p.t_line > sim.t => format!( + "crosses the line in {:.1} s at {:.0} km/h", + p.t_line - sim.t, + p.v_line * 3.6 + ), Some(p) => format!("crossed the line at {:.0} km/h", p.v_line * 3.6), None if c.managed(v.movement) => "none yet: waiting for one".to_string(), None => "not needed (right turns bypass the box)".to_string(), @@ -1405,15 +1482,28 @@ impl App { ("all + 30% CV", cv), ("100% connected vehicles", cv_only), ]; - for c in [CtrlKind::Actuated, CtrlKind::MaxPressure, CtrlKind::Predictive, CtrlKind::DeepRl, CtrlKind::Hybrid] { + for c in [ + CtrlKind::Actuated, + CtrlKind::MaxPressure, + CtrlKind::Predictive, + CtrlKind::DeepRl, + CtrlKind::Hybrid, + ] { for (name, s) in &packages { - jobs.push(Job { label: format!("{} · {}", c.short(), name), mode: ControlMode::Signal, ctrl: c, sensors: s.clone(), coop: None }); + jobs.push(Job { + label: format!("{} · {}", c.short(), name), + mode: ControlMode::Signal, + ctrl: c, + sensors: s.clone(), + coop: None, + }); } } } else if kind == LayoutKind::Cooperative { // Batches under the three safety margins, and the vehicles weaving through one // at a time under the fail-safe one. - let mut runs: Vec<(GapPolicy, Scheduling)> = GapPolicy::ALL.into_iter().map(|p| (p, Scheduling::Batches)).collect(); + let mut runs: Vec<(GapPolicy, Scheduling)> = + GapPolicy::ALL.into_iter().map(|p| (p, Scheduling::Batches)).collect(); runs.push((GapPolicy::FailSafe, Scheduling::FirstCome)); for (p, sch) in runs { let label = match sch { @@ -1421,22 +1511,42 @@ impl App { Scheduling::FirstCome => format!("First come, first served · {} gaps", p.label()), }; let coop = Some((p, sch, self.coop_max_cycle, self.coop_slack)); - jobs.push(Job { label, mode: ControlMode::Cooperative, ctrl: CtrlKind::Fixed, sensors: base.clone(), coop }); + jobs.push(Job { + label, + mode: ControlMode::Cooperative, + ctrl: CtrlKind::Fixed, + sensors: base.clone(), + coop, + }); } } else { for m in ControlMode::options(kind) { if m == ControlMode::Signal { for c in CtrlKind::ALL { - jobs.push(Job { label: c.name().to_string(), mode: m, ctrl: c, sensors: base.clone(), coop: None }); + jobs.push(Job { + label: c.name().to_string(), + mode: m, + ctrl: c, + sensors: base.clone(), + coop: None, + }); } } else { - jobs.push(Job { label: m.name().to_string(), mode: m, ctrl: CtrlKind::Fixed, sensors: base.clone(), coop: None }); + jobs.push(Job { + label: m.name().to_string(), + mode: m, + ctrl: CtrlKind::Fixed, + sensors: base.clone(), + coop: None, + }); } } } let title = match (sensor_study, kind) { (true, _) => format!("Sensor study — {}", kind.name()), - (false, LayoutKind::Cooperative) => format!("Scheduling and gap policy comparison — {}", kind.name()), + (false, LayoutKind::Cooperative) => { + format!("Scheduling and gap policy comparison — {}", kind.name()) + } _ => format!("Controller comparison — {}", kind.name()), }; self.bench = Some(Bench::start(title, kind, &demand, jobs)); @@ -1490,12 +1600,15 @@ impl App { return; } ui.label(RichText::new("Person delay incl. unserved (s) — lower is better").strong()); - let bars: Vec<(String, f64, bool)> = rows.iter().map(|r| (r.label.clone(), r.summary.person_delay_all, false)).collect(); + let bars: Vec<(String, f64, bool)> = + rows.iter().map(|r| (r.label.clone(), r.summary.person_delay_all, false)).collect(); charts::hbars(ui, &bars, " s", true); ui.separator(); egui::ScrollArea::vertical().max_height(300.0).show(ui, |ui| { egui::Grid::new("bench").striped(true).num_columns(8).show(ui, |ui| { - for h in ["strategy", "delay s", "p95 s", "veh/h", "stops", "pers dly*", "CO₂ g", "ped wait"] { + for h in + ["strategy", "delay s", "p95 s", "veh/h", "stops", "pers dly*", "CO₂ g", "ped wait"] + { ui.label(RichText::new(h).strong()); } ui.end_row(); @@ -1522,9 +1635,15 @@ impl App { fn bottom_panel(&mut self, ui: &mut Ui) { let coop = self.world.sims[self.focus].coop.is_some(); ui.horizontal(|ui| { - let title = if coop { "Stop-line crossings by movement (last 2.5 min and the next 30 s already scheduled)" } else { "Signal timing (last 3 min)" }; + let title = if coop { + "Stop-line crossings by movement (last 2.5 min and the next 30 s already scheduled)" + } else { + "Signal timing (last 3 min)" + }; ui.label(RichText::new(title).strong().size(12.0)); - if let Some((m, t)) = &self.message && t.elapsed().as_secs() < 5 { + if let Some((m, t)) = &self.message + && t.elapsed().as_secs() < 5 + { ui.separator(); ui.label(RichText::new(m).color(Color32::from_rgb(255, 210, 120))); } @@ -1549,7 +1668,8 @@ impl App { let extent = net.legs.iter().map(|l| l.stop_dist).fold(0.0, f64::max) + margin; let half_w = if self.twin.is_some() { rect.width() * 0.5 } else { rect.width() }; let span = (self.world.sims.len() - 1) as f64 * SPACING; - self.scale = (half_w / (2.0 * extent + span) as f32).min(rect.height() / (2.0 * extent as f32)).max(0.6); + self.scale = + (half_w / (2.0 * extent + span) as f32).min(rect.height() / (2.0 * extent as f32)).max(0.6); if let Some(z) = std::env::var("TS_ZOOM").ok().and_then(|v| v.parse::().ok()) { self.scale *= z; } @@ -1564,7 +1684,9 @@ impl App { if resp.hovered() { let (scroll, zoom) = ui.input(|i| (i.smooth_scroll_delta.y, i.zoom_delta())); let factor = zoom * (scroll * 0.0025).exp(); - if (factor - 1.0).abs() > 1e-4 && let Some(h) = resp.hover_pos() { + if (factor - 1.0).abs() > 1e-4 + && let Some(h) = resp.hover_pos() + { let cam = Camera { center: self.cam_center, scale: self.scale, rect }; let before = cam.to_world(h); self.scale = (self.scale * factor).clamp(0.6, 40.0); @@ -1576,7 +1698,10 @@ impl App { // Side by side: A on the left half, B on the right, same camera. let halves = if self.twin.is_some() { let mid = rect.center().x; - vec![egui::Rect::from_min_max(rect.min, egui::pos2(mid - 1.0, rect.max.y)), egui::Rect::from_min_max(egui::pos2(mid + 1.0, rect.min.y), rect.max)] + vec![ + egui::Rect::from_min_max(rect.min, egui::pos2(mid - 1.0, rect.max.y)), + egui::Rect::from_min_max(egui::pos2(mid + 1.0, rect.min.y), rect.max), + ] } else { vec![rect] }; @@ -1586,7 +1711,12 @@ impl App { let (world, mode, ctrl, policy) = if k == 0 { (&self.world, self.mode, self.ctrl, (self.coop_policy, self.coop_sched)) } else { - (self.twin.as_ref().unwrap(), self.twin_mode, self.twin_ctrl, (self.twin_policy, self.twin_sched)) + ( + self.twin.as_ref().unwrap(), + self.twin_mode, + self.twin_ctrl, + (self.twin_policy, self.twin_sched), + ) }; let label = match (self.twin.is_some(), k) { (false, _) => self.strategy_name(mode, ctrl, policy), @@ -1601,7 +1731,9 @@ impl App { self.legend(&painter, halves[0]); // Clicks: select / spawn (spawns go to both sides so arrivals stay identical). - if resp.clicked() && let Some(p) = resp.interact_pointer_pos() { + if resp.clicked() + && let Some(p) = resp.interact_pointer_pos() + { let k = halves.iter().position(|h| h.contains(p)).unwrap_or(0); let cam = Camera { center: self.cam_center, scale: self.scale, rect: halves[k] }; let w = cam.to_world(p); @@ -1628,7 +1760,8 @@ impl App { let l = &net.legs[li]; let r = local.dot(l.d); let lat = local.dot(l.in_side()); - let ok = r > l.stop_dist * 0.6 && lat > -l.out_width - 12.0 && lat < l.in_width + 12.0; + let ok = + r > l.stop_dist * 0.6 && lat > -l.out_width - 12.0 && lat < l.in_width + 12.0; (li, ok, (lat - l.in_width * 0.5).abs()) }) .filter(|x| x.1) @@ -1643,7 +1776,12 @@ impl App { if !ok { self.notify(format!("{} has no {} movement", bound, self.spawn_turn.name())); } else { - self.notify(format!("{} {} queued on {} (enters 260 m upstream)", self.spawn_kind.name(), self.spawn_turn.name(), bound)); + self.notify(format!( + "{} {} queued on {} (enters 260 m upstream)", + self.spawn_kind.name(), + self.spawn_turn.name(), + bound + )); } } } @@ -1654,7 +1792,9 @@ impl App { .iter() .flat_map(|c| [(c.id, false, c.a.dist(local)), (c.id, true, c.b.dist(local))]) .min_by(|a, b| a.2.partial_cmp(&b.2).unwrap()); - if let Some((cw, b, d)) = best && d < 12.0 { + if let Some((cw, b, d)) = best + && d < 12.0 + { self.world.sims[i].spawn_ped(cw, b); if let Some(t) = self.twin.as_mut() { t.sims[i].spawn_ped(cw, b); @@ -1663,26 +1803,47 @@ impl App { } } } - if self.tool != Tool::Select && let Some(h) = resp.hover_pos() { + if self.tool != Tool::Select + && let Some(h) = resp.hover_pos() + { let txt = match self.tool { - Tool::Vehicle => format!("click an approach: {} {}", self.spawn_kind.name(), self.spawn_turn.name()), + Tool::Vehicle => { + format!("click an approach: {} {}", self.spawn_kind.name(), self.spawn_turn.name()) + } _ => "click a crosswalk corner".to_string(), }; - painter.text(h + vec2(14.0, 10.0), egui::Align2::LEFT_TOP, txt, egui::FontId::proportional(12.0), Color32::WHITE); + painter.text( + h + vec2(14.0, 10.0), + egui::Align2::LEFT_TOP, + txt, + egui::FontId::proportional(12.0), + Color32::WHITE, + ); } } /// Intersection nearest to world point `w`, and `w` in its local coordinates. fn locate(&self, w: crate::sim::geom::V2) -> (usize, crate::sim::geom::V2) { let i = (0..self.world.offsets.len()) - .min_by(|&a, &b| w.dist(self.world.offsets[a]).partial_cmp(&w.dist(self.world.offsets[b])).unwrap()) + .min_by(|&a, &b| { + w.dist(self.world.offsets[a]).partial_cmp(&w.dist(self.world.offsets[b])).unwrap() + }) .unwrap_or(0); (i, w - self.world.offsets[i]) } - fn paint_world(&self, painter: &egui::Painter, world: &World, rect: egui::Rect, label: &str, alpha: f64, main: bool) { + fn paint_world( + &self, + painter: &egui::Painter, + world: &World, + rect: egui::Rect, + label: &str, + alpha: f64, + main: bool, + ) { painter.rect_filled(rect, 0.0, render::GRASS); - let cam_for = |i: usize| Camera { center: self.cam_center - world.offsets[i], scale: self.scale, rect }; + let cam_for = + |i: usize| Camera { center: self.cam_center - world.offsets[i], scale: self.scale, rect }; let n = world.sims.len(); for i in 0..n { // Each intersection's scenery covers its own block (half-way to each @@ -1731,7 +1892,13 @@ impl App { for (c, t) in items { let p = egui::pos2(rect.left() + 14.0, y); painter.circle_filled(p, 4.5, c); - painter.text(p + vec2(10.0, 0.0), egui::Align2::LEFT_CENTER, t, egui::FontId::proportional(11.5), Color32::from_gray(230)); + painter.text( + p + vec2(10.0, 0.0), + egui::Align2::LEFT_CENTER, + t, + egui::FontId::proportional(11.5), + Color32::from_gray(230), + ); y += 16.0; } } @@ -1777,7 +1944,9 @@ impl eframe::App for App { } self.bench_window(&ctx); self.about_window(&ctx); - if let Some((path, after)) = self.shot.clone() && self.world.sims[self.focus].t >= after { + if let Some((path, after)) = self.shot.clone() + && self.world.sims[self.focus].t >= after + { // Let the panels refresh at the stopped time before capturing. let since = *self.shot_since.get_or_insert_with(Instant::now); if since.elapsed().as_millis() > 600 { diff --git a/src/gui/render.rs b/src/gui/render.rs index c2a4560..6360bdf 100644 --- a/src/gui/render.rs +++ b/src/gui/render.rs @@ -58,11 +58,17 @@ pub struct Camera { impl Camera { pub fn to_screen(&self, p: V2) -> Pos2 { let c = self.rect.center(); - pos2(c.x + (p.x as f32 - self.center.x as f32) * self.scale, c.y + (p.y as f32 - self.center.y as f32) * self.scale) + pos2( + c.x + (p.x as f32 - self.center.x as f32) * self.scale, + c.y + (p.y as f32 - self.center.y as f32) * self.scale, + ) } pub fn to_world(&self, p: Pos2) -> V2 { let c = self.rect.center(); - v2(self.center.x + ((p.x - c.x) / self.scale) as f64, self.center.y + ((p.y - c.y) / self.scale) as f64) + v2( + self.center.x + ((p.x - c.x) / self.scale) as f64, + self.center.y + ((p.y - c.y) / self.scale) as f64, + ) } } @@ -165,7 +171,8 @@ impl Scene { } fn circle(&mut self, c: V2, r: f64, col: Color32) { let n = ((r * 3.0) as usize).clamp(12, 64); - let pts: Vec = (0..n).map(|i| c + V2::from_angle(i as f64 / n as f64 * std::f64::consts::TAU) * r).collect(); + let pts: Vec = + (0..n).map(|i| c + V2::from_angle(i as f64 / n as f64 * std::f64::consts::TAU) * r).collect(); self.poly(&pts, col); } fn ring(&mut self, c: V2, r0: f64, r1: f64, col: Color32) { @@ -186,7 +193,8 @@ impl Scene { }; let stem_end = base + fwd * 2.2; self.quad_line(base, stem_end, 0.28, col); - let (tip, from) = if turn == Turn::T { (base + fwd * 3.6, stem_end) } else { (stem_end + side * 1.4, stem_end) }; + let (tip, from) = + if turn == Turn::T { (base + fwd * 3.6, stem_end) } else { (stem_end + side * 1.4, stem_end) }; if turn != Turn::T { self.quad_line(from, tip - side * 0.3, 0.28, col); } @@ -195,7 +203,8 @@ impl Scene { } pub fn build(net: &Network) -> Scene { - let mut sc = Scene { kind: net.kind, pos: vec![], col: vec![], idx: vec![], trees: vec![], buildings: vec![] }; + let mut sc = + Scene { kind: net.kind, pos: vec![], col: vec![], idx: vec![], trees: vec![], buildings: vec![] }; let mut rng = crate::sim::rng::Rng::new(net.kind as u64 * 31 + 5); let ext = 420.0; // Terrain with a little texture. @@ -221,12 +230,23 @@ impl Scene { for leg in &net.legs { let (d, ins) = (leg.d, leg.in_side()); let p = |r: f64, lat: f64| d * r + ins * lat; - self.poly(&[p(0.0, -leg.out_width - sw), p(far, -leg.out_width - sw), p(far, leg.in_width + sw), p(0.0, leg.in_width + sw)], SIDEWALK); + self.poly( + &[ + p(0.0, -leg.out_width - sw), + p(far, -leg.out_width - sw), + p(far, leg.in_width + sw), + p(0.0, leg.in_width + sw), + ], + SIDEWALK, + ); } for leg in &net.legs { let (d, ins) = (leg.d, leg.in_side()); let p = |r: f64, lat: f64| d * r + ins * lat; - self.poly(&[p(0.0, -leg.out_width), p(far, -leg.out_width), p(far, leg.in_width), p(0.0, leg.in_width)], ASPHALT); + self.poly( + &[p(0.0, -leg.out_width), p(far, -leg.out_width), p(far, leg.in_width), p(0.0, leg.in_width)], + ASPHALT, + ); self.quad_line(p(0.0, -leg.out_width - 0.15), p(far, -leg.out_width - 0.15), 0.3, CURB); self.quad_line(p(0.0, leg.in_width + 0.15), p(far, leg.in_width + 0.15), 0.3, CURB); } @@ -255,7 +275,8 @@ impl Scene { while a1 - a2 > std::f64::consts::PI { a2 += std::f64::consts::TAU; } - let arc: Vec = (0..=16).map(|i| o + V2::from_angle(a1 + (a2 - a1) * i as f64 / 16.0) * rf).collect(); + let arc: Vec = + (0..=16).map(|i| o + V2::from_angle(a1 + (a2 - a1) * i as f64 / 16.0) * rf).collect(); // Sidewalk ring around the corner block, then asphalt fan. for w in arc.windows(2) { let (i0, i1) = (o + (w[0] - o) * ((rf - sw) / rf), o + (w[1] - o) * ((rf - sw) / rf)); @@ -270,7 +291,15 @@ impl Scene { for leg in &net.legs { let (d, ins) = (leg.d, leg.in_side()); let p = |r: f64, lat: f64| d * r + ins * lat; - self.poly(&[p(0.0, -leg.out_width), p(leg.stop_dist, -leg.out_width), p(leg.stop_dist, leg.in_width), p(0.0, leg.in_width)], ASPHALT); + self.poly( + &[ + p(0.0, -leg.out_width), + p(leg.stop_dist, -leg.out_width), + p(leg.stop_dist, leg.in_width), + p(0.0, leg.in_width), + ], + ASPHALT, + ); } // Markings. for (li, leg) in net.legs.iter().enumerate() { @@ -296,7 +325,10 @@ impl Scene { let rb = if bay > 0.0 { net.in_len - bay } else { far }; if bay > 0.0 { let taper = 18.0; - self.poly(&[p(rb + taper, 0.2), p(far, 0.2), p(far, w - 0.15), p(rb + taper, w - 0.15)], MEDIAN); + self.poly( + &[p(rb + taper, 0.2), p(far, 0.2), p(far, w - 0.15), p(rb + taper, w - 0.15)], + MEDIAN, + ); self.tri(p(rb, 0.2), p(rb + taper, 0.2), p(rb + taper, w - 0.15), MEDIAN); self.quad_line(p(rb, 0.2), p(rb + taper, w - 0.15), 0.14, YELLOW); self.quad_line(p(rb + taper, w - 0.15), p(far, w - 0.15), 0.12, YELLOW); @@ -350,13 +382,24 @@ impl Scene { for leg in &net.legs { let (d, ins) = (leg.d, leg.in_side()); let p = |r: f64, lat: f64| d * r + ins * lat; - self.poly(&[p(rr, -leg.out_width - sw), p(far, -leg.out_width - sw), p(far, leg.in_width + sw), p(rr, leg.in_width + sw)], SIDEWALK); + self.poly( + &[ + p(rr, -leg.out_width - sw), + p(far, -leg.out_width - sw), + p(far, leg.in_width + sw), + p(rr, leg.in_width + sw), + ], + SIDEWALK, + ); } self.circle(c0, rr + rw / 2.0 + 2.0 + sw, SIDEWALK); for leg in &net.legs { let (d, ins) = (leg.d, leg.in_side()); let p = |r: f64, lat: f64| d * r + ins * lat; - self.poly(&[p(rr, -leg.out_width), p(far, -leg.out_width), p(far, leg.in_width), p(rr, leg.in_width)], ASPHALT); + self.poly( + &[p(rr, -leg.out_width), p(far, -leg.out_width), p(far, leg.in_width), p(rr, leg.in_width)], + ASPHALT, + ); self.quad_line(p(rr + 6.0, -leg.out_width - 0.15), p(far, -leg.out_width - 0.15), 0.3, CURB); self.quad_line(p(rr + 6.0, leg.in_width + 0.15), p(far, leg.in_width + 0.15), 0.3, CURB); } @@ -372,8 +415,20 @@ impl Scene { let (d, ins) = (leg.d, leg.in_side()); let p = |r: f64, lat: f64| d * r + ins * lat; let r0 = rr + rw / 2.0 + 1.2; - self.poly(&[p(r0, 0.0), p(r0 + 14.0, -1.7), p(r0 + 26.0, -0.6), p(r0 + 26.0, 0.6), p(r0 + 14.0, 1.7)], CURB); - self.poly(&[p(r0 + 1.0, 0.0), p(r0 + 14.0, -1.3), p(r0 + 25.0, -0.35), p(r0 + 25.0, 0.35), p(r0 + 14.0, 1.3)], Color32::from_rgb(98, 146, 76)); + self.poly( + &[p(r0, 0.0), p(r0 + 14.0, -1.7), p(r0 + 26.0, -0.6), p(r0 + 26.0, 0.6), p(r0 + 14.0, 1.7)], + CURB, + ); + self.poly( + &[ + p(r0 + 1.0, 0.0), + p(r0 + 14.0, -1.3), + p(r0 + 25.0, -0.35), + p(r0 + 25.0, 0.35), + p(r0 + 14.0, 1.3), + ], + Color32::from_rgb(98, 146, 76), + ); self.quad_line(p(r0 + 26.0, 0.12), p(far, 0.12), 0.12, YELLOW); self.quad_line(p(r0 + 26.0, -0.12), p(far, -0.12), 0.12, YELLOW); let ry = leg.stop_dist; @@ -391,7 +446,12 @@ impl Scene { for i in (0..n).step_by(2) { let a0 = i as f64 / n as f64 * std::f64::consts::TAU; let a1 = (i as f64 + 0.8) / n as f64 * std::f64::consts::TAU; - self.quad_line(V2::from_angle(a0) * (rr + rw / 2.0), V2::from_angle(a1) * (rr + rw / 2.0), 0.15, WHITE); + self.quad_line( + V2::from_angle(a0) * (rr + rw / 2.0), + V2::from_angle(a1) * (rr + rw / 2.0), + 0.15, + WHITE, + ); } } @@ -416,7 +476,10 @@ impl Scene { let corner = |d: V2, ins: V2| ((d + ins) * rs, -ins, -d); let at = |(c, ex, ey): (V2, V2, V2), x: f64, y: f64| c + ex * x + ey * y; let arc = |k: (V2, V2, V2), r: f64, a0: f64, a1: f64, n: usize| -> Vec { - (0..=n).map(|i| a0 + (a1 - a0) * i as f64 / n as f64).map(|a| at(k, r * a.cos(), r * a.sin())).collect() + (0..=n) + .map(|i| a0 + (a1 - a0) * i as f64 / n as f64) + .map(|a| at(k, r * a.cos(), r * a.sin())) + .collect() }; let band = |sc: &mut Scene, outer_pts: &[V2], inner_pts: &[V2], col: Color32| { for i in 0..outer_pts.len() - 1 { @@ -428,7 +491,10 @@ impl Scene { let (d, ins) = (leg.d, leg.in_side()); let p = |r: f64, lat: f64| d * r + ins * lat; for s in [1.0, -1.0] { - self.poly(&[p(rs, s * outer), p(far, s * outer), p(far, s * (outer + sw)), p(rs, s * (outer + sw))], SIDEWALK); + self.poly( + &[p(rs, s * outer), p(far, s * outer), p(far, s * (outer + sw)), p(rs, s * (outer + sw))], + SIDEWALK, + ); } let k = corner(d, ins); band(self, &arc(k, slip_in, 0.0, quarter, 24), &arc(k, slip_in - sw, 0.0, quarter, 24), SIDEWALK); @@ -441,11 +507,19 @@ impl Scene { let w = inner + gutter; self.poly(&[p(r0 - 0.01, -w), p(far, -w), p(far, w), p(r0 - 0.01, w)], ASPHALT); for s in [1.0, -1.0] { - self.poly(&[p(rs, s * inner), p(far, s * inner), p(far, s * outer), p(rs, s * outer)], ASPHALT); + self.poly( + &[p(rs, s * inner), p(far, s * inner), p(far, s * outer), p(rs, s * outer)], + ASPHALT, + ); self.quad_line(p(rs, s * (outer + 0.15)), p(far, s * (outer + 0.15)), 0.3, CURB); } let k = corner(d, ins); - band(self, &arc(k, slip_out + gutter, 0.0, quarter, 24), &arc(k, slip_in, 0.0, quarter, 24), ASPHALT); + band( + self, + &arc(k, slip_out + gutter, 0.0, quarter, 24), + &arc(k, slip_in, 0.0, quarter, 24), + ASPHALT, + ); self.polyline(&arc(k, slip_in - 0.15, 0.0, quarter, 24), 0.3, CURB); } // Corner islands between the box lanes and the slip lanes. @@ -474,7 +548,10 @@ impl Scene { } let hw = m - 0.45; let nose = r0 + 1.2 + hw; - let mut pts: Vec = (0..=12).map(|i| std::f64::consts::PI * i as f64 / 12.0).map(|a| p(nose - hw * a.sin(), hw * a.cos())).collect(); + let mut pts: Vec = (0..=12) + .map(|i| std::f64::consts::PI * i as f64 / 12.0) + .map(|a| p(nose - hw * a.sin(), hw * a.cos())) + .collect(); pts.extend([p(far, -hw), p(far, hw)]); self.poly(&pts, CONCRETE); self.quad_line(p(nose + 1.0, 0.0), p(far, 0.0), hw * 1.2, shade(ISLAND, 0.9)); @@ -524,7 +601,8 @@ impl Scene { for leg in &net.legs { let k = corner(leg.d, leg.in_side()); let diag = (k.1 + k.2).norm(); - let mid = (slip_out + gutter) * std::f64::consts::FRAC_1_SQRT_2 * 0.5 + (rs - inner - gutter) * 0.5; + let mid = + (slip_out + gutter) * std::f64::consts::FRAC_1_SQRT_2 * 0.5 + (rs - inner - gutter) * 0.5; let (a, b) = (at(k, mid, mid), at(k, 12.0, 12.0)); self.tunnel(a, b); self.stairs(a, -diag); @@ -580,7 +658,10 @@ impl Scene { for leg in &net.legs { let r = p.dot(leg.d); let lat = p.dot(leg.in_side()); - if r > -leg.in_width.max(leg.out_width) - 6.0 && lat < leg.in_width + 4.5 + margin && lat > -leg.out_width - 4.5 - margin { + if r > -leg.in_width.max(leg.out_width) - 6.0 + && lat < leg.in_width + 4.5 + margin + && lat > -leg.out_width - 4.5 - margin + { return false; } } @@ -604,7 +685,11 @@ impl Scene { placed.push((c, rad)); let (hx, hy) = (v2(w / 2.0, 0.0), v2(0.0, h / 2.0)); let col = roofs[rng.below(roofs.len())]; - self.buildings.push(([c - hx - hy, c + hx - hy, c + hx + hy, c - hx + hy], col, rng.range(0.6, 1.4) as f32)); + self.buildings.push(( + [c - hx - hy, c + hx - hy, c + hx + hy, c - hx + hy], + col, + rng.range(0.6, 1.4) as f32, + )); if placed.len() > 70 { break; } @@ -619,7 +704,11 @@ impl Scene { let p = leg.d * r + leg.in_side() * lat; if !placed.iter().any(|&(q, rad)| q.dist(p) < rad + 3.0) { let g = rng.range(0.85, 1.15) as f32; - self.trees.push((p, rng.range(2.6, 3.8) as f32, shade(Color32::from_rgb(52, 110, 50), g))); + self.trees.push(( + p, + rng.range(2.6, 3.8) as f32, + shade(Color32::from_rgb(52, 110, 50), g), + )); } } r += rng.range(11.0, 17.0); @@ -632,7 +721,11 @@ impl Scene { let (ex, ey) = (-leg.in_side(), -leg.d); for (x, y) in [(4.0, 9.5), (9.5, 4.0), (3.5, 15.5), (15.5, 3.5), (6.0, 6.0)] { let g = rng.range(0.85, 1.15) as f32; - self.trees.push((c + ex * x + ey * y, rng.range(2.4, 3.4) as f32, shade(Color32::from_rgb(52, 110, 50), g))); + self.trees.push(( + c + ex * x + ey * y, + rng.range(2.4, 3.4) as f32, + shade(Color32::from_rgb(52, 110, 50), g), + )); } } } @@ -661,7 +754,11 @@ impl Scene { let sh: Vec = pts.iter().map(|p| cam.to_screen(*p + off)).collect(); painter.add(Shape::convex_polygon(sh, Color32::from_black_alpha(70), Stroke::NONE)); let s: Vec = pts.iter().map(|p| cam.to_screen(*p)).collect(); - painter.add(Shape::convex_polygon(s.clone(), shade(*col, tint), Stroke::new(1.0, shade(*col, 0.7 * tint)))); + painter.add(Shape::convex_polygon( + s.clone(), + shade(*col, tint), + Stroke::new(1.0, shade(*col, 0.7 * tint)), + )); let c = pts[0].lerp(pts[2], 0.5); let inset: Vec = pts.iter().map(|p| cam.to_screen(c.lerp(*p, 0.7))).collect(); painter.add(Shape::convex_polygon(inset, shade(*col, 1.12 * tint), Stroke::NONE)); @@ -726,7 +823,8 @@ impl DrawCtx<'_> { } pub fn center(&self, v: &Vehicle) -> V2 { - self.sim.point_behind(v, self.sim.lag(v, self.alpha) + v.len() * 0.5) + self.sim.lc_offset(v, self.alpha) + self.sim.point_behind(v, self.sim.lag(v, self.alpha) + v.len() * 0.5) + + self.sim.lc_offset(v, self.alpha) } } @@ -741,7 +839,10 @@ pub fn paint_dynamic(painter: &egui::Painter, dc: &DrawCtx) { for l in &sim.net.links { if l.kind.in_box() { let pts: Vec = l.path.pts.iter().step_by(2).map(|p| cam.to_screen(*p)).collect(); - painter.add(Shape::line(pts, Stroke::new(1.0, Color32::from_rgba_unmultiplied(120, 200, 255, 60)))); + painter.add(Shape::line( + pts, + Stroke::new(1.0, Color32::from_rgba_unmultiplied(120, 200, 255, 60)), + )); } } } @@ -782,7 +883,11 @@ pub fn paint_dynamic(painter: &egui::Painter, dc: &DrawCtx) { let reserved = sim.coop.as_ref().map(|c| { let mut st = vec![0u8; sim.net.conflicts.len()]; for z in c.plans.values().flat_map(|p| &p.zones) { - let s = if z.t_in <= sim.t && sim.t < z.t_out { 2 } else { u8::from(z.t_in > sim.t && z.t_in - sim.t < 3.0) }; + let s = if z.t_in <= sim.t && sim.t < z.t_out { + 2 + } else { + u8::from(z.t_in > sim.t && z.t_in - sim.t < 3.0) + }; st[z.cid] = st[z.cid].max(s); } st @@ -795,18 +900,30 @@ pub fn paint_dynamic(painter: &egui::Painter, dc: &DrawCtx) { match st[c.id] { 2 => painter.circle_filled(p, r, Color32::from_rgba_unmultiplied(255, 80, 60, 200)), 1 => painter.circle_stroke(p, r, Stroke::new(1.8, Color32::from_rgb(255, 190, 60))), - _ => painter.circle_stroke(p, r * 0.6, Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 230, 120, 70))), + _ => painter.circle_stroke( + p, + r * 0.6, + Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 230, 120, 70)), + ), }; } None => { let held = sim.vehicles.iter().any(|v| v.commits.contains(&c.id)); - let col = if held { Color32::from_rgb(255, 90, 60) } else if c.merge { Color32::from_rgb(120, 180, 255) } else { Color32::from_rgb(255, 230, 120) }; + let col = if held { + Color32::from_rgb(255, 90, 60) + } else if c.merge { + Color32::from_rgb(120, 180, 255) + } else { + Color32::from_rgb(255, 230, 120) + }; painter.circle_stroke(p, r, Stroke::new(1.5, col)); } } } } - if let Some(id) = dc.selected && let Some(v) = sim.vehicles.iter().find(|v| v.id == id) { + if let Some(id) = dc.selected + && let Some(v) = sim.vehicles.iter().find(|v| v.id == id) + { // Remaining route. let mut pts = vec![]; for k in v.ri..v.route.len() { @@ -819,14 +936,26 @@ pub fn paint_dynamic(painter: &egui::Painter, dc: &DrawCtx) { } } if pts.len() > 1 { - painter.add(Shape::dashed_line(&pts, Stroke::new(2.0, Color32::from_rgb(255, 255, 255)), 6.0, 4.0)); + painter.add(Shape::dashed_line( + &pts, + Stroke::new(2.0, Color32::from_rgb(255, 255, 255)), + 6.0, + 4.0, + )); } for (c, ax, hl, hw) in dc.body(v) { - let pts: Vec = rect_pts(c, ax, hl + 0.6, hw + 0.6).iter().map(|p| cam.to_screen(*p)).collect(); + let pts: Vec = + rect_pts(c, ax, hl + 0.6, hw + 0.6).iter().map(|p| cam.to_screen(*p)).collect(); painter.add(Shape::closed_line(pts, Stroke::new(2.0, Color32::from_rgb(255, 255, 120)))); } - if let Some(yid) = v.yield_to && let Some(w) = sim.vehicles.iter().find(|w| w.id == yid) { - painter.arrow(cam.to_screen(dc.front(v)), cam.to_screen(dc.center(w)) - cam.to_screen(dc.front(v)), Stroke::new(2.0, Color32::from_rgb(255, 160, 60))); + if let Some(yid) = v.yield_to + && let Some(w) = sim.vehicles.iter().find(|w| w.id == yid) + { + painter.arrow( + cam.to_screen(dc.front(v)), + cam.to_screen(dc.center(w)) - cam.to_screen(dc.front(v)), + Stroke::new(2.0, Color32::from_rgb(255, 160, 60)), + ); } } } @@ -846,10 +975,17 @@ fn paint_sensors(painter: &egui::Painter, dc: &DrawCtx) { // Stop-bar presence loop. let c = l.path.point(len - 4.5); let on = lo.stopbar; - let col = if on { Color32::from_rgba_unmultiplied(255, 190, 40, 110) } else { Color32::from_rgba_unmultiplied(255, 190, 40, 18) }; + let col = if on { + Color32::from_rgba_unmultiplied(255, 190, 40, 110) + } else { + Color32::from_rgba_unmultiplied(255, 190, 40, 18) + }; let pts = rect_pts(c, ax, 4.3, hw); poly(painter, &cam, &pts, col); - painter.add(Shape::closed_line(pts.iter().map(|p| cam.to_screen(*p)).collect(), Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 200, 60, 150)))); + painter.add(Shape::closed_line( + pts.iter().map(|p| cam.to_screen(*p)).collect(), + Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 200, 60, 150)), + )); if lo.fault { // Diagnostics flagged this station: red cross (lane on recall). let red = Stroke::new((0.25 * cam.scale).max(1.5), Color32::from_rgb(240, 60, 50)); @@ -860,10 +996,17 @@ fn paint_sensors(painter: &egui::Painter, dc: &DrawCtx) { if cfg.adv_dist < len { let c = l.path.point(len - cfg.adv_dist); let recent = sim.t - lo.last_pulse < 0.6 && !lo.stopbar; - let col = if recent { Color32::from_rgba_unmultiplied(255, 220, 80, 170) } else { Color32::from_rgba_unmultiplied(255, 200, 60, 30) }; + let col = if recent { + Color32::from_rgba_unmultiplied(255, 220, 80, 170) + } else { + Color32::from_rgba_unmultiplied(255, 200, 60, 30) + }; let pts = rect_pts(c, ax, 1.0, hw); poly(painter, &cam, &pts, col); - painter.add(Shape::closed_line(pts.iter().map(|p| cam.to_screen(*p)).collect(), Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 200, 60, 140)))); + painter.add(Shape::closed_line( + pts.iter().map(|p| cam.to_screen(*p)).collect(), + Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 200, 60, 140)), + )); } } } @@ -891,7 +1034,12 @@ fn paint_sensors(painter: &egui::Painter, dc: &DrawCtx) { let c = cam.to_screen(rp); let s = (0.7 * cam.scale).max(3.0); painter.add(Shape::convex_polygon( - vec![c + egui::vec2(0.0, -s), c + egui::vec2(s, 0.0), c + egui::vec2(0.0, s), c + egui::vec2(-s, 0.0)], + vec![ + c + egui::vec2(0.0, -s), + c + egui::vec2(s, 0.0), + c + egui::vec2(0.0, s), + c + egui::vec2(-s, 0.0), + ], Color32::from_rgb(190, 110, 255), Stroke::NONE, )); @@ -899,11 +1047,20 @@ fn paint_sensors(painter: &egui::Painter, dc: &DrawCtx) { let r = leg.d * (leg.stop_dist + cfg.radar_range); let a = cam.to_screen(r + ins * (leg.in_width + 1.0)); let b = cam.to_screen(r); - painter.add(Shape::dashed_line(&[a, b], Stroke::new(1.5, Color32::from_rgba_unmultiplied(190, 110, 255, 140)), 5.0, 4.0)); + painter.add(Shape::dashed_line( + &[a, b], + Stroke::new(1.5, Color32::from_rgba_unmultiplied(190, 110, 255, 140)), + 5.0, + 4.0, + )); // Sweep beam. let phase = (dc.time * 0.8).fract() as f32; let sweep = leg.d * (leg.stop_dist + cfg.radar_range * phase as f64) + ins * (leg.in_width * 0.5); - painter.circle_filled(cam.to_screen(sweep), (0.6 * cam.scale).max(2.0), Color32::from_rgba_unmultiplied(190, 110, 255, 90)); + painter.circle_filled( + cam.to_screen(sweep), + (0.6 * cam.scale).max(2.0), + Color32::from_rgba_unmultiplied(190, 110, 255, 90), + ); } } } @@ -920,8 +1077,14 @@ fn paint_signals(painter: &egui::Painter, dc: &DrawCtx) { let l = &net.links[lane]; // Lane head: show the most restrictive relevant indication for the lane. let moves = &l.lane_moves; - let sigs: Vec<(Turn, Sig)> = moves.iter().map(|&m| (net.movements[m].turn, sim.signal.status(net, m))).collect(); - let main = sigs.iter().find(|s| s.0 == Turn::T).or_else(|| sigs.first()).map(|s| s.1).unwrap_or(Sig::R); + let sigs: Vec<(Turn, Sig)> = + moves.iter().map(|&m| (net.movements[m].turn, sim.signal.status(net, m))).collect(); + let main = sigs + .iter() + .find(|s| s.0 == Turn::T) + .or_else(|| sigs.first()) + .map(|s| s.1) + .unwrap_or(Sig::R); // A turn-only lane has an arrow head: its permissive phase is the // flashing yellow arrow alone, never a green ball. let turn_only = !sigs.iter().any(|s| s.0 == Turn::T); @@ -943,7 +1106,11 @@ fn paint_signals(painter: &egui::Painter, dc: &DrawCtx) { let pc = cam.to_screen(p); let r = (0.36 * cam.scale).max(1.5); if lit { - painter.circle_filled(pc, r * 2.4, Color32::from_rgba_unmultiplied(col.r(), col.g(), col.b(), 40)); + painter.circle_filled( + pc, + r * 2.4, + Color32::from_rgba_unmultiplied(col.r(), col.g(), col.b(), 40), + ); } painter.circle_filled(pc, r, col); } @@ -951,8 +1118,22 @@ fn paint_signals(painter: &egui::Painter, dc: &DrawCtx) { let tturn = sigs.iter().find(|s| s.0 != Turn::T && (sigs.len() == 1 || s.1 != main)); if let Some(&(t, s)) = tturn { let p = base - ax * 1.4; - let col = if s == Sig::P { if blink { Color32::from_rgb(250, 200, 40) } else { shade(Color32::from_rgb(250, 200, 40), 0.3) } } else { sig_color(s) }; - let side = if t == Turn::L { ax.left() } else if t == Turn::R { ax.right() } else { ax }; + let col = if s == Sig::P { + if blink { + Color32::from_rgb(250, 200, 40) + } else { + shade(Color32::from_rgb(250, 200, 40), 0.3) + } + } else { + sig_color(s) + }; + let side = if t == Turn::L { + ax.left() + } else if t == Turn::R { + ax.right() + } else { + ax + }; let a = cam.to_screen(p - side * 0.6); let b = cam.to_screen(p + side * 0.6); painter.arrow(a, b - a, Stroke::new((0.25 * cam.scale).max(1.2), col)); @@ -961,7 +1142,13 @@ fn paint_signals(painter: &egui::Painter, dc: &DrawCtx) { let a = l.path.point(l.length()) + ax.right() * (leg.lane_w * 0.45); let b = l.path.point(l.length()) - ax.right() * (leg.lane_w * 0.45); let col = sig_color(main); - painter.line_segment([cam.to_screen(a), cam.to_screen(b)], Stroke::new((0.25 * cam.scale).max(1.0), Color32::from_rgba_unmultiplied(col.r(), col.g(), col.b(), 140))); + painter.line_segment( + [cam.to_screen(a), cam.to_screen(b)], + Stroke::new( + (0.25 * cam.scale).max(1.0), + Color32::from_rgba_unmultiplied(col.r(), col.g(), col.b(), 140), + ), + ); let _ = k; } } @@ -970,19 +1157,33 @@ fn paint_signals(painter: &egui::Painter, dc: &DrawCtx) { let st = sim.signal.cw[i].state; let col = match st { PedSig::Walk => Color32::from_rgb(235, 245, 255), - PedSig::Fdw => if blink { Color32::from_rgb(255, 140, 40) } else { Color32::from_rgb(70, 40, 20) }, + PedSig::Fdw => { + if blink { + Color32::from_rgb(255, 140, 40) + } else { + Color32::from_rgb(70, 40, 20) + } + } PedSig::Dw => Color32::from_rgb(230, 110, 30), }; let dir = (cw.b - cw.a).norm(); for (p, s) in [(cw.a, -1.0), (cw.b, 1.0)] { let q = cam.to_screen(p + dir * (s * 1.6)); let r = (0.55 * cam.scale).max(2.5); - painter.rect_filled(Rect::from_center_size(q, egui::vec2(r * 2.2, r * 2.2)), 2.0, Color32::from_rgb(20, 20, 22)); + painter.rect_filled( + Rect::from_center_size(q, egui::vec2(r * 2.2, r * 2.2)), + 2.0, + Color32::from_rgb(20, 20, 22), + ); painter.circle_filled(q, r * 0.8, col); } if sim.signal.cw[i].called && st == PedSig::Dw { let q = cam.to_screen(cw.a.lerp(cw.b, 0.5)); - painter.circle_stroke(q, (1.2 * cam.scale).max(4.0), Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 255, 255, 80))); + painter.circle_stroke( + q, + (1.2 * cam.scale).max(4.0), + Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 255, 255, 80)), + ); } } } @@ -1001,9 +1202,19 @@ fn paint_signals(painter: &egui::Painter, dc: &DrawCtx) { c + egui::vec2(a.cos(), a.sin()) * r }) .collect(); - painter.add(Shape::convex_polygon(pts, Color32::from_rgb(200, 30, 30), Stroke::new(1.0, Color32::WHITE))); + painter.add(Shape::convex_polygon( + pts, + Color32::from_rgb(200, 30, 30), + Stroke::new(1.0, Color32::WHITE), + )); if r > 6.0 { - painter.text(c, Align2::CENTER_CENTER, "STOP", FontId::proportional(r * 0.55), Color32::WHITE); + painter.text( + c, + Align2::CENTER_CENTER, + "STOP", + FontId::proportional(r * 0.55), + Color32::WHITE, + ); } } } @@ -1029,7 +1240,10 @@ fn paint_admission(painter: &egui::Painter, dc: &DrawCtx) { let n = ax.right() * (leg.lane_w * 0.45); let glow = Color32::from_rgba_unmultiplied(ADMITTING.r(), ADMITTING.g(), ADMITTING.b(), 28); poly(painter, &cam, &[end + n, end - n, end - n - ax * 8.0, end + n - ax * 8.0], glow); - painter.line_segment([cam.to_screen(end + n), cam.to_screen(end - n)], Stroke::new((0.3 * cam.scale).max(1.5), ADMITTING)); + painter.line_segment( + [cam.to_screen(end + n), cam.to_screen(end - n)], + Stroke::new((0.3 * cam.scale).max(1.5), ADMITTING), + ); } } } @@ -1049,7 +1263,11 @@ fn base_color(v: &Vehicle, by: ColorBy) -> Color32 { Turn::T => Color32::from_rgb(200, 200, 205), Turn::R => Color32::from_rgb(255, 150, 70), }, - ColorBy::Speed => lerp_color(Color32::from_rgb(220, 60, 60), Color32::from_rgb(70, 210, 120), (v.v / v.v0.max(1.0)) as f32), + ColorBy::Speed => lerp_color( + Color32::from_rgb(220, 60, 60), + Color32::from_rgb(70, 210, 120), + (v.v / v.v0.max(1.0)) as f32, + ), } } @@ -1080,13 +1298,24 @@ fn paint_vehicle(painter: &egui::Painter, dc: &DrawCtx, v: &Vehicle) { // Window band along both sides and roof units. for side in [1.0, -1.0] { let a = c + ax.right() * (hw * 0.78 * side); - poly(painter, &cam, &rect_pts(a, ax, hl * 0.92, hw * 0.16), shade(Color32::from_rgb(40, 60, 80), tint)); + poly( + painter, + &cam, + &rect_pts(a, ax, hl * 0.92, hw * 0.16), + shade(Color32::from_rgb(40, 60, 80), tint), + ); } if k == 1 || segs.len() == 1 { - poly(painter, &cam, &rect_pts(c, ax, 0.9, hw * 0.5), shade(Color32::from_rgb(220, 220, 220), tint)); + poly( + painter, + &cam, + &rect_pts(c, ax, 0.9, hw * 0.5), + shade(Color32::from_rgb(220, 220, 220), tint), + ); } } else if k > 0 || v.kind == VKind::Truck { - let pts: Vec = rect_pts(c, ax, hl, hw).iter().map(|p| cam.to_screen(*p)).collect(); + let pts: Vec = + rect_pts(c, ax, hl, hw).iter().map(|p| cam.to_screen(*p)).collect(); painter.add(Shape::closed_line(pts, Stroke::new(0.8, shade(body, 0.7)))); } } @@ -1094,7 +1323,12 @@ fn paint_vehicle(painter: &egui::Painter, dc: &DrawCtx, v: &Vehicle) { if !simple { // Windshield. let ws = fc + fax * (fhl - 0.55); - poly(painter, &cam, &rect_pts(ws, fax, 0.28, hw * 0.85), shade(Color32::from_rgb(40, 55, 70), tint)); + poly( + painter, + &cam, + &rect_pts(ws, fax, 0.28, hw * 0.85), + shade(Color32::from_rgb(40, 55, 70), tint), + ); } } _ => { @@ -1102,21 +1336,59 @@ fn paint_vehicle(painter: &egui::Painter, dc: &DrawCtx, v: &Vehicle) { let pts = rect_pts(c, ax, hl, hw); poly(painter, &cam, &pts, body); if v.kind == VKind::Motorcycle { - painter.circle_filled(cam.to_screen(c - ax * 0.1), (0.35 * cam.scale).max(1.5), Color32::from_rgb(25, 25, 25)); + painter.circle_filled( + cam.to_screen(c - ax * 0.1), + (0.35 * cam.scale).max(1.5), + Color32::from_rgb(25, 25, 25), + ); } else if !simple { let glass = shade(Color32::from_rgb(35, 48, 62), tint); // Windshield, roof, rear window. - poly(painter, &cam, &[c + ax * (hl * 0.55) + ax.right() * (hw * 0.82), c + ax * (hl * 0.55) - ax.right() * (hw * 0.82), c + ax * (hl * 0.2) - ax.right() * (hw * 0.72), c + ax * (hl * 0.2) + ax.right() * (hw * 0.72)], glass); - poly(painter, &cam, &rect_pts(c - ax * (hl * 0.12), ax, hl * 0.3, hw * 0.72), shade(body, 0.88)); + poly( + painter, + &cam, + &[ + c + ax * (hl * 0.55) + ax.right() * (hw * 0.82), + c + ax * (hl * 0.55) - ax.right() * (hw * 0.82), + c + ax * (hl * 0.2) - ax.right() * (hw * 0.72), + c + ax * (hl * 0.2) + ax.right() * (hw * 0.72), + ], + glass, + ); + poly( + painter, + &cam, + &rect_pts(c - ax * (hl * 0.12), ax, hl * 0.3, hw * 0.72), + shade(body, 0.88), + ); poly(painter, &cam, &rect_pts(c - ax * (hl * 0.58), ax, hl * 0.13, hw * 0.7), glass); if v.kind == VKind::Emergency { let on = blink; let l1 = if on { Color32::from_rgb(255, 40, 40) } else { Color32::from_rgb(40, 80, 255) }; let l2 = if on { Color32::from_rgb(40, 80, 255) } else { Color32::from_rgb(255, 40, 40) }; - poly(painter, &cam, &rect_pts(c - ax * 0.2 + ax.right() * (hw * 0.4), ax, 0.25, hw * 0.35), l1); - poly(painter, &cam, &rect_pts(c - ax * 0.2 - ax.right() * (hw * 0.4), ax, 0.25, hw * 0.35), l2); - painter.circle_filled(cam.to_screen(c), 4.0 * cam.scale, Color32::from_rgba_unmultiplied(l1.r(), l1.g(), l1.b(), 22)); - poly(painter, &cam, &rect_pts(c + ax * (hl * 0.05), ax, 0.3, hw * 0.9), Color32::from_rgb(210, 40, 40)); + poly( + painter, + &cam, + &rect_pts(c - ax * 0.2 + ax.right() * (hw * 0.4), ax, 0.25, hw * 0.35), + l1, + ); + poly( + painter, + &cam, + &rect_pts(c - ax * 0.2 - ax.right() * (hw * 0.4), ax, 0.25, hw * 0.35), + l2, + ); + painter.circle_filled( + cam.to_screen(c), + 4.0 * cam.scale, + Color32::from_rgba_unmultiplied(l1.r(), l1.g(), l1.b(), 22), + ); + poly( + painter, + &cam, + &rect_pts(c + ax * (hl * 0.05), ax, 0.3, hw * 0.9), + Color32::from_rgb(210, 40, 40), + ); } } } @@ -1144,25 +1416,49 @@ fn paint_vehicle(painter: &egui::Painter, dc: &DrawCtx, v: &Vehicle) { if dc.ov.night { let tip = p + fax * 14.0 + fax.right() * (2.5 * side); let tip2 = p + fax * 14.0 - fax.right() * (1.5 * side); - painter.add(Shape::convex_polygon(vec![cam.to_screen(p), cam.to_screen(tip), cam.to_screen(tip2)], Color32::from_rgba_unmultiplied(255, 240, 180, 26), Stroke::NONE)); + painter.add(Shape::convex_polygon( + vec![cam.to_screen(p), cam.to_screen(tip), cam.to_screen(tip2)], + Color32::from_rgba_unmultiplied(255, 240, 180, 26), + Stroke::NONE, + )); } - painter.circle_filled(cam.to_screen(p), (0.15 * cam.scale).max(0.8), Color32::from_rgb(255, 250, 215)); + painter.circle_filled( + cam.to_screen(p), + (0.15 * cam.scale).max(0.8), + Color32::from_rgb(255, 250, 215), + ); } // Turn indicators near and inside the intersection. let link = &sim.net.links[v.link()]; let near = link.kind.in_box() || (link.kind == LinkKind::Inbound && link.length() - v.s < 70.0); if near && v.turn != Turn::T && blink { let side = if v.turn == Turn::L { -1.0 } else { 1.0 }; - for p in [front - fax * 0.2 + fax.right() * (hw * side), rear + lax * 0.2 + lax.right() * (lhw * side)] { - painter.circle_filled(cam.to_screen(p), (0.2 * cam.scale).max(1.2), Color32::from_rgb(255, 170, 30)); + for p in + [front - fax * 0.2 + fax.right() * (hw * side), rear + lax * 0.2 + lax.right() * (lhw * side)] + { + painter.circle_filled( + cam.to_screen(p), + (0.2 * cam.scale).max(1.2), + Color32::from_rgb(255, 170, 30), + ); } } if v.manual { - painter.circle_stroke(cam.to_screen(fc), (1.2 * cam.scale).max(4.0), Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 255, 255, 90))); + painter.circle_stroke( + cam.to_screen(fc), + (1.2 * cam.scale).max(4.0), + Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 255, 255, 90)), + ); } if dc.ov.labels && cam.scale > 5.0 { let txt = format!("{:.0}s", v.delay); - painter.text(cam.to_screen(lc), Align2::CENTER_CENTER, txt, FontId::proportional(10.0), Color32::WHITE); + painter.text( + cam.to_screen(lc), + Align2::CENTER_CENTER, + txt, + FontId::proportional(10.0), + Color32::WHITE, + ); } if v.reason == Reason::Yielding && dc.ov.labels { painter.circle_filled(cam.to_screen(front + fax * 1.0), 2.5, Color32::from_rgb(255, 160, 60)); @@ -1199,16 +1495,22 @@ pub fn paint_detections(painter: &egui::Painter, dc: &DrawCtx) { } } Source::Radar => { - painter.circle_stroke(cam.to_screen(c), (hl as f32 * 0.6 * cam.scale).max(4.0), Stroke::new(1.0, Color32::from_rgba_unmultiplied(col.r(), col.g(), col.b(), 140))); + painter.circle_stroke( + cam.to_screen(c), + (hl as f32 * 0.6 * cam.scale).max(4.0), + Stroke::new(1.0, Color32::from_rgba_unmultiplied(col.r(), col.g(), col.b(), 140)), + ); } Source::Cv => { let p = cam.to_screen(c); for r in [3.0f32, 6.0] { painter.add(Shape::Path(PathShape::line( - (0..7).map(|i| { - let a = -2.3 + i as f32 * 0.27; - p + egui::vec2(a.cos(), a.sin()) * r * (cam.scale / 4.0).max(0.8) - }).collect(), + (0..7) + .map(|i| { + let a = -2.3 + i as f32 * 0.27; + p + egui::vec2(a.cos(), a.sin()) * r * (cam.scale / 4.0).max(0.8) + }) + .collect(), PathStroke::new(1.2, col), ))); } @@ -1225,7 +1527,11 @@ fn paint_ped(painter: &egui::Painter, dc: &DrawCtx, p: &crate::sim::Ped) { let cw = &sim.net.crosswalks[p.cw]; let dir = (cw.b - cw.a).norm() * p.dir; let fwd = if p.state == PedState::Leaving { sim.net.legs[cw.leg].d } else { dir }; - let alpha = if p.state == PedState::Leaving { (255.0 * (1.0 - p.t_state / 2.0)).clamp(0.0, 255.0) as u8 } else { 255 }; + let alpha = if p.state == PedState::Leaving { + (255.0 * (1.0 - p.t_state / 2.0)).clamp(0.0, 255.0) as u8 + } else { + 255 + }; let shirts = [ Color32::from_rgb(220, 80, 80), Color32::from_rgb(80, 120, 220), @@ -1243,12 +1549,19 @@ fn paint_ped(painter: &egui::Painter, dc: &DrawCtx, p: &crate::sim::Ped) { for (s, sw) in [(1.0, swing), (-1.0, -swing)] { let hip = pos + side * (0.12 * s); let foot = hip + fwd * sw; - painter.line_segment([cam.to_screen(hip), cam.to_screen(foot)], Stroke::new((0.12 * cam.scale).max(1.0), Color32::from_rgba_unmultiplied(40, 40, 50, alpha))); + painter.line_segment( + [cam.to_screen(hip), cam.to_screen(foot)], + Stroke::new((0.12 * cam.scale).max(1.0), Color32::from_rgba_unmultiplied(40, 40, 50, alpha)), + ); } // Shoulders and head. let sh = rect_pts(pos, side, 0.28, 0.14); poly(painter, &cam, &sh, shirt); - painter.circle_filled(cam.to_screen(pos), (0.13 * cam.scale).max(1.0), Color32::from_rgba_unmultiplied(70, 50, 40, alpha)); + painter.circle_filled( + cam.to_screen(pos), + (0.13 * cam.scale).max(1.0), + Color32::from_rgba_unmultiplied(70, 50, 40, alpha), + ); if p.state == PedState::Waiting && cam.scale > 3.0 { let wait = sim.t - p.arrive_t; if wait > 20.0 { @@ -1269,23 +1582,46 @@ pub fn hud(painter: &egui::Painter, rect: Rect, sim: &Sim, ctrl_name: &str, spee ctrl_name ); let pos = rect.left_top() + egui::vec2(12.0, 10.0); - let r = painter.text(pos + egui::vec2(1.0, 1.0), Align2::LEFT_TOP, &txt, FontId::monospace(14.0), Color32::from_black_alpha(160)); + let r = painter.text( + pos + egui::vec2(1.0, 1.0), + Align2::LEFT_TOP, + &txt, + FontId::monospace(14.0), + Color32::from_black_alpha(160), + ); painter.text(pos, Align2::LEFT_TOP, &txt, FontId::monospace(14.0), Color32::WHITE); if let Some(c) = &sim.coop { let mut line = if c.scheduling == Scheduling::FirstCome { format!("First come, first served · {} vehicles scheduled", c.plans.len()) } else { - let names: Vec<&str> = c.ring_phases().into_iter().flatten().map(|m| sim.net.movements[m].name.as_str()).collect(); + let names: Vec<&str> = + c.ring_phases().into_iter().flatten().map(|m| sim.net.movements[m].name.as_str()).collect(); let group = if c.group() == 0 { "N–S" } else { "E–W" }; let serving = if names.is_empty() { "switching".to_string() } else { names.join(" + ") }; format!("Serving {group}: {serving} · {} vehicles scheduled", c.plans.len()) }; - if sim.vehicles.iter().any(|v| v.kind == VKind::Emergency && c.managed(v.movement) && !c.plans.contains_key(&v.id)) { + if sim + .vehicles + .iter() + .any(|v| v.kind == VKind::Emergency && c.managed(v.movement) && !c.plans.contains_key(&v.id)) + { line.push_str(" · 🚨 EMERGENCY VEHICLE FIRST"); } let p2 = pos + egui::vec2(0.0, r.height() + 4.0); - painter.text(p2 + egui::vec2(1.0, 1.0), Align2::LEFT_TOP, &line, FontId::proportional(13.0), Color32::from_black_alpha(160)); - painter.text(p2, Align2::LEFT_TOP, &line, FontId::proportional(13.0), Color32::from_rgb(200, 245, 255)); + painter.text( + p2 + egui::vec2(1.0, 1.0), + Align2::LEFT_TOP, + &line, + FontId::proportional(13.0), + Color32::from_black_alpha(160), + ); + painter.text( + p2, + Align2::LEFT_TOP, + &line, + FontId::proportional(13.0), + Color32::from_rgb(200, 245, 255), + ); } if sim.signalized() { let st = &sim.net.stages[sim.signal.stage]; @@ -1299,8 +1635,20 @@ pub fn hud(painter: &egui::Painter, rect: Rect, sim: &Sim, ctrl_name: &str, spee line.push_str(" · 🚨 EMERGENCY PREEMPTION"); } let p2 = pos + egui::vec2(0.0, r.height() + 4.0); - painter.text(p2 + egui::vec2(1.0, 1.0), Align2::LEFT_TOP, &line, FontId::proportional(13.0), Color32::from_black_alpha(160)); - painter.text(p2, Align2::LEFT_TOP, &line, FontId::proportional(13.0), Color32::from_rgb(230, 240, 255)); + painter.text( + p2 + egui::vec2(1.0, 1.0), + Align2::LEFT_TOP, + &line, + FontId::proportional(13.0), + Color32::from_black_alpha(160), + ); + painter.text( + p2, + Align2::LEFT_TOP, + &line, + FontId::proportional(13.0), + Color32::from_rgb(230, 240, 255), + ); } } diff --git a/src/lib.rs b/src/lib.rs index 5bf37c3..41c59b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,8 @@ //! signal controllers (fixed, actuated, max-pressure, predictive MPC, deep RL). pub mod control; -pub mod rl; pub mod gui; +pub mod rl; pub mod sim; use control::CtrlKind; @@ -25,7 +25,14 @@ pub fn run_scenario(mut sim: Sim, warmup: f64, duration: f64) -> (Summary, u64) } /// A fresh headless simulation of a scenario, ready to step. -pub fn scenario(kind: LayoutKind, mode: ControlMode, ctrl: CtrlKind, demand: &DemandConfig, sensors: &SensorConfig, seed: u64) -> Sim { +pub fn scenario( + kind: LayoutKind, + mode: ControlMode, + ctrl: CtrlKind, + demand: &DemandConfig, + sensors: &SensorConfig, + seed: u64, +) -> Sim { let net = Network::build(kind); let mut sim = Sim::with_demand(net, mode, ctrl, demand.clone(), seed); sim.sensors = sensors.clone(); diff --git a/src/main.rs b/src/main.rs index c6f2f0b..182bd4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,8 @@ #[cfg(not(target_arch = "wasm32"))] fn main() -> eframe::Result { - let icon = egui::IconData { rgba: include_bytes!("../assets/icon_128.rgba").to_vec(), width: 128, height: 128 }; + let icon = + egui::IconData { rgba: include_bytes!("../assets/icon_128.rgba").to_vec(), width: 128, height: 128 }; let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([1600.0, 1000.0]) @@ -18,7 +19,11 @@ fn main() -> eframe::Result { .with_icon(icon), ..Default::default() }; - eframe::run_native("Intersection Lab", options, Box::new(|cc| Ok(Box::new(intersection_lab::gui::App::new(cc))))) + eframe::run_native( + "Intersection Lab", + options, + Box::new(|cc| Ok(Box::new(intersection_lab::gui::App::new(cc)))), + ) } /// Browser entry point: draw into the page's canvas (see web/index.html). @@ -32,7 +37,11 @@ fn main() { .and_then(|e| e.dyn_into::().ok()) .expect("no "); let result = eframe::WebRunner::new() - .start(canvas, eframe::WebOptions::default(), Box::new(|cc| Ok(Box::new(intersection_lab::gui::App::new(cc))))) + .start( + canvas, + eframe::WebOptions::default(), + Box::new(|cc| Ok(Box::new(intersection_lab::gui::App::new(cc)))), + ) .await; // Replace the loading message with the outcome. if let Some(status) = document.get_element_by_id("status") { diff --git a/src/rl/dqn.rs b/src/rl/dqn.rs index 1260847..a2ca383 100644 --- a/src/rl/dqn.rs +++ b/src/rl/dqn.rs @@ -57,7 +57,8 @@ impl SavedModel { impl Dqn { pub fn new(n_in: usize, n_out: usize, hidden: &[usize], seed: u64) -> Dqn { let mut rng = Rng::new(seed); - let sizes: Vec = std::iter::once(n_in).chain(hidden.iter().copied()).chain(std::iter::once(n_out)).collect(); + let sizes: Vec = + std::iter::once(n_in).chain(hidden.iter().copied()).chain(std::iter::once(n_out)).collect(); let online = Mlp::new(&sizes, &mut rng); Self::from_net(online, rng) } diff --git a/src/sim/coop.rs b/src/sim/coop.rs index 3573e49..a2f87f7 100644 --- a/src/sim/coop.rs +++ b/src/sim/coop.rs @@ -136,7 +136,9 @@ impl GapPolicy { let base = 2.0 * LATENCY + ACTUATION; match self { GapPolicy::FailSafe => base + v / (2.0 * brake), - GapPolicy::Mitigated if v > IMPACT_OK => base + (v * v - IMPACT_OK * IMPACT_OK) / (2.0 * brake * v), + GapPolicy::Mitigated if v > IMPACT_OK => { + base + (v * v - IMPACT_OK * IMPACT_OK) / (2.0 * brake * v) + } GapPolicy::Mitigated | GapPolicy::Nominal => base, } } @@ -299,8 +301,16 @@ fn fastest(d: f64, v: f64, ve: f64, vtop: f64, a: f64, b: f64) -> (f64, Prof, Op /// Speed `v` to cruise speed `u`, cruise, then `u` to `ve` at the line, over `d`. /// None if it does not fit. fn dca(d: f64, v: f64, u: f64, ve: f64, a: f64, b: f64) -> Option<(f64, Prof)> { - let (t1, d1, a1) = if u < v { ((v - u) / b, (v * v - u * u) / (2.0 * b), -b) } else { ((u - v) / a, (u * u - v * v) / (2.0 * a), a) }; - let (t3, d3, a3) = if ve > u { ((ve - u) / a, (ve * ve - u * u) / (2.0 * a), a) } else { ((u - ve) / b, (u * u - ve * ve) / (2.0 * b), -b) }; + let (t1, d1, a1) = if u < v { + ((v - u) / b, (v * v - u * u) / (2.0 * b), -b) + } else { + ((u - v) / a, (u * u - v * v) / (2.0 * a), a) + }; + let (t3, d3, a3) = if ve > u { + ((ve - u) / a, (ve * ve - u * u) / (2.0 * a), a) + } else { + ((u - ve) / b, (u * u - ve * ve) / (2.0 * b), -b) + }; let d2 = d - d1 - d3; if d2 < -1e-9 || (d2 > 1e-9 && u < 1e-9) { return None; @@ -622,7 +632,19 @@ fn simulate(r: &Req, target: f64) -> Option { need: r.policy.gap(w.2, av.brake), }) .collect(); - Some(Plan { k0: r.k, t0: r.t, x: xs, v: vs, a: acs, why, t_line, v_line, zones, movement: r.movement, lane: r.lane }) + Some(Plan { + k0: r.k, + t0: r.t, + x: xs, + v: vs, + a: acs, + why, + t_line, + v_line, + zones, + movement: r.movement, + lane: r.lane, + }) } // ---------------------------------------------------------------- manager @@ -849,7 +871,9 @@ impl CoopManager { chains.insert(*in_id, (VecDeque::new(), 0)); } } - let conns = |m: usize| -> Vec { net.movements[m].routes.iter().filter_map(|r| r.1.first().copied()).collect() }; + let conns = |m: usize| -> Vec { + net.movements[m].routes.iter().filter_map(|r| r.1.first().copied()).collect() + }; let mut mconf = vec![vec![false; n_moves]; n_moves]; for m in 0..n_moves { for n in 0..n_moves { @@ -922,7 +946,9 @@ impl CoopManager { /// critical phases, as in Webster's split. Until there is any measured demand every /// phase may run the whole cycle less the lost time. pub fn max_green(&self, m: usize) -> f64 { - let y = |m: usize| self.arrivals[m] / self.arr_time.max(1.0) / self.n_lanes[m].max(1) as f64 * self.sat_h[m]; + let y = |m: usize| { + self.arrivals[m] / self.arr_time.max(1.0) / self.n_lanes[m].max(1) as f64 * self.sat_h[m] + }; let crit: f64 = (0..2) .map(|g| { self.rings @@ -1027,7 +1053,9 @@ impl CoopManager { /// Does a trajectory fit around every reservation? fn fits(&self, p: &Plan) -> bool { p.zones.iter().all(|z| { - self.res[z.cid][1 - z.side].iter().all(|r| z.t_in >= r.t_out + z.need || r.t_in >= z.t_out + r.need) + self.res[z.cid][1 - z.side] + .iter() + .all(|r| z.t_in >= r.t_out + z.need || r.t_in >= z.t_out + r.need) }) } @@ -1037,7 +1065,11 @@ impl CoopManager { let mut target = r.t; for n in 1..=12 { let Some(p) = simulate(r, target) else { return (None, n) }; - let rel: Vec<_> = p.zones.iter().map(|z| (z.cid, z.side, z.t_in - p.t_line, z.t_out - p.t_line, z.need)).collect(); + let rel: Vec<_> = p + .zones + .iter() + .map(|z| (z.cid, z.side, z.t_in - p.t_line, z.t_out - p.t_line, z.need)) + .collect(); let t_ok = self.earliest(&rel, p.t_line, r.opens_batch); if t_ok <= p.t_line + 1e-9 { if self.fits(&p) { @@ -1125,15 +1157,7 @@ impl CoopManager { .collect(); let t_est = self.earliest(&zones, t_lo, false); let h = Self::headway(&av, v.len(), info.v_box); - out[info.movement].push(Cand { - vid, - idx: i, - lane, - movement: info.movement, - leader, - t_est, - h, - }); + out[info.movement].push(Cand { vid, idx: i, lane, movement: info.movement, leader, t_est, h }); } for c in out.iter_mut() { c.sort_by(|a, b| a.t_est.total_cmp(&b.t_est)); @@ -1208,7 +1232,9 @@ impl CoopManager { /// the through movement within a fraction of a second. fn order_rings(&mut self) { let groups: Vec>> = (0..2) - .map(|g| self.rings.iter().map(|r| r.seq.iter().filter(|x| x.0 == g).map(|x| x.1).collect()).collect()) + .map(|g| { + self.rings.iter().map(|r| r.seq.iter().filter(|x| x.0 == g).map(|x| x.1).collect()).collect() + }) .collect(); let n_rings = self.rings.len(); // Bit (g * n_rings + r) set: ring r serves group g in reverse order. @@ -1248,14 +1274,16 @@ impl CoopManager { /// longest clearance across each barrier. fn cycle_cost(&self, seqs: &[Vec<(usize, usize)>]) -> f64 { let d = |m: usize, n: usize| self.intergreen(m, n).unwrap_or(0.0); - let part = |r: usize, g: usize| -> Vec { seqs[r].iter().filter(|x| x.0 == g).map(|x| x.1).collect() }; + let part = + |r: usize, g: usize| -> Vec { seqs[r].iter().filter(|x| x.0 == g).map(|x| x.1).collect() }; let mut cost = 0.0; for g in 0..2 { for r in 0..seqs.len() { cost += part(r, g).windows(2).map(|w| d(w[0], w[1])).sum::(); } let lasts: Vec = (0..seqs.len()).filter_map(|r| part(r, g).last().copied()).collect(); - let firsts: Vec = (0..seqs.len()).filter_map(|r| part(r, 1 - g).first().copied()).collect(); + let firsts: Vec = + (0..seqs.len()).filter_map(|r| part(r, 1 - g).first().copied()).collect(); cost += lasts.iter().flat_map(|&m| firsts.iter().map(move |&n| d(m, n))).fold(0.0, f64::max); } cost @@ -1380,7 +1408,9 @@ impl CoopManager { (if st.granted > 0 { st.first_t } else { t }) + self.max_green(m) }; let mut limit = own(m); - let Some(r) = self.rings.iter().position(|ring| !ring.at_barrier && ring.seq[ring.pos].1 == m) else { return limit }; + let Some(r) = self.rings.iter().position(|ring| !ring.at_barrier && ring.seq[ring.pos].1 == m) else { + return limit; + }; let ring = &self.rings[r]; if ring.seq[(ring.pos + 1) % ring.seq.len()].0 == self.group { return limit; @@ -1389,7 +1419,11 @@ impl CoopManager { if q == r || other.at_barrier { continue; } - let rest: f64 = other.seq[other.pos + 1..].iter().take_while(|x| x.0 == self.group).map(|x| self.max_green(x.1)).sum(); + let rest: f64 = other.seq[other.pos + 1..] + .iter() + .take_while(|x| x.0 == self.group) + .map(|x| self.max_green(x.1)) + .sum(); limit = limit.max(own(other.seq[other.pos].1) + rest); } limit @@ -1397,7 +1431,16 @@ impl CoopManager { /// Grant the next vehicle of the open movement `m` that can continue its batch. #[allow(clippy::too_many_arguments)] - fn serve(&mut self, m: usize, cands: &[Vec], veh: &[Vehicle], t: f64, k: u64, dt: f64, missed: &mut Vec) -> Served { + fn serve( + &mut self, + m: usize, + cands: &[Vec], + veh: &[Vehicle], + t: f64, + k: u64, + dt: f64, + missed: &mut Vec, + ) -> Served { for c in &cands[m] { if missed.contains(&c.vid) { continue; @@ -1453,7 +1496,12 @@ impl CoopManager { let st = &mut self.moves[m]; st.open = false; if st.granted > 0 { - self.history.push_back(Batch { movement: m, start: st.first_t, end: st.frontier, vehicles: st.granted }); + self.history.push_back(Batch { + movement: m, + start: st.first_t, + end: st.frontier, + vehicles: st.granted, + }); } if why == PhaseEnd::MaxOut { self.stats.maxed += 1; @@ -1503,7 +1551,14 @@ impl CoopManager { } if self.scheduling == Scheduling::FirstCome { // Whoever can reach the line first gets the earliest slot that fits. - let Some(c) = cands.iter().flatten().min_by(|a, b| a.t_est.total_cmp(&b.t_est).then(a.vid.cmp(&b.vid))).copied() else { break }; + let Some(c) = cands + .iter() + .flatten() + .min_by(|a, b| a.t_est.total_cmp(&b.t_est).then(a.vid.cmp(&b.vid))) + .copied() + else { + break; + }; let (plan, sims) = self.plan_search(&self.request(veh, &c, t, k, dt)); self.stats.sims += sims; match plan { @@ -1593,7 +1648,10 @@ impl CoopManager { if xp < l_in && x >= l_in { self.stats.unauthorized += 1; if self.log.len() < 40 { - self.log.push(format!("t={t:.1} vehicle #{} entered the box without a reservation", v.id)); + self.log.push(format!( + "t={t:.1} vehicle #{} entered the box without a reservation", + v.id + )); } } } @@ -1605,7 +1663,11 @@ impl CoopManager { if self.log.len() < 40 { self.log.push(format!( "t={t:.1} vehicle #{} off its trajectory: x {:.4} vs {:.4}, v {:.4} vs {:.4}", - v.id, x, p.x[j + 1], v.v, p.v[j + 1] + v.id, + x, + p.x[j + 1], + v.v, + p.v[j + 1] )); } } @@ -1636,7 +1698,10 @@ impl CoopManager { if self.inside[cid][other] > 0 { self.stats.zone_violations += 1; if self.log.len() < 40 { - self.log.push(format!("t={t:.1} vehicle #{} entered zone {cid} while it was occupied", v.id)); + self.log.push(format!( + "t={t:.1} vehicle #{} entered zone {cid} while it was occupied", + v.id + )); } } let pet = time - self.last_out[cid][other]; @@ -1866,7 +1931,15 @@ mod tests { let mut min_gap = f64::INFINITY; for k in 0..1200 { let t = k as f64 * dt; - let al: f64 = if t < 5.0 { 0.0 } else if t < 9.4 { -2.5 } else if t < 20.0 { 0.0 } else { 1.5 }; + let al: f64 = if t < 5.0 { + 0.0 + } else if t < 9.4 { + -2.5 + } else if t < 20.0 { + 0.0 + } else { + 1.5 + }; let al = if vl + al * dt > 11.1 && t > 20.0 { 0.0 } else { al }; let gap = xl - 4.5 - x; min_gap = min_gap.min(gap); @@ -1883,4 +1956,3 @@ mod tests { assert!((gap - (2.0 + 0.42 * v)).abs() < 0.3, "settled at {gap:.2} m at {v:.2} m/s"); } } - diff --git a/src/sim/demand.rs b/src/sim/demand.rs index 2c10c4a..e61ce77 100644 --- a/src/sim/demand.rs +++ b/src/sim/demand.rs @@ -202,7 +202,14 @@ impl DemandGen { } /// Draw arrivals in (t, t+dt] using thinning of a non-homogeneous Poisson process. - pub fn generate(&mut self, cfg: &DemandConfig, net: &Network, t: f64, dt: f64, out: &mut Vec<(usize, Pending)>) { + pub fn generate( + &mut self, + cfg: &DemandConfig, + net: &Network, + t: f64, + dt: f64, + out: &mut Vec<(usize, Pending)>, + ) { let mmax = Self::max_multiplier(cfg.pattern); for li in 0..net.legs.len() { let ci = compass_index(net.legs[li].compass); @@ -232,9 +239,9 @@ impl DemandGen { let shares = cfg.valid_shares(net, leg); let ti = self.rng.weighted(&shares); let turn = Turn::ALL[ti]; - let movement = net.movement_for(leg, turn).unwrap_or_else(|| { - net.movements.iter().position(|m| m.leg == leg).expect("leg has movements") - }); + let movement = net + .movement_for(leg, turn) + .unwrap_or_else(|| net.movements.iter().position(|m| m.leg == leg).expect("leg has movements")); let mut mix = cfg.mix; if mix.iter().sum::() <= 0.0 { mix[0] = 1.0; diff --git a/src/sim/mod.rs b/src/sim/mod.rs index 4e83d15..396055b 100644 --- a/src/sim/mod.rs +++ b/src/sim/mod.rs @@ -327,8 +327,18 @@ impl Sim { Self::with_demand(net, mode, ctrl, demand, seed) } - pub fn with_demand(net: Network, mode: ControlMode, ctrl: CtrlKind, demand: DemandConfig, seed: u64) -> Sim { - let mode = if ControlMode::options(net.kind).contains(&mode) { mode } else { ControlMode::options(net.kind)[0] }; + pub fn with_demand( + net: Network, + mode: ControlMode, + ctrl: CtrlKind, + demand: DemandConfig, + seed: u64, + ) -> Sim { + let mode = if ControlMode::options(net.kind).contains(&mode) { + mode + } else { + ControlMode::options(net.kind)[0] + }; let signal = SignalState::new(&net, 0.0); let sensors = SensorConfig::default(); let controller = control::make(ctrl, &net, &demand); @@ -518,12 +528,18 @@ impl Sim { // Agency rule of thumb: protect the left when (left volume) x (opposing // through volume) exceeds 50k / 90k / 110k for 1 / 2 / 3+ opposing lanes. let mv = &self.net.movements[m]; - let opp_leg = self.net.legs.iter().position(|l| l.d.dist(-self.net.legs[mv.leg].d) < 1e-6); + let opp_leg = + self.net.legs.iter().position(|l| l.d.dist(-self.net.legs[mv.leg].d) < 1e-6); let opp = opp_leg.and_then(|l| self.net.movement_for(l, Turn::T)); let window = 600.0_f64.min(self.t.max(60.0)); let vol = |k: usize| { - self.stats.done.iter().rev().take_while(|d| d.t_exit > self.t - window).filter(|d| d.movement == k).count() - as f64 + self.stats + .done + .iter() + .rev() + .take_while(|d| d.t_exit > self.t - window) + .filter(|d| d.movement == k) + .count() as f64 * 3600.0 / window }; @@ -543,7 +559,10 @@ impl Sim { } LeftMode::Auto => self.left_protected[m], }; - if want != self.left_protected[m] && self.signal.status(&self.net, m) == Sig::R && self.signal.next == self.signal.stage { + if want != self.left_protected[m] + && self.signal.status(&self.net, m) == Sig::R + && self.signal.next == self.signal.stage + { self.left_protected[m] = want; changed = true; } @@ -554,7 +573,11 @@ impl Sim { .stat .iter() .enumerate() - .map(|(m, &x)| if self.left_protected[m] && x == Some(network::MStat::P) { None } else { x }) + .map( + |(m, &x)| { + if self.left_protected[m] && x == Some(network::MStat::P) { None } else { x } + }, + ) .collect(); self.net.stages[st].walk = self.net.walk_for(&stat); self.net.stages[st].stat = stat; @@ -591,7 +614,9 @@ impl Sim { let target = (0..self.net.stages.len()) .find(|&st| self.net.stages[st].stat[m] == Some(network::MStat::G)) .or_else(|| (0..self.net.stages.len()).find(|&st| serves(st))); - if let Some(j) = target && self.signal.green_elapsed > 2.0 { + if let Some(j) = target + && self.signal.green_elapsed > 2.0 + { self.signal.request(&self.net, j, self.t); } } @@ -646,7 +671,10 @@ impl Sim { g.clear(); } for (i, v) in self.vehicles.iter().enumerate() { - if let Some(l) = v.lc_from && v.ri == 0 && v.lc_t < GHOST_T { + if let Some(l) = v.lc_from + && v.ri == 0 + && v.lc_t < GHOST_T + { self.ghosts[l].push(i); } } @@ -710,7 +738,11 @@ impl Sim { (true, _) => speed * spec.speed_factor.min(1.0), (false, _) => speed * spec.speed_factor * p.speed_factor, }; - let v_safe = if room < 60.0 { lead_v.min(v0).max(((room - spec.min_gap) * 2.0 * spec.decel).sqrt().min(lead_v + 2.0)) } else { v0 }; + let v_safe = if room < 60.0 { + lead_v.min(v0).max(((room - spec.min_gap) * 2.0 * spec.decel).sqrt().min(lead_v + 2.0)) + } else { + v0 + }; let v = v_safe.min(v0).max(0.0); let ff = self.free_flow_time(p.movement, &route, p.kind, v0); let id = self.next_id; @@ -911,7 +943,11 @@ impl Sim { st.reason_stop = 1; } Sig::R => { - if v.turn == Turn::R && self.rtor_ok(v.movement) && v.stop_done && self.t - v.stop_t > 1.0 { + if v.turn == Turn::R + && self.rtor_ok(v.movement) + && v.stop_done + && self.t - v.stop_t > 1.0 + { st.rank = 0.0; st.hold_line = true; } else { @@ -978,7 +1014,9 @@ impl Sim { self.rtor && !self.signal.lpi_held(m) && !(self.ntor_peds - && self.peds.iter().any(|p| p.state != PedState::Leaving && self.net.movements[m].crosswalks.contains(&p.cw))) + && self.peds.iter().any(|p| { + p.state != PedState::Leaving && self.net.movements[m].crosswalks.contains(&p.cw) + })) } fn route_index(v: &Vehicle, link: LinkId) -> Option { @@ -1121,11 +1159,12 @@ impl Sim { } else if sw.rank < sa.rank { false } else { - let (ka, kw) = if sa.key < 1e8 && sw.key < 1e8 && sa.key < self.t + 1e2 && sw.key < self.t + 1e2 { - (sa.key, sw.key) - } else { - (self.t + ta_in, self.t + tv_in) - }; + let (ka, kw) = + if sa.key < 1e8 && sw.key < 1e8 && sa.key < self.t + 1e2 && sw.key < self.t + 1e2 { + (sa.key, sw.key) + } else { + (self.t + ta_in, self.t + tv_in) + }; kw < ka || (kw == ka && w.id < a.id) }; if !w_priority { @@ -1151,15 +1190,23 @@ impl Sim { let (_, other, s_other) = c.side(my_link); let (_, (zv0, zv1)) = c.zones(my_link); self.threats(other, s_other, zv1, i, buf); - buf.iter().find(|&&(j, d_v)| { - let w = &self.vehicles[j]; - w.v < 0.5 && d_v - zv0 < 0.3 && d_v + zv1 + w.len() > 0.0 - }).map(|x| x.0) + buf.iter() + .find(|&&(j, d_v)| { + let w = &self.vehicles[j]; + w.v < 0.5 && d_v - zv0 < 0.3 && d_v + zv1 + w.len() > 0.0 + }) + .map(|x| x.0) } /// Another vehicle committed to crossing zone `cid` that can no longer stop before it /// and will get there while we would still be in it. - fn unstoppable_rival(&self, i: usize, cid: usize, d_a: f64, buf: &mut Vec<(usize, f64)>) -> Option { + fn unstoppable_rival( + &self, + i: usize, + cid: usize, + d_a: f64, + buf: &mut Vec<(usize, f64)>, + ) -> Option { let a = &self.vehicles[i]; let c = &self.net.conflicts[cid]; if c.merge { @@ -1171,18 +1218,27 @@ impl Sim { let spec = a.spec(); let ta_out = Self::eta(d_a + za1 + spec.length, a.v, spec.accel * 0.7, self.vmax_on(a, my_link)); self.threats(other, s_other, zv1, i, buf); - buf.iter().find(|&&(j, d_v)| { - let w = &self.vehicles[j]; - w.commits.contains(&cid) - && w.v > 1.0 - && w.v * w.v / (2.0 * (d_v - zv0).max(0.1)) > 4.5 - && Self::eta(d_v - zv0, w.v, w.spec().accel, self.vmax_on(w, other)) < ta_out + 0.8 - }).map(|x| x.0) + buf.iter() + .find(|&&(j, d_v)| { + let w = &self.vehicles[j]; + w.commits.contains(&cid) + && w.v > 1.0 + && w.v * w.v / (2.0 * (d_v - zv0).max(0.1)) > 4.5 + && Self::eta(d_v - zv0, w.v, w.spec().accel, self.vmax_on(w, other)) < ta_out + 0.8 + }) + .map(|x| x.0) } /// The vehicle, if any, that a later conflict on link `lid` (beyond `s_from`) would /// make us yield to now. - fn rest_blocker(&self, i: usize, lid: LinkId, s_from: f64, base: f64, buf: &mut Vec<(usize, f64)>) -> Option { + fn rest_blocker( + &self, + i: usize, + lid: LinkId, + s_from: f64, + base: f64, + buf: &mut Vec<(usize, f64)>, + ) -> Option { let v = &self.vehicles[i]; for &cid in &self.net.links[lid].conflicts { let (s_c, _, _) = self.net.conflicts[cid].side(lid); @@ -1207,7 +1263,9 @@ impl Sim { buf.iter().any(|&(j, d_v)| { let w = &self.vehicles[j]; let sj = self.vstat[j]; - (!sj.excluded || sj.reason_stop == 1) && d_v > -1.0 && Self::eta(d_v, w.v, w.spec().accel, w.v0) < 12.0 + (!sj.excluded || sj.reason_stop == 1) + && d_v > -1.0 + && Self::eta(d_v, w.v, w.spec().accel, w.v0) < 12.0 }) } @@ -1250,7 +1308,8 @@ impl Sim { // ---------------------------------------------------------------- driving fn idm(spec: &VSpec, v: f64, v0: f64, gap: f64, dv: f64) -> f64 { - let s_star = spec.min_gap + (v * spec.headway + v * dv / (2.0 * (spec.accel * spec.decel).sqrt())).max(0.0); + let s_star = + spec.min_gap + (v * spec.headway + v * dv / (2.0 * (spec.accel * spec.decel).sqrt())).max(0.0); spec.accel * (1.0 - (v / v0.max(0.1)).powi(4) - (s_star / gap.max(0.05)).powi(2)) } @@ -1303,7 +1362,14 @@ impl Sim { /// Leaders on links that branch from `from` alongside `mine` (diverges), where /// `base` is the distance from our front to the start of `mine`. - fn scan_siblings(&self, i: usize, from: LinkId, mine: LinkId, base: f64, best: &mut Option<(f64, f64, usize)>) { + fn scan_siblings( + &self, + i: usize, + from: LinkId, + mine: LinkId, + base: f64, + best: &mut Option<(f64, f64, usize)>, + ) { let v = &self.vehicles[i]; for &sib in &self.net.links[from].succ { if sib == mine { @@ -1342,7 +1408,8 @@ impl Sim { fn drive(&mut self, dt: f64) { let accs = if self.coop.is_some() { self.coop_accels() } else { self.decide_all() }; // Speeds at the start of the step, for the cooperative monitors. - let v_start: Vec = if self.coop.is_some() { self.vehicles.iter().map(|v| v.v).collect() } else { Vec::new() }; + let v_start: Vec = + if self.coop.is_some() { self.vehicles.iter().map(|v| v.v).collect() } else { Vec::new() }; // Integrate. let mut done_idx = Vec::new(); let mut person_delay = 0.0; @@ -1456,7 +1523,10 @@ impl Sim { fresh.push(c); } } - if let Some(sd) = stop_done && sd && !v.stop_done { + if let Some(sd) = stop_done + && sd + && !v.stop_done + { v.stop_done = true; v.stop_t = self.t; } @@ -1466,12 +1536,30 @@ impl Sim { if cfg!(debug_assertions) || std::env::var_os("TS_DEBUG").is_some() { for c in fresh { for w in &self.vehicles { - if w.id != vid && w.commits.contains(&c) && !w.route.contains(&self.vehicles[i].route[self.vehicles[i].ri.min(self.vehicles[i].route.len()-1)]) && !self.vehicles[i].route.iter().any(|l| w.route[w.ri] == *l) { + if w.id != vid + && w.commits.contains(&c) + && !w.route.contains( + &self.vehicles[i].route + [self.vehicles[i].ri.min(self.vehicles[i].route.len() - 1)], + ) + && !self.vehicles[i].route.iter().any(|l| w.route[w.ri] == *l) + { let me = &self.vehicles[i]; let msg = format!( "t={:.1} DOUBLE commit c{} by #{} {} v={:.1} s={:.1} st={:?} | holder #{} {} v={:.1} s={:.1} blocked={} reason={:?}", - self.t, c, vid, self.net.movements[me.movement].name, me.v, me.s, self.vstat[i], - w.id, self.net.movements[w.movement].name, w.v, w.s, w.blocked, w.reason + self.t, + c, + vid, + self.net.movements[me.movement].name, + me.v, + me.s, + self.vstat[i], + w.id, + self.net.movements[w.movement].name, + w.v, + w.s, + w.blocked, + w.reason ); if self.collision_log.len() < 30 { self.collision_log.push(msg); @@ -1502,7 +1590,9 @@ impl Sim { continue; } let close = self.peds.iter().any(|p| { - p.cw == pc.cw && p.state == PedState::Crossing && ((p.u - pc.u).abs() - 1.0).max(0.0) / p.speed < 1.5 + p.cw == pc.cw + && p.state == PedState::Crossing + && ((p.u - pc.u).abs() - 1.0).max(0.0) / p.speed < 1.5 }); if close { self.stats.ped_near_miss.push(self.t); @@ -1564,7 +1654,11 @@ impl Sim { /// Returns (accel, reason, new commits, stop-done, yellow decision). #[allow(clippy::type_complexity)] - fn decide(&self, i: usize, buf: &mut Vec<(usize, f64)>) -> (f64, Reason, Vec, Option, Option<(u64, bool)>, (bool, Option), Vec) { + fn decide( + &self, + i: usize, + buf: &mut Vec<(usize, f64)>, + ) -> (f64, Reason, Vec, Option, Option<(u64, bool)>, (bool, Option), Vec) { let v = &self.vehicles[i]; let spec = v.spec(); let st = self.vstat[i]; @@ -1592,7 +1686,15 @@ impl Sim { let mut queue_gap = None; if let Some((gap, vl, j)) = self.leader(i) { let w = &self.vehicles[j]; - let waiting = w.blocked || matches!(w.reason, Reason::Yielding | Reason::Pedestrian | Reason::Signal | Reason::StopSign | Reason::LaneChange); + let waiting = w.blocked + || matches!( + w.reason, + Reason::Yielding + | Reason::Pedestrian + | Reason::Signal + | Reason::StopSign + | Reason::LaneChange + ); blocked = v.v < 0.5 && gap < 8.0 && w.v < 0.3 && waiting; if waiting || w.yield_to.is_some() { queue_gap = Some(gap + vl * vl / (2.0 * 3.0) - spec.min_gap + 0.6); @@ -1680,7 +1782,13 @@ impl Sim { line_hold = true; reason = Reason::Signal; } - if sig == Sig::R && v.turn == Turn::R && self.rtor_ok(v.movement) && d_line < 2.5 && v.v < 0.3 && !v.stop_done { + if sig == Sig::R + && v.turn == Turn::R + && self.rtor_ok(v.movement) + && d_line < 2.5 + && v.v < 0.3 + && !v.stop_done + { stop_done = Some(true); } } else if st.must_stop { @@ -1692,7 +1800,11 @@ impl Sim { } } // Don't enter the intersection without room to clear it on the far side. - if on_approach && !line_hold && v.commits.is_empty() && (can_stop(d_line) || v.v < 1.0) && d_line < 30.0 + if on_approach + && !line_hold + && v.commits.is_empty() + && (can_stop(d_line) || v.v < 1.0) + && d_line < 30.0 && let Some(room) = self.exit_space(v) && room < spec.length + spec.min_gap + 1.0 { @@ -1740,7 +1852,9 @@ impl Sim { // inside the zone since, or someone who committed in the same // instant can no longer stop, stop short of it while we still can. if d - zn > 0.3 - && let Some(j) = self.stalled_in_zone(i, cid, buf).or_else(|| self.unstoppable_rival(i, cid, d, buf)) + && let Some(j) = self + .stalled_in_zone(i, cid, buf) + .or_else(|| self.unstoppable_rival(i, cid, d, buf)) { let gap = d - zn - 0.8; if can_stop(gap) || v.v < 1.0 { @@ -1776,7 +1890,10 @@ impl Sim { let brake = v.v * v.v / (2.0 * 3.0) + 3.0; if d - zn < brake || hold_line_mode { // Permissive movers accept a gap only for the rest of the turn. - if st.rank <= 1.0 && !hold_line_mode && let Some(j) = self.rest_blocker(i, lid, s_c, base, buf) { + if st.rank <= 1.0 + && !hold_line_mode + && let Some(j) = self.rest_blocker(i, lid, s_c, base, buf) + { let gap = d - zn - 0.8; if can_stop(gap) || v.v < 1.0 { yield_to = Some(self.vehicles[j].id); @@ -1815,7 +1932,11 @@ impl Sim { // Don't block the box: if stopping at `gap` would leave our body in a conflict // zone (or only a crossing) that live traffic needs, including zones we would // only creep into while waiting, stop before it instead. - let unblock = |mut gap: f64, crossings: bool, drop: &mut Vec, buf: &mut Vec<(usize, f64)>| -> f64 { + let unblock = |mut gap: f64, + crossings: bool, + drop: &mut Vec, + buf: &mut Vec<(usize, f64)>| + -> f64 { let mut ahead = passing.clone(); let mut base = -v.s; for &lid in &v.route[v.ri..] { @@ -1837,7 +1958,10 @@ impl Sim { loop { let mut changed = false; for &(cid, d, zone, zn) in ahead.iter().rev() { - if drop.contains(&cid) || (crossings && self.net.conflicts[cid].merge) || !(d - zn < gap && d + zone > gap - spec.length) { + if drop.contains(&cid) + || (crossings && self.net.conflicts[cid].merge) + || !(d - zn < gap && d + zone > gap - spec.length) + { continue; } let g2 = d - zn - 0.8; @@ -1883,7 +2007,15 @@ impl Sim { } } } - (acc.clamp(-9.0, spec.accel), reason, commits, stop_done, ydec, (blocked, if stop_gap.is_some() { yield_to } else { None }), drop) + ( + acc.clamp(-9.0, spec.accel), + reason, + commits, + stop_done, + ydec, + (blocked, if stop_gap.is_some() { yield_to } else { None }), + drop, + ) } // ---------------------------------------------------------------- lane changes @@ -1920,7 +2052,9 @@ impl Sim { cands.extend(self.lane_target(v)); } else { for t in [k.wrapping_sub(1), k + 1] { - if let Some(&l) = lanes.get(t) && serves(l) { + if let Some(&l) = lanes.get(t) + && serves(l) + { cands.push(l); } } @@ -2037,7 +2171,8 @@ impl Sim { if self.net.links[out].kind != LinkKind::Outbound { return None; } - let tail = self.occ[out].iter().map(|&j| &self.vehicles[j]).min_by(|a, b| a.s.partial_cmp(&b.s).unwrap()); + let tail = + self.occ[out].iter().map(|&j| &self.vehicles[j]).min_by(|a, b| a.s.partial_cmp(&b.s).unwrap()); match tail { Some(w) if w.v < 1.0 => Some(w.s - w.len()), Some(_) => None, @@ -2090,7 +2225,17 @@ impl Sim { if self.collision_log.len() < 30 { let msg = format!( "t={:.1} same-lane overlap on link {l}: #{} {} s={:.1} v={:.1} lc_t={:.1} | #{} {} s={:.1} v={:.1} lc_t={:.1}", - self.t, a.id, a.kind.name(), a.s, a.v, a.lc_t, b.id, b.kind.name(), b.s, b.v, b.lc_t + self.t, + a.id, + a.kind.name(), + a.s, + a.v, + a.lc_t, + b.id, + b.kind.name(), + b.s, + b.v, + b.lc_t ); self.collision_log.push(msg); } @@ -2127,7 +2272,11 @@ impl Sim { self.net.links[v.link()].length(), v.commits, v.reason, - if self.signalized() { Some(self.signal.status(&self.net, v.movement)) } else { None } + if self.signalized() { + Some(self.signal.status(&self.net, v.movement)) + } else { + None + } ) }; let msg = format!("t={:.1} | {} | {}", self.t, d(vi), d(vj)); @@ -2198,12 +2347,14 @@ impl Sim { } PedState::Crossing => { let pos = self.ped_pos(&self.peds[pi]); - let dirv = (self.net.crosswalks[cw].b - self.net.crosswalks[cw].a).norm() * self.peds[pi].dir; + let dirv = + (self.net.crosswalks[cw].b - self.net.crosswalks[cw].a).norm() * self.peds[pi].dir; let blocked = self.vehicle_at(pos + dirv * 1.0, 0.9); let p = &mut self.peds[pi]; if !blocked { // Hurry up during flashing DON'T WALK. - let hurry = if signal && self.signal.cw[cw].state != PedSig::Walk { 1.25 } else { 1.0 }; + let hurry = + if signal && self.signal.cw[cw].state != PedSig::Walk { 1.25 } else { 1.0 }; p.u += p.dir * p.speed * hurry * dt; p.anim += dt * p.speed * 4.0; } @@ -2281,7 +2432,8 @@ impl Sim { let w = &self.vehicles[j]; let d = self.net.links[p].length() - w.s + pc.s; let cant_stop = w.v * w.v / (2.0 * (d - 2.6).max(0.1)) > 2.5; - let leaving = (w.v > 0.5 || w.a > 0.2) && (!w.commits.is_empty() || w.stop_done) && d < 35.0; + let leaving = + (w.v > 0.5 || w.a > 0.2) && (!w.commits.is_empty() || w.stop_done) && d < 35.0; if w.route[w.ri..].contains(&pc.link) && (cant_stop || leaving) { return true; } diff --git a/src/sim/network.rs b/src/sim/network.rs index 8828992..f4433f1 100644 --- a/src/sim/network.rs +++ b/src/sim/network.rs @@ -5,7 +5,7 @@ //! conflict points between paths through the intersection, all derived from a //! compact per-layout lane configuration. -use super::geom::{V2, Path, arc, connector, v2}; +use super::geom::{Path, V2, arc, connector, v2}; use serde::{Deserialize, Serialize}; pub type LinkId = usize; @@ -488,7 +488,9 @@ impl Network { .filter(|&cw| { let leg = self.crosswalks[cw].leg; !self.movements.iter().any(|m| { - stat[m.id].is_some() && m.crosswalks.contains(&cw) && (stat[m.id] == Some(MStat::G) || m.leg == leg) + stat[m.id].is_some() + && m.crosswalks.contains(&cw) + && (stat[m.id] == Some(MStat::G) || m.leg == leg) }) }) .collect() @@ -506,13 +508,17 @@ impl Network { } } -fn new_link(links: &mut Vec, kind: LinkKind, pts: &[V2], speed: f64, leg: usize, lane: usize) -> LinkId { +fn new_link( + links: &mut Vec, + kind: LinkKind, + pts: &[V2], + speed: f64, + leg: usize, + lane: usize, +) -> LinkId { let path = Path::new(pts); - let curve_speed = if path.min_radius.is_finite() { - (A_LAT * path.min_radius).sqrt().clamp(3.5, speed) - } else { - speed - }; + let curve_speed = + if path.min_radius.is_finite() { (A_LAT * path.min_radius).sqrt().clamp(3.5, speed) } else { speed }; let id = links.len(); links.push(Link { id, @@ -545,7 +551,14 @@ type LegDef = (Compass, Vec>, usize, bool); /// A stage by name, then the movements it serves as green, right-turn and permissive. type StageDef<'a> = (&'a str, &'a [&'a str], &'a [&'a str], &'a [&'a str]); -fn build_signal(kind: LayoutKind, defs: Vec, lane_w: f64, speed: f64, yellow: f64, all_red: f64) -> Network { +fn build_signal( + kind: LayoutKind, + defs: Vec, + lane_w: f64, + speed: f64, + yellow: f64, + all_red: f64, +) -> Network { let mut legs: Vec = defs .into_iter() .map(|(c, lm, n_out, minor)| Leg { @@ -570,7 +583,9 @@ fn build_signal(kind: LayoutKind, defs: Vec, lane_w: f64, speed: f64, ye for i in 0..legs.len() { let opp = find(&legs, -legs[i].d); let med = opp - .map(|o| legs[o].lane_moves.iter().position(|lm| lm.contains(&Turn::T)).unwrap_or(0) as f64 * lane_w) + .map(|o| { + legs[o].lane_moves.iter().position(|lm| lm.contains(&Turn::T)).unwrap_or(0) as f64 * lane_w + }) .unwrap_or(0.0); legs[i].med_out = med; legs[i].out_width = med + legs[i].n_out as f64 * lane_w; @@ -597,7 +612,8 @@ fn build_signal(kind: LayoutKind, defs: Vec, lane_w: f64, speed: f64, ye leg.in_lanes.push(id); } // A left-only lane next to a through lane is a bay carved out of the median. - if leg.lane_moves.len() >= 2 && leg.lane_moves[0] == [Turn::L] && leg.lane_moves[1].contains(&Turn::T) { + if leg.lane_moves.len() >= 2 && leg.lane_moves[0] == [Turn::L] && leg.lane_moves[1].contains(&Turn::T) + { let l = &mut links[leg.in_lanes[0]]; l.bay_start = (l.path.length - BAY_LEN).max(0.0); } @@ -753,7 +769,8 @@ fn build_roundabout() -> Network { while th_j > th_i - delta { th_j -= std::f64::consts::TAU; } - seg_b[i] = new_link(&mut links, LinkKind::Ring, &arc(c0, ring_r, th_i - delta, th_j + delta), speed, i, 0); + seg_b[i] = + new_link(&mut links, LinkKind::Ring, &arc(c0, ring_r, th_i - delta, th_j + delta), speed, i, 0); } for i in 0..n { let j = (i + 1) % n; @@ -869,13 +886,15 @@ pub fn build_cooperative(n_left: usize) -> Network { for k in 0..n_in { let lat = ins * g.lane_offset(k); let end = if k == g.right_lane() { g.slip_start } else { g.stop }; - let id = new_link(&mut links, LinkKind::Inbound, &[d * IN_LEN + lat, d * end + lat], speed, li, k); + let id = + new_link(&mut links, LinkKind::Inbound, &[d * IN_LEN + lat, d * end + lat], speed, li, k); leg.in_lanes.push(id); } for k in 0..n_in { let lat = outs * g.lane_offset(k); let start = if k == g.right_lane() { g.slip_start } else { g.stop }; - let id = new_link(&mut links, LinkKind::Outbound, &[d * start + lat, d * OUT_LEN + lat], speed, li, k); + let id = + new_link(&mut links, LinkKind::Outbound, &[d * start + lat, d * OUT_LEN + lat], speed, li, k); leg.out_lanes.push(id); } } @@ -1048,9 +1067,8 @@ impl Network { } for l in self.links.iter_mut() { let id = l.id; - l.conflicts.sort_by(|&x, &y| { - conflicts[x].side(id).0.partial_cmp(&conflicts[y].side(id).0).unwrap() - }); + l.conflicts + .sort_by(|&x, &y| conflicts[x].side(id).0.partial_cmp(&conflicts[y].side(id).0).unwrap()); } self.conflicts = conflicts; @@ -1065,7 +1083,9 @@ impl Network { } for (i, pc) in pcs.iter().enumerate() { self.links[pc.link].ped_conflicts.push(i); - if let Some(m) = self.links[pc.link].movement && !self.movements[m].crosswalks.contains(&pc.cw) { + if let Some(m) = self.links[pc.link].movement + && !self.movements[m].crosswalks.contains(&pc.cw) + { self.movements[m].crosswalks.push(pc.cw); } } diff --git a/src/sim/sensors.rs b/src/sim/sensors.rs index 712b7e4..00a0147 100644 --- a/src/sim/sensors.rs +++ b/src/sim/sensors.rs @@ -46,7 +46,8 @@ pub enum Conditions { } impl Conditions { - pub const ALL: [Conditions; 5] = [Conditions::Clear, Conditions::Night, Conditions::Rain, Conditions::Fog, Conditions::Snow]; + pub const ALL: [Conditions; 5] = + [Conditions::Clear, Conditions::Night, Conditions::Rain, Conditions::Fog, Conditions::Snow]; pub fn name(self) -> &'static str { match self { Conditions::Clear => "Clear day", @@ -127,7 +128,9 @@ impl SensorConfig { /// Deterministic hash to [0, 1). fn h01(a: u64, b: u64, c: u64) -> f64 { - let mut x = a.wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ b.wrapping_mul(0xC2B2_AE3D_27D4_EB4F) ^ c.wrapping_mul(0x1656_67B1_9E37_79F9); + let mut x = a.wrapping_mul(0x9E37_79B9_7F4A_7C15) + ^ b.wrapping_mul(0xC2B2_AE3D_27D4_EB4F) + ^ c.wrapping_mul(0x1656_67B1_9E37_79F9); x ^= x >> 33; x = x.wrapping_mul(0xFF51_AFD7_ED55_8CCD); x ^= x >> 33; @@ -279,7 +282,8 @@ impl Sim { let rich = cfg.camera || cfg.radar || cfg.cv; // Historical turning shares per movement for unknown-intent attribution. - let flows: Vec = (0..net.movements.len()).map(|m| self.demand.movement_flow(net, m).max(1.0)).collect(); + let flows: Vec = + (0..net.movements.len()).map(|m| self.demand.movement_flow(net, m).max(1.0)).collect(); for leg in &net.legs { for &lane in &leg.in_lanes { @@ -298,7 +302,9 @@ impl Sim { // Loops miss some vehicles (motorcycles especially) and faulty // stations report nothing. let loop_sees = fault == 0 - && (!cfg.realistic || h01(v.id, lane as u64, 0x10) >= if v.kind == VKind::Motorcycle { 0.35 } else { 0.01 }); + && (!cfg.realistic + || h01(v.id, lane as u64, 0x10) + >= if v.kind == VKind::Motorcycle { 0.35 } else { 0.01 }); if v.s > len - 9.0 && loop_sees { occupied = true; } @@ -309,7 +315,9 @@ impl Sim { } let cam = cfg.camera && self.camera_sees(&cfg, v, d, lane, t); let cvd = cfg.cv && v.connected; - let rad = cfg.radar && d <= cfg.radar_range && (!cfg.realistic || h01(v.id, (t / 0.5) as u64, 0x30) < cfg.conditions.radar()); + let rad = cfg.radar + && d <= cfg.radar_range + && (!cfg.realistic || h01(v.id, (t / 0.5) as u64, 0x30) < cfg.conditions.radar()); if v.kind == VKind::Emergency && d < 300.0 { view.emergency.push((v.movement, d / v.v.max(3.0))); } @@ -322,9 +330,15 @@ impl Sim { let (d, speed) = if !cfg.realistic || cam { (d, v.v) } else if rad { - (d + 1.5 * noise(v.id, 0x50, t, 1.0), (v.v + 0.5 * noise(v.id, 0x51, t, 1.0)).max(0.0)) + ( + d + 1.5 * noise(v.id, 0x50, t, 1.0), + (v.v + 0.5 * noise(v.id, 0x51, t, 1.0)).max(0.0), + ) } else { - (d + 3.0 * noise(v.id, 0x40, t, 2.0), (v.v + 0.3 * noise(v.id, 0x41, t, 2.0)).max(0.0)) + ( + d + 3.0 * noise(v.id, 0x40, t, 2.0), + (v.v + 0.3 * noise(v.id, 0x41, t, 2.0)).max(0.0), + ) }; let d = d.max(0.0); if d <= cfg.adv_dist { @@ -340,18 +354,27 @@ impl Sim { view.detections.push((v.id, src)); // A camera reads intent from turn signals; a driver who doesn't // signal looks like through traffic. - let signals = !cfg.realistic || v.turn == super::network::Turn::T || h01(v.id, 0x60, 0) < cfg.signal_use; + let signals = !cfg.realistic + || v.turn == super::network::Turn::T + || h01(v.id, 0x60, 0) < cfg.signal_use; let cam_move = if signals { Some(v.movement) } else { moves.iter().copied().find(|&m| net.movements[m].turn == super::network::Turn::T) }; let intent_known = cvd || (cam && cam_move.is_some()) || moves.len() == 1; - let seen_move = if cvd || moves.len() == 1 { v.movement } else { cam_move.unwrap_or(v.movement) }; + let seen_move = + if cvd || moves.len() == 1 { v.movement } else { cam_move.unwrap_or(v.movement) }; let class_known = cam || cvd; let queued = speed < 2.0 && d < 150.0; let wait = if cam || cvd { v.queue_since.map(|q| t - q).unwrap_or(0.0) } else { 0.0 }; - let persons = if class_known { v.persons() } else if v.len() > 10.0 { 8.0 } else { 1.4 }; + let persons = if class_known { + v.persons() + } else if v.len() > 10.0 { + 8.0 + } else { + 1.4 + }; let base = Obs { eta: if queued { 0.0 } else { d / speed.max(3.0) }, dist: d, @@ -405,7 +428,8 @@ impl Sim { // of the approach is busy (stuck off). // The no-activity timer scales with expected volume: ten expected // headways, between 2 and 15 minutes. - let lane_flow: f64 = moves.iter().map(|&m| flows[m] / net.movements[m].routes.len().max(1) as f64).sum(); + let lane_flow: f64 = + moves.iter().map(|&m| flows[m] / net.movements[m].routes.len().max(1) as f64).sum(); let silent_limit = (10.0 * 3600.0 / lane_flow.max(1.0)).clamp(120.0, 900.0); let stuck_on = lo.stopbar_since.is_some_and(|s0| t - s0 > 180.0); lo.fault = stuck_on || t - lo.last_activity > silent_limit; @@ -443,7 +467,10 @@ impl Sim { if lo.fault { // Failed detector: fall back to historical volume, i.e. the // vehicles expected to have arrived since the lane last had green. - let lane_flow: f64 = moves.iter().map(|&m| flows[m] / net.movements[m].routes.len().max(1) as f64).sum(); + let lane_flow: f64 = moves + .iter() + .map(|&m| flows[m] / net.movements[m].routes.len().max(1) as f64) + .sum(); let since = t - lo.last_green; est = (lane_flow / 3600.0 * since).clamp(1.0, 25.0); occ_wait = since; @@ -507,7 +534,8 @@ impl Sim { if u.dist <= own { continue; } - let moves: Vec = (0..net.movements.len()).filter(|&m| net.movements[m].leg == u.leg).collect(); + let moves: Vec = + (0..net.movements.len()).filter(|&m| net.movements[m].leg == u.leg).collect(); let total: f64 = moves.iter().map(|&m| flows[m]).sum(); for &m in &moves { view.moves[m].vehicles.push(Obs { diff --git a/src/sim/signal.rs b/src/sim/signal.rs index 6659f2a..cd8e808 100644 --- a/src/sim/signal.rs +++ b/src/sim/signal.rs @@ -177,8 +177,9 @@ impl SignalState { let b = &net.stages[j].stat; // With LPI, turns that continue into the next stage but cross a crosswalk about // to start WALK get a yellow/red arrow so pedestrians can lead. - let lpi_cw: Vec = - (0..self.cw.len()).map(|cw| self.lpi > 0.0 && self.cw[cw].called && net.stages[j].walk.contains(&cw)).collect(); + let lpi_cw: Vec = (0..self.cw.len()) + .map(|cw| self.lpi > 0.0 && self.cw[cw].called && net.stages[j].walk.contains(&cw)) + .collect(); let mut any = false; for m in 0..a.len() { let dep = match (a[m], b[m]) { @@ -224,7 +225,10 @@ impl SignalState { *hold = self.lpi > 0.0 && net.stages[self.stage].stat[m].is_some() && (net.stages[prev_stage].stat[m].is_none() || was_departing[m]) - && net.movements[m].crosswalks.iter().any(|&cw| prev[cw] != PedSig::Walk && self.cw[cw].state == PedSig::Walk); + && net.movements[m] + .crosswalks + .iter() + .any(|&cw| prev[cw] != PedSig::Walk && self.cw[cw].state == PedSig::Walk); } if self.lpi_hold.iter().any(|&h| h) { self.cur_min = self.cur_min.max(self.lpi + self.min_green); @@ -332,7 +336,11 @@ mod tests { let h = &s.history; // One step's rounding is allowed on each interval's end. let close = |x: f64, y: f64| (x - y).abs() <= dt + 1e-9; - assert!(close(h[0].end, t_req) && close(h[1].end - h[1].start, net.yellow) && close(h[2].end - h[2].start, net.all_red)); + assert!( + close(h[0].end, t_req) + && close(h[1].end - h[1].start, net.yellow) + && close(h[2].end - h[2].start, net.all_red) + ); assert!(h.iter().zip(h.iter().skip(1)).all(|(x, y)| x.end == y.start), "gaps between intervals"); assert_eq!((h[0].start, h[3].end), (0.0, t_end)); } diff --git a/src/sim/stats.rs b/src/sim/stats.rs index e147bff..023244a 100644 --- a/src/sim/stats.rs +++ b/src/sim/stats.rs @@ -5,10 +5,10 @@ //! person delay, level of service, per-approach/movement/class breakdowns and //! time series for charts. -use super::network::{Network, Turn}; +use super::Vehicle; use super::demand::Pending; +use super::network::{Network, Turn}; use super::vtypes::{ALL_KINDS, VKind}; -use super::{Vehicle}; use std::collections::VecDeque; #[derive(Clone, Debug)] @@ -195,7 +195,8 @@ impl Stats { s.ped_avg_wait = pw.iter().sum::() / pw.len() as f64; s.ped_max_wait = pw.iter().cloned().fold(0.0, f64::max); } - s.ped_near_miss_ph = self.ped_near_miss.iter().filter(|&&t| t >= from_t).count() as f64 * 3600.0 / s.duration; + s.ped_near_miss_ph = + self.ped_near_miss.iter().filter(|&&t| t >= from_t).count() as f64 * 3600.0 / s.duration; // Vehicles still waiting: the upstream virtual queue and long-delayed vehicles. let mut unserved = 0; let mut uw = 0.0; @@ -243,7 +244,8 @@ impl Stats { /// HCM level of service from average control delay. pub fn los(delay: f64, signalized: bool) -> &'static str { - let t: [f64; 5] = if signalized { [10.0, 20.0, 35.0, 55.0, 80.0] } else { [10.0, 15.0, 25.0, 35.0, 50.0] }; + let t: [f64; 5] = + if signalized { [10.0, 20.0, 35.0, 55.0, 80.0] } else { [10.0, 15.0, 25.0, 35.0, 50.0] }; let l = ["A", "B", "C", "D", "E"]; for i in 0..5 { if delay <= t[i] { diff --git a/src/sim/world.rs b/src/sim/world.rs index 62efbb5..58689f3 100644 --- a/src/sim/world.rs +++ b/src/sim/world.rs @@ -61,13 +61,27 @@ pub struct World { impl World { pub fn single(sim: Sim) -> World { - World { sims: vec![sim], offsets: vec![v2(0.0, 0.0)], journeys: vec![], coordinated: false, before_wave: vec![], rng: Rng::new(1) } + World { + sims: vec![sim], + offsets: vec![v2(0.0, 0.0)], + journeys: vec![], + coordinated: false, + before_wave: vec![], + rng: Rng::new(1), + } } /// `n` intersections of `kind` along the east-west main road. Interior approaches /// of the main road are fed by the neighboring intersection instead of random /// arrivals; each intersection draws its other arrivals from its own stream. - pub fn corridor(n: usize, kind: LayoutKind, mode: ControlMode, ctrl: CtrlKind, demand: &DemandConfig, seed: u64) -> World { + pub fn corridor( + n: usize, + kind: LayoutKind, + mode: ControlMode, + ctrl: CtrlKind, + demand: &DemandConfig, + seed: u64, + ) -> World { let n = n.max(1); let mut sims = Vec::with_capacity(n); for i in 0..n { @@ -145,11 +159,14 @@ impl World { /// exiting traffic, and what the previous one is about to send. fn link_neighbors(&mut self) { let n = self.sims.len(); - let mut rooms: Vec>> = self.sims.iter().map(|s| vec![None; s.net.legs.len()]).collect(); + let mut rooms: Vec>> = + self.sims.iter().map(|s| vec![None; s.net.legs.len()]).collect(); let mut ups: Vec> = vec![vec![]; n]; for (i, room_i) in rooms.iter_mut().enumerate() { for c in [Compass::E, Compass::W] { - let (Some(j), Some(out_leg)) = (self.neighbor(i, c), self.sims[i].net.leg_index(c)) else { continue }; + let (Some(j), Some(out_leg)) = (self.neighbor(i, c), self.sims[i].net.leg_index(c)) else { + continue; + }; let down = &self.sims[j]; let Some(in_leg) = down.net.leg_index(Self::opposite(c)) else { continue }; // Space on the downstream approach's entry lanes (bays excluded). @@ -170,14 +187,22 @@ impl World { let up = &self.sims[i]; for v in &up.vehicles { let last = *v.route.last().unwrap(); - if v.ri == 0 || up.net.links[last].leg != out_leg || up.net.links[last].kind != super::network::LinkKind::Outbound { + if v.ri == 0 + || up.net.links[last].leg != out_leg + || up.net.links[last].kind != super::network::LinkKind::Outbound + { continue; } let mut d = up.net.links[v.link()].length() - v.s; for &l in &v.route[v.ri + 1..] { d += up.net.links[l].length(); } - ups[j].push(UpstreamObs { leg: in_leg, dist: d + approach, speed: v.v.max(0.85 * v.v0), kind: v.kind }); + ups[j].push(UpstreamObs { + leg: in_leg, + dist: d + approach, + speed: v.v.max(0.85 * v.v0), + kind: v.kind, + }); } } } @@ -268,7 +293,11 @@ impl World { /// own measure, so side-street starvation still counts). pub fn mean_person_delay_all(&self, from_t: f64) -> f64 { let k = self.sims.len() as f64; - self.sims.iter().map(|s| s.stats.summary(from_t, s.t, &s.vehicles, &s.queues).person_delay_all).sum::() / k + self.sims + .iter() + .map(|s| s.stats.summary(from_t, s.t, &s.vehicles, &s.queues).person_delay_all) + .sum::() + / k } /// Turn the fixed-time green wave on or off. On, every signal runs fixed-time: a @@ -305,9 +334,8 @@ impl World { let net = &s.net; let mt = |c: Compass| net.leg_index(c).and_then(|l| net.movement_for(l, Turn::T)); let (e, w) = (mt(Compass::E), mt(Compass::W)); - sync[i] = (0..net.stages.len()).find(|&st| { - [e, w].iter().flatten().all(|&m| net.stages[st].stat[m] == Some(MStat::G)) - }); + sync[i] = (0..net.stages.len()) + .find(|&st| [e, w].iter().flatten().all(|&m| net.stages[st].stat[m] == Some(MStat::G))); } if cycle <= 0.0 { return; diff --git a/tests/cooperative.rs b/tests/cooperative.rs index 8001c9d..c3bdd23 100644 --- a/tests/cooperative.rs +++ b/tests/cooperative.rs @@ -101,11 +101,20 @@ fn geometry_has_only_crossings() { for (i, mv) in net.movements.iter().enumerate() { assert_eq!(mgr.managed(i), mv.turn != Turn::R, "{}", mv.name); let crossed = (0..net.movements.len()).filter(|&o| mgr.conflicts(i, o)).count(); - assert_eq!(crossed, if mv.turn == Turn::R { 0 } else { 4 }, "{} crosses {crossed} movements", mv.name); + assert_eq!( + crossed, + if mv.turn == Turn::R { 0 } else { 4 }, + "{} crosses {crossed} movements", + mv.name + ); } - assert!(!mgr.conflicts(m("N_L"), m("S_L")) && !mgr.conflicts(m("E_L"), m("W_L")), "opposite lefts pass clear"); + assert!( + !mgr.conflicts(m("N_L"), m("S_L")) && !mgr.conflicts(m("E_L"), m("W_L")), + "opposite lefts pass clear" + ); // Every lane has its own path through the box and its own receiving lane. - let mut outs: Vec = net.movements.iter().flat_map(|mv| mv.routes.iter().map(|r| *r.1.last().unwrap())).collect(); + let mut outs: Vec = + net.movements.iter().flat_map(|mv| mv.routes.iter().map(|r| *r.1.last().unwrap())).collect(); let n = outs.len(); outs.sort_unstable(); outs.dedup(); @@ -117,7 +126,11 @@ fn geometry_has_only_crossings() { fn left_turns_lag_the_through_movements() { let net = Network::build(LayoutKind::Cooperative); let mgr = CoopManager::new(&net); - let names: Vec> = mgr.ring_order().iter().map(|r| r.iter().map(|&m| net.movements[m].name.as_str()).collect()).collect(); + let names: Vec> = mgr + .ring_order() + .iter() + .map(|r| r.iter().map(|&m| net.movements[m].name.as_str()).collect()) + .collect(); assert_eq!(names, [["S_T", "N_L", "W_T", "E_L"], ["N_T", "S_L", "E_T", "W_L"]]); // A left turn crosses the opposing through lanes late in its arc, which those // vehicles reach early: it can follow them almost at once, but they must wait for it. @@ -141,7 +154,13 @@ fn every_gap_policy_is_safe_under_heavy_demand() { let mut sim = crossing(n_left, total, cars, policy, seed); sim.run_for(1200.0); let fleet = if cars { "cars" } else { "mixed" }; - safety_failures(&format!("{} / {n_left} left / {fleet} / {total} veh/h / seed {seed}", policy.short()), &sim) + safety_failures( + &format!( + "{} / {n_left} left / {fleet} / {total} veh/h / seed {seed}", + policy.short() + ), + &sim, + ) })); } } @@ -164,7 +183,9 @@ fn bursts_heavy_vehicles_and_uneven_demand_are_served_safely() { d.rate = [4_500.0; 4]; d }; - for (pattern, limits) in [(Pattern::Platoons, Some((100.0, 300.0))), (Pattern::RushHour, None), (Pattern::Surges, None)] { + for (pattern, limits) in + [(Pattern::Platoons, Some((100.0, 300.0))), (Pattern::RushHour, None), (Pattern::Surges, None)] + { let mut d = base(); d.pattern = pattern; cases.push((format!("{pattern:?} arrivals"), d, limits)); @@ -185,14 +206,18 @@ fn bursts_heavy_vehicles_and_uneven_demand_are_served_safely() { .into_iter() .map(|(name, d, limits)| { std::thread::spawn(move || { - let mut sim = Sim::with_demand(build_cooperative(2), ControlMode::Cooperative, CtrlKind::Fixed, d, 41); + let mut sim = + Sim::with_demand(build_cooperative(2), ControlMode::Cooperative, CtrlKind::Fixed, d, 41); sim.check_safety = true; sim.run_for(1200.0); let s = summary(&sim, 120.0); let mut f = safety_failures(&name, &sim); if let Some((p95, max)) = limits { if s.p95_delay > p95 || s.max_delay > max { - f.push(format!("{name}: delay {:.0} s at the 95th percentile, {:.0} s at most", s.p95_delay, s.max_delay)); + f.push(format!( + "{name}: delay {:.0} s at the 95th percentile, {:.0} s at most", + s.p95_delay, s.max_delay + )); } if queued(&sim) > 60 { f.push(format!("{name}: {} vehicles still waiting to enter", queued(&sim))); @@ -219,7 +244,10 @@ fn switching_policy_or_scheduling_mid_run_stays_safe() { let batches = sim.coop.as_ref().unwrap().stats.batches; sim.set_scheduling(Scheduling::Batches); sim.run_for(150.0); - assert!(sim.coop.as_ref().unwrap().stats.batches > batches + 4, "batches resume after first come, first served"); + assert!( + sim.coop.as_ref().unwrap().stats.batches > batches + 4, + "batches resume after first come, first served" + ); let failures = safety_failures("policy and scheduling switched live", &sim); assert!(failures.is_empty(), "{}", failures.join("\n")); } @@ -247,11 +275,28 @@ fn weaving_one_at_a_time_carries_far_less() { run(Scheduling::Batches, 14_000.0, 62), ]; let [light_zip, light_batch, heavy_zip, heavy_batch] = jobs.map(|j| j.join().unwrap()); - let failures: Vec = [&light_zip.3, &light_batch.3, &heavy_zip.3, &heavy_batch.3].into_iter().flatten().cloned().collect(); + let failures: Vec = + [&light_zip.3, &light_batch.3, &heavy_zip.3, &heavy_batch.3].into_iter().flatten().cloned().collect(); assert!(failures.is_empty(), "{}", failures.join("\n")); - assert!(light_zip.0 < light_batch.0 && light_zip.0 < 3.0, "light traffic: {:.1} s weaving, {:.1} s in batches", light_zip.0, light_batch.0); - assert!(heavy_batch.1 > 13_300.0 && heavy_batch.2 < 50 && heavy_batch.0 < 30.0, "batches at 14,000 veh/h: served {:.0}, {} waiting, {:.1} s", heavy_batch.1, heavy_batch.2, heavy_batch.0); - assert!(heavy_zip.1 < 11_000.0 && heavy_zip.2 > 500, "weaving at 14,000 veh/h: served {:.0} veh/h, {} waiting", heavy_zip.1, heavy_zip.2); + assert!( + light_zip.0 < light_batch.0 && light_zip.0 < 3.0, + "light traffic: {:.1} s weaving, {:.1} s in batches", + light_zip.0, + light_batch.0 + ); + assert!( + heavy_batch.1 > 13_300.0 && heavy_batch.2 < 50 && heavy_batch.0 < 30.0, + "batches at 14,000 veh/h: served {:.0}, {} waiting, {:.1} s", + heavy_batch.1, + heavy_batch.2, + heavy_batch.0 + ); + assert!( + heavy_zip.1 < 11_000.0 && heavy_zip.2 > 500, + "weaving at 14,000 veh/h: served {:.0} veh/h, {} waiting", + heavy_zip.1, + heavy_zip.2 + ); } #[test] @@ -275,7 +320,10 @@ fn dual_left_design_serves_18000_vehicles_per_hour() { f.push(format!("seed {seed}: {q} vehicles still waiting to enter")); } if s.avg_delay > 40.0 || s.max_delay > 180.0 { - f.push(format!("seed {seed}: delay {:.1} s on average, {:.0} s at most", s.avg_delay, s.max_delay)); + f.push(format!( + "seed {seed}: delay {:.1} s on average, {:.0} s at most", + s.avg_delay, s.max_delay + )); } f }) @@ -309,7 +357,10 @@ fn single_left_lanes_fall_short_of_the_design_demand() { let (dual, single) = (run(2), run(1)); let ((dual_vph, dual_q), (single_vph, single_q)) = (dual.join().unwrap(), single.join().unwrap()); assert!(dual_q < 50, "dual lefts left {dual_q} waiting"); - assert!(single_q > 300 && single_vph < dual_vph - 1_000.0, "single lefts served {single_vph:.0} veh/h, {single_q} waiting"); + assert!( + single_q > 300 && single_vph < dual_vph - 1_000.0, + "single lefts served {single_vph:.0} veh/h, {single_q} waiting" + ); } #[test] @@ -323,7 +374,15 @@ fn runs_are_reproducible() { let c = sim.coop.as_ref().unwrap(); let delay: f64 = sim.stats.done.iter().map(|d| d.delay).sum(); let pos: f64 = sim.vehicles.iter().map(|v| v.s + v.v).sum(); - (sim.stats.generated, sim.stats.done.len(), c.stats.plans, c.stats.batches, delay.to_bits(), pos.to_bits(), c.stats.pet_min.to_bits()) + ( + sim.stats.generated, + sim.stats.done.len(), + c.stats.plans, + c.stats.batches, + delay.to_bits(), + pos.to_bits(), + c.stats.pet_min.to_bits(), + ) }) }; let (a, b) = (run(), run()); @@ -350,9 +409,16 @@ fn emergency_vehicles_go_first() { assert!(sim.spawn_manual(leg, VKind::Emergency, Turn::L)); } sim.run_for(150.0); - let others: Vec = sim.stats.done.iter().filter(|d| d.t_exit > t0 && d.turn == Turn::L && d.kind != VKind::Emergency).map(|d| d.delay).collect(); + let others: Vec = sim + .stats + .done + .iter() + .filter(|d| d.t_exit > t0 && d.turn == Turn::L && d.kind != VKind::Emergency) + .map(|d| d.delay) + .collect(); let typical = others.iter().sum::() / others.len().max(1) as f64; - let em: Vec = sim.stats.done.iter().filter(|d| d.kind == VKind::Emergency).map(|d| d.delay).collect(); + let em: Vec = + sim.stats.done.iter().filter(|d| d.kind == VKind::Emergency).map(|d| d.delay).collect(); assert_eq!(em.len(), 4, "every emergency vehicle got through"); assert!(em.iter().all(|&d| d < 15.0), "emergency delays {em:?} (other left turns {typical:.1} s)"); assert!(sim.coop.as_ref().unwrap().stats.preempted >= 4); diff --git a/tests/green_wave.rs b/tests/green_wave.rs index af868ac..a8f2823 100644 --- a/tests/green_wave.rs +++ b/tests/green_wave.rs @@ -12,7 +12,14 @@ use intersection_lab::sim::world::World; fn corridor(n: usize) -> World { let kind = LayoutKind::Arterial4; - let mut w = World::corridor(n, kind, ControlMode::Signal, CtrlKind::Predictive, &DemandConfig::default_for(&Network::build(kind)), 1); + let mut w = World::corridor( + n, + kind, + ControlMode::Signal, + CtrlKind::Predictive, + &DemandConfig::default_for(&Network::build(kind)), + 1, + ); for s in &mut w.sims { s.check_safety = true; } @@ -39,7 +46,10 @@ fn the_green_wave_gives_back_the_controllers() { assert_eq!(w.controller_before_wave(2), None); w.run_for(30.0); w.set_coordination(false); - assert_eq!(controllers(&w), [CtrlKind::Predictive, CtrlKind::MaxPressure, CtrlKind::Fixed, CtrlKind::Fixed]); + assert_eq!( + controllers(&w), + [CtrlKind::Predictive, CtrlKind::MaxPressure, CtrlKind::Fixed, CtrlKind::Fixed] + ); assert!(w.sims.iter().all(|s| s.coordination.is_none())); w.run_for(30.0); assert_eq!(w.sims.iter().map(|s| s.collisions).sum::(), 0); @@ -47,7 +57,10 @@ fn the_green_wave_gives_back_the_controllers() { // A second wave starts from the controllers running now. w.set_coordination(true); w.set_coordination(false); - assert_eq!(controllers(&w), [CtrlKind::Predictive, CtrlKind::MaxPressure, CtrlKind::Fixed, CtrlKind::Fixed]); + assert_eq!( + controllers(&w), + [CtrlKind::Predictive, CtrlKind::MaxPressure, CtrlKind::Fixed, CtrlKind::Fixed] + ); } #[test] diff --git a/tests/pedestrian_rule.rs b/tests/pedestrian_rule.rs index f8c3814..451eb11 100644 --- a/tests/pedestrian_rule.rs +++ b/tests/pedestrian_rule.rs @@ -63,7 +63,12 @@ fn shield_serves_a_pedestrian_who_waited_too_long() { } let mut waits = vec![0.0; n]; waits[cw] = PED_MAX_WAIT + 5.0; - assert_eq!(mask_with_waits(&sim, &waits), serve, "{:?}: crosswalk {cw} waited too long but was not served next", net.kind); + assert_eq!( + mask_with_waits(&sim, &waits), + serve, + "{:?}: crosswalk {cw} waited too long but was not served next", + net.kind + ); checked += 1; } } @@ -97,7 +102,13 @@ fn shield_serves_the_longest_wait_first() { for b in 0..n { let (sa, sb) = (walking(net, a), walking(net, b)); // Two late crosswalks that no stage walks together, neither walked now. - if a == b || sa[cur] || sb[cur] || !sa.contains(&true) || !sb.contains(&true) || sa.iter().zip(&sb).any(|(&x, &y)| x && y) { + if a == b + || sa[cur] + || sb[cur] + || !sa.contains(&true) + || !sb.contains(&true) + || sa.iter().zip(&sb).any(|(&x, &y)| x && y) + { continue; } let mut waits = vec![0.0; n]; @@ -129,5 +140,8 @@ fn reward_counts_waiting_pedestrians() { // The reward is taken mid-step, so pedestrians who start or stop waiting in that step // can differ by one step from the end-of-step count. assert!(waiting > 1000.0, "too little pedestrian waiting to test: {waiting:.0} s"); - assert!((reward - waiting).abs() < 0.02 * waiting, "reward {reward:.0} s, pedestrian waiting {waiting:.0} s"); + assert!( + (reward - waiting).abs() < 0.02 * waiting, + "reward {reward:.0} s, pedestrian waiting {waiting:.0} s" + ); } diff --git a/tests/safety.rs b/tests/safety.rs index 6e61355..39c38fc 100644 --- a/tests/safety.rs +++ b/tests/safety.rs @@ -24,9 +24,16 @@ fn all_scenarios_safe_and_flowing() { let served = sim.stats.done.len() as f64 / sim.stats.generated.max(1) as f64; // Stop control is legitimately over capacity on busy roads (the arterial // carries ~2,500 veh/h, above all-way-stop capacity). - let over = mode == ControlMode::TwoWayStop || (mode == ControlMode::AllWayStop && kind == LayoutKind::Arterial4); + let over = mode == ControlMode::TwoWayStop + || (mode == ControlMode::AllWayStop && kind == LayoutKind::Arterial4); let need = if over { 0.65 } else { 0.8 }; - (format!("{} / {} / {}", kind.key(), mode.name(), c.short()), sim.collisions, served, s.avg_delay, need) + ( + format!("{} / {} / {}", kind.key(), mode.name(), c.short()), + sim.collisions, + served, + s.avg_delay, + need, + ) })); } } @@ -38,7 +45,8 @@ fn all_scenarios_safe_and_flowing() { failures.push(format!("{name}: {coll} overlap ticks")); } if served < need { - failures.push(format!("{name}: only {:.0}% of arrivals served (delay {delay:.0}s)", served * 100.0)); + failures + .push(format!("{name}: only {:.0}% of arrivals served (delay {delay:.0}s)", served * 100.0)); } } assert!(failures.is_empty(), "{}", failures.join("\n")); @@ -84,9 +92,18 @@ fn no_gridlock_when_oversaturated() { sim.step(); for v in &sim.vehicles { let t = still.entry(v.id).or_insert(0.0); - *t = if v.v < 0.05 && sim.net.links[v.link()].kind.in_box() { *t + sim.dt } else { 0.0 }; + *t = if v.v < 0.05 && sim.net.links[v.link()].kind.in_box() { + *t + sim.dt + } else { + 0.0 + }; if *t > 300.0 { - return Some(format!("{} seed {seed} ×{scale}: vehicle {} stuck in the box at t={:.0}", c.short(), v.id, sim.t)); + return Some(format!( + "{} seed {seed} ×{scale}: vehicle {} stuck in the box at t={:.0}", + c.short(), + v.id, + sim.t + )); } } } @@ -124,7 +141,9 @@ fn no_collision_between_late_yellow_and_clearing_turn() { .into_iter() .map(|h| h.join().unwrap()) .filter(|r| r.1 > 0) - .map(|(seed, n, first)| format!("seed {seed}: {n} overlap ticks, first: {}", first.unwrap_or_default())) + .map(|(seed, n, first)| { + format!("seed {seed}: {n} overlap ticks, first: {}", first.unwrap_or_default()) + }) .collect(); assert!(failures.is_empty(), "{}", failures.join("\n")); } diff --git a/tests/weights_files.rs b/tests/weights_files.rs index b14784f..98d9e75 100644 --- a/tests/weights_files.rs +++ b/tests/weights_files.rs @@ -86,15 +86,21 @@ fn misshapen_networks_are_rejected() { ("a layer with no inputs", edited(|l| l[0]["n_in"] = json!(0))), ("too few weights", edited(|l| _ = l[0]["w"].as_array_mut().unwrap().pop())), ("too few biases", edited(|l| _ = l[1]["b"].as_array_mut().unwrap().pop())), - ("layers that don't connect", edited(|l| { - l[1]["n_in"] = json!(64); - l[1]["w"].as_array_mut().unwrap().truncate(64 * 4); - })), - ("three outputs", edited(|l| { - l[1]["n_out"] = json!(3); - l[1]["w"].as_array_mut().unwrap().truncate(128 * 3); - l[1]["b"].as_array_mut().unwrap().pop(); - })), + ( + "layers that don't connect", + edited(|l| { + l[1]["n_in"] = json!(64); + l[1]["w"].as_array_mut().unwrap().truncate(64 * 4); + }), + ), + ( + "three outputs", + edited(|l| { + l[1]["n_out"] = json!(3); + l[1]["w"].as_array_mut().unwrap().truncate(128 * 3); + l[1]["b"].as_array_mut().unwrap().pop(); + }), + ), ("a weight too large for f32", edited(|l| l[0]["w"][0] = json!(1e39))), ]; for (what, txt) in &cases { @@ -125,12 +131,15 @@ fn hybrid_files_with_a_non_finite_score_load() { let mut h = Hybrid::new(&net, 1); h.base_action = 7; // The app knows no score; training has none when no tuning meets its pedestrian limit. - for (i, score) in [None, Some(f64::NAN), Some(f64::INFINITY), Some(f64::NEG_INFINITY)].into_iter().enumerate() { + for (i, score) in + [None, Some(f64::NAN), Some(f64::INFINITY), Some(f64::NEG_INFINITY)].into_iter().enumerate() + { for with_model in [false, true] { let path = dir.join(format!("hybrid_{i}_{with_model}.json")); h.save(&path, score, with_model).unwrap(); let saved = std::fs::read_to_string(&path).unwrap(); - let back = Hybrid::from_saved(&net, &saved).unwrap_or_else(|| panic!("saved with score {score:?}, doesn't load: {saved:.80}")); + let back = Hybrid::from_saved(&net, &saved) + .unwrap_or_else(|| panic!("saved with score {score:?}, doesn't load: {saved:.80}")); assert_eq!((back.base_action, back.has_model), (7, with_model)); } } @@ -142,7 +151,9 @@ fn hybrid_files_with_a_non_finite_score_load() { #[test] fn a_weights_file_that_does_not_load_is_skipped() { let net = Network::build(LayoutKind::Tee); - let load = |dirs: &[PathBuf]| weights::load_from(dirs, "hybrid_tee.json", |txt| Hybrid::from_saved(&net, txt)).unwrap(); + let load = |dirs: &[PathBuf]| { + weights::load_from(dirs, "hybrid_tee.json", |txt| Hybrid::from_saved(&net, txt)).unwrap() + }; let (bad, good) = (scratch("skip_bad"), scratch("skip_good")); std::fs::write(bad.join("hybrid_tee.json"), r#"{"base_action": 3, "base_sc"#).unwrap(); let mut h = Hybrid::new(&net, 1);