def bmc_equiv(circ1, circ2, horizon, assume=None) -> Iterator[bool]: """ Perform bounded model checking up to horizon to see if circ1 and circ2 are equivilent. """ # Create distinguishing predicate. expr = BV.uatom(1, val=0) for o1 in circ1.outputs: o2 = f'{o1}##copy' size = circ1.omap[o1].size expr |= BV.uatom(size, o1) != BV.uatom(size, o2) expr.with_output('distinguished') monitor = ((circ1 | circ2) >> expr.aigbv).aig assert len(monitor.outputs) == 1 # Make underlying aig lazy. monitor = monitor.lazy_aig # BMC loop for t in range(horizon): delta = horizon - t unrolled = monitor.unroll(delta, only_last_outputs=True) assert len(unrolled.outputs) == 1 unrolled = aiger.BoolExpr(unrolled) if assume is not None: unrolled |= assume yield aiger_sat.is_sat(unrolled)
def test_loopback_unroll(): x = BV.uatom(3, 'x') y = BV.uatom(3, 'y') adder = (x + y).with_output('z') pcirc = C.PCirc(adder, dist_map={'y': lambda _: 1/3}) \ .assume((y > 0) & (y < 4)) pcirc2 = pcirc.loopback({ 'input': 'x', 'output': 'z', 'init': 4, 'keep_output': True, }) assert pcirc2.inputs == set() assert pcirc2.outputs == {'z'} assert len(pcirc2.latches) == 1 assert 4 < pcirc2({})[0]['z'] < 8 pcirc3 = pcirc2.unroll(3) assert pcirc3.inputs == set() assert pcirc3.outputs == {'z##time_1', 'z##time_2', 'z##time_3'} pcirc4 = pcirc2.unroll(3, only_last_outputs=True) assert pcirc4.outputs == {'z##time_3'} pcirc4({}) # Could technically be any value due to roll back.
def test_never_false_redemption(): spec = LTL.atom('x').historically() monitor = BV.aig2aigbv(spec.aig) assert len(monitor.latches) == 1 monitor = monitor['l', {fn.first(monitor.latches): 'z'}] # Environment can save you. x, y = BV.uatom(1, 'x'), BV.uatom(1, 'y') xy = (x | y).with_output('x') # env y can override x. dyn = C.pcirc(xy.aigbv) \ .randomize({'y': {0: 0.75, 1: 0.25}}) horizon = 3 model = from_pcirc(dyn, monitor, steps=horizon) coeff = np.log(2) # Special coeff to make LSE visit powers of 2. actor = improviser(model, coeff) v8 = coeff v7 = 0 v6 = coeff / 4 v5 = np.logaddexp(v6, coeff) v4 = (np.log(8) + v5) / 4 v3 = np.logaddexp(v4, v5) v2 = (3 * np.log(4) + v3) / 4 v1 = np.logaddexp(v2, v3) expected = sorted([v8, v7, v6, v5, v4, v3, v2, v1]) expected = fn.lmap(pytest.approx, expected) vals = sorted(list(actor.node2val.values())) assert all(x == y for x, y in zip(vals, expected)) def lprob(elems): return actor.prob(elems, log=True) assert lprob([]) == 0 assert lprob([1]) == pytest.approx(v3 - v1) for prefix in [[1], [1, 0, 1], [1, 1, 1], [1, 1, 1, 1, 1]]: for bit in [0, 1]: expected = pytest.approx(-np.log(4) + bit * np.log(3)) assert lprob(prefix + [bit]) - lprob(prefix) == expected ctrl = actor.policy() example = [] for env in [None, 0, 0]: example.append(ctrl.send(env)) assert -float('inf') < lprob(example) ctrl = actor.policy(observe_states=True) example = [] for env in [None, ({'x': 1}, None), ({'x': 1}, None)]: example.append(ctrl.send(env)) assert -float('inf') < lprob(example)
def test_par_compose(): x = BV.uatom(3, 'x').with_output('x') y = BV.uatom(3, 'y').with_output('y') pcirc_x = C.PCirc(circ=x) pcirc_y = C.PCirc(circ=y, dist_map={'y': lambda _: 1 / 3}) pcirc_xy = pcirc_x | pcirc_y assert pcirc_xy.inputs == {'x'} assert pcirc_xy.outputs == {'x', 'y'} assert pcirc_xy.dist_map['y'](0) == 1 / 3
def selectors(): indices = range(len(edge_circuits)) for v in ctx.scope.variables: size = ctx.scope.get_aig_variable(v.name).size outputs = [BV.uatom(size, f"{v.name}-{idx}") for idx in indices] yield mux(outputs, key_name='edge').with_output(v.name).aigbv for v in ctx.scope.variables: if not v.is_local: mname = f"{v.name}-mod" size = 1 outputs = [BV.uatom(size, f"{mname}-{idx}") for idx in indices] yield mux(outputs, key_name='edge').with_output(mname).aigbv
def test_pcirc_smoke(): x = BV.uatom(3, 'x') y = BV.uatom(3, 'y') z = (x + y).with_output('z') pcirc = C.PCirc(circ=z, dist_map={'y': lambda _: 1/3}) \ .assume(y <= 2) rvar = C.RandomVarCirc(pcirc) # Warning. May be flaky. for i in range(3): assert 3 <= rvar({'x': 3}) <= 5
def preimage(expr): assert expr.inputs <= unrolled_d.outputs circ = expr.aigbv for name in unrolled_d.outputs - expr.inputs: circ >>= BV.sink(omap[name].size, [name]) valid = BV.uatom(1, unrolled_d.valid_id) sat = BV.uatom(1, expr.output) preimg = (unrolled_d >> circ).aigbv >> (sat & valid).aigbv assert preimg.inputs == unrolled_d.inputs return BV.UnsignedBVExpr(preimg)
def test_seq_compose(): x = BV.uatom(3, 'x').with_output('y') y = BV.uatom(3, 'y').with_output('y') pcirc = C.PCirc(circ=y) pcirc2 = C.PCirc(circ=x, dist_map={'x': lambda _: 1 / 3}) >> pcirc pcirc3 = pcirc << C.PCirc(circ=x, dist_map={'x': lambda _: 1 / 3}) assert pcirc2.outputs == pcirc3.outputs == {'y'} assert pcirc2.inputs == pcirc3.inputs == set() assert pcirc2.dist_map['x'](0) == pcirc3.dist_map['x'](0) == 1 / 3 assert 0 <= pcirc2({})[0]['y'] <= 7 pcirc4 = C.PCirc(circ=(x + 1).with_output('y')) >> pcirc assert pcirc4({'x': 0})[0] == {'y': 1}
def onehot_gadget(output: str): sat = BV.uatom(1, output) false, true = BV.uatom(2, 0b01), BV.uatom(2, 0b10) expr = BV.ite(sat, true, false) \ .with_output('sat') encoder = D.Encoding( encode=lambda x: 1 << int(x), decode=lambda x: bool((x >> 1) & 1), ) return D.from_aigbv( expr.aigbv, output_encodings={'sat': encoder}, )
def test_pcirc_relabel(): x = BV.uatom(3, 'x') pcirc = C.PCirc(circ=x, dist_map={'x': lambda _: 1 / 3}) pcirc2 = pcirc['i', {'x': 'y'}] assert pcirc2.inputs == set() assert pcirc2.circ.inputs == {'y'} assert pcirc2.dist_map['y'](0) == 1 / 3
def test_parallel_composition(): x = BV.uatom(3, 'x') circ1 = (x + 1).with_output('y').aigbv \ | (x < 5).with_output('##valid').aigbv func1 = from_aigbv( circ1, input_encodings={'x': INT_ENC}, output_encodings={'y': INT_ENC}, ) circ2 = x.with_output('z').aigbv \ | (x > 2).with_output('##valid').aigbv func2 = from_aigbv( circ2, input_encodings={'x': INT_ENC}, output_encodings={'z': INT_ENC}, ) func12 = func1 | func2 assert func12({'x': 3})[0] == {'y': 4, 'z': 3} with pytest.raises(ValueError): func12({'x': 0}) with pytest.raises(ValueError): func12({'x': 7})
def test_discrete_wrapper_smoke(): x = BV.uatom(3, 'x') circ = (x + 1).with_output('z') \ .aigbv func = from_aigbv(circ) assert func({'x': 6})[0] == {'z': 7} func2 = from_aigbv( circ, input_encodings={'x': NEG_ENC}, output_encodings={'z': NEG_ENC}, ) assert func2({'x': -2})[0] == {'z': -3} valid = (x <= 2).with_output('##valid') \ .aigbv func3 = from_aigbv( circ | valid, input_encodings={'x': NEG_ENC}, output_encodings={'z': NEG_ENC}, ) assert func3({'x': -2})[0] == {'z': -3} with pytest.raises(ValueError): func3({'x': -3}) assert func3.inputs == {'x'} assert func3.outputs == {'z'} assert func3.latches == set() assert func3.latch2init == {}
def test_readme(): # Will assume inputs are in 'A', 'B', 'C', 'D', or 'E'. ascii_encoder = Encoding( decode=lambda x: chr(x + ord('A')), # Make 'A' map to 0. encode=lambda x: ord(x) - ord('A'), ) # Create function which maps: A -> B, B -> C, C -> D, D -> E. x = BV.uatom(3, 'x') # Need 3 bits to capture 5 input types. update_expr = (x < 4).repeat(3) & (x + 1) # 0 if x < 4 else x + 1. circ = update_expr.with_output('y').aigbv # Need to assert that the inputs are less than 4. circ |= (x < 5).with_output('##valid').aigbv # Wrap using aiger_discrete. func = from_aigbv( circ, input_encodings={'x': ascii_encoder}, output_encodings={'y': ascii_encoder}, valid_id='##valid', ) assert func({'x': 'A'})[0] == {'y': 'B'} assert func({'x': 'B'})[0] == {'y': 'C'} assert func({'x': 'C'})[0] == {'y': 'D'} assert func({'x': 'D'})[0] == {'y': 'E'} assert func({'x': 'E'})[0] == {'y': 'A'}
def test_never_false_redemption(): spec = LTL.atom('x').historically() monitor = BV.aig2aigbv(spec.aig) # Environment can save you. x, y = BV.uatom(1, 'x'), BV.uatom(1, 'y') xy = (x | y).with_output('x') # env y can override x. dyn = C.pcirc(xy.aigbv) \ .randomize({'y': {0: 0.4, 1: 0.6}}) horizon = 3 model = from_pcirc(dyn, monitor, steps=horizon) graph = model.graph() assert len(graph.nodes) == 2 * horizon + 2 assert len(graph.edges) == 4 * horizon
def masked_outputs(outputs, key_name: str): size = min_bits(len(outputs)) key = BV.uatom(size, key_name) for idx, output in enumerate(outputs): mask = (key == idx).repeat(output.size) yield output & mask
def from_aigbv(circ: BV.AIGBV, input_encodings: Encodings = None, output_encodings: Encodings = None, valid_id="##valid") -> FiniteFunc: """Lift an bit-vector into a function over finite sets. Note: if `valid_id` is not present as an output of `circ`, then it will be added, and will always output True. Args: - input_encodings: Maps an input to an encoder. Default is identity. - output_encodings: Maps an output to an encoder. Default is identity. - valid_id: Denotes which output monitors if inputs are "valid". """ if input_encodings is None: input_encodings = {} if output_encodings is None: output_encodings = {} if valid_id not in circ.outputs: circ |= BV.uatom(1, 1).with_output(valid_id).aigbv input_encodings = project(input_encodings, circ.inputs) output_encodings = project(output_encodings, circ.outputs - {valid_id}) return FiniteFunc( circ=circ, input_encodings=input_encodings, output_encodings=output_encodings, valid_id=valid_id, )
def test_relabel(): x = BV.uatom(3, 'x') circ1 = (x + 1).with_output('y').aigbv \ | (x < 5).with_output('##valid').aigbv func1 = from_aigbv(circ1,) assert func1['i', {'x': 'z'}].inputs == {'z'} assert func1['o', {'y': 'z'}].outputs == {'z'} assert func1['i', {'x': 'z'}].valid_id == func1.valid_id
def onehot_output(expr): """Creates circuit that depends only on 1-hot active bit.""" bits = BV.uatom(expr.size, expr.output) def ite(test, idx): return BV.ite(expr[idx], bits[idx], test) # Create chained if then else testing 1-hot bit. return reduce(ite, range(1, expr.size), bits[0])
def to_var(bdl: Bundle, encoding: Optional[Encoding]) -> mdd.Variable: if encoding is None: encoding = Encoding() return mdd.Variable( encode=encoding.encode, decode=encoding.decode, valid=BV.uatom(bdl.size, bdl.name)[0] | 1, # const 1. )
def _translate_expression(data: dict, scope: JaniScope): """ Takes an expression in JANI json, returns a circuit. :param data: the expression AST :param scope: The scope with the variable definitions. :return: An expression in py-aiger-bv """ if isinstance(data, bool): return BV.uatom(1, 1 if data else 0) if isinstance(data, int): if data == 0: return BV.uatom(1, data) nr_bits = min_bits(data) return BV.uatom(nr_bits, data) if isinstance(data, str): return scope.get_aig_variable(data) if "op" not in data: raise ValueError(f"{data} is expected to have an operator") if "right" in data: try: op = BINARY_OPS[data["op"]] except KeyError: raise NotImplementedError(f"Operator {data['op']} not supported") left_subexpr = _translate_expression(data["left"], scope) right_subexpr = _translate_expression(data["right"], scope) # Match size if left_subexpr.size < right_subexpr.size: left_subexpr = left_subexpr.resize(right_subexpr.size) elif right_subexpr.size < left_subexpr.size: right_subexpr = right_subexpr.resize(left_subexpr.size) return op(left_subexpr, right_subexpr) else: try: op = UNARY_OPS[data["op"]] except KeyError: raise NotImplementedError(f"Operator {data['op']} not supported") subexpr = _translate_expression(data["exp"], scope) return op(subexpr)
def aigbv(self): assert "##valid" not in self.outputs circ = self._aigbv is_valid = uatom(1, 1) for dist in self.input2dist.values(): circ <<= dist.expr.aigbv is_valid &= dist.valid circ |= is_valid.with_output("##valid").aigbv return circ
def test_loopback_and_unroll(): x = BV.uatom(3, 'x') y = BV.uatom(3, 'y') circ1 = (x + y).with_output('y').aigbv \ | (x < 7).with_output('##valid').aigbv func1 = from_aigbv(circ1,) func2 = func1.loopback({ 'input': 'x', 'output': 'y', 'keep_output': True, 'init': 0, }) assert func2.simulate([{'y': 1}, {'y': 1}])[-1][0] == {'y': 2} with pytest.raises(ValueError): assert func2.simulate([{'y': 1}]*10) func3 = func2.unroll(2, only_last_outputs=True) assert func3({'y##time_0': 0, 'y##time_1': 1})[0] == {'y##time_2': 1} with pytest.raises(ValueError): assert func3({'y##time_0': 7, 'y##time_1': 0})
def gridworld(n, start=(None, None), compressed_inputs=False): # Gridworld is 2 synchronized chains. circ = chain(n, 'x', 'ax', start[1]) | chain(n, 'y', 'ay', start[0]) circ <<= split_gate('a', 2, 'ay', 2, 'ax') # Combine inputs. x = BV.uatom(circ.omap['x'].size, 'x') y = BV.uatom(circ.omap['y'].size, 'y') circ >>= y.concat(x).with_output('state').aigbv # Combine outputs. if compressed_inputs: uncompress = lookup(2, 4, COMPRESSION_MAPPING, 'a', 'a', in_signed=False, out_signed=False) circ <<= uncompress # Wrap using aiger discrete add encoding + valid inputs. actions_map = ACTIONS_C if compressed_inputs else ACTIONS action_encoding = aiger_discrete.Encoding( encode=actions_map.get, decode=actions_map.inv.get, ) state_encoding = aiger_discrete.Encoding( encode=lambda s: s.yx, decode=lambda yx: G.GridState(yx, n), ) func = aiger_discrete.from_aigbv( circ, input_encodings={'a': action_encoding}, output_encodings={'state': state_encoding}, ) if not compressed_inputs: action = BV.uatom(4, 'a') is_1hot = (action != 0) & ((action & (action - 1)) == 0) func = func.assume(is_1hot) return func
def test_minimdp(): x, y = BV.uatom(2, 'main-x'), BV.uatom(2, 'main-y') circ = translate_file("tests/minimdp.jani") assert circ.outputs == {'main-x', 'main-y'} # Fix edge and check probability of ending on x=3 given valid run. # TODO this currently only works with one. query = circ << BV.source(2, 0, 'edge', False) query >>= BV.sink(2, ['main-y']) query >>= (BV.uatom(2, 'main-x') == 3).aigbv assert infer.prob(query.unroll(1, only_last_outputs=True)) == approx(0) assert infer.prob(query.unroll(2, only_last_outputs=True)) == approx(1 / 4) assert infer.prob(query.unroll(3, only_last_outputs=True)) == approx(1 / 3) # Randomize edge and check probability of ending on x=y given valid run. query = circ.randomize({'edge': {0: 0.5, 1: 0.5}}) query >>= (x == y).aigbv assert infer.prob(query.unroll(1, only_last_outputs=True)) == approx(1 / 4) assert infer.prob(query.unroll(2, only_last_outputs=True)) == approx(1 / 4) assert infer.prob(query.unroll(3, only_last_outputs=True)) == approx(13 / 38)
def test_readme_mdd(): # Will assume inputs are in 'A', 'B', 'C', 'D', or 'E'. ascii_encoder = Encoding( decode=lambda x: chr(x + ord('A')), # Make 'A' map to 0. encode=lambda x: ord(x) - ord('A'), ) one_hot_ascii_encoder = Encoding( decode=lambda x: ascii_encoder.decode(ONE_HOT.inv[x]), encode=lambda x: ONE_HOT[ascii_encoder.encode(x)], ) # Create function which maps: A -> B, B -> C, C -> D, D -> E. x = BV.uatom(3, 'x') # Need 3 bits to capture 5 input types. update_expr = (x < 4).repeat(3) & (x + 1) # 0 if x < 4 else x + 1. circ = update_expr.with_output('y').aigbv circ |= (x < 5).with_output('##valid').aigbv one_hot_converter = BV.lookup(3, 5, ONE_HOT, 'y', 'y', in_signed=False, out_signed=False) circ >>= one_hot_converter func_circ = from_aigbv( circ, input_encodings={'x': ascii_encoder}, output_encodings={'y': one_hot_ascii_encoder}, valid_id='##valid', ) assert func_circ({'x': 'A'})[0] == {'y': 'B'} assert func_circ({'x': 'B'})[0] == {'y': 'C'} assert func_circ({'x': 'C'})[0] == {'y': 'D'} assert func_circ({'x': 'D'})[0] == {'y': 'E'} assert func_circ({'x': 'E'})[0] == {'y': 'A'} func_mdd = to_mdd(func_circ) assert func_mdd({'x': 'A'})[0] == 'B' assert func_mdd({'x': 'B'})[0] == 'C' assert func_mdd({'x': 'C'})[0] == 'D' assert func_mdd({'x': 'D'})[0] == 'E' assert func_mdd({'x': 'E'})[0] == 'A'
def _encode(self, prev_latch, action, state): (step, lmap), circ1 = self._cutlatches() curr_step = step << aiger.source(prev_latch) for a, v in action.items(): size = circ1.imap[a].size const = aiger_bv.source(size, aiger_bv.decode_int(v, signed=False), name=a, signed=False) curr_step <<= const.aig expr = uatom(1, "##valid") == 1 for k, v in fn.chain(state.items()): expr &= _constraint(k, v) curr_step >>= expr.aig query = curr_step >> aiger.sink(prev_latch.keys()) assert len(query.outputs) == 1 model = solve(query) assert model is not None # Fill in any model don't cares. model = fn.merge({i: False for i in circ1.aig.inputs}, model) # HACK. Put model back into bitvector. coins = circ1.imap.omit(self.inputs).unblast(model) if len(prev_latch) > 0: next_latch_circ = curr_step >> aiger.sink(expr.aig.outputs) next_latch = next_latch_circ(model)[0] assert next_latch.keys() == prev_latch.keys() prev_latch = next_latch return coins, prev_latch
def _transition_coin(self, start, action, end): # 1. Init latches to start. circ = self.aigbv.reinit(start) # 2. Omit observations. `end` specifies latches. for out in self.outputs: circ >>= BV.sink(circ.omap[out].size, [out]) assert circ.outputs == {'##valid'} # 3. Create circuit to check valid coin flips. assert circ.omap['##valid'].size == 1 is_valid = BV.UnsignedBVExpr(circ.unroll(1)) circ >>= BV.sink(1, {'##valid'}) # Assume circ has no outputs now. # 4. Expose latchouts via unrolling. circ = circ.unroll(1, omit_latches=False) end = {f'{k}##time_1': v for k, v in end.items()} action = {f'{k}##time_0': v for k, v in action.items()} assert set(end.keys()) == circ.outputs assert set(action.keys()) <= circ.inputs # 5. Create circuit to check if inputs lead to end. test_equals = uatom(1, 1) for k, v in end.items(): size = circ.omap[k].size test_equals &= uatom(size, k) == uatom(size, v) match_end = BV.UnsignedBVExpr(circ >> test_equals.aigbv) # 6. Create circuit to assert inputs match action. match_action = uatom(1, 1) for k, v in action.items(): size = circ.imap[k].size match_action &= uatom(size, k) == uatom(size, v) return aigc.Coin( expr=match_end & match_action, valid=is_valid & match_action, )
def _constraint(k, v): var = uatom(len(v), k) return var == aiger_bv.decode_int(v, signed=False)
def selectors(): for var in vars_written_to: size = ctx.scope.get_aig_variable(var).size outputs = [BV.uatom(size, f"{var}-{idx}") for idx in indices] yield mux(outputs, key_name='sel').with_output(var).aigbv
def test_rename_valid(): func = from_aigbv(BV.uatom(3, 'x').aigbv).rename_valid('foo') assert 'foo' in func.circ.outputs assert 'foo' == func.valid_id