rustsim-spaces 0.0.1

Space implementations (grid, continuous, graph, hybrid) for rustsim
Documentation
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rustsim_core::interaction::{PositionedAgent, SpaceInteraction};
use rustsim_spaces::{
    continuous::{ContinuousPos, ContinuousSpace2D},
    continuous3d::ContinuousPos3D,
    hybrid::{HybridPos, HybridSpace, ZoneExtent},
    link::{LinkGeometry, LinkSpace},
    prelude::HybridSpaceError,
};

#[test]
fn continuous_space_periodic_insert_and_move_always_normalize_into_extent() {
    let mut rng = StdRng::seed_from_u64(42);
    let mut space = ContinuousSpace2D::new(100.0, 50.0, true, 5.0).unwrap();

    struct DummyAgent {
        id: u64,
        pos: ContinuousPos,
    }
    impl rustsim_core::agent::Agent for DummyAgent {
        fn id(&self) -> u64 {
            self.id
        }
    }
    impl PositionedAgent for DummyAgent {
        type Position = ContinuousPos;
        fn position(&self) -> &Self::Position {
            &self.pos
        }
        fn set_position(&mut self, pos: Self::Position) {
            self.pos = pos;
        }
    }

    for id in 1..=200u64 {
        let pos = ContinuousPos::new(rng.gen_range(-500.0..500.0), rng.gen_range(-500.0..500.0));
        let agent = DummyAgent { id, pos };
        space.add_agent(&agent).unwrap();
        let stored = *space.agent_position(id).unwrap();
        assert!((0.0..100.0).contains(&stored.x));
        assert!((0.0..50.0).contains(&stored.y));

        let moved = space
            .move_agent_pos(
                id,
                ContinuousPos::new(rng.gen_range(-500.0..500.0), rng.gen_range(-500.0..500.0)),
            )
            .unwrap();
        assert!((0.0..100.0).contains(&moved.x));
        assert!((0.0..50.0).contains(&moved.y));
    }
}

#[test]
fn link_space_total_agents_matches_sum_of_link_populations_under_random_operations() {
    let mut rng = StdRng::seed_from_u64(7);
    let mut space: LinkSpace<()> = LinkSpace::new();
    for _ in 0..5 {
        space.add_node();
    }
    for i in 0..4 {
        space
            .add_link(i, i + 1, LinkGeometry::new(100.0).unwrap(), ())
            .unwrap();
    }

    for id in 1..=50u64 {
        let link = rng.gen_range(0..4usize);
        let pos = rng.gen_range(0.0..100.0);
        space.add_agent_to_link(id, link, pos).unwrap();
    }

    for _ in 0..500 {
        let id = rng.gen_range(1..=50u64);
        match rng.gen_range(0..3) {
            0 => {
                let _ = space.advance_agent(id, rng.gen_range(0.0..25.0));
            }
            1 => {
                if let Some((link, _)) = space.agent_position(id) {
                    if link + 1 < space.num_links() && space.agent_at_link_end(id) {
                        let _ = space.transfer_agent(id, link + 1);
                    }
                }
            }
            _ => {
                if let Some((link, _)) = space.agent_position(id) {
                    space.set_link_exit_blocked(link, rng.gen_bool(0.5));
                }
            }
        }

        let sum: usize = (0..space.num_links())
            .map(|link| space.agents_on_link(link))
            .sum();
        assert_eq!(space.total_agents(), sum);

        for link in 0..space.num_links() {
            let ids = space.agent_ids_on_link(link);
            for &aid in ids {
                let (actual_link, pos) = space.agent_position(aid).unwrap();
                assert_eq!(actual_link, link);
                assert!((0.0..=100.0).contains(&pos));
            }

            let positions: Vec<f64> = ids
                .iter()
                .map(|&aid| space.agent_position(aid).unwrap().1)
                .collect();
            assert!(positions.windows(2).all(|w| w[0] <= w[1]));
        }
    }
}

#[test]
fn hybrid_space_zone_assignments_remain_consistent_under_random_moves() {
    let mut rng = StdRng::seed_from_u64(123);
    let extent = ZoneExtent::cube(20.0).unwrap();
    let mut space = HybridSpace::uniform(4, extent);
    for i in 0..3 {
        assert!(space.add_edge(i, i + 1));
    }

    for id in 1..=40u64 {
        let zone = rng.gen_range(0..4usize);
        space
            .add_agent_internal(
                id,
                HybridPos::new(zone, ContinuousPos3D::new(1.0, 1.0, 1.0)),
            )
            .unwrap();
    }

    for _ in 0..400 {
        let id = rng.gen_range(1..=40u64);
        let current = *space.agent_position(id).unwrap();

        let dx = rng.gen_range(-10.0..10.0);
        let dy = rng.gen_range(-10.0..10.0);
        let dz = rng.gen_range(-10.0..10.0);
        let moved = space
            .move_within_zone(id, ContinuousPos3D::new(dx, dy, dz))
            .unwrap();
        assert_eq!(moved.node, current.node);
        assert!((0.0..20.0).contains(&moved.offset.x));
        assert!((0.0..20.0).contains(&moved.offset.y));
        assert!((0.0..20.0).contains(&moved.offset.z));

        let updated = *space.agent_position(id).unwrap();
        if rng.gen_bool(0.15) && updated.node + 1 < 4 {
            let moved = space
                .transition_to_zone(
                    id,
                    updated.node + 1,
                    ContinuousPos3D::new(50.0, -5.0, 100.0),
                )
                .unwrap();
            assert_eq!(moved.node, updated.node + 1);
            assert!((0.0..20.0).contains(&moved.offset.x));
            assert!((0.0..20.0).contains(&moved.offset.y));
            assert!((0.0..20.0).contains(&moved.offset.z));
        }
    }

    for id in 1..=40u64 {
        let pos = space.agent_position(id).unwrap();
        assert!(pos.node < 4);
        let occupants = space.agents_at_node(pos.node);
        assert!(occupants.contains(&id));
    }
}

#[test]
fn hybrid_space_rejects_non_adjacent_cross_zone_moves() {
    let extent = ZoneExtent::cube(10.0).unwrap();
    let mut space = HybridSpace::uniform(3, extent);
    assert!(space.add_edge(0, 1));
    space
        .add_agent_internal(1, HybridPos::new(0, ContinuousPos3D::new(1.0, 1.0, 1.0)))
        .unwrap();

    let err = space
        .transition_to_zone(1, 2, ContinuousPos3D::new(1.0, 1.0, 1.0))
        .unwrap_err();
    assert!(matches!(err, HybridSpaceError::NoEdge { .. }));
}