Coverage for python/lsst/validate/drp/matchreduce.py : 8%

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"""
24__all__ = ['build_matched_dataset', 'getKeysFilter', 'filterSources', 'summarizeSources']
26import numpy as np
27import astropy.units as u
28from sqlalchemy.exc import OperationalError
29import sqlite3
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
41from .util import (getCcdKeyName, raftSensorToInt, positionRmsFromCat,
42 ellipticity_from_cat)
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.
53 `lsst.verify.Blob` instances are serializable to JSON.
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.
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).
106 *Not serialized.*
107 matchesFaint : `afw.table.GroupView`
108 Faint matches containing only objects that have:
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.
116 *Not serialized.*
117 matchesBright : `afw.table.GroupView`
118 Bright matches matching a higher S/N threshold than matchesFaint.
120 *Not serialized.*
121 magKey
122 Key for `"base_PsfFlux_mag"` in the `matchesFaint` and `matchesBright`
123 catalog tables.
125 *Not serialized.*
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.")
139 blob = Blob('MatchedMultiVisitDataset')
141 if not matchRadius:
142 matchRadius = geom.Angle(1, geom.arcseconds)
144 # Extract single filter
145 blob['filterName'] = Datum(quantity=set([dId['filter'] for dId in dataIds]).pop(),
146 description='Filter name')
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.')
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)
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 blob['brightSnrMin'] = Datum(quantity=filterResult.brightSnrMin * u.Unit(''),
176 label='Bright SNR Min',
177 description='Minimum median SNR for a source to be considered bright')
178 blob['brightSnrMax'] = Datum(quantity=filterResult.brightSnrMax * u.Unit(''),
179 label='Bright SNR Max',
180 description='Maximum median SNR for a source to be considered bright')
181 summarizeSources(blob, filterResult)
182 return blob
185def _loadAndMatchCatalogs(repo, dataIds, matchRadius,
186 doApplyExternalPhotoCalib=False, externalPhotoCalibName=None,
187 doApplyExternalSkyWcs=False, externalSkyWcsName=None,
188 skipTEx=False, skipNonSrd=False):
189 """Load data from specific visits and returned a calibrated catalog matched
190 with a reference.
192 Parameters
193 ----------
194 repo : `str` or `lsst.daf.persistence.Butler`
195 A Butler or a repository URL that can be used to construct one.
196 dataIds : list of dict
197 List of butler data IDs of Image catalogs to compare to
198 reference. The calexp cpixel image is needed for the photometric
199 calibration.
200 matchRadius : `lsst.geom.Angle`, optional
201 Radius for matching. Default is 1 arcsecond.
202 doApplyExternalPhotoCalib : bool, optional
203 Apply external photoCalib to calibrate fluxes.
204 externalPhotoCalibName : str, optional
205 Type of external `PhotoCalib` to apply. Currently supported are jointcal,
206 fgcm, and fgcm_tract. Must be set if doApplyExternalPhotoCalib is True.
207 doApplyExternalSkyWcs : bool, optional
208 Apply external wcs to calibrate positions.
209 externalSkyWcsName : str, optional
210 Type of external `wcs` to apply. Currently supported is jointcal.
211 Must be set if "doApplyExternalWcs" is True.
212 skipTEx : `bool`, optional
213 Skip TEx calculations (useful for older catalogs that don't have
214 PsfShape measurements).
215 skipNonSrd : `bool`, optional
216 Skip any metrics not defined in the LSST SRD; default False.
218 Returns
219 -------
220 catalog : `lsst.afw.table.SourceCatalog`
221 A new calibrated SourceCatalog.
222 matches : `lsst.afw.table.GroupView`
223 A GroupView of the matched sources.
225 Raises
226 ------
227 RuntimeError:
228 Raised if "doApplyExternalPhotoCalib" is True and "externalPhotoCalibName"
229 is None, or if "doApplyExternalSkyWcs" is True and "externalSkyWcsName" is
230 None.
231 """
233 if doApplyExternalPhotoCalib and externalPhotoCalibName is None:
234 raise RuntimeError("Must set externalPhotoCalibName if doApplyExternalPhotoCalib is True.")
235 if doApplyExternalSkyWcs and externalSkyWcsName is None:
236 raise RuntimeError("Must set externalSkyWcsName if doApplyExternalSkyWcs is True.")
238 # Following
239 # https://github.com/lsst/afw/blob/tickets/DM-3896/examples/repeatability.ipynb
240 if isinstance(repo, dafPersist.Butler):
241 butler = repo
242 else:
243 butler = dafPersist.Butler(repo)
244 dataset = 'src'
246 # 2016-02-08 MWV:
247 # I feel like I could be doing something more efficient with
248 # something along the lines of the following:
249 # dataRefs = [dafPersist.ButlerDataRef(butler, vId) for vId in dataIds]
251 ccdKeyName = getCcdKeyName(dataIds[0])
253 # Hack to support raft and sensor 0,1 IDs as ints for multimatch
254 if ccdKeyName == 'sensor':
255 ccdKeyName = 'raft_sensor_int'
256 for vId in dataIds:
257 vId[ccdKeyName] = raftSensorToInt(vId)
259 schema = butler.get(dataset + "_schema").schema
260 mapper = SchemaMapper(schema)
261 mapper.addMinimalSchema(schema)
262 mapper.addOutputField(Field[float]('base_PsfFlux_snr',
263 'PSF flux SNR'))
264 mapper.addOutputField(Field[float]('base_PsfFlux_mag',
265 'PSF magnitude'))
266 mapper.addOutputField(Field[float]('base_PsfFlux_magErr',
267 'PSF magnitude uncertainty'))
268 if not skipNonSrd:
269 # Needed because addOutputField(... 'slot_ModelFlux_mag') will add a field with that literal name
270 aliasMap = schema.getAliasMap()
271 # Possibly not needed since base_GaussianFlux is the default, but this ought to be safe
272 modelName = aliasMap['slot_ModelFlux'] if 'slot_ModelFlux' in aliasMap.keys() else 'base_GaussianFlux'
273 mapper.addOutputField(Field[float](f'{modelName}_mag',
274 'Model magnitude'))
275 mapper.addOutputField(Field[float](f'{modelName}_magErr',
276 'Model magnitude uncertainty'))
277 mapper.addOutputField(Field[float](f'{modelName}_snr',
278 'Model flux snr'))
279 mapper.addOutputField(Field[float]('e1',
280 'Source Ellipticity 1'))
281 mapper.addOutputField(Field[float]('e2',
282 'Source Ellipticity 1'))
283 mapper.addOutputField(Field[float]('psf_e1',
284 'PSF Ellipticity 1'))
285 mapper.addOutputField(Field[float]('psf_e2',
286 'PSF Ellipticity 1'))
287 newSchema = mapper.getOutputSchema()
288 newSchema.setAliasMap(schema.getAliasMap())
290 # Create an object that matches multiple catalogs with same schema
291 mmatch = MultiMatch(newSchema,
292 dataIdFormat={'visit': np.int32, ccdKeyName: np.int32},
293 radius=matchRadius,
294 RecordClass=SimpleRecord)
296 # create the new extented source catalog
297 srcVis = SourceCatalog(newSchema)
299 for vId in dataIds:
300 if not butler.datasetExists('src', vId):
301 print(f'Could not find source catalog for {vId}; skipping.')
302 continue
304 photoCalib = _loadPhotoCalib(butler, vId,
305 doApplyExternalPhotoCalib, externalPhotoCalibName)
306 if photoCalib is None:
307 continue
309 if doApplyExternalSkyWcs:
310 wcs = _loadExternalSkyWcs(butler, vId, externalSkyWcsName)
311 if wcs is None:
312 continue
314 # We don't want to put this above the first _loadPhotoCalib call
315 # because we need to use the first `butler.get` in there to quickly
316 # catch dataIDs with no usable outputs.
317 try:
318 # HSC supports these flags, which dramatically improve I/O
319 # performance; support for other cameras is DM-6927.
320 oldSrc = butler.get('src', vId, flags=SOURCE_IO_NO_FOOTPRINTS)
321 except (OperationalError, sqlite3.OperationalError):
322 oldSrc = butler.get('src', vId)
324 print(len(oldSrc), "sources in ccd %s visit %s" %
325 (vId[ccdKeyName], vId["visit"]))
327 # create temporary catalog
328 tmpCat = SourceCatalog(SourceCatalog(newSchema).table)
329 tmpCat.extend(oldSrc, mapper=mapper)
330 tmpCat['base_PsfFlux_snr'][:] = tmpCat['base_PsfFlux_instFlux'] \
331 / tmpCat['base_PsfFlux_instFluxErr']
333 if doApplyExternalSkyWcs:
334 afwTable.updateSourceCoords(wcs, tmpCat)
335 photoCalib.instFluxToMagnitude(tmpCat, "base_PsfFlux", "base_PsfFlux")
336 if not skipNonSrd:
337 tmpCat['slot_ModelFlux_snr'][:] = (tmpCat['slot_ModelFlux_instFlux'] /
338 tmpCat['slot_ModelFlux_instFluxErr'])
339 photoCalib.instFluxToMagnitude(tmpCat, "slot_ModelFlux", "slot_ModelFlux")
341 if not skipTEx:
342 _, psf_e1, psf_e2 = ellipticity_from_cat(oldSrc, slot_shape='slot_PsfShape')
343 _, star_e1, star_e2 = ellipticity_from_cat(oldSrc, slot_shape='slot_Shape')
344 tmpCat['e1'][:] = star_e1
345 tmpCat['e2'][:] = star_e2
346 tmpCat['psf_e1'][:] = psf_e1
347 tmpCat['psf_e2'][:] = psf_e2
349 srcVis.extend(tmpCat, False)
350 mmatch.add(catalog=tmpCat, dataId=vId)
352 # Complete the match, returning a catalog that includes
353 # all matched sources with object IDs that can be used to group them.
354 matchCat = mmatch.finish()
356 # Create a mapping object that allows the matches to be manipulated
357 # as a mapping of object ID to catalog of sources.
358 allMatches = GroupView.build(matchCat)
360 return srcVis, allMatches
363def getKeysFilter(schema, nameFluxKey=None):
364 """ Get schema keys for filtering sources.
366 schema : `lsst.afw.table.Schema`
367 A table schema to retrieve keys from.
368 nameFluxKey : `str`
369 The name of a flux field to retrieve
371 Returns
372 -------
373 keys : `lsst.pipe.base.Struct`
374 A struct storing schema keys to aggregate over.
375 """
376 if nameFluxKey is None:
377 nameFluxKey = "base_PsfFlux"
378 # Filter down to matches with at least 2 sources and good flags
380 return pipeBase.Struct(
381 flags=[schema.find("base_PixelFlags_flag_%s" % flag).key
382 for flag in ("saturated", "cr", "bad", "edge")],
383 snr=schema.find(f"{nameFluxKey}_snr").key,
384 mag=schema.find(f"{nameFluxKey}_mag").key,
385 magErr=schema.find(f"{nameFluxKey}_magErr").key,
386 extended=schema.find("base_ClassificationExtendedness_value").key,
387 )
390def filterSources(allMatches, keys=None, faintSnrMin=None, brightSnrMin=None, safeExtendedness=None,
391 extended=False, faintSnrMax=None, brightSnrMax=None):
392 """Filter matched sources on flags and SNR.
394 Parameters
395 ----------
396 allMatches : `lsst.afw.table.GroupView`
397 GroupView object with matches.
398 keys : `lsst.pipe.base.Struct`
399 A struct storing schema keys to aggregate over.
400 faintSnrMin : float, optional
401 Minimum median SNR for a faint source match; default 5.
402 brightSnrMin : float, optional
403 Minimum median SNR for a bright source match; default 50.
404 safeExtendedness: float, optional
405 Maximum (exclusive) extendedness for sources or minimum (inclusive) if extended==True; default 1.
406 extended: bool, optional
407 Whether to select extended sources, i.e. galaxies.
408 faintSnrMax : float, optional
409 Maximum median SNR for a faint source match; default `numpy.Inf`.
410 brightSnrMax : float, optional
411 Maximum median SNR for a bright source match; default `numpy.Inf`.
413 Returns
414 -------
415 filterResult : `lsst.pipe.base.Struct`
416 A struct containing good and safe matches and the necessary keys to use them.
417 """
418 if brightSnrMin is None:
419 brightSnrMin = 50
420 if brightSnrMax is None:
421 brightSnrMax = np.Inf
422 if faintSnrMin is None:
423 faintSnrMin = 5
424 if faintSnrMax is None:
425 faintSnrMax = np.Inf
426 if safeExtendedness is None:
427 safeExtendedness = 1.0
428 if keys is None:
429 keys = getKeysFilter(allMatches.schema, "slot_ModelFlux" if extended else "base_PsfFlux")
430 nMatchesRequired = 2
431 snrMin, snrMax = faintSnrMin, faintSnrMax
433 def extendedFilter(cat):
434 if len(cat) < nMatchesRequired:
435 return False
436 for flagKey in keys.flags:
437 if cat.get(flagKey).any():
438 return False
439 if not np.isfinite(cat.get(keys.mag)).all():
440 return False
441 extendedness = cat.get(keys.extended)
442 return np.min(extendedness) >= safeExtendedness if extended else \
443 np.max(extendedness) < safeExtendedness
445 def snrFilter(cat):
446 # Note that this also implicitly checks for psfSnr being non-nan.
447 snr = np.median(cat.get(keys.snr))
448 return snrMax >= snr >= snrMin
450 def fullFilter(cat):
451 return extendedFilter(cat) and snrFilter(cat)
453 # If brightSnrMin range is a subset of faintSnrMin, it's safe to only filter on snr again
454 # Otherwise, filter on flags/extendedness first, then snr
455 isSafeSubset = faintSnrMax >= brightSnrMax and faintSnrMin <= brightSnrMin
456 matchesFaint = allMatches.where(fullFilter) if isSafeSubset else allMatches.where(extendedFilter)
457 snrMin, snrMax = brightSnrMin, brightSnrMax
458 matchesBright = matchesFaint.where(snrFilter)
459 # This means that matchesFaint has had extendedFilter but not snrFilter applied
460 if not isSafeSubset:
461 snrMin, snrMax = faintSnrMin, faintSnrMax
462 matchesFaint = matchesFaint.where(snrFilter)
464 return pipeBase.Struct(
465 extended=extended, keys=keys, matchesFaint=matchesFaint, matchesBright=matchesBright,
466 brightSnrMin=brightSnrMin, brightSnrMax=brightSnrMax,
467 faintSnrMin=faintSnrMin, faintSnrMax=faintSnrMax,
468 )
471def summarizeSources(blob, filterResult):
472 """Calculate summary statistics for each source. These are persisted
473 as object attributes.
475 Parameters
476 ----------
477 blob : `lsst.verify.blob.Blob`
478 A verification blob to store Datums in.
479 filterResult : `lsst.pipe.base.Struct`
480 A struct containing bright and faint filter matches, as returned by `filterSources`.
481 """
482 # Pass field=psfMagKey so np.mean just gets that as its input
483 typeMag = "model" if filterResult.extended else "PSF"
484 filter_name = blob['filterName']
485 source_type = f'{"extended" if filterResult.extended else "point"} sources"'
486 matches = filterResult.matchesFaint
487 keys = filterResult.keys
488 blob['snr'] = Datum(quantity=matches.aggregate(np.median, field=keys.snr) * u.Unit(''),
489 label='SNR({band})'.format(band=filter_name),
490 description=f'Median signal-to-noise ratio of {typeMag} magnitudes for {source_type}'
491 f' over multiple visits')
492 blob['mag'] = Datum(quantity=matches.aggregate(np.mean, field=keys.mag) * u.mag,
493 label='{band}'.format(band=filter_name),
494 description=f'Mean of {typeMag} magnitudes for {source_type} over multiple visits')
495 blob['magrms'] = Datum(quantity=matches.aggregate(np.std, field=keys.mag) * u.mag,
496 label='RMS({band})'.format(band=filter_name),
497 description=f'RMS of {typeMag} magnitudes for {source_type} over multiple visits')
498 blob['magerr'] = Datum(quantity=matches.aggregate(np.median, field=keys.magErr) * u.mag,
499 label='sigma({band})'.format(band=filter_name),
500 description=f'Median 1-sigma uncertainty of {typeMag} magnitudes for {source_type}'
501 f' over multiple visits')
502 # positionRmsFromCat knows how to query a group
503 # so we give it the whole thing by going with the default `field=None`.
504 blob['dist'] = Datum(quantity=matches.aggregate(positionRmsFromCat) * u.milliarcsecond,
505 label='d',
506 description=f'RMS of sky coordinates of {source_type} over multiple visits')
508 # These attributes are not serialized
509 blob.matchesFaint = filterResult.matchesFaint
510 blob.matchesBright = filterResult.matchesBright
513def _loadPhotoCalib(butler, dataId, doApplyExternalPhotoCalib, externalPhotoCalibName):
514 """
515 Load a photoCalib object.
517 Parameters
518 ----------
519 butler: `lsst.daf.persistence.Butler`
520 dataId: Butler dataId `dict`
521 doApplyExternalPhotoCalib: `bool`
522 Apply external photoCalib to calibrate fluxes.
523 externalPhotoCalibName: `str`
524 Type of external `PhotoCalib` to apply. Currently supported are jointcal,
525 fgcm, and fgcm_tract. Must be set if "doApplyExternalPhotoCalib" is True.
527 Returns
528 -------
529 photoCalib: `lsst.afw.image.PhotoCalib` or None
530 photoCalib to apply. None if a suitable one was not found.
531 """
533 photoCalib = None
535 if doApplyExternalPhotoCalib:
536 try:
537 photoCalib = butler.get(f"{externalPhotoCalibName}_photoCalib", dataId)
538 except (FitsError, dafPersist.NoResults) as e:
539 print(e)
540 print(f'Could not open external photometric calib for {dataId}; skipping.')
541 photoCalib = None
542 else:
543 try:
544 photoCalib = butler.get('calexp_photoCalib', dataId)
545 except (FitsError, dafPersist.NoResults) as e:
546 print(e)
547 print(f'Could not open calibrated image file for {dataId}; skipping.')
548 except TypeError as te:
549 # DECam images that haven't been properly reformatted
550 # can trigger a TypeError because of a residual FITS header
551 # LTV2 which is a float instead of the expected integer.
552 # This generates an error of the form:
553 #
554 # lsst::pex::exceptions::TypeError: 'LTV2 has mismatched type'
555 #
556 # See, e.g., DM-2957 for details.
557 print(te)
558 print(f'Calibration image header information malformed for {dataId}; skipping.')
559 photoCalib = None
561 return photoCalib
564def _loadExternalSkyWcs(butler, dataId, externalSkyWcsName):
565 """
566 Load a SkyWcs object.
568 Parameters
569 ----------
570 butler: `lsst.daf.persistence.Butler`
571 dataId: Butler dataId `dict`
572 externalSkyWcsName: `str`
573 Type of external `SkyWcs` to apply. Currently supported is jointcal.
574 Must be not None if "doApplyExternalSkyWcs" is True.
576 Returns
577 -------
578 SkyWcs: `lsst.afw.geom.SkyWcs` or None
579 SkyWcs to apply. None if a suitable one was not found.
580 """
582 try:
583 wcs = butler.get(f"{externalSkyWcsName}_wcs", dataId)
584 except (FitsError, dafPersist.NoResults) as e:
585 print(e)
586 print(f'Could not open external WCS for {dataId}; skipping.')
587 wcs = None
589 return wcs