Skip to main content

rust_igraph/algorithms/operators/
products.rs

1//! Graph product operators (ALGO-OP-015, ALGO-OP-027, ALGO-OP-029).
2//!
3//! Implements six graph products: Cartesian, tensor (categorical),
4//! strong, lexicographic, rooted, and modular. Also provides a unified
5//! `graph_product` dispatcher (`igraph_product`).
6
7use crate::core::error::IgraphError;
8use crate::core::{Graph, IgraphResult, VertexId};
9
10/// Selects which graph product type to compute.
11///
12/// Passed to [`graph_product`] to select the product type.
13/// See each variant's documentation for the adjacency condition.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum GraphProductType {
16    /// Cartesian product: `(u1,v1) ~ (u2,v2)` iff
17    /// `u1=u2, v1~v2` or `u1~u2, v1=v2`.
18    Cartesian,
19    /// Lexicographic product: `(u1,v1) ~ (u2,v2)` iff
20    /// `u1~u2` or (`u1=u2` and `v1~v2`). Not commutative.
21    Lexicographic,
22    /// Strong product (normal product): union of Cartesian and tensor.
23    Strong,
24    /// Tensor (categorical/direct) product: `(u1,v1) ~ (u2,v2)` iff
25    /// `u1~u2` and `v1~v2`.
26    Tensor,
27    /// Modular product: `(u1,v1) ~ (u2,v2)` iff
28    /// (`u1~u2` and `v1~v2`) or (`u1≁u2` and `v1≁v2`),
29    /// where `u1≠u2` and `v1≠v2`. Requires simple inputs.
30    Modular,
31}
32
33/// Computes a graph product selected by `product_type`.
34///
35/// Unified dispatcher for all five non-rooted graph product types,
36/// matching the C `igraph_product()` function. The rooted product
37/// requires an extra `root` parameter and is available separately
38/// via [`rooted_product`].
39///
40/// Both graphs must have the same directedness. The result has
41/// `|V1| * |V2|` vertices where vertex `(i, j)` is identified by
42/// `i * |V2| + j`.
43///
44/// # Examples
45///
46/// ```
47/// use rust_igraph::{Graph, graph_product, GraphProductType};
48///
49/// let mut g1 = Graph::with_vertices(2);
50/// g1.add_edge(0, 1).unwrap();
51/// let mut g2 = Graph::with_vertices(2);
52/// g2.add_edge(0, 1).unwrap();
53///
54/// let p = graph_product(&g1, &g2, GraphProductType::Cartesian).unwrap();
55/// assert_eq!(p.vcount(), 4);
56/// assert_eq!(p.ecount(), 4);
57/// ```
58pub fn graph_product(
59    g1: &Graph,
60    g2: &Graph,
61    product_type: GraphProductType,
62) -> IgraphResult<Graph> {
63    match product_type {
64        GraphProductType::Cartesian => cartesian_product(g1, g2),
65        GraphProductType::Lexicographic => lexicographic_product(g1, g2),
66        GraphProductType::Strong => strong_product(g1, g2),
67        GraphProductType::Tensor => tensor_product(g1, g2),
68        GraphProductType::Modular => modular_product(g1, g2),
69    }
70}
71
72/// Computes the Cartesian product of two graphs.
73///
74/// The result has `|V1| * |V2|` vertices. Vertex `(i, j)` in the product
75/// is identified by index `i * |V2| + j`. An edge exists between `(i, j)`
76/// and `(k, l)` iff:
77/// - `i == k` and `(j, l)` is an edge in `g2`, OR
78/// - `j == l` and `(i, k)` is an edge in `g1`.
79///
80/// Both graphs must have the same directedness.
81///
82/// # Arguments
83///
84/// * `g1` — the first factor graph.
85/// * `g2` — the second factor graph.
86///
87/// # Errors
88///
89/// Returns `InvalidArgument` if the graphs differ in directedness, or if
90/// the product vertex count overflows `u32`.
91///
92/// # Examples
93///
94/// ```
95/// use rust_igraph::{Graph, cartesian_product};
96///
97/// // P2 □ P2 = C4 (path of 2 vertices □ path of 2 vertices = 4-cycle)
98/// let mut g1 = Graph::with_vertices(2);
99/// g1.add_edge(0, 1).unwrap();
100/// let mut g2 = Graph::with_vertices(2);
101/// g2.add_edge(0, 1).unwrap();
102///
103/// let p = cartesian_product(&g1, &g2).unwrap();
104/// assert_eq!(p.vcount(), 4);
105/// assert_eq!(p.ecount(), 4);
106/// ```
107pub fn cartesian_product(g1: &Graph, g2: &Graph) -> IgraphResult<Graph> {
108    check_same_directedness(g1, g2, "cartesian_product")?;
109
110    let n1 = g1.vcount();
111    let n2 = g2.vcount();
112    let directed = g1.is_directed();
113
114    let n = product_vertex_count(n1, n2)?;
115
116    if n == 0 {
117        return Graph::new(0, directed);
118    }
119
120    let e1 = g1.ecount();
121    let e2 = g2.ecount();
122
123    let total_edges = (n1 as usize)
124        .checked_mul(e2)
125        .and_then(|a| a.checked_add((n2 as usize).checked_mul(e1)?))
126        .ok_or_else(|| {
127            IgraphError::InvalidArgument("edge count overflow in cartesian_product".to_string())
128        })?;
129
130    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
131
132    // For each g1 edge (u, v), add edges (u,j)→(v,j) for all j in V2
133    for eid in 0..e1 {
134        #[allow(clippy::cast_possible_truncation)]
135        let (u, v) = g1.edge(eid as u32)?;
136        for j in 0..n2 {
137            let src = u * n2 + j;
138            let tgt = v * n2 + j;
139            edges.push((src, tgt));
140        }
141    }
142
143    // For each g2 edge (u, v), add edges (i,u)→(i,v) for all i in V1
144    for eid in 0..e2 {
145        #[allow(clippy::cast_possible_truncation)]
146        let (u, v) = g2.edge(eid as u32)?;
147        for i in 0..n1 {
148            let src = i * n2 + u;
149            let tgt = i * n2 + v;
150            edges.push((src, tgt));
151        }
152    }
153
154    let mut result = Graph::new(n, directed)?;
155    result.add_edges(edges)?;
156    Ok(result)
157}
158
159/// Computes the tensor (categorical/direct) product of two graphs.
160///
161/// The result has `|V1| * |V2|` vertices. Vertex `(i, j)` is identified by
162/// `i * |V2| + j`. An edge exists between `(i, j)` and `(k, l)` iff
163/// `(i, k)` is an edge in `g1` AND `(j, l)` is an edge in `g2`.
164///
165/// For undirected graphs, each pair of edges generates two product edges
166/// (one for each orientation).
167///
168/// Both graphs must have the same directedness.
169///
170/// # Arguments
171///
172/// * `g1` — the first factor graph.
173/// * `g2` — the second factor graph.
174///
175/// # Errors
176///
177/// Returns `InvalidArgument` if the graphs differ in directedness, or if
178/// the product vertex count overflows `u32`.
179///
180/// # Examples
181///
182/// ```
183/// use rust_igraph::{Graph, tensor_product};
184///
185/// let mut g1 = Graph::with_vertices(3);
186/// g1.add_edge(0, 1).unwrap();
187/// g1.add_edge(1, 2).unwrap();
188///
189/// let mut g2 = Graph::with_vertices(2);
190/// g2.add_edge(0, 1).unwrap();
191///
192/// let p = tensor_product(&g1, &g2).unwrap();
193/// assert_eq!(p.vcount(), 6);
194/// // 2 edges × 1 edge × 2 (undirected) = 4 edges
195/// assert_eq!(p.ecount(), 4);
196/// ```
197pub fn tensor_product(g1: &Graph, g2: &Graph) -> IgraphResult<Graph> {
198    check_same_directedness(g1, g2, "tensor_product")?;
199
200    let n1 = g1.vcount();
201    let n2 = g2.vcount();
202    let directed = g1.is_directed();
203
204    let n = product_vertex_count(n1, n2)?;
205
206    if n == 0 {
207        return Graph::new(0, directed);
208    }
209
210    let e1 = g1.ecount();
211    let e2 = g2.ecount();
212
213    let multiplier: usize = if directed { 1 } else { 2 };
214    let total_edges = e1
215        .checked_mul(e2)
216        .and_then(|a| a.checked_mul(multiplier))
217        .ok_or_else(|| {
218            IgraphError::InvalidArgument("edge count overflow in tensor_product".to_string())
219        })?;
220
221    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
222
223    for eid1 in 0..e1 {
224        #[allow(clippy::cast_possible_truncation)]
225        let (u1, v1) = g1.edge(eid1 as u32)?;
226        for eid2 in 0..e2 {
227            #[allow(clippy::cast_possible_truncation)]
228            let (u2, v2) = g2.edge(eid2 as u32)?;
229
230            // (u1, u2) → (v1, v2)
231            let src = u1 * n2 + u2;
232            let tgt = v1 * n2 + v2;
233            edges.push((src, tgt));
234
235            if !directed {
236                // (u1, v2) → (v1, u2)
237                let src2 = u1 * n2 + v2;
238                let tgt2 = v1 * n2 + u2;
239                edges.push((src2, tgt2));
240            }
241        }
242    }
243
244    let mut result = Graph::new(n, directed)?;
245    result.add_edges(edges)?;
246    Ok(result)
247}
248
249/// Computes the strong product of two graphs.
250///
251/// The strong product is the union of the Cartesian product and the tensor
252/// product. An edge exists between `(i, j)` and `(k, l)` iff:
253/// - `i == k` and `(j, l)` is an edge in `g2`, OR
254/// - `j == l` and `(i, k)` is an edge in `g1`, OR
255/// - `(i, k)` is an edge in `g1` AND `(j, l)` is an edge in `g2`.
256///
257/// Both graphs must have the same directedness.
258///
259/// # Arguments
260///
261/// * `g1` — the first factor graph.
262/// * `g2` — the second factor graph.
263///
264/// # Errors
265///
266/// Returns `InvalidArgument` if the graphs differ in directedness, or if
267/// the product vertex count overflows `u32`.
268///
269/// # Examples
270///
271/// ```
272/// use rust_igraph::{Graph, strong_product};
273///
274/// let mut g1 = Graph::with_vertices(2);
275/// g1.add_edge(0, 1).unwrap();
276/// let mut g2 = Graph::with_vertices(2);
277/// g2.add_edge(0, 1).unwrap();
278///
279/// let p = strong_product(&g1, &g2).unwrap();
280/// assert_eq!(p.vcount(), 4);
281/// // Cartesian: 4 edges + Tensor: 2 edges = 6 edges (K4 minus one edge... actually it's 5 for the strong product of K2 x K2... let me check)
282/// // Actually: K2 ⊠ K2 gives 5 edges (it's a complete graph minus one edge? No.)
283/// // Cartesian of K2×K2 = C4 (4 edges), tensor of K2×K2 = 2 edges. Combined = 6? But some may overlap... no, they don't overlap for this case.
284/// // Wait - let me recompute. C4 has edges: (0,0)-(0,1), (1,0)-(1,1), (0,0)-(1,0), (0,1)-(1,1) = 4 edges
285/// // Tensor: (0,0)-(1,1), (0,1)-(1,0) = 2 edges. Total = 6.
286/// // But K2 ⊠ K2 = K4 has 6 edges. Yes!
287/// assert_eq!(p.ecount(), 6);
288/// ```
289pub fn strong_product(g1: &Graph, g2: &Graph) -> IgraphResult<Graph> {
290    check_same_directedness(g1, g2, "strong_product")?;
291
292    let n1 = g1.vcount();
293    let n2 = g2.vcount();
294    let directed = g1.is_directed();
295
296    let n = product_vertex_count(n1, n2)?;
297
298    if n == 0 {
299        return Graph::new(0, directed);
300    }
301
302    let e1 = g1.ecount();
303    let e2 = g2.ecount();
304
305    let multiplier: usize = if directed { 1 } else { 2 };
306    let cartesian_count = (n1 as usize) * e2 + (n2 as usize) * e1;
307    let tensor_count = e1 * e2 * multiplier;
308    let total_edges = cartesian_count.checked_add(tensor_count).ok_or_else(|| {
309        IgraphError::InvalidArgument("edge count overflow in strong_product".to_string())
310    })?;
311
312    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
313
314    // Cartesian part: g1 edges × V2
315    for eid in 0..e1 {
316        #[allow(clippy::cast_possible_truncation)]
317        let (u, v) = g1.edge(eid as u32)?;
318        for j in 0..n2 {
319            edges.push((u * n2 + j, v * n2 + j));
320        }
321    }
322
323    // Cartesian part: V1 × g2 edges
324    for eid in 0..e2 {
325        #[allow(clippy::cast_possible_truncation)]
326        let (u, v) = g2.edge(eid as u32)?;
327        for i in 0..n1 {
328            edges.push((i * n2 + u, i * n2 + v));
329        }
330    }
331
332    // Tensor part
333    for eid1 in 0..e1 {
334        #[allow(clippy::cast_possible_truncation)]
335        let (u1, v1) = g1.edge(eid1 as u32)?;
336        for eid2 in 0..e2 {
337            #[allow(clippy::cast_possible_truncation)]
338            let (u2, v2) = g2.edge(eid2 as u32)?;
339            edges.push((u1 * n2 + u2, v1 * n2 + v2));
340            if !directed {
341                edges.push((u1 * n2 + v2, v1 * n2 + u2));
342            }
343        }
344    }
345
346    let mut result = Graph::new(n, directed)?;
347    result.add_edges(edges)?;
348    Ok(result)
349}
350
351/// Computes the lexicographic product of two graphs.
352///
353/// The result has `|V1| * |V2|` vertices. Vertex `(i, j)` is identified by
354/// `i * |V2| + j`. An edge exists between `(i, j)` and `(k, l)` iff:
355/// - `(i, k)` is an edge in `g1` (regardless of `j` and `l`), OR
356/// - `i == k` and `(j, l)` is an edge in `g2`.
357///
358/// Note: unlike the other products, the lexicographic product is NOT
359/// commutative.
360///
361/// Both graphs must have the same directedness.
362///
363/// # Arguments
364///
365/// * `g1` — the first factor graph (outer).
366/// * `g2` — the second factor graph (inner).
367///
368/// # Errors
369///
370/// Returns `InvalidArgument` if the graphs differ in directedness, or if
371/// the product vertex count overflows `u32`.
372///
373/// # Examples
374///
375/// ```
376/// use rust_igraph::{Graph, lexicographic_product};
377///
378/// let mut g1 = Graph::with_vertices(2);
379/// g1.add_edge(0, 1).unwrap();
380/// let g2 = Graph::with_vertices(3); // 3 isolated vertices
381///
382/// let p = lexicographic_product(&g1, &g2).unwrap();
383/// assert_eq!(p.vcount(), 6);
384/// // 1 edge in g1 × 3² = 9 cross-edges (undirected, so 9 unique pairs)
385/// assert_eq!(p.ecount(), 9);
386/// ```
387pub fn lexicographic_product(g1: &Graph, g2: &Graph) -> IgraphResult<Graph> {
388    check_same_directedness(g1, g2, "lexicographic_product")?;
389
390    let n1 = g1.vcount();
391    let n2 = g2.vcount();
392    let directed = g1.is_directed();
393
394    let n = product_vertex_count(n1, n2)?;
395
396    if n == 0 {
397        return Graph::new(0, directed);
398    }
399
400    let e1 = g1.ecount();
401    let e2 = g2.ecount();
402
403    // Edges from g2 part: V1 copies of g2's edges
404    let g2_part = (n1 as usize) * e2;
405
406    // Edges from g1 part: for each g1 edge, all pairs (j, l) in V2×V2
407    let pairs_per_edge: usize = if directed {
408        (n2 as usize) * (n2 as usize)
409    } else {
410        // For undirected, we only add (j,l) with j <= l for the unique pairs
411        // Actually igraph C adds n2*n2 pairs and lets the graph store handle it
412        // But since undirected edges are canonicalized (smaller first), we need
413        // all n2*n2 pairs to get the full set of edges
414        (n2 as usize) * (n2 as usize)
415    };
416    let g1_part = e1.checked_mul(pairs_per_edge).ok_or_else(|| {
417        IgraphError::InvalidArgument("edge count overflow in lexicographic_product".to_string())
418    })?;
419
420    let total_edges = g2_part.checked_add(g1_part).ok_or_else(|| {
421        IgraphError::InvalidArgument("edge count overflow in lexicographic_product".to_string())
422    })?;
423
424    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
425
426    // Part 1: g2 edges replicated for each vertex in V1 (same as Cartesian g2 part)
427    for eid in 0..e2 {
428        #[allow(clippy::cast_possible_truncation)]
429        let (u, v) = g2.edge(eid as u32)?;
430        for i in 0..n1 {
431            edges.push((i * n2 + u, i * n2 + v));
432        }
433    }
434
435    // Part 2: for each g1 edge (u, v), connect all (u, j) to all (v, l)
436    for eid in 0..e1 {
437        #[allow(clippy::cast_possible_truncation)]
438        let (u, v) = g1.edge(eid as u32)?;
439        for j in 0..n2 {
440            for l in 0..n2 {
441                edges.push((u * n2 + j, v * n2 + l));
442            }
443        }
444    }
445
446    let mut result = Graph::new(n, directed)?;
447    result.add_edges(edges)?;
448    Ok(result)
449}
450
451/// Computes the rooted product of two graphs.
452///
453/// The result has `|V1| * |V2|` vertices. Vertex `(i, j)` is identified by
454/// `i * |V2| + j`. An edge exists between `(i1, j1)` and `(i2, j2)` iff:
455/// - `i1 == i2` and `(j1, j2)` is an edge in `g2`, OR
456/// - `j1 == j2 == root` and `(i1, i2)` is an edge in `g1`.
457///
458/// Intuitively, this replaces each vertex of `g1` with a copy of `g2`,
459/// connecting the copies through their `root` vertices according to the
460/// edges of `g1`.
461///
462/// The number of edges in the product is `|V1| * |E2| + |E1|`.
463///
464/// Both graphs must have the same directedness.
465///
466/// # Arguments
467///
468/// * `g1` — the first factor graph (whose vertices are "replaced").
469/// * `g2` — the second factor graph (the "replacement" graph).
470/// * `root` — a vertex in `g2` used as the connection point.
471///
472/// # Errors
473///
474/// Returns `InvalidArgument` if the graphs differ in directedness, if the
475/// product vertex count overflows `u32`, or if `root >= g2.vcount()`.
476///
477/// # Examples
478///
479/// ```
480/// use rust_igraph::{Graph, rooted_product};
481///
482/// // P3 with K2 rooted at vertex 0
483/// let mut g1 = Graph::with_vertices(3);
484/// g1.add_edge(0, 1).unwrap();
485/// g1.add_edge(1, 2).unwrap();
486///
487/// let mut g2 = Graph::with_vertices(2);
488/// g2.add_edge(0, 1).unwrap();
489///
490/// let p = rooted_product(&g1, &g2, 0).unwrap();
491/// assert_eq!(p.vcount(), 6); // 3 * 2
492/// assert_eq!(p.ecount(), 5); // 3 * 1 + 2
493/// ```
494pub fn rooted_product(g1: &Graph, g2: &Graph, root: u32) -> IgraphResult<Graph> {
495    check_same_directedness(g1, g2, "rooted_product")?;
496
497    let n1 = g1.vcount();
498    let n2 = g2.vcount();
499
500    if n2 == 0 || root >= n2 {
501        return Err(IgraphError::InvalidArgument(
502            "root vertex is not present in the second graph".to_string(),
503        ));
504    }
505
506    let directed = g1.is_directed();
507    let n = product_vertex_count(n1, n2)?;
508
509    if n == 0 {
510        return Graph::new(0, directed);
511    }
512
513    let e1 = g1.ecount();
514    let e2 = g2.ecount();
515
516    // Total edges: |V1| * |E2| + |E1|
517    let total_edges = (n1 as usize)
518        .checked_mul(e2)
519        .and_then(|v| v.checked_add(e1))
520        .ok_or_else(|| {
521            IgraphError::InvalidArgument("edge count overflow in rooted_product".to_string())
522        })?;
523
524    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
525
526    // Edges from g1: connect root copies.
527    // Edge (u, v) in g1 becomes ((u, root), (v, root)) in the product.
528    for eid_usize in 0..e1 {
529        #[allow(clippy::cast_possible_truncation)]
530        let eid = eid_usize as u32;
531        let (from, to) = g1.edge(eid)?;
532        let new_from = from * n2 + root;
533        let new_to = to * n2 + root;
534        edges.push((new_from, new_to));
535    }
536
537    // Edges from g2: for each vertex j in g1, copy all g2 edges.
538    // Edge (a, b) in g2 becomes ((j, a), (j, b)) for each j.
539    for eid_usize in 0..e2 {
540        #[allow(clippy::cast_possible_truncation)]
541        let eid = eid_usize as u32;
542        let (from, to) = g2.edge(eid)?;
543        for j in 0..n1 {
544            let new_from = j * n2 + from;
545            let new_to = j * n2 + to;
546            edges.push((new_from, new_to));
547        }
548    }
549
550    let mut result = Graph::new(n, directed)?;
551    result.add_edges(edges)?;
552    Ok(result)
553}
554
555/// Computes the modular product of two graphs.
556///
557/// The result has `|V1| * |V2|` vertices. Vertex `(i, j)` is identified by
558/// `i * |V2| + j`. An edge exists between `(i1, j1)` and `(i2, j2)` iff:
559/// - `(i1, i2)` is an edge in `g1` AND `(j1, j2)` is an edge in `g2`, OR
560/// - `(i1, i2)` is NOT an edge in `g1` AND `(j1, j2)` is NOT an edge in `g2`
561///   (with `i1 ≠ i2` and `j1 ≠ j2`).
562///
563/// Both graphs must be simple (no self-loops, no multi-edges) and have the
564/// same directedness.
565///
566/// Computed as `tensor(g1, g2) ∪ tensor(complement(g1), complement(g2))`.
567///
568/// # Arguments
569///
570/// * `g1` — the first factor graph.
571/// * `g2` — the second factor graph.
572///
573/// # Errors
574///
575/// Returns `InvalidArgument` if:
576/// - the graphs differ in directedness,
577/// - either graph is not simple,
578/// - the product vertex count overflows `u32`.
579///
580/// # Examples
581///
582/// ```
583/// use rust_igraph::{Graph, modular_product};
584///
585/// // P3 modular P3: modular product of two paths on 3 vertices
586/// let mut g1 = Graph::with_vertices(3);
587/// g1.add_edge(0, 1).unwrap();
588/// g1.add_edge(1, 2).unwrap();
589/// let mut g2 = Graph::with_vertices(3);
590/// g2.add_edge(0, 1).unwrap();
591/// g2.add_edge(1, 2).unwrap();
592///
593/// let p = modular_product(&g1, &g2).unwrap();
594/// assert_eq!(p.vcount(), 9);
595/// ```
596pub fn modular_product(g1: &Graph, g2: &Graph) -> IgraphResult<Graph> {
597    use crate::algorithms::operators::complementer::complementer;
598    use crate::algorithms::operators::union::union;
599    use crate::algorithms::properties::is_simple::is_simple;
600
601    check_same_directedness(g1, g2, "modular_product")?;
602
603    let simple1 = is_simple(g1)?;
604    let simple2 = is_simple(g2)?;
605    if !simple1 || !simple2 {
606        return Err(IgraphError::InvalidArgument(
607            "modular product requires simple graphs as input".to_string(),
608        ));
609    }
610
611    let n1 = g1.vcount();
612    let n2 = g2.vcount();
613
614    if n1 == 0 || n2 == 0 {
615        let directed = g1.is_directed();
616        return Graph::new(0, directed);
617    }
618
619    let g1_compl = complementer(g1, false)?;
620    let g2_compl = complementer(g2, false)?;
621
622    let tp_orig = tensor_product(g1, g2)?;
623    let tp_compl = tensor_product(&g1_compl, &g2_compl)?;
624
625    union(&tp_orig, &tp_compl)
626}
627
628fn check_same_directedness(g1: &Graph, g2: &Graph, op: &str) -> IgraphResult<()> {
629    if g1.is_directed() != g2.is_directed() {
630        return Err(IgraphError::InvalidArgument(format!(
631            "cannot compute {op} of directed and undirected graphs"
632        )));
633    }
634    Ok(())
635}
636
637fn product_vertex_count(n1: u32, n2: u32) -> IgraphResult<u32> {
638    let count = u64::from(n1) * u64::from(n2);
639    u32::try_from(count).map_err(|_| {
640        IgraphError::InvalidArgument("product vertex count exceeds u32::MAX".to_string())
641    })
642}
643
644#[cfg(test)]
645mod tests {
646    use super::*;
647
648    // --- Cartesian product tests ---
649
650    #[test]
651    fn test_cartesian_k2_k2() {
652        let mut g1 = Graph::with_vertices(2);
653        g1.add_edge(0, 1).unwrap();
654        let mut g2 = Graph::with_vertices(2);
655        g2.add_edge(0, 1).unwrap();
656
657        let p = cartesian_product(&g1, &g2).unwrap();
658        assert_eq!(p.vcount(), 4);
659        // C4: 4 edges
660        assert_eq!(p.ecount(), 4);
661    }
662
663    #[test]
664    fn test_cartesian_k2_k3() {
665        let mut g1 = Graph::with_vertices(2);
666        g1.add_edge(0, 1).unwrap();
667
668        let mut g2 = Graph::with_vertices(3);
669        g2.add_edge(0, 1).unwrap();
670        g2.add_edge(1, 2).unwrap();
671        g2.add_edge(0, 2).unwrap();
672
673        let p = cartesian_product(&g1, &g2).unwrap();
674        assert_eq!(p.vcount(), 6);
675        // 2*3 + 3*1 = 9 edges
676        assert_eq!(p.ecount(), 9);
677    }
678
679    #[test]
680    fn test_cartesian_empty_graph() {
681        let g1 = Graph::with_vertices(0);
682        let g2 = Graph::with_vertices(3);
683        let p = cartesian_product(&g1, &g2).unwrap();
684        assert_eq!(p.vcount(), 0);
685        assert_eq!(p.ecount(), 0);
686    }
687
688    #[test]
689    fn test_cartesian_isolated_vertices() {
690        let g1 = Graph::with_vertices(3);
691        let g2 = Graph::with_vertices(4);
692        let p = cartesian_product(&g1, &g2).unwrap();
693        assert_eq!(p.vcount(), 12);
694        assert_eq!(p.ecount(), 0);
695    }
696
697    #[test]
698    fn test_cartesian_directed() {
699        let mut g1 = Graph::new(2, true).unwrap();
700        g1.add_edge(0, 1).unwrap();
701        let mut g2 = Graph::new(2, true).unwrap();
702        g2.add_edge(0, 1).unwrap();
703
704        let p = cartesian_product(&g1, &g2).unwrap();
705        assert!(p.is_directed());
706        assert_eq!(p.vcount(), 4);
707        // 2*1 + 2*1 = 4 directed edges
708        assert_eq!(p.ecount(), 4);
709    }
710
711    #[test]
712    fn test_cartesian_mixed_error() {
713        let g1 = Graph::new(2, true).unwrap();
714        let g2 = Graph::with_vertices(2);
715        assert!(cartesian_product(&g1, &g2).is_err());
716    }
717
718    // --- Tensor product tests ---
719
720    #[test]
721    fn test_tensor_k2_k2() {
722        let mut g1 = Graph::with_vertices(2);
723        g1.add_edge(0, 1).unwrap();
724        let mut g2 = Graph::with_vertices(2);
725        g2.add_edge(0, 1).unwrap();
726
727        let p = tensor_product(&g1, &g2).unwrap();
728        assert_eq!(p.vcount(), 4);
729        // 1 edge × 1 edge × 2 (undirected) = 2 edges
730        assert_eq!(p.ecount(), 2);
731    }
732
733    #[test]
734    fn test_tensor_path_k2() {
735        let mut g1 = Graph::with_vertices(3);
736        g1.add_edge(0, 1).unwrap();
737        g1.add_edge(1, 2).unwrap();
738
739        let mut g2 = Graph::with_vertices(2);
740        g2.add_edge(0, 1).unwrap();
741
742        let p = tensor_product(&g1, &g2).unwrap();
743        assert_eq!(p.vcount(), 6);
744        // 2 edges × 1 edge × 2 = 4 edges
745        assert_eq!(p.ecount(), 4);
746    }
747
748    #[test]
749    fn test_tensor_directed() {
750        let mut g1 = Graph::new(2, true).unwrap();
751        g1.add_edge(0, 1).unwrap();
752        let mut g2 = Graph::new(2, true).unwrap();
753        g2.add_edge(0, 1).unwrap();
754
755        let p = tensor_product(&g1, &g2).unwrap();
756        assert!(p.is_directed());
757        assert_eq!(p.vcount(), 4);
758        // 1 × 1 × 1 (directed) = 1 edge: (0,0)→(1,1)
759        assert_eq!(p.ecount(), 1);
760        assert_eq!(p.edge(0).unwrap(), (0, 3)); // vertex (0,0)=0, (1,1)=1*2+1=3
761    }
762
763    #[test]
764    fn test_tensor_no_edges() {
765        let g1 = Graph::with_vertices(3);
766        let mut g2 = Graph::with_vertices(2);
767        g2.add_edge(0, 1).unwrap();
768
769        let p = tensor_product(&g1, &g2).unwrap();
770        assert_eq!(p.vcount(), 6);
771        assert_eq!(p.ecount(), 0);
772    }
773
774    #[test]
775    fn test_tensor_empty() {
776        let g1 = Graph::with_vertices(0);
777        let g2 = Graph::with_vertices(5);
778        let p = tensor_product(&g1, &g2).unwrap();
779        assert_eq!(p.vcount(), 0);
780    }
781
782    // --- Strong product tests ---
783
784    #[test]
785    fn test_strong_k2_k2() {
786        let mut g1 = Graph::with_vertices(2);
787        g1.add_edge(0, 1).unwrap();
788        let mut g2 = Graph::with_vertices(2);
789        g2.add_edge(0, 1).unwrap();
790
791        let p = strong_product(&g1, &g2).unwrap();
792        assert_eq!(p.vcount(), 4);
793        // Cartesian: 4 + Tensor: 2 = 6 (= K4)
794        assert_eq!(p.ecount(), 6);
795    }
796
797    #[test]
798    fn test_strong_directed() {
799        let mut g1 = Graph::new(2, true).unwrap();
800        g1.add_edge(0, 1).unwrap();
801        let mut g2 = Graph::new(2, true).unwrap();
802        g2.add_edge(0, 1).unwrap();
803
804        let p = strong_product(&g1, &g2).unwrap();
805        assert!(p.is_directed());
806        assert_eq!(p.vcount(), 4);
807        // Cartesian: 4 + Tensor: 1 = 5
808        assert_eq!(p.ecount(), 5);
809    }
810
811    #[test]
812    fn test_strong_one_edgeless() {
813        let mut g1 = Graph::with_vertices(2);
814        g1.add_edge(0, 1).unwrap();
815        let g2 = Graph::with_vertices(3);
816
817        let p = strong_product(&g1, &g2).unwrap();
818        assert_eq!(p.vcount(), 6);
819        // Cartesian: 0 + 3*1 = 3, Tensor: 0. Total = 3
820        assert_eq!(p.ecount(), 3);
821    }
822
823    // --- Lexicographic product tests ---
824
825    #[test]
826    fn test_lexicographic_k2_isolated() {
827        let mut g1 = Graph::with_vertices(2);
828        g1.add_edge(0, 1).unwrap();
829        let g2 = Graph::with_vertices(3);
830
831        let p = lexicographic_product(&g1, &g2).unwrap();
832        assert_eq!(p.vcount(), 6);
833        // g2 part: 0 edges. g1 part: 1 edge × 3² = 9.
834        assert_eq!(p.ecount(), 9);
835    }
836
837    #[test]
838    fn test_lexicographic_k2_k2() {
839        let mut g1 = Graph::with_vertices(2);
840        g1.add_edge(0, 1).unwrap();
841        let mut g2 = Graph::with_vertices(2);
842        g2.add_edge(0, 1).unwrap();
843
844        let p = lexicographic_product(&g1, &g2).unwrap();
845        assert_eq!(p.vcount(), 4);
846        // g2 part: 2 * 1 = 2. g1 part: 1 × 2² = 4. Total = 6.
847        assert_eq!(p.ecount(), 6);
848    }
849
850    #[test]
851    fn test_lexicographic_directed() {
852        let mut g1 = Graph::new(2, true).unwrap();
853        g1.add_edge(0, 1).unwrap();
854        let mut g2 = Graph::new(3, true).unwrap();
855        g2.add_edge(0, 1).unwrap();
856
857        let p = lexicographic_product(&g1, &g2).unwrap();
858        assert!(p.is_directed());
859        assert_eq!(p.vcount(), 6);
860        // g2 part: 2 * 1 = 2. g1 part: 1 × 3² = 9. Total = 11.
861        assert_eq!(p.ecount(), 11);
862    }
863
864    #[test]
865    fn test_lexicographic_both_edgeless() {
866        let g1 = Graph::with_vertices(3);
867        let g2 = Graph::with_vertices(4);
868
869        let p = lexicographic_product(&g1, &g2).unwrap();
870        assert_eq!(p.vcount(), 12);
871        assert_eq!(p.ecount(), 0);
872    }
873
874    #[test]
875    fn test_lexicographic_not_commutative() {
876        let mut g1 = Graph::with_vertices(2);
877        g1.add_edge(0, 1).unwrap();
878        let g2 = Graph::with_vertices(3);
879
880        let p1 = lexicographic_product(&g1, &g2).unwrap();
881        let p2 = lexicographic_product(&g2, &g1).unwrap();
882        // g1[g2]: 6 vertices, 0 + 1*3² = 9 edges
883        // g2[g1]: 6 vertices, 3*1 + 0 = 3 edges (inner g1 edges replicated)
884        assert_eq!(p1.ecount(), 9);
885        assert_eq!(p2.ecount(), 3);
886        assert_ne!(p1.ecount(), p2.ecount());
887    }
888
889    // --- Rooted product tests ---
890
891    #[test]
892    fn test_rooted_p3_k2() {
893        // P3 (0-1-2) with K2 (0-1) rooted at 0
894        let mut g1 = Graph::with_vertices(3);
895        g1.add_edge(0, 1).unwrap();
896        g1.add_edge(1, 2).unwrap();
897
898        let mut g2 = Graph::with_vertices(2);
899        g2.add_edge(0, 1).unwrap();
900
901        let p = rooted_product(&g1, &g2, 0).unwrap();
902        assert_eq!(p.vcount(), 6); // 3 * 2
903        // |V1|*|E2| + |E1| = 3*1 + 2 = 5
904        assert_eq!(p.ecount(), 5);
905    }
906
907    #[test]
908    fn test_rooted_k3_p3() {
909        // K3 (triangle) with P3 (0-1-2) rooted at vertex 1
910        let mut g1 = Graph::with_vertices(3);
911        g1.add_edge(0, 1).unwrap();
912        g1.add_edge(1, 2).unwrap();
913        g1.add_edge(0, 2).unwrap();
914
915        let mut g2 = Graph::with_vertices(3);
916        g2.add_edge(0, 1).unwrap();
917        g2.add_edge(1, 2).unwrap();
918
919        let p = rooted_product(&g1, &g2, 1).unwrap();
920        assert_eq!(p.vcount(), 9); // 3 * 3
921        // |V1|*|E2| + |E1| = 3*2 + 3 = 9
922        assert_eq!(p.ecount(), 9);
923    }
924
925    #[test]
926    fn test_rooted_single_vertex_g1() {
927        // Single vertex * K2 rooted at 0
928        let g1 = Graph::with_vertices(1);
929        let mut g2 = Graph::with_vertices(2);
930        g2.add_edge(0, 1).unwrap();
931
932        let p = rooted_product(&g1, &g2, 0).unwrap();
933        assert_eq!(p.vcount(), 2); // 1 * 2
934        assert_eq!(p.ecount(), 1); // 1*1 + 0
935    }
936
937    #[test]
938    fn test_rooted_no_edges_g2() {
939        // P3 with 2 isolated vertices rooted at 0
940        let mut g1 = Graph::with_vertices(3);
941        g1.add_edge(0, 1).unwrap();
942        g1.add_edge(1, 2).unwrap();
943
944        let g2 = Graph::with_vertices(2);
945
946        let p = rooted_product(&g1, &g2, 0).unwrap();
947        assert_eq!(p.vcount(), 6);
948        // 3*0 + 2 = 2
949        assert_eq!(p.ecount(), 2);
950    }
951
952    #[test]
953    fn test_rooted_no_edges_g1() {
954        // 3 isolated vertices with K2 rooted at 0
955        let g1 = Graph::with_vertices(3);
956        let mut g2 = Graph::with_vertices(2);
957        g2.add_edge(0, 1).unwrap();
958
959        let p = rooted_product(&g1, &g2, 0).unwrap();
960        assert_eq!(p.vcount(), 6);
961        // 3*1 + 0 = 3
962        assert_eq!(p.ecount(), 3);
963    }
964
965    #[test]
966    fn test_rooted_directed() {
967        let mut g1 = Graph::new(2, true).unwrap();
968        g1.add_edge(0, 1).unwrap();
969
970        let mut g2 = Graph::new(2, true).unwrap();
971        g2.add_edge(0, 1).unwrap();
972
973        let p = rooted_product(&g1, &g2, 0).unwrap();
974        assert!(p.is_directed());
975        assert_eq!(p.vcount(), 4);
976        // 2*1 + 1 = 3
977        assert_eq!(p.ecount(), 3);
978    }
979
980    #[test]
981    fn test_rooted_invalid_root() {
982        let g1 = Graph::with_vertices(2);
983        let g2 = Graph::with_vertices(3);
984
985        assert!(rooted_product(&g1, &g2, 3).is_err());
986        assert!(rooted_product(&g1, &g2, 5).is_err());
987    }
988
989    #[test]
990    fn test_rooted_directedness_mismatch() {
991        let g1 = Graph::with_vertices(2);
992        let g2 = Graph::new(2, true).unwrap();
993
994        assert!(rooted_product(&g1, &g2, 0).is_err());
995    }
996
997    #[test]
998    fn test_rooted_empty_g2() {
999        let g1 = Graph::with_vertices(2);
1000        let g2 = Graph::with_vertices(0);
1001
1002        assert!(rooted_product(&g1, &g2, 0).is_err());
1003    }
1004
1005    // --- Modular product tests ---
1006
1007    #[test]
1008    fn test_modular_k2_k2() {
1009        let mut g1 = Graph::with_vertices(2);
1010        g1.add_edge(0, 1).unwrap();
1011        let mut g2 = Graph::with_vertices(2);
1012        g2.add_edge(0, 1).unwrap();
1013
1014        let p = modular_product(&g1, &g2).unwrap();
1015        assert_eq!(p.vcount(), 4);
1016        // tensor(K2, K2) has 2 edges: (0,0)-(1,1), (0,1)-(1,0)
1017        // complement of K2 is edgeless → tensor of complements = 0 edges
1018        // union = 2 edges
1019        assert_eq!(p.ecount(), 2);
1020    }
1021
1022    #[test]
1023    fn test_modular_p3_p3() {
1024        // P3 = 0-1-2
1025        let mut g1 = Graph::with_vertices(3);
1026        g1.add_edge(0, 1).unwrap();
1027        g1.add_edge(1, 2).unwrap();
1028        let mut g2 = Graph::with_vertices(3);
1029        g2.add_edge(0, 1).unwrap();
1030        g2.add_edge(1, 2).unwrap();
1031
1032        let p = modular_product(&g1, &g2).unwrap();
1033        assert_eq!(p.vcount(), 9);
1034        // P3 tensor P3 has 2*2*2 = 8 edges (undirected, each pair generates 2)
1035        // complement(P3) = single edge (0,2), so tensor of complements
1036        // has 1*1*2 = 2 edges
1037        // Union of 8 + 2 = 10 edges
1038        assert_eq!(p.ecount(), 10);
1039    }
1040
1041    #[test]
1042    fn test_modular_empty_graphs() {
1043        let g1 = Graph::with_vertices(0);
1044        let g2 = Graph::with_vertices(3);
1045        let p = modular_product(&g1, &g2).unwrap();
1046        assert_eq!(p.vcount(), 0);
1047        assert_eq!(p.ecount(), 0);
1048    }
1049
1050    #[test]
1051    fn test_modular_edgeless() {
1052        // Two edgeless graphs: complement is complete, so
1053        // tensor of complements generates all cross-edges
1054        let g1 = Graph::with_vertices(3);
1055        let g2 = Graph::with_vertices(3);
1056
1057        let p = modular_product(&g1, &g2).unwrap();
1058        assert_eq!(p.vcount(), 9);
1059        // tensor(edgeless, edgeless) = 0 edges
1060        // tensor(K3, K3) = 3*3*2 = 18 edges
1061        // union = 18 edges
1062        assert_eq!(p.ecount(), 18);
1063    }
1064
1065    #[test]
1066    fn test_modular_complete_graphs() {
1067        // K3 modular K3: tensor of originals + tensor of edgeless complements
1068        let mut g1 = Graph::with_vertices(3);
1069        for i in 0..3u32 {
1070            for j in (i + 1)..3 {
1071                g1.add_edge(i, j).unwrap();
1072            }
1073        }
1074        let mut g2 = Graph::with_vertices(3);
1075        for i in 0..3u32 {
1076            for j in (i + 1)..3 {
1077                g2.add_edge(i, j).unwrap();
1078            }
1079        }
1080
1081        let p = modular_product(&g1, &g2).unwrap();
1082        assert_eq!(p.vcount(), 9);
1083        // tensor(K3, K3) = 3*3*2 = 18 edges
1084        // complement(K3) = edgeless → tensor = 0
1085        // union = 18 edges
1086        assert_eq!(p.ecount(), 18);
1087    }
1088
1089    #[test]
1090    fn test_modular_directed() {
1091        let mut g1 = Graph::new(2, true).unwrap();
1092        g1.add_edge(0, 1).unwrap();
1093        let mut g2 = Graph::new(2, true).unwrap();
1094        g2.add_edge(0, 1).unwrap();
1095
1096        let p = modular_product(&g1, &g2).unwrap();
1097        assert!(p.is_directed());
1098        assert_eq!(p.vcount(), 4);
1099        // tensor(g1, g2) directed: 1 edge (0,0)→(1,1)
1100        // complement(g1) = 1→0, complement(g2) = 1→0
1101        // tensor of complements: 1 edge (1,1)→(0,0)
1102        // union: 2 edges
1103        assert_eq!(p.ecount(), 2);
1104    }
1105
1106    #[test]
1107    fn test_modular_not_simple_error() {
1108        // Multi-edge graph
1109        let mut g1 = Graph::with_vertices(2);
1110        g1.add_edge(0, 1).unwrap();
1111        g1.add_edge(0, 1).unwrap();
1112        let g2 = Graph::with_vertices(2);
1113
1114        assert!(modular_product(&g1, &g2).is_err());
1115    }
1116
1117    #[test]
1118    fn test_modular_self_loop_error() {
1119        let mut g1 = Graph::with_vertices(2);
1120        g1.add_edge(0, 0).unwrap();
1121        let g2 = Graph::with_vertices(2);
1122
1123        assert!(modular_product(&g1, &g2).is_err());
1124    }
1125
1126    #[test]
1127    fn test_modular_mixed_error() {
1128        let g1 = Graph::new(2, true).unwrap();
1129        let g2 = Graph::with_vertices(2);
1130        assert!(modular_product(&g1, &g2).is_err());
1131    }
1132
1133    // --- graph_product dispatcher tests ---
1134
1135    #[test]
1136    fn test_graph_product_dispatcher() {
1137        let mut g1 = Graph::with_vertices(2);
1138        g1.add_edge(0, 1).unwrap();
1139        let mut g2 = Graph::with_vertices(2);
1140        g2.add_edge(0, 1).unwrap();
1141
1142        let p_c = graph_product(&g1, &g2, GraphProductType::Cartesian).unwrap();
1143        assert_eq!(p_c.ecount(), cartesian_product(&g1, &g2).unwrap().ecount());
1144
1145        let p_t = graph_product(&g1, &g2, GraphProductType::Tensor).unwrap();
1146        assert_eq!(p_t.ecount(), tensor_product(&g1, &g2).unwrap().ecount());
1147
1148        let p_s = graph_product(&g1, &g2, GraphProductType::Strong).unwrap();
1149        assert_eq!(p_s.ecount(), strong_product(&g1, &g2).unwrap().ecount());
1150
1151        let p_l = graph_product(&g1, &g2, GraphProductType::Lexicographic).unwrap();
1152        assert_eq!(
1153            p_l.ecount(),
1154            lexicographic_product(&g1, &g2).unwrap().ecount()
1155        );
1156
1157        let p_m = graph_product(&g1, &g2, GraphProductType::Modular).unwrap();
1158        assert_eq!(p_m.ecount(), modular_product(&g1, &g2).unwrap().ecount());
1159    }
1160
1161    // --- Overflow tests ---
1162
1163    #[test]
1164    fn test_product_overflow() {
1165        // u32::MAX ≈ 4.3 billion; 70000² > u32::MAX
1166        let g1 = Graph::with_vertices(70000);
1167        let g2 = Graph::with_vertices(70000);
1168        assert!(cartesian_product(&g1, &g2).is_err());
1169        assert!(tensor_product(&g1, &g2).is_err());
1170        assert!(strong_product(&g1, &g2).is_err());
1171        assert!(lexicographic_product(&g1, &g2).is_err());
1172        assert!(rooted_product(&g1, &g2, 0).is_err());
1173    }
1174}