645 def __init__(self, initial_stars_schema=None, **kwargs):
647 self.makeSubtask(
"snap_combine")
650 self.makeSubtask(
"install_simple_psf")
651 self.makeSubtask(
"psf_repair")
652 self.makeSubtask(
"psf_subtract_background")
657 doc=
"PSF max value.",
659 afwTable.CoordKey.addErrorFields(self.
psf_schema)
660 self.makeSubtask(
"psf_detection", schema=self.
psf_schema)
661 self.makeSubtask(
"psf_source_measurement", schema=self.
psf_schema)
662 self.makeSubtask(
"psf_measure_psf", schema=self.
psf_schema)
663 self.makeSubtask(
"psf_normalized_calibration_flux", schema=self.
psf_schema)
665 self.makeSubtask(
"measure_aperture_correction", schema=self.
psf_schema)
666 self.makeSubtask(
"astrometry", schema=self.
psf_schema)
669 if initial_stars_schema
is None:
670 initial_stars_schema = afwTable.SourceTable.makeMinimalSchema()
674 self.
psf_fields = (
"calib_psf_candidate",
"calib_psf_used",
"calib_psf_reserved",
675 "calib_astrometry_used",
677 "apcorr_slot_CalibFlux_used",
"apcorr_base_GaussianFlux_used",
678 "apcorr_base_PsfFlux_used",)
681 initial_stars_schema.addField(item.getField())
682 id_type = self.
psf_schema[
"id"].asField().getTypeString()
683 psf_max_value_type = self.
psf_schema[
'psf_max_value'].asField().getTypeString()
684 initial_stars_schema.addField(
"psf_id",
686 doc=
"id of this source in psf_stars; 0 if there is no match.")
687 initial_stars_schema.addField(
"psf_max_value",
688 type=psf_max_value_type,
689 doc=
"Maximum value in the star image used to train PSF.")
691 afwTable.CoordKey.addErrorFields(initial_stars_schema)
692 self.makeSubtask(
"star_detection", schema=initial_stars_schema)
693 self.makeSubtask(
"star_sky_sources", schema=initial_stars_schema)
694 self.makeSubtask(
"star_deblend", schema=initial_stars_schema)
695 self.makeSubtask(
"star_measurement", schema=initial_stars_schema)
696 self.makeSubtask(
"star_normalized_calibration_flux", schema=initial_stars_schema)
698 self.makeSubtask(
"star_apply_aperture_correction", schema=initial_stars_schema)
699 self.makeSubtask(
"star_catalog_calculation", schema=initial_stars_schema)
700 self.makeSubtask(
"star_set_primary_flags", schema=initial_stars_schema, isSingleFrame=
True)
701 self.makeSubtask(
"star_selector")
702 self.makeSubtask(
"photometry", schema=initial_stars_schema)
703 if self.config.doMaskDiffractionSpikes:
704 self.makeSubtask(
"diffractionSpikeMask")
705 self.makeSubtask(
"compute_summary_stats")
720 inputs = butlerQC.get(inputRefs)
721 exposures = inputs.pop(
"exposures")
723 id_generator = self.config.id_generator.apply(butlerQC.quantum.dataId)
726 dataIds=[ref.datasetRef.dataId
for ref
in inputRefs.astrometry_ref_cat],
727 refCats=inputs.pop(
"astrometry_ref_cat"),
728 name=self.config.connections.astrometry_ref_cat,
729 config=self.config.astrometry_ref_loader, log=self.log)
730 self.astrometry.setRefObjLoader(astrometry_loader)
733 dataIds=[ref.datasetRef.dataId
for ref
in inputRefs.photometry_ref_cat],
734 refCats=inputs.pop(
"photometry_ref_cat"),
735 name=self.config.connections.photometry_ref_cat,
736 config=self.config.photometry_ref_loader, log=self.log)
737 self.photometry.match.setRefObjLoader(photometry_loader)
739 if self.config.doMaskDiffractionSpikes:
741 self.diffractionSpikeMask.setRefObjLoader(photometry_loader)
743 if self.config.do_illumination_correction:
744 background_flat = inputs.pop(
"background_flat")
745 illumination_correction = inputs.pop(
"illumination_correction")
747 background_flat =
None
748 illumination_correction =
None
751 if self.config.useButlerCamera:
752 if "camera_model" in inputs:
753 camera_model = inputs.pop(
"camera_model")
755 self.log.warning(
"useButlerCamera=True, but camera is not available for filter %s. The "
756 "astrometry fit will use the WCS already attached to the exposure.",
757 exposures[0].filter.bandLabel)
760 assert not inputs,
"runQuantum got more inputs than expected"
764 result = pipeBase.Struct(
766 stars_footprints=
None,
767 psf_stars_footprints=
None,
768 background_to_photometric_ratio=
None,
774 id_generator=id_generator,
775 background_flat=background_flat,
776 illumination_correction=illumination_correction,
777 camera_model=camera_model,
779 except pipeBase.AlgorithmError
as e:
780 error = pipeBase.AnnotatedPartialOutputsError.annotate(
784 result.psf_stars_footprints,
785 result.stars_footprints,
788 butlerQC.put(result, outputRefs)
791 butlerQC.put(result, outputRefs)
800 background_flat=None,
801 illumination_correction=None,
804 """Find stars and perform psf measurement, then do a deeper detection
805 and measurement and calibrate astrometry and photometry from that.
809 exposures : `lsst.afw.image.Exposure` or `list` [`lsst.afw.image.Exposure`]
810 Post-ISR exposure(s), with an initial WCS, VisitInfo, and Filter.
811 Modified in-place during processing if only one is passed.
812 If two exposures are passed, treat them as snaps and combine
813 before doing further processing.
814 id_generator : `lsst.meas.base.IdGenerator`, optional
815 Object that generates source IDs and provides random seeds.
816 result : `lsst.pipe.base.Struct`, optional
817 Result struct that is modified to allow saving of partial outputs
818 for some failure conditions. If the task completes successfully,
819 this is also returned.
820 background_flat : `lsst.afw.image.Exposure`, optional
821 Background flat-field image.
822 illumination_correction : `lsst.afw.image.Exposure`, optional
823 Illumination correction image.
824 camera_model : `lsst.afw.cameraGeom.Camera`, optional
825 Camera to be used if constructing updated WCS.
829 result : `lsst.pipe.base.Struct`
830 Results as a struct with attributes:
833 Calibrated exposure, with pixels in nJy units.
834 (`lsst.afw.image.Exposure`)
836 Stars that were used to calibrate the exposure, with
837 calibrated fluxes and magnitudes.
838 (`astropy.table.Table`)
840 Footprints of stars that were used to calibrate the exposure.
841 (`lsst.afw.table.SourceCatalog`)
843 Stars that were used to determine the image PSF.
844 (`astropy.table.Table`)
845 ``psf_stars_footprints``
846 Footprints of stars that were used to determine the image PSF.
847 (`lsst.afw.table.SourceCatalog`)
849 Background that was fit to the exposure when detecting
850 ``stars``. (`lsst.afw.math.BackgroundList`)
851 ``applied_photo_calib``
852 Photometric calibration that was fit to the star catalog and
853 applied to the exposure. (`lsst.afw.image.PhotoCalib`)
854 This is `None` if ``config.do_calibrate_pixels`` is `False`.
855 ``astrometry_matches``
856 Reference catalog stars matches used in the astrometric fit.
857 (`list` [`lsst.afw.table.ReferenceMatch`] or `lsst.afw.table.BaseCatalog`)
858 ``photometry_matches``
859 Reference catalog stars matches used in the photometric fit.
860 (`list` [`lsst.afw.table.ReferenceMatch`] or `lsst.afw.table.BaseCatalog`)
862 Copy of the mask plane of `exposure`.
863 (`lsst.afw.image.Mask`)
866 result = pipeBase.Struct()
867 if id_generator
is None:
870 result.exposure = self.snap_combine.run(exposures).exposure
872 self.log.info(
"Initial PhotoCalib: %s", result.exposure.getPhotoCalib())
874 result.exposure.metadata[
"LSST CALIB ILLUMCORR APPLIED"] =
False
877 if self.config.do_illumination_correction:
878 if not result.exposure.metadata.get(
"LSST ISR FLAT APPLIED",
False):
879 raise pipeBase.InvalidQuantumError(
880 "Cannot use do_illumination_correction with an image that has not had a flat applied",
884 if camera_model.get(result.exposure.detector.getId()):
885 self.log.info(
"Updating WCS with the provided camera model.")
889 "useButlerCamera=True, but detector %s is not available in the provided camera. The "
890 "astrometry fit will use the WCS already attached to the exposure.",
891 result.exposure.detector.getId())
893 result.background =
None
894 summary_stat_catalog =
None
900 have_fit_astrometry =
False
901 have_fit_photometry =
False
906 illumination_correction,
909 result.psf_stars_footprints, result.background, _ = self.
_compute_psf(
912 background_to_photometric_ratio=result.background_to_photometric_ratio,
919 if result.psf_stars_footprints[
"slot_Centroid_flag"].all():
920 psf_shape = result.exposure.psf.computeShape(result.exposure.psf.getAveragePosition())
922 n_sources=len(result.psf_stars_footprints),
923 psf_shape_ixx=psf_shape.getIxx(),
924 psf_shape_iyy=psf_shape.getIyy(),
925 psf_shape_ixy=psf_shape.getIxy(),
926 psf_size=psf_shape.getDeterminantRadius(),
930 result.psf_stars = result.psf_stars_footprints.asAstropy()
933 result.exposure, result.psf_stars_footprints
935 self.metadata[
"astrometry_matches_count"] = len(astrometry_matches)
936 if "astrometry_matches" in self.config.optional_outputs:
939 result.psf_stars = result.psf_stars_footprints.asAstropy()
946 background_to_photometric_ratio=result.background_to_photometric_ratio,
953 sourceList=result.stars_footprints,
954 include_covariance=self.config.do_include_astrometric_errors
957 summary_stat_catalog = result.stars_footprints
958 result.stars = result.stars_footprints.asAstropy()
959 self.metadata[
"star_count"] = np.sum(~result.stars[
"sky_source"])
964 self.astrometry.check(result.exposure, result.stars_footprints, len(astrometry_matches))
965 result.stars = result.stars_footprints.asAstropy()
966 have_fit_astrometry =
True
968 result.stars_footprints, photometry_matches, \
969 photometry_meta, photo_calib = self.
_fit_photometry(result.exposure, result.stars_footprints)
970 have_fit_photometry =
True
971 self.metadata[
"photometry_matches_count"] = len(photometry_matches)
973 result.stars = result.stars_footprints.asAstropy()
977 summary_stat_catalog = result.stars_footprints
978 if "photometry_matches" in self.config.optional_outputs:
981 if self.config.doMaskDiffractionSpikes:
982 self.diffractionSpikeMask.run(result.exposure)
983 if "mask" in self.config.optional_outputs:
984 result.mask = result.exposure.mask.clone()
985 except pipeBase.AlgorithmError:
987 result.exposure.setPsf(
None)
988 if not have_fit_astrometry:
989 result.exposure.setWcs(
None)
990 if not have_fit_photometry:
991 result.exposure.setPhotoCalib(
None)
998 self.
_summarize(result.exposure, summary_stat_catalog, result.background)
1001 self.
_summarize(result.exposure, summary_stat_catalog, result.background)
1003 if self.config.do_calibrate_pixels:
1007 background_to_photometric_ratio=result.background_to_photometric_ratio,
1009 result.applied_photo_calib = photo_calib
1011 result.applied_photo_calib =
None
1013 if self.config.run_sattle:
1017 populate_sattle_visit_cache(result.exposure.getInfo().getVisitInfo(),
1018 historical=self.config.sattle_historical)
1019 self.log.info(
'Successfully triggered load of sattle visit cache')
1020 except requests.exceptions.HTTPError:
1021 self.log.exception(
"Sattle visit cache update failed; continuing with image processing")
1069 def _compute_psf(self, exposure, id_generator, background_to_photometric_ratio=None):
1070 """Find bright sources detected on an exposure and fit a PSF model to
1071 them, repairing likely cosmic rays before detection.
1073 Repair, detect, measure, and compute PSF twice, to ensure the PSF
1074 model does not include contributions from cosmic rays.
1078 exposure : `lsst.afw.image.Exposure`
1079 Exposure to detect and measure bright stars on.
1080 id_generator : `lsst.meas.base.IdGenerator`
1081 Object that generates source IDs and provides random seeds.
1082 background_to_photometric_ratio : `lsst.afw.image.Image`, optional
1083 Image to convert photometric-flattened image to
1084 background-flattened image.
1088 sources : `lsst.afw.table.SourceCatalog`
1089 Catalog of detected bright sources.
1090 background : `lsst.afw.math.BackgroundList`
1091 Background that was fit to the exposure during detection.
1092 cell_set : `lsst.afw.math.SpatialCellSet`
1093 PSF candidates returned by the psf determiner.
1095 def log_psf(msg, addToMetadata=False):
1096 """Log the parameters of the psf and background, with a prepended
1097 message. There is also the option to add the PSF sigma to the task
1103 Message to prepend the log info with.
1104 addToMetadata : `bool`, optional
1105 Whether to add the final psf sigma value to the task metadata
1106 (the default is False).
1108 position = exposure.psf.getAveragePosition()
1109 sigma = exposure.psf.computeShape(position).getDeterminantRadius()
1110 dimensions = exposure.psf.computeImage(position).getDimensions()
1111 median_background = np.median(background.getImage().array)
1112 self.log.info(
"%s sigma=%0.4f, dimensions=%s; median background=%0.2f",
1113 msg, sigma, dimensions, median_background)
1115 self.metadata[
"final_psf_sigma"] = sigma
1117 self.log.info(
"First pass detection with Guassian PSF FWHM=%s pixels",
1118 self.config.install_simple_psf.fwhm)
1119 self.install_simple_psf.run(exposure=exposure)
1121 background = self.psf_subtract_background.run(
1123 backgroundToPhotometricRatio=background_to_photometric_ratio,
1125 log_psf(
"Initial PSF:")
1126 self.psf_repair.run(exposure=exposure, keepCRs=
True)
1128 table = afwTable.SourceTable.make(self.
psf_schema, id_generator.make_table_id_factory())
1131 detections = self.psf_detection.run(
1134 background=background,
1135 backgroundToPhotometricRatio=background_to_photometric_ratio,
1137 self.metadata[
"initial_psf_positive_footprint_count"] = detections.numPos
1138 self.metadata[
"initial_psf_negative_footprint_count"] = detections.numNeg
1139 self.metadata[
"initial_psf_positive_peak_count"] = detections.numPosPeaks
1140 self.metadata[
"initial_psf_negative_peak_count"] = detections.numNegPeaks
1141 self.psf_source_measurement.run(detections.sources, exposure)
1142 psf_result = self.psf_measure_psf.run(exposure=exposure, sources=detections.sources)
1145 self.install_simple_psf.run(exposure=exposure)
1147 log_psf(
"Rerunning with simple PSF:")
1155 self.psf_repair.run(exposure=exposure, keepCRs=
True)
1158 detections = self.psf_detection.run(
1161 background=background,
1162 backgroundToPhotometricRatio=background_to_photometric_ratio,
1164 self.metadata[
"simple_psf_positive_footprint_count"] = detections.numPos
1165 self.metadata[
"simple_psf_negative_footprint_count"] = detections.numNeg
1166 self.metadata[
"simple_psf_positive_peak_count"] = detections.numPosPeaks
1167 self.metadata[
"simple_psf_negative_peak_count"] = detections.numNegPeaks
1168 self.psf_source_measurement.run(detections.sources, exposure)
1169 psf_result = self.psf_measure_psf.run(exposure=exposure, sources=detections.sources)
1171 log_psf(
"Final PSF:", addToMetadata=
True)
1174 self.psf_repair.run(exposure=exposure)
1176 self.psf_source_measurement.run(detections.sources, exposure)
1180 return detections.sources, background, psf_result.cellSet
1212 def _find_stars(self, exposure, background, id_generator, background_to_photometric_ratio=None):
1213 """Detect stars on an exposure that has a PSF model, and measure their
1214 PSF, circular aperture, compensated gaussian fluxes.
1218 exposure : `lsst.afw.image.Exposure`
1219 Exposure to detect and measure stars on.
1220 background : `lsst.afw.math.BackgroundList`
1221 Background that was fit to the exposure during detection;
1222 modified in-place during subsequent detection.
1223 id_generator : `lsst.meas.base.IdGenerator`
1224 Object that generates source IDs and provides random seeds.
1225 background_to_photometric_ratio : `lsst.afw.image.Image`, optional
1226 Image to convert photometric-flattened image to
1227 background-flattened image.
1231 stars : `SourceCatalog`
1232 Sources that are very likely to be stars, with a limited set of
1233 measurements performed on them.
1236 id_generator.make_table_id_factory())
1239 detections = self.star_detection.run(
1242 background=background,
1243 backgroundToPhotometricRatio=background_to_photometric_ratio,
1245 sources = detections.sources
1246 self.star_sky_sources.run(exposure.mask, id_generator.catalog_id, sources)
1248 n_sky_sources = np.sum(sources[
"sky_source"])
1249 if (self.config.do_downsample_footprints
1250 and (len(sources) - n_sky_sources) > self.config.downsample_max_footprints):
1251 if exposure.info.id
is None:
1252 self.log.warning(
"Exposure does not have a proper id; using 0 seed for downsample.")
1255 seed = exposure.info.id & 0xFFFFFFFF
1257 gen = np.random.RandomState(seed)
1260 indices = np.arange(len(sources))[~sources[
"sky_source"]]
1261 indices = gen.choice(
1263 size=self.config.downsample_max_footprints,
1266 skyIndices, = np.where(sources[
"sky_source"])
1267 indices = np.concatenate((indices, skyIndices))
1269 self.log.info(
"Downsampling from %d to %d non-sky-source footprints.", len(sources), len(indices))
1271 sel = np.zeros(len(sources), dtype=bool)
1273 sources = sources[sel]
1276 self.star_deblend.run(exposure=exposure, sources=sources)
1279 if not sources.isContiguous():
1280 sources = sources.copy(deep=
True)
1283 self.star_measurement.run(sources, exposure)
1284 self.metadata[
"post_deblend_source_count"] = np.sum(~sources[
"sky_source"])
1285 self.metadata[
"saturated_source_count"] = np.sum(sources[
"base_PixelFlags_flag_saturated"])
1286 self.metadata[
"bad_source_count"] = np.sum(sources[
"base_PixelFlags_flag_bad"])
1291 self.star_normalized_calibration_flux.run(exposure=exposure, catalog=sources)
1292 self.star_apply_aperture_correction.run(sources, exposure.apCorrMap)
1293 self.star_catalog_calculation.run(sources)
1294 self.star_set_primary_flags.run(sources)
1296 result = self.star_selector.run(sources)
1298 if not result.sourceCat.isContiguous():
1299 return result.sourceCat.copy(deep=
True)
1301 return result.sourceCat
1304 """Match calibration stars to psf stars, to identify which were psf
1305 candidates, and which were used or reserved during psf measurement
1306 and the astrometric fit.
1310 psf_stars : `lsst.afw.table.SourceCatalog`
1311 PSF candidate stars that were sent to the psf determiner and
1312 used in the astrometric fit. Used to populate psf and astrometry
1313 related flag fields.
1314 stars : `lsst.afw.table.SourceCatalog`
1315 Stars that will be used for calibration; psf-related fields will
1316 be updated in-place.
1320 This code was adapted from CalibrateTask.copyIcSourceFields().
1324 control.findOnlyClosest =
False
1326 deblend_key = stars.schema[
"deblend_nChild"].asKey()
1327 matches = [m
for m
in matches
if m[1].get(deblend_key) == 0]
1334 for match_psf, match_stars, d
in matches:
1335 match = best.get(match_psf.getId())
1336 if match
is None or d <= match[2]:
1337 best[match_psf.getId()] = (match_psf, match_stars, d)
1338 matches = list(best.values())
1340 ids = np.array([(match_psf.getId(), match_stars.getId())
for match_psf, match_stars, d
in matches]).T
1342 if (n_matches := len(matches)) == 0:
1345 self.log.info(
"%d psf/astrometry stars out of %d matched %d calib stars",
1346 n_matches, len(psf_stars), len(stars))
1347 self.metadata[
"matched_psf_star_count"] = n_matches
1352 n_unique = len(set(m[1].getId()
for m
in matches))
1353 if n_unique != n_matches:
1354 self.log.warning(
"%d psf_stars matched only %d stars", n_matches, n_unique)
1357 idx_psf_stars = np.searchsorted(psf_stars[
"id"], ids[0])
1358 idx_stars = np.searchsorted(stars[
"id"], ids[1])
1360 result = np.zeros(len(stars), dtype=bool)
1361 result[idx_stars] = psf_stars[field][idx_psf_stars]
1362 stars[field] = result
1363 stars[
'psf_id'][idx_stars] = psf_stars[
'id'][idx_psf_stars]
1364 stars[
'psf_max_value'][idx_stars] = psf_stars[
'psf_max_value'][idx_psf_stars]