feat(swarm): selectable flight + self-learning patterns, wired into training + viz

Adds multiple flight/coverage-optimization strategies and self-learning
strategies, selectable from the trainer, and fixes drone clustering — the
demo sweep now covers 36% of the area (was ~0.9%) with 4 disjoint strips.

## Flight patterns (planning/patterns.rs) — `FlightPattern`
- PartitionedLawnmower (new default): area split into per-drone strips → no
  overlap, coverage scales ~linearly with swarm size (clustering fix)
- Boustrophedon (baseline), Spiral, Pheromone (stigmergic), PotentialField,
  LevyFlight. from_str/name/all + next_target(&PatternContext).

## Self-learning patterns (marl/learning.rs) — `LearningPattern`
- Mappo (CTDE centralized critic), Ippo (independent, jamming-robust),
  MappoCuriosity (count-based intrinsic novelty), MetaRl (MAML fast-adapt).
- CuriosityModule (visit_bonus = beta/sqrt(count), novelty decays on revisit),
  MetaAdapter (base + fast-weights, reset_fast/consolidate), shaped_reward().

## Trainer wiring (bin/train_marl.rs)
- --flight-pattern {boustrophedon|partitioned|spiral|pheromone|potential|levy}
- --learn-pattern  {mappo|ippo|curiosity|meta}
- Rollout now moves each drone per the selected FlightPattern (PatternContext
  with visited trail + live peers), curiosity-shapes the reward, and logs
  CTDE vs independent. Telemetry meta profile carries the pattern labels so the
  viewer header shows `flight=… · learn=…`.

## Verification
- Browser pass (viz at localhost:8777): partitioned run renders 4 distinct
  serpentine coverage bands, header shows the patterns, final coverage 36.3%,
  scrubber/speed/playback work, ZERO console errors. Screenshot confirmed.
- Regenerated viz/sample_telemetry.jsonl: 1 meta / 120 step / 30 episode,
  coverage 0.9% → 36.3%.

## Tests
- --no-default-features: 103/103 (was 91; +6 patterns +6 learning)
- --features train: 108/108

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv 2026-05-30 14:20:13 -04:00
parent 5450bfdc60
commit 17f3a1a235
6 changed files with 1114 additions and 159 deletions

View File

