Found while running the image-sim m-bias chain end-to-end (#225): the m-bias came back at m ≈ −1 (no shear response at all) even after the estimator itself was fixed. The cause is a calibration step that is correct for data and silently fatal for constant-shear simulations.
Mechanism (exact, not just empirical)
scripts/calibration/calibrate_comprehensive_cat.py builds the science shear via calibration.get_calibrated_m_c():
# get_calibrated_quantities: correct ensemble metacal
g_corr = np.linalg.inv(R).dot(g_uncorr) # R = ensemble 2x2 (shear + selection)
# get_calibrated_m_c: additive-bias removal — the problem for sims
c = np.mean(g_uncorr[comp]) # "additive bias" := mean uncalibrated shear
g_corr_mc = g_corr - np.linalg.inv(R).dot(c) # subtracted from every object
The multiplicative step is right. But the additive step subtracts each catalogue's own mean — and since ⟨g_corr⟩ = R⁻¹⟨g_uncorr⟩ = R⁻¹c, we get ⟨g_corr_mc⟩ = 0 exactly, for every sim. For real data this is the intended additive-systematics removal (mean cosmic shear ≈ 0, so the mean is PSF leakage / additive bias). For a constant-shear sim the mean is the injected signal, so the subtraction deletes exactly what the m-bias measures: each of 1p2z/1m2z/1z2p/1z2m ends up with ⟨e⟩ = 0 by construction, the response is a difference of zeros, and m = −1.
Evidence (grid_1, 1 tile, inverse-variance weighted, paired estimator)
| shear column |
response 1+m₁ |
response 1+m₂ |
e_uncal (raw) |
+0.84 |
+0.92 |
e = g_corr_mc (what the m-bias currently reads) |
+0.20 |
+0.02 ← signal gone |
ensemble R⁻¹ on paired e_uncal, no additive step |
+1.14 → m₁ = +0.14 ± 0.10 |
+1.27 → m₂ = +0.27 ± 0.11 |
The last row is a hand-diagnostic using only the per-object R_g** columns (it omits the selection-response part of the ensemble R, a few-% term), so it's the cross-check that the pipeline is otherwise healthy — not the final number.
Possible fixes — design call needed, since this touches the shared calibration path
- Sim-mode flag (my leaning): an
additive_correction: true/false knob on calibrate_comprehensive_cat.py, default true so the data path is provably unchanged; the image-sim mask config sets it false and the calibration writes g_corr (multiplicative-only) as e1/e2. Full ensemble R preserved; minimal diff.
- Always expose
g_corr as extra columns (e1_mult_only/e2_mult_only); the sim m-bias reads those. Non-invasive but adds columns for everyone.
- The m-bias estimator applies R itself from
e_uncal + R_g**. No shared-code change, but it can only apply the per-object shear response, not the global selection response — a good cross-check, not the final number.
Leaving the choice open — @-free ping via the PR thread. Note this finding is the image-sim gate working: it caught a real calibration/sim incompatibility before merge. Whether the data path's additive step is doing the right thing there is probably fine (mean shear ≈ 0 on data) but may deserve a look while we're here.
— Fable, on behalf of Cail
Found while running the image-sim m-bias chain end-to-end (#225): the m-bias came back at m ≈ −1 (no shear response at all) even after the estimator itself was fixed. The cause is a calibration step that is correct for data and silently fatal for constant-shear simulations.
Mechanism (exact, not just empirical)
scripts/calibration/calibrate_comprehensive_cat.pybuilds the science shear viacalibration.get_calibrated_m_c():The multiplicative step is right. But the additive step subtracts each catalogue's own mean — and since ⟨g_corr⟩ = R⁻¹⟨g_uncorr⟩ = R⁻¹c, we get ⟨g_corr_mc⟩ = 0 exactly, for every sim. For real data this is the intended additive-systematics removal (mean cosmic shear ≈ 0, so the mean is PSF leakage / additive bias). For a constant-shear sim the mean is the injected signal, so the subtraction deletes exactly what the m-bias measures: each of
1p2z/1m2z/1z2p/1z2mends up with ⟨e⟩ = 0 by construction, the response is a difference of zeros, and m = −1.Evidence (grid_1, 1 tile, inverse-variance weighted, paired estimator)
e_uncal(raw)e=g_corr_mc(what the m-bias currently reads)e_uncal, no additive stepThe last row is a hand-diagnostic using only the per-object
R_g**columns (it omits the selection-response part of the ensemble R, a few-% term), so it's the cross-check that the pipeline is otherwise healthy — not the final number.Possible fixes — design call needed, since this touches the shared calibration path
additive_correction: true/falseknob oncalibrate_comprehensive_cat.py, defaulttrueso the data path is provably unchanged; the image-sim mask config sets itfalseand the calibration writesg_corr(multiplicative-only) ase1/e2. Full ensemble R preserved; minimal diff.g_corras extra columns (e1_mult_only/e2_mult_only); the sim m-bias reads those. Non-invasive but adds columns for everyone.e_uncal+R_g**. No shared-code change, but it can only apply the per-object shear response, not the global selection response — a good cross-check, not the final number.Leaving the choice open — @-free ping via the PR thread. Note this finding is the image-sim gate working: it caught a real calibration/sim incompatibility before merge. Whether the data path's additive step is doing the right thing there is probably fine (mean shear ≈ 0 on data) but may deserve a look while we're here.
— Fable, on behalf of Cail