Skip to content

Commit 0ee0e94

Browse files
cailmdaleyclaude
andcommitted
feat(ngmix): make the Jacobian centroid source configurable (hsm | wcs)
The galaxy Jacobian origin sets where the centroid prior sits, so it must land on the object. Two ways to place it, now selectable per run: - `hsm` (DEFAULT, unchanged behavior): re-center on the HSM adaptive-moment centroid measured from the stamp. Robust for galaxies — it follows the light, so the centroid prior does not bias an object offset from the stamp center. - `wcs` (Fabian's): place the origin at the catalog sky position projected through the WCS to a sub-pixel offset, with no shape measurement. Better for stars, whose HSM moments are too noisy to re-center on. Wiring: - `make_ngmix_observation` gains `centroid_source` (+ `wcs_full`/`ra`/`dec` for the wcs branch); the hsm branch is the prior code verbatim. - `do_ngmix_metacal`, `Ngmix.__init__`, and `Ngmix.process` thread it through; `Postage_stamp` carries the per-epoch WCS + ra/dec, filled in `prepare_postage_stamps` (used only by the wcs branch). - `ngmix_runner` reads the optional `CENTROID_SOURCE` ini key (default `hsm`); documented in `example/cfis/config_tile_Ng_template.ini`. The star-response guardrail declares `CENTROID_SOURCE = wcs` and threads it into the regeneration chain (`SP_CENTROID_SOURCE`), so the star grid is built with the star-appropriate centroid. Default path verified unchanged: 26 ngmix/science tests green; the wcs branch reproduces the WCS-projected sub-pixel offset on a synthetic exposure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 71f217b commit 0ee0e94

5 files changed

Lines changed: 120 additions & 19 deletions

File tree

example/cfis/config_tile_Ng_template.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,11 @@ MAG_ZP = 30.0
7676
# Pixel scale in arcsec
7777
PIXEL_SCALE = 0.186
7878

79+
# CENTROID_SOURCE (optional): how to place the galaxy Jacobian origin for the
80+
# centroid prior. "hsm" (default) re-centers on the HSM adaptive-moment
81+
# centroid — robust for galaxies. "wcs" uses the catalog sky position
82+
# projected through the WCS — better for stars, whose HSM moments are noisy.
83+
CENTROID_SOURCE = hsm
84+
7985
ID_OBJ_MIN = X
8086
ID_OBJ_MAX = X

src/shapepipe/modules/ngmix_package/ngmix.py

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ def __init__(
151151
self.flags = []
152152
self.bkg_rms = []
153153
self.jacobs = []
154+
# Per-epoch full WCS and the object's sky position, used only by the
155+
# "wcs" centroid source (skipped for the default "hsm" path).
156+
self.wcs = []
157+
self.ra = []
158+
self.dec = []
154159
self.bkg_sub = bkg_sub
155160
self.megacam_flip = megacam_flip
156161

@@ -232,6 +237,12 @@ class Ngmix(object):
232237
id_obj_max : int, optional
233238
Last galaxy ID to process, not used if the value is set to ``-1``;
234239
the default is ``-1``
240+
centroid_source : {"hsm", "wcs"}, optional
241+
How to place the galaxy Jacobian origin for the centroid prior. The
242+
default ``"hsm"`` re-centers on the HSM adaptive-moment centroid
243+
(robust for galaxies); ``"wcs"`` uses the catalog sky position
244+
projected through the WCS (better for stars, whose HSM moments are
245+
noisy). See :func:`make_ngmix_observation`.
235246
236247
Raises
237248
------
@@ -252,6 +263,7 @@ def __init__(
252263
save_batch=-1,
253264
id_obj_min=-1,
254265
id_obj_max=-1,
266+
centroid_source="hsm",
255267
):
256268

257269
if len(input_file_list) not in {6, 7}:
@@ -289,6 +301,7 @@ def __init__(
289301
self._save_batch = save_batch
290302
self._id_obj_min = id_obj_min
291303
self._id_obj_max = id_obj_max
304+
self._centroid_source = centroid_source
292305

293306
self._w_log = w_log
294307

@@ -654,6 +667,7 @@ def process(self):
654667
prior,
655668
flux_guess,
656669
self._rng,
670+
centroid_source=self._centroid_source,
657671
)
658672
except Exception as ee:
659673
self._w_log.info(
@@ -773,8 +787,9 @@ def prepare_postage_stamps(vignet, obj_id, i_tile, tile_cat):
773787
else None
774788
)
775789

790+
epoch_wcs = vignet.f_wcs_file[exp_name][int(ccd_n)]['WCS']
776791
jacob = get_galsim_jacobian(
777-
vignet.f_wcs_file[exp_name][int(ccd_n)]['WCS'],
792+
epoch_wcs,
778793
tile_cat.ra[i_tile],
779794
tile_cat.dec[i_tile]
780795
)
@@ -804,7 +819,11 @@ def prepare_postage_stamps(vignet, obj_id, i_tile, tile_cat):
804819
stamp.flags.append(flag_vign)
805820
stamp.bkg_rms.append(bkg_rms_vign_scaled)
806821
stamp.jacobs.append(jacob)
807-
822+
# For the "wcs" centroid source (see make_ngmix_observation).
823+
stamp.wcs.append(epoch_wcs)
824+
stamp.ra.append(tile_cat.ra[i_tile])
825+
stamp.dec.append(tile_cat.dec[i_tile])
826+
808827
return stamp
809828

810829
def background_subtract(gal,bkg):
@@ -997,11 +1016,25 @@ def prepare_ngmix_weights(gal, weight, flag, rng, bkg_rms=None):
9971016

9981017
return gal_masked, weight_map, noise_img
9991018

1000-
def make_ngmix_observation(gal, weight, flag, psf, wcs, rng, bkg_rms=None):
1019+
def make_ngmix_observation(
1020+
gal, weight, flag, psf, wcs, rng,
1021+
bkg_rms=None, centroid_source="hsm", wcs_full=None, ra=None, dec=None,
1022+
):
10011023
"""Build an ngmix Observation for a single galaxy epoch.
10021024
1003-
The galaxy Jacobian is re-centered on the HSM centroid so that the
1004-
centroid prior (centered at the Jacobian origin) does not bias the fit.
1025+
The galaxy Jacobian origin sets where the centroid prior is centered, so
1026+
it must sit on the object. Two ways to place it, selected by
1027+
``centroid_source``:
1028+
1029+
* ``"hsm"`` (default) — re-center on the HSM adaptive-moment centroid
1030+
measured from the stamp. Robust for **galaxies**: it follows the actual
1031+
light and so the centroid prior (centered at the Jacobian origin) does
1032+
not bias an object that is offset from the stamp center.
1033+
* ``"wcs"`` — place the origin at the object's catalog sky position,
1034+
projected through the WCS to a sub-pixel pixel offset from the stamp
1035+
center, with no shape measurement. Better for **stars**: their HSM
1036+
moments are noisy, so trusting the astrometry is more stable than
1037+
re-measuring the centroid.
10051038
10061039
Parameters
10071040
----------
@@ -1016,6 +1049,14 @@ def make_ngmix_observation(gal, weight, flag, psf, wcs, rng, bkg_rms=None):
10161049
reproducibility).
10171050
bkg_rms : numpy.ndarray, optional
10181051
Per-pixel background RMS map.
1052+
centroid_source : {"hsm", "wcs"}, optional
1053+
How to place the galaxy Jacobian origin; the default is ``"hsm"``.
1054+
wcs_full : astropy.wcs.WCS, optional
1055+
Full exposure WCS for the object's CCD. Required for
1056+
``centroid_source="wcs"`` (ignored for ``"hsm"``).
1057+
ra, dec : float, optional
1058+
Object sky position in degrees. Required for
1059+
``centroid_source="wcs"`` (ignored for ``"hsm"``).
10191060
10201061
Returns
10211062
-------
@@ -1032,18 +1073,36 @@ def make_ngmix_observation(gal, weight, flag, psf, wcs, rng, bkg_rms=None):
10321073
gal, weight, flag, rng, bkg_rms=bkg_rms
10331074
)
10341075

1035-
# Re-center Jacobian on HSM centroid (pixel offset from stamp center).
1036-
# Fixes: centroid prior biases fit when galaxy is offset from stamp center.
1037-
try:
1038-
_hsm = galsim.hsm.FindAdaptiveMom(
1039-
galsim.Image(gal, scale=1.0), strict=False
1076+
if centroid_source == "hsm":
1077+
# Re-center Jacobian on HSM centroid (pixel offset from stamp center).
1078+
# Fixes: centroid prior biases fit when galaxy is offset from stamp
1079+
# center. Robust for galaxies; noisy for stars (use "wcs" there).
1080+
try:
1081+
_hsm = galsim.hsm.FindAdaptiveMom(
1082+
galsim.Image(gal, scale=1.0), strict=False
1083+
)
1084+
if _hsm.error_message != "":
1085+
raise galsim.hsm.GalSimHSMError(_hsm.error_message)
1086+
_cen = _hsm.moments_centroid - galsim.Image(gal, scale=1.0).center
1087+
cen_row, cen_col = _cen.y, _cen.x
1088+
except Exception:
1089+
cen_row, cen_col = 0.0, 0.0
1090+
elif centroid_source == "wcs":
1091+
# Place the origin at the catalog sky position projected through the
1092+
# WCS — no shape measurement. Stars have noisy HSM moments, so trust
1093+
# the astrometry instead of re-measuring the centroid.
1094+
g_wcs = galsim.fitswcs.AstropyWCS(wcs=wcs_full)
1095+
world_pos = galsim.CelestialCoord(
1096+
ra * galsim.degrees, dec * galsim.degrees
1097+
)
1098+
pos = g_wcs.toImage(world_pos)
1099+
cen_col = pos.x - np.round(pos.x).astype(int)
1100+
cen_row = pos.y - np.round(pos.y).astype(int)
1101+
else:
1102+
raise ValueError(
1103+
f"Unknown centroid_source '{centroid_source}'; expected"
1104+
+ " 'hsm' or 'wcs'"
10401105
)
1041-
if _hsm.error_message != "":
1042-
raise galsim.hsm.GalSimHSMError(_hsm.error_message)
1043-
_cen = _hsm.moments_centroid - galsim.Image(gal, scale=1.0).center
1044-
cen_row, cen_col = _cen.y, _cen.x
1045-
except Exception:
1046-
cen_row, cen_col = 0.0, 0.0
10471106

10481107
gal_jacob = ngmix.Jacobian(
10491108
row=(gal.shape[0] - 1) / 2 + cen_row,
@@ -1144,7 +1203,7 @@ def make_runners(prior, flux_guess, rng):
11441203
)
11451204

11461205

1147-
def do_ngmix_metacal(stamp, prior, flux_guess, rng):
1206+
def do_ngmix_metacal(stamp, prior, flux_guess, rng, centroid_source="hsm"):
11481207
"""Do Ngmix Metacal.
11491208
11501209
Performs metacalibration on a single multi-epoch object and returns the
@@ -1160,6 +1219,12 @@ def do_ngmix_metacal(stamp, prior, flux_guess, rng):
11601219
Initial flux guess.
11611220
rng : numpy.random.RandomState
11621221
Random state for guesses and priors.
1222+
centroid_source : {"hsm", "wcs"}, optional
1223+
How to place the galaxy Jacobian origin; passed through to
1224+
:func:`make_ngmix_observation`. The default is ``"hsm"`` (HSM
1225+
adaptive-moment centroid); ``"wcs"`` uses the catalog sky position
1226+
projected through the WCS — see that function for the star-vs-galaxy
1227+
rationale.
11631228
11641229
Returns
11651230
-------
@@ -1182,6 +1247,10 @@ def do_ngmix_metacal(stamp, prior, flux_guess, rng):
11821247
stamp.jacobs[n_e],
11831248
rng,
11841249
bkg_rms=bkg_rms,
1250+
centroid_source=centroid_source,
1251+
wcs_full=stamp.wcs[n_e] if n_e < len(stamp.wcs) else None,
1252+
ra=stamp.ra[n_e] if n_e < len(stamp.ra) else None,
1253+
dec=stamp.dec[n_e] if n_e < len(stamp.dec) else None,
11851254
)
11861255
gal_obs_list.append(gal_obs)
11871256

src/shapepipe/modules/ngmix_runner.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ def ngmix_runner(
7979
id_obj_min = config.getint(module_config_sec, "ID_OBJ_MIN")
8080
id_obj_max = config.getint(module_config_sec, "ID_OBJ_MAX")
8181

82+
# Centroid source for the galaxy Jacobian origin: "hsm" (default,
83+
# HSM adaptive-moment centroid; robust for galaxies) or "wcs" (catalog
84+
# sky position through the WCS; better for stars).
85+
if config.has_option(module_config_sec, "CENTROID_SOURCE"):
86+
centroid_source = config.get(module_config_sec, "CENTROID_SOURCE")
87+
else:
88+
centroid_source = "hsm"
89+
8290
# Initialise class instance
8391
ngmix_inst = Ngmix(
8492
input_file_list,
@@ -91,6 +99,7 @@ def ngmix_runner(
9199
save_batch=save_batch,
92100
id_obj_min=id_obj_min,
93101
id_obj_max=id_obj_max,
102+
centroid_source=centroid_source,
94103
)
95104

96105
# Process ngmix shape measurement and metacalibration

tests/cluster/test_star_shear_response.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@
3434

3535
pytestmark = [pytest.mark.slow, pytest.mark.candide]
3636

37+
# The star grid uses the WCS centroid source: a star's HSM adaptive moments
38+
# are too noisy to re-center on, so the galaxy Jacobian origin is placed at the
39+
# catalog sky position projected through the WCS (ngmix_runner CENTROID_SOURCE
40+
# = wcs). When regenerating, this is threaded into the job chain so the outputs
41+
# this metric reads are produced with the star-appropriate centroid.
42+
CENTROID_SOURCE = "wcs"
43+
3744

3845
def _base_dir():
3946
"""Outputs dir to evaluate — overridable for alternate runs / regen."""
@@ -61,7 +68,9 @@ def star_response(artifacts_dir):
6168
tiles = os.environ.get("SHAPEPIPE_STAR_GRID_TILES", "").split()
6269
if not tiles:
6370
pytest.skip("set SHAPEPIPE_STAR_GRID_TILES to regenerate")
64-
wait_for_jobs(submit_star_grid_chain(tiles))
71+
wait_for_jobs(
72+
submit_star_grid_chain(tiles, centroid_source=CENTROID_SOURCE)
73+
)
6574

6675
summary = compute_star_response(base_dir)
6776
status = (

tests/helpers/cluster.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
seam to drive it on demand, NOT something the test suite runs by default.
2020
"""
2121

22+
import os
2223
import shutil
2324
import subprocess
2425
import time
@@ -86,20 +87,27 @@ def sbatch(job_script, *job_args, dependency=None):
8687
return out.strip().split(";")[0]
8788

8889

89-
def submit_star_grid_chain(tiles, *, launcher=None):
90+
def submit_star_grid_chain(tiles, *, launcher=None, centroid_source="wcs"):
9091
"""Submit the star-grid job chain for a list of tile ids.
9192
9293
Mirrors ``tile_launcher.job``: one ``job_per_tile_newversion.job`` per
9394
tile id. Returns the list of submitted job ids. Does NOT wait — pair
9495
with :func:`wait_for_jobs`.
9596
97+
``centroid_source`` ("wcs" for the star grid) is exported as
98+
``SP_CENTROID_SOURCE`` so the job chain runs ngmix with the
99+
star-appropriate centroid (the ngmix_runner ``CENTROID_SOURCE`` config
100+
key). Stars have noisy HSM moments, so the grid trusts the WCS-projected
101+
catalog position rather than re-measuring the centroid.
102+
96103
This is the heavy path (each tile is a multi-hour job). It is wired so a
97104
cluster test can regenerate outputs on demand; the suite never calls it
98105
automatically.
99106
"""
100107
launcher = Path(launcher) if launcher else SP_SIMU_ROOT / "job_per_tile_newversion.job"
101108
if not launcher.exists():
102109
raise FileNotFoundError(f"launcher job script not found: {launcher}")
110+
os.environ["SP_CENTROID_SOURCE"] = centroid_source
103111
return [sbatch(launcher, tile) for tile in tiles]
104112

105113

0 commit comments

Comments
 (0)