Skip to main content

rust_igraph/algorithms/properties/
multiplicity.rs

1//! Companion predicates to [`super::is_simple::is_simple`] (ALGO-PR-014).
2//!
3//! - [`has_loop`]      — does the graph contain at least one self-loop?
4//! - [`has_multiple`]  — does the graph contain at least one parallel
5//!   (multi-) edge?
6//! - [`is_loop`]       — per-edge: which edges are self-loops?
7//! - [`is_multiple`]   — per-edge: which edges are parallel duplicates?
8//! - [`count_loops`]   — number of self-loop edges (PR-014c).
9//! - [`count_multiple`] — per-edge multiplicity, the number of edges
10//!   sharing this edge's endpoint pair (PR-014c).
11//!
12//! Counterparts of `igraph_has_loop()` / `igraph_count_loops()` from
13//! `references/igraph/src/properties/loops.c` and `igraph_has_multiple()` /
14//! `igraph_count_multiple()` from `references/igraph/src/properties/multiplicity.c`.
15//!
16//! Both predicate variants run in O(|V| + |E|) (the upstream cache
17//! subsystem we don't have yet would shortcut to O(1) on subsequent
18//! calls — that's an ALGO-CORE-001f responsibility).
19
20use crate::core::cache::CachedProperty;
21use crate::core::graph::EdgeId;
22use crate::core::{Graph, IgraphResult};
23
24/// Returns `true` iff `graph` has at least one self-loop edge.
25///
26/// # Examples
27///
28/// ```
29/// use rust_igraph::{Graph, has_loop};
30///
31/// let mut g = Graph::with_vertices(2);
32/// g.add_edge(0, 1).unwrap();
33/// assert!(!has_loop(&g).unwrap());
34/// g.add_edge(1, 1).unwrap();
35/// assert!(has_loop(&g).unwrap());
36/// ```
37pub fn has_loop(graph: &Graph) -> IgraphResult<bool> {
38    if let Some(v) = graph.cache_get(CachedProperty::HasLoop) {
39        return Ok(v);
40    }
41    let m = u32::try_from(graph.ecount())
42        .map_err(|_| crate::IgraphError::Internal("ecount exceeds u32::MAX"))?;
43    for e in 0..m {
44        let (u, v) = graph.edge(e as EdgeId)?;
45        if u == v {
46            graph.cache_set(CachedProperty::HasLoop, true);
47            return Ok(true);
48        }
49    }
50    graph.cache_set(CachedProperty::HasLoop, false);
51    Ok(false)
52}
53
54/// Returns `true` iff `graph` has at least one parallel edge.
55///
56/// For undirected graphs, two self-loops at the same vertex *do* count
57/// as parallel (matching upstream `igraph_has_multiple()`), but a single
58/// self-loop does not. For directed graphs, `(a, b)` and `(b, a)` are
59/// distinct so only same-direction repeats count.
60///
61/// O(|E| log |E|) via sort-and-scan over stored edges. Storage already
62/// canonicalises undirected endpoints to `from <= to`, so `(a,b)` and
63/// `(b,a)` collapse to the same canonical pair, which is exactly the
64/// behaviour we want.
65///
66/// # Examples
67///
68/// ```
69/// use rust_igraph::{Graph, has_multiple};
70///
71/// let mut g = Graph::with_vertices(3);
72/// g.add_edge(0, 1).unwrap();
73/// g.add_edge(1, 2).unwrap();
74/// assert!(!has_multiple(&g).unwrap());
75/// g.add_edge(0, 1).unwrap();
76/// assert!(has_multiple(&g).unwrap());
77/// ```
78pub fn has_multiple(graph: &Graph) -> IgraphResult<bool> {
79    if let Some(v) = graph.cache_get(CachedProperty::HasMulti) {
80        return Ok(v);
81    }
82    let m = u32::try_from(graph.ecount())
83        .map_err(|_| crate::IgraphError::Internal("ecount exceeds u32::MAX"))?;
84    if m < 2 {
85        graph.cache_set(CachedProperty::HasMulti, false);
86        return Ok(false);
87    }
88    let mut pairs: Vec<(u32, u32)> = Vec::with_capacity(m as usize);
89    for e in 0..m {
90        pairs.push(graph.edge(e as EdgeId)?);
91    }
92    pairs.sort_unstable();
93    for i in 1..pairs.len() {
94        if pairs[i] == pairs[i - 1] {
95            graph.cache_set(CachedProperty::HasMulti, true);
96            return Ok(true);
97        }
98    }
99    graph.cache_set(CachedProperty::HasMulti, false);
100    Ok(false)
101}
102
103/// Returns a per-edge boolean vector marking self-loops.
104///
105/// `result[e] == true` iff `graph.edge(e) == (v, v)` for some `v`.
106/// Counterpart of `igraph_is_loop()` from
107/// `references/igraph/src/properties/loops.c` (with `es =
108/// igraph_ess_all()`).
109///
110/// # Examples
111///
112/// ```
113/// use rust_igraph::{Graph, is_loop};
114///
115/// let mut g = Graph::with_vertices(3);
116/// g.add_edge(0, 1).unwrap();
117/// g.add_edge(2, 2).unwrap();
118/// assert_eq!(is_loop(&g).unwrap(), vec![false, true]);
119/// ```
120pub fn is_loop(graph: &Graph) -> IgraphResult<Vec<bool>> {
121    let m = u32::try_from(graph.ecount())
122        .map_err(|_| crate::IgraphError::Internal("ecount exceeds u32::MAX"))?;
123    let mut out = Vec::with_capacity(m as usize);
124    for e in 0..m {
125        let (u, v) = graph.edge(e as EdgeId)?;
126        out.push(u == v);
127    }
128    Ok(out)
129}
130
131/// Returns a per-edge boolean vector marking multiple (parallel) edges.
132///
133/// `result[e] == true` iff there is another edge with the same canonical
134/// endpoint pair *and a smaller edge id*. Per upstream
135/// `igraph_is_multiple()`'s contract (loops.c:230): the result is true
136/// "only for the second or more appearances" — the canonical/first
137/// occurrence stays `false`, parallel copies after it are `true`.
138///
139/// O(|E| log |E|) via sort by canonical pair (within each pair group
140/// we keep edges in their natural id order, so the first id stays
141/// `false`).
142///
143/// # Examples
144///
145/// ```
146/// use rust_igraph::{Graph, is_multiple};
147///
148/// let mut g = Graph::with_vertices(3);
149/// g.add_edge(0, 1).unwrap();
150/// g.add_edge(0, 1).unwrap();
151/// g.add_edge(1, 2).unwrap();
152/// // Edge 0 is the canonical (0,1); edge 1 is the duplicate.
153/// assert_eq!(is_multiple(&g).unwrap(), vec![false, true, false]);
154/// ```
155pub fn is_multiple(graph: &Graph) -> IgraphResult<Vec<bool>> {
156    let m = u32::try_from(graph.ecount())
157        .map_err(|_| crate::IgraphError::Internal("ecount exceeds u32::MAX"))?;
158    let m_us = m as usize;
159    if m_us == 0 {
160        return Ok(Vec::new());
161    }
162    // Pull the original edges, then sort by canonical (from, to) with
163    // edge id as tiebreaker so the first-occurring id stays first.
164    let mut pairs: Vec<((u32, u32), u32)> = Vec::with_capacity(m_us);
165    for e in 0..m {
166        pairs.push((graph.edge(e as EdgeId)?, e));
167    }
168    pairs.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
169    let mut out = vec![false; m_us];
170    let mut i = 0usize;
171    while i < m_us {
172        let mut j = i + 1;
173        while j < m_us && pairs[j].0 == pairs[i].0 {
174            j += 1;
175        }
176        // Skip the canonical (first) edge in this group — leave it false.
177        for entry in &pairs[i + 1..j] {
178            out[entry.1 as usize] = true;
179        }
180        i = j;
181    }
182    Ok(out)
183}
184
185/// Counts self-loop edges in `graph`.
186///
187/// A self-loop is an edge `(v, v)`. Returns the total number of such
188/// edges, counting parallel self-loops separately.
189///
190/// Counterpart of `igraph_count_loops()` from
191/// `references/igraph/src/properties/loops.c`.
192///
193/// O(|E|) linear scan.
194///
195/// # Examples
196///
197/// ```
198/// use rust_igraph::{Graph, count_loops};
199///
200/// let mut g = Graph::with_vertices(3);
201/// g.add_edge(0, 1).unwrap();
202/// g.add_edge(1, 1).unwrap();
203/// g.add_edge(2, 2).unwrap();
204/// g.add_edge(2, 2).unwrap();
205/// assert_eq!(count_loops(&g).unwrap(), 3);
206/// ```
207pub fn count_loops(graph: &Graph) -> IgraphResult<usize> {
208    let m = u32::try_from(graph.ecount())
209        .map_err(|_| crate::IgraphError::Internal("ecount exceeds u32::MAX"))?;
210    let mut count = 0usize;
211    for e in 0..m {
212        let (u, v) = graph.edge(e as EdgeId)?;
213        if u == v {
214            count += 1;
215        }
216    }
217    Ok(count)
218}
219
220/// Per-edge multiplicity: how many edges share each edge's endpoint pair.
221///
222/// `result[e]` is the number of edges `e'` (including `e` itself) whose
223/// endpoint pair matches `e`'s. Storage already canonicalises undirected
224/// pairs to `(min, max)`, so undirected `(a,b)` and `(b,a)` collapse to
225/// the same group; directed pairs are kept ordered.
226///
227/// Self-loops at the same vertex group together — each such loop has
228/// multiplicity equal to the number of loops at that vertex (matching
229/// upstream's `IGRAPH_LOOPS_ONCE` lazy-adjlist semantics).
230///
231/// Counterpart of `igraph_count_multiple()` from
232/// `references/igraph/src/properties/multiplicity.c`.
233///
234/// O(|E| log |E|) via sort-and-group over stored edges.
235///
236/// # Examples
237///
238/// ```
239/// use rust_igraph::{Graph, count_multiple};
240///
241/// let mut g = Graph::with_vertices(3);
242/// g.add_edge(0, 1).unwrap();
243/// g.add_edge(0, 1).unwrap();
244/// g.add_edge(1, 2).unwrap();
245/// // Edge 0 and edge 1 share (0,1) → multiplicity 2 each.
246/// // Edge 2 is alone → multiplicity 1.
247/// assert_eq!(count_multiple(&g).unwrap(), vec![2, 2, 1]);
248/// ```
249pub fn count_multiple(graph: &Graph) -> IgraphResult<Vec<usize>> {
250    let m = u32::try_from(graph.ecount())
251        .map_err(|_| crate::IgraphError::Internal("ecount exceeds u32::MAX"))?;
252    let m_us = m as usize;
253    if m_us == 0 {
254        return Ok(Vec::new());
255    }
256    let mut pairs: Vec<((u32, u32), u32)> = Vec::with_capacity(m_us);
257    for e in 0..m {
258        pairs.push((graph.edge(e as EdgeId)?, e));
259    }
260    pairs.sort_unstable_by_key(|p| p.0);
261    let mut out = vec![0usize; m_us];
262    let mut i = 0usize;
263    while i < m_us {
264        let mut j = i + 1;
265        while j < m_us && pairs[j].0 == pairs[i].0 {
266            j += 1;
267        }
268        let group_size = j - i;
269        for entry in &pairs[i..j] {
270            out[entry.1 as usize] = group_size;
271        }
272        i = j;
273    }
274    Ok(out)
275}
276
277/// Multiplicity of a single edge: how many edges share the same
278/// endpoint pair as edge `eid`.
279///
280/// Counterpart of `igraph_count_multiple_1()` from
281/// `references/igraph/src/properties/multiplicity.c`.
282///
283/// O(deg(from)) — scans the neighbors of the edge's source vertex.
284///
285/// # Examples
286///
287/// ```
288/// use rust_igraph::{Graph, count_multiple_1};
289///
290/// let mut g = Graph::with_vertices(3);
291/// g.add_edge(0, 1).unwrap();
292/// g.add_edge(0, 1).unwrap();
293/// g.add_edge(1, 2).unwrap();
294/// assert_eq!(count_multiple_1(&g, 0).unwrap(), 2);
295/// assert_eq!(count_multiple_1(&g, 2).unwrap(), 1);
296/// ```
297pub fn count_multiple_1(graph: &Graph, eid: EdgeId) -> IgraphResult<usize> {
298    let m = graph.ecount();
299    if (eid as usize) >= m {
300        return Err(crate::IgraphError::InvalidArgument(format!(
301            "count_multiple_1: edge id {eid} out of range (ecount={m})"
302        )));
303    }
304    let (from, to) = graph.edge(eid)?;
305    let neighbors = graph.neighbors(from)?;
306    let mut count = neighbors.iter().filter(|&&nb| nb == to).count();
307    // Undirected self-loops: neighbors() returns the loop vertex twice
308    // per self-loop edge (LOOPS_TWICE), so halve the raw count.
309    if !graph.is_directed() && from == to {
310        count /= 2;
311    }
312    Ok(count)
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn empty_graph_has_no_loop() {
321        let g = Graph::with_vertices(0);
322        assert!(!has_loop(&g).unwrap());
323    }
324
325    #[test]
326    fn empty_graph_has_no_multi() {
327        let g = Graph::with_vertices(0);
328        assert!(!has_multiple(&g).unwrap());
329    }
330
331    #[test]
332    fn no_edge_graph_has_neither() {
333        let g = Graph::with_vertices(5);
334        assert!(!has_loop(&g).unwrap());
335        assert!(!has_multiple(&g).unwrap());
336    }
337
338    #[test]
339    fn path_has_neither() {
340        let mut g = Graph::with_vertices(4);
341        g.add_edge(0, 1).unwrap();
342        g.add_edge(1, 2).unwrap();
343        g.add_edge(2, 3).unwrap();
344        assert!(!has_loop(&g).unwrap());
345        assert!(!has_multiple(&g).unwrap());
346    }
347
348    #[test]
349    fn detects_self_loop() {
350        let mut g = Graph::with_vertices(2);
351        g.add_edge(0, 0).unwrap();
352        assert!(has_loop(&g).unwrap());
353        // A lone self-loop should NOT count as a multi-edge.
354        assert!(!has_multiple(&g).unwrap());
355    }
356
357    #[test]
358    fn detects_parallel_undirected() {
359        let mut g = Graph::with_vertices(2);
360        g.add_edge(0, 1).unwrap();
361        g.add_edge(1, 0).unwrap();
362        assert!(!has_loop(&g).unwrap());
363        assert!(has_multiple(&g).unwrap());
364    }
365
366    #[test]
367    fn detects_parallel_directed() {
368        let mut g = Graph::new(2, true).unwrap();
369        g.add_edge(0, 1).unwrap();
370        g.add_edge(0, 1).unwrap();
371        assert!(has_multiple(&g).unwrap());
372    }
373
374    #[test]
375    fn directed_mutual_pair_not_parallel() {
376        // Directed (a,b) and (b,a) are distinct → not parallel.
377        let mut g = Graph::new(2, true).unwrap();
378        g.add_edge(0, 1).unwrap();
379        g.add_edge(1, 0).unwrap();
380        assert!(!has_multiple(&g).unwrap());
381    }
382
383    #[test]
384    fn duplicate_self_loops_count_as_parallel() {
385        // Two self-loops on the same vertex: both has_loop and has_multiple
386        // return true (matches upstream igraph_has_multiple).
387        let mut g = Graph::with_vertices(1);
388        g.add_edge(0, 0).unwrap();
389        g.add_edge(0, 0).unwrap();
390        assert!(has_loop(&g).unwrap());
391        assert!(has_multiple(&g).unwrap());
392    }
393
394    #[test]
395    fn is_loop_per_edge_marks_self_loops_only() {
396        let mut g = Graph::with_vertices(3);
397        g.add_edge(0, 1).unwrap();
398        g.add_edge(2, 2).unwrap();
399        g.add_edge(1, 2).unwrap();
400        assert_eq!(is_loop(&g).unwrap(), vec![false, true, false]);
401    }
402
403    #[test]
404    fn is_loop_empty_graph() {
405        let g = Graph::with_vertices(0);
406        assert!(is_loop(&g).unwrap().is_empty());
407    }
408
409    #[test]
410    fn is_multiple_per_edge_marks_only_duplicates_after_first() {
411        // Per upstream's "second-or-more" contract, edge 0 (canonical
412        // (0,1)) stays false; edge 1 (the duplicate) is true; edge 2
413        // (lone (1,2)) is false.
414        let mut g = Graph::with_vertices(3);
415        g.add_edge(0, 1).unwrap();
416        g.add_edge(0, 1).unwrap();
417        g.add_edge(1, 2).unwrap();
418        assert_eq!(is_multiple(&g).unwrap(), vec![false, true, false]);
419    }
420
421    #[test]
422    fn is_multiple_directed_mutual_pair_not_multiple() {
423        let mut g = Graph::new(2, true).unwrap();
424        g.add_edge(0, 1).unwrap();
425        g.add_edge(1, 0).unwrap();
426        assert_eq!(is_multiple(&g).unwrap(), vec![false, false]);
427    }
428
429    #[test]
430    fn is_multiple_three_copies_first_canonical_only() {
431        // Three parallel edges → first one stays canonical, the next
432        // two flip to true.
433        let mut g = Graph::with_vertices(2);
434        for _ in 0..3 {
435            g.add_edge(0, 1).unwrap();
436        }
437        assert_eq!(is_multiple(&g).unwrap(), vec![false, true, true]);
438    }
439
440    #[test]
441    fn is_multiple_empty_graph() {
442        let g = Graph::with_vertices(0);
443        assert!(is_multiple(&g).unwrap().is_empty());
444    }
445
446    #[test]
447    fn matches_is_simple_negation_for_simple_graphs() {
448        // Simple graphs have neither.
449        let mut g = Graph::with_vertices(4);
450        for u in 0..4u32 {
451            for v in (u + 1)..4 {
452                g.add_edge(u, v).unwrap();
453            }
454        }
455        assert!(!has_loop(&g).unwrap());
456        assert!(!has_multiple(&g).unwrap());
457        assert!(crate::is_simple(&g).unwrap());
458    }
459
460    #[test]
461    fn count_loops_empty_graph() {
462        let g = Graph::with_vertices(0);
463        assert_eq!(count_loops(&g).unwrap(), 0);
464    }
465
466    #[test]
467    fn count_loops_no_loops() {
468        let mut g = Graph::with_vertices(4);
469        for (u, v) in [(0, 1), (1, 2), (2, 3)] {
470            g.add_edge(u, v).unwrap();
471        }
472        assert_eq!(count_loops(&g).unwrap(), 0);
473    }
474
475    #[test]
476    fn count_loops_counts_each_self_loop() {
477        let mut g = Graph::with_vertices(3);
478        g.add_edge(0, 1).unwrap();
479        g.add_edge(1, 1).unwrap();
480        g.add_edge(2, 2).unwrap();
481        g.add_edge(2, 2).unwrap();
482        assert_eq!(count_loops(&g).unwrap(), 3);
483    }
484
485    #[test]
486    fn count_loops_directed_self_loops() {
487        let mut g = Graph::new(3, true).unwrap();
488        g.add_edge(0, 0).unwrap();
489        g.add_edge(0, 1).unwrap();
490        g.add_edge(2, 2).unwrap();
491        assert_eq!(count_loops(&g).unwrap(), 2);
492    }
493
494    #[test]
495    fn count_multiple_empty_graph() {
496        let g = Graph::with_vertices(0);
497        assert!(count_multiple(&g).unwrap().is_empty());
498    }
499
500    #[test]
501    fn count_multiple_simple_graph_all_ones() {
502        let mut g = Graph::with_vertices(4);
503        for (u, v) in [(0, 1), (1, 2), (2, 3)] {
504            g.add_edge(u, v).unwrap();
505        }
506        assert_eq!(count_multiple(&g).unwrap(), vec![1, 1, 1]);
507    }
508
509    #[test]
510    fn count_multiple_groups_parallel_undirected() {
511        let mut g = Graph::with_vertices(3);
512        g.add_edge(0, 1).unwrap();
513        g.add_edge(1, 0).unwrap();
514        g.add_edge(1, 2).unwrap();
515        // Undirected (1,0) canonicalises to (0,1); both share group → multiplicity 2.
516        assert_eq!(count_multiple(&g).unwrap(), vec![2, 2, 1]);
517    }
518
519    #[test]
520    fn count_multiple_directed_mutual_pair_distinct() {
521        // Directed (0,1) and (1,0) are distinct pairs → multiplicity 1 each.
522        let mut g = Graph::new(2, true).unwrap();
523        g.add_edge(0, 1).unwrap();
524        g.add_edge(1, 0).unwrap();
525        assert_eq!(count_multiple(&g).unwrap(), vec![1, 1]);
526    }
527
528    #[test]
529    fn count_multiple_three_parallel_copies() {
530        let mut g = Graph::with_vertices(2);
531        for _ in 0..3 {
532            g.add_edge(0, 1).unwrap();
533        }
534        assert_eq!(count_multiple(&g).unwrap(), vec![3, 3, 3]);
535    }
536
537    #[test]
538    fn count_multiple_self_loops_grouped_per_vertex() {
539        // Two self-loops at v=0 → each has multiplicity 2.
540        // One self-loop at v=2 → multiplicity 1.
541        let mut g = Graph::with_vertices(3);
542        g.add_edge(0, 0).unwrap();
543        g.add_edge(0, 0).unwrap();
544        g.add_edge(2, 2).unwrap();
545        assert_eq!(count_multiple(&g).unwrap(), vec![2, 2, 1]);
546    }
547
548    #[test]
549    fn count_multiple_mixed_graph() {
550        // Mix of simple, parallel, and loop edges.
551        let mut g = Graph::with_vertices(4);
552        g.add_edge(0, 1).unwrap(); // 0
553        g.add_edge(1, 2).unwrap(); // 1
554        g.add_edge(0, 1).unwrap(); // 2 — parallel of 0
555        g.add_edge(3, 3).unwrap(); // 3 — lone loop
556        g.add_edge(2, 3).unwrap(); // 4
557        assert_eq!(count_multiple(&g).unwrap(), vec![2, 1, 2, 1, 1]);
558    }
559
560    #[test]
561    fn count_multiple_length_matches_ecount() {
562        let mut g = Graph::with_vertices(5);
563        for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4), (0, 4)] {
564            g.add_edge(u, v).unwrap();
565        }
566        assert_eq!(count_multiple(&g).unwrap().len(), g.ecount());
567    }
568
569    #[test]
570    fn count_multiple_consistent_with_is_multiple_and_has_multiple() {
571        let mut g = Graph::with_vertices(3);
572        g.add_edge(0, 1).unwrap();
573        g.add_edge(0, 1).unwrap();
574        g.add_edge(1, 2).unwrap();
575        let mults = count_multiple(&g).unwrap();
576        let is_mult = is_multiple(&g).unwrap();
577        // is_multiple is "second-or-more occurrence"; multiplicity > 1
578        // exactly when at least one of the group's edges is is_multiple.
579        let has_mult_via_count = mults.iter().any(|&m| m > 1);
580        assert_eq!(has_mult_via_count, has_multiple(&g).unwrap());
581        // For each edge e: is_multiple[e] implies count_multiple[e] > 1.
582        for (e, &flag) in is_mult.iter().enumerate() {
583            if flag {
584                assert!(mults[e] > 1, "is_multiple but mult = {}", mults[e]);
585            }
586        }
587    }
588
589    #[test]
590    fn count_loops_consistent_with_is_loop() {
591        let mut g = Graph::with_vertices(3);
592        g.add_edge(0, 1).unwrap();
593        g.add_edge(2, 2).unwrap();
594        g.add_edge(1, 2).unwrap();
595        let n_loops = count_loops(&g).unwrap();
596        let is_l = is_loop(&g).unwrap();
597        assert_eq!(n_loops, is_l.iter().filter(|&&b| b).count());
598    }
599
600    // ---- count_multiple_1 ----
601
602    #[test]
603    fn count_multiple_1_simple() {
604        let mut g = Graph::with_vertices(3);
605        g.add_edge(0, 1).unwrap();
606        g.add_edge(1, 2).unwrap();
607        assert_eq!(count_multiple_1(&g, 0).unwrap(), 1);
608        assert_eq!(count_multiple_1(&g, 1).unwrap(), 1);
609    }
610
611    #[test]
612    fn count_multiple_1_parallel() {
613        let mut g = Graph::with_vertices(2);
614        g.add_edge(0, 1).unwrap();
615        g.add_edge(0, 1).unwrap();
616        g.add_edge(0, 1).unwrap();
617        assert_eq!(count_multiple_1(&g, 0).unwrap(), 3);
618        assert_eq!(count_multiple_1(&g, 1).unwrap(), 3);
619        assert_eq!(count_multiple_1(&g, 2).unwrap(), 3);
620    }
621
622    #[test]
623    fn count_multiple_1_self_loop() {
624        let mut g = Graph::with_vertices(2);
625        g.add_edge(0, 0).unwrap();
626        g.add_edge(0, 0).unwrap();
627        g.add_edge(0, 1).unwrap();
628        // Two self-loops at vertex 0 → multiplicity 2.
629        assert_eq!(count_multiple_1(&g, 0).unwrap(), 2);
630        assert_eq!(count_multiple_1(&g, 1).unwrap(), 2);
631        assert_eq!(count_multiple_1(&g, 2).unwrap(), 1);
632    }
633
634    #[test]
635    fn count_multiple_1_out_of_range() {
636        let g = Graph::with_vertices(3);
637        assert!(count_multiple_1(&g, 5).is_err());
638    }
639
640    #[test]
641    fn count_multiple_1_consistent_with_count_multiple() {
642        let mut g = Graph::with_vertices(4);
643        g.add_edge(0, 1).unwrap();
644        g.add_edge(0, 1).unwrap();
645        g.add_edge(1, 2).unwrap();
646        g.add_edge(3, 3).unwrap();
647        let all = count_multiple(&g).unwrap();
648        #[allow(clippy::cast_possible_truncation)]
649        let ecount = g.ecount() as u32;
650        for eid in 0..ecount {
651            assert_eq!(
652                count_multiple_1(&g, eid).unwrap(),
653                all[eid as usize],
654                "mismatch at edge {eid}"
655            );
656        }
657    }
658}