@ -3,21 +3,32 @@
//! Real Candle autodiff PPO training loop. Runs on CPU, or CUDA when built
//! with `--features train,cuda` (local RTX 5080 or a GCP L4 instance).
//!
//! Movement is driven by a selectable `FlightPattern` (boustrophedon,
//! partitioned, spiral, pheromone, potential, levy) and reward is shaped by a
//! selectable `LearningPattern` (mappo, ippo, curiosity, meta). This makes each
//! pattern produce visibly distinct trajectories + telemetry instead of every
//! drone clustering on the orchestrator's internal coverage strategy.
//!
//! Usage:
//! cargo run --release -p ruview-swarm --features train,cuda --bin train_marl -- \
//! --episodes 5000 --drones 4 --profile sar --checkpoint-dir ./marl-checkpoints
//! --episodes 5000 --drones 4 --profile sar \
//! --flight-pattern partitioned --learn-pattern mappo_curiosity \
//! --checkpoint-dir ./marl-checkpoints
//!
//! Right-sizing note: the policy is a 64→128→64 MLP. The bottleneck is
//! environment-rollout throughput, not GPU matmul — an L4 + 16 vCPU beats an
//! 8× A100 box for this workload at ~1/20th the cost. See scripts/gcp/.
use std::collections::HashSet;
use ruview_swarm::config::SwarmConfig;
use ruview_swarm::integration::telemetry::{DroneFrame, TelemetryRecorder};
use ruview_swarm::marl::candle_ppo::{CandlePpoConfig, CandleTrainer};
use ruview_swarm::marl::learning::{shaped_reward, CuriosityModule, LearningPattern};
use ruview_swarm::marl::observation::LocalObservation;
use ruview_swarm::marl::reward::{RewardCalculator, RewardContext};
use ruview_swarm::orchestrator::SwarmOrchestrator;
use ruview_swarm::types::{NodeId, Position3D};
use ruview_swarm::planning::patterns::{FlightPattern, PatternContext};
use ruview_swarm::types::{DroneState, NodeId, Position3D, Velocity3D};
struct Args {
episodes: usize,
@ -28,6 +39,8 @@ struct Args {
checkpoint_every: usize,
telemetry: Option<String>,
telemetry_episode: usize,
flight_pattern: String,
learn_pattern: String,
}
impl Default for Args {
@ -41,6 +54,8 @@ impl Default for Args {
checkpoint_every: 100,
telemetry: None,
telemetry_episode: 0,
flight_pattern: "partitioned".to_string(),
learn_pattern: "mappo".to_string(),
}
}
}
@ -84,6 +99,14 @@ fn parse_args() -> Args {
args.telemetry_episode = next().parse().unwrap_or(args.telemetry_episode);
i += 1;
}
"--flight-pattern" => {
args.flight_pattern = next();
i += 1;
}
"--learn-pattern" => {
args.learn_pattern = next();
i += 1;
}
"-h" | "--help" => {
println!(
"train_marl — ruview-swarm MARL training (ADR-148 M4)\n\
@ -92,6 +115,8 @@ fn parse_args() -> Args {
--drones N swarm size (default 4)\n \
--profile NAME sar|inspection|mine|agriculture (default sar)\n \
--steps N steps per episode (default 200)\n \
--flight-pattern P boustrophedon|partitioned|spiral|pheromone|potential|levy (default partitioned)\n \
--learn-pattern P mappo|ippo|curiosity|meta (default mappo)\n \
--checkpoint-dir D checkpoint output dir (default ./marl-checkpoints)\n \
--checkpoint-every N save every N episodes (default 100)\n \
--telemetry FILE write JSONL telemetry for viz/swarm_viz.html\n \
@ -115,14 +140,67 @@ fn config_for(profile: &str) -> SwarmConfig {
}
}
/// Map a world coordinate to a grid cell index at `grid_res` metre resolution.
fn cell_of(x: f64, y: f64, grid_res: f64) -> (u32, u32) {
let gx = (x / grid_res).floor().max(0.0) as u32;
let gy = (y / grid_res).floor().max(0.0) as u32;
(gx, gy)
}
/// Mark every grid cell within the drone's circular scan footprint as scanned,
/// returning how many *newly* scanned cells this step contributed.
fn mark_scanned(
scanned: &mut HashSet<(u32, u32)>,
pos: &Position3D,
scan_width_m: f64,
grid_res: f64,
area_w: f64,
area_h: f64,
) -> u32 {
let r = scan_width_m * 0.5;
let cols = (area_w / grid_res).ceil() as i64;
let rows = (area_h / grid_res).ceil() as i64;
let (cx, cy) = cell_of(pos.x, pos.y, grid_res);
let span = (r / grid_res).ceil() as i64;
let mut new_cells = 0u32;
for dgx in -span..=span {
for dgy in -span..=span {
let gx = cx as i64 + dgx;
let gy = cy as i64 + dgy;
if gx < 0 || gy < 0 || gx >= cols || gy >= rows {
continue;
}
// Cell centre in metres.
let mx = (gx as f64 + 0.5) * grid_res;
let my = (gy as f64 + 0.5) * grid_res;
if (mx - pos.x).hypot(my - pos.y) <= r && scanned.insert((gx as u32, gy as u32)) {
new_cells += 1;
}
}
}
new_cells
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = parse_args();
let cfg = config_for(&args.profile);
let flight_pattern = FlightPattern::from_str(&args.flight_pattern);
let learn_pattern = LearningPattern::from_str(&args.learn_pattern);
println!(
"MARL training: profile={} drones={} episodes={} steps/ep={}",
args.profile, args.drones, args.episodes, args.steps_per_episode
"MARL training: profile={} drones={} episodes={} steps/ep={} flight={} learn={} ({})",
args.profile,
args.drones,
args.episodes,
args.steps_per_episode,
flight_pattern.name(),
learn_pattern.name(),
if learn_pattern.centralized_critic() {
"CTDE / centralized critic"
} else {
"independent learners"
}
);
let ppo_cfg = CandlePpoConfig::default();
@ -132,23 +210,33 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let reward_calc = RewardCalculator::default();
std::fs::create_dir_all(&args.checkpoint_dir).ok();
let area_w = cfg.mission.area_width_m;
let area_h = cfg.mission.area_height_m;
let grid_res = cfg.mission.grid_resolution_m.max(1.0);
let scan_w = cfg.planning.csi_scan_width_m;
let max_speed = cfg.planning.max_speed_ms.max(0.1);
let altitude_z = -cfg.planning.flight_altitude_m;
let total_cells = ((area_w / grid_res).ceil() * (area_h / grid_res).ceil()).max(1.0);
// Synthetic victims placed within the mission area for reward signal.
let victims = vec![
Position3D { x: cfg.mission.area_width_m * 0.2, y: cfg.mission.area_height_m * 0.3, z: 0.0 },
Position3D { x: cfg.mission.area_width_m * 0.6, y: cfg.mission.area_height_m * 0.45, z: 0.0 },
Position3D { x: area_w * 0.2, y: area_h * 0.3, z: 0.0 },
Position3D { x: area_w * 0.6, y: area_h * 0.45, z: 0.0 },
];
// Composite profile label so the viewer header surfaces the active patterns.
let profile_label = format!(
"{} · flight={} · learn={}",
args.profile,
flight_pattern.name(),
learn_pattern.name()
);
// 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,
)?;
rec.meta(&profile_label, args.drones, area_w, area_h, &victims)?;
println!("telemetry → {path} (spatial steps from episode {})", args.telemetry_episode);
Some(rec)
}
@ -158,24 +246,30 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut best_return = f32::MIN;
for episode in 0..args.episodes {
// Build a fresh swarm for this episode.
let mut drones: Vec<SwarmOrchestrator> = (0..args.drones)
// Per-episode curiosity module (count-based novelty over the area).
let mut curiosity = CuriosityModule::new(area_w, area_h, 32, 0.5);
// Build drone states directly so the FlightPattern fully drives motion.
let cols = (args.drones as f64).sqrt().ceil().max(1.0) as usize;
let mut states: Vec<DroneState> = (0..args.drones)
.map(|d| {
let cols = (args.drones as f64).sqrt().ceil().max(1.0) as usize;
let (row, col) = (d / cols, d % cols);
SwarmOrchestrator::new_demo(
NodeId(d as u32),
cfg.clone(),
Position3D {
x: 10.0 + col as f64 * (cfg.mission.area_width_m / cols as f64),
y: 10.0 + row as f64 * (cfg.mission.area_height_m / cols.max(1) as f64),
z: -cfg.planning.flight_altitude_m,
},
victims.clone(),
)
let mut s = DroneState::default_at_origin(NodeId(d as u32));
s.position = Position3D {
x: 10.0 + col as f64 * (area_w / cols as f64),
y: 10.0 + row as f64 * (area_h / cols.max(1) as f64),
z: altitude_z,
};
s.altitude_agl_m = cfg.planning.flight_altitude_m;
s
})
.collect();
// Coverage tracker (shared across drones — total area scanned).
let mut scanned: HashSet<(u32, u32)> = HashSet::new();
// Rolling recent-positions trail for pheromone/potential patterns.
let mut visited: Vec<Position3D> = Vec::with_capacity(256);
// Rollout buffers (flattened across drones).
let mut obs_buf: Vec<LocalObservation> = Vec::new();
let mut action_buf: Vec<[f32; 4]> = Vec::new();
@ -186,47 +280,108 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
for step in 0..args.steps_per_episode {
let is_last = step == args.steps_per_episode - 1;
// Snapshot peer positions for neighbor observations.
// Snapshot peer positions for this tick (observations + repulsion).
let positions: Vec<(NodeId, Position3D)> =
drones.iter().map(|d| (d.node_id, d.state.position)).collect();
states.iter().map(|s| (s.id, s.position)).collect();
for drone in &mut drones {
let cells_before = drone.stats.cells_covered;
let prev_pos = drone.state.position;
for idx in 0..states.len() {
let prev_pos = states[idx].position;
let node_id = states[idx].id;
// Observation from current state + neighbors.
// Neighbour positions (everyone except this drone).
let neighbors: Vec<(NodeId, Position3D)> = positions
.iter()
.filter(|(id, _)| *id != drone.node_id)
.filter(|(id, _)| *id != node_id)
.cloned()
.collect();
let peers: Vec<Position3D> = neighbors.iter().map(|(_, p)| *p).collect();
// Observation from the current (pre-move) state.
let obs =
LocalObservation::from_state_no_grid(&drone.state, &neighbors, None, None);
LocalObservation::from_state_no_grid(&states[idx], &neighbors, None, None);
// Advance the simulation one tick.
drone.step(1.0, true).await;
// --- FlightPattern drives the next waypoint --------------------
let ctx = PatternContext {
drone_id: node_id,
swarm_size: args.drones,
current: prev_pos,
area_w,
area_h,
altitude_z,
scan_width_m: scan_w,
step: step as u64,
visited: &visited,
peers: &peers,
};
let target = flight_pattern.next_target(&ctx);
// Reward from this step's deltas.
let new_cells = drone.stats.cells_covered.saturating_sub(cells_before);
let nearest = neighbors
// Move one tick toward the target at max_speed (no teleport).
let dx = target.x - prev_pos.x;
let dy = target.y - prev_pos.y;
let dist = dx.hypot(dy);
let new_pos = if dist > 1e-9 {
let stepd = dist.min(max_speed);
Position3D {
x: prev_pos.x + dx / dist * stepd,
y: prev_pos.y + dy / dist * stepd,
z: altitude_z,
}
} else {
prev_pos
};
let heading = if dist > 1e-9 { dy.atan2(dx) } else { states[idx].heading_rad };
let moved = prev_pos.distance_to(&new_pos);
// Commit the move to the drone state.
{
let s = &mut states[idx];
s.velocity = Velocity3D {
vx: (new_pos.x - prev_pos.x),
vy: (new_pos.y - prev_pos.y),
vz: 0.0,
};
s.position = new_pos;
s.heading_rad = heading;
s.timestamp_ms = s.timestamp_ms.saturating_add(1000);
}
// Coverage: mark scanned footprint, count new cells.
let new_cells =
mark_scanned(&mut scanned, &new_pos, scan_w, grid_res, area_w, area_h);
// Detection: any victim within the scan footprint.
let detected = victims.iter().any(|v| new_pos.distance_to(v) < scan_w);
// Nearest-neighbour distance (for collision shaping).
let nearest = peers
.iter()
.map(|(_, p)| prev_pos.distance_to(p))
.map(|p| new_pos.distance_to(p))
.fold(f64::MAX, f64::min);
let ctx = RewardContext {
state: &drone.state,
// Base extrinsic reward.
let ctx_r = RewardContext {
state: &states[idx],
new_cells_covered: new_cells,
victim_confirmed: false,
victim_confirmed: detected,
contributed_to_triangulation: false,
nearest_neighbor_dist: nearest,
geofence_breached: false,
battery_depleted_without_rth: false,
};
let reward = reward_calc.compute(&ctx);
let base = reward_calc.compute(&ctx_r);
// Curiosity shaping (only when the learning pattern uses it).
let reward = if learn_pattern.uses_curiosity() {
let bonus = curiosity.visit_bonus(new_pos.x, new_pos.y);
shaped_reward(learn_pattern, base, bonus)
} else {
base
};
let action = [
drone.state.heading_rad as f32,
drone.state.altitude_agl_m as f32,
drone.state.velocity.magnitude() as f32,
heading as f32,
states[idx].altitude_agl_m as f32,
(moved / 1.0) as f32,
0.0,
];
@ -235,35 +390,36 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
reward_buf.push(reward);
value_buf.push(0.0); // bootstrap value (critic learns this)
done_buf.push(is_last);
// Record the move in the shared visited trail (cap length).
visited.push(new_pos);
}
// Record spatial telemetry for the selected episode only (keeps the
// log small — one episode of step frames + metrics for every episode).
// Trim the visited trail to the most recent ~200 positions.
if visited.len() > 200 {
let drop = visited.len() - 200;
visited.drain(0..drop);
}
// Record spatial telemetry for the selected episode only.
if let Some(rec) = telem.as_mut() {
if episode == args.telemetry_episode {
let scan_w = cfg.planning.csi_scan_width_m;
let frames: Vec<DroneFrame> = drones
let frames: Vec<DroneFrame> = states
.iter()
.map(|d| {
let detected = victims
.iter()
.any(|v| d.state.position.distance_to(v) < scan_w);
DroneFrame::from_state(&d.state, detected)
.map(|s| {
let detected =
victims.iter().any(|v| s.position.distance_to(v) < scan_w);
DroneFrame::from_state(s, detected)
})
.collect();
let coverage = drones
.iter()
.map(|d| d.probability_grid.coverage_pct())
.sum::<f64>()
/ drones.len().max(1) as f64;
let coverage = scanned.len() as f64 / total_cells;
let _ = rec.step(episode, step, step as f64, &frames, coverage);
}
}
}
// PPO update on the episode's rollout.
let (advantages, returns) =
trainer.compute_gae(&reward_buf, &value_buf, &done_buf);
let (advantages, returns) = trainer.compute_gae(&reward_buf, &value_buf, &done_buf);
let old_log_probs = vec![0.0f32; obs_buf.len()];
let (policy_loss, value_loss, _entropy) =
trainer.update(&obs_buf, &action_buf, &advantages, &returns, &old_log_probs)?;
@ -284,15 +440,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
if episode % 10 == 0 || episode == args.episodes - 1 {
let coverage_pct = scanned.len() as f64 / total_cells * 100.0;
println!(
"ep {:>5}/{} mean_return={:>8.3} best={:>8.3} policy_loss={:>8.4} value_loss={:>8.4}",
episode, args.episodes, mean_return, best_return, policy_loss, value_loss
"ep {:>5}/{} mean_return={:>8.3} best={:>8.3} policy_loss={:>8.4} value_loss={:>8.4} coverage={:>5.1}%",
episode, args.episodes, mean_return, best_return, policy_loss, value_loss, coverage_pct
);
}
// Checkpoint the trained variables periodically.
if args.checkpoint_every > 0
&& (episode + 1) % args.checkpoint_every == 0
if args.checkpoint_every > 0 && (episode + 1) % args.checkpoint_every == 0
|| episode == args.episodes - 1
{
let path = format!("{}/marl-ep{}.safetensors", args.checkpoint_dir, episode + 1);

View File

@ -0,0 +1,304 @@
//! Selectable self-learning strategies for swarm MARL.
//!
//! - Mappo: centralized-critic, decentralized-execution (CTDE). Best cooperative
//! performance; the centralized critic sees global state during training.
//! - Ippo: independent PPO — each agent learns alone, no shared critic. Robust to
//! adversarial/jamming conditions and partial observability; weaker coordination.
//! - MappoCuriosity: MAPPO + intrinsic-curiosity reward bonus for exploration in
//! sparse-reward regimes (count-based novelty over visited regions).
//! - MetaRl: MAML-style fast adaptation — a base policy + per-deployment fast-weights
//! that adapt in a few in-flight steps to wind/sensor drift.
//!
//! Pure Rust — always compiled (no Candle needed). This is the *strategy* layer;
//! the gradient backend lives in `candle_ppo.rs` behind the `train` feature.
/// Which self-learning strategy the swarm trains under. Selectable at runtime.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LearningPattern {
/// Centralized critic, decentralized execution (CTDE).
Mappo,
/// Independent PPO — each agent learns alone, no shared critic.
Ippo,
/// MAPPO plus count-based intrinsic-curiosity reward bonus.
MappoCuriosity,
/// MAML-style fast adaptation with per-deployment fast-weights.
MetaRl,
}
impl Default for LearningPattern {
fn default() -> Self {
LearningPattern::Mappo
}
}
impl LearningPattern {
/// Parse from a short identifier. Unknown strings fall back to the default
/// (Mappo). Accepts both canonical names and friendly aliases.
pub fn from_str(s: &str) -> Self {
match s.trim().to_ascii_lowercase().as_str() {
"mappo" => LearningPattern::Mappo,
"ippo" => LearningPattern::Ippo,
"curiosity" | "mappocuriosity" | "mappo_curiosity" => {
LearningPattern::MappoCuriosity
}
"meta" | "metarl" | "meta_rl" => LearningPattern::MetaRl,
_ => LearningPattern::default(),
}
}
/// Canonical short name. `from_str(p.name()) == p` for every variant.
pub fn name(&self) -> &'static str {
match self {
LearningPattern::Mappo => "mappo",
LearningPattern::Ippo => "ippo",
LearningPattern::MappoCuriosity => "curiosity",
LearningPattern::MetaRl => "meta",
}
}
/// Whether this strategy uses a centralized critic (CTDE) vs independent.
pub fn centralized_critic(&self) -> bool {
matches!(
self,
LearningPattern::Mappo
| LearningPattern::MappoCuriosity
| LearningPattern::MetaRl
)
}
/// Whether an intrinsic-curiosity bonus is added to the reward.
pub fn uses_curiosity(&self) -> bool {
matches!(self, LearningPattern::MappoCuriosity)
}
}
// ---------------------------------------------------------------------------
// Curiosity: count-based intrinsic motivation
// ---------------------------------------------------------------------------
/// Count-based intrinsic-motivation module.
///
/// Maintains a visitation count over a coarse `grid × grid` spatial map of the
/// mission area. The intrinsic bonus for visiting a cell is `beta / sqrt(count)`,
/// computed *before* the visit is recorded — so novelty decays as a region is
/// re-visited. This rewards exploration in sparse-reward regimes.
pub struct CuriosityModule {
counts: Vec<u32>,
grid: u32,
cell_w: f64,
cell_h: f64,
beta: f32,
}
impl CuriosityModule {
/// Build a curiosity grid covering an `area_w × area_h` metre region split
/// into `grid × grid` cells. `beta` scales the intrinsic bonus magnitude.
pub fn new(area_w: f64, area_h: f64, grid: u32, beta: f32) -> Self {
let g = grid.max(1);
let cells = (g as usize) * (g as usize);
let cell_w = if area_w > 0.0 { area_w / g as f64 } else { 1.0 };
let cell_h = if area_h > 0.0 { area_h / g as f64 } else { 1.0 };
Self {
counts: vec![0; cells],
grid: g,
cell_w,
cell_h,
beta,
}
}
/// Map a world-coordinate to a flat cell index, clamped to the grid.
fn cell_index(&self, x: f64, y: f64) -> usize {
let gx = ((x / self.cell_w).floor() as i64).clamp(0, self.grid as i64 - 1) as usize;
let gy = ((y / self.cell_h).floor() as i64).clamp(0, self.grid as i64 - 1) as usize;
gy * self.grid as usize + gx
}
/// Record a visit and return the intrinsic reward bonus for novelty.
///
/// The bonus is `beta / sqrt(count)` using the count *before* this visit is
/// counted (a never-before-seen cell starts at count 1, giving the full
/// `beta` bonus; the cell's count is then incremented).
pub fn visit_bonus(&mut self, x: f64, y: f64) -> f32 {
let idx = self.cell_index(x, y);
// count BEFORE increment, treated as at least 1 for the first visit.
let prior = self.counts[idx].max(0) + 1;
let bonus = self.beta / (prior as f32).sqrt();
self.counts[idx] = self.counts[idx].saturating_add(1);
bonus
}
/// Total recorded visits across the whole grid.
pub fn total_visits(&self) -> u64 {
self.counts.iter().map(|&c| c as u64).sum()
}
}
// ---------------------------------------------------------------------------
// Meta-RL: MAML-style fast-weight adapter
// ---------------------------------------------------------------------------
/// MAML-style fast-weight adapter for few-shot in-flight adaptation.
///
/// Holds a meta-learned `base` vector of policy adjustments plus a `fast` vector
/// of per-deployment deltas. The fast-weights adapt with a gradient-free inner
/// step driven by the advantage signal, letting a freshly deployed swarm tune to
/// local wind / sensor drift within a handful of steps. `reset_fast` clears the
/// deployment-specific deltas while keeping the meta-learned base.
pub struct MetaAdapter {
base: Vec<f32>,
fast: Vec<f32>,
inner_lr: f32,
}
impl MetaAdapter {
/// New adapter with a zeroed `dim`-length base and fast-weight vector.
pub fn new(dim: usize, inner_lr: f32) -> Self {
Self {
base: vec![0.0; dim],
fast: vec![0.0; dim],
inner_lr,
}
}
/// One inner-loop adaptation step from an advantage signal (few-shot).
///
/// Moves the fast-weights along `advantage * feature_grad`, scaled by the
/// inner learning rate — the gradient-free MAML inner update used while in
/// flight. `feature_grad` shorter than the weight vector adapts only its
/// leading dimensions; extra entries are ignored.
pub fn adapt(&mut self, advantage: f32, feature_grad: &[f32]) {
let n = self.fast.len().min(feature_grad.len());
for i in 0..n {
self.fast[i] += self.inner_lr * advantage * feature_grad[i];
}
}
/// Current effective weights (base + fast).
pub fn effective(&self) -> Vec<f32> {
self.base
.iter()
.zip(self.fast.iter())
.map(|(b, f)| b + f)
.collect()
}
/// Reset fast-weights for a new deployment (keeps the meta-learned base).
pub fn reset_fast(&mut self) {
for f in self.fast.iter_mut() {
*f = 0.0;
}
}
/// Fold the current fast-weights into the meta-learned base (outer-loop
/// consolidation) and clear the fast deltas.
pub fn consolidate(&mut self) {
for (b, f) in self.base.iter_mut().zip(self.fast.iter()) {
*b += *f;
}
self.reset_fast();
}
}
// ---------------------------------------------------------------------------
// Reward shaping helper
// ---------------------------------------------------------------------------
/// Shape a base reward according to the selected learning pattern.
///
/// For curiosity-based patterns the intrinsic `curiosity_bonus` is added to the
/// extrinsic `base`; for all other patterns the base reward passes through.
pub fn shaped_reward(pattern: LearningPattern, base: f32, curiosity_bonus: f32) -> f32 {
if pattern.uses_curiosity() {
base + curiosity_bonus
} else {
base
}
}
#[cfg(test)]
mod tests {
use super::*;
const ALL: [LearningPattern; 4] = [
LearningPattern::Mappo,
LearningPattern::Ippo,
LearningPattern::MappoCuriosity,
LearningPattern::MetaRl,
];
#[test]
fn test_pattern_from_str_roundtrip() {
for p in ALL {
assert_eq!(
LearningPattern::from_str(p.name()),
p,
"round-trip failed for {}",
p.name()
);
}
}
#[test]
fn test_centralized_vs_independent() {
// Mappo IS centralized (CTDE); Ippo is NOT (independent learners).
assert!(LearningPattern::Mappo.centralized_critic());
assert!(!LearningPattern::Ippo.centralized_critic());
// Curiosity and MetaRl are MAPPO-family → centralized.
assert!(LearningPattern::MappoCuriosity.centralized_critic());
assert!(LearningPattern::MetaRl.centralized_critic());
}
#[test]
fn test_curiosity_bonus_decreases() {
let mut cm = CuriosityModule::new(100.0, 100.0, 10, 1.0);
let first = cm.visit_bonus(50.0, 50.0);
let second = cm.visit_bonus(50.0, 50.0); // same cell again
assert!(
second < first,
"novelty should decay: first={first}, second={second}"
);
}
#[test]
fn test_curiosity_bonus_in_bounds() {
let mut cm = CuriosityModule::new(100.0, 100.0, 8, 0.5);
// In-bounds, out-of-bounds, and negative coords all clamp safely.
for &(x, y) in &[(0.0, 0.0), (50.0, 50.0), (999.0, -999.0), (-5.0, 1000.0)] {
let b = cm.visit_bonus(x, y);
assert!(b.is_finite(), "bonus must be finite, got {b}");
assert!(b >= 0.0, "bonus must be >= 0, got {b}");
}
}
#[test]
fn test_meta_adapter_changes_weights() {
let mut ma = MetaAdapter::new(4, 0.1);
let base = ma.effective();
ma.adapt(2.0, &[1.0, -1.0, 0.5, 0.0]);
let adapted = ma.effective();
assert_ne!(base, adapted, "adapt() must change effective weights");
ma.reset_fast();
assert_eq!(
base,
ma.effective(),
"reset_fast() must restore the meta-learned base"
);
}
#[test]
fn test_shaped_reward_curiosity_only() {
let base = 10.0;
let bonus = 3.0;
// MappoCuriosity adds the bonus.
assert_eq!(
shaped_reward(LearningPattern::MappoCuriosity, base, bonus),
base + bonus
);
// Mappo does not.
assert_eq!(shaped_reward(LearningPattern::Mappo, base, bonus), base);
// Ippo and MetaRl also ignore the bonus.
assert_eq!(shaped_reward(LearningPattern::Ippo, base, bonus), base);
assert_eq!(shaped_reward(LearningPattern::MetaRl, base, bonus), base);
}
}

View File

@ -1,4 +1,5 @@
pub mod actor;
pub mod learning;
pub mod observation;
pub mod reward;
pub mod role_attention;
@ -6,6 +7,7 @@ pub mod trainer;
pub mod training_loop;
pub use actor::{MappoActor, ActorConfig, ActorAction};
pub use learning::{LearningPattern, CuriosityModule, MetaAdapter, shaped_reward};
pub use observation::LocalObservation;
pub use reward::{RewardCalculator, RewardContext};
pub use role_attention::{NodeRole, RoleAttention, triangulation_geometry_penalty};

View File

@ -4,7 +4,9 @@ pub mod rrt_apf;
pub mod coverage;
pub mod probability_grid;
pub mod pheromone;
pub mod patterns;
pub use rrt_apf::{RrtApfPlanner, Waypoint};
pub use coverage::{CoverageStrategy, Phase};
pub use probability_grid::ProbabilityGrid;
pub use patterns::{FlightPattern, PatternContext};

View File

@ -0,0 +1,431 @@
//! Flight / coverage-optimization patterns for swarm area search.
//!
//! Different strategies trade off coverage completeness, time, and robustness:
//! - Boustrophedon: systematic lawnmower; complete but drones overlap if unpartitioned
//! - PartitionedLawnmower: area split into per-drone strips → no overlap, ~Nx faster coverage
//! - Spiral: outward spiral from a seed; good for centred search (last-known-position SAR)
//! - Pheromone: stigmergic — steer away from recently-visited cells; robust to dropout
//! - PotentialField: repelled by visited cells + peers, attracted to unscanned frontier
//! - LevyFlight: heavy-tailed random walk; good exploration when target location unknown
use crate::types::{NodeId, Position3D};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlightPattern {
Boustrophedon,
PartitionedLawnmower,
Spiral,
Pheromone,
PotentialField,
LevyFlight,
}
impl Default for FlightPattern {
fn default() -> Self {
FlightPattern::PartitionedLawnmower
}
}
impl FlightPattern {
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"boustrophedon" | "lawnmower" => FlightPattern::Boustrophedon,
"partitioned" | "partitioned_lawnmower" => FlightPattern::PartitionedLawnmower,
"spiral" => FlightPattern::Spiral,
"pheromone" | "stigmergic" => FlightPattern::Pheromone,
"potential" | "potential_field" => FlightPattern::PotentialField,
"levy" | "levyflight" | "levy_flight" => FlightPattern::LevyFlight,
_ => FlightPattern::default(),
}
}
pub fn name(&self) -> &'static str {
match self {
FlightPattern::Boustrophedon => "boustrophedon",
FlightPattern::PartitionedLawnmower => "partitioned_lawnmower",
FlightPattern::Spiral => "spiral",
FlightPattern::Pheromone => "pheromone",
FlightPattern::PotentialField => "potential_field",
FlightPattern::LevyFlight => "levy_flight",
}
}
/// All pattern variants, for enumeration / UI selection.
pub fn all() -> [FlightPattern; 6] {
[
FlightPattern::Boustrophedon,
FlightPattern::PartitionedLawnmower,
FlightPattern::Spiral,
FlightPattern::Pheromone,
FlightPattern::PotentialField,
FlightPattern::LevyFlight,
]
}
}
/// Inputs for computing the next waypoint under a pattern.
pub struct PatternContext<'a> {
pub drone_id: NodeId,
pub swarm_size: usize,
pub current: Position3D,
pub area_w: f64,
pub area_h: f64,
pub altitude_z: f64, // flight z (negative NED)
pub scan_width_m: f64, // strip spacing
pub step: u64, // tick counter (for deterministic pseudo-random patterns)
pub visited: &'a [Position3D], // recently visited cell centres (for pheromone/potential)
pub peers: &'a [Position3D], // peer positions (for potential-field repulsion)
}
impl FlightPattern {
/// Compute the next target position for a drone under this pattern.
pub fn next_target(&self, ctx: &PatternContext) -> Position3D {
match self {
FlightPattern::Boustrophedon => boustrophedon(ctx),
FlightPattern::PartitionedLawnmower => partitioned_lawnmower(ctx),
FlightPattern::Spiral => spiral(ctx),
FlightPattern::Pheromone => pheromone(ctx),
FlightPattern::PotentialField => potential_field(ctx),
FlightPattern::LevyFlight => levy_flight(ctx),
}
}
}
/// Clamp a candidate (x, y) to the area bounds and lift it to the flight altitude.
fn clamp_to_area(x: f64, y: f64, ctx: &PatternContext) -> Position3D {
Position3D {
x: x.clamp(0.0, ctx.area_w),
y: y.clamp(0.0, ctx.area_h),
z: ctx.altitude_z,
}
}
/// Serpentine waypoint within a rectangular sub-region.
///
/// Walks rows of height `scan_width_m`; on each row sweeps left→right or
/// right→left depending on the row parity, advancing one `scan_width_m`
/// segment per `step`.
fn serpentine_in_region(
x0: f64,
x1: f64,
y0: f64,
y1: f64,
scan_width_m: f64,
step: u64,
) -> (f64, f64) {
let strip_w = (x1 - x0).max(scan_width_m);
let height = (y1 - y0).max(scan_width_m);
// Number of horizontal segments per row before stepping to the next row.
let cols = ((strip_w / scan_width_m).ceil() as u64).max(1);
// Number of rows in this region.
let rows = ((height / scan_width_m).ceil() as u64).max(1);
let total = cols * rows;
let s = step % total;
let row = s / cols;
let col = s % cols;
// Centre of the current row band.
let y = y0 + (row as f64 + 0.5) * scan_width_m;
let y = y.min(y1);
// Serpentine: even rows L→R, odd rows R→L.
let along = if row % 2 == 0 { col } else { cols - 1 - col };
let x = x0 + (along as f64 + 0.5) * scan_width_m;
let x = x.min(x1);
(x, y)
}
/// Classic full-area serpentine lawnmower (drones may overlap — baseline).
fn boustrophedon(ctx: &PatternContext) -> Position3D {
let (x, y) = serpentine_in_region(
0.0,
ctx.area_w,
0.0,
ctx.area_h,
ctx.scan_width_m,
ctx.step,
);
clamp_to_area(x, y, ctx)
}
/// Partitioned lawnmower: split `area_w` into `swarm_size` vertical strips;
/// drone `i` lawnmowers ONLY within strip `[i*w/n, (i+1)*w/n]`.
///
/// This is the clustering fix: each drone covers a disjoint band, so total
/// coverage scales ~linearly with swarm size instead of all drones tracing
/// the same path.
fn partitioned_lawnmower(ctx: &PatternContext) -> Position3D {
let n = ctx.swarm_size.max(1);
let i = (ctx.drone_id.0 as usize) % n;
let strip_w = ctx.area_w / n as f64;
let x0 = i as f64 * strip_w;
let x1 = x0 + strip_w;
let (x, y) =
serpentine_in_region(x0, x1, 0.0, ctx.area_h, ctx.scan_width_m, ctx.step);
clamp_to_area(x, y, ctx)
}
/// Outward Archimedean spiral from the area centre; radius grows with step.
fn spiral(ctx: &PatternContext) -> Position3D {
let cx = ctx.area_w / 2.0;
let cy = ctx.area_h / 2.0;
// Angular step keeps successive waypoints roughly `scan_width_m` apart.
let theta = ctx.step as f64 * 0.6;
// Archimedean spiral r = b * theta; b chosen so each turn adds scan_width_m.
let b = ctx.scan_width_m / (2.0 * std::f64::consts::PI);
let r = b * theta;
let x = cx + r * theta.cos();
let y = cy + r * theta.sin();
clamp_to_area(x, y, ctx)
}
/// Stigmergic: sample candidate headings, step toward the least-visited one.
fn pheromone(ctx: &PatternContext) -> Position3D {
let step_len = ctx.scan_width_m.max(1.0);
// Deterministic base heading offset per drone so they diverge.
let base = ctx.drone_id.0 as f64 * (std::f64::consts::PI / 3.0);
let n_candidates = 8;
let mut best: Option<(f64, f64, f64)> = None; // (score, x, y); lower score = less visited
for k in 0..n_candidates {
let theta = base + (k as f64) * (2.0 * std::f64::consts::PI / n_candidates as f64);
let cx = ctx.current.x + step_len * theta.cos();
let cy = ctx.current.y + step_len * theta.sin();
let cx = cx.clamp(0.0, ctx.area_w);
let cy = cy.clamp(0.0, ctx.area_h);
// Penalty = sum of inverse-distance to recently-visited cell centres.
let mut visit_pressure = 0.0;
for v in ctx.visited {
let d = (cx - v.x).hypot(cy - v.y);
visit_pressure += 1.0 / (1.0 + d);
}
if best.as_ref().is_none_or(|(bs, _, _)| visit_pressure < *bs) {
best = Some((visit_pressure, cx, cy));
}
}
let (_, x, y) = best.unwrap_or((0.0, ctx.current.x, ctx.current.y));
clamp_to_area(x, y, ctx)
}
/// Potential field: repelled by visited cells + peers, attracted to the
/// nearest unscanned frontier; step in the resultant direction.
fn potential_field(ctx: &PatternContext) -> Position3D {
let mut fx = 0.0;
let mut fy = 0.0;
// Repulsion from recently-visited cells.
for v in ctx.visited {
let dx = ctx.current.x - v.x;
let dy = ctx.current.y - v.y;
let d2 = dx * dx + dy * dy + 1.0;
let mag = 1.0 / d2;
fx += dx / d2.sqrt() * mag;
fy += dy / d2.sqrt() * mag;
}
// Repulsion from peers (collision / overlap avoidance).
for p in ctx.peers {
let dx = ctx.current.x - p.x;
let dy = ctx.current.y - p.y;
let d2 = dx * dx + dy * dy + 1.0;
let mag = 2.0 / d2; // peers repel more strongly than stale trail
fx += dx / d2.sqrt() * mag;
fy += dy / d2.sqrt() * mag;
}
// Attraction toward the nearest unscanned frontier point. Sample a grid of
// candidate area points; pick the one with greatest distance to any visited
// cell (i.e. the least-explored region) and pull toward it.
let mut frontier: Option<(f64, f64, f64)> = None; // (openness, x, y)
let samples = 5;
for ix in 0..=samples {
for iy in 0..=samples {
let px = ctx.area_w * ix as f64 / samples as f64;
let py = ctx.area_h * iy as f64 / samples as f64;
let mut nearest = f64::INFINITY;
for v in ctx.visited {
let d = (px - v.x).hypot(py - v.y);
if d < nearest {
nearest = d;
}
}
if !nearest.is_finite() {
nearest = (px - ctx.current.x).hypot(py - ctx.current.y);
}
if frontier.as_ref().is_none_or(|(o, _, _)| nearest > *o) {
frontier = Some((nearest, px, py));
}
}
}
if let Some((_, gx, gy)) = frontier {
let dx = gx - ctx.current.x;
let dy = gy - ctx.current.y;
let d = (dx * dx + dy * dy).sqrt().max(1e-6);
fx += dx / d * 1.5; // attraction gain
fy += dy / d * 1.5;
}
let fmag = (fx * fx + fy * fy).sqrt();
let step_len = ctx.scan_width_m.max(1.0);
let (x, y) = if fmag > 1e-9 {
(
ctx.current.x + fx / fmag * step_len,
ctx.current.y + fy / fmag * step_len,
)
} else {
(ctx.current.x, ctx.current.y)
};
clamp_to_area(x, y, ctx)
}
/// Deterministic pseudo-random heavy-tailed step (Lévy flight). Most steps are
/// short; occasional long jumps. Seeded from drone_id + step via an LCG so the
/// trajectory is reproducible.
fn levy_flight(ctx: &PatternContext) -> Position3D {
// Linear congruential generator (Numerical Recipes constants).
let seed = (ctx.drone_id.0 as u64)
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
.wrapping_add(ctx.step.wrapping_mul(0x2545_F491_4F6C_DD1D));
let r1 = lcg(seed);
let r2 = lcg(r1);
let u_angle = (r1 >> 11) as f64 / (1u64 << 53) as f64; // [0,1)
let u_len = ((r2 >> 11) as f64 / (1u64 << 53) as f64).max(1e-6); // (0,1]
let theta = u_angle * 2.0 * std::f64::consts::PI;
// Heavy-tailed step length: inverse power-law (Pareto-like), exponent ~1.5.
let step_len = ctx.scan_width_m.max(1.0) * u_len.powf(-1.0 / 1.5);
// Cap to the area diagonal so a single jump can't shoot arbitrarily far.
let max_jump = (ctx.area_w * ctx.area_w + ctx.area_h * ctx.area_h).sqrt();
let step_len = step_len.min(max_jump);
let x = ctx.current.x + step_len * theta.cos();
let y = ctx.current.y + step_len * theta.sin();
clamp_to_area(x, y, ctx)
}
#[inline]
fn lcg(state: u64) -> u64 {
state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407)
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx<'a>(
drone_id: u32,
swarm_size: usize,
step: u64,
current: Position3D,
visited: &'a [Position3D],
peers: &'a [Position3D],
) -> PatternContext<'a> {
PatternContext {
drone_id: NodeId(drone_id),
swarm_size,
current,
area_w: 100.0,
area_h: 80.0,
altitude_z: -20.0,
scan_width_m: 5.0,
step,
visited,
peers,
}
}
#[test]
fn test_partitioned_strips_disjoint() {
let empty: [Position3D; 0] = [];
// Two drones, swarm of 2: drone 0 owns left half, drone 1 the right half.
let mut d0_xs = Vec::new();
let mut d1_xs = Vec::new();
for s in 0..40u64 {
let c0 = ctx(0, 2, s, Position3D::zero(), &empty, &empty);
let c1 = ctx(1, 2, s, Position3D::zero(), &empty, &empty);
d0_xs.push(FlightPattern::PartitionedLawnmower.next_target(&c0).x);
d1_xs.push(FlightPattern::PartitionedLawnmower.next_target(&c1).x);
}
let mid = 100.0 / 2.0;
// Drone 0 stays strictly in the left half, drone 1 strictly in the right.
assert!(d0_xs.iter().all(|&x| x <= mid), "drone 0 left of midline");
assert!(d1_xs.iter().all(|&x| x >= mid), "drone 1 right of midline");
// And they never share an x position (disjoint strips → no overlap).
for &a in &d0_xs {
for &b in &d1_xs {
assert!(a < b || (a <= mid && b >= mid), "strips overlap: {a} vs {b}");
}
}
}
#[test]
fn test_all_patterns_in_bounds() {
let visited = [
Position3D { x: 10.0, y: 10.0, z: -20.0 },
Position3D { x: 50.0, y: 40.0, z: -20.0 },
];
let peers = [Position3D { x: 30.0, y: 20.0, z: -20.0 }];
for pat in FlightPattern::all() {
let mut current = Position3D { x: 25.0, y: 25.0, z: -20.0 };
for s in 0..20u64 {
let c = ctx(1, 4, s, current, &visited, &peers);
let t = pat.next_target(&c);
assert!(
t.x >= 0.0 && t.x <= 100.0,
"{} x out of bounds at step {s}: {}",
pat.name(),
t.x
);
assert!(
t.y >= 0.0 && t.y <= 80.0,
"{} y out of bounds at step {s}: {}",
pat.name(),
t.y
);
assert_eq!(t.z, -20.0, "{} altitude wrong", pat.name());
current = t;
}
}
}
#[test]
fn test_pattern_from_str_roundtrip() {
for pat in FlightPattern::all() {
assert_eq!(
FlightPattern::from_str(pat.name()),
pat,
"roundtrip failed for {}",
pat.name()
);
}
}
#[test]
fn test_spiral_radius_grows() {
let empty: [Position3D; 0] = [];
let centre_x = 100.0 / 2.0;
let centre_y = 80.0 / 2.0;
let dist = |s: u64| {
let c = ctx(0, 1, s, Position3D::zero(), &empty, &empty);
let t = FlightPattern::Spiral.next_target(&c);
((t.x - centre_x).powi(2) + (t.y - centre_y).powi(2)).sqrt()
};
let near = dist(1);
let far = dist(50);
assert!(
far > near,
"spiral radius should grow: step1={near}, step50={far}"
);
}
}

