Hide keyboard shortcuts

Hot-keys 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

1import numpy as np 

2import os 

3import numbers 

4import palpy 

5 

6from lsst.utils import getPackageDir 

7from lsst.sims.utils import ZernikePolynomialGenerator 

8from lsst.sims.coordUtils import lsst_camera 

9from lsst.sims.coordUtils import DMtoCameraPixelTransformer 

10from lsst.afw.cameraGeom import PIXELS, FOCAL_PLANE, FIELD_ANGLE 

11from lsst.afw.cameraGeom import DetectorType 

12import lsst.geom as geom 

13from lsst.sims.utils.CodeUtilities import _validate_inputs 

14 

15 

16__all__ = ["LsstZernikeFitter"] 

17 

18 

19def _rawPupilCoordsFromObserved(ra_obs, dec_obs, ra0, dec0, rotSkyPos): 

20 """ 

21 Convert Observed RA, Dec into pupil coordinates 

22 

23 Parameters 

24 ---------- 

25 ra_obs is the observed RA in radians 

26 

27 dec_obs is the observed Dec in radians 

28 

29 ra0 is the RA of the boresite in radians 

30 

31 dec0 is the Dec of the boresite in radians 

32 

33 rotSkyPos is in radians 

34 

35 Returns 

36 -------- 

37 A numpy array whose first row is the x coordinate on the pupil in 

38 radians and whose second row is the y coordinate in radians 

39 """ 

40 

41 are_arrays = _validate_inputs([ra_obs, dec_obs], ['ra_obs', 'dec_obs'], 

42 "pupilCoordsFromObserved") 

43 

44 theta = -1.0*rotSkyPos 

45 

46 ra_pointing = ra0 

47 dec_pointing = dec0 

48 

49 # palpy.ds2tp performs the gnomonic projection on ra_in and dec_in 

50 # with a tangent point at (pointingRA, pointingDec) 

51 # 

52 if not are_arrays: 

53 try: 

54 x, y = palpy.ds2tp(ra_obs, dec_obs, ra_pointing, dec_pointing) 

55 except: 

56 x = np.NaN 

57 y = np.NaN 

58 else: 

59 try: 

60 x, y = palpy.ds2tpVector(ra_obs, dec_obs, ra_pointing, dec_pointing) 

61 except: 

62 # apparently, one of your ra/dec values was improper; we will have to do this 

63 # element-wise, putting NaN in the place of the bad values 

64 x = [] 

65 y = [] 

66 for rr, dd in zip(ra_obs, dec_obs): 

67 try: 

68 xx, yy = palpy.ds2tp(rr, dd, ra_pointing, dec_pointing) 

69 except: 

70 xx = np.NaN 

71 yy = np.NaN 

72 x.append(xx) 

73 y.append(yy) 

74 x = np.array(x) 

75 y = np.array(y) 

76 

77 # rotate the result by rotskypos (rotskypos being "the angle of the sky relative to 

78 # camera coordinates" according to phoSim documentation) to account for 

79 # the rotation of the focal plane about the telescope pointing 

80 

81 x_out = x*np.cos(theta) - y*np.sin(theta) 

82 y_out = x*np.sin(theta) + y*np.cos(theta) 

83 

84 return np.array([x_out, y_out]) 

85 

86 

87class LsstZernikeFitter(object): 

88 """ 

89 This class will fit and then apply the Zernike polynomials needed 

90 to correct the FIELD_ANGLE to FOCAL_PLANE transformation for the 

91 filter-dependent part. 

92 """ 

93 

94 def __init__(self): 

95 self._camera = lsst_camera() 

96 self._pixel_transformer = DMtoCameraPixelTransformer() 

97 self._z_gen = ZernikePolynomialGenerator() 

98 

99 self._rr = 500.0 # radius in mm of circle containing LSST focal plane; 

100 # make it a little bigger to accommodate any slop in 

101 # the conversion from focal plane coordinates back to 

102 # pupil coordinates (in case the optical distortions 

103 # cause points to cross over the actual boundary of 

104 # the focal plane) 

105 

106 self._band_to_int = {} 

107 self._band_to_int['u'] = 0 

