aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/BoundScopePropagator.java
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/BoundScopePropagator.java')
-rw-r--r--subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/BoundScopePropagator.java240
1 files changed, 240 insertions, 0 deletions
diff --git a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/BoundScopePropagator.java b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/BoundScopePropagator.java
new file mode 100644
index 00000000..ecca6117
--- /dev/null
+++ b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/BoundScopePropagator.java
@@ -0,0 +1,240 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.reasoning.scope;
7
8import com.google.ortools.linearsolver.MPConstraint;
9import com.google.ortools.linearsolver.MPObjective;
10import com.google.ortools.linearsolver.MPSolver;
11import com.google.ortools.linearsolver.MPVariable;
12import org.eclipse.collections.api.factory.primitive.IntObjectMaps;
13import org.eclipse.collections.api.factory.primitive.IntSets;
14import org.eclipse.collections.api.map.primitive.MutableIntObjectMap;
15import org.eclipse.collections.api.set.primitive.MutableIntSet;
16import tools.refinery.store.dse.propagation.BoundPropagator;
17import tools.refinery.store.dse.propagation.PropagationResult;
18import tools.refinery.store.model.Interpretation;
19import tools.refinery.store.model.Model;
20import tools.refinery.store.query.ModelQueryAdapter;
21import tools.refinery.store.representation.cardinality.*;
22import tools.refinery.store.tuple.Tuple;
23
24class BoundScopePropagator implements BoundPropagator {
25 private final Model model;
26 private final ModelQueryAdapter queryEngine;
27 private final Interpretation<CardinalityInterval> countInterpretation;
28 private final MPSolver solver;
29 private final MPObjective objective;
30 private final MutableIntObjectMap<MPVariable> variables = IntObjectMaps.mutable.empty();
31 private final MutableIntSet activeVariables = IntSets.mutable.empty();
32 private final TypeScopePropagator[] propagators;
33 private boolean changed = true;
34
35 public BoundScopePropagator(Model model, ScopePropagator storeAdapter) {
36 this.model = model;
37 queryEngine = model.getAdapter(ModelQueryAdapter.class);
38 countInterpretation = model.getInterpretation(storeAdapter.getCountSymbol());
39 solver = MPSolver.createSolver("GLOP");
40 solver.suppressOutput();
41 objective = solver.objective();
42 initializeVariables();
43 countInterpretation.addListener(this::countChanged, true);
44 var propagatorFactories = storeAdapter.getTypeScopePropagatorFactories();
45 propagators = new TypeScopePropagator[propagatorFactories.size()];
46 for (int i = 0; i < propagators.length; i++) {
47 model.checkCancelled();
48 propagators[i] = propagatorFactories.get(i).createPropagator(this);
49 }
50 }
51
52 ModelQueryAdapter getQueryEngine() {
53 return queryEngine;
54 }
55
56 private void initializeVariables() {
57 var cursor = countInterpretation.getAll();
58 while (cursor.move()) {
59 var interval = cursor.getValue();
60 if (!interval.equals(CardinalityIntervals.ONE)) {
61 int nodeId = cursor.getKey().get(0);
62 createVariable(nodeId, interval);
63 activeVariables.add(nodeId);
64 }
65 }
66 }
67
68 private MPVariable createVariable(int nodeId, CardinalityInterval interval) {
69 double lowerBound = interval.lowerBound();
70 double upperBound = getUpperBound(interval);
71 var variable = solver.makeNumVar(lowerBound, upperBound, "x" + nodeId);
72 variables.put(nodeId, variable);
73 return variable;
74 }
75
76 private void countChanged(Tuple key, CardinalityInterval fromValue, CardinalityInterval toValue,
77 boolean ignoredRestoring) {
78 int nodeId = key.get(0);
79 if ((toValue == null || toValue.equals(CardinalityIntervals.ONE))) {
80 if (fromValue != null && !fromValue.equals(CardinalityIntervals.ONE)) {
81 removeActiveVariable(toValue, nodeId);
82 }
83 return;
84 }
85 if (fromValue == null || fromValue.equals(CardinalityIntervals.ONE)) {
86 activeVariables.add(nodeId);
87 }
88 var variable = variables.get(nodeId);
89 if (variable == null) {
90 createVariable(nodeId, toValue);
91 markAsChanged();
92 return;
93 }
94 double lowerBound = toValue.lowerBound();
95 double upperBound = getUpperBound(toValue);
96 if (variable.lb() != lowerBound) {
97 variable.setLb(lowerBound);
98 markAsChanged();
99 }
100 if (variable.ub() != upperBound) {
101 variable.setUb(upperBound);
102 markAsChanged();
103 }
104 }
105
106 private void removeActiveVariable(CardinalityInterval toValue, int nodeId) {
107 var variable = variables.get(nodeId);
108 if (variable == null || !activeVariables.remove(nodeId)) {
109 throw new AssertionError("Variable not active: " + nodeId);
110 }
111 if (toValue == null) {
112 variable.setBounds(0, 0);
113 } else {
114 // Until queries are flushed and the constraints can be properly updated,
115 // the variable corresponding to the (previous) multi-object has to stand in for a single object.
116 variable.setBounds(1, 1);
117 }
118 markAsChanged();
119 }
120
121 MPConstraint makeConstraint() {
122 return solver.makeConstraint();
123 }
124
125 MPVariable getVariable(int nodeId) {
126 var variable = variables.get(nodeId);
127 if (variable != null) {
128 return variable;
129 }
130 var interval = countInterpretation.get(Tuple.of(nodeId));
131 if (interval == null || interval.equals(CardinalityIntervals.ONE)) {
132 interval = CardinalityIntervals.NONE;
133 } else {
134 activeVariables.add(nodeId);
135 markAsChanged();
136 }
137 return createVariable(nodeId, interval);
138 }
139
140 void markAsChanged() {
141 changed = true;
142 }
143
144 @Override
145 public PropagationResult propagateOne() {
146 queryEngine.flushChanges();
147 if (!changed) {
148 return PropagationResult.UNCHANGED;
149 }
150 changed = false;
151 for (var propagator : propagators) {
152 model.checkCancelled();
153 if (!propagator.updateBounds()) {
154 // Avoid logging GLOP error to console by checking for inconsistent constraints in advance.
155 return PropagationResult.REJECTED;
156 }
157 }
158 var result = PropagationResult.UNCHANGED;
159 if (activeVariables.isEmpty()) {
160 return checkEmptiness();
161 }
162 var iterator = activeVariables.intIterator();
163 while (iterator.hasNext()) {
164 int nodeId = iterator.next();
165 var variable = variables.get(nodeId);
166 if (variable == null) {
167 throw new AssertionError("Missing active variable: " + nodeId);
168 }
169 result = result.andThen(propagateNode(nodeId, variable));
170 if (result.isRejected()) {
171 return result;
172 }
173 }
174 return result;
175 }
176
177 private PropagationResult checkEmptiness() {
178 model.checkCancelled();
179 var emptinessCheckingResult = solver.solve();
180 return switch (emptinessCheckingResult) {
181 case OPTIMAL, UNBOUNDED -> PropagationResult.UNCHANGED;
182 case INFEASIBLE -> PropagationResult.REJECTED;
183 default -> throw new IllegalStateException("Failed to check for consistency: " + emptinessCheckingResult);
184 };
185 }
186
187 private PropagationResult propagateNode(int nodeId, MPVariable variable) {
188 objective.setCoefficient(variable, 1);
189 try {
190 model.checkCancelled();
191 objective.setMinimization();
192 var minimizationResult = solver.solve();
193 int lowerBound;
194 switch (minimizationResult) {
195 case OPTIMAL -> lowerBound = RoundingUtil.roundUp(objective.value());
196 case UNBOUNDED -> lowerBound = 0;
197 case INFEASIBLE -> {
198 return PropagationResult.REJECTED;
199 }
200 default -> throw new IllegalStateException("Failed to solve for minimum of %s: %s"
201 .formatted(variable, minimizationResult));
202 }
203
204 model.checkCancelled();
205 objective.setMaximization();
206 var maximizationResult = solver.solve();
207 UpperCardinality upperBound;
208 switch (maximizationResult) {
209 case OPTIMAL -> upperBound = UpperCardinalities.atMost(RoundingUtil.roundDown(objective.value()));
210 // Problem was feasible when minimizing, the only possible source of {@code UNBOUNDED_OR_INFEASIBLE} is
211 // an unbounded maximization problem. See https://github.com/google/or-tools/issues/3319
212 case UNBOUNDED, INFEASIBLE -> upperBound = UpperCardinalities.UNBOUNDED;
213 default -> throw new IllegalStateException("Failed to solve for maximum of %s: %s"
214 .formatted(variable, minimizationResult));
215 }
216
217 var newInterval = CardinalityIntervals.between(lowerBound, upperBound);
218 var oldInterval = countInterpretation.put(Tuple.of(nodeId), newInterval);
219 if (newInterval.lowerBound() < oldInterval.lowerBound() ||
220 newInterval.upperBound().compareTo(oldInterval.upperBound()) > 0) {
221 throw new IllegalArgumentException("Failed to refine multiplicity %s of node %d to %s"
222 .formatted(oldInterval, nodeId, newInterval));
223 }
224 return newInterval.equals(oldInterval) ? PropagationResult.UNCHANGED : PropagationResult.PROPAGATED;
225 } finally {
226 objective.setCoefficient(variable, 0);
227 }
228 }
229
230 private static double getUpperBound(CardinalityInterval interval) {
231 var upperBound = interval.upperBound();
232 if (upperBound instanceof FiniteUpperCardinality finiteUpperCardinality) {
233 return finiteUpperCardinality.finiteUpperBound();
234 } else if (upperBound instanceof UnboundedUpperCardinality) {
235 return Double.POSITIVE_INFINITY;
236 } else {
237 throw new IllegalArgumentException("Unknown upper bound: " + upperBound);
238 }
239 }
240}