Coverage for tests/test_fitsRawFormatter.py: 36%
96 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-30 12:18 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-30 12:18 +0000
1# This file is part of obs_base.
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/>.
22import unittest
24import astropy.units as u
25import lsst.afw.geom
26import lsst.afw.math
27import lsst.daf.base
28import lsst.daf.butler
29import lsst.geom
30import lsst.resources
31import lsst.utils.tests
32from astro_metadata_translator import FitsTranslator, StubTranslator
33from astro_metadata_translator.translators.helpers import tracking_from_degree_headers
34from astropy.coordinates import Angle
35from lsst.afw.cameraGeom import makeUpdatedDetector
36from lsst.afw.cameraGeom.testUtils import CameraWrapper, DetectorWrapper
37from lsst.obs.base import (
38 FilterDefinition,
39 FilterDefinitionCollection,
40 FitsRawFormatterBase,
41 MakeRawVisitInfoViaObsInfo,
42)
43from lsst.obs.base.tests import make_ramp_exposure_untrimmed
44from lsst.obs.base.utils import InitialSkyWcsError, createInitialSkyWcs
47class SimpleTestingTranslator(FitsTranslator, StubTranslator):
48 """Simple `~astro_metadata_translator.MetadataTranslator` used for
49 testing.
50 """
52 _const_map = {
53 "boresight_rotation_angle": Angle(90 * u.deg),
54 "boresight_rotation_coord": "sky",
55 "detector_exposure_id": 12345,
56 # The following are defined to prevent warnings about
57 # undefined translators
58 "dark_time": 0.0 * u.s,
59 "exposure_time": 0.0 * u.s,
60 "physical_filter": "u",
61 "detector_num": 0,
62 "detector_name": "0",
63 "detector_group": "",
64 "detector_unique_name": "0",
65 "detector_serial": "",
66 "observation_id": "--",
67 "science_program": "unknown",
68 "object": "unknown",
69 "exposure_id": 0,
70 "visit_id": 0,
71 "relative_humidity": 30.0,
72 "pressure": 0.0 * u.MPa,
73 "temperature": 273 * u.K,
74 "altaz_begin": None,
75 }
76 _trivial_map = {"boresight_airmass": "AIRMASS", "observation_type": "OBSTYPE"}
78 def to_tracking_radec(self):
79 radecsys = ("RADESYS",)
80 radecpairs = (("RA", "DEC"),)
81 return tracking_from_degree_headers(self, radecsys, radecpairs, unit=(u.deg, u.deg))
84class MakeTestingRawVisitInfo(MakeRawVisitInfoViaObsInfo):
85 """Test class for VisitInfo creation."""
87 metadataTranslator = SimpleTestingTranslator
90class SimpleFitsRawFormatter(FitsRawFormatterBase):
91 """Simple test formatter for datastore interaction."""
93 filterDefinitions = FilterDefinitionCollection(FilterDefinition(physical_filter="u", band="u"))
95 @property
96 def translatorClass(self):
97 return SimpleTestingTranslator
99 def getDetector(self, id):
100 """Use CameraWrapper to create a fake detector that can map from
101 PIXELS to FIELD_ANGLE.
103 Always return Detector #10, so all the tests are self-consistent, and
104 make sure it is in "assembled" form, since that's what the base
105 formatter implementations assume.
106 """
107 return makeUpdatedDetector(CameraWrapper().camera.get(10))
110class FitsRawFormatterTestCase(lsst.utils.tests.TestCase):
111 """Test that we can read and write FITS files with butler."""
113 def setUp(self):
114 # The FITS WCS and VisitInfo coordinates in this header are
115 # intentionally different, to make comparisons between them more
116 # obvious.
117 self.boresight = lsst.geom.SpherePoint(10.0, 20.0, lsst.geom.degrees)
118 self.header = {
119 "TELESCOP": "TEST",
120 "INSTRUME": "UNKNOWN",
121 "AIRMASS": 1.2,
122 "RADESYS": "ICRS",
123 "OBSTYPE": "science",
124 "EQUINOX": 2000,
125 "OBSGEO-X": "-5464588.84421314",
126 "OBSGEO-Y": "-2493000.19137644",
127 "OBSGEO-Z": "2150653.35350771",
128 "RA": self.boresight.getLatitude().asDegrees(),
129 "DEC": self.boresight.getLongitude().asDegrees(),
130 "CTYPE1": "RA---SIN",
131 "CTYPE2": "DEC--SIN",
132 "CRPIX1": 5,
133 "CRPIX2": 6,
134 "CRVAL1": self.boresight.getLatitude().asDegrees() + 1,
135 "CRVAL2": self.boresight.getLongitude().asDegrees() + 1,
136 "CD1_1": 1e-5,
137 "CD1_2": 0,
138 "CD2_2": 1e-5,
139 "CD2_1": 0,
140 }
141 # make a property list of the above, for use by the formatter.
142 self.metadata = lsst.daf.base.PropertyList()
143 self.metadata.update(self.header)
145 maker = MakeTestingRawVisitInfo()
146 self.visitInfo = maker(self.header)
148 self.metadataSkyWcs = lsst.afw.geom.makeSkyWcs(self.metadata, strip=False)
149 self.boresightSkyWcs = createInitialSkyWcs(self.visitInfo, CameraWrapper().camera.get(10))
151 # set this to `contextlib.nullcontext()` to print the log warnings
152 self.warnContext = self.assertLogs(level="WARNING")
154 # Make a data ID to pass to the formatter.
155 universe = lsst.daf.butler.DimensionUniverse()
156 dataId = lsst.daf.butler.DataCoordinate.standardize(
157 instrument="Cam1", exposure=2, detector=10, physical_filter="u", band="u", universe=universe
158 )
160 # We have no file in these tests, so make an empty descriptor.
161 fileDescriptor = lsst.daf.butler.FileDescriptor(None, None)
162 self.formatter = SimpleFitsRawFormatter(fileDescriptor, dataId)
163 # Force the formatter's metadata to be what we've created above.
164 self.formatter._metadata = self.metadata
166 def test_makeWcs(self):
167 detector = self.formatter.getDetector(1)
168 wcs = self.formatter.makeWcs(self.visitInfo, detector)
169 self.assertNotEqual(wcs, self.metadataSkyWcs)
170 self.assertEqual(wcs, self.boresightSkyWcs)
172 def test_makeWcs_if_metadata_is_bad(self):
173 """Always use the VisitInfo WCS if available."""
174 detector = self.formatter.getDetector(1)
175 self.metadata.remove("CTYPE1")
176 wcs = self.formatter.makeWcs(self.visitInfo, detector)
177 self.assertNotEqual(wcs, self.metadataSkyWcs)
178 self.assertEqual(wcs, self.boresightSkyWcs)
180 def test_makeWcs_warn_if_visitInfo_is_None(self):
181 """If VisitInfo is None, log a warning and use the metadata WCS."""
182 detector = self.formatter.getDetector(1)
183 with self.warnContext:
184 wcs = self.formatter.makeWcs(None, detector)
185 self.assertEqual(wcs, self.metadataSkyWcs)
186 self.assertNotEqual(wcs, self.boresightSkyWcs)
188 def test_makeWcs_fail_if_visitInfo_is_None(self):
189 """If VisitInfo is None and metadata failed, raise an exception."""
190 detector = self.formatter.getDetector(1)
191 self.metadata.remove("CTYPE1")
192 with self.warnContext, self.assertRaises(InitialSkyWcsError):
193 self.formatter.makeWcs(None, detector)
195 def test_makeWcs_fail_if_detector_is_bad(self):
196 """If Detector is broken, raise an exception."""
197 # This detector doesn't know about FIELD_ANGLE, so can't be used to
198 # make a SkyWcs.
199 detector = DetectorWrapper().detector
200 with self.assertRaises(InitialSkyWcsError):
201 self.formatter.makeWcs(self.visitInfo, detector)
203 def test_amp_parameter(self):
204 """Test loading subimages with the 'amp' parameter."""
205 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile:
206 # Get a detector; this must be the same one that's baked into the
207 # simple formatter at the top of this file, so that's how we get
208 # it.
209 detector = self.formatter.getDetector(1)
210 # Make full exposure with ramp values and save just the image to
211 # the temp file (with metadata), so it looks like a raw.
212 full = make_ramp_exposure_untrimmed(detector)
213 full.image.writeFits(tmpFile, metadata=self.metadata)
214 # Loop over amps and try to read them via the formatter.
215 for n, amp in enumerate(detector):
216 for amp_parameter in [amp, amp.getName(), n]:
217 for parameters in [{"amp": amp_parameter}, {"amp": amp_parameter, "detector": detector}]:
218 with self.subTest(parameters=parameters):
219 # Make a new formatter that points at the new file
220 # and has the right parameters.
221 formatter = SimpleFitsRawFormatter(
222 lsst.daf.butler.FileDescriptor(
223 lsst.daf.butler.Location(None, path=lsst.resources.ResourcePath(tmpFile)),
224 lsst.daf.butler.StorageClassFactory().getStorageClass("ExposureI"),
225 parameters=parameters,
226 ),
227 self.formatter.dataId,
228 )
229 subexp = formatter.read()
230 self.assertImagesEqual(subexp.image, full[amp.getRawBBox()].image)
231 self.assertEqual(len(subexp.getDetector()), 1)
232 self.assertAmplifiersEqual(subexp.getDetector()[0], amp)
233 self.assertEqual(subexp.visitInfo.id, 2)
234 # We could try transformed amplifiers here that involve flips
235 # and offsets, but:
236 # - we already test the low-level code that does that in afw;
237 # - we test very similar high-level code (which calls that
238 # same afw code) in the non-raw Exposure formatter, in
239 # test_butlerFits.py;
240 # - the only instruments that actually have those kinds of
241 # amplifiers are those in obs_lsst, and that has a different
242 # raw formatter implementation that we need to test there
243 # anyway;
244 # - these are kind of expensive tests.
247class MemoryTester(lsst.utils.tests.MemoryTestCase):
248 """Check for file leaks."""
251def setup_module(module):
252 """Initialize pytest."""
253 lsst.utils.tests.init()
256if __name__ == "__main__": 256 ↛ 257line 256 didn't jump to line 257, because the condition on line 256 was never true
257 lsst.utils.tests.init()
258 unittest.main()