use crate::config::{SeparationConfig, SwarmControlConfig};
use crate::controller::OperatorDirective;
use crate::formation::{Formation, FormationAnchor, FormationName};
use crate::geo::{GeoOrigin, Ned};
use crate::precedence::ModePrecedence;
use crate::separation::min_pairwise_distance;
use crate::sim::{Rng, SimDrone, SimWorld, SIM_DT};
fn origin() -> GeoOrigin {
GeoOrigin::new(12.9716, 77.5946, 0.0)
}
fn cfg(mode: &str) -> SwarmControlConfig {
SwarmControlConfig {
enabled: true,
mode: mode.into(),
..Default::default()
}
}
fn scatter(
rng: &mut Rng,
n: usize,
half: f64,
alt: f64,
alt_spread: f64,
min_gap: f64,
) -> Vec<Ned> {
let mut out: Vec<Ned> = Vec::with_capacity(n);
'next: for i in 0..n {
for _ in 0..20_000 {
let p = Ned::new(
rng.signed_unit() * half,
rng.signed_unit() * half,
-alt + rng.signed_unit() * alt_spread,
);
if out.iter().all(|q| (*q - p).norm() >= min_gap) {
out.push(p);
continue 'next;
}
}
let r = (min_gap / (2.0 * (std::f64::consts::PI / n as f64).sin())).max(half);
let theta = std::f64::consts::TAU * i as f64 / n as f64;
out.push(Ned::new(r * theta.cos(), r * theta.sin(), -alt));
}
out
}
#[derive(Debug, Clone, PartialEq)]
pub struct SeparationScenario {
pub ticks: usize,
pub min_separation_m: f64,
pub climb_slot_1_m: f64,
pub climb_slot_2_m: f64,
pub hard_engaged: bool,
pub hard_ticks: usize,
pub start_separation_m: f64,
pub closing_speed_mps: f64,
}
pub fn separation_collision_course() -> SeparationScenario {
const START_GAP: f64 = 20.0;
const PER_DRONE_MPS: f64 = 2.0; let ticks = 900; let o = origin();
let c = cfg("hold");
let mut w = SimWorld::new(
o,
vec![
SimDrone::new(1, Ned::new(0.0, 0.0, -30.0), &c, &[1, 2]),
SimDrone::new(2, Ned::new(START_GAP, 0.0, -30.0), &c, &[1, 2]),
],
);
let mut min = f64::INFINITY;
let mut hard_ticks = 0;
for _ in 0..ticks {
let now = w.now();
let p1 = w.drone(1).expect("slot 1").pos;
let p2 = w.drone(2).expect("slot 2").pos;
for (slot, from, to) in [(1u8, p1, p2), (2u8, p2, p1)] {
let aim = from + (to - from).unit().scale(PER_DRONE_MPS);
let (lat, lon, alt) = o.to_geo(aim);
w.drone_mut(slot)
.expect("slot")
.controller
.set_operator_directive(
OperatorDirective::Goto {
lat_deg: lat,
lon_deg: lon,
alt_m: alt,
},
now,
);
}
let s = w.step();
if let Some(m) = s.min_pairwise_m {
min = min.min(m);
}
if s.precedence
.iter()
.any(|(_, p)| *p == ModePrecedence::HardSeparation)
{
hard_ticks += 1;
}
}
SeparationScenario {
ticks,
min_separation_m: min,
climb_slot_1_m: w.drone(1).expect("slot 1").climbed_m(),
climb_slot_2_m: w.drone(2).expect("slot 2").climbed_m(),
hard_engaged: hard_ticks > 0,
hard_ticks,
start_separation_m: START_GAP,
closing_speed_mps: PER_DRONE_MPS * 2.0,
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FlockingScenario {
pub ticks: usize,
pub fleet: usize,
pub min_pairwise_m: f64,
pub max_target_error_m: f64,
pub mean_target_error_m: f64,
pub arrived: usize,
pub target_range_m: f64,
pub start_spread_m: f64,
pub start_min_pairwise_m: f64,
}
pub fn flocking_to_target() -> FlockingScenario {
const FLEET: usize = 8;
const SPREAD: f64 = 100.0;
const RANGE: f64 = 500.0;
const ARRIVE_M: f64 = 30.0;
let ticks = 1600; let o = origin();
let c = cfg("flocking");
let slots: Vec<u8> = (1..=FLEET as u8).collect();
let target = Ned::new(RANGE, 0.0, -30.0);
let (tlat, tlon, talt) = o.to_geo(target);
let mut rng = Rng::new(0x5EED_5A1D);
let starts = scatter(
&mut rng,
FLEET,
SPREAD * 0.5,
30.0,
5.0,
crate::separation::SEPARATION_RADIUS_M,
);
let start_min = min_pairwise_distance(&starts).unwrap_or(f64::INFINITY);
let mut drones = Vec::with_capacity(FLEET);
for (slot, pos) in slots.iter().zip(&starts) {
let mut d = SimDrone::new(*slot, *pos, &c, &slots);
d.controller.set_target(Some((tlat, tlon, talt)));
drones.push(d);
}
let mut w = SimWorld::new(o, drones);
let report = w.run(ticks);
let errors: Vec<f64> = w.drones.iter().map(|d| (d.pos - target).norm()).collect();
FlockingScenario {
ticks,
fleet: FLEET,
min_pairwise_m: report.min_pairwise_m,
max_target_error_m: errors.iter().copied().fold(0.0, f64::max),
mean_target_error_m: errors.iter().sum::<f64>() / FLEET as f64,
arrived: errors.iter().filter(|e| **e <= ARRIVE_M).count(),
target_range_m: RANGE,
start_spread_m: SPREAD,
start_min_pairwise_m: start_min,
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FormationScenario {
pub ticks: usize,
pub fleet: usize,
pub spacing_m: f64,
pub max_offset_error_m: f64,
pub mean_offset_error_m: f64,
pub min_pairwise_m: f64,
pub start_min_pairwise_m: f64,
}
pub fn formation_wedge() -> FormationScenario {
const FLEET: usize = 7;
const SPACING: f64 = 12.0;
let ticks = 1200; let o = origin();
let mut c = cfg("formation");
c.default_formation = "wedge".into();
c.default_spacing = SPACING as i64;
let slots: Vec<u8> = (1..=FLEET as u8).collect();
let mut rng = Rng::new(0xC0FF_EE01);
let starts = scatter(
&mut rng,
FLEET,
40.0,
30.0,
3.0,
crate::separation::SEPARATION_RADIUS_M,
);
let start_min = min_pairwise_distance(&starts).unwrap_or(f64::INFINITY);
let drones: Vec<SimDrone> = slots
.iter()
.zip(&starts)
.map(|(slot, pos)| SimDrone::new(*slot, *pos, &c, &slots))
.collect();
let mut w = SimWorld::new(o, drones);
let report = w.run(ticks);
let table = Formation::built_in(
FormationName::Wedge,
&slots,
SPACING,
FormationAnchor::Centroid,
);
let mut centroid = Ned::ZERO;
for d in &w.drones {
centroid = centroid + d.pos;
}
centroid = centroid.scale(1.0 / FLEET as f64);
let errors: Vec<f64> = w
.drones
.iter()
.map(|d| {
let station = table.station(d.slot).expect("registered slot");
(d.pos - (centroid + station)).norm()
})
.collect();
FormationScenario {
ticks,
fleet: FLEET,
spacing_m: SPACING,
max_offset_error_m: errors.iter().copied().fold(0.0, f64::max),
mean_offset_error_m: errors.iter().sum::<f64>() / FLEET as f64,
min_pairwise_m: report.min_pairwise_m,
start_min_pairwise_m: start_min,
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BeaconLossScenario {
pub ticks: usize,
pub kill_at_tick: usize,
pub survivor_neighbours_after: usize,
pub survivor_neighbours_before: usize,
pub isolated_emissions_after_stale: usize,
pub isolated_drift_m: f64,
pub survivors_still_emitting: bool,
pub drop_delay_s: f64,
}
pub fn beacon_loss() -> BeaconLossScenario {
const FLEET: usize = 3;
let ticks = 400; let kill_at = 100; let o = origin();
let mut c = cfg("formation");
c.default_spacing = 15;
c.separation = SeparationConfig::default();
let slots: Vec<u8> = (1..=FLEET as u8).collect();
let mut w = SimWorld::new(
o,
vec![
SimDrone::new(1, Ned::new(0.0, -15.0, -30.0), &c, &slots),
SimDrone::new(2, Ned::new(0.0, 0.0, -30.0), &c, &slots),
SimDrone::new(3, Ned::new(0.0, 15.0, -30.0), &c, &slots),
],
);
let stale_ticks = (crate::NEIGHBOR_STALE.as_secs_f64() / SIM_DT).round() as usize;
let mut before = 0usize;
let mut after = 0usize;
let mut emissions_after = 0usize;
let mut survivors_emitting = true;
let mut drop_tick = None;
let mut pos_at_settle = None;
for tick in 0..ticks {
if tick == kill_at {
w.drone_mut(3).expect("slot 3").bus_alive = false;
}
let s = w.step();
if tick == kill_at - 1 {
before = s.neighbors_of(1);
}
if tick > kill_at && drop_tick.is_none() && s.neighbors_of(1) < before {
drop_tick = Some(tick);
}
if tick >= kill_at + stale_ticks + 5 {
if pos_at_settle.is_none() {
pos_at_settle = Some(w.drone(3).expect("slot 3").pos);
}
after = s.neighbors_of(1);
if s.emitted_by(3) {
emissions_after += 1;
}
if !s.emitted_by(1) && !s.emitted_by(2) {
survivors_emitting = false;
}
}
}
BeaconLossScenario {
ticks,
kill_at_tick: kill_at,
survivor_neighbours_after: after,
survivor_neighbours_before: before,
isolated_emissions_after_stale: emissions_after,
isolated_drift_m: pos_at_settle
.map(|p| (w.drone(3).expect("slot 3").pos - p).norm())
.unwrap_or(f64::NAN),
survivors_still_emitting: survivors_emitting,
drop_delay_s: drop_tick
.map(|t| (t - kill_at) as f64 * SIM_DT)
.unwrap_or(f64::NAN),
}
}
pub fn min_separation(positions: &[Ned]) -> Option<f64> {
min_pairwise_distance(positions)
}