Skip to main content

scirs2_graph/
attributed_graph.rs

1//! Attributed graphs with typed node and edge features
2//!
3//! This module provides `AttributedGraph<N, E>` – a graph where every node
4//! carries data of type `N` and every edge carries data of type `E`.  It
5//! differs from the attribute-map approach in [`crate::attributes`] in that
6//! the feature types are *statically known*, enabling zero-cost extraction of
7//! dense feature matrices via a user-supplied projection function.
8//!
9//! ## Key types
10//!
11//! | Type | Purpose |
12//! |------|---------|
13//! [`AttributedGraph<N,E>`]  | Core graph container |
14//! [`AttributedGraphBuilder<N,E>`] | Ergonomic builder |
15//! [`NeighborInfo<N,E>`] | Neighbour + data bundle returned by [`attributed_neighbors`] |
16//!
17//! ## Free functions
18//!
19//! - [`node_feature_matrix`] – project node data into an `Array2<f64>`
20//! - [`edge_feature_matrix`] – project edge data into an `Array2<f64>`
21//! - [`attributed_neighbors`] – enumerate neighbours with their data
22//! - [`dijkstra_attributed`] – generalised Dijkstra using a caller-supplied cost function
23
24use std::collections::HashMap;
25
26use scirs2_core::ndarray::Array2;
27
28use crate::error::{GraphError, Result};
29
30// ============================================================================
31// NodeId
32// ============================================================================
33
34/// Stable integer node identifier.
35///
36/// Assigned sequentially by [`AttributedGraph::add_node`].  Remains valid as
37/// long as the graph is not cleared.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
39pub struct NodeId(pub usize);
40
41impl std::fmt::Display for NodeId {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(f, "NodeId({})", self.0)
44    }
45}
46
47// ============================================================================
48// AttributedGraph
49// ============================================================================
50
51/// A directed graph whose nodes carry data of type `N` and whose edges carry
52/// data of type `E`.
53///
54/// Internally, the graph is represented as an adjacency list over [`NodeId`]
55/// indices.  Parallel edges (same source and target) are supported; the first
56/// matching edge is returned by lookup helpers.
57///
58/// # Example
59///
60/// ```
61/// use scirs2_graph::attributed_graph::{AttributedGraph, NodeId};
62///
63/// #[derive(Debug, Clone)]
64/// struct Person { name: String, age: u32 }
65///
66/// #[derive(Debug, Clone)]
67/// struct Knows { since: u32 }
68///
69/// let mut g: AttributedGraph<Person, Knows> = AttributedGraph::new();
70/// let alice = g.add_node(Person { name: "Alice".into(), age: 30 });
71/// let bob   = g.add_node(Person { name: "Bob".into(),   age: 25 });
72/// g.add_edge(alice, bob, Knows { since: 2020 }).unwrap();
73///
74/// assert_eq!(g.node_count(), 2);
75/// assert_eq!(g.edge_count(), 1);
76/// ```
77#[derive(Debug, Clone)]
78pub struct AttributedGraph<N, E> {
79    /// Node data indexed by internal position (= NodeId.0)
80    nodes: Vec<N>,
81    /// Adjacency list: `adj[src_idx]` holds `(target_idx, edge_idx)` pairs
82    adj: Vec<Vec<(usize, usize)>>,
83    /// Edge storage: `(src_idx, dst_idx, E)`
84    edges: Vec<(usize, usize, E)>,
85}
86
87impl<N, E> Default for AttributedGraph<N, E> {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl<N, E> AttributedGraph<N, E> {
94    /// Create an empty attributed graph.
95    pub fn new() -> Self {
96        Self {
97            nodes: Vec::new(),
98            adj: Vec::new(),
99            edges: Vec::new(),
100        }
101    }
102
103    /// Create an empty graph with pre-allocated capacity.
104    pub fn with_capacity(node_capacity: usize, edge_capacity: usize) -> Self {
105        Self {
106            nodes: Vec::with_capacity(node_capacity),
107            adj: Vec::with_capacity(node_capacity),
108            edges: Vec::with_capacity(edge_capacity),
109        }
110    }
111
112    // -----------------------------------------------------------------------
113    // Mutation
114    // -----------------------------------------------------------------------
115
116    /// Add a node with the given data; returns its [`NodeId`].
117    pub fn add_node(&mut self, data: N) -> NodeId {
118        let id = self.nodes.len();
119        self.nodes.push(data);
120        self.adj.push(Vec::new());
121        NodeId(id)
122    }
123
124    /// Add a directed edge from `src` to `dst` with data `edge_data`.
125    ///
126    /// Both nodes must already exist.
127    ///
128    /// # Errors
129    ///
130    /// Returns [`GraphError::NodeNotFound`] if either node is absent.
131    pub fn add_edge(&mut self, src: NodeId, dst: NodeId, edge_data: E) -> Result<()> {
132        self.validate_node(src)?;
133        self.validate_node(dst)?;
134        let edge_idx = self.edges.len();
135        self.edges.push((src.0, dst.0, edge_data));
136        self.adj[src.0].push((dst.0, edge_idx));
137        Ok(())
138    }
139
140    // -----------------------------------------------------------------------
141    // Query
142    // -----------------------------------------------------------------------
143
144    /// Number of nodes.
145    #[inline]
146    pub fn node_count(&self) -> usize {
147        self.nodes.len()
148    }
149
150    /// Number of (directed) edges.
151    #[inline]
152    pub fn edge_count(&self) -> usize {
153        self.edges.len()
154    }
155
156    /// Retrieve node data by id.
157    ///
158    /// Returns `None` if the id is out of range.
159    pub fn node_data(&self, id: NodeId) -> Option<&N> {
160        self.nodes.get(id.0)
161    }
162
163    /// Retrieve mutable node data by id.
164    pub fn node_data_mut(&mut self, id: NodeId) -> Option<&mut N> {
165        self.nodes.get_mut(id.0)
166    }
167
168    /// Iterate over all node ids and their data.
169    pub fn nodes(&self) -> impl Iterator<Item = (NodeId, &N)> {
170        self.nodes.iter().enumerate().map(|(i, n)| (NodeId(i), n))
171    }
172
173    /// Iterate over all edges as `(src, dst, &E)`.
174    pub fn edges_iter(&self) -> impl Iterator<Item = (NodeId, NodeId, &E)> {
175        self.edges
176            .iter()
177            .map(|(s, d, e)| (NodeId(*s), NodeId(*d), e))
178    }
179
180    /// Return the direct out-neighbours of `node` as `(target, &E)` pairs.
181    ///
182    /// # Errors
183    ///
184    /// Returns [`GraphError::NodeNotFound`] if the node is absent.
185    pub fn out_neighbors(&self, node: NodeId) -> Result<Vec<(NodeId, &E)>> {
186        self.validate_node(node)?;
187        let result = self.adj[node.0]
188            .iter()
189            .map(|&(dst, eidx)| (NodeId(dst), &self.edges[eidx].2))
190            .collect();
191        Ok(result)
192    }
193
194    /// Return whether an edge from `src` to `dst` exists.
195    pub fn has_edge(&self, src: NodeId, dst: NodeId) -> bool {
196        if src.0 >= self.nodes.len() || dst.0 >= self.nodes.len() {
197            return false;
198        }
199        self.adj[src.0].iter().any(|&(d, _)| d == dst.0)
200    }
201
202    /// Retrieve the data of the first edge from `src` to `dst`.
203    ///
204    /// Returns `None` if no such edge exists.
205    pub fn edge_data(&self, src: NodeId, dst: NodeId) -> Option<&E> {
206        if src.0 >= self.nodes.len() {
207            return None;
208        }
209        self.adj[src.0]
210            .iter()
211            .find(|&&(d, _)| d == dst.0)
212            .map(|&(_, eidx)| &self.edges[eidx].2)
213    }
214
215    // -----------------------------------------------------------------------
216    // Internal helpers
217    // -----------------------------------------------------------------------
218
219    fn validate_node(&self, id: NodeId) -> Result<()> {
220        if id.0 < self.nodes.len() {
221            Ok(())
222        } else {
223            Err(GraphError::node_not_found_with_context(
224                id.0,
225                self.nodes.len(),
226                "AttributedGraph node validation",
227            ))
228        }
229    }
230}
231
232// ============================================================================
233// AttributedGraphBuilder
234// ============================================================================
235
236/// Builder for [`AttributedGraph`] that provides a fluent API.
237///
238/// # Example
239///
240/// ```
241/// use scirs2_graph::attributed_graph::{AttributedGraphBuilder, NodeId};
242///
243/// let mut builder: AttributedGraphBuilder<f64, f32> = AttributedGraphBuilder::new();
244/// let a = builder.node(1.0);
245/// let b = builder.node(2.0);
246/// builder.edge(a, b, 0.5_f32);
247/// let graph = builder.build().expect("build failed");
248/// assert_eq!(graph.node_count(), 2);
249/// assert_eq!(graph.edge_count(), 1);
250/// ```
251#[derive(Debug)]
252pub struct AttributedGraphBuilder<N, E> {
253    graph: AttributedGraph<N, E>,
254    /// Accumulated errors from `edge()` calls
255    errors: Vec<GraphError>,
256}
257
258impl<N, E> Default for AttributedGraphBuilder<N, E> {
259    fn default() -> Self {
260        Self::new()
261    }
262}
263
264impl<N, E> AttributedGraphBuilder<N, E> {
265    /// Create a new empty builder.
266    pub fn new() -> Self {
267        Self {
268            graph: AttributedGraph::new(),
269            errors: Vec::new(),
270        }
271    }
272
273    /// Create a builder with capacity hints.
274    pub fn with_capacity(node_capacity: usize, edge_capacity: usize) -> Self {
275        Self {
276            graph: AttributedGraph::with_capacity(node_capacity, edge_capacity),
277            errors: Vec::new(),
278        }
279    }
280
281    /// Add a node; returns the assigned [`NodeId`].
282    pub fn node(&mut self, data: N) -> NodeId {
283        self.graph.add_node(data)
284    }
285
286    /// Add a directed edge.  Any error is deferred until [`build`][Self::build].
287    pub fn edge(&mut self, src: NodeId, dst: NodeId, data: E) -> &mut Self {
288        if let Err(e) = self.graph.add_edge(src, dst, data) {
289            self.errors.push(e);
290        }
291        self
292    }
293
294    /// Consume the builder and return the graph.
295    ///
296    /// # Errors
297    ///
298    /// Returns the first deferred error, if any.
299    pub fn build(self) -> Result<AttributedGraph<N, E>> {
300        if let Some(err) = self.errors.into_iter().next() {
301            return Err(err);
302        }
303        Ok(self.graph)
304    }
305
306    /// Consume the builder, returning the graph without checking errors.
307    ///
308    /// Silently drops any accumulated errors.
309    pub fn build_unchecked(self) -> AttributedGraph<N, E> {
310        self.graph
311    }
312}
313
314// ============================================================================
315// NeighborInfo
316// ============================================================================
317
318/// A neighbour together with its node data and the connecting edge data.
319///
320/// Returned by [`attributed_neighbors`].
321#[derive(Debug)]
322pub struct NeighborInfo<'a, N, E> {
323    /// The neighbour's id.
324    pub id: NodeId,
325    /// Reference to the neighbour's node data.
326    pub node_data: &'a N,
327    /// Reference to the edge data connecting source → neighbour.
328    pub edge_data: &'a E,
329}
330
331// ============================================================================
332// Free functions
333// ============================================================================
334
335/// Construct a dense node-feature matrix from an attributed graph.
336///
337/// `feature_fn` is called for each node in insertion order and must return a
338/// `Vec<f64>` of the same length for every node.  The resulting matrix has
339/// shape `(node_count, feature_dim)`.
340///
341/// # Errors
342///
343/// Returns [`GraphError::InvalidParameter`] if the node count is zero or if
344/// `feature_fn` returns vectors of inconsistent length.
345///
346/// # Example
347///
348/// ```
349/// use scirs2_graph::attributed_graph::{AttributedGraph, node_feature_matrix};
350///
351/// let mut g = AttributedGraph::<[f64; 2], ()>::new();
352/// g.add_node([1.0, 0.5]);
353/// g.add_node([0.0, 1.0]);
354/// let mat = node_feature_matrix(&g, |n| n.to_vec()).unwrap();
355/// assert_eq!(mat.shape(), &[2, 2]);
356/// ```
357pub fn node_feature_matrix<N, E, F>(
358    graph: &AttributedGraph<N, E>,
359    feature_fn: F,
360) -> Result<Array2<f64>>
361where
362    F: Fn(&N) -> Vec<f64>,
363{
364    let n = graph.node_count();
365    if n == 0 {
366        return Err(GraphError::invalid_parameter(
367            "graph",
368            "empty graph",
369            "at least one node",
370        ));
371    }
372
373    // Compute features for all nodes
374    let features: Vec<Vec<f64>> = graph.nodes.iter().map(feature_fn).collect();
375
376    let dim = features[0].len();
377    if dim == 0 {
378        return Err(GraphError::invalid_parameter(
379            "feature_fn",
380            "zero-length feature vector",
381            "non-empty feature vector",
382        ));
383    }
384
385    // Validate uniform dimensionality
386    for (i, fv) in features.iter().enumerate() {
387        if fv.len() != dim {
388            return Err(GraphError::InvalidParameter {
389                param: "feature_fn".to_string(),
390                value: format!("node {i} returned dim={}", fv.len()),
391                expected: format!("uniform dim={dim}"),
392                context: "node_feature_matrix".to_string(),
393            });
394        }
395    }
396
397    let mut mat = Array2::zeros((n, dim));
398    for (i, fv) in features.iter().enumerate() {
399        for (j, &v) in fv.iter().enumerate() {
400            mat[[i, j]] = v;
401        }
402    }
403    Ok(mat)
404}
405
406/// Construct a dense edge-feature matrix from an attributed graph.
407///
408/// `feature_fn` is called for each edge in insertion order and must return a
409/// `Vec<f64>` of the same length for every edge.  The resulting matrix has
410/// shape `(edge_count, feature_dim)`.
411///
412/// # Errors
413///
414/// Returns [`GraphError::InvalidParameter`] if the edge count is zero or if
415/// `feature_fn` returns vectors of inconsistent length.
416pub fn edge_feature_matrix<N, E, F>(
417    graph: &AttributedGraph<N, E>,
418    feature_fn: F,
419) -> Result<Array2<f64>>
420where
421    F: Fn(&E) -> Vec<f64>,
422{
423    let m = graph.edge_count();
424    if m == 0 {
425        return Err(GraphError::invalid_parameter(
426            "graph",
427            "graph has no edges",
428            "at least one edge",
429        ));
430    }
431
432    let features: Vec<Vec<f64>> = graph.edges.iter().map(|(_, _, e)| feature_fn(e)).collect();
433
434    let dim = features[0].len();
435    if dim == 0 {
436        return Err(GraphError::invalid_parameter(
437            "feature_fn",
438            "zero-length feature vector",
439            "non-empty feature vector",
440        ));
441    }
442
443    for (i, fv) in features.iter().enumerate() {
444        if fv.len() != dim {
445            return Err(GraphError::InvalidParameter {
446                param: "feature_fn".to_string(),
447                value: format!("edge {i} returned dim={}", fv.len()),
448                expected: format!("uniform dim={dim}"),
449                context: "edge_feature_matrix".to_string(),
450            });
451        }
452    }
453
454    let mut mat = Array2::zeros((m, dim));
455    for (i, fv) in features.iter().enumerate() {
456        for (j, &v) in fv.iter().enumerate() {
457            mat[[i, j]] = v;
458        }
459    }
460    Ok(mat)
461}
462
463/// Return the out-neighbours of `node` together with their node data and the
464/// connecting edge data.
465///
466/// # Errors
467///
468/// Returns [`GraphError::NodeNotFound`] if `node` is not in the graph.
469///
470/// # Example
471///
472/// ```
473/// use scirs2_graph::attributed_graph::{AttributedGraph, attributed_neighbors};
474///
475/// let mut g = AttributedGraph::<&str, f64>::new();
476/// let a = g.add_node("Alice");
477/// let b = g.add_node("Bob");
478/// g.add_edge(a, b, 1.5).unwrap();
479///
480/// let nbrs = attributed_neighbors(a, &g).unwrap();
481/// assert_eq!(nbrs.len(), 1);
482/// assert_eq!(*nbrs[0].node_data, "Bob");
483/// assert_eq!(*nbrs[0].edge_data, 1.5);
484/// ```
485pub fn attributed_neighbors<'a, N, E>(
486    node: NodeId,
487    graph: &'a AttributedGraph<N, E>,
488) -> Result<Vec<NeighborInfo<'a, N, E>>> {
489    graph.validate_node(node)?;
490    let result = graph.adj[node.0]
491        .iter()
492        .map(|&(dst, eidx)| NeighborInfo {
493            id: NodeId(dst),
494            node_data: &graph.nodes[dst],
495            edge_data: &graph.edges[eidx].2,
496        })
497        .collect();
498    Ok(result)
499}
500
501// ============================================================================
502// Dijkstra
503// ============================================================================
504
505/// Priority queue entry used internally by Dijkstra.
506#[derive(Debug, Clone, PartialEq)]
507struct DijkstraEntry {
508    cost: f64,
509    node: usize,
510}
511
512impl Eq for DijkstraEntry {}
513
514impl PartialOrd for DijkstraEntry {
515    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
516        Some(self.cmp(other))
517    }
518}
519
520impl Ord for DijkstraEntry {
521    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
522        // Reversed for min-heap behaviour with BinaryHeap (max-heap)
523        other
524            .cost
525            .partial_cmp(&self.cost)
526            .unwrap_or(std::cmp::Ordering::Equal)
527    }
528}
529
530/// Generalised Dijkstra shortest-path algorithm on an attributed graph.
531///
532/// `cost_fn` is called for each edge and must return a non-negative `f64`
533/// cost.  Negative costs will produce incorrect results (no check is
534/// performed for performance reasons).
535///
536/// Returns a map from each reachable [`NodeId`] to its minimum total cost
537/// from `source`.  Unreachable nodes are absent from the map.
538///
539/// # Errors
540///
541/// Returns [`GraphError::NodeNotFound`] if `source` is not in the graph.
542///
543/// # Example
544///
545/// ```
546/// use scirs2_graph::attributed_graph::{AttributedGraph, dijkstra_attributed, NodeId};
547///
548/// let mut g = AttributedGraph::<(), f64>::new();
549/// let a = g.add_node(());
550/// let b = g.add_node(());
551/// let c = g.add_node(());
552/// g.add_edge(a, b, 2.0).unwrap();
553/// g.add_edge(b, c, 3.0).unwrap();
554/// g.add_edge(a, c, 10.0).unwrap();
555///
556/// let dist = dijkstra_attributed(&g, a, |e| *e).unwrap();
557/// assert_eq!(dist[&b], 2.0);
558/// assert_eq!(dist[&c], 5.0); // 2 + 3, not 10
559/// ```
560pub fn dijkstra_attributed<N, E, F>(
561    graph: &AttributedGraph<N, E>,
562    source: NodeId,
563    cost_fn: F,
564) -> Result<HashMap<NodeId, f64>>
565where
566    F: Fn(&E) -> f64,
567{
568    use std::collections::BinaryHeap;
569
570    graph.validate_node(source)?;
571
572    let n = graph.node_count();
573    let mut dist = vec![f64::INFINITY; n];
574    dist[source.0] = 0.0;
575
576    let mut heap = BinaryHeap::new();
577    heap.push(DijkstraEntry {
578        cost: 0.0,
579        node: source.0,
580    });
581
582    while let Some(DijkstraEntry { cost, node }) = heap.pop() {
583        // Skip stale entries
584        if cost > dist[node] {
585            continue;
586        }
587
588        for &(dst, eidx) in &graph.adj[node] {
589            let edge_cost = cost_fn(&graph.edges[eidx].2);
590            let new_cost = cost + edge_cost;
591            if new_cost < dist[dst] {
592                dist[dst] = new_cost;
593                heap.push(DijkstraEntry {
594                    cost: new_cost,
595                    node: dst,
596                });
597            }
598        }
599    }
600
601    let result = dist
602        .into_iter()
603        .enumerate()
604        .filter(|(_, d)| d.is_finite())
605        .map(|(i, d)| (NodeId(i), d))
606        .collect();
607
608    Ok(result)
609}
610
611// ============================================================================
612// Additional graph analysis functions
613// ============================================================================
614
615/// Compute the in-degree of each node.
616///
617/// Returns a `Vec<usize>` of length `graph.node_count()` where entry `i` is
618/// the number of edges *targeting* node `i`.
619pub fn in_degrees<N, E>(graph: &AttributedGraph<N, E>) -> Vec<usize> {
620    let mut deg = vec![0usize; graph.node_count()];
621    for (_, dst, _) in &graph.edges {
622        deg[*dst] += 1;
623    }
624    deg
625}
626
627/// Compute the out-degree of each node.
628///
629/// Returns a `Vec<usize>` of length `graph.node_count()`.
630pub fn out_degrees<N, E>(graph: &AttributedGraph<N, E>) -> Vec<usize> {
631    graph.adj.iter().map(|nbrs| nbrs.len()).collect()
632}
633
634/// Filter nodes by a predicate on their data, returning matching [`NodeId`]s.
635///
636/// # Example
637///
638/// ```
639/// use scirs2_graph::attributed_graph::{AttributedGraph, filter_nodes};
640///
641/// let mut g = AttributedGraph::<i32, ()>::new();
642/// g.add_node(10);
643/// g.add_node(20);
644/// g.add_node(30);
645///
646/// let big = filter_nodes(&g, |v| *v > 15);
647/// assert_eq!(big.len(), 2);
648/// ```
649pub fn filter_nodes<N, E, P>(graph: &AttributedGraph<N, E>, predicate: P) -> Vec<NodeId>
650where
651    P: Fn(&N) -> bool,
652{
653    graph
654        .nodes
655        .iter()
656        .enumerate()
657        .filter_map(|(i, n)| if predicate(n) { Some(NodeId(i)) } else { None })
658        .collect()
659}
660
661// ============================================================================
662// Tests
663// ============================================================================
664
665#[cfg(test)]
666mod tests {
667    use super::*;
668
669    // ------------------------------------------------------------------
670    // Helpers
671    // ------------------------------------------------------------------
672
673    #[derive(Debug, Clone, PartialEq)]
674    struct Person {
675        name: String,
676        age: u32,
677    }
678
679    #[derive(Debug, Clone, PartialEq)]
680    struct Rel {
681        weight: f64,
682    }
683
684    fn make_graph() -> AttributedGraph<Person, Rel> {
685        let mut g = AttributedGraph::new();
686        let alice = g.add_node(Person {
687            name: "Alice".into(),
688            age: 30,
689        });
690        let bob = g.add_node(Person {
691            name: "Bob".into(),
692            age: 25,
693        });
694        let charlie = g.add_node(Person {
695            name: "Charlie".into(),
696            age: 35,
697        });
698        g.add_edge(alice, bob, Rel { weight: 1.0 }).unwrap();
699        g.add_edge(bob, charlie, Rel { weight: 2.0 }).unwrap();
700        g.add_edge(alice, charlie, Rel { weight: 5.0 }).unwrap();
701        g
702    }
703
704    // ------------------------------------------------------------------
705    // Basic construction
706    // ------------------------------------------------------------------
707
708    #[test]
709    fn test_basic_construction() {
710        let g = make_graph();
711        assert_eq!(g.node_count(), 3);
712        assert_eq!(g.edge_count(), 3);
713    }
714
715    #[test]
716    fn test_node_data_access() {
717        let g = make_graph();
718        let alice = NodeId(0);
719        let data = g.node_data(alice).unwrap();
720        assert_eq!(data.name, "Alice");
721        assert_eq!(data.age, 30);
722    }
723
724    #[test]
725    fn test_edge_data_access() {
726        let g = make_graph();
727        let alice = NodeId(0);
728        let bob = NodeId(1);
729        let ed = g.edge_data(alice, bob).unwrap();
730        assert!((ed.weight - 1.0).abs() < 1e-12);
731    }
732
733    #[test]
734    fn test_has_edge() {
735        let g = make_graph();
736        assert!(g.has_edge(NodeId(0), NodeId(1)));
737        assert!(!g.has_edge(NodeId(1), NodeId(0)));
738        assert!(!g.has_edge(NodeId(2), NodeId(0)));
739    }
740
741    #[test]
742    fn test_invalid_node_add_edge() {
743        let mut g = AttributedGraph::<i32, ()>::new();
744        g.add_node(1);
745        let err = g.add_edge(NodeId(0), NodeId(5), ());
746        assert!(err.is_err());
747    }
748
749    // ------------------------------------------------------------------
750    // Builder
751    // ------------------------------------------------------------------
752
753    #[test]
754    fn test_builder() {
755        let mut b = AttributedGraphBuilder::<i32, f64>::new();
756        let a = b.node(1);
757        let bb = b.node(2);
758        b.edge(a, bb, std::f64::consts::PI);
759        let g = b.build().unwrap();
760        assert_eq!(g.node_count(), 2);
761        assert_eq!(g.edge_count(), 1);
762    }
763
764    #[test]
765    fn test_builder_bad_edge_deferred() {
766        let mut b = AttributedGraphBuilder::<i32, f64>::new();
767        b.node(1);
768        // NodeId(99) doesn't exist
769        b.edge(NodeId(0), NodeId(99), 1.0);
770        assert!(b.build().is_err());
771    }
772
773    // ------------------------------------------------------------------
774    // Feature matrices
775    // ------------------------------------------------------------------
776
777    #[test]
778    fn test_node_feature_matrix() {
779        let g = make_graph();
780        let mat = node_feature_matrix(&g, |p| vec![p.age as f64]).unwrap();
781        assert_eq!(mat.shape(), &[3, 1]);
782        assert!((mat[[0, 0]] - 30.0).abs() < 1e-12);
783        assert!((mat[[1, 0]] - 25.0).abs() < 1e-12);
784        assert!((mat[[2, 0]] - 35.0).abs() < 1e-12);
785    }
786
787    #[test]
788    fn test_edge_feature_matrix() {
789        let g = make_graph();
790        let mat = edge_feature_matrix(&g, |r| vec![r.weight]).unwrap();
791        assert_eq!(mat.shape(), &[3, 1]);
792        // Edges in insertion order: Alice→Bob(1.0), Bob→Charlie(2.0), Alice→Charlie(5.0)
793        assert!((mat[[0, 0]] - 1.0).abs() < 1e-12);
794        assert!((mat[[1, 0]] - 2.0).abs() < 1e-12);
795        assert!((mat[[2, 0]] - 5.0).abs() < 1e-12);
796    }
797
798    #[test]
799    fn test_node_feature_matrix_multi_dim() {
800        let g = make_graph();
801        let mat = node_feature_matrix(&g, |p| vec![p.age as f64, p.name.len() as f64]).unwrap();
802        assert_eq!(mat.shape(), &[3, 2]);
803    }
804
805    #[test]
806    fn test_node_feature_matrix_empty_graph() {
807        let g = AttributedGraph::<i32, ()>::new();
808        let result = node_feature_matrix(&g, |v| vec![*v as f64]);
809        assert!(result.is_err());
810    }
811
812    #[test]
813    fn test_edge_feature_matrix_no_edges() {
814        let mut g = AttributedGraph::<i32, f64>::new();
815        g.add_node(1);
816        let result = edge_feature_matrix(&g, |v| vec![*v]);
817        assert!(result.is_err());
818    }
819
820    // ------------------------------------------------------------------
821    // Attributed neighbours
822    // ------------------------------------------------------------------
823
824    #[test]
825    fn test_attributed_neighbors() {
826        let g = make_graph();
827        let nbrs = attributed_neighbors(NodeId(0), &g).unwrap();
828        assert_eq!(nbrs.len(), 2);
829        // Bob
830        assert_eq!(nbrs[0].node_data.name, "Bob");
831        assert!((nbrs[0].edge_data.weight - 1.0).abs() < 1e-12);
832        // Charlie (via direct edge)
833        assert_eq!(nbrs[1].node_data.name, "Charlie");
834        assert!((nbrs[1].edge_data.weight - 5.0).abs() < 1e-12);
835    }
836
837    #[test]
838    fn test_attributed_neighbors_unknown_node() {
839        let g = make_graph();
840        assert!(attributed_neighbors(NodeId(99), &g).is_err());
841    }
842
843    // ------------------------------------------------------------------
844    // Dijkstra
845    // ------------------------------------------------------------------
846
847    #[test]
848    fn test_dijkstra_simple() {
849        let g = make_graph();
850        // Alice=0, Bob=1, Charlie=2
851        // 0→1 (1.0), 1→2 (2.0), 0→2 (5.0)
852        let dist = dijkstra_attributed(&g, NodeId(0), |r| r.weight).unwrap();
853        assert!((dist[&NodeId(0)] - 0.0).abs() < 1e-12);
854        assert!((dist[&NodeId(1)] - 1.0).abs() < 1e-12);
855        assert!((dist[&NodeId(2)] - 3.0).abs() < 1e-12); // 1+2 beats direct 5
856    }
857
858    #[test]
859    fn test_dijkstra_unreachable() {
860        let mut g = AttributedGraph::<(), f64>::new();
861        g.add_node(());
862        g.add_node(());
863        // No edges; node 1 is unreachable from node 0
864        let dist = dijkstra_attributed(&g, NodeId(0), |e| *e).unwrap();
865        assert!(dist.contains_key(&NodeId(0)));
866        assert!(!dist.contains_key(&NodeId(1)));
867    }
868
869    #[test]
870    fn test_dijkstra_invalid_source() {
871        let g = make_graph();
872        assert!(dijkstra_attributed(&g, NodeId(99), |r| r.weight).is_err());
873    }
874
875    #[test]
876    fn test_dijkstra_single_node() {
877        let mut g = AttributedGraph::<(), ()>::new();
878        g.add_node(());
879        let dist = dijkstra_attributed(&g, NodeId(0), |_| 1.0).unwrap();
880        assert_eq!(dist.len(), 1);
881        assert_eq!(dist[&NodeId(0)], 0.0);
882    }
883
884    // ------------------------------------------------------------------
885    // Utility functions
886    // ------------------------------------------------------------------
887
888    #[test]
889    fn test_in_out_degrees() {
890        let g = make_graph();
891        let out = out_degrees(&g);
892        // Alice: 2, Bob: 1, Charlie: 0
893        assert_eq!(out[0], 2);
894        assert_eq!(out[1], 1);
895        assert_eq!(out[2], 0);
896
897        let inn = in_degrees(&g);
898        // Alice: 0, Bob: 1, Charlie: 2
899        assert_eq!(inn[0], 0);
900        assert_eq!(inn[1], 1);
901        assert_eq!(inn[2], 2);
902    }
903
904    #[test]
905    fn test_filter_nodes() {
906        let g = make_graph();
907        let young = filter_nodes(&g, |p| p.age < 31);
908        // Alice(30), Bob(25)
909        assert_eq!(young.len(), 2);
910    }
911
912    #[test]
913    fn test_nodes_iterator() {
914        let g = make_graph();
915        let names: Vec<&str> = g.nodes().map(|(_, p)| p.name.as_str()).collect();
916        assert_eq!(names, vec!["Alice", "Bob", "Charlie"]);
917    }
918
919    #[test]
920    fn test_edges_iterator() {
921        let g = make_graph();
922        let edges: Vec<(NodeId, NodeId, f64)> =
923            g.edges_iter().map(|(s, d, e)| (s, d, e.weight)).collect();
924        assert_eq!(edges.len(), 3);
925    }
926}