Ctrl K
ComunidadGuías…

tools/bvdd.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3

# Copyright (c) the Selfie Project authors. All rights reserved.
# Please see the AUTHORS file for details. Use of this source code is
# governed by a BSD license that can be found in the LICENSE file.

# Selfie is a project of the Computational Systems Group at the
# Department of Computer Sciences of the University of Salzburg
# in Austria. For further information and code please refer to:

# selfie.cs.uni-salzburg.at

# Bitvector Decision Diagrams (BVDDs)

# Given an n-bit bitvector (currently for n == 8 only),
# we use 2**n-bit unsigned integers to represent
# sets of n-bit bitvector values that in turn represent
# branches in BVDDs over any number of n-bit bitvectors.

# Theta(2**n)-time set intersection (bitwise conjunction) with
# Omega(1/n) and O(2**n/n) spatial overhead

# ------------------------------------------------------------

import threading

def utilization(hits, misses):
    if hits + misses == 0:
        return "0.0%"
    else:
        return f"{round(hits / (hits + misses) * 100, 2)}% ({hits} hits, {misses} misses)"

class BVDD_Node:
    has_input = 0
    no_input = 1

    node_cache_lock = threading.Lock()
    node_cache = {}
    node_cache_hits = 0

    def __str__(self):
        if self.is_consistent() and self.is_dont_care():
            # all inputs map to the same output
            return "{" + f"[0,255] -> {self.get_dont_care_output()}" + "}"
        else:
            return "{" + ", ".join([f"{BVDD_Node.get_input_values(inputs)} -> {output}"
                for output, inputs in self.get_o2s().items()]) + "}"

    def cache(self):
        if self in BVDD_Node.node_cache:
            BVDD_Node.node_cache_hits += 1
        else:
            with BVDD_Node.node_cache_lock:
                if self not in BVDD_Node.node_cache:
                    BVDD_Node.node_cache[self] = self
        return BVDD_Node.node_cache[self]

    def is_consistent(self):
        return self.number_of_inputs() == 256

    def is_dont_care(self):
        return self.number_of_distinct_SBDD_outputs() == 1

    def is_constant(self):
        return self.is_dont_care() and isinstance(self.get_dont_care_output(), int)

    def number_of_connections(self):
        count = 0
        for output in self.get_s2o().values():
            if isinstance(output, BVDD):
                count += output.number_of_connections() + 1
        return count

    def number_of_outputs(self):
        count = 0
        for output in self.get_s2o().values():
            if isinstance(output, BVDD):
                count += output.number_of_outputs()
            else:
                count += 1
        return count

    def get_distinct_outputs(self):
        outputs = set()
        for output in self.get_s2o().values():
            if isinstance(output, BVDD):
                outputs |= output.get_distinct_outputs()
            else:
                outputs |= set({output})
        return outputs

    def number_of_distinct_outputs(self):
        return len(self.get_distinct_outputs())

    def number_of_exits(self):
        return self.number_of_distinct_outputs()

    def number_of_distinct_inputs(self, output_value = None):
        if self.is_dont_care():
            # dont-care inputs do not count
            output = self.get_dont_care_output()
            if isinstance(output, BVDD):
                return output.number_of_distinct_inputs(output_value)
            else:
                return 0
        else:
            count = 0
            s2o = self.get_s2o()
            for inputs in s2o:
                output = s2o[inputs]
                if isinstance(output, BVDD):
                    other_count = output.number_of_distinct_inputs(output_value)
                    assert output_value is not None or other_count > 0 # assert output.is_reduced()
                elif output_value is None or output == output_value:
                    other_count = 1
                else:
                    other_count = 0
                count += self.number_of_inputs_for_input(inputs) * other_count
            return count

    def number_of_solutions(self, output_value):
        return self.number_of_distinct_inputs(output_value)

    def get_input_values(inputs):
        assert inputs > 0
        input_values = [input_value for input_value in range(256)
            if 2**input_value & inputs != 0]
        if len(input_values) <= 128:
            return str(input_values)
        else:
            not_input_values = [not_input_value for not_input_value in range(256)
                if 2**not_input_value & inputs == 0]
            return "not " + str(not_input_values)

    def get_paths(self, exit_i, index_i = 0):
        path = []
        o2s = self.get_o2s()
        for output in o2s:
            inputs = o2s[output] if o2s[output] < 2**256-1 else 0
            if isinstance(output, BVDD):
                other_path = output.get_paths(exit_i, index_i + 1)
                if other_path:
                    path += [([(index_i, inputs)], other_path)]
            elif output == exit_i:
                path += [(index_i, inputs)]
        return path

    def inputs_union(self):
        # for s2o and o2s only
        union = 0
        for inputs in self.get_s2o():
            assert inputs & union == 0
            union |= inputs
        return union

    def number_of_inputs_for_input(self, inputs):
        # for s2o and o2s only
        return inputs.bit_count()

    def number_of_inputs(self):
        # for s2o and o2s only
        return self.number_of_inputs_for_input(self.inputs_union())

    def map(o2s, inputs, output):
        # for s2o and o2s only
        if output not in o2s:
            o2s[output] = inputs
        else:
            o2s[output] |= inputs

    def is_reduced(self):
        # only for i2o and o2s
        assert self.is_consistent()
        return True

    def reduce_SBDD(self):
        # only for i2o and o2s
        return self.cache()

    def reduce_BVDD(self, index = 1):
        # dont-care SBDDs are not reduced
        return self.cache()

    def reduce(self, reduction_tuple, index = 0):
        new_bvdd = type(self)({})
        s2o = self.get_s2o()
        for inputs in s2o:
            output = s2o[inputs]
            if isinstance(output, BVDD):
                reduced_output = output.reduce(reduction_tuple, index + 1)
                new_bvdd.set(inputs, reduced_output)
            else:
                new_bvdd.set(inputs, reduction_tuple[output])
        return new_bvdd.reduce_SBDD().reduce_BVDD(index)

    def compute_ite(self, bvdd2, bvdd3, op_id = None, bits = None):
        assert type(bvdd2) is type(self)
        assert type(bvdd3) is type(self)
        return self.compute_ternary(lambda x, y, z: y if x else z, bvdd2, bvdd3, op_id, bits)

    # for PDDs

    def number_of_partitioned_inputs(self):
        return self.number_of_distinct_inputs(BVDD_Node.has_input)

    def partitioned_constant():
        return BVDD.constant(BVDD_Node.has_input)

    def partitioned_projection(index, input_value):
        return BVDD.projection(index, 0, input_value)

    def op_union(output1, output2):
        return output1 if output2 == BVDD_Node.no_input else output2

    def union(self, bvdd2):
        assert type(bvdd2) is type(self)
        return self.compute_binary(lambda x, y: \
            BVDD_uncached.op_binary(BVDD_Node.op_union, x, y, "union", 1), bvdd2, "union")

    def op_intersection(output1, output2, output3 = 0):
        return output1 if output1 == BVDD_Node.no_input else output2 if output2 == BVDD_Node.no_input else output3

    def intersection(self, bvdd2, bvdd3 = None):
        assert type(bvdd2) is type(self)
        if bvdd3 is None:
            return self.compute_binary(lambda x, y: \
                BVDD_uncached.op_binary(BVDD_Node.op_intersection, x, y, "intersection", 1), bvdd2, "intersection")
        else:
            assert type(bvdd3) is type(self)
            return self.compute_ternary(lambda x, y, z: \
                BVDD_uncached.op_ternary(BVDD_Node.op_intersection, x, y, z, "intersection", 1), bvdd2, bvdd3, "intersection")

    def is_not_empty(self):
        return not (self.is_constant() and self.get_dont_care_output() == BVDD_Node.no_input)

    def is_not_full(self):
        return not (self.is_constant() and self.get_dont_care_output() == BVDD_Node.has_input)

    # for CFLOBVDDs

    def projection_proto(index, input_value = None):
        # use offset 1 for CFLOBVDDs
        return BVDD.projection(index, 1, input_value)

    def apply(self, return_tuple, index = 0, level = 0):
        new_bvdd = type(self)({})
        s2o = self.get_s2o()
        for inputs in s2o:
            output = s2o[inputs]
            if isinstance(output, BVDD):
                new_bvdd.set(inputs, output.apply(return_tuple, index + 1, level))
            elif index < 2**level - 1:
                new_bvdd.set(inputs, BVDD.constant(output).apply(return_tuple, index + 1, level))
            else:
                new_bvdd.set(inputs, return_tuple[output])
        return new_bvdd.reduce_SBDD().reduce_BVDD(index)

    def link(self, bvdd2, return_tuples):
        new_bvdd = type(self)({})
        s2o = self.get_s2o()
        for inputs in s2o:
            output = s2o[inputs]
            if isinstance(output, BVDD):
                new_bvdd.set(inputs, output.link(bvdd2, return_tuples))
            else:
                # apply may reduce to constant
                new_bvdd.set(inputs, bvdd2.apply(return_tuples[output], 1))
        return new_bvdd.reduce_BVDD()

    def swap(self):
        swap2right_bvdd = type(self)({})
        swap2right_exit_i = 1
        new_bvdd = BVDD.constant(1)
        return_tuples = {1:{}}
        s2o = self.get_s2o()
        for inputs in s2o:
            swap2right_bvdd.set(inputs, swap2right_exit_i)
            swap2right_exit_i += 1
            output = s2o[inputs]
            if not isinstance(output, BVDD):
                output = BVDD.constant(output)
            new_bvdd, pt = new_bvdd.pair_product(output)
            return_tuples = dict([(i,
                # pt[i][0] is the exit index of the already paired swap2left BVDDs
                # pt[i][1] is the exit index or output of the next swap2left BVDD being paired
                return_tuples[pt[i][0]] | {len(return_tuples[pt[i][0]]) + 1:pt[i][1]})
                    for i in pt])
        return new_bvdd.link(swap2right_bvdd, return_tuples)

    def unapply(self, b_rt_inv, rt = None, rt_inv = None, index = 0):
        new_bvdd = type(self)({})
        b_rt_inv = b_rt_inv if b_rt_inv is not None else {}
        rt = rt if rt is not None else {}
        rt_inv = rt_inv if rt_inv is not None else {}
        s2o = self.get_s2o()
        for inputs in s2o:
            output = s2o[inputs]
            if isinstance(output, BVDD):
                output, b_rt_inv, rt, rt_inv = output.unapply(b_rt_inv, rt, rt_inv, index + 1)
                new_bvdd.set(inputs, output)
            else:
                if output not in rt_inv:
                    rt_inv[output] = len(rt_inv) + 1
                    if output not in b_rt_inv:
                        b_rt_inv[output] = len(b_rt_inv) + 1
                    rt[rt_inv[output]] = b_rt_inv[output]
                new_bvdd.set(inputs, rt_inv[output])
        return new_bvdd.reduce_SBDD().reduce_BVDD(index), b_rt_inv, rt, rt_inv

    def upsample(self, level, a_rt_inv = None, b_rt_inv = None, b_cs = None, b_rts = None, index = 0):
        a_c = type(self)({})
        a_rt_inv = a_rt_inv if a_rt_inv is not None else {}
        b_rt_inv = b_rt_inv if b_rt_inv is not None else {}
        b_cs = b_cs if b_cs is not None else {}
        b_rts = b_rts if b_rts is not None else {}
        s2o = self.get_s2o()
        for inputs in s2o:
            output = s2o[inputs]
            if isinstance(output, BVDD):
                assert level > 0
                if index < 2**(level - 1) - 1:
                    output, a_rt_inv, b_rt_inv, b_cs, b_rts = output.upsample(level,
                        a_rt_inv, b_rt_inv, b_cs, b_rts, index + 1)
                    a_c.set(inputs, output)
                else:
                    output, b_rt_inv, rt, rt_inv = output.unapply(b_rt_inv)
                    key = (output, tuple(rt.values()))
                    if key not in a_rt_inv:
                        a_rt_inv[key] = len(a_rt_inv) + 1
                        b_cs[a_rt_inv[key]] = output
                        b_rts[a_rt_inv[key]] = rt
                    a_c.set(inputs, a_rt_inv[key])
            else:
                if output not in a_rt_inv:
                    a_rt_inv[output] = len(a_rt_inv) + 1
                    if output not in b_rt_inv:
                        b_rt_inv[output] = len(b_rt_inv) + 1
                    b_cs[a_rt_inv[output]] = BVDD.constant(1)
                    b_rts[a_rt_inv[output]] = {1:b_rt_inv[output]}
                a_c.set(inputs, a_rt_inv[output])
        return a_c.reduce_SBDD().reduce_BVDD(index), a_rt_inv, b_rt_inv, b_cs, b_rts

    def downsample(self, level, bvdds, return_tuples):
        assert level > 0
        # apply to bvdds may reduce to constants
        return self.apply(dict([(exit_i, bvdds[exit_i].apply(return_tuples[exit_i], 1))
            for exit_i in bvdds]), 0, level - 1)

    def tuple2exit(tuple_inv, tuple_ins):
        if tuple_ins not in tuple_inv:
            tuple_inv[tuple_ins] = len(tuple_inv) + 1
        return tuple_inv[tuple_ins]

    def pair_product(self, bvdd2):
        assert type(bvdd2) is type(self)
        pair_inv = {}
        return (self.compute_binary(lambda x, y: BVDD_Node.tuple2exit(pair_inv, (x, y)), bvdd2),
            dict([(pair_inv[pair], pair) for pair in pair_inv]))

    def triple_product(self, bvdd2, bvdd3):
        assert type(bvdd2) is type(self)
        assert type(bvdd3) is type(self)
        triple_inv = {}
        return (self.compute_ternary(lambda x, y, z: BVDD_Node.tuple2exit(triple_inv, (x, y, z)), bvdd2, bvdd3),
            dict([(triple_inv[triple], triple) for triple in triple_inv]))

    def print_profile():
        print("BVDD cache profile:")
        print(f"nodes: {utilization(BVDD_Node.node_cache_hits, len(BVDD_Node.node_cache))}")

    def extract(self, output_value):
        new_bvdd = BVDD({})
        s2o = self.get_s2o()
        for inputs in s2o:
            output = s2o[inputs]
            if output == output_value:
                new_bvdd.set(inputs, output)
            elif isinstance(output, BVDD):
                extracted_bvdd = output.extract(output_value)
                if extracted_bvdd.get_s2o():
                    new_bvdd.set(inputs, extracted_bvdd)
        # BVDD may be incomplete, only use for printing
        return new_bvdd

    def get_printed_inputs(self, output_value):
        return (f"{type(self).__name__}:\n" +
            f"{self.number_of_connections()} connections\n" +
            f"{self.number_of_outputs()} output values\n" +
            f"{self.number_of_distinct_outputs()} distinct output values (exits)\n" +
            f"{self.number_of_distinct_inputs()} distinct inputs\n" +
            f"{self.number_of_solutions(output_value)} solutions\n" +
            f"{self.extract(output_value)}")

