Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
28b0b92
test(ngmix): specify defect fill, epoch cuts and shear recovery near …
cailmdaley Sep 26, 2026
cd8b321
feat(ngmix): noise-fill defects under every BLEND_HANDLING, veto cent…
cailmdaley Sep 26, 2026
7777181
ngmix: read each epoch's OFFSET from the object dict in hand
cailmdaley Sep 26, 2026
9f0ea44
tests: name the none-mode uberseg test for none, not noisefill
cailmdaley Sep 26, 2026
a92a747
test(ngmix): specify DEFECT_FILL = interpolate and its shear recovery
cailmdaley Sep 26, 2026
1bb6437
feat(ngmix): DEFECT_FILL = interpolate, with a fill-dependent central…
cailmdaley Sep 26, 2026
ccbd195
Merge origin/develop into feat/defect-fill-veto
cailmdaley Sep 28, 2026
3e2c76f
Merge origin/feat/defect-fill-veto into feat/defect-interpolation
cailmdaley Sep 28, 2026
576f9c9
Merge origin/develop into rebuild/masked-pixels
cailmdaley Sep 29, 2026
b9d92a9
ngmix: restore the BLEND_HANDLING name noisefill
cailmdaley Sep 29, 2026
23cc5cb
test(ngmix): specify neighbour markers apart from defects
cailmdaley Sep 29, 2026
2517887
fix(ngmix): keep SExtractor's neighbour markers out of the defect set
cailmdaley Sep 29, 2026
7faa9e7
fix(ngmix): off-tile pixels are defects, not neighbours
cailmdaley Sep 29, 2026
f065405
fix(ngmix): interpolate defects from the pixels the image keeps
cailmdaley Sep 29, 2026
b27eca8
fix(ngmix): the defect fill keeps the stamp's dtype
cailmdaley Sep 29, 2026
dcec8c4
Merge remote-tracking branch 'origin/develop' into rebuild/masked-pixels
cailmdaley Sep 29, 2026
a5bfef1
fix(ngmix): off-tile pixels are the marked rows and columns at the st…
cailmdaley Sep 30, 2026
633425d
docs: -1e30 tile-VIGNET markers come from SExtractor or the catalogue…
cailmdaley Sep 30, 2026
d35af2e
Expand ${VAR:-default} in config values
cailmdaley Sep 29, 2026
540a220
Write SEG_VIGNET from the catalogue-mode converter
cailmdaley Sep 29, 2026
13f02c2
Write SEG_VIGNET from the tile SExtractor run
cailmdaley Sep 29, 2026
a4d6880
ngmix: read UberSeg's seg stamps from the tile catalogue's SEG_VIGNET
cailmdaley Sep 29, 2026
22cdd19
Carry blend_handling to tile_detect and ngmix as two prologue exports
cailmdaley Sep 29, 2026
0eb2cfb
make_cat: leave SEG_VIGNET out of final_cat, with VIGNET
cailmdaley Sep 29, 2026
f4e9515
astra.yaml: UberSeg reads the tile catalogue's SEG_VIGNET
cailmdaley Sep 29, 2026
d55abc8
Drop inherited blend variables before any rule
cailmdaley Sep 29, 2026
54807b6
Centre SExtractor-mode SEG_VIGNET on X/Y_IMAGE_DBL
cailmdaley Sep 29, 2026
60ae234
Point the stale SEG_VIGNET_PATH message and astra note at SEG_VIGNET
cailmdaley Sep 29, 2026
c704e63
Make UberSeg the default blend handling
cailmdaley Sep 30, 2026
cbec30f
Measure windowed centroids for UNIONS-catalogue tiles
cailmdaley Sep 30, 2026
da14676
Windowed centroid: mirror no-data pixels, guard NaN and wild FLUX_RADIUS
cailmdaley Sep 30, 2026
e167e0f
Revert the windowed-centroid port
cailmdaley Oct 1, 2026
4c25095
Merge origin/develop into fix/dr6-windowed-positions
cailmdaley Oct 1, 2026
a57e8ef
Config values expand ${VAR:-default}
cailmdaley Oct 1, 2026
cca72e0
sextractor_runner: join detections to an external catalogue
cailmdaley Oct 1, 2026
8274d99
Detect tiles with SExtractor on data too; drop the catalogue converter
cailmdaley Oct 1, 2026
f8b2e7b
Merge origin/develop into fix/dr6-windowed-positions
cailmdaley Oct 1, 2026
2a1fd49
sextractor_runner: single-image SExtractor without a detection image
cailmdaley Oct 1, 2026
7ac9884
match_catalogue: two-sided pairing guard; record the eight-tile valid…
cailmdaley Oct 1, 2026
e02f1f8
Config expansion rejects malformed ${ forms
cailmdaley Oct 1, 2026
c6725a7
tile_detect: 1 core, 4 GB, 20 min from eight measured tiles
cailmdaley Oct 1, 2026
5779c07
Update the params pin for this campaign boundary
cailmdaley Oct 1, 2026
6a8ee50
Tile SExtractor: MEMORY_PIXSTACK 3000000, so no object is truncated
cailmdaley Oct 1, 2026
5983208
match_catalogue: guard at 2% beyond 20 unpaired per side
cailmdaley Oct 1, 2026
976810c
Merge origin/feat/uberseg-seg-vignet (#925) onto #933's tile detection
cailmdaley Oct 1, 2026
2a058c0
SEG_VIGNET keeps own-vs-other through the DR6 join
cailmdaley Oct 1, 2026
45eef5d
tile_detect: 3000 MB more under uberseg for SEG_VIGNET
cailmdaley Oct 1, 2026
ed6a3db
Update the params pins for UberSeg on #933's detection
cailmdaley Oct 1, 2026
b152c76
tile.smk: keep the catalogue path on one line, as the tile-detection …
cailmdaley Oct 2, 2026
55af8dc
SEG_VIGNET centres as SExtractor centres VIGNET: (int)(mx + 0.49999)
cailmdaley Oct 2, 2026
3f5a263
match_catalogue: refuse NUMBERs the int32 columns cannot hold
cailmdaley Oct 2, 2026
a709bbc
ngmix: each chunk holds only its own rows' tile stamps
cailmdaley Oct 2, 2026
a45b70e
Merge origin/develop (#933 squashed, #927) into feat/uberseg-seg-vignet
cailmdaley Oct 2, 2026
b87b173
Merge origin/develop into feat/masked-pixels
cailmdaley Oct 2, 2026
74f5be3
Masked-pixel figures for #922: pixel classes on a synthetic and a rea…
cailmdaley Oct 2, 2026
b482d75
Defect gallery from real DR6 epochs; dr6_epochs.py scans an exposure …
cailmdaley Oct 2, 2026
575ed16
ngmix: one weight-symmetrization step, SYMMETRIZE_WEIGHTS names the c…
cailmdaley Oct 2, 2026
da73f3a
ngmix: interpolation is the only defect fill, and the veto reads the …
cailmdaley Oct 2, 2026
0226edf
CI: run tests/science once, in its own step
cailmdaley Oct 2, 2026
6c75fd4
ngmix: one-orientation defect interpolant; interpolated_defects names…
cailmdaley Oct 2, 2026
178ccf5
masked-pixel figures: one defect treatment, off-tile as zero weight
cailmdaley Oct 2, 2026
b82c77e
Bias-vs-distance figure for one defect: production treatment per defe…
cailmdaley Oct 3, 2026
3bd49d3
ngmix: epoch masked-fraction cut at 10%, matching DES
cailmdaley Oct 3, 2026
b0ce5af
bias_vs_distance: legend names the masked-fraction cut from its constant
cailmdaley Oct 3, 2026
8bc7946
ngmix: noise-fill central veto at 13 px, covering the 0.7" galaxy thr…
cailmdaley Oct 3, 2026
ef083a2
ngmix: DEFECT_WEIGHTING option; noise image interpolated in fixnoise'…
cailmdaley Oct 3, 2026
cce34ce
ngmix: per-object defect diagnostics in the shape catalogue
cailmdaley Oct 3, 2026
93c5472
Merge origin/develop into feat/masked-pixels
cailmdaley Oct 5, 2026
6ae3f9d
Plain-language defect_fill rationale; name DEFECT_WEIGHTING's default
cailmdaley Oct 5, 2026
4b73ecc
Merge origin/develop into feat/uberseg-seg-vignet
cailmdaley Oct 5, 2026
3ed6ebf
Merge feat/masked-pixels (#922) into feat/uberseg-seg-vignet
cailmdaley Oct 5, 2026
cbe95e2
Merge perf/ngmix-chunk-rows (#940) into feat/uberseg-seg-vignet
cailmdaley Oct 5, 2026
88ed8cd
des_y6: interpolate a quarter-turn copy only where the interpolant re…
cailmdaley Oct 5, 2026
7c0b563
Merge feat/masked-pixels (des_y6 copy fix) into feat/uberseg-seg-vignet
cailmdaley Oct 5, 2026
4c06ab5
ngmix: a failed epoch drops that epoch, not the object
cailmdaley Oct 5, 2026
f344fa6
Merge feat/masked-pixels (#922, failed-epoch drop) into feat/uberseg-…
cailmdaley Oct 5, 2026
6b9fa35
ngmix: neighbours are markers on another object's segmentation footprint
cailmdaley Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/deploy-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,18 @@ jobs:
docker run --rm -e HYPOTHESIS_PROFILE=ci "$IMAGE" \
pytest -rX -m "not slow" --no-cov tests/science

# The actual test suite, run inside the shipped image — replacing the
# The rest of the test suite, run inside the shipped image — replacing the
# retired conda-based suite. pytest exercises the same wheels, binaries,
# and Python (3.12) that production runs on, not a parallel environment.
# tests/science ran in the step above, so it is left out here.
# pyproject's addopts add `--cov=shapepipe`; COVERAGE_FILE is set to /tmp
# in the image so it works on read-only filesystems too. The Hypothesis
# profile is explicit here so CI always uses the deterministic, capped
# property-test profile even if the default changes for local exploration.
- name: Test — pytest suite
run: |
IMAGE=$(echo "${{ steps.meta.outputs.tags }}" | head -n1)
docker run --rm -e HYPOTHESIS_PROFILE=ci -e SHAPEPIPE_ON_CANDIDE=0 "$IMAGE" pytest -rX
docker run --rm -e HYPOTHESIS_PROFILE=ci -e SHAPEPIPE_ON_CANDIDE=0 "$IMAGE" pytest -rX --ignore=tests/science

# tests/workflow drives the Snakefile through snakemake's API. Snakemake
# is a host tool and stays out of the image (it wraps each job in the
Expand Down
362 changes: 253 additions & 109 deletions astra.yaml

Large diffs are not rendered by default.

221 changes: 221 additions & 0 deletions scripts/validation/masked_pixels/bias_vs_distance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
"""Shear bias from one detector defect against its distance from the object,
under the production defect treatment and under noise fill.

Production interpolates the defects ``interpolable_defects`` selects
(columns, 3-px bleeds, finite bleeds, single pixels) and noise-fills the
rest (4-column clusters, edge bands). The counterfactual noise-fills every defect, by making
``interpolated_defects`` select none. The recovery is the full-matrix
metacal measurement of ``tests/helpers/defect_response``; the figure plots
the worse axis of |m| and |c| with the veto radii and the bounds
|m| < 1%, |c| < 5e-4.

Run from the shapepipe checkout root inside the shapepipe container:
PYTHONPATH=src:. python scripts/validation/masked_pixels/bias_vs_distance.py \
scan OUT.json NPROC [KIND,KIND...]
PYTHONPATH=src:scripts/validation/masked_pixels \
python scripts/validation/masked_pixels/bias_vs_distance.py \
plot OUT.png IN.json [IN.json ...]
"""
import json
import sys

import numpy as np

N, CENTRE = 51, 25
DISTANCES = range(3, 17)
SEEDS = range(6)
GALAXIES = [(0.3, 0.7), (0.5, 0.7), (0.7, 0.9)] # (hlr, PSF FWHM) arcsec
INTERPOLATED = ("column", "bleed", "finite_bleed", "pixel")
KINDS = INTERPOLATED + ("cluster4", "edge")
BOUND_M, BOUND_C = 0.01, 5e-4


def geometry(kind, distance):
"""A defect whose nearest pixel is ``distance`` px from the stamp
centre (the geometries of tests/science/test_defect_recovery.py)."""
bad = np.zeros((N, N), dtype=bool)
near = CENTRE + distance
if kind == "pixel":
bad[CENTRE, near] = True
elif kind == "column":
bad[:, near] = True
elif kind == "bleed":
bad[:, near:near + 3] = True
elif kind == "finite_bleed":
bad[CENTRE - 5:CENTRE + 6, near:near + 3] = True
elif kind == "cluster4":
bad[:, near:near + 4] = True
elif kind == "edge":
bad[:, near:] = True
return bad


def one(job):
kind, fill, distance, hlr, psf = job
from shapepipe.modules.ngmix_package import ngmix as ngm
from tests.helpers.defect_response import defect_response

if fill == "noise":
ngm.interpolated_defects = (
lambda defect, neighbour, blend: np.zeros_like(defect))
row = dict(kind=kind, fill=fill, d=distance, hlr=hlr, psf=psf)
try:
r = defect_response(geometry(kind, distance), hlr=hlr, psf=psf,
seeds=SEEDS)
row.update(m=r["m"], m_err=r["m_err"], c=r["c"], c_err=r["c_err"])
except Exception as e: # noqa: BLE001
row["error"] = repr(e)
return row


def scan(out_path, nproc, kinds=KINDS):
from multiprocessing import get_context

jobs = [(k, f, d, h, p) for h, p in GALAXIES for d in DISTANCES
for k in kinds
for f in (("production", "noise") if k in INTERPOLATED
else ("production",))]
# spawn: each worker imports ngmix afresh, so the noise-fill patch
# cannot leak into a production job.
with get_context("spawn").Pool(nproc, maxtasksperchild=1) as pool:
rows = pool.map(one, jobs, chunksize=1)
with open(out_path, "w") as f:
json.dump(rows, f, indent=1)


def worst(row, q):
return max(abs(x) for x in row[q])


def smallest_passing(rows):
"""Smallest distance from which every larger distance passes both
bounds (None if the largest fails)."""
rows = sorted(rows, key=lambda r: r["d"])
passing = None
for r in reversed(rows):
if "error" in r or worst(r, "m") >= BOUND_M or worst(r, "c") >= BOUND_C:
break
passing = r["d"]
return passing


def plot(out_path, *in_paths):
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D

from figstyle import AQUA, BLUE, INK, INK2, ORANGE, VIOLET
from shapepipe.modules.ngmix_package.ngmix import (
EPOCH_CENTRAL_DEFECT_RADIUS as RN,
EPOCH_INTERPOLATED_DEFECT_RADIUS as RI,
EPOCH_MASKED_FRACTION_CUT as FRAC_CUT,
)

rows = [r for path in in_paths for r in json.load(open(path))]
grid, fail, bad_ink = "#e4e3df", "#f6e3dc", "#a8462a"
GOLD = "#c49a1c"
series = {
"column": ("bad column", BLUE),
"bleed": ("3-px bleed", ORANGE),
"finite_bleed": ("finite 3-px bleed (11 rows)", VIOLET),
"pixel": ("single pixel", AQUA),
"edge": ("edge band (noise-filled)", INK),
"cluster4": ("4-column cluster (noise-filled)", GOLD),
}
plt.rcParams.update({"font.size": 10.5, "axes.edgecolor": INK2,
"xtick.color": INK2, "ytick.color": INK2,
"axes.labelcolor": INK})
fig, axs = plt.subplots(
2, len(GALAXIES), figsize=(13, 7.4), dpi=150, sharex=True,
sharey="row", gridspec_kw=dict(hspace=0.1, wspace=0.05))
for j, (hlr, psf) in enumerate(GALAXIES):
for i, (q, bound, scale, ylabel) in enumerate([
("m", BOUND_M, 100, "worse-axis |m| [%]"),
("c", BOUND_C, 1, "worse-axis |c|")]):
ax = axs[i, j]
b = bound * scale
ax.axhspan(b, 1e4, color=fail, zorder=0, lw=0)
ax.axhline(b, color=bad_ink, lw=0.9, zorder=1)
for r, txt, ha, dx in (
(RI, f"interpolated\nveto {RI:g} px", "right", -0.2),
(RN, f"noise-fill\nveto {RN:g} px", "left", 0.2)):
ax.axvline(r, color=INK2, ls=":", lw=1.3, zorder=1)
if i == 0:
ax.text(r + dx, 8e2, txt, color=INK2, fontsize=8.5,
va="top", ha=ha, linespacing=1.1)
for kind, (label, col) in series.items():
for fill, ls, lw, alpha in (("production", "-", 2.0, 1.0),
("noise", (0, (3, 2)), 1.4, 0.8)):
sel = sorted(
(r for r in rows if r["kind"] == kind
and r["fill"] == fill and r["hlr"] == hlr
and r["psf"] == psf and "error" not in r),
key=lambda r: r["d"])
if not sel:
continue
d = np.array([r["d"] for r in sel])
v = np.array([worst(r, q) * scale for r in sel])
z = 3 if fill == "production" else 2
ax.plot(d, v, color=col, ls=ls, lw=lw, alpha=alpha,
zorder=z)
if fill != "production":
continue
cut = np.array([geometry(kind, x).mean() > FRAC_CUT
for x in d])
ax.plot(d[~cut], v[~cut], "o", color=col, ms=3.5,
zorder=z)
ax.plot(d[cut], v[cut], "o", mfc="white", mec=col,
mew=1.1, ms=4, zorder=z)
ax.set_yscale("log")
ax.set_ylim((1e-3, 1e3) if q == "m" else (1e-6, 0.5))
ax.grid(axis="y", color=grid, lw=0.6, which="major")
for s in ("top", "right"):
ax.spines[s].set_visible(False)
ax.set_xticks(range(4, 17, 2))
if j == 0:
ax.set_ylabel(ylabel)
ax.text(16.5, b * 1.3, "|m| > 1%" if q == "m" else
"|c| > 5e-4", color=bad_ink, fontsize=9,
ha="right", va="bottom")
if i == 0:
ax.set_title(f'{hlr}″ galaxy, {psf}″ PSF', color=INK,
fontsize=11, loc="left")
fig.supxlabel("distance of the defect's nearest pixel from the object "
"centre [px]", fontsize=10.5, color=INK, y=0.045)
fig.suptitle("Shear bias from one defect: production treatment (solid) "
"vs noise fill (dashed); 6 seeds, worse of axes 1, 2",
color=INK2, fontsize=10, y=0.965)
handles = [Line2D([], [], color=c, lw=2, marker="o", ms=3.5, label=l)
for l, c in series.values()]
handles += [
Line2D([], [], color=INK2, lw=2, marker="o", ms=3.5,
label="production (narrow defects interpolated)"),
Line2D([], [], color=INK2, lw=1.4, ls=(0, (3, 2)),
label="noise fill (not used for narrow defects)"),
Line2D([], [], color=INK2, lw=0, marker="o", mfc="white", mec=INK2,
ms=4, label=f"epoch dropped by the {FRAC_CUT:.0%} masked-fraction cut"),
]
fig.legend(handles=handles, frameon=False, fontsize=9.5, ncol=3,
loc="lower center", bbox_to_anchor=(0.5, -0.075))
fig.savefig(out_path, bbox_inches="tight")


if __name__ == "__main__":
if sys.argv[1] == "scan":
scan(sys.argv[2], int(sys.argv[3]),
*([tuple(sys.argv[4].split(","))] if len(sys.argv) > 4 else []))
elif sys.argv[1] == "plot":
plot(sys.argv[2], *sys.argv[3:])
elif sys.argv[1] == "summary":
rows = [r for path in sys.argv[2:] for r in json.load(open(path))]
for hlr, psf in GALAXIES:
for kind in KINDS:
for fill in ("production", "noise"):
sel = [r for r in rows if r["kind"] == kind
and r["fill"] == fill and r["hlr"] == hlr
and r["psf"] == psf]
if sel:
print(f"{hlr}/{psf} {kind:13s} {fill:10s} "
f"passes from {smallest_passing(sel)} px")
75 changes: 75 additions & 0 deletions scripts/validation/masked_pixels/defect_gallery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""One real DR6 epoch per defect type: the raw epoch stamp, then the image
and weights ngmix receives (BLEND_HANDLING = noisefill, the default).

Each epoch is an npz written by ``dr6_epochs.py cut``. Run from the
shapepipe checkout root inside the shapepipe container:
PYTHONPATH=src:scripts/validation/masked_pixels \
python scripts/validation/masked_pixels/defect_gallery.py OUT.png \
"bad column=ep_4202.npz" "3-column cluster=ep_28647.npz" ...
"""
import sys

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.lines import Line2D

from figstyle import (AQUA, BLUE, INK, ORANGE, WEIGHT_LEGEND, clean, outline,
show_image, show_weight)
from shapepipe.modules.ngmix_package.ngmix import (
defect_mask,
interpolated_defects,
prepare_ngmix_weights,
)

COLS = ["raw stamp", "image ngmix sees", "weight"]


def main(out_path, rows):
plt.rcParams.update({"font.size": 10})
fig, axs = plt.subplots(len(rows), 3, figsize=(7.4, 2.35 * len(rows)),
dpi=150,
gridspec_kw=dict(hspace=0.06, wspace=0.05))
for i, (kind, path) in enumerate(rows):
ep = dict(np.load(path, allow_pickle=True))
rms = float(np.median(ep["bkg_rms"]))
stretch = dict(vmax=120.0 * rms, soft=3.0 * rms)
defect = defect_mask(ep["weight"], ep["flag"], ep["bkg_rms"])
interp = interpolated_defects(defect, ep["neighbour"], "noisefill")
img, w, _ = prepare_ngmix_weights(
ep["gal"], ep["weight"], ep["flag"], np.random.RandomState(1),
bkg_rms=ep["bkg_rms"], neighbour=ep["neighbour"],
)
show_image(axs[i, 0], ep["gal"], **stretch)
outline(axs[i, 0], ep["neighbour"], BLUE, lw=0.9)
outline(axs[i, 0], defect, INK, lw=0.9)
show_image(axs[i, 1], img, **stretch)
outline(axs[i, 1], interp, AQUA, lw=0.9)
outline(axs[i, 1], defect & ~interp, ORANGE, lw=0.9)
show_weight(axs[i, 2], w, interp, defect | ep["neighbour"])
axs[i, 0].set_ylabel(
f"{kind}\n{ep['label']}\n{ep['source']}", color=INK, fontsize=8,
linespacing=1.15,
)
for j, t in enumerate(COLS):
axs[0, j].set_title(t, color=INK, fontsize=10)
for ax in axs.flat:
clean(ax)

handles = [
Line2D([], [], color=INK, lw=1.6, label="defect"),
Line2D([], [], color=BLUE, lw=1.6,
label="neighbour (noise-filled, zero weight)"),
Line2D([], [], color=AQUA, lw=1.6, label="defect, interpolated"),
Line2D([], [], color=ORANGE, lw=1.6, label="defect, noise-filled"),
] + WEIGHT_LEGEND
fig.legend(handles=handles, loc="upper center", ncol=2, frameon=False,
fontsize=8.5, handlelength=1.4,
bbox_to_anchor=(0.5, axs[-1, 0].get_position().y0 - 0.005))
fig.savefig(out_path, bbox_inches="tight")


if __name__ == "__main__":
main(sys.argv[1], [a.split("=", 1) for a in sys.argv[2:]])
Loading
Loading