def test_backend(self): ''' Test backend via observer. ''' ctl = Control() obs = TestObserverBackend(self) ctl.register_observer(obs) with ctl.backend() as backend: self.assertIn('init_program', obs.called) self.assertIn('begin_step', obs.called) backend.add_atom() backend.add_atom(Function('a')) backend.add_rule([1], [2, 3], True) self.assertIn('rule', obs.called) backend.add_weight_rule([2], 1, [(2, 3), (4, 5)]) self.assertIn('weight_rule', obs.called) backend.add_minimize(0, [(2, 3), (4, 5)]) self.assertIn('minimize', obs.called) backend.add_project([2, 4]) self.assertIn('project', obs.called) backend.add_heuristic(2, HeuristicType.Level, 5, 7, [1, 3]) self.assertIn('heuristic', obs.called) backend.add_assume([2, 3]) self.assertIn('assume', obs.called) backend.add_acyc_edge(1, 2, [3, 4]) self.assertIn('acyc_edge', obs.called) backend.add_external(3, TruthValue.Release) self.assertIn('external', obs.called) self.assertIn('output_atom', obs.called) ctl.solve() self.assertIn('end_step', obs.called)
def _check(self, prg, prg10, prg_str): ''' Check various ways to remap a program. 1. No remapping. 2. Identity remapping via Backend and Control. 3. Remapping via Backend and Control. 4. Remapping via remap function without Backend and Control. 5. Remap a program using the Remapping class. ''' self.assertEqual(self.prg, prg) self.assertEqual(str(self.prg), prg_str) r_prg = _remap(self.prg) self.assertEqual(self.prg, r_prg) self.assertEqual(str(r_prg), prg_str) r_prg10 = _remap(self.prg, _plus10) self.assertEqual(r_prg10, prg10) self.assertEqual(str(r_prg10), prg_str) ra_prg10 = self.prg.copy().remap(_plus10) self.assertEqual(ra_prg10, prg10) self.assertEqual(str(ra_prg10), prg_str) # note that the backend below is just used as an atom generator ctl = Control() with ctl.backend() as b: for _ in range(10): b.add_atom() rm_prg = prg.copy().remap(Remapping(b, self.prg.output_atoms, self.prg.facts)) self.assertEqual(str(rm_prg), prg_str)
def _remap(prg: Program, mapping=None): ''' Add the given program to a backend passing it through an observer and then return the observer program. The resulting program is initialized with the symbols from the orginial program. ''' ctl, chk = Control(), Program() # note that output atoms are not passed to the backend if mapping is None: chk.output_atoms = prg.output_atoms chk.shows = prg.shows else: chk.output_atoms = {mapping(lit): sym for lit, sym in prg.output_atoms.items()} chk.shows = [cast(Show, remap(x, mapping)) for x in prg.shows] chk.facts = prg.facts ctl.register_observer(ProgramObserver(chk)) with ctl.backend() as b: prg.add_to_backend(b, mapping) return chk
def test_aux_lit(self): ''' Test printing of auxiliary literals. ''' out, out10 = self._add_atoms('a', 'b', 'c') self.obs.rule(False, [4], [1]) self.assertEqual(self.prg, Program( output_atoms=out, rules=[Rule(choice=False, head=[4], body=[1])])) self.assertEqual(str(self.prg), '__x4 :- a.') prg10 = _remap(self.prg, _plus10) self.assertEqual(prg10, Program( output_atoms=out10, rules=[Rule(choice=False, head=[14], body=[11])])) self.assertEqual(str(prg10), '__x14 :- a.') ctl = Control() with ctl.backend() as b: b.add_atom() rm_prg = self.prg.copy().remap(Remapping(b, self.prg.output_atoms, self.prg.facts)) self.assertEqual(str(rm_prg), '__x5 :- a.')
class TestSymbolicBackend(TestCase): ''' Tests for the ymbolic symbolic_backend. ''' def setUp(self): self.prg = Program() self.obs = ProgramObserver(self.prg) self.ctl = Control(message_limit=0) self.ctl.register_observer(self.obs) def test_add_acyc_edge(self): ''' Test edge statement. ''' a = Function("a", [Function("c1")]) b = Function("b", [Function("c2")]) c = Function("c", [Function("c3")]) with SymbolicBackend(self.ctl.backend()) as symbolic_backend: symbolic_backend.add_acyc_edge(1, 3, [a], [b, c]) self.assertEqual(str(self.prg), "#edge (1,3): a(c1), not b(c2), not c(c3).") def test_add_assume(self): ''' Test assumptions. ''' a = Function("a", [Function("c1")]) b = Function("b", [Function("c2")]) c = Function("c", [Function("c3")]) with SymbolicBackend(self.ctl.backend()) as symbolic_backend: symbolic_backend.add_assume([a, b, c]) self.assertEqual(str(self.prg), "% assumptions: a(c1), b(c2), c(c3)") def test_add_external(self): ''' Test external statement. ''' a = Function("a", [Function("c1")]) with SymbolicBackend(self.ctl.backend()) as symbolic_backend: symbolic_backend.add_external(a, TruthValue.True_) self.assertEqual(str(self.prg), "#external a(c1). [True]") def test_add_heuristic(self): ''' Test heuristic statement. ''' a = Function("a", [Function("c1")]) b = Function("b", [Function("c2")]) c = Function("c", [Function("c3")]) with SymbolicBackend(self.ctl.backend()) as symbolic_backend: symbolic_backend.add_heuristic(a, HeuristicType.Level, 2, 3, [b], [c]) self.assertEqual(str(self.prg), "#heuristic a(c1): b(c2), not c(c3). [2@3, Level]") def test_add_minimize(self): ''' Test minimize statement. ''' a = Function("a", [Function("c1")]) b = Function("b", [Function("c2")]) c = Function("c", [Function("c3")]) with SymbolicBackend(self.ctl.backend()) as symbolic_backend: symbolic_backend.add_minimize(1, [(a, 3), (b, 5)], [(c, 7)]) self.assertEqual( str(self.prg), "#minimize{3@1,0: a(c1); 5@1,1: b(c2); 7@1,2: not c(c3)}.") def test_add_project(self): ''' Test project statements. ''' a = Function("a", [Function("c1")]) b = Function("b", [Function("c2")]) c = Function("c", [Function("c3")]) with SymbolicBackend(self.ctl.backend()) as symbolic_backend: symbolic_backend.add_project([a, b, c]) self.assertEqual(str(self.prg), "#project a(c1).\n#project b(c2).\n#project c(c3).") def test_add_empty_project(self): ''' Test project statements. ''' with SymbolicBackend(self.ctl.backend()) as symbolic_backend: symbolic_backend.add_project([]) self.assertEqual(str(self.prg), "#project x: #false.") def test_add_rule(self): ''' Test simple rules. ''' a = Function("a", [Function("c1")]) b = Function("b", [Function("c2")]) c = Function("c", [Function("c3")]) with SymbolicBackend(self.ctl.backend()) as symbolic_backend: symbolic_backend.add_rule([a], [b], [c]) self.assertEqual(str(self.prg), "a(c1) :- b(c2), not c(c3).") def test_add_choice_rule(self): ''' Test choice rules. ''' a = Function("a", [Function("c1")]) b = Function("b", [Function("c2")]) c = Function("c", [Function("c3")]) with SymbolicBackend(self.ctl.backend()) as symbolic_backend: symbolic_backend.add_rule([a], [b], [c], choice=True) self.assertEqual(str(self.prg), "{a(c1)} :- b(c2), not c(c3).") def test_add_weight_rule(self): ''' Test weight rules. ''' a = Function("a", [Function("c1")]) b = Function("b", [Function("c2")]) c = Function("c", [Function("c3")]) with SymbolicBackend(self.ctl.backend()) as symbolic_backend: symbolic_backend.add_weight_rule([a], 3, [(b, 5)], [(c, 7)]) self.assertEqual(str(self.prg), "a(c1) :- 3{5,0: b(c2), 7,1: not c(c3)}.") def test_add_weight_choice_rule(self): ''' Test weight rules that are also choice rules. ''' a = Function("a", [Function("c1")]) b = Function("b", [Function("c2")]) c = Function("c", [Function("c3")]) with SymbolicBackend(self.ctl.backend()) as symbolic_backend: symbolic_backend.add_weight_rule([a], 3, [(b, 5)], [(c, 7)], choice=True) self.assertEqual(str(self.prg), "{a(c1)} :- 3{5,0: b(c2), 7,1: not c(c3)}.")
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)