class SBDD_i2o(BVDD_Node):
    # single-byte decision diagram with naive input-to-output mapping
    def __init__(self, i2o):
        self.i2o = i2o

    def __hash__(self):
        return hash((tuple(self.i2o.items()), any(isinstance(o, bool) for o in self.i2o.values())))

    def __eq__(self, bvdd2):
        return self is bvdd2 or (type(bvdd2) is type(self) and self.i2o == bvdd2.i2o)

    def get_s2o(self):
        return self.i2o

    def get_o2s(self):
        o2s = {}
        for input_value in self.i2o:
            BVDD_Node.map(o2s, 2**input_value, self.i2o[input_value])
        return o2s

    def set(self, input_value, output):
        assert input_value not in self.i2o
        self.i2o[input_value] = output

    def number_of_inputs_for_input(self, input_value):
        return 1

    def number_of_inputs(self):
        return len(self.i2o)

    def number_of_SBDD_outputs(self):
        return len(self.i2o.values())

    def number_of_distinct_SBDD_outputs(self):
        return len(set(self.i2o.values()))

    def get_dont_care_output(self):
        assert self.is_dont_care()
        return next(iter(self.i2o.values()))

    def is_always_false(self):
        return self.is_constant() and False in self.i2o.values()

    def is_always_true(self):
        return self.is_constant() and True in self.i2o.values()

    def constant_BVDD(self, output):
        assert (isinstance(output, bool) or
            isinstance(output, int) or
            isinstance(output, type(self)))
        self.i2o = dict([(input_value, output) for input_value in range(256)])
        return self.reduce_SBDD()

    def constant(output_value):
        return SBDD_i2o({}).constant_BVDD(output_value)

    def projection_BVDD(self, index = 0, offset = 0):
        self.i2o = dict([(input_value, input_value + offset) for input_value in range(256)])
        return self.reduce_SBDD()

    def projection(index = 0, offset = 0):
        return SBDD_i2o({}).projection_BVDD(index, offset)

    def compute_unary(self, op, op_id = None, bits = None):
        return type(self)(dict([(input_value, op(self.i2o[input_value]))
            for input_value in self.i2o])).reduce_SBDD()

    def get_s(self):
        new_bvdd = type(self)({})
        new_bvdd.i2o = dict([(input_value, input_value_i) for input_value_i, input_value in enumerate(self.i2o)])
        return new_bvdd

    def intersect_binary(self, bvdd2):
        assert type(bvdd2) is type(self)
        # bvdd1.i2o.keys() & bvdd2.i2o.keys()
        return [(input_value, input_value) for input_value in range(256)]

    def compute_binary(self, op, bvdd2, op_id = None, bits = None):
        assert type(bvdd2) is type(self)
        bvdd1 = self
        return type(self)(dict([(input_value,
            op(bvdd1.i2o[input_value], bvdd2.i2o[input_value]))
                for input_value, _ in bvdd1.intersect_binary(bvdd2)])).reduce_SBDD()

    def intersect_ternary(self, bvdd2, bvdd3):
        assert type(bvdd2) is type(self)
        assert type(bvdd3) is type(self)
        # bvdd1.i2o.keys() & bvdd2.i2o.keys() & bvdd3.i2o.keys()
        return [(input_value, input_value, input_value) for input_value in range(256)]

    def compute_ternary(self, op, bvdd2, bvdd3, op_id = None, bits = None):
        assert type(bvdd2) is type(self)
        assert type(bvdd3) is type(self)
        bvdd1 = self
        return type(self)(dict([(input_value,
            op(bvdd1.i2o[input_value], bvdd2.i2o[input_value], bvdd3.i2o[input_value]))
                for input_value, _, _ in bvdd1.intersect_ternary(bvdd2, bvdd3)])).reduce_SBDD()

