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# LSST Data Management System 

2# Copyright 2016-2019 AURA/LSST. 

3# 

4# This product includes software developed by the 

5# LSST Project (http://www.lsst.org/). 

6# 

7# This program is free software: you can redistribute it and/or modify 

8# it under the terms of the GNU General Public License as published by 

9# the Free Software Foundation, either version 3 of the License, or 

10# (at your option) any later version. 

11# 

12# This program is distributed in the hope that it will be useful, 

13# but WITHOUT ANY WARRANTY; without even the implied warranty of 

14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

15# GNU General Public License for more details. 

16# 

17# You should have received a copy of the LSST License Statement and 

18# the GNU General Public License along with this program. If not, 

19# see <https://www.lsstcorp.org/LegalNotices/>. 

20"""Blob classes that reduce a multi-visit dataset and encapsulate data 

21for measurement classes, plotting functions, and JSON persistence. 

22""" 

23 

24__all__ = ['build_matched_dataset', 'getKeysFilter', 'filterSources', 'summarizeSources'] 

25 

26import numpy as np 

27import astropy.units as u 

28from sqlalchemy.exc import OperationalError 

29import sqlite3 

30 

31import lsst.geom as geom 

32import lsst.daf.persistence as dafPersist 

33from lsst.afw.table import (SourceCatalog, SchemaMapper, Field, 

34 MultiMatch, SimpleRecord, GroupView, 

35 SOURCE_IO_NO_FOOTPRINTS) 

36import lsst.afw.table as afwTable 

37from lsst.afw.fits import FitsError 

38import lsst.pipe.base as pipeBase 

39from lsst.verify import Blob, Datum 

40 

41from .util import (getCcdKeyName, raftSensorToInt, positionRmsFromCat, 

42 ellipticity_from_cat) 

43 

44 

45def build_matched_dataset(repo, dataIds, matchRadius=None, brightSnrMin=None, brightSnrMax=None, 

46 faintSnrMin=None, faintSnrMax=None, 

47 doApplyExternalPhotoCalib=False, externalPhotoCalibName=None, 

48 doApplyExternalSkyWcs=False, externalSkyWcsName=None, 

49 skipTEx=False, skipNonSrd=False): 

