Skip to content

Consistent shear response, weights and additive bias (DES case d) - #425

Open
cailmdaley wants to merge 36 commits into
developfrom
feat/consistent-response-weights
Open

cailmdaley wants to merge 36 commits into
developfrom
feat/consistent-response-weights

Conversation

@cailmdaley

@cailmdaley cailmdaley commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Goal: one definition of the galaxy weights, the metacal shear response R = R_shear + R_sel, the image-sim residual multiplicative bias m and the additive bias c, used end to end on data and image sims. One knob, metacal.weight_scheme: des | iv, sets all four.

Findings:

  • The response follows Martin's DES-weight definition ("case d"). With his settings, the code reproduces his R, weight table and paired m on all 20 sim products to 10⁻¹⁴.
  • Paired m is inconsistent with that response. m is now measured on full populations, which lowers it by ~1.5 percentage points (pp).
  • A weight table fitted on the galaxies it weights biases m low. The sims now use a fixed table fitted on v2 data.
  • The production residual is m = (−9.2 ± 3.2, −8.9 ± 3.6) × 10⁻³, about −1%. It is concentrated at low SNR and not explained.
    • Its shear-response part is (−15.8, −17.8) × 10⁻³. The selection part offsets it by (+6.6, +8.9) × 10⁻³.
    • Martin's check, with no SNR/size cuts and no weights (so R_sel ≡ 0), gives m = (−32 ± 4, −22 ± 4) × 10⁻³. That is not zero.
    • No fiducial m is adopted.
  • On v2 data, R rises by 10.6% (0.643 → 0.712), almost entirely from the weighting.

Figures are hosted on the orphan branch pr-assets/425, outside the PR's history. Definitions: docs/source/response_calibration.md. Reproduction: scripts/validation/response/README.md.

Terms

Term Meaning
Metacal branches Each galaxy is remeasured after an artificial shear ±h (h = 0.01) along component j = 1, 2, giving branches b ∈ {ns, 1+, 1−, 2+, 2−}; ns is the unsheared (no-shear) measurement, and e_b is the ellipticity measured on branch b.
R_shear (⟨e_{j+}⟩ − ⟨e_{j−}⟩) / 2h on the no-shear selection and weights: how the measured shape responds to shear. R = R_shear + R_sel calibrates g = R⁻¹e.
Branch mean ⟨x⟩_b = Σ w_b x / Σ w_b, over metacal branch b's own selection S_b and with its own weights w_b. Each branch has its own denominator, so R is the derivative of the same weighted ratio it calibrates.
Split R_sel (applied) (⟨e_ns⟩{j+} − ⟨e_ns⟩{j−}) / 2h. The no-shear ellipticity is averaged with each sheared branch's selection and weights. This captures cut crossings, weight changes and cell migration.
Joint R (diagnostic) (⟨e_{j+}⟩{j+} − ⟨e{j−}⟩_{j−}) / 2h, using each branch's own ellipticity.
Case d Fit the DES weight table W(SNR, T/T_psf) once on the no-shear branch, then evaluate it at each branch's own SNR and size. This is Martin's wdes_response.py.
IV weights 1/(2σ_ε² + σ₁² + σ₂²): σ_ε is the per-component intrinsic shape dispersion, σ₁, σ₂ each branch's own measured G_ERR.
c c_e = ⟨e⟩_ns raw, c_g = R⁻¹c_e calibrated, and g = R⁻¹e − c_g.
Paired m Match the +g and −g sim objects that pass the cuts in both, and give each pair the weight ½(w₊ + w₋). This was Martin's estimator.
Full-population m Each external-shear catalogue keeps its own selection, weights and R. This is now the production estimator.
The gap Paired m minus full-population m.
Self-weighting A table fitted on the galaxies it weights correlates W with their shapes. In a noiseless toy, this gives m = −1/N_cell; an independent table gives 0.
N_min Minimum number of no-shear training objects per DES cell. A cell below 3 pools moments over the smallest surrounding square of cells that reaches 3. Denser cells are unchanged.
Shared-tile jackknife Delete-one-tile errors. Each deletion removes the same tile from both external-shear catalogues and recomputes R and the numerator.
Refit-W errors For a self-fitted table, each deletion also refits W, so the errors carry the table's noise. An independent table stays fixed.

