flamapy-fm 2.0.0.dev8__tar.gz → 2.0.2.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/PKG-INFO +1 -1
- flamapy-fm-2.0.2.dev0/flamapy/metamodels/fm_metamodel/models/__init__.py +23 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/models/feature_model.py +87 -7
- flamapy-fm-2.0.2.dev0/flamapy/metamodels/fm_metamodel/operations/__init__.py +26 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_attribute.py +6 -6
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_metrics.py +11 -3
- flamapy-fm-2.0.2.dev0/flamapy/metamodels/fm_metamodel/operations/fm_variation_points.py +37 -0
- flamapy-fm-2.0.2.dev0/flamapy/metamodels/fm_metamodel/operations/interfaces/__init__.py +4 -0
- flamapy-fm-2.0.2.dev0/flamapy/metamodels/fm_metamodel/operations/interfaces/variation_points.py +21 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/__init__.py +7 -5
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/afm_reader.py +4 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/clafer_writer.py +8 -3
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/featureide_writer.py +21 -17
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/glencoe_writer.py +6 -1
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/json_reader.py +14 -11
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/json_writer.py +6 -1
- flamapy-fm-2.0.2.dev0/flamapy/metamodels/fm_metamodel/transformations/pl_writer.py +170 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/splot_writer.py +9 -3
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/uvl_reader.py +215 -93
- flamapy-fm-2.0.2.dev0/flamapy/metamodels/fm_metamodel/transformations/uvl_writer.py +169 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/xml_reader.py +2 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy_fm.egg-info/PKG-INFO +1 -1
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy_fm.egg-info/SOURCES.txt +5 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy_fm.egg-info/requires.txt +3 -3
- flamapy-fm-2.0.2.dev0/pyproject.toml +51 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/setup.py +1 -1
- flamapy-fm-2.0.0.dev8/flamapy/metamodels/fm_metamodel/models/__init__.py +0 -17
- flamapy-fm-2.0.0.dev8/flamapy/metamodels/fm_metamodel/operations/__init__.py +0 -21
- flamapy-fm-2.0.0.dev8/flamapy/metamodels/fm_metamodel/transformations/uvl_writer.py +0 -138
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/README.md +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/__init__.py +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_atomic_sets.py +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_average_branching_factor.py +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_core_features.py +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_count_leafs.py +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_estimated_configurations_number.py +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_feature_ancestors.py +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_max_depth_tree.py +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/afm_writer.py +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/featureide_reader.py +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/glencoe_reader.py +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy/metamodels/fm_metamodel/transformations/pysat_to_fm.py +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy_fm.egg-info/dependency_links.txt +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/flamapy_fm.egg-info/top_level.txt +0 -0
- {flamapy-fm-2.0.0.dev8 → flamapy-fm-2.0.2.dev0}/setup.cfg +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .feature_model import (
|
|
2
|
+
Feature,
|
|
3
|
+
Constraint,
|
|
4
|
+
FeatureModel,
|
|
5
|
+
Relation,
|
|
6
|
+
Domain,
|
|
7
|
+
Range,
|
|
8
|
+
Attribute,
|
|
9
|
+
Cardinality,
|
|
10
|
+
FeatureType
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'Attribute',
|
|
15
|
+
'Cardinality',
|
|
16
|
+
'Constraint',
|
|
17
|
+
'Domain',
|
|
18
|
+
'Feature',
|
|
19
|
+
'FeatureModel',
|
|
20
|
+
'FeatureType',
|
|
21
|
+
'Range',
|
|
22
|
+
'Relation',
|
|
23
|
+
]
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from typing import Any, Optional
|
|
2
2
|
from functools import total_ordering
|
|
3
|
+
from enum import Enum
|
|
3
4
|
|
|
4
5
|
from flamapy.core.exceptions import FlamaException
|
|
5
6
|
from flamapy.core.models import AST, VariabilityModel, VariabilityElement, ASTOperation
|
|
7
|
+
from flamapy.core.models.ast import LOGICAL_OPERATORS, ARITHMETIC_OPERATORS, AGGREGATION_OPERATORS
|
|
6
8
|
from flamapy.core.models.ast import simplify_formula, propagate_negation, to_cnf
|
|
7
9
|
|
|
8
10
|
|
|
@@ -68,6 +70,8 @@ class Relation:
|
|
|
68
70
|
else:
|
|
69
71
|
relation_type = "Other"
|
|
70
72
|
res = f"({relation_type}) " + res
|
|
73
|
+
if res.endswith(" "):
|
|
74
|
+
res = res[:-1]
|
|
71
75
|
return res
|
|
72
76
|
|
|
73
77
|
def __hash__(self) -> int:
|
|
@@ -88,21 +92,39 @@ class Relation:
|
|
|
88
92
|
return str(self) < str(other)
|
|
89
93
|
|
|
90
94
|
|
|
95
|
+
class FeatureType(Enum):
|
|
96
|
+
BOOLEAN = 'Boolean'
|
|
97
|
+
INTEGER = 'Integer'
|
|
98
|
+
REAL = 'Real'
|
|
99
|
+
STRING = 'String'
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Cardinality:
|
|
103
|
+
|
|
104
|
+
def __init__(self, card_min: int = 1, card_max: int = 1):
|
|
105
|
+
self.min = card_min
|
|
106
|
+
self.max = card_max
|
|
107
|
+
|
|
108
|
+
|
|
91
109
|
@total_ordering
|
|
92
110
|
class Feature(VariabilityElement):
|
|
93
111
|
|
|
94
|
-
def __init__(
|
|
112
|
+
def __init__( # noqa: PLR0913
|
|
95
113
|
self,
|
|
96
114
|
name: str,
|
|
97
115
|
relations: Optional[list["Relation"]] = None,
|
|
98
116
|
parent: Optional["Feature"] = None,
|
|
99
117
|
is_abstract: bool = False,
|
|
118
|
+
feature_type: FeatureType = FeatureType.BOOLEAN,
|
|
119
|
+
feature_cardinality: Cardinality = Cardinality(1, 1)
|
|
100
120
|
):
|
|
101
121
|
super().__init__(name)
|
|
102
122
|
self.name = name
|
|
103
123
|
self.relations = [] if relations is None else relations
|
|
104
124
|
self.parent = self._get_parent() if parent is None else parent
|
|
105
125
|
self.is_abstract = is_abstract
|
|
126
|
+
self.feature_type = feature_type
|
|
127
|
+
self.feature_cardinality = feature_cardinality
|
|
106
128
|
self.attributes = list["Attribute"]([])
|
|
107
129
|
|
|
108
130
|
def is_empty(self) -> bool:
|
|
@@ -170,6 +192,19 @@ class Feature(VariabilityElement):
|
|
|
170
192
|
def is_leaf(self) -> bool:
|
|
171
193
|
return len(self.get_relations()) == 0
|
|
172
194
|
|
|
195
|
+
def is_boolean(self) -> bool:
|
|
196
|
+
return self.feature_type == FeatureType.BOOLEAN
|
|
197
|
+
|
|
198
|
+
def is_numerical(self) -> bool:
|
|
199
|
+
return self.feature_type in [FeatureType.INTEGER, FeatureType.REAL]
|
|
200
|
+
|
|
201
|
+
def is_string(self) -> bool:
|
|
202
|
+
return self.feature_type == FeatureType.STRING
|
|
203
|
+
|
|
204
|
+
def is_multifeature(self) -> bool:
|
|
205
|
+
"""Return true if the feature has a cardinality different from [1..1]."""
|
|
206
|
+
return self.feature_cardinality.min != 1 or self.feature_cardinality.max != 1
|
|
207
|
+
|
|
173
208
|
def __str__(self) -> str:
|
|
174
209
|
return self.name
|
|
175
210
|
|
|
@@ -205,7 +240,11 @@ class Constraint:
|
|
|
205
240
|
stack = [self.ast.root]
|
|
206
241
|
while stack:
|
|
207
242
|
node = stack.pop()
|
|
243
|
+
if node is None:
|
|
244
|
+
continue
|
|
208
245
|
if node.is_unique_term():
|
|
246
|
+
if (isinstance(node.data, (int, float)) or node.data.startswith("'")):
|
|
247
|
+
continue
|
|
209
248
|
features.add(node.data)
|
|
210
249
|
elif node.is_unary_op():
|
|
211
250
|
stack.append(node.left)
|
|
@@ -214,6 +253,25 @@ class Constraint:
|
|
|
214
253
|
stack.append(node.left)
|
|
215
254
|
return list(features)
|
|
216
255
|
|
|
256
|
+
def is_logical_constraint(self) -> bool:
|
|
257
|
+
"""Return true if the constraint contains only logical operators."""
|
|
258
|
+
return all(op in LOGICAL_OPERATORS for op in self.ast.get_operators())
|
|
259
|
+
|
|
260
|
+
def is_arithmetic_constraint(self) -> bool:
|
|
261
|
+
"""Return true if the constraint contains at least one arithmetic operator."""
|
|
262
|
+
return any(op in ARITHMETIC_OPERATORS for op in self.ast.get_operators())
|
|
263
|
+
|
|
264
|
+
def is_aggregation_constraint(self) -> bool:
|
|
265
|
+
"""Return true if the constraint contains at least one aggregation operator."""
|
|
266
|
+
return any(op in AGGREGATION_OPERATORS for op in self.ast.get_operators())
|
|
267
|
+
|
|
268
|
+
def is_single_feature_constraint(self) -> bool:
|
|
269
|
+
"""Return true if the constraint is a single feature or its negation."""
|
|
270
|
+
root_op = self._ast.root
|
|
271
|
+
return (root_op.is_term() or
|
|
272
|
+
(root_op.data == ASTOperation.NOT and
|
|
273
|
+
(root_op.left.is_term() or root_op.right.is_term())))
|
|
274
|
+
|
|
217
275
|
def is_simple_constraint(self) -> bool:
|
|
218
276
|
"""Return true if the constraint is a simple constraint (requires or excludes)."""
|
|
219
277
|
return self.is_requires_constraint() or self.is_excludes_constraint()
|
|
@@ -221,7 +279,7 @@ class Constraint:
|
|
|
221
279
|
def is_complex_constraint(self) -> bool:
|
|
222
280
|
"""Return true if the constraint is a complex constraint
|
|
223
281
|
(i.e., it is not a simple constraint)."""
|
|
224
|
-
return not self.is_simple_constraint()
|
|
282
|
+
return self.is_logical_constraint() and not self.is_simple_constraint()
|
|
225
283
|
|
|
226
284
|
def is_requires_constraint(self) -> bool:
|
|
227
285
|
"""Return true if the constraint is a requires constraint."""
|
|
@@ -240,10 +298,10 @@ class Constraint:
|
|
|
240
298
|
and root_op.right.left.is_term()
|
|
241
299
|
)
|
|
242
300
|
return (
|
|
243
|
-
neg_left
|
|
244
|
-
and root_op.right.is_term()
|
|
245
|
-
or neg_right
|
|
246
|
-
and root_op.left.is_term()
|
|
301
|
+
(neg_left
|
|
302
|
+
and root_op.right.is_term())
|
|
303
|
+
or (neg_right
|
|
304
|
+
and root_op.left.is_term())
|
|
247
305
|
)
|
|
248
306
|
return False
|
|
249
307
|
|
|
@@ -276,6 +334,8 @@ class Constraint:
|
|
|
276
334
|
def is_pseudocomplex_constraint(self) -> bool:
|
|
277
335
|
"""Return true if the constraint is a pseudo-complex constraint
|
|
278
336
|
(i.e., it can be transformed to a set of simple constraints)."""
|
|
337
|
+
if not self.is_logical_constraint():
|
|
338
|
+
return False
|
|
279
339
|
split_ctcs = split_constraint(self)
|
|
280
340
|
return len(split_ctcs) > 1 and all(
|
|
281
341
|
ctc.is_simple_constraint() for ctc in split_ctcs
|
|
@@ -284,11 +344,13 @@ class Constraint:
|
|
|
284
344
|
def is_strictcomplex_constraint(self) -> bool:
|
|
285
345
|
"""Return true if the constraint is a strict-complex constraint
|
|
286
346
|
(i.e., it cannot be transformed to a set of simple constraints)."""
|
|
347
|
+
if not self.is_logical_constraint():
|
|
348
|
+
return False
|
|
287
349
|
split_ctcs = split_constraint(self)
|
|
288
350
|
return any(ctc.is_complex_constraint() for ctc in split_ctcs)
|
|
289
351
|
|
|
290
352
|
def __str__(self) -> str:
|
|
291
|
-
return f"({self.name}) {
|
|
353
|
+
return f"({self.name}) {self.ast!s}"
|
|
292
354
|
|
|
293
355
|
def __hash__(self) -> int:
|
|
294
356
|
return hash(str(self.ast).lower())
|
|
@@ -334,6 +396,15 @@ class FeatureModel(VariabilityModel):
|
|
|
334
396
|
features.extend(relation.children)
|
|
335
397
|
return features
|
|
336
398
|
|
|
399
|
+
def get_boolean_features(self) -> list["Feature"]:
|
|
400
|
+
return [f for f in self.get_features() if f.is_boolean()]
|
|
401
|
+
|
|
402
|
+
def get_numerical_features(self) -> list["Feature"]:
|
|
403
|
+
return [f for f in self.get_features() if f.is_numerical()]
|
|
404
|
+
|
|
405
|
+
def get_string_features(self) -> list["Feature"]:
|
|
406
|
+
return [f for f in self.get_features() if f.is_string()]
|
|
407
|
+
|
|
337
408
|
def get_constraints(self) -> list["Constraint"]:
|
|
338
409
|
return self.ctcs
|
|
339
410
|
|
|
@@ -352,6 +423,15 @@ class FeatureModel(VariabilityModel):
|
|
|
352
423
|
def get_feature_by_name(self, feature_name: str) -> Optional["Feature"]:
|
|
353
424
|
return next((f for f in self.get_features() if f.name == feature_name), None)
|
|
354
425
|
|
|
426
|
+
def get_logical_constraints(self) -> list["Constraint"]:
|
|
427
|
+
return [c for c in self.get_constraints() if c.is_logical_constraint()]
|
|
428
|
+
|
|
429
|
+
def get_arithmetic_constraints(self) -> list["Constraint"]:
|
|
430
|
+
return [c for c in self.get_constraints() if c.is_arithmetic_constraint()]
|
|
431
|
+
|
|
432
|
+
def get_aggregations_constraints(self) -> list["Constraint"]:
|
|
433
|
+
return [c for c in self.get_constraints() if c.is_aggregation_constraint()]
|
|
434
|
+
|
|
355
435
|
def get_complex_constraints(self) -> list["Constraint"]:
|
|
356
436
|
return [c for c in self.get_constraints() if c.is_complex_constraint()]
|
|
357
437
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .fm_core_features import FMCoreFeatures
|
|
2
|
+
from .fm_count_leafs import FMCountLeafs
|
|
3
|
+
from .fm_leaf_features import FMLeafFeatures
|
|
4
|
+
from .fm_average_branching_factor import FMAverageBranchingFactor
|
|
5
|
+
from .fm_feature_ancestors import FMFeatureAncestors
|
|
6
|
+
from .fm_max_depth_tree import FMMaxDepthTree
|
|
7
|
+
from .fm_estimated_configurations_number import FMEstimatedConfigurationsNumber
|
|
8
|
+
from .fm_atomic_sets import FMAtomicSets
|
|
9
|
+
from .fm_metrics import FMMetrics
|
|
10
|
+
from .fm_generate_random_attribute import GenerateRandomAttribute
|
|
11
|
+
from .fm_variation_points import FMVariationPoints
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'FMAtomicSets',
|
|
16
|
+
'FMAverageBranchingFactor',
|
|
17
|
+
'FMCoreFeatures',
|
|
18
|
+
'FMCountLeafs',
|
|
19
|
+
'FMEstimatedConfigurationsNumber',
|
|
20
|
+
'FMFeatureAncestors',
|
|
21
|
+
'FMLeafFeatures',
|
|
22
|
+
'FMMaxDepthTree',
|
|
23
|
+
'FMMetrics',
|
|
24
|
+
'FMVariationPoints',
|
|
25
|
+
'GenerateRandomAttribute',
|
|
26
|
+
]
|
|
@@ -5,9 +5,9 @@ from flamapy.core.models import VariabilityModel
|
|
|
5
5
|
from flamapy.core.operations import Operation
|
|
6
6
|
from flamapy.core.exceptions import FlamaException
|
|
7
7
|
from flamapy.metamodels.fm_metamodel.models import (
|
|
8
|
-
FeatureModel,
|
|
8
|
+
FeatureModel,
|
|
9
9
|
Range,
|
|
10
|
-
Domain,
|
|
10
|
+
Domain,
|
|
11
11
|
Attribute
|
|
12
12
|
)
|
|
13
13
|
|
|
@@ -44,19 +44,19 @@ class GenerateRandomAttribute(Operation):
|
|
|
44
44
|
if self._attribute_domain is None:
|
|
45
45
|
raise FlamaException("Attribute's domain has not been provided.")
|
|
46
46
|
fm_model = cast(FeatureModel, model)
|
|
47
|
-
self.result = generate_random_attribute_values(fm_model,
|
|
48
|
-
self._attribute_name,
|
|
47
|
+
self.result = generate_random_attribute_values(fm_model,
|
|
48
|
+
self._attribute_name,
|
|
49
49
|
self._attribute_domain,
|
|
50
50
|
self._only_leaf_features)
|
|
51
51
|
return self
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
def generate_random_attribute_values(feature_model: FeatureModel,
|
|
54
|
+
def generate_random_attribute_values(feature_model: FeatureModel,
|
|
55
55
|
name: str,
|
|
56
56
|
domain: Domain,
|
|
57
57
|
only_leaf_features: bool) -> FeatureModel:
|
|
58
58
|
for feature in feature_model.get_features():
|
|
59
|
-
if not only_leaf_features or only_leaf_features and feature.is_leaf():
|
|
59
|
+
if not only_leaf_features or (only_leaf_features and feature.is_leaf()):
|
|
60
60
|
if not any(name == attr.name for attr in feature.get_attributes()):
|
|
61
61
|
random_value = get_random_value_from_domain(domain)
|
|
62
62
|
new_attribute = Attribute(name, domain, random_value)
|
|
@@ -5,7 +5,7 @@ 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
7
|
from flamapy.metamodels.fm_metamodel.models import FeatureModel, Feature
|
|
8
|
-
from flamapy.metamodels.fm_metamodel import
|
|
8
|
+
from flamapy.metamodels.fm_metamodel.operations import FMAverageBranchingFactor
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def metric_method(func: Callable[..., dict[str, Any]]) -> Callable[..., dict[str, Any]]:
|
|
@@ -30,7 +30,7 @@ class FMMetrics(Metrics): # pylint: disable=too-many-instance-attributes
|
|
|
30
30
|
super().__init__()
|
|
31
31
|
self.model: Optional[FeatureModel] = None
|
|
32
32
|
self.result: list[dict[str, Any]] = []
|
|
33
|
-
self.
|
|
33
|
+
self._model_type_extension = "fm"
|
|
34
34
|
self._features: list[Feature] = []
|
|
35
35
|
self._features_by_name: dict[str, Feature] = {}
|
|
36
36
|
self._abstract_features: dict[str, Feature] = {}
|
|
@@ -39,6 +39,14 @@ class FMMetrics(Metrics): # pylint: disable=too-many-instance-attributes
|
|
|
39
39
|
self._constraints_per_features: list[int] = []
|
|
40
40
|
self._feature_ancestors: list[int] = []
|
|
41
41
|
|
|
42
|
+
@property
|
|
43
|
+
def model_type_extension(self) -> str:
|
|
44
|
+
return self._model_type_extension
|
|
45
|
+
|
|
46
|
+
@model_type_extension.setter
|
|
47
|
+
def model_type_extension(self, ext: str) -> None:
|
|
48
|
+
self._model_type_extension = ext
|
|
49
|
+
|
|
42
50
|
def get_result(self) -> list[dict[str, Any]]:
|
|
43
51
|
return self.result
|
|
44
52
|
|
|
@@ -516,7 +524,7 @@ class FMMetrics(Metrics): # pylint: disable=too-many-instance-attributes
|
|
|
516
524
|
raise FlamaException("Feature model is not defined.")
|
|
517
525
|
|
|
518
526
|
name = "Branching factor"
|
|
519
|
-
_avg_branching_factor =
|
|
527
|
+
_avg_branching_factor = FMAverageBranchingFactor().execute(self.model).get_result()
|
|
520
528
|
result = self.construct_result(
|
|
521
529
|
name=name, doc=self.branching_factor.__doc__, result=_avg_branching_factor
|
|
522
530
|
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
from flamapy.core.models import VariabilityModel
|
|
4
|
+
from flamapy.metamodels.fm_metamodel.operations.interfaces import VariationPoints
|
|
5
|
+
from flamapy.metamodels.fm_metamodel.models import FeatureModel, Feature
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FMVariationPoints(VariationPoints):
|
|
9
|
+
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.result: dict[Feature, list[Feature]] = {}
|
|
12
|
+
|
|
13
|
+
def get_result(self) -> dict[Feature, list[Feature]]:
|
|
14
|
+
return self.result
|
|
15
|
+
|
|
16
|
+
def execute(self, model: VariabilityModel) -> 'FMVariationPoints':
|
|
17
|
+
fm_model = cast(FeatureModel, model)
|
|
18
|
+
self.result = variation_points(fm_model)
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def variation_points(self) -> dict[Feature, list[Feature]]:
|
|
22
|
+
return self.get_result()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def variation_points(feature_model: FeatureModel) -> dict[Feature, list[Feature]]:
|
|
26
|
+
vps: dict[Feature, list[Feature]] = {}
|
|
27
|
+
features = [feature_model.root]
|
|
28
|
+
while features:
|
|
29
|
+
feature = features.pop()
|
|
30
|
+
variants = []
|
|
31
|
+
for relation in feature.get_relations():
|
|
32
|
+
if not relation.is_mandatory():
|
|
33
|
+
variants.extend(relation.children)
|
|
34
|
+
if variants:
|
|
35
|
+
vps[feature] = variants
|
|
36
|
+
features.extend(feature.get_children())
|
|
37
|
+
return vps
|
flamapy-fm-2.0.2.dev0/flamapy/metamodels/fm_metamodel/operations/interfaces/variation_points.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
|
|
3
|
+
from flamapy.core.operations import Operation
|
|
4
|
+
|
|
5
|
+
from flamapy.metamodels.fm_metamodel.models import Feature
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VariationPoints(Operation):
|
|
9
|
+
"""The variation points of a feature model are those features that require to make a choice
|
|
10
|
+
(i.e., select a variant).
|
|
11
|
+
|
|
12
|
+
This operation returns the variation points and the variants of each variation point.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def variation_points(self) -> dict[Feature, list[Feature]]:
|
|
21
|
+
pass
|
|
@@ -13,16 +13,18 @@ from .glencoe_writer import GlencoeWriter
|
|
|
13
13
|
from .clafer_writer import ClaferWriter
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
__all__ = [
|
|
16
|
+
__all__ = [
|
|
17
|
+
'AFMReader',
|
|
17
18
|
'AFMWriter',
|
|
19
|
+
'ClaferWriter',
|
|
18
20
|
'FeatureIDEReader',
|
|
19
21
|
'FeatureIDEWriter',
|
|
20
|
-
'
|
|
22
|
+
'GlencoeReader',
|
|
23
|
+
'GlencoeWriter',
|
|
21
24
|
'JSONReader',
|
|
25
|
+
'JSONWriter',
|
|
22
26
|
'SPLOTWriter',
|
|
23
27
|
'UVLReader',
|
|
24
28
|
'UVLWriter',
|
|
25
29
|
'XMLReader',
|
|
26
|
-
|
|
27
|
-
'GlencoeWriter',
|
|
28
|
-
'ClaferWriter']
|
|
30
|
+
]
|
|
@@ -128,6 +128,8 @@ class AFMReader(TextToModel):
|
|
|
128
128
|
if attribute_feature is None:
|
|
129
129
|
raise FlamaException("attribute_feature is not defined")
|
|
130
130
|
|
|
131
|
+
domain = None # type: Optional[Domain]
|
|
132
|
+
|
|
131
133
|
discrete_domain_node = attribute_spec.attribute_domain().discrete_domain_spec()
|
|
132
134
|
if discrete_domain_node is not None:
|
|
133
135
|
values = []
|
|
@@ -179,6 +181,8 @@ class AFMReader(TextToModel):
|
|
|
179
181
|
def build_ast_node(
|
|
180
182
|
self, expression: AFMParser.ExpressionContext, prefix: str
|
|
181
183
|
) -> Node:
|
|
184
|
+
var_name = None # Ensure var_name is initialized
|
|
185
|
+
|
|
182
186
|
if isinstance(expression, AFMParser.AtomContext):
|
|
183
187
|
if expression.variable() is not None:
|
|
184
188
|
var_name = prefix + expression.variable().getText()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
|
+
import string
|
|
2
3
|
from enum import Enum
|
|
3
4
|
from typing import Any, Optional
|
|
4
5
|
|
|
@@ -128,8 +129,8 @@ def serialize_constraint(ctc: Constraint) -> str:
|
|
|
128
129
|
|
|
129
130
|
|
|
130
131
|
def attributes_definition(feature_model: FeatureModel) -> str:
|
|
131
|
-
attributes = {attribute.get_name(): parse_type_value(attribute.get_default_value())
|
|
132
|
-
for feature in feature_model.get_features()
|
|
132
|
+
attributes = {attribute.get_name(): parse_type_value(attribute.get_default_value())
|
|
133
|
+
for feature in feature_model.get_features()
|
|
133
134
|
for attribute in feature.get_attributes()}
|
|
134
135
|
result = ''
|
|
135
136
|
if attributes:
|
|
@@ -152,4 +153,8 @@ def parse_type_value(value: Any) -> str:
|
|
|
152
153
|
|
|
153
154
|
|
|
154
155
|
def safename(name: str) -> str:
|
|
155
|
-
return f'"{name}"' if
|
|
156
|
+
return f'"{name}"' if any(char not in safecharacters() for char in name) else name
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def safecharacters() -> str:
|
|
160
|
+
return string.ascii_letters + string.digits + '_'
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import string
|
|
1
2
|
from xml.etree import ElementTree
|
|
2
3
|
from xml.etree.ElementTree import Element
|
|
3
4
|
from xml.dom import minidom
|
|
4
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
from flamapy.core.models.ast import Node, ASTOperation
|
|
7
8
|
from flamapy.core.transformations import ModelToText
|
|
@@ -31,9 +32,9 @@ class FeatureIDEWriter(ModelToText):
|
|
|
31
32
|
|
|
32
33
|
def transform(self) -> str:
|
|
33
34
|
fm_tree = _to_featureidexml(self._source_model).getroot()
|
|
34
|
-
xml_str = ElementTree.tostring(fm_tree,
|
|
35
|
-
encoding='UTF-8',
|
|
36
|
-
method='xml',
|
|
35
|
+
xml_str = ElementTree.tostring(fm_tree,
|
|
36
|
+
encoding='UTF-8',
|
|
37
|
+
method='xml',
|
|
37
38
|
xml_declaration=True)
|
|
38
39
|
xml_str = prettify(xml_str)
|
|
39
40
|
if self._path is not None:
|
|
@@ -47,8 +48,8 @@ def _to_featureidexml(feature_model: FeatureModel) -> ElementTree.ElementTree:
|
|
|
47
48
|
tree = ElementTree.ElementTree(fm_element)
|
|
48
49
|
struct = ElementTree.SubElement(fm_element, FeatureIDEReader.TAG_STRUCT)
|
|
49
50
|
constraints = ElementTree.SubElement(fm_element, FeatureIDEReader.TAG_CONSTRAINTS)
|
|
50
|
-
root = ElementTree.SubElement(struct,
|
|
51
|
-
_tag_element(feature_model.root),
|
|
51
|
+
root = ElementTree.SubElement(struct,
|
|
52
|
+
_tag_element(feature_model.root),
|
|
52
53
|
_get_attributes(feature_model.root))
|
|
53
54
|
_create_tree(root, feature_model.root.get_relations())
|
|
54
55
|
_get_constraints(constraints, feature_model.get_constraints())
|
|
@@ -66,24 +67,23 @@ def _create_tree(parent_element: Element, relations: list[Relation]) -> None:
|
|
|
66
67
|
|
|
67
68
|
def _get_attributes(feature: Feature) -> dict[str, str]:
|
|
68
69
|
atributes = {}
|
|
69
|
-
if feature.is_mandatory():
|
|
70
|
+
if feature.is_mandatory():
|
|
70
71
|
atributes['mandatory'] = 'true'
|
|
71
|
-
if feature.is_abstract:
|
|
72
|
+
if feature.is_abstract:
|
|
72
73
|
atributes['abstract'] = 'true'
|
|
73
74
|
atributes['name'] = safename(feature.name)
|
|
74
75
|
return atributes
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
def _tag_element(feature: Feature) -> str:
|
|
78
|
-
if feature.is_leaf():
|
|
79
|
+
if feature.is_leaf():
|
|
79
80
|
name = FeatureIDEReader.TAG_FEATURE
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
name = FeatureIDEReader.TAG_AND
|
|
81
|
+
elif feature.is_or_group():
|
|
82
|
+
name = FeatureIDEReader.TAG_OR
|
|
83
|
+
elif feature.is_alternative_group():
|
|
84
|
+
name = FeatureIDEReader.TAG_ALT
|
|
85
|
+
else:
|
|
86
|
+
name = FeatureIDEReader.TAG_AND
|
|
87
87
|
return name
|
|
88
88
|
|
|
89
89
|
|
|
@@ -139,4 +139,8 @@ def prettify(xml: str) -> bytes:
|
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
def safename(name: str) -> str:
|
|
142
|
-
return f'"{name}"' if
|
|
142
|
+
return f'"{name}"' if any(char not in safecharacters() for char in name) else name
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def safecharacters() -> str:
|
|
146
|
+
return string.ascii_letters + string.digits + '_'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import string
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
4
5
|
from flamapy.core.models.ast import Node, ASTOperation
|
|
@@ -106,4 +107,8 @@ def _get_ctc_info(ast_node: Node) -> dict[str, Any]:
|
|
|
106
107
|
|
|
107
108
|
|
|
108
109
|
def safename(name: str) -> str:
|
|
109
|
-
return f'"{name}"' if
|
|
110
|
+
return f'"{name}"' if any(char not in safecharacters() for char in name) else name
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def safecharacters() -> str:
|
|
114
|
+
return string.ascii_letters + string.digits + '_'
|
|
@@ -7,10 +7,10 @@ from flamapy.core.exceptions import ParsingException
|
|
|
7
7
|
from flamapy.core.transformations import TextToModel
|
|
8
8
|
|
|
9
9
|
from flamapy.metamodels.fm_metamodel.models import (
|
|
10
|
-
FeatureModel,
|
|
11
|
-
Relation,
|
|
12
|
-
Feature,
|
|
13
|
-
Constraint,
|
|
10
|
+
FeatureModel,
|
|
11
|
+
Relation,
|
|
12
|
+
Feature,
|
|
13
|
+
Constraint,
|
|
14
14
|
Attribute
|
|
15
15
|
)
|
|
16
16
|
|
|
@@ -64,8 +64,7 @@ def parse_attributes(feature: Feature, feature_node: Dict[str, Any]) -> None:
|
|
|
64
64
|
attr.set_parent(feature)
|
|
65
65
|
feature.add_attribute(attr)
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
def parse_relations(feature: Feature, feature_node: Dict[str, Any]) -> None:
|
|
67
|
+
def parse_relations(feature: Feature, feature_node: Dict[str, Any]) -> None: # noqa: C901
|
|
69
68
|
if 'relations' in feature_node:
|
|
70
69
|
for relation in feature_node['relations']:
|
|
71
70
|
children = []
|
|
@@ -73,6 +72,7 @@ def parse_relations(feature: Feature, feature_node: Dict[str, Any]) -> None:
|
|
|
73
72
|
child_feature = parse_tree(feature, child)
|
|
74
73
|
children.append(child_feature)
|
|
75
74
|
relation_type = relation['type']
|
|
75
|
+
new_relation: Optional[Relation] = None
|
|
76
76
|
if relation_type == JSONFeatureType.OPTIONAL.value:
|
|
77
77
|
new_relation = Relation(feature, children, 0, 1)
|
|
78
78
|
elif relation_type == JSONFeatureType.MANDATORY.value:
|
|
@@ -87,7 +87,10 @@ def parse_relations(feature: Feature, feature_node: Dict[str, Any]) -> None:
|
|
|
87
87
|
card_min = relation['card_min']
|
|
88
88
|
card_max = relation['card_max']
|
|
89
89
|
new_relation = Relation(feature, children, card_min, card_max)
|
|
90
|
-
|
|
90
|
+
if new_relation is not None:
|
|
91
|
+
feature.add_relation(new_relation)
|
|
92
|
+
else:
|
|
93
|
+
raise ParsingException(f'Invalid relation in JSON: {relation}')
|
|
91
94
|
|
|
92
95
|
|
|
93
96
|
def parse_constraints(constraints_info: List[Dict[str, Any]]) -> List[Constraint]:
|
|
@@ -129,13 +132,13 @@ def parse_ast_constraint(ctc_info: Dict[str, Any]) -> Node:
|
|
|
129
132
|
node = Node(ASTOperation.EQUIVALENCE, left, right)
|
|
130
133
|
elif ctc_type == ASTOperation.AND.value:
|
|
131
134
|
op_list = [parse_ast_constraint(op) for op in ctc_operands]
|
|
132
|
-
node = functools.reduce(lambda
|
|
135
|
+
node = functools.reduce(lambda lambd, r: Node(ASTOperation.AND, lambd, r), op_list)
|
|
133
136
|
elif ctc_type == ASTOperation.OR.value:
|
|
134
137
|
op_list = [parse_ast_constraint(op) for op in ctc_operands]
|
|
135
|
-
node = functools.reduce(lambda
|
|
138
|
+
node = functools.reduce(lambda lambd, r: Node(ASTOperation.OR, lambd, r), op_list)
|
|
136
139
|
elif ctc_type == ASTOperation.XOR.value:
|
|
137
140
|
op_list = [parse_ast_constraint(op) for op in ctc_operands]
|
|
138
|
-
node = functools.reduce(lambda
|
|
141
|
+
node = functools.reduce(lambda lambd, r: Node(ASTOperation.XOR, lambd, r), op_list)
|
|
139
142
|
else:
|
|
140
143
|
raise ParsingException(f'Invalid constraint in JSON: {ctc_info}')
|
|
141
|
-
return node
|
|
144
|
+
return node
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import string
|
|
2
3
|
from enum import Enum
|
|
3
4
|
from typing import Any, Dict, List
|
|
4
5
|
|
|
@@ -126,4 +127,8 @@ def get_ctc_info(ast_node: Node) -> Dict[str, Any]:
|
|
|
126
127
|
|
|
127
128
|
|
|
128
129
|
def safename(name: str) -> str:
|
|
129
|
-
return f'"{name}"' if
|
|
130
|
+
return f'"{name}"' if any(char not in safecharacters() for char in name) else name
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def safecharacters() -> str:
|
|
134
|
+
return string.ascii_letters + string.digits + '_'
|