lsst.jointcal  17.0.1-14-gec16405
jointcal.py
Go to the documentation of this file.
1 # This file is part of jointcal.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://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 <https://www.gnu.org/licenses/>.
21 
22 import collections
23 import numpy as np
24 import astropy.units as u
25 
26 import lsst.utils
27 import lsst.pex.config as pexConfig
28 import lsst.pipe.base as pipeBase
29 from lsst.afw.image import fluxErrFromABMagErr
30 import lsst.afw.geom as afwGeom
31 import lsst.pex.exceptions as pexExceptions
32 import lsst.afw.table
34 from lsst.pipe.tasks.colorterms import ColortermLibrary
35 from lsst.verify import Job, Measurement
36 
37 from lsst.meas.algorithms import LoadIndexedReferenceObjectsTask, ReferenceSourceSelectorTask
38 from lsst.meas.algorithms.sourceSelector import sourceSelectorRegistry
39 
40 from .dataIds import PerTractCcdDataIdContainer
41 
42 import lsst.jointcal
43 from lsst.jointcal import MinimizeResult
44 
45 __all__ = ["JointcalConfig", "JointcalRunner", "JointcalTask"]
46 
47 Photometry = collections.namedtuple('Photometry', ('fit', 'model'))
48 Astrometry = collections.namedtuple('Astrometry', ('fit', 'model', 'sky_to_tan_projection'))
49 
50 
51 # TODO: move this to MeasurementSet in lsst.verify per DM-12655.
52 def add_measurement(job, name, value):
53  meas = Measurement(job.metrics[name], value)
54  job.measurements.insert(meas)
55 
56 
57 class JointcalRunner(pipeBase.ButlerInitializedTaskRunner):
58  """Subclass of TaskRunner for jointcalTask
59 
60  jointcalTask.runDataRef() takes a number of arguments, one of which is a list of dataRefs
61  extracted from the command line (whereas most CmdLineTasks' runDataRef methods take
62  single dataRef, are are called repeatedly). This class transforms the processed
63  arguments generated by the ArgumentParser into the arguments expected by
64  Jointcal.runDataRef().
65 
66  See pipeBase.TaskRunner for more information.
67  """
68 
69  @staticmethod
70  def getTargetList(parsedCmd, **kwargs):
71  """
72  Return a list of tuples per tract, each containing (dataRefs, kwargs).
73 
74  Jointcal operates on lists of dataRefs simultaneously.
75  """
76  kwargs['profile_jointcal'] = parsedCmd.profile_jointcal
77  kwargs['butler'] = parsedCmd.butler
78 
79  # organize data IDs by tract
80  refListDict = {}
81  for ref in parsedCmd.id.refList:
82  refListDict.setdefault(ref.dataId["tract"], []).append(ref)
83  # we call runDataRef() once with each tract
84  result = [(refListDict[tract], kwargs) for tract in sorted(refListDict.keys())]
85  return result
86 
87  def __call__(self, args):
88  """
89  Parameters
90  ----------
91  args
92  Arguments for Task.runDataRef()
93 
94  Returns
95  -------
96  pipe.base.Struct
97  if self.doReturnResults is False:
98 
99  - ``exitStatus``: 0 if the task completed successfully, 1 otherwise.
100 
101  if self.doReturnResults is True:
102 
103  - ``result``: the result of calling jointcal.runDataRef()
104  - ``exitStatus``: 0 if the task completed successfully, 1 otherwise.
105  """
106  exitStatus = 0 # exit status for shell
107 
108  # NOTE: cannot call self.makeTask because that assumes args[0] is a single dataRef.
109  dataRefList, kwargs = args
110  butler = kwargs.pop('butler')
111  task = self.TaskClass(config=self.config, log=self.log, butler=butler)
112  result = None
113  try:
114  result = task.runDataRef(dataRefList, **kwargs)
115  exitStatus = result.exitStatus
116  job_path = butler.get('verify_job_filename')
117  result.job.write(job_path[0])
118  except Exception as e: # catch everything, sort it out later.
119  if self.doRaise:
120  raise e
121  else:
122  exitStatus = 1
123  eName = type(e).__name__
124  tract = dataRefList[0].dataId['tract']
125  task.log.fatal("Failed processing tract %s, %s: %s", tract, eName, e)
126 
127  # Put the butler back into kwargs for the other Tasks.
128  kwargs['butler'] = butler
129  if self.doReturnResults:
130  return pipeBase.Struct(result=result, exitStatus=exitStatus)
131  else:
132  return pipeBase.Struct(exitStatus=exitStatus)
133 
134 
135 class JointcalConfig(pexConfig.Config):
136  """Config for JointcalTask"""
137 
138  doAstrometry = pexConfig.Field(
139  doc="Fit astrometry and write the fitted result.",
140  dtype=bool,
141  default=True
142  )
143  doPhotometry = pexConfig.Field(
144  doc="Fit photometry and write the fitted result.",
145  dtype=bool,
146  default=True
147  )
148  coaddName = pexConfig.Field(
149  doc="Type of coadd, typically deep or goodSeeing",
150  dtype=str,
151  default="deep"
152  )
153  positionErrorPedestal = pexConfig.Field(
154  doc="Systematic term to apply to the measured position error (pixels)",
155  dtype=float,
156  default=0.02,
157  )
158  photometryErrorPedestal = pexConfig.Field(
159  doc="Systematic term to apply to the measured error on flux or magnitude as a "
160  "fraction of source flux or magnitude delta (e.g. 0.05 is 5% of flux or +50 millimag).",
161  dtype=float,
162  default=0.0,
163  )
164  # TODO: DM-6885 matchCut should be an afw.geom.Angle
165  matchCut = pexConfig.Field(
166  doc="Matching radius between fitted and reference stars (arcseconds)",
167  dtype=float,
168  default=3.0,
169  )
170  minMeasurements = pexConfig.Field(
171  doc="Minimum number of associated measured stars for a fitted star to be included in the fit",
172  dtype=int,
173  default=2,
174  )
175  minMeasuredStarsPerCcd = pexConfig.Field(
176  doc="Minimum number of measuredStars per ccdImage before printing warnings",
177  dtype=int,
178  default=100,
179  )
180  minRefStarsPerCcd = pexConfig.Field(
181  doc="Minimum number of measuredStars per ccdImage before printing warnings",
182  dtype=int,
183  default=30,
184  )
185  allowLineSearch = pexConfig.Field(
186  doc="Allow a line search during minimization, if it is reasonable for the model"
187  " (models with a significant non-linear component, e.g. constrainedPhotometry).",
188  dtype=bool,
189  default=False
190  )
191  astrometrySimpleOrder = pexConfig.Field(
192  doc="Polynomial order for fitting the simple astrometry model.",
193  dtype=int,
194  default=3,
195  )
196  astrometryChipOrder = pexConfig.Field(
197  doc="Order of the per-chip transform for the constrained astrometry model.",
198  dtype=int,
199  default=1,
200  )
201  astrometryVisitOrder = pexConfig.Field(
202  doc="Order of the per-visit transform for the constrained astrometry model.",
203  dtype=int,
204  default=5,
205  )
206  useInputWcs = pexConfig.Field(
207  doc="Use the input calexp WCSs to initialize a SimpleAstrometryModel.",
208  dtype=bool,
209  default=True,
210  )
211  astrometryModel = pexConfig.ChoiceField(
212  doc="Type of model to fit to astrometry",
213  dtype=str,
214  default="constrained",
215  allowed={"simple": "One polynomial per ccd",
216  "constrained": "One polynomial per ccd, and one polynomial per visit"}
217  )
218  photometryModel = pexConfig.ChoiceField(
219  doc="Type of model to fit to photometry",
220  dtype=str,
221  default="constrainedMagnitude",
222  allowed={"simpleFlux": "One constant zeropoint per ccd and visit, fitting in flux space.",
223  "constrainedFlux": "Constrained zeropoint per ccd, and one polynomial per visit,"
224  " fitting in flux space.",
225  "simpleMagnitude": "One constant zeropoint per ccd and visit,"
226  " fitting in magnitude space.",
227  "constrainedMagnitude": "Constrained zeropoint per ccd, and one polynomial per visit,"
228  " fitting in magnitude space.",
229  }
230  )
231  applyColorTerms = pexConfig.Field(
232  doc="Apply photometric color terms to reference stars?"
233  "Requires that colorterms be set to a ColortermLibrary",
234  dtype=bool,
235  default=False
236  )
237  colorterms = pexConfig.ConfigField(
238  doc="Library of photometric reference catalog name to color term dict.",
239  dtype=ColortermLibrary,
240  )
241  photometryVisitOrder = pexConfig.Field(
242  doc="Order of the per-visit polynomial transform for the constrained photometry model.",
243  dtype=int,
244  default=7,
245  )
246  photometryDoRankUpdate = pexConfig.Field(
247  doc="Do the rank update step during minimization. "
248  "Skipping this can help deal with models that are too non-linear.",
249  dtype=bool,
250  default=True,
251  )
252  astrometryDoRankUpdate = pexConfig.Field(
253  doc="Do the rank update step during minimization (should not change the astrometry fit). "
254  "Skipping this can help deal with models that are too non-linear.",
255  dtype=bool,
256  default=True,
257  )
258  outlierRejectSigma = pexConfig.Field(
259  doc="How many sigma to reject outliers at during minimization.",
260  dtype=float,
261  default=5.0,
262  )
263  maxPhotometrySteps = pexConfig.Field(
264  doc="Maximum number of minimize iterations to take when fitting photometry.",
265  dtype=int,
266  default=20,
267  )
268  maxAstrometrySteps = pexConfig.Field(
269  doc="Maximum number of minimize iterations to take when fitting photometry.",
270  dtype=int,
271  default=20,
272  )
273  astrometryRefObjLoader = pexConfig.ConfigurableField(
274  target=LoadIndexedReferenceObjectsTask,
275  doc="Reference object loader for astrometric fit",
276  )
277  photometryRefObjLoader = pexConfig.ConfigurableField(
278  target=LoadIndexedReferenceObjectsTask,
279  doc="Reference object loader for photometric fit",
280  )
281  sourceSelector = sourceSelectorRegistry.makeField(
282  doc="How to select sources for cross-matching",
283  default="astrometry"
284  )
285  astrometryReferenceSelector = pexConfig.ConfigurableField(
286  target=ReferenceSourceSelectorTask,
287  doc="How to down-select the loaded astrometry reference catalog.",
288  )
289  photometryReferenceSelector = pexConfig.ConfigurableField(
290  target=ReferenceSourceSelectorTask,
291  doc="How to down-select the loaded photometry reference catalog.",
292  )
293  writeInitMatrix = pexConfig.Field(
294  dtype=bool,
295  doc="Write the pre/post-initialization Hessian and gradient to text files, for debugging."
296  "The output files will be of the form 'astrometry_preinit-mat.txt', in the current directory."
297  "Note that these files are the dense versions of the matrix, and so may be very large.",
298  default=False
299  )
300  writeChi2FilesInitialFinal = pexConfig.Field(
301  dtype=bool,
302  doc="Write .csv files containing the contributions to chi2 for the initialization and final fit.",
303  default=False
304  )
305  writeChi2FilesOuterLoop = pexConfig.Field(
306  dtype=bool,
307  doc="Write .csv files containing the contributions to chi2 for the outer fit loop.",
308  default=False
309  )
310  sourceFluxType = pexConfig.Field(
311  dtype=str,
312  doc="Source flux field to use in source selection and to get fluxes from the catalog.",
313  default='Calib'
314  )
315 
316  def validate(self):
317  super().validate()
318  if self.applyColorTerms and len(self.colorterms.data) == 0:
319  msg = "applyColorTerms=True requires the `colorterms` field be set to a ColortermLibrary."
320  raise pexConfig.FieldValidationError(JointcalConfig.colorterms, self, msg)
321 
322  def setDefaults(self):
323  # Use science source selector which can filter on extendedness, SNR, and whether blended
324  self.sourceSelector.name = 'science'
325  # Use only stars because aperture fluxes of galaxies are biased and depend on seeing
326  self.sourceSelector['science'].doUnresolved = True
327  # with dependable signal to noise ratio.
328  self.sourceSelector['science'].doSignalToNoise = True
329  # Min SNR must be > 0 because jointcal cannot handle negative fluxes,
330  # and S/N > 10 to use sources that are not too faint, and thus better measured.
331  self.sourceSelector['science'].signalToNoise.minimum = 10.
332  # Base SNR on CalibFlux because that is the flux jointcal that fits and must be positive
333  fluxField = f"slot_{self.sourceFluxType}Flux_instFlux"
334  self.sourceSelector['science'].signalToNoise.fluxField = fluxField
335  self.sourceSelector['science'].signalToNoise.errField = fluxField + "Err"
336  # Do not trust blended sources' aperture fluxes which also depend on seeing
337  self.sourceSelector['science'].doIsolated = True
338  # Do not trust either flux or centroid measurements with flags,
339  # chosen from the usual QA flags for stars)
340  self.sourceSelector['science'].doFlags = True
341  badFlags = ['base_PixelFlags_flag_edge', 'base_PixelFlags_flag_saturated',
342  'base_PixelFlags_flag_interpolatedCenter', 'base_SdssCentroid_flag',
343  'base_PsfFlux_flag', 'base_PixelFlags_flag_suspectCenter']
344  self.sourceSelector['science'].flags.bad = badFlags
345 
346 
347 class JointcalTask(pipeBase.CmdLineTask):
348  """Jointly astrometrically and photometrically calibrate a group of images."""
349 
350  ConfigClass = JointcalConfig
351  RunnerClass = JointcalRunner
352  _DefaultName = "jointcal"
353 
354  def __init__(self, butler=None, profile_jointcal=False, **kwargs):
355  """
356  Instantiate a JointcalTask.
357 
358  Parameters
359  ----------
360  butler : `lsst.daf.persistence.Butler`
361  The butler is passed to the refObjLoader constructor in case it is
362  needed. Ignored if the refObjLoader argument provides a loader directly.
363  Used to initialize the astrometry and photometry refObjLoaders.
364  profile_jointcal : `bool`
365  Set to True to profile different stages of this jointcal run.
366  """
367  pipeBase.CmdLineTask.__init__(self, **kwargs)
368  self.profile_jointcal = profile_jointcal
369  self.makeSubtask("sourceSelector")
370  if self.config.doAstrometry:
371  self.makeSubtask('astrometryRefObjLoader', butler=butler)
372  self.makeSubtask("astrometryReferenceSelector")
373  else:
375  if self.config.doPhotometry:
376  self.makeSubtask('photometryRefObjLoader', butler=butler)
377  self.makeSubtask("photometryReferenceSelector")
378  else:
380 
381  # To hold various computed metrics for use by tests
382  self.job = Job.load_metrics_package(subset='jointcal')
383 
384  # We don't currently need to persist the metadata.
385  # If we do in the future, we will have to add appropriate dataset templates
386  # to each obs package (the metadata template should look like `jointcal_wcs`).
387  def _getMetadataName(self):
388  return None
389 
390  @classmethod
391  def _makeArgumentParser(cls):
392  """Create an argument parser"""
393  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
394  parser.add_argument("--profile_jointcal", default=False, action="store_true",
395  help="Profile steps of jointcal separately.")
396  parser.add_id_argument("--id", "calexp", help="data ID, e.g. --id visit=6789 ccd=0..9",
397  ContainerClass=PerTractCcdDataIdContainer)
398  return parser
399 
400  def _build_ccdImage(self, dataRef, associations, jointcalControl):
401  """
402  Extract the necessary things from this dataRef to add a new ccdImage.
403 
404  Parameters
405  ----------
406  dataRef : `lsst.daf.persistence.ButlerDataRef`
407  DataRef to extract info from.
408  associations : `lsst.jointcal.Associations`
409  Object to add the info to, to construct a new CcdImage
410  jointcalControl : `jointcal.JointcalControl`
411  Control object for associations management
412 
413  Returns
414  ------
415  namedtuple
416  ``wcs``
417  The TAN WCS of this image, read from the calexp
418  (`lsst.afw.geom.SkyWcs`).
419  ``key``
420  A key to identify this dataRef by its visit and ccd ids
421  (`namedtuple`).
422  ``filter``
423  This calexp's filter (`str`).
424  """
425  if "visit" in dataRef.dataId.keys():
426  visit = dataRef.dataId["visit"]
427  else:
428  visit = dataRef.getButler().queryMetadata("calexp", ("visit"), dataRef.dataId)[0]
429 
430  src = dataRef.get("src", flags=lsst.afw.table.SOURCE_IO_NO_FOOTPRINTS, immediate=True)
431 
432  visitInfo = dataRef.get('calexp_visitInfo')
433  detector = dataRef.get('calexp_detector')
434  ccdId = detector.getId()
435  photoCalib = dataRef.get('calexp_photoCalib')
436  tanWcs = dataRef.get('calexp_wcs')
437  bbox = dataRef.get('calexp_bbox')
438  filt = dataRef.get('calexp_filter')
439  filterName = filt.getName()
440 
441  goodSrc = self.sourceSelector.run(src)
442 
443  if len(goodSrc.sourceCat) == 0:
444  self.log.warn("No sources selected in visit %s ccd %s", visit, ccdId)
445  else:
446  self.log.info("%d sources selected in visit %d ccd %d", len(goodSrc.sourceCat), visit, ccdId)
447  associations.createCcdImage(goodSrc.sourceCat,
448  tanWcs,
449  visitInfo,
450  bbox,
451  filterName,
452  photoCalib,
453  detector,
454  visit,
455  ccdId,
456  jointcalControl)
457 
458  Result = collections.namedtuple('Result_from_build_CcdImage', ('wcs', 'key', 'filter'))
459  Key = collections.namedtuple('Key', ('visit', 'ccd'))
460  return Result(tanWcs, Key(visit, ccdId), filterName)
461 
462  @pipeBase.timeMethod
463  def runDataRef(self, dataRefs, profile_jointcal=False):
464  """
465  Jointly calibrate the astrometry and photometry across a set of images.
466 
467  Parameters
468  ----------
469  dataRefs : `list` of `lsst.daf.persistence.ButlerDataRef`
470  List of data references to the exposures to be fit.
471  profile_jointcal : `bool`
472  Profile the individual steps of jointcal.
473 
474  Returns
475  -------
476  result : `lsst.pipe.base.Struct`
477  Struct of metadata from the fit, containing:
478 
479  ``dataRefs``
480  The provided data references that were fit (with updated WCSs)
481  ``oldWcsList``
482  The original WCS from each dataRef
483  ``metrics``
484  Dictionary of internally-computed metrics for testing/validation.
485  """
486  if len(dataRefs) == 0:
487  raise ValueError('Need a non-empty list of data references!')
488 
489  exitStatus = 0 # exit status for shell
490 
491  sourceFluxField = "slot_%sFlux" % (self.config.sourceFluxType,)
492  jointcalControl = lsst.jointcal.JointcalControl(sourceFluxField)
493  associations = lsst.jointcal.Associations()
494 
495  visit_ccd_to_dataRef = {}
496  oldWcsList = []
497  filters = []
498  load_cat_prof_file = 'jointcal_build_ccdImage.prof' if profile_jointcal else ''
499  with pipeBase.cmdLineTask.profile(load_cat_prof_file):
500  # We need the bounding-box of the focal plane for photometry visit models.
501  # NOTE: we only need to read it once, because its the same for all exposures of a camera.
502  camera = dataRefs[0].get('camera', immediate=True)
503  self.focalPlaneBBox = camera.getFpBBox()
504  for ref in dataRefs:
505  result = self._build_ccdImage(ref, associations, jointcalControl)
506  oldWcsList.append(result.wcs)
507  visit_ccd_to_dataRef[result.key] = ref
508  filters.append(result.filter)
509  filters = collections.Counter(filters)
510 
511  associations.computeCommonTangentPoint()
512 
513  # Use external reference catalogs handled by LSST stack mechanism
514  # Get the bounding box overlapping all associated images
515  # ==> This is probably a bad idea to do it this way <== To be improved
516  bbox = associations.getRaDecBBox()
517  bboxCenter = bbox.getCenter()
518  center = afwGeom.SpherePoint(bboxCenter[0], bboxCenter[1], afwGeom.degrees)
519  bboxMax = bbox.getMax()
520  corner = afwGeom.SpherePoint(bboxMax[0], bboxMax[1], afwGeom.degrees)
521  radius = center.separation(corner).asRadians()
522 
523  # Get astrometry_net_data path
524  anDir = lsst.utils.getPackageDir('astrometry_net_data')
525  if anDir is None:
526  raise RuntimeError("astrometry_net_data is not setup")
527 
528  # Determine a default filter associated with the catalog. See DM-9093
529  defaultFilter = filters.most_common(1)[0][0]
530  self.log.debug("Using %s band for reference flux", defaultFilter)
531 
532  # TODO: need a better way to get the tract.
533  tract = dataRefs[0].dataId['tract']
534 
535  if self.config.doAstrometry:
536  astrometry = self._do_load_refcat_and_fit(associations, defaultFilter, center, radius,
537  name="astrometry",
538  refObjLoader=self.astrometryRefObjLoader,
539  referenceSelector=self.astrometryReferenceSelector,
540  fit_function=self._fit_astrometry,
541  profile_jointcal=profile_jointcal,
542  tract=tract)
543  self._write_astrometry_results(associations, astrometry.model, visit_ccd_to_dataRef)
544  else:
545  astrometry = Astrometry(None, None, None)
546 
547  if self.config.doPhotometry:
548  photometry = self._do_load_refcat_and_fit(associations, defaultFilter, center, radius,
549  name="photometry",
550  refObjLoader=self.photometryRefObjLoader,
551  referenceSelector=self.photometryReferenceSelector,
552  fit_function=self._fit_photometry,
553  profile_jointcal=profile_jointcal,
554  tract=tract,
555  filters=filters,
556  reject_bad_fluxes=True)
557  self._write_photometry_results(associations, photometry.model, visit_ccd_to_dataRef)
558  else:
559  photometry = Photometry(None, None)
560 
561  return pipeBase.Struct(dataRefs=dataRefs,
562  oldWcsList=oldWcsList,
563  job=self.job,
564  astrometryRefObjLoader=self.astrometryRefObjLoader,
565  photometryRefObjLoader=self.photometryRefObjLoader,
566  defaultFilter=defaultFilter,
567  exitStatus=exitStatus)
568 
569  def _do_load_refcat_and_fit(self, associations, defaultFilter, center, radius,
570  name="", refObjLoader=None, referenceSelector=None,
571  filters=[], fit_function=None,
572  tract=None, profile_jointcal=False, match_cut=3.0,
573  reject_bad_fluxes=False):
574  """Load reference catalog, perform the fit, and return the result.
575 
576  Parameters
577  ----------
578  associations : `lsst.jointcal.Associations`
579  The star/reference star associations to fit.
580  defaultFilter : `str`
581  filter to load from reference catalog.
582  center : `lsst.afw.geom.SpherePoint`
583  ICRS center of field to load from reference catalog.
584  radius : `lsst.afw.geom.Angle`
585  On-sky radius to load from reference catalog.
586  name : `str`
587  Name of thing being fit: "Astrometry" or "Photometry".
588  refObjLoader : `lsst.meas.algorithms.LoadReferenceObjectsTask`
589  Reference object loader to load from for fit.
590  filters : `list` of `str`, optional
591  List of filters to load from the reference catalog.
592  fit_function : callable
593  Function to call to perform fit (takes associations object).
594  tract : `str`
595  Name of tract currently being fit.
596  profile_jointcal : `bool`, optional
597  Separately profile the fitting step.
598  match_cut : `float`, optional
599  Radius in arcseconds to find cross-catalog matches to during
600  associations.associateCatalogs.
601  reject_bad_fluxes : `bool`, optional
602  Reject refCat sources with NaN/inf flux or NaN/0 fluxErr.
603 
604  Returns
605  -------
606  result : `Photometry` or `Astrometry`
607  Result of `fit_function()`
608  """
609  self.log.info("====== Now processing %s...", name)
610  # TODO: this should not print "trying to invert a singular transformation:"
611  # if it does that, something's not right about the WCS...
612  associations.associateCatalogs(match_cut)
613  add_measurement(self.job, 'jointcal.associated_%s_fittedStars' % name,
614  associations.fittedStarListSize())
615 
616  applyColorterms = False if name == "Astrometry" else self.config.applyColorTerms
617  if name == "Astrometry":
618  referenceSelector = self.config.astrometryReferenceSelector
619  elif name == "Photometry":
620  referenceSelector = self.config.photometryReferenceSelector
621  refCat, fluxField = self._load_reference_catalog(refObjLoader, referenceSelector,
622  center, radius, defaultFilter,
623  applyColorterms=applyColorterms)
624 
625  associations.collectRefStars(refCat, self.config.matchCut*afwGeom.arcseconds,
626  fluxField, reject_bad_fluxes)
627  add_measurement(self.job, 'jointcal.collected_%s_refStars' % name,
628  associations.refStarListSize())
629 
630  associations.prepareFittedStars(self.config.minMeasurements)
631 
632  self._check_star_lists(associations, name)
633  add_measurement(self.job, 'jointcal.selected_%s_refStars' % name,
634  associations.nFittedStarsWithAssociatedRefStar())
635  add_measurement(self.job, 'jointcal.selected_%s_fittedStars' % name,
636  associations.fittedStarListSize())
637  add_measurement(self.job, 'jointcal.selected_%s_ccdImages' % name,
638  associations.nCcdImagesValidForFit())
639 
640  load_cat_prof_file = 'jointcal_fit_%s.prof'%name if profile_jointcal else ''
641  dataName = "{}_{}".format(tract, defaultFilter)
642  with pipeBase.cmdLineTask.profile(load_cat_prof_file):
643  result = fit_function(associations, dataName)
644  # TODO DM-12446: turn this into a "butler save" somehow.
645  # Save reference and measurement chi2 contributions for this data
646  if self.config.writeChi2FilesInitialFinal:
647  baseName = f"{name}_final_chi2-{dataName}"
648  result.fit.saveChi2Contributions(baseName+"{type}")
649 
650  return result
651 
652  def _load_reference_catalog(self, refObjLoader, referenceSelector, center, radius, filterName,
653  applyColorterms=False):
654  """Load the necessary reference catalog sources, convert fluxes to
655  correct units, and apply color term corrections if requested.
656 
657  Parameters
658  ----------
659  refObjLoader : `lsst.meas.algorithms.LoadReferenceObjectsTask`
660  The reference catalog loader to use to get the data.
661  referenceSelector : `lsst.meas.algorithms.ReferenceSourceSelectorTask`
662  Source selector to apply to loaded reference catalog.
663  center : `lsst.geom.SpherePoint`
664  The center around which to load sources.
665  radius : `lsst.geom.Angle`
666  The radius around ``center`` to load sources in.
667  filterName : `str`
668  The name of the camera filter to load fluxes for.
669  applyColorterms : `bool`
670  Apply colorterm corrections to the refcat for ``filterName``?
671 
672  Returns
673  -------
674  refCat : `lsst.afw.table.SimpleCatalog`
675  The loaded reference catalog.
676  fluxField : `str`
677  The name of the reference catalog flux field appropriate for ``filterName``.
678  """
679  skyCircle = refObjLoader.loadSkyCircle(center,
680  afwGeom.Angle(radius, afwGeom.radians),
681  filterName)
682 
683  selected = referenceSelector.run(skyCircle.refCat)
684  # Need memory contiguity to get reference filters as a vector.
685  if not selected.sourceCat.isContiguous():
686  refCat = selected.sourceCat.copy(deep=True)
687  else:
688  refCat = selected.sourceCat
689 
690  if applyColorterms:
691  try:
692  refCatName = refObjLoader.ref_dataset_name
693  except AttributeError:
694  # NOTE: we need this try:except: block in place until we've completely removed a.net support.
695  raise RuntimeError("Cannot perform colorterm corrections with a.net refcats.")
696  self.log.info("Applying color terms for filterName=%r reference catalog=%s",
697  filterName, refCatName)
698  colorterm = self.config.colorterms.getColorterm(
699  filterName=filterName, photoCatName=refCatName, doRaise=True)
700 
701  refMag, refMagErr = colorterm.getCorrectedMagnitudes(refCat, filterName)
702  refCat[skyCircle.fluxField] = u.Magnitude(refMag, u.ABmag).to_value(u.nJy)
703  # TODO: I didn't want to use this, but I'll deal with it in DM-16903
704  refCat[skyCircle.fluxField+'Err'] = fluxErrFromABMagErr(refMagErr, refMag) * 1e9
705 
706  return refCat, skyCircle.fluxField
707 
708  def _check_star_lists(self, associations, name):
709  # TODO: these should be len(blah), but we need this properly wrapped first.
710  if associations.nCcdImagesValidForFit() == 0:
711  raise RuntimeError('No images in the ccdImageList!')
712  if associations.fittedStarListSize() == 0:
713  raise RuntimeError('No stars in the {} fittedStarList!'.format(name))
714  if associations.refStarListSize() == 0:
715  raise RuntimeError('No stars in the {} reference star list!'.format(name))
716 
717  def _logChi2AndValidate(self, associations, fit, model, chi2Label="Model",
718  writeChi2Name=None):
719  """Compute chi2, log it, validate the model, and return chi2.
720 
721  Parameters
722  ----------
723  associations : `lsst.jointcal.Associations`
724  The star/reference star associations to fit.
725  fit : `lsst.jointcal.FitterBase`
726  The fitter to use for minimization.
727  model : `lsst.jointcal.Model`
728  The model being fit.
729  chi2Label : str, optional
730  Label to describe the chi2 (e.g. "Initialized", "Final").
731  writeChi2Name : `str`, optional
732  Filename prefix to write the chi2 contributions to.
733  Do not supply an extension: an appropriate one will be added.
734 
735  Returns
736  -------
737  chi2: `lsst.jointcal.Chi2Accumulator`
738  The chi2 object for the current fitter and model.
739 
740  Raises
741  ------
742  FloatingPointError
743  Raised if chi2 is infinite or NaN.
744  ValueError
745  Raised if the model is not valid.
746  """
747  if writeChi2Name is not None:
748  fit.saveChi2Contributions(writeChi2Name+"{type}")
749  self.log.info("Wrote chi2 contributions files: %s", writeChi2Name)
750 
751  chi2 = fit.computeChi2()
752  self.log.info("%s %s", chi2Label, chi2)
753  self._check_stars(associations)
754  if not np.isfinite(chi2.chi2):
755  raise FloatingPointError('%s chi2 is invalid: %s', chi2Label, chi2)
756  if not model.validate(associations.getCcdImageList(), chi2.ndof):
757  raise ValueError("Model is not valid: check log messages for warnings.")
758  return chi2
759 
760  def _fit_photometry(self, associations, dataName=None):
761  """
762  Fit the photometric data.
763 
764  Parameters
765  ----------
766  associations : `lsst.jointcal.Associations`
767  The star/reference star associations to fit.
768  dataName : `str`
769  Name of the data being processed (e.g. "1234_HSC-Y"), for
770  identifying debugging files.
771 
772  Returns
773  -------
774  fit_result : `namedtuple`
775  fit : `lsst.jointcal.PhotometryFit`
776  The photometric fitter used to perform the fit.
777  model : `lsst.jointcal.PhotometryModel`
778  The photometric model that was fit.
779  """
780  self.log.info("=== Starting photometric fitting...")
781 
782  # TODO: should use pex.config.RegistryField here (see DM-9195)
783  if self.config.photometryModel == "constrainedFlux":
784  model = lsst.jointcal.ConstrainedFluxModel(associations.getCcdImageList(),
785  self.focalPlaneBBox,
786  visitOrder=self.config.photometryVisitOrder,
787  errorPedestal=self.config.photometryErrorPedestal)
788  # potentially nonlinear problem, so we may need a line search to converge.
789  doLineSearch = self.config.allowLineSearch
790  elif self.config.photometryModel == "constrainedMagnitude":
791  model = lsst.jointcal.ConstrainedMagnitudeModel(associations.getCcdImageList(),
792  self.focalPlaneBBox,
793  visitOrder=self.config.photometryVisitOrder,
794  errorPedestal=self.config.photometryErrorPedestal)
795  # potentially nonlinear problem, so we may need a line search to converge.
796  doLineSearch = self.config.allowLineSearch
797  elif self.config.photometryModel == "simpleFlux":
798  model = lsst.jointcal.SimpleFluxModel(associations.getCcdImageList(),
799  errorPedestal=self.config.photometryErrorPedestal)
800  doLineSearch = False # purely linear in model parameters, so no line search needed
801  elif self.config.photometryModel == "simpleMagnitude":
802  model = lsst.jointcal.SimpleMagnitudeModel(associations.getCcdImageList(),
803  errorPedestal=self.config.photometryErrorPedestal)
804  doLineSearch = False # purely linear in model parameters, so no line search needed
805 
806  fit = lsst.jointcal.PhotometryFit(associations, model)
807  # TODO DM-12446: turn this into a "butler save" somehow.
808  # Save reference and measurement chi2 contributions for this data
809  if self.config.writeChi2FilesInitialFinal:
810  baseName = f"photometry_initial_chi2-{dataName}"
811  else:
812  baseName = None
813  self._logChi2AndValidate(associations, fit, model, "Initialized", writeChi2Name=baseName)
814 
815  def getChi2Name(whatToFit):
816  if self.config.writeChi2FilesOuterLoop:
817  return f"photometry_init-%s_chi2-{dataName}" % whatToFit
818  else:
819  return None
820 
821  # The constrained model needs the visit transform fit first; the chip
822  # transform is initialized from the singleFrame PhotoCalib, so it's close.
823  dumpMatrixFile = "photometry_preinit" if self.config.writeInitMatrix else ""
824  if self.config.photometryModel.startswith("constrained"):
825  # no line search: should be purely (or nearly) linear,
826  # and we want a large step size to initialize with.
827  fit.minimize("ModelVisit", dumpMatrixFile=dumpMatrixFile)
828  self._logChi2AndValidate(associations, fit, model, writeChi2Name=getChi2Name("ModelVisit"))
829  dumpMatrixFile = "" # so we don't redo the output on the next step
830 
831  fit.minimize("Model", doLineSearch=doLineSearch, dumpMatrixFile=dumpMatrixFile)
832  self._logChi2AndValidate(associations, fit, model, writeChi2Name=getChi2Name("Model"))
833 
834  fit.minimize("Fluxes") # no line search: always purely linear.
835  self._logChi2AndValidate(associations, fit, model, writeChi2Name=getChi2Name("Fluxes"))
836 
837  fit.minimize("Model Fluxes", doLineSearch=doLineSearch)
838  self._logChi2AndValidate(associations, fit, model, "Fit prepared",
839  writeChi2Name=getChi2Name("ModelFluxes"))
840 
841  model.freezeErrorTransform()
842  self.log.debug("Photometry error scales are frozen.")
843 
844  chi2 = self._iterate_fit(associations,
845  fit,
846  self.config.maxPhotometrySteps,
847  "photometry",
848  "Model Fluxes",
849  doRankUpdate=self.config.photometryDoRankUpdate,
850  doLineSearch=doLineSearch,
851  dataName=dataName)
852 
853  add_measurement(self.job, 'jointcal.photometry_final_chi2', chi2.chi2)
854  add_measurement(self.job, 'jointcal.photometry_final_ndof', chi2.ndof)
855  return Photometry(fit, model)
856 
857  def _fit_astrometry(self, associations, dataName=None):
858  """
859  Fit the astrometric data.
860 
861  Parameters
862  ----------
863  associations : `lsst.jointcal.Associations`
864  The star/reference star associations to fit.
865  dataName : `str`
866  Name of the data being processed (e.g. "1234_HSC-Y"), for
867  identifying debugging files.
868 
869  Returns
870  -------
871  fit_result : `namedtuple`
872  fit : `lsst.jointcal.AstrometryFit`
873  The astrometric fitter used to perform the fit.
874  model : `lsst.jointcal.AstrometryModel`
875  The astrometric model that was fit.
876  sky_to_tan_projection : `lsst.jointcal.ProjectionHandler`
877  The model for the sky to tangent plane projection that was used in the fit.
878  """
879 
880  self.log.info("=== Starting astrometric fitting...")
881 
882  associations.deprojectFittedStars()
883 
884  # NOTE: need to return sky_to_tan_projection so that it doesn't get garbage collected.
885  # TODO: could we package sky_to_tan_projection and model together so we don't have to manage
886  # them so carefully?
887  sky_to_tan_projection = lsst.jointcal.OneTPPerVisitHandler(associations.getCcdImageList())
888 
889  if self.config.astrometryModel == "constrained":
890  model = lsst.jointcal.ConstrainedAstrometryModel(associations.getCcdImageList(),
891  sky_to_tan_projection,
892  chipOrder=self.config.astrometryChipOrder,
893  visitOrder=self.config.astrometryVisitOrder)
894  elif self.config.astrometryModel == "simple":
895  model = lsst.jointcal.SimpleAstrometryModel(associations.getCcdImageList(),
896  sky_to_tan_projection,
897  self.config.useInputWcs,
898  nNotFit=0,
899  order=self.config.astrometrySimpleOrder)
900 
901  fit = lsst.jointcal.AstrometryFit(associations, model, self.config.positionErrorPedestal)
902  # TODO DM-12446: turn this into a "butler save" somehow.
903  # Save reference and measurement chi2 contributions for this data
904  if self.config.writeChi2FilesInitialFinal:
905  baseName = f"astrometry_initial_chi2-{dataName}"
906  else:
907  baseName = None
908  self._logChi2AndValidate(associations, fit, model, "Initial", writeChi2Name=baseName)
909 
910  def getChi2Name(whatToFit):
911  if self.config.writeChi2FilesOuterLoop:
912  return f"astrometry_init-%s_chi2-{dataName}" % whatToFit
913  else:
914  return None
915 
916  dumpMatrixFile = "astrometry_preinit" if self.config.writeInitMatrix else ""
917  # The constrained model needs the visit transform fit first; the chip
918  # transform is initialized from the detector's cameraGeom, so it's close.
919  if self.config.astrometryModel == "constrained":
920  fit.minimize("DistortionsVisit", dumpMatrixFile=dumpMatrixFile)
921  self._logChi2AndValidate(associations, fit, model, writeChi2Name=getChi2Name("DistortionsVisit"))
922  dumpMatrixFile = "" # so we don't redo the output on the next step
923 
924  fit.minimize("Distortions", dumpMatrixFile=dumpMatrixFile)
925  self._logChi2AndValidate(associations, fit, model, writeChi2Name=getChi2Name("Distortions"))
926 
927  fit.minimize("Positions")
928  self._logChi2AndValidate(associations, fit, model, writeChi2Name=getChi2Name("Positions"))
929 
930  fit.minimize("Distortions Positions")
931  self._logChi2AndValidate(associations, fit, model, "Fit prepared",
932  writeChi2Name=getChi2Name("DistortionsPositions"))
933 
934  chi2 = self._iterate_fit(associations,
935  fit,
936  self.config.maxAstrometrySteps,
937  "astrometry",
938  "Distortions Positions",
939  doRankUpdate=self.config.astrometryDoRankUpdate,
940  dataName=dataName)
941 
942  add_measurement(self.job, 'jointcal.astrometry_final_chi2', chi2.chi2)
943  add_measurement(self.job, 'jointcal.astrometry_final_ndof', chi2.ndof)
944 
945  return Astrometry(fit, model, sky_to_tan_projection)
946 
947  def _check_stars(self, associations):
948  """Count measured and reference stars per ccd and warn/log them."""
949  for ccdImage in associations.getCcdImageList():
950  nMeasuredStars, nRefStars = ccdImage.countStars()
951  self.log.debug("ccdImage %s has %s measured and %s reference stars",
952  ccdImage.getName(), nMeasuredStars, nRefStars)
953  if nMeasuredStars < self.config.minMeasuredStarsPerCcd:
954  self.log.warn("ccdImage %s has only %s measuredStars (desired %s)",
955  ccdImage.getName(), nMeasuredStars, self.config.minMeasuredStarsPerCcd)
956  if nRefStars < self.config.minRefStarsPerCcd:
957  self.log.warn("ccdImage %s has only %s RefStars (desired %s)",
958  ccdImage.getName(), nRefStars, self.config.minRefStarsPerCcd)
959 
960  def _iterate_fit(self, associations, fitter, max_steps, name, whatToFit,
961  dataName="",
962  doRankUpdate=True,
963  doLineSearch=False):
964  """Run fitter.minimize up to max_steps times, returning the final chi2.
965 
966  Parameters
967  ----------
968  associations : `lsst.jointcal.Associations`
969  The star/reference star associations to fit.
970  fitter : `lsst.jointcal.FitterBase`
971  The fitter to use for minimization.
972  max_steps : `int`
973  Maximum number of steps to run outlier rejection before declaring
974  convergence failure.
975  name : {'photometry' or 'astrometry'}
976  What type of data are we fitting (for logs and debugging files).
977  whatToFit : `str`
978  Passed to ``fitter.minimize()`` to define the parameters to fit.
979  dataName : `str`, optional
980  Descriptive name for this dataset (e.g. tract and filter),
981  for debugging.
982  doRankUpdate : `bool`, optional
983  Do an Eigen rank update during minimization, or recompute the full
984  matrix and gradient?
985  doLineSearch : `bool`, optional
986  Do a line search for the optimum step during minimization?
987 
988  Returns
989  -------
990  chi2: `lsst.jointcal.Chi2Statistic`
991  The final chi2 after the fit converges, or is forced to end.
992 
993  Raises
994  ------
995  FloatingPointError
996  Raised if the fitter fails with a non-finite value.
997  RuntimeError
998  Raised if the fitter fails for some other reason;
999  log messages will provide further details.
1000  """
1001  dumpMatrixFile = "%s_postinit" % name if self.config.writeInitMatrix else ""
1002  for i in range(max_steps):
1003  if self.config.writeChi2FilesOuterLoop:
1004  writeChi2Name = f"{name}_iterate_{i}_chi2-{dataName}"
1005  else:
1006  writeChi2Name = None
1007  result = fitter.minimize(whatToFit,
1008  self.config.outlierRejectSigma,
1009  doRankUpdate=doRankUpdate,
1010  doLineSearch=doLineSearch,
1011  dumpMatrixFile=dumpMatrixFile)
1012  dumpMatrixFile = "" # clear it so we don't write the matrix again.
1013  chi2 = self._logChi2AndValidate(associations, fitter, fitter.getModel(),
1014  writeChi2Name=writeChi2Name)
1015 
1016  if result == MinimizeResult.Converged:
1017  if doRankUpdate:
1018  self.log.debug("fit has converged - no more outliers - redo minimization "
1019  "one more time in case we have lost accuracy in rank update.")
1020  # Redo minimization one more time in case we have lost accuracy in rank update
1021  result = fitter.minimize(whatToFit, self.config.outlierRejectSigma)
1022  chi2 = self._logChi2AndValidate(associations, fitter, fitter.getModel(), "Fit completed")
1023 
1024  # log a message for a large final chi2, TODO: DM-15247 for something better
1025  if chi2.chi2/chi2.ndof >= 4.0:
1026  self.log.error("Potentially bad fit: High chi-squared/ndof.")
1027 
1028  break
1029  elif result == MinimizeResult.Chi2Increased:
1030  self.log.warn("still some outliers but chi2 increases - retry")
1031  elif result == MinimizeResult.NonFinite:
1032  filename = "{}_failure-nonfinite_chi2-{}.csv".format(name, dataName)
1033  # TODO DM-12446: turn this into a "butler save" somehow.
1034  fitter.saveChi2Contributions(filename)
1035  msg = "Nonfinite value in chi2 minimization, cannot complete fit. Dumped star tables to: {}"
1036  raise FloatingPointError(msg.format(filename))
1037  elif result == MinimizeResult.Failed:
1038  raise RuntimeError("Chi2 minimization failure, cannot complete fit.")
1039  else:
1040  raise RuntimeError("Unxepected return code from minimize().")
1041  else:
1042  self.log.error("%s failed to converge after %d steps"%(name, max_steps))
1043 
1044  return chi2
1045 
1046  def _write_astrometry_results(self, associations, model, visit_ccd_to_dataRef):
1047  """
1048  Write the fitted astrometric results to a new 'jointcal_wcs' dataRef.
1049 
1050  Parameters
1051  ----------
1052  associations : `lsst.jointcal.Associations`
1053  The star/reference star associations to fit.
1054  model : `lsst.jointcal.AstrometryModel`
1055  The astrometric model that was fit.
1056  visit_ccd_to_dataRef : `dict` of Key: `lsst.daf.persistence.ButlerDataRef`
1057  Dict of ccdImage identifiers to dataRefs that were fit.
1058  """
1059 
1060  ccdImageList = associations.getCcdImageList()
1061  for ccdImage in ccdImageList:
1062  # TODO: there must be a better way to identify this ccdImage than a visit,ccd pair?
1063  ccd = ccdImage.ccdId
1064  visit = ccdImage.visit
1065  dataRef = visit_ccd_to_dataRef[(visit, ccd)]
1066  self.log.info("Updating WCS for visit: %d, ccd: %d", visit, ccd)
1067  skyWcs = model.makeSkyWcs(ccdImage)
1068  try:
1069  dataRef.put(skyWcs, 'jointcal_wcs')
1070  except pexExceptions.Exception as e:
1071  self.log.fatal('Failed to write updated Wcs: %s', str(e))
1072  raise e
1073 
1074  def _write_photometry_results(self, associations, model, visit_ccd_to_dataRef):
1075  """
1076  Write the fitted photometric results to a new 'jointcal_photoCalib' dataRef.
1077 
1078  Parameters
1079  ----------
1080  associations : `lsst.jointcal.Associations`
1081  The star/reference star associations to fit.
1082  model : `lsst.jointcal.PhotometryModel`
1083  The photoometric model that was fit.
1084  visit_ccd_to_dataRef : `dict` of Key: `lsst.daf.persistence.ButlerDataRef`
1085  Dict of ccdImage identifiers to dataRefs that were fit.
1086  """
1087 
1088  ccdImageList = associations.getCcdImageList()
1089  for ccdImage in ccdImageList:
1090  # TODO: there must be a better way to identify this ccdImage than a visit,ccd pair?
1091  ccd = ccdImage.ccdId
1092  visit = ccdImage.visit
1093  dataRef = visit_ccd_to_dataRef[(visit, ccd)]
1094  self.log.info("Updating PhotoCalib for visit: %d, ccd: %d", visit, ccd)
1095  photoCalib = model.toPhotoCalib(ccdImage)
1096  try:
1097  dataRef.put(photoCalib, 'jointcal_photoCalib')
1098  except pexExceptions.Exception as e:
1099  self.log.fatal('Failed to write updated PhotoCalib: %s', str(e))
1100  raise e
def runDataRef(self, dataRefs, profile_jointcal=False)
Definition: jointcal.py:463
def _load_reference_catalog(self, refObjLoader, referenceSelector, center, radius, filterName, applyColorterms=False)
Definition: jointcal.py:653
def _build_ccdImage(self, dataRef, associations, jointcalControl)
Definition: jointcal.py:400
def _fit_photometry(self, associations, dataName=None)
Definition: jointcal.py:760
def getTargetList(parsedCmd, kwargs)
Definition: jointcal.py:70
def _fit_astrometry(self, associations, dataName=None)
Definition: jointcal.py:857
def _check_star_lists(self, associations, name)
Definition: jointcal.py:708
The class that implements the relations between MeasuredStar and FittedStar.
Definition: Associations.h:53
A projection handler in which all CCDs from the same visit have the same tangent point.
std::string getPackageDir(std::string const &packageName)
def _logChi2AndValidate(self, associations, fit, model, chi2Label="Model", writeChi2Name=None)
Definition: jointcal.py:718
A model where there is one independent transform per CcdImage.
def _iterate_fit(self, associations, fitter, max_steps, name, whatToFit, dataName="", doRankUpdate=True, doLineSearch=False)
Definition: jointcal.py:963
def _write_photometry_results(self, associations, model, visit_ccd_to_dataRef)
Definition: jointcal.py:1074
def _check_stars(self, associations)
Definition: jointcal.py:947
Class that handles the photometric least squares problem.
Definition: PhotometryFit.h:45
Class that handles the astrometric least squares problem.
Definition: AstrometryFit.h:78
def add_measurement(job, name, value)
Definition: jointcal.py:52
def _do_load_refcat_and_fit(self, associations, defaultFilter, center, radius, name="", refObjLoader=None, referenceSelector=None, filters=[], fit_function=None, tract=None, profile_jointcal=False, match_cut=3.0, reject_bad_fluxes=False)
Definition: jointcal.py:573
A multi-component model, fitting mappings for sensors and visits simultaneously.
def _write_astrometry_results(self, associations, model, visit_ccd_to_dataRef)
Definition: jointcal.py:1046
def __init__(self, butler=None, profile_jointcal=False, kwargs)
Definition: jointcal.py:354