Coverage for tests/test_utils.py: 20%

170 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-28 12:51 +0000

1# This file is part of summit_utils. 

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"""Test cases for utils.""" 

23 

24import copy 

25import itertools 

26from typing import Iterable 

27import unittest 

28 

29import astropy.time 

30import astropy.units as u 

31import lsst.afw.detection as afwDetect 

32import lsst.afw.image as afwImage 

33import lsst.afw.geom as afwGeom 

34import lsst.geom as geom 

35import lsst.utils.tests 

36import numpy as np 

37from astro_metadata_translator import makeObservationInfo 

38from lsst.obs.base import createInitialSkyWcsFromBoresight 

39from lsst.obs.base.makeRawVisitInfoViaObsInfo import MakeRawVisitInfoViaObsInfo 

40from lsst.obs.lsst import Latiss 

41 

42from lsst.summit.utils.utils import (getExpPositionOffset, 

43 getFieldNameAndTileNumber, 

44 getAirmassSeeingCorrection, 

45 getFilterSeeingCorrection, 

46 quickSmooth, 

47 getQuantiles, 

48 fluxesFromFootprints, 

49 ) 

50 

51from lsst.obs.lsst.translators.latiss import AUXTEL_LOCATION 

52 

53 

54class ExpSkyPositionOffsetTestCase(lsst.utils.tests.TestCase): 

55 """A test case for testing sky position offsets for exposures.""" 

56 

57 def setUp(self): 

58 camera = Latiss.getCamera() 

59 self.assertTrue(len(camera) == 1) 

60 self.detector = camera[0] 

61 

62 self.viMaker = MakeRawVisitInfoViaObsInfo() 

63 self.mi = afwImage.MaskedImageF(0, 0) 

64 self.baseHeader = dict(boresight_airmass=1.5, 

65 temperature=15*u.deg_C, 

66 observation_type="science", 

67 exposure_time=5*u.ks, 

68 detector_num=32, 

69 location=AUXTEL_LOCATION, 

70 ) 

71 

72 def test_getExpPositionOffset(self): 

73 epsilon = 0.0001 

74 ra1s = [0, 45, 90] 

75 ra2s = copy.copy(ra1s) 

76 ra2s.extend([r + epsilon for r in ra1s]) 

77 ra1s = np.deg2rad(ra1s) 

78 ra2s = np.deg2rad(ra2s) 

79 

80 epsilon = 0.0001 

81 dec1s = [0, 45, 90] 

82 dec2s = copy.copy(dec1s) 

83 dec2s.extend([d + epsilon for d in dec1s[:-1]]) # skip last point as >90 not allowed for dec 

84 

85 rotAngle1 = geom.Angle(43.2, geom.degrees) # arbitrary non-zero 

86 rotAngle2 = geom.Angle(56.7, geom.degrees) 

87 

88 t1 = astropy.time.Time("2021-09-15T12:00:00", format="isot", scale="utc") 

89 t2 = astropy.time.Time("2021-09-15T12:01:00", format="isot", scale="utc") 

90 expTime = astropy.time.TimeDelta(20, format='sec') 

91 

92 header1 = copy.copy(self.baseHeader) 

93 header2 = copy.copy(self.baseHeader) 

94 header1['datetime_begin'] = astropy.time.Time(t1, format="isot", scale="utc") 

95 header2['datetime_begin'] = astropy.time.Time(t2, format="isot", scale="utc") 

96 

97 header1['datetime_end'] = astropy.time.Time(t1+expTime, format="isot", scale="utc") 

98 header2['datetime_end'] = astropy.time.Time(t2+expTime, format="isot", scale="utc") 

99 

100 obsInfo1 = makeObservationInfo(**header1) 

101 obsInfo2 = makeObservationInfo(**header2) 

102 

103 vi1 = self.viMaker.observationInfo2visitInfo(obsInfo1) 

104 vi2 = self.viMaker.observationInfo2visitInfo(obsInfo2) 

105 expInfo1 = afwImage.ExposureInfo() 

106 expInfo1.setVisitInfo(vi1) 

107 expInfo2 = afwImage.ExposureInfo() 

108 expInfo2.setVisitInfo(vi2) 

109 

