Skip to content

Line neighbour masks up with each exposure's own pixels - #985

Open
cailmdaley wants to merge 8 commits into
developfrom
fix/seg-exposure-grid
Open

cailmdaley wants to merge 8 commits into
developfrom
fix/seg-exposure-grid

Conversation

@cailmdaley

@cailmdaley cailmdaley commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Line neighbour masks up with each exposure's own pixels

Closes #984.

The problem. ngmix fits every galaxy on each single-exposure stamp, at native exposure pixels. The masks it applies come from detection on the coadded tile: which pixels belong to a neighbour (UberSeg / noisefill), which fall off the tile, and which footprint belongs to the object itself. On develop those tile-grid masks are pasted onto the exposure stamp with only the MegaCam flip applied. The tile and exposure grids are offset and rotated relative to each other, so the pasted masks are misaligned. On tile 188.307 the shift is 0.56 px at the median, 1.1 px at P95, and more at stamp edges, with up to 1.8° of rotation:

displacement between flip-only and WCS-registered masks

That changes which pixels get masked: UberSeg masks differ in about half of all stamps. It also mis-assigns defects. In the example below, a bad column 2 px from the object centre lies inside its footprint, but the flip-only overlay puts it outside:

object 5422: flip-only vs registered footprint

The fix. Vignetmaker looks up each exposure-stamp pixel in the full tile segmentation map through both WCSs (nearest tile pixel). It stores three per-epoch maps on the exposure stamp's own grid: segmentation, neighbour and off-tile. ngmix uses those instead of the overlay. The image, weights, flags and PSF are untouched; nothing is resampled.

What it does to shapes. On 188.307 every object was fitted both ways with identical seeds. For the 7,923 resolved, well-measured objects, the m-like shift is −0.19% ± 0.09%, and there is no detectable additive shift (≈ ±4×10⁻⁵). Both are well inside requirements (|m| < 1%, |c| < 5×10⁻⁴). Objects whose masks don't change reproduce bit for bit. So run 1 isn't invalidated; this is a correctness fix for run 2.

Cost and adoption. The image vignette store grows by about 1.9 GB per 120k epochs, and vignetmaker takes segmentation.fits as a fourth input. It lands at a campaign boundary: rerun from tile detection, and never mix old vignette stores with this contract (old stores fail loudly). A real sp run of 188.307 passed end to end within budget (tile_vignets 6m26s, 14.1 GiB). That run exposed two bugs, both fixed here: WCS staging files collided across containers, and a 15-min store scan ran per chunk.

Contracts, frame handling, full shape study, validation, campaign notes

Design and product contract

For native stamp row/column indices, the exposure pixel coordinates are (INT_POS[1] + col − half_width, INT_POS[0] + row − half_height); stored INT_POS is in [row,col] order.
Both WCS calls use zero-based pixel centres.
Nearest tile cells are floor(x+0.5) on each axis, including upward half-cell ties.
OFFSET retains the image extractor's subpixel centroid remainder for the prior; adding it to these integer pixel-centre coordinates would shift categories away from the image grid.
The native image, detector flag, weight, background and RMS stamps remain untouched.

