From 5450bfdc6043d55ea1c9b95eadb69c34e0be309e Mon Sep 17 00:00:00 2001 From: ruv Date: Sat, 30 May 2026 12:54:15 -0400 Subject: [PATCH] =?UTF-8?q?feat(swarm):=20training=20visualizer=20?= =?UTF-8?q?=E2=80=94=20JSONL=20telemetry=20+=20self-contained=20HTML=20vie?= =?UTF-8?q?wer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an offline, dependency-free visualization for the drone training system: a top-down swarm replay synced with training-metric curves, fed by a JSONL telemetry log the trainer emits. No server, no build step, no CDN. ## Telemetry recorder (integration/telemetry.rs, always compiled, no new deps) - TelemetryRecorder writes newline-delimited JSON: one `meta` (profile, area, ground-truth victims), many `step` (per-tick drone x/y/heading/battery/detection + coverage%), and per-episode `episode` (mean_return, policy_loss, value_loss). - Written by hand (no serde_json) so it stays in the default build; 2 tests. ## train_marl telemetry flags - `--telemetry FILE` writes the log; `--telemetry-episode N` selects which episode's spatial steps to record (metrics recorded for all episodes). ## Visualizer (viz/swarm_viz.html — single file, vanilla JS + canvas) - LEFT: top-down replay — heading-oriented drone triangles (cyan/lime on detection), victim markers, growing coverage heatmap, detection pulse rings, play/pause/scrub/speed controls + live coverage/detection readout. - RIGHT: three autoscaled line charts (mean return, policy loss, value loss) over episodes, hand-drawn (no chart library). - Loads via file picker/drag-drop or auto-fetches the bundled sample; dark drone-ops theme; graceful degradation on file:// CORS. - viz/sample_telemetry.jsonl: real 30-episode / 4-drone / 400×400 m run (value_loss 20052→7154 — visible critic learning). Parses 1 meta / 60 step / 30 episode. ## Usage cargo run --release -p ruview-swarm --features train,cuda --bin train_marl -- \ --episodes 5000 --telemetry run.jsonl open v2/crates/ruview-swarm/viz/swarm_viz.html # load run.jsonl Tests unchanged (91 default / 96 train / 104 ruflo+itar); telemetry adds 2. Co-Authored-By: claude-flow --- v2/crates/ruview-swarm/src/bin/train_marl.rs | 69 +- v2/crates/ruview-swarm/src/integration/mod.rs | 2 + .../ruview-swarm/src/integration/telemetry.rs | 183 +++++ .../ruview-swarm/viz/sample_telemetry.jsonl | 91 +++ v2/crates/ruview-swarm/viz/swarm_viz.html | 725 ++++++++++++++++++ 5 files changed, 1069 insertions(+), 1 deletion(-) create mode 100644 v2/crates/ruview-swarm/src/integration/telemetry.rs create mode 100644 v2/crates/ruview-swarm/viz/sample_telemetry.jsonl create mode 100644 v2/crates/ruview-swarm/viz/swarm_viz.html diff --git a/v2/crates/ruview-swarm/src/bin/train_marl.rs b/v2/crates/ruview-swarm/src/bin/train_marl.rs index a51e2073..bc2d5b8c 100644 --- a/v2/crates/ruview-swarm/src/bin/train_marl.rs +++ b/v2/crates/ruview-swarm/src/bin/train_marl.rs @@ -12,6 +12,7 @@ //! 8× A100 box for this workload at ~1/20th the cost. See scripts/gcp/. use ruview_swarm::config::SwarmConfig; +use ruview_swarm::integration::telemetry::{DroneFrame, TelemetryRecorder}; use ruview_swarm::marl::candle_ppo::{CandlePpoConfig, CandleTrainer}; use ruview_swarm::marl::observation::LocalObservation; use ruview_swarm::marl::reward::{RewardCalculator, RewardContext}; @@ -25,6 +26,8 @@ struct Args { steps_per_episode: usize, checkpoint_dir: String, checkpoint_every: usize, + telemetry: Option, + telemetry_episode: usize, } impl Default for Args { @@ -36,6 +39,8 @@ impl Default for Args { steps_per_episode: 200, checkpoint_dir: "./marl-checkpoints".to_string(), checkpoint_every: 100, + telemetry: None, + telemetry_episode: 0, } } } @@ -71,6 +76,14 @@ fn parse_args() -> Args { args.checkpoint_every = next().parse().unwrap_or(args.checkpoint_every); i += 1; } + "--telemetry" => { + args.telemetry = Some(next()); + i += 1; + } + "--telemetry-episode" => { + args.telemetry_episode = next().parse().unwrap_or(args.telemetry_episode); + i += 1; + } "-h" | "--help" => { println!( "train_marl — ruview-swarm MARL training (ADR-148 M4)\n\ @@ -80,7 +93,9 @@ fn parse_args() -> Args { --profile NAME sar|inspection|mine|agriculture (default sar)\n \ --steps N steps per episode (default 200)\n \ --checkpoint-dir D checkpoint output dir (default ./marl-checkpoints)\n \ - --checkpoint-every N save every N episodes (default 100)" + --checkpoint-every N save every N episodes (default 100)\n \ + --telemetry FILE write JSONL telemetry for viz/swarm_viz.html\n \ + --telemetry-episode N which episode's steps to record spatially (default 0)" ); std::process::exit(0); } @@ -123,6 +138,23 @@ async fn main() -> Result<(), Box> { Position3D { x: cfg.mission.area_width_m * 0.6, y: cfg.mission.area_height_m * 0.45, z: 0.0 }, ]; + // Optional telemetry recorder for the visualizer. + let mut telem = match &args.telemetry { + Some(path) => { + let mut rec = TelemetryRecorder::create(path)?; + rec.meta( + &args.profile, + args.drones, + cfg.mission.area_width_m, + cfg.mission.area_height_m, + &victims, + )?; + println!("telemetry → {path} (spatial steps from episode {})", args.telemetry_episode); + Some(rec) + } + None => None, + }; + let mut best_return = f32::MIN; for episode in 0..args.episodes { @@ -204,6 +236,29 @@ async fn main() -> Result<(), Box> { value_buf.push(0.0); // bootstrap value (critic learns this) done_buf.push(is_last); } + + // Record spatial telemetry for the selected episode only (keeps the + // log small — one episode of step frames + metrics for every episode). + if let Some(rec) = telem.as_mut() { + if episode == args.telemetry_episode { + let scan_w = cfg.planning.csi_scan_width_m; + let frames: Vec = drones + .iter() + .map(|d| { + let detected = victims + .iter() + .any(|v| d.state.position.distance_to(v) < scan_w); + DroneFrame::from_state(&d.state, detected) + }) + .collect(); + let coverage = drones + .iter() + .map(|d| d.probability_grid.coverage_pct()) + .sum::() + / drones.len().max(1) as f64; + let _ = rec.step(episode, step, step as f64, &frames, coverage); + } + } } // PPO update on the episode's rollout. @@ -223,6 +278,11 @@ async fn main() -> Result<(), Box> { best_return = mean_return; } + // Per-episode training-metric telemetry (every episode). + if let Some(rec) = telem.as_mut() { + let _ = rec.episode(episode, mean_return, policy_loss, value_loss, 0); + } + if episode % 10 == 0 || episode == args.episodes - 1 { println!( "ep {:>5}/{} mean_return={:>8.3} best={:>8.3} policy_loss={:>8.4} value_loss={:>8.4}", @@ -244,6 +304,13 @@ async fn main() -> Result<(), Box> { } } + if let Some(rec) = telem.as_mut() { + rec.flush()?; + if let Some(path) = &args.telemetry { + println!("telemetry written: {path} — open viz/swarm_viz.html and load it"); + } + } + println!("training complete. best mean_return={best_return:.3}"); Ok(()) } diff --git a/v2/crates/ruview-swarm/src/integration/mod.rs b/v2/crates/ruview-swarm/src/integration/mod.rs index 1462de7e..87f8b94f 100644 --- a/v2/crates/ruview-swarm/src/integration/mod.rs +++ b/v2/crates/ruview-swarm/src/integration/mod.rs @@ -3,8 +3,10 @@ pub mod mavlink_messages; pub mod mission_report; pub mod swarm_sim; +pub mod telemetry; pub use mission_report::{MissionReport, SotaComparison, VictimReport}; +pub use telemetry::{DroneFrame, TelemetryRecorder}; pub use mavlink_messages::{ SwarmNodeState, SwarmCsiReport, SwarmClusterHeartbeat, SwarmVictimConfirmed, SwarmMsgId, diff --git a/v2/crates/ruview-swarm/src/integration/telemetry.rs b/v2/crates/ruview-swarm/src/integration/telemetry.rs new file mode 100644 index 00000000..9eaa16e8 --- /dev/null +++ b/v2/crates/ruview-swarm/src/integration/telemetry.rs @@ -0,0 +1,183 @@ +//! JSONL telemetry recorder for the swarm training/sim visualizer. +//! +//! Emits newline-delimited JSON records consumed by `viz/swarm_viz.html`: +//! - one `meta` record (mission profile, area, ground-truth victims) +//! - many `step` records (per-tick drone positions, coverage, detections) +//! - optional `episode` records (per-episode training metrics) +//! +//! Written by hand (no serde_json dependency) so it stays in the default build +//! and never affects the test/CI surface. The schema is flat and the only +//! string fields are developer-controlled identifiers, so manual encoding is safe. + +use crate::types::{DroneState, Position3D}; +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::path::Path; + +/// Records swarm telemetry to a JSONL file for offline visualization. +pub struct TelemetryRecorder { + writer: BufWriter, +} + +/// One drone's per-step visual state. +pub struct DroneFrame { + pub id: u32, + pub x: f64, + pub y: f64, + pub heading_rad: f64, + pub battery_pct: f32, + pub detected: bool, +} + +impl DroneFrame { + pub fn from_state(state: &DroneState, detected: bool) -> Self { + Self { + id: state.id.0, + x: state.position.x, + y: state.position.y, + heading_rad: state.heading_rad, + battery_pct: state.battery_pct, + detected, + } + } +} + +impl TelemetryRecorder { + /// Open a telemetry file for writing. + pub fn create>(path: P) -> std::io::Result { + let file = File::create(path)?; + Ok(Self { writer: BufWriter::new(file) }) + } + + /// Write the one-time mission metadata header. + pub fn meta( + &mut self, + profile: &str, + drones: usize, + area_w: f64, + area_h: f64, + victims: &[Position3D], + ) -> std::io::Result<()> { + let vics: Vec = victims + .iter() + .map(|v| format!("[{:.2},{:.2}]", v.x, v.y)) + .collect(); + writeln!( + self.writer, + r#"{{"type":"meta","profile":"{}","drones":{},"area_w":{:.2},"area_h":{:.2},"victims":[{}]}}"#, + sanitize(profile), + drones, + area_w, + area_h, + vics.join(",") + ) + } + + /// Write one simulation step (all drones at this tick). + pub fn step( + &mut self, + episode: usize, + step: usize, + t_secs: f64, + drones: &[DroneFrame], + coverage_pct: f64, + ) -> std::io::Result<()> { + let ds: Vec = drones + .iter() + .map(|d| { + format!( + r#"{{"id":{},"x":{:.2},"y":{:.2},"hdg":{:.3},"batt":{:.1},"det":{}}}"#, + d.id, d.x, d.y, d.heading_rad, d.battery_pct, d.detected + ) + }) + .collect(); + writeln!( + self.writer, + r#"{{"type":"step","ep":{},"step":{},"t":{:.2},"coverage":{:.4},"drones":[{}]}}"#, + episode, + step, + t_secs, + coverage_pct, + ds.join(",") + ) + } + + /// Write one episode's training metrics. + pub fn episode( + &mut self, + episode: usize, + mean_return: f32, + policy_loss: f32, + value_loss: f32, + victims_found: usize, + ) -> std::io::Result<()> { + writeln!( + self.writer, + r#"{{"type":"episode","ep":{},"mean_return":{:.4},"policy_loss":{:.4},"value_loss":{:.4},"victims_found":{}}}"#, + episode, mean_return, policy_loss, value_loss, victims_found + ) + } + + /// Flush buffered records to disk. + pub fn flush(&mut self) -> std::io::Result<()> { + self.writer.flush() + } +} + +/// Strip characters that would break the flat JSON string field. +fn sanitize(s: &str) -> String { + s.chars().filter(|c| *c != '"' && *c != '\\' && *c != '\n').collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{NodeId, Velocity3D}; + + fn tmp_path(name: &str) -> std::path::PathBuf { + std::env::temp_dir().join(name) + } + + #[test] + fn test_records_valid_jsonl() { + let path = tmp_path("ruview_telemetry_test.jsonl"); + { + let mut rec = TelemetryRecorder::create(&path).unwrap(); + rec.meta("sar", 2, 400.0, 400.0, &[Position3D { x: 80.0, y: 120.0, z: 0.0 }]) + .unwrap(); + let state = DroneState { + id: NodeId(0), + position: Position3D { x: 10.5, y: 20.25, z: -30.0 }, + velocity: Velocity3D::default(), + heading_rad: 1.57, + altitude_agl_m: 30.0, + battery_pct: 88.0, + link_quality: 0.9, + timestamp_ms: 0, + }; + rec.step(0, 0, 0.0, &[DroneFrame::from_state(&state, true)], 0.05) + .unwrap(); + rec.episode(0, 103.7, -61.2, 12643.3, 1).unwrap(); + rec.flush().unwrap(); + } + let content = std::fs::read_to_string(&path).unwrap(); + let lines: Vec<&str> = content.lines().collect(); + assert_eq!(lines.len(), 3, "meta + step + episode = 3 records"); + assert!(lines[0].contains(r#""type":"meta""#)); + assert!(lines[1].contains(r#""type":"step""#)); + assert!(lines[1].contains(r#""det":true"#)); + assert!(lines[2].contains(r#""type":"episode""#)); + // Each line is balanced JSON (braces match) + for line in &lines { + let opens = line.matches('{').count(); + let closes = line.matches('}').count(); + assert_eq!(opens, closes, "balanced braces in: {line}"); + } + std::fs::remove_file(&path).ok(); + } + + #[test] + fn test_sanitize_strips_quotes() { + assert_eq!(sanitize("sa\"r\n"), "sar"); + } +} diff --git a/v2/crates/ruview-swarm/viz/sample_telemetry.jsonl b/v2/crates/ruview-swarm/viz/sample_telemetry.jsonl new file mode 100644 index 00000000..b3cebdf3 --- /dev/null +++ b/v2/crates/ruview-swarm/viz/sample_telemetry.jsonl @@ -0,0 +1,91 @@ +{"type":"meta","profile":"sar","drones":4,"area_w":400.00,"area_h":400.00,"victims":[[80.00,120.00],[240.00,180.00]]} +{"type":"episode","ep":0,"mean_return":123.6877,"policy_loss":4.3082,"value_loss":20052.5762,"victims_found":0} +{"type":"episode","ep":1,"mean_return":123.6877,"policy_loss":-35.5269,"value_loss":19947.0527,"victims_found":0} +{"type":"episode","ep":2,"mean_return":123.6877,"policy_loss":-74.4405,"value_loss":19841.1973,"victims_found":0} +{"type":"episode","ep":3,"mean_return":123.6877,"policy_loss":-116.1118,"value_loss":19727.8633,"victims_found":0} +{"type":"episode","ep":4,"mean_return":123.6877,"policy_loss":-162.6557,"value_loss":19607.3828,"victims_found":0} +{"type":"episode","ep":5,"mean_return":123.6877,"policy_loss":-215.9290,"value_loss":19478.0371,"victims_found":0} +{"type":"episode","ep":6,"mean_return":123.6877,"policy_loss":-279.5587,"value_loss":19331.7852,"victims_found":0} +{"type":"episode","ep":7,"mean_return":123.6877,"policy_loss":-357.7819,"value_loss":19170.1621,"victims_found":0} +{"type":"episode","ep":8,"mean_return":123.6877,"policy_loss":-452.8956,"value_loss":18987.3477,"victims_found":0} +{"type":"episode","ep":9,"mean_return":123.6877,"policy_loss":-565.1311,"value_loss":18776.0879,"victims_found":0} +{"type":"episode","ep":10,"mean_return":123.6877,"policy_loss":-693.7755,"value_loss":18532.1562,"victims_found":0} +{"type":"episode","ep":11,"mean_return":123.6877,"policy_loss":-840.3882,"value_loss":18255.1113,"victims_found":0} +{"type":"episode","ep":12,"mean_return":123.6877,"policy_loss":-1007.8695,"value_loss":17941.9434,"victims_found":0} +{"type":"episode","ep":13,"mean_return":123.6877,"policy_loss":-1198.8174,"value_loss":17589.8066,"victims_found":0} +{"type":"episode","ep":14,"mean_return":123.6877,"policy_loss":-1415.8530,"value_loss":17194.4316,"victims_found":0} +{"type":"episode","ep":15,"mean_return":123.6877,"policy_loss":-1662.0295,"value_loss":16753.6250,"victims_found":0} +{"type":"episode","ep":16,"mean_return":123.6877,"policy_loss":-1940.0228,"value_loss":16267.0869,"victims_found":0} +{"type":"episode","ep":17,"mean_return":123.6877,"policy_loss":-2252.8604,"value_loss":15734.1992,"victims_found":0} +{"type":"episode","ep":18,"mean_return":123.6877,"policy_loss":-2603.2175,"value_loss":15156.0713,"victims_found":0} +{"type":"episode","ep":19,"mean_return":123.6877,"policy_loss":-2993.5083,"value_loss":14534.6807,"victims_found":0} +{"type":"episode","ep":20,"mean_return":123.6877,"policy_loss":-3427.2791,"value_loss":13871.4297,"victims_found":0} +{"type":"episode","ep":21,"mean_return":123.6877,"policy_loss":-3908.3892,"value_loss":13167.9365,"victims_found":0} +{"type":"episode","ep":22,"mean_return":123.6877,"policy_loss":-4439.9995,"value_loss":12428.7100,"victims_found":0} +{"type":"episode","ep":23,"mean_return":123.6877,"policy_loss":-5023.8579,"value_loss":11662.3135,"victims_found":0} +{"type":"episode","ep":24,"mean_return":123.6877,"policy_loss":-5660.2412,"value_loss":10880.2861,"victims_found":0} +{"type":"episode","ep":25,"mean_return":123.6877,"policy_loss":-6351.8081,"value_loss":10091.8340,"victims_found":0} +{"type":"episode","ep":26,"mean_return":123.6877,"policy_loss":-7099.0146,"value_loss":9310.4336,"victims_found":0} +{"type":"episode","ep":27,"mean_return":123.6877,"policy_loss":-7901.1260,"value_loss":8551.1855,"victims_found":0} +{"type":"episode","ep":28,"mean_return":123.6877,"policy_loss":-8758.6396,"value_loss":7828.0469,"victims_found":0} +{"type":"step","ep":29,"step":0,"t":0.00,"coverage":0.0002,"drones":[{"id":0,"x":4.34,"y":4.34,"hdg":-2.356,"batt":100.0,"det":false},{"id":1,"x":202.01,"y":9.71,"hdg":-3.105,"batt":100.0,"det":false},{"id":2,"x":9.71,"y":202.01,"hdg":-1.607,"batt":100.0,"det":false},{"id":3,"x":204.34,"y":204.34,"hdg":-2.356,"batt":100.0,"det":false}]} +{"type":"step","ep":29,"step":1,"t":1.00,"coverage":0.0003,"drones":[{"id":0,"x":7.50,"y":2.50,"hdg":-0.528,"batt":99.9,"det":false},{"id":1,"x":194.01,"y":9.42,"hdg":-3.105,"batt":99.9,"det":false},{"id":2,"x":9.42,"y":194.01,"hdg":-1.607,"batt":99.9,"det":false},{"id":3,"x":198.69,"y":198.69,"hdg":-2.356,"batt":99.9,"det":false}]} +{"type":"step","ep":29,"step":2,"t":2.00,"coverage":0.0005,"drones":[{"id":0,"x":12.50,"y":2.50,"hdg":0.000,"batt":99.9,"det":false},{"id":1,"x":186.02,"y":9.13,"hdg":-3.105,"batt":99.9,"det":false},{"id":2,"x":9.13,"y":186.02,"hdg":-1.607,"batt":99.9,"det":false},{"id":3,"x":193.03,"y":193.03,"hdg":-2.356,"batt":99.9,"det":false}]} +{"type":"step","ep":29,"step":3,"t":3.00,"coverage":0.0006,"drones":[{"id":0,"x":17.50,"y":2.50,"hdg":0.000,"batt":99.9,"det":false},{"id":1,"x":178.02,"y":8.84,"hdg":-3.105,"batt":99.9,"det":false},{"id":2,"x":8.84,"y":178.02,"hdg":-1.607,"batt":99.9,"det":false},{"id":3,"x":187.37,"y":187.37,"hdg":-2.356,"batt":99.9,"det":false}]} +{"type":"step","ep":29,"step":4,"t":4.00,"coverage":0.0008,"drones":[{"id":0,"x":22.50,"y":2.50,"hdg":0.000,"batt":99.8,"det":false},{"id":1,"x":170.03,"y":8.56,"hdg":-3.105,"batt":99.8,"det":false},{"id":2,"x":8.56,"y":170.03,"hdg":-1.607,"batt":99.8,"det":false},{"id":3,"x":181.72,"y":181.72,"hdg":-2.356,"batt":99.8,"det":false}]} +{"type":"step","ep":29,"step":5,"t":5.00,"coverage":0.0009,"drones":[{"id":0,"x":27.50,"y":2.50,"hdg":0.000,"batt":99.8,"det":false},{"id":1,"x":162.03,"y":8.27,"hdg":-3.105,"batt":99.8,"det":false},{"id":2,"x":8.27,"y":162.03,"hdg":-1.607,"batt":99.8,"det":false},{"id":3,"x":176.06,"y":176.06,"hdg":-2.356,"batt":99.8,"det":false}]} +{"type":"step","ep":29,"step":6,"t":6.00,"coverage":0.0011,"drones":[{"id":0,"x":32.50,"y":2.50,"hdg":0.000,"batt":99.8,"det":false},{"id":1,"x":154.04,"y":7.98,"hdg":-3.105,"batt":99.8,"det":false},{"id":2,"x":7.98,"y":154.04,"hdg":-1.607,"batt":99.8,"det":false},{"id":3,"x":170.40,"y":170.40,"hdg":-2.356,"batt":99.8,"det":false}]} +{"type":"step","ep":29,"step":7,"t":7.00,"coverage":0.0013,"drones":[{"id":0,"x":37.50,"y":2.50,"hdg":0.000,"batt":99.7,"det":false},{"id":1,"x":146.04,"y":7.69,"hdg":-3.105,"batt":99.7,"det":false},{"id":2,"x":7.69,"y":146.04,"hdg":-1.607,"batt":99.7,"det":false},{"id":3,"x":164.75,"y":164.75,"hdg":-2.356,"batt":99.7,"det":false}]} +{"type":"step","ep":29,"step":8,"t":8.00,"coverage":0.0014,"drones":[{"id":0,"x":42.50,"y":2.50,"hdg":0.000,"batt":99.7,"det":false},{"id":1,"x":138.05,"y":7.40,"hdg":-3.105,"batt":99.7,"det":false},{"id":2,"x":7.40,"y":138.05,"hdg":-1.607,"batt":99.7,"det":false},{"id":3,"x":159.09,"y":159.09,"hdg":-2.356,"batt":99.7,"det":false}]} +{"type":"step","ep":29,"step":9,"t":9.00,"coverage":0.0016,"drones":[{"id":0,"x":47.50,"y":2.50,"hdg":0.000,"batt":99.7,"det":false},{"id":1,"x":130.05,"y":7.11,"hdg":-3.105,"batt":99.7,"det":false},{"id":2,"x":7.11,"y":130.05,"hdg":-1.607,"batt":99.7,"det":false},{"id":3,"x":153.43,"y":153.43,"hdg":-2.356,"batt":99.7,"det":false}]} +{"type":"step","ep":29,"step":10,"t":10.00,"coverage":0.0017,"drones":[{"id":0,"x":52.50,"y":2.50,"hdg":0.000,"batt":99.6,"det":false},{"id":1,"x":122.06,"y":6.82,"hdg":-3.105,"batt":99.6,"det":false},{"id":2,"x":6.82,"y":122.06,"hdg":-1.607,"batt":99.6,"det":false},{"id":3,"x":147.77,"y":147.77,"hdg":-2.356,"batt":99.6,"det":false}]} +{"type":"step","ep":29,"step":11,"t":11.00,"coverage":0.0019,"drones":[{"id":0,"x":57.50,"y":2.50,"hdg":0.000,"batt":99.6,"det":false},{"id":1,"x":114.06,"y":6.53,"hdg":-3.105,"batt":99.6,"det":false},{"id":2,"x":6.53,"y":114.06,"hdg":-1.607,"batt":99.6,"det":false},{"id":3,"x":142.12,"y":142.12,"hdg":-2.356,"batt":99.6,"det":false}]} +{"type":"step","ep":29,"step":12,"t":12.00,"coverage":0.0020,"drones":[{"id":0,"x":62.50,"y":2.50,"hdg":0.000,"batt":99.6,"det":false},{"id":1,"x":106.07,"y":6.24,"hdg":-3.105,"batt":99.6,"det":false},{"id":2,"x":6.24,"y":106.07,"hdg":-1.607,"batt":99.6,"det":false},{"id":3,"x":136.46,"y":136.46,"hdg":-2.356,"batt":99.6,"det":false}]} +{"type":"step","ep":29,"step":13,"t":13.00,"coverage":0.0022,"drones":[{"id":0,"x":67.50,"y":2.50,"hdg":0.000,"batt":99.5,"det":false},{"id":1,"x":98.07,"y":5.95,"hdg":-3.105,"batt":99.5,"det":false},{"id":2,"x":5.95,"y":98.07,"hdg":-1.607,"batt":99.5,"det":false},{"id":3,"x":130.80,"y":130.80,"hdg":-2.356,"batt":99.5,"det":false}]} +{"type":"step","ep":29,"step":14,"t":14.00,"coverage":0.0023,"drones":[{"id":0,"x":72.50,"y":2.50,"hdg":0.000,"batt":99.5,"det":false},{"id":1,"x":90.08,"y":5.67,"hdg":-3.105,"batt":99.5,"det":false},{"id":2,"x":5.67,"y":90.08,"hdg":-1.607,"batt":99.5,"det":false},{"id":3,"x":125.15,"y":125.15,"hdg":-2.356,"batt":99.5,"det":false}]} +{"type":"step","ep":29,"step":15,"t":15.00,"coverage":0.0025,"drones":[{"id":0,"x":77.50,"y":2.50,"hdg":0.000,"batt":99.5,"det":false},{"id":1,"x":82.08,"y":5.38,"hdg":-3.105,"batt":99.5,"det":false},{"id":2,"x":5.38,"y":82.08,"hdg":-1.607,"batt":99.5,"det":false},{"id":3,"x":119.49,"y":119.49,"hdg":-2.356,"batt":99.5,"det":false}]} +{"type":"step","ep":29,"step":16,"t":16.00,"coverage":0.0027,"drones":[{"id":0,"x":82.50,"y":2.50,"hdg":0.000,"batt":99.4,"det":false},{"id":1,"x":74.09,"y":5.09,"hdg":-3.105,"batt":99.4,"det":false},{"id":2,"x":5.09,"y":74.09,"hdg":-1.607,"batt":99.4,"det":false},{"id":3,"x":113.83,"y":113.83,"hdg":-2.356,"batt":99.4,"det":false}]} +{"type":"step","ep":29,"step":17,"t":17.00,"coverage":0.0028,"drones":[{"id":0,"x":87.50,"y":2.50,"hdg":0.000,"batt":99.4,"det":false},{"id":1,"x":66.09,"y":4.80,"hdg":-3.105,"batt":99.4,"det":false},{"id":2,"x":4.80,"y":66.09,"hdg":-1.607,"batt":99.4,"det":false},{"id":3,"x":108.18,"y":108.18,"hdg":-2.356,"batt":99.4,"det":false}]} +{"type":"step","ep":29,"step":18,"t":18.00,"coverage":0.0030,"drones":[{"id":0,"x":92.50,"y":2.50,"hdg":0.000,"batt":99.4,"det":false},{"id":1,"x":58.10,"y":4.51,"hdg":-3.105,"batt":99.4,"det":false},{"id":2,"x":4.51,"y":58.10,"hdg":-1.607,"batt":99.4,"det":false},{"id":3,"x":102.52,"y":102.52,"hdg":-2.356,"batt":99.4,"det":false}]} +{"type":"step","ep":29,"step":19,"t":19.00,"coverage":0.0031,"drones":[{"id":0,"x":97.50,"y":2.50,"hdg":0.000,"batt":99.3,"det":false},{"id":1,"x":50.10,"y":4.22,"hdg":-3.105,"batt":99.3,"det":false},{"id":2,"x":4.22,"y":50.10,"hdg":-1.607,"batt":99.3,"det":false},{"id":3,"x":96.86,"y":96.86,"hdg":-2.356,"batt":99.3,"det":false}]} +{"type":"step","ep":29,"step":20,"t":20.00,"coverage":0.0033,"drones":[{"id":0,"x":102.50,"y":2.50,"hdg":0.000,"batt":99.3,"det":false},{"id":1,"x":42.11,"y":3.93,"hdg":-3.105,"batt":99.3,"det":false},{"id":2,"x":3.93,"y":42.11,"hdg":-1.607,"batt":99.3,"det":false},{"id":3,"x":91.21,"y":91.21,"hdg":-2.356,"batt":99.3,"det":false}]} +{"type":"step","ep":29,"step":21,"t":21.00,"coverage":0.0034,"drones":[{"id":0,"x":107.50,"y":2.50,"hdg":0.000,"batt":99.3,"det":false},{"id":1,"x":34.11,"y":3.64,"hdg":-3.105,"batt":99.3,"det":false},{"id":2,"x":3.64,"y":34.11,"hdg":-1.607,"batt":99.3,"det":false},{"id":3,"x":85.55,"y":85.55,"hdg":-2.356,"batt":99.3,"det":false}]} +{"type":"step","ep":29,"step":22,"t":22.00,"coverage":0.0036,"drones":[{"id":0,"x":112.50,"y":2.50,"hdg":0.000,"batt":99.2,"det":false},{"id":1,"x":26.12,"y":3.35,"hdg":-3.105,"batt":99.2,"det":false},{"id":2,"x":3.35,"y":26.12,"hdg":-1.607,"batt":99.2,"det":false},{"id":3,"x":79.89,"y":79.89,"hdg":-2.356,"batt":99.2,"det":false}]} +{"type":"step","ep":29,"step":23,"t":23.00,"coverage":0.0037,"drones":[{"id":0,"x":117.50,"y":2.50,"hdg":0.000,"batt":99.2,"det":false},{"id":1,"x":18.13,"y":3.06,"hdg":-3.105,"batt":99.2,"det":false},{"id":2,"x":3.06,"y":18.13,"hdg":-1.607,"batt":99.2,"det":false},{"id":3,"x":74.24,"y":74.24,"hdg":-2.356,"batt":99.2,"det":false}]} +{"type":"step","ep":29,"step":24,"t":24.00,"coverage":0.0039,"drones":[{"id":0,"x":122.50,"y":2.50,"hdg":0.000,"batt":99.2,"det":false},{"id":1,"x":10.13,"y":2.78,"hdg":-3.105,"batt":99.2,"det":false},{"id":2,"x":2.78,"y":10.13,"hdg":-1.607,"batt":99.2,"det":false},{"id":3,"x":68.58,"y":68.58,"hdg":-2.356,"batt":99.2,"det":false}]} +{"type":"step","ep":29,"step":25,"t":25.00,"coverage":0.0041,"drones":[{"id":0,"x":127.50,"y":2.50,"hdg":0.000,"batt":99.1,"det":false},{"id":1,"x":2.50,"y":2.50,"hdg":-3.105,"batt":99.1,"det":false},{"id":2,"x":2.50,"y":2.50,"hdg":-1.607,"batt":99.1,"det":false},{"id":3,"x":62.92,"y":62.92,"hdg":-2.356,"batt":99.1,"det":false}]} +{"type":"step","ep":29,"step":26,"t":26.00,"coverage":0.0042,"drones":[{"id":0,"x":132.50,"y":2.50,"hdg":0.000,"batt":99.1,"det":false},{"id":1,"x":7.50,"y":2.50,"hdg":0.000,"batt":99.1,"det":false},{"id":2,"x":7.50,"y":2.50,"hdg":0.000,"batt":99.1,"det":false},{"id":3,"x":57.26,"y":57.26,"hdg":-2.356,"batt":99.1,"det":false}]} +{"type":"step","ep":29,"step":27,"t":27.00,"coverage":0.0044,"drones":[{"id":0,"x":137.50,"y":2.50,"hdg":0.000,"batt":99.1,"det":false},{"id":1,"x":15.50,"y":2.50,"hdg":0.000,"batt":99.1,"det":false},{"id":2,"x":12.50,"y":2.50,"hdg":0.000,"batt":99.1,"det":false},{"id":3,"x":51.61,"y":51.61,"hdg":-2.356,"batt":99.1,"det":false}]} +{"type":"step","ep":29,"step":28,"t":28.00,"coverage":0.0045,"drones":[{"id":0,"x":142.50,"y":2.50,"hdg":0.000,"batt":99.0,"det":false},{"id":1,"x":22.50,"y":2.50,"hdg":0.000,"batt":99.0,"det":false},{"id":2,"x":17.50,"y":2.50,"hdg":0.000,"batt":99.0,"det":false},{"id":3,"x":45.95,"y":45.95,"hdg":-2.356,"batt":99.0,"det":false}]} +{"type":"step","ep":29,"step":29,"t":29.00,"coverage":0.0046,"drones":[{"id":0,"x":147.50,"y":2.50,"hdg":0.000,"batt":99.0,"det":false},{"id":1,"x":30.50,"y":2.50,"hdg":0.000,"batt":99.0,"det":false},{"id":2,"x":22.50,"y":2.50,"hdg":0.000,"batt":99.0,"det":false},{"id":3,"x":40.29,"y":40.29,"hdg":-2.356,"batt":99.0,"det":false}]} +{"type":"step","ep":29,"step":30,"t":30.00,"coverage":0.0048,"drones":[{"id":0,"x":152.50,"y":2.50,"hdg":0.000,"batt":99.0,"det":false},{"id":1,"x":37.50,"y":2.50,"hdg":0.000,"batt":99.0,"det":false},{"id":2,"x":27.50,"y":2.50,"hdg":0.000,"batt":99.0,"det":false},{"id":3,"x":34.64,"y":34.64,"hdg":-2.356,"batt":99.0,"det":false}]} +{"type":"step","ep":29,"step":31,"t":31.00,"coverage":0.0049,"drones":[{"id":0,"x":157.50,"y":2.50,"hdg":0.000,"batt":98.9,"det":false},{"id":1,"x":45.50,"y":2.50,"hdg":0.000,"batt":98.9,"det":false},{"id":2,"x":32.50,"y":2.50,"hdg":0.000,"batt":98.9,"det":false},{"id":3,"x":28.98,"y":28.98,"hdg":-2.356,"batt":98.9,"det":false}]} +{"type":"step","ep":29,"step":32,"t":32.00,"coverage":0.0051,"drones":[{"id":0,"x":162.50,"y":2.50,"hdg":0.000,"batt":98.9,"det":false},{"id":1,"x":53.50,"y":2.50,"hdg":0.000,"batt":98.9,"det":false},{"id":2,"x":37.50,"y":2.50,"hdg":0.000,"batt":98.9,"det":false},{"id":3,"x":23.32,"y":23.32,"hdg":-2.356,"batt":98.9,"det":false}]} +{"type":"step","ep":29,"step":33,"t":33.00,"coverage":0.0052,"drones":[{"id":0,"x":167.50,"y":2.50,"hdg":0.000,"batt":98.9,"det":false},{"id":1,"x":61.50,"y":2.50,"hdg":0.000,"batt":98.9,"det":false},{"id":2,"x":42.50,"y":2.50,"hdg":0.000,"batt":98.9,"det":false},{"id":3,"x":17.67,"y":17.67,"hdg":-2.356,"batt":98.9,"det":false}]} +{"type":"step","ep":29,"step":34,"t":34.00,"coverage":0.0054,"drones":[{"id":0,"x":172.50,"y":2.50,"hdg":0.000,"batt":98.8,"det":false},{"id":1,"x":69.50,"y":2.50,"hdg":0.000,"batt":98.8,"det":false},{"id":2,"x":47.50,"y":2.50,"hdg":0.000,"batt":98.8,"det":false},{"id":3,"x":12.01,"y":12.01,"hdg":-2.356,"batt":98.8,"det":false}]} +{"type":"step","ep":29,"step":35,"t":35.00,"coverage":0.0055,"drones":[{"id":0,"x":177.50,"y":2.50,"hdg":0.000,"batt":98.8,"det":false},{"id":1,"x":72.50,"y":2.50,"hdg":0.000,"batt":98.8,"det":false},{"id":2,"x":52.50,"y":2.50,"hdg":0.000,"batt":98.8,"det":false},{"id":3,"x":6.35,"y":6.35,"hdg":-2.356,"batt":98.8,"det":false}]} +{"type":"step","ep":29,"step":36,"t":36.00,"coverage":0.0056,"drones":[{"id":0,"x":182.50,"y":2.50,"hdg":0.000,"batt":98.8,"det":false},{"id":1,"x":77.50,"y":2.50,"hdg":0.000,"batt":98.8,"det":false},{"id":2,"x":57.50,"y":2.50,"hdg":0.000,"batt":98.8,"det":false},{"id":3,"x":2.50,"y":2.50,"hdg":-2.356,"batt":98.8,"det":false}]} +{"type":"step","ep":29,"step":37,"t":37.00,"coverage":0.0058,"drones":[{"id":0,"x":187.50,"y":2.50,"hdg":0.000,"batt":98.7,"det":false},{"id":1,"x":82.50,"y":2.50,"hdg":0.000,"batt":98.7,"det":false},{"id":2,"x":62.50,"y":2.50,"hdg":0.000,"batt":98.7,"det":false},{"id":3,"x":7.50,"y":2.50,"hdg":0.000,"batt":98.7,"det":false}]} +{"type":"step","ep":29,"step":38,"t":38.00,"coverage":0.0059,"drones":[{"id":0,"x":192.50,"y":2.50,"hdg":0.000,"batt":98.7,"det":false},{"id":1,"x":87.50,"y":2.50,"hdg":0.000,"batt":98.7,"det":false},{"id":2,"x":67.50,"y":2.50,"hdg":0.000,"batt":98.7,"det":false},{"id":3,"x":12.50,"y":2.50,"hdg":0.000,"batt":98.7,"det":false}]} +{"type":"step","ep":29,"step":39,"t":39.00,"coverage":0.0061,"drones":[{"id":0,"x":197.50,"y":2.50,"hdg":0.000,"batt":98.7,"det":false},{"id":1,"x":92.50,"y":2.50,"hdg":0.000,"batt":98.7,"det":false},{"id":2,"x":72.50,"y":2.50,"hdg":0.000,"batt":98.7,"det":false},{"id":3,"x":17.50,"y":2.50,"hdg":0.000,"batt":98.7,"det":false}]} +{"type":"step","ep":29,"step":40,"t":40.00,"coverage":0.0062,"drones":[{"id":0,"x":202.50,"y":2.50,"hdg":0.000,"batt":98.6,"det":false},{"id":1,"x":97.50,"y":2.50,"hdg":0.000,"batt":98.6,"det":false},{"id":2,"x":77.50,"y":2.50,"hdg":0.000,"batt":98.6,"det":false},{"id":3,"x":22.50,"y":2.50,"hdg":0.000,"batt":98.6,"det":false}]} +{"type":"step","ep":29,"step":41,"t":41.00,"coverage":0.0064,"drones":[{"id":0,"x":207.50,"y":2.50,"hdg":0.000,"batt":98.6,"det":false},{"id":1,"x":102.50,"y":2.50,"hdg":0.000,"batt":98.6,"det":false},{"id":2,"x":82.50,"y":2.50,"hdg":0.000,"batt":98.6,"det":false},{"id":3,"x":27.50,"y":2.50,"hdg":0.000,"batt":98.6,"det":false}]} +{"type":"step","ep":29,"step":42,"t":42.00,"coverage":0.0066,"drones":[{"id":0,"x":212.50,"y":2.50,"hdg":0.000,"batt":98.6,"det":false},{"id":1,"x":107.50,"y":2.50,"hdg":0.000,"batt":98.6,"det":false},{"id":2,"x":87.50,"y":2.50,"hdg":0.000,"batt":98.6,"det":false},{"id":3,"x":32.50,"y":2.50,"hdg":0.000,"batt":98.6,"det":false}]} +{"type":"step","ep":29,"step":43,"t":43.00,"coverage":0.0067,"drones":[{"id":0,"x":217.50,"y":2.50,"hdg":0.000,"batt":98.5,"det":false},{"id":1,"x":112.50,"y":2.50,"hdg":0.000,"batt":98.5,"det":false},{"id":2,"x":92.50,"y":2.50,"hdg":0.000,"batt":98.5,"det":false},{"id":3,"x":37.50,"y":2.50,"hdg":0.000,"batt":98.5,"det":false}]} +{"type":"step","ep":29,"step":44,"t":44.00,"coverage":0.0069,"drones":[{"id":0,"x":222.50,"y":2.50,"hdg":0.000,"batt":98.5,"det":false},{"id":1,"x":117.50,"y":2.50,"hdg":0.000,"batt":98.5,"det":false},{"id":2,"x":97.50,"y":2.50,"hdg":0.000,"batt":98.5,"det":false},{"id":3,"x":42.50,"y":2.50,"hdg":0.000,"batt":98.5,"det":false}]} +{"type":"step","ep":29,"step":45,"t":45.00,"coverage":0.0070,"drones":[{"id":0,"x":227.50,"y":2.50,"hdg":0.000,"batt":98.5,"det":false},{"id":1,"x":122.50,"y":2.50,"hdg":0.000,"batt":98.5,"det":false},{"id":2,"x":102.50,"y":2.50,"hdg":0.000,"batt":98.5,"det":false},{"id":3,"x":47.50,"y":2.50,"hdg":0.000,"batt":98.5,"det":false}]} +{"type":"step","ep":29,"step":46,"t":46.00,"coverage":0.0072,"drones":[{"id":0,"x":232.50,"y":2.50,"hdg":0.000,"batt":98.4,"det":false},{"id":1,"x":127.50,"y":2.50,"hdg":0.000,"batt":98.4,"det":false},{"id":2,"x":107.50,"y":2.50,"hdg":0.000,"batt":98.4,"det":false},{"id":3,"x":52.50,"y":2.50,"hdg":0.000,"batt":98.4,"det":false}]} +{"type":"step","ep":29,"step":47,"t":47.00,"coverage":0.0073,"drones":[{"id":0,"x":237.50,"y":2.50,"hdg":0.000,"batt":98.4,"det":false},{"id":1,"x":132.50,"y":2.50,"hdg":0.000,"batt":98.4,"det":false},{"id":2,"x":112.50,"y":2.50,"hdg":0.000,"batt":98.4,"det":false},{"id":3,"x":57.50,"y":2.50,"hdg":0.000,"batt":98.4,"det":false}]} +{"type":"step","ep":29,"step":48,"t":48.00,"coverage":0.0075,"drones":[{"id":0,"x":242.50,"y":2.50,"hdg":0.000,"batt":98.4,"det":false},{"id":1,"x":137.50,"y":2.50,"hdg":0.000,"batt":98.4,"det":false},{"id":2,"x":117.50,"y":2.50,"hdg":0.000,"batt":98.4,"det":false},{"id":3,"x":62.50,"y":2.50,"hdg":0.000,"batt":98.4,"det":false}]} +{"type":"step","ep":29,"step":49,"t":49.00,"coverage":0.0077,"drones":[{"id":0,"x":247.50,"y":2.50,"hdg":0.000,"batt":98.3,"det":false},{"id":1,"x":142.50,"y":2.50,"hdg":0.000,"batt":98.3,"det":false},{"id":2,"x":122.50,"y":2.50,"hdg":0.000,"batt":98.3,"det":false},{"id":3,"x":67.50,"y":2.50,"hdg":0.000,"batt":98.3,"det":false}]} +{"type":"step","ep":29,"step":50,"t":50.00,"coverage":0.0078,"drones":[{"id":0,"x":252.50,"y":2.50,"hdg":0.000,"batt":98.3,"det":false},{"id":1,"x":147.50,"y":2.50,"hdg":0.000,"batt":98.3,"det":false},{"id":2,"x":127.50,"y":2.50,"hdg":0.000,"batt":98.3,"det":false},{"id":3,"x":72.50,"y":2.50,"hdg":0.000,"batt":98.3,"det":false}]} +{"type":"step","ep":29,"step":51,"t":51.00,"coverage":0.0080,"drones":[{"id":0,"x":257.50,"y":2.50,"hdg":0.000,"batt":98.3,"det":false},{"id":1,"x":152.50,"y":2.50,"hdg":0.000,"batt":98.3,"det":false},{"id":2,"x":132.50,"y":2.50,"hdg":0.000,"batt":98.3,"det":false},{"id":3,"x":77.50,"y":2.50,"hdg":0.000,"batt":98.3,"det":false}]} +{"type":"step","ep":29,"step":52,"t":52.00,"coverage":0.0081,"drones":[{"id":0,"x":262.50,"y":2.50,"hdg":0.000,"batt":98.2,"det":false},{"id":1,"x":157.50,"y":2.50,"hdg":0.000,"batt":98.2,"det":false},{"id":2,"x":137.50,"y":2.50,"hdg":0.000,"batt":98.2,"det":false},{"id":3,"x":82.50,"y":2.50,"hdg":0.000,"batt":98.2,"det":false}]} +{"type":"step","ep":29,"step":53,"t":53.00,"coverage":0.0083,"drones":[{"id":0,"x":267.50,"y":2.50,"hdg":0.000,"batt":98.2,"det":false},{"id":1,"x":162.50,"y":2.50,"hdg":0.000,"batt":98.2,"det":false},{"id":2,"x":142.50,"y":2.50,"hdg":0.000,"batt":98.2,"det":false},{"id":3,"x":87.50,"y":2.50,"hdg":0.000,"batt":98.2,"det":false}]} +{"type":"step","ep":29,"step":54,"t":54.00,"coverage":0.0084,"drones":[{"id":0,"x":272.50,"y":2.50,"hdg":0.000,"batt":98.2,"det":false},{"id":1,"x":167.50,"y":2.50,"hdg":0.000,"batt":98.2,"det":false},{"id":2,"x":147.50,"y":2.50,"hdg":0.000,"batt":98.2,"det":false},{"id":3,"x":92.50,"y":2.50,"hdg":0.000,"batt":98.2,"det":false}]} +{"type":"step","ep":29,"step":55,"t":55.00,"coverage":0.0086,"drones":[{"id":0,"x":277.50,"y":2.50,"hdg":0.000,"batt":98.1,"det":false},{"id":1,"x":172.50,"y":2.50,"hdg":0.000,"batt":98.1,"det":false},{"id":2,"x":152.50,"y":2.50,"hdg":0.000,"batt":98.1,"det":false},{"id":3,"x":97.50,"y":2.50,"hdg":0.000,"batt":98.1,"det":false}]} +{"type":"step","ep":29,"step":56,"t":56.00,"coverage":0.0087,"drones":[{"id":0,"x":282.50,"y":2.50,"hdg":0.000,"batt":98.1,"det":false},{"id":1,"x":177.50,"y":2.50,"hdg":0.000,"batt":98.1,"det":false},{"id":2,"x":157.50,"y":2.50,"hdg":0.000,"batt":98.1,"det":false},{"id":3,"x":102.50,"y":2.50,"hdg":0.000,"batt":98.1,"det":false}]} +{"type":"step","ep":29,"step":57,"t":57.00,"coverage":0.0089,"drones":[{"id":0,"x":287.50,"y":2.50,"hdg":0.000,"batt":98.1,"det":false},{"id":1,"x":182.50,"y":2.50,"hdg":0.000,"batt":98.1,"det":false},{"id":2,"x":162.50,"y":2.50,"hdg":0.000,"batt":98.1,"det":false},{"id":3,"x":107.50,"y":2.50,"hdg":0.000,"batt":98.1,"det":false}]} +{"type":"step","ep":29,"step":58,"t":58.00,"coverage":0.0091,"drones":[{"id":0,"x":292.50,"y":2.50,"hdg":0.000,"batt":98.0,"det":false},{"id":1,"x":187.50,"y":2.50,"hdg":0.000,"batt":98.0,"det":false},{"id":2,"x":167.50,"y":2.50,"hdg":0.000,"batt":98.0,"det":false},{"id":3,"x":112.50,"y":2.50,"hdg":0.000,"batt":98.0,"det":false}]} +{"type":"step","ep":29,"step":59,"t":59.00,"coverage":0.0092,"drones":[{"id":0,"x":297.50,"y":2.50,"hdg":0.000,"batt":98.0,"det":false},{"id":1,"x":192.50,"y":2.50,"hdg":0.000,"batt":98.0,"det":false},{"id":2,"x":172.50,"y":2.50,"hdg":0.000,"batt":98.0,"det":false},{"id":3,"x":117.50,"y":2.50,"hdg":0.000,"batt":98.0,"det":false}]} +{"type":"episode","ep":29,"mean_return":123.6877,"policy_loss":-9671.3232,"value_loss":7154.7334,"victims_found":0} diff --git a/v2/crates/ruview-swarm/viz/swarm_viz.html b/v2/crates/ruview-swarm/viz/swarm_viz.html new file mode 100644 index 00000000..c4503467 --- /dev/null +++ b/v2/crates/ruview-swarm/viz/swarm_viz.html @@ -0,0 +1,725 @@ + + + + + + +ruview-swarm — training visualizer (ADR-148) + + + +
+

ruview-swarm — training visualizer (ADR-148)

+
no telemetry loaded — drop a .jsonl file or use the picker below
+
+ +
+ + + +
+ +
+
+

spatial swarm replay

+ +
+ + + +
+
+
+ +
+

training metrics

+ +
+
+
+ + + +