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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

import numpy as np 

import os 

import numbers 

import palpy 

 

from lsst.utils import getPackageDir 

from lsst.sims.utils import ZernikePolynomialGenerator 

from lsst.sims.coordUtils import lsst_camera 

from lsst.sims.coordUtils import DMtoCameraPixelTransformer 

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

import lsst.geom as geom 

from lsst.sims.utils.CodeUtilities import _validate_inputs 

 

 

__all__ = ["LsstZernikeFitter"] 

 

 

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

""" 

Convert Observed RA, Dec into pupil coordinates 

 

Parameters 

---------- 

ra_obs is the observed RA in radians 

 

dec_obs is the observed Dec in radians 

 

ra0 is the RA of the boresite in radians 

 

dec0 is the Dec of the boresite in radians 

 

rotSkyPos is in radians 

 

Returns 

-------- 

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

radians and whose second row is the y coordinate in radians 

""" 

 

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

"pupilCoordsFromObserved") 

 

theta = -1.0*rotSkyPos 

 

ra_pointing = ra0 

dec_pointing = dec0 

 

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

# with a tangent point at (pointingRA, pointingDec) 

# 

if not are_arrays: 

try: 

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

except: 

x = np.NaN 

y = np.NaN 

else: 

try: 

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

except: 

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

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

x = [] 

y = [] 

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

try: 

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

except: 

xx = np.NaN 

yy = np.NaN 

x.append(xx) 

y.append(yy) 

x = np.array(x) 

y = np.array(y) 

 

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

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

# the rotation of the focal plane about the telescope pointing 

 

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

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

 

return np.array([x_out, y_out]) 

 

 

class LsstZernikeFitter(object): 

""" 

This class will fit and then apply the Zernike polynomials needed 

to correct the FIELD_ANGLE to FOCAL_PLANE transformation for the 

filter-dependent part. 

""" 

 

def __init__(self): 

self._camera = lsst_camera() 

self._pixel_transformer = DMtoCameraPixelTransformer() 

self._z_gen = ZernikePolynomialGenerator() 

 

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

# make it a little bigger to accommodate any slop in 

# the conversion from focal plane coordinates back to 

# pupil coordinates (in case the optical distortions 

# cause points to cross over the actual boundary of 

# the focal plane) 

 

self._band_to_int = {} 

self._band_to_int['u'] = 0 

self._band_to_int['g'] = 1 

self._band_to_int['r'] = 2 

self._band_to_int['i'] = 3 

self._band_to_int['z'] = 4 

self._band_to_int['y'] = 5 

 

self._int_to_band = 'ugrizy' 

 

self._n_grid = [] 

self._m_grid = [] 

 

# 2018 May 8 

# During development, I found that there was negligible 

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

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

for n in range(4): 

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

self._n_grid.append(n) 

self._m_grid.append(m) 

 

self._build_transformations() 

 

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

""" 

Get the coefficients of the best fit Zernike Polynomial 

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

 

Returns numpy arrays of the Zernike Polynomial expansion 

coefficients in x and y. Zernike Polynomials correspond 

to the radial and angular orders stored in self._n_grid 

and self._m_grid. 

""" 

 

polynomials = {} 

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

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

polynomials[(n,m)] = values 

 

poly_keys = list(polynomials.keys()) 

 

dx = x_out - x_in 

dy = y_out - y_in 

 

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

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

for k2 in poly_keys]) 

 

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

alpha_x = {} 

for ii, kk in enumerate(poly_keys): 

alpha_x[kk] = alpha_x_[ii] 

 

 

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

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

for k2 in poly_keys]) 

 

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

alpha_y = {} 

for ii, kk in enumerate(poly_keys): 

alpha_y[kk] = alpha_y_[ii] 

 

return alpha_x, alpha_y 

 

def _build_transformations(self): 

