flamapy-fm 2.6.0.dev7__tar.gz → 2.6.0.dev12__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 (67) hide show
  1. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/PKG-INFO +23 -2
  2. flamapy_fm-2.6.0.dev12/README.md +34 -0
  3. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/models/__init__.py +2 -0
  4. flamapy_fm-2.6.0.dev12/flamapy/metamodels/fm_metamodel/models/clause_set.py +201 -0
  5. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/fm_atomic_sets.py +15 -1
  6. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/fm_average_branching_factor.py +13 -0
  7. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/fm_count_leafs.py +11 -0
  8. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/fm_estimated_configurations_number.py +13 -0
  9. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/fm_feature_ancestors.py +24 -1
  10. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_attribute.py +26 -0
  11. flamapy_fm-2.6.0.dev12/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_feature_model.py +223 -0
  12. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/fm_language_level.py +9 -0
  13. flamapy_fm-2.6.0.dev12/flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +47 -0
  14. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/fm_max_depth_tree.py +11 -0
  15. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/fm_metrics.py +10 -0
  16. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/fm_variation_points.py +17 -1
  17. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy_fm.egg-info/PKG-INFO +23 -2
  18. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy_fm.egg-info/SOURCES.txt +1 -0
  19. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy_fm.egg-info/requires.txt +1 -1
  20. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/pyproject.toml +2 -2
  21. flamapy_fm-2.6.0.dev7/README.md +0 -13
  22. flamapy_fm-2.6.0.dev7/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_feature_model.py +0 -100
  23. flamapy_fm-2.6.0.dev7/flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +0 -27
  24. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/LICENSE +0 -0
  25. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/__init__.py +0 -0
  26. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/models/feature_model.py +0 -0
  27. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/__init__.py +0 -0
  28. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/fm_core_features.py +0 -0
  29. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/interfaces/__init__.py +0 -0
  30. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/operations/interfaces/variation_points.py +0 -0
  31. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/__init__.py +0 -0
  32. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/afm_reader.py +0 -0
  33. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/afm_writer.py +0 -0
  34. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/clafer_writer.py +0 -0
  35. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/featureide_reader.py +0 -0
  36. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/featureide_writer.py +0 -0
  37. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/flat_fm.py +0 -0
  38. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/fm_secure_features_names.py +0 -0
  39. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/glencoe_reader.py +0 -0
  40. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/glencoe_writer.py +0 -0
  41. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/json_reader.py +0 -0
  42. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/json_writer.py +0 -0
  43. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/pl_writer.py +0 -0
  44. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/pysat_to_fm.py +0 -0
  45. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/__init__.py +0 -0
  46. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/cardinality_group_refactoring.py +0 -0
  47. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/commitment_feature.py +0 -0
  48. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/deletion_feature.py +0 -0
  49. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring.py +0 -0
  50. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring_paper.py +0 -0
  51. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/multiple_group_decomposition_refactoring.py +0 -0
  52. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/mutex_group_refactoring.py +0 -0
  53. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/or_mandatory_refactoring.py +0 -0
  54. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/pseudocomplex_constraint_refactoring.py +0 -0
  55. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_exception.py +0 -0
  56. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_interface.py +0 -0
  57. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/strictcomplex_constraint_refactoring.py +0 -0
  58. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/refactorings/xor_mandatory_refactoring.py +0 -0
  59. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/splot_writer.py +0 -0
  60. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/uvl_reader.py +0 -0
  61. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/uvl_writer.py +0 -0
  62. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/xml_reader.py +0 -0
  63. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy/metamodels/fm_metamodel/transformations/xml_writer.py +0 -0
  64. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy_fm.egg-info/dependency_links.txt +0 -0
  65. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/flamapy_fm.egg-info/top_level.txt +0 -0
  66. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/setup.cfg +0 -0
  67. {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev12}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flamapy-fm
3
- Version: 2.6.0.dev7
3
+ Version: 2.6.0.dev12
4
4
  Summary: flamapy-fm is a plugin to Flamapy module
5
5
  Author-email: Flamapy <flamapy@us.es>
6
6
  License-Expression: GPL-3.0-or-later
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/flamapy/fm_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.dev5
11
+ Requires-Dist: flamapy-fw~=2.6.0.dev12
12
12
  Requires-Dist: uvlparser~=2.5.0
