use crate::geo::Ned;
use crate::neighbor::{NearestSet, NeighborState, MAX_NEAREST};
use crate::separation::SeparationTuning;
use crate::setpoint::MAX_COMMAND_SPEED_MPS;
pub const BARRIER_GAIN_HZ: f64 = 1.0;
pub const BARRIER_LAG_S: f64 = 0.5;
pub const HARD_PIN_MPS: f64 = 0.25;
pub const BARRIER_PASSES: usize = 4;
pub fn scan_radius(t: &SeparationTuning) -> f64 {
t.radius_m
.max(t.hard_m + 2.0 * MAX_COMMAND_SPEED_MPS * BARRIER_LAG_S)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BarrierOutcome {
pub command: Ned,
pub blocked_by: Option<u8>,
pub blocked_at_m: f64,
pub pinned_by: Option<u8>,
}
pub fn allowed_closing_rate(d: f64, closing_speed: f64, hard_m: f64, gain: f64) -> f64 {
if !d.is_finite() || !closing_speed.is_finite() {
return 0.0;
}
let margin = d - hard_m - closing_speed.max(0.0) * BARRIER_LAG_S;
(gain * margin).max(0.0)
}
pub fn constrain(
command: Ned,
own_vel: Ned,
neighbors: &[NeighborState],
t: &SeparationTuning,
gain: f64,
) -> BarrierOutcome {
let mut out = BarrierOutcome {
command,
blocked_by: None,
blocked_at_m: f64::INFINITY,
pinned_by: None,
};
if !command.is_finite() {
out.command = Ned::ZERO;
return out;
}
let set = NearestSet::build(neighbors, scan_radius(t), MAX_NEAREST);
let mut ordered = [(0usize, 0.0f64, 0.0f64); MAX_NEAREST];
let mut len = 0usize;
for (i, d) in set.iter() {
if d <= f64::EPSILON {
continue;
}
let toward = neighbors[i].pos.scale(1.0 / d);
let relative = own_vel - neighbors[i].vel;
let closing_now = toward.n * relative.n + toward.e * relative.e + toward.d * relative.d;
ordered[len] = (i, d, allowed_closing_rate(d, closing_now, t.hard_m, gain));
len += 1;
}
let mut pinned_at = f64::INFINITY;
for _ in 0..BARRIER_PASSES {
let mut bound_any = false;
for &(i, d, allowed) in ordered[..len].iter().rev() {
let toward = neighbors[i].pos.scale(1.0 / d);
let closing_cmd =
out.command.n * toward.n + out.command.e * toward.e + out.command.d * toward.d;
if closing_cmd <= allowed {
continue;
}
bound_any = true;
out.command = out.command - toward.scale(closing_cmd - allowed);
if d < out.blocked_at_m {
out.blocked_by = Some(neighbors[i].slot);
out.blocked_at_m = d;
}
if allowed <= HARD_PIN_MPS && d < pinned_at {
out.pinned_by = Some(neighbors[i].slot);
pinned_at = d;
}
}
if !bound_any {
break;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::separation::{SEPARATION_HARD_M, SEPARATION_RADIUS_M};
fn nbr(slot: u8, n: f64, e: f64, d: f64) -> NeighborState {
NeighborState::new(slot, Ned::new(n, e, d), Ned::ZERO, 0)
}
fn moving(slot: u8, pos: Ned, vel: Ned) -> NeighborState {
NeighborState::new(slot, pos, vel, 0)
}
fn t() -> SeparationTuning {
SeparationTuning::default()
}
fn still(command: Ned, ns: &[NeighborState]) -> BarrierOutcome {
constrain(command, Ned::ZERO, ns, &t(), BARRIER_GAIN_HZ)
}
#[test]
fn the_allowance_is_a_cap_that_bottoms_out_at_zero() {
let floor = SEPARATION_HARD_M;
assert_eq!(
allowed_closing_rate(floor, 0.0, floor, BARRIER_GAIN_HZ),
0.0
);
assert_eq!(allowed_closing_rate(1.0, 0.0, floor, BARRIER_GAIN_HZ), 0.0);
assert_eq!(allowed_closing_rate(0.0, 5.0, floor, BARRIER_GAIN_HZ), 0.0);
assert!(
(allowed_closing_rate(SEPARATION_RADIUS_M, 0.0, floor, BARRIER_GAIN_HZ) - 4.0).abs()
< 1e-12
);
assert_eq!(allowed_closing_rate(f64::NAN, 0.0, floor, 1.0), 0.0);
assert_eq!(allowed_closing_rate(8.0, f64::NAN, floor, 1.0), 0.0);
}
#[test]
fn braking_distance_eats_the_margin_so_a_fast_approach_is_stopped_early() {
let floor = SEPARATION_HARD_M;
let idle = allowed_closing_rate(10.0, 0.0, floor, BARRIER_GAIN_HZ);
assert!((idle - 6.0).abs() < 1e-12, "{idle}");
let fast = allowed_closing_rate(10.0, MAX_COMMAND_SPEED_MPS, floor, BARRIER_GAIN_HZ);
assert!((fast - 1.0).abs() < 1e-12, "{fast}");
assert_eq!(
allowed_closing_rate(8.0, MAX_COMMAND_SPEED_MPS, floor, BARRIER_GAIN_HZ),
0.0,
"at 8 m and 10 m/s the vehicle cannot afford any closure at all"
);
assert!((allowed_closing_rate(8.0, -10.0, floor, BARRIER_GAIN_HZ) - 4.0).abs() < 1e-12);
}
#[test]
fn the_scan_radius_reaches_past_the_soft_radius_by_the_braking_distance() {
let r = scan_radius(&t());
assert!(r > SEPARATION_RADIUS_M, "{r}");
assert!(
r >= SEPARATION_HARD_M + MAX_COMMAND_SPEED_MPS * BARRIER_LAG_S,
"a vehicle at cruise must be inside the barrier before it can reach the floor: {r}"
);
let wide = SeparationTuning {
radius_m: 40.0,
..Default::default()
};
assert_eq!(scan_radius(&wide), 40.0);
}
#[test]
fn a_command_that_closes_too_fast_is_capped_to_the_allowance() {
let out = still(Ned::new(6.0, 0.0, 0.0), &[nbr(2, 5.0, 0.0, 0.0)]);
assert!((out.command.n - 1.0).abs() < 1e-12, "{:?}", out.command);
assert_eq!(out.blocked_by, Some(2));
assert!((out.blocked_at_m - 5.0).abs() < 1e-12);
assert_eq!(out.pinned_by, None, "1 m/s of headroom is not a deadlock");
}
#[test]
fn a_command_inside_the_allowance_passes_through_untouched() {
let cmd = Ned::new(0.5, 0.0, 0.0);
let out = still(cmd, &[nbr(2, 5.0, 0.0, 0.0)]);
assert_eq!(out.command, cmd);
assert_eq!(out.blocked_by, None);
assert_eq!(out.pinned_by, None);
}
#[test]
fn opening_and_perpendicular_motion_is_never_restricted() {
let ns = [nbr(2, 4.5, 0.0, 0.0)];
let away = still(Ned::new(-10.0, 0.0, 0.0), &ns);
assert_eq!(away.command, Ned::new(-10.0, 0.0, 0.0));
assert_eq!(away.blocked_by, None);
let across = still(Ned::new(0.0, 10.0, 0.0), &ns);
assert!((across.command.e - 10.0).abs() < 1e-12);
assert!(across.command.n.abs() < 1e-12);
assert_eq!(across.blocked_by, None);
}
#[test]
fn only_the_closing_component_is_removed() {
let out = still(Ned::new(6.0, 6.0, 0.0), &[nbr(2, 5.0, 0.0, 0.0)]);
assert!(
(out.command.n - 1.0).abs() < 1e-12,
"closing capped to 1 m/s"
);
assert!((out.command.e - 6.0).abs() < 1e-12, "across untouched");
}
#[test]
fn at_the_floor_the_vehicle_is_pinned_not_merely_trimmed() {
let out = still(
Ned::new(9.0, 0.0, 0.0),
&[nbr(2, SEPARATION_HARD_M - 0.5, 0.0, 0.0)],
);
assert!(out.command.n.abs() < 1e-12, "{:?}", out.command);
assert_eq!(out.blocked_by, Some(2));
assert_eq!(
out.pinned_by,
Some(2),
"no closing allowance at all is the deadlock the vertical rule breaks"
);
}
#[test]
fn a_fast_approach_is_trimmed_hard_without_being_called_a_deadlock() {
let ns = [moving(2, Ned::new(12.0, 0.0, 0.0), Ned::ZERO)];
let out = constrain(
Ned::new(10.0, 0.0, 0.0),
Ned::new(10.0, 0.0, 0.0),
&ns,
&t(),
BARRIER_GAIN_HZ,
);
assert!((out.command.n - 3.0).abs() < 1e-9, "{:?}", out.command);
assert_eq!(out.blocked_by, Some(2));
assert_eq!(
out.pinned_by, None,
"an ordinary trim must not freeze a flock"
);
}
#[test]
fn a_neighbour_fleeing_ahead_of_us_is_not_a_closure() {
let ns = [moving(2, Ned::new(6.0, 0.0, 0.0), Ned::new(8.0, 0.0, 0.0))];
let out = constrain(
Ned::new(8.0, 0.0, 0.0),
Ned::new(8.0, 0.0, 0.0),
&ns,
&t(),
BARRIER_GAIN_HZ,
);
assert!((out.command.n - 2.0).abs() < 1e-9, "{:?}", out.command);
assert_eq!(out.pinned_by, None);
}
#[test]
fn the_nearest_neighbour_wins_when_several_bind() {
let ns = [nbr(2, 7.0, 0.0, 0.0), nbr(9, 4.2, 0.0, 0.0)];
let out = still(Ned::new(8.0, 0.0, 0.0), &ns);
assert!(
(out.command.n - 0.2).abs() < 1e-9,
"the 4.2 m cap must hold, got {:?}",
out.command
);
assert_eq!(out.blocked_by, Some(9), "reported offender is the nearest");
assert_eq!(out.pinned_by, Some(9), "0.2 m/s is under the pin threshold");
}
#[test]
fn opposing_neighbours_leave_a_command_that_escapes_both() {
let ns = [nbr(2, 4.5, 0.0, 0.0), nbr(3, -4.5, 0.0, 0.0)];
let out = still(Ned::new(5.0, 2.0, 0.0), &ns);
assert!(
out.command.n.abs() < 0.51,
"north closure capped: {:?}",
out.command
);
assert!(
out.command.n > -0.51,
"not shoved into the southern neighbour"
);
assert!(
(out.command.e - 2.0).abs() < 1e-12,
"the escape lane stays open"
);
}
#[test]
fn neighbours_beyond_the_scan_radius_do_not_constrain() {
let r = scan_radius(&t());
let ns = [nbr(2, r + 0.1, 0.0, 0.0)];
let out = still(Ned::new(10.0, 0.0, 0.0), &ns);
assert_eq!(out.command, Ned::new(10.0, 0.0, 0.0));
assert_eq!(out.blocked_by, None);
}
#[test]
fn a_non_finite_command_is_zeroed_not_radiated() {
let out = still(Ned::new(f64::NAN, 0.0, 0.0), &[nbr(2, 5.0, 0.0, 0.0)]);
assert_eq!(out.command, Ned::ZERO);
}
#[test]
fn a_coincident_neighbour_is_skipped_without_poisoning_the_command() {
let out = still(Ned::new(3.0, 0.0, 0.0), &[nbr(2, 0.0, 0.0, 0.0)]);
assert!(out.command.is_finite());
assert_eq!(out.command, Ned::new(3.0, 0.0, 0.0));
}
#[test]
fn the_barrier_holds_the_floor_against_a_lagging_plant() {
let dt = 0.1;
let tau = 0.3;
let (mut a, mut b) = (0.0f64, 40.0f64);
let (mut va, mut vb) = (0.0f64, 0.0f64);
let mut min_gap = f64::INFINITY;
for _ in 0..3000 {
let gap = b - a;
min_gap = min_gap.min(gap);
let ca = constrain(
Ned::new(MAX_COMMAND_SPEED_MPS, 0.0, 0.0),
Ned::new(va, 0.0, 0.0),
&[moving(2, Ned::new(gap, 0.0, 0.0), Ned::new(vb, 0.0, 0.0))],
&t(),
BARRIER_GAIN_HZ,
)
.command
.n;
let cb = constrain(
Ned::new(-MAX_COMMAND_SPEED_MPS, 0.0, 0.0),
Ned::new(vb, 0.0, 0.0),
&[moving(1, Ned::new(-gap, 0.0, 0.0), Ned::new(va, 0.0, 0.0))],
&t(),
BARRIER_GAIN_HZ,
)
.command
.n;
let alpha = dt / tau;
va += (ca - va) * alpha;
vb += (cb - vb) * alpha;
a += va * dt;
b += vb * dt;
}
assert!(
min_gap > SEPARATION_HARD_M,
"barrier let a lagging plant through the floor: min gap {min_gap}"
);
assert!(min_gap < SEPARATION_HARD_M + 1.5, "min gap {min_gap}");
}
}