flamapy-sat 2.6.0.dev4__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.
Files changed (59) hide show
  1. {flamapy_sat-2.6.0.dev4/flamapy_sat.egg-info → flamapy_sat-2.6.0.dev5}/PKG-INFO +3 -3
  2. flamapy_sat-2.6.0.dev5/flamapy/metamodels/pysat_diagnosis_metamodel/operations/__init__.py +20 -0
  3. flamapy_sat-2.6.0.dev5/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_configuration_diagnosis.py +162 -0
  4. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/fm_to_diag_pysat.py +13 -13
  5. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/models/pysat_model.py +3 -0
  6. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/__init__.py +6 -0
  7. flamapy_sat-2.6.0.dev5/flamapy/metamodels/pysat_metamodel/operations/pysat_attribute_optimization.py +127 -0
  8. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations.py +3 -1
  9. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations_number.py +3 -0
  10. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_false_optional_features.py +5 -5
  11. flamapy_sat-2.6.0.dev5/flamapy/metamodels/pysat_metamodel/operations/pysat_minimum_configuration.py +49 -0
  12. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_sampling.py +28 -3
  13. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable_configuration.py +9 -10
  14. flamapy_sat-2.6.0.dev5/flamapy/metamodels/pysat_metamodel/operations/pysat_twise_sampling.py +86 -0
  15. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/transformations/fm_to_pysat.py +27 -6
  16. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5/flamapy_sat.egg-info}/PKG-INFO +3 -3
  17. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy_sat.egg-info/SOURCES.txt +4 -0
  18. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy_sat.egg-info/requires.txt +2 -2
  19. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/pyproject.toml +3 -3
  20. flamapy_sat-2.6.0.dev4/flamapy/metamodels/pysat_diagnosis_metamodel/operations/__init__.py +0 -12
  21. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/LICENSE +0 -0
  22. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/README.md +0 -0
  23. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/__init__.py +0 -0
  24. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/models/__init__.py +0 -0
  25. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/models/pysat_diagnosis_model.py +0 -0
  26. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/__init__.py +0 -0
  27. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/checker.py +0 -0
  28. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/fastdiag.py +0 -0
  29. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/__init__.py +0 -0
  30. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/hsdag.py +0 -0
  31. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/__init__.py +0 -0
  32. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/fastdiag_labeler.py +0 -0
  33. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/labeler.py +0 -0
  34. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/quickxplain_labeler.py +0 -0
  35. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/node.py +0 -0
  36. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/quickxplain.py +0 -0
  37. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/utils.py +0 -0
  38. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_abstract_identifier.py +0 -0
  39. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_conflict.py +0 -0
  40. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_diagnosis.py +0 -0
  41. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/__init__.py +0 -0
  42. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/__init__.py +0 -0
  43. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/models/__init__.py +0 -0
  44. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/models/txtcnf_model.py +0 -0
  45. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_backbone.py +0 -0
  46. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_commonality.py +0 -0
  47. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_core_features.py +0 -0
  48. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_dead_features.py +0 -0
  49. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_filter.py +0 -0
  50. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_metrics.py +0 -0
  51. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable.py +0 -0
  52. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/transformations/__init__.py +0 -0
  53. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/transformations/cnf_to_pysat.py +0 -0
  54. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_reader.py +0 -0
  55. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_writer.py +0 -0
  56. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy_sat.egg-info/dependency_links.txt +0 -0
  57. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/flamapy_sat.egg-info/top_level.txt +0 -0
  58. {flamapy_sat-2.6.0.dev4 → flamapy_sat-2.6.0.dev5}/setup.cfg +0 -0
  59. {flamapy_sat-2.6.0.dev4 → 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.dev4
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.dev4
12
- Requires-Dist: flamapy-fm~=2.6.0.dev4
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
- def get_term_variable(term: Any) -> int:
55
- negated = False
56
- if term.startswith('-'):
57
- term = term[1:]
58
- negated = True
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
- if negated:
63
- return -var
64
- return var
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
  ]
@@ -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
- product[model.features.get(variable)] = True
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
- try:
35
- feature_model = cast(FeatureModel, sat_model.original_model)
36
- except FlamaException:
37
- LOGGER.exception("The transformation didn't attach the source model, "
38
- "which is required for this operation.")
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()]
@@ -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] # pylint: disable=unused-argument
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
- product[model.features.get(variable)] = True
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
- def get_term_variable(term: Any) -> int:
149
- if term.startswith('-'):
150
- return -self.destination_model.get_variable(term[1:])
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
- return self.destination_model.get_variable(term)
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.dev4
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.dev4
12
- Requires-Dist: flamapy-fm~=2.6.0.dev4
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
@@ -1,5 +1,5 @@
1
- flamapy-fw~=2.6.0.dev4
2
- flamapy-fm~=2.6.0.dev4
1
+ flamapy-fw~=2.6.0.dev5
2
+ flamapy-fm~=2.6.0.dev5
3
3
  python-sat~=0.1.7.dev1
4
4
 
5
5
  [dev]
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flamapy-sat"
7
- version = "2.6.0.dev4"
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.dev4",
15
- "flamapy-fm~=2.6.0.dev4",
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
- ]