Coverage for python/lsst/sims/coordUtils/LsstZernikeFitter.py : 11%

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
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
16__all__ = ["LsstZernikeFitter"]
19def _rawPupilCoordsFromObserved(ra_obs, dec_obs, ra0, dec0, rotSkyPos):
20 """
21 Convert Observed RA, Dec into pupil coordinates
23 Parameters
24 ----------
25 ra_obs is the observed RA in radians
27 dec_obs is the observed Dec in radians
29 ra0 is the RA of the boresite in radians
31 dec0 is the Dec of the boresite in radians
33 rotSkyPos is in radians
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 """
41 are_arrays = _validate_inputs([ra_obs, dec_obs], ['ra_obs', 'dec_obs'],
42 "pupilCoordsFromObserved")
44 theta = -1.0*rotSkyPos
46 ra_pointing = ra0
47 dec_pointing = dec0
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)
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
81 x_out = x*np.cos(theta) - y*np.sin(theta)
82 y_out = x*np.sin(theta) + y*np.cos(theta)
84 return np.array([x_out, y_out])
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 """
94 def __init__(self):
95 self._camera = lsst_camera()
96 self._pixel_transformer = DMtoCameraPixelTransformer()
97 self._z_gen = ZernikePolynomialGenerator()
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)
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
114 self._int_to_band = 'ugrizy'
116 self._n_grid = []
117 self._m_grid = []
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)
128 self._build_transformations()
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.
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 """
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
146 poly_keys = list(polynomials.keys())
148 dx = x_out - x_in
149 dy = y_out - y_in
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])
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]
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])
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]
170 return alpha_x, alpha_y
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')
183 phosim_dir = os.path.join(getPackageDir('sims_data'),
184 'FocalPlaneData',
185 'PhoSimData')
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')
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]))
198 catsim_dtype = np.dtype([('id', int), ('xmm_old', float), ('ymm_old', float),
199 ('xpup', float), ('ypup', float),
200 ('raObs', float), ('decObs', float)])
202 catsim_data = np.genfromtxt(catsim_catalog, dtype=catsim_dtype)
204 sorted_dex = np.argsort(catsim_data['id'])
205 catsim_data=catsim_data[sorted_dex]
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)
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()
222 phosim_dtype = np.dtype([('id', int), ('phot', float),
223 ('xpix', float), ('ypix', float)])
225 self._pupil_to_focal = {}
226 self._focal_to_pupil = {}
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)
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(' ','_')
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)
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
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
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)
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
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)
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
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
294 xmm -- the input x position in mm
296 ymm -- the input y position in mm
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.)
301 Returns
302 -------
303 dx -- the x offset resulting from the transformation
305 dy -- the y offset resulting from the transformation
306 """
307 if isinstance(band, int):
308 band = self._int_to_band[band]
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)
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
322 return dx, dy
324 def dxdy(self, xmm, ymm, band):
325 """
326 Apply the transformation necessary when going from pupil
327 coordinates to focal plane coordinates.
329 The recipe to correctly use this method is
331 xf0, yf0 = focalPlaneCoordsFromPupilCoords(xpupil, ypupil,
332 camera=lsst_camera())
334 dx, dy = LsstZernikeFitter().dxdy(xf0, yf0, band=band)
336 xf = xf0 + dx
337 yf = yf0 + dy
339 xf and yf are now the actual position in millimeters on the
340 LSST focal plane corresponding to xpupil, ypupil
342 Parameters
343 ----------
344 xmm -- the naive x focal plane position in mm
346 ymm -- the naive y focal plane position in mm
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.)
351 Returns
352 -------
353 dx -- the offset in the x focal plane position in mm
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)
359 def dxdy_inverse(self, xmm, ymm, band):
360 """
361 Apply the transformation necessary when going from focal
362 plane coordinates to pupil coordinates.
364 The recipe to correctly use this method is
366 dx, dy = LsstZernikeFitter().dxdy_inverse(xf, yf, band=band)
368 xp, yp = pupilCoordsFromFocalPlaneCoords(xf+dx,
369 yf+dy,
370 camera=lsst_camera())
372 xp and yp are now the actual position in radians on the pupil
373 corresponding to the focal plane coordinates xf, yf
375 Parameters
376 ----------
377 xmm -- the naive x focal plane position in mm
379 ymm -- the naive y focal plane position in mm
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.)
384 Returns
385 -------
386 dx -- the offset in the x focal plane position in mm
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)