diff --git a/README.md b/README.md
index 7eb35490..4d53bb42 100644
--- a/README.md
+++ b/README.md
@@ -123,6 +123,23 @@ setup -k -r .
1. While testing with the sky images obtained with the auxiliary telescope (localed in `tests/testData/testImages/auxTel`), this package and [cwfs](https://github.com/bxin/cwfs) show the similar result in "onAxis" optical model. However, for the "paraxial" optical model, the results of two packages are different.
2. The main difference comes from the stratege of compensation in the initial loop of solving the TIE. However, it is hard to have a conclusion at this moment because of the low singal-to-noise ratio in test images.
+## Test Gen 3 Repository
+
+In the folder `tests/testData/` there is a test repository for tasks that run with the Gen 3 DM middleware.
+This repository is the folder `tests/testData/gen3TestRepo` and was built using the files `tests/testData/createGen3TestRepo.sh`.
+This script performs the following steps to create the Gen 3 repository:
+
+1. Ingest a reference catalog with the Gen 2 middleware.
+This reference catalog is the file `tests/testData/phosimOutput/realComCam/skyComCamInfoRefCatalog.txt` which contains the same information as `skyComCamInfo.txt` in the same folder but formatted to be read in by the Gen 2 middleware as a reference catalog.
+A column of g magnitude error is added with a value of 0.1 to fit reference catalog format.
+The format of the reference catalog is configured with the file `tests/testData/gen3TestRepo/refCat.cfg`.
+2. Convert the Gen 2 repository with the reference catalog into a Gen 3 repository since this is currently the only way to create a Gen 3 reference catalog.
+This requires the configuration file `tests/testData/gen3TestRepo/convertRefCat.cfg`.
+3. Ingest the raw files in `tests/testData/phosimOutput/realComCam/repackagedFiles`.
+4. Clean up the original Gen 2 repository.
+
+Inside the Gen 3 repository there are files for the Gen 3 Butler configuration (`butler.yaml`) and the repository database (`gen3.sqlite3`).
+
## Build the Document
To build project documentation, run `package-docs build` to build the documentation.
diff --git a/doc/content.rst b/doc/content.rst
index 08d5d9cd..85ca6405 100644
--- a/doc/content.rst
+++ b/doc/content.rst
@@ -128,3 +128,18 @@ This module does the image deblending.
* **DeblendDefault**: Default deblend class.
* **DeblendAdapt**: DeblendDefault child class to do the deblending by the adaptive threshold method.
* **nelderMeadModify**: Do the numerical optimation according to the Nelder-Mead algorithm.
+
+.. _lsst.ts.wep-modules_wep_task:
+
+-------------
+wep.task
+-------------
+
+This module has the tasks to run WEP as a pipeline with Gen 3 LSST DM middleware.
+
+.. uml:: uml/taskClass.uml
+ :caption: Class diagram of wep.task
+
+* **GenerateDonutCatalogOnlineTaskConnections**: Connections setup for GenerateDonutCatalogOnlineTask to run in a pipeline with Gen 3 middleware.
+* **GenerateDonutCatalogOnlineTaskConfig**: Configuration setup for GenerateDonutCatalogOnlineTask.
+* **GenerateDonutCatalogOnlineTask**: Gen 3 middleware task to take pointing information and create a catalog of donut sources in that pointing.
diff --git a/doc/uml/taskClass.uml b/doc/uml/taskClass.uml
new file mode 100644
index 00000000..d9ae0828
--- /dev/null
+++ b/doc/uml/taskClass.uml
@@ -0,0 +1,4 @@
+@startuml
+GenerateDonutCatalogOnlineTaskConfig *-- GenerateDonutCatalogOnlineTaskConnections
+GenerateDonutCatalogOnlineTask *-- GenerateDonutCatalogOnlineTaskConfig
+@enduml
diff --git a/doc/versionHistory.rst b/doc/versionHistory.rst
index c0cb28de..45afd1f0 100644
--- a/doc/versionHistory.rst
+++ b/doc/versionHistory.rst
@@ -6,6 +6,16 @@
Version History
##################
+.. _lsst.ts.wep-1.6.0:
+
+-------------
+1.6.0
+-------------
+
+* Create new task module
+* Add GenerateDonutCatalogOnlineTask.py in task module
+* Add `tests/testData/gen3TestRepo` as sample Gen 3 repo for testing
+
.. _lsst.ts.wep-1.5.9:
-------------
diff --git a/python/lsst/ts/wep/cwfs/DonutTemplatePhosim.py b/python/lsst/ts/wep/cwfs/DonutTemplatePhosim.py
index a5231041..aa675ef1 100644
--- a/python/lsst/ts/wep/cwfs/DonutTemplatePhosim.py
+++ b/python/lsst/ts/wep/cwfs/DonutTemplatePhosim.py
@@ -69,7 +69,7 @@ def makeTemplate(self, sensorName, defocalType, imageSize):
templateFilename = os.path.join(
phosimTemplateDir, "intra_template-%s.txt" % sensorName
)
- templateArray = np.genfromtxt(templateFilename, dtype=np.int)
+ templateArray = np.genfromtxt(templateFilename, dtype=int)
# Make the template the desired square shape by trimming edges of
# template
@@ -83,8 +83,8 @@ def makeTemplate(self, sensorName, defocalType, imageSize):
# Find the left and right edges by trimming half pixels from
# left and right.
# Do the same for the top and bottom.
- leftEdge = topEdge = np.int(templateTrim / 2)
- rightEdge = bottomEdge = np.int(templateSize - (templateTrim - leftEdge))
+ leftEdge = topEdge = int(templateTrim / 2)
+ rightEdge = bottomEdge = int(templateSize - (templateTrim - leftEdge))
templateFinal = templateArray[leftEdge:rightEdge, topEdge:bottomEdge]
else:
@@ -93,8 +93,8 @@ def makeTemplate(self, sensorName, defocalType, imageSize):
templatePad = imageSize - templateSize
# Pad each side by equal amount of pixels
- padLeft = padTop = np.int(templatePad / 2)
- padRight = padBottom = np.int(imageSize - (templatePad - padLeft))
+ padLeft = padTop = int(templatePad / 2)
+ padRight = padBottom = int(imageSize - (templatePad - padLeft))
templateFinal = np.zeros((imageSize, imageSize))
templateFinal[padLeft:padRight, padTop:padBottom] = templateArray
diff --git a/python/lsst/ts/wep/task/GenerateDonutCatalogOnlineTask.py b/python/lsst/ts/wep/task/GenerateDonutCatalogOnlineTask.py
new file mode 100644
index 00000000..c6464321
--- /dev/null
+++ b/python/lsst/ts/wep/task/GenerateDonutCatalogOnlineTask.py
@@ -0,0 +1,203 @@
+# This file is part of ts_wep.
+#
+# Developed for the LSST Telescope and Site Systems.
+# This product includes software developed by the LSST Project
+# (https://www.lsst.org).
+# See the COPYRIGHT file at the top-level directory of this distribution
+# for details of code ownership.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import typing
+import os
+import numpy as np
+import pandas as pd
+import lsst.afw.table as afwTable
+import lsst.pex.config as pexConfig
+import lsst.pipe.base as pipeBase
+import lsst.pipe.base.connectionTypes as connectionTypes
+import lsst.obs.lsst as obs_lsst
+from lsst.meas.algorithms import ReferenceObjectLoader, LoadReferenceObjectsConfig
+from lsst.ts.wep.bsc.WcsSol import WcsSol
+
+
+class GenerateDonutCatalogOnlineTaskConnections(
+ pipeBase.PipelineTaskConnections, dimensions=("instrument",)
+):
+ """
+ Specify the connections needed for GenerateDonutCatalogOnlineTask.
+ We need the reference catalogs and will produce donut catalogs for
+ a specified instrument.
+ """
+
+ refCatalogs = connectionTypes.PrerequisiteInput(
+ doc="Reference catalog",
+ storageClass="SimpleCatalog",
+ dimensions=("htm7",),
+ multiple=True,
+ deferLoad=True,
+ name="cal_ref_cat",
+ )
+ donutCatalog = connectionTypes.Output(
+ doc="Donut Locations",
+ dimensions=("instrument",),
+ storageClass="DataFrame",
+ name="donutCatalog",
+ )
+
+
+class GenerateDonutCatalogOnlineTaskConfig(
+ pipeBase.PipelineTaskConfig,
+ pipelineConnections=GenerateDonutCatalogOnlineTaskConnections,
+):
+ """
+ Configuration settings for GenerateDonutCatalogOnlineTask. Specifies
+ pointing information, filter and camera details.
+ """
+
+ filterName = pexConfig.Field(doc="Reference filter", dtype=str, default="g")
+ boresightRa = pexConfig.Field(
+ doc="Boresight RA in degrees", dtype=float, default=0.0
+ )
+ boresightDec = pexConfig.Field(
+ doc="Boresight Dec in degrees", dtype=float, default=0.0
+ )
+ boresightRotAng = pexConfig.Field(
+ doc="Boresight Rotation Angle in degrees", dtype=float, default=0.0
+ )
+ cameraName = pexConfig.Field(doc="Camera Name", dtype=str, default="lsstCam")
+
+
+class GenerateDonutCatalogOnlineTask(pipeBase.PipelineTask):
+ """
+ Create a WCS from boresight info and then use this
+ with a reference catalog to select sources on the detectors for AOS.
+ """
+
+ ConfigClass = GenerateDonutCatalogOnlineTaskConfig
+ _DefaultName = "generateDonutCatalogOnlineTask"
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+
+ # The filter in the reference catalog we want to use to find sources.
+ self.filterName = self.config.filterName
+
+ # Pointing information to construct the WCS. All values in degrees.
+ self.boresightRa = self.config.boresightRa
+ self.boresightDec = self.config.boresightDec
+ self.boresightRotAng = self.config.boresightRotAng
+
+ # Need camera name to get the detectors to use to find X,Y pixel
+ # values for the sources
+ self.cameraName = self.config.cameraName
+
+ # TODO: Temporary until DM-24162 is closed at which point we
+ # can remove this
+ os.environ["NUMEXPR_MAX_THREADS"] = "1"
+
+ def filterResults(self, resultsDataFrame):
+ """
+ Run filtering on full set of sources on detector and return
+ the dataframe with only sources that are acceptable for
+ wavefront estimation.
+
+ Parameters
+ ----------
+ resultsDataFrame: pandas DataFrame
+ Full list of sources from reference catalog that appear
+ on the detector.
+
+ Returns
+ -------
+ pandas DataFrame
+ Subset of resultsDataFrame sources that pass required filtering.
+ """
+
+ # TODO: Here is where we will set up specifications for the sources
+ # we want to use (i.e., filter on magnitude, blended, etc.).
+ # For now it just returns all sources.
+
+ return resultsDataFrame
+
+ def run(
+ self,
+ refCatalogs: typing.List[afwTable.SimpleCatalog],
+ ) -> pipeBase.Struct:
+
+ refObjLoader = ReferenceObjectLoader(
+ dataIds=[ref.dataId for ref in refCatalogs],
+ refCats=refCatalogs,
+ config=LoadReferenceObjectsConfig(),
+ )
+ # This removes the padding around the border of detector BBox when
+ # matching to reference catalog.
+ # We remove this since we only want sources within detector.
+ refObjLoader.config.pixelMargin = 0
+
+ # Set up pandas dataframe
+ fieldObjects = pd.DataFrame([])
+ ra = []
+ dec = []
+ centroidX = []
+ centroidY = []
+ det_names = []
+
+ # Get camera. Only 'lsstCam' for now.
+ if self.cameraName == "lsstCam":
+ camera = obs_lsst.LsstCam.getCamera()
+ else:
+ raise ValueError(f"{self.cameraName} is not a valid camera name.")
+
+ # Create WCS holder
+ detWcs = WcsSol(camera=camera)
+
+ for detector in camera:
+ # Set WCS from boresight information
+ detWcs.setObsMetaData(
+ self.boresightRa,
+ self.boresightDec,
+ self.boresightRotAng,
+ centerCcd=detector.getName(),
+ )
+
+ try:
+ # Match detector layout to reference catalog
+ donutCatalog = refObjLoader.loadPixelBox(
+ detector.getBBox(), detWcs.skyWcs, filterName=self.filterName
+ ).refCat
+
+ # Add matched information to list
+ ra.append(donutCatalog["coord_ra"])
+ dec.append(donutCatalog["coord_dec"])
+ centroidX.append(donutCatalog["centroid_x"])
+ centroidY.append(donutCatalog["centroid_y"])
+ det_names.append([detector.getName()] * len(donutCatalog))
+
+ except RuntimeError:
+ continue
+
+ # Flatten information from all detector lists and enter into dataframe
+ fieldObjects["coord_ra"] = np.hstack(ra).squeeze()
+ fieldObjects["coord_dec"] = np.hstack(dec).squeeze()
+ fieldObjects["centroid_x"] = np.hstack(centroidX).squeeze()
+ fieldObjects["centroid_y"] = np.hstack(centroidY).squeeze()
+ fieldObjects["detector"] = np.hstack(det_names).squeeze()
+
+ # Return pandas DataFrame with sources in pointing
+ # with ra, dec, filter flux, pixel XY information and detector name
+ # for each source
+ finalSources = self.filterResults(fieldObjects)
+
+ return pipeBase.Struct(donutCatalog=finalSources)
diff --git a/tests/task/test_generateDonutCatalogOnlineTask.py b/tests/task/test_generateDonutCatalogOnlineTask.py
new file mode 100644
index 00000000..04d3f680
--- /dev/null
+++ b/tests/task/test_generateDonutCatalogOnlineTask.py
@@ -0,0 +1,125 @@
+# This file is part of ts_wep.
+#
+# Developed for the LSST Telescope and Site Systems.
+# This product includes software developed by the LSST Project
+# (https://www.lsst.org).
+# See the COPYRIGHT file at the top-level directory of this distribution
+# for details of code ownership.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import os
+import unittest
+import numpy as np
+
+from lsst.daf import butler as dafButler
+from lsst.ts.wep.Utility import getModulePath
+from lsst.ts.wep.task.GenerateDonutCatalogOnlineTask import (
+ GenerateDonutCatalogOnlineTask,
+ GenerateDonutCatalogOnlineTaskConfig,
+)
+
+
+class TestGenerateDonutCatalogOnlineTask(unittest.TestCase):
+ def setUp(self):
+
+ self.config = GenerateDonutCatalogOnlineTaskConfig()
+ self.task = GenerateDonutCatalogOnlineTask(config=self.config)
+
+ moduleDir = getModulePath()
+ self.testDataDir = os.path.join(moduleDir, "tests", "testData")
+ self.repoDir = os.path.join(self.testDataDir, "gen3TestRepo")
+
+ self.butler = dafButler.Butler(self.repoDir)
+ self.registry = self.butler.registry
+
+ def validateConfigs(self):
+
+ self.config.boresightRa = 0.03
+ self.config.boresightDec = -0.02
+ self.config.boresightRotAng = 90.0
+ self.config.filterName = "r"
+ self.cameraName = "lsstFamCam"
+ self.task = GenerateDonutCatalogOnlineTask(config=self.config)
+
+ self.assertEqual(self.task.boresightRa, 0.03)
+ self.assertEqual(self.task.boresightDec, -0.02)
+ self.assertEqual(self.task.boresightRotAng, 90.0)
+ self.assertEqual(self.task.filterName, "r")
+ self.assertEqual(self.task.cameraName, "lsstFamCam")
+
+ def testFilterResults(self):
+
+ refCat = self.butler.get(
+ "cal_ref_cat", dataId={"htm7": 253952}, collections=["refcats"]
+ )
+ testDataFrame = refCat.asAstropy().to_pandas()
+ filteredDataFrame = self.task.filterResults(testDataFrame)
+ np.testing.assert_array_equal(filteredDataFrame, testDataFrame)
+
+ def testDonutCatalogGeneration(self):
+ """
+ Test that task creates a dataframe with detector information.
+ """
+
+ # Create list of deferred loaders for the ref cat
+ deferredList = []
+ datasetGenerator = self.registry.queryDatasets(
+ datasetType="cal_ref_cat", collections=["refcats"]
+ ).expanded()
+ for ref in datasetGenerator:
+ deferredList.append(self.butler.getDeferred(ref, collections=["refcats"]))
+ taskOutput = self.task.run(deferredList)
+ outputDf = taskOutput.donutCatalog
+
+ # Compare ra, dec info to original input catalog
+ inputCat = np.genfromtxt(
+ os.path.join(
+ self.testDataDir, "phosimOutput", "realComCam", "skyComCamInfo.txt"
+ ),
+ names=["id", "ra", "dec", "mag"],
+ )
+
+ # Check that all 8 sources are present and 4 assigned to each detector
+ self.assertEqual(len(outputDf), 8)
+ self.assertCountEqual(np.radians(inputCat["ra"]), outputDf["coord_ra"])
+ self.assertCountEqual(np.radians(inputCat["dec"]), outputDf["coord_dec"])
+ self.assertEqual(len(outputDf.query('detector == "R22_S11"')), 4)
+ self.assertEqual(len(outputDf.query('detector == "R22_S10"')), 4)
+ self.assertCountEqual(
+ [
+ 3806.7636478057957,
+ 2806.982895217227,
+ 607.3861483168994,
+ 707.3972344551466,
+ 614.607342274194,
+ 714.6336433247832,
+ 3815.2649173460436,
+ 2815.0561553920156,
+ ],
+ outputDf["centroid_x"],
+ )
+ self.assertCountEqual(
+ [
+ 3196.070534224157,
+ 2195.666002294077,
+ 394.8907003737886,
+ 394.9087004171349,
+ 396.2407036464963,
+ 396.22270360324296,
+ 3196.1965343932648,
+ 2196.188002312585,
+ ],
+ outputDf["centroid_y"],
+ )
diff --git a/tests/testData/createGen3TestRepo.sh b/tests/testData/createGen3TestRepo.sh
new file mode 100644
index 00000000..d9c11382
--- /dev/null
+++ b/tests/testData/createGen3TestRepo.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# Make Gen 2 Repo for reference catalog
+mkdir gen2TestRepo
+echo "lsst.obs.lsst.LsstCamMapper" > gen2TestRepo/_mapper
+ingestReferenceCatalog.py gen2TestRepo phosimOutput/realComCam/skyComCamInfoRefCatalog.txt --configfile gen3TestRepo/refCat.cfg
+
+# Convert to Gen 3 (currently only way to get Gen3 ref cat)
+butler convert --gen2root gen2TestRepo --config-file gen3TestRepo/convertRefCat.cfg gen3TestRepo
+
+# Ingest Raws
+butler ingest-raws -t relsymlink gen3TestRepo phosimOutput/realComCam/repackagedFiles/extra/*.fits
+butler ingest-raws -t relsymlink gen3TestRepo phosimOutput/realComCam/repackagedFiles/intra/*.fits
+butler define-visits gen3TestRepo lsst.obs.lsst.LsstCam
+
+# Clean Up
+rm -r gen2TestRepo
diff --git a/tests/testData/gen3TestRepo/LSSTCam/calib/unbounded/camera/camera_LSSTCam_LSSTCam_calib_unbounded.fits b/tests/testData/gen3TestRepo/LSSTCam/calib/unbounded/camera/camera_LSSTCam_LSSTCam_calib_unbounded.fits
new file mode 100644
index 00000000..e3f237bb
--- /dev/null
+++ b/tests/testData/gen3TestRepo/LSSTCam/calib/unbounded/camera/camera_LSSTCam_LSSTCam_calib_unbounded.fits
@@ -0,0 +1,27409 @@
+SIMPLE = T / file does conform to FITS standard BITPIX = 16 / number of bits per data pixel NAXIS = 0 / number of data axes EXTEND = T / FITS dataset may contain extensions COMMENT FITS (Flexible Image Transport System) format is defined in 'AstronomyCOMMENT and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H END XTENSION= 'BINTABLE' / binary table extension BITPIX = 8 / 8-bit bytes NAXIS = 2 / 2-dimensional binary table NAXIS1 = 148 / width of table in bytes NAXIS2 = 824 / number of rows in table PCOUNT = 0 / size of special data area GCOUNT = 1 / one data group (required keyword) TFIELDS = 7 / number of fields in each row TTYPE1 = 'id ' / Archive ID of the persistable object that owns TFORM1 = '1J ' / format of field TDOC1 = 'Archive ID of the persistable object that owns the records pointed &'CONTINUE 'at by this entry' TCCLS1 = 'Scalar ' / Field template used by lsst.afw.table TTYPE2 = 'cat.archive' / index of the catalog this entry points to, fromTFORM2 = '1J ' / format of field TDOC2 = 'index of the catalog this entry points to, from the perspective of &'CONTINUE 'the archive' TCCLS2 = 'Scalar ' / Field template used by lsst.afw.table TTYPE3 = 'cat.persistable' / index of the catalog this entry points to, fromTFORM3 = '1J ' / format of field TDOC3 = 'index of the catalog this entry points to, from the perspective of &'CONTINUE 'the Persistable' TCCLS3 = 'Scalar ' / Field template used by lsst.afw.table TTYPE4 = 'row0 ' / first row used by the persistable object in thiTFORM4 = '1J ' / format of field TDOC4 = 'first row used by the persistable object in this catalog' TCCLS4 = 'Scalar ' / Field template used by lsst.afw.table TTYPE5 = 'nrows ' / number of rows used by the persistable object iTFORM5 = '1J ' / format of field TDOC5 = 'number of rows used by the persistable object in this catalog' TCCLS5 = 'Scalar ' / Field template used by lsst.afw.table TTYPE6 = 'name ' / unique name for the persistable object's class TFORM6 = '64A ' / format of field TDOC6 = 'unique name for the persistable object''s class' TCCLS6 = 'String ' / Field template used by lsst.afw.table TTYPE7 = 'module ' / Python module that should be imported to registTFORM7 = '64A ' / format of field TDOC7 = 'Python module that should be imported to register the object''s fac&'CONTINUE 'tory ' TCCLS7 = 'String ' / Field template used by lsst.afw.table EXTTYPE = 'ARCHIVE_INDEX' AR_CATN = 0 / # of this catalog relative to the start of thisAR_NCAT = 7 / # of catalogs in this archive, including the inHIERARCH AFW_TABLE_VERSION = 3 END TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom
+ TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom
TransformPoint2ToPoint2 lsst.afw.geom
+ TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom
TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom TransformPoint2ToPoint2 lsst.afw.geom ! TransformPoint2ToPoint2 lsst.afw.geom " TransformPoint2ToPoint2 lsst.afw.geom # TransformPoint2ToPoint2 lsst.afw.geom $ TransformPoint2ToPoint2 lsst.afw.geom % ! TransformPoint2ToPoint2 lsst.afw.geom &