Skip to content

Coverage nexp masks#797

Open
martinkilbinger wants to merge 29 commits into
developfrom
coverage
Open

Coverage nexp masks#797
martinkilbinger wants to merge 29 commits into
developfrom
coverage

Conversation

@martinkilbinger

@martinkilbinger martinkilbinger commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements creation of coverage healsparse masks encoding the exposures on which PSF information is avialable.

The current code, implemented in utilities does the following steps:

  1. Read CCD list and download corresponding FITS exposure images headers (header_downloader.py)
  2. Extract CCD corner coordinates using WCS from header (field_corners_extractor.py)
  3. Create healsparse mask (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

Reviewers should tick the following boxes before approving and merging the PR.

  • The PR targets the develop branch
  • The PR is assigned to the developer
  • The PR has appropriate labels
  • The PR is included in appropriate projects and/or milestones
  • The PR includes a clear description of the proposed changes
  • If the PR addresses an open issue the description includes "closes #"
  • The code and documentation style match the current standards
  • Documentation has been added/updated consistently with the code
  • All CI tests are passing
  • API docs have been built and checked at least once (if relevant)
  • All changed files have been checked and comments provided to the developer
  • All of the reviewer's comments have been satisfactorily addressed by the developer

martinkilbinger and others added 25 commits January 9, 2026 14:18
# 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.
@martinkilbinger martinkilbinger self-assigned this Jul 9, 2026
@martinkilbinger martinkilbinger added the enhancement New feature or request label Jul 9, 2026
cailmdaley and others added 2 commits July 11, 2026 14:05
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 cailmdaley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread pyproject.toml Outdated
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-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.

cailmdaley and others added 2 commits July 11, 2026 14:46
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
@cailmdaley

Copy link
Copy Markdown
Contributor

Update — dug into the failing checks and took care of them so this is green for you.

Merging develop in ran its full suite against the branch, surfacing that all six coverage console scripts failed the entry-point smoke test (tests/unit/test_entrypoints.py::test_console_entrypoint_help_runs). Two causes, both fixed:

  • The five runners parsed the full sys.argv (including argv[0]) rather than sys.argv[1:], so -h collided with the integer -c option (error: option -c: invalid integer value: '-h'). Fixed to slice argv[1:], matching cs_util.args.parse_options' own convention.
  • coverage_pipeline pointed at run_pipeline, which had been removed — dropped the dangling entry point (easy to re-add as a real orchestrator later if you'd like one).

Also declared hpgeom explicitly (imported directly by coverage_map_builder, previously only resolving transitively via healsparse).

And I added tests/module/test_coverage.py — unit + property tests for the pure 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 handling. Happy to extend these as the feature grows.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants