Coverage for tests/test_utils.py: 19%

191 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-01-29 12:26 +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 

37import datetime 

38from astro_metadata_translator import makeObservationInfo 

39from lsst.obs.base import createInitialSkyWcsFromBoresight 

40from lsst.obs.base.makeRawVisitInfoViaObsInfo import MakeRawVisitInfoViaObsInfo 

41from lsst.obs.lsst import Latiss 

42 

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

44 getFieldNameAndTileNumber, 

45 getAirmassSeeingCorrection, 

46 getFilterSeeingCorrection, 

47 quickSmooth, 

48 getQuantiles, 

49 fluxesFromFootprints, 

50 getCurrentDayObs_datetime, 

51 getCurrentDayObs_int, 

52 getCurrentDayObs_humanStr, 

53 ) 

54 

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

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(boresight_airmass=1.5, 

69 temperature=15*u.deg_C, 

70 observation_type="science", 

71 exposure_time=5*u.ks, 

72 detector_num=32, 

73 location=AUXTEL_LOCATION, 

74 ) 

75 

76 def test_getExpPositionOffset(self): 

77 epsilon = 0.0001 

78 ra1s = [0, 45, 90] 

79 ra2s = copy.copy(ra1s) 

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

81 ra1s = np.deg2rad(ra1s) 

82 ra2s = np.deg2rad(ra2s) 

83 

84 epsilon = 0.0001 

85 dec1s = [0, 45, 90] 

86 dec2s = copy.copy(dec1s) 

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

88 

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

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

91 

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

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

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

95 

96 header1 = copy.copy(self.baseHeader) 

97 header2 = copy.copy(self.baseHeader) 

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

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

100 

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

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

103 

104 obsInfo1 = makeObservationInfo(**header1) 

105 obsInfo2 = makeObservationInfo(**header2) 

106 

107 vi1 = self.viMaker.observationInfo2visitInfo(obsInfo1) 

108 vi2 = self.viMaker.observationInfo2visitInfo(obsInfo2) 

109 expInfo1 = afwImage.ExposureInfo() 

110 expInfo1.setVisitInfo(vi1) 

111 expInfo2 = afwImage.ExposureInfo() 

112 expInfo2.setVisitInfo(vi2) 

113 

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

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

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

117 

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

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

120 

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

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

123 

124 exp1.setWcs(wcs1) 

125 exp2.setWcs(wcs2) 

126 

127 result = getExpPositionOffset(exp1, exp2) 

128 

129 deltaRa = ra1 - ra2 

130 deltaDec = dec1 - dec2 

131 

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

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

134 

135 

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

137 

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

214 dayObs = getCurrentDayObs_int() 

215 self.assertIsInstance(dayObs, int) 

216 self.assertLess(dayObs, 21000101) 

217 self.assertGreater(dayObs, 19700101) 

218 

219 def test_getCurrentDayObs_humanStr(self): 

220 """Just a basic formatting check. 

221 """ 

222 dateStr = getCurrentDayObs_humanStr() 

223 self.assertIsInstance(dateStr, str) 

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

225 self.assertRegex(dateStr, r'\d{4}-\d{2}-\d{2}') 

226 

227 

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

229 def setUp(self) -> None: 

230 return super().setUp() 

231 

232 def test_quantiles(self): 

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

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

235 np.random.seed(1234) 

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

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

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

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

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

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

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

243 edges1 = getQuantiles(data, nColors) 

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

245 else: 

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

247 edges1 = getQuantiles(data, nColors) 

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

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

250 

251 

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

253 def test_fluxFromFootprint(self): 

254 image = afwImage.Image( 

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

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

257 dtype="I" 

258 ) 

259 

260 radius = 3 

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

262 footprint1 = afwDetect.Footprint(spans) 

263 

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

265 # spans and the overlapped bow with the image 

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

267 

268 radius = 3 

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

270 footprint2 = afwDetect.Footprint(spans) 

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

272 

273 allFootprints = [footprint1, footprint2] 

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

275 footprintSet.setFootprints(allFootprints) 

276 

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

278 # footprints 

279 with self.assertRaises(TypeError): 

280 fluxesFromFootprints(10, image) 

281 

282 with self.assertRaises(TypeError): 

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

284 

285 # check the footPrintSet 

286 fluxes = fluxesFromFootprints(footprintSet, image) 

287 expectedLength = len(footprintSet.getFootprints()) 

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

289 self.assertIsInstance(fluxes, Iterable) 

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

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

292 

293 # check the list of footprints 

294 fluxes = fluxesFromFootprints(allFootprints, image) 

295 expectedLength = 2 

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

297 self.assertIsInstance(fluxes, Iterable) 

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

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

300 

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

302 # pixels untouched 

303 oldImageArray = copy.deepcopy(image.array) 

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

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

306 

307 

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

309 pass 

310 

311 

312def setup_module(module): 

313 lsst.utils.tests.init() 

314 

315 

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

317 lsst.utils.tests.init() 

318 unittest.main()