50 """Construct a container for matched star catalogs from multple visits, with filtering, 

51 summary statistics, and modelling. 

52 

53 `lsst.verify.Blob` instances are serializable to JSON. 

54 

55 Parameters 

56 ---------- 

57 repo : `str` or `lsst.daf.persistence.Butler` 

58 A Butler instance or a repository URL that can be used to construct 

59 one. 

60 dataIds : `list` of `dict` 

61 List of `butler` data IDs of Image catalogs to compare to reference. 

62 The `calexp` cpixel image is needed for the photometric calibration. 

63 matchRadius : `lsst.geom.Angle`, optional 

64 Radius for matching. Default is 1 arcsecond. 

65 brightSnrMin : `float`, optional 

66 Minimum median SNR for a source to be considered bright; passed to `filterSources`. 

67 brightSnrMax : `float`, optional 

68 Maximum median SNR for a source to be considered bright; passed to `filterSources`. 

69 faintSnrMin : `float`, optional 

70 Minimum median SNR for a source to be considered faint; passed to `filterSources`. 

71 faintSnrMax : `float`, optional 

72 Maximum median SNR for a source to be considered faint; passed to `filterSources`. 

73 doApplyExternalPhotoCalib : bool, optional 

74 Apply external photoCalib to calibrate fluxes. 

75 externalPhotoCalibName : str, optional 

76 Type of external `PhotoCalib` to apply. Currently supported are jointcal, 

77 fgcm, and fgcm_tract. Must be set if "doApplyExternalPhotoCalib" is True. 

78 doApplyExternalSkyWcs : bool, optional 

79 Apply external wcs to calibrate positions. 

80 externalSkyWcsName : str, optional: 

81 Type of external `wcs` to apply. Currently supported is jointcal. 

82 Must be set if "doApplyExternalSkyWcs" is True. 

83 skipTEx : `bool`, optional 

84 Skip TEx calculations (useful for older catalogs that don't have 

85 PsfShape measurements). 

86 skipNonSrd : `bool`, optional 

87 Skip any metrics not defined in the LSST SRD; default False. 

88 

89 Attributes of returned Blob 

90 ---------- 

91 filterName : `str` 

92 Name of filter used for all observations. 

93 mag : `astropy.units.Quantity` 

94 Mean PSF magnitudes of stars over multiple visits (magnitudes). 

95 magerr : `astropy.units.Quantity` 

96 Median 1-sigma uncertainty of PSF magnitudes over multiple visits 

97 (magnitudes). 

98 magrms : `astropy.units.Quantity` 

99 RMS of PSF magnitudes over multiple visits (magnitudes). 

100 snr : `astropy.units.Quantity` 

101 Median signal-to-noise ratio of PSF magnitudes over multiple visits 

102 (dimensionless). 

103 dist : `astropy.units.Quantity` 

104 RMS of sky coordinates of stars over multiple visits (milliarcseconds). 

105 

106 *Not serialized.* 

107 matchesFaint : `afw.table.GroupView` 

108 Faint matches containing only objects that have: 

109 

110 1. A PSF Flux measurement with sufficient S/N. 

111 2. A finite (non-nan) PSF magnitude. This separate check is largely 

112 to reject failed zeropoints. 

113 3. No flags set for bad, cosmic ray, edge or saturated. 

114 4. Extendedness consistent with a point source. 

115 

116 *Not serialized.* 

117 matchesBright : `afw.table.GroupView` 

118 Bright matches matching a higher S/N threshold than matchesFaint. 

119 

120 *Not serialized.* 

121 magKey 

122 Key for `"base_PsfFlux_mag"` in the `matchesFaint` and `matchesBright` 

123 catalog tables. 

124 

125 *Not serialized.* 

126 

127 Raises 

128 ------ 

129 RuntimeError: 

130 Raised if "doApplyExternalPhotoCalib" is True and "externalPhotoCalibName" 

131 is None, or if "doApplyExternalSkyWcs" is True and "externalSkyWcsName" is 

132 None. 

133 """ 

134 if doApplyExternalPhotoCalib and externalPhotoCalibName is None: 

135 raise RuntimeError("Must set externalPhotoCalibName if doApplyExternalPhotoCalib is True.") 

136 if doApplyExternalSkyWcs and externalSkyWcsName is None: 

137 raise RuntimeError("Must set externalSkyWcsName if doApplyExternalSkyWcs is True.") 

138 

139 blob = Blob('MatchedMultiVisitDataset') 

140 

141 if not matchRadius: 

142 matchRadius = geom.Angle(1, geom.arcseconds) 

143 

144 # Extract single filter 

145 blob['filterName'] = Datum(quantity=set([dId['filter'] for dId in dataIds]).pop(), 

146 description='Filter name') 

147 

148 # Record important configuration 

149 blob['doApplyExternalPhotoCalib'] = Datum(quantity=doApplyExternalPhotoCalib, 

150 description=('Whether external photometric ' 

151 'calibrations were used.')) 

152 blob['externalPhotoCalibName'] = Datum(quantity=externalPhotoCalibName, 

153 description='Name of external PhotoCalib dataset used.') 

154 blob['doApplyExternalSkyWcs'] = Datum(quantity=doApplyExternalSkyWcs, 

155 description='Whether external wcs calibrations were used.') 

156 blob['externalSkyWcsName'] = Datum(quantity=externalSkyWcsName, 

157 description='Name of external wcs dataset used.') 

158 

159 # Match catalogs across visits 

160 blob._catalog, blob._matchedCatalog = \ 

161 _loadAndMatchCatalogs(repo, dataIds, matchRadius, 

162 doApplyExternalPhotoCalib=doApplyExternalPhotoCalib, 

163 externalPhotoCalibName=externalPhotoCalibName, 

164 doApplyExternalSkyWcs=doApplyExternalSkyWcs, 

165 externalSkyWcsName=externalSkyWcsName, 

166 skipTEx=skipTEx, skipNonSrd=skipNonSrd) 

