diff --git a/flamapy/metamodels/bdd_metamodel/models/bdd_model.py b/flamapy/metamodels/bdd_metamodel/models/bdd_model.py
index ed105cc..616ad6f 100644
--- a/flamapy/metamodels/bdd_metamodel/models/bdd_model.py
+++ b/flamapy/metamodels/bdd_metamodel/models/bdd_model.py
@@ -1,9 +1,11 @@
-from typing import Optional
+from typing import Optional, Any
-from dd.autoref import BDD, Function
+try:
+ import dd.cudd as _bdd
+except ImportError:
+ import dd.autoref as _bdd
from flamapy.core.models import VariabilityModel
-
from flamapy.metamodels.bdd_metamodel.models.utils.txtcnf import (
CNFLogicConnective,
TextCNFNotation,
@@ -26,43 +28,41 @@ def get_extension() -> str:
return 'bdd'
def __init__(self) -> None:
- self.bdd = BDD() # BDD manager
- self.cnf_formula: Optional[str] = None
- self.root: Optional[Function] = None
- self.variables: list[str] = []
+ self.bdd = _bdd.BDD() # BDD manager
+ self.root: Optional[_bdd.Function] = None
+ self.variables: dict[str, int] = {}
- def from_textual_cnf(self, textual_cnf_formula: str, variables: list[str]) -> None:
+ @classmethod
+ def from_textual_cnf(cls, cnf_formula: str, variables: list[str]) -> 'BDDModel':
"""Build the BDD from a textual representation of the CNF formula,
and the list of variables."""
- self.cnf_formula = textual_cnf_formula
- self.variables = variables
-
+ bdd_model = cls()
# Declare variables
- for var in self.variables:
- self.bdd.declare(var)
-
+ for var in variables:
+ bdd_model.bdd.declare(var)
# Build the BDD
- self.root = self.bdd.add_expr(self.cnf_formula)
+ bdd_model.root = bdd_model.bdd.add_expr(cnf_formula)
+ # Store variables
+ bdd_model.variables = bdd_model.bdd.vars
+ return bdd_model
def nof_nodes(self) -> int:
"""Return number of nodes in the BDD."""
return len(self.bdd)
- def get_node(self, index: int) -> Function:
- """Return the node at the given position (index)."""
- return self.bdd.var(self.bdd.var_at_level(index))
+ def get_node(self, var: Any) -> _bdd.Function:
+ """Return the node of the named var 'var'."""
+ return self.bdd.var(var)
- @staticmethod
- def level(node: Function) -> int:
+ def level(self, node: _bdd.Function) -> int:
"""Return the level of the node.
Non-terminal nodes start at 0.
Terminal nodes have level `s' being the `s' the number of variables.
"""
- return node.level
+ return node.level if not self.is_terminal_node(node) else len(self.bdd.vars)
- @staticmethod
- def index(node: Function) -> int:
+ def index(self, node: _bdd.Function) -> int:
"""Position (index) of the variable that labels the node `n` in the ordering.
Indexes start at 1.
@@ -72,42 +72,62 @@ def index(node: Function) -> int:
Example: node `n4` is labeled `B`, and `B` is in the 2nd position in ordering `[A,B,C]`,
thus level(n4) = 2.
"""
- return node.level + 1
+ return self.level(node) + 1
- @staticmethod
- def is_terminal_node(node: Function) -> bool:
+ def is_terminal_node(self, node: Any) -> bool:
"""Check if the node is a terminal node."""
- return node.var is None
+ #return node.var is None
+ return self.is_terminal_n0(node) or self.is_terminal_n1(node)
- @staticmethod
- def is_terminal_n1(node: Function) -> bool:
+ def is_terminal_n1(self, node: Any) -> bool:
"""Check if the node is the terminal node 1 (n1)."""
- return node.var is None and node.node == 1
+ return node == self.bdd.true
- @staticmethod
- def is_terminal_n0(node: Function) -> bool:
+ def is_terminal_n0(self, node: Any) -> bool:
"""Check if the node is the terminal node 0 (n0)."""
- return node.var is None and node.node == -1
+ return node == self.bdd.false
- @staticmethod
- def get_high_node(node: Function) -> Optional[Function]:
+ def get_high_node(self, node: _bdd.Function) -> Optional[_bdd.Function]:
"""Return the high (right, solid) node."""
return node.high
- @staticmethod
- def get_low_node(node: Function) -> Optional[Function]:
+ def get_low_node(self, node: _bdd.Function) -> Optional[_bdd.Function]:
"""Return the low (left, dashed) node.
If the arc is complemented it returns the negation of the left node.
"""
return node.low
- @staticmethod
- def get_value(node: Function, complemented: bool = False) -> int:
+ def get_value(self, node: _bdd.Function, complemented: bool = False) -> int:
"""Return the value (id) of the node considering complemented arcs."""
- value = node.node
- if BDDModel.is_terminal_n0(node):
+ value = int(node)
+ if self.is_terminal_n0(node):
value = 1 if complemented else 0
- elif BDDModel.is_terminal_n1(node):
+ elif self.is_terminal_n1(node):
value = 0 if complemented else 1
return value
+
+ def __str__(self) -> str:
+ result = f'Root: {self.root.var} ' \
+ f'(id: {int(self.root)}) ' \
+ f'(level: {self.level(self.root)}) ' \
+ f'(index: {self.index(self.root)})\n'
+ result += f'Vars: ({len(self.bdd.vars)})\n'
+ var_levels = dict(sorted(self.bdd.var_levels.items(), key=lambda item: item[1]))
+ for var in var_levels:
+ node = self.get_node(var)
+ result += f' |-{node.var} ' \
+ f'(id: {int(node)}) ' \
+ f'(level: {self.level(node)}) ' \
+ f'(index: {self.index(node)})\n'
+ node = self.bdd.false
+ result += f'Terminal node (n0): {node.var} ' \
+ f'(id: {int(node)}) ' \
+ f'(level: {self.level(node)}) ' \
+ f'(index: {self.index(node)})\n'
+ node = self.bdd.true
+ result += f'Terminal node (n1): {node.var} ' \
+ f'(id: {int(node)}) ' \
+ f'(level: {self.level(node)}) ' \
+ f'(index: {self.index(node)})\n'
+ return result
diff --git a/flamapy/metamodels/bdd_metamodel/operations/bdd_configurations.py b/flamapy/metamodels/bdd_metamodel/operations/bdd_configurations.py
index 9895a60..d3d7977 100644
--- a/flamapy/metamodels/bdd_metamodel/operations/bdd_configurations.py
+++ b/flamapy/metamodels/bdd_metamodel/operations/bdd_configurations.py
@@ -1,9 +1,9 @@
from typing import Optional, cast
from flamapy.core.models import VariabilityModel
-from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
from flamapy.core.operations import Configurations
-from flamapy.metamodels.bdd_metamodel.models.bdd_model import BDDModel
+from flamapy.metamodels.configuration_metamodel.models import Configuration
+from flamapy.metamodels.bdd_metamodel.models import BDDModel
class BDDConfigurations(Configurations):
@@ -12,8 +12,11 @@ class BDDConfigurations(Configurations):
It also supports the computation of all solutions from a partial configuration.
"""
- def __init__(self, partial_configuration: Optional[Configuration] = None) -> None:
+ def __init__(self) -> None:
self.result: list[Configuration] = []
+ self.partial_configuration: Optional[Configuration] = None
+
+ def set_partial_configuration(self, partial_configuration: Configuration) -> None:
self.partial_configuration = partial_configuration
def execute(self, model: VariabilityModel) -> 'BDDConfigurations':
diff --git a/flamapy/metamodels/bdd_metamodel/operations/bdd_configurations_number.py b/flamapy/metamodels/bdd_metamodel/operations/bdd_configurations_number.py
index cd36e41..5399947 100644
--- a/flamapy/metamodels/bdd_metamodel/operations/bdd_configurations_number.py
+++ b/flamapy/metamodels/bdd_metamodel/operations/bdd_configurations_number.py
@@ -1,9 +1,9 @@
from typing import Optional, cast
from flamapy.core.models import VariabilityModel
-from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
from flamapy.core.operations import ConfigurationsNumber
-from flamapy.metamodels.bdd_metamodel.models.bdd_model import BDDModel
+from flamapy.metamodels.configuration_metamodel.models import Configuration
+from flamapy.metamodels.bdd_metamodel.models import BDDModel
class BDDConfigurationsNumber(ConfigurationsNumber):
@@ -12,8 +12,11 @@ class BDDConfigurationsNumber(ConfigurationsNumber):
It also supports counting the solutions from a given partial configuration.
"""
- def __init__(self, partial_configuration: Optional[Configuration] = None) -> None:
+ def __init__(self) -> None:
self.result = 0
+ self.partial_configuration: Optional[Configuration] = None
+
+ def set_partial_configuration(self, partial_configuration: Optional[Configuration]) -> None:
self.partial_configuration = partial_configuration
def execute(self, model: VariabilityModel) -> 'BDDConfigurationsNumber':
@@ -37,5 +40,4 @@ def configurations_number(bdd_model: BDDModel,
values = dict(partial_configuration.elements.items())
u_func = bdd_model.bdd.let(values, bdd_model.root)
n_vars = len(bdd_model.variables) - len(values)
-
return bdd_model.bdd.count(u_func, nvars=n_vars)
diff --git a/flamapy/metamodels/bdd_metamodel/operations/bdd_feature_inclusion_probability.py b/flamapy/metamodels/bdd_metamodel/operations/bdd_feature_inclusion_probability.py
index d0f5f59..f2797f0 100644
--- a/flamapy/metamodels/bdd_metamodel/operations/bdd_feature_inclusion_probability.py
+++ b/flamapy/metamodels/bdd_metamodel/operations/bdd_feature_inclusion_probability.py
@@ -1,22 +1,19 @@
from typing import Any, Optional, cast
from collections import defaultdict
-from dd.autoref import Function
+#from dd.autoref import Function
from flamapy.core.models import VariabilityModel
from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
from flamapy.metamodels.bdd_metamodel.models import BDDModel
from flamapy.metamodels.bdd_metamodel.operations.interfaces import FeatureInclusionProbability
-from flamapy.metamodels.bdd_metamodel.operations import BDDConfigurations
+from flamapy.metamodels.bdd_metamodel.operations import BDDConfigurationsNumber
class BDDFeatureInclusionProbability(FeatureInclusionProbability):
"""The Feature Inclusion Probability (FIP) operation determines the probability
for a variable to be included in a valid solution.
- This is a brute-force implementation that enumerates all solutions
- for calculating the probabilities.
-
Ref.: [Heradio et al. 2019. Supporting the Statistical Analysis of Variability Models. SPLC.
(https://doi.org/10.1109/ICSE.2019.00091)]
"""
@@ -28,6 +25,7 @@ def __init__(self, partial_configuration: Optional[Configuration] = None) -> Non
def execute(self, model: VariabilityModel) -> 'BDDFeatureInclusionProbability':
bdd_model = cast(BDDModel, model)
self.result = feature_inclusion_probability(bdd_model, self.partial_configuration)
+ #self.result = variable_probabilities_single_traverse(bdd_model)
return self
def get_result(self) -> dict[Any, float]:
@@ -39,98 +37,133 @@ def feature_inclusion_probability(self) -> dict[Any, float]:
def feature_inclusion_probability(bdd_model: BDDModel,
config: Optional[Configuration] = None) -> dict[Any, float]:
- products = BDDConfigurations(config).execute(bdd_model).get_result()
- n_products = len(products)
- if n_products == 0:
- return {feature: 0.0 for feature in bdd_model.variables}
-
- prob = {}
- for feature in bdd_model.variables:
- prob[feature] = sum(feature in p.elements for p in products) / n_products
+ n_configs_op = BDDConfigurationsNumber()
+ n_configs_op.set_partial_configuration(config)
+ total_configs = n_configs_op.execute(bdd_model).get_result()
+
+ prob: dict[Any, float] = defaultdict(float)
+ if config is None:
+ for feature in bdd_model.variables:
+ values = {feature: True}
+ u_func = bdd_model.bdd.let(values, bdd_model.root)
+ n_vars = len(bdd_model.variables) - len(values)
+ prob[feature] = bdd_model.bdd.count(u_func, nvars=n_vars) / total_configs
+ else:
+ values = dict(config.elements.items())
+ for feature in bdd_model.variables:
+ feature_selected = values.get(feature, None)
+ values = {feature: True}
+ u_func = bdd_model.bdd.let(values, bdd_model.root)
+ n_vars = len(bdd_model.variables) - len(values)
+ prob[feature] = bdd_model.bdd.count(u_func, nvars=n_vars) / total_configs
+ if feature_selected is None:
+ values.pop(feature)
+ else:
+ values[feature] = feature_selected
return prob
+## TODO: The following is the optimized implementation from UNED
+## Algoritm available in the paper: https://doi.org/10.1109/ICSE.2019.00091
+## Currently, the implementation is not correct at all.
+
# def feature_inclusion_probability(bdd_model: BDDModel,
# config: Optional[Configuration] = None) -> dict[Any, float]:
# root = bdd_model.root
-# id_root = BDDModel.get_value(root, root.negated)
+# id_root = bdd_model.get_value(root, root.negated)
# prob: dict[int, float] = defaultdict(float)
# prob[id_root] = 1/2
# mark: dict[int, bool] = defaultdict(bool)
-# get_node_pr(root, prob, mark, root.negated)
+# get_node_pr(bdd_model, root, prob, mark, root.negated)
-# prob_n_phi: dict[int, float] = defaultdict(float)
+# prob_n_phi: dict[tuple[int, Any], float] = defaultdict(float)
+# prob_phi_n: dict[tuple[int, Any], float] = defaultdict(float)
# mark: dict[int, bool] = defaultdict(bool)
-# get_join_pr(root, prob, prob_n_phi, mark, root.negated)
-# # prob_phi = BDDModel.
-# # return dist[id_root]
-
-
-def get_node_pr(node: Function,
- prob: dict[int, float],
- mark: dict[int, bool],
- complemented: bool) -> float:
- id_node = BDDModel.get_value(node, complemented)
- mark[id_node] = mark[id_node]
-
- if not BDDModel.is_terminal_node(node):
-
- # explore low
- low = BDDModel.get_low_node(node)
- id_low = BDDModel.get_value(low, complemented)
- if BDDModel.is_terminal_node(low):
- prob[id_low] = prob[id_low] + prob[id_node]
- else:
- prob[id_low] = prob[id_low] + prob[id_node]/2
-
- if mark[id_node] != mark[id_low]:
- get_node_pr(low, prob, mark, complemented ^ low.negated)
-
- # explore high
- high = BDDModel.get_high_node(node)
- id_high = BDDModel.get_value(high, complemented)
- if BDDModel.is_terminal_node(high):
- prob[id_high] = prob[id_high] + prob[id_node]
- else:
- prob[id_high] = prob[id_high] + prob[id_node]/2
-
- if mark[id_node] != mark[id_high]:
- get_node_pr(high, prob, mark, complemented ^ high.negated)
-
-
-def get_join_pr(node: Function,
- prob: dict[int, float],
- prob_n_phi: dict[int, float],
- mark: dict[int, bool],
- complemented: bool) -> float:
- id_node = BDDModel.get_value(node, complemented)
- mark[id_node] = mark[id_node]
-
- if not BDDModel.is_terminal_node(node):
-
- # explore low
- low = BDDModel.get_low_node(node)
- id_low = BDDModel.get_value(low, complemented)
- if low == node:
- pass
-
-
- # if BDDModel.is_terminal_node(low):
- # prob[id_low] = prob[id_low] + prob[id_node]
- # else:
- # prob[id_low] = prob[id_low] + prob[id_node]/2
-
- # if mark[id_node] != mark[id_low]:
- # get_node_pr(low, prob, mark, complemented ^ low.negated)
-
- # # explore high
- # high = BDDModel.get_high_node(node)
- # id_high = BDDModel.get_value(high, complemented)
- # if BDDModel.is_terminal_node(high):
- # prob[id_high] = prob[id_high] + prob[id_node]
- # else:
- # prob[id_high] = prob[id_high] + prob[id_node]/2
-
- # if mark[id_node] != mark[id_high]:
- # get_node_pr(high, prob, mark, complemented ^ high.negated)
-
\ No newline at end of file
+# get_joint_pr(bdd_model, root, prob, prob_n_phi, prob_phi_n, mark, root.negated)
+
+# fip = {}
+# for xj in bdd_model.variables:
+# xj_node = bdd_model.get_node(xj)
+# xj_id = bdd_model.get_value(xj_node)
+# fip[xj] = prob_n_phi[xj_id] / prob[1]
+# return fip
+
+
+# def get_node_pr(bdd_model: BDDModel,
+# node: Function,
+# prob: dict[int, float],
+# mark: dict[int, bool],
+# complemented: bool) -> None:
+# id_node = bdd_model.get_value(node, complemented)
+# mark[id_node] = not mark[id_node]
+
+# if not bdd_model.is_terminal_node(node):
+
+# # explore low
+# low = bdd_model.get_low_node(node)
+# id_low = bdd_model.get_value(low, complemented)
+# if bdd_model.is_terminal_node(low):
+# prob[id_low] = prob[id_low] + prob[id_node]
+# else:
+# prob[id_low] = prob[id_low] + (prob[id_node] / 2)
+# if mark[id_node] != mark[id_low]:
+# get_node_pr(bdd_model, low, prob, mark, complemented ^ low.negated)
+
+# # explore high
+# high = bdd_model.get_high_node(node)
+# id_high = bdd_model.get_value(high, complemented)
+# if bdd_model.is_terminal_node(high):
+# prob[id_high] = prob[id_high] + prob[id_node]
+# else:
+# prob[id_high] = prob[id_high] + (prob[id_node] / 2)
+# if mark[id_node] != mark[id_high]:
+# get_node_pr(bdd_model, high, prob, mark, complemented ^ high.negated)
+
+
+# def get_joint_pr(bdd_model: BDDModel,
+# node: Function,
+# prob: dict[int, float],
+# prob_n_phi: dict[tuple[int, Any], float],
+# prob_phi_n: dict[tuple[int, Any], float],
+# mark: dict[int, bool],
+# complemented: bool) -> None:
+# id_node = bdd_model.get_value(node, complemented)
+# mark[id_node] = not mark[id_node]
+
+# if not bdd_model.is_terminal_node(node):
+
+# # explore low
+# low = bdd_model.get_low_node(node)
+# id_low = bdd_model.get_value(low, complemented)
+# if bdd_model.is_terminal_n0(low):
+# prob_phi_n[(id_node, False)] = 0.0
+# elif bdd_model.is_terminal_n1(low):
+# prob_phi_n[(id_node, False)] = 1.0
+# else:
+# if mark[id_node] != mark[id_low]:
+# get_joint_pr(bdd_model, low, prob, prob_n_phi, prob_phi_n, mark, complemented ^ low.negated)
+# prob_phi_n[(id_node, False)] = prob_phi_n[(id_low, None)] / (2 * prob[id_low])
+# prob_n_phi[(id_node, False)] = prob_phi_n[(id_node, False)] * prob[id_node]
+
+# # explore high
+# high = bdd_model.get_high_node(node)
+# id_high = bdd_model.get_value(high, complemented)
+# if bdd_model.is_terminal_n0(high):
+# prob_phi_n[(id_node, True)] = 0.0
+# elif bdd_model.is_terminal_n1(high):
+# prob_phi_n[(id_node, True)] = 1.0
+# else:
+# if mark[id_node] != mark[id_high]:
+# get_joint_pr(bdd_model, high, prob, prob_n_phi, prob_phi_n, mark, complemented ^ high.negated)
+# prob_phi_n[(id_node, True)] = prob_phi_n[(id_high, None)] / (2 * prob[id_high])
+# prob_n_phi[(id_node, True)] = prob_phi_n[(id_node, True)] * prob[id_node]
+
+# # Combine both low and high
+# prob_phi_n[(id_node, None)] = prob_phi_n[(id_node, True)] + prob_phi_n[(id_node, False)]
+# prob_n_phi[bdd_model.index(node)] = prob[bdd_model.index(node)] + prob_n_phi[id_node]
+
+# # Add joint probabilities of the removed nodes
+# for xj in range(bdd_model.index(node) + 1, bdd_model.index(high)):
+# prob_n_phi[xj] = prob_n_phi[xj] + (prob_n_phi[(id_node, True)] / 2)
+# for xj in range(bdd_model.index(node) + 1, bdd_model.index(low)):
+# prob_n_phi[xj] = prob_n_phi[xj] + (prob_n_phi[(id_node, False)] / 2)
diff --git a/flamapy/metamodels/bdd_metamodel/operations/bdd_metrics.py b/flamapy/metamodels/bdd_metamodel/operations/bdd_metrics.py
index 0d00139..5a91a45 100644
--- a/flamapy/metamodels/bdd_metamodel/operations/bdd_metrics.py
+++ b/flamapy/metamodels/bdd_metamodel/operations/bdd_metrics.py
@@ -30,9 +30,7 @@ def __init__(self) -> None:
self.model: Optional[VariabilityModel] = None
self.result: list[dict[str, Any]] = []
self.model_type_extension = "bdd"
- self._features: dict[int, str] = {}
- self._common_features: list[Any] = []
- self._dead_features: list[Any] = []
+ self._features: list[Any] = []
def get_result(self) -> list[dict[str, Any]]:
return self.result
@@ -41,6 +39,7 @@ def calculate_metamodel_metrics(self, model: VariabilityModel) -> list[dict[str,
self.model = cast(BDDModel, model)
#Do some basic calculations to speedup the rest
+ self._features = self.model.variables
# Get all methods that are marked with the metric_method decorator
metric_methods = [getattr(self, method_name) for method_name in dir(self)
@@ -53,13 +52,13 @@ def calculate_metamodel_metrics(self, model: VariabilityModel) -> list[dict[str,
return [method() for method in metric_methods]
@metric_method
- def valid(self) -> dict[str, Any]:
- """A feature model is valid if it represents at least one configuration."""
+ def satisfiable(self) -> dict[str, Any]:
+ """A feature model is satisfiable if it represents at least one configuration."""
if self.model is None:
raise FlamaException('Model not initialized.')
- name = "Valid (not void)"
- _valid = bdd_operations.BDDValid().execute(self.model).get_result()
- result = self.construct_result(name=name, doc=self.valid.__doc__, result=_valid)
+ name = "satisfiable (valid) (not void)"
+ _satisfiable = bdd_operations.BDDSatisfiable().execute(self.model).get_result()
+ result = self.construct_result(name=name, doc=self.satisfiable.__doc__, result=_satisfiable)
return result
@metric_method
@@ -94,7 +93,7 @@ def configurations(self) -> dict[str, Any]:
if self.model is None:
raise FlamaException('Model not initialized.')
name = "Configurations"
- _configurations = bdd_operations.BDDProducts().execute(self.model).get_result()
+ _configurations = bdd_operations.BDDConfigurations().execute(self.model).get_result()
result = self.construct_result(name=name,
doc=self.configurations.__doc__,
result=_configurations,
@@ -107,9 +106,35 @@ def number_of_configurations(self) -> dict[str, Any]:
if self.model is None:
raise FlamaException('Model not initialized.')
name = "Configurations"
- _configurations = bdd_operations.BDDProductsNumber().execute(self.model).get_result()
+ _configurations = bdd_operations.BDDConfigurationsNumber().execute(self.model).get_result()
result = self.construct_result(name=name,
- doc=self.configurations.__doc__,
+ doc=self.number_of_configurations.__doc__,
result=_configurations,
size=None)
return result
+
+ @metric_method
+ def product_distribution(self) -> dict[str, Any]:
+ """Product distribution of the feature model."""
+ if self.model is None:
+ raise FlamaException('Model not initialized.')
+ name = "Product distribution"
+ _dist = bdd_operations.BDDProductDistribution().execute(self.model).get_result()
+ result = self.construct_result(name=name,
+ doc=self.product_distribution.__doc__,
+ result=_dist,
+ size=None)
+ return result
+
+ @metric_method
+ def feature_inclusion_probabilities(self) -> dict[str, Any]:
+ """Feature inclusion probabilities of the feature model."""
+ if self.model is None:
+ raise FlamaException('Model not initialized.')
+ name = "Feature inclusion probabilities"
+ _prob = bdd_operations.BDDFeatureInclusionProbability().execute(self.model).get_result()
+ result = self.construct_result(name=name,
+ doc=self.feature_inclusion_probabilities.__doc__,
+ result=_prob,
+ size=None)
+ return result
\ No newline at end of file
diff --git a/flamapy/metamodels/bdd_metamodel/operations/bdd_product_distribution.py b/flamapy/metamodels/bdd_metamodel/operations/bdd_product_distribution.py
index b735501..85a1c87 100644
--- a/flamapy/metamodels/bdd_metamodel/operations/bdd_product_distribution.py
+++ b/flamapy/metamodels/bdd_metamodel/operations/bdd_product_distribution.py
@@ -48,47 +48,49 @@ def product_distribution(bdd_model: BDDModel) -> list[int]:
+ In index n, the number of products with n features activated.
"""
root = bdd_model.root
- id_root = BDDModel.get_value(root, root.negated)
+ id_root = bdd_model.get_value(root, root.negated)
dist: dict[int, list[int]] = {0: [], 1: [1]}
mark: dict[int, bool] = defaultdict(bool)
- get_prod_dist(root, dist, mark, root.negated)
- return dist[id_root]
+ get_prod_dist(bdd_model, root, dist, mark, root.negated)
+ # Complete distribution
+ distribution = dist[id_root] + [0] * (len(bdd_model.variables) + 1 - len(dist[id_root]))
+ return distribution
-def get_prod_dist(node: Function,
+def get_prod_dist(bdd_model: BDDModel,
+ node: Function,
dist: dict[int, list[int]],
mark: dict[int, bool],
complemented: bool) -> None:
- id_node = BDDModel.get_value(node, complemented)
+ id_node = bdd_model.get_value(node, complemented)
mark[id_node] = not mark[id_node]
- if not BDDModel.is_terminal_node(node):
+ if not bdd_model.is_terminal_node(node):
# traverse
- low = BDDModel.get_low_node(node)
- id_low = BDDModel.get_value(low, complemented)
-
+ low = bdd_model.get_low_node(node)
+ id_low = bdd_model.get_value(low, complemented)
if mark[id_node] != mark[id_low]:
- get_prod_dist(low, dist, mark, complemented ^ low.negated)
+ get_prod_dist(bdd_model, low, dist, mark, complemented ^ low.negated)
# compute low_dist to account for the removed nodes through low
- removed_nodes = BDDModel.index(low) - BDDModel.index(node) - 1
+ removed_nodes = bdd_model.index(low) - bdd_model.index(node) - 1
low_dist = [0] * (removed_nodes + len(dist[id_low]))
for i in range(removed_nodes + 1):
for j in range(len(dist[id_low])):
low_dist[i + j] = low_dist[i + j] + dist[id_low][j] * math.comb(removed_nodes, i)
# traverse
- high = BDDModel.get_high_node(node)
- id_high = BDDModel.get_value(high, complemented)
+ high = bdd_model.get_high_node(node)
+ id_high = bdd_model.get_value(high, complemented)
- high = BDDModel.get_high_node(node)
- id_high = BDDModel.get_value(high, complemented)
+ high = bdd_model.get_high_node(node)
+ id_high = bdd_model.get_value(high, complemented)
if mark[id_node] != mark[id_high]:
- get_prod_dist(high, dist, mark, complemented ^ high.negated)
+ get_prod_dist(bdd_model, high, dist, mark, complemented ^ high.negated)
# compute high_dist to account for the removed nodes through high
- removed_nodes = BDDModel.index(high) - BDDModel.index(node) - 1
+ removed_nodes = bdd_model.index(high) - bdd_model.index(node) - 1
high_dist = [0] * (removed_nodes + len(dist[id_high]))
for i in range(removed_nodes + 1):
for j in range(len(dist[id_high])):
diff --git a/flamapy/metamodels/bdd_metamodel/operations/bdd_sampling.py b/flamapy/metamodels/bdd_metamodel/operations/bdd_sampling.py
index 84e9de4..c737ad5 100644
--- a/flamapy/metamodels/bdd_metamodel/operations/bdd_sampling.py
+++ b/flamapy/metamodels/bdd_metamodel/operations/bdd_sampling.py
@@ -4,7 +4,7 @@
from flamapy.core.models import VariabilityModel
from flamapy.core.exceptions import FlamaException
from flamapy.core.operations import Sampling
-from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
+from flamapy.metamodels.configuration_metamodel.models import Configuration
from flamapy.metamodels.bdd_metamodel.models import BDDModel
diff --git a/flamapy/metamodels/bdd_metamodel/operations/bdd_satisfiable.py b/flamapy/metamodels/bdd_metamodel/operations/bdd_satisfiable.py
index cdc2a58..6dc3adb 100644
--- a/flamapy/metamodels/bdd_metamodel/operations/bdd_satisfiable.py
+++ b/flamapy/metamodels/bdd_metamodel/operations/bdd_satisfiable.py
@@ -3,10 +3,16 @@
from flamapy.core.models import VariabilityModel
from flamapy.core.operations import Satisfiable
from flamapy.metamodels.bdd_metamodel.models import BDDModel
-from flamapy.metamodels.bdd_metamodel.operations import BDDConfigurationsNumber
class BDDSatisfiable(Satisfiable):
+ """Checks if the BDD is not equal to its false terminal node to determine satisfiability.
+
+ If the BDD is not equal to the false node, it means there exists at least one valid assignment
+ of variables that satisfies the formula, indicating that the formula is satisfiable.
+ Otherwise, if the BDD is equal to the false node, it means no valid assignment of variables
+ satisfies the formula, indicating that the formula is not satisfiable.
+ """
def __init__(self) -> None:
self.result: bool = False
@@ -24,5 +30,4 @@ def execute(self, model: VariabilityModel) -> 'BDDSatisfiable':
def is_satisfiable(bdd_model: BDDModel) -> bool:
- n_configs = BDDConfigurationsNumber().execute(bdd_model).get_result()
- return n_configs > 0
+ return not bdd_model.is_terminal_n0(bdd_model.root)
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/__init__.py b/flamapy/metamodels/bdd_metamodel/transformations/__init__.py
index 1e5d100..d31eda9 100644
--- a/flamapy/metamodels/bdd_metamodel/transformations/__init__.py
+++ b/flamapy/metamodels/bdd_metamodel/transformations/__init__.py
@@ -1,5 +1,24 @@
from .fm_to_bdd import FmToBDD
-from .bdd_writer import BDDWriter
+from .json_writer import JSONWriter
+from .json_reader import JSONReader
+from .pickle_writer import PickleWriter
+from .pickle_reader import PickleReader
+from .dddmpv2_writer import DDDMPv2Writer
+from .dddmpv3_writer import DDDMPv3Writer
+from .dddmp_reader import DDDMPReader
+from .png_writer import PNGWriter
+from .svg_writer import SVGWriter
+from .pdf_writer import PDFWriter
-__all__ = ['FmToBDD', 'BDDWriter']
\ No newline at end of file
+__all__ = ['FmToBDD',
+ 'JSONWriter',
+ 'JSONReader',
+ 'PickleWriter',
+ 'PickleReader',
+ 'DDDMPv2Writer',
+ 'DDDMPv3Writer',
+ 'DDDMPReader',
+ 'PNGWriter',
+ 'SVGWriter',
+ 'PDFWriter']
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/_bdd_writer.py b/flamapy/metamodels/bdd_metamodel/transformations/_bdd_writer.py
new file mode 100644
index 0000000..ea74901
--- /dev/null
+++ b/flamapy/metamodels/bdd_metamodel/transformations/_bdd_writer.py
@@ -0,0 +1,27 @@
+from typing import Any
+
+from flamapy.core.transformations import ModelToText
+from flamapy.metamodels.bdd_metamodel.models import BDDModel
+
+
+class BDDWriter(ModelToText):
+
+ def __init__(self, path: str, source_model: BDDModel) -> None:
+ self.path = path
+ self.source_model = source_model
+ self._roots = None
+
+ def set_roots(self, roots: Any) -> None:
+ self._roots = roots
+
+ def transform(self) -> str:
+ if self._roots is None:
+ try:
+ self.source_model.bdd.dump(filename=self.path,
+ filetype=self.get_destination_extension())
+ except Exception:
+ self._roots = [self.source_model.root]
+ self.source_model.bdd.dump(filename=self.path,
+ roots=self._roots,
+ filetype=self.get_destination_extension())
+ return ''
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/bdd_writer.py b/flamapy/metamodels/bdd_metamodel/transformations/bdd_writer.py
deleted file mode 100644
index 1a96215..0000000
--- a/flamapy/metamodels/bdd_metamodel/transformations/bdd_writer.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import os
-from typing import Optional
-from enum import Enum
-
-from dd.autoref import Function
-
-from flamapy.core.transformations import ModelToText
-
-from flamapy.metamodels.bdd_metamodel.models.bdd_model import BDDModel
-
-
-class BDDDumpFormat(Enum):
- """Possible output format for representing a BDD."""
- DDDMP_V3 = 'dddmp'
- DDDMP_V2 = 'dddmp2'
- PDF = 'pdf'
- PNG = 'png'
- SVG = 'svg'
-
-
-class BDDWriter(ModelToText):
- """Create the dump file representing the argument BDD.
-
- The format can be:
- - dddmp v3: `'.dddmp'` (default)
- - dddmp v2: `'.dddmp2'`
- - PDF: `'.pdf'`
- - PNG: `'.png'`
- - SVG: `'.svg'`
- """
-
- @staticmethod
- def get_destination_extension() -> str:
- return BDDDumpFormat.DDDMP_V3.value
-
- def __init__(self, path: str, source_model: BDDModel, roots: Optional[list[Function]] = None,
- output_format: BDDDumpFormat = BDDDumpFormat.DDDMP_V3) -> None:
- self._path = path
- self._source_model = source_model
- self._output_format = output_format
- self._roots = roots
-
- def set_format(self, output_format: BDDDumpFormat) -> None:
- self._output_format = output_format
-
- def set_roots(self, roots: list[Function]) -> None:
- self._roots = roots
-
- def transform(self) -> str:
- self._source_model.bdd.dump(filename=self._path, roots=self._roots)
- if self._output_format == BDDDumpFormat.DDDMP_V3:
- # Convert to dddmp format version 3.0 (adding the '.varnames' field)
- result = dddmp_v2_to_v3(self._path)
- elif self._output_format == BDDDumpFormat.DDDMP_V2:
- with open(self._path, 'r', encoding='utf-8') as file:
- result = os.linesep.join(file.readlines())
- else:
- result = ''
- return result
-
-
-def dddmp_v2_to_v3(filepath: str) -> str:
- """Convert the file with the BDD dump in format dddmp version 2 to version 3.
-
- The difference between versions 2.0 and 3.0 is the addition of the '.varnames' field.
- """
- with open(filepath, 'r', encoding='utf-8') as file:
- lines = file.readlines()
- # Change version from 2.0 to 3.0
- i, line = next((i, l) for i, l in enumerate(lines) if '.ver DDDMP-2.0' in l)
- lines[i] = line.replace('2.0', '3.0')
-
- # Add '.varnames' field
- i, line = next((i, l) for i, l in enumerate(lines) if '.orderedvarnames' in l)
- lines.insert(i - 1, line.replace('.orderedvarnames', '.varnames'))
-
- with open(filepath, 'w', encoding='utf-8') as file:
- file.writelines(lines)
- return os.linesep.join(lines)
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/dddmp_reader.py b/flamapy/metamodels/bdd_metamodel/transformations/dddmp_reader.py
new file mode 100644
index 0000000..b9ebc20
--- /dev/null
+++ b/flamapy/metamodels/bdd_metamodel/transformations/dddmp_reader.py
@@ -0,0 +1,19 @@
+from flamapy.core.transformations import TextToModel
+from flamapy.metamodels.bdd_metamodel.models import BDDModel
+
+
+class DDDMPReader(TextToModel):
+
+ @staticmethod
+ def get_source_extension() -> str:
+ return 'dddmp'
+
+ def __init__(self, path: str) -> None:
+ self.path: str = path
+
+ def transform(self) -> BDDModel:
+ bdd_model = BDDModel()
+ bdd_model.root = bdd_model.bdd.load(self.path)[0]
+
+ bdd_model.variables = list(bdd_model.bdd.vars)
+ return bdd_model
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/dddmpv2_writer.py b/flamapy/metamodels/bdd_metamodel/transformations/dddmpv2_writer.py
new file mode 100644
index 0000000..19d3565
--- /dev/null
+++ b/flamapy/metamodels/bdd_metamodel/transformations/dddmpv2_writer.py
@@ -0,0 +1,19 @@
+import os
+import tempfile
+
+from flamapy.metamodels.bdd_metamodel.transformations._bdd_writer import BDDWriter
+
+
+class DDDMPv2Writer(BDDWriter):
+
+ @staticmethod
+ def get_destination_extension() -> str:
+ return 'dddmp'
+
+ def transform(self) -> str:
+ if self.path is None:
+ self.path = tempfile.NamedTemporaryFile(mode='w', encoding='utf8')
+ result = super().transform()
+ with open(self.path, 'r', encoding='utf8') as file:
+ result = os.linesep.join(file.readlines())
+ return result
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/dddmpv3_writer.py b/flamapy/metamodels/bdd_metamodel/transformations/dddmpv3_writer.py
new file mode 100644
index 0000000..908487e
--- /dev/null
+++ b/flamapy/metamodels/bdd_metamodel/transformations/dddmpv3_writer.py
@@ -0,0 +1,36 @@
+import os
+import tempfile
+
+from flamapy.metamodels.bdd_metamodel.transformations._bdd_writer import BDDWriter
+
+
+class DDDMPv3Writer(BDDWriter):
+ """The difference between versions 2.0 and 3.0 is the addition of the '.varnames' field."""
+
+ @staticmethod
+ def get_destination_extension() -> str:
+ return 'dddmp'
+
+ def transform(self) -> str:
+ if self.path is None:
+ self.path = tempfile.NamedTemporaryFile(mode='w', encoding='utf8')
+ result = super().transform()
+ result = dddmp_v2_to_v3(self.path)
+ return result
+
+
+def dddmp_v2_to_v3(filepath: str) -> str:
+ """Convert the file with the BDD dump in format dddmp version 2 to version 3."""
+ with open(filepath, 'r', encoding='utf8') as file:
+ lines = file.readlines()
+ # Change version from 2.0 to 3.0
+ i, line = next((i, l) for i, l in enumerate(lines) if '.ver DDDMP-2.0' in l)
+ lines[i] = line.replace('2.0', '3.0')
+
+ # Add '.varnames' field
+ i, line = next((i, l) for i, l in enumerate(lines) if '.orderedvarnames' in l)
+ lines.insert(i - 1, line.replace('.orderedvarnames', '.varnames'))
+
+ with open(filepath, 'w', encoding='utf8') as file:
+ file.writelines(lines)
+ return os.linesep.join(lines)
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/fm_to_bdd.py b/flamapy/metamodels/bdd_metamodel/transformations/fm_to_bdd.py
index 9b963be..b61f5cc 100644
--- a/flamapy/metamodels/bdd_metamodel/transformations/fm_to_bdd.py
+++ b/flamapy/metamodels/bdd_metamodel/transformations/fm_to_bdd.py
@@ -1,4 +1,5 @@
import itertools
+from typing import Optional
from flamapy.core.transformations import ModelToModel
from flamapy.metamodels.fm_metamodel.models import (
@@ -22,7 +23,7 @@ def get_destination_extension() -> str:
def __init__(self, source_model: FeatureModel) -> None:
self.source_model = source_model
self.counter = 1
- self.destination_model = BDDModel()
+ self.destination_model: Optional[BDDModel] = None
self.variables: dict[str, int] = {}
self.features: dict[int, str] = {}
self.clauses: list[list[int]] = []
@@ -163,10 +164,7 @@ def transform(self) -> BDDModel:
)
+ ")"
)
-
cnf_formula = and_connective.join(cnf_list)
- self.destination_model.from_textual_cnf(
- cnf_formula, list(self.variables.keys())
- )
-
+ self.destination_model = BDDModel.from_textual_cnf(cnf_formula,
+ list(self.variables.keys()))
return self.destination_model
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/json_reader.py b/flamapy/metamodels/bdd_metamodel/transformations/json_reader.py
new file mode 100644
index 0000000..2dc9f27
--- /dev/null
+++ b/flamapy/metamodels/bdd_metamodel/transformations/json_reader.py
@@ -0,0 +1,36 @@
+import json
+
+try:
+ import dd.cudd as _bdd
+except ImportError:
+ import dd.autoref as _bdd
+
+from flamapy.core.transformations import TextToModel
+from flamapy.metamodels.bdd_metamodel.models import BDDModel
+
+
+class JSONReader(TextToModel):
+
+ @staticmethod
+ def get_source_extension() -> str:
+ return 'json'
+
+ def __init__(self, path: str) -> None:
+ self.path: str = path
+ self.preserve_original_ordering: bool = False
+
+ def set_preserve_original_ordering(self, preserve_original_ordering: bool) -> None:
+ self.preserve_original_ordering = preserve_original_ordering
+
+ def transform(self) -> BDDModel:
+ bdd_model = BDDModel()
+ bdd_model.root = bdd_model.bdd.load(self.path)[0]
+ if self.preserve_original_ordering:
+ with open(self.path, 'r', encoding='utf8') as json_file:
+ data = json.load(json_file)
+ level_of_var = data['level_of_var']
+ bdd_model.bdd.reorder(level_of_var)
+ else:
+ _bdd.reorder(bdd_model.bdd)
+ bdd_model.variables = list(bdd_model.bdd.vars)
+ return bdd_model
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/json_writer.py b/flamapy/metamodels/bdd_metamodel/transformations/json_writer.py
new file mode 100644
index 0000000..49b2119
--- /dev/null
+++ b/flamapy/metamodels/bdd_metamodel/transformations/json_writer.py
@@ -0,0 +1,19 @@
+import os
+import tempfile
+
+from flamapy.metamodels.bdd_metamodel.transformations._bdd_writer import BDDWriter
+
+
+class JSONWriter(BDDWriter):
+
+ @staticmethod
+ def get_destination_extension() -> str:
+ return 'json'
+
+ def transform(self) -> str:
+ if self.path is None:
+ self.path = tempfile.NamedTemporaryFile(mode='w', encoding='utf8')
+ result = super().transform()
+ with open(self.path, 'r', encoding='utf8') as file:
+ result = os.linesep.join(file.readlines())
+ return result
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/pdf_writer.py b/flamapy/metamodels/bdd_metamodel/transformations/pdf_writer.py
new file mode 100644
index 0000000..67632e8
--- /dev/null
+++ b/flamapy/metamodels/bdd_metamodel/transformations/pdf_writer.py
@@ -0,0 +1,8 @@
+from flamapy.metamodels.bdd_metamodel.transformations._bdd_writer import BDDWriter
+
+
+class PDFWriter(BDDWriter):
+
+ @staticmethod
+ def get_destination_extension() -> str:
+ return 'pdf'
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/pickle_reader.py b/flamapy/metamodels/bdd_metamodel/transformations/pickle_reader.py
new file mode 100644
index 0000000..b84d532
--- /dev/null
+++ b/flamapy/metamodels/bdd_metamodel/transformations/pickle_reader.py
@@ -0,0 +1,19 @@
+from flamapy.core.transformations import TextToModel
+from flamapy.metamodels.bdd_metamodel.models import BDDModel
+
+
+class PickleReader(TextToModel):
+
+ @staticmethod
+ def get_source_extension() -> str:
+ return 'p'
+
+ def __init__(self, path: str) -> None:
+ self.path: str = path
+ self.preserve_original_ordering: bool = False
+
+ def transform(self) -> BDDModel:
+ bdd_model = BDDModel()
+ bdd_model.root = bdd_model.bdd.load(self.path)[0]
+ bdd_model.variables = list(bdd_model.bdd.vars)
+ return bdd_model
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/pickle_writer.py b/flamapy/metamodels/bdd_metamodel/transformations/pickle_writer.py
new file mode 100644
index 0000000..f854c4f
--- /dev/null
+++ b/flamapy/metamodels/bdd_metamodel/transformations/pickle_writer.py
@@ -0,0 +1,17 @@
+from flamapy.metamodels.bdd_metamodel.transformations._bdd_writer import BDDWriter
+
+
+class PickleWriter(BDDWriter):
+
+ @staticmethod
+ def get_destination_extension() -> str:
+ return 'p'
+
+ def transform(self) -> str:
+ if self._roots is None:
+ try:
+ self.source_model.bdd.dump(filename=self.path)
+ except Exception:
+ self._roots = [self.source_model.root]
+ self.source_model.bdd.dump(filename=self.path, roots=self._roots)
+ return ''
\ No newline at end of file
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/png_writer.py b/flamapy/metamodels/bdd_metamodel/transformations/png_writer.py
new file mode 100644
index 0000000..6eb3694
--- /dev/null
+++ b/flamapy/metamodels/bdd_metamodel/transformations/png_writer.py
@@ -0,0 +1,8 @@
+from flamapy.metamodels.bdd_metamodel.transformations._bdd_writer import BDDWriter
+
+
+class PNGWriter(BDDWriter):
+
+ @staticmethod
+ def get_destination_extension() -> str:
+ return 'png'
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/sat_to_bdd.py b/flamapy/metamodels/bdd_metamodel/transformations/sat_to_bdd.py
index a14abe4..ac89521 100644
--- a/flamapy/metamodels/bdd_metamodel/transformations/sat_to_bdd.py
+++ b/flamapy/metamodels/bdd_metamodel/transformations/sat_to_bdd.py
@@ -1,10 +1,12 @@
-from flamapy.core.transformations import ModelToModel
+from typing import Optional
+from flamapy.core.transformations import ModelToModel
from flamapy.metamodels.bdd_metamodel.models import BDDModel
from flamapy.metamodels.pysat_metamodel.models import PySATModel
class SATToBDD(ModelToModel):
+
@staticmethod
def get_source_extension() -> str:
return "pysat"
@@ -15,7 +17,7 @@ def get_destination_extension() -> str:
def __init__(self, source_model: PySATModel) -> None:
self.source_model = source_model
- self.destination_model = BDDModel()
+ self.destination_model: Optional[BDDModel] = None
def transform(self) -> BDDModel:
# Transform clauses to textual CNF notation required by the BDD
@@ -39,9 +41,8 @@ def transform(self) -> BDDModel:
)
+ ")"
)
-
cnf_formula = and_connective.join(cnf_list)
- self.destination_model.from_textual_cnf(cnf_formula,
- list(self.source_model.variables.keys()))
-
+ bdd_model = BDDModel.from_textual_cnf(cnf_formula,
+ list(self.source_model.variables.keys()))
+ self.destination_model = bdd_model
return self.destination_model
diff --git a/flamapy/metamodels/bdd_metamodel/transformations/svg_writer.py b/flamapy/metamodels/bdd_metamodel/transformations/svg_writer.py
new file mode 100644
index 0000000..ef38216
--- /dev/null
+++ b/flamapy/metamodels/bdd_metamodel/transformations/svg_writer.py
@@ -0,0 +1,8 @@
+from flamapy.metamodels.bdd_metamodel.transformations._bdd_writer import BDDWriter
+
+
+class SVGWriter(BDDWriter):
+
+ @staticmethod
+ def get_destination_extension() -> str:
+ return 'svg'
diff --git a/setup.py b/setup.py
index 5da95cd..540462e 100644
--- a/setup.py
+++ b/setup.py
@@ -24,7 +24,7 @@
install_requires=[
'flamapy~=1.7.0.dev0',
'flamapy-fm~=1.6.0.dev0',
- 'dd>=0.5.6',
+ 'dd~=0.5.7',
'graphviz~=0.20',
],
extras_require={
diff --git a/test_bdd_metamodel.py b/test_bdd_metamodel.py
new file mode 100644
index 0000000..c001e74
--- /dev/null
+++ b/test_bdd_metamodel.py
@@ -0,0 +1,154 @@
+"""
+The BDD plugin relies on the dd library.
+The representation depends on what you want and have installed.
+For solving small to medium size problems, say for teaching, or prototyping new algorithms,
+pure Python can be more convenient.
+To work with larger problems, it works better if you install the C library CUDD.
+Let's call these “backends”.
+
+The Flamapy BDD plugin imports the best available interface:
+try:
+ import dd.cudd as _bdd
+except ImportError:
+ import dd.autoref as _bdd
+
+The same user code can run with both the Python and C backends.
+The interfaces are almost identical, some differences may have, and thus,
+they are controlled with exceptions
+(e.g., Using the PickleWriter is only supported with dd.autoref,
+using the DDDMPv2Writer or DDDMPv3Writer is only supported with dd.cudd).
+"""
+
+import os
+
+from flamapy.metamodels.fm_metamodel.transformations import UVLReader
+from flamapy.metamodels.bdd_metamodel.models import BDDModel
+from flamapy.metamodels.bdd_metamodel.transformations import (
+ FmToBDD,
+ JSONWriter,
+ PNGWriter,
+ PickleWriter,
+ DDDMPv2Writer,
+ DDDMPv3Writer,
+ PDFWriter,
+ SVGWriter,
+ JSONReader,
+ PickleReader,
+ DDDMPReader
+)
+from flamapy.metamodels.bdd_metamodel.operations import (
+ BDDProductDistribution,
+ BDDFeatureInclusionProbability,
+ BDDSampling,
+ BDDConfigurationsNumber,
+ BDDConfigurations,
+ BDDCoreFeatures,
+ BDDDeadFeatures,
+ BDDSatisfiable
+)
+
+
+FM_PATH = 'tests/models/uvl_models/Pizzas.uvl'
+BDD_MODELS_PATH = 'tests/models/bdd_models/'
+
+
+def analyze_bdd(bdd_model: BDDModel) -> None:
+ # Satisfiable (valid)
+ satisfiable = BDDSatisfiable().execute(bdd_model).get_result()
+ print(f'Satisfiable (valid)?: {satisfiable}')
+
+ # Configurations numbers
+ n_configs = BDDConfigurationsNumber().execute(bdd_model).get_result()
+ print(f'#Configs: {n_configs}')
+
+ assert n_configs > 0 if satisfiable else n_configs == 0
+
+ # Configurations
+ configs = BDDConfigurations().execute(bdd_model).get_result()
+ for i, config in enumerate(configs, 1):
+ print(f'Config {i}: {config.get_selected_elements()}')
+
+ # BDD product distribution
+ dist = BDDProductDistribution().execute(bdd_model).get_result()
+ print(f'Product Distribution: {dist}')
+ print(f'#Products: {sum(dist)}')
+
+ # BDD feature inclusion probabilities
+ probabilities = BDDFeatureInclusionProbability().execute(bdd_model).get_result()
+ print('Feature Inclusion Probabilities:')
+ for feat, prob in probabilities.items():
+ print(f'{feat}: {prob}')
+
+ # Core features
+ core_features = BDDCoreFeatures().execute(bdd_model).get_result()
+ print(f'Core features: {core_features}')
+
+ # Dead features
+ dead_features = BDDDeadFeatures().execute(bdd_model).get_result()
+ print(f'Dead features: {dead_features}')
+
+ # BDD Sampling
+ sample_op = BDDSampling()
+ sample_op.set_sample_size(5)
+ sample = sample_op.execute(bdd_model).get_result()
+ print('Uniform Random Sampling:')
+ for i, config in enumerate(sample, 1):
+ print(f'Config {i}: {config.get_selected_elements()}')
+
+
+def main():
+ path, filename = os.path.split(FM_PATH)
+ filename = ''.join(filename.split('.')[:-1])
+
+ # Load the feature model from UVLReader
+ feature_model = UVLReader(FM_PATH).transform()
+
+ # Create the BDD from the FM
+ bdd_model = FmToBDD(feature_model).transform()
+ print(f'BDD model:\n{bdd_model}')
+
+ # Save the BDD to different image formats
+ PNGWriter(f'{filename}.{PNGWriter.get_destination_extension()}', bdd_model).transform()
+ SVGWriter(f'{filename}.{SVGWriter.get_destination_extension()}', bdd_model).transform()
+ PDFWriter(f'{filename}.{PDFWriter.get_destination_extension()}', bdd_model).transform()
+ # Serialize the BDD to different formats
+ JSONWriter(f'{filename}.{JSONWriter.get_destination_extension()}', bdd_model).transform()
+ try:
+ DDDMPv2Writer(f'{filename}.{DDDMPv2Writer.get_destination_extension()}2', bdd_model).transform()
+ except:
+ print(f'Warning: DDDMPv2 serialization is not supported.')
+ try:
+ DDDMPv3Writer(f'{filename}.{DDDMPv3Writer.get_destination_extension()}', bdd_model).transform()
+ except:
+ print(f'Warning: DDDMPv3 serialization is not supported.')
+ try:
+ PickleWriter(f'{filename}.p', bdd_model).transform()
+ except:
+ print(f'Warning: Pickle serialization is not supported.')
+
+ # Apply different analysis operations
+ analyze_bdd(bdd_model)
+
+ # Load the BDD model from a .json file
+ reader = JSONReader(f'{os.path.join(BDD_MODELS_PATH, filename)}.json')
+ reader.set_preserve_original_ordering(True)
+ bdd_model = reader.transform()
+ print(f'BDD model:\n{bdd_model}')
+ analyze_bdd(bdd_model)
+
+ # TODO: The following reader are not fully supported.
+ # Load the BDD model from a .dddmp file
+ # reader = DDDMPReader(f'{os.path.join(BDD_MODELS_PATH, filename)}.dddmp')
+ # bdd_model = reader.transform()
+ # print(f'BDD model:\n{bdd_model}')
+ # analyze_bdd(bdd_model)
+
+ # Load the BDD model from a .p file
+ # reader = PickleReader(f'{os.path.join(BDD_MODELS_PATH, filename)}.p')
+ # bdd_model = reader.transform()
+ # print(f'BDD model:\n{bdd_model}')
+ # analyze_bdd(bdd_model)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test_uned.py b/test_uned.py
new file mode 100644
index 0000000..18c30bc
--- /dev/null
+++ b/test_uned.py
@@ -0,0 +1,69 @@
+from flamapy.metamodels.fm_metamodel.transformations import UVLReader
+
+from flamapy.metamodels.bdd_metamodel.transformations import FmToBDD, DDDMPWriter
+from flamapy.metamodels.bdd_metamodel.operations import (
+ BDDProductDistribution,
+ BDDFeatureInclusionProbability,
+ BDDSampling,
+ BDDConfigurationsNumber,
+ BDDConfigurations,
+ BDDCoreFeatures,
+ BDDDeadFeatures,
+ BDDSatisfiable
+)
+
+
+def main():
+ # Load the feature model from UVLReader
+ feature_model = UVLReader('tests/input_fms/uvl_models/Pizzas.uvl').transform()
+
+ # Create the BDD from the FM
+ bdd_model = FmToBDD(feature_model).transform()
+
+ # Save the BDD as .dddmp file
+ DDDMPWriter(f'{feature_model.root.name}_bdd.{DDDMPWriter.get_destination_extension()}',
+ bdd_model).transform()
+
+ # Satisfiable (valid)
+ satisfiable = BDDSatisfiable().execute(bdd_model).get_result()
+ print(f'Satisfiable (valid)?: {satisfiable}')
+
+ # Configurations numbers
+ n_configs = BDDConfigurationsNumber().execute(bdd_model).get_result()
+ print(f'#Configs: {n_configs}')
+
+ # Configurations
+ configs = BDDConfigurations().execute(bdd_model).get_result()
+ for i, config in enumerate(configs, 1):
+ print(f'Config {i}: {config.get_selected_elements()}')
+
+ # BDD product distribution
+ dist = BDDProductDistribution().execute(bdd_model).get_result()
+ print(f'Product Distribution: {dist}')
+ print(f'#Products: {sum(dist)}')
+
+ # BDD feature inclusion probabilities
+ probabilities = BDDFeatureInclusionProbability().execute(bdd_model).get_result()
+ print('Feature Inclusion Probabilities:')
+ for feat, prob in probabilities.items():
+ print(f'{feat}: {prob}')
+
+ # Core features
+ core_features = BDDCoreFeatures().execute(bdd_model).get_result()
+ print(f'Core features: {core_features}')
+
+ # Dead features
+ dead_features = BDDDeadFeatures().execute(bdd_model).get_result()
+ print(f'Dead features: {dead_features}')
+
+ # BDD Sampling
+ sample_op = BDDSampling()
+ sample_op.set_sample_size(5)
+ sample = sample_op.execute(bdd_model).get_result()
+ print('Uniform Random Sampling:')
+ for i, config in enumerate(sample, 1):
+ print(f'Config {i}: {config.get_selected_elements()}')
+
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/tests/input_fms/featureide_models/jHipster.xml b/tests/input_fms/featureide_models/jHipster.xml
deleted file mode 100644
index 2138ace..0000000
--- a/tests/input_fms/featureide_models/jHipster.xml
+++ /dev/null
@@ -1,224 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- OAuth2
-
-
- SocialLogin
-
-
- MicroserviceApplication
-
-
-
-
- SQL
- MongoDB
-
-
-
-
-
- SocialLogin
-
-
- HTTPSession
- JWT
-
-
- Monolithic
-
- SQL
- MongoDB
-
-
-
-
-
-
-
- UaaServer
- Uaa
-
-
-
-
-
-
- OAuth2
-
-
-
- SocialLogin
-
-
- MicroserviceApplication
-
-
-
-
- SQL
-
- MongoDB
- Cassandra
-
-
-
-
-
-
- Server
-
- Protractor
-
-
-
-
-
-
- Server
-
- Protractor
-
-
-
-
- MySQL
-
- H2
- MySql
-
-
-
-
-
-
- MicroserviceApplication
- MicroserviceGateway
-
-
- JWT
- Uaa
-
-
-
-
-
- Monolithic
-
- JWT
-
- HTTPSession
- OAuth2
-
-
-
-
-
-
- MariaDB
-
- H2
- MariaDBDev
-
-
-
-
-
- PostgreSQL
-
- H2
- PostgreeSQLDev
-
-
-
-
-
-
- SpringWebSockets
- ClusteredSession
-
- Application
-
-
-
-
- Libsass
- Application
-
-
-
-
diff --git a/tests/input_fms/featureide_models/pizzas.xml b/tests/input_fms/featureide_models/pizzas.xml
deleted file mode 100644
index bb2d5cd..0000000
--- a/tests/input_fms/featureide_models/pizzas.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- CheesyCrust
- Big
-
-
-
-
diff --git a/tests/models/bdd_models/Pizzas.dddmp b/tests/models/bdd_models/Pizzas.dddmp
new file mode 100644
index 0000000..fa9589c
--- /dev/null
+++ b/tests/models/bdd_models/Pizzas.dddmp
@@ -0,0 +1,33 @@
+.ver DDDMP-3.0
+.mode A
+.varinfo 3
+.nnodes 18
+.nvars 12
+.nsuppvars 12
+.varnames Pizza Topping Salami Ham Mozzarella Size Normal Big Dough Neapolitan Sicilian CheesyCrust
+.suppvarnames Pizza Topping Salami Ham Mozzarella Size Normal Big Dough Neapolitan Sicilian CheesyCrust
+.orderedvarnames Pizza Topping Salami Ham Mozzarella Size Normal Big Dough Neapolitan Sicilian CheesyCrust
+.ids 0 1 2 3 4 5 6 7 8 9 10 11
+.permids 0 1 2 3 4 5 6 7 8 9 10 11
+.nroots 1
+.rootids -18
+.nodes
+1 T 1 0 0
+2 CheesyCrust 11 1 -1
+3 Sicilian 10 1 2
+4 Sicilian 10 2 1
+5 Neapolitan 9 3 4
+6 Dough 8 5 1
+7 Big 7 1 6
+8 Sicilian 10 1 -1
+9 Neapolitan 9 8 -8
+10 Dough 8 9 1
+11 Big 7 10 1
+12 Normal 6 7 11
+13 Size 5 12 1
+14 Mozzarella 4 13 1
+15 Ham 3 13 14
+16 Salami 2 13 15
+17 Topping 1 16 1
+18 Pizza 0 17 1
+.end
diff --git a/tests/models/bdd_models/Pizzas.json b/tests/models/bdd_models/Pizzas.json
new file mode 100644
index 0000000..1beca83
--- /dev/null
+++ b/tests/models/bdd_models/Pizzas.json
@@ -0,0 +1,21 @@
+{
+"level_of_var": {"Salami": 2, "Size": 5, "Sicilian": 10, "Pizza": 0, "Normal": 6, "Big": 7, "Dough": 8, "Ham": 3, "Topping": 1, "Neapolitan": 9, "Mozzarella": 4, "CheesyCrust": 11},
+"roots": [-94713387456898],
+"94713387453922": [10, "F", "T"],
+"94713387456066": [9, -94713387453922, 94713387453922],
+"94713387456098": [8, "T", 94713387456066],
+"94713387456162": [7, "T", 94713387456098],
+"94713387453954": [11, "F", "T"],
+"94713387456578": [10, "T", 94713387453954],
+"94713387456546": [10, 94713387453954, "T"],
+"94713387456610": [9, 94713387456578, 94713387456546],
+"94713387456642": [8, "T", 94713387456610],
+"94713387456674": [7, 94713387456642, "T"],
+"94713387456706": [6, 94713387456162, 94713387456674],
+"94713387456738": [5, "T", 94713387456706],
+"94713387456770": [4, "T", 94713387456738],
+"94713387456802": [3, 94713387456770, 94713387456738],
+"94713387456834": [2, 94713387456802, 94713387456738],
+"94713387456866": [1, "T", 94713387456834],
+"94713387456898": [0, "T", 94713387456866]
+}
diff --git a/tests/models/bdd_models/Pizzas.p b/tests/models/bdd_models/Pizzas.p
new file mode 100644
index 0000000..95adde6
Binary files /dev/null and b/tests/models/bdd_models/Pizzas.p differ
diff --git a/tests/models/bdd_models/Pizzas.pdf b/tests/models/bdd_models/Pizzas.pdf
new file mode 100644
index 0000000..acbc469
Binary files /dev/null and b/tests/models/bdd_models/Pizzas.pdf differ
diff --git a/tests/models/bdd_models/Pizzas.png b/tests/models/bdd_models/Pizzas.png
new file mode 100644
index 0000000..09ae81e
Binary files /dev/null and b/tests/models/bdd_models/Pizzas.png differ
diff --git a/tests/models/bdd_models/Pizzas.svg b/tests/models/bdd_models/Pizzas.svg
new file mode 100644
index 0000000..1176ba8
--- /dev/null
+++ b/tests/models/bdd_models/Pizzas.svg
@@ -0,0 +1,438 @@
+
+
+
+
+
diff --git a/tests/models/bdd_models/Pizzas_uned.dddmp b/tests/models/bdd_models/Pizzas_uned.dddmp
new file mode 100644
index 0000000..2f1b4ac
--- /dev/null
+++ b/tests/models/bdd_models/Pizzas_uned.dddmp
@@ -0,0 +1,34 @@
+.ver DDDMP-3.0
+.mode A
+.varinfo 1
+.dd Pizzas_uned.dddmp
+.nnodes 18
+.nvars 12
+.nsuppvars 12
+.varnames Pizza Topping Salami Ham Mozzarella Size Normal Big Dough Neapolitan Sicilian CheesyCrust
+.suppvarnames Pizza Topping Salami Ham Mozzarella Size Normal Big Dough Neapolitan Sicilian CheesyCrust
+.orderedvarnames Pizza Topping Salami Ham Mozzarella Size Normal Big Dough Neapolitan Sicilian CheesyCrust
+.ids 0 1 2 3 4 5 6 7 8 9 10 11
+.permids 0 1 2 3 4 5 6 7 8 9 10 11
+.nroots 1
+.rootids -18
+.nodes
+1 T 1 0 0
+2 11 11 1 -1
+3 10 10 1 2
+4 10 10 2 1
+5 9 9 3 4
+6 8 8 5 1
+7 7 7 1 6
+8 10 10 1 -1
+9 9 9 8 -8
+10 8 8 9 1
+11 7 7 10 1
+12 6 6 7 11
+13 5 5 12 1
+14 4 4 13 1
+15 3 3 13 14
+16 2 2 13 15
+17 1 1 16 1
+18 0 0 17 1
+.end
diff --git a/tests/models/bdd_models/Pizzas_v2.dddmp b/tests/models/bdd_models/Pizzas_v2.dddmp
new file mode 100644
index 0000000..8966e0e
--- /dev/null
+++ b/tests/models/bdd_models/Pizzas_v2.dddmp
@@ -0,0 +1,32 @@
+.ver DDDMP-2.0
+.mode A
+.varinfo 3
+.nnodes 18
+.nvars 12
+.nsuppvars 12
+.suppvarnames Pizza Topping Salami Ham Mozzarella Size Normal Big Dough Neapolitan Sicilian CheesyCrust
+.orderedvarnames Pizza Topping Salami Ham Mozzarella Size Normal Big Dough Neapolitan Sicilian CheesyCrust
+.ids 0 1 2 3 4 5 6 7 8 9 10 11
+.permids 0 1 2 3 4 5 6 7 8 9 10 11
+.nroots 1
+.rootids -18
+.nodes
+1 T 1 0 0
+2 CheesyCrust 11 1 -1
+3 Sicilian 10 1 2
+4 Sicilian 10 2 1
+5 Neapolitan 9 3 4
+6 Dough 8 5 1
+7 Big 7 1 6
+8 Sicilian 10 1 -1
+9 Neapolitan 9 8 -8
+10 Dough 8 9 1
+11 Big 7 10 1
+12 Normal 6 7 11
+13 Size 5 12 1
+14 Mozzarella 4 13 1
+15 Ham 3 13 14
+16 Salami 2 13 15
+17 Topping 1 16 1
+18 Pizza 0 17 1
+.end
diff --git a/tests/input_fms/uvl_models/JHipster.uvl b/tests/models/uvl_models/JHipster.uvl
similarity index 100%
rename from tests/input_fms/uvl_models/JHipster.uvl
rename to tests/models/uvl_models/JHipster.uvl
diff --git a/tests/input_fms/uvl_models/Pizzas.uvl b/tests/models/uvl_models/Pizzas.uvl
similarity index 100%
rename from tests/input_fms/uvl_models/Pizzas.uvl
rename to tests/models/uvl_models/Pizzas.uvl
diff --git a/tests/input_fms/uvl_models/Pizzas_complex.uvl b/tests/models/uvl_models/Pizzas_complex.uvl
similarity index 100%
rename from tests/input_fms/uvl_models/Pizzas_complex.uvl
rename to tests/models/uvl_models/Pizzas_complex.uvl