flamapy-sat 2.6.0.dev3__tar.gz → 2.6.0.dev5__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.dev3/flamapy_sat.egg-info → flamapy_sat-2.6.0.dev5}/PKG-INFO +3 -3
- flamapy_sat-2.6.0.dev5/flamapy/metamodels/pysat_diagnosis_metamodel/operations/__init__.py +20 -0
- flamapy_sat-2.6.0.dev5/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_configuration_diagnosis.py +162 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/fm_to_diag_pysat.py +13 -13
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/models/pysat_model.py +3 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/__init__.py +6 -0
- flamapy_sat-2.6.0.dev5/flamapy/metamodels/pysat_metamodel/operations/pysat_attribute_optimization.py +127 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations.py +3 -1
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations_number.py +3 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_false_optional_features.py +5 -5
- flamapy_sat-2.6.0.dev5/flamapy/metamodels/pysat_metamodel/operations/pysat_minimum_configuration.py +49 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_sampling.py +28 -3
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable_configuration.py +9 -10
- flamapy_sat-2.6.0.dev5/flamapy/metamodels/pysat_metamodel/operations/pysat_twise_sampling.py +86 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/transformations/fm_to_pysat.py +27 -6
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5/flamapy_sat.egg-info}/PKG-INFO +3 -3
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy_sat.egg-info/SOURCES.txt +4 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy_sat.egg-info/requires.txt +2 -2
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/pyproject.toml +3 -3
- flamapy_sat-2.6.0.dev3/flamapy/metamodels/pysat_diagnosis_metamodel/operations/__init__.py +0 -12
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/LICENSE +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/README.md +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/models/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/models/pysat_diagnosis_model.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/checker.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/fastdiag.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/hsdag.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/fastdiag_labeler.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/labeler.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/quickxplain_labeler.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/node.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/quickxplain.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/utils.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_abstract_identifier.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_conflict.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_diagnosis.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/models/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/models/txtcnf_model.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_backbone.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_commonality.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_core_features.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_dead_features.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_filter.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_metrics.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/transformations/__init__.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/transformations/cnf_to_pysat.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_reader.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_writer.py +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy_sat.egg-info/dependency_links.txt +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy_sat.egg-info/top_level.txt +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/setup.cfg +0 -0
- {flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/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.dev5
|
|
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.dev5
|
|
12
|
+
Requires-Dist: flamapy-fm~=2.6.0.dev5
|
|
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,162 @@
|
|
|
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.metamodels.configuration_metamodel.models.configuration import Configuration
|
|
21
|
+
from flamapy.metamodels.pysat_metamodel.models.pysat_model import PySATModel
|
|
22
|
+
|
|
23
|
+
from .diagnosis.checker import ConsistencyChecker
|
|
24
|
+
from .diagnosis.quickxplain import QuickXPlain
|
|
25
|
+
from .diagnosis.fastdiag import FastDiag
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
Decision = tuple[str, bool]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _clauses_and_decisions(
|
|
32
|
+
model: PySATModel, configuration: Configuration
|
|
33
|
+
) -> tuple[list[list[int]], list[tuple[int, str, bool]]]:
|
|
34
|
+
clauses = [list(clause) for clause in model.get_all_clauses().clauses]
|
|
35
|
+
decisions: list[tuple[int, str, bool]] = []
|
|
36
|
+
for name, value in configuration.elements.items():
|
|
37
|
+
variable = model.variables.get(name)
|
|
38
|
+
if variable is None:
|
|
39
|
+
continue
|
|
40
|
+
selected = bool(value)
|
|
41
|
+
decisions.append((variable if selected else -variable, name, selected))
|
|
42
|
+
return clauses, decisions
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _decisions_from_literals(
|
|
46
|
+
literals: list[int], decisions: list[tuple[int, str, bool]]
|
|
47
|
+
) -> list[Decision]:
|
|
48
|
+
by_literal = {literal: (name, value) for literal, name, value in decisions}
|
|
49
|
+
return [by_literal[literal] for literal in literals if literal in by_literal]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PySATConfigurationConflict(Operation):
|
|
53
|
+
"""Minimal subset of the user's decisions that is inconsistent with the model."""
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self._configuration: Optional[Configuration] = None
|
|
57
|
+
self._result: list[Decision] = []
|
|
58
|
+
|
|
59
|
+
def set_configuration(self, configuration: Configuration) -> None:
|
|
60
|
+
self._configuration = configuration
|
|
61
|
+
|
|
62
|
+
def get_conflict(self) -> list[Decision]:
|
|
63
|
+
return self._result
|
|
64
|
+
|
|
65
|
+
def get_result(self) -> list[Decision]:
|
|
66
|
+
return self._result
|
|
67
|
+
|
|
68
|
+
def execute(self, model: VariabilityModel) -> 'PySATConfigurationConflict':
|
|
69
|
+
sat_model = cast(PySATModel, model)
|
|
70
|
+
clauses, decisions = _clauses_and_decisions(sat_model, self._configuration)
|
|
71
|
+
checker = ConsistencyChecker('glucose3', clauses)
|
|
72
|
+
conflict = QuickXPlain(checker).find_conflict([lit for lit, _, _ in decisions], [])
|
|
73
|
+
checker.delete()
|
|
74
|
+
self._result = _decisions_from_literals(conflict, decisions)
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class PySATConfigurationRepair(Operation):
|
|
79
|
+
"""Minimal subset of the user's decisions to retract to restore consistency."""
|
|
80
|
+
|
|
81
|
+
def __init__(self) -> None:
|
|
82
|
+
self._configuration: Optional[Configuration] = None
|
|
83
|
+
self._result: list[Decision] = []
|
|
84
|
+
|
|
85
|
+
def set_configuration(self, configuration: Configuration) -> None:
|
|
86
|
+
self._configuration = configuration
|
|
87
|
+
|
|
88
|
+
def get_repair(self) -> list[Decision]:
|
|
89
|
+
return self._result
|
|
90
|
+
|
|
91
|
+
def get_result(self) -> list[Decision]:
|
|
92
|
+
return self._result
|
|
93
|
+
|
|
94
|
+
def execute(self, model: VariabilityModel) -> 'PySATConfigurationRepair':
|
|
95
|
+
sat_model = cast(PySATModel, model)
|
|
96
|
+
clauses, decisions = _clauses_and_decisions(sat_model, self._configuration)
|
|
97
|
+
checker = ConsistencyChecker('glucose3', clauses)
|
|
98
|
+
diagnosis = FastDiag(checker).find_diagnosis([lit for lit, _, _ in decisions], [])
|
|
99
|
+
checker.delete()
|
|
100
|
+
self._result = _decisions_from_literals(diagnosis, decisions)
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class PySATFeatureExplanation(Operation):
|
|
105
|
+
"""Explain why a feature is forced by the current decisions.
|
|
106
|
+
|
|
107
|
+
``get_forced_value()`` is the value the feature is forced to (or ``None`` if it is not
|
|
108
|
+
forced), and ``get_result()`` is the minimal set of decisions responsible.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self) -> None:
|
|
112
|
+
self._configuration: Optional[Configuration] = None
|
|
113
|
+
self._feature: Optional[str] = None
|
|
114
|
+
self._forced_value: Optional[bool] = None
|
|
115
|
+
self._result: list[Decision] = []
|
|
116
|
+
|
|
117
|
+
def set_configuration(self, configuration: Configuration) -> None:
|
|
118
|
+
self._configuration = configuration
|
|
119
|
+
|
|
120
|
+
def set_feature(self, feature_name: str) -> None:
|
|
121
|
+
self._feature = feature_name
|
|
122
|
+
|
|
123
|
+
def get_forced_value(self) -> Optional[bool]:
|
|
124
|
+
return self._forced_value
|
|
125
|
+
|
|
126
|
+
def get_explanation(self) -> list[Decision]:
|
|
127
|
+
return self._result
|
|
128
|
+
|
|
129
|
+
def get_result(self) -> list[Decision]:
|
|
130
|
+
return self._result
|
|
131
|
+
|
|
132
|
+
def execute(self, model: VariabilityModel) -> 'PySATFeatureExplanation':
|
|
133
|
+
sat_model = cast(PySATModel, model)
|
|
134
|
+
self._forced_value = None
|
|
135
|
+
self._result = []
|
|
136
|
+
if self._feature is None:
|
|
137
|
+
return self
|
|
138
|
+
clauses, decisions = _clauses_and_decisions(sat_model, self._configuration)
|
|
139
|
+
variable = sat_model.variables.get(self._feature)
|
|
140
|
+
if variable is None:
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
literals = [lit for lit, _, _ in decisions]
|
|
144
|
+
checker = ConsistencyChecker('glucose3', clauses)
|
|
145
|
+
can_be_true = checker.is_consistent([*literals, variable], [])
|
|
146
|
+
can_be_false = checker.is_consistent([*literals, -variable], [])
|
|
147
|
+
if can_be_true and not can_be_false:
|
|
148
|
+
self._forced_value = True
|
|
149
|
+
forbidden = -variable
|
|
150
|
+
elif can_be_false and not can_be_true:
|
|
151
|
+
self._forced_value = False
|
|
152
|
+
forbidden = variable
|
|
153
|
+
else:
|
|
154
|
+
checker.delete() # not forced (or already contradictory) -> no explanation
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
# The minimal decisions that, with the model, entail the forced value are those
|
|
158
|
+
# inconsistent with the model plus the opposite assignment.
|
|
159
|
+
explanation = QuickXPlain(checker).find_conflict(literals, [forbidden])
|
|
160
|
+
checker.delete()
|
|
161
|
+
self._result = _decisions_from_literals(explanation, decisions)
|
|
162
|
+
return self
|
|
@@ -20,8 +20,8 @@ class FmToDiagPysat(FmToPysat):
|
|
|
20
20
|
def get_destination_extension() -> str:
|
|
21
21
|
return 'pysat_diagnosis'
|
|
22
22
|
|
|
23
|
-
def __init__(self, source_model: FeatureModel) -> None:
|
|
24
|
-
super().__init__(source_model)
|
|
23
|
+
def __init__(self, source_model: FeatureModel, cnf_method: str = 'distributive') -> None:
|
|
24
|
+
super().__init__(source_model, cnf_method)
|
|
25
25
|
self.destination_model = DiagnosisModel()
|
|
26
26
|
|
|
27
27
|
def add_root(self, feature: Feature) -> None:
|
|
@@ -51,20 +51,20 @@ class FmToDiagPysat(FmToPysat):
|
|
|
51
51
|
self.destination_model.add_clause_to_map(str(relation), clauses)
|
|
52
52
|
|
|
53
53
|
def add_constraint(self, ctc: Constraint) -> None:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
var = self.destination_model.get_variable(term)
|
|
54
|
+
if self.cnf_method == 'tseytin':
|
|
55
|
+
clauses, aux_names = ctc.ast.get_clauses_with_aux(method='tseytin')
|
|
56
|
+
aux_map = self._allocate_auxiliary(aux_names)
|
|
57
|
+
else:
|
|
58
|
+
clauses = ctc.ast.get_clauses()
|
|
59
|
+
aux_map = {}
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
def get_term_variable(term: Any) -> int:
|
|
62
|
+
negated = term.startswith('-')
|
|
63
|
+
name = term[1:] if negated else term
|
|
64
|
+
var = aux_map[name] if name in aux_map else self.destination_model.get_variable(name)
|
|
65
|
+
return -var if negated else var
|
|
65
66
|
|
|
66
67
|
ctc_clauses = []
|
|
67
|
-
clauses = ctc.ast.get_clauses()
|
|
68
68
|
for clause in clauses:
|
|
69
69
|
clause_variables = list(map(get_term_variable, clause))
|
|
70
70
|
ctc_clauses.append(clause_variables)
|
|
@@ -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
|
]
|
flamapy_sat-2.6.0.dev5/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
|
|
@@ -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()]
|
flamapy_sat-2.6.0.dev5/flamapy/metamodels/pysat_metamodel/operations/pysat_minimum_configuration.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
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 Operation
|
|
8
|
+
from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
|
|
9
|
+
from flamapy.metamodels.pysat_metamodel.models.pysat_model import PySATModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PySATMinimumConfiguration(Operation):
|
|
13
|
+
"""Return a valid configuration with the fewest selected features, via MaxSAT (RC2).
|
|
14
|
+
|
|
15
|
+
Every feature contributes a unit soft clause preferring it to be deselected, so the
|
|
16
|
+
optimum is a valid configuration minimizing the number of selected features (the
|
|
17
|
+
minimum working configuration).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self._result: Optional[Configuration] = None
|
|
22
|
+
|
|
23
|
+
def get_result(self) -> Optional[Configuration]:
|
|
24
|
+
return self._result
|
|
25
|
+
|
|
26
|
+
def get_minimum_configuration(self) -> Optional[Configuration]:
|
|
27
|
+
return self._result
|
|
28
|
+
|
|
29
|
+
def execute(self, model: VariabilityModel) -> 'PySATMinimumConfiguration':
|
|
30
|
+
sat_model = cast(PySATModel, model)
|
|
31
|
+
wcnf = WCNF()
|
|
32
|
+
for clause in sat_model.get_all_clauses():
|
|
33
|
+
wcnf.append(list(clause)) # hard clauses
|
|
34
|
+
for variable in sat_model.features: # one unit soft clause per feature variable
|
|
35
|
+
wcnf.append([-variable], weight=1)
|
|
36
|
+
|
|
37
|
+
with RC2(wcnf) as rc2:
|
|
38
|
+
assignment = rc2.compute()
|
|
39
|
+
if assignment is None:
|
|
40
|
+
self._result = None
|
|
41
|
+
return self
|
|
42
|
+
elements: dict[Any, Any] = {}
|
|
43
|
+
for literal in assignment:
|
|
44
|
+
if literal > 0:
|
|
45
|
+
feature_name = sat_model.features.get(literal)
|
|
46
|
+
if feature_name is not None: # skip auxiliary variables
|
|
47
|
+
elements[feature_name] = True
|
|
48
|
+
self._result = Configuration(elements)
|
|
49
|
+
return self
|
|
@@ -49,23 +49,48 @@ def sample(solver: Solver,
|
|
|
49
49
|
model: PySATModel,
|
|
50
50
|
sample_size: int,
|
|
51
51
|
with_replacement: bool, # pylint: disable=unused-argument
|
|
52
|
-
partial_configuration: Optional[Configuration]
|
|
52
|
+
partial_configuration: Optional[Configuration]
|
|
53
53
|
) -> list[Configuration]:
|
|
54
|
+
"""Return up to ``sample_size`` valid configurations by enumeration.
|
|
55
|
+
|
|
56
|
+
``partial_configuration`` restricts the sample to configurations that fix the given
|
|
57
|
+
feature selections (applied as SAT assumptions). Note this is a deterministic
|
|
58
|
+
enumerator, not a uniform random sampler: it returns distinct configurations, so
|
|
59
|
+
``with_replacement`` has no effect here — for almost-uniform random sampling use the
|
|
60
|
+
``flamapy-sharpsat`` (UniGen) backend instead.
|
|
61
|
+
"""
|
|
54
62
|
if sample_size == 0:
|
|
55
63
|
return []
|
|
56
64
|
|
|
57
65
|
for clause in model.get_all_clauses():
|
|
58
66
|
solver.add_clause(clause)
|
|
59
67
|
|
|
68
|
+
assumptions = _partial_configuration_assumptions(model, partial_configuration)
|
|
69
|
+
|
|
60
70
|
products = []
|
|
61
|
-
for solutions in solver.enum_models():
|
|
71
|
+
for solutions in solver.enum_models(assumptions=assumptions):
|
|
62
72
|
product: dict[Any, bool] = {}
|
|
63
73
|
for variable in solutions:
|
|
64
74
|
if variable > 0:
|
|
65
|
-
|
|
75
|
+
name = model.features.get(variable)
|
|
76
|
+
if name is not None: # skip auxiliary (Tseytin) variables
|
|
77
|
+
product[name] = True
|
|
66
78
|
products.append(Configuration(product))
|
|
67
79
|
if len(products) == sample_size:
|
|
68
80
|
solver.delete()
|
|
69
81
|
return products
|
|
70
82
|
solver.delete()
|
|
71
83
|
return products
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _partial_configuration_assumptions(
|
|
87
|
+
model: PySATModel, partial_configuration: Optional[Configuration]
|
|
88
|
+
) -> list[int]:
|
|
89
|
+
"""SAT assumptions fixing the features decided in a partial configuration."""
|
|
90
|
+
assumptions: list[int] = []
|
|
91
|
+
if partial_configuration is not None:
|
|
92
|
+
for name, value in partial_configuration.elements.items():
|
|
93
|
+
variable = model.variables.get(name)
|
|
94
|
+
if variable is not None:
|
|
95
|
+
assumptions.append(variable if value else -variable)
|
|
96
|
+
return assumptions
|
|
@@ -33,6 +33,15 @@ class PySATSatisfiableConfiguration(SatisfiableConfiguration):
|
|
|
33
33
|
for clause in sat_model.get_all_clauses(): # AC es conjunto de conjuntos
|
|
34
34
|
self.solver.add_clause(clause) # añadimos la constraint
|
|
35
35
|
|
|
36
|
+
missing_features = [feature for feature in self.configuration.elements.keys()
|
|
37
|
+
if feature not in sat_model.variables.keys()]
|
|
38
|
+
if missing_features:
|
|
39
|
+
logger.warning("The features that are missing are: %s", list(missing_features))
|
|
40
|
+
logger.warning("The feature model contains the following features: %s",
|
|
41
|
+
list(sat_model.variables.keys()))
|
|
42
|
+
self.result = False
|
|
43
|
+
return self
|
|
44
|
+
|
|
36
45
|
if not self.configuration.is_full:
|
|
37
46
|
assumptions = []
|
|
38
47
|
for feature, selected in self.configuration.elements.items():
|
|
@@ -41,16 +50,6 @@ class PySATSatisfiableConfiguration(SatisfiableConfiguration):
|
|
|
41
50
|
else:
|
|
42
51
|
assumptions.append(-sat_model.variables[feature])
|
|
43
52
|
else:
|
|
44
|
-
missing_features = [feature for feature in self.configuration.elements.keys()
|
|
45
|
-
if feature not in sat_model.variables.keys()]
|
|
46
|
-
|
|
47
|
-
if missing_features:
|
|
48
|
-
logger.warning("The features that are missing are: %s", list(missing_features))
|
|
49
|
-
logger.warning("The feature model contains the following features: %s",
|
|
50
|
-
list(sat_model.variables.keys()))
|
|
51
|
-
self.result = False
|
|
52
|
-
return self
|
|
53
|
-
|
|
54
53
|
assumptions = []
|
|
55
54
|
for feature in sat_model.features.values():
|
|
56
55
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
|
|
4
|
+
from pysat.solvers import Solver
|
|
5
|
+
|
|
6
|
+
from flamapy.core.models import VariabilityModel
|
|
7
|
+
from flamapy.core.operations import Operation
|
|
8
|
+
from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
|
|
9
|
+
from flamapy.metamodels.pysat_metamodel.models.pysat_model import PySATModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PySATTWiseSampling(Operation):
|
|
13
|
+
"""t-wise (combinatorial) sampling: a set of valid configurations that covers every
|
|
14
|
+
satisfiable combination of ``t`` feature selections.
|
|
15
|
+
|
|
16
|
+
For ``t = 2`` this yields a pairwise covering sample. The sample is built greedily:
|
|
17
|
+
each configuration is grown to cover as many still-uncovered feature combinations as
|
|
18
|
+
the constraints allow.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, t: int = 2) -> None:
|
|
22
|
+
self.t = t
|
|
23
|
+
self.result: list[Configuration] = []
|
|
24
|
+
|
|
25
|
+
def set_t(self, t: int) -> None:
|
|
26
|
+
self.t = t
|
|
27
|
+
|
|
28
|
+
def get_sample(self) -> list[Configuration]:
|
|
29
|
+
return self.result
|
|
30
|
+
|
|
31
|
+
def get_result(self) -> list[Configuration]:
|
|
32
|
+
return self.result
|
|
33
|
+
|
|
34
|
+
def execute(self, model: VariabilityModel) -> 'PySATTWiseSampling':
|
|
35
|
+
sat_model = cast(PySATModel, model)
|
|
36
|
+
self.result = _twise_sample(sat_model, self.t)
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _satisfiable_targets(solver: Solver, feature_vars: list[int], t: int) -> list[tuple[int, ...]]:
|
|
41
|
+
"""Every satisfiable combination of ``t`` feature variables with each sign pattern."""
|
|
42
|
+
targets: list[tuple[int, ...]] = []
|
|
43
|
+
for combination in itertools.combinations(feature_vars, t):
|
|
44
|
+
for signs in itertools.product((1, -1), repeat=t):
|
|
45
|
+
literals = tuple(sign * var for sign, var in zip(signs, combination))
|
|
46
|
+
if solver.solve(assumptions=list(literals)):
|
|
47
|
+
targets.append(literals)
|
|
48
|
+
return targets
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _twise_sample(model: PySATModel, t: int) -> list[Configuration]:
|
|
52
|
+
clauses = list(model.get_all_clauses().clauses)
|
|
53
|
+
solver = Solver(name='glucose3', bootstrap_with=clauses)
|
|
54
|
+
feature_vars = model.feature_variables() if hasattr(model, 'feature_variables') \
|
|
55
|
+
else list(model.features.keys())
|
|
56
|
+
|
|
57
|
+
uncovered = _satisfiable_targets(solver, feature_vars, t)
|
|
58
|
+
configurations: list[Configuration] = []
|
|
59
|
+
|
|
60
|
+
while uncovered:
|
|
61
|
+
current: set[int] = set(uncovered[0])
|
|
62
|
+
# Greedily merge other uncovered targets that stay jointly satisfiable.
|
|
63
|
+
for target in uncovered[1:]:
|
|
64
|
+
if any(-literal in current for literal in target):
|
|
65
|
+
continue
|
|
66
|
+
tentative = current | set(target)
|
|
67
|
+
if solver.solve(assumptions=list(tentative)):
|
|
68
|
+
current = tentative
|
|
69
|
+
solver.solve(assumptions=list(current))
|
|
70
|
+
assignment = solver.get_model() or []
|
|
71
|
+
configurations.append(_to_configuration(model, assignment))
|
|
72
|
+
satisfied = set(assignment)
|
|
73
|
+
uncovered = [tg for tg in uncovered if not set(tg).issubset(satisfied)]
|
|
74
|
+
|
|
75
|
+
solver.delete()
|
|
76
|
+
return configurations
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _to_configuration(model: PySATModel, assignment: list[int]) -> Configuration:
|
|
80
|
+
elements: dict[Any, Any] = {}
|
|
81
|
+
for literal in assignment:
|
|
82
|
+
if literal > 0:
|
|
83
|
+
name = model.features.get(literal)
|
|
84
|
+
if name is not None: # skip auxiliary variables
|
|
85
|
+
elements[name] = True
|
|
86
|
+
return Configuration(elements)
|
|
@@ -20,9 +20,13 @@ class FmToPysat(ModelToModel):
|
|
|
20
20
|
def get_destination_extension() -> str:
|
|
21
21
|
return 'pysat'
|
|
22
22
|
|
|
23
|
-
def __init__(self, source_model: FeatureModel) -> None:
|
|
23
|
+
def __init__(self, source_model: FeatureModel, cnf_method: str = 'distributive') -> None:
|
|
24
24
|
self.source_model = source_model
|
|
25
25
|
self.counter = 1
|
|
26
|
+
# 'distributive' (default) or 'tseytin'. Tseytin keeps complex cross-tree
|
|
27
|
+
# constraints compact at the cost of auxiliary variables; it is a framework-only
|
|
28
|
+
# opt-in and is not exposed through the facade/CLI.
|
|
29
|
+
self.cnf_method = cnf_method
|
|
26
30
|
self.destination_model = PySATModel()
|
|
27
31
|
self.destination_model.original_model = source_model
|
|
28
32
|
|
|
@@ -144,14 +148,31 @@ class FmToPysat(ModelToModel):
|
|
|
144
148
|
clauses = self._add_constraint_relation(relation)
|
|
145
149
|
self._store_constraint_clauses(clauses)
|
|
146
150
|
|
|
151
|
+
def _allocate_auxiliary(self, aux_names: list[str]) -> dict[str, int]:
|
|
152
|
+
"""Assign a fresh SAT variable id to each Tseytin auxiliary name, recording it as
|
|
153
|
+
a non-feature auxiliary variable so it is excluded from feature enumeration."""
|
|
154
|
+
aux_map: dict[str, int] = {}
|
|
155
|
+
for name in aux_names:
|
|
156
|
+
var = self.counter
|
|
157
|
+
self.counter += 1
|
|
158
|
+
aux_map[name] = var
|
|
159
|
+
self.destination_model.auxiliary_variables.add(var)
|
|
160
|
+
return aux_map
|
|
161
|
+
|
|
147
162
|
def add_constraint(self, ctc: Constraint) -> None:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
if self.cnf_method == 'tseytin':
|
|
164
|
+
clauses, aux_names = ctc.ast.get_clauses_with_aux(method='tseytin')
|
|
165
|
+
aux_map = self._allocate_auxiliary(aux_names)
|
|
166
|
+
else:
|
|
167
|
+
clauses = ctc.ast.get_clauses()
|
|
168
|
+
aux_map = {}
|
|
151
169
|
|
|
152
|
-
|
|
170
|
+
def get_term_variable(term: Any) -> int:
|
|
171
|
+
negated = term.startswith('-')
|
|
172
|
+
name = term[1:] if negated else term
|
|
173
|
+
var = aux_map[name] if name in aux_map else self.destination_model.get_variable(name)
|
|
174
|
+
return -var if negated else var
|
|
153
175
|
|
|
154
|
-
clauses = ctc.ast.get_clauses()
|
|
155
176
|
for clause in clauses:
|
|
156
177
|
clause_variables = list(map(get_term_variable, clause))
|
|
157
178
|
self.destination_model.add_clause(clause_variables)
|
|
@@ -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.dev5
|
|
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.dev5
|
|
12
|
+
Requires-Dist: flamapy-fm~=2.6.0.dev5
|
|
13
13
|
Requires-Dist: python-sat~=0.1.7.dev1
|
|
14
14
|
Provides-Extra: dev
|
|
15
15
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -7,6 +7,7 @@ flamapy/metamodels/pysat_diagnosis_metamodel/models/__init__.py
|
|
|
7
7
|
flamapy/metamodels/pysat_diagnosis_metamodel/models/pysat_diagnosis_model.py
|
|
8
8
|
flamapy/metamodels/pysat_diagnosis_metamodel/operations/__init__.py
|
|
9
9
|
flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_abstract_identifier.py
|
|
10
|
+
flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_configuration_diagnosis.py
|
|
10
11
|
flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_conflict.py
|
|
11
12
|
flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_diagnosis.py
|
|
12
13
|
flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/__init__.py
|
|
@@ -28,6 +29,7 @@ flamapy/metamodels/pysat_metamodel/models/__init__.py
|
|
|
28
29
|
flamapy/metamodels/pysat_metamodel/models/pysat_model.py
|
|
29
30
|
flamapy/metamodels/pysat_metamodel/models/txtcnf_model.py
|
|
30
31
|
flamapy/metamodels/pysat_metamodel/operations/__init__.py
|
|
32
|
+
flamapy/metamodels/pysat_metamodel/operations/pysat_attribute_optimization.py
|
|
31
33
|
flamapy/metamodels/pysat_metamodel/operations/pysat_backbone.py
|
|
32
34
|
flamapy/metamodels/pysat_metamodel/operations/pysat_commonality.py
|
|
33
35
|
flamapy/metamodels/pysat_metamodel/operations/pysat_configurations.py
|
|
@@ -37,9 +39,11 @@ flamapy/metamodels/pysat_metamodel/operations/pysat_dead_features.py
|
|
|
37
39
|
flamapy/metamodels/pysat_metamodel/operations/pysat_false_optional_features.py
|
|
38
40
|
flamapy/metamodels/pysat_metamodel/operations/pysat_filter.py
|
|
39
41
|
flamapy/metamodels/pysat_metamodel/operations/pysat_metrics.py
|
|
42
|
+
flamapy/metamodels/pysat_metamodel/operations/pysat_minimum_configuration.py
|
|
40
43
|
flamapy/metamodels/pysat_metamodel/operations/pysat_sampling.py
|
|
41
44
|
flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable.py
|
|
42
45
|
flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable_configuration.py
|
|
46
|
+
flamapy/metamodels/pysat_metamodel/operations/pysat_twise_sampling.py
|
|
43
47
|
flamapy/metamodels/pysat_metamodel/transformations/__init__.py
|
|
44
48
|
flamapy/metamodels/pysat_metamodel/transformations/cnf_to_pysat.py
|
|
45
49
|
flamapy/metamodels/pysat_metamodel/transformations/dimacs_reader.py
|
|
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flamapy-sat"
|
|
7
|
-
version = "2.6.0.
|
|
7
|
+
version = "2.6.0.dev5"
|
|
8
8
|
description = "flamapy-sat is a plugin to flamapy module"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "GPL-3.0-or-later"
|
|
11
11
|
authors = [{ name = "Flamapy", email = "flamapy@us.es" }]
|
|
12
12
|
requires-python = ">=3.11"
|
|
13
13
|
dependencies = [
|
|
14
|
-
"flamapy-fw~=2.6.0.
|
|
15
|
-
"flamapy-fm~=2.6.0.
|
|
14
|
+
"flamapy-fw~=2.6.0.dev5",
|
|
15
|
+
"flamapy-fm~=2.6.0.dev5",
|
|
16
16
|
"python-sat~=0.1.7.dev1",
|
|
17
17
|
]
|
|
18
18
|
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
__all__ = [
|
|
9
|
-
'PySATAbstractIdentifier',
|
|
10
|
-
'PySATConflict',
|
|
11
|
-
'PySATDiagnosis'
|
|
12
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flamapy_sat-2.6.0.dev3 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|