lsst.jointcal g84d6eb7eb7+1652af9b63
cameraGeometry.py
Go to the documentation of this file.
1# This file is part of jointcal.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22"""Code to convert jointcal's output WCS models to distortion maps that can be
23used by afw CameraGeom.
24"""
25import numpy as np
26
27from lsst.afw import cameraGeom
28import lsst.afw.geom
29import astshim as ast
30import lsst.log
31from lsst.geom import SpherePoint, Point2D, radians
32
33_LOG = lsst.log.Log.getLogger(__name__)
34
35
37 """Convert a jointcal `~lsst.afw.geom.SkyWcs` into a distortion model and
38 detector positions (TODO) that can be used by `~lsst.afw.cameraGeom`.
39
40 Because this code only operates on the WCS, it is independent of the
41 format of the persisted output (e.g. gen2 separate files vs. gen3 bundled
42 visits).
43
44 Parameters
45 ----------
46 wcsList : `list` [`lsst.afw.geom.SkyWcs`]
47 The WCS to use to compute the distortion model from, preferably from
48 multiple visits on the same tract.
49 detectors : `list` [`int`]
50 Detector ids that correspond one-to-one with ``wcsList``.
52 The camera these WCS were fit for.
53 n : `int`
54 Number of points to compute the pixel scale at, along the +y axis.
55 """
56 def __init__(self, wcsList, detectors, camera, n=100):
57 self.wcsListwcsList = wcsList
58 self.cameracamera = camera
59 self.detectorsdetectors = detectors
60 self.maxFocalRadiusmaxFocalRadius = self.cameracamera.computeMaxFocalPlaneRadius()
61 self.nn = n
62 # the computed radius and pixel scales
63 self.fieldAnglefieldAngle = None # degrees
64 self.radialScaleradialScale = None # arcsec
65 self.tangentialScaletangentialScale = None # arcsec
66 # the computed values for every input wcs
67 self.fieldAnglesfieldAngles = None
68 self.radialScalesradialScales = None
69 self.tangentialScalestangentialScales = None
70 self.fieldAngleStdfieldAngleStd = None
71 self.radialScaleStdradialScaleStd = None
72 self.tangentialScaleStdtangentialScaleStd = None
73
74 self.loglog = _LOG.getChild("CameraModel")
75
77 """Calculate the afw cameraGeom distortion model to be included in an
78 on-disk camera model.
79
80 PLACEHOLDER: This may be as simple as running `computePixelScale` and
81 then doing a numpy polynomial fit to it for the cameraGeom input.
82 However, we need to check details of how that distortion model is
83 stored in a Camera. e.g.:
84 np.polyfit(self.fieldAnglefieldAngle, self.radialScaleradialScale, poly_degree)
85 """
86 raise NotImplementedError("not yet!")
87
89 """Compute the radial and tangential pixel scale by averaging over
90 multiple jointcal WCS models.
91
92 Also computes the standard deviation and logs any WCS that are
93 significant outliers.
94 The calculations are stored in the ``fieldAngle[s]``,
95 ``radialScale[s]``, and ``tangentialScale[s]`` member variables.
96 """
97 self.fieldAnglesfieldAngles = []
98 self.radialScalesradialScales = []
99 self.tangentialScalestangentialScales = []
100 for id, wcs in zip(self.detectorsdetectors, self.wcsListwcsList):
101 fieldAngle, radial, tangential = self._computeDetectorPixelScale_computeDetectorPixelScale(id, wcs)
102 self.fieldAnglesfieldAngles.append(fieldAngle)
103 self.radialScalesradialScales.append(radial)
104 self.tangentialScalestangentialScales.append(tangential)
105 # TODO: For now, don't worry about small differences in the computed
106 # field angles vs. their respective radial/tangential scales, just
107 # assume all fieldAngle positions are "close enough" and warn if not.
108 self.fieldAnglefieldAngle = np.mean(self.fieldAnglesfieldAngles, axis=0)
109 self.fieldAngleStdfieldAngleStd = np.std(self.fieldAnglesfieldAngles, axis=0)
110 if self.fieldAngleStdfieldAngleStd.max() > 1e-4:
111 self.loglog.warning("Large stddev in computed field angles between visits (max: %s degree).",
112 self.fieldAngleStdfieldAngleStd.max())
113 # import os; print(os.getpid()); import ipdb; ipdb.set_trace();
114 self.radialScaleradialScale = np.mean(self.radialScalesradialScales, axis=0)
115 self.radialScaleStdradialScaleStd = np.std(self.radialScalesradialScales, axis=0)
116 if self.radialScaleStdradialScaleStd.max() > 1e-4:
117 self.loglog.warning("Large stddev in computed radial scales between visits (max: %s arcsec).",
118 self.radialScaleStdradialScaleStd.max())
119 self.tangentialScaletangentialScale = np.mean(self.tangentialScalestangentialScales, axis=0)
120 self.tangentialScaleStdtangentialScaleStd = np.std(self.tangentialScalestangentialScales, axis=0)
121 if self.tangentialScaleStdtangentialScaleStd.max() > 1e-4:
122 self.loglog.warning("Large stddev in computed tangential scales between visits (max: %s arcsec).",
123 self.tangentialScaleStdtangentialScaleStd.max())
124
125 def computeCameraPixelScale(self, detector_id=30):
126 """Compute the radial and tangential pixel scales using the distortion
127 model supplied with the camera.
128
129 This is designed to be directly comparable with the results of
130 `~CameraModel.computePixelScale`.
131
132 Parameters
133 ----------
134 detector_id: `int`
135 Detector identifier for the detector_id to use for the calculation.
136
137 Returns
138 -------
139 fieldAngle : `numpy.ndarray`
140 Field angles in degrees.
141 radialScale : `numpy.ndarray`
142 Radial direction pixel scales in arcseconds/pixel.
143 tangentialScale : `numpy.ndarray`
144 Tangential direction pixel scales in arcseconds/pixel.
145 """
146 # Make a trivial SkyWcs to get a field angle->sky transform from.
147 iwcToSkyWcs = lsst.afw.geom.makeSkyWcs(Point2D(0, 0), SpherePoint(0, 0, radians),
148 lsst.afw.geom.makeCdMatrix(1.0 * radians, 0 * radians, True))
149 iwcToSkyMap = iwcToSkyWcs.getFrameDict().getMapping("PIXELS", "SKY")
150 skyFrame = iwcToSkyWcs.getFrameDict().getFrame("SKY")
151
152 # Extract the transforms that are defined just on the camera.
153 pixSys = self.cameracamera[detector_id].makeCameraSys(cameraGeom.PIXELS)
154 pixelsToFocal = self.cameracamera.getTransform(pixSys, cameraGeom.FOCAL_PLANE)
155 focalToField = self.cameracamera.getTransform(cameraGeom.FOCAL_PLANE, cameraGeom.FIELD_ANGLE)
156
157 # Build a SkyWcs that combines each of the above components.
158 pixelFrame = ast.Frame(2, "Domain=PIXELS")
159 focalFrame = ast.Frame(2, "Domain=FOCAL")
160 iwcFrame = ast.Frame(2, "Domain=IWC")
161 frameDict = ast.FrameDict(pixelFrame)
162 frameDict.addFrame("PIXELS", pixelsToFocal.getMapping(), focalFrame)
163 frameDict.addFrame("FOCAL", focalToField.getMapping(), iwcFrame)
164 frameDict.addFrame("IWC", iwcToSkyMap, skyFrame)
165 wcs = lsst.afw.geom.SkyWcs(frameDict)
166
167 return self._computeDetectorPixelScale_computeDetectorPixelScale(detector_id, wcs)
168
169 def _computeDetectorPixelScale(self, detector_id, wcs):
170 """Compute pixel scale in radial and tangential directions as a
171 function of field angle.
172
173 Parameters
174 ----------
175 detector_id: `int`
176 Detector identifier for the detector of this wcs.
178 Full focal-plane model to compute pixel scale on.
179
180 Returns
181 -------
182 fieldAngle : `numpy.ndarray`
183 Field angles in degrees.
184 radialScale : `numpy.ndarray`
185 Radial direction pixel scales in arcseconds/pixel.
186 tangentialScale : `numpy.ndarray`
187 Tangential direction pixel scales in arcseconds/pixel.
188
189 Notes
190 -----
191 Pixel scales are calculated from finite differences only along the +y
192 focal plane direction.
193 """
194 focalToSky = wcs.getFrameDict().getMapping('FOCAL', 'SKY')
195 mmPerPixel = self.cameracamera[detector_id].getPixelSize()
196
197 focalToPixels = wcs.getFrameDict().getMapping('FOCAL', 'PIXELS')
198 trans = wcs.getTransform() # Pixels to Sky as Point2d -> SpherePoint
199 boresight = trans.applyForward(Point2D(focalToPixels.applyForward([0, 0])))
200
201 rs = np.linspace(0, self.maxFocalRadiusmaxFocalRadius, self.nn) # focal plane units
202 fieldAngle = np.zeros_like(rs)
203 radialScale = np.zeros_like(rs)
204 tangentialScale = np.zeros_like(rs)
205 for i, r in enumerate(rs):
206 # point on the sky at position r along the focal plane +y axis
207 sp1 = SpherePoint(*focalToSky.applyForward(Point2D([0, r])), radians)
208 # point on the sky one pixel further along the focal plane +y axis
209 sp2 = SpherePoint(*focalToSky.applyForward(Point2D([0, r + mmPerPixel.getY()])), radians)
210 # point on the sky one pixel off of the focal plane +y axis at r
211 sp3 = SpherePoint(*focalToSky.applyForward(Point2D([mmPerPixel.getX(), r])), radians)
212 fieldAngle[i] = boresight.separation(sp1).asDegrees()
213 radialScale[i] = sp1.separation(sp2).asArcseconds()
214 tangentialScale[i] = sp1.separation(sp3).asArcseconds()
215 return fieldAngle, radialScale, tangentialScale
def computeCameraPixelScale(self, detector_id=30)
def __init__(self, wcsList, detectors, camera, n=100)
def _computeDetectorPixelScale(self, detector_id, wcs)
static Log getLogger(std::string const &loggername)
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
Eigen::Matrix2d makeCdMatrix(lsst::geom::Angle const &scale, lsst::geom::Angle const &orientation=0 *lsst::geom::degrees, bool flipX=false)