Coverage for tests/test_utils.py: 20%
186 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-17 11:44 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-17 11:44 +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/>.
22"""Test cases for utils."""
24import copy
25import itertools
26from typing import Iterable
27import unittest
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
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 )
55from lsst.obs.lsst.translators.latiss import AUXTEL_LOCATION
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(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 )
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)
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
89 rotAngle1 = geom.Angle(43.2, geom.degrees) # arbitrary non-zero
90 rotAngle2 = geom.Angle(56.7, geom.degrees)
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')
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")
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")
104 obsInfo1 = makeObservationInfo(**header1)
105 obsInfo2 = makeObservationInfo(**header2)
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)
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)
118 wcs1 = createInitialSkyWcsFromBoresight(pos1, rotAngle1, self.detector, flipX=True)
119 wcs2 = createInitialSkyWcsFromBoresight(pos2, rotAngle2, self.detector, flipX=True)
121 exp1 = afwImage.ExposureF(self.mi, expInfo1)
122 exp2 = afwImage.ExposureF(self.mi, expInfo2)
124 exp1.setWcs(wcs1)
125 exp2.setWcs(wcs2)
127 result = getExpPositionOffset(exp1, exp2)
129 deltaRa = ra1 - ra2
130 deltaDec = dec1 - dec2
132 self.assertAlmostEqual(result.deltaRa.asDegrees(), deltaRa, 6)
133 self.assertAlmostEqual(result.deltaDec.asDegrees(), deltaDec, 6)
136class 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 """
214 dayObs = getCurrentDayObs_int()
215 self.assertIsInstance(dayObs, int)
216 self.assertLess(dayObs, 21000101)
217 self.assertGreater(dayObs, 19700101)
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}')
228class QuantileTestCase(lsst.utils.tests.TestCase):
229 def setUp(self) -> None:
230 return super().setUp()
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 # too big of a width violates the tolerance in the test to cap at 10k
237 dataRanges = [(50, 1, -1), (100_000, 5_000, -1), (5_000_000, 10_000, -2)]
238 colorRanges = [2, 256, 999] # [very few, nominal, lots and an odd number]
239 for nColors, (mean, width, decimal) in itertools.product(colorRanges, dataRanges):
240 data = np.random.normal(mean, width, (100, 100))
241 data[10, 10] = np.nan # check we're still nan-safe
242 edges1 = getQuantiles(data, nColors)
243 edges2 = np.nanquantile(data, np.linspace(0, 1, nColors + 1)) # must check with nanquantile
244 np.testing.assert_almost_equal(edges1, edges2, decimal=decimal)
247class ImageBasedTestCase(lsst.utils.tests.TestCase):
248 def test_fluxFromFootprint(self):
249 image = afwImage.Image(
250 np.arange(8100, dtype=np.int32).reshape(90, 90),
251 xy0=lsst.geom.Point2I(10, 12),
252 dtype="I"
253 )
255 radius = 3
256 spans = afwGeom.SpanSet.fromShape(radius, afwGeom.Stencil.CIRCLE, offset=(27, 30))
257 footprint1 = afwDetect.Footprint(spans)
259 # The extracted footprint should be the same as the product of the
260 # spans and the overlapped bow with the image
261 truth1 = spans.asArray() * image.array[15:22, 14:21]
263 radius = 3
264 spans = afwGeom.SpanSet.fromShape(radius, afwGeom.Stencil.CIRCLE, offset=(44, 49))
265 footprint2 = afwDetect.Footprint(spans)
266 truth2 = spans.asArray() * image.array[34:41, 31:38]
268 allFootprints = [footprint1, footprint2]
269 footprintSet = afwDetect.FootprintSet(image.getBBox())
270 footprintSet.setFootprints(allFootprints)
272 # check it can accept a footprintSet, and single and iterables of
273 # footprints
274 with self.assertRaises(TypeError):
275 fluxesFromFootprints(10, image)
277 with self.assertRaises(TypeError):
278 fluxesFromFootprints([8, 6, 7, 5, 3, 0, 9], image)
280 # check the footPrintSet
281 fluxes = fluxesFromFootprints(footprintSet, image)
282 expectedLength = len(footprintSet.getFootprints())
283 self.assertEqual(len(fluxes), expectedLength) # always one flux per footprint
284 self.assertIsInstance(fluxes, Iterable)
285 self.assertAlmostEqual(fluxes[0], np.sum(truth1))
286 self.assertAlmostEqual(fluxes[1], np.sum(truth2))
288 # check the list of footprints
289 fluxes = fluxesFromFootprints(allFootprints, image)
290 expectedLength = 2
291 self.assertEqual(len(fluxes), expectedLength) # always one flux per footprint
292 self.assertIsInstance(fluxes, Iterable)
293 self.assertAlmostEqual(fluxes[0], np.sum(truth1))
294 self.assertAlmostEqual(fluxes[1], np.sum(truth2))
296 # ensure that subtracting the image median from fluxes leave image
297 # pixels untouched
298 oldImageArray = copy.deepcopy(image.array)
299 fluxes = fluxesFromFootprints(footprintSet, image, subtractImageMedian=True)
300 np.testing.assert_array_equal(image.array, oldImageArray)
303class TestMemory(lsst.utils.tests.MemoryTestCase):
304 pass
307def setup_module(module):
308 lsst.utils.tests.init()
311if __name__ == "__main__": 311 ↛ 312line 311 didn't jump to line 312, because the condition on line 311 was never true
312 lsst.utils.tests.init()
313 unittest.main()