Coverage for tests / test_psfResiduals.py: 28%

61 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-26 09:36 +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/>. 

21 

22import unittest 

23 

24import galsim 

25import numpy as np 

26 

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 

31 

32 

33class ShapeSizeTestCase(lsst.utils.tests.TestCase): 

34 """Test ellipiticity and size calculations.""" 

35 

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 ) 

48 

49 cls.kwargs = {"band": "i"} 

50 

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) 

55 

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]) 

63 

64 def test_complex_shear(self): 

65 """Test CalcE functor 

66 

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) 

83 

84 def test_halve_angle(self): 

85 """Test ``halvePhaseAngle`` parameter in CalcE 

86 

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)) 

93 

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 ) 

101 

102 @lsst.utils.tests.methodParameters(ellipticityType=("distortion", "shear")) 

103 def test_shear_components(self, ellipticityType): 

104 """Test CalcE1 and CalcE2 functors 

105 

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) 

112 

113 self.assertFloatsAlmostEqual(np.real(ellip), e1) 

114 self.assertFloatsAlmostEqual(np.imag(ellip), e2) 

115 

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() 

121 

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() 

128 

129 

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()