108 self._band_to_int['g'] = 1 

109 self._band_to_int['r'] = 2 

110 self._band_to_int['i'] = 3 

111 self._band_to_int['z'] = 4 

112 self._band_to_int['y'] = 5 

113 

114 self._int_to_band = 'ugrizy' 

115 

116 self._n_grid = [] 

117 self._m_grid = [] 

118 

119 # 2018 May 8 

120 # During development, I found that there was negligible 

121 # improvement in the fit when allowing n>4, so I am 

122 # hard-coding the limit of n=4 here. 

123 for n in range(4): 

124 for m in range(-n, n+1, 2): 

125 self._n_grid.append(n) 

126 self._m_grid.append(m) 

127 

128 self._build_transformations() 

129 

130 def _get_coeffs(self, x_in, y_in, x_out, y_out): 

131 """ 

132 Get the coefficients of the best fit Zernike Polynomial 

133 expansion that transforms from x_in, y_in to x_out, y_out. 

134 

135 Returns numpy arrays of the Zernike Polynomial expansion 

136 coefficients in x and y. Zernike Polynomials correspond 

137 to the radial and angular orders stored in self._n_grid 

138 and self._m_grid. 

139 """ 

140 

141 polynomials = {} 

142 for n, m in zip(self._n_grid, self._m_grid): 

143 values = self._z_gen.evaluate_xy(x_in/self._rr, y_in/self._rr, n, m) 

144 polynomials[(n,m)] = values 

145 

146 poly_keys = list(polynomials.keys()) 

147 

148 dx = x_out - x_in 

149 dy = y_out - y_in 

150 

151 b = np.array([(dx*polynomials[k]).sum() for k in poly_keys]) 

152 m = np.array([[(polynomials[k1]*polynomials[k2]).sum() for k1 in poly_keys] 

153 for k2 in poly_keys]) 

154 

155 alpha_x_ = np.linalg.solve(m, b) 

156 alpha_x = {} 

157 for ii, kk in enumerate(poly_keys): 

158 alpha_x[kk] = alpha_x_[ii] 

159 

160 

161 b = np.array([(dy*polynomials[k]).sum() for k in poly_keys]) 

162 m = np.array([[(polynomials[k1]*polynomials[k2]).sum() for k1 in poly_keys] 

163 for k2 in poly_keys]) 

164 

165 alpha_y_ = np.linalg.solve(m, b) 

166 alpha_y = {} 

167 for ii, kk in enumerate(poly_keys): 

168 alpha_y[kk] = alpha_y_[ii] 

169 

170 return alpha_x, alpha_y 

171 

172 def _build_transformations(self): 

173 """ 

174 Solve for and store the coefficients of the Zernike 

175 polynomial expansion of the difference between the 

176 naive and the bandpass-dependent optical distortions 

177 in the LSST camera. 

178 """ 

179 catsim_dir = os.path.join(getPackageDir('sims_data'), 

180 'FocalPlaneData', 

181 'CatSimData') 

182 

183 phosim_dir = os.path.join(getPackageDir('sims_data'), 

184 'FocalPlaneData', 

185 'PhoSimData') 

186 

187 # the file which contains the input sky positions of the objects 

188 # that were given to PhoSim 

189 catsim_catalog = os.path.join(catsim_dir,'predicted_positions.txt') 

190 

191 with open(catsim_catalog, 'r') as input_file: 

192 header = input_file.readline() 

193 params = header.strip().split() 

194 ra0 = np.radians(float(params[2])) 

195 dec0 = np.radians(float(params[4])) 

196 rotSkyPos = np.radians(float(params[6])) 

197 

198 catsim_dtype = np.dtype([('id', int), ('xmm_old', float), ('ymm_old', float), 

199 ('xpup', float), ('ypup', float), 

200 ('raObs', float), ('decObs', float)]) 

201 

202 catsim_data = np.genfromtxt(catsim_catalog, dtype=catsim_dtype) 

203 

204 sorted_dex = np.argsort(catsim_data['id']) 

205 catsim_data=catsim_data[sorted_dex] 

206 

207 # convert from RA, Dec to pupil coors/FIELD_ANGLE 

