def test_path_negation(): levels = {'c1': 0, 'a1': 1, 'c2': 2, 'a2': 3} manager = BDD(levels) manager.configure(reordering=False) c1, a1, c2, a2 = map(manager.var, ['c1', 'a1', 'c2', 'a2']) bexpr = ((c1 & a1) | (~c1 & ~a1)) & ((c2 & a2) | (~c2 & ~a2)) assert bexpr.low.negated assert not bexpr.high.negated assert len(list(path(bexpr, (True, False, False, False)))) == 3 assert len(list(path(bexpr, (True, True, True, True)))) == 5 def merge(ctx, val, acc): if ctx.is_leaf: return ctx.path_negated ^ ctx.node_val return None def evaluate(vals): return fold_path(merge, bexpr, vals, initial=[]) for val in product(*(4 * [[False, True]])): expected = (val[0] == val[1]) and (val[2] == val[3]) assert evaluate(val) == expected
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)
class DataManager(): def __init__(self, variables, dimension=20): super().__init__() self.variables = set(variables) self.dimension = dimension # Dimension per variable self.bdd = BDD() # BDD Manager (dd/CUDD/Sylvan) self.bdd.declare(*[ '{}{}'.format(var, i) for var in self.variables for i in range(self.dimension) ]) self.size = {var: 0 for var in self.variables} # Current size for variables self.dejavu = {var: dict() for var in self.variables } # Dicts of values seen before def const(self, value): return self.bdd.true if value else self.bdd.false def encode(self, value, variable): if value == None: return self.bdd.false try: cube = self.dejavu[variable][value] except KeyError: # If the value is not seen before index = self.size[variable] # Bit-blasting integer-valued index d = { '{}{}'.format(variable, bit_index): bit_value == '1' for (bit_index, bit_value) in zip( range(self.dimension), "{0:0{length}b}".format( index, length=self.dimension)) } cube = self.bdd.cube(d) self.dejavu[variable][value] = cube self.size[variable] = self.size[variable] + 1 return cube def decode(self, value, variable): pass
def __init__(self, variables, dimension=20): super().__init__() self.variables = set(variables) self.dimension = dimension # Dimension per variable self.bdd = BDD() # BDD Manager (dd/CUDD/Sylvan) self.bdd.declare(*[ '{}{}'.format(var, i) for var in self.variables for i in range(self.dimension) ]) self.size = {var: 0 for var in self.variables} # Current size for variables self.dejavu = {var: dict() for var in self.variables } # Dicts of values seen before
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) 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} 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]
def to_bdd(circ_or_expr, output=None, manager=None, renamer=None, levels=None): if renamer is None: _count = 0 def renamer(*_): nonlocal _count _count += 1 return f"x{_count}" if not isinstance(circ_or_expr, aiger.BoolExpr): circ = aiger.to_aig(circ_or_expr, allow_lazy=True) assert len(circ.latches) == 0 if output is None: assert len(circ.outputs) == 1 output = fn.first(circ.outputs) expr = aiger.BoolExpr(circ) else: expr = circ_or_expr manager = BDD() if manager is None else manager input_refs_to_var = { ref: renamer(i, ref) for i, ref in enumerate(expr.inputs) } manager.declare(*input_refs_to_var.values()) if levels is not None: assert set(manager.vars.keys()) <= set(levels.keys()) levels = fn.project(levels, manager.vars.keys()) levels = fn.walk_keys(input_refs_to_var.get, levels) manager.reorder(levels) manager.configure(reordering=False) def lift(obj): if isinstance(obj, bool): return manager.true if obj else manager.false return obj inputs = {i: manager.var(input_refs_to_var[i]) for i in expr.inputs} out = expr(inputs, lift=lift) return out, out.bdd, bidict(input_refs_to_var)
def test_to_nx(): levels = {'x': 0, 'y': 1, 'z': 2, 'w': 3} manager = BDD() manager.declare(*levels.keys()) manager.reorder(levels) x, y, z, w = map(manager.var, "xyzw") bexpr = reduce(xor, [x, y, z, w]) g = to_nx(bexpr, pydot=True) assert len(g.nodes) == 4 + 2 assert len(g.edges) == 2 * 4 + 1
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)
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
def create_manager(): manager = BDD() manager.declare('x', 'y') manager.reorder({'x': 1, 'y': 0}) manager.configure(reordering=False) return manager
import funcy as fn import aiger from aiger_analysis.bdd import from_bdd, to_bdd try: from dd.cudd import BDD except ImportError: try: from dd.autoref import BDD except ImportError: raise ImportError("Cannot import dd.cudd or dd.autoref." + "Reinstall with BDD support.") aa = None bddmgr = BDD() to_bdd = fn.partial(to_bdd, manager=bddmgr) from_bdd = fn.partial(from_bdd, manager=bddmgr) class AAG(): """ Wrapper around py-aiger's BoolExpr class. Used to support an interface that's analogous to dd. """ def __init__(self, aag): self.aag = aag