flamapy-sat 2.5.0.dev0__tar.gz → 2.6.0__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.5.0.dev0/flamapy_sat.egg-info → flamapy_sat-2.6.0}/PKG-INFO +14 -15
- flamapy_sat-2.6.0/README.md +15 -0
- flamapy_sat-2.6.0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/__init__.py +20 -0
- flamapy_sat-2.6.0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_configuration_diagnosis.py +205 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_conflict.py +23 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_diagnosis.py +24 -0
- flamapy_sat-2.6.0/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/fm_to_diag_pysat.py +37 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/models/pysat_model.py +3 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/__init__.py +8 -0
- flamapy_sat-2.6.0/flamapy/metamodels/pysat_metamodel/operations/pysat_attribute_optimization.py +127 -0
- flamapy_sat-2.6.0/flamapy/metamodels/pysat_metamodel/operations/pysat_backbone.py +83 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_commonality.py +15 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations.py +3 -1
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations_number.py +3 -0
- flamapy_sat-2.6.0/flamapy/metamodels/pysat_metamodel/operations/pysat_core_features.py +24 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_dead_features.py +3 -13
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_false_optional_features.py +5 -5
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_filter.py +15 -0
- flamapy_sat-2.6.0/flamapy/metamodels/pysat_metamodel/operations/pysat_minimum_configuration.py +62 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_sampling.py +28 -3
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable_configuration.py +9 -10
- flamapy_sat-2.6.0/flamapy/metamodels/pysat_metamodel/operations/pysat_twise_sampling.py +102 -0
- flamapy_sat-2.6.0/flamapy/metamodels/pysat_metamodel/transformations/fm_to_pysat.py +37 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0/flamapy_sat.egg-info}/PKG-INFO +14 -15
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy_sat.egg-info/SOURCES.txt +5 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy_sat.egg-info/requires.txt +2 -2
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/pyproject.toml +5 -5
- flamapy_sat-2.5.0.dev0/README.md +0 -16
- flamapy_sat-2.5.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/__init__.py +0 -12
- flamapy_sat-2.5.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/fm_to_diag_pysat.py +0 -73
- flamapy_sat-2.5.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_core_features.py +0 -35
- flamapy_sat-2.5.0.dev0/flamapy/metamodels/pysat_metamodel/transformations/fm_to_pysat.py +0 -171
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/LICENSE +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/__init__.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/models/__init__.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/models/pysat_diagnosis_model.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/__init__.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/checker.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/fastdiag.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/__init__.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/hsdag.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/__init__.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/fastdiag_labeler.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/labeler.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/quickxplain_labeler.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/node.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/quickxplain.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/utils.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_abstract_identifier.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/__init__.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/__init__.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/models/__init__.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/models/txtcnf_model.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_metrics.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/transformations/__init__.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/transformations/cnf_to_pysat.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_reader.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_writer.py +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy_sat.egg-info/dependency_links.txt +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy_sat.egg-info/top_level.txt +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/setup.cfg +0 -0
- {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/setup.py +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flamapy-sat
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
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
|
|
7
7
|
Project-URL: Homepage, https://github.com/flamapy/pysat_metamodel
|
|
8
|
-
Requires-Python: >=3.
|
|
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.6.0
|
|
12
|
+
Requires-Dist: flamapy-fm~=2.6.0
|
|
13
13
|
Requires-Dist: python-sat~=0.1.7.dev1
|
|
14
14
|
Provides-Extra: dev
|
|
15
15
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -19,19 +19,18 @@ Requires-Dist: mypy; extra == "dev"
|
|
|
19
19
|
Requires-Dist: coverage; extra == "dev"
|
|
20
20
|
Dynamic: license-file
|
|
21
21
|
|
|
22
|
-
#
|
|
22
|
+
# flamapy-sat
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
SAT-based analysis for [flamapy](https://flamapy.org) feature models (PySAT
|
|
25
|
+
backend, giving access to more than ten SAT solvers). Also ships the diagnosis
|
|
26
|
+
metamodel (conflict detection and diagnosis).
|
|
25
27
|
|
|
28
|
+
**Documentation:**
|
|
29
|
+
- SAT plugin: https://docs.flamapy.org/framework/plugins/sat_plugin
|
|
30
|
+
- Diagnosis plugin: https://docs.flamapy.org/framework/plugins/diagnosis_plugin
|
|
26
31
|
|
|
27
|
-
##
|
|
32
|
+
## Installation
|
|
28
33
|
|
|
29
|
-
```
|
|
30
|
-
pip install -
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Make sure that you have installed python-dev
|
|
34
|
-
|
|
35
|
-
``` bash
|
|
36
|
-
sudo apt install python-dev #python3-dev in Ubuntu derivatives
|
|
34
|
+
```bash
|
|
35
|
+
pip install flamapy-sat
|
|
37
36
|
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# flamapy-sat
|
|
2
|
+
|
|
3
|
+
SAT-based analysis for [flamapy](https://flamapy.org) feature models (PySAT
|
|
4
|
+
backend, giving access to more than ten SAT solvers). Also ships the diagnosis
|
|
5
|
+
metamodel (conflict detection and diagnosis).
|
|
6
|
+
|
|
7
|
+
**Documentation:**
|
|
8
|
+
- SAT plugin: https://docs.flamapy.org/framework/plugins/sat_plugin
|
|
9
|
+
- Diagnosis plugin: https://docs.flamapy.org/framework/plugins/diagnosis_plugin
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install flamapy-sat
|
|
15
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# pylint: disable=cyclic-import
|
|
2
|
+
|
|
3
|
+
from .pysat_abstract_identifier import PySATAbstractIdentifier
|
|
4
|
+
from .pysat_conflict import PySATConflict
|
|
5
|
+
from .pysat_diagnosis import PySATDiagnosis
|
|
6
|
+
from .pysat_configuration_diagnosis import (
|
|
7
|
+
PySATConfigurationConflict,
|
|
8
|
+
PySATConfigurationRepair,
|
|
9
|
+
PySATFeatureExplanation,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'PySATAbstractIdentifier',
|
|
15
|
+
'PySATConfigurationConflict',
|
|
16
|
+
'PySATConfigurationRepair',
|
|
17
|
+
'PySATConflict',
|
|
18
|
+
'PySATDiagnosis',
|
|
19
|
+
'PySATFeatureExplanation',
|
|
20
|
+
]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Diagnosis-powered operations for interactive configuration.
|
|
2
|
+
|
|
3
|
+
These turn the low-level QuickXPlain/FastDiag machinery into configuration-level answers
|
|
4
|
+
that a configurator can surface directly:
|
|
5
|
+
|
|
6
|
+
- ``PySATConfigurationConflict``: a minimal set of user decisions that together with the
|
|
7
|
+
model are inconsistent ("these choices clash").
|
|
8
|
+
- ``PySATConfigurationRepair``: a minimal set of decisions to retract to restore
|
|
9
|
+
consistency ("undo one of these to continue").
|
|
10
|
+
- ``PySATFeatureExplanation``: for a feature that is forced by the current decisions, the
|
|
11
|
+
minimal set of decisions responsible ("this feature is on because of these choices").
|
|
12
|
+
|
|
13
|
+
Each consumes a ``PySATModel`` (the feature-model CNF) and a partial ``Configuration``.
|
|
14
|
+
Decisions are returned as ``(feature_name, selected)`` pairs.
|
|
15
|
+
"""
|
|
16
|
+
from typing import Optional, cast
|
|
17
|
+
|
|
18
|
+
from flamapy.core.models import VariabilityModel
|
|
19
|
+
from flamapy.core.operations import Operation
|
|
20
|
+
from flamapy.core.operations.descriptor import OperationDescriptor, Input
|
|
21
|
+
from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
|
|
22
|
+
from flamapy.metamodels.pysat_metamodel.models.pysat_model import PySATModel
|
|
23
|
+
|
|
24
|
+
from .diagnosis.checker import ConsistencyChecker
|
|
25
|
+
from .diagnosis.quickxplain import QuickXPlain
|
|
26
|
+
from .diagnosis.fastdiag import FastDiag
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Decision = tuple[str, bool]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _clauses_and_decisions(
|
|
33
|
+
model: PySATModel, configuration: Configuration
|
|
34
|
+
) -> tuple[list[list[int]], list[tuple[int, str, bool]]]:
|
|
35
|
+
clauses = [list(clause) for clause in model.get_all_clauses().clauses]
|
|
36
|
+
decisions: list[tuple[int, str, bool]] = []
|
|
37
|
+
for name, value in configuration.elements.items():
|
|
38
|
+
variable = model.variables.get(name)
|
|
39
|
+
if variable is None:
|
|
40
|
+
continue
|
|
41
|
+
selected = bool(value)
|
|
42
|
+
decisions.append((variable if selected else -variable, name, selected))
|
|
43
|
+
return clauses, decisions
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _decisions_from_literals(
|
|
47
|
+
literals: list[int], decisions: list[tuple[int, str, bool]]
|
|
48
|
+
) -> list[Decision]:
|
|
49
|
+
by_literal = {literal: (name, value) for literal, name, value in decisions}
|
|
50
|
+
return [by_literal[literal] for literal in literals if literal in by_literal]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PySATConfigurationConflict(Operation):
|
|
54
|
+
"""Minimal subset of the user's decisions that is inconsistent with the model."""
|
|
55
|
+
|
|
56
|
+
facade = OperationDescriptor(
|
|
57
|
+
doc=(
|
|
58
|
+
'Returns a minimal subset of the configuration decisions that is inconsistent with\n'
|
|
59
|
+
'the feature model — the conflict explaining why the (partial) configuration cannot\n'
|
|
60
|
+
'be completed. Requires the pysat_diagnosis plugin.'
|
|
61
|
+
),
|
|
62
|
+
returns='Union[None, List[Any]]',
|
|
63
|
+
name='configuration_conflict', operation='PySATConfigurationConflict',
|
|
64
|
+
default_backend='pysat_diagnosis',
|
|
65
|
+
inputs=(Input('configuration_path', str, required=True, kind='configuration',
|
|
66
|
+
setter='set_configuration'),),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def __init__(self) -> None:
|
|
70
|
+
self._configuration: Optional[Configuration] = None
|
|
71
|
+
self._result: list[Decision] = []
|
|
72
|
+
|
|
73
|
+
def set_configuration(self, configuration: Configuration) -> None:
|
|
74
|
+
self._configuration = configuration
|
|
75
|
+
|
|
76
|
+
def get_conflict(self) -> list[Decision]:
|
|
77
|
+
return self._result
|
|
78
|
+
|
|
79
|
+
def get_result(self) -> list[Decision]:
|
|
80
|
+
return self._result
|
|
81
|
+
|
|
82
|
+
def execute(self, model: VariabilityModel) -> 'PySATConfigurationConflict':
|
|
83
|
+
sat_model = cast(PySATModel, model)
|
|
84
|
+
clauses, decisions = _clauses_and_decisions(sat_model, self._configuration)
|
|
85
|
+
checker = ConsistencyChecker('glucose3', clauses)
|
|
86
|
+
conflict = QuickXPlain(checker).find_conflict([lit for lit, _, _ in decisions], [])
|
|
87
|
+
checker.delete()
|
|
88
|
+
self._result = _decisions_from_literals(conflict, decisions)
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class PySATConfigurationRepair(Operation):
|
|
93
|
+
"""Minimal subset of the user's decisions to retract to restore consistency."""
|
|
94
|
+
|
|
95
|
+
facade = OperationDescriptor(
|
|
96
|
+
doc=(
|
|
97
|
+
'Returns a minimal subset of the configuration decisions to retract in order to\n'
|
|
98
|
+
'restore consistency with the feature model — how to repair an over-constrained\n'
|
|
99
|
+
'configuration. Requires the pysat_diagnosis plugin.'
|
|
100
|
+
),
|
|
101
|
+
returns='Union[None, List[Any]]',
|
|
102
|
+
name='configuration_repair', operation='PySATConfigurationRepair',
|
|
103
|
+
default_backend='pysat_diagnosis',
|
|
104
|
+
inputs=(Input('configuration_path', str, required=True, kind='configuration',
|
|
105
|
+
setter='set_configuration'),),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def __init__(self) -> None:
|
|
109
|
+
self._configuration: Optional[Configuration] = None
|
|
110
|
+
self._result: list[Decision] = []
|
|
111
|
+
|
|
112
|
+
def set_configuration(self, configuration: Configuration) -> None:
|
|
113
|
+
self._configuration = configuration
|
|
114
|
+
|
|
115
|
+
def get_repair(self) -> list[Decision]:
|
|
116
|
+
return self._result
|
|
117
|
+
|
|
118
|
+
def get_result(self) -> list[Decision]:
|
|
119
|
+
return self._result
|
|
120
|
+
|
|
121
|
+
def execute(self, model: VariabilityModel) -> 'PySATConfigurationRepair':
|
|
122
|
+
sat_model = cast(PySATModel, model)
|
|
123
|
+
clauses, decisions = _clauses_and_decisions(sat_model, self._configuration)
|
|
124
|
+
checker = ConsistencyChecker('glucose3', clauses)
|
|
125
|
+
diagnosis = FastDiag(checker).find_diagnosis([lit for lit, _, _ in decisions], [])
|
|
126
|
+
checker.delete()
|
|
127
|
+
self._result = _decisions_from_literals(diagnosis, decisions)
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class PySATFeatureExplanation(Operation):
|
|
132
|
+
"""Explain why a feature is forced by the current decisions.
|
|
133
|
+
|
|
134
|
+
``get_forced_value()`` is the value the feature is forced to (or ``None`` if it is not
|
|
135
|
+
forced), and ``get_result()`` is the minimal set of decisions responsible.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
facade = OperationDescriptor(
|
|
139
|
+
doc=(
|
|
140
|
+
'Explains why a feature is forced (selected or deselected) by the current\n'
|
|
141
|
+
'configuration: returns the minimal set of decisions responsible. ``feature_name``\n'
|
|
142
|
+
'is the feature to explain. Requires the pysat_diagnosis plugin.'
|
|
143
|
+
),
|
|
144
|
+
returns='Union[None, List[Any]]',
|
|
145
|
+
name='feature_explanation', operation='PySATFeatureExplanation',
|
|
146
|
+
default_backend='pysat_diagnosis',
|
|
147
|
+
inputs=(
|
|
148
|
+
Input('configuration_path', str, required=True, kind='configuration',
|
|
149
|
+
setter='set_configuration'),
|
|
150
|
+
Input('feature_name', str, required=True, setter='set_feature'),
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def __init__(self) -> None:
|
|
155
|
+
self._configuration: Optional[Configuration] = None
|
|
156
|
+
self._feature: Optional[str] = None
|
|
157
|
+
self._forced_value: Optional[bool] = None
|
|
158
|
+
self._result: list[Decision] = []
|
|
159
|
+
|
|
160
|
+
def set_configuration(self, configuration: Configuration) -> None:
|
|
161
|
+
self._configuration = configuration
|
|
162
|
+
|
|
163
|
+
def set_feature(self, feature_name: str) -> None:
|
|
164
|
+
self._feature = feature_name
|
|
165
|
+
|
|
166
|
+
def get_forced_value(self) -> Optional[bool]:
|
|
167
|
+
return self._forced_value
|
|
168
|
+
|
|
169
|
+
def get_explanation(self) -> list[Decision]:
|
|
170
|
+
return self._result
|
|
171
|
+
|
|
172
|
+
def get_result(self) -> list[Decision]:
|
|
173
|
+
return self._result
|
|
174
|
+
|
|
175
|
+
def execute(self, model: VariabilityModel) -> 'PySATFeatureExplanation':
|
|
176
|
+
sat_model = cast(PySATModel, model)
|
|
177
|
+
self._forced_value = None
|
|
178
|
+
self._result = []
|
|
179
|
+
if self._feature is None:
|
|
180
|
+
return self
|
|
181
|
+
clauses, decisions = _clauses_and_decisions(sat_model, self._configuration)
|
|
182
|
+
variable = sat_model.variables.get(self._feature)
|
|
183
|
+
if variable is None:
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
literals = [lit for lit, _, _ in decisions]
|
|
187
|
+
checker = ConsistencyChecker('glucose3', clauses)
|
|
188
|
+
can_be_true = checker.is_consistent([*literals, variable], [])
|
|
189
|
+
can_be_false = checker.is_consistent([*literals, -variable], [])
|
|
190
|
+
if can_be_true and not can_be_false:
|
|
191
|
+
self._forced_value = True
|
|
192
|
+
forbidden = -variable
|
|
193
|
+
elif can_be_false and not can_be_true:
|
|
194
|
+
self._forced_value = False
|
|
195
|
+
forbidden = variable
|
|
196
|
+
else:
|
|
197
|
+
checker.delete() # not forced (or already contradictory) -> no explanation
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
# The minimal decisions that, with the model, entail the forced value are those
|
|
201
|
+
# inconsistent with the model plus the opposite assignment.
|
|
202
|
+
explanation = QuickXPlain(checker).find_conflict(literals, [forbidden])
|
|
203
|
+
checker.delete()
|
|
204
|
+
self._result = _decisions_from_literals(explanation, decisions)
|
|
205
|
+
return self
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Tuple
|
|
2
2
|
|
|
3
3
|
from flamapy.metamodels.pysat_diagnosis_metamodel.models import DiagnosisModel
|
|
4
|
+
from flamapy.core.operations.descriptor import OperationDescriptor, Input
|
|
4
5
|
from . import PySATAbstractIdentifier
|
|
5
6
|
from .diagnosis.checker import ConsistencyChecker
|
|
6
7
|
from .diagnosis.hsdag.hsdag import HSDAG
|
|
@@ -18,6 +19,28 @@ class PySATConflict(PySATAbstractIdentifier):
|
|
|
18
19
|
- max_depth - specify the maximum depth of the HSDAG to be computed
|
|
19
20
|
"""
|
|
20
21
|
|
|
22
|
+
facade = OperationDescriptor(
|
|
23
|
+
doc=(
|
|
24
|
+
'Returns a list of conflict sets: minimal subsets of the model constraints that\n'
|
|
25
|
+
'are inconsistent with the given configuration. Requires the pysat_diagnosis\n'
|
|
26
|
+
'plugin.\n'
|
|
27
|
+
'\n'
|
|
28
|
+
'``configuration_path`` accepts a configuration file path, a ``{feature:\n'
|
|
29
|
+
'value}`` mapping, or a Configuration object. ``test_case_path`` is optional\n'
|
|
30
|
+
'and, when given, looks for conflicts against that expected outcome instead;\n'
|
|
31
|
+
'same input types.'
|
|
32
|
+
),
|
|
33
|
+
returns='Union[None, List[str]]',
|
|
34
|
+
name='conflict', operation='PySATConflict', default_backend='pysat_diagnosis',
|
|
35
|
+
inputs=(
|
|
36
|
+
Input('configuration_path', str, required=True, kind='configuration',
|
|
37
|
+
setter='set_configuration'),
|
|
38
|
+
Input('test_case_path', str, default=None, kind='configuration',
|
|
39
|
+
setter='set_test_case'),
|
|
40
|
+
Input('max_conflicts', int, default=None, setter='set_max_conflicts'),
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
21
44
|
def __init__(self) -> None:
|
|
22
45
|
super().__init__()
|
|
23
46
|
self.max_conflicts = -1 # -1 means no limit
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Tuple
|
|
2
2
|
|
|
3
3
|
from flamapy.metamodels.pysat_diagnosis_metamodel.models import DiagnosisModel
|
|
4
|
+
from flamapy.core.operations.descriptor import OperationDescriptor, Input
|
|
4
5
|
from . import PySATAbstractIdentifier
|
|
5
6
|
from .diagnosis.checker import ConsistencyChecker
|
|
6
7
|
from .diagnosis.hsdag.hsdag import HSDAG
|
|
@@ -18,6 +19,29 @@ class PySATDiagnosis(PySATAbstractIdentifier):
|
|
|
18
19
|
- max_depth - specify the maximum depth of the HSDAG to be computed
|
|
19
20
|
"""
|
|
20
21
|
|
|
22
|
+
facade = OperationDescriptor(
|
|
23
|
+
doc=(
|
|
24
|
+
'Returns a list of diagnoses explaining why a configuration is not valid\n'
|
|
25
|
+
'against the feature model: each diagnosis is a minimal set of constraints\n'
|
|
26
|
+
'whose removal would resolve the inconsistency. Requires the pysat_diagnosis\n'
|
|
27
|
+
'plugin.\n'
|
|
28
|
+
'\n'
|
|
29
|
+
'``configuration_path`` accepts a configuration file path, a ``{feature:\n'
|
|
30
|
+
'value}`` mapping, or a Configuration object. ``test_case_path`` is optional\n'
|
|
31
|
+
'and, when given, diagnoses against that expected outcome instead; it accepts\n'
|
|
32
|
+
'the same input types.'
|
|
33
|
+
),
|
|
34
|
+
returns='Union[None, List[str]]',
|
|
35
|
+
name='diagnosis', operation='PySATDiagnosis', default_backend='pysat_diagnosis',
|
|
36
|
+
inputs=(
|
|
37
|
+
Input('configuration_path', str, required=True, kind='configuration',
|
|
38
|
+
setter='set_configuration'),
|
|
39
|
+
Input('test_case_path', str, default=None, kind='configuration',
|
|
40
|
+
setter='set_test_case'),
|
|
41
|
+
Input('max_diagnoses', int, default=None, setter='set_max_diagnoses'),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
21
45
|
def __init__(self) -> None:
|
|
22
46
|
super().__init__()
|
|
23
47
|
self.max_diagnoses = -1 # -1 means no limit
|
flamapy_sat-2.6.0/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/fm_to_diag_pysat.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from flamapy.metamodels.fm_metamodel.models import FeatureModel, ClauseSet
|
|
2
|
+
from flamapy.metamodels.pysat_metamodel.transformations.fm_to_pysat import FmToPysat
|
|
3
|
+
from ..models.pysat_diagnosis_model import DiagnosisModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FmToDiagPysat(FmToPysat):
|
|
7
|
+
"""Like FmToPysat, but into a DiagnosisModel that also keeps a clause → constraint map.
|
|
8
|
+
|
|
9
|
+
The CNF and its per-source clause grouping both come from the shared feature-model encoder
|
|
10
|
+
(``ClauseSet``); this transformation just wraps them into the diagnosis model.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def get_source_extension() -> str:
|
|
15
|
+
return 'fm'
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def get_destination_extension() -> str:
|
|
19
|
+
return 'pysat_diagnosis'
|
|
20
|
+
|
|
21
|
+
def __init__(self, source_model: FeatureModel, cnf_method: str = 'distributive') -> None:
|
|
22
|
+
super().__init__(source_model, cnf_method)
|
|
23
|
+
self.destination_model = DiagnosisModel()
|
|
24
|
+
|
|
25
|
+
def transform(self) -> DiagnosisModel:
|
|
26
|
+
clause_set = ClauseSet.from_feature_model(self.source_model, cnf_method=self.cnf_method)
|
|
27
|
+
model = self.destination_model
|
|
28
|
+
for clause in clause_set.clauses:
|
|
29
|
+
model.add_clause(clause)
|
|
30
|
+
model.variables = dict(clause_set.variables)
|
|
31
|
+
model.features = dict(clause_set.features)
|
|
32
|
+
model.auxiliary_variables = set(clause_set.auxiliary_variables)
|
|
33
|
+
model.original_model = self.source_model
|
|
34
|
+
# Clause → constraint mapping (root feature / relations / cross-tree constraints).
|
|
35
|
+
for description, clauses in clause_set.clause_groups:
|
|
36
|
+
model.add_clause_to_map(description, clauses)
|
|
37
|
+
return model
|
|
@@ -13,6 +13,9 @@ class PySATModel(VariabilityModel):
|
|
|
13
13
|
self._cnf = CNF()
|
|
14
14
|
self.variables: dict[str, int] = {} # feature's name -> id
|
|
15
15
|
self.features: dict[int, str] = {} # id -> feature's name
|
|
16
|
+
# Auxiliary (Tseytin) variable ids that are not features. They must be excluded
|
|
17
|
+
# from feature enumeration/counting; models over the features are unaffected.
|
|
18
|
+
self.auxiliary_variables: set[int] = set()
|
|
16
19
|
self.original_model: VariabilityModel
|
|
17
20
|
|
|
18
21
|
def add_clause(self, clause: list[int]) -> None:
|
|
@@ -4,13 +4,19 @@ from .pysat_configurations import PySATConfigurations
|
|
|
4
4
|
from .pysat_configurations_number import PySATConfigurationsNumber
|
|
5
5
|
from .pysat_commonality import PySATCommonality
|
|
6
6
|
from .pysat_filter import PySATFilter
|
|
7
|
+
from .pysat_backbone import PySATBackbone
|
|
7
8
|
from .pysat_core_features import PySATCoreFeatures
|
|
8
9
|
from .pysat_dead_features import PySATDeadFeatures
|
|
9
10
|
from .pysat_false_optional_features import PySATFalseOptionalFeatures
|
|
10
11
|
from .pysat_metrics import PySATMetrics
|
|
12
|
+
from .pysat_attribute_optimization import PySATAttributeOptimization
|
|
13
|
+
from .pysat_minimum_configuration import PySATMinimumConfiguration
|
|
14
|
+
from .pysat_twise_sampling import PySATTWiseSampling
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
__all__ = [
|
|
18
|
+
'PySATAttributeOptimization',
|
|
19
|
+
'PySATBackbone',
|
|
14
20
|
'PySATCommonality',
|
|
15
21
|
'PySATConfigurations',
|
|
16
22
|
'PySATConfigurationsNumber',
|
|
@@ -19,6 +25,8 @@ __all__ = [
|
|
|
19
25
|
'PySATFalseOptionalFeatures',
|
|
20
26
|
'PySATFilter',
|
|
21
27
|
'PySATMetrics',
|
|
28
|
+
'PySATMinimumConfiguration',
|
|
22
29
|
'PySATSatisfiable',
|
|
23
30
|
'PySATSatisfiableConfiguration',
|
|
31
|
+
'PySATTWiseSampling',
|
|
24
32
|
]
|
flamapy_sat-2.6.0/flamapy/metamodels/pysat_metamodel/operations/pysat_attribute_optimization.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from typing import Any, Optional, cast
|
|
2
|
+
|
|
3
|
+
from pysat.formula import WCNF
|
|
4
|
+
from pysat.examples.rc2 import RC2
|
|
5
|
+
|
|
6
|
+
from flamapy.core.models import VariabilityModel
|
|
7
|
+
from flamapy.core.operations import AttributeOptimization, OptimizationGoal
|
|
8
|
+
from flamapy.core.exceptions import FlamaException
|
|
9
|
+
from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
|
|
10
|
+
from flamapy.metamodels.fm_metamodel.models.feature_model import FeatureModel
|
|
11
|
+
from flamapy.metamodels.pysat_metamodel.models.pysat_model import PySATModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PySATAttributeOptimization(AttributeOptimization):
|
|
15
|
+
"""Optimal configuration for a single numeric feature attribute, via MaxSAT (RC2).
|
|
16
|
+
|
|
17
|
+
The objective is the sum of the attribute value over the selected features. The SAT
|
|
18
|
+
clauses are the hard constraints; per-feature attribute values become weighted soft
|
|
19
|
+
clauses so RC2 returns a valid configuration with the optimal objective.
|
|
20
|
+
|
|
21
|
+
Multi-objective/Pareto optimization is not supported here — use the z3 backend for that.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
self._attributes: dict[str, OptimizationGoal] = {}
|
|
26
|
+
self._result: list[Configuration] = []
|
|
27
|
+
self._optimum: dict[str, float] = {}
|
|
28
|
+
|
|
29
|
+
def set_attributes(self, attributes: dict[str, OptimizationGoal]) -> None:
|
|
30
|
+
self._attributes = attributes
|
|
31
|
+
|
|
32
|
+
def optimize(self) -> list[Configuration]:
|
|
33
|
+
return self.get_result()
|
|
34
|
+
|
|
35
|
+
def get_result(self) -> list[Configuration]:
|
|
36
|
+
return self._result
|
|
37
|
+
|
|
38
|
+
def get_optimum(self) -> dict[str, float]:
|
|
39
|
+
"""The optimal objective value per attribute, e.g. ``{'Cost': 12.0}``."""
|
|
40
|
+
return self._optimum
|
|
41
|
+
|
|
42
|
+
def execute(self, model: VariabilityModel) -> 'PySATAttributeOptimization':
|
|
43
|
+
sat_model = cast(PySATModel, model)
|
|
44
|
+
if len(self._attributes) != 1:
|
|
45
|
+
raise FlamaException(
|
|
46
|
+
'PySATAttributeOptimization optimizes a single attribute; use the z3 '
|
|
47
|
+
'backend for multi-objective/Pareto optimization.'
|
|
48
|
+
)
|
|
49
|
+
(attr_name, goal), = self._attributes.items()
|
|
50
|
+
feature_model = getattr(sat_model, 'original_model', None)
|
|
51
|
+
if feature_model is None:
|
|
52
|
+
raise FlamaException(
|
|
53
|
+
'The SAT model has no attached feature model to read attributes from.'
|
|
54
|
+
)
|
|
55
|
+
weights = _feature_weights(cast(FeatureModel, feature_model), attr_name)
|
|
56
|
+
config, optimum = _optimize(sat_model, weights, goal)
|
|
57
|
+
if config is None or optimum is None:
|
|
58
|
+
self._result = []
|
|
59
|
+
self._optimum = {}
|
|
60
|
+
else:
|
|
61
|
+
self._result = [config]
|
|
62
|
+
self._optimum = {attr_name: optimum}
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _feature_weights(feature_model: FeatureModel, attribute_name: str) -> dict[str, float]:
|
|
67
|
+
"""Map each feature carrying the given numeric attribute to its value."""
|
|
68
|
+
weights: dict[str, float] = {}
|
|
69
|
+
for feature in feature_model.get_features():
|
|
70
|
+
for attribute in feature.get_attributes():
|
|
71
|
+
value = attribute.default_value
|
|
72
|
+
if attribute.name == attribute_name and isinstance(value, (int, float)) \
|
|
73
|
+
and not isinstance(value, bool):
|
|
74
|
+
weights[feature.name] = float(value)
|
|
75
|
+
return weights
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _integer_scale(values: list[float]) -> int:
|
|
79
|
+
"""Smallest power of ten that turns all values into integers (capped at 1e6)."""
|
|
80
|
+
max_decimals = 0
|
|
81
|
+
for value in values:
|
|
82
|
+
text = f"{value:.6f}".rstrip('0')
|
|
83
|
+
if '.' in text:
|
|
84
|
+
max_decimals = max(max_decimals, len(text.split('.')[1]))
|
|
85
|
+
return 10 ** max_decimals
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _optimize(sat_model: PySATModel,
|
|
89
|
+
weights: dict[str, float],
|
|
90
|
+
goal: OptimizationGoal) -> tuple[Optional[Configuration], Optional[float]]:
|
|
91
|
+
# Maximizing an objective is minimizing its negation.
|
|
92
|
+
sign = 1.0 if goal == OptimizationGoal.MINIMIZE else -1.0
|
|
93
|
+
effective = {name: sign * weight for name, weight in weights.items()}
|
|
94
|
+
scale = _integer_scale(list(effective.values()))
|
|
95
|
+
|
|
96
|
+
wcnf = WCNF()
|
|
97
|
+
for clause in sat_model.get_all_clauses():
|
|
98
|
+
wcnf.append(list(clause)) # hard clauses
|
|
99
|
+
|
|
100
|
+
for name, weight in effective.items():
|
|
101
|
+
variable = sat_model.variables.get(name)
|
|
102
|
+
if variable is None:
|
|
103
|
+
continue
|
|
104
|
+
int_weight = round(weight * scale)
|
|
105
|
+
if int_weight > 0:
|
|
106
|
+
# Cost paid when the feature is selected -> pushes it out to reduce the objective.
|
|
107
|
+
wcnf.append([-variable], weight=int_weight)
|
|
108
|
+
elif int_weight < 0:
|
|
109
|
+
# Cost paid when the feature is deselected -> pulls it in.
|
|
110
|
+
wcnf.append([variable], weight=-int_weight)
|
|
111
|
+
|
|
112
|
+
with RC2(wcnf) as rc2:
|
|
113
|
+
assignment = rc2.compute()
|
|
114
|
+
if assignment is None:
|
|
115
|
+
return None, None
|
|
116
|
+
selected_vars = {literal for literal in assignment if literal > 0}
|
|
117
|
+
elements: dict[Any, Any] = {}
|
|
118
|
+
for literal in assignment:
|
|
119
|
+
if literal > 0:
|
|
120
|
+
feature_name = sat_model.features.get(literal)
|
|
121
|
+
if feature_name is not None: # skip auxiliary variables
|
|
122
|
+
elements[feature_name] = True
|
|
123
|
+
configuration = Configuration(elements)
|
|
124
|
+
|
|
125
|
+
optimum = sum(weight for name, weight in weights.items()
|
|
126
|
+
if sat_model.variables.get(name) in selected_vars)
|
|
127
|
+
return configuration, optimum
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from typing import Any, cast
|
|
2
|
+
|
|
3
|
+
from pysat.solvers import Solver
|
|
4
|
+
|
|
5
|
+
from flamapy.core.operations import Operation
|
|
6
|
+
from flamapy.core.operations.descriptor import OperationDescriptor
|
|
7
|
+
from flamapy.metamodels.pysat_metamodel.models.pysat_model import PySATModel
|
|
8
|
+
from flamapy.core.models import VariabilityModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PySATBackbone(Operation):
|
|
12
|
+
facade = OperationDescriptor(
|
|
13
|
+
doc=(
|
|
14
|
+
'Returns the backbone of the feature model: the set of features that must\n'
|
|
15
|
+
'always be selected (core) and those that must never be selected (dead) across\n'
|
|
16
|
+
"all valid configurations, grouped under 'core' and 'dead' keys."
|
|
17
|
+
),
|
|
18
|
+
returns='Union[None, Dict[str, List[Any]]]',
|
|
19
|
+
name='backbone', operation='PySATBackbone', default_backend='sat'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._result: dict[str, list[Any]] = {"core": [], "dead": []}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_backbone(self) -> dict[str, list[Any]]:
|
|
27
|
+
return self.get_result()
|
|
28
|
+
|
|
29
|
+
def get_result(self) -> dict[str, list[Any]]:
|
|
30
|
+
return self._result
|
|
31
|
+
|
|
32
|
+
def execute(self, model: VariabilityModel) -> 'PySATBackbone':
|
|
33
|
+
sat_model = cast(PySATModel, model)
|
|
34
|
+
self._result = get_backbone(sat_model)
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_backbone(model: PySATModel) -> dict[str, list[Any]]:
|
|
39
|
+
# B <- Ø
|
|
40
|
+
backbone_core = []
|
|
41
|
+
backbone_dead = []
|
|
42
|
+
|
|
43
|
+
id_to_name = model.features
|
|
44
|
+
relevant_ids = set(id_to_name.keys())
|
|
45
|
+
|
|
46
|
+
solver = Solver(name='glucose3')
|
|
47
|
+
for clause in model.get_all_clauses().clauses:
|
|
48
|
+
solver.add_clause(clause)
|
|
49
|
+
|
|
50
|
+
# (out, C) <- SAT(phi)
|
|
51
|
+
if not solver.solve():
|
|
52
|
+
solver.delete()
|
|
53
|
+
return {"core": [], "dead": list(id_to_name.values())}
|
|
54
|
+
|
|
55
|
+
# 3. C <- filter(C)
|
|
56
|
+
initial_model = solver.get_model()
|
|
57
|
+
candidates = {lit for lit in initial_model if abs(lit) in relevant_ids}
|
|
58
|
+
|
|
59
|
+
# while C != Ø do
|
|
60
|
+
while candidates:
|
|
61
|
+
# l <- pick a literal from C
|
|
62
|
+
literal = next(iter(candidates))
|
|
63
|
+
|
|
64
|
+
# (out, S) <- SAT(phi U {not l})
|
|
65
|
+
if not solver.solve(assumptions=[-literal]):
|
|
66
|
+
# out = unsat -> l is in the backbone
|
|
67
|
+
name = id_to_name[abs(literal)]
|
|
68
|
+
if literal > 0:
|
|
69
|
+
backbone_core.append(name)
|
|
70
|
+
else:
|
|
71
|
+
backbone_dead.append(name)
|
|
72
|
+
|
|
73
|
+
# C <- C \ {l}
|
|
74
|
+
candidates.remove(literal)
|
|
75
|
+
|
|
76
|
+
# phi <- phi U {l}
|
|
77
|
+
solver.add_clause([literal])
|
|
78
|
+
else:
|
|
79
|
+
new_model_set = set(solver.get_model())
|
|
80
|
+
# C <- C ∩ S
|
|
81
|
+
candidates &= new_model_set
|
|
82
|
+
solver.delete()
|
|
83
|
+
return {"core": backbone_core, "dead": backbone_dead}
|