class TestSolving(TestCase): ''' Tests basic solving and related functions. ''' def setUp(self): self.mcb = _MCB() self.mit = _MCB() self.ctl = Control(['0']) def tearDown(self): self.mcb = None self.mit = None self.ctl = None def test_solve_result_str(self): ''' Test string representation of solve results. ''' ret = self.ctl.solve() self.assertEqual(str(ret), 'SAT') self.assertRegex(repr(ret), 'SolveResult(.*)') def test_model_str(self): ''' Test string representation of models. ''' self.ctl.add('base', [], 'a.') self.ctl.ground([('base', [])]) with self.ctl.solve(yield_=True) as hnd: for mdl in hnd: self.assertEqual(str(mdl), "a") self.assertRegex(repr(mdl), "Model(.*)") def test_solve_cb(self): ''' Test solving using callback. ''' self.ctl.add("base", [], "1 {a; b} 1. c.") self.ctl.ground([("base", [])]) _check_sat(self, cast(SolveResult, self.ctl.solve(on_model=self.mcb.on_model, yield_=False, async_=False))) self.assertEqual(self.mcb.models, _p(['a', 'c'], ['b', 'c'])) self.assertEqual(self.mcb.last[0], ModelType.StableModel) def test_solve_async(self): ''' Test asynchonous solving. ''' self.ctl.add("base", [], "1 {a; b} 1. c.") self.ctl.ground([("base", [])]) with cast(SolveHandle, self.ctl.solve(on_model=self.mcb.on_model, yield_=False, async_=True)) as hnd: _check_sat(self, hnd.get()) self.assertEqual(self.mcb.models, _p(['a', 'c'], ['b', 'c'])) def test_solve_yield(self): ''' Test solving yielding models. ''' self.ctl.add("base", [], "1 {a; b} 1. c.") self.ctl.ground([("base", [])]) with cast(SolveHandle, self.ctl.solve(on_model=self.mcb.on_model, yield_=True, async_=False)) as hnd: for m in hnd: self.mit.on_model(m) _check_sat(self, hnd.get()) self.assertEqual(self.mcb.models, _p(['a', 'c'], ['b', 'c'])) self.assertEqual(self.mit.models, _p(['a', 'c'], ['b', 'c'])) def test_solve_async_yield(self): ''' Test solving yielding models asynchronously. ''' self.ctl.add("base", [], "1 {a; b} 1. c.") self.ctl.ground([("base", [])]) with self.ctl.solve(on_model=self.mcb.on_model, yield_=True, async_=True) as hnd: while True: hnd.resume() _ = hnd.wait() m = hnd.model() if m is None: break self.mit.on_model(m) _check_sat(self, hnd.get()) self.assertEqual(self.mcb.models, _p(['a', 'c'], ['b', 'c'])) self.assertEqual(self.mit.models, _p(['a', 'c'], ['b', 'c'])) def test_solve_interrupt(self): ''' Test interrupting solving. ''' self.ctl.add("base", [], "1 { p(P,H): H=1..99 } 1 :- P=1..100.\n1 { p(P,H): P=1..100 } 1 :- H=1..99.") self.ctl.ground([("base", [])]) with self.ctl.solve(async_=True) as hnd: hnd.resume() hnd.cancel() ret = hnd.get() self.assertTrue(ret.interrupted) with self.ctl.solve(async_=True) as hnd: hnd.resume() self.ctl.interrupt() ret = hnd.get() self.assertTrue(ret.interrupted) def test_solve_core(self): ''' Test core retrieval. ''' self.ctl.add("base", [], "3 { p(1..10) } 3.") self.ctl.ground([("base", [])]) ass = [] for atom in self.ctl.symbolic_atoms.by_signature("p", 1): ass.append(-atom.literal) ret = cast(SolveResult, self.ctl.solve(on_core=self.mcb.on_core, assumptions=ass)) self.assertTrue(ret.unsatisfiable) self.assertTrue(len(self.mcb.core) > 7) def test_enum(self): ''' Test core retrieval. ''' self.ctl = Control(['0', '-e', 'cautious']) self.ctl.add("base", [], "1 {a; b} 1. c.") self.ctl.ground([("base", [])]) self.ctl.solve(on_model=self.mcb.on_model) self.assertEqual(self.mcb.last[0], ModelType.CautiousConsequences) self.assertEqual([self.mcb.last[1]], _p(['c'])) self.ctl = Control(['0', '-e', 'brave']) self.ctl.add("base", [], "1 {a; b} 1. c.") self.ctl.ground([("base", [])]) self.ctl.solve(on_model=self.mcb.on_model) self.assertEqual(self.mcb.last[0], ModelType.BraveConsequences) self.assertEqual([self.mcb.last[1]], _p(['a', 'b', 'c'])) def test_model(self): ''' Test functions of model. ''' def on_model(m: Model): self.assertTrue(m.contains(Function('a'))) self.assertTrue(m.is_true(cast(SymbolicAtom, m.context.symbolic_atoms[Function('a')]).literal)) self.assertFalse(m.is_true(1000)) self.assertEqual(m.thread_id, 0) self.assertEqual(m.number, 1) self.assertFalse(m.optimality_proven) self.assertEqual(m.cost, [3]) m.extend([Function('e')]) self.assertSequenceEqual(m.symbols(theory=True), [Function('e')]) self.ctl.add("base", [], "a. b. c. #minimize { 1,a:a; 1,b:b; 1,c:c }.") self.ctl.ground([("base", [])]) self.ctl.solve(on_model=on_model) def test_control_clause(self): ''' Test adding clauses while solving. ''' self.ctl.add("base", [], "1 {a; b; c} 1.") self.ctl.ground([("base", [])]) with cast(SolveHandle, self.ctl.solve(on_model=self.mcb.on_model, yield_=True, async_=False)) as hnd: for m in hnd: clause = [] if m.contains(Function('a')): clause.append((Function('b'), False)) else: clause.append((Function('a'), False)) m.context.add_clause(clause) _check_sat(self, hnd.get()) self.assertEqual(len(self.mcb.models), 2) def test_control_nogood(self): ''' Test adding nogoods while solving. ''' self.ctl.add("base", [], "1 {a; b; c} 1.") self.ctl.ground([("base", [])]) with cast(SolveHandle, self.ctl.solve(on_model=self.mcb.on_model, yield_=True, async_=False)) as hnd: for m in hnd: clause = [] if m.contains(Function('a')): clause.append((Function('b'), True)) else: clause.append((Function('a'), True)) m.context.add_nogood(clause) _check_sat(self, hnd.get()) self.assertEqual(len(self.mcb.models), 2)
class VizloControl(Control): def add_to_painter(self, model: Union[Model, PythonModel, Collection[clingo.Symbol]]): """ will register model with the internal painter. On all consecutive calls to paint(), this model will be painted. :param model: the model to add to the painter. :return: """ self.painter.append(PythonModel(model)) def __init__(self, arguments: List[str] = [], logger=None, message_limit: int = 20, print_entire_models=False, atom_draw_maximum=15): self.control = Control(arguments, logger, message_limit) self.painter: List[PythonModel] = list() self.program: ASTProgram = list() self.raw_program: str = "" self.transformer = JustTheRulesTransformer() self._print_changes_only = not print_entire_models self._atom_draw_maximum = atom_draw_maximum def _set_print_only_changes(self, value: bool) -> None: self._print_changes_only = value def ground(self, parts: List[Tuple[str, List[Symbol]]], context: Any = None) -> None: self.control.ground(parts, context) def solve(self, assumptions: List[Union[Tuple[Symbol, bool], int]] = [], on_model=None, on_statistics=None, on_finish=None, yield_: bool = False, async_: bool = False) -> Union[SolveHandle, SolveResult]: return self.control.solve(assumptions, on_model, on_statistics, on_finish, yield_, async_) def load(self, path): prg = "" with open(path) as f: for line in f: prg += line self.program += prg self.control.load(path) def add(self, name: str, parameters: List[str], program: str) -> None: self.raw_program += program self.control.add(name, parameters, program) def find_nodes_corresponding_to_stable_models(self, g, stable_models): correspoding_nodes = set() for model in stable_models: for node in g.nodes(): log(f"{node} {type(node.model)} == {model} {type(model)} -> {set(node.model) == model}" ) if set(node.model) == model and len( g.edges(node)) == 0: # This is a leaf log(f"{node} <-> {model}") correspoding_nodes.add(node) break return correspoding_nodes def prune_graph_leading_to_models(self, graph: nx.DiGraph, models_as_nodes): before = len(graph) relevant_nodes = set() for model in models_as_nodes: for relevant_node in nx.all_simple_paths(graph, INITIAL_EMPTY_SET, model): relevant_nodes.update(relevant_node) all_nodes = set(graph.nodes()) irrelevant_nodes = all_nodes - relevant_nodes graph.remove_nodes_from(irrelevant_nodes) after = len(graph) log(f"Removed {before - after} of {before} nodes ({(before - after) / before})" ) def _make_graph(self, _sort=True): """ Ties together transformation and solving. Transforms the already added program parts and creates a solving tree. :param _sort: Whether the program should be sorted automatically. Setting this to false will likely result into wrong results! :return: :raises ValueError: """ if not len(self.raw_program): raise ValueError("Can't paint an empty program.") else: t = JustTheRulesTransformer() program = t.transform(self.raw_program, _sort) if len(self.painter): universe = get_ground_universe(program) global_assumptions = make_global_assumptions( universe, self.painter) solve_runner = SolveRunner(program, t.rule2signatures) g = solve_runner.make_graph(global_assumptions) else: solve_runner = SolveRunner(program, symbols_in_heads_map=t.rule2signatures) g = solve_runner.make_graph() return g def paint(self, atom_draw_maximum: int = 20, show_entire_model: bool = False, sort_program: bool = True, **kwargs): """ Will create a graph visualization of the solving process. If models have been added using add_to_painter, only the solving paths that lead to these models will be drawn. :param atom_draw_maximum: int The maximum amount of atoms that will be printed for each partial model. (default=20) :param show_entire_model: bool If false, only the atoms that have been added at a solving step will be printed (up to atom_draw_maximum). If true, all atoms will always be printed (up to atom_draw_maximum). (default=False) :param sort_program: If true, the rules of a program will be sorted and grouped by their dependencies. Each set of rules will contain all rules in which each atom in its heads is contained in a head. :param kwargs: kwargs will be forwarded to the visualisation module. See graph.draw() :return: """ if type(atom_draw_maximum) != int: raise ValueError( f"Argument atom_draw_maximum should be an integer (received {atom_draw_maximum})." ) g = self._make_graph(sort_program) display = NetworkxDisplay(g, atom_draw_maximum, not show_entire_model) img = display.draw(**kwargs) return img def _add_and_ground(self, prg): """Short cut for complex add and ground calls, should only be used for debugging purposes.""" self.add("base", [], prg) self.ground([("base", [])]) ################## # Just pass-through stuff ################## @property def configuration(self) -> Configuration: return self.control.configuration @property def is_conflicting(self) -> bool: return self.control.is_conflicting @property def statistics(self) -> dict: return self.control.statistics @property def symbolic_atoms(self) -> SymbolicAtoms: return self.control.symbolic_atoms @property def theory_atoms(self) -> TheoryAtomIter: return self.control.theory_atoms @property def use_enumeration_assumption(self) -> bool: return self.control.use_enumeration_assumption def assign_external(self, external: Union[Symbol, int], truth: Optional[bool], **kwargs) -> None: self.control.assign_external(external, truth, **kwargs) def backend(self) -> Backend: return self.control.backend() def builder(self) -> ProgramBuilder: return self.control.builder() def cleanup(self) -> None: self.control.cleanup() def get_const(self, name: str) -> Optional[Symbol]: return self.control.get_const(name) def interrupt(self): self.control.interrupt() def register_observer(self, observer, replace=False): self.register_observer(observer, replace) def release_external(self, symbol: Union[Symbol, int]) -> None: self.control.release_external(symbol)