Coverage for python/lsst/afw/geom/utils.py: 18%

52 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-07 02:37 -0800

1# This file is part of afw. 

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 

22"""Utilities that should be imported into the lsst.afw.geom namespace when lsst.afw.geom is used 

23 

24In the case of the assert functions, importing them makes them available in lsst.utils.tests.TestCase 

25""" 

26__all__ = ["wcsAlmostEqualOverBBox"] 

27 

28import itertools 

29import math 

30 

31import numpy as np 

32 

33import lsst.utils.tests 

34import lsst.geom 

35from ._geom import GenericEndpoint, Point2Endpoint, SpherePointEndpoint 

36 

37 

38def _compareWcsOverBBox(wcs0, wcs1, bbox, maxDiffSky=0.01*lsst.geom.arcseconds, 

39 maxDiffPix=0.01, nx=5, ny=5, doShortCircuit=True): 

40 """Compare two :py:class:`WCS <lsst.afw.geom.SkyWcs>` over a rectangular grid of pixel positions 

41 

42 Parameters 

43 ---------- 

44 wcs0 : `lsst.afw.geom.SkyWcs` 

45 WCS 0 

46 wcs1 : `lsst.afw.geom.SkyWcs` 

47 WCS 1 

48 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D` 

49 boundaries of pixel grid over which to compare the WCSs 

50 maxDiffSky : `lsst.geom.Angle` 

51 maximum separation between sky positions computed using Wcs.pixelToSky 

52 maxDiffPix : `float` 

53 maximum separation between pixel positions computed using Wcs.skyToPixel 

54 nx : `int` 

55 number of points in x for the grid of pixel positions 

56 ny : `int` 

57 number of points in y for the grid of pixel positions 

58 doShortCircuit : `bool` 

59 if True then stop at the first error, else test all values in the grid 

60 and return information about the worst violations found 

61 

62 Returns 

63 ------- 

64 msg : `str` 

65 an empty string if the WCS are sufficiently close; else return a string describing 

66 the largest error measured in pixel coordinates (if sky to pixel error was excessive) 

67 and sky coordinates (if pixel to sky error was excessive). If doShortCircuit is true 

68 then the reported error is likely to be much less than the maximum error across the 

69 whole pixel grid. 

70 """ 

71 if nx < 1 or ny < 1: 

72 raise RuntimeError(f"nx = {nx} and ny = {ny} must both be positive") 

73 if maxDiffSky < 0*lsst.geom.arcseconds: 

74 raise RuntimeError(f"maxDiffSky = {maxDiffSky} must not be negative") 

75 if maxDiffPix < 0: 

76 raise RuntimeError(f"maxDiffPix = {maxDiffPix} must not be negative") 

77 

78 bboxd = lsst.geom.Box2D(bbox) 

79 xList = np.linspace(bboxd.getMinX(), bboxd.getMaxX(), nx) 

80 yList = np.linspace(bboxd.getMinY(), bboxd.getMaxY(), ny) 

81 # we don't care about measured error unless it is too large, so initialize 

82 # to max allowed 

83 measDiffSky = (maxDiffSky, "?") # (sky diff, pix pos) 

84 measDiffPix = (maxDiffPix, "?") # (pix diff, sky pos) 

85 for x, y in itertools.product(xList, yList): 

86 fromPixPos = lsst.geom.Point2D(x, y) 

87 sky0 = wcs0.pixelToSky(fromPixPos) 

88 sky1 = wcs1.pixelToSky(fromPixPos) 

89 diffSky = sky0.separation(sky1) 

90 if diffSky > measDiffSky[0]: 

91 measDiffSky = (diffSky, fromPixPos) 

92 if doShortCircuit: 

93 break 

94 

95 toPixPos0 = wcs0.skyToPixel(sky0) 

96 toPixPos1 = wcs1.skyToPixel(sky0) 

97 diffPix = math.hypot(*(toPixPos0 - toPixPos1)) 

98 if diffPix > measDiffPix[0]: 

99 measDiffPix = (diffPix, sky0) 

100 if doShortCircuit: 

101 break 

102 

103 msgList = [] 

104 if measDiffSky[0] > maxDiffSky: 

105 msgList.append(f"{measDiffSky[0].asArcseconds()} arcsec max measured sky error " 

106 f"> {maxDiffSky.asArcseconds()} arcsec max allowed sky error " 

107 f"at pix pos=measDiffSky[1]") 

108 if measDiffPix[0] > maxDiffPix: 

109 msgList.append(f"{measDiffPix[0]} max measured pix error " 

110 f"> {maxDiffPix} max allowed pix error " 

111 f"at sky pos={measDiffPix[1]}") 

