Repository: esheldon/metadetect
Pull request #229: Add gauss fitter for lsst
Author: login: esheldon
Created: 2025-04-23T15:00:57Z | Merged: 2025-04-25T14:13:44Z

This adds the basic calls and data type support.

This needs to be tested for accuracy


Diff:
diff --git a/metadetect/fitting.py b/metadetect/fitting.py
index 6d51ffcf..83e3ab21 100644
--- a/metadetect/fitting.py
+++ b/metadetect/fitting.py
@@ -131,6 +131,16 @@ def fit_mbobs_gauss(
     if flags == 0:
         res["gauss_obj_flags"] = ores["flags"]
         res["gauss_T_flags"] = ores["flags"]
+
+        if len(res["gauss_band_flux_flags"].shape) == 2:
+            res["gauss_band_flux_flags"][0, shear_bands] = ores["flags"]
+            res["gauss_band_flux"][0, shear_bands] = ores["flux"]
+            res["gauss_band_flux_err"][0, shear_bands] = ores["flux_err"]
+        else:
+            res["gauss_band_flux_flags"] = ores["flags"]
+            res["gauss_band_flux"] = ores["flux"]
+            res["gauss_band_flux_err"] = ores["flux_err"]
+
         if ores["flags"] == 0:
             res["gauss_s2n"] = ores["s2n"]
             res["gauss_g"] = ores["g"]
@@ -143,7 +153,7 @@ def fit_mbobs_gauss(
             res["gauss_psf_flags"] = pres["flags"]
             if res["gauss_psf_flags"] == 0:
                 res["gauss_psf_T"] = pres["T"]
-                res["gauss_psf_g"] = pres["g"]
+                res["gauss_psf_g"] = _extract_g(pres)
                 if ores["flags"] == 0:
                     res["gauss_T_ratio"] = res["gauss_T"] / res["gauss_psf_T"]
         else:
@@ -161,8 +171,9 @@ def fit_mbobs_gauss(
                         else:
                             _wgt = np.median(obs.weight[msk])
                             psf_T_sum += obs.psf.meta["result"]["T"] * _wgt
+                            g = _extract_g(obs.psf.meta["result"])
                             psf_g_sum += (
-                                obs.psf.meta["result"]["g"]
+                                g
                                 * _wgt
                                 * obs.psf.meta["result"]["T"]
                             )
@@ -182,6 +193,20 @@ def fit_mbobs_gauss(
     return res
 
 
+def _extract_g(res):
+    """
+    We may have fit the PSF with adaptive moments, so check for
+    'e' as well
+    """
+    if 'g' in res:
+        g = res['g']
+    elif 'e' in res:
+        e1, e2 = res['e']
+        g1, g2 = ngmix.shape.e1e2_to_g1g2(e1, e2)
+        g = np.array([g1, g2])
+    return g
+
+
 def get_gauss_psf_runner(rng):
     psf_guesser = SimplePSFGuesser(
         rng=rng,
diff --git a/metadetect/lsst/defaults.py b/metadetect/lsst/defaults.py
index e0b82441..18ee6dc8 100644
--- a/metadetect/lsst/defaults.py
+++ b/metadetect/lsst/defaults.py
@@ -14,6 +14,7 @@
     'ksigma': 64,
     'pgauss': 49,
     'am': 49,
+    'gauss': 49,
 }
 
 # threshold for detection
diff --git a/metadetect/lsst/measure.py b/metadetect/lsst/measure.py
index d0ca4d4e..45db3bce 100644
--- a/metadetect/lsst/measure.py
+++ b/metadetect/lsst/measure.py
@@ -24,7 +24,11 @@
 from ..procflags import (
     EDGE_HIT, ZERO_WEIGHTS, CENTROID_FAILURE, NO_ATTEMPT,
 )
-from ..fitting import fit_mbobs_wavg, get_wavg_output_struct
+from ..fitting import (
+    fit_mbobs_gauss,
+    fit_mbobs_wavg,
+    get_wavg_output_struct,
+)
 
 from . import util
 from .util import ContextNoiseReplacer, get_jacobian
@@ -246,6 +250,7 @@ def measure(
     fitter,
     stamp_size,
     fwhm_reg=0,
+    rng=None,
 ):
     """
     run measurements on the input exposure, given the input measurement task,
@@ -260,12 +265,14 @@ def measure(
     sources: list of sources
         From a detection task
     fitter: e.g. ngmix.gaussmom.GaussMom or ngmix.ksigmamom.PGaussMom
-        For calculating moments
+        For calculating moments. Can also be string 'gauss'
     stamp_size: int
         Size for postage stamps
     fwhm_reg: float, optional
         Optional regularization for calculating shapes.  The fwhm is converted
         to T and T+Treg is used in the denominator
+    rng: np.random.RandomState
+        Random number generator, only used when fitter == 'gauss'
 
     Returns
     -------
@@ -297,12 +304,28 @@ def measure(
             )
 
             # TODO do something with bmask_flags?
-            this_res = fit_mbobs_wavg(
-                mbobs=mbobs,
-                fitter=fitter,
-                bmask_flags=0,
-                fwhm_reg=fwhm_reg,
-            )
+            if fitter == 'gauss':
+                psf_fitter = ngmix.admom.AdmomFitter(rng=rng)
+                psf_guesser = ngmix.guessers.GMixPSFGuesser(
+                    rng=rng, ngauss=1, guess_from_moms=True,
+                )
+                psf_runner = ngmix.runners.PSFRunner(
+                    fitter=psf_fitter, guesser=psf_guesser, ntry=4,
+                )
+
+                this_res = fit_mbobs_gauss(
+                    mbobs=mbobs,
+                    bmask_flags=0,
+                    psf_runner=psf_runner,
+                    rng=rng,
+                )
+            else:
+                this_res = fit_mbobs_wavg(
+                    mbobs=mbobs,
+                    fitter=fitter,
+                    bmask_flags=0,
+                    fwhm_reg=fwhm_reg,
+                )
         except LengthError as err:
             # This is raised when a bbox hits an edge
             LOG.debug('%s', err)
diff --git a/metadetect/lsst/metadetect.py b/metadetect/lsst/metadetect.py
index 7ec28e45..359b193b 100644
--- a/metadetect/lsst/metadetect.py
+++ b/metadetect/lsst/metadetect.py
@@ -179,6 +179,7 @@ class MetadetectConfig(Config):
             "ksigma": "Fourier moments",  # TODO: improve docstring
             "pgauss": "Pre-seeing moments",
             "am": "Adaptive moments",
+            "gauss": "Gaussian Fit",
         }
     )
 
@@ -340,6 +341,7 @@ def detect_deblend_and_measure(
         fitter=fitter,
         stamp_size=config['stamp_size'],
         fwhm_reg=fwhm_reg,
+        rng=rng,
     )
 
     return results
@@ -507,8 +509,8 @@ def get_fitter(config, rng=None):
         # TODO is there a better way?
         fitter.kind = 'am'
 
-    elif meas_type == 'em':
-        fitter = None
+    elif meas_type == 'gauss':
+        fitter = 'gauss'
     else:
         fwhm = config['weight']['fwhm']
 
diff --git a/metadetect/lsst/tests/test_lsst_metadetect.py b/metadetect/lsst/tests/test_lsst_metadetect.py
index bfc56ddc..a06f259c 100644
--- a/metadetect/lsst/tests/test_lsst_metadetect.py
+++ b/metadetect/lsst/tests/test_lsst_metadetect.py
@@ -286,11 +286,38 @@ def test_lsst_metadetect_am():
             len(res[shear][flux_name][0])
 
 
+def test_lsst_metadetect_gauss():
+    rng = np.random.RandomState(seed=918)
+
+    bands = ['r', 'i', 'z']
+    sim_data = make_lsst_sim(116, bands=bands)
+    data = do_coadding(rng=rng, sim_data=sim_data, nowarp=True)
+
+    meas_type = 'gauss'
+    config = {'meas_type': meas_type}
+
+    res = run_metadetect(rng=rng, config=config, **data)
+
+    gname = f'{meas_type}_g'
+    flux_name = f'{meas_type}_band_flux'
+    assert gname in res['noshear'].dtype.names
+
+    for shear in ('noshear', '1p', '1m'):
+        # 5x5 grid of sources
+        assert res[shear].size == 25
+
+        assert np.all(res[shear][f"{meas_type}_flags"] == 0)
+        assert np.all(res[shear]["mfrac"] == 0)
+
+        assert res[shear][flux_name].shape[1] == len(bands)
+        assert np.all(np.isfinite(res[shear][flux_name]))
+
+
 def test_lsst_metadetect_fullcoadd_smoke():
     rng = np.random.RandomState(seed=116)
 
     bands = ['r', 'i']
-    sim_data = make_lsst_sim(116, bands=bands)
+    sim_data = make_lsst_sim(882, bands=bands)
     data = do_coadding(rng=rng, sim_data=sim_data, nowarp=False)
 
     config = {'meas_type': 'pgauss'}
@@ -463,6 +490,7 @@ def test_lsst_metadetect_mfrac_ormask(show=False):
 
 if __name__ == '__main__':
     # test_lsst_metadetect_am()
-    test_lsst_masked_as_bright(show=True)
+    test_lsst_metadetect_gauss()
+    # test_lsst_masked_as_bright(show=True)
     # test_lsst_metadetect_smoke('wmom', 'False')
     # test_lsst_metadetect_mfrac_ormask(show=True)
diff --git a/metadetect/tests/test_fitting_joint.py b/metadetect/tests/test_fitting_joint.py
index 4087d528..3059eea5 100644
--- a/metadetect/tests/test_fitting_joint.py
+++ b/metadetect/tests/test_fitting_joint.py
@@ -765,9 +765,15 @@ def test_fit_mbobs_gauss_smoke(shear_bands):
 
     for col in res.dtype.names:
         if col.endswith("band_flux_flags"):
-            assert np.all(res[col] == procflags.NO_ATTEMPT), (col, res[col])
+            assert np.all(res[col][0, shear_bands] == 0), (col, res[col])
+            if len(shear_bands) != len(mbobs):
+                tocheck = [i for i in range(len(mbobs)) if i not in shear_bands]
+                assert np.all(
+                    res[col][:, tocheck] == procflags.NO_ATTEMPT
+                ), (col, res[col])
+
         elif "band_flux" in col:
-            assert np.all(np.isnan(res[col])), (col, res[col])
+            assert np.all(np.isfinite(res[col][0, shear_bands])), (col, res[col])
         elif col == "shear_bands":
             assert np.all(
                 res[col]
@@ -802,11 +808,22 @@ def test_fit_mbobs_gauss_coadd():
 
     for col in res.dtype.names:
         if col.endswith("band_flux_flags"):
-            assert np.all(res[col] == procflags.NO_ATTEMPT), (col, res[col])
-            assert np.all(res_coadd[col] == procflags.NO_ATTEMPT), (col, res_coadd[col])
-        elif "band_flux" in col:
-            assert np.all(np.isnan(res[col])), (col, res[col])
-            assert np.all(np.isnan(res_coadd[col])), (col, res_coadd[col])
+            assert np.all(res[col][0, shear_bands] == 0), (col, res[col])
+            assert np.all(res_coadd[col][0, shear_bands] == 0), (col, res_coadd[col])
+            if len(shear_bands) != len(mbobs):
+                assert np.any(
+                    res[col][0, shear_bands] == procflags.NO_ATTEMPT
+                ), (col, res[col])
+
+                tocheck = [i for i in range(len(mbobs)) if i not in shear_bands]
+                assert np.all(
+                    res_coadd[col][0, tocheck] == procflags.NO_ATTEMPT
+                ), (col, res_coadd[col])
+        elif col.endswith("band_flux"):
+            assert np.all(np.isfinite(res[col][0, shear_bands])), (col, res[col])
+            assert np.all(
+                np.isfinite(res_coadd[col][0, shear_bands])
+            ), (col, res_coadd[col])
         elif col == "shear_bands":
             assert np.all(
                 res[col]
diff --git a/metadetect/tests/test_metadetect.py b/metadetect/tests/test_metadetect.py
index 0770633e..0516a97b 100644
--- a/metadetect/tests/test_metadetect.py
+++ b/metadetect/tests/test_metadetect.py
@@ -175,7 +175,7 @@ def _check_result_array(res, shear, msk, model):
             assert np.all(res[shear][msk][col] == "012")
         else:
             # admom doesn't make band fluxes
-            if model in ["admom", "am", "gauss"] and "band_flux" in col:
+            if model in ["admom", "am"] and "band_flux" in col:
                 if col.endswith("band_flux_flags"):
                     assert np.array_equal(
                         res[shear][msk][col],


Reviews:
user: login: beckermr
body: Changes look good. LGTM!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-23T15:58:09Z
user: login: beckermr
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T18:04:14Z
user: login: beckermr
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T18:05:37Z
user: login: beckermr
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T18:05:57Z
user: login: beckermr
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T18:06:34Z
user: login: beckermr
body: A few random comments.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T18:07:10Z
user: login: esheldon
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-24T17:35:09Z
user: login: beckermr
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-24T23:18:44Z
user: login: beckermr
body: LGTM!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-24T23:21:09Z

Review comments:
diff_hunk: @@ -286,11 +286,38 @@ def test_lsst_metadetect_am():
             len(res[shear][flux_name][0])
 
 
+def test_lsst_metadetect_gauss():
+    rng = np.random.RandomState(seed=918)
+
+    bands = ['r', 'i', 'z']
+    sim_data = make_lsst_sim(116, bands=bands)
+    data = do_coadding(rng=rng, sim_data=sim_data, nowarp=True)
+
+    meas_type = 'gauss'
+    config = {'meas_type': meas_type}
+
+    res = run_metadetect(rng=rng, config=config, **data)
+
+    gname = f'{meas_type}_g'
+    flux_name = f'{meas_type}_band_flux'
+    assert gname in res['noshear'].dtype.names
+
+    for shear in ('noshear', '1p', '1m'):
+        # 5x5 grid
+        assert res[shear].size == 25
+
+        assert np.all(res[shear][f"{meas_type}_flags"] == 0)
+        assert np.all(res[shear]["mfrac"] == 0)
+
+        assert res[shear][flux_name].shape[1] == len(bands)
+        assert np.all(np.isfinite(res[shear][flux_name]))
+
+
 def test_lsst_metadetect_fullcoadd_smoke():
     rng = np.random.RandomState(seed=116)
 
     bands = ['r', 'i']
-    sim_data = make_lsst_sim(116, bands=bands)
+    sim_data = make_lsst_sim(882, bands=bands)
path: metadetect/lsst/tests/test_lsst_metadetect.py
user: login: beckermr
body: Why the seed change here?
created_at: 2025-04-23T18:04:14Z
author_association: COLLABORATOR
diff_hunk: @@ -764,10 +764,18 @@ def test_fit_mbobs_gauss_smoke(shear_bands):
         shear_bands = list(range(len(mbobs)))
 
     for col in res.dtype.names:
+        # if col.endswith("band_flux_flags"):
+        #     assert np.all(res[col] == procflags.NO_ATTEMPT), (col, res[col])
+        # elif "band_flux" in col:
+        #     assert np.all(np.isnan(res[col])), (col, res[col])
         if col.endswith("band_flux_flags"):
-            assert np.all(res[col] == procflags.NO_ATTEMPT), (col, res[col])
+            # assert np.all(res[col] == procflags.NO_ATTEMPT), (col, res[col])
path: metadetect/tests/test_fitting_joint.py
user: login: beckermr
body: Let's remove any commented code here.
created_at: 2025-04-23T18:05:37Z
author_association: COLLABORATOR
diff_hunk: @@ -764,10 +764,18 @@ def test_fit_mbobs_gauss_smoke(shear_bands):
         shear_bands = list(range(len(mbobs)))
 
     for col in res.dtype.names:
+        # if col.endswith("band_flux_flags"):
+        #     assert np.all(res[col] == procflags.NO_ATTEMPT), (col, res[col])
+        # elif "band_flux" in col:
+        #     assert np.all(np.isnan(res[col])), (col, res[col])
         if col.endswith("band_flux_flags"):
-            assert np.all(res[col] == procflags.NO_ATTEMPT), (col, res[col])
+            # assert np.all(res[col] == procflags.NO_ATTEMPT), (col, res[col])
+            assert np.all(res[col][0, shear_bands] == 0), (col, res[col])
+            if len(shear_bands) != len(mbobs):
+                assert np.any(res[col] == procflags.NO_ATTEMPT), (col, res[col])
path: metadetect/tests/test_fitting_joint.py
user: login: beckermr
body: We should check the NO_ATTEMPT flags are in the correct columns.
created_at: 2025-04-23T18:05:57Z
author_association: COLLABORATOR
diff_hunk: @@ -802,11 +810,20 @@ def test_fit_mbobs_gauss_coadd():
 
     for col in res.dtype.names:
         if col.endswith("band_flux_flags"):
-            assert np.all(res[col] == procflags.NO_ATTEMPT), (col, res[col])
-            assert np.all(res_coadd[col] == procflags.NO_ATTEMPT), (col, res_coadd[col])
-        elif "band_flux" in col:
-            assert np.all(np.isnan(res[col])), (col, res[col])
-            assert np.all(np.isnan(res_coadd[col])), (col, res_coadd[col])
+            assert np.all(res[col][0, shear_bands] == 0), (col, res[col])
+            assert np.all(res_coadd[col][0, shear_bands] == 0), (col, res_coadd[col])
+            if len(shear_bands) != len(mbobs):
+                assert np.any(
+                    res[col][0, shear_bands] == procflags.NO_ATTEMPT
+                ), (col, res[col])
+                assert np.any(
+                    res_coadd[col][0, shear_bands] == procflags.NO_ATTEMPT
+                ), (col, res_coadd[col])
path: metadetect/tests/test_fitting_joint.py
user: login: beckermr
body: Same comment on checking the correct columns.
created_at: 2025-04-23T18:06:34Z
author_association: COLLABORATOR
diff_hunk: @@ -286,11 +286,38 @@ def test_lsst_metadetect_am():
             len(res[shear][flux_name][0])
 
 
+def test_lsst_metadetect_gauss():
+    rng = np.random.RandomState(seed=918)
+
+    bands = ['r', 'i', 'z']
+    sim_data = make_lsst_sim(116, bands=bands)
+    data = do_coadding(rng=rng, sim_data=sim_data, nowarp=True)
+
+    meas_type = 'gauss'
+    config = {'meas_type': meas_type}
+
+    res = run_metadetect(rng=rng, config=config, **data)
+
+    gname = f'{meas_type}_g'
+    flux_name = f'{meas_type}_band_flux'
+    assert gname in res['noshear'].dtype.names
+
+    for shear in ('noshear', '1p', '1m'):
+        # 5x5 grid
+        assert res[shear].size == 25
+
+        assert np.all(res[shear][f"{meas_type}_flags"] == 0)
+        assert np.all(res[shear]["mfrac"] == 0)
+
+        assert res[shear][flux_name].shape[1] == len(bands)
+        assert np.all(np.isfinite(res[shear][flux_name]))
+
+
 def test_lsst_metadetect_fullcoadd_smoke():
     rng = np.random.RandomState(seed=116)
 
     bands = ['r', 'i']
-    sim_data = make_lsst_sim(116, bands=bands)
+    sim_data = make_lsst_sim(882, bands=bands)
path: metadetect/lsst/tests/test_lsst_metadetect.py
user: login: esheldon
body: I didn't see why those seeds should both be the same
created_at: 2025-04-24T17:35:09Z
author_association: OWNER
diff_hunk: @@ -286,11 +286,38 @@ def test_lsst_metadetect_am():
             len(res[shear][flux_name][0])
 
 
+def test_lsst_metadetect_gauss():
+    rng = np.random.RandomState(seed=918)
+
+    bands = ['r', 'i', 'z']
+    sim_data = make_lsst_sim(116, bands=bands)
+    data = do_coadding(rng=rng, sim_data=sim_data, nowarp=True)
+
+    meas_type = 'gauss'
+    config = {'meas_type': meas_type}
+
+    res = run_metadetect(rng=rng, config=config, **data)
+
+    gname = f'{meas_type}_g'
+    flux_name = f'{meas_type}_band_flux'
+    assert gname in res['noshear'].dtype.names
+
+    for shear in ('noshear', '1p', '1m'):
+        # 5x5 grid
path: metadetect/lsst/tests/test_lsst_metadetect.py
user: login: beckermr
body: ```suggestion
        # 5x5 grid of sources
```
created_at: 2025-04-24T23:18:44Z
author_association: COLLABORATOR

PR comments:
user: login: esheldon
created_at: 2025-04-23T15:02:30Z
body: This will close #225 
author_association: OWNER
user: login: esheldon
created_at: 2025-04-23T16:15:46Z
body: More to come
author_association: OWNER
user: login: esheldon
created_at: 2025-04-24T17:43:37Z
body: github actions are not running
author_association: OWNER
user: login: esheldon
created_at: 2025-04-25T14:13:37Z
body: I'm going to merge with the understanding that lsst+gauss will be tested for accuracy using large sims in the future
author_association: OWNER