167 

168 blob.magKey = blob._matchedCatalog.schema.find("base_PsfFlux_mag").key 

169 # Reduce catalogs into summary statistics. 

170 # These are the serializable attributes of this class. 

171 filterResult = filterSources( 

172 blob._matchedCatalog, brightSnrMin=brightSnrMin, brightSnrMax=brightSnrMax, 

173 faintSnrMin=faintSnrMin, faintSnrMax=faintSnrMax, 

174 ) 

175 summarizeSources(blob, filterResult) 

176 return blob 

177 

178 

179def _loadAndMatchCatalogs(repo, dataIds, matchRadius, 

180 doApplyExternalPhotoCalib=False, externalPhotoCalibName=None, 

181 doApplyExternalSkyWcs=False, externalSkyWcsName=None, 

182 skipTEx=False, skipNonSrd=False): 

183 """Load data from specific visits and returned a calibrated catalog matched 

184 with a reference. 

185 

186 Parameters 

187 ---------- 

188 repo : `str` or `lsst.daf.persistence.Butler` 

189 A Butler or a repository URL that can be used to construct one. 

190 dataIds : list of dict 

191 List of butler data IDs of Image catalogs to compare to 

192 reference. The calexp cpixel image is needed for the photometric 

193 calibration. 

194 matchRadius : `lsst.geom.Angle`, optional 

195 Radius for matching. Default is 1 arcsecond. 

196 doApplyExternalPhotoCalib : bool, optional 

197 Apply external photoCalib to calibrate fluxes. 

198 externalPhotoCalibName : str, optional 

199 Type of external `PhotoCalib` to apply. Currently supported are jointcal, 

200 fgcm, and fgcm_tract. Must be set if doApplyExternalPhotoCalib is True. 

201 doApplyExternalSkyWcs : bool, optional 

202 Apply external wcs to calibrate positions. 

203 externalSkyWcsName : str, optional 

204 Type of external `wcs` to apply. Currently supported is jointcal. 

205 Must be set if "doApplyExternalWcs" is True. 

206 skipTEx : `bool`, optional 

207 Skip TEx calculations (useful for older catalogs that don't have 

208 PsfShape measurements). 

209 skipNonSrd : `bool`, optional 

210 Skip any metrics not defined in the LSST SRD; default False. 

211 

212 Returns 

213 ------- 

214 catalog : `lsst.afw.table.SourceCatalog` 

215 A new calibrated SourceCatalog. 

216 matches : `lsst.afw.table.GroupView` 

217 A GroupView of the matched sources. 

218 

219 Raises 

220 ------ 

221 RuntimeError: 

222 Raised if "doApplyExternalPhotoCalib" is True and "externalPhotoCalibName" 

223 is None, or if "doApplyExternalSkyWcs" is True and "externalSkyWcsName" is 

224 None. 

225 """ 

226 

227 if doApplyExternalPhotoCalib and externalPhotoCalibName is None: 

228 raise RuntimeError("Must set externalPhotoCalibName if doApplyExternalPhotoCalib is True.") 

229 if doApplyExternalSkyWcs and externalSkyWcsName is None: 

230 raise RuntimeError("Must set externalSkyWcsName if doApplyExternalSkyWcs is True.") 

231 

232 # Following 

233 # https://github.com/lsst/afw/blob/tickets/DM-3896/examples/repeatability.ipynb 

234 if isinstance(repo, dafPersist.Butler): 

235 butler = repo 

236 else: 

237 butler = dafPersist.Butler(repo) 

238 dataset = 'src' 

239 

240 # 2016-02-08 MWV: 

241 # I feel like I could be doing something more efficient with 

242 # something along the lines of the following: 

243 # dataRefs = [dafPersist.ButlerDataRef(butler, vId) for vId in dataIds] 

244 

245 ccdKeyName = getCcdKeyName(dataIds[0]) 

246 

