CROSSWIRE / tests /test_temporal.py
Kai9987kai's picture
Release crosswire-formal-synthesis-2026-11-03 from GitHub 91e7ac87c4e4
450ac2f verified
Raw History Blame Contribute Delete
4.78 kB
import pytest
from crosswire.core import Adapter, CandidateSpace, Contract, Machine, compare_adapter
from crosswire.fixtures import Case, make_case
def _machine():
transitions = (((1, 0), (0, 3), (1, 2)), ((1, 1), (1, 1), (1, 2)))
return Machine("test", transitions, 3, 4, 0)
def test_temporal_candidate_space_size():
m = _machine()
c = Contract(goal=1, hazard=2, horizon=4)
static_space = CandidateSpace(m, c, temporal=False)
assert len(static_space.adapters) == 144
temporal_space = CandidateSpace(m, c, temporal=True)
# 144 static + (144 * 4 initial output options) = 720 adapters
assert len(temporal_space.adapters) == 720
def test_temporal_adapter_predicts_delayed_output():
m = _machine()
c = Contract(goal=1, hazard=2, horizon=4)
space = CandidateSpace(m, c, temporal=True)
# Pick a temporal adapter (id >= 144)
temporal_adapter = next(a for a in space.adapters if a.delay == 1)
word = [0, 0, 1]
preds = space.predict(temporal_adapter.id, word)
assert len(preds) == len(word)
# At t=0, output is the mapped init_output
assert preds[0] == temporal_adapter.output_map[temporal_adapter.init_output]
def test_temporal_adapter_eliminates_delay_fixture_mismatch():
case = make_case("delay", seed=17)
space = CandidateSpace(Machine.from_dict(case.public["reference"]), Contract.from_dict(case.public["contract"]), temporal=True)
# In delay fixture, an observation delay of 1 exists.
# Static adapters have non-zero loss or fail to explain delayed traces;
# with temporal adapters, there exists a delay=1 adapter that matches!
# Generate a trace from delay fixture
from crosswire.fixtures import make_engine
engine = make_engine(case.private)
probes = [{"word": [0, 1, 0], "outputs": [engine.step(0), engine.step(1), engine.step(0)], "complete": True}]
losses = space.losses(probes)
zero_loss = [aid for aid, loss in losses.items() if loss == 0]
assert len(zero_loss) > 0
# At least one zero loss adapter has delay == 1
assert any(space.adapters[aid].delay == 1 for aid in zero_loss)
def test_observational_classes_distinguish_delay_and_initial_buffer():
space = CandidateSpace(_machine(), Contract(), temporal=True)
ids = (0, 144, 147) # identity maps: static, delay with initial 0, initial 3
assert space.predict(0, (0, 0)) != space.predict(144, (0, 0))
assert space.predict(144, (0,)) != space.predict(147, (0,))
assert space.ambiguity(ids)['observational_classes'] == 3
def test_observational_classes_merge_equivalent_static_and_delayed_models():
machine = Machine('constant observation', (((0, 0), (0, 0), (0, 0)),))
space = CandidateSpace(machine, Contract(), temporal=True)
assert space.ambiguity((0, 144))['observational_classes'] == 1
assert space.ambiguity((0, 144, 145))['observational_classes'] == 2
def test_temporal_classes_match_independent_exact_product_comparisons():
machine = _machine()
space = CandidateSpace(machine, Contract(horizon=1), temporal=True)
selected = (0, 1, 24, 144, 145, 147, 148, 240, 241, 288)
representatives = []
for identifier in selected:
adapter = space.adapters[identifier]
if adapter.delay:
# Independently materialize all source-state/output-buffer states.
rows = tuple(tuple((following * machine.outputs + output, adapter.output_map[buffer])
for following, output in (machine.transitions[state][symbol]
for symbol in adapter.input_map))
for state in range(len(machine.transitions)) for buffer in range(machine.outputs))
initial = machine.initial * machine.outputs + adapter.init_output
else:
rows = tuple(tuple((row[symbol][0], adapter.output_map[row[symbol][1]])
for symbol in adapter.input_map) for row in machine.transitions)
initial = machine.initial
target = Machine('materialized adapter', rows, initial=initial)
if not any(compare_adapter(machine, space.adapters[other], target)['equivalent']
for other in representatives):
representatives.append(identifier)
assert space.ambiguity(selected)['observational_classes'] == len(representatives)
@pytest.mark.parametrize('delay', [0, 1])
@pytest.mark.parametrize('initial', [4, 100])
def test_adapter_rejects_initial_buffer_outside_output_alphabet(delay, initial):
with pytest.raises(ValueError, match='init_output'):
Adapter(0, (0, 1, 2), (0, 1, 2, 3), delay=delay, init_output=initial)