flamapy-fm 2.0.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flamapy/metamodels/fm_metamodel/__init__.py +0 -0
- flamapy/metamodels/fm_metamodel/models/__init__.py +17 -0
- flamapy/metamodels/fm_metamodel/models/feature_model.py +606 -0
- flamapy/metamodels/fm_metamodel/operations/__init__.py +20 -0
- flamapy/metamodels/fm_metamodel/operations/fm_atomic_sets.py +47 -0
- flamapy/metamodels/fm_metamodel/operations/fm_average_branching_factor.py +36 -0
- flamapy/metamodels/fm_metamodel/operations/fm_core_features.py +71 -0
- flamapy/metamodels/fm_metamodel/operations/fm_count_leafs.py +26 -0
- flamapy/metamodels/fm_metamodel/operations/fm_estimated_configurations_number.py +55 -0
- flamapy/metamodels/fm_metamodel/operations/fm_feature_ancestors.py +38 -0
- flamapy/metamodels/fm_metamodel/operations/fm_generate_random_attribute.py +100 -0
- flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +27 -0
- flamapy/metamodels/fm_metamodel/operations/fm_max_depth_tree.py +26 -0
- flamapy/metamodels/fm_metamodel/operations/fm_metrics.py +770 -0
- flamapy/metamodels/fm_metamodel/transformations/__init__.py +28 -0
- flamapy/metamodels/fm_metamodel/transformations/afm_reader.py +229 -0
- flamapy/metamodels/fm_metamodel/transformations/afm_writer.py +140 -0
- flamapy/metamodels/fm_metamodel/transformations/clafer_writer.py +151 -0
- flamapy/metamodels/fm_metamodel/transformations/featureide_reader.py +185 -0
- flamapy/metamodels/fm_metamodel/transformations/featureide_writer.py +138 -0
- flamapy/metamodels/fm_metamodel/transformations/glencoe_reader.py +136 -0
- flamapy/metamodels/fm_metamodel/transformations/glencoe_writer.py +105 -0
- flamapy/metamodels/fm_metamodel/transformations/json_reader.py +145 -0
- flamapy/metamodels/fm_metamodel/transformations/json_writer.py +125 -0
- flamapy/metamodels/fm_metamodel/transformations/pysat_to_fm.py +69 -0
- flamapy/metamodels/fm_metamodel/transformations/splot_writer.py +75 -0
- flamapy/metamodels/fm_metamodel/transformations/uvl_reader.py +427 -0
- flamapy/metamodels/fm_metamodel/transformations/uvl_writer.py +134 -0
- flamapy/metamodels/fm_metamodel/transformations/xml_reader.py +128 -0
- flamapy_fm-2.0.0.dev1.dist-info/METADATA +37 -0
- flamapy_fm-2.0.0.dev1.dist-info/RECORD +34 -0
- flamapy_fm-2.0.0.dev1.dist-info/WHEEL +5 -0
- flamapy_fm-2.0.0.dev1.dist-info/dependency_links.txt +1 -0
- flamapy_fm-2.0.0.dev1.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
from functools import total_ordering
|
|
3
|
+
|
|
4
|
+
from flamapy.core.exceptions import FlamaException
|
|
5
|
+
from flamapy.core.models import AST, VariabilityModel, VariabilityElement, ASTOperation
|
|
6
|
+
from flamapy.core.models.ast import simplify_formula, propagate_negation, to_cnf
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Relation:
|
|
10
|
+
def __init__(
|
|
11
|
+
self, parent: "Feature", children: list["Feature"], card_min: int, card_max: int
|
|
12
|
+
) -> None:
|
|
13
|
+
self.parent = parent
|
|
14
|
+
self.children = children
|
|
15
|
+
self.card_min = card_min
|
|
16
|
+
self.card_max = card_max
|
|
17
|
+
|
|
18
|
+
def add_child(self, feature: "Feature") -> None:
|
|
19
|
+
self.children.append(feature)
|
|
20
|
+
|
|
21
|
+
def is_mandatory(self) -> bool:
|
|
22
|
+
return self.card_min == 1 and self.card_max == 1 and len(self.children) == 1
|
|
23
|
+
|
|
24
|
+
def is_optional(self) -> bool:
|
|
25
|
+
return self.card_min == 0 and self.card_max == 1 and len(self.children) == 1
|
|
26
|
+
|
|
27
|
+
def is_or(self) -> bool:
|
|
28
|
+
return (
|
|
29
|
+
self.card_min == 1
|
|
30
|
+
and self.card_max == len(self.children)
|
|
31
|
+
and len(self.children) > 1
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def is_alternative(self) -> bool:
|
|
35
|
+
return self.card_min == 1 and self.card_max == 1 and len(self.children) > 1
|
|
36
|
+
|
|
37
|
+
def is_mutex(self) -> bool:
|
|
38
|
+
return self.card_min == 0 and self.card_max == 1 and len(self.children) > 1
|
|
39
|
+
|
|
40
|
+
def is_cardinal(self) -> bool:
|
|
41
|
+
return (
|
|
42
|
+
self.is_group()
|
|
43
|
+
and not self.is_alternative()
|
|
44
|
+
and not self.is_or()
|
|
45
|
+
and not self.is_mutex()
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def is_group(self) -> bool:
|
|
49
|
+
return len(self.children) > 1
|
|
50
|
+
|
|
51
|
+
def __str__(self) -> str:
|
|
52
|
+
parent_name = self.parent.name if self.parent else ""
|
|
53
|
+
res = f"{parent_name}[{self.card_min},{self.card_max}]"
|
|
54
|
+
for _child in self.children:
|
|
55
|
+
res += _child.name + " "
|
|
56
|
+
if self.is_alternative():
|
|
57
|
+
relation_type = "alternative"
|
|
58
|
+
elif self.is_or():
|
|
59
|
+
relation_type = "or"
|
|
60
|
+
elif self.is_mandatory():
|
|
61
|
+
relation_type = "mandatory"
|
|
62
|
+
elif self.is_optional():
|
|
63
|
+
relation_type = "optional"
|
|
64
|
+
elif self.is_mutex():
|
|
65
|
+
relation_type = "mutex"
|
|
66
|
+
elif self.is_cardinal():
|
|
67
|
+
relation_type = "cardinality"
|
|
68
|
+
else:
|
|
69
|
+
relation_type = "Other"
|
|
70
|
+
res = f"({relation_type}) " + res
|
|
71
|
+
return res
|
|
72
|
+
|
|
73
|
+
def __hash__(self) -> int:
|
|
74
|
+
return hash(
|
|
75
|
+
(self.parent, frozenset(self.children), self.card_min, self.card_max)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def __eq__(self, other: Any) -> bool:
|
|
79
|
+
return (
|
|
80
|
+
isinstance(other, Relation)
|
|
81
|
+
and self.parent == other.parent
|
|
82
|
+
and sorted(self.children) == sorted(other.children)
|
|
83
|
+
and self.card_min == other.card_min
|
|
84
|
+
and self.card_max == other.card_max
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def __lt__(self, other: Any) -> bool:
|
|
88
|
+
return str(self) < str(other)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@total_ordering
|
|
92
|
+
class Feature(VariabilityElement):
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
name: str,
|
|
97
|
+
relations: Optional[list["Relation"]] = None,
|
|
98
|
+
parent: Optional["Feature"] = None,
|
|
99
|
+
is_abstract: bool = False,
|
|
100
|
+
):
|
|
101
|
+
super().__init__(name)
|
|
102
|
+
self.name = name
|
|
103
|
+
self.relations = [] if relations is None else relations
|
|
104
|
+
self.parent = self._get_parent() if parent is None else parent
|
|
105
|
+
self.is_abstract = is_abstract
|
|
106
|
+
self.attributes = list["Attribute"]([])
|
|
107
|
+
|
|
108
|
+
def is_empty(self) -> bool:
|
|
109
|
+
return self.parent is None and self.relations == []
|
|
110
|
+
|
|
111
|
+
def add_relation(self, relation: "Relation") -> None:
|
|
112
|
+
self.relations.append(relation)
|
|
113
|
+
for child in relation.children:
|
|
114
|
+
child.parent = self
|
|
115
|
+
|
|
116
|
+
def add_attribute(self, attribute: "Attribute") -> None:
|
|
117
|
+
attribute.parent = self
|
|
118
|
+
self.attributes.append(attribute)
|
|
119
|
+
|
|
120
|
+
def get_attributes(self) -> list["Attribute"]:
|
|
121
|
+
return self.attributes
|
|
122
|
+
|
|
123
|
+
def set_attributes(self, attributes: list["Attribute"]) -> None:
|
|
124
|
+
self.attributes = attributes
|
|
125
|
+
|
|
126
|
+
def get_relations(self) -> list["Relation"]:
|
|
127
|
+
return self.relations
|
|
128
|
+
|
|
129
|
+
def get_parent(self) -> Optional["Feature"]:
|
|
130
|
+
return self.parent
|
|
131
|
+
|
|
132
|
+
def _get_parent(self) -> Optional["Feature"]:
|
|
133
|
+
return next((r.parent for r in self.get_relations() if not r.children), None)
|
|
134
|
+
|
|
135
|
+
def get_children(self) -> list["Feature"]:
|
|
136
|
+
"""Direct children of the feature regardless the relation type."""
|
|
137
|
+
return [f for r in self.get_relations() for f in r.children]
|
|
138
|
+
|
|
139
|
+
def is_root(self) -> bool:
|
|
140
|
+
return self.parent is None
|
|
141
|
+
|
|
142
|
+
def is_mandatory(self) -> bool:
|
|
143
|
+
return self.parent is not None and any(
|
|
144
|
+
r.is_mandatory() and self in r.children for r in self.parent.get_relations()
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def is_optional(self) -> bool:
|
|
148
|
+
return self.parent is not None and any(
|
|
149
|
+
r.is_optional() and self in r.children for r in self.parent.get_relations()
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def is_or_group(self) -> bool:
|
|
153
|
+
return any(r.is_or() for r in self.get_relations())
|
|
154
|
+
|
|
155
|
+
def is_alternative_group(self) -> bool:
|
|
156
|
+
return any(r.is_alternative() for r in self.get_relations())
|
|
157
|
+
|
|
158
|
+
def is_mutex_group(self) -> bool:
|
|
159
|
+
return any(r.is_mutex() for r in self.get_relations())
|
|
160
|
+
|
|
161
|
+
def is_cardinality_group(self) -> bool:
|
|
162
|
+
return any(r.is_cardinal() for r in self.get_relations())
|
|
163
|
+
|
|
164
|
+
def is_group(self) -> bool:
|
|
165
|
+
return any(r.is_group() for r in self.get_relations())
|
|
166
|
+
|
|
167
|
+
def is_multiple_group_decomposition(self) -> bool:
|
|
168
|
+
return sum(r.is_group() for r in self.get_relations()) > 1
|
|
169
|
+
|
|
170
|
+
def is_leaf(self) -> bool:
|
|
171
|
+
return len(self.get_relations()) == 0
|
|
172
|
+
|
|
173
|
+
def __str__(self) -> str:
|
|
174
|
+
return self.name
|
|
175
|
+
|
|
176
|
+
def __hash__(self) -> int:
|
|
177
|
+
return hash(self.name)
|
|
178
|
+
|
|
179
|
+
def __eq__(self, other: Any) -> bool:
|
|
180
|
+
return isinstance(other, Feature) and self.name == other.name
|
|
181
|
+
|
|
182
|
+
def __lt__(self, other: Any) -> bool:
|
|
183
|
+
return str(self) < str(other)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class Constraint:
|
|
187
|
+
def __init__(self, name: str, ast: AST):
|
|
188
|
+
self.name = name
|
|
189
|
+
self._ast = ast
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def ast(self) -> AST:
|
|
193
|
+
return self._ast
|
|
194
|
+
|
|
195
|
+
@ast.setter
|
|
196
|
+
def ast(self, ast: AST) -> None:
|
|
197
|
+
self._ast = ast
|
|
198
|
+
|
|
199
|
+
def get_features(self) -> list[str]:
|
|
200
|
+
"""List of features' names involved in the constraint."""
|
|
201
|
+
features = set()
|
|
202
|
+
stack = [self.ast.root]
|
|
203
|
+
while stack:
|
|
204
|
+
node = stack.pop()
|
|
205
|
+
if node.is_unique_term():
|
|
206
|
+
features.add(node.data)
|
|
207
|
+
elif node.is_unary_op():
|
|
208
|
+
stack.append(node.left)
|
|
209
|
+
elif node.is_binary_op():
|
|
210
|
+
stack.append(node.right)
|
|
211
|
+
stack.append(node.left)
|
|
212
|
+
return list(features)
|
|
213
|
+
|
|
214
|
+
def is_simple_constraint(self) -> bool:
|
|
215
|
+
"""Return true if the constraint is a simple constraint (requires or excludes)."""
|
|
216
|
+
return self.is_requires_constraint() or self.is_excludes_constraint()
|
|
217
|
+
|
|
218
|
+
def is_complex_constraint(self) -> bool:
|
|
219
|
+
"""Return true if the constraint is a complex constraint
|
|
220
|
+
(i.e., it is not a simple constraint)."""
|
|
221
|
+
return not self.is_simple_constraint()
|
|
222
|
+
|
|
223
|
+
def is_requires_constraint(self) -> bool:
|
|
224
|
+
"""Return true if the constraint is a requires constraint."""
|
|
225
|
+
root_op = self._ast.root
|
|
226
|
+
if root_op.is_binary_op():
|
|
227
|
+
if root_op.data in [ASTOperation.REQUIRES, ASTOperation.IMPLIES]:
|
|
228
|
+
return root_op.left.is_term() and root_op.right.is_term()
|
|
229
|
+
|
|
230
|
+
if root_op.data == ASTOperation.OR:
|
|
231
|
+
neg_left = (
|
|
232
|
+
root_op.left.data == ASTOperation.NOT
|
|
233
|
+
and root_op.left.left.is_term()
|
|
234
|
+
)
|
|
235
|
+
neg_right = (
|
|
236
|
+
root_op.right.data == ASTOperation.NOT
|
|
237
|
+
and root_op.right.left.is_term()
|
|
238
|
+
)
|
|
239
|
+
return (
|
|
240
|
+
neg_left
|
|
241
|
+
and root_op.right.is_term()
|
|
242
|
+
or neg_right
|
|
243
|
+
and root_op.left.is_term()
|
|
244
|
+
)
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
def is_excludes_constraint(self) -> bool:
|
|
248
|
+
"""Return true if the constraint is an excludes constraint."""
|
|
249
|
+
root_op = self._ast.root
|
|
250
|
+
if root_op.is_binary_op():
|
|
251
|
+
if root_op.data in [ASTOperation.EXCLUDES, ASTOperation.XOR]:
|
|
252
|
+
return root_op.left.is_term() and root_op.right.is_term()
|
|
253
|
+
|
|
254
|
+
if root_op.data in [ASTOperation.REQUIRES, ASTOperation.IMPLIES]:
|
|
255
|
+
neg_right = (
|
|
256
|
+
root_op.right.data == ASTOperation.NOT
|
|
257
|
+
and root_op.right.left.is_term()
|
|
258
|
+
)
|
|
259
|
+
return root_op.left.is_term() and neg_right
|
|
260
|
+
|
|
261
|
+
if root_op.data == ASTOperation.OR:
|
|
262
|
+
neg_left = (
|
|
263
|
+
root_op.left.data == ASTOperation.NOT
|
|
264
|
+
and root_op.left.left.is_term()
|
|
265
|
+
)
|
|
266
|
+
neg_right = (
|
|
267
|
+
root_op.right.data == ASTOperation.NOT
|
|
268
|
+
and root_op.right.left.is_term()
|
|
269
|
+
)
|
|
270
|
+
return neg_left and neg_right
|
|
271
|
+
return False
|
|
272
|
+
|
|
273
|
+
def is_pseudocomplex_constraint(self) -> bool:
|
|
274
|
+
"""Return true if the constraint is a pseudo-complex constraint
|
|
275
|
+
(i.e., it can be transformed to a set of simple constraints)."""
|
|
276
|
+
split_ctcs = split_constraint(self)
|
|
277
|
+
return len(split_ctcs) > 1 and all(
|
|
278
|
+
self.is_simple_constraint() for ctc in split_ctcs
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def is_strictcomplex_constraint(self) -> bool:
|
|
282
|
+
"""Return true if the constraint is a strict-complex constraint
|
|
283
|
+
(i.e., it cannot be transformed to a set of simple constraints)."""
|
|
284
|
+
split_ctcs = split_constraint(self)
|
|
285
|
+
return any(self.is_complex_constraint() for ctc in split_ctcs)
|
|
286
|
+
|
|
287
|
+
def __str__(self) -> str:
|
|
288
|
+
return f"({self.name}) {str(self.ast)}"
|
|
289
|
+
|
|
290
|
+
def __hash__(self) -> int:
|
|
291
|
+
return hash(str(self.ast).lower())
|
|
292
|
+
|
|
293
|
+
def __eq__(self, other: Any) -> bool:
|
|
294
|
+
return (
|
|
295
|
+
isinstance(other, Constraint)
|
|
296
|
+
and str(self.ast).lower() == str(other.ast).lower()
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def __lt__(self, other: Any) -> bool:
|
|
300
|
+
return str(self.ast).lower() < str(other.ast).lower()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class FeatureModel(VariabilityModel):
|
|
304
|
+
@staticmethod
|
|
305
|
+
def get_extension() -> str:
|
|
306
|
+
return "fm"
|
|
307
|
+
|
|
308
|
+
def __init__(
|
|
309
|
+
self, root: "Feature", constraints: Optional[list["Constraint"]] = None
|
|
310
|
+
) -> None:
|
|
311
|
+
self.root = root
|
|
312
|
+
self.ctcs = [] if constraints is None else constraints
|
|
313
|
+
|
|
314
|
+
def get_relations(self, feature: Optional["Feature"] = None) -> list["Relation"]:
|
|
315
|
+
if self.root is None or self.root.is_empty():
|
|
316
|
+
return []
|
|
317
|
+
if feature is None:
|
|
318
|
+
feature = self.root
|
|
319
|
+
relations = []
|
|
320
|
+
for relation in feature.relations:
|
|
321
|
+
relations.append(relation)
|
|
322
|
+
for _feature in relation.children:
|
|
323
|
+
relations.extend(self.get_relations(_feature))
|
|
324
|
+
return relations
|
|
325
|
+
|
|
326
|
+
def get_features(self) -> list["Feature"]:
|
|
327
|
+
features: list["Feature"] = []
|
|
328
|
+
if self.root is not None:
|
|
329
|
+
features.append(self.root)
|
|
330
|
+
for relation in self.get_relations():
|
|
331
|
+
features.extend(relation.children)
|
|
332
|
+
return features
|
|
333
|
+
|
|
334
|
+
def get_constraints(self) -> list["Constraint"]:
|
|
335
|
+
return self.ctcs
|
|
336
|
+
|
|
337
|
+
def get_mandatory_features(self) -> list["Feature"]:
|
|
338
|
+
return [f for f in self.get_features() if f.is_mandatory()]
|
|
339
|
+
|
|
340
|
+
def get_optional_features(self) -> list["Feature"]:
|
|
341
|
+
return [f for f in self.get_features() if f.is_optional()]
|
|
342
|
+
|
|
343
|
+
def get_alternative_group_features(self) -> list["Feature"]:
|
|
344
|
+
return [f for f in self.get_features() if f.is_alternative_group()]
|
|
345
|
+
|
|
346
|
+
def get_or_group_features(self) -> list["Feature"]:
|
|
347
|
+
return [f for f in self.get_features() if f.is_or_group()]
|
|
348
|
+
|
|
349
|
+
def get_feature_by_name(self, feature_name: str) -> Optional["Feature"]:
|
|
350
|
+
return next((f for f in self.get_features() if f.name == feature_name), None)
|
|
351
|
+
|
|
352
|
+
def get_complex_constraints(self) -> list["Constraint"]:
|
|
353
|
+
return [c for c in self.get_constraints() if c.is_complex_constraint()]
|
|
354
|
+
|
|
355
|
+
def get_simple_constraints(self) -> list["Constraint"]:
|
|
356
|
+
return [c for c in self.get_constraints() if c.is_simple_constraint()]
|
|
357
|
+
|
|
358
|
+
def get_pseudocomplex_constraints(self) -> list["Constraint"]:
|
|
359
|
+
return [c for c in self.get_constraints() if c.is_pseudocomplex_constraint()]
|
|
360
|
+
|
|
361
|
+
def get_strictcomplex_constraints(self) -> list["Constraint"]:
|
|
362
|
+
return [c for c in self.get_constraints() if c.is_strictcomplex_constraint()]
|
|
363
|
+
|
|
364
|
+
def get_excludes_constraints(self) -> list["Constraint"]:
|
|
365
|
+
return [c for c in self.get_constraints() if c.is_excludes_constraint()]
|
|
366
|
+
|
|
367
|
+
def get_requires_constraints(self) -> list["Constraint"]:
|
|
368
|
+
return [c for c in self.get_constraints() if c.is_requires_constraint()]
|
|
369
|
+
|
|
370
|
+
def import_model(
|
|
371
|
+
self, root: Feature, parent: Feature, ctcs: list[Constraint]
|
|
372
|
+
) -> None:
|
|
373
|
+
root.parent = parent
|
|
374
|
+
for ctc in ctcs:
|
|
375
|
+
if ctc not in self.ctcs:
|
|
376
|
+
self.ctcs.append(ctc)
|
|
377
|
+
|
|
378
|
+
def __str__(self) -> str:
|
|
379
|
+
res = "root: " + ("None" if self.root is None else self.root.name) + "\r\n"
|
|
380
|
+
res += "Relations:\r\n"
|
|
381
|
+
for i, relation in enumerate(self.get_relations()):
|
|
382
|
+
res += f"R{i}: {relation}\r\n"
|
|
383
|
+
for i, ctc in enumerate(self.ctcs):
|
|
384
|
+
res += f"CTC{i}: {ctc}\r\n"
|
|
385
|
+
attributes_res = ""
|
|
386
|
+
for feature in self.get_features():
|
|
387
|
+
for attribute in feature.get_attributes():
|
|
388
|
+
attributes_res += f"{attribute}" + "\r\n"
|
|
389
|
+
if attributes_res != "":
|
|
390
|
+
res += "Attributes:\r\n" + attributes_res
|
|
391
|
+
return res
|
|
392
|
+
|
|
393
|
+
def __hash__(self) -> int:
|
|
394
|
+
return hash(
|
|
395
|
+
(
|
|
396
|
+
self.root,
|
|
397
|
+
frozenset(self.get_features()),
|
|
398
|
+
frozenset(self.get_relations()),
|
|
399
|
+
frozenset(self.ctcs),
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
def __eq__(self, other: Any) -> bool:
|
|
404
|
+
return (
|
|
405
|
+
isinstance(other, FeatureModel)
|
|
406
|
+
and self.root == other.root
|
|
407
|
+
and sorted(self.get_features()) == sorted(other.get_features())
|
|
408
|
+
and sorted(self.get_relations()) == sorted(other.get_relations())
|
|
409
|
+
and sorted(self.get_constraints()) == sorted(other.get_constraints())
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class Range:
|
|
414
|
+
def __init__(self, min_value: int, max_value: int):
|
|
415
|
+
self.min_value: int = min_value
|
|
416
|
+
self.max_value: int = max_value
|
|
417
|
+
|
|
418
|
+
def __str__(self) -> str:
|
|
419
|
+
return "[ " + str(self.min_value) + " to " + str(self.max_value) + "]"
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class Domain:
|
|
423
|
+
def __init__(
|
|
424
|
+
self, ranges: Optional[list["Range"]], elements: Optional[list["Any"]]
|
|
425
|
+
):
|
|
426
|
+
self.range_list = [] if ranges is None else ranges
|
|
427
|
+
self.element_list = [] if elements is None else elements
|
|
428
|
+
|
|
429
|
+
def get_range_list(self) -> list["Range"]:
|
|
430
|
+
return self.range_list
|
|
431
|
+
|
|
432
|
+
def get_element_list(self) -> list["Any"]:
|
|
433
|
+
return self.element_list
|
|
434
|
+
|
|
435
|
+
def add_range(self, new_range: Range) -> None:
|
|
436
|
+
self.range_list.append(new_range)
|
|
437
|
+
|
|
438
|
+
def add_element(self, element: Any) -> None:
|
|
439
|
+
self.element_list.append(element)
|
|
440
|
+
|
|
441
|
+
def set_range_list(self, range_list: list["Range"]) -> None:
|
|
442
|
+
self.range_list = range_list
|
|
443
|
+
|
|
444
|
+
def set_element_list(self, element_list: list["Any"]) -> None:
|
|
445
|
+
self.element_list = element_list
|
|
446
|
+
|
|
447
|
+
def __str__(self) -> str:
|
|
448
|
+
result = ""
|
|
449
|
+
element_list = self.element_list
|
|
450
|
+
if len(element_list) > 0:
|
|
451
|
+
result = str(element_list)
|
|
452
|
+
|
|
453
|
+
range_list = self.range_list
|
|
454
|
+
if len(range_list) > 0:
|
|
455
|
+
result = result + "Integer"
|
|
456
|
+
for rng in range_list:
|
|
457
|
+
result = result + str(rng)
|
|
458
|
+
|
|
459
|
+
return result
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class Attribute:
|
|
463
|
+
def __init__(
|
|
464
|
+
self,
|
|
465
|
+
name: str,
|
|
466
|
+
domain: Optional[Domain] = None,
|
|
467
|
+
default_value: Any = None,
|
|
468
|
+
null_value: Optional["Any"] = None,
|
|
469
|
+
):
|
|
470
|
+
self.name: "str" = name
|
|
471
|
+
self.parent: Optional["Feature"] = None
|
|
472
|
+
self.domain: Optional["Domain"] = domain
|
|
473
|
+
self.default_value: "Any" = default_value
|
|
474
|
+
self.null_value: Optional[Any] = null_value
|
|
475
|
+
|
|
476
|
+
def get_name(self) -> str:
|
|
477
|
+
return self.name
|
|
478
|
+
|
|
479
|
+
def get_parent(self) -> Optional["Feature"]:
|
|
480
|
+
return self.parent
|
|
481
|
+
|
|
482
|
+
def get_domain(self) -> Domain:
|
|
483
|
+
if self.domain is None:
|
|
484
|
+
raise FlamaException("Attribute domain is not defined")
|
|
485
|
+
return self.domain
|
|
486
|
+
|
|
487
|
+
def get_default_value(self) -> Any:
|
|
488
|
+
return self.default_value
|
|
489
|
+
|
|
490
|
+
def get_null_value(self) -> Any:
|
|
491
|
+
return self.null_value
|
|
492
|
+
|
|
493
|
+
def set_name(self, name: str) -> None:
|
|
494
|
+
self.name = name
|
|
495
|
+
|
|
496
|
+
def set_parent(self, parent: Feature) -> None:
|
|
497
|
+
self.parent = parent
|
|
498
|
+
|
|
499
|
+
def set_domain(self, domain: Domain) -> None:
|
|
500
|
+
self.domain = domain
|
|
501
|
+
|
|
502
|
+
def set_default_value(self, default_value: Any) -> None:
|
|
503
|
+
self.default_value = default_value
|
|
504
|
+
|
|
505
|
+
def set_null_value(self, null_value: Any) -> None:
|
|
506
|
+
self.null_value = null_value
|
|
507
|
+
|
|
508
|
+
def __str__(self) -> str:
|
|
509
|
+
if self.parent is None:
|
|
510
|
+
raise TypeError("self.parent is None, expected Feature type")
|
|
511
|
+
|
|
512
|
+
result = "[" + self.parent.name + "." + self.name + "]"
|
|
513
|
+
if self.domain is not None:
|
|
514
|
+
result = result + "Domain: " + str(self.domain)
|
|
515
|
+
if self.default_value is not None:
|
|
516
|
+
result = result + "Default value: " + str(self.default_value)
|
|
517
|
+
if self.null_value is not None:
|
|
518
|
+
result = result + "Null value: " + str(self.null_value)
|
|
519
|
+
|
|
520
|
+
return result
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# This is a list of ultils to work with constraints
|
|
524
|
+
def get_new_ctc_name(ctcs_names: list[str], prefix_name: str) -> str:
|
|
525
|
+
"""Return a new name for a constraint (based on the provided prefix) that is not already
|
|
526
|
+
in the given list of constraints' names."""
|
|
527
|
+
count = 1
|
|
528
|
+
new_name = f"{prefix_name}"
|
|
529
|
+
while new_name in ctcs_names:
|
|
530
|
+
new_name = f"{prefix_name}{count}"
|
|
531
|
+
count += 1
|
|
532
|
+
return new_name
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def left_right_features_from_simple_constraint(
|
|
536
|
+
simple_ctc: Constraint,
|
|
537
|
+
) -> tuple[str, str]:
|
|
538
|
+
"""Return the names of the features involved in a simple constraint.
|
|
539
|
+
|
|
540
|
+
A simple constraint can be a requires constraint or an excludes constraint.
|
|
541
|
+
A requires constraint can be represented in the AST of the constraint with one of the
|
|
542
|
+
following structures:
|
|
543
|
+
A requires B
|
|
544
|
+
A => B
|
|
545
|
+
!A v B
|
|
546
|
+
An excludes constraint can be represented in the AST of the constraint with one of the
|
|
547
|
+
following structures:
|
|
548
|
+
A excludes B
|
|
549
|
+
A => !B
|
|
550
|
+
!A v !B
|
|
551
|
+
"""
|
|
552
|
+
root_op = simple_ctc.ast.root
|
|
553
|
+
if root_op.data in [
|
|
554
|
+
ASTOperation.REQUIRES,
|
|
555
|
+
ASTOperation.IMPLIES,
|
|
556
|
+
ASTOperation.EXCLUDES,
|
|
557
|
+
]:
|
|
558
|
+
left = root_op.left.data
|
|
559
|
+
right = root_op.right.data
|
|
560
|
+
if right == ASTOperation.NOT:
|
|
561
|
+
right = root_op.right.left.data
|
|
562
|
+
elif root_op.data == ASTOperation.OR:
|
|
563
|
+
left = root_op.left.data
|
|
564
|
+
right = root_op.right.data
|
|
565
|
+
if left == ASTOperation.NOT and right == ASTOperation.NOT: # excludes
|
|
566
|
+
left = root_op.left.left.data
|
|
567
|
+
right = root_op.right.left.data
|
|
568
|
+
elif left == ASTOperation.NOT: # implies: A -> B
|
|
569
|
+
left = root_op.left.left.data
|
|
570
|
+
right = root_op.right.data
|
|
571
|
+
elif right == ASTOperation.NOT: # implies: B -> A
|
|
572
|
+
left = root_op.right.left.data
|
|
573
|
+
right = root_op.left.data
|
|
574
|
+
return (left, right)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def split_constraint(constraint: Constraint) -> list[Constraint]:
|
|
578
|
+
"""Given a constraint, split it in multiple constraints separated by the AND operator."""
|
|
579
|
+
asts = split_formula(constraint.ast)
|
|
580
|
+
asts_simplified = [simplify_formula(ast) for ast in asts]
|
|
581
|
+
asts = []
|
|
582
|
+
for ctc in asts_simplified:
|
|
583
|
+
asts.extend(split_formula(ctc))
|
|
584
|
+
|
|
585
|
+
asts_negation_propagated = [propagate_negation(ast.root) for ast in asts]
|
|
586
|
+
asts = []
|
|
587
|
+
for ctc in asts_negation_propagated:
|
|
588
|
+
asts.extend(split_formula(ctc))
|
|
589
|
+
|
|
590
|
+
asts_cnf = [to_cnf(ast) for ast in asts]
|
|
591
|
+
asts = []
|
|
592
|
+
for ctc in asts_cnf:
|
|
593
|
+
asts.extend(split_formula(ctc))
|
|
594
|
+
return [Constraint(f"{constraint.name}{i}", ast) for i, ast in enumerate(asts)]
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def split_formula(formula: AST) -> list[AST]:
|
|
598
|
+
"""Given a formula, returns a list of formulas separated by the AND operator."""
|
|
599
|
+
res = []
|
|
600
|
+
node = formula.root
|
|
601
|
+
if node.data == ASTOperation.AND:
|
|
602
|
+
res.extend(split_formula(AST(node.left)))
|
|
603
|
+
res.extend(split_formula(AST(node.right)))
|
|
604
|
+
else:
|
|
605
|
+
res.append(formula)
|
|
606
|
+
return res
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .fm_core_features import FMCoreFeatures, get_core_features
|
|
2
|
+
from .fm_count_leafs import FMCountLeafs, count_leaf_features
|
|
3
|
+
from .fm_leaf_features import FMLeafFeatures, get_leaf_features
|
|
4
|
+
from .fm_average_branching_factor import FMAverageBranchingFactor, average_branching_factor
|
|
5
|
+
from .fm_feature_ancestors import FMFeatureAncestors, get_feature_ancestors
|
|
6
|
+
from .fm_max_depth_tree import FMMaxDepthTree, max_depth_tree
|
|
7
|
+
from .fm_estimated_configurations_number import FMEstimatedConfigurationsNumber, count_configurations
|
|
8
|
+
from .fm_atomic_sets import FMAtomicSets, get_atomic_sets
|
|
9
|
+
from .fm_metrics import FMMetrics
|
|
10
|
+
from .fm_generate_random_attribute import GenerateRandomAttribute
|
|
11
|
+
|
|
12
|
+
__all__ = ['FMCoreFeatures', 'get_core_features',
|
|
13
|
+
'FMCountLeafs', 'count_leaf_features',
|
|
14
|
+
'FMLeafFeatures', 'get_leaf_features',
|
|
15
|
+
'FMAverageBranchingFactor', 'average_branching_factor',
|
|
16
|
+
'FMFeatureAncestors', 'get_feature_ancestors',
|
|
17
|
+
'FMMaxDepthTree', 'max_depth_tree',
|
|
18
|
+
'count_configurations',
|
|
19
|
+
'FMAtomicSets', 'get_atomic_sets', 'FMMetrics',
|
|
20
|
+
'GenerateRandomAttribute', 'FMEstimatedConfigurationsNumber']
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
from flamapy.core.models import VariabilityModel
|
|
4
|
+
from flamapy.core.operations.atomic_sets import AtomicSets
|
|
5
|
+
from flamapy.metamodels.fm_metamodel.models import FeatureModel, Feature
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FMAtomicSets(AtomicSets):
|
|
9
|
+
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.result: list[set[Feature]] = []
|
|
12
|
+
|
|
13
|
+
def get_result(self) -> list[set[Feature]]:
|
|
14
|
+
return self.result
|
|
15
|
+
|
|
16
|
+
def execute(self, model: VariabilityModel) -> 'FMAtomicSets':
|
|
17
|
+
fm_model = cast(FeatureModel, model)
|
|
18
|
+
self.result = get_atomic_sets(fm_model)
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def atomic_sets(self) -> list[set[Feature]]:
|
|
22
|
+
return self.get_result()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_atomic_sets(feature_model: FeatureModel) -> list[set[Feature]]:
|
|
26
|
+
# if feature_model.root is None:
|
|
27
|
+
# return []
|
|
28
|
+
|
|
29
|
+
atomic_sets = []
|
|
30
|
+
root = feature_model.root
|
|
31
|
+
atomic_set = {root}
|
|
32
|
+
atomic_sets.append(atomic_set)
|
|
33
|
+
compute_atomic_sets(atomic_sets, root, atomic_set)
|
|
34
|
+
return atomic_sets
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def compute_atomic_sets(atomic_sets: list[set[Feature]],
|
|
38
|
+
feature: Feature,
|
|
39
|
+
current_set: set[Feature]) -> None:
|
|
40
|
+
for child in feature.get_children():
|
|
41
|
+
if child.is_mandatory():
|
|
42
|
+
current_set.add(child)
|
|
43
|
+
compute_atomic_sets(atomic_sets, child, current_set)
|
|
44
|
+
else:
|
|
45
|
+
new_as = {child}
|
|
46
|
+
atomic_sets.append(new_as)
|
|
47
|
+
compute_atomic_sets(atomic_sets, child, new_as)
|