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.
Files changed (63) hide show
  1. {flamapy_sat-2.5.0.dev0/flamapy_sat.egg-info → flamapy_sat-2.6.0}/PKG-INFO +14 -15
  2. flamapy_sat-2.6.0/README.md +15 -0
  3. flamapy_sat-2.6.0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/__init__.py +20 -0
  4. flamapy_sat-2.6.0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_configuration_diagnosis.py +205 -0
  5. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_conflict.py +23 -0
  6. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_diagnosis.py +24 -0
  7. flamapy_sat-2.6.0/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/fm_to_diag_pysat.py +37 -0
  8. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/models/pysat_model.py +3 -0
  9. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/__init__.py +8 -0
  10. flamapy_sat-2.6.0/flamapy/metamodels/pysat_metamodel/operations/pysat_attribute_optimization.py +127 -0
  11. flamapy_sat-2.6.0/flamapy/metamodels/pysat_metamodel/operations/pysat_backbone.py +83 -0
  12. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_commonality.py +15 -0
  13. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations.py +3 -1
  14. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations_number.py +3 -0
  15. flamapy_sat-2.6.0/flamapy/metamodels/pysat_metamodel/operations/pysat_core_features.py +24 -0
  16. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_dead_features.py +3 -13
  17. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_false_optional_features.py +5 -5
  18. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_filter.py +15 -0
  19. flamapy_sat-2.6.0/flamapy/metamodels/pysat_metamodel/operations/pysat_minimum_configuration.py +62 -0
  20. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_sampling.py +28 -3
  21. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable_configuration.py +9 -10
  22. flamapy_sat-2.6.0/flamapy/metamodels/pysat_metamodel/operations/pysat_twise_sampling.py +102 -0
  23. flamapy_sat-2.6.0/flamapy/metamodels/pysat_metamodel/transformations/fm_to_pysat.py +37 -0
  24. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0/flamapy_sat.egg-info}/PKG-INFO +14 -15
  25. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy_sat.egg-info/SOURCES.txt +5 -0
  26. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy_sat.egg-info/requires.txt +2 -2
  27. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/pyproject.toml +5 -5
  28. flamapy_sat-2.5.0.dev0/README.md +0 -16
  29. flamapy_sat-2.5.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/__init__.py +0 -12
  30. flamapy_sat-2.5.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/fm_to_diag_pysat.py +0 -73
  31. flamapy_sat-2.5.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_core_features.py +0 -35
  32. flamapy_sat-2.5.0.dev0/flamapy/metamodels/pysat_metamodel/transformations/fm_to_pysat.py +0 -171
  33. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/LICENSE +0 -0
  34. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/__init__.py +0 -0
  35. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/models/__init__.py +0 -0
  36. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/models/pysat_diagnosis_model.py +0 -0
  37. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/__init__.py +0 -0
  38. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/checker.py +0 -0
  39. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/fastdiag.py +0 -0
  40. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/__init__.py +0 -0
  41. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/hsdag.py +0 -0
  42. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/__init__.py +0 -0
  43. {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
  44. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/labeler.py +0 -0
  45. {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
  46. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/node.py +0 -0
  47. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/quickxplain.py +0 -0
  48. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/utils.py +0 -0
  49. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_abstract_identifier.py +0 -0
  50. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/__init__.py +0 -0
  51. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/__init__.py +0 -0
  52. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/models/__init__.py +0 -0
  53. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/models/txtcnf_model.py +0 -0
  54. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_metrics.py +0 -0
  55. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable.py +0 -0
  56. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/transformations/__init__.py +0 -0
  57. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/transformations/cnf_to_pysat.py +0 -0
  58. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_reader.py +0 -0
  59. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_writer.py +0 -0
  60. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy_sat.egg-info/dependency_links.txt +0 -0
  61. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/flamapy_sat.egg-info/top_level.txt +0 -0
  62. {flamapy_sat-2.5.0.dev0 → flamapy_sat-2.6.0}/setup.cfg +0 -0
  63. {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.5.0.dev0
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.9
8
+ Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: flamapy-fw~=2.5.0.dev0
12
- Requires-Dist: flamapy-fm~=2.5.0.dev0
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
- # pysat_metamodel
22
+ # flamapy-sat
23
23
 
24
- This repository will host the pysat metamodel and its operation implementation
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
- ## Install for development
32
+ ## Installation
28
33
 
29
- ``` bash
30
- pip install -e .
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
@@ -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
  ]
@@ -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}