Skip to content

UberSeg end to end on #933's tile detection (SEG_VIGNET from the tile SExtractor run) - #925

Open
cailmdaley wants to merge 52 commits into
developfrom
feat/uberseg-seg-vignet
Open

cailmdaley wants to merge 52 commits into
developfrom
feat/uberseg-seg-vignet

Conversation

@cailmdaley

@cailmdaley cailmdaley commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

UberSeg on #933's tile detection: on data as on sims, the SEG_VIGNET stamps come from the tile SExtractor run's own segmentation map. Contains #922 (masked pixels), which this builds on.

UberSeg on a blended galaxy
UberSeg on a blended galaxy (tile 202.301, NUMBER 15152, MAG_AUTO 22.7). Left: the tile VIGNET, with the brighter neighbour's footprint masked (orange). Middle: SEG_VIGNET, the SExtractor segmentation relabelled to DR6 NUMBERs. Right: the UberSeg weight. Every pixel nearer the neighbour's footprint than to the galaxy's own (outlined) gets zero weight, 33% of the stamp here, and the galaxy keeps full weight.

Relabel collision
Why every label goes through one map. The join drops SExtractor detection 23409, and 23409 is also an unrelated DR6 NUMBER on the same tile; all 8 dropped labels on 202.301 collide like this. A partial relabel (middle) leaves those pixels reading as that other object. The full relabel (right) marks them −1, an anonymous neighbour.

Problem

UberSeg (uberseg_weight, seg_has_neighbour, Ngmix._check_central_seg_label) needs a segmentation stamp per object, on VIGNET's grid. It compares the stamp's labels with the row's NUMBER. #925 writes these stamps as the tile catalogue's SEG_VIGNET column in two ways: from the DR6 segmentation map in the catalogue converter, and from SExtractor's SEGMENTATION check image. #933 deletes the converter. Data now runs ShapePipe's own tile SExtractor, and match_catalogue joins it to DR6, so each row takes DR6's NUMBER and unpaired detections leave the catalogue. The SExtractor path now serves data and sims alike. On data, though, the check image is labelled with SExtractor's numbers, while the rows carry DR6's.

The two numberings overlap almost completely. On tile 202.301, 35,971 of the 36,014 final NUMBERs are also SExtractor NUMBERs on the tile, and all 8 dropped detections have SExtractor labels that are also final NUMBERs of other rows. A stamp that keeps any old label can therefore show a neighbour carrying the object's own number. UberSeg would then give the neighbour's pixels to the object.

Change

  • Order. sextractor_runner cuts SEG_VIGNET (add_seg_vignet) right after SExtractor and before match_catalogue. The join relabels a SEG_VIGNET column it finds.
  • Centring. SEG_VIGNET is cut where SExtractor cuts VIGNET. SExtractor 2.25.0 (src/analyse.c, ix=(int)(obj->mx+0.49999)) truncates the 0-based barycentre plus 0.49999, and the SIF's Debian 2.25.0+ds-3 package doesn't patch that file. vignet_centre mirrors the rule on X_IMAGE_DBL - 1 and Y_IMAGE_DBL - 1. It differs from round-to-nearest at exact halves with an odd floor and for fractional parts in (0.5, 0.50001). The float32 X_IMAGE can't resolve those cases, so SExtractor also writes the double positions, which are dropped once the stamps are cut.
  • Label design: one full relabel. match_catalogue maps every stamp label through a single lookup table. A paired row's SExtractor NUMBER becomes its DR6 NUMBER. A dropped row's label, and any label not in the catalogue, becomes UNMATCHED_LABEL = −1. Sky stays 0. The pairing is one-to-one, so this keeps the property UberSeg relies on: the row's own footprint is exactly the set of pixels labelled with its NUMBER. This works because the consumers only ask own-versus-other. The dropped footprint stays a neighbour (its light is still in the image) under a label no row can have.
    This holds only if DR6 NUMBERs are unique integers that the int32 NUMBER and SEG_VIGNET columns hold exactly. The join now refuses a catalogue with a repeated, non-positive, non-integer or above-2³¹−1 NUMBER; for example, 4294967295 would narrow to −1, the dropped label. That is the only code change to match_catalogue. Detect tiles with SExtractor on data too; take NUMBER from the UNIONS catalogue #933 already had the relabel; this PR tests it against collisions and guards its premise.
    A partial relabel, which maps only the paired labels and leaves the rest, collides exactly as described above. The tests include it as a negative control.
  • Memory. Under blend_handling: uberseg, tile_detect asks for 3,000 MB more (DETECT_SEG_MEM_MB), i.e. 7,000 MB. On 202.301, peak RSS is 1.53 GiB under noisefill and 2.93 GiB under uberseg. add_seg_vignet reads the 400 MB check image and adds an int32 column the size of VIGNET, which the join holds twice. Scaled to Detect tiles with SExtractor on data too; take NUMBER from the UNIONS catalogue #933's worst tile (1.84 GiB), uberseg needs about 3.5 GiB. Runtime is 170 s against 80 s, well inside 20 min.
  • Merge resolutions. The converter, its runner, tests and config_tile_Uc.ini stay deleted. pipeline/config.py takes Detect tiles with SExtractor on data too; take NUMBER from the UNIONS catalogue #933's stricter ${VAR:-default} expansion and keeps UberSeg end to end on #933's tile detection (SEG_VIGNET from the tile SExtractor run) #925's getexpandedboolean. tile.smk keeps Detect tiles with SExtractor on data too; take NUMBER from the UNIONS catalogue #933's single tile_detect, and detect_env adds blend_env("tile_detect") to SP_MATCH_CATALOGUE. astra.yaml keeps catalogue_neighbour_marking deleted and drops the converter from the blend and defect texts. Some masked-pixels tests in test_ngmix_defect_fill.py built tile VIGNETs with the converter. They now mark stamps as SExtractor does (cut_stamps) on the same DR6 patch. UberSeg end to end on #933's tile detection (SEG_VIGNET from the tile SExtractor run) #925's make_cat exclusion test moves to the SExtractor tests.
  • Params pins, regenerated with --update-params-pin (SIF plus host snakemake 9.23.1). params_pin_noisefill.json is byte-identical to Detect tiles with SExtractor on data too; take NUMBER from the UNIONS catalogue #933's params_pin.json, so blend_handling: noisefill plans exactly Detect tiles with SExtractor on data too; take NUMBER from the UNIONS catalogue #933's chain. The uberseg default moves tile_detect and tile_ngmix only.