112 

113 return "; ".join(msgList) 

114 

115 

116def wcsAlmostEqualOverBBox(wcs0, wcs1, bbox, maxDiffSky=0.01*lsst.geom.arcseconds, 

117 maxDiffPix=0.01, nx=5, ny=5): 

118 """Test if two :py:class:`WCS <lsst.afw.geom.SkyWcs>` are almost equal over a grid of pixel positions. 

119 

120 Parameters 

121 ---------- 

122 wcs0 : `lsst.afw.geom.SkyWcs` 

123 WCS 0 

124 wcs1 : `lsst.afw.geom.SkyWcs` 

125 WCS 1 

126 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D` 

127 boundaries of pixel grid over which to compare the WCSs 

128 maxDiffSky : `lsst.geom.Angle` 

129 maximum separation between sky positions computed using Wcs.pixelToSky 

130 maxDiffPix : `float` 

131 maximum separation between pixel positions computed using Wcs.skyToPixel 

132 nx : `int` 

133 number of points in x for the grid of pixel positions 

134 ny : `int` 

135 number of points in y for the grid of pixel positions 

136 

137 Returns 

138 ------- 

139 almostEqual: `bool` 

140 `True` if two WCS are almost equal over a grid of pixel positions, else `False` 

141 """ 

142 return not bool(_compareWcsOverBBox( 

143 wcs0=wcs0, 

144 wcs1=wcs1, 

145 bbox=bbox, 

146 maxDiffSky=maxDiffSky, 

147 maxDiffPix=maxDiffPix, 

148 nx=nx, 

149 ny=ny, 

150 doShortCircuit=True, 

151 )) 

152 

153 

154@lsst.utils.tests.inTestCase 

155def assertWcsAlmostEqualOverBBox(testCase, wcs0, wcs1, bbox, maxDiffSky=0.01*lsst.geom.arcseconds, 

156 maxDiffPix=0.01, nx=5, ny=5, msg="WCSs differ"): 

157 """Assert that two :py:class:`WCS <lsst.afw.geom.SkyWcs>` are almost equal over a grid of pixel positions 

158 

159 Compare pixelToSky and skyToPixel for two WCS over a rectangular grid of pixel positions. 

160 If the WCS are too divergent at any point, call testCase.fail; the message describes 

161 the largest error measured in pixel coordinates (if sky to pixel error was excessive) 

162 and sky coordinates (if pixel to sky error was excessive) across the entire pixel grid. 

163 

164 Parameters 

165 ---------- 

166 testCase : `unittest.TestCase` 

167 test case the test is part of; an object supporting one method: fail(self, msgStr) 

168 wcs0 : `lsst.afw.geom.SkyWcs` 

169 WCS 0 

170 wcs1 : `lsst.afw.geom.SkyWcs` 

171 WCS 1 

172 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D` 

173 boundaries of pixel grid over which to compare the WCSs 

174 maxDiffSky : `lsst.geom.Angle` 

175 maximum separation between sky positions computed using Wcs.pixelToSky 

176 maxDiffPix : `float` 

177 maximum separation between pixel positions computed using Wcs.skyToPixel 

178 nx : `int` 

179 number of points in x for the grid of pixel positions 

180 ny : `int` 

181 number of points in y for the grid of pixel positions 

182 msg : `str` 

183 exception message prefix; details of the error are appended after ": " 

184 """ 

185 errMsg = _compareWcsOverBBox( 

186 wcs0=wcs0, 

187 wcs1=wcs1, 

188 bbox=bbox, 

189 maxDiffSky=maxDiffSky, 

190 maxDiffPix=maxDiffPix, 

191 nx=nx, 

192 ny=ny, 

193 doShortCircuit=False, 

194 ) 

195 if errMsg: 

196 testCase.fail(f"{msg}: {errMsg}") 

197 

198 

199@lsst.utils.tests.inTestCase 

200def makeEndpoints(testCase): 

201 """Generate a representative sample of ``Endpoints``. 

202 

203 Parameters 

204 ---------- 

205 testCase : `unittest.TestCase` 

206 test case the test is part of; an object supporting one method: fail(self, msgStr) 

207 

208 Returns 

209 ------- 

210 endpoints : `list` 

211 List of endpoints with enough diversity to exercise ``Endpoint``-related 

212 code. Each invocation of this method shall return independent objects. 

213 """ 

214 return [GenericEndpoint(n) for n in range(1, 6)] + \ 

215 [Point2Endpoint(), SpherePointEndpoint()]