247 # Hack to support raft and sensor 0,1 IDs as ints for multimatch 

248 if ccdKeyName == 'sensor': 

249 ccdKeyName = 'raft_sensor_int' 

250 for vId in dataIds: 

251 vId[ccdKeyName] = raftSensorToInt(vId) 

252 

253 schema = butler.get(dataset + "_schema").schema 

254 mapper = SchemaMapper(schema) 

255 mapper.addMinimalSchema(schema) 

256 mapper.addOutputField(Field[float]('base_PsfFlux_snr', 

257 'PSF flux SNR')) 

258 mapper.addOutputField(Field[float]('base_PsfFlux_mag', 

259 'PSF magnitude')) 

260 mapper.addOutputField(Field[float]('base_PsfFlux_magErr', 

261 'PSF magnitude uncertainty')) 

262 if not skipNonSrd: 

263 # Needed because addOutputField(... 'slot_ModelFlux_mag') will add a field with that literal name 

264 aliasMap = schema.getAliasMap() 

265 # Possibly not needed since base_GaussianFlux is the default, but this ought to be safe 

266 modelName = aliasMap['slot_ModelFlux'] if 'slot_ModelFlux' in aliasMap.keys() else 'base_GaussianFlux' 

267 mapper.addOutputField(Field[float](f'{modelName}_mag', 

268 'Model magnitude')) 

269 mapper.addOutputField(Field[float](f'{modelName}_magErr', 

270 'Model magnitude uncertainty')) 

271 mapper.addOutputField(Field[float](f'{modelName}_snr', 

272 'Model flux snr')) 

273 mapper.addOutputField(Field[float]('e1', 

274 'Source Ellipticity 1')) 

275 mapper.addOutputField(Field[float]('e2', 

276 'Source Ellipticity 1')) 

277 mapper.addOutputField(Field[float]('psf_e1', 

278 'PSF Ellipticity 1')) 

279 mapper.addOutputField(Field[float]('psf_e2', 

280 'PSF Ellipticity 1')) 

281 newSchema = mapper.getOutputSchema() 

282 newSchema.setAliasMap(schema.getAliasMap()) 

283 

284 # Create an object that matches multiple catalogs with same schema 

285 mmatch = MultiMatch(newSchema, 

286 dataIdFormat={'visit': np.int32, ccdKeyName: np.int32}, 

287 radius=matchRadius, 

288 RecordClass=SimpleRecord) 

289 

290 # create the new extented source catalog 

291 srcVis = SourceCatalog(newSchema) 

292 

293 for vId in dataIds: 

294 if not butler.datasetExists('src', vId): 

295 print(f'Could not find source catalog for {vId}; skipping.') 

296 continue 

297 

298 photoCalib = _loadPhotoCalib(butler, vId, 

299 doApplyExternalPhotoCalib, externalPhotoCalibName) 

300 if photoCalib is None: 

301 continue 

302 

303 if doApplyExternalSkyWcs: 

304 wcs = _loadExternalSkyWcs(butler, vId, externalSkyWcsName) 

305 if wcs is None: 

306 continue 

307 

308 # We don't want to put this above the first _loadPhotoCalib call 

309 # because we need to use the first `butler.get` in there to quickly 

310 # catch dataIDs with no usable outputs. 

311 try: 

312 # HSC supports these flags, which dramatically improve I/O 

313 # performance; support for other cameras is DM-6927. 

314 oldSrc = butler.get('src', vId, flags=SOURCE_IO_NO_FOOTPRINTS) 

315 except (OperationalError, sqlite3.OperationalError): 

316 oldSrc = butler.get('src', vId) 

317 

318 print(len(oldSrc), "sources in ccd %s visit %s" % 

319 (vId[ccdKeyName], vId["visit"])) 

320 

321 # create temporary catalog 

322 tmpCat = SourceCatalog(SourceCatalog(newSchema).table) 

323 tmpCat.extend(oldSrc, mapper=mapper) 

324 tmpCat['base_PsfFlux_snr'][:] = tmpCat['base_PsfFlux_instFlux'] \ 

