Skip to main content

rust_igraph/algorithms/operators/
line_graph.rs

1//! Line graph construction (ALGO-OP-028).
2//!
3//! Counterpart of `igraph_linegraph()` from
4//! `references/igraph/src/operators/misc_internal.c`.
5
6use crate::core::graph::EdgeId;
7use crate::core::{Graph, IgraphResult, VertexId};
8
9/// Construct the line graph L(G) of the input graph.
10///
11/// The line graph has one vertex for each edge of the original graph.
12/// Two vertices of L(G) are adjacent if and only if the corresponding
13/// edges in G share an endpoint.
14///
15/// For directed graphs, edge (u→v) is adjacent to edge (w→x) in the
16/// line graph iff v == w (the head of the first matches the tail of
17/// the second), matching igraph's upstream convention.
18///
19/// Counterpart of `igraph_linegraph()`.
20///
21/// # Examples
22///
23/// ```
24/// use rust_igraph::{Graph, line_graph};
25///
26/// // Triangle 0-1-2-0 has 3 edges; L(G) is also a triangle.
27/// let mut g = Graph::with_vertices(3);
28/// g.add_edge(0, 1).unwrap();
29/// g.add_edge(1, 2).unwrap();
30/// g.add_edge(2, 0).unwrap();
31///
32/// let lg = line_graph(&g).unwrap();
33/// assert_eq!(lg.vcount(), 3);
34/// assert_eq!(lg.ecount(), 3);
35/// ```
36///
37/// ```
38/// use rust_igraph::{Graph, line_graph};
39///
40/// // Path 0-1-2 has 2 edges; L(G) is a single edge.
41/// let mut g = Graph::with_vertices(3);
42/// g.add_edge(0, 1).unwrap();
43/// g.add_edge(1, 2).unwrap();
44///
45/// let lg = line_graph(&g).unwrap();
46/// assert_eq!(lg.vcount(), 2);
47/// assert_eq!(lg.ecount(), 1);
48/// ```
49pub fn line_graph(graph: &Graph) -> IgraphResult<Graph> {
50    let m = graph.ecount();
51    let directed = graph.is_directed();
52    let m_u32 = u32::try_from(m).map_err(|_| {
53        crate::core::IgraphError::Internal("edge count exceeds u32::MAX for line graph")
54    })?;
55    let mut result = Graph::new(m_u32, directed)?;
56
57    if m == 0 {
58        return Ok(result);
59    }
60
61    let n = graph.vcount();
62
63    if directed {
64        // For directed: edge i (u→v) connects to edge j (w→x) in L(G) iff v == w.
65        // For each vertex v, every incoming edge connects to every outgoing edge.
66        for v in 0..n {
67            let in_eids = graph.incident_in(v)?;
68            let out_eids = graph.incident(v)?;
69
70            for &in_eid in &in_eids {
71                for &out_eid in &out_eids {
72                    result.add_edge(in_eid as VertexId, out_eid as VertexId)?;
73                }
74            }
75        }
76    } else {
77        // For undirected: two edges are adjacent in L(G) iff they share
78        // a vertex. For each vertex v, all edges incident to v form a
79        // clique in L(G).
80        for v in 0..n {
81            let incident = incident_unique(graph, v)?;
82            let len = incident.len();
83            for i in 0..len {
84                for j in (i + 1)..len {
85                    result.add_edge(incident[i] as VertexId, incident[j] as VertexId)?;
86                }
87            }
88        }
89    }
90
91    Ok(result)
92}
93
94/// All edge IDs incident to vertex v, deduplicated (self-loops only once).
95fn incident_unique(graph: &Graph, v: VertexId) -> IgraphResult<Vec<EdgeId>> {
96    let mut ids = graph.incident(v)?;
97    ids.sort_unstable();
98    ids.dedup();
99    Ok(ids)
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn empty_graph_line_graph_is_empty() {
108        let g = Graph::with_vertices(5);
109        let lg = line_graph(&g).unwrap();
110        assert_eq!(lg.vcount(), 0);
111        assert_eq!(lg.ecount(), 0);
112    }
113
114    #[test]
115    fn single_edge_line_graph_is_single_vertex() {
116        let mut g = Graph::with_vertices(2);
117        g.add_edge(0, 1).unwrap();
118        let lg = line_graph(&g).unwrap();
119        assert_eq!(lg.vcount(), 1);
120        assert_eq!(lg.ecount(), 0);
121    }
122
123    #[test]
124    fn triangle_line_graph_is_triangle() {
125        let mut g = Graph::with_vertices(3);
126        g.add_edge(0, 1).unwrap();
127        g.add_edge(1, 2).unwrap();
128        g.add_edge(2, 0).unwrap();
129        let lg = line_graph(&g).unwrap();
130        assert_eq!(lg.vcount(), 3);
131        assert_eq!(lg.ecount(), 3);
132    }
133
134    #[test]
135    fn path_line_graph() {
136        // Path 0-1-2-3 has 3 edges. L(G) is a path of length 2.
137        let mut g = Graph::with_vertices(4);
138        g.add_edge(0, 1).unwrap();
139        g.add_edge(1, 2).unwrap();
140        g.add_edge(2, 3).unwrap();
141        let lg = line_graph(&g).unwrap();
142        assert_eq!(lg.vcount(), 3);
143        assert_eq!(lg.ecount(), 2);
144    }
145
146    #[test]
147    fn star_line_graph_is_complete() {
148        // Star K_{1,4}: center 0 connected to 1,2,3,4.
149        // 4 edges, all share vertex 0 → L(G) = K_4 with 6 edges.
150        let mut g = Graph::with_vertices(5);
151        g.add_edge(0, 1).unwrap();
152        g.add_edge(0, 2).unwrap();
153        g.add_edge(0, 3).unwrap();
154        g.add_edge(0, 4).unwrap();
155        let lg = line_graph(&g).unwrap();
156        assert_eq!(lg.vcount(), 4);
157        assert_eq!(lg.ecount(), 6);
158    }
159
160    #[test]
161    fn directed_path_line_graph() {
162        // 0 -> 1 -> 2: two edges. Edge 0 (0→1) connects to edge 1 (1→2)
163        // because target of edge 0 == source of edge 1.
164        let mut g = Graph::new(3, true).unwrap();
165        g.add_edge(0, 1).unwrap();
166        g.add_edge(1, 2).unwrap();
167        let lg = line_graph(&g).unwrap();
168        assert_eq!(lg.vcount(), 2);
169        assert_eq!(lg.ecount(), 1);
170        assert!(lg.is_directed());
171    }
172
173    #[test]
174    fn directed_divergent_edges_not_connected() {
175        // 0 -> 1, 0 -> 2: both share source 0, but in the directed line
176        // graph, edge i→j connects to edge k→l only if j == k. Here
177        // targets are 1 and 2, sources are both 0. No connection.
178        let mut g = Graph::new(3, true).unwrap();
179        g.add_edge(0, 1).unwrap();
180        g.add_edge(0, 2).unwrap();
181        let lg = line_graph(&g).unwrap();
182        assert_eq!(lg.vcount(), 2);
183        assert_eq!(lg.ecount(), 0);
184    }
185
186    #[test]
187    fn directed_convergent_edges_not_connected() {
188        // 1 -> 0, 2 -> 0: edges share target 0. For connection we need
189        // target of one == source of other. Target = 0, but sources are 1,2.
190        // No connection in directed line graph.
191        let mut g = Graph::new(3, true).unwrap();
192        g.add_edge(1, 0).unwrap();
193        g.add_edge(2, 0).unwrap();
194        let lg = line_graph(&g).unwrap();
195        assert_eq!(lg.vcount(), 2);
196        assert_eq!(lg.ecount(), 0);
197    }
198
199    #[test]
200    fn self_loop_undirected() {
201        // Self-loop on vertex 0 plus edge 0-1.
202        // Edge 0: self-loop on 0, Edge 1: 0-1.
203        // Both incident to vertex 0 → connected in L(G).
204        let mut g = Graph::with_vertices(2);
205        g.add_edge(0, 0).unwrap();
206        g.add_edge(0, 1).unwrap();
207        let lg = line_graph(&g).unwrap();
208        assert_eq!(lg.vcount(), 2);
209        assert_eq!(lg.ecount(), 1);
210    }
211}