From 4a31528bb3e480b96abe22d67716febd03fb7745 Mon Sep 17 00:00:00 2001 From: Dan Taranu Date: Wed, 28 Jan 2026 11:04:29 -0800 Subject: [PATCH 1/3] Add .idea IDE path to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a1c918a67..9968b36e6 100644 --- a/.gitignore +++ b/.gitignore @@ -181,6 +181,7 @@ tests/data/kernel*.boost ups/*.cfgc extensions/ .vscode/ +.idea/ # Output of tests doc/htmlDir From 07c571117cb587ffd1505d91eee2ff4a7c910e00 Mon Sep 17 00:00:00 2001 From: Dan Taranu Date: Wed, 28 Jan 2026 11:09:07 -0800 Subject: [PATCH 2/3] Template calculateCoordCovariance to allow float and double --- include/lsst/afw/table/wcsUtils.h | 18 +++++++++++++----- src/table/wcsUtils.cc | 16 ++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/include/lsst/afw/table/wcsUtils.h b/include/lsst/afw/table/wcsUtils.h index 47d2a7b5b..f818483ae 100644 --- a/include/lsst/afw/table/wcsUtils.h +++ b/include/lsst/afw/table/wcsUtils.h @@ -64,15 +64,23 @@ template void updateSourceCoords(geom::SkyWcs const& wcs, SourceCollection& sourceList, bool include_covariance=true); /** - * Calculate covariance for sky coordinates + * Calculate covariance for sky coordinates given a pixel centroid and errors. + * + * @tparam t Floating point type of the input errors/output covariance. * - * * @param[in] wcs WCS to map from pixels to sky. - * @param[in] center The object centroid. + * @param[in] center The object centroid in pixels. * @param[in] err Covariance matrix of the centroid. - * + * @param[in] factor Factor to multiply the WCS matrix terms, which are in degrees. + * Default is pi/180 to convert to radians. + * + * @return The RA/Dec sky covariance matrix in degrees, multiplied by factor. */ -Eigen::Matrix2f calculateCoordCovariance(geom::SkyWcs const& wcs, lsst::geom::Point2D center, Eigen::Matrix2f err); +template +Eigen::Matrix calculateCoordCovariance( + geom::SkyWcs const& wcs, lsst::geom::Point2D center, Eigen::Matrix err, + double factor=lsst::geom::PI/180.0 +); } // namespace table } // namespace afw diff --git a/src/table/wcsUtils.cc b/src/table/wcsUtils.cc index 8628c74f8..5aec0c966 100644 --- a/src/table/wcsUtils.cc +++ b/src/table/wcsUtils.cc @@ -92,10 +92,11 @@ void updateRefCentroids(geom::SkyWcs const &wcs, ReferenceCollection &refList) { } } -Eigen::Matrix2f calculateCoordCovariance(geom::SkyWcs const &wcs, lsst::geom::Point2D center, - Eigen::Matrix2f err) { +template +Eigen::Matrix calculateCoordCovariance(geom::SkyWcs const &wcs, lsst::geom::Point2D center, + Eigen::Matrix err, double factor) { if (!std::isfinite(center.getX()) || !std::isfinite(center.getY())) { - return Eigen::Matrix2f::Constant(NAN); + return Eigen::Matrix::Constant(NAN); } // Get the derivative of the pixel-to-sky transformation, then use it to // propagate the centroid uncertainty to coordinate uncertainty. Note that @@ -109,9 +110,9 @@ Eigen::Matrix2f calculateCoordCovariance(geom::SkyWcs const &wcs, lsst::geom::Po auto measurementToLocalGnomonic = wcs.getTransform()->then(*localGnomonicWcs->getTransform()->inverted()); Eigen::Matrix2d localMatrix = measurementToLocalGnomonic->getJacobian(center); - Eigen::Matrix2f d = localMatrix.cast() * scale * (lsst::geom::PI / 180.0); + Eigen::Matrix d = localMatrix.cast() * scale * factor; - Eigen::Matrix2f skyCov = d * err * d.transpose(); + Eigen::Matrix skyCov = d * err * d.transpose(); // Because the transform from pixels to RA/Dec was done at a local // gnomonic, the RA and Dec covariance are already commensurate, and // multiplying the RA covariance by cos(Dec) is not necessary. @@ -137,7 +138,7 @@ void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList, b pixelList.emplace_back(center); if (include_covariance) { auto err = getValue(source, centroidErrKey); - Eigen::Matrix2f skyCov = calculateCoordCovariance(wcs, center, err); + Eigen::Matrix2f skyCov = calculateCoordCovariance(wcs, center, err); skyErrList.emplace_back(skyCov); } } @@ -163,6 +164,9 @@ template void updateRefCentroids(geom::SkyWcs const &, SimpleCatalog &); template void updateSourceCoords(geom::SkyWcs const &, std::vector> &, bool include_covariance); template void updateSourceCoords(geom::SkyWcs const &, SourceCatalog &, bool include_covariance); + +template Eigen::Matrix2f calculateCoordCovariance(geom::SkyWcs const &wcs, lsst::geom::Point2D center, Eigen::Matrix2f err, double factor); +template Eigen::Matrix2d calculateCoordCovariance(geom::SkyWcs const &wcs, lsst::geom::Point2D center, Eigen::Matrix2d err, double factor); /// @endcond } // namespace table From 02380580fafc7e1fff0fb7fd42bf76c2ba450367 Mon Sep 17 00:00:00 2001 From: Dan Taranu Date: Wed, 28 Jan 2026 11:09:53 -0800 Subject: [PATCH 3/3] Add convertCentroid --- include/lsst/afw/table/wcsUtils.h | 23 +++++++++++++++++++++++ python/lsst/afw/table/_wcsUtils.cc | 9 +++++++++ src/table/wcsUtils.cc | 26 ++++++++++++++++++++++++++ tests/test_tableUtils.py | 20 +++++++++++++++++++- 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/include/lsst/afw/table/wcsUtils.h b/include/lsst/afw/table/wcsUtils.h index f818483ae..f2df980f2 100644 --- a/include/lsst/afw/table/wcsUtils.h +++ b/include/lsst/afw/table/wcsUtils.h @@ -82,6 +82,29 @@ Eigen::Matrix calculateCoordCovariance( double factor=lsst::geom::PI/180.0 ); +/** + * Convert an x/y centroid with errors into RA/dec. + * + * @param[in] wcs WCS to map from pixels to sky. + * @param[in] x The x centroid in pixels. + * @param[in] y The y centroid in pixels. + * @param[in] xErr The standard error on the x centroid in pixels. + * @param[in] yErr The standard error on the y centroid in pixels. + * @param[in] xy_covariance The xy covariance in pixels squared. + * + * @return A pair of tuples with the RA/dec centroid in degrees, + * and the RA error, dec error and RA/dec covariance in degrees + * (squared for the covariance). + */ +std::pair, std::tuple> convertCentroid( + geom::SkyWcs const &wcs, + double x, + double y, + double xErr, + double yErr, + double xy_covariance=0 +); + } // namespace table } // namespace afw } // namespace lsst diff --git a/python/lsst/afw/table/_wcsUtils.cc b/python/lsst/afw/table/_wcsUtils.cc index 5f0ae8609..35180c2b5 100644 --- a/python/lsst/afw/table/_wcsUtils.cc +++ b/python/lsst/afw/table/_wcsUtils.cc @@ -60,9 +60,18 @@ void declareUpdateSourceCoords(WrapperCollection &wrappers) { }); } +void declareConvertCentroid(WrapperCollection &wrappers) { + wrappers.wrap([](auto &mod) { + mod.def("convertCentroid", convertCentroid, "wcs"_a, "x"_a, "y"_a, "xErr"_a, "yErr"_a, + "xy_covariance"_a=0.); + }); +} + } // namespace void wrapWcsUtils(WrapperCollection &wrappers) { + declareConvertCentroid(wrappers); + declareUpdateRefCentroids>>(wrappers); declareUpdateRefCentroids(wrappers); diff --git a/src/table/wcsUtils.cc b/src/table/wcsUtils.cc index 5aec0c966..7e3248337 100644 --- a/src/table/wcsUtils.cc +++ b/src/table/wcsUtils.cc @@ -157,6 +157,32 @@ void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList, b } } +std::pair, std::tuple> convertCentroid( + geom::SkyWcs const &wcs, + double x, + double y, + double xErr, + double yErr, + double xy_covariance +) { + lsst::geom::Point2D center{x, y}; + + auto radec = wcs.pixelToSky(center); + + Eigen::Matrix2d err; + err(0, 0) = xErr*xErr; + err(1, 1) = yErr*yErr; + err(0, 1) = xy_covariance; + err(1, 0) = xy_covariance; + + Eigen::Matrix2d result = calculateCoordCovariance(wcs, center, err, 1.0); + + return { + {radec.getRa().asDegrees(), radec.getDec().asDegrees()}, + {sqrt(result(0, 0)), sqrt(result(1, 1)), result(0, 1)} + }; +} + /// @cond template void updateRefCentroids(geom::SkyWcs const &, std::vector> &); template void updateRefCentroids(geom::SkyWcs const &, SimpleCatalog &); diff --git a/tests/test_tableUtils.py b/tests/test_tableUtils.py index 94c2a78d2..96e4b3983 100644 --- a/tests/test_tableUtils.py +++ b/tests/test_tableUtils.py @@ -161,6 +161,8 @@ def testCoordErrors(self): scale = (1.0 * lsst.geom.arcseconds).asDegrees() # update the catalogs afwTable.updateSourceCoords(self.wcs, self.sourceCat) + factor = math.pi/180. + factor_sq = factor**2 for src in self.sourceCat: center = src.get(self.srcCentroidKey) skyCenter = self.wcs.pixelToSky(center) @@ -175,7 +177,23 @@ def testCoordErrors(self): centroidErr = src.get(self.srcCentroidErrKey) coordErr = radMatrix.dot(centroidErr.dot(radMatrix.T)) catCoordErr = src.get(self.srcCoordErrKey) - np.testing.assert_almost_equal(coordErr, catCoordErr) + np.testing.assert_allclose(coordErr, catCoordErr, rtol=1e-10, atol=1e-10) + + (ra, dec), (ra_err, dec_err, ra_dec_cov) = afwTable.convertCentroid( + self.wcs, + center.getX(), center.getY(), + math.sqrt(centroidErr[0, 0]), math.sqrt(centroidErr[1, 1]), centroidErr[0, 1], + ) + np.testing.assert_allclose( + (ra, dec), + (skyCenter[0].asDegrees(), skyCenter[1].asDegrees()), + rtol=1e-10, atol=1e-10, + ) + np.testing.assert_allclose( + ((ra_err*factor)**2, (dec_err*factor)**2, ra_dec_cov*factor_sq), + (catCoordErr[0, 0], catCoordErr[1, 1], catCoordErr[0, 1]), + rtol=1e-10, atol=1e-10, + ) def checkCatalogs(self, maxPixDiff=1e-5, maxSkyDiff=0.001*lsst.geom.arcseconds): """Check that the source and reference object catalogs have equal centroids and coords"""