110 for ra1, dec1, ra2, dec2 in itertools.product(ra1s, dec1s, ra2s, dec2s): 

111 pos1 = geom.SpherePoint(ra1, dec1, geom.degrees) 

112 pos2 = geom.SpherePoint(ra2, dec2, geom.degrees) 

113 

114 wcs1 = createInitialSkyWcsFromBoresight(pos1, rotAngle1, self.detector, flipX=True) 

115 wcs2 = createInitialSkyWcsFromBoresight(pos2, rotAngle2, self.detector, flipX=True) 

116 

117 exp1 = afwImage.ExposureF(self.mi, expInfo1) 

118 exp2 = afwImage.ExposureF(self.mi, expInfo2) 

119 

120 exp1.setWcs(wcs1) 

121 exp2.setWcs(wcs2) 

122 

123 result = getExpPositionOffset(exp1, exp2) 

124 

125 deltaRa = ra1 - ra2 

126 deltaDec = dec1 - dec2 

127 

128 self.assertAlmostEqual(result.deltaRa.asDegrees(), deltaRa, 6) 

129 self.assertAlmostEqual(result.deltaDec.asDegrees(), deltaDec, 6) 

130 

131 

132class MiscUtilsTestCase(lsst.utils.tests.TestCase): 

133 

134 def setUp(self) -> None: 

135 return super().setUp() 

136 

137 def test_getFieldNameAndTileNumber(self): 

138 field, num = getFieldNameAndTileNumber('simple') 

139 self.assertEqual(field, 'simple') 

140 self.assertIsNone(num) 

141 

142 field, num = getFieldNameAndTileNumber('_simple') 

143 self.assertEqual(field, '_simple') 

144 self.assertIsNone(num) 

145 

146 field, num = getFieldNameAndTileNumber('simple_321') 

147 self.assertEqual(field, 'simple') 

148 self.assertEqual(num, 321) 

149 

150 field, num = getFieldNameAndTileNumber('_simple_321') 

151 self.assertEqual(field, '_simple') 

152 self.assertEqual(num, 321) 

153 

154 field, num = getFieldNameAndTileNumber('test_321a_123') 

155 self.assertEqual(field, 'test_321a') 

156 self.assertEqual(num, 123) 

157 

158 field, num = getFieldNameAndTileNumber('test_321a_123_') 

159 self.assertEqual(field, 'test_321a_123_') 

160 self.assertIsNone(num) 

161 

162 field, num = getFieldNameAndTileNumber('test_321a_123a') 

163 self.assertEqual(field, 'test_321a_123a') 

164 self.assertIsNone(num) 

165 

166 field, num = getFieldNameAndTileNumber('test_321a:asd_asd-dsa_321') 

167 self.assertEqual(field, 'test_321a:asd_asd-dsa') 

168 self.assertEqual(num, 321) 

169 

170 def test_getAirmassSeeingCorrection(self): 

171 for airmass in (1.1, 2.0, 20.0): 

172 correction = getAirmassSeeingCorrection(airmass) 

173 self.assertGreater(correction, 0.01) 

174 self.assertLess(correction, 1.0) 

175 

176 correction = getAirmassSeeingCorrection(1) 

177 self.assertEqual(correction, 1.0) 

178 

179 with self.assertRaises(ValueError): 

180 getAirmassSeeingCorrection(0.5) 

181 

182 def test_getFilterSeeingCorrection(self): 

183 for filterName in ('SDSSg_65mm', 'SDSSr_65mm', 'SDSSi_65mm'): 

184 correction = getFilterSeeingCorrection(filterName) 

185 self.assertGreater(correction, 0.5) 

186 self.assertLess(correction, 1.5) 

187 

188 def test_quickSmooth(self): 

189 # just test that it runs and returns the right shape. It's a wrapper on 

190 # scipy.ndimage.gaussian_filter we can trust that it does what it 

191 # should, and we just test the interface hasn't bitrotted on either end 

192 data = np.zeros((100, 100), dtype=np.float32) 

193 data = quickSmooth(data, 5.0) 

194 self.assertEqual(data.shape, (100, 100)) 

195 

196 

197class QuantileTestCase(lsst.utils.tests.TestCase): 

198 def setUp(self) -> None: 

