lsst.jointcal gf6ad1f1eeb+63b6a7d77c
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
35 """Convert a jointcal `~lsst.afw.geom.SkyWcs` into a distortion model and
36 detector positions (TODO) that can be used by `~lsst.afw.cameraGeom`.
37
38 Because this code only operates on the WCS, it is independent of the
39 format of the persisted output (e.g. gen2 separate files vs. gen3 bundled
40 visits).
41
42 Parameters
43 ----------
44 wcsList : `list` [`lsst.afw.geom.SkyWcs`]
45 The WCS to use to compute the distortion model from, preferably from
46 multiple visits on the same tract.
47 detectors : `list` [`int`]
48 Detector ids that correspond one-to-one with ``wcsList``.
50 The camera these WCS were fit for.
51 n : `int`
52 Number of points to compute the pixel scale at, along the +y axis.
53 """
54 def __init__(self, wcsList, detectors, camera, n=100):
55 self.wcsListwcsList = wcsList
56 self.cameracamera = camera
57 self.detectorsdetectors = detectors
58 self.maxFocalRadiusmaxFocalRadius = self.cameracamera.computeMaxFocalPlaneRadius()
59 self.nn = n
60 # the computed radius and pixel scales
61 self.fieldAnglefieldAngle = None # degrees
62 self.radialScaleradialScale = None # arcsec
63 self.tangentialScaletangentialScale = None # arcsec
64 # the computed values for every input wcs
65 self.fieldAnglesfieldAngles = None
66 self.radialScalesradialScales = None
67 self.tangentialScalestangentialScales = None
68 self.fieldAngleStdfieldAngleStd = None
69 self.radialScaleStdradialScaleStd = None
70 self.tangentialScaleStdtangentialScaleStd = None
71
72 self.loglog = lsst.log.Log.getLogger("jointcal.cameraGeom.CameraModel")
73
75 """Calculate the afw cameraGeom distortion model to be included in an
76 on-disk camera model.
77
78
79 PLACEHOLDER: This may be as simple as running `computePixelScale` and
80 then doing a numpy polynomial fit to it for the cameraGeom input.
81 However, we need to check details of how that distortion model is
82 stored in a Camera.
83 e.g.: np.polyfit(self.fieldAnglefieldAngle, self.radialScaleradialScale, poly_degree))
84 """
85 raise NotImplementedError("not yet!")
86
88 """Compute the radial and tangential pixel scale by averaging over
89 multiple jointcal WCS models.
90
91 Also computes the standard deviation and logs any WCS that are
92 significant outliers.
93 The calculations are stored in the ``fieldAngle[s]``,
94 ``radialScale[s]``, and ``tangentialScale[s]`` member variables.
95 """
96 self.fieldAnglesfieldAngles = []
97 self.radialScalesradialScales = []
98 self.tangentialScalestangentialScales = []
99 for id, wcs in zip(self.detectorsdetectors, self.wcsListwcsList):
100 fieldAngle, radial, tangential = self._computeDetectorPixelScale_computeDetectorPixelScale(id, wcs)
101 self.fieldAnglesfieldAngles.append(fieldAngle)
102 self.radialScalesradialScales.append(radial)
103 self.tangentialScalestangentialScales.append(tangential)
104 # TODO: For now, don't worry about small differences in the computed
105 # field angles vs. their respective radial/tangential scales, just
106 # assume all fieldAngle positions are "close enough" and warn if not.
107 self.fieldAnglefieldAngle = np.mean(self.fieldAnglesfieldAngles, axis=0)
108 self.fieldAngleStdfieldAngleStd = np.std(self.fieldAnglesfieldAngles, axis=0)
109 if self.fieldAngleStdfieldAngleStd.max() > 1e-4:
110 self.loglog.warn("Large stddev in computed field angles between visits (max: %s degree).",
111 self.fieldAngleStdfieldAngleStd.max())
112 # import os; print(os.getpid()); import ipdb; ipdb.set_trace();
113 self.radialScaleradialScale = np.mean(self.radialScalesradialScales, axis=0)
114 self.radialScaleStdradialScaleStd = np.std(self.radialScalesradialScales, axis=0)
115 if self.radialScaleStdradialScaleStd.max() > 1e-4:
116 self.loglog.warn("Large stddev in computed radial scales between visits (max: %s arcsec).",
117 self.radialScaleStdradialScaleStd.max())
118 self.tangentialScaletangentialScale = np.mean(self.tangentialScalestangentialScales, axis=0)
119 self.tangentialScaleStdtangentialScaleStd = np.std(self.tangentialScalestangentialScales, axis=0)
120 if self.tangentialScaleStdtangentialScaleStd.max() > 1e-4:
121 self.loglog.warn("Large stddev in computed tangential scales between visits (max: %s arcsec).",
122 self.tangentialScaleStdtangentialScaleStd.max())
123
124 def computeCameraPixelScale(self, detector_id=30):
125 """Compute the radial and tangential pixel scales using the distortion
126 model supplied with the camera.
127
128 This is designed to be directly comparable with the results of
129 `~CameraModel.computePixelScale`.
130
131 Parameters
132 ----------
133 detector_id: `int`
134 Detector identifier for the detector_id to use for the calculation.
135
136 Returns
137 -------
138 fieldAngle : `numpy.ndarray`
139 Field angles in degrees.
140 radialScale : `numpy.ndarray`
141 Radial direction pixel scales in arcseconds/pixel.
142 tangentialScale : `numpy.ndarray`
143 Tangential direction pixel scales in arcseconds/pixel.
144 """
145 # Make a trivial SkyWcs to get a field angle->sky transform from.
146 iwcToSkyWcs = lsst.afw.geom.makeSkyWcs(Point2D(0, 0), SpherePoint(0, 0, radians),
147 lsst.afw.geom.makeCdMatrix(1.0 * radians, 0 * radians, True))
148 iwcToSkyMap = iwcToSkyWcs.getFrameDict().getMapping("PIXELS", "SKY")
149 skyFrame = iwcToSkyWcs.getFrameDict().getFrame("SKY")
150
151 # Extract the transforms that are defined just on the camera.
152 pixSys = self.cameracamera[detector_id].makeCameraSys(cameraGeom.PIXELS)
153 pixelsToFocal = self.cameracamera.getTransform(pixSys, cameraGeom.FOCAL_PLANE)
154 focalToField = self.cameracamera.getTransform(cameraGeom.FOCAL_PLANE, cameraGeom.FIELD_ANGLE)
155
156 # Build a SkyWcs that combines each of the above components.
157 pixelFrame = ast.Frame(2, "Domain=PIXELS")
158 focalFrame = ast.Frame(2, "Domain=FOCAL")
159 iwcFrame = ast.Frame(2, "Domain=IWC")
160 frameDict = ast.FrameDict(pixelFrame)
161 frameDict.addFrame("PIXELS", pixelsToFocal.getMapping(), focalFrame)
162 frameDict.addFrame("FOCAL", focalToField.getMapping(), iwcFrame)
163 frameDict.addFrame("IWC", iwcToSkyMap, skyFrame)
164 wcs = lsst.afw.geom.SkyWcs(frameDict)
165
166 return self._computeDetectorPixelScale_computeDetectorPixelScale(detector_id, wcs)
167
168 def _computeDetectorPixelScale(self, detector_id, wcs):
169 """Compute pixel scale in radial and tangential directions as a
170 function of field angle.
171
172 Parameters
173 ----------
174 detector_id: `int`
175 Detector identifier for the detector of this wcs.
177 Full focal-plane model to compute pixel scale on.
178
179 Returns
180 -------
181 fieldAngle : `numpy.ndarray`
182 Field angles in degrees.
183 radialScale : `numpy.ndarray`
184 Radial direction pixel scales in arcseconds/pixel.
185 tangentialScale : `numpy.ndarray`
186 Tangential direction pixel scales in arcseconds/pixel.
187
188 Notes
189 -----
190 Pixel scales are calculated from finite differences only along the +y
191 focal plane direction.
192 """
193 focalToSky = wcs.getFrameDict().getMapping('FOCAL', 'SKY')
194 mmPerPixel = self.cameracamera[detector_id].getPixelSize()
195
196 focalToPixels = wcs.getFrameDict().getMapping('FOCAL', 'PIXELS')
197 trans = wcs.getTransform() # Pixels to Sky as Point2d -> SpherePoint
198 boresight = trans.applyForward(Point2D(focalToPixels.applyForward([0, 0])))
199
200 rs = np.linspace(0, self.maxFocalRadiusmaxFocalRadius, self.nn) # focal plane units
201 fieldAngle = np.zeros_like(rs)
202 radialScale = np.zeros_like(rs)
203 tangentialScale = np.zeros_like(rs)
204 for i, r in enumerate(rs):
205 # point on the sky at position r along the focal plane +y axis
206 sp1 = SpherePoint(*focalToSky.applyForward(Point2D([0, r])), radians)
207 # point on the sky one pixel further along the focal plane +y axis
208 sp2 = SpherePoint(*focalToSky.applyForward(Point2D([0, r + mmPerPixel.getY()])), radians)
209 # point on the sky one pixel off of the focal plane +y axis at r
210 sp3 = SpherePoint(*focalToSky.applyForward(Point2D([mmPerPixel.getX(), r])), radians)
211 fieldAngle[i] = boresight.separation(sp1).asDegrees()
212 radialScale[i] = sp1.separation(sp2).asArcseconds()
213 tangentialScale[i] = sp1.separation(sp3).asArcseconds()
214 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)