Skip to main content

pumpkin_constraints/constraints/
table.rs

1use std::collections::BTreeMap;
2
3use pumpkin_core::Solver;
4use pumpkin_core::constraints::Constraint;
5use pumpkin_core::constraints::NegatableConstraint;
6use pumpkin_core::predicate;
7use pumpkin_core::proof::ConstraintTag;
8use pumpkin_core::variables::IntegerVariable;
9use pumpkin_core::variables::Literal;
10
11/// Create the [table](https://sofdem.github.io/gccat/gccat/Cin_relation.html#uid22830) [`NegatableConstraint`].
12///
13/// A table constraint constrains a tuple of variables to have pre-defined values. For example:
14/// ```ignore
15/// (x1, x2, x3) in {(1, 3, 5), (3, 1, 4)}
16/// ```
17/// This has two solutions: either the first tuple of values is assigned to the variables, or the
18/// second. The set of value tuples is the 'table'.
19///
20/// In the XCSP3 specification, this is the "positive table"
21/// (<https://www.xcsp.org/specifications/constraints/generic/extension/>).
22pub fn table<Var: IntegerVariable + 'static>(
23    xs: impl IntoIterator<Item = Var>,
24    table: Vec<Vec<i32>>,
25    constraint_tag: ConstraintTag,
26) -> impl NegatableConstraint {
27    Table {
28        xs: xs.into_iter().collect(),
29        table,
30        constraint_tag,
31    }
32}
33
34/// Create the negative [table](https://sofdem.github.io/gccat/gccat/Cin_relation.html#uid22830) [`NegatableConstraint`].
35///
36/// A negative table is essentially a set of conflicts over the given variables. For example:
37/// ```ignore
38/// (x1, x2, x3) not in {(1, 3, 5), (3, 1, 4)}
39/// ```
40/// This prevents any solution where the variables have both the first and the second tuple as
41/// values.
42///
43/// In the XCSP3 specification, this is the "negative table"
44/// (<https://www.xcsp.org/specifications/constraints/generic/extension/>).
45pub fn negative_table<Var: IntegerVariable + 'static>(
46    xs: impl IntoIterator<Item = Var>,
47    table: Vec<Vec<i32>>,
48    constraint_tag: ConstraintTag,
49) -> impl NegatableConstraint {
50    NegativeTable {
51        xs: xs.into_iter().collect(),
52        table,
53        constraint_tag,
54    }
55}
56
57struct Table<Var> {
58    xs: Vec<Var>,
59    table: Vec<Vec<i32>>,
60    constraint_tag: ConstraintTag,
61}
62
63impl<Var: IntegerVariable> Table<Var> {
64    fn encode(self, solver: &mut Solver, reification_literal: Option<Literal>) {
65        // 1. Create a variable `y_i` that selects the row from the table which is chosen.
66        let ys: Vec<_> = (0..self.table.len())
67            .map(|_| solver.new_literal())
68            .collect();
69
70        // 2. Setup the implications between values and `ys`.
71        for (col, x_col) in self.xs.iter().enumerate() {
72            // A map from domain values to the `ys` variables that support this value.
73            let mut values = BTreeMap::new();
74
75            // For every value in this column, aggregate the `ys` that support it.
76            for (row, &y_row) in ys.iter().enumerate() {
77                let value = self.table[row][col];
78
79                let supports = values.entry(value).or_insert(vec![]);
80                supports.push(y_row);
81            }
82
83            // For every value in this column, add the clause
84            //   `condition <-> (\/ supports)`
85            for (value, supports) in values {
86                let condition = predicate![x_col == value];
87
88                // For every `support in supports`: `support -> condition`
89                for support in supports.iter() {
90                    let mut clause = vec![support.get_false_predicate(), condition];
91
92                    // Account for possible reification.
93                    // l -> clause
94                    clause.extend(reification_literal.iter().map(|l| l.get_false_predicate()));
95
96                    solver.add_clause(clause, self.constraint_tag);
97                }
98
99                // `condition -> (\/ supports)`
100                let mut clause = vec![!condition];
101                clause.extend(supports.iter().map(|l| l.get_true_predicate()));
102                // Account for possible reification.
103                clause.extend(reification_literal.iter().map(|l| l.get_false_predicate()));
104
105                solver.add_clause(clause, self.constraint_tag);
106            }
107        }
108
109        // 4. Enforce at least one `y` to be true.
110        let poster = solver.add_constraint(crate::constraints::clause(ys, self.constraint_tag));
111        if let Some(literal) = reification_literal {
112            poster.implied_by(literal);
113        } else {
114            poster.post();
115        }
116    }
117}
118
119impl<Var: IntegerVariable> Constraint for Table<Var> {
120    fn post(self, solver: &mut Solver) {
121        self.encode(solver, None)
122    }
123
124    fn implied_by(self, solver: &mut Solver, reification_literal: Literal) {
125        self.encode(solver, Some(reification_literal))
126    }
127}
128
129impl<Var: IntegerVariable + 'static> NegatableConstraint for Table<Var> {
130    type NegatedConstraint = NegativeTable<Var>;
131
132    fn negation(&self) -> Self::NegatedConstraint {
133        let xs = self.xs.clone();
134        let table = self.table.clone();
135        let constraint_tag = self.constraint_tag;
136
137        NegativeTable {
138            xs,
139            table,
140            constraint_tag,
141        }
142    }
143}
144
145struct NegativeTable<Var> {
146    xs: Vec<Var>,
147    table: Vec<Vec<i32>>,
148    constraint_tag: ConstraintTag,
149}
150
151impl<Var: IntegerVariable> Constraint for NegativeTable<Var> {
152    fn post(self, solver: &mut Solver) {
153        for row in self.table {
154            let clause: Vec<_> = self
155                .xs
156                .iter()
157                .zip(row)
158                .map(|(x, value)| predicate![x != value])
159                .collect();
160
161            solver.add_clause(clause, self.constraint_tag);
162        }
163    }
164
165    fn implied_by(self, solver: &mut Solver, reification_literal: Literal) {
166        for row in self.table {
167            let clause: Vec<_> = self
168                .xs
169                .iter()
170                .zip(row)
171                .map(|(x, value)| predicate![x != value])
172                .chain(std::iter::once(reification_literal.get_false_predicate()))
173                .collect();
174
175            solver.add_clause(clause, self.constraint_tag);
176        }
177    }
178}
179
180impl<Var: IntegerVariable + 'static> NegatableConstraint for NegativeTable<Var> {
181    type NegatedConstraint = Table<Var>;
182
183    fn negation(&self) -> Self::NegatedConstraint {
184        let xs = self.xs.clone();
185        let table = self.table.clone();
186        let constraint_tag = self.constraint_tag;
187
188        Table {
189            xs,
190            table,
191            constraint_tag,
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use pumpkin_core::results::CSPSolverExecutionFlag;
199
200    use super::*;
201
202    #[test]
203    fn eliminating_all_supporting_rows_prunes_the_value() {
204        let mut solver = Solver::default();
205
206        let constraint_tag = solver.new_constraint_tag();
207        let x1 = solver.new_named_bounded_integer(1, 2, "x1");
208        let x2 = solver.new_named_sparse_integer([10, 20, 30], "x2");
209
210        // Rows 2 and 3 are the only support for `x2 == 30`, and are exactly the rows that support
211        // `x1 == 2`. Fixing `x1 == 1` should eliminate both of them, which should in turn prune
212        // `30` from the domain of `x2`.
213        let rows = vec![vec![1, 10], vec![1, 20], vec![2, 30], vec![2, 30]];
214        table(vec![x1, x2], rows, constraint_tag).post(&mut solver);
215
216        let result = solver.propagate_to_fixpoint();
217        assert_eq!(result, CSPSolverExecutionFlag::Feasible);
218
219        solver.add_clause([predicate![x1 == 1]], constraint_tag);
220
221        let result = solver.propagate_to_fixpoint();
222        assert_eq!(result, CSPSolverExecutionFlag::Feasible);
223
224        assert_eq!(solver.upper_bound(&x2), 20);
225    }
226}