Coverage for tests/test_utils.py: 19%
191 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-10 05:49 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-10 05:49 -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/>.
22"""Test cases for utils."""
24import copy
25import datetime
26import itertools
27import unittest
28from typing import Iterable
30import astropy.time
31import astropy.units as u
32import numpy as np
33from astro_metadata_translator import makeObservationInfo
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)
58class ExpSkyPositionOffsetTestCase(lsst.utils.tests.TestCase):
59 """A test case for testing sky position offsets for exposures."""
61 def setUp(self):
62 camera = Latiss.getCamera()
63 self.assertTrue(len(camera) == 1)
64 self.detector = camera[0]
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 )
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)
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
90 rotAngle1 = geom.Angle(43.2, geom.degrees) # arbitrary non-zero
91 rotAngle2 = geom.Angle(56.7, geom.degrees)
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")
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")
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")
105 obsInfo1 = makeObservationInfo(**header1)
106 obsInfo2 = makeObservationInfo(**header2)
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)
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)
119 wcs1 = createInitialSkyWcsFromBoresight(pos1, rotAngle1, self.detector, flipX=True)
120 wcs2 = createInitialSkyWcsFromBoresight(pos2, rotAngle2, self.detector, flipX=True)
122 exp1 = afwImage.ExposureF(self.mi, expInfo1)
123 exp2 = afwImage.ExposureF(self.mi, expInfo2)
125 exp1.setWcs(wcs1)
126 exp2.setWcs(wcs2)
128 result = getExpPositionOffset(exp1, exp2)
130 deltaRa = ra1 - ra2
131 deltaDec = dec1 - dec2
133 self.assertAlmostEqual(result.deltaRa.asDegrees(), deltaRa, 6)
134 self.assertAlmostEqual(result.deltaDec.asDegrees(), deltaDec, 6)
137class MiscUtilsTestCase(lsst.utils.tests.TestCase):
138 def setUp(self) -> None:
139 return super().setUp()
141 def test_getFieldNameAndTileNumber(self):
142 field, num = getFieldNameAndTileNumber("simple")
143 self.assertEqual(field, "simple")
144 self.assertIsNone(num)
146 field, num = getFieldNameAndTileNumber("_simple")
147 self.assertEqual(field, "_simple")
148 self.assertIsNone(num)
150 field, num = getFieldNameAndTileNumber("simple_321")
151 self.assertEqual(field, "simple")
152 self.assertEqual(num, 321)
154 field, num = getFieldNameAndTileNumber("_simple_321")
155 self.assertEqual(field, "_simple")
156 self.assertEqual(num, 321)
158 field, num = getFieldNameAndTileNumber("test_321a_123")
159 self.assertEqual(field, "test_321a")
160 self.assertEqual(num, 123)
162 field, num = getFieldNameAndTileNumber("test_321a_123_")
163 self.assertEqual(field, "test_321a_123_")
164 self.assertIsNone(num)
166 field, num = getFieldNameAndTileNumber("test_321a_123a")
167 self.assertEqual(field, "test_321a_123a")
168 self.assertIsNone(num)
170 field, num = getFieldNameAndTileNumber("test_321a:asd_asd-dsa_321")
171 self.assertEqual(field, "test_321a:asd_asd-dsa")
172 self.assertEqual(num, 321)
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)
180 correction = getAirmassSeeingCorrection(1)
181 self.assertEqual(correction, 1.0)
183 with self.assertRaises(ValueError):
184 getAirmassSeeingCorrection(0.5)
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)
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))
200 def test_getCurrentDayObs_datetime(self):
201 """Just a type check and a basic sanity check on the range.
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))
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)
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}")
226class QuantileTestCase(lsst.utils.tests.TestCase):
227 def setUp(self) -> None:
228 return super().setUp()
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)
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 )
256 radius = 3
257 spans = afwGeom.SpanSet.fromShape(radius, afwGeom.Stencil.CIRCLE, offset=(27, 30))
258 footprint1 = afwDetect.Footprint(spans)
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]
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]
269 allFootprints = [footprint1, footprint2]
270 footprintSet = afwDetect.FootprintSet(image.getBBox())
271 footprintSet.setFootprints(allFootprints)
273 # check it can accept a footprintSet, and single and iterables of
274 # footprints
275 with self.assertRaises(TypeError):
276 fluxesFromFootprints(10, image)
278 with self.assertRaises(TypeError):
279 fluxesFromFootprints([8, 6, 7, 5, 3, 0, 9], image)
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))
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))
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)
304class TestMemory(lsst.utils.tests.MemoryTestCase):
305 pass
308def setup_module(module):
309 lsst.utils.tests.init()
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()