class SBDD_s2o(BVDD_Node):
    # single-byte decision diagram with input-sets-to-output mapping
    def __init__(self, s2o):
        self.s2o = s2o

    def __hash__(self):
        return hash((tuple(self.s2o.items()), any(isinstance(o, bool) for o in self.s2o.values())))

    def __eq__(self, bvdd2):
        return self is bvdd2 or (type(bvdd2) is type(self) and self.s2o == bvdd2.s2o)

    def get_s2o(self):
        return self.s2o

    def get_o2s(self):
        assert self.is_reduced()
        return dict([(output, inputs) for inputs, output in self.s2o.items()])

    def set(self, inputs, output):
        assert inputs not in self.s2o
        self.s2o[inputs] = output

    def number_of_SBDD_outputs(self):
        return len(self.s2o.values())

    def number_of_distinct_SBDD_outputs(self):
        return len(set(self.s2o.values()))

    def get_dont_care_output(self):
        assert self.is_dont_care()
        return next(iter(self.s2o.values()))

    def is_reduced(self):
        return self.number_of_SBDD_outputs() == self.number_of_distinct_SBDD_outputs()

    def is_always_false(self):
        return self.is_constant() and False in self.s2o.values()

    def is_always_true(self):
        return self.is_constant() and True in self.s2o.values()

    def constant_BVDD(self, output):
        assert (isinstance(output, bool) or
            isinstance(output, int) or
            type(output) is type(self))
        self.s2o = {2**256-1:output}
        return self.reduce_SBDD()

    def constant(output_value):
        return SBDD_s2o({}).constant_BVDD(output_value)

    def projection_BVDD(self, index = 0, offset = 0):
        self.s2o = dict([(2**input_value, input_value + offset) for input_value in range(256)])
        return self.reduce_SBDD()

    def projection(index = 0, offset = 0):
        return SBDD_s2o({}).projection_BVDD(index, offset)

    def reduce_SBDD(self):
        if not self.is_reduced():
            o2s = {}
            for inputs in self.s2o:
                BVDD_Node.map(o2s, inputs, self.s2o[inputs])
            self.s2o = dict([(inputs, output) for output, inputs in o2s.items()])
        return self.cache()

    def compute_unary(self, op, op_id = None, bits = None):
        return type(self)(dict([(inputs, op(self.s2o[inputs])) for inputs in self.s2o])).reduce_SBDD()

    def get_s(self):
        new_bvdd = type(self)({})
        new_bvdd.s2o = dict([(inputs, inputs_i) for inputs_i, inputs in enumerate(self.s2o)])
        return new_bvdd

    def intersect_binary(self, bvdd2):
        assert type(bvdd2) is type(self)
        bvdd1 = self
        return [(inputs1 & inputs2, inputs1_i, inputs2_i)
            for inputs1_i, inputs1 in enumerate(bvdd1.s2o)
                for inputs2_i, inputs2 in enumerate(bvdd2.s2o)
                    if inputs1 & inputs2]

    def compute_binary(self, op, bvdd2, op_id = None, bits = None):
        assert type(bvdd2) is type(self)
        bvdd1 = self
        bvdd1_s2o = list(bvdd1.s2o.values())
        bvdd2_s2o = list(bvdd2.s2o.values())
        return type(self)(dict([(inputs, op(bvdd1_s2o[inputs1_i], bvdd2_s2o[inputs2_i]))
            for inputs, inputs1_i, inputs2_i in bvdd1.intersect_binary(bvdd2)])).reduce_SBDD()

    def intersect_ternary(self, bvdd2, bvdd3):
        assert type(bvdd2) is type(self)
        assert type(bvdd3) is type(self)
        bvdd1 = self
        return [(inputs1 & inputs2 & inputs3, inputs1_i, inputs2_i, inputs3_i)
            for inputs1_i, inputs1 in enumerate(bvdd1.s2o)
                for inputs2_i, inputs2 in enumerate(bvdd2.s2o)
                    for inputs3_i, inputs3 in enumerate(bvdd3.s2o)
                        if inputs1 & inputs2 & inputs3]

    def compute_ternary(self, op, bvdd2, bvdd3, op_id = None, bits = None):
        assert type(bvdd2) is type(self)
        assert type(bvdd3) is type(self)
        bvdd1 = self
        bvdd1_s2o = list(bvdd1.s2o.values())
        bvdd2_s2o = list(bvdd2.s2o.values())
        bvdd3_s2o = list(bvdd3.s2o.values())
        return type(self)(dict([(inputs,
            op(bvdd1_s2o[inputs1_i], bvdd2_s2o[inputs2_i], bvdd3_s2o[inputs3_i]))
                for inputs, inputs1_i, inputs2_i, inputs3_i in bvdd1.intersect_ternary(bvdd2, bvdd3)])).reduce_SBDD()

