Skip to content

Consistent use of the external healsparse masks - #847

Open
cailmdaley wants to merge 18 commits into
feat/snakemake-orchestrationfrom
feat/healsparse-external-masks
Open

Consistent use of the external healsparse masks#847
cailmdaley wants to merge 18 commits into
feat/snakemake-orchestrationfrom
feat/healsparse-external-masks

Conversation

@cailmdaley

@cailmdaley cailmdaley commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Closes #846. Closes #837. Part of the #845 epic; builds on #852 and merges after it.

Until now, ShapePipe made its own masks: the mask module rasterized GSC star positions, Messier/NGC regions, and the instrument flags into a per-image flag file, and the star selection cut on that image. The mask force's external masks never entered the pipeline.

This PR removes all mask generation from ShapePipe. Two kinds of mask input remain, ingested differently.

  1. The instrument flags delivered with each exposure (bad columns, saturation) — the only masks that touch pixels. They flow as before: SExtractor reads them per CCD and records them per object as IMAFLAGS_ISO; ngmix sees them in its stamp weights.
  2. The UNIONS healsparse masks, fixed on the sky — never rasterized. Each map is queried once per object at its (RA, Dec) and the answer becomes a catalogue column: make_cat writes every bit, per band, into MASK_<band>, unfiltered. All selection on these columns happens downstream.

PSF star selection — the one decision that cannot be redone later — cuts only on the instrument flags: they mark corrupted measurements, stars whose own photometry and shape are unreliable. For everything the sky masks describe, the pipeline deliberately starts by relying on PSFEx's outlier rejection alone, and the first runs measure how well that holds. If it proves insufficient, there is machinery to pass any of the external masks — for example the star mask — to the PSF star selection, switched on in config.

Shape measurement uses no external mask: ngmix zero-weights instrument-flagged pixels and drops heavily flagged epochs, exactly as before. Whether an object in a halo, a trail, or a masked star enters the science sample is decided downstream from the MASK_<band> columns.

