"""Writers for small synthetic IDAT, BPM and EGT files (plan ยง16.4). They produce structurally valid files for tests and synthetic data sets. Layouts mirror the readers in :mod:`simurg.formats`; they are not meant to reproduce vendor files byte-for-byte. """ from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass, field import numpy as np import numpy.typing as npt from simurg.formats import idat as idatfmt from simurg.formats.binutil import BinaryWriter @dataclass class SynthIdat: address_ids: npt.NDArray[np.int32] mean: npt.NDArray[np.uint16] sd: npt.NDArray[np.uint16] | None = None n_beads: npt.NDArray[np.uint8] | None = None barcode: str = "200000000001" position: str = "R01C01" chip_type: str = "BeadChip 24x1x4" sample_id: str = "" plate: str = "" well: str = "" manifest: str = "" run_info: list[tuple[str, str, str, str, str]] = field( default_factory=lambda: [ ("12/27/2023 5:52:21 PM", "Scan", "ScannerID=N0", "iScan Control Software", "4.2.1.729") ] ) mid_block_itemsize: int = 4 version: int = 3 def encode_idat(s: SynthIdat) -> bytes: n = len(s.address_ids) sd = s.sd if s.sd is not None else np.full(n, 100, np.uint16) nb = s.n_beads if s.n_beads is not None else np.full(n, 12, np.uint8) sections: list[tuple[int, bytes]] = [] def sec(code: int, *parts: tuple[str, object]) -> None: w = BinaryWriter() for kind, val in parts: if kind == "i32": w.i32(int(val)) # type: ignore[call-overload] elif kind == "str": w.string(str(val)) elif kind == "raw": w.write(val) # type: ignore[arg-type] else: w.array(val, kind) # type: ignore[arg-type] sections.append((code, w.getvalue())) mid_dtype = " bytes: w = BinaryWriter() w.write(b"BPM") w.u8(1) w.i32(bpm_version) w.string(manifest_name) w.string(control_config) n = len(loci) w.i32(n) w.array(np.arange(1, n + 1), "= 6: w.u8(0) w.write(int(lc.exp_clusters).to_bytes(1, "little", signed=True)) w.u8(lc.intensity_only) w.u8(lc.assay_type) if entry_version >= 7: for f in (0.25, 0.25, 0.25, 0.25): w.f32(f) if entry_version >= 8: w.string(lc.ref_strand) w.i32(len(trailer)) for line in trailer: w.string(line) return w.getvalue() def encode_egt( names: Sequence[str], *, theta_mean: npt.ArrayLike, r_mean: npt.ArrayLike, theta_dev: npt.ArrayLike | None = None, r_dev: npt.ArrayLike | None = None, n: npt.ArrayLike | None = None, gentrain: npt.ArrayLike | None = None, intensity_threshold: npt.ArrayLike | None = None, data_block_version: int = 9, manifest_name: str = "SYNTH-1v1_A1", corrupt_counts: bool = False, ) -> bytes: k = len(names) tm = np.asarray(theta_mean, np.float32).reshape(k, 3) rm = np.asarray(r_mean, np.float32).reshape(k, 3) td = np.asarray(theta_dev if theta_dev is not None else np.full((k, 3), 0.03), np.float32).reshape(k, 3) rd = np.asarray(r_dev if r_dev is not None else np.full((k, 3), 0.1), np.float32).reshape(k, 3) nn = np.asarray(n if n is not None else np.full((k, 3), 100), np.int32).reshape(k, 3) gt = np.asarray(gentrain if gentrain is not None else np.full(k, 0.8), np.float32) thr = np.asarray(intensity_threshold if intensity_threshold is not None else np.zeros(k), np.float32) w = BinaryWriter() w.i32(3) for s in ("< 6.1.3.0", "7.0.0", "7.0.0", "1.2.0", "1/1/2026 12:00:00 PM"): w.string(s) w.u8(1) w.string(manifest_name) w.i32(data_block_version) w.string("") w.i32(k) for i in range(k): w.array(nn[i], "= 7: w.f32(float(thr[i])) w.array(np.full(14, np.nan), "= 6: w.array(np.zeros(15), "