clingexplaid 1.0.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clingexplaid/__init__.py +3 -0
- clingexplaid/__main__.py +21 -0
- clingexplaid/cli/__init__.py +0 -0
- clingexplaid/cli/clingo_app.py +352 -0
- clingexplaid/muc/__init__.py +9 -0
- clingexplaid/muc/core_computer.py +140 -0
- clingexplaid/propagators/__init__.py +16 -0
- clingexplaid/propagators/constants.py +11 -0
- clingexplaid/propagators/propagator_decision_order.py +176 -0
- clingexplaid/py.typed +0 -0
- clingexplaid/transformers/__init__.py +19 -0
- clingexplaid/transformers/constants.py +6 -0
- clingexplaid/transformers/exceptions.py +15 -0
- clingexplaid/transformers/transformer_assumption.py +112 -0
- clingexplaid/transformers/transformer_constraint.py +72 -0
- clingexplaid/transformers/transformer_fact.py +77 -0
- clingexplaid/transformers/transformer_optimization_remover.py +61 -0
- clingexplaid/transformers/transformer_rule_id.py +78 -0
- clingexplaid/transformers/transformer_rule_splitter.py +100 -0
- clingexplaid/unsat_constraints/__init__.py +9 -0
- clingexplaid/unsat_constraints/constants.py +5 -0
- clingexplaid/unsat_constraints/unsat_constraint_computer.py +121 -0
- clingexplaid/utils/__init__.py +90 -0
- clingexplaid/utils/logging.py +90 -0
- clingexplaid/utils/parser.py +58 -0
- clingexplaid/utils/types.py +13 -0
- clingexplaid-1.0.9.dist-info/LICENSE +21 -0
- clingexplaid-1.0.9.dist-info/METADATA +244 -0
- clingexplaid-1.0.9.dist-info/RECORD +32 -0
- clingexplaid-1.0.9.dist-info/WHEEL +5 -0
- clingexplaid-1.0.9.dist-info/entry_points.txt +2 -0
- clingexplaid-1.0.9.dist-info/top_level.txt +1 -0
clingexplaid/__init__.py
ADDED
clingexplaid/__main__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The main entry point for the application.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from clingo.application import clingo_main
|
|
8
|
+
|
|
9
|
+
from .cli.clingo_app import ClingoExplaidApp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
"""
|
|
14
|
+
Run the main function.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
clingo_main(ClingoExplaidApp(sys.argv[0]), sys.argv[1:] + ["-V0"])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
App Module: clingexplaid CLI clingo app
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from importlib.metadata import version
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Callable, Dict, List, Optional, Sequence, Set, Tuple
|
|
10
|
+
from warnings import warn
|
|
11
|
+
|
|
12
|
+
import clingo
|
|
13
|
+
from clingo.application import Application, Flag
|
|
14
|
+
|
|
15
|
+
from ..muc import CoreComputer
|
|
16
|
+
from ..propagators import DecisionOrderPropagator
|
|
17
|
+
from ..transformers import AssumptionTransformer, OptimizationRemover
|
|
18
|
+
from ..unsat_constraints import UnsatConstraintComputer
|
|
19
|
+
from ..utils import get_constants_from_arguments
|
|
20
|
+
from ..utils.logging import BACKGROUND_COLORS, COLORS
|
|
21
|
+
|
|
22
|
+
HYPERLINK_MASK = "\033]8;{};{}\033\\{}\033]8;;\033\\"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ClingoExplaidApp(Application):
|
|
26
|
+
"""
|
|
27
|
+
Application class for executing clingo-explaid functionality on the command line
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# pylint: disable = too-many-instance-attributes
|
|
31
|
+
|
|
32
|
+
CLINGEXPLAID_METHODS = {
|
|
33
|
+
"muc": "Description for MUC method",
|
|
34
|
+
"unsat-constraints": "Description for unsat-constraints method",
|
|
35
|
+
"show-decisions": "Visualize the decision process of clingo during solving",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def __init__(self, name: str) -> None:
|
|
39
|
+
# pylint: disable = unused-argument
|
|
40
|
+
self.methods: Set[str] = set()
|
|
41
|
+
self.method_functions: Dict[str, Callable] = { # type: ignore
|
|
42
|
+
m: getattr(self, f'_method_{m.replace("-", "_")}') for m in self.CLINGEXPLAID_METHODS
|
|
43
|
+
}
|
|
44
|
+
self.method_flags: Dict[str, Flag] = {m: Flag() for m in self.CLINGEXPLAID_METHODS}
|
|
45
|
+
self.argument_constants: Dict[str, str] = {}
|
|
46
|
+
|
|
47
|
+
# SHOW DECISIONS
|
|
48
|
+
self._show_decisions_decision_signatures: Dict[str, int] = {}
|
|
49
|
+
self._show_decisions_model_id: int = 1
|
|
50
|
+
|
|
51
|
+
# MUC
|
|
52
|
+
self._muc_assumption_signatures: Dict[str, int] = {}
|
|
53
|
+
self._muc_id: int = 1
|
|
54
|
+
|
|
55
|
+
def _initialize(self) -> None:
|
|
56
|
+
# add enabled methods to self.methods
|
|
57
|
+
for method, flag in self.method_flags.items():
|
|
58
|
+
if flag.flag:
|
|
59
|
+
self.methods.add(method)
|
|
60
|
+
|
|
61
|
+
if len(self.methods) == 0:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Clingexplaid was called without any method, please select at least one of the following methods: "
|
|
64
|
+
f"[{', '.join(['--' + str(m) for m in self.CLINGEXPLAID_METHODS])}]"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _parse_signature(signature_string: str) -> Tuple[str, int]:
|
|
69
|
+
match_result = re.match(r"^([a-zA-Z]+)/([0-9]+)$", signature_string)
|
|
70
|
+
if match_result is None:
|
|
71
|
+
raise ValueError("Wrong signature Format")
|
|
72
|
+
return match_result.group(1), int(match_result.group(2))
|
|
73
|
+
|
|
74
|
+
def _parse_assumption_signature(self, assumption_signature: str) -> bool:
|
|
75
|
+
if not self.method_flags["muc"]:
|
|
76
|
+
print("PARSE ERROR: The assumption signature option is only available if the flag --muc is enabled")
|
|
77
|
+
return False
|
|
78
|
+
assumption_signature_string = assumption_signature.replace("=", "").strip()
|
|
79
|
+
try:
|
|
80
|
+
signature, arity = self._parse_signature(assumption_signature_string)
|
|
81
|
+
except ValueError:
|
|
82
|
+
print(
|
|
83
|
+
"PARSE ERROR: Wrong signature format. The assumption signatures have to follow the format "
|
|
84
|
+
"<assumption-name>/<arity>"
|
|
85
|
+
)
|
|
86
|
+
return False
|
|
87
|
+
self._muc_assumption_signatures[signature] = arity
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
def _parse_decision_signature(self, decision_signature: str) -> bool:
|
|
91
|
+
if not self.method_flags["show-decisions"]:
|
|
92
|
+
print(
|
|
93
|
+
"PARSE ERROR: The decision signature option is only available if the flag --show-decisions is enabled"
|
|
94
|
+
)
|
|
95
|
+
return False
|
|
96
|
+
decision_signature_string = decision_signature.replace("=", "").strip()
|
|
97
|
+
try:
|
|
98
|
+
signature, arity = self._parse_signature(decision_signature_string)
|
|
99
|
+
except ValueError:
|
|
100
|
+
print(
|
|
101
|
+
"PARSE ERROR: Wrong signature format. The decision signatures have to follow the format "
|
|
102
|
+
"<assumption-name>/<arity>"
|
|
103
|
+
)
|
|
104
|
+
return False
|
|
105
|
+
self._show_decisions_decision_signatures[signature] = arity
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
def register_options(self, options: clingo.ApplicationOptions) -> None:
|
|
109
|
+
group = "Clingo-Explaid Methods"
|
|
110
|
+
|
|
111
|
+
for method, description in self.CLINGEXPLAID_METHODS.items():
|
|
112
|
+
options.add_flag(
|
|
113
|
+
group=group,
|
|
114
|
+
option=method,
|
|
115
|
+
description=description,
|
|
116
|
+
target=self.method_flags[method],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
group = "MUC Options"
|
|
120
|
+
|
|
121
|
+
options.add(
|
|
122
|
+
group,
|
|
123
|
+
"assumption-signature,a",
|
|
124
|
+
"Facts matching with this signature will be converted to assumptions for finding a MUC "
|
|
125
|
+
"(default: all facts)",
|
|
126
|
+
self._parse_assumption_signature,
|
|
127
|
+
multi=True,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
group = "Show Decisions Options"
|
|
131
|
+
|
|
132
|
+
options.add(
|
|
133
|
+
group,
|
|
134
|
+
"decision-signature",
|
|
135
|
+
"When --show-decisions is enabled, show only decisions matching with this signature "
|
|
136
|
+
"(default: show all decisions)",
|
|
137
|
+
self._parse_decision_signature,
|
|
138
|
+
multi=True,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# group = "General Options"
|
|
142
|
+
|
|
143
|
+
def _apply_assumption_transformer(
|
|
144
|
+
self, signatures: Dict[str, int], files: List[str]
|
|
145
|
+
) -> Tuple[str, AssumptionTransformer]:
|
|
146
|
+
signature_set = set(self._muc_assumption_signatures.items()) if signatures else None
|
|
147
|
+
at = AssumptionTransformer(signatures=signature_set)
|
|
148
|
+
if not files:
|
|
149
|
+
program_transformed = at.parse_files("-")
|
|
150
|
+
else:
|
|
151
|
+
program_transformed = at.parse_files(files)
|
|
152
|
+
return program_transformed, at
|
|
153
|
+
|
|
154
|
+
def _print_muc(self, muc_string: str) -> None:
|
|
155
|
+
print(f"{BACKGROUND_COLORS['BLUE']} MUC {BACKGROUND_COLORS['LIGHT_BLUE']} {self._muc_id} {COLORS['NORMAL']}")
|
|
156
|
+
print(f"{COLORS['BLUE']}{muc_string}{COLORS['NORMAL']}")
|
|
157
|
+
self._muc_id += 1
|
|
158
|
+
|
|
159
|
+
def _method_muc(
|
|
160
|
+
self,
|
|
161
|
+
control: clingo.Control,
|
|
162
|
+
files: List[str],
|
|
163
|
+
compute_unsat_constraints: bool = False,
|
|
164
|
+
) -> None:
|
|
165
|
+
program_transformed, at = self._apply_assumption_transformer(
|
|
166
|
+
signatures=self._muc_assumption_signatures, files=files
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# remove optimization statements
|
|
170
|
+
optr = OptimizationRemover()
|
|
171
|
+
program_transformed = optr.parse_string(program_transformed)
|
|
172
|
+
|
|
173
|
+
control.add("base", [], program_transformed)
|
|
174
|
+
control.ground([("base", [])])
|
|
175
|
+
|
|
176
|
+
assumptions = at.get_assumptions(control, constants=self.argument_constants)
|
|
177
|
+
cc = CoreComputer(control, assumptions)
|
|
178
|
+
|
|
179
|
+
max_models = int(control.configuration.solve.models) # type: ignore
|
|
180
|
+
print("Solving...")
|
|
181
|
+
|
|
182
|
+
# Case: Finding a single MUC
|
|
183
|
+
if max_models == -1:
|
|
184
|
+
control.solve(assumptions=list(assumptions), on_core=cc.shrink)
|
|
185
|
+
|
|
186
|
+
if cc.minimal is None:
|
|
187
|
+
print("SATISFIABLE: Instance has no MUCs")
|
|
188
|
+
return
|
|
189
|
+
if len(list(cc.minimal)) == 0:
|
|
190
|
+
print(
|
|
191
|
+
"NO MUCS CONTAINED: The unsatisfiability of this program is not induced by the provided assumptions"
|
|
192
|
+
)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
muc_string = " ".join(cc.muc_to_string(cc.minimal))
|
|
196
|
+
self._print_muc(muc_string)
|
|
197
|
+
|
|
198
|
+
if compute_unsat_constraints:
|
|
199
|
+
self._method_unsat_constraints(
|
|
200
|
+
control=clingo.Control(),
|
|
201
|
+
files=files,
|
|
202
|
+
assumption_string=muc_string,
|
|
203
|
+
output_prefix_active=f"{COLORS['RED']}├──{COLORS['NORMAL']}",
|
|
204
|
+
output_prefix_passive=f"{COLORS['RED']}│ {COLORS['NORMAL']}",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Case: Finding multiple MUCs
|
|
208
|
+
if max_models >= 0:
|
|
209
|
+
program_unsat = False
|
|
210
|
+
with control.solve(assumptions=list(assumptions), yield_=True) as solve_handle:
|
|
211
|
+
if not solve_handle.get().satisfiable:
|
|
212
|
+
program_unsat = True
|
|
213
|
+
|
|
214
|
+
if program_unsat:
|
|
215
|
+
mucs = 0
|
|
216
|
+
for muc in cc.get_multiple_minimal(max_mucs=max_models):
|
|
217
|
+
mucs += 1
|
|
218
|
+
muc_string = " ".join(cc.muc_to_string(muc))
|
|
219
|
+
self._print_muc(muc_string)
|
|
220
|
+
|
|
221
|
+
if compute_unsat_constraints:
|
|
222
|
+
self._method_unsat_constraints(
|
|
223
|
+
control=clingo.Control(),
|
|
224
|
+
files=files,
|
|
225
|
+
assumption_string=muc_string,
|
|
226
|
+
output_prefix_active=f"{COLORS['RED']}├──{COLORS['NORMAL']}",
|
|
227
|
+
output_prefix_passive=f"{COLORS['RED']}│ {COLORS['NORMAL']}",
|
|
228
|
+
)
|
|
229
|
+
if not mucs:
|
|
230
|
+
print(
|
|
231
|
+
"NO MUCS CONTAINED: The unsatisfiability of this program is not induced by the provided "
|
|
232
|
+
"assumptions"
|
|
233
|
+
)
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
def _print_unsat_constraints(
|
|
237
|
+
self,
|
|
238
|
+
unsat_constraints: Dict[int, str],
|
|
239
|
+
ucc: UnsatConstraintComputer,
|
|
240
|
+
prefix: Optional[str] = None,
|
|
241
|
+
) -> None:
|
|
242
|
+
if prefix is None:
|
|
243
|
+
prefix = ""
|
|
244
|
+
print(f"{prefix}{BACKGROUND_COLORS['RED']} Unsat Constraints {COLORS['NORMAL']}")
|
|
245
|
+
for cid, constraint in unsat_constraints.items():
|
|
246
|
+
location = ucc.get_constraint_location(cid)
|
|
247
|
+
if location is None:
|
|
248
|
+
warn(f"Couldn't find a corresponding file for constraint with id {cid}")
|
|
249
|
+
continue
|
|
250
|
+
relative_file_path = location.begin.filename
|
|
251
|
+
absolute_file_path = str(Path(relative_file_path).absolute().resolve())
|
|
252
|
+
line_beginning = location.begin.line
|
|
253
|
+
line_end = location.end.line
|
|
254
|
+
line_string = (
|
|
255
|
+
f"Line {line_beginning}" if line_beginning == line_end else f"Lines {line_beginning}-{line_end}"
|
|
256
|
+
)
|
|
257
|
+
file_link = "file://" + absolute_file_path
|
|
258
|
+
if " " in absolute_file_path:
|
|
259
|
+
# If there's a space in the filename use a hyperlink
|
|
260
|
+
file_link = HYPERLINK_MASK.format("", file_link, file_link)
|
|
261
|
+
|
|
262
|
+
if location is not None:
|
|
263
|
+
print(
|
|
264
|
+
f"{prefix}{COLORS['RED']}{constraint}"
|
|
265
|
+
f"{COLORS['GREY']} [ {file_link} ]({line_string}){COLORS['NORMAL']}"
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
print(f"{prefix}{COLORS['RED']}{constraint}{COLORS['NORMAL']}")
|
|
269
|
+
|
|
270
|
+
def _method_unsat_constraints(
|
|
271
|
+
self,
|
|
272
|
+
control: clingo.Control,
|
|
273
|
+
files: List[str],
|
|
274
|
+
assumption_string: Optional[str] = None,
|
|
275
|
+
output_prefix_active: str = "",
|
|
276
|
+
output_prefix_passive: str = "",
|
|
277
|
+
) -> None:
|
|
278
|
+
# register DecisionOrderPropagator if flag is enabled
|
|
279
|
+
if self.method_flags["show-decisions"]:
|
|
280
|
+
decision_signatures = set(self._show_decisions_decision_signatures.items())
|
|
281
|
+
dop = DecisionOrderPropagator(signatures=decision_signatures, prefix=output_prefix_passive)
|
|
282
|
+
control.register_propagator(dop) # type: ignore
|
|
283
|
+
|
|
284
|
+
ucc = UnsatConstraintComputer(control=control)
|
|
285
|
+
ucc.parse_files(files)
|
|
286
|
+
unsat_constraints = ucc.get_unsat_constraints(assumption_string=assumption_string)
|
|
287
|
+
self._print_unsat_constraints(unsat_constraints, ucc=ucc, prefix=output_prefix_active)
|
|
288
|
+
|
|
289
|
+
def _print_model(
|
|
290
|
+
self,
|
|
291
|
+
model: clingo.Model,
|
|
292
|
+
prefix_active: str = "",
|
|
293
|
+
prefix_passive: str = "",
|
|
294
|
+
) -> None:
|
|
295
|
+
print(prefix_passive)
|
|
296
|
+
print(
|
|
297
|
+
f"{prefix_active}"
|
|
298
|
+
f"{BACKGROUND_COLORS['LIGHT-GREY']}{COLORS['BLACK']} Model {COLORS['NORMAL']}{BACKGROUND_COLORS['GREY']} "
|
|
299
|
+
f"{self._show_decisions_model_id} {COLORS['NORMAL']} "
|
|
300
|
+
f"{model}"
|
|
301
|
+
)
|
|
302
|
+
# print(f"{COLORS['BLUE']}{model}{COLORS['NORMAL']}")
|
|
303
|
+
print(prefix_passive)
|
|
304
|
+
self._show_decisions_model_id += 1
|
|
305
|
+
|
|
306
|
+
def _method_show_decisions(
|
|
307
|
+
self,
|
|
308
|
+
control: clingo.Control,
|
|
309
|
+
files: List[str],
|
|
310
|
+
) -> None:
|
|
311
|
+
decision_signatures = set(self._show_decisions_decision_signatures.items())
|
|
312
|
+
dop = DecisionOrderPropagator(signatures=decision_signatures)
|
|
313
|
+
control.register_propagator(dop) # type: ignore
|
|
314
|
+
for f in files:
|
|
315
|
+
control.load(f)
|
|
316
|
+
if not files:
|
|
317
|
+
control.load("-")
|
|
318
|
+
control.ground()
|
|
319
|
+
control.solve(on_model=lambda model: self._print_model(model, "├", "│"))
|
|
320
|
+
|
|
321
|
+
def print_model(self, model: clingo.Model, _) -> None: # type: ignore
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
def main(self, control: clingo.Control, files: Sequence[str]) -> None:
|
|
325
|
+
print("clingexplaid", "version", version("clingexplaid"))
|
|
326
|
+
self._initialize()
|
|
327
|
+
|
|
328
|
+
# printing the input files
|
|
329
|
+
if not files:
|
|
330
|
+
print("Reading from -")
|
|
331
|
+
else:
|
|
332
|
+
print(f"Reading from {files[0]} {'...' if len(files) > 1 else ''}")
|
|
333
|
+
|
|
334
|
+
self.argument_constants = get_constants_from_arguments(sys.argv)
|
|
335
|
+
|
|
336
|
+
# standard case: only one method
|
|
337
|
+
if len(self.methods) == 1:
|
|
338
|
+
method = list(self.methods)[0]
|
|
339
|
+
method_function = self.method_functions[method]
|
|
340
|
+
method_function(control, files)
|
|
341
|
+
# special cases where specific pipelines have to be configured
|
|
342
|
+
elif self.methods == {"muc", "unsat-constraints"}:
|
|
343
|
+
self.method_functions["muc"](control, files, compute_unsat_constraints=True)
|
|
344
|
+
elif self.methods == {"muc", "unsat-constraints", "show-decisions"}:
|
|
345
|
+
self.method_functions["muc"](control, files, compute_unsat_constraints=True)
|
|
346
|
+
elif self.methods == {"unsat-constraints", "show-decisions"}:
|
|
347
|
+
self.method_functions["unsat-constraints"](control, files)
|
|
348
|
+
else:
|
|
349
|
+
print(
|
|
350
|
+
f"METHOD ERROR: the combination of the methods {[f'--{m}' for m in self.methods]} is invalid. "
|
|
351
|
+
f"Please remove the conflicting method flags"
|
|
352
|
+
)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MUC Module: Core Computer to get Minimal Unsatisfiable Cores
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from itertools import chain, combinations
|
|
6
|
+
from typing import Dict, Generator, List, Optional, Set, Tuple
|
|
7
|
+
|
|
8
|
+
import clingo
|
|
9
|
+
|
|
10
|
+
from ..utils import get_solver_literal_lookup
|
|
11
|
+
from ..utils.types import Assumption, AssumptionSet, SymbolSet
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CoreComputer:
|
|
15
|
+
"""
|
|
16
|
+
A container class that allows for a passed program_string and assumption_set to compute a minimal unsatisfiable
|
|
17
|
+
core.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, control: clingo.Control, assumption_set: AssumptionSet):
|
|
21
|
+
self.control = control
|
|
22
|
+
self.assumption_set = assumption_set
|
|
23
|
+
self.literal_lookup = get_solver_literal_lookup(control=self.control)
|
|
24
|
+
self.minimal: Optional[AssumptionSet] = None
|
|
25
|
+
|
|
26
|
+
def _solve(self, assumptions: Optional[AssumptionSet] = None) -> Tuple[bool, SymbolSet, SymbolSet]:
|
|
27
|
+
"""
|
|
28
|
+
Internal function that is used to make the single solver calls for finding the minimal unsatisfiable core.
|
|
29
|
+
"""
|
|
30
|
+
if assumptions is None:
|
|
31
|
+
assumptions = self.assumption_set
|
|
32
|
+
|
|
33
|
+
with self.control.solve(assumptions=list(assumptions), yield_=True) as solve_handle:
|
|
34
|
+
satisfiable = bool(solve_handle.get().satisfiable)
|
|
35
|
+
model = solve_handle.model().symbols(atoms=True) if solve_handle.model() is not None else []
|
|
36
|
+
core = {self.literal_lookup[literal_id] for literal_id in solve_handle.core()}
|
|
37
|
+
|
|
38
|
+
return satisfiable, set(model), core
|
|
39
|
+
|
|
40
|
+
def _compute_single_minimal(self, assumptions: Optional[AssumptionSet] = None) -> AssumptionSet:
|
|
41
|
+
"""
|
|
42
|
+
Function to compute a single minimal unsatisfiable core from the passed set of assumptions and the program of
|
|
43
|
+
the CoreComputer. If there is no minimal unsatisfiable core, since for example the program with assumptions
|
|
44
|
+
assumed is satisfiable, an empty set is returned. The algorithm that is used to compute this minimal
|
|
45
|
+
unsatisfiable core is the iterative deletion algorithm.
|
|
46
|
+
"""
|
|
47
|
+
if assumptions is None:
|
|
48
|
+
assumptions = self.assumption_set
|
|
49
|
+
|
|
50
|
+
# check that the assumption set isn't empty
|
|
51
|
+
if not assumptions:
|
|
52
|
+
raise ValueError("A minimal unsatisfiable core cannot be computed on an empty assumption set")
|
|
53
|
+
|
|
54
|
+
# check if the problem with the full assumption set is unsatisfiable in the first place, and if not skip the
|
|
55
|
+
# rest of the algorithm and return an empty set.
|
|
56
|
+
satisfiable, _, _ = self._solve(assumptions=assumptions)
|
|
57
|
+
if satisfiable:
|
|
58
|
+
return set()
|
|
59
|
+
|
|
60
|
+
muc_members: Set[Assumption] = set()
|
|
61
|
+
working_set = set(assumptions)
|
|
62
|
+
|
|
63
|
+
for assumption in assumptions:
|
|
64
|
+
# remove the current assumption from the working set
|
|
65
|
+
working_set.remove(assumption)
|
|
66
|
+
|
|
67
|
+
satisfiable, _, _ = self._solve(assumptions=working_set.union(muc_members))
|
|
68
|
+
# if the working set now becomes satisfiable without the assumption it is added to the muc_members
|
|
69
|
+
if satisfiable:
|
|
70
|
+
muc_members.add(assumption)
|
|
71
|
+
# every time we discover a new muc member we also check if all currently found muc members already
|
|
72
|
+
# suffice to make the instance unsatisfiable. If so we can stop the search sice we fund our muc.
|
|
73
|
+
if not self._solve(assumptions=muc_members)[0]:
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
return muc_members
|
|
77
|
+
|
|
78
|
+
def shrink(self, assumptions: Optional[AssumptionSet] = None) -> None:
|
|
79
|
+
"""
|
|
80
|
+
This function applies the unsatisfiable core minimization (`self._compute_single_minimal`) on the assumptions
|
|
81
|
+
set `assumptions` and stores the resulting MUC inside `self.minimal`.
|
|
82
|
+
"""
|
|
83
|
+
self.minimal = self._compute_single_minimal(assumptions=assumptions)
|
|
84
|
+
|
|
85
|
+
def get_multiple_minimal(self, max_mucs: Optional[int] = None) -> Generator[AssumptionSet, None, None]:
|
|
86
|
+
"""
|
|
87
|
+
This function generates all minimal unsatisfiable cores of the provided assumption set. It implements the
|
|
88
|
+
generator pattern since finding all mucs of an assumption set is exponential in nature and the search might not
|
|
89
|
+
fully complete in reasonable time. The parameter `max_mucs` can be used to specify the maximum number of
|
|
90
|
+
mucs that are found before stopping the search.
|
|
91
|
+
"""
|
|
92
|
+
assumptions = self.assumption_set
|
|
93
|
+
assumption_powerset = chain.from_iterable(
|
|
94
|
+
combinations(assumptions, r) for r in reversed(range(len(list(assumptions)) + 1))
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
found_sat: List[AssumptionSet] = []
|
|
98
|
+
found_mucs: List[AssumptionSet] = []
|
|
99
|
+
|
|
100
|
+
for current_subset in (set(s) for s in assumption_powerset):
|
|
101
|
+
# skip if empty subset
|
|
102
|
+
if len(current_subset) == 0:
|
|
103
|
+
continue
|
|
104
|
+
# skip if an already found satisfiable subset is superset
|
|
105
|
+
if any(set(sat).issuperset(current_subset) for sat in found_sat):
|
|
106
|
+
continue
|
|
107
|
+
# skip if an already found muc is a subset
|
|
108
|
+
if any(set(muc).issubset(current_subset) for muc in found_mucs):
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
muc = self._compute_single_minimal(assumptions=current_subset)
|
|
112
|
+
|
|
113
|
+
# if the current subset wasn't unsatisfiable store this info and continue
|
|
114
|
+
if len(list(muc)) == 0:
|
|
115
|
+
found_sat.append(current_subset)
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# if iterative deletion finds a muc that wasn't discovered before update sets and yield
|
|
119
|
+
if muc not in found_mucs:
|
|
120
|
+
found_mucs.append(muc)
|
|
121
|
+
yield muc
|
|
122
|
+
# if the maximum muc amount is found stop search
|
|
123
|
+
if max_mucs is not None and len(found_mucs) == max_mucs:
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
def muc_to_string(self, muc: AssumptionSet, literal_lookup: Optional[Dict[int, clingo.Symbol]] = None) -> Set[str]:
|
|
127
|
+
"""
|
|
128
|
+
Converts a MUC into a set containing the string representations of the contained assumptions
|
|
129
|
+
"""
|
|
130
|
+
# take class literal_lookup as default if no other is provided
|
|
131
|
+
if literal_lookup is None:
|
|
132
|
+
literal_lookup = self.literal_lookup
|
|
133
|
+
|
|
134
|
+
muc_string = set()
|
|
135
|
+
for a in muc:
|
|
136
|
+
if isinstance(a, int):
|
|
137
|
+
muc_string.add(str(literal_lookup[a]))
|
|
138
|
+
else:
|
|
139
|
+
muc_string.add(str(a[0]))
|
|
140
|
+
return muc_string
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Propagators for Explanation
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# pragma: no cover
|
|
6
|
+
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
from .propagator_decision_order import DecisionOrderPropagator
|
|
10
|
+
|
|
11
|
+
DecisionLevel = List[int]
|
|
12
|
+
DecisionLevelList = List[DecisionLevel]
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DecisionOrderPropagator",
|
|
16
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constant definitions for the propagators package
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from ..utils.logging import COLORS
|
|
6
|
+
|
|
7
|
+
UNKNOWN_SYMBOL_TOKEN = "INTERNAL"
|
|
8
|
+
|
|
9
|
+
INDENT_START = "├─"
|
|
10
|
+
INDENT_STEP = f"─{COLORS['GREY']}┼{COLORS['NORMAL']}──"
|
|
11
|
+
INDENT_END = f"─{COLORS['GREY']}┤{COLORS['NORMAL']} "
|