"""IDAT reader: synthetic files + malformed-input matrix (spec IN-023, EDGE-004, EDGE-033).""" from __future__ import annotations import gzip import struct import numpy as np import pytest from simurg.core.errors import FormatError from simurg.formats.idat import check_pair, parse_idat_filename, parse_run_time, read_idat from simurg.synth.files import SynthIdat, encode_idat def _synth(n: int = 50, **kw) -> SynthIdat: # type: ignore[no-untyped-def] rng = np.random.default_rng(1) ids = np.sort(rng.choice(10_000_000, n, replace=False)).astype(np.int32) return SynthIdat( address_ids=ids, mean=rng.integers(0, 60000, n).astype(np.uint16), n_beads=rng.integers(0, 30, n).astype(np.uint8), **kw, ) def test_roundtrip(tmp_path) -> None: # type: ignore[no-untyped-def] s = _synth(sample_id="NA12878", plate="P1", well="A01") p = tmp_path / "200000000001_R01C01_Grn.idat" p.write_bytes(encode_idat(s)) i = read_idat(p) assert i.n_addresses == 50 assert np.array_equal(i.address_ids, s.address_ids) assert np.array_equal(i.mean, s.mean) assert np.array_equal(i.n_beads, s.n_beads) assert (i.barcode, i.position, i.sample_id, i.plate, i.well) == ("200000000001", "R01C01", "NA12878", "P1", "A01") assert i.scan_time_iso == "2023-12-27T17:52:21" assert i.scanner_software == "4.2.1.729" assert i.mid_block_itemsize == 4 def test_midblock_uint8_inferred(tmp_path) -> None: # type: ignore[no-untyped-def] p = tmp_path / "x.idat" p.write_bytes(encode_idat(_synth(n=40, mid_block_itemsize=1))) assert read_idat(p).mid_block_itemsize == 1 def test_gzip(tmp_path) -> None: # type: ignore[no-untyped-def] p = tmp_path / "200000000001_R01C01_Red.idat.gz" p.write_bytes(gzip.compress(encode_idat(_synth()))) assert read_idat(p).n_addresses == 50 def test_metadata_only(tmp_path) -> None: # type: ignore[no-untyped-def] p = tmp_path / "x.idat" p.write_bytes(encode_idat(_synth())) i = read_idat(p, load_arrays=False) assert i.n_addresses == 50 and len(i.mean) == 0 and i.barcode == "200000000001" @pytest.mark.parametrize( "mutate,code", [ (lambda b: b"XDAT" + b[4:], "E_IDAT_FORMAT"), # wrong magic (lambda b: b[:4] + struct.pack(" None: # type: ignore[no-untyped-def] p = tmp_path / "bad.idat" p.write_bytes(mutate(encode_idat(_synth()))) with pytest.raises(FormatError) as e: read_idat(p) assert e.value.code == code def test_pair_mismatch(tmp_path) -> None: # type: ignore[no-untyped-def] a, b = tmp_path / "a.idat", tmp_path / "b.idat" a.write_bytes(encode_idat(_synth(n=50))) b.write_bytes(encode_idat(_synth(n=49))) with pytest.raises(FormatError) as e: check_pair(read_idat(a), read_idat(b)) assert e.value.code == "E_IDAT_PAIR_MISMATCH" @pytest.mark.parametrize( "name,expected", [ ("207895980019_R03C01_Grn.idat", ("207895980019", "R03C01", "Grn")), ("207895980019_r03c01_red.idat.gz", ("207895980019", "R03C01", "Red")), ("5723646052_R02C02_GRN.IDAT", ("5723646052", "R02C02", "Grn")), ("4019585376_B_Red.idat", None), ("notes.txt", None), ], ) def test_filename(name, expected) -> None: # type: ignore[no-untyped-def] assert parse_idat_filename(name) == expected @pytest.mark.parametrize( "raw,iso", [ ("12/27/2023 5:53:34 PM", "2023-12-27T17:53:34"), ("1/2/2020 12:00:01 AM", "2020-01-02T00:00:01"), ("2020-01-02 13:14:15", "2020-01-02T13:14:15"), ("", ""), ("garbage", ""), ], ) def test_run_time(raw, iso) -> None: # type: ignore[no-untyped-def] assert parse_run_time(raw) == iso def test_duplicate_addresses_rejected(tmp_path) -> None: # type: ignore[no-untyped-def] from simurg.formats.idat import has_duplicates assert not has_duplicates(np.array([1, 5, 9], np.int32)) assert not has_duplicates(np.array([9, 1, 5], np.int32)) assert has_duplicates(np.array([9, 1, 9], np.int32)) s = _synth(n=5) s.address_ids = np.array([5, 1, 5, 7, 8], np.int32) p = tmp_path / "dup.idat" p.write_bytes(encode_idat(s)) with pytest.raises(FormatError): read_idat(p)