class SBDD_o2s(BVDD_Node):
    # single-byte decision diagram with output-to-input-sets mapping
    def __init__(self, o2s):
        self.o2s = o2s

    def __hash__(self):
        return hash((tuple(self.o2s.items()), any(isinstance(o, bool) for o in self.o2s)))

    def __eq__(self, bvdd2):
        return self is bvdd2 or (type(bvdd2) is type(self) and self.o2s == bvdd2.o2s)

    def get_s2o(self):
        return dict([(inputs, output) for output, inputs in self.o2s.items()])

    def get_o2s(self):
        return self.o2s

    def map(self, inputs, output):
        BVDD_Node.map(self.o2s, inputs, output)

    def set(self, inputs, output):
        self.map(inputs, output)

    def number_of_SBDD_outputs(self):
        return len(self.o2s)

    def number_of_distinct_SBDD_outputs(self):
        return self.number_of_SBDD_outputs()

    def get_dont_care_output(self):
        assert self.is_dont_care()
        return next(iter(self.o2s))

    def is_always_false(self):
        return self.is_constant() and False in self.o2s

    def is_always_true(self):
        return self.is_constant() and True in self.o2s

    def constant_BVDD(self, output):
        assert (isinstance(output, bool) or
            isinstance(output, int) or
            type(output) is type(self) or
            output is None)
        self.o2s = {output:2**256-1}
        return self.reduce_SBDD()

    def constant(output_value):
        return SBDD_o2s({}).constant_BVDD(output_value)

    def projection_BVDD(self, index = 0, offset = 0, input_value = None):
        if input_value is not None:
            if isinstance(input_value, int):
                self.o2s = {BVDD_Node.has_input + offset:2**input_value,
                    BVDD_Node.no_input + offset:(2**256-1) & ~(2**input_value)}
            else:
                # (0,8) normalizes to input_value (+ 0) with 8 bits
                self.o2s = {(0,8):2**256-1}
        else:
            self.o2s = dict([(input_value + offset, 2**input_value)
                for input_value in range(256)])
        return self.reduce_SBDD()

    def projection(index = 0, offset = 0, input_value = None):
        return SBDD_o2s({}).projection_BVDD(index, offset, input_value)

    def are_not_normalized_outputs(output1, output2, output3):
        return (not isinstance(output1, tuple) and
            (output2 is None or not isinstance(output2, tuple)) and
            (output3 is None or not isinstance(output3, tuple)))

    def are_not_BVDD(output1, output2, output3):
        return (not isinstance(output1, BVDD) and
            (output2 is None or not isinstance(output2, BVDD)) and
            (output3 is None or not isinstance(output3, BVDD)))

    def normalize(inputs, output_value, bits):
        if (isinstance(output_value, BVDD) or
            isinstance(output_value, bool) or
            bits is None or
            abs(bits) <= 64):
            # TODO: determine threshold on bits
            return output_value
        else:
            assert inputs > 0
            if inputs & (inputs - 1) != 0:
                # multiple input values
                return output_value
            else:
                # just one input value, make output value dependent on input value
                input_value = inputs.bit_length() - 1
                return output_value - input_value if bits >= 0 else \
                    (2**-bits - output_value) - input_value, bits

    def denormalize(input_value, output):
        if not isinstance(output, tuple):
            return output
        else:
            normalized_value = output[0]
            bits = output[1]
            assert 0 <= input_value < 256
            return input_value + normalized_value if bits >= 0 else \
                2**-bits - (input_value + normalized_value)

    def denormalize_op(op, input_value, output1, output2 = None, output3 = None):
        output1_value = SBDD_o2s.denormalize(input_value, output1)
        if output2 is None and output2 is None:
            return op(output1_value)
        else:
            output2_value = SBDD_o2s.denormalize(input_value, output2)
            if output3 is None:
                return op(output1_value, output2_value)
            else:
                output3_value = SBDD_o2s.denormalize(input_value, output3)
                return op(output1_value, output2_value, output3_value)

    def normalized_map(self, bits, op, op_id, inputs, output1, output2 = None, output3 = None):
        if SBDD_o2s.are_not_normalized_outputs(output1, output2, output3):
            self.map(inputs,
                SBDD_o2s.normalize(inputs,
                    SBDD_o2s.denormalize_op(op, inputs, output1, output2, output3), bits))
        else:
            if SBDD_o2s.are_not_BVDD(output1, output2, output3):
                if op_id == "ite":
                    self.map(inputs, op(output1, output2, output3))
                    return
                # TODO: pass normalized outputs to other op
            while inputs > 0:
                input_value = (inputs & -inputs).bit_length() - 1
                self.map(2**input_value,
                    SBDD_o2s.normalize(2**input_value,
                        SBDD_o2s.denormalize_op(op, input_value, output1, output2, output3), bits))
                inputs &= inputs - 1

    def compute_unary(self, op, op_id = None, bits = None):
        new_bvdd = type(self)({})
        for output in self.o2s:
            new_bvdd.normalized_map(bits, op, op_id, self.o2s[output], output)
        return new_bvdd.reduce_SBDD()

    def get_s(self):
        new_bvdd = type(self)({})
        new_bvdd.o2s = dict(enumerate(self.o2s.values()))
        return new_bvdd

    def intersect_binary(self, bvdd2):
        assert type(bvdd2) is type(self)
        bvdd1 = self
        return [(bvdd1.o2s[output1] & bvdd2.o2s[output2], output1_i, output2_i)
            for output1_i, output1 in enumerate(bvdd1.o2s)
                for output2_i, output2 in enumerate(bvdd2.o2s)
                    if bvdd1.o2s[output1] & bvdd2.o2s[output2]]

    def compute_binary(self, op, bvdd2, op_id = None, bits = None):
        assert type(bvdd2) is type(self)
        bvdd1 = self
        bvdd1_o2s = list(bvdd1.o2s)
        bvdd2_o2s = list(bvdd2.o2s)
        new_bvdd = type(self)({})
        for inputs, output1_i, output2_i in bvdd1.intersect_binary(bvdd2):
            new_bvdd.normalized_map(bits, op, op_id, inputs,
                bvdd1_o2s[output1_i], bvdd2_o2s[output2_i])
        return new_bvdd.reduce_SBDD()

    def intersect_ternary(self, bvdd2, bvdd3):
        assert type(bvdd2) is type(self)
        assert type(bvdd3) is type(self)
        bvdd1 = self
        return [(bvdd1.o2s[output1] & bvdd2.o2s[output2] & bvdd3.o2s[output3],
            output1_i, output2_i, output3_i)
                for output1_i, output1 in enumerate(bvdd1.o2s)
                    for output2_i, output2 in enumerate(bvdd2.o2s)
                        for output3_i, output3 in enumerate(bvdd3.o2s)
                            if bvdd1.o2s[output1] & bvdd2.o2s[output2] & bvdd3.o2s[output3]]

    def compute_ternary(self, op, bvdd2, bvdd3, op_id = None, bits = None):
        assert type(bvdd2) is type(self)
        assert type(bvdd3) is type(self)
        bvdd1 = self
        bvdd1_o2s = list(bvdd1.o2s)
        bvdd2_o2s = list(bvdd2.o2s)
        bvdd3_o2s = list(bvdd3.o2s)
        new_bvdd = type(self)({})
        for inputs, output1_i, output2_i, output3_i in bvdd1.intersect_ternary(bvdd2, bvdd3):
            new_bvdd.normalized_map(bits, op, op_id, inputs,
                bvdd1_o2s[output1_i], bvdd2_o2s[output2_i], bvdd3_o2s[output3_i])
        return new_bvdd.reduce_SBDD()

