clingo-hmknf-test/propagator.py

186 lines
5.7 KiB
Python
Raw Normal View History

2022-06-25 18:57:56 -06:00
from copy import deepcopy
from itertools import chain
2022-06-27 17:26:35 -06:00
from operator import getitem
2022-06-25 18:57:56 -06:00
from typing import Iterable, List, Tuple
2022-06-27 17:26:35 -06:00
from functools import partial
2022-06-25 18:57:56 -06:00
import clingo
2022-06-27 17:26:35 -06:00
from clingo import (
PropagateControl,
PropagateInit,
PropagateControl,
PropagatorCheckMode,
Assignment,
Symbol,
Control,
TheoryAtom,
)
"""
API notes:
add_clause is disjunctive
add_nogood is conjunctive
defined appear in a rule head
No way to strict atoms except through adding nogoods/clauses?
"""
2022-06-25 18:57:56 -06:00
import ontology as O
class Ontology:
def init(self, init: PropagateInit) -> None:
init.check_mode = PropagatorCheckMode.Total
2022-06-27 17:26:35 -06:00
2022-06-25 18:57:56 -06:00
self.assignment = dict()
self.symbolic_atoms = {
init.solver_literal(atom.literal): str(atom.symbol)
for atom in init.symbolic_atoms
}
self.theory_atoms = {
init.solver_literal(atom.literal): atom.elements[0].terms[0].name
for atom in init.theory_atoms
}
2022-06-27 17:26:35 -06:00
self.symbolic_atoms_inv = {v: k for k, v in self.symbolic_atoms.items()}
self.theory_atoms_inv = {v: k for k, v in self.theory_atoms.items()}
2022-06-25 18:57:56 -06:00
# Might only need to watch just theory atoms / just symbol atoms but for now
2022-06-27 17:26:35 -06:00
# watching everything is easier
2022-06-25 18:57:56 -06:00
for lit in chain(self.symbolic_atoms, self.theory_atoms):
init.add_watch(lit)
2022-06-27 17:26:35 -06:00
# Could add these with additional rules, but I think that will change the semantics of the O atoms.
# An ontology atom must be true if it's regular counterpart is also true
# The opposite direction is already enforced by rules.
for theory_atom in self.theory_atoms_inv:
theory_lit = self.theory_atoms_inv[theory_atom]
symbolic_lit = self.symbolic_atoms_inv[theory_atom]
init.add_clause((-symbolic_lit, theory_lit))
2022-06-25 18:57:56 -06:00
def truthy_atoms_text(self):
2022-06-27 17:26:35 -06:00
return (
str(self.lookup_solver_lit(atom)[0])
for atom in self.assignment
if self.assignment[atom]
)
2022-06-25 18:57:56 -06:00
def atom_text_to_dl_atom(self, atoms: Iterable[str]) -> Iterable[int]:
2022-06-27 17:26:35 -06:00
return (
lit
for atom in atoms
if (lit := self.theory_atoms_inv.get(atom)) is not None
)
2022-06-25 18:57:56 -06:00
def falsey_atoms_text(self):
2022-06-27 17:26:35 -06:00
return (
str(self.lookup_solver_lit(atom)[0])
for atom in self.assignment
if not self.assignment[atom]
)
def assign_nogood_true(self, pcontrol: PropagateControl, lits: Iterable[int]):
not_lits = (-lit for lit in lits)
assignment = (
lit if is_pos else -lit
for lit, is_pos in chain(
self.symbolic_atoms.items(), self.theory_atoms.items()
)
)
pcontrol.add_nogood(chain(assignment, not_lits))
2022-06-25 18:57:56 -06:00
def propagate(self, pcontrol: PropagateControl, changes) -> None:
2022-06-27 17:26:35 -06:00
print("propagate: ", end="")
self.print_assignment()
2022-06-25 18:57:56 -06:00
for change in changes:
atom = abs(change)
assert atom not in self.assignment
self.assignment[atom] = change >= 0
2022-06-27 17:26:35 -06:00
2022-06-25 18:57:56 -06:00
in_atoms = set(self.truthy_atoms_text())
2022-06-27 17:26:35 -06:00
out_atoms = set(self.atom_text_to_dl_atom(O.propagate(in_atoms)))
2022-06-25 18:57:56 -06:00
new_atoms = out_atoms - in_atoms
2022-06-27 17:26:35 -06:00
self.assign_nogood_true(pcontrol, new_atoms)
pcontrol.propagate()
2022-06-25 18:57:56 -06:00
def undo(self, thread_id: int, assignment: Assignment, changes: List[int]):
for change in changes:
atom = abs(change)
del self.assignment[atom]
def check(self, pcontrol: PropagateControl) -> None:
2022-06-27 17:26:35 -06:00
print("check: ", end="")
2022-06-25 18:57:56 -06:00
self.print_assignment()
def print_assignment(self):
print("assignment: ", end="")
for lit in self.assignment:
lit = -lit if not self.assignment[lit] else lit
print(self.solver_lit_text(lit), end=" ")
print()
def is_theory_atom(self, lit: int):
_, _, a = self.lookup_solver_lit(lit)
return a
def solver_lit_text(self, lit: int):
symbol, is_neg, is_theory = self.lookup_solver_lit(lit)
if symbol is None:
return None
theory = "O: " if is_theory else ""
neg = "not " if is_neg else ""
return f"({theory}{neg}{symbol})"
def lookup_solver_lit(self, lit: int) -> Tuple[Symbol, bool, bool]:
atom = abs(lit)
if (atom_symb := self.symbolic_atoms.get(atom, None)) is not None:
return atom_symb, lit < 0, False
if (theo_symbol := self.theory_atoms.get(atom, None)) is not None:
return theo_symbol, lit < 0, True
return None, False, False
2022-06-27 17:26:35 -06:00
program = """
2022-06-25 18:57:56 -06:00
a :- not b.
b :- not a.
"""
# Need to ground program before we can look up symbolic atoms and
# Having two waves of grounding might be having some weird effects
# So we parse and ground and then revert back to text then reparse
def add_external_atoms(program: str) -> str:
control = clingo.Control(["0"])
2022-06-27 17:26:35 -06:00
control.add("base", [], program.replace("\n", ""))
2022-06-25 18:57:56 -06:00
control.ground([("base", [])])
theory_grammar = """
#theory o {
kterm {- : 0, unary };
&o/0 : kterm, any
}.
2022-06-27 17:26:35 -06:00
"""
2022-06-25 18:57:56 -06:00
external_atoms = "\n".join(
2022-06-27 17:26:35 -06:00
f"{atom} :- &o{{{atom}}}."
for atom in (str(atom.symbol) for atom in control.symbolic_atoms)
2022-06-25 18:57:56 -06:00
)
return theory_grammar + program + external_atoms
program = add_external_atoms(program)
2022-06-27 17:26:35 -06:00
print(program)
2022-06-25 18:57:56 -06:00
control = clingo.Control(["0"])
propagator = Ontology()
control.register_propagator(propagator)
control.add("base", [], program)
control.ground([("base", [])])
# control.add("external", [], theory_grammar + external_atoms)
# control.ground([("external", [])])
with control.solve(yield_=True) as solve_handle:
for model in solve_handle:
print("answer set:", model)