flamapy-sat 2.6.0.dev4__tar.gz → 2.6.0.dev11__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.dev4/flamapy_sat.egg-info → flamapy_sat-2.6.0.dev11}/PKG-INFO +3 -3
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/__init__.py +20 -0
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_configuration_diagnosis.py +205 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_conflict.py +23 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_diagnosis.py +24 -0
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/fm_to_diag_pysat.py +37 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/models/pysat_model.py +3 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/operations/__init__.py +6 -0
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/operations/pysat_attribute_optimization.py +127 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/operations/pysat_backbone.py +10 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/operations/pysat_commonality.py +15 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations.py +3 -1
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations_number.py +3 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/operations/pysat_false_optional_features.py +5 -5
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/operations/pysat_filter.py +15 -0
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/operations/pysat_minimum_configuration.py +62 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/operations/pysat_sampling.py +28 -3
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable_configuration.py +9 -10
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/operations/pysat_twise_sampling.py +102 -0
- flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/transformations/fm_to_pysat.py +37 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11/flamapy_sat.egg-info}/PKG-INFO +3 -3
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy_sat.egg-info/SOURCES.txt +4 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy_sat.egg-info/requires.txt +2 -2
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/pyproject.toml +3 -3
- flamapy_sat-2.6.0.dev4/flamapy/metamodels/pysat_diagnosis_metamodel/operations/__init__.py +0 -12
- flamapy_sat-2.6.0.dev4/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/fm_to_diag_pysat.py +0 -73
- flamapy_sat-2.6.0.dev4/flamapy/metamodels/pysat_metamodel/transformations/fm_to_pysat.py +0 -171
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/LICENSE +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/README.md +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/models/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/models/pysat_diagnosis_model.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/checker.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/fastdiag.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/hsdag.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/fastdiag_labeler.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/labeler.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/quickxplain_labeler.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/node.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/quickxplain.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/utils.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_abstract_identifier.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/models/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/models/txtcnf_model.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/operations/pysat_core_features.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/operations/pysat_dead_features.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/operations/pysat_metrics.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/transformations/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/transformations/cnf_to_pysat.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_reader.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_writer.py +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy_sat.egg-info/dependency_links.txt +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/flamapy_sat.egg-info/top_level.txt +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/setup.cfg +0 -0
- {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev11}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flamapy-sat
|
|
3
|
-
Version: 2.6.0.
|
|
3
|
+
Version: 2.6.0.dev11
|
|
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.6.0.
|
|
12
|
-
Requires-Dist: flamapy-fm~=2.6.0.
|
|
11
|
+
Requires-Dist: flamapy-fw~=2.6.0.dev11
|
|
12
|
+
Requires-Dist: flamapy-fm~=2.6.0.dev11
|
|
13
13
|
Requires-Dist: python-sat~=0.1.7.dev1
|
|
14
14
|
Provides-Extra: dev
|
|
15
15
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -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
|
|
@@ -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:
|
|
@@ -9,9 +9,13 @@ from .pysat_core_features import PySATCoreFeatures
|
|
|
9
9
|
from .pysat_dead_features import PySATDeadFeatures
|
|
10
10
|
from .pysat_false_optional_features import PySATFalseOptionalFeatures
|
|
11
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
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
__all__ = [
|
|
18
|
+
'PySATAttributeOptimization',
|
|
15
19
|
'PySATBackbone',
|
|
16
20
|
'PySATCommonality',
|
|
17
21
|
'PySATConfigurations',
|
|
@@ -21,6 +25,8 @@ __all__ = [
|
|
|
21
25
|
'PySATFalseOptionalFeatures',
|
|
22
26
|
'PySATFilter',
|
|
23
27
|
'PySATMetrics',
|
|
28
|
+
'PySATMinimumConfiguration',
|
|
24
29
|
'PySATSatisfiable',
|
|
25
30
|
'PySATSatisfiableConfiguration',
|
|
31
|
+
'PySATTWiseSampling',
|
|
26
32
|
]
|
|
@@ -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
|
|
@@ -3,11 +3,21 @@ from typing import Any, cast
|
|
|
3
3
|
from pysat.solvers import Solver
|
|
4
4
|
|
|
5
5
|
from flamapy.core.operations import Operation
|
|
6
|
+
from flamapy.core.operations.descriptor import OperationDescriptor
|
|
6
7
|
from flamapy.metamodels.pysat_metamodel.models.pysat_model import PySATModel
|
|
7
8
|
from flamapy.core.models import VariabilityModel
|
|
8
9
|
|
|
9
10
|
|
|
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
|
+
)
|
|
11
21
|
|
|
12
22
|
def __init__(self) -> None:
|
|
13
23
|
self._result: dict[str, list[Any]] = {"core": [], "dead": []}
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
from flamapy.core.operations import Commonality
|
|
2
|
+
from flamapy.core.operations.descriptor import OperationDescriptor, Input
|
|
2
3
|
from flamapy.core.models import VariabilityModel
|
|
3
4
|
from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
|
|
4
5
|
from .pysat_configurations import PySATConfigurations
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class PySATCommonality(Commonality):
|
|
9
|
+
facade = OperationDescriptor(
|
|
10
|
+
doc=(
|
|
11
|
+
'This is a measure of how often a feature appears in the products of a product\n'
|
|
12
|
+
"line. It's usually expressed as a percentage. A feature with 100 per cent\n"
|
|
13
|
+
'commonality is a core feature, as it appears in all products.\n'
|
|
14
|
+
'\n'
|
|
15
|
+
'``configuration_path`` accepts a configuration file path, a ``{feature:\n'
|
|
16
|
+
'value}`` mapping, or a Configuration object.'
|
|
17
|
+
),
|
|
18
|
+
returns='Union[None, float]',
|
|
19
|
+
name='commonality', operation='PySATCommonality', default_backend='sat',
|
|
20
|
+
inputs=(Input('configuration_path', str, required=True, kind='configuration',
|
|
21
|
+
setter='set_configuration'),),
|
|
22
|
+
)
|
|
8
23
|
|
|
9
24
|
def __init__(self) -> None:
|
|
10
25
|
self.result: float = 0
|
|
@@ -35,7 +35,9 @@ def configurations(solver: Solver, model: PySATModel) -> list[Configuration]:
|
|
|
35
35
|
product: dict[Any, bool] = {}
|
|
36
36
|
for variable in solutions:
|
|
37
37
|
if variable > 0:
|
|
38
|
-
|
|
38
|
+
name = model.features.get(variable)
|
|
39
|
+
if name is not None: # skip auxiliary (Tseytin) variables
|
|
40
|
+
product[name] = True
|
|
39
41
|
result.append(Configuration(product))
|
|
40
42
|
solver.delete()
|
|
41
43
|
return result
|
|
@@ -25,6 +25,9 @@ class PySATConfigurationsNumber(ConfigurationsNumber):
|
|
|
25
25
|
for clause in model.get_all_clauses(): # AC es conjunto de conjuntos
|
|
26
26
|
self.solver.add_clause(clause) # añadimos la constraint
|
|
27
27
|
|
|
28
|
+
# Auxiliary (Tseytin) variables are functionally determined by their biconditional
|
|
29
|
+
# gate definitions, so each feature-level model has exactly one full model here;
|
|
30
|
+
# the count is unaffected by their presence.
|
|
28
31
|
for _ in self.solver.enum_models():
|
|
29
32
|
self.products_number += 1
|
|
30
33
|
self.solver.delete()
|
|
@@ -31,11 +31,11 @@ class PySATFalseOptionalFeatures(FalseOptionalFeatures):
|
|
|
31
31
|
return self.result
|
|
32
32
|
|
|
33
33
|
def _get_false_optional_features(self, sat_model: PySATModel) -> list[Any]:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
original_model = getattr(sat_model, 'original_model', None)
|
|
35
|
+
if original_model is None:
|
|
36
|
+
raise FlamaException("The transformation didn't attach the source model, "
|
|
37
|
+
"which is required for this operation.")
|
|
38
|
+
feature_model = cast(FeatureModel, original_model)
|
|
39
39
|
|
|
40
40
|
real_optional_features = [f for f in feature_model.get_features()
|
|
41
41
|
if not f.is_root() and not f.is_mandatory()]
|
|
@@ -4,11 +4,26 @@ from pysat.solvers import Solver
|
|
|
4
4
|
|
|
5
5
|
from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
|
|
6
6
|
from flamapy.core.operations import Filter
|
|
7
|
+
from flamapy.core.operations.descriptor import OperationDescriptor, Input
|
|
7
8
|
from flamapy.metamodels.pysat_metamodel.models.pysat_model import PySATModel
|
|
8
9
|
from flamapy.core.models import VariabilityModel
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class PySATFilter(Filter):
|
|
13
|
+
facade = OperationDescriptor(
|
|
14
|
+
doc=(
|
|
15
|
+
'This operation selects a subset of the products of a product line based on\n'
|
|
16
|
+
'certain criteria. For example, you might filter the products to only include\n'
|
|
17
|
+
'those that contain a certain feature.\n'
|
|
18
|
+
'\n'
|
|
19
|
+
'``configuration_path`` accepts a configuration file path, a ``{feature:\n'
|
|
20
|
+
'value}`` mapping, or a Configuration object.'
|
|
21
|
+
),
|
|
22
|
+
returns='Union[None, List[Configuration]]',
|
|
23
|
+
name='filter', operation='PySATFilter', default_backend='sat',
|
|
24
|
+
inputs=(Input('configuration_path', str, required=True, kind='configuration',
|
|
25
|
+
setter='set_configuration'),),
|
|
26
|
+
)
|
|
12
27
|
|
|
13
28
|
def __init__(self) -> None:
|
|
14
29
|
self.filter_products: list[list[Any]] = []
|