Coverage for tests / test_psfResiduals.py: 28%
61 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-06 09:07 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-06 09:07 +0000
1# This file is part of analysis_tools.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import unittest
24import galsim
25import numpy as np
27import lsst.utils.tests
28from lsst.afw.geom import Quadrupole
29from lsst.analysis.tools.actions.vector import CalcE, CalcE1, CalcE2, CalcEDiff, CalcMomentSize
30from lsst.pex.config import FieldValidationError
33class ShapeSizeTestCase(lsst.utils.tests.TestCase):
34 """Test ellipiticity and size calculations."""
36 @classmethod
37 def setUpClass(cls):
38 cls.data = np.array(
39 [
40 (1.3, 1.3, 0.0), # e1 = e2 = 0
41 (2.4, 1.2, 0.6), # e1 = e2 != 0
42 (1.0, 2.0, 0.0), # e1 < 0; e2 = 0
43 (3.5, 3.5, 0.5), # e1 = 0; e2 > 0
44 (3.0, 1.5, -1.2), # e1 > 0; e2 < 0
45 ],
46 dtype=[("i_ixx", "<f8"), ("i_iyy", "<f8"), ("i_ixy", "<f8")],
47 )
49 cls.kwargs = {"band": "i"}
51 def test_size(self):
52 """Test CalcMomentSize functor"""
53 traceSize = CalcMomentSize(sizeType="trace")(self.data, **self.kwargs)
54 determinantSize = CalcMomentSize(sizeType="determinant")(self.data, **self.kwargs)
56 for idx, row in enumerate(self.data):
57 shape = Quadrupole(ixx=row["i_ixx"], iyy=row["i_iyy"], ixy=row["i_ixy"])
58 self.assertFloatsAlmostEqual(traceSize[idx], shape.getTraceRadius(), rtol=1e-8)
59 self.assertFloatsAlmostEqual(determinantSize[idx], shape.getDeterminantRadius(), rtol=1e-8)
60 # Arithmetic mean >= Geometric mean implies that
61 # trace radius is never smaller than determinant radius.
62 self.assertGreaterEqual(traceSize[idx], determinantSize[idx])
64 def test_complex_shear(self):
65 """Test CalcE functor
67 Test that our ellipticity calculation under the two conventions are
68 accurate by comparing with GalSim routines.
69 """
70 shear = CalcE(ellipticityType="shear")(self.data, **self.kwargs)
71 distortion = CalcE(ellipticityType="distortion")(self.data, **self.kwargs)
72 size = CalcMomentSize(sizeType="determinant")(self.data, **self.kwargs)
73 for idx, row in enumerate(self.data):
74 galsim_shear = galsim.Shear(shear[idx])
75 self.assertFloatsAlmostEqual(distortion[idx].real, galsim_shear.e1)
76 self.assertFloatsAlmostEqual(distortion[idx].imag, galsim_shear.e2)
77 # Check that the ellipiticity values correspond to the moments.
78 A = galsim_shear.getMatrix() * size[idx]
79 M = np.dot(A.transpose(), A)
80 self.assertFloatsAlmostEqual(M[0, 0], row["i_ixx"], rtol=1e-8)
81 self.assertFloatsAlmostEqual(M[1, 1], row["i_iyy"], rtol=1e-8)
82 self.assertFloatsAlmostEqual(M[0, 1], row["i_ixy"], rtol=1e-8)
84 def test_halve_angle(self):
85 """Test ``halvePhaseAngle`` parameter in CalcE
87 Test that setting ``halvePhaseAngle`` to True halves the phase angle
88 while keeping the magnitude the same.
89 """
90 ellip = CalcE(ellipticityType="shear")(self.data, **self.kwargs)
91 ellip_half = CalcE(ellipticityType="shear", halvePhaseAngle=True)(self.data, **self.kwargs)
92 self.assertFloatsAlmostEqual(np.abs(ellip), np.abs(ellip_half))
94 for idx, row in enumerate(self.data):
95 galsim_shear = galsim.Shear(ellip[idx])
96 galsim_shear_half = galsim.Shear(ellip_half[idx])
97 self.assertFloatsAlmostEqual(np.abs(ellip_half[idx]), galsim_shear.g)
98 self.assertFloatsAlmostEqual(
99 galsim_shear.beta / galsim.radians, 2 * galsim_shear_half.beta / galsim.radians
100 )
102 @lsst.utils.tests.methodParameters(ellipticityType=("distortion", "shear"))
103 def test_shear_components(self, ellipticityType):
104 """Test CalcE1 and CalcE2 functors
106 This test checks if CalcE1 and CalcE2 correspond to the real and
107 imaginary components of CalcE.
108 """
109 ellip = CalcE(ellipticityType=ellipticityType)(self.data, **self.kwargs)
110 e1 = CalcE1(ellipticityType=ellipticityType)(self.data, **self.kwargs)
111 e2 = CalcE2(ellipticityType=ellipticityType)(self.data, **self.kwargs)
113 self.assertFloatsAlmostEqual(np.real(ellip), e1)
114 self.assertFloatsAlmostEqual(np.imag(ellip), e2)
116 def test_e1_validation(self):
117 """Test that CalcE1 throws an exception when misconfigured."""
118 CalcE1(ellipticityType="distortion", colXy=None).validate()
119 with self.assertRaises(FieldValidationError):
120 CalcE1(ellipticityType="shear", colXy=None).validate()
122 def test_ediff_validation(self):
123 """Test that CalcEDiff takes ellipticities of same convention."""
124 ellipA = CalcE(ellipticityType="shear")
125 ellipB = CalcE(ellipticityType="distortion")
126 with self.assertRaises(FieldValidationError):
127 CalcEDiff(colA=ellipA, colB=ellipB).validate()
130if __name__ == "__main__": 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true
131 lsst.utils.tests.init()
132 unittest.main()