Skip to content
Open
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
00c4dd1
feat(eq): Add Directed Eq/Neq graph with specific algorithms
matthiasgreen Jun 26, 2025
7bea240
feat(eq): Graph edge removal
matthiasgreen Jun 27, 2025
b18484e
feat(eq): Implement eq propagation
matthiasgreen Jun 27, 2025
e4fc873
fix(eq): handle activation events
matthiasgreen Jun 30, 2025
30c562c
fix(eq): Fix and test backtracking
matthiasgreen Jun 30, 2025
02b7278
fix(eq): Add inference causes
matthiasgreen Jul 1, 2025
2caba47
refactor(eq): Refactor a couple functions
matthiasgreen Jul 1, 2025
53c659f
feat(eq): Explain first draft
matthiasgreen Jul 2, 2025
c1f5ef6
fix(eq): Fix explanations and propagation, first working impl
matthiasgreen Jul 4, 2025
5d26032
fix(eq): Add stats, change dft to bft, switch to cycle propagation
matthiasgreen Jul 9, 2025
59dd5be
fix(eq): Small improvements
matthiasgreen Jul 9, 2025
7aed0b5
fix(eq): Add correct constraints in solver_impl for eq and neq
matthiasgreen Jul 10, 2025
f598dc2
fix(eq): Improve AltEqTheory unit tests
matthiasgreen Jul 10, 2025
5f607cd
refactor(eq): Reorganize modules
matthiasgreen Jul 10, 2025
6b0d34e
feat(eq): Add propagation checking algorithm
matthiasgreen Jul 15, 2025
8e2043f
fix(eq): Improve propagation algorithm
matthiasgreen Jul 15, 2025
259c98f
fix(eq): fix infinite loop in path restitution
matthiasgreen Jul 15, 2025
a01c7f1
fix(eq): Remove undecided graph, replace with constraint hashmap, imp…
matthiasgreen Jul 17, 2025
7f213b1
refactor(eq): Simplify generics, remove hashset of undecided props
matthiasgreen Jul 17, 2025
71e3864
perf(eq): Greatly improve paths_requiring algorithm
matthiasgreen Jul 17, 2025
59b4107
perf(eq): Multiple smaller optimisations
matthiasgreen Jul 18, 2025
1000e09
fix(eq): Fix error with Neq diff expr
matthiasgreen Jul 22, 2025
450b0ca
feat(eq): Replace HashMaps with RefMaps
matthiasgreen Jul 23, 2025
1816877
feat(eq): Add graph statistics
matthiasgreen Jul 24, 2025
932637e
perf(eq): Improve propagator addition
matthiasgreen Jul 24, 2025
cb34988
perf(ref): Improve ref collection allocation efficiency
matthiasgreen Jul 29, 2025
a9b7096
refactor(eq): Clean up theory
matthiasgreen Jul 29, 2025
80c7295
feat(eq): Add id to node map with union-find
matthiasgreen Jul 29, 2025
5c1651f
feat(eq): Rework graph traversal API and handle node groups
matthiasgreen Aug 1, 2025
c5bae7b
test(eq): Improve unit tests
matthiasgreen Aug 1, 2025
8a0011f
fix(eq): Bug fixes and small performance improvements
matthiasgreen Aug 18, 2025
8a98878
perf(eq): Improved graph node merging
matthiasgreen Aug 20, 2025
1aa3910
chore(eq): Clean up
matthiasgreen Aug 25, 2025
9dda90a
fix(eq): Fix tests and path enumerating algorithm
matthiasgreen Aug 27, 2025
4e9fffe
fix(eq): Bugfixes and stats
matthiasgreen Aug 28, 2025
1d992a8
refactor(eq): Simplify propagation
matthiasgreen Sep 1, 2025
c7849b4
perf(eq): Improve propagator handling
matthiasgreen Sep 8, 2025
184c030
refactor(eq): Replace graph subset + fold with graph transform, add m…
matthiasgreen Sep 8, 2025
f7424b3
perf(eq): Add reusable scratches for fast graph traversal
matthiasgreen Sep 9, 2025
c0b0312
doc(eq): Lots of documentation
matthiasgreen Sep 11, 2025
c2933df
doc(eq): Finish documentation and clean up
matthiasgreen Sep 12, 2025
0e95f51
chore(eq): Fix CI failures
matthiasgreen Sep 12, 2025
b31629b
feat(eq): Use BFS for explanations
matthiasgreen Sep 15, 2025
970b67e
refactor(eq): Small improvements
matthiasgreen Sep 16, 2025
310c2e1
feat(eq): Add edge deactivation propagation type
matthiasgreen Sep 25, 2025
f85df35
chore(eq): Fix reviewed changes
matthiasgreen Sep 25, 2025
0a4836c
doc(eq): Add some high level documentation
matthiasgreen Sep 25, 2025
8c5a7f6
perf(eq): Avoid allocations while collecting activations
matthiasgreen Sep 25, 2025
dacd024
test(eq): Add fuzzing tests for eq solver
matthiasgreen Sep 26, 2025
80496df
test(eq): Add scopes to fuzz tests
matthiasgreen Sep 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ aries_fzn/share/aries_fzn
__pycache__/
*.profraw
lcov.info
profile.json.gz
17 changes: 15 additions & 2 deletions solver/src/collections/ref_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,9 @@ impl<K, V> Default for RefMap<K, V> {
impl<K: Ref, V> RefMap<K, V> {
pub fn insert(&mut self, k: K, v: V) {
let index = k.into();
if index > self.entries.len() {
self.entries.reserve_exact(index - self.entries.len());
}
while self.entries.len() <= index {
self.entries.push(None);
}
Expand Down Expand Up @@ -442,7 +445,8 @@ impl<K: Ref, V> RefMap<K, V> {
if index >= self.entries.len() {
None
} else {
self.entries[index].as_ref()
let res: &Option<V> = &self.entries[index];
res.as_ref()
}
}

Expand All @@ -451,9 +455,13 @@ impl<K: Ref, V> RefMap<K, V> {
if index >= self.entries.len() {
None
} else {
self.entries[index].as_mut()
let res: &mut Option<V> = &mut self.entries[index];
res.as_mut()
}
}

// pub fn get_many_mut_or_insert<const N: usize>(&mut self, ks: [K; N], default: impl Fn() -> V) -> [&mut V; N] {}

pub fn get_or_insert(&mut self, k: K, default: impl FnOnce() -> V) -> &V {
if !self.contains(k) {
self.insert(k, default())
Expand Down Expand Up @@ -562,6 +570,11 @@ impl<K: Ref, V> IterableRefMap<K, V> {
self.map.insert(k, v)
}

pub fn remove(&mut self, k: K) {
self.map.remove(k);
self.keys.retain(|e| *e != k);
}

/// Removes all elements from the Map.
#[inline(never)]
pub fn clear(&mut self) {
Expand Down
14 changes: 14 additions & 0 deletions solver/src/collections/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ impl<K: Ref> Default for RefSet<K> {
}
}

impl<K: Ref> FromIterator<K> for IterableRefSet<K> {
fn from_iter<T: IntoIterator<Item = K>>(iter: T) -> Self {
let mut set = Self::new();
for i in iter {
set.insert(i);
}
set
}
}

/// A set of values that can be converted into small unsigned integers.
/// This extends `RefSet` with a vector of all elements of the set, allowing for fast iteration
/// and clearing.
Expand Down Expand Up @@ -89,6 +99,10 @@ impl<K: Ref> IterableRefSet<K> {
self.set.insert(k, ());
}

pub fn remove(&mut self, k: K) {
self.set.remove(k);
}

pub fn clear(&mut self) {
self.set.clear()
}
Expand Down
7 changes: 6 additions & 1 deletion solver/src/core/state/domain.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
core::{cst_int_to_long, IntCst, LongCst, INT_CST_MAX, INT_CST_MIN},
core::{cst_int_to_long, IntCst, Lit, LongCst, INT_CST_MAX, INT_CST_MIN},
model::lang::Rational,
};
use std::fmt::{Display, Formatter};
Expand Down Expand Up @@ -53,6 +53,11 @@ impl IntDomain {
pub fn disjoint(&self, other: &IntDomain) -> bool {
self.ub < other.lb || other.ub < self.lb
}

pub fn entails(&self, literal: Lit) -> bool {
literal.svar().is_plus() && literal.variable().leq(self.ub).entails(literal)
|| literal.svar().is_minus() && literal.variable().geq(self.lb).entails(literal)
}
}

impl std::ops::Mul for IntDomain {
Expand Down
9 changes: 9 additions & 0 deletions solver/src/core/state/domains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ impl Domains {
self.lb(var) >= self.ub(var)
}

pub fn get_bound(&self, var: VarRef) -> Option<IntCst> {
let (lb, ub) = self.bounds(var);
if lb == ub {
Some(lb)
} else {
None
}
}

pub fn entails(&self, lit: Lit) -> bool {
debug_assert!(!self.doms.entails(lit) || !self.doms.entails(!lit));
self.doms.entails(lit)
Expand Down
18 changes: 18 additions & 0 deletions solver/src/core/state/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use crate::backtrack::{DecLvl, EventIndex};
use crate::core::state::{Domains, Event, Term};
use crate::core::{IntCst, Lit, SignedVar};

use super::IntDomain;

/// View of the domains at a given point in time.
///
/// This is primarily intended to query the state as it was when a literal was inferred.
Expand Down Expand Up @@ -60,11 +62,27 @@ impl<'a> DomainsSnapshot<'a> {
-self.ub(-var.into())
}

pub fn int_domain(&self, var: impl Into<SignedVar>) -> IntDomain {
let (lb, ub) = self.bounds(var.into());
IntDomain::new(lb, ub)
}

pub fn bounds(&self, var: impl Into<SignedVar>) -> (IntCst, IntCst) {
let var = var.into();
(self.lb(var), self.ub(var))
}

/// Returns Some(bound) is ub = lb
pub fn get_bound(&self, var: impl Into<SignedVar>) -> Option<IntCst> {
let var = var.into();
let (lb, ub) = self.bounds(var);
if lb == ub {
Some(lb)
} else {
None
}
}

/// Returns true if the given literal is entailed by the current state;
pub fn entails(&self, lit: Lit) -> bool {
let curr_ub = self.ub(lit.svar());
Expand Down
151 changes: 151 additions & 0 deletions solver/src/reasoners/eq_alt/constraints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use hashbrown::HashMap;
use std::fmt::Debug;

use crate::{
collections::ref_store::RefVec,
core::{literals::Watches, Lit},
create_ref_type,
};

use super::{node::Node, relation::EqRelation};

// TODO: Identical to STN, maybe identify some other common logic and bump up to reasoner module

/// Enabling information for a propagator.
/// A propagator should be enabled iff both literals `active` and `valid` are true.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct Enabler {
/// A literal that is true (but not necessarily present) when the propagator must be active if present
pub active: Lit,
/// A literal that is true when the propagator is within its validity scope, i.e.,
/// when is known to be sound to propagate a change from the source to the target.
///
/// In the simplest case, we have `valid = presence(active)` since by construction
/// `presence(active)` is true iff both variables of the constraint are present.
///
/// `valid` might a more specific literal but always with the constraints that
/// `presence(active) => valid`
pub valid: Lit,
}

impl Enabler {
pub fn new(active: Lit, valid: Lit) -> Enabler {
Enabler { active, valid }
}
}

#[derive(Debug, Clone, Copy)]
pub struct ActivationEvent {
/// the edge to enable
pub prop_id: ConstraintId,
}

impl ActivationEvent {
pub(crate) fn new(prop_id: ConstraintId) -> Self {
Self { prop_id }
}
}

create_ref_type!(ConstraintId);

impl Debug for ConstraintId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Propagator {}", self.to_u32())
}
}

/// One direction of a semi-reified eq or neq constraint.
///
/// Formally enabler.active => a (relation) b
/// with enabler.valid = presence(b) => presence(a)
#[derive(Clone, Hash, Debug, PartialEq, Eq)]
pub struct Constraint {
pub a: Node,
pub b: Node,
pub relation: EqRelation,
pub enabler: Enabler,
}

impl Constraint {
pub fn new(a: Node, b: Node, relation: EqRelation, active: Lit, valid: Lit) -> Self {
Self {
a,
b,
relation,
enabler: Enabler::new(active, valid),
}
}

pub fn new_pair(a: Node, b: Node, relation: EqRelation, active: Lit, ab_valid: Lit, ba_valid: Lit) -> (Self, Self) {
(
Self::new(a, b, relation, active, ab_valid),
Self::new(b, a, relation, active, ba_valid),
)
}
}

// #[derive(Debug, Clone, Copy)]
// enum Event {
// PropagatorAdded,
// WatchAdded(ConstraintId, Lit),
// }

/// Data structures to store propagators.
#[derive(Clone, Default)]
pub struct ConstraintStore {
constraints: RefVec<ConstraintId, Constraint>,
// constraint_lookup: HashMap<(Node, Node), Vec<ConstraintId>>,
in_constraints: HashMap<Node, Vec<ConstraintId>>,
out_constraints: HashMap<Node, Vec<ConstraintId>>,
watches: Watches<(Enabler, ConstraintId)>,
// trail: Trail<Event>,
}

impl ConstraintStore {
pub fn add_constraint(&mut self, constraint: Constraint) -> ConstraintId {
// assert_eq!(self.current_decision_level(), DecLvl::ROOT);
// self.trail.push(Event::PropagatorAdded);
let id = self.constraints.len().into();
self.constraints.push(constraint.clone());
self.out_constraints
.entry(constraint.a)
.and_modify(|v| v.push(id))
.or_insert(vec![id]);
self.in_constraints
.entry(constraint.b)
.and_modify(|v| v.push(id))
.or_insert(vec![id]);
id
}

pub fn add_watch(&mut self, id: ConstraintId, literal: Lit) {
let enabler = self.constraints[id].enabler;
self.watches.add_watch((enabler, id), literal);
// self.trail.push(Event::WatchAdded(id, literal));
}

pub fn get_constraint(&self, constraint_id: ConstraintId) -> &Constraint {
&self.constraints[constraint_id]
}

// Get valid propagators by source and target
// pub fn get_constraints_between(&self, source: Node, target: Node) -> Vec<ConstraintId> {
// self.constraint_lookup.get(&(source, target)).cloned().unwrap_or(vec![])
// }

pub fn get_out_constraints(&self, source: Node) -> Vec<ConstraintId> {
self.out_constraints.get(&source).cloned().unwrap_or_default()
}

pub fn get_in_constraints(&self, source: Node) -> Vec<ConstraintId> {
self.in_constraints.get(&source).cloned().unwrap_or_default()
}

pub fn enabled_by(&self, literal: Lit) -> impl Iterator<Item = (Enabler, ConstraintId)> + '_ {
self.watches.watches_on(literal)
}

pub fn iter(&self) -> impl Iterator<Item = (ConstraintId, &Constraint)> + use<'_> {
self.constraints.entries()
}
}
68 changes: 68 additions & 0 deletions solver/src/reasoners/eq_alt/graph/adj_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::fmt::{Debug, Formatter};

use crate::collections::ref_store::IterableRefMap;

use super::{Edge, NodeId};

#[derive(Default, Clone)]
pub struct EqAdjList(IterableRefMap<NodeId, Vec<Edge>>);

impl Debug for EqAdjList {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f)?;
for (node, edges) in self.0.entries() {
if !edges.is_empty() {
writeln!(f, "{:?}:", node)?;
for edge in edges {
writeln!(f, " -> {:?} {:?}", edge.target, edge)?;
}
}
}
Ok(())
}
}

