Skip to content

Consume external healsparse masks by per-object query at the catalogue level; instrument flags remain the only per-exposure input; no rasterization - #847

Open
cailmdaley wants to merge 5 commits into
developfrom
feat/healsparse-external-masks
Open

Consume external healsparse masks by per-object query at the catalogue level; instrument flags remain the only per-exposure input; no rasterization#847
cailmdaley wants to merge 5 commits into
developfrom
feat/healsparse-external-masks

Conversation

@cailmdaley

@cailmdaley cailmdaley commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

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):

bit n value 2ⁿ meaning source
0 1 wide star halo (outer) maskstars.wide2.reg
1 2 wide star halo maskstars.wide.reg
2 4 star mask (tight) maskstars.tight.reg
3 8 manual mask (galaxies) Mike, galaxy_mask/large_galaxy.reg on arc
4–8 16–256 ugriz footprint — 1 = no data in that filter
9 512 tile RA,Dec cut
10 1024 MaxiMask (streaks, dead columns, leftovers) — tile-level product Ludo, run on r-band stacks
11–14 2048–16384 free (11 → Pan-STARRS z2, confirmed by Mike)

MaxiMask 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 published finalmask_to_binary.py for 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 0x01 in the WeightWatcher config).

2. The design

Decisions — how each mask input reaches the two in-pipeline decisions:

flowchart TB
  classDef input fill:#dbeafe,stroke:#2563eb,color:#111827
  classDef flag fill:#f3f4f6,stroke:#6b7280,color:#111827
  classDef decision fill:#ffedd5,stroke:#ea580c,color:#111827

  INST["instrument flag images<br/>(per exposure, delivered)"]:::input
  SEL["sky-fixed healsparse<br/>halos · stars · manual ·<br/>footprint · tile cut · MaxiMask"]:::input

  INST -->|"pixel path,<br/>unchanged"| PIX["IMAFLAGS_ISO ·<br/>stamp weights"]:::flag
  SEL -->|"query per object:<br/>star mask (bit 2) +<br/>MaxiMask (bit 10) only<br/>(⚠ Q6)"| B4["star-selection flag"]:::flag

  PIX --> NGQ["ngmix:<br/>drop flagged epoch,<br/>keep the object"]:::decision
  PIX --> SETQ["PSF star selection:<br/>drop flagged candidates"]:::decision
  B4 --> SETQ
Loading

Outputs — everything ShapePipe emits:

flowchart TB
  classDef input fill:#dbeafe,stroke:#2563eb,color:#111827
  classDef output fill:#dcfce7,stroke:#16a34a,color:#111827

  INST["instrument flag images<br/>(per exposure)"]:::input
  SEL["sky-fixed healsparse"]:::input

  SEL -->|"per-object query<br/>(make_cat)"| CAT["catalogue flag columns:<br/>all healsparse bits per band ·<br/>instrument flags"]:::output
  INST -->|"IMAFLAGS_ISO"| CAT

  INST -->|"footprint ∧ ¬defect,<br/>map algebra"| WIN["n_epoch + n_pointings<br/>healsparse maps<br/>(survey window)"]:::output
Loading

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:

  1. Instrument flag images, one per exposure (delivered with the data). The only per-exposure input.
  2. Sky-fixed healsparse masks: halos, star mask, manual galaxy masks, footprint, tile cut, MaxiMask.

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

  • Instrument flag images: flow exactly as today. SExtractor reads them as IMAFLAGS_ISO; ngmix zero-weights flagged pixels in its stamps. Unchanged.
  • Sky-fixed healsparse bits (incl. MaxiMask): queried once per object at its (RA,Dec) → catalogue flag columns. Objects are rejected downstream, never pixels.

All decisions are object-level:

  • PSF star selection drops a candidate flagged in that CCD by the instrument flags (as today, via 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).
  • ngmix fits each object from postage stamps across all contributing exposures (epochs). When one epoch is flagged by the instrument flags, ngmix drops that stamp and fits the object from the remaining epochs. The object itself is lost only when fewer than three clean epochs remain (the existing n_epoch ≥ 3 cut).