208 x_field, y_field = _rawPupilCoordsFromObserved(np.radians(catsim_data['raObs']), 

209 np.radians(catsim_data['decObs']), 

210 ra0, dec0, rotSkyPos) 

211 

212 # convert from FIELD_ANGLE to FOCAL_PLANE without attempting to model 

213 # the optical distortions in the telescope 

214 field_to_focal = self._camera.getTransform(FIELD_ANGLE, FOCAL_PLANE) 

215 catsim_xmm = np.zeros(len(x_field), dtype=float) 

216 catsim_ymm = np.zeros(len(y_field), dtype=float) 

217 for ii, (xx, yy) in enumerate(zip(x_field, y_field)): 

218 focal_pt = field_to_focal.applyForward(geom.Point2D(xx, yy)) 

219 catsim_xmm[ii] = focal_pt.getX() 

220 catsim_ymm[ii] = focal_pt.getY() 

221 

222 phosim_dtype = np.dtype([('id', int), ('phot', float), 

223 ('xpix', float), ('ypix', float)]) 

224 

225 self._pupil_to_focal = {} 

226 self._focal_to_pupil = {} 

227 

228 for i_filter in range(6): 

229 self._pupil_to_focal[self._int_to_band[i_filter]] = {} 

230 self._focal_to_pupil[self._int_to_band[i_filter]] = {} 

231 phosim_xmm = np.zeros(len(catsim_data['ypup']), dtype=float) 

232 phosim_ymm = np.zeros(len(catsim_data['ypup']), dtype=float) 

233 

234 for det in self._camera: 

235 if det.getType() != DetectorType.SCIENCE: 

236 continue 

237 pixels_to_focal = det.getTransform(PIXELS, FOCAL_PLANE) 

238 det_name = det.getName() 

239 bbox = det.getBBox() 

240 det_name_m = det_name.replace(':','').replace(',','').replace(' ','_') 

241 

242 # read in the actual pixel positions of the sources as realized 

243 # by PhoSim 

244 centroid_name = 'centroid_lsst_e_2_f%d_%s_E000.txt' % (i_filter, det_name_m) 

245 full_name = os.path.join(phosim_dir, centroid_name) 

246 phosim_data = np.genfromtxt(full_name, dtype=phosim_dtype, skip_header=1) 

247 

248 # make sure that the data we are fitting to is not too close 

249 # to the edge of the detector 

250 assert phosim_data['xpix'].min() > bbox.getMinY() + 50.0 

251 assert phosim_data['xpix'].max() < bbox.getMaxY() - 50.0 

252 assert phosim_data['ypix'].min() > bbox.getMinX() + 50.0 

253 assert phosim_data['ypix'].max() < bbox.getMaxX() - 50.0 

254 

255 xpix, ypix = self._pixel_transformer.dmPixFromCameraPix(phosim_data['xpix'], 

256 phosim_data['ypix'], 

257 det_name) 

258 xmm = np.zeros(len(xpix), dtype=float) 

259 ymm = np.zeros(len(ypix), dtype=float) 

260 for ii in range(len(xpix)): 

261 focal_pt = pixels_to_focal.applyForward(geom.Point2D(xpix[ii], ypix[ii])) 

262 xmm[ii] = focal_pt.getX() 

263 ymm[ii] = focal_pt.getY() 

264 phosim_xmm[phosim_data['id']-1] = xmm 

265 phosim_ymm[phosim_data['id']-1] = ymm 

266 

267 # solve for the coefficients of the Zernike expansions 

268 # necessary to model the optical transformations and go 

269 # from the naive focal plane positions (catsim_xmm, catsim_ymm) 

270 # to the PhoSim realized focal plane positions 

271 alpha_x, alpha_y = self._get_coeffs(catsim_xmm, catsim_ymm, 

272 phosim_xmm, phosim_ymm) 

273 

274 self._pupil_to_focal[self._int_to_band[i_filter]]['x'] = alpha_x 

275 self._pupil_to_focal[self._int_to_band[i_filter]]['y'] = alpha_y 

276 

277 # solve for the coefficients to the Zernike expansions 

278 # necessary to go back from the PhoSim realized focal plane 