13
13
  Requires-Dist: afmparser~=1.0.3
14
14
  Provides-Extra: dev
@@ -33,3 +33,24 @@ FeatureIDE, Glencoe, AFM, SPLOT, JSON, …).
33
33
  ```bash
34
34
  pip install flamapy-fm
35
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))
@@ -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 = ''
@@ -0,0 +1,223 @@
1
+ import random
2
+ from typing import Optional
3
+
4
+ from flamapy.core.models import VariabilityModel
5
+ from flamapy.core.models.ast import AST, ASTOperation, Node, NodeType
6
+ from flamapy.core.operations import Operation
7
+ from flamapy.core.operations.descriptor import OperationDescriptor, Input
8
+ from flamapy.core.exceptions import FlamaException
9
+ from flamapy.metamodels.fm_metamodel.models import (
10
+ FeatureModel,
11
+ Feature,
12
+ FeatureType,
13
+ Relation,
14
+ Constraint,
15
+ Attribute,
16
+ Domain,
17
+ Range,
18
+ )
19
+
20
+ # Minimum children an or/alternative group needs (and minimum features for a cross-tree constraint).
21
+ _MIN_GROUP_MEMBERS = 2
22
+
23
+ # UVL language levels this generator can target.
24
+ _LANGUAGE_LEVELS = ('boolean', 'arithmetic', 'typed')
25
+ # Non-boolean feature types used for the 'typed' level.
26
+ _TYPED_FEATURE_TYPES = (FeatureType.INTEGER, FeatureType.REAL, FeatureType.STRING)
27
+ # Name of the numeric attribute added to features for the 'arithmetic' level.
28
+ _ARITHMETIC_ATTRIBUTE = 'value'
29
+
30
+
31
+ class GenerateRandomFeatureModel(Operation):
32
+ """Generate a random synthetic feature model as a :class:`FeatureModel` object.
33
+
34
+ The model is built directly from feature-model objects (features, relations and cross-tree
35
+ constraints) — no serialization is involved. Serialize it to UVL (or any format) afterwards
36
+ with the corresponding writer transformation if needed. Useful to build corpora for
37
+ benchmarking, testing, or training learning-based operations.
38
+
39
+ ``language_level`` selects the UVL expressiveness of the generated model: ``'boolean'`` (tree +
40
+ requires/excludes constraints), ``'arithmetic'`` (features gain a numeric attribute and the
41
+ model gets arithmetic cross-tree constraints such as ``F1.value + F2.value > 40``), or
42
+ ``'typed'`` (some features are typed Integer/Real/String). When ``void`` is set, a contradictory
43
+ boolean constraint is added so the resulting model is unsatisfiable.
44
+
45
+ The operation produces a model, so ``execute`` ignores its (optional) input model.
46
+ """
47
+
48
+ facade = OperationDescriptor(
49
+ doc=(
50
+ 'Generates a random synthetic feature model and returns it as a FeatureModel.\n'
51
+ '``num_features`` (>= 2) sizes the tree, ``max_constraints`` bounds the number of\n'
52
+ 'cross-tree constraints, ``seed`` makes it reproducible, ``void=True`` forces an\n'
53
+ 'unsatisfiable model, and ``language_level`` picks the UVL level to target:\n'
54
+ "'boolean', 'arithmetic' (numeric attributes + arithmetic constraints) or 'typed'\n"
55
+ '(Integer/Real/String features). Useful for building corpora, testing, or training.'
56
+ ),
57
+ returns='FeatureModel',
58
+ name='generate_random_feature_model', operation='GenerateRandomFeatureModel',
59
+ kind='producer',
60
+ inputs=(
61
+ Input('num_features', int, default=10, setter='set_num_features'),
62
+ Input('max_constraints', int, default=3, setter='set_max_constraints'),
63
+ Input('seed', int, default=0, setter='set_seed'),
64
+ Input('void', bool, default=False, setter='set_void'),
65
+ Input('language_level', str, default='boolean', setter='set_language_level'),
66
+ ),
67
+ )
68
+
69
+ def __init__(self) -> None:
70
+ self.result: FeatureModel
71
+ self._num_features: int = 10
72
+ self._max_constraints: int = 3
73
+ self._seed: int = 0
74
+ self._void: bool = False
75
+ self._language_level: str = 'boolean'
76
+
77
+ def get_result(self) -> FeatureModel:
78
+ return self.result
79
+
80
+ def set_num_features(self, num_features: int) -> None:
81
+ self._num_features = max(2, num_features)
82
+
83
+ def set_max_constraints(self, max_constraints: int) -> None:
84
+ self._max_constraints = max(0, max_constraints)
85
+
86
+ def set_seed(self, seed: int) -> None:
87
+ self._seed = seed
88
+
89
+ def set_void(self, void: bool) -> None:
90
+ self._void = void
91
+
92
+ def set_language_level(self, language_level: str) -> None:
93
+ level = language_level.lower()
94
+ if level not in _LANGUAGE_LEVELS:
95
+ raise FlamaException(
96
+ f"Unknown language level '{language_level}'. "
97
+ f"Choose from {list(_LANGUAGE_LEVELS)}.")
98
+ self._language_level = level
99
+
100
+ def _build_tree(self, rng: random.Random) -> Feature:
101
+ names = [f'F{i}' for i in range(self._num_features)]
102
+ features = {name: Feature(name, is_abstract=(index == 0))
103
+ for index, name in enumerate(names)}
104
+
105
+ assignments: dict[str, list[str]] = {name: [] for name in names}
106
+ for name in names[1:]:
107
+ parent = rng.choice(names[: names.index(name)])
108
+ assignments[parent].append(name)
109
+
110
+ for parent_name, kids in assignments.items():
111
+ if not kids:
112
+ continue
113
+ parent = features[parent_name]
114
+ kid_features = [features[kid] for kid in kids]
115
+ for kid in kid_features:
116
+ kid.parent = parent
117
+ group = rng.choice(['mandatory', 'optional', 'or', 'alternative'])
118
+ if group in ('or', 'alternative') and len(kids) < _MIN_GROUP_MEMBERS:
119
+ group = 'optional'
120
+ if group == 'mandatory':
121
+ parent.relations.extend(Relation(parent, [kid], 1, 1) for kid in kid_features)
122
+ elif group == 'optional':
123
+ parent.relations.extend(Relation(parent, [kid], 0, 1) for kid in kid_features)
124
+ elif group == 'or':
125
+ parent.relations.append(Relation(parent, kid_features, 1, len(kid_features)))
126
+ else: # alternative
127
+ parent.relations.append(Relation(parent, kid_features, 1, 1))
128
+ return features[names[0]]
129
+
130
+ def _apply_types(self, root: Feature, rng: random.Random) -> None:
131
+ """Give a random subset of non-root features a non-boolean type (=> TYPE level)."""
132
+ candidates = [feature for feature in _descendants(root) if feature is not root]
133
+ if not candidates:
134
+ return
135
+ count = max(1, len(candidates) // 3)
136
+ for feature in rng.sample(candidates, min(count, len(candidates))):
137
+ feature.feature_type = rng.choice(_TYPED_FEATURE_TYPES)
138
+
139
+ def _apply_attributes(self, root: Feature, rng: random.Random) -> None:
140
+ """Add a numeric attribute to non-root features for arithmetic constraints to reference."""
141
+ for feature in _descendants(root):
142
+ if feature is root:
143
+ continue
144
+ domain = Domain([Range(0, 100)], None)
145
+ feature.add_attribute(
146
+ Attribute(_ARITHMETIC_ATTRIBUTE, domain, rng.randint(0, 100)))
147
+
148
+ def _build_boolean_constraints(
149
+ self, non_root: list[str], rng: random.Random
150
+ ) -> list[Constraint]:
151
+ constraints: list[Constraint] = []
152
+ if len(non_root) >= _MIN_GROUP_MEMBERS and self._max_constraints > 0:
153
+ for index in range(rng.randint(1, self._max_constraints)):
154
+ left, right = rng.sample(non_root, 2)
155
+ operation = rng.choice([ASTOperation.REQUIRES, ASTOperation.EXCLUDES])
156
+ constraints.append(
157
+ Constraint(f'ctc{index}',
158
+ AST.create_simple_binary_operation(operation, left, right))
159
+ )
160
+ return constraints
161
+
162
+ def _build_arithmetic_constraints(
163
+ self, non_root: list[str], rng: random.Random
164
+ ) -> list[Constraint]:
165
+ """Constraints like ``F1.value + F2.value > 40`` (=> ARITHMETIC level). At least one."""
166
+ constraints: list[Constraint] = []
167
+ if len(non_root) < _MIN_GROUP_MEMBERS:
168
+ return constraints
169
+ arithmetic_ops = [ASTOperation.ADD, ASTOperation.SUB, ASTOperation.MUL]
170
+ comparisons = [ASTOperation.GREATER, ASTOperation.LOWER, ASTOperation.GREATER_EQUALS,
171
+ ASTOperation.LOWER_EQUALS, ASTOperation.EQUALS]
172
+ for index in range(rng.randint(1, max(1, self._max_constraints))):
173
+ left, right = rng.sample(non_root, 2)
174
+ expression = Node(
175
+ rng.choice(arithmetic_ops),
176
+ Node(f'{left}.{_ARITHMETIC_ATTRIBUTE}', node_type=NodeType.FEATURE),
177
+ Node(f'{right}.{_ARITHMETIC_ATTRIBUTE}', node_type=NodeType.FEATURE))
178
+ comparison = Node(
179
+ rng.choice(comparisons), expression,
180
+ Node(rng.randint(1, 200), node_type=NodeType.LITERAL))
181
+ constraints.append(Constraint(f'arithmetic_ctc{index}', AST(comparison)))
182
+ return constraints
183
+
184
+ def _build_void_constraints(
185
+ self, root: Feature, non_root: list[str], rng: random.Random
186
+ ) -> list[Constraint]:
187
+ # Force a feature to be present (required by the always-selected root) and absent.
188
+ forced = rng.choice(non_root)
189
+ return [
190
+ Constraint('void_requires',
191
+ AST.create_simple_binary_operation(
192
+ ASTOperation.REQUIRES, root.name, forced)),
193
+ Constraint('void_excludes',
194
+ AST.create_simple_unary_operation(ASTOperation.NOT, forced)),
195
+ ]
196
+
197
+ def _build_constraints(self, root: Feature, rng: random.Random) -> list[Constraint]:
198
+ non_root = [feature.name for feature in _descendants(root) if feature is not root]
199
+ constraints = self._build_boolean_constraints(non_root, rng)
200
+ if self._language_level == 'arithmetic':
201
+ constraints.extend(self._build_arithmetic_constraints(non_root, rng))
202
+ if self._void and non_root:
203
+ constraints.extend(self._build_void_constraints(root, non_root, rng))
204
+ return constraints
205
+
206
+ def execute(self, model: Optional[VariabilityModel] = None) -> 'GenerateRandomFeatureModel':
207
+ rng = random.Random(self._seed)
208
+ root = self._build_tree(rng)
209
+ if self._language_level == 'typed':
210
+ self._apply_types(root, rng)
211
+ elif self._language_level == 'arithmetic':
212
+ self._apply_attributes(root, rng)
213
+ constraints = self._build_constraints(root, rng)
214
+ self.result = FeatureModel(root, constraints)
215
+ return self
216
+
217
+
218
+ def _descendants(feature: Feature) -> list[Feature]:
219
+ collected = [feature]
220
+ for relation in feature.relations:
221
+ for child in relation.children:
222
+ collected.extend(_descendants(child))
223
+ return collected
@@ -4,6 +4,7 @@ from typing import cast
4
4
 
5
5
  from flamapy.core.models import VariabilityModel, ASTOperation
6
6
  from flamapy.core.operations import Operation
7
+ from flamapy.core.operations.descriptor import OperationDescriptor
7
8
 
8
9
  from flamapy.metamodels.fm_metamodel.models import FeatureModel, Constraint
9
10
 
@@ -34,6 +35,14 @@ class LanguageLevel:
34
35
  class FMLanguageLevel(Operation):
35
36
  """Operation to calculate the maximum language level of a feature model."""
36
37
 
38
+ facade = OperationDescriptor(
39
+ doc=(
40
+ 'Return the UVL language level.'
41
+ ),
42
+ returns='Union[None, Any]',
43
+ name='language_level', operation='FMLanguageLevel'
44
+ )
45
+
37
46
  def __init__(self) -> None:
38
47
  self.result: LanguageLevel = LanguageLevel(MajorLevel.BOOLEAN, set())
39
48
 
@@ -0,0 +1,47 @@
1
+ from typing import Any, cast
2
+
3
+ from flamapy.core.models import VariabilityModel
4
+ from flamapy.core.operations import Operation
5
+ from flamapy.core.operations.descriptor import OperationDescriptor
6
+ from flamapy.metamodels.fm_metamodel.models import FeatureModel, Feature
7
+
8
+
9
+ def _leaf_features_result(result: Any) -> Any:
10
+ return [feature.name for feature in result]
11
+
12
+
13
+ class FMLeafFeatures(Operation):
14
+ """
15
+ This operation returns the list of features that are leaves of the feature model tree
16
+ (i.e., they have not children).
17
+ """
18
+
19
+ facade = OperationDescriptor(
20
+ doc=(
21
+ 'This operation is used to find leaf features in a model: It returns the leaf\n'
22
+ 'features if they are found in the model. If the model does not follow the UVL\n'
23
+ 'specification, an exception is raised and the operation returns False.\n'
24
+ '\n'
25
+ 'Traditionally you would use the flama tool by features =\n'
26
+ "discover_metamodel.use_operation_from_file('OperationString', model) however,\n"
27
+ 'in this tool we know that this operation is from the fm metamodel, so we avoid\n'
28
+ 'to execute the transformation if possible'
29
+ ),
30
+ returns='Union[None, List[str]]',
31
+ name='leaf_features', operation='FMLeafFeatures', result_adapter=_leaf_features_result
32
+ )
33
+
34
+ def __init__(self) -> None:
35
+ self.result: list[Feature] = []
36
+
37
+ def get_result(self) -> list[Feature]:
38
+ return self.result
39
+
40
+ def execute(self, model: VariabilityModel) -> 'FMLeafFeatures':
41
+ fm_model = cast(FeatureModel, model)
42
+ self.result = get_leaf_features(fm_model)
43
+ return self
44
+
45
+
46
+ def get_leaf_features(feature_model: FeatureModel) -> list[Feature]:
47
+ return [f for f in feature_model.get_features() if len(f.get_relations()) == 0]
@@ -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 Operation
5
+ from flamapy.core.operations.descriptor import OperationDescriptor
5
6
  from flamapy.metamodels.fm_metamodel.models import FeatureModel
6
7
  from flamapy.metamodels.fm_metamodel.operations.fm_feature_ancestors import get_feature_ancestors
7
8
  from flamapy.metamodels.fm_metamodel.operations.fm_leaf_features import get_leaf_features
@@ -10,6 +11,16 @@ from flamapy.metamodels.fm_metamodel.operations.fm_leaf_features import get_leaf
10
11
  class FMMaxDepthTree(Operation):
11
12
  """This operation returns the maximum depth of the feature model tree."""
12
13
 
14
+ facade = OperationDescriptor(
15
+ doc=(
16
+ 'This operation is used to find the max depth of the tree in a model: It\n'
17
+ 'returns the max depth of the tree. If the model does not follow the UVL\n'
18
+ 'specification, an exception is raised and the operation returns False.'
19
+ ),
20
+ returns='Union[None, int]',
21
+ name='max_depth', operation='FMMaxDepthTree'
22
+ )
23
+
13
24
  def __init__(self) -> None:
14
25
  self.result = 0
15
26
 
@@ -4,6 +4,7 @@ import statistics
4
4
  from flamapy.core.exceptions import FlamaException
5
5
  from flamapy.core.models.variability_model import VariabilityModel
6
6
  from flamapy.core.operations.metrics_operation import Metrics
7
+ from flamapy.core.operations.descriptor import OperationDescriptor
7
8
  from flamapy.metamodels.fm_metamodel.models import FeatureModel, Feature
8
9
  from flamapy.metamodels.fm_metamodel.operations import FMAverageBranchingFactor
9
10
 
@@ -26,6 +27,15 @@ def metric_method(func: Callable[..., dict[str, Any]]) -> Callable[..., dict[str
26
27
 
27
28
  class FMMetrics(Metrics): # pylint: disable=too-many-instance-attributes
28
29
 
30
+ facade = OperationDescriptor(
31
+ doc=(
32
+ 'Returns a collection of structural metrics for the feature model, such as\n'
33
+ 'number of features, constraints, relations, and complexity indicators.'
34
+ ),
35
+ returns='Union[None, List[Dict[str, Any]]]',
36
+ name='metrics', operation='FMMetrics'
37
+ )
38
+
29
39
  def __init__(self) -> None:
30
40
  super().__init__()
31
41
  self.model: Optional[FeatureModel] = None
@@ -1,11 +1,27 @@
1
- from typing import cast
1
+ from typing import Any, cast
2
2
 
3
3
  from flamapy.core.models import VariabilityModel
4
+ from flamapy.core.operations.descriptor import OperationDescriptor
4
5
  from flamapy.metamodels.fm_metamodel.operations.interfaces import VariationPoints
5
6
  from flamapy.metamodels.fm_metamodel.models import FeatureModel, Feature
6
7
 
7
8
 
9
+ def _variation_points_result(result: Any) -> Any:
10
+ return {vp.name: [variant.name for variant in variants] for vp, variants in result.items()}
11
+
12
+
8
13
  class FMVariationPoints(VariationPoints):
14
+ facade = OperationDescriptor(
15
+ doc=(
16
+ 'Returns the variation points of the feature model. A variation point is a\n'
17
+ 'feature that has at least one non-mandatory child, representing a decision\n'
18
+ 'point in the model. The result maps each variation point name to the list of\n'
19
+ 'its variant feature names.'
20
+ ),
21
+ returns='Union[None, Dict[str, List[str]]]',
22
+ name='variation_points', operation='FMVariationPoints',
23
+ result_adapter=_variation_points_result,
24
+ )
9
25
 
10
26
  def __init__(self) -> None:
11
27
  self.result: dict[Feature, list[Feature]] = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flamapy-fm
3
- Version: 2.6.0.dev7
3
+ Version: 2.6.0.dev12
4
4
  Summary: flamapy-fm is a plugin to Flamapy module
5
5
  Author-email: Flamapy <flamapy@us.es>
6
6
  License-Expression: GPL-3.0-or-later
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/flamapy/fm_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.dev5
11
+ Requires-Dist: flamapy-fw~=2.6.0.dev12
12
12
  Requires-Dist: uvlparser~=2.5.0
13
13
  Requires-Dist: afmparser~=1.0.3
14
14
  Provides-Extra: dev
@@ -33,3 +33,24 @@ FeatureIDE, Glencoe, AFM, SPLOT, JSON, …).
33
33
  ```bash
