flamapy-sat 2.6.0.dev11__tar.gz → 2.9.0.dev0__tar.gz
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.
- {flamapy_sat-2.6.0.dev11/flamapy_sat.egg-info → flamapy_sat-2.9.0.dev0}/PKG-INFO +6 -3
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/__init__.py +9 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/models/__init__.py +8 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/models/explanation.py +35 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/models/pysat_diagnosis_model.py +4 -1
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/__init__.py +10 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/fastdiag.py +2 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/hsdag.py +2 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/fastdiag_labeler.py +5 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/labeler.py +6 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/quickxplain_labeler.py +5 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/node.py +2 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/quickxplain.py +2 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/utils.py +2 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_explanations.py +257 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/fm_to_diag_pysat.py +3 -2
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/__init__.py +46 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/models/pysat_model.py +102 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/__init__.py +12 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_atomic_sets.py +108 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_backbone.py +90 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_commonality.py +67 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations.py +67 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations_number.py +3 -4
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_false_optional_features.py +106 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_filter.py +5 -7
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_metrics.py +20 -6
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_minimum_configuration.py +24 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_sampling.py +6 -7
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_testing.py +212 -0
- flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_twise_sampling.py +347 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_reader.py +6 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_writer.py +5 -1
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/transformations/fm_to_pysat.py +5 -3
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0/flamapy_sat.egg-info}/PKG-INFO +6 -3
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy_sat.egg-info/SOURCES.txt +5 -0
- flamapy_sat-2.9.0.dev0/flamapy_sat.egg-info/entry_points.txt +3 -0
- flamapy_sat-2.9.0.dev0/flamapy_sat.egg-info/requires.txt +14 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/pyproject.toml +13 -3
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/__init__.py +0 -0
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/models/__init__.py +0 -5
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/fastdiag.py +0 -95
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/hsdag.py +0 -312
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/fastdiag_labeler.py +0 -71
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/labeler.py +0 -49
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/quickxplain_labeler.py +0 -69
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/node.py +0 -78
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/quickxplain.py +0 -88
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/utils.py +0 -46
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/__init__.py +0 -0
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/models/pysat_model.py +0 -39
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/operations/pysat_backbone.py +0 -83
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/operations/pysat_commonality.py +0 -50
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations.py +0 -43
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/operations/pysat_false_optional_features.py +0 -57
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/operations/pysat_twise_sampling.py +0 -102
- flamapy_sat-2.6.0.dev11/flamapy_sat.egg-info/requires.txt +0 -10
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/LICENSE +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/README.md +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/checker.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_abstract_identifier.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_configuration_diagnosis.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_conflict.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_diagnosis.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/models/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/models/txtcnf_model.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_attribute_optimization.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_core_features.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_dead_features.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable_configuration.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/transformations/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/transformations/cnf_to_pysat.py +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy_sat.egg-info/dependency_links.txt +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy_sat.egg-info/top_level.txt +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/setup.cfg +0 -0
- {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flamapy-sat
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0.dev0
|
|
4
4
|
Summary: flamapy-sat is a plugin to flamapy module
|
|
5
5
|
Author-email: Flamapy <flamapy@us.es>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -8,8 +8,8 @@ Project-URL: Homepage, https://github.com/flamapy/pysat_metamodel
|
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: flamapy-fw~=2.
|
|
12
|
-
Requires-Dist: flamapy-fm~=2.
|
|
11
|
+
Requires-Dist: flamapy-fw~=2.9.0.dev0
|
|
12
|
+
Requires-Dist: flamapy-fm~=2.9.0.dev0
|
|
13
13
|
Requires-Dist: python-sat~=0.1.7.dev1
|
|
14
14
|
Provides-Extra: dev
|
|
15
15
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -17,6 +17,9 @@ Requires-Dist: pytest-mock; extra == "dev"
|
|
|
17
17
|
Requires-Dist: prospector; extra == "dev"
|
|
18
18
|
Requires-Dist: mypy; extra == "dev"
|
|
19
19
|
Requires-Dist: coverage; extra == "dev"
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: pytest; extra == "test"
|
|
22
|
+
Requires-Dist: flamapy-z3~=2.9.0.dev0; extra == "test"
|
|
20
23
|
Dynamic: license-file
|
|
21
24
|
|
|
22
25
|
# flamapy-sat
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from flamapy.core.manifest import PluginManifest
|
|
2
|
+
from flamapy.core.models.language_level import LanguageLevel, MajorLevel, MinorLevel
|
|
3
|
+
|
|
4
|
+
MANIFEST = PluginManifest(
|
|
5
|
+
name='pysat_diagnosis',
|
|
6
|
+
extension='pysat_diagnosis',
|
|
7
|
+
# Same encoder as the SAT plugin (ClauseSet), so the same language level.
|
|
8
|
+
supported_level=LanguageLevel(MajorLevel.BOOLEAN, {MinorLevel.GROUP_CARDINALITY}),
|
|
9
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Human-readable explanation values returned by the explain_* operations.
|
|
2
|
+
|
|
3
|
+
Each item names a relationship or cross-tree constraint of the model (the same
|
|
4
|
+
descriptions the diagnosis encoder records), and keeps the underlying assumption index so
|
|
5
|
+
tools can act on the result programmatically.
|
|
6
|
+
"""
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ExplanationItem:
|
|
12
|
+
constraint_repr: str # human-readable description of the relationship/constraint
|
|
13
|
+
index: int # assumption id in the diagnosis model (machine-usable)
|
|
14
|
+
kind: str # 'relationship' (tree) | 'constraint' (cross-tree)
|
|
15
|
+
|
|
16
|
+
def __str__(self) -> str:
|
|
17
|
+
return self.constraint_repr
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Explanation:
|
|
22
|
+
items: tuple[ExplanationItem, ...] = ()
|
|
23
|
+
|
|
24
|
+
def descriptions(self) -> list[str]:
|
|
25
|
+
return [item.constraint_repr for item in self.items]
|
|
26
|
+
|
|
27
|
+
def __bool__(self) -> bool:
|
|
28
|
+
return bool(self.items)
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
if not self.items:
|
|
32
|
+
return 'No conflict found.'
|
|
33
|
+
return '\n'.join(
|
|
34
|
+
f'{number}. {item.constraint_repr}'
|
|
35
|
+
for number, item in enumerate(self.items, start=1))
|
|
@@ -117,7 +117,10 @@ class DiagnosisModel(PySATModel):
|
|
|
117
117
|
test_case: Configuration = None) -> None:
|
|
118
118
|
assumption: List[int] = []
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
# Assumption selectors start above every variable of the encoding: with Tseytin
|
|
121
|
+
# ('tseytin'/'auto') the ids after the features belong to auxiliary variables.
|
|
122
|
+
# Without auxiliary variables this is len(self.variables) + 1, as before.
|
|
123
|
+
id_assumption = self.top_variable() + 1
|
|
121
124
|
id_assumption = self._prepare_assumptions_for_kb(assumption, id_assumption)
|
|
122
125
|
|
|
123
126
|
start_id_configuration = len(assumption)
|
|
@@ -8,6 +8,12 @@ from .pysat_configuration_diagnosis import (
|
|
|
8
8
|
PySATConfigurationRepair,
|
|
9
9
|
PySATFeatureExplanation,
|
|
10
10
|
)
|
|
11
|
+
from .pysat_explanations import (
|
|
12
|
+
PySATExplainDeadFeature,
|
|
13
|
+
PySATExplainFalseOptional,
|
|
14
|
+
PySATExplainVoidModel,
|
|
15
|
+
PySATMinimalCorrections,
|
|
16
|
+
)
|
|
11
17
|
|
|
12
18
|
|
|
13
19
|
__all__ = [
|
|
@@ -16,5 +22,9 @@ __all__ = [
|
|
|
16
22
|
'PySATConfigurationRepair',
|
|
17
23
|
'PySATConflict',
|
|
18
24
|
'PySATDiagnosis',
|
|
25
|
+
'PySATExplainDeadFeature',
|
|
26
|
+
'PySATExplainFalseOptional',
|
|
27
|
+
'PySATExplainVoidModel',
|
|
19
28
|
'PySATFeatureExplanation',
|
|
29
|
+
'PySATMinimalCorrections',
|
|
20
30
|
]
|
flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_explanations.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""User-facing explanation operations: minimal conflicts in constraint names, not indices.
|
|
2
|
+
|
|
3
|
+
Both operations compute a QuickXPlain minimal conflict over the model's relationships and
|
|
4
|
+
cross-tree constraints and report it through the :class:`Explanation` value type, whose
|
|
5
|
+
items carry the encoder's human-readable descriptions.
|
|
6
|
+
|
|
7
|
+
They rebuild a pristine diagnosis model from ``original_model`` on every execution:
|
|
8
|
+
``prepare_diagnosis_task`` mutates the encoded clauses in place, so reusing the (facade-
|
|
9
|
+
cached) model across calls with different inputs would corrupt the assumption ids.
|
|
10
|
+
"""
|
|
11
|
+
from typing import Optional, cast
|
|
12
|
+
|
|
13
|
+
from flamapy.core.exceptions import FlamaException
|
|
14
|
+
from flamapy.core.models import VariabilityModel
|
|
15
|
+
from flamapy.core.operations import Operation
|
|
16
|
+
from flamapy.core.operations.descriptor import OperationDescriptor, Input
|
|
17
|
+
from flamapy.core.reasoning import QuickXPlain
|
|
18
|
+
from flamapy.core.reasoning.hsdag import HSDAG
|
|
19
|
+
from flamapy.core.reasoning.hsdag.labeler.fastdiag_labeler import (
|
|
20
|
+
FastDiagLabeler,
|
|
21
|
+
FastDiagParameters,
|
|
22
|
+
)
|
|
23
|
+
from flamapy.metamodels.fm_metamodel.models import DEFAULT_CNF_METHOD
|
|
24
|
+
from flamapy.metamodels.configuration_metamodel.models import Configuration
|
|
25
|
+
|
|
26
|
+
from ..models.explanation import Explanation, ExplanationItem
|
|
27
|
+
from ..models.pysat_diagnosis_model import DiagnosisModel
|
|
28
|
+
from ..transformations.fm_to_diag_pysat import FmToDiagPysat
|
|
29
|
+
from .diagnosis.checker import ConsistencyChecker
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _explanation_descriptions(explanation: Explanation) -> list[str]:
|
|
33
|
+
return explanation.descriptions()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _corrections_descriptions(explanations: list[Explanation]) -> list[list[str]]:
|
|
37
|
+
return [explanation.descriptions() for explanation in explanations]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _prepared_model(
|
|
41
|
+
model: DiagnosisModel, test_case: Optional[Configuration]
|
|
42
|
+
) -> DiagnosisModel:
|
|
43
|
+
"""A pristine diagnosis model with the task prepared.
|
|
44
|
+
|
|
45
|
+
``prepare_diagnosis_task`` mutates the encoded clauses in place, so the (facade-cached)
|
|
46
|
+
input model is never prepared directly: we re-encode from ``original_model``.
|
|
47
|
+
"""
|
|
48
|
+
original = getattr(model, 'original_model', None)
|
|
49
|
+
cnf_method = getattr(model, 'cnf_method', None) or DEFAULT_CNF_METHOD
|
|
50
|
+
working = (FmToDiagPysat(original, cnf_method=cnf_method).transform()
|
|
51
|
+
if original is not None else model)
|
|
52
|
+
working.prepare_diagnosis_task(test_case=test_case)
|
|
53
|
+
return working
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _explanation_from(working: DiagnosisModel, indices: list[int]) -> Explanation:
|
|
57
|
+
original = getattr(working, 'original_model', None)
|
|
58
|
+
constraint_reprs = (
|
|
59
|
+
{str(ctc) for ctc in original.get_constraints()} if original is not None else set())
|
|
60
|
+
return Explanation(tuple(
|
|
61
|
+
ExplanationItem(
|
|
62
|
+
constraint_repr=working.constraint_assumption_map[index],
|
|
63
|
+
index=index,
|
|
64
|
+
kind='constraint'
|
|
65
|
+
if working.constraint_assumption_map[index] in constraint_reprs
|
|
66
|
+
else 'relationship',
|
|
67
|
+
)
|
|
68
|
+
for index in sorted(indices)))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _minimal_conflict(
|
|
72
|
+
model: DiagnosisModel, test_case: Optional[Configuration], solver_name: str
|
|
73
|
+
) -> Explanation:
|
|
74
|
+
working = _prepared_model(model, test_case)
|
|
75
|
+
checker = ConsistencyChecker(solver_name, working.get_kb())
|
|
76
|
+
try:
|
|
77
|
+
conflict = QuickXPlain(checker).find_conflict(working.get_c(), working.get_b())
|
|
78
|
+
finally:
|
|
79
|
+
checker.delete()
|
|
80
|
+
return _explanation_from(working, conflict)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class PySATExplainVoidModel(Operation):
|
|
84
|
+
"""Why is the model void? A minimal, human-readable conflict among its constraints."""
|
|
85
|
+
|
|
86
|
+
facade = OperationDescriptor(
|
|
87
|
+
doc=(
|
|
88
|
+
'Explains why the feature model is void (encodes no valid configuration): a\n'
|
|
89
|
+
'minimal set of relationships/cross-tree constraints that is already\n'
|
|
90
|
+
'unsatisfiable (a QuickXPlain minimal conflict), in human-readable form.\n'
|
|
91
|
+
'Returns an empty list when the model is satisfiable. Requires the\n'
|
|
92
|
+
'pysat_diagnosis plugin.'
|
|
93
|
+
),
|
|
94
|
+
returns='Union[None, List[str]]',
|
|
95
|
+
name='explain_void_model', operation='PySATExplainVoidModel',
|
|
96
|
+
default_backend='pysat_diagnosis',
|
|
97
|
+
result_adapter=_explanation_descriptions,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def __init__(self) -> None:
|
|
101
|
+
self.result = Explanation()
|
|
102
|
+
self.solver_name = 'glucose3'
|
|
103
|
+
|
|
104
|
+
def execute(self, model: VariabilityModel) -> 'PySATExplainVoidModel':
|
|
105
|
+
self.result = _minimal_conflict(cast(DiagnosisModel, model), None, self.solver_name)
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def get_result(self) -> Explanation:
|
|
109
|
+
return self.result
|
|
110
|
+
|
|
111
|
+
def get_explanation(self) -> Explanation:
|
|
112
|
+
return self.result
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class PySATExplainFalseOptional(Operation):
|
|
116
|
+
"""Why is this feature false-optional? Conflict of the model plus parent without child."""
|
|
117
|
+
|
|
118
|
+
facade = OperationDescriptor(
|
|
119
|
+
doc=(
|
|
120
|
+
'Explains why a feature is false-optional (declared optional but present in\n'
|
|
121
|
+
'every valid configuration that includes its parent): a minimal set of\n'
|
|
122
|
+
'relationships/cross-tree constraints that forbids deselecting it while its\n'
|
|
123
|
+
'parent is selected (a QuickXPlain minimal conflict of the model plus\n'
|
|
124
|
+
'"parent = true, feature = false"), in human-readable form. Returns an empty\n'
|
|
125
|
+
'list when the feature is not false-optional. Requires the pysat_diagnosis\n'
|
|
126
|
+
'plugin.'
|
|
127
|
+
),
|
|
128
|
+
returns='Union[None, List[str]]',
|
|
129
|
+
name='explain_false_optional', operation='PySATExplainFalseOptional',
|
|
130
|
+
default_backend='pysat_diagnosis',
|
|
131
|
+
inputs=(Input('feature_name', str, required=True, setter='set_feature'),),
|
|
132
|
+
result_adapter=_explanation_descriptions,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def __init__(self) -> None:
|
|
136
|
+
self.feature = ''
|
|
137
|
+
self.result = Explanation()
|
|
138
|
+
self.solver_name = 'glucose3'
|
|
139
|
+
|
|
140
|
+
def set_feature(self, feature_name: str) -> None:
|
|
141
|
+
self.feature = feature_name
|
|
142
|
+
|
|
143
|
+
def execute(self, model: VariabilityModel) -> 'PySATExplainFalseOptional':
|
|
144
|
+
diag_model = cast(DiagnosisModel, model)
|
|
145
|
+
original = getattr(diag_model, 'original_model', None)
|
|
146
|
+
feature = (original.get_feature_by_name(self.feature)
|
|
147
|
+
if original is not None else None)
|
|
148
|
+
if feature is None:
|
|
149
|
+
raise FlamaException(f"Feature '{self.feature}' is not in the model.")
|
|
150
|
+
parent = feature.get_parent()
|
|
151
|
+
if parent is None:
|
|
152
|
+
raise FlamaException(
|
|
153
|
+
f"Feature '{self.feature}' is the root; it cannot be false-optional.")
|
|
154
|
+
test_case = Configuration({parent.name: True, self.feature: False})
|
|
155
|
+
try:
|
|
156
|
+
self.result = _minimal_conflict(diag_model, test_case, self.solver_name)
|
|
157
|
+
except KeyError as error:
|
|
158
|
+
raise FlamaException(
|
|
159
|
+
f"Feature '{self.feature}' is not in the model.") from error
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
def get_result(self) -> Explanation:
|
|
163
|
+
return self.result
|
|
164
|
+
|
|
165
|
+
def get_explanation(self) -> Explanation:
|
|
166
|
+
return self.result
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class PySATMinimalCorrections(Operation):
|
|
170
|
+
"""Minimal constraint sets whose removal makes a void model satisfiable (FastDiag)."""
|
|
171
|
+
|
|
172
|
+
facade = OperationDescriptor(
|
|
173
|
+
doc=(
|
|
174
|
+
'Returns the minimal corrections for a void feature model: each correction is\n'
|
|
175
|
+
'a minimal set of relationships/cross-tree constraints whose removal makes\n'
|
|
176
|
+
'the model satisfiable (FastDiag diagnoses enumerated via HSDAG), in\n'
|
|
177
|
+
'human-readable form. Returns an empty list when the model is already\n'
|
|
178
|
+
'satisfiable. ``max_corrections`` bounds how many are enumerated. Requires\n'
|
|
179
|
+
'the pysat_diagnosis plugin.'
|
|
180
|
+
),
|
|
181
|
+
returns='Union[None, List[List[str]]]',
|
|
182
|
+
name='minimal_corrections', operation='PySATMinimalCorrections',
|
|
183
|
+
default_backend='pysat_diagnosis',
|
|
184
|
+
inputs=(Input('max_corrections', int, default=None, setter='set_max_corrections'),),
|
|
185
|
+
result_adapter=_corrections_descriptions,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def __init__(self) -> None:
|
|
189
|
+
self.max_corrections = -1 # -1 means no limit
|
|
190
|
+
self.result: list[Explanation] = []
|
|
191
|
+
self.solver_name = 'glucose3'
|
|
192
|
+
|
|
193
|
+
def set_max_corrections(self, max_corrections: int) -> None:
|
|
194
|
+
self.max_corrections = max_corrections
|
|
195
|
+
|
|
196
|
+
def execute(self, model: VariabilityModel) -> 'PySATMinimalCorrections':
|
|
197
|
+
working = _prepared_model(cast(DiagnosisModel, model), None)
|
|
198
|
+
checker = ConsistencyChecker(self.solver_name, working.get_kb())
|
|
199
|
+
try:
|
|
200
|
+
parameters = FastDiagParameters(working.get_c(), [], working.get_b())
|
|
201
|
+
hsdag = HSDAG(FastDiagLabeler(checker, parameters))
|
|
202
|
+
hsdag.max_number_diagnoses = self.max_corrections
|
|
203
|
+
hsdag.construct()
|
|
204
|
+
diagnoses = hsdag.get_diagnoses()
|
|
205
|
+
finally:
|
|
206
|
+
checker.delete()
|
|
207
|
+
self.result = [_explanation_from(working, diagnosis) for diagnosis in diagnoses]
|
|
208
|
+
return self
|
|
209
|
+
|
|
210
|
+
def get_result(self) -> list[Explanation]:
|
|
211
|
+
return self.result
|
|
212
|
+
|
|
213
|
+
def get_corrections(self) -> list[Explanation]:
|
|
214
|
+
return self.result
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class PySATExplainDeadFeature(Operation):
|
|
218
|
+
"""Why is this feature dead? A minimal conflict of the model plus 'feature = true'."""
|
|
219
|
+
|
|
220
|
+
facade = OperationDescriptor(
|
|
221
|
+
doc=(
|
|
222
|
+
'Explains why a feature is dead (appears in no valid configuration): a\n'
|
|
223
|
+
'minimal set of relationships/cross-tree constraints that forbids selecting\n'
|
|
224
|
+
'it (a QuickXPlain minimal conflict of the model plus "feature = true"), in\n'
|
|
225
|
+
'human-readable form. Returns an empty list when the feature is not dead.\n'
|
|
226
|
+
'Requires the pysat_diagnosis plugin.'
|
|
227
|
+
),
|
|
228
|
+
returns='Union[None, List[str]]',
|
|
229
|
+
name='explain_dead_feature', operation='PySATExplainDeadFeature',
|
|
230
|
+
default_backend='pysat_diagnosis',
|
|
231
|
+
inputs=(Input('feature_name', str, required=True, setter='set_feature'),),
|
|
232
|
+
result_adapter=_explanation_descriptions,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def __init__(self) -> None:
|
|
236
|
+
self.feature = ''
|
|
237
|
+
self.result = Explanation()
|
|
238
|
+
self.solver_name = 'glucose3'
|
|
239
|
+
|
|
240
|
+
def set_feature(self, feature_name: str) -> None:
|
|
241
|
+
self.feature = feature_name
|
|
242
|
+
|
|
243
|
+
def execute(self, model: VariabilityModel) -> 'PySATExplainDeadFeature':
|
|
244
|
+
diag_model = cast(DiagnosisModel, model)
|
|
245
|
+
test_case = Configuration({self.feature: True})
|
|
246
|
+
try:
|
|
247
|
+
self.result = _minimal_conflict(diag_model, test_case, self.solver_name)
|
|
248
|
+
except KeyError as error:
|
|
249
|
+
raise FlamaException(
|
|
250
|
+
f"Feature '{self.feature}' is not in the model.") from error
|
|
251
|
+
return self
|
|
252
|
+
|
|
253
|
+
def get_result(self) -> Explanation:
|
|
254
|
+
return self.result
|
|
255
|
+
|
|
256
|
+
def get_explanation(self) -> Explanation:
|
|
257
|
+
return self.result
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from flamapy.metamodels.fm_metamodel.models import
|
|
1
|
+
from flamapy.metamodels.fm_metamodel.models import DEFAULT_CNF_METHOD, ClauseSet, FeatureModel
|
|
2
2
|
from flamapy.metamodels.pysat_metamodel.transformations.fm_to_pysat import FmToPysat
|
|
3
3
|
from ..models.pysat_diagnosis_model import DiagnosisModel
|
|
4
4
|
|
|
@@ -18,7 +18,7 @@ class FmToDiagPysat(FmToPysat):
|
|
|
18
18
|
def get_destination_extension() -> str:
|
|
19
19
|
return 'pysat_diagnosis'
|
|
20
20
|
|
|
21
|
-
def __init__(self, source_model: FeatureModel, cnf_method: str =
|
|
21
|
+
def __init__(self, source_model: FeatureModel, cnf_method: str = DEFAULT_CNF_METHOD) -> None:
|
|
22
22
|
super().__init__(source_model, cnf_method)
|
|
23
23
|
self.destination_model = DiagnosisModel()
|
|
24
24
|
|
|
@@ -30,6 +30,7 @@ class FmToDiagPysat(FmToPysat):
|
|
|
30
30
|
model.variables = dict(clause_set.variables)
|
|
31
31
|
model.features = dict(clause_set.features)
|
|
32
32
|
model.auxiliary_variables = set(clause_set.auxiliary_variables)
|
|
33
|
+
model.cnf_method = self.cnf_method
|
|
33
34
|
model.original_model = self.source_model
|
|
34
35
|
# Clause → constraint mapping (root feature / relations / cross-tree constraints).
|
|
35
36
|
for description, clauses in clause_set.clause_groups:
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from flamapy.core.manifest import OperationCapability, PluginManifest
|
|
2
|
+
from flamapy.core.models.language_level import LanguageLevel, MajorLevel, MinorLevel
|
|
3
|
+
|
|
4
|
+
# Costs are estimated milliseconds from the 2026-11-06 calibration (betty synthetic models at 1%
|
|
5
|
+
# and 30% constraint density, 20-10,000 features, plus JHipster and busybox; worst case of the
|
|
6
|
+
# two densities, carried forward so a curve never decreases). SAT queries scale to the largest
|
|
7
|
+
# models (satisfiable 8 ms, core/dead 70 ms and false-optional 300 ms at 10,000 features), so the
|
|
8
|
+
# only limits are on the operations that enumerate every configuration.
|
|
9
|
+
_SAT_QUERY = ((2000, 1), (5000, 4), (10000, 8))
|
|
10
|
+
_IMPLICATION_QUERIES = ((1000, 2), (2000, 4), (5000, 40), (10000, 70))
|
|
11
|
+
|
|
12
|
+
MANIFEST = PluginManifest(
|
|
13
|
+
name='pysat',
|
|
14
|
+
calibration='2026-11-06',
|
|
15
|
+
extension='pysat',
|
|
16
|
+
supported_level=LanguageLevel(MajorLevel.BOOLEAN, {MinorLevel.GROUP_CARDINALITY}),
|
|
17
|
+
transformation_cost_curve=((200, 1), (1000, 4), (2000, 12), (5000, 19), (10000, 46)),
|
|
18
|
+
operations={
|
|
19
|
+
'satisfiable': OperationCapability(cost_curve=_SAT_QUERY),
|
|
20
|
+
'satisfiable_configuration': OperationCapability(cost_curve=_SAT_QUERY),
|
|
21
|
+
'core_features': OperationCapability(cost_curve=_IMPLICATION_QUERIES),
|
|
22
|
+
'dead_features': OperationCapability(cost_curve=_IMPLICATION_QUERIES),
|
|
23
|
+
'false_optional_features': OperationCapability(
|
|
24
|
+
cost_curve=((500, 2), (1000, 6), (2000, 34), (5000, 110), (10000, 300))),
|
|
25
|
+
# Atomic sets: the classes of features with the same value in every configuration,
|
|
26
|
+
# proved with grouped implication queries like the backbone.
|
|
27
|
+
'atomic_sets': OperationCapability(cost_curve=_IMPLICATION_QUERIES),
|
|
28
|
+
'minimum_configuration': OperationCapability(
|
|
29
|
+
cost_curve=((200, 2), (1000, 5), (2000, 30), (5000, 38), (10000, 110))),
|
|
30
|
+
# Pairwise sampling (the only t-wise implementation): 1.2 s at 200 features, 56 s at
|
|
31
|
+
# 500, 6.5 min at 1,000; the 2,000-feature model did not finish within 10 minutes.
|
|
32
|
+
't_wise_sampling': OperationCapability(
|
|
33
|
+
cost_curve=((50, 100), (100, 310), (200, 1200), (500, 56000), (1000, 390000),
|
|
34
|
+
(2000, 600000))),
|
|
35
|
+
# PySATSampling returns valid configurations, but in the solver's fixed order instead of
|
|
36
|
+
# uniformly at random (BDD sampling is uniform), so it is not the sample the operation
|
|
37
|
+
# promises: approximate, and the planner only takes it on allow_approximate.
|
|
38
|
+
'sampling': OperationCapability(
|
|
39
|
+
exact=False, cost_curve=((1000, 1), (5000, 9), (10000, 15))),
|
|
40
|
+
# Counting and listing enumerate every configuration: exponential in the number of
|
|
41
|
+
# configurations rather than in the features (6 ms on the 20-feature calibration model).
|
|
42
|
+
'configurations_number': OperationCapability(max_features=30, cost=400),
|
|
43
|
+
'configurations': OperationCapability(max_features=30, cost=400),
|
|
44
|
+
'commonality': OperationCapability(max_features=30, cost=400),
|
|
45
|
+
},
|
|
46
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from itertools import chain
|
|
2
|
+
from typing import Iterable, Iterator, Optional
|
|
3
|
+
|
|
4
|
+
from pysat.formula import CNF
|
|
5
|
+
from pysat.solvers import Solver
|
|
6
|
+
|
|
7
|
+
from flamapy.core.exceptions import FlamaException
|
|
8
|
+
from flamapy.core.models import VariabilityModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PySATModel(VariabilityModel):
|
|
12
|
+
@staticmethod
|
|
13
|
+
def get_extension() -> str:
|
|
14
|
+
return 'pysat'
|
|
15
|
+
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
self._cnf = CNF()
|
|
18
|
+
self.variables: dict[str, int] = {} # feature's name -> id
|
|
19
|
+
self.features: dict[int, str] = {} # id -> feature's name
|
|
20
|
+
# Auxiliary (Tseytin) variable ids that are not features. They must be excluded
|
|
21
|
+
# from feature enumeration/counting; models over the features are unaffected.
|
|
22
|
+
self.auxiliary_variables: set[int] = set()
|
|
23
|
+
# The ClauseSet encoding the clauses come from ('auto', 'distributive' or 'tseytin'),
|
|
24
|
+
# or None when flamapy did not encode them (e.g. a model read from DIMACS). Operations
|
|
25
|
+
# that re-encode the feature model use it to keep the same encoding.
|
|
26
|
+
self.cnf_method: Optional[str] = None
|
|
27
|
+
self.original_model: VariabilityModel
|
|
28
|
+
|
|
29
|
+
def add_clause(self, clause: list[int]) -> None:
|
|
30
|
+
self._cnf.append(clause)
|
|
31
|
+
|
|
32
|
+
def get_variable(self, key: str) -> int:
|
|
33
|
+
if key not in self.variables:
|
|
34
|
+
raise FlamaException(f'Feature {key} not found')
|
|
35
|
+
return self.variables[key]
|
|
36
|
+
|
|
37
|
+
def get_all_clauses(self) -> CNF:
|
|
38
|
+
return self._cnf
|
|
39
|
+
|
|
40
|
+
# ---- feature projection helpers -------------------------------------------------------
|
|
41
|
+
# With cnf_method 'tseytin'/'auto' the CNF also contains auxiliary variables. Results are
|
|
42
|
+
# always reported over the features only; these helpers keep that in one place.
|
|
43
|
+
|
|
44
|
+
def is_feature(self, variable: int) -> bool:
|
|
45
|
+
"""Whether a variable (or literal) is a feature, i.e. not an auxiliary variable."""
|
|
46
|
+
return abs(variable) in self.features
|
|
47
|
+
|
|
48
|
+
def is_auxiliary(self, variable: int) -> bool:
|
|
49
|
+
"""Whether a variable (or literal) is an auxiliary (Tseytin) variable."""
|
|
50
|
+
return abs(variable) in self.auxiliary_variables
|
|
51
|
+
|
|
52
|
+
def feature_variables(self) -> list[int]:
|
|
53
|
+
"""The ids of the feature variables, in increasing order."""
|
|
54
|
+
return sorted(self.features)
|
|
55
|
+
|
|
56
|
+
def top_variable(self) -> int:
|
|
57
|
+
"""The highest variable id used by the encoding (features and auxiliary variables).
|
|
58
|
+
|
|
59
|
+
New variables (e.g. diagnosis assumption selectors) must start above it.
|
|
60
|
+
"""
|
|
61
|
+
return max(chain(self.features, self.variables.values(), self.auxiliary_variables),
|
|
62
|
+
default=0)
|
|
63
|
+
|
|
64
|
+
def selected_features(self, assignment: Iterable[int]) -> list[str]:
|
|
65
|
+
"""Names of the features set to true in a solver assignment, in assignment order.
|
|
66
|
+
|
|
67
|
+
Auxiliary variables are skipped.
|
|
68
|
+
"""
|
|
69
|
+
return [self.features[literal] for literal in assignment
|
|
70
|
+
if literal > 0 and literal in self.features]
|
|
71
|
+
|
|
72
|
+
def enum_feature_models(self, solver: Solver,
|
|
73
|
+
assumptions: Iterable[int] = ()) -> Iterator[list[int]]:
|
|
74
|
+
"""Enumerate the solver's models, one per distinct assignment of the features.
|
|
75
|
+
|
|
76
|
+
``solver`` must already hold this model's clauses. Without auxiliary variables this is
|
|
77
|
+
exactly ``solver.enum_models``. With them, each model found is blocked on its feature
|
|
78
|
+
literals only, so models that differ only in auxiliary variables count once. (The
|
|
79
|
+
Tseytin gates of ClauseSet are biconditionals, which already makes the auxiliary
|
|
80
|
+
assignment unique, but the operations do not have to rely on that.) Like
|
|
81
|
+
``enum_models``, this adds the blocking clauses to ``solver``.
|
|
82
|
+
"""
|
|
83
|
+
assumptions = list(assumptions)
|
|
84
|
+
if not self.auxiliary_variables:
|
|
85
|
+
yield from solver.enum_models(assumptions=assumptions)
|
|
86
|
+
return
|
|
87
|
+
feature_variables = self.feature_variables()
|
|
88
|
+
while solver.solve(assumptions=assumptions):
|
|
89
|
+
model = solver.get_model()
|
|
90
|
+
yield model
|
|
91
|
+
true_literals = set(model)
|
|
92
|
+
solver.add_clause([-variable if variable in true_literals else variable
|
|
93
|
+
for variable in feature_variables])
|
|
94
|
+
|
|
95
|
+
def __eq__(self, other: object) -> bool:
|
|
96
|
+
if other is None or not isinstance(other, PySATModel):
|
|
97
|
+
return False
|
|
98
|
+
return set(self.get_all_clauses().clauses) == set(other.get_all_clauses().clauses)
|
|
99
|
+
|
|
100
|
+
def __hash__(self) -> int:
|
|
101
|
+
clause_tuples = (tuple(clause) for clause in self.get_all_clauses().clauses)
|
|
102
|
+
return hash(frozenset(clause_tuples))
|
|
@@ -3,6 +3,7 @@ from .pysat_satisfiable_configuration import PySATSatisfiableConfiguration
|
|
|
3
3
|
from .pysat_configurations import PySATConfigurations
|
|
4
4
|
from .pysat_configurations_number import PySATConfigurationsNumber
|
|
5
5
|
from .pysat_commonality import PySATCommonality
|
|
6
|
+
from .pysat_atomic_sets import PySATAtomicSets
|
|
6
7
|
from .pysat_filter import PySATFilter
|
|
7
8
|
from .pysat_backbone import PySATBackbone
|
|
8
9
|
from .pysat_core_features import PySATCoreFeatures
|
|
@@ -12,20 +13,31 @@ from .pysat_metrics import PySATMetrics
|
|
|
12
13
|
from .pysat_attribute_optimization import PySATAttributeOptimization
|
|
13
14
|
from .pysat_minimum_configuration import PySATMinimumConfiguration
|
|
14
15
|
from .pysat_twise_sampling import PySATTWiseSampling
|
|
16
|
+
from .pysat_sampling import PySATSampling
|
|
17
|
+
from .pysat_testing import (
|
|
18
|
+
PySATCoverage,
|
|
19
|
+
PySATCoveringArray,
|
|
20
|
+
PySATSampleReduction,
|
|
21
|
+
)
|
|
15
22
|
|
|
16
23
|
|
|
17
24
|
__all__ = [
|
|
25
|
+
'PySATAtomicSets',
|
|
18
26
|
'PySATAttributeOptimization',
|
|
19
27
|
'PySATBackbone',
|
|
20
28
|
'PySATCommonality',
|
|
21
29
|
'PySATConfigurations',
|
|
22
30
|
'PySATConfigurationsNumber',
|
|
23
31
|
'PySATCoreFeatures',
|
|
32
|
+
'PySATCoverage',
|
|
33
|
+
'PySATCoveringArray',
|
|
24
34
|
'PySATDeadFeatures',
|
|
25
35
|
'PySATFalseOptionalFeatures',
|
|
26
36
|
'PySATFilter',
|
|
27
37
|
'PySATMetrics',
|
|
28
38
|
'PySATMinimumConfiguration',
|
|
39
|
+
'PySATSampleReduction',
|
|
40
|
+
'PySATSampling',
|
|
29
41
|
'PySATSatisfiable',
|
|
30
42
|
'PySATSatisfiableConfiguration',
|
|
31
43
|
'PySATTWiseSampling',
|