Consume external healsparse masks: rasterize to pipeline flag images + per-band catalogue flags#847
Open
cailmdaley wants to merge 4 commits into
Open
Consume external healsparse masks: rasterize to pipeline flag images + per-band catalogue flags#847cailmdaley wants to merge 4 commits into
cailmdaley wants to merge 4 commits into
Conversation
First step of the external-healsparse-mask path (#846): the reader rasterizes maskforce healsparse products onto image pixel grids in place of internal mask generation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CfRuCQa2UHo44yp2MZDsJX
…images New mask_ext module (PRD in #847): reads the image WCS, evaluates the pixel grid in memory-bounded row chunks, queries the healsparse map, applies a config-driven bit->flag mapping, optionally sums the external instrument flag (matching Mask._build_final_mask semantics), and writes the standard int16 <PREFIX>_flag<num>.fits. Same module serves tiles and exposure CCDs; mask files and bit meanings live only in config. Example tile/exposure configs; 7 unit tests on synthetic maps (bit mapping, chunk-seam independence, RA wrap, off-map, ext-flag sum, WCS round-trip). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CfRuCQa2UHo44yp2MZDsJX
… maps Optional MASK_EXT_PATHS (band:path pairs, env-expanded) in the make_cat section: each band's healsparse map is queried at object world positions (XWIN_WORLD/YWIN_WORLD) and added as a MASK_<BAND> column — the ShapePipe end of UNIONS-WL/spherex#38. Strict no-op when unset; off-map objects carry the map sentinel (-1 for integer maps). 3 unit tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CfRuCQa2UHo44yp2MZDsJX
The real 2025 r-band UNIONS mask (mask_r_nside131072.hsp) is a boolean healsparse map (True = masked), not an integer bit-flag map. With a boolean map, any BIT_FLAG_MAP bit other than 1 silently selects nothing (True & 64 == 0), producing an all-clean flag image. Raise instead, and document the two mask flavours in the example configs. Found by the real-data smoke: rasterizing the candide mask copy onto the CFIS.233.293 tile grid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013btLTHCgmiiggZmxJ4hM3n
This was referenced Jul 16, 2026
cailmdaley
marked this pull request as ready for review
July 16, 2026 17:00
Contributor
Author
|
Exposure-path smoke: PASS (Nibi, real data).
With the tile smoke above, both halves of the interface (§1) are validated on real data end-to-end. Old-vs-new comparison continues in #850. — Fable, on behalf of Cail |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #846. Part of the masking-unification epic #845; design reviewed in person by Martin + Cail 2026-07-16 (starting from the proposal on #839); Axel's architecture comments from the Slack thread (2026-07-16) are folded in below.
Motivation
UNIONS has converged on a single mask system: the external healsparse masks produced by the photometry-side mask effort (PhotoPipe ugriz footprint + bright stars, MaxiMask r, manual 2MASS galaxy masks — merged and converted to healsparse at nside 131072 ≈ 1.6″, conservatively fattened). These are the masks that set the catalogue flags and feed cosmology. ShapePipe should consume them rather than generate its own THELI-vintage masks (GSC-2.3 star halos/spikes rasterized by WeightWatcher, NGC/Messier region files, live Vizier queries).
What this buys:
The interface
Everything downstream of the mask module consumes a single artifact: the per-tile / per-exposure-CCD pixel flag image
pipeline_flag<num>.fits. SExtractor turns it intoIMAFLAGS_ISO; setools star selection cutsIMAFLAGS_ISO == 0; vignetmaker extracts flag stamps; ngmix zeroes weights on flagged pixels. Nothing downstream cares how the flag image was made — so the swap is local: read the healsparse map, rasterize it onto the image pixel grid through the WCS, write the same flag FITS.Design
1. New module:
mask_ext_runnerA sibling of
mask_runner(the old module stays untouched and config-selectable — see §5). Per input image:astropy.wcs.WCS(same machinery asMask._set_image_coordinates).healsparse.HealSparseMap.get_values_pos(ra, dec)→ per-pixel mask bits.int16flag image.USE_EXT_FLAGpattern — needed for exposures, where saturation/bleeding/bad columns arrive with the data).pipeline_flag<num>.fitswith the image WCS in the header.Config sketch:
The same module serves tiles and exposure CCDs — only the input image (hence WCS) differs, exactly as the current mask module runs twice. On the content of exposure-level masks, Axel (Slack, 2026-07-16) answers the previously-open question: pixel masks belong primarily at the single-exposure level (where PSF and shape measurement happen), composed of Stephen's bad pixels + cosmic rays, updated star spikes/halos, and bright nearby objects; the healsparse maps can be created per exposure and combined (Mike's alternative: reproject the tile-built maps). Either resolution leaves this module unchanged — it rasterizes whatever healsparse file
MASK_PATHpoints at through each image's WCS.2. Strictly mask-file-agnostic
Mask paths and bit meanings live only in config. The mask products are actively evolving (DR6 three-set merge in progress; Axel also proposes moving the catalogue-level product to healsparse polygons — effectively infinite resolution, straight from the DS9 regions); we start with the 2025 all-band masks and swap files later at zero code cost.
3. PSF star selection: no change
setools already cuts
IMAFLAGS_ISO == 0; once the flag image is healsparse-derived, star selection inherits the new masks automatically.4. Per-band catalogue flags via
make_catShape measurement only needs the r-band mask rasterized. The other bands are catalogue metadata, and healsparse maps are query-shaped products — so
make_catgains an optional per-band healsparse lookup at each object's (RA, Dec) (the existingadd_colpattern), writing e.g.MASK_u,MASK_g, … columns into the final catalogue. Off-footprint objects carry the map's sentinel (Falsefor the boolean maps) in these columns, while the rasterized pixel mask maps off-footprint to a configurableOFF_MAP_FLAG(default 0) — a deliberate split (pixel flags feedIMAFLAGS_ISO; catalogue columns are metadata). This is the ShapePipe end of UNIONS-WL/spherex#38, and mirrors how the healsparse masks already set catalogue flags on the photometry side.5. The old path stays, and gets compared
Nothing is deleted.
mask_runner(WeightWatcher/GSC/region files) remains fully functional and config-selectable. The comparison gate is #850: old-vs-new on real data (the Nibi P3 tiles/exposures) — visual overlays on real pixels, a component-provenance audit (where saturation/bleeding/bad columns/cosmic rays/halos/spikes/galaxies enter under each path), masked-area fractions,IMAFLAGS_ISOpopulations, PSF star selections and PSF model quality. Removal of the old path is gated on #850. Axel's further proposal — drop the tile-level mask input to SExtractor entirely (redundant if flags are applied at catalogue level) — is evaluated there too, not here. Mask-size validation via tangential shear around bright stars is #851.Out of scope
Data
All known mask products are boolean healsparse maps (dtype bool,
True= masked, valid pixels only where masked), nside 131072, coverage nside 128:arc:home/mhudson/masks/, now mirrored to Nibi at~/projects/def-mjhudson/cdaley/masks/) come as one file per bit,mask_ugriz_nside131072_n<bit>.hsp, produced by Mike'sHealsparse_mask_pool.pyfrom Ludo's tile-by-tile pixel FITS masks. Documented reason bits (script header):1faint star halo /2bright star halo /4bright stars /8manual mask /1024maximask;512= out-of-bounds. Empirical check (P3 region, 200k random points):OR{1,2,4,8,64,1024}reproducesmask_r_nside131072.hspexactly — the 2025 series is a per-bit decomposition of the same masking (which also says the r masking is unchanged Oct 2024 → May 2025 there). The earlier per-band reading (16_u/32_g/64_r/128_i/256_z/2048_z2as band masks) is contradicted:64is a reason bit inside Mike's default bitmask (0b10001001111), and16/32/128/256behave like per-band coverage flags (n128fires on 100% of the P3 region, where i-band is absent;n256on 71%;n2048is empty). Definitive bit semantics need Mike — until then, per-bandMASK_<band>catalogue columns (§4) should not be wired to then16/n32/…files./n17data/UNIONS/WL/masks/mask_r_nside131072.hsp, 932 MB, RICE/bit-packed) is the same bool flavour.For bool maps the config is
BIT_FLAG_MAP = 1:<flag>; any other bit would silently select nothing (True & 64 == 0), so the module fails loudly on that misconfiguration. Integer bit-flag maps (a single map with per-band/per-reason bits) don't currently exist, but the bit→flag mapping supports them if the DR6 merge produces one.Implementation plan
healsparse,hpgeominpyproject.toml/uv.lock(also declared on thecoveragebranch — no code overlap, only dep-file merge surface).mask_ext_package+mask_ext_runner; 7 tests on synthetic maps (bit mapping, chunk-seam independence, RA wrap, off-map, ext-flag summing, WCS round-trip).config_tile_MaExt.ini,config_exp_MaExt.ini);shapepipe_rundrivesmask_ext_runnerend-to-end (smoke below).make_catper-band columns + 3 tests.mask_r_nside131072.hsp) rasterized onto the exact CFIS.233.293 tile pixel grid (10000², TAN, 0.187″/px) viashapepipe_run: 38.0% masked, matching a direct healsparse query of the region (37.0%); morphology is unmistakably real (polygonal bright-star halos, diffraction-spike crosses, a bleed column, defect speckle). Overlay against actual image pixels (do masks land on the stars?) folds into Old-vs-new mask comparison on real data (tiles + exposures) #850.USE_EXT_FLAGsumming) — the same mask rasterized through the WCS of P3 split CCD 2086317-0 (2112×4644, from the Set up ShapePipe on Nibi and run end-to-end on a first batch of P3 tiles #808 run outputs on Nibi) with the instrument flag summed in, viashapepipe_runon the develop-runtime container: external mask lands on 20.4% of the CCD as bit 512; instrument bits (1/2/3/8) preserved exactly; combined values (513/515/523) confirm clean summing; morphology shows spike crosses, a bright-star polygon, and faint-star speckle against the instrument panel's border + bad columns.Implementation was independently reviewed against this description (fresh-context adversarial pass; tests rerun in the container): no correctness findings.
— Fable, on behalf of Cail