"""Resume planning: turn a file's frontier into a set of angles to re-acquire. Pure logic, no Qt and no file I/O beyond what SrasFile already parsed, so the non-obvious contiguity rule is testable on its own. """ from __future__ import annotations from dataclasses import dataclass, field from core.scan_engine import ResumeState, ResumeTarget from core.sras_format import WRITABLE_VERSIONS, SrasFile @dataclass class ResumePlan: targets: list[ResumeTarget] auto_added: list[int] = field(default_factory=list) # indices forced in frontier_idx: int = 0 @property def total_rows(self) -> int: return sum(t.n_rows for t in self.targets) def to_state(self, sras: SrasFile) -> ResumeState: return ResumeState(path=sras.path, targets=self.targets, samples_per_frame=sras.header.samples_per_frame) def plan_resume(statuses, selected: set[int]) -> ResumePlan: """Expand an operator's angle selection into a runnable resume plan. Waveform data is one contiguous append-only stream, so nothing can be written past a gap: if the operator picks an angle at or beyond the frontier (the first incomplete angle), every angle from the frontier up to it must be re-acquired too. Those extras are reported in ``auto_added`` so the UI can say so. """ frontier_idx = next((s.index for s in statuses if not s.complete), len(statuses)) at_or_past = {i for i in selected if i >= frontier_idx} if at_or_past: final = selected | set(range(frontier_idx, max(at_or_past) + 1)) else: final = set(selected) targets = [ ResumeTarget(angle_idx=s.index, bg_offset=s.bg_offset, data_offset=s.data_offset, n_rows=s.n_rows, angle_deg=s.angle_deg) for s in statuses if s.index in final ] return ResumePlan(targets=targets, auto_added=sorted(final - set(selected)), frontier_idx=frontier_idx) def is_compatible(sras: SrasFile, *, velocity: float, laser_freq: float, sample_rate: float, n_channels: int) -> bool: """Whether appending to this file with the current settings is safe. A legacy v6/v10 file is not: it has one background for the whole scan, and every angle this engine acquires writes a background block of its own, which the older layout has no room for. """ h = sras.header return (sras.version in WRITABLE_VERSIONS and h.bytes_per_sample == 1 and h.n_channels == n_channels and abs(h.velocity - velocity) <= 1e-3 and abs(h.laser_freq - laser_freq) <= 1e-3 and abs(h.sample_rate - sample_rate) <= 1.0)