""" 

Solve for and store the coefficients of the Zernike 

polynomial expansion of the difference between the 

naive and the bandpass-dependent optical distortions 

in the LSST camera. 

""" 

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

'FocalPlaneData', 

'CatSimData') 

 

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

'FocalPlaneData', 

'PhoSimData') 

 

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

# that were given to PhoSim 

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

 

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

header = input_file.readline() 

params = header.strip().split() 

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

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

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

 

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

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

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

 

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

 

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

catsim_data=catsim_data[sorted_dex] 

 

# convert from RA, Dec to pupil coors/FIELD_ANGLE 

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

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

ra0, dec0, rotSkyPos) 

 

# convert from FIELD_ANGLE to FOCAL_PLANE without attempting to model 

# the optical distortions in the telescope 

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

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

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

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

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

catsim_xmm[ii] = focal_pt.getX() 

catsim_ymm[ii] = focal_pt.getY() 

 

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

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

 

self._pupil_to_focal = {} 

self._focal_to_pupil = {} 

 

for i_filter in range(6): 

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

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

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

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

 

for det in self._camera: 

if det.getType() != SCIENCE: 

continue 

pixels_to_focal = det.getTransform(PIXELS, FOCAL_PLANE) 

det_name = det.getName() 

bbox = det.getBBox() 

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

 

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

# by PhoSim 

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

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

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

 

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

# to the edge of the detector 

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

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

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

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

 

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

phosim_data['ypix'], 

det_name) 

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

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

for ii in range(len(xpix)): 

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

xmm[ii] = focal_pt.getX() 

ymm[ii] = focal_pt.getY() 

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

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

 

# solve for the coefficients of the Zernike expansions 

# necessary to model the optical transformations and go 

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

# to the PhoSim realized focal plane positions 

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

phosim_xmm, phosim_ymm) 

 

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

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

 

# solve for the coefficients to the Zernike expansions 

# necessary to go back from the PhoSim realized focal plane 

# positions to the naive CatSim predicted focal plane 

# positions 

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

catsim_xmm, catsim_ymm) 

 

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

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

 

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

""" 

Parameters 

---------- 

tranformation_dict -- a dict containing the coefficients 

of the Zernike decomposition to be applied 

 

xmm -- the input x position in mm 

 

ymm -- the input y position in mm 

 

band -- the filter in which we are operating 

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

 

Returns 

------- 

dx -- the x offset resulting from the transformation 

 

dy -- the y offset resulting from the transformation 

""" 

if isinstance(band, int): 

band = self._int_to_band[band] 

 

if isinstance(xmm, numbers.Number): 

dx = 0.0 

dy = 0.0 

else: 

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

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

 

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

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

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

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

 

return dx, dy 

 

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

""" 

Apply the transformation necessary when going from pupil 

coordinates to focal plane coordinates. 

 

The recipe to correctly use this method is 

 

xf0, yf0 = focalPlaneCoordsFromPupilCoords(xpupil, ypupil, 

camera=lsst_camera()) 

 

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

 

xf = xf0 + dx 

yf = yf0 + dy 

 

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

LSST focal plane corresponding to xpupil, ypupil 

 

Parameters 

---------- 

xmm -- the naive x focal plane position in mm 

 

ymm -- the naive y focal plane position in mm 

 

band -- the filter in which we are operating 

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

 

Returns 

------- 

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

 

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

""" 

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

 

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

""" 

Apply the transformation necessary when going from focal 

plane coordinates to pupil coordinates. 

 

The recipe to correctly use this method is 

 

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

 

xp, yp = pupilCoordsFromFocalPlaneCoords(xf+dx, 

yf+dy, 

camera=lsst_camera()) 

 

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

corresponding to the focal plane coordinates xf, yf 

 

Parameters 

---------- 

xmm -- the naive x focal plane position in mm 

 

ymm -- the naive y focal plane position in mm 

 

band -- the filter in which we are operating 

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

 

Returns 

------- 

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

 

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

""" 

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