From bec2e05f317c91aaaad0a37461a8b9ef381258ae Mon Sep 17 00:00:00 2001 From: Jim Schreckengast Date: Wed, 11 Nov 2026 06:28:52 -0700 Subject: [PATCH 1/8] Load the Hybrid weights files that the app and train save The app's Save weights passed NaN as the Hybrid's score, and train saved an infinite score when no static tuning passed the pedestrian check. JSON stores both as null, which SavedHybrid couldn't read, so the file didn't load. And because weights::load returned the first readable copy without checking that it loads, such a file also hid the user's other copy and the built-in one: the Hybrid fell back to the planner's default tuning. * base_score is now an Option. A score that isn't finite is saved as unknown, and the null that 0.5.1 wrote loads. * weights::load takes the parser and returns the first copy it accepts, so a copy that doesn't load is skipped for the next one. * The app saves the Hybrid's network only when it is trained or learning. It used to save an untrained network too, which a Hybrid loading the file would then follow, now that the file loads. tests/weights_files.rs saves with each kind of score, with and without a network, and loads the file back; loads a 0.5.1 file with a null score; and checks that a damaged copy is skipped for the next one and, failing that, for the built-in copy. Signed-off-by: Jim Schreckengast --- src/bin/train.rs | 5 ++-- src/control/hybrid.rs | 15 +++++++----- src/control/rl.rs | 4 +-- src/control/weights.rs | 33 ++++++++++++++++--------- src/gui/mod.rs | 4 ++- tests/weights_files.rs | 55 ++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 90 insertions(+), 26 deletions(-) diff --git a/src/bin/train.rs b/src/bin/train.rs index d115e6e..0d28be6 100644 --- a/src/bin/train.rs +++ b/src/bin/train.rs @@ -145,7 +145,7 @@ impl Agent { std::fs::create_dir_all("weights").ok(); match self { Agent::Dqn(a) => a.save(&rl::weights_path(layout.key())).ok(), - Agent::Hybrid(a) => a.save(&hybrid::weights_path(layout.key()), score, true).ok(), + Agent::Hybrid(a) => a.save(&hybrid::weights_path(layout.key()), Some(score), true).ok(), }; } fn loss(&self) -> f32 { @@ -272,10 +272,11 @@ fn train(kind: LayoutKind, episodes: u64, secs: f64, which: &str) { if let Agent::Hybrid(h) = &mut agent { // The base tuning is the exploration expert and the score to beat, also when // resuming; save it now so the hybrid falls back to it if no network does better. + // When no tuning met the pedestrian limit, its score is infinite and isn't saved. h.base_action = base.1; best = base.0; std::fs::create_dir_all("weights").ok(); - h.save(&hybrid::weights_path(kind.key()), base.0, false).ok(); + h.save(&hybrid::weights_path(kind.key()), Some(base.0), false).ok(); saved_any = true; } agent.set(true, 0.0, 0.0); diff --git a/src/control/hybrid.rs b/src/control/hybrid.rs index d94ab65..dc47a66 100644 --- a/src/control/hybrid.rs +++ b/src/control/hybrid.rs @@ -56,7 +56,10 @@ pub const HIDDEN: &[usize] = &[128]; #[derive(Serialize, Deserialize)] pub struct SavedHybrid { pub base_action: usize, - pub base_score: f64, + /// Validation person delay (s) of what the file holds, when training measured one. + /// Version 0.5.1 wrote `null` for a score it couldn't store; that loads as `None`. + #[serde(default)] + pub base_score: Option, pub model: Option, } @@ -121,9 +124,7 @@ 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((txt, source)) = super::weights::load(&weights_file(net.kind.key())) - && let Some(mut h) = 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; } @@ -148,7 +149,9 @@ impl Hybrid { Some(h) } - pub fn save(&self, path: &std::path::Path, base_score: f64, with_model: bool) -> std::io::Result<()> { + /// 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<()> { let model = with_model.then(|| SavedModel { layout: self.layout.clone(), n_in: self.agent.online.n_in(), @@ -156,7 +159,7 @@ impl Hybrid { episodes: self.episodes, net: self.agent.online.clone(), }); - let sv = SavedHybrid { base_action: self.base_action, base_score, 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()) } diff --git a/src/control/rl.rs b/src/control/rl.rs index d364a7a..ac59465 100644 --- a/src/control/rl.rs +++ b/src/control/rl.rs @@ -105,9 +105,7 @@ impl RlController { } pub fn load_or_new(net: &Network) -> Self { - if let Some((txt, source)) = super::weights::load(&weights_file(net.kind.key())) - && let Some(mut c) = 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; } diff --git a/src/control/weights.rs b/src/control/weights.rs index ab25e95..41927d4 100644 --- a/src/control/weights.rs +++ b/src/control/weights.rs @@ -5,11 +5,12 @@ //! //! Loading looks in `weights/` under the working directory (development and training), //! then in the per-user data directory (weights saved from the app), then falls back to -//! the copies built into the binary, so an installed app works anywhere. Saving goes to -//! `weights/` when that directory exists in the working directory, otherwise to the -//! per-user data directory. +//! the copies built into the binary, so an installed app works anywhere. A copy that +//! can't be read or doesn't load is skipped, so a damaged file doesn't hide a good copy +//! further down the list. Saving goes to `weights/` when that directory exists in the +//! working directory, otherwise to the per-user data directory. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// Weights shipped with the application. const EMBEDDED: &[(&str, &str)] = &[ @@ -38,16 +39,24 @@ pub fn user_dir() -> Option { } } -/// Contents of weights file `file` and where they came from. -pub fn load(file: &str) -> Option<(String, String)> { - let local = PathBuf::from("weights").join(file); - let user = user_dir().map(|d| d.join("weights").join(file)); - for p in std::iter::once(local).chain(user) { - if let Ok(txt) = std::fs::read_to_string(&p) { - return Some((txt, p.display().to_string())); +/// 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(); + 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)> { + 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) + { + return Some((v, p.display().to_string())); } } - EMBEDDED.iter().find(|e| e.0 == file).map(|e| (e.1.to_string(), format!("built-in {file}"))) + let txt = EMBEDDED.iter().find(|e| e.0 == file)?.1; + parse(txt).map(|v| (v, format!("built-in {file}"))) } /// Where to save weights file `file` (the directory may still need creating). diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 7471c7f..9d75a34 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -842,7 +842,9 @@ impl App { if !WEB && ui.button("💾 Save weights").clicked() { let p = crate::control::hybrid::weights_path(layout.key()); std::fs::create_dir_all(p.parent().unwrap()).ok(); - saved = Some(h.save(&p, f64::NAN, true).map(|_| p.display().to_string())); + // The app measures no validation score. The network is saved only + // if it is choosing the tunings (trained or learning). + saved = Some(h.save(&p, None, h.has_model || h.learning).map(|_| p.display().to_string())); } } if let Some(r) = saved { diff --git a/tests/weights_files.rs b/tests/weights_files.rs index a2c705f..f675349 100644 --- a/tests/weights_files.rs +++ b/tests/weights_files.rs @@ -3,16 +3,27 @@ //! Weights files are read from disk, where they can be damaged or edited by hand. The //! loaders must reject a network that doesn't fit its controller, and nothing an -//! accepted network computes may crash the app. +//! accepted network computes may crash the app. What the app and the trainer save must +//! load again, and a copy that doesn't load mustn't hide one that does. use intersection_lab::control::hybrid::{Hybrid, N_ACTIONS}; use intersection_lab::control::rl::{RlController, n_inputs}; -use intersection_lab::control::{Controller, CtrlKind}; +use intersection_lab::control::{Controller, CtrlKind, weights}; use intersection_lab::sim::network::{LayoutKind, Network}; use intersection_lab::sim::{ControlMode, Sim}; use serde_json::{Value, json}; +use std::path::PathBuf; const DQN_TEE: &str = include_str!("../weights/dqn_tee.json"); +const HYBRID_TEE: &str = include_str!("../weights/hybrid_tee.json"); + +/// An empty directory for this test's files. +fn scratch(name: &str) -> PathBuf { + let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(name); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir +} /// The shipped tee-intersection network with `edit` applied to its layers. fn edited(edit: impl FnOnce(&mut Vec)) -> String { @@ -93,6 +104,46 @@ fn hybrid_without_a_fitting_network_keeps_its_base_tuning() { assert_eq!(h.base_action, 10); } +#[test] +fn hybrid_files_with_a_non_finite_score_load() { + let net = Network::build(LayoutKind::Tee); + let dir = scratch("non_finite_score"); + 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 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}")); + assert_eq!((back.base_action, back.has_model), (7, with_model)); + } + } + // What version 0.5.1 wrote for such a score. + let old = json!({ "base_action": 7, "base_score": null, "model": null }).to_string(); + assert_eq!(Hybrid::from_saved(&net, &old).unwrap().base_action, 7); +} + +#[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 (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); + h.base_action = 5; + h.save(&good.join("hybrid_tee.json"), None, false).unwrap(); + + let (h, source) = load(&[bad.clone(), good.clone()]); + assert_eq!(h.base_action, 5); + assert_eq!(source, good.join("hybrid_tee.json").display().to_string()); + // With no copy on disk that loads, the built-in one is used. + let (h, source) = load(&[bad]); + assert_eq!(source, "built-in hybrid_tee.json"); + assert_eq!(h.base_action, Hybrid::from_saved(&net, HYBRID_TEE).unwrap().base_action); +} + #[test] fn networks_that_overflow_do_not_crash() { let net = Network::build(LayoutKind::Tee); From 218ae018cafbcb033da5d3eb8761922e843cd566 Mon Sep 17 00:00:00 2001 From: Jim Schreckengast Date: Wed, 11 Nov 2026 06:29:17 -0700 Subject: [PATCH 2/8] Say when a saved Hybrid network doesn't fit the layout A Hybrid weights file whose network doesn't fit the layout loads with its base tuning only, and the controller brain then said "no network beat the best static tuning", which is what a file saved without a network means. Hybrid now records that the network was left out (model_rejected), and the brain says "the saved network doesn't fit this layout" instead. tests/weights_files.rs checks both messages. Signed-off-by: Jim Schreckengast --- src/control/hybrid.rs | 9 +++++++-- tests/weights_files.rs | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/control/hybrid.rs b/src/control/hybrid.rs index dc47a66..2c6bd9b 100644 --- a/src/control/hybrid.rs +++ b/src/control/hybrid.rs @@ -69,6 +69,8 @@ pub struct Hybrid { pub base_action: usize, /// The network is trained (loaded or being trained) and may choose tunings. pub has_model: bool, + /// The weights file's network doesn't fit this layout, so it was left out. + pub model_rejected: bool, pub agent: Dqn, pub learning: bool, pub epsilon: f64, @@ -102,6 +104,7 @@ impl Hybrid { planner: Predictive::new(net), base_action: DEFAULT_ACTION, has_model: false, + model_rejected: false, agent, learning: false, epsilon: 0.0, @@ -133,7 +136,8 @@ impl Hybrid { /// The tuning and network saved as `txt` (a weights file's contents), or `None` if /// it doesn't parse. Weights files can be damaged or edited by hand, so a network - /// that doesn't fit this layout is left out and only the saved base tuning is kept. + /// that doesn't fit this layout is left out (`model_rejected`) and only the saved + /// base tuning is kept. pub fn from_saved(net: &Network, txt: &str) -> Option { let sv = serde_json::from_str::(txt).ok()?; let mut h = match sv.model { @@ -143,7 +147,7 @@ impl Hybrid { h.has_model = true; h } - _ => Self::new(net, 11), + other => Self { model_rejected: other.is_some(), ..Self::new(net, 11) }, }; h.base_action = sv.base_action.min(N_ACTIONS - 1); Some(h) @@ -232,6 +236,7 @@ impl Controller for Hybrid { 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)), (None, _) if self.learning => "learning online from scratch".into(), (None, _) => "no trained weights: running the planner's default tuning".into(), diff --git a/tests/weights_files.rs b/tests/weights_files.rs index f675349..b14784f 100644 --- a/tests/weights_files.rs +++ b/tests/weights_files.rs @@ -25,6 +25,12 @@ fn scratch(name: &str) -> PathBuf { dir } +/// What the app says about where the hybrid's tuning comes from. +fn status(mut h: Hybrid) -> String { + h.loaded_from = Some("a weights file".into()); + h.brain().notes.join("\n") +} + /// The shipped tee-intersection network with `edit` applied to its layers. fn edited(edit: impl FnOnce(&mut Vec)) -> String { let mut v: Value = serde_json::from_str(DQN_TEE).unwrap(); @@ -102,6 +108,14 @@ fn hybrid_without_a_fitting_network_keeps_its_base_tuning() { let h = Hybrid::from_saved(&net, &hybrid_file(&net, json!([]))).unwrap(); assert!(!h.has_model); assert_eq!(h.base_action, 10); + // The app says the network was left out, not that training found none worth keeping. + let notes = status(h); + assert!(notes.contains("network doesn't fit"), "{notes}"); + assert!(!notes.contains("no network beat"), "{notes}"); + + let no_network = json!({ "base_action": 10, "base_score": 0.0, "model": null }).to_string(); + let notes = status(Hybrid::from_saved(&net, &no_network).unwrap()); + assert!(notes.contains("no network beat the best static tuning"), "{notes}"); } #[test] From 87bac239ba05f13bb833c1fb3a619dee62475c60 Mon Sep 17 00:00:00 2001 From: Jim Schreckengast Date: Wed, 11 Nov 2026 06:29:34 -0700 Subject: [PATCH 3/8] Show yellow and all-red on the signal timing chart The signal's history recorded one span per stage, from the start of its green to the start of the next green, so the bottom panel's timeline drew the yellow change and the all-red clearance as part of the green. Each StageSpan now has a phase, and the history records the yellow and the all-red that end a green as spans of their own. The timeline draws them in yellow and red after the green. The history keeps 1,200 spans, up from 400, since a stage change can now add three. A unit test in sim/signal.rs checks that a change records green, yellow, all-red and the next green, with the right durations and no gaps. Signed-off-by: Jim Schreckengast --- src/gui/charts.rs | 24 +++++++---------- src/sim/signal.rs | 69 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/src/gui/charts.rs b/src/gui/charts.rs index 4bd33f6..6f4469e 100644 --- a/src/gui/charts.rs +++ b/src/gui/charts.rs @@ -4,6 +4,7 @@ //! Small hand-painted charts (line, bars, histograms, signal and crossing timelines) in //! egui. +use crate::sim::signal::Phase; use egui::{Align2, Color32, FontId, Pos2, Rect, Sense, Stroke, Ui, pos2, vec2}; pub const SERIES: [Color32; 6] = [ @@ -293,26 +294,21 @@ pub fn timeline(ui: &mut Ui, sim: &crate::sim::Sim, window: f64) { 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); } } - let hist: Vec<_> = sim.signal.history.iter().collect(); - for (hi, h) in hist.iter().enumerate() { + // Each stage's green in its own color, then the yellow change and all-red clearance + // that end it. + for h in &sim.signal.history { if h.end < t0 { continue; } let y = plot.top() + h.stage as f32 * row_h; let a = tx(h.start.max(t0)); let b = tx(h.end); - p.rect_filled(Rect::from_min_max(pos2(a, y + 1.5), pos2(b.max(a + 1.0), y + row_h - 2.5)), 2.0, SERIES[h.stage % SERIES.len()]); - // Clearance gap (yellow + all-red) before the next stage. - if let Some(nx) = hist.get(hi + 1) && nx.start > h.end + 0.05 { - let ya = plot.top() + h.stage as f32 * row_h; - p.rect_filled(Rect::from_min_max(pos2(b, ya + 1.5), pos2(tx(nx.start).max(b + 1.0), ya + row_h - 2.5)), 1.0, Color32::from_rgb(230, 190, 40)); - } - } - if sim.signal.phase != crate::sim::signal::Phase::Green { - let y = plot.top() + sim.signal.stage as f32 * row_h; - let a = tx(sim.signal.history.back().map(|h| h.end).unwrap_or(t1)); - let col = if sim.signal.phase == crate::sim::signal::Phase::Yellow { Color32::from_rgb(230, 190, 40) } else { Color32::from_rgb(220, 60, 50) }; - p.rect_filled(Rect::from_min_max(pos2(a, y + 1.5), pos2(tx(t1).max(a + 1.0), y + row_h - 2.5)), 1.0, col); + let (col, round) = match h.phase { + Phase::Green => (SERIES[h.stage % SERIES.len()], 2.0), + 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); } } diff --git a/src/sim/signal.rs b/src/sim/signal.rs index 1b7ca1f..6659f2a 100644 --- a/src/sim/signal.rs +++ b/src/sim/signal.rs @@ -46,13 +46,20 @@ pub struct CwSignal { pub called: bool, } +/// One interval of the signal's history: a stage's green, or the yellow change or the +/// all-red clearance that ends it. #[derive(Clone, Debug)] pub struct StageSpan { pub stage: usize, + pub phase: Phase, pub start: f64, pub end: f64, } +/// Intervals the history keeps (up to three per stage change): far more than the +/// 3-minute timing chart shows. +const HISTORY_LEN: usize = 1200; + #[derive(Clone, Debug)] pub struct SignalState { pub stage: usize, @@ -105,10 +112,22 @@ impl SignalState { lpi_hold: vec![false; net.movements.len()], no_late_walk: false, }; - s.history.push_back(StageSpan { stage: first, start: t, end: t }); + s.history.push_back(StageSpan { stage: first, phase: Phase::Green, start: t, end: t }); s } + /// End the interval the history is recording at `t` and start one for `phase` of + /// the current stage. + fn record(&mut self, phase: Phase, t: f64) { + if let Some(h) = self.history.back_mut() { + h.end = t; + } + self.history.push_back(StageSpan { stage: self.stage, phase, start: t, end: t }); + while self.history.len() > HISTORY_LEN { + self.history.pop_front(); + } + } + pub fn status(&self, net: &Network, m: usize) -> Sig { if net.stages.is_empty() { return Sig::G; @@ -181,15 +200,13 @@ impl SignalState { if any { self.phase = Phase::Yellow; self.t_phase = 0.0; + self.record(Phase::Yellow, t); } else { self.start_green(net, t); } } fn start_green(&mut self, net: &Network, t: f64) { - if let Some(h) = self.history.back_mut() { - h.end = t; - } let prev_stage = self.stage; self.stage = self.next; self.phase = Phase::Green; @@ -197,10 +214,7 @@ impl SignalState { self.green_elapsed = 0.0; self.cur_min = self.min_green; let was_departing = std::mem::replace(&mut self.departing, vec![false; net.movements.len()]); - self.history.push_back(StageSpan { stage: self.stage, start: t, end: t }); - while self.history.len() > 400 { - self.history.pop_front(); - } + self.record(Phase::Green, t); let prev: Vec = self.cw.iter().map(|c| c.state).collect(); self.serve_peds(net); // LPI applies only to WALK that starts with the stage (never mid-green), and @@ -256,6 +270,7 @@ impl SignalState { if self.t_phase >= self.yellow { self.phase = Phase::AllRed; self.t_phase = 0.0; + self.record(Phase::AllRed, t); } } Phase::AllRed => { @@ -284,3 +299,41 @@ impl SignalState { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::control::needs_clearance; + use crate::sim::network::LayoutKind; + + /// The history records the yellow change and all-red clearance between two greens, + /// so the signal timing chart can show them. + #[test] + fn history_records_the_clearance() { + let net = Network::build(LayoutKind::Arterial4); + let dt = 0.1; + let mut s = SignalState::new(&net, 0.0); + let mut t = 0.0; + let mut run = |s: &mut SignalState, secs: f64| { + for _ in 0..(secs / dt).round() as usize { + t += dt; + s.step(&net, dt, t); + } + t + }; + let t_req = run(&mut s, 10.0); + let (a, b) = (s.stage, (0..net.stages.len()).find(|&j| needs_clearance(&net, s.stage, j)).unwrap()); + s.request(&net, b, t_req); + let t_end = run(&mut s, net.yellow + net.all_red + 5.0); + assert_eq!((s.stage, s.phase), (b, Phase::Green)); + + let spans: Vec<(usize, Phase)> = s.history.iter().map(|h| (h.stage, h.phase)).collect(); + assert_eq!(spans, [(a, Phase::Green), (a, Phase::Yellow), (a, Phase::AllRed), (b, Phase::Green)]); + 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!(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)); + } +} From c54a35c4f48ed6958f5c9e65bd3c16c721081b26 Mon Sep 17 00:00:00 2001 From: Jim Schreckengast Date: Wed, 11 Nov 2026 06:29:51 -0700 Subject: [PATCH 4/8] Show the sign of the cooperative crossing's margin over the gap The safety monitors showed the smallest margin over the required gap clamped at zero, so a crossing that broke its gap read "+0.00 s", in red. margin_kpi now formats the real value with its sign, so such a margin reads, for example, "-0.25 s". Rounding error just below zero still reads "+0.00 s", in green. A unit test in gui/mod.rs covers a positive, a negative, a rounding error and an unset margin. Signed-off-by: Jim Schreckengast --- src/gui/mod.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 9d75a34..8a78210 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1282,9 +1282,10 @@ impl App { let st = &c.stats; let ok = Color32::from_rgb(110, 220, 130); 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", if st.margin_min.is_finite() { format!("{:+.2} s", st.margin_min.max(0.0)) } else { "–".into() }, if st.margin_min > -1e-6 { ok } else { Color32::from_rgb(240, 90, 90) }); + 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)); @@ -1713,6 +1714,16 @@ impl App { } } +/// The safety monitors' smallest margin over the required gap, with its sign, and +/// whether every crossing kept its gap. Rounding error just below zero counts as zero. +fn margin_kpi(margin: f64) -> (String, bool) { + if !margin.is_finite() { + return ("–".into(), true); + } + let ok = margin > -1e-6; + (format!("{:+.2} s", if ok { margin.max(0.0) } else { margin }), ok) +} + impl eframe::App for App { fn ui(&mut self, ui: &mut Ui, _frame: &mut eframe::Frame) { let ctx = ui.ctx().clone(); @@ -1748,3 +1759,16 @@ impl eframe::App for App { } } } + +#[cfg(test)] +mod tests { + use super::margin_kpi; + + #[test] + fn margin_over_gap_keeps_its_sign() { + assert_eq!(margin_kpi(0.437), ("+0.44 s".to_string(), true)); + assert_eq!(margin_kpi(-1e-9), ("+0.00 s".to_string(), true)); + assert_eq!(margin_kpi(-0.25), ("-0.25 s".to_string(), false)); + assert_eq!(margin_kpi(f64::INFINITY), ("–".to_string(), true)); + } +} From eb80b1a7c81f195d6a385b371250a3a957b21256 Mon Sep 17 00:00:00 2001 From: Jim Schreckengast Date: Wed, 11 Nov 2026 06:29:59 -0700 Subject: [PATCH 5/8] Give the roundabout its own pedestrian-tool tooltip The Pedestrian spawn tool is disabled on the layouts without crosswalks, and its tooltip said "pedestrians use underground passages" on all of them. That is true of the cooperative crossing only; the roundabout is simulated without pedestrians. Each layout now gets its own reason. Signed-off-by: Jim Schreckengast --- src/gui/mod.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 8a78210..e7081b9 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -446,12 +446,18 @@ impl App { self.spawn_kind = k; } } - let walk = !self.world.sims[self.focus].net.crosswalks.is_empty(); + let net = &self.world.sims[self.focus].net; + 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", + _ => "No crosswalks here", + }; let ped = ui .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 crosswalks here: pedestrians use underground passages"); + .on_disabled_hover_text(no_walk); if ped.clicked() { self.tool = if self.tool == Tool::Pedestrian { Tool::Select } else { Tool::Pedestrian }; } From 8c325d1b130d8cfcc1be18cd3719f0d0a0df1d43 Mon Sep 17 00:00:00 2001 From: Jim Schreckengast Date: Wed, 11 Nov 2026 06:30:30 -0700 Subject: [PATCH 6/8] Show the focused intersection's controller In a corridor, the Controller list and the status overlay showed the last controller chosen, not the one the focused intersection runs, once the focus moved to another intersection. They also kept showing the old controller after the green wave switched every signal to fixed-time. App::sync_ctrl now reads both sides' controllers from the focused intersection after a rebuild and at the end of every frame, and asks for another frame when they changed, since panels drawn earlier in the frame showed the old ones. A rebuild such as Reset uses the controller the list shows. App::new needs an eframe creation context, so this has no automated test. It was checked in the app with scripted screenshots of a three-signal corridor with different controllers, with the focus moved. Signed-off-by: Jim Schreckengast --- src/gui/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index e7081b9..a5fc622 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -261,6 +261,7 @@ impl App { // The view is centered on the middle of the corridor: start the panels there. self.focus = self.world.sims.len() / 2; self.rebuild_twin(); + self.sync_ctrl(); if self.scene.kind != self.layout { self.scene = Scene::build(&self.world.sims[0].net); } @@ -286,6 +287,17 @@ impl App { self.twin_summary = Summary::default(); } + /// Show the controllers the focused intersection runs, on both sides: the focus may + /// have moved, and the green wave changes controllers. Whether anything changed. + fn sync_ctrl(&mut self) -> bool { + let before = (self.ctrl, self.twin_ctrl); + self.ctrl = self.world.sims[self.focus].ctrl_kind; + if let Some(t) = &self.twin { + self.twin_ctrl = t.sims[self.focus.min(t.sims.len() - 1)].ctrl_kind; + } + (self.ctrl, self.twin_ctrl) != before + } + fn settings_of(s: &Sim) -> Sim { // A throwaway carrier for the settings a rebuild keeps. let mut t = Sim::new(LayoutKind::Town4, ControlMode::AllWayStop, CtrlKind::Fixed, 0); @@ -1753,6 +1765,11 @@ impl eframe::App for App { egui::Panel::right("right").default_size(380.0).resizable(true).show(ui, |ui| self.right_panel(ui)); egui::Panel::bottom("bottom").show(ui, |ui| self.bottom_panel(ui)); egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| self.canvas(ui)); + // Panels drawn before a change of focus or controller this frame showed the old + // controller: draw again. + if self.sync_ctrl() { + ctx.request_repaint(); + } self.bench_window(&ctx); self.about_window(&ctx); if let Some((path, after)) = self.shot.clone() && self.world.sims[self.focus].t >= after { From a65893bf0d6aac7a72a200a4563d2493d43bea72 Mon Sep 17 00:00:00 2001 From: Jim Schreckengast Date: Wed, 11 Nov 2026 06:31:04 -0700 Subject: [PATCH 7/8] Give the controllers back when the green wave ends The corridor's green wave switches every signal to fixed-time. Turning it off left them all fixed-time, so the controllers they ran before were lost. World now remembers each signal's controller when the wave starts (controller_before_wave) and gives it back, when the wave ends, to each signal still running fixed-time. A controller chosen during the wave, through the new World::set_controller, is kept instead. A rebuild during the wave, such as Reset, builds with the remembered controller, so ending the wave still restores it. The green wave's tooltip now says what it does to the controllers, and the bottom panel confirms each change. tests/green_wave.rs turns the wave on and off on a four-signal corridor with mixed controllers, one of them chosen during the wave, runs it without collisions, and checks that a single intersection is left alone. Signed-off-by: Jim Schreckengast --- src/gui/mod.rs | 17 ++++++++----- src/sim/world.rs | 35 ++++++++++++++++++++++----- tests/green_wave.rs | 59 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 tests/green_wave.rs diff --git a/src/gui/mod.rs b/src/gui/mod.rs index a5fc622..3d5c0e3 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -257,7 +257,9 @@ impl App { 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 template = Self::settings_of(old); - self.world = self.make_world(self.mode, self.ctrl, (self.coop_policy, self.coop_sched), demand, &template); + // 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); + self.world = self.make_world(self.mode, ctrl, (self.coop_policy, self.coop_sched), demand, &template); // The view is centered on the middle of the corridor: start the panels there. self.focus = self.world.sims.len() / 2; self.rebuild_twin(); @@ -283,7 +285,9 @@ impl App { let src = &self.world.sims[0]; let template = Self::settings_of(src); let demand = src.demand.clone(); - self.twin = Some(self.make_world(self.twin_mode, self.twin_ctrl, (self.twin_policy, self.twin_sched), demand, &template)); + // 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_summary = Summary::default(); } @@ -541,7 +545,7 @@ impl App { let mut gw = self.green_wave; if ui .checkbox(&mut gw, "Fixed-time green wave") - .on_hover_text("Common cycle and offsets that maximize the two-way green band (applies to fixed-time signals)") + .on_hover_text("Every signal runs fixed-time, with a common cycle and offsets that maximize the two-way green band. Turning it off gives each signal back the controller it ran before.") .changed() { self.green_wave = gw; @@ -549,6 +553,7 @@ impl App { if let Some(t) = self.twin.as_mut() { t.set_coordination(gw); } + self.notify(if gw { "Green wave on: every signal runs fixed-time" } else { "Green wave off: the signals run their previous controllers again" }); } } let mut mode = self.mode; @@ -583,7 +588,7 @@ impl App { if c != self.ctrl { // Hot-swap: traffic keeps flowing, only the brain changes. self.ctrl = c; - self.world.sims[self.focus].set_controller(c); + self.world.set_controller(self.focus, c); self.notify(format!("Controller switched live to {}", c.name())); } ui.label(RichText::new(self.ctrl.description()).small().color(Color32::from_gray(170))); @@ -680,8 +685,8 @@ impl App { } else if c != self.twin_ctrl { self.twin_ctrl = c; if let Some(t) = self.twin.as_mut() { - for s in &mut t.sims { - s.set_controller(c); + for i in 0..t.sims.len() { + t.set_controller(i, c); } } } diff --git a/src/sim/world.rs b/src/sim/world.rs index 6c8ffda..62efbb5 100644 --- a/src/sim/world.rs +++ b/src/sim/world.rs @@ -54,12 +54,14 @@ pub struct World { pub journeys: Vec, /// Fixed-time intersections run a coordinated (green wave) plan. pub coordinated: bool, + /// Each intersection's controller from before the green wave made it fixed-time. + before_wave: Vec>, rng: Rng, } impl World { pub fn single(sim: Sim) -> World { - World { sims: vec![sim], offsets: vec![v2(0.0, 0.0)], journeys: vec![], coordinated: false, 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 @@ -85,6 +87,7 @@ impl World { sims, journeys: vec![], coordinated: false, + before_wave: vec![], rng: Rng::new(seed ^ 0xC0FF_EE00), } } @@ -268,16 +271,22 @@ impl World { 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. A common cycle (the longest Webster - /// cycle), the main-road through stage first in each cycle, and offsets chosen - /// greedily to maximize the two-way green band at progression speed. + /// Turn the fixed-time green wave on or off. On, every signal runs fixed-time: a + /// common cycle (the longest Webster cycle), the main-road through stage first in + /// each cycle, and offsets chosen greedily to maximize the two-way green band at + /// progression speed. Off, each signal the wave made fixed-time gets back the + /// controller it ran before, and fixed-time signals return to their own Webster plans. pub fn set_coordination(&mut self, on: bool) { + if on && !self.coordinated { + self.before_wave = self.sims.iter().map(|s| s.signalized().then_some(s.ctrl_kind)).collect(); + } self.coordinated = on; if !on || !self.is_corridor() { - for s in &mut self.sims { + let before = std::mem::take(&mut self.before_wave); + for (i, s) in self.sims.iter_mut().enumerate() { s.coordination = None; if s.ctrl_kind == CtrlKind::Fixed && s.signalized() { - s.set_controller(CtrlKind::Fixed); + s.set_controller(before.get(i).copied().flatten().unwrap_or(CtrlKind::Fixed)); } } return; @@ -347,6 +356,20 @@ impl World { } } + /// Switch intersection `i` to `ctrl`, which it keeps when the green wave ends. + pub fn set_controller(&mut self, i: usize, ctrl: CtrlKind) { + self.sims[i].set_controller(ctrl); + if let Some(b) = self.before_wave.get_mut(i) { + *b = None; + } + } + + /// The controller intersection `i` ran before the green wave made it fixed-time, + /// which it gets back when the wave ends. + pub fn controller_before_wave(&self, i: usize) -> Option { + self.before_wave.get(i).copied().flatten() + } + /// Copy the user-adjustable settings (sensors, operations, demand) of intersection /// `from` to every other intersection. pub fn sync_settings(&mut self, from: usize) { diff --git a/tests/green_wave.rs b/tests/green_wave.rs new file mode 100644 index 0000000..af868ac --- /dev/null +++ b/tests/green_wave.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2026 Jim Schreckengast +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! The corridor's green wave runs every signal fixed-time. Turning it off gives each +//! signal back the controller it ran before, unless a new one was chosen meanwhile. + +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; + +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); + for s in &mut w.sims { + s.check_safety = true; + } + w +} + +fn controllers(w: &World) -> Vec { + w.sims.iter().map(|s| s.ctrl_kind).collect() +} + +#[test] +fn the_green_wave_gives_back_the_controllers() { + let mut w = corridor(4); + w.set_controller(2, CtrlKind::Actuated); + w.set_controller(3, CtrlKind::Fixed); + w.set_coordination(true); + assert_eq!(controllers(&w), [CtrlKind::Fixed; 4]); + assert!(w.sims.iter().all(|s| s.coordination.is_some())); + assert_eq!(w.controller_before_wave(2), Some(CtrlKind::Actuated)); + + // Chosen while the wave runs: kept when it ends. + w.set_controller(1, CtrlKind::MaxPressure); + w.set_controller(2, CtrlKind::Fixed); + 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!(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); + + // 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]); +} + +#[test] +fn a_single_intersection_keeps_its_controller() { + let mut w = corridor(1); + w.set_coordination(true); + assert_eq!(controllers(&w), [CtrlKind::Predictive]); + assert!(w.sims[0].coordination.is_none()); +} From 43e2f8c8f27339faa798ce9f9d23b407c8205836 Mon Sep 17 00:00:00 2001 From: Jim Schreckengast Date: Wed, 11 Nov 2026 06:31:12 -0700 Subject: [PATCH 8/8] Update the architecture document for the 0.5.1 fixes * Section 8.7: each copy of a weights file is tried until one loads, Hybrid files with a score that isn't finite load, and a Hybrid whose network doesn't fit says so. The flowchart and the table of outcomes follow. * Section 6.10 and the tour: the green wave gives back the controllers it replaced, and the Controller list and status overlay follow the focused intersection. * The signal history and the timing chart: the yellow and all-red are recorded and drawn. timeline.png is captured again with the settings in Appendix A; only the yellow and red segments changed. * The margin over the gap shows its sign. * Tests, file sizes and the repository layout include the new tests. The document now says it describes the code on main: 0.5.1 and the fixes since. Signed-off-by: Jim Schreckengast --- docs/architecture.md | 169 +++++++++++++++----------- docs/images/architecture/timeline.png | Bin 46515 -> 32455 bytes 2 files changed, 98 insertions(+), 71 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 0a06a53..54caf13 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -3,8 +3,9 @@ This document describes how Intersection Lab is put together. It covers what each Rust source file does, how the code is split into layers, and how those layers work together during a simulation step, a corridor run, a cooperative crossing, a benchmark and a -training run. It describes version 0.5.1. Section 9.4 is an illustrated tour of the -app's window, and Appendix A says how the screenshots were made. +training run. It describes the code on `main`: version 0.5.1 and the fixes made since. +Section 9.4 is an illustrated tour of the app's window, and Appendix A says how the +screenshots were made. The diagrams are written in [Mermaid](https://mermaid.js.org/), which GitHub renders inline. Their colors mean the same thing throughout: @@ -212,7 +213,7 @@ src/ bin/train.rs headless training of the Deep RL and Hybrid networks examples/ studies: cooperative, corridor, fya, lpi, preempt, satflow, sweep, plus make_icons -tests/ safety, cooperative, pedestrian_rule, weights_files +tests/ safety, cooperative, pedestrian_rule, weights_files, green_wave fuzz/ cargo-fuzz target for weights files (a workspace of its own) weights/ trained networks (JSON), also compiled into the binary web/index.html page that hosts the browser build (Trunk) @@ -226,14 +227,14 @@ docs/ this document, insights.md, cooperative-intersections.md (wi | Directory | Files | Lines | |---|---:|---:| -| `src/sim/` | 11 | 7,602 | -| `src/control/` | 8 | 1,474 | +| `src/sim/` | 11 | 7,678 | +| `src/control/` | 8 | 1,489 | | `src/rl/` | 3 | 360 | -| `src/gui/` | 5 | 3,644 | -| `src/bin/` | 2 | 589 | +| `src/gui/` | 5 | 3,694 | +| `src/bin/` | 2 | 590 | | `src/lib.rs`, `src/main.rs` | 2 | 78 | | `examples/` | 8 | 575 | -| `tests/` | 4 | 749 | +| `tests/` | 5 | 873 | | `fuzz/fuzz_targets/` | 1 | 54 | | Target | Built from | Purpose | @@ -405,8 +406,8 @@ Three things about this ordering matter: | `coop.rs` | 1,883 | The cooperative crossing's intersection manager | | `network.rs` | 1,074 | The layouts: legs, lanes, paths, movements, stages and conflict points | | `sensors.rs` | 572 | Sensor models and `Sim::sense` | -| `world.rs` | 387 | One intersection or a corridor | -| `signal.rs` | 286 | The signal state machine | +| `world.rs` | 410 | One intersection or a corridor | +| `signal.rs` | 339 | The signal state machine | | `demand.rs` | 273 | Arrivals | | `geom.rs` | 268 | Vectors, paths and curves | | `stats.rs` | 254 | Measurement | @@ -862,8 +863,9 @@ stateDiagram-v2 * **`no_late_walk`** is set while a coordinated fixed-time corridor runs. WALK then starts only with its stage, so a pedestrian call never pushes a stage past its force-off. -* **`history`** holds the last 400 `StageSpan`s, which the app draws as the signal - timeline. +* **`history`** holds the last 1,200 `StageSpan`s, which the app draws as the signal + timeline: each stage's green and, when the change needs them, the yellow and all-red + that end it. Each crosswalk has its own small machine (`CwSignal`), with a push-button `called` flag: @@ -1120,7 +1122,10 @@ flowchart TB longest of their Webster cycles, and offsets for a green wave. The offsets are chosen one intersection at a time, in 1 s steps, to give the most green to platoons in both directions traveling at 92% of the speed limit. It first sets every signalized - intersection to fixed-time control. + intersection to fixed-time control, remembering the controller each one ran + (`controller_before_wave`). Turning the wave off gives those controllers back, and + signals that were fixed-time return to their own Webster plans. An intersection + given a controller during the wave, through `World::set_controller`, keeps it. * **`sync_settings` and `follow`:** copy settings edited in the app to the other intersections and to the comparison twin. Each intersection keeps its own `fed` flags. @@ -1371,14 +1376,14 @@ but only as their reward (§8.3). | File | Lines | What it holds | |---|---:|---| -| `rl.rs` | 335 | `RlController`, the Deep RL controller, with its input features and safety shield | +| `rl.rs` | 333 | `RlController`, the Deep RL controller, with its input features and safety shield | | `predictive.rs` | 289 | `Predictive`, the rolling-horizon planner | -| `hybrid.rs` | 246 | `Hybrid`, the planner re-tuned by a network | +| `hybrid.rs` | 254 | `Hybrid`, the planner re-tuned by a network | | `fixed.rs` | 183 | `FixedTime`: Webster's plan and corridor coordination | | `mod.rs` | 141 | The `Controller` trait, `Ctx`, `Brain`, `CtrlKind` and `make` | | `actuated.rs` | 134 | `Actuated`: gap-out and max-out control | | `maxpressure.rs` | 86 | `MaxPressure` | -| `weights.rs` | 60 | Where trained networks are loaded from and saved to (§8.7) | +| `weights.rs` | 69 | Where trained networks are loaded from and saved to (§8.7) | ### 7.1 `control/mod.rs`: the contract @@ -1606,7 +1611,7 @@ Hybrid's major/minor and T-intersection files hold only its best static tuning | File | Lines | What it holds | |---|---:|---| -| `src/bin/train.rs` | 428 | Training, model selection and the test report | +| `src/bin/train.rs` | 429 | Training, model selection and the test report | | `rl/nn.rs` | 190 | `Mlp`, `Grads` and `Adam` | | `rl/dqn.rs` | 165 | `Dqn`, `Transition`, `SavedModel` and `argmax_masked` | | `rl/mod.rs` | 5 | The module list | @@ -2034,32 +2039,32 @@ the static tuning; on the town layout it all comes from the network. ### 8.7 `control/weights.rs`: loading and saving -`make()` loads a controller's file through `load_or_new`, which asks `weights::load` -for the file's text and `from_saved` for a controller built from it: +`make()` loads a controller's file through `load_or_new`. It gives `weights::load` +the file's name and `from_saved`, which builds a controller from a copy's text, and +`weights::load` tries each copy in turn: ```mermaid flowchart TB MK["make() for Deep RL
or the Hybrid"] - L1{{"weights/FILE in the
working directory
readable?"}} - L2{{"FILE in the user
data directory
readable?"}} - L3["The built-in copy,
compiled in with
include_str!"] - V{{"Parses, and the
network fits?"}} - RUN["Run the trained
network"] - NO["Otherwise, as in
the table below"] + L1{{"weights/FILE in the
working directory
readable, and loads?"}} + L2{{"FILE in the user
data directory
readable, and loads?"}} + L3{{"The built-in copy,
compiled in with
include_str!, loads?"}} + RUN["Run what
that copy holds"] + NO["Otherwise, as
described below"] MK --> L1 - L1 -->|yes| V + L1 -->|yes| RUN L1 -->|no| L2 - L2 -->|yes| V - L2 -->|no| L3 --> V - V -->|yes| RUN - V -->|"no: the other
copies aren't tried"| NO + L2 -->|yes| RUN + L2 -->|no| L3 + L3 -->|yes| RUN + L3 -->|no| NO classDef ctrl fill:#e8f0fe,stroke:#2b6cb0,color:#0b2545 classDef safe fill:#fdecec,stroke:#c53030,color:#4a0f0f - class MK,L1,L2,L3,RUN,NO ctrl - class V safe + class MK,RUN,NO ctrl + class L1,L2,L3 safe ``` -* **Where it looks:** the first *readable* copy wins. `weights/` in the working +* **Where it looks:** the first copy that *loads* wins. `weights/` in the working directory is where `train` writes. The user data directory is where the app saves: * macOS: `~/Library/Application Support/Intersection Lab/weights/`; * Windows: `%APPDATA%\Intersection Lab\weights\`; @@ -2080,18 +2085,24 @@ flowchart TB The layout name in the file isn't checked, but the layouts' input counts all differ (45, 83, 84 and 85), so a file can't be applied to the wrong layout. -* **What happens when a check fails:** nothing is logged, and the other copies aren't - tried. +* **What happens when a check fails:** nothing is logged, and the next copy is tried, + so a damaged file doesn't hide a good copy further down the list. A Hybrid file + whose network doesn't fit still loads, without the network, since its base tuning + can still run. -| The file found | Deep RL | Hybrid | +| The copy | Deep RL | Hybrid | |---|---|---| -| Parses, and its network fits | Runs the network, with learning off | Runs the network | -| Parses, with no network or one that doesn't fit | A fresh network (seed 7) that learns online with ε = 0.2 | The saved base tuning, with no network | -| Doesn't parse | The same fresh network | The planner's default tuning (`DEFAULT_ACTION`: fairness 0.004, no bias, 60 s), so it behaves exactly like `Predictive` | - -The panel then says "no pretrained weights found" for Deep RL. For the Hybrid it says -"no network beat the best static tuning", the same as for the shipped static files, or -"running the planner's default tuning". +| Parses, and its network fits | Loads, and runs the network with learning off | Loads, and runs the network | +| Parses, with no network or one that doesn't fit | Is skipped | Loads, and runs the saved base tuning with no network | +| Can't be read or doesn't parse | Is skipped | Is skipped | + +When no copy loads, Deep RL starts a fresh network (seed 7) that learns online with +ε = 0.2, and its panel says "no pretrained weights found". The Hybrid then runs the +planner's default tuning (`DEFAULT_ACTION`: fairness 0.004, no bias, 60 s), so it +behaves exactly like `Predictive`, and its panel says "running the planner's default +tuning". A Hybrid that loaded only a base tuning says "no network beat the best static +tuning" when its file has no network, the same as for the shipped static files, and +"the saved network doesn't fit this layout" when the file's network was left out. Saving goes through `save_path`. It writes to `weights/` in the working directory when that directory exists, which, run from a checkout, overwrites the shipped files. @@ -2099,25 +2110,29 @@ Otherwise it writes to the user data directory, creating it first, or to `weight there is no user data directory. `train` creates `weights/` when it saves, and the next build embeds whatever it wrote. -In 0.5.1 two saves write a Hybrid file that doesn't load. The app's *Save weights* -records the Hybrid's score as NaN, and `train` records an infinite score when no static -tuning passes the pedestrian check. The JSON writer stores either as `null`, which the -reader rejects, and since the other copies aren't tried, that Hybrid then runs the -planner's default tuning. +A Hybrid file also records `base_score`, the validation person delay of what it +holds, when there is one. The app's *Save weights* measures no score, and when no +static tuning passes the pedestrian check, `train`'s score is infinite, which JSON +can't represent. Both are written as `null`, which loads as no score. The app saves +the network only while it is choosing the tunings, trained or learning. Version 0.5.1 +wrote the same `null` but couldn't read it back, so such a file didn't load and that +Hybrid ran the planner's default tuning. In the app, each learned controller has a *Keep learning online* box, an ε slider and, on the desktop, *Save weights*. A loaded Deep RL network starts with learning off and ε = 0.05, and its updates start once 1,000 transitions are stored. Deep RL also has -*Reload pretrained*, which rebuilds the controller through `make`, so it loads whichever -copy the search finds first: after a save, the saved one. In the browser, learning and +*Reload pretrained*, which rebuilds the controller through `make`, so it gets the +first copy that loads: after a save, the saved one. In the browser, learning and *Reload pretrained* work in memory, and *Save weights* is hidden. Weights files can be damaged or edited by hand, so their loading is tested and fuzzed: * `tests/weights_files.rs` checks that every shipped file loads, that seven kinds of misshapen network are rejected (one with a weight of 10³⁹, which is infinite as an - `f32`), that a Hybrid whose network doesn't fit keeps its base tuning and that - networks whose outputs overflow don't crash anything. + `f32`), that a Hybrid whose network doesn't fit keeps its base tuning and says so, + that Hybrid files saved with a score that isn't finite load again, that a copy that + doesn't load is skipped for the next one and that networks whose outputs overflow + don't crash anything. * The `weights` fuzz target reads each input as both kinds of file for the T-intersection. A file that is accepted gets a training step if it has a network, then 20 s of simulation. The CI job "Fuzz (weights)" runs it for 60 s, starting from @@ -2266,8 +2281,9 @@ The app changes the simulation in only these ways: left-turn mode and the max-wait guard. `World::sync_settings` copies them to the other intersections of a corridor, and `World::follow` to the twin (§6.10). An edit to demand also rebuilds every fixed-time controller, so its Webster plan follows. -* **Controllers:** `set_controller` swaps the focused intersection's controller, or - side B's on every twin intersection. `set_coordination` runs the green wave. +* **Controllers:** `World::set_controller` swaps the focused intersection's + controller, or side B's on every twin intersection. `set_coordination` runs the + green wave, and gives back the controllers it replaced when the wave ends. * **The cooperative crossing:** `set_scheduling`, `set_gap_policy` and the maximum cycle and batch join window, all live. * **Rebuilds:** a new `World` when the layout, control mode or corridor size changes, @@ -2299,10 +2315,12 @@ arrivals, and hand-spawned vehicles and pedestrians are added to both. This section goes through every part of the window: what it shows, how to read it and which code produces it. The screenshots come from the desktop app of version 0.5.1 in -its default 1600 × 1000 window (the smallest it allows is 1100 × 700). They were taken -with the scripted-screenshot settings of Appendix A, two simulated minutes into the -default scenario unless a caption says otherwise. The browser version looks the same, -without the *Save weights* buttons and training commands of the learning panels. +its default 1600 × 1000 window (the smallest it allows is 1100 × 700), except the +close-up of the signal timing chart in part 5, which comes from `main` and shows the +yellow and all-red drawn since. They were taken with the scripted-screenshot settings +of Appendix A, two simulated minutes into the default scenario unless a caption says +otherwise. The browser version looks the same, without the *Save weights* buttons and +training commands of the learning panels. #### The window at a glance @@ -2381,7 +2399,9 @@ controller with its description, seed 1 with Restart and the Compare side by sid * **Fixed-time green wave** appears with a corridor. It runs `set_coordination` (§6.10), which makes every signal of the corridor fixed-time, in both worlds when comparing, with a common cycle and offsets that maximize the two-way green band. - Turning it off gives each signal back its own Webster plan; they stay fixed-time. + While it runs, *Controller* shows *Fixed-time (Webster)*. Turning it off gives each + signal back the controller it ran before, unless another was chosen meanwhile, and + the bottom panel confirms each change. Scenario
   section with 3 signals along the main road and the Fixed-time green wave box @@ -2392,7 +2412,7 @@ controller with its description, seed 1 with Restart and the Compare side by sid * **Controller** (signals only): one of six strategies (§7.3), with a short description below the list. Changing it swaps the controller live: traffic keeps moving, only the decision-maker changes and the bottom panel confirms the switch. In a corridor it - changes only the focused intersection. + shows and changes the focused intersection's controller. * **Seed** (1 to 99,999) fixes every random arrival: the same seed gives the same traffic. *Restart* rebuilds the world from time zero, like *Reset*. * **Compare side by side:** see [Comparing two strategies](#comparing-two-strategies). @@ -2614,14 +2634,16 @@ it describes the focused intersection. Signal timing chart for
 the first two minutes: NS Protected Left, then NS Through, then EW Through for a
