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 @@ + + + + + + +bdd + + + +L-1 +ref + + + +L0 +0 + + + + +ref-28 + +@-28 + + + +28 + +Pizza-28 + + + +ref-28->28 + + +-1 + + + +L1 +1 + + + + +26 + +Topping-26 + + + +28->26 + + + + + +1 + +True-1 + + + +28->1 + + + + + + +L2 +2 + + + + +24 + +Salami-24 + + + +26->24 + + + + + +26->1 + + + + + + +L3 +3 + + + + +22 + +Ham-22 + + + +24->22 + + + + + + +18 + +Size-18 + + + +24->18 + + + + + +L4 +4 + + + + +20 + +Mozzarella-20 + + + +22->20 + + + + + + +22->18 + + + + + +L5 +5 + + + + +20->18 + + + + + +20->1 + + + + + + +L6 +6 + + + + +16 + +Normal-16 + + + +18->16 + + + + + +18->1 + + + + + + +L7 +7 + + + + +8 + +Big-8 + + + +16->8 + + + + + + +14 + +Big-14 + + + +16->14 + + + + + +L8 +8 + + + + +6 + +Dough-6 + + + +8->6 + + + + + +8->1 + + + + + + +13 + +Dough-13 + + + +14->13 + + + + + + +14->1 + + + + + +L9 +9 + + + + +4 + +Neapolitan-4 + + + +6->4 + + + + + +6->1 + + + + + + +12 + +Neapolitan-12 + + + +13->12 + + + + + +13->1 + + + + + + +L10 +10 + + + + +2 + +Sicilian-2 + + + +4->2 + + +-1 + + + +4->2 + + + + + +10 + +Sicilian-10 + + + +12->10 + + + + + + +11 + +Sicilian-11 + + + +12->11 + + + + + +L11 +11 + + + + +2->1 + + +-1 + + + +2->1 + + + + + +9 + +CheesyCrust-9 + + + +10->9 + + + + + +10->1 + + + + + + +11->9 + + + + + + +11->1 + + + + + +L12 +12 + + + + +9->1 + + +-1 + + + +9->1 + + + + + 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