weavatrix-graph 0.6.1

Deterministic, evidence-carrying graph primitives for repository intelligence
Documentation
use weavatrix_graph::{
    EdgeEndpoints, GraphError, NodeIndex, SubgraphMode, Topology, UndirectedTopology,
    all_simple_paths, bridges_and_articulation_points, floyd_warshall, graph_isomorphic,
    johnson_all_pairs, johnson_cycles, k_shortest_paths, subgraph_isomorphisms,
};

fn directed(node_count: usize, edges: &[(u32, u32)]) -> Topology {
    Topology::try_from_edges(
        node_count,
        edges
            .iter()
            .map(|&(source, target)| endpoints(source, target)),
    )
    .unwrap()
}

const fn endpoints(source: u32, target: u32) -> EdgeEndpoints {
    EdgeEndpoints::new(NodeIndex::new(source), NodeIndex::new(target))
}

#[test]
fn floyd_warshall_and_johnson_agree_with_negative_edges() {
    let graph = directed(5, &[(0, 1), (0, 2), (1, 2), (1, 3), (2, 3), (3, 4)]);
    let weights = [3_i64, 8, -2, 5, 1, 2];
    let floyd = floyd_warshall(&graph, |edge| weights[edge.index()]).unwrap();
    let johnson = johnson_all_pairs(&graph, |edge| weights[edge.index()]).unwrap();

    for source in 0..5 {
        for target in 0..5 {
            let source = NodeIndex::new(source);
            let target = NodeIndex::new(target);
            assert_eq!(
                floyd.distance(source, target),
                johnson.distance(source, target)
            );
        }
    }
    assert_eq!(
        floyd
            .path(NodeIndex::new(0), NodeIndex::new(4))
            .unwrap()
            .nodes(),
        [0, 1, 2, 3, 4].map(NodeIndex::new)
    );
    assert_eq!(
        johnson.distance(NodeIndex::new(0), NodeIndex::new(4)),
        Some(4)
    );
    assert_eq!(johnson.distance(NodeIndex::new(4), NodeIndex::new(0)), None);
}

#[test]
fn all_pairs_algorithms_reject_negative_cycles() {
    let graph = directed(3, &[(0, 1), (1, 2), (2, 0)]);
    let weights = [-2_i64, 0, 1];
    assert!(matches!(
        floyd_warshall(&graph, |edge| weights[edge.index()]),
        Err(GraphError::NegativeCycle { .. })
    ));
    assert!(matches!(
        johnson_all_pairs(&graph, |edge| weights[edge.index()]),
        Err(GraphError::NegativeCycle { .. })
    ));
}

#[test]
fn simple_and_k_shortest_paths_are_bounded_and_ordered() {
    let graph = directed(4, &[(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]);
    let paths = all_simple_paths(&graph, NodeIndex::new(0), NodeIndex::new(3), 4, 10);
    assert_eq!(paths.paths().len(), 3);
    assert!(!paths.truncated());

    let weights = [1_u64, 4, 1, 5, 1];
    let shortest = k_shortest_paths(&graph, NodeIndex::new(0), NodeIndex::new(3), 3, |edge| {
        weights[edge.index()]
    });
    assert_eq!(
        shortest
            .iter()
            .map(weavatrix_graph::WeightedPath::total_cost)
            .collect::<Vec<_>>(),
        [3, 5, 6]
    );

    let limited = all_simple_paths(&graph, NodeIndex::new(0), NodeIndex::new(3), 4, 1);
    assert_eq!(limited.paths().len(), 1);
    assert!(limited.truncated());
}

#[test]
fn circuit_enumeration_is_canonical_and_bounded() {
    let graph = directed(4, &[(0, 1), (1, 2), (2, 0), (1, 3), (3, 1)]);
    let cycles = johnson_cycles(&graph, 10);
    assert_eq!(
        cycles.paths(),
        &[
            vec![
                NodeIndex::new(0),
                NodeIndex::new(1),
                NodeIndex::new(2),
                NodeIndex::new(0),
            ],
            vec![NodeIndex::new(1), NodeIndex::new(3), NodeIndex::new(1)],
        ]
    );
    assert!(!cycles.truncated());

    let limited = johnson_cycles(&graph, 1);
    assert_eq!(limited.paths().len(), 1);
    assert!(limited.truncated());
}

#[test]
fn tarjan_finds_bridges_and_articulation_points() {
    let graph = UndirectedTopology::try_from_edges(
        4,
        [
            endpoints(0, 1),
            endpoints(1, 2),
            endpoints(2, 0),
            endpoints(1, 3),
        ],
    )
    .unwrap();
    let cuts = bridges_and_articulation_points(&graph);
    assert_eq!(
        cuts.bridges()
            .iter()
            .map(|edge| edge.index())
            .collect::<Vec<_>>(),
        [3]
    );
    assert_eq!(cuts.articulation_points(), &[NodeIndex::new(1)]);
}

#[test]
fn graph_and_subgraph_isomorphism_respect_direction_and_extra_edges() {
    let left = directed(3, &[(0, 1), (1, 2), (2, 0)]);
    let right = directed(3, &[(2, 0), (0, 1), (1, 2)]);
    assert!(graph_isomorphic(&left, &right, |_, _| true, |_, _| true));

    let pattern = directed(3, &[(0, 1), (1, 2)]);
    let target = directed(5, &[(0, 1), (1, 2), (2, 3), (4, 0)]);
    let matches = subgraph_isomorphisms(
        &pattern,
        &target,
        SubgraphMode::NonInduced,
        20,
        |_, _| true,
        |_, _| true,
    );
    assert!(!matches.mappings().is_empty());

    let reversed = directed(3, &[(1, 0), (2, 1)]);
    assert!(!graph_isomorphic(
        &pattern,
        &reversed,
        |pattern, target| pattern == target,
        |_, _| true
    ));
}