What the PR contains

  • Library: branch-normalised R, case-d DES and exact per-branch IV weights, and c in explicit units.
    • The producer and the cosmo_val consumer read the same components.
    • Products record their weight scheme and applied R in their headers. Readers refuse a mismatched weight or a second R.
  • Image-sim m:
    • Full populations only. Modern products refuse pair_match: true.
    • The DES table is a named, independent product, held fixed across branches and deletions. des_weight_source: self remains as a diagnostic.
    • Errors use the shared-tile jackknife, with full (m1, c1, m2, c2) covariance.
  • Tests for each rule, including the self-weighting toy. Fast suite: 586 passed, 0 failed.
  • Martin's image-sims workflow is also merged here: the workflow/bin/spv launcher, workflow/scripts/status_sims.py, the image-sims Snakefile/config, and his Global shear response is IV-weighted while shear statistics are w_des-weighted #395 reference scripts in scripts/validation/wdes_response/.

Results

All four unblinded grids are combined as inverse-variance means, assuming the grids are independent. m is given as (m1, m2) × 10⁻³.

Estimator

Combined-grid m for each estimator

  • The gap, ~1.5 pp.
    • Paired m combines to (−4, 0). With the same self-fitted table, full populations give (−19.3 ± 4.0, −14.3 ± 4.7).
    • Symmetric pair weights drop the weight change from the numerator, but case-d R keeps it.
    • Matched objects with separate branch weights already show most of the gap, so it comes mostly from the weights; restricting the population contributes less.
  • Self-weighting, ~0.5–1 pp.
    • Training on the other three grids gives (−11.2 ± 3.3, −8.1 ± 3.9).
    • The v2 data table, which is the production rule, gives (−9.2 ± 3.2, −8.9 ± 3.6).
    • Paired m (self-fitted table) minus full-population m with the other-grids table is ~0.7 pp. That's a cross-table difference, not the gap measured with one independent table.
  • Exact IV: (−16.1 ± 4.1, −17.0 ± 4.7).
    • No fitted table, so self-weighting can't explain it. Freezing the no-shear G_ERR would shift m by +0.2 to +0.4 pp. Modern IV therefore refuses inputs that lack the sheared G_ERR columns.
  • N_min = 3. Without it, one two-object cell inflated a grid's σ(m) fivefold. Errors plateau over N_min = 3–10; m moves ≤ 0.21σ.

m split into shear-response and selection parts

m_shear-only calibrates with R_shear alone. δm_sel is the change from adding R_sel, at fixed numerator and population, so m = m_shear-only + δm_sel. Grids are combined with the total-m weights so the split stays additive.

Combined-grid m split into shear-response-only and selection contributions

Configuration m_shear-only δm_sel m
No SNR/size cuts, unit weights (−32.1 ± 3.8, −21.7 ± 3.6) 0 (−32.1 ± 3.8, −21.7 ± 3.6)
SNR/size cuts, unit weights (+12.8 ± 4.2, +14.3 ± 4.1) (−29.4 ± 4.4, −31.5 ± 4.9) (−16.6 ± 4.3, −17.2 ± 4.9)
SNR/size cuts, DES (production) (−15.8 ± 3.1, −17.8 ± 3.2) (+6.6 ± 3.2, +8.9 ± 3.8) (−9.2 ± 3.2, −8.9 ± 3.6)

Martin's check (top row). Every retained object passes all five branch masks, so R_sel is exactly zero.

  • What it shows: m is still 8σ and 6σ from zero, so R_shear alone does not calibrate these sims.
  • What it doesn't show: it does not place the bias in shape measurement. The parent cuts (FLAGS ≤ 2, N_EPOCH ≥ 2, 15 ≤ mag ≤ 30, ngmix failure flags) and detection/blending still act, and none of them enters R.

Under production cuts and weights, the negative residual sits in the shear-response part, and the selection part partly cancels it. Changing cuts or weights also changes the numerator, so comparing rows is not a physical split into measurement bias and selection bias.

Open: a −1% residual, concentrated at low SNR

Combined-grid production m in SNR terciles

The terciles are set by the v2 data's SNR distribution.

  • By tercile: m1 is (−39 ± 25, −24 ± 11, −2 ± 4), and m2 is similar. High minus low is 1.3–1.6σ.
  • Occupancy: the sims hold ~30% fewer galaxies than the data at SNR 10–15, and the per-galaxy response climbs from ≈0.4 to ≈0.8 over SNR 10–30. Reweighting the sims to the data's SNR × size occupancy gives (−11.9 ± 3.5, −10.4 ± 4.2).
  • Response at fixed SNR: data galaxies below SNR 40 also respond less than sim galaxies at the same SNR, as if noisier than nominal (inferred). If so, the sims understate the low-SNR bias.
  • Not tested: detection and blending, and the non-metacal parent cuts.

