Coverage for python/lsst/fgcmcal/fgcmCalibrateTract.py : 16%

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# See COPYRIGHT file at the top of the source tree.
2#
3# This file is part of fgcmcal.
4#
5# Developed for the LSST Data Management System.
6# This product includes software developed by the LSST Project
7# (https://www.lsst.org).
8# See the COPYRIGHT file at the top-level directory of this distribution
9# for details of code ownership.
10#
11# This program is free software: you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation, either version 3 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program. If not, see <https://www.gnu.org/licenses/>.
23"""Run fgcmcal on a single tract.
25"""
27import sys
28import traceback
30import numpy as np
32import lsst.pex.config as pexConfig
33import lsst.pipe.base as pipeBase
34from lsst.jointcal.dataIds import PerTractCcdDataIdContainer
36from .fgcmBuildStars import FgcmBuildStarsTask
37from .fgcmFitCycle import FgcmFitCycleConfig
38from .fgcmOutputProducts import FgcmOutputProductsTask
39from .utilities import makeConfigDict, translateFgcmLut, translateVisitCatalog
40from .utilities import computeCcdOffsets, computeApertureRadius, extractReferenceMags
41from .utilities import makeZptSchema, makeZptCat
42from .utilities import makeAtmSchema, makeAtmCat
43from .utilities import makeStdSchema, makeStdCat
45import fgcm
47__all__ = ['FgcmCalibrateTractConfig', 'FgcmCalibrateTractTask', 'FgcmCalibrateTractRunner']
50class FgcmCalibrateTractConfig(pexConfig.Config):
51 """Config for FgcmCalibrateTract"""
53 fgcmBuildStars = pexConfig.ConfigurableField(
54 target=FgcmBuildStarsTask,
55 doc="Task to load and match stars for fgcm",
56 )
57 fgcmFitCycle = pexConfig.ConfigField(
58 dtype=FgcmFitCycleConfig,
59 doc="Config to run a single fgcm fit cycle",
60 )
61 fgcmOutputProducts = pexConfig.ConfigurableField(
62 target=FgcmOutputProductsTask,
63 doc="Task to output fgcm products",
64 )
65 convergenceTolerance = pexConfig.Field(
66 doc="Tolerance on repeatability convergence (per band)",
67 dtype=float,
68 default=0.005,
69 )
70 maxFitCycles = pexConfig.Field(
71 doc="Maximum number of fit cycles",
72 dtype=int,
73 default=5,
74 )
75 doDebuggingPlots = pexConfig.Field(
76 doc="Make plots for debugging purposes?",
77 dtype=bool,
78 default=False,
79 )
81 def setDefaults(self):
82 pexConfig.Config.setDefaults(self)
84 self.fgcmBuildStars.checkAllCcds = False
85 self.fgcmBuildStars.densityCutMaxPerPixel = 10000
86 self.fgcmFitCycle.useRepeatabilityForExpGrayCuts = [True]
87 self.fgcmFitCycle.quietMode = True
88 self.fgcmOutputProducts.doReferenceCalibration = False
89 self.fgcmOutputProducts.doRefcatOutput = False
90 self.fgcmOutputProducts.cycleNumber = 0
91 self.fgcmOutputProducts.photoCal.applyColorTerms = False
94class FgcmCalibrateTractRunner(pipeBase.ButlerInitializedTaskRunner):
95 """Subclass of TaskRunner for FgcmCalibrateTractTask
97 fgcmCalibrateTractTask.run() takes a number of arguments, one of which is
98 the butler (for persistence and mapper data), and a list of dataRefs
99 extracted from the command line. This task runs on a constrained set
100 of dataRefs, typically a single tract.
101 This class transforms the process arguments generated by the ArgumentParser
102 into the arguments expected by FgcmCalibrateTractTask.run().
103 This runner does not use any parallelization.
104 """
106 @staticmethod
107 def getTargetList(parsedCmd):
108 """
109 Return a list with one element: a tuple with the butler and
110 list of dataRefs
111 """
112 # we want to combine the butler with any (or no!) dataRefs
113 return [(parsedCmd.butler, parsedCmd.id.refList)]
115 def __call__(self, args):
116 """
117 Parameters
118 ----------
119 args: `tuple` with (butler, dataRefList)
121 Returns
122 -------
123 exitStatus: `list` with `lsst.pipe.base.Struct`
124 exitStatus (0: success; 1: failure)
125 """
126 butler, dataRefList = args
128 task = self.TaskClass(config=self.config, log=self.log)
130 exitStatus = 0
131 if self.doRaise:
132 results = task.runDataRef(butler, dataRefList)
133 else:
134 try:
135 results = task.runDataRef(butler, dataRefList)
136 except Exception as e:
137 exitStatus = 1
138 task.log.fatal("Failed: %s" % e)
139 if not isinstance(e, pipeBase.TaskError):
140 traceback.print_exc(file=sys.stderr)
142 task.writeMetadata(butler)
144 if self.doReturnResults:
145 return [pipeBase.Struct(exitStatus=exitStatus,
146 results=results)]
147 else:
148 return [pipeBase.Struct(exitStatus=exitStatus)]
150 def run(self, parsedCmd):
151 """
152 Run the task, with no multiprocessing
154 Parameters
155 ----------
156 parsedCmd: `lsst.pipe.base.ArgumentParser` parsed command line
157 """
159 resultList = []
161 if self.precall(parsedCmd):
162 targetList = self.getTargetList(parsedCmd)
163 resultList = self(targetList[0])
165 return resultList
168class FgcmCalibrateTractTask(pipeBase.CmdLineTask):
169 """
170 Calibrate a single tract using fgcmcal
171 """
173 ConfigClass = FgcmCalibrateTractConfig
174 RunnerClass = FgcmCalibrateTractRunner
175 _DefaultName = "fgcmCalibrateTract"
177 def __init__(self, butler=None, **kwargs):
178 """
179 Instantiate an `FgcmCalibrateTractTask`.
181 Parameters
182 ----------
183 butler : `lsst.daf.persistence.Butler`
184 """
186 pipeBase.CmdLineTask.__init__(self, **kwargs)
188 @classmethod
189 def _makeArgumentParser(cls):
190 """Create an argument parser"""
192 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
193 parser.add_id_argument("--id", "calexp", help="Data ID, e.g. --id visit=6789",
194 ContainerClass=PerTractCcdDataIdContainer)
196 return parser
198 # no saving of metadata for now
199 def _getMetadataName(self):
200 return None
202 @pipeBase.timeMethod
203 def runDataRef(self, butler, dataRefs):
204 """
205 Run full FGCM calibration on a single tract, including building star list,
206 fitting multiple cycles, and making outputs.
208 Parameters
209 ----------
210 butler: `lsst.daf.persistence.Butler`
211 dataRefs: `list` of `lsst.daf.persistence.ButlerDataRef`
212 Data references for the input visits.
213 If this is an empty list, all visits with src catalogs in
214 the repository are used.
215 Only one individual dataRef from a visit need be specified
216 and the code will find the other source catalogs from
217 each visit.
219 Raises
220 ------
221 RuntimeError: Raised if `config.fgcmBuildStars.doReferenceMatches` is
222 not True, or if fgcmLookUpTable is not available, or if
223 doSubtractLocalBackground is True and aperture radius cannot be
224 determined.
225 """
227 if not butler.datasetExists('fgcmLookUpTable'):
228 raise RuntimeError("Must run FgcmCalibrateTract with an fgcmLookUpTable")
230 if not self.config.fgcmBuildStars.doReferenceMatches:
231 raise RuntimeError("Must run FgcmCalibrateTract with fgcmBuildStars.doReferenceMatches")
232 if self.config.fgcmBuildStars.checkAllCcds:
233 raise RuntimeError("Cannot run FgcmCalibrateTract with fgcmBuildStars.checkAllCcds set to True")
235 self.makeSubtask("fgcmBuildStars", butler=butler)
236 self.makeSubtask("fgcmOutputProducts", butler=butler)
238 # Compute the aperture radius if necessary. This is useful to do now before
239 # any heavy lifting has happened (fail early).
240 calibFluxApertureRadius = None
241 if self.config.fgcmBuildStars.doSubtractLocalBackground:
242 sourceSchema = butler.get('src_schema').schema
243 try:
244 calibFluxApertureRadius = computeApertureRadius(sourceSchema,
245 self.config.fgcmBuildStars.instFluxField)
246 except (RuntimeError, LookupError):
247 raise RuntimeError("Could not determine aperture radius from %s. "
248 "Cannot use doSubtractLocalBackground." %
249 (self.config.instFluxField))
251 # Run the build stars tasks
252 tract = dataRefs[0].dataId['tract']
253 self.log.info("Running on tract %d" % (tract))
255 # Note that we will need visitCat at the end of the procedure for the outputs
256 groupedDataRefs = self.fgcmBuildStars.findAndGroupDataRefs(butler, dataRefs)
257 camera = butler.get('camera')
258 visitCat = self.fgcmBuildStars.fgcmMakeVisitCatalog(camera, groupedDataRefs)
259 rad = calibFluxApertureRadius
260 fgcmStarObservationCat = self.fgcmBuildStars.fgcmMakeAllStarObservations(groupedDataRefs,
261 visitCat,
262 calibFluxApertureRadius=rad)
264 fgcmStarIdCat, fgcmStarIndicesCat, fgcmRefCat = \
265 self.fgcmBuildStars.fgcmMatchStars(butler,
266 visitCat,
267 fgcmStarObservationCat)
269 # Load the LUT
270 lutCat = butler.get('fgcmLookUpTable')
271 fgcmLut, lutIndexVals, lutStd = translateFgcmLut(lutCat,
272 dict(self.config.fgcmFitCycle.filterMap))
273 del lutCat
275 # Translate the visit catalog into fgcm format
276 fgcmExpInfo = translateVisitCatalog(visitCat)
278 camera = butler.get('camera')
279 configDict = makeConfigDict(self.config.fgcmFitCycle, self.log, camera,
280 self.config.fgcmFitCycle.maxIterBeforeFinalCycle,
281 True, False, tract=tract)
282 # Turn off plotting in tract mode
283 configDict['doPlots'] = False
285 # Use the first orientation.
286 # TODO: DM-21215 will generalize to arbitrary camera orientations
287 ccdOffsets = computeCcdOffsets(camera, fgcmExpInfo['TELROT'][0])
288 del camera
290 # Set up the fit cycle task
292 noFitsDict = {'lutIndex': lutIndexVals,
293 'lutStd': lutStd,
294 'expInfo': fgcmExpInfo,
295 'ccdOffsets': ccdOffsets}
297 fgcmFitCycle = fgcm.FgcmFitCycle(configDict, useFits=False,
298 noFitsDict=noFitsDict, noOutput=True)
300 # We determine the conversion from the native units (typically radians) to
301 # degrees for the first star. This allows us to treat coord_ra/coord_dec as
302 # numpy arrays rather than Angles, which would we approximately 600x slower.
303 conv = fgcmStarObservationCat[0]['ra'].asDegrees() / float(fgcmStarObservationCat[0]['ra'])
305 # To load the stars, we need an initial parameter object
306 fgcmPars = fgcm.FgcmParameters.newParsWithArrays(fgcmFitCycle.fgcmConfig,
307 fgcmLut,
308 fgcmExpInfo)
310 # Match star observations to visits
311 # Only those star observations that match visits from fgcmExpInfo['VISIT'] will
312 # actually be transferred into fgcm using the indexing below.
314 obsIndex = fgcmStarIndicesCat['obsIndex']
315 visitIndex = np.searchsorted(fgcmExpInfo['VISIT'],
316 fgcmStarObservationCat['visit'][obsIndex])
318 refMag, refMagErr = extractReferenceMags(fgcmRefCat,
319 self.config.fgcmFitCycle.bands,
320 self.config.fgcmFitCycle.filterMap)
321 refId = fgcmRefCat['fgcm_id'][:]
323 fgcmStars = fgcm.FgcmStars(fgcmFitCycle.fgcmConfig)
324 fgcmStars.loadStars(fgcmPars,
325 fgcmStarObservationCat['visit'][obsIndex],
326 fgcmStarObservationCat['ccd'][obsIndex],
327 fgcmStarObservationCat['ra'][obsIndex] * conv,
328 fgcmStarObservationCat['dec'][obsIndex] * conv,
329 fgcmStarObservationCat['instMag'][obsIndex],
330 fgcmStarObservationCat['instMagErr'][obsIndex],
331 fgcmExpInfo['FILTERNAME'][visitIndex],
332 fgcmStarIdCat['fgcm_id'][:],
333 fgcmStarIdCat['ra'][:],
334 fgcmStarIdCat['dec'][:],
335 fgcmStarIdCat['obsArrIndex'][:],
336 fgcmStarIdCat['nObs'][:],
337 obsX=fgcmStarObservationCat['x'][obsIndex],
338 obsY=fgcmStarObservationCat['y'][obsIndex],
339 psfCandidate=fgcmStarObservationCat['psf_candidate'][obsIndex],
340 refID=refId,
341 refMag=refMag,
342 refMagErr=refMagErr,
343 flagID=None,
344 flagFlag=None,
345 computeNobs=True)
347 fgcmFitCycle.setLUT(fgcmLut)
348 fgcmFitCycle.setStars(fgcmStars)
350 # Clear out some memory
351 # del fgcmStarObservationCat
352 del fgcmStarIdCat
353 del fgcmStarIndicesCat
354 del fgcmRefCat
356 converged = False
357 cycleNumber = 0
359 previousReservedRawRepeatability = np.zeros(fgcmPars.nBands) + 1000.0
360 previousParInfo = None
361 previousParams = None
362 previousSuperStar = None
364 while (not converged and cycleNumber < self.config.maxFitCycles):
366 fgcmFitCycle.fgcmConfig.updateCycleNumber(cycleNumber)
368 if cycleNumber > 0:
369 # Use parameters from previous cycle
370 fgcmPars = fgcm.FgcmParameters.loadParsWithArrays(fgcmFitCycle.fgcmConfig,
371 fgcmExpInfo,
372 previousParInfo,
373 previousParams,
374 previousSuperStar)
375 # We need to reset the star magnitudes and errors for the next
376 # cycle
377 fgcmFitCycle.fgcmStars.reloadStarMagnitudes(fgcmStarObservationCat['instMag'][obsIndex],
378 fgcmStarObservationCat['instMagErr'][obsIndex])
379 fgcmFitCycle.initialCycle = False
381 fgcmFitCycle.setPars(fgcmPars)
382 fgcmFitCycle.finishSetup()
384 fgcmFitCycle.run()
386 # Grab the parameters for the next cycle
387 previousParInfo, previousParams = fgcmFitCycle.fgcmPars.parsToArrays()
388 previousSuperStar = fgcmFitCycle.fgcmPars.parSuperStarFlat.copy()
390 self.log.info("Raw repeatability after cycle number %d is:" % (cycleNumber))
391 for i, band in enumerate(fgcmFitCycle.fgcmPars.bands):
392 if not fgcmFitCycle.fgcmPars.hasExposuresInBand[i]:
393 continue
394 rep = fgcmFitCycle.fgcmPars.compReservedRawRepeatability[i] * 1000.0
395 self.log.info(" Band %s, repeatability: %.2f mmag" % (band, rep))
397 # Check for convergence
398 if np.alltrue((previousReservedRawRepeatability -
399 fgcmFitCycle.fgcmPars.compReservedRawRepeatability) <
400 self.config.convergenceTolerance):
401 self.log.info("Raw repeatability has converged after cycle number %d." % (cycleNumber))
402 converged = True
403 else:
404 fgcmFitCycle.fgcmConfig.expGrayPhotometricCut[:] = fgcmFitCycle.updatedPhotometricCut
405 fgcmFitCycle.fgcmConfig.expGrayHighCut[:] = fgcmFitCycle.updatedHighCut
406 fgcmFitCycle.fgcmConfig.precomputeSuperStarInitialCycle = False
407 fgcmFitCycle.fgcmConfig.freezeStdAtmosphere = False
408 previousReservedRawRepeatability[:] = fgcmFitCycle.fgcmPars.compReservedRawRepeatability
409 self.log.info("Setting exposure gray photometricity cuts to:")
410 for i, band in enumerate(fgcmFitCycle.fgcmPars.bands):
411 if not fgcmFitCycle.fgcmPars.hasExposuresInBand[i]:
412 continue
413 cut = fgcmFitCycle.updatedPhotometricCut[i] * 1000.0
414 self.log.info(" Band %s, photometricity cut: %.2f mmag" % (band, cut))
416 cycleNumber += 1
418 # Log warning if not converged
419 if not converged:
420 self.log.warn("Maximum number of fit cycles exceeded (%d) without convergence." % (cycleNumber))
422 # Do final clean-up iteration
423 fgcmFitCycle.fgcmConfig.freezeStdAtmosphere = False
424 fgcmFitCycle.fgcmConfig.resetParameters = False
425 fgcmFitCycle.fgcmConfig.maxIter = 0
426 fgcmFitCycle.fgcmConfig.outputZeropoints = True
427 fgcmFitCycle.fgcmConfig.outputStandards = True
428 fgcmFitCycle.fgcmConfig.doPlots = self.config.doDebuggingPlots
429 fgcmFitCycle.fgcmConfig.updateCycleNumber(cycleNumber)
430 fgcmFitCycle.initialCycle = False
432 fgcmPars = fgcm.FgcmParameters.loadParsWithArrays(fgcmFitCycle.fgcmConfig,
433 fgcmExpInfo,
434 previousParInfo,
435 previousParams,
436 previousSuperStar)
437 fgcmFitCycle.fgcmStars.reloadStarMagnitudes(fgcmStarObservationCat['instMag'][obsIndex],
438 fgcmStarObservationCat['instMagErr'][obsIndex])
439 fgcmFitCycle.setPars(fgcmPars)
440 fgcmFitCycle.finishSetup()
442 self.log.info("Running final clean-up fit cycle...")
443 fgcmFitCycle.run()
445 self.log.info("Raw repeatability after clean-up cycle is:")
446 for i, band in enumerate(fgcmFitCycle.fgcmPars.bands):
447 if not fgcmFitCycle.fgcmPars.hasExposuresInBand[i]:
448 continue
449 rep = fgcmFitCycle.fgcmPars.compReservedRawRepeatability[i] * 1000.0
450 self.log.info(" Band %s, repeatability: %.2f mmag" % (band, rep))
452 # Do the outputs. Need to keep track of tract.
454 superStarChebSize = fgcmFitCycle.fgcmZpts.zpStruct['FGCM_FZPT_SSTAR_CHEB'].shape[1]
455 zptChebSize = fgcmFitCycle.fgcmZpts.zpStruct['FGCM_FZPT_CHEB'].shape[1]
457 zptSchema = makeZptSchema(superStarChebSize, zptChebSize)
458 zptCat = makeZptCat(zptSchema, fgcmFitCycle.fgcmZpts.zpStruct)
460 atmSchema = makeAtmSchema()
461 atmCat = makeAtmCat(atmSchema, fgcmFitCycle.fgcmZpts.atmStruct)
463 stdStruct, goodBands = fgcmFitCycle.fgcmStars.retrieveStdStarCatalog(fgcmFitCycle.fgcmPars)
464 stdSchema = makeStdSchema(len(goodBands))
465 stdCat = makeStdCat(stdSchema, stdStruct, goodBands)
467 outStruct = self.fgcmOutputProducts.generateTractOutputProducts(butler, tract,
468 visitCat,
469 zptCat, atmCat, stdCat,
470 self.config.fgcmBuildStars,
471 self.config.fgcmFitCycle)
472 outStruct.repeatability = fgcmFitCycle.fgcmPars.compReservedRawRepeatability
474 return outStruct