ngmix: config-selectable UberSeg neighbour masking (BLEND_HANDLING)#770
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! |
Brings the image-simulation machinery (example/cfis_image_sims/, fake_psf_runner, vignetmaker/sextractor updates, init_run_v2.0/run_job scripts — develop #766/#803) onto the ngmix uberseg A/B arm (BLEND_HANDLING, SEG_VIGNET_PATH, DILATE_NEIGHBOUR). Conflicts, all additive unions (no design tension): - example/cfis/config_tile_Ng_template.ini: kept both config-doc blocks (uberseg BLEND_HANDLING/SEG_VIGNET_PATH/DILATE_NEIGHBOUR + develop SEED_FROM_POSITION/METACAL_PSF). - ngmix_runner.py: kept both config reads and both sets of Ngmix() kwargs. - ngmix.py: kept both docstring blocks, __init__ params/attrs, the two added methods (_check_central_seg_label + log_mean_ellipticity), and both kwarg sets on do_ngmix_metacal (uberseg params + metacal_psf). seed_from_position (per-object RNG) landed cleanly from develop and coexists with the uberseg seg-check in the object loop. All touched files byte-compile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XSWPiisH6y7NFFbjHqthuC
…g input) Wire the coadd-frame segmentation vignets that ngmix BLEND_HANDLING = uberseg consumes into the image-sims tile chain, mirroring the real-data PiViVi RUN_3: - config_tile_Sx_nomask.ini: CHECKIMAGE = BACKGROUND, SEGMENTATION (was BACKGROUND only) so the tile SExtractor emits the segmentation check-image. - config_tile_PiViVi_canfar_sx.ini: add a third vignetmaker_runner and a [VIGNETMAKER_RUNNER_RUN_3] (CLASSIC, STAMP_SIZE 51, PREFIX = seg, FILE_PATTERN = sexcat, segmentation) cutting per-object coadd seg stamps row-aligned to the tile catalogue on the coadd VIGNET grid. Output lands at run_sp_tile_ViVi/vignetmaker_runner_run_3/output/seg_vignet*.fits. Config-only; no vignetmaker module change (CLASSIC mode cuts any input image). The uberseg arm points NGMIX_RUNNER SEG_VIGNET_PATH there; the noisefill arm gets the same DAG (extra Sx key + RUN_3) with BLEND_HANDLING left at default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XSWPiisH6y7NFFbjHqthuC
|
Interesting, I was not expecting scipy to be faster, good to know! |
… test reaches its assertion The ngmix runner reads BKG_SUB (config.getboolean) before the SEG_VIGNET_PATH existence check that test_runner_missing_seg_vignet_file_raises targets, so the double must answer getboolean to reach the FileNotFoundError. Honor the caller's fallback; the production path is unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dA9sas4vvNBcNKRmi973L
|
Merged into develop with origin/develop merged in first (through #843; all conflict resolutions additive). As validation, a noisefill control run of this exact merged tree on the image simulations reproduces the baseline catalog bit-for-bit — the new code is a verified no-op when — Claude (Fable), on behalf of Cail. |
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