Skip to content

Commit 6029b08

Browse files
committed
count to accept variables, xor implemented
1 parent 9f84c1c commit 6029b08

File tree

8 files changed

+406
-0
lines changed

8 files changed

+406
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
44

55
## [0.14.0] - 2025-10-16
66
- count(var, var) now uses Vies-s and can work with both const and vars
7+
- Boolean XOR implemented
78

89
## [0.12.8] - 2025-10-16
910
- Introduced count_var() constraint for variable counts

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ m.gcc(&vars, values, counts); // global cardinality constraint
6060
let and_result = m.bool_and(&[a, b]); // a AND b
6161
let or_result = m.bool_or(&[a, b]); // a OR b
6262
let not_result = m.bool_not(a); // NOT a
63+
let xor_result = m.bool_xor(a, b); // a XOR b
6364
m.implies(a, b); // a → b (if a then b)
6465
m.bool_clause(&[a, b], &[c]); // a ∨ b ∨ ¬c (CNF clause)
6566

src/constraints/api/boolean.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,24 @@ impl Model {
6262
result
6363
}
6464

65+
#[doc(hidden)]
66+
/// Create a variable representing the boolean XOR of two operands.
67+
/// Returns a variable that is 1 if exactly one operand is non-zero, 0 otherwise.
68+
///
69+
/// # Examples
70+
/// ```
71+
/// use selen::prelude::*;
72+
/// let mut m = Model::default();
73+
/// let a = m.bool();
74+
/// let b = m.bool();
75+
/// let xor_result = m.bool_xor(a, b);
76+
/// ```
77+
pub fn bool_xor(&mut self, x: VarId, y: VarId) -> VarId {
78+
let result = self.bool(); // Create a boolean variable (0 or 1)
79+
self.props.bool_xor(x, y, result);
80+
result
81+
}
82+
6583
/// Post a boolean implication constraint: `a → b` (if a then b).
6684
///
6785
/// If `a` is true (1), then `b` must be true (1).

src/constraints/props/bool_logic.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,113 @@ impl Propagate for BoolNot {
269269
.chain(core::iter::once(self.operand))
270270
}
271271
}
272+
273+
/// Boolean XOR constraint: `result = a XOR b`.
274+
/// This constraint enforces that the result variable equals the logical XOR of two boolean operands.
275+
/// XOR is true (1) when exactly one operand is true (non-zero).
276+
/// Variables are treated as boolean: 0 = false, non-zero = true.
277+
#[derive(Clone, Debug)]
278+
#[doc(hidden)]
279+
pub struct BoolXor {
280+
x: VarId,
281+
y: VarId,
282+
result: VarId,
283+
}
284+
285+
impl BoolXor {
286+
pub fn new(x: VarId, y: VarId, result: VarId) -> Self {
287+
Self { x, y, result }
288+
}
289+
}
290+
291+
impl Prune for BoolXor {
292+
fn prune(&self, ctx: &mut Context) -> Option<()> {
293+
// XOR truth table (treating as boolean: 0 = false, 1 = true):
294+
// x | y | x XOR y
295+
// --+---+--------
296+
// 0 | 0 | 0
297+
// 0 | 1 | 1
298+
// 1 | 0 | 1
299+
// 1 | 1 | 0
300+
//
301+
// So: result = 1 iff (x = 0 and y = 1) or (x = 1 and y = 0)
302+
// result = 0 iff (x = 0 and y = 0) or (x = 1 and y = 1)
303+
304+
let x_min = self.x.min(ctx);
305+
let x_max = self.x.max(ctx);
306+
let y_min = self.y.min(ctx);
307+
let y_max = self.y.max(ctx);
308+
let result_min = self.result.min(ctx);
309+
let result_max = self.result.max(ctx);
310+
311+
// Case 1: result is fixed to 1 (must be true)
312+
if result_min >= Val::ValI(1) {
313+
// x XOR y must be true
314+
// This means: (x=0 AND y=1) OR (x=1 AND y=0)
315+
316+
// If x is definitely 0, then y must be definitely 1
317+
if x_max <= Val::ValI(0) {
318+
let _min = self.y.try_set_min(Val::ValI(1), ctx)?;
319+
}
320+
// If x is definitely 1 (>= 1), then y must be definitely 0
321+
if x_min >= Val::ValI(1) {
322+
let _max = self.y.try_set_max(Val::ValI(0), ctx)?;
323+
}
324+
// Similarly for y
325+
if y_max <= Val::ValI(0) {
326+
let _min = self.x.try_set_min(Val::ValI(1), ctx)?;
327+
}
328+
if y_min >= Val::ValI(1) {
329+
let _max = self.x.try_set_max(Val::ValI(0), ctx)?;
330+
}
331+
}
332+
333+
// Case 2: result is fixed to 0 (must be false)
334+
if result_max <= Val::ValI(0) {
335+
// x XOR y must be false
336+
// This means: (x=0 AND y=0) OR (x=1 AND y=1)
337+
338+
// If x is definitely 0, then y must be definitely 0
339+
if x_max <= Val::ValI(0) {
340+
let _max = self.y.try_set_max(Val::ValI(0), ctx)?;
341+
}
342+
// If x is definitely 1, then y must be definitely 1
343+
if x_min >= Val::ValI(1) {
344+
let _min = self.y.try_set_min(Val::ValI(1), ctx)?;
345+
}
346+
// Similarly for y
347+
if y_max <= Val::ValI(0) {
348+
let _max = self.x.try_set_max(Val::ValI(0), ctx)?;
349+
}
350+
if y_min >= Val::ValI(1) {
351+
let _min = self.x.try_set_min(Val::ValI(1), ctx)?;
352+
}
353+
}
354+
355+
// Case 3: Propagate from fixed x and y to result
356+
// If both x and y are fixed (or determinable)
357+
if x_min == x_max && y_min == y_max {
358+
let x_bool = x_min >= Val::ValI(1);
359+
let y_bool = y_min >= Val::ValI(1);
360+
let xor_result = x_bool != y_bool; // XOR operation
361+
362+
if xor_result {
363+
let _min = self.result.try_set_min(Val::ValI(1), ctx)?;
364+
let _max = self.result.try_set_max(Val::ValI(1), ctx)?;
365+
} else {
366+
let _min = self.result.try_set_min(Val::ValI(0), ctx)?;
367+
let _max = self.result.try_set_max(Val::ValI(0), ctx)?;
368+
}
369+
}
370+
371+
Some(())
372+
}
373+
}
374+
375+
impl Propagate for BoolXor {
376+
fn list_trigger_vars(&self) -> impl Iterator<Item = VarId> {
377+
core::iter::once(self.result)
378+
.chain(core::iter::once(self.x))
379+
.chain(core::iter::once(self.y))
380+
}
381+
}