A release's m must be measured with that release's own table.

v2 data: what catalogue users feel

Global calibration of v2 data at d604206, with #424 (halo masks dropped):

old this PR
R_shear (diag) 0.641, 0.643 0.734, 0.735
R_sel (diag) +0.002, +0.002 −0.022, −0.022
R total (diag) 0.643, 0.645 0.711, 0.713
c_e (×10⁻⁴) (−0.6, +0.7) (−0.1, +1.8) ± 0.3
  • Old calibration: an IV-weighted R_shear (unsquared errors) combined with an unweighted R_sel.
  • Source of the shift: weighting accounts for 99.6% of it; Default external-mask cut excludes the star halos #424 leaves R unchanged.
  • Mechanism: DES weights favour high-response galaxies. R_shear is 0.602 unweighted, 0.610 with IV², and 0.734 with DES.
  • Effect: calibrated shear falls by 9.6% (1/1.106), and ξ by ~18%.

Follow-ups

Closes #395, Closes #156, Closes #21, Closes #22. Refs #23, #346, #382.

Claude (Opus) on behalf of Cail

🤖 Generated with Claude Code

cailmdaley and others added 26 commits August 31, 2026 03:03
The ShapePipe pipeline stage's ngmix ini (which owns METACAL_PSF) is
resolved from the shapepipe worktree's example/cfis_image_sims via
run_job's self-computed config_dir, so nothing in the run config could
select a different ini -- an A/B on the metacal reconvolution PSF needed
a separate worktree per arm.

Add an optional operational key `pipeline_config_dir`. When set it is
passed to run_job as `-c DIR`, redirecting the pipeline stage's whole
ShapePipe config tree (ngmix ini included) at a per-arm copy; when empty
(the default) no `-c` is passed and run_job self-computes exactly as
before, so runs predating this key reproduce bit-exact. Merge/final_cat
still read the top-level cfis symlink (config_dir), untouched.

Candidate upstream PR for the image-sims orchestration branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSWPiisH6y7NFFbjHqthuC
ImageSimMBias.load_catalogs hardcoded the '_grid_' infix between branch
name and run number, so blended arms ({branch}_{num} dirs) hit
FileNotFoundError on a '{branch}_grid_{num}' path that never existed.
Derive the suffix from sims_type (defaulting to grid for backward
compat and the synthetic-recovery tests), mirroring the workflow's own
_SUFFIX contract. Emit sims_type into the generated m_bias_config, and
bump im_mbias mem to 8G since blended catalogues are ~5x grid size and
OOM the snakemake 1000M default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HubFhoU6bH8ij6hcTWxw7s
…lization

Add an optional `exp_num` config key so the exposure input can be pulled from a
different SKiLLS realization than the tiles. Tiles stay on `{branch}_{num}`;
exposures resolve `{branch}_{exp_num}` via a new SIM_EXP_BASE mapping, consumed
by rule im_init's `sim_exp` symlink source. Unset `exp_num` defaults to `num`,
reproducing prior behaviour bit-for-bit. Guarded to non-grid sims (grid names
embed sims_type; the pathology this addresses is a blended-family concern).

Motivation: the blended-family realizations _1.._4 carry stale cross-linked
SP_exp (grid-sims exposures symlinked in, signal-free); _5 is the clean all-real
exposure set. This reproduces Fabian's production split (exposures from _5, tiles
from _1), unblocking the blended smoke arms whose ngmix flux collapsed ~10x on
the contaminated z-branch exposures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HubFhoU6bH8ij6hcTWxw7s
…flow

im_pipeline shelled out per tile to the legacy bash runner
(run_job_sp_canfar_v2.0.bash -t image_sims) over example/cfis_image_sims,
configs frozen before the real-data chain gained background-RMS vignets,
PIXEL_SCALE, chunked ngmix and the new flags. An m measured that way does
not calibrate the pipeline that makes the real catalogue.

im_shapepipe replaces it: one localrule per shear branch writes a ShapePipe
run config (input_type: image_sims, psf_model, tile list, the branch's
SP_tiles/SP_exp, run_dir = grids_base/<sim>) and runs `sp run`, whose jobs
ShapePipe submits through its own profile. The output is a campaign record
(ShapePipe commit, resolved image, final_cat count) that im_merge consumes.
im_init no longer stages input_tiles/input_exp; its cfis link now points at
the checkout's workflow/config/cfis_image_sims, and it writes tiles_<sim>.txt,
which is both the campaign's tile_list and params.py's tile-ID file.