View File

@ -1,91 +1,151 @@
{"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}
{"type":"meta","profile":"sar · flight=partitioned_lawnmower · learn=curiosity","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":741.0585,"policy_loss":-620.2698,"value_loss":895311.9375,"victims_found":0}
{"type":"episode","ep":1,"mean_return":741.0585,"policy_loss":-990.6716,"value_loss":894566.8750,"victims_found":0}
{"type":"episode","ep":2,"mean_return":741.0585,"policy_loss":-1413.1008,"value_loss":893805.3125,"victims_found":0}
{"type":"episode","ep":3,"mean_return":741.0585,"policy_loss":-1880.1860,"value_loss":892984.3125,"victims_found":0}
{"type":"episode","ep":4,"mean_return":741.0585,"policy_loss":-2413.7896,"value_loss":892056.8750,"victims_found":0}
{"type":"episode","ep":5,"mean_return":741.0585,"policy_loss":-3023.4106,"value_loss":891006.7500,"victims_found":0}
{"type":"episode","ep":6,"mean_return":741.0585,"policy_loss":-3718.3889,"value_loss":889813.3750,"victims_found":0}
{"type":"episode","ep":7,"mean_return":741.0585,"policy_loss":-4531.2881,"value_loss":888444.9375,"victims_found":0}
{"type":"episode","ep":8,"mean_return":741.0585,"policy_loss":-5481.8413,"value_loss":886880.6250,"victims_found":0}
{"type":"episode","ep":9,"mean_return":741.0585,"policy_loss":-6585.8242,"value_loss":885090.9375,"victims_found":0}
{"type":"episode","ep":10,"mean_return":741.0585,"policy_loss":-7877.0918,"value_loss":883025.6875,"victims_found":0}
{"type":"episode","ep":11,"mean_return":741.0585,"policy_loss":-9397.7676,"value_loss":880637.3125,"victims_found":0}
{"type":"episode","ep":12,"mean_return":741.0585,"policy_loss":-11170.0850,"value_loss":877859.1875,"victims_found":0}
{"type":"episode","ep":13,"mean_return":741.0585,"policy_loss":-13207.6719,"value_loss":874662.1875,"victims_found":0}
{"type":"episode","ep":14,"mean_return":741.0585,"policy_loss":-15539.2266,"value_loss":871001.6250,"victims_found":0}
{"type":"episode","ep":15,"mean_return":741.0585,"policy_loss":-18196.9355,"value_loss":866829.7500,"victims_found":0}
{"type":"episode","ep":16,"mean_return":741.0585,"policy_loss":-21217.7344,"value_loss":862094.5000,"victims_found":0}
{"type":"episode","ep":17,"mean_return":741.0585,"policy_loss":-24640.4941,"value_loss":856742.5625,"victims_found":0}
{"type":"episode","ep":18,"mean_return":741.0585,"policy_loss":-28501.7402,"value_loss":850726.5625,"victims_found":0}
{"type":"episode","ep":19,"mean_return":741.0585,"policy_loss":-32825.4297,"value_loss":844003.0000,"victims_found":0}
{"type":"episode","ep":20,"mean_return":741.0585,"policy_loss":-37638.2344,"value_loss":836530.1250,"victims_found":0}
{"type":"episode","ep":21,"mean_return":741.0585,"policy_loss":-42972.3633,"value_loss":828270.1875,"victims_found":0}
{"type":"episode","ep":22,"mean_return":741.0585,"policy_loss":-48866.8125,"value_loss":819184.3750,"victims_found":0}
{"type":"episode","ep":23,"mean_return":741.0585,"policy_loss":-55348.8242,"value_loss":809248.5000,"victims_found":0}
{"type":"episode","ep":24,"mean_return":741.0585,"policy_loss":-62441.0039,"value_loss":798454.4375,"victims_found":0}
{"type":"episode","ep":25,"mean_return":741.0585,"policy_loss":-70163.4062,"value_loss":786799.1875,"victims_found":0}
{"type":"episode","ep":26,"mean_return":741.0585,"policy_loss":-78538.6641,"value_loss":774283.5625,"victims_found":0}
{"type":"episode","ep":27,"mean_return":741.0585,"policy_loss":-87601.0078,"value_loss":760896.3750,"victims_found":0}
{"type":"episode","ep":28,"mean_return":741.0585,"policy_loss":-97379.8828,"value_loss":746647.8750,"victims_found":0}
{"type":"step","ep":29,"step":0,"t":0.00,"coverage":0.0155,"drones":[{"id":0,"x":14.00,"y":14.00,"hdg":0.785,"batt":100.0,"det":false},{"id":1,"x":202.01,"y":10.33,"hdg":3.100,"batt":100.0,"det":false},{"id":2,"x":15.77,"y":204.46,"hdg":-0.765,"batt":100.0,"det":false},{"id":3,"x":213.75,"y":202.93,"hdg":-1.083,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":1,"t":1.00,"coverage":0.0208,"drones":[{"id":0,"x":22.00,"y":14.00,"hdg":0.000,"batt":100.0,"det":false},{"id":1,"x":194.02,"y":10.82,"hdg":3.081,"batt":100.0,"det":false},{"id":2,"x":21.89,"y":199.31,"hdg":-0.700,"batt":100.0,"det":false},{"id":3,"x":218.24,"y":196.31,"hdg":-0.974,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":2,"t":2.00,"coverage":0.0264,"drones":[{"id":0,"x":30.00,"y":14.00,"hdg":0.000,"batt":100.0,"det":false},{"id":1,"x":186.09,"y":11.87,"hdg":3.010,"batt":100.0,"det":false},{"id":2,"x":28.30,"y":194.52,"hdg":-0.641,"batt":100.0,"det":false},{"id":3,"x":223.36,"y":190.17,"hdg":-0.877,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":3,"t":3.00,"coverage":0.0306,"drones":[{"id":0,"x":38.00,"y":14.00,"hdg":0.000,"batt":100.0,"det":false},{"id":1,"x":193.97,"y":13.28,"hdg":0.177,"batt":100.0,"det":false},{"id":2,"x":34.95,"y":190.07,"hdg":-0.590,"batt":100.0,"det":false},{"id":3,"x":228.99,"y":184.48,"hdg":-0.790,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":4,"t":4.00,"coverage":0.0362,"drones":[{"id":0,"x":45.25,"y":17.38,"hdg":0.437,"batt":100.0,"det":false},{"id":1,"x":195.08,"y":21.20,"hdg":1.431,"batt":100.0,"det":false},{"id":2,"x":41.92,"y":186.14,"hdg":-0.513,"batt":100.0,"det":false},{"id":3,"x":235.11,"y":179.33,"hdg":-0.700,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":5,"t":5.00,"coverage":0.0413,"drones":[{"id":0,"x":50.92,"y":23.02,"hdg":0.783,"batt":100.0,"det":false},{"id":1,"x":188.92,"y":26.31,"hdg":2.449,"batt":100.0,"det":false},{"id":2,"x":48.68,"y":181.87,"hdg":-0.564,"batt":100.0,"det":false},{"id":3,"x":240.72,"y":173.62,"hdg":-0.794,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":6,"t":6.00,"coverage":0.0466,"drones":[{"id":0,"x":47.52,"y":30.26,"hdg":2.010,"batt":100.0,"det":false},{"id":1,"x":181.33,"y":28.85,"hdg":2.819,"batt":100.0,"det":false},{"id":2,"x":55.16,"y":177.18,"hdg":-0.626,"batt":100.0,"det":false},{"id":3,"x":245.59,"y":167.28,"hdg":-0.915,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":7,"t":7.00,"coverage":0.0519,"drones":[{"id":0,"x":39.97,"y":32.91,"hdg":2.805,"batt":100.0,"det":false},{"id":1,"x":173.48,"y":30.38,"hdg":2.949,"batt":100.0,"det":false},{"id":2,"x":61.25,"y":172.00,"hdg":-0.705,"batt":100.0,"det":false},{"id":3,"x":249.43,"y":160.26,"hdg":-1.071,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":8,"t":8.00,"coverage":0.0573,"drones":[{"id":0,"x":35.38,"y":39.46,"hdg":2.182,"batt":100.0,"det":false},{"id":1,"x":166.82,"y":34.81,"hdg":2.554,"batt":100.0,"det":false},{"id":2,"x":67.91,"y":167.55,"hdg":-0.589,"batt":100.0,"det":false},{"id":3,"x":254.08,"y":153.75,"hdg":-0.950,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":9,"t":9.00,"coverage":0.0628,"drones":[{"id":0,"x":37.07,"y":47.28,"hdg":1.357,"batt":100.0,"det":false},{"id":1,"x":162.21,"y":41.35,"hdg":2.185,"batt":100.0,"det":false},{"id":2,"x":74.89,"y":163.64,"hdg":-0.511,"batt":100.0,"det":false},{"id":3,"x":259.88,"y":148.23,"hdg":-0.761,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":10,"t":10.00,"coverage":0.0684,"drones":[{"id":0,"x":43.66,"y":51.82,"hdg":0.604,"batt":100.0,"det":false},{"id":1,"x":164.31,"y":49.07,"hdg":1.305,"batt":100.0,"det":false},{"id":2,"x":82.10,"y":160.18,"hdg":-0.447,"batt":100.0,"det":false},{"id":3,"x":266.40,"y":143.60,"hdg":-0.618,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":11,"t":11.00,"coverage":0.0736,"drones":[{"id":0,"x":51.25,"y":54.36,"hdg":0.323,"batt":100.0,"det":false},{"id":1,"x":171.11,"y":53.29,"hdg":0.556,"batt":100.0,"det":false},{"id":2,"x":89.48,"y":157.10,"hdg":-0.396,"batt":100.0,"det":false},{"id":3,"x":273.38,"y":139.70,"hdg":-0.510,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":12,"t":12.00,"coverage":0.0794,"drones":[{"id":0,"x":57.09,"y":59.82,"hdg":0.751,"batt":100.0,"det":false},{"id":1,"x":175.23,"y":60.15,"hdg":1.029,"batt":100.0,"det":false},{"id":2,"x":97.18,"y":154.92,"hdg":-0.276,"batt":100.0,"det":false},{"id":3,"x":280.97,"y":137.16,"hdg":-0.323,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":13,"t":13.00,"coverage":0.0848,"drones":[{"id":0,"x":59.66,"y":67.40,"hdg":1.245,"batt":100.0,"det":false},{"id":1,"x":174.14,"y":68.07,"hdg":1.708,"batt":100.0,"det":false},{"id":2,"x":104.78,"y":152.41,"hdg":-0.318,"batt":100.0,"det":false},{"id":3,"x":288.29,"y":133.94,"hdg":-0.414,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":14,"t":14.00,"coverage":0.0903,"drones":[{"id":0,"x":55.66,"y":74.33,"hdg":2.094,"batt":100.0,"det":false},{"id":1,"x":168.28,"y":73.52,"hdg":2.392,"batt":100.0,"det":false},{"id":2,"x":112.21,"y":149.46,"hdg":-0.378,"batt":100.0,"det":false},{"id":3,"x":294.94,"y":129.49,"hdg":-0.590,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":15,"t":15.00,"coverage":0.0956,"drones":[{"id":0,"x":48.70,"y":78.28,"hdg":2.625,"batt":100.0,"det":false},{"id":1,"x":160.99,"y":76.81,"hdg":2.718,"batt":100.0,"det":false},{"id":2,"x":119.35,"y":145.86,"hdg":-0.468,"batt":100.0,"det":false},{"id":3,"x":299.08,"y":122.64,"hdg":-1.026,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":16,"t":16.00,"coverage":0.1017,"drones":[{"id":0,"x":44.00,"y":84.75,"hdg":2.200,"batt":100.0,"det":false},{"id":1,"x":155.46,"y":82.60,"hdg":2.333,"batt":100.0,"det":false},{"id":2,"x":127.18,"y":144.21,"hdg":-0.207,"batt":100.0,"det":false},{"id":3,"x":306.89,"y":124.40,"hdg":0.221,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":17,"t":17.00,"coverage":0.1072,"drones":[{"id":0,"x":43.61,"y":92.74,"hdg":1.619,"batt":100.0,"det":false},{"id":1,"x":153.09,"y":90.24,"hdg":1.872,"batt":100.0,"det":false},{"id":2,"x":135.08,"y":142.96,"hdg":-0.157,"batt":100.0,"det":false},{"id":3,"x":314.88,"y":124.76,"hdg":0.046,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":18,"t":18.00,"coverage":0.1123,"drones":[{"id":0,"x":48.58,"y":99.01,"hdg":0.900,"batt":100.0,"det":false},{"id":1,"x":156.51,"y":97.47,"hdg":1.129,"batt":100.0,"det":false},{"id":2,"x":143.02,"y":141.96,"hdg":-0.125,"batt":100.0,"det":false},{"id":3,"x":322.88,"y":124.94,"hdg":0.022,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":19,"t":19.00,"coverage":0.1181,"drones":[{"id":0,"x":55.60,"y":102.84,"hdg":0.500,"batt":100.0,"det":false},{"id":1,"x":163.10,"y":102.00,"hdg":0.602,"batt":100.0,"det":false},{"id":2,"x":150.98,"y":141.14,"hdg":-0.103,"batt":100.0,"det":false},{"id":3,"x":330.87,"y":125.06,"hdg":0.014,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":20,"t":20.00,"coverage":0.1234,"drones":[{"id":0,"x":60.71,"y":109.00,"hdg":0.879,"batt":100.0,"det":false},{"id":1,"x":167.56,"y":108.65,"hdg":0.980,"batt":100.0,"det":false},{"id":2,"x":158.95,"y":141.84,"hdg":0.087,"batt":100.0,"det":false},{"id":3,"x":338.22,"y":128.22,"hdg":0.407,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":21,"t":21.00,"coverage":0.1283,"drones":[{"id":0,"x":62.33,"y":116.84,"hdg":1.367,"batt":100.0,"det":false},{"id":1,"x":167.99,"y":116.63,"hdg":1.517,"batt":100.0,"det":false},{"id":2,"x":166.90,"y":142.71,"hdg":0.109,"batt":100.0,"det":false},{"id":3,"x":344.43,"y":133.26,"hdg":0.681,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":22,"t":22.00,"coverage":0.1334,"drones":[{"id":0,"x":58.49,"y":123.86,"hdg":2.071,"batt":100.0,"det":false},{"id":1,"x":163.42,"y":123.20,"hdg":2.179,"batt":100.0,"det":false},{"id":2,"x":174.81,"y":143.90,"hdg":0.149,"batt":100.0,"det":false},{"id":3,"x":343.50,"y":141.21,"hdg":1.688,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":23,"t":23.00,"coverage":0.1372,"drones":[{"id":0,"x":51.86,"y":128.34,"hdg":2.546,"batt":100.0,"det":false},{"id":1,"x":156.63,"y":127.43,"hdg":2.584,"batt":100.0,"det":false},{"id":2,"x":182.56,"y":145.90,"hdg":0.252,"batt":100.0,"det":false},{"id":3,"x":336.16,"y":144.39,"hdg":2.732,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":24,"t":24.00,"coverage":0.1416,"drones":[{"id":0,"x":47.25,"y":134.88,"hdg":2.185,"batt":100.0,"det":false},{"id":1,"x":151.71,"y":133.74,"hdg":2.234,"batt":100.0,"det":false},{"id":2,"x":187.81,"y":151.93,"hdg":0.854,"batt":100.0,"det":false},{"id":3,"x":332.10,"y":151.28,"hdg":2.103,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":25,"t":25.00,"coverage":0.1459,"drones":[{"id":0,"x":46.37,"y":142.83,"hdg":1.682,"batt":100.0,"det":false},{"id":1,"x":150.13,"y":141.58,"hdg":1.769,"batt":100.0,"det":false},{"id":2,"x":194.81,"y":155.81,"hdg":0.507,"batt":100.0,"det":false},{"id":3,"x":334.55,"y":158.90,"hdg":1.259,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":26,"t":26.00,"coverage":0.1502,"drones":[{"id":0,"x":50.50,"y":149.68,"hdg":1.028,"batt":100.0,"det":false},{"id":1,"x":153.66,"y":148.76,"hdg":1.114,"batt":100.0,"det":false},{"id":2,"x":202.36,"y":158.44,"hdg":0.335,"batt":100.0,"det":false},{"id":3,"x":341.26,"y":163.27,"hdg":0.578,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":27,"t":27.00,"coverage":0.1539,"drones":[{"id":0,"x":57.11,"y":154.18,"hdg":0.597,"batt":100.0,"det":false},{"id":1,"x":160.06,"y":153.56,"hdg":0.643,"batt":100.0,"det":false},{"id":2,"x":210.13,"y":160.36,"hdg":0.242,"batt":100.0,"det":false},{"id":3,"x":348.85,"y":165.77,"hdg":0.319,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":28,"t":28.00,"coverage":0.1578,"drones":[{"id":0,"x":61.84,"y":160.63,"hdg":0.939,"batt":100.0,"det":false},{"id":1,"x":164.52,"y":160.20,"hdg":0.979,"batt":100.0,"det":false},{"id":2,"x":217.10,"y":164.29,"hdg":0.514,"batt":100.0,"det":false},{"id":3,"x":354.80,"y":171.13,"hdg":0.733,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":29,"t":29.00,"coverage":0.1606,"drones":[{"id":0,"x":63.14,"y":168.53,"hdg":1.407,"batt":100.0,"det":false},{"id":1,"x":165.40,"y":168.15,"hdg":1.461,"batt":100.0,"det":false},{"id":2,"x":223.15,"y":169.52,"hdg":0.713,"batt":100.0,"det":false},{"id":3,"x":357.71,"y":178.58,"hdg":1.198,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":30,"t":30.00,"coverage":0.1634,"drones":[{"id":0,"x":59.51,"y":175.65,"hdg":2.042,"batt":100.0,"det":false},{"id":1,"x":161.49,"y":175.13,"hdg":2.081,"batt":100.0,"det":false},{"id":2,"x":226.53,"y":176.77,"hdg":1.135,"batt":100.0,"det":false},{"id":3,"x":354.14,"y":185.73,"hdg":2.034,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":31,"t":31.00,"coverage":0.1661,"drones":[{"id":0,"x":53.13,"y":180.47,"hdg":2.495,"batt":100.0,"det":false},{"id":1,"x":155.05,"y":179.87,"hdg":2.508,"batt":100.0,"det":false},{"id":2,"x":223.70,"y":184.26,"hdg":1.931,"batt":100.0,"det":false},{"id":3,"x":347.29,"y":189.87,"hdg":2.598,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":32,"t":32.00,"coverage":0.1692,"drones":[{"id":0,"x":48.63,"y":187.09,"hdg":2.168,"batt":100.0,"det":false},{"id":1,"x":150.43,"y":186.40,"hdg":2.186,"batt":100.0,"det":false},{"id":2,"x":222.28,"y":192.13,"hdg":1.749,"batt":100.0,"det":false},{"id":3,"x":342.74,"y":196.45,"hdg":2.176,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":33,"t":33.00,"coverage":0.1741,"drones":[{"id":0,"x":47.59,"y":195.02,"hdg":1.700,"batt":100.0,"det":false},{"id":1,"x":149.14,"y":194.30,"hdg":1.733,"batt":100.0,"det":false},{"id":2,"x":225.44,"y":199.48,"hdg":1.165,"batt":100.0,"det":false},{"id":3,"x":342.60,"y":204.45,"hdg":1.589,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":34,"t":34.00,"coverage":0.1792,"drones":[{"id":0,"x":51.29,"y":202.12,"hdg":1.090,"batt":100.0,"det":false},{"id":1,"x":152.59,"y":201.52,"hdg":1.125,"batt":100.0,"det":false},{"id":2,"x":231.49,"y":204.71,"hdg":0.713,"batt":100.0,"det":false},{"id":3,"x":347.66,"y":210.64,"hdg":0.886,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":35,"t":35.00,"coverage":0.1847,"drones":[{"id":0,"x":57.64,"y":206.99,"hdg":0.655,"batt":100.0,"det":false},{"id":1,"x":158.82,"y":206.53,"hdg":0.677,"batt":100.0,"det":false},{"id":2,"x":238.65,"y":208.29,"hdg":0.464,"batt":100.0,"det":false},{"id":3,"x":354.69,"y":214.46,"hdg":0.498,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":36,"t":36.00,"coverage":0.1902,"drones":[{"id":0,"x":62.15,"y":213.59,"hdg":0.971,"batt":100.0,"det":false},{"id":1,"x":163.22,"y":213.21,"hdg":0.988,"batt":100.0,"det":false},{"id":2,"x":244.38,"y":213.87,"hdg":0.771,"batt":100.0,"det":false},{"id":3,"x":359.83,"y":220.59,"hdg":0.872,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":37,"t":37.00,"coverage":0.1952,"drones":[{"id":0,"x":63.34,"y":221.50,"hdg":1.422,"batt":100.0,"det":false},{"id":1,"x":164.24,"y":221.14,"hdg":1.443,"batt":100.0,"det":false},{"id":2,"x":247.91,"y":221.05,"hdg":1.114,"batt":100.0,"det":false},{"id":3,"x":361.58,"y":228.40,"hdg":1.351,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":38,"t":38.00,"coverage":0.2009,"drones":[{"id":0,"x":59.88,"y":228.72,"hdg":2.018,"batt":100.0,"det":false},{"id":1,"x":160.69,"y":228.31,"hdg":2.031,"batt":100.0,"det":false},{"id":2,"x":246.87,"y":228.98,"hdg":1.702,"batt":100.0,"det":false},{"id":3,"x":357.89,"y":235.49,"hdg":2.051,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":39,"t":39.00,"coverage":0.2064,"drones":[{"id":0,"x":53.67,"y":233.76,"hdg":2.459,"batt":100.0,"det":false},{"id":1,"x":154.46,"y":233.34,"hdg":2.462,"batt":100.0,"det":false},{"id":2,"x":241.56,"y":234.96,"hdg":2.297,"batt":100.0,"det":false},{"id":3,"x":351.32,"y":240.06,"hdg":2.534,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":40,"t":40.00,"coverage":0.2119,"drones":[{"id":0,"x":49.27,"y":240.44,"hdg":2.153,"batt":100.0,"det":false},{"id":1,"x":150.02,"y":239.99,"hdg":2.159,"batt":100.0,"det":false},{"id":2,"x":238.17,"y":242.21,"hdg":2.008,"batt":100.0,"det":false},{"id":3,"x":346.77,"y":246.64,"hdg":2.176,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":41,"t":41.00,"coverage":0.2177,"drones":[{"id":0,"x":48.19,"y":248.37,"hdg":1.706,"batt":100.0,"det":false},{"id":1,"x":148.85,"y":247.90,"hdg":1.718,"batt":100.0,"det":false},{"id":2,"x":238.76,"y":250.19,"hdg":1.497,"batt":100.0,"det":false},{"id":3,"x":345.97,"y":254.60,"hdg":1.671,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":42,"t":42.00,"coverage":0.2233,"drones":[{"id":0,"x":51.64,"y":255.59,"hdg":1.125,"batt":100.0,"det":false},{"id":1,"x":152.19,"y":255.17,"hdg":1.141,"batt":100.0,"det":false},{"id":2,"x":243.41,"y":256.71,"hdg":0.951,"batt":100.0,"det":false},{"id":3,"x":350.13,"y":261.43,"hdg":1.023,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":43,"t":43.00,"coverage":0.2289,"drones":[{"id":0,"x":57.80,"y":260.69,"hdg":0.692,"batt":100.0,"det":false},{"id":1,"x":158.29,"y":260.35,"hdg":0.703,"batt":100.0,"det":false},{"id":2,"x":250.01,"y":261.22,"hdg":0.599,"batt":100.0,"det":false},{"id":3,"x":356.75,"y":265.93,"hdg":0.598,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":44,"t":44.00,"coverage":0.2338,"drones":[{"id":0,"x":62.19,"y":267.38,"hdg":0.990,"batt":100.0,"det":false},{"id":1,"x":162.62,"y":267.07,"hdg":0.999,"batt":100.0,"det":false},{"id":2,"x":254.97,"y":267.50,"hdg":0.902,"batt":100.0,"det":false},{"id":3,"x":361.49,"y":272.37,"hdg":0.936,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":45,"t":45.00,"coverage":0.2397,"drones":[{"id":0,"x":63.32,"y":275.30,"hdg":1.429,"batt":100.0,"det":false},{"id":1,"x":163.69,"y":275.00,"hdg":1.437,"batt":100.0,"det":false},{"id":2,"x":257.10,"y":275.21,"hdg":1.302,"batt":100.0,"det":false},{"id":3,"x":362.84,"y":280.26,"hdg":1.401,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":46,"t":46.00,"coverage":0.2452,"drones":[{"id":0,"x":60.00,"y":282.58,"hdg":1.999,"batt":100.0,"det":false},{"id":1,"x":160.33,"y":282.27,"hdg":2.003,"batt":100.0,"det":false},{"id":2,"x":254.64,"y":282.82,"hdg":1.883,"batt":100.0,"det":false},{"id":3,"x":359.27,"y":287.41,"hdg":2.034,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":47,"t":47.00,"coverage":0.2500,"drones":[{"id":0,"x":53.92,"y":287.79,"hdg":2.433,"batt":100.0,"det":false},{"id":1,"x":154.26,"y":287.47,"hdg":2.433,"batt":100.0,"det":false},{"id":2,"x":248.88,"y":288.37,"hdg":2.375,"batt":100.0,"det":false},{"id":3,"x":352.91,"y":292.27,"hdg":2.489,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":48,"t":48.00,"coverage":0.2559,"drones":[{"id":0,"x":49.60,"y":294.52,"hdg":2.141,"batt":100.0,"det":false},{"id":1,"x":149.93,"y":294.20,"hdg":2.143,"batt":100.0,"det":false},{"id":2,"x":244.94,"y":295.34,"hdg":2.086,"batt":100.0,"det":false},{"id":3,"x":348.44,"y":298.90,"hdg":2.164,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":49,"t":49.00,"coverage":0.2614,"drones":[{"id":0,"x":48.52,"y":302.45,"hdg":1.707,"batt":100.0,"det":false},{"id":1,"x":148.80,"y":302.12,"hdg":1.712,"batt":100.0,"det":false},{"id":2,"x":244.51,"y":303.33,"hdg":1.625,"batt":100.0,"det":false},{"id":3,"x":347.44,"y":306.84,"hdg":1.696,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":50,"t":50.00,"coverage":0.2672,"drones":[{"id":0,"x":51.81,"y":309.74,"hdg":1.147,"batt":100.0,"det":false},{"id":1,"x":152.04,"y":309.44,"hdg":1.154,"batt":100.0,"det":false},{"id":2,"x":248.34,"y":310.35,"hdg":1.071,"batt":100.0,"det":false},{"id":3,"x":351.14,"y":313.93,"hdg":1.089,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":51,"t":51.00,"coverage":0.2727,"drones":[{"id":0,"x":57.84,"y":314.99,"hdg":0.717,"batt":100.0,"det":false},{"id":1,"x":158.04,"y":314.73,"hdg":0.723,"batt":100.0,"det":false},{"id":2,"x":254.60,"y":315.34,"hdg":0.674,"batt":100.0,"det":false},{"id":3,"x":357.48,"y":318.81,"hdg":0.656,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":52,"t":52.00,"coverage":0.2781,"drones":[{"id":0,"x":62.14,"y":321.74,"hdg":1.003,"batt":100.0,"det":false},{"id":1,"x":162.31,"y":321.49,"hdg":1.007,"batt":100.0,"det":false},{"id":2,"x":259.15,"y":321.92,"hdg":0.965,"batt":100.0,"det":false},{"id":3,"x":362.00,"y":325.41,"hdg":0.971,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":53,"t":53.00,"coverage":0.2833,"drones":[{"id":0,"x":63.25,"y":329.66,"hdg":1.432,"batt":100.0,"det":false},{"id":1,"x":163.39,"y":329.42,"hdg":1.436,"batt":100.0,"det":false},{"id":2,"x":260.67,"y":329.77,"hdg":1.380,"batt":100.0,"det":false},{"id":3,"x":363.21,"y":333.32,"hdg":1.420,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":54,"t":54.00,"coverage":0.2891,"drones":[{"id":0,"x":60.03,"y":336.99,"hdg":1.985,"batt":100.0,"det":false},{"id":1,"x":160.17,"y":336.74,"hdg":1.986,"batt":100.0,"det":false},{"id":2,"x":257.78,"y":337.23,"hdg":1.940,"batt":100.0,"det":false},{"id":3,"x":359.77,"y":340.55,"hdg":2.014,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":55,"t":55.00,"coverage":0.2944,"drones":[{"id":0,"x":54.06,"y":342.31,"hdg":2.414,"batt":100.0,"det":false},{"id":1,"x":154.20,"y":342.07,"hdg":2.412,"batt":100.0,"det":false},{"id":2,"x":251.93,"y":342.68,"hdg":2.392,"batt":100.0,"det":false},{"id":3,"x":353.58,"y":345.61,"hdg":2.456,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":56,"t":56.00,"coverage":0.2998,"drones":[{"id":0,"x":49.49,"y":348.88,"hdg":2.178,"batt":100.0,"det":false},{"id":1,"x":149.64,"y":348.64,"hdg":2.177,"batt":100.0,"det":false},{"id":2,"x":247.51,"y":349.35,"hdg":2.155,"batt":100.0,"det":false},{"id":3,"x":348.88,"y":352.08,"hdg":2.200,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":57,"t":57.00,"coverage":0.3050,"drones":[{"id":0,"x":48.33,"y":356.79,"hdg":1.716,"batt":100.0,"det":false},{"id":1,"x":148.46,"y":356.56,"hdg":1.718,"batt":100.0,"det":false},{"id":2,"x":246.65,"y":357.31,"hdg":1.679,"batt":100.0,"det":false},{"id":3,"x":347.74,"y":360.00,"hdg":1.713,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":58,"t":58.00,"coverage":0.3109,"drones":[{"id":0,"x":51.92,"y":363.95,"hdg":1.106,"batt":100.0,"det":false},{"id":1,"x":152.02,"y":363.73,"hdg":1.111,"batt":100.0,"det":false},{"id":2,"x":250.49,"y":364.33,"hdg":1.070,"batt":100.0,"det":false},{"id":3,"x":351.63,"y":366.99,"hdg":1.063,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":59,"t":59.00,"coverage":0.3159,"drones":[{"id":0,"x":58.22,"y":368.88,"hdg":0.664,"batt":100.0,"det":false},{"id":1,"x":158.30,"y":368.68,"hdg":0.668,"batt":100.0,"det":false},{"id":2,"x":256.88,"y":369.13,"hdg":0.644,"batt":100.0,"det":false},{"id":3,"x":358.15,"y":371.63,"hdg":0.619,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":60,"t":60.00,"coverage":0.3172,"drones":[{"id":0,"x":57.23,"y":360.94,"hdg":-1.695,"batt":100.0,"det":false},{"id":1,"x":157.31,"y":360.74,"hdg":-1.695,"batt":100.0,"det":false},{"id":2,"x":255.92,"y":361.19,"hdg":-1.691,"batt":100.0,"det":false},{"id":3,"x":357.17,"y":363.69,"hdg":-1.694,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":61,"t":61.00,"coverage":0.3177,"drones":[{"id":0,"x":56.88,"y":352.94,"hdg":-1.615,"batt":100.0,"det":false},{"id":1,"x":156.95,"y":352.75,"hdg":-1.615,"batt":100.0,"det":false},{"id":2,"x":255.60,"y":353.19,"hdg":-1.611,"batt":100.0,"det":false},{"id":3,"x":356.82,"y":355.70,"hdg":-1.614,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":62,"t":62.00,"coverage":0.3177,"drones":[{"id":0,"x":57.19,"y":344.95,"hdg":-1.532,"batt":100.0,"det":false},{"id":1,"x":157.26,"y":344.76,"hdg":-1.532,"batt":100.0,"det":false},{"id":2,"x":255.94,"y":345.20,"hdg":-1.528,"batt":100.0,"det":false},{"id":3,"x":357.13,"y":347.71,"hdg":-1.532,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":63,"t":63.00,"coverage":0.3178,"drones":[{"id":0,"x":58.17,"y":337.01,"hdg":-1.448,"batt":100.0,"det":false},{"id":1,"x":158.24,"y":336.82,"hdg":-1.448,"batt":100.0,"det":false},{"id":2,"x":256.95,"y":337.26,"hdg":-1.444,"batt":100.0,"det":false},{"id":3,"x":358.10,"y":339.76,"hdg":-1.449,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":64,"t":64.00,"coverage":0.3181,"drones":[{"id":0,"x":59.24,"y":329.08,"hdg":-1.437,"batt":100.0,"det":false},{"id":1,"x":159.31,"y":328.89,"hdg":-1.437,"batt":100.0,"det":false},{"id":2,"x":258.05,"y":329.34,"hdg":-1.433,"batt":100.0,"det":false},{"id":3,"x":359.16,"y":331.84,"hdg":-1.438,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":65,"t":65.00,"coverage":0.3181,"drones":[{"id":0,"x":59.54,"y":321.09,"hdg":-1.533,"batt":100.0,"det":false},{"id":1,"x":159.61,"y":320.89,"hdg":-1.534,"batt":100.0,"det":false},{"id":2,"x":258.39,"y":321.35,"hdg":-1.529,"batt":100.0,"det":false},{"id":3,"x":359.46,"y":323.84,"hdg":-1.533,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":66,"t":66.00,"coverage":0.3192,"drones":[{"id":0,"x":59.04,"y":313.10,"hdg":-1.634,"batt":100.0,"det":false},{"id":1,"x":159.10,"y":312.91,"hdg":-1.634,"batt":100.0,"det":false},{"id":2,"x":257.92,"y":313.36,"hdg":-1.629,"batt":100.0,"det":false},{"id":3,"x":358.97,"y":315.86,"hdg":-1.633,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":67,"t":67.00,"coverage":0.3202,"drones":[{"id":0,"x":57.73,"y":305.21,"hdg":-1.735,"batt":100.0,"det":false},{"id":1,"x":157.79,"y":305.02,"hdg":-1.736,"batt":100.0,"det":false},{"id":2,"x":256.64,"y":305.46,"hdg":-1.731,"batt":100.0,"det":false},{"id":3,"x":357.67,"y":307.96,"hdg":-1.734,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":68,"t":68.00,"coverage":0.3203,"drones":[{"id":0,"x":56.26,"y":297.35,"hdg":-1.755,"batt":100.0,"det":false},{"id":1,"x":156.32,"y":297.15,"hdg":-1.755,"batt":100.0,"det":false},{"id":2,"x":255.21,"y":297.59,"hdg":-1.750,"batt":100.0,"det":false},{"id":3,"x":356.23,"y":300.09,"hdg":-1.752,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":69,"t":69.00,"coverage":0.3203,"drones":[{"id":0,"x":55.76,"y":289.36,"hdg":-1.633,"batt":100.0,"det":false},{"id":1,"x":155.82,"y":289.17,"hdg":-1.634,"batt":100.0,"det":false},{"id":2,"x":254.75,"y":289.60,"hdg":-1.629,"batt":100.0,"det":false},{"id":3,"x":355.73,"y":292.11,"hdg":-1.633,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":70,"t":70.00,"coverage":0.3209,"drones":[{"id":0,"x":56.28,"y":281.38,"hdg":-1.506,"batt":100.0,"det":false},{"id":1,"x":156.34,"y":281.19,"hdg":-1.506,"batt":100.0,"det":false},{"id":2,"x":255.30,"y":281.62,"hdg":-1.501,"batt":100.0,"det":false},{"id":3,"x":356.25,"y":284.13,"hdg":-1.507,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":71,"t":71.00,"coverage":0.3209,"drones":[{"id":0,"x":57.83,"y":273.53,"hdg":-1.376,"batt":100.0,"det":false},{"id":1,"x":157.88,"y":273.34,"hdg":-1.376,"batt":100.0,"det":false},{"id":2,"x":256.89,"y":273.78,"hdg":-1.372,"batt":100.0,"det":false},{"id":3,"x":357.78,"y":276.27,"hdg":-1.378,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":72,"t":72.00,"coverage":0.3212,"drones":[{"id":0,"x":59.61,"y":265.73,"hdg":-1.346,"batt":100.0,"det":false},{"id":1,"x":159.67,"y":265.54,"hdg":-1.346,"batt":100.0,"det":false},{"id":2,"x":258.71,"y":265.99,"hdg":-1.341,"batt":100.0,"det":false},{"id":3,"x":359.54,"y":268.47,"hdg":-1.349,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":73,"t":73.00,"coverage":0.3237,"drones":[{"id":0,"x":60.11,"y":257.75,"hdg":-1.509,"batt":100.0,"det":false},{"id":1,"x":160.16,"y":257.55,"hdg":-1.509,"batt":100.0,"det":false},{"id":2,"x":259.24,"y":258.01,"hdg":-1.504,"batt":100.0,"det":false},{"id":3,"x":360.03,"y":260.48,"hdg":-1.510,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":74,"t":74.00,"coverage":0.3253,"drones":[{"id":0,"x":59.21,"y":249.80,"hdg":-1.684,"batt":100.0,"det":false},{"id":1,"x":159.26,"y":249.60,"hdg":-1.684,"batt":100.0,"det":false},{"id":2,"x":258.39,"y":250.06,"hdg":-1.678,"batt":100.0,"det":false},{"id":3,"x":359.15,"y":252.53,"hdg":-1.681,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":75,"t":75.00,"coverage":0.3256,"drones":[{"id":0,"x":56.92,"y":242.13,"hdg":-1.860,"batt":100.0,"det":false},{"id":1,"x":156.97,"y":241.94,"hdg":-1.861,"batt":100.0,"det":false},{"id":2,"x":256.15,"y":242.38,"hdg":-1.855,"batt":100.0,"det":false},{"id":3,"x":356.90,"y":244.85,"hdg":-1.855,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":76,"t":76.00,"coverage":0.3258,"drones":[{"id":0,"x":54.15,"y":234.63,"hdg":-1.925,"batt":100.0,"det":false},{"id":1,"x":154.19,"y":234.44,"hdg":-1.926,"batt":100.0,"det":false},{"id":2,"x":253.42,"y":234.85,"hdg":-1.918,"batt":100.0,"det":false},{"id":3,"x":354.19,"y":237.33,"hdg":-1.917,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":77,"t":77.00,"coverage":0.3269,"drones":[{"id":0,"x":53.26,"y":226.68,"hdg":-1.682,"batt":100.0,"det":false},{"id":1,"x":153.29,"y":226.49,"hdg":-1.683,"batt":100.0,"det":false},{"id":2,"x":252.59,"y":226.90,"hdg":-1.675,"batt":100.0,"det":false},{"id":3,"x":353.32,"y":229.38,"hdg":-1.680,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":78,"t":78.00,"coverage":0.3280,"drones":[{"id":0,"x":54.57,"y":218.79,"hdg":-1.406,"batt":100.0,"det":false},{"id":1,"x":154.61,"y":218.60,"hdg":-1.406,"batt":100.0,"det":false},{"id":2,"x":253.95,"y":219.02,"hdg":-1.400,"batt":100.0,"det":false},{"id":3,"x":354.59,"y":221.48,"hdg":-1.411,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":79,"t":79.00,"coverage":0.3292,"drones":[{"id":0,"x":57.96,"y":211.54,"hdg":-1.133,"batt":100.0,"det":false},{"id":1,"x":158.00,"y":211.35,"hdg":-1.133,"batt":100.0,"det":false},{"id":2,"x":257.37,"y":211.78,"hdg":-1.128,"batt":100.0,"det":false},{"id":3,"x":357.90,"y":214.20,"hdg":-1.144,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":80,"t":80.00,"coverage":0.3330,"drones":[{"id":0,"x":62.53,"y":204.97,"hdg":-0.963,"batt":100.0,"det":false},{"id":1,"x":162.58,"y":204.79,"hdg":-0.962,"batt":100.0,"det":false},{"id":2,"x":261.97,"y":205.24,"hdg":-0.958,"batt":100.0,"det":false},{"id":3,"x":362.34,"y":207.54,"hdg":-0.983,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":81,"t":81.00,"coverage":0.3355,"drones":[{"id":0,"x":63.69,"y":197.06,"hdg":-1.425,"batt":100.0,"det":false},{"id":1,"x":163.73,"y":196.87,"hdg":-1.426,"batt":100.0,"det":false},{"id":2,"x":263.21,"y":197.34,"hdg":-1.415,"batt":100.0,"det":false},{"id":3,"x":363.47,"y":199.62,"hdg":-1.429,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":82,"t":82.00,"coverage":0.3370,"drones":[{"id":0,"x":60.09,"y":189.91,"hdg":-2.037,"batt":100.0,"det":false},{"id":1,"x":160.12,"y":189.74,"hdg":-2.040,"batt":100.0,"det":false},{"id":2,"x":259.69,"y":190.15,"hdg":-2.026,"batt":100.0,"det":false},{"id":3,"x":360.06,"y":192.38,"hdg":-2.011,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":83,"t":83.00,"coverage":0.3372,"drones":[{"id":0,"x":53.78,"y":185.00,"hdg":-2.480,"batt":100.0,"det":false},{"id":1,"x":153.79,"y":184.84,"hdg":-2.482,"batt":100.0,"det":false},{"id":2,"x":253.42,"y":185.19,"hdg":-2.472,"batt":100.0,"det":false},{"id":3,"x":353.92,"y":187.26,"hdg":-2.447,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":84,"t":84.00,"coverage":0.3381,"drones":[{"id":0,"x":45.81,"y":184.40,"hdg":-3.066,"batt":100.0,"det":false},{"id":1,"x":145.81,"y":184.27,"hdg":-3.070,"batt":100.0,"det":false},{"id":2,"x":245.45,"y":184.54,"hdg":-3.061,"batt":100.0,"det":false},{"id":3,"x":345.99,"y":186.21,"hdg":-3.011,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":85,"t":85.00,"coverage":0.3391,"drones":[{"id":0,"x":42.00,"y":182.00,"hdg":-2.580,"batt":100.0,"det":false},{"id":1,"x":142.00,"y":182.00,"hdg":-2.605,"batt":100.0,"det":false},{"id":2,"x":242.00,"y":182.00,"hdg":-2.506,"batt":100.0,"det":false},{"id":3,"x":342.00,"y":182.00,"hdg":-2.328,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":86,"t":86.00,"coverage":0.3391,"drones":[{"id":0,"x":50.00,"y":182.00,"hdg":0.000,"batt":100.0,"det":false},{"id":1,"x":150.00,"y":182.00,"hdg":0.000,"batt":100.0,"det":false},{"id":2,"x":250.00,"y":182.00,"hdg":0.000,"batt":100.0,"det":false},{"id":3,"x":350.00,"y":182.00,"hdg":0.000,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":87,"t":87.00,"coverage":0.3395,"drones":[{"id":0,"x":58.00,"y":182.00,"hdg":0.000,"batt":100.0,"det":false},{"id":1,"x":158.00,"y":182.00,"hdg":0.000,"batt":100.0,"det":false},{"id":2,"x":258.00,"y":182.00,"hdg":0.000,"batt":100.0,"det":false},{"id":3,"x":358.00,"y":182.00,"hdg":0.000,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":88,"t":88.00,"coverage":0.3416,"drones":[{"id":0,"x":64.55,"y":186.59,"hdg":0.611,"batt":100.0,"det":false},{"id":1,"x":164.55,"y":186.59,"hdg":0.611,"batt":100.0,"det":false},{"id":2,"x":264.55,"y":186.59,"hdg":0.611,"batt":100.0,"det":false},{"id":3,"x":364.55,"y":186.59,"hdg":0.611,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":89,"t":89.00,"coverage":0.3431,"drones":[{"id":0,"x":66.37,"y":194.38,"hdg":1.342,"batt":100.0,"det":false},{"id":1,"x":166.37,"y":194.38,"hdg":1.342,"batt":100.0,"det":false},{"id":2,"x":266.37,"y":194.38,"hdg":1.342,"batt":100.0,"det":false},{"id":3,"x":366.37,"y":194.38,"hdg":1.342,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":90,"t":90.00,"coverage":0.3431,"drones":[{"id":0,"x":59.63,"y":198.70,"hdg":2.572,"batt":100.0,"det":false},{"id":1,"x":159.63,"y":198.70,"hdg":2.572,"batt":100.0,"det":false},{"id":2,"x":259.63,"y":198.70,"hdg":2.572,"batt":100.0,"det":false},{"id":3,"x":359.63,"y":198.70,"hdg":2.572,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":91,"t":91.00,"coverage":0.3431,"drones":[{"id":0,"x":51.87,"y":200.62,"hdg":2.899,"batt":100.0,"det":false},{"id":1,"x":151.87,"y":200.62,"hdg":2.899,"batt":100.0,"det":false},{"id":2,"x":251.87,"y":200.62,"hdg":2.899,"batt":100.0,"det":false},{"id":3,"x":351.87,"y":200.62,"hdg":2.899,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":92,"t":92.00,"coverage":0.3441,"drones":[{"id":0,"x":46.17,"y":206.24,"hdg":2.363,"batt":100.0,"det":false},{"id":1,"x":146.17,"y":206.24,"hdg":2.363,"batt":100.0,"det":false},{"id":2,"x":246.17,"y":206.24,"hdg":2.363,"batt":100.0,"det":false},{"id":3,"x":346.17,"y":206.24,"hdg":2.363,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":93,"t":93.00,"coverage":0.3456,"drones":[{"id":0,"x":45.13,"y":214.17,"hdg":1.701,"batt":100.0,"det":false},{"id":1,"x":145.13,"y":214.17,"hdg":1.701,"batt":100.0,"det":false},{"id":2,"x":245.13,"y":214.17,"hdg":1.701,"batt":100.0,"det":false},{"id":3,"x":345.13,"y":214.17,"hdg":1.701,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":94,"t":94.00,"coverage":0.3456,"drones":[{"id":0,"x":50.91,"y":219.71,"hdg":0.764,"batt":100.0,"det":false},{"id":1,"x":150.91,"y":219.71,"hdg":0.764,"batt":100.0,"det":false},{"id":2,"x":250.91,"y":219.71,"hdg":0.764,"batt":100.0,"det":false},{"id":3,"x":350.91,"y":219.71,"hdg":0.764,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":95,"t":95.00,"coverage":0.3459,"drones":[{"id":0,"x":58.36,"y":222.60,"hdg":0.371,"batt":100.0,"det":false},{"id":1,"x":158.36,"y":222.60,"hdg":0.371,"batt":100.0,"det":false},{"id":2,"x":258.36,"y":222.60,"hdg":0.371,"batt":100.0,"det":false},{"id":3,"x":358.36,"y":222.60,"hdg":0.371,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":96,"t":96.00,"coverage":0.3477,"drones":[{"id":0,"x":63.76,"y":228.51,"hdg":0.831,"batt":100.0,"det":false},{"id":1,"x":163.76,"y":228.51,"hdg":0.831,"batt":100.0,"det":false},{"id":2,"x":263.76,"y":228.51,"hdg":0.831,"batt":100.0,"det":false},{"id":3,"x":363.76,"y":228.51,"hdg":0.831,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":97,"t":97.00,"coverage":0.3505,"drones":[{"id":0,"x":65.07,"y":236.40,"hdg":1.406,"batt":100.0,"det":false},{"id":1,"x":165.07,"y":236.40,"hdg":1.406,"batt":100.0,"det":false},{"id":2,"x":265.07,"y":236.40,"hdg":1.406,"batt":100.0,"det":false},{"id":3,"x":365.07,"y":236.40,"hdg":1.406,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":98,"t":98.00,"coverage":0.3505,"drones":[{"id":0,"x":60.15,"y":242.71,"hdg":2.233,"batt":100.0,"det":false},{"id":1,"x":160.15,"y":242.71,"hdg":2.233,"batt":100.0,"det":false},{"id":2,"x":260.15,"y":242.71,"hdg":2.233,"batt":100.0,"det":false},{"id":3,"x":360.15,"y":242.71,"hdg":2.233,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":99,"t":99.00,"coverage":0.3505,"drones":[{"id":0,"x":53.01,"y":246.32,"hdg":2.674,"batt":100.0,"det":false},{"id":1,"x":153.01,"y":246.32,"hdg":2.674,"batt":100.0,"det":false},{"id":2,"x":253.01,"y":246.32,"hdg":2.674,"batt":100.0,"det":false},{"id":3,"x":353.01,"y":246.32,"hdg":2.674,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":100,"t":100.00,"coverage":0.3508,"drones":[{"id":0,"x":47.95,"y":252.51,"hdg":2.256,"batt":100.0,"det":false},{"id":1,"x":147.95,"y":252.51,"hdg":2.256,"batt":100.0,"det":false},{"id":2,"x":247.95,"y":252.51,"hdg":2.256,"batt":100.0,"det":false},{"id":3,"x":347.95,"y":252.51,"hdg":2.256,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":101,"t":101.00,"coverage":0.3522,"drones":[{"id":0,"x":46.81,"y":260.43,"hdg":1.713,"batt":100.0,"det":false},{"id":1,"x":146.81,"y":260.43,"hdg":1.713,"batt":100.0,"det":false},{"id":2,"x":246.81,"y":260.43,"hdg":1.713,"batt":100.0,"det":false},{"id":3,"x":346.81,"y":260.43,"hdg":1.713,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":102,"t":102.00,"coverage":0.3527,"drones":[{"id":0,"x":51.36,"y":267.01,"hdg":0.966,"batt":100.0,"det":false},{"id":1,"x":151.36,"y":267.01,"hdg":0.966,"batt":100.0,"det":false},{"id":2,"x":251.36,"y":267.01,"hdg":0.966,"batt":100.0,"det":false},{"id":3,"x":351.36,"y":267.01,"hdg":0.966,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":103,"t":103.00,"coverage":0.3527,"drones":[{"id":0,"x":58.28,"y":271.02,"hdg":0.525,"batt":100.0,"det":false},{"id":1,"x":158.28,"y":271.02,"hdg":0.525,"batt":100.0,"det":false},{"id":2,"x":258.28,"y":271.02,"hdg":0.525,"batt":100.0,"det":false},{"id":3,"x":358.28,"y":271.02,"hdg":0.525,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":104,"t":104.00,"coverage":0.3533,"drones":[{"id":0,"x":63.20,"y":277.33,"hdg":0.909,"batt":100.0,"det":false},{"id":1,"x":163.20,"y":277.33,"hdg":0.909,"batt":100.0,"det":false},{"id":2,"x":263.20,"y":277.33,"hdg":0.909,"batt":100.0,"det":false},{"id":3,"x":363.20,"y":277.33,"hdg":0.909,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":105,"t":105.00,"coverage":0.3552,"drones":[{"id":0,"x":64.40,"y":285.24,"hdg":1.420,"batt":100.0,"det":false},{"id":1,"x":164.40,"y":285.24,"hdg":1.420,"batt":100.0,"det":false},{"id":2,"x":264.40,"y":285.24,"hdg":1.420,"batt":100.0,"det":false},{"id":3,"x":364.40,"y":285.24,"hdg":1.420,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":106,"t":106.00,"coverage":0.3558,"drones":[{"id":0,"x":60.24,"y":292.07,"hdg":2.118,"batt":100.0,"det":false},{"id":1,"x":160.24,"y":292.07,"hdg":2.118,"batt":100.0,"det":false},{"id":2,"x":260.24,"y":292.07,"hdg":2.118,"batt":100.0,"det":false},{"id":3,"x":360.24,"y":292.07,"hdg":2.118,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":107,"t":107.00,"coverage":0.3558,"drones":[{"id":0,"x":53.52,"y":296.41,"hdg":2.567,"batt":100.0,"det":false},{"id":1,"x":153.52,"y":296.41,"hdg":2.567,"batt":100.0,"det":false},{"id":2,"x":253.52,"y":296.41,"hdg":2.567,"batt":100.0,"det":false},{"id":3,"x":353.52,"y":296.41,"hdg":2.567,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":108,"t":108.00,"coverage":0.3558,"drones":[{"id":0,"x":48.78,"y":302.85,"hdg":2.206,"batt":100.0,"det":false},{"id":1,"x":148.78,"y":302.85,"hdg":2.206,"batt":100.0,"det":false},{"id":2,"x":248.78,"y":302.85,"hdg":2.206,"batt":100.0,"det":false},{"id":3,"x":348.78,"y":302.85,"hdg":2.206,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":109,"t":109.00,"coverage":0.3569,"drones":[{"id":0,"x":47.64,"y":310.77,"hdg":1.714,"batt":100.0,"det":false},{"id":1,"x":147.64,"y":310.77,"hdg":1.714,"batt":100.0,"det":false},{"id":2,"x":247.64,"y":310.77,"hdg":1.714,"batt":100.0,"det":false},{"id":3,"x":347.64,"y":310.77,"hdg":1.714,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":110,"t":110.00,"coverage":0.3575,"drones":[{"id":0,"x":51.60,"y":317.72,"hdg":1.053,"batt":100.0,"det":false},{"id":1,"x":151.60,"y":317.72,"hdg":1.053,"batt":100.0,"det":false},{"id":2,"x":251.60,"y":317.72,"hdg":1.053,"batt":100.0,"det":false},{"id":3,"x":351.60,"y":317.72,"hdg":1.053,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":111,"t":111.00,"coverage":0.3575,"drones":[{"id":0,"x":58.17,"y":322.29,"hdg":0.608,"batt":100.0,"det":false},{"id":1,"x":158.17,"y":322.29,"hdg":0.608,"batt":100.0,"det":false},{"id":2,"x":258.17,"y":322.29,"hdg":0.608,"batt":100.0,"det":false},{"id":3,"x":358.17,"y":322.29,"hdg":0.608,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":112,"t":112.00,"coverage":0.3577,"drones":[{"id":0,"x":62.82,"y":328.80,"hdg":0.950,"batt":100.0,"det":false},{"id":1,"x":162.82,"y":328.80,"hdg":0.950,"batt":100.0,"det":false},{"id":2,"x":262.82,"y":328.80,"hdg":0.950,"batt":100.0,"det":false},{"id":3,"x":362.82,"y":328.80,"hdg":0.950,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":113,"t":113.00,"coverage":0.3591,"drones":[{"id":0,"x":63.97,"y":336.71,"hdg":1.426,"batt":100.0,"det":false},{"id":1,"x":163.97,"y":336.71,"hdg":1.426,"batt":100.0,"det":false},{"id":2,"x":263.97,"y":336.71,"hdg":1.426,"batt":100.0,"det":false},{"id":3,"x":363.97,"y":336.71,"hdg":1.426,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":114,"t":114.00,"coverage":0.3591,"drones":[{"id":0,"x":60.22,"y":343.78,"hdg":2.060,"batt":100.0,"det":false},{"id":1,"x":160.22,"y":343.78,"hdg":2.060,"batt":100.0,"det":false},{"id":2,"x":260.22,"y":343.78,"hdg":2.060,"batt":100.0,"det":false},{"id":3,"x":360.22,"y":343.78,"hdg":2.060,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":115,"t":115.00,"coverage":0.3591,"drones":[{"id":0,"x":53.79,"y":348.54,"hdg":2.504,"batt":100.0,"det":false},{"id":1,"x":153.79,"y":348.54,"hdg":2.504,"batt":100.0,"det":false},{"id":2,"x":253.79,"y":348.54,"hdg":2.504,"batt":100.0,"det":false},{"id":3,"x":353.79,"y":348.54,"hdg":2.504,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":116,"t":116.00,"coverage":0.3591,"drones":[{"id":0,"x":48.89,"y":354.87,"hdg":2.229,"batt":100.0,"det":false},{"id":1,"x":148.89,"y":354.87,"hdg":2.229,"batt":100.0,"det":false},{"id":2,"x":248.89,"y":354.87,"hdg":2.229,"batt":100.0,"det":false},{"id":3,"x":348.89,"y":354.87,"hdg":2.229,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":117,"t":117.00,"coverage":0.3594,"drones":[{"id":0,"x":47.69,"y":362.77,"hdg":1.722,"batt":100.0,"det":false},{"id":1,"x":147.69,"y":362.77,"hdg":1.722,"batt":100.0,"det":false},{"id":2,"x":247.69,"y":362.77,"hdg":1.722,"batt":100.0,"det":false},{"id":3,"x":347.69,"y":362.77,"hdg":1.722,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":118,"t":118.00,"coverage":0.3609,"drones":[{"id":0,"x":51.80,"y":369.64,"hdg":1.031,"batt":100.0,"det":false},{"id":1,"x":151.80,"y":369.64,"hdg":1.031,"batt":100.0,"det":false},{"id":2,"x":251.80,"y":369.64,"hdg":1.031,"batt":100.0,"det":false},{"id":3,"x":351.80,"y":369.64,"hdg":1.031,"batt":100.0,"det":false}]}
{"type":"step","ep":29,"step":119,"t":119.00,"coverage":0.3627,"drones":[{"id":0,"x":58.48,"y":374.03,"hdg":0.581,"batt":100.0,"det":false},{"id":1,"x":158.48,"y":374.03,"hdg":0.581,"batt":100.0,"det":false},{"id":2,"x":258.48,"y":374.03,"hdg":0.581,"batt":100.0,"det":false},{"id":3,"x":358.48,"y":374.03,"hdg":0.581,"batt":100.0,"det":false}]}
{"type":"episode","ep":29,"mean_return":741.0585,"policy_loss":-107902.2578,"value_loss":731556.4375,"victims_found":0}