Skip to main content

csp_solver/solver/gac/
mod.rs

1//! Unified GAC all-different core (Régin 1994), sentinel-generic and incremental.
2//!
3//! One propagator body serves both the plain all-different constraint and the
4//! sentinel-aware "all-different-except" variant. The distinction is a single
5//! [`Option<&D::Value>`] parameter:
6//!
7//! * `sentinel = None` — plain all-different. Every unassigned variable must be
8//!   matched to a distinct value.
9//! * `sentinel = Some(s)` — the value `s` is an "escape valve" any number of
10//!   variables may take; it is removed from the value side of the bipartite
11//!   graph, and variables whose domain has narrowed to `{s}` drop out of the
12//!   variable side.
13//!
14//! The Régin pipeline (Hopcroft-Karp maximum matching → residual graph → Tarjan
15//! SCC → prune edges outside every maximum matching) is identical for both; only
16//! participant/value collection and free-vertex reachability seeding branch on
17//! the sentinel. The graph primitives live in [`matching`].
18//!
19//! # Incrementality
20//!
21//! Two costs the from-scratch predecessor paid on *every* `revise()` are gone:
22//!
23//! 1. **Per-call heap allocation.** All working buffers (adjacency, matching,
24//!    residual graph, Tarjan stacks) live in a thread-local [`GacScratch`] that
25//!    is cleared — not freed — between calls. The value universe is the sole
26//!    per-call `Vec<V>` the generic bound cannot pool.
27//! 2. **Cold Hopcroft-Karp.** With a stable constraint id, the previous maximum
28//!    matching is cached per constraint and warm-starts the next call: cached
29//!    edges still valid against the live domains seed `match_u`/`match_v`, and
30//!    Hopcroft-Karp only augments from the vertices left free — O(E) repair
31//!    rather than O(E·√V) reconstruction. The cache is a pure hint: every seeded
32//!    edge is validated against live domains, so a stale cache costs extra
33//!    augmentation, never correctness.
34
35mod matching;
36
37use std::any::Any;
38use std::cell::RefCell;
39use std::collections::HashMap;
40use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
41
42use crate::constraint::traits::{Revision, VarId};
43use crate::domain::Domain;
44use crate::variable::Variable;
45
46use matching::{NONE, hopcroft_karp, reset_adj, tarjan_scc};
47
48/// Instrumentation: total entries into the unified GAC core (all variants).
49pub static GAC_CORE_CALLS: AtomicU64 = AtomicU64::new(0);
50
51/// Monotonic source of per-constraint cache ids.
52static NEXT_GAC_ID: AtomicU32 = AtomicU32::new(0);
53
54/// Allocate a fresh, process-unique constraint id for matching-cache keying.
55pub fn next_gac_id() -> u32 {
56    NEXT_GAC_ID.fetch_add(1, Ordering::Relaxed)
57}
58
59/// Below this live-participant count, GAC finds nothing singleton removal
60/// misses, so the caller's cheaper path is preferred. Exposed so constraints
61/// gate consistently.
62pub const GAC_MIN_PARTICIPANTS: usize = 3;
63
64/// A/B measurement toggle for the plain `AllDifferent` GAC path (Sudoku,
65/// Futoshiki). Default on; flipping it off reverts `AllDifferent` to
66/// singleton-removal-only so the pruning-strength delta can be measured.
67pub static GAC_IN_ALLDIFF_ENABLED: AtomicBool = AtomicBool::new(true);
68
69/// Cap on the per-thread matching cache; cleared wholesale past this many
70/// distinct constraints so a long-lived worker thread cannot leak unboundedly.
71const CACHE_CAP: usize = 8192;
72
73// ---------------------------------------------------------------------------
74// Reusable scratch
75// ---------------------------------------------------------------------------
76
77/// Per-thread, per-value-type reusable working set for the GAC core.
78///
79/// Generic in the value type only for the value universe and matching cache;
80/// every other buffer is integer-indexed and value-agnostic.
81struct GacScratch<V> {
82    participants: Vec<usize>,
83    has_sentinel: Vec<bool>,
84    assigned_ns: Vec<V>,
85    all_vals: Vec<V>,
86    adj: Vec<Vec<u32>>,
87    match_u: Vec<u32>,
88    match_v: Vec<u32>,
89    dist: Vec<u32>,
90    queue: Vec<u32>,
91    res_adj: Vec<Vec<u32>>,
92    reachable: Vec<bool>,
93    bfs: Vec<u32>,
94    t_index: Vec<u32>,
95    t_lowlink: Vec<u32>,
96    t_onstack: Vec<bool>,
97    t_scc: Vec<u32>,
98    t_stack: Vec<u32>,
99    t_call: Vec<(u32, u32)>,
100    cache: HashMap<u32, Vec<Option<V>>>,
101}
102
103impl<V> Default for GacScratch<V> {
104    fn default() -> Self {
105        Self {
106            participants: Vec::new(),
107            has_sentinel: Vec::new(),
108            assigned_ns: Vec::new(),
109            all_vals: Vec::new(),
110            adj: Vec::new(),
111            match_u: Vec::new(),
112            match_v: Vec::new(),
113            dist: Vec::new(),
114            queue: Vec::new(),
115            res_adj: Vec::new(),
116            reachable: Vec::new(),
117            bfs: Vec::new(),
118            t_index: Vec::new(),
119            t_lowlink: Vec::new(),
120            t_onstack: Vec::new(),
121            t_scc: Vec::new(),
122            t_stack: Vec::new(),
123            t_call: Vec::new(),
124            cache: HashMap::new(),
125        }
126    }
127}
128
129thread_local! {
130    /// One type-erased [`GacScratch`] slot per thread. The concrete `V` for a
131    /// given solve is fixed, so the slot re-materializes only if a thread runs
132    /// solves over different value types.
133    static SCRATCH: RefCell<Box<dyn Any>> = RefCell::new(Box::new(()));
134}
135
136fn with_scratch<V, R>(f: impl FnOnce(&mut GacScratch<V>) -> R) -> R
137where
138    V: 'static,
139{
140    SCRATCH.with(|slot| {
141        let mut b = slot.borrow_mut();
142        if !b.is::<GacScratch<V>>() {
143            *b = Box::new(GacScratch::<V>::default());
144        }
145        let s = b.downcast_mut::<GacScratch<V>>().unwrap();
146        f(s)
147    })
148}
149
150// ---------------------------------------------------------------------------
151// Unified GAC core
152// ---------------------------------------------------------------------------
153
154/// Run Régin GAC on `scope`, sentinel-generic and incremental.
155///
156/// * `sentinel = None` → plain all-different; `Some(s)` → all-different-except.
157/// * `gac_id = Some(id)` enables the per-constraint matching warm-start cache;
158///   `None` runs stateless (used by the one-shot public wrappers and tests).
159///
160/// Only requires `D::Value: Clone + PartialEq + Debug` (all implied by `Domain`)
161/// plus `'static` for the thread-local scratch — no `Ord`, `Hash`, or
162/// `ValueIndex` bound, so `FiniteDomain<String>` still compiles.
163pub fn propagate_gac_core<D: Domain>(
164    scope: &[VarId],
165    sentinel: Option<&D::Value>,
166    variables: &mut [Variable<D>],
167    depth: usize,
168    gac_id: Option<u32>,
169) -> Revision
170where
171    D::Value: 'static,
172{
173    GAC_CORE_CALLS.fetch_add(1, Ordering::Relaxed);
174    with_scratch::<D::Value, _>(|s| propagate_inner(scope, sentinel, variables, depth, gac_id, s))
175}
176
177fn propagate_inner<D: Domain>(
178    scope: &[VarId],
179    sentinel: Option<&D::Value>,
180    variables: &mut [Variable<D>],
181    depth: usize,
182    gac_id: Option<u32>,
183    s: &mut GacScratch<D::Value>,
184) -> Revision {
185    // ----- Participant + assigned-value collection -----
186    s.assigned_ns.clear();
187    for &v in scope {
188        let dom = &variables[v as usize].domain;
189        if dom.is_empty() {
190            return Revision::Unsatisfiable;
191        }
192        if let Some(val) = dom.singleton_value()
193            && sentinel != Some(&val)
194        {
195            s.assigned_ns.push(val);
196        }
197    }
198
199    s.participants.clear();
200    s.has_sentinel.clear();
201    for (i, &v) in scope.iter().enumerate() {
202        let dom = &variables[v as usize].domain;
203        if dom.is_singleton() {
204            continue;
205        }
206        let dom_has_sentinel = sentinel.map(|sv| dom.contains(sv)).unwrap_or(false);
207        let non_sentinel_count = dom.size() - dom_has_sentinel as usize;
208        if non_sentinel_count == 0 {
209            // {sentinel}-only domain: committed to escape, drops out.
210            continue;
211        }
212        s.participants.push(i);
213        s.has_sentinel.push(dom_has_sentinel);
214    }
215
216    if s.participants.is_empty() {
217        return Revision::Unchanged;
218    }
219    let n_vars = s.participants.len();
220
221    // Plain all-different: below the threshold, singleton removal (which the
222    // caller performs) captures everything GAC would — skip the machinery.
223    if sentinel.is_none() && n_vars < GAC_MIN_PARTICIPANTS {
224        return Revision::Unchanged;
225    }
226
227    // ----- Value universe + bipartite adjacency -----
228    s.all_vals.clear();
229    reset_adj(&mut s.adj, n_vars);
230    for pu in 0..n_vars {
231        let var_id = scope[s.participants[pu]] as usize;
232        for val in variables[var_id].domain.iter() {
233            if sentinel == Some(&val) {
234                continue;
235            }
236            if s.assigned_ns.contains(&val) {
237                continue;
238            }
239            let vi = match s.all_vals.iter().position(|x| *x == val) {
240                Some(k) => k as u32,
241                None => {
242                    s.all_vals.push(val);
243                    (s.all_vals.len() - 1) as u32
244                }
245            };
246            s.adj[pu].push(vi);
247        }
248    }
249
250    // All non-sentinel values consumed by assigned singletons.
251    if s.all_vals.is_empty() {
252        return finish_all_consumed::<D>(scope, sentinel, variables, depth, s);
253    }
254    let n_vals = s.all_vals.len();
255
256    // ----- Warm-start matching from the per-constraint cache -----
257    s.match_u.clear();
258    s.match_u.resize(n_vars, NONE);
259    s.match_v.clear();
260    s.match_v.resize(n_vals, NONE);
261    s.dist.clear();
262    s.dist.resize(n_vars, 0);
263
264    if let Some(id) = gac_id
265        && let Some(cvec) = s.cache.get(&id)
266    {
267        for pu in 0..n_vars {
268            let Some(Some(cv)) = cvec.get(s.participants[pu]) else {
269                continue;
270            };
271            let Some(vi) = s.all_vals.iter().position(|x| x == cv) else {
272                continue;
273            };
274            let vi = vi as u32;
275            if s.match_v[vi as usize] == NONE && s.adj[pu].contains(&vi) {
276                s.match_u[pu] = vi;
277                s.match_v[vi as usize] = pu as u32;
278            }
279        }
280    }
281
282    hopcroft_karp(
283        n_vars,
284        n_vals,
285        &s.adj,
286        &mut s.match_u,
287        &mut s.match_v,
288        &mut s.dist,
289        &mut s.queue,
290    );
291
292    // ----- Coverage: sentinel-less participants must be matched -----
293    for pu in 0..n_vars {
294        if s.match_u[pu] == NONE && !s.has_sentinel[pu] {
295            return Revision::Unsatisfiable;
296        }
297    }
298
299    // Persist the fresh matching for the next call's warm start.
300    if let Some(id) = gac_id {
301        if s.cache.len() > CACHE_CAP {
302            s.cache.clear();
303        }
304        let cvec = s.cache.entry(id).or_default();
305        cvec.clear();
306        cvec.resize(scope.len(), None);
307        for pu in 0..n_vars {
308            if s.match_u[pu] != NONE {
309                cvec[s.participants[pu]] = Some(s.all_vals[s.match_u[pu] as usize].clone());
310            }
311        }
312    }
313
314    // ----- Residual graph -----
315    let total_nodes = n_vars + n_vals;
316    reset_adj(&mut s.res_adj, total_nodes);
317    for u in 0..n_vars {
318        let matched_vi = s.match_u[u];
319        for &vi in &s.adj[u] {
320            let val_node = (n_vars as u32) + vi;
321            if vi == matched_vi {
322                s.res_adj[val_node as usize].push(u as u32);
323            } else {
324                s.res_adj[u].push(val_node);
325            }
326        }
327    }
328
329    // ----- Reachability from free vertices -----
330    s.reachable.clear();
331    s.reachable.resize(total_nodes, false);
332    s.bfs.clear();
333    for vi in 0..n_vals {
334        if s.match_v[vi] == NONE {
335            let node = n_vars + vi;
336            s.reachable[node] = true;
337            s.bfs.push(node as u32);
338        }
339    }
340    for pu in 0..n_vars {
341        if s.match_u[pu] == NONE && s.has_sentinel[pu] {
342            s.reachable[pu] = true;
343            s.bfs.push(pu as u32);
344        }
345    }
346    let mut head = 0;
347    while head < s.bfs.len() {
348        let node = s.bfs[head] as usize;
349        head += 1;
350        for i in 0..s.res_adj[node].len() {
351            let next = s.res_adj[node][i] as usize;
352            if !s.reachable[next] {
353                s.reachable[next] = true;
354                s.bfs.push(next as u32);
355            }
356        }
357    }
358
359    // ----- SCCs -----
360    resize_tarjan(s, total_nodes);
361    tarjan_scc(
362        total_nodes,
363        &s.res_adj,
364        &mut s.t_index,
365        &mut s.t_lowlink,
366        &mut s.t_onstack,
367        &mut s.t_scc,
368        &mut s.t_stack,
369        &mut s.t_call,
370    );
371
372    // ----- Prune -----
373    let mut changed = false;
374
375    // Phase 1: assigned-singleton non-sentinel values were excluded from the
376    // graph; remove them from every participant's live domain.
377    if !s.assigned_ns.is_empty() {
378        for pu in 0..n_vars {
379            let var_id = scope[s.participants[pu]] as usize;
380            for k in 0..s.assigned_ns.len() {
381                let val = s.assigned_ns[k].clone();
382                if variables[var_id].prune(&val, depth) {
383                    changed = true;
384                }
385            }
386            if variables[var_id].domain.is_empty() {
387                return Revision::Unsatisfiable;
388            }
389        }
390    }
391
392    // Phase 2: Régin SCC pruning — drop unmatched (var, val) edges crossing SCC
393    // boundaries and not reachable from a free vertex.
394    for pu in 0..n_vars {
395        let var_id = scope[s.participants[pu]] as usize;
396        let matched_vi = s.match_u[pu];
397        for i in 0..s.adj[pu].len() {
398            let vi = s.adj[pu][i];
399            if vi == matched_vi {
400                continue;
401            }
402            let val_node = n_vars + vi as usize;
403            if s.t_scc[pu] == s.t_scc[val_node] || s.reachable[val_node] {
404                continue;
405            }
406            let val = s.all_vals[vi as usize].clone();
407            if variables[var_id].prune(&val, depth) {
408                changed = true;
409            }
410        }
411        if variables[var_id].domain.is_empty() {
412            return Revision::Unsatisfiable;
413        }
414    }
415
416    if changed {
417        Revision::Changed
418    } else {
419        Revision::Unchanged
420    }
421}
422
423/// Handle the degenerate case where the value universe is empty (every
424/// non-sentinel value was consumed by an assigned singleton).
425fn finish_all_consumed<D: Domain>(
426    scope: &[VarId],
427    sentinel: Option<&D::Value>,
428    variables: &mut [Variable<D>],
429    depth: usize,
430    s: &mut GacScratch<D::Value>,
431) -> Revision {
432    // Plain variant: an unassigned variable with no available value is UNSAT.
433    if sentinel.is_none() {
434        return Revision::Unsatisfiable;
435    }
436    // Sentinel variant: participants survive only via the escape valve. Any
437    // participant without a sentinel is unsatisfiable; the rest have their
438    // (now-forbidden) assigned values pruned.
439    for pu in 0..s.participants.len() {
440        if !s.has_sentinel[pu] {
441            return Revision::Unsatisfiable;
442        }
443    }
444    let mut changed = false;
445    for pu in 0..s.participants.len() {
446        let var_id = scope[s.participants[pu]] as usize;
447        for k in 0..s.assigned_ns.len() {
448            let val = s.assigned_ns[k].clone();
449            if variables[var_id].prune(&val, depth) {
450                changed = true;
451            }
452        }
453        if variables[var_id].domain.is_empty() {
454            return Revision::Unsatisfiable;
455        }
456    }
457    if changed {
458        Revision::Changed
459    } else {
460        Revision::Unchanged
461    }
462}
463
464fn resize_tarjan<V>(s: &mut GacScratch<V>, n: usize) {
465    s.t_index.resize(n, NONE);
466    s.t_lowlink.resize(n, 0);
467    s.t_onstack.clear();
468    s.t_onstack.resize(n, false);
469    s.t_scc.resize(n, NONE);
470}