Source code for clinguin.server.application.backends.explanation_backend

"""
Module that contains the Explanation Backend.
"""

import textwrap
from functools import cached_property

from clingexplaid.mus import CoreComputer
from clingexplaid.transformers import AssumptionTransformer
from clingo.script import enable_python

from clinguin.server.application.backends.clingo_backend import (
    ClingoBackend,
)
from clinguin.utils.annotations import extends

from ....utils.logger import domctl_log

enable_python()


[docs]class ExplanationBackend(ClingoBackend): """ Extends ClingoBackend. This backend will treat an UNSAT result by adding the Minimal Unsatisfiable Core (MUC) to the domain-state, thus allowing the UI to show the faulty assumptions. """ # --------------------------------------------- # Properties # --------------------------------------------- @property def _assumption_list(self): """ Gets the set of assumptions used for solving. It includes the assumptions from the assumption signatures provided. Warning: Overwrites :meth:`ClingoBackend._assumption_list` """ return self._assumptions.union(self._assumptions_from_signature) # --------------------------------------------- # Initialization # --------------------------------------------- @extends(ClingoBackend) def _init_command_line(self): """ Sets the assumption signature and the transformer used for the input files Attributes: _assumption_sig (List[Tuple[str, int]]): The list of assumption signatures _assumption_transformer (clingexplaid.AssumptionTransformer): The transformer used for the input files """ super()._init_command_line() # pylint: disable= attribute-defined-outside-init self._assumption_sig = [] for a in self._args.assumption_signature or []: try: name, arity = a.split(",") self._assumption_sig.append((name, int(arity))) except Exception as ex: raise ValueError( "Argument assumption_signature must have format name,arity" ) from ex self._assumption_transformer = AssumptionTransformer( signatures=self._assumption_sig ) @extends(ClingoBackend) def _init_ds_constructors(self): super()._init_ds_constructors() self._add_domain_state_constructor("_ds_mus") @extends(ClingoBackend) def _load_file(self, f): """ Loads a file into the control. Transforms the program to add choices around assumption signatures. Arguments: f (str): The file path """ transformed_program = self._assumption_transformer.parse_files([f]) self._logger.debug(domctl_log(f'domctl.add("base", [], {transformed_program})')) self._ctl.add("base", [], transformed_program) # --------------------------------------------- # Solving # --------------------------------------------- @extends(ClingoBackend) def _ground(self, program="base", arguments=None): """ Sets the list of assumptions that were taken from the input files using the assumption_signature. Attributes: _assumptions_from_signature (Set[Tuple(str,bool)]): The set of assumptions from the assumption signatures arguments (list, optional): The list of arguments to ground the program. Defaults to an empty list. """ super()._ground(program, arguments) # pylint: disable= attribute-defined-outside-init transformer_assumptions = self._assumption_transformer.get_assumption_symbols( self._ctl, arguments=self._ctl_arguments_list ) self._assumptions_from_signature = [(a, True) for a in transformer_assumptions] # --------------------------------------------- # Class methods # --------------------------------------------- @classmethod @extends(ClingoBackend) def register_options(cls, parser): """ Registers command line options for ClingraphBackend. """ ClingoBackend.register_options(parser) parser.add_argument( "--assumption-signature", help=textwrap.dedent( """\ Signatures that will be considered as true assumptions. Must be have format name,arity""" ), nargs="+", metavar="", ) # --------------------------------------------- # Domain state # --------------------------------------------- @cached_property def _ds_mus(self): """ Adds information about the Minimal Unsatisfiable Set (MUS) Includes predicate ``_clinguin_mus/1`` for every assumption in the MUC It uses a cache that is erased after an operation makes changes in the control. """ prg = "#defined _clinguin_mus/1. " if self._unsat_core is not None: self._logger.info("UNSAT Answer, will add explanation") mus_core = CoreComputer(self._ctl, self._assumption_list).shrink() prg += " ".join([f"_clinguin_mus({str(s)})." for s, _ in mus_core]) + "\n" return prg