""" Unit tests for seq_interp.src.gui.adapters — pure data-conversion helpers, no Qt or hardware dependencies required. """ import math import pytest import numpy as np from seq_interp.src.gui.adapters import ( gate_to_step, build_block_rows, find_block_at_time, validate_timing, seq_metadata, block_cumtimes, ) # ─── Fixtures / helpers ──────────────────────────────────────────────────────── class _HW: """Minimal stand-in for HardwareConstraints.""" rf_raster_time = 1e-6 grad_raster_time = 10e-6 adc_raster_time = 100e-9 block_duration_raster = 10e-6 RF_DELAY = 800e-9 TR_DELAY = 800e-9 START_DELAY = 1600e-9 MIN_BLOCK_DURATION = 20e-9 gamma = 42.576e6 GRAD_MAX = 9e-3 * 42.576e6 def _make_seq_data(blocks: list, rf=None, t_rf=None, gx=None, t_gx=None, gy=None, t_gy=None, gz=None, t_gz=None): return { "blocks": blocks, "params": {"scale_rf": 1.0}, "rf": rf or np.array([]), "t_rf": t_rf or np.array([]), "gx": gx or np.array([]), "t_gx": t_gx or np.array([]), "gy": gy or np.array([]), "t_gy": t_gy or np.array([]), "gz": gz or np.array([]), "t_gz": t_gz or np.array([]), } def _make_sync_data(gate_rf, gate_adc, gate_tr, durs, n_blocks=None): return { "gate_rf": list(gate_rf), "gate_adc": list(gate_adc), "gate_tr_switch": list(gate_tr), "blocks_duration": list(durs), "number_of_blocks": n_blocks if n_blocks is not None else len(durs) - 1, "synchro_block_timer": 20e-9, "min_block_time": 800e-9, } # ─── gate_to_step ────────────────────────────────────────────────────────────── class TestGateToStep: def test_output_length(self): gate = [0, 1, 0] durs = [1e-3, 2e-3, 1e-3] t, v = gate_to_step(gate, durs) assert len(t) == 6 assert len(v) == 6 def test_values_match_gate(self): gate = [0, 1, 0] durs = [1e-3, 2e-3, 1e-3] t, v = gate_to_step(gate, durs) assert v[0] == 0 assert v[2] == 1 assert v[4] == 0 def test_time_monotonic(self): gate = [1, 0, 1, 0] durs = [0.5e-3] * 4 t, _ = gate_to_step(gate, durs) assert all(t[i] <= t[i + 1] for i in range(len(t) - 1)) def test_empty_gate(self): t, v = gate_to_step([], []) assert len(t) == 0 and len(v) == 0 def test_step_coverage(self): """Each gate value spans exactly its block duration.""" gate = [1] durs = [2e-3] t, v = gate_to_step(gate, durs) assert math.isclose(t[1] - t[0], 2e-3) # ─── block_cumtimes ──────────────────────────────────────────────────────────── class TestBlockCumtimes: def test_starts_at_zero(self): ct = block_cumtimes([1e-3, 2e-3, 3e-3]) assert ct[0] == pytest.approx(0.0) def test_length(self): ct = block_cumtimes([1e-3, 2e-3]) assert len(ct) == 3 def test_values(self): ct = block_cumtimes([1e-3, 2e-3, 3e-3]) assert ct[-1] == pytest.approx(6e-3) # ─── build_block_rows ───────────────────────────────────────────────────────── class TestBuildBlockRows: def _simple_sync(self, blocks): """ Produce a minimal consistent sync_data that matches what Synchronizer would generate for the given block list. Keeps bookkeeping manual but deterministic. """ start = 1.6e-6 rf_delay = 0.8e-6 tr_delay = 0.8e-6 block_dur = 5e-3 g_rf, g_adc, g_tr, durs = [0], [0], [1], [start] for blk in blocks: has_adc = blk.get("has_adc", False) has_rf = "RF" in blk.get("type", []) if has_adc: g_adc.append(0); g_rf.append(g_rf[-1]) durs[-1] -= tr_delay; durs.append(tr_delay) g_tr.append(0) g_adc.append(1); g_tr.append(0) else: g_tr.append(1); g_adc.append(0) if has_rf and not has_adc: g_rf.append(1); g_adc.append(g_adc[-1]) durs[-1] -= rf_delay; durs.append(rf_delay) g_tr.append(g_tr[-1]) g_rf.append(1) else: g_rf.append(0) durs.append(block_dur) return _make_sync_data(g_rf, g_adc, g_tr, durs) def test_start_delay_is_first_row(self): blocks = [{"type": [], "duration": 5e-3, "has_adc": False}] sync = self._simple_sync(blocks) seq = _make_seq_data(blocks) rows = build_block_rows(seq, sync) assert rows[0].is_delay assert rows[0].delay_type == "START" assert rows[0].sync_index == 0 def test_plain_block_no_delay_inserted(self): blocks = [{"type": [], "duration": 5e-3, "has_adc": False}] sync = self._simple_sync(blocks) seq = _make_seq_data(blocks) rows = build_block_rows(seq, sync) # start_delay + 1 actual block = 2 rows assert len(rows) == 2 assert not rows[1].is_delay def test_rf_block_inserts_rf_delay(self): blocks = [{"type": ["RF"], "duration": 5e-3, "has_adc": False}] sync = self._simple_sync(blocks) seq = _make_seq_data(blocks) rows = build_block_rows(seq, sync) # start_delay + RF_DELAY + RF_block = 3 rows assert len(rows) == 3 assert rows[1].delay_type == "RF" assert rows[2].has_rf def test_adc_block_inserts_tr_delay(self): blocks = [{"type": ["ADC"], "duration": 5e-3, "has_adc": True}] sync = self._simple_sync(blocks) seq = _make_seq_data(blocks) rows = build_block_rows(seq, sync) # start_delay + TR_DELAY + ADC_block = 3 rows assert len(rows) == 3 assert rows[1].delay_type == "TR" assert rows[2].has_adc def test_t_start_t_end_coverage(self): blocks = [ {"type": [], "duration": 5e-3, "has_adc": False}, {"type": [], "duration": 5e-3, "has_adc": False}, ] sync = self._simple_sync(blocks) seq = _make_seq_data(blocks) rows = build_block_rows(seq, sync) for r in rows: assert r.t_end > r.t_start def test_empty_blocks(self): sync = _make_sync_data([0], [0], [1], [1.6e-6]) seq = _make_seq_data([]) rows = build_block_rows(seq, sync) assert len(rows) == 1 assert rows[0].delay_type == "START" # ─── find_block_at_time ─────────────────────────────────────────────────────── class TestFindBlockAtTime: def _rows(self): blocks = [{"type": [], "duration": 5e-3, "has_adc": False}] * 3 sync = { "gate_rf": [0, 0, 0, 0], "gate_adc": [0, 0, 0, 0], "gate_tr_switch": [1, 1, 1, 1], "blocks_duration": [1e-3, 5e-3, 5e-3, 5e-3], "number_of_blocks": 3, "synchro_block_timer": 20e-9, "min_block_time": 800e-9, } return build_block_rows(_make_seq_data(blocks), sync) def test_finds_correct_row(self): rows = self._rows() # First block: [0, 1e-3), second: [1e-3, 6e-3) … found = find_block_at_time(0.5e-3, rows) assert found is not None assert found.t_start == pytest.approx(0.0) def test_returns_none_past_end(self): rows = self._rows() found = find_block_at_time(1e3, rows) assert found is None def test_boundary_belongs_to_next(self): rows = self._rows() # Exactly at t=1e-3 should be the SECOND block (t_start=1e-3) found = find_block_at_time(1e-3, rows) assert found is not None assert found.t_start == pytest.approx(1e-3) # ─── validate_timing ────────────────────────────────────────────────────────── class TestValidateTiming: def test_no_warnings_for_valid_data(self): hw = _HW() seq = _make_seq_data([]) sync = _make_sync_data([0], [0], [1], [20e-9]) warns = validate_timing(hw, seq, sync) assert warns == [] def test_rf_delay_below_min(self): hw = _HW() hw.RF_DELAY = 1e-9 # below MIN_BLOCK_DURATION = 20 ns seq = _make_seq_data([]) sync = _make_sync_data([0], [0], [1], [20e-9]) warns = validate_timing(hw, seq, sync) assert any("RF_DELAY" in w for w in warns) def test_cl_rounds_to_zero(self): hw = _HW() # synchro_block_timer = 20e-9; duration = 10e-9 → CL = 0 seq = _make_seq_data([]) sync = _make_sync_data([0], [0], [1], [10e-9]) sync["synchro_block_timer"] = 20e-9 warns = validate_timing(hw, seq, sync) assert any("CL rounds to zero" in w for w in warns) def test_waveform_missing(self): hw = _HW() seq = {"blocks": [], "params": {}} # no rf/t_rf sync = _make_sync_data([0], [0], [1], [20e-9]) warns = validate_timing(hw, seq, sync) assert any("'rf'" in w or "'t_rf'" in w for w in warns) def test_length_mismatch(self): hw = _HW() seq = _make_seq_data([], rf=np.array([1.0, 2.0]), t_rf=np.array([0.0])) sync = _make_sync_data([0], [0], [1], [20e-9]) warns = validate_timing(hw, seq, sync) assert any("mismatch" in w.lower() for w in warns) # ─── seq_metadata ───────────────────────────────────────────────────────────── class TestSeqMetadata: def test_counts(self): hw = _HW() blocks = [ {"type": ["RF"], "duration": 5e-3, "has_adc": False}, {"type": ["ADC"], "duration": 5e-3, "has_adc": True}, {"type": ["GRAD"], "duration": 5e-3, "has_adc": False}, {"type": [], "duration": 5e-3, "has_adc": False}, ] meta = seq_metadata(_make_seq_data(blocks), hw) assert meta["Total blocks (orig)"] == 4 assert meta["RF blocks"] == 1 assert meta["ADC blocks"] == 1 assert meta["Grad blocks"] == 1 def test_raster_times_in_human_units(self): hw = _HW() meta = seq_metadata(_make_seq_data([]), hw) assert float(meta["RF raster (µs)"]) == pytest.approx(1.0) assert float(meta["Grad raster (µs)"]) == pytest.approx(10.0) assert float(meta["ADC raster (ns)"]) == pytest.approx(100.0)