Coverage for tests / test_butlerFits.py: 14%
271 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-07 01:29 -0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-07 01:29 -0800
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# (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 shutil
26import tempfile
27import unittest
28import unittest.mock
29import uuid
31import astropy.table
33import lsst.afw.cameraGeom.testUtils # for test asserts injected into TestCase
34import lsst.afw.image
35import lsst.pex.config
36import lsst.utils.tests
37from lsst.afw.fits import readMetadata
38from lsst.afw.image import LOCAL, PARENT, ExposureFitsReader, MaskedImageFitsReader
39from lsst.afw.math import flipImage
40from lsst.daf.base import PropertyList, PropertySet
41from lsst.daf.butler import Config, DatasetProvenance, DatasetRef, DatasetType, StorageClassFactory
42from lsst.daf.butler.tests import addDatasetType, makeTestCollection, makeTestRepo
43from lsst.geom import Box2I, Extent2I, Point2I
44from lsst.obs.base.exposureAssembler import ExposureAssembler
45from lsst.obs.base.tests import make_ramp_exposure_trimmed, make_ramp_exposure_untrimmed
47TESTDIR = os.path.dirname(__file__)
49BUTLER_CONFIG = """
50storageClasses:
51 ExposureCompositeF:
52 inheritsFrom: ExposureF
53datastore:
54 # Want to check disassembly so can't use InMemory
55 cls: lsst.daf.butler.datastores.fileDatastore.FileDatastore
56 formatters:
57 ExposureCompositeF: lsst.obs.base.formatters.fitsExposure.FitsExposureFormatter
58 lossless:
59 formatter: lsst.obs.base.formatters.fitsExposure.FitsExposureFormatter
60 parameters:
61 recipe: lossless
62 uncompressed:
63 formatter: lsst.obs.base.formatters.fitsExposure.FitsExposureFormatter
64 parameters:
65 recipe: noCompression
66 lossy:
67 formatter: lsst.obs.base.formatters.fitsExposure.FitsExposureFormatter
68 parameters:
69 recipe: lossy16
70 composites:
71 disassembled:
72 ExposureCompositeF: True
73 # Always run with cache disabled. None of these datasets are remote but
74 # we want to check that URI reading does work properly.
75 cached:
76 expiry:
77 default: false
78 mode: disabled
79"""
81# Components present in the test file
82COMPONENTS = {
83 "wcs",
84 "image",
85 "mask",
86 "coaddInputs",
87 "psf",
88 "visitInfo",
89 "variance",
90 "metadata",
91 "photoCalib",
92 "filter",
93 "validPolygon",
94 "transmissionCurve",
95 "detector",
96 "apCorrMap",
97 "summaryStats",
98 "id",
99}
100READ_COMPONENTS = {
101 "bbox",
102 "xy0",
103 "dimensions",
104}
107class ButlerFitsTests(lsst.utils.tests.TestCase):
108 """Tests for butler interaction with FITS files."""
110 @classmethod
111 def setUpClass(cls):
112 """Create a new butler once only."""
113 cls.storageClassFactory = StorageClassFactory()
115 cls.root = tempfile.mkdtemp(dir=TESTDIR)
117 dataIds = {
118 "instrument": ["DummyCam"],
119 "physical_filter": ["HSC-I"],
120 "visit": [42, 43, 44],
121 "band": ["300"],
122 }
124 # Ensure that we test in a directory that will include some
125 # metacharacters
126 subdir = "sub?#dir"
127 butlerRoot = os.path.join(cls.root, subdir)
129 cls.creatorButler = makeTestRepo(butlerRoot, dataIds, config=Config.fromYaml(BUTLER_CONFIG))
130 cls.enterClassContext(cls.creatorButler)
132 # Create dataset types used by the tests
133 for datasetTypeName, storageClassName in (
134 ("calexp", "ExposureF"),
135 ("noise", "MaskedImageF"),
136 ("unknown", "ExposureCompositeF"),
137 ("testCatalog", "SourceCatalog"),
138 ("lossless", "ExposureF"),
139 ("uncompressed", "ExposureF"),
140 ("lossy", "ExposureF"),
141 ):
142 storageClass = cls.storageClassFactory.getStorageClass(storageClassName)
143 addDatasetType(cls.creatorButler, datasetTypeName, set(dataIds), storageClass)
145 # And some dataset types that have no dimensions for easy testing
146 for datasetTypeName, storageClassName in (
147 ("ps", "PropertySet"),
148 ("pl", "PropertyList"),
149 ("int_exp_trimmed", "ExposureI"),
150 ("int_exp_untrimmed", "ExposureI"),
151 ):
152 storageClass = cls.storageClassFactory.getStorageClass(storageClassName)
153 addDatasetType(cls.creatorButler, datasetTypeName, {}, storageClass)
155 @classmethod
156 def tearDownClass(cls):
157 if cls.root is not None:
158 shutil.rmtree(cls.root, ignore_errors=True)
160 def setUp(self):
161 self.butler = makeTestCollection(self.creatorButler, uniqueId=self.id())
162 self.enterContext(self.butler)
164 def makeExampleCatalog(self) -> lsst.afw.table.SourceCatalog:
165 catalogPath = os.path.join(TESTDIR, "data", "source_catalog.fits")
166 return lsst.afw.table.SourceCatalog.readFits(catalogPath)
168 def assertCatalogEqual(
169 self, inputCatalog: lsst.afw.table.SourceCatalog, outputCatalog: lsst.afw.table.SourceCatalog
170 ) -> None:
171 self.assertIsInstance(outputCatalog, lsst.afw.table.SourceCatalog)
172 inputTable = inputCatalog.getTable()
173 inputRecord = inputCatalog[0]
174 outputTable = outputCatalog.getTable()
175 outputRecord = outputCatalog[0]
176 self.assertEqual(inputRecord.getPsfInstFlux(), outputRecord.getPsfInstFlux())
177 self.assertEqual(inputRecord.getPsfFluxFlag(), outputRecord.getPsfFluxFlag())
178 self.assertEqual(
179 inputTable.getSchema().getAliasMap().get("slot_Centroid"),
180 outputTable.getSchema().getAliasMap().get("slot_Centroid"),
181 )
182 self.assertEqual(inputRecord.getCentroid(), outputRecord.getCentroid())
183 self.assertFloatsAlmostEqual(
184 inputRecord.getCentroidErr()[0, 0], outputRecord.getCentroidErr()[0, 0], rtol=1e-6
185 )
186 self.assertFloatsAlmostEqual(
187 inputRecord.getCentroidErr()[1, 1], outputRecord.getCentroidErr()[1, 1], rtol=1e-6
188 )
189 self.assertEqual(
190 inputTable.getSchema().getAliasMap().get("slot_Shape"),
191 outputTable.getSchema().getAliasMap().get("slot_Shape"),
192 )
193 self.assertFloatsAlmostEqual(
194 inputRecord.getShapeErr()[0, 0], outputRecord.getShapeErr()[0, 0], rtol=1e-6
195 )
196 self.assertFloatsAlmostEqual(
197 inputRecord.getShapeErr()[1, 1], outputRecord.getShapeErr()[1, 1], rtol=1e-6
198 )
199 self.assertFloatsAlmostEqual(
200 inputRecord.getShapeErr()[2, 2], outputRecord.getShapeErr()[2, 2], rtol=1e-6
201 )
203 def runFundamentalTypeTest(self, datasetTypeName, entity):
204 """Put and get the supplied entity and compare."""
205 ref = self.butler.put(entity, datasetTypeName)
206 butler_ps = self.butler.get(ref)
207 self.assertEqual(butler_ps, entity)
209 # Break the contact by ensuring that we are writing YAML
210 uri = self.butler.getURI(ref)
211 self.assertEqual(uri.getExtension(), ".yaml", f"Check extension of {uri}")
213 def testFundamentalTypes(self) -> None:
214 """Ensure that some fundamental stack types round trip."""
215 ps = PropertySet()
216 ps["a.b"] = 5
217 ps["c.d.e"] = "string"
218 self.runFundamentalTypeTest("ps", ps)
220 pl = PropertyList()
221 pl["A"] = 1
222 pl.setComment("A", "An int comment")
223 pl["B"] = "string"
224 pl.setComment("B", "A string comment")
225 self.runFundamentalTypeTest("pl", pl)
227 def _make_provenance(self):
228 """Return provenance information for testing."""
229 prov = DatasetProvenance(quantum_id=uuid.uuid4())
230 ref = DatasetRef(
231 datasetType=self.butler.get_dataset_type("ps"),
232 dataId=[],
233 run="run",
234 )
235 prov.add_input(ref)
236 prov.add_extra_provenance(ref.id, {"EXTRA": 42})
237 return prov
239 def testFitsCatalog(self) -> None:
240 """Test reading of a FITS catalog."""
241 catalog = self.makeExampleCatalog()
242 dataId = {"visit": 42, "instrument": "DummyCam", "physical_filter": "HSC-I"}
243 prov = self._make_provenance()
244 ref = self.butler.put(catalog, "testCatalog", dataId, provenance=prov)
245 stored = self.butler.get(ref)
246 self.assertCatalogEqual(catalog, stored)
247 self.assertEqual(stored.metadata["LSST BUTLER ID"], str(ref.id))
248 self.assertEqual(stored.metadata["LSST BUTLER QUANTUM"], str(prov.quantum_id))
249 self.assertEqual(stored.metadata["LSST BUTLER INPUT 0 RUN"], "run")
250 self.assertEqual(stored.metadata["LSST BUTLER INPUT 0 EXTRA"], 42)
252 # Override the storage class.
253 astropy_table = self.butler.get(ref, storageClass="AstropyTable")
254 self.assertIsInstance(astropy_table, astropy.table.Table)
255 self.assertEqual(len(astropy_table), len(stored))
257 def testExposureCompositePutGetConcrete(self) -> None:
258 """Test composite with no disassembly."""
259 ref = self.runExposureCompositePutGetTest("calexp")
261 uri = self.butler.getURI(ref)
262 self.assertTrue(uri.exists(), f"Checking URI {uri} existence")
264 def testExposureCompositePutGetConcreteAstropy(self) -> None:
265 """Test composite with no disassembly and astropy component read."""
266 with unittest.mock.patch(
267 "lsst.obs.base.formatters.fitsExposure._ALWAYS_USE_ASTROPY_FOR_COMPONENT_READ", True
268 ):
269 self.runExposureCompositePutGetTest("calexp")
271 def testExposureCompositePutGetVirtual(self) -> None:
272 """Testing composite disassembly."""
273 ref = self.runExposureCompositePutGetTest("unknown")
275 primary, components = self.butler.getURIs(ref)
276 self.assertIsNone(primary)
277 self.assertEqual(set(components), COMPONENTS)
278 for compName, uri in components.items():
279 self.assertTrue(uri.exists(), f"Checking URI {uri} existence for component {compName}")
281 def runExposureCompositePutGetTest(self, datasetTypeName: str) -> DatasetRef:
282 example = os.path.join(TESTDIR, "data", "calexp.fits")
283 exposure = lsst.afw.image.ExposureF(example)
285 prov = self._make_provenance()
286 dataId = {"visit": 42, "instrument": "DummyCam", "physical_filter": "HSC-I"}
287 ref = self.butler.put(exposure, datasetTypeName, dataId, provenance=prov)
289 # Get the full thing
290 composite = self.butler.get(datasetTypeName, dataId)
292 # Check that provenance has been written.
293 self.assertEqual(composite.metadata["LSST BUTLER ID"], str(ref.id))
294 self.assertEqual(composite.metadata["LSST BUTLER QUANTUM"], str(prov.quantum_id))
295 self.assertEqual(composite.metadata["LSST BUTLER INPUT 0 RUN"], "run")
296 self.assertEqual(composite.metadata["LSST BUTLER INPUT 0 EXTRA"], 42)
298 # There is no assert for Exposure so just look at maskedImage
299 self.assertMaskedImagesEqual(composite.maskedImage, exposure.maskedImage)
301 # Helper for extracting components
302 assembler = ExposureAssembler(ref.datasetType.storageClass)
304 # Check all possible components that can be read
305 allComponents = set()
306 allComponents.update(COMPONENTS, READ_COMPONENTS)
308 # Get each component from butler independently
309 for compName in allComponents:
310 compTypeName = DatasetType.nameWithComponent(datasetTypeName, compName)
311 component = self.butler.get(compTypeName, dataId)
313 reference = assembler.getComponent(exposure, compName)
315 self.assertIsInstance(component, type(reference), f"Checking type of component {compName}")
317 if compName in ("image", "variance"):
318 self.assertImagesEqual(component, reference)
319 elif compName == "mask":
320 self.assertMasksEqual(component, reference)
321 elif compName == "wcs":
322 self.assertWcsAlmostEqualOverBBox(component, reference, exposure.getBBox())
323 elif compName == "coaddInputs":
324 self.assertEqual(
325 len(component.visits), len(reference.visits), f"cf visits {component.visits}"
326 )
327 self.assertEqual(len(component.ccds), len(reference.ccds), f"cf CCDs {component.ccds}")
328 elif compName == "psf":
329 # Equality for PSF does not work
330 pass
331 elif compName == "filter":
332 # FilterLabel has different values depending whether you load
333 # it using a ref with full data ID values or not. With a full
334 # data ID value, it uses the physical_filter data ID value
335 # instead of reading it from the file.
336 self.assertTrue(component.physicalLabel == "HSC-I" or component.physicalLabel == "HSC-I")
337 elif compName == "id":
338 self.assertEqual(component, reference, compName)
339 elif compName == "visitInfo":
340 self.assertEqual(component, reference, "VisitInfo comparison")
341 elif compName == "metadata":
342 # The component metadata has extra fields in it so cannot
343 # compare directly.
344 for k, v in reference.items():
345 self.assertEqual(component[k], v, f"{compName} key: {k}")
346 elif compName == "photoCalib":
347 # This example has a
348 # "spatially constant with mean: inf error: nan" entry
349 # which does not compare directly.
350 self.assertEqual(str(component), str(reference), compName)
351 self.assertIn("spatially constant with mean: 1.99409", str(component), "Checking photoCalib")
352 elif compName in ("bbox", "xy0", "dimensions", "validPolygon"):
353 self.assertEqual(component, reference, compName)
354 elif compName == "apCorrMap":
355 self.assertEqual(set(component.keys()), set(reference.keys()), compName)
356 elif compName == "transmissionCurve":
357 self.assertEqual(
358 component.getThroughputAtBounds(), reference.getThroughputAtBounds(), compName
359 )
360 elif compName == "detector":
361 c_amps = {a.getName() for a in component.getAmplifiers()}
362 r_amps = {a.getName() for a in reference.getAmplifiers()}
363 self.assertEqual(c_amps, r_amps, compName)
364 elif compName == "summaryStats":
365 self.assertEqual(component.psfSigma, reference.psfSigma, compName)
366 else:
367 raise RuntimeError(f"Unexpected component '{compName}' encountered in test")
369 # Full Exposure with parameters
370 for origin in (LOCAL, PARENT):
371 inBBox = Box2I(minimum=Point2I(5, 3), maximum=Point2I(42, 16))
372 parameters = {"bbox": inBBox, "origin": origin}
373 subset = self.butler.get(datasetTypeName, dataId, parameters=parameters)
374 outBBox = subset.getBBox()
375 self.assertEqual(inBBox, outBBox)
376 self.assertImagesEqual(subset.getImage(), exposure.subset(inBBox, origin=LOCAL).getImage())
378 return ref
380 def putFits(self, exposure, datasetTypeName, visit):
381 """Put different datasetTypes and return information."""
382 dataId = {"visit": visit, "instrument": "DummyCam", "physical_filter": "HSC-I"}
383 refC = self.butler.put(exposure, datasetTypeName, dataId)
384 uriC = self.butler.getURI(refC)
385 stat = os.stat(uriC.ospath)
386 size = stat.st_size
387 # We can't use butler's Exposure storage class metadata component,
388 # because that intentionally strips keywords that science code
389 # shouldn't ever read (to at least weaken our assumptions that we write
390 # to FITS). Instead use a lower-level method on the URI for this test.
391 meta = readMetadata(uriC.ospath)
392 return meta, size
394 def testCompression(self):
395 """Test that we can write compressed and uncompressed FITS."""
396 example = os.path.join(TESTDIR, "data", "small.fits")
397 exposure = lsst.afw.image.ExposureF(example)
399 # Write a lossless compressed
400 metaC, sizeC = self.putFits(exposure, "lossless", 42)
401 self.assertEqual(metaC["TTYPE1"], "COMPRESSED_DATA")
402 self.assertEqual(metaC["ZCMPTYPE"], "GZIP_2")
404 # Write an uncompressed FITS file
405 metaN, sizeN = self.putFits(exposure, "uncompressed", 43)
406 self.assertNotIn("ZCMPTYPE", metaN)
408 # Write a lossy-compressed FITS file.
409 metaL, sizeL = self.putFits(exposure, "lossy", 44)
410 self.assertEqual(metaL["TTYPE1"], "COMPRESSED_DATA")
411 # "RICE_ONE" is some bad CFITSIO behavior that we have to tolerate for
412 # now; it's an old pre-standard key that has become acceptable to at
413 # least astropy (when reading) that CFITSIO continues to write for the
414 # dubious purpose of supporting even older readers.
415 self.assertIn(metaL["ZCMPTYPE"], ("RICE_1", "RICE_ONE"))
417 self.assertNotEqual(sizeC, sizeN)
418 # Data file is so small that Lossy and Compressed are dominated
419 # by the extra compression tables
420 self.assertEqual(sizeL, sizeC)
422 def testExposureFormatterAmpParameter(self):
423 """Test the FitsExposureFormatter implementation of the Exposure
424 StorageClass's 'amp' and 'detector' parameters.
425 """
426 # Our example exposure file has a realistic detector (looks like HSC),
427 # but the image itself doesn't match it. So we just load the detector,
428 # and use it to make our own more useful images and put them.
429 detector = ExposureFitsReader(os.path.join(TESTDIR, "data", "calexp.fits")).readDetector()
430 trimmed_full = make_ramp_exposure_trimmed(detector)
431 untrimmed_full = make_ramp_exposure_untrimmed(detector)
432 trimmed_ref = self.butler.put(trimmed_full, "int_exp_trimmed")
433 untrimmed_ref = self.butler.put(untrimmed_full, "int_exp_untrimmed")
434 for n, amp in enumerate(detector):
435 # Try to read each amp as it is on disk, with a variety of
436 # parameters that should all do the same thing.
437 for amp_parameter in [amp, amp.getName(), n]:
438 for parameters in [{"amp": amp_parameter}, {"amp": amp_parameter, "detector": detector}]:
439 with self.subTest(parameters=repr(parameters)):
440 test_trimmed = self.butler.get(trimmed_ref, parameters=parameters)
441 test_untrimmed = self.butler.get(untrimmed_ref, parameters=parameters)
442 self.assertImagesEqual(test_trimmed.image, trimmed_full[amp.getBBox()].image)
443 self.assertImagesEqual(test_untrimmed.image, untrimmed_full[amp.getRawBBox()].image)
444 self.assertEqual(len(test_trimmed.getDetector()), 1)
445 self.assertEqual(len(test_untrimmed.getDetector()), 1)
446 self.assertAmplifiersEqual(test_trimmed.getDetector()[0], amp)
447 self.assertAmplifiersEqual(test_untrimmed.getDetector()[0], amp)
448 # Try to read various transformed versions of the original amp,
449 # to make sure flips and offsets are applied correctly.
450 # First flip X only.
451 amp_t1 = amp.rebuild().transform(outFlipX=True).finish()
452 test_t1_trimmed = self.butler.get(trimmed_ref, parameters={"amp": amp_t1})
453 self.assertImagesEqual(
454 test_t1_trimmed.image, flipImage(trimmed_full[amp.getBBox()].image, flipLR=True, flipTB=False)
455 )
456 self.assertAmplifiersEqual(test_t1_trimmed.getDetector()[0], amp_t1)
457 test_t1_untrimmed = self.butler.get(untrimmed_ref, parameters={"amp": amp_t1})
458 self.assertImagesEqual(
459 test_t1_untrimmed.image,
460 flipImage(untrimmed_full[amp.getRawBBox()].image, flipLR=True, flipTB=False),
461 )
462 self.assertAmplifiersEqual(test_t1_trimmed.getDetector()[0], amp_t1)
463 # Flip Y only.
464 amp_t2 = amp.rebuild().transform(outFlipY=True).finish()
465 test_t2_trimmed = self.butler.get(trimmed_ref, parameters={"amp": amp_t2})
466 self.assertImagesEqual(
467 test_t2_trimmed.image, flipImage(trimmed_full[amp.getBBox()].image, flipLR=False, flipTB=True)
468 )
469 self.assertAmplifiersEqual(test_t2_trimmed.getDetector()[0], amp_t2)
470 test_t2_untrimmed = self.butler.get(untrimmed_ref, parameters={"amp": amp_t2})
471 self.assertImagesEqual(
472 test_t2_untrimmed.image,
473 flipImage(untrimmed_full[amp.getRawBBox()].image, flipLR=False, flipTB=True),
474 )
475 self.assertAmplifiersEqual(test_t2_trimmed.getDetector()[0], amp_t2)
476 # Add an XY offset only.
477 amp_t3 = amp.rebuild().transform(outOffset=Extent2I(5, 4)).finish()
478 test_t3_trimmed = self.butler.get(trimmed_ref, parameters={"amp": amp_t3})
479 self.assertImagesEqual(test_t3_trimmed.image, trimmed_full[amp.getBBox()].image)
480 self.assertAmplifiersEqual(test_t3_trimmed.getDetector()[0], amp_t3)
481 test_t3_untrimmed = self.butler.get(untrimmed_ref, parameters={"amp": amp_t3})
482 self.assertImagesEqual(test_t3_untrimmed.image, untrimmed_full[amp.getRawBBox()].image)
483 self.assertAmplifiersEqual(test_t3_trimmed.getDetector()[0], amp_t3)
485 def testMaskedImageFormatter(self):
486 """Test that a MaskedImage can be persisted and read from a Butler."""
487 # Read in an Exposure as MaskedImage using MaskedImageFitsReader.
488 reader = MaskedImageFitsReader(os.path.join(TESTDIR, "data", "calexp.fits"))
489 mi = reader.read()
491 # Put the MaskedImage into the Butler and get a reference to it.
492 dataId = {"visit": 42, "instrument": "DummyCam", "physical_filter": "HSC-I"}
493 ref = self.butler.put(mi, "noise", dataId)
495 # Check that the MaskedImage can be retrieved from the butler.
496 maskedImage = self.butler.get(ref)
497 self.assertImagesEqual(maskedImage.image, mi.image)
498 self.assertImagesEqual(maskedImage.mask, mi.mask)
499 self.assertImagesEqual(maskedImage.variance, mi.variance)
501 # Get a DeferredDatasetHandle to load parts of the MaskedImage.
502 handle = self.butler.getDeferred(ref)
504 for parameters in (
505 {},
506 {"bbox": reader.readBBox()},
507 {"bbox": Box2I(minimum=Point2I(3, 3), maximum=Point2I(21, 16))},
508 {"bbox": Box2I(minimum=Point2I(3, 3), maximum=Point2I(21, 16)), "origin": LOCAL},
509 ):
510 bbox = parameters.get("bbox", reader.readBBox())
511 with self.subTest(parameters=repr(parameters)):
512 # Check that the reader supports reading sub-regions.
513 subMaskedImage = reader.read(**parameters)
514 self.assertImagesEqual(subMaskedImage.image, mi.image[bbox])
515 self.assertImagesEqual(subMaskedImage.mask, mi.mask[bbox])
516 self.assertImagesEqual(subMaskedImage.variance, mi.variance[bbox])
518 # Get a maskedImage within a bounding box from the butler.
519 subMaskedImage = handle.get(parameters=parameters)
520 self.assertImagesEqual(subMaskedImage.image, mi.image[bbox])
521 self.assertImagesEqual(subMaskedImage.mask, mi.mask[bbox])
522 self.assertImagesEqual(subMaskedImage.variance, mi.variance[bbox])
524 # Get one component at a time from the butler.
525 subImage = handle.get(parameters=parameters, component="image")
526 subMask = handle.get(parameters=parameters, component="mask")
527 subVariance = handle.get(parameters=parameters, component="variance")
528 self.assertImagesEqual(subImage, mi.image[bbox])
529 self.assertImagesEqual(subMask, mi.mask[bbox])
530 self.assertImagesEqual(subVariance, mi.variance[bbox])
533if __name__ == "__main__": 533 ↛ 534line 533 didn't jump to line 534 because the condition on line 533 was never true
534 unittest.main()