Config: drop config_dir, pipeline_config_dir and n_smp (dead with the bash
runner; an A/B of the ngmix ini is now a ShapePipe config change); add
sp_profile, sp_jobs, clean_exposures; psf_model defaults to fake, the true
PSF of the star-free grid sims.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSjZrhgsGCLAJCLb5Zjmc3
…iles

im_shapepipe recorded the ShapePipe commit and dirty state by asking the
live checkout when the campaign ENDED -- hours after sp run snapshotted the
code it actually ran, so an edit or commit in between put the wrong code in
the record. It now copies sp's own launch snapshot
(<run_dir>-state/code/snapshot.json: source, branch, head, dirty, taken_at).

im_mbias's provenance adds those per-branch records (commit, image, tile and
final_cat counts) as shapepipe_campaigns, beside the checkout the merge
stage reads; the records are now declared inputs.

config.yaml: tile_ids must leave out tiles without data. A tile with no
detections fails its ShapePipe campaign and so im_shapepipe; the SKiLLS
coadds of grid_1 248.288, grid_2 259.285, grid_3 265.285 and grid_4
281.281/282.281 are all zeros, and 1m2z_2's 257.289 is truncated. The
psf_model comment no longer offers *_star_grid (inputs for 1z2z only).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VSU9pPZjpBjzWorAL88aKQ
…r run

null (the default) keeps the profile's node-local store and leaves the
key out of the branch run configs, so earlier campaigns keep their
params. The star sims set it to NFS: candide's 31 GB node /tmp cannot
hold their ~5 GB tile stores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qv5MwRiVKa8wnH665dWBia
…l's real-data workflow

A from-scratch, one-tile-scale port of the image-sims chain onto the
per-unit manifest/completeness pattern from shapepipe/workflow/ (calling
shapepipe_run directly, no run_job_sp_canfar_v2.0.bash), built and verified
step by step (prepare -> exposure -> tile chain -> tile_ngmix ->
tile_make_cat) against the 240.291 reference tile from grid_1, plus
clean_exposure/clean_tile reclamation reusing shapepipe's own scripts
unmodified.

Kept separate from feat/imsims-shapepipe-workflow (the more mature,
overlay-config-based port already in progress there) -- this is an
independent, simpler exploration, not meant to replace it.
run.sh's mktemp targeted /tmp directly -- a small node-local disk shared by
every job/user on the node (candide policy), and this runs on the login
node besides. Use a gitignored .batch/ dir alongside the workflow instead.
…atch

Node-local /scratch (the previous fix's instinct, per CLAUDE.md's per-job
scratch pattern) doesn't actually work here: verified empirically that a
login-node /scratch path is invisible from the compute node the job lands
on, so both the job's own stdout file and a file it tried to write there
silently never appeared. This script runs before the job exists (login
node) and its log is written from a not-yet-chosen compute node, so it
needs network storage -- CLAUDE.md's own fallback for anything that must
be visible beyond one node -- not /scratch, not /tmp, and not the
repository directory either. Moved to /n17data/$USER/.im_sims_batch/.
TIME[compute]=02:00:00 was sized for the original single reference-tile
run (~80 min end to end). COMPUTE runs the whole tile list in one
--cores 8 allocation, so its wall clock has to cover the campaign, not
one tile: at 40 tiles (1z2z_grid_1) the 2h cap timed out at 40/376 steps
with no ngmix job finished (candide job 867554). 47h leaves margin under
comp/pscomp's 48h MaxTime.
The 47h compute job hung on n23 with zero CPU for ~30 min after
tile_merge_headers; shapepipe's candide profile already excludes n23
for past job failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NzWDtdQbsK1VhvYbDnmTo
…o /tmp

- scripts/run_config_sims.py: $name in any run-config value expands to the
  top-level scalar key `name` (my_base_dir, sim, ...), repeatedly; an
  unexpanded $name is an error rather than a literal directory. Used by the
  Snakefile and by status_sims.py.
- config.yaml: container, psf_dict and shapepipe_repo become shared defaults,
  so a run config need not repeat them (it may still override them).
- status_sims.py: optional -c/--config reads outputs.run_dir and
  outputs.products_dir from the run config; final_cat is counted on the
  products root, not under run_dir.
