Coverage for python/lsst/jointcal/cameraGeometry.py: 17%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

82 statements  

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 

36class CameraModel: 

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``. 

51 camera : `lsst.afw.cameraGeom.Camera` 

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.wcsList = wcsList 

58 self.camera = camera 

59 self.detectors = detectors 

60 self.maxFocalRadius = self.camera.computeMaxFocalPlaneRadius() 

61 self.n = n 

62 # the computed radius and pixel scales 

63 self.fieldAngle = None # degrees 

64 self.radialScale = None # arcsec 

65 self.tangentialScale = None # arcsec 

66 # the computed values for every input wcs 

67 self.fieldAngles = None 

68 self.radialScales = None 

69 self.tangentialScales = None 

70 self.fieldAngleStd = None 

71 self.radialScaleStd = None 

72 self.tangentialScaleStd = None 

73 

74 self.log = _LOG.getChild("CameraModel") 

75 

76 def computeDistortionModel(self): 

77 """Calculate the afw cameraGeom distortion model to be included in an 

78 on-disk camera model. 

79 

80 

81 PLACEHOLDER: This may be as simple as running `computePixelScale` and 

82 then doing a numpy polynomial fit to it for the cameraGeom input. 

83 However, we need to check details of how that distortion model is 

84 stored in a Camera. 

85 e.g.: np.polyfit(self.fieldAngle, self.radialScale, poly_degree)) 

86 """ 

87 raise NotImplementedError("not yet!") 

88 

89 def computePixelScale(self): 

90 """Compute the radial and tangential pixel scale by averaging over 

91 multiple jointcal WCS models. 

92 

93 Also computes the standard deviation and logs any WCS that are 

94 significant outliers. 

95 The calculations are stored in the ``fieldAngle[s]``, 

96 ``radialScale[s]``, and ``tangentialScale[s]`` member variables. 

97 """ 

98 self.fieldAngles = [] 

99 self.radialScales = [] 

100 self.tangentialScales = [] 

101 for id, wcs in zip(self.detectors, self.wcsList): 

102 fieldAngle, radial, tangential = self._computeDetectorPixelScale(id, wcs) 

103 self.fieldAngles.append(fieldAngle) 

104 self.radialScales.append(radial) 

105 self.tangentialScales.append(tangential) 

106 # TODO: For now, don't worry about small differences in the computed 

107 # field angles vs. their respective radial/tangential scales, just 

108 # assume all fieldAngle positions are "close enough" and warn if not. 

109 self.fieldAngle = np.mean(self.fieldAngles, axis=0) 

110 self.fieldAngleStd = np.std(self.fieldAngles, axis=0) 

111 if self.fieldAngleStd.max() > 1e-4: 

112 self.log.warning("Large stddev in computed field angles between visits (max: %s degree).", 

113 self.fieldAngleStd.max()) 

114 # import os; print(os.getpid()); import ipdb; ipdb.set_trace(); 

115 self.radialScale = np.mean(self.radialScales, axis=0) 

116 self.radialScaleStd = np.std(self.radialScales, axis=0) 

117 if self.radialScaleStd.max() > 1e-4: 

118 self.log.warning("Large stddev in computed radial scales between visits (max: %s arcsec).", 

119 self.radialScaleStd.max()) 

120 self.tangentialScale = np.mean(self.tangentialScales, axis=0) 

121 self.tangentialScaleStd = np.std(self.tangentialScales, axis=0) 

122 if self.tangentialScaleStd.max() > 1e-4: 

123 self.log.warning("Large stddev in computed tangential scales between visits (max: %s arcsec).", 

124 self.tangentialScaleStd.max()) 

125 

126 def computeCameraPixelScale(self, detector_id=30): 

127 """Compute the radial and tangential pixel scales using the distortion 

128 model supplied with the camera. 

129 

130 This is designed to be directly comparable with the results of 

131 `~CameraModel.computePixelScale`. 

132 

133 Parameters 

134 ---------- 

135 detector_id: `int` 

136 Detector identifier for the detector_id to use for the calculation. 

137 

138 Returns 

139 ------- 

140 fieldAngle : `numpy.ndarray` 

141 Field angles in degrees. 

142 radialScale : `numpy.ndarray` 

143 Radial direction pixel scales in arcseconds/pixel. 

144 tangentialScale : `numpy.ndarray` 

145 Tangential direction pixel scales in arcseconds/pixel. 

146 """ 