Celestial-frame contract: an exposure identified as FK5 with equinox J2000 is treated as numerical ICRS only when its header's ASTCAT contains a case-insensitive gaia token.
The implementation casefolds ASTCAT, splits it with [^a-z0-9]+, and requires gaia among those tokens; the real header is ASTCAT='test.gaia'.
It deep-copies that exposure WCS, sets radesys='ICRS' and equinox=NaN, and calls wcs.set(); it does not transform the numerical sky coordinates or mutate the stored WCS/header.
Every other exposure retains its declared frame.
After this normalisation, tile_frame.is_equivalent_frame(epoch_frame) must hold, including equinox where applicable; any remaining mismatch raises ValueError.
This is not a blanket FK5-to-ICRS conversion.
For 500 compact-object centroids in 188.307, the median residual is (+0.006,+0.003) px with common numerical coordinates, versus (−0.044,−0.010) px after FK5-to-ICRS conversion, supporting the measured-header interpretation in the #985 review.

  • Tile detection writes SEG_LABEL, an int32 pre-join check-image label.
    The catalogue join retains it while relabelling NUMBER and tile SEG_VIGNET.
    Vignetmaker uses it to relabel the full segmentation; sky is 0 and detections absent from the joined catalogue are −1.
  • Multi-epoch vignetmaker takes segmentation.fits as its fourth positional input, after sexcat, log_exp_headers and exp_numbers.
    Each existing image_vignet epoch entry gains SEG_VIGNET (int32), NEIGHBOUR (bool), and OFF_TILE (bool) with the image stamp's shape and extraction grid.
    make_cat drops SEG_LABEL with the tile stamp fields under its normal stamp-removal setting.
  • UberSeg recomputes its finite-stamp Voronoi partition on the registered segmentation, then applies the configured dilation.
    Full source support covers cells beyond the original tile VIGNET; a padded-with-sky 51×51 source would not.
    Off-tile cells come from the full tile rectangle, not a rotated-row marker heuristic.
  • Noisefill neighbours are every foreign nonzero registered segmentation footprint, including −1.
    This provides full-tile categorical support without guessing unavailable markers or inflating tile vignette products.
    On this retained tile, 29,751 of 2,468,504 foreign-footprint source pixels lack SExtractor's intensity marker, affecting 2,456 objects.
    The paired UberSeg science comparison does not measure this additional noisefill change.
    The tile SEG_VIGNET still supplies the catalogue-level neighbour_flag and tile-label integrity diagnostic.

Consumers touched are Ngmix.prepare_postage_stamps, make_cat's stamp-field removal, production data/simulation vignetmaker configurations, the simulation example producer/consumer chain, and the two masked-pixel validation helpers.
The sampling convention and footprint/blend descriptions are recorded in ASTRA and universes/committed.yaml.

What registration does to shapes

On tile 188.307, every object passing a FLAGS=0, 10≤SNR_WIN≤500 proxy cut (16,655) was fitted twice, flip-only (develop 5702688e) and registered (this branch). Inputs, settings and per-object RNG seeds are identical in both runs. 16,609 objects have all five metacal fits unflagged in both runs. All differences below are registered minus flip-only. Errors are paired object-bootstrap standard errors; they do not include field-to-field variation across tiles.

On resolved, well-measured objects the effect is small: an m-like shift of about −0.2% and no detectable additive shift.

Sample Pairs Response-only m-like shift Calibrated mean shift, component 1 Calibrated mean shift, component 2
Fitted 10≤S/N≤500, T/T_PSF,reconv ≥ 0.5 in both runs 7,923 −1.9×10⁻³ ± 0.9×10⁻³ −2.2×10⁻⁵ ± 3.4×10⁻⁵ +3.9×10⁻⁵ ± 3.8×10⁻⁵
Same cuts with the original-PSF size 8,331 −2.4×10⁻³ ± 1.0×10⁻³ −3.4×10⁻⁵ ± 4.4×10⁻⁵ +4.0×10⁻⁵ ± 3.8×10⁻⁵
Whole proxy sample, including unresolved and poor fits 16,609 −1.0×10⁻² ± 0.6×10⁻² +4.6×10⁻⁴ ± 2.5×10⁻⁴ +1.5×10⁻⁴ ± 1.5×10⁻⁴

The whole proxy sample is dominated by fits that carry zero flags but are pathological, e.g. one object whose fitted T/T_PSF goes from 2431 to −0.26. It is a stress figure, not a bias estimate. The requirements are |m| < 10⁻² and |c| < 5×10⁻⁴.

Per object, the changes are mostly zero with a long tail: P95/P99 of |Δe| = 0.010/0.026, and 5% of pairs have |Δe| > 0.01. |Δe| tracks the change in central mask area (Spearman 0.91). All 5,506 objects whose masks don't change reproduce all five metacal shapes bit for bit.

paired ellipticity differences

