Consume external healsparse masks: rasterize to pipeline flag images + per-band catalogue flags#847
Consume external healsparse masks: rasterize to pipeline flag images + per-band catalogue flags#847cailmdaley 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
|
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 |
|
Policy update from the 2026-07-21 mask-force telecon (details on #845): the rasterizer design here is unchanged, but the — Claude (fable) on behalf of Cail |
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: consumer-aware bit policy
setools cuts
IMAFLAGS_ISO == 0, so any bit mapped into the exposure-level flag image rejects PSF star candidates. Per the 2026-07-21 mask-force telecon (notes), star-halo bits (1 faint, 2 bright) must not reject PSF stars — halo masks flag, selection happens at the catalogue level — while defect bits (e.g. MaxiMask streaks) may cut. TheBIT_FLAG_MAPconfig expresses this directly: map only the bits that should reachIMAFLAGS_ISO, and carry the rest as catalogue flags. Provisional on the surviving-PSF-star overlay check in #850.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