aopera 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aopera/__init__.py +51 -0
- aopera/aopsd.py +344 -0
- aopera/control.py +355 -0
- aopera/data/ekarus.ini +45 -0
- aopera/data/harmoni-scao.ini +43 -0
- aopera/data/ohp.ini +13 -0
- aopera/data/papyrus.ini +45 -0
- aopera/data/paranal.ini +13 -0
- aopera/data/sphere.ini +45 -0
- aopera/ffwfs.py +335 -0
- aopera/fiber.py +61 -0
- aopera/otfpsf.py +212 -0
- aopera/photometry.py +219 -0
- aopera/readconfig.py +267 -0
- aopera/shwfs.py +316 -0
- aopera/simulation.py +358 -0
- aopera/trajectory.py +120 -0
- aopera/turbulence.py +445 -0
- aopera/utils.py +142 -0
- aopera/variance.py +112 -0
- aopera/zernike.py +193 -0
- aopera-0.1.0.dist-info/METADATA +741 -0
- aopera-0.1.0.dist-info/RECORD +26 -0
- aopera-0.1.0.dist-info/WHEEL +5 -0
- aopera-0.1.0.dist-info/licenses/LICENSE +674 -0
- aopera-0.1.0.dist-info/top_level.txt +1 -0
aopera/ffwfs.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on Fri Nov 11 19:00:43 2022
|
|
3
|
+
Fourier-Filtering Wavefront Sensor
|
|
4
|
+
@author: v.chambouleyron r.fetick
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy.signal import fftconvolve
|
|
9
|
+
from numpy.fft import fft2, fftshift, rfft2, irfft2
|
|
10
|
+
from aopera.readconfig import read_config_file, read_config_tiptop, set_attribute
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
#%% FF-WFS BASIC FUNCTIONS
|
|
15
|
+
|
|
16
|
+
@lru_cache(maxsize=2)
|
|
17
|
+
def modulation(nx, rmod, samp):
|
|
18
|
+
"""
|
|
19
|
+
Build the modulation ring
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
nx : number of pixels.
|
|
24
|
+
rmod : modulation radius [lambda/D].
|
|
25
|
+
samp : sampling (2=Shannon).
|
|
26
|
+
"""
|
|
27
|
+
rmod_px = rmod*samp
|
|
28
|
+
xx,yy = np.mgrid[0:nx,0:nx] - nx//2
|
|
29
|
+
rr = np.sqrt(xx**2+yy**2)
|
|
30
|
+
return (rr<(rmod_px+0.5))*(rr>=(rmod_px-0.5))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@lru_cache(maxsize=2)
|
|
34
|
+
def pyramid_mask(nx, nFaces, angle=0.35, theta_ref=0, four_pixels=False):
|
|
35
|
+
"""
|
|
36
|
+
Build the pyramid mask
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
nx : number of pixels.
|
|
41
|
+
nFaces : number of faces for the pyramidal mask.
|
|
42
|
+
angle : angle of the pyramid #TODO: definition? which unit?
|
|
43
|
+
theta_ref : rotation angle of the pyramid faces.
|
|
44
|
+
"""
|
|
45
|
+
if four_pixels:
|
|
46
|
+
center = nx//2 - 0.5
|
|
47
|
+
else:
|
|
48
|
+
center = nx//2
|
|
49
|
+
xx,yy = np.mgrid[0:nx,0:nx] - center
|
|
50
|
+
theta = np.mod(np.arctan2(xx,yy)+np.pi-theta_ref, 2*np.pi)
|
|
51
|
+
theta_face = 2*np.pi/nFaces
|
|
52
|
+
msk = np.zeros((nx,nx), dtype=complex)
|
|
53
|
+
|
|
54
|
+
for k in range(0,nFaces):
|
|
55
|
+
# Tesselation
|
|
56
|
+
theta_bnd = ((k*theta_face)<=theta) * (theta<((k+1)*theta_face))
|
|
57
|
+
# Tip-Tilt
|
|
58
|
+
theta_direction = (k+0.5)*theta_face
|
|
59
|
+
c_tip = np.sin(theta_direction+theta_ref)
|
|
60
|
+
c_tilt = np.cos(theta_direction+theta_ref)
|
|
61
|
+
# Complex mask on each face
|
|
62
|
+
msk += theta_bnd * np.exp(2j*np.pi*angle*(c_tip*xx+c_tilt*yy))
|
|
63
|
+
|
|
64
|
+
return msk
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def ffwfs_impulse_response(mask, modu, psf):
|
|
68
|
+
"""
|
|
69
|
+
Compute the impulse response of a FF-WFS.
|
|
70
|
+
"""
|
|
71
|
+
wtot = fftshift(irfft2(rfft2(modu)*rfft2(psf), s=psf.shape))
|
|
72
|
+
wtot = wtot/np.sum(wtot)
|
|
73
|
+
mask_ft = fft2(fftshift(mask)) / np.size(mask)
|
|
74
|
+
return 2*np.imag(np.conj(mask_ft)*fft2(fftshift(mask*wtot)))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def ffwfs_transfer_function(*args, **kwargs):
|
|
78
|
+
"""
|
|
79
|
+
Compute the transfer function of a FF-WFS.
|
|
80
|
+
"""
|
|
81
|
+
return fftshift(fft2(ffwfs_impulse_response(*args, **kwargs)))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def ffwfs_sensitivity(mask, modu, psf_calib, psf):
|
|
85
|
+
"""
|
|
86
|
+
Compute the sensitivity map of a FF-WFS in the (fx,fy) domain.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
mask : the complex mask of the FF-WFS.
|
|
91
|
+
modu : the modulation circle mask.
|
|
92
|
+
psf_calib : PSF used as reference for calibration.
|
|
93
|
+
psf : current PSF shape to compute optical gains.
|
|
94
|
+
"""
|
|
95
|
+
otf_calib = rfft2(psf_calib)
|
|
96
|
+
TF = ffwfs_transfer_function(mask, modu, psf)
|
|
97
|
+
return np.sqrt(np.real(fftshift(irfft2(rfft2(np.abs(TF)**2)*otf_calib, s=psf.shape))))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def ffwfs_reconstructor(*args, thresh=1e-2):
|
|
101
|
+
"""
|
|
102
|
+
Compute the reconstructor map of a FF-WFS in the (fx,fy) domain.
|
|
103
|
+
See also: ffwfs_sensitivity
|
|
104
|
+
"""
|
|
105
|
+
sensi = ffwfs_sensitivity(*args)
|
|
106
|
+
rec = np.zeros(sensi.shape)
|
|
107
|
+
vld = np.where(sensi>thresh)
|
|
108
|
+
rec[vld] = 1/sensi[vld]
|
|
109
|
+
return rec
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def ffwfs_visibility(*args, **kwargs):
|
|
113
|
+
"""
|
|
114
|
+
Compute the FF-WFS visible frequencies.
|
|
115
|
+
See also: ffwfs_sensitivity, ffwfs_reconstructor
|
|
116
|
+
"""
|
|
117
|
+
sensi = ffwfs_sensitivity(*args)
|
|
118
|
+
rec = ffwfs_reconstructor(*args, **kwargs)
|
|
119
|
+
return np.real(sensi*rec)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def optical_gain(mask, modu, psf_calib, psf_onsky, obj_sky=None, modu_sky=True):
|
|
123
|
+
"""
|
|
124
|
+
Compute optical gains map in the (fx,fy) domain.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
mask : the complex mask of the FF-WFS.
|
|
129
|
+
modu : the modulation circle mask.
|
|
130
|
+
psf_calib : PSF used as reference for calibration.
|
|
131
|
+
psf_onsky : current PSF shape to compute optical gains.
|
|
132
|
+
|
|
133
|
+
Keywords
|
|
134
|
+
--------
|
|
135
|
+
obj_sky : the extended object image (if applicable)
|
|
136
|
+
modu_sky : should we modulate onsky?
|
|
137
|
+
"""
|
|
138
|
+
if modu_sky:
|
|
139
|
+
modu_onsky = modu
|
|
140
|
+
else:
|
|
141
|
+
modu_onsky = modulation(modu.shape[0], 0, 2) # Dirac
|
|
142
|
+
if obj_sky is not None:
|
|
143
|
+
psf_onsky = fftconvolve(psf_onsky, obj_sky/np.sum(obj_sky), mode='same')
|
|
144
|
+
sensi_calib = ffwfs_sensitivity(mask, modu, psf_calib, psf_calib)
|
|
145
|
+
sensi_sky = ffwfs_sensitivity(mask, modu_onsky, psf_calib, psf_onsky)
|
|
146
|
+
return sensi_sky/sensi_calib
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
#%% POWER SPECTRAL DENSITIES
|
|
150
|
+
|
|
151
|
+
def ffwfs_psd_aliasing(fx, fy, psd_input, ffwfsrec, pix):
|
|
152
|
+
"""
|
|
153
|
+
Aliasing PSD of a FF-WFS.
|
|
154
|
+
Assume a constant FF-WFS sensitivity at high-frequency.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
fx : np.array
|
|
159
|
+
The spatial frequencies on X [1/m].
|
|
160
|
+
fy : np.array
|
|
161
|
+
The spatial frequencies on Y [1/m].
|
|
162
|
+
psd_input : function
|
|
163
|
+
The input PSD to be aliased. Should be called as: psd=psd_input(fx,fy)
|
|
164
|
+
ffwfsrec : np.array
|
|
165
|
+
see ffwfs_reconstructor
|
|
166
|
+
pix : float
|
|
167
|
+
Pixel size in the pupil plane [m].
|
|
168
|
+
"""
|
|
169
|
+
psd_alias = np.zeros(fx.shape)
|
|
170
|
+
fcut = 1/(2*pix)
|
|
171
|
+
nshift = 3
|
|
172
|
+
ffwfssensi = 1/np.mean(ffwfsrec[0,:]) # constant sensitivity at HF
|
|
173
|
+
psdfilter = np.abs(ffwfsrec*ffwfssensi)**2
|
|
174
|
+
for i in range(-nshift,nshift+1):
|
|
175
|
+
for j in range(-nshift,nshift+1):
|
|
176
|
+
if (i!=0) or (j!=0):
|
|
177
|
+
fx_shift = fx - i*2*fcut
|
|
178
|
+
fy_shift = fy - j*2*fcut
|
|
179
|
+
psd_atmo_shift = psd_input(fx_shift,fy_shift)
|
|
180
|
+
pix_filter = np.sinc(fx_shift*pix)*np.sinc(fy_shift*pix)
|
|
181
|
+
psd_alias += psd_atmo_shift * psdfilter * pix_filter**2
|
|
182
|
+
return psd_alias
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def psd_pyrwfs_noise_ron(mask, modu, nphot, var_ron, nssp, psf_onsky, psf_calib):
|
|
186
|
+
"""
|
|
187
|
+
Compute the phase PSD due to the AO RON.
|
|
188
|
+
The PSD is given on the (fx,fy) after reconstruction of the phase,
|
|
189
|
+
but before the loop temporal noise filtering.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
mask : the complex mask of the Pyramid-WFS.
|
|
194
|
+
modu : the modulation circle mask.
|
|
195
|
+
nphot : float (>=0)
|
|
196
|
+
Number of photons available for the neasurement in total.
|
|
197
|
+
var_ron : camera RON in photons unit/frame/px
|
|
198
|
+
nssp = number of pixels in one pupil image
|
|
199
|
+
psf_onsky : current PSF shape to compute sensitivity on-sky.
|
|
200
|
+
psf_calib : PSF used as reference for calibration.
|
|
201
|
+
|
|
202
|
+
Return
|
|
203
|
+
------
|
|
204
|
+
the AO error PSD [rad²m²].
|
|
205
|
+
The PSD is given at the measurement wvl !!!
|
|
206
|
+
To get it at the scientific wvl, it should be multiplied by (wvl_wfs/wvl_sci)**2.
|
|
207
|
+
|
|
208
|
+
References
|
|
209
|
+
----------
|
|
210
|
+
V. Chambouleyron, 2021, PhD thesis: section 3.1
|
|
211
|
+
"""
|
|
212
|
+
sensi_ron = ffwfs_sensitivity(mask,modu,psf_calib,psf_onsky)
|
|
213
|
+
var_ron = (nssp*var_ron)/nphot**2 * (1/sensi_ron)**2
|
|
214
|
+
return var_ron
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def psd_pyrwfs_noise_photon(mask, nfaces, modu, nphot, psf_onsky, psf_calib, emccd=False):
|
|
218
|
+
"""
|
|
219
|
+
Compute the phase PSD due to the photon noise.
|
|
220
|
+
The PSD is given on the (fx,fy) after reconstruction of the phase,
|
|
221
|
+
but before the loop temporal noise filtering.
|
|
222
|
+
|
|
223
|
+
Parameters
|
|
224
|
+
----------
|
|
225
|
+
mask : the complex mask of the Pyramid-WFS.
|
|
226
|
+
nfaces : number of faces of the Pyramid-WFS
|
|
227
|
+
modu : the modulation circle mask.
|
|
228
|
+
nphot : float (>=0)
|
|
229
|
+
Number of photons available for the neasurement in total.
|
|
230
|
+
psf_onsky : current PSF shape to compute sensitivity on-sky.
|
|
231
|
+
|
|
232
|
+
Keywords
|
|
233
|
+
--------
|
|
234
|
+
emccd : boolean
|
|
235
|
+
Multiply result by excess factor if camera is EMCCD
|
|
236
|
+
|
|
237
|
+
Return
|
|
238
|
+
------
|
|
239
|
+
the AO error PSD [rad²m²].
|
|
240
|
+
The PSD is given at the measurement wvl !!!
|
|
241
|
+
To get it at the scientific wvl, it should be multiplied by (wvl_wfs/wvl_sci)**2.
|
|
242
|
+
|
|
243
|
+
References
|
|
244
|
+
----------
|
|
245
|
+
V. Chambouleyron, 2021, PhD thesis: section 3.1
|
|
246
|
+
"""
|
|
247
|
+
#FIXME: this sensitivity gives correct results wrt OOPAO, but why?
|
|
248
|
+
logging.debug('FFWFS sensitivity has been increased to match OOPAO.')
|
|
249
|
+
sensi_phot = ffwfs_sensitivity(mask,modu,psf_calib,psf_onsky) / np.sqrt(nfaces/2.5) #/ np.sqrt(nfaces) # Rough formula, to be checked more !!!
|
|
250
|
+
var_phot = 1/nphot*(1/sensi_phot)**2
|
|
251
|
+
if emccd:
|
|
252
|
+
var_phot *= 2 # excess factor squared
|
|
253
|
+
return var_phot
|
|
254
|
+
|
|
255
|
+
#%% FFWFS CLASS
|
|
256
|
+
class PWFS:
|
|
257
|
+
def __init__(self, dictionary):
|
|
258
|
+
self.nface = 4
|
|
259
|
+
self.angle = 0.35
|
|
260
|
+
set_attribute(self, dictionary, 'pwfs')
|
|
261
|
+
|
|
262
|
+
def __repr__(self):
|
|
263
|
+
s = 'aopera.PWFS\n'
|
|
264
|
+
s += '------------\n'
|
|
265
|
+
s += 'lenslet : %u \n'%self.lenslet
|
|
266
|
+
s += 'ron : %.1f e-\n'%self.ron
|
|
267
|
+
s += 'EMCCD : %s\n'%self.emccd
|
|
268
|
+
s += 'R_modul : %.1f l/D\n'%self.modulation
|
|
269
|
+
s += 'OG comp : %s\n'%self.og_compensation
|
|
270
|
+
return s
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def from_file(filepath, category='pwfs'):
|
|
274
|
+
return PWFS(read_config_file(filepath, category))
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def from_file_tiptop(filepath):
|
|
278
|
+
return PWFS(read_config_tiptop(filepath)[2])
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def from_oopao(wfs):
|
|
282
|
+
return PWFS({'lenslet':wfs.nSubap,
|
|
283
|
+
'ron':wfs.cam.readoutNoise,
|
|
284
|
+
'emccd':wfs.cam.sensor=='EMCCD',
|
|
285
|
+
'modulation':wfs.modulation})
|
|
286
|
+
|
|
287
|
+
def modulation_mask(self, nx, samp):
|
|
288
|
+
return modulation(nx, self.modulation, samp)
|
|
289
|
+
|
|
290
|
+
def pyramid_mask(self, nx, theta_ref=0):
|
|
291
|
+
return pyramid_mask(nx, self.nface, angle=self.angle, theta_ref=theta_ref)
|
|
292
|
+
|
|
293
|
+
def sensitivity(self, samp, psf_calib, psf):
|
|
294
|
+
nx = psf.shape[0]
|
|
295
|
+
mask = self.pyramid_mask(nx)
|
|
296
|
+
modu = self.modulation_mask(nx, samp)
|
|
297
|
+
return ffwfs_sensitivity(mask, modu, psf_calib, psf)
|
|
298
|
+
|
|
299
|
+
def reconstructor(self, samp, psf_calib, thresh=1e-2):
|
|
300
|
+
nx = psf_calib.shape[0]
|
|
301
|
+
mask = self.pyramid_mask(nx)
|
|
302
|
+
modu = self.modulation_mask(nx, samp)
|
|
303
|
+
return ffwfs_reconstructor(mask, modu, psf_calib, psf_calib, thresh=thresh)
|
|
304
|
+
|
|
305
|
+
def visibility(self, samp, psf_calib, thresh=1e-2):
|
|
306
|
+
nx = psf_calib.shape[0]
|
|
307
|
+
mask = self.pyramid_mask(nx)
|
|
308
|
+
modu = self.modulation_mask(nx, samp)
|
|
309
|
+
return ffwfs_visibility(mask, modu, psf_calib, psf_calib, thresh=thresh)
|
|
310
|
+
|
|
311
|
+
def optical_gain(self, samp, psf_calib, psf_onsky, **kwargs):
|
|
312
|
+
nx = psf_calib.shape[0]
|
|
313
|
+
mask = self.pyramid_mask(nx)
|
|
314
|
+
modu = self.modulation_mask(nx, samp)
|
|
315
|
+
return optical_gain(mask, modu, psf_calib, psf_onsky, **kwargs)
|
|
316
|
+
|
|
317
|
+
def psd_aliasing(self, fx, fy, psd_input, ffwfsrec, diameter, nact=np.inf):
|
|
318
|
+
# NOTE : small subtelty on <nact> :
|
|
319
|
+
# if a mode is not shown to the WFS by the DM, it cannot
|
|
320
|
+
# be disantangled by the reconstructor, so it is aliased.
|
|
321
|
+
return ffwfs_psd_aliasing(fx, fy, psd_input, ffwfsrec, diameter/min(self.lenslet,nact-1))
|
|
322
|
+
|
|
323
|
+
def psd_noise_ron(self, samp, nphot, psf_onsky, psf_calib):
|
|
324
|
+
nx = psf_calib.shape[0]
|
|
325
|
+
mask = self.pyramid_mask(nx)
|
|
326
|
+
modu = self.modulation_mask(nx, samp)
|
|
327
|
+
nssp = np.pi*(self.lenslet/2)**2
|
|
328
|
+
return psd_pyrwfs_noise_ron(mask, modu, nphot, self.ron**2, nssp, psf_onsky, psf_calib)
|
|
329
|
+
|
|
330
|
+
def psd_noise_photon(self, samp, nphot, psf_onsky, psf_calib):
|
|
331
|
+
nx = psf_calib.shape[0]
|
|
332
|
+
mask = self.pyramid_mask(nx)
|
|
333
|
+
modu = self.modulation_mask(nx, samp)
|
|
334
|
+
return psd_pyrwfs_noise_photon(mask, self.nface, modu, nphot, psf_onsky, psf_calib, emccd=self.emccd)
|
|
335
|
+
|
aopera/fiber.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Step-index single-mode fibers (SMF)
|
|
3
|
+
|
|
4
|
+
date: 2025-02-15
|
|
5
|
+
author: astriffling, rfetick
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def normalized_frequency(r_core:float, na:float, wl:float):
|
|
13
|
+
"""
|
|
14
|
+
Compute the normalized frequency of the fiber
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
r_core : float, radius of the fiber core [m].
|
|
19
|
+
na : float, numerical aperture of the fiber.
|
|
20
|
+
wl : float, wavelength of interest [m].
|
|
21
|
+
"""
|
|
22
|
+
return (2 * np.pi * r_core * na) / wl
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def mode_field_radius(*args):
|
|
26
|
+
"""
|
|
27
|
+
Compute the fondamental MFR in [m]
|
|
28
|
+
"""
|
|
29
|
+
v = normalized_frequency(*args)
|
|
30
|
+
return args[0] * (0.65 + 1.619/v**(3/2) + 2.879/v**6 - (0.016 + 1.561*v**(-7)))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@lru_cache(maxsize=1)
|
|
34
|
+
def fundamental_mode(nx:int, sampling:int, *args):
|
|
35
|
+
"""
|
|
36
|
+
Compute the fundamental mode of the fiber, using gaussian approximation of
|
|
37
|
+
the mode LP01/TEM00.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
nx : int, number of pixel of table.
|
|
42
|
+
"""
|
|
43
|
+
mfr = mode_field_radius(*args)
|
|
44
|
+
xx,yy = (np.mgrid[0:nx,0:nx]-nx//2) * args[2] / (2 * args[1] * sampling)
|
|
45
|
+
return np.exp(-(xx**2 + yy**2) / (mfr**2))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def coupling_efficiency(field:np.array, *args):
|
|
49
|
+
"""
|
|
50
|
+
Compute the SMF coupling efficiency for a focal plane EM field
|
|
51
|
+
|
|
52
|
+
From G. P. P. L. Otten et al, A&A, 2021, p4
|
|
53
|
+
https://doi.org/10.1051/0004-6361/202038517
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
field : array.
|
|
58
|
+
"""
|
|
59
|
+
nx = field.shape[0]
|
|
60
|
+
mode = fundamental_mode(nx, *args)
|
|
61
|
+
return np.abs(np.sum(mode * np.conj(field)))**2 / (np.sum(np.abs(mode)**2) * np.sum(np.abs(field)**2))
|
aopera/otfpsf.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Convert phase PSD to OTF and to PSF
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.fft import fft2, ifft2, ifftshift, fftshift
|
|
8
|
+
from aopera.utils import aperture
|
|
9
|
+
from aopera.readconfig import read_config_file, read_config_tiptop, set_attribute
|
|
10
|
+
from aopera.aopsd import piston_filter, psd_wvl_scaling
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def angle2freq(pix_angle,wvl):
|
|
15
|
+
"""Computes the PSD frequency step [1/m] to get the corresponding angular
|
|
16
|
+
pixel size [rad] on the PSF."""
|
|
17
|
+
df = pix_angle/wvl
|
|
18
|
+
return df
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@lru_cache(maxsize=2)
|
|
22
|
+
def otf_diffraction(npix, occ=0, samp=2):
|
|
23
|
+
"""
|
|
24
|
+
Compute the diffraction OTF
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
npix : int
|
|
29
|
+
Shape of the output array is (npix,npix).
|
|
30
|
+
|
|
31
|
+
Keywords
|
|
32
|
+
--------
|
|
33
|
+
occ : float
|
|
34
|
+
Eventual central obstruction (0<=occ<1).
|
|
35
|
+
samp : float
|
|
36
|
+
Required sampling. Must verify the condition samp>=2.
|
|
37
|
+
"""
|
|
38
|
+
if samp<2:
|
|
39
|
+
raise ValueError("Sampling cannot be less than 2.0")
|
|
40
|
+
aper = aperture(npix, samp=samp, occ=occ)
|
|
41
|
+
otf = ifftshift(ifft2(np.abs(fft2(aper))**2)) / np.sum(aper)
|
|
42
|
+
return otf
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def otf2psf(otf):
|
|
46
|
+
"""Get the PSF for a given OTF"""
|
|
47
|
+
psf = np.abs(np.real(fftshift(ifft2(fftshift(otf)))))
|
|
48
|
+
return psf
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@lru_cache(maxsize=2)
|
|
52
|
+
def psf_diffraction(npix, occ=0, samp=2):
|
|
53
|
+
"""Compute the diffraction PSF"""
|
|
54
|
+
return otf2psf(otf_diffraction(npix, occ=occ, samp=samp))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def psd2otf(psd, df, psdsum=None):
|
|
58
|
+
"""
|
|
59
|
+
Get the OTF for a given PSD
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
psd : np.array
|
|
64
|
+
The PSD 2D array.
|
|
65
|
+
df : float
|
|
66
|
+
Frequency step of the PSD array [1/m].
|
|
67
|
+
|
|
68
|
+
Keywords
|
|
69
|
+
--------
|
|
70
|
+
psdsum : float, None
|
|
71
|
+
Integral of the PSD.
|
|
72
|
+
If psdsum=None, the integral is the numerical sum on the array.
|
|
73
|
+
The PSF is then of unit energy, since max(otf)=1.
|
|
74
|
+
"""
|
|
75
|
+
Bphi = ifftshift(np.real(fft2(fftshift(psd)))) * df**2
|
|
76
|
+
if psdsum is None:
|
|
77
|
+
psdsum = np.sum(psd) * df**2
|
|
78
|
+
otf = np.exp(-psdsum+Bphi)
|
|
79
|
+
return otf
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def otf_jitter(freq, rms):
|
|
83
|
+
"""
|
|
84
|
+
Give the Gaussian jitter OTF from its jitter RMS value.
|
|
85
|
+
Not tested yet, do not use.
|
|
86
|
+
"""
|
|
87
|
+
return np.exp(-0.5*(2*np.pi*freq*rms)**2)
|
|
88
|
+
|
|
89
|
+
#%% MODES and DEFORMABLE MIRROR
|
|
90
|
+
def number_modes(nact, occ=0):
|
|
91
|
+
"""Compute the number of corrected modes"""
|
|
92
|
+
surf_ratio = np.pi*(1-occ**2)/4 #between occulted disk and square
|
|
93
|
+
nact_total = nact**2
|
|
94
|
+
return int(round(nact_total * surf_ratio))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def number_radial(nact, occ=0):
|
|
98
|
+
"""Compute the number of corrected radial modes"""
|
|
99
|
+
nmode = number_modes(nact, occ=occ)
|
|
100
|
+
return int(round(np.sqrt(2*nmode)-0.5)) # nr(nr+1)/2 = nmode
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def controllability(fx, fy, D, actuator, nmode_ratio=1):
|
|
104
|
+
"""Return a DM frequency map that is 1 on the corrected frequencies, 0 otherwise"""
|
|
105
|
+
cutoff = (actuator-1)/(2*D)
|
|
106
|
+
if cutoff <= 0: # no DM case
|
|
107
|
+
return np.zeros(fx.shape)
|
|
108
|
+
rr = np.sqrt(fx**2+fy**2)
|
|
109
|
+
if nmode_ratio >1:
|
|
110
|
+
raise ValueError('nmode_ratio cannot be above 1.')
|
|
111
|
+
thresh = np.pi/4
|
|
112
|
+
if nmode_ratio > thresh: # smooth transition from square to circle
|
|
113
|
+
modal_radial_cutoff = (1-nmode_ratio)/(1-thresh) * (rr <= cutoff)
|
|
114
|
+
actuator_square_cutoff = (nmode_ratio-thresh)/(1-thresh) * (np.abs(fx)<=cutoff)*(np.abs(fy)<=cutoff)
|
|
115
|
+
return actuator_square_cutoff + modal_radial_cutoff
|
|
116
|
+
else:
|
|
117
|
+
nact_equiv = np.sqrt(nmode_ratio/thresh)
|
|
118
|
+
return (rr <= (nact_equiv*cutoff)).astype(float)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
#%% TELESCOPE CLASS
|
|
122
|
+
class Pupil:
|
|
123
|
+
def __init__(self, dictionary):
|
|
124
|
+
# self.jitter = 0 # [nm RMS]
|
|
125
|
+
# self.jitter_angle = 0 # [deg]
|
|
126
|
+
# self.jitter_freq = 0 # [Hz]
|
|
127
|
+
set_attribute(self, dictionary, 'pupil')
|
|
128
|
+
|
|
129
|
+
def __repr__(self):
|
|
130
|
+
s = 'aopera.PUPIL\n'
|
|
131
|
+
s += '-------------\n'
|
|
132
|
+
s += 'diameter : %.1f m\n'%self.diameter
|
|
133
|
+
s += 'occultation: %u %%\n'%(100*self.occultation)
|
|
134
|
+
s += 'nb act : %u\n'%self.nact
|
|
135
|
+
s += 'mode ratio : %.2f\n'%self.nmode_ratio
|
|
136
|
+
s += 'ncpa : %u nm\n'%self.ncpa
|
|
137
|
+
# if self.jitter:
|
|
138
|
+
# s += 'jitter : %u nm\n'%self.jitter
|
|
139
|
+
return s
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def from_file(filepath, category='pupil'):
|
|
143
|
+
return Pupil(read_config_file(filepath, category))
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def from_file_tiptop(filepath):
|
|
147
|
+
return Pupil(read_config_tiptop(filepath)[0])
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def from_oopao(tel, dm, M2C):
|
|
151
|
+
return Pupil({'diameter':tel.D,
|
|
152
|
+
'occultation':tel.centralObstruction,
|
|
153
|
+
'nact':dm.nAct,
|
|
154
|
+
'nmode_ratio':M2C.shape[1]/M2C.shape[0]})
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def surface(self):
|
|
158
|
+
return np.pi * (self.diameter/2)**2 * (1-self.occultation**2)
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def mode_max(self):
|
|
162
|
+
return number_modes(self.nact, occ=self.occultation)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def radial_max(self):
|
|
166
|
+
return number_radial(self.nact, occ=self.occultation)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def pitch_dm(self):
|
|
170
|
+
return self.diameter/(self.nact-1)
|
|
171
|
+
|
|
172
|
+
def pitch_wfs(self, nb_lenslet):
|
|
173
|
+
return self.diameter/nb_lenslet
|
|
174
|
+
|
|
175
|
+
def pitch_ao(self, nb_lenslet):
|
|
176
|
+
return max(self.pitch_dm, self.pitch_wfs(nb_lenslet))
|
|
177
|
+
|
|
178
|
+
def cutoff_frequency(self, nb_lenslet):
|
|
179
|
+
return 1/(2*self.pitch_ao(nb_lenslet))
|
|
180
|
+
|
|
181
|
+
def frequency_step(self, samp):
|
|
182
|
+
return 1/(samp*self.diameter)
|
|
183
|
+
|
|
184
|
+
def frequency_grid(self, nx, samp):
|
|
185
|
+
return (np.mgrid[0:nx,0:nx]-nx//2) * self.frequency_step(samp)
|
|
186
|
+
|
|
187
|
+
def piston_filter(self, nx, samp):
|
|
188
|
+
fx,fy = self.frequency_grid(nx, samp)
|
|
189
|
+
return piston_filter(np.sqrt(fx**2+fy**2), self.diameter)
|
|
190
|
+
|
|
191
|
+
def otf_diffraction(self, nx, samp):
|
|
192
|
+
return otf_diffraction(nx, occ=self.occultation, samp=samp)
|
|
193
|
+
|
|
194
|
+
def psf_diffraction(self, nx, samp):
|
|
195
|
+
return psf_diffraction(nx, occ=self.occultation, samp=samp)
|
|
196
|
+
|
|
197
|
+
def controllability(self, fx, fy):
|
|
198
|
+
return controllability(fx, fy, self.diameter, self.nact, nmode_ratio=self.nmode_ratio)
|
|
199
|
+
|
|
200
|
+
def otf_ao(self, psd, samp, wvl_scale=1):
|
|
201
|
+
if wvl_scale == 1:
|
|
202
|
+
otf_diff = self.otf_diffraction(psd.shape[0], samp)
|
|
203
|
+
df = self.frequency_step(samp)
|
|
204
|
+
return psd2otf(psd, df)*otf_diff
|
|
205
|
+
else:
|
|
206
|
+
psd_scale = psd_wvl_scaling(psd, 1/wvl_scale)
|
|
207
|
+
samp_scale = samp * wvl_scale
|
|
208
|
+
return self.otf_ao(psd_scale, samp_scale)
|
|
209
|
+
|
|
210
|
+
def psf_ao(self, *args, **kwargs):
|
|
211
|
+
return otf2psf(self.otf_ao(*args, **kwargs))
|
|
212
|
+
|