CROSSWIRE / crosswire /baselines.py
Kai9987kai's picture
Release crosswire-formal-synthesis-2026-11-03 from GitHub 91e7ac87c4e4
450ac2f verified
Raw History Blame Contribute Delete
5.96 kB
"""Budgeted classical model learning with the same opaque target interface."""
from __future__ import annotations
from importlib.metadata import version
import random
from .core import Machine, Contract, CandidateSpace, compare_adapter
from .interfaces import InterfaceError
from .backends import make_space
class _BudgetExhausted(RuntimeError):
pass
def acquire_aalpy(public: dict, budget: int, target, cancel=None, *, candidate_backend='flat'):
from aalpy.base import SUL, Oracle
from aalpy.learning_algs import run_Lstar
from .study import Cancelled, fit_evidence
space = make_space(Machine.from_dict(public['reference']), Contract.from_dict(public['contract']), candidate_backend)
anchors = {int(k): int(v) for k, v in public.get('anchors', {}).items()}
probes, error = [], None
detail = {'library': 'aalpy', 'version': version('aalpy'), 'model_complete': False,
'oracle': 'budgeted local-RNG random words, no exact target equivalence',
'model_product_pairs': 0, 'equivalence_probe_steps': 0,
'comparison_source_steps': 0, 'comparison_hypothesis_steps': 0,
'budget_unit': 'target calls (resets + actions)',
'completion': 'budget_exhausted'}
class BoundarySUL(SUL):
def __init__(self):
super().__init__()
self.word, self.outputs = [], []
self.active = False
def check(self):
if cancel and cancel():
raise Cancelled('Classical baseline cancelled')
# Same shared system-call cap as other policies: resets and steps both count.
if target.counts['target_calls'] >= budget:
raise _BudgetExhausted()
def pre(self):
self.post()
self.check()
target.reset()
self.word, self.outputs, self.active = [], [], True
def post(self):
if self.active:
probes.append({'word': list(self.word), 'outputs': list(self.outputs),
'complete': len(self.word) == len(self.outputs),
'survivors_before': None, 'survivors_after': None})
self.active = False
def step(self, letter):
if letter is None:
return None
self.check()
self.word.append(letter)
output = target.step(letter)
self.outputs.append(output)
return output
sul = BoundarySUL()
class BudgetOracle(Oracle):
def __init__(self):
super().__init__(list(range(space.machine.inputs)), sul)
self.rng = random.Random(432871)
def find_cex(self, hypothesis):
for _ in range(16):
remaining = budget - target.counts['target_calls'] - 1 # reserve this query's reset
if remaining <= 0:
break
word = tuple(self.rng.choice(self.alphabet) for _ in range(self.rng.randint(1, min(3, remaining))))
hypothesis.reset_to_initial()
sul.pre()
mismatched = False
for symbol in word:
actual = sul.step(symbol)
expected = hypothesis.step(symbol)
detail['equivalence_probe_steps'] += 1
if actual != expected:
mismatched = True
break
sul.post()
if mismatched:
return list(sul.word)
return None
learned = None
try:
learned = run_Lstar(list(range(space.machine.inputs)), sul, BudgetOracle(), 'mealy',
cache_and_non_det_check=False, print_level=0)
detail['model_complete'] = True
detail['completion'] = 'empirical_complete_approximate_oracle'
except _BudgetExhausted:
pass
except InterfaceError as exc:
error = {'code': exc.code, 'message': str(exc)}
detail['completion'] = 'interface_error'
finally:
sul.post()
evidence = fit_evidence(probes)
observed_survivors = space.survivors(evidence, anchors)
survivors = ()
if learned is not None:
states = list(learned.states)
indices = {id(state): i for i, state in enumerate(states)}
transitions = tuple(tuple((indices[id(state.transitions[symbol])], state.output_fun[symbol])
for symbol in range(space.machine.inputs)) for state in states)
raw_model = Machine('aalpy empirical hypothesis', transitions, space.machine.inputs,
space.machine.outputs, indices[id(learned.initial_state)])
matches = []
for adapter_id in observed_survivors:
result = compare_adapter(space.machine, space.adapters[adapter_id], raw_model)
detail['model_product_pairs'] += result['visited_pairs']
# Product comparison runs outside CandidateSpace; charge it separately.
detail['comparison_source_steps'] += result['source_steps']
detail['comparison_hypothesis_steps'] += result['target_model_steps']
if result['equivalent']:
matches.append(adapter_id)
survivors = tuple(matches)
detail['model_states'] = len(states)
# Preserve raw transcript and cumulative costs even if learning is incomplete.
prefix = []
for entry in probes:
before = len(space.survivors(fit_evidence(prefix), anchors))
prefix.append(entry)
entry['survivors_before'] = before
entry['survivors_after'] = len(space.survivors(fit_evidence(prefix), anchors))
detail['observed_survivors'] = len(observed_survivors)
return space, survivors, probes, error, detail