class BVDD_uncached(SBDD_o2s):
    def constant(output_value):
        return BVDD({}).constant_BVDD(output_value)

    def projection(index, offset = 0, input_value = None):
        if index == 0:
            return BVDD({}).projection_BVDD(0, offset, input_value)
        else:
            return BVDD({}).constant_BVDD(BVDD.projection(index - 1, offset, input_value))

    def reduce_BVDD(self, index = 1):
        assert self.is_reduced()
        if index > 0 and self.is_constant():
            # all inputs map to a constant
            return self.get_dont_care_output()
        else:
            # keep dont-care SBDDs at index 0
            return self.cache()

    def op_unary(op, output1, op_id, bits):
        if isinstance(output1, BVDD):
            return output1.compute_unary(op, op_id, bits).reduce_BVDD()
        else:
            return op(output1)

    def compute_unary(self, op, op_id = None, bits = None):
        return super().compute_unary(lambda x: BVDD.op_unary(op, x, op_id, bits), op_id, bits)

    def op_binary(op, output1, output2, op_id, bits):
        if not (isinstance(output1, BVDD) or isinstance(output2, BVDD)):
            return op(output1, output2)
        else:
            output1 = output1 if isinstance(output1, BVDD) else BVDD.constant(output1)
            output2 = output2 if isinstance(output2, BVDD) else BVDD.constant(output2)
            return output1.compute_binary(op, output2, op_id, bits).reduce_BVDD()

    def compute_binary(self, op, bvdd2, op_id = None, bits = None):
        assert isinstance(bvdd2, bool) or isinstance(bvdd2, int) or isinstance(bvdd2, BVDD)
        return super().compute_binary(lambda x, y: BVDD.op_binary(op, x, y, op_id, bits), bvdd2,
            op_id, bits)

    def op_ternary(op, output1, output2, output3, op_id, bits):
        if not (isinstance(output1, BVDD) or isinstance(output2, BVDD) or isinstance(output3, BVDD)):
            return op(output1, output2, output3)
        else:
            output1 = output1 if isinstance(output1, BVDD) else BVDD.constant(output1)
            output2 = output2 if isinstance(output2, BVDD) else BVDD.constant(output2)
            output3 = output3 if isinstance(output3, BVDD) else BVDD.constant(output3)
            return output1.compute_ternary(op, output2, output3, op_id, bits).reduce_BVDD()

    def compute_ternary(self, op, bvdd2, bvdd3, op_id = None, bits = None):
        assert isinstance(bvdd2, bool) or isinstance(bvdd2, int) or isinstance(bvdd2, BVDD)
        assert isinstance(bvdd3, bool) or isinstance(bvdd3, int) or isinstance(bvdd3, BVDD)
        return super().compute_ternary(lambda x, y, z: BVDD.op_ternary(op, x, y, z, op_id, bits),
            bvdd2, bvdd3, op_id, bits)

