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/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
+