- run.sh: TMPDIR is /n23data1/mkilbing/tmp, passed to snakemake
  (--default-resources tmpdir=) and into the container (--env TMPDIR, plus a
  bind), since --cleanenv drops the host value. Nothing writes to the node's
  /tmp any more, per ~/CLAUDE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NzWDtdQbsK1VhvYbDnmTo
Brings the image-sims m-bias changes of imsims_workflow onto develop:
pipeline_config_dir, sims_type naming, exp_num, ShapePipe processing through
ShapePipe's own workflow (sp), campaign provenance, tile_store_root, and the
candide profile's latency-wait 60 / job cap. The native shapepipe_run
prototype (workflow/image_sims/native) is left out: ShapePipe's unified
workflow superseded it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e's sp

- spv run -c RUN_CONFIG [-t TARGET] [snakemake args]: the main config
  (workflow/image_sims/config.yaml) plus the user's run config, default
  target im_mbias. The code is snapshotted into <grids_base>/spv-state/code
  and the run executes out of it (image_sims.sp_validation_repo is set to the
  snapshot), so editing the checkout cannot reach running jobs.
- On candide TMPDIR/APPTAINER_TMPDIR must be set outside /tmp and /scratch
  (the slurm executor exports the head's TMPDIR to every job, and Slurm
  falls back to the node's /tmp when it cannot be created); the snapshot's
  profile passes the same directory into the container.
- spv status -c sp_*.yaml | DIR: disk-scan status of ShapePipe campaigns,
  workflow/scripts/status_sims.py (from the native prototype's script, which
  already read the unified layout; now with its own $name/${name} reader).
- The image-sims Snakefile loads its config next to itself, not relative to
  the working directory; im_mbias_config.py reads repo provenance from a
  snapshot's snapshot.json when there is no .git.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The state the grid m-bias runs actually used (sp_validation_imsims_workflow
working tree, 2026-10-02), so that spv runs it: the image_sims.smk chain on
top of ShapePipe's own workflow (init -> extract -> calibrate -> m-bias), the
matching workflow/image_sims/config.yaml, the per-tile response jackknife and
related calibration/m-bias changes with their tests, and the mask configs
mask_v1.X.10_im_sim{,_R_now}.yaml. Tests: test_calibration.py and
test_image_sims.py pass (20) in the m-bias image.

Left out: the profile's `config: image_sims={"sif": ...}` -- the image is a
per-user choice and goes in the run config (image_sims.sif). Added: n35 to
the candide excludes (its /tmp was filled on 2026-09-30).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
config_mask.yaml was a symlink into the sp_validation checkout the run came
from. Removing that checkout left every run dir's link dangling, and the next
launch failed at DAG time ("config_mask.yaml seems to be a broken symlink",
evaluating im_calibrate's mem_mb from its input sizes). With spv the source
is the code snapshot, which each launch refreshes. A copy keeps the mask the
run dir was calibrated with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
n34 hit the per-user thread limit during the image-sims runs, as did n35.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge of the uncommitted imsims_workflow work (b268fd6) dropped exp_num
from the recognised keys while EXP_NUM still reads it and config.yaml still
documents it for the star sims (tiles from *_N, exposures from *_5), so a
star-sim run config naming it died at parse as an unknown key.

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

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

✅ ruff is clean — nothing to fix here.

cailmdaley and others added 7 commits October 26, 2026 20:29
Science files keep this branch's versions (calibration.py,
calibrate_comprehensive_cat.py, image_sims.py, compute_m_bias_image_sims.py
and their tests): the case-d response and shared-tile jackknife supersede
spv-launcher's response_jackknife, R_joint and R_selection_std.
Re-applied from spv-launcher: sims_type picks the sim-dir suffix in
ImageSimMBias.load_catalogs (blended sims are {branch}_{num}).

Kept from spv-launcher: workflow/bin/spv, status_sims.py, exp_num,
tile_store_root, campaign caps, candide profile, snapshot provenance,
mask_v1.X.10_im_sim configs. im_init stages the mask config with
stage_image_sim_config.py (a copy, never a link).

scripts/validation/wdes_response/: the #395 scripts that define case d,
from /n23data1/mkilbing/imsim_wdes_response; docs now point there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each weighted mean is divided by its own weight sum over its own
selection, so R is the response of the ratio <e>_w that it calibrates,
including the normalisation term of its derivative.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cailmdaley
cailmdaley force-pushed the feat/consistent-response-weights branch from 3912954 to 8473903 Compare October 26, 2026 19:41
@cailmdaley
cailmdaley marked this pull request as ready for review October 26, 2026 19:41

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

1 participant