325 / tmpCat['base_PsfFlux_instFluxErr'] 

326 

327 if doApplyExternalSkyWcs: 

328 afwTable.updateSourceCoords(wcs, tmpCat) 

329 photoCalib.instFluxToMagnitude(tmpCat, "base_PsfFlux", "base_PsfFlux") 

330 if not skipNonSrd: 

331 tmpCat['slot_ModelFlux_snr'][:] = (tmpCat['slot_ModelFlux_instFlux'] / 

332 tmpCat['slot_ModelFlux_instFluxErr']) 

333 photoCalib.instFluxToMagnitude(tmpCat, "slot_ModelFlux", "slot_ModelFlux") 

334 

335 if not skipTEx: 

336 _, psf_e1, psf_e2 = ellipticity_from_cat(oldSrc, slot_shape='slot_PsfShape') 

337 _, star_e1, star_e2 = ellipticity_from_cat(oldSrc, slot_shape='slot_Shape') 

338 tmpCat['e1'][:] = star_e1 

339 tmpCat['e2'][:] = star_e2 

340 tmpCat['psf_e1'][:] = psf_e1 

341 tmpCat['psf_e2'][:] = psf_e2 

342 

343 srcVis.extend(tmpCat, False) 

344 mmatch.add(catalog=tmpCat, dataId=vId) 

345 

346 # Complete the match, returning a catalog that includes 

347 # all matched sources with object IDs that can be used to group them. 

348 matchCat = mmatch.finish() 

349 

350 # Create a mapping object that allows the matches to be manipulated 

351 # as a mapping of object ID to catalog of sources. 

352 allMatches = GroupView.build(matchCat) 

353 

354 return srcVis, allMatches 

355 

356 

357def getKeysFilter(schema, nameFluxKey=None): 

358 """ Get schema keys for filtering sources. 

359 

360 schema : `lsst.afw.table.Schema` 

361 A table schema to retrieve keys from. 

362 nameFluxKey : `str` 

363 The name of a flux field to retrieve 

364 

365 Returns 

366 ------- 

367 keys : `lsst.pipe.base.Struct` 

368 A struct storing schema keys to aggregate over. 

369 """ 

370 if nameFluxKey is None: 

371 nameFluxKey = "base_PsfFlux" 

372 # Filter down to matches with at least 2 sources and good flags 

373 

374 return pipeBase.Struct( 

375 flags=[schema.find("base_PixelFlags_flag_%s" % flag).key 

376 for flag in ("saturated", "cr", "bad", "edge")], 

377 snr=schema.find(f"{nameFluxKey}_snr").key, 

378 mag=schema.find(f"{nameFluxKey}_mag").key, 

379 magErr=schema.find(f"{nameFluxKey}_magErr").key, 

380 extended=schema.find("base_ClassificationExtendedness_value").key, 

381 ) 

382 

383 

384def filterSources(allMatches, keys=None, faintSnrMin=None, brightSnrMin=None, safeExtendedness=None, 

385 extended=False, faintSnrMax=None, brightSnrMax=None): 

386 """Filter matched sources on flags and SNR. 

387 

388 Parameters 

389 ---------- 

390 allMatches : `lsst.afw.table.GroupView` 

391 GroupView object with matches. 

392 keys : `lsst.pipe.base.Struct` 

393 A struct storing schema keys to aggregate over. 

394 faintSnrMin : float, optional 

395 Minimum median SNR for a faint source match; default 5. 

396 brightSnrMin : float, optional 

397 Minimum median SNR for a bright source match; default 50. 

398 safeExtendedness: float, optional 

399 Maximum (exclusive) extendedness for sources or minimum (inclusive) if extended==True; default 1. 

400 extended: bool, optional 

401 Whether to select extended sources, i.e. galaxies. 

402 faintSnrMax : float, optional 

403 Maximum median SNR for a faint source match; default `numpy.Inf`. 

404 brightSnrMax : float, optional 

405 Maximum median SNR for a bright source match; default `numpy.Inf`. 

406 

407 Returns 

408 ------- 

409 filterResult : `lsst.pipe.base.Struct` 

410 A struct containing good and safe matches and the necessary keys to use them. 

411 """ 

