23"""Build star observations for input to FGCM using sourceTable_visit.
25This task finds all the visits and sourceTable_visits in a repository (or a
26subset based on command line parameters) and extracts all the potential
27calibration stars for input into fgcm. This task additionally uses fgcm to
28match star observations into unique stars, and performs
as much cleaning of the
29input catalog
as possible.
37import lsst.pex.config as pexConfig
38import lsst.pipe.base as pipeBase
39from lsst.pipe.base import connectionTypes
40import lsst.afw.table as afwTable
41from lsst.meas.algorithms import ReferenceObjectLoader, LoadReferenceObjectsConfig
43from .fgcmBuildStarsBase import FgcmBuildStarsConfigBase, FgcmBuildStarsBaseTask
44from .utilities import computeApproxPixelAreaFields, computeApertureRadiusFromName
45from .utilities import lookupStaticCalibrations
47__all__ = ['FgcmBuildStarsTableConfig', 'FgcmBuildStarsTableTask']
50class FgcmBuildStarsTableConnections(pipeBase.PipelineTaskConnections,
51 dimensions=("instrument",),
53 camera = connectionTypes.PrerequisiteInput(
54 doc=
"Camera instrument",
56 storageClass=
"Camera",
57 dimensions=(
"instrument",),
58 lookupFunction=lookupStaticCalibrations,
62 fgcmLookUpTable = connectionTypes.PrerequisiteInput(
63 doc=(
"Atmosphere + instrument look-up-table for FGCM throughput and "
64 "chromatic corrections."),
65 name=
"fgcmLookUpTable",
66 storageClass=
"Catalog",
67 dimensions=(
"instrument",),
71 sourceSchema = connectionTypes.InitInput(
72 doc=
"Schema for source catalogs",
74 storageClass=
"SourceCatalog",
77 refCat = connectionTypes.PrerequisiteInput(
78 doc=
"Reference catalog to use for photometric calibration",
80 storageClass=
"SimpleCatalog",
81 dimensions=(
"skypix",),
86 sourceTable_visit = connectionTypes.Input(
87 doc=
"Source table in parquet format, per visit",
88 name=
"sourceTable_visit",
89 storageClass=
"DataFrame",
90 dimensions=(
"instrument",
"visit"),
95 visitSummary = connectionTypes.Input(
96 doc=(
"Per-visit consolidated exposure metadata. These catalogs use "
97 "detector id for the id and must be sorted for fast lookups of a "
100 storageClass=
"ExposureCatalog",
101 dimensions=(
"instrument",
"visit"),
106 background = connectionTypes.Input(
107 doc=
"Calexp background model",
108 name=
"calexpBackground",
109 storageClass=
"Background",
110 dimensions=(
"instrument",
"visit",
"detector"),
115 fgcmVisitCatalog = connectionTypes.Output(
116 doc=
"Catalog of visit information for fgcm",
117 name=
"fgcmVisitCatalog",
118 storageClass=
"Catalog",
119 dimensions=(
"instrument",),
122 fgcmStarObservations = connectionTypes.Output(
123 doc=
"Catalog of star observations for fgcm",
124 name=
"fgcmStarObservations",
125 storageClass=
"Catalog",
126 dimensions=(
"instrument",),
129 fgcmStarIds = connectionTypes.Output(
130 doc=
"Catalog of fgcm calibration star IDs",
132 storageClass=
"Catalog",
133 dimensions=(
"instrument",),
136 fgcmStarIndices = connectionTypes.Output(
137 doc=
"Catalog of fgcm calibration star indices",
138 name=
"fgcmStarIndices",
139 storageClass=
"Catalog",
140 dimensions=(
"instrument",),
143 fgcmReferenceStars = connectionTypes.Output(
144 doc=
"Catalog of fgcm-matched reference stars",
145 name=
"fgcmReferenceStars",
146 storageClass=
"Catalog",
147 dimensions=(
"instrument",),
153 if not config.doReferenceMatches:
154 self.prerequisiteInputs.remove(
"refCat")
155 self.prerequisiteInputs.remove(
"fgcmLookUpTable")
157 if not config.doModelErrorsWithBackground:
158 self.inputs.remove(
"background")
160 if not config.doReferenceMatches:
161 self.outputs.remove(
"fgcmReferenceStars")
165 pipelineConnections=FgcmBuildStarsTableConnections):
166 """Config for FgcmBuildStarsTableTask"""
168 referenceCCD = pexConfig.Field(
169 doc=
"Reference CCD for checking PSF and background",
191 sourceSelector.flags.bad = [
'pixelFlags_edge',
192 'pixelFlags_interpolatedCenter',
193 'pixelFlags_saturatedCenter',
194 'pixelFlags_crCenter',
196 'pixelFlags_interpolated',
197 'pixelFlags_saturated',
203 sourceSelector.flags.bad.append(localBackgroundFlagName)
208 sourceSelector.isolated.parentName =
'parentSourceId'
209 sourceSelector.isolated.nChildName =
'deblend_nChild'
211 sourceSelector.unresolved.name =
'extendedness'
216 Build stars for the FGCM
global calibration, using sourceTable_visit catalogs.
218 ConfigClass = FgcmBuildStarsTableConfig
219 _DefaultName = "fgcmBuildStarsTable"
221 canMultiprocess =
False
224 super().
__init__(initInputs=initInputs, **kwargs)
225 if initInputs
is not None:
229 inputRefDict = butlerQC.get(inputRefs)
231 sourceTableHandles = inputRefDict[
'sourceTable_visit']
233 self.log.
info(
"Running with %d sourceTable_visit handles",
234 len(sourceTableHandles))
236 sourceTableHandleDict = {sourceTableHandle.dataId[
'visit']: sourceTableHandle
for
237 sourceTableHandle
in sourceTableHandles}
239 if self.config.doReferenceMatches:
241 lutHandle = inputRefDict[
'fgcmLookUpTable']
245 refConfig.filterMap = self.config.fgcmLoadReferenceCatalog.filterMap
247 for ref
in inputRefs.refCat],
248 refCats=butlerQC.get(inputRefs.refCat),
251 self.makeSubtask(
'fgcmLoadReferenceCatalog',
252 refObjLoader=refObjLoader,
253 refCatName=self.config.connections.refCat)
259 calibFluxApertureRadius =
None
260 if self.config.doSubtractLocalBackground:
263 except RuntimeError
as e:
264 raise RuntimeError(
"Could not determine aperture radius from %s. "
265 "Cannot use doSubtractLocalBackground." %
266 (self.config.instFluxField))
from e
268 visitSummaryHandles = inputRefDict[
'visitSummary']
269 visitSummaryHandleDict = {visitSummaryHandle.dataId[
'visit']: visitSummaryHandle
for
270 visitSummaryHandle
in visitSummaryHandles}
272 camera = inputRefDict[
'camera']
273 groupedHandles = self.
_groupHandles_groupHandles(sourceTableHandleDict,
274 visitSummaryHandleDict)
276 if self.config.doModelErrorsWithBackground:
277 bkgHandles = inputRefDict[
'background']
278 bkgHandleDict = {(bkgHandle.dataId.byName()[
'visit'],
279 bkgHandle.dataId.byName()[
'detector']): bkgHandle
for
280 bkgHandle
in bkgHandles}
285 bkgHandleDict=bkgHandleDict)
287 rad = calibFluxApertureRadius
292 calibFluxApertureRadius=rad)
294 butlerQC.put(visitCat, outputRefs.fgcmVisitCatalog)
295 butlerQC.put(fgcmStarObservationCat, outputRefs.fgcmStarObservations)
297 fgcmStarIdCat, fgcmStarIndicesCat, fgcmRefCat = self.
fgcmMatchStarsfgcmMatchStars(visitCat,
298 fgcmStarObservationCat,
301 butlerQC.put(fgcmStarIdCat, outputRefs.fgcmStarIds)
302 butlerQC.put(fgcmStarIndicesCat, outputRefs.fgcmStarIndices)
303 if fgcmRefCat
is not None:
304 butlerQC.put(fgcmRefCat, outputRefs.fgcmReferenceStars)
306 def _groupHandles(self, sourceTableHandleDict, visitSummaryHandleDict):
307 """Group sourceTable and visitSummary handles.
311 sourceTableHandleDict : `dict` [`int`, `str`]
312 Dict of source tables, keyed by visit.
313 visitSummaryHandleDict : `dict` [int, `str`]
314 Dict of visit summary catalogs, keyed by visit.
318 groupedHandles : `dict` [`int`, `list`]
319 Dictionary with sorted visit keys,
and `list`s
with
320 `lsst.daf.butler.DeferredDataSetHandle`. The first
321 item
in the list will be the visitSummary ref,
and
322 the second will be the source table ref.
324 groupedHandles = collections.defaultdict(list)
325 visits = sorted(sourceTableHandleDict.keys())
328 groupedHandles[visit] = [visitSummaryHandleDict[visit],
329 sourceTableHandleDict[visit]]
331 return groupedHandles
336 calibFluxApertureRadius=None):
337 startTime = time.time()
339 if self.config.doSubtractLocalBackground
and calibFluxApertureRadius
is None:
340 raise RuntimeError(
"Must set calibFluxApertureRadius if doSubtractLocalBackground is True.")
345 outputSchema = sourceMapper.getOutputSchema()
349 for ccdIndex, detector
in enumerate(camera):
350 ccdMapping[detector.getId()] = ccdIndex
356 visitKey = outputSchema[
'visit'].asKey()
357 ccdKey = outputSchema[
'ccd'].asKey()
358 instMagKey = outputSchema[
'instMag'].asKey()
359 instMagErrKey = outputSchema[
'instMagErr'].asKey()
360 deltaMagAperKey = outputSchema[
'deltaMagAper'].asKey()
363 if self.config.doSubtractLocalBackground:
364 localBackgroundArea = np.pi*calibFluxApertureRadius**2.
370 for counter, visit
in enumerate(visitCat):
371 expTime = visit[
'exptime']
373 handle = groupedHandles[visit[
'visit']][-1]
376 inColumns = handle.get(component=
'columns')
378 df = handle.get(parameters={
'columns': columns})
380 goodSrc = self.sourceSelector.selectSources(df)
384 if self.config.doSubtractLocalBackground:
385 localBackground = localBackgroundArea*df[self.config.localBackgroundFluxField].values
386 use, = np.where((goodSrc.selected)
387 & ((df[self.config.instFluxField].values - localBackground) > 0.0))
389 use, = np.where(goodSrc.selected)
392 tempCat.resize(use.size)
394 tempCat[
'ra'][:] = np.deg2rad(df[
'ra'].values[use])
395 tempCat[
'dec'][:] = np.deg2rad(df[
'decl'].values[use])
396 tempCat[
'x'][:] = df[
'x'].values[use]
397 tempCat[
'y'][:] = df[
'y'].values[use]
399 tempCat[visitKey][:] = df[
'visit'].values[use]
400 tempCat[ccdKey][:] = df[detColumn].values[use]
401 tempCat[
'psf_candidate'] = df[self.config.psfCandidateName].values[use]
403 with np.warnings.catch_warnings():
405 np.warnings.simplefilter(
"ignore")
407 instMagInner = -2.5*np.log10(df[self.config.apertureInnerInstFluxField].values[use])
408 instMagErrInner = k*(df[self.config.apertureInnerInstFluxField +
'Err'].values[use]
409 / df[self.config.apertureInnerInstFluxField].values[use])
410 instMagOuter = -2.5*np.log10(df[self.config.apertureOuterInstFluxField].values[use])
411 instMagErrOuter = k*(df[self.config.apertureOuterInstFluxField +
'Err'].values[use]
412 / df[self.config.apertureOuterInstFluxField].values[use])
413 tempCat[deltaMagAperKey][:] = instMagInner - instMagOuter
415 tempCat[deltaMagAperKey][~np.isfinite(tempCat[deltaMagAperKey][:])] = 99.0
417 if self.config.doSubtractLocalBackground:
431 tempCat[
'deltaMagBkg'] = (-2.5*np.log10(df[self.config.instFluxField].values[use]
432 - localBackground[use]) -
433 -2.5*np.log10(df[self.config.instFluxField].values[use]))
435 tempCat[
'deltaMagBkg'][:] = 0.0
438 for detector
in camera:
439 ccdId = detector.getId()
441 use2 = (tempCat[ccdKey] == ccdId)
442 tempCat[
'jacobian'][use2] = approxPixelAreaFields[ccdId].evaluate(tempCat[
'x'][use2],
444 scaledInstFlux = (df[self.config.instFluxField].values[use[use2]]
445 * visit[
'scaling'][ccdMapping[ccdId]])
446 tempCat[instMagKey][use2] = (-2.5*np.log10(scaledInstFlux) + 2.5*np.log10(expTime))
450 tempCat[instMagErrKey][:] = k*(df[self.config.instFluxField +
'Err'].values[use]
451 / df[self.config.instFluxField].values[use])
454 if self.config.doApplyWcsJacobian:
455 tempCat[instMagKey][:] -= 2.5*np.log10(tempCat[
'jacobian'][:])
457 fullCatalog.extend(tempCat)
459 deltaOk = (np.isfinite(instMagInner) & np.isfinite(instMagErrInner)
460 & np.isfinite(instMagOuter) & np.isfinite(instMagErrOuter))
462 visit[
'deltaAper'] = np.median(instMagInner[deltaOk] - instMagOuter[deltaOk])
463 visit[
'sources_read'] =
True
465 self.log.
info(
" Found %d good stars in visit %d (deltaAper = %0.3f)",
466 use.size, visit[
'visit'], visit[
'deltaAper'])
468 self.log.
info(
"Found all good star observations in %.2f s" %
469 (time.time() - startTime))
473 def _get_sourceTable_visit_columns(self, inColumns):
475 Get the sourceTable_visit columns from the config.
480 List of columns available
in the sourceTable_visit
485 List of columns to read
from sourceTable_visit.
486 detectorColumn : `str`
487 Name of the detector column.
489 if 'detector' in inColumns:
491 detectorColumn =
'detector'
494 detectorColumn =
'ccd'
496 columns = [
'visit', detectorColumn,
497 'ra',
'decl',
'x',
'y', self.config.psfCandidateName,
498 self.config.instFluxField, self.config.instFluxField +
'Err',
499 self.config.apertureInnerInstFluxField, self.config.apertureInnerInstFluxField +
'Err',
500 self.config.apertureOuterInstFluxField, self.config.apertureOuterInstFluxField +
'Err']
501 if self.sourceSelector.config.doFlags:
502 columns.extend(self.sourceSelector.config.flags.bad)
503 if self.sourceSelector.config.doUnresolved:
504 columns.append(self.sourceSelector.config.unresolved.name)
505 if self.sourceSelector.config.doIsolated:
506 columns.append(self.sourceSelector.config.isolated.parentName)
507 columns.append(self.sourceSelector.config.isolated.nChildName)
508 if self.config.doSubtractLocalBackground:
509 columns.append(self.config.localBackgroundFluxField)
511 return columns, detectorColumn
def fgcmMatchStars(self, visitCat, obsCat, lutHandle=None)
def _makeSourceMapper(self, sourceSchema)
def fgcmMakeAllStarObservations(self, groupedHandles, visitCat, sourceSchema, camera, calibFluxApertureRadius=None)
def fgcmMakeVisitCatalog(self, camera, groupedHandles, bkgHandleDict=None)
apertureOuterInstFluxField
apertureInnerInstFluxField
doSubtractLocalBackground
apertureInnerInstFluxField
apertureOuterInstFluxField
def __init__(self, *config=None)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def fgcmMakeAllStarObservations(self, groupedHandles, visitCat, sourceSchema, camera, calibFluxApertureRadius=None)
def _get_sourceTable_visit_columns(self, inColumns)
def _groupHandles(self, sourceTableHandleDict, visitSummaryHandleDict)
def __init__(self, initInputs=None, **kwargs)
def computeApertureRadiusFromName(fluxField)
def computeApproxPixelAreaFields(camera)