624 def __init__(self, initial_stars_schema=None, **kwargs):
626 self.makeSubtask(
"snap_combine")
629 self.makeSubtask(
"install_simple_psf")
630 self.makeSubtask(
"psf_repair")
631 self.makeSubtask(
"psf_subtract_background")
636 doc=
"PSF max value.",
638 afwTable.CoordKey.addErrorFields(self.
psf_schema)
639 self.makeSubtask(
"psf_detection", schema=self.
psf_schema)
640 self.makeSubtask(
"psf_source_measurement", schema=self.
psf_schema)
641 self.makeSubtask(
"psf_measure_psf", schema=self.
psf_schema)
642 self.makeSubtask(
"psf_normalized_calibration_flux", schema=self.
psf_schema)
644 self.makeSubtask(
"measure_aperture_correction", schema=self.
psf_schema)
645 self.makeSubtask(
"astrometry", schema=self.
psf_schema)
648 if initial_stars_schema
is None:
649 initial_stars_schema = afwTable.SourceTable.makeMinimalSchema()
653 self.
psf_fields = (
"calib_psf_candidate",
"calib_psf_used",
"calib_psf_reserved",
654 "calib_astrometry_used",
656 "apcorr_slot_CalibFlux_used",
"apcorr_base_GaussianFlux_used",
657 "apcorr_base_PsfFlux_used",)
660 initial_stars_schema.addField(item.getField())
661 id_type = self.
psf_schema[
"id"].asField().getTypeString()
662 psf_max_value_type = self.
psf_schema[
'psf_max_value'].asField().getTypeString()
663 initial_stars_schema.addField(
"psf_id",
665 doc=
"id of this source in psf_stars; 0 if there is no match.")
666 initial_stars_schema.addField(
"psf_max_value",
667 type=psf_max_value_type,
668 doc=
"Maximum value in the star image used to train PSF.")
670 afwTable.CoordKey.addErrorFields(initial_stars_schema)
671 self.makeSubtask(
"star_detection", schema=initial_stars_schema)
672 self.makeSubtask(
"star_sky_sources", schema=initial_stars_schema)
673 self.makeSubtask(
"star_deblend", schema=initial_stars_schema)
674 self.makeSubtask(
"star_measurement", schema=initial_stars_schema)
675 self.makeSubtask(
"star_normalized_calibration_flux", schema=initial_stars_schema)
677 self.makeSubtask(
"star_apply_aperture_correction", schema=initial_stars_schema)
678 self.makeSubtask(
"star_catalog_calculation", schema=initial_stars_schema)
679 self.makeSubtask(
"star_set_primary_flags", schema=initial_stars_schema, isSingleFrame=
True)
680 self.makeSubtask(
"star_selector")
681 self.makeSubtask(
"photometry", schema=initial_stars_schema)
682 self.makeSubtask(
"compute_summary_stats")
697 inputs = butlerQC.get(inputRefs)
698 exposures = inputs.pop(
"exposures")
700 id_generator = self.config.id_generator.apply(butlerQC.quantum.dataId)
703 dataIds=[ref.datasetRef.dataId
for ref
in inputRefs.astrometry_ref_cat],
704 refCats=inputs.pop(
"astrometry_ref_cat"),
705 name=self.config.connections.astrometry_ref_cat,
706 config=self.config.astrometry_ref_loader, log=self.log)
707 self.astrometry.setRefObjLoader(astrometry_loader)
710 dataIds=[ref.datasetRef.dataId
for ref
in inputRefs.photometry_ref_cat],
711 refCats=inputs.pop(
"photometry_ref_cat"),
712 name=self.config.connections.photometry_ref_cat,
713 config=self.config.photometry_ref_loader, log=self.log)
714 self.photometry.match.setRefObjLoader(photometry_loader)
716 if self.config.do_illumination_correction:
717 background_flat = inputs.pop(
"background_flat")
718 illumination_correction = inputs.pop(
"illumination_correction")
720 background_flat =
None
721 illumination_correction =
None
724 if self.config.useButlerCamera:
725 if "camera_model" in inputs:
726 camera_model = inputs.pop(
"camera_model")
728 self.log.warning(
"useButlerCamera=True, but camera is not available for filter %s. The "
729 "astrometry fit will use the WCS already attached to the exposure.",
730 exposures[0].filter.bandLabel)
733 assert not inputs,
"runQuantum got more inputs than expected"
737 result = pipeBase.Struct(
739 stars_footprints=
None,
740 psf_stars_footprints=
None,
741 background_to_photometric_ratio=
None,
747 id_generator=id_generator,
748 background_flat=background_flat,
749 illumination_correction=illumination_correction,
750 camera_model=camera_model,
752 except pipeBase.AlgorithmError
as e:
753 error = pipeBase.AnnotatedPartialOutputsError.annotate(
757 result.psf_stars_footprints,
758 result.stars_footprints,
761 butlerQC.put(result, outputRefs)
764 butlerQC.put(result, outputRefs)
773 background_flat=None,
774 illumination_correction=None,
777 """Find stars and perform psf measurement, then do a deeper detection
778 and measurement and calibrate astrometry and photometry from that.
782 exposures : `lsst.afw.image.Exposure` or `list` [`lsst.afw.image.Exposure`]
783 Post-ISR exposure(s), with an initial WCS, VisitInfo, and Filter.
784 Modified in-place during processing if only one is passed.
785 If two exposures are passed, treat them as snaps and combine
786 before doing further processing.
787 id_generator : `lsst.meas.base.IdGenerator`, optional
788 Object that generates source IDs and provides random seeds.
789 result : `lsst.pipe.base.Struct`, optional
790 Result struct that is modified to allow saving of partial outputs
791 for some failure conditions. If the task completes successfully,
792 this is also returned.
793 background_flat : `lsst.afw.image.Exposure`, optional
794 Background flat-field image.
795 illumination_correction : `lsst.afw.image.Exposure`, optional
796 Illumination correction image.
797 camera_model : `lsst.afw.cameraGeom.Camera`, optional
798 Camera to be used if constructing updated WCS.
802 result : `lsst.pipe.base.Struct`
803 Results as a struct with attributes:
806 Calibrated exposure, with pixels in nJy units.
807 (`lsst.afw.image.Exposure`)
809 Stars that were used to calibrate the exposure, with
810 calibrated fluxes and magnitudes.
811 (`astropy.table.Table`)
813 Footprints of stars that were used to calibrate the exposure.
814 (`lsst.afw.table.SourceCatalog`)
816 Stars that were used to determine the image PSF.
817 (`astropy.table.Table`)
818 ``psf_stars_footprints``
819 Footprints of stars that were used to determine the image PSF.
820 (`lsst.afw.table.SourceCatalog`)
822 Background that was fit to the exposure when detecting
823 ``stars``. (`lsst.afw.math.BackgroundList`)
824 ``applied_photo_calib``
825 Photometric calibration that was fit to the star catalog and
826 applied to the exposure. (`lsst.afw.image.PhotoCalib`)
827 This is `None` if ``config.do_calibrate_pixels`` is `False`.
828 ``astrometry_matches``
829 Reference catalog stars matches used in the astrometric fit.
830 (`list` [`lsst.afw.table.ReferenceMatch`] or `lsst.afw.table.BaseCatalog`)
831 ``photometry_matches``
832 Reference catalog stars matches used in the photometric fit.
833 (`list` [`lsst.afw.table.ReferenceMatch`] or `lsst.afw.table.BaseCatalog`)
836 result = pipeBase.Struct()
837 if id_generator
is None:
840 result.exposure = self.snap_combine.run(exposures).exposure
842 self.log.info(
"Initial PhotoCalib: %s", result.exposure.getPhotoCalib())
844 result.exposure.metadata[
"LSST CALIB ILLUMCORR APPLIED"] =
False
847 if self.config.do_illumination_correction:
848 if not result.exposure.metadata.get(
"LSST ISR FLAT APPLIED",
False):
849 raise pipeBase.InvalidQuantumError(
850 "Cannot use do_illumination_correction with an image that has not had a flat applied",
854 if camera_model.get(result.exposure.detector.getId()):
855 self.log.info(
"Updating WCS with the provided camera model.")
859 "useButlerCamera=True, but detector %s is not available in the provided camera. The "
860 "astrometry fit will use the WCS already attached to the exposure.",
861 result.exposure.detector.getId())
863 result.background =
None
864 summary_stat_catalog =
None
870 have_fit_astrometry =
False
871 have_fit_photometry =
False
876 illumination_correction,
879 result.psf_stars_footprints, result.background, _ = self.
_compute_psf(
882 background_to_photometric_ratio=result.background_to_photometric_ratio,
889 if result.psf_stars_footprints[
"slot_Centroid_flag"].all():
890 psf_shape = result.exposure.psf.computeShape(result.exposure.psf.getAveragePosition())
892 n_sources=len(result.psf_stars_footprints),
893 psf_shape_ixx=psf_shape.getIxx(),
894 psf_shape_iyy=psf_shape.getIyy(),
895 psf_shape_ixy=psf_shape.getIxy(),
896 psf_size=psf_shape.getDeterminantRadius(),
900 result.psf_stars = result.psf_stars_footprints.asAstropy()
903 result.exposure, result.psf_stars_footprints
905 self.metadata[
"astrometry_matches_count"] = len(astrometry_matches)
906 if "astrometry_matches" in self.config.optional_outputs:
909 result.psf_stars = result.psf_stars_footprints.asAstropy()
916 background_to_photometric_ratio=result.background_to_photometric_ratio,
923 sourceList=result.stars_footprints,
924 include_covariance=self.config.do_include_astrometric_errors
927 summary_stat_catalog = result.stars_footprints
928 result.stars = result.stars_footprints.asAstropy()
929 self.metadata[
"star_count"] = np.sum(~result.stars[
"sky_source"])
934 self.astrometry.check(result.exposure, result.stars_footprints, len(astrometry_matches))
935 result.stars = result.stars_footprints.asAstropy()
936 have_fit_astrometry =
True
938 result.stars_footprints, photometry_matches, \
939 photometry_meta, photo_calib = self.
_fit_photometry(result.exposure, result.stars_footprints)
940 have_fit_photometry =
True
941 self.metadata[
"photometry_matches_count"] = len(photometry_matches)
943 result.stars = result.stars_footprints.asAstropy()
947 summary_stat_catalog = result.stars_footprints
948 if "photometry_matches" in self.config.optional_outputs:
951 except pipeBase.AlgorithmError:
953 result.exposure.setPsf(
None)
954 if not have_fit_astrometry:
955 result.exposure.setWcs(
None)
956 if not have_fit_photometry:
957 result.exposure.setPhotoCalib(
None)
964 self.
_summarize(result.exposure, summary_stat_catalog, result.background)
967 self.
_summarize(result.exposure, summary_stat_catalog, result.background)
969 if self.config.do_calibrate_pixels:
973 background_to_photometric_ratio=result.background_to_photometric_ratio,
975 result.applied_photo_calib = photo_calib
977 result.applied_photo_calib =
None
979 if self.config.run_sattle:
983 populate_sattle_visit_cache(result.exposure.getInfo().getVisitInfo(),
984 historical=self.config.sattle_historical)
985 self.log.info(
'Successfully triggered load of sattle visit cache')
986 except requests.exceptions.HTTPError:
987 self.log.exception(
"Sattle visit cache update failed; continuing with image processing")
1035 def _compute_psf(self, exposure, id_generator, background_to_photometric_ratio=None):
1036 """Find bright sources detected on an exposure and fit a PSF model to
1037 them, repairing likely cosmic rays before detection.
1039 Repair, detect, measure, and compute PSF twice, to ensure the PSF
1040 model does not include contributions from cosmic rays.
1044 exposure : `lsst.afw.image.Exposure`
1045 Exposure to detect and measure bright stars on.
1046 id_generator : `lsst.meas.base.IdGenerator`
1047 Object that generates source IDs and provides random seeds.
1048 background_to_photometric_ratio : `lsst.afw.image.Image`, optional
1049 Image to convert photometric-flattened image to
1050 background-flattened image.
1054 sources : `lsst.afw.table.SourceCatalog`
1055 Catalog of detected bright sources.
1056 background : `lsst.afw.math.BackgroundList`
1057 Background that was fit to the exposure during detection.
1058 cell_set : `lsst.afw.math.SpatialCellSet`
1059 PSF candidates returned by the psf determiner.
1061 def log_psf(msg, addToMetadata=False):
1062 """Log the parameters of the psf and background, with a prepended
1063 message. There is also the option to add the PSF sigma to the task
1069 Message to prepend the log info with.
1070 addToMetadata : `bool`, optional
1071 Whether to add the final psf sigma value to the task metadata
1072 (the default is False).
1074 position = exposure.psf.getAveragePosition()
1075 sigma = exposure.psf.computeShape(position).getDeterminantRadius()
1076 dimensions = exposure.psf.computeImage(position).getDimensions()
1077 median_background = np.median(background.getImage().array)
1078 self.log.info(
"%s sigma=%0.4f, dimensions=%s; median background=%0.2f",
1079 msg, sigma, dimensions, median_background)
1081 self.metadata[
"final_psf_sigma"] = sigma
1083 self.log.info(
"First pass detection with Guassian PSF FWHM=%s pixels",
1084 self.config.install_simple_psf.fwhm)
1085 self.install_simple_psf.run(exposure=exposure)
1087 background = self.psf_subtract_background.run(
1089 backgroundToPhotometricRatio=background_to_photometric_ratio,
1091 log_psf(
"Initial PSF:")
1092 self.psf_repair.run(exposure=exposure, keepCRs=
True)
1094 table = afwTable.SourceTable.make(self.
psf_schema, id_generator.make_table_id_factory())
1097 detections = self.psf_detection.run(
1100 background=background,
1101 backgroundToPhotometricRatio=background_to_photometric_ratio,
1103 self.metadata[
"initial_psf_positive_footprint_count"] = detections.numPos
1104 self.metadata[
"initial_psf_negative_footprint_count"] = detections.numNeg
1105 self.metadata[
"initial_psf_positive_peak_count"] = detections.numPosPeaks
1106 self.metadata[
"initial_psf_negative_peak_count"] = detections.numNegPeaks
1107 self.psf_source_measurement.run(detections.sources, exposure)
1108 psf_result = self.psf_measure_psf.run(exposure=exposure, sources=detections.sources)
1111 self.install_simple_psf.run(exposure=exposure)
1113 log_psf(
"Rerunning with simple PSF:")
1121 self.psf_repair.run(exposure=exposure, keepCRs=
True)
1124 detections = self.psf_detection.run(
1127 background=background,
1128 backgroundToPhotometricRatio=background_to_photometric_ratio,
1130 self.metadata[
"simple_psf_positive_footprint_count"] = detections.numPos
1131 self.metadata[
"simple_psf_negative_footprint_count"] = detections.numNeg
1132 self.metadata[
"simple_psf_positive_peak_count"] = detections.numPosPeaks
1133 self.metadata[
"simple_psf_negative_peak_count"] = detections.numNegPeaks
1134 self.psf_source_measurement.run(detections.sources, exposure)
1135 psf_result = self.psf_measure_psf.run(exposure=exposure, sources=detections.sources)
1137 log_psf(
"Final PSF:", addToMetadata=
True)
1140 self.psf_repair.run(exposure=exposure)
1142 self.psf_source_measurement.run(detections.sources, exposure)
1146 return detections.sources, background, psf_result.cellSet
1178 def _find_stars(self, exposure, background, id_generator, background_to_photometric_ratio=None):
1179 """Detect stars on an exposure that has a PSF model, and measure their
1180 PSF, circular aperture, compensated gaussian fluxes.
1184 exposure : `lsst.afw.image.Exposure`
1185 Exposure to detect and measure stars on.
1186 background : `lsst.afw.math.BackgroundList`
1187 Background that was fit to the exposure during detection;
1188 modified in-place during subsequent detection.
1189 id_generator : `lsst.meas.base.IdGenerator`
1190 Object that generates source IDs and provides random seeds.
1191 background_to_photometric_ratio : `lsst.afw.image.Image`, optional
1192 Image to convert photometric-flattened image to
1193 background-flattened image.
1197 stars : `SourceCatalog`
1198 Sources that are very likely to be stars, with a limited set of
1199 measurements performed on them.
1202 id_generator.make_table_id_factory())
1205 detections = self.star_detection.run(
1208 background=background,
1209 backgroundToPhotometricRatio=background_to_photometric_ratio,
1211 sources = detections.sources
1212 self.star_sky_sources.run(exposure.mask, id_generator.catalog_id, sources)
1214 n_sky_sources = np.sum(sources[
"sky_source"])
1215 if (self.config.do_downsample_footprints
1216 and (len(sources) - n_sky_sources) > self.config.downsample_max_footprints):
1217 if exposure.info.id
is None:
1218 self.log.warning(
"Exposure does not have a proper id; using 0 seed for downsample.")
1221 seed = exposure.info.id & 0xFFFFFFFF
1223 gen = np.random.RandomState(seed)
1226 indices = np.arange(len(sources))[~sources[
"sky_source"]]
1227 indices = gen.choice(
1229 size=self.config.downsample_max_footprints,
1232 skyIndices, = np.where(sources[
"sky_source"])
1233 indices = np.concatenate((indices, skyIndices))
1235 self.log.info(
"Downsampling from %d to %d non-sky-source footprints.", len(sources), len(indices))
1237 sel = np.zeros(len(sources), dtype=bool)
1239 sources = sources[sel]
1242 self.star_deblend.run(exposure=exposure, sources=sources)
1245 if not sources.isContiguous():
1246 sources = sources.copy(deep=
True)
1249 self.star_measurement.run(sources, exposure)
1250 self.metadata[
"post_deblend_source_count"] = np.sum(~sources[
"sky_source"])
1251 self.metadata[
"saturated_source_count"] = np.sum(sources[
"base_PixelFlags_flag_saturated"])
1252 self.metadata[
"bad_source_count"] = np.sum(sources[
"base_PixelFlags_flag_bad"])
1257 self.star_normalized_calibration_flux.run(exposure=exposure, catalog=sources)
1258 self.star_apply_aperture_correction.run(sources, exposure.apCorrMap)
1259 self.star_catalog_calculation.run(sources)
1260 self.star_set_primary_flags.run(sources)
1262 result = self.star_selector.run(sources)
1264 if not result.sourceCat.isContiguous():
1265 return result.sourceCat.copy(deep=
True)
1267 return result.sourceCat
1270 """Match calibration stars to psf stars, to identify which were psf
1271 candidates, and which were used or reserved during psf measurement
1272 and the astrometric fit.
1276 psf_stars : `lsst.afw.table.SourceCatalog`
1277 PSF candidate stars that were sent to the psf determiner and
1278 used in the astrometric fit. Used to populate psf and astrometry
1279 related flag fields.
1280 stars : `lsst.afw.table.SourceCatalog`
1281 Stars that will be used for calibration; psf-related fields will
1282 be updated in-place.
1286 This code was adapted from CalibrateTask.copyIcSourceFields().
1290 control.findOnlyClosest =
False
1292 deblend_key = stars.schema[
"deblend_nChild"].asKey()
1293 matches = [m
for m
in matches
if m[1].get(deblend_key) == 0]
1300 for match_psf, match_stars, d
in matches:
1301 match = best.get(match_psf.getId())
1302 if match
is None or d <= match[2]:
1303 best[match_psf.getId()] = (match_psf, match_stars, d)
1304 matches = list(best.values())
1306 ids = np.array([(match_psf.getId(), match_stars.getId())
for match_psf, match_stars, d
in matches]).T
1308 if (n_matches := len(matches)) == 0:
1311 self.log.info(
"%d psf/astrometry stars out of %d matched %d calib stars",
1312 n_matches, len(psf_stars), len(stars))
1313 self.metadata[
"matched_psf_star_count"] = n_matches
1318 n_unique = len(set(m[1].getId()
for m
in matches))
1319 if n_unique != n_matches:
1320 self.log.warning(
"%d psf_stars matched only %d stars", n_matches, n_unique)
1323 idx_psf_stars = np.searchsorted(psf_stars[
"id"], ids[0])
1324 idx_stars = np.searchsorted(stars[
"id"], ids[1])
1326 result = np.zeros(len(stars), dtype=bool)
1327 result[idx_stars] = psf_stars[field][idx_psf_stars]
1328 stars[field] = result
1329 stars[
'psf_id'][idx_stars] = psf_stars[
'id'][idx_psf_stars]
1330 stars[
'psf_max_value'][idx_stars] = psf_stars[
'psf_max_value'][idx_psf_stars]