def to_bdd(circ_or_expr, output=None, manager=None, renamer=None): if renamer is None: _count = 0 def renamer(*_): nonlocal _count _count += 1 return f"x{_count}" if isinstance(circ_or_expr, aiger.BoolExpr): circ, output = circ_or_expr.aig, circ_or_expr.output else: circ = circ_or_expr node_map = dict(circ.node_map) assert len(circ.latches) == 0 if output is None: assert len(circ.outputs) == 1 output = node_map[fn.first(circ.outputs)] else: output = node_map[output] # By name instead. manager = BDD() if manager is None else manager input_refs_to_var = { ref: renamer(i, ref) for i, ref in enumerate(circ.inputs) } manager.declare(*input_refs_to_var.values()) gate_nodes = {} for gate in cmn.eval_order(circ): if isinstance(gate, aiger.aig.ConstFalse): gate_nodes[gate] = manager.add_expr('False') elif isinstance(gate, aiger.aig.Inverter): gate_nodes[gate] = ~gate_nodes[gate.input] elif isinstance(gate, aiger.aig.Input): gate_nodes[gate] = manager.add_expr(input_refs_to_var[gate.name]) elif isinstance(gate, aiger.aig.AndGate): gate_nodes[gate] = gate_nodes[gate.left] & gate_nodes[gate.right] return gate_nodes[output], manager, bidict(input_refs_to_var)
def to_bdd(aag: AAG): assert len(aag.outputs) == 1 assert len(aag.latches) == 0 gate_deps = {a & -2: {b & -2, c & -2} for a, b, c in aag.gates} gate_lookup = {a & -2: (a, b, c) for a, b, c in aag.gates} eval_order = list(toposort(gate_deps)) assert eval_order[0] <= set(aag.inputs) | {0, 1} bdd = BDD() bdd.declare(*(f'x{i}' for i in aag.inputs)) gate_nodes = {i: bdd.add_expr(f'x{i}') for i in aag.inputs} gate_nodes[0] = bdd.add_expr('False') gate_nodes[1] = bdd.add_expr('True') for gate in chain(*eval_order[1:]): out, i1, i2 = gate_lookup[gate] f1 = ~gate_nodes[i1 & -2] if i1 & 1 else gate_nodes[i1 & -2] f2 = ~gate_nodes[i2 & -2] if i2 & 1 else gate_nodes[i2 & -2] gate_nodes[out] = f1 & f2 out = aag.outputs[0] return (~gate_nodes[out & -2] if out & 1 else gate_nodes[out & -2]), bdd
class BDDModel: """ A Binary Decision Diagram (BDD) representation of the feature model given as a CNF formula. It relies on the dd module: https://pypi.org/project/dd/ """ AND = '&' OR = '|' NOT = '!' def __init__(self, feature_model: FeatureModel, cnf_formula: str): self.feature_model = feature_model self.cnf = cnf_formula.replace('-', '') self.variables = self._extract_variables(self.cnf) self.bdd = BDD() # Instantiate a manager self.declare_variables(self.variables) # Declare variables self.expression = self.bdd.add_expr(self.cnf) def _extract_variables(self, cnf_formula: str) -> list[str]: variables = set() for v in cnf_formula.split(): if BDDModel.AND not in v and BDDModel.OR not in v: var = v.strip().replace('(', '').replace(')', '').replace( BDDModel.NOT, '') variables.add(var) return list(variables) def declare_variables(self, variables: list[str]): for v in variables: self.bdd.declare(v) def serialize(self, filepath: str, filetype: str = 'png'): self.bdd.dump(filename=filepath, roots=[self.expression], filetype=filetype) def get_number_of_configurations( self, selected_features: list[Feature] = None, deselected_features: list[Feature] = None) -> int: if not selected_features: expr = self.cnf else: expr = f' {BDDModel.AND} '.join( [f.name for f in selected_features]) if deselected_features: expr += f' {BDDModel.AND} ' + f' {BDDModel.AND} !'.join( [f.name for f in deselected_features]) expr += f' {BDDModel.AND} ' + '{x}'.format(x=self.expression) u = self.bdd.add_expr(expr) return self.bdd.count(u, nvars=len(self.variables)) def get_configurations( self, selected_features: list[Feature] = None, deselected_features: list[Feature] = None ) -> list[FMConfiguration]: if not selected_features: expr = self.cnf else: expr = f' {BDDModel.AND} '.join( [f.name for f in selected_features]) if deselected_features: expr += f' {BDDModel.AND} ' + f' {BDDModel.AND} '.join( ['!' + f.name for f in deselected_features]) expr += f' {BDDModel.AND} ' + '{x}'.format(x=self.expression) u = self.bdd.add_expr(expr) configs = [] for c in self.bdd.pick_iter(u, care_vars=self.variables): elements = { self.feature_model.get_feature_by_name(f): True for f in c.keys() if c[f] } configs.append(FMConfiguration(elements)) return configs
def enumerate(self, cudd=True, export_pdf=None, return_count=False, return_solutions=False): if cudd: from dd.cudd import BDD else: from dd.autoref import BDD kconfig_file = f"{self.cwd}/{self.kconfig}" kconfig_hash = self.file_hash(kconfig_file) with cd(self.cwd): kconf = kconfiglib.Kconfig(kconfig_file) pre_variables = list() pre_expressions = list() for choice in kconf.choices: var_name = f"_choice_{choice.name}" pre_variables.append(var_name) # Build "exactly_one", expressing that exactly one of the symbols managed by this choice must be selected symbols = list(map(lambda sym: sym.name, choice.syms)) exactly_one = list() for sym1 in symbols: subexpr = list() for sym2 in symbols: if sym1 == sym2: subexpr.append(sym2) else: subexpr.append(f"!{sym2}") exactly_one.append("(" + " & ".join(subexpr) + ")") exactly_one = " | ".join(exactly_one) # If the choice is selected, exactly one choice element must be selected pre_expressions.append(f"{var_name} -> ({exactly_one})") # Each choice symbol in exactly_once depends on the choice itself, which will lead to "{symbol} -> {var_name}" rules being generated later on. This # ensures that if the choice is false, each symbol is false too. We do not need to handle that case here. # The choice may depend on other variables depends_on = self._dependencies_to_bdd_expr(choice.direct_dep) if depends_on: if choice.is_optional: pre_expressions.append(f"{var_name} -> {depends_on}") else: pre_expressions.append(f"{var_name} <-> {depends_on}") elif not choice.is_optional: # Always active pre_expressions.append(var_name) for symbol in kconf.syms.values(): if not self._can_be_handled_by_bdd(symbol): continue pre_variables.append(symbol.name) depends_on = self._dependencies_to_bdd_expr(symbol.direct_dep) if depends_on: pre_expressions.append(f"{symbol.name} -> {depends_on}") for selected_symbol, depends_on in symbol.selects: depends_on = self._dependencies_to_bdd_expr(depends_on) if depends_on: pre_expressions.append( f"({symbol.name} & ({depends_on})) -> {selected_symbol.name}" ) else: pre_expressions.append( f"{symbol.name} -> {selected_symbol.name}") logger.debug("Variables:") logger.debug("\n".join(pre_variables)) logger.debug("Expressions:") logger.debug("\n".join(pre_expressions)) variables = list() expressions = list() bdd = BDD() variable_count = 0 for variable in pre_variables: if variable[0] != "#": variables.append(variable) variable_count += 1 bdd.declare(variable) logger.debug(f"Got {variable_count} variables") constraint = "True" expression_count = 0 for expression in pre_expressions: if expression[0] != "#": expressions.append(expression) expression_count += 1 constraint += f" & ({expression})" logger.debug(f"Got {expression_count} rules") logger.debug(constraint) constraint = bdd.add_expr(constraint) if cudd: # Egal? logger.debug("Reordering ...") BDD.reorder(bdd) else: # Wichtig! Lesbarkeit++ falls gedumpt wird, Performance vermutlich auch. logger.debug("Collecting Garbage ...") bdd.collect_garbage() # See <http://www.ecs.umass.edu/ece/labs/vlsicad/ece667/reading/somenzi99bdd.pdf> for how to read the graphical representation. # A solid line is followed if the origin node is 1 # A dashed line is followed if the origin node is 0 # A path from a top node to 1 satisfies the function iff the number of negations ("-1" annotations) is even if export_pdf is not None: logger.info(f"Dumping to {export_pdf} ...") bdd.dump(export_pdf) logger.debug("Solving ...") # still need to be set, otherwise autoref and cudd complain and set them anyways. # care_vars = list(filter(lambda x: "meta_" not in x and "_choice_" not in x, variables)) if return_solutions: return bdd.pick_iter(constraint, care_vars=variables) if return_count: return len(bdd.pick_iter(constraint, care_vars=variables)) config_file = f"{self.cwd}/.config" for solution in bdd.pick_iter(constraint, care_vars=variables): logger.debug(f"Set {solution}") with open(config_file, "w") as f: for k, v in solution.items(): if v: print(f"CONFIG_{k}=y", file=f) else: print(f"# CONFIG_{k} is not set", file=f) with cd(self.cwd): kconf = kconfiglib.Kconfig(kconfig_file) kconf.load_config(config_file) int_values = list() int_names = list() for symbol in kconf.syms.values(): if (kconfiglib.TYPE_TO_STR[symbol.type] == "int" and symbol.visibility and symbol.ranges): for min_val, max_val, condition in symbol.ranges: if condition.tri_value: int_names.append(symbol.name) min_val = int(min_val.str_value, 0) max_val = int(max_val.str_value, 0) step_size = (max_val - min_val) // 8 if step_size == 0: step_size = 1 int_values.append( list(range(min_val, max_val + 1, step_size))) continue for int_config in itertools.product(*int_values): for i, int_name in enumerate(int_names): val = int_config[i] symbol = kconf.syms[int_name] logger.debug(f"Set {symbol.name} to {val}") symbol.set_value(str(val)) self._run_explore_experiment(kconf, kconfig_hash, config_file)
class BDDModel: """ A Binary Decision Diagram (BDD) representation of the feature model given as a CNF formula. It relies on the dd module: https://pypi.org/project/dd/ """ AND = '&' OR = '|' NOT = '!' def __init__(self, cnf_formula: str): self.cnf = cnf_formula.replace('-', '') self.variables = self._extract_variables(self.cnf) self.bdd = BDD() # Instantiate a manager self.declare_variables(self.variables) # Declare variables self.expression = self.bdd.add_expr(self.cnf) def _extract_variables(self, cnf_formula: str) -> list[str]: variables = set() for v in cnf_formula.split(): if BDDModel.AND not in v and BDDModel.OR not in v: var = v.strip().replace('(', '').replace(')', '').replace( BDDModel.NOT, '') variables.add(var) return list(variables) def declare_variables(self, variables: list[str]): for v in variables: self.bdd.declare(v) def serialize(self, filepath: str, filetype: str = 'png'): self.bdd.dump(filename=filepath, roots=[self.expression], filetype=filetype) def get_number_of_configurations(self, features: list[Feature] = None) -> int: if features is None: return self.bdd.count(self.expression, nvars=len(self.variables)) expr = f' {BDDModel.AND} '.join([ f.name for f in features ]) + f' {BDDModel.AND} ' + '{x}'.format(x=self.expression) u = self.bdd.add_expr(expr) return self.bdd.count(u, nvars=len(self.variables)) def get_configurations(self, features: list[Feature] = None ) -> list[FMConfiguration]: if features is None: expr = self.cnf else: expr = f' {BDDModel.AND} '.join([ f.name for f in features ]) + f' {BDDModel.AND} ' + '{x}'.format(x=self.expression) u = self.bdd.add_expr(expr) configs = [] for c in self.bdd.pick_iter(u, care_vars=self.variables): configs.append(sorted([f for f in c.keys() if c[f]])) return configs def get_uniform_random_sample(self, size: int) -> list[list[str]]: """This generates all configurations.""" configs = self.get_configurations() if size > len(configs): size = len(configs) return random.sample(configs, size) def get_random_configuration(self) -> list[str]: """This follows the Knut algorithm, but needs to be optimized""" solutions = self.bdd.count(self.expression, nvars=len(self.variables)) expr = "" variables = list(self.variables) while solutions > 1: feature = random.choice(variables) variables.remove(feature) possible_expr = expr + f' {BDDModel.AND} '.join( [feature]) + f' {BDDModel.AND} ' formula = possible_expr + '{x}'.format(x=self.expression) u = self.bdd.add_expr(formula) solutions = self.bdd.count(u, nvars=len(self.variables)) if solutions <= 0: possible_expr = expr + f' {BDDModel.AND} '.join( ['!' + feature]) + f' {BDDModel.AND} ' formula = possible_expr + '{x}'.format(x=self.expression) u = self.bdd.add_expr(formula) solutions = self.bdd.count(u, nvars=len(self.variables)) expr = possible_expr config = self.bdd.pick(u) return sorted([f for f in config.keys() if config[f]]) def get_sample_of_configurations(self, size: int) -> list[list[str]]: """ Bad implementation, we need to: The original algorithm by Knuth is specified on BDDs very efficiently, as the probabilities required for all the possible SAT solutions are computed just once with a single BDD traversal, and then reused every time a solution is generated. """ nof_configs = self.get_number_of_configurations() if size > nof_configs: size = nof_configs sample = list() while len(sample) < size: config = self.get_random_configuration() if config not in sample: sample.append(config) return sample