def _create_state_labels(locs, ctx: AutomatonContext): # TODO support multiple locations assert len(locs) == 1, "Support only a single location" this_loc = locs[0] if "transient-values" not in this_loc: # TODO put an empty circuit. return BV.source(wordlen=1, value=1, name="dummy", signed=False) \ >> BV.sink(1, inputs=['dummy']) label_assignments = [] labels_assigned = set() for assignment in this_loc["transient-values"]: var_name = assignment["ref"] val = assignment["value"] labels_assigned.add(var_name) circ = _translate_expression(val, ctx.scope) \ .resize(ctx.scope.get_aig_variable(var_name).size) \ .with_output(var_name) \ .aigbv label_assignments.append(circ) loc_circuit = par_compose(label_assignments) for v in ctx.scope.transient_variables: if not v.is_local: loc_circuit |= BV.source( wordlen=1, value=int(v.name in labels_assigned), name=f'{v.name}-mod', signed=False, ) return loc_circuit
def translate_jani(data: json): global_scope = JaniScope() if "constants" in data: _translate_constants(data["constants"], global_scope) if "variables" in data: _translate_variables(data["variables"], global_scope) if len(data["automata"]) != 1: # TODO raise NotImplementedError("Only support monolithic jani.") aut, *_ = data["automata"] aut_encoding = _translate_automaton(aut, global_scope.make_local_scope_copy()) # TODO use mod variables to select which variables to update for var in global_scope.variables: aut_encoding = aut_encoding >> BV.sink(1, [var.name + "-mod"]) # TODO use transient mod-variables to check validity # TODO use transient mod-variables being set to false to indicate # use of default values if len(global_scope.variables) == 0: return aut_encoding wires, relabels = [], {} for var in global_scope.variables: name = f'global-{var.name}' wires.append({ 'input': var.name, 'output': var.name, 'init': var.initial, 'latch': name, 'keep_output': True, }) relabels[var.name] = name return aut_encoding.loopback(*wires)['o', relabels]
def unroll(self, horizon, *, init=True, omit_latches=True, only_last_outputs=False): hist_valid = LTL.atom(self.valid_id) \ .historically() \ .with_output(self.valid_id) monitor = BV.aig2aigbv(hist_valid.aig) circ = (self.circ >> monitor).unroll( horizon, init=init, omit_latches=omit_latches, only_last_outputs=only_last_outputs) if not only_last_outputs: times = range(1, horizon) circ >>= BV.sink(1, (f'{self.valid_id}##time_{t}' for t in times)) valid_id = f'{self.valid_id}##time_{horizon}' assert valid_id in circ.outputs input_encodings = timed_encodings(self.input_encodings, circ.inputs) output_encodings = timed_encodings(self.output_encodings, circ.outputs) return from_aigbv( circ=circ, input_encodings=input_encodings, output_encodings=output_encodings, valid_id=valid_id, )
def cone(circ: BV.AIGBV, output: str) -> BV.AIGBV: """Return cone of influence aigbv output.""" for out in circ.outputs - {output}: size = circ.omap[out].size circ >>= BV.sink(size, [out]) assert len(circ.outputs) == 1 assert fn.first(circ.outputs) == output return circ
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 test_preimage(): spec, mdp = scenario_reactive() sys1 = mdp.aigbv >> BV.sink(1, ['c_next', '##valid']) sys2 = sys1 >> BV.aig2aigbv(spec.aig) def act(action, coin): return {'a': (action, ), 'c': (coin, )} actions = [act(True, True), act(True, False), act(True, True)] observations = fn.lpluck(0, sys1.simulate(actions)) expr = preimage(observations, sys1) assert expr.inputs == { 'c##time_0', 'c##time_1', 'c##time_2', 'a##time_0', 'a##time_1', 'a##time_2', } bexpr1, manager, order = to_bdd2(sys2, horizon=3) def accepts(bexpr, actions): """Check if bexpr accepts action sequence.""" timed_actions = {} for t, action in enumerate(actions): c, a = action['c'], action['a'] timed_actions.update({ f'c##time_{t}[0]': c[0], f'a##time_{t}[0]': a[0] }) assert timed_actions.keys() == manager.vars.keys() tmp = manager.let(timed_actions, bexpr) assert tmp in (manager.true, manager.false) return tmp == manager.true assert not accepts(bexpr1, actions) bexpr2, _, input2var = aiger_bdd.to_bdd(expr, manager=manager, renamer=lambda _, x: x) assert accepts(bexpr2, actions) assert not accepts(~bexpr2, actions) bexpr3 = xor(bexpr1, bexpr2) assert accepts(bexpr3, actions)
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_reweighted(): spec, sys = scenario_reactive() # Hack too re-weight coinl sys2 = C.coin((1, 4), 'c') >> C.MDP(sys.aigbv >> BV.sink(1, ['##valid'])) cspec2 = concretize(spec, sys2, 3) graph, root, _ = spec2graph(cspec2) assert nx.is_directed_acyclic_graph(graph) assert len(graph.nodes) == 12 assert len(graph.edges) == 20 for node in graph.nodes: assert graph.out_degree[node] <= 2
def test_smoke(): spec = PLTL.atom('a').historically() spec = BV.aig2aigbv(spec.aig) spec = C.circ2mdp(spec) spec <<= C.coin((1, 8), name='c') spec >>= C.circ2mdp(BV.sink(1, ['c'])) # HACK bdd, manager, order = to_bdd2(spec, horizon=3) assert bdd.dag_size == 4 for i in range(order.total_bits*order.horizon): t = order.time_step(i) var = manager.var_at_level(i) action, t2, _ = TIMED_INPUT_MATCHER.match(var).groups() assert t == int(t2) decision = action in spec.inputs assert decision == order.is_decision(i)
def concretize( monitor, sys: C.MDP, horizon: int, manager=None ) -> ConcreteSpec: """ Convert an abstract specification monitor and a i/o transition system into a concrete specification over the horizion. """ # Make format correct. if not isinstance(monitor, C.MDP): assert hasattr(monitor, 'aig') or hasattr(monitor, 'aigbv') if hasattr(monitor, 'aigbv'): monitor = monitor.aigbv else: monitor = BV.aig2aigbv(monitor.aig) monitor = C.MDP(monitor) # Remove ignored outputs of sys. for sym in (monitor.inputs ^ sys.outputs): size = sys._aigbv.omap[sym].size monitor >>= C.MDP(BV.sink(size, [sym])) bexpr, manager, order = to_bdd2(sys >> monitor, horizon) return ConcreteSpec(bexpr, order, sys_inputs=sys.inputs, mdp=sys)
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 sys2(): mdp = sys1() mdp <<= C.coin((1, 8), name='c') mdp >>= C.circ2mdp(BV.sink(1, ['c'])) # HACK return mdp