Skip to content

Commit 7338d04

Browse files
committed
chore: release 2.6.0
2 parents 898b107 + 45dea2c commit 7338d04

17 files changed

Lines changed: 136 additions & 218 deletions

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
- name: Set up Python
3939
uses: actions/setup-python@v5
4040
with:
41-
python-version: '3.10'
41+
python-version: '3.11'
4242

4343
- name: Install dependencies
4444
run: |

README.md

Lines changed: 7 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -1,198 +1,12 @@
1-
# BDD plugin for flamapy
2-
- [BDD plugin for flamapy](#bdd-plugin-for-flamapy)
3-
- [Description](#description)
4-
- [Requirements and Installation](#requirements-and-installation)
5-
- [Functionality and usage](#functionality-and-usage)
6-
- [Load a feature model in UVL and create the BDD](#load-a-feature-model-in-uvl-and-create-the-bdd)
7-
- [Save the BDD in a file](#save-the-bdd-in-a-file)
8-
- [Load the BDD from a file](#load-the-bdd-from-a-file)
9-
- [Analysis operations](#analysis-operations)
10-
- [Contributing to the BDD plugin](#contributing-to-the-bdd-plugin)
1+
# flamapy-bdd
112

3+
BDD-based analysis for [flamapy](https://flamapy.org) feature models, built on
4+
the [dd](https://github.com/tulip-control/dd) library.
125

13-
## Description
14-
This plugin supports Binary Decision Diagrams (BDDs) representations for feature models.
6+
**Documentation:** https://docs.flamapy.org/framework/plugins/bdd_plugin
157

16-
The plugin is based on [flamapy](https://github.com/flamapy/core) and thus, it follows the same architecture:
8+
## Installation
179

18-
<p align="center">
19-
<img width="750" src="doc/bdd_plugin.png">
20-
</p>
21-
22-
The BDD plugin relies on the [dd](https://github.com/tulip-control/dd) library to manipulate BDDs.
23-
The complete documentation of such library is available [here](https://github.com/tulip-control/dd/blob/main/doc.md).
24-
25-
The following is an example of feature model and its BDD using complemented arcs.
26-
27-
<p align="center">
28-
<img width="750" src="doc/fm_example.png">
29-
</p>
30-
31-
<p align="center">
32-
<img width="750" src="doc/bdd_example.svg">
33-
</p>
34-
35-
## Requirements and Installation
36-
- Python 3.9+
37-
- This plugin depends on the [flamapy core](https://github.com/flamapy/core) and on the [Feature Model plugin](https://github.com/flamapy/fm_metamodel).
38-
39-
```
40-
pip install flamapy flamapy-fm flamapy-bdd
41-
```
42-
43-
We have tested the plugin on Linux, but Windows is also supported.
44-
45-
46-
## Functionality and usage
47-
The executable script [test_bdd_metamodel.py](https://github.com/flamapy/bdd_metamodel/blob/master/tests/test_bdd_metamodel.py) serves as an entry point to show the plugin in action.
48-
49-
The following functionality is provided:
50-
51-
52-
### Load a feature model in UVL and create the BDD
53-
```python
54-
from flamapy.metamodels.fm_metamodel.transformations import UVLReader
55-
from flamapy.metamodels.bdd_metamodel.transformations import FmToBDD
56-
57-
# Load the feature model from UVL
58-
feature_model = UVLReader('models/uvl_models/pizzas.uvl').transform()
59-
# Create the BDD from the feature model
60-
bdd_model = FmToBDD(feature_model).transform()
61-
```
62-
63-
64-
### Save the BDD in a file
65-
```python
66-
from flamapy.metamodels.bdd_metamodel.transformations import PNGWriter, DDDMPv3Writer
67-
# Save the BDD as an image in PNG
68-
PNGWriter(path='my_bdd.png', bdd_model).transform()
69-
# Save the BDD in a .dddmp file
70-
DDDMPv3Writer(f'my_bdd.dddmp', bdd_model).transform()
71-
```
72-
Writers available: DDDMPv3 ('dddmp'), DDDMPv2 ('dddmp'), JSON ('json'), Pickle ('p'), PDF ('pdf'), PNG ('png'), SVG ('svg').
73-
74-
### Load the BDD from a file
75-
```python
76-
from flamapy.metamodels.bdd_metamodel.transformations import JSONReader
77-
# Load the BDD from a .json file
78-
bdd_model = JSONReader(path='path/to/my_bdd.json').transform()
79-
```
80-
Readers available: JSON ('json'), DDDMP ('dddmp'), Pickle ('p').
81-
82-
*NOTE:* DDDMP and Pickle readers are not fully supported yet.
83-
84-
### Analysis operations
85-
86-
- Satisfiable
87-
88-
Return whether the model is satisfiable (valid):
89-
```python
90-
from flamapy.metamodels.bdd_metamodel.operations import BDDSatisfiable
91-
satisfiable = BDDSatisfiable().execute(bdd_model).get_result()
92-
print(f'Satisfiable? (valid?): {satisfiable}')
93-
```
94-
95-
- Configurations number
96-
97-
Return the number of configurations:
98-
```python
99-
from flamapy.metamodels.bdd_metamodel.operations import BDDConfigurationsNumber
100-
n_configs = BDDConfigurationsNumber().execute(bdd_model).get_result()
101-
print(f'#Configurations: {n_configs}')
102-
```
103-
104-
- Configurations
105-
106-
Enumerate the configurations of the model:
107-
```python
108-
from flamapy.metamodels.bdd_metamodel.operations import BDDConfigurations
109-
configurations = BDDConfigurations().execute(bdd_model).get_result()
110-
for i, config in enumerate(configurations, 1):
111-
print(f'Config {i}: {[feat for feat in config.elements if config.elements[feat]]}')
112-
```
113-
114-
- Sampling
115-
116-
Return a sample of the given size of uniform random configurations with or without replacement:
117-
```python
118-
from flamapy.metamodels.bdd_metamodel.operations import BDDSampling
119-
sampling_op = BDDSampling()
120-
sampling_op.set_sample_size(5)
121-
sampling_op.set_with_replacement(False) # Default False
122-
sample = sampling_op.execute(bdd_model).get_result()
123-
for i, config in enumerate(sample, 1):
124-
print(f'Config {i}: {[feat for feat in config.elements if config.elements[feat]]}')
125-
```
126-
127-
- Product Distribution
128-
129-
Return the number of products (configurations) having a given number of features:
130-
```python
131-
from flamapy.metamodels.bdd_metamodel.operations import BDDProductDistribution
132-
dist = BDDProductDistribution().execute(bdd_model).get_result()
133-
print(f'Product Distribution: {dist}')
134-
```
135-
136-
- Feature Inclusion Probability
137-
138-
Return the probability for a feature to be included in a valid configuration:
139-
```python
140-
from flamapy.metamodels.bdd_metamodel.operations import BDDFeatureInclusionProbability
141-
prob = BDDFeatureInclusionProbability().execute(bdd_model).get_result()
142-
for feat in prob.keys():
143-
print(f'{feat}: {prob[feat]}')
144-
```
145-
146-
- Core features
147-
148-
Return the core features (those features that are present in all the configurations):
149-
```python
150-
from flamapy.metamodels.bdd_metamodel.operations import BDDCoreFeatures
151-
core_features = BDDCoreFeatures().execute(bdd_model).get_result()
152-
print(f'Core features: {core_features}')
153-
```
154-
155-
- Dead features
156-
157-
Return the dead features (those features that are not present in any configuration):
158-
```python
159-
from flamapy.metamodels.bdd_metamodel.operations import BDDDeadFeatures
160-
dead_features = BDDDeadFeatures().execute(bdd_model).get_result()
161-
print(f'Dead features: {dead_features}')
162-
```
163-
164-
Most analysis operations support also a partial configuration as an additional argument, so the operation will return the result taking into account the given partial configuration. For example:
165-
166-
```python
167-
from flamapy.core.models import Configuration
168-
# Create a partial configuration
169-
elements = {'Pizza': True, 'Big': True}
170-
partial_config = Configuration(elements)
171-
# Calculate the number of configuration from the partial configuration
172-
configs_number_op = BDDConfigurationsNumber()
173-
configs_number_op.set_partial_configuration(partial_config)
174-
n_configs = configs_number_op.execute(bdd_model).get_result()
175-
print(f'#Configurations: {n_configs}')
10+
```bash
11+
pip install flamapy-bdd
17612
```
177-
178-
179-
## Contributing to the BDD plugin
180-
To contribute in the development of this plugin:
181-
182-
1. Fork the repository into your GitHub account.
183-
2. Clone the repository: `git@github.com:<<username>>/bdd_metamodel.git`
184-
3. Create a virtual environment: `python -m venv env`
185-
4. Activate the virtual environment: `source env/bin/activate`
186-
5. Install the plugin dependencies: `pip install flamapy flamapy-fm`
187-
6. Install the BDD plugin from the source code: `pip install -e bdd_metamodel`
188-
189-
Please try to follow the standards code quality to contribute to this plugin before creating a Pull Request:
190-
191-
- To analyze your Python code and output information about errors, potential problems, convention violations and complexity, pass the prospector with:
192-
193-
`make lint`
194-
195-
- To analyze the static type checker for Python and find bugs, pass the Mypy:
196-
197-
`make mypy`
198-

flamapy/metamodels/bdd_metamodel/models/bdd_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def get_all_paths(self) -> list[dict[str, bool]]:
5555

5656
def count_solutions(self) -> int:
5757
"""Count the number of valid configurations (SAT)."""
58-
return self.bdd.count(self.root, len(self.vars)) # count(node, n_vars)
58+
return self.bdd.count(self.root, len(self.vars_order)) # count(node, n_vars)
5959

6060
def dump_structure(self) -> None:
6161
"""Prints the structure of the BDD for debugging purposes."""

flamapy/metamodels/bdd_metamodel/models/utils/pl_model.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,21 +89,20 @@ def _get_alternative_formula(self, relation: Relation) -> str:
8989
return f" {and_str} ".join(f"({f})" for f in formula)
9090

9191
def _get_mutex_formula(self, relation: Relation) -> str:
92-
formula = []
93-
parent = relation.parent.name
94-
children = {child.name for child in relation.children}
9592
equiv_str = self.logic_connectives['EQUIVALENCE']
9693
and_str = self.logic_connectives['AND']
9794
not_str = self.logic_connectives['NOT']
9895
or_str = self.logic_connectives['OR']
96+
parent = relation.parent.name
97+
children = {child.name for child in relation.children}
98+
99+
one_child_formula = []
99100
for child in children:
100-
children_negatives = children - {child}
101-
neg_children = f" {and_str} ".join(not_str + cn for cn in children_negatives)
102-
formula.append(f"{child} {equiv_str} ({neg_children} {and_str} {parent})")
103-
formula_str = f" {and_str} ".join(f"({f})" for f in formula)
104-
children_or = f" {or_str} ".join(child for child in children)
105-
left = f"({parent} {equiv_str} {not_str} ({children_or}))"
106-
return f"{left} {or_str} ({formula_str})"
101+
other_children_or = f" {or_str} ".join(c for c in children - {child})
102+
one_child_formula.append(f"({not_str} {child} {equiv_str} ({other_children_or}))")
103+
one_child_str = f" {and_str} ".join(f for f in one_child_formula)
104+
no_child_str = f" {and_str} ".join(f"{not_str} {c}" for c in children)
105+
return f"({no_child_str}) {or_str} (({one_child_str}) {and_str} {parent})"
107106

108107
def _get_cardinality_formula(self, relation: Relation) -> str:
109108
parent = relation.parent.name
@@ -112,11 +111,13 @@ def _get_cardinality_formula(self, relation: Relation) -> str:
112111
and_str = self.logic_connectives['AND']
113112
or_str = self.logic_connectives['OR']
114113

114+
card_min = relation.card_min
115+
card_max = relation.card_max if relation.card_max != -1 else len(children)
115116
all_clauses = []
116117

117118
# 1. If the parent is active, prohibit combinations outside the range [min..max]
118119
for val in range(len(children) + 1):
119-
if val < relation.card_min or val > relation.card_max:
120+
if val < card_min or val > card_max:
120121
for combination in itertools.combinations(children, val):
121122
# To prohibit an exact combination: (NOT parent OR NOT child1 OR child2...)
122123
clause_parts = [f"{not_str}{parent}"]

flamapy/metamodels/bdd_metamodel/operations/bdd_configurations_with_n_features.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,30 @@
22

33
from flamapy.core.models import VariabilityModel
44
from flamapy.core.operations import Operation
5+
from flamapy.core.operations.descriptor import OperationDescriptor, Input
56
from flamapy.metamodels.configuration_metamodel.models import Configuration
67
from flamapy.metamodels.bdd_metamodel.models import BDDModel
78
from flamapy.metamodels.bdd_metamodel.operations.bdd_product_distribution import DistributionEngine
89

910

11+
def _configurations_with_n_features_result(result: Any) -> Any:
12+
return list(result)
13+
14+
1015
class BDDConfigurationsWithNFeatures(Operation):
1116
"""Operation to get all configurations with exactly n selected features from a BDD model."""
1217

18+
facade = OperationDescriptor(
19+
doc=(
20+
'Returns all valid configurations that have exactly n features selected.'
21+
),
22+
returns='Union[None, List[Configuration]]',
23+
name='configurations_with_n_features', operation='BDDConfigurationsWithNFeatures',
24+
default_backend='bdd',
25+
inputs=(Input('n', int, required=True, setter='set_n_features'),),
26+
result_adapter=_configurations_with_n_features_result,
27+
)
28+
1329
def __init__(self) -> None:
1430
self._result: Generator[Configuration, None, None]
1531
self.n_features: int = 0

flamapy/metamodels/bdd_metamodel/operations/bdd_false_optional_features.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from flamapy.metamodels.bdd_metamodel.models import BDDModel
1010

1111

12-
LOGGER = logging.getLogger('PySATFalseOptionalFeatures')
12+
LOGGER = logging.getLogger('BDDFalseOptionalFeatures')
1313

1414

1515
class BDDFalseOptionalFeatures(FalseOptionalFeatures):
@@ -25,11 +25,11 @@ def get_result(self) -> list[Any]:
2525

2626
def execute(self, model: VariabilityModel) -> 'BDDFalseOptionalFeatures':
2727
bdd_model = cast(BDDModel, model)
28-
try:
29-
feature_model = cast(FeatureModel, model.original_model)
30-
except FlamaException:
31-
LOGGER.exception("The transformation didn't attach the source model, "
32-
"which is required for this operation.")
28+
original_model = getattr(model, 'original_model', None)
29+
if original_model is None:
30+
raise FlamaException("The transformation didn't attach the source model, "
31+
"which is required for this operation.")
32+
feature_model = cast(FeatureModel, original_model)
3333
self._result = get_false_optional_features(bdd_model, feature_model)
3434
return self
3535

flamapy/metamodels/bdd_metamodel/operations/bdd_feature_inclusion_probability.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Any, Optional, cast
22

33
from flamapy.core.models import VariabilityModel
4+
from flamapy.core.operations.descriptor import OperationDescriptor
45
from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
56
from flamapy.metamodels.bdd_metamodel.models import BDDModel
67
from flamapy.metamodels.bdd_metamodel.operations.interfaces import FeatureInclusionProbability
@@ -14,6 +15,17 @@ class BDDFeatureInclusionProbability(FeatureInclusionProbability):
1415
(https://doi.org/10.1109/ICSE.2019.00091)]
1516
"""
1617

18+
facade = OperationDescriptor(
19+
doc=(
20+
'Returns a mapping from each feature name to its inclusion probability: the\n'
21+
'fraction of valid configurations in which that feature is selected. Core\n'
22+
'features have probability 1.0 and dead features have probability 0.0.'
23+
),
24+
returns='Union[None, Dict[str, float]]',
25+
name='feature_inclusion_probability', operation='BDDFeatureInclusionProbability',
26+
default_backend='bdd',
27+
)
28+
1729
def __init__(self) -> None:
1830
self._result: dict[Any, float] = {}
1931
self._partial_configuration: Optional[Configuration] = None

flamapy/metamodels/bdd_metamodel/operations/bdd_homogeneity.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
from typing import cast
22

33
from flamapy.core.models import VariabilityModel
4+
from flamapy.core.operations.descriptor import OperationDescriptor
45
from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
56
from flamapy.metamodels.bdd_metamodel.models import BDDModel
67
from flamapy.metamodels.bdd_metamodel.operations.interfaces import Homogeneity
78
from flamapy.metamodels.bdd_metamodel.operations import BDDCommonalityFactor
89

910

1011
class BDDHomogeneity(Homogeneity):
12+
13+
facade = OperationDescriptor(
14+
doc=(
15+
'Measures how similar the products of the feature model are to each other. It\n'
16+
'is computed as the average commonality factor across all features. A value of\n'
17+
'1.0 means all products are identical; lower values indicate more diversity.'
18+
),
19+
returns='Union[None, float]',
20+
name='homogeneity', operation='BDDHomogeneity', default_backend='bdd'
21+
)
1122
def __init__(self) -> None:
1223
self._result: float = 0.0
1324

flamapy/metamodels/bdd_metamodel/operations/bdd_metrics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def calculate_metamodel_metrics(self, model: VariabilityModel) -> list[dict[str,
5252
self.model = cast(BDDModel, model)
5353

5454
# Do some basic calculations to speedup the rest
55-
self._features = list(self.model.features_variables.keys())
55+
self._features = list(self.model.features_vars.keys())
5656
self._configurations_number = (
5757
bdd_operations.BDDConfigurationsNumber().execute(self.model).get_result()
5858
)

0 commit comments

Comments
 (0)