switching to high quality piper tts and added label translations
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
from .boolalg import (to_cnf, to_dnf, to_nnf, And, Or, Not, Xor, Nand, Nor, Implies,
|
||||
Equivalent, ITE, POSform, SOPform, simplify_logic, bool_map, true, false,
|
||||
gateinputcount)
|
||||
from .inference import satisfiable
|
||||
|
||||
__all__ = [
|
||||
'to_cnf', 'to_dnf', 'to_nnf', 'And', 'Or', 'Not', 'Xor', 'Nand', 'Nor',
|
||||
'Implies', 'Equivalent', 'ITE', 'POSform', 'SOPform', 'simplify_logic',
|
||||
'bool_map', 'true', 'false', 'gateinputcount',
|
||||
|
||||
'satisfiable',
|
||||
]
|
||||
@@ -0,0 +1,308 @@
|
||||
"""Implementation of DPLL algorithm
|
||||
|
||||
Further improvements: eliminate calls to pl_true, implement branching rules,
|
||||
efficient unit propagation.
|
||||
|
||||
References:
|
||||
- https://en.wikipedia.org/wiki/DPLL_algorithm
|
||||
- https://www.researchgate.net/publication/242384772_Implementations_of_the_DPLL_Algorithm
|
||||
"""
|
||||
|
||||
from sympy.core.sorting import default_sort_key
|
||||
from sympy.logic.boolalg import Or, Not, conjuncts, disjuncts, to_cnf, \
|
||||
to_int_repr, _find_predicates
|
||||
from sympy.assumptions.cnf import CNF
|
||||
from sympy.logic.inference import pl_true, literal_symbol
|
||||
|
||||
|
||||
def dpll_satisfiable(expr):
|
||||
"""
|
||||
Check satisfiability of a propositional sentence.
|
||||
It returns a model rather than True when it succeeds
|
||||
|
||||
>>> from sympy.abc import A, B
|
||||
>>> from sympy.logic.algorithms.dpll import dpll_satisfiable
|
||||
>>> dpll_satisfiable(A & ~B)
|
||||
{A: True, B: False}
|
||||
>>> dpll_satisfiable(A & ~A)
|
||||
False
|
||||
|
||||
"""
|
||||
if not isinstance(expr, CNF):
|
||||
clauses = conjuncts(to_cnf(expr))
|
||||
else:
|
||||
clauses = expr.clauses
|
||||
if False in clauses:
|
||||
return False
|
||||
symbols = sorted(_find_predicates(expr), key=default_sort_key)
|
||||
symbols_int_repr = set(range(1, len(symbols) + 1))
|
||||
clauses_int_repr = to_int_repr(clauses, symbols)
|
||||
result = dpll_int_repr(clauses_int_repr, symbols_int_repr, {})
|
||||
if not result:
|
||||
return result
|
||||
output = {}
|
||||
for key in result:
|
||||
output.update({symbols[key - 1]: result[key]})
|
||||
return output
|
||||
|
||||
|
||||
def dpll(clauses, symbols, model):
|
||||
"""
|
||||
Compute satisfiability in a partial model.
|
||||
Clauses is an array of conjuncts.
|
||||
|
||||
>>> from sympy.abc import A, B, D
|
||||
>>> from sympy.logic.algorithms.dpll import dpll
|
||||
>>> dpll([A, B, D], [A, B], {D: False})
|
||||
False
|
||||
|
||||
"""
|
||||
# compute DP kernel
|
||||
P, value = find_unit_clause(clauses, model)
|
||||
while P:
|
||||
model.update({P: value})
|
||||
symbols.remove(P)
|
||||
if not value:
|
||||
P = ~P
|
||||
clauses = unit_propagate(clauses, P)
|
||||
P, value = find_unit_clause(clauses, model)
|
||||
P, value = find_pure_symbol(symbols, clauses)
|
||||
while P:
|
||||
model.update({P: value})
|
||||
symbols.remove(P)
|
||||
if not value:
|
||||
P = ~P
|
||||
clauses = unit_propagate(clauses, P)
|
||||
P, value = find_pure_symbol(symbols, clauses)
|
||||
# end DP kernel
|
||||
unknown_clauses = []
|
||||
for c in clauses:
|
||||
val = pl_true(c, model)
|
||||
if val is False:
|
||||
return False
|
||||
if val is not True:
|
||||
unknown_clauses.append(c)
|
||||
if not unknown_clauses:
|
||||
return model
|
||||
if not clauses:
|
||||
return model
|
||||
P = symbols.pop()
|
||||
model_copy = model.copy()
|
||||
model.update({P: True})
|
||||
model_copy.update({P: False})
|
||||
symbols_copy = symbols[:]
|
||||
return (dpll(unit_propagate(unknown_clauses, P), symbols, model) or
|
||||
dpll(unit_propagate(unknown_clauses, Not(P)), symbols_copy, model_copy))
|
||||
|
||||
|
||||
def dpll_int_repr(clauses, symbols, model):
|
||||
"""
|
||||
Compute satisfiability in a partial model.
|
||||
Arguments are expected to be in integer representation
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll import dpll_int_repr
|
||||
>>> dpll_int_repr([{1}, {2}, {3}], {1, 2}, {3: False})
|
||||
False
|
||||
|
||||
"""
|
||||
# compute DP kernel
|
||||
P, value = find_unit_clause_int_repr(clauses, model)
|
||||
while P:
|
||||
model.update({P: value})
|
||||
symbols.remove(P)
|
||||
if not value:
|
||||
P = -P
|
||||
clauses = unit_propagate_int_repr(clauses, P)
|
||||
P, value = find_unit_clause_int_repr(clauses, model)
|
||||
P, value = find_pure_symbol_int_repr(symbols, clauses)
|
||||
while P:
|
||||
model.update({P: value})
|
||||
symbols.remove(P)
|
||||
if not value:
|
||||
P = -P
|
||||
clauses = unit_propagate_int_repr(clauses, P)
|
||||
P, value = find_pure_symbol_int_repr(symbols, clauses)
|
||||
# end DP kernel
|
||||
unknown_clauses = []
|
||||
for c in clauses:
|
||||
val = pl_true_int_repr(c, model)
|
||||
if val is False:
|
||||
return False
|
||||
if val is not True:
|
||||
unknown_clauses.append(c)
|
||||
if not unknown_clauses:
|
||||
return model
|
||||
P = symbols.pop()
|
||||
model_copy = model.copy()
|
||||
model.update({P: True})
|
||||
model_copy.update({P: False})
|
||||
symbols_copy = symbols.copy()
|
||||
return (dpll_int_repr(unit_propagate_int_repr(unknown_clauses, P), symbols, model) or
|
||||
dpll_int_repr(unit_propagate_int_repr(unknown_clauses, -P), symbols_copy, model_copy))
|
||||
|
||||
### helper methods for DPLL
|
||||
|
||||
|
||||
def pl_true_int_repr(clause, model={}):
|
||||
"""
|
||||
Lightweight version of pl_true.
|
||||
Argument clause represents the set of args of an Or clause. This is used
|
||||
inside dpll_int_repr, it is not meant to be used directly.
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll import pl_true_int_repr
|
||||
>>> pl_true_int_repr({1, 2}, {1: False})
|
||||
>>> pl_true_int_repr({1, 2}, {1: False, 2: False})
|
||||
False
|
||||
|
||||
"""
|
||||
result = False
|
||||
for lit in clause:
|
||||
if lit < 0:
|
||||
p = model.get(-lit)
|
||||
if p is not None:
|
||||
p = not p
|
||||
else:
|
||||
p = model.get(lit)
|
||||
if p is True:
|
||||
return True
|
||||
elif p is None:
|
||||
result = None
|
||||
return result
|
||||
|
||||
|
||||
def unit_propagate(clauses, symbol):
|
||||
"""
|
||||
Returns an equivalent set of clauses
|
||||
If a set of clauses contains the unit clause l, the other clauses are
|
||||
simplified by the application of the two following rules:
|
||||
|
||||
1. every clause containing l is removed
|
||||
2. in every clause that contains ~l this literal is deleted
|
||||
|
||||
Arguments are expected to be in CNF.
|
||||
|
||||
>>> from sympy.abc import A, B, D
|
||||
>>> from sympy.logic.algorithms.dpll import unit_propagate
|
||||
>>> unit_propagate([A | B, D | ~B, B], B)
|
||||
[D, B]
|
||||
|
||||
"""
|
||||
output = []
|
||||
for c in clauses:
|
||||
if c.func != Or:
|
||||
output.append(c)
|
||||
continue
|
||||
for arg in c.args:
|
||||
if arg == ~symbol:
|
||||
output.append(Or(*[x for x in c.args if x != ~symbol]))
|
||||
break
|
||||
if arg == symbol:
|
||||
break
|
||||
else:
|
||||
output.append(c)
|
||||
return output
|
||||
|
||||
|
||||
def unit_propagate_int_repr(clauses, s):
|
||||
"""
|
||||
Same as unit_propagate, but arguments are expected to be in integer
|
||||
representation
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll import unit_propagate_int_repr
|
||||
>>> unit_propagate_int_repr([{1, 2}, {3, -2}, {2}], 2)
|
||||
[{3}]
|
||||
|
||||
"""
|
||||
negated = {-s}
|
||||
return [clause - negated for clause in clauses if s not in clause]
|
||||
|
||||
|
||||
def find_pure_symbol(symbols, unknown_clauses):
|
||||
"""
|
||||
Find a symbol and its value if it appears only as a positive literal
|
||||
(or only as a negative) in clauses.
|
||||
|
||||
>>> from sympy.abc import A, B, D
|
||||
>>> from sympy.logic.algorithms.dpll import find_pure_symbol
|
||||
>>> find_pure_symbol([A, B, D], [A|~B,~B|~D,D|A])
|
||||
(A, True)
|
||||
|
||||
"""
|
||||
for sym in symbols:
|
||||
found_pos, found_neg = False, False
|
||||
for c in unknown_clauses:
|
||||
if not found_pos and sym in disjuncts(c):
|
||||
found_pos = True
|
||||
if not found_neg and Not(sym) in disjuncts(c):
|
||||
found_neg = True
|
||||
if found_pos != found_neg:
|
||||
return sym, found_pos
|
||||
return None, None
|
||||
|
||||
|
||||
def find_pure_symbol_int_repr(symbols, unknown_clauses):
|
||||
"""
|
||||
Same as find_pure_symbol, but arguments are expected
|
||||
to be in integer representation
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll import find_pure_symbol_int_repr
|
||||
>>> find_pure_symbol_int_repr({1,2,3},
|
||||
... [{1, -2}, {-2, -3}, {3, 1}])
|
||||
(1, True)
|
||||
|
||||
"""
|
||||
all_symbols = set().union(*unknown_clauses)
|
||||
found_pos = all_symbols.intersection(symbols)
|
||||
found_neg = all_symbols.intersection([-s for s in symbols])
|
||||
for p in found_pos:
|
||||
if -p not in found_neg:
|
||||
return p, True
|
||||
for p in found_neg:
|
||||
if -p not in found_pos:
|
||||
return -p, False
|
||||
return None, None
|
||||
|
||||
|
||||
def find_unit_clause(clauses, model):
|
||||
"""
|
||||
A unit clause has only 1 variable that is not bound in the model.
|
||||
|
||||
>>> from sympy.abc import A, B, D
|
||||
>>> from sympy.logic.algorithms.dpll import find_unit_clause
|
||||
>>> find_unit_clause([A | B | D, B | ~D, A | ~B], {A:True})
|
||||
(B, False)
|
||||
|
||||
"""
|
||||
for clause in clauses:
|
||||
num_not_in_model = 0
|
||||
for literal in disjuncts(clause):
|
||||
sym = literal_symbol(literal)
|
||||
if sym not in model:
|
||||
num_not_in_model += 1
|
||||
P, value = sym, not isinstance(literal, Not)
|
||||
if num_not_in_model == 1:
|
||||
return P, value
|
||||
return None, None
|
||||
|
||||
|
||||
def find_unit_clause_int_repr(clauses, model):
|
||||
"""
|
||||
Same as find_unit_clause, but arguments are expected to be in
|
||||
integer representation.
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll import find_unit_clause_int_repr
|
||||
>>> find_unit_clause_int_repr([{1, 2, 3},
|
||||
... {2, -3}, {1, -2}], {1: True})
|
||||
(2, False)
|
||||
|
||||
"""
|
||||
bound = set(model) | {-sym for sym in model}
|
||||
for clause in clauses:
|
||||
unbound = clause - bound
|
||||
if len(unbound) == 1:
|
||||
p = unbound.pop()
|
||||
if p < 0:
|
||||
return -p, False
|
||||
else:
|
||||
return p, True
|
||||
return None, None
|
||||
@@ -0,0 +1,688 @@
|
||||
"""Implementation of DPLL algorithm
|
||||
|
||||
Features:
|
||||
- Clause learning
|
||||
- Watch literal scheme
|
||||
- VSIDS heuristic
|
||||
|
||||
References:
|
||||
- https://en.wikipedia.org/wiki/DPLL_algorithm
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from heapq import heappush, heappop
|
||||
|
||||
from sympy.core.sorting import ordered
|
||||
from sympy.assumptions.cnf import EncodedCNF
|
||||
|
||||
from sympy.logic.algorithms.lra_theory import LRASolver
|
||||
|
||||
|
||||
def dpll_satisfiable(expr, all_models=False, use_lra_theory=False):
|
||||
"""
|
||||
Check satisfiability of a propositional sentence.
|
||||
It returns a model rather than True when it succeeds.
|
||||
Returns a generator of all models if all_models is True.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.abc import A, B
|
||||
>>> from sympy.logic.algorithms.dpll2 import dpll_satisfiable
|
||||
>>> dpll_satisfiable(A & ~B)
|
||||
{A: True, B: False}
|
||||
>>> dpll_satisfiable(A & ~A)
|
||||
False
|
||||
|
||||
"""
|
||||
if not isinstance(expr, EncodedCNF):
|
||||
exprs = EncodedCNF()
|
||||
exprs.add_prop(expr)
|
||||
expr = exprs
|
||||
|
||||
# Return UNSAT when False (encoded as 0) is present in the CNF
|
||||
if {0} in expr.data:
|
||||
if all_models:
|
||||
return (f for f in [False])
|
||||
return False
|
||||
|
||||
if use_lra_theory:
|
||||
lra, immediate_conflicts = LRASolver.from_encoded_cnf(expr)
|
||||
else:
|
||||
lra = None
|
||||
immediate_conflicts = []
|
||||
solver = SATSolver(expr.data + immediate_conflicts, expr.variables, set(), expr.symbols, lra_theory=lra)
|
||||
models = solver._find_model()
|
||||
|
||||
if all_models:
|
||||
return _all_models(models)
|
||||
|
||||
try:
|
||||
return next(models)
|
||||
except StopIteration:
|
||||
return False
|
||||
|
||||
# Uncomment to confirm the solution is valid (hitting set for the clauses)
|
||||
#else:
|
||||
#for cls in clauses_int_repr:
|
||||
#assert solver.var_settings.intersection(cls)
|
||||
|
||||
|
||||
def _all_models(models):
|
||||
satisfiable = False
|
||||
try:
|
||||
while True:
|
||||
yield next(models)
|
||||
satisfiable = True
|
||||
except StopIteration:
|
||||
if not satisfiable:
|
||||
yield False
|
||||
|
||||
|
||||
class SATSolver:
|
||||
"""
|
||||
Class for representing a SAT solver capable of
|
||||
finding a model to a boolean theory in conjunctive
|
||||
normal form.
|
||||
"""
|
||||
|
||||
def __init__(self, clauses, variables, var_settings, symbols=None,
|
||||
heuristic='vsids', clause_learning='none', INTERVAL=500,
|
||||
lra_theory = None):
|
||||
|
||||
self.var_settings = var_settings
|
||||
self.heuristic = heuristic
|
||||
self.is_unsatisfied = False
|
||||
self._unit_prop_queue = []
|
||||
self.update_functions = []
|
||||
self.INTERVAL = INTERVAL
|
||||
|
||||
if symbols is None:
|
||||
self.symbols = list(ordered(variables))
|
||||
else:
|
||||
self.symbols = symbols
|
||||
|
||||
self._initialize_variables(variables)
|
||||
self._initialize_clauses(clauses)
|
||||
|
||||
if 'vsids' == heuristic:
|
||||
self._vsids_init()
|
||||
self.heur_calculate = self._vsids_calculate
|
||||
self.heur_lit_assigned = self._vsids_lit_assigned
|
||||
self.heur_lit_unset = self._vsids_lit_unset
|
||||
self.heur_clause_added = self._vsids_clause_added
|
||||
|
||||
# Note: Uncomment this if/when clause learning is enabled
|
||||
#self.update_functions.append(self._vsids_decay)
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
if 'simple' == clause_learning:
|
||||
self.add_learned_clause = self._simple_add_learned_clause
|
||||
self.compute_conflict = self._simple_compute_conflict
|
||||
self.update_functions.append(self._simple_clean_clauses)
|
||||
elif 'none' == clause_learning:
|
||||
self.add_learned_clause = lambda x: None
|
||||
self.compute_conflict = lambda: None
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
# Create the base level
|
||||
self.levels = [Level(0)]
|
||||
self._current_level.varsettings = var_settings
|
||||
|
||||
# Keep stats
|
||||
self.num_decisions = 0
|
||||
self.num_learned_clauses = 0
|
||||
self.original_num_clauses = len(self.clauses)
|
||||
|
||||
self.lra = lra_theory
|
||||
|
||||
def _initialize_variables(self, variables):
|
||||
"""Set up the variable data structures needed."""
|
||||
self.sentinels = defaultdict(set)
|
||||
self.occurrence_count = defaultdict(int)
|
||||
self.variable_set = [False] * (len(variables) + 1)
|
||||
|
||||
def _initialize_clauses(self, clauses):
|
||||
"""Set up the clause data structures needed.
|
||||
|
||||
For each clause, the following changes are made:
|
||||
- Unit clauses are queued for propagation right away.
|
||||
- Non-unit clauses have their first and last literals set as sentinels.
|
||||
- The number of clauses a literal appears in is computed.
|
||||
"""
|
||||
self.clauses = [list(clause) for clause in clauses]
|
||||
|
||||
for i, clause in enumerate(self.clauses):
|
||||
|
||||
# Handle the unit clauses
|
||||
if 1 == len(clause):
|
||||
self._unit_prop_queue.append(clause[0])
|
||||
continue
|
||||
|
||||
self.sentinels[clause[0]].add(i)
|
||||
self.sentinels[clause[-1]].add(i)
|
||||
|
||||
for lit in clause:
|
||||
self.occurrence_count[lit] += 1
|
||||
|
||||
def _find_model(self):
|
||||
"""
|
||||
Main DPLL loop. Returns a generator of models.
|
||||
|
||||
Variables are chosen successively, and assigned to be either
|
||||
True or False. If a solution is not found with this setting,
|
||||
the opposite is chosen and the search continues. The solver
|
||||
halts when every variable has a setting.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll2 import SATSolver
|
||||
>>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
|
||||
... {3, -2}], {1, 2, 3}, set())
|
||||
>>> list(l._find_model())
|
||||
[{1: True, 2: False, 3: False}, {1: True, 2: True, 3: True}]
|
||||
|
||||
>>> from sympy.abc import A, B, C
|
||||
>>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
|
||||
... {3, -2}], {1, 2, 3}, set(), [A, B, C])
|
||||
>>> list(l._find_model())
|
||||
[{A: True, B: False, C: False}, {A: True, B: True, C: True}]
|
||||
|
||||
"""
|
||||
|
||||
# We use this variable to keep track of if we should flip a
|
||||
# variable setting in successive rounds
|
||||
flip_var = False
|
||||
|
||||
# Check if unit prop says the theory is unsat right off the bat
|
||||
self._simplify()
|
||||
if self.is_unsatisfied:
|
||||
return
|
||||
|
||||
# While the theory still has clauses remaining
|
||||
while True:
|
||||
# Perform cleanup / fixup at regular intervals
|
||||
if self.num_decisions % self.INTERVAL == 0:
|
||||
for func in self.update_functions:
|
||||
func()
|
||||
|
||||
if flip_var:
|
||||
# We have just backtracked and we are trying to opposite literal
|
||||
flip_var = False
|
||||
lit = self._current_level.decision
|
||||
|
||||
else:
|
||||
# Pick a literal to set
|
||||
lit = self.heur_calculate()
|
||||
self.num_decisions += 1
|
||||
|
||||
# Stopping condition for a satisfying theory
|
||||
if 0 == lit:
|
||||
|
||||
# check if assignment satisfies lra theory
|
||||
if self.lra:
|
||||
for enc_var in self.var_settings:
|
||||
res = self.lra.assert_lit(enc_var)
|
||||
if res is not None:
|
||||
break
|
||||
res = self.lra.check()
|
||||
self.lra.reset_bounds()
|
||||
else:
|
||||
res = None
|
||||
if res is None or res[0]:
|
||||
yield {self.symbols[abs(lit) - 1]:
|
||||
lit > 0 for lit in self.var_settings}
|
||||
else:
|
||||
self._simple_add_learned_clause(res[1])
|
||||
|
||||
# backtrack until we unassign one of the literals causing the conflict
|
||||
while not any(-lit in res[1] for lit in self._current_level.var_settings):
|
||||
self._undo()
|
||||
|
||||
while self._current_level.flipped:
|
||||
self._undo()
|
||||
if len(self.levels) == 1:
|
||||
return
|
||||
flip_lit = -self._current_level.decision
|
||||
self._undo()
|
||||
self.levels.append(Level(flip_lit, flipped=True))
|
||||
flip_var = True
|
||||
continue
|
||||
|
||||
# Start the new decision level
|
||||
self.levels.append(Level(lit))
|
||||
|
||||
# Assign the literal, updating the clauses it satisfies
|
||||
self._assign_literal(lit)
|
||||
|
||||
# _simplify the theory
|
||||
self._simplify()
|
||||
|
||||
# Check if we've made the theory unsat
|
||||
if self.is_unsatisfied:
|
||||
|
||||
self.is_unsatisfied = False
|
||||
|
||||
# We unroll all of the decisions until we can flip a literal
|
||||
while self._current_level.flipped:
|
||||
self._undo()
|
||||
|
||||
# If we've unrolled all the way, the theory is unsat
|
||||
if 1 == len(self.levels):
|
||||
return
|
||||
|
||||
# Detect and add a learned clause
|
||||
self.add_learned_clause(self.compute_conflict())
|
||||
|
||||
# Try the opposite setting of the most recent decision
|
||||
flip_lit = -self._current_level.decision
|
||||
self._undo()
|
||||
self.levels.append(Level(flip_lit, flipped=True))
|
||||
flip_var = True
|
||||
|
||||
########################
|
||||
# Helper Methods #
|
||||
########################
|
||||
@property
|
||||
def _current_level(self):
|
||||
"""The current decision level data structure
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll2 import SATSolver
|
||||
>>> l = SATSolver([{1}, {2}], {1, 2}, set())
|
||||
>>> next(l._find_model())
|
||||
{1: True, 2: True}
|
||||
>>> l._current_level.decision
|
||||
0
|
||||
>>> l._current_level.flipped
|
||||
False
|
||||
>>> l._current_level.var_settings
|
||||
{1, 2}
|
||||
|
||||
"""
|
||||
return self.levels[-1]
|
||||
|
||||
def _clause_sat(self, cls):
|
||||
"""Check if a clause is satisfied by the current variable setting.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll2 import SATSolver
|
||||
>>> l = SATSolver([{1}, {-1}], {1}, set())
|
||||
>>> try:
|
||||
... next(l._find_model())
|
||||
... except StopIteration:
|
||||
... pass
|
||||
>>> l._clause_sat(0)
|
||||
False
|
||||
>>> l._clause_sat(1)
|
||||
True
|
||||
|
||||
"""
|
||||
for lit in self.clauses[cls]:
|
||||
if lit in self.var_settings:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _is_sentinel(self, lit, cls):
|
||||
"""Check if a literal is a sentinel of a given clause.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll2 import SATSolver
|
||||
>>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
|
||||
... {3, -2}], {1, 2, 3}, set())
|
||||
>>> next(l._find_model())
|
||||
{1: True, 2: False, 3: False}
|
||||
>>> l._is_sentinel(2, 3)
|
||||
True
|
||||
>>> l._is_sentinel(-3, 1)
|
||||
False
|
||||
|
||||
"""
|
||||
return cls in self.sentinels[lit]
|
||||
|
||||
def _assign_literal(self, lit):
|
||||
"""Make a literal assignment.
|
||||
|
||||
The literal assignment must be recorded as part of the current
|
||||
decision level. Additionally, if the literal is marked as a
|
||||
sentinel of any clause, then a new sentinel must be chosen. If
|
||||
this is not possible, then unit propagation is triggered and
|
||||
another literal is added to the queue to be set in the future.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll2 import SATSolver
|
||||
>>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
|
||||
... {3, -2}], {1, 2, 3}, set())
|
||||
>>> next(l._find_model())
|
||||
{1: True, 2: False, 3: False}
|
||||
>>> l.var_settings
|
||||
{-3, -2, 1}
|
||||
|
||||
>>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
|
||||
... {3, -2}], {1, 2, 3}, set())
|
||||
>>> l._assign_literal(-1)
|
||||
>>> try:
|
||||
... next(l._find_model())
|
||||
... except StopIteration:
|
||||
... pass
|
||||
>>> l.var_settings
|
||||
{-1}
|
||||
|
||||
"""
|
||||
self.var_settings.add(lit)
|
||||
self._current_level.var_settings.add(lit)
|
||||
self.variable_set[abs(lit)] = True
|
||||
self.heur_lit_assigned(lit)
|
||||
|
||||
sentinel_list = list(self.sentinels[-lit])
|
||||
|
||||
for cls in sentinel_list:
|
||||
if not self._clause_sat(cls):
|
||||
other_sentinel = None
|
||||
for newlit in self.clauses[cls]:
|
||||
if newlit != -lit:
|
||||
if self._is_sentinel(newlit, cls):
|
||||
other_sentinel = newlit
|
||||
elif not self.variable_set[abs(newlit)]:
|
||||
self.sentinels[-lit].remove(cls)
|
||||
self.sentinels[newlit].add(cls)
|
||||
other_sentinel = None
|
||||
break
|
||||
|
||||
# Check if no sentinel update exists
|
||||
if other_sentinel:
|
||||
self._unit_prop_queue.append(other_sentinel)
|
||||
|
||||
def _undo(self):
|
||||
"""
|
||||
_undo the changes of the most recent decision level.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll2 import SATSolver
|
||||
>>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
|
||||
... {3, -2}], {1, 2, 3}, set())
|
||||
>>> next(l._find_model())
|
||||
{1: True, 2: False, 3: False}
|
||||
>>> level = l._current_level
|
||||
>>> level.decision, level.var_settings, level.flipped
|
||||
(-3, {-3, -2}, False)
|
||||
>>> l._undo()
|
||||
>>> level = l._current_level
|
||||
>>> level.decision, level.var_settings, level.flipped
|
||||
(0, {1}, False)
|
||||
|
||||
"""
|
||||
# Undo the variable settings
|
||||
for lit in self._current_level.var_settings:
|
||||
self.var_settings.remove(lit)
|
||||
self.heur_lit_unset(lit)
|
||||
self.variable_set[abs(lit)] = False
|
||||
|
||||
# Pop the level off the stack
|
||||
self.levels.pop()
|
||||
|
||||
#########################
|
||||
# Propagation #
|
||||
#########################
|
||||
"""
|
||||
Propagation methods should attempt to soundly simplify the boolean
|
||||
theory, and return True if any simplification occurred and False
|
||||
otherwise.
|
||||
"""
|
||||
def _simplify(self):
|
||||
"""Iterate over the various forms of propagation to simplify the theory.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll2 import SATSolver
|
||||
>>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
|
||||
... {3, -2}], {1, 2, 3}, set())
|
||||
>>> l.variable_set
|
||||
[False, False, False, False]
|
||||
>>> l.sentinels
|
||||
{-3: {0, 2}, -2: {3, 4}, 2: {0, 3}, 3: {2, 4}}
|
||||
|
||||
>>> l._simplify()
|
||||
|
||||
>>> l.variable_set
|
||||
[False, True, False, False]
|
||||
>>> l.sentinels
|
||||
{-3: {0, 2}, -2: {3, 4}, -1: set(), 2: {0, 3},
|
||||
...3: {2, 4}}
|
||||
|
||||
"""
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
changed |= self._unit_prop()
|
||||
changed |= self._pure_literal()
|
||||
|
||||
def _unit_prop(self):
|
||||
"""Perform unit propagation on the current theory."""
|
||||
result = len(self._unit_prop_queue) > 0
|
||||
while self._unit_prop_queue:
|
||||
next_lit = self._unit_prop_queue.pop()
|
||||
if -next_lit in self.var_settings:
|
||||
self.is_unsatisfied = True
|
||||
self._unit_prop_queue = []
|
||||
return False
|
||||
else:
|
||||
self._assign_literal(next_lit)
|
||||
|
||||
return result
|
||||
|
||||
def _pure_literal(self):
|
||||
"""Look for pure literals and assign them when found."""
|
||||
return False
|
||||
|
||||
#########################
|
||||
# Heuristics #
|
||||
#########################
|
||||
def _vsids_init(self):
|
||||
"""Initialize the data structures needed for the VSIDS heuristic."""
|
||||
self.lit_heap = []
|
||||
self.lit_scores = {}
|
||||
|
||||
for var in range(1, len(self.variable_set)):
|
||||
self.lit_scores[var] = float(-self.occurrence_count[var])
|
||||
self.lit_scores[-var] = float(-self.occurrence_count[-var])
|
||||
heappush(self.lit_heap, (self.lit_scores[var], var))
|
||||
heappush(self.lit_heap, (self.lit_scores[-var], -var))
|
||||
|
||||
def _vsids_decay(self):
|
||||
"""Decay the VSIDS scores for every literal.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll2 import SATSolver
|
||||
>>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
|
||||
... {3, -2}], {1, 2, 3}, set())
|
||||
|
||||
>>> l.lit_scores
|
||||
{-3: -2.0, -2: -2.0, -1: 0.0, 1: 0.0, 2: -2.0, 3: -2.0}
|
||||
|
||||
>>> l._vsids_decay()
|
||||
|
||||
>>> l.lit_scores
|
||||
{-3: -1.0, -2: -1.0, -1: 0.0, 1: 0.0, 2: -1.0, 3: -1.0}
|
||||
|
||||
"""
|
||||
# We divide every literal score by 2 for a decay factor
|
||||
# Note: This doesn't change the heap property
|
||||
for lit in self.lit_scores.keys():
|
||||
self.lit_scores[lit] /= 2.0
|
||||
|
||||
def _vsids_calculate(self):
|
||||
"""
|
||||
VSIDS Heuristic Calculation
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll2 import SATSolver
|
||||
>>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
|
||||
... {3, -2}], {1, 2, 3}, set())
|
||||
|
||||
>>> l.lit_heap
|
||||
[(-2.0, -3), (-2.0, 2), (-2.0, -2), (0.0, 1), (-2.0, 3), (0.0, -1)]
|
||||
|
||||
>>> l._vsids_calculate()
|
||||
-3
|
||||
|
||||
>>> l.lit_heap
|
||||
[(-2.0, -2), (-2.0, 2), (0.0, -1), (0.0, 1), (-2.0, 3)]
|
||||
|
||||
"""
|
||||
if len(self.lit_heap) == 0:
|
||||
return 0
|
||||
|
||||
# Clean out the front of the heap as long the variables are set
|
||||
while self.variable_set[abs(self.lit_heap[0][1])]:
|
||||
heappop(self.lit_heap)
|
||||
if len(self.lit_heap) == 0:
|
||||
return 0
|
||||
|
||||
return heappop(self.lit_heap)[1]
|
||||
|
||||
def _vsids_lit_assigned(self, lit):
|
||||
"""Handle the assignment of a literal for the VSIDS heuristic."""
|
||||
pass
|
||||
|
||||
def _vsids_lit_unset(self, lit):
|
||||
"""Handle the unsetting of a literal for the VSIDS heuristic.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll2 import SATSolver
|
||||
>>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
|
||||
... {3, -2}], {1, 2, 3}, set())
|
||||
>>> l.lit_heap
|
||||
[(-2.0, -3), (-2.0, 2), (-2.0, -2), (0.0, 1), (-2.0, 3), (0.0, -1)]
|
||||
|
||||
>>> l._vsids_lit_unset(2)
|
||||
|
||||
>>> l.lit_heap
|
||||
[(-2.0, -3), (-2.0, -2), (-2.0, -2), (-2.0, 2), (-2.0, 3), (0.0, -1),
|
||||
...(-2.0, 2), (0.0, 1)]
|
||||
|
||||
"""
|
||||
var = abs(lit)
|
||||
heappush(self.lit_heap, (self.lit_scores[var], var))
|
||||
heappush(self.lit_heap, (self.lit_scores[-var], -var))
|
||||
|
||||
def _vsids_clause_added(self, cls):
|
||||
"""Handle the addition of a new clause for the VSIDS heuristic.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll2 import SATSolver
|
||||
>>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
|
||||
... {3, -2}], {1, 2, 3}, set())
|
||||
|
||||
>>> l.num_learned_clauses
|
||||
0
|
||||
>>> l.lit_scores
|
||||
{-3: -2.0, -2: -2.0, -1: 0.0, 1: 0.0, 2: -2.0, 3: -2.0}
|
||||
|
||||
>>> l._vsids_clause_added({2, -3})
|
||||
|
||||
>>> l.num_learned_clauses
|
||||
1
|
||||
>>> l.lit_scores
|
||||
{-3: -1.0, -2: -2.0, -1: 0.0, 1: 0.0, 2: -1.0, 3: -2.0}
|
||||
|
||||
"""
|
||||
self.num_learned_clauses += 1
|
||||
for lit in cls:
|
||||
self.lit_scores[lit] += 1
|
||||
|
||||
########################
|
||||
# Clause Learning #
|
||||
########################
|
||||
def _simple_add_learned_clause(self, cls):
|
||||
"""Add a new clause to the theory.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll2 import SATSolver
|
||||
>>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
|
||||
... {3, -2}], {1, 2, 3}, set())
|
||||
|
||||
>>> l.num_learned_clauses
|
||||
0
|
||||
>>> l.clauses
|
||||
[[2, -3], [1], [3, -3], [2, -2], [3, -2]]
|
||||
>>> l.sentinels
|
||||
{-3: {0, 2}, -2: {3, 4}, 2: {0, 3}, 3: {2, 4}}
|
||||
|
||||
>>> l._simple_add_learned_clause([3])
|
||||
|
||||
>>> l.clauses
|
||||
[[2, -3], [1], [3, -3], [2, -2], [3, -2], [3]]
|
||||
>>> l.sentinels
|
||||
{-3: {0, 2}, -2: {3, 4}, 2: {0, 3}, 3: {2, 4, 5}}
|
||||
|
||||
"""
|
||||
cls_num = len(self.clauses)
|
||||
self.clauses.append(cls)
|
||||
|
||||
for lit in cls:
|
||||
self.occurrence_count[lit] += 1
|
||||
|
||||
self.sentinels[cls[0]].add(cls_num)
|
||||
self.sentinels[cls[-1]].add(cls_num)
|
||||
|
||||
self.heur_clause_added(cls)
|
||||
|
||||
def _simple_compute_conflict(self):
|
||||
""" Build a clause representing the fact that at least one decision made
|
||||
so far is wrong.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.algorithms.dpll2 import SATSolver
|
||||
>>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
|
||||
... {3, -2}], {1, 2, 3}, set())
|
||||
>>> next(l._find_model())
|
||||
{1: True, 2: False, 3: False}
|
||||
>>> l._simple_compute_conflict()
|
||||
[3]
|
||||
|
||||
"""
|
||||
return [-(level.decision) for level in self.levels[1:]]
|
||||
|
||||
def _simple_clean_clauses(self):
|
||||
"""Clean up learned clauses."""
|
||||
pass
|
||||
|
||||
|
||||
class Level:
|
||||
"""
|
||||
Represents a single level in the DPLL algorithm, and contains
|
||||
enough information for a sound backtracking procedure.
|
||||
"""
|
||||
|
||||
def __init__(self, decision, flipped=False):
|
||||
self.decision = decision
|
||||
self.var_settings = set()
|
||||
self.flipped = flipped
|
||||
@@ -0,0 +1,912 @@
|
||||
"""Implements "A Fast Linear-Arithmetic Solver for DPLL(T)"
|
||||
|
||||
The LRASolver class defined in this file can be used
|
||||
in conjunction with a SAT solver to check the
|
||||
satisfiability of formulas involving inequalities.
|
||||
|
||||
Here's an example of how that would work:
|
||||
|
||||
Suppose you want to check the satisfiability of
|
||||
the following formula:
|
||||
|
||||
>>> from sympy.core.relational import Eq
|
||||
>>> from sympy.abc import x, y
|
||||
>>> f = ((x > 0) | (x < 0)) & (Eq(x, 0) | Eq(y, 1)) & (~Eq(y, 1) | Eq(1, 2))
|
||||
|
||||
First a preprocessing step should be done on f. During preprocessing,
|
||||
f should be checked for any predicates such as `Q.prime` that can't be
|
||||
handled. Also unequality like `~Eq(y, 1)` should be split.
|
||||
|
||||
I should mention that the paper says to split both equalities and
|
||||
unequality, but this implementation only requires that unequality
|
||||
be split.
|
||||
|
||||
>>> f = ((x > 0) | (x < 0)) & (Eq(x, 0) | Eq(y, 1)) & ((y < 1) | (y > 1) | Eq(1, 2))
|
||||
|
||||
Then an LRASolver instance needs to be initialized with this formula.
|
||||
|
||||
>>> from sympy.assumptions.cnf import CNF, EncodedCNF
|
||||
>>> from sympy.assumptions.ask import Q
|
||||
>>> from sympy.logic.algorithms.lra_theory import LRASolver
|
||||
>>> cnf = CNF.from_prop(f)
|
||||
>>> enc = EncodedCNF()
|
||||
>>> enc.add_from_cnf(cnf)
|
||||
>>> lra, conflicts = LRASolver.from_encoded_cnf(enc)
|
||||
|
||||
Any immediate one-lital conflicts clauses will be detected here.
|
||||
In this example, `~Eq(1, 2)` is one such conflict clause. We'll
|
||||
want to add it to `f` so that the SAT solver is forced to
|
||||
assign Eq(1, 2) to False.
|
||||
|
||||
>>> f = f & ~Eq(1, 2)
|
||||
|
||||
Now that the one-literal conflict clauses have been added
|
||||
and an lra object has been initialized, we can pass `f`
|
||||
to a SAT solver. The SAT solver will give us a satisfying
|
||||
assignment such as:
|
||||
|
||||
(1 = 2): False
|
||||
(y = 1): True
|
||||
(y < 1): True
|
||||
(y > 1): True
|
||||
(x = 0): True
|
||||
(x < 0): True
|
||||
(x > 0): True
|
||||
|
||||
Next you would pass this assignment to the LRASolver
|
||||
which will be able to determine that this particular
|
||||
assignment is satisfiable or not.
|
||||
|
||||
Note that since EncodedCNF is inherently non-deterministic,
|
||||
the int each predicate is encoded as is not consistent. As a
|
||||
result, the code below likely does not reflect the assignment
|
||||
given above.
|
||||
|
||||
>>> lra.assert_lit(-1) #doctest: +SKIP
|
||||
>>> lra.assert_lit(2) #doctest: +SKIP
|
||||
>>> lra.assert_lit(3) #doctest: +SKIP
|
||||
>>> lra.assert_lit(4) #doctest: +SKIP
|
||||
>>> lra.assert_lit(5) #doctest: +SKIP
|
||||
>>> lra.assert_lit(6) #doctest: +SKIP
|
||||
>>> lra.assert_lit(7) #doctest: +SKIP
|
||||
>>> is_sat, conflict_or_assignment = lra.check()
|
||||
|
||||
As the particular assignment suggested is not satisfiable,
|
||||
the LRASolver will return unsat and a conflict clause when
|
||||
given that assignment. The conflict clause will always be
|
||||
minimal, but there can be multiple minimal conflict clauses.
|
||||
One possible conflict clause could be `~(x < 0) | ~(x > 0)`.
|
||||
|
||||
We would then add whatever conflict clause is given to
|
||||
`f` to prevent the SAT solver from coming up with an
|
||||
assignment with the same conflicting literals. In this case,
|
||||
the conflict clause `~(x < 0) | ~(x > 0)` would prevent
|
||||
any assignment where both (x < 0) and (x > 0) were both
|
||||
true.
|
||||
|
||||
The SAT solver would then find another assignment
|
||||
and we would check that assignment with the LRASolver
|
||||
and so on. Eventually either a satisfying assignment
|
||||
that the SAT solver and LRASolver agreed on would be found
|
||||
or enough conflict clauses would be added so that the
|
||||
boolean formula was unsatisfiable.
|
||||
|
||||
|
||||
This implementation is based on [1]_, which includes a
|
||||
detailed explanation of the algorithm and pseudocode
|
||||
for the most important functions.
|
||||
|
||||
[1]_ also explains how backtracking and theory propagation
|
||||
could be implemented to speed up the current implementation,
|
||||
but these are not currently implemented.
|
||||
|
||||
TODO:
|
||||
- Handle non-rational real numbers
|
||||
- Handle positive and negative infinity
|
||||
- Implement backtracking and theory proposition
|
||||
- Simplify matrix by removing unused variables using Gaussian elimination
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
.. [1] Dutertre, B., de Moura, L.:
|
||||
A Fast Linear-Arithmetic Solver for DPLL(T)
|
||||
https://link.springer.com/chapter/10.1007/11817963_11
|
||||
"""
|
||||
from sympy.solvers.solveset import linear_eq_to_matrix
|
||||
from sympy.matrices.dense import eye
|
||||
from sympy.assumptions import Predicate
|
||||
from sympy.assumptions.assume import AppliedPredicate
|
||||
from sympy.assumptions.ask import Q
|
||||
from sympy.core import Dummy
|
||||
from sympy.core.mul import Mul
|
||||
from sympy.core.add import Add
|
||||
from sympy.core.relational import Eq, Ne
|
||||
from sympy.core.sympify import sympify
|
||||
from sympy.core.singleton import S
|
||||
from sympy.core.numbers import Rational, oo
|
||||
from sympy.matrices.dense import Matrix
|
||||
|
||||
class UnhandledInput(Exception):
|
||||
"""
|
||||
Raised while creating an LRASolver if non-linearity
|
||||
or non-rational numbers are present.
|
||||
"""
|
||||
|
||||
# predicates that LRASolver understands and makes use of
|
||||
ALLOWED_PRED = {Q.eq, Q.gt, Q.lt, Q.le, Q.ge}
|
||||
|
||||
# if true ~Q.gt(x, y) implies Q.le(x, y)
|
||||
HANDLE_NEGATION = True
|
||||
|
||||
class LRASolver():
|
||||
"""
|
||||
Linear Arithmetic Solver for DPLL(T) implemented with an algorithm based on
|
||||
the Dual Simplex method. Uses Bland's pivoting rule to avoid cycling.
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
.. [1] Dutertre, B., de Moura, L.:
|
||||
A Fast Linear-Arithmetic Solver for DPLL(T)
|
||||
https://link.springer.com/chapter/10.1007/11817963_11
|
||||
"""
|
||||
|
||||
def __init__(self, A, slack_variables, nonslack_variables, enc_to_boundary, s_subs, testing_mode):
|
||||
"""
|
||||
Use the "from_encoded_cnf" method to create a new LRASolver.
|
||||
"""
|
||||
self.run_checks = testing_mode
|
||||
self.s_subs = s_subs # used only for test_lra_theory.test_random_problems
|
||||
|
||||
if any(not isinstance(a, Rational) for a in A):
|
||||
raise UnhandledInput("Non-rational numbers are not handled")
|
||||
if any(not isinstance(b.bound, Rational) for b in enc_to_boundary.values()):
|
||||
raise UnhandledInput("Non-rational numbers are not handled")
|
||||
m, n = len(slack_variables), len(slack_variables)+len(nonslack_variables)
|
||||
if m != 0:
|
||||
assert A.shape == (m, n)
|
||||
if self.run_checks:
|
||||
assert A[:, n-m:] == -eye(m)
|
||||
|
||||
self.enc_to_boundary = enc_to_boundary # mapping of int to Boundary objects
|
||||
self.boundary_to_enc = {value: key for key, value in enc_to_boundary.items()}
|
||||
self.A = A
|
||||
self.slack = slack_variables
|
||||
self.nonslack = nonslack_variables
|
||||
self.all_var = nonslack_variables + slack_variables
|
||||
|
||||
self.slack_set = set(slack_variables)
|
||||
|
||||
self.is_sat = True # While True, all constraints asserted so far are satisfiable
|
||||
self.result = None # always one of: (True, assignment), (False, conflict clause), None
|
||||
|
||||
@staticmethod
|
||||
def from_encoded_cnf(encoded_cnf, testing_mode=False):
|
||||
"""
|
||||
Creates an LRASolver from an EncodedCNF object
|
||||
and a list of conflict clauses for propositions
|
||||
that can be simplified to True or False.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
|
||||
encoded_cnf : EncodedCNF
|
||||
|
||||
testing_mode : bool
|
||||
Setting testing_mode to True enables some slow assert statements
|
||||
and sorting to reduce nonterministic behavior.
|
||||
|
||||
Returns
|
||||
=======
|
||||
|
||||
(lra, conflicts)
|
||||
|
||||
lra : LRASolver
|
||||
|
||||
conflicts : list
|
||||
Contains a one-literal conflict clause for each proposition
|
||||
that can be simplified to True or False.
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
>>> from sympy.core.relational import Eq
|
||||
>>> from sympy.assumptions.cnf import CNF, EncodedCNF
|
||||
>>> from sympy.assumptions.ask import Q
|
||||
>>> from sympy.logic.algorithms.lra_theory import LRASolver
|
||||
>>> from sympy.abc import x, y, z
|
||||
>>> phi = (x >= 0) & ((x + y <= 2) | (x + 2 * y - z >= 6))
|
||||
>>> phi = phi & (Eq(x + y, 2) | (x + 2 * y - z > 4))
|
||||
>>> phi = phi & Q.gt(2, 1)
|
||||
>>> cnf = CNF.from_prop(phi)
|
||||
>>> enc = EncodedCNF()
|
||||
>>> enc.from_cnf(cnf)
|
||||
>>> lra, conflicts = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
>>> lra #doctest: +SKIP
|
||||
<sympy.logic.algorithms.lra_theory.LRASolver object at 0x7fdcb0e15b70>
|
||||
>>> conflicts #doctest: +SKIP
|
||||
[[4]]
|
||||
"""
|
||||
# This function has three main jobs:
|
||||
# - raise errors if the input formula is not handled
|
||||
# - preprocesses the formula into a matrix and single variable constraints
|
||||
# - create one-literal conflict clauses from predicates that are always True
|
||||
# or always False such as Q.gt(3, 2)
|
||||
#
|
||||
# See the preprocessing section of "A Fast Linear-Arithmetic Solver for DPLL(T)"
|
||||
# for an explanation of how the formula is converted into a matrix
|
||||
# and a set of single variable constraints.
|
||||
|
||||
encoding = {} # maps int to boundary
|
||||
A = []
|
||||
|
||||
basic = []
|
||||
s_count = 0
|
||||
s_subs = {}
|
||||
nonbasic = []
|
||||
|
||||
if testing_mode:
|
||||
# sort to reduce nondeterminism
|
||||
encoded_cnf_items = sorted(encoded_cnf.encoding.items(), key=lambda x: str(x))
|
||||
else:
|
||||
encoded_cnf_items = encoded_cnf.encoding.items()
|
||||
|
||||
empty_var = Dummy()
|
||||
var_to_lra_var = {}
|
||||
conflicts = []
|
||||
|
||||
for prop, enc in encoded_cnf_items:
|
||||
if isinstance(prop, Predicate):
|
||||
prop = prop(empty_var)
|
||||
if not isinstance(prop, AppliedPredicate):
|
||||
if prop == True:
|
||||
conflicts.append([enc])
|
||||
continue
|
||||
if prop == False:
|
||||
conflicts.append([-enc])
|
||||
continue
|
||||
|
||||
raise ValueError(f"Unhandled Predicate: {prop}")
|
||||
|
||||
assert prop.function in ALLOWED_PRED
|
||||
if prop.lhs == S.NaN or prop.rhs == S.NaN:
|
||||
raise ValueError(f"{prop} contains nan")
|
||||
if prop.lhs.is_imaginary or prop.rhs.is_imaginary:
|
||||
raise UnhandledInput(f"{prop} contains an imaginary component")
|
||||
if prop.lhs == oo or prop.rhs == oo:
|
||||
raise UnhandledInput(f"{prop} contains infinity")
|
||||
|
||||
prop = _eval_binrel(prop) # simplify variable-less quantities to True / False if possible
|
||||
if prop == True:
|
||||
conflicts.append([enc])
|
||||
continue
|
||||
elif prop == False:
|
||||
conflicts.append([-enc])
|
||||
continue
|
||||
elif prop is None:
|
||||
raise UnhandledInput(f"{prop} could not be simplified")
|
||||
|
||||
expr = prop.lhs - prop.rhs
|
||||
if prop.function in [Q.ge, Q.gt]:
|
||||
expr = -expr
|
||||
|
||||
# expr should be less than (or equal to) 0
|
||||
# otherwise prop is False
|
||||
if prop.function in [Q.le, Q.ge]:
|
||||
bool = (expr <= 0)
|
||||
elif prop.function in [Q.lt, Q.gt]:
|
||||
bool = (expr < 0)
|
||||
else:
|
||||
assert prop.function == Q.eq
|
||||
bool = Eq(expr, 0)
|
||||
|
||||
if bool == True:
|
||||
conflicts.append([enc])
|
||||
continue
|
||||
elif bool == False:
|
||||
conflicts.append([-enc])
|
||||
continue
|
||||
|
||||
|
||||
vars, const = _sep_const_terms(expr) # example: (2x + 3y + 2) --> (2x + 3y), (2)
|
||||
vars, var_coeff = _sep_const_coeff(vars) # examples: (2x) --> (x, 2); (2x + 3y) --> (2x + 3y), (1)
|
||||
const = const / var_coeff
|
||||
|
||||
terms = _list_terms(vars) # example: (2x + 3y) --> [2x, 3y]
|
||||
for term in terms:
|
||||
term, _ = _sep_const_coeff(term)
|
||||
assert len(term.free_symbols) > 0
|
||||
if term not in var_to_lra_var:
|
||||
var_to_lra_var[term] = LRAVariable(term)
|
||||
nonbasic.append(term)
|
||||
|
||||
if len(terms) > 1:
|
||||
if vars not in s_subs:
|
||||
s_count += 1
|
||||
d = Dummy(f"s{s_count}")
|
||||
var_to_lra_var[d] = LRAVariable(d)
|
||||
basic.append(d)
|
||||
s_subs[vars] = d
|
||||
A.append(vars - d)
|
||||
var = s_subs[vars]
|
||||
else:
|
||||
var = terms[0]
|
||||
|
||||
assert var_coeff != 0
|
||||
|
||||
equality = prop.function == Q.eq
|
||||
upper = var_coeff > 0 if not equality else None
|
||||
strict = prop.function in [Q.gt, Q.lt]
|
||||
b = Boundary(var_to_lra_var[var], -const, upper, equality, strict)
|
||||
encoding[enc] = b
|
||||
|
||||
fs = [v.free_symbols for v in nonbasic + basic]
|
||||
assert all(len(syms) > 0 for syms in fs)
|
||||
fs_count = sum(len(syms) for syms in fs)
|
||||
if len(fs) > 0 and len(set.union(*fs)) < fs_count:
|
||||
raise UnhandledInput("Nonlinearity is not handled")
|
||||
|
||||
A, _ = linear_eq_to_matrix(A, nonbasic + basic)
|
||||
nonbasic = [var_to_lra_var[nb] for nb in nonbasic]
|
||||
basic = [var_to_lra_var[b] for b in basic]
|
||||
for idx, var in enumerate(nonbasic + basic):
|
||||
var.col_idx = idx
|
||||
|
||||
return LRASolver(A, basic, nonbasic, encoding, s_subs, testing_mode), conflicts
|
||||
|
||||
def reset_bounds(self):
|
||||
"""
|
||||
Resets the state of the LRASolver to before
|
||||
anything was asserted.
|
||||
"""
|
||||
self.result = None
|
||||
for var in self.all_var:
|
||||
var.lower = LRARational(-float("inf"), 0)
|
||||
var.lower_from_eq = False
|
||||
var.lower_from_neg = False
|
||||
var.upper = LRARational(float("inf"), 0)
|
||||
var.upper_from_eq= False
|
||||
var.lower_from_neg = False
|
||||
var.assign = LRARational(0, 0)
|
||||
|
||||
def assert_lit(self, enc_constraint):
|
||||
"""
|
||||
Assert a literal representing a constraint
|
||||
and update the internal state accordingly.
|
||||
|
||||
Note that due to peculiarities of this implementation
|
||||
asserting ~(x > 0) will assert (x <= 0) but asserting
|
||||
~Eq(x, 0) will not do anything.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
|
||||
enc_constraint : int
|
||||
A mapping of encodings to constraints
|
||||
can be found in `self.enc_to_boundary`.
|
||||
|
||||
Returns
|
||||
=======
|
||||
|
||||
None or (False, explanation)
|
||||
|
||||
explanation : set of ints
|
||||
A conflict clause that "explains" why
|
||||
the literals asserted so far are unsatisfiable.
|
||||
"""
|
||||
if abs(enc_constraint) not in self.enc_to_boundary:
|
||||
return None
|
||||
|
||||
if not HANDLE_NEGATION and enc_constraint < 0:
|
||||
return None
|
||||
|
||||
boundary = self.enc_to_boundary[abs(enc_constraint)]
|
||||
sym, c, negated = boundary.var, boundary.bound, enc_constraint < 0
|
||||
|
||||
if boundary.equality and negated:
|
||||
return None # negated equality is not handled and should only appear in conflict clauses
|
||||
|
||||
upper = boundary.upper != negated
|
||||
if boundary.strict != negated:
|
||||
delta = -1 if upper else 1
|
||||
c = LRARational(c, delta)
|
||||
else:
|
||||
c = LRARational(c, 0)
|
||||
|
||||
if boundary.equality:
|
||||
res1 = self._assert_lower(sym, c, from_equality=True, from_neg=negated)
|
||||
if res1 and res1[0] == False:
|
||||
res = res1
|
||||
else:
|
||||
res2 = self._assert_upper(sym, c, from_equality=True, from_neg=negated)
|
||||
res = res2
|
||||
elif upper:
|
||||
res = self._assert_upper(sym, c, from_neg=negated)
|
||||
else:
|
||||
res = self._assert_lower(sym, c, from_neg=negated)
|
||||
|
||||
if self.is_sat and sym not in self.slack_set:
|
||||
self.is_sat = res is None
|
||||
else:
|
||||
self.is_sat = False
|
||||
|
||||
return res
|
||||
|
||||
def _assert_upper(self, xi, ci, from_equality=False, from_neg=False):
|
||||
"""
|
||||
Adjusts the upper bound on variable xi if the new upper bound is
|
||||
more limiting. The assignment of variable xi is adjusted to be
|
||||
within the new bound if needed.
|
||||
|
||||
Also calls `self._update` to update the assignment for slack variables
|
||||
to keep all equalities satisfied.
|
||||
"""
|
||||
if self.result:
|
||||
assert self.result[0] != False
|
||||
self.result = None
|
||||
if ci >= xi.upper:
|
||||
return None
|
||||
if ci < xi.lower:
|
||||
assert (xi.lower[1] >= 0) is True
|
||||
assert (ci[1] <= 0) is True
|
||||
|
||||
lit1, neg1 = Boundary.from_lower(xi)
|
||||
|
||||
lit2 = Boundary(var=xi, const=ci[0], strict=ci[1] != 0, upper=True, equality=from_equality)
|
||||
if from_neg:
|
||||
lit2 = lit2.get_negated()
|
||||
neg2 = -1 if from_neg else 1
|
||||
|
||||
conflict = [-neg1*self.boundary_to_enc[lit1], -neg2*self.boundary_to_enc[lit2]]
|
||||
self.result = False, conflict
|
||||
return self.result
|
||||
xi.upper = ci
|
||||
xi.upper_from_eq = from_equality
|
||||
xi.upper_from_neg = from_neg
|
||||
if xi in self.nonslack and xi.assign > ci:
|
||||
self._update(xi, ci)
|
||||
|
||||
if self.run_checks and all(v.assign[0] != float("inf") and v.assign[0] != -float("inf")
|
||||
for v in self.all_var):
|
||||
M = self.A
|
||||
X = Matrix([v.assign[0] for v in self.all_var])
|
||||
assert all(abs(val) < 10 ** (-10) for val in M * X)
|
||||
|
||||
return None
|
||||
|
||||
def _assert_lower(self, xi, ci, from_equality=False, from_neg=False):
|
||||
"""
|
||||
Adjusts the lower bound on variable xi if the new lower bound is
|
||||
more limiting. The assignment of variable xi is adjusted to be
|
||||
within the new bound if needed.
|
||||
|
||||
Also calls `self._update` to update the assignment for slack variables
|
||||
to keep all equalities satisfied.
|
||||
"""
|
||||
if self.result:
|
||||
assert self.result[0] != False
|
||||
self.result = None
|
||||
if ci <= xi.lower:
|
||||
return None
|
||||
if ci > xi.upper:
|
||||
assert (xi.upper[1] <= 0) is True
|
||||
assert (ci[1] >= 0) is True
|
||||
|
||||
lit1, neg1 = Boundary.from_upper(xi)
|
||||
|
||||
lit2 = Boundary(var=xi, const=ci[0], strict=ci[1] != 0, upper=False, equality=from_equality)
|
||||
if from_neg:
|
||||
lit2 = lit2.get_negated()
|
||||
neg2 = -1 if from_neg else 1
|
||||
|
||||
conflict = [-neg1*self.boundary_to_enc[lit1],-neg2*self.boundary_to_enc[lit2]]
|
||||
self.result = False, conflict
|
||||
return self.result
|
||||
xi.lower = ci
|
||||
xi.lower_from_eq = from_equality
|
||||
xi.lower_from_neg = from_neg
|
||||
if xi in self.nonslack and xi.assign < ci:
|
||||
self._update(xi, ci)
|
||||
|
||||
if self.run_checks and all(v.assign[0] != float("inf") and v.assign[0] != -float("inf")
|
||||
for v in self.all_var):
|
||||
M = self.A
|
||||
X = Matrix([v.assign[0] for v in self.all_var])
|
||||
assert all(abs(val) < 10 ** (-10) for val in M * X)
|
||||
|
||||
return None
|
||||
|
||||
def _update(self, xi, v):
|
||||
"""
|
||||
Updates all slack variables that have equations that contain
|
||||
variable xi so that they stay satisfied given xi is equal to v.
|
||||
"""
|
||||
i = xi.col_idx
|
||||
for j, b in enumerate(self.slack):
|
||||
aji = self.A[j, i]
|
||||
b.assign = b.assign + (v - xi.assign)*aji
|
||||
xi.assign = v
|
||||
|
||||
def check(self):
|
||||
"""
|
||||
Searches for an assignment that satisfies all constraints
|
||||
or determines that no such assignment exists and gives
|
||||
a minimal conflict clause that "explains" why the
|
||||
constraints are unsatisfiable.
|
||||
|
||||
Returns
|
||||
=======
|
||||
|
||||
(True, assignment) or (False, explanation)
|
||||
|
||||
assignment : dict of LRAVariables to values
|
||||
Assigned values are tuples that represent a rational number
|
||||
plus some infinatesimal delta.
|
||||
|
||||
explanation : set of ints
|
||||
"""
|
||||
if self.is_sat:
|
||||
return True, {var: var.assign for var in self.all_var}
|
||||
if self.result:
|
||||
return self.result
|
||||
|
||||
from sympy.matrices.dense import Matrix
|
||||
M = self.A.copy()
|
||||
basic = {s: i for i, s in enumerate(self.slack)} # contains the row index associated with each basic variable
|
||||
nonbasic = set(self.nonslack)
|
||||
while True:
|
||||
if self.run_checks:
|
||||
# nonbasic variables must always be within bounds
|
||||
assert all(((nb.assign >= nb.lower) == True) and ((nb.assign <= nb.upper) == True) for nb in nonbasic)
|
||||
|
||||
# assignments for x must always satisfy Ax = 0
|
||||
# probably have to turn this off when dealing with strict ineq
|
||||
if all(v.assign[0] != float("inf") and v.assign[0] != -float("inf")
|
||||
for v in self.all_var):
|
||||
X = Matrix([v.assign[0] for v in self.all_var])
|
||||
assert all(abs(val) < 10**(-10) for val in M*X)
|
||||
|
||||
# check upper and lower match this format:
|
||||
# x <= rat + delta iff x < rat
|
||||
# x >= rat - delta iff x > rat
|
||||
# this wouldn't make sense:
|
||||
# x <= rat - delta
|
||||
# x >= rat + delta
|
||||
assert all(x.upper[1] <= 0 for x in self.all_var)
|
||||
assert all(x.lower[1] >= 0 for x in self.all_var)
|
||||
|
||||
cand = [b for b in basic if b.assign < b.lower or b.assign > b.upper]
|
||||
|
||||
if len(cand) == 0:
|
||||
return True, {var: var.assign for var in self.all_var}
|
||||
|
||||
xi = min(cand, key=lambda v: v.col_idx) # Bland's rule
|
||||
i = basic[xi]
|
||||
|
||||
if xi.assign < xi.lower:
|
||||
cand = [nb for nb in nonbasic
|
||||
if (M[i, nb.col_idx] > 0 and nb.assign < nb.upper)
|
||||
or (M[i, nb.col_idx] < 0 and nb.assign > nb.lower)]
|
||||
if len(cand) == 0:
|
||||
N_plus = [nb for nb in nonbasic if M[i, nb.col_idx] > 0]
|
||||
N_minus = [nb for nb in nonbasic if M[i, nb.col_idx] < 0]
|
||||
|
||||
conflict = []
|
||||
conflict += [Boundary.from_upper(nb) for nb in N_plus]
|
||||
conflict += [Boundary.from_lower(nb) for nb in N_minus]
|
||||
conflict.append(Boundary.from_lower(xi))
|
||||
conflict = [-neg*self.boundary_to_enc[c] for c, neg in conflict]
|
||||
return False, conflict
|
||||
xj = min(cand, key=str)
|
||||
M = self._pivot_and_update(M, basic, nonbasic, xi, xj, xi.lower)
|
||||
|
||||
if xi.assign > xi.upper:
|
||||
cand = [nb for nb in nonbasic
|
||||
if (M[i, nb.col_idx] < 0 and nb.assign < nb.upper)
|
||||
or (M[i, nb.col_idx] > 0 and nb.assign > nb.lower)]
|
||||
|
||||
if len(cand) == 0:
|
||||
N_plus = [nb for nb in nonbasic if M[i, nb.col_idx] > 0]
|
||||
N_minus = [nb for nb in nonbasic if M[i, nb.col_idx] < 0]
|
||||
|
||||
conflict = []
|
||||
conflict += [Boundary.from_upper(nb) for nb in N_minus]
|
||||
conflict += [Boundary.from_lower(nb) for nb in N_plus]
|
||||
conflict.append(Boundary.from_upper(xi))
|
||||
|
||||
conflict = [-neg*self.boundary_to_enc[c] for c, neg in conflict]
|
||||
return False, conflict
|
||||
xj = min(cand, key=lambda v: v.col_idx)
|
||||
M = self._pivot_and_update(M, basic, nonbasic, xi, xj, xi.upper)
|
||||
|
||||
def _pivot_and_update(self, M, basic, nonbasic, xi, xj, v):
|
||||
"""
|
||||
Pivots basic variable xi with nonbasic variable xj,
|
||||
and sets value of xi to v and adjusts the values of all basic variables
|
||||
to keep equations satisfied.
|
||||
"""
|
||||
i, j = basic[xi], xj.col_idx
|
||||
assert M[i, j] != 0
|
||||
theta = (v - xi.assign)*(1/M[i, j])
|
||||
xi.assign = v
|
||||
xj.assign = xj.assign + theta
|
||||
for xk in basic:
|
||||
if xk != xi:
|
||||
k = basic[xk]
|
||||
akj = M[k, j]
|
||||
xk.assign = xk.assign + theta*akj
|
||||
# pivot
|
||||
basic[xj] = basic[xi]
|
||||
del basic[xi]
|
||||
nonbasic.add(xi)
|
||||
nonbasic.remove(xj)
|
||||
return self._pivot(M, i, j)
|
||||
|
||||
@staticmethod
|
||||
def _pivot(M, i, j):
|
||||
"""
|
||||
Performs a pivot operation about entry i, j of M by performing
|
||||
a series of row operations on a copy of M and returning the result.
|
||||
The original M is left unmodified.
|
||||
|
||||
Conceptually, M represents a system of equations and pivoting
|
||||
can be thought of as rearranging equation i to be in terms of
|
||||
variable j and then substituting in the rest of the equations
|
||||
to get rid of other occurances of variable j.
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
>>> from sympy.matrices.dense import Matrix
|
||||
>>> from sympy.logic.algorithms.lra_theory import LRASolver
|
||||
>>> from sympy import var
|
||||
>>> Matrix(3, 3, var('a:i'))
|
||||
Matrix([
|
||||
[a, b, c],
|
||||
[d, e, f],
|
||||
[g, h, i]])
|
||||
|
||||
This matrix is equivalent to:
|
||||
0 = a*x + b*y + c*z
|
||||
0 = d*x + e*y + f*z
|
||||
0 = g*x + h*y + i*z
|
||||
|
||||
>>> LRASolver._pivot(_, 1, 0)
|
||||
Matrix([
|
||||
[ 0, -a*e/d + b, -a*f/d + c],
|
||||
[-1, -e/d, -f/d],
|
||||
[ 0, h - e*g/d, i - f*g/d]])
|
||||
|
||||
We rearrange equation 1 in terms of variable 0 (x)
|
||||
and substitute to remove x from the other equations.
|
||||
|
||||
0 = 0 + (-a*e/d + b)*y + (-a*f/d + c)*z
|
||||
0 = -x + (-e/d)*y + (-f/d)*z
|
||||
0 = 0 + (h - e*g/d)*y + (i - f*g/d)*z
|
||||
"""
|
||||
_, _, Mij = M[i, :], M[:, j], M[i, j]
|
||||
if Mij == 0:
|
||||
raise ZeroDivisionError("Tried to pivot about zero-valued entry.")
|
||||
A = M.copy()
|
||||
A[i, :] = -A[i, :]/Mij
|
||||
for row in range(M.shape[0]):
|
||||
if row != i:
|
||||
A[row, :] = A[row, :] + A[row, j] * A[i, :]
|
||||
|
||||
return A
|
||||
|
||||
|
||||
def _sep_const_coeff(expr):
|
||||
"""
|
||||
Example
|
||||
=======
|
||||
|
||||
>>> from sympy.logic.algorithms.lra_theory import _sep_const_coeff
|
||||
>>> from sympy.abc import x, y
|
||||
>>> _sep_const_coeff(2*x)
|
||||
(x, 2)
|
||||
>>> _sep_const_coeff(2*x + 3*y)
|
||||
(2*x + 3*y, 1)
|
||||
"""
|
||||
if isinstance(expr, Add):
|
||||
return expr, sympify(1)
|
||||
|
||||
if isinstance(expr, Mul):
|
||||
coeffs = expr.args
|
||||
else:
|
||||
coeffs = [expr]
|
||||
|
||||
var, const = [], []
|
||||
for c in coeffs:
|
||||
c = sympify(c)
|
||||
if len(c.free_symbols)==0:
|
||||
const.append(c)
|
||||
else:
|
||||
var.append(c)
|
||||
return Mul(*var), Mul(*const)
|
||||
|
||||
|
||||
def _list_terms(expr):
|
||||
if not isinstance(expr, Add):
|
||||
return [expr]
|
||||
|
||||
return expr.args
|
||||
|
||||
|
||||
def _sep_const_terms(expr):
|
||||
"""
|
||||
Example
|
||||
=======
|
||||
|
||||
>>> from sympy.logic.algorithms.lra_theory import _sep_const_terms
|
||||
>>> from sympy.abc import x, y
|
||||
>>> _sep_const_terms(2*x + 3*y + 2)
|
||||
(2*x + 3*y, 2)
|
||||
"""
|
||||
if isinstance(expr, Add):
|
||||
terms = expr.args
|
||||
else:
|
||||
terms = [expr]
|
||||
|
||||
var, const = [], []
|
||||
for t in terms:
|
||||
if len(t.free_symbols) == 0:
|
||||
const.append(t)
|
||||
else:
|
||||
var.append(t)
|
||||
return sum(var), sum(const)
|
||||
|
||||
|
||||
def _eval_binrel(binrel):
|
||||
"""
|
||||
Simplify binary relation to True / False if possible.
|
||||
"""
|
||||
if not (len(binrel.lhs.free_symbols) == 0 and len(binrel.rhs.free_symbols) == 0):
|
||||
return binrel
|
||||
if binrel.function == Q.lt:
|
||||
res = binrel.lhs < binrel.rhs
|
||||
elif binrel.function == Q.gt:
|
||||
res = binrel.lhs > binrel.rhs
|
||||
elif binrel.function == Q.le:
|
||||
res = binrel.lhs <= binrel.rhs
|
||||
elif binrel.function == Q.ge:
|
||||
res = binrel.lhs >= binrel.rhs
|
||||
elif binrel.function == Q.eq:
|
||||
res = Eq(binrel.lhs, binrel.rhs)
|
||||
elif binrel.function == Q.ne:
|
||||
res = Ne(binrel.lhs, binrel.rhs)
|
||||
|
||||
if res == True or res == False:
|
||||
return res
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class Boundary:
|
||||
"""
|
||||
Represents an upper or lower bound or an equality between a symbol
|
||||
and some constant.
|
||||
"""
|
||||
def __init__(self, var, const, upper, equality, strict=None):
|
||||
if not equality in [True, False]:
|
||||
assert equality in [True, False]
|
||||
|
||||
|
||||
self.var = var
|
||||
if isinstance(const, tuple):
|
||||
s = const[1] != 0
|
||||
if strict:
|
||||
assert s == strict
|
||||
self.bound = const[0]
|
||||
self.strict = s
|
||||
else:
|
||||
self.bound = const
|
||||
self.strict = strict
|
||||
self.upper = upper if not equality else None
|
||||
self.equality = equality
|
||||
self.strict = strict
|
||||
assert self.strict is not None
|
||||
|
||||
@staticmethod
|
||||
def from_upper(var):
|
||||
neg = -1 if var.upper_from_neg else 1
|
||||
b = Boundary(var, var.upper[0], True, var.upper_from_eq, var.upper[1] != 0)
|
||||
if neg < 0:
|
||||
b = b.get_negated()
|
||||
return b, neg
|
||||
|
||||
@staticmethod
|
||||
def from_lower(var):
|
||||
neg = -1 if var.lower_from_neg else 1
|
||||
b = Boundary(var, var.lower[0], False, var.lower_from_eq, var.lower[1] != 0)
|
||||
if neg < 0:
|
||||
b = b.get_negated()
|
||||
return b, neg
|
||||
|
||||
def get_negated(self):
|
||||
return Boundary(self.var, self.bound, not self.upper, self.equality, not self.strict)
|
||||
|
||||
def get_inequality(self):
|
||||
if self.equality:
|
||||
return Eq(self.var.var, self.bound)
|
||||
elif self.upper and self.strict:
|
||||
return self.var.var < self.bound
|
||||
elif not self.upper and self.strict:
|
||||
return self.var.var > self.bound
|
||||
elif self.upper:
|
||||
return self.var.var <= self.bound
|
||||
else:
|
||||
return self.var.var >= self.bound
|
||||
|
||||
def __repr__(self):
|
||||
return repr("Boundary(" + repr(self.get_inequality()) + ")")
|
||||
|
||||
def __eq__(self, other):
|
||||
other = (other.var, other.bound, other.strict, other.upper, other.equality)
|
||||
return (self.var, self.bound, self.strict, self.upper, self.equality) == other
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.var, self.bound, self.strict, self.upper, self.equality))
|
||||
|
||||
|
||||
class LRARational():
|
||||
"""
|
||||
Represents a rational plus or minus some amount
|
||||
of arbitrary small deltas.
|
||||
"""
|
||||
def __init__(self, rational, delta):
|
||||
self.value = (rational, delta)
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.value < other.value
|
||||
|
||||
def __le__(self, other):
|
||||
return self.value <= other.value
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.value == other.value
|
||||
|
||||
def __add__(self, other):
|
||||
return LRARational(self.value[0] + other.value[0], self.value[1] + other.value[1])
|
||||
|
||||
def __sub__(self, other):
|
||||
return LRARational(self.value[0] - other.value[0], self.value[1] - other.value[1])
|
||||
|
||||
def __mul__(self, other):
|
||||
assert not isinstance(other, LRARational)
|
||||
return LRARational(self.value[0] * other, self.value[1] * other)
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.value[index]
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.value)
|
||||
|
||||
|
||||
class LRAVariable():
|
||||
"""
|
||||
Object to keep track of upper and lower bounds
|
||||
on `self.var`.
|
||||
"""
|
||||
def __init__(self, var):
|
||||
self.upper = LRARational(float("inf"), 0)
|
||||
self.upper_from_eq = False
|
||||
self.upper_from_neg = False
|
||||
self.lower = LRARational(-float("inf"), 0)
|
||||
self.lower_from_eq = False
|
||||
self.lower_from_neg = False
|
||||
self.assign = LRARational(0,0)
|
||||
self.var = var
|
||||
self.col_idx = None
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.var)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, LRAVariable):
|
||||
return False
|
||||
return other.var == self.var
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.var)
|
||||
@@ -0,0 +1,46 @@
|
||||
from sympy.assumptions.cnf import EncodedCNF
|
||||
|
||||
def minisat22_satisfiable(expr, all_models=False, minimal=False):
|
||||
|
||||
if not isinstance(expr, EncodedCNF):
|
||||
exprs = EncodedCNF()
|
||||
exprs.add_prop(expr)
|
||||
expr = exprs
|
||||
|
||||
from pysat.solvers import Minisat22
|
||||
|
||||
# Return UNSAT when False (encoded as 0) is present in the CNF
|
||||
if {0} in expr.data:
|
||||
if all_models:
|
||||
return (f for f in [False])
|
||||
return False
|
||||
|
||||
r = Minisat22(expr.data)
|
||||
|
||||
if minimal:
|
||||
r.set_phases([-(i+1) for i in range(r.nof_vars())])
|
||||
|
||||
if not r.solve():
|
||||
return False
|
||||
|
||||
if not all_models:
|
||||
return {expr.symbols[abs(lit) - 1]: lit > 0 for lit in r.get_model()}
|
||||
|
||||
else:
|
||||
# Make solutions SymPy compatible by creating a generator
|
||||
def _gen(results):
|
||||
satisfiable = False
|
||||
while results.solve():
|
||||
sol = results.get_model()
|
||||
yield {expr.symbols[abs(lit) - 1]: lit > 0 for lit in sol}
|
||||
if minimal:
|
||||
results.add_clause([-i for i in sol if i>0])
|
||||
else:
|
||||
results.add_clause([-i for i in sol])
|
||||
satisfiable = True
|
||||
if not satisfiable:
|
||||
yield False
|
||||
raise StopIteration
|
||||
|
||||
|
||||
return _gen(r)
|
||||
@@ -0,0 +1,41 @@
|
||||
from sympy.assumptions.cnf import EncodedCNF
|
||||
|
||||
|
||||
def pycosat_satisfiable(expr, all_models=False):
|
||||
import pycosat
|
||||
if not isinstance(expr, EncodedCNF):
|
||||
exprs = EncodedCNF()
|
||||
exprs.add_prop(expr)
|
||||
expr = exprs
|
||||
|
||||
# Return UNSAT when False (encoded as 0) is present in the CNF
|
||||
if {0} in expr.data:
|
||||
if all_models:
|
||||
return (f for f in [False])
|
||||
return False
|
||||
|
||||
if not all_models:
|
||||
r = pycosat.solve(expr.data)
|
||||
result = (r != "UNSAT")
|
||||
if not result:
|
||||
return result
|
||||
return {expr.symbols[abs(lit) - 1]: lit > 0 for lit in r}
|
||||
else:
|
||||
r = pycosat.itersolve(expr.data)
|
||||
result = (r != "UNSAT")
|
||||
if not result:
|
||||
return result
|
||||
|
||||
# Make solutions SymPy compatible by creating a generator
|
||||
def _gen(results):
|
||||
satisfiable = False
|
||||
try:
|
||||
while True:
|
||||
sol = next(results)
|
||||
yield {expr.symbols[abs(lit) - 1]: lit > 0 for lit in sol}
|
||||
satisfiable = True
|
||||
except StopIteration:
|
||||
if not satisfiable:
|
||||
yield False
|
||||
|
||||
return _gen(r)
|
||||
@@ -0,0 +1,115 @@
|
||||
from sympy.printing.smtlib import smtlib_code
|
||||
from sympy.assumptions.assume import AppliedPredicate
|
||||
from sympy.assumptions.cnf import EncodedCNF
|
||||
from sympy.assumptions.ask import Q
|
||||
|
||||
from sympy.core import Add, Mul
|
||||
from sympy.core.relational import Equality, LessThan, GreaterThan, StrictLessThan, StrictGreaterThan
|
||||
from sympy.functions.elementary.complexes import Abs
|
||||
from sympy.functions.elementary.exponential import Pow
|
||||
from sympy.functions.elementary.miscellaneous import Min, Max
|
||||
from sympy.logic.boolalg import And, Or, Xor, Implies
|
||||
from sympy.logic.boolalg import Not, ITE
|
||||
from sympy.assumptions.relation.equality import StrictGreaterThanPredicate, StrictLessThanPredicate, GreaterThanPredicate, LessThanPredicate, EqualityPredicate
|
||||
from sympy.external import import_module
|
||||
|
||||
def z3_satisfiable(expr, all_models=False):
|
||||
if not isinstance(expr, EncodedCNF):
|
||||
exprs = EncodedCNF()
|
||||
exprs.add_prop(expr)
|
||||
expr = exprs
|
||||
|
||||
z3 = import_module("z3")
|
||||
if z3 is None:
|
||||
raise ImportError("z3 is not installed")
|
||||
|
||||
s = encoded_cnf_to_z3_solver(expr, z3)
|
||||
|
||||
res = str(s.check())
|
||||
if res == "unsat":
|
||||
return False
|
||||
elif res == "sat":
|
||||
return z3_model_to_sympy_model(s.model(), expr)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def z3_model_to_sympy_model(z3_model, enc_cnf):
|
||||
rev_enc = {value : key for key, value in enc_cnf.encoding.items()}
|
||||
return {rev_enc[int(var.name()[1:])] : bool(z3_model[var]) for var in z3_model}
|
||||
|
||||
|
||||
def clause_to_assertion(clause):
|
||||
clause_strings = [f"d{abs(lit)}" if lit > 0 else f"(not d{abs(lit)})" for lit in clause]
|
||||
return "(assert (or " + " ".join(clause_strings) + "))"
|
||||
|
||||
|
||||
def encoded_cnf_to_z3_solver(enc_cnf, z3):
|
||||
def dummify_bool(pred):
|
||||
return False
|
||||
assert isinstance(pred, AppliedPredicate)
|
||||
|
||||
if pred.function in [Q.positive, Q.negative, Q.zero]:
|
||||
return pred
|
||||
else:
|
||||
return False
|
||||
|
||||
s = z3.Solver()
|
||||
|
||||
declarations = [f"(declare-const d{var} Bool)" for var in enc_cnf.variables]
|
||||
assertions = [clause_to_assertion(clause) for clause in enc_cnf.data]
|
||||
|
||||
symbols = set()
|
||||
for pred, enc in enc_cnf.encoding.items():
|
||||
if not isinstance(pred, AppliedPredicate):
|
||||
continue
|
||||
if pred.function not in (Q.gt, Q.lt, Q.ge, Q.le, Q.ne, Q.eq, Q.positive, Q.negative, Q.extended_negative, Q.extended_positive, Q.zero, Q.nonzero, Q.nonnegative, Q.nonpositive, Q.extended_nonzero, Q.extended_nonnegative, Q.extended_nonpositive):
|
||||
continue
|
||||
|
||||
pred_str = smtlib_code(pred, auto_declare=False, auto_assert=False, known_functions=known_functions)
|
||||
|
||||
symbols |= pred.free_symbols
|
||||
pred = pred_str
|
||||
clause = f"(implies d{enc} {pred})"
|
||||
assertion = "(assert " + clause + ")"
|
||||
assertions.append(assertion)
|
||||
|
||||
for sym in symbols:
|
||||
declarations.append(f"(declare-const {sym} Real)")
|
||||
|
||||
declarations = "\n".join(declarations)
|
||||
assertions = "\n".join(assertions)
|
||||
s.from_string(declarations)
|
||||
s.from_string(assertions)
|
||||
|
||||
return s
|
||||
|
||||
|
||||
known_functions = {
|
||||
Add: '+',
|
||||
Mul: '*',
|
||||
|
||||
Equality: '=',
|
||||
LessThan: '<=',
|
||||
GreaterThan: '>=',
|
||||
StrictLessThan: '<',
|
||||
StrictGreaterThan: '>',
|
||||
|
||||
EqualityPredicate(): '=',
|
||||
LessThanPredicate(): '<=',
|
||||
GreaterThanPredicate(): '>=',
|
||||
StrictLessThanPredicate(): '<',
|
||||
StrictGreaterThanPredicate(): '>',
|
||||
|
||||
Abs: 'abs',
|
||||
Min: 'min',
|
||||
Max: 'max',
|
||||
Pow: '^',
|
||||
|
||||
And: 'and',
|
||||
Or: 'or',
|
||||
Xor: 'xor',
|
||||
Not: 'not',
|
||||
ITE: 'ite',
|
||||
Implies: '=>',
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,340 @@
|
||||
"""Inference in propositional logic"""
|
||||
|
||||
from sympy.logic.boolalg import And, Not, conjuncts, to_cnf, BooleanFunction
|
||||
from sympy.core.sorting import ordered
|
||||
from sympy.core.sympify import sympify
|
||||
from sympy.external.importtools import import_module
|
||||
|
||||
|
||||
def literal_symbol(literal):
|
||||
"""
|
||||
The symbol in this literal (without the negation).
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.abc import A
|
||||
>>> from sympy.logic.inference import literal_symbol
|
||||
>>> literal_symbol(A)
|
||||
A
|
||||
>>> literal_symbol(~A)
|
||||
A
|
||||
|
||||
"""
|
||||
|
||||
if literal is True or literal is False:
|
||||
return literal
|
||||
elif literal.is_Symbol:
|
||||
return literal
|
||||
elif literal.is_Not:
|
||||
return literal_symbol(literal.args[0])
|
||||
else:
|
||||
raise ValueError("Argument must be a boolean literal.")
|
||||
|
||||
|
||||
def satisfiable(expr, algorithm=None, all_models=False, minimal=False, use_lra_theory=False):
|
||||
"""
|
||||
Check satisfiability of a propositional sentence.
|
||||
Returns a model when it succeeds.
|
||||
Returns {true: true} for trivially true expressions.
|
||||
|
||||
On setting all_models to True, if given expr is satisfiable then
|
||||
returns a generator of models. However, if expr is unsatisfiable
|
||||
then returns a generator containing the single element False.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.abc import A, B
|
||||
>>> from sympy.logic.inference import satisfiable
|
||||
>>> satisfiable(A & ~B)
|
||||
{A: True, B: False}
|
||||
>>> satisfiable(A & ~A)
|
||||
False
|
||||
>>> satisfiable(True)
|
||||
{True: True}
|
||||
>>> next(satisfiable(A & ~A, all_models=True))
|
||||
False
|
||||
>>> models = satisfiable((A >> B) & B, all_models=True)
|
||||
>>> next(models)
|
||||
{A: False, B: True}
|
||||
>>> next(models)
|
||||
{A: True, B: True}
|
||||
>>> def use_models(models):
|
||||
... for model in models:
|
||||
... if model:
|
||||
... # Do something with the model.
|
||||
... print(model)
|
||||
... else:
|
||||
... # Given expr is unsatisfiable.
|
||||
... print("UNSAT")
|
||||
>>> use_models(satisfiable(A >> ~A, all_models=True))
|
||||
{A: False}
|
||||
>>> use_models(satisfiable(A ^ A, all_models=True))
|
||||
UNSAT
|
||||
|
||||
"""
|
||||
if use_lra_theory:
|
||||
if algorithm is not None and algorithm != "dpll2":
|
||||
raise ValueError(f"Currently only dpll2 can handle using lra theory. {algorithm} is not handled.")
|
||||
algorithm = "dpll2"
|
||||
|
||||
if algorithm is None or algorithm == "pycosat":
|
||||
pycosat = import_module('pycosat')
|
||||
if pycosat is not None:
|
||||
algorithm = "pycosat"
|
||||
else:
|
||||
if algorithm == "pycosat":
|
||||
raise ImportError("pycosat module is not present")
|
||||
# Silently fall back to dpll2 if pycosat
|
||||
# is not installed
|
||||
algorithm = "dpll2"
|
||||
|
||||
if algorithm=="minisat22":
|
||||
pysat = import_module('pysat')
|
||||
if pysat is None:
|
||||
algorithm = "dpll2"
|
||||
|
||||
if algorithm=="z3":
|
||||
z3 = import_module('z3')
|
||||
if z3 is None:
|
||||
algorithm = "dpll2"
|
||||
|
||||
if algorithm == "dpll":
|
||||
from sympy.logic.algorithms.dpll import dpll_satisfiable
|
||||
return dpll_satisfiable(expr)
|
||||
elif algorithm == "dpll2":
|
||||
from sympy.logic.algorithms.dpll2 import dpll_satisfiable
|
||||
return dpll_satisfiable(expr, all_models, use_lra_theory=use_lra_theory)
|
||||
elif algorithm == "pycosat":
|
||||
from sympy.logic.algorithms.pycosat_wrapper import pycosat_satisfiable
|
||||
return pycosat_satisfiable(expr, all_models)
|
||||
elif algorithm == "minisat22":
|
||||
from sympy.logic.algorithms.minisat22_wrapper import minisat22_satisfiable
|
||||
return minisat22_satisfiable(expr, all_models, minimal)
|
||||
elif algorithm == "z3":
|
||||
from sympy.logic.algorithms.z3_wrapper import z3_satisfiable
|
||||
return z3_satisfiable(expr, all_models)
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def valid(expr):
|
||||
"""
|
||||
Check validity of a propositional sentence.
|
||||
A valid propositional sentence is True under every assignment.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.abc import A, B
|
||||
>>> from sympy.logic.inference import valid
|
||||
>>> valid(A | ~A)
|
||||
True
|
||||
>>> valid(A | B)
|
||||
False
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
.. [1] https://en.wikipedia.org/wiki/Validity
|
||||
|
||||
"""
|
||||
return not satisfiable(Not(expr))
|
||||
|
||||
|
||||
def pl_true(expr, model=None, deep=False):
|
||||
"""
|
||||
Returns whether the given assignment is a model or not.
|
||||
|
||||
If the assignment does not specify the value for every proposition,
|
||||
this may return None to indicate 'not obvious'.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
|
||||
model : dict, optional, default: {}
|
||||
Mapping of symbols to boolean values to indicate assignment.
|
||||
deep: boolean, optional, default: False
|
||||
Gives the value of the expression under partial assignments
|
||||
correctly. May still return None to indicate 'not obvious'.
|
||||
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.abc import A, B
|
||||
>>> from sympy.logic.inference import pl_true
|
||||
>>> pl_true( A & B, {A: True, B: True})
|
||||
True
|
||||
>>> pl_true(A & B, {A: False})
|
||||
False
|
||||
>>> pl_true(A & B, {A: True})
|
||||
>>> pl_true(A & B, {A: True}, deep=True)
|
||||
>>> pl_true(A >> (B >> A))
|
||||
>>> pl_true(A >> (B >> A), deep=True)
|
||||
True
|
||||
>>> pl_true(A & ~A)
|
||||
>>> pl_true(A & ~A, deep=True)
|
||||
False
|
||||
>>> pl_true(A & B & (~A | ~B), {A: True})
|
||||
>>> pl_true(A & B & (~A | ~B), {A: True}, deep=True)
|
||||
False
|
||||
|
||||
"""
|
||||
|
||||
from sympy.core.symbol import Symbol
|
||||
|
||||
boolean = (True, False)
|
||||
|
||||
def _validate(expr):
|
||||
if isinstance(expr, Symbol) or expr in boolean:
|
||||
return True
|
||||
if not isinstance(expr, BooleanFunction):
|
||||
return False
|
||||
return all(_validate(arg) for arg in expr.args)
|
||||
|
||||
if expr in boolean:
|
||||
return expr
|
||||
expr = sympify(expr)
|
||||
if not _validate(expr):
|
||||
raise ValueError("%s is not a valid boolean expression" % expr)
|
||||
if not model:
|
||||
model = {}
|
||||
model = {k: v for k, v in model.items() if v in boolean}
|
||||
result = expr.subs(model)
|
||||
if result in boolean:
|
||||
return bool(result)
|
||||
if deep:
|
||||
model = dict.fromkeys(result.atoms(), True)
|
||||
if pl_true(result, model):
|
||||
if valid(result):
|
||||
return True
|
||||
else:
|
||||
if not satisfiable(result):
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def entails(expr, formula_set=None):
|
||||
"""
|
||||
Check whether the given expr_set entail an expr.
|
||||
If formula_set is empty then it returns the validity of expr.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.abc import A, B, C
|
||||
>>> from sympy.logic.inference import entails
|
||||
>>> entails(A, [A >> B, B >> C])
|
||||
False
|
||||
>>> entails(C, [A >> B, B >> C, A])
|
||||
True
|
||||
>>> entails(A >> B)
|
||||
False
|
||||
>>> entails(A >> (B >> A))
|
||||
True
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
.. [1] https://en.wikipedia.org/wiki/Logical_consequence
|
||||
|
||||
"""
|
||||
if formula_set:
|
||||
formula_set = list(formula_set)
|
||||
else:
|
||||
formula_set = []
|
||||
formula_set.append(Not(expr))
|
||||
return not satisfiable(And(*formula_set))
|
||||
|
||||
|
||||
class KB:
|
||||
"""Base class for all knowledge bases"""
|
||||
def __init__(self, sentence=None):
|
||||
self.clauses_ = set()
|
||||
if sentence:
|
||||
self.tell(sentence)
|
||||
|
||||
def tell(self, sentence):
|
||||
raise NotImplementedError
|
||||
|
||||
def ask(self, query):
|
||||
raise NotImplementedError
|
||||
|
||||
def retract(self, sentence):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def clauses(self):
|
||||
return list(ordered(self.clauses_))
|
||||
|
||||
|
||||
class PropKB(KB):
|
||||
"""A KB for Propositional Logic. Inefficient, with no indexing."""
|
||||
|
||||
def tell(self, sentence):
|
||||
"""Add the sentence's clauses to the KB
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.inference import PropKB
|
||||
>>> from sympy.abc import x, y
|
||||
>>> l = PropKB()
|
||||
>>> l.clauses
|
||||
[]
|
||||
|
||||
>>> l.tell(x | y)
|
||||
>>> l.clauses
|
||||
[x | y]
|
||||
|
||||
>>> l.tell(y)
|
||||
>>> l.clauses
|
||||
[y, x | y]
|
||||
|
||||
"""
|
||||
for c in conjuncts(to_cnf(sentence)):
|
||||
self.clauses_.add(c)
|
||||
|
||||
def ask(self, query):
|
||||
"""Checks if the query is true given the set of clauses.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.inference import PropKB
|
||||
>>> from sympy.abc import x, y
|
||||
>>> l = PropKB()
|
||||
>>> l.tell(x & ~y)
|
||||
>>> l.ask(x)
|
||||
True
|
||||
>>> l.ask(y)
|
||||
False
|
||||
|
||||
"""
|
||||
return entails(query, self.clauses_)
|
||||
|
||||
def retract(self, sentence):
|
||||
"""Remove the sentence's clauses from the KB
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.inference import PropKB
|
||||
>>> from sympy.abc import x, y
|
||||
>>> l = PropKB()
|
||||
>>> l.clauses
|
||||
[]
|
||||
|
||||
>>> l.tell(x | y)
|
||||
>>> l.clauses
|
||||
[x | y]
|
||||
|
||||
>>> l.retract(x | y)
|
||||
>>> l.clauses
|
||||
[]
|
||||
|
||||
"""
|
||||
for c in conjuncts(to_cnf(sentence)):
|
||||
self.clauses_.discard(c)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,234 @@
|
||||
"""Various tests on satisfiability using dimacs cnf file syntax
|
||||
You can find lots of cnf files in
|
||||
ftp://dimacs.rutgers.edu/pub/challenge/satisfiability/benchmarks/cnf/
|
||||
"""
|
||||
|
||||
from sympy.logic.utilities.dimacs import load
|
||||
from sympy.logic.algorithms.dpll import dpll_satisfiable
|
||||
|
||||
|
||||
def test_f1():
|
||||
assert bool(dpll_satisfiable(load(f1)))
|
||||
|
||||
|
||||
def test_f2():
|
||||
assert bool(dpll_satisfiable(load(f2)))
|
||||
|
||||
|
||||
def test_f3():
|
||||
assert bool(dpll_satisfiable(load(f3)))
|
||||
|
||||
|
||||
def test_f4():
|
||||
assert not bool(dpll_satisfiable(load(f4)))
|
||||
|
||||
|
||||
def test_f5():
|
||||
assert bool(dpll_satisfiable(load(f5)))
|
||||
|
||||
f1 = """c simple example
|
||||
c Resolution: SATISFIABLE
|
||||
c
|
||||
p cnf 3 2
|
||||
1 -3 0
|
||||
2 3 -1 0
|
||||
"""
|
||||
|
||||
|
||||
f2 = """c an example from Quinn's text, 16 variables and 18 clauses.
|
||||
c Resolution: SATISFIABLE
|
||||
c
|
||||
p cnf 16 18
|
||||
1 2 0
|
||||
-2 -4 0
|
||||
3 4 0
|
||||
-4 -5 0
|
||||
5 -6 0
|
||||
6 -7 0
|
||||
6 7 0
|
||||
7 -16 0
|
||||
8 -9 0
|
||||
-8 -14 0
|
||||
9 10 0
|
||||
9 -10 0
|
||||
-10 -11 0
|
||||
10 12 0
|
||||
11 12 0
|
||||
13 14 0
|
||||
14 -15 0
|
||||
15 16 0
|
||||
"""
|
||||
|
||||
f3 = """c
|
||||
p cnf 6 9
|
||||
-1 0
|
||||
-3 0
|
||||
2 -1 0
|
||||
2 -4 0
|
||||
5 -4 0
|
||||
-1 -3 0
|
||||
-4 -6 0
|
||||
1 3 -2 0
|
||||
4 6 -2 -5 0
|
||||
"""
|
||||
|
||||
f4 = """c
|
||||
c file: hole6.cnf [http://people.sc.fsu.edu/~jburkardt/data/cnf/hole6.cnf]
|
||||
c
|
||||
c SOURCE: John Hooker (jh38+@andrew.cmu.edu)
|
||||
c
|
||||
c DESCRIPTION: Pigeon hole problem of placing n (for file 'holen.cnf') pigeons
|
||||
c in n+1 holes without placing 2 pigeons in the same hole
|
||||
c
|
||||
c NOTE: Part of the collection at the Forschungsinstitut fuer
|
||||
c anwendungsorientierte Wissensverarbeitung in Ulm Germany.
|
||||
c
|
||||
c NOTE: Not satisfiable
|
||||
c
|
||||
p cnf 42 133
|
||||
-1 -7 0
|
||||
-1 -13 0
|
||||
-1 -19 0
|
||||
-1 -25 0
|
||||
-1 -31 0
|
||||
-1 -37 0
|
||||
-7 -13 0
|
||||
-7 -19 0
|
||||
-7 -25 0
|
||||
-7 -31 0
|
||||
-7 -37 0
|
||||
-13 -19 0
|
||||
-13 -25 0
|
||||
-13 -31 0
|
||||
-13 -37 0
|
||||
-19 -25 0
|
||||
-19 -31 0
|
||||
-19 -37 0
|
||||
-25 -31 0
|
||||
-25 -37 0
|
||||
-31 -37 0
|
||||
-2 -8 0
|
||||
-2 -14 0
|
||||
-2 -20 0
|
||||
-2 -26 0
|
||||
-2 -32 0
|
||||
-2 -38 0
|
||||
-8 -14 0
|
||||
-8 -20 0
|
||||
-8 -26 0
|
||||
-8 -32 0
|
||||
-8 -38 0
|
||||
-14 -20 0
|
||||
-14 -26 0
|
||||
-14 -32 0
|
||||
-14 -38 0
|
||||
-20 -26 0
|
||||
-20 -32 0
|
||||
-20 -38 0
|
||||
-26 -32 0
|
||||
-26 -38 0
|
||||
-32 -38 0
|
||||
-3 -9 0
|
||||
-3 -15 0
|
||||
-3 -21 0
|
||||
-3 -27 0
|
||||
-3 -33 0
|
||||
-3 -39 0
|
||||
-9 -15 0
|
||||
-9 -21 0
|
||||
-9 -27 0
|
||||
-9 -33 0
|
||||
-9 -39 0
|
||||
-15 -21 0
|
||||
-15 -27 0
|
||||
-15 -33 0
|
||||
-15 -39 0
|
||||
-21 -27 0
|
||||
-21 -33 0
|
||||
-21 -39 0
|
||||
-27 -33 0
|
||||
-27 -39 0
|
||||
-33 -39 0
|
||||
-4 -10 0
|
||||
-4 -16 0
|
||||
-4 -22 0
|
||||
-4 -28 0
|
||||
-4 -34 0
|
||||
-4 -40 0
|
||||
-10 -16 0
|
||||
-10 -22 0
|
||||
-10 -28 0
|
||||
-10 -34 0
|
||||
-10 -40 0
|
||||
-16 -22 0
|
||||
-16 -28 0
|
||||
-16 -34 0
|
||||
-16 -40 0
|
||||
-22 -28 0
|
||||
-22 -34 0
|
||||
-22 -40 0
|
||||
-28 -34 0
|
||||
-28 -40 0
|
||||
-34 -40 0
|
||||
-5 -11 0
|
||||
-5 -17 0
|
||||
-5 -23 0
|
||||
-5 -29 0
|
||||
-5 -35 0
|
||||
-5 -41 0
|
||||
-11 -17 0
|
||||
-11 -23 0
|
||||
-11 -29 0
|
||||
-11 -35 0
|
||||
-11 -41 0
|
||||
-17 -23 0
|
||||
-17 -29 0
|
||||
-17 -35 0
|
||||
-17 -41 0
|
||||
-23 -29 0
|
||||
-23 -35 0
|
||||
-23 -41 0
|
||||
-29 -35 0
|
||||
-29 -41 0
|
||||
-35 -41 0
|
||||
-6 -12 0
|
||||
-6 -18 0
|
||||
-6 -24 0
|
||||
-6 -30 0
|
||||
-6 -36 0
|
||||
-6 -42 0
|
||||
-12 -18 0
|
||||
-12 -24 0
|
||||
-12 -30 0
|
||||
-12 -36 0
|
||||
-12 -42 0
|
||||
-18 -24 0
|
||||
-18 -30 0
|
||||
-18 -36 0
|
||||
-18 -42 0
|
||||
-24 -30 0
|
||||
-24 -36 0
|
||||
-24 -42 0
|
||||
-30 -36 0
|
||||
-30 -42 0
|
||||
-36 -42 0
|
||||
6 5 4 3 2 1 0
|
||||
12 11 10 9 8 7 0
|
||||
18 17 16 15 14 13 0
|
||||
24 23 22 21 20 19 0
|
||||
30 29 28 27 26 25 0
|
||||
36 35 34 33 32 31 0
|
||||
42 41 40 39 38 37 0
|
||||
"""
|
||||
|
||||
f5 = """c simple example requiring variable selection
|
||||
c
|
||||
c NOTE: Satisfiable
|
||||
c
|
||||
p cnf 5 5
|
||||
1 2 3 0
|
||||
1 -2 3 0
|
||||
4 5 -3 0
|
||||
1 -4 -3 0
|
||||
-1 -5 0
|
||||
"""
|
||||
@@ -0,0 +1,396 @@
|
||||
"""For more tests on satisfiability, see test_dimacs"""
|
||||
|
||||
from sympy.assumptions.ask import Q
|
||||
from sympy.core.symbol import symbols
|
||||
from sympy.core.relational import Unequality
|
||||
from sympy.logic.boolalg import And, Or, Implies, Equivalent, true, false
|
||||
from sympy.logic.inference import literal_symbol, \
|
||||
pl_true, satisfiable, valid, entails, PropKB
|
||||
from sympy.logic.algorithms.dpll import dpll, dpll_satisfiable, \
|
||||
find_pure_symbol, find_unit_clause, unit_propagate, \
|
||||
find_pure_symbol_int_repr, find_unit_clause_int_repr, \
|
||||
unit_propagate_int_repr
|
||||
from sympy.logic.algorithms.dpll2 import dpll_satisfiable as dpll2_satisfiable
|
||||
|
||||
from sympy.logic.algorithms.z3_wrapper import z3_satisfiable
|
||||
from sympy.assumptions.cnf import CNF, EncodedCNF
|
||||
from sympy.logic.tests.test_lra_theory import make_random_problem
|
||||
from sympy.core.random import randint
|
||||
|
||||
from sympy.testing.pytest import raises, skip
|
||||
from sympy.external import import_module
|
||||
|
||||
|
||||
def test_literal():
|
||||
A, B = symbols('A,B')
|
||||
assert literal_symbol(True) is True
|
||||
assert literal_symbol(False) is False
|
||||
assert literal_symbol(A) is A
|
||||
assert literal_symbol(~A) is A
|
||||
|
||||
|
||||
def test_find_pure_symbol():
|
||||
A, B, C = symbols('A,B,C')
|
||||
assert find_pure_symbol([A], [A]) == (A, True)
|
||||
assert find_pure_symbol([A, B], [~A | B, ~B | A]) == (None, None)
|
||||
assert find_pure_symbol([A, B, C], [ A | ~B, ~B | ~C, C | A]) == (A, True)
|
||||
assert find_pure_symbol([A, B, C], [~A | B, B | ~C, C | A]) == (B, True)
|
||||
assert find_pure_symbol([A, B, C], [~A | ~B, ~B | ~C, C | A]) == (B, False)
|
||||
assert find_pure_symbol(
|
||||
[A, B, C], [~A | B, ~B | ~C, C | A]) == (None, None)
|
||||
|
||||
|
||||
def test_find_pure_symbol_int_repr():
|
||||
assert find_pure_symbol_int_repr([1], [{1}]) == (1, True)
|
||||
assert find_pure_symbol_int_repr([1, 2],
|
||||
[{-1, 2}, {-2, 1}]) == (None, None)
|
||||
assert find_pure_symbol_int_repr([1, 2, 3],
|
||||
[{1, -2}, {-2, -3}, {3, 1}]) == (1, True)
|
||||
assert find_pure_symbol_int_repr([1, 2, 3],
|
||||
[{-1, 2}, {2, -3}, {3, 1}]) == (2, True)
|
||||
assert find_pure_symbol_int_repr([1, 2, 3],
|
||||
[{-1, -2}, {-2, -3}, {3, 1}]) == (2, False)
|
||||
assert find_pure_symbol_int_repr([1, 2, 3],
|
||||
[{-1, 2}, {-2, -3}, {3, 1}]) == (None, None)
|
||||
|
||||
|
||||
def test_unit_clause():
|
||||
A, B, C = symbols('A,B,C')
|
||||
assert find_unit_clause([A], {}) == (A, True)
|
||||
assert find_unit_clause([A, ~A], {}) == (A, True) # Wrong ??
|
||||
assert find_unit_clause([A | B], {A: True}) == (B, True)
|
||||
assert find_unit_clause([A | B], {B: True}) == (A, True)
|
||||
assert find_unit_clause(
|
||||
[A | B | C, B | ~C, A | ~B], {A: True}) == (B, False)
|
||||
assert find_unit_clause([A | B | C, B | ~C, A | B], {A: True}) == (B, True)
|
||||
assert find_unit_clause([A | B | C, B | ~C, A ], {}) == (A, True)
|
||||
|
||||
|
||||
def test_unit_clause_int_repr():
|
||||
assert find_unit_clause_int_repr(map(set, [[1]]), {}) == (1, True)
|
||||
assert find_unit_clause_int_repr(map(set, [[1], [-1]]), {}) == (1, True)
|
||||
assert find_unit_clause_int_repr([{1, 2}], {1: True}) == (2, True)
|
||||
assert find_unit_clause_int_repr([{1, 2}], {2: True}) == (1, True)
|
||||
assert find_unit_clause_int_repr(map(set,
|
||||
[[1, 2, 3], [2, -3], [1, -2]]), {1: True}) == (2, False)
|
||||
assert find_unit_clause_int_repr(map(set,
|
||||
[[1, 2, 3], [3, -3], [1, 2]]), {1: True}) == (2, True)
|
||||
|
||||
A, B, C = symbols('A,B,C')
|
||||
assert find_unit_clause([A | B | C, B | ~C, A ], {}) == (A, True)
|
||||
|
||||
|
||||
def test_unit_propagate():
|
||||
A, B, C = symbols('A,B,C')
|
||||
assert unit_propagate([A | B], A) == []
|
||||
assert unit_propagate([A | B, ~A | C, ~C | B, A], A) == [C, ~C | B, A]
|
||||
|
||||
|
||||
def test_unit_propagate_int_repr():
|
||||
assert unit_propagate_int_repr([{1, 2}], 1) == []
|
||||
assert unit_propagate_int_repr(map(set,
|
||||
[[1, 2], [-1, 3], [-3, 2], [1]]), 1) == [{3}, {-3, 2}]
|
||||
|
||||
|
||||
def test_dpll():
|
||||
"""This is also tested in test_dimacs"""
|
||||
A, B, C = symbols('A,B,C')
|
||||
assert dpll([A | B], [A, B], {A: True, B: True}) == {A: True, B: True}
|
||||
|
||||
|
||||
def test_dpll_satisfiable():
|
||||
A, B, C = symbols('A,B,C')
|
||||
assert dpll_satisfiable( A & ~A ) is False
|
||||
assert dpll_satisfiable( A & ~B ) == {A: True, B: False}
|
||||
assert dpll_satisfiable(
|
||||
A | B ) in ({A: True}, {B: True}, {A: True, B: True})
|
||||
assert dpll_satisfiable(
|
||||
(~A | B) & (~B | A) ) in ({A: True, B: True}, {A: False, B: False})
|
||||
assert dpll_satisfiable( (A | B) & (~B | C) ) in ({A: True, B: False},
|
||||
{A: True, C: True}, {B: True, C: True})
|
||||
assert dpll_satisfiable( A & B & C ) == {A: True, B: True, C: True}
|
||||
assert dpll_satisfiable( (A | B) & (A >> B) ) == {B: True}
|
||||
assert dpll_satisfiable( Equivalent(A, B) & A ) == {A: True, B: True}
|
||||
assert dpll_satisfiable( Equivalent(A, B) & ~A ) == {A: False, B: False}
|
||||
|
||||
|
||||
def test_dpll2_satisfiable():
|
||||
A, B, C = symbols('A,B,C')
|
||||
assert dpll2_satisfiable( A & ~A ) is False
|
||||
assert dpll2_satisfiable( A & ~B ) == {A: True, B: False}
|
||||
assert dpll2_satisfiable(
|
||||
A | B ) in ({A: True}, {B: True}, {A: True, B: True})
|
||||
assert dpll2_satisfiable(
|
||||
(~A | B) & (~B | A) ) in ({A: True, B: True}, {A: False, B: False})
|
||||
assert dpll2_satisfiable( (A | B) & (~B | C) ) in ({A: True, B: False, C: True},
|
||||
{A: True, B: True, C: True})
|
||||
assert dpll2_satisfiable( A & B & C ) == {A: True, B: True, C: True}
|
||||
assert dpll2_satisfiable( (A | B) & (A >> B) ) in ({B: True, A: False},
|
||||
{B: True, A: True})
|
||||
assert dpll2_satisfiable( Equivalent(A, B) & A ) == {A: True, B: True}
|
||||
assert dpll2_satisfiable( Equivalent(A, B) & ~A ) == {A: False, B: False}
|
||||
|
||||
|
||||
def test_minisat22_satisfiable():
|
||||
A, B, C = symbols('A,B,C')
|
||||
minisat22_satisfiable = lambda expr: satisfiable(expr, algorithm="minisat22")
|
||||
assert minisat22_satisfiable( A & ~A ) is False
|
||||
assert minisat22_satisfiable( A & ~B ) == {A: True, B: False}
|
||||
assert minisat22_satisfiable(
|
||||
A | B ) in ({A: True}, {B: False}, {A: False, B: True}, {A: True, B: True}, {A: True, B: False})
|
||||
assert minisat22_satisfiable(
|
||||
(~A | B) & (~B | A) ) in ({A: True, B: True}, {A: False, B: False})
|
||||
assert minisat22_satisfiable( (A | B) & (~B | C) ) in ({A: True, B: False, C: True},
|
||||
{A: True, B: True, C: True}, {A: False, B: True, C: True}, {A: True, B: False, C: False})
|
||||
assert minisat22_satisfiable( A & B & C ) == {A: True, B: True, C: True}
|
||||
assert minisat22_satisfiable( (A | B) & (A >> B) ) in ({B: True, A: False},
|
||||
{B: True, A: True})
|
||||
assert minisat22_satisfiable( Equivalent(A, B) & A ) == {A: True, B: True}
|
||||
assert minisat22_satisfiable( Equivalent(A, B) & ~A ) == {A: False, B: False}
|
||||
|
||||
def test_minisat22_minimal_satisfiable():
|
||||
A, B, C = symbols('A,B,C')
|
||||
minisat22_satisfiable = lambda expr, minimal=True: satisfiable(expr, algorithm="minisat22", minimal=True)
|
||||
assert minisat22_satisfiable( A & ~A ) is False
|
||||
assert minisat22_satisfiable( A & ~B ) == {A: True, B: False}
|
||||
assert minisat22_satisfiable(
|
||||
A | B ) in ({A: True}, {B: False}, {A: False, B: True}, {A: True, B: True}, {A: True, B: False})
|
||||
assert minisat22_satisfiable(
|
||||
(~A | B) & (~B | A) ) in ({A: True, B: True}, {A: False, B: False})
|
||||
assert minisat22_satisfiable( (A | B) & (~B | C) ) in ({A: True, B: False, C: True},
|
||||
{A: True, B: True, C: True}, {A: False, B: True, C: True}, {A: True, B: False, C: False})
|
||||
assert minisat22_satisfiable( A & B & C ) == {A: True, B: True, C: True}
|
||||
assert minisat22_satisfiable( (A | B) & (A >> B) ) in ({B: True, A: False},
|
||||
{B: True, A: True})
|
||||
assert minisat22_satisfiable( Equivalent(A, B) & A ) == {A: True, B: True}
|
||||
assert minisat22_satisfiable( Equivalent(A, B) & ~A ) == {A: False, B: False}
|
||||
g = satisfiable((A | B | C),algorithm="minisat22",minimal=True,all_models=True)
|
||||
sol = next(g)
|
||||
first_solution = {key for key, value in sol.items() if value}
|
||||
sol=next(g)
|
||||
second_solution = {key for key, value in sol.items() if value}
|
||||
sol=next(g)
|
||||
third_solution = {key for key, value in sol.items() if value}
|
||||
assert not first_solution <= second_solution
|
||||
assert not second_solution <= third_solution
|
||||
assert not first_solution <= third_solution
|
||||
|
||||
def test_satisfiable():
|
||||
A, B, C = symbols('A,B,C')
|
||||
assert satisfiable(A & (A >> B) & ~B) is False
|
||||
|
||||
|
||||
def test_valid():
|
||||
A, B, C = symbols('A,B,C')
|
||||
assert valid(A >> (B >> A)) is True
|
||||
assert valid((A >> (B >> C)) >> ((A >> B) >> (A >> C))) is True
|
||||
assert valid((~B >> ~A) >> (A >> B)) is True
|
||||
assert valid(A | B | C) is False
|
||||
assert valid(A >> B) is False
|
||||
|
||||
|
||||
def test_pl_true():
|
||||
A, B, C = symbols('A,B,C')
|
||||
assert pl_true(True) is True
|
||||
assert pl_true( A & B, {A: True, B: True}) is True
|
||||
assert pl_true( A | B, {A: True}) is True
|
||||
assert pl_true( A | B, {B: True}) is True
|
||||
assert pl_true( A | B, {A: None, B: True}) is True
|
||||
assert pl_true( A >> B, {A: False}) is True
|
||||
assert pl_true( A | B | ~C, {A: False, B: True, C: True}) is True
|
||||
assert pl_true(Equivalent(A, B), {A: False, B: False}) is True
|
||||
|
||||
# test for false
|
||||
assert pl_true(False) is False
|
||||
assert pl_true( A & B, {A: False, B: False}) is False
|
||||
assert pl_true( A & B, {A: False}) is False
|
||||
assert pl_true( A & B, {B: False}) is False
|
||||
assert pl_true( A | B, {A: False, B: False}) is False
|
||||
|
||||
#test for None
|
||||
assert pl_true(B, {B: None}) is None
|
||||
assert pl_true( A & B, {A: True, B: None}) is None
|
||||
assert pl_true( A >> B, {A: True, B: None}) is None
|
||||
assert pl_true(Equivalent(A, B), {A: None}) is None
|
||||
assert pl_true(Equivalent(A, B), {A: True, B: None}) is None
|
||||
|
||||
# Test for deep
|
||||
assert pl_true(A | B, {A: False}, deep=True) is None
|
||||
assert pl_true(~A & ~B, {A: False}, deep=True) is None
|
||||
assert pl_true(A | B, {A: False, B: False}, deep=True) is False
|
||||
assert pl_true(A & B & (~A | ~B), {A: True}, deep=True) is False
|
||||
assert pl_true((C >> A) >> (B >> A), {C: True}, deep=True) is True
|
||||
|
||||
|
||||
def test_pl_true_wrong_input():
|
||||
from sympy.core.numbers import pi
|
||||
raises(ValueError, lambda: pl_true('John Cleese'))
|
||||
raises(ValueError, lambda: pl_true(42 + pi + pi ** 2))
|
||||
raises(ValueError, lambda: pl_true(42))
|
||||
|
||||
|
||||
def test_entails():
|
||||
A, B, C = symbols('A, B, C')
|
||||
assert entails(A, [A >> B, ~B]) is False
|
||||
assert entails(B, [Equivalent(A, B), A]) is True
|
||||
assert entails((A >> B) >> (~A >> ~B)) is False
|
||||
assert entails((A >> B) >> (~B >> ~A)) is True
|
||||
|
||||
|
||||
def test_PropKB():
|
||||
A, B, C = symbols('A,B,C')
|
||||
kb = PropKB()
|
||||
assert kb.ask(A >> B) is False
|
||||
assert kb.ask(A >> (B >> A)) is True
|
||||
kb.tell(A >> B)
|
||||
kb.tell(B >> C)
|
||||
assert kb.ask(A) is False
|
||||
assert kb.ask(B) is False
|
||||
assert kb.ask(C) is False
|
||||
assert kb.ask(~A) is False
|
||||
assert kb.ask(~B) is False
|
||||
assert kb.ask(~C) is False
|
||||
assert kb.ask(A >> C) is True
|
||||
kb.tell(A)
|
||||
assert kb.ask(A) is True
|
||||
assert kb.ask(B) is True
|
||||
assert kb.ask(C) is True
|
||||
assert kb.ask(~C) is False
|
||||
kb.retract(A)
|
||||
assert kb.ask(C) is False
|
||||
|
||||
|
||||
def test_propKB_tolerant():
|
||||
""""tolerant to bad input"""
|
||||
kb = PropKB()
|
||||
A, B, C = symbols('A,B,C')
|
||||
assert kb.ask(B) is False
|
||||
|
||||
def test_satisfiable_non_symbols():
|
||||
x, y = symbols('x y')
|
||||
assumptions = Q.zero(x*y)
|
||||
facts = Implies(Q.zero(x*y), Q.zero(x) | Q.zero(y))
|
||||
query = ~Q.zero(x) & ~Q.zero(y)
|
||||
refutations = [
|
||||
{Q.zero(x): True, Q.zero(x*y): True},
|
||||
{Q.zero(y): True, Q.zero(x*y): True},
|
||||
{Q.zero(x): True, Q.zero(y): True, Q.zero(x*y): True},
|
||||
{Q.zero(x): True, Q.zero(y): False, Q.zero(x*y): True},
|
||||
{Q.zero(x): False, Q.zero(y): True, Q.zero(x*y): True}]
|
||||
assert not satisfiable(And(assumptions, facts, query), algorithm='dpll')
|
||||
assert satisfiable(And(assumptions, facts, ~query), algorithm='dpll') in refutations
|
||||
assert not satisfiable(And(assumptions, facts, query), algorithm='dpll2')
|
||||
assert satisfiable(And(assumptions, facts, ~query), algorithm='dpll2') in refutations
|
||||
|
||||
def test_satisfiable_bool():
|
||||
from sympy.core.singleton import S
|
||||
assert satisfiable(true) == {true: true}
|
||||
assert satisfiable(S.true) == {true: true}
|
||||
assert satisfiable(false) is False
|
||||
assert satisfiable(S.false) is False
|
||||
|
||||
|
||||
def test_satisfiable_all_models():
|
||||
from sympy.abc import A, B
|
||||
assert next(satisfiable(False, all_models=True)) is False
|
||||
assert list(satisfiable((A >> ~A) & A, all_models=True)) == [False]
|
||||
assert list(satisfiable(True, all_models=True)) == [{true: true}]
|
||||
|
||||
models = [{A: True, B: False}, {A: False, B: True}]
|
||||
result = satisfiable(A ^ B, all_models=True)
|
||||
models.remove(next(result))
|
||||
models.remove(next(result))
|
||||
raises(StopIteration, lambda: next(result))
|
||||
assert not models
|
||||
|
||||
assert list(satisfiable(Equivalent(A, B), all_models=True)) == \
|
||||
[{A: False, B: False}, {A: True, B: True}]
|
||||
|
||||
models = [{A: False, B: False}, {A: False, B: True}, {A: True, B: True}]
|
||||
for model in satisfiable(A >> B, all_models=True):
|
||||
models.remove(model)
|
||||
assert not models
|
||||
|
||||
# This is a santiy test to check that only the required number
|
||||
# of solutions are generated. The expr below has 2**100 - 1 models
|
||||
# which would time out the test if all are generated at once.
|
||||
from sympy.utilities.iterables import numbered_symbols
|
||||
from sympy.logic.boolalg import Or
|
||||
sym = numbered_symbols()
|
||||
X = [next(sym) for i in range(100)]
|
||||
result = satisfiable(Or(*X), all_models=True)
|
||||
for i in range(10):
|
||||
assert next(result)
|
||||
|
||||
|
||||
def test_z3():
|
||||
z3 = import_module("z3")
|
||||
|
||||
if not z3:
|
||||
skip("z3 not installed.")
|
||||
A, B, C = symbols('A,B,C')
|
||||
x, y, z = symbols('x,y,z')
|
||||
assert z3_satisfiable((x >= 2) & (x < 1)) is False
|
||||
assert z3_satisfiable( A & ~A ) is False
|
||||
|
||||
model = z3_satisfiable(A & (~A | B | C))
|
||||
assert bool(model) is True
|
||||
assert model[A] is True
|
||||
|
||||
# test nonlinear function
|
||||
assert z3_satisfiable((x ** 2 >= 2) & (x < 1) & (x > -1)) is False
|
||||
|
||||
|
||||
def test_z3_vs_lra_dpll2():
|
||||
z3 = import_module("z3")
|
||||
if z3 is None:
|
||||
skip("z3 not installed.")
|
||||
|
||||
def boolean_formula_to_encoded_cnf(bf):
|
||||
cnf = CNF.from_prop(bf)
|
||||
enc = EncodedCNF()
|
||||
enc.from_cnf(cnf)
|
||||
return enc
|
||||
|
||||
def make_random_cnf(num_clauses=5, num_constraints=10, num_var=2):
|
||||
assert num_clauses <= num_constraints
|
||||
constraints = make_random_problem(num_variables=num_var, num_constraints=num_constraints, rational=False)
|
||||
clauses = [[cons] for cons in constraints[:num_clauses]]
|
||||
for cons in constraints[num_clauses:]:
|
||||
if isinstance(cons, Unequality):
|
||||
cons = ~cons
|
||||
i = randint(0, num_clauses-1)
|
||||
clauses[i].append(cons)
|
||||
|
||||
clauses = [Or(*clause) for clause in clauses]
|
||||
cnf = And(*clauses)
|
||||
return boolean_formula_to_encoded_cnf(cnf)
|
||||
|
||||
lra_dpll2_satisfiable = lambda x: dpll2_satisfiable(x, use_lra_theory=True)
|
||||
|
||||
for _ in range(50):
|
||||
cnf = make_random_cnf(num_clauses=10, num_constraints=15, num_var=2)
|
||||
|
||||
try:
|
||||
z3_sat = z3_satisfiable(cnf)
|
||||
except z3.z3types.Z3Exception:
|
||||
continue
|
||||
|
||||
lra_dpll2_sat = lra_dpll2_satisfiable(cnf) is not False
|
||||
|
||||
assert z3_sat == lra_dpll2_sat
|
||||
|
||||
def test_issue_27733():
|
||||
x, y = symbols('x,y')
|
||||
clauses = [[1, -3, -2], [5, 7, -8, -6, -4], [-10, -9, 10, 11, -4], [-12, 13, 14], [-10, 9, -6, 11, -4],
|
||||
[16, -15, 18, -19, -17], [11, -6, 10, -9], [9, 11, -10, -9], [2, -3, -1], [-13, 12], [-15, 3, -17],
|
||||
[-16, -15, 19, -17], [-6, -9, 10, 11, -4], [20, -1, -2], [-23, -22, -21], [10, 11, -10, -9],
|
||||
[9, 11, -4, -10], [24, -6, -4], [-14, 12], [-10, -9, 9, -6, 11], [25, -27, -26], [-15, 19, -18, -17],
|
||||
[5, 8, -7, -6, -4], [-30, -29, 28], [12], [14]]
|
||||
|
||||
encoding = {Q.gt(y, i): i for i in range(1, 31) if i != 11 and i != 12}
|
||||
encoding[Q.gt(x, 0)] = 11
|
||||
encoding[Q.lt(x, 0)] = 12
|
||||
|
||||
cnf = EncodedCNF(clauses, encoding)
|
||||
assert satisfiable(cnf, use_lra_theory=True) is False
|
||||
@@ -0,0 +1,440 @@
|
||||
from sympy.core.numbers import Rational, I, oo
|
||||
from sympy.core.relational import Eq
|
||||
from sympy.core.symbol import symbols
|
||||
from sympy.core.singleton import S
|
||||
from sympy.matrices.dense import Matrix
|
||||
from sympy.matrices.dense import randMatrix
|
||||
from sympy.assumptions.ask import Q
|
||||
from sympy.logic.boolalg import And
|
||||
from sympy.abc import x, y, z
|
||||
from sympy.assumptions.cnf import CNF, EncodedCNF
|
||||
from sympy.functions.elementary.trigonometric import cos
|
||||
from sympy.external import import_module
|
||||
|
||||
from sympy.logic.algorithms.lra_theory import LRASolver, UnhandledInput, LRARational, HANDLE_NEGATION
|
||||
from sympy.core.random import random, choice, randint
|
||||
from sympy.core.sympify import sympify
|
||||
from sympy.ntheory.generate import randprime
|
||||
from sympy.core.relational import StrictLessThan, StrictGreaterThan
|
||||
import itertools
|
||||
|
||||
from sympy.testing.pytest import raises, XFAIL, skip
|
||||
|
||||
def make_random_problem(num_variables=2, num_constraints=2, sparsity=.1, rational=True,
|
||||
disable_strict = False, disable_nonstrict=False, disable_equality=False):
|
||||
def rand(sparsity=sparsity):
|
||||
if random() < sparsity:
|
||||
return sympify(0)
|
||||
if rational:
|
||||
int1, int2 = [randprime(0, 50) for _ in range(2)]
|
||||
return Rational(int1, int2) * choice([-1, 1])
|
||||
else:
|
||||
return randint(1, 10) * choice([-1, 1])
|
||||
|
||||
variables = symbols('x1:%s' % (num_variables + 1))
|
||||
constraints = []
|
||||
for _ in range(num_constraints):
|
||||
lhs, rhs = sum(rand() * x for x in variables), rand(sparsity=0) # sparsity=0 bc of bug with smtlib_code
|
||||
options = []
|
||||
if not disable_equality:
|
||||
options += [Eq(lhs, rhs)]
|
||||
if not disable_nonstrict:
|
||||
options += [lhs <= rhs, lhs >= rhs]
|
||||
if not disable_strict:
|
||||
options += [lhs < rhs, lhs > rhs]
|
||||
|
||||
constraints.append(choice(options))
|
||||
|
||||
return constraints
|
||||
|
||||
def check_if_satisfiable_with_z3(constraints):
|
||||
from sympy.external.importtools import import_module
|
||||
from sympy.printing.smtlib import smtlib_code
|
||||
from sympy.logic.boolalg import And
|
||||
boolean_formula = And(*constraints)
|
||||
z3 = import_module("z3")
|
||||
if z3:
|
||||
smtlib_string = smtlib_code(boolean_formula)
|
||||
s = z3.Solver()
|
||||
s.from_string(smtlib_string)
|
||||
res = str(s.check())
|
||||
if res == 'sat':
|
||||
return True
|
||||
elif res == 'unsat':
|
||||
return False
|
||||
else:
|
||||
raise ValueError(f"z3 was not able to check the satisfiability of {boolean_formula}")
|
||||
|
||||
def find_rational_assignment(constr, assignment, iter=20):
|
||||
eps = sympify(1)
|
||||
|
||||
for _ in range(iter):
|
||||
assign = {key: val[0] + val[1]*eps for key, val in assignment.items()}
|
||||
try:
|
||||
for cons in constr:
|
||||
assert cons.subs(assign) == True
|
||||
return assign
|
||||
except AssertionError:
|
||||
eps = eps/2
|
||||
|
||||
return None
|
||||
|
||||
def boolean_formula_to_encoded_cnf(bf):
|
||||
cnf = CNF.from_prop(bf)
|
||||
enc = EncodedCNF()
|
||||
enc.from_cnf(cnf)
|
||||
return enc
|
||||
|
||||
|
||||
def test_from_encoded_cnf():
|
||||
s1, s2 = symbols("s1 s2")
|
||||
|
||||
# Test preprocessing
|
||||
# Example is from section 3 of paper.
|
||||
phi = (x >= 0) & ((x + y <= 2) | (x + 2 * y - z >= 6)) & (Eq(x + y, 2) | (x + 2 * y - z > 4))
|
||||
enc = boolean_formula_to_encoded_cnf(phi)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
assert lra.A.shape == (2, 5)
|
||||
assert str(lra.slack) == '[_s1, _s2]'
|
||||
assert str(lra.nonslack) == '[x, y, z]'
|
||||
assert lra.A == Matrix([[ 1, 1, 0, -1, 0],
|
||||
[-1, -2, 1, 0, -1]])
|
||||
assert {(str(b.var), b.bound, b.upper, b.equality, b.strict) for b in lra.enc_to_boundary.values()} == {('_s1', 2, None, True, False),
|
||||
('_s1', 2, True, False, False),
|
||||
('_s2', -4, True, False, True),
|
||||
('_s2', -6, True, False, False),
|
||||
('x', 0, False, False, False)}
|
||||
|
||||
|
||||
def test_problem():
|
||||
from sympy.logic.algorithms.lra_theory import LRASolver
|
||||
from sympy.assumptions.cnf import CNF, EncodedCNF
|
||||
cons = [-2 * x - 2 * y >= 7, -9 * y >= 7, -6 * y >= 5]
|
||||
cnf = CNF().from_prop(And(*cons))
|
||||
enc = EncodedCNF()
|
||||
enc.from_cnf(cnf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc)
|
||||
lra.assert_lit(1)
|
||||
lra.assert_lit(2)
|
||||
lra.assert_lit(3)
|
||||
is_sat, assignment = lra.check()
|
||||
assert is_sat is True
|
||||
|
||||
|
||||
def test_random_problems():
|
||||
z3 = import_module("z3")
|
||||
if z3 is None:
|
||||
skip("z3 is not installed")
|
||||
|
||||
special_cases = []; x1, x2, x3 = symbols("x1 x2 x3")
|
||||
special_cases.append([x1 - 3 * x2 <= -5, 6 * x1 + 4 * x2 <= 0, -7 * x1 + 3 * x2 <= 3])
|
||||
special_cases.append([-3 * x1 >= 3, Eq(4 * x1, -1)])
|
||||
special_cases.append([-4 * x1 < 4, 6 * x1 <= -6])
|
||||
special_cases.append([-3 * x2 >= 7, 6 * x1 <= -5, -3 * x2 <= -4])
|
||||
special_cases.append([x + y >= 2, x + y <= 1])
|
||||
special_cases.append([x >= 0, x + y <= 2, x + 2 * y - z >= 6]) # from paper example
|
||||
special_cases.append([-2 * x1 - 2 * x2 >= 7, -9 * x1 >= 7, -6 * x1 >= 5])
|
||||
special_cases.append([2 * x1 > -3, -9 * x1 < -6, 9 * x1 <= 6])
|
||||
special_cases.append([-2*x1 < -4, 9*x1 > -9])
|
||||
special_cases.append([-6*x1 >= -1, -8*x1 + x2 >= 5, -8*x1 + 7*x2 < 4, x1 > 7])
|
||||
special_cases.append([Eq(x1, 2), Eq(5*x1, -2), Eq(-7*x2, -6), Eq(9*x1 + 10*x2, 9)])
|
||||
special_cases.append([Eq(3*x1, 6), Eq(x1 - 8*x2, -9), Eq(-7*x1 + 5*x2, 3), Eq(3*x2, 7)])
|
||||
special_cases.append([-4*x1 < 4, 6*x1 <= -6])
|
||||
special_cases.append([-3*x1 + 8*x2 >= -8, -10*x2 > 9, 8*x1 - 4*x2 < 8, 10*x1 - 9*x2 >= -9])
|
||||
special_cases.append([x1 + 5*x2 >= -6, 9*x1 - 3*x2 >= -9, 6*x1 + 6*x2 < -10, -3*x1 + 3*x2 < -7])
|
||||
special_cases.append([-9*x1 < 7, -5*x1 - 7*x2 < -1, 3*x1 + 7*x2 > 1, -6*x1 - 6*x2 > 9])
|
||||
special_cases.append([9*x1 - 6*x2 >= -7, 9*x1 + 4*x2 < -8, -7*x2 <= 1, 10*x2 <= -7])
|
||||
|
||||
feasible_count = 0
|
||||
for i in range(50):
|
||||
if i % 8 == 0:
|
||||
constraints = make_random_problem(num_variables=1, num_constraints=2, rational=False)
|
||||
elif i % 8 == 1:
|
||||
constraints = make_random_problem(num_variables=2, num_constraints=4, rational=False, disable_equality=True,
|
||||
disable_nonstrict=True)
|
||||
elif i % 8 == 2:
|
||||
constraints = make_random_problem(num_variables=2, num_constraints=4, rational=False, disable_strict=True)
|
||||
elif i % 8 == 3:
|
||||
constraints = make_random_problem(num_variables=3, num_constraints=12, rational=False)
|
||||
else:
|
||||
constraints = make_random_problem(num_variables=3, num_constraints=6, rational=False)
|
||||
|
||||
if i < len(special_cases):
|
||||
constraints = special_cases[i]
|
||||
|
||||
if False in constraints or True in constraints:
|
||||
continue
|
||||
|
||||
phi = And(*constraints)
|
||||
if phi == False:
|
||||
continue
|
||||
cnf = CNF.from_prop(phi); enc = EncodedCNF()
|
||||
enc.from_cnf(cnf)
|
||||
assert all(0 not in clause for clause in enc.data)
|
||||
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
s_subs = lra.s_subs
|
||||
|
||||
lra.run_checks = True
|
||||
s_subs_rev = {value: key for key, value in s_subs.items()}
|
||||
lits = {lit for clause in enc.data for lit in clause}
|
||||
|
||||
bounds = [(lra.enc_to_boundary[l], l) for l in lits if l in lra.enc_to_boundary]
|
||||
bounds = sorted(bounds, key=lambda x: (str(x[0].var), x[0].bound, str(x[0].upper))) # to remove nondeterminism
|
||||
|
||||
for b, l in bounds:
|
||||
if lra.result and lra.result[0] == False:
|
||||
break
|
||||
lra.assert_lit(l)
|
||||
|
||||
feasible = lra.check()
|
||||
|
||||
if feasible[0] == True:
|
||||
feasible_count += 1
|
||||
assert check_if_satisfiable_with_z3(constraints) is True
|
||||
cons_funcs = [cons.func for cons in constraints]
|
||||
assignment = feasible[1]
|
||||
assignment = {key.var : value for key, value in assignment.items()}
|
||||
if not (StrictLessThan in cons_funcs or StrictGreaterThan in cons_funcs):
|
||||
assignment = {key: value[0] for key, value in assignment.items()}
|
||||
for cons in constraints:
|
||||
assert cons.subs(assignment) == True
|
||||
|
||||
else:
|
||||
rat_assignment = find_rational_assignment(constraints, assignment)
|
||||
assert rat_assignment is not None
|
||||
else:
|
||||
assert check_if_satisfiable_with_z3(constraints) is False
|
||||
|
||||
conflict = feasible[1]
|
||||
assert len(conflict) >= 2
|
||||
conflict = {lra.enc_to_boundary[-l].get_inequality() for l in conflict}
|
||||
conflict = {clause.subs(s_subs_rev) for clause in conflict}
|
||||
assert check_if_satisfiable_with_z3(conflict) is False
|
||||
|
||||
# check that conflict clause is probably minimal
|
||||
for subset in itertools.combinations(conflict, len(conflict)-1):
|
||||
assert check_if_satisfiable_with_z3(subset) is True
|
||||
|
||||
|
||||
@XFAIL
|
||||
def test_pos_neg_zero():
|
||||
bf = Q.positive(x) & Q.negative(x) & Q.zero(y)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for lit in enc.encoding.values():
|
||||
if lra.assert_lit(lit) is not None:
|
||||
break
|
||||
assert len(lra.enc_to_boundary) == 3
|
||||
assert lra.check()[0] == False
|
||||
|
||||
bf = Q.positive(x) & Q.lt(x, -1)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for lit in enc.encoding.values():
|
||||
if lra.assert_lit(lit) is not None:
|
||||
break
|
||||
assert len(lra.enc_to_boundary) == 2
|
||||
assert lra.check()[0] == False
|
||||
|
||||
bf = Q.positive(x) & Q.zero(x)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for lit in enc.encoding.values():
|
||||
if lra.assert_lit(lit) is not None:
|
||||
break
|
||||
assert len(lra.enc_to_boundary) == 2
|
||||
assert lra.check()[0] == False
|
||||
|
||||
bf = Q.positive(x) & Q.zero(y)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for lit in enc.encoding.values():
|
||||
if lra.assert_lit(lit) is not None:
|
||||
break
|
||||
assert len(lra.enc_to_boundary) == 2
|
||||
assert lra.check()[0] == True
|
||||
|
||||
|
||||
@XFAIL
|
||||
def test_pos_neg_infinite():
|
||||
bf = Q.positive_infinite(x) & Q.lt(x, 10000000) & Q.positive_infinite(y)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for lit in enc.encoding.values():
|
||||
if lra.assert_lit(lit) is not None:
|
||||
break
|
||||
assert len(lra.enc_to_boundary) == 3
|
||||
assert lra.check()[0] == False
|
||||
|
||||
bf = Q.positive_infinite(x) & Q.gt(x, 10000000) & Q.positive_infinite(y)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for lit in enc.encoding.values():
|
||||
if lra.assert_lit(lit) is not None:
|
||||
break
|
||||
assert len(lra.enc_to_boundary) == 3
|
||||
assert lra.check()[0] == True
|
||||
|
||||
bf = Q.positive_infinite(x) & Q.negative_infinite(x)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for lit in enc.encoding.values():
|
||||
if lra.assert_lit(lit) is not None:
|
||||
break
|
||||
assert len(lra.enc_to_boundary) == 2
|
||||
assert lra.check()[0] == False
|
||||
|
||||
|
||||
def test_binrel_evaluation():
|
||||
bf = Q.gt(3, 2)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, conflicts = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
assert len(lra.enc_to_boundary) == 0
|
||||
assert conflicts == [[1]]
|
||||
|
||||
bf = Q.lt(3, 2)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, conflicts = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
assert len(lra.enc_to_boundary) == 0
|
||||
assert conflicts == [[-1]]
|
||||
|
||||
|
||||
def test_negation():
|
||||
assert HANDLE_NEGATION is True
|
||||
bf = Q.gt(x, 1) & ~Q.gt(x, 0)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for clause in enc.data:
|
||||
for lit in clause:
|
||||
lra.assert_lit(lit)
|
||||
assert len(lra.enc_to_boundary) == 2
|
||||
assert lra.check()[0] == False
|
||||
assert sorted(lra.check()[1]) in [[-1, 2], [-2, 1]]
|
||||
|
||||
bf = ~Q.gt(x, 1) & ~Q.lt(x, 0)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for clause in enc.data:
|
||||
for lit in clause:
|
||||
lra.assert_lit(lit)
|
||||
assert len(lra.enc_to_boundary) == 2
|
||||
assert lra.check()[0] == True
|
||||
|
||||
bf = ~Q.gt(x, 0) & ~Q.lt(x, 1)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for clause in enc.data:
|
||||
for lit in clause:
|
||||
lra.assert_lit(lit)
|
||||
assert len(lra.enc_to_boundary) == 2
|
||||
assert lra.check()[0] == False
|
||||
|
||||
bf = ~Q.gt(x, 0) & ~Q.le(x, 0)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for clause in enc.data:
|
||||
for lit in clause:
|
||||
lra.assert_lit(lit)
|
||||
assert len(lra.enc_to_boundary) == 2
|
||||
assert lra.check()[0] == False
|
||||
|
||||
bf = ~Q.le(x+y, 2) & ~Q.ge(x-y, 2) & ~Q.ge(y, 0)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for clause in enc.data:
|
||||
for lit in clause:
|
||||
lra.assert_lit(lit)
|
||||
assert len(lra.enc_to_boundary) == 3
|
||||
assert lra.check()[0] == False
|
||||
assert len(lra.check()[1]) == 3
|
||||
assert all(i > 0 for i in lra.check()[1])
|
||||
|
||||
|
||||
def test_unhandled_input():
|
||||
nan = S.NaN
|
||||
bf = Q.gt(3, nan) & Q.gt(x, nan)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
raises(ValueError, lambda: LRASolver.from_encoded_cnf(enc, testing_mode=True))
|
||||
|
||||
bf = Q.gt(3, I) & Q.gt(x, I)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
raises(UnhandledInput, lambda: LRASolver.from_encoded_cnf(enc, testing_mode=True))
|
||||
|
||||
bf = Q.gt(3, float("inf")) & Q.gt(x, float("inf"))
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
raises(UnhandledInput, lambda: LRASolver.from_encoded_cnf(enc, testing_mode=True))
|
||||
|
||||
bf = Q.gt(3, oo) & Q.gt(x, oo)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
raises(UnhandledInput, lambda: LRASolver.from_encoded_cnf(enc, testing_mode=True))
|
||||
|
||||
# test non-linearity
|
||||
bf = Q.gt(x**2 + x, 2)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
raises(UnhandledInput, lambda: LRASolver.from_encoded_cnf(enc, testing_mode=True))
|
||||
|
||||
bf = Q.gt(cos(x) + x, 2)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
raises(UnhandledInput, lambda: LRASolver.from_encoded_cnf(enc, testing_mode=True))
|
||||
|
||||
@XFAIL
|
||||
def test_infinite_strict_inequalities():
|
||||
# Extensive testing of the interaction between strict inequalities
|
||||
# and constraints containing infinity is needed because
|
||||
# the paper's rule for strict inequalities don't work when
|
||||
# infinite numbers are allowed. Using the paper's rules you
|
||||
# can end up with situations where oo + delta > oo is considered
|
||||
# True when oo + delta should be equal to oo.
|
||||
# See https://math.stackexchange.com/questions/4757069/can-this-method-of-converting-strict-inequalities-to-equisatisfiable-nonstrict-i
|
||||
bf = (-x - y >= -float("inf")) & (x > 0) & (y >= float("inf"))
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for lit in sorted(enc.encoding.values()):
|
||||
if lra.assert_lit(lit) is not None:
|
||||
break
|
||||
assert len(lra.enc_to_boundary) == 3
|
||||
assert lra.check()[0] == True
|
||||
|
||||
|
||||
def test_pivot():
|
||||
for _ in range(10):
|
||||
m = randMatrix(5)
|
||||
rref = m.rref()
|
||||
for _ in range(5):
|
||||
i, j = randint(0, 4), randint(0, 4)
|
||||
if m[i, j] != 0:
|
||||
assert LRASolver._pivot(m, i, j).rref() == rref
|
||||
|
||||
|
||||
def test_reset_bounds():
|
||||
bf = Q.ge(x, 1) & Q.lt(x, 1)
|
||||
enc = boolean_formula_to_encoded_cnf(bf)
|
||||
lra, _ = LRASolver.from_encoded_cnf(enc, testing_mode=True)
|
||||
for clause in enc.data:
|
||||
for lit in clause:
|
||||
lra.assert_lit(lit)
|
||||
assert len(lra.enc_to_boundary) == 2
|
||||
assert lra.check()[0] == False
|
||||
|
||||
lra.reset_bounds()
|
||||
assert lra.check()[0] == True
|
||||
for var in lra.all_var:
|
||||
assert var.upper == LRARational(float("inf"), 0)
|
||||
assert var.upper_from_eq == False
|
||||
assert var.upper_from_neg == False
|
||||
assert var.lower == LRARational(-float("inf"), 0)
|
||||
assert var.lower_from_eq == False
|
||||
assert var.lower_from_neg == False
|
||||
assert var.assign == LRARational(0, 0)
|
||||
assert var.var is not None
|
||||
assert var.col_idx is not None
|
||||
|
||||
|
||||
def test_empty_cnf():
|
||||
cnf = CNF()
|
||||
enc = EncodedCNF()
|
||||
enc.from_cnf(cnf)
|
||||
lra, conflict = LRASolver.from_encoded_cnf(enc)
|
||||
assert len(conflict) == 0
|
||||
assert lra.check() == (True, {})
|
||||
@@ -0,0 +1,3 @@
|
||||
from .dimacs import load_file
|
||||
|
||||
__all__ = ['load_file']
|
||||
@@ -0,0 +1,69 @@
|
||||
"""For reading in DIMACS file format
|
||||
|
||||
www.cs.ubc.ca/~hoos/SATLIB/Benchmarks/SAT/satformat.ps
|
||||
|
||||
"""
|
||||
|
||||
from sympy.core import Symbol
|
||||
from sympy.logic.boolalg import And, Or
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load(s):
|
||||
"""Loads a boolean expression from a string.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
>>> from sympy.logic.utilities.dimacs import load
|
||||
>>> load('1')
|
||||
cnf_1
|
||||
>>> load('1 2')
|
||||
cnf_1 | cnf_2
|
||||
>>> load('1 \\n 2')
|
||||
cnf_1 & cnf_2
|
||||
>>> load('1 2 \\n 3')
|
||||
cnf_3 & (cnf_1 | cnf_2)
|
||||
"""
|
||||
clauses = []
|
||||
|
||||
lines = s.split('\n')
|
||||
|
||||
pComment = re.compile(r'c.*')
|
||||
pStats = re.compile(r'p\s*cnf\s*(\d*)\s*(\d*)')
|
||||
|
||||
while len(lines) > 0:
|
||||
line = lines.pop(0)
|
||||
|
||||
# Only deal with lines that aren't comments
|
||||
if not pComment.match(line):
|
||||
m = pStats.match(line)
|
||||
|
||||
if not m:
|
||||
nums = line.rstrip('\n').split(' ')
|
||||
list = []
|
||||
for lit in nums:
|
||||
if lit != '':
|
||||
if int(lit) == 0:
|
||||
continue
|
||||
num = abs(int(lit))
|
||||
sign = True
|
||||
if int(lit) < 0:
|
||||
sign = False
|
||||
|
||||
if sign:
|
||||
list.append(Symbol("cnf_%s" % num))
|
||||
else:
|
||||
list.append(~Symbol("cnf_%s" % num))
|
||||
|
||||
if len(list) > 0:
|
||||
clauses.append(Or(*list))
|
||||
|
||||
return And(*clauses)
|
||||
|
||||
|
||||
def load_file(location):
|
||||
"""Loads a boolean expression from a file."""
|
||||
s = Path(location).read_text()
|
||||
return load(s)
|
||||
Reference in New Issue
Block a user