pandrs/graph/
core.rs

1//! Core graph data structures for graph analytics
2//!
3//! This module provides the fundamental building blocks for graph representation:
4//! - `Graph`: The main graph container supporting directed/undirected and weighted/unweighted graphs
5//! - `Node`: Vertex representation with optional data
6//! - `Edge`: Edge representation with optional weights
7//!
8//! # Examples
9//!
10//! ```
11//! use pandrs::graph::{Graph, GraphType};
12//!
13//! // Create an undirected graph
14//! let mut graph: Graph<String, f64> = Graph::new(GraphType::Undirected);
15//! let a = graph.add_node("A".to_string());
16//! let b = graph.add_node("B".to_string());
17//! graph.add_edge(a, b, Some(1.0));
18//! ```
19
20use std::collections::{HashMap, HashSet, VecDeque};
21use std::fmt::{Debug, Display};
22use std::hash::Hash;
23
24/// Represents the type of graph
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum GraphType {
27    /// Edges have no direction
28    Undirected,
29    /// Edges have a direction from source to target
30    Directed,
31}
32
33/// Unique identifier for a node in the graph
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub struct NodeId(pub(crate) usize);
36
37impl Display for NodeId {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        write!(f, "Node({})", self.0)
40    }
41}
42
43/// Unique identifier for an edge in the graph
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub struct EdgeId(pub(crate) usize);
46
47impl Display for EdgeId {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        write!(f, "Edge({})", self.0)
50    }
51}
52
53/// A node (vertex) in the graph
54#[derive(Debug, Clone)]
55pub struct Node<N> {
56    /// Unique identifier
57    pub id: NodeId,
58    /// Optional data associated with the node
59    pub data: N,
60    /// Outgoing edge IDs (or all edges for undirected graphs)
61    pub(crate) outgoing: Vec<EdgeId>,
62    /// Incoming edge IDs (only for directed graphs)
63    pub(crate) incoming: Vec<EdgeId>,
64}
65
66impl<N> Node<N> {
67    /// Creates a new node with the given ID and data
68    pub fn new(id: NodeId, data: N) -> Self {
69        Node {
70            id,
71            data,
72            outgoing: Vec::new(),
73            incoming: Vec::new(),
74        }
75    }
76
77    /// Returns the degree of the node (number of edges)
78    pub fn degree(&self) -> usize {
79        self.outgoing.len() + self.incoming.len()
80    }
81
82    /// Returns the out-degree of the node
83    pub fn out_degree(&self) -> usize {
84        self.outgoing.len()
85    }
86
87    /// Returns the in-degree of the node
88    pub fn in_degree(&self) -> usize {
89        self.incoming.len()
90    }
91}
92
93/// An edge in the graph
94#[derive(Debug, Clone)]
95pub struct Edge<W> {
96    /// Unique identifier
97    pub id: EdgeId,
98    /// Source node ID
99    pub source: NodeId,
100    /// Target node ID
101    pub target: NodeId,
102    /// Optional weight
103    pub weight: Option<W>,
104}
105
106impl<W> Edge<W> {
107    /// Creates a new edge
108    pub fn new(id: EdgeId, source: NodeId, target: NodeId, weight: Option<W>) -> Self {
109        Edge {
110            id,
111            source,
112            target,
113            weight,
114        }
115    }
116}
117
118/// Error types for graph operations
119#[derive(Debug, Clone)]
120pub enum GraphError {
121    /// Node not found
122    NodeNotFound(NodeId),
123    /// Edge not found
124    EdgeNotFound(EdgeId),
125    /// Invalid operation
126    InvalidOperation(String),
127    /// Cycle detected (for operations that require acyclic graphs)
128    CycleDetected,
129    /// Graph is not connected
130    NotConnected,
131    /// Negative weight cycle detected
132    NegativeWeightCycle,
133}
134
135impl std::fmt::Display for GraphError {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        match self {
138            GraphError::NodeNotFound(id) => write!(f, "Node not found: {}", id),
139            GraphError::EdgeNotFound(id) => write!(f, "Edge not found: {}", id),
140            GraphError::InvalidOperation(msg) => write!(f, "Invalid operation: {}", msg),
141            GraphError::CycleDetected => write!(f, "Cycle detected in graph"),
142            GraphError::NotConnected => write!(f, "Graph is not connected"),
143            GraphError::NegativeWeightCycle => write!(f, "Negative weight cycle detected"),
144        }
145    }
146}
147
148impl std::error::Error for GraphError {}
149
150/// A graph data structure supporting both directed and undirected graphs
151///
152/// Type parameters:
153/// - `N`: Node data type
154/// - `W`: Edge weight type
155#[derive(Debug, Clone)]
156pub struct Graph<N, W> {
157    /// Type of graph (directed or undirected)
158    graph_type: GraphType,
159    /// All nodes in the graph
160    nodes: HashMap<NodeId, Node<N>>,
161    /// All edges in the graph
162    edges: HashMap<EdgeId, Edge<W>>,
163    /// Counter for generating unique node IDs
164    next_node_id: usize,
165    /// Counter for generating unique edge IDs
166    next_edge_id: usize,
167    /// Adjacency list for fast neighbor lookup (source -> [(target, edge_id)])
168    adjacency: HashMap<NodeId, Vec<(NodeId, EdgeId)>>,
169    /// Reverse adjacency list for directed graphs (target -> [(source, edge_id)])
170    reverse_adjacency: HashMap<NodeId, Vec<(NodeId, EdgeId)>>,
171}
172
173impl<N, W> Graph<N, W>
174where
175    N: Clone + Debug,
176    W: Clone + Debug,
177{
178    /// Creates a new empty graph
179    pub fn new(graph_type: GraphType) -> Self {
180        Graph {
181            graph_type,
182            nodes: HashMap::new(),
183            edges: HashMap::new(),
184            next_node_id: 0,
185            next_edge_id: 0,
186            adjacency: HashMap::new(),
187            reverse_adjacency: HashMap::new(),
188        }
189    }
190
191    /// Returns the type of the graph
192    pub fn graph_type(&self) -> GraphType {
193        self.graph_type
194    }
195
196    /// Returns the number of nodes in the graph
197    pub fn node_count(&self) -> usize {
198        self.nodes.len()
199    }
200
201    /// Returns the number of edges in the graph
202    pub fn edge_count(&self) -> usize {
203        self.edges.len()
204    }
205
206    /// Checks if the graph is empty
207    pub fn is_empty(&self) -> bool {
208        self.nodes.is_empty()
209    }
210
211    /// Adds a node to the graph and returns its ID
212    pub fn add_node(&mut self, data: N) -> NodeId {
213        let id = NodeId(self.next_node_id);
214        self.next_node_id += 1;
215
216        let node = Node::new(id, data);
217        self.nodes.insert(id, node);
218        self.adjacency.insert(id, Vec::new());
219        self.reverse_adjacency.insert(id, Vec::new());
220
221        id
222    }
223
224    /// Removes a node and all its connected edges from the graph
225    pub fn remove_node(&mut self, node_id: NodeId) -> Result<N, GraphError> {
226        // First, collect all edges connected to this node
227        let edges_to_remove: Vec<EdgeId> = self
228            .edges
229            .iter()
230            .filter(|(_, edge)| edge.source == node_id || edge.target == node_id)
231            .map(|(id, _)| *id)
232            .collect();
233
234        // Remove all connected edges
235        for edge_id in edges_to_remove {
236            self.remove_edge(edge_id)?;
237        }
238
239        // Remove the node itself
240        let node = self
241            .nodes
242            .remove(&node_id)
243            .ok_or(GraphError::NodeNotFound(node_id))?;
244
245        self.adjacency.remove(&node_id);
246        self.reverse_adjacency.remove(&node_id);
247
248        Ok(node.data)
249    }
250
251    /// Gets a reference to a node by ID
252    pub fn get_node(&self, node_id: NodeId) -> Option<&Node<N>> {
253        self.nodes.get(&node_id)
254    }
255
256    /// Gets a mutable reference to a node by ID
257    pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node<N>> {
258        self.nodes.get_mut(&node_id)
259    }
260
261    /// Adds an edge between two nodes
262    pub fn add_edge(
263        &mut self,
264        source: NodeId,
265        target: NodeId,
266        weight: Option<W>,
267    ) -> Result<EdgeId, GraphError> {
268        // Check that both nodes exist
269        if !self.nodes.contains_key(&source) {
270            return Err(GraphError::NodeNotFound(source));
271        }
272        if !self.nodes.contains_key(&target) {
273            return Err(GraphError::NodeNotFound(target));
274        }
275
276        let edge_id = EdgeId(self.next_edge_id);
277        self.next_edge_id += 1;
278
279        let edge = Edge::new(edge_id, source, target, weight);
280        self.edges.insert(edge_id, edge);
281
282        // Update adjacency lists
283        self.adjacency
284            .get_mut(&source)
285            .unwrap()
286            .push((target, edge_id));
287
288        if self.graph_type == GraphType::Directed {
289            self.reverse_adjacency
290                .get_mut(&target)
291                .unwrap()
292                .push((source, edge_id));
293
294            // Update node edge lists
295            self.nodes.get_mut(&source).unwrap().outgoing.push(edge_id);
296            self.nodes.get_mut(&target).unwrap().incoming.push(edge_id);
297        } else {
298            // For undirected graphs, add the reverse edge in adjacency
299            self.adjacency
300                .get_mut(&target)
301                .unwrap()
302                .push((source, edge_id));
303
304            // Update node edge lists (both directions)
305            self.nodes.get_mut(&source).unwrap().outgoing.push(edge_id);
306            self.nodes.get_mut(&target).unwrap().outgoing.push(edge_id);
307        }
308
309        Ok(edge_id)
310    }
311
312    /// Removes an edge from the graph
313    pub fn remove_edge(&mut self, edge_id: EdgeId) -> Result<Edge<W>, GraphError> {
314        let edge = self
315            .edges
316            .remove(&edge_id)
317            .ok_or(GraphError::EdgeNotFound(edge_id))?;
318
319        // Update adjacency lists
320        if let Some(neighbors) = self.adjacency.get_mut(&edge.source) {
321            neighbors.retain(|(_, eid)| *eid != edge_id);
322        }
323
324        if self.graph_type == GraphType::Directed {
325            if let Some(neighbors) = self.reverse_adjacency.get_mut(&edge.target) {
326                neighbors.retain(|(_, eid)| *eid != edge_id);
327            }
328
329            // Update node edge lists
330            if let Some(node) = self.nodes.get_mut(&edge.source) {
331                node.outgoing.retain(|eid| *eid != edge_id);
332            }
333            if let Some(node) = self.nodes.get_mut(&edge.target) {
334                node.incoming.retain(|eid| *eid != edge_id);
335            }
336        } else {
337            // For undirected graphs
338            if let Some(neighbors) = self.adjacency.get_mut(&edge.target) {
339                neighbors.retain(|(_, eid)| *eid != edge_id);
340            }
341
342            // Update node edge lists
343            if let Some(node) = self.nodes.get_mut(&edge.source) {
344                node.outgoing.retain(|eid| *eid != edge_id);
345            }
346            if let Some(node) = self.nodes.get_mut(&edge.target) {
347                node.outgoing.retain(|eid| *eid != edge_id);
348            }
349        }
350
351        Ok(edge)
352    }
353
354    /// Gets a reference to an edge by ID
355    pub fn get_edge(&self, edge_id: EdgeId) -> Option<&Edge<W>> {
356        self.edges.get(&edge_id)
357    }
358
359    /// Gets a mutable reference to an edge by ID
360    pub fn get_edge_mut(&mut self, edge_id: EdgeId) -> Option<&mut Edge<W>> {
361        self.edges.get_mut(&edge_id)
362    }
363
364    /// Returns an iterator over all nodes
365    pub fn nodes(&self) -> impl Iterator<Item = (&NodeId, &Node<N>)> {
366        self.nodes.iter()
367    }
368
369    /// Returns an iterator over all edges
370    pub fn edges(&self) -> impl Iterator<Item = (&EdgeId, &Edge<W>)> {
371        self.edges.iter()
372    }
373
374    /// Returns an iterator over all node IDs
375    pub fn node_ids(&self) -> impl Iterator<Item = NodeId> + '_ {
376        self.nodes.keys().copied()
377    }
378
379    /// Returns an iterator over all edge IDs
380    pub fn edge_ids(&self) -> impl Iterator<Item = EdgeId> + '_ {
381        self.edges.keys().copied()
382    }
383
384    /// Returns the neighbors of a node (outgoing edges for directed graphs)
385    pub fn neighbors(&self, node_id: NodeId) -> Option<Vec<NodeId>> {
386        self.adjacency
387            .get(&node_id)
388            .map(|neighbors| neighbors.iter().map(|(n, _)| *n).collect())
389    }
390
391    /// Returns the predecessors of a node (incoming edges for directed graphs)
392    pub fn predecessors(&self, node_id: NodeId) -> Option<Vec<NodeId>> {
393        if self.graph_type == GraphType::Directed {
394            self.reverse_adjacency
395                .get(&node_id)
396                .map(|neighbors| neighbors.iter().map(|(n, _)| *n).collect())
397        } else {
398            // For undirected graphs, predecessors are the same as neighbors
399            self.neighbors(node_id)
400        }
401    }
402
403    /// Returns the edges connected to a node
404    pub fn edges_of(&self, node_id: NodeId) -> Option<Vec<EdgeId>> {
405        let node = self.nodes.get(&node_id)?;
406
407        if self.graph_type == GraphType::Directed {
408            let mut edges: Vec<EdgeId> = node.outgoing.clone();
409            edges.extend(node.incoming.iter().cloned());
410            Some(edges)
411        } else {
412            Some(node.outgoing.clone())
413        }
414    }
415
416    /// Checks if an edge exists between two nodes
417    pub fn has_edge(&self, source: NodeId, target: NodeId) -> bool {
418        if let Some(neighbors) = self.adjacency.get(&source) {
419            neighbors.iter().any(|(n, _)| *n == target)
420        } else {
421            false
422        }
423    }
424
425    /// Gets the edge between two nodes if it exists
426    pub fn get_edge_between(&self, source: NodeId, target: NodeId) -> Option<&Edge<W>> {
427        let neighbors = self.adjacency.get(&source)?;
428        let (_, edge_id) = neighbors.iter().find(|(n, _)| *n == target)?;
429        self.edges.get(edge_id)
430    }
431
432    /// Returns the degree of a node
433    pub fn degree(&self, node_id: NodeId) -> Option<usize> {
434        self.nodes.get(&node_id).map(|n| {
435            if self.graph_type == GraphType::Directed {
436                n.outgoing.len() + n.incoming.len()
437            } else {
438                n.outgoing.len()
439            }
440        })
441    }
442
443    /// Returns the in-degree of a node (for directed graphs)
444    pub fn in_degree(&self, node_id: NodeId) -> Option<usize> {
445        if self.graph_type == GraphType::Directed {
446            self.nodes.get(&node_id).map(|n| n.incoming.len())
447        } else {
448            self.degree(node_id)
449        }
450    }
451
452    /// Returns the out-degree of a node (for directed graphs)
453    pub fn out_degree(&self, node_id: NodeId) -> Option<usize> {
454        self.nodes.get(&node_id).map(|n| n.outgoing.len())
455    }
456
457    /// Creates a subgraph containing only the specified nodes and their connecting edges
458    pub fn subgraph(&self, node_ids: &[NodeId]) -> Graph<N, W> {
459        let node_set: HashSet<NodeId> = node_ids.iter().copied().collect();
460        let mut subgraph = Graph::new(self.graph_type);
461
462        // Map old node IDs to new node IDs
463        let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
464
465        // Add nodes
466        for &node_id in node_ids {
467            if let Some(node) = self.nodes.get(&node_id) {
468                let new_id = subgraph.add_node(node.data.clone());
469                id_map.insert(node_id, new_id);
470            }
471        }
472
473        // Add edges between nodes in the subgraph
474        for edge in self.edges.values() {
475            if node_set.contains(&edge.source) && node_set.contains(&edge.target) {
476                let new_source = id_map[&edge.source];
477                let new_target = id_map[&edge.target];
478                let _ = subgraph.add_edge(new_source, new_target, edge.weight.clone());
479            }
480        }
481
482        subgraph
483    }
484
485    /// Returns a reversed version of the graph (only meaningful for directed graphs)
486    pub fn reverse(&self) -> Graph<N, W> {
487        let mut reversed = Graph::new(self.graph_type);
488
489        // Map old node IDs to new node IDs
490        let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
491
492        // Add all nodes
493        for (node_id, node) in &self.nodes {
494            let new_id = reversed.add_node(node.data.clone());
495            id_map.insert(*node_id, new_id);
496        }
497
498        // Add reversed edges
499        for edge in self.edges.values() {
500            let new_source = id_map[&edge.target];
501            let new_target = id_map[&edge.source];
502            let _ = reversed.add_edge(new_source, new_target, edge.weight.clone());
503        }
504
505        reversed
506    }
507
508    /// Converts a directed graph to an undirected graph
509    pub fn to_undirected(&self) -> Graph<N, W> {
510        let mut undirected = Graph::new(GraphType::Undirected);
511
512        // Map old node IDs to new node IDs
513        let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
514
515        // Add all nodes
516        for (node_id, node) in &self.nodes {
517            let new_id = undirected.add_node(node.data.clone());
518            id_map.insert(*node_id, new_id);
519        }
520
521        // Add edges (avoiding duplicates for undirected)
522        let mut added_edges: HashSet<(NodeId, NodeId)> = HashSet::new();
523        for edge in self.edges.values() {
524            let new_source = id_map[&edge.source];
525            let new_target = id_map[&edge.target];
526
527            // Normalize edge direction to avoid duplicates
528            let normalized = if new_source.0 < new_target.0 {
529                (new_source, new_target)
530            } else {
531                (new_target, new_source)
532            };
533
534            if !added_edges.contains(&normalized) {
535                let _ = undirected.add_edge(new_source, new_target, edge.weight.clone());
536                added_edges.insert(normalized);
537            }
538        }
539
540        undirected
541    }
542
543    /// Clears all nodes and edges from the graph
544    pub fn clear(&mut self) {
545        self.nodes.clear();
546        self.edges.clear();
547        self.adjacency.clear();
548        self.reverse_adjacency.clear();
549        self.next_node_id = 0;
550        self.next_edge_id = 0;
551    }
552}
553
554impl<N, W> Default for Graph<N, W>
555where
556    N: Clone + Debug,
557    W: Clone + Debug,
558{
559    fn default() -> Self {
560        Graph::new(GraphType::Undirected)
561    }
562}
563
564/// Builder for creating graphs more conveniently
565pub struct GraphBuilder<N, W> {
566    graph: Graph<N, W>,
567    node_map: HashMap<String, NodeId>,
568}
569
570impl<N, W> GraphBuilder<N, W>
571where
572    N: Clone + Debug,
573    W: Clone + Debug,
574{
575    /// Creates a new graph builder
576    pub fn new(graph_type: GraphType) -> Self {
577        GraphBuilder {
578            graph: Graph::new(graph_type),
579            node_map: HashMap::new(),
580        }
581    }
582
583    /// Creates a new undirected graph builder
584    pub fn undirected() -> Self {
585        Self::new(GraphType::Undirected)
586    }
587
588    /// Creates a new directed graph builder
589    pub fn directed() -> Self {
590        Self::new(GraphType::Directed)
591    }
592
593    /// Adds a named node
594    pub fn add_node(mut self, name: &str, data: N) -> Self {
595        let id = self.graph.add_node(data);
596        self.node_map.insert(name.to_string(), id);
597        self
598    }
599
600    /// Adds an edge between named nodes
601    pub fn add_edge(mut self, source: &str, target: &str, weight: Option<W>) -> Self {
602        if let (Some(&src), Some(&tgt)) = (self.node_map.get(source), self.node_map.get(target)) {
603            let _ = self.graph.add_edge(src, tgt, weight);
604        }
605        self
606    }
607
608    /// Builds and returns the graph
609    pub fn build(self) -> Graph<N, W> {
610        self.graph
611    }
612
613    /// Returns the node ID for a given name
614    pub fn get_node_id(&self, name: &str) -> Option<NodeId> {
615        self.node_map.get(name).copied()
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622
623    #[test]
624    fn test_create_graph() {
625        let graph: Graph<&str, f64> = Graph::new(GraphType::Undirected);
626        assert_eq!(graph.node_count(), 0);
627        assert_eq!(graph.edge_count(), 0);
628        assert!(graph.is_empty());
629    }
630
631    #[test]
632    fn test_add_nodes() {
633        let mut graph: Graph<&str, f64> = Graph::new(GraphType::Undirected);
634        let a = graph.add_node("A");
635        let b = graph.add_node("B");
636
637        assert_eq!(graph.node_count(), 2);
638        assert_eq!(graph.get_node(a).unwrap().data, "A");
639        assert_eq!(graph.get_node(b).unwrap().data, "B");
640    }
641
642    #[test]
643    fn test_add_edges() {
644        let mut graph: Graph<&str, f64> = Graph::new(GraphType::Undirected);
645        let a = graph.add_node("A");
646        let b = graph.add_node("B");
647        let c = graph.add_node("C");
648
649        graph.add_edge(a, b, Some(1.0)).unwrap();
650        graph.add_edge(b, c, Some(2.0)).unwrap();
651
652        assert_eq!(graph.edge_count(), 2);
653        assert!(graph.has_edge(a, b));
654        assert!(graph.has_edge(b, a)); // Undirected, so reverse exists
655        assert!(graph.has_edge(b, c));
656        assert!(!graph.has_edge(a, c));
657    }
658
659    #[test]
660    fn test_directed_graph() {
661        let mut graph: Graph<&str, f64> = Graph::new(GraphType::Directed);
662        let a = graph.add_node("A");
663        let b = graph.add_node("B");
664
665        graph.add_edge(a, b, Some(1.0)).unwrap();
666
667        assert!(graph.has_edge(a, b));
668        assert!(!graph.has_edge(b, a)); // Directed, so reverse doesn't exist
669    }
670
671    #[test]
672    fn test_neighbors() {
673        let mut graph: Graph<&str, f64> = Graph::new(GraphType::Undirected);
674        let a = graph.add_node("A");
675        let b = graph.add_node("B");
676        let c = graph.add_node("C");
677
678        graph.add_edge(a, b, None).unwrap();
679        graph.add_edge(a, c, None).unwrap();
680
681        let neighbors = graph.neighbors(a).unwrap();
682        assert_eq!(neighbors.len(), 2);
683        assert!(neighbors.contains(&b));
684        assert!(neighbors.contains(&c));
685    }
686
687    #[test]
688    fn test_graph_builder() {
689        let graph: Graph<&str, f64> = GraphBuilder::undirected()
690            .add_node("a", "A")
691            .add_node("b", "B")
692            .add_node("c", "C")
693            .add_edge("a", "b", Some(1.0))
694            .add_edge("b", "c", Some(2.0))
695            .build();
696
697        assert_eq!(graph.node_count(), 3);
698        assert_eq!(graph.edge_count(), 2);
699    }
700
701    #[test]
702    fn test_remove_node() {
703        let mut graph: Graph<&str, f64> = Graph::new(GraphType::Undirected);
704        let a = graph.add_node("A");
705        let b = graph.add_node("B");
706        let c = graph.add_node("C");
707
708        graph.add_edge(a, b, None).unwrap();
709        graph.add_edge(b, c, None).unwrap();
710
711        assert_eq!(graph.edge_count(), 2);
712
713        graph.remove_node(b).unwrap();
714
715        assert_eq!(graph.node_count(), 2);
716        assert_eq!(graph.edge_count(), 0); // All edges connected to B are removed
717    }
718
719    #[test]
720    fn test_subgraph() {
721        let mut graph: Graph<&str, f64> = Graph::new(GraphType::Undirected);
722        let a = graph.add_node("A");
723        let b = graph.add_node("B");
724        let c = graph.add_node("C");
725        let d = graph.add_node("D");
726
727        graph.add_edge(a, b, None).unwrap();
728        graph.add_edge(b, c, None).unwrap();
729        graph.add_edge(c, d, None).unwrap();
730
731        let subgraph = graph.subgraph(&[a, b, c]);
732        assert_eq!(subgraph.node_count(), 3);
733        assert_eq!(subgraph.edge_count(), 2); // Only edges within the subgraph
734    }
735}