412 if brightSnrMin is None: 

413 brightSnrMin = 50 

414 if brightSnrMax is None: 

415 brightSnrMax = np.Inf 

416 if faintSnrMin is None: 

417 faintSnrMin = 5 

418 if faintSnrMax is None: 

419 faintSnrMax = np.Inf 

420 if safeExtendedness is None: 

421 safeExtendedness = 1.0 

422 if keys is None: 

423 keys = getKeysFilter(allMatches.schema, "slot_ModelFlux" if extended else "base_PsfFlux") 

424 nMatchesRequired = 2 

425 snrMin, snrMax = faintSnrMin, faintSnrMax 

426 

427 def extendedFilter(cat): 

428 if len(cat) < nMatchesRequired: 

429 return False 

430 for flagKey in keys.flags: 

431 if cat.get(flagKey).any(): 

432 return False 

433 if not np.isfinite(cat.get(keys.mag)).all(): 

434 return False 

435 extendedness = cat.get(keys.extended) 

436 return np.min(extendedness) >= safeExtendedness if extended else \ 

437 np.max(extendedness) < safeExtendedness 

438 

439 def snrFilter(cat): 

440 # Note that this also implicitly checks for psfSnr being non-nan. 

441 snr = np.median(cat.get(keys.snr)) 

442 return snrMax >= snr >= snrMin 

443 

444 def fullFilter(cat): 

445 return extendedFilter(cat) and snrFilter(cat) 

446 

447 # If brightSnrMin range is a subset of faintSnrMin, it's safe to only filter on snr again 

448 # Otherwise, filter on flags/extendedness first, then snr 

449 isSafeSubset = faintSnrMax >= brightSnrMax and faintSnrMin <= brightSnrMin 

450 matchesFaint = allMatches.where(fullFilter) if isSafeSubset else allMatches.where(extendedFilter) 

451 snrMin, snrMax = brightSnrMin, brightSnrMax 

452 matchesBright = matchesFaint.where(snrFilter) 

453 # This means that matchesFaint has had extendedFilter but not snrFilter applied 

454 if not isSafeSubset: 

455 snrMin, snrMax = faintSnrMin, faintSnrMax 

456 matchesFaint = matchesFaint.where(snrFilter) 

457 

458 return pipeBase.Struct( 

459 extended=extended, keys=keys, matchesFaint=matchesFaint, matchesBright=matchesBright, 

460 ) 

461 

462 

463def summarizeSources(blob, filterResult): 

464 """Calculate summary statistics for each source. These are persisted 

465 as object attributes. 

466 

467 Parameters 

468 ---------- 

469 blob : `lsst.verify.blob.Blob` 

470 A verification blob to store Datums in. 

471 filterResult : `lsst.pipe.base.Struct` 

472 A struct containing bright and faint filter matches, as returned by `filterSources`. 

473 """ 

474 # Pass field=psfMagKey so np.mean just gets that as its input 

475 typeMag = "model" if filterResult.extended else "PSF" 

476 filter_name = blob['filterName'] 

477 source_type = f'{"extended" if filterResult.extended else "point"} sources"' 

478 matches = filterResult.matchesFaint 

479 keys = filterResult.keys 

480 blob['snr'] = Datum(quantity=matches.aggregate(np.median, field=keys.snr) * u.Unit(''), 

481 label='SNR({band})'.format(band=filter_name), 

482 description=f'Median signal-to-noise ratio of {typeMag} magnitudes for {source_type}' 

483 f' over multiple visits') 

484 blob['mag'] = Datum(quantity=matches.aggregate(np.mean, field=keys.mag) * u.mag, 

485 label='{band}'.format(band=filter_name), 

486 description=f'Mean of {typeMag} magnitudes for {source_type} over multiple visits') 

487 blob['magrms'] = Datum(quantity=matches.aggregate(np.std, field=keys.mag) * u.mag, 

488 label='RMS({band})'.format(band=filter_name), 

489 description=f'RMS of {typeMag} magnitudes for {source_type} over multiple visits') 

