From d524e9637949d3944a0501c366dbf7b666a1f12a Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 21 Aug 2023 15:54:36 +0200 Subject: [PATCH 01/82] added experimental meta encoding for multi MUC finding --- .../asp_appraoch/meta_encoding.unsat.lp | 37 +++++++++++++ .../test.head_disjunction.converted.lp | 33 ++++++++++++ ...test.head_disjunction.converted.reified.lp | 53 +++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 experiments/asp_appraoch/meta_encoding.unsat.lp create mode 100644 experiments/asp_appraoch/test.head_disjunction.converted.lp create mode 100644 experiments/asp_appraoch/test.head_disjunction.converted.reified.lp diff --git a/experiments/asp_appraoch/meta_encoding.unsat.lp b/experiments/asp_appraoch/meta_encoding.unsat.lp new file mode 100644 index 0000000..599e9e0 --- /dev/null +++ b/experiments/asp_appraoch/meta_encoding.unsat.lp @@ -0,0 +1,37 @@ +% CLASSIC KRR META ENCODING + +conjunction(B) :- literal_tuple(B), + hold(L): literal_tuple(B,L), L>0; + not hold(L): literal_tuple(B,-L), L>0. + +body(normal(B)) :- rule(_,normal(B)), conjunction(B). +body(sum(B,G)) :- rule(_,sum(B,G)), + #sum { + W,L : hold(L), weighted_literal_tuple(B,L,W), L>0; + W,L : not hold(L), weighted_literal_tuple(B,-L,W), L>0 + } >= G . + +% hold(A): atom_tuple(H,A) :- rule(disjunction(H),B), body(B). +% Splitting this rule into two cases: +% 1. the body of a rule holds but the rule has not head atoms (integrity constraint) +% - an unsat atom containing the body of the rule is created +% 2. the body of a rule holds but the rule has at least one head atom +% - the classic way of handeling disjunctive heads is applied (at least one of the head atoms has to be true) +unsat(B) :- rule(disjunction(H),B), body(B), not atom_tuple(H,_). +hold(A): atom_tuple(H,A) :- rule(disjunction(H),B), body(B), atom_tuple(H,_). + +% get the ids of the literals representing the selected atoms from the output +selected_ID(ID) :- output(selected(_),N), literal_tuple(N,ID). + +% modified the hold choice rule to infer choice_hold(Atom_ID) +{ choice_hold(A): atom_tuple(H,A) } :- rule(choice(H),B), body(B). +% for every choice hold infer a real hold +hold(X) :- choice_hold(X). +% add heuristic false for every choice hold that was infered by a choice rule which contained selected(X) (done by comparing with selected_ID(A)) +#heuristic choice_hold(A): selected_ID(A). [1,false] +:- not unsat(_). + +#show. +% #show unsat/1. +% #show choice_hold/2. +#show T: output(T,B), conjunction(B). diff --git a/experiments/asp_appraoch/test.head_disjunction.converted.lp b/experiments/asp_appraoch/test.head_disjunction.converted.lp new file mode 100644 index 0000000..31c148c --- /dev/null +++ b/experiments/asp_appraoch/test.head_disjunction.converted.lp @@ -0,0 +1,33 @@ +% Transformed Program + +{selected(a)}. +{selected(b)}. +{selected(c)}. +{selected(d)}. + +a :- selected(a). +b :- selected(b). +c :- selected(c). +d :- selected(d). + +% {a;b;c}. + +% selected(a) :- a. +% selected(b) :- b. +% selected(c) :- c. + +% #heuristic selected(_). [1,false] +% #heuristic a. [1,false] +% #heuristic b. [1,false] +% #heuristic c. [1,false] + +% t. + +{x;y}. + +% d(1); d(2) :- t. +:- b, c. +:- a. +:- c, d. + +#show selected/1. diff --git a/experiments/asp_appraoch/test.head_disjunction.converted.reified.lp b/experiments/asp_appraoch/test.head_disjunction.converted.reified.lp new file mode 100644 index 0000000..2fdeeff --- /dev/null +++ b/experiments/asp_appraoch/test.head_disjunction.converted.reified.lp @@ -0,0 +1,53 @@ +atom_tuple(0). +atom_tuple(0,1). +literal_tuple(0). +rule(choice(0),normal(0)). +atom_tuple(1). +atom_tuple(1,2). +literal_tuple(1). +literal_tuple(1,1). +rule(disjunction(1),normal(1)). +atom_tuple(2). +atom_tuple(2,3). +rule(choice(2),normal(0)). +atom_tuple(3). +atom_tuple(3,4). +literal_tuple(2). +literal_tuple(2,3). +rule(disjunction(3),normal(2)). +atom_tuple(4). +literal_tuple(3). +literal_tuple(3,2). +literal_tuple(3,4). +rule(disjunction(4),normal(3)). +atom_tuple(5). +atom_tuple(5,5). +rule(choice(5),normal(0)). +atom_tuple(6). +atom_tuple(6,6). +literal_tuple(4). +literal_tuple(4,5). +rule(disjunction(6),normal(4)). +literal_tuple(5). +literal_tuple(5,6). +rule(disjunction(4),normal(5)). +atom_tuple(7). +atom_tuple(7,7). +rule(choice(7),normal(0)). +atom_tuple(8). +atom_tuple(8,8). +literal_tuple(6). +literal_tuple(6,7). +rule(disjunction(8),normal(6)). +literal_tuple(7). +literal_tuple(7,4). +literal_tuple(7,8). +rule(disjunction(4),normal(7)). +atom_tuple(9). +atom_tuple(9,9). +atom_tuple(9,10). +rule(choice(9),normal(0)). +output(selected(d),1). +output(selected(c),2). +output(selected(a),4). +output(selected(b),6). From f24abd6c8e08bed2820586ef62009bd74a510d8d Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 21 Aug 2023 15:56:40 +0200 Subject: [PATCH 02/82] fixed comments --- experiments/asp_appraoch/meta_encoding.unsat.lp | 3 ++- .../asp_appraoch/test.head_disjunction.converted.lp | 13 ------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/experiments/asp_appraoch/meta_encoding.unsat.lp b/experiments/asp_appraoch/meta_encoding.unsat.lp index 599e9e0..48be71b 100644 --- a/experiments/asp_appraoch/meta_encoding.unsat.lp +++ b/experiments/asp_appraoch/meta_encoding.unsat.lp @@ -1,4 +1,5 @@ -% CLASSIC KRR META ENCODING +% CLASSIC KRR META ENCODING : +% - Modified for finding all Minimal Unsatisfiable Cores of an unsatisfiable program conjunction(B) :- literal_tuple(B), hold(L): literal_tuple(B,L), L>0; diff --git a/experiments/asp_appraoch/test.head_disjunction.converted.lp b/experiments/asp_appraoch/test.head_disjunction.converted.lp index 31c148c..7a5d5ce 100644 --- a/experiments/asp_appraoch/test.head_disjunction.converted.lp +++ b/experiments/asp_appraoch/test.head_disjunction.converted.lp @@ -10,22 +10,9 @@ b :- selected(b). c :- selected(c). d :- selected(d). -% {a;b;c}. - -% selected(a) :- a. -% selected(b) :- b. -% selected(c) :- c. - -% #heuristic selected(_). [1,false] -% #heuristic a. [1,false] -% #heuristic b. [1,false] -% #heuristic c. [1,false] - -% t. {x;y}. -% d(1); d(2) :- t. :- b, c. :- a. :- c, d. From 4e2b10125bf79f7cb1ec021e048aa81a74de8bb3 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 21 Aug 2023 15:59:26 +0200 Subject: [PATCH 03/82] added example before conversion --- experiments/asp_appraoch/test.head_disjunction.lp | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 experiments/asp_appraoch/test.head_disjunction.lp diff --git a/experiments/asp_appraoch/test.head_disjunction.lp b/experiments/asp_appraoch/test.head_disjunction.lp new file mode 100644 index 0000000..631b6af --- /dev/null +++ b/experiments/asp_appraoch/test.head_disjunction.lp @@ -0,0 +1,10 @@ +% Transformed Program + +a. b. c. d. + + +{x;y}. + +:- b, c. +:- a. +:- c, d. From 52668c27e45caa56eab1ab727fdc47f9bcc18603 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 21 Aug 2023 16:10:07 +0200 Subject: [PATCH 04/82] renamed examples --- ...ad_disjunction.converted.lp => example.multi_muc.converted.lp} | 0 ...onverted.reified.lp => example.multi_muc.converted.reified.lp} | 0 .../{test.head_disjunction.lp => example.multi_muc.lp} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename experiments/asp_appraoch/{test.head_disjunction.converted.lp => example.multi_muc.converted.lp} (100%) rename experiments/asp_appraoch/{test.head_disjunction.converted.reified.lp => example.multi_muc.converted.reified.lp} (100%) rename experiments/asp_appraoch/{test.head_disjunction.lp => example.multi_muc.lp} (100%) diff --git a/experiments/asp_appraoch/test.head_disjunction.converted.lp b/experiments/asp_appraoch/example.multi_muc.converted.lp similarity index 100% rename from experiments/asp_appraoch/test.head_disjunction.converted.lp rename to experiments/asp_appraoch/example.multi_muc.converted.lp diff --git a/experiments/asp_appraoch/test.head_disjunction.converted.reified.lp b/experiments/asp_appraoch/example.multi_muc.converted.reified.lp similarity index 100% rename from experiments/asp_appraoch/test.head_disjunction.converted.reified.lp rename to experiments/asp_appraoch/example.multi_muc.converted.reified.lp diff --git a/experiments/asp_appraoch/test.head_disjunction.lp b/experiments/asp_appraoch/example.multi_muc.lp similarity index 100% rename from experiments/asp_appraoch/test.head_disjunction.lp rename to experiments/asp_appraoch/example.multi_muc.lp From 229814a22492a45886966701e41069896ef60a46 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 21 Aug 2023 16:10:30 +0200 Subject: [PATCH 05/82] added README for more context --- experiments/asp_appraoch/README.md | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 experiments/asp_appraoch/README.md diff --git a/experiments/asp_appraoch/README.md b/experiments/asp_appraoch/README.md new file mode 100644 index 0000000..3ccbb4a --- /dev/null +++ b/experiments/asp_appraoch/README.md @@ -0,0 +1,35 @@ +# How to apply + +1. Convert your given unsatisifable input program in the way shown in `example.multi_muc.lp` and `example.multi_muc.converted.lp` + + Here every fact that should be an assumption is tranformed in the following way: + +Original: +``` +a. +``` + +Transformed: +``` +{selected(a)}. +a :- selected(a). + +#show selected/1. +``` + +2. Reify this transformed input program + + This could be done like this: + +```bash +clingo test.head_disjunction.converted.lp --output=reify > test.head_disjunction.converted.reified.lp +``` + +3. Call the reified input program together with the meta encoding for finding Minimal Unsatisifiable Cores + + important here are the flags `--heuristic=Domain` and `--enum-mode=domRec` for clingo + +```bash +clingo 0 meta_encoding.unsat.lp test.head_disjunction.converted.reified.lp --heuristic=Domain --enum-mode=domRec +``` + +# TODOs: + ++ [ ] Implement/modify a transformer that does the fact transformation From 00c272ddcaf21496fcfdad12890f0d23767bbb1f Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 28 Aug 2023 18:30:42 +0200 Subject: [PATCH 06/82] added reformatted version of asp_approach --- .../example.multi_muc.converted.reified.v2.lp | 85 +++++++++++++++++++ .../example.multi_muc.converted.v2.lp | 20 +++++ .../asp_appraoch/meta_encoding.unsat.v2.lp | 36 ++++++++ 3 files changed, 141 insertions(+) create mode 100644 experiments/asp_appraoch/example.multi_muc.converted.reified.v2.lp create mode 100644 experiments/asp_appraoch/example.multi_muc.converted.v2.lp create mode 100644 experiments/asp_appraoch/meta_encoding.unsat.v2.lp diff --git a/experiments/asp_appraoch/example.multi_muc.converted.reified.v2.lp b/experiments/asp_appraoch/example.multi_muc.converted.reified.v2.lp new file mode 100644 index 0000000..55b0336 --- /dev/null +++ b/experiments/asp_appraoch/example.multi_muc.converted.reified.v2.lp @@ -0,0 +1,85 @@ +atom_tuple(0). +atom_tuple(0,1). +literal_tuple(0). +rule(disjunction(0),normal(0)). +atom_tuple(1). +atom_tuple(1,2). +rule(disjunction(1),normal(0)). +atom_tuple(2). +atom_tuple(2,3). +rule(disjunction(2),normal(0)). +atom_tuple(3). +atom_tuple(3,4). +rule(disjunction(3),normal(0)). +atom_tuple(4). +atom_tuple(4,5). +rule(choice(4),normal(0)). +atom_tuple(5). +atom_tuple(5,6). +rule(choice(5),normal(0)). +atom_tuple(6). +literal_tuple(1). +literal_tuple(1,5). +literal_tuple(1,6). +rule(disjunction(6),normal(1)). +atom_tuple(7). +atom_tuple(7,7). +rule(choice(7),normal(0)). +literal_tuple(2). +literal_tuple(2,7). +rule(disjunction(6),normal(2)). +atom_tuple(8). +atom_tuple(8,8). +rule(choice(8),normal(0)). +literal_tuple(3). +literal_tuple(3,6). +literal_tuple(3,8). +rule(disjunction(6),normal(3)). +atom_tuple(9). +atom_tuple(9,9). +atom_tuple(9,10). +rule(choice(9),normal(0)). +atom_tuple(10). +atom_tuple(10,11). +literal_tuple(4). +literal_tuple(4,5). +rule(disjunction(10),normal(4)). +atom_tuple(11). +atom_tuple(11,12). +literal_tuple(5). +literal_tuple(5,6). +rule(disjunction(11),normal(5)). +atom_tuple(12). +atom_tuple(12,13). +literal_tuple(6). +literal_tuple(6,8). +rule(disjunction(12),normal(6)). +atom_tuple(13). +atom_tuple(13,14). +rule(disjunction(13),normal(2)). +output(a,2). +output(b,6). +output(c,5). +output(d,4). +literal_tuple(7). +literal_tuple(7,11). +output(_muc(d),7). +literal_tuple(8). +literal_tuple(8,12). +output(_muc(c),8). +literal_tuple(9). +literal_tuple(9,13). +output(_muc(b),9). +literal_tuple(10). +literal_tuple(10,14). +output(_muc(a),10). +literal_tuple(11). +literal_tuple(11,9). +output(x,11). +literal_tuple(12). +literal_tuple(12,10). +output(y,12). +output(_assumption(a),0). +output(_assumption(b),0). +output(_assumption(c),0). +output(_assumption(d),0). diff --git a/experiments/asp_appraoch/example.multi_muc.converted.v2.lp b/experiments/asp_appraoch/example.multi_muc.converted.v2.lp new file mode 100644 index 0000000..f06f2d5 --- /dev/null +++ b/experiments/asp_appraoch/example.multi_muc.converted.v2.lp @@ -0,0 +1,20 @@ +% Transformed Program + +_assumption(a). +_assumption(b). +_assumption(c). +_assumption(d). + +{a}. {b}. {c}. {d}. + +_muc(a) :- a. +_muc(b) :- b. +_muc(c) :- c. +_muc(d) :- d. + +{x;y}. + +:- b, c. +:- a. +:- c, d. + diff --git a/experiments/asp_appraoch/meta_encoding.unsat.v2.lp b/experiments/asp_appraoch/meta_encoding.unsat.v2.lp new file mode 100644 index 0000000..194c6a7 --- /dev/null +++ b/experiments/asp_appraoch/meta_encoding.unsat.v2.lp @@ -0,0 +1,36 @@ +% CLASSIC KRR META ENCODING : +% - Modified for finding all Minimal Unsatisfiable Cores of an unsatisfiable program + +conjunction(B) :- literal_tuple(B), + hold(L): literal_tuple(B,L), L>0; + not hold(L): literal_tuple(B,-L), L>0. + +body(normal(B)) :- rule(_,normal(B)), conjunction(B). +body(sum(B,G)) :- rule(_,sum(B,G)), + #sum { + W,L : hold(L), weighted_literal_tuple(B,L,W), L>0; + W,L : not hold(L), weighted_literal_tuple(B,-L,W), L>0 + } >= G . + +% hold(A): atom_tuple(H,A) :- rule(disjunction(H),B), body(B). +% Splitting this rule into two cases: +% 1. the body of a rule holds but the rule has not head atoms (integrity constraint) +% - an unsat atom containing the body of the rule is created +% 2. the body of a rule holds but the rule has at least one head atom +% - the classic way of handeling disjunctive heads is applied (at least one of the head atoms has to be true) +unsat(B) :- rule(disjunction(H),B), body(B), not atom_tuple(H,_). +hold(A): atom_tuple(H,A) :- rule(disjunction(H),B), body(B), atom_tuple(H,_). + + +% modified the hold choice rule to infer choice_hold(Atom_ID) +{ choice_hold(A): atom_tuple(H,A) } :- rule(choice(H),B), body(B). +% for every choice hold infer a real hold +hold(X) :- choice_hold(X). +% add heuristic false for every choice hold (This works in experiments but might have an undiscovered edge case) +#heuristic choice_hold(_).[1,false] +:- not unsat(_). + +#show. +% #show unsat/1. +% #show choice_hold/2. +#show muc(T): output(_muc(T),B), conjunction(B). From 4069540f085e0bb1b5b7c14c3596e786eff9f632 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Thu, 31 Aug 2023 19:42:27 +0200 Subject: [PATCH 07/82] extended cli with first implementation of asp approach --- requirements.txt | 1 + src/clingexplaid/utils/cli.py | 110 +++++++++++++++++- .../utils/logic_programs/asp_approach.lp | 36 ++++++ 3 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 src/clingexplaid/utils/logic_programs/asp_approach.lp diff --git a/requirements.txt b/requirements.txt index 54f9cc3..7d2a6f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ cffi==1.15.1 clingo==5.6.2 +clingox==1.2.0 pycparser==2.21 diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 8a17f7f..55a4180 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -3,9 +3,13 @@ """ import configparser +import functools from pathlib import Path +from typing import Dict, Callable +import clingo from clingo.application import Application +from clingox.reify import reify_program from clingexplaid.utils import get_solver_literal_lookup from clingexplaid.utils.logger import BACKGROUND_COLORS, COLORS @@ -24,6 +28,7 @@ class CoreComputerApp(Application): def __init__(self, name): # pylint: disable = unused-argument self.signatures = {} + self.method = 1 def _parse_assumption_signature(self, input_string: str) -> bool: # signature_strings = input_string.strip().split(",") @@ -34,6 +39,17 @@ def _parse_assumption_signature(self, input_string: str) -> bool: self.signatures[signature_list[0]] = int(signature_list[1]) return True + def _parse_mode(self, input_string: str) -> bool: + try: + method_id = int(input_string.strip()) + except ValueError: + print("Not valid format for mode, expected 1 or 2") + return False + if method_id in (1, 2): + self.method = method_id + return True + return False + def print_model(self, model, _): return @@ -52,13 +68,14 @@ def register_options(self, options): self._parse_assumption_signature, multi=True, ) + options.add( + group, + "muc-method,m", + "This sets the method of finding the MUCs. (1) Iterative Deletion [default] (2) Meta Encoding", + self._parse_mode, + ) - def main(self, control, files): - setup_file_path = Path(__file__).parent.joinpath("../../../setup.cfg") - setup_config = configparser.ConfigParser() - setup_config.read(setup_file_path) - metadata = setup_config["metadata"] - print(metadata["name"], "version", metadata["version"]) + def _find_single_muc(self, control: clingo.Control, files): signature_set = set(self.signatures.items()) if self.signatures else None at = AssumptionTransformer(signatures=signature_set) if not files: @@ -91,3 +108,84 @@ def main(self, control, files): f"{BACKGROUND_COLORS['BLUE']} MUC: {muc_id} {COLORS['NORMAL']}{COLORS['DARK_BLUE']}{COLORS['NORMAL']}" ) print(f"{COLORS['BLUE']}{result}{COLORS['NORMAL']}") + + def _find_multi_mucs(self, control: clingo.Control, files): + print("ASP APPROACH") + + signature_set = set(self.signatures.items()) if self.signatures else None + at = AssumptionTransformer(signatures=signature_set) + if not files: + program_transformed = at.parse_files("-") + print("Reading from -") + else: + program_transformed = at.parse_files(files) + print(f"Reading from {files[0]} {'...' if len(files) > 1 else ''}") + + # First Grounding for getting the assumptions + control.add("base", [], program_transformed) + control.ground([("base", [])]) + + literal_lookup = get_solver_literal_lookup(control) + + additional_rules = [] + + assumption_signatures = set() + for assumption_literal in at.get_assumptions(control): + assumption = literal_lookup[assumption_literal] + assumption_signatures.add((assumption.name, len(assumption.arguments))) + additional_rules.append(f"_assumption({str(assumption)}).") + additional_rules.append(f"_muc({assumption}) :- {assumption}.") + + for signature, arity in assumption_signatures: + additional_rules.append(f"#show {signature}/{arity}.") + + final_program = "\n".join(( + program_transformed, + "#show _muc/1.", + "\n".join(additional_rules), + )) + + # print(final_program) + + # Implicit Grounding for reification + + symbols = reify_program(final_program) + reified_program = "\n".join([f"{str(s)}." for s in symbols]) + + # print(reified_program) + + with open(Path(__file__).resolve().parent.joinpath("logic_programs/asp_approach.lp"), "r") as f: + meta_encoding = f.read() + + # Second Grounding to get MUCs + + muc_control = clingo.Control(["--heuristic=Domain", "--enum-mode=domRec"]) + muc_control.add("base", [], reified_program) + muc_control.add("base", [], meta_encoding) + + # print(meta_encoding) + + muc_control.ground([("base", [])]) + + with muc_control.solve(yield_=True) as solve_handle: + satisfiable = bool(solve_handle.get().satisfiable) + model = ( + solve_handle.model().symbols(shown=True, atoms=True) + if solve_handle.model() is not None + else [] + ) + + print(satisfiable) + print(".\n".join([str(a) for a in model])) + + def main(self, control, files): + setup_file_path = Path(__file__).parent.joinpath("../../../setup.cfg") + setup_config = configparser.ConfigParser() + setup_config.read(setup_file_path) + metadata = setup_config["metadata"] + print(metadata["name"], "version", metadata["version"]) + + if self.method == 1: + self._find_single_muc(control, files) + elif self.method == 2: + self._find_multi_mucs(control, files) diff --git a/src/clingexplaid/utils/logic_programs/asp_approach.lp b/src/clingexplaid/utils/logic_programs/asp_approach.lp new file mode 100644 index 0000000..194c6a7 --- /dev/null +++ b/src/clingexplaid/utils/logic_programs/asp_approach.lp @@ -0,0 +1,36 @@ +% CLASSIC KRR META ENCODING : +% - Modified for finding all Minimal Unsatisfiable Cores of an unsatisfiable program + +conjunction(B) :- literal_tuple(B), + hold(L): literal_tuple(B,L), L>0; + not hold(L): literal_tuple(B,-L), L>0. + +body(normal(B)) :- rule(_,normal(B)), conjunction(B). +body(sum(B,G)) :- rule(_,sum(B,G)), + #sum { + W,L : hold(L), weighted_literal_tuple(B,L,W), L>0; + W,L : not hold(L), weighted_literal_tuple(B,-L,W), L>0 + } >= G . + +% hold(A): atom_tuple(H,A) :- rule(disjunction(H),B), body(B). +% Splitting this rule into two cases: +% 1. the body of a rule holds but the rule has not head atoms (integrity constraint) +% - an unsat atom containing the body of the rule is created +% 2. the body of a rule holds but the rule has at least one head atom +% - the classic way of handeling disjunctive heads is applied (at least one of the head atoms has to be true) +unsat(B) :- rule(disjunction(H),B), body(B), not atom_tuple(H,_). +hold(A): atom_tuple(H,A) :- rule(disjunction(H),B), body(B), atom_tuple(H,_). + + +% modified the hold choice rule to infer choice_hold(Atom_ID) +{ choice_hold(A): atom_tuple(H,A) } :- rule(choice(H),B), body(B). +% for every choice hold infer a real hold +hold(X) :- choice_hold(X). +% add heuristic false for every choice hold (This works in experiments but might have an undiscovered edge case) +#heuristic choice_hold(_).[1,false] +:- not unsat(_). + +#show. +% #show unsat/1. +% #show choice_hold/2. +#show muc(T): output(_muc(T),B), conjunction(B). From 182c0d5c6b1dd4d2c6c515e04ac6d64fd58db7ea Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 11 Sep 2023 15:16:15 +0200 Subject: [PATCH 08/82] added possibility to provide constants for simplified assumption controll --- src/clingexplaid/utils/transformer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/clingexplaid/utils/transformer.py b/src/clingexplaid/utils/transformer.py index b12dc64..598f004 100644 --- a/src/clingexplaid/utils/transformer.py +++ b/src/clingexplaid/utils/transformer.py @@ -3,7 +3,7 @@ """ from pathlib import Path -from typing import List, Optional, Sequence, Set, Tuple, Union +from typing import List, Optional, Sequence, Set, Tuple, Union, Dict import clingo from clingo import ast as _ast @@ -154,7 +154,7 @@ def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: self.transformed = True return "\n".join(out) - def get_assumptions(self, control: clingo.Control) -> Set[int]: + def get_assumptions(self, control: clingo.Control, constants: Optional[Dict] = None) -> Set[int]: """ Returns the assumptions which were gathered during the transformation of the program. Has to be called after a program has already been transformed. @@ -167,7 +167,8 @@ def get_assumptions(self, control: clingo.Control) -> Set[int]: "The get_assumptions method cannot be called before a program has been " "transformed" ) - fact_control = clingo.Control() + constant_strings = [f"-c {k}={v}" for k, v in constants.items()] if constants is not None else [] + fact_control = clingo.Control(constant_strings) fact_control.add("base", [], "\n".join(self.fact_rules)) fact_control.ground([("base", [])]) fact_symbols = [ From 593bebcb6c22b4b767ddd8a4e20953c4767482a0 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 11 Sep 2023 16:06:31 +0200 Subject: [PATCH 09/82] added working asp-approach to cli --- experiments/asp_appraoch/example.multi_muc.lp | 3 + src/clingexplaid/utils/cli.py | 70 +++++++++++-------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/experiments/asp_appraoch/example.multi_muc.lp b/experiments/asp_appraoch/example.multi_muc.lp index 631b6af..4eb07f5 100644 --- a/experiments/asp_appraoch/example.multi_muc.lp +++ b/experiments/asp_appraoch/example.multi_muc.lp @@ -2,9 +2,12 @@ a. b. c. d. +test(1..k). {x;y}. :- b, c. :- a. :- c, d. + +:- test(1). \ No newline at end of file diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 55a4180..8d0c135 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -4,8 +4,10 @@ import configparser import functools +import sys from pathlib import Path from typing import Dict, Callable +from importlib.metadata import version import clingo from clingo.application import Application @@ -29,6 +31,7 @@ def __init__(self, name): # pylint: disable = unused-argument self.signatures = {} self.method = 1 + self.muc_id = 1 def _parse_assumption_signature(self, input_string: str) -> bool: # signature_strings = input_string.strip().split(",") @@ -121,16 +124,31 @@ def _find_multi_mucs(self, control: clingo.Control, files): program_transformed = at.parse_files(files) print(f"Reading from {files[0]} {'...' if len(files) > 1 else ''}") + # TODO: Ok this is not a nice way to do it but I have no clue how to do it otherwise :) + arguments = sys.argv[1:] + constant_names = [] + next_is_const = False + for arg in arguments: + if next_is_const: + constant_name = arg.strip().split("=")[0] + constant_names.append(constant_name) + next_is_const = False + if arg == "-c": + next_is_const = True + + constants = {name: control.get_const(name) for name in constant_names} + # First Grounding for getting the assumptions - control.add("base", [], program_transformed) - control.ground([("base", [])]) + assumption_control = clingo.Control([f"-c {k}={str(v)}" for k, v in constants.items()]) + assumption_control.add("base", [], program_transformed) + assumption_control.ground([("base", [])]) - literal_lookup = get_solver_literal_lookup(control) + literal_lookup = get_solver_literal_lookup(assumption_control) additional_rules = [] assumption_signatures = set() - for assumption_literal in at.get_assumptions(control): + for assumption_literal in at.get_assumptions(assumption_control, constants=constants): assumption = literal_lookup[assumption_literal] assumption_signatures.add((assumption.name, len(assumption.arguments))) additional_rules.append(f"_assumption({str(assumption)}).") @@ -140,50 +158,46 @@ def _find_multi_mucs(self, control: clingo.Control, files): additional_rules.append(f"#show {signature}/{arity}.") final_program = "\n".join(( + # add constants like this because clingox reify doesn't support a custom control or other way to + # provide constants. + "\n".join([f"#const {k}={str(v)}."for k, v in constants.items()]), program_transformed, "#show _muc/1.", "\n".join(additional_rules), )) - # print(final_program) - # Implicit Grounding for reification symbols = reify_program(final_program) reified_program = "\n".join([f"{str(s)}." for s in symbols]) - # print(reified_program) - with open(Path(__file__).resolve().parent.joinpath("logic_programs/asp_approach.lp"), "r") as f: meta_encoding = f.read() - # Second Grounding to get MUCs + # Second Grounding to get MUCs with original control - muc_control = clingo.Control(["--heuristic=Domain", "--enum-mode=domRec"]) - muc_control.add("base", [], reified_program) - muc_control.add("base", [], meta_encoding) + control.add("base", [], reified_program) + control.add("base", [], meta_encoding) - # print(meta_encoding) + control.configuration.solve.enum_mode = "domRec" + control.configuration.solver.heuristic = "Domain" - muc_control.ground([("base", [])]) + control.ground([("base", [])]) - with muc_control.solve(yield_=True) as solve_handle: - satisfiable = bool(solve_handle.get().satisfiable) - model = ( - solve_handle.model().symbols(shown=True, atoms=True) - if solve_handle.model() is not None - else [] - ) + control.solve(on_model=self.print_found_muc) - print(satisfiable) - print(".\n".join([str(a) for a in model])) + def print_found_muc(self, model): + result = ".\n".join([str(a) for a in model.symbols(shown=True)]) + if result: + result += "." + print( + f"{BACKGROUND_COLORS['BLUE']} MUC: {self.muc_id} {COLORS['NORMAL']}{COLORS['DARK_BLUE']}{COLORS['NORMAL']}" + ) + print(f"{COLORS['BLUE']}{result}{COLORS['NORMAL']}") + self.muc_id += 1 def main(self, control, files): - setup_file_path = Path(__file__).parent.joinpath("../../../setup.cfg") - setup_config = configparser.ConfigParser() - setup_config.read(setup_file_path) - metadata = setup_config["metadata"] - print(metadata["name"], "version", metadata["version"]) + print("clingexplaid", "version", version("clingexplaid")) if self.method == 1: self._find_single_muc(control, files) From 41bdbc018ec14a537bb3abf99b7b34c742b40433 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 11 Sep 2023 16:08:10 +0200 Subject: [PATCH 10/82] added first draft of RuleSplitter transformer --- src/clingexplaid/utils/transformer.py | 79 +++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/clingexplaid/utils/transformer.py b/src/clingexplaid/utils/transformer.py index 598f004..c448a1b 100644 --- a/src/clingexplaid/utils/transformer.py +++ b/src/clingexplaid/utils/transformer.py @@ -233,8 +233,87 @@ def parse_file(self, path: Union[str, Path], encoding: str = "utf-8") -> str: return self.parse_string(f.read()) +class RuleSplitter(_ast.Transformer): + + def __init__(self): + pass + + def visit_Rule(self, node): + head = node.head + body = node.body + + print(f"HEAD={head} BODY={body}") + + if body: + # remove MUS literals from rule + cleaned_body_literals = [x for x in node.body if x.atom.symbol.name not in ("__mus__",)] + cleaned_body = "; ".join([str(l) for l in cleaned_body_literals]) + + # get all variables used in body (to later reference in head) + variables = set() + for lit in cleaned_body_literals: + arguments = lit.atom.symbol.arguments + if arguments: + for arg in arguments: + variables.add(arg) + + # convert the cleaned body to a base64 string + rule_body_string = cleaned_body + rule_body_string_bytes = rule_body_string.encode("ascii") + rule_body_base64_bytes = base64.b64encode(rule_body_string_bytes) + rule_body_base64 = rule_body_base64_bytes.decode("ascii") + + print(cleaned_body, str(node), variables) + + # create a new '_body' head for the original rule + new_head_arguments = [ + _ast.SymbolicTerm(node.location, clingo.parse_term(f'"{rule_body_base64}"')), + _ast.Function( + location=node.location, + name="", + arguments=variables, # TODO: How to create a new ASTSequence + external=0 + ) + ] + new_head = _ast.Function( + location=node.location, + name="_body", + arguments=new_head_arguments, + external=0 + ) + node.head = new_head + + # create new second rule that links the head with the '_body' matching predicate + new_head_rule = _ast.Rule( + location=node.location, + head=head, + body=[new_head], + ) + + # TODO: How to return multiple rules? ASTSequence? + return _ast.TheorySequence( + location=node.location, + sequence_type=1, + terms=[node, new_head_rule] + ) + + # default case + return node + + def parse_string(self, string: str) -> str: + """ + Function that applies the transformation to the `program_string` it's called with and returns the transformed + program string. + """ + out = [] + _ast.parse_string(string, lambda stm: out.append((str(self(stm))))) + + return "\n".join(out) + + __all__ = [ RuleIDTransformer.__name__, AssumptionTransformer.__name__, ConstraintTransformer.__name__, + RuleSplitter.__name__, ] From 0f2f3078e8675b6538f44062fb8c328fc3a5b59d Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Wed, 13 Sep 2023 18:41:26 +0200 Subject: [PATCH 11/82] fixed creation of new rules in RuleSplitter --- src/clingexplaid/utils/transformer.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/clingexplaid/utils/transformer.py b/src/clingexplaid/utils/transformer.py index c448a1b..50aa6f9 100644 --- a/src/clingexplaid/utils/transformer.py +++ b/src/clingexplaid/utils/transformer.py @@ -1,7 +1,7 @@ """ Transformers for Explanation """ - +import base64 from pathlib import Path from typing import List, Optional, Sequence, Set, Tuple, Union, Dict @@ -236,13 +236,13 @@ def parse_file(self, path: Union[str, Path], encoding: str = "utf-8") -> str: class RuleSplitter(_ast.Transformer): def __init__(self): - pass + self.head_rules = [] def visit_Rule(self, node): head = node.head body = node.body - print(f"HEAD={head} BODY={body}") + # print(f"HEAD={head} BODY={body}") if body: # remove MUS literals from rule @@ -263,7 +263,7 @@ def visit_Rule(self, node): rule_body_base64_bytes = base64.b64encode(rule_body_string_bytes) rule_body_base64 = rule_body_base64_bytes.decode("ascii") - print(cleaned_body, str(node), variables) + # print(cleaned_body, str(node), variables) # create a new '_body' head for the original rule new_head_arguments = [ @@ -271,7 +271,7 @@ def visit_Rule(self, node): _ast.Function( location=node.location, name="", - arguments=variables, # TODO: How to create a new ASTSequence + arguments=variables, external=0 ) ] @@ -289,13 +289,9 @@ def visit_Rule(self, node): head=head, body=[new_head], ) + self.head_rules.append(new_head_rule) - # TODO: How to return multiple rules? ASTSequence? - return _ast.TheorySequence( - location=node.location, - sequence_type=1, - terms=[node, new_head_rule] - ) + return node # default case return node @@ -305,8 +301,10 @@ def parse_string(self, string: str) -> str: Function that applies the transformation to the `program_string` it's called with and returns the transformed program string. """ + self.head_rules = [] out = [] _ast.parse_string(string, lambda stm: out.append((str(self(stm))))) + out += [str(r) for r in self.head_rules] return "\n".join(out) From bdf89c6606e658bb7e83a273e560efe8c1326b72 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 25 Sep 2023 09:53:41 +0200 Subject: [PATCH 12/82] switched ASP meta encoding muc finding strategy to default --- src/clingexplaid/utils/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 8d0c135..f291708 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -74,7 +74,7 @@ def register_options(self, options): options.add( group, "muc-method,m", - "This sets the method of finding the MUCs. (1) Iterative Deletion [default] (2) Meta Encoding", + "This sets the method of finding the MUCs. (1) ASP Meta Encoding [default] (2) Iterative Deletion", self._parse_mode, ) @@ -200,6 +200,6 @@ def main(self, control, files): print("clingexplaid", "version", version("clingexplaid")) if self.method == 1: - self._find_single_muc(control, files) - elif self.method == 2: self._find_multi_mucs(control, files) + elif self.method == 2: + self._find_single_muc(control, files) From d3c704944dbc1cbce76c2bca9d9fa80ab3041ab6 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 25 Sep 2023 16:25:52 +0200 Subject: [PATCH 13/82] reformatting and code cleanup --- setup.cfg | 1 + src/clingexplaid/utils/cli.py | 81 ++++++++++--------- src/clingexplaid/utils/transformer.py | 52 ++++++++---- tests/clingexplaid/res/test_program_rules.lp | 5 ++ .../res/transformed_program_rules_split.lp | 10 +++ tests/clingexplaid/test_main.py | 18 +++++ 6 files changed, 117 insertions(+), 50 deletions(-) create mode 100644 tests/clingexplaid/res/test_program_rules.lp create mode 100644 tests/clingexplaid/res/transformed_program_rules_split.lp diff --git a/setup.cfg b/setup.cfg index d703afc..2f8d12e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ include_package_data = True install_requires = importlib_metadata;python_version<'3.8' clingo>=5.6.0 + clingox>=1.2.0 autoflake [options.packages.find] diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index f291708..a279557 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -2,12 +2,10 @@ Command Line Interface Utilities """ -import configparser -import functools import sys -from pathlib import Path -from typing import Dict, Callable from importlib.metadata import version +from pathlib import Path +from typing import Dict, List, Tuple import clingo from clingo.application import Application @@ -78,8 +76,10 @@ def register_options(self, options): self._parse_mode, ) - def _find_single_muc(self, control: clingo.Control, files): - signature_set = set(self.signatures.items()) if self.signatures else None + def _apply_assumption_transformer( + self, signatures: Dict[str, int], files: List[str] + ) -> Tuple[str, AssumptionTransformer]: + signature_set = set(self.signatures.items()) if signatures else None at = AssumptionTransformer(signatures=signature_set) if not files: program_transformed = at.parse_files("-") @@ -87,6 +87,22 @@ def _find_single_muc(self, control: clingo.Control, files): else: program_transformed = at.parse_files(files) print(f"Reading from {files[0]} {'...' if len(files) > 1 else ''}") + return program_transformed, at + + def _print_found_muc(self, model): + result = ".\n".join([str(a) for a in model.symbols(shown=True)]) + if result: + result += "." + print( + f"{BACKGROUND_COLORS['BLUE']} MUC: {self.muc_id} {COLORS['NORMAL']}{COLORS['DARK_BLUE']}{COLORS['NORMAL']}" + ) + print(f"{COLORS['BLUE']}{result}{COLORS['NORMAL']}") + self.muc_id += 1 + + def _find_single_muc(self, control: clingo.Control, files): + program_transformed, at = self._apply_assumption_transformer( + signatures=self.signatures, files=files + ) control.add("base", [], program_transformed) control.ground([("base", [])]) @@ -115,16 +131,10 @@ def _find_single_muc(self, control: clingo.Control, files): def _find_multi_mucs(self, control: clingo.Control, files): print("ASP APPROACH") - signature_set = set(self.signatures.items()) if self.signatures else None - at = AssumptionTransformer(signatures=signature_set) - if not files: - program_transformed = at.parse_files("-") - print("Reading from -") - else: - program_transformed = at.parse_files(files) - print(f"Reading from {files[0]} {'...' if len(files) > 1 else ''}") + program_transformed, at = self._apply_assumption_transformer( + signatures=self.signatures, files=files + ) - # TODO: Ok this is not a nice way to do it but I have no clue how to do it otherwise :) arguments = sys.argv[1:] constant_names = [] next_is_const = False @@ -139,7 +149,9 @@ def _find_multi_mucs(self, control: clingo.Control, files): constants = {name: control.get_const(name) for name in constant_names} # First Grounding for getting the assumptions - assumption_control = clingo.Control([f"-c {k}={str(v)}" for k, v in constants.items()]) + assumption_control = clingo.Control( + [f"-c {k}={str(v)}" for k, v in constants.items()] + ) assumption_control.add("base", [], program_transformed) assumption_control.ground([("base", [])]) @@ -148,7 +160,9 @@ def _find_multi_mucs(self, control: clingo.Control, files): additional_rules = [] assumption_signatures = set() - for assumption_literal in at.get_assumptions(assumption_control, constants=constants): + for assumption_literal in at.get_assumptions( + assumption_control, constants=constants + ): assumption = literal_lookup[assumption_literal] assumption_signatures.add((assumption.name, len(assumption.arguments))) additional_rules.append(f"_assumption({str(assumption)}).") @@ -157,44 +171,39 @@ def _find_multi_mucs(self, control: clingo.Control, files): for signature, arity in assumption_signatures: additional_rules.append(f"#show {signature}/{arity}.") - final_program = "\n".join(( + final_program = "\n".join( + ( # add constants like this because clingox reify doesn't support a custom control or other way to # provide constants. - "\n".join([f"#const {k}={str(v)}."for k, v in constants.items()]), + "\n".join([f"#const {k}={str(v)}." for k, v in constants.items()]), program_transformed, "#show _muc/1.", + "#show _assumption/1.", "\n".join(additional_rules), - )) + ) + ) # Implicit Grounding for reification - symbols = reify_program(final_program) reified_program = "\n".join([f"{str(s)}." for s in symbols]) - with open(Path(__file__).resolve().parent.joinpath("logic_programs/asp_approach.lp"), "r") as f: + with open( + Path(__file__).resolve().parent.joinpath("logic_programs/asp_approach.lp"), + "r", + encoding="utf-8", + ) as f: meta_encoding = f.read() # Second Grounding to get MUCs with original control - control.add("base", [], reified_program) control.add("base", [], meta_encoding) - control.configuration.solve.enum_mode = "domRec" - control.configuration.solver.heuristic = "Domain" + control.configuration.solve.enum_mode = "domRec" # type: ignore + control.configuration.solver.heuristic = "Domain" # type: ignore control.ground([("base", [])]) - control.solve(on_model=self.print_found_muc) - - def print_found_muc(self, model): - result = ".\n".join([str(a) for a in model.symbols(shown=True)]) - if result: - result += "." - print( - f"{BACKGROUND_COLORS['BLUE']} MUC: {self.muc_id} {COLORS['NORMAL']}{COLORS['DARK_BLUE']}{COLORS['NORMAL']}" - ) - print(f"{COLORS['BLUE']}{result}{COLORS['NORMAL']}") - self.muc_id += 1 + control.solve(on_model=self._print_found_muc) def main(self, control, files): print("clingexplaid", "version", version("clingexplaid")) diff --git a/src/clingexplaid/utils/transformer.py b/src/clingexplaid/utils/transformer.py index 50aa6f9..e6a7d97 100644 --- a/src/clingexplaid/utils/transformer.py +++ b/src/clingexplaid/utils/transformer.py @@ -3,7 +3,7 @@ """ import base64 from pathlib import Path -from typing import List, Optional, Sequence, Set, Tuple, Union, Dict +from typing import Dict, List, Optional, Sequence, Set, Tuple, Union import clingo from clingo import ast as _ast @@ -154,7 +154,9 @@ def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: self.transformed = True return "\n".join(out) - def get_assumptions(self, control: clingo.Control, constants: Optional[Dict] = None) -> Set[int]: + def get_assumptions( + self, control: clingo.Control, constants: Optional[Dict] = None + ) -> Set[int]: """ Returns the assumptions which were gathered during the transformation of the program. Has to be called after a program has already been transformed. @@ -167,7 +169,11 @@ def get_assumptions(self, control: clingo.Control, constants: Optional[Dict] = N "The get_assumptions method cannot be called before a program has been " "transformed" ) - constant_strings = [f"-c {k}={v}" for k, v in constants.items()] if constants is not None else [] + constant_strings = ( + [f"-c {k}={v}" for k, v in constants.items()] + if constants is not None + else [] + ) fact_control = clingo.Control(constant_strings) fact_control.add("base", [], "\n".join(self.fact_rules)) fact_control.ground([("base", [])]) @@ -234,19 +240,30 @@ def parse_file(self, path: Union[str, Path], encoding: str = "utf-8") -> str: class RuleSplitter(_ast.Transformer): + """ + A transformer that is used to split rules into two. This is done using an intermediate predicate called `_body`, + which contains a base64 representation of the original rule and all body variable assignments for explanation + purposes. This intermediate predicate replaces the head of the original rule and a new rule with the old head and + the newly generated `_body` predicate as the body is also inserted. Use the `parse_string` method to apply this + transformer. + """ def __init__(self): self.head_rules = [] - def visit_Rule(self, node): + def visit_Rule(self, node): # pylint: disable=C0103 + """ + Replaces the head of every rule with the intermediate `_body` predicate and stores all new head rules using this + intermediary predicate in `self.head_rules` + """ head = node.head body = node.body - # print(f"HEAD={head} BODY={body}") - if body: # remove MUS literals from rule - cleaned_body_literals = [x for x in node.body if x.atom.symbol.name not in ("__mus__",)] + cleaned_body_literals = [ + x for x in node.body if x.atom.symbol.name not in ("__mus__",) + ] cleaned_body = "; ".join([str(l) for l in cleaned_body_literals]) # get all variables used in body (to later reference in head) @@ -263,23 +280,23 @@ def visit_Rule(self, node): rule_body_base64_bytes = base64.b64encode(rule_body_string_bytes) rule_body_base64 = rule_body_base64_bytes.decode("ascii") - # print(cleaned_body, str(node), variables) - # create a new '_body' head for the original rule new_head_arguments = [ - _ast.SymbolicTerm(node.location, clingo.parse_term(f'"{rule_body_base64}"')), + _ast.SymbolicTerm( + node.location, clingo.parse_term(f'"{rule_body_base64}"') + ), _ast.Function( location=node.location, name="", - arguments=variables, - external=0 - ) + arguments=sorted(variables), + external=0, + ), ] new_head = _ast.Function( location=node.location, name="_body", arguments=new_head_arguments, - external=0 + external=0, ) node.head = new_head @@ -308,6 +325,13 @@ def parse_string(self, string: str) -> str: return "\n".join(out) + def parse_file(self, path: Union[str, Path], encoding: str = "utf-8") -> str: + """ + Parses the file at path and returns a string with the transformed program. + """ + with open(path, "r", encoding=encoding) as f: + return self.parse_string(f.read()) + __all__ = [ RuleIDTransformer.__name__, diff --git a/tests/clingexplaid/res/test_program_rules.lp b/tests/clingexplaid/res/test_program_rules.lp new file mode 100644 index 0000000..3543e09 --- /dev/null +++ b/tests/clingexplaid/res/test_program_rules.lp @@ -0,0 +1,5 @@ +constant. +head(1) :- body(1). +head(2) :- body(2,X,Y). +head(3) :- body(X). +:- constraint. \ No newline at end of file diff --git a/tests/clingexplaid/res/transformed_program_rules_split.lp b/tests/clingexplaid/res/transformed_program_rules_split.lp new file mode 100644 index 0000000..7e745f2 --- /dev/null +++ b/tests/clingexplaid/res/transformed_program_rules_split.lp @@ -0,0 +1,10 @@ +#program base. +constant. +_body("Ym9keSgxKQ==",(1,)) :- body(1). +_body("Ym9keSgyLFgsWSk=",(X,Y,2)) :- body(2,X,Y). +_body("Ym9keShYKQ==",(X,)) :- body(X). +_body("Y29uc3RyYWludA==",()) :- constraint. +head(1) :- _body("Ym9keSgxKQ==",(1,)). +head(2) :- _body("Ym9keSgyLFgsWSk=",(X,Y,2)). +head(3) :- _body("Ym9keShYKQ==",(X,)). +#false :- _body("Y29uc3RyYWludA==",()). \ No newline at end of file diff --git a/tests/clingexplaid/test_main.py b/tests/clingexplaid/test_main.py index 926de46..1998d28 100644 --- a/tests/clingexplaid/test_main.py +++ b/tests/clingexplaid/test_main.py @@ -15,6 +15,7 @@ AssumptionTransformer, ConstraintTransformer, RuleIDTransformer, + RuleSplitter, UntransformedException, ) @@ -159,6 +160,23 @@ def test_constraint_transformer(self): result.strip(), self.read_file(program_path_transformed).strip() ) + # --- RULE SPLITTER + + def test_rule_splitter(self): + """ + Test the RuleSplitter's `parse_file` method. + """ + + program_path = TEST_DIR.joinpath("res/test_program_rules.lp") + program_path_transformed = TEST_DIR.joinpath( + "res/transformed_program_rules_split.lp" + ) + rs = RuleSplitter() + result = rs.parse_file(program_path) + self.assertEqual( + result.strip(), self.read_file(program_path_transformed).strip() + ) + # MUC def test_core_computer_shrink_single_muc(self): From 1ec141a4ebffc8546f50d1e99043ff23c4f9e01f Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 9 Oct 2023 22:29:51 +0200 Subject: [PATCH 14/82] current progress on debuging the asp approach --- examples/sudoku_4x4.lp | 4 +-- examples/sudoku_encoding.lp | 2 +- examples/sudoku_encoding_2.lp | 15 +++++++++ examples/test.lp | 3 ++ examples/test2.lp | 8 +++++ examples/test3.lp | 4 +++ examples/test4.lp | 13 ++++++++ examples/x.lp | 32 +++++++++++++++++++ experiments/asp_appraoch/example.multi_muc.lp | 4 ++- src/clingexplaid/utils/cli.py | 16 ++++++++++ .../utils/logic_programs/asp_approach.lp | 2 ++ 11 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 examples/sudoku_encoding_2.lp create mode 100644 examples/test.lp create mode 100644 examples/test2.lp create mode 100644 examples/test3.lp create mode 100644 examples/test4.lp create mode 100644 examples/x.lp diff --git a/examples/sudoku_4x4.lp b/examples/sudoku_4x4.lp index 6dfbe05..6b7e0b2 100644 --- a/examples/sudoku_4x4.lp +++ b/examples/sudoku_4x4.lp @@ -1,8 +1,8 @@ % SUDOKU EXAMPLE (4x4) - -#include "sudoku_encoding.lp". #const subgrid_size=2. +#include "sudoku_encoding_2.lp". + initial(1,1,4). initial(4,1,4). initial(1,4,2). diff --git a/examples/sudoku_encoding.lp b/examples/sudoku_encoding.lp index bbc4630..3c927ca 100644 --- a/examples/sudoku_encoding.lp +++ b/examples/sudoku_encoding.lp @@ -4,7 +4,7 @@ % #const subgrid_size=2/3. -number(1..subgrid_size**2). +number(1..2**2). solution(X,Y,V) :- initial(X,Y,V). {solution(X,Y,N): number(N)}=1 :- number(X) ,number(Y). diff --git a/examples/sudoku_encoding_2.lp b/examples/sudoku_encoding_2.lp new file mode 100644 index 0000000..22e5851 --- /dev/null +++ b/examples/sudoku_encoding_2.lp @@ -0,0 +1,15 @@ +% ########### SUDOKU SOLVER ########### + +% GENERATING + +% #const subgrid_size=2/3. + +solution(X,Y,V) :- initial(X,Y,V). +{solution(X,Y,N): N=1..4}=1 :- X=1..4, Y=1..4. +cage(X1,Y1,X2,Y2):- solution(X1,Y1,_), solution(X2,Y2,_), ((X1-1)/subgrid_size)==((X2-1)/subgrid_size), ((Y1-1)/subgrid_size)==((Y2-1)/subgrid_size). + +:- solution(X,Y1,N), solution(X,Y2,N), Y1 != Y2. +:- solution(X1,Y,N), solution(X2,Y,N), X1 != X2. +:- cage(X1,Y1,X2,Y2), solution(X1,Y1,N), solution(X2,Y2,N), X1!=X2, Y1!=Y2. + +#show solution/3. diff --git a/examples/test.lp b/examples/test.lp new file mode 100644 index 0000000..2fb89da --- /dev/null +++ b/examples/test.lp @@ -0,0 +1,3 @@ +a. b. c. + +{a; b}=1. diff --git a/examples/test2.lp b/examples/test2.lp new file mode 100644 index 0000000..04ae796 --- /dev/null +++ b/examples/test2.lp @@ -0,0 +1,8 @@ +{a}. {b}. {c}. + +% {a; b}. + +unsat :- a, b. +unsat :- not a, not b. + +:- not unsat. diff --git a/examples/test3.lp b/examples/test3.lp new file mode 100644 index 0000000..88739da --- /dev/null +++ b/examples/test3.lp @@ -0,0 +1,4 @@ +a. b. c. + +:- a, b. +:- not a, not b. diff --git a/examples/test4.lp b/examples/test4.lp new file mode 100644 index 0000000..2dccda4 --- /dev/null +++ b/examples/test4.lp @@ -0,0 +1,13 @@ +{a}. {b}. {c}. + +% {a; b}. + +unsat :- a, b. +unsat :- not a, not b. + +:- not unsat. +:- not a, not b, not c. + +#heuristic a.[1,false] +#heuristic b.[1,false] +#heuristic c.[1,false] diff --git a/examples/x.lp b/examples/x.lp new file mode 100644 index 0000000..5378bb7 --- /dev/null +++ b/examples/x.lp @@ -0,0 +1,32 @@ +#program base. +#const subgrid_size = 2. +solution(X,Y,V) :- initial(X,Y,V). +1 = { solution(X,Y,N): N = (1..4) } :- X = (1..4); Y = (1..4). +cage(X1,Y1,X2,Y2) :- solution(X1,Y1,_); solution(X2,Y2,_); ((X1-1)/subgrid_size) = ((X2-1)/subgrid_size); ((Y1-1)/subgrid_size) = ((Y2-1)/subgrid_size). +#false :- solution(X,Y1,N); solution(X,Y2,N); Y1 != Y2. +#false :- solution(X1,Y,N); solution(X2,Y,N); X1 != X2. +#false :- cage(X1,Y1,X2,Y2); solution(X1,Y1,N); solution(X2,Y2,N); X1 != X2; Y1 != Y2. +#show solution/3. +#program base. +%{ initial(1,1,4) }. +%{ initial(4,1,4) }. +%{ initial(1,4,2) }. +%{ initial(2,3,3) }. +%{ initial(3,3,1) }. +%{ initial(3,4,3) }. +#show _muc/1. +#show _assumption/1. +_assumption(initial(1,1,4)). +_muc(initial(1,1,4)) :- initial(1,1,4). +_assumption(initial(4,1,4)). +_muc(initial(4,1,4)) :- initial(4,1,4). +_assumption(initial(1,4,2)). +_muc(initial(1,4,2)) :- initial(1,4,2). +_assumption(initial(2,3,3)). +_muc(initial(2,3,3)) :- initial(2,3,3). +_assumption(initial(3,3,1)). +_muc(initial(3,3,1)) :- initial(3,3,1). +_assumption(initial(3,4,3)). +_muc(initial(3,4,3)) :- initial(3,4,3). +#show initial/3. + diff --git a/experiments/asp_appraoch/example.multi_muc.lp b/experiments/asp_appraoch/example.multi_muc.lp index 4eb07f5..448d77c 100644 --- a/experiments/asp_appraoch/example.multi_muc.lp +++ b/experiments/asp_appraoch/example.multi_muc.lp @@ -10,4 +10,6 @@ test(1..k). :- a. :- c, d. -:- test(1). \ No newline at end of file +:- test(1). + +test_out(X,Y) :- test(X), test(Y). \ No newline at end of file diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index a279557..7a95d49 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -90,6 +90,8 @@ def _apply_assumption_transformer( return program_transformed, at def _print_found_muc(self, model): + print("-" * 50) + print([str(s) for s in model.symbols(atoms=True) if "unsat" in str(s)]) result = ".\n".join([str(a) for a in model.symbols(shown=True)]) if result: result += "." @@ -158,19 +160,24 @@ def _find_multi_mucs(self, control: clingo.Control, files): literal_lookup = get_solver_literal_lookup(assumption_control) additional_rules = [] + assumption_strings = set() assumption_signatures = set() for assumption_literal in at.get_assumptions( assumption_control, constants=constants ): assumption = literal_lookup[assumption_literal] + print(assumption) assumption_signatures.add((assumption.name, len(assumption.arguments))) + assumption_strings.add(str(assumption)) additional_rules.append(f"_assumption({str(assumption)}).") additional_rules.append(f"_muc({assumption}) :- {assumption}.") for signature, arity in assumption_signatures: additional_rules.append(f"#show {signature}/{arity}.") + print("ADDITIONAL RULES:", additional_rules) + final_program = "\n".join( ( # add constants like this because clingox reify doesn't support a custom control or other way to @@ -183,6 +190,9 @@ def _find_multi_mucs(self, control: clingo.Control, files): ) ) + print("-"*50) + print(final_program) + # Implicit Grounding for reification symbols = reify_program(final_program) reified_program = "\n".join([f"{str(s)}." for s in symbols]) @@ -194,6 +204,12 @@ def _find_multi_mucs(self, control: clingo.Control, files): ) as f: meta_encoding = f.read() + # meta_encoding += "\n:- not " + ", not ".join([f"assumption_hold({a})" for a in assumption_strings]) + "." + + # print(meta_encoding) + # print("-"*50) + # print(reified_program) + # Second Grounding to get MUCs with original control control.add("base", [], reified_program) control.add("base", [], meta_encoding) diff --git a/src/clingexplaid/utils/logic_programs/asp_approach.lp b/src/clingexplaid/utils/logic_programs/asp_approach.lp index 194c6a7..89b4341 100644 --- a/src/clingexplaid/utils/logic_programs/asp_approach.lp +++ b/src/clingexplaid/utils/logic_programs/asp_approach.lp @@ -34,3 +34,5 @@ hold(X) :- choice_hold(X). % #show unsat/1. % #show choice_hold/2. #show muc(T): output(_muc(T),B), conjunction(B). + +assumption_hold(T) :- output(_muc(T),B), conjunction(B). \ No newline at end of file From b4f3b5ffb17f6a652fdc59ed11dba8d4fb23f371 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 6 Nov 2023 13:54:30 +0100 Subject: [PATCH 15/82] switched ID approach back to default + documentation --- README.md | 31 +++++++++++++++++++++++++++++-- src/clingexplaid/utils/cli.py | 6 +++--- src/clingexplaid/utils/muc.py | 2 +- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 463d734..478041d 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,13 @@ pip install clingexplaid ## Usage -```shell +```bash clingexplaid -h ``` Compute Minimal Unsatisfiable Core from unsatisfiable program: -```shell +```bash clingexplaid --assumption-signature signature/arity ``` @@ -85,6 +85,33 @@ pre-commit install This blackens the source code whenever `git commit` is used. +## Experimental Features + +### Meta-encoding based approach (ASP-Approach) + +Using the `--muc-method` or `-m` option the approach for finding the MUCs can +be switched from the iterative deletion algorithm to the meta encoding based +approach. + ++ `-m 1` [default] Iterative deletion approach ++ `-m 2` Meta-encoding approach + +**Important Notes:** + ++ The Meta-encoding approach as it stands is not fully functional + +**Problem:** + + In the meta encoding all facts (or a selection matching a certain signature) are + transformed into assumptions which are then used as the assumption set for finding + the MUC + + During the MUC search when subsets of this assumption set are fixed for satisfiability + checking it is important that even though they are not fixed, the other assumptions + are not assumed as false but as undefined + + This is currently not possible with the meta-encoding, since assumptions are chosen + through a choice rule and all assumptions that aren't selected are defaulted to false + + This doesn't allow for properly checking if such subsets entail unsatisfiability and + thus prevents us from finding the proper MUCs + [doc]: https://potassco.org/clingo/python-api/current/ [nox]: https://nox.thea.codes/en/stable/index.html [pipx]: https://pypa.github.io/pipx/ diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 7a95d49..fc43e51 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -72,7 +72,7 @@ def register_options(self, options): options.add( group, "muc-method,m", - "This sets the method of finding the MUCs. (1) ASP Meta Encoding [default] (2) Iterative Deletion", + "EXPERIMENTAL: This sets the method of finding the MUCs. (1) Iterative Deletion (2) ASP Meta Encoding [default]", self._parse_mode, ) @@ -225,6 +225,6 @@ def main(self, control, files): print("clingexplaid", "version", version("clingexplaid")) if self.method == 1: - self._find_multi_mucs(control, files) - elif self.method == 2: self._find_single_muc(control, files) + elif self.method == 2: + self._find_multi_mucs(control, files) diff --git a/src/clingexplaid/utils/muc.py b/src/clingexplaid/utils/muc.py index 1ae4309..5aa582f 100644 --- a/src/clingexplaid/utils/muc.py +++ b/src/clingexplaid/utils/muc.py @@ -70,7 +70,7 @@ def _compute_single_minimal( muc_members: Set[Assumption] = set() working_set = set(assumptions) - for assumption in self.assumption_set: + for assumption in assumptions: # remove the current assumption from the working set working_set.remove(assumption) From a3bef2f28fd5aa0a56a628b7cd354d3dd8a1e8d2 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 6 Nov 2023 15:00:32 +0100 Subject: [PATCH 16/82] removed asp approach as option --- README.md | 4 + src/clingexplaid/utils/cli.py | 202 +++++++++++++++++----------------- 2 files changed, 108 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 478041d..05dd815 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,8 @@ This blackens the source code whenever `git commit` is used. ### Meta-encoding based approach (ASP-Approach) + + **Important Notes:** + The Meta-encoding approach as it stands is not fully functional diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index fc43e51..12846c1 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -69,12 +69,12 @@ def register_options(self, options): self._parse_assumption_signature, multi=True, ) - options.add( - group, - "muc-method,m", - "EXPERIMENTAL: This sets the method of finding the MUCs. (1) Iterative Deletion (2) ASP Meta Encoding [default]", - self._parse_mode, - ) + # options.add( + # group, + # "muc-method,m", + # "EXPERIMENTAL: This sets the method of finding the MUCs. (1) Iterative Deletion (2) ASP Meta Encoding [default]", + # self._parse_mode, + # ) def _apply_assumption_transformer( self, signatures: Dict[str, int], files: List[str] @@ -130,101 +130,107 @@ def _find_single_muc(self, control: clingo.Control, files): ) print(f"{COLORS['BLUE']}{result}{COLORS['NORMAL']}") - def _find_multi_mucs(self, control: clingo.Control, files): - print("ASP APPROACH") - - program_transformed, at = self._apply_assumption_transformer( - signatures=self.signatures, files=files - ) - - arguments = sys.argv[1:] - constant_names = [] - next_is_const = False - for arg in arguments: - if next_is_const: - constant_name = arg.strip().split("=")[0] - constant_names.append(constant_name) - next_is_const = False - if arg == "-c": - next_is_const = True - - constants = {name: control.get_const(name) for name in constant_names} - - # First Grounding for getting the assumptions - assumption_control = clingo.Control( - [f"-c {k}={str(v)}" for k, v in constants.items()] + def _find_multi_mucs_asp_approach(self, control: clingo.Control, files): + raise NotImplementedError( + "The meta encoding approach is faulty in its current state and thus not able to be used" ) - assumption_control.add("base", [], program_transformed) - assumption_control.ground([("base", [])]) - - literal_lookup = get_solver_literal_lookup(assumption_control) - - additional_rules = [] - assumption_strings = set() - - assumption_signatures = set() - for assumption_literal in at.get_assumptions( - assumption_control, constants=constants - ): - assumption = literal_lookup[assumption_literal] - print(assumption) - assumption_signatures.add((assumption.name, len(assumption.arguments))) - assumption_strings.add(str(assumption)) - additional_rules.append(f"_assumption({str(assumption)}).") - additional_rules.append(f"_muc({assumption}) :- {assumption}.") - - for signature, arity in assumption_signatures: - additional_rules.append(f"#show {signature}/{arity}.") - - print("ADDITIONAL RULES:", additional_rules) - - final_program = "\n".join( - ( - # add constants like this because clingox reify doesn't support a custom control or other way to - # provide constants. - "\n".join([f"#const {k}={str(v)}." for k, v in constants.items()]), - program_transformed, - "#show _muc/1.", - "#show _assumption/1.", - "\n".join(additional_rules), - ) - ) - - print("-"*50) - print(final_program) - # Implicit Grounding for reification - symbols = reify_program(final_program) - reified_program = "\n".join([f"{str(s)}." for s in symbols]) - - with open( - Path(__file__).resolve().parent.joinpath("logic_programs/asp_approach.lp"), - "r", - encoding="utf-8", - ) as f: - meta_encoding = f.read() - - # meta_encoding += "\n:- not " + ", not ".join([f"assumption_hold({a})" for a in assumption_strings]) + "." - - # print(meta_encoding) - # print("-"*50) - # print(reified_program) - - # Second Grounding to get MUCs with original control - control.add("base", [], reified_program) - control.add("base", [], meta_encoding) - - control.configuration.solve.enum_mode = "domRec" # type: ignore - control.configuration.solver.heuristic = "Domain" # type: ignore - - control.ground([("base", [])]) - - control.solve(on_model=self._print_found_muc) + # print("ASP APPROACH") + # + # program_transformed, at = self._apply_assumption_transformer( + # signatures=self.signatures, files=files + # ) + # + # arguments = sys.argv[1:] + # constant_names = [] + # next_is_const = False + # for arg in arguments: + # if next_is_const: + # constant_name = arg.strip().split("=")[0] + # constant_names.append(constant_name) + # next_is_const = False + # if arg == "-c": + # next_is_const = True + # + # constants = {name: control.get_const(name) for name in constant_names} + # + # # First Grounding for getting the assumptions + # assumption_control = clingo.Control( + # [f"-c {k}={str(v)}" for k, v in constants.items()] + # ) + # assumption_control.add("base", [], program_transformed) + # assumption_control.ground([("base", [])]) + # + # literal_lookup = get_solver_literal_lookup(assumption_control) + # + # additional_rules = [] + # assumption_strings = set() + # + # assumption_signatures = set() + # for assumption_literal in at.get_assumptions( + # assumption_control, constants=constants + # ): + # assumption = literal_lookup[assumption_literal] + # print(assumption) + # assumption_signatures.add((assumption.name, len(assumption.arguments))) + # assumption_strings.add(str(assumption)) + # additional_rules.append(f"_assumption({str(assumption)}).") + # additional_rules.append(f"_muc({assumption}) :- {assumption}.") + # + # for signature, arity in assumption_signatures: + # additional_rules.append(f"#show {signature}/{arity}.") + # + # print("ADDITIONAL RULES:", additional_rules) + # + # final_program = "\n".join( + # ( + # # add constants like this because clingox reify doesn't support a custom control or other way to + # # provide constants. + # "\n".join([f"#const {k}={str(v)}." for k, v in constants.items()]), + # program_transformed, + # "#show _muc/1.", + # "#show _assumption/1.", + # "\n".join(additional_rules), + # ) + # ) + # + # print("-"*50) + # print(final_program) + # + # # Implicit Grounding for reification + # symbols = reify_program(final_program) + # reified_program = "\n".join([f"{str(s)}." for s in symbols]) + # + # with open( + # Path(__file__).resolve().parent.joinpath("logic_programs/asp_approach.lp"), + # "r", + # encoding="utf-8", + # ) as f: + # meta_encoding = f.read() + # + # # meta_encoding += "\n:- not " + ", not ".join([f"assumption_hold({a})" for a in assumption_strings]) + "." + # + # # print(meta_encoding) + # # print("-"*50) + # # print(reified_program) + # + # # Second Grounding to get MUCs with original control + # control.add("base", [], reified_program) + # control.add("base", [], meta_encoding) + # + # control.configuration.solve.enum_mode = "domRec" # type: ignore + # control.configuration.solver.heuristic = "Domain" # type: ignore + # + # control.ground([("base", [])]) + # + # control.solve(on_model=self._print_found_muc) def main(self, control, files): print("clingexplaid", "version", version("clingexplaid")) - if self.method == 1: - self._find_single_muc(control, files) - elif self.method == 2: - self._find_multi_mucs(control, files) + self._find_single_muc(control, files) + + # if self.method == 1: + # self._find_single_muc(control, files) + # elif self.method == 2: + # self._find_multi_mucs_asp_approach(control, files) From 37665a9057faca2e2836e5154ea5c31d7f45607e Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Sat, 11 Nov 2023 16:33:33 +0100 Subject: [PATCH 17/82] started cli refactoring --- src/clingexplaid/__main__.py | 4 +- src/clingexplaid/utils/cli.py | 249 ++++++------------------------ src/clingexplaid/utils/cli_old.py | 236 ++++++++++++++++++++++++++++ 3 files changed, 284 insertions(+), 205 deletions(-) create mode 100644 src/clingexplaid/utils/cli_old.py diff --git a/src/clingexplaid/__main__.py b/src/clingexplaid/__main__.py index 570f764..aeec471 100644 --- a/src/clingexplaid/__main__.py +++ b/src/clingexplaid/__main__.py @@ -6,14 +6,14 @@ from clingo.application import clingo_main -from clingexplaid.utils.cli import CoreComputerApp +from clingexplaid.utils.cli import ClingoExplaidApp def main(): """ Main function calling the application class """ - clingo_main(CoreComputerApp(sys.argv[0]), sys.argv[1:] + ["-V0"]) + clingo_main(ClingoExplaidApp(sys.argv[0]), sys.argv[1:] + ["-V0"]) sys.exit() diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 12846c1..bc7d4fe 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -1,63 +1,64 @@ -""" -Command Line Interface Utilities -""" - -import sys +import re from importlib.metadata import version -from pathlib import Path -from typing import Dict, List, Tuple -import clingo from clingo.application import Application -from clingox.reify import reify_program - -from clingexplaid.utils import get_solver_literal_lookup -from clingexplaid.utils.logger import BACKGROUND_COLORS, COLORS -from clingexplaid.utils.muc import CoreComputer -from clingexplaid.utils.transformer import AssumptionTransformer -class CoreComputerApp(Application): +class ClingoExplaidApp(Application): """ - Application class realizing the CoreComputer functionality of the `clingexplaid.utils.muc.CoreComputer` class. + Application class for executing clingo-explaid functionality on the command line """ - program_name: str = "core-computer" - version: str = "0.1" + CLINGEXPLAID_METHODS = { + "muc": None, + "unsat_constraints": None + } def __init__(self, name): # pylint: disable = unused-argument - self.signatures = {} - self.method = 1 - self.muc_id = 1 + self.method = None + + # MUC + self._muc_assumption_signatures = {} + self._muc_id = 1 - def _parse_assumption_signature(self, input_string: str) -> bool: - # signature_strings = input_string.strip().split(",") - signature_list = input_string.split("/") - if len(signature_list) != 2: - print("Not valid format for signature, expected name/arity") + def _check_integrity(self) -> None: + if self.method is None: + raise ValueError("No method assigned: A valid clingexplaid method has to be provided with --method,-m") + + def _parse_method(self, method: str) -> bool: + method_string = method.replace("=", "").strip() + if method_string not in self.CLINGEXPLAID_METHODS: + method_strings = ", ".join([f"[{str(k)}]" for k in self.CLINGEXPLAID_METHODS.keys()]) + print("PARSE ERROR: The clingexplaid method has to be one of the following:", method_strings) return False - self.signatures[signature_list[0]] = int(signature_list[1]) + self.method = method_string return True - def _parse_mode(self, input_string: str) -> bool: - try: - method_id = int(input_string.strip()) - except ValueError: - print("Not valid format for mode, expected 1 or 2") + def _parse_assumption_signature(self, assumption_signature: str) -> bool: + if self.method != "muc": + print("PARSE ERROR: The assumption signature option is only available for --mode=muc") return False - if method_id in (1, 2): - self.method = method_id - return True - return False - - def print_model(self, model, _): - return + assumption_signature_string = assumption_signature.replace("=", "").strip() + match_result = re.match(r"^([a-zA-Z]+)/([1-9][0-9]*)$", assumption_signature_string) + if match_result is None: + print("PARSE ERROR: Wrong signature format. The assumption signatures have to follow the format " + "/") + return False + self._muc_assumption_signatures[match_result.group(1)] = int(match_result.group(2)) + return True def register_options(self, options): - """ - See clingo.clingo_main(). - """ + group = "Clingo-Explaid Options" + + method_string_list = "\n".join([f"\t- {k}" for k in self.CLINGEXPLAID_METHODS.keys()]) + options.add( + group, + "method,m", + "For selecting the mode of clingexplaid. Possible options for are:\n" + method_string_list, + self._parse_method, + multi=False, + ) group = "MUC Options" @@ -69,168 +70,10 @@ def register_options(self, options): self._parse_assumption_signature, multi=True, ) - # options.add( - # group, - # "muc-method,m", - # "EXPERIMENTAL: This sets the method of finding the MUCs. (1) Iterative Deletion (2) ASP Meta Encoding [default]", - # self._parse_mode, - # ) - def _apply_assumption_transformer( - self, signatures: Dict[str, int], files: List[str] - ) -> Tuple[str, AssumptionTransformer]: - signature_set = set(self.signatures.items()) if signatures else None - at = AssumptionTransformer(signatures=signature_set) - if not files: - program_transformed = at.parse_files("-") - print("Reading from -") - else: - program_transformed = at.parse_files(files) - print(f"Reading from {files[0]} {'...' if len(files) > 1 else ''}") - return program_transformed, at - - def _print_found_muc(self, model): - print("-" * 50) - print([str(s) for s in model.symbols(atoms=True) if "unsat" in str(s)]) - result = ".\n".join([str(a) for a in model.symbols(shown=True)]) - if result: - result += "." - print( - f"{BACKGROUND_COLORS['BLUE']} MUC: {self.muc_id} {COLORS['NORMAL']}{COLORS['DARK_BLUE']}{COLORS['NORMAL']}" - ) - print(f"{COLORS['BLUE']}{result}{COLORS['NORMAL']}") - self.muc_id += 1 - - def _find_single_muc(self, control: clingo.Control, files): - program_transformed, at = self._apply_assumption_transformer( - signatures=self.signatures, files=files - ) - - control.add("base", [], program_transformed) - control.ground([("base", [])]) - - literal_lookup = get_solver_literal_lookup(control) - - assumptions = at.get_assumptions(control) - - cc = CoreComputer(control, assumptions) - - print("Solving...") - control.solve(assumptions=list(assumptions), on_core=cc.shrink) - - if cc.minimal is None: - print("SATISFIABLE: Instance has no MUCs") - return - - result = " ".join([str(literal_lookup[a]) for a in cc.minimal]) - - muc_id = 1 - print( - f"{BACKGROUND_COLORS['BLUE']} MUC: {muc_id} {COLORS['NORMAL']}{COLORS['DARK_BLUE']}{COLORS['NORMAL']}" - ) - print(f"{COLORS['BLUE']}{result}{COLORS['NORMAL']}") - - def _find_multi_mucs_asp_approach(self, control: clingo.Control, files): - raise NotImplementedError( - "The meta encoding approach is faulty in its current state and thus not able to be used" - ) - - # print("ASP APPROACH") - # - # program_transformed, at = self._apply_assumption_transformer( - # signatures=self.signatures, files=files - # ) - # - # arguments = sys.argv[1:] - # constant_names = [] - # next_is_const = False - # for arg in arguments: - # if next_is_const: - # constant_name = arg.strip().split("=")[0] - # constant_names.append(constant_name) - # next_is_const = False - # if arg == "-c": - # next_is_const = True - # - # constants = {name: control.get_const(name) for name in constant_names} - # - # # First Grounding for getting the assumptions - # assumption_control = clingo.Control( - # [f"-c {k}={str(v)}" for k, v in constants.items()] - # ) - # assumption_control.add("base", [], program_transformed) - # assumption_control.ground([("base", [])]) - # - # literal_lookup = get_solver_literal_lookup(assumption_control) - # - # additional_rules = [] - # assumption_strings = set() - # - # assumption_signatures = set() - # for assumption_literal in at.get_assumptions( - # assumption_control, constants=constants - # ): - # assumption = literal_lookup[assumption_literal] - # print(assumption) - # assumption_signatures.add((assumption.name, len(assumption.arguments))) - # assumption_strings.add(str(assumption)) - # additional_rules.append(f"_assumption({str(assumption)}).") - # additional_rules.append(f"_muc({assumption}) :- {assumption}.") - # - # for signature, arity in assumption_signatures: - # additional_rules.append(f"#show {signature}/{arity}.") - # - # print("ADDITIONAL RULES:", additional_rules) - # - # final_program = "\n".join( - # ( - # # add constants like this because clingox reify doesn't support a custom control or other way to - # # provide constants. - # "\n".join([f"#const {k}={str(v)}." for k, v in constants.items()]), - # program_transformed, - # "#show _muc/1.", - # "#show _assumption/1.", - # "\n".join(additional_rules), - # ) - # ) - # - # print("-"*50) - # print(final_program) - # - # # Implicit Grounding for reification - # symbols = reify_program(final_program) - # reified_program = "\n".join([f"{str(s)}." for s in symbols]) - # - # with open( - # Path(__file__).resolve().parent.joinpath("logic_programs/asp_approach.lp"), - # "r", - # encoding="utf-8", - # ) as f: - # meta_encoding = f.read() - # - # # meta_encoding += "\n:- not " + ", not ".join([f"assumption_hold({a})" for a in assumption_strings]) + "." - # - # # print(meta_encoding) - # # print("-"*50) - # # print(reified_program) - # - # # Second Grounding to get MUCs with original control - # control.add("base", [], reified_program) - # control.add("base", [], meta_encoding) - # - # control.configuration.solve.enum_mode = "domRec" # type: ignore - # control.configuration.solver.heuristic = "Domain" # type: ignore - # - # control.ground([("base", [])]) - # - # control.solve(on_model=self._print_found_muc) + def print_model(self, model, _): + return def main(self, control, files): print("clingexplaid", "version", version("clingexplaid")) - - self._find_single_muc(control, files) - - # if self.method == 1: - # self._find_single_muc(control, files) - # elif self.method == 2: - # self._find_multi_mucs_asp_approach(control, files) + self._check_integrity() diff --git a/src/clingexplaid/utils/cli_old.py b/src/clingexplaid/utils/cli_old.py new file mode 100644 index 0000000..12846c1 --- /dev/null +++ b/src/clingexplaid/utils/cli_old.py @@ -0,0 +1,236 @@ +""" +Command Line Interface Utilities +""" + +import sys +from importlib.metadata import version +from pathlib import Path +from typing import Dict, List, Tuple + +import clingo +from clingo.application import Application +from clingox.reify import reify_program + +from clingexplaid.utils import get_solver_literal_lookup +from clingexplaid.utils.logger import BACKGROUND_COLORS, COLORS +from clingexplaid.utils.muc import CoreComputer +from clingexplaid.utils.transformer import AssumptionTransformer + + +class CoreComputerApp(Application): + """ + Application class realizing the CoreComputer functionality of the `clingexplaid.utils.muc.CoreComputer` class. + """ + + program_name: str = "core-computer" + version: str = "0.1" + + def __init__(self, name): + # pylint: disable = unused-argument + self.signatures = {} + self.method = 1 + self.muc_id = 1 + + def _parse_assumption_signature(self, input_string: str) -> bool: + # signature_strings = input_string.strip().split(",") + signature_list = input_string.split("/") + if len(signature_list) != 2: + print("Not valid format for signature, expected name/arity") + return False + self.signatures[signature_list[0]] = int(signature_list[1]) + return True + + def _parse_mode(self, input_string: str) -> bool: + try: + method_id = int(input_string.strip()) + except ValueError: + print("Not valid format for mode, expected 1 or 2") + return False + if method_id in (1, 2): + self.method = method_id + return True + return False + + def print_model(self, model, _): + return + + def register_options(self, options): + """ + See clingo.clingo_main(). + """ + + group = "MUC Options" + + options.add( + group, + "assumption-signature,a", + "Facts matching with this signature will be converted to assumptions for finding a MUC " + "(default: all facts)", + self._parse_assumption_signature, + multi=True, + ) + # options.add( + # group, + # "muc-method,m", + # "EXPERIMENTAL: This sets the method of finding the MUCs. (1) Iterative Deletion (2) ASP Meta Encoding [default]", + # self._parse_mode, + # ) + + def _apply_assumption_transformer( + self, signatures: Dict[str, int], files: List[str] + ) -> Tuple[str, AssumptionTransformer]: + signature_set = set(self.signatures.items()) if signatures else None + at = AssumptionTransformer(signatures=signature_set) + if not files: + program_transformed = at.parse_files("-") + print("Reading from -") + else: + program_transformed = at.parse_files(files) + print(f"Reading from {files[0]} {'...' if len(files) > 1 else ''}") + return program_transformed, at + + def _print_found_muc(self, model): + print("-" * 50) + print([str(s) for s in model.symbols(atoms=True) if "unsat" in str(s)]) + result = ".\n".join([str(a) for a in model.symbols(shown=True)]) + if result: + result += "." + print( + f"{BACKGROUND_COLORS['BLUE']} MUC: {self.muc_id} {COLORS['NORMAL']}{COLORS['DARK_BLUE']}{COLORS['NORMAL']}" + ) + print(f"{COLORS['BLUE']}{result}{COLORS['NORMAL']}") + self.muc_id += 1 + + def _find_single_muc(self, control: clingo.Control, files): + program_transformed, at = self._apply_assumption_transformer( + signatures=self.signatures, files=files + ) + + control.add("base", [], program_transformed) + control.ground([("base", [])]) + + literal_lookup = get_solver_literal_lookup(control) + + assumptions = at.get_assumptions(control) + + cc = CoreComputer(control, assumptions) + + print("Solving...") + control.solve(assumptions=list(assumptions), on_core=cc.shrink) + + if cc.minimal is None: + print("SATISFIABLE: Instance has no MUCs") + return + + result = " ".join([str(literal_lookup[a]) for a in cc.minimal]) + + muc_id = 1 + print( + f"{BACKGROUND_COLORS['BLUE']} MUC: {muc_id} {COLORS['NORMAL']}{COLORS['DARK_BLUE']}{COLORS['NORMAL']}" + ) + print(f"{COLORS['BLUE']}{result}{COLORS['NORMAL']}") + + def _find_multi_mucs_asp_approach(self, control: clingo.Control, files): + raise NotImplementedError( + "The meta encoding approach is faulty in its current state and thus not able to be used" + ) + + # print("ASP APPROACH") + # + # program_transformed, at = self._apply_assumption_transformer( + # signatures=self.signatures, files=files + # ) + # + # arguments = sys.argv[1:] + # constant_names = [] + # next_is_const = False + # for arg in arguments: + # if next_is_const: + # constant_name = arg.strip().split("=")[0] + # constant_names.append(constant_name) + # next_is_const = False + # if arg == "-c": + # next_is_const = True + # + # constants = {name: control.get_const(name) for name in constant_names} + # + # # First Grounding for getting the assumptions + # assumption_control = clingo.Control( + # [f"-c {k}={str(v)}" for k, v in constants.items()] + # ) + # assumption_control.add("base", [], program_transformed) + # assumption_control.ground([("base", [])]) + # + # literal_lookup = get_solver_literal_lookup(assumption_control) + # + # additional_rules = [] + # assumption_strings = set() + # + # assumption_signatures = set() + # for assumption_literal in at.get_assumptions( + # assumption_control, constants=constants + # ): + # assumption = literal_lookup[assumption_literal] + # print(assumption) + # assumption_signatures.add((assumption.name, len(assumption.arguments))) + # assumption_strings.add(str(assumption)) + # additional_rules.append(f"_assumption({str(assumption)}).") + # additional_rules.append(f"_muc({assumption}) :- {assumption}.") + # + # for signature, arity in assumption_signatures: + # additional_rules.append(f"#show {signature}/{arity}.") + # + # print("ADDITIONAL RULES:", additional_rules) + # + # final_program = "\n".join( + # ( + # # add constants like this because clingox reify doesn't support a custom control or other way to + # # provide constants. + # "\n".join([f"#const {k}={str(v)}." for k, v in constants.items()]), + # program_transformed, + # "#show _muc/1.", + # "#show _assumption/1.", + # "\n".join(additional_rules), + # ) + # ) + # + # print("-"*50) + # print(final_program) + # + # # Implicit Grounding for reification + # symbols = reify_program(final_program) + # reified_program = "\n".join([f"{str(s)}." for s in symbols]) + # + # with open( + # Path(__file__).resolve().parent.joinpath("logic_programs/asp_approach.lp"), + # "r", + # encoding="utf-8", + # ) as f: + # meta_encoding = f.read() + # + # # meta_encoding += "\n:- not " + ", not ".join([f"assumption_hold({a})" for a in assumption_strings]) + "." + # + # # print(meta_encoding) + # # print("-"*50) + # # print(reified_program) + # + # # Second Grounding to get MUCs with original control + # control.add("base", [], reified_program) + # control.add("base", [], meta_encoding) + # + # control.configuration.solve.enum_mode = "domRec" # type: ignore + # control.configuration.solver.heuristic = "Domain" # type: ignore + # + # control.ground([("base", [])]) + # + # control.solve(on_model=self._print_found_muc) + + def main(self, control, files): + print("clingexplaid", "version", version("clingexplaid")) + + self._find_single_muc(control, files) + + # if self.method == 1: + # self._find_single_muc(control, files) + # elif self.method == 2: + # self._find_multi_mucs_asp_approach(control, files) From cdb6b02f37915a75721425e782af828ad0f390f4 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Sun, 12 Nov 2023 13:56:40 +0100 Subject: [PATCH 18/82] restored full old functionallity after refactor --- setup.cfg | 2 +- src/clingexplaid/utils/cli.py | 111 +++++++++++++++++++++++++++++----- 2 files changed, 98 insertions(+), 15 deletions(-) diff --git a/setup.cfg b/setup.cfg index 2f8d12e..d1b3c83 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = clingexplaid -version = 1.0.0 +version = 1.0.1 author = Hannes Weichelt author_email = weichelt.h@uni-potsdam.de description = Tools for Explaination with clingo. diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index bc7d4fe..81a4e5f 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -1,22 +1,30 @@ import re from importlib.metadata import version +from typing import Dict, List, Tuple +import clingo from clingo.application import Application +from .muc import CoreComputer +from .transformer import AssumptionTransformer +from ..utils import get_solver_literal_lookup +from ..utils.logger import BACKGROUND_COLORS, COLORS + class ClingoExplaidApp(Application): """ Application class for executing clingo-explaid functionality on the command line """ - CLINGEXPLAID_METHODS = { - "muc": None, - "unsat_constraints": None - } + CLINGEXPLAID_METHODS = {"muc", "unsat-constraints"} def __init__(self, name): # pylint: disable = unused-argument self.method = None + self.method_functions = { + m: getattr(self, f'_method_{m.replace("-", "_")}') + for m in self.CLINGEXPLAID_METHODS + } # MUC self._muc_assumption_signatures = {} @@ -24,38 +32,54 @@ def __init__(self, name): def _check_integrity(self) -> None: if self.method is None: - raise ValueError("No method assigned: A valid clingexplaid method has to be provided with --method,-m") + raise ValueError( + "No method assigned: A valid clingexplaid method has to be provided with --method,-m" + ) def _parse_method(self, method: str) -> bool: method_string = method.replace("=", "").strip() if method_string not in self.CLINGEXPLAID_METHODS: - method_strings = ", ".join([f"[{str(k)}]" for k in self.CLINGEXPLAID_METHODS.keys()]) - print("PARSE ERROR: The clingexplaid method has to be one of the following:", method_strings) + method_strings = ", ".join( + [f"[{str(k)}]" for k in self.CLINGEXPLAID_METHODS] + ) + print( + "PARSE ERROR: The clingexplaid method has to be one of the following:", + method_strings, + ) return False self.method = method_string return True def _parse_assumption_signature(self, assumption_signature: str) -> bool: if self.method != "muc": - print("PARSE ERROR: The assumption signature option is only available for --mode=muc") + print( + "PARSE ERROR: The assumption signature option is only available for --mode=muc" + ) return False assumption_signature_string = assumption_signature.replace("=", "").strip() - match_result = re.match(r"^([a-zA-Z]+)/([1-9][0-9]*)$", assumption_signature_string) + match_result = re.match( + r"^([a-zA-Z]+)/([1-9][0-9]*)$", assumption_signature_string + ) if match_result is None: - print("PARSE ERROR: Wrong signature format. The assumption signatures have to follow the format " - "/") + print( + "PARSE ERROR: Wrong signature format. The assumption signatures have to follow the format " + "/" + ) return False - self._muc_assumption_signatures[match_result.group(1)] = int(match_result.group(2)) + self._muc_assumption_signatures[match_result.group(1)] = int( + match_result.group(2) + ) return True def register_options(self, options): group = "Clingo-Explaid Options" - method_string_list = "\n".join([f"\t- {k}" for k in self.CLINGEXPLAID_METHODS.keys()]) + method_string_list = "\n".join([f"\t- {k}" for k in self.CLINGEXPLAID_METHODS]) options.add( group, "method,m", - "For selecting the mode of clingexplaid. Possible options for are:\n" + method_string_list, + "For selecting the mode of clingexplaid. Possible options for are:\n" + + method_string_list, self._parse_method, multi=False, ) @@ -71,9 +95,68 @@ def register_options(self, options): multi=True, ) + def _apply_assumption_transformer( + self, signatures: Dict[str, int], files: List[str] + ) -> Tuple[str, AssumptionTransformer]: + signature_set = ( + set(self._muc_assumption_signatures.items()) if signatures else None + ) + at = AssumptionTransformer(signatures=signature_set) + if not files: + program_transformed = at.parse_files("-") + else: + program_transformed = at.parse_files(files) + return program_transformed, at + + def _print_muc(self, muc) -> None: + print( + f"{BACKGROUND_COLORS['BLUE']} MUC: {self._muc_id} {COLORS['NORMAL']}{COLORS['DARK_BLUE']}{COLORS['NORMAL']}" + ) + print(f"{COLORS['BLUE']}{muc}{COLORS['NORMAL']}") + self._muc_id += 1 + + def _method_muc(self, control: clingo.Control, files: List[str]): + print("method: muc") + + program_transformed, at = self._apply_assumption_transformer( + signatures=self._muc_assumption_signatures, files=files + ) + + control.add("base", [], program_transformed) + control.ground([("base", [])]) + + literal_lookup = get_solver_literal_lookup(control) + assumptions = at.get_assumptions(control) + cc = CoreComputer(control, assumptions) + + print("Solving...") + control.solve(assumptions=list(assumptions), on_core=cc.shrink) + + if cc.minimal is None: + print("SATISFIABLE: Instance has no MUCs") + return + + muc_string = " ".join([str(literal_lookup[a]) for a in cc.minimal]) + self._print_muc(muc_string) + + def _method_unsat_constraints(self, control: clingo.Control, files: List[str]): + print("method: unsat-constraints") + raise NotImplementedError( + "The unsat-constraints method has not been implemented yet" + ) + def print_model(self, model, _): return def main(self, control, files): print("clingexplaid", "version", version("clingexplaid")) self._check_integrity() + + # printing the input files + if not files: + print("Reading from -") + else: + print(f"Reading from {files[0]} {'...' if len(files) > 1 else ''}") + + method_function = self.method_functions[self.method] + method_function(control, files) From 1d8806eed48cc98a0ca7f799e61bfc21a9d29250 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Wed, 15 Nov 2023 15:08:24 +0100 Subject: [PATCH 19/82] added first implemenetation of unsat-constraints method --- src/clingexplaid/utils/cli.py | 62 ++++++++++++++++++++++++--- src/clingexplaid/utils/logger.py | 3 ++ src/clingexplaid/utils/transformer.py | 26 ++++++++--- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 81a4e5f..a313e99 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -6,7 +6,7 @@ from clingo.application import Application from .muc import CoreComputer -from .transformer import AssumptionTransformer +from .transformer import AssumptionTransformer, ConstraintTransformer from ..utils import get_solver_literal_lookup from ..utils.logger import BACKGROUND_COLORS, COLORS @@ -110,7 +110,7 @@ def _apply_assumption_transformer( def _print_muc(self, muc) -> None: print( - f"{BACKGROUND_COLORS['BLUE']} MUC: {self._muc_id} {COLORS['NORMAL']}{COLORS['DARK_BLUE']}{COLORS['NORMAL']}" + f"{BACKGROUND_COLORS['BLUE']} MUC {BACKGROUND_COLORS['LIGHT_BLUE']} {self._muc_id} {COLORS['NORMAL']}" ) print(f"{COLORS['BLUE']}{muc}{COLORS['NORMAL']}") self._muc_id += 1 @@ -139,11 +139,63 @@ def _method_muc(self, control: clingo.Control, files: List[str]): muc_string = " ".join([str(literal_lookup[a]) for a in cc.minimal]) self._print_muc(muc_string) + def _print_unsat_constraints(self, unsat_constraints) -> None: + print(f"{BACKGROUND_COLORS['RED']} Unsat Constraints {COLORS['NORMAL']}") + for c in unsat_constraints: + print(f"{COLORS['RED']}{c}{COLORS['NORMAL']}") + def _method_unsat_constraints(self, control: clingo.Control, files: List[str]): print("method: unsat-constraints") - raise NotImplementedError( - "The unsat-constraints method has not been implemented yet" - ) + + unsat_constraint_atom = "__unsat__" + ct = ConstraintTransformer(unsat_constraint_atom, include_id=True) + + print(files) + + if not files: + program_transformed = ct.parse_files("-") + else: + program_transformed = ct.parse_files(files) + + minimizer_rule = f"#minimize {{1,X : {unsat_constraint_atom}(X)}}." + final_program = program_transformed + "\n" + minimizer_rule + + constraint_lookup = {} + for line in final_program.split("\n"): + id_re = re.compile(f"{unsat_constraint_atom}\(([1-9][0-9]*)\)") + match_result = id_re.match(line) + if match_result is None: + continue + constraint_id = match_result.group(1) + constraint_lookup[int(constraint_id)] = ( + str(line) + .replace(f"{unsat_constraint_atom}({constraint_id})", "") + .strip() + ) + + control.add("base", [], final_program) + control.ground([("base", [])]) + + with control.solve(yield_=True) as solve_handle: + model = solve_handle.model() + unsat_constraint_atoms = [] + model_symbols_shown = [] + while model is not None: + unsat_constraint_atoms = [ + a + for a in model.symbols(atoms=True) + if a.match(unsat_constraint_atom, 1, True) + ] + model_symbols_shown = model.symbols(shown=True) + solve_handle.resume() + model = solve_handle.model() + print(" ".join([str(s) for s in model_symbols_shown])) + unsat_constraints = [] + for a in unsat_constraint_atoms: + constraint = constraint_lookup.get(a.arguments[0].number) + unsat_constraints.append(constraint) + + self._print_unsat_constraints(unsat_constraints) def print_model(self, model, _): return diff --git a/src/clingexplaid/utils/logger.py b/src/clingexplaid/utils/logger.py index 0c174a5..83e8806 100644 --- a/src/clingexplaid/utils/logger.py +++ b/src/clingexplaid/utils/logger.py @@ -12,11 +12,14 @@ "GREEN": "\033[92m", "YELLOW": "\033[93m", "RED": "\033[91m", + "DARK_RED": "\033[91m", "NORMAL": "\033[0m", } BACKGROUND_COLORS = { "BLUE": "\033[44m", + "LIGHT_BLUE": "\033[104m", + "RED": "\033[41m", } diff --git a/src/clingexplaid/utils/transformer.py b/src/clingexplaid/utils/transformer.py index e6a7d97..ad9ae3b 100644 --- a/src/clingexplaid/utils/transformer.py +++ b/src/clingexplaid/utils/transformer.py @@ -196,8 +196,10 @@ class ConstraintTransformer(_ast.Transformer): A Transformer that takes all constraint rules and adds an atom to their head to avoid deriving false through them. """ - def __init__(self, constraint_head_symbol: str): + def __init__(self, constraint_head_symbol: str, include_id: bool = False): self.constraint_head_symbol = constraint_head_symbol + self.include_id = include_id + self.constraint_id = 1 def visit_Rule(self, node): # pylint: disable=C0103 """ @@ -210,12 +212,21 @@ def visit_Rule(self, node): # pylint: disable=C0103 if node.head.atom.value != 0: return node + arguments = [] + if self.include_id: + arguments = [ + _ast.SymbolicTerm( + node.location, clingo.parse_term(str(self.constraint_id)) + ) + ] + head_symbol = _ast.Function( location=node.location, name=self.constraint_head_symbol, - arguments=[], + arguments=arguments, external=0, ) + self.constraint_id += 1 # insert id symbol into body of rule node.head = head_symbol @@ -231,12 +242,15 @@ def parse_string(self, string: str) -> str: return "\n".join(out) - def parse_file(self, path: Union[str, Path], encoding: str = "utf-8") -> str: + def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: """ - Parses the file at path and returns a string with the transformed program. + Parses the files and returns a string with the transformed program. """ - with open(path, "r", encoding=encoding) as f: - return self.parse_string(f.read()) + out = [] + _ast.parse_files( + [str(p) for p in paths], lambda stm: out.append((str(self(stm)))) + ) + return "\n".join(out) class RuleSplitter(_ast.Transformer): From 0259af57961c6a83016c39b79ccb7176e4bb1106 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 18 Dec 2023 15:17:26 +0100 Subject: [PATCH 20/82] added todos to README --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 05dd815..9eeca92 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,24 @@ pre-commit install This blackens the source code whenever `git commit` is used. +## ToDos + ++ [ ] New CLI structure + + different modes: + + MUC + + UNSAT-CONSTRAINTS + + can be enabled through flags ++ [ ] Iterative Deltion for Multiple MUCs + + variation of the QuickXplain algorithm ++ [ ] Finish unsat-constraints implementation for the API ++ [ ] Give a warning in Transformer if control is not grounded yet ++ [ ] New option to enable verbose derivation output ++ [ ] Documentation + + Proper README + + Docstrings for all API functions + + CLI documentation with examples + + Examples folder + ## Experimental Features ### Meta-encoding based approach (ASP-Approach) From 7c9c39093dab3d92a35af532a88cf7571b293086 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 18 Dec 2023 15:17:45 +0100 Subject: [PATCH 21/82] added multi muc search --- src/clingexplaid/utils/cli.py | 109 ++++++++++++++++++++-------------- src/clingexplaid/utils/muc.py | 45 +++++++++++++- 2 files changed, 107 insertions(+), 47 deletions(-) diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index a313e99..4147b66 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -3,7 +3,7 @@ from typing import Dict, List, Tuple import clingo -from clingo.application import Application +from clingo.application import Application, Flag from .muc import CoreComputer from .transformer import AssumptionTransformer, ConstraintTransformer @@ -16,42 +16,38 @@ class ClingoExplaidApp(Application): Application class for executing clingo-explaid functionality on the command line """ - CLINGEXPLAID_METHODS = {"muc", "unsat-constraints"} + CLINGEXPLAID_METHODS = { + "muc": "Description for MUC method", + "unsat-constraints": "Description for unsat-constraints method", + } def __init__(self, name): # pylint: disable = unused-argument - self.method = None + self.methods = set() self.method_functions = { m: getattr(self, f'_method_{m.replace("-", "_")}') - for m in self.CLINGEXPLAID_METHODS + for m in self.CLINGEXPLAID_METHODS.keys() } + self.method_flags = {m: Flag() for m in self.CLINGEXPLAID_METHODS.keys()} # MUC self._muc_assumption_signatures = {} self._muc_id = 1 - def _check_integrity(self) -> None: - if self.method is None: - raise ValueError( - "No method assigned: A valid clingexplaid method has to be provided with --method,-m" - ) + def _initialize(self) -> None: + # add enabled methods to self.methods + for method, flag in self.method_flags.items(): + if flag.flag: + self.methods.add(method) - def _parse_method(self, method: str) -> bool: - method_string = method.replace("=", "").strip() - if method_string not in self.CLINGEXPLAID_METHODS: - method_strings = ", ".join( - [f"[{str(k)}]" for k in self.CLINGEXPLAID_METHODS] - ) - print( - "PARSE ERROR: The clingexplaid method has to be one of the following:", - method_strings, + if len(self.methods) == 0: + raise ValueError( + f"Clingexplaid was called without any method, pleas select at least one of the following methods: " + f"[{', '.join(['--' + str(m) for m in self.CLINGEXPLAID_METHODS.keys()])}]" ) - return False - self.method = method_string - return True def _parse_assumption_signature(self, assumption_signature: str) -> bool: - if self.method != "muc": + if "muc" in self.methods: print( "PARSE ERROR: The assumption signature option is only available for --mode=muc" ) @@ -72,17 +68,15 @@ def _parse_assumption_signature(self, assumption_signature: str) -> bool: return True def register_options(self, options): - group = "Clingo-Explaid Options" - - method_string_list = "\n".join([f"\t- {k}" for k in self.CLINGEXPLAID_METHODS]) - options.add( - group, - "method,m", - "For selecting the mode of clingexplaid. Possible options for are:\n" - + method_string_list, - self._parse_method, - multi=False, - ) + group = "Clingo-Explaid Methods" + + for method, description in self.CLINGEXPLAID_METHODS.items(): + options.add_flag( + group=group, + option=method, + description=description, + target=self.method_flags[method], + ) group = "MUC Options" @@ -116,8 +110,6 @@ def _print_muc(self, muc) -> None: self._muc_id += 1 def _method_muc(self, control: clingo.Control, files: List[str]): - print("method: muc") - program_transformed, at = self._apply_assumption_transformer( signatures=self._muc_assumption_signatures, files=files ) @@ -129,15 +121,33 @@ def _method_muc(self, control: clingo.Control, files: List[str]): assumptions = at.get_assumptions(control) cc = CoreComputer(control, assumptions) + max_models = int(control.configuration.solve.models) print("Solving...") - control.solve(assumptions=list(assumptions), on_core=cc.shrink) - if cc.minimal is None: - print("SATISFIABLE: Instance has no MUCs") - return + # Case: Finding a single MUC + if max_models == -1: + control.solve(assumptions=list(assumptions), on_core=cc.shrink) + + if cc.minimal is None: + print("SATISFIABLE: Instance has no MUCs") + return + + muc_string = " ".join([str(literal_lookup[a]) for a in cc.minimal]) + self._print_muc(muc_string) - muc_string = " ".join([str(literal_lookup[a]) for a in cc.minimal]) - self._print_muc(muc_string) + # Case: Finding multiple MUCs + if max_models >= 0: + program_unsat = False + with control.solve( + assumptions=list(assumptions), yield_=True + ) as solve_handle: + if not solve_handle.get().satisfiable: + program_unsat = True + + if program_unsat: + for muc in cc.get_multiple_minimal(max_mucs=max_models): + muc_string = " ".join([str(literal_lookup[a]) for a in muc]) + self._print_muc(muc_string) def _print_unsat_constraints(self, unsat_constraints) -> None: print(f"{BACKGROUND_COLORS['RED']} Unsat Constraints {COLORS['NORMAL']}") @@ -145,8 +155,6 @@ def _print_unsat_constraints(self, unsat_constraints) -> None: print(f"{COLORS['RED']}{c}{COLORS['NORMAL']}") def _method_unsat_constraints(self, control: clingo.Control, files: List[str]): - print("method: unsat-constraints") - unsat_constraint_atom = "__unsat__" ct = ConstraintTransformer(unsat_constraint_atom, include_id=True) @@ -202,7 +210,7 @@ def print_model(self, model, _): def main(self, control, files): print("clingexplaid", "version", version("clingexplaid")) - self._check_integrity() + self._initialize() # printing the input files if not files: @@ -210,5 +218,14 @@ def main(self, control, files): else: print(f"Reading from {files[0]} {'...' if len(files) > 1 else ''}") - method_function = self.method_functions[self.method] - method_function(control, files) + # standard case: only one method + if len(self.methods) == 1: + method = list(self.methods)[0] + method_function = self.method_functions[method] + method_function(control, files) + # special cases where specific pipelines have to be configured + elif self.methods == {"muc", "unsat-constraints"}: + print( + "NOT IMPLEMENTED: first find muc -> apply unsat-constraints to find conflicting constraints for" + "specific MUC" + ) diff --git a/src/clingexplaid/utils/muc.py b/src/clingexplaid/utils/muc.py index 5aa582f..20ddc0b 100644 --- a/src/clingexplaid/utils/muc.py +++ b/src/clingexplaid/utils/muc.py @@ -1,8 +1,9 @@ """ Unsatisfiable Core Utilities """ - +import time from typing import Optional, Set, Tuple +from itertools import chain, combinations import clingo @@ -92,6 +93,48 @@ def shrink(self, assumptions: Optional[AssumptionSet] = None) -> None: """ self.minimal = self._compute_single_minimal(assumptions=assumptions) + def get_multiple_minimal(self, max_mucs: Optional[int] = None): + """ + This function generates all minimal unsatisfiable cores of the provided assumption set. It implements the + generator pattern since finding all mucs of an assumption set is exponential in nature and the search might not + fully complete in reasonable time. The parameter `max_mucs` can be used to specify the maximum number of + mucs that are found before stopping the search. + """ + assumptions = self.assumption_set + assumption_powerset = chain.from_iterable( + combinations(assumptions, r) + for r in reversed(range(len(list(assumptions)) + 1)) + ) + + found_sat = [] + found_mucs = [] + + for current_subset in (set(s) for s in assumption_powerset): + # skip if empty subset + if len(current_subset) == 0: + continue + # skip if an already found muc is a subset + if any(set(muc).issubset(current_subset) for muc in found_mucs): + continue + # skip if an already found satisfiable subset is superset + if any(set(sat).issuperset(current_subset) for sat in found_sat): + continue + + muc = self._compute_single_minimal(assumptions=current_subset) + + # if the current subset wasn't unsatisfiable store this info and continue + if len(list(muc)) == 0: + found_sat.append(current_subset) + continue + + # if iterative deletion finds a muc that wasn't discovered before update sets and yield + if muc not in found_mucs: + found_mucs.append(muc) + yield muc + # if the maximum muc amount is found stop search + if max_mucs is not None and len(found_mucs) == max_mucs: + break + __all__ = [ CoreComputer.__name__, From 49f5d7f0d2bb70a8d919dd854d94c73601b6d1cc Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 15 Jan 2024 12:57:09 +0100 Subject: [PATCH 22/82] added FactTransformer to remove program facts --- src/clingexplaid/utils/transformer.py | 64 +++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/clingexplaid/utils/transformer.py b/src/clingexplaid/utils/transformer.py index ad9ae3b..3f6b1cc 100644 --- a/src/clingexplaid/utils/transformer.py +++ b/src/clingexplaid/utils/transformer.py @@ -11,6 +11,7 @@ from clingexplaid.utils import match_ast_symbolic_atom_signature RULE_ID_SIGNATURE = "_rule" +REMOVED_TOKEN = "__REMOVED__" class UntransformedException(Exception): @@ -191,6 +192,69 @@ def get_assumptions( } +class FactTransformer(_ast.Transformer): + """ + CLASS DOC COMMENT + """ + + def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None): + self.signatures = signatures if signatures is not None else set() + + def visit_Rule(self, node): # pylint: disable=C0103 + """ + Removes all facts from a program that match the given signatures (if none are given all facts are removed). + """ + if node.head.ast_type != _ast.ASTType.Literal: + return node + if node.body: + return node + has_matching_signature = any( + match_ast_symbolic_atom_signature(node.head.atom, (name, arity)) + for (name, arity) in self.signatures + ) + # if signatures are defined only transform facts that match them, else transform all facts + if self.signatures and not has_matching_signature: + return node + + return _ast.Rule( + location=node.location, + head=_ast.Function( + location=node.location, name=REMOVED_TOKEN, arguments=[], external=0 + ), + body=[], + ) + + @staticmethod + def post_transform(program_string: str) -> str: + # remove the transformed REMOVED_TOKENS from the resulting program string + rules = program_string.split("\n") + out = [] + for rule in rules: + if not rule.startswith(REMOVED_TOKEN): + out.append(rule) + return "\n".join(out) + + def parse_string(self, string: str) -> str: + """ + Function that applies the transformation to the `program_string` it's called with and returns the transformed + program string. + """ + out = [] + _ast.parse_string(string, lambda stm: out.append(str(self(stm)))) + return self.post_transform("\n".join(out)) + + def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: + """ + Parses the files and returns a string with the transformed program. + """ + out = [] + _ast.parse_files( + [str(p) for p in paths], + lambda stm: out.append(str(self(stm))), + ) + return self.post_transform("\n".join(out)) + + class ConstraintTransformer(_ast.Transformer): """ A Transformer that takes all constraint rules and adds an atom to their head to avoid deriving false through them. From 01a184b51689c9fcdec2b85858d3a8a9225275a5 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 15 Jan 2024 13:27:06 +0100 Subject: [PATCH 23/82] added util function to get model/string signatures --- src/clingexplaid/utils/__init__.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/clingexplaid/utils/__init__.py b/src/clingexplaid/utils/__init__.py index c804ea5..0da09bb 100644 --- a/src/clingexplaid/utils/__init__.py +++ b/src/clingexplaid/utils/__init__.py @@ -2,6 +2,7 @@ Utilities """ +import re from typing import Dict, Iterable, Set, Tuple, Union import clingo @@ -44,3 +45,32 @@ def get_solver_literal_lookup(control: clingo.Control) -> Dict[int, clingo.Symbo match_ast_symbolic_atom_signature.__name__, get_solver_literal_lookup.__name__, ] + + +def get_signatures_from_model_string(model_string: str) -> Dict[str, int]: + """ + This function returns a dictionary of the signatures/arities of all atoms of a model string. Model strings are of + the form: `"signature1(X1, ..., XN) ... signatureM(X1, ..., XK)"` + """ + signatures = {} + for atom_string in model_string.split(): + result = re.search(r"([^(]*)\(", atom_string) + if len(result.groups()) == 0: + continue + signature = result.group(1) + # calculate arity for the signature + arity = 0 + level = 0 + for c in atom_string.removeprefix(signature): + if c == "(": + level += 1 + elif c == ")": + level -= 1 + else: + if level == 1 and c == ",": + arity += 1 + # if arity is not 0 increase by 1 for the last remaining parameter that is not followed by a comma + if arity > 0: + arity += 1 + signatures[signature] = arity + return signatures From f603e15df7c13276b8756eb53bff88c6e0d9966c Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 15 Jan 2024 13:29:33 +0100 Subject: [PATCH 24/82] fixed skipping order in get_multiple_minimal --- src/clingexplaid/utils/muc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clingexplaid/utils/muc.py b/src/clingexplaid/utils/muc.py index 20ddc0b..bd9e1a4 100644 --- a/src/clingexplaid/utils/muc.py +++ b/src/clingexplaid/utils/muc.py @@ -113,12 +113,12 @@ def get_multiple_minimal(self, max_mucs: Optional[int] = None): # skip if empty subset if len(current_subset) == 0: continue - # skip if an already found muc is a subset - if any(set(muc).issubset(current_subset) for muc in found_mucs): - continue # skip if an already found satisfiable subset is superset if any(set(sat).issuperset(current_subset) for sat in found_sat): continue + # skip if an already found muc is a subset + if any(set(muc).issubset(current_subset) for muc in found_mucs): + continue muc = self._compute_single_minimal(assumptions=current_subset) From 9b5153bd35a82a542df1f4276fb6550fecb94e8d Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 15 Jan 2024 13:30:17 +0100 Subject: [PATCH 25/82] added functionality for combined --muc and --unsat-constraints --- src/clingexplaid/utils/cli.py | 64 +++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 4147b66..69c9aa4 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -1,13 +1,13 @@ import re from importlib.metadata import version -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Optional import clingo from clingo.application import Application, Flag from .muc import CoreComputer -from .transformer import AssumptionTransformer, ConstraintTransformer -from ..utils import get_solver_literal_lookup +from .transformer import AssumptionTransformer, ConstraintTransformer, FactTransformer +from ..utils import get_solver_literal_lookup, get_signatures_from_model_string from ..utils.logger import BACKGROUND_COLORS, COLORS @@ -109,7 +109,12 @@ def _print_muc(self, muc) -> None: print(f"{COLORS['BLUE']}{muc}{COLORS['NORMAL']}") self._muc_id += 1 - def _method_muc(self, control: clingo.Control, files: List[str]): + def _method_muc( + self, + control: clingo.Control, + files: List[str], + compute_unsat_constraints: bool = False, + ): program_transformed, at = self._apply_assumption_transformer( signatures=self._muc_assumption_signatures, files=files ) @@ -149,17 +154,35 @@ def _method_muc(self, control: clingo.Control, files: List[str]): muc_string = " ".join([str(literal_lookup[a]) for a in muc]) self._print_muc(muc_string) - def _print_unsat_constraints(self, unsat_constraints) -> None: - print(f"{BACKGROUND_COLORS['RED']} Unsat Constraints {COLORS['NORMAL']}") + if compute_unsat_constraints: + self._method_unsat_constraints( + control=clingo.Control(), + files=files, + assumption_string=muc_string, + output_prefix=f"{COLORS['RED']}├──{COLORS['NORMAL']}", + ) + + def _print_unsat_constraints( + self, unsat_constraints, prefix: Optional[str] = None + ) -> None: + if prefix is None: + prefix = "" + print( + f"{prefix}{BACKGROUND_COLORS['RED']} Unsat Constraints {COLORS['NORMAL']}" + ) for c in unsat_constraints: - print(f"{COLORS['RED']}{c}{COLORS['NORMAL']}") - - def _method_unsat_constraints(self, control: clingo.Control, files: List[str]): + print(f"{prefix}{COLORS['RED']}{c}{COLORS['NORMAL']}") + + def _method_unsat_constraints( + self, + control: clingo.Control, + files: List[str], + assumption_string: Optional[str] = None, + output_prefix: Optional[str] = None, + ): unsat_constraint_atom = "__unsat__" ct = ConstraintTransformer(unsat_constraint_atom, include_id=True) - print(files) - if not files: program_transformed = ct.parse_files("-") else: @@ -181,6 +204,16 @@ def _method_unsat_constraints(self, control: clingo.Control, files: List[str]): .strip() ) + if assumption_string is not None and len(assumption_string) > 0: + assumptions_signatures = set( + get_signatures_from_model_string(assumption_string).items() + ) + ft = FactTransformer(signatures=assumptions_signatures) + # first remove all facts from the programs matching the assumption signatures from the assumption_string + final_program = ft.parse_string(final_program) + # then add the assumed atoms as the only remaining facts + final_program += "\n" + ". ".join(assumption_string.split()) + "." + control.add("base", [], final_program) control.ground([("base", [])]) @@ -197,13 +230,13 @@ def _method_unsat_constraints(self, control: clingo.Control, files: List[str]): model_symbols_shown = model.symbols(shown=True) solve_handle.resume() model = solve_handle.model() - print(" ".join([str(s) for s in model_symbols_shown])) + # print(" ".join([str(s) for s in model_symbols_shown])) unsat_constraints = [] for a in unsat_constraint_atoms: constraint = constraint_lookup.get(a.arguments[0].number) unsat_constraints.append(constraint) - self._print_unsat_constraints(unsat_constraints) + self._print_unsat_constraints(unsat_constraints, prefix=output_prefix) def print_model(self, model, _): return @@ -225,7 +258,4 @@ def main(self, control, files): method_function(control, files) # special cases where specific pipelines have to be configured elif self.methods == {"muc", "unsat-constraints"}: - print( - "NOT IMPLEMENTED: first find muc -> apply unsat-constraints to find conflicting constraints for" - "specific MUC" - ) + self.method_functions["muc"](control, files, compute_unsat_constraints=True) From bccecd0912585aca6f47949c897728cded187162 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 15 Jan 2024 15:35:08 +0100 Subject: [PATCH 26/82] added API functionality for unsat-constraints --- src/clingexplaid/utils/cli.py | 66 ++---------- src/clingexplaid/utils/unsat_constraints.py | 109 ++++++++++++++++++++ 2 files changed, 117 insertions(+), 58 deletions(-) create mode 100644 src/clingexplaid/utils/unsat_constraints.py diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 69c9aa4..d2009fe 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -5,10 +5,11 @@ import clingo from clingo.application import Application, Flag +from .logger import BACKGROUND_COLORS, COLORS from .muc import CoreComputer from .transformer import AssumptionTransformer, ConstraintTransformer, FactTransformer +from .unsat_constraints import UnsatConstraintComputer from ..utils import get_solver_literal_lookup, get_signatures_from_model_string -from ..utils.logger import BACKGROUND_COLORS, COLORS class ClingoExplaidApp(Application): @@ -180,63 +181,12 @@ def _method_unsat_constraints( assumption_string: Optional[str] = None, output_prefix: Optional[str] = None, ): - unsat_constraint_atom = "__unsat__" - ct = ConstraintTransformer(unsat_constraint_atom, include_id=True) - - if not files: - program_transformed = ct.parse_files("-") - else: - program_transformed = ct.parse_files(files) - - minimizer_rule = f"#minimize {{1,X : {unsat_constraint_atom}(X)}}." - final_program = program_transformed + "\n" + minimizer_rule - - constraint_lookup = {} - for line in final_program.split("\n"): - id_re = re.compile(f"{unsat_constraint_atom}\(([1-9][0-9]*)\)") - match_result = id_re.match(line) - if match_result is None: - continue - constraint_id = match_result.group(1) - constraint_lookup[int(constraint_id)] = ( - str(line) - .replace(f"{unsat_constraint_atom}({constraint_id})", "") - .strip() - ) - - if assumption_string is not None and len(assumption_string) > 0: - assumptions_signatures = set( - get_signatures_from_model_string(assumption_string).items() - ) - ft = FactTransformer(signatures=assumptions_signatures) - # first remove all facts from the programs matching the assumption signatures from the assumption_string - final_program = ft.parse_string(final_program) - # then add the assumed atoms as the only remaining facts - final_program += "\n" + ". ".join(assumption_string.split()) + "." - - control.add("base", [], final_program) - control.ground([("base", [])]) - - with control.solve(yield_=True) as solve_handle: - model = solve_handle.model() - unsat_constraint_atoms = [] - model_symbols_shown = [] - while model is not None: - unsat_constraint_atoms = [ - a - for a in model.symbols(atoms=True) - if a.match(unsat_constraint_atom, 1, True) - ] - model_symbols_shown = model.symbols(shown=True) - solve_handle.resume() - model = solve_handle.model() - # print(" ".join([str(s) for s in model_symbols_shown])) - unsat_constraints = [] - for a in unsat_constraint_atoms: - constraint = constraint_lookup.get(a.arguments[0].number) - unsat_constraints.append(constraint) - - self._print_unsat_constraints(unsat_constraints, prefix=output_prefix) + ucc = UnsatConstraintComputer(control=control) + ucc.parse_files(files) + unsat_constraints = ucc.get_unsat_constraints( + assumption_string=assumption_string + ) + self._print_unsat_constraints(unsat_constraints, prefix=output_prefix) def print_model(self, model, _): return diff --git a/src/clingexplaid/utils/unsat_constraints.py b/src/clingexplaid/utils/unsat_constraints.py new file mode 100644 index 0000000..1a5bdb5 --- /dev/null +++ b/src/clingexplaid/utils/unsat_constraints.py @@ -0,0 +1,109 @@ +""" +Unsat Constraint Utilities +""" + +import re +from typing import List, Optional + +import clingo + +from .transformer import ConstraintTransformer, FactTransformer +from ..utils import get_signatures_from_model_string + + +UNSAT_CONSTRAINT_SIGNATURE = "__unsat__" + + +class UnsatConstraintComputer: + """ + A container class that allows for a passed unsatisfiable program_string to identify the underlying constraints + making it unsatisfiable + """ + + def __init__( + self, + control: Optional[clingo.Control] = None, + ): + self.control = control if control is not None else clingo.Control() + self.program_transformed = None + self.initialized = False + + def parse_string(self, program_string: str) -> None: + ct = ConstraintTransformer(UNSAT_CONSTRAINT_SIGNATURE, include_id=True) + self.program_transformed = ct.parse_string(program_string) + self.initialized = True + + def parse_files(self, files: List[str]) -> None: + ct = ConstraintTransformer(UNSAT_CONSTRAINT_SIGNATURE, include_id=True) + if not files: + program_transformed = ct.parse_files("-") + else: + program_transformed = ct.parse_files(files) + self.program_transformed = program_transformed + self.initialized = True + + def get_unsat_constraints( + self, assumption_string: Optional[str] = None + ) -> List[str]: + # only execute if the UnsatConstraintComputer was properly initialized + if not self.initialized: + raise ValueError( + "UnsatConstraintComputer is not properly initialized. To do so call either `parse_files` " + "or `parse_string`." + ) + + program_string = self.program_transformed + # if an assumption string is provided use a FactTransformer to remove interfering facts + if assumption_string is not None and len(assumption_string) > 0: + assumptions_signatures = set( + get_signatures_from_model_string(assumption_string).items() + ) + ft = FactTransformer(signatures=assumptions_signatures) + # first remove all facts from the programs matching the assumption signatures from the assumption_string + program_string = ft.parse_string(program_string) + # then add the assumed atoms as the only remaining facts + program_string += "\n" + ". ".join(assumption_string.split()) + "." + + # add minimization soft constraint to optimize for the smallest set of unsat constraints + minimizer_rule = f"#minimize {{1,X : {UNSAT_CONSTRAINT_SIGNATURE}(X)}}." + program_string = program_string + "\n" + minimizer_rule + + # create a rule lookup for every constraint in the program associated with it's unsat id + constraint_lookup = {} + for line in program_string.split("\n"): + id_re = re.compile(f"{UNSAT_CONSTRAINT_SIGNATURE}\(([1-9][0-9]*)\)") + match_result = id_re.match(line) + if match_result is None: + continue + constraint_id = match_result.group(1) + constraint_lookup[int(constraint_id)] = ( + str(line) + .replace(f"{UNSAT_CONSTRAINT_SIGNATURE}({constraint_id})", "") + .strip() + ) + + self.control.add("base", [], program_string) + self.control.ground([("base", [])]) + + with self.control.solve(yield_=True) as solve_handle: + model = solve_handle.model() + unsat_constraint_atoms = [] + while model is not None: + unsat_constraint_atoms = [ + a + for a in model.symbols(atoms=True) + if a.match(UNSAT_CONSTRAINT_SIGNATURE, 1, True) + ] + solve_handle.resume() + model = solve_handle.model() + unsat_constraints = [] + for a in unsat_constraint_atoms: + constraint = constraint_lookup.get(a.arguments[0].number) + unsat_constraints.append(constraint) + + return unsat_constraints + + +__all__ = [ + UnsatConstraintComputer.__name__, +] From 235ed104e13fbe4ee2bfebccdb8b7c6537c439fb Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 15 Jan 2024 15:38:11 +0100 Subject: [PATCH 27/82] removed old CLI --- src/clingexplaid/utils/cli_old.py | 236 ------------------------------ 1 file changed, 236 deletions(-) delete mode 100644 src/clingexplaid/utils/cli_old.py diff --git a/src/clingexplaid/utils/cli_old.py b/src/clingexplaid/utils/cli_old.py deleted file mode 100644 index 12846c1..0000000 --- a/src/clingexplaid/utils/cli_old.py +++ /dev/null @@ -1,236 +0,0 @@ -""" -Command Line Interface Utilities -""" - -import sys -from importlib.metadata import version -from pathlib import Path -from typing import Dict, List, Tuple - -import clingo -from clingo.application import Application -from clingox.reify import reify_program - -from clingexplaid.utils import get_solver_literal_lookup -from clingexplaid.utils.logger import BACKGROUND_COLORS, COLORS -from clingexplaid.utils.muc import CoreComputer -from clingexplaid.utils.transformer import AssumptionTransformer - - -class CoreComputerApp(Application): - """ - Application class realizing the CoreComputer functionality of the `clingexplaid.utils.muc.CoreComputer` class. - """ - - program_name: str = "core-computer" - version: str = "0.1" - - def __init__(self, name): - # pylint: disable = unused-argument - self.signatures = {} - self.method = 1 - self.muc_id = 1 - - def _parse_assumption_signature(self, input_string: str) -> bool: - # signature_strings = input_string.strip().split(",") - signature_list = input_string.split("/") - if len(signature_list) != 2: - print("Not valid format for signature, expected name/arity") - return False - self.signatures[signature_list[0]] = int(signature_list[1]) - return True - - def _parse_mode(self, input_string: str) -> bool: - try: - method_id = int(input_string.strip()) - except ValueError: - print("Not valid format for mode, expected 1 or 2") - return False - if method_id in (1, 2): - self.method = method_id - return True - return False - - def print_model(self, model, _): - return - - def register_options(self, options): - """ - See clingo.clingo_main(). - """ - - group = "MUC Options" - - options.add( - group, - "assumption-signature,a", - "Facts matching with this signature will be converted to assumptions for finding a MUC " - "(default: all facts)", - self._parse_assumption_signature, - multi=True, - ) - # options.add( - # group, - # "muc-method,m", - # "EXPERIMENTAL: This sets the method of finding the MUCs. (1) Iterative Deletion (2) ASP Meta Encoding [default]", - # self._parse_mode, - # ) - - def _apply_assumption_transformer( - self, signatures: Dict[str, int], files: List[str] - ) -> Tuple[str, AssumptionTransformer]: - signature_set = set(self.signatures.items()) if signatures else None - at = AssumptionTransformer(signatures=signature_set) - if not files: - program_transformed = at.parse_files("-") - print("Reading from -") - else: - program_transformed = at.parse_files(files) - print(f"Reading from {files[0]} {'...' if len(files) > 1 else ''}") - return program_transformed, at - - def _print_found_muc(self, model): - print("-" * 50) - print([str(s) for s in model.symbols(atoms=True) if "unsat" in str(s)]) - result = ".\n".join([str(a) for a in model.symbols(shown=True)]) - if result: - result += "." - print( - f"{BACKGROUND_COLORS['BLUE']} MUC: {self.muc_id} {COLORS['NORMAL']}{COLORS['DARK_BLUE']}{COLORS['NORMAL']}" - ) - print(f"{COLORS['BLUE']}{result}{COLORS['NORMAL']}") - self.muc_id += 1 - - def _find_single_muc(self, control: clingo.Control, files): - program_transformed, at = self._apply_assumption_transformer( - signatures=self.signatures, files=files - ) - - control.add("base", [], program_transformed) - control.ground([("base", [])]) - - literal_lookup = get_solver_literal_lookup(control) - - assumptions = at.get_assumptions(control) - - cc = CoreComputer(control, assumptions) - - print("Solving...") - control.solve(assumptions=list(assumptions), on_core=cc.shrink) - - if cc.minimal is None: - print("SATISFIABLE: Instance has no MUCs") - return - - result = " ".join([str(literal_lookup[a]) for a in cc.minimal]) - - muc_id = 1 - print( - f"{BACKGROUND_COLORS['BLUE']} MUC: {muc_id} {COLORS['NORMAL']}{COLORS['DARK_BLUE']}{COLORS['NORMAL']}" - ) - print(f"{COLORS['BLUE']}{result}{COLORS['NORMAL']}") - - def _find_multi_mucs_asp_approach(self, control: clingo.Control, files): - raise NotImplementedError( - "The meta encoding approach is faulty in its current state and thus not able to be used" - ) - - # print("ASP APPROACH") - # - # program_transformed, at = self._apply_assumption_transformer( - # signatures=self.signatures, files=files - # ) - # - # arguments = sys.argv[1:] - # constant_names = [] - # next_is_const = False - # for arg in arguments: - # if next_is_const: - # constant_name = arg.strip().split("=")[0] - # constant_names.append(constant_name) - # next_is_const = False - # if arg == "-c": - # next_is_const = True - # - # constants = {name: control.get_const(name) for name in constant_names} - # - # # First Grounding for getting the assumptions - # assumption_control = clingo.Control( - # [f"-c {k}={str(v)}" for k, v in constants.items()] - # ) - # assumption_control.add("base", [], program_transformed) - # assumption_control.ground([("base", [])]) - # - # literal_lookup = get_solver_literal_lookup(assumption_control) - # - # additional_rules = [] - # assumption_strings = set() - # - # assumption_signatures = set() - # for assumption_literal in at.get_assumptions( - # assumption_control, constants=constants - # ): - # assumption = literal_lookup[assumption_literal] - # print(assumption) - # assumption_signatures.add((assumption.name, len(assumption.arguments))) - # assumption_strings.add(str(assumption)) - # additional_rules.append(f"_assumption({str(assumption)}).") - # additional_rules.append(f"_muc({assumption}) :- {assumption}.") - # - # for signature, arity in assumption_signatures: - # additional_rules.append(f"#show {signature}/{arity}.") - # - # print("ADDITIONAL RULES:", additional_rules) - # - # final_program = "\n".join( - # ( - # # add constants like this because clingox reify doesn't support a custom control or other way to - # # provide constants. - # "\n".join([f"#const {k}={str(v)}." for k, v in constants.items()]), - # program_transformed, - # "#show _muc/1.", - # "#show _assumption/1.", - # "\n".join(additional_rules), - # ) - # ) - # - # print("-"*50) - # print(final_program) - # - # # Implicit Grounding for reification - # symbols = reify_program(final_program) - # reified_program = "\n".join([f"{str(s)}." for s in symbols]) - # - # with open( - # Path(__file__).resolve().parent.joinpath("logic_programs/asp_approach.lp"), - # "r", - # encoding="utf-8", - # ) as f: - # meta_encoding = f.read() - # - # # meta_encoding += "\n:- not " + ", not ".join([f"assumption_hold({a})" for a in assumption_strings]) + "." - # - # # print(meta_encoding) - # # print("-"*50) - # # print(reified_program) - # - # # Second Grounding to get MUCs with original control - # control.add("base", [], reified_program) - # control.add("base", [], meta_encoding) - # - # control.configuration.solve.enum_mode = "domRec" # type: ignore - # control.configuration.solver.heuristic = "Domain" # type: ignore - # - # control.ground([("base", [])]) - # - # control.solve(on_model=self._print_found_muc) - - def main(self, control, files): - print("clingexplaid", "version", version("clingexplaid")) - - self._find_single_muc(control, files) - - # if self.method == 1: - # self._find_single_muc(control, files) - # elif self.method == 2: - # self._find_multi_mucs_asp_approach(control, files) From 96ac7047127a6706068f95f33b94caa56aaeb5c5 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 15 Jan 2024 17:49:42 +0100 Subject: [PATCH 28/82] added DecisionOrderPropagator --- src/clingexplaid/utils/propagators.py | 118 ++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/clingexplaid/utils/propagators.py diff --git a/src/clingexplaid/utils/propagators.py b/src/clingexplaid/utils/propagators.py new file mode 100644 index 0000000..2e36ffb --- /dev/null +++ b/src/clingexplaid/utils/propagators.py @@ -0,0 +1,118 @@ +from typing import List, Optional, Tuple, Set + +import clingo + +from .logger import COLORS + +DecisionLevel = List[int] +DecisionLevelList = List[DecisionLevel] + +UNKNOWN_SYMBOL_TOKEN = "INTERNAL" + +INDENT_START = "├─" +INDENT_STEP = f"─{COLORS['GREY']}┼{COLORS['NORMAL']}──" +INDENT_END = f"─{COLORS['GREY']}┤{COLORS['NORMAL']} " + + +class DecisionOrderPropagator: + def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None): + self.slit_symbol_lookup = {} + self.signatures = signatures if signatures is not None else set() + + self.last_decisions = [] + self.last_entailments = {} + + def init(self, init): + for atom in init.symbolic_atoms: + program_literal = atom.literal + solver_literal = init.solver_literal(program_literal) + self.slit_symbol_lookup[solver_literal] = atom.symbol + + for atom in init.symbolic_atoms: + if len(self.signatures) > 0 and not any( + atom.match(name=s, arity=a) for s, a in self.signatures + ): + continue + query_program_literal = init.symbolic_atoms[atom.symbol].literal + query_solver_literal = init.solver_literal(query_program_literal) + init.add_watch(query_solver_literal) + init.add_watch(-query_solver_literal) + + def propagate(self, control, changes) -> None: + decisions, entailments = self.get_decisions(control.assignment) + + print_level = 0 + for d in decisions: + print_level += 1 + if d in self.last_decisions: + continue + decision_symbol = self.get_symbol(d) + decision_negative = d < 0 + + indent_string = INDENT_START + INDENT_STEP * (print_level - 1) + print( + f"{indent_string}[{['+', '-'][int(decision_negative)]}] {decision_symbol} [{d}]" + ) + entailment_list = entailments[d] if d in entailments else [] + for e in entailment_list: + if e == d: + continue + entailment_indent = ( + (INDENT_START + INDENT_STEP * (print_level - 2) + INDENT_END) + if print_level > 1 + else "│ " + ) + entailment_symbol = self.get_symbol(e) + entailment_negative = e < 0 + print( + f"{entailment_indent}{COLORS['GREY']}[{['+', '-'][int(entailment_negative)]}] " + f"{entailment_symbol} [{e}]{COLORS['NORMAL']}" + ) + + self.last_decisions = decisions + self.last_entailments = entailments + + def undo(self, thread_id: int, assignment, changes) -> None: + if len(self.last_decisions) < 1: + return + decision = self.last_decisions[-1] + decision_symbol = self.get_symbol(decision) + indent_string = INDENT_START + INDENT_STEP * (len(self.last_decisions) - 1) + print( + f"{indent_string}{COLORS['RED']}[✕] {decision_symbol} [{decision}]{COLORS['NORMAL']}" + ) + self.last_decisions = self.last_decisions[:-1] + + @staticmethod + def get_decisions(assignment): + level = 0 + decisions = [] + entailments = {} + try: + while True: + decision = assignment.decision(level) + decisions.append(decision) + + trail = assignment.trail + level_offset_start = trail.begin(level) + level_offset_end = trail.end(level) + level_offset_diff = level_offset_end - level_offset_start + if level_offset_diff > 1: + entailments[decision] = trail[ + (level_offset_start + 1) : level_offset_end + ] + level += 1 + except RuntimeError: + return decisions, entailments + + def get_symbol(self, literal) -> clingo.Symbol: + try: + if literal > 0: + symbol = self.slit_symbol_lookup[literal] + else: + # negate symbol + symbol = clingo.parse_term(str(self.slit_symbol_lookup[-literal])) + except KeyError: + # internal literals + symbol = UNKNOWN_SYMBOL_TOKEN + return symbol From 44db05fdfa4f6e550df7bc1ddebabb6a99ba7db4 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 15 Jan 2024 18:40:56 +0100 Subject: [PATCH 29/82] updated DecisionOrderPropagator to only print provided signatures --- src/clingexplaid/utils/propagators.py | 42 ++++++++++++++++++++------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/clingexplaid/utils/propagators.py b/src/clingexplaid/utils/propagators.py index 2e36ffb..f41768d 100644 --- a/src/clingexplaid/utils/propagators.py +++ b/src/clingexplaid/utils/propagators.py @@ -15,9 +15,12 @@ class DecisionOrderPropagator: - def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None): + def __init__( + self, signatures: Optional[Set[Tuple[str, int]]] = None, prefix: str = "" + ): self.slit_symbol_lookup = {} self.signatures = signatures if signatures is not None else set() + self.prefix = prefix self.last_decisions = [] self.last_entailments = {} @@ -47,12 +50,20 @@ def propagate(self, control, changes) -> None: if d in self.last_decisions: continue decision_symbol = self.get_symbol(d) + + # don't print decision if its signature is not matching the provided ones + skip_print = False + if len(self.signatures) > 0 and decision_symbol != UNKNOWN_SYMBOL_TOKEN: + if not any(decision_symbol.match(s, a) for s, a in self.signatures): + skip_print = True + decision_negative = d < 0 indent_string = INDENT_START + INDENT_STEP * (print_level - 1) - print( - f"{indent_string}[{['+', '-'][int(decision_negative)]}] {decision_symbol} [{d}]" - ) + if not skip_print: + print( + f"{self.prefix}{indent_string}[{['+', '-'][int(decision_negative)]}] {decision_symbol} [{d}]" + ) entailment_list = entailments[d] if d in entailments else [] for e in entailment_list: if e == d: @@ -64,10 +75,11 @@ def propagate(self, control, changes) -> None: ) entailment_symbol = self.get_symbol(e) entailment_negative = e < 0 - print( - f"{entailment_indent}{COLORS['GREY']}[{['+', '-'][int(entailment_negative)]}] " - f"{entailment_symbol} [{e}]{COLORS['NORMAL']}" - ) + if not skip_print: + print( + f"{self.prefix}{entailment_indent}{COLORS['GREY']}[{['+', '-'][int(entailment_negative)]}] " + f"{entailment_symbol} [{e}]{COLORS['NORMAL']}" + ) self.last_decisions = decisions self.last_entailments = entailments @@ -77,10 +89,18 @@ def undo(self, thread_id: int, assignment, changes) -> None: return decision = self.last_decisions[-1] decision_symbol = self.get_symbol(decision) + + # don't print decision undo if its signature is not matching the provided ones + skip_print = False + if len(self.signatures) > 0 and decision_symbol != UNKNOWN_SYMBOL_TOKEN: + if not any(decision_symbol.match(s, a) for s, a in self.signatures): + skip_print = True + indent_string = INDENT_START + INDENT_STEP * (len(self.last_decisions) - 1) - print( - f"{indent_string}{COLORS['RED']}[✕] {decision_symbol} [{decision}]{COLORS['NORMAL']}" - ) + if not skip_print: + print( + f"{self.prefix}{indent_string}{COLORS['RED']}[✕] {decision_symbol} [{decision}]{COLORS['NORMAL']}" + ) self.last_decisions = self.last_decisions[:-1] @staticmethod From 7aa83ecde068197694fc439c8f457f5681385430 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 15 Jan 2024 18:43:00 +0100 Subject: [PATCH 30/82] extended CLI with --show-decisions flag and fine-grained --decision-signature selection --- src/clingexplaid/utils/cli.py | 79 +++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index d2009fe..716648a 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -7,6 +7,7 @@ from .logger import BACKGROUND_COLORS, COLORS from .muc import CoreComputer +from .propagators import DecisionOrderPropagator from .transformer import AssumptionTransformer, ConstraintTransformer, FactTransformer from .unsat_constraints import UnsatConstraintComputer from ..utils import get_solver_literal_lookup, get_signatures_from_model_string @@ -30,6 +31,10 @@ def __init__(self, name): for m in self.CLINGEXPLAID_METHODS.keys() } self.method_flags = {m: Flag() for m in self.CLINGEXPLAID_METHODS.keys()} + self.flag_show_decisions = Flag() + + # SHOW DECISIONS + self._decision_signatures = {} # MUC self._muc_assumption_signatures = {} @@ -47,25 +52,47 @@ def _initialize(self) -> None: f"[{', '.join(['--' + str(m) for m in self.CLINGEXPLAID_METHODS.keys()])}]" ) + @staticmethod + def _parse_signature(signature_string: str) -> Tuple[str, int]: + match_result = re.match(r"^([a-zA-Z]+)/([1-9][0-9]*)$", signature_string) + if match_result is None: + raise ValueError("Wrong signature Format") + return match_result.group(1), int(match_result.group(2)) + def _parse_assumption_signature(self, assumption_signature: str) -> bool: - if "muc" in self.methods: + if not self.method_flags["muc"]: print( - "PARSE ERROR: The assumption signature option is only available for --mode=muc" + "PARSE ERROR: The assumption signature option is only available if the flag --muc is enabled" ) return False assumption_signature_string = assumption_signature.replace("=", "").strip() - match_result = re.match( - r"^([a-zA-Z]+)/([1-9][0-9]*)$", assumption_signature_string - ) - if match_result is None: + try: + signature, arity = self._parse_signature(assumption_signature_string) + except ValueError: print( "PARSE ERROR: Wrong signature format. The assumption signatures have to follow the format " "/" ) return False - self._muc_assumption_signatures[match_result.group(1)] = int( - match_result.group(2) - ) + self._muc_assumption_signatures[signature] = arity + return True + + def _parse_decision_signature(self, decision_signature: str) -> bool: + if not self.flag_show_decisions: + print( + "PARSE ERROR: The decision signature option is only available if the flag --show-decisions is enabled" + ) + return False + decision_signature_string = decision_signature.replace("=", "").strip() + try: + signature, arity = self._parse_signature(decision_signature_string) + except ValueError: + print( + "PARSE ERROR: Wrong signature format. The decision signatures have to follow the format " + "/" + ) + return False + self._decision_signatures[signature] = arity return True def register_options(self, options): @@ -90,6 +117,24 @@ def register_options(self, options): multi=True, ) + group = "General Options" + + options.add_flag( + group=group, + option="show-decisions", + description="Shows a visualization of the decisions made by the solver during the solving process", + target=self.flag_show_decisions, + ) + + options.add( + group, + "decision-signature", + "When --show-decisions is enabled, show only decisions matching with this signature " + "(default: show all decisions)", + self._parse_decision_signature, + multi=True, + ) + def _apply_assumption_transformer( self, signatures: Dict[str, int], files: List[str] ) -> Tuple[str, AssumptionTransformer]: @@ -160,7 +205,8 @@ def _method_muc( control=clingo.Control(), files=files, assumption_string=muc_string, - output_prefix=f"{COLORS['RED']}├──{COLORS['NORMAL']}", + output_prefix_active=f"{COLORS['RED']}├──{COLORS['NORMAL']}", + output_prefix_passive=f"{COLORS['RED']}│ {COLORS['NORMAL']}", ) def _print_unsat_constraints( @@ -179,14 +225,23 @@ def _method_unsat_constraints( control: clingo.Control, files: List[str], assumption_string: Optional[str] = None, - output_prefix: Optional[str] = None, + output_prefix_active: str = "", + output_prefix_passive: str = "", ): + # register DecisionOrderPropagator if flag is enabled + if self.flag_show_decisions: + decision_signatures = set(self._decision_signatures.items()) + dop = DecisionOrderPropagator( + signatures=decision_signatures, prefix=output_prefix_passive + ) + control.register_propagator(dop) + ucc = UnsatConstraintComputer(control=control) ucc.parse_files(files) unsat_constraints = ucc.get_unsat_constraints( assumption_string=assumption_string ) - self._print_unsat_constraints(unsat_constraints, prefix=output_prefix) + self._print_unsat_constraints(unsat_constraints, prefix=output_prefix_active) def print_model(self, model, _): return From f3c9dc65a7af9d5027cef7115051f23d3e34fe13 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 15 Jan 2024 18:45:33 +0100 Subject: [PATCH 31/82] updated README --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9eeca92..ae856e5 100644 --- a/README.md +++ b/README.md @@ -87,16 +87,17 @@ This blackens the source code whenever `git commit` is used. ## ToDos -+ [ ] New CLI structure ++ [x] New CLI structure + different modes: + MUC + UNSAT-CONSTRAINTS + can be enabled through flags -+ [ ] Iterative Deltion for Multiple MUCs - + variation of the QuickXplain algorithm -+ [ ] Finish unsat-constraints implementation for the API ++ [x] Iterative Deltion for Multiple MUCs + + variation of the QuickXplain algorithm : `SKIPPED` ++ [x] Finish unsat-constraints implementation for the API + [ ] Give a warning in Transformer if control is not grounded yet -+ [ ] New option to enable verbose derivation output ++ [x] New option to enable verbose derivation output + + `--show-decisions` with more fine grained `--decision-signature` option + [ ] Documentation + Proper README + Docstrings for all API functions From 16ce89d1ffcc60a87b07c05c7c51c1a58f921b5d Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Tue, 16 Jan 2024 19:43:01 +0100 Subject: [PATCH 32/82] updated README ToDos --- README.md | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ae856e5..c978e9d 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,10 @@ This blackens the source code whenever `git commit` is used. ## ToDos +### Important Features + +`2023` + + [x] New CLI structure + different modes: + MUC @@ -95,14 +99,31 @@ This blackens the source code whenever `git commit` is used. + [x] Iterative Deltion for Multiple MUCs + variation of the QuickXplain algorithm : `SKIPPED` + [x] Finish unsat-constraints implementation for the API -+ [ ] Give a warning in Transformer if control is not grounded yet + +`2024 - JAN` + + [x] New option to enable verbose derivation output + `--show-decisions` with more fine grained `--decision-signature` option ++ [ ] Give a warning in Transformer if control is not grounded yet + [ ] Documentation - + Proper README - + Docstrings for all API functions - + CLI documentation with examples - + Examples folder + + [ ] Proper README + + [ ] Docstrings for all API functions + + [ ] CLI documentation with examples + + [ ] Examples folder + + [ ] Sudoku + + [ ] Graph Coloring + + [ ] 1 More ... + ++ [ ] Make `--show-decisions` its own mode ++ [ ] In `--show-decisions` hide INTERNAL when `--decision-signature` is active + + ++ [ ] Features for `--unsat-constraints` + + [ ] Access comments in the same line as constraint + + [ ] File + Line (Clickable link) + +### Extra Features ++ [ ] Timeout ## Experimental Features From 1c5b84dc9b9d915f1e31cca24644bced78b8e567 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 22 Jan 2024 15:28:23 +0100 Subject: [PATCH 33/82] added --show-decisions as its own mode --- src/clingexplaid/utils/cli.py | 65 +++++++++++++++++++++++++------- src/clingexplaid/utils/logger.py | 4 ++ 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 716648a..c8db944 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -21,6 +21,7 @@ class ClingoExplaidApp(Application): CLINGEXPLAID_METHODS = { "muc": "Description for MUC method", "unsat-constraints": "Description for unsat-constraints method", + "show-decisions": "Visualize the decision process of clingo during solving", } def __init__(self, name): @@ -31,10 +32,10 @@ def __init__(self, name): for m in self.CLINGEXPLAID_METHODS.keys() } self.method_flags = {m: Flag() for m in self.CLINGEXPLAID_METHODS.keys()} - self.flag_show_decisions = Flag() # SHOW DECISIONS - self._decision_signatures = {} + self._show_decisions_decision_signatures = {} + self._show_decisions_model_id = 1 # MUC self._muc_assumption_signatures = {} @@ -78,7 +79,7 @@ def _parse_assumption_signature(self, assumption_signature: str) -> bool: return True def _parse_decision_signature(self, decision_signature: str) -> bool: - if not self.flag_show_decisions: + if not self.method_flags["show-decisions"]: print( "PARSE ERROR: The decision signature option is only available if the flag --show-decisions is enabled" ) @@ -92,7 +93,7 @@ def _parse_decision_signature(self, decision_signature: str) -> bool: "/" ) return False - self._decision_signatures[signature] = arity + self._show_decisions_decision_signatures[signature] = arity return True def register_options(self, options): @@ -117,14 +118,7 @@ def register_options(self, options): multi=True, ) - group = "General Options" - - options.add_flag( - group=group, - option="show-decisions", - description="Shows a visualization of the decisions made by the solver during the solving process", - target=self.flag_show_decisions, - ) + group = "Show Decisions Options" options.add( group, @@ -135,6 +129,8 @@ def register_options(self, options): multi=True, ) + # group = "General Options" + def _apply_assumption_transformer( self, signatures: Dict[str, int], files: List[str] ) -> Tuple[str, AssumptionTransformer]: @@ -229,8 +225,8 @@ def _method_unsat_constraints( output_prefix_passive: str = "", ): # register DecisionOrderPropagator if flag is enabled - if self.flag_show_decisions: - decision_signatures = set(self._decision_signatures.items()) + if self.method_flags["show-decisions"]: + decision_signatures = set(self._show_decisions_decision_signatures.items()) dop = DecisionOrderPropagator( signatures=decision_signatures, prefix=output_prefix_passive ) @@ -243,6 +239,38 @@ def _method_unsat_constraints( ) self._print_unsat_constraints(unsat_constraints, prefix=output_prefix_active) + def _print_model( + self, + model, + prefix_active: str = "", + prefix_passive: str = "", + ) -> None: + print(prefix_passive) + print( + f"{prefix_active}" + f"{BACKGROUND_COLORS['LIGHT-GREY']}{COLORS['BLACK']} Model {COLORS['NORMAL']}{BACKGROUND_COLORS['GREY']} " + f"{self._show_decisions_model_id} {COLORS['NORMAL']} " + f"{model}" + ) + # print(f"{COLORS['BLUE']}{model}{COLORS['NORMAL']}") + print(prefix_passive) + self._show_decisions_model_id += 1 + + def _method_show_decisions( + self, + control: clingo.Control, + files: List[str], + ): + decision_signatures = set(self._show_decisions_decision_signatures.items()) + dop = DecisionOrderPropagator(signatures=decision_signatures) + control.register_propagator(dop) + for f in files: + control.load(f) + if not files: + control.load("-") + control.ground() + control.solve(on_model=lambda model: self._print_model(model, "├", "│")) + def print_model(self, model, _): return @@ -264,3 +292,12 @@ def main(self, control, files): # special cases where specific pipelines have to be configured elif self.methods == {"muc", "unsat-constraints"}: self.method_functions["muc"](control, files, compute_unsat_constraints=True) + elif self.methods == {"muc", "unsat-constraints", "show-decisions"}: + self.method_functions["muc"](control, files, compute_unsat_constraints=True) + elif self.methods == {"unsat-constraints", "show-decisions"}: + self.method_functions["unsat-constraints"](control, files) + else: + print( + f"METHOD ERROR: the combination of the methods {[f'--{m}' for m in self.methods]} is invalid. " + f"Please remove the conflicting method flags" + ) diff --git a/src/clingexplaid/utils/logger.py b/src/clingexplaid/utils/logger.py index 83e8806..66e97eb 100644 --- a/src/clingexplaid/utils/logger.py +++ b/src/clingexplaid/utils/logger.py @@ -14,12 +14,16 @@ "RED": "\033[91m", "DARK_RED": "\033[91m", "NORMAL": "\033[0m", + "BLACK": "\033[30m", } BACKGROUND_COLORS = { "BLUE": "\033[44m", "LIGHT_BLUE": "\033[104m", "RED": "\033[41m", + "WHITE": "\033[107m", + "GREY": "\033[100m", + "LIGHT-GREY": "\033[47m", } From 89bf3c8cfa231b13552a31452e66ee28ca7d59ee Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 22 Jan 2024 15:29:46 +0100 Subject: [PATCH 34/82] updated README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index c978e9d..c8e6ec9 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ This blackens the source code whenever `git commit` is used. + [x] New option to enable verbose derivation output + `--show-decisions` with more fine grained `--decision-signature` option ++ [x] Make `--show-decisions` its own mode + [ ] Give a warning in Transformer if control is not grounded yet + [ ] Documentation + [ ] Proper README @@ -114,10 +115,8 @@ This blackens the source code whenever `git commit` is used. + [ ] Graph Coloring + [ ] 1 More ... -+ [ ] Make `--show-decisions` its own mode + [ ] In `--show-decisions` hide INTERNAL when `--decision-signature` is active - + [ ] Features for `--unsat-constraints` + [ ] Access comments in the same line as constraint + [ ] File + Line (Clickable link) From 3ed2dee97547d261586cbf23fe82743bf3463b96 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 22 Jan 2024 18:01:29 +0100 Subject: [PATCH 35/82] moved old examples to misc directory --- examples/{ => misc}/sudoku_4x4.lp | 0 examples/{ => misc}/sudoku_encoding.lp | 0 examples/{ => misc}/sudoku_encoding_2.lp | 0 examples/{ => misc}/test.lp | 0 examples/{ => misc}/test2.lp | 0 examples/{ => misc}/test3.lp | 0 examples/{ => misc}/test4.lp | 0 examples/{ => misc}/x.lp | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename examples/{ => misc}/sudoku_4x4.lp (100%) rename examples/{ => misc}/sudoku_encoding.lp (100%) rename examples/{ => misc}/sudoku_encoding_2.lp (100%) rename examples/{ => misc}/test.lp (100%) rename examples/{ => misc}/test2.lp (100%) rename examples/{ => misc}/test3.lp (100%) rename examples/{ => misc}/test4.lp (100%) rename examples/{ => misc}/x.lp (100%) diff --git a/examples/sudoku_4x4.lp b/examples/misc/sudoku_4x4.lp similarity index 100% rename from examples/sudoku_4x4.lp rename to examples/misc/sudoku_4x4.lp diff --git a/examples/sudoku_encoding.lp b/examples/misc/sudoku_encoding.lp similarity index 100% rename from examples/sudoku_encoding.lp rename to examples/misc/sudoku_encoding.lp diff --git a/examples/sudoku_encoding_2.lp b/examples/misc/sudoku_encoding_2.lp similarity index 100% rename from examples/sudoku_encoding_2.lp rename to examples/misc/sudoku_encoding_2.lp diff --git a/examples/test.lp b/examples/misc/test.lp similarity index 100% rename from examples/test.lp rename to examples/misc/test.lp diff --git a/examples/test2.lp b/examples/misc/test2.lp similarity index 100% rename from examples/test2.lp rename to examples/misc/test2.lp diff --git a/examples/test3.lp b/examples/misc/test3.lp similarity index 100% rename from examples/test3.lp rename to examples/misc/test3.lp diff --git a/examples/test4.lp b/examples/misc/test4.lp similarity index 100% rename from examples/test4.lp rename to examples/misc/test4.lp diff --git a/examples/x.lp b/examples/misc/x.lp similarity index 100% rename from examples/x.lp rename to examples/misc/x.lp From 003ccc3ca36efee686752a0e9d45dba5d145297a Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 22 Jan 2024 18:01:52 +0100 Subject: [PATCH 36/82] added proper sudoku example --- examples/sudoku/README.md | 60 ++++++++++++++++++++++++++++++ examples/sudoku/encoding.lp | 13 +++++++ examples/sudoku/instance.lp | 6 +++ examples/sudoku/sudoku_example.svg | 4 ++ 4 files changed, 83 insertions(+) create mode 100644 examples/sudoku/README.md create mode 100644 examples/sudoku/encoding.lp create mode 100644 examples/sudoku/instance.lp create mode 100644 examples/sudoku/sudoku_example.svg diff --git a/examples/sudoku/README.md b/examples/sudoku/README.md new file mode 100644 index 0000000..f3fba55 --- /dev/null +++ b/examples/sudoku/README.md @@ -0,0 +1,60 @@ +# Example : Sudoku + ++ This example is a 4 by 4 sudoku encoding which is given an unsatisfiable instance ++ Using `clingexplaid` we can discover the underlying Minimal Unsatisfiable Cores (MUCs) and their respective unsatisfiable constraints + +## Visualization + +![Sudoku 4x4 example with its associated MUCs](sudoku_example.svg) + +## Run + ++ Finding all MUCs: + + ```bash + clingexplaid 0 encoding.lp instance.lp --muc -a initial/3 + ``` + Expected Output: + + ```bash + MUC 1 + initial(4,1,4) initial(3,3,1) initial(3,4,3) + MUC 2 + initial(1,1,4) initial(1,4,2) initial(2,3,3) initial(3,3,1) + MUC 3 + initial(1,1,4) initial(4,1,4) + ``` + ++ Finding the unsatisfiable constraints + + ```bash + clingexplaid 0 encoding.lp instance.lp --unsat-constraints + ``` + Expected Output: + + ```bash + Unsat Constraints + :- solution(X1,Y,N); solution(X2,Y,N); X1 != X2. + ``` + ++ Combined call with unsatisfiable constraints for every found MUC + ```bash + clingexplaid 0 encoding.lp instance.lp --muc --unsat-constraints -a initial/3 + ``` + + Expected Output: + + ```bash + MUC 1 + initial(4,1,4) initial(3,3,1) initial(3,4,3) + ├── Unsat Constraints + ├──:- solution(X,Y1,N); solution(X,Y2,N); Y1 != Y2. + MUC 2 + initial(1,1,4) initial(1,4,2) initial(2,3,3) initial(3,3,1) + ├── Unsat Constraints + ├──:- solution(X,Y1,N); solution(X,Y2,N); Y1 != Y2. + MUC 3 + initial(1,1,4) initial(4,1,4) + ├── Unsat Constraints + ├──:- solution(X1,Y,N); solution(X2,Y,N); X1 != X2. + ``` \ No newline at end of file diff --git a/examples/sudoku/encoding.lp b/examples/sudoku/encoding.lp new file mode 100644 index 0000000..4a17289 --- /dev/null +++ b/examples/sudoku/encoding.lp @@ -0,0 +1,13 @@ +number(1..4). + +solution(X,Y,V) :- initial(X,Y,V). +{solution(X,Y,N): number(N)}=1 :- number(X) ,number(Y). +cage(X1,Y1,X2,Y2) :- solution(X1,Y1,_), solution(X2,Y2,_), + ((X1-1)/2)==((X2-1)/2), + ((Y1-1)/2)==((Y2-1)/2). + +:- solution(X,Y1,N), solution(X,Y2,N), Y1 != Y2. +:- solution(X1,Y,N), solution(X2,Y,N), X1 != X2. +:- cage(X1,Y1,X2,Y2), solution(X1,Y1,N), solution(X2,Y2,N), X1!=X2, Y1!=Y2. + +#show solution/3. diff --git a/examples/sudoku/instance.lp b/examples/sudoku/instance.lp new file mode 100644 index 0000000..1ef5e02 --- /dev/null +++ b/examples/sudoku/instance.lp @@ -0,0 +1,6 @@ +initial(1,1,4). +initial(4,1,4). +initial(1,4,2). +initial(2,3,3). +initial(3,3,1). +initial(3,4,3). diff --git a/examples/sudoku/sudoku_example.svg b/examples/sudoku/sudoku_example.svg new file mode 100644 index 0000000..2414a8a --- /dev/null +++ b/examples/sudoku/sudoku_example.svg @@ -0,0 +1,4 @@ + + + +
4
4
3
3
2
2
4
4
1
1
3
3
4
4
3
3
2
2
4
4
1
1
3
3
4
4
3
3
2
2
4
4
1
1
3
3
4
4
3
3
2
2
4
4
1
1
3
3
Original
Original
MUC 3
MUC 3
MUC 1
MUC 1
MUC 2
MUC 2
Text is not SVG - cannot display
\ No newline at end of file From 09700ba623cb5051ceec51b06b700cb40b4aa87b Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 22 Jan 2024 18:40:48 +0100 Subject: [PATCH 37/82] fixed sudoku example README spelling --- examples/sudoku/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/sudoku/README.md b/examples/sudoku/README.md index f3fba55..ddd6cf7 100644 --- a/examples/sudoku/README.md +++ b/examples/sudoku/README.md @@ -9,7 +9,7 @@ ## Run -+ Finding all MUCs: ++ Finding all MUCs ```bash clingexplaid 0 encoding.lp instance.lp --muc -a initial/3 From 4869cec37d73c8765d0b6446371545619e77204d Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 22 Jan 2024 18:41:08 +0100 Subject: [PATCH 38/82] added graph-coloring example --- examples/graph_coloring/README.md | 59 +++++++++++++++++++ examples/graph_coloring/encoding.lp | 5 ++ .../graph_coloring/graph_coloring_example.svg | 4 ++ examples/graph_coloring/instance.lp | 20 +++++++ 4 files changed, 88 insertions(+) create mode 100644 examples/graph_coloring/README.md create mode 100644 examples/graph_coloring/encoding.lp create mode 100644 examples/graph_coloring/graph_coloring_example.svg create mode 100644 examples/graph_coloring/instance.lp diff --git a/examples/graph_coloring/README.md b/examples/graph_coloring/README.md new file mode 100644 index 0000000..b1a21b4 --- /dev/null +++ b/examples/graph_coloring/README.md @@ -0,0 +1,59 @@ +# Example : Graph coloring + ++ This example encodes the classic graph coloring problem together with an unsatisfiable instance ++ Here for the sake of illustration some of the graph nodes are already assigned a color from the start, which is where the conflict occurs ++ Using `clingexplaid` we can discover the underlying Minimal Unsatisfiable Cores (MUCs) and their respective unsatisfiable constraints + + +## Visualization + +![](graph_coloring_example.svg) + +## Run + ++ Finding all MUCs + + ```bash + clingexplaid 0 encoding.lp instance.lp --muc -a assign/3 + ``` + + Expected Output: + + ```bash + MUC 1 + assign(1,green) assign(5,red) assign(7,green) + MUC 2 + assign(1,green) assign(2,blue) assign(5,red) + ``` + ++ Finding the unsatisfiable constraints + + ```bash + clingexplaid 0 encoding.lp instance.lp --unsat-constraints + ``` + + Expected Output: + + ```bash + Unsat Constraints + :- edge(N1,N2); assign(N1,C); assign(N2,C). + ``` + ++ Combined call with unsatisfiable constraints for every found MUC + ```bash + clingexplaid 0 encoding.lp instance.lp --muc --unsat-constraints -a assign/3 + ``` + + Expected Output: + + ```bash + MUC 1 + assign(1,green) assign(5,red) assign(7,green) + ├── Unsat Constraints + ├──:- edge(N1,N2); assign(N1,C); assign(N2,C). + MUC 2 + assign(1,green) assign(2,blue) assign(5,red) + ├── Unsat Constraints + ├──:- edge(N1,N2); assign(N1,C); assign(N2,C). + ``` + diff --git a/examples/graph_coloring/encoding.lp b/examples/graph_coloring/encoding.lp new file mode 100644 index 0000000..d8f947a --- /dev/null +++ b/examples/graph_coloring/encoding.lp @@ -0,0 +1,5 @@ +{assign(N, C): color(C)}=1 :- node(N). + +:- edge(N1, N2), assign(N1, C), assign(N2, C). + +#show assign/2. \ No newline at end of file diff --git a/examples/graph_coloring/graph_coloring_example.svg b/examples/graph_coloring/graph_coloring_example.svg new file mode 100644 index 0000000..23f9c81 --- /dev/null +++ b/examples/graph_coloring/graph_coloring_example.svg @@ -0,0 +1,4 @@ + + + +
1
1
3
3
5
5
4
4
2
2
6
6
9
9
8
8
7
7
1
1
3
3
5
5
4
4
2
2
6
6
9
9
8
8
7
7
Original
Original
MUC 1
MUC 1
1
1
3
3
5
5
4
4
2
2
6
6
9
9
8
8
7
7
MUC 2
MUC 2
Text is not SVG - cannot display
\ No newline at end of file diff --git a/examples/graph_coloring/instance.lp b/examples/graph_coloring/instance.lp new file mode 100644 index 0000000..35b5e8f --- /dev/null +++ b/examples/graph_coloring/instance.lp @@ -0,0 +1,20 @@ +node(1..9). + +edge(1,2). edge(1,3). edge(1,4). +edge(2,1). edge(2,3). +edge(3,1). edge(3,2). edge(3,5). +edge(4,1). edge(4,5). edge(4,8). +edge(5,3). edge(5,4). edge(5,6). edge(5,8). +edge(6,5). edge(6,9). +edge(7,8). +edge(8,7). edge(8,4). edge(8,5). edge(8,9). +edge(9,6). edge(9,8). + +color(red; green; blue). + +% pre assignments + +assign(1,green). +assign(2,blue). +assign(5,red). +assign(7,green). \ No newline at end of file From 56fd1677a831d36be63edc99c6f38459f1408b43 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 22 Jan 2024 19:53:20 +0100 Subject: [PATCH 39/82] updated README for graph coloring example --- examples/graph_coloring/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/graph_coloring/README.md b/examples/graph_coloring/README.md index b1a21b4..b2f3147 100644 --- a/examples/graph_coloring/README.md +++ b/examples/graph_coloring/README.md @@ -1,4 +1,4 @@ -# Example : Graph coloring +# Example : Graph Coloring + This example encodes the classic graph coloring problem together with an unsatisfiable instance + Here for the sake of illustration some of the graph nodes are already assigned a color from the start, which is where the conflict occurs From b08a7fcf03f5fc98265748f0bc8eab201f97090d Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 22 Jan 2024 19:53:55 +0100 Subject: [PATCH 40/82] added nqueens example --- examples/queens/README.md | 54 ++++++++++++++++++++++++++++++ examples/queens/encoding.lp | 9 +++++ examples/queens/instance.lp | 3 ++ examples/queens/queens_example.svg | 4 +++ 4 files changed, 70 insertions(+) create mode 100644 examples/queens/README.md create mode 100644 examples/queens/encoding.lp create mode 100644 examples/queens/instance.lp create mode 100644 examples/queens/queens_example.svg diff --git a/examples/queens/README.md b/examples/queens/README.md new file mode 100644 index 0000000..b162c44 --- /dev/null +++ b/examples/queens/README.md @@ -0,0 +1,54 @@ +# Example : N Queens Problem + ++ This example encodes the classic n queens problem ++ The correct problem encoding is provided with an unsatisfiable instance ++ Using `clingexplaid` we can discover the underlying Minimal Unsatisfiable Cores (MUCs) and their respective unsatisfiable constraints + + +## Visualization + +![](queens_example.svg) + +## Run + ++ Finding all MUCs + + ```bash + clingexplaid 0 encoding.lp instance.lp --muc -a queen/2 + ``` + + Expected Output: + + ```bash + MUC 1 + queen(1,1) queen(2,5) + ``` + ++ Finding the unsatisfiable constraints + + ```bash + clingexplaid 0 encoding.lp instance.lp --unsat-constraints + ``` + + Expected Output: + + ```bash + Unsat Constraints + :- 2 <= { queen((D-J),J) }; D = (2..(2*n)). + :- 2 <= { queen((D+J),J) }; D = ((1-n)..(n-1)). + ``` + ++ Combined call with unsatisfiable constraints for every found MUC + ```bash + clingexplaid 0 encoding.lp instance.lp --muc --unsat-constraints -a queen/2 + ``` + + Expected Output: + + ```bash + MUC 1 + queen(1,1) queen(2,5) + ├── Unsat Constraints + ├──:- 2 <= { queen((D-J),J) }; D = (2..(2*n)). + ``` + diff --git a/examples/queens/encoding.lp b/examples/queens/encoding.lp new file mode 100644 index 0000000..902106b --- /dev/null +++ b/examples/queens/encoding.lp @@ -0,0 +1,9 @@ +#const n=5. +number(1..n). +cell(X,Y) :- number(X), number(Y). + +1 { queen(X,Y): number(Y) } 1 :- number(X). +1 { queen(X,Y): number(X) } 1 :- number(Y). + +:- 2 { queen(D-J,J) }, D = 2..2*n. +:- 2 { queen(D+J,J) }, D = 1-n..n-1. \ No newline at end of file diff --git a/examples/queens/instance.lp b/examples/queens/instance.lp new file mode 100644 index 0000000..e8c874e --- /dev/null +++ b/examples/queens/instance.lp @@ -0,0 +1,3 @@ +queen(1,1). +queen(2,5). +queen(3,2). \ No newline at end of file diff --git a/examples/queens/queens_example.svg b/examples/queens/queens_example.svg new file mode 100644 index 0000000..b98e149 --- /dev/null +++ b/examples/queens/queens_example.svg @@ -0,0 +1,4 @@ + + + +
Original
Original
MUC 1
MUC 1
Text is not SVG - cannot display
\ No newline at end of file From 781faf8791e85a41698c50dff27f971d38cead07 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 22 Jan 2024 19:54:45 +0100 Subject: [PATCH 41/82] updated README --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c8e6ec9..dc99172 100644 --- a/README.md +++ b/README.md @@ -110,11 +110,16 @@ This blackens the source code whenever `git commit` is used. + [ ] Proper README + [ ] Docstrings for all API functions + [ ] CLI documentation with examples - + [ ] Examples folder - + [ ] Sudoku - + [ ] Graph Coloring - + [ ] 1 More ... - + + [x] Examples folder + + [x] Sudoku + + [x] Graph Coloring + + [x] N-Queens ++ Error when calling `--muc` constants aren't properly kept:: + + call: + + `clingexplaid examples/queens/encoding.lp examples/queens/instance.lp 0 --muc` + + error: + + `:4:9-13: info: interval undefined: 1..n` + + CRITICAL! + [ ] In `--show-decisions` hide INTERNAL when `--decision-signature` is active + [ ] Features for `--unsat-constraints` From 25825ca5546de4e65a7e92ae9723517a2034fae4 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 29 Jan 2024 17:51:43 +0100 Subject: [PATCH 42/82] added until function to retrieve constants from CL argument vector --- src/clingexplaid/utils/__init__.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/clingexplaid/utils/__init__.py b/src/clingexplaid/utils/__init__.py index 0da09bb..937a8ed 100644 --- a/src/clingexplaid/utils/__init__.py +++ b/src/clingexplaid/utils/__init__.py @@ -3,7 +3,7 @@ """ import re -from typing import Dict, Iterable, Set, Tuple, Union +from typing import Dict, Iterable, Set, Tuple, Union, List import clingo from clingo.ast import ASTType @@ -74,3 +74,19 @@ def get_signatures_from_model_string(model_string: str) -> Dict[str, int]: arity += 1 signatures[signature] = arity return signatures + + +def get_constants_from_arguments(argument_vector: List[str]) -> Dict: + constants = dict() + next_constant = False + for element in argument_vector: + if next_constant: + result = re.search(r"(.*)=(.*)", element) + if len(result.groups()) == 0: + continue + constants[result.group(1)] = result.group(2) + next_constant = False + if element in ("-c", "--const"): + next_constant = True + + return constants From 32ad3ee71f26501a46dd8df38faadd31bc7089e6 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 29 Jan 2024 17:52:30 +0100 Subject: [PATCH 43/82] fixed bug where assumption transformer dind't have access to constants --- src/clingexplaid/utils/cli.py | 12 ++++++++++-- src/clingexplaid/utils/transformer.py | 14 +++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index c8db944..2669713 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -1,4 +1,5 @@ import re +import sys from importlib.metadata import version from typing import Dict, List, Tuple, Optional @@ -10,7 +11,11 @@ from .propagators import DecisionOrderPropagator from .transformer import AssumptionTransformer, ConstraintTransformer, FactTransformer from .unsat_constraints import UnsatConstraintComputer -from ..utils import get_solver_literal_lookup, get_signatures_from_model_string +from ..utils import ( + get_solver_literal_lookup, + get_signatures_from_model_string, + get_constants_from_arguments, +) class ClingoExplaidApp(Application): @@ -32,6 +37,7 @@ def __init__(self, name): for m in self.CLINGEXPLAID_METHODS.keys() } self.method_flags = {m: Flag() for m in self.CLINGEXPLAID_METHODS.keys()} + self.argument_constants = dict() # SHOW DECISIONS self._show_decisions_decision_signatures = {} @@ -165,7 +171,7 @@ def _method_muc( control.ground([("base", [])]) literal_lookup = get_solver_literal_lookup(control) - assumptions = at.get_assumptions(control) + assumptions = at.get_assumptions(control, constants=self.argument_constants) cc = CoreComputer(control, assumptions) max_models = int(control.configuration.solve.models) @@ -284,6 +290,8 @@ def main(self, control, files): else: print(f"Reading from {files[0]} {'...' if len(files) > 1 else ''}") + self.argument_constants = get_constants_from_arguments(sys.argv) + # standard case: only one method if len(self.methods) == 1: method = list(self.methods)[0] diff --git a/src/clingexplaid/utils/transformer.py b/src/clingexplaid/utils/transformer.py index 3f6b1cc..d690002 100644 --- a/src/clingexplaid/utils/transformer.py +++ b/src/clingexplaid/utils/transformer.py @@ -104,6 +104,7 @@ def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None): self.signatures = signatures if signatures is not None else set() self.fact_rules: List[str] = [] self.transformed: bool = False + self.program_constants = {} def visit_Rule(self, node): # pylint: disable=C0103 """ @@ -134,6 +135,13 @@ def visit_Rule(self, node): # pylint: disable=C0103 body=[], ) + def visit_Definition(self, node): + """ + All defined constants of the program are stored in self.program_constants + """ + self.program_constants[node.name] = node.value.symbol + return node + def parse_string(self, string: str) -> str: """ Function that applies the transformation to the `program_string` it's called with and returns the transformed @@ -170,8 +178,12 @@ def get_assumptions( "The get_assumptions method cannot be called before a program has been " "transformed" ) + constants = constants if constants is not None else {} + + all_constants = dict(self.program_constants) + all_constants.update(constants) constant_strings = ( - [f"-c {k}={v}" for k, v in constants.items()] + [f"-c {k}={v}" for k, v in all_constants.items()] if constants is not None else [] ) From 532359e5f66a3627ec024fa7fdc8a29152ed976b Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 29 Jan 2024 17:52:43 +0100 Subject: [PATCH 44/82] updated README --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dc99172..0e67089 100644 --- a/README.md +++ b/README.md @@ -114,12 +114,11 @@ This blackens the source code whenever `git commit` is used. + [x] Sudoku + [x] Graph Coloring + [x] N-Queens -+ Error when calling `--muc` constants aren't properly kept:: - + call: - + `clingexplaid examples/queens/encoding.lp examples/queens/instance.lp 0 --muc` - + error: - + `:4:9-13: info: interval undefined: 1..n` - + CRITICAL! ++ [x] Error when calling `--muc` constants aren't properly kept: + + The problem was in `AssumptionTransformer` where get_assumptions didn't have proper access to constants defined over + the CL and the program constants ++ [ ] `AssumptionTransformer` doesn't work properly on included files + + IMPORTANT TO FIX! + [ ] In `--show-decisions` hide INTERNAL when `--decision-signature` is active + [ ] Features for `--unsat-constraints` From bfa98c20920f3327ebd548102a5e71867d1047d0 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 29 Jan 2024 17:59:24 +0100 Subject: [PATCH 45/82] updated README: bug didnt occur --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0e67089..10d916f 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,8 @@ This blackens the source code whenever `git commit` is used. + [x] Error when calling `--muc` constants aren't properly kept: + The problem was in `AssumptionTransformer` where get_assumptions didn't have proper access to constants defined over the CL and the program constants -+ [ ] `AssumptionTransformer` doesn't work properly on included files - + IMPORTANT TO FIX! ++ [x] `AssumptionTransformer` doesn't work properly on included files + + It actually did work fine + [ ] In `--show-decisions` hide INTERNAL when `--decision-signature` is active + [ ] Features for `--unsat-constraints` From 936b0b0ac9ba88036a13f8474d1a540c3e2b6c8f Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 29 Jan 2024 18:09:26 +0100 Subject: [PATCH 46/82] added except when AssumptionTransformer.get_assumptions is called without grounding first --- README.md | 2 +- src/clingexplaid/utils/transformer.py | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 10d916f..f7f13aa 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ This blackens the source code whenever `git commit` is used. + [x] New option to enable verbose derivation output + `--show-decisions` with more fine grained `--decision-signature` option + [x] Make `--show-decisions` its own mode -+ [ ] Give a warning in Transformer if control is not grounded yet ++ [x] Give a warning in Transformer if control is not grounded yet + [ ] Documentation + [ ] Proper README + [ ] Docstrings for all API functions diff --git a/src/clingexplaid/utils/transformer.py b/src/clingexplaid/utils/transformer.py index d690002..6824f88 100644 --- a/src/clingexplaid/utils/transformer.py +++ b/src/clingexplaid/utils/transformer.py @@ -20,6 +20,12 @@ class UntransformedException(Exception): """ +class NotGroundedException(Exception): + """Exception raised if the get_assumptions method of an AssumptionTransformer is called without the control object + having been grounded beforehand. + """ + + class RuleIDTransformer(_ast.Transformer): """ A Transformer that takes all the rules of a program and adds an atom with `self.rule_id_signature` in their bodys, @@ -170,14 +176,22 @@ def get_assumptions( Returns the assumptions which were gathered during the transformation of the program. Has to be called after a program has already been transformed. """ - # Just taking the fact symbolic atoms of the control given doesn't work here since we anticipate that - # this control is ground on the already transformed program. This means that all facts are now choice rules - # which means we cannot detect them like this anymore. + # Just taking the fact symbolic atoms of the control given doesn't work here since we anticipate that + # this control is ground on the already transformed program. This means that all facts are now choice rules + # which means we cannot detect them like this anymore. if not self.transformed: raise UntransformedException( "The get_assumptions method cannot be called before a program has been " "transformed" ) + # If the control has not been grounded yet except since without grounding we don't have access to the symbolic + # atoms. + if len(control.symbolic_atoms) == 0: + raise NotGroundedException( + "The get_assumptions method cannot be called before the control has been " + "grounded" + ) + constants = constants if constants is not None else {} all_constants = dict(self.program_constants) From 157df02677c61cfe34dd71f2f31c7243ac08f41b Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 29 Jan 2024 18:21:21 +0100 Subject: [PATCH 47/82] fixed --show-decisions print when signatures are selected --- README.md | 3 ++- src/clingexplaid/utils/propagators.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f7f13aa..c6b817f 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,8 @@ This blackens the source code whenever `git commit` is used. the CL and the program constants + [x] `AssumptionTransformer` doesn't work properly on included files + It actually did work fine -+ [ ] In `--show-decisions` hide INTERNAL when `--decision-signature` is active ++ [x] In `--show-decisions` hide INTERNAL when `--decision-signature` is active ++ [ ] cleanup `DecisionOrderPropagator` print functions + [ ] Features for `--unsat-constraints` + [ ] Access comments in the same line as constraint diff --git a/src/clingexplaid/utils/propagators.py b/src/clingexplaid/utils/propagators.py index f41768d..17fbc7f 100644 --- a/src/clingexplaid/utils/propagators.py +++ b/src/clingexplaid/utils/propagators.py @@ -53,6 +53,9 @@ def propagate(self, control, changes) -> None: # don't print decision if its signature is not matching the provided ones skip_print = False + # skip UNKNOWN print if signatures is set + if len(self.signatures) > 0 and decision_symbol == UNKNOWN_SYMBOL_TOKEN: + skip_print = True if len(self.signatures) > 0 and decision_symbol != UNKNOWN_SYMBOL_TOKEN: if not any(decision_symbol.match(s, a) for s, a in self.signatures): skip_print = True @@ -74,6 +77,20 @@ def propagate(self, control, changes) -> None: else "│ " ) entailment_symbol = self.get_symbol(e) + # skip UNKNOWN print in entailments if signatures is set + if ( + len(self.signatures) > 0 + and entailment_symbol == UNKNOWN_SYMBOL_TOKEN + ): + continue + if ( + len(self.signatures) > 0 + and entailment_symbol != UNKNOWN_SYMBOL_TOKEN + ): + if not any( + entailment_symbol.match(s, a) for s, a in self.signatures + ): + continue entailment_negative = e < 0 if not skip_print: print( From 0fe56465fd7a9daf374cdc0a18c7e165c7412fe2 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 29 Jan 2024 19:03:29 +0100 Subject: [PATCH 48/82] cleanup in print function --- README.md | 2 +- src/clingexplaid/utils/propagators.py | 80 ++++++++++++++------------- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index c6b817f..a695c04 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ This blackens the source code whenever `git commit` is used. + [x] `AssumptionTransformer` doesn't work properly on included files + It actually did work fine + [x] In `--show-decisions` hide INTERNAL when `--decision-signature` is active -+ [ ] cleanup `DecisionOrderPropagator` print functions ++ [x] cleanup `DecisionOrderPropagator` print functions + [ ] Features for `--unsat-constraints` + [ ] Access comments in the same line as constraint diff --git a/src/clingexplaid/utils/propagators.py b/src/clingexplaid/utils/propagators.py index 17fbc7f..2ebd3ab 100644 --- a/src/clingexplaid/utils/propagators.py +++ b/src/clingexplaid/utils/propagators.py @@ -41,6 +41,18 @@ def init(self, init): init.add_watch(query_solver_literal) init.add_watch(-query_solver_literal) + def _is_printed(self, symbol): + printed = True + # skip UNKNOWN print if signatures is set + if len(self.signatures) > 0 and symbol == UNKNOWN_SYMBOL_TOKEN: + printed = False + # skip if symbol signature is not in self.signatures + if len(self.signatures) > 0 and symbol != UNKNOWN_SYMBOL_TOKEN: + if not any(symbol.match(s, a) for s, a in self.signatures): + printed = False + + return printed + def propagate(self, control, changes) -> None: decisions, entailments = self.get_decisions(control.assignment) @@ -49,53 +61,46 @@ def propagate(self, control, changes) -> None: print_level += 1 if d in self.last_decisions: continue - decision_symbol = self.get_symbol(d) - - # don't print decision if its signature is not matching the provided ones - skip_print = False - # skip UNKNOWN print if signatures is set - if len(self.signatures) > 0 and decision_symbol == UNKNOWN_SYMBOL_TOKEN: - skip_print = True - if len(self.signatures) > 0 and decision_symbol != UNKNOWN_SYMBOL_TOKEN: - if not any(decision_symbol.match(s, a) for s, a in self.signatures): - skip_print = True + decision_symbol = self.get_symbol(d) + decision_printed = self._is_printed(decision_symbol) decision_negative = d < 0 - indent_string = INDENT_START + INDENT_STEP * (print_level - 1) - if not skip_print: + # build decision indent string + decision_indent_string = INDENT_START + INDENT_STEP * (print_level - 1) + # print decision if it matches the signatures (if provided) + if decision_printed: print( - f"{self.prefix}{indent_string}[{['+', '-'][int(decision_negative)]}] {decision_symbol} [{d}]" + f"{self.prefix}{decision_indent_string}" + f"[{['+', '-'][int(decision_negative)]}]" + f" {decision_symbol} " + f"[{d}]" ) + entailment_list = entailments[d] if d in entailments else [] + # build entailment indent string + entailment_indent_string = ( + (INDENT_START + INDENT_STEP * (print_level - 2) + INDENT_END) + if print_level > 1 + else "│ " + ) for e in entailment_list: + # skip decision in entailments if e == d: continue - entailment_indent = ( - (INDENT_START + INDENT_STEP * (print_level - 2) + INDENT_END) - if print_level > 1 - else "│ " - ) entailment_symbol = self.get_symbol(e) - # skip UNKNOWN print in entailments if signatures is set - if ( - len(self.signatures) > 0 - and entailment_symbol == UNKNOWN_SYMBOL_TOKEN - ): + entailment_printed = self._is_printed(entailment_symbol) + # skip if entailment symbol doesn't mach signatures (if provided) + if not entailment_printed: continue - if ( - len(self.signatures) > 0 - and entailment_symbol != UNKNOWN_SYMBOL_TOKEN - ): - if not any( - entailment_symbol.match(s, a) for s, a in self.signatures - ): - continue + entailment_negative = e < 0 - if not skip_print: + if decision_printed: print( - f"{self.prefix}{entailment_indent}{COLORS['GREY']}[{['+', '-'][int(entailment_negative)]}] " - f"{entailment_symbol} [{e}]{COLORS['NORMAL']}" + f"{self.prefix}{entailment_indent_string}{COLORS['GREY']}" + f"[{['+', '-'][int(entailment_negative)]}] " + f"{entailment_symbol} " + f"[{e}]{COLORS['NORMAL']}" ) self.last_decisions = decisions @@ -108,13 +113,10 @@ def undo(self, thread_id: int, assignment, changes) -> None: decision_symbol = self.get_symbol(decision) # don't print decision undo if its signature is not matching the provided ones - skip_print = False - if len(self.signatures) > 0 and decision_symbol != UNKNOWN_SYMBOL_TOKEN: - if not any(decision_symbol.match(s, a) for s, a in self.signatures): - skip_print = True + printed = self._is_printed(decision_symbol) indent_string = INDENT_START + INDENT_STEP * (len(self.last_decisions) - 1) - if not skip_print: + if printed: print( f"{self.prefix}{indent_string}{COLORS['RED']}[✕] {decision_symbol} [{decision}]{COLORS['NORMAL']}" ) From c3c6ca87502050f513b04a79e9d3ccb07976adb6 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 29 Jan 2024 21:11:56 +0100 Subject: [PATCH 49/82] added file and linenumber to unsat constraint output --- README.md | 9 +-- src/clingexplaid/utils/cli.py | 14 ++++- src/clingexplaid/utils/unsat_constraints.py | 64 ++++++++++++++++++++- 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a695c04..354e5cb 100644 --- a/README.md +++ b/README.md @@ -121,12 +121,13 @@ This blackens the source code whenever `git commit` is used. + It actually did work fine + [x] In `--show-decisions` hide INTERNAL when `--decision-signature` is active + [x] cleanup `DecisionOrderPropagator` print functions - -+ [ ] Features for `--unsat-constraints` - + [ ] Access comments in the same line as constraint - + [ ] File + Line (Clickable link) ++ [x] Features for `--unsat-constraints` + + [x] File + Line (Clickable link) ### Extra Features ++ [ ] `--unsat-constraints`: + + [ ] Access comments in the same line as constraint + + [ ] Currently, for multiline constraints a line number cannot be found + [ ] Timeout ## Experimental Features diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 2669713..1ff4284 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -212,7 +212,10 @@ def _method_muc( ) def _print_unsat_constraints( - self, unsat_constraints, prefix: Optional[str] = None + self, + unsat_constraints, + ucc: UnsatConstraintComputer, + prefix: Optional[str] = None, ) -> None: if prefix is None: prefix = "" @@ -220,7 +223,10 @@ def _print_unsat_constraints( f"{prefix}{BACKGROUND_COLORS['RED']} Unsat Constraints {COLORS['NORMAL']}" ) for c in unsat_constraints: - print(f"{prefix}{COLORS['RED']}{c}{COLORS['NORMAL']}") + file, line = ucc.get_constraint_location(c) + print( + f"{prefix}{COLORS['RED']}{c}{COLORS['GREY']} [file://{file}](Line {line}){COLORS['NORMAL']}" + ) def _method_unsat_constraints( self, @@ -243,7 +249,9 @@ def _method_unsat_constraints( unsat_constraints = ucc.get_unsat_constraints( assumption_string=assumption_string ) - self._print_unsat_constraints(unsat_constraints, prefix=output_prefix_active) + self._print_unsat_constraints( + unsat_constraints, ucc=ucc, prefix=output_prefix_active + ) def _print_model( self, diff --git a/src/clingexplaid/utils/unsat_constraints.py b/src/clingexplaid/utils/unsat_constraints.py index 1a5bdb5..3a2bade 100644 --- a/src/clingexplaid/utils/unsat_constraints.py +++ b/src/clingexplaid/utils/unsat_constraints.py @@ -3,7 +3,9 @@ """ import re -from typing import List, Optional +from difflib import SequenceMatcher +from pathlib import Path +from typing import List, Optional, Tuple import clingo @@ -28,6 +30,9 @@ def __init__( self.program_transformed = None self.initialized = False + self.included_files = set() + self.file_constraint_lookup = dict() + def parse_string(self, program_string: str) -> None: ct = ConstraintTransformer(UNSAT_CONSTRAINT_SIGNATURE, include_id=True) self.program_transformed = ct.parse_string(program_string) @@ -38,10 +43,67 @@ def parse_files(self, files: List[str]) -> None: if not files: program_transformed = ct.parse_files("-") else: + for file in files: + # add includes to included_files for every file + self._register_included_files(file) program_transformed = ct.parse_files(files) self.program_transformed = program_transformed self.initialized = True + def _register_included_files(self, file: str) -> None: + absolute_file_path = str(Path(file).absolute().resolve()) + # skip if file was already checked for includes + if absolute_file_path in self.included_files: + return + self.included_files.add(absolute_file_path) + + included_filenames = [] + with open(file, "r") as f: + result = re.search(r'#include "([^"]*)".', str(f.read())) + if result is not None: + included_filenames = list(result.groups()) + + for included_file in included_filenames: + # join original file relative path with included files -> absolute path + absolute_file_path = str( + Path(file).parent.joinpath(Path(included_file)).absolute().resolve() + ) + if absolute_file_path not in self.included_files: + self.included_files.add(absolute_file_path) + self._register_included_files(absolute_file_path) + + def _create_file_constraint_lookup(self) -> None: + for file in self.included_files: + with open(file, "r") as f: + rule_strings = [rule.strip() for rule in f.read().split(".")] + constraints = [rule for rule in rule_strings if rule.startswith(":-")] + self.file_constraint_lookup[file] = constraints + + def get_constraint_location(self, constraint_string: str) -> Tuple[str, int]: + self._create_file_constraint_lookup() + file_similarities = {f: 0.0 for f in self.included_files} + best_constraints = dict() + for file, constraints in self.file_constraint_lookup.items(): + for constraint in constraints: + string_similarity = SequenceMatcher( + None, constraint_string, constraint + ).ratio() + # update similarity dictionary to find file with the highest matching constraint + if string_similarity > file_similarities[file]: + file_similarities[file] = string_similarity + best_constraints[file] = constraint + # get file with the highest similarity + best_matching_file = max(file_similarities.items(), key=lambda x: x[1])[0] + # get the line number from the file + original_constraint = best_constraints[best_matching_file] + line_number = -1 + with open(best_matching_file, "r") as f: + for i, line in enumerate(f.readlines(), 1): + # this currently only works for non-multiline constraints! + if original_constraint in line: + line_number = i + return best_matching_file, line_number + def get_unsat_constraints( self, assumption_string: Optional[str] = None ) -> List[str]: From d0119a4da6647e9ff4386043c3f5dc4074e06cdc Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Wed, 31 Jan 2024 12:10:39 +0100 Subject: [PATCH 50/82] updated README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 354e5cb..2207e9b 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,8 @@ This blackens the source code whenever `git commit` is used. + [x] cleanup `DecisionOrderPropagator` print functions + [x] Features for `--unsat-constraints` + [x] File + Line (Clickable link) ++ [ ] File-Link test with space in filename + + with `urllib.parsequote` ### Extra Features + [ ] `--unsat-constraints`: From 651cb7bf146a3b7b7583bd52501f82d4ada90096 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 12 Feb 2024 18:08:09 +0100 Subject: [PATCH 51/82] Added OptimizationRemover This is used for the MUC finding since optimization is not necessary there --- README.md | 2 ++ src/clingexplaid/utils/cli.py | 11 +++++- src/clingexplaid/utils/transformer.py | 52 ++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 354e5cb..d72221d 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,8 @@ This blackens the source code whenever `git commit` is used. + [x] cleanup `DecisionOrderPropagator` print functions + [x] Features for `--unsat-constraints` + [x] File + Line (Clickable link) ++ [ ] Confusing Optimization prints during `--muc` when finding mucs in optimized Programs ++ [ ] Problem with `-a` between finding single MUC and multiple MUCs ### Extra Features + [ ] `--unsat-constraints`: diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 1ff4284..973a30e 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -9,7 +9,7 @@ from .logger import BACKGROUND_COLORS, COLORS from .muc import CoreComputer from .propagators import DecisionOrderPropagator -from .transformer import AssumptionTransformer, ConstraintTransformer, FactTransformer +from .transformer import AssumptionTransformer, OptimizationRemover from .unsat_constraints import UnsatConstraintComputer from ..utils import ( get_solver_literal_lookup, @@ -167,6 +167,10 @@ def _method_muc( signatures=self._muc_assumption_signatures, files=files ) + # remove optimization statements + optr = OptimizationRemover() + program_transformed = optr.parse_string(program_transformed) + control.add("base", [], program_transformed) control.ground([("base", [])]) @@ -184,6 +188,11 @@ def _method_muc( if cc.minimal is None: print("SATISFIABLE: Instance has no MUCs") return + if len(cc.minimal) == 0: + print( + "NO MUCS CONTAINED: The unsatisfiability of this program is not induced by the provided assumptions" + ) + return muc_string = " ".join([str(literal_lookup[a]) for a in cc.minimal]) self._print_muc(muc_string) diff --git a/src/clingexplaid/utils/transformer.py b/src/clingexplaid/utils/transformer.py index 6824f88..3e5c5dd 100644 --- a/src/clingexplaid/utils/transformer.py +++ b/src/clingexplaid/utils/transformer.py @@ -220,7 +220,7 @@ def get_assumptions( class FactTransformer(_ast.Transformer): """ - CLASS DOC COMMENT + Transformer that removes all facts from a program that match provided signatures """ def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None): @@ -281,6 +281,54 @@ def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: return self.post_transform("\n".join(out)) +class OptimizationRemover(_ast.Transformer): + """ + Transformer that removes all optimization statements + """ + + def visit_Minimize(self, node): # pylint: disable=C0103 + """ + Removes all facts from a program that match the given signatures (if none are given all facts are removed). + """ + return _ast.Rule( + location=node.location, + head=_ast.Function( + location=node.location, name=REMOVED_TOKEN, arguments=[], external=0 + ), + body=[], + ) + + @staticmethod + def post_transform(program_string: str) -> str: + # remove the transformed REMOVED_TOKENS from the resulting program string + rules = program_string.split("\n") + out = [] + for rule in rules: + if not rule.startswith(REMOVED_TOKEN): + out.append(rule) + return "\n".join(out) + + def parse_string(self, string: str) -> str: + """ + Function that applies the transformation to the `program_string` it's called with and returns the transformed + program string. + """ + out = [] + _ast.parse_string(string, lambda stm: out.append(str(self(stm)))) + return self.post_transform("\n".join(out)) + + def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: + """ + Parses the files and returns a string with the transformed program. + """ + out = [] + _ast.parse_files( + [str(p) for p in paths], + lambda stm: out.append(str(self(stm))), + ) + return self.post_transform("\n".join(out)) + + class ConstraintTransformer(_ast.Transformer): """ A Transformer that takes all constraint rules and adds an atom to their head to avoid deriving false through them. @@ -441,5 +489,7 @@ def parse_file(self, path: Union[str, Path], encoding: str = "utf-8") -> str: RuleIDTransformer.__name__, AssumptionTransformer.__name__, ConstraintTransformer.__name__, + FactTransformer.__name__, RuleSplitter.__name__, + OptimizationRemover.__name__, ] From b8accea8f5d50b071a81c8520bbcfb4c192fb2fb Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 12 Feb 2024 18:39:40 +0100 Subject: [PATCH 52/82] fixed space in filename problem --- README.md | 2 +- src/clingexplaid/utils/cli.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 065dea2..35ea0e1 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ This blackens the source code whenever `git commit` is used. + [x] File + Line (Clickable link) + [ ] Confusing Optimization prints during `--muc` when finding mucs in optimized Programs + [ ] Problem with `-a` between finding single MUC and multiple MUCs -+ [ ] File-Link test with space in filename ++ [x] File-Link test with space in filename + with `urllib.parsequote` ### Extra Features diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 973a30e..b5db18d 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -2,6 +2,7 @@ import sys from importlib.metadata import version from typing import Dict, List, Tuple, Optional +from urllib.parse import quote_plus import clingo from clingo.application import Application, Flag @@ -18,6 +19,9 @@ ) +HYPERLINK_MASK = "\033]8;{};{}\033\\{}\033]8;;\033\\" + + class ClingoExplaidApp(Application): """ Application class for executing clingo-explaid functionality on the command line @@ -233,8 +237,13 @@ def _print_unsat_constraints( ) for c in unsat_constraints: file, line = ucc.get_constraint_location(c) + file_link = "file://" + file + if " " in file: + # If there's a space in the filename use a hyperlink + file_link = HYPERLINK_MASK.format("", file_link, file_link) + print( - f"{prefix}{COLORS['RED']}{c}{COLORS['GREY']} [file://{file}](Line {line}){COLORS['NORMAL']}" + f"{prefix}{COLORS['RED']}{c}{COLORS['GREY']} [{file_link}](Line {line}){COLORS['NORMAL']}" ) def _method_unsat_constraints( From a48988dde8540ad30e54411cc56bd2fcc17553af Mon Sep 17 00:00:00 2001 From: Hannes Weichelt <69758320+hweichelt@users.noreply.github.com> Date: Mon, 12 Feb 2024 18:40:38 +0100 Subject: [PATCH 53/82] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 35ea0e1..2fc2863 100644 --- a/README.md +++ b/README.md @@ -123,8 +123,7 @@ This blackens the source code whenever `git commit` is used. + [x] cleanup `DecisionOrderPropagator` print functions + [x] Features for `--unsat-constraints` + [x] File + Line (Clickable link) -+ [ ] Confusing Optimization prints during `--muc` when finding mucs in optimized Programs -+ [ ] Problem with `-a` between finding single MUC and multiple MUCs ++ [x] Confusing Optimization prints during `--muc` when finding mucs in optimized Programs + [x] File-Link test with space in filename + with `urllib.parsequote` From bcddc8d82fc03ca5a0ee96146587abe1c388713f Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Mon, 12 Feb 2024 18:49:01 +0100 Subject: [PATCH 54/82] added NO MUCS CONTAINED message for multi MUC --- src/clingexplaid/utils/cli.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index b5db18d..c30c2d6 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -211,7 +211,9 @@ def _method_muc( program_unsat = True if program_unsat: + mucs = 0 for muc in cc.get_multiple_minimal(max_mucs=max_models): + mucs += 1 muc_string = " ".join([str(literal_lookup[a]) for a in muc]) self._print_muc(muc_string) @@ -223,6 +225,12 @@ def _method_muc( output_prefix_active=f"{COLORS['RED']}├──{COLORS['NORMAL']}", output_prefix_passive=f"{COLORS['RED']}│ {COLORS['NORMAL']}", ) + if not mucs: + print( + "NO MUCS CONTAINED: The unsatisfiability of this program is not induced by the provided " + "assumptions" + ) + return def _print_unsat_constraints( self, From 346552d890f0f9887c95f7e20b9b462de4377f26 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Tue, 13 Feb 2024 17:53:59 +0100 Subject: [PATCH 55/82] added spaces around clickable file link for MAC --- src/clingexplaid/utils/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index c30c2d6..5b1028d 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -251,7 +251,7 @@ def _print_unsat_constraints( file_link = HYPERLINK_MASK.format("", file_link, file_link) print( - f"{prefix}{COLORS['RED']}{c}{COLORS['GREY']} [{file_link}](Line {line}){COLORS['NORMAL']}" + f"{prefix}{COLORS['RED']}{c}{COLORS['GREY']} [ {file_link} ](Line {line}){COLORS['NORMAL']}" ) def _method_unsat_constraints( From bba0c423a01f9b54e8bdce07578fdb32c1edb8e3 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Tue, 13 Feb 2024 17:54:20 +0100 Subject: [PATCH 56/82] updated README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 2fc2863..6496c60 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,11 @@ This blackens the source code whenever `git commit` is used. + [x] Confusing Optimization prints during `--muc` when finding mucs in optimized Programs + [x] File-Link test with space in filename + with `urllib.parsequote` ++ [ ] Write up why negated assumptions in MUC's are a problem + + One which is currently not addressed by clingo-explaid ++ [ ] Remove minimization also from `--unsat-constaints` mode ++ [ ] Change file identification to use `clingo.ast.Location` instead of the subtring search and own built file tree ++ [x] Add spaces around Link to make it clickable on MAC ### Extra Features + [ ] `--unsat-constraints`: From 526d7776b32afec1303af5179bc7c4816d742e56 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Tue, 13 Feb 2024 19:32:27 +0100 Subject: [PATCH 57/82] added explaination to README for MUC problems with signatures --- README.md | 70 ++++++++++++++++++++++++++++++++------- examples/misc/bad_mucs.lp | 5 +++ 2 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 examples/misc/bad_mucs.lp diff --git a/README.md b/README.md index 6496c60..44e3d58 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,9 @@ This blackens the source code whenever `git commit` is used. the CL and the program constants + [x] `AssumptionTransformer` doesn't work properly on included files + It actually did work fine + +`2024 - FEB` + + [x] In `--show-decisions` hide INTERNAL when `--decision-signature` is active + [x] cleanup `DecisionOrderPropagator` print functions + [x] Features for `--unsat-constraints` @@ -131,6 +134,7 @@ This blackens the source code whenever `git commit` is used. + [ ] Remove minimization also from `--unsat-constaints` mode + [ ] Change file identification to use `clingo.ast.Location` instead of the subtring search and own built file tree + [x] Add spaces around Link to make it clickable on MAC ++ [ ] Add way for `-a` to allow for signatures without variables (`test/0`) ### Extra Features + [ ] `--unsat-constraints`: @@ -138,21 +142,10 @@ This blackens the source code whenever `git commit` is used. + [ ] Currently, for multiline constraints a line number cannot be found + [ ] Timeout -## Experimental Features +## Problems and Limitations ### Meta-encoding based approach (ASP-Approach) - - **Important Notes:** + The Meta-encoding approach as it stands is not fully functional @@ -169,6 +162,59 @@ approach. + This doesn't allow for properly checking if such subsets entail unsatisfiability and thus prevents us from finding the proper MUCs +### Specifying Assumption Set using only Signatures + +**Important Notes:** + ++ clingo-explaid provides the `--muc` mode which gives you Minimal Unsatisfiable Cores for a given set of assumption + signatures that can be defined with `-a` ++ These signatures though allow not always for finding the best fitting MUC for a given encoding, compared + to an assumption set generated by hand + +**Problem:** + ++ Imagine this [example encoding](examples/misc/bad_mucs.lp): + +```MATLAB +a(1..3). +:- a(X). + +unsat. +:- unsat. +``` + ++ So when I execute `clingexplaid examples/misc/bad_mucs.lp --muc 0` I get the MUCs: + +``` +MUC 1 +a(3) +MUC 2 +a(2) +MUC 3 +a(1) +MUC 4 +unsat +``` + ++ So you would generally expect that executing `clingexplaid examples/misc/bad_mucs.lp --muc 0 -a/1` would return the + first 3 found MUCs from before ++ But what actually happens is that there are no MUCs detected: + +``` +NO MUCS CONTAINED: The unsatisfiability of this program is not induced by the provided assumptions +UNSATISFIABLE +``` + ++ This is actually due to an implicit `-unsat` in the first 3 MUCs that isn't printed ++ Since the standard mode of `--muc` converts all facts to choices when no `-a` is provided `a(1)`, `a(2)`, `a(3)`, + and `unsat` are all converted to choices ++ We know that for the program to become satisfiable `unsat` cannot be true (line 4) ++ But since it is provided as a fact the choice rule conversion is necessary for the iterative deletion algorithm to + find any MUCs ++ This holds vice versa for the last MUC 4 just so that all `a/1` need to be converted to choice rules for the MUC to be + found + + [doc]: https://potassco.org/clingo/python-api/current/ [nox]: https://nox.thea.codes/en/stable/index.html [pipx]: https://pypa.github.io/pipx/ diff --git a/examples/misc/bad_mucs.lp b/examples/misc/bad_mucs.lp new file mode 100644 index 0000000..b371215 --- /dev/null +++ b/examples/misc/bad_mucs.lp @@ -0,0 +1,5 @@ +a(1..3). +:- a(X). + +unsat. +:- unsat. \ No newline at end of file From 04db65a9514d456de1dcb01df4ec151fa7d2ce57 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Tue, 13 Feb 2024 19:35:36 +0100 Subject: [PATCH 58/82] updated README: literal representation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 44e3d58..e0cfcbc 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ NO MUCS CONTAINED: The unsatisfiability of this program is not induced by the pr UNSATISFIABLE ``` -+ This is actually due to an implicit `-unsat` in the first 3 MUCs that isn't printed ++ This is actually due to an implicit `(unsat, False)` in the first 3 MUCs that isn't printed + Since the standard mode of `--muc` converts all facts to choices when no `-a` is provided `a(1)`, `a(2)`, `a(3)`, and `unsat` are all converted to choices + We know that for the program to become satisfiable `unsat` cannot be true (line 4) From 31728f0d19e827df87e2fd7a66682236e8b7e0f0 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Tue, 20 Feb 2024 19:33:04 +0100 Subject: [PATCH 59/82] improved file+line references for unsat-constraints --- src/clingexplaid/utils/cli.py | 31 ++++++--- src/clingexplaid/utils/transformer.py | 21 ++++-- src/clingexplaid/utils/unsat_constraints.py | 75 ++++----------------- 3 files changed, 48 insertions(+), 79 deletions(-) diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 5b1028d..9174ac5 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -1,8 +1,9 @@ import re import sys from importlib.metadata import version +from pathlib import Path from typing import Dict, List, Tuple, Optional -from urllib.parse import quote_plus + import clingo from clingo.application import Application, Flag @@ -234,7 +235,7 @@ def _method_muc( def _print_unsat_constraints( self, - unsat_constraints, + unsat_constraints: Dict[int, str], ucc: UnsatConstraintComputer, prefix: Optional[str] = None, ) -> None: @@ -243,16 +244,28 @@ def _print_unsat_constraints( print( f"{prefix}{BACKGROUND_COLORS['RED']} Unsat Constraints {COLORS['NORMAL']}" ) - for c in unsat_constraints: - file, line = ucc.get_constraint_location(c) - file_link = "file://" + file - if " " in file: + for cid, constraint in unsat_constraints.items(): + location = ucc.get_constraint_location(cid) + relative_file_path = location.begin.filename + absolute_file_path = str(Path(relative_file_path).absolute().resolve()) + line_beginning = location.begin.line + line_end = location.end.line + line_string = ( + f"Line {line_beginning}" + if line_beginning == line_end + else f"Lines {line_beginning}-{line_end}" + ) + file_link = "file://" + absolute_file_path + if " " in absolute_file_path: # If there's a space in the filename use a hyperlink file_link = HYPERLINK_MASK.format("", file_link, file_link) - print( - f"{prefix}{COLORS['RED']}{c}{COLORS['GREY']} [ {file_link} ](Line {line}){COLORS['NORMAL']}" - ) + if location is not None: + print( + f"{prefix}{COLORS['RED']}{constraint}{COLORS['GREY']} [ {file_link} ]({line_string}){COLORS['NORMAL']}" + ) + else: + print(f"{prefix}{COLORS['RED']}{constraint}{COLORS['NORMAL']}") def _method_unsat_constraints( self, diff --git a/src/clingexplaid/utils/transformer.py b/src/clingexplaid/utils/transformer.py index 3e5c5dd..4d5fbca 100644 --- a/src/clingexplaid/utils/transformer.py +++ b/src/clingexplaid/utils/transformer.py @@ -335,9 +335,11 @@ class ConstraintTransformer(_ast.Transformer): """ def __init__(self, constraint_head_symbol: str, include_id: bool = False): - self.constraint_head_symbol = constraint_head_symbol - self.include_id = include_id - self.constraint_id = 1 + self._constraint_head_symbol = constraint_head_symbol + self._include_id = include_id + self._constraint_id = 1 + + self.constraint_location_lookup = {} def visit_Rule(self, node): # pylint: disable=C0103 """ @@ -351,20 +353,25 @@ def visit_Rule(self, node): # pylint: disable=C0103 return node arguments = [] - if self.include_id: + if self._include_id: arguments = [ _ast.SymbolicTerm( - node.location, clingo.parse_term(str(self.constraint_id)) + node.location, clingo.parse_term(str(self._constraint_id)) ) ] head_symbol = _ast.Function( location=node.location, - name=self.constraint_head_symbol, + name=self._constraint_head_symbol, arguments=arguments, external=0, ) - self.constraint_id += 1 + + # add constraint location to lookup indexed by the constraint id + self.constraint_location_lookup[self._constraint_id] = node.location + + # increase constraint id + self._constraint_id += 1 # insert id symbol into body of rule node.head = head_symbol diff --git a/src/clingexplaid/utils/unsat_constraints.py b/src/clingexplaid/utils/unsat_constraints.py index 3a2bade..d06db14 100644 --- a/src/clingexplaid/utils/unsat_constraints.py +++ b/src/clingexplaid/utils/unsat_constraints.py @@ -5,9 +5,10 @@ import re from difflib import SequenceMatcher from pathlib import Path -from typing import List, Optional, Tuple +from typing import List, Optional, Dict import clingo +from clingo.ast import Location from .transformer import ConstraintTransformer, FactTransformer from ..utils import get_signatures_from_model_string @@ -30,12 +31,12 @@ def __init__( self.program_transformed = None self.initialized = False - self.included_files = set() - self.file_constraint_lookup = dict() + self._file_constraint_lookup = dict() def parse_string(self, program_string: str) -> None: ct = ConstraintTransformer(UNSAT_CONSTRAINT_SIGNATURE, include_id=True) self.program_transformed = ct.parse_string(program_string) + self._file_constraint_lookup = ct.constraint_location_lookup self.initialized = True def parse_files(self, files: List[str]) -> None: @@ -43,70 +44,17 @@ def parse_files(self, files: List[str]) -> None: if not files: program_transformed = ct.parse_files("-") else: - for file in files: - # add includes to included_files for every file - self._register_included_files(file) program_transformed = ct.parse_files(files) self.program_transformed = program_transformed + self._file_constraint_lookup = ct.constraint_location_lookup self.initialized = True - def _register_included_files(self, file: str) -> None: - absolute_file_path = str(Path(file).absolute().resolve()) - # skip if file was already checked for includes - if absolute_file_path in self.included_files: - return - self.included_files.add(absolute_file_path) - - included_filenames = [] - with open(file, "r") as f: - result = re.search(r'#include "([^"]*)".', str(f.read())) - if result is not None: - included_filenames = list(result.groups()) - - for included_file in included_filenames: - # join original file relative path with included files -> absolute path - absolute_file_path = str( - Path(file).parent.joinpath(Path(included_file)).absolute().resolve() - ) - if absolute_file_path not in self.included_files: - self.included_files.add(absolute_file_path) - self._register_included_files(absolute_file_path) - - def _create_file_constraint_lookup(self) -> None: - for file in self.included_files: - with open(file, "r") as f: - rule_strings = [rule.strip() for rule in f.read().split(".")] - constraints = [rule for rule in rule_strings if rule.startswith(":-")] - self.file_constraint_lookup[file] = constraints - - def get_constraint_location(self, constraint_string: str) -> Tuple[str, int]: - self._create_file_constraint_lookup() - file_similarities = {f: 0.0 for f in self.included_files} - best_constraints = dict() - for file, constraints in self.file_constraint_lookup.items(): - for constraint in constraints: - string_similarity = SequenceMatcher( - None, constraint_string, constraint - ).ratio() - # update similarity dictionary to find file with the highest matching constraint - if string_similarity > file_similarities[file]: - file_similarities[file] = string_similarity - best_constraints[file] = constraint - # get file with the highest similarity - best_matching_file = max(file_similarities.items(), key=lambda x: x[1])[0] - # get the line number from the file - original_constraint = best_constraints[best_matching_file] - line_number = -1 - with open(best_matching_file, "r") as f: - for i, line in enumerate(f.readlines(), 1): - # this currently only works for non-multiline constraints! - if original_constraint in line: - line_number = i - return best_matching_file, line_number + def get_constraint_location(self, constraint_id: int) -> Optional[Location]: + return self._file_constraint_lookup.get(constraint_id) def get_unsat_constraints( self, assumption_string: Optional[str] = None - ) -> List[str]: + ) -> Dict[int, str]: # only execute if the UnsatConstraintComputer was properly initialized if not self.initialized: raise ValueError( @@ -158,10 +106,11 @@ def get_unsat_constraints( ] solve_handle.resume() model = solve_handle.model() - unsat_constraints = [] + unsat_constraints = {} for a in unsat_constraint_atoms: - constraint = constraint_lookup.get(a.arguments[0].number) - unsat_constraints.append(constraint) + constraint_id = a.arguments[0].number + constraint = constraint_lookup.get(constraint_id) + unsat_constraints[constraint_id] = constraint return unsat_constraints From 37721a0c3fb144c589416b67198f367613d927e8 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt <69758320+hweichelt@users.noreply.github.com> Date: Tue, 20 Feb 2024 19:34:01 +0100 Subject: [PATCH 60/82] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e0cfcbc..846ec3d 100644 --- a/README.md +++ b/README.md @@ -129,10 +129,10 @@ This blackens the source code whenever `git commit` is used. + [x] Confusing Optimization prints during `--muc` when finding mucs in optimized Programs + [x] File-Link test with space in filename + with `urllib.parsequote` -+ [ ] Write up why negated assumptions in MUC's are a problem ++ [x] Write up why negated assumptions in MUC's are a problem + One which is currently not addressed by clingo-explaid + [ ] Remove minimization also from `--unsat-constaints` mode -+ [ ] Change file identification to use `clingo.ast.Location` instead of the subtring search and own built file tree ++ [x] Change file identification to use `clingo.ast.Location` instead of the subtring search and own built file tree + [x] Add spaces around Link to make it clickable on MAC + [ ] Add way for `-a` to allow for signatures without variables (`test/0`) From 5068ae9e33bdc8e2aae211b0feefd033cd487831 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Tue, 20 Feb 2024 19:42:27 +0100 Subject: [PATCH 61/82] added removal of optimization statements from input encodings for unsat-constraints --- src/clingexplaid/utils/unsat_constraints.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/clingexplaid/utils/unsat_constraints.py b/src/clingexplaid/utils/unsat_constraints.py index d06db14..71168dd 100644 --- a/src/clingexplaid/utils/unsat_constraints.py +++ b/src/clingexplaid/utils/unsat_constraints.py @@ -10,7 +10,7 @@ import clingo from clingo.ast import Location -from .transformer import ConstraintTransformer, FactTransformer +from .transformer import ConstraintTransformer, FactTransformer, OptimizationRemover from ..utils import get_signatures_from_model_string @@ -45,6 +45,11 @@ def parse_files(self, files: List[str]) -> None: program_transformed = ct.parse_files("-") else: program_transformed = ct.parse_files(files) + + # remove optimization statements + optr = OptimizationRemover() + program_transformed = optr.parse_string(program_transformed) + self.program_transformed = program_transformed self._file_constraint_lookup = ct.constraint_location_lookup self.initialized = True From 65ebeabf2ccf2dfc1d063b4ec31ec5e0b4003700 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Tue, 20 Feb 2024 19:43:24 +0100 Subject: [PATCH 62/82] Updated README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 846ec3d..e7f0724 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ This blackens the source code whenever `git commit` is used. + with `urllib.parsequote` + [x] Write up why negated assumptions in MUC's are a problem + One which is currently not addressed by clingo-explaid -+ [ ] Remove minimization also from `--unsat-constaints` mode ++ [x] Remove minimization also from `--unsat-constaints` mode + [x] Change file identification to use `clingo.ast.Location` instead of the subtring search and own built file tree + [x] Add spaces around Link to make it clickable on MAC + [ ] Add way for `-a` to allow for signatures without variables (`test/0`) From 446e601df2c71b3ad8bc07bc510961faf1671371 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Sun, 3 Mar 2024 17:35:19 +0100 Subject: [PATCH 63/82] extended signatures in CLI to ones with zero arity --- README.md | 5 ++++- examples/misc/zero_arity_assumptions.lp | 5 +++++ src/clingexplaid/utils/cli.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 examples/misc/zero_arity_assumptions.lp diff --git a/README.md b/README.md index e7f0724..0b8eff9 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,10 @@ This blackens the source code whenever `git commit` is used. + [x] Remove minimization also from `--unsat-constaints` mode + [x] Change file identification to use `clingo.ast.Location` instead of the subtring search and own built file tree + [x] Add spaces around Link to make it clickable on MAC -+ [ ] Add way for `-a` to allow for signatures without variables (`test/0`) + +`2024 - MAR` + ++ [x] Add way for `-a` to allow for signatures without variables (`test/0`) ### Extra Features + [ ] `--unsat-constraints`: diff --git a/examples/misc/zero_arity_assumptions.lp b/examples/misc/zero_arity_assumptions.lp new file mode 100644 index 0000000..e009fe7 --- /dev/null +++ b/examples/misc/zero_arity_assumptions.lp @@ -0,0 +1,5 @@ +test. + +not_test :- not test. + +:- not not_test. \ No newline at end of file diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 9174ac5..2346076 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -66,7 +66,7 @@ def _initialize(self) -> None: @staticmethod def _parse_signature(signature_string: str) -> Tuple[str, int]: - match_result = re.match(r"^([a-zA-Z]+)/([1-9][0-9]*)$", signature_string) + match_result = re.match(r"^([a-zA-Z]+)/([0-9]+)$", signature_string) if match_result is None: raise ValueError("Wrong signature Format") return match_result.group(1), int(match_result.group(2)) From add933e2c54abcb2bf36121c5f1da2acb7636c4c Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Sun, 3 Mar 2024 18:08:28 +0100 Subject: [PATCH 64/82] fixed errors in get_signatures_from_model_string function + tests --- src/clingexplaid/utils/__init__.py | 12 +++++++----- src/clingexplaid/utils/unsat_constraints.py | 4 +--- tests/clingexplaid/test_main.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/clingexplaid/utils/__init__.py b/src/clingexplaid/utils/__init__.py index 937a8ed..88655a6 100644 --- a/src/clingexplaid/utils/__init__.py +++ b/src/clingexplaid/utils/__init__.py @@ -47,15 +47,16 @@ def get_solver_literal_lookup(control: clingo.Control) -> Dict[int, clingo.Symbo ] -def get_signatures_from_model_string(model_string: str) -> Dict[str, int]: +def get_signatures_from_model_string(model_string: str) -> Set[Tuple[str, int]]: """ This function returns a dictionary of the signatures/arities of all atoms of a model string. Model strings are of the form: `"signature1(X1, ..., XN) ... signatureM(X1, ..., XK)"` """ - signatures = {} + signatures = set() for atom_string in model_string.split(): result = re.search(r"([^(]*)\(", atom_string) - if len(result.groups()) == 0: + if result is None: + signatures.add((atom_string, 0)) continue signature = result.group(1) # calculate arity for the signature @@ -70,9 +71,10 @@ def get_signatures_from_model_string(model_string: str) -> Dict[str, int]: if level == 1 and c == ",": arity += 1 # if arity is not 0 increase by 1 for the last remaining parameter that is not followed by a comma - if arity > 0: + # also increase arity by one for the case that there's only one parameter and no commas are contained just '(' + if arity > 0 or "(" in atom_string: arity += 1 - signatures[signature] = arity + signatures.add((signature, arity)) return signatures diff --git a/src/clingexplaid/utils/unsat_constraints.py b/src/clingexplaid/utils/unsat_constraints.py index 71168dd..aab03e2 100644 --- a/src/clingexplaid/utils/unsat_constraints.py +++ b/src/clingexplaid/utils/unsat_constraints.py @@ -70,9 +70,7 @@ def get_unsat_constraints( program_string = self.program_transformed # if an assumption string is provided use a FactTransformer to remove interfering facts if assumption_string is not None and len(assumption_string) > 0: - assumptions_signatures = set( - get_signatures_from_model_string(assumption_string).items() - ) + assumptions_signatures = get_signatures_from_model_string(assumption_string) ft = FactTransformer(signatures=assumptions_signatures) # first remove all facts from the programs matching the assumption signatures from the assumption_string program_string = ft.parse_string(program_string) diff --git a/tests/clingexplaid/test_main.py b/tests/clingexplaid/test_main.py index 1998d28..3dd3fac 100644 --- a/tests/clingexplaid/test_main.py +++ b/tests/clingexplaid/test_main.py @@ -155,7 +155,7 @@ def test_constraint_transformer(self): "res/transformed_program_constraints.lp" ) ct = ConstraintTransformer(constraint_head_symbol="unsat") - result = ct.parse_file(program_path) + result = ct.parse_files([program_path]) self.assertEqual( result.strip(), self.read_file(program_path_transformed).strip() ) From c1ff07894a119bf1b0b02023a1aa322e284cdcf1 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Tue, 5 Mar 2024 16:27:30 +0100 Subject: [PATCH 65/82] fixed bug where muc+unsat_constraints only works for multiple answersets --- src/clingexplaid/utils/cli.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 2346076..78b95c2 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -202,6 +202,15 @@ def _method_muc( muc_string = " ".join([str(literal_lookup[a]) for a in cc.minimal]) self._print_muc(muc_string) + if compute_unsat_constraints: + self._method_unsat_constraints( + control=clingo.Control(), + files=files, + assumption_string=muc_string, + output_prefix_active=f"{COLORS['RED']}├──{COLORS['NORMAL']}", + output_prefix_passive=f"{COLORS['RED']}│ {COLORS['NORMAL']}", + ) + # Case: Finding multiple MUCs if max_models >= 0: program_unsat = False From 502bd93eeaaa0c4248c85ee1f5a078469b6507fa Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Wed, 6 Mar 2024 14:33:50 +0100 Subject: [PATCH 66/82] fixed arity of -a in graph coloring example --- examples/graph_coloring/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/graph_coloring/README.md b/examples/graph_coloring/README.md index b2f3147..ac07418 100644 --- a/examples/graph_coloring/README.md +++ b/examples/graph_coloring/README.md @@ -14,7 +14,7 @@ + Finding all MUCs ```bash - clingexplaid 0 encoding.lp instance.lp --muc -a assign/3 + clingexplaid 0 encoding.lp instance.lp --muc -a assign/2 ``` Expected Output: @@ -41,7 +41,7 @@ + Combined call with unsatisfiable constraints for every found MUC ```bash - clingexplaid 0 encoding.lp instance.lp --muc --unsat-constraints -a assign/3 + clingexplaid 0 encoding.lp instance.lp --muc --unsat-constraints -a assign/2 ``` Expected Output: From 3f95703a075681716efd7f24c646905e723613a3 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Fri, 5 Apr 2024 15:58:42 +0200 Subject: [PATCH 67/82] moved to new potassco project template --- .coveragerc | 9 -- .flake8 | 6 -- .gitignore | 3 - .isort.cfg | 2 - .pre-commit-config.yaml | 18 +++- .pylintrc | 31 ------ CHANGES.md | 5 + CONTRIBUTING.md | 31 ++++++ DEPLOYMENT.md | 15 +++ DEVELOPMENT.md | 41 ++++++++ LICENSE | 2 +- README.md | 15 ++- doc/_static/css/custom.css | 33 ++++++ doc/_static/logo-dark-mode.png | Bin 0 -> 9697 bytes doc/_static/logo-light-mode.png | Bin 0 -> 8934 bytes doc/conf.py | 62 ++++++++--- doc/content/encodings/index.md | 7 ++ doc/content/encodings/instance.md | 32 ++++++ doc/content/installation.md | 41 ++++++++ doc/content/quickstart.md | 11 ++ doc/index.md | 9 ++ doc/index.rst | 10 -- experiments/asp_appraoch/README.md | 35 ------- .../example.multi_muc.converted.lp | 20 ---- .../example.multi_muc.converted.reified.lp | 53 ---------- .../example.multi_muc.converted.reified.v2.lp | 85 ---------------- .../example.multi_muc.converted.v2.lp | 20 ---- experiments/asp_appraoch/example.multi_muc.lp | 15 --- .../asp_appraoch/meta_encoding.unsat.lp | 38 ------- .../asp_appraoch/meta_encoding.unsat.v2.lp | 36 ------- init.py | 68 ------------- noxfile.py | 96 +++++++++--------- pyproject.toml | 76 ++++++++++++++ requirements.txt | 4 - setup.cfg | 51 ---------- setup.py | 6 -- src/clingexplaid/__init__.py | 3 + src/clingexplaid/__main__.py | 10 +- src/clingexplaid/py.typed | 0 src/clingexplaid/utils/__init__.py | 3 +- src/clingexplaid/utils/cli.py | 7 +- src/clingexplaid/utils/logger.py | 72 ------------- src/clingexplaid/utils/logging.py | 90 ++++++++++++++++ .../utils/logic_programs/asp_approach.lp | 38 ------- src/clingexplaid/utils/muc.py | 1 - src/clingexplaid/utils/parser.py | 27 +++-- src/clingexplaid/utils/propagators.py | 4 +- src/clingexplaid/utils/transformer.py | 2 +- src/clingexplaid/utils/unsat_constraints.py | 4 +- tests/test_main.py | 14 +-- 50 files changed, 546 insertions(+), 715 deletions(-) delete mode 100644 .coveragerc delete mode 100644 .flake8 delete mode 100644 .isort.cfg delete mode 100644 .pylintrc create mode 100644 CHANGES.md create mode 100644 CONTRIBUTING.md create mode 100644 DEPLOYMENT.md create mode 100644 DEVELOPMENT.md create mode 100644 doc/_static/css/custom.css create mode 100644 doc/_static/logo-dark-mode.png create mode 100644 doc/_static/logo-light-mode.png create mode 100644 doc/content/encodings/index.md create mode 100644 doc/content/encodings/instance.md create mode 100644 doc/content/installation.md create mode 100644 doc/content/quickstart.md create mode 100644 doc/index.md delete mode 100644 doc/index.rst delete mode 100644 experiments/asp_appraoch/README.md delete mode 100644 experiments/asp_appraoch/example.multi_muc.converted.lp delete mode 100644 experiments/asp_appraoch/example.multi_muc.converted.reified.lp delete mode 100644 experiments/asp_appraoch/example.multi_muc.converted.reified.v2.lp delete mode 100644 experiments/asp_appraoch/example.multi_muc.converted.v2.lp delete mode 100644 experiments/asp_appraoch/example.multi_muc.lp delete mode 100644 experiments/asp_appraoch/meta_encoding.unsat.lp delete mode 100644 experiments/asp_appraoch/meta_encoding.unsat.v2.lp delete mode 100755 init.py delete mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py create mode 100644 src/clingexplaid/py.typed delete mode 100644 src/clingexplaid/utils/logger.py create mode 100644 src/clingexplaid/utils/logging.py delete mode 100644 src/clingexplaid/utils/logic_programs/asp_approach.lp diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 1ccca84..0000000 --- a/.coveragerc +++ /dev/null @@ -1,9 +0,0 @@ -[run] -source = clingexplaid - tests -omit = */clingexplaid/__main__.py - */clingexplaid/utils/cli.py -[report] -exclude_lines = - assert - nocoverage diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 31b8987..0000000 --- a/.flake8 +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -max-line-length = 120 -exclude = - .git - .github -ignore = E741 diff --git a/.gitignore b/.gitignore index 6c794bd..90edded 100644 --- a/.gitignore +++ b/.gitignore @@ -23,9 +23,6 @@ pip-delete-this-directory.txt # pyenv .python-version -# venv -.venv - # mypy .mypy_cache/ .dmypy.json diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index f238bf7..0000000 --- a/.isort.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[settings] -profile = black diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b503400..d2c4c30 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,27 +1,35 @@ repos: - repo: https://github.com/myint/autoflake - rev: v1.4 + rev: v2.3.0 hooks: - id: autoflake args: ["--in-place", "--imports=clingexplaid", "--ignore-init-module-imports", "--remove-unused-variables"] exclude: ^.github/ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.5.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace exclude: ^.github/ - repo: https://github.com/pycqa/isort - rev: 5.11.5 + rev: 5.13.2 hooks: - id: isort - args: ["--profile", "black"] exclude: ^.github/ - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 24.2.0 hooks: - id: black exclude: ^.github/ + + - repo: https://github.com/executablebooks/mdformat + rev: 0.7.17 + hooks: + - id: mdformat + args: ["--wrap", "79"] + exclude: ^doc/ + additional_dependencies: + - mdformat-gfm diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 82e3aa7..0000000 --- a/.pylintrc +++ /dev/null @@ -1,31 +0,0 @@ -[FORMAT] - -max-line-length=120 - -[DESIGN] - -max-args=10 -max-attributes=7 -max-bool-expr=5 -max-branches=12 -max-locals=30 -max-parents=7 -max-public-methods=20 -max-returns=10 -max-statements=50 -min-public-methods=1 - -[SIMILARITIES] - -ignore-comments=yes -ignore-docstrings=yes -ignore-imports=yes -ignore-signatures=yes - -[BASIC] - -argument-rgx=^[a-z][a-z0-9]*((_[a-z0-9]+)*_?)?$ -variable-rgx=^[a-z][a-z0-9]*((_[a-z0-9]+)*_?)?$ - -# Good variable names which should always be accepted, separated by a comma. -good-names=_,M,N,B,A,Nn,Bn,An diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..dcbbb26 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,5 @@ +# Changes + +## v0.1.0 + +- create project diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d7e5503 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +Thanks for considering a contribution to clingexplaid. ❤️ + +## How to get help or discuss possible contributions + +To avoid duplicating issues, please search our [issue tracker][issues] and our +[mailing list][mailing_list] before filing a new issue. + +- Open an [issue][new_issue] describing your problem. +- [Subscribe] to our mailing list on SourceForge. + +## How to make a contribution + +- Fork the [clingexplaid][project_url] repository and create a branch for your + changes. +- Submit a pull request to the master branch with your changes. +- Respond to feedback on your pull request. +- If everything is fine your pull request is merged. 🥳 + +## License + +When contributing to this project, you agree that you have authored 100% of the +content, that you have the necessary rights to the content and that the content +you contribute may be provided under the project license. + +[issues]: https://github.com/krr-up/clingo-explaidissues/ +[mailing_list]: https://sourceforge.net/p/potassco/mailman/potassco-users/ +[new_issue]: https://github.com/krr-up/clingo-explaidissues/new/ +[project_url]: https://github.com/krr-up/clingo-explaid +[subscribe]: https://sourceforge.net/projects/potassco/lists/potassco-users/ diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..0cbbd77 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,15 @@ +# Deployment + +Releases are deployed on [pypi] whenever a tag of form `vMajor.Minor.Revision` +is pushed. Furthermore, the deployment workflow can be triggered manually to +deploy test releases on [test.pypi]. + +For this to work, the workflow has to be granted permission to deploy on the +two services. Please follow this packaging [guide] to setup your accounts +accordingly. We also recommend to setup a github [environment] to restrict +which contributors can deploy packages. + +[environment]: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment/ +[guide]: https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ +[pypi]: https://pypi.org/ +[test.pypi]: https://test.pypi.org/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..25fd335 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,41 @@ +# Development + +To improve code quality, we use [nox] to run linters, type checkers, unit +tests, documentation and more. We recommend installing nox using [pipx] to have +it available globally. + +```bash +# install +python -m pip install pipx +python -m pipx install nox + +# run all sessions +nox + +# list all sessions +nox -l + +# run individual session +nox -s session_name + +# run individual session (reuse install) +nox -Rs session_name +``` + +Note that the nox sessions create [editable] installs. In case there are +issues, try recreating environments by dropping the `-R` option. If your +project is incompatible with editable installs, adjust the `noxfile.py` to +disable them. + +We also provide a [pre-commit][pre] config to autoformat code upon commits. It +can be set up using the following commands: + +```bash +python -m pipx install pre-commit +pre-commit install +``` + +[editable]: https://setuptools.pypa.io/en/latest/userguide/development_mode.html +[nox]: https://nox.thea.codes/en/stable/index.html +[pipx]: https://pypa.github.io/pipx/ +[pre]: https://pre-commit.com/ diff --git a/LICENSE b/LICENSE index 7623932..6380a11 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Susana Hahn +Copyright (c) 2024 Hannes Weichelt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0b8eff9..ea2d26e 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,17 @@ # clingexplaid -This project template is configured to ease collaboration. Linters, formatters, -and actions are already configured and ready to use. - -To use the project template, run the `init.py` script to give the project a -name and some metadata. The script can then be removed afterward and the -`setup.cfg` file adjusted. - ## Installation -```shell -pip install clingexplaid +To install the project, run + +```bash +pip install . ``` ## Usage +Run the following for basic usage information: + ```bash clingexplaid -h ``` diff --git a/doc/_static/css/custom.css b/doc/_static/css/custom.css new file mode 100644 index 0000000..b5215ee --- /dev/null +++ b/doc/_static/css/custom.css @@ -0,0 +1,33 @@ +div.admonition.example { + border-color: hsl(257, 20%, 50%); + background-color: hsl(257, 20%, 80%); +} + +div.admonition.example > .admonition-title { + color: white; + background-color: hsl(257, 20%, 50%); +} + +div.admonition.example > .admonition-title::before { + content: "\f2a7"; +} + +.sidebar-logo { + margin: inherit; +} +.sidebar-logo-container{ + max-width: 20%!important; + margin-top: 0.2rem; + margin-right: 0.2rem; + justify-content: flex-end; + display: flex; +} +.sidebar-brand-text { + text-align: center !important; + align-items: center; + display: inline-flex; +} + +.sidebar-brand { + flex-direction: row !important; +} diff --git a/doc/_static/logo-dark-mode.png b/doc/_static/logo-dark-mode.png new file mode 100644 index 0000000000000000000000000000000000000000..81ed997ac70bcf9c2cfe1fa3b484808576528db0 GIT binary patch literal 9697 zcmZ{Kby!qi)b62&4ut`61XMy|7(zlCM5IGPVx$CAV5lJ^WTaG(P)Vr)9FQ8ihLrA3 z=^kl8O6rck@80M6{<(jg=RD`#d%b&|v-jF(t@oS{dOB*fR5z#q0H9TW{LlaZh;W1> z7er1#&MFRy2`5U|$EF?tKz;4sK?HnFVI_b>9tLX40Ji_;FTw?>gOauq0F*>ipIegw z0JoR=LnT8WqV3r$&(Zc9?Y}!x&N&cdjLfaMoS8w2WQkNLDi|}sT$~H!5JymPG8cU3 z%nqs%d%;qaP*I=%TJ47V{D3sh@ncJ9YTT{*15I9HmR9NG!N!nf@do3|6QRqawd3oJ zjrj(+qo!TUwmT=dgLi8ASJZQ zp!H{ev=NT3Y~E9BW{^_pn7iRkxaB+kV&-B_Qb;(9K>3KzId&74Y;Gs`8g+S1j}pnWNM-k^7C4Fx^;*iK;%Dm8;+Se^-F+%3cg7iDa;7&0PR zTaCdhmbjhpZ)a|1%3~An7g|~wkexQV#beDgNu1MAVcC$ole1n)6s%kz~=LhzfmGltxdT$V=6@cWEFT z+2q%!rQbH34^k?N-=8K|-ZI9W`gS|k+B^-jz4m7D#Cda5y6QmVSnYg(s4diB zeQ_i@xtfi1CYmB3JXJKDu>(+9kQ)-uZ-@iQJ|vxes4cZ{Bk(+br~iz4HhQG^nt#Sd zzBsJ_qfdm|pmr}Q3D(H}SrRTh(kl6Fs+#B+|GWn2oDC!Lnn(%CaWr(A3><$Nk>0nh z-Kcr#kA1j6WJd{R46Hlq2*g?}?t1--7MpwivwK~==~n$Y1l#2*l1KJ@se9|^`2xP5bgO#jq43L_JHwuV zym^u|?Qy27^##GxF$2nn{Jq^=@M=9*NRfJ2R9;$HJ4d-w`Y-BiFN zY_Q&pvT4@BdkH&x0)^PRB!VUm-^4Qc6p<}BV3VEp$0K)x?<~OdvD9@Ql9zo;KlNvc zCT32IEbzB!!4x=N5q2zlhk31A{c2rvX3g(>DPp>^^>%u~a0#TED+(t$h`KM)sFtPb z-z3o+bUlj-Tf#F^ASuB#cFDc*s1ljpBz710O(Kdd*JT|uYkQux4$o^kVb8#7O+? zxI7+(BGIaL<0&YB)WZ^~B)1AQ+80q;OEvfZOg?}o@g=aoAv(b<(=l{*M~c1a_$m|2 zn?{p93mcC@XjtiC{m7Sp(mHHce^R|SQ(T)8;?V4tL-Dbdw)Up=b~G<>`-(!yzzi(l z#8Hiz;`$F-1(`2oYpm}!Z$&*I&67eaV*U7j>wPhjcZD>eXJB7+#-sS@_`YuD)kY&& z9XqNa?jEzf-CXjc*70C(m!^l+i+Qyb3OZyf3t>s7UGa)d4{NL)EIm?Dg(pPMYU>C- zQCjWKxQdA-eIsT^B*xe_Q^M|$z$J;B>0Zvj99Qr`fPJT@?O&CxJf<~T*Q}4rHP97z zI%p9jxVveq#+oNDq~wQ2Q-;N5%(4ol;CtH?4sN$om8joRR&Qru@FYjI$wesEFUq1n zMh1*h^*D8~!$MkaPj;*pt~96n@c@II^bT|rH=F4uEaqa&58jd#_pEYM-Psx>uwa>h zt;Mh@SSk?2P*qkXra>bY!b<5&% zHW5_I?HY0u6;yNn#Sm00?J?Wrg6(*bLJiPJHPZ#u_?IR3lL28CbL<}Rw|YOhj8;LT zJ%|)}p!1d1A+sqb?f%>QQefcu+P;ti=hj-2G34NNwb8c zQv>8{AQ>l*Q+uIQYT(h*ei(!uuJxfu%>0)H0256R-jb&tS=jF_mIVXXn^&o8tCXiK z(%9k!h!m|Y8jAhy#ddj~wVIys0erDNV6O^2mIQ}=N?k5MPd0&fYuw_BYMFgyc*;yB zdzv^AM1b%^{GnwW3Fb1&bWN?*veqWl%T$pITP56dD0TNE>nfJ^<^7L6BGaIzZ_;PQ z`Jrwnl2^fHo=_l>$bsDp+`M^Maq9(lz&o`Q`DPivKWPsTEaPv+ujK*Q!?lly63Yrk!8jpHbv?w(V)!fko z1p7Z(gKt^%;g&K8NKeHw9a)p|>E`@3@4!XHPTH<1Z&SkDkVQ<4MORte5w6jp>z(p4 zO``_(QVC0!!6$urGYKZ>Fm2bRGXtR9|KrHIP65QWPVLye<^cc=#-Oe#>w821TCg8M z-2Jm8nj1jh?e`+q5F#`#I`N|S@DM=JG-Z}Fw!$|4U!#2DD~EM0;2(#!qBQ~1icQ`m zRd%RKvD(I$lA_c>8Ed$?x&-WWkW;;<96h_HC9dYrpmgo49Si`dhW}upO0L3ATOR0b zs#Bz0^)$Ll_{_)v*IPZseyJ1(xJr=qIcjVBUmNcyAJ8F2G1hE`ZwHqJ?DtkyePwc} zl1icE17I-kG6DDPu!Dz>bYeek@D%?;?Z-w+*uEY=eaM9$p-e! zMB`7yK2<XpRdu!X-Tt~xYg%QDNpN*_D|P;)lk&q0+6%bSKQPU&Hdey@)0;=`j}(E;sc zV*M=79JME&q?3R(V)8Tnc-Rd+09O>Hr`oOjBQ=A;Bokzqm{9H-u)tFCD&KP!V)`-- z>(6qoOX+TitZKM; z3Fl7Jdk|ZXYim4vNXl`$X9CsrSV0zQ-9F{~%Ciu+j=9ZWS{-c*D4>k z{IOZCu;r=~^JL52H-$0{l3)|RsKBvVbqQ_@WR?Ah(eKG}^NItUNXOBuWB&l=#>;GM z{m)n|mP@|^a`$Dz^)hsXH&=qjaU!C}-=--vK*e#9wNSzMvP@dTg=BkNvzjk#%C~L1 zpj=F)l!b0chFvfi8e&l>-do<=Sv5p`elqLz)lSq*qiu@?r0QEBVa1Up*xGZKffxn7TsSgHq)&is%0pp!VO~Fv&9WP*w`Y|m&}eLcqDn( zKDWP)x<7I&R=YS9x+v^jpJ09f6i6J?`^vTPNaL3jUsb=)mf#t);KBM?Z9Duh*uOn~ z#!5@Ekgb%5V>4PRRJhURy6N$SfkasL@!^Nz!&i+I8V(|w-?>*x^T-_OmlHKv%xj-D zjqi3l<$q4j>?2Ks(LwTZT>XWWeFuF0FefUc{T044#+k`IEyxlx<%F&nQc90@A5RsA z-sHEto}b{Mt(leTR(bP0Jz#A<-8IWC@AA@+UR?>i`$7O?`_|{#)UFMfe>DVhFz~EY z<8>(e)o{m(S>SmVbRp~+#|{3rSi2YVeg;vrE2H84HpYG7aQ;$TX{B4zE}YfUNjwp5 ze2&RepSWhA?R9fusC}g|zQT^o@lSI1>VpxE_XGJO-k8r6GP!rYCi&5)ek)G&gPaAX z+&rJv3FqLo0&2eBXdI~KZJBL|H2$J@gG&E$ZwKR>VT}BPxe>Tex&bmk1_OQZK%JQ z;)fy%GXnTZ7UJwzAv4uhYpR3RcMg{VzCW56x#Gyty5P{?h%pN2r9Upj3%0K%h6G8& z=|~P?@MLFq;jhXKovx(DoNgv^?64z+W24mhTE2&*xQ8$)C7ARdOR5=7 ze9xat8?e(+q51Xb!l(nO!~sFN$>eIhBoNkc#%A+6Bwq-ayV#ITyn1t~|25c;<;)Pt zGB)E!RISaeWpqb|Yo-6bW$tVOMbZ83YsiXW%q0mC`eCF~xfs248-ImS;A{Q|Swf9B zcrpN1eT>%9QPcaKP)m_Kgl>B#YyEVhFKSzd=V@P)0oeN1+Pu-9IrL4ZCZw9&@j6Q+U`4Q(_8D19dVKHwj=$3Y12df6VGL-pVf{EV zW)}bGB?6LDD~7jGHr5yc0o@`iHd~|ocIwm}?3i4av&b`koY^ndbP~PKmiJRe=L91l zxja7>a+j;1UDd*PLIdiOd@42YkwMzvn@wB49qI>ODnzI>_W_=i3_(fqDss3*i~c>@ z*?1s6$_>|o@jBe87pczAEaOW`a{f@0)j@pxGB%9RvOP<;g+*)5I&ODgVc3Zd9t|$a zM-IbuysPV@w`&9lPb%XpNwE((J=iW~N9u4#@`yZj=&yA9I)MICy34(c>^RMp+^-Ce z`YjCzKz!&FNZd|$(3 z`J#KwGijKE7VwDu>yP_y1|w~@bI=jgZBHoV*@=M4`x<5CZaCwMS=O9slOZDwwFz!u z<~law-X=7mvX`^MVNY#frGYTs8ughuag#Rr<}V#nd)HVF=Dh~AX6pNWBLEd(Fb>|G zJ+y5%!<>GyI}3uIejpxI@MH$I}9ear`o3&S%{FbXNa>zA(fU}4iW2$kCX=^<2 z%Qp3fOAyCnm|aQdH^=z5J;cuJ$S>!;d6F~AcXi*uc5&B%1$jZSYSo*;{z5W`!G#Jl zOOcLbK(+2G{XH$Q>jGoZrMYVodE4R8it7#G+|s}TLN*=oj=tt+qknur#osfp!69c# zfCT2_G|<9I^96hfxjb2Dtc!K#u5!acka5G`IXy+ZEZ&PSeePXZF{S zxyv4fdi+5MUo&fU_sc?RY>-^=19dMVoV_U&?x+#eP{k zQ)mTjkIa}^BC}~e+VoSH@^r&-NA32;cwT0fA5D|tb>9o}w5&~+W&kuM!60|2n8(0LH=OZ~BEW;`} zdfwwNgN}nR4DRdQd|veTgD?qSf%&7|*v4(iA4lCB(dSp|SLcjri=!z#qj%eI=|kD)*M2xxSK4Dr~O?n+xjUVGI4~xieMY&#IxM?dMC>Q z?lV*go3UrPwH?o6u`u7t|5B8ue`?$6OtgaXZaiz2d5n+bS!cSe!$w}MnN&NPtEyxS zQ4tuBxyk!**{cwt6PW(o#G-EKv?Y)*82I@7mw`u;Wt z3%A0Ihm(M_Jz^sy$hf0+KmCB3xwQ(8*_oLwH)ReK@W0p`kKS<{kvF(XF}|?)wQfgb zd3^qiJ?ev}?k&MeZ&q6x$p?#@49_i>9mOUBt0)t82-VH?Zkus#0BF072Y|9igG7LR zB$@>1y`e+_B;F6A17MDXRbn*`dH^&PFd_kzDYcSheuoGE5G1XC{on}%tPF(HAW(&e z|Lf0%C+sG$Cx-viIpJ^l+HjgYEzm^&A7TsnNB2hGckn_NVCncLw93w0Ty}M-3!ph! zl#cUZ>c5qZ+Ljl`8PD_5g>A+lfiv*N1eN?{!W|9xo*Y)C&bvqkzUcxx&3olsu8Lol zh8ImQua5Nx8oX?*w~lMRnvF%v?VU|;pL%ZkEv_q12k0wR_E!wrCm;Kt-JG7Fi=*?v z50Di`mF?EmY#vt*!(A2KF;J0HDp0r&0-IX3A z5cz^<+beyeb9Z-jK7YKi?srEU19({G@)oAT6w};#;NI^QPJwD*V2`4+Q~%p zhVYD>923uvD(`Je`S%d#zb`I++kb4zn)P*5P1YXk<|qdN~qG_;qYVHcOep!Mb(cL7@|u7X{)dpSSX0 zgH(EEuN24Z=0yAb$W`?djsz*+tqYglH?A{6&thw8%PaQNPuIKO$Mh_fxKuq#a>d#) zmagh+w98KZ4K{#rVVo+Yeix*gB6DZLVtUq+_Ve0m?1fSsax@eNg<33?=^VP(wK_A5 zpQSjymemi^9>id!b3;BGDbrQJ?i=-MSfn`C7-|$#;#@RTR81 z$bRmx&>pPNctZI!P2<${AW;`XBWe<^neb=Mj0_UJac@*_WFdinxh*onUcP+lkF1vT zL_ttgWNL|_#Z2})iRA2|;HZ*tcOzvT3-JJR949LnxC9%X&k1gVry`jN~! z1BCmP(U;wI7jG~zEKW?8DXdm$f*&%Qph%2hAu^W?g7()vSEhnNgXmy?tOkw^c)t~^ zPyuLGLatGf>35qzTq2CWfqC6|#}5G7eRXgDy@x-Sf`7*wct6f16C@EkK{d-&ubvg; z5Mr#^St;QG7ls7@$f|`1R=KPMG?vq3kgT_X6!?g68DtZ>=^#glD!pY~H-6KVj(xJrXV6YTHc?&FuF;927Tct39ZkZVk@ON&yABKUx`9E9^hz8IjE z4T-JR8qj5v$NNbDz}l6n);uejKN#49rWN2>7@(h<1#P+R+4>7U-xR%m+-pw<8Boln=@YJjOTvaR$rq z;03+ZGxA6PnvDgt*hYhY^O76T6cd$;4_evL6uCzRz|gkA|B-yV$&4&HD>b@tYnB9H z?JZ%3$NUUntGf;WB=~Dc0nC{u0MIPp(Fd~$g~Q$&(stT}X95Z!ICMB9M^q*(LSac} z1)3jyGLJUzveJa{5Of;4Q5g?kvecn0KjqGUgT)xlheLgekOjFUFmYTmINN)W&6(2p zzosV|s^F)Fo<(GUB2TW;vCc0e_~gA+LjdMnsQ2v~TI_2qQwm|B(HIEenw}d_Y0?6K z^j!*gO6)KnttKtZC>MSVh?afF6Gud*S?Z11{RovRy1DBj0N57=oCW95qI?Lqn=Ov@4Jh~?%-M5%2tU3kp-!}mE-xkX9 zb&b@vrbA=`n5?|AHuY2!>~Vru&%fX?mP;as`|}Bi!Frmz62M?>e>LuB(9Lk@(|Zsj z2oKlX%xaUWSi14)Ama482-~Vm7TF4sQR>@|V4Ict?-v5kKb@v<;`d*UJ&7Vbm@FjM zicjfVTAc>j20o6Tg3&X(j{O6znHVIk1LcF3yd6x@20xdPz)DQn#!^s{+Rky@JKWl* zQA7E*S{{^(C-1qlu3E3^RiFJ?Z=c2T6-}-;KI+Kd9o- zYg4Cp@6+p_1f?*;6^%@Qz#$r54)3p(#w1#QN}lEgI~Iykq%O9zRmb~d?Pv*OWA}|_ zYhspit4GR*yLWhYj4#hkKd@id45Tj~7c%51w91J4QR9`_yY&2X#c(J+{tk=X(fah# zXruohHSRxSf0uKwZqhL+{ywj{?i{~S?^5<-e9ax3MA)(?GuQ8WPpsM({K$-@@1eyv zYcUA-N2?}0wTx7_WaVE8a=koR{&ck;_nqnQi&3~B#VjhwL&%gBaoY0Ahtp3L+!{Kc zkyrKWh zR2#cuAq9sW3f+TaWzh;`L*skU;RzSPCFf3Xv>6I z>Sk}-oP^pQTTt&f=GQ(gH_BfwukydzgeXiP8vE@ja$H6KE-N@JTM+cnc8XC=hj;rN zfZi<*y@dXDLKN{u(trOs#fIcz1^|lRM2ACDAlo-_1Y8}&>mW%;>a+mG*ZzvBGBQYO zVxW5ow@U8l5d0sSvfUaxK-f;j`w=!8d&d9itviWKqXdYc&vwLCY#v>b0O*_jh>loP z$0t&UD?#|AUxgO_bphB>K0j7vRkUW^F!J9FZvg{s}|#2 zi)psyP9^vr;ECs7?4I{fPrY7c-%e$LrqNX3u4RVLUm8j3#=Kfr$VZ$mXZvzF|Bsxw z7A?H!c=h@;Q@PbkCbDA}hiL3OQ&`oL88wm(@**T9n@c~PH!bou}+l-*L(&p+Bc~ToPm#J?u37pRBb&rE(@69CctF z$L12-K(K4eu|fHChQ#)feL;7cM`407Jh|}sw-Yq=`8)2JdNcVj-V`8Eu*AHlF`?+9 zr)F@X=4z&H2(|PuPg8pn+aPXR&c1GTqfEjNP5It?sOUNG3>*%1cBkS^`*3g~PZty; zcI~Hgu``kMgjQn3)LniY?Nu}#FQHNe%RfwiMI@@VWr|a9_qJtnUsKlY&ziX=AT`m; zO@CiIGjp0rIX5nqS7k0Wn)?~d;cEBhB!kROh(tcAKnBIS7MmN^?vwurESI3T+Ke+# zqTx60-ycvjaH+hJh$s=N0Qm6afhnL39qXhG@vx`*ZAemc0=;2DNm}ELdrThMB+~x&lR zRw;S4Vg^mUv=Vzr`@!WUZl39PR2Uy4=aEoM;7eLVQvo2gHN z7`zH1e>nFBBief`G=B7THTUx42_zk*sUO%1Kr&VmAY5pYxoUzVkNqBEtu_Zx?BYKZ zuSHXf_O;Fk@}-+i8a0m^HeWYV{$$UFR7&~q@&etK8wNo+#uf9G(c+rk&DLxsKp4CL z7*`n`juR?VJO_L&ZXv(MPH;jVpLx@93nW8I3el@pC56ub4+Ldi*hLhp(8U3XrW&e} z4=2ZnVPuAj)Mbfi>PnW_s?rKd8J0Mb)jN0=dwnJFbDCPtE%d$tfU*&U;)8mvkK-fJ zL`^fL93vxY?ExYrX}$nZ`Nl{$f);0mDn47FZzln)9!Ag+JcB>AOc2=WAJsO14889a0s%gTpd%`qv2pd?9O(g*a*U`Fk(6Q>>1=$jHCP85t!0pudb1wr z%s>7{u60eR>dwJ%FiM^OKzWcT^bY=rm6O2hX=h9fJXal@wLulm&tOx$wSw*4|iu!bdj1Dh$1OFAR?FE`J8=#nJ*+@6nnI zEl@2TN-h=g#=|q>N6T8Db#l?(h3@6kwt8_QfQSJLpOI|EU7+{O@wHdxQ$bM$jFG{h3)p|9ADsN1yCZ5(avJmL-J{ZaKNp7zi7r zQvH9m=F=OvFmM=}WH`}Md|Ac!6TMOuj1&?R7Qv$@6@<7nvknIh{(2dDn-Qs)1bWZ} zQlhYWX=bet!kuP`Ci47rCZzYuj7TwWclYKlN$v9KR#$9X1ll~zIPu0U?k~FiimyVK z7znoC>Gl`v;jBRjwg%9XmNuWSQWI1U33ZL6^(!;3h+dfiS1j{;v^n{XXwEx{o;%(c zt=j*tbs9eWHd1>bh7;bd_-~gP=WrP%tS8_ybLxGBJ)MiFcW^zpz%I3~yC$CS&q&9; z^&3i6H!Z}S>-Tx*YW973pSD_IIHGoC=?-2SsO@;B>u2XCOo#|k;{x|wj{ z$ASAnh{Ywc^6R&E?mm?Y-(-$5&w=M3b$NyPznMW)J%#c3AY#w{o7SaVY zcDzUsfXH7~?>aQ@oYqU+#K>o)dTeu=_E%hyyp6v;7-l&9hww)hpsuR(5UXqz^nU=n CT93^D literal 0 HcmV?d00001 diff --git a/doc/_static/logo-light-mode.png b/doc/_static/logo-light-mode.png new file mode 100644 index 0000000000000000000000000000000000000000..d2ac73df81da80b1e28398ecee1ad8385a2740c5 GIT binary patch literal 8934 zcmajFcOcaNA2|Lxdt?ilsf?Ue_I89iva&fNMUj2>8JS7OJ7jZ|5J$4_tjNsX#i6XT zMTj%LFQ4D<`}g;c*S%h^$9g>;&+%B#i!;>Mp`+oX0RVvR!To#2006;)S1A<*2qD() ze+F;Vp7$+%0DzX|@`3F(Mg{XfW(z-Peq*jrgFkJ0E)23 zgp)R+&qd()83u0umP#?TfMNWK?hHp!<7hJW<7}9PKBvV^x_jK?`tQVi@GCUhF?Em|7S77k+~gd4I%!w<_)?Ez^qz z2Yk-XxUWwOi|dKxjR^YHFOt@;%(w=}w%7efI^&oak@8y86STg+CaLa^y&moer&65l zwsKJ`qpj0c=bgkncMLKoxyw)RBz(61!X%j!s#oc;1_>{7GS5bY{G5={O0bzhh;`DD zEkv>5Q3tNGwsF(~!scZaY5oEW8f5sUzuzGzRdTa>F|R1|!$thH9JvR zXw8yzo$&E_RattLe%pZ@iz+I}kJZ61t^-g;b=eL4u9z#a{>Vsf|7DZR2~+>mS^wuo zSXo8(L3Bo-T&gx+w%)DP6tR=paWrVvM5NJHI}*svmV%r(OQ5^l!`6Xtw(G{Q`OzB@>1DYpDjr|`ejAN zYuJ=U;{Ja77DtoOd1 zQC#{C1i(xo4$9?Y@hCqOY^OyfCQMl zzl(K}eY{C`KqVry`;NpjwKl`n^)wPGMnZo;gR^>m8sCX|hKJNka~`$Th>xZb^1juG zN{24|=!AA(b#mJ@y6zH2O! z=bhnTZBgV2)rYa+q%C!$0Wa%WC1D9McI-NB5C+=y#Q*NJyF;z~Gm`HJ=9G8B$Z|xU z<@g5p-8q4)cSgkQe5whnWD3_MOOR^%%%&nyV))*Hcu{|20g#}jQww#zuw^ui<99Z_ zOHa8}J-X|-fqYu{Oj81we+5;)UXT9Oy<_1MdxPB9|Ag=N_378{aQkP6SG1K5V`!9! z^%3D3R|3;nf(5zFgW3YjIAu3&sW^V<(v8J>c>L1_;Ed9noBc)M|4nCmypmJE$fNaS zy`Gfxinx05wzb7%AirD%VMrK$tTUDhhvBI205)H{yplUeYX~BBxofiFUgmBKBx@O- zXQs_EQ-YO!-(yxX!`PnNOZzK#D zd%|96-pA+X(HVVL{0V=$k7pp1BJHID$K%4+33L34hv_CzBdhrco&=FBDNJ00-8$hZ)T?OWTX) zk;9hgCH{azA#MN6`dIvSqFt6U92%K{RL3G^ha)TDIpcGsb2wlK5=Y^o1s1XoCxc^K zuL7yAAbyIflX@>7)&7VI7`GR=Kv(bIUTtlEvDBIp383kr$FV#rR^*nTQsHT*9TiO4 zQ$1nb-n!gCtyS+uJKs&3J-&3uI;QZSn0saJcPIDonV=fd{QvuV(}Lhv3EeLT=178?PyAQtiMQx=Is%98}QFMISZW7J)Y)CK_7TRjp>0e|TMw7l!FuO$TpFih44 zQI~vgYTK1H+{FNOjG)p>PkyW@#NJ(WJPXlm?0@vBgCYrU5%iw=U^uXL#g-Z{6rru_ ztgtP^wXP^6WJHS`R-an=0O8(bzL45?29ZvsG~~y%D*=AYz?(9UszCCYsvu0Cg<|mO zFINZ<<2QXZ6FQwZL2G)W$D@M`0(fr-CFLk}tswGEdN=!PQRfS30MIDC9yBk@DKVXB z`X_}U5_LX|0f4*+EaQ(7SA{fI_t;vaUz2;sn(QRN2l*UzWAD#+Od(DJB*S7n7CEo>|k`0gXEN#d}#2U|cco8}9A9YXM0kVoTMZv#T1hE{%TXCcG<6o zbk-{oA%;gSqQ5@4&dg!G+FMS}*tSv1r^lsRl>tlpL&Txhk{LU%izIsmiM}KX*&>d8?;&7Lo33FQz_=>6U1dPYa(h z{Gd_zvG{gnc4)`Qan=;SamU=JqNr=?*-xokKKa(-+t|O%OZ%~+Q8wkeOIhBfwK3Hu zXESA~oQo}SFhs+{KP1&*ivQsC*4BO%jeWa2ZZcnL1$TAya@^)!`!F%`&%M2VTWiki zyS!ErK9nYPwUh}$SyT8Xq+=Fa`J|bNjw*gD8a)O!(m1R~zFpAtrz@ zK-aBv-lau^{>SKdFu8W{W<=zoG8!c-xK}0DEwgN!uooz@mgQYpyH^&dHzB?tn!1_8 zw6pXmr)4%mb(jyXBD%|8l6N+g z>t*|-;=W~f>z!36;)JlXxZgYy+@)M zqO9WJFy-SI?W8}>R)p}zXGQ>aWV0qTB#afiLlXZF@xP}9(n3Qg zt+xouRz9uNZ6OhcWoDtGOJ-RW<6Po7O_P7$qFqzPS9t^gdr=JP-b1A&JrgTk8SD58 zqmv0I=ynu|y)x{0dis$Gwz1#g{#r|oIAJA~Pg#y&E5egv2 z8JRE|D2gTv&#HlsJySJy{-oNFx1EYsdJdbrX$5I(s)76LJ&YH+Q3PZCtM0$UD26RL zo*kqBf{dPHQnmL-MFyG|kxAr;g_5CMp%`^x?zD^e~vo4ZPqM9$-{2?_uHH20xA%u@VH zp7LH8696}XKH~VYy9VANk6%liqu(q*>o!kWt-N)J~`KQ!B|DmNSa6Scnok>6`6~+4mx3ajI6uv}q#QMF|Brq(b zd};YdK&T0diVVzK`pt77UYh~oj{l$J*R9rV2HQk!63~VP`L4B?eSg}2?!)9oHwnN( zXlc?GbrxmPpPu)y^q+3M_gH~|jG0^UajvNqRTqt1NWFWI@TR%j12$L*mGi+1r!4=e zr%-#_!tnijMa>>7pZo3k^TmIDE-O~RibDVf$IP#1?G^9##<(eRyJO->0A;~iqg~^% zC;aDTEiCTsDM}aWug8N^AZ<3gC~Tz+E=b~Ha{VYzOc5&vS&#;`sfu~<{`oV%+PMks zz<2t}YFDsm=~9M1(r|?bD$bL$pIrS8g3R;B$!9f=XS3eC#nxP>0X#pAZ|iaumpwgv zI!~fml*v4y}_(b2e;grnYUjY^{G6&C^z*lx=Zd`IW*akfW7rwl3?6T zXt+rONM-z!nPvCeNo~xMK=iREYS*8+uz5Pa;Ft*!ftN`?BH&=x>l(yr6aT|*$}M20yAbb zQoJPl%LQLf@=u`-b?C1^Ruls&LUbYPnHq>I7CFO*6{`L ztYSN&K;@9swpyH8vEP(6dD~z)GmlUD;iOe<)qt6hUi%*!tZ=FbO>JEJY|UN>#SClh zdi#Kt`=2)#^I?&R?z#YndD-8m6ptKdIyAP1&bs5PyCTGe>0p#-L8GU87EXpZGsNdw zZyR>PNMXE2Z=oTVIPYrd#A(DJX5x@B2~D~XkIojVd`eN|npW^X2=PX$xC^$r@}tP> zkaxPJ?96%JufXr#FWchhbwuWqwQ*svDEM4msezQRR@qByH-=-g3{qd+*c>BBSQQjW>A<#IuBj(ir#C3sq5*j--hMRi(Q^!Q~yk8e&nL<*9Y~r8BjolCp0rZr9U% z>edddj~>Vf!EufgpnsJ#;={IpH`bAbX?=tJb+mvKUTbF>f&&I!* z8>eezbYc4Tpt>vNDI-y=Xp`&$?n{G3&GS)Mzt6-WZywe%x>PeF9;c7P7hT3x6awF4 z>t;A=P5qTH(d&HKbyFV1v#QqP&(OCr8I&zXM=?~`S%Y;a>H>W~b!8Rf*t}BFQWl!R z!6}CE^jn){^Ue+zzQj{pWWG|Jfy93&!G+@cmUaej{sk-r|3QBgE}^a3Im(}Tv7SUs z!dPhkMUO*^FVpvNuze@#K*j^uk5VZDZ6BDBK==dD=Kv|%!w^6)0Zj_%al$Bp4Cx4Z zK=L^oG9_q*5%h=uBOdXk1`$bUS`q+lSaZqb1Y;)$+U|`Mf^1o!;~UG;4L*?ufUx&k zmog?tK$%f=eEjHeN2!;@e*phwJ zbe zdu&z*PFN+fiwv>Y^_!J+4v8E+L_BI9{rv7zaB!NTE?vCiKyi86Dzo3$U!700*?J?<5KG2S$;eT*EhDi{~+?mKPyIr@La7~sP2QQR9Bfk=qtwo+-CdT+m0vX zi>1}JRM;;vzi+e5vGoyK?E3z|*}{TLn>fA71FIJ|8<`oA`@8RxJ2Z|(qqsSjJFDJx z(+Utk4>s_C5zYpJ3S?n{68(|MnuZaogce~sG3*xs8KPI^PBiDTvYl7?@_)$k0~mXn zAXP>_8r2ZcFC@%xXsIfmVpKS)%mTj}mE)Z|EB}0@L$%Cc6tmtGk;tXwSRvmy5gTGw zJQA5$_1c@9Ur0ZTK+X@oic@6>MbL9-dk<%pNWf zJ-bZ-wv}0zdUG$FR=||dc$)%m2iFAG>g(V5LgFRv78sjZjDgfQ#hCtnJUSk(b0m-) z6R*`af=5?drt95@bZHGk67#r^t=6Mhx06W#;jSvQuG>g6xUi$;*^rn~i+Ww~IaECF z9?~ve{DA&;LU^D(1)pJq&=D&4;Bt#JfN6x#2We1w;E4P(r+YI70cet;ge%n&5a2zS zWhk>HR_B3x1}nsX+A%0905<>!UK2JYl7K08B?H=Cc~j&*zr6F(C~_6Mro~*!HBt#& zBJysmHLxfF_R>k@c%>poV;axA>wr5itzr_;lU${CvQ2C9;tJmT*t-afbcA>g8QYg)jPL4Mackl zic*+!lVH*3aR68tyh*UC8VB~%e)=+8mr6oQ@yZ3ATh^?5FWk#9Pu$`nZ%Z&A{^7H3 z%n)GZ8_0-kbK_?54~DQL}ECEgV)O59s82=&{F1bow7l8N?+T?Itii}-=Sr0q4*LH(x|^pl#bHQHb;8IG5>0x5}`kB zbmWt!SmEBnhfD(#)y;a;>x?&}uFF2wl2*Q|g~_^uDL?Hr#Ep&_k;nHC!aBp5c4>ne zi9%fe78brQM(#P}FaH;eTR|E0BaFlp$NGv*o$=|g`&Xm6z3Z^@4l5vX&u~il?V)uA zXe>stn$38G?w!tHiW77xnP{;ztv@O%cYoi8JDitt&WBFuRmh5whqP#7?cblD zzVyC26x{#UF;7z(fz>(_XsObzOz6T*f4SCUJ<*8onsZL*V)|C)_xwlW+?EAVyMvIu z$18C<|5!UK7QIcjyLi6OZ+%PEoxlArp4@W0d#?s+H6({`TwyoV4v6z3Ao4ir|rw>tZA1wXz39fiud&f@3WZ5Pn|=o}cnX7K_%eIx(V#K(=u z0K#v`>}6-{*@Et`$Mx~+39wye$gx9t73@8M-qm04$LsE60HCS)&u$t+ekl`$lWcUQ z0ULUGhzwB$knQ=+*3Z=fly9a$Ps+SB6W#lk6j1k4#z|sq!A;!$;#&ZiBeY-g)jw{c z2b@;9(%ksxWxULBCl5Gg9?|)aFh%p%*Vhjb+t-?o{11rezi25Bzk}-2(=QjfKF*XH z)fXIxc0~PphGfsx3l=cK%jDf({BKBYN3C8*Eo3_E9Jc&<>l9@-zu|PDqVn&2JXoCG zpy|(o>tSFV5u-NQe!kzaxVe2!b99N$$1tV_L^|<@1@n*g5q4jV8XVuUk27{ez`ccL5W;G^fe<`)759KputV<`+ zbMZLo?A^SvuSd1QcJ)Z}+a2v$m(Y|)n+cCbxiU$2y(TvD%+_eD2)ul@e*#}782%Cx0Y9Gl!r*Jyu7ts%IU>)#_LlQ9sTkyK{`v zXLR{vwU3(^y{Y=*9mSG9Y*{%DXUgGD5G}G<+j{+t53$$!82W@Rds$%U*LhDSBg)a5 z)LbB%qip@y%lu;U@AgM8u_gO>tBV5V&1O8+-0zC>Qk4AUUZZ@ai`&RGrQTDW7APSS zo0;(lE3!x;eu|}?@)rq zBWgv*uYX}JW?5@zlb(0=-PM16WJPv$Z4{y~-+pq;O6UxO>4dnxlamXKeL{?3Hz)3S z#l30BBiQgj|9HgUz2l*8(z5jO%eZ1dy*EN!x)UvhIdy)#Vu4nTx;`#QB}1d}-$zbl zmz8~MpE<^XazUHX>1iL#*=*0wPkkDquJw@@+4YVmU>?Z&teF%?r~Ye&5h>Wssnkv% zgC?bsOGj5PQz+!Y)u;Jm!PvXPj1=^rmLV}DwfZF`Rg9K03hYSKT2?=%!e(Y#gINxW z!Mxd6j+Y7${>B|ScyF0jDY%J2E`kEEs}9o}5-8l8WgjaC4G1sZK^7=b=En7L`MKWV z1%UTi!Nb?A?b4x}Z0mxhWRPN7l7$%b#?lofYe$`FNZWlXELX_1)-f$EsI`opL{c_} z#VH1j6LX&a@%6w=&p0!SyBX-&`2B#cgavj~TY_?MHhcWGFO?Cg{)-B`ShyF1*Fcy% zF#^}25!<1l+TyRcG)UF`$nYJ7d$GB-n+-aGz!QyO74hwLyS=bHd;xgE0wj~e{IkxP z)OJ#b(gMaAfQFj{eP@TGe8kyDR7L_dFi3^n|KS)AYRU`4ztIE2St7RYLYHZA;8}qj z8GhMp9ecN#{ff^N1>kvaxTvc4w(LzQ8o=;YggC-P?&g^5@mF>TFw2NkEyTXd?QTE} z3S0psmB?}{Cn`cmmlyrK%)y?EJC$OXITkg$&3i-I$|G!c~E6*-*fwn=G`3Zmy8UC*pghPY% zH3;}$%_J#@q#L>dqx{h8HTp(FpA6p*&IhM{*5#b6#cL!`4WHaAxDUz~jG*__7=~nI z3-{B&Z;BA1D_vUh#z506Xq%g9RAx*=j zJGph~*wCkDU{##{( + + + """, + "class": "", + }, + ], # Toc options - "collapse_navigation": True, - "sticky_navigation": False, - "navigation_depth": 4, - "includehidden": True, - "titles_only": False, } +# -- Options for Markdown files ---------------------------------------------- +# + +myst_enable_extensions = [ + "colon_fence", + "deflist", +] +myst_heading_anchors = 3 + +# -- Custom css +html_css_files = [ + "css/custom.css", +] + +html_static_path = ["_static"] + +add_module_names = False # Class names without full module path + # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] + +copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " +copybutton_prompt_is_regexp = True diff --git a/doc/content/encodings/index.md b/doc/content/encodings/index.md new file mode 100644 index 0000000..2b8e2f8 --- /dev/null +++ b/doc/content/encodings/index.md @@ -0,0 +1,7 @@ +# ASP Encodings + +Descriptions of ASP encodings. + +```{toctree} +instance.md +``` diff --git a/doc/content/encodings/instance.md b/doc/content/encodings/instance.md new file mode 100644 index 0000000..f325dd4 --- /dev/null +++ b/doc/content/encodings/instance.md @@ -0,0 +1,32 @@ +# Instance + +## Constants +```{list-table} +:header-rows: 1 +:widths: 25 100 + +* - Name + - Description +* - `horizon` + - The description of a constant. +``` + +## Predicates + +### `predicate(X,Y)` + +Description + +```{admonition} Example +```prolog +predicate(1,2). +``` + +### `another_predicate(X)` + +Description + +```{admonition} Example +```prolog +another_predicate(1). +``` diff --git a/doc/content/installation.md b/doc/content/installation.md new file mode 100644 index 0000000..39da015 --- /dev/null +++ b/doc/content/installation.md @@ -0,0 +1,41 @@ +# Installation + +clingexplaid requires Python 3.8+. We recommend version 3.10. + +You can check a successful installation by running + +```console +$ clingexplaid -h +``` + +## Installing with pip + + +The python clingexplaid package can be found [here](https://github.com/krr-up/clingo-explaid). + +```console +$ pip install clingexplaid +``` + +## Development + +### Installing from source + +The project is hosted on [github](https://github.com/krr-up/clingo-explaid) and can +also be installed from source. + +```{warning} +We recommend this only for development purposes. +``` + +```{note} +The `setuptools` package is required to run the commands below. +``` + +Execute the following command in the top level clingexplaid directory: + +```console +$ git clone https://github.com/krr-up/clingo-explaid +$ cd clingexplaid +$ pip install -e .[all] +``` diff --git a/doc/content/quickstart.md b/doc/content/quickstart.md new file mode 100644 index 0000000..26bd656 --- /dev/null +++ b/doc/content/quickstart.md @@ -0,0 +1,11 @@ +# Quick start + +A simple explanation on how to use the system. + +```console +$ clingexplaid -h +``` + +```{tip} +Tips on how to use the system. +``` diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 0000000..5971dba --- /dev/null +++ b/doc/index.md @@ -0,0 +1,9 @@ +# clingexplaid + +An example project template. + +```{toctree} +content/installation.md +content/quickstart.md +content/encodings/index.md +``` diff --git a/doc/index.rst b/doc/index.rst deleted file mode 100644 index ab7a307..0000000 --- a/doc/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -The clingexplaid project -==================== - -An example project template. - -.. autosummary:: - :toctree: _autosummary - :recursive: - - clingexplaid diff --git a/experiments/asp_appraoch/README.md b/experiments/asp_appraoch/README.md deleted file mode 100644 index 3ccbb4a..0000000 --- a/experiments/asp_appraoch/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# How to apply - -1. Convert your given unsatisifable input program in the way shown in `example.multi_muc.lp` and `example.multi_muc.converted.lp` - + Here every fact that should be an assumption is tranformed in the following way: - -Original: -``` -a. -``` - -Transformed: -``` -{selected(a)}. -a :- selected(a). - -#show selected/1. -``` - -2. Reify this transformed input program - + This could be done like this: - -```bash -clingo test.head_disjunction.converted.lp --output=reify > test.head_disjunction.converted.reified.lp -``` - -3. Call the reified input program together with the meta encoding for finding Minimal Unsatisifiable Cores - + important here are the flags `--heuristic=Domain` and `--enum-mode=domRec` for clingo - -```bash -clingo 0 meta_encoding.unsat.lp test.head_disjunction.converted.reified.lp --heuristic=Domain --enum-mode=domRec -``` - -# TODOs: - -+ [ ] Implement/modify a transformer that does the fact transformation diff --git a/experiments/asp_appraoch/example.multi_muc.converted.lp b/experiments/asp_appraoch/example.multi_muc.converted.lp deleted file mode 100644 index 7a5d5ce..0000000 --- a/experiments/asp_appraoch/example.multi_muc.converted.lp +++ /dev/null @@ -1,20 +0,0 @@ -% Transformed Program - -{selected(a)}. -{selected(b)}. -{selected(c)}. -{selected(d)}. - -a :- selected(a). -b :- selected(b). -c :- selected(c). -d :- selected(d). - - -{x;y}. - -:- b, c. -:- a. -:- c, d. - -#show selected/1. diff --git a/experiments/asp_appraoch/example.multi_muc.converted.reified.lp b/experiments/asp_appraoch/example.multi_muc.converted.reified.lp deleted file mode 100644 index 2fdeeff..0000000 --- a/experiments/asp_appraoch/example.multi_muc.converted.reified.lp +++ /dev/null @@ -1,53 +0,0 @@ -atom_tuple(0). -atom_tuple(0,1). -literal_tuple(0). -rule(choice(0),normal(0)). -atom_tuple(1). -atom_tuple(1,2). -literal_tuple(1). -literal_tuple(1,1). -rule(disjunction(1),normal(1)). -atom_tuple(2). -atom_tuple(2,3). -rule(choice(2),normal(0)). -atom_tuple(3). -atom_tuple(3,4). -literal_tuple(2). -literal_tuple(2,3). -rule(disjunction(3),normal(2)). -atom_tuple(4). -literal_tuple(3). -literal_tuple(3,2). -literal_tuple(3,4). -rule(disjunction(4),normal(3)). -atom_tuple(5). -atom_tuple(5,5). -rule(choice(5),normal(0)). -atom_tuple(6). -atom_tuple(6,6). -literal_tuple(4). -literal_tuple(4,5). -rule(disjunction(6),normal(4)). -literal_tuple(5). -literal_tuple(5,6). -rule(disjunction(4),normal(5)). -atom_tuple(7). -atom_tuple(7,7). -rule(choice(7),normal(0)). -atom_tuple(8). -atom_tuple(8,8). -literal_tuple(6). -literal_tuple(6,7). -rule(disjunction(8),normal(6)). -literal_tuple(7). -literal_tuple(7,4). -literal_tuple(7,8). -rule(disjunction(4),normal(7)). -atom_tuple(9). -atom_tuple(9,9). -atom_tuple(9,10). -rule(choice(9),normal(0)). -output(selected(d),1). -output(selected(c),2). -output(selected(a),4). -output(selected(b),6). diff --git a/experiments/asp_appraoch/example.multi_muc.converted.reified.v2.lp b/experiments/asp_appraoch/example.multi_muc.converted.reified.v2.lp deleted file mode 100644 index 55b0336..0000000 --- a/experiments/asp_appraoch/example.multi_muc.converted.reified.v2.lp +++ /dev/null @@ -1,85 +0,0 @@ -atom_tuple(0). -atom_tuple(0,1). -literal_tuple(0). -rule(disjunction(0),normal(0)). -atom_tuple(1). -atom_tuple(1,2). -rule(disjunction(1),normal(0)). -atom_tuple(2). -atom_tuple(2,3). -rule(disjunction(2),normal(0)). -atom_tuple(3). -atom_tuple(3,4). -rule(disjunction(3),normal(0)). -atom_tuple(4). -atom_tuple(4,5). -rule(choice(4),normal(0)). -atom_tuple(5). -atom_tuple(5,6). -rule(choice(5),normal(0)). -atom_tuple(6). -literal_tuple(1). -literal_tuple(1,5). -literal_tuple(1,6). -rule(disjunction(6),normal(1)). -atom_tuple(7). -atom_tuple(7,7). -rule(choice(7),normal(0)). -literal_tuple(2). -literal_tuple(2,7). -rule(disjunction(6),normal(2)). -atom_tuple(8). -atom_tuple(8,8). -rule(choice(8),normal(0)). -literal_tuple(3). -literal_tuple(3,6). -literal_tuple(3,8). -rule(disjunction(6),normal(3)). -atom_tuple(9). -atom_tuple(9,9). -atom_tuple(9,10). -rule(choice(9),normal(0)). -atom_tuple(10). -atom_tuple(10,11). -literal_tuple(4). -literal_tuple(4,5). -rule(disjunction(10),normal(4)). -atom_tuple(11). -atom_tuple(11,12). -literal_tuple(5). -literal_tuple(5,6). -rule(disjunction(11),normal(5)). -atom_tuple(12). -atom_tuple(12,13). -literal_tuple(6). -literal_tuple(6,8). -rule(disjunction(12),normal(6)). -atom_tuple(13). -atom_tuple(13,14). -rule(disjunction(13),normal(2)). -output(a,2). -output(b,6). -output(c,5). -output(d,4). -literal_tuple(7). -literal_tuple(7,11). -output(_muc(d),7). -literal_tuple(8). -literal_tuple(8,12). -output(_muc(c),8). -literal_tuple(9). -literal_tuple(9,13). -output(_muc(b),9). -literal_tuple(10). -literal_tuple(10,14). -output(_muc(a),10). -literal_tuple(11). -literal_tuple(11,9). -output(x,11). -literal_tuple(12). -literal_tuple(12,10). -output(y,12). -output(_assumption(a),0). -output(_assumption(b),0). -output(_assumption(c),0). -output(_assumption(d),0). diff --git a/experiments/asp_appraoch/example.multi_muc.converted.v2.lp b/experiments/asp_appraoch/example.multi_muc.converted.v2.lp deleted file mode 100644 index f06f2d5..0000000 --- a/experiments/asp_appraoch/example.multi_muc.converted.v2.lp +++ /dev/null @@ -1,20 +0,0 @@ -% Transformed Program - -_assumption(a). -_assumption(b). -_assumption(c). -_assumption(d). - -{a}. {b}. {c}. {d}. - -_muc(a) :- a. -_muc(b) :- b. -_muc(c) :- c. -_muc(d) :- d. - -{x;y}. - -:- b, c. -:- a. -:- c, d. - diff --git a/experiments/asp_appraoch/example.multi_muc.lp b/experiments/asp_appraoch/example.multi_muc.lp deleted file mode 100644 index 448d77c..0000000 --- a/experiments/asp_appraoch/example.multi_muc.lp +++ /dev/null @@ -1,15 +0,0 @@ -% Transformed Program - -a. b. c. d. - -test(1..k). - -{x;y}. - -:- b, c. -:- a. -:- c, d. - -:- test(1). - -test_out(X,Y) :- test(X), test(Y). \ No newline at end of file diff --git a/experiments/asp_appraoch/meta_encoding.unsat.lp b/experiments/asp_appraoch/meta_encoding.unsat.lp deleted file mode 100644 index 48be71b..0000000 --- a/experiments/asp_appraoch/meta_encoding.unsat.lp +++ /dev/null @@ -1,38 +0,0 @@ -% CLASSIC KRR META ENCODING : -% - Modified for finding all Minimal Unsatisfiable Cores of an unsatisfiable program - -conjunction(B) :- literal_tuple(B), - hold(L): literal_tuple(B,L), L>0; - not hold(L): literal_tuple(B,-L), L>0. - -body(normal(B)) :- rule(_,normal(B)), conjunction(B). -body(sum(B,G)) :- rule(_,sum(B,G)), - #sum { - W,L : hold(L), weighted_literal_tuple(B,L,W), L>0; - W,L : not hold(L), weighted_literal_tuple(B,-L,W), L>0 - } >= G . - -% hold(A): atom_tuple(H,A) :- rule(disjunction(H),B), body(B). -% Splitting this rule into two cases: -% 1. the body of a rule holds but the rule has not head atoms (integrity constraint) -% - an unsat atom containing the body of the rule is created -% 2. the body of a rule holds but the rule has at least one head atom -% - the classic way of handeling disjunctive heads is applied (at least one of the head atoms has to be true) -unsat(B) :- rule(disjunction(H),B), body(B), not atom_tuple(H,_). -hold(A): atom_tuple(H,A) :- rule(disjunction(H),B), body(B), atom_tuple(H,_). - -% get the ids of the literals representing the selected atoms from the output -selected_ID(ID) :- output(selected(_),N), literal_tuple(N,ID). - -% modified the hold choice rule to infer choice_hold(Atom_ID) -{ choice_hold(A): atom_tuple(H,A) } :- rule(choice(H),B), body(B). -% for every choice hold infer a real hold -hold(X) :- choice_hold(X). -% add heuristic false for every choice hold that was infered by a choice rule which contained selected(X) (done by comparing with selected_ID(A)) -#heuristic choice_hold(A): selected_ID(A). [1,false] -:- not unsat(_). - -#show. -% #show unsat/1. -% #show choice_hold/2. -#show T: output(T,B), conjunction(B). diff --git a/experiments/asp_appraoch/meta_encoding.unsat.v2.lp b/experiments/asp_appraoch/meta_encoding.unsat.v2.lp deleted file mode 100644 index 194c6a7..0000000 --- a/experiments/asp_appraoch/meta_encoding.unsat.v2.lp +++ /dev/null @@ -1,36 +0,0 @@ -% CLASSIC KRR META ENCODING : -% - Modified for finding all Minimal Unsatisfiable Cores of an unsatisfiable program - -conjunction(B) :- literal_tuple(B), - hold(L): literal_tuple(B,L), L>0; - not hold(L): literal_tuple(B,-L), L>0. - -body(normal(B)) :- rule(_,normal(B)), conjunction(B). -body(sum(B,G)) :- rule(_,sum(B,G)), - #sum { - W,L : hold(L), weighted_literal_tuple(B,L,W), L>0; - W,L : not hold(L), weighted_literal_tuple(B,-L,W), L>0 - } >= G . - -% hold(A): atom_tuple(H,A) :- rule(disjunction(H),B), body(B). -% Splitting this rule into two cases: -% 1. the body of a rule holds but the rule has not head atoms (integrity constraint) -% - an unsat atom containing the body of the rule is created -% 2. the body of a rule holds but the rule has at least one head atom -% - the classic way of handeling disjunctive heads is applied (at least one of the head atoms has to be true) -unsat(B) :- rule(disjunction(H),B), body(B), not atom_tuple(H,_). -hold(A): atom_tuple(H,A) :- rule(disjunction(H),B), body(B), atom_tuple(H,_). - - -% modified the hold choice rule to infer choice_hold(Atom_ID) -{ choice_hold(A): atom_tuple(H,A) } :- rule(choice(H),B), body(B). -% for every choice hold infer a real hold -hold(X) :- choice_hold(X). -% add heuristic false for every choice hold (This works in experiments but might have an undiscovered edge case) -#heuristic choice_hold(_).[1,false] -:- not unsat(_). - -#show. -% #show unsat/1. -% #show choice_hold/2. -#show muc(T): output(_muc(T),B), conjunction(B). diff --git a/init.py b/init.py deleted file mode 100755 index be05cc6..0000000 --- a/init.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python3 -""" -Init script to rename project. -""" - -import os -import re - - -def read(prompt, regex): - """ - Read a string from command line. - - The string has to match the given regular expression. - """ - while True: - ret = input(prompt) - match = re.match(regex, ret) - if match is not None: - return ret - print(f"the project name has to match the regular expression: {regex}") - - -def main(): - """ - Rename the project. - """ - project = read("project name: ", r"^[a-z][a-z0-9_]*$") - author = read("author: ", r".+") - email = read("email: ", r".+") - - replacements = { - "fillname": project, - "": email, - "": author, - } - - def replace(filepath): - with open(filepath, "r", encoding="utf-8") as hnd: - content = hnd.read() - for key, val in replacements.items(): - content = content.replace(key, val) - with open(filepath, "w", encoding="utf-8") as hnd: - hnd.write(content) - - for rootpath in [os.path.join("src", "fillname"), "tests"]: - for dirpath, _, filenames in os.walk(rootpath): - for filename in filenames: - if not filename.endswith(".py"): - continue - filepath = os.path.join(dirpath, filename) - replace(filepath) - - for filepath in [ - "setup.cfg", - "noxfile.py", - "README.md", - "doc/index.rst", - ".pre-commit-config.yaml", - ".coveragerc", - ]: - replace(filepath) - - os.rename(os.path.join("src", "fillname"), os.path.join("src", project)) - - -if __name__ == "__main__": - main() diff --git a/noxfile.py b/noxfile.py index 386cbcc..0767129 100644 --- a/noxfile.py +++ b/noxfile.py @@ -2,87 +2,91 @@ import nox -nox.options.sessions = "lint_flake8", "lint_pylint", "typecheck", "test" +nox.options.sessions = "lint_pylint", "typecheck", "test" EDITABLE_TESTS = True PYTHON_VERSIONS = None if "GITHUB_ACTIONS" in os.environ: - PYTHON_VERSIONS = ["3.7", "3.11"] + PYTHON_VERSIONS = ["3.9", "3.11"] EDITABLE_TESTS = False -@nox.session -def format(session): - session.install("-e", ".[format]") - check = "check" in session.posargs - - autoflake_args = [ - "--in-place", - "--imports=clingexplaid", - "--ignore-init-module-imports", - "--remove-unused-variables", - "-r", - "src", - "tests", - ] - if check: - autoflake_args.remove("--in-place") - session.run("autoflake", *autoflake_args) - - isort_args = ["--profile", "black", "src", "tests"] - if check: - isort_args.insert(0, "--check") - isort_args.insert(1, "--diff") - session.run("isort", *isort_args) - - black_args = ["src", "tests"] - if check: - black_args.insert(0, "--check") - black_args.insert(1, "--diff") - session.run("black", *black_args) - - @nox.session def doc(session): + """ + Build the documentation. + + Accepts the following arguments: + - open: open documentation after build + - clean: clean up the build folder + - : build the given with the given + """ target = "html" options = [] + open_doc = "open" in session.posargs + clean = "clean" in session.posargs + + if open_doc: + session.posargs.remove("open") + if clean: + session.posargs.remove("clean") + if session.posargs: target = session.posargs[0] options = session.posargs[1:] session.install("-e", ".[doc]") session.cd("doc") + if clean: + session.run("rm", "-rf", "_build") session.run("sphinx-build", "-M", target, ".", "_build", *options) + if open_doc: + session.run("open", "_build/html/index.html") @nox.session -def lint_flake8(session): - session.install("-e", ".[lint_flake8]") - session.run("flake8", "src", "tests") +def dev(session): + """ + Create a development environment in editable mode. + + Activate it by running `source .nox/dev/bin/activate`. + """ + session.install("-e", ".[dev]") @nox.session def lint_pylint(session): + """ + Run pylint. + """ session.install("-e", ".[lint_pylint]") session.run("pylint", "clingexplaid", "tests") @nox.session def typecheck(session): + """ + Typecheck the code using mypy. + """ session.install("-e", ".[typecheck]") - session.run("mypy", "-p", "src.clingexplaid", "-p", "tests", "--namespace-packages", "--ignore-missing-imports") + session.run("mypy", "--strict", "-p", "clingexplaid", "-p", "tests") @nox.session(python=PYTHON_VERSIONS) def test(session): - args = ['.[test]'] - if EDITABLE_TESTS: - args.insert(0, '-e') - session.install(*args) - session.run("coverage", "run", "-m", "unittest", "discover", "-v") - session.run("coverage", "report", "-m", "--fail-under=100") + """ + Run the tests. + Accepts an additional arguments which are passed to the unittest module. + This can for example be used to selectively run test cases. + """ -@nox.session -def dev(session): - session.install("-e", ".[dev]") + args = [".[test]"] + if EDITABLE_TESTS: + args.insert(0, "-e") + session.install(*args) + if session.posargs: + session.run("coverage", "run", "-m", "unittest", session.posargs[0], "-v") + else: + session.run("coverage", "run", "-m", "unittest", "discover", "-v") + session.run("coverage", "report", "-m", "--fail-under=100") diff --git a/pyproject.toml b/pyproject.toml index d214b53..cc2125b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,3 +4,79 @@ requires = [ "setuptools-scm", ] build-backend = "setuptools.build_meta" + +[project] +name = "clingexplaid" +authors = [ + { name = "Hannes Weichelt", email = "hweichelt@uni-potsdam.de" } +] +description = "A template project." +requires-python = ">=3.9" +license = {file = "LICENSE"} +dynamic = [ "version" ] +readme = "README.md" +dependencies = [ + "clingo>=5.6.0", + "autoflake", +] + +[project.urls] +Homepage = "https://github.com/krr-up/clingo-explaid" + +[project.optional-dependencies] +format = [ "black", "isort", "autoflake" ] +lint_pylint = [ "pylint" ] +typecheck = [ "types-setuptools", "mypy" ] +test = [ "coverage[toml]" ] +doc = [ "sphinx", "furo", "nbsphinx", "sphinx_copybutton", "myst-parser" ] +dev = [ "clingexplaid[test,typecheck,lint_pylint]" ] + +[project.scripts] +clingexplaid = "clingexplaid.__main__:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools_scm] +version_scheme = "python-simplified-semver" +local_scheme = "no-local-version" + +[tool.isort] +profile = "black" +line_length = 120 + +[tool.black] +line-length = 120 + +[tool.pylint.format] +max-line-length = 120 + +[tool.pylint.design] +max-args = 10 +max-attributes = 7 +max-bool-expr = 5 +max-branches = 12 +max-locals = 30 +max-parents = 7 +max-public-methods = 20 +max-returns = 10 +max-statements = 50 +min-public-methods = 1 + +[tool.pylint.similarities] +ignore-comments = true +ignore-docstrings = true +ignore-imports = true +ignore-signatures = true + +[tool.pylint.basic] +argument-rgx = "^[a-z][a-z0-9]*((_[a-z0-9]+)*_?)?$" +variable-rgx = "^[a-z][a-z0-9]*((_[a-z0-9]+)*_?)?$" +good-names = ["_"] + +[tool.coverage.run] +source = ["clingexplaid", "tests"] +omit = ["*/clingexplaid/__main__.py"] + +[tool.coverage.report] +exclude_lines = ["assert", "nocoverage"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7d2a6f6..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -cffi==1.15.1 -clingo==5.6.2 -clingox==1.2.0 -pycparser==2.21 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d1b3c83..0000000 --- a/setup.cfg +++ /dev/null @@ -1,51 +0,0 @@ -[metadata] -name = clingexplaid -version = 1.0.1 -author = Hannes Weichelt -author_email = weichelt.h@uni-potsdam.de -description = Tools for Explaination with clingo. -long_description = file: README.md -long_description_content_type = text/markdown -license = MIT -url = https://github.com/krr-up/clingo-explaid - -[options] -packages = find: -package_dir = - =src -include_package_data = True -install_requires = - importlib_metadata;python_version<'3.8' - clingo>=5.6.0 - clingox>=1.2.0 - autoflake - -[options.packages.find] -where = src - -[options.extras_require] -format = - black - isort - autoflake -lint_flake8 = - flake8 - flake8-black - flake8-isort -lint_pylint = - pylint -typecheck = - types-setuptools - mypy -test = - coverage -doc = - sphinx - sphinx_rtd_theme - nbsphinx -dev = - clingexplaid[test,typecheck,lint_pylint,lint_flake8] - -[options.entry_points] -console_scripts = - clingexplaid = clingexplaid.__main__:main diff --git a/setup.py b/setup.py deleted file mode 100644 index 9ab6a32..0000000 --- a/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -This is provided for compatibility. -""" -from setuptools import setup - -setup() diff --git a/src/clingexplaid/__init__.py b/src/clingexplaid/__init__.py index e69de29..b0cbfd4 100644 --- a/src/clingexplaid/__init__.py +++ b/src/clingexplaid/__init__.py @@ -0,0 +1,3 @@ +""" +The clingexplaid project. +""" diff --git a/src/clingexplaid/__main__.py b/src/clingexplaid/__main__.py index aeec471..20ddabf 100644 --- a/src/clingexplaid/__main__.py +++ b/src/clingexplaid/__main__.py @@ -6,15 +6,17 @@ from clingo.application import clingo_main -from clingexplaid.utils.cli import ClingoExplaidApp +from .utils.logging import configure_logging, get_logger +from .utils.parser import get_parser +from .utils.cli import ClingoExplaidApp -def main(): +def main() -> None: """ - Main function calling the application class + Run the main function. """ + clingo_main(ClingoExplaidApp(sys.argv[0]), sys.argv[1:] + ["-V0"]) - sys.exit() if __name__ == "__main__": diff --git a/src/clingexplaid/py.typed b/src/clingexplaid/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/clingexplaid/utils/__init__.py b/src/clingexplaid/utils/__init__.py index 88655a6..ecb64aa 100644 --- a/src/clingexplaid/utils/__init__.py +++ b/src/clingexplaid/utils/__init__.py @@ -1,5 +1,5 @@ """ -Utilities +Utilities. """ import re @@ -8,6 +8,7 @@ import clingo from clingo.ast import ASTType + SymbolSet = Set[clingo.Symbol] Literal = Tuple[clingo.Symbol, bool] LiteralSet = Set[Literal] diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/utils/cli.py index 78b95c2..a354ea7 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/utils/cli.py @@ -8,14 +8,13 @@ import clingo from clingo.application import Application, Flag -from .logger import BACKGROUND_COLORS, COLORS +from .logging import BACKGROUND_COLORS, COLORS from .muc import CoreComputer from .propagators import DecisionOrderPropagator from .transformer import AssumptionTransformer, OptimizationRemover from .unsat_constraints import UnsatConstraintComputer -from ..utils import ( +from . import ( get_solver_literal_lookup, - get_signatures_from_model_string, get_constants_from_arguments, ) @@ -60,7 +59,7 @@ def _initialize(self) -> None: if len(self.methods) == 0: raise ValueError( - f"Clingexplaid was called without any method, pleas select at least one of the following methods: " + f"Clingexplaid was called without any method, please select at least one of the following methods: " f"[{', '.join(['--' + str(m) for m in self.CLINGEXPLAID_METHODS.keys()])}]" ) diff --git a/src/clingexplaid/utils/logger.py b/src/clingexplaid/utils/logger.py deleted file mode 100644 index 66e97eb..0000000 --- a/src/clingexplaid/utils/logger.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -Setup project wide loggers. -""" - -import logging -import sys - -COLORS = { - "GREY": "\033[90m", - "BLUE": "\033[94m", - "DARK_BLUE": "\033[34m", - "GREEN": "\033[92m", - "YELLOW": "\033[93m", - "RED": "\033[91m", - "DARK_RED": "\033[91m", - "NORMAL": "\033[0m", - "BLACK": "\033[30m", -} - -BACKGROUND_COLORS = { - "BLUE": "\033[44m", - "LIGHT_BLUE": "\033[104m", - "RED": "\033[41m", - "WHITE": "\033[107m", - "GREY": "\033[100m", - "LIGHT-GREY": "\033[47m", -} - - -class SingleLevelFilter(logging.Filter): - """ - Filter levels. - """ - - def __init__(self, passlevel, reject): - # pylint: disable=super-init-not-called - self.passlevel = passlevel - self.reject = reject - - def filter(self, record): - if self.reject: - return record.levelno != self.passlevel # nocoverage - - return record.levelno == self.passlevel - - -def setup_logger(name, level): - """ - Setup logger. - """ - - logger = logging.getLogger(name) - logger.propagate = False - logger.setLevel(level) - log_message_str = "{}%(levelname)s:{} - %(message)s{}" - - def set_handler(level, color): - handler = logging.StreamHandler(sys.stderr) - handler.addFilter(SingleLevelFilter(level, False)) - handler.setLevel(level) - formatter = logging.Formatter( - log_message_str.format(COLORS[color], COLORS["GREY"], COLORS["NORMAL"]) - ) - handler.setFormatter(formatter) - logger.addHandler(handler) - - set_handler(logging.INFO, "GREEN") - set_handler(logging.WARNING, "YELLOW") - set_handler(logging.DEBUG, "BLUE") - set_handler(logging.ERROR, "RED") - - return logger diff --git a/src/clingexplaid/utils/logging.py b/src/clingexplaid/utils/logging.py new file mode 100644 index 0000000..1798999 --- /dev/null +++ b/src/clingexplaid/utils/logging.py @@ -0,0 +1,90 @@ +""" +Setup project wide loggers. + +This is a thin wrapper around Python's logging module. It supports colored +logging. +""" + +import logging +from typing import TextIO + +NOTSET = logging.NOTSET +DEBUG = logging.DEBUG +INFO = logging.INFO +WARNING = logging.WARNING +ERROR = logging.ERROR +CRITICAL = logging.CRITICAL + +COLORS = { + "GREY": "\033[90m", + "BLUE": "\033[94m", + "GREEN": "\033[92m", + "YELLOW": "\033[93m", + "RED": "\033[91m", + "NORMAL": "\033[0m", + "DARK_RED": "\033[91m", + "BLACK": "\033[30m", +} + +BACKGROUND_COLORS = { + "BLUE": "\033[44m", + "LIGHT_BLUE": "\033[104m", + "RED": "\033[41m", + "WHITE": "\033[107m", + "GREY": "\033[100m", + "LIGHT-GREY": "\033[47m", +} + + +class SingleLevelFilter(logging.Filter): + """ + Filter levels. + """ + + passlevel: int + reject: bool + + def __init__(self, passlevel: int, reject: bool): + # pylint: disable=super-init-not-called + self.passlevel = passlevel + self.reject = reject + + def filter(self, record: logging.LogRecord) -> bool: + if self.reject: + return record.levelno != self.passlevel # nocoverage + + return record.levelno == self.passlevel + + +def configure_logging(stream: TextIO, level: int, use_color: bool) -> None: + """ + Configure application logging. + """ + + def format_str(color: str) -> str: + if use_color: + return f"{COLORS[color]}%(levelname)s:{COLORS['GREY']} - %(message)s{COLORS['NORMAL']}" + return "%(levelname)s: - %(message)s" # nocoverage + + def make_handler(level: int, color: str) -> "logging.StreamHandler[TextIO]": + handler = logging.StreamHandler(stream) + handler.addFilter(SingleLevelFilter(level, False)) + handler.setLevel(level) + formatter = logging.Formatter(format_str(color)) + handler.setFormatter(formatter) + return handler + + handlers = [ + make_handler(logging.INFO, "GREEN"), + make_handler(logging.WARNING, "YELLOW"), + make_handler(logging.DEBUG, "BLUE"), + make_handler(logging.ERROR, "RED"), + ] + logging.basicConfig(handlers=handlers, level=level) + + +def get_logger(name: str) -> logging.Logger: + """ + Get a logger with the given name. + """ + return logging.getLogger(name) diff --git a/src/clingexplaid/utils/logic_programs/asp_approach.lp b/src/clingexplaid/utils/logic_programs/asp_approach.lp deleted file mode 100644 index 89b4341..0000000 --- a/src/clingexplaid/utils/logic_programs/asp_approach.lp +++ /dev/null @@ -1,38 +0,0 @@ -% CLASSIC KRR META ENCODING : -% - Modified for finding all Minimal Unsatisfiable Cores of an unsatisfiable program - -conjunction(B) :- literal_tuple(B), - hold(L): literal_tuple(B,L), L>0; - not hold(L): literal_tuple(B,-L), L>0. - -body(normal(B)) :- rule(_,normal(B)), conjunction(B). -body(sum(B,G)) :- rule(_,sum(B,G)), - #sum { - W,L : hold(L), weighted_literal_tuple(B,L,W), L>0; - W,L : not hold(L), weighted_literal_tuple(B,-L,W), L>0 - } >= G . - -% hold(A): atom_tuple(H,A) :- rule(disjunction(H),B), body(B). -% Splitting this rule into two cases: -% 1. the body of a rule holds but the rule has not head atoms (integrity constraint) -% - an unsat atom containing the body of the rule is created -% 2. the body of a rule holds but the rule has at least one head atom -% - the classic way of handeling disjunctive heads is applied (at least one of the head atoms has to be true) -unsat(B) :- rule(disjunction(H),B), body(B), not atom_tuple(H,_). -hold(A): atom_tuple(H,A) :- rule(disjunction(H),B), body(B), atom_tuple(H,_). - - -% modified the hold choice rule to infer choice_hold(Atom_ID) -{ choice_hold(A): atom_tuple(H,A) } :- rule(choice(H),B), body(B). -% for every choice hold infer a real hold -hold(X) :- choice_hold(X). -% add heuristic false for every choice hold (This works in experiments but might have an undiscovered edge case) -#heuristic choice_hold(_).[1,false] -:- not unsat(_). - -#show. -% #show unsat/1. -% #show choice_hold/2. -#show muc(T): output(_muc(T),B), conjunction(B). - -assumption_hold(T) :- output(_muc(T),B), conjunction(B). \ No newline at end of file diff --git a/src/clingexplaid/utils/muc.py b/src/clingexplaid/utils/muc.py index bd9e1a4..ab5ee01 100644 --- a/src/clingexplaid/utils/muc.py +++ b/src/clingexplaid/utils/muc.py @@ -1,7 +1,6 @@ """ Unsatisfiable Core Utilities """ -import time from typing import Optional, Set, Tuple from itertools import chain, combinations diff --git a/src/clingexplaid/utils/parser.py b/src/clingexplaid/utils/parser.py index 674aadf..ab1aa0e 100644 --- a/src/clingexplaid/utils/parser.py +++ b/src/clingexplaid/utils/parser.py @@ -2,19 +2,21 @@ The command line parser for the project. """ -import logging +import sys from argparse import ArgumentParser from textwrap import dedent -from typing import Any, cast +from typing import Any, Optional, cast -from pkg_resources import DistributionNotFound, require +from . import logging __all__ = ["get_parser"] -try: - VERSION = require("fillname")[0].version -except DistributionNotFound: # nocoverage - VERSION = "local" # nocoverage +if sys.version_info[1] < 8: + import importlib_metadata as metadata # nocoverage +else: + from importlib import metadata # nocoverage + +VERSION = metadata.version("clingexplaid") def get_parser() -> ArgumentParser: @@ -22,15 +24,14 @@ def get_parser() -> ArgumentParser: Return the parser for command line options. """ parser = ArgumentParser( - prog="fillname", + prog="clingexplaid", description=dedent( """\ - fillname + clingexplaid filldescription """ ), ) - levels = [ ("error", logging.ERROR), ("warning", logging.WARNING), @@ -38,7 +39,7 @@ def get_parser() -> ArgumentParser: ("debug", logging.DEBUG), ] - def get(levels, name): + def get(levels: list[tuple[str, int]], name: str) -> Optional[int]: for key, val in levels: if key == name: return val @@ -53,7 +54,5 @@ def get(levels, name): type=cast(Any, lambda name: get(levels, name)), ) - parser.add_argument( - "--version", "-v", action="version", version=f"%(prog)s {VERSION}" - ) + parser.add_argument("--version", "-v", action="version", version=f"%(prog)s {VERSION}") return parser diff --git a/src/clingexplaid/utils/propagators.py b/src/clingexplaid/utils/propagators.py index 2ebd3ab..cdf8436 100644 --- a/src/clingexplaid/utils/propagators.py +++ b/src/clingexplaid/utils/propagators.py @@ -2,7 +2,7 @@ import clingo -from .logger import COLORS +from .logging import COLORS DecisionLevel = List[int] DecisionLevelList = List[DecisionLevel] @@ -138,7 +138,7 @@ def get_decisions(assignment): level_offset_diff = level_offset_end - level_offset_start if level_offset_diff > 1: entailments[decision] = trail[ - (level_offset_start + 1) : level_offset_end + (level_offset_start + 1): level_offset_end ] level += 1 except RuntimeError: diff --git a/src/clingexplaid/utils/transformer.py b/src/clingexplaid/utils/transformer.py index 4d5fbca..9ad3f02 100644 --- a/src/clingexplaid/utils/transformer.py +++ b/src/clingexplaid/utils/transformer.py @@ -8,7 +8,7 @@ import clingo from clingo import ast as _ast -from clingexplaid.utils import match_ast_symbolic_atom_signature +from . import match_ast_symbolic_atom_signature RULE_ID_SIGNATURE = "_rule" REMOVED_TOKEN = "__REMOVED__" diff --git a/src/clingexplaid/utils/unsat_constraints.py b/src/clingexplaid/utils/unsat_constraints.py index aab03e2..a697d07 100644 --- a/src/clingexplaid/utils/unsat_constraints.py +++ b/src/clingexplaid/utils/unsat_constraints.py @@ -3,15 +3,13 @@ """ import re -from difflib import SequenceMatcher -from pathlib import Path from typing import List, Optional, Dict import clingo from clingo.ast import Location from .transformer import ConstraintTransformer, FactTransformer, OptimizationRemover -from ..utils import get_signatures_from_model_string +from . import get_signatures_from_model_string UNSAT_CONSTRAINT_SIGNATURE = "__unsat__" diff --git a/tests/test_main.py b/tests/test_main.py index b3ae8ce..19730b2 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,11 +1,12 @@ """ Test cases for main application functionality. """ -import logging + from io import StringIO from unittest import TestCase -from clingexplaid.utils.logger import setup_logger +from clingexplaid.utils import logging +from clingexplaid.utils.logging import configure_logging, get_logger from clingexplaid.utils.parser import get_parser @@ -14,18 +15,17 @@ class TestMain(TestCase): Test cases for main application functionality. """ - def test_logger(self): + def test_logger(self) -> None: """ Test the logger. """ - log = setup_logger("global", logging.INFO) sio = StringIO() - for handler in log.handlers: - handler.setStream(sio) + configure_logging(sio, logging.INFO, True) + log = get_logger("main") log.info("test123") self.assertRegex(sio.getvalue(), "test123") - def test_parser(self): + def test_parser(self) -> None: """ Test the parser. """ From 358dc82eed69b6b112fc99f19eb5547a6026ddaa Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Tue, 9 Apr 2024 14:16:12 +0200 Subject: [PATCH 68/82] restructuring and reformatting of source code --- examples/misc/sat.lp | 17 + examples/misc/simple.lp | 4 + examples/misc/soup.lp | 3 + examples/misc/test/blob.lp | 3 + examples/misc/test_inert.lp | 1 + src/clingexplaid/__main__.py | 4 +- src/clingexplaid/cli/__init__.py | 0 .../{utils/cli.py => cli/clingo_app.py} | 51 +- src/clingexplaid/muc/__init__.py | 5 + .../{utils/muc.py => muc/core_computer.py} | 37 +- src/clingexplaid/propagators/__init__.py | 10 + src/clingexplaid/propagators/constants.py | 7 + .../propagator_decision_order.py} | 34 +- src/clingexplaid/transformers/__init__.py | 10 + src/clingexplaid/transformers/constants.py | 2 + src/clingexplaid/transformers/exceptions.py | 10 + .../transformers/transformer_assumption.py | 107 ++++ .../transformers/transformer_constraint.py | 68 +++ .../transformers/transformer_fact.py | 67 +++ .../transformer_optimization_remover.py | 52 ++ .../transformers/transformer_rule_id.py | 74 +++ .../transformers/transformer_rule_splitter.py | 96 ++++ .../unsat_constraints/__init__.py | 5 + .../unsat_constraints/constants.py | 1 + .../unsat_constraint_computer.py} | 25 +- src/clingexplaid/utils/transformer.py | 502 ------------------ tests/clingexplaid/test_main.py | 88 +-- 27 files changed, 605 insertions(+), 678 deletions(-) create mode 100644 examples/misc/sat.lp create mode 100644 examples/misc/simple.lp create mode 100644 examples/misc/soup.lp create mode 100644 examples/misc/test/blob.lp create mode 100644 examples/misc/test_inert.lp create mode 100644 src/clingexplaid/cli/__init__.py rename src/clingexplaid/{utils/cli.py => cli/clingo_app.py} (88%) create mode 100644 src/clingexplaid/muc/__init__.py rename src/clingexplaid/{utils/muc.py => muc/core_computer.py} (82%) create mode 100644 src/clingexplaid/propagators/__init__.py create mode 100644 src/clingexplaid/propagators/constants.py rename src/clingexplaid/{utils/propagators.py => propagators/propagator_decision_order.py} (83%) create mode 100644 src/clingexplaid/transformers/__init__.py create mode 100644 src/clingexplaid/transformers/constants.py create mode 100644 src/clingexplaid/transformers/exceptions.py create mode 100644 src/clingexplaid/transformers/transformer_assumption.py create mode 100644 src/clingexplaid/transformers/transformer_constraint.py create mode 100644 src/clingexplaid/transformers/transformer_fact.py create mode 100644 src/clingexplaid/transformers/transformer_optimization_remover.py create mode 100644 src/clingexplaid/transformers/transformer_rule_id.py create mode 100644 src/clingexplaid/transformers/transformer_rule_splitter.py create mode 100644 src/clingexplaid/unsat_constraints/__init__.py create mode 100644 src/clingexplaid/unsat_constraints/constants.py rename src/clingexplaid/{utils/unsat_constraints.py => unsat_constraints/unsat_constraint_computer.py} (86%) delete mode 100644 src/clingexplaid/utils/transformer.py diff --git a/examples/misc/sat.lp b/examples/misc/sat.lp new file mode 100644 index 0000000..a704413 --- /dev/null +++ b/examples/misc/sat.lp @@ -0,0 +1,17 @@ +number(1..4). + +solution(X,Y,V) :- initial(X,Y,V). +{solution(X,Y,N): number(N)}=1 :- number(X) ,number(Y). +cage(X1,Y1,X2,Y2) :- solution(X1,Y1,_), solution(X2,Y2,_), + ((X1-1)/2)==((X2-1)/2), + ((Y1-1)/2)==((Y2-1)/2). + +:- solution(X,Y1,N), solution(X,Y2,N), Y1 != Y2. +:- solution(X1,Y,N), solution(X2,Y,N), X1 != X2. +:- cage(X1,Y1,X2,Y2), solution(X1,Y1,N), solution(X2,Y2,N), X1!=X2, Y1!=Y2. + +initial(1,1,1). +initial(2,2,2). +initial(3,3,3). + +#show solution/3. diff --git a/examples/misc/simple.lp b/examples/misc/simple.lp new file mode 100644 index 0000000..bc2baa3 --- /dev/null +++ b/examples/misc/simple.lp @@ -0,0 +1,4 @@ +a(1..5). +b(5..10). + +:- a(X), b(X). \ No newline at end of file diff --git a/examples/misc/soup.lp b/examples/misc/soup.lp new file mode 100644 index 0000000..5a14062 --- /dev/null +++ b/examples/misc/soup.lp @@ -0,0 +1,3 @@ +#include "test/blob.lp". + +soup(10). \ No newline at end of file diff --git a/examples/misc/test/blob.lp b/examples/misc/test/blob.lp new file mode 100644 index 0000000..f9461b3 --- /dev/null +++ b/examples/misc/test/blob.lp @@ -0,0 +1,3 @@ +#include "../test_inert.lp". + +blob(123). \ No newline at end of file diff --git a/examples/misc/test_inert.lp b/examples/misc/test_inert.lp new file mode 100644 index 0000000..d179321 --- /dev/null +++ b/examples/misc/test_inert.lp @@ -0,0 +1 @@ +blub(42). \ No newline at end of file diff --git a/src/clingexplaid/__main__.py b/src/clingexplaid/__main__.py index 20ddabf..86244c8 100644 --- a/src/clingexplaid/__main__.py +++ b/src/clingexplaid/__main__.py @@ -6,9 +6,7 @@ from clingo.application import clingo_main -from .utils.logging import configure_logging, get_logger -from .utils.parser import get_parser -from .utils.cli import ClingoExplaidApp +from .cli.clingo_app import ClingoExplaidApp def main() -> None: diff --git a/src/clingexplaid/cli/__init__.py b/src/clingexplaid/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/clingexplaid/utils/cli.py b/src/clingexplaid/cli/clingo_app.py similarity index 88% rename from src/clingexplaid/utils/cli.py rename to src/clingexplaid/cli/clingo_app.py index a354ea7..a0f43c4 100644 --- a/src/clingexplaid/utils/cli.py +++ b/src/clingexplaid/cli/clingo_app.py @@ -8,15 +8,15 @@ import clingo from clingo.application import Application, Flag -from .logging import BACKGROUND_COLORS, COLORS -from .muc import CoreComputer -from .propagators import DecisionOrderPropagator -from .transformer import AssumptionTransformer, OptimizationRemover -from .unsat_constraints import UnsatConstraintComputer -from . import ( +from ..muc import CoreComputer +from ..propagators import DecisionOrderPropagator +from ..unsat_constraints import UnsatConstraintComputer +from ..utils import ( get_solver_literal_lookup, get_constants_from_arguments, ) +from ..utils.logging import BACKGROUND_COLORS, COLORS +from ..transformers import AssumptionTransformer, OptimizationRemover HYPERLINK_MASK = "\033]8;{};{}\033\\{}\033]8;;\033\\" @@ -37,8 +37,7 @@ def __init__(self, name): # pylint: disable = unused-argument self.methods = set() self.method_functions = { - m: getattr(self, f'_method_{m.replace("-", "_")}') - for m in self.CLINGEXPLAID_METHODS.keys() + m: getattr(self, f'_method_{m.replace("-", "_")}') for m in self.CLINGEXPLAID_METHODS.keys() } self.method_flags = {m: Flag() for m in self.CLINGEXPLAID_METHODS.keys()} self.argument_constants = dict() @@ -72,9 +71,7 @@ def _parse_signature(signature_string: str) -> Tuple[str, int]: def _parse_assumption_signature(self, assumption_signature: str) -> bool: if not self.method_flags["muc"]: - print( - "PARSE ERROR: The assumption signature option is only available if the flag --muc is enabled" - ) + print("PARSE ERROR: The assumption signature option is only available if the flag --muc is enabled") return False assumption_signature_string = assumption_signature.replace("=", "").strip() try: @@ -144,9 +141,7 @@ def register_options(self, options): def _apply_assumption_transformer( self, signatures: Dict[str, int], files: List[str] ) -> Tuple[str, AssumptionTransformer]: - signature_set = ( - set(self._muc_assumption_signatures.items()) if signatures else None - ) + signature_set = set(self._muc_assumption_signatures.items()) if signatures else None at = AssumptionTransformer(signatures=signature_set) if not files: program_transformed = at.parse_files("-") @@ -155,9 +150,7 @@ def _apply_assumption_transformer( return program_transformed, at def _print_muc(self, muc) -> None: - print( - f"{BACKGROUND_COLORS['BLUE']} MUC {BACKGROUND_COLORS['LIGHT_BLUE']} {self._muc_id} {COLORS['NORMAL']}" - ) + print(f"{BACKGROUND_COLORS['BLUE']} MUC {BACKGROUND_COLORS['LIGHT_BLUE']} {self._muc_id} {COLORS['NORMAL']}") print(f"{COLORS['BLUE']}{muc}{COLORS['NORMAL']}") self._muc_id += 1 @@ -213,9 +206,7 @@ def _method_muc( # Case: Finding multiple MUCs if max_models >= 0: program_unsat = False - with control.solve( - assumptions=list(assumptions), yield_=True - ) as solve_handle: + with control.solve(assumptions=list(assumptions), yield_=True) as solve_handle: if not solve_handle.get().satisfiable: program_unsat = True @@ -249,9 +240,7 @@ def _print_unsat_constraints( ) -> None: if prefix is None: prefix = "" - print( - f"{prefix}{BACKGROUND_COLORS['RED']} Unsat Constraints {COLORS['NORMAL']}" - ) + print(f"{prefix}{BACKGROUND_COLORS['RED']} Unsat Constraints {COLORS['NORMAL']}") for cid, constraint in unsat_constraints.items(): location = ucc.get_constraint_location(cid) relative_file_path = location.begin.filename @@ -259,9 +248,7 @@ def _print_unsat_constraints( line_beginning = location.begin.line line_end = location.end.line line_string = ( - f"Line {line_beginning}" - if line_beginning == line_end - else f"Lines {line_beginning}-{line_end}" + f"Line {line_beginning}" if line_beginning == line_end else f"Lines {line_beginning}-{line_end}" ) file_link = "file://" + absolute_file_path if " " in absolute_file_path: @@ -286,19 +273,13 @@ def _method_unsat_constraints( # register DecisionOrderPropagator if flag is enabled if self.method_flags["show-decisions"]: decision_signatures = set(self._show_decisions_decision_signatures.items()) - dop = DecisionOrderPropagator( - signatures=decision_signatures, prefix=output_prefix_passive - ) + dop = DecisionOrderPropagator(signatures=decision_signatures, prefix=output_prefix_passive) control.register_propagator(dop) ucc = UnsatConstraintComputer(control=control) ucc.parse_files(files) - unsat_constraints = ucc.get_unsat_constraints( - assumption_string=assumption_string - ) - self._print_unsat_constraints( - unsat_constraints, ucc=ucc, prefix=output_prefix_active - ) + unsat_constraints = ucc.get_unsat_constraints(assumption_string=assumption_string) + self._print_unsat_constraints(unsat_constraints, ucc=ucc, prefix=output_prefix_active) def _print_model( self, diff --git a/src/clingexplaid/muc/__init__.py b/src/clingexplaid/muc/__init__.py new file mode 100644 index 0000000..a128ac4 --- /dev/null +++ b/src/clingexplaid/muc/__init__.py @@ -0,0 +1,5 @@ +""" +Minimal Unsatisfiable Core Utilities +""" + +from .core_computer import CoreComputer diff --git a/src/clingexplaid/utils/muc.py b/src/clingexplaid/muc/core_computer.py similarity index 82% rename from src/clingexplaid/utils/muc.py rename to src/clingexplaid/muc/core_computer.py index ab5ee01..73cdcd1 100644 --- a/src/clingexplaid/utils/muc.py +++ b/src/clingexplaid/muc/core_computer.py @@ -1,12 +1,9 @@ -""" -Unsatisfiable Core Utilities -""" from typing import Optional, Set, Tuple from itertools import chain, combinations import clingo -from . import Assumption, AssumptionSet, SymbolSet, get_solver_literal_lookup +from ..utils import Assumption, AssumptionSet, SymbolSet, get_solver_literal_lookup class CoreComputer: @@ -21,9 +18,7 @@ def __init__(self, control: clingo.Control, assumption_set: AssumptionSet): self.literal_lookup = get_solver_literal_lookup(control=self.control) self.minimal: Optional[AssumptionSet] = None - def _solve( - self, assumptions: Optional[AssumptionSet] = None - ) -> Tuple[bool, SymbolSet, SymbolSet]: + def _solve(self, assumptions: Optional[AssumptionSet] = None) -> Tuple[bool, SymbolSet, SymbolSet]: """ Internal function that is used to make the single solver calls for finding the minimal unsatisfiable core. """ @@ -32,23 +27,15 @@ def _solve( with self.control.solve(assumptions=list(assumptions), yield_=True) as solve_handle: # type: ignore[union-attr] satisfiable = bool(solve_handle.get().satisfiable) - model = ( - solve_handle.model().symbols(atoms=True) - if solve_handle.model() is not None - else [] - ) - core = { - self.literal_lookup[literal_id] for literal_id in solve_handle.core() - } + model = solve_handle.model().symbols(atoms=True) if solve_handle.model() is not None else [] + core = {self.literal_lookup[literal_id] for literal_id in solve_handle.core()} return satisfiable, set(model), core - def _compute_single_minimal( - self, assumptions: Optional[AssumptionSet] = None - ) -> AssumptionSet: + def _compute_single_minimal(self, assumptions: Optional[AssumptionSet] = None) -> AssumptionSet: """ Function to compute a single minimal unsatisfiable core from the passed set of assumptions and the program of - the CoreComputer. If there is not minimal unsatisfiable core, since for example the program with assumptions + the CoreComputer. If there is no minimal unsatisfiable core, since for example the program with assumptions assumed is satisfiable, an empty set is returned. The algorithm that is used to compute this minimal unsatisfiable core is the iterative deletion algorithm. """ @@ -57,9 +44,7 @@ def _compute_single_minimal( # check that the assumption set isn't empty if not assumptions: - raise ValueError( - "A minimal unsatisfiable core cannot be computed on an empty assumption set" - ) + raise ValueError("A minimal unsatisfiable core cannot be computed on an empty assumption set") # check if the problem with the full assumption set is unsatisfiable in the first place, and if not skip the # rest of the algorithm and return an empty set. @@ -101,8 +86,7 @@ def get_multiple_minimal(self, max_mucs: Optional[int] = None): """ assumptions = self.assumption_set assumption_powerset = chain.from_iterable( - combinations(assumptions, r) - for r in reversed(range(len(list(assumptions)) + 1)) + combinations(assumptions, r) for r in reversed(range(len(list(assumptions)) + 1)) ) found_sat = [] @@ -133,8 +117,3 @@ def get_multiple_minimal(self, max_mucs: Optional[int] = None): # if the maximum muc amount is found stop search if max_mucs is not None and len(found_mucs) == max_mucs: break - - -__all__ = [ - CoreComputer.__name__, -] diff --git a/src/clingexplaid/propagators/__init__.py b/src/clingexplaid/propagators/__init__.py new file mode 100644 index 0000000..d05fdd8 --- /dev/null +++ b/src/clingexplaid/propagators/__init__.py @@ -0,0 +1,10 @@ +""" +Propagators for Explanation +""" + +from typing import List + +from .propagator_decision_order import DecisionOrderPropagator + +DecisionLevel = List[int] +DecisionLevelList = List[DecisionLevel] diff --git a/src/clingexplaid/propagators/constants.py b/src/clingexplaid/propagators/constants.py new file mode 100644 index 0000000..f10f872 --- /dev/null +++ b/src/clingexplaid/propagators/constants.py @@ -0,0 +1,7 @@ +from ..utils.logging import COLORS + +UNKNOWN_SYMBOL_TOKEN = "INTERNAL" + +INDENT_START = "├─" +INDENT_STEP = f"─{COLORS['GREY']}┼{COLORS['NORMAL']}──" +INDENT_END = f"─{COLORS['GREY']}┤{COLORS['NORMAL']} " diff --git a/src/clingexplaid/utils/propagators.py b/src/clingexplaid/propagators/propagator_decision_order.py similarity index 83% rename from src/clingexplaid/utils/propagators.py rename to src/clingexplaid/propagators/propagator_decision_order.py index cdf8436..3d0bbd2 100644 --- a/src/clingexplaid/utils/propagators.py +++ b/src/clingexplaid/propagators/propagator_decision_order.py @@ -1,23 +1,13 @@ -from typing import List, Optional, Tuple, Set +from typing import Optional, Tuple, Set import clingo -from .logging import COLORS - -DecisionLevel = List[int] -DecisionLevelList = List[DecisionLevel] - -UNKNOWN_SYMBOL_TOKEN = "INTERNAL" - -INDENT_START = "├─" -INDENT_STEP = f"─{COLORS['GREY']}┼{COLORS['NORMAL']}──" -INDENT_END = f"─{COLORS['GREY']}┤{COLORS['NORMAL']} " +from .constants import UNKNOWN_SYMBOL_TOKEN, INDENT_STEP, INDENT_START, INDENT_END +from ..utils.logging import COLORS class DecisionOrderPropagator: - def __init__( - self, signatures: Optional[Set[Tuple[str, int]]] = None, prefix: str = "" - ): + def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None, prefix: str = ""): self.slit_symbol_lookup = {} self.signatures = signatures if signatures is not None else set() self.prefix = prefix @@ -32,9 +22,7 @@ def init(self, init): self.slit_symbol_lookup[solver_literal] = atom.symbol for atom in init.symbolic_atoms: - if len(self.signatures) > 0 and not any( - atom.match(name=s, arity=a) for s, a in self.signatures - ): + if len(self.signatures) > 0 and not any(atom.match(name=s, arity=a) for s, a in self.signatures): continue query_program_literal = init.symbolic_atoms[atom.symbol].literal query_solver_literal = init.solver_literal(query_program_literal) @@ -80,9 +68,7 @@ def propagate(self, control, changes) -> None: entailment_list = entailments[d] if d in entailments else [] # build entailment indent string entailment_indent_string = ( - (INDENT_START + INDENT_STEP * (print_level - 2) + INDENT_END) - if print_level > 1 - else "│ " + (INDENT_START + INDENT_STEP * (print_level - 2) + INDENT_END) if print_level > 1 else "│ " ) for e in entailment_list: # skip decision in entailments @@ -117,9 +103,7 @@ def undo(self, thread_id: int, assignment, changes) -> None: indent_string = INDENT_START + INDENT_STEP * (len(self.last_decisions) - 1) if printed: - print( - f"{self.prefix}{indent_string}{COLORS['RED']}[✕] {decision_symbol} [{decision}]{COLORS['NORMAL']}" - ) + print(f"{self.prefix}{indent_string}{COLORS['RED']}[✕] {decision_symbol} [{decision}]{COLORS['NORMAL']}") self.last_decisions = self.last_decisions[:-1] @staticmethod @@ -137,9 +121,7 @@ def get_decisions(assignment): level_offset_end = trail.end(level) level_offset_diff = level_offset_end - level_offset_start if level_offset_diff > 1: - entailments[decision] = trail[ - (level_offset_start + 1): level_offset_end - ] + entailments[decision] = trail[(level_offset_start + 1) : level_offset_end] level += 1 except RuntimeError: return decisions, entailments diff --git a/src/clingexplaid/transformers/__init__.py b/src/clingexplaid/transformers/__init__.py new file mode 100644 index 0000000..36e7eb0 --- /dev/null +++ b/src/clingexplaid/transformers/__init__.py @@ -0,0 +1,10 @@ +""" +Transformers for Explanation +""" + +from .transformer_assumption import AssumptionTransformer +from .transformer_constraint import ConstraintTransformer +from .transformer_fact import FactTransformer +from .transformer_optimization_remover import OptimizationRemover +from .transformer_rule_id import RuleIDTransformer +from .transformer_rule_splitter import RuleSplitter diff --git a/src/clingexplaid/transformers/constants.py b/src/clingexplaid/transformers/constants.py new file mode 100644 index 0000000..80979a1 --- /dev/null +++ b/src/clingexplaid/transformers/constants.py @@ -0,0 +1,2 @@ +REMOVED_TOKEN = "__REMOVED__" +RULE_ID_SIGNATURE = "_rule" diff --git a/src/clingexplaid/transformers/exceptions.py b/src/clingexplaid/transformers/exceptions.py new file mode 100644 index 0000000..f5daafd --- /dev/null +++ b/src/clingexplaid/transformers/exceptions.py @@ -0,0 +1,10 @@ +class UntransformedException(Exception): + """Exception raised if the get_assumptions method of an AssumptionTransformer is called before it is used to + transform a program. + """ + + +class NotGroundedException(Exception): + """Exception raised if the get_assumptions method of an AssumptionTransformer is called without the control object + having been grounded beforehand. + """ diff --git a/src/clingexplaid/transformers/transformer_assumption.py b/src/clingexplaid/transformers/transformer_assumption.py new file mode 100644 index 0000000..099db5a --- /dev/null +++ b/src/clingexplaid/transformers/transformer_assumption.py @@ -0,0 +1,107 @@ +from pathlib import Path +from typing import Dict, List, Optional, Sequence, Set, Tuple, Union + +import clingo +import clingo.ast as _ast + +from .exceptions import NotGroundedException, UntransformedException +from ..utils import match_ast_symbolic_atom_signature + + +class AssumptionTransformer(_ast.Transformer): + """ + A transformer that transforms facts that match with one of the signatures provided (no signatures means all facts) + into choice rules and also provides the according assumptions for them. + """ + + def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None): + self.signatures = signatures if signatures is not None else set() + self.fact_rules: List[str] = [] + self.transformed: bool = False + self.program_constants = {} + + def visit_Rule(self, node): # pylint: disable=C0103 + """ + Transforms head of a rule into a choice rule if it is a fact and adheres to the given signatures. + """ + if node.head.ast_type != _ast.ASTType.Literal: + return node + if node.body: + return node + has_matching_signature = any( + match_ast_symbolic_atom_signature(node.head.atom, (name, arity)) for (name, arity) in self.signatures + ) + # if signatures are defined only transform facts that match them, else transform all facts + if self.signatures and not has_matching_signature: + return node + + self.fact_rules.append(str(node)) + + return _ast.Rule( + location=node.location, + head=_ast.Aggregate( + location=node.location, + left_guard=None, + elements=[node.head], + right_guard=None, + ), + body=[], + ) + + def visit_Definition(self, node): + """ + All defined constants of the program are stored in self.program_constants + """ + self.program_constants[node.name] = node.value.symbol + return node + + def parse_string(self, string: str) -> str: + """ + Function that applies the transformation to the `program_string` it's called with and returns the transformed + program string. + """ + out = [] + _ast.parse_string(string, lambda stm: out.append((str(self(stm))))) + self.transformed = True + return "\n".join(out) + + def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: + """ + Parses the files and returns a string with the transformed program. + """ + out = [] + _ast.parse_files([str(p) for p in paths], lambda stm: out.append((str(self(stm))))) + self.transformed = True + return "\n".join(out) + + def get_assumptions(self, control: clingo.Control, constants: Optional[Dict] = None) -> Set[int]: + """ + Returns the assumptions which were gathered during the transformation of the program. Has to be called after + a program has already been transformed. + """ + # Just taking the fact symbolic atoms of the control given doesn't work here since we anticipate that + # this control is ground on the already transformed program. This means that all facts are now choice rules + # which means we cannot detect them like this anymore. + if not self.transformed: + raise UntransformedException( + "The get_assumptions method cannot be called before a program has been " "transformed" + ) + # If the control has not been grounded yet except since without grounding we don't have access to the symbolic + # atoms. + if len(control.symbolic_atoms) == 0: + raise NotGroundedException( + "The get_assumptions method cannot be called before the control has been " "grounded" + ) + + constants = constants if constants is not None else {} + + all_constants = dict(self.program_constants) + all_constants.update(constants) + constant_strings = [f"-c {k}={v}" for k, v in all_constants.items()] if constants is not None else [] + fact_control = clingo.Control(constant_strings) + fact_control.add("base", [], "\n".join(self.fact_rules)) + fact_control.ground([("base", [])]) + fact_symbols = [sym.symbol for sym in fact_control.symbolic_atoms if sym.is_fact] + + symbol_to_literal_lookup = {sym.symbol: sym.literal for sym in control.symbolic_atoms} + return {symbol_to_literal_lookup[sym] for sym in fact_symbols if sym in symbol_to_literal_lookup} diff --git a/src/clingexplaid/transformers/transformer_constraint.py b/src/clingexplaid/transformers/transformer_constraint.py new file mode 100644 index 0000000..f50eb08 --- /dev/null +++ b/src/clingexplaid/transformers/transformer_constraint.py @@ -0,0 +1,68 @@ +from pathlib import Path +from typing import Sequence, Union + +import clingo +import clingo.ast as _ast + + +class ConstraintTransformer(_ast.Transformer): + """ + A Transformer that takes all constraint rules and adds an atom to their head to avoid deriving false through them. + """ + + def __init__(self, constraint_head_symbol: str, include_id: bool = False): + self._constraint_head_symbol = constraint_head_symbol + self._include_id = include_id + self._constraint_id = 1 + + self.constraint_location_lookup = {} + + def visit_Rule(self, node): # pylint: disable=C0103 + """ + Adds a constraint_head_symbol atom to the head of every constraint. + """ + if node.head.ast_type != _ast.ASTType.Literal: + return node + if node.head.atom.ast_type != _ast.ASTType.BooleanConstant: + return node + if node.head.atom.value != 0: + return node + + arguments = [] + if self._include_id: + arguments = [_ast.SymbolicTerm(node.location, clingo.parse_term(str(self._constraint_id)))] + + head_symbol = _ast.Function( + location=node.location, + name=self._constraint_head_symbol, + arguments=arguments, + external=0, + ) + + # add constraint location to lookup indexed by the constraint id + self.constraint_location_lookup[self._constraint_id] = node.location + + # increase constraint id + self._constraint_id += 1 + + # insert id symbol into body of rule + node.head = head_symbol + return node.update(**self.visit_children(node)) + + def parse_string(self, string: str) -> str: + """ + Function that applies the transformation to the `program_string` it's called with and returns the transformed + program string. + """ + out = [] + _ast.parse_string(string, lambda stm: out.append((str(self(stm))))) + + return "\n".join(out) + + def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: + """ + Parses the files and returns a string with the transformed program. + """ + out = [] + _ast.parse_files([str(p) for p in paths], lambda stm: out.append((str(self(stm))))) + return "\n".join(out) diff --git a/src/clingexplaid/transformers/transformer_fact.py b/src/clingexplaid/transformers/transformer_fact.py new file mode 100644 index 0000000..aaa81f3 --- /dev/null +++ b/src/clingexplaid/transformers/transformer_fact.py @@ -0,0 +1,67 @@ +from pathlib import Path +from typing import Optional, Sequence, Set, Tuple, Union + +import clingo.ast as _ast + +from .constants import REMOVED_TOKEN +from ..utils import match_ast_symbolic_atom_signature + + +class FactTransformer(_ast.Transformer): + """ + Transformer that removes all facts from a program that match provided signatures + """ + + def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None): + self.signatures = signatures if signatures is not None else set() + + def visit_Rule(self, node): # pylint: disable=C0103 + """ + Removes all facts from a program that match the given signatures (if none are given all facts are removed). + """ + if node.head.ast_type != _ast.ASTType.Literal: + return node + if node.body: + return node + has_matching_signature = any( + match_ast_symbolic_atom_signature(node.head.atom, (name, arity)) for (name, arity) in self.signatures + ) + # if signatures are defined only transform facts that match them, else transform all facts + if self.signatures and not has_matching_signature: + return node + + return _ast.Rule( + location=node.location, + head=_ast.Function(location=node.location, name=REMOVED_TOKEN, arguments=[], external=0), + body=[], + ) + + @staticmethod + def post_transform(program_string: str) -> str: + # remove the transformed REMOVED_TOKENS from the resulting program string + rules = program_string.split("\n") + out = [] + for rule in rules: + if not rule.startswith(REMOVED_TOKEN): + out.append(rule) + return "\n".join(out) + + def parse_string(self, string: str) -> str: + """ + Function that applies the transformation to the `program_string` it's called with and returns the transformed + program string. + """ + out = [] + _ast.parse_string(string, lambda stm: out.append(str(self(stm)))) + return self.post_transform("\n".join(out)) + + def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: + """ + Parses the files and returns a string with the transformed program. + """ + out = [] + _ast.parse_files( + [str(p) for p in paths], + lambda stm: out.append(str(self(stm))), + ) + return self.post_transform("\n".join(out)) diff --git a/src/clingexplaid/transformers/transformer_optimization_remover.py b/src/clingexplaid/transformers/transformer_optimization_remover.py new file mode 100644 index 0000000..45e5198 --- /dev/null +++ b/src/clingexplaid/transformers/transformer_optimization_remover.py @@ -0,0 +1,52 @@ +from pathlib import Path +from typing import Sequence, Union + +import clingo.ast as _ast + +from .constants import REMOVED_TOKEN + + +class OptimizationRemover(_ast.Transformer): + """ + Transformer that removes all optimization statements + """ + + def visit_Minimize(self, node): # pylint: disable=C0103 + """ + Removes all facts from a program that match the given signatures (if none are given all facts are removed). + """ + return _ast.Rule( + location=node.location, + head=_ast.Function(location=node.location, name=REMOVED_TOKEN, arguments=[], external=0), + body=[], + ) + + @staticmethod + def post_transform(program_string: str) -> str: + # remove the transformed REMOVED_TOKENS from the resulting program string + rules = program_string.split("\n") + out = [] + for rule in rules: + if not rule.startswith(REMOVED_TOKEN): + out.append(rule) + return "\n".join(out) + + def parse_string(self, string: str) -> str: + """ + Function that applies the transformation to the `program_string` it's called with and returns the transformed + program string. + """ + out = [] + _ast.parse_string(string, lambda stm: out.append(str(self(stm)))) + return self.post_transform("\n".join(out)) + + def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: + """ + Parses the files and returns a string with the transformed program. + """ + out = [] + _ast.parse_files( + [str(p) for p in paths], + lambda stm: out.append(str(self(stm))), + ) + return self.post_transform("\n".join(out)) diff --git a/src/clingexplaid/transformers/transformer_rule_id.py b/src/clingexplaid/transformers/transformer_rule_id.py new file mode 100644 index 0000000..95e17f9 --- /dev/null +++ b/src/clingexplaid/transformers/transformer_rule_id.py @@ -0,0 +1,74 @@ +from pathlib import Path +from typing import Optional, Set, Tuple, Union + +import clingo +import clingo.ast as _ast + +from .constants import RULE_ID_SIGNATURE + + +class RuleIDTransformer(_ast.Transformer): + """ + A Transformer that takes all the rules of a program and adds an atom with `self.rule_id_signature` in their bodys, + to make the original rule the generated them identifiable even after grounding. Additionally, a choice rule + containing all generated `self.rule_id_signature` atoms is added, which allows us to add assumptions that assume + them. This is done in order to not modify the original program's reasoning by assuming all `self.rule_id_signature` + atoms as True. + """ + + def __init__(self, rule_id_signature: str = RULE_ID_SIGNATURE): + self.rule_id = 0 + self.rule_id_signature = rule_id_signature + + def visit_Rule(self, node): # pylint: disable=C0103 + """ + Adds a rule_id_signature(id) atom to the body of every rule that is visited. + """ + # add for each rule a theory atom (self.rule_id_signature) with the id as an argument + symbol = _ast.Function( + location=node.location, + name=self.rule_id_signature, + arguments=[_ast.SymbolicTerm(node.location, clingo.parse_term(str(self.rule_id)))], + external=0, + ) + + # increase the rule_id by one after every transformed rule + self.rule_id += 1 + + # insert id symbol into body of rule + node.body.insert(len(node.body), symbol) + return node.update(**self.visit_children(node)) + + def _get_number_of_rules(self): + return self.rule_id - 1 if self.rule_id > 1 else self.rule_id + + def parse_string(self, string: str) -> str: + """ + Function that applies the transformation to the `program_string` it's called with and returns the transformed + program string. + """ + self.rule_id = 1 + out = [] + _ast.parse_string(string, lambda stm: out.append((str(self(stm))))) + out.append( + f"{{_rule(1..{self._get_number_of_rules()})}}" + f" % Choice rule to allow all _rule atoms to become assumptions" + ) + + return "\n".join(out) + + def parse_file(self, path: Union[str, Path], encoding: str = "utf-8") -> str: + """ + Parses the file at path and returns a string with the transformed program. + """ + with open(path, "r", encoding=encoding) as f: + return self.parse_string(f.read()) + + def get_assumptions(self, n_rules: Optional[int] = None) -> Set[Tuple[clingo.Symbol, bool]]: + """ + Returns the rule_id_signature assumptions depending on the number of rules contained in the transformed + program. Can only be called after parse_file has been executed before. + """ + if n_rules is None: + n_rules = self._get_number_of_rules() + return {(clingo.parse_term(f"{self.rule_id_signature}({rule_id})"), True) for rule_id in range(1, n_rules + 1)} diff --git a/src/clingexplaid/transformers/transformer_rule_splitter.py b/src/clingexplaid/transformers/transformer_rule_splitter.py new file mode 100644 index 0000000..7b3edb6 --- /dev/null +++ b/src/clingexplaid/transformers/transformer_rule_splitter.py @@ -0,0 +1,96 @@ +import base64 +from pathlib import Path +from typing import Union + +import clingo +import clingo.ast as _ast + + +class RuleSplitter(_ast.Transformer): + """ + A transformer that is used to split rules into two. This is done using an intermediate predicate called `_body`, + which contains a base64 representation of the original rule and all body variable assignments for explanation + purposes. This intermediate predicate replaces the head of the original rule and a new rule with the old head and + the newly generated `_body` predicate as the body is also inserted. Use the `parse_string` method to apply this + transformer. + """ + + def __init__(self): + self.head_rules = [] + + def visit_Rule(self, node): # pylint: disable=C0103 + """ + Replaces the head of every rule with the intermediate `_body` predicate and stores all new head rules using this + intermediary predicate in `self.head_rules` + """ + head = node.head + body = node.body + + if body: + # remove MUS literals from rule + cleaned_body_literals = [x for x in node.body if x.atom.symbol.name not in ("__mus__",)] + cleaned_body = "; ".join([str(l) for l in cleaned_body_literals]) + + # get all variables used in body (to later reference in head) + variables = set() + for lit in cleaned_body_literals: + arguments = lit.atom.symbol.arguments + if arguments: + for arg in arguments: + variables.add(arg) + + # convert the cleaned body to a base64 string + rule_body_string = cleaned_body + rule_body_string_bytes = rule_body_string.encode("ascii") + rule_body_base64_bytes = base64.b64encode(rule_body_string_bytes) + rule_body_base64 = rule_body_base64_bytes.decode("ascii") + + # create a new '_body' head for the original rule + new_head_arguments = [ + _ast.SymbolicTerm(node.location, clingo.parse_term(f'"{rule_body_base64}"')), + _ast.Function( + location=node.location, + name="", + arguments=sorted(variables), + external=0, + ), + ] + new_head = _ast.Function( + location=node.location, + name="_body", + arguments=new_head_arguments, + external=0, + ) + node.head = new_head + + # create new second rule that links the head with the '_body' matching predicate + new_head_rule = _ast.Rule( + location=node.location, + head=head, + body=[new_head], + ) + self.head_rules.append(new_head_rule) + + return node + + # default case + return node + + def parse_string(self, string: str) -> str: + """ + Function that applies the transformation to the `program_string` it's called with and returns the transformed + program string. + """ + self.head_rules = [] + out = [] + _ast.parse_string(string, lambda stm: out.append((str(self(stm))))) + out += [str(r) for r in self.head_rules] + + return "\n".join(out) + + def parse_file(self, path: Union[str, Path], encoding: str = "utf-8") -> str: + """ + Parses the file at path and returns a string with the transformed program. + """ + with open(path, "r", encoding=encoding) as f: + return self.parse_string(f.read()) diff --git a/src/clingexplaid/unsat_constraints/__init__.py b/src/clingexplaid/unsat_constraints/__init__.py new file mode 100644 index 0000000..ab248d9 --- /dev/null +++ b/src/clingexplaid/unsat_constraints/__init__.py @@ -0,0 +1,5 @@ +""" +Functionality for Unsat Constraints +""" + +from .unsat_constraint_computer import UnsatConstraintComputer diff --git a/src/clingexplaid/unsat_constraints/constants.py b/src/clingexplaid/unsat_constraints/constants.py new file mode 100644 index 0000000..947c035 --- /dev/null +++ b/src/clingexplaid/unsat_constraints/constants.py @@ -0,0 +1 @@ +UNSAT_CONSTRAINT_SIGNATURE = "__unsat__" diff --git a/src/clingexplaid/utils/unsat_constraints.py b/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py similarity index 86% rename from src/clingexplaid/utils/unsat_constraints.py rename to src/clingexplaid/unsat_constraints/unsat_constraint_computer.py index a697d07..e5d234c 100644 --- a/src/clingexplaid/utils/unsat_constraints.py +++ b/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py @@ -8,11 +8,9 @@ import clingo from clingo.ast import Location -from .transformer import ConstraintTransformer, FactTransformer, OptimizationRemover -from . import get_signatures_from_model_string - - -UNSAT_CONSTRAINT_SIGNATURE = "__unsat__" +from .constants import UNSAT_CONSTRAINT_SIGNATURE +from ..transformers import ConstraintTransformer, FactTransformer, OptimizationRemover +from ..utils import get_signatures_from_model_string class UnsatConstraintComputer: @@ -55,9 +53,7 @@ def parse_files(self, files: List[str]) -> None: def get_constraint_location(self, constraint_id: int) -> Optional[Location]: return self._file_constraint_lookup.get(constraint_id) - def get_unsat_constraints( - self, assumption_string: Optional[str] = None - ) -> Dict[int, str]: + def get_unsat_constraints(self, assumption_string: Optional[str] = None) -> Dict[int, str]: # only execute if the UnsatConstraintComputer was properly initialized if not self.initialized: raise ValueError( @@ -88,9 +84,7 @@ def get_unsat_constraints( continue constraint_id = match_result.group(1) constraint_lookup[int(constraint_id)] = ( - str(line) - .replace(f"{UNSAT_CONSTRAINT_SIGNATURE}({constraint_id})", "") - .strip() + str(line).replace(f"{UNSAT_CONSTRAINT_SIGNATURE}({constraint_id})", "").strip() ) self.control.add("base", [], program_string) @@ -101,9 +95,7 @@ def get_unsat_constraints( unsat_constraint_atoms = [] while model is not None: unsat_constraint_atoms = [ - a - for a in model.symbols(atoms=True) - if a.match(UNSAT_CONSTRAINT_SIGNATURE, 1, True) + a for a in model.symbols(atoms=True) if a.match(UNSAT_CONSTRAINT_SIGNATURE, 1, True) ] solve_handle.resume() model = solve_handle.model() @@ -114,8 +106,3 @@ def get_unsat_constraints( unsat_constraints[constraint_id] = constraint return unsat_constraints - - -__all__ = [ - UnsatConstraintComputer.__name__, -] diff --git a/src/clingexplaid/utils/transformer.py b/src/clingexplaid/utils/transformer.py deleted file mode 100644 index 9ad3f02..0000000 --- a/src/clingexplaid/utils/transformer.py +++ /dev/null @@ -1,502 +0,0 @@ -""" -Transformers for Explanation -""" -import base64 -from pathlib import Path -from typing import Dict, List, Optional, Sequence, Set, Tuple, Union - -import clingo -from clingo import ast as _ast - -from . import match_ast_symbolic_atom_signature - -RULE_ID_SIGNATURE = "_rule" -REMOVED_TOKEN = "__REMOVED__" - - -class UntransformedException(Exception): - """Exception raised if the get_assumptions method of an AssumptionTransformer is called before it is used to - transform a program. - """ - - -class NotGroundedException(Exception): - """Exception raised if the get_assumptions method of an AssumptionTransformer is called without the control object - having been grounded beforehand. - """ - - -class RuleIDTransformer(_ast.Transformer): - """ - A Transformer that takes all the rules of a program and adds an atom with `self.rule_id_signature` in their bodys, - to make the original rule the generated them identifiable even after grounding. Additionally, a choice rule - containing all generated `self.rule_id_signature` atoms is added, which allows us to add assumptions that assume - them. This is done in order to not modify the original program's reasoning by assuming all `self.rule_id_signature` - atoms as True. - """ - - def __init__(self, rule_id_signature: str = RULE_ID_SIGNATURE): - self.rule_id = 0 - self.rule_id_signature = rule_id_signature - - def visit_Rule(self, node): # pylint: disable=C0103 - """ - Adds a rule_id_signature(id) atom to the body of every rule that is visited. - """ - # add for each rule a theory atom (self.rule_id_signature) with the id as an argument - symbol = _ast.Function( - location=node.location, - name=self.rule_id_signature, - arguments=[ - _ast.SymbolicTerm(node.location, clingo.parse_term(str(self.rule_id))) - ], - external=0, - ) - - # increase the rule_id by one after every transformed rule - self.rule_id += 1 - - # insert id symbol into body of rule - node.body.insert(len(node.body), symbol) - return node.update(**self.visit_children(node)) - - def _get_number_of_rules(self): - return self.rule_id - 1 if self.rule_id > 1 else self.rule_id - - def parse_string(self, string: str) -> str: - """ - Function that applies the transformation to the `program_string` it's called with and returns the transformed - program string. - """ - self.rule_id = 1 - out = [] - _ast.parse_string(string, lambda stm: out.append((str(self(stm))))) - out.append( - f"{{_rule(1..{self._get_number_of_rules()})}}" - f" % Choice rule to allow all _rule atoms to become assumptions" - ) - - return "\n".join(out) - - def parse_file(self, path: Union[str, Path], encoding: str = "utf-8") -> str: - """ - Parses the file at path and returns a string with the transformed program. - """ - with open(path, "r", encoding=encoding) as f: - return self.parse_string(f.read()) - - def get_assumptions( - self, n_rules: Optional[int] = None - ) -> Set[Tuple[clingo.Symbol, bool]]: - """ - Returns the rule_id_signature assumptions depending on the number of rules contained in the transformed - program. Can only be called after parse_file has been executed before. - """ - if n_rules is None: - n_rules = self._get_number_of_rules() - return { - (clingo.parse_term(f"{self.rule_id_signature}({rule_id})"), True) - for rule_id in range(1, n_rules + 1) - } - - -class AssumptionTransformer(_ast.Transformer): - """ - A transformer that transforms facts that match with one of the signatures provided (no signatures means all facts) - into choice rules and also provides the according assumptions for them. - """ - - def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None): - self.signatures = signatures if signatures is not None else set() - self.fact_rules: List[str] = [] - self.transformed: bool = False - self.program_constants = {} - - def visit_Rule(self, node): # pylint: disable=C0103 - """ - Transforms head of a rule into a choice rule if it is a fact and adheres to the given signatures. - """ - if node.head.ast_type != _ast.ASTType.Literal: - return node - if node.body: - return node - has_matching_signature = any( - match_ast_symbolic_atom_signature(node.head.atom, (name, arity)) - for (name, arity) in self.signatures - ) - # if signatures are defined only transform facts that match them, else transform all facts - if self.signatures and not has_matching_signature: - return node - - self.fact_rules.append(str(node)) - - return _ast.Rule( - location=node.location, - head=_ast.Aggregate( - location=node.location, - left_guard=None, - elements=[node.head], - right_guard=None, - ), - body=[], - ) - - def visit_Definition(self, node): - """ - All defined constants of the program are stored in self.program_constants - """ - self.program_constants[node.name] = node.value.symbol - return node - - def parse_string(self, string: str) -> str: - """ - Function that applies the transformation to the `program_string` it's called with and returns the transformed - program string. - """ - out = [] - _ast.parse_string(string, lambda stm: out.append((str(self(stm))))) - self.transformed = True - return "\n".join(out) - - def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: - """ - Parses the files and returns a string with the transformed program. - """ - out = [] - _ast.parse_files( - [str(p) for p in paths], lambda stm: out.append((str(self(stm)))) - ) - self.transformed = True - return "\n".join(out) - - def get_assumptions( - self, control: clingo.Control, constants: Optional[Dict] = None - ) -> Set[int]: - """ - Returns the assumptions which were gathered during the transformation of the program. Has to be called after - a program has already been transformed. - """ - # Just taking the fact symbolic atoms of the control given doesn't work here since we anticipate that - # this control is ground on the already transformed program. This means that all facts are now choice rules - # which means we cannot detect them like this anymore. - if not self.transformed: - raise UntransformedException( - "The get_assumptions method cannot be called before a program has been " - "transformed" - ) - # If the control has not been grounded yet except since without grounding we don't have access to the symbolic - # atoms. - if len(control.symbolic_atoms) == 0: - raise NotGroundedException( - "The get_assumptions method cannot be called before the control has been " - "grounded" - ) - - constants = constants if constants is not None else {} - - all_constants = dict(self.program_constants) - all_constants.update(constants) - constant_strings = ( - [f"-c {k}={v}" for k, v in all_constants.items()] - if constants is not None - else [] - ) - fact_control = clingo.Control(constant_strings) - fact_control.add("base", [], "\n".join(self.fact_rules)) - fact_control.ground([("base", [])]) - fact_symbols = [ - sym.symbol for sym in fact_control.symbolic_atoms if sym.is_fact - ] - - symbol_to_literal_lookup = { - sym.symbol: sym.literal for sym in control.symbolic_atoms - } - return { - symbol_to_literal_lookup[sym] - for sym in fact_symbols - if sym in symbol_to_literal_lookup - } - - -class FactTransformer(_ast.Transformer): - """ - Transformer that removes all facts from a program that match provided signatures - """ - - def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None): - self.signatures = signatures if signatures is not None else set() - - def visit_Rule(self, node): # pylint: disable=C0103 - """ - Removes all facts from a program that match the given signatures (if none are given all facts are removed). - """ - if node.head.ast_type != _ast.ASTType.Literal: - return node - if node.body: - return node - has_matching_signature = any( - match_ast_symbolic_atom_signature(node.head.atom, (name, arity)) - for (name, arity) in self.signatures - ) - # if signatures are defined only transform facts that match them, else transform all facts - if self.signatures and not has_matching_signature: - return node - - return _ast.Rule( - location=node.location, - head=_ast.Function( - location=node.location, name=REMOVED_TOKEN, arguments=[], external=0 - ), - body=[], - ) - - @staticmethod - def post_transform(program_string: str) -> str: - # remove the transformed REMOVED_TOKENS from the resulting program string - rules = program_string.split("\n") - out = [] - for rule in rules: - if not rule.startswith(REMOVED_TOKEN): - out.append(rule) - return "\n".join(out) - - def parse_string(self, string: str) -> str: - """ - Function that applies the transformation to the `program_string` it's called with and returns the transformed - program string. - """ - out = [] - _ast.parse_string(string, lambda stm: out.append(str(self(stm)))) - return self.post_transform("\n".join(out)) - - def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: - """ - Parses the files and returns a string with the transformed program. - """ - out = [] - _ast.parse_files( - [str(p) for p in paths], - lambda stm: out.append(str(self(stm))), - ) - return self.post_transform("\n".join(out)) - - -class OptimizationRemover(_ast.Transformer): - """ - Transformer that removes all optimization statements - """ - - def visit_Minimize(self, node): # pylint: disable=C0103 - """ - Removes all facts from a program that match the given signatures (if none are given all facts are removed). - """ - return _ast.Rule( - location=node.location, - head=_ast.Function( - location=node.location, name=REMOVED_TOKEN, arguments=[], external=0 - ), - body=[], - ) - - @staticmethod - def post_transform(program_string: str) -> str: - # remove the transformed REMOVED_TOKENS from the resulting program string - rules = program_string.split("\n") - out = [] - for rule in rules: - if not rule.startswith(REMOVED_TOKEN): - out.append(rule) - return "\n".join(out) - - def parse_string(self, string: str) -> str: - """ - Function that applies the transformation to the `program_string` it's called with and returns the transformed - program string. - """ - out = [] - _ast.parse_string(string, lambda stm: out.append(str(self(stm)))) - return self.post_transform("\n".join(out)) - - def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: - """ - Parses the files and returns a string with the transformed program. - """ - out = [] - _ast.parse_files( - [str(p) for p in paths], - lambda stm: out.append(str(self(stm))), - ) - return self.post_transform("\n".join(out)) - - -class ConstraintTransformer(_ast.Transformer): - """ - A Transformer that takes all constraint rules and adds an atom to their head to avoid deriving false through them. - """ - - def __init__(self, constraint_head_symbol: str, include_id: bool = False): - self._constraint_head_symbol = constraint_head_symbol - self._include_id = include_id - self._constraint_id = 1 - - self.constraint_location_lookup = {} - - def visit_Rule(self, node): # pylint: disable=C0103 - """ - Adds a constraint_head_symbol atom to the head of every constraint. - """ - if node.head.ast_type != _ast.ASTType.Literal: - return node - if node.head.atom.ast_type != _ast.ASTType.BooleanConstant: - return node - if node.head.atom.value != 0: - return node - - arguments = [] - if self._include_id: - arguments = [ - _ast.SymbolicTerm( - node.location, clingo.parse_term(str(self._constraint_id)) - ) - ] - - head_symbol = _ast.Function( - location=node.location, - name=self._constraint_head_symbol, - arguments=arguments, - external=0, - ) - - # add constraint location to lookup indexed by the constraint id - self.constraint_location_lookup[self._constraint_id] = node.location - - # increase constraint id - self._constraint_id += 1 - - # insert id symbol into body of rule - node.head = head_symbol - return node.update(**self.visit_children(node)) - - def parse_string(self, string: str) -> str: - """ - Function that applies the transformation to the `program_string` it's called with and returns the transformed - program string. - """ - out = [] - _ast.parse_string(string, lambda stm: out.append((str(self(stm))))) - - return "\n".join(out) - - def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: - """ - Parses the files and returns a string with the transformed program. - """ - out = [] - _ast.parse_files( - [str(p) for p in paths], lambda stm: out.append((str(self(stm)))) - ) - return "\n".join(out) - - -class RuleSplitter(_ast.Transformer): - """ - A transformer that is used to split rules into two. This is done using an intermediate predicate called `_body`, - which contains a base64 representation of the original rule and all body variable assignments for explanation - purposes. This intermediate predicate replaces the head of the original rule and a new rule with the old head and - the newly generated `_body` predicate as the body is also inserted. Use the `parse_string` method to apply this - transformer. - """ - - def __init__(self): - self.head_rules = [] - - def visit_Rule(self, node): # pylint: disable=C0103 - """ - Replaces the head of every rule with the intermediate `_body` predicate and stores all new head rules using this - intermediary predicate in `self.head_rules` - """ - head = node.head - body = node.body - - if body: - # remove MUS literals from rule - cleaned_body_literals = [ - x for x in node.body if x.atom.symbol.name not in ("__mus__",) - ] - cleaned_body = "; ".join([str(l) for l in cleaned_body_literals]) - - # get all variables used in body (to later reference in head) - variables = set() - for lit in cleaned_body_literals: - arguments = lit.atom.symbol.arguments - if arguments: - for arg in arguments: - variables.add(arg) - - # convert the cleaned body to a base64 string - rule_body_string = cleaned_body - rule_body_string_bytes = rule_body_string.encode("ascii") - rule_body_base64_bytes = base64.b64encode(rule_body_string_bytes) - rule_body_base64 = rule_body_base64_bytes.decode("ascii") - - # create a new '_body' head for the original rule - new_head_arguments = [ - _ast.SymbolicTerm( - node.location, clingo.parse_term(f'"{rule_body_base64}"') - ), - _ast.Function( - location=node.location, - name="", - arguments=sorted(variables), - external=0, - ), - ] - new_head = _ast.Function( - location=node.location, - name="_body", - arguments=new_head_arguments, - external=0, - ) - node.head = new_head - - # create new second rule that links the head with the '_body' matching predicate - new_head_rule = _ast.Rule( - location=node.location, - head=head, - body=[new_head], - ) - self.head_rules.append(new_head_rule) - - return node - - # default case - return node - - def parse_string(self, string: str) -> str: - """ - Function that applies the transformation to the `program_string` it's called with and returns the transformed - program string. - """ - self.head_rules = [] - out = [] - _ast.parse_string(string, lambda stm: out.append((str(self(stm))))) - out += [str(r) for r in self.head_rules] - - return "\n".join(out) - - def parse_file(self, path: Union[str, Path], encoding: str = "utf-8") -> str: - """ - Parses the file at path and returns a string with the transformed program. - """ - with open(path, "r", encoding=encoding) as f: - return self.parse_string(f.read()) - - -__all__ = [ - RuleIDTransformer.__name__, - AssumptionTransformer.__name__, - ConstraintTransformer.__name__, - FactTransformer.__name__, - RuleSplitter.__name__, - OptimizationRemover.__name__, -] diff --git a/tests/clingexplaid/test_main.py b/tests/clingexplaid/test_main.py index 3dd3fac..5208bf0 100644 --- a/tests/clingexplaid/test_main.py +++ b/tests/clingexplaid/test_main.py @@ -10,14 +10,15 @@ import clingo from clingexplaid.utils import AssumptionSet, get_solver_literal_lookup -from clingexplaid.utils.muc import CoreComputer -from clingexplaid.utils.transformer import ( +from clingexplaid.muc import CoreComputer +from clingexplaid.transformers import ( AssumptionTransformer, ConstraintTransformer, RuleIDTransformer, RuleSplitter, - UntransformedException, ) +from clingexplaid.transformers.exceptions import UntransformedException + TEST_DIR = parent = Path(__file__).resolve().parent @@ -70,10 +71,7 @@ def assert_muc( """ Asserts if a MUC is one of several valid MUC's. """ - valid_mucs = [ - {clingo.parse_term(s) for s in lit_strings} - for lit_strings in valid_mucs_string_lists - ] + valid_mucs = [{clingo.parse_term(s) for s in lit_strings} for lit_strings in valid_mucs_string_lists] self.assertIn(muc, valid_mucs) # TRANSFORMERS @@ -84,28 +82,20 @@ def test_assumption_transformer_parse_file(self): Test the AssumptionTransformer's `parse_file` method. """ program_path = TEST_DIR.joinpath("res/test_program.lp") - program_path_transformed = TEST_DIR.joinpath( - "res/transformed_program_assumptions_certain_signatures.lp" - ) + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_assumptions_certain_signatures.lp") at = AssumptionTransformer(signatures={(c, 1) for c in "abcdef"}) result = at.parse_files([program_path]) - self.assertEqual( - result.strip(), self.read_file(program_path_transformed).strip() - ) + self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) def test_assumption_transformer_parse_file_no_signatures(self): """ Test the AssumptionTransformer's `parse_file` method with no signatures provided. """ program_path = TEST_DIR.joinpath("res/test_program.lp") - program_path_transformed = TEST_DIR.joinpath( - "res/transformed_program_assumptions_all.lp" - ) + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_assumptions_all.lp") at = AssumptionTransformer() result = at.parse_files([program_path]) - self.assertEqual( - result.strip(), self.read_file(program_path_transformed).strip() - ) + self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) def test_assumption_transformer_get_assumptions_before_transformation(self): """ @@ -122,14 +112,10 @@ def test_rule_id_transformer(self): Test the RuleIDTransformer's `parse_file` and `get_assumptions` methods. """ program_path = TEST_DIR.joinpath("res/test_program.lp") - program_path_transformed = TEST_DIR.joinpath( - "res/transformed_program_rule_ids.lp" - ) + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_rule_ids.lp") rt = RuleIDTransformer() result = rt.parse_file(program_path) - self.assertEqual( - result.strip(), self.read_file(program_path_transformed).strip() - ) + self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) assumptions = { (clingo.parse_term(s), True) for s in [ @@ -151,14 +137,10 @@ def test_constraint_transformer(self): Test the ConstraintTransformer's `parse_file` method. """ program_path = TEST_DIR.joinpath("res/test_program_constraints.lp") - program_path_transformed = TEST_DIR.joinpath( - "res/transformed_program_constraints.lp" - ) + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_constraints.lp") ct = ConstraintTransformer(constraint_head_symbol="unsat") result = ct.parse_files([program_path]) - self.assertEqual( - result.strip(), self.read_file(program_path_transformed).strip() - ) + self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) # --- RULE SPLITTER @@ -168,14 +150,10 @@ def test_rule_splitter(self): """ program_path = TEST_DIR.joinpath("res/test_program_rules.lp") - program_path_transformed = TEST_DIR.joinpath( - "res/transformed_program_rules_split.lp" - ) + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_rules_split.lp") rs = RuleSplitter() result = rs.parse_file(program_path) - self.assertEqual( - result.strip(), self.read_file(program_path_transformed).strip() - ) + self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) # MUC @@ -192,9 +170,7 @@ def test_core_computer_shrink_single_muc(self): """ signatures = {("a", 1)} - muc = self.get_muc_of_program( - program_string=program, assumption_signatures=signatures, control=ctl - ) + muc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) literal_lookup = get_solver_literal_lookup(ctl) @@ -213,9 +189,7 @@ def test_core_computer_shrink_single_atomic_muc(self): """ signatures = {("a", 1)} - muc = self.get_muc_of_program( - program_string=program, assumption_signatures=signatures, control=ctl - ) + muc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) literal_lookup = get_solver_literal_lookup(ctl) @@ -236,15 +210,11 @@ def test_core_computer_shrink_multiple_atomic_mucs(self): """ signatures = {("a", 1)} - muc = self.get_muc_of_program( - program_string=program, assumption_signatures=signatures, control=ctl - ) + muc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) literal_lookup = get_solver_literal_lookup(ctl) - self.assert_muc( - {literal_lookup[a] for a in muc}, [{"a(3)"}, {"a(5)"}, {"a(9)"}] - ) + self.assert_muc({literal_lookup[a] for a in muc}, [{"a(3)"}, {"a(5)"}, {"a(9)"}]) def test_core_computer_shrink_multiple_mucs(self): """ @@ -261,9 +231,7 @@ def test_core_computer_shrink_multiple_mucs(self): """ signatures = {("a", 1)} - muc = self.get_muc_of_program( - program_string=program, assumption_signatures=signatures, control=ctl - ) + muc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) literal_lookup = get_solver_literal_lookup(ctl) @@ -291,15 +259,11 @@ def test_core_computer_shrink_large_instance_random(self): """ signatures = {("a", 1)} - muc = self.get_muc_of_program( - program_string=program, assumption_signatures=signatures, control=ctl - ) + muc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) literal_lookup = get_solver_literal_lookup(ctl) - self.assert_muc( - {literal_lookup[a] for a in muc}, [{f"a({i})" for i in random_core}] - ) + self.assert_muc({literal_lookup[a] for a in muc}, [{f"a({i})" for i in random_core}]) def test_core_computer_shrink_satisfiable(self): """ @@ -313,9 +277,7 @@ def test_core_computer_shrink_satisfiable(self): """ signatures = {("a", 1)} - muc = self.get_muc_of_program( - program_string=program, assumption_signatures=signatures, control=ctl - ) + muc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) self.assertEqual(muc, set()) @@ -352,6 +314,4 @@ def test_core_computer_internal_compute_single_minimal_no_assumptions(self): control = clingo.Control() cc = CoreComputer(control, set()) - self.assertRaises( - ValueError, cc._compute_single_minimal # pylint: disable=W0212 - ) + self.assertRaises(ValueError, cc._compute_single_minimal) # pylint: disable=W0212 From d3d07b86f53af2a6998d26b50cc55e8d5b4ed95f Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Tue, 9 Apr 2024 15:44:05 +0200 Subject: [PATCH 69/82] fixed linting errors --- src/clingexplaid/cli/clingo_app.py | 19 +++++++---- src/clingexplaid/muc/core_computer.py | 4 +++ src/clingexplaid/propagators/constants.py | 4 +++ .../propagators/propagator_decision_order.py | 33 ++++++++++++++++++- src/clingexplaid/transformers/constants.py | 4 +++ src/clingexplaid/transformers/exceptions.py | 5 +++ .../transformers/transformer_assumption.py | 9 +++-- .../transformers/transformer_constraint.py | 4 +++ .../transformers/transformer_fact.py | 9 +++++ .../transformer_optimization_remover.py | 9 +++++ .../transformers/transformer_rule_id.py | 4 +++ .../transformers/transformer_rule_splitter.py | 4 +++ .../unsat_constraints/constants.py | 4 +++ .../unsat_constraint_computer.py | 19 +++++++++-- src/clingexplaid/utils/__init__.py | 6 +++- 15 files changed, 124 insertions(+), 13 deletions(-) diff --git a/src/clingexplaid/cli/clingo_app.py b/src/clingexplaid/cli/clingo_app.py index a0f43c4..aae0106 100644 --- a/src/clingexplaid/cli/clingo_app.py +++ b/src/clingexplaid/cli/clingo_app.py @@ -1,3 +1,7 @@ +""" +App Module: clingexplaid CLI clingo app +""" + import re import sys from importlib.metadata import version @@ -27,6 +31,8 @@ class ClingoExplaidApp(Application): Application class for executing clingo-explaid functionality on the command line """ + # pylint: disable = too-many-instance-attributes + CLINGEXPLAID_METHODS = { "muc": "Description for MUC method", "unsat-constraints": "Description for unsat-constraints method", @@ -36,11 +42,9 @@ class ClingoExplaidApp(Application): def __init__(self, name): # pylint: disable = unused-argument self.methods = set() - self.method_functions = { - m: getattr(self, f'_method_{m.replace("-", "_")}') for m in self.CLINGEXPLAID_METHODS.keys() - } - self.method_flags = {m: Flag() for m in self.CLINGEXPLAID_METHODS.keys()} - self.argument_constants = dict() + self.method_functions = {m: getattr(self, f'_method_{m.replace("-", "_")}') for m in self.CLINGEXPLAID_METHODS} + self.method_flags = {m: Flag() for m in self.CLINGEXPLAID_METHODS} + self.argument_constants = {} # SHOW DECISIONS self._show_decisions_decision_signatures = {} @@ -59,7 +63,7 @@ def _initialize(self) -> None: if len(self.methods) == 0: raise ValueError( f"Clingexplaid was called without any method, please select at least one of the following methods: " - f"[{', '.join(['--' + str(m) for m in self.CLINGEXPLAID_METHODS.keys()])}]" + f"[{', '.join(['--' + str(m) for m in self.CLINGEXPLAID_METHODS])}]" ) @staticmethod @@ -257,7 +261,8 @@ def _print_unsat_constraints( if location is not None: print( - f"{prefix}{COLORS['RED']}{constraint}{COLORS['GREY']} [ {file_link} ]({line_string}){COLORS['NORMAL']}" + f"{prefix}{COLORS['RED']}{constraint}" + f"{COLORS['GREY']} [ {file_link} ]({line_string}){COLORS['NORMAL']}" ) else: print(f"{prefix}{COLORS['RED']}{constraint}{COLORS['NORMAL']}") diff --git a/src/clingexplaid/muc/core_computer.py b/src/clingexplaid/muc/core_computer.py index 73cdcd1..ad2e0e0 100644 --- a/src/clingexplaid/muc/core_computer.py +++ b/src/clingexplaid/muc/core_computer.py @@ -1,3 +1,7 @@ +""" +MUC Module: Core Computer to get Minimal Unsatisfiable Cores +""" + from typing import Optional, Set, Tuple from itertools import chain, combinations diff --git a/src/clingexplaid/propagators/constants.py b/src/clingexplaid/propagators/constants.py index f10f872..edddc78 100644 --- a/src/clingexplaid/propagators/constants.py +++ b/src/clingexplaid/propagators/constants.py @@ -1,3 +1,7 @@ +""" +Constant definitions for the propagators package +""" + from ..utils.logging import COLORS UNKNOWN_SYMBOL_TOKEN = "INTERNAL" diff --git a/src/clingexplaid/propagators/propagator_decision_order.py b/src/clingexplaid/propagators/propagator_decision_order.py index 3d0bbd2..b8d7522 100644 --- a/src/clingexplaid/propagators/propagator_decision_order.py +++ b/src/clingexplaid/propagators/propagator_decision_order.py @@ -1,3 +1,7 @@ +""" +Propagator Module: Decision Order +""" + from typing import Optional, Tuple, Set import clingo @@ -7,7 +11,12 @@ class DecisionOrderPropagator: + """ + Propagator for showing the Decision Order of clingo + """ + def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None, prefix: str = ""): + # pylint: disable=missing-function-docstring self.slit_symbol_lookup = {} self.signatures = signatures if signatures is not None else set() self.prefix = prefix @@ -16,6 +25,9 @@ def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None, prefix: st self.last_entailments = {} def init(self, init): + """ + Method to initialize the Decision Order Propagator. Here the literals are added to the Propagator's watch list. + """ for atom in init.symbolic_atoms: program_literal = atom.literal solver_literal = init.solver_literal(program_literal) @@ -29,7 +41,10 @@ def init(self, init): init.add_watch(query_solver_literal) init.add_watch(-query_solver_literal) - def _is_printed(self, symbol): + def _is_printed(self, symbol: clingo.Symbol) -> bool: + """ + Helper function to check if a specific symbol should be printed or not + """ printed = True # skip UNKNOWN print if signatures is set if len(self.signatures) > 0 and symbol == UNKNOWN_SYMBOL_TOKEN: @@ -42,6 +57,11 @@ def _is_printed(self, symbol): return printed def propagate(self, control, changes) -> None: + """ + Propagate method the is called when one the registered literals is propagated by clasp. Here useful information + about the decision progress is recorded to be visualized later. + """ + # pylint: disable=unused-argument decisions, entailments = self.get_decisions(control.assignment) print_level = 0 @@ -93,6 +113,11 @@ def propagate(self, control, changes) -> None: self.last_entailments = entailments def undo(self, thread_id: int, assignment, changes) -> None: + """ + This function is called when one of the solvers decisions is undone. + """ + # pylint: disable=unused-argument + if len(self.last_decisions) < 1: return decision = self.last_decisions[-1] @@ -108,6 +133,9 @@ def undo(self, thread_id: int, assignment, changes) -> None: @staticmethod def get_decisions(assignment): + """ + Helper function to extract a list of decisions and entailments from a clingo propagator assignment. + """ level = 0 decisions = [] entailments = {} @@ -127,6 +155,9 @@ def get_decisions(assignment): return decisions, entailments def get_symbol(self, literal) -> clingo.Symbol: + """ + Helper function to get a literal's associated symbol. + """ try: if literal > 0: symbol = self.slit_symbol_lookup[literal] diff --git a/src/clingexplaid/transformers/constants.py b/src/clingexplaid/transformers/constants.py index 80979a1..e940e8b 100644 --- a/src/clingexplaid/transformers/constants.py +++ b/src/clingexplaid/transformers/constants.py @@ -1,2 +1,6 @@ +""" +Constant definitions for the transformers package +""" + REMOVED_TOKEN = "__REMOVED__" RULE_ID_SIGNATURE = "_rule" diff --git a/src/clingexplaid/transformers/exceptions.py b/src/clingexplaid/transformers/exceptions.py index f5daafd..c97cf37 100644 --- a/src/clingexplaid/transformers/exceptions.py +++ b/src/clingexplaid/transformers/exceptions.py @@ -1,3 +1,8 @@ +""" +Exceptions for the Transformers Module +""" + + class UntransformedException(Exception): """Exception raised if the get_assumptions method of an AssumptionTransformer is called before it is used to transform a program. diff --git a/src/clingexplaid/transformers/transformer_assumption.py b/src/clingexplaid/transformers/transformer_assumption.py index 099db5a..40d5b50 100644 --- a/src/clingexplaid/transformers/transformer_assumption.py +++ b/src/clingexplaid/transformers/transformer_assumption.py @@ -1,3 +1,7 @@ +""" +Transformer Module: Assumption Transformer for converting facts to choices that can be assumed +""" + from pathlib import Path from typing import Dict, List, Optional, Sequence, Set, Tuple, Union @@ -52,6 +56,7 @@ def visit_Definition(self, node): """ All defined constants of the program are stored in self.program_constants """ + # pylint: disable=invalid-name self.program_constants[node.name] = node.value.symbol return node @@ -84,13 +89,13 @@ def get_assumptions(self, control: clingo.Control, constants: Optional[Dict] = N # which means we cannot detect them like this anymore. if not self.transformed: raise UntransformedException( - "The get_assumptions method cannot be called before a program has been " "transformed" + "The get_assumptions method cannot be called before a program has been transformed" ) # If the control has not been grounded yet except since without grounding we don't have access to the symbolic # atoms. if len(control.symbolic_atoms) == 0: raise NotGroundedException( - "The get_assumptions method cannot be called before the control has been " "grounded" + "The get_assumptions method cannot be called before the control has been grounded" ) constants = constants if constants is not None else {} diff --git a/src/clingexplaid/transformers/transformer_constraint.py b/src/clingexplaid/transformers/transformer_constraint.py index f50eb08..065be01 100644 --- a/src/clingexplaid/transformers/transformer_constraint.py +++ b/src/clingexplaid/transformers/transformer_constraint.py @@ -1,3 +1,7 @@ +""" +Transformer Module: Adding atoms to constraint heads to retrace the ones firing in the case of an unsatisfiable program. +""" + from pathlib import Path from typing import Sequence, Union diff --git a/src/clingexplaid/transformers/transformer_fact.py b/src/clingexplaid/transformers/transformer_fact.py index aaa81f3..738dadd 100644 --- a/src/clingexplaid/transformers/transformer_fact.py +++ b/src/clingexplaid/transformers/transformer_fact.py @@ -1,3 +1,7 @@ +""" +Transformer Module: Fact Remover +""" + from pathlib import Path from typing import Optional, Sequence, Set, Tuple, Union @@ -12,6 +16,8 @@ class FactTransformer(_ast.Transformer): Transformer that removes all facts from a program that match provided signatures """ + # pylint: disable=duplicate-code + def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None): self.signatures = signatures if signatures is not None else set() @@ -38,6 +44,9 @@ def visit_Rule(self, node): # pylint: disable=C0103 @staticmethod def post_transform(program_string: str) -> str: + """ + Helper function that is called after the transformation process for cleanup purposes + """ # remove the transformed REMOVED_TOKENS from the resulting program string rules = program_string.split("\n") out = [] diff --git a/src/clingexplaid/transformers/transformer_optimization_remover.py b/src/clingexplaid/transformers/transformer_optimization_remover.py index 45e5198..d4e354b 100644 --- a/src/clingexplaid/transformers/transformer_optimization_remover.py +++ b/src/clingexplaid/transformers/transformer_optimization_remover.py @@ -1,3 +1,7 @@ +""" +Transformer Module: Removing all optimization statements +""" + from pathlib import Path from typing import Sequence, Union @@ -11,6 +15,8 @@ class OptimizationRemover(_ast.Transformer): Transformer that removes all optimization statements """ + # pylint: disable=duplicate-code + def visit_Minimize(self, node): # pylint: disable=C0103 """ Removes all facts from a program that match the given signatures (if none are given all facts are removed). @@ -23,6 +29,9 @@ def visit_Minimize(self, node): # pylint: disable=C0103 @staticmethod def post_transform(program_string: str) -> str: + """ + Helper function that is called after the transformation process for cleanup purposes + """ # remove the transformed REMOVED_TOKENS from the resulting program string rules = program_string.split("\n") out = [] diff --git a/src/clingexplaid/transformers/transformer_rule_id.py b/src/clingexplaid/transformers/transformer_rule_id.py index 95e17f9..21d7da6 100644 --- a/src/clingexplaid/transformers/transformer_rule_id.py +++ b/src/clingexplaid/transformers/transformer_rule_id.py @@ -1,3 +1,7 @@ +""" +Transformer Module: Adding unique rule identifiers to the body of rules +""" + from pathlib import Path from typing import Optional, Set, Tuple, Union diff --git a/src/clingexplaid/transformers/transformer_rule_splitter.py b/src/clingexplaid/transformers/transformer_rule_splitter.py index 7b3edb6..ab21bff 100644 --- a/src/clingexplaid/transformers/transformer_rule_splitter.py +++ b/src/clingexplaid/transformers/transformer_rule_splitter.py @@ -1,3 +1,7 @@ +""" +Transformer Module: Split Rules into dedicated body and head parts +""" + import base64 from pathlib import Path from typing import Union diff --git a/src/clingexplaid/unsat_constraints/constants.py b/src/clingexplaid/unsat_constraints/constants.py index 947c035..84b97f6 100644 --- a/src/clingexplaid/unsat_constraints/constants.py +++ b/src/clingexplaid/unsat_constraints/constants.py @@ -1 +1,5 @@ +""" +Constant definitions for the unsat_constraints package +""" + UNSAT_CONSTRAINT_SIGNATURE = "__unsat__" diff --git a/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py b/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py index e5d234c..1388e50 100644 --- a/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py +++ b/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py @@ -27,15 +27,21 @@ def __init__( self.program_transformed = None self.initialized = False - self._file_constraint_lookup = dict() + self._file_constraint_lookup = {} def parse_string(self, program_string: str) -> None: + """ + Method to parse a provided program string + """ ct = ConstraintTransformer(UNSAT_CONSTRAINT_SIGNATURE, include_id=True) self.program_transformed = ct.parse_string(program_string) self._file_constraint_lookup = ct.constraint_location_lookup self.initialized = True def parse_files(self, files: List[str]) -> None: + """ + Method to parse a provided sequence of filenames + """ ct = ConstraintTransformer(UNSAT_CONSTRAINT_SIGNATURE, include_id=True) if not files: program_transformed = ct.parse_files("-") @@ -51,9 +57,16 @@ def parse_files(self, files: List[str]) -> None: self.initialized = True def get_constraint_location(self, constraint_id: int) -> Optional[Location]: + """ + Method to get the file that a constraint (identified by its `constraint_id`) is located in. + """ return self._file_constraint_lookup.get(constraint_id) def get_unsat_constraints(self, assumption_string: Optional[str] = None) -> Dict[int, str]: + """ + Method to get the unsat constraints of an initialized `UnsatConstraintComputer` Object. + """ + # only execute if the UnsatConstraintComputer was properly initialized if not self.initialized: raise ValueError( @@ -78,7 +91,9 @@ def get_unsat_constraints(self, assumption_string: Optional[str] = None) -> Dict # create a rule lookup for every constraint in the program associated with it's unsat id constraint_lookup = {} for line in program_string.split("\n"): - id_re = re.compile(f"{UNSAT_CONSTRAINT_SIGNATURE}\(([1-9][0-9]*)\)") + id_re = re.compile( + f"{UNSAT_CONSTRAINT_SIGNATURE}\(([1-9][0-9]*)\)" # pylint: disable=anomalous-backslash-in-string) + ) match_result = id_re.match(line) if match_result is None: continue diff --git a/src/clingexplaid/utils/__init__.py b/src/clingexplaid/utils/__init__.py index ecb64aa..0df04bd 100644 --- a/src/clingexplaid/utils/__init__.py +++ b/src/clingexplaid/utils/__init__.py @@ -80,7 +80,11 @@ def get_signatures_from_model_string(model_string: str) -> Set[Tuple[str, int]]: def get_constants_from_arguments(argument_vector: List[str]) -> Dict: - constants = dict() + """ + Function that is used to parse the command line argument vector to extract a dictionary of provided constants and + their values. For example "-c test=42" would be converted to {"test": "42"}. + """ + constants = {} next_constant = False for element in argument_vector: if next_constant: From caf597a0c32bda189afdf3e9db13974f9e737db8 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Tue, 9 Apr 2024 17:23:41 +0200 Subject: [PATCH 70/82] fixed typing issues --- src/clingexplaid/muc/__init__.py | 4 ++ src/clingexplaid/muc/core_computer.py | 10 ++-- src/clingexplaid/propagators/__init__.py | 4 ++ .../propagators/propagator_decision_order.py | 34 ++++++----- src/clingexplaid/transformers/__init__.py | 9 +++ .../transformers/transformer_assumption.py | 8 +-- .../transformers/transformer_constraint.py | 6 +- .../transformers/transformer_fact.py | 3 +- .../transformer_optimization_remover.py | 3 +- .../transformers/transformer_rule_id.py | 4 +- .../transformers/transformer_rule_splitter.py | 8 +-- .../unsat_constraints/__init__.py | 4 ++ .../unsat_constraint_computer.py | 12 ++-- src/clingexplaid/utils/__init__.py | 6 +- tests/clingexplaid/test_main.py | 56 +++++++++++-------- 15 files changed, 105 insertions(+), 66 deletions(-) diff --git a/src/clingexplaid/muc/__init__.py b/src/clingexplaid/muc/__init__.py index a128ac4..37371fc 100644 --- a/src/clingexplaid/muc/__init__.py +++ b/src/clingexplaid/muc/__init__.py @@ -3,3 +3,7 @@ """ from .core_computer import CoreComputer + +__all__ = [ + "CoreComputer", +] diff --git a/src/clingexplaid/muc/core_computer.py b/src/clingexplaid/muc/core_computer.py index ad2e0e0..ca352f3 100644 --- a/src/clingexplaid/muc/core_computer.py +++ b/src/clingexplaid/muc/core_computer.py @@ -2,7 +2,7 @@ MUC Module: Core Computer to get Minimal Unsatisfiable Cores """ -from typing import Optional, Set, Tuple +from typing import Optional, Set, Tuple, Generator, List from itertools import chain, combinations import clingo @@ -29,7 +29,7 @@ def _solve(self, assumptions: Optional[AssumptionSet] = None) -> Tuple[bool, Sym if assumptions is None: assumptions = self.assumption_set - with self.control.solve(assumptions=list(assumptions), yield_=True) as solve_handle: # type: ignore[union-attr] + with self.control.solve(assumptions=list(assumptions), yield_=True) as solve_handle: satisfiable = bool(solve_handle.get().satisfiable) model = solve_handle.model().symbols(atoms=True) if solve_handle.model() is not None else [] core = {self.literal_lookup[literal_id] for literal_id in solve_handle.core()} @@ -81,7 +81,7 @@ def shrink(self, assumptions: Optional[AssumptionSet] = None) -> None: """ self.minimal = self._compute_single_minimal(assumptions=assumptions) - def get_multiple_minimal(self, max_mucs: Optional[int] = None): + def get_multiple_minimal(self, max_mucs: Optional[int] = None) -> Generator[AssumptionSet, None, None]: """ This function generates all minimal unsatisfiable cores of the provided assumption set. It implements the generator pattern since finding all mucs of an assumption set is exponential in nature and the search might not @@ -93,8 +93,8 @@ def get_multiple_minimal(self, max_mucs: Optional[int] = None): combinations(assumptions, r) for r in reversed(range(len(list(assumptions)) + 1)) ) - found_sat = [] - found_mucs = [] + found_sat: List[AssumptionSet] = [] + found_mucs: List[AssumptionSet] = [] for current_subset in (set(s) for s in assumption_powerset): # skip if empty subset diff --git a/src/clingexplaid/propagators/__init__.py b/src/clingexplaid/propagators/__init__.py index d05fdd8..5306e6a 100644 --- a/src/clingexplaid/propagators/__init__.py +++ b/src/clingexplaid/propagators/__init__.py @@ -8,3 +8,7 @@ DecisionLevel = List[int] DecisionLevelList = List[DecisionLevel] + +__all__ = [ + "DecisionOrderPropagator", +] diff --git a/src/clingexplaid/propagators/propagator_decision_order.py b/src/clingexplaid/propagators/propagator_decision_order.py index b8d7522..23a9b51 100644 --- a/src/clingexplaid/propagators/propagator_decision_order.py +++ b/src/clingexplaid/propagators/propagator_decision_order.py @@ -2,7 +2,7 @@ Propagator Module: Decision Order """ -from typing import Optional, Tuple, Set +from typing import Optional, Tuple, Set, Dict, List, Sequence, Union import clingo @@ -17,14 +17,14 @@ class DecisionOrderPropagator: def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None, prefix: str = ""): # pylint: disable=missing-function-docstring - self.slit_symbol_lookup = {} + self.slit_symbol_lookup: Dict[int, clingo.Symbol] = {} self.signatures = signatures if signatures is not None else set() self.prefix = prefix - self.last_decisions = [] - self.last_entailments = {} + self.last_decisions: List[int] = [] + self.last_entailments: Dict[int, List[int]] = {} - def init(self, init): + def init(self, init: clingo.PropagateInit) -> None: """ Method to initialize the Decision Order Propagator. Here the literals are added to the Propagator's watch list. """ @@ -36,12 +36,15 @@ def init(self, init): for atom in init.symbolic_atoms: if len(self.signatures) > 0 and not any(atom.match(name=s, arity=a) for s, a in self.signatures): continue - query_program_literal = init.symbolic_atoms[atom.symbol].literal + symbolic_atom = init.symbolic_atoms[atom.symbol] + if symbolic_atom is None: + continue + query_program_literal = symbolic_atom.literal query_solver_literal = init.solver_literal(query_program_literal) init.add_watch(query_solver_literal) init.add_watch(-query_solver_literal) - def _is_printed(self, symbol: clingo.Symbol) -> bool: + def _is_printed(self, symbol: Union[clingo.Symbol, str]) -> bool: """ Helper function to check if a specific symbol should be printed or not """ @@ -50,13 +53,16 @@ def _is_printed(self, symbol: clingo.Symbol) -> bool: if len(self.signatures) > 0 and symbol == UNKNOWN_SYMBOL_TOKEN: printed = False # skip if symbol signature is not in self.signatures - if len(self.signatures) > 0 and symbol != UNKNOWN_SYMBOL_TOKEN: - if not any(symbol.match(s, a) for s, a in self.signatures): + elif len(self.signatures) > 0 and symbol != UNKNOWN_SYMBOL_TOKEN: + # `symbol` can only be a `str` if it is the UNKNOWN_SYMBOL_TOKEN + if isinstance(symbol, str): + printed = False + elif not any(symbol.match(s, a) for s, a in self.signatures): printed = False return printed - def propagate(self, control, changes) -> None: + def propagate(self, control: clingo.PropagateControl, changes: Sequence[int]) -> None: """ Propagate method the is called when one the registered literals is propagated by clasp. Here useful information about the decision progress is recorded to be visualized later. @@ -112,7 +118,7 @@ def propagate(self, control, changes) -> None: self.last_decisions = decisions self.last_entailments = entailments - def undo(self, thread_id: int, assignment, changes) -> None: + def undo(self, thread_id: int, assignment: clingo.Assignment, changes: Sequence[int]) -> None: """ This function is called when one of the solvers decisions is undone. """ @@ -132,7 +138,7 @@ def undo(self, thread_id: int, assignment, changes) -> None: self.last_decisions = self.last_decisions[:-1] @staticmethod - def get_decisions(assignment): + def get_decisions(assignment: clingo.Assignment) -> Tuple[List[int], Dict[int, List[int]]]: """ Helper function to extract a list of decisions and entailments from a clingo propagator assignment. """ @@ -154,7 +160,7 @@ def get_decisions(assignment): except RuntimeError: return decisions, entailments - def get_symbol(self, literal) -> clingo.Symbol: + def get_symbol(self, literal: int) -> Union[clingo.Symbol, str]: """ Helper function to get a literal's associated symbol. """ @@ -166,5 +172,5 @@ def get_symbol(self, literal) -> clingo.Symbol: symbol = clingo.parse_term(str(self.slit_symbol_lookup[-literal])) except KeyError: # internal literals - symbol = UNKNOWN_SYMBOL_TOKEN + return UNKNOWN_SYMBOL_TOKEN return symbol diff --git a/src/clingexplaid/transformers/__init__.py b/src/clingexplaid/transformers/__init__.py index 36e7eb0..7f374c1 100644 --- a/src/clingexplaid/transformers/__init__.py +++ b/src/clingexplaid/transformers/__init__.py @@ -8,3 +8,12 @@ from .transformer_optimization_remover import OptimizationRemover from .transformer_rule_id import RuleIDTransformer from .transformer_rule_splitter import RuleSplitter + +__all__ = [ + "AssumptionTransformer", + "ConstraintTransformer", + "FactTransformer", + "OptimizationRemover", + "RuleIDTransformer", + "RuleSplitter", +] diff --git a/src/clingexplaid/transformers/transformer_assumption.py b/src/clingexplaid/transformers/transformer_assumption.py index 40d5b50..f1f10c9 100644 --- a/src/clingexplaid/transformers/transformer_assumption.py +++ b/src/clingexplaid/transformers/transformer_assumption.py @@ -22,9 +22,9 @@ def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None): self.signatures = signatures if signatures is not None else set() self.fact_rules: List[str] = [] self.transformed: bool = False - self.program_constants = {} + self.program_constants: Dict[str, str] = {} - def visit_Rule(self, node): # pylint: disable=C0103 + def visit_Rule(self, node: clingo.ast.AST) -> clingo.ast.AST: # pylint: disable=C0103 """ Transforms head of a rule into a choice rule if it is a fact and adheres to the given signatures. """ @@ -52,7 +52,7 @@ def visit_Rule(self, node): # pylint: disable=C0103 body=[], ) - def visit_Definition(self, node): + def visit_Definition(self, node: clingo.ast.AST) -> clingo.ast.AST: """ All defined constants of the program are stored in self.program_constants """ @@ -79,7 +79,7 @@ def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: self.transformed = True return "\n".join(out) - def get_assumptions(self, control: clingo.Control, constants: Optional[Dict] = None) -> Set[int]: + def get_assumptions(self, control: clingo.Control, constants: Optional[Dict[str, str]] = None) -> Set[int]: """ Returns the assumptions which were gathered during the transformation of the program. Has to be called after a program has already been transformed. diff --git a/src/clingexplaid/transformers/transformer_constraint.py b/src/clingexplaid/transformers/transformer_constraint.py index 065be01..86b37f0 100644 --- a/src/clingexplaid/transformers/transformer_constraint.py +++ b/src/clingexplaid/transformers/transformer_constraint.py @@ -3,7 +3,7 @@ """ from pathlib import Path -from typing import Sequence, Union +from typing import Sequence, Union, Dict import clingo import clingo.ast as _ast @@ -19,9 +19,9 @@ def __init__(self, constraint_head_symbol: str, include_id: bool = False): self._include_id = include_id self._constraint_id = 1 - self.constraint_location_lookup = {} + self.constraint_location_lookup: Dict[int, clingo.ast.Location] = {} - def visit_Rule(self, node): # pylint: disable=C0103 + def visit_Rule(self, node: clingo.ast.AST) -> clingo.ast.AST: # pylint: disable=C0103 """ Adds a constraint_head_symbol atom to the head of every constraint. """ diff --git a/src/clingexplaid/transformers/transformer_fact.py b/src/clingexplaid/transformers/transformer_fact.py index 738dadd..19a70d0 100644 --- a/src/clingexplaid/transformers/transformer_fact.py +++ b/src/clingexplaid/transformers/transformer_fact.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import Optional, Sequence, Set, Tuple, Union +import clingo.ast import clingo.ast as _ast from .constants import REMOVED_TOKEN @@ -21,7 +22,7 @@ class FactTransformer(_ast.Transformer): def __init__(self, signatures: Optional[Set[Tuple[str, int]]] = None): self.signatures = signatures if signatures is not None else set() - def visit_Rule(self, node): # pylint: disable=C0103 + def visit_Rule(self, node: clingo.ast.AST) -> clingo.ast.AST: # pylint: disable=C0103 """ Removes all facts from a program that match the given signatures (if none are given all facts are removed). """ diff --git a/src/clingexplaid/transformers/transformer_optimization_remover.py b/src/clingexplaid/transformers/transformer_optimization_remover.py index d4e354b..83afde1 100644 --- a/src/clingexplaid/transformers/transformer_optimization_remover.py +++ b/src/clingexplaid/transformers/transformer_optimization_remover.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import Sequence, Union +import clingo.ast import clingo.ast as _ast from .constants import REMOVED_TOKEN @@ -17,7 +18,7 @@ class OptimizationRemover(_ast.Transformer): # pylint: disable=duplicate-code - def visit_Minimize(self, node): # pylint: disable=C0103 + def visit_Minimize(self, node: clingo.ast.AST) -> clingo.ast.AST: # pylint: disable=C0103 """ Removes all facts from a program that match the given signatures (if none are given all facts are removed). """ diff --git a/src/clingexplaid/transformers/transformer_rule_id.py b/src/clingexplaid/transformers/transformer_rule_id.py index 21d7da6..b878bf8 100644 --- a/src/clingexplaid/transformers/transformer_rule_id.py +++ b/src/clingexplaid/transformers/transformer_rule_id.py @@ -24,7 +24,7 @@ def __init__(self, rule_id_signature: str = RULE_ID_SIGNATURE): self.rule_id = 0 self.rule_id_signature = rule_id_signature - def visit_Rule(self, node): # pylint: disable=C0103 + def visit_Rule(self, node: clingo.ast.AST) -> clingo.ast.AST: # pylint: disable=C0103 """ Adds a rule_id_signature(id) atom to the body of every rule that is visited. """ @@ -43,7 +43,7 @@ def visit_Rule(self, node): # pylint: disable=C0103 node.body.insert(len(node.body), symbol) return node.update(**self.visit_children(node)) - def _get_number_of_rules(self): + def _get_number_of_rules(self) -> int: return self.rule_id - 1 if self.rule_id > 1 else self.rule_id def parse_string(self, string: str) -> str: diff --git a/src/clingexplaid/transformers/transformer_rule_splitter.py b/src/clingexplaid/transformers/transformer_rule_splitter.py index ab21bff..e09145e 100644 --- a/src/clingexplaid/transformers/transformer_rule_splitter.py +++ b/src/clingexplaid/transformers/transformer_rule_splitter.py @@ -4,7 +4,7 @@ import base64 from pathlib import Path -from typing import Union +from typing import Union, List import clingo import clingo.ast as _ast @@ -19,10 +19,10 @@ class RuleSplitter(_ast.Transformer): transformer. """ - def __init__(self): - self.head_rules = [] + def __init__(self) -> None: + self.head_rules: List[clingo.ast.AST] = [] - def visit_Rule(self, node): # pylint: disable=C0103 + def visit_Rule(self, node: clingo.ast.AST) -> clingo.ast.AST: # pylint: disable=C0103 """ Replaces the head of every rule with the intermediate `_body` predicate and stores all new head rules using this intermediary predicate in `self.head_rules` diff --git a/src/clingexplaid/unsat_constraints/__init__.py b/src/clingexplaid/unsat_constraints/__init__.py index ab248d9..59d320a 100644 --- a/src/clingexplaid/unsat_constraints/__init__.py +++ b/src/clingexplaid/unsat_constraints/__init__.py @@ -3,3 +3,7 @@ """ from .unsat_constraint_computer import UnsatConstraintComputer + +__all__ = [ + "UnsatConstraintComputer", +] diff --git a/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py b/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py index 1388e50..d51487b 100644 --- a/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py +++ b/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py @@ -24,10 +24,10 @@ def __init__( control: Optional[clingo.Control] = None, ): self.control = control if control is not None else clingo.Control() - self.program_transformed = None - self.initialized = False + self.program_transformed: Optional[str] = None + self.initialized: bool = False - self._file_constraint_lookup = {} + self._file_constraint_lookup: Dict[int, clingo.ast.Location] = {} def parse_string(self, program_string: str) -> None: """ @@ -74,7 +74,7 @@ def get_unsat_constraints(self, assumption_string: Optional[str] = None) -> Dict "or `parse_string`." ) - program_string = self.program_transformed + program_string = str(self.program_transformed) # if an assumption string is provided use a FactTransformer to remove interfering facts if assumption_string is not None and len(assumption_string) > 0: assumptions_signatures = get_signatures_from_model_string(assumption_string) @@ -114,10 +114,10 @@ def get_unsat_constraints(self, assumption_string: Optional[str] = None) -> Dict ] solve_handle.resume() model = solve_handle.model() - unsat_constraints = {} + unsat_constraints: Dict[int, str] = {} for a in unsat_constraint_atoms: constraint_id = a.arguments[0].number - constraint = constraint_lookup.get(constraint_id) + constraint = str(constraint_lookup.get(constraint_id)) unsat_constraints[constraint_id] = constraint return unsat_constraints diff --git a/src/clingexplaid/utils/__init__.py b/src/clingexplaid/utils/__init__.py index 0df04bd..d4532f6 100644 --- a/src/clingexplaid/utils/__init__.py +++ b/src/clingexplaid/utils/__init__.py @@ -18,7 +18,7 @@ def match_ast_symbolic_atom_signature( ast_symbol: ASTType.SymbolicAtom, signature: Tuple[str, int] -): +) -> bool: """ Function to match the signature of an AST SymbolicAtom to a tuple containing a string and int value, representing a matching signature. @@ -79,7 +79,7 @@ def get_signatures_from_model_string(model_string: str) -> Set[Tuple[str, int]]: return signatures -def get_constants_from_arguments(argument_vector: List[str]) -> Dict: +def get_constants_from_arguments(argument_vector: List[str]) -> Dict[str, str]: """ Function that is used to parse the command line argument vector to extract a dictionary of provided constants and their values. For example "-c test=42" would be converted to {"test": "42"}. @@ -89,7 +89,7 @@ def get_constants_from_arguments(argument_vector: List[str]) -> Dict: for element in argument_vector: if next_constant: result = re.search(r"(.*)=(.*)", element) - if len(result.groups()) == 0: + if result is None or len(result.groups()) == 0: continue constants[result.group(1)] = result.group(2) next_constant = False diff --git a/tests/clingexplaid/test_main.py b/tests/clingexplaid/test_main.py index 5208bf0..fbffe2b 100644 --- a/tests/clingexplaid/test_main.py +++ b/tests/clingexplaid/test_main.py @@ -4,7 +4,7 @@ import random from pathlib import Path -from typing import List, Optional, Set, Tuple, Union +from typing import List, Optional, Set, Tuple, Union, Dict from unittest import TestCase import clingo @@ -65,19 +65,29 @@ def get_muc_of_program( def assert_muc( self, - muc: Set[Tuple[clingo.Symbol, bool]], + muc: Set[str], valid_mucs_string_lists: List[Set[str]], - ): + ) -> None: """ Asserts if a MUC is one of several valid MUC's. """ valid_mucs = [{clingo.parse_term(s) for s in lit_strings} for lit_strings in valid_mucs_string_lists] self.assertIn(muc, valid_mucs) + @staticmethod + def muc_to_string(muc: AssumptionSet, literal_lookup: Dict[int, clingo.Symbol]) -> Set[str]: + muc_string = set() + for a in muc: + if isinstance(a, int): + muc_string.add(str(literal_lookup[a])) + else: + muc_string.add(str(a[0])) + return muc_string + # TRANSFORMERS # --- ASSUMPTION TRANSFORMER - def test_assumption_transformer_parse_file(self): + def test_assumption_transformer_parse_file(self) -> None: """ Test the AssumptionTransformer's `parse_file` method. """ @@ -87,7 +97,7 @@ def test_assumption_transformer_parse_file(self): result = at.parse_files([program_path]) self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) - def test_assumption_transformer_parse_file_no_signatures(self): + def test_assumption_transformer_parse_file_no_signatures(self) -> None: """ Test the AssumptionTransformer's `parse_file` method with no signatures provided. """ @@ -97,7 +107,7 @@ def test_assumption_transformer_parse_file_no_signatures(self): result = at.parse_files([program_path]) self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) - def test_assumption_transformer_get_assumptions_before_transformation(self): + def test_assumption_transformer_get_assumptions_before_transformation(self) -> None: """ Test the AssumptionTransformer's behavior when get_assumptions is called before transformation. """ @@ -107,7 +117,7 @@ def test_assumption_transformer_get_assumptions_before_transformation(self): # --- RULE ID TRANSFORMER - def test_rule_id_transformer(self): + def test_rule_id_transformer(self) -> None: """ Test the RuleIDTransformer's `parse_file` and `get_assumptions` methods. """ @@ -132,7 +142,7 @@ def test_rule_id_transformer(self): # --- CONSTRAINT TRANSFORMER - def test_constraint_transformer(self): + def test_constraint_transformer(self) -> None: """ Test the ConstraintTransformer's `parse_file` method. """ @@ -144,7 +154,7 @@ def test_constraint_transformer(self): # --- RULE SPLITTER - def test_rule_splitter(self): + def test_rule_splitter(self) -> None: """ Test the RuleSplitter's `parse_file` method. """ @@ -157,7 +167,7 @@ def test_rule_splitter(self): # MUC - def test_core_computer_shrink_single_muc(self): + def test_core_computer_shrink_single_muc(self) -> None: """ Test the CoreComputer's `shrink` function with a single MUC. """ @@ -174,9 +184,9 @@ def test_core_computer_shrink_single_muc(self): literal_lookup = get_solver_literal_lookup(ctl) - self.assert_muc({literal_lookup[a] for a in muc}, [{"a(1)", "a(4)", "a(5)"}]) + self.assert_muc(self.muc_to_string(muc, literal_lookup), [{"a(1)", "a(4)", "a(5)"}]) - def test_core_computer_shrink_single_atomic_muc(self): + def test_core_computer_shrink_single_atomic_muc(self) -> None: """ Test the CoreComputer's `shrink` function with a single atomic MUC. """ @@ -193,9 +203,9 @@ def test_core_computer_shrink_single_atomic_muc(self): literal_lookup = get_solver_literal_lookup(ctl) - self.assert_muc({literal_lookup[a] for a in muc}, [{"a(3)"}]) + self.assert_muc(self.muc_to_string(muc, literal_lookup), [{"a(3)"}]) - def test_core_computer_shrink_multiple_atomic_mucs(self): + def test_core_computer_shrink_multiple_atomic_mucs(self) -> None: """ Test the CoreComputer's `shrink` function with multiple atomic MUC's. """ @@ -214,9 +224,9 @@ def test_core_computer_shrink_multiple_atomic_mucs(self): literal_lookup = get_solver_literal_lookup(ctl) - self.assert_muc({literal_lookup[a] for a in muc}, [{"a(3)"}, {"a(5)"}, {"a(9)"}]) + self.assert_muc(self.muc_to_string(muc, literal_lookup), [{"a(3)"}, {"a(5)"}, {"a(9)"}]) - def test_core_computer_shrink_multiple_mucs(self): + def test_core_computer_shrink_multiple_mucs(self) -> None: """ Test the CoreComputer's `shrink` function with multiple MUC's. """ @@ -236,7 +246,7 @@ def test_core_computer_shrink_multiple_mucs(self): literal_lookup = get_solver_literal_lookup(ctl) self.assert_muc( - {literal_lookup[a] for a in muc}, + self.muc_to_string(muc, literal_lookup), [ {"a(3)", "a(9)", "a(5)"}, {"a(5)", "a(1)", "a(2)"}, @@ -244,7 +254,7 @@ def test_core_computer_shrink_multiple_mucs(self): ], ) - def test_core_computer_shrink_large_instance_random(self): + def test_core_computer_shrink_large_instance_random(self) -> None: """ Test the CoreComputer's `shrink` function with a large random assumption set. """ @@ -263,9 +273,9 @@ def test_core_computer_shrink_large_instance_random(self): literal_lookup = get_solver_literal_lookup(ctl) - self.assert_muc({literal_lookup[a] for a in muc}, [{f"a({i})" for i in random_core}]) + self.assert_muc(self.muc_to_string(muc, literal_lookup), [{f"a({i})" for i in random_core}]) - def test_core_computer_shrink_satisfiable(self): + def test_core_computer_shrink_satisfiable(self) -> None: """ Test the CoreComputer's `shrink` function with a satisfiable assumption set. """ @@ -283,7 +293,7 @@ def test_core_computer_shrink_satisfiable(self): # --- INTERNAL - def test_core_computer_internal_solve_no_assumptions(self): + def test_core_computer_internal_solve_no_assumptions(self) -> None: """ Test the CoreComputer's `_solve` function with no assumptions. """ @@ -293,7 +303,7 @@ def test_core_computer_internal_solve_no_assumptions(self): satisfiable, _, _ = cc._solve() # pylint: disable=W0212 self.assertTrue(satisfiable) - def test_core_computer_internal_compute_single_minimal_satisfiable(self): + def test_core_computer_internal_compute_single_minimal_satisfiable(self) -> None: """ Test the CoreComputer's `_compute_single_minimal` function with a satisfiable assumption set. """ @@ -307,7 +317,7 @@ def test_core_computer_internal_compute_single_minimal_satisfiable(self): muc = cc._compute_single_minimal() # pylint: disable=W0212 self.assertEqual(muc, set()) - def test_core_computer_internal_compute_single_minimal_no_assumptions(self): + def test_core_computer_internal_compute_single_minimal_no_assumptions(self) -> None: """ Test the CoreComputer's `_compute_single_minimal` function with no assumptions. """ From 2edd24637f763d751e0a8d560f774e3d243ff3c0 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Wed, 10 Apr 2024 16:15:26 +0200 Subject: [PATCH 71/82] fixed more typing issues --- src/clingexplaid/cli/clingo_app.py | 58 +++++++++-------- src/clingexplaid/muc/core_computer.py | 18 +++++- .../transformers/transformer_fact.py | 16 ++--- .../transformer_optimization_remover.py | 15 +++-- tests/clingexplaid/test_main.py | 63 ++++++++----------- 5 files changed, 90 insertions(+), 80 deletions(-) diff --git a/src/clingexplaid/cli/clingo_app.py b/src/clingexplaid/cli/clingo_app.py index aae0106..e6b6b83 100644 --- a/src/clingexplaid/cli/clingo_app.py +++ b/src/clingexplaid/cli/clingo_app.py @@ -6,7 +6,8 @@ import sys from importlib.metadata import version from pathlib import Path -from typing import Dict, List, Tuple, Optional +from typing import Dict, List, Tuple, Optional, Set, Callable, Sequence +from warnings import warn import clingo @@ -16,7 +17,6 @@ from ..propagators import DecisionOrderPropagator from ..unsat_constraints import UnsatConstraintComputer from ..utils import ( - get_solver_literal_lookup, get_constants_from_arguments, ) from ..utils.logging import BACKGROUND_COLORS, COLORS @@ -39,20 +39,22 @@ class ClingoExplaidApp(Application): "show-decisions": "Visualize the decision process of clingo during solving", } - def __init__(self, name): + def __init__(self, name: str) -> None: # pylint: disable = unused-argument - self.methods = set() - self.method_functions = {m: getattr(self, f'_method_{m.replace("-", "_")}') for m in self.CLINGEXPLAID_METHODS} - self.method_flags = {m: Flag() for m in self.CLINGEXPLAID_METHODS} - self.argument_constants = {} + self.methods: Set[str] = set() + self.method_functions: Dict[str, Callable] = { # type: ignore + m: getattr(self, f'_method_{m.replace("-", "_")}') for m in self.CLINGEXPLAID_METHODS + } + self.method_flags: Dict[str, Flag] = {m: Flag() for m in self.CLINGEXPLAID_METHODS} + self.argument_constants: Dict[str, str] = {} # SHOW DECISIONS - self._show_decisions_decision_signatures = {} - self._show_decisions_model_id = 1 + self._show_decisions_decision_signatures: Dict[str, int] = {} + self._show_decisions_model_id: int = 1 # MUC - self._muc_assumption_signatures = {} - self._muc_id = 1 + self._muc_assumption_signatures: Dict[str, int] = {} + self._muc_id: int = 1 def _initialize(self) -> None: # add enabled methods to self.methods @@ -107,7 +109,7 @@ def _parse_decision_signature(self, decision_signature: str) -> bool: self._show_decisions_decision_signatures[signature] = arity return True - def register_options(self, options): + def register_options(self, options: clingo.ApplicationOptions) -> None: group = "Clingo-Explaid Methods" for method, description in self.CLINGEXPLAID_METHODS.items(): @@ -153,9 +155,9 @@ def _apply_assumption_transformer( program_transformed = at.parse_files(files) return program_transformed, at - def _print_muc(self, muc) -> None: + def _print_muc(self, muc_string: str) -> None: print(f"{BACKGROUND_COLORS['BLUE']} MUC {BACKGROUND_COLORS['LIGHT_BLUE']} {self._muc_id} {COLORS['NORMAL']}") - print(f"{COLORS['BLUE']}{muc}{COLORS['NORMAL']}") + print(f"{COLORS['BLUE']}{muc_string}{COLORS['NORMAL']}") self._muc_id += 1 def _method_muc( @@ -163,7 +165,7 @@ def _method_muc( control: clingo.Control, files: List[str], compute_unsat_constraints: bool = False, - ): + ) -> None: program_transformed, at = self._apply_assumption_transformer( signatures=self._muc_assumption_signatures, files=files ) @@ -175,11 +177,10 @@ def _method_muc( control.add("base", [], program_transformed) control.ground([("base", [])]) - literal_lookup = get_solver_literal_lookup(control) assumptions = at.get_assumptions(control, constants=self.argument_constants) cc = CoreComputer(control, assumptions) - max_models = int(control.configuration.solve.models) + max_models = int(control.configuration.solve.models) # type: ignore print("Solving...") # Case: Finding a single MUC @@ -189,13 +190,13 @@ def _method_muc( if cc.minimal is None: print("SATISFIABLE: Instance has no MUCs") return - if len(cc.minimal) == 0: + if len(list(cc.minimal)) == 0: print( "NO MUCS CONTAINED: The unsatisfiability of this program is not induced by the provided assumptions" ) return - muc_string = " ".join([str(literal_lookup[a]) for a in cc.minimal]) + muc_string = " ".join(cc.muc_to_string(cc.minimal)) self._print_muc(muc_string) if compute_unsat_constraints: @@ -218,7 +219,7 @@ def _method_muc( mucs = 0 for muc in cc.get_multiple_minimal(max_mucs=max_models): mucs += 1 - muc_string = " ".join([str(literal_lookup[a]) for a in muc]) + muc_string = " ".join(cc.muc_to_string(muc)) self._print_muc(muc_string) if compute_unsat_constraints: @@ -247,6 +248,9 @@ def _print_unsat_constraints( print(f"{prefix}{BACKGROUND_COLORS['RED']} Unsat Constraints {COLORS['NORMAL']}") for cid, constraint in unsat_constraints.items(): location = ucc.get_constraint_location(cid) + if location is None: + warn(f"Couldn't find a corresponding file for constraint with id {cid}") + continue relative_file_path = location.begin.filename absolute_file_path = str(Path(relative_file_path).absolute().resolve()) line_beginning = location.begin.line @@ -274,12 +278,12 @@ def _method_unsat_constraints( assumption_string: Optional[str] = None, output_prefix_active: str = "", output_prefix_passive: str = "", - ): + ) -> None: # register DecisionOrderPropagator if flag is enabled if self.method_flags["show-decisions"]: decision_signatures = set(self._show_decisions_decision_signatures.items()) dop = DecisionOrderPropagator(signatures=decision_signatures, prefix=output_prefix_passive) - control.register_propagator(dop) + control.register_propagator(dop) # type: ignore ucc = UnsatConstraintComputer(control=control) ucc.parse_files(files) @@ -288,7 +292,7 @@ def _method_unsat_constraints( def _print_model( self, - model, + model: clingo.Model, prefix_active: str = "", prefix_passive: str = "", ) -> None: @@ -307,10 +311,10 @@ def _method_show_decisions( self, control: clingo.Control, files: List[str], - ): + ) -> None: decision_signatures = set(self._show_decisions_decision_signatures.items()) dop = DecisionOrderPropagator(signatures=decision_signatures) - control.register_propagator(dop) + control.register_propagator(dop) # type: ignore for f in files: control.load(f) if not files: @@ -318,10 +322,10 @@ def _method_show_decisions( control.ground() control.solve(on_model=lambda model: self._print_model(model, "├", "│")) - def print_model(self, model, _): + def print_model(self, model: clingo.Model, _) -> None: # type: ignore return - def main(self, control, files): + def main(self, control: clingo.Control, files: Sequence[str]) -> None: print("clingexplaid", "version", version("clingexplaid")) self._initialize() diff --git a/src/clingexplaid/muc/core_computer.py b/src/clingexplaid/muc/core_computer.py index ca352f3..2e16890 100644 --- a/src/clingexplaid/muc/core_computer.py +++ b/src/clingexplaid/muc/core_computer.py @@ -2,7 +2,7 @@ MUC Module: Core Computer to get Minimal Unsatisfiable Cores """ -from typing import Optional, Set, Tuple, Generator, List +from typing import Optional, Set, Tuple, Generator, List, Dict from itertools import chain, combinations import clingo @@ -121,3 +121,19 @@ def get_multiple_minimal(self, max_mucs: Optional[int] = None) -> Generator[Assu # if the maximum muc amount is found stop search if max_mucs is not None and len(found_mucs) == max_mucs: break + + def muc_to_string(self, muc: AssumptionSet, literal_lookup: Optional[Dict[int, clingo.Symbol]] = None) -> Set[str]: + """ + Converts a MUC into a set containing the string representations of the contained assumptions + """ + # take class literal_lookup as default if no other is provided + if literal_lookup is None: + literal_lookup = self.literal_lookup + + muc_string = set() + for a in muc: + if isinstance(a, int): + muc_string.add(str(literal_lookup[a])) + else: + muc_string.add(str(a[0])) + return muc_string diff --git a/src/clingexplaid/transformers/transformer_fact.py b/src/clingexplaid/transformers/transformer_fact.py index 19a70d0..0b07be8 100644 --- a/src/clingexplaid/transformers/transformer_fact.py +++ b/src/clingexplaid/transformers/transformer_fact.py @@ -5,14 +5,14 @@ from pathlib import Path from typing import Optional, Sequence, Set, Tuple, Union -import clingo.ast -import clingo.ast as _ast +import clingo +from clingo import ast from .constants import REMOVED_TOKEN from ..utils import match_ast_symbolic_atom_signature -class FactTransformer(_ast.Transformer): +class FactTransformer(ast.Transformer): """ Transformer that removes all facts from a program that match provided signatures """ @@ -26,7 +26,7 @@ def visit_Rule(self, node: clingo.ast.AST) -> clingo.ast.AST: # pylint: disable """ Removes all facts from a program that match the given signatures (if none are given all facts are removed). """ - if node.head.ast_type != _ast.ASTType.Literal: + if node.head.ast_type != ast.ASTType.Literal: return node if node.body: return node @@ -37,9 +37,9 @@ def visit_Rule(self, node: clingo.ast.AST) -> clingo.ast.AST: # pylint: disable if self.signatures and not has_matching_signature: return node - return _ast.Rule( + return ast.Rule( location=node.location, - head=_ast.Function(location=node.location, name=REMOVED_TOKEN, arguments=[], external=0), + head=ast.Function(location=node.location, name=REMOVED_TOKEN, arguments=[], external=0), body=[], ) @@ -62,7 +62,7 @@ def parse_string(self, string: str) -> str: program string. """ out = [] - _ast.parse_string(string, lambda stm: out.append(str(self(stm)))) + ast.parse_string(string, lambda stm: out.append(str(self(stm)))) return self.post_transform("\n".join(out)) def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: @@ -70,7 +70,7 @@ def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: Parses the files and returns a string with the transformed program. """ out = [] - _ast.parse_files( + ast.parse_files( [str(p) for p in paths], lambda stm: out.append(str(self(stm))), ) diff --git a/src/clingexplaid/transformers/transformer_optimization_remover.py b/src/clingexplaid/transformers/transformer_optimization_remover.py index 83afde1..af2b20d 100644 --- a/src/clingexplaid/transformers/transformer_optimization_remover.py +++ b/src/clingexplaid/transformers/transformer_optimization_remover.py @@ -5,26 +5,25 @@ from pathlib import Path from typing import Sequence, Union -import clingo.ast -import clingo.ast as _ast +from clingo import ast from .constants import REMOVED_TOKEN -class OptimizationRemover(_ast.Transformer): +class OptimizationRemover(ast.Transformer): """ Transformer that removes all optimization statements """ # pylint: disable=duplicate-code - def visit_Minimize(self, node: clingo.ast.AST) -> clingo.ast.AST: # pylint: disable=C0103 + def visit_Minimize(self, node: ast.AST) -> ast.AST: # pylint: disable=C0103 """ Removes all facts from a program that match the given signatures (if none are given all facts are removed). """ - return _ast.Rule( + return ast.Rule( location=node.location, - head=_ast.Function(location=node.location, name=REMOVED_TOKEN, arguments=[], external=0), + head=ast.Function(location=node.location, name=REMOVED_TOKEN, arguments=[], external=0), body=[], ) @@ -47,7 +46,7 @@ def parse_string(self, string: str) -> str: program string. """ out = [] - _ast.parse_string(string, lambda stm: out.append(str(self(stm)))) + ast.parse_string(string, lambda stm: out.append(str(self(stm)))) return self.post_transform("\n".join(out)) def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: @@ -55,7 +54,7 @@ def parse_files(self, paths: Sequence[Union[str, Path]]) -> str: Parses the files and returns a string with the transformed program. """ out = [] - _ast.parse_files( + ast.parse_files( [str(p) for p in paths], lambda stm: out.append(str(self(stm))), ) diff --git a/tests/clingexplaid/test_main.py b/tests/clingexplaid/test_main.py index fbffe2b..d4a5784 100644 --- a/tests/clingexplaid/test_main.py +++ b/tests/clingexplaid/test_main.py @@ -4,12 +4,12 @@ import random from pathlib import Path -from typing import List, Optional, Set, Tuple, Union, Dict +from typing import List, Optional, Set, Tuple, Union from unittest import TestCase import clingo -from clingexplaid.utils import AssumptionSet, get_solver_literal_lookup +from clingexplaid.utils import AssumptionSet from clingexplaid.muc import CoreComputer from clingexplaid.transformers import ( AssumptionTransformer, @@ -41,7 +41,7 @@ def get_muc_of_program( program_string: str, assumption_signatures: Set[Tuple[str, int]], control: Optional[clingo.Control] = None, - ) -> AssumptionSet: + ) -> Tuple[AssumptionSet, CoreComputer]: """ Helper function to directly get the MUC of a given program string. """ @@ -61,7 +61,7 @@ def get_muc_of_program( # if the instance was satisfiable and the on_core function wasn't called an empty set is returned, else the muc. result = cc.minimal if cc.minimal is not None else set() - return result + return result, cc def assert_muc( self, @@ -72,17 +72,8 @@ def assert_muc( Asserts if a MUC is one of several valid MUC's. """ valid_mucs = [{clingo.parse_term(s) for s in lit_strings} for lit_strings in valid_mucs_string_lists] - self.assertIn(muc, valid_mucs) - - @staticmethod - def muc_to_string(muc: AssumptionSet, literal_lookup: Dict[int, clingo.Symbol]) -> Set[str]: - muc_string = set() - for a in muc: - if isinstance(a, int): - muc_string.add(str(literal_lookup[a])) - else: - muc_string.add(str(a[0])) - return muc_string + parsed_muc = {clingo.parse_term(s) for s in muc} + self.assertIn(parsed_muc, valid_mucs) # TRANSFORMERS # --- ASSUMPTION TRANSFORMER @@ -180,11 +171,11 @@ def test_core_computer_shrink_single_muc(self) -> None: """ signatures = {("a", 1)} - muc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) + muc, cc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) - literal_lookup = get_solver_literal_lookup(ctl) - - self.assert_muc(self.muc_to_string(muc, literal_lookup), [{"a(1)", "a(4)", "a(5)"}]) + if cc.minimal is None: + self.fail() + self.assert_muc(cc.muc_to_string(muc), [{"a(1)", "a(4)", "a(5)"}]) def test_core_computer_shrink_single_atomic_muc(self) -> None: """ @@ -199,11 +190,11 @@ def test_core_computer_shrink_single_atomic_muc(self) -> None: """ signatures = {("a", 1)} - muc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) - - literal_lookup = get_solver_literal_lookup(ctl) + muc, cc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) - self.assert_muc(self.muc_to_string(muc, literal_lookup), [{"a(3)"}]) + if cc.minimal is None: + self.fail() + self.assert_muc(cc.muc_to_string(muc), [{"a(3)"}]) def test_core_computer_shrink_multiple_atomic_mucs(self) -> None: """ @@ -220,11 +211,11 @@ def test_core_computer_shrink_multiple_atomic_mucs(self) -> None: """ signatures = {("a", 1)} - muc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) + muc, cc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) - literal_lookup = get_solver_literal_lookup(ctl) - - self.assert_muc(self.muc_to_string(muc, literal_lookup), [{"a(3)"}, {"a(5)"}, {"a(9)"}]) + if cc.minimal is None: + self.fail() + self.assert_muc(cc.muc_to_string(muc), [{"a(3)"}, {"a(5)"}, {"a(9)"}]) def test_core_computer_shrink_multiple_mucs(self) -> None: """ @@ -241,12 +232,12 @@ def test_core_computer_shrink_multiple_mucs(self) -> None: """ signatures = {("a", 1)} - muc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) - - literal_lookup = get_solver_literal_lookup(ctl) + muc, cc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) + if cc.minimal is None: + self.fail() self.assert_muc( - self.muc_to_string(muc, literal_lookup), + cc.muc_to_string(muc), [ {"a(3)", "a(9)", "a(5)"}, {"a(5)", "a(1)", "a(2)"}, @@ -269,11 +260,11 @@ def test_core_computer_shrink_large_instance_random(self) -> None: """ signatures = {("a", 1)} - muc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) - - literal_lookup = get_solver_literal_lookup(ctl) + muc, cc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) - self.assert_muc(self.muc_to_string(muc, literal_lookup), [{f"a({i})" for i in random_core}]) + if cc.minimal is None: + self.fail() + self.assert_muc(cc.muc_to_string(muc), [{f"a({i})" for i in random_core}]) def test_core_computer_shrink_satisfiable(self) -> None: """ @@ -287,7 +278,7 @@ def test_core_computer_shrink_satisfiable(self) -> None: """ signatures = {("a", 1)} - muc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) + muc, _ = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) self.assertEqual(muc, set()) From ad9747507d139bf0c9fe4271b9428cd3fc560761 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Wed, 10 Apr 2024 19:16:16 +0200 Subject: [PATCH 72/82] added new test for coverage --- pyproject.toml | 11 +- src/clingexplaid/propagators/__init__.py | 2 + .../propagators/propagator_decision_order.py | 14 +- .../unsat_constraint_computer.py | 2 +- .../res/test_program_constants.lp | 2 + .../res/test_program_decision_order.lp | 4 + .../res/test_program_multi_muc.lp | 6 + .../res/test_program_optimization.lp | 5 + .../res/test_program_unsat_constraints.lp | 4 + .../res/transformed_program_constraints_id.lp | 6 + .../res/transformed_program_facts.lp | 5 + .../res/transformed_program_optimization.lp | 4 + tests/clingexplaid/test_main.py | 227 +++++++++++++++++- tests/clingexplaid/test_utils.py | 29 +++ 14 files changed, 310 insertions(+), 11 deletions(-) create mode 100644 tests/clingexplaid/res/test_program_constants.lp create mode 100644 tests/clingexplaid/res/test_program_decision_order.lp create mode 100644 tests/clingexplaid/res/test_program_multi_muc.lp create mode 100644 tests/clingexplaid/res/test_program_optimization.lp create mode 100644 tests/clingexplaid/res/test_program_unsat_constraints.lp create mode 100644 tests/clingexplaid/res/transformed_program_constraints_id.lp create mode 100644 tests/clingexplaid/res/transformed_program_facts.lp create mode 100644 tests/clingexplaid/res/transformed_program_optimization.lp create mode 100644 tests/clingexplaid/test_utils.py diff --git a/pyproject.toml b/pyproject.toml index cc2125b..893a48b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,16 @@ good-names = ["_"] [tool.coverage.run] source = ["clingexplaid", "tests"] -omit = ["*/clingexplaid/__main__.py"] +omit = [ + "*/clingexplaid/__main__.py", + "*/clingexplaid/cli/*", + "*/clingexplaid/propagators/__init__.py", + "*/clingexplaid/transformers/__init__.py", + "*/clingexplaid/muc/__init__.py", + "*/clingexplaid/unsat_constraints/__init__.py", + "*/tests/*", + "*/constants.py" +] [tool.coverage.report] exclude_lines = ["assert", "nocoverage"] diff --git a/src/clingexplaid/propagators/__init__.py b/src/clingexplaid/propagators/__init__.py index 5306e6a..a7df86b 100644 --- a/src/clingexplaid/propagators/__init__.py +++ b/src/clingexplaid/propagators/__init__.py @@ -2,6 +2,8 @@ Propagators for Explanation """ +# pragma: no cover + from typing import List from .propagator_decision_order import DecisionOrderPropagator diff --git a/src/clingexplaid/propagators/propagator_decision_order.py b/src/clingexplaid/propagators/propagator_decision_order.py index 23a9b51..106c0fc 100644 --- a/src/clingexplaid/propagators/propagator_decision_order.py +++ b/src/clingexplaid/propagators/propagator_decision_order.py @@ -38,7 +38,7 @@ def init(self, init: clingo.PropagateInit) -> None: continue symbolic_atom = init.symbolic_atoms[atom.symbol] if symbolic_atom is None: - continue + continue # nocoverage query_program_literal = symbolic_atom.literal query_solver_literal = init.solver_literal(query_program_literal) init.add_watch(query_solver_literal) @@ -51,13 +51,13 @@ def _is_printed(self, symbol: Union[clingo.Symbol, str]) -> bool: printed = True # skip UNKNOWN print if signatures is set if len(self.signatures) > 0 and symbol == UNKNOWN_SYMBOL_TOKEN: - printed = False + printed = False # nocoverage # skip if symbol signature is not in self.signatures elif len(self.signatures) > 0 and symbol != UNKNOWN_SYMBOL_TOKEN: # `symbol` can only be a `str` if it is the UNKNOWN_SYMBOL_TOKEN - if isinstance(symbol, str): + if isinstance(symbol, str): # nocoverage printed = False - elif not any(symbol.match(s, a) for s, a in self.signatures): + elif not any(symbol.match(s, a) for s, a in self.signatures): # nocoverage printed = False return printed @@ -99,12 +99,12 @@ def propagate(self, control: clingo.PropagateControl, changes: Sequence[int]) -> for e in entailment_list: # skip decision in entailments if e == d: - continue + continue # nocoverage entailment_symbol = self.get_symbol(e) entailment_printed = self._is_printed(entailment_symbol) # skip if entailment symbol doesn't mach signatures (if provided) if not entailment_printed: - continue + continue # nocoverage entailment_negative = e < 0 if decision_printed: @@ -125,7 +125,7 @@ def undo(self, thread_id: int, assignment: clingo.Assignment, changes: Sequence[ # pylint: disable=unused-argument if len(self.last_decisions) < 1: - return + return # nocoverage decision = self.last_decisions[-1] decision_symbol = self.get_symbol(decision) diff --git a/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py b/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py index d51487b..33207ba 100644 --- a/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py +++ b/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py @@ -44,7 +44,7 @@ def parse_files(self, files: List[str]) -> None: """ ct = ConstraintTransformer(UNSAT_CONSTRAINT_SIGNATURE, include_id=True) if not files: - program_transformed = ct.parse_files("-") + program_transformed = ct.parse_files("-") # nocoverage else: program_transformed = ct.parse_files(files) diff --git a/tests/clingexplaid/res/test_program_constants.lp b/tests/clingexplaid/res/test_program_constants.lp new file mode 100644 index 0000000..17d5808 --- /dev/null +++ b/tests/clingexplaid/res/test_program_constants.lp @@ -0,0 +1,2 @@ +#const number=42. +#const message=helloworld. \ No newline at end of file diff --git a/tests/clingexplaid/res/test_program_decision_order.lp b/tests/clingexplaid/res/test_program_decision_order.lp new file mode 100644 index 0000000..63c3b84 --- /dev/null +++ b/tests/clingexplaid/res/test_program_decision_order.lp @@ -0,0 +1,4 @@ +{a}. +{b} :- a. +:- not a. +x. \ No newline at end of file diff --git a/tests/clingexplaid/res/test_program_multi_muc.lp b/tests/clingexplaid/res/test_program_multi_muc.lp new file mode 100644 index 0000000..d5bd004 --- /dev/null +++ b/tests/clingexplaid/res/test_program_multi_muc.lp @@ -0,0 +1,6 @@ +a(1..10). + +:- a(1), a(2). +:- a(5), a(8), a(3). +:- a(1), a(9). +:- a(1), a(9), a(4). \ No newline at end of file diff --git a/tests/clingexplaid/res/test_program_optimization.lp b/tests/clingexplaid/res/test_program_optimization.lp new file mode 100644 index 0000000..aad8c05 --- /dev/null +++ b/tests/clingexplaid/res/test_program_optimization.lp @@ -0,0 +1,5 @@ +1{a(1..10)}. +a_count(C) :- C=#count{X: a(X)}. +#minimize{C@2: a_count(C)}. +a_sum(S) :- S=#sum{X: a(X)}. +#maximize{S@1: a_sum(S)}. \ No newline at end of file diff --git a/tests/clingexplaid/res/test_program_unsat_constraints.lp b/tests/clingexplaid/res/test_program_unsat_constraints.lp new file mode 100644 index 0000000..8ded791 --- /dev/null +++ b/tests/clingexplaid/res/test_program_unsat_constraints.lp @@ -0,0 +1,4 @@ +{a;b;c;d}. + +:- a. +:- not a. \ No newline at end of file diff --git a/tests/clingexplaid/res/transformed_program_constraints_id.lp b/tests/clingexplaid/res/transformed_program_constraints_id.lp new file mode 100644 index 0000000..d7d339a --- /dev/null +++ b/tests/clingexplaid/res/transformed_program_constraints_id.lp @@ -0,0 +1,6 @@ +#program base. +{ a; b; c; d }. +x(1). +#true :- x(2). +unsat(1) :- a; b; c. +unsat(2) :- a; d. \ No newline at end of file diff --git a/tests/clingexplaid/res/transformed_program_facts.lp b/tests/clingexplaid/res/transformed_program_facts.lp new file mode 100644 index 0000000..356e5e0 --- /dev/null +++ b/tests/clingexplaid/res/transformed_program_facts.lp @@ -0,0 +1,5 @@ +#program base. +b(2) :- x. +c(3); c(4) :- x. +f(17); f(18) :- e(16). +x(19). \ No newline at end of file diff --git a/tests/clingexplaid/res/transformed_program_optimization.lp b/tests/clingexplaid/res/transformed_program_optimization.lp new file mode 100644 index 0000000..6e270d9 --- /dev/null +++ b/tests/clingexplaid/res/transformed_program_optimization.lp @@ -0,0 +1,4 @@ +#program base. +1 <= { a((1..10)) }. +a_count(C) :- C = #count { X: a(X) }. +a_sum(S) :- S = #sum { X: a(X) }. \ No newline at end of file diff --git a/tests/clingexplaid/test_main.py b/tests/clingexplaid/test_main.py index d4a5784..84f318e 100644 --- a/tests/clingexplaid/test_main.py +++ b/tests/clingexplaid/test_main.py @@ -4,20 +4,24 @@ import random from pathlib import Path -from typing import List, Optional, Set, Tuple, Union +from typing import List, Optional, Set, Tuple, Union, Dict from unittest import TestCase import clingo from clingexplaid.utils import AssumptionSet from clingexplaid.muc import CoreComputer +from clingexplaid.unsat_constraints import UnsatConstraintComputer from clingexplaid.transformers import ( AssumptionTransformer, ConstraintTransformer, RuleIDTransformer, RuleSplitter, + OptimizationRemover, + FactTransformer, ) -from clingexplaid.transformers.exceptions import UntransformedException +from clingexplaid.propagators import DecisionOrderPropagator +from clingexplaid.transformers.exceptions import UntransformedException, NotGroundedException TEST_DIR = parent = Path(__file__).resolve().parent @@ -28,6 +32,8 @@ class TestMain(TestCase): Test cases for clingexplaid. """ + # pylint: disable=too-many-public-methods + @staticmethod def read_file(path: Union[str, Path], encoding: str = "utf-8") -> str: """ @@ -106,6 +112,31 @@ def test_assumption_transformer_get_assumptions_before_transformation(self) -> N control = clingo.Control() self.assertRaises(UntransformedException, lambda: at.get_assumptions(control)) + def test_assumption_transformer_get_assumptions_before_grounding(self) -> None: + """ + Test the AssumptionTransformer's behavior when get_assumptions is called before transformation. + """ + program_path = TEST_DIR.joinpath("res/test_program.lp") + at = AssumptionTransformer() + control = clingo.Control() + at.parse_files([program_path]) + self.assertRaises(NotGroundedException, lambda: at.get_assumptions(control)) + + def test_assumption_transformer_visit_definition(self) -> None: + """ + Test the AssumptionTransformer's detection of constant definitions. + """ + program_path = TEST_DIR.joinpath("res/test_program_constants.lp") + at = AssumptionTransformer() + control = clingo.Control() + result = at.parse_files([program_path]) + control.add("base", [], result) + control.ground([("base", [])]) + self.assertEqual( + at.program_constants, + {k: clingo.parse_term(v) for k, v in {"number": "42", "message": "helloworld"}.items()}, + ) + # --- RULE ID TRANSFORMER def test_rule_id_transformer(self) -> None: @@ -143,6 +174,17 @@ def test_constraint_transformer(self) -> None: result = ct.parse_files([program_path]) self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) + def test_constraint_transformer_include_id(self) -> None: + """ + Test the ConstraintTransformer's `parse_file` method. + """ + program_path = TEST_DIR.joinpath("res/test_program_constraints.lp") + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_constraints_id.lp") + ct = ConstraintTransformer(constraint_head_symbol="unsat", include_id=True) + with open(program_path, "r", encoding="utf-8") as f: + result = ct.parse_string(f.read()) + self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) + # --- RULE SPLITTER def test_rule_splitter(self) -> None: @@ -156,6 +198,69 @@ def test_rule_splitter(self) -> None: result = rs.parse_file(program_path) self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) + # --- OPTIMIZATION REMOVER + + def test_optimization_remover(self) -> None: + """ + Test the OptimizationRemover's `parse_file` and `parse_string_method` method. + """ + + program_path = TEST_DIR.joinpath("res/test_program_optimization.lp") + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_optimization.lp") + optrm = OptimizationRemover() + result_files = optrm.parse_files([program_path]) + with open(program_path, "r", encoding="utf-8") as f: + result_string = optrm.parse_string(f.read()) + self.assertEqual(result_files.strip(), self.read_file(program_path_transformed).strip()) + self.assertEqual(result_files.strip(), result_string.strip()) + + # --- FACT TRANSFORMER + + def test_fact_transformer(self) -> None: + """ + Test the FactTransformer's `parse_files` and `parse_string_method` method. + """ + + program_path = TEST_DIR.joinpath("res/test_program.lp") + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_facts.lp") + ft = FactTransformer(signatures={("a", 1), ("d", 1), ("e", 1)}) + result_files = ft.parse_files([program_path]) + with open(program_path, "r", encoding="utf-8") as f: + result_string = ft.parse_string(f.read()) + self.assertEqual(result_files.strip(), self.read_file(program_path_transformed).strip()) + self.assertEqual(result_files.strip(), result_string.strip()) + + # PROPAGATORS + # --- DECISION ORDER PROPAGATOR + + def test_decision_order_propagator(self) -> None: + """ + Testing the functionality of the DecisionOrderPropagator without signatures + """ + program_path = TEST_DIR.joinpath("res/test_program_decision_order.lp") + control = clingo.Control() + dop = DecisionOrderPropagator() + control.register_propagator(dop) # type: ignore + control.load(str(program_path)) + control.ground() + control.solve() + + # No asserts since the propagator currently doesn't support any outputs but only prints. + + def test_decision_order_propagator_with_signatures(self) -> None: + """ + Testing the functionality of the DecisionOrderPropagator with signatures + """ + program_path = TEST_DIR.joinpath("res/test_program_decision_order.lp") + control = clingo.Control() + dop = DecisionOrderPropagator(signatures={("a", 0), ("b", 0), ("x", 1)}) + control.register_propagator(dop) # type: ignore + control.load(str(program_path)) + control.ground() + control.solve() + + # No asserts since the propagator currently doesn't support any outputs but only prints. + # MUC def test_core_computer_shrink_single_muc(self) -> None: @@ -282,6 +387,54 @@ def test_core_computer_shrink_satisfiable(self) -> None: self.assertEqual(muc, set()) + def test_core_computer_get_multiple_minimal(self) -> None: + """ + Test the CoreComputer's `get_multiple_minimal` function to get multiple MUCs. + """ + + ctl = clingo.Control() + + program_path = TEST_DIR.joinpath("res/test_program_multi_muc.lp") + at = AssumptionTransformer(signatures={("a", 1)}) + parsed = at.parse_files([program_path]) + ctl.add("base", [], parsed) + ctl.ground([("base", [])]) + cc = CoreComputer(ctl, at.get_assumptions(ctl)) + + muc_generator = cc.get_multiple_minimal() + + muc_string_sets = [cc.muc_to_string(muc) for muc in list(muc_generator)] + for muc_string_set in muc_string_sets: + self.assertIn( + muc_string_set, + [{"a(1)", "a(2)"}, {"a(1)", "a(9)"}, {"a(3)", "a(5)", "a(8)"}], + ) + + def test_core_computer_get_multiple_minimal_max_mucs_2(self) -> None: + """ + Test the CoreComputer's `get_multiple_minimal` function to get multiple MUCs. + """ + + ctl = clingo.Control() + + program_path = TEST_DIR.joinpath("res/test_program_multi_muc.lp") + at = AssumptionTransformer(signatures={("a", 1)}) + parsed = at.parse_files([program_path]) + ctl.add("base", [], parsed) + ctl.ground([("base", [])]) + cc = CoreComputer(ctl, at.get_assumptions(ctl)) + + muc_generator = cc.get_multiple_minimal(max_mucs=2) + + muc_string_sets = [cc.muc_to_string(muc) for muc in list(muc_generator)] + for muc_string_set in muc_string_sets: + self.assertIn( + muc_string_set, + [{"a(1)", "a(2)"}, {"a(1)", "a(9)"}, {"a(3)", "a(5)", "a(8)"}], + ) + + self.assertEqual(len(muc_string_sets), 2) + # --- INTERNAL def test_core_computer_internal_solve_no_assumptions(self) -> None: @@ -316,3 +469,73 @@ def test_core_computer_internal_compute_single_minimal_no_assumptions(self) -> N control = clingo.Control() cc = CoreComputer(control, set()) self.assertRaises(ValueError, cc._compute_single_minimal) # pylint: disable=W0212 + + def test_core_computer_muc_to_string(self) -> None: + """ + Test the CoreComputer's `_compute_single_minimal` function with no assumptions. + """ + + control = clingo.Control() + cc = CoreComputer(control, set()) + self.assertEqual( + cc.muc_to_string({(clingo.parse_term(string), True) for string in ["this", "is", "a", "test"]}), + {"this", "is", "a", "test"}, + ) # pylint: disable=W0212 + + # UNSAT CONSTRAINT COMPUTER + + def unsat_constraint_computer_helper( + self, + constraint_strings: Dict[int, str], + constraint_lines: Dict[int, int], + constraint_files: Dict[int, str], + assumption_string: Optional[str] = None, + ) -> None: + """ + Helper function for testing the UnsatConstraintComputer + """ + for method in ["from_files", "from_string"]: + program_path = TEST_DIR.joinpath("res/test_program_unsat_constraints.lp") + ucc = UnsatConstraintComputer() + if method == "from_files": + ucc.parse_files([str(program_path)]) + elif method == "from_string": + with open(program_path, "r", encoding="utf-8") as f: + ucc.parse_string(f.read()) + unsat_constraints = ucc.get_unsat_constraints(assumption_string=assumption_string) + self.assertEqual(set(unsat_constraints.values()), set(constraint_strings.values())) + + for c_id in unsat_constraints: + loc = ucc.get_constraint_location(c_id) + if method == "from_files": + # only check the source file if .from_files is used to initialize + self.assertEqual(loc.begin.filename, constraint_files[c_id]) # type: ignore + self.assertEqual(loc.begin.line, constraint_lines[c_id]) # type: ignore + + def test_unsat_constraint_computer(self) -> None: + """ + Testing the UnsatConstraintComputer without assumptions. + """ + self.unsat_constraint_computer_helper( + constraint_strings={2: ":- not a."}, + constraint_lines={2: 4}, + constraint_files={2: str(TEST_DIR.joinpath("res/test_program_unsat_constraints.lp"))}, + ) + + def test_unsat_constraint_computer_with_assumptions(self) -> None: + """ + Testing the UnsatConstraintComputer with assumptions. + """ + self.unsat_constraint_computer_helper( + constraint_strings={1: ":- a."}, + constraint_lines={1: 3}, + constraint_files={1: str(TEST_DIR.joinpath("res/test_program_unsat_constraints.lp"))}, + assumption_string="a", + ) + + def test_unsat_constraint_computer_not_initialized(self) -> None: + """ + Testing the UnsatConstraintComputer without initializing it. + """ + ucc = UnsatConstraintComputer() + self.assertRaises(ValueError, ucc.get_unsat_constraints) diff --git a/tests/clingexplaid/test_utils.py b/tests/clingexplaid/test_utils.py new file mode 100644 index 0000000..e1bbcbc --- /dev/null +++ b/tests/clingexplaid/test_utils.py @@ -0,0 +1,29 @@ +""" +Tests for the utils package +""" + +from unittest import TestCase + +from clingexplaid.utils import get_signatures_from_model_string, get_constants_from_arguments + + +class TestMain(TestCase): + """ + Test cases for clingexplaid. + """ + + def test_get_signatures_from_model_string(self) -> None: + """ + Test getting signatures from a model string. + """ + model_string = "a(1,2) a(1,5) a(3,5) a(1,2,3) a(1,3,5), foo(bar), zero" + signatures = get_signatures_from_model_string(model_string) + self.assertEqual(signatures, {("a", 2), ("a", 3), ("foo", 1), ("zero", 0)}) + + def test_get_constants_from_arguments(self) -> None: + """ + Test getting constants from argument vector. + """ + self.assertEqual(get_constants_from_arguments(["-c", "a=42"]), {"a": "42"}) + self.assertEqual(get_constants_from_arguments(["test/dir/file.lp", "--const", "blob=value"]), {"blob": "value"}) + self.assertEqual(get_constants_from_arguments(["--const", "-a", "test/42"]), {}) From 90e9ca4dd1ed7eab5d767353d363a1b4df6fef7e Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Wed, 10 Apr 2024 19:21:15 +0200 Subject: [PATCH 73/82] increased min python version to 3.11 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 893a48b..c72ca97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ { name = "Hannes Weichelt", email = "hweichelt@uni-potsdam.de" } ] description = "A template project." -requires-python = ">=3.9" +requires-python = ">=3.11" license = {file = "LICENSE"} dynamic = [ "version" ] readme = "README.md" From de7344280f7d5ebf8292b2f480433a29418da487 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Wed, 10 Apr 2024 19:22:50 +0200 Subject: [PATCH 74/82] removed 3.9 from noxfile --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 0767129..e7dc802 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,7 +7,7 @@ EDITABLE_TESTS = True PYTHON_VERSIONS = None if "GITHUB_ACTIONS" in os.environ: - PYTHON_VERSIONS = ["3.9", "3.11"] + PYTHON_VERSIONS = ["3.11"] EDITABLE_TESTS = False From dd14f905bbbbcfd127102a9e230666d76d93c660 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Thu, 11 Apr 2024 09:26:40 +0200 Subject: [PATCH 75/82] separated README and TODO --- README.md | 60 ------------------------------------------------------- TODO.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 60 deletions(-) create mode 100644 TODO.md diff --git a/README.md b/README.md index ea2d26e..9581069 100644 --- a/README.md +++ b/README.md @@ -82,66 +82,6 @@ pre-commit install This blackens the source code whenever `git commit` is used. -## ToDos - -### Important Features - -`2023` - -+ [x] New CLI structure - + different modes: - + MUC - + UNSAT-CONSTRAINTS - + can be enabled through flags -+ [x] Iterative Deltion for Multiple MUCs - + variation of the QuickXplain algorithm : `SKIPPED` -+ [x] Finish unsat-constraints implementation for the API - -`2024 - JAN` - -+ [x] New option to enable verbose derivation output - + `--show-decisions` with more fine grained `--decision-signature` option -+ [x] Make `--show-decisions` its own mode -+ [x] Give a warning in Transformer if control is not grounded yet -+ [ ] Documentation - + [ ] Proper README - + [ ] Docstrings for all API functions - + [ ] CLI documentation with examples - + [x] Examples folder - + [x] Sudoku - + [x] Graph Coloring - + [x] N-Queens -+ [x] Error when calling `--muc` constants aren't properly kept: - + The problem was in `AssumptionTransformer` where get_assumptions didn't have proper access to constants defined over - the CL and the program constants -+ [x] `AssumptionTransformer` doesn't work properly on included files - + It actually did work fine - -`2024 - FEB` - -+ [x] In `--show-decisions` hide INTERNAL when `--decision-signature` is active -+ [x] cleanup `DecisionOrderPropagator` print functions -+ [x] Features for `--unsat-constraints` - + [x] File + Line (Clickable link) -+ [x] Confusing Optimization prints during `--muc` when finding mucs in optimized Programs -+ [x] File-Link test with space in filename - + with `urllib.parsequote` -+ [x] Write up why negated assumptions in MUC's are a problem - + One which is currently not addressed by clingo-explaid -+ [x] Remove minimization also from `--unsat-constaints` mode -+ [x] Change file identification to use `clingo.ast.Location` instead of the subtring search and own built file tree -+ [x] Add spaces around Link to make it clickable on MAC - -`2024 - MAR` - -+ [x] Add way for `-a` to allow for signatures without variables (`test/0`) - -### Extra Features -+ [ ] `--unsat-constraints`: - + [ ] Access comments in the same line as constraint - + [ ] Currently, for multiline constraints a line number cannot be found -+ [ ] Timeout - ## Problems and Limitations ### Meta-encoding based approach (ASP-Approach) diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..4c877ed --- /dev/null +++ b/TODO.md @@ -0,0 +1,59 @@ +## ToDos + +### Important Features + +`2023` + ++ [x] New CLI structure + + different modes: + + MUC + + UNSAT-CONSTRAINTS + + can be enabled through flags ++ [x] Iterative Deltion for Multiple MUCs + + variation of the QuickXplain algorithm : `SKIPPED` ++ [x] Finish unsat-constraints implementation for the API + +`2024 - JAN` + ++ [x] New option to enable verbose derivation output + + `--show-decisions` with more fine grained `--decision-signature` option ++ [x] Make `--show-decisions` its own mode ++ [x] Give a warning in Transformer if control is not grounded yet ++ [ ] Documentation + + [ ] Proper README + + [ ] Docstrings for all API functions + + [ ] CLI documentation with examples + + [x] Examples folder + + [x] Sudoku + + [x] Graph Coloring + + [x] N-Queens ++ [x] Error when calling `--muc` constants aren't properly kept: + + The problem was in `AssumptionTransformer` where get_assumptions didn't have proper access to constants defined over + the CL and the program constants ++ [x] `AssumptionTransformer` doesn't work properly on included files + + It actually did work fine + +`2024 - FEB` + ++ [x] In `--show-decisions` hide INTERNAL when `--decision-signature` is active ++ [x] cleanup `DecisionOrderPropagator` print functions ++ [x] Features for `--unsat-constraints` + + [x] File + Line (Clickable link) ++ [x] Confusing Optimization prints during `--muc` when finding mucs in optimized Programs ++ [x] File-Link test with space in filename + + with `urllib.parsequote` ++ [x] Write up why negated assumptions in MUC's are a problem + + One which is currently not addressed by clingo-explaid ++ [x] Remove minimization also from `--unsat-constaints` mode ++ [x] Change file identification to use `clingo.ast.Location` instead of the subtring search and own built file tree ++ [x] Add spaces around Link to make it clickable on MAC + +`2024 - MAR` + ++ [x] Add way for `-a` to allow for signatures without variables (`test/0`) + +### Extra Features ++ [ ] `--unsat-constraints`: + + [ ] Access comments in the same line as constraint + + [ ] Currently, for multiline constraints a line number cannot be found ++ [ ] Timeout From 547cdbf5dccd140d6cae57909ea8878b8dca7d0c Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Thu, 11 Apr 2024 09:47:23 +0200 Subject: [PATCH 76/82] updated README --- README.md | 44 ++++++++++++++++++++++++++++++++------------ TODO.md | 8 ++++---- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9581069..873bb1b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,5 @@ # clingexplaid -## Installation - -To install the project, run - -```bash -pip install . -``` - ## Usage Run the following for basic usage information: @@ -16,17 +8,45 @@ Run the following for basic usage information: clingexplaid -h ``` -Compute Minimal Unsatisfiable Core from unsatisfiable program: +The clingexplaid CLI (based on the `clingo.Application` class) can be called using this generic command. ```bash -clingexplaid --assumption-signature signature/arity +clingexplaid ``` -+ `--assumption-signature` is optional to allow for only specific facts to be transformed to assumptions - + if no such option is given all facts are transformed to assumptions regardless of their signature ++ ``: has to be replaced by a list of all files or a single filename ++ ``: defines how many models are computed (Default=`1`, All=`0`) ++ ``: specifies which Clingexplaid method is used (Required) + + Options: + + `--muc`: + + Computes the Minimal Unsatisfiable Cores (MUCs) of the provided unsatisfiable program + + `--unsat-constraints`: + + Computes the Unsatisfiable Constraints of the unsatisfiable program provided. + + `--show-decisions`: + + Visualizes the decision process of clasp ++ ``: Additional options for the different methods + + For `--muc`: + + `-a`, `--assumption-signature`: limits which facts of the current program are converted to choices/assumptions for + finding the MUCs (Default: all facts are converted) + + For `--show-decisions`: + + `--decision-signature`: limits which decisions are shown in the visualization (Default: all atom's decisions are + shown) + +### Examples + ++ A selection of examples can be found [here](examples) ## Development +### Installation + +To install the project, run + +```bash +pip install . +``` + + To improve code quality, we run linters, type checkers, and unit tests. The tools can be run using [nox]. We recommend installing nox using [pipx] to have it available globally: diff --git a/TODO.md b/TODO.md index 4c877ed..9c8f594 100644 --- a/TODO.md +++ b/TODO.md @@ -19,10 +19,10 @@ + `--show-decisions` with more fine grained `--decision-signature` option + [x] Make `--show-decisions` its own mode + [x] Give a warning in Transformer if control is not grounded yet -+ [ ] Documentation - + [ ] Proper README - + [ ] Docstrings for all API functions - + [ ] CLI documentation with examples ++ [x] Documentation + + [x] Proper README + + [x] Docstrings for all API functions + + [x] CLI documentation with examples + [x] Examples folder + [x] Sudoku + [x] Graph Coloring From 10673d3f4afad06024fa9eedda5c95912e042b25 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Wed, 17 Apr 2024 17:31:11 +0200 Subject: [PATCH 77/82] added .venv to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 90edded..0fd4d08 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ pip-delete-this-directory.txt # Unit test / coverage reports .pytest_cache/ +#venv +.venv + # pyenv .python-version From fc2887b4674e74818f8a2ee0ee51a37dba4060e4 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Wed, 17 Apr 2024 17:31:31 +0200 Subject: [PATCH 78/82] fixed regex parantheses escaping --- src/clingexplaid/unsat_constraints/unsat_constraint_computer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py b/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py index 33207ba..b61fcac 100644 --- a/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py +++ b/src/clingexplaid/unsat_constraints/unsat_constraint_computer.py @@ -92,7 +92,7 @@ def get_unsat_constraints(self, assumption_string: Optional[str] = None) -> Dict constraint_lookup = {} for line in program_string.split("\n"): id_re = re.compile( - f"{UNSAT_CONSTRAINT_SIGNATURE}\(([1-9][0-9]*)\)" # pylint: disable=anomalous-backslash-in-string) + f"{UNSAT_CONSTRAINT_SIGNATURE}[(]([1-9][0-9]*)[)]" ) match_result = id_re.match(line) if match_result is None: From 54f4de9a7e134e707b48f4651a678fefdc542bdb Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Wed, 17 Apr 2024 18:29:42 +0200 Subject: [PATCH 79/82] added assumptions argument to clingo.solve function in test --- tests/clingexplaid/test_main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/clingexplaid/test_main.py b/tests/clingexplaid/test_main.py index 84f318e..b328604 100644 --- a/tests/clingexplaid/test_main.py +++ b/tests/clingexplaid/test_main.py @@ -243,7 +243,7 @@ def test_decision_order_propagator(self) -> None: control.register_propagator(dop) # type: ignore control.load(str(program_path)) control.ground() - control.solve() + control.solve(assumptions=[]) # No asserts since the propagator currently doesn't support any outputs but only prints. @@ -257,7 +257,7 @@ def test_decision_order_propagator_with_signatures(self) -> None: control.register_propagator(dop) # type: ignore control.load(str(program_path)) control.ground() - control.solve() + control.solve(assumptions=[]) # No asserts since the propagator currently doesn't support any outputs but only prints. From bae46923e9e09ab59bbc605a055093765e320bca Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Thu, 18 Apr 2024 15:27:10 +0200 Subject: [PATCH 80/82] moved custom types to seperate module --- src/clingexplaid/muc/core_computer.py | 5 +++-- src/clingexplaid/utils/__init__.py | 9 +-------- src/clingexplaid/utils/types.py | 13 +++++++++++++ 3 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 src/clingexplaid/utils/types.py diff --git a/src/clingexplaid/muc/core_computer.py b/src/clingexplaid/muc/core_computer.py index 2e16890..bd6b7cb 100644 --- a/src/clingexplaid/muc/core_computer.py +++ b/src/clingexplaid/muc/core_computer.py @@ -2,12 +2,13 @@ MUC Module: Core Computer to get Minimal Unsatisfiable Cores """ -from typing import Optional, Set, Tuple, Generator, List, Dict from itertools import chain, combinations +from typing import Optional, Set, Tuple, Generator, List, Dict import clingo -from ..utils import Assumption, AssumptionSet, SymbolSet, get_solver_literal_lookup +from ..utils import get_solver_literal_lookup +from ..utils.types import Assumption, AssumptionSet, SymbolSet class CoreComputer: diff --git a/src/clingexplaid/utils/__init__.py b/src/clingexplaid/utils/__init__.py index d4532f6..c153b21 100644 --- a/src/clingexplaid/utils/__init__.py +++ b/src/clingexplaid/utils/__init__.py @@ -3,19 +3,12 @@ """ import re -from typing import Dict, Iterable, Set, Tuple, Union, List +from typing import Dict, Set, Tuple, List import clingo from clingo.ast import ASTType -SymbolSet = Set[clingo.Symbol] -Literal = Tuple[clingo.Symbol, bool] -LiteralSet = Set[Literal] -Assumption = Union[Literal, int] -AssumptionSet = Iterable[Assumption] - - def match_ast_symbolic_atom_signature( ast_symbol: ASTType.SymbolicAtom, signature: Tuple[str, int] ) -> bool: diff --git a/src/clingexplaid/utils/types.py b/src/clingexplaid/utils/types.py new file mode 100644 index 0000000..7ef7eb2 --- /dev/null +++ b/src/clingexplaid/utils/types.py @@ -0,0 +1,13 @@ +""" +Custom types for clingexplaid +""" + +from typing import Set, Tuple, Union, Iterable + +import clingo + +SymbolSet = Set[clingo.Symbol] +Literal = Tuple[clingo.Symbol, bool] +LiteralSet = Set[Literal] +Assumption = Union[Literal, int] +AssumptionSet = Iterable[Assumption] From 1114076b3d6b483e792bc067035497076b38db9b Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Thu, 18 Apr 2024 15:27:30 +0200 Subject: [PATCH 81/82] separated tests --- tests/clingexplaid/test_main.py | 534 +------------------ tests/clingexplaid/test_muc.py | 277 ++++++++++ tests/clingexplaid/test_propagators.py | 46 ++ tests/clingexplaid/test_transformers.py | 172 ++++++ tests/clingexplaid/test_unsat_constraints.py | 74 +++ tests/clingexplaid/test_utils.py | 2 +- 6 files changed, 578 insertions(+), 527 deletions(-) create mode 100644 tests/clingexplaid/test_muc.py create mode 100644 tests/clingexplaid/test_propagators.py create mode 100644 tests/clingexplaid/test_transformers.py create mode 100644 tests/clingexplaid/test_unsat_constraints.py diff --git a/tests/clingexplaid/test_main.py b/tests/clingexplaid/test_main.py index b328604..35336a8 100644 --- a/tests/clingexplaid/test_main.py +++ b/tests/clingexplaid/test_main.py @@ -2,540 +2,22 @@ Test cases for main application functionality. """ -import random from pathlib import Path -from typing import List, Optional, Set, Tuple, Union, Dict +from typing import Union from unittest import TestCase -import clingo - -from clingexplaid.utils import AssumptionSet -from clingexplaid.muc import CoreComputer -from clingexplaid.unsat_constraints import UnsatConstraintComputer -from clingexplaid.transformers import ( - AssumptionTransformer, - ConstraintTransformer, - RuleIDTransformer, - RuleSplitter, - OptimizationRemover, - FactTransformer, -) -from clingexplaid.propagators import DecisionOrderPropagator -from clingexplaid.transformers.exceptions import UntransformedException, NotGroundedException +TEST_DIR = parent = Path(__file__).resolve().parent -TEST_DIR = parent = Path(__file__).resolve().parent +def read_file(path: Union[str, Path], encoding: str = "utf-8") -> str: + """ + Read file at path and return contents as string. + """ + with open(path, "r", encoding=encoding) as f: + return f.read() class TestMain(TestCase): """ Test cases for clingexplaid. """ - - # pylint: disable=too-many-public-methods - - @staticmethod - def read_file(path: Union[str, Path], encoding: str = "utf-8") -> str: - """ - Read file at path and return contents as string. - """ - with open(path, "r", encoding=encoding) as f: - return f.read() - - @staticmethod - def get_muc_of_program( - program_string: str, - assumption_signatures: Set[Tuple[str, int]], - control: Optional[clingo.Control] = None, - ) -> Tuple[AssumptionSet, CoreComputer]: - """ - Helper function to directly get the MUC of a given program string. - """ - ctl = control if control is not None else clingo.Control() - - at = AssumptionTransformer(signatures=assumption_signatures) - transformed_program = at.parse_string(program_string) - - ctl.add("base", [], transformed_program) - ctl.ground([("base", [])]) - - assumptions = at.get_assumptions(ctl) - - cc = CoreComputer(ctl, assumptions) - ctl.solve(assumptions=list(assumptions), on_core=cc.shrink) - - # if the instance was satisfiable and the on_core function wasn't called an empty set is returned, else the muc. - result = cc.minimal if cc.minimal is not None else set() - - return result, cc - - def assert_muc( - self, - muc: Set[str], - valid_mucs_string_lists: List[Set[str]], - ) -> None: - """ - Asserts if a MUC is one of several valid MUC's. - """ - valid_mucs = [{clingo.parse_term(s) for s in lit_strings} for lit_strings in valid_mucs_string_lists] - parsed_muc = {clingo.parse_term(s) for s in muc} - self.assertIn(parsed_muc, valid_mucs) - - # TRANSFORMERS - # --- ASSUMPTION TRANSFORMER - - def test_assumption_transformer_parse_file(self) -> None: - """ - Test the AssumptionTransformer's `parse_file` method. - """ - program_path = TEST_DIR.joinpath("res/test_program.lp") - program_path_transformed = TEST_DIR.joinpath("res/transformed_program_assumptions_certain_signatures.lp") - at = AssumptionTransformer(signatures={(c, 1) for c in "abcdef"}) - result = at.parse_files([program_path]) - self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) - - def test_assumption_transformer_parse_file_no_signatures(self) -> None: - """ - Test the AssumptionTransformer's `parse_file` method with no signatures provided. - """ - program_path = TEST_DIR.joinpath("res/test_program.lp") - program_path_transformed = TEST_DIR.joinpath("res/transformed_program_assumptions_all.lp") - at = AssumptionTransformer() - result = at.parse_files([program_path]) - self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) - - def test_assumption_transformer_get_assumptions_before_transformation(self) -> None: - """ - Test the AssumptionTransformer's behavior when get_assumptions is called before transformation. - """ - at = AssumptionTransformer() - control = clingo.Control() - self.assertRaises(UntransformedException, lambda: at.get_assumptions(control)) - - def test_assumption_transformer_get_assumptions_before_grounding(self) -> None: - """ - Test the AssumptionTransformer's behavior when get_assumptions is called before transformation. - """ - program_path = TEST_DIR.joinpath("res/test_program.lp") - at = AssumptionTransformer() - control = clingo.Control() - at.parse_files([program_path]) - self.assertRaises(NotGroundedException, lambda: at.get_assumptions(control)) - - def test_assumption_transformer_visit_definition(self) -> None: - """ - Test the AssumptionTransformer's detection of constant definitions. - """ - program_path = TEST_DIR.joinpath("res/test_program_constants.lp") - at = AssumptionTransformer() - control = clingo.Control() - result = at.parse_files([program_path]) - control.add("base", [], result) - control.ground([("base", [])]) - self.assertEqual( - at.program_constants, - {k: clingo.parse_term(v) for k, v in {"number": "42", "message": "helloworld"}.items()}, - ) - - # --- RULE ID TRANSFORMER - - def test_rule_id_transformer(self) -> None: - """ - Test the RuleIDTransformer's `parse_file` and `get_assumptions` methods. - """ - program_path = TEST_DIR.joinpath("res/test_program.lp") - program_path_transformed = TEST_DIR.joinpath("res/transformed_program_rule_ids.lp") - rt = RuleIDTransformer() - result = rt.parse_file(program_path) - self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) - assumptions = { - (clingo.parse_term(s), True) - for s in [ - "_rule(1)", - "_rule(2)", - "_rule(3)", - "_rule(4)", - "_rule(5)", - "_rule(6)", - "_rule(7)", - ] - } - self.assertEqual(assumptions, rt.get_assumptions()) - - # --- CONSTRAINT TRANSFORMER - - def test_constraint_transformer(self) -> None: - """ - Test the ConstraintTransformer's `parse_file` method. - """ - program_path = TEST_DIR.joinpath("res/test_program_constraints.lp") - program_path_transformed = TEST_DIR.joinpath("res/transformed_program_constraints.lp") - ct = ConstraintTransformer(constraint_head_symbol="unsat") - result = ct.parse_files([program_path]) - self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) - - def test_constraint_transformer_include_id(self) -> None: - """ - Test the ConstraintTransformer's `parse_file` method. - """ - program_path = TEST_DIR.joinpath("res/test_program_constraints.lp") - program_path_transformed = TEST_DIR.joinpath("res/transformed_program_constraints_id.lp") - ct = ConstraintTransformer(constraint_head_symbol="unsat", include_id=True) - with open(program_path, "r", encoding="utf-8") as f: - result = ct.parse_string(f.read()) - self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) - - # --- RULE SPLITTER - - def test_rule_splitter(self) -> None: - """ - Test the RuleSplitter's `parse_file` method. - """ - - program_path = TEST_DIR.joinpath("res/test_program_rules.lp") - program_path_transformed = TEST_DIR.joinpath("res/transformed_program_rules_split.lp") - rs = RuleSplitter() - result = rs.parse_file(program_path) - self.assertEqual(result.strip(), self.read_file(program_path_transformed).strip()) - - # --- OPTIMIZATION REMOVER - - def test_optimization_remover(self) -> None: - """ - Test the OptimizationRemover's `parse_file` and `parse_string_method` method. - """ - - program_path = TEST_DIR.joinpath("res/test_program_optimization.lp") - program_path_transformed = TEST_DIR.joinpath("res/transformed_program_optimization.lp") - optrm = OptimizationRemover() - result_files = optrm.parse_files([program_path]) - with open(program_path, "r", encoding="utf-8") as f: - result_string = optrm.parse_string(f.read()) - self.assertEqual(result_files.strip(), self.read_file(program_path_transformed).strip()) - self.assertEqual(result_files.strip(), result_string.strip()) - - # --- FACT TRANSFORMER - - def test_fact_transformer(self) -> None: - """ - Test the FactTransformer's `parse_files` and `parse_string_method` method. - """ - - program_path = TEST_DIR.joinpath("res/test_program.lp") - program_path_transformed = TEST_DIR.joinpath("res/transformed_program_facts.lp") - ft = FactTransformer(signatures={("a", 1), ("d", 1), ("e", 1)}) - result_files = ft.parse_files([program_path]) - with open(program_path, "r", encoding="utf-8") as f: - result_string = ft.parse_string(f.read()) - self.assertEqual(result_files.strip(), self.read_file(program_path_transformed).strip()) - self.assertEqual(result_files.strip(), result_string.strip()) - - # PROPAGATORS - # --- DECISION ORDER PROPAGATOR - - def test_decision_order_propagator(self) -> None: - """ - Testing the functionality of the DecisionOrderPropagator without signatures - """ - program_path = TEST_DIR.joinpath("res/test_program_decision_order.lp") - control = clingo.Control() - dop = DecisionOrderPropagator() - control.register_propagator(dop) # type: ignore - control.load(str(program_path)) - control.ground() - control.solve(assumptions=[]) - - # No asserts since the propagator currently doesn't support any outputs but only prints. - - def test_decision_order_propagator_with_signatures(self) -> None: - """ - Testing the functionality of the DecisionOrderPropagator with signatures - """ - program_path = TEST_DIR.joinpath("res/test_program_decision_order.lp") - control = clingo.Control() - dop = DecisionOrderPropagator(signatures={("a", 0), ("b", 0), ("x", 1)}) - control.register_propagator(dop) # type: ignore - control.load(str(program_path)) - control.ground() - control.solve(assumptions=[]) - - # No asserts since the propagator currently doesn't support any outputs but only prints. - - # MUC - - def test_core_computer_shrink_single_muc(self) -> None: - """ - Test the CoreComputer's `shrink` function with a single MUC. - """ - - ctl = clingo.Control() - - program = """ - a(1..5). - :- a(1), a(4), a(5). - """ - signatures = {("a", 1)} - - muc, cc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) - - if cc.minimal is None: - self.fail() - self.assert_muc(cc.muc_to_string(muc), [{"a(1)", "a(4)", "a(5)"}]) - - def test_core_computer_shrink_single_atomic_muc(self) -> None: - """ - Test the CoreComputer's `shrink` function with a single atomic MUC. - """ - - ctl = clingo.Control() - - program = """ - a(1..5). - :- a(3). - """ - signatures = {("a", 1)} - - muc, cc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) - - if cc.minimal is None: - self.fail() - self.assert_muc(cc.muc_to_string(muc), [{"a(3)"}]) - - def test_core_computer_shrink_multiple_atomic_mucs(self) -> None: - """ - Test the CoreComputer's `shrink` function with multiple atomic MUC's. - """ - - ctl = clingo.Control() - - program = """ - a(1..10). - :- a(3). - :- a(5). - :- a(9). - """ - signatures = {("a", 1)} - - muc, cc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) - - if cc.minimal is None: - self.fail() - self.assert_muc(cc.muc_to_string(muc), [{"a(3)"}, {"a(5)"}, {"a(9)"}]) - - def test_core_computer_shrink_multiple_mucs(self) -> None: - """ - Test the CoreComputer's `shrink` function with multiple MUC's. - """ - - ctl = clingo.Control() - - program = """ - a(1..10). - :- a(3), a(9), a(5). - :- a(5), a(1), a(2). - :- a(9), a(2), a(7). - """ - signatures = {("a", 1)} - - muc, cc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) - - if cc.minimal is None: - self.fail() - self.assert_muc( - cc.muc_to_string(muc), - [ - {"a(3)", "a(9)", "a(5)"}, - {"a(5)", "a(1)", "a(2)"}, - {"a(9)", "a(2)", "a(7)"}, - ], - ) - - def test_core_computer_shrink_large_instance_random(self) -> None: - """ - Test the CoreComputer's `shrink` function with a large random assumption set. - """ - - ctl = clingo.Control() - - n_assumptions = 1000 - random_core = random.choices(range(n_assumptions), k=10) - program = f""" - a(1..{n_assumptions}). - :- {', '.join([f"a({i})" for i in random_core])}. - """ - signatures = {("a", 1)} - - muc, cc = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) - - if cc.minimal is None: - self.fail() - self.assert_muc(cc.muc_to_string(muc), [{f"a({i})" for i in random_core}]) - - def test_core_computer_shrink_satisfiable(self) -> None: - """ - Test the CoreComputer's `shrink` function with a satisfiable assumption set. - """ - - ctl = clingo.Control() - - program = """ - a(1..5). - """ - signatures = {("a", 1)} - - muc, _ = self.get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) - - self.assertEqual(muc, set()) - - def test_core_computer_get_multiple_minimal(self) -> None: - """ - Test the CoreComputer's `get_multiple_minimal` function to get multiple MUCs. - """ - - ctl = clingo.Control() - - program_path = TEST_DIR.joinpath("res/test_program_multi_muc.lp") - at = AssumptionTransformer(signatures={("a", 1)}) - parsed = at.parse_files([program_path]) - ctl.add("base", [], parsed) - ctl.ground([("base", [])]) - cc = CoreComputer(ctl, at.get_assumptions(ctl)) - - muc_generator = cc.get_multiple_minimal() - - muc_string_sets = [cc.muc_to_string(muc) for muc in list(muc_generator)] - for muc_string_set in muc_string_sets: - self.assertIn( - muc_string_set, - [{"a(1)", "a(2)"}, {"a(1)", "a(9)"}, {"a(3)", "a(5)", "a(8)"}], - ) - - def test_core_computer_get_multiple_minimal_max_mucs_2(self) -> None: - """ - Test the CoreComputer's `get_multiple_minimal` function to get multiple MUCs. - """ - - ctl = clingo.Control() - - program_path = TEST_DIR.joinpath("res/test_program_multi_muc.lp") - at = AssumptionTransformer(signatures={("a", 1)}) - parsed = at.parse_files([program_path]) - ctl.add("base", [], parsed) - ctl.ground([("base", [])]) - cc = CoreComputer(ctl, at.get_assumptions(ctl)) - - muc_generator = cc.get_multiple_minimal(max_mucs=2) - - muc_string_sets = [cc.muc_to_string(muc) for muc in list(muc_generator)] - for muc_string_set in muc_string_sets: - self.assertIn( - muc_string_set, - [{"a(1)", "a(2)"}, {"a(1)", "a(9)"}, {"a(3)", "a(5)", "a(8)"}], - ) - - self.assertEqual(len(muc_string_sets), 2) - - # --- INTERNAL - - def test_core_computer_internal_solve_no_assumptions(self) -> None: - """ - Test the CoreComputer's `_solve` function with no assumptions. - """ - - control = clingo.Control() - cc = CoreComputer(control, set()) - satisfiable, _, _ = cc._solve() # pylint: disable=W0212 - self.assertTrue(satisfiable) - - def test_core_computer_internal_compute_single_minimal_satisfiable(self) -> None: - """ - Test the CoreComputer's `_compute_single_minimal` function with a satisfiable assumption set. - """ - - control = clingo.Control() - program = "a.b.c." - control.add("base", [], program) - control.ground([("base", [])]) - assumptions = {(clingo.parse_term(c), True) for c in "abc"} - cc = CoreComputer(control, assumptions) - muc = cc._compute_single_minimal() # pylint: disable=W0212 - self.assertEqual(muc, set()) - - def test_core_computer_internal_compute_single_minimal_no_assumptions(self) -> None: - """ - Test the CoreComputer's `_compute_single_minimal` function with no assumptions. - """ - - control = clingo.Control() - cc = CoreComputer(control, set()) - self.assertRaises(ValueError, cc._compute_single_minimal) # pylint: disable=W0212 - - def test_core_computer_muc_to_string(self) -> None: - """ - Test the CoreComputer's `_compute_single_minimal` function with no assumptions. - """ - - control = clingo.Control() - cc = CoreComputer(control, set()) - self.assertEqual( - cc.muc_to_string({(clingo.parse_term(string), True) for string in ["this", "is", "a", "test"]}), - {"this", "is", "a", "test"}, - ) # pylint: disable=W0212 - - # UNSAT CONSTRAINT COMPUTER - - def unsat_constraint_computer_helper( - self, - constraint_strings: Dict[int, str], - constraint_lines: Dict[int, int], - constraint_files: Dict[int, str], - assumption_string: Optional[str] = None, - ) -> None: - """ - Helper function for testing the UnsatConstraintComputer - """ - for method in ["from_files", "from_string"]: - program_path = TEST_DIR.joinpath("res/test_program_unsat_constraints.lp") - ucc = UnsatConstraintComputer() - if method == "from_files": - ucc.parse_files([str(program_path)]) - elif method == "from_string": - with open(program_path, "r", encoding="utf-8") as f: - ucc.parse_string(f.read()) - unsat_constraints = ucc.get_unsat_constraints(assumption_string=assumption_string) - self.assertEqual(set(unsat_constraints.values()), set(constraint_strings.values())) - - for c_id in unsat_constraints: - loc = ucc.get_constraint_location(c_id) - if method == "from_files": - # only check the source file if .from_files is used to initialize - self.assertEqual(loc.begin.filename, constraint_files[c_id]) # type: ignore - self.assertEqual(loc.begin.line, constraint_lines[c_id]) # type: ignore - - def test_unsat_constraint_computer(self) -> None: - """ - Testing the UnsatConstraintComputer without assumptions. - """ - self.unsat_constraint_computer_helper( - constraint_strings={2: ":- not a."}, - constraint_lines={2: 4}, - constraint_files={2: str(TEST_DIR.joinpath("res/test_program_unsat_constraints.lp"))}, - ) - - def test_unsat_constraint_computer_with_assumptions(self) -> None: - """ - Testing the UnsatConstraintComputer with assumptions. - """ - self.unsat_constraint_computer_helper( - constraint_strings={1: ":- a."}, - constraint_lines={1: 3}, - constraint_files={1: str(TEST_DIR.joinpath("res/test_program_unsat_constraints.lp"))}, - assumption_string="a", - ) - - def test_unsat_constraint_computer_not_initialized(self) -> None: - """ - Testing the UnsatConstraintComputer without initializing it. - """ - ucc = UnsatConstraintComputer() - self.assertRaises(ValueError, ucc.get_unsat_constraints) diff --git a/tests/clingexplaid/test_muc.py b/tests/clingexplaid/test_muc.py new file mode 100644 index 0000000..2476ed2 --- /dev/null +++ b/tests/clingexplaid/test_muc.py @@ -0,0 +1,277 @@ +""" +Tests for the muc package +""" +import random +from typing import Set, Tuple, Optional, List +from unittest import TestCase + +import clingo +from clingexplaid.muc import CoreComputer +from clingexplaid.transformers import AssumptionTransformer +from clingexplaid.utils.types import AssumptionSet + +from .test_main import TEST_DIR + + +def get_muc_of_program( + program_string: str, + assumption_signatures: Set[Tuple[str, int]], + control: Optional[clingo.Control] = None, +) -> Tuple[AssumptionSet, CoreComputer]: + """ + Helper function to directly get the MUC of a given program string. + """ + ctl = control if control is not None else clingo.Control() + + at = AssumptionTransformer(signatures=assumption_signatures) + transformed_program = at.parse_string(program_string) + + ctl.add("base", [], transformed_program) + ctl.ground([("base", [])]) + + assumptions = at.get_assumptions(ctl) + + cc = CoreComputer(ctl, assumptions) + ctl.solve(assumptions=list(assumptions), on_core=cc.shrink) + + # if the instance was satisfiable and the on_core function wasn't called an empty set is returned, else the muc. + result = cc.minimal if cc.minimal is not None else set() + + return result, cc + + +class TestMUC(TestCase): + """ + Test cases for MUC functionality. + """ + + def _assert_muc( + self, + muc: Set[str], + valid_mucs_string_lists: List[Set[str]], + ) -> None: + """ + Asserts if a MUC is one of several valid MUC's. + """ + valid_mucs = [{clingo.parse_term(s) for s in lit_strings} for lit_strings in valid_mucs_string_lists] + parsed_muc = {clingo.parse_term(s) for s in muc} + self.assertIn(parsed_muc, valid_mucs) + + def test_core_computer_shrink_single_muc(self) -> None: + """ + Test the CoreComputer's `shrink` function with a single MUC. + """ + + ctl = clingo.Control() + + program = """ + a(1..5). + :- a(1), a(4), a(5). + """ + signatures = {("a", 1)} + + muc, cc = get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) + + if cc.minimal is None: + self.fail() + self._assert_muc(cc.muc_to_string(muc), [{"a(1)", "a(4)", "a(5)"}]) + + def test_core_computer_shrink_single_atomic_muc(self) -> None: + """ + Test the CoreComputer's `shrink` function with a single atomic MUC. + """ + + ctl = clingo.Control() + + program = """ + a(1..5). + :- a(3). + """ + signatures = {("a", 1)} + + muc, cc = get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) + + if cc.minimal is None: + self.fail() + self._assert_muc(cc.muc_to_string(muc), [{"a(3)"}]) + + def test_core_computer_shrink_multiple_atomic_mucs(self) -> None: + """ + Test the CoreComputer's `shrink` function with multiple atomic MUC's. + """ + + ctl = clingo.Control() + + program = """ + a(1..10). + :- a(3). + :- a(5). + :- a(9). + """ + signatures = {("a", 1)} + + muc, cc = get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) + + if cc.minimal is None: + self.fail() + self._assert_muc(cc.muc_to_string(muc), [{"a(3)"}, {"a(5)"}, {"a(9)"}]) + + def test_core_computer_shrink_multiple_mucs(self) -> None: + """ + Test the CoreComputer's `shrink` function with multiple MUC's. + """ + + ctl = clingo.Control() + + program = """ + a(1..10). + :- a(3), a(9), a(5). + :- a(5), a(1), a(2). + :- a(9), a(2), a(7). + """ + signatures = {("a", 1)} + + muc, cc = get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) + + if cc.minimal is None: + self.fail() + self._assert_muc( + cc.muc_to_string(muc), + [ + {"a(3)", "a(9)", "a(5)"}, + {"a(5)", "a(1)", "a(2)"}, + {"a(9)", "a(2)", "a(7)"}, + ], + ) + + def test_core_computer_shrink_large_instance_random(self) -> None: + """ + Test the CoreComputer's `shrink` function with a large random assumption set. + """ + + ctl = clingo.Control() + + n_assumptions = 1000 + random_core = random.choices(range(n_assumptions), k=10) + program = f""" + a(1..{n_assumptions}). + :- {', '.join([f"a({i})" for i in random_core])}. + """ + signatures = {("a", 1)} + + muc, cc = get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) + + if cc.minimal is None: + self.fail() + self._assert_muc(cc.muc_to_string(muc), [{f"a({i})" for i in random_core}]) + + def test_core_computer_shrink_satisfiable(self) -> None: + """ + Test the CoreComputer's `shrink` function with a satisfiable assumption set. + """ + + ctl = clingo.Control() + + program = """ + a(1..5). + """ + signatures = {("a", 1)} + + muc, _ = get_muc_of_program(program_string=program, assumption_signatures=signatures, control=ctl) + + self.assertEqual(muc, set()) + + def test_core_computer_get_multiple_minimal(self) -> None: + """ + Test the CoreComputer's `get_multiple_minimal` function to get multiple MUCs. + """ + + ctl = clingo.Control() + + program_path = TEST_DIR.joinpath("res/test_program_multi_muc.lp") + at = AssumptionTransformer(signatures={("a", 1)}) + parsed = at.parse_files([program_path]) + ctl.add("base", [], parsed) + ctl.ground([("base", [])]) + cc = CoreComputer(ctl, at.get_assumptions(ctl)) + + muc_generator = cc.get_multiple_minimal() + + muc_string_sets = [cc.muc_to_string(muc) for muc in list(muc_generator)] + for muc_string_set in muc_string_sets: + self.assertIn( + muc_string_set, + [{"a(1)", "a(2)"}, {"a(1)", "a(9)"}, {"a(3)", "a(5)", "a(8)"}], + ) + + def test_core_computer_get_multiple_minimal_max_mucs_2(self) -> None: + """ + Test the CoreComputer's `get_multiple_minimal` function to get multiple MUCs. + """ + + ctl = clingo.Control() + + program_path = TEST_DIR.joinpath("res/test_program_multi_muc.lp") + at = AssumptionTransformer(signatures={("a", 1)}) + parsed = at.parse_files([program_path]) + ctl.add("base", [], parsed) + ctl.ground([("base", [])]) + cc = CoreComputer(ctl, at.get_assumptions(ctl)) + + muc_generator = cc.get_multiple_minimal(max_mucs=2) + + muc_string_sets = [cc.muc_to_string(muc) for muc in list(muc_generator)] + for muc_string_set in muc_string_sets: + self.assertIn( + muc_string_set, + [{"a(1)", "a(2)"}, {"a(1)", "a(9)"}, {"a(3)", "a(5)", "a(8)"}], + ) + + self.assertEqual(len(muc_string_sets), 2) + + # INTERNAL + + def test_core_computer_internal_solve_no_assumptions(self) -> None: + """ + Test the CoreComputer's `_solve` function with no assumptions. + """ + + control = clingo.Control() + cc = CoreComputer(control, set()) + satisfiable, _, _ = cc._solve() # pylint: disable=W0212 + self.assertTrue(satisfiable) + + def test_core_computer_internal_compute_single_minimal_satisfiable(self) -> None: + """ + Test the CoreComputer's `_compute_single_minimal` function with a satisfiable assumption set. + """ + + control = clingo.Control() + program = "a.b.c." + control.add("base", [], program) + control.ground([("base", [])]) + assumptions = {(clingo.parse_term(c), True) for c in "abc"} + cc = CoreComputer(control, assumptions) + muc = cc._compute_single_minimal() # pylint: disable=W0212 + self.assertEqual(muc, set()) + + def test_core_computer_internal_compute_single_minimal_no_assumptions(self) -> None: + """ + Test the CoreComputer's `_compute_single_minimal` function with no assumptions. + """ + + control = clingo.Control() + cc = CoreComputer(control, set()) + self.assertRaises(ValueError, cc._compute_single_minimal) # pylint: disable=W0212 + + def test_core_computer_muc_to_string(self) -> None: + """ + Test the CoreComputer's `_compute_single_minimal` function with no assumptions. + """ + + control = clingo.Control() + cc = CoreComputer(control, set()) + self.assertEqual( + cc.muc_to_string({(clingo.parse_term(string), True) for string in ["this", "is", "a", "test"]}), + {"this", "is", "a", "test"}, + ) # pylint: disable=W0212 diff --git a/tests/clingexplaid/test_propagators.py b/tests/clingexplaid/test_propagators.py new file mode 100644 index 0000000..a08a4c3 --- /dev/null +++ b/tests/clingexplaid/test_propagators.py @@ -0,0 +1,46 @@ +""" +Tests for the propagators package +""" + +from unittest import TestCase + +import clingo +from clingexplaid.propagators import DecisionOrderPropagator + +from .test_main import TEST_DIR + + +class TestPropagators(TestCase): + """ + Test cases for propagators. + """ + + # DECISION ORDER PROPAGATOR + + def test_decision_order_propagator(self) -> None: + """ + Testing the functionality of the DecisionOrderPropagator without signatures + """ + program_path = TEST_DIR.joinpath("res/test_program_decision_order.lp") + control = clingo.Control() + dop = DecisionOrderPropagator() + control.register_propagator(dop) # type: ignore + control.load(str(program_path)) + control.ground() + control.solve(assumptions=[]) + + # No asserts since the propagator currently doesn't support any outputs but only prints. + + def test_decision_order_propagator_with_signatures(self) -> None: + """ + Testing the functionality of the DecisionOrderPropagator with signatures + """ + program_path = TEST_DIR.joinpath("res/test_program_decision_order.lp") + control = clingo.Control() + dop = DecisionOrderPropagator(signatures={("a", 0), ("b", 0), ("x", 1)}) + control.register_propagator(dop) # type: ignore + control.load(str(program_path)) + control.ground() + control.solve(assumptions=[]) + + # No asserts since the propagator currently doesn't support any outputs but only prints. diff --git a/tests/clingexplaid/test_transformers.py b/tests/clingexplaid/test_transformers.py new file mode 100644 index 0000000..9a20738 --- /dev/null +++ b/tests/clingexplaid/test_transformers.py @@ -0,0 +1,172 @@ +""" +Tests for the transformers package +""" + +from unittest import TestCase + +import clingo +from clingexplaid.transformers import ( + AssumptionTransformer, + RuleIDTransformer, + ConstraintTransformer, + RuleSplitter, + OptimizationRemover, + FactTransformer, +) +from clingexplaid.transformers.exceptions import UntransformedException, NotGroundedException + +from .test_main import TEST_DIR, read_file + + +class TestTransformers(TestCase): + """ + Test cases for transformers. + """ + + # ASSUMPTION TRANSFORMER + + def test_assumption_transformer_parse_file(self) -> None: + """ + Test the AssumptionTransformer's `parse_file` method. + """ + program_path = TEST_DIR.joinpath("res/test_program.lp") + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_assumptions_certain_signatures.lp") + at = AssumptionTransformer(signatures={(c, 1) for c in "abcdef"}) + result = at.parse_files([program_path]) + self.assertEqual(result.strip(), read_file(program_path_transformed).strip()) + + def test_assumption_transformer_parse_file_no_signatures(self) -> None: + """ + Test the AssumptionTransformer's `parse_file` method with no signatures provided. + """ + program_path = TEST_DIR.joinpath("res/test_program.lp") + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_assumptions_all.lp") + at = AssumptionTransformer() + result = at.parse_files([program_path]) + self.assertEqual(result.strip(), read_file(program_path_transformed).strip()) + + def test_assumption_transformer_get_assumptions_before_transformation(self) -> None: + """ + Test the AssumptionTransformer's behavior when get_assumptions is called before transformation. + """ + at = AssumptionTransformer() + control = clingo.Control() + self.assertRaises(UntransformedException, lambda: at.get_assumptions(control)) + + def test_assumption_transformer_get_assumptions_before_grounding(self) -> None: + """ + Test the AssumptionTransformer's behavior when get_assumptions is called before transformation. + """ + program_path = TEST_DIR.joinpath("res/test_program.lp") + at = AssumptionTransformer() + control = clingo.Control() + at.parse_files([program_path]) + self.assertRaises(NotGroundedException, lambda: at.get_assumptions(control)) + + def test_assumption_transformer_visit_definition(self) -> None: + """ + Test the AssumptionTransformer's detection of constant definitions. + """ + program_path = TEST_DIR.joinpath("res/test_program_constants.lp") + at = AssumptionTransformer() + control = clingo.Control() + result = at.parse_files([program_path]) + control.add("base", [], result) + control.ground([("base", [])]) + self.assertEqual( + at.program_constants, + {k: clingo.parse_term(v) for k, v in {"number": "42", "message": "helloworld"}.items()}, + ) + + # RULE ID TRANSFORMER + + def test_rule_id_transformer(self) -> None: + """ + Test the RuleIDTransformer's `parse_file` and `get_assumptions` methods. + """ + program_path = TEST_DIR.joinpath("res/test_program.lp") + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_rule_ids.lp") + rt = RuleIDTransformer() + result = rt.parse_file(program_path) + self.assertEqual(result.strip(), read_file(program_path_transformed).strip()) + assumptions = { + (clingo.parse_term(s), True) + for s in [ + "_rule(1)", + "_rule(2)", + "_rule(3)", + "_rule(4)", + "_rule(5)", + "_rule(6)", + "_rule(7)", + ] + } + self.assertEqual(assumptions, rt.get_assumptions()) + + # CONSTRAINT TRANSFORMER + + def test_constraint_transformer(self) -> None: + """ + Test the ConstraintTransformer's `parse_file` method. + """ + program_path = TEST_DIR.joinpath("res/test_program_constraints.lp") + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_constraints.lp") + ct = ConstraintTransformer(constraint_head_symbol="unsat") + result = ct.parse_files([program_path]) + self.assertEqual(result.strip(), read_file(program_path_transformed).strip()) + + def test_constraint_transformer_include_id(self) -> None: + """ + Test the ConstraintTransformer's `parse_file` method. + """ + program_path = TEST_DIR.joinpath("res/test_program_constraints.lp") + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_constraints_id.lp") + ct = ConstraintTransformer(constraint_head_symbol="unsat", include_id=True) + with open(program_path, "r", encoding="utf-8") as f: + result = ct.parse_string(f.read()) + self.assertEqual(result.strip(), read_file(program_path_transformed).strip()) + + # RULE SPLITTER + + def test_rule_splitter(self) -> None: + """ + Test the RuleSplitter's `parse_file` method. + """ + + program_path = TEST_DIR.joinpath("res/test_program_rules.lp") + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_rules_split.lp") + rs = RuleSplitter() + result = rs.parse_file(program_path) + self.assertEqual(result.strip(), read_file(program_path_transformed).strip()) + + # OPTIMIZATION REMOVER + + def test_optimization_remover(self) -> None: + """ + Test the OptimizationRemover's `parse_file` and `parse_string_method` method. + """ + + program_path = TEST_DIR.joinpath("res/test_program_optimization.lp") + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_optimization.lp") + optrm = OptimizationRemover() + result_files = optrm.parse_files([program_path]) + with open(program_path, "r", encoding="utf-8") as f: + result_string = optrm.parse_string(f.read()) + self.assertEqual(result_files.strip(), read_file(program_path_transformed).strip()) + self.assertEqual(result_files.strip(), result_string.strip()) + + # FACT TRANSFORMER + + def test_fact_transformer(self) -> None: + """ + Test the FactTransformer's `parse_files` and `parse_string_method` method. + """ + + program_path = TEST_DIR.joinpath("res/test_program.lp") + program_path_transformed = TEST_DIR.joinpath("res/transformed_program_facts.lp") + ft = FactTransformer(signatures={("a", 1), ("d", 1), ("e", 1)}) + result_files = ft.parse_files([program_path]) + with open(program_path, "r", encoding="utf-8") as f: + result_string = ft.parse_string(f.read()) + self.assertEqual(result_files.strip(), read_file(program_path_transformed).strip()) + self.assertEqual(result_files.strip(), result_string.strip()) diff --git a/tests/clingexplaid/test_unsat_constraints.py b/tests/clingexplaid/test_unsat_constraints.py new file mode 100644 index 0000000..1625924 --- /dev/null +++ b/tests/clingexplaid/test_unsat_constraints.py @@ -0,0 +1,74 @@ +""" +Tests for the unsat_constraints package +""" + +from typing import Dict, Optional +from unittest import TestCase + +from clingexplaid.unsat_constraints import UnsatConstraintComputer + +from .test_main import TEST_DIR + + +class TestUnsatConstraints(TestCase): + """ + Test cases for unsat constraints functionality. + """ + + # UNSAT CONSTRAINT COMPUTER + + def unsat_constraint_computer_helper( + self, + constraint_strings: Dict[int, str], + constraint_lines: Dict[int, int], + constraint_files: Dict[int, str], + assumption_string: Optional[str] = None, + ) -> None: + """ + Helper function for testing the UnsatConstraintComputer + """ + for method in ["from_files", "from_string"]: + program_path = TEST_DIR.joinpath("res/test_program_unsat_constraints.lp") + ucc = UnsatConstraintComputer() + if method == "from_files": + ucc.parse_files([str(program_path)]) + elif method == "from_string": + with open(program_path, "r", encoding="utf-8") as f: + ucc.parse_string(f.read()) + unsat_constraints = ucc.get_unsat_constraints(assumption_string=assumption_string) + self.assertEqual(set(unsat_constraints.values()), set(constraint_strings.values())) + + for c_id in unsat_constraints: + loc = ucc.get_constraint_location(c_id) + if method == "from_files": + # only check the source file if .from_files is used to initialize + self.assertEqual(loc.begin.filename, constraint_files[c_id]) # type: ignore + self.assertEqual(loc.begin.line, constraint_lines[c_id]) # type: ignore + + def test_unsat_constraint_computer(self) -> None: + """ + Testing the UnsatConstraintComputer without assumptions. + """ + self.unsat_constraint_computer_helper( + constraint_strings={2: ":- not a."}, + constraint_lines={2: 4}, + constraint_files={2: str(TEST_DIR.joinpath("res/test_program_unsat_constraints.lp"))}, + ) + + def test_unsat_constraint_computer_with_assumptions(self) -> None: + """ + Testing the UnsatConstraintComputer with assumptions. + """ + self.unsat_constraint_computer_helper( + constraint_strings={1: ":- a."}, + constraint_lines={1: 3}, + constraint_files={1: str(TEST_DIR.joinpath("res/test_program_unsat_constraints.lp"))}, + assumption_string="a", + ) + + def test_unsat_constraint_computer_not_initialized(self) -> None: + """ + Testing the UnsatConstraintComputer without initializing it. + """ + ucc = UnsatConstraintComputer() + self.assertRaises(ValueError, ucc.get_unsat_constraints) diff --git a/tests/clingexplaid/test_utils.py b/tests/clingexplaid/test_utils.py index e1bbcbc..7b3e30f 100644 --- a/tests/clingexplaid/test_utils.py +++ b/tests/clingexplaid/test_utils.py @@ -7,7 +7,7 @@ from clingexplaid.utils import get_signatures_from_model_string, get_constants_from_arguments -class TestMain(TestCase): +class TestUtils(TestCase): """ Test cases for clingexplaid. """ From 644aaf315a6c93eb243d02ee6a34c911ca317ce7 Mon Sep 17 00:00:00 2001 From: Hannes Weichelt Date: Thu, 18 Apr 2024 15:32:02 +0200 Subject: [PATCH 82/82] Fixed CI --- .github/workflows/ci-test.yml | 11 ++++-- noxfile.py | 2 +- pyproject.toml | 6 ++-- src/clingexplaid/utils/__init__.py | 2 +- tests/clingexplaid/test_propagators.py | 46 -------------------------- 5 files changed, 13 insertions(+), 54 deletions(-) delete mode 100644 tests/clingexplaid/test_propagators.py diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 5accb48..f8f3706 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -23,16 +23,21 @@ jobs: - name: "checkout repository" uses: actions/checkout@v3 - - name: "setup python 3.7" + - name: "setup python 3.9" uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.9 - name: "setup python 3.11" uses: actions/setup-python@v4 with: python-version: 3.11 - + + - name: "setup python 3.12" + uses: actions/setup-python@v4 + with: + python-version: 3.12 + - name: install nox run: python -m pip install nox diff --git a/noxfile.py b/noxfile.py index e7dc802..c085be7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,7 +7,7 @@ EDITABLE_TESTS = True PYTHON_VERSIONS = None if "GITHUB_ACTIONS" in os.environ: - PYTHON_VERSIONS = ["3.11"] + PYTHON_VERSIONS = ["3.12", "3.11", "3.9"] EDITABLE_TESTS = False diff --git a/pyproject.toml b/pyproject.toml index c72ca97..81e74e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,12 +11,12 @@ authors = [ { name = "Hannes Weichelt", email = "hweichelt@uni-potsdam.de" } ] description = "A template project." -requires-python = ">=3.11" +requires-python = ">=3.9" license = {file = "LICENSE"} dynamic = [ "version" ] readme = "README.md" dependencies = [ - "clingo>=5.6.0", + "clingo>=5.7.1", "autoflake", ] @@ -79,7 +79,7 @@ source = ["clingexplaid", "tests"] omit = [ "*/clingexplaid/__main__.py", "*/clingexplaid/cli/*", - "*/clingexplaid/propagators/__init__.py", + "*/clingexplaid/propagators/*", "*/clingexplaid/transformers/__init__.py", "*/clingexplaid/muc/__init__.py", "*/clingexplaid/unsat_constraints/__init__.py", diff --git a/src/clingexplaid/utils/__init__.py b/src/clingexplaid/utils/__init__.py index c153b21..7c661b5 100644 --- a/src/clingexplaid/utils/__init__.py +++ b/src/clingexplaid/utils/__init__.py @@ -82,7 +82,7 @@ def get_constants_from_arguments(argument_vector: List[str]) -> Dict[str, str]: for element in argument_vector: if next_constant: result = re.search(r"(.*)=(.*)", element) - if result is None or len(result.groups()) == 0: + if result is None: continue constants[result.group(1)] = result.group(2) next_constant = False diff --git a/tests/clingexplaid/test_propagators.py b/tests/clingexplaid/test_propagators.py deleted file mode 100644 index a08a4c3..0000000 --- a/tests/clingexplaid/test_propagators.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Tests for the propagators package -""" - -from unittest import TestCase - -import clingo -from clingexplaid.propagators import DecisionOrderPropagator - -from .test_main import TEST_DIR - - -class TestPropagators(TestCase): - """ - Test cases for propagators. - """ - - # DECISION ORDER PROPAGATOR - - def test_decision_order_propagator(self) -> None: - """ - Testing the functionality of the DecisionOrderPropagator without signatures - """ - program_path = TEST_DIR.joinpath("res/test_program_decision_order.lp") - control = clingo.Control() - dop = DecisionOrderPropagator() - control.register_propagator(dop) # type: ignore - control.load(str(program_path)) - control.ground() - control.solve(assumptions=[]) - - # No asserts since the propagator currently doesn't support any outputs but only prints. - - def test_decision_order_propagator_with_signatures(self) -> None: - """ - Testing the functionality of the DecisionOrderPropagator with signatures - """ - program_path = TEST_DIR.joinpath("res/test_program_decision_order.lp") - control = clingo.Control() - dop = DecisionOrderPropagator(signatures={("a", 0), ("b", 0), ("x", 1)}) - control.register_propagator(dop) # type: ignore - control.load(str(program_path)) - control.ground() - control.solve(assumptions=[]) - - # No asserts since the propagator currently doesn't support any outputs but only prints.