28import astropy.units
as u
41from lsst.verify
import Job, Measurement
44 ReferenceObjectLoader)
46from lsst.utils.timer
import timeMethod
48from .dataIds
import PerTractCcdDataIdContainer
53__all__ = [
"JointcalConfig",
"JointcalRunner",
"JointcalTask"]
55Photometry = collections.namedtuple(
'Photometry', (
'fit',
'model'))
56Astrometry = collections.namedtuple(
'Astrometry', (
'fit',
'model',
'sky_to_tan_projection'))
61 meas = Measurement(job.metrics[name], value)
62 job.measurements.insert(meas)
66 """Subclass of TaskRunner for jointcalTask (gen2)
68 jointcalTask.runDataRef() takes a number of arguments, one of which is a list of dataRefs
69 extracted
from the command line (whereas most CmdLineTasks
' runDataRef methods take
70 single dataRef, are are called repeatedly). This class transforms the processed
71 arguments generated by the ArgumentParser into the arguments expected by
72 Jointcal.runDataRef().
74 See pipeBase.TaskRunner
for more information.
80 Return a list of tuples per tract, each containing (dataRefs, kwargs).
82 Jointcal operates on lists of dataRefs simultaneously.
84 kwargs['butler'] = parsedCmd.butler
88 for ref
in parsedCmd.id.refList:
89 refListDict.setdefault(ref.dataId[
"tract"], []).
append(ref)
91 result = [(refListDict[tract], kwargs)
for tract
in sorted(refListDict.keys())]
99 Arguments for Task.runDataRef()
104 if self.doReturnResults
is False:
106 - ``exitStatus``: 0
if the task completed successfully, 1 otherwise.
108 if self.doReturnResults
is True:
110 - ``result``: the result of calling jointcal.runDataRef()
111 - ``exitStatus``: 0
if the task completed successfully, 1 otherwise.
116 dataRefList, kwargs = args
117 butler = kwargs.pop(
'butler')
118 task = self.TaskClass(config=self.config, log=self.log, butler=butler)
121 result = task.runDataRef(dataRefList, **kwargs)
122 exitStatus = result.exitStatus
123 job_path = butler.get(
'verify_job_filename')
124 result.job.write(job_path[0])
125 except Exception
as e:
130 eName =
type(e).__name__
131 tract = dataRefList[0].dataId[
'tract']
132 task.log.fatal(
"Failed processing tract %s, %s: %s", tract, eName, e)
135 kwargs[
'butler'] = butler
136 if self.doReturnResults:
137 return pipeBase.Struct(result=result, exitStatus=exitStatus)
139 return pipeBase.Struct(exitStatus=exitStatus)
143 """Lookup function that asserts/hopes that a static calibration dataset
144 exists in a particular collection, since this task can
't provide a single
145 date/time to use to search for one properly.
147 This
is mostly useful
for the ``camera`` dataset,
in cases where the task
's
148 quantum dimensions do *not* include something temporal, like ``exposure``
153 datasetType : `lsst.daf.butler.DatasetType`
154 Type of dataset being searched
for.
155 registry : `lsst.daf.butler.Registry`
156 Data repository registry to search.
157 quantumDataId : `lsst.daf.butler.DataCoordinate`
158 Data ID of the quantum this camera should match.
159 collections : `Iterable` [ `str` ]
160 Collections that should be searched - but this lookup function works
161 by ignoring this
in favor of a more-
or-less hard-coded value.
165 refs : `Iterator` [ `lsst.daf.butler.DatasetRef` ]
166 Iterator over dataset references; should have only one element.
170 This implementation duplicates one
in fgcmcal,
and is at least quite
171 similar to another
in cp_pipe. This duplicate has the most documentation.
172 Fixing this
is DM-29661.
174 instrument = Instrument.fromName(quantumDataId["instrument"], registry)
175 unboundedCollection = instrument.makeUnboundedCalibrationRunName()
176 return registry.queryDatasets(datasetType,
177 dataId=quantumDataId,
178 collections=[unboundedCollection],
183 """Lookup function that finds all refcats for all visits that overlap a
184 tract, rather than just the refcats that directly overlap the tract.
188 datasetType : `lsst.daf.butler.DatasetType`
189 Type of dataset being searched for.
190 registry : `lsst.daf.butler.Registry`
191 Data repository registry to search.
192 quantumDataId : `lsst.daf.butler.DataCoordinate`
193 Data ID of the quantum; expected to be something we can use
as a
194 constraint to query
for overlapping visits.
195 collections : `Iterable` [ `str` ]
196 Collections to search.
200 refs : `Iterator` [ `lsst.daf.butler.DatasetRef` ]
201 Iterator over refcat references.
210 for visit_data_id
in set(registry.queryDataIds(
"visit", dataId=quantumDataId).expanded()):
212 registry.queryDatasets(
214 collections=collections,
215 dataId=visit_data_id,
223 dimensions=(
"skymap",
"tract",
"instrument",
"physical_filter")):
224 """Middleware input/output connections for jointcal data."""
225 inputCamera = pipeBase.connectionTypes.PrerequisiteInput(
226 doc=
"The camera instrument that took these observations.",
228 storageClass=
"Camera",
229 dimensions=(
"instrument",),
231 lookupFunction=lookupStaticCalibrations,
233 inputSourceTableVisit = pipeBase.connectionTypes.Input(
234 doc=
"Source table in parquet format, per visit",
235 name=
"sourceTable_visit",
236 storageClass=
"DataFrame",
237 dimensions=(
"instrument",
"visit"),
241 inputVisitSummary = pipeBase.connectionTypes.Input(
242 doc=(
"Per-visit consolidated exposure metadata built from calexps. "
243 "These catalogs use detector id for the id and must be sorted for "
244 "fast lookups of a detector."),
246 storageClass=
"ExposureCatalog",
247 dimensions=(
"instrument",
"visit"),
251 astrometryRefCat = pipeBase.connectionTypes.PrerequisiteInput(
252 doc=
"The astrometry reference catalog to match to loaded input catalog sources.",
253 name=
"gaia_dr2_20200414",
254 storageClass=
"SimpleCatalog",
255 dimensions=(
"skypix",),
258 lookupFunction=lookupVisitRefCats,
260 photometryRefCat = pipeBase.connectionTypes.PrerequisiteInput(
261 doc=
"The photometry reference catalog to match to loaded input catalog sources.",
262 name=
"ps1_pv3_3pi_20170110",
263 storageClass=
"SimpleCatalog",
264 dimensions=(
"skypix",),
267 lookupFunction=lookupVisitRefCats,
270 outputWcs = pipeBase.connectionTypes.Output(
271 doc=(
"Per-tract, per-visit world coordinate systems derived from the fitted model."
272 " These catalogs only contain entries for detectors with an output, and use"
273 " the detector id for the catalog id, sorted on id for fast lookups of a detector."),
274 name=
"jointcalSkyWcsCatalog",
275 storageClass=
"ExposureCatalog",
276 dimensions=(
"instrument",
"visit",
"skymap",
"tract"),
279 outputPhotoCalib = pipeBase.connectionTypes.Output(
280 doc=(
"Per-tract, per-visit photometric calibrations derived from the fitted model."
281 " These catalogs only contain entries for detectors with an output, and use"
282 " the detector id for the catalog id, sorted on id for fast lookups of a detector."),
283 name=
"jointcalPhotoCalibCatalog",
284 storageClass=
"ExposureCatalog",
285 dimensions=(
"instrument",
"visit",
"skymap",
"tract"),
293 for name
in (
"astrometry",
"photometry"):
294 vars()[f
"{name}_matched_fittedStars"] = pipeBase.connectionTypes.Output(
295 doc=f
"The number of cross-matched fittedStars for {name}",
296 name=f
"metricvalue_jointcal_{name}_matched_fittedStars",
297 storageClass=
"MetricValue",
298 dimensions=(
"skymap",
"tract",
"instrument",
"physical_filter"),
300 vars()[f
"{name}_collected_refStars"] = pipeBase.connectionTypes.Output(
301 doc=f
"The number of {name} reference stars drawn from the reference catalog, before matching.",
302 name=f
"metricvalue_jointcal_{name}_collected_refStars",
303 storageClass=
"MetricValue",
304 dimensions=(
"skymap",
"tract",
"instrument",
"physical_filter"),
306 vars()[f
"{name}_prepared_refStars"] = pipeBase.connectionTypes.Output(
307 doc=f
"The number of {name} reference stars matched to fittedStars.",
308 name=f
"metricvalue_jointcal_{name}_prepared_refStars",
309 storageClass=
"MetricValue",
310 dimensions=(
"skymap",
"tract",
"instrument",
"physical_filter"),
312 vars()[f
"{name}_prepared_fittedStars"] = pipeBase.connectionTypes.Output(
313 doc=f
"The number of cross-matched fittedStars after cleanup, for {name}.",
314 name=f
"metricvalue_jointcal_{name}_prepared_fittedStars",
315 storageClass=
"MetricValue",
316 dimensions=(
"skymap",
"tract",
"instrument",
"physical_filter"),
318 vars()[f
"{name}_prepared_ccdImages"] = pipeBase.connectionTypes.Output(
319 doc=f
"The number of ccdImages that will be fit for {name}, after cleanup.",
320 name=f
"metricvalue_jointcal_{name}_prepared_ccdImages",
321 storageClass=
"MetricValue",
322 dimensions=(
"skymap",
"tract",
"instrument",
"physical_filter"),
324 vars()[f
"{name}_final_chi2"] = pipeBase.connectionTypes.Output(
325 doc=f
"The final chi2 of the {name} fit.",
326 name=f
"metricvalue_jointcal_{name}_final_chi2",
327 storageClass=
"MetricValue",
328 dimensions=(
"skymap",
"tract",
"instrument",
"physical_filter"),
330 vars()[f
"{name}_final_ndof"] = pipeBase.connectionTypes.Output(
331 doc=f
"The number of degrees of freedom of the fitted {name}.",
332 name=f
"metricvalue_jointcal_{name}_final_ndof",
333 storageClass=
"MetricValue",
334 dimensions=(
"skymap",
"tract",
"instrument",
"physical_filter"),
345 if not config.doAstrometry:
346 self.prerequisiteInputs.remove(
"astrometryRefCat")
347 self.outputs.remove(
"outputWcs")
348 for key
in list(self.outputs):
349 if "metricvalue_jointcal_astrometry" in key:
350 self.outputs.remove(key)
351 if not config.doPhotometry:
352 self.prerequisiteInputs.remove(
"photometryRefCat")
353 self.outputs.remove(
"outputPhotoCalib")
354 for key
in list(self.outputs):
355 if "metricvalue_jointcal_photometry" in key:
356 self.outputs.remove(key)
360 pipelineConnections=JointcalTaskConnections):
361 """Configuration for JointcalTask"""
363 doAstrometry = pexConfig.Field(
364 doc=
"Fit astrometry and write the fitted result.",
368 doPhotometry = pexConfig.Field(
369 doc=
"Fit photometry and write the fitted result.",
373 coaddName = pexConfig.Field(
374 doc=
"Type of coadd, typically deep or goodSeeing",
375 deprecated=
"Only applies to gen2; will be removed when gen2 support is removed (DM-20572).",
379 sourceFluxType = pexConfig.Field(
381 doc=
"Source flux field to use in source selection and to get fluxes from the catalog.",
382 default=
'apFlux_12_0'
384 positionErrorPedestal = pexConfig.Field(
385 doc=
"Systematic term to apply to the measured position error (pixels)",
389 photometryErrorPedestal = pexConfig.Field(
390 doc=
"Systematic term to apply to the measured error on flux or magnitude as a "
391 "fraction of source flux or magnitude delta (e.g. 0.05 is 5% of flux or +50 millimag).",
396 matchCut = pexConfig.Field(
397 doc=
"Matching radius between fitted and reference stars (arcseconds)",
401 minMeasurements = pexConfig.Field(
402 doc=
"Minimum number of associated measured stars for a fitted star to be included in the fit",
406 minMeasuredStarsPerCcd = pexConfig.Field(
407 doc=
"Minimum number of measuredStars per ccdImage before printing warnings",
411 minRefStarsPerCcd = pexConfig.Field(
412 doc=
"Minimum number of measuredStars per ccdImage before printing warnings",
416 allowLineSearch = pexConfig.Field(
417 doc=
"Allow a line search during minimization, if it is reasonable for the model"
418 " (models with a significant non-linear component, e.g. constrainedPhotometry).",
422 astrometrySimpleOrder = pexConfig.Field(
423 doc=
"Polynomial order for fitting the simple astrometry model.",
427 astrometryChipOrder = pexConfig.Field(
428 doc=
"Order of the per-chip transform for the constrained astrometry model.",
432 astrometryVisitOrder = pexConfig.Field(
433 doc=
"Order of the per-visit transform for the constrained astrometry model.",
437 useInputWcs = pexConfig.Field(
438 doc=
"Use the input calexp WCSs to initialize a SimpleAstrometryModel.",
442 astrometryModel = pexConfig.ChoiceField(
443 doc=
"Type of model to fit to astrometry",
445 default=
"constrained",
446 allowed={
"simple":
"One polynomial per ccd",
447 "constrained":
"One polynomial per ccd, and one polynomial per visit"}
449 photometryModel = pexConfig.ChoiceField(
450 doc=
"Type of model to fit to photometry",
452 default=
"constrainedMagnitude",
453 allowed={
"simpleFlux":
"One constant zeropoint per ccd and visit, fitting in flux space.",
454 "constrainedFlux":
"Constrained zeropoint per ccd, and one polynomial per visit,"
455 " fitting in flux space.",
456 "simpleMagnitude":
"One constant zeropoint per ccd and visit,"
457 " fitting in magnitude space.",
458 "constrainedMagnitude":
"Constrained zeropoint per ccd, and one polynomial per visit,"
459 " fitting in magnitude space.",
462 applyColorTerms = pexConfig.Field(
463 doc=
"Apply photometric color terms to reference stars?"
464 "Requires that colorterms be set to a ColortermLibrary",
468 colorterms = pexConfig.ConfigField(
469 doc=
"Library of photometric reference catalog name to color term dict.",
470 dtype=ColortermLibrary,
472 photometryVisitOrder = pexConfig.Field(
473 doc=
"Order of the per-visit polynomial transform for the constrained photometry model.",
477 photometryDoRankUpdate = pexConfig.Field(
478 doc=(
"Do the rank update step during minimization. "
479 "Skipping this can help deal with models that are too non-linear."),
483 astrometryDoRankUpdate = pexConfig.Field(
484 doc=(
"Do the rank update step during minimization (should not change the astrometry fit). "
485 "Skipping this can help deal with models that are too non-linear."),
489 outlierRejectSigma = pexConfig.Field(
490 doc=
"How many sigma to reject outliers at during minimization.",
494 astrometryOutlierRelativeTolerance = pexConfig.Field(
495 doc=(
"Convergence tolerance for outlier rejection threshold when fitting astrometry. Iterations will "
496 "stop when the fractional change in the chi2 cut level is below this value. If tolerance is set "
497 "to zero, iterations will continue until there are no more outliers. We suggest a value of 0.002"
498 "as a balance between a shorter minimization runtime and achieving a final fitted model that is"
499 "close to the solution found when removing all outliers."),
503 maxPhotometrySteps = pexConfig.Field(
504 doc=
"Maximum number of minimize iterations to take when fitting photometry.",
508 maxAstrometrySteps = pexConfig.Field(
509 doc=
"Maximum number of minimize iterations to take when fitting astrometry.",
513 astrometryRefObjLoader = pexConfig.ConfigurableField(
514 target=LoadIndexedReferenceObjectsTask,
515 doc=
"Reference object loader for astrometric fit",
517 photometryRefObjLoader = pexConfig.ConfigurableField(
518 target=LoadIndexedReferenceObjectsTask,
519 doc=
"Reference object loader for photometric fit",
521 sourceSelector = sourceSelectorRegistry.makeField(
522 doc=
"How to select sources for cross-matching",
525 astrometryReferenceSelector = pexConfig.ConfigurableField(
526 target=ReferenceSourceSelectorTask,
527 doc=
"How to down-select the loaded astrometry reference catalog.",
529 photometryReferenceSelector = pexConfig.ConfigurableField(
530 target=ReferenceSourceSelectorTask,
531 doc=
"How to down-select the loaded photometry reference catalog.",
533 astrometryReferenceErr = pexConfig.Field(
534 doc=(
"Uncertainty on reference catalog coordinates [mas] to use in place of the `coord_*Err` fields. "
535 "If None, then raise an exception if the reference catalog is missing coordinate errors. "
536 "If specified, overrides any existing `coord_*Err` values."),
543 writeInitMatrix = pexConfig.Field(
545 doc=(
"Write the pre/post-initialization Hessian and gradient to text files, for debugging. "
546 "Output files will be written to `config.debugOutputPath` and will "
547 "be of the form 'astrometry_[pre|post]init-TRACT-FILTER-mat.txt'. "
548 "Note that these files are the dense versions of the matrix, and so may be very large."),
551 writeChi2FilesInitialFinal = pexConfig.Field(
553 doc=(
"Write .csv files containing the contributions to chi2 for the initialization and final fit. "
554 "Output files will be written to `config.debugOutputPath` and will "
555 "be of the form `astrometry_[initial|final]_chi2-TRACT-FILTER."),
558 writeChi2FilesOuterLoop = pexConfig.Field(
560 doc=(
"Write .csv files containing the contributions to chi2 for the outer fit loop. "
561 "Output files will be written to `config.debugOutputPath` and will "
562 "be of the form `astrometry_init-NN_chi2-TRACT-FILTER`."),
565 writeInitialModel = pexConfig.Field(
567 doc=(
"Write the pre-initialization model to text files, for debugging. "
568 "Output files will be written to `config.debugOutputPath` and will be "
569 "of the form `initial_astrometry_model-TRACT_FILTER.txt`."),
572 debugOutputPath = pexConfig.Field(
575 doc=(
"Path to write debug output files to. Used by "
576 "`writeInitialModel`, `writeChi2Files*`, `writeInitMatrix`.")
578 detailedProfile = pexConfig.Field(
581 doc=
"Output separate profiling information for different parts of jointcal, e.g. data read, fitting"
587 msg =
"applyColorTerms=True requires the `colorterms` field be set to a ColortermLibrary."
588 raise pexConfig.FieldValidationError(JointcalConfig.colorterms, self, msg)
590 msg = (
"Only doing astrometry, but Colorterms are not applied for astrometry;"
591 "applyColorTerms=True will be ignored.")
592 lsst.log.warning(msg)
597 self.
sourceSelectorsourceSelector[
"science"].unresolved.name =
"extendedness"
599 self.
sourceSelectorsourceSelector[
"science"].doSignalToNoise =
True
602 self.
sourceSelectorsourceSelector[
"science"].signalToNoise.minimum = 10.
604 self.
sourceSelectorsourceSelector[
"science"].signalToNoise.fluxField = f
"{self.sourceFluxType}_instFlux"
605 self.
sourceSelectorsourceSelector[
"science"].signalToNoise.errField = f
"{self.sourceFluxType}_instFluxErr"
608 self.
sourceSelectorsourceSelector[
"science"].isolated.parentName =
"parentSourceId"
609 self.
sourceSelectorsourceSelector[
"science"].isolated.nChildName =
"deblend_nChild"
613 badFlags = [
"pixelFlags_edge",
614 "pixelFlags_saturated",
615 "pixelFlags_interpolatedCenter",
616 "pixelFlags_interpolated",
617 "pixelFlags_crCenter",
619 "hsmPsfMoments_flag",
620 f
"{self.sourceFluxType}_flag",
633 """Write model to outfile."""
634 with open(filename,
"w")
as file:
635 file.write(repr(model))
636 log.info(
"Wrote %s to file: %s", model, filename)
639@dataclasses.dataclass
641 """The input data jointcal needs for each detector/visit."""
643 """The visit identifier of this exposure."""
645 """The catalog derived from this exposure."""
647 """The VisitInfo of this exposure."""
649 """The detector of this exposure."""
651 """The photometric calibration of this exposure."""
653 """The WCS of this exposure."""
655 """The bounding box of this exposure."""
657 """The filter of this exposure."""
661 """Astrometricly and photometricly calibrate across multiple visits of the
667 The butler is passed to the refObjLoader constructor
in case it
is
668 needed. Ignored
if the refObjLoader argument provides a loader directly.
669 Used to initialize the astrometry
and photometry refObjLoaders.
670 initInputs : `dict`, optional
671 Dictionary used to initialize PipelineTasks (empty
for jointcal).
674 ConfigClass = JointcalConfig
675 RunnerClass = JointcalRunner
676 _DefaultName = "jointcal"
678 def __init__(self, butler=None, initInputs=None, **kwargs):
680 self.makeSubtask(
"sourceSelector")
681 if self.config.doAstrometry:
682 if initInputs
is None:
684 self.makeSubtask(
'astrometryRefObjLoader', butler=butler)
685 self.makeSubtask(
"astrometryReferenceSelector")
688 if self.config.doPhotometry:
689 if initInputs
is None:
691 self.makeSubtask(
'photometryRefObjLoader', butler=butler)
692 self.makeSubtask(
"photometryReferenceSelector")
697 self.
jobjob = Job.load_metrics_package(subset=
'jointcal')
702 inputs = butlerQC.get(inputRefs)
704 tract = butlerQC.quantum.dataId[
'tract']
705 if self.config.doAstrometry:
707 dataIds=[ref.datasetRef.dataId
708 for ref
in inputRefs.astrometryRefCat],
709 refCats=inputs.pop(
'astrometryRefCat'),
710 config=self.config.astrometryRefObjLoader,
712 if self.config.doPhotometry:
714 dataIds=[ref.datasetRef.dataId
715 for ref
in inputRefs.photometryRefCat],
716 refCats=inputs.pop(
'photometryRefCat'),
717 config=self.config.photometryRefObjLoader,
719 outputs = self.
runrun(**inputs, tract=tract)
720 self.
_put_metrics_put_metrics(butlerQC, outputs.job, outputRefs)
721 if self.config.doAstrometry:
722 self.
_put_output_put_output(butlerQC, outputs.outputWcs, outputRefs.outputWcs,
723 inputs[
'inputCamera'],
"setWcs")
724 if self.config.doPhotometry:
725 self.
_put_output_put_output(butlerQC, outputs.outputPhotoCalib, outputRefs.outputPhotoCalib,
726 inputs[
'inputCamera'],
"setPhotoCalib")
728 def _put_metrics(self, butlerQC, job, outputRefs):
729 """Persist all measured metrics stored in a job.
733 butlerQC : `lsst.pipe.base.ButlerQuantumContext`
734 A butler which is specialized to operate
in the context of a
735 `lsst.daf.butler.Quantum`; This
is the input to `runQuantum`.
736 job : `lsst.verify.job.Job`
737 Measurements of metrics to persist.
738 outputRefs : `list` [`lsst.pipe.base.connectionTypes.OutputQuantizedConnection`]
739 The DatasetRefs to persist the data to.
741 for key
in job.measurements.keys():
742 butlerQC.put(job.measurements[key], getattr(outputRefs, key.fqn.replace(
'jointcal.',
'')))
744 def _put_output(self, butlerQC, outputs, outputRefs, camera, setter):
745 """Persist the output datasets to their appropriate datarefs.
749 butlerQC : `lsst.pipe.base.ButlerQuantumContext`
750 A butler which is specialized to operate
in the context of a
751 `lsst.daf.butler.Quantum`; This
is the input to `runQuantum`.
754 The fitted objects to persist.
755 outputRefs : `list` [`lsst.pipe.base.connectionTypes.OutputQuantizedConnection`]
756 The DatasetRefs to persist the data to.
758 The camera
for this instrument, to get detector ids
from.
760 The method to call on the ExposureCatalog to set each output.
763 schema.addField('visit', type=
'L', doc=
'Visit number')
765 def new_catalog(visit, size):
766 """Return an catalog ready to be filled with appropriate output."""
769 catalog[
'visit'] = visit
771 metadata.add(
"COMMENT",
"Catalog id is detector id, sorted.")
772 metadata.add(
"COMMENT",
"Only detectors with data have entries.")
776 detectors_per_visit = collections.defaultdict(int)
779 detectors_per_visit[key[0]] += 1
781 for ref
in outputRefs:
782 visit = ref.dataId[
'visit']
783 catalog = new_catalog(visit, detectors_per_visit[visit])
786 for detector
in camera:
787 detectorId = detector.getId()
788 key = (ref.dataId[
'visit'], detectorId)
789 if key
not in outputs:
791 self.log.
debug(
"No %s output for detector %s in visit %s",
792 setter[3:], detectorId, visit)
795 catalog[i].setId(detectorId)
796 getattr(catalog[i], setter)(outputs[key])
800 butlerQC.put(catalog, ref)
801 self.log.
info(
"Wrote %s detectors to %s", i, ref)
803 def run(self, inputSourceTableVisit, inputVisitSummary, inputCamera, tract=None):
809 sourceFluxField =
"flux"
813 oldWcsList, bands = self.
_load_data_load_data(inputSourceTableVisit,
819 boundingCircle, center, radius, defaultFilter, epoch = self.
_prep_sky_prep_sky(associations, bands)
821 if self.config.doAstrometry:
825 referenceSelector=self.astrometryReferenceSelector,
829 astrometry_output = self.
_make_output_make_output(associations.getCcdImageList(),
833 astrometry_output =
None
835 if self.config.doPhotometry:
839 referenceSelector=self.photometryReferenceSelector,
843 reject_bad_fluxes=
True)
844 photometry_output = self.
_make_output_make_output(associations.getCcdImageList(),
848 photometry_output =
None
850 return pipeBase.Struct(outputWcs=astrometry_output,
851 outputPhotoCalib=photometry_output,
856 def _load_data(self, inputSourceTableVisit, inputVisitSummary, associations,
857 jointcalControl, camera):
858 """Read the data that jointcal needs to run. (Gen3 version)
860 Modifies ``associations`` in-place
with the loaded data.
864 inputSourceTableVisit : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
865 References to visit-level DataFrames to load the catalog data
from.
866 inputVisitSummary : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
867 Visit-level exposure summary catalog
with metadata.
869 Object to add the loaded data to by constructing new CcdImages.
871 Control object
for C++ associations management.
873 Camera object
for detector geometry.
878 The original WCS of the input data, to aid
in writing tests.
879 bands : `list` [`str`]
880 The filter bands of each input dataset.
884 load_cat_prof_file = 'jointcal_load_data.prof' if self.config.detailedProfile
else ''
885 with pipeBase.cmdLineTask.profile(load_cat_prof_file):
888 catalogMap = {ref.dataId[
'visit']: i
for i, ref
in enumerate(inputSourceTableVisit)}
889 detectorDict = {detector.getId(): detector
for detector
in camera}
893 for visitSummaryRef
in inputVisitSummary:
894 visitSummary = visitSummaryRef.get()
896 dataRef = inputSourceTableVisit[catalogMap[visitSummaryRef.dataId[
'visit']]]
898 inColumns = dataRef.get(component=
'columns')
902 visitCatalog = dataRef.get(parameters={
'columns': columns})
904 selected = self.sourceSelector.
run(visitCatalog)
907 detectors = {id: index
for index, id
in enumerate(visitSummary[
'id'])}
908 for id, index
in detectors.items():
914 self.config.sourceFluxType,
918 data = self.
_make_one_input_data_make_one_input_data(visitSummary[index], catalog, detectorDict)
919 result = self.
_build_ccdImage_build_ccdImage(data, associations, jointcalControl)
920 if result
is not None:
921 oldWcsList.append(result.wcs)
923 filters.append(data.filter)
924 if len(filters) == 0:
925 raise RuntimeError(
"No data to process: did source selector remove all sources?")
926 filters = collections.Counter(filters)
928 return oldWcsList, filters
930 def _make_one_input_data(self, visitRecord, catalog, detectorDict):
931 """Return a data structure for this detector+visit."""
934 visitInfo=visitRecord.getVisitInfo(),
935 detector=detectorDict[visitRecord.getId()],
936 photoCalib=visitRecord.getPhotoCalib(),
937 wcs=visitRecord.getWcs(),
938 bbox=visitRecord.getBBox(),
942 physical=visitRecord[
'physical_filter']))
947 def _getMetadataName(self):
951 def _makeArgumentParser(cls):
952 """Create an argument parser"""
953 parser = pipeBase.ArgumentParser(name=cls._DefaultName_DefaultName)
954 parser.add_id_argument("--id",
"calexp", help=
"data ID, e.g. --id visit=6789 ccd=0..9",
955 ContainerClass=PerTractCcdDataIdContainer)
958 def _build_ccdImage(self, data, associations, jointcalControl):
960 Extract the necessary things from this catalog+metadata to add a new
965 data : `JointcalInputData`
966 The loaded input data.
968 Object to add the info to, to construct a new CcdImage
970 Control object
for associations management
976 The TAN WCS of this image, read
from the calexp
979 A key to identify this dataRef by its visit
and ccd ids
982 if there are no sources
in the loaded catalog.
984 if len(data.catalog) == 0:
985 self.log.
warning(
"No sources selected in visit %s ccd %s", data.visit, data.detector.getId())
988 associations.createCcdImage(data.catalog,
992 data.filter.physicalLabel,
996 data.detector.getId(),
999 Result = collections.namedtuple(
'Result_from_build_CcdImage', (
'wcs',
'key'))
1000 Key = collections.namedtuple(
'Key', (
'visit',
'ccd'))
1001 return Result(data.wcs, Key(data.visit, data.detector.getId()))
1003 def _readDataId(self, butler, dataId):
1004 """Read all of the data for one dataId from the butler. (gen2 version)"""
1006 if "visit" in dataId.keys():
1007 visit = dataId[
"visit"]
1009 visit = butler.getButler().queryMetadata(
"calexp", (
"visit"), butler.dataId)[0]
1010 detector = butler.get(
'calexp_detector', dataId=dataId)
1012 catalog = butler.get(
'src',
1013 flags=lsst.afw.table.SOURCE_IO_NO_FOOTPRINTS,
1015 goodSrc = self.sourceSelector.
run(catalog)
1016 self.log.
debug(
"%d sources selected in visit %d detector %d",
1017 len(goodSrc.sourceCat),
1021 catalog=goodSrc.sourceCat,
1022 visitInfo=butler.get(
'calexp_visitInfo', dataId=dataId),
1024 photoCalib=butler.get(
'calexp_photoCalib', dataId=dataId),
1025 wcs=butler.get(
'calexp_wcs', dataId=dataId),
1026 bbox=butler.get(
'calexp_bbox', dataId=dataId),
1027 filter=butler.get(
'calexp_filter', dataId=dataId))
1029 def loadData(self, dataRefs, associations, jointcalControl):
1030 """Read the data that jointcal needs to run. (Gen2 version)"""
1031 visit_ccd_to_dataRef = {}
1034 load_cat_prof_file =
'jointcal_loadData.prof' if self.config.detailedProfile
else ''
1035 with pipeBase.cmdLineTask.profile(load_cat_prof_file):
1037 camera = dataRefs[0].get(
'camera', immediate=
True)
1039 for dataRef
in dataRefs:
1040 data = self.
_readDataId_readDataId(dataRef.getButler(), dataRef.dataId)
1041 result = self.
_build_ccdImage_build_ccdImage(data, associations, jointcalControl)
1044 oldWcsList.append(result.wcs)
1045 visit_ccd_to_dataRef[result.key] = dataRef
1046 filters.append(data.filter)
1047 if len(filters) == 0:
1048 raise RuntimeError(
"No data to process: did source selector remove all sources?")
1049 filters = collections.Counter(filters)
1051 return oldWcsList, filters, visit_ccd_to_dataRef
1053 def _getDebugPath(self, filename):
1054 """Constructs a path to filename using the configured debug path.
1056 return os.path.join(self.config.debugOutputPath, filename)
1058 def _prep_sky(self, associations, filters):
1059 """Prepare on-sky and other data that must be computed after data has
1062 associations.computeCommonTangentPoint()
1064 boundingCircle = associations.computeBoundingCircle()
1066 radius = lsst.geom.Angle(boundingCircle.getOpeningAngle().asRadians(), lsst.geom.radians)
1068 self.log.info(f"Data has center={center} with radius={radius.asDegrees()} degrees.")
1071 defaultFilter = filters.most_common(1)[0][0]
1072 self.log.
debug(
"Using '%s' filter for reference flux", defaultFilter.physicalLabel)
1076 associations.setEpoch(epoch.jyear)
1078 return boundingCircle, center, radius, defaultFilter, epoch
1083 Jointly calibrate the astrometry and photometry across a set of images.
1085 NOTE: this
is for gen2 middleware only.
1090 List of data references to the exposures to be fit.
1094 result : `lsst.pipe.base.Struct`
1095 Struct of metadata
from the fit, containing:
1098 The provided data references that were fit (
with updated WCSs)
1100 The original WCS
from each dataRef
1102 Dictionary of internally-computed metrics
for testing/validation.
1104 if len(dataRefs) == 0:
1105 raise ValueError(
'Need a non-empty list of data references!')
1109 sourceFluxField =
"slot_%sFlux" % (self.config.sourceFluxType,)
1113 oldWcsList, filters, visit_ccd_to_dataRef = self.
loadDataloadData(dataRefs,
1117 boundingCircle, center, radius, defaultFilter, epoch = self.
_prep_sky_prep_sky(associations, filters)
1119 tract = dataRefs[0].dataId[
'tract']
1121 if self.config.doAstrometry:
1125 referenceSelector=self.astrometryReferenceSelector,
1133 if self.config.doPhotometry:
1137 referenceSelector=self.photometryReferenceSelector,
1141 reject_bad_fluxes=
True)
1146 return pipeBase.Struct(dataRefs=dataRefs,
1147 oldWcsList=oldWcsList,
1151 defaultFilter=defaultFilter,
1153 exitStatus=exitStatus)
1155 def _get_refcat_coordinate_error_override(self, refCat, name):
1156 """Check whether we should override the refcat coordinate errors, and
1157 return the overridden error
if necessary.
1162 The reference catalog to check
for a ``coord_raErr`` field.
1164 Whether we are doing
"astrometry" or "photometry".
1168 refCoordErr : `float`
1169 The refcat coordinate error to use,
or NaN
if we are
not overriding
1175 Raised
if the refcat does
not contain coordinate errors
and
1176 ``config.astrometryReferenceErr``
is not set.
1180 if name.lower() ==
"photometry":
1181 if 'coord_raErr' not in refCat.schema:
1186 if self.config.astrometryReferenceErr
is None and 'coord_raErr' not in refCat.schema:
1187 msg = (
"Reference catalog does not contain coordinate errors, "
1188 "and config.astrometryReferenceErr not supplied.")
1189 raise pexConfig.FieldValidationError(JointcalConfig.astrometryReferenceErr,
1193 if self.config.astrometryReferenceErr
is not None and 'coord_raErr' in refCat.schema:
1194 self.log.
warning(
"Overriding reference catalog coordinate errors with %f/coordinate [mas]",
1195 self.config.astrometryReferenceErr)
1197 if self.config.astrometryReferenceErr
is None:
1200 return self.config.astrometryReferenceErr
1202 def _compute_proper_motion_epoch(self, ccdImageList):
1203 """Return the proper motion correction epoch of the provided images.
1208 The images to compute the appropriate epoch for.
1212 epoch : `astropy.time.Time`
1213 The date to use
for proper motion corrections.
1215 return astropy.time.Time(np.mean([ccdImage.getEpoch()
for ccdImage
in ccdImageList]),
1219 def _do_load_refcat_and_fit(self, associations, defaultFilter, center, radius,
1220 tract="", match_cut=3.0,
1221 reject_bad_fluxes=False, *,
1222 name="", refObjLoader=None, referenceSelector=None,
1223 fit_function=None, epoch=None):
1224 """Load reference catalog, perform the fit, and return the result.
1229 The star/reference star associations to fit.
1231 filter to load from reference catalog.
1233 ICRS center of field to load
from reference catalog.
1235 On-sky radius to load
from reference catalog.
1237 Name of thing being fit:
"astrometry" or "photometry".
1239 Reference object loader to use to load a reference catalog.
1241 Selector to use to pick objects
from the loaded reference catalog.
1242 fit_function : callable
1243 Function to call to perform fit (takes Associations object).
1244 tract : `str`, optional
1245 Name of tract currently being fit.
1246 match_cut : `float`, optional
1247 Radius
in arcseconds to find cross-catalog matches to during
1248 associations.associateCatalogs.
1249 reject_bad_fluxes : `bool`, optional
1250 Reject refCat sources
with NaN/inf flux
or NaN/0 fluxErr.
1251 epoch : `astropy.time.Time`, optional
1252 Epoch to which to correct refcat proper motion
and parallax,
1253 or `
None` to
not apply such corrections.
1257 result : `Photometry`
or `Astrometry`
1258 Result of `fit_function()`
1260 self.log.info("====== Now processing %s...", name)
1263 associations.associateCatalogs(match_cut)
1265 associations.fittedStarListSize())
1267 applyColorterms =
False if name.lower() ==
"astrometry" else self.config.applyColorTerms
1269 center, radius, defaultFilter,
1270 applyColorterms=applyColorterms,
1274 associations.collectRefStars(refCat,
1275 self.config.matchCut*lsst.geom.arcseconds,
1277 refCoordinateErr=refCoordErr,
1278 rejectBadFluxes=reject_bad_fluxes)
1280 associations.refStarListSize())
1282 associations.prepareFittedStars(self.config.minMeasurements)
1286 associations.nFittedStarsWithAssociatedRefStar())
1288 associations.fittedStarListSize())
1290 associations.nCcdImagesValidForFit())
1292 load_cat_prof_file =
'jointcal_fit_%s.prof'%name
if self.config.detailedProfile
else ''
1293 dataName =
"{}_{}".
format(tract, defaultFilter.physicalLabel)
1294 with pipeBase.cmdLineTask.profile(load_cat_prof_file):
1295 result = fit_function(associations, dataName)
1298 if self.config.writeChi2FilesInitialFinal:
1299 baseName = self.
_getDebugPath_getDebugPath(f
"{name}_final_chi2-{dataName}")
1300 result.fit.saveChi2Contributions(baseName+
"{type}")
1301 self.log.
info(
"Wrote chi2 contributions files: %s", baseName)
1305 def _load_reference_catalog(self, refObjLoader, referenceSelector, center, radius, filterLabel,
1306 applyColorterms=False, epoch=None):
1307 """Load the necessary reference catalog sources, convert fluxes to
1308 correct units, and apply color term corrections
if requested.
1313 The reference catalog loader to use to get the data.
1315 Source selector to apply to loaded reference catalog.
1317 The center around which to load sources.
1319 The radius around ``center`` to load sources
in.
1321 The camera filter to load fluxes
for.
1322 applyColorterms : `bool`
1323 Apply colorterm corrections to the refcat
for ``filterName``?
1324 epoch : `astropy.time.Time`, optional
1325 Epoch to which to correct refcat proper motion
and parallax,
1326 or `
None` to
not apply such corrections.
1331 The loaded reference catalog.
1333 The name of the reference catalog flux field appropriate
for ``filterName``.
1335 skyCircle = refObjLoader.loadSkyCircle(center,
1337 filterLabel.bandLabel,
1340 selected = referenceSelector.run(skyCircle.refCat)
1342 if not selected.sourceCat.isContiguous():
1343 refCat = selected.sourceCat.copy(deep=
True)
1345 refCat = selected.sourceCat
1349 refCatName = refObjLoader.ref_dataset_name
1350 except AttributeError:
1351 refCatName = self.config.connections.photometryRefCat
1353 self.log.
info(
"Applying color terms for physical filter=%r reference catalog=%s",
1354 filterLabel.physicalLabel, refCatName)
1355 colorterm = self.config.colorterms.getColorterm(filterLabel.physicalLabel,
1359 refMag, refMagErr = colorterm.getCorrectedMagnitudes(refCat)
1360 refCat[skyCircle.fluxField] = u.Magnitude(refMag, u.ABmag).to_value(u.nJy)
1364 return refCat, skyCircle.fluxField
1366 def _check_star_lists(self, associations, name):
1368 if associations.nCcdImagesValidForFit() == 0:
1369 raise RuntimeError(
'No images in the ccdImageList!')
1370 if associations.fittedStarListSize() == 0:
1371 raise RuntimeError(
'No stars in the {} fittedStarList!'.
format(name))
1372 if associations.refStarListSize() == 0:
1373 raise RuntimeError(
'No stars in the {} reference star list!'.
format(name))
1375 def _logChi2AndValidate(self, associations, fit, model, chi2Label, writeChi2Name=None):
1376 """Compute chi2, log it, validate the model, and return chi2.
1381 The star/reference star associations to fit.
1383 The fitter to use for minimization.
1384 model : `lsst.jointcal.Model`
1385 The model being fit.
1387 Label to describe the chi2 (e.g.
"Initialized",
"Final").
1388 writeChi2Name : `str`, optional
1389 Filename prefix to write the chi2 contributions to.
1390 Do
not supply an extension: an appropriate one will be added.
1395 The chi2 object
for the current fitter
and model.
1400 Raised
if chi2
is infinite
or NaN.
1402 Raised
if the model
is not valid.
1404 if writeChi2Name
is not None:
1406 fit.saveChi2Contributions(fullpath+
"{type}")
1407 self.log.
info(
"Wrote chi2 contributions files: %s", fullpath)
1409 chi2 = fit.computeChi2()
1410 self.log.
info(
"%s %s", chi2Label, chi2)
1412 if not np.isfinite(chi2.chi2):
1413 raise FloatingPointError(f
'{chi2Label} chi2 is invalid: {chi2}')
1414 if not model.validate(associations.getCcdImageList(), chi2.ndof):
1415 raise ValueError(
"Model is not valid: check log messages for warnings.")
1418 def _fit_photometry(self, associations, dataName=None):
1420 Fit the photometric data.
1425 The star/reference star associations to fit.
1427 Name of the data being processed (e.g. "1234_HSC-Y"),
for
1428 identifying debugging files.
1432 fit_result : `namedtuple`
1434 The photometric fitter used to perform the fit.
1436 The photometric model that was fit.
1438 self.log.info("=== Starting photometric fitting...")
1441 if self.config.photometryModel ==
"constrainedFlux":
1444 visitOrder=self.config.photometryVisitOrder,
1445 errorPedestal=self.config.photometryErrorPedestal)
1447 doLineSearch = self.config.allowLineSearch
1448 elif self.config.photometryModel ==
"constrainedMagnitude":
1451 visitOrder=self.config.photometryVisitOrder,
1452 errorPedestal=self.config.photometryErrorPedestal)
1454 doLineSearch = self.config.allowLineSearch
1455 elif self.config.photometryModel ==
"simpleFlux":
1457 errorPedestal=self.config.photometryErrorPedestal)
1458 doLineSearch =
False
1459 elif self.config.photometryModel ==
"simpleMagnitude":
1461 errorPedestal=self.config.photometryErrorPedestal)
1462 doLineSearch =
False
1467 if self.config.writeChi2FilesInitialFinal:
1468 baseName = f
"photometry_initial_chi2-{dataName}"
1471 if self.config.writeInitialModel:
1472 fullpath = self.
_getDebugPath_getDebugPath(f
"initial_photometry_model-{dataName}.txt")
1474 self.
_logChi2AndValidate_logChi2AndValidate(associations, fit, model,
"Initialized", writeChi2Name=baseName)
1476 def getChi2Name(whatToFit):
1477 if self.config.writeChi2FilesOuterLoop:
1478 return f
"photometry_init-%s_chi2-{dataName}" % whatToFit
1484 if self.config.writeInitMatrix:
1485 dumpMatrixFile = self.
_getDebugPath_getDebugPath(f
"photometry_preinit-{dataName}")
1488 if self.config.photometryModel.startswith(
"constrained"):
1491 fit.minimize(
"ModelVisit", dumpMatrixFile=dumpMatrixFile)
1492 self.
_logChi2AndValidate_logChi2AndValidate(associations, fit, model,
"Initialize ModelVisit",
1493 writeChi2Name=getChi2Name(
"ModelVisit"))
1496 fit.minimize(
"Model", doLineSearch=doLineSearch, dumpMatrixFile=dumpMatrixFile)
1498 writeChi2Name=getChi2Name(
"Model"))
1500 fit.minimize(
"Fluxes")
1501 self.
_logChi2AndValidate_logChi2AndValidate(associations, fit, model,
"Initialize Fluxes",
1502 writeChi2Name=getChi2Name(
"Fluxes"))
1504 fit.minimize(
"Model Fluxes", doLineSearch=doLineSearch)
1505 self.
_logChi2AndValidate_logChi2AndValidate(associations, fit, model,
"Initialize ModelFluxes",
1506 writeChi2Name=getChi2Name(
"ModelFluxes"))
1508 model.freezeErrorTransform()
1509 self.log.
debug(
"Photometry error scales are frozen.")
1513 self.config.maxPhotometrySteps,
1516 doRankUpdate=self.config.photometryDoRankUpdate,
1517 doLineSearch=doLineSearch,
1524 def _fit_astrometry(self, associations, dataName=None):
1526 Fit the astrometric data.
1531 The star/reference star associations to fit.
1533 Name of the data being processed (e.g. "1234_HSC-Y"),
for
1534 identifying debugging files.
1538 fit_result : `namedtuple`
1540 The astrometric fitter used to perform the fit.
1542 The astrometric model that was fit.
1544 The model
for the sky to tangent plane projection that was used
in the fit.
1547 self.log.info("=== Starting astrometric fitting...")
1549 associations.deprojectFittedStars()
1556 if self.config.astrometryModel ==
"constrained":
1558 sky_to_tan_projection,
1559 chipOrder=self.config.astrometryChipOrder,
1560 visitOrder=self.config.astrometryVisitOrder)
1561 elif self.config.astrometryModel ==
"simple":
1563 sky_to_tan_projection,
1564 self.config.useInputWcs,
1566 order=self.config.astrometrySimpleOrder)
1571 if self.config.writeChi2FilesInitialFinal:
1572 baseName = f
"astrometry_initial_chi2-{dataName}"
1575 if self.config.writeInitialModel:
1576 fullpath = self.
_getDebugPath_getDebugPath(f
"initial_astrometry_model-{dataName}.txt")
1578 self.
_logChi2AndValidate_logChi2AndValidate(associations, fit, model,
"Initial", writeChi2Name=baseName)
1580 def getChi2Name(whatToFit):
1581 if self.config.writeChi2FilesOuterLoop:
1582 return f
"astrometry_init-%s_chi2-{dataName}" % whatToFit
1586 if self.config.writeInitMatrix:
1587 dumpMatrixFile = self.
_getDebugPath_getDebugPath(f
"astrometry_preinit-{dataName}")
1592 if self.config.astrometryModel ==
"constrained":
1593 fit.minimize(
"DistortionsVisit", dumpMatrixFile=dumpMatrixFile)
1594 self.
_logChi2AndValidate_logChi2AndValidate(associations, fit, model,
"Initialize DistortionsVisit",
1595 writeChi2Name=getChi2Name(
"DistortionsVisit"))
1598 fit.minimize(
"Distortions", dumpMatrixFile=dumpMatrixFile)
1599 self.
_logChi2AndValidate_logChi2AndValidate(associations, fit, model,
"Initialize Distortions",
1600 writeChi2Name=getChi2Name(
"Distortions"))
1602 fit.minimize(
"Positions")
1603 self.
_logChi2AndValidate_logChi2AndValidate(associations, fit, model,
"Initialize Positions",
1604 writeChi2Name=getChi2Name(
"Positions"))
1606 fit.minimize(
"Distortions Positions")
1607 self.
_logChi2AndValidate_logChi2AndValidate(associations, fit, model,
"Initialize DistortionsPositions",
1608 writeChi2Name=getChi2Name(
"DistortionsPositions"))
1612 self.config.maxAstrometrySteps,
1614 "Distortions Positions",
1615 sigmaRelativeTolerance=self.config.astrometryOutlierRelativeTolerance,
1616 doRankUpdate=self.config.astrometryDoRankUpdate,
1622 return Astrometry(fit, model, sky_to_tan_projection)
1624 def _check_stars(self, associations):
1625 """Count measured and reference stars per ccd and warn/log them."""
1626 for ccdImage
in associations.getCcdImageList():
1627 nMeasuredStars, nRefStars = ccdImage.countStars()
1628 self.log.
debug(
"ccdImage %s has %s measured and %s reference stars",
1629 ccdImage.getName(), nMeasuredStars, nRefStars)
1630 if nMeasuredStars < self.config.minMeasuredStarsPerCcd:
1631 self.log.
warning(
"ccdImage %s has only %s measuredStars (desired %s)",
1632 ccdImage.getName(), nMeasuredStars, self.config.minMeasuredStarsPerCcd)
1633 if nRefStars < self.config.minRefStarsPerCcd:
1634 self.log.
warning(
"ccdImage %s has only %s RefStars (desired %s)",
1635 ccdImage.getName(), nRefStars, self.config.minRefStarsPerCcd)
1637 def _iterate_fit(self, associations, fitter, max_steps, name, whatToFit,
1639 sigmaRelativeTolerance=0,
1641 doLineSearch=False):
1642 """Run fitter.minimize up to max_steps times, returning the final chi2.
1647 The star/reference star associations to fit.
1649 The fitter to use for minimization.
1651 Maximum number of steps to run outlier rejection before declaring
1652 convergence failure.
1653 name : {
'photometry' or 'astrometry'}
1654 What type of data are we fitting (
for logs
and debugging files).
1656 Passed to ``fitter.minimize()`` to define the parameters to fit.
1657 dataName : `str`, optional
1658 Descriptive name
for this dataset (e.g. tract
and filter),
1660 sigmaRelativeTolerance : `float`, optional
1661 Convergence tolerance
for the fractional change
in the chi2 cut
1662 level
for determining outliers. If set to zero, iterations will
1663 continue until there are no outliers.
1664 doRankUpdate : `bool`, optional
1665 Do an Eigen rank update during minimization,
or recompute the full
1666 matrix
and gradient?
1667 doLineSearch : `bool`, optional
1668 Do a line search
for the optimum step during minimization?
1673 The final chi2 after the fit converges,
or is forced to end.
1678 Raised
if the fitter fails
with a non-finite value.
1680 Raised
if the fitter fails
for some other reason;
1681 log messages will provide further details.
1683 if self.config.writeInitMatrix:
1684 dumpMatrixFile = self.
_getDebugPath_getDebugPath(f
"{name}_postinit-{dataName}")
1688 oldChi2.chi2 =
float(
"inf")
1689 for i
in range(max_steps):
1690 if self.config.writeChi2FilesOuterLoop:
1691 writeChi2Name = f
"{name}_iterate_{i}_chi2-{dataName}"
1693 writeChi2Name =
None
1694 result = fitter.minimize(whatToFit,
1695 self.config.outlierRejectSigma,
1696 sigmaRelativeTolerance=sigmaRelativeTolerance,
1697 doRankUpdate=doRankUpdate,
1698 doLineSearch=doLineSearch,
1699 dumpMatrixFile=dumpMatrixFile)
1701 chi2 = self.
_logChi2AndValidate_logChi2AndValidate(associations, fitter, fitter.getModel(),
1702 f
"Fit iteration {i}", writeChi2Name=writeChi2Name)
1704 if result == MinimizeResult.Converged:
1706 self.log.
debug(
"fit has converged - no more outliers - redo minimization "
1707 "one more time in case we have lost accuracy in rank update.")
1709 result = fitter.minimize(whatToFit, self.config.outlierRejectSigma,
1710 sigmaRelativeTolerance=sigmaRelativeTolerance)
1711 chi2 = self.
_logChi2AndValidate_logChi2AndValidate(associations, fitter, fitter.getModel(),
"Fit completed")
1714 if chi2.chi2/chi2.ndof >= 4.0:
1715 self.log.
error(
"Potentially bad fit: High chi-squared/ndof.")
1718 elif result == MinimizeResult.Chi2Increased:
1719 self.log.
warning(
"Still some outliers remaining but chi2 increased - retry")
1721 chi2Ratio = chi2.chi2 / oldChi2.chi2
1723 self.log.
warning(
'Significant chi2 increase by a factor of %.4g / %.4g = %.4g',
1724 chi2.chi2, oldChi2.chi2, chi2Ratio)
1731 msg = (
"Large chi2 increase between steps: fit likely cannot converge."
1732 " Try setting one or more of the `writeChi2*` config fields and looking"
1733 " at how individual star chi2-values evolve during the fit.")
1734 raise RuntimeError(msg)
1736 elif result == MinimizeResult.NonFinite:
1737 filename = self.
_getDebugPath_getDebugPath(
"{}_failure-nonfinite_chi2-{}.csv".
format(name, dataName))
1739 fitter.saveChi2Contributions(filename+
"{type}")
1740 msg =
"Nonfinite value in chi2 minimization, cannot complete fit. Dumped star tables to: {}"
1741 raise FloatingPointError(msg.format(filename))
1742 elif result == MinimizeResult.Failed:
1743 raise RuntimeError(
"Chi2 minimization failure, cannot complete fit.")
1745 raise RuntimeError(
"Unxepected return code from minimize().")
1747 self.log.
error(
"%s failed to converge after %d steps"%(name, max_steps))
1751 def _make_output(self, ccdImageList, model, func):
1752 """Return the internal jointcal models converted to the afw
1753 structures that will be saved to disk.
1758 The list of CcdImages to get the output for.
1762 The name of the function to call on ``model`` to get the converted
1769 The data to be saved, keyed on (visit, detector).
1772 for ccdImage
in ccdImageList:
1773 ccd = ccdImage.ccdId
1774 visit = ccdImage.visit
1775 self.log.
debug(
"%s for visit: %d, ccd: %d", func, visit, ccd)
1776 output[(visit, ccd)] = getattr(model, func)(ccdImage)
1779 def _write_astrometry_results(self, associations, model, visit_ccd_to_dataRef):
1781 Write the fitted astrometric results to a new 'jointcal_wcs' dataRef.
1786 The star/reference star associations to fit.
1788 The astrometric model that was fit.
1790 Dict of ccdImage identifiers to dataRefs that were fit.
1792 ccdImageList = associations.getCcdImageList()
1793 output = self._make_output_make_output(ccdImageList, model, "makeSkyWcs")
1794 for key, skyWcs
in output.items():
1795 dataRef = visit_ccd_to_dataRef[key]
1797 dataRef.put(skyWcs,
'jointcal_wcs')
1799 self.log.
fatal(
'Failed to write updated Wcs: %s',
str(e))
1802 def _write_photometry_results(self, associations, model, visit_ccd_to_dataRef):
1804 Write the fitted photometric results to a new 'jointcal_photoCalib' dataRef.
1809 The star/reference star associations to fit.
1811 The photoometric model that was fit.
1813 Dict of ccdImage identifiers to dataRefs that were fit.
1816 ccdImageList = associations.getCcdImageList()
1817 output = self._make_output_make_output(ccdImageList, model, "toPhotoCalib")
1818 for key, photoCalib
in output.items():
1819 dataRef = visit_ccd_to_dataRef[key]
1821 dataRef.put(photoCalib,
'jointcal_photoCalib')
1823 self.log.
fatal(
'Failed to write updated PhotoCalib: %s',
str(e))
1828 """Return an afw SourceTable to use as a base for creating the
1829 SourceCatalog to insert values from the dataFrame into.
1834 Table
with schema
and slots to use to make SourceCatalogs.
1837 schema.addField("centroid_x",
"D")
1838 schema.addField(
"centroid_y",
"D")
1839 schema.addField(
"centroid_xErr",
"F")
1840 schema.addField(
"centroid_yErr",
"F")
1841 schema.addField(
"shape_xx",
"D")
1842 schema.addField(
"shape_yy",
"D")
1843 schema.addField(
"shape_xy",
"D")
1844 schema.addField(
"flux_instFlux",
"D")
1845 schema.addField(
"flux_instFluxErr",
"D")
1847 table.defineCentroid(
"centroid")
1848 table.defineShape(
"shape")
1854 Get the sourceTable_visit columns to load from the catalogs.
1859 List of columns known to be available
in the sourceTable_visit.
1860 config : `JointcalConfig`
1861 A filled-
in config to to help define column names.
1863 A configured source selector to define column names to load.
1868 List of columns to read
from sourceTable_visit.
1869 detectorColumn : `str`
1870 Name of the detector column.
1872 Name of the ixx/iyy/ixy columns.
1874 if 'detector' in inColumns:
1876 detectorColumn =
'detector'
1879 detectorColumn =
'ccd'
1881 columns = [
'visit', detectorColumn,
1882 'sourceId',
'x',
'xErr',
'y',
'yErr',
1883 config.sourceFluxType +
'_instFlux', config.sourceFluxType +
'_instFluxErr']
1885 if 'ixx' in inColumns:
1887 ixxColumns = [
'ixx',
'iyy',
'ixy']
1890 ixxColumns = [
'Ixx',
'Iyy',
'Ixy']
1891 columns.extend(ixxColumns)
1893 if sourceSelector.config.doFlags:
1894 columns.extend(sourceSelector.config.flags.bad)
1895 if sourceSelector.config.doUnresolved:
1896 columns.append(sourceSelector.config.unresolved.name)
1897 if sourceSelector.config.doIsolated:
1898 columns.append(sourceSelector.config.isolated.parentName)
1899 columns.append(sourceSelector.config.isolated.nChildName)
1901 return columns, detectorColumn, ixxColumns
1905 detectorColumn, ixxColumns, sourceFluxType, log):
1906 """Return an afw SourceCatalog extracted from a visit-level dataframe,
1907 limited to just one detector.
1912 Table factory to use to make the SourceCatalog that will be
1913 populated with data
from ``visitCatalog``.
1914 visitCatalog : `pandas.DataFrame`
1915 DataFrame to extract a detector catalog
from.
1917 Numeric id of the detector to extract
from ``visitCatalog``.
1918 detectorColumn : `str`
1919 Name of the detector column
in the catalog.
1920 ixxColumns : `list` [`str`]
1921 Names of the ixx/iyy/ixy columns
in the catalog.
1922 sourceFluxType : `str`
1923 Name of the catalog field to load instFluxes
from.
1925 Logging instance to log to.
1930 Detector-level catalog extracted
from ``visitCatalog``,
or `
None`
1931 if there was no data to load.
1934 mapping = {
'x':
'centroid_x',
1936 'xErr':
'centroid_xErr',
1937 'yErr':
'centroid_yErr',
1938 ixxColumns[0]:
'shape_xx',
1939 ixxColumns[1]:
'shape_yy',
1940 ixxColumns[2]:
'shape_xy',
1941 f
'{sourceFluxType}_instFlux':
'flux_instFlux',
1942 f
'{sourceFluxType}_instFluxErr':
'flux_instFluxErr',
1946 matched = visitCatalog[detectorColumn] == detectorId
1950 catalog.resize(sum(matched))
1951 view = visitCatalog.loc[matched]
1952 catalog[
'id'] = view.index
1953 for dfCol, afwCol
in mapping.items():
1954 catalog[afwCol] = view[dfCol]
1956 log.debug(
"%d sources selected in visit %d detector %d",
1958 view[
'visit'].iloc[0],
An immutable representation of a camera.
A representation of a detector in a mosaic camera.
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
A group of labels for a filter in an exposure or coadd.
The photometric calibration of an exposure.
Information about a single exposure of an imaging camera.
Custom catalog class for ExposureRecord/Table.
static Schema makeMinimalSchema()
Return a minimal schema for Exposure tables and records.
Custom catalog class for record/table subclasses that are guaranteed to have an ID,...
Table class that contains measurements made on a single exposure.
static std::shared_ptr< SourceTable > make(Schema const &schema, std::shared_ptr< IdFactory > const &idFactory)
Construct a new table.
static Schema makeMinimalSchema()
Return a minimal schema for Source tables and records.
Class for storing ordered metadata with comments.
A class representing an angle.
An integer coordinate rectangle.
Point in an unspecified spherical coordinate system.
The class that implements the relations between MeasuredStar and FittedStar.
Class that handles the astrometric least squares problem.
Interface between AstrometryFit and the combinations of Mappings from pixels to some tangent plane (a...
Handler of an actual image from a single CCD.
Base class for Chi2Statistic and Chi2List, to allow addEntry inside Fitter for either class.
Simple structure to accumulate chi2 and ndof.
A multi-component model, fitting mappings for sensors and visits simultaneously.
A projection handler in which all CCDs from the same visit have the same tangent point.
Class that handles the photometric least squares problem.
A model where there is one independent transform per CcdImage.
def getTargetList(parsedCmd, **kwargs)
def __init__(self, *config=None)
def _load_data(self, inputSourceTableVisit, inputVisitSummary, associations, jointcalControl, camera)
def _compute_proper_motion_epoch(self, ccdImageList)
def _get_refcat_coordinate_error_override(self, refCat, name)
def _getDebugPath(self, filename)
def _check_star_lists(self, associations, name)
def _make_one_input_data(self, visitRecord, catalog, detectorDict)
def _iterate_fit(self, associations, fitter, max_steps, name, whatToFit, dataName="", sigmaRelativeTolerance=0, doRankUpdate=True, doLineSearch=False)
def _write_astrometry_results(self, associations, model, visit_ccd_to_dataRef)
def _check_stars(self, associations)
def _prep_sky(self, associations, filters)
def _readDataId(self, butler, dataId)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def _do_load_refcat_and_fit(self, associations, defaultFilter, center, radius, tract="", match_cut=3.0, reject_bad_fluxes=False, *name="", refObjLoader=None, referenceSelector=None, fit_function=None, epoch=None)
def _put_metrics(self, butlerQC, job, outputRefs)
def _build_ccdImage(self, data, associations, jointcalControl)
def runDataRef(self, dataRefs)
def __init__(self, butler=None, initInputs=None, **kwargs)
def run(self, inputSourceTableVisit, inputVisitSummary, inputCamera, tract=None)
def _fit_photometry(self, associations, dataName=None)
def _write_photometry_results(self, associations, model, visit_ccd_to_dataRef)
def _logChi2AndValidate(self, associations, fit, model, chi2Label, writeChi2Name=None)
def _make_output(self, ccdImageList, model, func)
def _put_output(self, butlerQC, outputs, outputRefs, camera, setter)
def _load_reference_catalog(self, refObjLoader, referenceSelector, center, radius, filterLabel, applyColorterms=False, epoch=None)
def _fit_astrometry(self, associations, dataName=None)
def loadData(self, dataRefs, associations, jointcalControl)
This static class includes a variety of methods for interacting with the the logging module.
Provides consistent interface for LSST exceptions.
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
double fluxErrFromABMagErr(double magErr, double mag) noexcept
Compute flux error in Janskys from AB magnitude error and AB magnitude.
def lookupStaticCalibrations(datasetType, registry, quantumDataId, collections)
def add_measurement(job, name, value)
def get_sourceTable_visit_columns(inColumns, config, sourceSelector)
def lookupVisitRefCats(datasetType, registry, quantumDataId, collections)
def extract_detector_catalog_from_visit_catalog(table, visitCatalog, detectorId, detectorColumn, ixxColumns, sourceFluxType, log)
def writeModel(model, filename, log)
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
This is a virtual class that allows a lot of freedom in the choice of the projection from "Sky" (wher...