Verification

  • Label tests (tests/module/test_sextractor_seg_vignet.py) go through sextractor_runner with SExtractor's outputs stubbed. They use a crowd in which each paired row's DR6 NUMBER is another object's SExtractor NUMBER, one of them the dropped detection's. Each own footprint carries the final NUMBER and nothing else does. The dropped footprint is −1. uberseg_weight and seg_has_neighbour give the same answers as on the SExtractor-labelled stamps. Without a join (sims), the stamps are the check image's. Each of these injected bugs fails the tests: the join running before add_seg_vignet, relabelling only paired labels, dropping the NUMBER guard or its type and range checks, and centring with np.rint.
  • Centring tests check vignet_centre against hand-written centres: exact halves (31.5 → 30, 32.5 → 31), fractions just below and above 0.50001, and a negative 0-based position. A scene test cuts its expected stamps by plain slicing, not by cut_stamps, at those hand-written centres. It includes objects where np.rint of the double positions is wrong, and one where SExtractor's rule on the float32 positions is wrong.
  • Boundary tests for the NUMBER guard: repeats, 0, −3, 5.5, 5.0, 2³¹ and 2³²−1 are refused, and 2³¹−1 is accepted.
  • One real tile, DR6 202.301, staged from /project/def-mjhudson/unions-wl/ into /scratch/cdaley/uberseg933/tile/. The real sextractor_runner ran with the committed config_tile_Sx.ini, SP_SEG_VIGNET=True and SP_MATCH_CATALOGUE set, and post-processing off (allocation 23087393). Results:
    • Of 202.301's 36,022 detections, none falls where SExtractor's rule and np.rint disagree, and none has an exact half (checked by a separate SExtractor run that writes X/Y_IMAGE_DBL, allocation 23094126). The numbers below were measured before the centring fix, so they hold unchanged.
    • The join paired 36,014 rows, dropped 8 and left 51 DR6-only objects.
    • The central pixel carries the row's final NUMBER in 36,013 of 36,014 stamps. The exception is a 24.2 mag object whose barycentre lies on sky (centre label 0); its own label is in the stamp.
    • No stamp has a pixel outside the row's own SExtractor footprint carrying its NUMBER.
    • Alignment: the check script finds VIGNET's own grid from the pixels (VIGNET = image − background) for 35,857 objects; 45 of these lie off the rounded X/Y_IMAGE centre (half-pixel ties). On that grid, SEG_VIGNET equals the relabelled check image for all 35,857. For the other 157 objects, VIGNET holds a constant value on some of the object's own pixels, so the grid can't be found from the pixels. For those, SEG_VIGNET equals the relabelled cut at the rounded centre.
    • VIGNET's −1e30 marks agree with SEG_VIGNET's foreign labels to 99.2% both ways.
    • A neighbour appears in 70.1% of stamps, and UberSeg zero-weights 26.8% of stamp pixels on average (dilation 0 or 1). These match UberSeg end to end on #933's tile detection (SEG_VIGNET from the tile SExtractor run) #925's DR6 converter numbers (70%, 27%).
  • Full suite (Nibi allocation 23136112, at a45b70e, develop e3ef252 merged in): 973 passed, 4 skipped; tests/workflow: 22 passed.

