Coverage for tests/test_utils.py: 19%

191 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-17 04:43 -0700

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 datetime 

26import itertools 

27import unittest 

28from typing import Iterable 

29 

30import astropy.time 

31import astropy.units as u 

32import numpy as np 

33from astro_metadata_translator import makeObservationInfo 

34 

35import lsst.afw.detection as afwDetect 

36import lsst.afw.geom as afwGeom 

37import lsst.afw.image as afwImage 

38import lsst.geom as geom 

39import lsst.utils.tests 

40from lsst.obs.base import createInitialSkyWcsFromBoresight 

41from lsst.obs.base.makeRawVisitInfoViaObsInfo import MakeRawVisitInfoViaObsInfo 

42from lsst.obs.lsst import Latiss 

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

44from lsst.summit.utils.utils import ( 

45 fluxesFromFootprints, 

46 getAirmassSeeingCorrection, 

47 getCurrentDayObs_datetime, 

48 getCurrentDayObs_humanStr, 

49 getCurrentDayObs_int, 

50 getExpPositionOffset, 

51 getFieldNameAndTileNumber, 

52 getFilterSeeingCorrection, 

53 getQuantiles, 

54 quickSmooth, 

55) 

56 

57 

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

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

60 

61 def setUp(self): 

62 camera = Latiss.getCamera() 

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

64 self.detector = camera[0] 

65 

66 self.viMaker = MakeRawVisitInfoViaObsInfo() 

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

68 self.baseHeader = dict( 

69 boresight_airmass=1.5, 

70 temperature=15 * u.deg_C, 

71 observation_type="science", 

72 exposure_time=5 * u.ks, 

73 detector_num=32, 

74 location=AUXTEL_LOCATION, 

75 ) 

76 

77 def test_getExpPositionOffset(self): 

78 epsilon = 0.0001 

79 ra1s = [0, 45, 90] 

80 ra2s = copy.copy(ra1s) 

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

82 ra1s = np.deg2rad(ra1s) 

83 ra2s = np.deg2rad(ra2s) 

84 

85 epsilon = 0.0001 

86 dec1s = [0, 45, 90] 

87 dec2s = copy.copy(dec1s) 

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

89 

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

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

92 

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

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

95 expTime = astropy.time.TimeDelta(20, format="sec") 

96 

97 header1 = copy.copy(self.baseHeader) 

98 header2 = copy.copy(self.baseHeader) 

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

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

101 

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

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

104 

105 obsInfo1 = makeObservationInfo(**header1) 

106 obsInfo2 = makeObservationInfo(**header2) 

107 

108 vi1 = self.viMaker.observationInfo2visitInfo(obsInfo1) 

109 vi2 = self.viMaker.observationInfo2visitInfo(obsInfo2) 

110 expInfo1 = afwImage.ExposureInfo() 

111 expInfo1.setVisitInfo(vi1) 

112 expInfo2 = afwImage.ExposureInfo() 

113 expInfo2.setVisitInfo(vi2) 

114 

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

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

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

118 

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

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

121 

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

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

124 

125 exp1.setWcs(wcs1) 

126 exp2.setWcs(wcs2) 

127 

128 result = getExpPositionOffset(exp1, exp2) 

129 

130 deltaRa = ra1 - ra2 

131 deltaDec = dec1 - dec2 

132 

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

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

135 

136 

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

138 def setUp(self) -> None: 

139 return super().setUp() 

140 

141 def test_getFieldNameAndTileNumber(self): 

142 field, num = getFieldNameAndTileNumber("simple") 

143 self.assertEqual(field, "simple") 

144 self.assertIsNone(num) 

145 

146 field, num = getFieldNameAndTileNumber("_simple") 

147 self.assertEqual(field, "_simple") 

148 self.assertIsNone(num) 

149 

150 field, num = getFieldNameAndTileNumber("simple_321") 

151 self.assertEqual(field, "simple") 

152 self.assertEqual(num, 321) 

153 

154 field, num = getFieldNameAndTileNumber("_simple_321") 

155 self.assertEqual(field, "_simple") 

156 self.assertEqual(num, 321) 

157 

158 field, num = getFieldNameAndTileNumber("test_321a_123") 

159 self.assertEqual(field, "test_321a") 

160 self.assertEqual(num, 123) 

161 

162 field, num = getFieldNameAndTileNumber("test_321a_123_") 

163 self.assertEqual(field, "test_321a_123_") 

164 self.assertIsNone(num) 

165 

166 field, num = getFieldNameAndTileNumber("test_321a_123a") 

167 self.assertEqual(field, "test_321a_123a") 

