#[cfg(feature = "ngg1")]
use crate::ngg1::Ngg1;
#[derive(Debug, Clone, Copy)]
pub struct SimConfig {
pub charge: f32,
pub link_distance: f32,
pub link_strength: f32,
pub damping: f32,
pub center_gravity: f32,
pub center: (f32, f32),
pub alpha: f32,
pub alpha_decay: f32,
pub alpha_min: f32,
}
impl Default for SimConfig {
fn default() -> Self {
Self {
charge: 2000.0,
link_distance: 60.0,
link_strength: 0.5,
damping: 0.6,
center_gravity: 0.02,
center: (0.0, 0.0),
alpha: 1.0,
alpha_decay: 0.0228,
alpha_min: 0.001,
}
}
}
pub struct CsrSimulation {
config: SimConfig,
n: usize,
positions: Vec<f32>,
velocities: Vec<f32>,
edge_a: Vec<u32>,
edge_b: Vec<u32>,
edge_type: Vec<u8>,
alpha: f32,
finished: bool,
}
impl Default for CsrSimulation {
fn default() -> Self {
Self::new()
}
}
impl CsrSimulation {
pub fn new() -> Self {
let config = SimConfig::default();
Self {
n: 0,
positions: Vec::new(),
velocities: Vec::new(),
edge_a: Vec::new(),
edge_b: Vec::new(),
edge_type: Vec::new(),
alpha: config.alpha,
config,
finished: false,
}
}
pub fn load_arrays(
&mut self,
positions: Vec<f32>,
edge_a: Vec<u32>,
edge_b: Vec<u32>,
edge_type: Vec<u8>,
) {
assert!(
positions.len() % 2 == 0,
"positions must be interleaved [x,y,…]; got an odd length of {}",
positions.len()
);
assert!(
edge_a.len() == edge_b.len() && edge_b.len() == edge_type.len(),
"edge arrays must be parallel; got {}/{}/{}",
edge_a.len(),
edge_b.len(),
edge_type.len()
);
self.n = positions.len() / 2;
self.velocities = vec![0.0f32; positions.len()];
self.positions = positions;
self.edge_a = edge_a;
self.edge_b = edge_b;
self.edge_type = edge_type;
self.alpha = self.config.alpha;
self.finished = self.n == 0;
}
#[cfg(feature = "ngg1")]
#[cfg_attr(docsrs, doc(cfg(feature = "ngg1")))]
pub fn load_csr(&mut self, bytes: &[u8]) -> Result<(), crate::ngg1::Ngg1Error> {
let g = Ngg1::parse(bytes)?;
self.load(&g);
Ok(())
}
#[cfg(feature = "ngg1")]
#[cfg_attr(docsrs, doc(cfg(feature = "ngg1")))]
pub fn load(&mut self, g: &Ngg1<'_>) {
self.n = g.node_count();
self.positions = g.seed_positions();
self.velocities = vec![0.0f32; self.n * 2];
let (a, b, ty) = g.edges();
self.edge_a = a;
self.edge_b = b;
self.edge_type = ty;
self.alpha = self.config.alpha;
self.finished = self.n == 0;
}
pub fn tick(&mut self) {
if self.finished || self.n == 0 {
return;
}
let n = self.n;
let mut fx = vec![0.0f32; n];
let mut fy = vec![0.0f32; n];
let charge = self.config.charge;
for i in 0..n {
let xi = self.positions[i * 2];
let yi = self.positions[i * 2 + 1];
for j in (i + 1)..n {
let mut dx = xi - self.positions[j * 2];
let mut dy = yi - self.positions[j * 2 + 1];
let mut d2 = dx * dx + dy * dy;
if d2 < 1e-6 {
let s = (i as f32) * 12.9898 + (j as f32) * 78.233;
dx = s.sin() * 1e-2;
dy = s.cos() * 1e-2;
d2 = dx * dx + dy * dy;
if d2 < 1e-9 {
dx = 1e-2;
dy = 0.0;
d2 = 1e-4;
}
}
let inv_d = 1.0 / d2.sqrt();
let mag = charge / d2; let ux = dx * inv_d;
let uy = dy * inv_d;
fx[i] += ux * mag;
fy[i] += uy * mag;
fx[j] -= ux * mag;
fy[j] -= uy * mag;
}
}
let ld = self.config.link_distance;
let ls = self.config.link_strength;
for e in 0..self.edge_a.len() {
let a = self.edge_a[e] as usize;
let b = self.edge_b[e] as usize;
if a >= n || b >= n || a == b {
continue;
}
let dx = self.positions[b * 2] - self.positions[a * 2];
let dy = self.positions[b * 2 + 1] - self.positions[a * 2 + 1];
let dist = (dx * dx + dy * dy).sqrt().max(1e-4);
let disp = dist - ld;
let f = disp * ls / dist;
let fxe = dx * f;
let fye = dy * f;
fx[a] += fxe;
fy[a] += fye;
fx[b] -= fxe;
fy[b] -= fye;
}
let (cx, cy) = self.config.center;
let cg = self.config.center_gravity;
let damping = self.config.damping;
let alpha = self.alpha;
for i in 0..n {
let px = self.positions[i * 2];
let py = self.positions[i * 2 + 1];
let ax = fx[i] + (cx - px) * cg;
let ay = fy[i] + (cy - py) * cg;
let mut vx = self.velocities[i * 2] + ax * alpha;
let mut vy = self.velocities[i * 2 + 1] + ay * alpha;
vx *= damping;
vy *= damping;
self.velocities[i * 2] = vx;
self.velocities[i * 2 + 1] = vy;
self.positions[i * 2] = px + vx;
self.positions[i * 2 + 1] = py + vy;
}
self.alpha *= 1.0 - self.config.alpha_decay;
if self.alpha < self.config.alpha_min {
self.alpha = self.config.alpha_min;
self.finished = true;
}
}
#[inline]
pub fn is_finished(&self) -> bool {
self.finished
}
#[inline]
pub fn alpha(&self) -> f32 {
self.alpha
}
#[inline]
pub fn node_count(&self) -> usize {
self.n
}
#[inline]
pub fn edge_count(&self) -> usize {
self.edge_a.len()
}
#[inline]
pub fn positions_ptr(&self) -> *const f32 {
self.positions.as_ptr()
}
#[inline]
pub fn positions_len(&self) -> usize {
self.positions.len()
}
pub fn write_positions_into(&self, out: &mut [f32]) {
let k = out.len().min(self.positions.len());
out[..k].copy_from_slice(&self.positions[..k]);
}
pub fn set_charge(&mut self, v: f32) {
self.config.charge = v;
}
pub fn set_link_distance(&mut self, v: f32) {
self.config.link_distance = v;
}
pub fn set_link_strength(&mut self, v: f32) {
self.config.link_strength = v;
}
pub fn set_damping(&mut self, v: f32) {
self.config.damping = v;
}
pub fn set_center_gravity(&mut self, v: f32) {
self.config.center_gravity = v;
}
pub fn set_alpha_min(&mut self, v: f32) {
self.config.alpha_min = v;
}
pub fn reheat(&mut self, v: f32) {
self.alpha = v.max(self.config.alpha_min);
self.finished = self.n == 0;
}
pub fn set_param(&mut self, key: &str, val: f32) {
match key {
"charge" => self.set_charge(val),
"linkDistance" => self.set_link_distance(val),
"linkStrength" => self.set_link_strength(val),
"damping" => self.set_damping(val),
"centerGravity" => self.set_center_gravity(val),
"alphaMin" => self.set_alpha_min(val),
"reheat" => self.reheat(val),
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "ngg1")]
use crate::ngg1::worked_example;
fn loaded() -> CsrSimulation {
let mut sim = CsrSimulation::new();
sim.load_arrays(
vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
vec![0, 0],
vec![1, 2],
vec![0, 1],
);
sim
}
#[test]
fn load_arrays_seeds_scope() {
let sim = loaded();
assert_eq!(sim.node_count(), 3);
assert_eq!(sim.edge_count(), 2);
assert_eq!(sim.positions_len(), 6);
assert_eq!(&sim.positions[..], &[0.0f32, 0.0, 1.0, 0.0, 0.0, 1.0][..]);
assert!(!sim.is_finished());
}
#[test]
#[should_panic(expected = "odd length")]
fn load_arrays_rejects_odd_positions() {
CsrSimulation::new().load_arrays(vec![0.0, 0.0, 1.0], vec![], vec![], vec![]);
}
#[test]
#[should_panic(expected = "parallel")]
fn load_arrays_rejects_ragged_edges() {
CsrSimulation::new().load_arrays(vec![0.0, 0.0], vec![0], vec![], vec![]);
}
#[cfg(feature = "ngg1")]
#[test]
fn load_seeds_from_ngg1() {
let mut sim = CsrSimulation::new();
sim.load_csr(&worked_example()).unwrap();
assert_eq!(sim.node_count(), 3);
assert_eq!(sim.edge_count(), 2);
assert_eq!(sim.positions_len(), 6);
assert_eq!(&sim.positions[..], &[0.0f32, 0.0, 1.0, 0.0, 0.0, 1.0][..]);
assert!(!sim.is_finished());
assert_eq!(sim.positions, loaded().positions);
}
#[test]
fn tick_moves_nodes_and_stays_finite() {
let mut sim = loaded();
let before = sim.positions.clone();
for _ in 0..50 {
sim.tick();
}
assert_ne!(before, sim.positions, "nodes must move under forces");
for v in &sim.positions {
assert!(v.is_finite(), "no NaN/inf may escape the sim");
}
}
#[test]
fn deterministic_given_seeds() {
let mut a = loaded();
let mut b = loaded();
for _ in 0..200 {
a.tick();
b.tick();
}
assert_eq!(
a.positions, b.positions,
"same seeds ⇒ identical trajectory"
);
assert_eq!(a.alpha(), b.alpha());
}
#[test]
fn auto_finishes_at_alpha_floor() {
let mut sim = loaded();
for _ in 0..1000 {
sim.tick();
}
assert!(sim.is_finished());
assert!(sim.alpha() <= sim.config.alpha_min + f32::EPSILON);
}
#[test]
fn finished_tick_is_noop() {
let mut sim = loaded();
for _ in 0..1000 {
sim.tick();
}
let frozen = sim.positions.clone();
sim.tick();
assert_eq!(frozen, sim.positions);
}
#[test]
fn reheat_resumes_motion() {
let mut sim = loaded();
for _ in 0..1000 {
sim.tick();
}
assert!(sim.is_finished());
sim.reheat(1.0);
assert!(!sim.is_finished());
let before = sim.positions.clone();
sim.tick();
assert_ne!(before, sim.positions);
}
#[test]
fn write_positions_into_matches_ptr_buffer() {
let mut sim = loaded();
for _ in 0..10 {
sim.tick();
}
let mut out = vec![0.0f32; sim.positions_len()];
sim.write_positions_into(&mut out);
assert_eq!(out, sim.positions);
}
#[test]
fn set_param_dispatch() {
let mut sim = loaded();
sim.set_param("charge", 999.0);
sim.set_param("linkDistance", 42.0);
sim.set_param("unknownKey", 1.0); assert_eq!(sim.config.charge, 999.0);
assert_eq!(sim.config.link_distance, 42.0);
}
#[cfg(feature = "ngg1")]
#[test]
fn load_csr_rejects_malformed() {
let mut sim = CsrSimulation::new();
assert!(sim.load_csr(&[0u8; 8]).is_err());
let mut truncated = worked_example();
truncated.truncate(100);
assert!(sim.load_csr(&truncated).is_err());
}
#[test]
fn empty_scope_is_finished_and_safe() {
let mut sim = CsrSimulation::new();
assert!(sim.is_finished() || sim.node_count() == 0);
sim.tick(); assert_eq!(sim.positions_len(), 0);
}
#[test]
fn positions_ptr_stable_across_ticks() {
let mut sim = loaded();
let p0 = sim.positions_ptr();
for _ in 0..20 {
sim.tick();
}
assert_eq!(p0, sim.positions_ptr());
}
}