Coverage for tests/test_fitsRawFormatter.py: 37%

95 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-20 09:20 +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/>. 

21 

22import unittest 

23 

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 

45 

46 

47class SimpleTestingTranslator(FitsTranslator, StubTranslator): 

48 """Simple `~astro_metadata_translator.MetadataTranslator` used for 

49 testing. 

50 """ 

51 

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

77 

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

82 

83 

84class MakeTestingRawVisitInfo(MakeRawVisitInfoViaObsInfo): 

85 """Test class for VisitInfo creation.""" 

86 

87 metadataTranslator = SimpleTestingTranslator 

88 

89 

90class SimpleFitsRawFormatter(FitsRawFormatterBase): 

91 """Simple test formatter for datastore interaction.""" 

92 

93 filterDefinitions = FilterDefinitionCollection(FilterDefinition(physical_filter="u", band="u")) 

94 

95 @property 

96 def translatorClass(self): 

97 return SimpleTestingTranslator 

98 

99 def getDetector(self, id): 

100 """Use CameraWrapper to create a fake detector that can map from 

101 PIXELS to FIELD_ANGLE. 

102 

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

108 

109 

110class FitsRawFormatterTestCase(lsst.utils.tests.TestCase): 

111 """Test that we can read and write FITS files with butler.""" 

112 

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) 

144 

145 maker = MakeTestingRawVisitInfo() 

146 self.visitInfo = maker(self.header) 

147 

148 self.metadataSkyWcs = lsst.afw.geom.makeSkyWcs(self.metadata, strip=False) 

149 self.boresightSkyWcs = createInitialSkyWcs(self.visitInfo, CameraWrapper().camera.get(10)) 

150 

151 # set this to `contextlib.nullcontext()` to print the log warnings 

152 self.warnContext = self.assertLogs(level="WARNING") 

153 

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 ) 

159 

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 

165 

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) 

171 

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) 

179 

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) 

187 

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) 

194 

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) 

202 

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 # We could try transformed amplifiers here that involve flips 

234 # and offsets, but: 

235 # - we already test the low-level code that does that in afw; 

236 # - we test very similar high-level code (which calls that 

237 # same afw code) in the non-raw Exposure formatter, in 

238 # test_butlerFits.py; 

239 # - the only instruments that actually have those kinds of 

240 # amplifiers are those in obs_lsst, and that has a different 

241 # raw formatter implementation that we need to test there 

242 # anyway; 

243 # - these are kind of expensive tests. 

244 

245 

246class MemoryTester(lsst.utils.tests.MemoryTestCase): 

247 """Check for file leaks.""" 

248 

249 

250def setup_module(module): 

251 """Initialize pytest.""" 

252 lsst.utils.tests.init() 

253 

254 

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

256 lsst.utils.tests.init() 

257 unittest.main()