Take morning darks at the night's science exposure times (#831) - #840
Conversation
Exposes EXPTIME on the archive API (FrameInfo, list_frames/list_options, both PyobsArchive and LocalArchive), adds science_exptimes_for_night() to derive per-instrument/binning exptimes from a night's OBJECT frames, and extends DarkBiasScript with exptimes/match_science_exptimes so it can take one dark series per exposure time instead of always one fixed exptime. Exptimes below dark_min_exptime (default 5s, ADR 0015) are dropped before grouping since calibration treats them as bias-only. ADR 0015 flipped to accepted; this plan's status flipped to implemented.
ReviewNice, well-scoped PR — the archive/exptime plumbing, the shared tolerance helper, A few things to consider before merge: 1. (Medium) 2. (Medium) Edge semantics for empty/zero exptimes.
3. (Low/Med) 4. (Low) 5. (Low) 6. (Low) The 7. (Nit) 8. (Process) Plan section 4's deliverable — documenting the new Nits: Verdict: approve with minor feedback. Items 1 and 2 I'd want resolved (or explicitly documented as intended) before merge; the rest can be follow-ups. |
…imes Task.create_script() re-validates a fresh Script (and Archive) instance on every can_run()/estimate_duration() call, so the two never share instance state -- caching a resolved exptime list on self wouldn't have worked. Instead, science_exptimes_for_night() now caches its result at module scope, keyed by (site, night, tolerance, min_exptime), with a 5 minute TTL. can_run() (async) warms it; estimate_duration() (sync, can't query the archive itself) reads it via a new synchronous, cache-only peek_cached_science_exptimes_for_night(). Falls back to the previous placeholder estimate when nothing is cached yet. can_run() also now surfaces an unreachable archive as a cant-run reason instead of leaving that failure to surface later during run().
- DarkBiasScript only picks up science exptimes at its own configured binning now, instead of unioning across every (instrument, binning) science_exptimes_for_night found that night. - run() leaves the camera's image type/exptime untouched when match_science_exptimes resolves no exptimes, instead of switching to DARK for a series that never runs. - exptimes can no longer include 0 -- bias stays its own single series, per the plan; mixing it into a dark list under IMAGETYP=DARK was wrong. - estimate_duration()'s match_science_exptimes fallback (nothing cached yet) now uses a 600s placeholder (ADR 0015's reference exptime) instead of the always-0 configured exptime, which badly underestimated a real run's duration for scheduling purposes. - exptimes_close() is now symmetric (tolerance relative to max(a, b)); group_exptimes()'s docstring documents its greedy, non-transitive grouping instead of overstating a clustering guarantee it doesn't hold.
|
Addressed in 411a2dc:
Full suite still green (1721 passed, same 7 pre-existing/unrelated |
Re-review (after
|
- Pin Time.now() in test_can_run_true_with_observer_configured instead of hitting astropy's IERS auto-download, so the suite stays standalone offline. - Guard _resolve_night() inside can_run()'s try/except so a night-resolution failure (e.g. IERS) reports "cannot run" instead of raising. - Key science_exptimes_for_night's cache on archive class too, not just (site, night, tolerance, min_exptime), so two differently-backed archives serving the same site/night don't collide within the TTL. - Type PyobsArchiveFrameInfoDict.EXPTIME as NotRequired, matching the .get() read; correct the client-refilter comment's overstated guarantee.
…xptime-matching # Conflicts: # specs/steering/fleet-open-items.md
PR #840 landed the robotic/archive side of #831 on develop: flip the plan's index entry and ADR 0015 to their real status, drop #831's row from the open-issues tracker per its own maintenance rule (fix landed, even though the GitHub issue stays open pending a main release), and update #832/pipeline#13's notes to stop describing #831 as still pending.
- Pin Time.now() in test_can_run_true_with_observer_configured instead of hitting astropy's IERS auto-download, so the suite stays standalone offline. - Guard _resolve_night() inside can_run()'s try/except so a night-resolution failure (e.g. IERS) reports "cannot run" instead of raising. - Key science_exptimes_for_night's cache on archive class too, not just (site, night, tolerance, min_exptime), so two differently-backed archives serving the same site/night don't collide within the TTL. - Type PyobsArchiveFrameInfoDict.EXPTIME as NotRequired, matching the .get() read; correct the client-refilter comment's overstated guarantee.
Take morning darks at the night's science exposure times (#831)
PR #840 landed the robotic/archive side of #831 on develop: flip the plan's index entry and ADR 0015 to their real status, drop #831's row from the open-issues tracker per its own maintenance rule (fix landed, even though the GitHub issue stays open pending a main release), and update #832/pipeline#13's notes to stop describing #831 as still pending.
#832) - Reduction._create_master_darks() groups a night's raw DARK frames by exptime (tolerance-grouped, reusing #831's exptime_grouping) and creates one master per group instead of a single night-wide dark, longest-first. Frame groups with <3 members are skipped individually rather than aborting dark reduction for the whole instrument/binning. FILENAME gains an exptime component (shared by BIAS/SKYFLAT too, since it's one pattern) via a new FilenameFormatter "exptime" function; MasterCalibCreated carries the dark's exptime; ReductionBase's master-frame cache key gains exptime (None for non-DARK). - Pipeline.find_master() gains exptime/exptime_tolerance/exptime_max params: an exptime match ranks ahead of a merely time-close one for DARK, and exptime_max enforces scale-down-only when searching for a reference master. - Calibration implements ADR 0015's matching policy via a new _find_dark_master(): exact match (unscaled) > bias-only below dark_min_exptime > reference master (dark_scale_exptime, scaled down only) > allow_unmatched_dark_scale fallback > strict ValueError, caught the same way as any other missing-master case. _CalibrationCache keys DARK entries by the target exptime searched for (science exptime or the reference), since a science image's own EXPTIME no longer identifies which master it needs. _CCDDataCalibrator.dark_scale threads through to ccd_process() so an exact match isn't rescaled. Scoped to issue #832 only (reduction/pipeline side); #831 (archive/robotic side) already landed on develop via PR #840. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add pyobs-portal#143 (dashboard timeline shows local time, not UTC); drop #831/#832 (dark-exptime matching), already landed on develop via PR #840/#842; clear the stale vfs-token-auth unblock caveat now that pyobs-web-client's own index has caught up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BoEVuHBfy1A7M46kKCfYa1
Summary
EXPTIMEon the archive API:FrameInfo.exptime, anexptimefilter/tolerance param onlist_frames()/list_options(), and anexptimeskey inlist_options()'s return, in bothPyobsArchiveandLocalArchive.pyobs.robotic.utils.calibration.science_exptimes_for_night(), deriving the distinct (tolerance-grouped) exptimes a night's science frames used, per instrument/binning. Caches its result at module scope for 5 minutes, keyed by(site, night, tolerance, min_exptime).pyobs.utils.exptime_grouping(exptimes_close/group_exptimes), a shared relative-tolerance helper reused by the archive filter and the grouping helper (and intended for Dark masters per exposure time: match science frames by exptime, scale only a reference (600 s) dark #832's per-exptime dark-master grouping later).DarkBiasScriptwithexptimes(explicit list) andmatch_science_exptimes(derived), running one dark series per exptime, longest-first; the single-exptimedefault path (including bias,exptime=0) is unchanged. Mutual exclusivity betweenexptime/exptimes/match_science_exptimesis validated at construction.dark_min_exptime(default 5s, per ADR 0015) are dropped before grouping, since calibration treats them as bias-only and never needs a dark master for them.0015-dark-master-strict-exptime-matching-reference-scale-down-only.mdflipped fromproposedtoaccepted; plan2026-09-01-morning-darks-match-science-exptimes.mdflipped toimplemented.Scoped to issue #831 only (archive/robotic side). #832 (reduction/
Calibrationmatching+scaling policy) is a separate follow-up plan, not touched here.estimate_duration()vs.match_science_exptimesTask.create_script()re-validates a freshScript(andArchive) instance on everycan_run()/estimate_duration()call, so instance-level caching between them doesn't work. Instead,science_exptimes_for_night()'s cache lives at module scope:can_run()(async) warms it, andestimate_duration()(sync, can't query the archive itself) reads it via a new synchronouspeek_cached_science_exptimes_for_night(). Falls back to a single-series placeholder estimate if nothing is cached yet (e.g.can_run()hasn't run for that site/night within the cache's 5-minute TTL).can_run()now also surfaces an unreachable archive as a cant-run reason instead of letting that failure surface later duringrun().Other known gaps (called out in the plan's acceptance criteria, not solved by this PR)
PyobsArchive.list_options()'sexptimeskey depends on the pyobs-archive server actually returning one — not verifiable from this repo.list_frames()is safe either way: it re-filters by tolerance client-side regardless of server-sideEXPTIMEsupport.v2.1.1.dev1) is already stale relative to the just-releasedv2.1.1tag, a pre-existing gap unrelated to this PR — didn't want to guess a version number and add more noise there.Test plan
pytest tests/(excludingintegration/xmpp): 1718 passed, 7 pre-existing/unrelated failures intests/cli/test_pyobsd.py(local-environmentpyobsexecutable lookup, present ondeveloptoo).ruff checkclean.pyrefly check(CI's actual invocation,pyobs/only): 0 errors, same baseline suppressed/warning counts asdevelop.tests/utils/test_exptime_grouping.py,tests/robotic/utils/test_calibration.py(including cache hit/expiry/clear coverage), plus exptime coverage added totests/robotic/utils/archive/test_{local,pyobs}_archive.pyandtests/robotic/scripts/test_darkbias.py(including the cache-warm-up/fallback split forestimate_duration()).🤖 Generated with Claude Code