flamapy-fm 2.6.0.dev7__tar.gz → 2.6.0.dev11__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.6.0.dev7 → flamapy_fm-2.6.0.dev11}/PKG-INFO +23 -2
- flamapy_fm-2.6.0.dev11/README.md +34 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/models/__init__.py +2 -0
- flamapy_fm-2.6.0.dev11/flamapy/metamodels/fm_metamodel/models/clause_set.py +201 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/fm_atomic_sets.py +15 -1
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/fm_average_branching_factor.py +13 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/fm_count_leafs.py +11 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/fm_estimated_configurations_number.py +13 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/fm_feature_ancestors.py +24 -1
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_attribute.py +26 -0
- flamapy_fm-2.6.0.dev11/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_feature_model.py +223 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/fm_language_level.py +9 -0
- flamapy_fm-2.6.0.dev11/flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +47 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/fm_max_depth_tree.py +11 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/fm_metrics.py +10 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/fm_variation_points.py +17 -1
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy_fm.egg-info/PKG-INFO +23 -2
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy_fm.egg-info/SOURCES.txt +1 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy_fm.egg-info/requires.txt +1 -1
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/pyproject.toml +2 -2
- flamapy_fm-2.6.0.dev7/README.md +0 -13
- flamapy_fm-2.6.0.dev7/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_feature_model.py +0 -100
- flamapy_fm-2.6.0.dev7/flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +0 -27
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/LICENSE +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/__init__.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/models/feature_model.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/__init__.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/fm_core_features.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/interfaces/__init__.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/operations/interfaces/variation_points.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/__init__.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/afm_reader.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/afm_writer.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/clafer_writer.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/featureide_reader.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/featureide_writer.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/flat_fm.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/fm_secure_features_names.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/glencoe_reader.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/glencoe_writer.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/json_reader.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/json_writer.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/pl_writer.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/pysat_to_fm.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/__init__.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/cardinality_group_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/commitment_feature.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/deletion_feature.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring_paper.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/multiple_group_decomposition_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/mutex_group_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/or_mandatory_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/pseudocomplex_constraint_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_exception.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_interface.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/strictcomplex_constraint_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/refactorings/xor_mandatory_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/splot_writer.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/uvl_reader.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/uvl_writer.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/xml_reader.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/transformations/xml_writer.py +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy_fm.egg-info/dependency_links.txt +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy_fm.egg-info/top_level.txt +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/setup.cfg +0 -0
- {flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flamapy-fm
|
|
3
|
-
Version: 2.6.0.
|
|
3
|
+
Version: 2.6.0.dev11
|
|
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.
|
|
11
|
+
Requires-Dist: flamapy-fw~=2.6.0.dev11
|
|
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
|
+
```
|
|
@@ -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.
|
|
3
|
+
Version: 2.6.0.dev11
|
|
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.
|
|
11
|
+
Requires-Dist: flamapy-fw~=2.6.0.dev11
|
|
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
|
|
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flamapy-fm"
|
|
7
|
-
version = "2.6.0.
|
|
7
|
+
version = "2.6.0.dev11"
|
|
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.
|
|
14
|
+
"flamapy-fw~=2.6.0.dev11",
|
|
15
15
|
"uvlparser~=2.5.0",
|
|
16
16
|
"afmparser~=1.0.3",
|
|
17
17
|
]
|
flamapy_fm-2.6.0.dev7/README.md
DELETED
|
@@ -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
|
-
```
|
flamapy_fm-2.6.0.dev7/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_feature_model.py
DELETED
|
@@ -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]
|
|
File without changes
|
{flamapy_fm-2.6.0.dev7 → flamapy_fm-2.6.0.dev11}/flamapy/metamodels/fm_metamodel/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|