34
34
  pip install flamapy-fm
35
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
+ ```
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  setup.py
5
5
  flamapy/metamodels/fm_metamodel/__init__.py
6
6
  flamapy/metamodels/fm_metamodel/models/__init__.py
7
+ flamapy/metamodels/fm_metamodel/models/clause_set.py
7
8
  flamapy/metamodels/fm_metamodel/models/feature_model.py
8
9
  flamapy/metamodels/fm_metamodel/operations/__init__.py
9
10
  flamapy/metamodels/fm_metamodel/operations/fm_atomic_sets.py
@@ -1,4 +1,4 @@
1
- flamapy-fw~=2.6.0.dev5
1
+ flamapy-fw~=2.6.0.dev12
2
2
  uvlparser~=2.5.0
3
3
  afmparser~=1.0.3
4
4
 
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flamapy-fm"
7
- version = "2.6.0.dev7"
7
+ version = "2.6.0.dev12"
8
8
  description = "flamapy-fm 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.dev5",
14
+ "flamapy-fw~=2.6.0.dev12",
15
15
  "uvlparser~=2.5.0",
16
16
  "afmparser~=1.0.3",
17
17
  ]
@@ -1,13 +0,0 @@
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
- ```
@@ -1,100 +0,0 @@
1
- import os
2
- import random
3
- import tempfile
4
- from typing import Optional
5
-
6
- from flamapy.core.models import VariabilityModel
7
- from flamapy.core.operations import Operation
8
- from flamapy.metamodels.fm_metamodel.models import FeatureModel
9
- from flamapy.metamodels.fm_metamodel.transformations import UVLReader
10
-
11
- # Minimum children an or/alternative group needs (and minimum features for a cross-tree constraint).
12
- _MIN_GROUP_MEMBERS = 2
13
-
14
-
15
- class GenerateRandomFeatureModel(Operation):
16
- """Generate a random synthetic feature model.
17
-
18
- A random feature tree (mandatory/optional/or/alternative groups) plus random cross-tree
19
- constraints are emitted as UVL and parsed back into a :class:`FeatureModel`. Useful to build
20
- corpora for benchmarking, testing, or training learning-based operations. When ``void`` is
21
- set, a contradictory constraint is added so the resulting model is unsatisfiable.
22
-
23
- The operation produces a model, so ``execute`` ignores its (optional) input model.
24
- """
25
-
26
- def __init__(self) -> None:
27
- self.result: FeatureModel
28
- self._num_features: int = 10
29
- self._max_constraints: int = 3
30
- self._seed: int = 0
31
- self._void: bool = False
32
-
33
- def get_result(self) -> FeatureModel:
34
- return self.result
35
-
36
- def set_num_features(self, num_features: int) -> None:
37
- self._num_features = max(2, num_features)
38
-
39
- def set_max_constraints(self, max_constraints: int) -> None:
40
- self._max_constraints = max(0, max_constraints)
41
-
42
- def set_seed(self, seed: int) -> None:
43
- self._seed = seed
44
-
45
- def set_void(self, void: bool) -> None:
46
- self._void = void
47
-
48
- def _emit_uvl(self, rng: random.Random) -> str:
49
- names = [f'F{i}' for i in range(self._num_features)]
50
- root = names[0]
51
- lines = ['features', f' {root} {{abstract}}']
52
-
53
- children: dict[str, list[str]] = {name: [] for name in names}
54
- for name in names[1:]:
55
- parent = rng.choice(names[: names.index(name)])
56
- children[parent].append(name)
57
-
58
- def emit(parent: str, depth: int) -> None:
59
- kids = children[parent]
60
- if not kids:
61
- return
62
- indent = ' ' * (depth + 1)
63
- group = rng.choice(['mandatory', 'optional', 'or', 'alternative'])
64
- if group in ('or', 'alternative') and len(kids) < _MIN_GROUP_MEMBERS:
65
- group = 'optional'
66
- lines.append(f'{indent}{group}')
67
- lines.extend(f'{indent} {kid}' for kid in kids)
68
- for kid in kids:
69
- emit(kid, depth + 2)
70
-
71
- emit(root, 1)
72
-
73
- constraints = []
74
- non_root = names[1:]
75
- if len(non_root) >= _MIN_GROUP_MEMBERS and self._max_constraints > 0:
76
- for _ in range(rng.randint(1, self._max_constraints)):
77
- left, right = rng.sample(non_root, 2)
78
- constraints.append(
79
- rng.choice([f'{left} => {right}', f'!{left} | {right}', f'!{left} | !{right}'])
80
- )
81
- if self._void and non_root:
82
- forced = rng.choice(non_root)
83
- constraints.append(f'{root} => {forced}')
84
- constraints.append(f'!{forced}')
85
- if constraints:
86
- lines.append('constraints')
87
- lines.extend(f' {constraint}' for constraint in constraints)
88
- return '\n'.join(lines) + '\n'
89
-
90
- def execute(self, model: Optional[VariabilityModel] = None) -> 'GenerateRandomFeatureModel':
91
- rng = random.Random(self._seed)
92
- uvl = self._emit_uvl(rng)
93
- handle, path = tempfile.mkstemp(suffix='.uvl')
94
- try:
95
- with os.fdopen(handle, 'w') as file:
96
- file.write(uvl)
97
- self.result = UVLReader(path).transform()
98
- finally:
99
- os.remove(path)
100
- return self
@@ -1,27 +0,0 @@
1
- from typing import cast
2
-
3
- from flamapy.core.models import VariabilityModel
4
- from flamapy.core.operations import Operation
5
- from flamapy.metamodels.fm_metamodel.models import FeatureModel, Feature
6
-
7
-
8
- class FMLeafFeatures(Operation):
9
- """
10
- This operation returns the list of features that are leaves of the feature model tree
11
- (i.e., they have not children).
12
- """
13
-
14
- def __init__(self) -> None:
15
- self.result: list[Feature] = []
16
-
17
- def get_result(self) -> list[Feature]:
18
- return self.result
19
-
20
- def execute(self, model: VariabilityModel) -> 'FMLeafFeatures':
21
- fm_model = cast(FeatureModel, model)
22
- self.result = get_leaf_features(fm_model)
23
- return self
24
-
25
-
26
- def get_leaf_features(feature_model: FeatureModel) -> list[Feature]:
27
- return [f for f in feature_model.get_features() if len(f.get_relations()) == 0]