use std::collections::BTreeMap;
use std::time::Instant;
use crate::config::SwarmControlConfig;
use crate::controller::{NeighborFix, OwnState, SwarmController, CONTROL_PERIOD};
use crate::geo::{GeoOrigin, Ned};
use crate::precedence::ModePrecedence;
use crate::separation::min_pairwise_distance;
use crate::status::{STATUS_ARMED, STATUS_EMERGENCY, STATUS_GPS_OK, STATUS_GUIDED};
pub const SIM_DT: f64 = 0.1;
pub const VELOCITY_TAU_S: f64 = 0.3;
pub const BEACON_PERIOD_TICKS: usize = 5;
#[derive(Debug, Clone, Copy)]
struct SimBeacon {
at_tick: usize,
lat_e7: i32,
lon_e7: i32,
alt_dm: i16,
vx_cms: i16,
vy_cms: i16,
vz_cms: i16,
status: u8,
}
pub struct SimDrone {
pub slot: u8,
pub pos: Ned,
pub vel: Ned,
pub armed: bool,
pub guided: bool,
pub bus_alive: bool,
pub controller: SwarmController,
pub spawn_alt_m: f64,
pub emitted_last: bool,
}
impl SimDrone {
pub fn new(slot: u8, pos: Ned, cfg: &SwarmControlConfig, slots: &[u8]) -> Self {
Self {
slot,
pos,
vel: Ned::ZERO,
armed: true,
guided: true,
bus_alive: true,
controller: SwarmController::new(cfg, slots),
spawn_alt_m: -pos.d,
emitted_last: false,
}
}
pub fn with_velocity(mut self, vel: Ned) -> Self {
self.vel = vel;
self
}
pub fn alt_m(&self) -> f64 {
-self.pos.d
}
pub fn climbed_m(&self) -> f64 {
self.alt_m() - self.spawn_alt_m
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SimSample {
pub t_s: f64,
pub min_pairwise_m: Option<f64>,
pub positions: Vec<(u8, Ned)>,
pub precedence: Vec<(u8, ModePrecedence)>,
pub emitted: Vec<(u8, bool)>,
pub neighbors_seen: Vec<(u8, usize)>,
}
impl SimSample {
pub fn precedence_of(&self, slot: u8) -> Option<ModePrecedence> {
self.precedence
.iter()
.find(|(s, _)| *s == slot)
.map(|(_, p)| *p)
}
pub fn emitted_by(&self, slot: u8) -> bool {
self.emitted
.iter()
.find(|(s, _)| *s == slot)
.is_some_and(|(_, e)| *e)
}
pub fn neighbors_of(&self, slot: u8) -> usize {
self.neighbors_seen
.iter()
.find(|(s, _)| *s == slot)
.map_or(0, |(_, n)| *n)
}
}
#[derive(Debug, Clone)]
pub struct SimReport {
pub ticks: usize,
pub min_pairwise_m: f64,
pub samples: Vec<SimSample>,
}
impl SimReport {
pub fn last(&self) -> &SimSample {
self.samples.last().expect("a run has at least one sample")
}
}
pub struct SimWorld {
pub origin: GeoOrigin,
pub drones: Vec<SimDrone>,
beacons: BTreeMap<u8, SimBeacon>,
t0: Instant,
tick: usize,
}
impl SimWorld {
pub fn new(origin: GeoOrigin, drones: Vec<SimDrone>) -> Self {
Self {
origin,
drones,
beacons: BTreeMap::new(),
t0: Instant::now(),
tick: 0,
}
}
pub fn drone(&self, slot: u8) -> Option<&SimDrone> {
self.drones.iter().find(|d| d.slot == slot)
}
pub fn drone_mut(&mut self, slot: u8) -> Option<&mut SimDrone> {
self.drones.iter_mut().find(|d| d.slot == slot)
}
pub fn t_s(&self) -> f64 {
self.tick as f64 * SIM_DT
}
pub fn now(&self) -> Instant {
self.t0 + CONTROL_PERIOD * self.tick as u32
}
pub fn step(&mut self) -> SimSample {
let now = self.t0 + CONTROL_PERIOD * self.tick as u32;
if self.tick.is_multiple_of(BEACON_PERIOD_TICKS) {
for d in &self.drones {
if !d.bus_alive {
continue;
}
self.beacons.insert(d.slot, self.beacon_of(d));
}
}
let mut inputs: Vec<(OwnState, Vec<NeighborFix>)> = Vec::with_capacity(self.drones.len());
for d in &self.drones {
let (lat, lon, _) = self.origin.to_geo(d.pos);
inputs.push((
OwnState {
slot: d.slot,
lat_deg: lat,
lon_deg: lon,
alt_rel_m: d.alt_m(),
vn: d.vel.n,
ve: d.vel.e,
vd: d.vel.d,
armed: d.armed,
guided: d.guided,
fix_age: Some(std::time::Duration::ZERO),
},
if d.bus_alive {
self.fixes_for(d.slot)
} else {
Vec::new()
},
));
}
let mut sample = SimSample {
t_s: self.t_s(),
min_pairwise_m: min_pairwise_distance(
&self.drones.iter().map(|d| d.pos).collect::<Vec<_>>(),
),
positions: self.drones.iter().map(|d| (d.slot, d.pos)).collect(),
precedence: Vec::with_capacity(self.drones.len()),
emitted: Vec::with_capacity(self.drones.len()),
neighbors_seen: Vec::with_capacity(self.drones.len()),
};
let alpha = (SIM_DT / VELOCITY_TAU_S).min(1.0);
for (i, (own, fixes)) in inputs.iter().enumerate() {
let outcome = self.drones[i].controller.tick(own, fixes, now);
let want = outcome
.setpoint
.map(|s| Ned::new(s.vn as f64, s.ve as f64, s.vd as f64))
.unwrap_or(Ned::ZERO);
let d = &mut self.drones[i];
d.emitted_last = outcome.setpoint.is_some();
d.vel = d.vel + (want - d.vel).scale(alpha);
d.pos = d.pos + d.vel.scale(SIM_DT);
sample.precedence.push((d.slot, outcome.precedence));
sample.emitted.push((d.slot, outcome.setpoint.is_some()));
sample.neighbors_seen.push((d.slot, fixes.len()));
}
self.tick += 1;
sample
}
pub fn run(&mut self, ticks: usize) -> SimReport {
let mut samples = Vec::with_capacity(ticks);
let mut min = f64::INFINITY;
for _ in 0..ticks {
let s = self.step();
if let Some(m) = s.min_pairwise_m {
min = min.min(m);
}
samples.push(s);
}
SimReport {
ticks,
min_pairwise_m: min,
samples,
}
}
fn beacon_of(&self, d: &SimDrone) -> SimBeacon {
let (lat, lon, alt) = self.origin.to_geo(d.pos);
let mut status = STATUS_GPS_OK;
if d.armed {
status |= STATUS_ARMED;
}
if d.guided {
status |= STATUS_GUIDED;
}
if d.controller.emergency() {
status |= STATUS_EMERGENCY;
}
status |= d.controller.precedence().as_status_bits();
SimBeacon {
at_tick: self.tick,
lat_e7: crate::geo::deg_to_e7(lat),
lon_e7: crate::geo::deg_to_e7(lon),
alt_dm: (alt * 10.0).round().clamp(i16::MIN as f64, i16::MAX as f64) as i16,
vx_cms: (d.vel.n * 100.0)
.round()
.clamp(i16::MIN as f64, i16::MAX as f64) as i16,
vy_cms: (d.vel.e * 100.0)
.round()
.clamp(i16::MIN as f64, i16::MAX as f64) as i16,
vz_cms: (d.vel.d * 100.0)
.round()
.clamp(i16::MIN as f64, i16::MAX as f64) as i16,
status,
}
}
fn fixes_for(&self, slot: u8) -> Vec<NeighborFix> {
let stale_ticks = (crate::NEIGHBOR_STALE.as_secs_f64() / SIM_DT).round() as usize;
self.beacons
.iter()
.filter(|(s, _)| **s != slot)
.filter_map(|(s, b)| {
let age_ticks = self.tick.saturating_sub(b.at_tick);
if age_ticks >= stale_ticks {
return None;
}
let dt = age_ticks as f64 * SIM_DT;
let (vn, ve, vd) = (
b.vx_cms as f64 / 100.0,
b.vy_cms as f64 / 100.0,
b.vz_cms as f64 / 100.0,
);
let lat = crate::geo::e7_to_deg(b.lat_e7);
let lon = crate::geo::e7_to_deg(b.lon_e7);
let alt = b.alt_dm as f64 / 10.0;
let o = GeoOrigin::new(lat, lon, alt);
let (plat, plon, palt) = o.to_geo(Ned::new(vn * dt, ve * dt, vd * dt));
Some(NeighborFix {
slot: *s,
lat_deg: plat,
lon_deg: plon,
alt_m: palt,
vn,
ve,
vd,
status: b.status,
sender_order: std::cmp::Ordering::Equal,
})
})
.collect()
}
}
pub struct Rng(u64);
impl Rng {
pub fn new(seed: u64) -> Self {
Self(seed | 1)
}
pub fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
pub fn signed_unit(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_plant_tracks_a_commanded_velocity() {
let cfg = SwarmControlConfig {
enabled: true,
mode: "hold".into(),
..Default::default()
};
let mut w = SimWorld::new(
GeoOrigin::new(12.9716, 77.5946, 0.0),
vec![SimDrone::new(1, Ned::new(0.0, 0.0, -30.0), &cfg, &[1])],
);
let r = w.run(60);
assert!((r.last().positions[0].1 - Ned::new(0.0, 0.0, -30.0)).norm() < 1e-9);
}
#[test]
fn beacons_are_quantised_and_dead_reckoned_between_them() {
let cfg = SwarmControlConfig {
enabled: true,
..Default::default()
};
let mut w = SimWorld::new(
GeoOrigin::new(12.9716, 77.5946, 0.0),
vec![
SimDrone::new(1, Ned::new(0.0, 0.0, -30.0), &cfg, &[1, 2]),
SimDrone::new(2, Ned::new(50.0, 0.0, -30.0), &cfg, &[1, 2])
.with_velocity(Ned::new(5.0, 0.0, 0.0)),
],
);
w.beacons.insert(2, w.beacon_of(&w.drones[1]));
let fixes = w.fixes_for(1);
assert_eq!(fixes.len(), 1);
let seen = w
.origin
.to_ned(fixes[0].lat_deg, fixes[0].lon_deg, fixes[0].alt_m);
assert!(
(seen - Ned::new(50.0, 0.0, -30.0)).norm() < 0.06,
"{seen:?}"
);
assert!(
(fixes[0].vn - 5.0).abs() < 0.01,
"velocity survives: {fixes:?}"
);
w.tick += 1;
let later = w.fixes_for(1);
let advanced = w
.origin
.to_ned(later[0].lat_deg, later[0].lon_deg, later[0].alt_m);
assert!(
(advanced.n - seen.n - 5.0 * SIM_DT).abs() < 0.01,
"expected {} m of dead reckoning, got {}",
5.0 * SIM_DT,
advanced.n - seen.n
);
}
#[test]
fn a_dead_bus_stops_both_directions() {
let cfg = SwarmControlConfig {
enabled: true,
..Default::default()
};
let mut w = SimWorld::new(
GeoOrigin::new(12.9716, 77.5946, 0.0),
vec![
SimDrone::new(1, Ned::new(0.0, 0.0, -30.0), &cfg, &[1, 2]),
SimDrone::new(2, Ned::new(50.0, 0.0, -30.0), &cfg, &[1, 2]),
],
);
w.step();
assert_eq!(w.fixes_for(1).len(), 1);
w.drone_mut(2).expect("slot 2").bus_alive = false;
let s = w.step();
assert_eq!(s.neighbors_of(2), 0, "a dead bus hears nothing");
}
#[test]
fn the_rng_is_deterministic_and_bounded() {
let mut a = Rng::new(42);
let mut b = Rng::new(42);
for _ in 0..1000 {
let x = a.signed_unit();
assert_eq!(x, b.signed_unit());
assert!((-1.0..=1.0).contains(&x), "{x}");
}
assert_ne!(Rng::new(1).next_u64(), Rng::new(2).next_u64());
}
}