test_adapters.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. """
  2. Unit tests for seq_interp.src.gui.adapters - pure data-conversion helpers,
  3. no Qt or hardware dependencies required.
  4. """
  5. import math
  6. import pytest
  7. import numpy as np
  8. from seq_interp.src.gui.adapters import (
  9. gate_to_step,
  10. build_block_rows,
  11. find_block_at_time,
  12. validate_timing,
  13. seq_metadata,
  14. block_cumtimes,
  15. )
  16. # --- Fixtures / helpers --------------------------------------------------------
  17. class _HW:
  18. """Minimal stand-in for HardwareConstraints."""
  19. rf_raster_time = 1e-6
  20. grad_raster_time = 10e-6
  21. adc_raster_time = 100e-9
  22. block_duration_raster = 10e-6
  23. RF_DELAY = 800e-9
  24. TR_DELAY = 800e-9
  25. START_DELAY = 1600e-9
  26. MIN_BLOCK_DURATION = 20e-9
  27. gamma = 42.576e6
  28. GRAD_MAX = 9e-3 * 42.576e6
  29. def _make_seq_data(blocks: list, rf=None, t_rf=None,
  30. gx=None, t_gx=None, gy=None, t_gy=None, gz=None, t_gz=None):
  31. return {
  32. "blocks": blocks,
  33. "params": {"scale_rf": 1.0},
  34. "rf": rf or np.array([]),
  35. "t_rf": t_rf or np.array([]),
  36. "gx": gx or np.array([]),
  37. "t_gx": t_gx or np.array([]),
  38. "gy": gy or np.array([]),
  39. "t_gy": t_gy or np.array([]),
  40. "gz": gz or np.array([]),
  41. "t_gz": t_gz or np.array([]),
  42. }
  43. def _make_sync_data(gate_rf, gate_adc, gate_tr, durs, n_blocks=None):
  44. return {
  45. "gate_rf": list(gate_rf),
  46. "gate_adc": list(gate_adc),
  47. "gate_tr_switch": list(gate_tr),
  48. "blocks_duration": list(durs),
  49. "number_of_blocks": n_blocks if n_blocks is not None else len(durs) - 1,
  50. "synchro_block_timer": 20e-9,
  51. "min_block_time": 800e-9,
  52. }
  53. # --- gate_to_step --------------------------------------------------------------
  54. class TestGateToStep:
  55. def test_output_length(self):
  56. gate = [0, 1, 0]
  57. durs = [1e-3, 2e-3, 1e-3]
  58. t, v = gate_to_step(gate, durs)
  59. assert len(t) == 6
  60. assert len(v) == 6
  61. def test_values_match_gate(self):
  62. gate = [0, 1, 0]
  63. durs = [1e-3, 2e-3, 1e-3]
  64. t, v = gate_to_step(gate, durs)
  65. assert v[0] == 0
  66. assert v[2] == 1
  67. assert v[4] == 0
  68. def test_time_monotonic(self):
  69. gate = [1, 0, 1, 0]
  70. durs = [0.5e-3] * 4
  71. t, _ = gate_to_step(gate, durs)
  72. assert all(t[i] <= t[i + 1] for i in range(len(t) - 1))
  73. def test_empty_gate(self):
  74. t, v = gate_to_step([], [])
  75. assert len(t) == 0 and len(v) == 0
  76. def test_step_coverage(self):
  77. """Each gate value spans exactly its block duration."""
  78. gate = [1]
  79. durs = [2e-3]
  80. t, v = gate_to_step(gate, durs)
  81. assert math.isclose(t[1] - t[0], 2e-3)
  82. # --- block_cumtimes ------------------------------------------------------------
  83. class TestBlockCumtimes:
  84. def test_starts_at_zero(self):
  85. ct = block_cumtimes([1e-3, 2e-3, 3e-3])
  86. assert ct[0] == pytest.approx(0.0)
  87. def test_length(self):
  88. ct = block_cumtimes([1e-3, 2e-3])
  89. assert len(ct) == 3
  90. def test_values(self):
  91. ct = block_cumtimes([1e-3, 2e-3, 3e-3])
  92. assert ct[-1] == pytest.approx(6e-3)
  93. # --- build_block_rows ---------------------------------------------------------
  94. class TestBuildBlockRows:
  95. def _simple_sync(self, blocks):
  96. """
  97. Produce a minimal consistent sync_data that matches what Synchronizer would
  98. generate for the given block list. Keeps bookkeeping manual but deterministic.
  99. """
  100. start = 1.6e-6
  101. rf_delay = 0.8e-6
  102. tr_delay = 0.8e-6
  103. block_dur = 5e-3
  104. g_rf, g_adc, g_tr, durs = [0], [0], [1], [start]
  105. for blk in blocks:
  106. has_adc = blk.get("has_adc", False)
  107. has_rf = "RF" in blk.get("type", [])
  108. if has_adc:
  109. g_adc.append(0); g_rf.append(g_rf[-1])
  110. durs[-1] -= tr_delay; durs.append(tr_delay)
  111. g_tr.append(0)
  112. g_adc.append(1); g_tr.append(0)
  113. else:
  114. g_tr.append(1); g_adc.append(0)
  115. if has_rf and not has_adc:
  116. g_rf.append(1); g_adc.append(g_adc[-1])
  117. durs[-1] -= rf_delay; durs.append(rf_delay)
  118. g_tr.append(g_tr[-1])
  119. g_rf.append(1)
  120. else:
  121. g_rf.append(0)
  122. durs.append(block_dur)
  123. return _make_sync_data(g_rf, g_adc, g_tr, durs)
  124. def test_start_delay_is_first_row(self):
  125. blocks = [{"type": [], "duration": 5e-3, "has_adc": False}]
  126. sync = self._simple_sync(blocks)
  127. seq = _make_seq_data(blocks)
  128. rows = build_block_rows(seq, sync)
  129. assert rows[0].is_delay
  130. assert rows[0].delay_type == "START"
  131. assert rows[0].sync_index == 0
  132. def test_plain_block_no_delay_inserted(self):
  133. blocks = [{"type": [], "duration": 5e-3, "has_adc": False}]
  134. sync = self._simple_sync(blocks)
  135. seq = _make_seq_data(blocks)
  136. rows = build_block_rows(seq, sync)
  137. # start_delay + 1 actual block = 2 rows
  138. assert len(rows) == 2
  139. assert not rows[1].is_delay
  140. def test_rf_block_inserts_rf_delay(self):
  141. blocks = [{"type": ["RF"], "duration": 5e-3, "has_adc": False}]
  142. sync = self._simple_sync(blocks)
  143. seq = _make_seq_data(blocks)
  144. rows = build_block_rows(seq, sync)
  145. # start_delay + RF_DELAY + RF_block = 3 rows
  146. assert len(rows) == 3
  147. assert rows[1].delay_type == "RF"
  148. assert rows[2].has_rf
  149. def test_adc_block_inserts_tr_delay(self):
  150. blocks = [{"type": ["ADC"], "duration": 5e-3, "has_adc": True}]
  151. sync = self._simple_sync(blocks)
  152. seq = _make_seq_data(blocks)
  153. rows = build_block_rows(seq, sync)
  154. # start_delay + TR_DELAY + ADC_block = 3 rows
  155. assert len(rows) == 3
  156. assert rows[1].delay_type == "TR"
  157. assert rows[2].has_adc
  158. def test_t_start_t_end_coverage(self):
  159. blocks = [
  160. {"type": [], "duration": 5e-3, "has_adc": False},
  161. {"type": [], "duration": 5e-3, "has_adc": False},
  162. ]
  163. sync = self._simple_sync(blocks)
  164. seq = _make_seq_data(blocks)
  165. rows = build_block_rows(seq, sync)
  166. for r in rows:
  167. assert r.t_end > r.t_start
  168. def test_empty_blocks(self):
  169. sync = _make_sync_data([0], [0], [1], [1.6e-6])
  170. seq = _make_seq_data([])
  171. rows = build_block_rows(seq, sync)
  172. assert len(rows) == 1
  173. assert rows[0].delay_type == "START"
  174. # --- find_block_at_time -------------------------------------------------------
  175. class TestFindBlockAtTime:
  176. def _rows(self):
  177. blocks = [{"type": [], "duration": 5e-3, "has_adc": False}] * 3
  178. sync = {
  179. "gate_rf": [0, 0, 0, 0], "gate_adc": [0, 0, 0, 0],
  180. "gate_tr_switch": [1, 1, 1, 1],
  181. "blocks_duration": [1e-3, 5e-3, 5e-3, 5e-3],
  182. "number_of_blocks": 3, "synchro_block_timer": 20e-9,
  183. "min_block_time": 800e-9,
  184. }
  185. return build_block_rows(_make_seq_data(blocks), sync)
  186. def test_finds_correct_row(self):
  187. rows = self._rows()
  188. # First block: [0, 1e-3), second: [1e-3, 6e-3) ...
  189. found = find_block_at_time(0.5e-3, rows)
  190. assert found is not None
  191. assert found.t_start == pytest.approx(0.0)
  192. def test_returns_none_past_end(self):
  193. rows = self._rows()
  194. found = find_block_at_time(1e3, rows)
  195. assert found is None
  196. def test_boundary_belongs_to_next(self):
  197. rows = self._rows()
  198. # Exactly at t=1e-3 should be the SECOND block (t_start=1e-3)
  199. found = find_block_at_time(1e-3, rows)
  200. assert found is not None
  201. assert found.t_start == pytest.approx(1e-3)
  202. # --- validate_timing ----------------------------------------------------------
  203. class TestValidateTiming:
  204. def test_no_warnings_for_valid_data(self):
  205. hw = _HW()
  206. seq = _make_seq_data([])
  207. sync = _make_sync_data([0], [0], [1], [20e-9])
  208. warns = validate_timing(hw, seq, sync)
  209. assert warns == []
  210. def test_rf_delay_below_min(self):
  211. hw = _HW()
  212. hw.RF_DELAY = 1e-9 # below MIN_BLOCK_DURATION = 20 ns
  213. seq = _make_seq_data([])
  214. sync = _make_sync_data([0], [0], [1], [20e-9])
  215. warns = validate_timing(hw, seq, sync)
  216. assert any("RF_DELAY" in w for w in warns)
  217. def test_cl_rounds_to_zero(self):
  218. hw = _HW()
  219. # synchro_block_timer = 20e-9; duration = 10e-9 -> CL = 0
  220. seq = _make_seq_data([])
  221. sync = _make_sync_data([0], [0], [1], [10e-9])
  222. sync["synchro_block_timer"] = 20e-9
  223. warns = validate_timing(hw, seq, sync)
  224. assert any("CL rounds to zero" in w for w in warns)
  225. def test_waveform_missing(self):
  226. hw = _HW()
  227. seq = {"blocks": [], "params": {}} # no rf/t_rf
  228. sync = _make_sync_data([0], [0], [1], [20e-9])
  229. warns = validate_timing(hw, seq, sync)
  230. assert any("'rf'" in w or "'t_rf'" in w for w in warns)
  231. def test_length_mismatch(self):
  232. hw = _HW()
  233. seq = _make_seq_data([], rf=np.array([1.0, 2.0]), t_rf=np.array([0.0]))
  234. sync = _make_sync_data([0], [0], [1], [20e-9])
  235. warns = validate_timing(hw, seq, sync)
  236. assert any("mismatch" in w.lower() for w in warns)
  237. # --- seq_metadata -------------------------------------------------------------
  238. class TestSeqMetadata:
  239. def test_counts(self):
  240. hw = _HW()
  241. blocks = [
  242. {"type": ["RF"], "duration": 5e-3, "has_adc": False},
  243. {"type": ["ADC"], "duration": 5e-3, "has_adc": True},
  244. {"type": ["GRAD"], "duration": 5e-3, "has_adc": False},
  245. {"type": [], "duration": 5e-3, "has_adc": False},
  246. ]
  247. meta = seq_metadata(_make_seq_data(blocks), hw)
  248. assert meta["Total blocks (orig)"] == 4
  249. assert meta["RF blocks"] == 1
  250. assert meta["ADC blocks"] == 1
  251. assert meta["Grad blocks"] == 1
  252. def test_raster_times_in_human_units(self):
  253. hw = _HW()
  254. meta = seq_metadata(_make_seq_data([]), hw)
  255. assert float(meta["RF raster (uss)"]) == pytest.approx(1.0)
  256. assert float(meta["Grad raster (uss)"]) == pytest.approx(10.0)
  257. assert float(meta["ADC raster (ns)"]) == pytest.approx(100.0)