Consume external healsparse masks by per-object query at the catalogue level; instrument flags remain the only per-exposure input; no rasterization - #847
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 |
martinkilbinger
left a comment
There was a problem hiding this comment.
Looks all pretty good.
One question: Why is only the footprint mask ("BIT_FLAG_MAP = 64:1") in the config files? Should these not also include the star, maximask, and manual masks?
The catalogue-level mask query path (parse_mask_ext_paths, save_mask_ext_data) added in this PR had no shipped example. Document MASK_EXT_PATHS in config_make_cat_psfex.ini, the tile-level MAKE_CAT_RUNNER config, matching the design language used for the rasterizer's BIT_FLAG_MAP in config_tile_MaExt.ini. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KQnUyBXB85PdFF4xAKC6WC
Closes #846. Part of the masking-unification epic #845. The design follows the 2026-07-21 mask-force telecon (notes): measure everything, flag, reject at catalogue level. Every healsparse mask bit is queried — looked up per object at its (RA,Dec) and written as a flag column. No bit is rasterized onto the image pixel grid, and no healsparse bit acts at exposure level: per-exposure information comes only from the delivered instrument flag images, exactly as today.
1. Background
UNIONS aspires to one mask system: the external masks assembled by the photometry-side mask force (PhotoPipe, Ludo → Hendrik, with Mike's healsparse conversion). ShapePipe should consume this product, not generate its own THELI-vintage masks.
The canonical bit definitions (Ludo, #mask-force 2026-07-21; reg-file provenance per Hendrik 2026-07-22):
maskstars.wide2.regmaskstars.wide.regmaskstars.tight.reggalaxy_mask/large_galaxy.regon arcMaxiMask deserves a note, since it constrains the design: it is trained on single exposures, but Ludo has tuned its parameters specifically for Megacam r-band stacks, where it reliably catches satellite trails and other features that survive the upstream masks (this is its main job — PhotoPipe does no satellite-trail masking). The tuning does not transfer to other data, and there is no per-exposure MaxiMask product nor a plan to make one. Bit 1024 is therefore sky-fixed like every other healsparse bit.
Provenance note (2026-07-30): Hendrik found a GSC2 interface bug that affected the star masks of all tiles — stars between the GSC1 limit (mag ~15) and the intended 17.5 cut were never masked. The group voted to re-run everything with GSC1+GSC2. Stellar masks are re-done; Ludo is re-running the final combined masks (
/arc/projects/unions/catalogues/unions/GAaP_photometry/UNIONS_DR6_stars); Mike then regenerates the healsparse products. Every healsparse mask referenced in this PR and in the #850 comparisons will be superseded by that run. The design is unaffected (config-only coupling, §3.3), but the #850 evidence should be refreshed against the new products, and the star-mask footprint grows. Ludo also publishedfinalmask_to_binary.pyfor building binary masks from arbitrary bit combinations.Each exposure also arrives with an instrument flag image (
<exp>p.flag.fits.fz, produced by WeightWatcher in the CADC preprocessing). We could not find a bit table for these files. Observed values on P3 exposures are{0, 1, 2, 3, 8, 11}= bits {0, 1, 3} and combinations. ShapePipe currently uses only bit 0 (FLAG_MASKS 0x01in the WeightWatcher config).2. The design
Decisions — how each mask input reaches the two in-pipeline decisions:
Outputs — everything ShapePipe emits:
Many ideas have circulated: rasterize healsparse masks onto exposures, regenerate masks per exposure, reproject tile masks, feed external masks to SExtractor. This design reduces them to one rule. There are two mask inputs:
Only the instrument flag images touch pixels — and only they carry per-exposure information. Every healsparse bit is queried at object positions, once. A query touches ~10⁵ positions per tile instead of 10⁸ pixels, and masks stored as polygons are queried exactly, with no rasterization loss.
The known cost of tile-level MaxiMask: a satellite trail crosses one exposure, but a tile-level flag costs the object in all epochs. We accept this (discussed in #mask-force 2026-07-23) — the affected object fraction is small, and the alternative (per-exposure MaxiMask) does not exist. For PSF star selection specifically, PSFEx outlier rejection is a second line of defence against trail objects (Hendrik).
Input masks and how ShapePipe uses them
IMAFLAGS_ISO; ngmix zero-weights flagged pixels in its stamps. Unchanged.All decisions are object-level:
IMAFLAGS_ISO), or by the queried star mask (bit 2) or MaxiMask (bit 10). The halo bits (0, 1) do not reject candidates, per the telecon and Mike's reconfirmation 2026-07-23 (⚠ Q6 asks the group to bless the full diet).n_epoch ≥ 3cut).Output masks, flags, and columns
n_epoch(θ) = Σ_e [footprint_e ∧ ¬defect_e](θ)map, built by per-exposure map algebra over the instrument flags, with no object loop. This map is the survey window; sky-fixed healsparse cuts are applied to it at analysis time by map algebra, not baked in. Ann_pointingsmap follows from the same algebra and we plan to emit both. These maps must come from ShapePipe: only ShapePipe reads the per-exposure instrument flags that go into them.n_epochcolumn: what survived for each object after epoch drops and stamp failures. Map and column will not agree exactly; the difference is the object-level selection.3. Module design
3.1 Query machinery (the main path)
One lookup, one primitive:
make_catcallshealsparse.get_values_posat each object's (RA,Dec) per band and writesMASK_u,MASK_g, … (or one integerMASK_BITScolumn if the DR6 merge delivers one integer map — ⚠ Q2). ShapePipe end of UNIONS-WL/spherex#38. No epoch-level healsparse query exists: per-epoch information is the instrument flags, which already flow through the pixel path.3.2 The rasterizer (implemented, kept as fallback)
mask_ext_runnerrasterizes any healsparse map through an image WCS intopipeline_flagFITS. It is implemented and tested on this branch. Under the query design it is off the main path; it stays config-selectable for the #850 comparison.3.3 Mask-file-agnostic
Paths and bit meanings live only in config. Per-bit boolean files (2025 products) get a fail-fast guard against mis-specified bits; integer bit-flag maps use the table above. Product format changes cost zero code. (Note: Ludo is currently reprocessing the star reg files — repeated vertices and non-standard multi-
globalheaders — so the assembled products will churn; config-only coupling is what insulates us.)4. Consumers and their bit diets
IMAFLAGS_ISO+ query)5. The old path stays, and gets compared
Nothing is deleted;
mask_runnerremains config-selectable. The comparison gate is #850 (visual overlays, PSF star selections, masked-area fractions). Axel's proposal to drop the tile-level mask input to SExtractor is realized by construction: no healsparse bit reaches SExtractor.6. Out of scope
⚠ Open questions for the group
n_epochmap: in this PR or a follow-up? It needs only exposure footprints and defect maps, so it can be a sidecar step.