rust_igraph/algorithms/constructors/linegraph.rs
1//! Line graph constructor (ALGO-CN-015).
2//!
3//! Counterpart of `igraph_linegraph()` in
4//! `references/igraph/src/constructors/linegraph.c:31-174`.
5//!
6//! The line graph `L(G)` of `G` has one vertex for every edge of `G`
7//! (edge id `e` ↔ L-vertex id `e`). Adjacency depends on the directedness
8//! of `G`:
9//!
10//! * **Undirected `G`**: two distinct L-vertices `e1`, `e2` are connected
11//! by an L-edge for every shared endpoint between the two G-edges.
12//! Multigraphs therefore yield multi-L-edges (one per shared endpoint).
13//! A G-self-loop counts its endpoint twice — so an L-vertex from a
14//! self-loop has a single L-self-loop on itself, plus *two* L-edges
15//! to every other G-edge sharing the same vertex (since the loop "uses
16//! two endpoints" at that vertex).
17//! * **Directed `G`**: an L-arc goes from `e ↦ e1` whenever the target of
18//! `e` equals the source of `e1` — i.e. the two G-arcs can be chained.
19//!
20//! The result is directed iff `G` is directed.
21//!
22//! Edge emission order in both branches matches upstream byte-for-byte
23//! so the three-source conformance test can compare raw ordered edge
24//! lists rather than canonicalised multisets.
25//!
26//! Time complexity: `O(|V(G)| + |E(G)|)` — exactly the same as upstream;
27//! the inner walks are bounded by `Σ_v deg(v) = 2|E|` so amortised cost
28//! per L-edge is constant.
29
30use crate::core::graph::EdgeId;
31use crate::core::{Graph, IgraphResult, VertexId};
32
33/// Build the line graph `L(G)` of `graph`.
34///
35/// `L(G)` has one vertex for every edge of `graph` (edge id `e` maps to
36/// L-vertex id `e`). For an undirected input two L-vertices are
37/// connected by one L-edge per shared endpoint between their G-edges
38/// (so multigraphs and self-loops behave per the upstream docstring).
39/// For a directed input an L-arc `e → e1` exists exactly when the target
40/// of `e` equals the source of `e1`. The result inherits `graph`'s
41/// directedness.
42///
43/// # Errors
44///
45/// Propagates errors from [`Graph::new`] / [`Graph::add_edges`]; the
46/// algorithm itself does not introduce new failure modes.
47///
48/// # Examples
49///
50/// ```
51/// use rust_igraph::{Graph, linegraph};
52///
53/// // L of the path P_4 (edges e0=(0,1), e1=(1,2), e2=(2,3)) is the path
54/// // P_3 on the three L-vertices 0, 1, 2.
55/// let mut g = Graph::with_vertices(4);
56/// g.add_edge(0, 1).unwrap();
57/// g.add_edge(1, 2).unwrap();
58/// g.add_edge(2, 3).unwrap();
59/// let l = linegraph(&g).unwrap();
60/// assert_eq!(l.vcount(), 3);
61/// assert_eq!(l.ecount(), 2);
62/// assert!(!l.is_directed());
63/// ```
64pub fn linegraph(graph: &Graph) -> IgraphResult<Graph> {
65 if graph.is_directed() {
66 linegraph_directed(graph)
67 } else {
68 linegraph_undirected(graph)
69 }
70}
71
72/// Build a per-vertex incidence table with LOOPS-TWICE semantics — the
73/// same semantics upstream's `igraph_incident(_, IGRAPH_ALL, IGRAPH_LOOPS)`
74/// produces (where `IGRAPH_LOOPS == IGRAPH_LOOPS_TWICE`). A self-loop on
75/// `v` appears in `incident[v]` exactly twice; a non-loop edge `(u, v)`
76/// with `u != v` appears once in each of `incident[u]` and `incident[v]`.
77/// Edges are pushed in global edge-id order, which makes per-source
78/// incidence lists monotonic in edge id and lets `e2 < e1` comparisons
79/// short-circuit naturally.
80fn build_incident_loops_twice(graph: &Graph) -> IgraphResult<Vec<Vec<EdgeId>>> {
81 let vcount = graph.vcount() as usize;
82 let ecount = graph.ecount();
83 let mut incident: Vec<Vec<EdgeId>> = vec![Vec::new(); vcount];
84 for eid in 0..u32::try_from(ecount).map_err(|_| {
85 crate::core::IgraphError::InvalidArgument(format!(
86 "linegraph: ecount {ecount} does not fit u32"
87 ))
88 })? {
89 let (u, v) = graph.edge(eid)?;
90 incident[u as usize].push(eid);
91 // Push at `v` too. For a self-loop u == v this pushes a second
92 // copy at the same vertex (LOOPS-TWICE).
93 incident[v as usize].push(eid);
94 }
95 Ok(incident)
96}
97
98fn linegraph_undirected(graph: &Graph) -> IgraphResult<Graph> {
99 let ecount = graph.ecount();
100 let new_vcount = u32::try_from(ecount).map_err(|_| {
101 crate::core::IgraphError::InvalidArgument(format!(
102 "linegraph: source ecount {ecount} cannot index a u32 vertex space"
103 ))
104 })?;
105
106 let incident = build_incident_loops_twice(graph)?;
107 let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
108
109 for e1 in 0..new_vcount {
110 let (from, to) = graph.edge(e1)?;
111
112 // Walk edges incident at `from`: emit (e1, e2) for every e2 < e1.
113 // With LOOPS-TWICE incidence a self-loop at `from` shows up here
114 // twice — which is exactly the "self-loop counts its endpoint
115 // twice" rule from the upstream docstring.
116 for &e2 in &incident[from as usize] {
117 if e2 < e1 {
118 edges.push((e1, e2));
119 }
120 }
121 // Walk edges incident at `to`. For a non-loop G-edge this is a
122 // distinct vertex from `from`; for a G-self-loop (from == to)
123 // this re-walks the same list — adding a second pass for every
124 // shared edge there.
125 for &e2 in &incident[to as usize] {
126 if e2 < e1 {
127 edges.push((e1, e2));
128 }
129 }
130 // A G-self-loop yields one L-self-loop on its L-vertex.
131 if from == to {
132 edges.push((e1, e1));
133 }
134 }
135
136 let mut out = Graph::new(new_vcount, false)?;
137 out.add_edges(edges)?;
138 Ok(out)
139}
140
141fn linegraph_directed(graph: &Graph) -> IgraphResult<Graph> {
142 let ecount = graph.ecount();
143 let new_vcount = u32::try_from(ecount).map_err(|_| {
144 crate::core::IgraphError::InvalidArgument(format!(
145 "linegraph: source ecount {ecount} cannot index a u32 vertex space"
146 ))
147 })?;
148
149 // Per-vertex incoming edge list. Build it by walking every edge in
150 // edge-id order so the per-vertex lists are sorted, exactly matching
151 // the order `igraph_incident(_, IGRAPH_IN, _)` would have produced in
152 // upstream.
153 let vcount = graph.vcount() as usize;
154 let mut incoming: Vec<Vec<EdgeId>> = vec![Vec::new(); vcount];
155 for eid in 0..new_vcount {
156 let (_from, to) = graph.edge(eid)?;
157 incoming[to as usize].push(eid);
158 }
159
160 let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
161 for e1 in 0..new_vcount {
162 let (from, _to) = graph.edge(e1)?;
163 for &e in &incoming[from as usize] {
164 // Upstream pushes (e, e1) — the incoming-at-`from` edge as
165 // source, the current edge as target, since arc `e` ends at
166 // `from` and arc `e1` starts at `from`.
167 edges.push((e, e1));
168 }
169 }
170
171 let mut out = Graph::new(new_vcount, true)?;
172 out.add_edges(edges)?;
173 Ok(out)
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 fn dump_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
181 let ec = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
182 let mut out = Vec::with_capacity(g.ecount());
183 for e in 0..ec {
184 out.push(g.edge(e).expect("edge in range"));
185 }
186 out
187 }
188
189 #[test]
190 fn empty_graph() {
191 let g = Graph::new(0, false).expect("empty G");
192 let l = linegraph(&g).expect("L(G)");
193 assert_eq!(l.vcount(), 0);
194 assert_eq!(l.ecount(), 0);
195 assert!(!l.is_directed());
196 }
197
198 #[test]
199 fn no_edges_graph() {
200 let g = Graph::new(5, false).expect("isolated vertices");
201 let l = linegraph(&g).expect("L(G)");
202 assert_eq!(l.vcount(), 0);
203 assert_eq!(l.ecount(), 0);
204 }
205
206 #[test]
207 fn path_p4_undirected() {
208 // P_4: 0-1-2-3 with edges e0=(0,1), e1=(1,2), e2=(2,3).
209 // L(P_4) = P_3 on vertices {0, 1, 2} with edges {0-1, 1-2}.
210 let mut g = Graph::with_vertices(4);
211 g.add_edge(0, 1).unwrap();
212 g.add_edge(1, 2).unwrap();
213 g.add_edge(2, 3).unwrap();
214 let l = linegraph(&g).expect("L(P_4)");
215 assert_eq!(l.vcount(), 3);
216 assert_eq!(l.ecount(), 2);
217 // Emission order per upstream: when e1=1 we push (1, 0) (shared
218 // endpoint at vertex 1); when e1=2 we push (2, 1) (shared at 2).
219 // Undirected Graph canonicalises endpoints to (min, max).
220 assert_eq!(dump_edges(&l), vec![(0, 1), (1, 2)]);
221 }
222
223 #[test]
224 fn triangle_k3_undirected() {
225 // K_3: vertices 0,1,2, edges e0=(0,1), e1=(0,2), e2=(1,2).
226 // Every pair shares an endpoint → L(K_3) = K_3.
227 let mut g = Graph::with_vertices(3);
228 g.add_edge(0, 1).unwrap();
229 g.add_edge(0, 2).unwrap();
230 g.add_edge(1, 2).unwrap();
231 let l = linegraph(&g).expect("L(K_3)");
232 assert_eq!(l.vcount(), 3);
233 assert_eq!(l.ecount(), 3);
234 // Upstream emission: e1=1 walks from=0 → e2=0 < 1: (1,0); walks
235 // to=2 → no e2<1 there. e1=2 walks from=1 → e2=0 < 2: (2,0);
236 // walks to=2 → e2=1 < 2: (2,1).
237 // Canonicalised to (min, max) by the undirected Graph store.
238 assert_eq!(dump_edges(&l), vec![(0, 1), (0, 2), (1, 2)]);
239 }
240
241 #[test]
242 fn star_s4_undirected() {
243 // Star with centre 0, leaves 1..=3. All three edges share vertex
244 // 0, so L = K_3.
245 let mut g = Graph::with_vertices(4);
246 g.add_edge(0, 1).unwrap();
247 g.add_edge(0, 2).unwrap();
248 g.add_edge(0, 3).unwrap();
249 let l = linegraph(&g).expect("L(star)");
250 assert_eq!(l.vcount(), 3);
251 assert_eq!(l.ecount(), 3);
252 }
253
254 #[test]
255 fn self_loop_undirected() {
256 // Single self-loop at vertex 0. L(G) has one vertex and one
257 // self-loop (the self-loop-on-self-loop case).
258 let mut g = Graph::with_vertices(1);
259 g.add_edge(0, 0).unwrap();
260 let l = linegraph(&g).expect("L(self-loop)");
261 assert_eq!(l.vcount(), 1);
262 assert_eq!(l.ecount(), 1);
263 assert_eq!(dump_edges(&l), vec![(0, 0)]);
264 }
265
266 #[test]
267 fn self_loop_plus_non_loop_undirected() {
268 // e0 = (0, 0) self-loop; e1 = (0, 1) non-loop. With LOOPS-TWICE:
269 // incident[0] = [0, 0, 1], incident[1] = [1].
270 // - e1=0 (self-loop): walks incident[0] twice, no e2 < 0 in
271 // either walk; from==to → push (0, 0).
272 // - e1=1 (non-loop): walks incident[from=0] = [0, 0, 1] → e2=0,
273 // e2=0 both < 1 → push (1, 0), (1, 0). Walks incident[to=1] =
274 // [1] → no e2 < 1.
275 // So output edges = [(0,0), (1,0), (1,0)] in raw order. After
276 // the undirected store canonicalises to (min, max):
277 // [(0,0), (0,1), (0,1)] — two parallel L-edges between the
278 // self-loop's L-vertex and the non-loop's L-vertex.
279 let mut g = Graph::with_vertices(2);
280 g.add_edge(0, 0).unwrap();
281 g.add_edge(0, 1).unwrap();
282 let l = linegraph(&g).expect("L");
283 assert_eq!(l.vcount(), 2);
284 assert_eq!(l.ecount(), 3);
285 assert_eq!(dump_edges(&l), vec![(0, 0), (0, 1), (0, 1)]);
286 }
287
288 #[test]
289 fn non_loop_then_self_loop_undirected() {
290 // Now swap order: e0 = (0, 1) non-loop, e1 = (0, 0) self-loop at 0.
291 // - e1=0: from=0, to=1; no e2<0 in either incident list; from!=to.
292 // No edges emitted.
293 // - e1=1: from=0, to=0 (self-loop). Walk from=0 → e2=0<1: (1,0).
294 // Walk to=0 → e2=0<1: (1,0). Push (1,1) for the self-loop.
295 // Output edges = [(1,0), (1,0), (1,1)] — TWO edges between L-vertex
296 // 1 (the self-loop) and L-vertex 0 (the non-loop) + a self-loop.
297 let mut g = Graph::with_vertices(2);
298 g.add_edge(0, 1).unwrap();
299 g.add_edge(0, 0).unwrap();
300 let l = linegraph(&g).expect("L");
301 assert_eq!(l.vcount(), 2);
302 assert_eq!(l.ecount(), 3);
303 // Canonicalised: (1,0) → (0,1); the self-loop on 1 stays as (1,1).
304 assert_eq!(dump_edges(&l), vec![(0, 1), (0, 1), (1, 1)]);
305 }
306
307 #[test]
308 fn multi_edge_undirected() {
309 // Two parallel edges between 0 and 1: e0 = (0, 1), e1 = (0, 1).
310 // Each shares BOTH endpoints, so L emits two edges (1, 0).
311 let mut g = Graph::with_vertices(2);
312 g.add_edge(0, 1).unwrap();
313 g.add_edge(0, 1).unwrap();
314 let l = linegraph(&g).expect("L");
315 assert_eq!(l.vcount(), 2);
316 assert_eq!(l.ecount(), 2);
317 assert_eq!(dump_edges(&l), vec![(0, 1), (0, 1)]);
318 }
319
320 #[test]
321 fn directed_chain() {
322 // 0 -> 1 -> 2: e0=(0,1), e1=(1,2). L has arc (e0 -> e1) because
323 // target(e0) = 1 = source(e1).
324 let mut g = Graph::new(3, true).expect("directed");
325 g.add_edge(0, 1).unwrap();
326 g.add_edge(1, 2).unwrap();
327 let l = linegraph(&g).expect("L");
328 assert_eq!(l.vcount(), 2);
329 assert_eq!(l.ecount(), 1);
330 assert!(l.is_directed());
331 assert_eq!(dump_edges(&l), vec![(0, 1)]);
332 }
333
334 #[test]
335 fn directed_fan_in() {
336 // Multiple incoming arcs at the fan vertex: 0->2, 1->2, 2->3.
337 // Edges: e0=(0,2), e1=(1,2), e2=(2,3). target(e0)=target(e1)=2,
338 // source(e2)=2 — so L gets (e0->e2), (e1->e2).
339 let mut g = Graph::new(4, true).expect("directed");
340 g.add_edge(0, 2).unwrap();
341 g.add_edge(1, 2).unwrap();
342 g.add_edge(2, 3).unwrap();
343 let l = linegraph(&g).expect("L");
344 assert_eq!(l.vcount(), 3);
345 assert_eq!(l.ecount(), 2);
346 // Upstream emits per-source: for e1=2 (source=2), incoming at 2
347 // = [e0, e1] → push (e0, 2), (e1, 2).
348 assert_eq!(dump_edges(&l), vec![(0, 2), (1, 2)]);
349 }
350
351 #[test]
352 fn directed_self_loop() {
353 // Self-loop at 0: e0 = (0, 0). target(e0) = 0 = source(e0), so
354 // L(G) has a self-loop on the single L-vertex.
355 let mut g = Graph::new(1, true).expect("directed");
356 g.add_edge(0, 0).unwrap();
357 let l = linegraph(&g).expect("L");
358 assert_eq!(l.vcount(), 1);
359 assert_eq!(l.ecount(), 1);
360 assert!(l.is_directed());
361 assert_eq!(dump_edges(&l), vec![(0, 0)]);
362 }
363
364 #[test]
365 fn directed_isolated_arcs() {
366 // 0->1 and 2->3 — no chaining possible.
367 let mut g = Graph::new(4, true).expect("directed");
368 g.add_edge(0, 1).unwrap();
369 g.add_edge(2, 3).unwrap();
370 let l = linegraph(&g).expect("L");
371 assert_eq!(l.vcount(), 2);
372 assert_eq!(l.ecount(), 0);
373 }
374}
375
376#[cfg(all(test, feature = "proptest-harness"))]
377mod proptest_invariants {
378 use super::*;
379 use proptest::collection::vec as pvec;
380 use proptest::prelude::*;
381 use std::collections::BTreeMap;
382
383 fn arb_undirected_graph() -> impl Strategy<Value = Graph> {
384 // 0..=8 vertices, 0..=12 edges. Edges may form multi-edges and
385 // self-loops since linegraph is documented for both.
386 (1u32..=8u32).prop_flat_map(|n| {
387 let edge = (0u32..n, 0u32..n);
388 pvec(edge, 0..=12).prop_map(move |raw| {
389 let mut g = Graph::with_vertices(n);
390 for (u, v) in raw {
391 g.add_edge(u, v).expect("add_edge");
392 }
393 g
394 })
395 })
396 }
397
398 fn arb_directed_graph() -> impl Strategy<Value = Graph> {
399 (1u32..=8u32).prop_flat_map(|n| {
400 let edge = (0u32..n, 0u32..n);
401 pvec(edge, 0..=12).prop_map(move |raw| {
402 let mut g = Graph::new(n, true).expect("directed");
403 for (u, v) in raw {
404 g.add_edge(u, v).expect("add_edge");
405 }
406 g
407 })
408 })
409 }
410
411 proptest! {
412 #![proptest_config(ProptestConfig::with_cases(96))]
413
414 /// L(G) has exactly `ecount(G)` vertices and inherits `G`'s directedness.
415 #[test]
416 fn vcount_and_directedness_match(g in arb_undirected_graph()) {
417 let l = linegraph(&g).expect("L(G)");
418 prop_assert_eq!(l.vcount() as usize, g.ecount());
419 prop_assert!(!l.is_directed());
420 }
421
422 #[test]
423 fn directed_vcount_and_directedness_match(g in arb_directed_graph()) {
424 let l = linegraph(&g).expect("L(G)");
425 prop_assert_eq!(l.vcount() as usize, g.ecount());
426 prop_assert!(l.is_directed());
427 }
428
429 /// Reference oracle for undirected L(G): build the expected multiset
430 /// of L-edges directly from the per-vertex endpoint multiset of G
431 /// and compare against the algorithm's output as a multiset.
432 #[test]
433 fn undirected_edge_set_matches_endpoint_oracle(g in arb_undirected_graph()) {
434 let ec = u32::try_from(g.ecount()).unwrap();
435
436 // Build LOOPS-TWICE per-vertex incidence: every edge eid is
437 // pushed at *both* endpoints; a self-loop therefore appears
438 // twice in the list of its single endpoint.
439 let vcount = g.vcount() as usize;
440 let mut incident: Vec<Vec<u32>> = vec![Vec::new(); vcount];
441 for eid in 0..ec {
442 let (u, v) = g.edge(eid).unwrap();
443 incident[u as usize].push(eid);
444 incident[v as usize].push(eid);
445 }
446
447 // Expected multiset, by closed-form rule: for every unordered
448 // pair (e1, e2) with e2 < e1, the multiplicity equals the
449 // number of shared endpoints (0, 1, or 2). For e2 == e1
450 // (self-loop in G at v), one L-self-loop is added.
451 let mut expected: BTreeMap<(u32, u32), u32> = BTreeMap::new();
452 for e1 in 0..ec {
453 let (a, b) = g.edge(e1).unwrap();
454 for &e2 in &incident[a as usize] {
455 if e2 < e1 {
456 *expected.entry((e1, e2)).or_insert(0) += 1;
457 }
458 }
459 for &e2 in &incident[b as usize] {
460 if e2 < e1 {
461 *expected.entry((e1, e2)).or_insert(0) += 1;
462 }
463 }
464 if a == b {
465 *expected.entry((e1, e1)).or_insert(0) += 1;
466 }
467 }
468
469 let l = linegraph(&g).expect("L(G)");
470 let lec = u32::try_from(l.ecount()).unwrap();
471 let mut got: BTreeMap<(u32, u32), u32> = BTreeMap::new();
472 for e in 0..lec {
473 let (u, v) = l.edge(e).unwrap();
474 // Canonicalise the unordered key: smaller, larger.
475 let key = if u >= v { (u, v) } else { (v, u) };
476 *got.entry(key).or_insert(0) += 1;
477 }
478 prop_assert_eq!(got, expected);
479 }
480
481 /// Reference oracle for directed L(G): for every G-edge `e1` with
482 /// source `s`, every incoming G-arc `e` at `s` produces exactly
483 /// one L-arc `(e, e1)`.
484 #[test]
485 fn directed_arc_set_matches_chain_oracle(g in arb_directed_graph()) {
486 let ec = u32::try_from(g.ecount()).unwrap();
487 let vcount = g.vcount() as usize;
488 let mut incoming: Vec<Vec<u32>> = vec![Vec::new(); vcount];
489 for eid in 0..ec {
490 let (_u, v) = g.edge(eid).unwrap();
491 incoming[v as usize].push(eid);
492 }
493 let mut expected: BTreeMap<(u32, u32), u32> = BTreeMap::new();
494 for e1 in 0..ec {
495 let (s, _t) = g.edge(e1).unwrap();
496 for &e in &incoming[s as usize] {
497 *expected.entry((e, e1)).or_insert(0) += 1;
498 }
499 }
500
501 let l = linegraph(&g).expect("L(G)");
502 let lec = u32::try_from(l.ecount()).unwrap();
503 let mut got: BTreeMap<(u32, u32), u32> = BTreeMap::new();
504 for e in 0..lec {
505 let (u, v) = l.edge(e).unwrap();
506 *got.entry((u, v)).or_insert(0) += 1;
507 }
508 prop_assert_eq!(got, expected);
509 }
510 }
511}