clingexplaid 1.0.9__py3-none-any.whl

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