Hide keyboard shortcuts

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/>. 

21 

22from __future__ import annotations 

23 

24import os 

25import unittest 

26import tempfile 

27import shutil 

28 

29from typing import TYPE_CHECKING 

30 

31import lsst.utils.tests 

32 

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 

40 

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 

45 

46from lsst.obs.base.exposureAssembler import ExposureAssembler 

47 

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 

50 

51TESTDIR = os.path.dirname(__file__) 

52 

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

78 

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

83 

84 

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) 

89 

90 

91class ButlerFitsTests(DatasetTestHelper, lsst.utils.tests.TestCase): 

92 

93 @classmethod 

94 def setUpClass(cls): 

95 """Create a new butler once only.""" 

96 

97 cls.storageClassFactory = StorageClassFactory() 

98 

99 cls.root = tempfile.mkdtemp(dir=TESTDIR) 

100 

101 dataIds = { 

102 "instrument": ["DummyCam"], 

103 "physical_filter": ["d-r"], 

104 "visit": [42, 43, 44], 

105 } 

106 

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) 

111 

112 cls.creatorButler = makeTestRepo(butlerRoot, dataIds, config=Config.fromYaml(BUTLER_CONFIG)) 

113 

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) 

124 

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) 

133 

134 @classmethod 

135 def tearDownClass(cls): 

136 if cls.root is not None: 

137 shutil.rmtree(cls.root, ignore_errors=True) 

138 

139 def setUp(self): 

140 self.butler = makeTestCollection(self.creatorButler) 

141 

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) 

145 

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) 

175 

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) 

181 

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

185 

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) 

192 

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) 

199 

200 pkg = Packages.fromSystem() 

201 self.runFundamentalTypeTest("pkg", pkg) 

202 

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) 

211 

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) 

219 

220 def testExposureCompositePutGetConcrete(self) -> None: 

221 """Test composite with no disassembly""" 

222 ref = self.runExposureCompositePutGetTest("calexp") 

223 

224 uri = self.butler.getURI(ref) 

225 self.assertTrue(uri.exists(), f"Checking URI {uri} existence") 

226 

227 def testExposureCompositePutGetVirtual(self) -> None: 

228 """Testing composite disassembly""" 

229 ref = self.runExposureCompositePutGetTest("unknown") 

230 

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

237 

238 def runExposureCompositePutGetTest(self, datasetTypeName: str) -> DatasetRef: 

239 example = os.path.join(TESTDIR, "data", "calexp.fits") 

240 exposure = lsst.afw.image.ExposureF(example) 

241 

242 dataId = {"visit": 42, "instrument": "DummyCam", "physical_filter": "d-r"} 

243 ref = self.butler.put(exposure, datasetTypeName, dataId) 

244 

245 # Get the full thing 

246 composite = self.butler.get(datasetTypeName, dataId) 

247 

248 # There is no assert for Exposure so just look at maskedImage 

249 self.assertMaskedImagesEqual(composite.maskedImage, exposure.maskedImage) 

250 

251 # Helper for extracting components 

252 assembler = ExposureAssembler(ref.datasetType.storageClass) 

253 

254 # Check all possible components that can be read 

255 allComponents = set() 

256 allComponents.update(COMPONENTS, READ_COMPONENTS) 

257 

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) 

262 

263 reference = assembler.getComponent(exposure, compName) 

264 

265 self.assertIsInstance(component, type(reference), f"Checking type of component {compName}") 

266 

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

315 

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

323 

324 return ref 

325 

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 

339 

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) 

344 

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

349 

350 # Write an uncompressed FITS file 

351 metaN, sizeN = self.putFits(exposure, "uncompressed", 43) 

352 self.assertNotIn("ZCMPTYPE", metaN) 

353 

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

358 

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) 

363 

364 

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

366 unittest.main()