use nalgebra::Vector2;
use crate::geometry::Circle;
use crate::homothetic::{external_homothetic_center, triangle_area};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Agent {
pub position: Vector2<f64>,
pub trust_radius: f64,
}
impl Agent {
pub fn new(position: Vector2<f64>, trust_radius: f64) -> Self {
assert!(trust_radius > 0.0, "trust radius must be positive");
Self {
position,
trust_radius,
}
}
pub fn as_circle(&self) -> Circle {
Circle::new(self.position, self.trust_radius)
}
}
pub fn zero_holonomy_area(agents: &[Agent; 3]) -> Option<f64> {
let c1 = agents[0].as_circle();
let c2 = agents[1].as_circle();
let c3 = agents[2].as_circle();
let s12 = external_homothetic_center(&c1, &c2)?;
let s23 = external_homothetic_center(&c2, &c3)?;
let s31 = external_homothetic_center(&c3, &c1)?;
Some(triangle_area(&s12, &s23, &s31))
}
pub fn consensus_step(
agents: &[Agent; 3],
step_size: f64,
) -> ([Vector2<f64>; 3], Option<f64>) {
let c1 = agents[0].as_circle();
let c2 = agents[1].as_circle();
let c3 = agents[2].as_circle();
let s12 = external_homothetic_center(&c1, &c2)
.unwrap_or((agents[0].position + agents[1].position) / 2.0);
let s23 = external_homothetic_center(&c2, &c3)
.unwrap_or((agents[1].position + agents[2].position) / 2.0);
let s31 = external_homothetic_center(&c3, &c1)
.unwrap_or((agents[2].position + agents[0].position) / 2.0);
let holonomy = triangle_area(&s12, &s23, &s31);
let new_p0 = agents[0].position + step_size * (s12 + s31 - 2.0 * agents[0].position) / 2.0;
let new_p1 = agents[1].position + step_size * (s12 + s23 - 2.0 * agents[1].position) / 2.0;
let new_p2 = agents[2].position + step_size * (s23 + s31 - 2.0 * agents[2].position) / 2.0;
([new_p0, new_p1, new_p2], Some(holonomy))
}
pub fn simulate_consensus(
agents: &[Agent; 3],
steps: usize,
step_size: f64,
) -> Vec<f64> {
let mut current = *agents;
let mut holonomies = Vec::with_capacity(steps);
for _ in 0..steps {
let (new_positions, holonomy_opt) = consensus_step(¤t, step_size);
if let Some(h) = holonomy_opt {
holonomies.push(h);
}
current = [
Agent::new(new_positions[0], current[0].trust_radius),
Agent::new(new_positions[1], current[1].trust_radius),
Agent::new(new_positions[2], current[2].trust_radius),
];
}
holonomies
}
#[cfg(test)]
mod tests {
use super::*;
fn test_agents() -> [Agent; 3] {
[
Agent::new(Vector2::new(0.0, 0.0), 1.0),
Agent::new(Vector2::new(4.0, 0.0), 2.0),
Agent::new(Vector2::new(2.0, 3.0), 1.5),
]
}
#[test]
fn test_zero_holonomy_initial() {
let agents = test_agents();
let area = zero_holonomy_area(&agents);
assert!(area.is_some());
assert!(
area.unwrap() < 1e-10,
"Initial holonomy should be near zero: {}",
area.unwrap()
);
}
#[test]
fn test_holonomy_nonzero_noncollinear() {
let agents = [
Agent::new(Vector2::new(0.0, 0.0), 2.0),
Agent::new(Vector2::new(3.0, 1.0), 1.0),
Agent::new(Vector2::new(1.0, 4.0), 3.0),
];
let area = zero_holonomy_area(&agents);
assert!(area.is_some());
assert!(
area.unwrap() < 1e-10,
"Monge's theorem says all three circles have collinear centers: {}",
area.unwrap()
);
}
#[test]
fn test_consensus_step_reduces_holonomy() {
let agents = [
Agent::new(Vector2::new(0.0, 0.0), 1.0),
Agent::new(Vector2::new(3.0, 0.0), 2.0),
Agent::new(Vector2::new(0.0, 4.0), 1.5),
];
let holonomy_before = zero_holonomy_area(&agents).unwrap_or(1.0);
let (_, holonomy_opt) = consensus_step(&agents, 0.1);
let holonomy_after = holonomy_opt.unwrap_or(1.0);
assert!(
holonomy_after <= holonomy_before + 1e-10,
"Holonomy should not increase: before={}, after={}",
holonomy_before,
holonomy_after
);
}
#[test]
fn test_simulate_convergence() {
let agents = [
Agent::new(Vector2::new(0.0, 0.0), 1.0),
Agent::new(Vector2::new(3.0, 0.0), 2.0),
Agent::new(Vector2::new(0.0, 4.0), 1.5),
];
let holonomies = simulate_consensus(&agents, 50, 0.1);
assert!(holonomies.len() == 50);
for i in 1..holonomies.len() {
assert!(
holonomies[i] <= holonomies[i - 1] + 1e-12,
"Holonomy should be non-increasing at step {}: {} > {}",
i,
holonomies[i],
holonomies[i - 1]
);
}
}
}