199 return super().setUp() 

200 

201 def test_quantiles(self): 

202 # We understand that our algorithm gives very large rounding error 

203 # compared to the generic numpy method. But still test it. 

204 np.random.seed(1234) 

205 # too big of a width violates the tolerance in the test to cap at 10k 

206 dataRanges = [(50, 1, -1), (100_000, 5_000, -1), (5_000_000, 10_000, -2)] 

207 colorRanges = [2, 256, 999] # [very few, nominal, lots and an odd number] 

208 for nColors, (mean, width, decimal) in itertools.product(colorRanges, dataRanges): 

209 data = np.random.normal(mean, width, (100, 100)) 

210 data[10, 10] = np.nan # check we're still nan-safe 

211 edges1 = getQuantiles(data, nColors) 

212 edges2 = np.nanquantile(data, np.linspace(0, 1, nColors + 1)) # must check with nanquantile 

213 np.testing.assert_almost_equal(edges1, edges2, decimal=decimal) 

214 

215 

216class ImageBasedTestCase(lsst.utils.tests.TestCase): 

217 def test_fluxFromFootprint(self): 

218 image = afwImage.Image( 

219 np.arange(8100, dtype=np.int32).reshape(90, 90), 

220 xy0=lsst.geom.Point2I(10, 12), 

221 dtype="I" 

222 ) 

223 

224 radius = 3 

225 spans = afwGeom.SpanSet.fromShape(radius, afwGeom.Stencil.CIRCLE, offset=(27, 30)) 

226 footprint1 = afwDetect.Footprint(spans) 

227 

228 # The extracted footprint should be the same as the product of the 

229 # spans and the overlapped bow with the image 

230 truth1 = spans.asArray() * image.array[15:22, 14:21] 

231 

232 radius = 3 

233 spans = afwGeom.SpanSet.fromShape(radius, afwGeom.Stencil.CIRCLE, offset=(44, 49)) 

234 footprint2 = afwDetect.Footprint(spans) 

235 truth2 = spans.asArray() * image.array[34:41, 31:38] 

236 

237 allFootprints = [footprint1, footprint2] 

238 footprintSet = afwDetect.FootprintSet(image.getBBox()) 

239 footprintSet.setFootprints(allFootprints) 

240 

241 # check it can accept a footprintSet, and single and iterables of 

242 # footprints 

243 with self.assertRaises(TypeError): 

244 fluxesFromFootprints(10, image) 

245 

246 with self.assertRaises(TypeError): 

247 fluxesFromFootprints([8, 6, 7, 5, 3, 0, 9], image) 

248 

249 # check the footPrintSet 

250 fluxes = fluxesFromFootprints(footprintSet, image) 

251 expectedLength = len(footprintSet.getFootprints()) 

252 self.assertEqual(len(fluxes), expectedLength) # always one flux per footprint 

253 self.assertIsInstance(fluxes, Iterable) 

254 self.assertAlmostEqual(fluxes[0], np.sum(truth1)) 

255 self.assertAlmostEqual(fluxes[1], np.sum(truth2)) 

256 

257 # check the list of footprints 

258 fluxes = fluxesFromFootprints(allFootprints, image) 

259 expectedLength = 2 

260 self.assertEqual(len(fluxes), expectedLength) # always one flux per footprint 

261 self.assertIsInstance(fluxes, Iterable) 

262 self.assertAlmostEqual(fluxes[0], np.sum(truth1)) 

263 self.assertAlmostEqual(fluxes[1], np.sum(truth2)) 

264 

265 # ensure that subtracting the image median from fluxes leave image 

266 # pixels untouched 

267 oldImageArray = copy.deepcopy(image.array) 

268 fluxes = fluxesFromFootprints(footprintSet, image, subtractImageMedian=True) 

269 np.testing.assert_array_equal(image.array, oldImageArray) 

270 

271 

272class TestMemory(lsst.utils.tests.MemoryTestCase): 

273 pass 

274 

275 

276def setup_module(module): 

277 lsst.utils.tests.init() 

278 

279 

280if __name__ == "__main__": 280 ↛ 281line 280 didn't jump to line 281, because the condition on line 280 was never true

281 lsst.utils.tests.init() 

282 unittest.main()