-minute, then NS Through again; the other stages unused +minute, then NS Through again, with a yellow and a red segment after each of the first +three greens; the other stages unused"> The chart spans 3 minutes, the last three once the run is longer: * there is one row per stage, and the current stage's name is white; -* a bar runs from the start of a stage's green to the start of the next stage's green, - so it includes the stage's yellow and all-red; -* the colors only tell rows apart; +* a stage's green is a bar in the row's color, followed by the yellow change and the + all-red clearance that end it, in yellow and red; a change that needs neither + goes straight to the next green; +* the green bars' colors only tell rows apart; * the time axis has a tick every 30 s. In this window the predictive controller has used only three of its eight stages. @@ -2743,7 +2765,8 @@ by Q-value"> * the three tunings with the highest Q-values. On a layout whose shipped file has no network, the notes say that no network beat the -best static tuning, and name the tuning that runs (§8.7). +best static tuning, and name the tuning that runs (§8.7). When a saved file's network +doesn't fit the layout, they say that instead. **Other controllers** @@ -2855,7 +2878,7 @@ intersection's measurements"> * **Main-road trips end to end:** for vehicles that traveled the whole east–west road, the number of trips, travel time, stops and delay per trip. * The numbers below, the charts, the timeline and the brain describe only the focused - intersection. + intersection, and so do the *Controller* list and the status overlay. * Settings changed in the left panel apply to every intersection, except the controller, which changes only the focused one. Side B's controller applies to all of B's intersections. @@ -2966,8 +2989,8 @@ These numbers come from `observe` (§6.11), which measures what the vehicles act did, independently of the plans: * **closest crossing (PET):** the shortest post-encroachment time; -* **margin over gap:** the smallest PET minus the gap the policy required, green - while it isn't negative; +* **margin over gap:** the smallest PET minus the gap the policy required, with its + sign: green while it isn't negative, red otherwise; * **zone overlaps**, **entries without slot** (crossings without a reservation) and **off trajectory**, green at zero and red otherwise; * **worst timing error:** the largest difference between planned and actual times. @@ -3059,16 +3082,20 @@ The examples are the studies behind `docs/insights.md` and ### 10.3 Tests -`cargo test` runs four integration test files and the unit tests in `sim/coop.rs`. CI -runs them in release mode on Linux, macOS and Windows (§11). +`cargo test` runs five integration test files and the unit tests in `sim/coop.rs`, +`sim/signal.rs` and `gui/mod.rs`. CI runs them in release mode on Linux, macOS and +Windows (§11). | File | Tests | What they require | |---|---:|---| | `tests/safety.rs` | 4 | Every layout and control combination runs without collisions and serves at least 80% of arrivals at default volumes (65% under two-way stop and under all-way stop on the arterial, which are over capacity); different controllers receive identical arrivals; no gridlock in seeds that used to jam; no collision between a turn finishing late in the yellow and the next stage's traffic | | `tests/cooperative.rs` | 14 | The cooperative crossing: no signals, sensors or crosswalks; its geometry and phase order; every gap policy safe under heavy demand, bursts, heavy vehicles and live switching; about 18,000 vehicles per hour served at the design demand, about 19,000 oversaturated and less with single left lanes or first come, first served; reproducible runs; light traffic barely delayed; emergency vehicles first; hand-placed vehicles crossing every way. Every run must show no monitor violations | | `tests/pedestrian_rule.rs` | 4 | Deep RL's shield serves a crosswalk whose pedestrians have waited longer than `PED_MAX_WAIT`, longest wait first, and leaves shorter waits to the controller; the reward counts waiting pedestrians | -| `tests/weights_files.rs` | 4 | The shipped weights load, misshapen networks are rejected, a Hybrid without a fitting network keeps its base tuning and networks that overflow don't crash (§8.7) | +| `tests/weights_files.rs` | 6 | The shipped weights load, misshapen networks are rejected, a Hybrid without a fitting network keeps its base tuning and says so, Hybrid files saved with a score that isn't finite load again, a copy that doesn't load is skipped for the next one and networks that overflow don't crash (§8.7) | +| `tests/green_wave.rs` | 2 | The green wave runs every signal of a corridor fixed-time, and ending it gives back the controllers they ran before, except where another was chosen during the wave, without collisions; a single intersection isn't coordinated | | `src/sim/coop.rs` | 4 | Approach profiles arrive on time and at speed, a short run-up enters slower but on time, the gap policies give the gaps in §6.11 and the following law holds its time gap | +| `src/sim/signal.rs` | 1 | The signal history records a change's yellow and all-red between the two greens, with no gaps | +| `src/gui/mod.rs` | 1 | The safety monitors' margin over the gap keeps its sign | The `weights` fuzz target (`fuzz/`, a workspace of its own) feeds arbitrary bytes to both weights loaders. A network they accept must survive a training step, 20 s of diff --git a/docs/images/architecture/timeline.png b/docs/images/architecture/timeline.png index 50407071a62a501d8b97f8cae5edd7eb6fcf8fdc..7eaad1446b696b48bc686b10ae0ca9d823cfad86 100644 GIT binary patch literal 32455 zcmbq)XH-*5-*42T9z{izj)0H$yV41S9s-1bBE3ot zA%viG0tqdI7IMd<=Xu||*0b)Hn-5u(?48+D{_|_|?vbX->67dyArQ!EHC06&2;?{~ z1aj!k--p3pd>Y@sgFu3#)fDgQJs)4ec?UA0$G@!E8&rB+Zu$Gfx%9IaznarAUK_Xe zrfiMAVO}D724EY#KdX9N&#Og-{T0sOt7R}peR9d=V=wQW(7}Voti3v0`@TLYudU8< zS*eZp=gzSUgD?8^3Vr*@>Q^&amspTAwqH9PeC z$NzfC-~5JuJ_s&J(xU|jX|-|sSNOR&eLtd{wRxrq4k@1UlQ)KMp|1tpOIxfvQCQa)I;^4D_Qb5sQxw?tz{$d z`YWhC>H;71L_`7!*!P78?*nzmRtOLwE&3SpALX>eI6W^0bPZNNKheM?u zs)BLc$G27rS8Vk4_2b`4u_$RX&UW?m?2%A;LyLA3&k0voX5(V;T9QoDi;LP>ZRdH- zeA`zLB^K0qR|K9DledSX$UZVK*qTJTH?FToCb_-tGM?XSa~yK_@$m2?&l;PF{BGSN zzN=S68xCNWsnli|Vxrc%6^bPKeX^5R_fDQ@*yq(`rF`9U57}6uh%{__y1GVlV)(MF zUAcF(sG;AQ?@5$eR@rvf@DI5`Cz4r1WW|XsA3k5^D9=%9>R%a$dxeD;zI=JO?vr=f z9)-BNzP|ju!r4lD0Po74BxL;k^ZmUAVo+seWu4Q%edUzkoHZ$@G&5_6FRUYw65SJa znVD-t^AFQW7n5T?t+duL&=r?BZG-T zsWMVh?^M;)IxCW=5flD~Zf>Q+s>dR~DSj?4cGkJ)GW&L7VnQ~cay09~a9oU-82TGn}z$2{VYisba2 zX7$F##wL+%ApvOdD{^%WAGQh&Y3JMP!tJ#Ip<|1^6i>G5AgyI;wG0 z+avT#D1@G#UdGWO)=3IIJY0QM-SAl0|0Mcwe$qmUAlaWH7} z_I=Ue@3|y?yhY&s6yz^`HnBtASBzh=8C9r0yidK%BhARjNW_PHzgtBp%VK21I1(Fn zp0sN$4cB4CF1Pn(AU^q$GF8s3?CcizYqj!C1QIeBVsXTVy@Qu8Ut+54orxaJ z^RmG=qI7i!^ZRS_RLTy;n%67rcT7W_XqT@E2skaz&o3=?>HjI=gOB*p1)2Vn))j%P zli0pvuZF!N>#XCJ_m_ua#^KI;%_T|hP1BhQ=mUK-9nU4b{Ruz19=odHju9>?&u4pE z8IFsKQc`u@iLeEn@1whS@Agh$2<{D4PUO_ruUq2dhx=)Jvn#$6)62{IZ3K}lG$#oP z9rcevWz1GYU|cdKNR-;z^R2B{IV4;g58kq!IPvb-E35HMQ;T$J*Vk8JpxT7&kP)B1 zDLcYb=QD#UcQO21TH<0tyLx*x;iVes?epxhK2@=2&&$ju+>via#erMRLNyKa-U!(ZztK%jdva|_Zf7HR} zFz&rsG=6=3okGM>Kw*=m_Vh#w3Vs@JXufwp>-qk`X#(vLqwSV@*#kK!i=b)a()Z7o z{rq-RVz|=4Y-*y4<2ySS5p^4+`n`iF4vG2MS(tH^&%&y$#6`VKFno8(sE8xL_XG+t05}w@I^N$D2kuqT0A{$c*_@!)wDL zdAkl!dbifD5uRJmSMOk>_=}3_>gr-Mcv`;x9Q4mx?wQZLnbwQId&Hm)NwWv4s)mQR zw6&Sv`GKPa?ZCs|a|Xf4whl&x;V$@!s%VUCe~WX^z>zfZgPrfMC{j{V8W}zt?q2ii za?*vPo%S?pR=DD`<%{|e-=Ij!^2SqN_n~}m*Zb9N7QHt`%xeZQm>Z=|4Xs%bEU|W0 zpUuR6KljDc37$-`88W^qzNFh*^=i@?M5KF@bZy8y1s}x8%6bvb!dy$}!!j!EtQWiI zmZsae?$l=>eIr$EY+g@SBqSMbZ$C$?DnsW}z7~rG6-H!StvG(>0%Ppd)F&rwRb?gF zAN8vFFg$a)RDchgr*2PAPvMmDO-M}4aDRFG#urOPXhJ+JKu1yWioCqGVsmOn=M5sJ z%23e_NuD#of3oP%9hO#7j^UJ8q|(dx_4j8m_VpIuQ?Tvv4Awa{a7qWZ5Rae!{j43Wxn{G=sIeH?T}4VJRXHYub@0mb zz0uPMoPV5N|C5ZJGVc^CxQgRD)NG=?#c0R))vH${Rfj&D#YWxE*TU5M;W}$CtDUHL zx7PmT$(j1Py77E5LeP$)=;ALj?rLU9MPzqxq zhm2HpS$?;hAYa(rj#g2O=91#|);GPudP<~mb@>1#^HYmU{Xw|KgK%8kleq8&Z~MwN zk*0NJ&-KMHt_D(Mzrq=J%snT$pF2WcLPFQENMBk2DP*|Lx&PI1`Y3LREl_5Y>QB-Q zFDvQm+b1pA25zjcD_xP4tS!yaPfCv0yFD4r8Oac<+}QX(gP-XMi~sOJayfP2=+UE~ z9cCMIy)BdD1EAJ2NcnFj;}UYo{DL1+Qm*p2xw%9=Vqp7(gHHH8Wa8#+HPSWYgU`Ao4?>dTf3xubJI(vK1 z$8N1)M~okl`xO%L9(ypPFNsaz$6K&HQ@C#|!w{2IoS7IG;)wlJ4~ykVdiT1ym|2xG zBF_mtR!T>%ybbx^4d^vRO}#FDCH=U$5*JFniTyu3V~pUW(Du40d+(;BL^z8486zddGnnn?RgrW@cPX~K*yl@I6xFRZ zSm(imrKwAo4Pd)DpFUj_5^^1ZDs**UcS$PAbj6pyIm(Ds*BD7f6+)*OZRE7XZ{Ac> zymv1(z5P<(19kN{lasf`cvDG83bDNWyHaH0gipI%0A)o)SXiEko3rwv4ZfbdopBm20Wj=GN@&JhHBC@tJh{kX-?bQjx1PG{lD7;g2&ax7-+q}lG zUE85Sv{&rxWlK(%Ev>;EcmNmMNAq* z`K6=;1fZU5C1bfYE?&`am^!Rx>KU0kBtjK0k8&>6sFpKvF-s4B?&lxz&HEF36z{Fd zxZ#|IP+arj2MIjK=Cx1OWb&B;C^nqwNBEC#!jjCE$vY($Wmz)5FNXDdMl5)+ z7FLIxUBl>v1RuFpg@vgo+u3<|oG|=T1yx_=no-%sq&*$_xr-AgL-2C0T)D%Z%bXsHw5oDAH>8otIpCNLd5W;Nw_n!v8_!V0h={Szj$PH zK34rflcV8VFHjsM^ zw(^J$3Cj=$&XEx;*12QqE`A?RE0KDDd~-B6u@%Z?0|z4 zCnhNP43N}f1K|^Nbg&Hvqz_4qH9b|r-43}nT%`N;>s?UFt0E(-!UI=-v|}(*5_r++ z*(3`WoWR868-H!@Q3z=CmQa;NAQ5h8ucG8lUVRD}@D~;C#=V{)t;B=`6zwbD=rP*f zI<2d_+rB4R+T*9BmR7$TE_>2{W5oF_JzNHgpWENY6mVPI4lK;dvV8n_s+Q2UFFxNc zL#ibMWYXVs!*ofoNyr;qfc`xc(}TKlya3f!RKED|Ma0DE#DY}(UK)Eb0To- zU`LlD&x9~4RTv4{+S>AIwc`EILE!OX$4LkBfin&^NyMy>-2p(zNsWXv4EzED7AXJT zv5Fi}hSW%-`r`MA zX(D@D93R1!u#vmPE&043MUUEATK4k_8Fdf^6%^Fs5j|r(blBk7k9#NSu5}$vFX^;d znykWm^~-yIBgehn->Nbw77`J`hfGZYSQN#vCv`Av*LWjfdRMHh@s=}ylL8WbwTm^N z&BQk;{i1Hm6u^E`AI^QfuhKy2OVcfQ*tnCw7?E8FHL2yG+}b9M!G{s4SWHm)phE3UvP3kKuWJs$bDbl)rP2e`YJ37H~B zFzc}@SvOK1Dg@I`$N5+V?<^ueejJ>)>$Co&);!{a6(o^t7v_L`K^o?7b`eiXNc8gy zpe$j6<~Fpn()_<=IipdO$*{mv(Q^CWsS;)ioesBB9`Z&hzl)Bj--tG~4Dj~RbR)K? z=@Lq;oW#>Kc!y>>&)>Rro1ROyB``B7jH^DRXF24mJZzs7s1qVJ-j(l)TN`G0h?b&0b7$YuA0{#C*LtD1JNi|G$2e;t4Q z&rknVP5b{9;QrkEziD<5_SeLhc*lxlpfKr2wpv?p{h29dC`O<*DJc$s1r}u>?9~PS zUxiW4|I_3z3a_Bx6%pQQJ?bE60@KuHtdr$8jBIrgPb(6J9HWaZtV)(k^O4GHS?~ATY z`70*Zc;vadYG3RUjxD4fmH6U*XIfW?4$Z~_lPGHKa?nZ|;OhIVX>xq!eAHatPM$hl z_j(WGnZu#QTLg{a+9?Z(=dYg)BmHl4Bpr~}8k%Rm?Sv)HbD4CyKp+=}{QY-EtBNo# z){UXpTU$po6d$NZq(smSS+(s6smKw3vOou9ZgbRHhb%5-n6tmxRf+a=WK8vYJTmN@ z&Xu_|v+`NA<10!oK9ixo;hvZ*?y2QvUlCVMM>7#vep<0^qmtYsyP2FH)}S8@=h12O zCKh|i>N$^uEZv2t@ze4vZzcUC)Ue3+Ck};%pz()IINuz-BHLi=`Lh#9wR28vt3Yqb z@|XT>8_BGA<6@HCA=O6|wVopf@-OSPPQGWCaC`g>?|jScQ*d>GIu#qUbz{{7@@2$t zuW$Pk@q9|y^=$2#g)a+hK;XS|;L2}E@9T@QC>t^tvpo-hB$#5_f2&S?{F~@~Brdec z%INJ;v3-1V?@Q6o{;pJPn6$R_-;mP2CKZ3P{wp_V5XcT?*i(%4)Vnngz>PU1pC{vv z6~t)*7DHLa_3!HB49Fggka|#LyEXoY^;pOE|=Sy+@@)Eb@?QuV^?b~YV-Kn$0%X5gk`4eEiG3{+W4tTfkpI!hJDrh-*fIDd45#EN{ zJ5wIyx4C-g)z>1W=UAs+J?;L}v(U%AXNv#2^3<{PnXCxi>#LBdtT*$v=8HS?B+zxU z8g(XdVK>`jw9;<`_fG~{hEO%4jycVy_A&RSQ3zQ3z32>}96RAEZ3#7?;-YrPImk=u zdPu_kvc{`9C)LgWteww^&aiXm&#{OY?{=wIHGeaG(Cb}ppX3$IE_P2bQOH!7C9_b^ z8GGCD=XRV&`=HLbA)Ry6(cCE#6holgVI8{S2?!U9tfvCvVpngtFs$;aZxTI?lONW>kcGmXX06e$x3ygDx&@Hi zn>TMK{U|{hJjfb1tkuM}+QytnInjT9MWS(=RG{`wZODFNBIL`5x{W!!e|N8`I1KF| zsz16hYLo=c!%|Q$G!ZfBABIfBZhN5hO*tB9UtSaq zxt(&yx%G^#afrJ#?3Ky{@BT2gX8c`~K7@eUDYrp`wpz2vxVGmVL6=)uOTABXyI7(LVoS#CI@-MKLu6`h3SLH`uQn+=y zYJG#QhBTIvn##*#j`XSXXxr2;Hi${P;fLEjUE$#&(jd7vXX@sRsca>bq0I7}T%#ED zx<#BBXPdHuOiWDJM_}{wG^(Y)zp{zRc&Y+AF)x)psa3h zat~dGzhToc1$g4$y1yUj&cW=Pqa}XrKwc|rTAM43z1;S`uBNN6s-{M zRokW`S%4$JN3UOYBR(ySfFuQd$jWkWdb8pIb=#f#7Q>`e2fFvf|Fnc3-L?5cg*(K5 zdo@-|;ov(ChjS}k`|{!yLq`3iKQ;p>_Q2zr9e$Tg3tCG_OG`6rd0+_lgt_YH<=HmM z%X`nMr@eihTVT(~%xr*dwECu;p~-^b&+ZeeN$8w-ab&j`y1NrXGwlAmI9&-4~*IhO+vZTF^wD`G$gFoEyJtexi#6LTlG#Py%zyd^oe z@D)e{ovlpFg(*5f*nn%y32bHlGIG*0GH&0x<-|qoh|S>{EYKo(MLH%(OP@J&MnFUa zf&J7q#UvBy1*8^zebA!R0pQV<4yq3cWxC=1K}hzjyV@o<^wT^$|IJE2YOn4eV%yIU(y z#M9_GrCowjQeqc>{QfFR-@G!fOhGN9^k%l6HcfPtM&jIt)2b zdw6x{;o*>&Baj(9kgK5O+ldoZ&ku47U%k?+-Zad%tP4JPa z&vT#vD$s7(4|g!o&C$dk-=P`&_)%q#TAilgTiW?#$(vpYz086}&v!Q$jYVW>fc?D< zLu>*EZtU~@`3}vR`K;8$twnp<=@Ta~HMY~=FTFZ)cps<$pkVV@TH^hM(N}+*G;4@= zZMu14o8zr*AT;kQJx7+cnrJEkf6%~w1ahDp662Ef?TXpJ&OG5Nt&7d)V8#YjMMm_J@Shnq{9A`In2QH(IX=Zi%UB2FE0cbnIXb& zs?D1Uh{^4;Q>4q@?G4~J5fgi-)ypDg?q(*B3}{>G8|pifkg278|EdpV5{3`94)Pq4 z!S-Ab$eC-GKK~%Q0IuH_C>FmR*G}Pam+ze#ZeR^gl&|<~=A!e3^X=J3e)t0&&_3-G$B;T9iV*N)CJ|9TLnid9E<^D{& z%5*yDF}Z!>Be$1&_%+D(d0bH;4UqopctQdhCt+D;j&K7LZP9Ttj!&z> zZo9m&u(Y#4tBG&}?T)9XVA z#(}2~TBQE=483^#Z%=?NqxJtw76E*CD*NI4)zoneQujk-(guFbkk>D7?v&T6?`Kq>8Rui~1;d}joH&~z zr1<*@e`Oz_JI~GH2r_=u43&kI>=pC`%+d?-LTG}^EFNIrgC9kjr@dG0?xw2bvn#e# ztA$uzMo44Bmf)0~y2$MPWYdSMzdPa5Y{V|%EQ6HC`X-1ZJfHdgLVa92G7;9MQTrA5 z?S9UHiC`iQ32>W@e)7u<-bV1YW+Yf|lz6n$a0HQOd*ZM-`s2mSe+a{IO=V1@3e zt)H!%H+?2-7|TXF3bcG6+y&Y%HC)r zhl~aym9=H%P#@#Y&G%D_3d_G^TBUB3t0A~hNb&+stiIiz8x)}1*1ZVHP zYAm|6GM#hn!F_IhCI=HW2Zh zQ1LT%bknQ}XN_WdbM$CMd(2xgXOHd`%0w{bh&84`9`u;5Qw($k3_@~t#A zk5ysP)8gbPXzp9L2BDlA<2TQja^Ak%cg3&UB4l=_%N!lm)My^2dF<6)eW4vcKd<@y z51!J-PK;+4&N3`6E%EX&M_!fU|B+TEFLV0{PnCsVL8YyAa}f|JbqQG+8Rq%8=cZ5N z&cY1r#nW_jb%J*fx+^PhVJ{<0i9cr5xr zuC~5T%xT8nb&h~t0F!lO@tHHt%x`<@fLG(&+tb|Mq*pOQ#*)7MM9%raHlFT8!Lhm^ z^{Du=_0=c+{{H^1m})n4pX-YkU(wMSPv_<6Y1=qMzMs|tSlQLpxTt-05JK|CXJ*vZ zNqyBw6?a<}5$_JPm!m!J)pZ!|AWa_UmPpgMo+uO%B{iOJ9>$QE3j+IJ;qD zocEgK%$5uZQMfdB+L=PaKe-_ZTv1G(GX=QOQ`intN!Z!(%V~S~eR0!!az|l(++%W5ULlw=n>y0H;pr94DYyAmrtDqEYc<{L*!Qg7 z=9in=n)Ww)2bDJB;?6}zMp!?IcEeOR_UR@|Pm447kyZ?SND~H+J~VmlB_7=f9w3m< z^Jx{k-1h5=icsDFT(raz!7C!cCnsc!`FRwqx?*JcouK+)LV~$WjS2D~=%CCW75tMt z1TA?~>Uzla{&2(}A^;}NSWt8vSYHNqZR29svGACM?>7hCtPaI>ZuX+H5|War^PMTW z_7mfx)hWO=H{Dp&1%r3&m>54lzwq_zFtZ`!+oxFvMFlV3xHdQTqoKhJdr8RThDyPZ zxr|+42I40eq=b|diB_9Q z?O)Lsq(4EOjH(hxldO78ws*y)104X^{^~axrhh^3g)94bNd=s+?q$v`UyZLJR0S1o z$$KW4{MlAR&FY&EC-4tM5opV-3{t^^@wK(AX%Bza?VEKi&iGCyjf^g&m z7n6z%cE$N*x+yODuWMGMBRT+HUMaPL<&@u9F&;30!OTfDXzv{8eLlpW+1WAsuuecn ztY~+IMy0G3SfqPwWLSo*t)-NhM|o%7Y>#faAj880bb{zcw?W;0)P8FL2_2ZJpSNn@ zT0-xvqd#JOGbu5iI9B6_8y^|HTAJY4o7$;C3%$so_TWLGU(b5wEwJylt1FwnMKH%M zxXI-p$t88@{(hR`#F|~NDVP-oZ9AG~j~|_9sGBApcUBt~fI@eV{}rLy8)j=92Ln#f z#oBjpLfD08E9K>(e|!I#R-x)2J@uh1R6!wrRE}50K z8j@MJmWZ4TBIJ!rOER!*d_!11FsH{rQ+S3QM0HGq%elHG;6TlGF+$j_q$a@oiVUuU zTC3D|n_4vDq%?yaEfdNdz&d$E|)lJzrE*PpjblA6ZMQYKJpXc15 zXo;QftFLwCgo|FxO20!C+ZJalJtUi^bHy>l@0e49Q&uEer+Vrgba%TA+}X#-sHHE+sBl0?A7Or zrVu~l6tOiY%65bf{_ydU8s4kCHa59%XYL=1v$n|RUK1_+KQR+wor?fV&yq?l#MkKY4N;I2@71*A=vrh6f%FjfhPFkR13s zfH{R%CM?dH_UGGqp$6sHqw;BAK4Xkij?*tEEU7DM`MzIR{;{ZUVo)DM;AN;INADd! z%|)CeY8#k1-D1O56C`aG!nL2Y5y+YwbNddI6MF2h8gL6K3H7=a$ResM%D6Vt(^Anq=artlF9IcbW zXxzIOc8ZgMC1Aq(W_xR!qEGbFuvAxBMN_APppejs6DL-1zM+7F8yq_uJ2rh2beL}x zaiF8D0%IdAB;+{SVB2)?3g)b*tNRgT|0ci&p4;-*6D6hR#o~ZsMny>p@PK+F;~l+( zK{mz3>4M=3uXbRQcUTCYZjUx#4g((FMv@`&mU9($RHE3!={QN#Hyw@F%?QIVCVIAiyzOAMku;&L$}-0Yw?@2B8%< zTusp4nT%=^SgVV$fUvY5Pw6^}H*O7uuw=XK=85#-lzH|ZR4Cp!em)uXGBih&a%AiJ z2n)AXng^j5fy)_i09jUBsKm`5!!qtYskf$U3Kueic2A!J+aExhD@#izI}c&a%`Xua z0UrA;{GfjOkOow=8G(;=iyaKFuFl82t!zkXsoO6wX$K3%+|H65BBxaqJVaiVuD8bv zm?QOl%&`-Ed{>2yMcuc>a=3T;?Cmu#@L(b#bG_{RUO1YCP^F3ZWdwy)L!r6!d4FC)D`oxE1j+)!Qa z;&|6F6Larxb90YAy?>Z6c!TT*0o#&}-!PQq1M&FnbOE~yQg@Eu@U-slas*Bpy!nsG zD9XyI3o%ogkZVZ(>wlSJwX8~ScA5qL^(@aC)E)j%yp(j~OTw)4Dqr8-U-Jq~F39VD zAp+!o9Syi=1xUvC^5$b8yXewL8cZN~0wa{e2_d!1}E!eSsS%qv-!lXgd4G&PAiUikX8ZZXqsz(=>Sga6eZ zJ9&2A;i+6EE31cBb-;&$(RcXP!*@nPMA)7Y?`CT3iTl#8Kb*XvU|V|-TXf8wKJV@s z;>zdE_uuDO)Vf6r_Vy=GaBuvpg}jkxUfx|2rf1^WcAjn5ABNNhTArwuK1ny`F)idP zcY7nD*x`$otQ?^mL9vir)l(sc3)E;p|%<( z&n@N7P6PIOpbt0mmZol0Ss;+bAiE7u%4CX7slp2i3;UB`NraFw@C#Ut)CH=9i-hOcjgae&6@=*T}(lclMdO5UGN5Xs37c+V!`2VeuoC@gk- zFxik!sjlnSjiuKb1R;f_qeH*thYoyA`5gnhw*dHUt^;(pTO`rzKbrwo>_`oQG^VDg znejeO+6x6I)^{1ahYG0wdw#?!q1Xs*Cvb+vAyZS+Ws;{MC`=Pu<5G^}nuNlD+BafP zDRr&ErXd`z^0Y@^s1Lu%ZvxwjZO_`++#GZewX3Sj&3)cF_(tUnccIIZ8?3yVnUkU= zbzdKUcMyd_6B$`h)VAfWmnQ)720)}|$_B&|0B`aNpcCjqv--V}$$7Hx)!E1>B8&y`8*Hf9UoOf{FQQgYxaUvDilHafAb=OMG^`L8%r<2(8v@6#C)C zP2RW)Ps7KLKXlp%2nv3vCuB)ihLB2gRL;!J&WWCOm4CKfNGO=Bw#GECm`WH^cEz^b zaW&MUI1fPXdU`uOyuF-<7asb_SmKp^@svW%GE=18$d6Zh_fp@U=9FIoM9_49&pm*$ zTy3seJk0tgTQ4gbOZ2}eT5a4j@C}P_mCi_?nO(sl@Sij@a==yk`DRQ?J$Bu@O$Xsp zJ}ow)HK*L=JU45PbNej;IkvawA>Y$KkN3I!PF`-ybAA0IhhMo!fPjsj{>aGnq-zm_ zND>5V?ndMSek4^7F6Q=N9~C zf_z#nCK8i3sK2S?I!Jr!KIwkGp)U7yHHc zI?q=Cg^6v^aczw+&#B8u|EhwB@7}0}U!9ba(wjZX%E1Ck0=P4UH>cm3{Km0&4m9}D z^>lRMsvaJ(VPOR;iGXmhEf#^PCz{r4Uhh*pF*&AiCubmC^Fg6yKb%w?8yf|B1k>7z zM#SoMMj0qPP#r1{4gHk-By3~Dvo(Kosd=Yq=~=yV|J)rI!E}Ws3Z4?(c;sz-QUc7p zO2=u&wwV4=(LqlIm_zxWtrUfRH{>EMyZJEimAhBPf9+D^na2GJ7CZCrH;0*pOppI{ zEUzFhLRGOq*aYVGZC62Q251a`m6)019z2MEAr)sFCf5@okC?#mcU4e&L@a)m6tZnbtCuTGH^7JMA{kPOxifqauM8r$C~5Z%QPu4<+I z84)9*{m1+1Xu38Fh5lP3q#c0Ft_rj>SJ}cvDLb+&|(D^B*vz}18&o5)n<@0pz2y=rX)ajq7W*; zW0GOu#QU;;s}xO8YH<)9=dRUY0S%SqjKf^vd61#c@q17p9SXd&P$Tu!(a+fp4Y|3s z+Lsj=*lsw~W?uuMK-omVQcfhNMnpZo1Ckat^Jwl|jXZnq4))3af)@;m+}5#gdI zPQd(({-HhJcjuQ&9x|+LZu&ejni_s5$*@NMcl?qT1uFjxL;;sFzW>>!FVf=Ui=m@c zR-QU9H4ErZogz+Lt-q+yZ(=;M1QNu>;857=YN<9(Y356#9GO&q@aAYd^hE#oNjjtM z4xgn7`1n@2osdfw0W38hp5Q2|l9R>G~H~m{lLV?^o4wMawr2bz}3d+y28{FeXn8Kz>C%BH|>rhVZzo{7YRDP%C%0 zw`<&n8i~z^#f}Ru`CWw{f)whynK8#fAmsYTZJuBMfhAjByUagLQSJu}m1&DMqq3oV`}p|hH9r#z&W+kHlAJko#)l2%ld-&`01 zDp7aRftzVExqqZPBmES;{JLJT_{yoiK8s0tSFHE2BfLhgpGpn`4_I{DjZ@yb8iCMK z9s2$mZ5Gg5NPx01pRG$ibOiE9i_ft2#tF!yemc9rh<}_fC$*9r8EDgUeN}Q0NNvxv zq^V?Rc=ajw+k^yPCn6U9#Cf*z5rb4lI^$@G((>99F&6JkX*NS0v*o3^8h4VWRF+M# z3CU&|bILy90n{h1RSIJaT;reB9PZpV9L81|o;>8$wQ%cW$wIjc@jh}SWoCfm9ZyQr zB)xZq04>+okL%QXWc2hgt*-fR<(xGb+M+giNn-#PKqksO(jTd;9L#OmPW%Cj>`#0n zClhUQlJ1c)fM!q6jkXz+OxFr!4!Xlt(AW3NNg zgt||5Zz}@WC8K-q-ZuiKI{Rf4S|kDRmrZWTO?XmP@wEbZ#^nw221{&u$*KIqEmFd7 z%D?{TEpwd4cJ+7SzPLa_U8$oJV zdn>~g|1rCUuIiO}?*E4vN}?QZMrG%no~Fib}PrHA#pKnLP5 zWTp$|tzpE`F1t$a9eUAM5OIg<^KK1$we78Z`R)_9&X~HZug^jt0Fj!7F1Vo%zkC_P zDHWgSGmcldQK|l{YEB4+3Yw#CU5@)>LI3tmwEE9*$|xyO{=p5Y@7pFO(#vQRH63f& z|9lQ^cBo^vUL$7d{Iz@j85txjxEt?@U!Ny}D zKQH`;mpQfn55oR`9;b!8ZnvkZ%dK|ANyCV*d@Yf=C?H~VN=&d$)<@g z9%LftxPN~sNEFhp5Z$-{%hI8E?&0W<9hdl}%ys9J!8Y2P3cvV=r2ybQ{y}AUF*A$& z7H1Ap&t<61^zfc^s-2n#V1_6&+P6ONORxn|g@lz_FK5Wf$@XVW&rnkQhh2#p%*@!7 znblmobYOEo1C667ED?!oAa83P5%UAXVxb(dgWLSTnxjs?3KQ26)OuhRLfOtkg?k05X60@c!NY29C4 zNrJ<#@$-Z7R7PV!H3U_gxSE(m;7}TAZ>5m6foquKKulJCqI6k-HtIUA zYuzW)xjAM^WaSz}$JOza-7bkNcwnns#tifi6WAzFj>fkL1rwrRwP$As=wT;6KgkVGmu8!vN-pB>yn3}2 zKtUn$=38&)$pFIaOz!JwhK!tgvdgC`9g#)8&(PB}scT7+{`*G`E6r>!Ua5%lW|{p+ zq5nO8fG{X2emr+RRm>ezdcUPch{F{ThH$&ggA3Tgp^Ce3#9q^C##n$4GYv&Ru4OT{ zJ#%H^sz0UAm1hv>_W)*s46QRk&%QF!^xt+I=hid4SPHnD`Vid;WRl^1Kh;b&kqBn3>Kl}%VOu^R&NE#0))Ry8z~U;rw(1=4QTEzRVR2@oH- z0%wZ4gzD-rAhKRhF3f3r&n;%x;2i+kMG#Tw@Bgz=1DWmDuGaeFaQpO4mEF^<6WO-c zvGLAek%PL=9NDUH@lnALM*a=|#T#QT;w((enP|y%PcK2{NPtSnwpqbPf&w|HyBi4p zPBoxJT)cQgLZZFidDA;C8%chz;BCFuTnA>=0M4JhFoLjso~ES`SiC+Ns1!dq_))?# z=(C-QHqJkqOXjA`^$mn&V|RxtX#^PpJi|0m$6ZYOyiOr-Kft-JqEDY515cqnhTGfA zi}d|wWu@LrC=@J#AX+br7`^=3?jp!6U6Ao~__iw+k<=Bl133`=y}n_w``o3rMcxi9j5EWB9w0jwk+I4 zYn=T1Z?j-=HdphVJz#JEFfiIPFKk-*cz#Q0|NU<)4@~xpNwwv7bgUboP~bY%a2!9y zo)2AGN&v^Zeu2FeVPD^vhKBSF&ai@lB;_1%vMI4@Q7N+3bAK#0JG&WdI0c0wj_&U4+@1M`u#V>J z?CrE@+@t^`O+9j0#`j%K4IrX9?V24xa+=C8q!2O2W_t0lnOo?rVo$Hi>oGZF-NE9{ zlee;UlW;PKe}=ChPK%E(cw@vKAb#K_`}k7IYaH1o>s|Mv0sqqbZ1w|_tKc~W$1{L1kCa}&sxDx zlWG^Bs~PC%xZr-#Z7!%5-p!2&v}V|NHR0R{4vU@t{xt}+xv0Sp?-o7KjV&lI|1>AJ z4VasbIC$ynPg}>R!TS3uvoba} z*SmUqi)wt^YkFfdat^XC}g*)BC&gdw13~795NZlo=YE;clBc3p? zARv1Csi}DY)0F4d$@}@b{B?p44i6(?D;`Jx5a0U)sM5 zlTs>&S`y0;i?;3+srom9f3FLE=-LChZof>;2g8fcKwO0fsmQ`OV{dD#N8H_rVS+-> za7kx7>-jQBmAfN`;rTjC7XFS+R>2mfNMzVoOx^evh`;FTYpBZ!*?YDy`DZlN>>dGS zRB}Q3s+4igK(*75rc-}q;K@X2RajNLT&jM(nbh4H!;mS@fca*o#yk)um?Gp^E}!C!v2deuD#h;T4_U0@Su+5NU}q@@1w zUM6ly@FCr4U%_-0V(!+$hv)~nyZOuu|3U{K=!5ii^Lm;Nsu4g}qcHQ8S|pd;p0o@S zScm!JTQ6S*inwQmYnnHw!mkMkes`*-9y`P4w^G|MaRJL0YHM%n1DqD9cUtWj>#MKd zi@eaxLe1yTPirXWU|gJrYVK3Gf*#uGs(HkwAp@{JoHpLOzXjW1XlwH@U*tV@3(ho#+n0F@c(OZH>E@*O1C1Stn5=+m6dVq5webzy{X)l5h^>YYzIfiad3>5 zy+X&qu`QsNAib9z47tq&%Tj< z68bDyQq$tPp~&D6kSu8&)r$K9oc<{E`pUYw)3*^)3iNdQ?Flv0AE#mbgYw~wiuX~| z)GlPGH4XjZ?fwOHET|x1UI8g7W#1a|*^eZR6NtNszQ|`i zs+p{kAa=!Jt_a#WeF4XuY7+_aR`TzA(VG!X5u*eV#$?;(O__QyZ5b-Yo8@#cBrWSoRddHh{TJ@%G87=Nw{u| zpv0GE`WDwWj9Q)ha2Wno^uGKj})F+pK#&{Tg#o#u2=GE?lrE{Q$k$&YE@i{=Lt^ z^4-~N-wpVOa#3N}r&$dZcmMaoJWKxIZ5+w{>sP7%%7zXPxu|e*uwhPU<@mo~;{St{ zi;SF@%`hq__se5$5}}v^Y)}b@>CZN$M;aFg?Ix#vY7`-MiKe4?RLaqul$gKHPSzj9K_MA6W2dSk#hC=BuS z`}e*aonSl3br@@}>{i2|oBx7l$BblPh^5`B%fav#i54X^H7&s6Q&+xV3tMK(L~)pn(^ z%&n7qMxz+0w#8jMBczp_6PI6{?Axx_ zwB1_nronPJWuIWmwC>tj0~pO$3L4tiWK@xh+m&ly2+dP4%MmT<^99l)WOENg%eG5> zvIiU%`)4osZA>?AuDi;+hN3#l)26OpVi1C9$53)|L3=wOjFg5T^kOj@{VF;MZ%P^i zNd^86e!*{{pB$9@j9Id>c*!aYjI}gHR;2f{OfU<`NK549zNDn004YD;YiFvyj@*lZ z3*e%0?d@sQM`Ym+{&8k|BqE#L(N)#rzr2+iii_uSidGd2g*VsH5p>!c>-vS=-hKa^ zLK$)Bx?weCvl6>`xVTyxoA+BWYm)RTS0_3mI&P__4>%Gl!kmL9bqi0DyN8k8iE(j- zelxsPHCs^mItg+&JZ6mj^V01g(grmTzY)f43jiv%k;+AFVUhL1VPSTbfWqF|KT8NT z7LJ296KGPD0f%nhNSd0HJ*7<2vKDLs3M*g zLTkN+*7$%TtB!s!MmYYav0yzNyvTFE1-HFyB-_?hvfA=0SVgbMhJ&4n$x0|p?auQ& z0_rfe|0Xh7ZTa+?D`O1GD|TDuG0A9h3S)!lL?WLFCt_61&cfye{=$C`j){4*^=@0 z8mMyXhfg|={MeGFSxQS!T$-N`n>^OXUyjA$S}_Tvpx^`CdQ0C3#KFmOQ&RfNmoEd9 z6W}+JiR5u;md*To<82DNd!fvO6%AvmPKl?5A6dgA0aA=*MG5?3u|bDzK&Be1VAp2X z^H{wc(`|2PA_*!|-qs`E9O&pk8(GvqDmbXNB@g(K@LuvE8d(W~m}-;NH9s~LV8k!s z2DsVH3%R(iykT8bo0{yYa?`qVKOFQuXug}*ynznV*1q#&z1(PNhGeUEQUt}^u=ha*2-5RX`>?q{7i zF{a+Q)8$fOj)8YTZ`ij z(8OB{^}vkQAlY+a>gs(`)L}GQ*v1Vuf#O&ty&y;7WgYv9wL>N5BQfQ@@c{Qqxy=0& zjdQcqo}ZS4)p2>GTn!^cVUv(grqs!AclHaoNVkX2D?zYgu*yJVOG|+rXj}bN_RAb% zyJ)z257tdX4PB6vv+!}*5SRW$;dYR^x-v9?-;W)N_ul!!;kzY|4R>VbhZB~iSm^0D+7GJS8dcML z0e`37eFk7Bs2{QxTPs7DC08eH7#SI5+fIhmY?SvV`L2jvy+E3Neu&y%IYUAD_Wif? z#O6Mqo@b;D=5>JWK({~=K$e7dVQhN|AOnZ{m9!Fb5xD4O?!qLxIPd$mzwdK+%efaX zHo(Tgf}35mbza^KgV8nCTJyA2316yJ*YmIEW_$+otE*c3@D@)+^beuCNMhvG+G@yL zDeNxa%0qx`cZDhB(nq~87Rl-Nl-gq$YLAYokn$Ty`rv3$A}xhB?n+Hhgx1DDQ}b0~;#zG(8e7$NdVjrDPY6z&rT8|5W1{@P zdPvMeY-{IR5SIW}Wb+wX-P-dM_Gq-wUslWvCr`3CP2L9~qU+w@j4j(pptSXbw5-P& z{L&^mdxELHLUL6SA*xW;Nh*fx~hm484 z|MB}ss_a4E=7t9PI7O>w@9)zFB)p7^JqaT{Ih*48G`-7X=bFGb(W!9j)+~B7SnHC} zu#UWJx$XF``b06f_9I0e;tBf8@Z8{N66%!|UIhUL0qu z43+dPQX(AzStlSsE``p^Z8LZ?$I9TGMAC=d>?Ztile~#oZXu4dbHp)aDs$P|xPMc*l(Dw$JZ73_gum@m8J?`!Tw1SsAb)1m6r#eBLiKqI z5QEMOMPq_&D>Q0L8-tr|&}h%eZ)asyd)xx<-_d?YBFW1X6c(a85qE6(F+o#zHU8vt zz-Dp~vcNdmAth7>+zC7srd-qdcxTRI&1@y*+s~a`s4wN(P!|o$b{=E)jow$KBbb-N=yzI9EX90Mb z>XCQ%^axh+bngA<6eaGfdVaKb4|WJ<;ZS>~E)C^EdClI=UXJ|&bNKGjwDJ79BHQ)9 z=tR9OVSv0FDUe6)??n8TSS>3HZ0-wYE^`19SPuQJ1AnaWa6{F!j9B=T6`l{^{w+n3 zv^Er8Zt*XB!gND>XJhhuetZ2lE_+0++C!>Cqb57v|nNt8#}u84_q3kV2ELahQgZlnik zzRa%*KvjTHCB5%&gE9&DDQtFb)5rQ?&|}XwU?7guC8p^XmcWP%GLmriBn@4)_AALw z+t;OC#QO3OKI$Hs@sm8k)$LsT6F^M>-=+pmfkSumXxK`?4iB0K@MQItii*#o3z0^V zy4<_Ex-0z#re6;}7q%6baSeokBKT#c^hX~a!AV9DKW%|hUKzXeA-7pk&Qw!#3_e(R zQj!J9{Ocpplv>4-;Y1pv*hR%=<<=wXn&h|eeYbc2?(j=Ix&C8V{mBc`+V_?F5`sAX z6gBk?cQ0Ox|3!FG@L|Z0bd0JpzC`ppDvQcGwlfK#O4V;|?Y@`&L1Sl&0`Y+HmTFze zZ%rG@kvUpUW~8cNi^d>ZQ~ljD|8k?eSkPMj)GO}@*CBFcu62{+(0!NL|6j3%+pcmd zF(c6{7GTVuKX9<|B}U_Ln%TMD-@kqpb?Q6?^as7bj@a=o3iCBAvqKU=K8(l10chm) zy)<}c2)t@ghE?RMhc(fjOkcBtIg8zLrI@9LT4XHtlUz$YMst z(aT0Sk)|tWRB36$-1@x(pYMDF5MAxBCNmk&vj_0!lg5x(K`4WR-~wP|o} z!MKB?x>`v(4m)#{j;`B<9Ca9`WaDnPg++_jg_795+B?>36VP8@AC4uN4TyE+p$0jsRr^AAGaRXWVT;YT=vr|r-a1@ z@yL+l$CWn+qLVbq$yOca>;2nu$!87OerOj65z zb`43fibh7xuGY>p#0nNp3{9ZKQxTe%NaeG9|Ixu5+pOSh7f=A8FAn0(?aoHzKw`pv^cU4MiOXk|OJMgcdu^VlAq>hv^2SsF< z7Xou+KjB^&dFrBUA?eL}hS;^;iqK?{bbNaTk!NLD2q?&tL#LS9vKlSes!Z`U*d8V> zA%;rGz5o$G@y3o-Xys~0-HD4l&m$tq@jW??LYIVOWnSTIT+WZ`x*6{x?d927@IbO$o5=4M z;LNN)y)1E$pLtswI|(yNG@6>`sbZ{ewrw0#v71hdLEtj!DJI_^ub^dQJOR-F78bV; zb219^x-2$ufYXMkRN~uySODh02_m|V*7S5Ca_R#gb|>G_-g%{WY3R`jkxh|>SE@iD z57Q3xRJy1;sH)zA_*0u_`N_#W_@Llm(1hk2w|y5nt@*_8A_LAONyo&;`#u>aXa-mJ zg7t5>jpEm22i?Q5SfTpsEj0Re%TAy7j^k#jF5^F9iiowfwOGaUM4dwpy^cysK(RFL#mqy{VE9Ibd=Z%aG3$yfKTUIPe zOsM3R`u2m@UBmJrH3vl3E%{{IL^zY~fYQ|Qy@mX2=UaiAhdST2`8HiCL<++9Yk65j zoaqpFt93()`RAGXa>v0WvUi`*6%ldp4N17v>ft@X8R(eK|6rB8?IU5f1|j;sc>yua>8YRodF8X2@2Qg~ zwfXdLl`D*6e#Rfx1O-7|kgHt=VG4I_4jcsfyvicacMlCMgb+bGArO--tE=K*k@Uzs z(a1o3)k^ICkNdPLt!BZD@t1h++of4MstvuAoqu8^u=ZwK0!-*SI^AG}{Qa@O#I=9_ z;XI3upr8W3{?#x*RLZ>a!VvBP;@shu?*?M?lBxnm?wMUaai-;0`~pRIfbb#F#(}E0 zJ6D(DVc7Q2u(sM2d2{EF;DjAbCbwK9u>P~N)lk87ABxyA4q|=>irdH#F`CY3aC)81#ckPrxjA~mhUw@@!LhP>xRoj8JRF?YefvYF^os2a{VEgC z6Qu}lW?2~|ef1iDIWo@bYJa77{UazxK^*$|18-B3FC#o$1Gz|h2)(T*=KPHQ1pqOJ z`KeTB`9g}P)9^4IavRtVXtP{{L;|4Pi73H0#?63Iee19v1o}&Z!~#RY@hy?@@fn-t zlG0Mpv}8Y`On$upbTWnk;5_Byn|VcI18MDP2Juv06Aw2xwvQEBjsTW0(VjCE*=l(< z-(e}!%g%$X)y%>|2RB0LI&teC#E=tquaExn{866xQ9)JeA$t9~Qha>;Mfkbu>9y|e z?iqM<`=PXEo<54y4=%g>s#LPP%rqZ>Y{^=mdk*)8A(X^6IlBA{qC59JS1`6cd4xlOnpp}U_yXWCT&(O#q* zl5DAX9-d#Y)n0v2x#n~<1ol4r?|rL2qPW`~T6)02aKfB?d|sQQw>vwlx8k+w%Z7Are4<50)C``VkprFw2y|}N|ABuMv;DwT$gPu9MEyswDKaJ?9 z2Ll46NlbOve^$cvnq^GdOj&E^dPQB*fjWXAd8dV2skL5VP60}V;RPs|Bs&`-#uT*A ztmFBttcf6HnW!VLsYU=w)fw$s(UI9UTv%8rwfSuda4O)QfCQ5q>oVSc@UF!mIoB5W z!;$e(-k4K<--Y4Ym*J+7mDcV_xDR4WTzfuV75wn91^P&HAJQLMdH%T-C` zZW3db8qT!VSN7&dJ4gyVKojnA{XR}9Ev(bfCkHwq&XT|``UCe}9lCLD#3XCYfI3hM zx!tg~L{$j&mb&ZX(Oh>$EWr+DTeh^825jJo#4n~PXYg@zhX+yPFN=|GX#5pVGn|uM8XY_5rv??%k0RIAbMOAVkj&vD*DoB&P9HG z7Vr)OoGZ3XTUAZ1H{C$~g=nWi^DEpPun~x~ zwVIrn8JjlL*ZA9&O(U(7yQQy^fcKoj01^teKc_$=Bp#*fH;zZ0Eiu6EN+ZwI4|76@C23s1Udg|u1pSPb z1!8z`5S}+;U(PR3?n0_-QQbp*_!M9quzB(7m0xQ~72_LW*n0VLD9LZdb4>|{vm%`Z z(hg<|Vwu$71CW(T$<3*Mt?~WKn*47@4$c4~b%A{=FB19F125%6^Xi$L^)YoB@Tn9Q z@B=nyjn%dG=-jiu6rePro<}5R1eXlgf>7B(%4_X`{0rQ4g}9t^<%)6Mwsi=E{7jA@dt6OfaDo@9vMR za-Q`jn3S52oD4~?+%s=Ga)BLi*v@Fs7CbBZtE(4x*Yy6@aIi$qlY%463Ci1$LK3OR z!$1(OxAsvV)dL+>_V3G?``_$LB{oi3N>+H2yT8~76ShlqbYj7@X7#0xPn_Ec98iQM zVbloYPmQS$gcNXxMUs9ZH+CHu>E%Qp@DqT?36Pt(;?rd=Gd>byO={g>d+0z3!uOEK zng~b@n}F*}x$I5Iw*bzO{bfRA_s`%4+aVLvL)6k4Ee3i;f2k-w!%g+o;@XB)D!K#( z6*ai{xSd>_$Q^EE6p=FF=X%RpH>;<;u8WdjG zR@-at)9mc=3{dq^zsZ?94Zmo*U;^Z5EEF~=O+`J=jiCut?mISJU%{c(Fr=eR^I<=*XH-Frv@fQq7W4~Jw zLz(Pj1d+%~AhD6R*M{>DTHUNu#H7lK@11xcy5=Dn<8M)}H)HjbCiCimYby*5}@x+YC;;Ha+kldpE6Yb=8ft>QujH zSZBWcd4d>>zIL{)|Gl#N&kRdn6GmPo8^Fn57`UHepCZQ`Qe0AENes;;cVaN}1~53J zWs&FwtP)9hv{9ZGJPug^8rv^AzuT3MVxaki_#>c_)i0=QEH%2J(fs2jz1OGN+&90( zmHT)D;10(wCc$NIsay)!KW{uelAy0|fJ8#-6bPmib0`9lC-|uC5i$v}4!dpz4#9ko_Bhu#fEvE;U39r;9)GmySR5 zq@Z1Gh(#+ZBZF8$D}_;wAR1+ztAoHmINil%WB{(?3^Ea9?r2vV>#m9+oA-KKzzf0% zy}XE9Q*Tv4R)Z4`Y~b+{Xrx2wu*`jb)@+ub5fHeFa9zP(2pF%n1DawaNx?ZdUk^%nR%9 z9Up0--5&og=eq?!nPItQV)%QmWOd?^)B7FuuRvDNveZh<`@67=;)HW@3w;3Amk(h| z3J=GE0|7vKTR?pQDJoiG7NnxC~>@BH)d@Qw>?SSg(WsXRG0!p7}I&(q%k zUtaFg_~hl#5Lh7SWxs#EqNW-FGr$-R{mL^{>(~9?|7cscPuxG}gZ~JuB;_^yvD6-L zwV_x@5M$EQer{xF2o_NpRaImB5*?JHP@#%3j)Z4C5j-4+SkAE369ywSTbL+1z&YSd zox{Gp)caKMvrE^dFk$quld$`@fQ=1_!unJKihO7e_#ld7#c%^a#$lBNPTq?s zQe{d;G%^2NG2^_|*|Wt1+J8Cf?}8u^n0-(@&<#z{1t2`wUDu11;4ZddW;XXy!uWEy z6_mcTurSlk8}V7gSsRVsmhv5nXt3vj)6M%54b|#$nts;k0ewxlfFP@YX_b+ILBlRy ze4K`kNz_x{c7_?uEXkZekz;yI_+yK0yEBPo2=ytlQ>9O{T|WYg4f?fgn_{~u6kycO z%Z5!7yhhKj?w?(llkyz7^Frx)YutQF00nW{s_n*mJq_4PO|3=wZ5U-ehTo>0Mlg=} zZK|rOw&i-+o3lpiKxPP(YMQ)83tzqR;+oA>CwvXh2Nn5y;$nb@;JJ)*g~ z{O5nM4>{yLg@Uc+Ox-%|+|sN;rSn($&NF!`5yZC^tw$&p)1rzPoUO&oso)p(JS91x z|B=oV8HPJKIxcu#kj)b6p`pb(aSRk#`Cz#{KvA7Cs`t9jK|}R*R)>n&5H$0!^^u@lsCJyYU@WQna^&Z?3PSw6kcX~{$8YrJ*il_Id~QL7unRt z?qCl~ZKnj~=%aqgW7Y8@wXi7x^0e3!4A|~5I#qf)afhv>3yFTSqd4xW{I1d%ar~F^ z7;AX5|4a;Mx2vf`0}A(+4&*fFYun6h;@miJ8r!;+rq(`rBfJ>OT?2Ez*Y7=G+;$Td zIH4H8hwskgotfOp0m+sT0*E&)EiGndX@mm^PuHb4P&aj}5JaUhdi?>+MxEl=cYZgXE2s$pvjOt9! z%zU@j8mE52G}cd_8VoXMbkF{|0!P_4ORR2THxK(vys(r-+P|0u-QD468}Qxq zP&GCl#mM>kvYY5dd_=!~eN6_=HkHIPb!W9MPTgB3(h!F9XGS#i^)(7(>+iV=izJoC z93OvPHD!NdQpN*o&jZ^51U&BYe{yxd)fyC2q$fp-rxcpc@PX326TpTcmy-PA{h4#)$*xYsyvN#Lg(bdmyY(!(x#86)$-U~hx z_oCN6(WKg(*7(gQLbBGvabj+P7{@EiMcW~1sUYYX|CQF9pBv85SN3o=0rbe1n`A51eXfc^`Ta4ZakO9G_7@>O<+%v+8R{C zb8C~`IEYBYddTi8dAWxQBtd;mh5;uz>4i;bCdV>@6ImFr^)P=r>g2}Xjpn(X$6YnMFTo|{84X*x(q*DvBv2qR#$O{?B~DB+Y@8$ zCFildTao%&y1EUKER_51AvZf8pOy?9`nkn@9Ua+ojKe~akx@_%YBU!vz5dnB7jhWYn-*o6lCgF~qgcRLt@ki5oLdyjvLgj0|K0iM$$ zX`Y+i*!U6}d&usX+Uhx~jf6;eV4~+0gx*~1TBHBxJL`<|g1lqTvs2zaXs+&czdK+@ zX9XVqGLCbSgteQ?6W9Cy){s$ChwV@^Jfrs{J+0|BQV_>uf?4Mwc4%QsB z(lic^MV;8)btep{+{aA%G5%9Ec9NdKb(MvTPS){H2`igS4o6DS2M*pO7zqb7&lf4A1acuK5G z(>cNU;~_eod}m|ZnO|p99k{Cp(lZ5_!@t(>+Q^!Z~p99-KS(RI;L+8j#m|pW|j2*+;%nU zp)^IV^*j2Y(G-+X@cFnV8S2$WdXIEn)S8nL${V49S3n!pFDm0*;dFoCcMu-EV=o!^ zs`>ob!4h`>dm!y(27(%slW(XBQi8ks*Ym+<=b_nEAb#UDz-wtlSPS-C89py$Pge!LHAuXrPQH{9 zb#bD@D1&I*^R~EWX;-|ttUH`D^gf?h`aFfm~ zCn#bywB3bU3i*LoI_D6|%`j3y^UZnb(MEmjM+{R!Y^>ekIS3XmwkGDoFsLZ?QzJ>d z+$HW7vGvC?tv=S|)&^Hx@M_>N5UBEUt}c#_7wcNtoR>~D_8LLmJLll!#&-F>HZ%-S z0lIyjM5r{eK)l*iWG`gx%(#!;W~J}F2QnaJ4lw}ijn%i`)NO&qhVcalQF#ydACO~X z_u*v7n}eLyxmhbHpW%3No$4xsdXz*RCMnv{_tCW99?|W)Z?YFldVQ9t&Gs}hDGBHO zeFngrv>E~J^Z{qD@aK0RWnWd*cLF+1gkJ`lVT@8wX7rAL-8+QYETbC;-7dQQx9la#y+8(*pG1nr1iZtNR@s`5{G(<5SWK zIDc1(2QfMDl!lh>>cvIKk0`FtjfWH6Es#~v`5!P8qtVdjG)8T(dTnG5i-aLgT!n#0 zfRldx8NVS9$XOv8`BuW4pnNE&EqjEHe>qs#0Qhlmb0b<70V$(cGntbE&zjECc|%w_ zr2wkaMpn3!@0dwwBibqY8{AAcFX1M=XTPr>cHtnB6JSuK;s9$Q8}^&RKTiSrnV9wm zPyz4%Z?W}X)#AR(9eP>HvlUa$B6gUVL8DPMNKN_^ulB(2#X;?-=+e~v20Nn1^K9@0 zrYV-9umDRm^6a1}&;6>8X-%aWj$VW-yL+N~XS($cNlAQthZh`Q_vQ_eQ+T{H|Du|s zZN*LS@z8npTLGJtL6}fHpHgjN-oNwrbqGx2yXxNQyzqlcv${O+R5Wm7x0T0#4*8l(j literal 46515 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