168 self.assertIsNone(num) 

169 

170 field, num = getFieldNameAndTileNumber("test_321a:asd_asd-dsa_321") 

171 self.assertEqual(field, "test_321a:asd_asd-dsa") 

172 self.assertEqual(num, 321) 

173 

174 def test_getAirmassSeeingCorrection(self): 

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

176 correction = getAirmassSeeingCorrection(airmass) 

177 self.assertGreater(correction, 0.01) 

178 self.assertLess(correction, 1.0) 

179 

180 correction = getAirmassSeeingCorrection(1) 

181 self.assertEqual(correction, 1.0) 

182 

183 with self.assertRaises(ValueError): 

184 getAirmassSeeingCorrection(0.5) 

185 

186 def test_getFilterSeeingCorrection(self): 

187 for filterName in ("SDSSg_65mm", "SDSSr_65mm", "SDSSi_65mm"): 

188 correction = getFilterSeeingCorrection(filterName) 

189 self.assertGreater(correction, 0.5) 

190 self.assertLess(correction, 1.5) 

191 

192 def test_quickSmooth(self): 

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

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

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

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

197 data = quickSmooth(data, 5.0) 

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

199 

200 def test_getCurrentDayObs_datetime(self): 

201 """Just a type check and a basic sanity check on the range. 

202 

203 Setting days=3 as the tolerance just because of timezones and who knows 

204 what really. 

205 """ 

206 dt = getCurrentDayObs_datetime() 

207 self.assertIsInstance(dt, datetime.date) 

208 self.assertLess(dt, datetime.date.today() + datetime.timedelta(days=3)) 

209 self.assertGreater(dt, datetime.date.today() - datetime.timedelta(days=3)) 

210 

211 def test_getCurrentDayObs_int(self): 

212 """Just a type check and a basic sanity check on the range.""" 

213 dayObs = getCurrentDayObs_int() 

214 self.assertIsInstance(dayObs, int) 

215 self.assertLess(dayObs, 21000101) 

216 self.assertGreater(dayObs, 19700101) 

217 

218 def test_getCurrentDayObs_humanStr(self): 

219 """Just a basic formatting check.""" 

220 dateStr = getCurrentDayObs_humanStr() 

221 self.assertIsInstance(dateStr, str) 

222 self.assertEqual(len(dateStr), 10) 

223 self.assertRegex(dateStr, r"\d{4}-\d{2}-\d{2}") 

224 

225 

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

227 def setUp(self) -> None: 

228 return super().setUp() 

229 

230 def test_quantiles(self): 

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

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

233 np.random.seed(1234) 

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

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

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

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

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

239 if np.nanmax(data) - np.nanmin(data) > 300_000: 

240 with self.assertLogs(level="WARNING") as cm: 

241 edges1 = getQuantiles(data, nColors) 

242 self.assertIn("Data range is very large", cm.output[0]) 

243 else: 

244 with self.assertNoLogs(level="WARNING") as cm: 

245 edges1 = getQuantiles(data, nColors) 

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

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

248 

249 

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

251 def test_fluxFromFootprint(self): 

252 image = afwImage.Image( 

253 np.arange(8100, dtype=np.int32).reshape(90, 90), xy0=lsst.geom.Point2I(10, 12), dtype="I" 

254 ) 

255 

256 radius = 3 

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

258 footprint1 = afwDetect.Footprint(spans) 

259 

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

261 # spans and the overlapped bow with the image 

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

263 

264 radius = 3 

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

266 footprint2 = afwDetect.Footprint(spans) 

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

268 

269 allFootprints = [footprint1, footprint2] 

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

271 footprintSet.setFootprints(allFootprints) 

272 

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

274 # footprints 

275 with self.assertRaises(TypeError): 

276 fluxesFromFootprints(10, image) 

277 

278 with self.assertRaises(TypeError): 

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

280 

281 # check the footPrintSet 

282 fluxes = fluxesFromFootprints(footprintSet, image) 

283 expectedLength = len(footprintSet.getFootprints()) 

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

285 self.assertIsInstance(fluxes, Iterable) 

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

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

288 

289 # check the list of footprints 

290 fluxes = fluxesFromFootprints(allFootprints, image) 

291 expectedLength = 2 

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

293 self.assertIsInstance(fluxes, Iterable) 

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

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

296 

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

298 # pixels untouched 

299 oldImageArray = copy.deepcopy(image.array) 

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

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

302 

303 

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

305 pass 

306 

307 

308def setup_module(module): 

309 lsst.utils.tests.init() 

310 

311 

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

313 lsst.utils.tests.init() 

314 unittest.main()