Skip to main content

rust_igraph/algorithms/isomorphism/
subiso.rs

1//! VF2 subgraph isomorphism (`ALGO-ISO-002`).
2//!
3//! Port of the VF2 *subgraph-isomorphism* engine and its three public
4//! wrappers from `references/igraph/src/isomorphism/vf2.c`:
5//!
6//! - the backtracking engine `igraph_get_subisomorphisms_vf2_callback`
7//!   (lines 1023-1514),
8//! - [`subisomorphic_vf2`] — stop at the first embedding
9//!   (`igraph_subisomorphic_vf2`, line 1570),
10//! - [`count_subisomorphisms_vf2`] — count all embeddings
11//!   (`igraph_count_subisomorphisms_vf2`, line 1652),
12//! - [`get_subisomorphisms_vf2`] — collect every embedding
13//!   (`igraph_get_subisomorphisms_vf2`, line 1717).
14//!
15//! Unlike [`isomorphic_vf2`](super::vf2::isomorphic_vf2), which decides whether
16//! two whole graphs are isomorphic, the subgraph engine looks for copies of a
17//! *pattern* `graph2` inside a (usually larger) *target* `graph1`. The match is
18//! the **non-induced** subgraph isomorphism igraph implements: every edge of
19//! the pattern must map to an edge of the target, but the target may carry
20//! extra edges among the mapped vertices. The search shares VF2's structure —
21//! candidate pairs, the `in_*`/`out_*` terminal sets, depth-first backtracking
22//! — but relaxes every exact test to an inequality: a target vertex may host a
23//! pattern vertex only if its degree is *at least* as large, and the look-ahead
24//! counts must dominate rather than match.
25//!
26//! As in upstream, self-loops are rejected up front and colours are optional
27//! per-vertex / per-edge label slices: a pattern element matches a target
28//! element only if their labels are equal. Supplying a colour for only one
29//! side makes that colour be ignored.
30
31// See the sibling `vf2` module for the rationale: this engine juggles `usize`,
32// `u32`, and `-1`-sentinel `i64` views of the same bounded vertex ids, so the
33// integer casts cannot truncate, wrap, or lose sign in practice.
34#![allow(
35    clippy::cast_possible_truncation,
36    clippy::cast_possible_wrap,
37    clippy::cast_sign_loss
38)]
39
40use super::vf2::{Flow, adjacency, contains_sorted, in_degree, out_degree, perform_pre_checks};
41use crate::core::{Graph, IgraphError, IgraphResult};
42
43/// Result of a VF2 subgraph-isomorphism test ([`subisomorphic_vf2`]).
44#[derive(Debug, Clone)]
45pub struct Vf2Subisomorphism {
46    /// Whether the pattern (`graph2`) is a subgraph of the target (`graph1`).
47    pub iso: bool,
48    /// The embedding of the pattern into the target: `map21[j]` is the vertex
49    /// of `graph1` that pattern vertex `j` maps to. Empty when no embedding
50    /// exists.
51    pub map21: Vec<u32>,
52    /// The reverse view: `map12[i]` is `Some(p)` when target vertex `i` hosts
53    /// pattern vertex `p`, or `None` when it is unused by the embedding. Empty
54    /// when no embedding exists.
55    pub map12: Vec<Option<u32>>,
56}
57
58/// The VF2 backtracking engine for (non-induced) subgraph isomorphism.
59///
60/// Faithful translation of `igraph_get_subisomorphisms_vf2_callback`. `graph1`
61/// is the target and `graph2` the pattern. For every complete embedding found,
62/// `isohandler` is invoked with `(core_1, core_2)` where `core_1[i]` is the
63/// pattern vertex hosted by target vertex `i` (or `-1`) and `core_2[j]` is the
64/// target vertex hosting pattern vertex `j`; returning [`Flow::Stop`] ends the
65/// search.
66#[allow(clippy::too_many_arguments)]
67#[allow(clippy::too_many_lines)]
68fn subiso_engine(
69    graph1: &Graph,
70    graph2: &Graph,
71    mut vertex_color1: Option<&[u32]>,
72    mut vertex_color2: Option<&[u32]>,
73    mut edge_color1: Option<&[u32]>,
74    mut edge_color2: Option<&[u32]>,
75    mut isohandler: impl FnMut(&[i64], &[i64]) -> Flow,
76) -> IgraphResult<()> {
77    perform_pre_checks(graph1, graph2)?;
78
79    let no_of_nodes1 = i64::from(graph1.vcount());
80    let no_of_nodes2 = i64::from(graph2.vcount());
81    let no_of_edges1 = graph1.ecount() as i64;
82    let no_of_edges2 = graph2.ecount() as i64;
83
84    // The pattern cannot fit into a smaller target.
85    if no_of_nodes1 < no_of_nodes2 || no_of_edges1 < no_of_edges2 {
86        return Ok(());
87    }
88
89    // Only one graph coloured -> ignore colours, matching upstream.
90    if vertex_color1.is_some() != vertex_color2.is_some() {
91        vertex_color1 = None;
92        vertex_color2 = None;
93    }
94    if edge_color1.is_some() != edge_color2.is_some() {
95        edge_color1 = None;
96        edge_color2 = None;
97    }
98
99    if let (Some(c1), Some(c2)) = (vertex_color1, vertex_color2) {
100        if c1.len() as i64 != no_of_nodes1 || c2.len() as i64 != no_of_nodes2 {
101            return Err(IgraphError::InvalidArgument(
102                "invalid vertex color vector length".into(),
103            ));
104        }
105    }
106    if let (Some(c1), Some(c2)) = (edge_color1, edge_color2) {
107        if c1.len() as i64 != no_of_edges1 || c2.len() as i64 != no_of_edges2 {
108            return Err(IgraphError::InvalidArgument(
109                "invalid edge color vector length".into(),
110            ));
111        }
112    }
113
114    let n1 = no_of_nodes1 as usize;
115    let n2 = no_of_nodes2 as usize;
116
117    let inneis_1 = adjacency(graph1, true)?;
118    let outneis_1 = adjacency(graph1, false)?;
119    let inneis_2 = adjacency(graph2, true)?;
120    let outneis_2 = adjacency(graph2, false)?;
121
122    let mut indeg1 = vec![0i64; n1];
123    let mut outdeg1 = vec![0i64; n1];
124    for v in 0..no_of_nodes1 {
125        let vu = v as usize;
126        indeg1[vu] = in_degree(graph1, v as u32)?;
127        outdeg1[vu] = out_degree(graph1, v as u32)?;
128    }
129    let mut indeg2 = vec![0i64; n2];
130    let mut outdeg2 = vec![0i64; n2];
131    for v in 0..no_of_nodes2 {
132        let vu = v as usize;
133        indeg2[vu] = in_degree(graph2, v as u32)?;
134        outdeg2[vu] = out_degree(graph2, v as u32)?;
135    }
136
137    // core_1[i] = pattern vertex hosted by target vertex i, or -1.
138    let mut core_1 = vec![-1i64; n1];
139    // core_2[j] = target vertex hosting pattern vertex j, or -1.
140    let mut core_2 = vec![-1i64; n2];
141    // in_*/out_*[v] = depth at which v entered the terminal set, or 0.
142    let mut in_1 = vec![0i64; n1];
143    let mut in_2 = vec![0i64; n2];
144    let mut out_1 = vec![0i64; n1];
145    let mut out_2 = vec![0i64; n2];
146    let mut in_1_size = 0i64;
147    let mut in_2_size = 0i64;
148    let mut out_1_size = 0i64;
149    let mut out_2_size = 0i64;
150
151    // path holds (cand1, cand2) pairs pushed on each successful step.
152    let mut path: Vec<i64> = Vec::with_capacity(2 * n2);
153    let mut matched_nodes = 0i64;
154    let mut depth = 0i64;
155    let mut last1 = -1i64;
156    let mut last2 = -1i64;
157
158    while depth >= 0 {
159        let mut cand1 = -1i64;
160        let mut cand2 = -1i64;
161
162        // Search for the next pair to try. The pattern's terminal sets must
163        // not outgrow the target's, otherwise step back.
164        if in_1_size < in_2_size || out_1_size < out_2_size {
165            // step back, nothing to do
166        } else if out_1_size > 0 && out_2_size > 0 {
167            if last2 >= 0 {
168                cand2 = last2;
169            } else {
170                let mut i = 0i64;
171                while cand2 < 0 && i < no_of_nodes2 {
172                    if out_2[i as usize] > 0 && core_2[i as usize] < 0 {
173                        cand2 = i;
174                    }
175                    i += 1;
176                }
177            }
178            let mut i = last1 + 1;
179            while cand1 < 0 && i < no_of_nodes1 {
180                if out_1[i as usize] > 0 && core_1[i as usize] < 0 {
181                    cand1 = i;
182                }
183                i += 1;
184            }
185        } else if in_1_size > 0 && in_2_size > 0 {
186            if last2 >= 0 {
187                cand2 = last2;
188            } else {
189                let mut i = 0i64;
190                while cand2 < 0 && i < no_of_nodes2 {
191                    if in_2[i as usize] > 0 && core_2[i as usize] < 0 {
192                        cand2 = i;
193                    }
194                    i += 1;
195                }
196            }
197            let mut i = last1 + 1;
198            while cand1 < 0 && i < no_of_nodes1 {
199                if in_1[i as usize] > 0 && core_1[i as usize] < 0 {
200                    cand1 = i;
201                }
202                i += 1;
203            }
204        } else {
205            if last2 >= 0 {
206                cand2 = last2;
207            } else {
208                let mut i = 0i64;
209                while cand2 < 0 && i < no_of_nodes2 {
210                    if core_2[i as usize] < 0 {
211                        cand2 = i;
212                    }
213                    i += 1;
214                }
215            }
216            let mut i = last1 + 1;
217            while cand1 < 0 && i < no_of_nodes1 {
218                if core_1[i as usize] < 0 {
219                    cand1 = i;
220                }
221                i += 1;
222            }
223        }
224
225        if cand1 < 0 || cand2 < 0 {
226            // Dead end: step back if possible, otherwise terminate.
227            if depth >= 1 {
228                last2 = path
229                    .pop()
230                    .ok_or(IgraphError::Internal("subiso: empty path"))?;
231                last1 = path
232                    .pop()
233                    .ok_or(IgraphError::Internal("subiso: empty path"))?;
234                let l1 = last1 as usize;
235                let l2 = last2 as usize;
236                matched_nodes -= 1;
237                core_1[l1] = -1;
238                core_2[l2] = -1;
239
240                if in_1[l1] != 0 {
241                    in_1_size += 1;
242                }
243                if out_1[l1] != 0 {
244                    out_1_size += 1;
245                }
246                if in_2[l2] != 0 {
247                    in_2_size += 1;
248                }
249                if out_2[l2] != 0 {
250                    out_2_size += 1;
251                }
252
253                for &node in &inneis_1[l1] {
254                    if in_1[node as usize] == depth {
255                        in_1[node as usize] = 0;
256                        in_1_size -= 1;
257                    }
258                }
259                for &node in &outneis_1[l1] {
260                    if out_1[node as usize] == depth {
261                        out_1[node as usize] = 0;
262                        out_1_size -= 1;
263                    }
264                }
265                for &node in &inneis_2[l2] {
266                    if in_2[node as usize] == depth {
267                        in_2[node as usize] = 0;
268                        in_2_size -= 1;
269                    }
270                }
271                for &node in &outneis_2[l2] {
272                    if out_2[node as usize] == depth {
273                        out_2[node as usize] = 0;
274                        out_2_size -= 1;
275                    }
276                }
277            }
278            depth -= 1;
279        } else {
280            // Step forward if the (cand1, cand2) pair is feasible.
281            let c1 = cand1 as usize;
282            let c2 = cand2 as usize;
283            let mut xin1 = 0i64;
284            let mut xin2 = 0i64;
285            let mut xout1 = 0i64;
286            let mut xout2 = 0i64;
287            let mut end = false;
288
289            // The target host must have at least the pattern vertex's degree.
290            if indeg1[c1] < indeg2[c2] || outdeg1[c1] < outdeg2[c2] {
291                end = true;
292            }
293            if let (Some(vc1), Some(vc2)) = (vertex_color1, vertex_color2) {
294                if vc1[c1] != vc2[c2] {
295                    end = true;
296                }
297            }
298
299            // cand1's neighbours only contribute look-ahead counts; the
300            // target may have extra edges among mapped vertices (non-induced).
301            for &node in &inneis_1[c1] {
302                if end {
303                    break;
304                }
305                let nu = node as usize;
306                if core_1[nu] < 0 {
307                    if in_1[nu] != 0 {
308                        xin1 += 1;
309                    }
310                    if out_1[nu] != 0 {
311                        xout1 += 1;
312                    }
313                }
314            }
315            for &node in &outneis_1[c1] {
316                if end {
317                    break;
318                }
319                let nu = node as usize;
320                if core_1[nu] < 0 {
321                    if in_1[nu] != 0 {
322                        xin1 += 1;
323                    }
324                    if out_1[nu] != 0 {
325                        xout1 += 1;
326                    }
327                }
328            }
329            // cand2's neighbours: every mapped pattern edge must exist in the
330            // target, otherwise reject; unmapped ones feed the look-ahead.
331            for &node in &inneis_2[c2] {
332                if end {
333                    break;
334                }
335                let nu = node as usize;
336                if core_2[nu] >= 0 {
337                    let node2 = core_2[nu];
338                    if !contains_sorted(&inneis_1[c1], node2 as u32) {
339                        end = true;
340                    } else if edge_color1.is_some() {
341                        let eid1 = graph1.get_eid(node2 as u32, cand1 as u32)? as usize;
342                        let eid2 = graph2.get_eid(node, cand2 as u32)? as usize;
343                        if let (Some(ec1), Some(ec2)) = (edge_color1, edge_color2) {
344                            if ec1[eid1] != ec2[eid2] {
345                                end = true;
346                            }
347                        }
348                    }
349                } else {
350                    if in_2[nu] != 0 {
351                        xin2 += 1;
352                    }
353                    if out_2[nu] != 0 {
354                        xout2 += 1;
355                    }
356                }
357            }
358            for &node in &outneis_2[c2] {
359                if end {
360                    break;
361                }
362                let nu = node as usize;
363                if core_2[nu] >= 0 {
364                    let node2 = core_2[nu];
365                    if !contains_sorted(&outneis_1[c1], node2 as u32) {
366                        end = true;
367                    } else if edge_color1.is_some() {
368                        let eid1 = graph1.get_eid(cand1 as u32, node2 as u32)? as usize;
369                        let eid2 = graph2.get_eid(cand2 as u32, node)? as usize;
370                        if let (Some(ec1), Some(ec2)) = (edge_color1, edge_color2) {
371                            if ec1[eid1] != ec2[eid2] {
372                                end = true;
373                            }
374                        }
375                    }
376                } else {
377                    if in_2[nu] != 0 {
378                        xin2 += 1;
379                    }
380                    if out_2[nu] != 0 {
381                        xout2 += 1;
382                    }
383                }
384            }
385
386            if !end && xin1 >= xin2 && xout1 >= xout2 {
387                // Add (cand1, cand2) to the mapping.
388                depth += 1;
389                path.push(cand1);
390                path.push(cand2);
391                matched_nodes += 1;
392                core_1[c1] = cand2;
393                core_2[c2] = cand1;
394
395                if in_1[c1] != 0 {
396                    in_1_size -= 1;
397                }
398                if out_1[c1] != 0 {
399                    out_1_size -= 1;
400                }
401                if in_2[c2] != 0 {
402                    in_2_size -= 1;
403                }
404                if out_2[c2] != 0 {
405                    out_2_size -= 1;
406                }
407
408                for &node in &inneis_1[c1] {
409                    let nu = node as usize;
410                    if in_1[nu] == 0 && core_1[nu] < 0 {
411                        in_1[nu] = depth;
412                        in_1_size += 1;
413                    }
414                }
415                for &node in &outneis_1[c1] {
416                    let nu = node as usize;
417                    if out_1[nu] == 0 && core_1[nu] < 0 {
418                        out_1[nu] = depth;
419                        out_1_size += 1;
420                    }
421                }
422                for &node in &inneis_2[c2] {
423                    let nu = node as usize;
424                    if in_2[nu] == 0 && core_2[nu] < 0 {
425                        in_2[nu] = depth;
426                        in_2_size += 1;
427                    }
428                }
429                for &node in &outneis_2[c2] {
430                    let nu = node as usize;
431                    if out_2[nu] == 0 && core_2[nu] < 0 {
432                        out_2[nu] = depth;
433                        out_2_size += 1;
434                    }
435                }
436
437                last1 = -1;
438                last2 = -1;
439            } else {
440                last1 = cand1;
441                last2 = cand2;
442            }
443        }
444
445        if matched_nodes == no_of_nodes2 {
446            if let Flow::Stop = isohandler(&core_1, &core_2) {
447                break;
448            }
449        }
450    }
451
452    Ok(())
453}
454
455/// Test whether the pattern `graph2` is a (non-induced) subgraph of the target
456/// `graph1`, using the VF2 algorithm.
457///
458/// Optional `vertex_color*` / `edge_color*` slices restrict the matching: a
459/// pattern vertex (edge) may map onto a target vertex (edge) only if their
460/// colours are equal. Pass `None` for uncoloured graphs; supplying a colour for
461/// only one side makes that colour be ignored (matching upstream).
462///
463/// On success [`Vf2Subisomorphism::iso`] tells whether an embedding exists; when
464/// it does, `map21` holds the embedding (pattern → target) and `map12` its
465/// reverse view, otherwise both are empty.
466///
467/// # Errors
468///
469/// Returns [`IgraphError::InvalidArgument`] if the two graphs differ in
470/// directedness, if either contains a self-loop (VF2 does not support loops),
471/// or if a supplied colour vector has the wrong length.
472///
473/// # Examples
474///
475/// ```
476/// use rust_igraph::{Graph, subisomorphic_vf2};
477///
478/// // A triangle (pattern) sits inside K4 (target).
479/// let mut k4 = Graph::new(4, false).unwrap();
480/// for i in 0..4u32 {
481///     for j in (i + 1)..4 {
482///         k4.add_edge(i, j).unwrap();
483///     }
484/// }
485/// let mut tri = Graph::new(3, false).unwrap();
486/// tri.add_edge(0, 1).unwrap();
487/// tri.add_edge(1, 2).unwrap();
488/// tri.add_edge(2, 0).unwrap();
489/// let r = subisomorphic_vf2(&k4, &tri, None, None, None, None).unwrap();
490/// assert!(r.iso);
491/// assert_eq!(r.map21.len(), 3);
492/// ```
493#[allow(clippy::too_many_arguments)]
494pub fn subisomorphic_vf2(
495    graph1: &Graph,
496    graph2: &Graph,
497    vertex_color1: Option<&[u32]>,
498    vertex_color2: Option<&[u32]>,
499    edge_color1: Option<&[u32]>,
500    edge_color2: Option<&[u32]>,
501) -> IgraphResult<Vf2Subisomorphism> {
502    let mut map21: Vec<u32> = Vec::new();
503    let mut map12: Vec<Option<u32>> = Vec::new();
504    let mut iso = false;
505
506    subiso_engine(
507        graph1,
508        graph2,
509        vertex_color1,
510        vertex_color2,
511        edge_color1,
512        edge_color2,
513        |core_1, core_2| {
514            iso = true;
515            map21 = core_2.iter().map(|&x| x as u32).collect();
516            map12 = core_1
517                .iter()
518                .map(|&x| if x < 0 { None } else { Some(x as u32) })
519                .collect();
520            Flow::Stop
521        },
522    )?;
523
524    if !iso {
525        map21.clear();
526        map12.clear();
527    }
528    Ok(Vf2Subisomorphism { iso, map21, map12 })
529}
530
531/// Count the number of VF2 subgraph-isomorphic embeddings of the pattern
532/// `graph2` into the target `graph1`.
533///
534/// Colour arguments behave as in [`subisomorphic_vf2`].
535///
536/// # Errors
537///
538/// Same conditions as [`subisomorphic_vf2`].
539///
540/// # Examples
541///
542/// ```
543/// use rust_igraph::{Graph, count_subisomorphisms_vf2};
544///
545/// // A single edge embeds into a triangle in 6 ways (3 edges x 2 directions).
546/// let mut tri = Graph::new(3, false).unwrap();
547/// tri.add_edge(0, 1).unwrap();
548/// tri.add_edge(1, 2).unwrap();
549/// tri.add_edge(2, 0).unwrap();
550/// let mut edge = Graph::new(2, false).unwrap();
551/// edge.add_edge(0, 1).unwrap();
552/// let count = count_subisomorphisms_vf2(&tri, &edge, None, None, None, None).unwrap();
553/// assert_eq!(count, 6);
554/// ```
555#[allow(clippy::too_many_arguments)]
556pub fn count_subisomorphisms_vf2(
557    graph1: &Graph,
558    graph2: &Graph,
559    vertex_color1: Option<&[u32]>,
560    vertex_color2: Option<&[u32]>,
561    edge_color1: Option<&[u32]>,
562    edge_color2: Option<&[u32]>,
563) -> IgraphResult<u64> {
564    let mut count = 0u64;
565    subiso_engine(
566        graph1,
567        graph2,
568        vertex_color1,
569        vertex_color2,
570        edge_color1,
571        edge_color2,
572        |_core_1, _core_2| {
573            count += 1;
574            Flow::Continue
575        },
576    )?;
577    Ok(count)
578}
579
580/// Collect every VF2 subgraph-isomorphic embedding of the pattern `graph2` into
581/// the target `graph1`.
582///
583/// Each returned vector is a `map21` embedding: position `j` holds the target
584/// vertex that pattern vertex `j` maps to. The list is empty when the pattern
585/// does not embed. Colour arguments behave as in [`subisomorphic_vf2`].
586///
587/// # Errors
588///
589/// Same conditions as [`subisomorphic_vf2`].
590///
591/// # Examples
592///
593/// ```
594/// use rust_igraph::{Graph, get_subisomorphisms_vf2};
595///
596/// // A path P3 embeds into a 4-cycle in 8 ways.
597/// let mut c4 = Graph::new(4, false).unwrap();
598/// c4.add_edge(0, 1).unwrap();
599/// c4.add_edge(1, 2).unwrap();
600/// c4.add_edge(2, 3).unwrap();
601/// c4.add_edge(3, 0).unwrap();
602/// let mut p3 = Graph::new(3, false).unwrap();
603/// p3.add_edge(0, 1).unwrap();
604/// p3.add_edge(1, 2).unwrap();
605/// let maps = get_subisomorphisms_vf2(&c4, &p3, None, None, None, None).unwrap();
606/// assert_eq!(maps.len(), 8);
607/// ```
608#[allow(clippy::too_many_arguments)]
609pub fn get_subisomorphisms_vf2(
610    graph1: &Graph,
611    graph2: &Graph,
612    vertex_color1: Option<&[u32]>,
613    vertex_color2: Option<&[u32]>,
614    edge_color1: Option<&[u32]>,
615    edge_color2: Option<&[u32]>,
616) -> IgraphResult<Vec<Vec<u32>>> {
617    let mut maps: Vec<Vec<u32>> = Vec::new();
618    subiso_engine(
619        graph1,
620        graph2,
621        vertex_color1,
622        vertex_color2,
623        edge_color1,
624        edge_color2,
625        |_core_1, core_2| {
626            maps.push(core_2.iter().map(|&x| x as u32).collect());
627            Flow::Continue
628        },
629    )?;
630    Ok(maps)
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636
637    /// Undirected (or directed) ring on `n` vertices: 0-1-...-(n-1)-0.
638    fn ring(n: u32, directed: bool) -> Graph {
639        let mut g = Graph::new(n, directed).expect("graph");
640        for i in 0..n {
641            g.add_edge(i, (i + 1) % n).expect("edge");
642        }
643        g
644    }
645
646    /// Build a graph from an explicit edge list.
647    fn graph_from(n: u32, directed: bool, edges: &[(u32, u32)]) -> Graph {
648        let mut g = Graph::new(n, directed).expect("graph");
649        for &(u, v) in edges {
650            g.add_edge(u, v).expect("edge");
651        }
652        g
653    }
654
655    /// Complete graph `K_n` (undirected).
656    fn complete(n: u32) -> Graph {
657        let mut g = Graph::new(n, false).expect("graph");
658        for i in 0..n {
659            for j in (i + 1)..n {
660                g.add_edge(i, j).expect("edge");
661            }
662        }
663        g
664    }
665
666    fn triangle() -> Graph {
667        graph_from(3, false, &[(0, 1), (1, 2), (2, 0)])
668    }
669
670    #[test]
671    fn triangle_embeds_into_k4() {
672        let k4 = complete(4);
673        let tri = triangle();
674        let r = subisomorphic_vf2(&k4, &tri, None, None, None, None).expect("ok");
675        assert!(r.iso);
676        assert_eq!(r.map21.len(), 3);
677        // The embedding must preserve adjacency: every pattern edge maps to a
678        // target edge.
679        for e in 0..tri.ecount() {
680            let eid = u32::try_from(e).expect("fits");
681            let (u, v) = tri.edge(eid).expect("edge");
682            let mu = r.map21[u as usize];
683            let mv = r.map21[v as usize];
684            assert!(k4.find_eid(mu, mv).expect("lookup").is_some());
685        }
686        // map12 is the reverse view of map21.
687        for (j, &t) in r.map21.iter().enumerate() {
688            assert_eq!(r.map12[t as usize], Some(j as u32));
689        }
690    }
691
692    #[test]
693    fn triangle_count_into_k4_and_k5() {
694        // K4 has 4 triangles, each with 3! = 6 orderings -> 24.
695        assert_eq!(
696            count_subisomorphisms_vf2(&complete(4), &triangle(), None, None, None, None)
697                .expect("ok"),
698            24
699        );
700        // K5 has 10 triangles x 6 = 60.
701        assert_eq!(
702            count_subisomorphisms_vf2(&complete(5), &triangle(), None, None, None, None)
703                .expect("ok"),
704            60
705        );
706    }
707
708    #[test]
709    fn edge_count_into_triangle_and_path() {
710        let edge = graph_from(2, false, &[(0, 1)]);
711        // A triangle has 3 edges, each embeddable in 2 orientations -> 6.
712        assert_eq!(
713            count_subisomorphisms_vf2(&triangle(), &edge, None, None, None, None).expect("ok"),
714            6
715        );
716        // P4 (path on 4 vertices) has 3 edges -> 6 embeddings of a single edge.
717        let p4 = graph_from(4, false, &[(0, 1), (1, 2), (2, 3)]);
718        assert_eq!(
719            count_subisomorphisms_vf2(&p4, &edge, None, None, None, None).expect("ok"),
720            6
721        );
722    }
723
724    #[test]
725    fn path_embeds_into_cycle() {
726        // P3 (path on 3 vertices, 2 edges) embeds into C4 in 8 ways.
727        let c4 = ring(4, false);
728        let p3 = graph_from(3, false, &[(0, 1), (1, 2)]);
729        let maps = get_subisomorphisms_vf2(&c4, &p3, None, None, None, None).expect("ok");
730        assert_eq!(maps.len(), 8);
731        let c = count_subisomorphisms_vf2(&c4, &p3, None, None, None, None).expect("ok");
732        assert_eq!(c, 8);
733    }
734
735    #[test]
736    fn triangle_not_in_cycle() {
737        // A 4-cycle is triangle-free.
738        let c4 = ring(4, false);
739        let r = subisomorphic_vf2(&c4, &triangle(), None, None, None, None).expect("ok");
740        assert!(!r.iso);
741        assert!(r.map21.is_empty());
742        assert!(r.map12.is_empty());
743        assert_eq!(
744            count_subisomorphisms_vf2(&c4, &triangle(), None, None, None, None).expect("ok"),
745            0
746        );
747    }
748
749    #[test]
750    fn pattern_larger_than_target_has_no_embedding() {
751        let tri = triangle();
752        let edge = graph_from(2, false, &[(0, 1)]);
753        // More pattern vertices than target vertices.
754        let r = subisomorphic_vf2(&edge, &tri, None, None, None, None).expect("ok");
755        assert!(!r.iso);
756        assert_eq!(
757            count_subisomorphisms_vf2(&edge, &tri, None, None, None, None).expect("ok"),
758            0
759        );
760        // Equal vertices, more pattern edges than target edges.
761        let path = graph_from(3, false, &[(0, 1), (1, 2)]);
762        assert_eq!(
763            count_subisomorphisms_vf2(&path, &triangle(), None, None, None, None).expect("ok"),
764            0
765        );
766    }
767
768    #[test]
769    fn self_subiso_equals_automorphism_count() {
770        // When pattern == target, every subgraph isomorphism is an
771        // automorphism, so the count matches ISO-001's verified values.
772        assert_eq!(
773            count_subisomorphisms_vf2(&ring(6, false), &ring(6, false), None, None, None, None)
774                .expect("ok"),
775            12
776        );
777        assert_eq!(
778            count_subisomorphisms_vf2(&ring(4, false), &ring(4, false), None, None, None, None)
779                .expect("ok"),
780            8
781        );
782        assert_eq!(
783            count_subisomorphisms_vf2(&ring(4, true), &ring(4, true), None, None, None, None)
784                .expect("ok"),
785            4
786        );
787    }
788
789    #[test]
790    fn empty_pattern_embeds_once() {
791        // The empty pattern embeds into any target in exactly one (empty) way.
792        let empty = Graph::new(0, false).expect("graph");
793        let tri = triangle();
794        let r = subisomorphic_vf2(&tri, &empty, None, None, None, None).expect("ok");
795        assert!(r.iso);
796        assert!(r.map21.is_empty());
797        assert_eq!(
798            count_subisomorphisms_vf2(&tri, &empty, None, None, None, None).expect("ok"),
799            1
800        );
801    }
802
803    #[test]
804    fn directed_pattern_respects_orientation() {
805        // Directed path 0->1->2 embeds into a directed triangle 0->1->2->0
806        // following the orientation; there are 3 such directed paths.
807        let dtri = ring(3, true);
808        let dpath = graph_from(3, true, &[(0, 1), (1, 2)]);
809        assert_eq!(
810            count_subisomorphisms_vf2(&dtri, &dpath, None, None, None, None).expect("ok"),
811            3
812        );
813    }
814
815    #[test]
816    fn vertex_colors_constrain_embedding() {
817        // Colour the target triangle's vertices distinctly and require the
818        // single pattern edge's endpoints to carry colours 0 and 1.
819        let tri = triangle();
820        let tcolors = [0u32, 1, 2];
821        let edge = graph_from(2, false, &[(0, 1)]);
822        let pcolors = [0u32, 1];
823        // Only the (0,1) target edge matches, in one orientation -> 1.
824        let c = count_subisomorphisms_vf2(&tri, &edge, Some(&tcolors), Some(&pcolors), None, None)
825            .expect("ok");
826        assert_eq!(c, 1);
827    }
828
829    #[test]
830    fn only_one_side_colored_ignores_colors() {
831        let tri = triangle();
832        let tcolors = [0u32, 1, 2];
833        let edge = graph_from(2, false, &[(0, 1)]);
834        // Pattern uncoloured -> colours ignored, full 6 embeddings restored.
835        let c =
836            count_subisomorphisms_vf2(&tri, &edge, Some(&tcolors), None, None, None).expect("ok");
837        assert_eq!(c, 6);
838    }
839
840    #[test]
841    fn edge_colors_constrain_embedding() {
842        // Triangle target with one distinctly coloured edge; a single
843        // coloured pattern edge can only land on the matching target edge.
844        let tri = triangle();
845        let mut tec = vec![0u32; tri.ecount()];
846        for (e, slot) in tec.iter_mut().enumerate() {
847            let eid = u32::try_from(e).expect("fits");
848            let (a, b) = tri.edge(eid).expect("edge");
849            if (a, b) == (1, 2) || (a, b) == (2, 1) {
850                *slot = 1;
851            }
852        }
853        let edge = graph_from(2, false, &[(0, 1)]);
854        let pec = [1u32];
855        // Only the colour-1 target edge matches, in 2 orientations -> 2.
856        let c =
857            count_subisomorphisms_vf2(&tri, &edge, None, None, Some(&tec), Some(&pec)).expect("ok");
858        assert_eq!(c, 2);
859    }
860
861    #[test]
862    fn get_and_count_agree() {
863        let k4 = complete(4);
864        let tri = triangle();
865        let maps = get_subisomorphisms_vf2(&k4, &tri, None, None, None, None).expect("ok");
866        let c = count_subisomorphisms_vf2(&k4, &tri, None, None, None, None).expect("ok");
867        assert_eq!(maps.len() as u64, c);
868        // Every returned embedding preserves pattern adjacency.
869        for m in &maps {
870            for e in 0..tri.ecount() {
871                let eid = u32::try_from(e).expect("fits");
872                let (u, v) = tri.edge(eid).expect("edge");
873                assert!(
874                    k4.find_eid(m[u as usize], m[v as usize])
875                        .expect("lookup")
876                        .is_some()
877                );
878            }
879        }
880    }
881
882    #[test]
883    fn self_loops_are_rejected() {
884        let g = graph_from(2, false, &[(0, 0), (0, 1)]);
885        let h = graph_from(2, false, &[(0, 1)]);
886        assert!(subisomorphic_vf2(&g, &h, None, None, None, None).is_err());
887        assert!(subisomorphic_vf2(&h, &g, None, None, None, None).is_err());
888    }
889
890    #[test]
891    fn directedness_mismatch_is_rejected() {
892        let u = ring(3, false);
893        let d = ring(3, true);
894        assert!(subisomorphic_vf2(&u, &d, None, None, None, None).is_err());
895    }
896
897    #[test]
898    fn wrong_color_vector_length_errors() {
899        let tri = triangle();
900        let edge = graph_from(2, false, &[(0, 1)]);
901        let short = [0u32];
902        // Target vertex colour vector too short.
903        assert!(
904            subisomorphic_vf2(&tri, &edge, Some(&short), Some(&[0u32, 0]), None, None).is_err()
905        );
906    }
907}
908
909#[cfg(all(test, feature = "proptest-harness"))]
910mod proptests {
911    use super::*;
912    use proptest::prelude::*;
913    use std::collections::HashSet;
914
915    /// Generate a simple, loopless graph: `n` vertices and a deduplicated,
916    /// self-loop-free edge set, with a directedness flag.
917    fn arb_simple_graph(max_v: u32) -> impl Strategy<Value = (u32, Vec<(u32, u32)>, bool)> {
918        (1..=max_v, any::<bool>()).prop_flat_map(|(n, directed)| {
919            proptest::collection::vec((0..n, 0..n), 0..=12).prop_map(move |raw| {
920                let mut seen: HashSet<(u32, u32)> = HashSet::new();
921                let mut edges = Vec::new();
922                for (u, v) in raw {
923                    if u == v {
924                        continue;
925                    }
926                    let key = if directed {
927                        (u, v)
928                    } else {
929                        (u.min(v), u.max(v))
930                    };
931                    if seen.insert(key) {
932                        edges.push((u, v));
933                    }
934                }
935                (n, edges, directed)
936            })
937        })
938    }
939
940    fn relabel(edges: &[(u32, u32)], perm: &[u32]) -> Vec<(u32, u32)> {
941        edges
942            .iter()
943            .map(|&(u, v)| (perm[u as usize], perm[v as usize]))
944            .collect()
945    }
946
947    fn build(n: u32, directed: bool, edges: &[(u32, u32)]) -> Graph {
948        let mut g = Graph::new(n, directed).expect("graph");
949        for &(u, v) in edges {
950            g.add_edge(u, v).expect("edge");
951        }
952        g
953    }
954
955    proptest! {
956        /// A graph always subgraph-embeds into itself (the identity is one such
957        /// embedding), so the self-count equals the automorphism count and is
958        /// at least 1.
959        #[test]
960        fn self_embeds((n, edges, directed) in arb_simple_graph(6)) {
961            let g = build(n, directed, &edges);
962            let r = subisomorphic_vf2(&g, &g, None, None, None, None).expect("ok");
963            prop_assert!(r.iso);
964            prop_assert_eq!(r.map21.len(), n as usize);
965            let c = count_subisomorphisms_vf2(&g, &g, None, None, None, None).expect("ok");
966            prop_assert!(c >= 1, "identity embedding always exists");
967        }
968
969        /// `get_subisomorphisms_vf2` returns exactly as many embeddings as
970        /// `count_subisomorphisms_vf2`, and every embedding preserves the
971        /// pattern's adjacency in the target.
972        #[test]
973        fn get_count_consistency_and_adjacency(
974            (n, edges, directed) in arb_simple_graph(6),
975        ) {
976            let g = build(n, directed, &edges);
977            let maps = get_subisomorphisms_vf2(&g, &g, None, None, None, None).expect("ok");
978            let c = count_subisomorphisms_vf2(&g, &g, None, None, None, None).expect("ok");
979            prop_assert_eq!(maps.len() as u64, c);
980            for m in &maps {
981                prop_assert_eq!(m.len(), n as usize);
982                for e in 0..g.ecount() {
983                    let eid = u32::try_from(e).expect("fits");
984                    let (u, v) = g.edge(eid).expect("edge");
985                    prop_assert!(
986                        g.find_eid(m[u as usize], m[v as usize]).expect("lookup").is_some()
987                    );
988                }
989            }
990        }
991
992        /// Relabelling the target by a random permutation preserves the number
993        /// of embeddings of a fixed pattern.
994        #[test]
995        fn count_invariant_under_target_relabelling(
996            (n, edges, directed) in arb_simple_graph(6),
997            seed in any::<u64>(),
998        ) {
999            let mut perm: Vec<u32> = (0..n).collect();
1000            let mut state = seed;
1001            for i in (1..n as usize).rev() {
1002                state = state
1003                    .wrapping_mul(6_364_136_223_846_793_005)
1004                    .wrapping_add(1_442_695_040_888_963_407);
1005                let j = (state >> 33) as usize % (i + 1);
1006                perm.swap(i, j);
1007            }
1008            let g = build(n, directed, &edges);
1009            let h = build(n, directed, &relabel(&edges, &perm));
1010            // Use a single edge as the fixed pattern (well-defined whenever the
1011            // graph has at least one edge); else the empty-vertex pattern.
1012            let pattern = build(2, directed, &[(0, 1)]);
1013            let cg = count_subisomorphisms_vf2(&g, &pattern, None, None, None, None).expect("ok");
1014            let ch = count_subisomorphisms_vf2(&h, &pattern, None, None, None, None).expect("ok");
1015            prop_assert_eq!(cg, ch);
1016        }
1017
1018        /// A pattern with more vertices or edges than the target never embeds.
1019        #[test]
1020        fn oversized_pattern_never_embeds(
1021            (n, edges, directed) in arb_simple_graph(5),
1022        ) {
1023            let target = build(n, directed, &edges);
1024            // Pattern = target plus one extra isolated vertex: strictly more
1025            // vertices, so no embedding can exist.
1026            let bigger = build(n + 1, directed, &edges);
1027            let c = count_subisomorphisms_vf2(&target, &bigger, None, None, None, None)
1028                .expect("ok");
1029            prop_assert_eq!(c, 0);
1030        }
1031    }
1032}