response shift and mask-area dependence, resolved sample

Limits. These are real, unsheared stamps from one tile. They measure how much the shapes and responses move, not a calibrated multiplicative bias; that needs paired known-shear scenes with neighbours at sub-pixel mask offsets. If each run is allowed its own size/S/N selection, the 95% envelope on the additive-like shift reaches ~5×10⁻⁴. Selection effects through the cuts are therefore the term to watch. A 2,600-object enriched experiment, the influence checks and the per-object records are in the science report (Nibi /scratch/cdaley/fix984/SCIENCE.md).

Validation

  • 1,222 passed, 5 skipped, 2 slow deselected: complete not-slow suite, including workflow tests.
  • 49 passed, 1 skipped: separate complete workflow suite with Snakemake 9.23.1.
  • 30 registration/frame tests: independent WCSs, integer/subpixel translation, rotation, SIP distortion, asymmetric footprints, rotated tile edges, half-cell ties, full source support, joined labels, upstream writing, pre-cut consumption and object 5422 / epoch 2254827-27.
    The full retained exposure header includes RADECSYS=FK5, EQUINOX=2000, and ASTCAT=test.gaia; its numerical lookup remains identical, genuinely mixed FK5/ICRS inputs raise, and the ICRS control passes.
    Additional cases check Gaia token matching, other equinoxes/frames, equal FK5 frames, and the header-store ingestion path.
  • 39 real epochs / 101,439 lookups: all three maps and the numerical sky coordinates are identical to the unconverted numerical lookup, with input stores opened read-only and immutable.
  • tests/mutations/flip_only_overlay.py restores the flip-only overlay in an isolated source tree: all three selected regressions fail, including missed off-tile rejection and imaflags_iso=0 instead of 3 for the retained example.
  • ASTRA-tools 0.2.17 schema/semantic validation passes for both records; 65 decision-contract tests pass.
  • The two science arms reproduce all old/new retained-epoch lists from the independent registration census exactly, with no seed mismatch.
    All 5,506 unchanged UberSeg/off-tile mask controls reproduce all five fitted metacal shapes bit-for-bit.
    Eight fit/weight/response functions have identical executable ASTs apart from docstrings and diagnostic error messages.

Logs: Nibi /scratch/cdaley/fix984/ (science, first round) and /scratch/cdaley/fix985/ (frame contract).

Campaign boundary

Use this at a campaign boundary, including the matching calibration simulations.
Rerun tile detection/join to obtain SEG_LABEL, then epoch vignette generation, ngmix, and final catalogue assembly; image stores without the registered fields fail loudly.
The full segmentation check image must remain available to vignetmaker.
Native exposure images, WCSs, detector maps, backgrounds and PSF models can be reused when otherwise unchanged, but old image-vignette/category stores must not be mixed with this contract.
The same native-grid footprint feeds #973 and can feed #983's footprint-touch veto; this branch retains develop's fixed-radius central veto.
For 120,421 epochs of size 51×51 the three category arrays add about 1.9 GB of uncompressed payload to the image vignette store, plus small dictionary overhead, without adding another product file.

Real run through sp

DR6 tile 188.307 was run privately through workflow/bin/sp in the campaign container, from tile detection to tile_make_cat, in one 16-core allocation at 3cec9076:

  • Registration fits its budget. tile_vignets took 6m26s with a 14.1 GiB peak, against a 20 min / 32 GB budget. The vignette store carries SEG_VIGNET (int32), NEIGHBOUR and OFF_TILE (bool) at the stamp shape on all 120,421 object-epochs.
  • The final catalogue builds. It has 36,051 rows and 299 columns, of which 35,708 objects are fitted. All 242 checked values on 11 objects are bit-identical to the paired study's registered-arm fits. This checks that the values propagate unchanged, not shear calibration.
  • Two bugs the run exposed, now fixed here. PID-isolated containers share a shell PID, so their WCS staging files collided; the staging file is now unique (4bf50f98). Each ngmix chunk scanned the whole vignette store before fitting, about 15 min on NFS; the scan now stops at the first populated entry (3cec9076). Both have regression tests that fail without the fix.