147 # Make a trivial SkyWcs to get a field angle->sky transform from. 

148 iwcToSkyWcs = lsst.afw.geom.makeSkyWcs(Point2D(0, 0), SpherePoint(0, 0, radians), 

149 lsst.afw.geom.makeCdMatrix(1.0 * radians, 0 * radians, True)) 

150 iwcToSkyMap = iwcToSkyWcs.getFrameDict().getMapping("PIXELS", "SKY") 

151 skyFrame = iwcToSkyWcs.getFrameDict().getFrame("SKY") 

152 

153 # Extract the transforms that are defined just on the camera. 

154 pixSys = self.camera[detector_id].makeCameraSys(cameraGeom.PIXELS) 

155 pixelsToFocal = self.camera.getTransform(pixSys, cameraGeom.FOCAL_PLANE) 

156 focalToField = self.camera.getTransform(cameraGeom.FOCAL_PLANE, cameraGeom.FIELD_ANGLE) 

157 

158 # Build a SkyWcs that combines each of the above components. 

159 pixelFrame = ast.Frame(2, "Domain=PIXELS") 

160 focalFrame = ast.Frame(2, "Domain=FOCAL") 

161 iwcFrame = ast.Frame(2, "Domain=IWC") 

162 frameDict = ast.FrameDict(pixelFrame) 

163 frameDict.addFrame("PIXELS", pixelsToFocal.getMapping(), focalFrame) 

164 frameDict.addFrame("FOCAL", focalToField.getMapping(), iwcFrame) 

165 frameDict.addFrame("IWC", iwcToSkyMap, skyFrame) 

166 wcs = lsst.afw.geom.SkyWcs(frameDict) 

167 

168 return self._computeDetectorPixelScale(detector_id, wcs) 

169 

170 def _computeDetectorPixelScale(self, detector_id, wcs): 

171 """Compute pixel scale in radial and tangential directions as a 

172 function of field angle. 

173 

174 Parameters 

175 ---------- 

176 detector_id: `int` 

177 Detector identifier for the detector of this wcs. 

178 wcs : `lsst.afw.geom.SkyWcs` 

179 Full focal-plane model to compute pixel scale on. 

180 

181 Returns 

182 ------- 

183 fieldAngle : `numpy.ndarray` 

184 Field angles in degrees. 

185 radialScale : `numpy.ndarray` 

186 Radial direction pixel scales in arcseconds/pixel. 

187 tangentialScale : `numpy.ndarray` 

188 Tangential direction pixel scales in arcseconds/pixel. 

189 

190 Notes 

191 ----- 

192 Pixel scales are calculated from finite differences only along the +y 

193 focal plane direction. 

194 """ 

195 focalToSky = wcs.getFrameDict().getMapping('FOCAL', 'SKY') 

196 mmPerPixel = self.camera[detector_id].getPixelSize() 

197 

198 focalToPixels = wcs.getFrameDict().getMapping('FOCAL', 'PIXELS') 

199 trans = wcs.getTransform() # Pixels to Sky as Point2d -> SpherePoint 

200 boresight = trans.applyForward(Point2D(focalToPixels.applyForward([0, 0]))) 

201 

202 rs = np.linspace(0, self.maxFocalRadius, self.n) # focal plane units 

203 fieldAngle = np.zeros_like(rs) 

204 radialScale = np.zeros_like(rs) 

205 tangentialScale = np.zeros_like(rs) 

206 for i, r in enumerate(rs): 

207 # point on the sky at position r along the focal plane +y axis 

208 sp1 = SpherePoint(*focalToSky.applyForward(Point2D([0, r])), radians) 

209 # point on the sky one pixel further along the focal plane +y axis 

210 sp2 = SpherePoint(*focalToSky.applyForward(Point2D([0, r + mmPerPixel.getY()])), radians) 

211 # point on the sky one pixel off of the focal plane +y axis at r 

212 sp3 = SpherePoint(*focalToSky.applyForward(Point2D([mmPerPixel.getX(), r])), radians) 

213 fieldAngle[i] = boresight.separation(sp1).asDegrees() 

214 radialScale[i] = sp1.separation(sp2).asArcseconds() 

215 tangentialScale[i] = sp1.separation(sp3).asArcseconds() 

216 return fieldAngle, radialScale, tangentialScale