Output masks, flags, and columns

  • Catalogue flag columns: every healsparse bit, per band, on every object (§3.1).
  • Per-epoch flag columns: instrument flags per object per exposure, consumed by the PSF star selection and the ngmix epoch loop (as today).
  • Coverage map: a healsparse 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. An n_pointings map 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.
  • Realized per-object n_epoch column: 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_cat calls healsparse.get_values_pos at each object's (RA,Dec) per band and writes MASK_u, MASK_g, … (or one integer MASK_BITS column 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_runner rasterizes any healsparse map through an image WCS into pipeline_flag FITS. 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-global headers — so the assembled products will churn; config-only coupling is what insulates us.)

4. Consumers and their bit diets

consumer route receives must not receive
SExtractor / ngmix stamp weights instrument flag images (pixel, unchanged) instrument bits any healsparse bit
PSF star selection per-CCD object flags (IMAFLAGS_ISO + query) instrument, star mask (bit 2), MaxiMask (bit 10) halo bits 0, 1 (⚠ Q6)
ngmix epoch loop per-epoch instrument flags instrument any healsparse bit
catalogue queried columns everything

5. The old path stays, and gets compared

Nothing is deleted; mask_runner remains 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

  1. Confirm the bit table as shipped in the DR6 merge (Mike; bit 11 = z2 confirmed verbally, wide/wide2/tight ↔ bit mapping per Hendrik 07-22 to be reflected in the merged product).
  2. One integer bit-map vs per-bit boolean files as the delivered format (drives the §3.1 column shape).
  3. Does MaxiMask enlargement/smoothing (Axel's dented-pattern concern) change bit 1024 upstream? No ShapePipe impact either way.
  4. Old-vs-new mask comparison on real data (tiles + exposures) #850 verdict on the halo-bit policy.
  5. Instrument-flag bit semantics: no bit table found; bits {0, 1, 3} populated on P3; only bit 0 used today. Ask Stephen for the authoritative table.
  6. PSF star-selection filter = instrument + star mask (bit 2) + MaxiMask (bit 10); halos excluded. The telecon settled the halo half (Mike reconfirmed 07-23); the star-body + MaxiMask cut is proposed here. Group to confirm; Old-vs-new mask comparison on real data (tiles + exposures) #850 is the evidence.
  7. The coverage/n_epoch map: in this PR or a follow-up? It needs only exposure footprints and defect maps, so it can be a sidecar step.

cailmdaley and others added 3 commits July 16, 2026 17:18
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
@cailmdaley

Copy link
Copy Markdown
Contributor Author

Exposure-path smoke: PASS (Nibi, real data). mask_ext_runner run standalone on split CCD 2114049-28 (MegaCam 2112×4644, the densest instrument-flag CCD of the exposure: 4.9% nonzero, values {1,2,3,8,11} — bad columns/saturation), with MASK_PATH = mask_ugriz_nside131072_n2.hsp (bright-star halos, bool), BIT_FLAG_MAP = 1:16, USE_EXT_FLAG = True.

  • Healsparse component vs an independent full-resolution rasterization (separate code, same WCS): 100.000000% pixel agreement, 0/9,808,128 mismatches.
  • Instrument flag summed through everywhere; output − inst_flag ∈ {0, 16} — clean additive decomposition, no bit collisions.
  • Fractions: healsparse 11.84%, instrument 4.94%, overlap 0.92%, total 15.86% (union checks out).
  • ~13 s for the 9.8M-px CCD, single chunk, WCS carried into the output header.

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

@cailmdaley cailmdaley linked an issue Jul 16, 2026 that may be closed by this pull request
@cailmdaley

Copy link
Copy Markdown
Contributor Author

Policy update from the 2026-07-21 mask-force telecon (details on #845): the rasterizer design here is unchanged, but the BIT_FLAG_MAP policy is now consumer-aware. Star-halo bits (1, 2) must not enter the flags that setools' IMAFLAGS_ISO==0 cut sees for PSF star selection — halo masks flag, they don't reject. Defect bits (e.g. MaxiMask/streaks) may still cut. The issue body's PSF-star-selection paragraph has been updated accordingly; provisional on the surviving-star overlay check (#850).

— Claude (fable) on behalf of Cail

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

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?

@cailmdaley cailmdaley changed the title Consume external healsparse masks: rasterize to pipeline flag images + per-band catalogue flags Consume external healsparse masks by per-object query at the catalogue level; instrument flags remain the only per-exposure input; no rasterization Jul 30, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consume external healsparse masks: rasterize to pipeline flag images + per-band catalogue flags Extend size of bright star masks [BUG]

2 participants