279 # positions to the naive CatSim predicted focal plane 

280 # positions 

281 alpha_x, alpha_y = self._get_coeffs(phosim_xmm, phosim_ymm, 

282 catsim_xmm, catsim_ymm) 

283 

284 self._focal_to_pupil[self._int_to_band[i_filter]]['x'] = alpha_x 

285 self._focal_to_pupil[self._int_to_band[i_filter]]['y'] = alpha_y 

286 

287 def _apply_transformation(self, transformation_dict, xmm, ymm, band): 

288 """ 

289 Parameters 

290 ---------- 

291 tranformation_dict -- a dict containing the coefficients 

292 of the Zernike decomposition to be applied 

293 

294 xmm -- the input x position in mm 

295 

296 ymm -- the input y position in mm 

297 

298 band -- the filter in which we are operating 

299 (can be either a string or an int; 0=u, 1=g, 2=r, etc.) 

300 

301 Returns 

302 ------- 

303 dx -- the x offset resulting from the transformation 

304 

305 dy -- the y offset resulting from the transformation 

306 """ 

307 if isinstance(band, int): 

308 band = self._int_to_band[band] 

309 

310 if isinstance(xmm, numbers.Number): 

311 dx = 0.0 

312 dy = 0.0 

313 else: 

314 dx = np.zeros(len(xmm), dtype=float) 

315 dy = np.zeros(len(ymm), dtype=float) 

316 

317 for kk in self._pupil_to_focal[band]['x']: 

318 values = self._z_gen.evaluate_xy(xmm/self._rr, ymm/self._rr, kk[0], kk[1]) 

319 dx += transformation_dict[band]['x'][kk]*values 

320 dy += transformation_dict[band]['y'][kk]*values 

321 

322 return dx, dy 

323 

324 def dxdy(self, xmm, ymm, band): 

325 """ 

326 Apply the transformation necessary when going from pupil 

327 coordinates to focal plane coordinates. 

328 

329 The recipe to correctly use this method is 

330 

331 xf0, yf0 = focalPlaneCoordsFromPupilCoords(xpupil, ypupil, 

332 camera=lsst_camera()) 

333 

334 dx, dy = LsstZernikeFitter().dxdy(xf0, yf0, band=band) 

335 

336 xf = xf0 + dx 

337 yf = yf0 + dy 

338 

339 xf and yf are now the actual position in millimeters on the 

340 LSST focal plane corresponding to xpupil, ypupil 

341 

342 Parameters 

343 ---------- 

344 xmm -- the naive x focal plane position in mm 

345 

346 ymm -- the naive y focal plane position in mm 

347 

348 band -- the filter in which we are operating 

349 (can be either a string or an int; 0=u, 1=g, 2=r, etc.) 

350 

351 Returns 

352 ------- 

353 dx -- the offset in the x focal plane position in mm 

354 

355 dy -- the offset in the y focal plane position in mm 

356 """ 

357 return self._apply_transformation(self._pupil_to_focal, xmm, ymm, band) 

358 

359 def dxdy_inverse(self, xmm, ymm, band): 

360 """ 

361 Apply the transformation necessary when going from focal 

362 plane coordinates to pupil coordinates. 

363 

364 The recipe to correctly use this method is 

365 

366 dx, dy = LsstZernikeFitter().dxdy_inverse(xf, yf, band=band) 

367 

368 xp, yp = pupilCoordsFromFocalPlaneCoords(xf+dx, 

369 yf+dy, 

370 camera=lsst_camera()) 

371 

372 xp and yp are now the actual position in radians on the pupil 

373 corresponding to the focal plane coordinates xf, yf 

374 

375 Parameters 

376 ---------- 

377 xmm -- the naive x focal plane position in mm 

378 

379 ymm -- the naive y focal plane position in mm 

380 

381 band -- the filter in which we are operating 

382 (can be either a string or an int; 0=u, 1=g, 2=r, etc.) 

383 

384 Returns 

385 ------- 

386 dx -- the offset in the x focal plane position in mm 

387 

388 dy -- the offset in the y focal plane position in mm 

389 """ 

390 return self._apply_transformation(self._focal_to_pupil, xmm, ymm, band)