mnemosyne-graph-core 0.1.0

Shared graph kernels for the Mnemosyne memory substrate: force-directed simulation, R-tree viewport index, and the SIMD primitives both the native PyO3 crate and the WASM sub-crate depend on.
Documentation
//! Tests for the 3D force simulation — Phase 224 Wave 2.
//!
//! Mirror of the 2D tests in `force.rs` with a 3D slant. Checks:
//!
//! * three-node triangle settles into a (near-)equilateral layout
//!   when the only forces are pairwise link springs;
//! * star topology stabilises — max velocity drops below 0.5 after
//!   enough ticks;
//! * two independently seeded simulations produce bit-identical
//!   positions after the same tick sequence.

use crate::force_3d::build_octree;
use crate::{Simulation, SimulationConfig, Vec3};

#[test]
fn triangle_converges() {
    let mut sim = Simulation::new(
        3,
        SimulationConfig {
            seed: 99,
            link_strength: 0.8,
            link_distance: 30.0,
            repulsion_strength: -30.0,
            center_strength: 0.0,
            velocity_decay: 0.9,
            dimensions: 3,
            ..Default::default()
        },
    );
    sim.set_edges(&[(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0)]);
    for _ in 0..2000 {
        sim.tick(0.02);
    }
    let positions = sim.positions_3d();
    let d01 = (positions[0] - positions[1]).length();
    let d12 = (positions[1] - positions[2]).length();
    let d20 = (positions[2] - positions[0]).length();
    let target = 30.0_f32;
    for (label, d) in [("d01", d01), ("d12", d12), ("d20", d20)] {
        assert!(
            d > target * 0.75 && d < target * 1.25,
            "expected triangle side ~30, {label}={d}",
        );
    }
    // All three sides should be roughly equal (equilateral layout).
    let mean = (d01 + d12 + d20) / 3.0;
    let max_dev = [d01, d12, d20]
        .iter()
        .map(|d| (d - mean).abs() / mean)
        .fold(0.0_f32, f32::max);
    assert!(
        max_dev < 0.2,
        "triangle sides should be within 20% of the mean, got {max_dev} deviation",
    );
}

#[test]
fn star_topology_stabilizes_3d() {
    let n = 9u32;
    let mut sim = Simulation::new(
        n,
        SimulationConfig {
            seed: 1,
            link_strength: 0.2,
            link_distance: 30.0,
            repulsion_strength: -30.0,
            center_strength: 0.1,
            velocity_decay: 0.5,
            dimensions: 3,
            ..Default::default()
        },
    );
    let edges: Vec<_> = (1..n).map(|i| (0u32, i, 1.0)).collect();
    sim.set_edges(&edges);
    for _ in 0..1500 {
        sim.tick(0.02);
    }
    // positions_3d() returns Vec3 slice; derive velocities check via
    // raw access (private) -> use a deterministic indirect check:
    // compute the max displacement over 10 follow-up ticks — if the
    // system has settled the displacement per tick is tiny.
    let before: Vec<Vec3> = sim.positions_3d().to_vec();
    for _ in 0..10 {
        sim.tick(0.02);
    }
    let after = sim.positions_3d();
    let max_delta = before
        .iter()
        .zip(after.iter())
        .map(|(a, b)| (*b - *a).length())
        .fold(0.0_f32, f32::max);
    assert!(
        max_delta < 0.5,
        "star topology should settle; max delta over 10 ticks = {max_delta}",
    );
}

#[test]
fn deterministic_tick_sequence_3d() {
    let config = SimulationConfig {
        seed: 12345,
        dimensions: 3,
        ..Default::default()
    };
    let mut a = Simulation::new(50, config);
    let mut b = Simulation::new(50, config);
    let edges: Vec<_> = (0..49u32).map(|i| (i, i + 1, 1.0)).collect();
    a.set_edges(&edges);
    b.set_edges(&edges);
    for _ in 0..100 {
        a.tick(0.02);
        b.tick(0.02);
    }
    for (pa, pb) in a.positions_3d().iter().zip(b.positions_3d().iter()) {
        assert_eq!(pa, pb, "deterministic 3D run diverged");
    }
    // Snapshot a canonical position so accidental layout changes
    // surface as a test break rather than a silent shift.
    let first = a.positions_3d()[0];
    assert!(
        first.length().is_finite(),
        "first position must be finite: {first:?}",
    );
}

#[test]
fn octree_handles_empty_input() {
    let mut arena = Vec::new();
    build_octree(&mut arena, &[], 1.0);
    assert!(arena.is_empty());
}

#[test]
fn octree_single_body() {
    let mut arena = Vec::new();
    let p = Vec3::new(1.0, 2.0, 3.0);
    build_octree(&mut arena, &[p], 1.0);
    assert_eq!(arena.len(), 1);
    assert_eq!(arena[0].mass(), 1.0);
    assert_eq!(arena[0].com(), p);
}

#[test]
fn two_body_repulsion_pushes_apart() {
    // Two bodies initially very close should repel each other.
    let mut sim = Simulation::new(
        2,
        SimulationConfig {
            seed: 42,
            repulsion_strength: -80.0,
            link_strength: 0.0,
            center_strength: 0.0,
            velocity_decay: 0.6,
            dimensions: 3,
            ..Default::default()
        },
    );
    // Place them near each other in 3D.
    let initial_distance = (sim.positions_3d()[0] - sim.positions_3d()[1]).length();
    for _ in 0..500 {
        sim.tick(0.02);
    }
    let final_distance = (sim.positions_3d()[0] - sim.positions_3d()[1]).length();
    assert!(
        final_distance > initial_distance,
        "repulsion should push bodies apart: initial={initial_distance}, final={final_distance}",
    );
}