| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118 |
- """
- Tests for XMLGenerator.generate — the sync_v2.xml writer.
- Covers the block-emission contract:
- * blocks are emitted for array indices 1..nb (seed at index 0 is represented
- by the header tag), so the last real block is never dropped;
- * ADC is reported as a single event per continuous HIGH run, and its window
- duration equals the full (un-truncated) intended duration;
- * the gradient system goes HIGH on the very last event;
- * every emitted CL value is within [20, 999999] ticks.
- """
- from __future__ import annotations
- import xml.etree.ElementTree as ET
- from seq_interp.src.core.synchronizer import Synchronizer
- from seq_interp.src.hardware.constraints import HardwareConstraints
- from seq_interp.src.interfaces.xml_generator import XMLGenerator
- 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 _generate(make_seq, specs, tmp_path, **flags):
- """Run synchronizer + generator, return (parsed_channels, adc_vals, adc_starts)."""
- hw = _hw(**flags)
- sync_data = Synchronizer(hw).process(make_seq(specs))
- path = tmp_path / "sync_v2.xml"
- adc_vals, adc_starts = XMLGenerator().generate(sync_data, str(path), hw)
- root = ET.parse(str(path)).getroot()
- channels = {}
- for ch in ("RF", "SW", "ADC", "GR", "CL"):
- node = root.find(ch)
- channels[ch] = [int(child.text) for child in node]
- channels["ParamCount"] = int(root.find("ParamCount").text)
- return channels, adc_vals, adc_starts
- # --------------------------------------------------------------------------- #
- # Emission contract / off-by-one regression
- # --------------------------------------------------------------------------- #
- def test_param_count_and_channel_lengths(make_seq, tmp_path):
- ch, _, _ = _generate(
- make_seq,
- [{"rf": True, "dur": 1e-3}, {"adc": True, "dur": 2e-3}],
- tmp_path,
- )
- n = ch["ParamCount"]
- for name in ("RF", "SW", "ADC", "GR", "CL"):
- assert len(ch[name]) == n, name
- def test_trailing_adc_block_is_emitted(make_seq, tmp_path):
- """Regression: a sequence whose LAST block is ADC must still emit ADC=1."""
- ch, adc_vals, _ = _generate(make_seq, [{"adc": True, "dur": 2e-3}], tmp_path)
- assert sum(ch["ADC"]) >= 1 # the ADC HIGH is not dropped
- assert len(adc_vals) == 1 # exactly one ADC event
- # --------------------------------------------------------------------------- #
- # ADC single event + exact window
- # --------------------------------------------------------------------------- #
- def test_split_adc_single_event_full_window(make_seq, tmp_path):
- """A 50 ms ADC block split into parts is ONE event spanning the full 50 ms."""
- ch, adc_vals, _ = _generate(make_seq, [{"adc": True, "dur": 50e-3}], tmp_path)
- assert sum(ch["ADC"]) >= 2 # multiple HIGH columns (was split)
- assert len(adc_vals) == 1 # but a single ADC event / trigger
- assert adc_vals[0] == 50e-3 # full window, not a truncated part
- def test_adc_window_not_truncated_by_min_clamp(make_seq, tmp_path):
- """Min-clamp compensation must not steal duration from the ADC block."""
- _, adc_vals, _ = _generate(
- make_seq,
- [{"rf": True, "dur": 1e-3}, {"adc": True, "dur": 2e-3}],
- tmp_path,
- TR_DELAY_ENABLED=True,
- )
- assert len(adc_vals) == 1
- assert adc_vals[0] == 2e-3
- # --------------------------------------------------------------------------- #
- # Gradient system: last event HIGH
- # --------------------------------------------------------------------------- #
- def test_gradient_high_on_last_event(make_seq, tmp_path):
- ch, _, _ = _generate(
- make_seq,
- [{"rf": True, "dur": 1e-3}, {"dur": 5e-3}, {"adc": True, "dur": 2e-3}],
- tmp_path,
- )
- assert ch["GR"][-1] == 1 # last event HIGH
- assert all(v == 0 for v in ch["GR"][1:-1]) # all middle blocks LOW
- # --------------------------------------------------------------------------- #
- # CL bounds
- # --------------------------------------------------------------------------- #
- def test_cl_values_within_bounds(make_seq, tmp_path):
- ch, _, _ = _generate(
- make_seq,
- [{"rf": True, "dur": 1e-3}, {"adc": True, "dur": 50e-3}],
- tmp_path,
- )
- assert min(ch["CL"]) >= MIN_TICKS
- assert max(ch["CL"]) <= MAX_TICKS
|