Practice gaps closed

A cross-family audit against our scientific-software practice asked for the following. Each item has its own commit:

  • Stale marker prose is rewritten, and the contract ids use *-footprints.
  • Twelve committed tests cover the boundary failures: a sexcat without SEG_LABEL, a 3-input MULTI-EPOCH config, and missing, misshapen or non-boolean categories.
  • The settings that select the registered path (the PiViVi segmentation patterns, the SEG_VIGNET gate and the SEG_LABEL writer) are tagged and pinned in preparation.categorical_registration, with tests showing that changing them fails.
  • The decision records the rejected flip_only_overlay option and the measured misregistration. It states the shape shift and notes that the noisefill footprint effect is unmeasured.

The branch merges develop at 6060c6de (#981) with no conflict resolution. A separate commit (fb4755a2) adapts the zero-object tests to the 4-input vignetmaker: a zero-object tile still has a full-size, all-zero segmentation image carrying the tile WCS.

Branch: fix/seg-exposure-grid, head fb4755a2, containing develop 6060c6de.


Claude (Opus 5.5) on behalf of Cail; implemented and measured by GPT-6.1 Sol.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JK7o9EvVyXSSb8s7hMDX1H

cailmdaley and others added 8 commits October 29, 2026 14:37
Sample full-tile labels through the independent WCSs at image INT_POS pixel centres. Store segmentation, neighbours and off-tile maps with each image epoch and consume them before ngmix masks, cuts and diagnostics. Keep native images unchanged and fail on missing categories.

Preserve pre-join labels as SEG_LABEL and strip this bookkeeping from final catalogues. Define noisefill neighbours from full foreign segmentation footprints so category coverage does not depend on finite tile VIGNET markers.

Cover independent astrometry, categorical sampling, joined labels, tile edges, the retained 188.307 example and a killed flip-only-overlay mutant. Record the convention in ASTRA and the workflow documentation.
Describe registered foreign footprints and the measured conditional shape sensitivity.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Use mktemp for atomic WCS publication and exercise concurrent copies with identical container shell PIDs. Pin the intentional campaign-boundary prologue change.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
A populated entry disproves all-empty; stop before reading the rest of each shared store in every chunk. Keep the all-empty product and guard ordering, open preflight stores read-only, and test both short circuits with poisoned later payloads.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Exercise the real four-input runner with an all-sky segmentation FITS product, preserve empty catalogue schemas, and verify registered-store dtypes, shapes, empty ranges and publication. Document the zero-detection segmentation and zero-key store contracts.

Co-Authored-By: Sol <noreply@openai.com>
cailmdaley added a commit to cailmdaley/shapepipe that referenced this pull request Nov 1, 2026
cailmdaley added a commit to cailmdaley/shapepipe that referenced this pull request Nov 1, 2026
@cailmdaley cailmdaley changed the title Register tile segmentation and neighbour maps on each exposure stamp's own grid Line neighbour masks up with each exposure's own pixels Nov 1, 2026
cailmdaley added a commit that referenced this pull request Nov 6, 2026
Conflicts: only tests/workflow/params_pin.json and params_pin_noisefill.json,
both generated digests. Took neither side: regenerated both with
--update-params-pin on the merged tree. tile_vignets, tile_ngmix and
tile_make_cat move from edge's values, the three rules whose prologue carries
#985's mktemp WCS staging; every other rule keeps edge's digest, so #994's and
#997's exposure and tile-store changes stand as they were.

Everything else merged cleanly and needed no adaptation. #997's Snakefile
tile_store_root bind and #985's tile_local() edit touch separate lines, and the
vignetmaker configs #985 changed are not the exposure configs #994 changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JK7o9HXHK2JHphJN9p7BYu

This branch has not been deployed

No deployments
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.

ngmix overlays tile-grid segmentation and neighbour maps on exposure stamps without WCS registration

1 participant