This commit is contained in:
Spencer Killen 2022-06-27 17:26:35 -06:00
parent 8e6e9edf64
commit 99a9a6e6a1
No known key found for this signature in database
GPG Key ID: F5DA11602FA5BE3D
4 changed files with 75 additions and 24 deletions

View File

@ -1,3 +1,4 @@
{ {
"discord.enabled": true "discord.enabled": true,
"python.formatting.provider": "black"
} }

Binary file not shown.

View File

@ -17,3 +17,4 @@ def propagate(atoms: Set[str]) -> Set[str]:
def check(atoms: Set[str]) -> bool: def check(atoms: Set[str]) -> bool:
atoms = atoms.intersection(DL_ATOMS) atoms = atoms.intersection(DL_ATOMS)
return atoms in models return atoms in models

View File

@ -1,9 +1,29 @@
from copy import deepcopy from copy import deepcopy
from itertools import chain from itertools import chain
from operator import itemgetter from operator import getitem
from typing import Iterable, List, Tuple from typing import Iterable, List, Tuple
from functools import partial
import clingo import clingo
from clingo import PropagateControl, PropagateInit, PropagateControl, PropagatorCheckMode, Assignment, Symbol, Control, TheoryAtom 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?
"""
import ontology as O import ontology as O
@ -22,37 +42,68 @@ class Ontology:
init.solver_literal(atom.literal): atom.elements[0].terms[0].name init.solver_literal(atom.literal): atom.elements[0].terms[0].name
for atom in init.theory_atoms for atom in init.theory_atoms
} }
self.symbolic_atoms_inv = { v: k for k, v in self.symbolic_atoms.items() } 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() } self.theory_atoms_inv = {v: k for k, v in self.theory_atoms.items()}
# Might only need to watch just theory atoms / just symbol atoms but for now # Might only need to watch just theory atoms / just symbol atoms but for now
# watching everything is easier # watching everything is easier
for lit in chain(self.symbolic_atoms, self.theory_atoms): for lit in chain(self.symbolic_atoms, self.theory_atoms):
init.add_watch(lit) init.add_watch(lit)
# 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))
def truthy_atoms_text(self): def truthy_atoms_text(self):
return (str(self.lookup_solver_lit(atom)[0]) for atom in self.assignment if self.assignment[atom]) return (
str(self.lookup_solver_lit(atom)[0])
for atom in self.assignment
if self.assignment[atom]
)
def atom_text_to_dl_atom(self, atoms: Iterable[str]) -> Iterable[int]: def atom_text_to_dl_atom(self, atoms: Iterable[str]) -> Iterable[int]:
return map(self.theory_atoms.get, atoms) return (
lit
for atom in atoms
if (lit := self.theory_atoms_inv.get(atom)) is not None
)
def falsey_atoms_text(self): def falsey_atoms_text(self):
return (str(self.lookup_solver_lit(atom)[0]) for atom in self.assignment if not self.assignment[atom]) 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))
def propagate(self, pcontrol: PropagateControl, changes) -> None: def propagate(self, pcontrol: PropagateControl, changes) -> None:
print("propagate: ", end="")
self.print_assignment()
for change in changes: for change in changes:
atom = abs(change) atom = abs(change)
assert atom not in self.assignment assert atom not in self.assignment
self.assignment[atom] = change >= 0 self.assignment[atom] = change >= 0
in_atoms = set(self.truthy_atoms_text()) in_atoms = set(self.truthy_atoms_text())
out_atoms = self.atom_text_to_dl_atom(O.propagate()) out_atoms = set(self.atom_text_to_dl_atom(O.propagate(in_atoms)))
new_atoms = out_atoms - in_atoms new_atoms = out_atoms - in_atoms
new_lits = self.atom_text_to_dl_atom(new_atoms)
self.assign_nogood_true(pcontrol, new_atoms)
pcontrol.propagate()
def undo(self, thread_id: int, assignment: Assignment, changes: List[int]): def undo(self, thread_id: int, assignment: Assignment, changes: List[int]):
for change in changes: for change in changes:
@ -60,9 +111,9 @@ class Ontology:
del self.assignment[atom] del self.assignment[atom]
def check(self, pcontrol: PropagateControl) -> None: def check(self, pcontrol: PropagateControl) -> None:
print("check: ", end="")
self.print_assignment() self.print_assignment()
def print_assignment(self): def print_assignment(self):
print("assignment: ", end="") print("assignment: ", end="")
for lit in self.assignment: for lit in self.assignment:
@ -82,7 +133,6 @@ class Ontology:
neg = "not " if is_neg else "" neg = "not " if is_neg else ""
return f"({theory}{neg}{symbol})" return f"({theory}{neg}{symbol})"
def lookup_solver_lit(self, lit: int) -> Tuple[Symbol, bool, bool]: def lookup_solver_lit(self, lit: int) -> Tuple[Symbol, bool, bool]:
atom = abs(lit) atom = abs(lit)
if (atom_symb := self.symbolic_atoms.get(atom, None)) is not None: if (atom_symb := self.symbolic_atoms.get(atom, None)) is not None:
@ -92,10 +142,9 @@ class Ontology:
return None, False, False return None, False, False
program=""" program = """
a :- not b. a :- not b.
b :- not a. b :- not a.
c :- not d.
""" """
# Need to ground program before we can look up symbolic atoms and # Need to ground program before we can look up symbolic atoms and
@ -103,7 +152,7 @@ c :- not d.
# So we parse and ground and then revert back to text then reparse # So we parse and ground and then revert back to text then reparse
def add_external_atoms(program: str) -> str: def add_external_atoms(program: str) -> str:
control = clingo.Control(["0"]) control = clingo.Control(["0"])
control.add("base", [], program) control.add("base", [], program.replace("\n", ""))
control.ground([("base", [])]) control.ground([("base", [])])
theory_grammar = """ theory_grammar = """
@ -111,15 +160,16 @@ def add_external_atoms(program: str) -> str:
kterm {- : 0, unary }; kterm {- : 0, unary };
&o/0 : kterm, any &o/0 : kterm, any
}. }.
""" """
external_atoms = "\n".join( external_atoms = "\n".join(
f"{atom} :- &o{{{atom}}}." for atom in f"{atom} :- &o{{{atom}}}."
(str(atom.symbol) for atom in control.symbolic_atoms) for atom in (str(atom.symbol) for atom in control.symbolic_atoms)
) )
return theory_grammar + program + external_atoms return theory_grammar + program + external_atoms
program = add_external_atoms(program) program = add_external_atoms(program)
print(program)
control = clingo.Control(["0"]) control = clingo.Control(["0"])
propagator = Ontology() propagator = Ontology()
control.register_propagator(propagator) control.register_propagator(propagator)
@ -133,4 +183,3 @@ control.ground([("base", [])])
with control.solve(yield_=True) as solve_handle: with control.solve(yield_=True) as solve_handle:
for model in solve_handle: for model in solve_handle:
print("answer set:", model) print("answer set:", model)