impl EqAdjList {
/// Insert a node if not present
fn insert_node(&mut self, node: NodeId) {
if !self.0.contains(node) {
self.0.insert(node, Default::default());
}
}

/// Possibly insert an edge and both nodes
/// Returns true if edge was inserted
pub fn insert_edge(&mut self, edge: Edge) -> bool {
self.insert_node(edge.source);
self.insert_node(edge.target);
let edges = self.get_edges_mut(edge.source).unwrap();
if edges.contains(&edge) {
false
} else {
edges.push(edge);
true
}
}

pub fn iter_edges(&self, node: NodeId) -> impl Iterator<Item = &Edge> {
self.0.get(node).into_iter().flat_map(|v| v.iter())
}

pub fn get_edges_mut(&mut self, node: NodeId) -> Option<&mut Vec<Edge>> {
self.0.get_mut(node)
}

pub fn iter_all_edges(&self) -> impl Iterator<Item = Edge> + use<'_> {
self.0.entries().flat_map(|(_, e)| e.iter().cloned())
}

pub fn iter_nodes(&self) -> impl Iterator<Item = NodeId> + use<'_> {
self.0.entries().map(|(n, _)| n)
}

pub fn remove_edge(&mut self, edge: Edge) {
if let Some(set) = self.0.get_mut(edge.source) {
set.retain(|e| *e != edge)
}
}
}
Loading