| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- """
- 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
|