Merge notes

Out of scope

  • A full UberSeg campaign through ngmix, and the noisefill-vs-UberSeg comparison that decides the default (A/B validation of off-by-default shape-measurement options #813).
  • match_catalogue still holds the catalogue twice. Streaming the join would cut tile_detect's peak memory under both blend handlings.
  • The tile run had post-processing off, which adds per-epoch HDUs and touches no LDAC_OBJECTS column, so it doesn't measure the post-processing's memory or time with the larger catalogue.

Claude Opus 5.5 on behalf of Cail

🤖 Generated with Claude Code

cailmdaley and others added 29 commits September 26, 2026 07:07
…defects

Defects (nonzero flag, zero weight, invalid RMS) must be zero-weighted and
noise-filled under every BLEND_HANDLING, with neighbour pixels left raw
under uberseg. The masked-fraction cut must count that same raw set, and an
epoch with a defect closer than EPOCH_CENTRAL_DEFECT_RADIUS to the stamp
centre must be dropped, at a strict boundary and at the configured radius.
The runner must pass both cut options on, the HSM centroid must ignore raw
defect values, each epoch must keep its own OFFSET, and every tile must log
its epoch-cut tally.

The science test recovers the full 2x2 response for columns, 3-px bleeds,
single pixels and edge bands at the veto radius, on 0.3" and 0.5" galaxies
through round and elliptical 0.7" PSFs, and requires |c1|, |c2| < 5e-4 and
|m11|, |m22| < 1%. Flagged pixels in the simulation hold a hot value, so an
unfilled defect shows up. A column two pixels inside the radius is the
positive control.

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

Metacal shears the whole stamp without reading the weights, so a defect
left raw under uberseg reached the fit. Every defect (nonzero flag, zero
weight, invalid RMS) is now zero-weighted and noise-filled whatever
BLEND_HANDLING is; uberseg only zeroes the weight of neighbour-side pixels
and keeps their light. The option that meant "no neighbour treatment" is
renamed from noisefill to none, and the retired name is rejected.

The defect set is not symmetrized: at the veto radius the one-sided fill
leaves |c| <= 3e-4, while a four-fold mask would quadruple m (-2.7% against
-0.64% for a 3-px bleed at 10 px on a 0.5" galaxy) and, through an elliptical
PSF, still leave c1 = 7.3e-4.

An epoch is dropped when a defect lies closer than
EPOCH_CENTRAL_DEFECT_RADIUS (default 10 px) to the stamp centre, the
smallest radius at which columns, 3-px bleeds, single pixels and edge bands
give |m11|, |m22| < 1% and |c1|, |c2| < 5e-4 on 0.3" and 0.5" galaxies
through a 0.7" PSF. The masked-fraction cut counts the same raw defect set;
EPOCH_MASKED_FRACTION_CUT (default 1/3) makes it configurable. Each tile
logs "epoch cuts: considered=N masked_fraction=A central_veto=B
objects_emptied=C". The HSM centroid is measured on the filled image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QAc1ywyd9Sbbra6po2iuaA
prepare_postage_stamps already holds the object's galaxy vignet dict, so the
per-epoch OFFSET is read from it instead of a second sqlitedict lookup. The
pixel scale sets only the centroid-prior width; the comments that also
credited it with a noise window are corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QAc1ywyd9Sbbra6po2iuaA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The interpolation fills only short defect runs (at most 3 px along a row or
column, with clean pixels at both ends); anything wider, and runs reaching
the stamp border, stays noise-filled. The interpolant must reproduce linear
planes without reading defect values, commute with quarter turns of the
stamp, and apply one operator to the science and metacal noise images. The
weight is zero on the defects and on the quarter-turn orbit of the
interpolated pixels, whose light stays. Each defect is vetoed at the radius
of its own fill: EPOCH_INTERPOLATED_DEFECT_RADIUS for interpolated pixels,
EPOCH_CENTRAL_DEFECT_RADIUS for noise-filled ones. The runner and Ngmix pass
DEFECT_FILL to the epoch cuts and to metacal; noise stays the default.

The science test recovers the full response matrix for interpolated columns,
full and finite 3-px bleeds and single pixels at the interpolated radius, on
0.3" and 0.5" galaxies through round and elliptical 0.7" PSFs and on a 0.7"
galaxy through a 0.9" PSF, and for noise-filled wide defects at the noise
radius. A bleed three pixels inside the radius is the positive control.

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

DEFECT_FILL = interpolate fills short defect runs (at most 3 px along a row
or column, with clean pixels at both ends: columns, 3-px bleeds, single
pixels) with a Clough-Tocher interpolant of the clean pixels within 4 px.
The interpolant is averaged over the four quarter turns of the stamp, and
each turn's triangulation serves the science image and the metacal noise
image alike. Wider holes and runs reaching the stamp border keep the noise
fill. noise stays the default and is unchanged.

Interpolation restores the object's light, so the remaining bias is the
hole in the likelihood: a one-sided zero-weight column 8 px from a 0.5"
galaxy still gives c1 = -1.3e-3. The weight is therefore also zero on the
quarter-turn orbit of the interpolated pixels (their light stays), which
brings that column to c1 = +2e-6. Symmetrizing the fill instead would trade
true light for interpolated light over four times the area.

Each defect pixel is vetoed at the radius of its own fill:
EPOCH_INTERPOLATED_DEFECT_RADIUS (default 7 px) for interpolated pixels and
EPOCH_CENTRAL_DEFECT_RADIUS (10 px) for noise-filled ones. 7 px is the
smallest radius at which interpolated columns, 3-px bleeds and single pixels
give |m11|, |m22| < 1% and |c1|, |c2| < 5e-4 on 0.3", 0.5" and 0.7" galaxies,
round and elliptical PSFs; it does not scale with galaxy size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QAc1ywyd9Sbbra6po2iuaA
Brings the decision record to what this branch does:
- central_defect_veto: committed option fixed_radii (was disabled); the
  10 px EPOCH_CENTRAL_DEFECT_RADIUS is pinned by a Values entry on its
  module constant, now tagged.
- epoch_masked_fraction_cut: the cut counts the defect set against
  EPOCH_MASKED_FRACTION_CUT (no longer [HARDCODED] on flags).
- defect_fill: defects are noise-filled under every BLEND_HANDLING; the
  docstring [LINT] this branch fixes is dropped.
- blend_handling: the no-treatment option is BLEND_HANDLING = none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings develop (#875's decision record) in through the updated base, and
amends the record for this branch:
- defect_fill: the interpolate option is implemented (DEFECT_FILL =
  interpolate); noise stays the committed fill.
- central_defect_veto: EPOCH_INTERPOLATED_DEFECT_RADIUS = 7 joins the
  Values, on its now-tagged module constant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: the ngmix runner imports both the epoch-cut defaults and
write_empty_tile_output; Ngmix.process logs the epoch-cut tally and then
calls log_run_health. Develop's run-health test stubs prepare_postage_stamps
with the epoch-cut kwargs and tally and sets the epoch-cut attributes; the
defect-fill process tests give their vignettes an OFFSET and their logger
warning/error methods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The neighbour option keeps develop's name, noisefill, in the code, the runner
config key, tests, astra.yaml and the committed universe; the name none is
dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SExtractor's -1e30 markers in the tile VIGNET form their own per-epoch mask
(stamp.neighbours): a footprint 3 px from the centre, or covering 45% of the
stamp, drops no epoch; noisefill zero-weights and noise-fills exactly the
marked pixels, bit for bit as develop does; uberseg leaves them raw and
weighted; a flagged column near the centre is still vetoed; and
do_ngmix_metacal hands each epoch's mask to make_ngmix_observation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
prepare_postage_stamps wrote the tile VIGNET's -1e30 markers (other
detections' footprints) into each epoch's flag stamp as 2**10, so
defect_mask counted them: they fed the masked-fraction cut and the central
veto. Every epoch shares the tile VIGNET, so a neighbour within
EPOCH_CENTRAL_DEFECT_RADIUS dropped every epoch of the object.

The markers are now their own per-epoch mask, stamp.neighbours
(MegaCam-flipped with the epoch), threaded through do_ngmix_metacal and
make_ngmix_observation to prepare_ngmix_weights. The epoch cuts count
genuine defects only. Under noisefill the marked pixels get weight 0 and
the defects' noise realisation, the output noisefill gave when they arrived
as flags; under uberseg they are ignored and the seg-based weight handles
neighbours. Defects are treated identically under both. Docstrings, @sc
contracts, runner comments and the astra.yaml decisions say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SExtractor writes -1e30 into the tile VIGNET beyond the tile's edge as well
as on other detections' footprints. Beyond the edge the epoch holds the
object's own light, cut off, so treating those pixels as neighbours exempted
edge objects from both epoch cuts and measured them with a noise-filled band
near their centre.

split_tile_markers classifies the markers geometrically on the flipped tile
VIGNET: the union of stamp rows and columns that are entirely -1e30 (the
off-image part of a rectangle clip) is off-tile, the rest is the neighbour
mask. Off-tile pixels are flagged OFF_TILE_FLAG (2**10) in the epoch's flag
stamp, so under both blend handlings they are zero-weighted, filled, and
counted by the masked-fraction cut and the central veto. Docstrings, @sc
contracts, runner comments and astra.yaml say so.

Tests: an object 3 px from the tile edge loses every epoch; a band 12 px
away passes the default cuts and is vetoed at radius 13; a corner's L-shaped
off-tile region is flagged exactly and a border-touching neighbour stays a
neighbour; off-tile pixels are filled under both blend handlings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under DEFECT_FILL = interpolate and BLEND_HANDLING = noisefill, the
Clough-Tocher support included marked neighbour pixels that noisefill
replaces with noise, so a defect beside a bright neighbour was filled with
light the image no longer contains. The support now excludes ~clean (the
defects and the removed neighbour pixels); the NaN fallback to the noise
fill covers degenerate supports. Under uberseg the neighbour light stays in
the image and the support is unchanged. interpolate_defects names its
second argument `excluded`.

Test: a flagged pixel next to a 1e4 marked neighbour interpolates to the
kept-pixel interpolant (1.4, against 3102 with the neighbour in the
support) under noisefill, and from the raw neighbour light under uberseg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fill_defects used np.where, which promotes a float32 stamp to float64;
develop's noisefill kept the galaxy stamp's dtype. The filled image is cast
back to the input dtype, so noisefill stays bit-identical to develop on
float32 stamps. The develop-equivalence test gains a float32 case and
checks dtypes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…amp border

Every entirely -1e30 stamp row or column counted as off-tile. In an edge
stamp, a neighbour footprint that covers the on-image part of a row (a
wide DR6 footprint running from the tile edge) completes that row, so the
neighbour's pixels became OFF_TILE_FLAG defects: counted by the epoch
cuts, interpolated under DEFECT_FILL = interpolate, filled under uberseg.
Only the runs of marked rows and columns that start at a stamp border are
off-tile now.

Tests build tile VIGNETs with the catalogue-mode converter: on the real
202.301 DR6 patch every stamp splits exactly into off-image pixels and
seg-map neighbour pixels, and the edge case above keeps its footprint in
the neighbour mask through prepare_postage_stamps.

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

The neighbour-marking rationale and the converter's BIG comment describe
the markers as ngmix neighbours under flag 2**10; ngmix splits them into
off-tile defects and a neighbour mask. The masking text names the tile
VIGNET, not SExtractor, as the markers' carrier, since catalogue mode
paints them from the DR6 segmentation map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbT81hykcvtuefzB3FhYvY
A committed ini can then name a workflow knob with its default, and the
workflow exports the variable only when the knob leaves its default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under SEG_VIGNET = True the converter adds one int32 stamp per object of
the relabelled segmentation map, cut with the window and centres VIGNET
is cut with, so the two register pixel for pixel. The stamp cutter moves
to sextractor_script.cut_stamps for the SExtractor path to share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under SEG_VIGNET = True, sextractor_runner cuts the SEGMENTATION check
image on each object's VIGNET grid and adds it to the sexcat. VIGNET is
centred on the pixel nearest SExtractor's double barycentre; the float32
X_IMAGE/Y_IMAGE round to it except on exact half pixels, which are
resolved by matching VIGNET against image minus BACKGROUND.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tile_cat takes the stamps from the catalogue it already reads, and
BLEND_HANDLING = uberseg fails before the object loop when the column is
missing. SEG_VIGNET_PATH, its separate catalogue and its checks go. The
stamp columns stay views into the loaded table, so it is held once, and
BLEND_HANDLING is environment-expanded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
blend_handling: uberseg (config.yaml, default noisefill) exports
SP_SEG_VIGNET=True to tile_detect and SP_BLEND_HANDLING=uberseg to
tile_ngmix, and adds the SEG_VIGNET column's memory to each ngmix chunk.
The committed inis read them as ${SP_SEG_VIGNET:-False} and
${SP_BLEND_HANDLING:-noisefill}, so noisefill plans the campaign it
planned without the knob and the params pin does not move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A noisefill campaign launched from a shell that exports SP_SEG_VIGNET or
SP_BLEND_HANDLING (bare, or in the APPTAINERENV_/SINGULARITYENV_ forms
that survive --cleanenv) would hand them to every job through the slurm
executor's --export=ALL. The Snakefile now pops them at parse time, so
only blend_env's exports under uberseg set them. Prologues and shells are
unchanged; the DAG harness gains a launch_env to test it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under SEG_VIGNET, sextractor_runner runs SExtractor with a copy of the
param file that adds X_IMAGE_DBL and Y_IMAGE_DBL; add_seg_vignet centres
each stamp at rint(DBL - 1), as SExtractor centres VIGNET, and drops the
two columns, so the catalogue is noisefill's plus SEG_VIGNET. The
half-pixel tie matching against image minus background goes, and with it
the BACKGROUND check image dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
blend_handling defaults to uberseg in config.yaml and the Snakefile, so a
campaign without the key writes SEG_VIGNET in both detection modes and runs
ngmix with BLEND_HANDLING = uberseg. The committed inis keep the modules'
own noise-fill defaults and the workflow exports the uberseg values, so
blend_handling: noisefill plans exactly the previous noise-fill campaign:
params_pin_noisefill.json is the previous pin, and a noise-fill run dir
resumes with the key set explicitly. The default pin moves in tile_detect
and tile_ngmix only.

astra's committed blend_handling becomes uberseg, with noisefill the
comparison arm. test_committed_blend_handling_fills_defects checks the
committed blend_handling x defect_fill pair against prepare_ngmix_weights:
the workflow default is the committed blend handling, and under it every
defect is zero-weighted and filled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbT81hykcvtuefzB3FhYvY
@cailmdaley
cailmdaley force-pushed the feat/uberseg-seg-vignet branch from 7af2d80 to c704e63 Compare September 30, 2026 01:10
@cailmdaley

Copy link
Copy Markdown
Contributor Author

Yikes, is there any way to use uberseg without actually doubling the disk and memory?

cailmdaley and others added 3 commits October 1, 2026 04:01
A value with ${VAR:-default} expands to default when VAR is unset or
empty, as in the shell; $VAR and ${VAR} stay strict. The same change as
feat/uberseg-seg-vignet's, so the two merge cleanly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MATCH_CATALOGUE (an ASCII SExtractor catalogue of the same image) pairs
each SExtractor row with its mutual nearest neighbour within MATCH_RADIUS
pixels; paired rows take its NUMBER, unpaired rows leave the catalogue, and
a SEG_VIGNET column, when present, is relabelled to the new numbering. Fewer
than MATCH_MIN_FRACTION of the rows pairing stops the run: that is what a
catalogue measured on other pixels (a DR5 image against DR6) looks like.
The join runs before the post-processing keys epochs on NUMBER.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Data and image simulations both run tile_detect as SExtractor
(config_tile_Sx.ini), so XWIN, FLAGS_WIN, the VIGNET neighbour markers and
every SExtractor column are measured one way. Under tile_detection:
unions_catalogue, tile_get_catalogue fetches only the UNIONS .cat and
tile_detect joins its detections to it (SP_MATCH_CATALOGUE), keeping the
shared NUMBER and TILE_UNIQUE_ID. On DR6 186.307 all 41,201 detections pair
(median separation 4e-5 px); 45 catalogue objects, faint deblended children,
have no detection.

Gone: read_ext_sexcat and its tests, config_tile_Uc.ini, the runner link,
the segmentation-map fetch, merge_final_cat's SEXTRACTOR_ONLY_COLUMNS and
--tile-detection, and the detection.catalogue_neighbour_marking decision.
completeness.py changes, so this lands at a campaign boundary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cailmdaley and others added 8 commits October 1, 2026 04:27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without DETECTION_IMAGE the runner called SExtractor in dual-image mode
with the same image and weight twice, which changes the background,
MAG_AUTO and FLUX_RADIUS of a fifth of a tile's objects against the
single-image call and the UNIONS catalogue. Dual-image mode stays for a
real detection image.

Closes #936

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation

The run also stops when fewer than MATCH_MIN_FRACTION of the catalogue
objects pair, which catches a detection configuration that finds fewer
objects than the catalogue. Tests cover the radius bound and both sides
of the guard. astra.yaml records the eight-tile numbers, single-image
detection, and the MAG_AUTO differences beyond a pixel-stack overflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
${VAR-x}, ${VAR:=x} and ${ VAR } passed through as literal paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Peak RSS 1.75 GiB and wall time 143 s at most; SExtractor is built
without threads. One retry doubles memory and runtime. Image
simulations export SP_MATCH_CATALOGUE empty. Stale comments on the
catalogue fetch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
300000 overflowed on four of eight DR6 tiles (a cD galaxy, a bright
star's halo, 13 times on Alioth's tile); 3000000 clears all eight.
Peak RSS rises to 1.84 GiB and wall time to 160 s on the worst tile;
tile_detect's 4000 MB stays at 2.1x. The exposure config never
overflows on the 40 CCDs checked and is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guard catches other pixels (DR5 pairs ~95%), not deblending near
large objects (DR6 tiles pair at least 99.2%) or a few children on a
near-empty edge tile. New key MATCH_TOLERATED_UNPAIRED = 20;
MATCH_MIN_FRACTION = 0.98.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cailmdaley

Copy link
Copy Markdown
Contributor Author

Any way to use UberSeg without doubling disk and memory? Yes. It doesn't double anything today, and two small changes bring its overhead close to zero. Measured on the smk-ab925 A/B (same 25 tiles, code 4570a907).

What it costs now (measured)

  • Final products: nothing. make_cat drops SEG_VIGNET. final_cat_smk-ab925-us.hdf5 and -nf.hdf5 are both exactly 953,220,227 B.
  • Scratch: SEG_VIGNET is int32 51×51, the same bytes as VIGNET: ~323 MiB per tile (calculated as 32.6K rows × 2601 × 4), living in the tile sexcat until the tile is cleaned. For 330 tiles that's ≤104 GiB, ~3.6% of p3a's 2.8 TiB peak.
  • tile_shape memory: +4.13 GB per tile (≈ +10%; median MaxRSS 40.1 vs 35.9 GB), +245 MiB per ngmix chunk (1.51 vs 1.24 GB).

Why memory grows 16×, not 1×

TileCat.get_data (ngmix.py ~l.556) opens the whole tile catalogue through FITSCatalogue, which has memmap=False by default (file_io.py l.441). With 16 chunks, each chunk process loads the full VIGNET and SEG_VIGNET columns. So both stamp columns are held 16 times per tile. This hits noisefill too: VIGNET alone is ~323 MiB × 16 ≈ 5 GB of duplicated stamps per tile_shape job.

Fixes, cheapest first

  1. Load only the chunk's rows, using memmap=True or a row-range read. This cuts the uberseg overhead to ~1/16, and it removes ~15/16 of the VIGNET memory that noisefill already pays. It applies to every campaign whatever blend_handling says, and it's the bigger win for tile_shape's memory request.
  2. Store less in the seg stamp. uberseg_weight, seg_has_neighbour and _check_central_seg_label only ever compare the stamp against 0 and the object's own NUMBER, so a neighbour's identity is never used.
    • A uint8 3-state stamp (0 sky, 1 own, 2 other) is 1/4 the size.
    • A bitpacked own-footprint mask is ~10 MiB per tile, with "other" taken from VIGNET's −1e30 markers. That pixel alignment is not checked.
  3. Not worth it: cutting seg stamps from the seg check image at shape time. That means a ~400 MB per-tile image to stage, and it brings back the second file and the grid-alignment problem that UberSeg end to end on #933's tile detection (SEG_VIGNET from the tile SExtractor run) #925's single-file design removed.

Under #933

On data, #925's SExtractor-mode path is the live one. config_tile_Sx.ini already writes CHECKIMAGE = BACKGROUND, SEGMENTATION, so it adds no new file, only a transient copy inside tile_detect.

Unverified: why the per-chunk delta (245 MiB) sits below the column size (323 MiB). The likely cause is the Snakemake benchmark's sampling.

Claude Opus 5.5 on behalf of Cail, from the nibi Surveyor's measurements

cailmdaley and others added 7 commits October 1, 2026 18:46
#933 runs ShapePipe's tile SExtractor on data too and joins DR6 by
match_catalogue, deleting the catalogue converter. SEG_VIGNET now comes
from SExtractor mode on data and sims alike.

Resolutions:
- read_ext_sexcat.py, read_ext_sexcat_runner.py, test_read_ext_sexcat.py,
  config_tile_Uc.ini: deleted, as in #933.
- sextractor_runner.py: add_seg_vignet runs before match_catalogue, which
  relabels the stamps it finds.
- pipeline/config.py: #933's stricter ${VAR:-default} expansion, plus #925's
  getexpandedboolean.
- tile.smk: #933's single tile_detect; blend_env("tile_detect") joins
  SP_MATCH_CATALOGUE in detect_env. The rule docstring describes SEG_VIGNET
  on the SExtractor path only.
- astra.yaml: catalogue_neighbour_marking stays deleted; the blend and
  defect texts drop the converter.
- workflow/README.md: #933's tile_detection text plus #925's blend_handling.
- params_pin.json: #933's side, regenerated in a following commit.
- ngmix.py, test_ngmix_defect_fill.py: the masked-pixels tests built tile
  VIGNETs with the deleted converter; they now mark them as SExtractor does,
  with sextractor_script.cut_stamps, on the same DR6 patch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JQtu8oaZQEAqzbxZbPpWs
match_catalogue relabels every SEG_VIGNET label through the whole
SExtractor -> DR6 map, unpaired footprints to UNMATCHED_LABEL (-1). That
separates each row's own footprint from every other one only if the
catalogue's NUMBERs are unique and positive, so the join now refuses a
catalogue where they are not.

Tests, through sextractor_runner with SExtractor's outputs stubbed: on a
crowd where each paired row's DR6 NUMBER is another object's SExtractor
NUMBER (one of them the dropped detection's), each own footprint carries
the final NUMBER and nothing else does, the dropped footprint is -1, and
uberseg_weight and seg_has_neighbour see the stamps as they saw the
SExtractor-labelled ones. Sims (no join) keep the check image's stamps. A
negative control shows a partial relabel collides. make_cat's exclusion of
SEG_VIGNET moves here from the deleted converter tests. Swapping the join
before add_seg_vignet, relabelling only paired labels, or dropping the
NUMBER guard each fails a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JQtu8oaZQEAqzbxZbPpWs
Measured on DR6 tile 202.301 with sextractor_runner as tile_detect runs it
(post-processing off): peak RSS 1.53 GiB under noisefill, 2.93 GiB under
uberseg. add_seg_vignet reads the 400 MB check image and adds an int32
column the size of VIGNET, which the join holds twice. Scaled to #933's
1.84 GiB worst tile, uberseg needs ~3.5 GiB, above the 4000 MB noisefill
request; 7000 MB is 2x that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JQtu8oaZQEAqzbxZbPpWs
Regenerated with --update-params-pin in shapepipe-develop-240b37e4.sif
with host snakemake 9.23.1. The noisefill pin is #933's params_pin.json
byte for byte, so blend_handling: noisefill plans exactly #933's chain;
the uberseg default moves in tile_detect and tile_ngmix only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JQtu8oaZQEAqzbxZbPpWs
…test reads it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JQtu8oaZQEAqzbxZbPpWs
SExtractor 2.25.0 (src/analyse.c, ix=(int)(obj->mx+0.49999); the SIF's
Debian 2.25.0+ds-3 does not patch it) truncates the 0-based barycentre
plus 0.49999. np.rint differs at exact halves with an odd floor and for
fractional parts in (0.5, 0.50001); vignet_centre now mirrors SExtractor.

The tests' expected centres are written out by hand from that rule and
their stamps cut by plain slicing, not by cut_stamps; they cover exact
halves and fractions either side of 0.50001, and an object only the double
positions resolve. Reverting to np.rint fails five of them. On DR6 tile
202.301 no object falls where the two rules disagree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JQtu8oaZQEAqzbxZbPpWs
The guard took unique positive NUMBERs but not their type or range: a
non-integer, or a value past 2**31 - 1, narrows on the way into the int32
NUMBER and SEG_VIGNET columns (4294967295 becomes -1, UNMATCHED_LABEL). The
join now requires integers in [1, MAX_NUMBER]. Boundary tests cover
repeats, 0, negatives, 5.5, 5.0, 2**31, 2**32 - 1 and accept 2**31 - 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JQtu8oaZQEAqzbxZbPpWs
@cailmdaley cailmdaley changed the title UberSeg end to end: seg stamps in the tile catalogue (SEG_VIGNET) UberSeg end to end on #933's tile detection (SEG_VIGNET from the tile SExtractor run) Oct 2, 2026
@cailmdaley
cailmdaley changed the base branch from feat/masked-pixels to fix/dr6-windowed-positions October 2, 2026 01:39
Base automatically changed from fix/dr6-windowed-positions to develop October 2, 2026 14:41
The sextractor, match_catalogue, tile.smk, workflow README and params pin
conflicts are develop's squashed #933 against the same content here, so
they keep this branch's side: add_seg_vignet before match_catalogue, the
full-table relabel, the NUMBER guard and SExtractor's VIGNET centring.

ngmix: #927 replaces the PIXEL_SCALE option with each object's own
pixel scale from its epochs' Jacobians; develop's side is taken, and the
masked-pixels tests stop passing a pixel scale to Ngmix. #927's centroid
prior test sets the defect-fill attributes and epoch_cuts that process
reads on this branch.

The regenerated params pins are unchanged; the noisefill pin is
develop's params_pin.json byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JQtu8oaZQEAqzbxZbPpWs
cailmdaley added a commit to cailmdaley/shapepipe that referenced this pull request Oct 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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.

1 participant