flamapy-fm 2.5.0.dev0__tar.gz → 2.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flamapy_fm-2.6.0/PKG-INFO +56 -0
- flamapy_fm-2.6.0/README.md +34 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/models/__init__.py +2 -0
- flamapy_fm-2.6.0/flamapy/metamodels/fm_metamodel/models/clause_set.py +201 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/models/feature_model.py +16 -7
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/__init__.py +2 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_atomic_sets.py +15 -1
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_average_branching_factor.py +13 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_count_leafs.py +11 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_estimated_configurations_number.py +13 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_feature_ancestors.py +24 -1
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_attribute.py +26 -0
- flamapy_fm-2.6.0/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_feature_model.py +223 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_language_level.py +9 -0
- flamapy_fm-2.6.0/flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +47 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_max_depth_tree.py +11 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_metrics.py +10 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_variation_points.py +17 -1
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/__init__.py +2 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/uvl_reader.py +26 -16
- flamapy_fm-2.6.0/flamapy/metamodels/fm_metamodel/transformations/xml_writer.py +140 -0
- flamapy_fm-2.6.0/flamapy_fm.egg-info/PKG-INFO +56 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy_fm.egg-info/SOURCES.txt +3 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy_fm.egg-info/requires.txt +2 -2
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/pyproject.toml +5 -5
- flamapy_fm-2.5.0.dev0/PKG-INFO +0 -32
- flamapy_fm-2.5.0.dev0/README.md +0 -10
- flamapy_fm-2.5.0.dev0/flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +0 -27
- flamapy_fm-2.5.0.dev0/flamapy_fm.egg-info/PKG-INFO +0 -32
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/LICENSE +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/__init__.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/fm_core_features.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/interfaces/__init__.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/interfaces/variation_points.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/afm_reader.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/afm_writer.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/clafer_writer.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/featureide_reader.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/featureide_writer.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/flat_fm.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/fm_secure_features_names.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/glencoe_reader.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/glencoe_writer.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/json_reader.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/json_writer.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/pl_writer.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/pysat_to_fm.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/__init__.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/cardinality_group_refactoring.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/commitment_feature.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/deletion_feature.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring_paper.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/multiple_group_decomposition_refactoring.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/mutex_group_refactoring.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/or_mandatory_refactoring.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/pseudocomplex_constraint_refactoring.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_exception.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_interface.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/strictcomplex_constraint_refactoring.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/xor_mandatory_refactoring.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/splot_writer.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/uvl_writer.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/transformations/xml_reader.py +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy_fm.egg-info/dependency_links.txt +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy_fm.egg-info/top_level.txt +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/setup.cfg +0 -0
- {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/setup.py +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flamapy-fm
|
|
3
|
+
Version: 2.6.0
|
|
4
|
+
Summary: flamapy-fm is a plugin to Flamapy module
|
|
5
|
+
Author-email: Flamapy <flamapy@us.es>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/flamapy/fm_metamodel
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: flamapy-fw~=2.6.0
|
|
12
|
+
Requires-Dist: uvlparser~=2.5.0
|
|
13
|
+
Requires-Dist: afmparser~=1.0.3
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
17
|
+
Requires-Dist: prospector; extra == "dev"
|
|
18
|
+
Requires-Dist: mypy; extra == "dev"
|
|
19
|
+
Requires-Dist: coverage; extra == "dev"
|
|
20
|
+
Requires-Dist: antlr4-tools; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# flamapy-fm
|
|
24
|
+
|
|
25
|
+
The feature model metamodel plugin for [flamapy](https://flamapy.org): the
|
|
26
|
+
feature model classes plus readers and writers for many formats (UVL,
|
|
27
|
+
FeatureIDE, Glencoe, AFM, SPLOT, JSON, …).
|
|
28
|
+
|
|
29
|
+
**Documentation:** https://docs.flamapy.org/framework/plugins/feature_model_plugin
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install flamapy-fm
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Operations
|
|
38
|
+
|
|
39
|
+
Beyond the metrics/analysis operations (core/dead features, atomic sets, branching factor, …),
|
|
40
|
+
this plugin includes a **random feature-model generator**:
|
|
41
|
+
|
|
42
|
+
- `GenerateRandomFeatureModel` — builds a random synthetic `FeatureModel` object directly
|
|
43
|
+
(random tree of mandatory/optional/or/alternative groups plus random cross-tree constraints;
|
|
44
|
+
an optional `void` flag makes it unsatisfiable). Useful for benchmarking, testing, and building
|
|
45
|
+
corpora for learning-based operations. Serialize it to UVL (or any format) with the writer
|
|
46
|
+
transformations if you need text.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from flamapy.metamodels.fm_metamodel.operations import GenerateRandomFeatureModel
|
|
50
|
+
|
|
51
|
+
op = GenerateRandomFeatureModel()
|
|
52
|
+
op.set_num_features(20)
|
|
53
|
+
op.set_max_constraints(5)
|
|
54
|
+
op.set_seed(0)
|
|
55
|
+
fm = op.execute().get_result() # -> FeatureModel
|
|
56
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# flamapy-fm
|
|
2
|
+
|
|
3
|
+
The feature model metamodel plugin for [flamapy](https://flamapy.org): the
|
|
4
|
+
feature model classes plus readers and writers for many formats (UVL,
|
|
5
|
+
FeatureIDE, Glencoe, AFM, SPLOT, JSON, …).
|
|
6
|
+
|
|
7
|
+
**Documentation:** https://docs.flamapy.org/framework/plugins/feature_model_plugin
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install flamapy-fm
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Operations
|
|
16
|
+
|
|
17
|
+
Beyond the metrics/analysis operations (core/dead features, atomic sets, branching factor, …),
|
|
18
|
+
this plugin includes a **random feature-model generator**:
|
|
19
|
+
|
|
20
|
+
- `GenerateRandomFeatureModel` — builds a random synthetic `FeatureModel` object directly
|
|
21
|
+
(random tree of mandatory/optional/or/alternative groups plus random cross-tree constraints;
|
|
22
|
+
an optional `void` flag makes it unsatisfiable). Useful for benchmarking, testing, and building
|
|
23
|
+
corpora for learning-based operations. Serialize it to UVL (or any format) with the writer
|
|
24
|
+
transformations if you need text.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from flamapy.metamodels.fm_metamodel.operations import GenerateRandomFeatureModel
|
|
28
|
+
|
|
29
|
+
op = GenerateRandomFeatureModel()
|
|
30
|
+
op.set_num_features(20)
|
|
31
|
+
op.set_max_constraints(5)
|
|
32
|
+
op.set_seed(0)
|
|
33
|
+
fm = op.execute().get_result() # -> FeatureModel
|
|
34
|
+
```
|
|
@@ -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))
|
{flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/models/feature_model.py
RENAMED
|
@@ -3,7 +3,7 @@ from functools import total_ordering
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
|
|
5
5
|
from flamapy.core.exceptions import FlamaException
|
|
6
|
-
from flamapy.core.models import AST, VariabilityModel, VariabilityElement, ASTOperation
|
|
6
|
+
from flamapy.core.models import AST, VariabilityModel, VariabilityElement, ASTOperation, NodeType
|
|
7
7
|
from flamapy.core.models.ast import LOGICAL_OPERATORS, ARITHMETIC_OPERATORS, AGGREGATION_OPERATORS
|
|
8
8
|
from flamapy.core.models.ast import simplify_formula, propagate_negation, to_cnf
|
|
9
9
|
|
|
@@ -247,7 +247,11 @@ class Constraint:
|
|
|
247
247
|
self._ast = ast
|
|
248
248
|
|
|
249
249
|
def get_features(self) -> list[str]:
|
|
250
|
-
"""List of features' names involved in the constraint.
|
|
250
|
+
"""List of features' names involved in the constraint.
|
|
251
|
+
|
|
252
|
+
Uses node_type information when available (set by readers that support it).
|
|
253
|
+
Falls back to heuristics for nodes without explicit type info.
|
|
254
|
+
"""
|
|
251
255
|
features = set()
|
|
252
256
|
stack = [self.ast.root]
|
|
253
257
|
while stack:
|
|
@@ -255,9 +259,15 @@ class Constraint:
|
|
|
255
259
|
if node is None:
|
|
256
260
|
continue
|
|
257
261
|
if node.is_unique_term():
|
|
258
|
-
if
|
|
259
|
-
|
|
260
|
-
|
|
262
|
+
if node.node_type is not None:
|
|
263
|
+
if node.node_type == NodeType.FEATURE:
|
|
264
|
+
features.add(node.data)
|
|
265
|
+
# NodeType.LITERAL: skip
|
|
266
|
+
# Fallback heuristics for nodes without explicit type (backward compatibility)
|
|
267
|
+
elif not isinstance(node.data, (int, float)) and not (
|
|
268
|
+
isinstance(node.data, str) and node.data.startswith("'")
|
|
269
|
+
):
|
|
270
|
+
features.add(node.data)
|
|
261
271
|
elif node.is_unary_op():
|
|
262
272
|
stack.append(node.left)
|
|
263
273
|
elif node.is_binary_op():
|
|
@@ -281,8 +291,7 @@ class Constraint:
|
|
|
281
291
|
"""Return true if the constraint is a single feature or its negation."""
|
|
282
292
|
root_op = self._ast.root
|
|
283
293
|
return (root_op.is_term() or
|
|
284
|
-
(root_op.data == ASTOperation.NOT and
|
|
285
|
-
(root_op.left.is_term() or root_op.right.is_term())))
|
|
294
|
+
(root_op.data == ASTOperation.NOT and root_op.left.is_term()))
|
|
286
295
|
|
|
287
296
|
def is_simple_constraint(self) -> bool:
|
|
288
297
|
"""Return true if the constraint is a simple constraint (requires or excludes)."""
|
{flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0}/flamapy/metamodels/fm_metamodel/operations/__init__.py
RENAMED
|
@@ -8,6 +8,7 @@ from .fm_estimated_configurations_number import FMEstimatedConfigurationsNumber
|
|
|
8
8
|
from .fm_atomic_sets import FMAtomicSets
|
|
9
9
|
from .fm_metrics import FMMetrics
|
|
10
10
|
from .fm_generate_random_attribute import GenerateRandomAttribute
|
|
11
|
+
from .fm_generate_random_feature_model import GenerateRandomFeatureModel
|
|
11
12
|
from .fm_variation_points import FMVariationPoints
|
|
12
13
|
from .fm_language_level import FMLanguageLevel, LanguageLevel, MajorLevel, MinorLevel
|
|
13
14
|
|
|
@@ -25,6 +26,7 @@ __all__ = [
|
|
|
25
26
|
'FMMetrics',
|
|
26
27
|
'FMVariationPoints',
|
|
27
28
|
'GenerateRandomAttribute',
|
|
29
|
+
'GenerateRandomFeatureModel',
|
|
28
30
|
'LanguageLevel',
|
|
29
31
|
'MajorLevel',
|
|
30
32
|
'MinorLevel'
|
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
from typing import cast
|
|
1
|
+
from typing import Any, cast
|
|
2
2
|
|
|
3
3
|
from flamapy.core.models import VariabilityModel
|
|
4
4
|
from flamapy.core.operations.atomic_sets import AtomicSets
|
|
5
|
+
from flamapy.core.operations.descriptor import OperationDescriptor
|
|
5
6
|
from flamapy.metamodels.fm_metamodel.models import FeatureModel, Feature
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
def _atomic_sets_result(result: Any) -> Any:
|
|
10
|
+
return [[feature.name for feature in atomic_set] for atomic_set in result]
|
|
11
|
+
|
|
12
|
+
|
|
8
13
|
class FMAtomicSets(AtomicSets):
|
|
14
|
+
facade = OperationDescriptor(
|
|
15
|
+
doc=(
|
|
16
|
+
'This operation is used to find the atomic sets in a model: It returns the\n'
|
|
17
|
+
'atomic sets if they are found in the model. If the model does not follow the\n'
|
|
18
|
+
'UVL specification, an exception is raised and the operation returns False.'
|
|
19
|
+
),
|
|
20
|
+
returns='Union[None, List[List[Any]]]',
|
|
21
|
+
name='atomic_sets', operation='FMAtomicSets', result_adapter=_atomic_sets_result
|
|
22
|
+
)
|
|
9
23
|
|
|
10
24
|
def __init__(self) -> None:
|
|
11
25
|
self.result: list[set[Feature]] = []
|
|
@@ -2,6 +2,7 @@ from typing import cast
|
|
|
2
2
|
|
|
3
3
|
from flamapy.core.models import VariabilityModel
|
|
4
4
|
from flamapy.core.operations import AverageBranchingFactor
|
|
5
|
+
from flamapy.core.operations.descriptor import OperationDescriptor
|
|
5
6
|
from flamapy.metamodels.fm_metamodel.models.feature_model import FeatureModel
|
|
6
7
|
|
|
7
8
|
|
|
@@ -11,6 +12,18 @@ class FMAverageBranchingFactor(AverageBranchingFactor):
|
|
|
11
12
|
The number of branches is the number of features that have at least one child.
|
|
12
13
|
"""
|
|
13
14
|
|
|
15
|
+
facade = OperationDescriptor(
|
|
16
|
+
doc=(
|
|
17
|
+
'This refers to the average number of child features that a parent feature has\n'
|
|
18
|
+
"in a feature model. It's calculated by dividing the total number of child\n"
|
|
19
|
+
'features by the total number of parent features. A high average branching\n'
|
|
20
|
+
'factor indicates a complex feature model with many options, while a low\n'
|
|
21
|
+
'average branching factor indicates a simpler model.'
|
|
22
|
+
),
|
|
23
|
+
returns='Union[None, float]',
|
|
24
|
+
name='average_branching_factor', operation='FMAverageBranchingFactor'
|
|
25
|
+
)
|
|
26
|
+
|
|
14
27
|
def __init__(self) -> None:
|
|
15
28
|
self.result: float = 0
|
|
16
29
|
|
|
@@ -2,11 +2,22 @@ from typing import cast
|
|
|
2
2
|
|
|
3
3
|
from flamapy.core.models import VariabilityModel
|
|
4
4
|
from flamapy.core.operations import CountLeafs
|
|
5
|
+
from flamapy.core.operations.descriptor import OperationDescriptor
|
|
5
6
|
from flamapy.metamodels.fm_metamodel.models import FeatureModel
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class FMCountLeafs(CountLeafs):
|
|
9
10
|
|
|
11
|
+
facade = OperationDescriptor(
|
|
12
|
+
doc=(
|
|
13
|
+
'This operation counts the number of leaf features in a feature model. Leaf\n'
|
|
14
|
+
'features are those that do not have any child features. They represent the\n'
|
|
15
|
+
'most specific options in a product line.'
|
|
16
|
+
),
|
|
17
|
+
returns='Union[None, int]',
|
|
18
|
+
name='count_leafs', operation='FMCountLeafs'
|
|
19
|
+
)
|
|
20
|
+
|
|
10
21
|
def __init__(self) -> None:
|
|
11
22
|
self.result = 0
|
|
12
23
|
|
|
@@ -4,6 +4,7 @@ from typing import Optional, cast
|
|
|
4
4
|
from flamapy.core.models import VariabilityModel
|
|
5
5
|
from flamapy.core.exceptions import FlamaException
|
|
6
6
|
from flamapy.core.operations import EstimatedConfigurationsNumber
|
|
7
|
+
from flamapy.core.operations.descriptor import OperationDescriptor
|
|
7
8
|
from flamapy.metamodels.fm_metamodel.models import FeatureModel, Feature, Relation
|
|
8
9
|
|
|
9
10
|
|
|
@@ -15,6 +16,18 @@ class FMEstimatedConfigurationsNumber(EstimatedConfigurationsNumber):
|
|
|
15
16
|
and thus, the number is an upper limit of the real number of products.
|
|
16
17
|
"""
|
|
17
18
|
|
|
19
|
+
facade = OperationDescriptor(
|
|
20
|
+
doc=(
|
|
21
|
+
'This is an estimate of the total number of different products that can be\n'
|
|
22
|
+
"produced from a feature model. It's calculated by considering all possible\n"
|
|
23
|
+
'combinations of features. This can be a simple multiplication if all features\n'
|
|
24
|
+
'are independent, but in most cases, constraints and dependencies between\n'
|
|
25
|
+
'features need to be taken into account.'
|
|
26
|
+
),
|
|
27
|
+
returns='Union[None, int]',
|
|
28
|
+
name='estimated_number_of_configurations', operation='FMEstimatedConfigurationsNumber'
|
|
29
|
+
)
|
|
30
|
+
|
|
18
31
|
def __init__(self) -> None:
|
|
19
32
|
self.result = 0
|
|
20
33
|
self.feature_model: Optional[FeatureModel] = None
|
|
@@ -1,17 +1,40 @@
|
|
|
1
|
-
from typing import Optional
|
|
1
|
+
from typing import Any, Optional
|
|
2
2
|
|
|
3
3
|
from flamapy.core.models import VariabilityModel
|
|
4
4
|
from flamapy.core.exceptions import FlamaException
|
|
5
5
|
from flamapy.core.operations import Operation
|
|
6
|
+
from flamapy.core.operations.descriptor import OperationDescriptor, Input
|
|
6
7
|
from flamapy.metamodels.fm_metamodel.models import Feature
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
def _feature_ancestors_inputs(operation: Any, facade: Any, kwargs: dict[str, Any]) -> None:
|
|
11
|
+
operation.set_feature(facade.fm_model.get_feature_by_name(kwargs['feature_name']))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _feature_ancestors_result(result: Any) -> Any:
|
|
15
|
+
return [feature.name for feature in result]
|
|
16
|
+
|
|
17
|
+
|
|
9
18
|
class FMFeatureAncestors(Operation):
|
|
10
19
|
"""
|
|
11
20
|
This operation returns the list of ancestors of a given feature
|
|
12
21
|
(i.e., all parents recursively up to the root feature).
|
|
13
22
|
"""
|
|
14
23
|
|
|
24
|
+
facade = OperationDescriptor(
|
|
25
|
+
doc=(
|
|
26
|
+
'These are the features that are directly or indirectly the parent of a given\n'
|
|
27
|
+
'feature in a feature model. Ancestors of a feature are found by traversing up\n'
|
|
28
|
+
'the feature hierarchy. This information can be useful to understand the\n'
|
|
29
|
+
'context and dependencies of a feature.'
|
|
30
|
+
),
|
|
31
|
+
returns='Union[None, List[str]]',
|
|
32
|
+
name='feature_ancestors', operation='FMFeatureAncestors',
|
|
33
|
+
inputs=(Input('feature_name', str, required=True),),
|
|
34
|
+
input_adapter=_feature_ancestors_inputs,
|
|
35
|
+
result_adapter=_feature_ancestors_result,
|
|
36
|
+
)
|
|
37
|
+
|
|
15
38
|
def __init__(self) -> None:
|
|
16
39
|
self.result: list[Feature] = []
|
|
17
40
|
self.feature: Optional[Feature] = None
|
|
@@ -3,6 +3,7 @@ from typing import Any, cast
|
|
|
3
3
|
|
|
4
4
|
from flamapy.core.models import VariabilityModel
|
|
5
5
|
from flamapy.core.operations import Operation
|
|
6
|
+
from flamapy.core.operations.descriptor import OperationDescriptor, Input
|
|
6
7
|
from flamapy.core.exceptions import FlamaException
|
|
7
8
|
from flamapy.metamodels.fm_metamodel.models import (
|
|
8
9
|
FeatureModel,
|
|
@@ -12,12 +13,37 @@ from flamapy.metamodels.fm_metamodel.models import (
|
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
|
|
16
|
+
def _generate_random_attribute_inputs(operation: Any, facade: Any, kwargs: dict[str, Any]) -> None:
|
|
17
|
+
operation.set_name(kwargs['name'])
|
|
18
|
+
operation.set_domain(Domain([Range(kwargs['min_value'], kwargs['max_value'])], None))
|
|
19
|
+
operation.set_only_leaf_features(bool(kwargs.get('only_leaf_features', False)))
|
|
20
|
+
|
|
21
|
+
|
|
15
22
|
class GenerateRandomAttribute(Operation):
|
|
16
23
|
"""
|
|
17
24
|
This operation generate random values for a given attribute and set the attribute for all
|
|
18
25
|
features in the model.
|
|
19
26
|
"""
|
|
20
27
|
|
|
28
|
+
facade = OperationDescriptor(
|
|
29
|
+
doc=(
|
|
30
|
+
'Augments the feature model with a randomly-valued numeric attribute on its\n'
|
|
31
|
+
'features and returns the modified model. ``name`` is the attribute name, values are\n'
|
|
32
|
+
'drawn from the ``[min_value, max_value]`` range, and ``only_leaf_features`` limits\n'
|
|
33
|
+
'it to leaf features. Useful before attribute_optimization or feature bounds.'
|
|
34
|
+
),
|
|
35
|
+
returns='FeatureModel',
|
|
36
|
+
name='generate_random_attribute', operation='GenerateRandomAttribute',
|
|
37
|
+
kind='transformer',
|
|
38
|
+
inputs=(
|
|
39
|
+
Input('name', str, required=True),
|
|
40
|
+
Input('min_value', int, default=0),
|
|
41
|
+
Input('max_value', int, default=100),
|
|
42
|
+
Input('only_leaf_features', bool, default=False),
|
|
43
|
+
),
|
|
44
|
+
input_adapter=_generate_random_attribute_inputs,
|
|
45
|
+
)
|
|
46
|
+
|
|
21
47
|
def __init__(self) -> None:
|
|
22
48
|
self.result: FeatureModel
|
|
23
49
|
self._attribute_name: str = ''
|