|
| 1 | +# Phase 2 FlatZinc Integration - Reification Comparison Constraints |
| 2 | + |
| 3 | +## Summary |
| 4 | + |
| 5 | +This document describes the implementation of reified comparison constraints for FlatZinc Phase 2 integration. |
| 6 | + |
| 7 | +## Implementation Date |
| 8 | + |
| 9 | +October 1, 2025 |
| 10 | + |
| 11 | +## What Was Implemented |
| 12 | + |
| 13 | +### New Reification Constraints |
| 14 | + |
| 15 | +Added four new reified comparison constraints to complement the existing `int_eq_reif` and `int_ne_reif`: |
| 16 | + |
| 17 | +1. **`int_lt_reif(x, y, b)`** - Reified less-than: `b ⇔ (x < y)` |
| 18 | +2. **`int_le_reif(x, y, b)`** - Reified less-or-equal: `b ⇔ (x ≤ y)` |
| 19 | +3. **`int_gt_reif(x, y, b)`** - Reified greater-than: `b ⇔ (x > y)` |
| 20 | +4. **`int_ge_reif(x, y, b)`** - Reified greater-or-equal: `b ⇔ (x ≥ y)` |
| 21 | + |
| 22 | +### How Reification Works |
| 23 | + |
| 24 | +A reified constraint `b ⇔ C` creates a bidirectional implication: |
| 25 | +- If `b = 1` (true), then constraint `C` must hold |
| 26 | +- If `b = 0` (false), then constraint `C` must NOT hold |
| 27 | +- If `C` holds, then `b` must be 1 |
| 28 | +- If `C` does not hold, then `b` must be 0 |
| 29 | + |
| 30 | +### Propagation Strategy |
| 31 | + |
| 32 | +All reification propagators use **bidirectional propagation** without early returns: |
| 33 | + |
| 34 | +**Direction 1: From variable domains → infer boolean** |
| 35 | +- If the constraint is definitely true (domains force it), set `b = 1` |
| 36 | +- If the constraint is definitely false (domains prevent it), set `b = 0` |
| 37 | + |
| 38 | +**Direction 2: From boolean → enforce constraint** |
| 39 | +- If `b = 1`, enforce the constraint on the variables |
| 40 | +- If `b = 0`, enforce the negation of the constraint |
| 41 | + |
| 42 | +This design ensures: |
| 43 | +- No dependency on constraint posting order |
| 44 | +- Deterministic behavior regardless of propagation scheduling |
| 45 | +- Proper interaction with other constraints (bool_clause, linear, etc.) |
| 46 | + |
| 47 | +## Files Modified |
| 48 | + |
| 49 | +### Core Implementation |
| 50 | +- `src/constraints/props/reification.rs` - Added 4 new propagator structs |
| 51 | +- `src/constraints/props/mod.rs` - Added registration methods and fixed optimization bug |
| 52 | +- `src/model/constraints.rs` - Added 4 public API methods |
| 53 | +- `src/optimization/constraint_metadata.rs` - Added 4 new constraint types |
| 54 | + |
| 55 | +### Testing |
| 56 | +- `tests/test_phase2_integration.rs` - Created 12 comprehensive integration tests |
| 57 | + - All tests passing ✅ |
| 58 | + |
| 59 | +### Documentation |
| 60 | +- `examples/reification_comparison.rs` - Comprehensive examples demonstrating: |
| 61 | + - Conditional constraints (if-then-else logic) |
| 62 | + - Maximum computation using reification |
| 63 | + - Counting constraints |
| 64 | + - Partial ordering constraints |
| 65 | + - Range membership checking |
| 66 | + |
| 67 | +## Critical Bug Fix |
| 68 | + |
| 69 | +### Problem |
| 70 | +The `optimize_universal_constraint_order()` function was removing reification constraints during optimization because they weren't in the `constraint_types_to_optimize` list. This caused reification propagators to be deleted before search started. |
| 71 | + |
| 72 | +### Symptoms |
| 73 | +- Reification propagators were created successfully |
| 74 | +- They were registered in the dependency graph |
| 75 | +- But they disappeared during `prepare_for_search()` |
| 76 | +- Tests failed with incorrect solutions |
| 77 | +- No errors or warnings were generated |
| 78 | + |
| 79 | +### Root Cause |
| 80 | +The optimization function clears `self.state` and rebuilds it with only the constraints in the optimization list: |
| 81 | + |
| 82 | +```rust |
| 83 | +// Reorder propagators based on priority |
| 84 | +self.state.clear(); // ← CLEARS ALL PROPAGATORS |
| 85 | +for &(prop_idx, _, _) in &constraint_priorities { |
| 86 | + self.state.push(original_state[prop_idx].clone()); // ← Only adds optimized ones! |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +### Solution |
| 91 | +Added all 6 reification constraint types to the `constraint_types_to_optimize` array: |
| 92 | + |
| 93 | +```rust |
| 94 | +let constraint_types_to_optimize = [ |
| 95 | + // ... existing types ... |
| 96 | + ConstraintType::EqualityReified, |
| 97 | + ConstraintType::InequalityReified, |
| 98 | + ConstraintType::LessThanReified, |
| 99 | + ConstraintType::LessEqualReified, |
| 100 | + ConstraintType::GreaterThanReified, |
| 101 | + ConstraintType::GreaterEqualReified, |
| 102 | +]; |
| 103 | +``` |
| 104 | + |
| 105 | +## Test Results |
| 106 | + |
| 107 | +All 12 integration tests pass consistently: |
| 108 | + |
| 109 | +``` |
| 110 | +running 12 tests |
| 111 | +test test_cnf_with_linear_le ... ok |
| 112 | +test test_cnf_with_linear_eq ... ok |
| 113 | +test test_multiple_linear_with_clause ... ok |
| 114 | +test test_reified_linear_eq ... ok |
| 115 | +test test_linear_eq_with_bool_clause ... ok |
| 116 | +test test_3sat_with_linear ... ok |
| 117 | +test test_reified_ne_with_clause ... ok |
| 118 | +test test_negative_coeff_with_clause ... ok |
| 119 | +test test_linear_le_with_bool_clause ... ok |
| 120 | +test test_chained_reifications ... ok |
| 121 | +test test_reified_linear_eq_false ... ok |
| 122 | +test test_large_integration ... ok |
| 123 | +
|
| 124 | +test result: ok. 12 passed; 0 failed |
| 125 | +``` |
| 126 | + |
| 127 | +## Usage Examples |
| 128 | + |
| 129 | +### Basic Reification |
| 130 | + |
| 131 | +```rust |
| 132 | +use selen::prelude::*; |
| 133 | + |
| 134 | +let mut m = Model::default(); |
| 135 | +let x = m.int(0, 10); |
| 136 | +let y = m.int(0, 10); |
| 137 | +let b = m.bool(); |
| 138 | + |
| 139 | +// b will be 1 iff x < y |
| 140 | +m.int_lt_reif(x, y, b); |
| 141 | +``` |
| 142 | + |
| 143 | +### Conditional Constraints |
| 144 | + |
| 145 | +```rust |
| 146 | +// If x > 5, then y = 10, else y = 0 |
| 147 | +let x = m.int(0, 10); |
| 148 | +let y = m.int(0, 10); |
| 149 | +let b = m.bool(); |
| 150 | +let five = m.int(5, 5); |
| 151 | + |
| 152 | +m.int_gt_reif(x, five, b); |
| 153 | + |
| 154 | +let y_eq_10 = m.bool(); |
| 155 | +let y_eq_0 = m.bool(); |
| 156 | +let ten = m.int(10, 10); |
| 157 | +let zero = m.int(0, 0); |
| 158 | + |
| 159 | +m.int_eq_reif(y, ten, y_eq_10); |
| 160 | +m.int_eq_reif(y, zero, y_eq_0); |
| 161 | + |
| 162 | +// b → y_eq_10 and ¬b → y_eq_0 |
| 163 | +m.bool_clause(&[y_eq_10], &[b]); |
| 164 | +m.bool_clause(&[b, y_eq_0], &[]); |
| 165 | +``` |
| 166 | + |
| 167 | +### Counting |
| 168 | + |
| 169 | +```rust |
| 170 | +// Count how many of x, y, z are greater than 5 |
| 171 | +let x = m.int(0, 10); |
| 172 | +let y = m.int(0, 10); |
| 173 | +let z = m.int(0, 10); |
| 174 | +let five = m.int(5, 5); |
| 175 | + |
| 176 | +let x_gt_5 = m.bool(); |
| 177 | +let y_gt_5 = m.bool(); |
| 178 | +let z_gt_5 = m.bool(); |
| 179 | + |
| 180 | +m.int_gt_reif(x, five, x_gt_5); |
| 181 | +m.int_gt_reif(y, five, y_gt_5); |
| 182 | +m.int_gt_reif(z, five, z_gt_5); |
| 183 | + |
| 184 | +// Require exactly 2 to be > 5 |
| 185 | +m.int_lin_eq(&[1, 1, 1], &[x_gt_5, y_gt_5, z_gt_5], 2); |
| 186 | +``` |
| 187 | + |
| 188 | +## Design Decisions |
| 189 | + |
| 190 | +### Why Bidirectional Propagation Without Early Returns? |
| 191 | + |
| 192 | +The original implementation used early returns after checking one direction: |
| 193 | + |
| 194 | +```rust |
| 195 | +// OLD - BAD |
| 196 | +if x_max < y_min { |
| 197 | + self.b.try_set_min(Val::ValI(1), ctx)?; |
| 198 | + return Some(()); // ← Early return |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +This caused non-deterministic failures when constraints were posted in certain orders. The fix removes early returns and always checks all directions: |
| 203 | + |
| 204 | +```rust |
| 205 | +// NEW - GOOD |
| 206 | +if x_max < y_min { |
| 207 | + self.b.try_set_min(Val::ValI(1), ctx)?; |
| 208 | +} |
| 209 | +// Continue to check other directions... |
| 210 | +``` |
| 211 | + |
| 212 | +This ensures the propagator makes progress regardless of when it runs or what other constraints have propagated. |
| 213 | + |
| 214 | +### Why Include Reification in Optimization? |
| 215 | + |
| 216 | +Reification constraints can be expensive to propagate, so including them in constraint optimization allows the solver to: |
| 217 | +- Schedule them appropriately relative to other constraints |
| 218 | +- Prioritize them based on variable connectivity |
| 219 | +- Maintain consistency with the optimization framework |
| 220 | + |
| 221 | +Without inclusion in optimization, they would be silently removed, causing subtle and hard-to-debug failures. |
| 222 | + |
| 223 | +## API Completeness |
| 224 | + |
| 225 | +All six comparison reification constraints are now available: |
| 226 | + |
| 227 | +| Constraint | Meaning | API Method | |
| 228 | +|-----------|---------|-----------| |
| 229 | +| `b ⇔ (x = y)` | Equality | `int_eq_reif(x, y, b)` ✅ | |
| 230 | +| `b ⇔ (x ≠ y)` | Inequality | `int_ne_reif(x, y, b)` ✅ | |
| 231 | +| `b ⇔ (x < y)` | Less than | `int_lt_reif(x, y, b)` ✅ | |
| 232 | +| `b ⇔ (x ≤ y)` | Less or equal | `int_le_reif(x, y, b)` ✅ | |
| 233 | +| `b ⇔ (x > y)` | Greater than | `int_gt_reif(x, y, b)` ✅ | |
| 234 | +| `b ⇔ (x ≥ y)` | Greater or equal | `int_ge_reif(x, y, b)` ✅ | |
| 235 | + |
| 236 | +## Next Steps |
| 237 | + |
| 238 | +### Immediate |
| 239 | +- ✅ Run full test suite to ensure no regressions |
| 240 | +- ✅ Document the implementation |
| 241 | +- Commit changes with clear message |
| 242 | + |
| 243 | +### Future Phase 2 Work |
| 244 | +- Implement more FlatZinc global constraints |
| 245 | +- Add set constraints |
| 246 | +- Implement float constraints |
| 247 | +- Create FlatZinc parser integration tests |
| 248 | + |
| 249 | +## References |
| 250 | + |
| 251 | +- FlatZinc specification: https://www.minizinc.org/doc-2.5.5/en/fzn-spec.html |
| 252 | +- Reification documentation: `docs/development/ZINC_REIFICATION_FIX.md` |
| 253 | +- Integration tests: `tests/test_phase2_integration.rs` |
| 254 | +- Examples: `examples/reification_comparison.rs` |
0 commit comments