490 blob['magerr'] = Datum(quantity=matches.aggregate(np.median, field=keys.magErr) * u.mag, 

491 label='sigma({band})'.format(band=filter_name), 

492 description=f'Median 1-sigma uncertainty of {typeMag} magnitudes for {source_type}' 

493 f' over multiple visits') 

494 # positionRmsFromCat knows how to query a group 

495 # so we give it the whole thing by going with the default `field=None`. 

496 blob['dist'] = Datum(quantity=matches.aggregate(positionRmsFromCat) * u.milliarcsecond, 

497 label='d', 

498 description=f'RMS of sky coordinates of {source_type} over multiple visits') 

499 

500 # These attributes are not serialized 

501 blob.matchesFaint = filterResult.matchesFaint 

502 blob.matchesBright = filterResult.matchesBright 

503 

504 

505def _loadPhotoCalib(butler, dataId, doApplyExternalPhotoCalib, externalPhotoCalibName): 

506 """ 

507 Load a photoCalib object. 

508 

509 Parameters 

510 ---------- 

511 butler: `lsst.daf.persistence.Butler` 

512 dataId: Butler dataId `dict` 

513 doApplyExternalPhotoCalib: `bool` 

514 Apply external photoCalib to calibrate fluxes. 

515 externalPhotoCalibName: `str` 

516 Type of external `PhotoCalib` to apply. Currently supported are jointcal, 

517 fgcm, and fgcm_tract. Must be set if "doApplyExternalPhotoCalib" is True. 

518 

519 Returns 

520 ------- 

521 photoCalib: `lsst.afw.image.PhotoCalib` or None 

522 photoCalib to apply. None if a suitable one was not found. 

523 """ 

524 

525 photoCalib = None 

526 

527 if doApplyExternalPhotoCalib: 

528 try: 

529 photoCalib = butler.get(f"{externalPhotoCalibName}_photoCalib", dataId) 

530 except (FitsError, dafPersist.NoResults) as e: 

531 print(e) 

532 print(f'Could not open external photometric calib for {dataId}; skipping.') 

533 photoCalib = None 

534 else: 

535 try: 

536 photoCalib = butler.get('calexp_photoCalib', dataId) 

537 except (FitsError, dafPersist.NoResults) as e: 

538 print(e) 

539 print(f'Could not open calibrated image file for {dataId}; skipping.') 

540 except TypeError as te: 

541 # DECam images that haven't been properly reformatted 

542 # can trigger a TypeError because of a residual FITS header 

543 # LTV2 which is a float instead of the expected integer. 

544 # This generates an error of the form: 

545 # 

546 # lsst::pex::exceptions::TypeError: 'LTV2 has mismatched type' 

547 # 

548 # See, e.g., DM-2957 for details. 

549 print(te) 

550 print(f'Calibration image header information malformed for {dataId}; skipping.') 

551 photoCalib = None 

552 

553 return photoCalib 

554 

555 

556def _loadExternalSkyWcs(butler, dataId, externalSkyWcsName): 

557 """ 

558 Load a SkyWcs object. 

559 

560 Parameters 

561 ---------- 

562 butler: `lsst.daf.persistence.Butler` 

563 dataId: Butler dataId `dict` 

564 externalSkyWcsName: `str` 

565 Type of external `SkyWcs` to apply. Currently supported is jointcal. 

566 Must be not None if "doApplyExternalSkyWcs" is True. 

567 

568 Returns 

569 ------- 

570 SkyWcs: `lsst.afw.geom.SkyWcs` or None 

571 SkyWcs to apply. None if a suitable one was not found. 

572 """ 

573 

574 try: 

575 wcs = butler.get(f"{externalSkyWcsName}_wcs", dataId) 

576 except (FitsError, dafPersist.NoResults) as e: 

577 print(e) 

578 print(f'Could not open external WCS for {dataId}; skipping.') 

579 wcs = None 

580 

581 return wcs