""" Tests for Synchronizer.process — the synchro-sequence builder. Covers the constraints added for the LF-MRI hardware: * minimum event duration = 20 clock ticks (real duration, TR preserved); * maximum block duration = 999999 ticks (long blocks split into equal parts); * ADC stays a single continuous HIGH level when its block is split; * the TR/RF/START delay enable flags (no 1/0-tick artifacts); * parallel gate arrays stay length-balanced. """ from __future__ import annotations import pytest from seq_interp.src.core.synchronizer import Synchronizer from seq_interp.src.hardware.constraints import HardwareConstraints TIMER = 20e-9 # MIN_BLOCK_DURATION — one clock tick MIN_TICKS = 20 MAX_TICKS = 999999 def _hw(**flags) -> HardwareConstraints: hw = HardwareConstraints() for k, v in flags.items(): setattr(hw, k, v) return hw def _ticks(sync_data) -> list[int]: t = sync_data["synchro_block_timer"] return [round(d / t) for d in sync_data["blocks_duration"]] def _array_lengths(sync_data) -> set[int]: return {len(sync_data[k]) for k in ("gate_adc", "gate_rf", "gate_tr_switch", "blocks_duration")} # --------------------------------------------------------------------------- # # Array bookkeeping # --------------------------------------------------------------------------- # def test_gate_arrays_balanced_and_seed_offset(make_seq): """All gate arrays share one length == number_of_blocks + 1 (leading seed).""" seq = make_seq([{"rf": True, "dur": 1e-3}, {"dur": 5e-3}, {"adc": True, "dur": 2e-3}]) d = Synchronizer(_hw()).process(seq) lengths = _array_lengths(d) assert len(lengths) == 1 assert lengths.pop() == d["number_of_blocks"] + 1 # --------------------------------------------------------------------------- # # Minimum event duration (20 ticks), TR preserved # --------------------------------------------------------------------------- # def test_min_event_is_20_ticks(make_seq): """No emitted block is shorter than 20 ticks, none is zero.""" seq = make_seq([{"rf": True, "dur": 1e-3}, {"adc": True, "dur": 2e-3}]) d = Synchronizer(_hw()).process(seq) assert min(_ticks(d)) >= MIN_TICKS def test_total_duration_preserved_by_min_clamp(make_seq): """Stretching short blocks is compensated from a neighbour — TR unchanged.""" seq = make_seq([{"rf": True, "dur": 1e-3}, {"dur": 5e-3}, {"adc": True, "dur": 2e-3}]) hw = _hw() total_in = sum(seq.block_durations.values()) + max(hw.START_DELAY, hw.RF_DELAY) d = Synchronizer(hw).process(seq) assert sum(d["blocks_duration"]) == pytest.approx(total_in) # --------------------------------------------------------------------------- # # Maximum block duration (999999 ticks) — splitting # --------------------------------------------------------------------------- # def test_long_block_split_under_max(make_seq): """A 50 ms ADC block (2.5M ticks) is split so every part <= 999999 ticks.""" seq = make_seq([{"adc": True, "dur": 50e-3}]) d = Synchronizer(_hw()).process(seq) assert max(_ticks(d)) <= MAX_TICKS def test_split_preserves_total_duration(make_seq): seq = make_seq([{"adc": True, "dur": 50e-3}]) hw = _hw() total_in = 50e-3 + max(hw.START_DELAY, hw.RF_DELAY) d = Synchronizer(hw).process(seq) assert sum(d["blocks_duration"]) == pytest.approx(total_in) def test_split_keeps_adc_high_continuous(make_seq): """Split parts of an ADC block all stay HIGH (level, not per-block pulse).""" seq = make_seq([{"adc": True, "dur": 50e-3}]) d = Synchronizer(_hw()).process(seq) high = [i for i, v in enumerate(d["gate_adc"]) if v == 1] # contiguous run, more than one part assert len(high) >= 2 assert high == list(range(high[0], high[0] + len(high))) # --------------------------------------------------------------------------- # # Delay enable flags — no inserted block when off # --------------------------------------------------------------------------- # def test_tr_delay_flag_removes_inserted_block(make_seq): seq = make_seq([{"adc": True, "dur": 2e-3}]) on = Synchronizer(_hw(TR_DELAY_ENABLED=True)).process(seq) off = Synchronizer(_hw(TR_DELAY_ENABLED=False)).process(seq) assert off["number_of_blocks"] == on["number_of_blocks"] - 1 assert min(_ticks(off)) >= MIN_TICKS def test_rf_delay_flag_removes_inserted_block(make_seq): seq = make_seq([{"rf": True, "dur": 1e-3}]) on = Synchronizer(_hw(RF_DELAY_ENABLED=True)).process(seq) off = Synchronizer(_hw(RF_DELAY_ENABLED=False)).process(seq) assert off["number_of_blocks"] == on["number_of_blocks"] - 1 assert min(_ticks(off)) >= MIN_TICKS def test_start_delay_flag_uses_minimal_nonzero_start(make_seq): """START off keeps the (non-emitted) seed block, shrunk to the 20-tick min.""" seq = make_seq([{"adc": True, "dur": 2e-3}]) off = Synchronizer(_hw(START_DELAY_ENABLED=False)).process(seq) assert off["blocks_duration"][0] == pytest.approx(20 * off["synchro_block_timer"]) assert min(_ticks(off)) >= MIN_TICKS def test_all_flags_off_no_artifacts(make_seq): seq = make_seq([{"rf": True, "dur": 1e-3}, {"dur": 5e-3}, {"adc": True, "dur": 2e-3}]) d = Synchronizer(_hw(TR_DELAY_ENABLED=False, RF_DELAY_ENABLED=False, START_DELAY_ENABLED=False)).process(seq) ticks = _ticks(d) assert min(ticks) >= MIN_TICKS assert max(ticks) <= MAX_TICKS assert len(_array_lengths(d)) == 1