flamapy-fm 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 (68) hide show
  1. flamapy_fm-2.6.0/PKG-INFO +56 -0
  2. flamapy_fm-2.6.0/README.md +34 -0
  3. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/models/__init__.py +2 -0
  4. flamapy_fm-2.6.0/flamapy/metamodels/fm_metamodel/models/clause_set.py +201 -0
  5. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/models/feature_model.py +16 -7
  6. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/__init__.py +2 -0
  7. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_atomic_sets.py +15 -1
  8. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_average_branching_factor.py +13 -0
  9. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_count_leafs.py +11 -0
  10. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_estimated_configurations_number.py +13 -0
  11. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_feature_ancestors.py +24 -1
  12. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_attribute.py +26 -0
  13. flamapy_fm-2.6.0/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_feature_model.py +223 -0
  14. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_language_level.py +9 -0
  15. flamapy_fm-2.6.0/flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +47 -0
  16. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_max_depth_tree.py +11 -0
  17. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_metrics.py +10 -0
  18. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_variation_points.py +17 -1
  19. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/__init__.py +2 -0
  20. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/uvl_reader.py +26 -16
  21. flamapy_fm-2.6.0/flamapy/metamodels/fm_metamodel/transformations/xml_writer.py +140 -0
  22. flamapy_fm-2.6.0/flamapy_fm.egg-info/PKG-INFO +56 -0
  23. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy_fm.egg-info/SOURCES.txt +3 -0
  24. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy_fm.egg-info/requires.txt +2 -2
  25. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/pyproject.toml +5 -5
  26. flamapy_fm-2.5.0.dev0/PKG-INFO +0 -32
  27. flamapy_fm-2.5.0.dev0/README.md +0 -10
  28. flamapy_fm-2.5.0.dev0/flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +0 -27
  29. flamapy_fm-2.5.0.dev0/flamapy_fm.egg-info/PKG-INFO +0 -32
  30. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/LICENSE +0 -0
  31. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/__init__.py +0 -0
  32. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_core_features.py +0 -0
  33. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/interfaces/__init__.py +0 -0
  34. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/interfaces/variation_points.py +0 -0
  35. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/afm_reader.py +0 -0
  36. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/afm_writer.py +0 -0
  37. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/clafer_writer.py +0 -0
  38. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/featureide_reader.py +0 -0
  39. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/featureide_writer.py +0 -0
  40. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/flat_fm.py +0 -0
  41. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/fm_secure_features_names.py +0 -0
  42. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/glencoe_reader.py +0 -0
  43. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/glencoe_writer.py +0 -0
  44. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/json_reader.py +0 -0
  45. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/json_writer.py +0 -0
  46. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/pl_writer.py +0 -0
  47. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/pysat_to_fm.py +0 -0
  48. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/__init__.py +0 -0
  49. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/cardinality_group_refactoring.py +0 -0
  50. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/commitment_feature.py +0 -0
  51. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/deletion_feature.py +0 -0
  52. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring.py +0 -0
  53. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring_paper.py +0 -0
  54. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/multiple_group_decomposition_refactoring.py +0 -0
  55. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/mutex_group_refactoring.py +0 -0
  56. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/or_mandatory_refactoring.py +0 -0
  57. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/pseudocomplex_constraint_refactoring.py +0 -0
  58. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_exception.py +0 -0
  59. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_interface.py +0 -0
  60. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/strictcomplex_constraint_refactoring.py +0 -0
  61. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/xor_mandatory_refactoring.py +0 -0
  62. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/splot_writer.py +0 -0
  63. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/uvl_writer.py +0 -0
  64. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/xml_reader.py +0 -0
  65. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy_fm.egg-info/dependency_links.txt +0 -0
  66. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy_fm.egg-info/top_level.txt +0 -0
  67. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/setup.cfg +0 -0
  68. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/setup.py +0 -0
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: flamapy-fm
3
+ Version: 2.6.0
4
+ Summary: flamapy-fm is a plugin to Flamapy module
5
+ Author-email: Flamapy <flamapy@us.es>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/flamapy/fm_metamodel
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: flamapy-fw~=2.6.0
12
+ Requires-Dist: uvlparser~=2.5.0
13
+ Requires-Dist: afmparser~=1.0.3
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest; extra == "dev"
16
+ Requires-Dist: pytest-mock; extra == "dev"
17
+ Requires-Dist: prospector; extra == "dev"
18
+ Requires-Dist: mypy; extra == "dev"
19
+ Requires-Dist: coverage; extra == "dev"
20
+ Requires-Dist: antlr4-tools; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ # flamapy-fm
24
+
25
+ The feature model metamodel plugin for [flamapy](https://flamapy.org): the
26
+ feature model classes plus readers and writers for many formats (UVL,
27
+ FeatureIDE, Glencoe, AFM, SPLOT, JSON, …).
28
+
29
+ **Documentation:** https://docs.flamapy.org/framework/plugins/feature_model_plugin
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install flamapy-fm
35
+ ```
36
+
37
+ ## Operations
38
+
39
+ Beyond the metrics/analysis operations (core/dead features, atomic sets, branching factor, …),
40
+ this plugin includes a **random feature-model generator**:
41
+
42
+ - `GenerateRandomFeatureModel` — builds a random synthetic `FeatureModel` object directly
43
+ (random tree of mandatory/optional/or/alternative groups plus random cross-tree constraints;
44
+ an optional `void` flag makes it unsatisfiable). Useful for benchmarking, testing, and building
45
+ corpora for learning-based operations. Serialize it to UVL (or any format) with the writer
46
+ transformations if you need text.
47
+
48
+ ```python
49
+ from flamapy.metamodels.fm_metamodel.operations import GenerateRandomFeatureModel
50
+
51
+ op = GenerateRandomFeatureModel()
52
+ op.set_num_features(20)
53
+ op.set_max_constraints(5)
54
+ op.set_seed(0)
55
+ fm = op.execute().get_result() # -> FeatureModel
56
+ ```
@@ -0,0 +1,34 @@
1
+ # flamapy-fm
2
+
3
+ The feature model metamodel plugin for [flamapy](https://flamapy.org): the
4
+ feature model classes plus readers and writers for many formats (UVL,
5
+ FeatureIDE, Glencoe, AFM, SPLOT, JSON, …).
6
+
7
+ **Documentation:** https://docs.flamapy.org/framework/plugins/feature_model_plugin
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install flamapy-fm
13
+ ```
14
+
15
+ ## Operations
16
+
17
+ Beyond the metrics/analysis operations (core/dead features, atomic sets, branching factor, …),
18
+ this plugin includes a **random feature-model generator**:
19
+
20
+ - `GenerateRandomFeatureModel` — builds a random synthetic `FeatureModel` object directly
21
+ (random tree of mandatory/optional/or/alternative groups plus random cross-tree constraints;
22
+ an optional `void` flag makes it unsatisfiable). Useful for benchmarking, testing, and building
23
+ corpora for learning-based operations. Serialize it to UVL (or any format) with the writer
24
+ transformations if you need text.
25
+
26
+ ```python
27
+ from flamapy.metamodels.fm_metamodel.operations import GenerateRandomFeatureModel
28
+
29
+ op = GenerateRandomFeatureModel()
30
+ op.set_num_features(20)
31
+ op.set_max_constraints(5)
32
+ op.set_seed(0)
33
+ fm = op.execute().get_result() # -> FeatureModel
34
+ ```
@@ -10,11 +10,13 @@ from .feature_model import (
10
10
  FeatureType,
11
11
  AttributeType
12
12
  )
13
+ from .clause_set import ClauseSet
13
14
 
14
15
  __all__ = [
15
16
  'Attribute',
16
17
  'AttributeType',
17
18
  'Cardinality',
19
+ 'ClauseSet',
18
20
  'Constraint',
19
21
  'Domain',
20
22
  'Feature',
@@ -0,0 +1,201 @@
1
+ """A solver-agnostic CNF (clause set) of a feature model.
2
+
3
+ `ClauseSet` is a lightweight data holder — the clauses plus the variable/feature maps — that any
4
+ plugin can build from a feature model without depending on a SAT solver. It is deliberately NOT a
5
+ ``VariabilityModel`` (it is not a discoverable metamodel) and the encoder lives here as a
6
+ classmethod builder rather than as a transformation (flamapy transformations are m2m/t2m/m2t).
7
+
8
+ The SAT plugin (`FmToPysat`) and the compilation/counting plugins (sdd, dnnf, sharpsat) all build
9
+ their representation from this single encoding.
10
+ """
11
+ import itertools
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Optional
14
+
15
+ from flamapy.core.exceptions import FlamaException
16
+
17
+ from flamapy.metamodels.fm_metamodel.models.feature_model import (
18
+ FeatureModel,
19
+ Feature,
20
+ Relation,
21
+ Constraint,
22
+ )
23
+
24
+
25
+ @dataclass
26
+ class ClauseSet:
27
+ """CNF encoding of a feature model as signed-integer clauses plus name/id maps."""
28
+
29
+ clauses: list[list[int]] = field(default_factory=list)
30
+ variables: dict[str, int] = field(default_factory=dict) # feature name -> variable id
31
+ features: dict[int, str] = field(default_factory=dict) # variable id -> feature name
32
+ auxiliary_variables: set[int] = field(default_factory=set) # Tseytin aux ids (non-feature)
33
+ original_model: Optional[FeatureModel] = None
34
+ # Provenance: the clauses grouped by their source (root feature / relation / constraint),
35
+ # labelled by ``str(source)``. Needed by diagnosis (clause → constraint mapping); ignored by
36
+ # consumers that only want the flat clause list.
37
+ clause_groups: list[tuple[str, list[list[int]]]] = field(default_factory=list)
38
+
39
+ def feature_variables(self) -> list[int]:
40
+ """Variable ids that correspond to features (excludes Tseytin auxiliaries)."""
41
+ return list(self.features.keys())
42
+
43
+ @classmethod
44
+ def from_feature_model(
45
+ cls, feature_model: FeatureModel, cnf_method: str = 'distributive'
46
+ ) -> 'ClauseSet':
47
+ """Encode ``feature_model`` into CNF.
48
+
49
+ ``cnf_method`` controls only how cross-tree constraints are turned into clauses:
50
+ ``'distributive'`` (default) expands them directly, ``'tseytin'`` introduces auxiliary
51
+ variables for a compact, model-count-preserving encoding.
52
+ """
53
+ return _Encoder(cnf_method).encode(feature_model)
54
+
55
+
56
+ class _Encoder:
57
+ """Ported ~1:1 from the SAT plugin's FmToPysat so the produced clauses are identical."""
58
+
59
+ def __init__(self, cnf_method: str) -> None:
60
+ self.cnf_method = cnf_method
61
+ self.counter = 1
62
+ self.variables: dict[str, int] = {}
63
+ self.features: dict[int, str] = {}
64
+ self.clauses: list[list[int]] = []
65
+ self.auxiliary_variables: set[int] = set()
66
+ self.clause_groups: list[tuple[str, list[list[int]]]] = []
67
+
68
+ def encode(self, feature_model: FeatureModel) -> ClauseSet:
69
+ for feature in feature_model.get_features():
70
+ self._add_feature(feature)
71
+ self._add_root(feature_model.root)
72
+ for relation in feature_model.get_relations():
73
+ self._add_relation(relation)
74
+ for constraint in feature_model.get_logical_constraints():
75
+ self._add_constraint(constraint)
76
+ return ClauseSet(
77
+ clauses=self.clauses,
78
+ variables=self.variables,
79
+ features=self.features,
80
+ auxiliary_variables=self.auxiliary_variables,
81
+ original_model=feature_model,
82
+ clause_groups=self.clause_groups,
83
+ )
84
+
85
+ def _get_variable(self, name: str) -> int:
86
+ if name not in self.variables:
87
+ raise FlamaException(f'Feature {name} is not in the model')
88
+ return self.variables[name]
89
+
90
+ def _add_feature(self, feature: Feature) -> None:
91
+ if feature.name not in self.variables:
92
+ self.variables[feature.name] = self.counter
93
+ self.features[self.counter] = feature.name
94
+ self.counter += 1
95
+
96
+ def _add_root(self, feature: Feature) -> None:
97
+ clause = [self._get_variable(feature.name)]
98
+ self.clauses.append(clause)
99
+ self.clause_groups.append((str(feature), [clause]))
100
+
101
+ def _add_mandatory_relation(self, relation: Relation) -> list[list[int]]:
102
+ parent = self._get_variable(relation.parent.name)
103
+ child = self._get_variable(relation.children[0].name)
104
+ return [[-parent, child], [-child, parent]]
105
+
106
+ def _add_optional_relation(self, relation: Relation) -> list[list[int]]:
107
+ parent = self._get_variable(relation.parent.name)
108
+ child = self._get_variable(relation.children[0].name)
109
+ return [[-child, parent]]
110
+
111
+ def _add_or_relation(self, relation: Relation) -> list[list[int]]:
112
+ parent = self._get_variable(relation.parent.name)
113
+ alt = [-parent] + [self._get_variable(c.name) for c in relation.children]
114
+ clauses = [alt]
115
+ for child in relation.children:
116
+ clauses.append([-self._get_variable(child.name), parent])
117
+ return clauses
118
+
119
+ def _add_alternative_relation(self, relation: Relation) -> list[list[int]]:
120
+ parent = self._get_variable(relation.parent.name)
121
+ alt = [-parent] + [self._get_variable(c.name) for c in relation.children]
122
+ clauses = [alt]
123
+ for i, _ in enumerate(relation.children):
124
+ for j in range(i + 1, len(relation.children)):
125
+ if i != j:
126
+ clauses.append([
127
+ -self._get_variable(relation.children[i].name),
128
+ -self._get_variable(relation.children[j].name),
129
+ ])
130
+ clauses.append([-self._get_variable(relation.children[i].name), parent])
131
+ return clauses
132
+
133
+ def _add_constraint_relation(self, relation: Relation) -> list[list[int]]:
134
+ parent = self._get_variable(relation.parent.name)
135
+ card_min = relation.card_min
136
+ card_max = relation.card_max if relation.card_max != -1 else len(relation.children)
137
+ clauses = []
138
+ for val in range(len(relation.children) + 1):
139
+ if val < card_min or val > card_max:
140
+ for combination in itertools.combinations(relation.children, val):
141
+ cnf = [-parent]
142
+ for feat in relation.children:
143
+ if feat in combination:
144
+ cnf.append(-self._get_variable(feat.name))
145
+ else:
146
+ cnf.append(self._get_variable(feat.name))
147
+ clauses.append(cnf)
148
+ for val in range(1, len(relation.children) + 1):
149
+ for combination in itertools.combinations(relation.children, val):
150
+ cnf = [parent]
151
+ for feat in relation.children:
152
+ if feat in combination:
153
+ cnf.append(-self._get_variable(feat.name))
154
+ else:
155
+ cnf.append(self._get_variable(feat.name))
156
+ clauses.append(cnf)
157
+ return clauses
158
+
159
+ def _add_relation(self, relation: Relation) -> None:
160
+ if relation.is_mandatory():
161
+ clauses = self._add_mandatory_relation(relation)
162
+ elif relation.is_optional():
163
+ clauses = self._add_optional_relation(relation)
164
+ elif relation.is_or():
165
+ clauses = self._add_or_relation(relation)
166
+ elif relation.is_alternative():
167
+ clauses = self._add_alternative_relation(relation)
168
+ else:
169
+ clauses = self._add_constraint_relation(relation)
170
+ self.clauses.extend(clauses)
171
+ self.clause_groups.append((str(relation), clauses))
172
+
173
+ def _allocate_auxiliary(self, aux_names: list[str]) -> dict[str, int]:
174
+ aux_map: dict[str, int] = {}
175
+ for name in aux_names:
176
+ var = self.counter
177
+ self.counter += 1
178
+ aux_map[name] = var
179
+ self.auxiliary_variables.add(var)
180
+ return aux_map
181
+
182
+ def _add_constraint(self, ctc: Constraint) -> None:
183
+ if self.cnf_method == 'tseytin':
184
+ clauses, aux_names = ctc.ast.get_clauses_with_aux(method='tseytin')
185
+ aux_map = self._allocate_auxiliary(aux_names)
186
+ else:
187
+ clauses = ctc.ast.get_clauses()
188
+ aux_map = {}
189
+
190
+ def term_to_variable(term: Any) -> int:
191
+ negated = term.startswith('-')
192
+ name = term[1:] if negated else term
193
+ var = aux_map[name] if name in aux_map else self._get_variable(name)
194
+ return -var if negated else var
195
+
196
+ group = []
197
+ for clause in clauses:
198
+ resolved = [term_to_variable(term) for term in clause]
199
+ self.clauses.append(resolved)
200
+ group.append(resolved)
201
+ self.clause_groups.append((str(ctc), group))
@@ -3,7 +3,7 @@ from functools import total_ordering
3
3
  from enum import Enum
4
4
 
5
5
  from flamapy.core.exceptions import FlamaException
6
- from flamapy.core.models import AST, VariabilityModel, VariabilityElement, ASTOperation
6
+ from flamapy.core.models import AST, VariabilityModel, VariabilityElement, ASTOperation, NodeType
7
7
  from flamapy.core.models.ast import LOGICAL_OPERATORS, ARITHMETIC_OPERATORS, AGGREGATION_OPERATORS
8
8
  from flamapy.core.models.ast import simplify_formula, propagate_negation, to_cnf
9
9
 
@@ -247,7 +247,11 @@ class Constraint:
247
247
  self._ast = ast
248
248
 
249
249
  def get_features(self) -> list[str]:
250
- """List of features' names involved in the constraint."""
250
+ """List of features' names involved in the constraint.
251
+
252
+ Uses node_type information when available (set by readers that support it).
253
+ Falls back to heuristics for nodes without explicit type info.
254
+ """
251
255
  features = set()
252
256
  stack = [self.ast.root]
253
257
  while stack:
@@ -255,9 +259,15 @@ class Constraint:
255
259
  if node is None:
256
260
  continue
257
261
  if node.is_unique_term():
258
- if (isinstance(node.data, (int, float)) or node.data.startswith("'")):
259
- continue
260
- features.add(node.data)
262
+ if node.node_type is not None:
263
+ if node.node_type == NodeType.FEATURE:
264
+ features.add(node.data)
265
+ # NodeType.LITERAL: skip
266
+ # Fallback heuristics for nodes without explicit type (backward compatibility)
267
+ elif not isinstance(node.data, (int, float)) and not (
268
+ isinstance(node.data, str) and node.data.startswith("'")
269
+ ):
270
+ features.add(node.data)
261
271
  elif node.is_unary_op():
262
272
  stack.append(node.left)
263
273
  elif node.is_binary_op():
@@ -281,8 +291,7 @@ class Constraint:
281
291
  """Return true if the constraint is a single feature or its negation."""
282
292
  root_op = self._ast.root
283
293
  return (root_op.is_term() or
284
- (root_op.data == ASTOperation.NOT and
285
- (root_op.left.is_term() or root_op.right.is_term())))
294
+ (root_op.data == ASTOperation.NOT and root_op.left.is_term()))
286
295
 
287
296
  def is_simple_constraint(self) -> bool:
288
297
  """Return true if the constraint is a simple constraint (requires or excludes)."""
@@ -8,6 +8,7 @@ from .fm_estimated_configurations_number import FMEstimatedConfigurationsNumber
8
8
  from .fm_atomic_sets import FMAtomicSets
9
9
  from .fm_metrics import FMMetrics
10
10
  from .fm_generate_random_attribute import GenerateRandomAttribute
11
+ from .fm_generate_random_feature_model import GenerateRandomFeatureModel
11
12
  from .fm_variation_points import FMVariationPoints
12
13
  from .fm_language_level import FMLanguageLevel, LanguageLevel, MajorLevel, MinorLevel
13
14
 
@@ -25,6 +26,7 @@ __all__ = [
25
26
  'FMMetrics',
26
27
  'FMVariationPoints',
27
28
  'GenerateRandomAttribute',
29
+ 'GenerateRandomFeatureModel',
28
30
  'LanguageLevel',
29
31
  'MajorLevel',
30
32
  'MinorLevel'
@@ -1,11 +1,25 @@
1
- from typing import cast
1
+ from typing import Any, cast
2
2
 
3
3
  from flamapy.core.models import VariabilityModel
4
4
  from flamapy.core.operations.atomic_sets import AtomicSets
5
+ from flamapy.core.operations.descriptor import OperationDescriptor
5
6
  from flamapy.metamodels.fm_metamodel.models import FeatureModel, Feature
6
7
 
7
8
 
9
+ def _atomic_sets_result(result: Any) -> Any:
10
+ return [[feature.name for feature in atomic_set] for atomic_set in result]
11
+
12
+
8
13
  class FMAtomicSets(AtomicSets):
14
+ facade = OperationDescriptor(
15
+ doc=(
16
+ 'This operation is used to find the atomic sets in a model: It returns the\n'
17
+ 'atomic sets if they are found in the model. If the model does not follow the\n'
18
+ 'UVL specification, an exception is raised and the operation returns False.'
19
+ ),
20
+ returns='Union[None, List[List[Any]]]',
21
+ name='atomic_sets', operation='FMAtomicSets', result_adapter=_atomic_sets_result
22
+ )
9
23
 
10
24
  def __init__(self) -> None:
11
25
  self.result: list[set[Feature]] = []
@@ -2,6 +2,7 @@ from typing import cast
2
2
 
3
3
  from flamapy.core.models import VariabilityModel
4
4
  from flamapy.core.operations import AverageBranchingFactor
5
+ from flamapy.core.operations.descriptor import OperationDescriptor
5
6
  from flamapy.metamodels.fm_metamodel.models.feature_model import FeatureModel
6
7
 
7
8
 
@@ -11,6 +12,18 @@ class FMAverageBranchingFactor(AverageBranchingFactor):
11
12
  The number of branches is the number of features that have at least one child.
12
13
  """
13
14
 
15
+ facade = OperationDescriptor(
16
+ doc=(
17
+ 'This refers to the average number of child features that a parent feature has\n'
18
+ "in a feature model. It's calculated by dividing the total number of child\n"
19
+ 'features by the total number of parent features. A high average branching\n'
20
+ 'factor indicates a complex feature model with many options, while a low\n'
21
+ 'average branching factor indicates a simpler model.'
22
+ ),
23
+ returns='Union[None, float]',
24
+ name='average_branching_factor', operation='FMAverageBranchingFactor'
25
+ )
26
+
14
27
  def __init__(self) -> None:
15
28
  self.result: float = 0
16
29
 
@@ -2,11 +2,22 @@ from typing import cast
2
2
 
3
3
  from flamapy.core.models import VariabilityModel
4
4
  from flamapy.core.operations import CountLeafs
5
+ from flamapy.core.operations.descriptor import OperationDescriptor
5
6
  from flamapy.metamodels.fm_metamodel.models import FeatureModel
6
7
 
7
8
 
8
9
  class FMCountLeafs(CountLeafs):
9
10
 
11
+ facade = OperationDescriptor(
12
+ doc=(
13
+ 'This operation counts the number of leaf features in a feature model. Leaf\n'
14
+ 'features are those that do not have any child features. They represent the\n'
15
+ 'most specific options in a product line.'
16
+ ),
17
+ returns='Union[None, int]',
18
+ name='count_leafs', operation='FMCountLeafs'
19
+ )
20
+
10
21
  def __init__(self) -> None:
11
22
  self.result = 0
12
23
 
@@ -4,6 +4,7 @@ from typing import Optional, cast
4
4
  from flamapy.core.models import VariabilityModel
5
5
  from flamapy.core.exceptions import FlamaException
6
6
  from flamapy.core.operations import EstimatedConfigurationsNumber
7
+ from flamapy.core.operations.descriptor import OperationDescriptor
7
8
  from flamapy.metamodels.fm_metamodel.models import FeatureModel, Feature, Relation
8
9
 
9
10
 
@@ -15,6 +16,18 @@ class FMEstimatedConfigurationsNumber(EstimatedConfigurationsNumber):
15
16
  and thus, the number is an upper limit of the real number of products.
16
17
  """
17
18
 
19
+ facade = OperationDescriptor(
20
+ doc=(
21
+ 'This is an estimate of the total number of different products that can be\n'
22
+ "produced from a feature model. It's calculated by considering all possible\n"
23
+ 'combinations of features. This can be a simple multiplication if all features\n'
24
+ 'are independent, but in most cases, constraints and dependencies between\n'
25
+ 'features need to be taken into account.'
26
+ ),
27
+ returns='Union[None, int]',
28
+ name='estimated_number_of_configurations', operation='FMEstimatedConfigurationsNumber'
29
+ )
30
+
18
31
  def __init__(self) -> None:
19
32
  self.result = 0
20
33
  self.feature_model: Optional[FeatureModel] = None
@@ -1,17 +1,40 @@
1
- from typing import Optional
1
+ from typing import Any, Optional
2
2
 
3
3
  from flamapy.core.models import VariabilityModel
4
4
  from flamapy.core.exceptions import FlamaException
5
5
  from flamapy.core.operations import Operation
6
+ from flamapy.core.operations.descriptor import OperationDescriptor, Input
6
7
  from flamapy.metamodels.fm_metamodel.models import Feature
7
8
 
8
9
 
10
+ def _feature_ancestors_inputs(operation: Any, facade: Any, kwargs: dict[str, Any]) -> None:
11
+ operation.set_feature(facade.fm_model.get_feature_by_name(kwargs['feature_name']))
12
+
13
+
14
+ def _feature_ancestors_result(result: Any) -> Any:
15
+ return [feature.name for feature in result]
16
+
17
+
9
18
  class FMFeatureAncestors(Operation):
10
19
  """
11
20
  This operation returns the list of ancestors of a given feature
12
21
  (i.e., all parents recursively up to the root feature).
13
22
  """
14
23
 
24
+ facade = OperationDescriptor(
25
+ doc=(
26
+ 'These are the features that are directly or indirectly the parent of a given\n'
27
+ 'feature in a feature model. Ancestors of a feature are found by traversing up\n'
28
+ 'the feature hierarchy. This information can be useful to understand the\n'
29
+ 'context and dependencies of a feature.'
30
+ ),
31
+ returns='Union[None, List[str]]',
32
+ name='feature_ancestors', operation='FMFeatureAncestors',
33
+ inputs=(Input('feature_name', str, required=True),),
34
+ input_adapter=_feature_ancestors_inputs,
35
+ result_adapter=_feature_ancestors_result,
36
+ )
37
+
15
38
  def __init__(self) -> None:
16
39
  self.result: list[Feature] = []
17
40
  self.feature: Optional[Feature] = None
@@ -3,6 +3,7 @@ from typing import Any, cast
3
3
 
4
4
  from flamapy.core.models import VariabilityModel
5
5
  from flamapy.core.operations import Operation
6
+ from flamapy.core.operations.descriptor import OperationDescriptor, Input
6
7
  from flamapy.core.exceptions import FlamaException
7
8
  from flamapy.metamodels.fm_metamodel.models import (
8
9
  FeatureModel,
@@ -12,12 +13,37 @@ from flamapy.metamodels.fm_metamodel.models import (
12
13
  )
13
14
 
14
15
 
16
+ def _generate_random_attribute_inputs(operation: Any, facade: Any, kwargs: dict[str, Any]) -> None:
17
+ operation.set_name(kwargs['name'])
18
+ operation.set_domain(Domain([Range(kwargs['min_value'], kwargs['max_value'])], None))
19
+ operation.set_only_leaf_features(bool(kwargs.get('only_leaf_features', False)))
20
+
21
+
15
22
  class GenerateRandomAttribute(Operation):
16
23
  """
17
24
  This operation generate random values for a given attribute and set the attribute for all
18
25
  features in the model.
19
26
  """
20
27
 
28
+ facade = OperationDescriptor(
29
+ doc=(
30
+ 'Augments the feature model with a randomly-valued numeric attribute on its\n'
31
+ 'features and returns the modified model. ``name`` is the attribute name, values are\n'
32
+ 'drawn from the ``[min_value, max_value]`` range, and ``only_leaf_features`` limits\n'
33
+ 'it to leaf features. Useful before attribute_optimization or feature bounds.'
34
+ ),
35
+ returns='FeatureModel',
36
+ name='generate_random_attribute', operation='GenerateRandomAttribute',
37
+ kind='transformer',
38
+ inputs=(
39
+ Input('name', str, required=True),
40
+ Input('min_value', int, default=0),
41
+ Input('max_value', int, default=100),
42
+ Input('only_leaf_features', bool, default=False),
43
+ ),
44
+ input_adapter=_generate_random_attribute_inputs,
45
+ )
46
+
21
47
  def __init__(self) -> None:
22
48
  self.result: FeatureModel
23
49
  self._attribute_name: str = ''