src/constraints/props/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,6 +1362,27 @@ impl Propagators {
13621362
)
13631363
}
13641364

1365+
pub fn bool_xor(&mut self, x: VarId, y: VarId, result: VarId) -> PropId {
1366+
use crate::optimization::constraint_metadata::{ConstraintData, ConstraintType, ViewInfo};
1367+
1368+
let x_info = ViewInfo::Variable { var_id: x };
1369+
let y_info = ViewInfo::Variable { var_id: y };
1370+
let result_info = ViewInfo::Variable { var_id: result };
1371+
1372+
let variables = vec![x, y, result];
1373+
1374+
let metadata = ConstraintData::NAry {
1375+
operands: vec![x_info, y_info, result_info],
1376+
};
1377+
1378+
self.push_new_prop_with_metadata(
1379+
self::bool_logic::BoolXor::new(x, y, result),
1380+
ConstraintType::BooleanXor,
1381+
variables,
1382+
metadata,
1383+
)
1384+
}
1385+
13651386
// ═══════════════════════════════════════════════════════════════════════
13661387
// 🔄 Reification Constraints
13671388
// ═══════════════════════════════════════════════════════════════════════

src/optimization/constraint_metadata.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ pub enum ConstraintType {
6666
BooleanOr,
6767
/// Boolean NOT constraint (result = NOT operand)
6868
BooleanNot,
69+
/// Boolean XOR constraint (result = a XOR b)
70+
BooleanXor,
6971
/// Reified equality constraint (b ⇔ (x = y))
7072
EqualityReified,
7173
/// Reified inequality constraint (b ⇔ (x ≠ y))
@@ -417,6 +419,7 @@ impl ConstraintRegistry {
417419
ConstraintType::BooleanAnd |
418420
ConstraintType::BooleanOr |
419421
ConstraintType::BooleanNot |
422+
ConstraintType::BooleanXor |
420423
ConstraintType::AllDifferent |
421424
ConstraintType::AllEqual |
422425
ConstraintType::Element |

tests/main_tests.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,5 +179,8 @@ mod test_count_var;
179179
#[path = "../tests_all/test_count_view_flexibility.rs"]
180180
mod test_count_view_flexibility;
181181

182+
#[path = "../tests_all/test_bool_xor.rs"]
183+
mod test_bool_xor;
184+
182185
#[path = "../tests_all/test_element_computed_index.rs"]
183186
mod test_element_computed_index;

0 commit comments

Comments
 (0)