Coverage for tests/test_butlerFits.py : 19%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://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 <http://www.gnu.org/licenses/>.
22from __future__ import annotations
24import os
25import unittest
26import tempfile
27import shutil
29from typing import TYPE_CHECKING
31import lsst.utils.tests
33import lsst.pex.config
34import lsst.afw.image
35from lsst.afw.image import LOCAL
36from lsst.afw.fits import readMetadata
37from lsst.geom import Box2I, Point2I
38from lsst.base import Packages
39from lsst.daf.base import PropertyList, PropertySet
41from lsst.daf.butler import Config
42from lsst.daf.butler import StorageClassFactory
43from lsst.daf.butler import DatasetType
44from lsst.daf.butler.tests import DatasetTestHelper, makeTestRepo, addDatasetType, makeTestCollection
46from lsst.obs.base.exposureAssembler import ExposureAssembler
48if TYPE_CHECKING: 48 ↛ 49line 48 didn't jump to line 49, because the condition on line 48 was never true
49 from lsst.daf.butler import DatasetRef
51TESTDIR = os.path.dirname(__file__)
53BUTLER_CONFIG = """
54storageClasses:
55 ExposureCompositeF:
56 inheritsFrom: ExposureF
57datastore:
58 # Want to check disassembly so can't use InMemory
59 cls: lsst.daf.butler.datastores.fileDatastore.FileDatastore
60 formatters:
61 ExposureCompositeF: lsst.obs.base.formatters.fitsExposure.FitsExposureFormatter
62 lossless:
63 formatter: lsst.obs.base.formatters.fitsExposure.FitsExposureFormatter
64 parameters:
65 recipe: lossless
66 uncompressed:
67 formatter: lsst.obs.base.formatters.fitsExposure.FitsExposureFormatter
68 parameters:
69 recipe: noCompression
70 lossy:
71 formatter: lsst.obs.base.formatters.fitsExposure.FitsExposureFormatter
72 parameters:
73 recipe: lossyBasic
74 composites:
75 disassembled:
76 ExposureCompositeF: True
77"""
79# Components present in the test file
80COMPONENTS = {"wcs", "image", "mask", "coaddInputs", "psf", "visitInfo", "variance", "metadata", "photoCalib",
81 "filterLabel", "validPolygon", "transmissionCurve", "detector", "apCorrMap", "summaryStats"}
82READ_COMPONENTS = {"bbox", "xy0", "dimensions", "filter"}
85class SimpleConfig(lsst.pex.config.Config):
86 """Config to use in tests for butler put/get"""
87 i = lsst.pex.config.Field("integer test", int)
88 c = lsst.pex.config.Field("string", str)
91class ButlerFitsTests(DatasetTestHelper, lsst.utils.tests.TestCase):
93 @classmethod
94 def setUpClass(cls):
95 """Create a new butler once only."""
97 cls.storageClassFactory = StorageClassFactory()
99 cls.root = tempfile.mkdtemp(dir=TESTDIR)
101 dataIds = {
102 "instrument": ["DummyCam"],
103 "physical_filter": ["d-r"],
104 "visit": [42, 43, 44],
105 }
107 # Ensure that we test in a directory that will include some
108 # metacharacters
109 subdir = "sub?#dir"
110 butlerRoot = os.path.join(cls.root, subdir)
112 cls.creatorButler = makeTestRepo(butlerRoot, dataIds, config=Config.fromYaml(BUTLER_CONFIG))
114 # Create dataset types used by the tests
115 for datasetTypeName, storageClassName in (("calexp", "ExposureF"),
116 ("unknown", "ExposureCompositeF"),
117 ("testCatalog", "SourceCatalog"),
118 ("lossless", "ExposureF"),
119 ("uncompressed", "ExposureF"),
120 ("lossy", "ExposureF"),
121 ):
122 storageClass = cls.storageClassFactory.getStorageClass(storageClassName)
123 addDatasetType(cls.creatorButler, datasetTypeName, set(dataIds), storageClass)
125 # And some dataset types that have no dimensions for easy testing
126 for datasetTypeName, storageClassName in (("ps", "PropertySet"),
127 ("pl", "PropertyList"),
128 ("pkg", "Packages"),
129 ("config", "Config"),
130 ):
131 storageClass = cls.storageClassFactory.getStorageClass(storageClassName)
132 addDatasetType(cls.creatorButler, datasetTypeName, {}, storageClass)
134 @classmethod
135 def tearDownClass(cls):
136 if cls.root is not None:
137 shutil.rmtree(cls.root, ignore_errors=True)
139 def setUp(self):
140 self.butler = makeTestCollection(self.creatorButler)
142 def makeExampleCatalog(self) -> lsst.afw.table.SourceCatalog:
143 catalogPath = os.path.join(TESTDIR, "data", "source_catalog.fits")
144 return lsst.afw.table.SourceCatalog.readFits(catalogPath)
146 def assertCatalogEqual(self, inputCatalog: lsst.afw.table.SourceCatalog,
147 outputCatalog: lsst.afw.table.SourceCatalog) -> None:
148 self.assertIsInstance(outputCatalog, lsst.afw.table.SourceCatalog)
149 inputTable = inputCatalog.getTable()
150 inputRecord = inputCatalog[0]
151 outputTable = outputCatalog.getTable()
152 outputRecord = outputCatalog[0]
153 self.assertEqual(inputRecord.getPsfInstFlux(), outputRecord.getPsfInstFlux())
154 self.assertEqual(inputRecord.getPsfFluxFlag(), outputRecord.getPsfFluxFlag())
155 self.assertEqual(inputTable.getSchema().getAliasMap().get("slot_Centroid"),
156 outputTable.getSchema().getAliasMap().get("slot_Centroid"))
157 self.assertEqual(inputRecord.getCentroid(), outputRecord.getCentroid())
158 self.assertFloatsAlmostEqual(
159 inputRecord.getCentroidErr()[0, 0],
160 outputRecord.getCentroidErr()[0, 0], rtol=1e-6)
161 self.assertFloatsAlmostEqual(
162 inputRecord.getCentroidErr()[1, 1],
163 outputRecord.getCentroidErr()[1, 1], rtol=1e-6)
164 self.assertEqual(inputTable.getSchema().getAliasMap().get("slot_Shape"),
165 outputTable.getSchema().getAliasMap().get("slot_Shape"))
166 self.assertFloatsAlmostEqual(
167 inputRecord.getShapeErr()[0, 0],
168 outputRecord.getShapeErr()[0, 0], rtol=1e-6)
169 self.assertFloatsAlmostEqual(
170 inputRecord.getShapeErr()[1, 1],
171 outputRecord.getShapeErr()[1, 1], rtol=1e-6)
172 self.assertFloatsAlmostEqual(
173 inputRecord.getShapeErr()[2, 2],
174 outputRecord.getShapeErr()[2, 2], rtol=1e-6)
176 def runFundamentalTypeTest(self, datasetTypeName, entity):
177 """Put and get the supplied entity and compare."""
178 ref = self.butler.put(entity, datasetTypeName)
179 butler_ps = self.butler.get(ref)
180 self.assertEqual(butler_ps, entity)
182 # Break the contact by ensuring that we are writing YAML
183 uri = self.butler.getURI(ref)
184 self.assertTrue(uri.path.endswith(".yaml"), f"Check extension of {uri}")
186 def testFundamentalTypes(self) -> None:
187 """Ensure that some fundamental stack types round trip."""
188 ps = PropertySet()
189 ps["a.b"] = 5
190 ps["c.d.e"] = "string"
191 self.runFundamentalTypeTest("ps", ps)
193 pl = PropertyList()
194 pl["A"] = 1
195 pl.setComment("A", "An int comment")
196 pl["B"] = "string"
197 pl.setComment("B", "A string comment")
198 self.runFundamentalTypeTest("pl", pl)
200 pkg = Packages.fromSystem()
201 self.runFundamentalTypeTest("pkg", pkg)
203 def testPexConfig(self) -> None:
204 """Test that we can put and get pex_config Configs"""
205 c = SimpleConfig(i=10, c="hello")
206 self.assertEqual(c.i, 10)
207 ref = self.butler.put(c, "config")
208 butler_c = self.butler.get(ref)
209 self.assertEqual(c, butler_c)
210 self.assertIsInstance(butler_c, SimpleConfig)
212 def testFitsCatalog(self) -> None:
213 """Test reading of a FITS catalog"""
214 catalog = self.makeExampleCatalog()
215 dataId = {"visit": 42, "instrument": "DummyCam", "physical_filter": "d-r"}
216 ref = self.butler.put(catalog, "testCatalog", dataId)
217 stored = self.butler.get(ref)
218 self.assertCatalogEqual(catalog, stored)
220 def testExposureCompositePutGetConcrete(self) -> None:
221 """Test composite with no disassembly"""
222 ref = self.runExposureCompositePutGetTest("calexp")
224 uri = self.butler.getURI(ref)
225 self.assertTrue(uri.exists(), f"Checking URI {uri} existence")
227 def testExposureCompositePutGetVirtual(self) -> None:
228 """Testing composite disassembly"""
229 ref = self.runExposureCompositePutGetTest("unknown")
231 primary, components = self.butler.getURIs(ref)
232 self.assertIsNone(primary)
233 self.assertEqual(set(components), COMPONENTS)
234 for compName, uri in components.items():
235 self.assertTrue(uri.exists(),
236 f"Checking URI {uri} existence for component {compName}")
238 def runExposureCompositePutGetTest(self, datasetTypeName: str) -> DatasetRef:
239 example = os.path.join(TESTDIR, "data", "calexp.fits")
240 exposure = lsst.afw.image.ExposureF(example)
242 dataId = {"visit": 42, "instrument": "DummyCam", "physical_filter": "d-r"}
243 ref = self.butler.put(exposure, datasetTypeName, dataId)
245 # Get the full thing
246 composite = self.butler.get(datasetTypeName, dataId)
248 # There is no assert for Exposure so just look at maskedImage
249 self.assertMaskedImagesEqual(composite.maskedImage, exposure.maskedImage)
251 # Helper for extracting components
252 assembler = ExposureAssembler(ref.datasetType.storageClass)
254 # Check all possible components that can be read
255 allComponents = set()
256 allComponents.update(COMPONENTS, READ_COMPONENTS)
258 # Get each component from butler independently
259 for compName in allComponents:
260 compTypeName = DatasetType.nameWithComponent(datasetTypeName, compName)
261 component = self.butler.get(compTypeName, dataId)
263 reference = assembler.getComponent(exposure, compName)
265 self.assertIsInstance(component, type(reference), f"Checking type of component {compName}")
267 if compName in ("image", "variance"):
268 self.assertImagesEqual(component, reference)
269 elif compName == "mask":
270 self.assertMasksEqual(component, reference)
271 elif compName == "wcs":
272 self.assertWcsAlmostEqualOverBBox(component, reference, exposure.getBBox())
273 elif compName == "coaddInputs":
274 self.assertEqual(len(component.visits), len(reference.visits),
275 f"cf visits {component.visits}")
276 self.assertEqual(len(component.ccds), len(reference.ccds),
277 f"cf CCDs {component.ccds}")
278 elif compName == "psf":
279 # Equality for PSF does not work
280 pass
281 elif compName == "filter":
282 self.assertEqual(component.getCanonicalName(), reference.getCanonicalName())
283 elif compName == "filterLabel":
284 self.assertEqual(component, reference)
285 elif compName == "visitInfo":
286 self.assertEqual(component.getExposureId(), reference.getExposureId(),
287 "VisitInfo comparison")
288 elif compName == "metadata":
289 # The component metadata has extra fields in it so cannot
290 # compare directly.
291 for k, v in reference.items():
292 self.assertEqual(component[k], v)
293 elif compName == "photoCalib":
294 # This example has a
295 # "spatially constant with mean: inf error: nan" entry
296 # which does not compare directly.
297 self.assertEqual(str(component), str(reference))
298 self.assertIn("spatially constant with mean: 1.99409", str(component),
299 "Checking photoCalib")
300 elif compName in ("bbox", "xy0", "dimensions", "validPolygon"):
301 self.assertEqual(component, reference)
302 elif compName == "apCorrMap":
303 self.assertEqual(set(component.keys()), set(reference.keys()))
304 elif compName == "transmissionCurve":
305 self.assertEqual(component.getThroughputAtBounds(),
306 reference.getThroughputAtBounds())
307 elif compName == "detector":
308 c_amps = {a.getName() for a in component.getAmplifiers()}
309 r_amps = {a.getName() for a in reference.getAmplifiers()}
310 self.assertEqual(c_amps, r_amps)
311 elif compName == 'summaryStats':
312 self.assertEqual(component.psfSigma, reference.psfSigma)
313 else:
314 raise RuntimeError(f"Unexpected component '{compName}' encountered in test")
316 # Full Exposure with parameters
317 inBBox = Box2I(minimum=Point2I(3, 3), maximum=Point2I(21, 16))
318 parameters = dict(bbox=inBBox, origin=LOCAL)
319 subset = self.butler.get(datasetTypeName, dataId, parameters=parameters)
320 outBBox = subset.getBBox()
321 self.assertEqual(inBBox, outBBox)
322 self.assertImagesEqual(subset.getImage(), exposure.subset(inBBox, origin=LOCAL).getImage())
324 return ref
326 def putFits(self, exposure, datasetTypeName, visit):
327 """Put different datasetTypes and return information."""
328 dataId = {"visit": visit, "instrument": "DummyCam", "physical_filter": "d-r"}
329 refC = self.butler.put(exposure, datasetTypeName, dataId)
330 uriC = self.butler.getURI(refC)
331 stat = os.stat(uriC.ospath)
332 size = stat.st_size
333 # We can't use butler's Exposure storage class metadata component,
334 # because that intentionally strips keywords that science code
335 # shouldn't ever read (to at least weaken our assumptions that we write
336 # to FITS). Instead use a lower-level method on the URI for this test.
337 meta = readMetadata(uriC.ospath)
338 return meta, size
340 def testCompression(self):
341 """Test that we can write compressed and uncompressed FITS."""
342 example = os.path.join(TESTDIR, "data", "small.fits")
343 exposure = lsst.afw.image.ExposureF(example)
345 # Write a lossless compressed
346 metaC, sizeC = self.putFits(exposure, "lossless", 42)
347 self.assertEqual(metaC["TTYPE1"], "COMPRESSED_DATA")
348 self.assertEqual(metaC["ZCMPTYPE"], "GZIP_2")
350 # Write an uncompressed FITS file
351 metaN, sizeN = self.putFits(exposure, "uncompressed", 43)
352 self.assertNotIn("ZCMPTYPE", metaN)
354 # Write an uncompressed FITS file
355 metaL, sizeL = self.putFits(exposure, "lossy", 44)
356 self.assertEqual(metaL["TTYPE1"], "COMPRESSED_DATA")
357 self.assertEqual(metaL["ZCMPTYPE"], "RICE_1")
359 self.assertNotEqual(sizeC, sizeN)
360 # Data file is so small that Lossy and Compressed are dominated
361 # by the extra compression tables
362 self.assertEqual(sizeL, sizeC)
365if __name__ == "__main__": 365 ↛ 366line 365 didn't jump to line 366, because the condition on line 365 was never true
366 unittest.main()