├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── examples ├── diamond.lp ├── diamond_with_mute.lp ├── dont_drive_drunk.lp ├── pool_and_choice.lp └── pool_and_choice2.lp ├── setup.py ├── tests ├── test_Preprocessor.py ├── test_Utils.py ├── test_Utils │ ├── test_show_all_input │ ├── test_show_all_output │ ├── test_trace_all_input │ ├── test_trace_all_output │ ├── test_trace_input │ └── test_trace_output ├── test_xclingo.py └── test_xclingo │ ├── count_aggregate.lp │ ├── expected_count_aggregate.txt │ ├── expected_ignore_shows.txt │ └── ignore_shows.lp └── xclingo ├── __init__.py ├── __main__.py ├── _main.py ├── _version.py ├── explanation ├── __init__.py └── _explanation.py ├── preprocessor ├── __init__.py ├── _preprocessor.py └── _utils.py └── xclingo_lp ├── __init__.py ├── autotrace_all.lp ├── autotrace_facts.lp └── xclingo.lp /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | Makefile 132 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Brais Muñiz Castro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include xclingo/xclingo_lp/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xclingo 2 | 3 | A tool for explaining and debugging Answer Set Programs. 4 | 5 | ***IMPORTANT:*** This is a new version of [xclingo](https://github.com/bramucas/xclingo). This version is intended to replace the previous one in the future. 6 | 7 | **xclingo** is a clingo-based tool which produces text explanations for the solutions of ASP programs. The original program must be annotated with clingo-friendly annotations and then provided to xclingo. 8 | 9 | All the directives start with a ```%``` character, so they are recognized by clingo as comments and therefore they don't modify the meaning of the original program. In other words: a xclingo-annotated ASP program will still produce the original output when called with clingo. 10 | 11 | ## Installation 12 | *Install with python3* 13 | 14 | ```bash 15 | python3 -m pip install xclingo 16 | ``` 17 | 18 | ## Short usage 19 | xclingo must be provided with a maximum number of solutions to be computed for the original program and with the maximum number of explanations to be printed for each solution. 20 | 21 | An example (all the solutions and 2 explanations): 22 | ``` 23 | xclingo -n 0 2 examples/drive.lp 24 | ``` 25 | 26 | Defaults are 1 solution and 1 explanation. 27 | 28 | ## Example 29 | 30 | We have any ASP program: 31 | ``` 32 | % examples/dont_drive_drunk.lp 33 | 34 | person(gabriel;clare). 35 | 36 | drive(gabriel). 37 | alcohol(gabriel, 40). 38 | resist(gabriel). 39 | 40 | drive(clare). 41 | alcohol(clare, 5). 42 | 43 | punish(P) :- drive(P), alcohol(P,A), A>30, person(P). 44 | punish(P) :- resist(P), person(P). 45 | 46 | sentence(P, prison) :- punish(P). 47 | sentence(P, innocent) :- person(P), not punish(P). 48 | ``` 49 | 50 | And we write some special comments: 51 | ``` 52 | % examples/dont_drive_drunk.lp 53 | 54 | person(gabriel;clare). 55 | 56 | drive(gabriel). 57 | alcohol(gabriel, 40). 58 | resist(gabriel). 59 | 60 | drive(clare). 61 | alcohol(clare, 5). 62 | 63 | %!trace_rule {"% drove drunk", P} 64 | punish(P) :- drive(P), alcohol(P,A), A>30, person(P). 65 | 66 | %!trace_rule {"% resisted to authority", P} 67 | punish(P) :- resist(P), person(P). 68 | 69 | %!trace_rule {"% goes to prison",P} 70 | sentence(P, prison) :- punish(P). 71 | 72 | %!trace_rule {"% is innocent by default",P} 73 | sentence(P, innocent) :- person(P), not punish(P). 74 | 75 | %!trace {"% alcohol's level is %",P,A} alcohol(P,A). 76 | %!trace {"% was drunk",P} alcohol(P,A). 77 | 78 | %!show_trace sentence(P,S). 79 | ``` 80 | We will call those comments *annotations* from now on. 81 | 82 | Now we can obtain the answer sets of the (annotated) program with clingo (```clingo -n 0 examples/dont_drive_drunk.lp```): 83 | ``` 84 | Answer: 1 85 | person(gabriel) person(clare) alcohol(gabriel,40) alcohol(clare,5) drive(gabriel) drive(clare) punish(gabriel) resist(gabriel) sentence(clare,innocent) sentence(gabriel,prison) 86 | SATISFIABLE 87 | ``` 88 | 89 | But also very fashion natural language explanations of with xclingo (```xclingo -n 0 0 examples/dont_drive_drunk.lp```): 90 | ``` 91 | Answer 1 92 | * 93 | |__clare is innocent by default 94 | 95 | * 96 | |__gabriel goes to prison 97 | | |__gabriel drove drunk 98 | | | |__gabriel alcohol's level is 40;gabriel was drunk 99 | 100 | * 101 | |__gabriel goes to prison 102 | | |__gabriel resisted to authority 103 | ``` 104 | 105 | *Note:* for this small example almost all the rules and facts of the program have its own text label (this is, they have been *labelled*). Actually, only labelled rules are used to build the explanations so you can still obtain clear, short explanations even for most complex ASP programs. 106 | 107 | ## Annotations 108 | 109 | ### %!trace_rule 110 | Assigns a text to the atom in the head of a rule. 111 | ``` 112 | %!trace_rule {"% resisted to authority", P} 113 | punish(P) :- resist(P), person(P). 114 | ``` 115 | 116 | ### %!trace 117 | Assigns a text to the set of atoms produced by a conditional atom. 118 | ``` 119 | %!trace {"% alcohol's level is above permitted (%)",P,A} alcohol(P,A) : A>40. 120 | ``` 121 | 122 | ### %!show_trace 123 | Selects which atoms should be explained via conditional atoms. 124 | ``` 125 | %!show_trace sentence(P,S). 126 | ``` 127 | 128 | ### %!mute 129 | Mark some atoms as *untraceable*. Therefore, explanations will not include them, nor will atoms cause them. 130 | ``` 131 | %!mute punish(P) : vip_person(P). 132 | ``` 133 | 134 | ## Usage 135 | 136 | ``` 137 | usage: xclingo [-h] [--version] [--only-translate | --only-translate-annotations | --only-explanation-atoms] [--auto-tracing {none,facts,all}] [-n N N] infiles [infiles ...] 138 | 139 | Tool for explaining (and debugging) ASP programs 140 | 141 | positional arguments: 142 | infiles ASP program 143 | 144 | optional arguments: 145 | -h, --help show this help message and exit 146 | --version Prints the version and exists. 147 | --only-translate Prints the internal translation and exits. 148 | --only-translate-annotations 149 | Prints the internal translation and exits. 150 | --only-explanation-atoms 151 | Prints the atoms used by the explainer to build the explanations. 152 | --auto-tracing {none,facts,all} 153 | Automatically creates traces for the rules of the program. Default: none. 154 | -n N N Number of answer sets and number of desired explanations. 155 | ``` 156 | 157 | ## Differences with respect to the previous version 158 | 159 | ### Choice rules and pooling 160 | They are now supported. 161 | ``` 162 | n(1..20). 163 | switch(s1;s2;s3;s4). 164 | 2{num(N):n(N)}4 :- n(N), N>15. 165 | ``` 166 | Then can be traced with ```%!trace_rule``` annotations. 167 | 168 | ### Multiple labels for the same atom 169 | 170 | In the [previous version](https://github.com/bramucas/xclingo), multiple labels for the same atom lead to alternative explanations. In this version a single explanation is be produced in which labels are concatenated. 171 | 172 | As an example, the following situation: 173 | ``` 174 | %!trace {"% alcohol's level is above permitted (%)",P,A} alcohol(P,A) : A>40. 175 | %!trace {"% was drunk",P} alcohol(P,A) : A>40. 176 | ``` 177 | 178 | would lead to the following result in the previous version: 179 | ``` 180 | * 181 | |__gabriel goes to prison 182 | | |__gabriel drove drunk 183 | | | |__gabriel alcohol's level is 40 184 | 185 | * 186 | |__gabriel goes to prison 187 | | |__gabriel drove drunk 188 | | | |__gabriel was drunk 189 | ``` 190 | 191 | while the new version will produce: 192 | 193 | ``` 194 | * 195 | |__gabriel goes to prison 196 | | |__gabriel drove drunk 197 | | | |__gabriel alcohol's level is 40;gabriel was drunk 198 | ``` 199 | 200 | ### 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /examples/diamond.lp: -------------------------------------------------------------------------------- 1 | max(3). 2 | p(0). 3 | 4 | %!trace_rule {"a(%)",N} 5 | a(N) :- p(N),N add/delete 'spaces' between '%' and '!' 18 | %!mute a(2). 19 | % !mute p(N) : N=1. 20 | -------------------------------------------------------------------------------- /examples/dont_drive_drunk.lp: -------------------------------------------------------------------------------- 1 | person(gabriel;clare). 2 | 3 | drive(gabriel). 4 | alcohol(gabriel, 40). 5 | resist(gabriel). 6 | 7 | drive(clare). 8 | alcohol(clare, 5). 9 | 10 | %!trace_rule {"% drove drunk", P} 11 | punish(P) :- drive(P), alcohol(P,A), A>30, person(P). 12 | 13 | %!trace_rule {"% resisted to authority", P} 14 | punish(P) :- resist(P), person(P). 15 | 16 | %!trace_rule {"% goes to prison",P} 17 | sentence(P, prison) :- punish(P). 18 | 19 | %!trace_rule {"% is innocent by default",P} 20 | sentence(P, innocent) :- person(P), not punish(P). 21 | 22 | %!trace {"% alcohol's level is %",P,A} alcohol(P,A). 23 | %!trace {"% was drunk",P} alcohol(P,A). 24 | 25 | %!show_trace sentence(P,S). -------------------------------------------------------------------------------- /examples/pool_and_choice.lp: -------------------------------------------------------------------------------- 1 | {switch(s1;s2;s3)}. 2 | 3 | %!trace_rule {"bulb is OFF"} 4 | bulb(off) :- not bulb(on). 5 | 6 | %!trace_rule {"bulb is ON"} 7 | bulb(on) :- switch(s1), switch(s3). 8 | 9 | %!trace {"switch % is enabled",S} switch(S). 10 | 11 | %!show_trace bulb(V). 12 | -------------------------------------------------------------------------------- /examples/pool_and_choice2.lp: -------------------------------------------------------------------------------- 1 | {switch(1..3)}. 2 | 3 | %!trace_rule {"bulb is OFF"} 4 | bulb(off) :- not bulb(on). 5 | 6 | %!trace_rule {"bulb is ON"} 7 | bulb(on) :- switch(1), switch(3). 8 | 9 | %!trace {"switch % is enabled",S} switch(S). 10 | 11 | %!show_trace bulb(V). 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | version = {} 4 | with open("./xclingo/_version.py") as fp: 5 | exec(fp.read(), version) 6 | 7 | with open("README.md", "r") as fh: 8 | long_description = fh.read() 9 | 10 | setuptools.setup( 11 | name="xclingo", 12 | version=version["__version__"], 13 | author="Brais Muñiz", 14 | author_email="mc.brais@gmail.com", 15 | description="Tool for explaining and debugging Answer Set Programs.", 16 | long_description=long_description, 17 | long_description_content_type="text/markdown", 18 | url="https://github.com/bramucas/xclingo2", 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | ], 24 | keywords=[ 25 | "logic programming", 26 | "answer set programming", 27 | ], 28 | include_package_data=True, 29 | python_requires=">=3.6.0", 30 | install_requires=[ 31 | "clingo>=5.5.0.post3", 32 | "argparse", 33 | "importlib_resources", 34 | ], 35 | packages=["xclingo", "xclingo.preprocessor", "xclingo.explanation", "xclingo.xclingo_lp"], 36 | entry_points={"console_scripts": ["xclingo=xclingo.__main__:main"]}, 37 | ) 38 | -------------------------------------------------------------------------------- /tests/test_Preprocessor.py: -------------------------------------------------------------------------------- 1 | from clingo.symbol import Number 2 | import pytest 3 | import clingo.ast as ast 4 | from clingo import Number, String 5 | from xclingo.preprocessor import Preprocessor 6 | 7 | class TestPreprocessor: 8 | 9 | @pytest.fixture(scope='class') 10 | def custom_body(self): 11 | loc = ast.Location(ast.Position("",0,0), ast.Position("",0,0)) 12 | custom_body = [ 13 | ast.Literal( 14 | loc, 15 | ast.Sign.NoSign, 16 | ast.SymbolicAtom(ast.Function(loc, "person",[ast.Variable(loc, 'P')],False)), 17 | ), 18 | ast.Literal( 19 | loc, 20 | ast.Sign.NoSign, 21 | ast.SymbolicAtom(ast.Function(loc, "hola",[],False)), 22 | ), 23 | ast.Literal( 24 | loc, 25 | ast.Sign.Negation, 26 | ast.SymbolicAtom(ast.Function(loc, "nothola",[],False)), 27 | ), 28 | ast.Literal( 29 | loc, 30 | ast.Sign.NoSign, 31 | ast.Comparison( 32 | ast.ComparisonOperator.Equal, 33 | ast.SymbolicTerm(loc, Number(1)), 34 | ast.SymbolicTerm(loc, Number(1)), 35 | ) 36 | ) 37 | ] 38 | return custom_body 39 | 40 | @pytest.fixture(scope='class') 41 | def expected_sup_body(self): 42 | loc = ast.Location(ast.Position("",0,0), ast.Position("",0,0)) 43 | expected_body = [ 44 | ast.Literal( 45 | loc, 46 | ast.Sign.NoSign, 47 | ast.SymbolicAtom(ast.Function(loc, "_xclingo_model", [ast.Function(loc, "person",[ast.Variable(loc, 'P')],False)], False)), 48 | ), 49 | ast.Literal( 50 | loc, 51 | ast.Sign.NoSign, 52 | ast.SymbolicAtom(ast.Function(loc, "_xclingo_model", [ast.Function(loc, "hola",[],False)], False)), 53 | ), 54 | ast.Literal( 55 | loc, 56 | ast.Sign.Negation, 57 | ast.SymbolicAtom(ast.Function(loc, "_xclingo_model", [ast.Function(loc, "nothola",[],False)], False)), 58 | ), 59 | ast.Literal( 60 | loc, 61 | ast.Sign.NoSign, 62 | ast.Comparison( 63 | ast.ComparisonOperator.Equal, 64 | ast.SymbolicTerm(loc, Number(1)), 65 | ast.SymbolicTerm(loc, Number(1)), 66 | ) 67 | ) 68 | ] 69 | return expected_body 70 | 71 | @pytest.fixture(scope='class') 72 | def expected_fbody_body(self): 73 | loc = ast.Location(ast.Position("",0,0), ast.Position("",0,0)) 74 | expected_body = [ 75 | ast.Literal( 76 | loc, 77 | ast.Sign.NoSign, 78 | ast.SymbolicAtom(ast.Function(loc, "_xclingo_f_atom", [ast.Function(loc, "person",[ast.Variable(loc, 'P')],False)], False)), 79 | ), 80 | ast.Literal( 81 | loc, 82 | ast.Sign.NoSign, 83 | ast.SymbolicAtom(ast.Function(loc, "_xclingo_f_atom", [ast.Function(loc, "hola",[],False)], False)), 84 | ), 85 | ast.Literal( 86 | loc, 87 | ast.Sign.Negation, 88 | ast.SymbolicAtom(ast.Function(loc, "_xclingo_model", [ast.Function(loc, "nothola",[],False)], False)), 89 | ), 90 | ast.Literal( 91 | loc, 92 | ast.Sign.NoSign, 93 | ast.Comparison( 94 | ast.ComparisonOperator.Equal, 95 | ast.SymbolicTerm(loc, Number(1)), 96 | ast.SymbolicTerm(loc, Number(1)), 97 | ) 98 | ) 99 | ] 100 | return expected_body 101 | 102 | @pytest.fixture(scope='class') 103 | def expected_propagates(self): 104 | loc = ast.Location(ast.Position("",0,0), ast.Position("",0,0)) 105 | expected_propagates = [ 106 | ast.Literal( 107 | loc, 108 | ast.Sign.NoSign, 109 | ast.SymbolicAtom(ast.Function(loc, "person",[ast.Variable(loc, 'P')],False)), 110 | ), 111 | ast.Literal( 112 | loc, 113 | ast.Sign.NoSign, 114 | ast.SymbolicAtom(ast.Function(loc, "hola",[],False)), 115 | ) 116 | ] 117 | return expected_propagates 118 | 119 | @pytest.fixture(scope='class') 120 | def custom_rule(self, custom_body): 121 | loc = ast.Location(ast.Position('', 0, 0), ast.Position('', 0, 0)) 122 | lit = ast.Literal( 123 | loc, 124 | ast.Sign.NoSign, 125 | ast.SymbolicAtom(ast.Function(loc, 'b', [], False)) 126 | ) 127 | custom_rule = ast.Rule(loc, lit, custom_body) 128 | return custom_rule 129 | 130 | @pytest.fixture(scope='class') 131 | def expected_support_rule(self, expected_sup_body): 132 | rule_id=32 133 | loc = ast.Location( 134 | ast.Position("", 0, 0), ast.Position("", 0, 0) 135 | ) 136 | head = ast.Literal( 137 | loc, 138 | ast.Sign.NoSign, 139 | ast.SymbolicAtom(ast.Function( 140 | loc, 141 | '_xclingo_sup', 142 | [ 143 | ast.SymbolicTerm(loc, Number(rule_id)), 144 | ast.SymbolicAtom(ast.Function(loc, 'b', [], False)), 145 | ast.Function( 146 | loc, 147 | '', 148 | [ 149 | ast.Literal( 150 | loc, 151 | ast.Sign.NoSign, 152 | ast.SymbolicAtom(ast.Function(loc, "person",[ast.Variable(loc, 'P')],False)), 153 | ), 154 | ast.Literal( 155 | loc, 156 | ast.Sign.NoSign, 157 | ast.SymbolicAtom(ast.Function(loc, "hola",[],False)), 158 | ) 159 | ], 160 | False, 161 | ) 162 | ], 163 | False, 164 | )) 165 | ) 166 | expected_sup_rule = ast.Rule(loc, head, expected_sup_body) 167 | return rule_id, expected_sup_rule 168 | 169 | @pytest.fixture(scope='class') 170 | def expected_fbody_rule(self, expected_fbody_body): 171 | rule_id=32 172 | loc = ast.Location( 173 | ast.Position("", 0, 0), ast.Position("", 0, 0) 174 | ) 175 | head = ast.Literal( 176 | loc, 177 | ast.Sign.NoSign, 178 | ast.SymbolicAtom(ast.Function( 179 | loc, 180 | '_xclingo_fbody', 181 | [ 182 | ast.SymbolicTerm(loc, Number(rule_id)), 183 | ast.SymbolicAtom(ast.Function(loc, 'b', [], False)), 184 | ast.Function( 185 | loc, 186 | '', 187 | [ 188 | ast.Literal( 189 | loc, 190 | ast.Sign.NoSign, 191 | ast.SymbolicAtom(ast.Function(loc, "person",[ast.Variable(loc, 'P')],False)), 192 | ), 193 | ast.Literal( 194 | loc, 195 | ast.Sign.NoSign, 196 | ast.SymbolicAtom(ast.Function(loc, "hola",[],False)), 197 | ) 198 | ], 199 | False, 200 | ) 201 | ], 202 | False, 203 | )) 204 | ) 205 | expected_fbody_rule = ast.Rule(loc, head, expected_fbody_body) 206 | return rule_id, expected_fbody_rule 207 | 208 | @pytest.fixture(scope='class') 209 | def custom_label_rule(self): 210 | loc = ast.Location( 211 | ast.Position("", 0, 0), ast.Position("", 0, 0) 212 | ) 213 | return ast.Rule( 214 | loc, 215 | ast.Literal( 216 | loc, 217 | ast.Sign.NoSign, 218 | ast.SymbolicAtom(ast.Function( 219 | loc, 220 | '_xclingo_label', 221 | [ 222 | ast.SymbolicAtom(ast.Function( 223 | loc, 224 | 'id', 225 | [], 226 | False, 227 | )), 228 | ast.SymbolicAtom(ast.Function( 229 | loc, 230 | 'label', 231 | [ 232 | ast.SymbolicTerm(loc, String("persona %")), 233 | ast.Function( 234 | loc, 235 | '', 236 | [ast.Variable(loc, 'P')], 237 | False, 238 | ) 239 | ], 240 | True, 241 | )), 242 | ], 243 | False, 244 | )) 245 | ), 246 | [], 247 | ) 248 | 249 | @pytest.fixture(scope='class') 250 | def expected_label_rule(self): 251 | rule_id = 32 252 | loc = ast.Location( 253 | ast.Position("", 0, 0), ast.Position("", 0, 0) 254 | ) 255 | head_var = ast.Variable(loc, 'Head') 256 | head = ast.Literal( 257 | loc, 258 | ast.Sign.NoSign, 259 | ast.SymbolicAtom(ast.Function( 260 | loc, 261 | '_xclingo_label', 262 | [ 263 | head_var, 264 | ast.SymbolicAtom(ast.Function( 265 | loc, 266 | 'label', 267 | [ 268 | ast.SymbolicTerm(loc, String("persona %")), 269 | ast.Function( 270 | loc, 271 | '', 272 | [ast.Variable(loc, 'P')], 273 | False, 274 | ) 275 | ], 276 | True, 277 | )), 278 | ], 279 | False, 280 | )) 281 | ), 282 | body = [ 283 | ast.Literal( 284 | loc, 285 | ast.Sign.NoSign, 286 | ast.SymbolicAtom(ast.Function( 287 | loc, 288 | '_xclingo_f', 289 | [ 290 | ast.SymbolicTerm(loc, Number(rule_id)), 291 | head_var, 292 | ast.Function( 293 | loc, 294 | '', 295 | [ 296 | ast.Literal( 297 | loc, 298 | ast.Sign.NoSign, 299 | ast.SymbolicAtom(ast.Function(loc, "person",[ast.Variable(loc, 'P')],False)), 300 | ), 301 | ast.Literal( 302 | loc, 303 | ast.Sign.NoSign, 304 | ast.SymbolicAtom(ast.Function(loc, "hola",[],False)), 305 | ) 306 | ], 307 | False, 308 | ) 309 | 310 | ], 311 | False, 312 | )) 313 | ) 314 | ], 315 | rule = ast.Rule( 316 | loc, 317 | head[0], 318 | body[0], 319 | ) 320 | return rule_id, rule 321 | 322 | @pytest.fixture(scope='class') 323 | def custom_label_atom(self): 324 | loc = ast.Location( 325 | ast.Position("", 0, 0), ast.Position("", 0, 0) 326 | ) 327 | head = ast.Literal( 328 | loc, 329 | ast.Sign.NoSign, 330 | ast.SymbolicAtom(ast.Function( 331 | loc, 332 | '_xclingo_label', 333 | [ 334 | ast.SymbolicAtom(ast.Function( 335 | loc, 336 | 'alcohol', 337 | [ast.Variable(loc, 'P'), ast.Variable(loc, 'A')], 338 | False, 339 | )), 340 | ast.SymbolicAtom(ast.Function( 341 | loc, 342 | 'label', 343 | [ 344 | ast.SymbolicTerm(loc, String("% alcohol's level is %")), 345 | ast.Function( 346 | loc, 347 | '', 348 | [ast.Variable(loc, 'P'), ast.Variable(loc, 'A')], 349 | False, 350 | ) 351 | ], 352 | True, 353 | )), 354 | ], 355 | False, 356 | )) 357 | ) 358 | body = [ 359 | ast.Literal( 360 | loc, 361 | ast.Sign.NoSign, 362 | ast.SymbolicAtom(ast.Function( 363 | loc, 364 | 'person', 365 | [ast.Variable(loc, 'P')], 366 | False, 367 | )) 368 | ), 369 | ast.Literal( 370 | loc, 371 | ast.Sign.NoSign, 372 | ast.Comparison( 373 | ast.ComparisonOperator.GreaterThan, 374 | ast.Variable(loc, 'A'), 375 | ast.SymbolicTerm(loc, Number(30)), 376 | ) 377 | ), 378 | ast.Literal( 379 | loc, 380 | ast.Sign.Negation, 381 | ast.SymbolicAtom(ast.Function( 382 | loc, 383 | 'inprison', 384 | [ast.Variable(loc, 'P')], 385 | False, 386 | )) 387 | ) 388 | ] 389 | rule = ast.Rule(loc, head, body) 390 | return rule 391 | 392 | @pytest.fixture(scope='class') 393 | def expected_label_atom(self): 394 | loc = ast.Location( 395 | ast.Position("", 0, 0), ast.Position("", 0, 0) 396 | ) 397 | head = ast.Literal( 398 | loc, 399 | ast.Sign.NoSign, 400 | ast.SymbolicAtom(ast.Function( 401 | loc, 402 | '_xclingo_label', 403 | [ 404 | ast.SymbolicAtom(ast.Function( 405 | loc, 406 | 'alcohol', 407 | [ast.Variable(loc, 'P'), ast.Variable(loc, 'A')], 408 | False, 409 | )), 410 | ast.SymbolicAtom(ast.Function( 411 | loc, 412 | 'label', 413 | [ 414 | ast.SymbolicTerm(loc, String("% alcohol's level is %")), 415 | ast.Function( 416 | loc, 417 | '', 418 | [ast.Variable(loc, 'P'), ast.Variable(loc, 'A')], 419 | False, 420 | ) 421 | ], 422 | True, 423 | )), 424 | ], 425 | False, 426 | )) 427 | ) 428 | body = [ 429 | ast.Literal( 430 | loc, 431 | ast.Sign.NoSign, 432 | ast.SymbolicAtom(ast.Function( 433 | loc, 434 | '_xclingo_intree', 435 | [ 436 | ast.SymbolicAtom(ast.Function( 437 | loc, 438 | 'alcohol', 439 | [ast.Variable(loc, 'P'), ast.Variable(loc, 'A')], 440 | False, 441 | )), 442 | ], 443 | False, 444 | )) 445 | ), 446 | ast.Literal( 447 | loc, 448 | ast.Sign.NoSign, 449 | ast.SymbolicAtom(ast.Function( 450 | loc, 451 | '_xclingo_model', 452 | [ 453 | ast.Function( 454 | loc, 455 | 'person', 456 | [ast.Variable(loc, 'P')], 457 | False, 458 | ) 459 | ], 460 | False, 461 | )) 462 | ), 463 | ast.Literal( 464 | loc, 465 | ast.Sign.NoSign, 466 | ast.Comparison( 467 | ast.ComparisonOperator.GreaterThan, 468 | ast.Variable(loc, 'A'), 469 | ast.SymbolicTerm(loc, Number(30)), 470 | ) 471 | ), 472 | ast.Literal( 473 | loc, 474 | ast.Sign.Negation, 475 | ast.SymbolicAtom(ast.Function( 476 | loc, 477 | '_xclingo_model', 478 | [ 479 | ast.Function( 480 | loc, 481 | 'inprison', 482 | [ast.Variable(loc, 'P')], 483 | False, 484 | ) 485 | ], 486 | False 487 | )), 488 | ) 489 | ] 490 | rule = ast.Rule(loc, head, body) 491 | return rule 492 | 493 | @pytest.fixture(scope='class') 494 | def custom_show_trace(self): 495 | loc = ast.Location( 496 | ast.Position('', 0, 0), 497 | ast.Position('', 0, 0), 498 | ) 499 | head = ast.Literal( 500 | loc, 501 | ast.Sign.NoSign, 502 | ast.SymbolicAtom(ast.Function( 503 | loc, 504 | '_xclingo_show_trace', 505 | [ 506 | ast.Function( 507 | loc, 508 | 'sentence', 509 | [ast.Variable(loc, 'P'), ast.Variable(loc, 'S')], 510 | False, 511 | ), 512 | ], 513 | False, 514 | )) 515 | ) 516 | body = [ 517 | ast.Literal( 518 | loc, 519 | ast.Sign.NoSign, 520 | ast.SymbolicAtom(ast.Function( 521 | loc, 522 | 'person', 523 | [ast.Variable(loc, 'P')], 524 | False, 525 | )) 526 | ), 527 | ast.Literal( 528 | loc, 529 | ast.Sign.NoSign, 530 | ast.Comparison( 531 | ast.ComparisonOperator.Equal, 532 | ast.Variable(loc, 'P'), 533 | ast.SymbolicAtom(ast.Function( 534 | loc, 535 | 'gabriel', 536 | [], 537 | False, 538 | )), 539 | ) 540 | ), 541 | ast.Literal( 542 | loc, 543 | ast.Sign.Negation, 544 | ast.SymbolicAtom(ast.Function( 545 | loc, 546 | 'inprison', 547 | [ast.Variable(loc, 'P')], 548 | False, 549 | )) 550 | ) 551 | ] 552 | rule = ast.Rule(loc, head, body) 553 | return rule 554 | 555 | @pytest.fixture(scope='class') 556 | def expected_show_trace(self): 557 | loc = ast.Location( 558 | ast.Position('', 0, 0), 559 | ast.Position('', 0, 0), 560 | ) 561 | head = ast.Literal( 562 | loc, 563 | ast.Sign.NoSign, 564 | ast.SymbolicAtom(ast.Function( 565 | loc, 566 | '_xclingo_show_trace', 567 | [ 568 | ast.Function( 569 | loc, 570 | 'sentence', 571 | [ast.Variable(loc, 'P'), ast.Variable(loc, 'S')], 572 | False, 573 | ), 574 | ], 575 | False, 576 | )) 577 | ) 578 | body = [ 579 | ast.Literal( 580 | loc, 581 | ast.Sign.NoSign, 582 | ast.SymbolicAtom(ast.Function( 583 | loc, 584 | '_xclingo_model', 585 | [ 586 | ast.Function( 587 | loc, 588 | 'sentence', 589 | [ast.Variable(loc, 'P'), ast.Variable(loc, 'S')], 590 | False, 591 | ), 592 | ], 593 | False, 594 | )) 595 | ), 596 | ast.Literal( 597 | loc, 598 | ast.Sign.NoSign, 599 | ast.SymbolicAtom(ast.Function( 600 | loc, 601 | '_xclingo_model', 602 | [ 603 | ast.Function( 604 | loc, 605 | 'person', 606 | [ast.Variable(loc, 'P')], 607 | False, 608 | ), 609 | ], 610 | False, 611 | )) 612 | ), 613 | ast.Literal( 614 | loc, 615 | ast.Sign.NoSign, 616 | ast.Comparison( 617 | ast.ComparisonOperator.Equal, 618 | ast.Variable(loc, 'P'), 619 | ast.SymbolicAtom(ast.Function( 620 | loc, 621 | 'gabriel', 622 | [], 623 | False, 624 | )), 625 | ) 626 | ), 627 | ast.Literal( 628 | loc, 629 | ast.Sign.Negation, 630 | ast.SymbolicAtom(ast.Function( 631 | loc, 632 | '_xclingo_model', 633 | [ 634 | ast.Function( 635 | loc, 636 | 'inprison', 637 | [ast.Variable(loc, 'P')], 638 | False, 639 | ) 640 | ], 641 | False, 642 | )) 643 | ) 644 | ] 645 | rule = ast.Rule(loc, head, body) 646 | return rule 647 | 648 | 649 | def test_propagates(self, custom_body, expected_propagates): 650 | preprocessor = Preprocessor() 651 | assert expected_propagates == list(preprocessor.propagates(custom_body)) 652 | 653 | def test_sup_body(self, custom_body, expected_sup_body): 654 | preprocessor = Preprocessor() 655 | assert expected_sup_body == list(preprocessor.sup_body(custom_body)) 656 | 657 | def test_sup_rule(self, custom_rule, expected_support_rule): 658 | rule_id, expected = expected_support_rule 659 | preprocessor = Preprocessor() 660 | support_rule = preprocessor.support_rule(rule_id, custom_rule) 661 | assert expected == support_rule 662 | 663 | def test_fbody_body(self, custom_body, expected_fbody_body): 664 | preprocessor = Preprocessor() 665 | body = list(preprocessor.fbody_body(custom_body)) 666 | assert expected_fbody_body == body 667 | 668 | def test_fbody_rule(self, custom_rule, expected_fbody_rule): 669 | preprocessor = Preprocessor() 670 | rule_id, expected = expected_fbody_rule 671 | rule = preprocessor.fbody_rule(rule_id, custom_rule) 672 | assert expected == rule 673 | 674 | def test_label_rule(self, custom_label_rule, expected_label_rule, custom_body): 675 | preprocessor = Preprocessor() 676 | rule_id, expected = expected_label_rule 677 | rule = preprocessor.label_rule(rule_id, custom_label_rule, custom_body) 678 | assert expected == rule 679 | 680 | def test_label_atom(self, custom_label_atom, expected_label_atom): 681 | preprocessor = Preprocessor() 682 | rule = preprocessor.label_atom(custom_label_atom) 683 | assert expected_label_atom == rule 684 | 685 | def test_show_trace(self, custom_show_trace, expected_show_trace): 686 | preprocessor = Preprocessor() 687 | rule = preprocessor.show_trace(custom_show_trace) 688 | assert expected_show_trace == rule 689 | -------------------------------------------------------------------------------- /tests/test_Utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from xclingo.preprocessor._utils import translate_show_all, translate_trace_all, translate_trace 4 | 5 | class TestUtils: 6 | 7 | def test_translate_trace_all(self, datadir): 8 | input_text = (datadir / 'test_trace_all_input').read_text() 9 | expected_text = (datadir / 'test_trace_all_output').read_text() 10 | translated = translate_trace_all(input_text) 11 | assert expected_text == translated 12 | 13 | def test_translate_show_all(self, datadir): 14 | input_text = (datadir / 'test_show_all_input').read_text() 15 | expected_text = (datadir / 'test_show_all_output').read_text() 16 | translated = translate_show_all(input_text) 17 | assert expected_text == translated 18 | 19 | def test_translate_trace(self, datadir): 20 | input_text = (datadir / 'test_trace_input').read_text() 21 | expected_text = (datadir / 'test_trace_output').read_text() 22 | translated = translate_trace(input_text) 23 | print(translated) 24 | print('--------') 25 | print(expected_text) 26 | assert expected_text == translated 27 | -------------------------------------------------------------------------------- /tests/test_Utils/test_show_all_input: -------------------------------------------------------------------------------- 1 | %!show_trace sentence(P,A) : P=gabriel. 2 | %!show_trace sentence(P, 100). -------------------------------------------------------------------------------- /tests/test_Utils/test_show_all_output: -------------------------------------------------------------------------------- 1 | _xclingo_show_trace(sentence(P,A)) :- P=gabriel. 2 | _xclingo_show_trace(sentence(P, 100)). -------------------------------------------------------------------------------- /tests/test_Utils/test_trace_all_input: -------------------------------------------------------------------------------- 1 | %!trace {"% alcohol's level is %",P,A} alcohol(P,A) : A>30, not inprison(P). 2 | %!trace {"% has driven",P} drive(P). -------------------------------------------------------------------------------- /tests/test_Utils/test_trace_all_output: -------------------------------------------------------------------------------- 1 | _xclingo_label(alcohol(P,A), @label("% alcohol's level is %", (P,A,)) ) :- A>30, not inprison(P). 2 | _xclingo_label(drive(P), @label("% has driven", (P,)) ). -------------------------------------------------------------------------------- /tests/test_Utils/test_trace_input: -------------------------------------------------------------------------------- 1 | %!trace_rule {"% has driven drunk",P} 2 | punish(P) :- person(P), drive(P), alcohol(P,A), A>30. 3 | 4 | %!trace_rule {"p"} 5 | p. 6 | -------------------------------------------------------------------------------- /tests/test_Utils/test_trace_output: -------------------------------------------------------------------------------- 1 | _xclingo_label(id, @label("% has driven drunk", (P,) )). 2 | punish(P) :- person(P), drive(P), alcohol(P,A), A>30. 3 | 4 | _xclingo_label(id, @label("p", (,) )). 5 | p. 6 | -------------------------------------------------------------------------------- /tests/test_xclingo.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from xclingo import XclingoControl, XclingoContext 4 | 5 | class TestXclingo: 6 | 7 | def assert_test_case(self, datadir, test_case, auto_tracing): 8 | xcontrol = XclingoControl( 9 | n_solutions=0, 10 | n_explanations=0, 11 | auto_trace=auto_tracing, 12 | ) 13 | xcontrol.add('base', [], (datadir / f'{test_case}.lp').read_text()) 14 | xcontrol.ground() 15 | 16 | result = xcontrol._default_output() 17 | expected = (datadir / f'expected_{test_case}.txt').read_text() 18 | assert expected == result 19 | 20 | def test_count_aggregate(self, datadir): 21 | self.assert_test_case(datadir, 'count_aggregate', 'none') 22 | self.assert_test_case(datadir, 'ignore_shows', 'all') -------------------------------------------------------------------------------- /tests/test_xclingo/count_aggregate.lp: -------------------------------------------------------------------------------- 1 | numberObjectsbyEntityatTime(N,E,T):- N=#count{O: held_by(O,E,T)}, entity(E),time(T). 2 | 3 | time(T):-held_by(O,E,T). 4 | entity(E):-held_by(O,E,T). 5 | 6 | held_by(football,mary,0). 7 | held_by(football,mary,0). 8 | held_by(apple,mary,0). 9 | held_by(football,mary,1). 10 | held_by(football,john,1). 11 | 12 | %!trace {"% is holding % items at time point %", E,N,T} numberObjectsbyEntityatTime(N,E,T). 13 | %!show_trace numberObjectsbyEntityatTime(N,E,T). -------------------------------------------------------------------------------- /tests/test_xclingo/expected_count_aggregate.txt: -------------------------------------------------------------------------------- 1 | Answer 1 2 | * 3 | |__john is holding 0 items at time point 0 4 | 5 | * 6 | |__john is holding 1 items at time point 1 7 | 8 | * 9 | |__mary is holding 1 items at time point 1 10 | 11 | * 12 | |__mary is holding 2 items at time point 0 13 | 14 | -------------------------------------------------------------------------------- /tests/test_xclingo/expected_ignore_shows.txt: -------------------------------------------------------------------------------- 1 | Answer 1 2 | * 3 | |__b(1) 4 | | |__a(1) 5 | 6 | -------------------------------------------------------------------------------- /tests/test_xclingo/ignore_shows.lp: -------------------------------------------------------------------------------- 1 | a(1). 2 | b(X) :- a(X). 3 | 4 | #show b/1. 5 | %!show_trace b(X). 6 | -------------------------------------------------------------------------------- /xclingo/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__ 2 | from ._main import Explainer 3 | from ._main import XclingoControl 4 | from ._main import Context as XclingoContext 5 | -------------------------------------------------------------------------------- /xclingo/__main__.py: -------------------------------------------------------------------------------- 1 | from xclingo import Explainer as Explainer 2 | from xclingo import XclingoControl 3 | from xclingo import __version__ as xclingo_version 4 | from argparse import ArgumentParser, FileType 5 | import sys 6 | 7 | def check_options(): 8 | # Handles arguments of xclingo 9 | parser = ArgumentParser(description='Tool for explaining (and debugging) ASP programs', prog='xclingo') 10 | parser.add_argument('--version', action='version', 11 | version='xclingo {version}'.format(version=xclingo_version), 12 | help='Prints the version and exists.') 13 | optional_group = parser.add_mutually_exclusive_group() 14 | optional_group.add_argument('--only-translate', action='store_true', 15 | help="Prints the internal translation and exits.") 16 | optional_group.add_argument('--only-translate-annotations', action='store_true', 17 | help="Prints the internal translation and exits.") 18 | optional_group.add_argument('--only-explanation-atoms', action='store_true', 19 | help="Prints the atoms used by the explainer to build the explanations.") 20 | parser.add_argument('--auto-tracing', type=str, choices=["none", "facts", "all"], default="none", 21 | help="Automatically creates traces for the rules of the program. Default: none.") 22 | parser.add_argument('-n', nargs=2, default=(1,1), type=int, help="Number of answer sets and number of desired explanations.") 23 | parser.add_argument('infiles', nargs='+', type=FileType('r'), default=sys.stdin, help="ASP program") 24 | return parser.parse_args() 25 | 26 | def read_files(files): 27 | return "\n".join([file.read() for file in files]) 28 | 29 | def translate(program, auto_trace): 30 | explainer = Explainer(auto_trace=auto_trace) 31 | explainer.add('base', [], program) 32 | explainer._translate_program() 33 | translation = explainer._preprocessor.get_translation() 34 | translation += explainer._getExplainerLP(auto_trace=auto_trace) 35 | return translation 36 | 37 | def print_explanation_atoms(xControl: XclingoControl): 38 | n = 0 39 | for xmodel in xControl.get_xclingo_models(): 40 | n += 1 41 | print(f'Answer {n}') 42 | print(xmodel) 43 | 44 | def print_text_explanations(xControl: XclingoControl): 45 | n = 0 46 | for answer in xControl.explain(): 47 | n += 1 48 | print(f'Answer {1}') 49 | for expl in answer: 50 | print(expl.ascii_tree()) 51 | 52 | 53 | 54 | def main(): 55 | args = check_options() 56 | 57 | if args.only_translate_annotations: 58 | program = read_files(args.infiles) 59 | from xclingo.preprocessor import Preprocessor 60 | print(Preprocessor.translate_annotations(program)) 61 | return 0 62 | 63 | if args.only_translate: 64 | program = read_files(args.infiles) 65 | print(translate(program, args.auto_tracing)) 66 | return 0 67 | 68 | xControl = XclingoControl( 69 | n_solutions=str(args.n[0]), 70 | n_explanations=str(args.n[1]), 71 | auto_trace=args.auto_tracing, 72 | ) 73 | 74 | for file in args.infiles: 75 | xControl.add("base", [], file.read()) 76 | 77 | xControl.ground() 78 | 79 | if args.only_explanation_atoms: 80 | print_explanation_atoms(xControl) 81 | else: 82 | print_text_explanations(xControl) 83 | 84 | if __name__ == '__main__': 85 | main() 86 | -------------------------------------------------------------------------------- /xclingo/_main.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Sequence 2 | from clingo import Model, Function, String 3 | from clingo.ast import ProgramBuilder, parse_string 4 | from clingo.control import Control 5 | from clingo.symbol import SymbolType 6 | from xclingo.explanation import Explanation 7 | from xclingo.preprocessor import Preprocessor 8 | 9 | from clingo.core import MessageCode 10 | 11 | class Context: 12 | def label(self, text, tup): 13 | if text.type == SymbolType.String: 14 | text = text.string 15 | else: 16 | text = str(text).strip('"') 17 | for val in tup.arguments: 18 | text = text.replace("%", val.string if val.type==SymbolType.String else str(val), 1) 19 | return [String(text)] 20 | 21 | def inbody(self, body): 22 | if len(body.arguments)>0: 23 | return [Function( 24 | '', 25 | [a, body], 26 | True, 27 | ) 28 | for a in body.arguments] 29 | else: 30 | return Function('empty', [], True) 31 | 32 | class Explainer(): 33 | def __init__(self, internal_control_arguments=['1'], auto_trace="none"): 34 | self._preprocessor = Preprocessor() 35 | self._memory = [] 36 | 37 | self._internal_control_arguments = internal_control_arguments 38 | self._auto_trace = auto_trace 39 | self._translated = False 40 | self._current_model = [] 41 | 42 | self._no_labels = False 43 | self._no_show_trace = False 44 | 45 | def logger(self, _code, msg): 46 | if _code == MessageCode.AtomUndefined: 47 | if 'xclingo_muted(Cause)' in msg: 48 | return 49 | if '_xclingo_label_tree/3' in msg: 50 | return 51 | if '_xclingo_label' in msg: 52 | self._no_labels = True 53 | return 54 | if '_xclingo_show_trace' in msg: 55 | self._no_show_trace = True 56 | print(msg) 57 | 58 | def print_messages(self): 59 | if self._no_labels: 60 | print('xclingo info: any atom or rule has been labelled.') 61 | if self._no_show_trace: 62 | print('xclingo info: any atom has been affected by a %!show_trace annotation.') 63 | 64 | def clean_log(self): 65 | self._no_labels = False 66 | self._no_show_trace = False 67 | 68 | def _getExplainerLP(self, auto_trace="none"): 69 | if hasattr(self, '_explainerLP') == False: 70 | setattr(self, '_explainerLP', self._loadExplainerLP(auto_trace)) 71 | return self._explainerLP 72 | 73 | def _loadExplainerLP(self, auto_trace="none"): 74 | try: 75 | import importlib.resources as pkg_resources 76 | except ImportError: 77 | # Try backported to PY<37 `importlib_resources`. 78 | import importlib_resources as pkg_resources 79 | 80 | from . import xclingo_lp # relative-import the *package* containing the templates 81 | program = pkg_resources.read_text(xclingo_lp, 'xclingo.lp') 82 | if auto_trace == "all": 83 | program += pkg_resources.read_text(xclingo_lp, 'autotrace_all.lp') 84 | elif auto_trace == "facts": 85 | program += pkg_resources.read_text(xclingo_lp, 'autotrace_facts.lp') 86 | return program 87 | 88 | def add(self, program_name:str, parameters: Iterable[str], program:str): 89 | self._memory.append((program_name, program)) 90 | 91 | def _initialize_control(self): 92 | return Control( 93 | self._internal_control_arguments + \ 94 | [ 95 | '--project=project' 96 | ], 97 | logger=self.logger) 98 | 99 | def _translate_program(self): 100 | self._preprocessor._rule_count = 1 101 | for name, program in self._memory: 102 | self._preprocessor.translate_program(program, name=name) 103 | 104 | def _ground(self, control, model, context=None): 105 | """Grounding for the explainer clingo control. It translates the program and adds the original program's model as facts. 106 | 107 | Args: 108 | control (_type_): _description_ 109 | model (_type_): _description_ 110 | context (_type_, optional): _description_. Defaults to None. 111 | """ 112 | if not self._translated: 113 | self._translate_program() 114 | self._translated 115 | 116 | with ProgramBuilder(control) as builder: 117 | parse_string( 118 | self._getExplainerLP(auto_trace=self._auto_trace)+self._preprocessor.get_translation(), 119 | lambda ast: builder.add(ast), 120 | ) 121 | 122 | with control.backend() as backend: 123 | for sym in model.symbols(atoms=True): 124 | atm_id = backend.add_atom(Function('_xclingo_model', [sym], True)) 125 | backend.add_rule([atm_id], [], False) 126 | 127 | control.ground([('base', [])], context=context if context is not None else Context()) 128 | 129 | 130 | def _get_explanations(self, control): 131 | with control.solve(yield_=True) as it: 132 | for expl_model in it: 133 | syms = expl_model.symbols(shown=True) # shown is True because we want to get only the summarized graph 134 | if len(syms)>0: 135 | yield Explanation.from_model(syms) 136 | 137 | def _get_models(self, control): 138 | with control.solve(yield_=True) as it: 139 | for expl_model in it: 140 | yield expl_model 141 | 142 | def get_xclingo_models(self, model:Model) -> Iterable[Explanation]: 143 | control = self._initialize_control() 144 | self.clean_log() 145 | self._ground(control, model) 146 | self.print_messages() 147 | return self._get_models(control) 148 | 149 | def explain(self, model:Model, context=None) -> Iterable[Explanation]: 150 | control = self._initialize_control() 151 | self.clean_log() 152 | self._ground(control, model, context) 153 | self.print_messages() 154 | return self._get_explanations(control) 155 | 156 | 157 | class XclingoControl: 158 | def __init__(self, n_solutions='1', n_explanations='1', auto_trace='none'): 159 | self.n_solutions = n_solutions 160 | self.n_explanations = n_explanations 161 | 162 | self.control = Control([n_solutions if type(n_solutions)==str else str(n_solutions)]) 163 | self.explainer = Explainer( 164 | [ 165 | n_explanations if type(n_explanations)==str else str(n_explanations), 166 | ], 167 | auto_trace=auto_trace 168 | ) 169 | 170 | self._explainer_context = None 171 | 172 | def add(self, name, parameters, program): 173 | """It adds a program to the control. 174 | 175 | Args: 176 | name (str): name of program block to add. 177 | parameters (Iterable[str]): a list (or iterable) of for the program. 178 | program (str): a logic program in ASP format. 179 | """ 180 | self.control.add("base", parameters, program) 181 | self.explainer.add(name, [], program) 182 | 183 | def ground(self, context=None): 184 | """Ground (only base for now) programs. 185 | 186 | Args: 187 | context (Object, optional): Context to be passed to the original program control. Defaults to None. 188 | """ 189 | self.control.ground([("base", [])], context) 190 | 191 | def get_xclingo_models(self): 192 | """Returns the clingo.Model objects of the explainer, this is the models which represent the explanations. 193 | 194 | Returns: 195 | Generator[cilngo.Model]: a generator of clingo.Model objects. 196 | """ 197 | with self.control.solve(yield_=True) as it: 198 | for model in it: 199 | return self.explainer.get_xclingo_models(model) 200 | 201 | def explain(self, on_explanation=None): 202 | """Returns a generator of xclingo.explanation.Explanation objects. If on_explanation is not None, it is called for each explanation. 203 | 204 | Args: 205 | on_explanation (Callable, optional): callable that will be called for each Explanation, it must receive Explanation as a parameter. Defaults to None. 206 | 207 | Yields: 208 | Explation: a tree-like object that represents an explanation. 209 | """ 210 | with self.control.solve(yield_=True) as it: 211 | for m in it: 212 | if on_explanation is None: 213 | yield self.explainer.explain(m, context=self._explainer_context) 214 | else: 215 | on_explanation(self.explainer.explain(m, context=self._explainer_context)) 216 | 217 | def _default_output(self): 218 | output = '' 219 | n = 0 220 | for answer in self.explain(): 221 | n = 1 222 | output += f'Answer {n}\n' 223 | output += '\n'.join([expl.ascii_tree() for expl in answer]) 224 | output += '\n' 225 | return output -------------------------------------------------------------------------------- /xclingo/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.0b14" 2 | -------------------------------------------------------------------------------- /xclingo/explanation/__init__.py: -------------------------------------------------------------------------------- 1 | from ._explanation import Explanation -------------------------------------------------------------------------------- /xclingo/explanation/_explanation.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | from clingo import Symbol 3 | 4 | 5 | class Explanation: 6 | @staticmethod 7 | def from_model(symbols: Iterable[Symbol]): 8 | table = dict() 9 | for s in symbols: 10 | parent = str(s.arguments[0]) 11 | child = str(s.arguments[1]) 12 | child_item = table.get(child, None) 13 | parent_item = table.get(parent, None) 14 | 15 | if child_item is None: 16 | child_item = ExplanationNode() 17 | table[child] = child_item 18 | child_item.add_label(str(s.arguments[2]).strip('"')) 19 | 20 | if parent_item is None: 21 | parent_item = ( 22 | ExplanationRoot(explanation_atoms=symbols) 23 | if parent == "root" 24 | else ExplanationNode() 25 | ) 26 | parent_item.add_cause(child_item) 27 | table[parent] = parent_item 28 | 29 | if child_item not in parent_item.causes: 30 | parent_item.add_cause(child_item) 31 | 32 | return table["root"] 33 | 34 | @staticmethod 35 | def ascii_branch(level): 36 | if level > 0: 37 | return " |" * (level) + "__" 38 | else: 39 | return "" 40 | 41 | def preorder_iterator(self): 42 | stack = [iter([self])] 43 | level = 0 44 | while stack: 45 | try: 46 | current = next(stack[-1]) 47 | yield (current, level) 48 | stack.append(iter(current.causes)) 49 | level += 1 50 | except StopIteration: 51 | stack.pop() 52 | level += -1 53 | 54 | def ascii_tree(self): 55 | expl = "" 56 | for node, level in self.preorder_iterator(): 57 | expl += "{branch}{text}\n".format( 58 | branch=Explanation.ascii_branch(level), 59 | text=node.get_node_text(), 60 | ) 61 | return expl 62 | 63 | def is_equal(self, other): 64 | if not isinstance(other, Explanation): 65 | return False 66 | 67 | for (node1, level1), (node2, level2) in zip( 68 | self.preorder_iterator(), other.preorder_iterator() 69 | ): 70 | if not node1._node_equals(node2): 71 | return False 72 | 73 | if level1 != level2: 74 | return False 75 | 76 | return True 77 | 78 | def add_cause(self, cause): 79 | self.causes.append(cause) 80 | 81 | def add_label(self, label): 82 | self.labels.add(label) 83 | 84 | 85 | class ExplanationRoot(Explanation): 86 | def __init__(self, causes=None, explanation_atoms=None): 87 | self.causes = list() if causes is None else causes 88 | self._explanation_atoms = explanation_atoms 89 | 90 | def get_node_text(self): 91 | return " *" 92 | 93 | def _node_equals(self, other): 94 | if not isinstance(other, ExplanationRoot): 95 | return False 96 | 97 | return True 98 | 99 | 100 | class ExplanationNode(Explanation): 101 | """ 102 | A non-binary tree. 103 | """ 104 | 105 | def __init__(self, labels=None, causes=None): 106 | self.labels = set() if labels is None else labels 107 | self.causes = list() if causes is None else causes 108 | 109 | def get_node_text(self): 110 | return ";".join(sorted(list(self.labels))) 111 | 112 | def _node_equals(self, other): 113 | if not isinstance(other, ExplanationNode): 114 | return False 115 | 116 | if self.label.replace_values() != other.label.replace_values(): 117 | return False 118 | 119 | return True 120 | -------------------------------------------------------------------------------- /xclingo/preprocessor/__init__.py: -------------------------------------------------------------------------------- 1 | from ._preprocessor import Preprocessor -------------------------------------------------------------------------------- /xclingo/preprocessor/_preprocessor.py: -------------------------------------------------------------------------------- 1 | from clingo.symbol import Number 2 | from ._utils import ( 3 | translate_show_all, 4 | translate_trace, 5 | translate_trace_all, 6 | translate_mute, 7 | is_xclingo_label, 8 | is_xclingo_show_trace, 9 | is_choice_rule, 10 | is_label_rule, 11 | is_xclingo_mute, 12 | is_constraint, 13 | is_disyunctive_head, 14 | ) 15 | from clingo import ast 16 | 17 | 18 | class Preprocessor: 19 | def __init__(self): 20 | self._rule_count = 1 21 | self._last_trace_rule = None 22 | self._translation = "" 23 | 24 | def increment_rule_count(self): 25 | n = self._rule_count 26 | self._rule_count += 1 27 | return n 28 | 29 | @staticmethod 30 | def translate_annotations(program): 31 | return translate_trace_all(translate_show_all(translate_trace(translate_mute(program)))) 32 | 33 | def propagates(self, lit_list): 34 | for lit in lit_list: 35 | if lit.sign == ast.Sign.NoSign and lit.atom.ast_type == ast.ASTType.SymbolicAtom: 36 | yield lit 37 | 38 | def sup_body(self, lit_list): 39 | loc = ast.Location( 40 | ast.Position("", 0, 0), 41 | ast.Position("", 0, 0), 42 | ) 43 | for lit in lit_list: 44 | if lit.ast_type == ast.ASTType.Literal: 45 | if lit.atom.ast_type == ast.ASTType.SymbolicAtom: 46 | yield ast.Literal( 47 | loc, 48 | lit.sign, 49 | ast.SymbolicAtom( 50 | ast.Function( 51 | loc, 52 | "_xclingo_model", 53 | [lit.atom.symbol], 54 | False, 55 | ) 56 | ), 57 | ) 58 | 59 | elif lit.atom.ast_type == ast.ASTType.BodyAggregate: 60 | yield ast.Literal( 61 | loc, 62 | lit.sign, 63 | ast.BodyAggregate( 64 | loc, 65 | left_guard=lit.atom.left_guard, 66 | function=lit.atom.function, 67 | elements=[ 68 | ast.BodyAggregateElement( 69 | terms=list(self.sup_body(e.terms)), 70 | condition=list(self.sup_body(e.condition)), 71 | ) 72 | for e in lit.atom.elements 73 | ], 74 | right_guard=lit.atom.right_guard, 75 | ), 76 | ) 77 | 78 | else: 79 | yield lit 80 | 81 | else: 82 | yield lit 83 | 84 | def sup_head(self, rule_id, rule_ast): 85 | loc = ast.Location( 86 | ast.Position("", 0, 0), 87 | ast.Position("", 0, 0), 88 | ) 89 | head = ast.Literal( 90 | loc, 91 | ast.Sign.NoSign, 92 | ast.SymbolicAtom( 93 | ast.Function( 94 | loc, 95 | "_xclingo_sup", 96 | [ 97 | ast.SymbolicTerm(loc, Number(rule_id)), 98 | rule_ast.head.atom, 99 | ast.Function(loc, "", list(self.propagates(rule_ast.body)), False), # tuple 100 | ], 101 | False, 102 | ), 103 | ), 104 | ) 105 | return head 106 | 107 | def support_rule(self, rule_id, rule_ast): 108 | loc = ast.Location( 109 | ast.Position("", 0, 0), 110 | ast.Position("", 0, 0), 111 | ) 112 | head = self.sup_head(rule_id, rule_ast) 113 | body = list(self.sup_body(rule_ast.body)) 114 | 115 | return ast.Rule(loc, head, body) 116 | 117 | def fbody_head(self, rule_id, rule_ast): 118 | loc = ast.Location( 119 | ast.Position("", 0, 0), 120 | ast.Position("", 0, 0), 121 | ) 122 | head = ast.Literal( 123 | loc, 124 | ast.Sign.NoSign, 125 | ast.SymbolicAtom( 126 | ast.Function( 127 | loc, 128 | "_xclingo_fbody", 129 | [ 130 | ast.SymbolicTerm(loc, Number(rule_id)), 131 | rule_ast.head.atom, 132 | ast.Function(loc, "", list(self.propagates(rule_ast.body)), False), # tuple 133 | ], 134 | False, 135 | ), 136 | ), 137 | ) 138 | return head 139 | 140 | def fbody_body(self, lit_list): 141 | loc = ast.Location( 142 | ast.Position("", 0, 0), 143 | ast.Position("", 0, 0), 144 | ) 145 | for lit in lit_list: 146 | if lit.ast_type == ast.ASTType.Literal: 147 | if lit.atom.ast_type == ast.ASTType.SymbolicAtom: 148 | if lit.sign == ast.Sign.NoSign: 149 | yield ast.Literal( 150 | loc, 151 | lit.sign, 152 | ast.SymbolicAtom( 153 | ast.Function( 154 | loc, 155 | "_xclingo_f_atom", 156 | [lit.atom.symbol], 157 | False, 158 | ) 159 | ), 160 | ) 161 | else: 162 | yield ast.Literal( 163 | loc, 164 | ast.Sign.Negation, 165 | ast.SymbolicAtom( 166 | ast.Function( 167 | loc, 168 | "_xclingo_model", 169 | [lit.atom.symbol], 170 | False, 171 | ) 172 | ), 173 | ) 174 | 175 | elif lit.atom.ast_type == ast.ASTType.BodyAggregate: 176 | yield ast.Literal( 177 | loc, 178 | lit.sign, 179 | ast.BodyAggregate( 180 | loc, 181 | left_guard=lit.atom.left_guard, 182 | function=lit.atom.function, 183 | elements=[ 184 | ast.BodyAggregateElement( 185 | terms=list(self.fbody_body(e.terms)), 186 | condition=list(self.fbody_body(e.condition)), 187 | ) 188 | for e in lit.atom.elements 189 | ], 190 | right_guard=lit.atom.right_guard, 191 | ), 192 | ) 193 | 194 | else: 195 | yield lit 196 | else: 197 | yield lit 198 | 199 | def fbody_rule(self, rule_id, rule_ast): 200 | loc = ast.Location( 201 | ast.Position("", 0, 0), 202 | ast.Position("", 0, 0), 203 | ) 204 | head = self.fbody_head(rule_id, rule_ast) 205 | body = list(self.fbody_body(rule_ast.body)) 206 | return ast.Rule(loc, head, body) 207 | 208 | def label_rule(self, rule_id, label_rule_ast, rule_body): 209 | loc = ast.Location( 210 | ast.Position("", 0, 0), 211 | ast.Position("", 0, 0), 212 | ) 213 | head_var = ast.Variable(loc, "Head") 214 | head = ast.Literal( 215 | loc, 216 | label_rule_ast.head.sign, 217 | ast.SymbolicAtom( 218 | ast.Function( 219 | loc, 220 | label_rule_ast.head.atom.symbol.name, 221 | [head_var, label_rule_ast.head.atom.symbol.arguments[1]], 222 | False, 223 | ) 224 | ), 225 | ) 226 | body = [ 227 | ast.Literal( 228 | loc, 229 | ast.Sign.NoSign, 230 | ast.SymbolicAtom( 231 | ast.Function( 232 | loc, 233 | "_xclingo_f", 234 | [ 235 | ast.SymbolicTerm(loc, Number(rule_id)), 236 | head_var, 237 | ast.Function( 238 | loc, 239 | "", 240 | list(self.propagates(rule_body)), 241 | False, 242 | ), 243 | ], 244 | False, 245 | ) 246 | ), 247 | ) 248 | ] 249 | rule = ast.Rule(loc, head, body) 250 | return rule 251 | 252 | def label_atom(self, rule_ast): 253 | loc = ast.Location( 254 | ast.Position("", 0, 0), 255 | ast.Position("", 0, 0), 256 | ) 257 | fatom = ast.Literal( 258 | loc, 259 | ast.Sign.NoSign, 260 | ast.SymbolicAtom( 261 | ast.Function( 262 | loc, 263 | "_xclingo_intree", 264 | [rule_ast.head.atom.symbol.arguments[0]], 265 | False, 266 | ) 267 | ), 268 | ) 269 | body = [fatom] + list(self.sup_body(rule_ast.body)) 270 | rule = ast.Rule(loc, rule_ast.head, body) 271 | return rule 272 | 273 | def show_trace(self, rule_ast): 274 | loc = ast.Location( 275 | ast.Position("", 0, 0), 276 | ast.Position("", 0, 0), 277 | ) 278 | literal_head = ast.Literal( 279 | loc, 280 | ast.Sign.NoSign, 281 | ast.SymbolicAtom(rule_ast.head.atom.symbol.arguments[0]), 282 | ) 283 | rule = ast.Rule( 284 | loc, rule_ast.head, list(self.sup_body([literal_head] + list(rule_ast.body))) 285 | ) 286 | return rule 287 | 288 | def mute(self, rule_ast): 289 | loc = ast.Location( 290 | ast.Position("", 0, 0), 291 | ast.Position("", 0, 0), 292 | ) 293 | literal_head = ast.Literal( 294 | loc, 295 | ast.Sign.NoSign, 296 | ast.SymbolicAtom(rule_ast.head.atom.symbol.arguments[0]), 297 | ) 298 | rule = ast.Rule( 299 | loc, rule_ast.head, list(self.sup_body([literal_head] + list(rule_ast.body))) 300 | ) 301 | return rule 302 | 303 | def add_to_translation(self, a): 304 | self._translation += f"{a}\n" 305 | 306 | def add_comment_to_translation(self, a): 307 | self._translation += f"% {a}\n" 308 | 309 | def translate_rule(self, rule_ast): 310 | self.add_comment_to_translation(rule_ast) 311 | if rule_ast.ast_type == ast.ASTType.Rule and not is_constraint(rule_ast): 312 | if is_xclingo_label(rule_ast): 313 | if is_label_rule(rule_ast): 314 | self._last_trace_rule = rule_ast 315 | return 316 | self.add_to_translation(self.label_atom(rule_ast)) 317 | elif is_xclingo_show_trace(rule_ast): 318 | self.add_to_translation(self.show_trace(rule_ast)) 319 | pass 320 | elif is_xclingo_mute(rule_ast): 321 | self.add_to_translation(self.mute(rule_ast)) 322 | else: 323 | rule_id = self.increment_rule_count() 324 | if is_choice_rule(rule_ast) or is_disyunctive_head(rule_ast): 325 | for cond_lit in rule_ast.head.elements: 326 | false_rule = ast.Rule( 327 | ast.Location( 328 | ast.Position("", 0, 0), 329 | ast.Position("", 0, 0), 330 | ), 331 | cond_lit.literal, 332 | list(cond_lit.condition) + list(rule_ast.body), 333 | ) 334 | self.add_to_translation(self.support_rule(rule_id, false_rule)) 335 | self.add_to_translation(self.fbody_rule(rule_id, false_rule)) 336 | if self._last_trace_rule is not None: 337 | self.add_to_translation( 338 | self.label_rule(rule_id, self._last_trace_rule, false_rule.body) 339 | ) 340 | if self._last_trace_rule is not None: 341 | self._last_trace_rule = None 342 | else: # Other cases 343 | self.add_to_translation(self.support_rule(rule_id, rule_ast)) 344 | self.add_to_translation(self.fbody_rule(rule_id, rule_ast)) 345 | if self._last_trace_rule is not None: 346 | self.add_to_translation( 347 | self.label_rule(rule_id, self._last_trace_rule, rule_ast.body) 348 | ) 349 | self._last_trace_rule = None 350 | 351 | def translate_program(self, program, name=""): 352 | self._translation += "%" * 8 + name + "%" * 8 + "\n" 353 | ast.parse_string( 354 | Preprocessor.translate_annotations(program), 355 | lambda ast: self.translate_rule(ast), 356 | ) 357 | 358 | def get_translation(self): 359 | return self._translation 360 | -------------------------------------------------------------------------------- /xclingo/preprocessor/_utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | from clingo import ast 3 | 4 | 5 | def translate_trace(program): 6 | """ 7 | Replaces the 'label_rule' magic comments in the given program for a version of the rules labelled with theory atoms. 8 | @param str program: the program that is intended to be modified. 9 | @return str: 10 | """ 11 | for hit in re.findall('(%!trace_rule \{(".*")(?:,(.*))?\}[ ]*[\n ]*)', program): 12 | # 0: original match 1: label text 2: label parameters 3: head of the rule 4: body of the rule 13 | program = program.replace( 14 | hit[0], 15 | "{name}(id, @label({text}, ({parameters},) )).\n".format( 16 | text=hit[1], parameters=hit[2] if hit[2] else "", name="_xclingo_label" 17 | ), 18 | ) 19 | 20 | return program 21 | 22 | 23 | def translate_trace_all(program): 24 | """ 25 | Replaces the 'label_atoms' magic comments in the given program for label_atoms rule. 26 | @param str program: the program that is intended to be modified 27 | @return str: 28 | """ 29 | for hit in re.findall( 30 | '(%!trace \{(".*")(?:,(.*))?\} (\-?[_a-z][_a-zA-Z0-9]*(?:\((?:[\-\+a-zA-Z0-9 \(\)\,\_])+\))?)(?:[ ]*:[ ]*(.*))?\.)', 31 | program, 32 | ): 33 | # 0: original match 1: "label" 2:v1,v2 3: head 4: body. 34 | program = program.replace( 35 | hit[0], 36 | "{name}({head}, @label({text}, ({parameters},)) ){body}.".format( 37 | head=hit[3], 38 | text=hit[1], 39 | parameters=hit[2], 40 | body=(" :- " + hit[4]) if hit[4] else "", 41 | name="_xclingo_label", 42 | ), 43 | ) 44 | 45 | return program 46 | 47 | 48 | def translate_show_all(program): 49 | """ 50 | Replaces 'explain' magic comments in the given program for a rule version of those magic comments. 51 | @param str program: 52 | @return: 53 | """ 54 | for hit in re.findall( 55 | "(%!show_trace ((\-)?([_a-z][_a-zA-Z0-9]*(?:\((?:[\-a-zA-Z0-9 \(\)\,\_])+\))?)(?:[ ]*:[ ]*(.*))?\.))", 56 | program, 57 | ): 58 | # 0: original match 1: rule 2: negative_sign 3: head of the rule 4: body of the rule 59 | program = program.replace( 60 | hit[0], 61 | "{name}({classic_negation}{head}){body}.".format( 62 | sign="" if not hit[2] else "n", 63 | name="_xclingo_show_trace", 64 | head=hit[3], 65 | classic_negation="" if not hit[2] else "-", 66 | body=" :- " + hit[4] if hit[4] else "", 67 | ), 68 | ) 69 | 70 | return program 71 | 72 | 73 | def translate_mute(program): 74 | """ 75 | Replaces 'explain' magic comments in the given program for a rule version of those magic comments. 76 | @param str program: 77 | @return: 78 | """ 79 | for hit in re.findall( 80 | "(%!mute ((\-)?([_a-z][_a-zA-Z0-9]*(?:\((?:[\-a-zA-Z0-9 \(\)\,\_])+\))?)(?:[ ]*:[ ]*(.*))?\.))", 81 | program, 82 | ): 83 | # 0: original match 1: rule 2: negative_sign 3: head of the rule 4: body of the rule 84 | program = program.replace( 85 | hit[0], 86 | "{name}({classic_negation}{head}){body}.".format( 87 | sign="" if not hit[2] else "n", 88 | name="_xclingo_muted", 89 | head=hit[3], 90 | classic_negation="" if not hit[2] else "-", 91 | body=" :- " + hit[4] if hit[4] else "", 92 | ), 93 | ) 94 | 95 | return program 96 | 97 | 98 | def is_constraint(rule_ast): 99 | if rule_ast.ast_type == ast.ASTType.Rule: 100 | if hasattr(rule_ast.head, "atom"): 101 | return ( 102 | rule_ast.head.atom.ast_type == ast.ASTType.BooleanConstant 103 | and rule_ast.head.atom == ast.BooleanConstant(0) 104 | ) 105 | return False 106 | 107 | 108 | def is_xclingo_label(rule_ast): 109 | return ( 110 | rule_ast.head.ast_type == ast.ASTType.Literal 111 | and rule_ast.head.atom.symbol.ast_type == ast.ASTType.Function 112 | and rule_ast.head.atom.symbol.name == "_xclingo_label" 113 | ) 114 | 115 | 116 | def is_xclingo_show_trace(rule_ast): 117 | return ( 118 | rule_ast.head.ast_type == ast.ASTType.Literal 119 | and rule_ast.head.atom.symbol.ast_type == ast.ASTType.Function 120 | and rule_ast.head.atom.symbol.name == "_xclingo_show_trace" 121 | ) 122 | 123 | 124 | def is_xclingo_mute(rule_ast): 125 | return ( 126 | rule_ast.head.ast_type == ast.ASTType.Literal 127 | and rule_ast.head.atom.symbol.ast_type == ast.ASTType.Function 128 | and rule_ast.head.atom.symbol.name == "_xclingo_muted" 129 | ) 130 | 131 | 132 | def is_label_rule(rule_ast): 133 | # Precondition: is_xclingo_label(rule_ast) == True 134 | return str(rule_ast.head.atom.symbol.arguments[0]) == "id" 135 | 136 | 137 | def is_choice_rule(rule_ast): 138 | return ( 139 | rule_ast.head.ast_type == ast.ASTType.Aggregate 140 | and hasattr(rule_ast.head, "function") == False 141 | ) 142 | 143 | 144 | def is_disyunctive_head(rule_ast): 145 | return rule_ast.head.ast_type == ast.ASTType.Disjunction 146 | -------------------------------------------------------------------------------- /xclingo/xclingo_lp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramucas/xclingo2/07e69273fe6a45b4586018d3c7f250ac370f9882/xclingo/xclingo_lp/__init__.py -------------------------------------------------------------------------------- /xclingo/xclingo_lp/autotrace_all.lp: -------------------------------------------------------------------------------- 1 | % Genrerates a label for each atom in the explanation 2 | _xclingo_label(Head, Head) :- _xclingo_child(_, Head). 3 | -------------------------------------------------------------------------------- /xclingo/xclingo_lp/autotrace_facts.lp: -------------------------------------------------------------------------------- 1 | % Genrerates a label for each fact atom in the explanation 2 | _xclingo_label(Head, Head) :- _xclingo_child(_, Head), _xclingo_fbody(_, Head, ()). 3 | -------------------------------------------------------------------------------- /xclingo/xclingo_lp/xclingo.lp: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%% xclingo.lp %%%%%%%%%%%%%%%%% 2 | % TODO: tuples 3 | _xclingo_inbody(@inbody(Body)) :- _xclingo_sup(_, _, Body). 4 | 5 | % Which atom to explain 6 | 1 {_xclingo_to_explain(A) : _xclingo_show_trace(A)} 1. 7 | 8 | % Whcih atom to use for explain it. 9 | _xclingo_relevant(ToExplainAtom) :- _xclingo_to_explain(ToExplainAtom). 10 | _xclingo_relevant(R) :- _xclingo_inbody((R, Body)), _xclingo_sup(_, Atom, Body), _xclingo_relevant(Atom), _xclingo_model(R). 11 | %%%%%%%%%%%%%%%%%%%%% 12 | 13 | % Generates explanations. 14 | 1{_xclingo_f(RuleID, Atom, Body) : _xclingo_fbody(RuleID, Atom, Body)}1 :- _xclingo_relevant(Atom). 15 | _xclingo_f_atom(Atom) :- _xclingo_f(_, Atom, _). 16 | 17 | % Atom tree 18 | _xclingo_child(root, ToExplainAtom) :- _xclingo_f(_, ToExplainAtom, _), _xclingo_to_explain(ToExplainAtom). 19 | _xclingo_child(Caused, Cause) :- not _xclingo_muted(Cause), _xclingo_inbody((Cause, Body)), _xclingo_f(_, Caused, Body), _xclingo_child(_, Caused). 20 | _xclingo_intree(X;Y) :- _xclingo_child(X,Y). 21 | 22 | % Label tree 23 | _xclingo_marked(X) :- _xclingo_label(X, _). 24 | _xclingo_marked(root). 25 | % 26 | _xclingo_skip(X, Y) :- _xclingo_child(X, Y), not _xclingo_label(X, _). 27 | _xclingo_skip(X, Y) :- _xclingo_child(X, Y), not _xclingo_label(Y, _). 28 | % 29 | _xclingo_reach(X, Z) :- _xclingo_skip(X, Z). 30 | _xclingo_reach(X, Z) :- _xclingo_reach(X, Y), _xclingo_skip(Y, Z), not _xclingo_marked(Y). 31 | % 32 | _xclingo_tree(P, C) :- _xclingo_child(P, C), not _xclingo_skip(P, C). 33 | _xclingo_tree(P, C) :- _xclingo_reach(P, C), _xclingo_marked(P), _xclingo_marked(C). 34 | % 35 | _xclingo_label_tree(X, Y, Label) :- _xclingo_tree(X, Y), _xclingo_label(Y, Label). 36 | 37 | % for projection 38 | _xclingo_label_tree(root, ChildLabel) :- _xclingo_label_tree(root, C, ChildLabel). 39 | _xclingo_label_tree(ParentLabel, ChildLabel) :- _xclingo_label_tree(PP, P, ParentLabel), _xclingo_label_tree(P, C, ChildLabel). 40 | 41 | % Necesitamos todo? 42 | % cause(IDCause, ToExplainAtom, root, root) :- f(IDCause, ToExplainAtom, _), to_explain(ToExplainAtom). 43 | % cause(IDCause, Cause, Caused, IDCaused) :- f(IDCause, Cause, _), inbody((Cause, Body)), f(IDCaused, Caused, Body). 44 | % cause(true, true, Caused, ID) :- f(ID, Caused, empty). 45 | 46 | #show _xclingo_label_tree/3. 47 | #project _xclingo_label_tree/2. 48 | --------------------------------------------------------------------------------