373 """Make the schema mapper from the input schema to the output schema.
377 input_schema : `lsst.afw.table.Schema`
382 mapper : `lsst.afw.table.SchemaMapper`
384 output_schema : `lsst.afw.table.Schema`
385 Output schema (with alias map)
387 mapper = afwTable.SchemaMapper(input_schema)
388 mapper.addMinimalSchema(afwTable.SourceTable.makeMinimalSchema())
389 mapper.addMapping(input_schema[
'slot_Centroid_x'].asKey())
390 mapper.addMapping(input_schema[
'slot_Centroid_y'].asKey())
393 aper_fields = input_schema.extract(
'base_CircularApertureFlux_*')
394 for field, item
in aper_fields.items():
395 mapper.addMapping(item.key)
400 apflux_fields = input_schema.extract(
'slot_ApFlux_*')
401 for field, item
in apflux_fields.items():
402 mapper.addMapping(item.key)
404 calibflux_fields = input_schema.extract(
'slot_CalibFlux_*')
405 for field, item
in calibflux_fields.items():
406 mapper.addMapping(item.key)
409 input_schema[self.config.source_selector.active.signalToNoise.fluxField].asKey(),
410 'calib_psf_selection_flux')
412 input_schema[self.config.source_selector.active.signalToNoise.errField].asKey(),
413 'calib_psf_selection_flux_err')
415 output_schema = mapper.getOutputSchema()
417 output_schema.addField(
418 'calib_psf_candidate',
420 doc=(
'set if the source was a candidate for PSF determination, '
421 'as determined from FinalizeCharacterizationTask.'),
423 output_schema.addField(
424 'calib_psf_reserved',
426 doc=(
'set if source was reserved from PSF determination by '
427 'FinalizeCharacterizationTask.'),
429 output_schema.addField(
432 doc=(
'set if source was used in the PSF determination by '
433 'FinalizeCharacterizationTask.'),
435 output_schema.addField(
438 doc=
'Visit number for the sources.',
440 output_schema.addField(
443 doc=
'Detector number for the sources.',
445 output_schema.addField(
448 doc=
"Color used in PSF fit."
450 output_schema.addField(
454 doc=
"Color used in PSF fit."
456 output_schema.addField(
459 doc=
"Maximum value in the star image used to train PSF.",
463 if self.config.do_add_sky_moments:
465 output_schema.addField(
468 doc=
"Second moment of source shape along RA axis in sky coordinates (arcsec^2).",
470 output_schema.addField(
473 doc=
"Second moment of source shape along Dec axis in sky coordinates (arcsec^2).",
475 output_schema.addField(
478 doc=
"Cross-term of source shape second moments in sky coordinates (arcsec^2).",
482 output_schema.addField(
485 doc=
"Second moment of PSF shape along RA axis in sky coordinates (arcsec^2).",
487 output_schema.addField(
490 doc=
"Second moment of PSF shape along Dec axis in sky coordinates (arcsec^2).",
492 output_schema.addField(
495 doc=
"Cross-term of PSF shape second moments in sky coordinates (arcsec^2).",
499 output_schema.addField(
502 doc=
"Second moment of source shape along altitude axis (arcsec^2).",
504 output_schema.addField(
507 doc=
"Second moment of source shape along azimuth axis (arcsec^2).",
509 output_schema.addField(
512 doc=
"Cross-term of source shape second moments in alt/az coordinates (arcsec^2).",
516 output_schema.addField(
519 doc=
"Second moment of PSF shape along altitude axis (arcsec^2).",
521 output_schema.addField(
524 doc=
"Second moment of PSF shape along azimuth axis (arcsec^2).",
526 output_schema.addField(
529 doc=
"Cross-term of PSF shape second moments in alt/az coordinates (arcsec^2).",
532 if self.config.do_add_fgcm_photometry:
534 for band_name
in self.config.fgcmPhotometryBands:
535 output_schema.addField(
536 f
'fgcm_mag_{band_name}',
538 doc=f
"FGCM standard star magnitude in {band_name} band (mag).",
541 alias_map = input_schema.getAliasMap()
542 alias_map_output = afwTable.AliasMap()
543 alias_map_output.set(
'slot_Centroid', alias_map.get(
'slot_Centroid'))
544 alias_map_output.set(
'slot_ApFlux', alias_map.get(
'slot_ApFlux'))
545 alias_map_output.set(
'slot_CalibFlux', alias_map.get(
'slot_CalibFlux'))
547 output_schema.setAliasMap(alias_map_output)
549 return mapper, output_schema
594 """Compute second moments in sky coordinates from pixel moments.
596 Transforms the second moments tensor from pixel coordinates to sky
597 coordinates (RA/Dec) using the local WCS CD matrix at each source
602 wcs : `lsst.afw.geom.SkyWcs`
603 The WCS of the exposure.
605 X pixel coordinates of the sources.
607 Y pixel coordinates of the sources.
608 ixx : `numpy.ndarray`
609 Second moment Ixx in pixel coordinates (pixels^2).
610 iyy : `numpy.ndarray`
611 Second moment Iyy in pixel coordinates (pixels^2).
612 ixy : `numpy.ndarray`
613 Second moment Ixy in pixel coordinates (pixels^2).
617 iuu : `numpy.ndarray`
618 Second moment along RA axis in sky coordinates (arcsec^2).
619 ivv : `numpy.ndarray`
620 Second moment along Dec axis in sky coordinates (arcsec^2).
621 iuv : `numpy.ndarray`
622 Cross-term of second moments in sky coordinates (arcsec^2).
625 iuu = np.full(n_sources, np.nan, dtype=np.float32)
626 ivv = np.full(n_sources, np.nan, dtype=np.float32)
627 iuv = np.full(n_sources, np.nan, dtype=np.float32)
631 deg2_to_arcsec2 = 3600.0 ** 2
633 for i
in range(n_sources):
635 if not np.isfinite(ixx[i])
or not np.isfinite(iyy[i])
or not np.isfinite(ixy[i]):
639 cd_matrix = wcs.getCdMatrix(center)
641 cd11 = cd_matrix[0, 0]
642 cd12 = cd_matrix[0, 1]
643 cd21 = cd_matrix[1, 0]
644 cd22 = cd_matrix[1, 1]
648 iuu[i] = (cd11 * (ixx[i] * cd11 + ixy[i] * cd12)
649 + cd12 * (ixy[i] * cd11 + iyy[i] * cd12))
651 ivv[i] = (cd21 * (ixx[i] * cd21 + ixy[i] * cd22)
652 + cd22 * (ixy[i] * cd21 + iyy[i] * cd22))
654 iuv[i] = ((cd11 * ixx[i] + cd12 * ixy[i]) * cd21
655 + (cd11 * ixy[i] + cd12 * iyy[i]) * cd22)
658 iuu[i] *= deg2_to_arcsec2
659 ivv[i] *= deg2_to_arcsec2
660 iuv[i] *= deg2_to_arcsec2
665 """Rotate second moments from RA/Dec to Alt/Az coordinates.
667 Rotates the second moments tensor from equatorial (RA/Dec) coordinates
668 to horizontal (Alt/Az) coordinates using the parallactic angle.
672 iuu : `numpy.ndarray`
673 Second moment along RA axis in sky coordinates (arcsec^2).
674 ivv : `numpy.ndarray`
675 Second moment along Dec axis in sky coordinates (arcsec^2).
676 iuv : `numpy.ndarray`
677 Cross-term of second moments in sky coordinates (arcsec^2).
678 parallactic_angle : `lsst.geom.Angle`
679 The parallactic angle (from visitInfo.boresightParAngle).
683 ialtalt : `numpy.ndarray`
684 Second moment along altitude axis (arcsec^2).
685 iazaz : `numpy.ndarray`
686 Second moment along azimuth axis (arcsec^2).
687 ialtaz : `numpy.ndarray`
688 Cross-term of second moments in alt/az coordinates (arcsec^2).
692 ialtalt = np.full(n_sources, np.nan, dtype=np.float32)
693 iazaz = np.full(n_sources, np.nan, dtype=np.float32)
694 ialtaz = np.full(n_sources, np.nan, dtype=np.float32)
697 rot_transform = LinearTransform.makeRotation(parallactic_angle)
699 for i
in range(n_sources):
701 if not np.isfinite(iuu[i])
or not np.isfinite(ivv[i])
or not np.isfinite(iuv[i]):
705 shape = Quadrupole(iuu[i], ivv[i], iuv[i])
706 rot_shape = shape.transform(rot_transform)
708 ialtalt[i] = rot_shape.getIxx()
709 iazaz[i] = rot_shape.getIyy()
710 ialtaz[i] = rot_shape.getIxy()
712 return ialtalt, iazaz, ialtaz
717 isolated_star_cat_dict,
718 isolated_star_source_dict,
723 Concatenate isolated star catalogs and make reserve selection.
728 Band name. Used to select reserved stars.
729 isolated_star_cat_dict : `dict`
730 Per-tract dict of isolated star catalog handles.
731 isolated_star_source_dict : `dict`
732 Per-tract dict of isolated star source catalog handles.
733 visit : `int`, optional
734 Visit to down-select sources.
735 detector : `int`, optional
736 Detector to down-select sources. Will only be used if visit
741 isolated_table : `astropy.table.Table` (N,)
742 Table of isolated stars, with indexes to isolated sources.
743 Returns None if there are no usable isolated catalogs.
744 isolated_source_table : `astropy.table.Table` (M,)
745 Table of isolated sources, with indexes to isolated stars.
746 Returns None if there are no usable isolated catalogs.
749 isolated_sources = []
750 merge_cat_counter = 0
751 merge_source_counter = 0
753 handle = isolated_star_cat_dict[list(isolated_star_cat_dict.keys())[0]]
754 all_source_columns = handle.get(component=
'columns')
755 source_columns = [self.config.id_column,
'obj_index']
757 if visit
is not None and visit
in all_source_columns:
758 source_columns.append(
'visit')
759 if detector
is not None:
760 source_columns.append(
'detector')
765 for tract
in isolated_star_cat_dict:
766 astropy_cat = isolated_star_cat_dict[tract].get()
767 astropy_source = isolated_star_source_dict[tract].get(
768 parameters={
'columns': source_columns}
772 sources_downselected =
False
773 if visit
is not None:
774 sources_downselected =
True
775 these_sources = (astropy_source[
'visit'] == visit)
777 if these_sources.sum() == 0:
778 self.log.info(
'No sources found for visit %d in tract %d.', visit, tract)
781 if detector
is not None:
782 these_sources &= (astropy_source[
'detector'] == detector)
783 if these_sources.sum() == 0:
785 'No sources found for visit %d, detector %d in tract %d.',
792 astropy_source = astropy_source[these_sources]
797 (use_band,) = (astropy_cat[f
'nsource_{band}'] > 0).nonzero()
799 if len(use_band) == 0:
801 self.log.info(
"No sources found in %s band in tract %d.", band, tract)
806 a, b = esutil.numpy_util.match(use_band, np.asarray(astropy_source[
'obj_index']))
809 astropy_source[
'obj_index'][b] = a
810 if sources_downselected:
819 astropy_cat[f
'source_cat_index_{band}'][use_band][a] = b
823 _, index_new = np.unique(a, return_index=
True)
824 astropy_cat[f
'source_cat_index_{band}'][use_band] = index_new
835 astropy_source = astropy_source[b]
836 astropy_cat = astropy_cat[use_band]
839 astropy_cat[
'reserved'] =
False
840 astropy_source[
'reserved'] =
False
843 astropy_cat[
'reserved'][:] = self.reserve_selection.run(
845 extra=f
'{band}_{tract}',
847 astropy_source[
'reserved'][:] = astropy_cat[
'reserved'][astropy_source[
'obj_index']]
850 astropy_cat[f
'source_cat_index_{band}'] += merge_source_counter
851 astropy_source[
'obj_index'] += merge_cat_counter
853 isolated_tables.append(astropy_cat)
854 isolated_sources.append(astropy_source)
856 merge_cat_counter += len(astropy_cat)
857 merge_source_counter += len(astropy_source)
859 if len(isolated_tables) > 0:
860 isolated_table = astropy.table.vstack(isolated_tables, metadata_conflicts=
'silent')
861 isolated_source_table = astropy.table.vstack(isolated_sources, metadata_conflicts=
'silent')
863 isolated_table =
None
864 isolated_source_table =
None
866 return isolated_table, isolated_source_table
932 isolated_source_table, fgcm_standard_star_cat):
933 """Compute psf model and aperture correction map for a single exposure.
938 Visit number (for logging).
940 Detector number (for logging).
941 exposure : `lsst.afw.image.ExposureF`
942 src : `lsst.afw.table.SourceCatalog`
943 isolated_source_table : `np.ndarray` or `astropy.table.Table`
944 fgcm_standard_star_cat : `np.ndarray`
948 psf : `lsst.meas.algorithms.ImagePsf`
950 ap_corr_map : `lsst.afw.image.ApCorrMap`
951 Aperture correction map.
952 measured_src : `lsst.afw.table.SourceCatalog`
953 Updated source catalog with measurements, flags and aperture corrections.
956 footprints = SingleFrameMeasurementTask.getFootprintsFromCatalog(src)
959 good_src = self.source_selector.selectSources(src)
960 if sum(good_src.selected) == 0:
961 self.log.warning(
'No good sources remain after cuts for visit %d, detector %d',
963 return None,
None,
None
972 selected_src = afwTable.SourceCatalog(selection_schema)
973 selected_src.reserve(good_src.selected.sum())
974 selected_src.extend(src[good_src.selected], mapper=selection_mapper)
978 selected_src[
'calib_psf_candidate'] = np.zeros(len(selected_src), dtype=bool)
979 selected_src[
'calib_psf_used'] = np.zeros(len(selected_src), dtype=bool)
980 selected_src[
'calib_psf_reserved'] = np.zeros(len(selected_src), dtype=bool)
983 matched_src, matched_iso = esutil.numpy_util.match(
985 np.asarray(isolated_source_table[self.config.id_column]),
987 if len(matched_src) == 0:
989 "No candidates from matched isolate stars for visit=%s, detector=%s "
990 "(this is probably the result of an earlier astrometry failure).",
993 return None,
None,
None
995 matched_arr = np.zeros(len(selected_src), dtype=bool)
996 matched_arr[matched_src] =
True
997 selected_src[
'calib_psf_candidate'] = matched_arr
999 reserved_arr = np.zeros(len(selected_src), dtype=bool)
1000 reserved_arr[matched_src] = np.asarray(isolated_source_table[
'reserved'][matched_iso])
1001 selected_src[
'calib_psf_reserved'] = reserved_arr
1003 selected_src = selected_src[selected_src[
'calib_psf_candidate']].copy(deep=
True)
1006 measured_src = afwTable.SourceCatalog(self.
schema)
1007 measured_src.reserve(len(selected_src))
1008 measured_src.extend(selected_src, mapper=self.
schema_mapper)
1011 measured_src[
'calib_psf_candidate'] = selected_src[
'calib_psf_candidate']
1012 measured_src[
'calib_psf_reserved'] = selected_src[
'calib_psf_reserved']
1013 if exposure.filter.hasBandLabel():
1014 band = exposure.filter.bandLabel
1021 if self.config.do_add_fgcm_photometry:
1026 psf_selection_result = self.make_psf_candidates.run(selected_src, exposure=exposure)
1027 _ = self.make_psf_candidates.run(measured_src, exposure=exposure)
1028 except Exception
as e:
1029 self.log.exception(
'Failed to make PSF candidates for visit %d, detector %d: %s',
1031 return None,
None, measured_src
1033 psf_cand_cat = psf_selection_result.goodStarCat
1037 psf_determiner_list = [cand
for cand, use
1038 in zip(psf_selection_result.psfCandidates,
1039 ~psf_cand_cat[
'calib_psf_reserved'])
if use]
1040 flag_key = psf_cand_cat.schema[
'calib_psf_used'].asKey()
1044 psf_determiner_list,
1047 except Exception
as e:
1048 self.log.exception(
'Failed to determine PSF for visit %d, detector %d: %s',
1050 return None,
None, measured_src
1052 sigma = psf.computeShape(psf.getAveragePosition(), psf.getAverageColor()).getDeterminantRadius()
1054 self.log.warning(
'Failed to determine psf for visit %d, detector %d: '
1055 'Computed final PSF size is NAN.',
1057 return None,
None, measured_src
1060 exposure.setPsf(psf)
1064 matched_selected, matched_measured = esutil.numpy_util.match(
1068 measured_used = np.zeros(len(measured_src), dtype=bool)
1069 measured_used[matched_measured] = selected_src[
'calib_psf_used'][matched_selected]
1070 measured_src[
'calib_psf_used'] = measured_used
1075 self.measurement.run(measCat=measured_src, exposure=exposure, footprints=footprints)
1076 except Exception
as e:
1077 self.log.warning(
'Failed to make measurements for visit %d, detector %d: %s',
1079 return psf,
None, measured_src
1082 if self.config.do_add_sky_moments:
1083 wcs = exposure.getWcs()
1084 x = np.array(measured_src[
'slot_Centroid_x'])
1085 y = np.array(measured_src[
'slot_Centroid_y'])
1088 shape_xx = np.array(measured_src[
'slot_Shape_xx'])
1089 shape_yy = np.array(measured_src[
'slot_Shape_yy'])
1090 shape_xy = np.array(measured_src[
'slot_Shape_xy'])
1092 wcs, x, y, shape_xx, shape_yy, shape_xy
1094 measured_src[
'shape_Iuu'] = shape_iuu
1095 measured_src[
'shape_Ivv'] = shape_ivv
1096 measured_src[
'shape_Iuv'] = shape_iuv
1099 psf_xx = np.array(measured_src[
'slot_PsfShape_xx'])
1100 psf_yy = np.array(measured_src[
'slot_PsfShape_yy'])
1101 psf_xy = np.array(measured_src[
'slot_PsfShape_xy'])
1103 wcs, x, y, psf_xx, psf_yy, psf_xy
1105 measured_src[
'psfShape_Iuu'] = psf_iuu
1106 measured_src[
'psfShape_Ivv'] = psf_ivv
1107 measured_src[
'psfShape_Iuv'] = psf_iuv
1110 visit_info = exposure.info.getVisitInfo()
1111 parallactic_angle = visit_info.boresightParAngle
1115 shape_iuu, shape_ivv, shape_iuv, parallactic_angle
1117 measured_src[
'shape_Ialtalt'] = shape_ialtalt
1118 measured_src[
'shape_Iazaz'] = shape_iazaz
1119 measured_src[
'shape_Ialtaz'] = shape_ialtaz
1123 psf_iuu, psf_ivv, psf_iuv, parallactic_angle
1125 measured_src[
'psfShape_Ialtalt'] = psf_ialtalt
1126 measured_src[
'psfShape_Iazaz'] = psf_iazaz
1127 measured_src[
'psfShape_Ialtaz'] = psf_ialtaz
1131 ap_corr_map = self.measure_ap_corr.run(exposure=exposure,
1132 catalog=measured_src).apCorrMap
1133 except Exception
as e:
1134 self.log.warning(
'Failed to compute aperture corrections for visit %d, detector %d: %s',
1136 return psf,
None, measured_src
1139 ap_corr_map_input = exposure.apCorrMap
1140 for key
in ap_corr_map_input:
1141 if key
not in ap_corr_map:
1142 ap_corr_map[key] = ap_corr_map_input[key]
1144 self.apply_ap_corr.run(catalog=measured_src, apCorrMap=ap_corr_map)
1146 return psf, ap_corr_map, measured_src