LSST Applications  21.0.0-147-g0e635eb1+1acddb5be5,22.0.0+052faf71bd,22.0.0+1ea9a8b2b2,22.0.0+6312710a6c,22.0.0+729191ecac,22.0.0+7589c3a021,22.0.0+9f079a9461,22.0.1-1-g7d6de66+b8044ec9de,22.0.1-1-g87000a6+536b1ee016,22.0.1-1-g8e32f31+6312710a6c,22.0.1-10-gd060f87+016f7cdc03,22.0.1-12-g9c3108e+df145f6f68,22.0.1-16-g314fa6d+c825727ab8,22.0.1-19-g93a5c75+d23f2fb6d8,22.0.1-19-gb93eaa13+aab3ef7709,22.0.1-2-g8ef0a89+b8044ec9de,22.0.1-2-g92698f7+9f079a9461,22.0.1-2-ga9b0f51+052faf71bd,22.0.1-2-gac51dbf+052faf71bd,22.0.1-2-gb66926d+6312710a6c,22.0.1-2-gcb770ba+09e3807989,22.0.1-20-g32debb5+b8044ec9de,22.0.1-23-gc2439a9a+fb0756638e,22.0.1-3-g496fd5d+09117f784f,22.0.1-3-g59f966b+1e6ba2c031,22.0.1-3-g849a1b8+f8b568069f,22.0.1-3-gaaec9c0+c5c846a8b1,22.0.1-32-g5ddfab5d3+60ce4897b0,22.0.1-4-g037fbe1+64e601228d,22.0.1-4-g8623105+b8044ec9de,22.0.1-5-g096abc9+d18c45d440,22.0.1-5-g15c806e+57f5c03693,22.0.1-7-gba73697+57f5c03693,master-g6e05de7fdc+c1283a92b8,master-g72cdda8301+729191ecac,w.2021.39
LSST Data Management Base Package
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
23 used by afw CameraGeom.
24 """
25 import numpy as np
26 
27 from lsst.afw import cameraGeom
28 import lsst.afw.geom
29 import astshim as ast
30 import lsst.log
31 from 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``.
49  camera : `lsst.afw.cameraGeom.Camera`
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.fieldAngle, self.radialScale, poly_degree))
84  """
85  raise NotImplementedError("not yet!")
86 
87  def computePixelScale(self):
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.
176  wcs : `lsst.afw.geom.SkyWcs`
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
int max
A FrameSet whose frames can be referenced by domain name.
Definition: FrameDict.h:67
Frame is used to represent a coordinate system.
Definition: Frame.h:157
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
Definition: SkyWcs.h:117
Point in an unspecified spherical coordinate system.
Definition: SpherePoint.h:57
def computeCameraPixelScale(self, detector_id=30)
def __init__(self, wcsList, detectors, camera, n=100)
def _computeDetectorPixelScale(self, detector_id, wcs)
static Log getLogger(Log const &logger)
Definition: Log.h:772
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
Construct a SkyWcs from FITS keywords.
Definition: SkyWcs.cc:521
Eigen::Matrix2d makeCdMatrix(lsst::geom::Angle const &scale, lsst::geom::Angle const &orientation=0 *lsst::geom::degrees, bool flipX=false)
Make a WCS CD matrix.
Definition: SkyWcs.cc:133
Point< double, 2 > Point2D
Definition: Point.h:324
Definition: Log.h:717