Coverage nexp masks#797
Conversation
# Conflicts: # docs/source/pipeline_canfar.md # pyproject.toml
- Extract _expnum_from_path, _parse_header_to_wcs, _megacam_field_corners as module-level helpers. process_single_header (static, for multiprocessing) and the instance methods now share one implementation. - Remove dead get_wcs_from_header and get_megacam_field (duplicated logic that had already been inlined into process_single_header). - Fix re.search(r'(\d+)\.txt') missing its subject string. The new helper also replaces the hard-coded p[end-6:end] exposure-number extraction in run(), so exposure numbers of any length work. - Declare verbose in params_default so the contract is local instead of relying on cs_util.args.parse_options to inject it. Addresses review feedback on #699.
- Replace hsp_map + 0 with hsp_map.copy() (healsparse supports .copy since at least 1.x; more explicit intent). - Declare verbose in params_default; currently populated only by cs_util.args.parse_options auto-injection. - Fix typo: "Install te cs_util" -> "Install the cs_util". Addresses review feedback on #699.
- coverage_plotter, header_downloader: declare verbose explicitly in params_default. - coverage_plotter: comment acknowledging that FootprintPlotter._regions is a private cs_util attribute; refactor to public accessor when cs_util exposes one. - header_downloader: expand the vos:cfis/pitcairn comment to flag it as UNIONS/CFIS-specific and document override. - build_and_plot_coverage_maps.sh: explain the BUILD_NSIDE=131072 choice (matches UNIONS bit-mask pixel scale, ~0.1"), and note the CoverageMapBuilder default of 2048 for lighter use. Addresses review feedback on #699.
Resolve pyproject.toml conflict: the coverage branch forked before develop's uv/pyproject modernization, so take develop's file as the base and layer in the coverage additions — healsparse + skyproj runtime deps, the plot optional-deps group, plot added to the dev extra, and the six coverage/get-ccds console entry points. All other files auto-merged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HvP7uKkfDU2PvWC7YmbuMW
The coverage feature adds healsparse (map building) and skyproj (plotting) to pyproject. healsparse was already resolved transitively via cs_util; this pins skyproj 2.5.0 and records both as shapepipe's own direct dependencies so 'uv lock --check' passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HvP7uKkfDU2PvWC7YmbuMW
cailmdaley
left a comment
There was a problem hiding this comment.
Thanks Martin. The merge conflict's resolved — I merged develop in (the branch had fallen well behind), reconciled pyproject.toml, and regenerated uv.lock, so it's MERGEABLE again. Structure looks clean; the main thing is a scope question, then a few specific items inline.
Per-exposure vs per-CCD coverage. The mask is currently built per exposure: get_exposures() collapses the per-CCD valid-PSF IDs down to unique exposure numbers, _megacam_field_corners() draws a single polygon over the whole ~1° MegaCam mosaic, and each exposure adds a flat +1 across it. So an exposure with even one valid-PSF CCD stamps +1 over its entire field — including the sky where its other CCDs had no valid PSF. nexp then over-reports PSF depth exactly where CCD-level failures cluster (chip gaps, flagged/edge CCDs). The per-CCD validity is already computed upstream, so the fix is to carry the exp-shdu identity through and draw one polygon per surviving CCD instead of collapsing to exposures.
Two questions so we're reviewing against the right target: (1) do you intend to build the mask at CCD level? (2) does the valid-CCD-with-PSF selection land in this PR, or a follow-up? The description flags it as still-to-do, so I want to make sure we're not measuring against the wrong scope.
I think this wants test coverage before it merges — there's none yet, and the pure parsing functions (exposure-number regex, header→WCS split, the corner convention) are exactly where silent failures would hide, and are cheap to cover. Happy to write those with you, and to help wire the per-CCD path if that's the direction.
A few specific items inline.
— Fable, on behalf of Cail
| extract_field_corners = "shapepipe.coverage_run:run_extract_corners" | ||
| build_coverage_map = "shapepipe.coverage_run:run_build_coverage" | ||
| plot_coverage_map = "shapepipe.coverage_run:run_plot_coverage" | ||
| coverage_pipeline = "shapepipe.coverage_run:run_pipeline" |
There was a problem hiding this comment.
Blocker. coverage_pipeline points at coverage_run:run_pipeline, which no longer exists (removed in an earlier commit). The installed console script raises AttributeError on invocation — drop this line, or restore an orchestrator function.
| exp_all = self.get_exp(patches) | ||
|
|
||
| # Turn exposures into exposure-single-HDU names (CCDs) | ||
| exp_shdu_all = summary.get_all_shdus(exp_all, n_CCD) |
There was a problem hiding this comment.
The missing-CCD set is computed at line 226 but never subtracted here, so this returns every CCD as valid — unfiltered. (The v1.3 path does filter on non-nan FWHM, so the two versions disagree.) Likely part of the valid-CCD-with-PSF work still to come; flagging since the current path silently returns unfiltered. Fix is exp_shdu_all -= exp_shdu_missing_all before returning.
| for i, r in enumerate(rows): | ||
| ra = [r["tlra"], r["trra"], r["brra"], r["blra"]] | ||
| dec = [r["tldec"], r["trdec"], r["brdec"], r["bldec"]] | ||
| poly = hsp.Polygon(ra=ra, dec=dec, value=1) |
There was a problem hiding this comment.
No RA-wrap or pole guard. Corners arrive straight from astropy as degrees in [0, 360), so an exposure straddling RA=0 yields non-monotonic corners (e.g. [359.8, 0.2, ...]) and healsparse fills the ~360° complement, corrupting nexp across that RA stripe; near Dec ≈ +90 (the driver runs dechi=90) the four-corner ordering also degenerates. Worth unwrapping RA to a common branch per polygon and guarding the pole.
|
|
||
| import numpy as np | ||
| import healsparse as hsp | ||
| import hpgeom as hpg |
There was a problem hiding this comment.
hpgeom is imported directly but isn't in dependencies — it only resolves today because healsparse pulls it in transitively. Worth declaring explicitly so it doesn't break the day healsparse drops it.
|
|
||
| # Download from VOSpace | ||
| try: | ||
| client.copy(source, dest, head=True) |
There was a problem hiding this comment.
client.copy writes straight to dest, so an interrupted transfer leaves a partial .txt that the exists(dest) check at line 194 treats as complete on the next run — a resumed job then builds on a truncated header. Download to a temp path and os.rename on success (atomic).
| @@ -0,0 +1,72 @@ | |||
| #!/bin/bash | |||
There was a problem hiding this comment.
No set -euo pipefail — a failed build/plot for one version doesn't stop the loop, and the final success message prints unconditionally.
| Then, build the healsparse coverage mask file as | ||
| ```bash | ||
| build_coverage_map | ||
| build_coverage_map -i exp_ra_dec_v1.6 -o coverage_v1.6.x.hsp -c 128 -n 131072 -v |
There was a problem hiding this comment.
-i exp_ra_dec_v1.6, but the extract_field_corners step above is run without -o, so it writes the default exp_ra_dec.txt — this filename is never produced, and the example throws FileNotFoundError if followed verbatim. Align the two.
The five coverage/get-ccds runners fell back to `args = sys.argv` (the full argv, including argv[0]) instead of `sys.argv[1:]`, so under the entry-point smoke test's `python -c "...; run_*()" -h` the parser read the program slot as the integer `-c` option and exited non-zero (`invalid integer value: '-h'`). Slice argv[1:] to match cs_util.args.parse_options' own convention. Also drop the `coverage_pipeline` console script, whose target `coverage_run:run_pipeline` was removed in an earlier commit (dangling entry point -> AttributeError on invocation), and declare `hpgeom` explicitly (imported directly by coverage_map_builder, previously only transitive via healsparse). Together these fix all six entry-point test failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HvP7uKkfDU2PvWC7YmbuMW
Cover the pure and lightly-fixtured logic: exposure-number parsing, the CCD-list -> unique-exposure reduction, the MegaCam field-corner convention, the multi-HDU header -> WCS split, power-of-two nside validation, and a regression guard for the -h/argv handling of the console runners. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HvP7uKkfDU2PvWC7YmbuMW
|
Update — dug into the failing checks and took care of them so this is green for you. Merging
Also declared And I added CI is green now (357 passed). With that, the one substantive thing still open is the design question at the top: whether the mask should be built per-CCD — so PSF validity is honoured per-CCD footprint — rather than per-exposure. That one's yours; glad to help wire it once you've settled the intent (and whether the valid-CCD-with-PSF selection lands here or in a follow-up). — Fable, on behalf of Cail |
Summary
Implements creation of coverage healsparse masks encoding the exposures on which PSF information is avialable.
The current code, implemented in
utilitiesdoes the following steps:header_downloader.py)field_corners_extractor.py)coverage_map_builder.py)This can be integrated into a workflow. We also need to implement obtaining the list of valid CCDs with PSF model.
Reviewer Checklist
developbranch