ngmix: config-selectable UberSeg neighbour masking (BLEND_HANDLING)#770
ngmix: config-selectable UberSeg neighbour masking (BLEND_HANDLING)#770cailmdaley wants to merge 9 commits into
Conversation
A new ngmix-module option BLEND_HANDLING = {noisefill, uberseg} selects how
a neighbour sharing a galaxy's stamp is treated. The default `noisefill` is
byte-for-byte the historical behaviour (flagged pixels replaced by a noise
realisation, kept at inverse-variance weight); `uberseg` instead hard-masks
(weight -> 0) every pixel closer to a neighbour's segmentation footprint than
to the central object's, leaving the image untouched.
`uberseg_weight()` reimplements Erin Sheldon's MEDS uberseg (the
`meds._uberseg.uberseg_tree` nearest-segment Voronoi partition) with a
scipy cKDTree, rather than depending on `meds` whose import drags the full
fitsio/esutil stack. The surviving central core is a single connected,
roughly circular region — emergent geometry of the partition, not a separate
aperture, faithful to MEDS `get_uberseg`.
Plumbed runner -> Ngmix -> do_ngmix_metacal -> make_ngmix_observation ->
prepare_ngmix_weights; Postage_stamp gains a per-epoch `segs` list. The
segmentation-map *source* is not wired here (it is plumbing-gated: the seg
map is the coadd's, ngmix fits per-epoch stamps); selecting `uberseg`
without a seg map raises with a pointer to the gating issue.
Tests: synthetic two-object stamp asserts the mask geometry (neighbour-side
zeroed, central core single-connected, matches brute-force nearest-segment),
the noisefill default is byte-identical to the legacy noise-fill, and uberseg
leaves the image untouched while zeroing neighbour/flagged weight.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DLfuwQzavQQ9GbcKZQPc5
19fd652 to
0534daf
Compare
… rationale Name Erin Sheldon, the esheldon/meds repo, the MacCrann/Zuntz lineage, and DES use in the uberseg_weight docstring, and state that the reimplementation was validated bit-for-bit against his reference get_uberseg. Correct the dependency note: fitsio is already in the ShapePipe stack; esutil (+ a C-extension build) is what import meds would add. The argument for reimplementing is proportionality (a 5-line scipy cKDTree partition vs the whole MEDS file-format library), now backed by the validation harness in the uberseg fiber. Docstring-only; no behaviour change. Local checkpoint — not pushed (pending the next session's read of Axel's comment). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K35XrdS1Bn8EKp4VJGYpp6
# Conflicts: # src/shapepipe/modules/ngmix_package/ngmix.py
… flag Populate the UberSeg segmentation transport end to end, closing the gap where selecting BLEND_HANDLING=uberseg raised for lack of a seg source (shapepipe#776). - Tile_cat loads a coadd-frame segmentation VIGNET catalogue (seg_cat_path), one integer stamp per object, row-aligned to the tile catalogue. It is overlaid unchanged on every epoch (no per-epoch reprojection): prepare_ postage_stamps copies the coadd seg onto each surviving epoch, MegaCam- flipped to match its galaxy stamp so the overlay stays registered. - central_seg_label identifies the central object from the seg stamp's centre pixel rather than a plumbed SExtractor NUMBER, keeping the stamp self- describing and robust to a few-pixel coadd-vs-epoch offset. A sky centre pixel fails fast; the object is dropped by the per-object try/except. - uberseg_weight gains dilate_neighbour: binary-dilation iterations enlarging the neighbour mask to absorb that offset. Additive over the validated Voronoi partition; dilate_neighbour=0 recovers the pure Sheldon mask. - seg_has_neighbour computes a per-object blend flag (non-central footprint present), carried through compile_results into every shear HDU. - Ngmix fails fast at construction when uberseg is selected without seg_cat_path, replacing the deep per-epoch error as the primary guard. The default noisefill path is byte-for-byte unchanged; every new argument is optional and ignored off the uberseg branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…maker run
Wire the coadd-seg UberSeg inputs through the runner and example pipeline.
- ngmix_runner reads SEG_VIGNET_PATH (optional, required for uberseg; missing
file -> error) and DILATE_NEIGHBOUR (int, default 1), passing them to Ngmix.
- config_tile_Sx.ini emits the coadd SEGMENTATION check image alongside
BACKGROUND (CLI CHECKIMAGE overrides the .sex file, so no .sex edit).
- config_tile_PiViVi_canfar_sx.ini adds VIGNETMAKER_RUNNER_RUN_3, a CLASSIC/PIX
run cutting 51x51 integer seg stamps from segmentation{NUM}.fits at the tile
catalogue centres — the same grid as the coadd VIGNET, so the overlay is
well-defined.
- config_tile_Ng_template.ini documents BLEND_HANDLING, SEG_VIGNET_PATH and
DILATE_NEIGHBOUR (commented; uberseg requires the seg path).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Propagate the per-object blend flag (shapepipe#776) from the ngmix catalogue into the final catalogue as NGMIX_NEIGHBOUR_FLAG — a galaxy-only, OBJECT/ SHEAR-less metadata column alongside NGMIX_N_EPOCH and NGMIX_MCAL_TYPES_FAIL, enabling the systematics tests Axel requested. Added to final_cat.param so it survives the final selection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend test_ngmix_uberseg with: - central_seg_label: reads the centre-pixel label (not "label 1"), and fails fast on a sky centre. - seg_has_neighbour: true iff a non-central footprint is present. - uberseg dilation: dilate=0 is bit-identical to the pure Sheldon mask (and the O(N^2) brute force); dilate>0 zeros a strict superset once the grown neighbour footprint crosses the Voronoi bisector into the central cell. - noisefill ignores seg/dilate_neighbour kwargs (additive-safety). Add neighbour_flag to the compile_results / make_cat synthetic fixtures (test_ngmix, test_make_cat) so the new per-object column round-trips, and assert NGMIX_NEIGHBOUR_FLAG lands in the final catalogue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two Hypothesis property files that drive make_cat's SaveCatalogue._save_ngmix_data still built synthetic ngmix catalogues on the pre-#776 column set. Since the coadd-seg UberSeg work, _save_ngmix_data reads "neighbour_flag" unconditionally and emits NGMIX_NEIGHBOUR_FLAG, so those fixtures were one column short of the real writer contract: - test_psf_averaging_properties.py: the writer's get_data()["neighbour_flag"] raised on the column-less cat, failing test_absent_obj_id_keeps_sentinel_fill. - test_psf_grammar_properties.py: same missing-column read, and once fed the column the emitted NGMIX_NEIGHBOUR_FLAG was off-grammar for GRAMMAR_RE / FROZEN_GRAMMAR_RE. On a failing property Hypothesis runs its full shrink search, which is what made the sweep look hung at ~21% for minutes rather than fail promptly. Fix, matching the update commit 3bd759c already made to test_ngmix / test_make_cat: add "neighbour_flag" to each file's NGMIX key + INT key lists and base/measured row, add NGMIX_NEIGHBOUR_FLAG to the averaging test's _SENTINELS (so its zero-fill is now covered) and to the grammar regexes + frozen-grammar accept list (so the new metadata column is a recognised token). Additive only; no assertion weakened. Full tests/module tests/unit sweep: 332 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JxsRFJfbvpnx7mgAtqfJqe
The central segmentation label is now the object's SExtractor NUMBER (obj_id) everywhere — uberseg mask and neighbour flag alike. Seg labels are NUMBERs from the same SE run, so obj_id names the central footprint directly and cannot be stolen by a neighbour when a few-pixel coadd-vs- epoch overlay offset lands a neighbour's label on the centre pixel (which would invert the mask). This also unifies the neighbour flag's semantics across blend_handling modes. central_seg_label is demoted to a diagnostic: Ngmix._check_central_seg_label cross-checks the centre pixel against obj_id, warns on a mismatch (a registration signal) but proceeds with obj_id, and raises if the seg stamp contains obj_id nowhere (object dropped by the per-object try/except, loud in the log). Tile_cat.get_data now fails fast if the seg VIGNET catalogue is not row-aligned to the tile catalogue (row-count mismatch, or a NUMBER column that disagrees) — a silent misalignment would hand every object the wrong footprint. Regression tests: uberseg __init__ without SEG_VIGNET_PATH, the runner's FileNotFoundError on a missing seg file, and the three _check_central_seg_label outcomes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Hello, |
|
Regarding the implementation, I see that you are doing a pure python with KDTree. What is the compute time? Is it trackable? |
|
here's what Claude found, scipy cKDTree uses Cython and the inner loop is C++:
|
|
thank you for the comment and checking with DES folks, let me know if you have any more comments on this PR! |
Closes #776.
What
A config-selectable UberSeg neighbour treatment for the ngmix module, wired end-to-end and off by default:
BLEND_HANDLING = {noisefill, uberseg}(defaultnoisefill— the historical behaviour, byte-for-byte unchanged, asserted in tests).uberseghard-masks (weight → 0) every stamp pixel closer to a neighbour's segmentation footprint than to the central object's, leaving the image untouched — Erin Sheldon's MEDS/ngmix treatment.SEGMENTATION(one config line); a CLASSIC vignetmaker run cuts integer, non-interpolated seg stamps in the coadd frame; that single stamp is overlaid unchanged on every per-epoch stamp — no reprojection (UNIONS coadds carry no field rotation vs single exposures, so residual offsets are a few px at most).DILATE_NEIGHBOUR(int, default 1): binary dilation of the neighbour footprints to absorb those few-px offsets. Geometrically self-targeting: for well-separated objects the Voronoi bisector already claims the whole neighbour side and dilation adds nothing; it only bites for close blends — exactly the offset-sensitive regime.NGMIX_NEIGHBOUR_FLAGpropagated to the final catalogue (does the seg stamp contain any non-central object?), enabling downstream systematics splits.Design notes
uberseg_weightreimplementsmeds._uberseg.uberseg_tree(nearest-segment Voronoi partition) with a scipycKDTree, validated bit-for-bit against Sheldon's referenceget_uberseg(only measure-zero boundary ties differ). Reimplemented rather than depended on:esheldon/medsdrags thefitsio/esutilstack for a single small algorithm.NUMBER(seg labels are NUMBERs from the same run) — robust to registration offsets, which could otherwise let a neighbour's label sit on the centre pixel and invert the mask. The centre-pixel label is kept as a per-object diagnostic (warns on mismatch; the object is dropped if its footprint is absent).NUMBERmismatch).Why (fitting, not shearing)
Neighbour handling matters for the model fit: masking strictly along one side of a galaxy makes the weight anisotropic and lets the fit be "pulled" from the unmasked side. UberSeg reduces that pull by construction; 4-fold weight symmetrisation (@aguinot's least-biased endpoint) removes it entirely and is the declared follow-on PR — it composes with, and is orthogonal to, this one. The metacal shearing side (discontinuities, noise-fill of flagged pixels) is untouched here.
Validation & default
Unit tests cover the mask geometry (vs brute force and vs Sheldon), byte-identity of the default, dilation additivity, the fail-fast guards, and the flag reaching the final catalogue (56 ngmix/make_cat tests; full module+unit sweep green). The production default stays
noisefilluntil a real-data A/B (FLAGS=2-inclusive B-modes) says otherwise — that comparison is tracked separately and is not a merge condition for this dormant, default-unchanged capability.— Claude, on behalf of Cail