View Javadoc

1   /*******************************************************************************
2    * SAT4J: a SATisfiability library for Java Copyright (C) 2004, 2012 Artois University and CNRS
3    *
4    * All rights reserved. This program and the accompanying materials
5    * are made available under the terms of the Eclipse Public License v1.0
6    * which accompanies this distribution, and is available at
7    *  http://www.eclipse.org/legal/epl-v10.html
8    *
9    * Alternatively, the contents of this file may be used under the terms of
10   * either the GNU Lesser General Public License Version 2.1 or later (the
11   * "LGPL"), in which case the provisions of the LGPL are applicable instead
12   * of those above. If you wish to allow use of your version of this file only
13   * under the terms of the LGPL, and not to allow others to use your version of
14   * this file under the terms of the EPL, indicate your decision by deleting
15   * the provisions above and replace them with the notice and other provisions
16   * required by the LGPL. If you do not delete the provisions above, a recipient
17   * may use your version of this file under the terms of the EPL or the LGPL.
18   *
19   * Based on the original MiniSat specification from:
20   *
21   * An extensible SAT solver. Niklas Een and Niklas Sorensson. Proceedings of the
22   * Sixth International Conference on Theory and Applications of Satisfiability
23   * Testing, LNCS 2919, pp 502-518, 2003.
24   *
25   * See www.minisat.se for the original solver in C++.
26   *
27   * Contributors:
28   *   CRIL - initial API and implementation
29   *******************************************************************************/
30  package org.sat4j.pb;
31  
32  import static org.junit.Assert.assertEquals;
33  import static org.junit.Assert.assertFalse;
34  import static org.junit.Assert.assertTrue;
35  import static org.sat4j.pb.tools.WeightedObject.newWO;
36  
37  import java.util.List;
38  import java.util.Set;
39  
40  import org.junit.Before;
41  import org.junit.Test;
42  import org.sat4j.pb.tools.DependencyHelper;
43  import org.sat4j.specs.ContradictionException;
44  import org.sat4j.specs.TimeoutException;
45  
46  public class XplainTests {
47      private static final int EXTRA_IMPLICATIONS_PER_LEVEL = 10;
48      private DependencyHelper<String, String> helper;
49  
50      @Before
51      public void setUp() {
52          // TODO: We would like to use SolverFactory.newEclipseP2();
53          // currently this throws a class cast exception in the DependencyHelper
54          // DLB: FIXED
55          this.helper = new DependencyHelper<String, String>(
56                  SolverFactory.newEclipseP2());
57      }
58  
59      @Test(timeout = 10000)
60      public void testRequiredSoftwareDependsOnOlderVersion()
61              throws ContradictionException, TimeoutException {
62          this.helper.setTrue("profile", "profile must exist");
63          this.helper.implication("profile").implies("a_1").named("profile->a_1");
64          addExtraImplications("profile");
65          this.helper.implication("a_1").implies("b_1").named("a_1->b_1");
66          addExtraImplications("a_1");
67          this.helper.implication("b_1").implies("c_[2,3)").named("b_1->c_[2,3)");
68          addExtraImplications("b_1");
69          this.helper.implication("c_[2,3)").implies("p(c_[2,3))")
70                  .named("c_[2,3) does not exist");
71          this.helper.setFalse("p(c_[2,3))", "placeholder(c_[2,3))");
72  
73          Explanation<String> explanation = new Explanation<String>();
74          explanation.newFalseRoot("profile must exist").newChild("profile->a_1")
75                  .newChild("a_1->b_1").newChild("b_1->c_[2,3)")
76                  .newChild("c_[2,3) does not exist")
77                  .newChild("placeholder(c_[2,3))");
78          checkExplanationForMissingRequirement(explanation);
79      }
80  
81      @Test(timeout = 10000)
82      public void testRequiredSoftwareDependsOnOlderVersionDeepTree()
83              throws ContradictionException, TimeoutException {
84          this.helper.setTrue("profile", "profile must exist");
85          this.helper.implication("profile").implies("a_1").named("profile->a_1");
86          addExtraImplications("profile");
87  
88          Explanation<String> explanation = new Explanation<String>();
89          DepdendenyNode<String> node = explanation.newFalseRoot(
90                  "profile must exist").newChild("profile->a_1");
91  
92          String lastThing = "a_1";
93          for (int i = 0; i < 10; i++) {
94              String newThing = "newThing" + i;
95              String name = lastThing + "->" + newThing;
96              this.helper.implication(lastThing).implies(newThing).named(name);
97              node = node.newChild(name);
98              addExtraImplications(lastThing);
99              lastThing = newThing;
100         }
101         this.helper.implication(lastThing).implies("c_[2,3)")
102                 .named("b_1->c_[2,3)");
103         node = node.newChild("b_1->c_[2,3)");
104         addExtraImplications(lastThing);
105         this.helper.implication("c_[2,3)").implies("p(c_[2,3))")
106                 .named("c_[2,3) does not exist");
107         node = node.newChild("c_[2,3) does not exist");
108         this.helper.setFalse("p(c_[2,3))", "placeholder(c_[2,3))");
109         node = node.newChild("placeholder(c_[2,3))");
110 
111         checkExplanationForMissingRequirement(explanation);
112     }
113 
114     // Currently expected to fail, need weighting rules on a_1, a_2, b_1, and
115     // b_2
116     @Test(timeout = 10000)
117     public void testUseWeightToOrderSolutions() throws ContradictionException,
118             TimeoutException {
119         this.helper.setTrue("profile", "profile must exist");
120         this.helper.implication("profile").implies("a_1").named("profile->a_1");
121         this.helper.implication("profile").implies("a_2").named("profile->a_2");
122         addExtraImplications("profile");
123         this.helper.implication("a_1").implies("b_1").named("a_1->b_1");
124         this.helper.implication("a_1").implies("b_2").named("a_1->b_2");
125         addExtraImplications("a_1");
126         this.helper.implication("a_2").implies("b_1").named("a_2->b_1");
127         this.helper.implication("a_2").implies("b_2").named("a_2->b_2");
128         addExtraImplications("a_2");
129         this.helper.implication("b_1").implies("c_[2,3)").named("b_1->c_[2,3)");
130         addExtraImplications("b_1");
131         this.helper.implication("b_2").implies("c_[2,3)").named("b_2->c_[2,3)");
132         addExtraImplications("b_2");
133 
134         // Need some way to weight a_2 > a_1 & b_2 > b_1
135         this.helper.setObjectiveFunction(newWO("a_1", 2), newWO("a_2", 1),
136                 newWO("b_1", 8), newWO("b_2", 4));
137         this.helper.implication("c_[2,3)").implies("p(c_[2,3))")
138                 .named("c_[2,3) does not exist");
139         this.helper.setFalse("p(c_[2,3))", "placeholder(c_[2,3))");
140 
141         Explanation<String> explanation = new Explanation<String>();
142         explanation.newFalseRoot("profile must exist").newChild("profile->a_2")
143                 .newChild("a_2->b_2").newChild("b_2->c_[2,3)")
144                 .newChild("c_[2,3) does not exist")
145                 .newChild("placeholder(c_[2,3))");
146         checkExplanationForMissingRequirement(explanation);
147     }
148 
149     @Test(timeout = 10000)
150     public void testUseNumberOfMissingVariablesToOrderExplanations()
151             throws ContradictionException, TimeoutException {
152         this.helper.setTrue("profile", "profile must exist");
153         this.helper.implication("profile").implies("a_1").named("profile->a_1");
154         this.helper.implication("profile").implies("a_2").named("profile->a_2");
155         addExtraImplications("profile");
156         this.helper.implication("a_1").implies("b_1").named("a_1->b_1");
157         addExtraImplications("a_1");
158         this.helper.implication("a_2").implies("b_1").named("a_2->b_1");
159         this.helper.implication("a_2").implies("d").named("a_2->d");
160         addExtraImplications("a_2");
161         this.helper.implication("b_1").implies("c_[2,3)").named("b_1->c_[2,3)");
162         addExtraImplications("b_1");
163 
164         // Need some way to weight a_2 > a_1 & b_2 > b_1
165         this.helper.setObjectiveFunction(newWO("a_1", 2), newWO("a_2", 1),
166                 newWO("b_1", 8), newWO("b_2", 4));
167         this.helper.implication("c_[2,3)").implies("p(c_[2,3))")
168                 .named("c_[2,3) does not exist");
169         this.helper.setFalse("p(c_[2,3))", "placeholder(c_[2,3))");
170         this.helper.implication("d").implies("p(d)").named("d does not exist");
171         this.helper.setFalse("p(d)", "placeholder(d)");
172 
173         Explanation<String> explanation = new Explanation<String>();
174         explanation.newFalseRoot("profile must exist").newChild("profile->a_1")
175                 .newChild("a_1->b_1").newChild("b_1->c_[2,3)")
176                 .newChild("c_[2,3) does not exist")
177                 .newChild("placeholder(c_[2,3))");
178         checkExplanationForMissingRequirement(explanation);
179     }
180 
181     // Expected to fail, need some way to weight a_2 > a_1 & b_2 > b_1
182     @Test(timeout = 10000)
183     public void testUseNumberOfMissingVariablesAndWeightToOrderExplanations()
184             throws ContradictionException, TimeoutException {
185         this.helper.setTrue("profile", "profile must exist");
186         this.helper.implication("profile").implies("a_1").named("profile->a_1");
187         this.helper.implication("profile").implies("a_2").named("profile->a_2");
188         addExtraImplications("profile");
189         this.helper.implication("a_1").implies("b_1").named("a_1->b_1");
190         this.helper.implication("a_1").implies("b_2").named("a_1->b_2");
191         addExtraImplications("a_1");
192         this.helper.implication("a_2").implies("b_1").named("a_2->b_1");
193         this.helper.implication("a_2").implies("b_2").named("a_2->b_2");
194         this.helper.implication("a_2").implies("d").named("a_2->d");
195         addExtraImplications("a_2");
196         this.helper.implication("b_1").implies("c_[2,3)").named("b_1->c_[2,3)");
197         addExtraImplications("b_1");
198         this.helper.implication("b_2").implies("c_[2,3)").named("b_2->c_[2,3)");
199         addExtraImplications("b_2");
200 
201         // Need some way to weight a_2 > a_1 & b_2 > b_1
202         this.helper.setObjectiveFunction(newWO("a_1", 2), newWO("a_2", 1),
203                 newWO("b_1", 8), newWO("b_2", 4));
204         this.helper.implication("c_[2,3)").implies("p(c_[2,3))")
205                 .named("c_[2,3) does not exist");
206         this.helper.setFalse("p(c_[2,3))", "placeholder(c_[2,3))");
207         this.helper.implication("d").implies("p(d)").named("d does not exist");
208         this.helper.setFalse("p(d)", "placeholder(d)");
209 
210         Explanation<String> explanation = new Explanation<String>();
211         explanation.newFalseRoot("profile must exist").newChild("profile->a_1")
212                 .newChild("a_1->b_2").newChild("b_2->c_[2,3)")
213                 .newChild("c_[2,3) does not exist")
214                 .newChild("placeholder(c_[2,3))");
215         checkExplanationForMissingRequirement(explanation);
216     }
217 
218     @Test
219     public void testConflictingRequirements() throws ContradictionException,
220             TimeoutException {
221         this.helper.setTrue("profile", "profile must exist");
222         this.helper.implication("profile").implies("a_1").named("profile->a_1");
223         this.helper.implication("profile").implies("b_1").named("profile->b_1");
224         addExtraImplications("profile");
225         this.helper.implication("a_1").implies("c_1").named("a_1->c_1");
226         addExtraImplications("a_1");
227         this.helper.implication("b_1").implies("d_1").named("b_1->d_1");
228         addExtraImplications("b_1");
229         this.helper.implication("c_1").implies("d_2").named("c_1->d_2");
230         addExtraImplications("c_1");
231         this.helper.setTrue("d_1", "d_1 exists");
232         this.helper.setTrue("d_2", "d_2 exists");
233         this.helper.atMost(1, "d_1", "d_2").named("singleton on d");
234 
235         Explanation<String> explanation = new Explanation<String>();
236         Conflict<String> conflict = explanation.newConflict();
237         conflict.newRoot("profile must exist").newChild("profile->a_1")
238                 .newChild("a_1->c_1").newChild("c_1->d_2");
239         conflict.newRoot("profile must exist").newChild("profile->b_1")
240                 .newChild("b_1->d_1");
241         checkExplanationForConflict(explanation);
242     }
243 
244     private void checkExplanationForConflict(Explanation<String> explanation)
245             throws TimeoutException {
246         assertFalse(this.helper.hasASolution());
247 
248         // TODO: would like all conflicting elements
249         // This throws an ArrayIndexOutOfBounds
250         // DLB: this is a real bug. I still need to work on that issue :(
251         // String conflictingElement = helper.getConflictingElement();
252 
253         List<Conflict<String>> conflicts = explanation.getConflicts();
254 
255         // TODO: why throws an NPE
256         // DLB: fixed.
257         Set<String> cause = this.helper.why();
258 
259         // TODO: Change this logic to actually use the tree of dependencies
260         for (Conflict<String> conflict : conflicts) {
261             List<DepdendenyNode<String>> roots = conflict.getRoots();
262             for (DepdendenyNode<String> node : roots) {
263                 while (node != null) {
264                     assertTrue("Could not find " + node.getName(),
265                             cause.remove(node.getName()));
266                     node = node.getOnlyChild();
267                 }
268             }
269         }
270         assertTrue(cause.isEmpty());
271     }
272 
273     private void checkExplanationForMissingRequirement(
274             Explanation<String> explanation) throws TimeoutException {
275         assertFalse(this.helper.hasASolution());
276 
277         Set<String> cause = this.helper.why();
278         List<DepdendenyNode<String>> roots = explanation.getRoots();
279         assertEquals(1, roots.size());
280 
281         // TODO: Fix this logic to actually investigate the tree returned
282         DepdendenyNode<String> root = roots.get(0);
283         assertFalse(root.hasBranches());
284         assertEquals(root.getMaxDepth(), cause.size());
285 
286         DepdendenyNode<String> node = root;
287         while (node != null) {
288             assertTrue("Could not find " + node.getName() + " in " + cause,
289                     cause.contains(node.getName()));
290             node = node.getOnlyChild();
291         }
292     }
293 
294     /**
295      * Creates extra rules for the given dependent. This allows us to prove that
296      * the explanation doesn't contain unnecessary rules.
297      * 
298      * @param variable
299      *            the variable that will have extra implications added to it
300      * @throws ContradictionException
301      */
302     private void addExtraImplications(String variable)
303             throws ContradictionException {
304         for (int i = 0; i < EXTRA_IMPLICATIONS_PER_LEVEL; i++) {
305             String newVariable = variable + "Extra" + i;
306             this.helper.setTrue(newVariable, newVariable + " exists");
307             this.helper.implication(variable).implies(newVariable)
308                     .named(variable + "->" + newVariable);
309         }
310     }
311 }