Skip to main content

rust_igraph/algorithms/isomorphism/
lad.rs

1//! LAD subgraph isomorphism (`ALGO-ISO-020`).
2//!
3//! Pure-Rust port of `igraph_subisomorphic_lad` and the AllDifferent-based
4//! constraint-propagation engine behind it, from
5//! `references/igraph/src/isomorphism/lad.c` (Christine Solnon's LADv1,
6//! adapted by igraph).
7//!
8//! LAD models subgraph isomorphism as a constraint satisfaction problem: each
9//! pattern vertex `u` owns a *domain* `D(u)` of candidate target vertices, and
10//! an AllDifferent constraint forbids two pattern vertices from mapping to the
11//! same target vertex. Filtering interleaves three propagators —
12//! `checkLAD` (each `(u,v)` must admit an `adj(u)`-covering matching),
13//! `ensureGACallDiff` (generalized arc consistency for AllDifferent via a
14//! residual-graph SCC analysis) and forward-checking on edges — driven to a
15//! fixpoint, then a branch-and-propagate search instantiates the smallest
16//! remaining domain.
17//!
18//! Like upstream, multi-edges are rejected; self-loops are allowed. Directed
19//! and undirected graphs are both supported (the pattern and target must agree
20//! on directedness). The optional `domains` argument restricts each pattern
21//! vertex to an explicit candidate list (e.g. to emulate vertex colours).
22
23// The LAD engine is a faithful port of a pointer/index-heavy C algorithm that
24// juggles `usize` slice indices, `u32` [`Graph`] ids and `i64` `-1`-sentinel
25// slots. Every value is bounded by the vertex/edge count, which fits `u32` by
26// the [`Graph`] contract, so the conversions cannot truncate, wrap or lose
27// sign in practice.
28#![allow(
29    clippy::cast_possible_truncation,
30    clippy::cast_possible_wrap,
31    clippy::cast_sign_loss,
32    // Faithful port: the C source uses terse, deliberately similar index names
33    // (u/v/d/gp/gt, nb_v/nb_dv, list_v/list_dv) that are clearest kept as-is.
34    clippy::many_single_char_names,
35    clippy::similar_names,
36    // Prose mentions algorithm concepts (AllDifferent, LADv1, ensureGACallDiff).
37    clippy::doc_markdown,
38    // Mirrors the C control flow `if (a != b) { ... } else { ... }`.
39    clippy::if_not_else,
40    // `Solver` collects the C out-params (firstSol/iso/want_map/want_maps).
41    clippy::struct_excessive_bools
42)]
43
44use crate::core::graph::EdgeId;
45use crate::core::{Graph, IgraphError, IgraphResult};
46
47/// Result of a first-solution LAD subgraph-isomorphism search
48/// ([`subisomorphic_lad`]).
49#[derive(Debug, Clone)]
50pub struct LadSubisomorphism {
51    /// Whether `pattern` is isomorphic to a subgraph of `target`.
52    pub iso: bool,
53    /// For each pattern vertex (in id order), the matched target vertex.
54    /// Empty when no embedding exists.
55    pub map: Vec<u32>,
56}
57
58/// igraph's `Tgraph`: out-adjacency view with loops-once, no multi-edges.
59struct Tgraph {
60    nb_vertices: i64,
61    nb_succ: Vec<i64>,
62    succ: Vec<Vec<i64>>,
63    is_edge: Vec<bool>,
64}
65
66impl Tgraph {
67    #[inline]
68    fn is_edge(&self, i: i64, j: i64) -> bool {
69        self.is_edge[(i * self.nb_vertices + j) as usize]
70    }
71}
72
73/// Build the LAD `Tgraph` from a [`Graph`], reproducing
74/// `igraph_i_lad_createGraph` (out-adjacency, `IGRAPH_LOOPS_ONCE`,
75/// multi-edges rejected).
76fn create_graph(g: &Graph) -> IgraphResult<Tgraph> {
77    let n = i64::from(g.vcount());
78    let directed = g.is_directed();
79    let mut succ: Vec<Vec<i64>> = vec![Vec::new(); n as usize];
80    for eid in 0..g.ecount() {
81        let (a, b) = g.edge(eid as EdgeId)?;
82        let (a, b) = (i64::from(a), i64::from(b));
83        if directed {
84            succ[a as usize].push(b);
85        } else if a == b {
86            succ[a as usize].push(a); // self-loop counted once
87        } else {
88            succ[a as usize].push(b);
89            succ[b as usize].push(a);
90        }
91    }
92    let mut is_edge = vec![false; (n * n) as usize];
93    let mut nb_succ = vec![0i64; n as usize];
94    for i in 0..n as usize {
95        succ[i].sort_unstable();
96        for w in 0..succ[i].len() {
97            if w > 0 && succ[i][w] == succ[i][w - 1] {
98                return Err(IgraphError::InvalidArgument(
99                    "LAD functions do not support graphs with multi-edges.".into(),
100                ));
101            }
102            let j = succ[i][w];
103            is_edge[i * n as usize + j as usize] = true;
104        }
105        nb_succ[i] = succ[i].len() as i64;
106    }
107    Ok(Tgraph {
108        nb_vertices: n,
109        nb_succ,
110        succ,
111        is_edge,
112    })
113}
114
115/// igraph's `Tdomain`: per-pattern-vertex candidate domains plus the global
116/// AllDifferent matching and the `checkLAD` matching cache.
117struct Tdomain {
118    nb_t: i64,
119    nb_val: Vec<i64>,
120    first_val: Vec<i64>,
121    val: Vec<i64>,
122    pos_in_val: Vec<i64>,
123    first_match: Vec<i64>,
124    matching: Vec<i64>,
125    next_out_to_filter: i64,
126    last_in_to_filter: i64,
127    to_filter: Vec<i64>,
128    marked_to_filter: Vec<bool>,
129    global_matching_p: Vec<i64>,
130    global_matching_t: Vec<i64>,
131}
132
133impl Tdomain {
134    #[inline]
135    fn pos(&self, u: i64, v: i64) -> i64 {
136        self.pos_in_val[(u * self.nb_t + v) as usize]
137    }
138    #[inline]
139    fn set_pos(&mut self, u: i64, v: i64, p: i64) {
140        let nb_t = self.nb_t;
141        self.pos_in_val[(u * nb_t + v) as usize] = p;
142    }
143    #[inline]
144    fn first_match(&self, u: i64, v: i64) -> i64 {
145        self.first_match[(u * self.nb_t + v) as usize]
146    }
147    #[inline]
148    fn is_in_d(&self, u: i64, v: i64) -> bool {
149        self.pos(u, v) < self.first_val[u as usize] + self.nb_val[u as usize]
150    }
151    #[inline]
152    fn to_filter_empty(&self) -> bool {
153        self.next_out_to_filter < 0
154    }
155    fn reset_to_filter(&mut self) {
156        for m in &mut self.marked_to_filter {
157            *m = false;
158        }
159        self.next_out_to_filter = -1;
160    }
161    fn next_to_filter(&mut self, size: i64) -> i64 {
162        let u = self.to_filter[self.next_out_to_filter as usize];
163        self.marked_to_filter[u as usize] = false;
164        if self.next_out_to_filter == self.last_in_to_filter {
165            self.next_out_to_filter = -1;
166        } else if self.next_out_to_filter == size - 1 {
167            self.next_out_to_filter = 0;
168        } else {
169            self.next_out_to_filter += 1;
170        }
171        u
172    }
173    fn add_to_filter(&mut self, u: i64, size: i64) {
174        if self.marked_to_filter[u as usize] {
175            return;
176        }
177        self.marked_to_filter[u as usize] = true;
178        if self.next_out_to_filter < 0 {
179            self.last_in_to_filter = 0;
180            self.next_out_to_filter = 0;
181        } else if self.last_in_to_filter == size - 1 {
182            self.last_in_to_filter = 0;
183        } else {
184            self.last_in_to_filter += 1;
185        }
186        self.to_filter[self.last_in_to_filter as usize] = u;
187    }
188}
189
190/// Find an augmenting path from pattern vertex `u` in the bipartite graph
191/// `{(u,v): v in D(u)} ∪ {(v,u): globalMatchingP[u]=v}`; update the global
192/// matching and return whether one was found
193/// (`igraph_i_lad_augmentingPath`).
194fn augmenting_path(u: i64, d: &mut Tdomain, nb_v: i64) -> bool {
195    let nb_v = nb_v as usize;
196    let mut fifo = vec![0i64; nb_v];
197    let mut pred = vec![0i64; nb_v];
198    let mut marked = vec![false; nb_v];
199    let mut next_in = 0usize;
200    let mut next_out = 0usize;
201
202    let fv = d.first_val[u as usize];
203    for i in 0..d.nb_val[u as usize] {
204        let v = d.val[(fv + i) as usize];
205        if d.global_matching_t[v as usize] < 0 {
206            d.global_matching_p[u as usize] = v;
207            d.global_matching_t[v as usize] = u;
208            return true;
209        }
210        pred[v as usize] = u;
211        fifo[next_in] = v;
212        next_in += 1;
213        marked[v as usize] = true;
214    }
215    while next_out < next_in {
216        let u2 = d.global_matching_t[fifo[next_out] as usize];
217        next_out += 1;
218        let fv2 = d.first_val[u2 as usize];
219        for i in 0..d.nb_val[u2 as usize] {
220            let v = d.val[(fv2 + i) as usize];
221            if d.global_matching_t[v as usize] < 0 {
222                let mut u2m = u2;
223                let mut vv = v;
224                while u2m != u {
225                    let v2 = d.global_matching_p[u2m as usize];
226                    d.global_matching_p[u2m as usize] = vv;
227                    d.global_matching_t[vv as usize] = u2m;
228                    vv = v2;
229                    u2m = pred[vv as usize];
230                }
231                d.global_matching_p[u as usize] = vv;
232                d.global_matching_t[vv as usize] = u;
233                return true;
234            }
235            if !marked[v as usize] {
236                pred[v as usize] = u2;
237                fifo[next_in] = v;
238                next_in += 1;
239                marked[v as usize] = true;
240            }
241        }
242    }
243    false
244}
245
246/// Remove every value but `v` from `D(u)`, queue `u`'s successors, and repair
247/// the global matching (`igraph_i_lad_removeAllValuesButOne`). Returns
248/// `false` if an inconsistency wrt the global AllDifferent is detected.
249fn remove_all_values_but_one(u: i64, v: i64, d: &mut Tdomain, gp: &Tgraph, gt: &Tgraph) -> bool {
250    for j in 0..gp.succ[u as usize].len() {
251        let s = gp.succ[u as usize][j];
252        d.add_to_filter(s, gp.nb_vertices);
253    }
254    let old_pos = d.pos(u, v);
255    let new_pos = d.first_val[u as usize];
256    d.val[old_pos as usize] = d.val[new_pos as usize];
257    d.val[new_pos as usize] = v;
258    let a = d.val[new_pos as usize];
259    let b = d.val[old_pos as usize];
260    d.set_pos(u, a, new_pos);
261    d.set_pos(u, b, old_pos);
262    d.nb_val[u as usize] = 1;
263    if d.global_matching_p[u as usize] != v {
264        let old = d.global_matching_p[u as usize];
265        d.global_matching_t[old as usize] = -1;
266        d.global_matching_p[u as usize] = -1;
267        augmenting_path(u, d, gt.nb_vertices)
268    } else {
269        true
270    }
271}
272
273/// Remove value `v` from `D(u)`, queue `u`'s successors, and repair the
274/// global matching (`igraph_i_lad_removeValue`). Returns `false` on a global
275/// AllDifferent inconsistency.
276fn remove_value(u: i64, v: i64, d: &mut Tdomain, gp: &Tgraph, gt: &Tgraph) -> bool {
277    for j in 0..gp.succ[u as usize].len() {
278        let s = gp.succ[u as usize][j];
279        d.add_to_filter(s, gp.nb_vertices);
280    }
281    let old_pos = d.pos(u, v);
282    d.nb_val[u as usize] -= 1;
283    let new_pos = d.first_val[u as usize] + d.nb_val[u as usize];
284    d.val[old_pos as usize] = d.val[new_pos as usize];
285    d.val[new_pos as usize] = v;
286    let a = d.val[old_pos as usize];
287    let b = d.val[new_pos as usize];
288    d.set_pos(u, a, old_pos);
289    d.set_pos(u, b, new_pos);
290    if d.global_matching_p[u as usize] == v {
291        d.global_matching_p[u as usize] = -1;
292        d.global_matching_t[v as usize] = -1;
293        augmenting_path(u, d, gt.nb_vertices)
294    } else {
295        true
296    }
297}
298
299/// Match every vertex in `stack` to the single value of its domain and forward
300/// -check the others wrt edges and AllDifferent
301/// (`igraph_i_lad_matchVertices`). Returns `true` if an inconsistency is
302/// detected (the C `invalid` out-param).
303fn match_vertices(
304    mut stack: Vec<i64>,
305    induced: bool,
306    d: &mut Tdomain,
307    gp: &Tgraph,
308    gt: &Tgraph,
309) -> bool {
310    while let Some(u) = stack.pop() {
311        let v = d.val[d.first_val[u as usize] as usize];
312        for u2 in 0..gp.nb_vertices {
313            if u == u2 {
314                continue;
315            }
316            let old_nb_val = d.nb_val[u2 as usize];
317            if d.is_in_d(u2, v) && !remove_value(u2, v, d, gp, gt) {
318                return true;
319            }
320            if gp.is_edge(u, u2) {
321                let mut j = d.first_val[u2 as usize];
322                while j < d.first_val[u2 as usize] + d.nb_val[u2 as usize] {
323                    let val_j = d.val[j as usize];
324                    if gt.is_edge(v, val_j) {
325                        j += 1;
326                    } else if !remove_value(u2, val_j, d, gp, gt) {
327                        return true;
328                    }
329                }
330            } else if induced {
331                if d.nb_val[u2 as usize] < gt.nb_succ[v as usize] {
332                    let mut j = d.first_val[u2 as usize];
333                    while j < d.first_val[u2 as usize] + d.nb_val[u2 as usize] {
334                        let val_j = d.val[j as usize];
335                        if !gt.is_edge(v, val_j) {
336                            j += 1;
337                        } else if !remove_value(u2, val_j, d, gp, gt) {
338                            return true;
339                        }
340                    }
341                } else {
342                    for j in 0..gt.nb_succ[v as usize] {
343                        let v2 = gt.succ[v as usize][j as usize];
344                        if d.is_in_d(u2, v2) && !remove_value(u2, v2, d, gp, gt) {
345                            return true;
346                        }
347                    }
348                }
349            }
350            if d.nb_val[u2 as usize] == 0 {
351                return true;
352            }
353            if d.nb_val[u2 as usize] == 1 && old_nb_val > 1 {
354                stack.push(u2);
355            }
356        }
357    }
358    false
359}
360
361/// Single-vertex convenience wrapper over [`match_vertices`]
362/// (`igraph_i_lad_matchVertex`). Returns `true` on success (no
363/// inconsistency).
364fn match_vertex(u: i64, induced: bool, d: &mut Tdomain, gp: &Tgraph, gt: &Tgraph) -> bool {
365    !match_vertices(vec![u], induced, d, gp, gt)
366}
367
368/// `igraph_i_lad_compare`: true iff every element of `mu` can be matched to a
369/// distinct element of `mv` that is `>=` it.
370fn compare(mu: &mut [i64], mv: &mut [i64]) -> bool {
371    mu.sort_unstable();
372    mv.sort_unstable();
373    let mut i = mv.len() as i64 - 1;
374    for j in (0..mu.len()).rev() {
375        if mu[j] > mv[i as usize] {
376            return false;
377        }
378        i -= 1;
379    }
380    true
381}
382
383/// Initialize the domains (`igraph_i_lad_initDomains`). `v in D(u)` iff the
384/// optional `domains[u]` mask admits it and the neighbour-degree multiset of
385/// `u` can be matched into that of `v`. Returns the domain `D` and whether
386/// some domain ended up empty.
387#[allow(clippy::too_many_lines)]
388fn init_domains(domains: Option<&[Vec<u32>]>, gp: &Tgraph, gt: &Tgraph) -> (Tdomain, bool) {
389    let nb_p = gp.nb_vertices;
390    let nb_t = gt.nb_vertices;
391    let mut val: Vec<i64> = Vec::new();
392    let mut nb_val = vec![0i64; nb_p as usize];
393    let mut first_val = vec![0i64; nb_p as usize];
394    let mut pos_in_val = vec![0i64; (nb_p * nb_t) as usize];
395    let mut first_match = vec![0i64; (nb_p * nb_t) as usize];
396    let global_matching_p = vec![-1i64; nb_p as usize];
397    let global_matching_t = vec![-1i64; nb_t as usize];
398    let mut to_filter = vec![0i64; nb_p as usize];
399    let mut marked_to_filter = vec![false; nb_p as usize];
400
401    let mut val_size: i64 = 0;
402    let mut matching_size: i64 = 0;
403    let mut dom = vec![false; nb_t as usize];
404
405    for u in 0..nb_p {
406        if let Some(doms) = domains {
407            dom.fill(false);
408            for &v in &doms[u as usize] {
409                dom[v as usize] = true;
410            }
411        }
412        marked_to_filter[u as usize] = true;
413        to_filter[u as usize] = u;
414        nb_val[u as usize] = 0;
415        first_val[u as usize] = val_size;
416        for v in 0..nb_t {
417            if domains.is_some() && !dom[v as usize] {
418                pos_in_val[(u * nb_t + v) as usize] = first_val[u as usize] + nb_t;
419            } else {
420                first_match[(u * nb_t + v) as usize] = matching_size;
421                matching_size += gp.nb_succ[u as usize];
422                if gp.nb_succ[u as usize] <= gt.nb_succ[v as usize] {
423                    let mut mu: Vec<i64> = gp.succ[u as usize]
424                        .iter()
425                        .map(|&w| gp.nb_succ[w as usize])
426                        .collect();
427                    let mut mv: Vec<i64> = gt.succ[v as usize]
428                        .iter()
429                        .map(|&w| gt.nb_succ[w as usize])
430                        .collect();
431                    if compare(&mut mu, &mut mv) {
432                        val.push(v);
433                        nb_val[u as usize] += 1;
434                        pos_in_val[(u * nb_t + v) as usize] = val_size;
435                        val_size += 1;
436                    } else {
437                        pos_in_val[(u * nb_t + v) as usize] = first_val[u as usize] + nb_t;
438                    }
439                } else {
440                    pos_in_val[(u * nb_t + v) as usize] = first_val[u as usize] + nb_t;
441                }
442            }
443        }
444        if nb_val[u as usize] == 0 {
445            let d = Tdomain {
446                nb_t,
447                nb_val,
448                first_val,
449                val: Vec::new(),
450                pos_in_val,
451                first_match,
452                matching: Vec::new(),
453                next_out_to_filter: -1,
454                last_in_to_filter: 0,
455                to_filter,
456                marked_to_filter,
457                global_matching_p,
458                global_matching_t,
459            };
460            return (d, true);
461        }
462    }
463
464    let matching = vec![-1i64; matching_size as usize];
465    let d = Tdomain {
466        nb_t,
467        nb_val,
468        first_val,
469        val,
470        pos_in_val,
471        first_match,
472        matching,
473        next_out_to_filter: 0,
474        last_in_to_filter: nb_p - 1,
475        to_filter,
476        marked_to_filter,
477        global_matching_p,
478        global_matching_t,
479    };
480    (d, false)
481}
482
483const WHITE: i64 = 0;
484const GREY: i64 = 1;
485const BLACK: i64 = 2;
486const TO_BE_DELETED: i64 = 3;
487const DELETED: i64 = 4;
488
489fn add_to_delete(u: i64, list: &mut [i64], nb: &mut i64, marked: &mut [i64]) {
490    if marked[u as usize] < TO_BE_DELETED {
491        list[*nb as usize] = u;
492        *nb += 1;
493        marked[u as usize] = TO_BE_DELETED;
494    }
495}
496
497/// Hopcroft-Karp style maximum bipartite matching covering `U`
498/// (`igraph_i_lad_updateMatching`). `matched_with_u[u]` is updated in place;
499/// returns `true` if no covering matching exists (the C `invalid` out-param).
500#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
501fn update_matching(
502    size_of_u: i64,
503    size_of_v: i64,
504    degree: &[i64],
505    first_adj: &[i64],
506    adj: &[i64],
507    matched_with_u: &mut [i64],
508) -> bool {
509    if size_of_u > size_of_v {
510        return true;
511    }
512    let su = size_of_u as usize;
513    let sv = size_of_v as usize;
514    let mut matched_with_v = vec![-1i64; sv];
515    let mut nb_pred = vec![0i64; sv];
516    let mut pred = vec![0i64; sv * su];
517    let mut nb_succ = vec![0i64; su];
518    let mut succ = vec![0i64; su * sv];
519    let mut list_v = vec![0i64; sv];
520    let mut list_u = vec![0i64; su];
521    let mut list_dv = vec![0i64; sv];
522    let mut list_du = vec![0i64; su];
523    let mut marked_v = vec![0i64; sv];
524    let mut marked_u = vec![0i64; su];
525    let mut unmatched = vec![0i64; su];
526    let mut pos_in_unmatched = vec![0i64; su];
527    let mut path: Vec<i64> = Vec::new();
528    let mut nb_unmatched: i64 = 0;
529
530    for u in 0..su {
531        if matched_with_u[u] >= 0 {
532            matched_with_v[matched_with_u[u] as usize] = u as i64;
533        } else {
534            pos_in_unmatched[u] = nb_unmatched;
535            unmatched[nb_unmatched as usize] = u as i64;
536            nb_unmatched += 1;
537        }
538    }
539
540    // greedy: match unmatched U with any free V neighbour
541    let mut j: i64 = 0;
542    while j < nb_unmatched {
543        let u = unmatched[j as usize];
544        let start = first_adj[u as usize];
545        let end = start + degree[u as usize];
546        let mut i = start;
547        while i < end && matched_with_v[adj[i as usize] as usize] >= 0 {
548            i += 1;
549        }
550        if i == end {
551            j += 1;
552        } else {
553            let v = adj[i as usize];
554            matched_with_u[u as usize] = v;
555            matched_with_v[v as usize] = u;
556            nb_unmatched -= 1;
557            unmatched[j as usize] = unmatched[nb_unmatched as usize];
558            pos_in_unmatched[unmatched[j as usize] as usize] = j;
559        }
560    }
561
562    while nb_unmatched > 0 {
563        // step 1: build the layered DAG
564        marked_u.fill(WHITE);
565        nb_succ.fill(0);
566        marked_v.fill(WHITE);
567        nb_pred.fill(0);
568        let mut nb_v: i64 = 0;
569        for jj in 0..nb_unmatched {
570            let u = unmatched[jj as usize];
571            marked_u[u as usize] = BLACK;
572            let start = first_adj[u as usize];
573            let end = start + degree[u as usize];
574            for i in start..end {
575                let v = adj[i as usize];
576                pred[(v * size_of_u + nb_pred[v as usize]) as usize] = u;
577                nb_pred[v as usize] += 1;
578                succ[(u * size_of_v + nb_succ[u as usize]) as usize] = v;
579                nb_succ[u as usize] += 1;
580                if marked_v[v as usize] == WHITE {
581                    marked_v[v as usize] = GREY;
582                    list_v[nb_v as usize] = v;
583                    nb_v += 1;
584                }
585            }
586        }
587        let mut stop = false;
588        while !stop && nb_v > 0 {
589            let mut nb_u: i64 = 0;
590            for i in 0..nb_v {
591                let v = list_v[i as usize];
592                marked_v[v as usize] = BLACK;
593                let u = matched_with_v[v as usize];
594                if marked_u[u as usize] == WHITE {
595                    marked_u[u as usize] = GREY;
596                    list_u[nb_u as usize] = u;
597                    nb_u += 1;
598                }
599            }
600            nb_v = 0;
601            for jj in 0..nb_u {
602                let u = list_u[jj as usize];
603                marked_u[u as usize] = BLACK;
604                let start = first_adj[u as usize];
605                let end = start + degree[u as usize];
606                for i in start..end {
607                    let v = adj[i as usize];
608                    if marked_v[v as usize] != BLACK {
609                        pred[(v * size_of_u + nb_pred[v as usize]) as usize] = u;
610                        nb_pred[v as usize] += 1;
611                        succ[(u * size_of_v + nb_succ[u as usize]) as usize] = v;
612                        nb_succ[u as usize] += 1;
613                        if marked_v[v as usize] == WHITE {
614                            marked_v[v as usize] = GREY;
615                            list_v[nb_v as usize] = v;
616                            nb_v += 1;
617                        }
618                        if matched_with_v[v as usize] == -1 {
619                            stop = true;
620                        }
621                    }
622                }
623            }
624        }
625        if nb_v == 0 {
626            return true;
627        }
628
629        // step 2: extract vertex-disjoint augmenting paths
630        for k in 0..nb_v {
631            let mut v = list_v[k as usize];
632            if matched_with_v[v as usize] == -1 && nb_pred[v as usize] > 0 {
633                path.clear();
634                path.push(v);
635                let mut nb_dv: i64 = 0;
636                let mut nb_du: i64 = 0;
637                add_to_delete(v, &mut list_dv, &mut nb_dv, &mut marked_v);
638                let mut u;
639                loop {
640                    u = pred[(v * size_of_u) as usize];
641                    path.push(u);
642                    add_to_delete(u, &mut list_du, &mut nb_du, &mut marked_u);
643                    if matched_with_u[u as usize] != -1 {
644                        v = matched_with_u[u as usize];
645                        path.push(v);
646                        add_to_delete(v, &mut list_dv, &mut nb_dv, &mut marked_v);
647                    }
648                    if matched_with_u[u as usize] == -1 {
649                        break;
650                    }
651                }
652
653                while nb_dv > 0 || nb_du > 0 {
654                    while nb_dv > 0 {
655                        nb_dv -= 1;
656                        let vv = list_dv[nb_dv as usize];
657                        marked_v[vv as usize] = DELETED;
658                        let uu = matched_with_v[vv as usize];
659                        if uu != -1 {
660                            add_to_delete(uu, &mut list_du, &mut nb_du, &mut marked_u);
661                        }
662                        for i in 0..nb_pred[vv as usize] {
663                            let up = pred[(vv * size_of_u + i) as usize];
664                            let mut jj: i64 = 0;
665                            while jj < nb_succ[up as usize]
666                                && vv != succ[(up * size_of_v + jj) as usize]
667                            {
668                                jj += 1;
669                            }
670                            nb_succ[up as usize] -= 1;
671                            succ[(up * size_of_v + jj) as usize] =
672                                succ[(up * size_of_v + nb_succ[up as usize]) as usize];
673                            if nb_succ[up as usize] == 0 {
674                                add_to_delete(up, &mut list_du, &mut nb_du, &mut marked_u);
675                            }
676                        }
677                    }
678                    while nb_du > 0 {
679                        nb_du -= 1;
680                        let uu = list_du[nb_du as usize];
681                        marked_u[uu as usize] = DELETED;
682                        let vv = matched_with_u[uu as usize];
683                        if vv != -1 {
684                            add_to_delete(vv, &mut list_dv, &mut nb_dv, &mut marked_v);
685                        }
686                        for i in 0..nb_succ[uu as usize] {
687                            let vp = succ[(uu * size_of_v + i) as usize];
688                            let mut jj: i64 = 0;
689                            while jj < nb_pred[vp as usize]
690                                && uu != pred[(vp * size_of_u + jj) as usize]
691                            {
692                                jj += 1;
693                            }
694                            nb_pred[vp as usize] -= 1;
695                            pred[(vp * size_of_u + jj) as usize] =
696                                pred[(vp * size_of_u + nb_pred[vp as usize]) as usize];
697                            if nb_pred[vp as usize] == 0 {
698                                add_to_delete(vp, &mut list_dv, &mut nb_dv, &mut marked_v);
699                            }
700                        }
701                    }
702                }
703                let last = path[path.len() - 1];
704                let i = pos_in_unmatched[last as usize];
705                nb_unmatched -= 1;
706                unmatched[i as usize] = unmatched[nb_unmatched as usize];
707                pos_in_unmatched[unmatched[i as usize] as usize] = i;
708                while path.len() > 1 {
709                    let pu = path.pop().unwrap_or(-1);
710                    let pv = path.pop().unwrap_or(-1);
711                    matched_with_u[pu as usize] = pv;
712                    matched_with_v[pv as usize] = pu;
713                }
714            }
715        }
716    }
717    false
718}
719
720/// Depth-first numbering used by [`scc`] (`igraph_i_lad_DFS`).
721#[allow(clippy::too_many_arguments)]
722fn dfs(
723    nb_u: i64,
724    u: i64,
725    marked: &mut [bool],
726    nb_succ: &[i64],
727    succ: &[i64],
728    matched_with_u: &[i64],
729    order: &mut [i64],
730    nb: &mut i64,
731) {
732    let v = matched_with_u[u as usize];
733    marked[u as usize] = true;
734    if v >= 0 {
735        for i in 0..nb_succ[v as usize] {
736            let w = succ[(v * nb_u + i) as usize];
737            if !marked[w as usize] {
738                dfs(nb_u, w, marked, nb_succ, succ, matched_with_u, order, nb);
739            }
740        }
741    }
742    order[*nb as usize] = u;
743    *nb -= 1;
744}
745
746/// Tarjan/Kosaraju-style strongly-connected-component labelling of the
747/// residual bipartite graph (`igraph_i_lad_SCC`). Fills `num_v`/`num_u` with
748/// matching component ids.
749#[allow(clippy::too_many_arguments)]
750fn scc(
751    nb_u: i64,
752    nb_v: i64,
753    num_v: &mut [i64],
754    num_u: &mut [i64],
755    nb_succ: &[i64],
756    succ: &[i64],
757    nb_pred: &[i64],
758    pred: &[i64],
759    matched_with_u: &[i64],
760    matched_with_v: &[i64],
761) {
762    let mut order = vec![0i64; nb_u as usize];
763    let mut marked = vec![false; nb_u as usize];
764    let mut fifo = vec![0i64; nb_v as usize];
765    let mut nb = nb_u - 1;
766    for u in 0..nb_u {
767        if !marked[u as usize] {
768            dfs(
769                nb_u,
770                u,
771                &mut marked,
772                nb_succ,
773                succ,
774                matched_with_u,
775                &mut order,
776                &mut nb,
777            );
778        }
779    }
780    let mut nb_scc: i64 = 0;
781    for x in num_u.iter_mut() {
782        *x = -1;
783    }
784    for x in num_v.iter_mut() {
785        *x = -1;
786    }
787    for i in 0..nb_u {
788        let u0 = order[i as usize];
789        let v0 = matched_with_u[u0 as usize];
790        if v0 == -1 {
791            continue;
792        }
793        if num_v[v0 as usize] == -1 {
794            nb_scc += 1;
795            let mut k: i64 = 1;
796            fifo[0] = v0;
797            num_v[v0 as usize] = nb_scc;
798            while k > 0 {
799                k -= 1;
800                let v = fifo[k as usize];
801                let u = matched_with_v[v as usize];
802                if u != -1 {
803                    num_u[u as usize] = nb_scc;
804                    for j in 0..nb_pred[u as usize] {
805                        let vp = pred[(u * nb_v + j) as usize];
806                        if num_v[vp as usize] == -1 {
807                            num_v[vp as usize] = nb_scc;
808                            fifo[k as usize] = vp;
809                            k += 1;
810                        }
811                    }
812                }
813            }
814        }
815    }
816}
817
818/// Enforce generalized arc consistency for the global AllDifferent constraint
819/// (`igraph_i_lad_ensureGACallDiff`). Returns `true` if an inconsistency is
820/// found (the C `invalid` out-param).
821#[allow(clippy::too_many_lines)]
822fn ensure_gac_all_diff(induced: bool, gp: &Tgraph, gt: &Tgraph, d: &mut Tdomain) -> bool {
823    let nb_p = gp.nb_vertices;
824    let nb_t = gt.nb_vertices;
825    let mut nb_pred = vec![0i64; nb_p as usize];
826    let mut pred = vec![0i64; (nb_p * nb_t) as usize];
827    let mut nb_succ = vec![0i64; nb_t as usize];
828    let mut succ = vec![0i64; (nb_t * nb_p) as usize];
829    let mut num_v = vec![0i64; nb_t as usize];
830    let mut num_u = vec![0i64; nb_p as usize];
831    let mut used = vec![false; (nb_p * nb_t) as usize];
832    let mut list = vec![0i64; nb_t as usize];
833    let mut nb: i64 = 0;
834
835    for u in 0..nb_p {
836        let fv = d.first_val[u as usize];
837        for i in 0..d.nb_val[u as usize] {
838            let v = d.val[(fv + i) as usize];
839            used[(u * nb_t + v) as usize] = false;
840            if v != d.global_matching_p[u as usize] {
841                pred[(u * nb_t + nb_pred[u as usize]) as usize] = v;
842                nb_pred[u as usize] += 1;
843                succ[(v * nb_p + nb_succ[v as usize]) as usize] = u;
844                nb_succ[v as usize] += 1;
845            }
846        }
847    }
848
849    // mark edges of alternating paths from free target vertices
850    for v in 0..nb_t {
851        if d.global_matching_t[v as usize] < 0 {
852            list[nb as usize] = v;
853            nb += 1;
854            num_v[v as usize] = 1;
855        }
856    }
857    while nb > 0 {
858        nb -= 1;
859        let v = list[nb as usize];
860        for i in 0..nb_succ[v as usize] {
861            let u = succ[(v * nb_p + i) as usize];
862            used[(u * nb_t + v) as usize] = true;
863            if num_u[u as usize] == 0 {
864                num_u[u as usize] = 1;
865                let w = d.global_matching_p[u as usize];
866                used[(u * nb_t + w) as usize] = true;
867                if num_v[w as usize] == 0 {
868                    list[nb as usize] = w;
869                    nb += 1;
870                    num_v[w as usize] = 1;
871                }
872            }
873        }
874    }
875
876    scc(
877        nb_p,
878        nb_t,
879        &mut num_v,
880        &mut num_u,
881        &nb_succ,
882        &succ,
883        &nb_pred,
884        &pred,
885        &d.global_matching_p,
886        &d.global_matching_t,
887    );
888
889    let mut to_match: Vec<i64> = Vec::new();
890    for u in 0..nb_p {
891        let old_nb_val = d.nb_val[u as usize];
892        // NOTE: upstream uses a `for (i=0; i<nbVal[u]; i++)` loop here and
893        // advances `i` unconditionally even after a removal (unlike
894        // `match_vertices`); the GAC removal set is fixed by the SCC labelling
895        // computed above, so a swapped-in tail value is intentionally not
896        // re-examined in this pass.
897        let mut i: i64 = 0;
898        while i < d.nb_val[u as usize] {
899            let v = d.val[(d.first_val[u as usize] + i) as usize];
900            if !used[(u * nb_t + v) as usize]
901                && num_v[v as usize] != num_u[u as usize]
902                && d.global_matching_p[u as usize] != v
903                && !remove_value(u, v, d, gp, gt)
904            {
905                return true;
906            }
907            i += 1;
908        }
909        if d.nb_val[u as usize] == 0 {
910            return true;
911        }
912        if old_nb_val > 1 && d.nb_val[u as usize] == 1 {
913            to_match.push(u);
914        }
915    }
916    match_vertices(to_match, induced, d, gp, gt)
917}
918
919/// Check whether `G_(u,v)` has an `adj(u)`-covering matching, updating the
920/// cached matching for `(u,v)` (`igraph_i_lad_checkLAD`).
921#[allow(clippy::too_many_lines)]
922fn check_lad(u: i64, v: i64, d: &mut Tdomain, gp: &Tgraph, gt: &Tgraph) -> bool {
923    let nb_succ_u = gp.nb_succ[u as usize];
924    let fm = d.first_match(u, v);
925
926    // special case: u has a single neighbour => no Hopcroft-Karp needed
927    if nb_succ_u == 1 {
928        let u2 = gp.succ[u as usize][0];
929        let v2 = d.matching[fm as usize];
930        if v2 != -1 && d.is_in_d(u2, v2) {
931            return true;
932        }
933        let start = d.first_val[u2 as usize];
934        let end = start + d.nb_val[u2 as usize];
935        for i in start..end {
936            if gt.is_edge(v, d.val[i as usize]) {
937                d.matching[fm as usize] = d.val[i as usize];
938                return true;
939            }
940        }
941        return false;
942    }
943
944    // general case
945    let mut nb_matched = 0i64;
946    for i in 0..nb_succ_u {
947        let u2 = gp.succ[u as usize][i as usize];
948        let v2 = d.matching[(fm + i) as usize];
949        if v2 != -1 && d.is_in_d(u2, v2) {
950            nb_matched += 1;
951        }
952    }
953    if nb_matched == nb_succ_u {
954        return true;
955    }
956
957    let nb_t = gt.nb_vertices;
958    let mut num = vec![-1i64; nb_t as usize];
959    let mut num_inv = vec![0i64; nb_t as usize];
960    let mut nb_comp = vec![0i64; nb_succ_u as usize];
961    let mut first_comp = vec![0i64; nb_succ_u as usize];
962    let mut comp = vec![0i64; (nb_succ_u * nb_t) as usize];
963    let mut matched_with_u = vec![0i64; nb_succ_u as usize];
964    let mut nb_num = 0i64;
965    let mut pos_in_comp = 0i64;
966
967    for i in 0..nb_succ_u {
968        let u2 = gp.succ[u as usize][i as usize];
969        nb_comp[i as usize] = 0;
970        first_comp[i as usize] = pos_in_comp;
971        if d.nb_val[u2 as usize] > gt.nb_succ[v as usize] {
972            let start = d.first_val[u2 as usize];
973            let end = start + d.nb_val[u2 as usize];
974            for j in start..end {
975                let v2 = d.val[j as usize];
976                if gt.is_edge(v, v2) {
977                    if num[v2 as usize] < 0 {
978                        num[v2 as usize] = nb_num;
979                        num_inv[nb_num as usize] = v2;
980                        nb_num += 1;
981                    }
982                    comp[pos_in_comp as usize] = num[v2 as usize];
983                    pos_in_comp += 1;
984                    nb_comp[i as usize] += 1;
985                }
986            }
987        } else {
988            for j in 0..gt.nb_succ[v as usize] {
989                let v2 = gt.succ[v as usize][j as usize];
990                if d.is_in_d(u2, v2) {
991                    if num[v2 as usize] < 0 {
992                        num[v2 as usize] = nb_num;
993                        num_inv[nb_num as usize] = v2;
994                        nb_num += 1;
995                    }
996                    comp[pos_in_comp as usize] = num[v2 as usize];
997                    pos_in_comp += 1;
998                    nb_comp[i as usize] += 1;
999                }
1000            }
1001        }
1002        if nb_comp[i as usize] == 0 {
1003            return false;
1004        }
1005        let v2 = d.matching[(fm + i) as usize];
1006        matched_with_u[i as usize] = if v2 != -1 && d.is_in_d(u2, v2) {
1007            num[v2 as usize]
1008        } else {
1009            -1
1010        };
1011    }
1012
1013    let invalid = update_matching(
1014        nb_succ_u,
1015        nb_num,
1016        &nb_comp,
1017        &first_comp,
1018        &comp,
1019        &mut matched_with_u,
1020    );
1021    if invalid {
1022        return false;
1023    }
1024    for i in 0..nb_succ_u {
1025        d.matching[(fm + i) as usize] = num_inv[matched_with_u[i as usize] as usize];
1026    }
1027    true
1028}
1029
1030/// Filter all queued domains wrt LAD and AllDifferent to a fixpoint
1031/// (`igraph_i_lad_filter`). Returns `false` if some domain becomes empty.
1032fn filter(induced: bool, d: &mut Tdomain, gp: &Tgraph, gt: &Tgraph) -> bool {
1033    while !d.to_filter_empty() {
1034        while !d.to_filter_empty() {
1035            let u = d.next_to_filter(gp.nb_vertices);
1036            let old_nb_val = d.nb_val[u as usize];
1037            let mut i = d.first_val[u as usize];
1038            while i < d.first_val[u as usize] + d.nb_val[u as usize] {
1039                let v = d.val[i as usize];
1040                if check_lad(u, v, d, gp, gt) {
1041                    i += 1;
1042                } else if !remove_value(u, v, d, gp, gt) {
1043                    return false;
1044                }
1045            }
1046            if d.nb_val[u as usize] == 1 && old_nb_val > 1 && !match_vertex(u, induced, d, gp, gt) {
1047                return false;
1048            }
1049            if d.nb_val[u as usize] == 0 {
1050                return false;
1051            }
1052        }
1053        if ensure_gac_all_diff(induced, gp, gt, d) {
1054            return false;
1055        }
1056    }
1057    true
1058}
1059
1060/// Accumulators threaded through the recursive [`solve`] search.
1061struct Solver {
1062    first_sol: bool,
1063    iso: bool,
1064    map: Vec<i64>,
1065    maps: Vec<Vec<i64>>,
1066    want_map: bool,
1067    want_maps: bool,
1068    nb_sol: i64,
1069}
1070
1071/// Branch-and-propagate search (`igraph_i_lad_solve`): instantiate the
1072/// smallest non-singleton domain and recurse, collecting solutions per the
1073/// [`Solver`] configuration.
1074fn solve(induced: bool, d: &mut Tdomain, gp: &Tgraph, gt: &Tgraph, s: &mut Solver) {
1075    let nb_p = gp.nb_vertices;
1076    if !filter(induced, d, gp, gt) {
1077        d.reset_to_filter();
1078        return;
1079    }
1080
1081    let mut nb_val_snapshot = vec![0i64; nb_p as usize];
1082    let mut global_snapshot = vec![0i64; nb_p as usize];
1083    let mut min_dom = -1i64;
1084    for u in 0..nb_p {
1085        nb_val_snapshot[u as usize] = d.nb_val[u as usize];
1086        if d.nb_val[u as usize] > 1
1087            && (min_dom < 0 || d.nb_val[u as usize] < d.nb_val[min_dom as usize])
1088        {
1089            min_dom = u;
1090        }
1091        global_snapshot[u as usize] = d.global_matching_p[u as usize];
1092    }
1093
1094    if min_dom == -1 {
1095        // every vertex is fixed => solution found
1096        s.iso = true;
1097        s.nb_sol += 1;
1098        if s.want_map && s.map.is_empty() {
1099            s.map = (0..nb_p)
1100                .map(|u| d.val[d.first_val[u as usize] as usize])
1101                .collect();
1102        }
1103        if s.want_maps {
1104            let m: Vec<i64> = (0..nb_p)
1105                .map(|u| d.val[d.first_val[u as usize] as usize])
1106                .collect();
1107            s.maps.push(m);
1108        }
1109        d.reset_to_filter();
1110        return;
1111    }
1112
1113    let nb_min = nb_val_snapshot[min_dom as usize];
1114    let mut branch_vals = vec![0i64; nb_min as usize];
1115    for i in 0..nb_min {
1116        branch_vals[i as usize] = d.val[(d.first_val[min_dom as usize] + i) as usize];
1117    }
1118
1119    let mut i = 0i64;
1120    while i < nb_min && (!s.first_sol || s.nb_sol == 0) {
1121        let v = branch_vals[i as usize];
1122        let ok = remove_all_values_but_one(min_dom, v, d, gp, gt)
1123            && match_vertex(min_dom, induced, d, gp, gt);
1124        if ok {
1125            solve(induced, d, gp, gt, s);
1126        } else {
1127            d.reset_to_filter();
1128        }
1129        // restore domain sizes and the global AllDifferent matching
1130        for x in &mut d.global_matching_t {
1131            *x = -1;
1132        }
1133        for u in 0..nb_p {
1134            d.nb_val[u as usize] = nb_val_snapshot[u as usize];
1135            d.global_matching_p[u as usize] = global_snapshot[u as usize];
1136            d.global_matching_t[global_snapshot[u as usize] as usize] = u;
1137        }
1138        i += 1;
1139    }
1140}
1141
1142/// Shared engine behind the two public wrappers. `first_sol` stops at the first
1143/// embedding; otherwise every embedding is enumerated. Returns
1144/// `(iso, map, maps)` with pattern→target vertex ids.
1145fn lad_engine(
1146    pattern: &Graph,
1147    target: &Graph,
1148    domains: Option<&[Vec<u32>]>,
1149    induced: bool,
1150    first_sol: bool,
1151) -> IgraphResult<(bool, Vec<u32>, Vec<Vec<u32>>)> {
1152    if pattern.is_directed() != target.is_directed() {
1153        return Err(IgraphError::InvalidArgument(
1154            "Cannot search for a directed pattern in an undirected target or vice versa.".into(),
1155        ));
1156    }
1157
1158    let want_maps = !first_sol;
1159    let n_pat = pattern.vcount();
1160
1161    // null pattern: trivially isomorphic to the empty subgraph
1162    if n_pat == 0 {
1163        let maps = if want_maps {
1164            vec![Vec::new()]
1165        } else {
1166            Vec::new()
1167        };
1168        return Ok((true, Vec::new(), maps));
1169    }
1170
1171    if let Some(doms) = domains {
1172        if doms.len() != n_pat as usize {
1173            return Err(IgraphError::InvalidArgument(format!(
1174                "Domain list length ({}) must match the number of pattern vertices ({}).",
1175                doms.len(),
1176                n_pat
1177            )));
1178        }
1179        let n_t = target.vcount();
1180        for d in doms {
1181            for &v in d {
1182                if v >= n_t {
1183                    return Err(IgraphError::InvalidArgument(format!(
1184                        "Domain contains target vertex id {v} out of range (target has {n_t} vertices)."
1185                    )));
1186                }
1187            }
1188        }
1189    }
1190
1191    let gp = create_graph(pattern)?;
1192    let gt = create_graph(target)?;
1193
1194    if gp.nb_vertices > gt.nb_vertices {
1195        return Ok((false, Vec::new(), Vec::new()));
1196    }
1197
1198    let (mut d, invalid_domain) = init_domains(domains, &gp, &gt);
1199    if invalid_domain {
1200        return Ok((false, Vec::new(), Vec::new()));
1201    }
1202
1203    // global AllDifferent matching over the full domains
1204    let nb_p = gp.nb_vertices;
1205    let mut gmp = std::mem::take(&mut d.global_matching_p);
1206    let invalid = update_matching(
1207        gp.nb_vertices,
1208        gt.nb_vertices,
1209        &d.nb_val,
1210        &d.first_val,
1211        &d.val,
1212        &mut gmp,
1213    );
1214    d.global_matching_p = gmp;
1215    if invalid {
1216        return Ok((false, Vec::new(), Vec::new()));
1217    }
1218
1219    if ensure_gac_all_diff(induced, &gp, &gt, &mut d) {
1220        return Ok((false, Vec::new(), Vec::new()));
1221    }
1222
1223    for u in 0..nb_p {
1224        let v = d.global_matching_p[u as usize];
1225        d.global_matching_t[v as usize] = u;
1226    }
1227
1228    let mut to_match: Vec<i64> = Vec::new();
1229    for u in 0..nb_p {
1230        if d.nb_val[u as usize] == 1 {
1231            to_match.push(u);
1232        }
1233    }
1234    if match_vertices(to_match, induced, &mut d, &gp, &gt) {
1235        return Ok((false, Vec::new(), Vec::new()));
1236    }
1237
1238    let mut solver = Solver {
1239        first_sol,
1240        iso: false,
1241        map: Vec::new(),
1242        maps: Vec::new(),
1243        want_map: !want_maps,
1244        want_maps,
1245        nb_sol: 0,
1246    };
1247    solve(induced, &mut d, &gp, &gt, &mut solver);
1248
1249    let map_u32: Vec<u32> = solver.map.iter().map(|&x| x as u32).collect();
1250    let maps_u32: Vec<Vec<u32>> = solver
1251        .maps
1252        .iter()
1253        .map(|m| m.iter().map(|&x| x as u32).collect())
1254        .collect();
1255    Ok((solver.iso, map_u32, maps_u32))
1256}
1257
1258/// Decide whether `pattern` is isomorphic to a subgraph of `target` using the
1259/// LAD algorithm, returning the first embedding found.
1260///
1261/// `pattern` and `target` must agree on directedness. Multi-edges are
1262/// rejected; self-loops are allowed. The optional `domains` restricts each
1263/// pattern vertex (in id order) to an explicit list of compatible target
1264/// vertices — useful for colour-constrained matching. When `induced` is
1265/// `true`, non-edges of the pattern must map to non-edges of the target
1266/// (induced subgraph); when `false`, only pattern edges must be preserved
1267/// (monomorphism).
1268///
1269/// Port of `igraph_subisomorphic_lad` with `maps == NULL` (first-solution
1270/// mode).
1271///
1272/// # Examples
1273///
1274/// ```
1275/// use rust_igraph::{Graph, subisomorphic_lad};
1276///
1277/// // A triangle pattern embeds into K4.
1278/// let mut target = Graph::new(4, false)?;
1279/// for (u, v) in [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] {
1280///     target.add_edge(u, v)?;
1281/// }
1282/// let mut pattern = Graph::new(3, false)?;
1283/// for (u, v) in [(0, 1), (1, 2), (2, 0)] {
1284///     pattern.add_edge(u, v)?;
1285/// }
1286/// let res = subisomorphic_lad(&pattern, &target, None, false)?;
1287/// assert!(res.iso);
1288/// assert_eq!(res.map.len(), 3);
1289/// # Ok::<(), rust_igraph::IgraphError>(())
1290/// ```
1291pub fn subisomorphic_lad(
1292    pattern: &Graph,
1293    target: &Graph,
1294    domains: Option<&[Vec<u32>]>,
1295    induced: bool,
1296) -> IgraphResult<LadSubisomorphism> {
1297    let (iso, map, _) = lad_engine(pattern, target, domains, induced, true)?;
1298    Ok(LadSubisomorphism { iso, map })
1299}
1300
1301/// Enumerate *all* subgraph isomorphisms from `pattern` into `target` with the
1302/// LAD algorithm. Each returned vector maps pattern vertex `i` (by index) to
1303/// its target vertex.
1304///
1305/// Arguments mirror [`subisomorphic_lad`]. A null pattern yields a single
1306/// empty mapping. Port of `igraph_subisomorphic_lad` with a non-null `maps`.
1307///
1308/// # Examples
1309///
1310/// ```
1311/// use rust_igraph::{Graph, get_subisomorphisms_lad};
1312///
1313/// // A single edge embeds into a triangle in six ordered ways.
1314/// let mut target = Graph::new(3, false)?;
1315/// for (u, v) in [(0, 1), (1, 2), (2, 0)] {
1316///     target.add_edge(u, v)?;
1317/// }
1318/// let mut pattern = Graph::new(2, false)?;
1319/// pattern.add_edge(0, 1)?;
1320/// let maps = get_subisomorphisms_lad(&pattern, &target, None, false)?;
1321/// assert_eq!(maps.len(), 6);
1322/// # Ok::<(), rust_igraph::IgraphError>(())
1323/// ```
1324pub fn get_subisomorphisms_lad(
1325    pattern: &Graph,
1326    target: &Graph,
1327    domains: Option<&[Vec<u32>]>,
1328    induced: bool,
1329) -> IgraphResult<Vec<Vec<u32>>> {
1330    let (_, _, maps) = lad_engine(pattern, target, domains, induced, false)?;
1331    Ok(maps)
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336    use super::*;
1337
1338    fn undirected(n: u32, edges: &[(u32, u32)]) -> Graph {
1339        let mut g = Graph::new(n, false).expect("graph init");
1340        for &(a, b) in edges {
1341            g.add_edge(a, b).expect("edge in range");
1342        }
1343        g
1344    }
1345
1346    /// The graphs from `igraph_subisomorphic_lad.out`.
1347    fn example_graphs() -> (Graph, Graph) {
1348        let target = undirected(
1349            9,
1350            &[
1351                (0, 1),
1352                (0, 4),
1353                (0, 6),
1354                (1, 4),
1355                (1, 2),
1356                (2, 3),
1357                (3, 4),
1358                (3, 5),
1359                (3, 7),
1360                (3, 8),
1361                (4, 5),
1362                (4, 6),
1363                (5, 6),
1364                (5, 8),
1365                (7, 8),
1366            ],
1367        );
1368        let pattern = undirected(5, &[(0, 1), (0, 4), (1, 4), (1, 2), (2, 3), (3, 4)]);
1369        (pattern, target)
1370    }
1371
1372    fn sorted(mut maps: Vec<Vec<u32>>) -> Vec<Vec<u32>> {
1373        maps.sort();
1374        maps
1375    }
1376
1377    /// Verify `map` is a valid (monomorphic) embedding of `pattern` in `target`.
1378    fn is_valid_embedding(map: &[u32], pattern: &Graph, target: &Graph) -> bool {
1379        if map.len() != pattern.vcount() as usize {
1380            return false;
1381        }
1382        // injective
1383        let mut seen = std::collections::HashSet::new();
1384        if !map.iter().all(|&v| seen.insert(v)) {
1385            return false;
1386        }
1387        for eid in 0..pattern.ecount() {
1388            let (a, b) = pattern.edge(eid as EdgeId).expect("pattern edge");
1389            if target
1390                .find_eid(map[a as usize], map[b as usize])
1391                .expect("valid target vertices")
1392                .is_none()
1393            {
1394                return false;
1395            }
1396        }
1397        true
1398    }
1399
1400    #[test]
1401    fn example_oracle_monomorphism() {
1402        let (pattern, target) = example_graphs();
1403        let maps = get_subisomorphisms_lad(&pattern, &target, None, false).expect("lad");
1404        assert_eq!(maps.len(), 20, "monomorphism count from the C example");
1405        for m in &maps {
1406            assert!(is_valid_embedding(m, &pattern, &target));
1407        }
1408    }
1409
1410    #[test]
1411    fn example_oracle_induced() {
1412        let (pattern, target) = example_graphs();
1413        let maps = get_subisomorphisms_lad(&pattern, &target, None, true).expect("lad");
1414        let expected = sorted(vec![
1415            vec![0, 1, 2, 3, 4],
1416            vec![5, 3, 2, 1, 4],
1417            vec![5, 4, 1, 2, 3],
1418            vec![0, 4, 3, 2, 1],
1419        ]);
1420        assert_eq!(
1421            sorted(maps),
1422            expected,
1423            "induced embeddings from the C example"
1424        );
1425    }
1426
1427    #[test]
1428    fn example_oracle_domains() {
1429        let (pattern, target) = example_graphs();
1430        let domains = vec![
1431            vec![0u32, 2, 8],
1432            vec![4, 5, 6, 7],
1433            vec![1, 3, 5, 6, 7, 8],
1434            vec![0, 2, 8],
1435            vec![1, 3, 7, 8],
1436        ];
1437        let maps = get_subisomorphisms_lad(&pattern, &target, Some(&domains), false).expect("lad");
1438        assert_eq!(
1439            maps,
1440            vec![vec![0u32, 4, 3, 2, 1]],
1441            "domain-restricted embedding"
1442        );
1443    }
1444
1445    #[test]
1446    fn first_solution_mode() {
1447        let (pattern, target) = example_graphs();
1448        let res = subisomorphic_lad(&pattern, &target, None, false).expect("lad");
1449        assert!(res.iso);
1450        assert!(is_valid_embedding(&res.map, &pattern, &target));
1451    }
1452
1453    #[test]
1454    fn null_pattern_is_trivially_iso() {
1455        let target = undirected(3, &[(0, 1), (1, 2)]);
1456        let empty = Graph::new(0, false).expect("graph init");
1457        let res = subisomorphic_lad(&empty, &target, None, false).expect("lad");
1458        assert!(res.iso);
1459        assert!(res.map.is_empty());
1460        let maps = get_subisomorphisms_lad(&empty, &target, None, false).expect("lad");
1461        assert_eq!(maps, vec![Vec::<u32>::new()]);
1462    }
1463
1464    #[test]
1465    fn pattern_larger_than_target() {
1466        let pattern = undirected(4, &[(0, 1), (1, 2), (2, 3)]);
1467        let target = undirected(3, &[(0, 1), (1, 2)]);
1468        let res = subisomorphic_lad(&pattern, &target, None, false).expect("lad");
1469        assert!(!res.iso);
1470        assert!(res.map.is_empty());
1471    }
1472
1473    #[test]
1474    fn triangle_not_in_path() {
1475        let triangle = undirected(3, &[(0, 1), (1, 2), (2, 0)]);
1476        let path = undirected(3, &[(0, 1), (1, 2)]);
1477        let res = subisomorphic_lad(&triangle, &path, None, false).expect("lad");
1478        assert!(!res.iso);
1479    }
1480
1481    #[test]
1482    fn edge_into_triangle_has_six_maps() {
1483        let target = undirected(3, &[(0, 1), (1, 2), (2, 0)]);
1484        let pattern = undirected(2, &[(0, 1)]);
1485        let maps = get_subisomorphisms_lad(&pattern, &target, None, false).expect("lad");
1486        assert_eq!(maps.len(), 6);
1487    }
1488
1489    #[test]
1490    fn multi_edge_rejected() {
1491        let mut pattern = Graph::new(2, false).expect("graph init");
1492        pattern.add_edge(0, 1).expect("edge");
1493        pattern.add_edge(0, 1).expect("edge");
1494        let target = undirected(3, &[(0, 1), (1, 2), (2, 0)]);
1495        assert!(subisomorphic_lad(&pattern, &target, None, false).is_err());
1496    }
1497
1498    #[test]
1499    fn directedness_mismatch_rejected() {
1500        let pattern = Graph::new(2, true).expect("graph init");
1501        let target = undirected(3, &[(0, 1), (1, 2)]);
1502        assert!(subisomorphic_lad(&pattern, &target, None, false).is_err());
1503    }
1504}