367 """Make the schema mapper from the input schema to the output schema.
371 input_schema : `lsst.afw.table.Schema`
376 mapper : `lsst.afw.table.SchemaMapper`
378 output_schema : `lsst.afw.table.Schema`
379 Output schema (with alias map)
381 mapper = afwTable.SchemaMapper(input_schema)
382 mapper.addMinimalSchema(afwTable.SourceTable.makeMinimalSchema())
383 mapper.addMapping(input_schema[
'slot_Centroid_x'].asKey())
384 mapper.addMapping(input_schema[
'slot_Centroid_y'].asKey())
387 aper_fields = input_schema.extract(
'base_CircularApertureFlux_*')
388 for field, item
in aper_fields.items():
389 mapper.addMapping(item.key)
394 apflux_fields = input_schema.extract(
'slot_ApFlux_*')
395 for field, item
in apflux_fields.items():
396 mapper.addMapping(item.key)
398 calibflux_fields = input_schema.extract(
'slot_CalibFlux_*')
399 for field, item
in calibflux_fields.items():
400 mapper.addMapping(item.key)
403 input_schema[self.config.source_selector.active.signalToNoise.fluxField].asKey(),
404 'calib_psf_selection_flux')
406 input_schema[self.config.source_selector.active.signalToNoise.errField].asKey(),
407 'calib_psf_selection_flux_err')
409 output_schema = mapper.getOutputSchema()
411 output_schema.addField(
412 'calib_psf_candidate',
414 doc=(
'set if the source was a candidate for PSF determination, '
415 'as determined from FinalizeCharacterizationTask.'),
417 output_schema.addField(
418 'calib_psf_reserved',
420 doc=(
'set if source was reserved from PSF determination by '
421 'FinalizeCharacterizationTask.'),
423 output_schema.addField(
426 doc=(
'set if source was used in the PSF determination by '
427 'FinalizeCharacterizationTask.'),
429 output_schema.addField(
432 doc=
'Visit number for the sources.',
434 output_schema.addField(
437 doc=
'Detector number for the sources.',
439 output_schema.addField(
442 doc=
"Color used in PSF fit."
444 output_schema.addField(
448 doc=
"Color used in PSF fit."
450 output_schema.addField(
453 doc=
"Maximum value in the star image used to train PSF.",
457 if self.config.do_add_sky_moments:
459 output_schema.addField(
462 doc=
"Second moment of source shape along RA axis in sky coordinates (arcsec^2).",
464 output_schema.addField(
467 doc=
"Second moment of source shape along Dec axis in sky coordinates (arcsec^2).",
469 output_schema.addField(
472 doc=
"Cross-term of source shape second moments in sky coordinates (arcsec^2).",
476 output_schema.addField(
479 doc=
"Second moment of PSF shape along RA axis in sky coordinates (arcsec^2).",
481 output_schema.addField(
484 doc=
"Second moment of PSF shape along Dec axis in sky coordinates (arcsec^2).",
486 output_schema.addField(
489 doc=
"Cross-term of PSF shape second moments in sky coordinates (arcsec^2).",
493 output_schema.addField(
496 doc=
"Second moment of source shape along altitude axis (arcsec^2).",
498 output_schema.addField(
501 doc=
"Second moment of source shape along azimuth axis (arcsec^2).",
503 output_schema.addField(
506 doc=
"Cross-term of source shape second moments in alt/az coordinates (arcsec^2).",
510 output_schema.addField(
513 doc=
"Second moment of PSF shape along altitude axis (arcsec^2).",
515 output_schema.addField(
518 doc=
"Second moment of PSF shape along azimuth axis (arcsec^2).",
520 output_schema.addField(
523 doc=
"Cross-term of PSF shape second moments in alt/az coordinates (arcsec^2).",
526 if self.config.do_add_fgcm_photometry:
528 for band_name
in self.config.fgcmPhotometryBands:
529 output_schema.addField(
530 f
'fgcm_mag_{band_name}',
532 doc=f
"FGCM standard star magnitude in {band_name} band (mag).",
535 alias_map = input_schema.getAliasMap()
536 alias_map_output = afwTable.AliasMap()
537 alias_map_output.set(
'slot_Centroid', alias_map.get(
'slot_Centroid'))
538 alias_map_output.set(
'slot_ApFlux', alias_map.get(
'slot_ApFlux'))
539 alias_map_output.set(
'slot_CalibFlux', alias_map.get(
'slot_CalibFlux'))
541 output_schema.setAliasMap(alias_map_output)
543 return mapper, output_schema
588 """Compute second moments in sky coordinates from pixel moments.
590 Transforms the second moments tensor from pixel coordinates to sky
591 coordinates (RA/Dec) using the local WCS CD matrix at each source
596 wcs : `lsst.afw.geom.SkyWcs`
597 The WCS of the exposure.
599 X pixel coordinates of the sources.
601 Y pixel coordinates of the sources.
602 ixx : `numpy.ndarray`
603 Second moment Ixx in pixel coordinates (pixels^2).
604 iyy : `numpy.ndarray`
605 Second moment Iyy in pixel coordinates (pixels^2).
606 ixy : `numpy.ndarray`
607 Second moment Ixy in pixel coordinates (pixels^2).
611 iuu : `numpy.ndarray`
612 Second moment along RA axis in sky coordinates (arcsec^2).
613 ivv : `numpy.ndarray`
614 Second moment along Dec axis in sky coordinates (arcsec^2).
615 iuv : `numpy.ndarray`
616 Cross-term of second moments in sky coordinates (arcsec^2).
619 iuu = np.full(n_sources, np.nan, dtype=np.float32)
620 ivv = np.full(n_sources, np.nan, dtype=np.float32)
621 iuv = np.full(n_sources, np.nan, dtype=np.float32)
625 deg2_to_arcsec2 = 3600.0 ** 2
627 for i
in range(n_sources):
629 if not np.isfinite(ixx[i])
or not np.isfinite(iyy[i])
or not np.isfinite(ixy[i]):
633 cd_matrix = wcs.getCdMatrix(center)
635 cd11 = cd_matrix[0, 0]
636 cd12 = cd_matrix[0, 1]
637 cd21 = cd_matrix[1, 0]
638 cd22 = cd_matrix[1, 1]
642 iuu[i] = (cd11 * (ixx[i] * cd11 + ixy[i] * cd12)
643 + cd12 * (ixy[i] * cd11 + iyy[i] * cd12))
645 ivv[i] = (cd21 * (ixx[i] * cd21 + ixy[i] * cd22)
646 + cd22 * (ixy[i] * cd21 + iyy[i] * cd22))
648 iuv[i] = ((cd11 * ixx[i] + cd12 * ixy[i]) * cd21
649 + (cd11 * ixy[i] + cd12 * iyy[i]) * cd22)
652 iuu[i] *= deg2_to_arcsec2
653 ivv[i] *= deg2_to_arcsec2
654 iuv[i] *= deg2_to_arcsec2
659 """Rotate second moments from RA/Dec to Alt/Az coordinates.
661 Rotates the second moments tensor from equatorial (RA/Dec) coordinates
662 to horizontal (Alt/Az) coordinates using the parallactic angle.
666 iuu : `numpy.ndarray`
667 Second moment along RA axis in sky coordinates (arcsec^2).
668 ivv : `numpy.ndarray`
669 Second moment along Dec axis in sky coordinates (arcsec^2).
670 iuv : `numpy.ndarray`
671 Cross-term of second moments in sky coordinates (arcsec^2).
672 parallactic_angle : `lsst.geom.Angle`
673 The parallactic angle (from visitInfo.boresightParAngle).
677 ialtalt : `numpy.ndarray`
678 Second moment along altitude axis (arcsec^2).
679 iazaz : `numpy.ndarray`
680 Second moment along azimuth axis (arcsec^2).
681 ialtaz : `numpy.ndarray`
682 Cross-term of second moments in alt/az coordinates (arcsec^2).
686 ialtalt = np.full(n_sources, np.nan, dtype=np.float32)
687 iazaz = np.full(n_sources, np.nan, dtype=np.float32)
688 ialtaz = np.full(n_sources, np.nan, dtype=np.float32)
691 rot_transform = LinearTransform.makeRotation(parallactic_angle)
693 for i
in range(n_sources):
695 if not np.isfinite(iuu[i])
or not np.isfinite(ivv[i])
or not np.isfinite(iuv[i]):
699 shape = Quadrupole(iuu[i], ivv[i], iuv[i])
700 rot_shape = shape.transform(rot_transform)
702 ialtalt[i] = rot_shape.getIxx()
703 iazaz[i] = rot_shape.getIyy()
704 ialtaz[i] = rot_shape.getIxy()
706 return ialtalt, iazaz, ialtaz
711 isolated_star_cat_dict,
712 isolated_star_source_dict,
717 Concatenate isolated star catalogs and make reserve selection.
722 Band name. Used to select reserved stars.
723 isolated_star_cat_dict : `dict`
724 Per-tract dict of isolated star catalog handles.
725 isolated_star_source_dict : `dict`
726 Per-tract dict of isolated star source catalog handles.
727 visit : `int`, optional
728 Visit to down-select sources.
729 detector : `int`, optional
730 Detector to down-select sources. Will only be used if visit
735 isolated_table : `astropy.table.Table` (N,)
736 Table of isolated stars, with indexes to isolated sources.
737 Returns None if there are no usable isolated catalogs.
738 isolated_source_table : `astropy.table.Table` (M,)
739 Table of isolated sources, with indexes to isolated stars.
740 Returns None if there are no usable isolated catalogs.
743 isolated_sources = []
744 merge_cat_counter = 0
745 merge_source_counter = 0
747 source_columns = [self.config.id_column,
'obj_index']
748 if visit
is not None:
749 source_columns.append(
'visit')
750 if detector
is not None:
751 source_columns.append(
'detector')
753 for tract
in isolated_star_cat_dict:
754 astropy_cat = isolated_star_cat_dict[tract].get()
755 astropy_source = isolated_star_source_dict[tract].get(
756 parameters={
'columns': source_columns}
760 sources_downselected =
False
761 if visit
is not None:
762 sources_downselected =
True
763 these_sources = (astropy_source[
'visit'] == visit)
765 if these_sources.sum() == 0:
766 self.log.info(
'No sources found for visit %d in tract %d.', visit, tract)
769 if detector
is not None:
770 these_sources &= (astropy_source[
'detector'] == detector)
771 if these_sources.sum() == 0:
773 'No sources found for visit %d, detector %d in tract %d.',
780 astropy_source = astropy_source[these_sources]
785 (use_band,) = (astropy_cat[f
'nsource_{band}'] > 0).nonzero()
787 if len(use_band) == 0:
789 self.log.info(
"No sources found in %s band in tract %d.", band, tract)
794 a, b = esutil.numpy_util.match(use_band, np.asarray(astropy_source[
'obj_index']))
797 astropy_source[
'obj_index'][b] = a
798 if sources_downselected:
807 astropy_cat[f
'source_cat_index_{band}'][use_band][a] = b
811 _, index_new = np.unique(a, return_index=
True)
812 astropy_cat[f
'source_cat_index_{band}'][use_band] = index_new
823 astropy_source = astropy_source[b]
824 astropy_cat = astropy_cat[use_band]
827 astropy_cat[
'reserved'] =
False
828 astropy_source[
'reserved'] =
False
831 astropy_cat[
'reserved'][:] = self.reserve_selection.run(
833 extra=f
'{band}_{tract}',
835 astropy_source[
'reserved'][:] = astropy_cat[
'reserved'][astropy_source[
'obj_index']]
838 astropy_cat[f
'source_cat_index_{band}'] += merge_source_counter
839 astropy_source[
'obj_index'] += merge_cat_counter
841 isolated_tables.append(astropy_cat)
842 isolated_sources.append(astropy_source)
844 merge_cat_counter += len(astropy_cat)
845 merge_source_counter += len(astropy_source)
847 if len(isolated_tables) > 0:
848 isolated_table = astropy.table.vstack(isolated_tables, metadata_conflicts=
'silent')
849 isolated_source_table = astropy.table.vstack(isolated_sources, metadata_conflicts=
'silent')
851 isolated_table =
None
852 isolated_source_table =
None
854 return isolated_table, isolated_source_table
920 isolated_source_table, fgcm_standard_star_cat):
921 """Compute psf model and aperture correction map for a single exposure.
926 Visit number (for logging).
928 Detector number (for logging).
929 exposure : `lsst.afw.image.ExposureF`
930 src : `lsst.afw.table.SourceCatalog`
931 isolated_source_table : `np.ndarray` or `astropy.table.Table`
932 fgcm_standard_star_cat : `np.ndarray`
936 psf : `lsst.meas.algorithms.ImagePsf`
938 ap_corr_map : `lsst.afw.image.ApCorrMap`
939 Aperture correction map.
940 measured_src : `lsst.afw.table.SourceCatalog`
941 Updated source catalog with measurements, flags and aperture corrections.
944 footprints = SingleFrameMeasurementTask.getFootprintsFromCatalog(src)
947 good_src = self.source_selector.selectSources(src)
948 if sum(good_src.selected) == 0:
949 self.log.warning(
'No good sources remain after cuts for visit %d, detector %d',
951 return None,
None,
None
960 selected_src = afwTable.SourceCatalog(selection_schema)
961 selected_src.reserve(good_src.selected.sum())
962 selected_src.extend(src[good_src.selected], mapper=selection_mapper)
966 selected_src[
'calib_psf_candidate'] = np.zeros(len(selected_src), dtype=bool)
967 selected_src[
'calib_psf_used'] = np.zeros(len(selected_src), dtype=bool)
968 selected_src[
'calib_psf_reserved'] = np.zeros(len(selected_src), dtype=bool)
971 matched_src, matched_iso = esutil.numpy_util.match(
973 np.asarray(isolated_source_table[self.config.id_column]),
975 if len(matched_src) == 0:
977 "No candidates from matched isolate stars for visit=%s, detector=%s "
978 "(this is probably the result of an earlier astrometry failure).",
981 return None,
None,
None
983 matched_arr = np.zeros(len(selected_src), dtype=bool)
984 matched_arr[matched_src] =
True
985 selected_src[
'calib_psf_candidate'] = matched_arr
987 reserved_arr = np.zeros(len(selected_src), dtype=bool)
988 reserved_arr[matched_src] = np.asarray(isolated_source_table[
'reserved'][matched_iso])
989 selected_src[
'calib_psf_reserved'] = reserved_arr
991 selected_src = selected_src[selected_src[
'calib_psf_candidate']].copy(deep=
True)
994 measured_src = afwTable.SourceCatalog(self.
schema)
995 measured_src.reserve(len(selected_src))
996 measured_src.extend(selected_src, mapper=self.schema_mapper)
999 measured_src[
'calib_psf_candidate'] = selected_src[
'calib_psf_candidate']
1000 measured_src[
'calib_psf_reserved'] = selected_src[
'calib_psf_reserved']
1001 if exposure.filter.hasBandLabel():
1002 band = exposure.filter.bandLabel
1009 if self.config.do_add_fgcm_photometry:
1014 psf_selection_result = self.make_psf_candidates.run(selected_src, exposure=exposure)
1015 _ = self.make_psf_candidates.run(measured_src, exposure=exposure)
1016 except Exception
as e:
1017 self.log.exception(
'Failed to make PSF candidates for visit %d, detector %d: %s',
1019 return None,
None, measured_src
1021 psf_cand_cat = psf_selection_result.goodStarCat
1025 psf_determiner_list = [cand
for cand, use
1026 in zip(psf_selection_result.psfCandidates,
1027 ~psf_cand_cat[
'calib_psf_reserved'])
if use]
1028 flag_key = psf_cand_cat.schema[
'calib_psf_used'].asKey()
1032 psf_determiner_list,
1035 except Exception
as e:
1036 self.log.exception(
'Failed to determine PSF for visit %d, detector %d: %s',
1038 return None,
None, measured_src
1040 sigma = psf.computeShape(psf.getAveragePosition(), psf.getAverageColor()).getDeterminantRadius()
1042 self.log.warning(
'Failed to determine psf for visit %d, detector %d: '
1043 'Computed final PSF size is NAN.',
1045 return None,
None, measured_src
1048 exposure.setPsf(psf)
1052 matched_selected, matched_measured = esutil.numpy_util.match(
1056 measured_used = np.zeros(len(measured_src), dtype=bool)
1057 measured_used[matched_measured] = selected_src[
'calib_psf_used'][matched_selected]
1058 measured_src[
'calib_psf_used'] = measured_used
1063 self.measurement.run(measCat=measured_src, exposure=exposure, footprints=footprints)
1064 except Exception
as e:
1065 self.log.warning(
'Failed to make measurements for visit %d, detector %d: %s',
1067 return psf,
None, measured_src
1070 if self.config.do_add_sky_moments:
1071 wcs = exposure.getWcs()
1072 x = np.array(measured_src[
'slot_Centroid_x'])
1073 y = np.array(measured_src[
'slot_Centroid_y'])
1076 shape_xx = np.array(measured_src[
'slot_Shape_xx'])
1077 shape_yy = np.array(measured_src[
'slot_Shape_yy'])
1078 shape_xy = np.array(measured_src[
'slot_Shape_xy'])
1080 wcs, x, y, shape_xx, shape_yy, shape_xy
1082 measured_src[
'shape_Iuu'] = shape_iuu
1083 measured_src[
'shape_Ivv'] = shape_ivv
1084 measured_src[
'shape_Iuv'] = shape_iuv
1087 psf_xx = np.array(measured_src[
'slot_PsfShape_xx'])
1088 psf_yy = np.array(measured_src[
'slot_PsfShape_yy'])
1089 psf_xy = np.array(measured_src[
'slot_PsfShape_xy'])
1091 wcs, x, y, psf_xx, psf_yy, psf_xy
1093 measured_src[
'psfShape_Iuu'] = psf_iuu
1094 measured_src[
'psfShape_Ivv'] = psf_ivv
1095 measured_src[
'psfShape_Iuv'] = psf_iuv
1098 visit_info = exposure.info.getVisitInfo()
1099 parallactic_angle = visit_info.boresightParAngle
1103 shape_iuu, shape_ivv, shape_iuv, parallactic_angle
1105 measured_src[
'shape_Ialtalt'] = shape_ialtalt
1106 measured_src[
'shape_Iazaz'] = shape_iazaz
1107 measured_src[
'shape_Ialtaz'] = shape_ialtaz
1111 psf_iuu, psf_ivv, psf_iuv, parallactic_angle
1113 measured_src[
'psfShape_Ialtalt'] = psf_ialtalt
1114 measured_src[
'psfShape_Iazaz'] = psf_iazaz
1115 measured_src[
'psfShape_Ialtaz'] = psf_ialtaz
1119 ap_corr_map = self.measure_ap_corr.run(exposure=exposure,
1120 catalog=measured_src).apCorrMap
1121 except Exception
as e:
1122 self.log.warning(
'Failed to compute aperture corrections for visit %d, detector %d: %s',
1124 return psf,
None, measured_src
1127 ap_corr_map_input = exposure.apCorrMap
1128 for key
in ap_corr_map_input:
1129 if key
not in ap_corr_map:
1130 ap_corr_map[key] = ap_corr_map_input[key]
1132 self.apply_ap_corr.run(catalog=measured_src, apCorrMap=ap_corr_map)
1134 return psf, ap_corr_map, measured_src