class BVDD_cached(BVDD_uncached):
    intersect_binary_lock = threading.Lock()
    intersect_binary_cache = {}
    intersect_binary_hits = 0

    def intersect_binary(self, bvdd2, intersection = None):
        s1 = self.get_s()
        s2 = bvdd2.get_s()
        if (s1, s2) in BVDD_cached.intersect_binary_cache:
            BVDD_cached.intersect_binary_hits += 1
        elif intersection:
            # lock is acquired
            BVDD_cached.intersect_binary_cache[(s1, s2)] = intersection
        else:
            # concurrent without acquiring lock
            intersection = super().intersect_binary(bvdd2)
            with BVDD_cached.intersect_binary_lock:
                return self.intersect_binary(bvdd2, intersection)
        return BVDD_cached.intersect_binary_cache[(s1, s2)]

    intersect_ternary_lock = threading.Lock()
    intersect_ternary_cache = {}
    intersect_ternary_hits = 0

    def intersect_ternary(self, bvdd2, bvdd3, intersection = None):
        s1 = self.get_s()
        s2 = bvdd2.get_s()
        s3 = bvdd3.get_s()
        if (s1, s2, s3) in BVDD_cached.intersect_ternary_cache:
            BVDD_cached.intersect_ternary_hits += 1
        elif intersection:
            # lock is acquired
            BVDD_cached.intersect_ternary_cache[(s1, s2, s3)] = intersection
        else:
            # concurrent without acquiring lock
            intersection = super().intersect_ternary(bvdd2, bvdd3)
            with BVDD_cached.intersect_ternary_lock:
                return self.intersect_ternary(bvdd2, bvdd3, intersection)
        return BVDD_cached.intersect_ternary_cache[(s1, s2, s3)]

    constant_lock = threading.Lock()
    constant_cache = {}
    boolean_cache = {}
    constant_hits = 0

    def constant_BVDD(self, output, constant = None):
        cache = BVDD_cached.boolean_cache if isinstance(output, bool) \
            else BVDD_cached.constant_cache
        if output in cache:
            BVDD_cached.constant_hits += 1
        elif constant:
            # lock is acquired
            cache[output] = constant
        else:
            # concurrent without acquiring lock
            constant = super().constant_BVDD(output)
            with BVDD_cached.constant_lock:
                return self.constant_BVDD(output, constant)
        return cache[output]

    projection_lock = threading.Lock()
    projection_cache = {}
    projection_hits = 0

    def projection_BVDD(self, index = 0, offset = 0, input_value = None, projection = None):
        if (index, offset, input_value) in BVDD_cached.projection_cache:
            BVDD_cached.projection_hits += 1
        elif projection:
            # lock is acquired
            BVDD_cached.projection_cache[(index, offset, input_value)] = projection
        else:
            # concurrent without acquiring lock
            projection = super().projection_BVDD(index, offset, input_value)
            with BVDD_cached.projection_lock:
                return self.projection_BVDD(index, offset, input_value, projection)
        return BVDD_cached.projection_cache[(index, offset, input_value)]

    compute_unary_lock = threading.Lock()
    compute_unary_cache = {}
    compute_unary_hits = 0

    def compute_unary(self, op, op_id = None, bits = None, unary = None):
        if op_id is None:
            return super().compute_unary(op, op_id, bits)
        elif (op_id, bits, self) in BVDD_cached.compute_unary_cache:
            # assert (super().compute_unary(op, op_id, bits) ==
            #     BVDD_cached.compute_unary_cache[(op_id, bits, self)])
            BVDD_cached.compute_unary_hits += 1
        elif unary:
            # lock is acquired
            BVDD_cached.compute_unary_cache[(op_id, bits, self)] = unary
        else:
            # concurrent without acquiring lock
            unary = super().compute_unary(op, op_id, bits)
            with BVDD_cached.compute_unary_lock:
                return self.compute_unary(op, op_id, bits, unary)
        return BVDD_cached.compute_unary_cache[(op_id, bits, self)]

    compute_binary_lock = threading.Lock()
    compute_binary_cache = {}
    compute_binary_hits = 0

    def compute_binary(self, op, bvdd2, op_id = None, bits = None, binary = None):
        if op_id is None:
            return super().compute_binary(op, bvdd2, op_id, bits)
        elif (op_id, bits, self, bvdd2) in BVDD_cached.compute_binary_cache:
            # assert (super().compute_binary(op, bvdd2, op_id, bits) ==
            #     BVDD_cached.compute_binary_cache[(op_id, bits, self, bvdd2)])
            BVDD_cached.compute_binary_hits += 1
        elif binary:
            # lock is acquired
            BVDD_cached.compute_binary_cache[(op_id, bits, self, bvdd2)] = binary
        else:
            # concurrent without acquiring lock
            binary = super().compute_binary(op, bvdd2, op_id, bits)
            with BVDD_cached.compute_binary_lock:
                return self.compute_binary(op, bvdd2, op_id, bits, binary)
        return BVDD_cached.compute_binary_cache[(op_id, bits, self, bvdd2)]

    compute_ternary_lock = threading.Lock()
    compute_ternary_cache = {}
    compute_ternary_hits = 0

    def compute_ternary(self, op, bvdd2, bvdd3, op_id = None, bits = None, ternary = None):
        if op_id is None:
            return super().compute_ternary(op, bvdd2, bvdd3, op_id, bits)
        elif (op_id, bits, self, bvdd2, bvdd3) in BVDD_cached.compute_ternary_cache:
            # assert (super().compute_ternary(op, bvdd2, bvdd3, op_id, bits) ==
            #     BVDD_cached.compute_ternary_cache[(op_id, bits, self, bvdd2, bvdd3)])
            BVDD_cached.compute_ternary_hits += 1
        elif ternary:
            # lock is acquired
            BVDD_cached.compute_ternary_cache[(op_id, bits, self, bvdd2, bvdd3)] = ternary
        else:
            # concurrent without acquiring lock
            ternary = super().compute_ternary(op, bvdd2, bvdd3, op_id, bits)
            with BVDD_cached.compute_ternary_lock:
                return self.compute_ternary(op, bvdd2, bvdd3, op_id, bits, ternary)
        return BVDD_cached.compute_ternary_cache[(op_id, bits, self, bvdd2, bvdd3)]

    def print_profile():
        print("BVDD cache profile:")
        print(f"nodes:                {utilization(BVDD_Node.node_cache_hits, len(BVDD_Node.node_cache))}")
        print(f"binary intersection:  {utilization(BVDD_cached.intersect_binary_hits, len(BVDD_cached.intersect_binary_cache))}")
        print(f"ternary intersection: {utilization(BVDD_cached.intersect_ternary_hits, len(BVDD_cached.intersect_ternary_cache))}")
        print(f"constants:            {utilization(BVDD_cached.constant_hits, len(BVDD_cached.constant_cache) + len(BVDD_cached.boolean_cache))}")
        print(f"projection:           {utilization(BVDD_cached.projection_hits, len(BVDD_cached.projection_cache))}")
        print(f"unary operators:      {utilization(BVDD_cached.compute_unary_hits, len(BVDD_cached.compute_unary_cache))}")
        print(f"binary operators:     {utilization(BVDD_cached.compute_binary_hits, len(BVDD_cached.compute_binary_cache))}")
        print(f"ternary operators:    {utilization(BVDD_cached.compute_ternary_hits, len(BVDD_cached.compute_ternary_cache))}")

class BVDD(BVDD_cached):
    pass