The survey footprint is a separate, positive coverage map — the exposure map of CCDs with a PSF model (#797), or the pointing-coverage map — and the mask bits subtract from it to give the final footprint.

The mask products

Sky-fixed boolean healsparse maps, one per bit, nside 131072 ≈ 1.6″; current products mask_ugriz_nside131072_n<bit>.hsp (2026-08-05, post-GSC2-fix; locations on the wiki):

bit value meaning
0, 1 1, 2 faint / bright star halo
2 4 star mask (the star's core and spikes)
3 8 manual mask (large galaxies)
4–8 16–256 no u g r i z data
9 512 outside the tile's unique region
10 1024 MaxiMask (trails, dead columns; tile-level)
11 2048 no Pan-STARRS z2 data

Paths and bit choices live only in config, so regenerated products cost no code.

What changed in the code

The mask module is deleted, together with everything that existed only to feed it: the GSC star-catalogue machinery, the WeightWatcher configs, and the tile-level masking variants. random_cat is deleted too — randoms and unmasked area are map algebra on the coverage map downstream, which is what closes #837. Exposure SExtractor now reads the instrument flag file straight from split_exp, and tiles are detected without any flag image. In the Snakemake workflow the exposure chain shrinks to get-images → split → psf: the star-catalogue staging, its config keys, and the mask rules are removed. The old CANFAR bash drivers still name the deleted configs; they are superseded by the workflow and left for a separate cleanup.

The first run of this branch refreshes the #850 old-vs-new comparison against the 2026 products.

— Claude on behalf of Cail

cailmdaley added a commit that referenced this pull request Jul 16, 2026
…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
@cailmdaley
cailmdaley marked this pull request as ready for review July 16, 2026 17:00
@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
cailmdaley added a commit that referenced this pull request Aug 31, 2026
…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
@cailmdaley
cailmdaley force-pushed the feat/healsparse-external-masks branch from c25cfdf to f6b9461 Compare August 31, 2026 12:59
cailmdaley and others added 5 commits August 31, 2026 09:05
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
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
cailmdaley and others added 11 commits August 31, 2026 10:45
ShapePipe stops making masks (#846, part of #845). Gone in one piece: the
`mask` module (GSC 2.3 Vizier queries, Messier/NGC region files, the
WeightWatcher .ww default and the halo/star .reg templates), the `mask_ext`
rasterizer that briefly stood in for it, and every config that ran either —
the *.mask / *.mask_simu mask configs, config_{exp,tile}_Ma_onthefly.ini,
config_{exp,tile}_MaExt.ini, the defunct MaMa and Sx_exp_* variants, and
workflow/config/cfis/config_exp_Ma.ini with its three dangling symlinks.

The star-catalogue staging the mask module needed goes with it: the
`star_catalogue` / `exp_star_cat` Snakemake halves (workflow/scripts/star_cats.py),
scripts/python/create_star_cat.py and collate_star_cat.py, and
shapepipe.utilities.vizier. Two utilities become dead in the same stroke and
are deleted rather than left orphaned: `focal_plane` (only create_star_cat and
star_cats.py called focal_plane_disc) and `utilities.file_io` (write_atomic had
exactly those two callers; it is prose-cited from tile.smk, fixed there).

Nothing replaces them with code. The sky-fixed masks are healsparse maps and
are queried per object — MASK_<band> columns in make_cat, FLAG_EXT in the new
mask_query module — so what used to be a pipeline stage is now a config entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cem4A9vjxA7nkPnyBKrc5W
…e masks

One healsparse lookup, two consumers. shapepipe.utilities.mask_query holds the
primitive: `query_map` reads a map and returns its value at each (RA, Dec), and
`flag_positions` ORs several maps into one integer per object.

`make_cat` now calls query_map for its per-band MASK_<band> columns instead of
opening healsparse itself, and the new `mask_query` module runs between
sextractor and setools on exposures, writing an integer FLAG_EXT (0 = clean)
into a NEW sexcat_ext<num>.fits beside the input — the spread_model `add`
pattern, so the LDAC_IMHEAD HDU survives and nothing mutates in place.
star_selection.setools gains `FLAG_EXT == 0` beside every `IMAFLAGS_ISO == 0`:
setools expressions have no bitwise operators, so the bit selection happens in
the module (MASK_PATHS, optional MASK_BITS) and the config only tests for zero.

The one interpretive choice is off-coverage. get_values_pos returns a map's
sentinel outside its coverage — False for boolean maps, -1 for integer ones.
make_cat passes that through verbatim (its documented off-map flag), but
flag_positions treats it as NOT flagged for both kinds: OR-ing -1 in would flag
every object a map does not reach, i.e. a map whose footprint stops short of an
exposure would silently reject all of its stars. Off-coverage counts are logged.

Tests build tiny nside_sparse=4096 maps and a three-HDU LDAC fixture and lock
in the boolean/integer/MASK_BITS/OR/off-coverage cases, that the LDAC structure
and the input file survive, and that make_cat's verbatim behaviour is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cem4A9vjxA7nkPnyBKrc5W
Exposures. SExtractor now reads the instrument flag straight from split_exp
(`FILE_PATTERN = image, weight, flag` — split_exp already writes an unprefixed
flag<num>.fits per CCD), with the run_sp_exp_Ma INPUT_DIR entry gone;
mask_query_runner sits between it and setools in both the example and the
workflow config, and star_selection.setools cuts on FLAG_EXT == 0 beside every
IMAFLAGS_ISO == 0. Same rewiring in config_exp_mccd.ini, which shares that
setools file. sextractor_runner's declared input_module follows the real parent.

Tiles. config_tile_Sx_nomask.ini becomes THE tile config: tiles have no
instrument flag image and no mask module now, so FLAG_IMAGE = False with
default_noimaflags.param is the only variant, in example/cfis and in the sims
dir. random_cat still needs a pixel mask image, but nothing in the pipeline
produces one — its second input is now declared external (the healsparse-native
replacement is #797), which is a real capability gap and is flagged as one in
the config, the runner and docs/source/random_cat.md rather than papered over.

Snakemake. `exp_psf` now depends on `exp_split` directly; `star_catalogue`,
`exp_star_cat`, `exp_mask`, `star_cat_cmd`, `in_container` and the STAR_CAT_*
helpers are gone, along with `config["star_cats"]` and the two mid-chain
localrules. The `exp_short` group goes too, and the docstring says why: it
existed to fuse exp_split with exp_mask, and a group of one rule submits exactly
the job the ungrouped rule submits. completeness.py trades its exp_mask stage
(mask_runner 40/1) for a mask_query_runner row inside exp_psf, floor 2 rather
than 40 because setools tolerates the same sparse-CCD attrition either side of
it; run_report drops the two stages; clean_exposure stops reclaiming link farms
that no longer exist.

Verified in the container: all 30 module runners import, every example and
workflow config's MODULE list resolves through get_module_runners, and the
workflow parses and builds its DAG (`snakemake --lint`, `-n`).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cem4A9vjxA7nkPnyBKrc5W
pipeline_tutorial.md's "Mask images" section becomes "Masks" and explains the
design instead of the old two-run mask procedure: healsparse maps queried per
object into FLAG_EXT and MASK_<band>, the instrument flag as the one mask that
reaches pixels, no internet access and no star-catalogue download anywhere.
pipeline_canfar.md loses its mask-tiles and mask-exposures steps and the
combine_runs flag_* staging; random_cat.md gains a warning that its input mask
images now come from outside ShapePipe; workflow/README.md and the sims README
describe the chains as they now are.

Dependencies. weightwatcher leaves the Dockerfile and the three docs pages that
listed it — the deleted mask module was its only caller (`ww` appears nowhere
else). astroquery and hpgeom leave pyproject.toml: astroquery had exactly two
importers, utilities/vizier.py and star_cats.py, and hpgeom was the mask_ext
rasterizer's. `uv lock` regenerated the manifest (astroquery, html5lib, pyvo
removed; hpgeom stays, healsparse pulls it in). canfar_avail_results loses its
-m / pipeline_flag check mode, and scripts/README.rst its create_star_cat entry.

Left deliberately: shapepipe.utilities.summary{,_params_pre_v2}'s mask_runner
entries. Those describe the pre-v2 CANFAR job map and parse the logs of runs
that already exist on disk — removing them would break summary_run against
historical trees without making anything current cleaner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cem4A9vjxA7nkPnyBKrc5W
The diet is settled: instrument flags (IMAFLAGS_ISO, already read by
SExtractor) plus the UNIONS star-body map (bit 2,
mask_ugriz_nside131072_n4.hsp), and nothing else. The committed
[MASK_QUERY_RUNNER] examples in config_exp_psfex.ini (both copies) and
config_exp_mccd.ini now name exactly that one map instead of the two-map
placeholder.

Halo bits 0 and 1 stay out on purpose — halos flag objects for the final
catalogue, they do not reject PSF stars (mask-force telecon, 2026-07-21) — and
MaxiMask is out too. The module docstring says so under its own heading, so
that a reader who finds one path in the config knows it is a decision rather
than an unfinished list, and knows widening it is a config edit and no code.

That asymmetry also explains the contract: MASK_PATHS is a path list rather
than a bit mask because the UNIONS products are one boolean map per bit, so
choosing bits is choosing files. MASK_BITS survives for integer maps that pack
several bits into one file, and its commented example drops from 1028 to 4 to
match the diet. The setools header and the tutorial's Masks section carry the
same note.

Configs re-validated in the container (0 bad, all MODULE lists resolve) and the
10 mask tests still pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cem4A9vjxA7nkPnyBKrc5W
… docs

Three follow-ups from review.

CI. deploy-image.yml's runtime binary smoke still ran `weightwatcher --version`
after the Dockerfile dropped the package, so the very next image build would
have failed on a tool nothing calls. Step line and the comment's mention both go.

SEGMENTATION, and this one was a real regression I introduced. Folding
config_tile_Sx_nomask.ini into config_tile_Sx.ini carried the nomask variant's
`CHECKIMAGE = BACKGROUND` over the masked config's `BACKGROUND, SEGMENTATION`.
The segmentation check image is not masking furniture: vignetmaker cuts
per-object segmentation stamps from it (config_tile_PiViVi_canfar_sx.ini
FILE_PATTERN = sexcat, segmentation), and ngmix's uberseg blend handling raises
at construction without them (ngmix.py ~496, shapepipe#776). Restored in both
copies. The sims config already had it, which is why nothing else caught this.

Docs. pipeline_canfar.md's "Collate star catalogues" section drove
collate_star_cat.py, deleted with the star-catalogue tooling. The recipe is
replaced with a warning that names what is missing rather than a silent cut: the
merge step below it consumes validation_psf_conv files that now have no producer
in this repo, and the star_cat/ + `combine_runs.bash -c psf_conv` staging around
it does not apply either. The PSF measurement itself is unchanged — shapes are
measured in sky coordinates during interpolation — so the collation is a
pass-through that is straightforward to rebuild. No other docs page referenced a
deleted script.

Re-verified in the container: 30/30 module runners import, 65 configs' MODULE
lists resolve (0 bad), the 10 mask tests pass, the workflow still parses and
builds its DAG, and deploy-image.yml is valid YAML.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cem4A9vjxA7nkPnyBKrc5W
Five review items, all in the query path.

PARTIAL READS. query_map did HealSparseMap.read(path) on a 583 MB map, per CCD
— ~22 GB of I/O per 40-CCD exposure to answer questions about 0.06 deg². It now
reads HealSparseCoverage first, computes the coverage pixels the positions touch
with hpgeom.angle_to_pixel at the map's nside_coverage, and loads only those.
make_cat gets this for free, sharing the primitive.

Measured on the DR6 star map for 2000 positions in one CCD-sized box, one
process each: partial 0.102 s / 157 MiB peak RSS, full 12.8 s / 3364 MiB,
IDENTICAL values. 125x faster, 21x less memory. The full-read footprint is
3.3 GiB — under the 4 GB mark — and the partial read adds ~0.15 GB to a rule
already asking for 16 GB, so exp_psf's mem_mb is left alone. Per exposure this
is ~4 s of map reading instead of ~8.5 min. test_partial_read_matches_full pins
partial == full for both map dtypes.

Two things the reviewer's sketch did not anticipate, both found by probing the
real map rather than reasoning. (a) healsparse RAISES when no requested pixel is
in the coverage map, so the all-off-coverage case is answered without asking it:
one small probe read for dtype and sentinel, then the sentinel everywhere.
(b) The neighbours padding is insurance, not necessity — every position falls in
exactly one coverage pixel and all are requested — but at nside_coverage=128 it
costs under a megabyte, so it stays.

BOOLEAN OFF-COVERAGE. n_off was hardcoded to 0 for boolean maps, so a map that
misses an exposure logged "0 flagged, 0 outside coverage" — indistinguishable
from clean. valid_mask=True cannot fix this: the UNIONS products are BOOLEAN, and
healsparse stores only the True pixels, so valid_mask returns the value itself
(verified against mask_r_nside131072_n4.hsp). Coverage now comes from the
coverage mask, which works for both dtypes, and all-off-coverage logs a warning
saying the zero column means "the map does not reach here", not "clean".

Also: FLAG_EXT's docstring no longer claims the value says which bits fired —
true only for integer maps, since boolean maps can only contribute 1.
config_Rc.ini's mask INPUT_DIR placeholder becomes <path/to/tile_masks> rather
than a $SP_CONFIG path that does not exist. A zero-detection CCD no longer
raises on data["XWIN_WORLD"] — it writes an empty FLAG_EXT column, matching the
sparse-CCD tolerance setools and the completeness floor already carry. hpgeom
returns to pyproject.toml, now imported directly rather than via healsparse.
pipeline_canfar.md's HSM sky-coordinates paragraph is lifted out of the
collate_star_cat deletion warning into standing prose, pointing at the test that
pins the convention.

Verified: 15/15 mask tests pass, 30/30 runners import, 65 configs resolve, the
workflow still builds its DAG, uv.lock regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cem4A9vjxA7nkPnyBKrc5W
… masking

Scope error in eb93838, mine to own: collate_star_cat.py was swept up with the
GSC masking star catalogues on a name match. It is the PSF VALIDATION collation
— per-exposure validation_psf files gathered into the validation_psf_conv
catalogues config_Ms_psfex_conv.ini merges — and has nothing to do with mask
generation. Restored byte-identical from origin/feat/snakemake-orchestration,
with tests/module/test_collate_star_cat.py, and pipeline_canfar.md's "Collate
star catalogues" section put back verbatim in place of the deletion warning
ba2271a wrote (the HSM sky-coordinates paragraph returns with it, as the
section's own prose, which is where it started).

Two things asked for in the revert turn out not to be needed, checked rather
than assumed:

  * focal_plane.py and utilities/file_io.py stay deleted. collate_star_cat.py
    imports nothing from shapepipe at all — only stdlib plus tqdm, joblib,
    numpy, astropy, galsim and cs_util. focal_plane_disc and write_atomic had
    exactly two callers between them, create_star_cat.py and star_cats.py, and
    both are masking and both stay deleted.
  * There is no scripts/README.rst entry to restore. That file listed
    create_star_cat (the masking one) and never listed collate_star_cat, so the
    edit in 1bceb2d was already correct.

create_star_cat.py, star_cats.py and utilities/vizier.py remain deleted, as
intended.

Verified in the container: the 9 restored tests pass alongside the 15 mask ones
(24 total), 30/30 runners import, 65 configs resolve, the workflow builds its
DAG, and both restored files diff clean against origin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cem4A9vjxA7nkPnyBKrc5W
…name

Three from final review.

`mask_map.sentinel` replaces `mask_map._sentinel` in the probe path — same
value (verified for both int and bool maps on healsparse 1.12.2), public API.

The probe also indexed `np.flatnonzero(coverage.coverage_mask)[0]` unguarded,
so a map with no coverage at all died with a bare IndexError from inside the
utility. It now raises `ValueError("healsparse map <path> has empty coverage")`,
naming the file, with a test.

The shipped MASK_PATHS placeholder becomes the real DR6 product name,
`mask_r_nside131072_n4.hsp`, in all three configs. The products are staged at
/project/6001537/cdaley/masks/dr6/ (same tree as
/project/def-mjhudson/cdaley/masks/dr6/), which is where the partial-read
measurement in f7d1fd6 was taken — that docstring already named the file
correctly, so nothing else mentioned the old placeholder.

25 tests pass in the container (mask_query, make_cat_mask_ext,
collate_star_cat); 65 configs resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cem4A9vjxA7nkPnyBKrc5W
…ra downstream

Cail's call. random_cat computed a tile's effective unmasked area and drew
randoms inside it by counting zero pixels in a tile mask IMAGE. That input was
the deleted mask module's output, and with the query design there is no such
image and no plausible producer for one: the survey window is map algebra on the
healsparse coverage map (#797), done downstream, not a per-tile pipeline step
that rasterizes to pixels first.

Rather than keep a module wired to a placeholder path nobody can fill — which is
what f7d1fd6 left, an honest but unpaid IOU — the module goes: the package, the
runner, config_Rc.ini, docs/source/random_cat.md and its toc.rst entry. There
were no tests to remove.

The module was already unsound independently of masking, which is part of why
keeping it had no value: `save_as_healpix` is called as `_save_as_healpix`, and
`process` references `file_name` and `output_dir` which are never bound. Any run
past the first few lines would have raised.

`reproject` leaves pyproject.toml with it — random_cat.py held its only import
(the two ngmix hits for "reprojection" are prose in comments). uv.lock
regenerated: reproject, plus pims, pyavm, slicerator, toolz and zarr that came
in behind it.

Verified in the container: 29 runners import (was 30), 64 configs resolve (was
65, 0 bad), the 25 mask/collate tests pass, the workflow still builds its DAG,
and random_cat / N_RANDOM / RandomCat / config_Rc / run_sp_Rc appear nowhere
outside scripts/sh/, which stays untouched as agreed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cem4A9vjxA7nkPnyBKrc5W
… placeholder at the real ladder

The Aug-2026 post-GSC2 band-combined healsparse products (arc:home/mhudson/masks/)
use the mask_ugriz_nside131072_n<bit>.hsp naming; the mask_r_* files on canfar's
ShapePipe/mask are the 2025 vintage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cem4A9vjxA7nkPnyBKrc5W
@cailmdaley cailmdaley changed the title Consume external healsparse masks by per-object query at the catalogue level; instrument flags remain the only per-exposure input; no rasterization Consistent use of the external healsparse masks Aug 31, 2026
cailmdaley and others added 2 commits August 31, 2026 12:21
…hing by default

The PSF star selection no longer cuts on the external masks. mask_query stays in
the chain and keeps writing FLAG_EXT from the shipped MASK_PATHS (the star-body
map, bit 2); star_selection.setools drops `FLAG_EXT == 0` from all four mask
blocks and rejects on the instrument flags alone, leaning on outlier rejection
for the rest.

This makes the exposure side agree with the tile side, which was already
permissive: make_cat writes MASK_<band> unfiltered. Flag transparently, cut
downstream — so the effect of a mask on the star sample can be MEASURED before
it is imposed, rather than baked in on the way past.

The escape hatch is documented where someone would look for it, because a
permissive default is only safe if reversing it is obvious. star_selection.setools'
header now spells out the change (add `FLAG_EXT == 0` beside each
`IMAFLAGS_ISO == 0`, one line per block) and says why it exists: if outlier
rejection turns out not to be robust enough. The module docstring gains a
"Nothing cuts on it by default" section, and the config comments beside
MASK_PATHS say NOTHING CUTS ON IT rather than naming a cut that is no longer
there. The tutorial's Masks section, pipeline_canfar.md and workflow/README.md
lose the same stale claim; the shared utility now says setools *could* cut on
the column, not that it does.

The diet section is retitled: MASK_PATHS is a list of maps to RECORD against
each candidate, not to reject on. Halos stay out for the same reason as
before — they say nothing about whether a star is a good PSF sample.

Verified: 25 tests pass, 29 runners import, 64 configs resolve, ruff clean.
Only the four cut lines changed in the setools body — the mask blocks and their
IMAFLAGS_ISO tests are otherwise untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cem4A9vjxA7nkPnyBKrc5W
…ASK_EXT

Two changes, both about making the naming and the default say what the design
means.

MASK_EXT, not FLAG_EXT. The column is one family with make_cat's MASK_<band>:
an external mask, queried and stored per object. "FLAG" belonged to the other
kind of mask entirely, which is the distinction the docstrings now lead with —
instrument flags mark a CORRUPTED MEASUREMENT (bad columns, saturation) and are
the only masks that reject anything in-pipeline, via IMAFLAGS_ISO in setools
and zero-weighted pixels and dropped epochs in ngmix; healsparse masks are
sky-fixed LOCATION flags that say where an object sits, not that its pixels are
broken, so they are queried to columns and every rejection is downstream.
Renamed in code, tests, docstrings, the setools header, the tutorial and both
READMEs.

MASK_PATHS ships commented out, and absence is now a strict no-op rather than a
ValueError: the runner treats a missing key the way make_cat treats a missing
MASK_EXT_PATHS, and MaskQuery with no maps copies the catalogue through
byte-for-byte with NO MASK_EXT column. The copy is the load-bearing part —
setools reads this module's output, so a no-op that wrote no file would break
the chain instead of disabling the query. mask_query stays in the MODULE chain
either way, so turning the query on is uncommenting one line and never editing
the chain. Two tests cover it, one asserting the output is byte-identical to
the input.

The framing that ties both: the PSF star selection deliberately starts from
outlier rejection alone, and MASK_EXT is the configurable pickup if that proves
insufficient — write the column, measure the effect, impose it only if the
measurement says to.

Also corrected in passing: edec546 renamed the measured file in the
partial-read note along with the config placeholders, which credited the
measurement to a file I did not run against. The note now says what was
actually measured — the staged single-band copy mask_r_nside131072_n4.hsp — and
names the mask_ugriz ladder it belongs to.

Verified: 27 tests pass, 29 runners import, 64 configs resolve (MASK_PATHS
absent, section and chain entry present in all three), ruff clean on the
touched files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cem4A9vjxA7nkPnyBKrc5W
@cailmdaley

Copy link
Copy Markdown
Contributor Author

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?

Question moot now, BIT_FLAG_MAP was a holdover from when the masks were getting rasterized onto the image grids. All bit masks are now consistently written to the catalog.

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