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 ADDED
@@ -0,0 +1,51 @@
1
+ """
2
+ AOPERA initialization file
3
+ """
4
+
5
+ #%% SETUP LOGGING
6
+ import logging
7
+ import os
8
+
9
+ class COLORS:
10
+ GREEN = "\x1b[32;20m"
11
+ BLUE = "\033[34m"
12
+ YELLOW = "\x1b[33;20m"
13
+ RED = "\x1b[31;20m"
14
+ CYAN = '\033[36m'
15
+ RESET = '\033[0m'
16
+
17
+ log_in_file = False
18
+ loglvl = logging.INFO # set logging.DEBUG to see all internal issues
19
+
20
+ if log_in_file:
21
+ datefmt='%Y-%m-%d %H:%M:%S'
22
+ logfmt = '[%(asctime)s] %(levelname)s in <%(funcName)s> : %(message)s' # %(module)s
23
+ aoerrorpath = os.sep.join(__file__.split(os.sep)[:-2])
24
+ logpath = aoerrorpath + os.sep + 'log'
25
+ if not os.path.isdir(logpath):
26
+ os.mkdir(logpath)
27
+ logging.basicConfig(filename=logpath+os.sep+'default.log', encoding='utf-8',
28
+ level=loglvl, format=logfmt, datefmt=datefmt)
29
+ else:
30
+ logfmt = COLORS.YELLOW + '%(levelname)s' + COLORS.RESET + ' in <%(funcName)s> : %(message)s' # %(module)s
31
+ logging.basicConfig(level=loglvl, format=logfmt)
32
+
33
+ logging.debug('Load <aoerror> library')
34
+ del log_in_file, logfmt, loglvl, os, logging
35
+
36
+
37
+ #%% IMPORT MODULES
38
+ from . import utils
39
+ from . import zernike
40
+ from . import readconfig
41
+ from . import variance
42
+ from . import control
43
+ from . import turbulence
44
+ from . import aopsd
45
+ from . import shwfs
46
+ from . import ffwfs
47
+ from . import otfpsf
48
+ from . import photometry
49
+ from . import fiber
50
+ from . import simulation
51
+ from . import trajectory
aopera/aopsd.py ADDED
@@ -0,0 +1,344 @@
1
+ """
2
+ General functions to compute PSD terms of the AO system
3
+ """
4
+
5
+ import numpy as np
6
+ from scipy.special import j1
7
+ from aopera.turbulence import propagation_spherical_coord, vonkarmanshape, phase_psd, cn2dh_to_r0
8
+ from aopera.zernike import zernike_fourier
9
+ from scipy.interpolate import RegularGridInterpolator
10
+ import logging
11
+
12
+ def piston_filter(ff, D):
13
+ """Piston filtering function, to be applied on a PSD"""
14
+ ff = np.pi*D*ff
15
+ out = np.zeros_like(ff)
16
+ idx = (ff!=0)
17
+ out[idx] = 1 - (2*j1(ff[idx])/(ff[idx]))**2
18
+ return out
19
+
20
+
21
+ def aomask_in(fx, fy, aocutoff, shape='circle'):
22
+ """Mask that is equal to 1 for the corrected frequencies."""
23
+ print(DeprecationWarning('Function `aomask_in` is deprecated, use `controllability`.'))
24
+ if shape=='circle':
25
+ msk = ((fx**2 + fy**2) < aocutoff**2)
26
+ elif shape=='square':
27
+ msk = (np.abs(fx)<aocutoff)*(np.abs(fy)<aocutoff)
28
+ else:
29
+ raise ValueError('Your mask shape is not available')
30
+ return msk
31
+
32
+
33
+ def aomask_out(*args, **kwargs):
34
+ """
35
+ Mask that is equal to 1 outside the correction area.
36
+ See `aomask_in` for input arguments.
37
+ """
38
+ return 1 - aomask_in(*args, **kwargs)
39
+
40
+
41
+ def psd_wvl_scaling(psd_1, w1_over_w2):
42
+ """
43
+ Scale a PSD [rad²m²] from one wavelength to another.
44
+ The frequecy step verifies df_2 = df_1 * w1_over_w2
45
+
46
+ Parameters
47
+ ----------
48
+ psd_1 : np.array
49
+ The PSD given at wvl_1
50
+ w1_over_w2 : float
51
+ Ratio wvl_1 / wvl_2
52
+ """
53
+ nx = psd_1.shape[0]
54
+ xx_1 = np.arange(nx)-nx//2
55
+ xx_2 = np.tile(xx_1 * w1_over_w2, (nx,1))
56
+ interp = RegularGridInterpolator((xx_1,xx_1), psd_1, bounds_error=False, fill_value=0)
57
+ psd_2 = interp((xx_2.T,xx_2)) * w1_over_w2**2
58
+ return psd_2
59
+
60
+
61
+ def psd_fitting(fx, fy, psdatmo, aocutoff, **kwargs):
62
+ """
63
+ Return the fitting error of the AO system.
64
+
65
+ Parameters
66
+ ----------
67
+ fx : np.array
68
+ The spatial frequencies on X [1/m].
69
+ fy : np.array
70
+ The spatial frequencies on Y [1/m].
71
+ psdatmo : np.array
72
+ The atmospherical PSD (for example Von-Karman) at the corresponding frequencies.
73
+ aocutoff : float
74
+ The AO cutoff frequency [1/m]
75
+ """
76
+ return psdatmo * aomask_out(fx, fy, aocutoff, **kwargs)
77
+
78
+
79
+ def psd_aliasing(fx, fy, aocutoff, var, df, **kwargs):
80
+ """
81
+ Return the aliasing error of the AO system.
82
+ The aliasing PSD is considered as constant on the corrected area.
83
+
84
+ Parameters
85
+ ----------
86
+ fx : np.array
87
+ The spatial frequencies on X [1/m].
88
+ fy : np.array
89
+ The spatial frequencies on Y [1/m].
90
+ aocutoff : float
91
+ The AO cutoff frequency [1/m].
92
+ var : float
93
+ The required aliasing variance [rad²].
94
+ df : float
95
+ Frequency step of the `freq` array [1/m].
96
+ """
97
+ # TODO: remove this function and use dedicated aliasing functions from SH-WFS and FF-WFS
98
+ print(DeprecationWarning('The generic function `psd_aliasing` is deprecated, use aliasing from shwfs or ffwfs instead.'))
99
+ mask = aomask_in(fx, fy, aocutoff, **kwargs)
100
+ f2 = fx**2 + fy**2
101
+ mask[np.where(f2==0)] = 0
102
+ psd = var*mask/(np.sum(mask)*df**2)
103
+ return psd
104
+
105
+
106
+ def psd_temporal(fx, fy, wspd, psdatmo, cltf, aocutoff=None, wnd_angle=0, **kwargs):
107
+ """
108
+ Compute servolag error of the AO system.
109
+ Computation is based on input PSD filtered by the ETF.
110
+
111
+ Parameters
112
+ ----------
113
+ fx : np.array
114
+ The spatial frequencies on X [1/m].
115
+ fy : np.array
116
+ The spatial frequencies on Y [1/m].
117
+ wspd : float
118
+ Turbulence equivalent windspeed [m/s].
119
+ psdatmo : np.array
120
+ The turbulent phase PSD [eg. rad2 m2].
121
+ cltf : callable
122
+ Closed-loop transfer function, to be evaluated on temporal frequencies (Hz).
123
+ aocutoff : float
124
+ The AO cutoff frequency [1/m].
125
+ """
126
+ ft = (fx*np.cos(wnd_angle)+fy*np.sin(wnd_angle))*wspd
127
+ ftnull = np.where(ft==0)
128
+ ft[ftnull] = 1e-8 # avoid numerical issue in f=0
129
+ etf2 = np.abs(cltf(ft))**2.0
130
+ if aocutoff is not None:
131
+ mask = aomask_in(fx, fy, aocutoff, **kwargs)
132
+ else:
133
+ mask = np.ones(fx.shape)
134
+ mask[ftnull] = 0
135
+ return psdatmo*etf2*mask
136
+
137
+
138
+ def air_refractive_index(wvl_um):
139
+ return 1 + 0.0579/(238.02-wvl_um**(-2)) + 0.0017/(57.4-wvl_um**(-2))
140
+
141
+
142
+ def psd_chromatic_filter(wvl_wfs_nm, wvl_sci_nm):
143
+ n_wfs = air_refractive_index(wvl_wfs_nm*1e-3)
144
+ n_sci = air_refractive_index(wvl_sci_nm*1e-3)
145
+ return (1-(n_sci-1)/(n_wfs-1))**2
146
+
147
+
148
+ def psd_ncpa(ncpa_rms, freq, df, diameter, ncpa_exp=2.2):
149
+ """
150
+ Compute the PSD from Non-Common Path Aberrations.
151
+
152
+ Parameters
153
+ ----------
154
+ ncpa_rms : float
155
+ RMS value of NCPA [unit]. Output PSD is given in [unit²m²].
156
+ freq : np.array
157
+ Array of spatial frequencies [1/m].
158
+ df : float
159
+ Frequency step [1/m].
160
+ diameter : float
161
+ Telescope diameter [m].
162
+ """
163
+ pstflt = piston_filter(freq, diameter)
164
+ ncpa = pstflt / (1e-8 + freq**ncpa_exp)
165
+ ncpa_total = np.sum(ncpa)*df**2
166
+ return ncpa * ncpa_rms**2 / ncpa_total
167
+
168
+
169
+ def psd_jitter(jitter_x_rms, jitter_y_rms, npix, samp):
170
+ """
171
+ Compute jitter PSD
172
+
173
+ Parameters
174
+ ----------
175
+ jitter_x_rms : float
176
+ RMS value [unit] of jitter along X. Output PSD is given in [unit²m²].
177
+ jitter_y_rms : float
178
+ RMS value [unit] of jitter along Y.
179
+ npix : int
180
+ Number of pixels of the output array.
181
+ samp : float
182
+ PSF sampling.
183
+ """
184
+ zx = np.abs(zernike_fourier(1, -1, npix, samp))**2
185
+ return zx * jitter_x_rms**2 + zx.T * jitter_y_rms**2
186
+
187
+
188
+ def psd_anisoplanetism(fx, fy, cn2dh, alt, wvl, theta_x, theta_y, lext=np.inf, lint=0, zenith=0):
189
+ """
190
+ Compute the anisoplanetism PSD, for a source located at infinite distance.
191
+
192
+ Parameters
193
+ ----------
194
+ fx : np.array(float)
195
+ Array of spatial frequencies on the X axis [1/m].
196
+ fy : np.array(float)
197
+ Array of spatial frequencies on the Y axis [1/m].
198
+ cn2dh : list(float)
199
+ List of the cn2*dh of the atmosphere layers [m^(1/3)].
200
+ alt : list(float)
201
+ List of the altitudes corresponding to cn2dh [m].
202
+ The zero altitude is the pupil of the telescope.
203
+ wvl : float
204
+ Wavelength of wavefront [m].
205
+ theta_x : float
206
+ Separation angle along the X coordinate [rad].
207
+ theta_y : float
208
+ Separation angle along the Y coordinate [rad].
209
+
210
+ Reference
211
+ ---------
212
+ Rigaut, 1998, SPIE Vol. 3353
213
+ """
214
+
215
+ psd = np.zeros(fx.shape)
216
+ psd_norm = phase_psd(np.sqrt(fx**2+fy**2), 1, lext=lext, lint=lint)
217
+
218
+ for i in range(len(alt)):
219
+ r0 = cn2dh_to_r0([cn2dh[i]], wvl, zenith=zenith)
220
+ psd += psd_norm * r0**(-5/3) * 2 * (1-np.cos(2*np.pi*alt[i]*(theta_x*fx+theta_y*fy)))
221
+
222
+ return psd
223
+
224
+
225
+ def psd_anisoplanetism_extended_object(fx, fy, cn2dh, alt, dpup, wvl, objsize, src_alt=np.inf, src_zenith=0, lext=np.inf, lint=0, return_all=False):
226
+ """
227
+ Return the phase, scintillation and coupling anisoplanetism PSD from the WFS measurement on extended object.
228
+ The result is a PSD array in units of rad²m², evaluated on [fx,fy].
229
+
230
+ Parameters
231
+ ----------
232
+ fx : np.array(float)
233
+ Array of spatial frequencies on the X axis [1/m].
234
+ fy : np.array(float)
235
+ Array of spatial frequencies on the Y axis [1/m].
236
+ cn2dh : list(float)
237
+ List of the cn2*dh of the atmosphere layers [m^(1/3)].
238
+ alt : list(float)
239
+ List of the altitudes corresponding to cn2dh [m].
240
+ The zero altitude is the pupil of the telescope.
241
+ dpup : float
242
+ Size of a subpupil of the WFS [m].
243
+ wvl : float
244
+ Wavelength of wavefront sensing [m].
245
+ objsize : float
246
+ Characterisitic apparent size of a square-like object [rad].
247
+ src_alt : float
248
+ Source altitude [m].
249
+ src_zenith : float
250
+ Source zenital angle [rad].
251
+ A zero angle means a source at zenith.
252
+ lext : float
253
+ Atmospheric turbulence external scale [m].
254
+ lint : float
255
+ Atmospheric turbulence internal scale [m].
256
+ return_all : boolean
257
+ Activate to return all the PSD terms.
258
+
259
+ Note
260
+ ----
261
+ Multiply the result by `(wvl_wfs/wvl_sci)**2` to get the PSD at the science wavelength.
262
+ Multiply the result by mask of AO corrected frequencies, see the function `aomask_in`.
263
+
264
+ Reference
265
+ ---------
266
+ Vedrenne et al, 2007, JOSAA.
267
+ """
268
+
269
+ nlayer = len(cn2dh)
270
+ npix = fx.shape[0]
271
+ k0 = 2*np.pi/wvl
272
+
273
+ freq = np.sqrt(fx**2+fy**2)
274
+ vldx = np.where(fx!=0)
275
+ vldy = np.where(fy!=0)
276
+
277
+ tfpup = np.sinc(fx*dpup)*np.sinc(fy*dpup) # ok with definition: np.sinc(x)=sinc(pi*x)
278
+
279
+ psd = np.zeros((nlayer,npix,npix))
280
+ FF = np.zeros((nlayer,npix,npix))
281
+ GG = np.zeros((nlayer,npix,npix))
282
+ HH = np.zeros((nlayer,npix,npix), dtype=complex)
283
+ Dx = np.zeros((nlayer,npix,npix), dtype=complex)
284
+ Dy = np.zeros((nlayer,npix,npix), dtype=complex)
285
+ EE = np.zeros((nlayer,npix,npix), dtype=complex)
286
+
287
+ xx,yy = np.mgrid[0:npix,0:npix] - npix//2
288
+
289
+ fconv = k0*dpup # facteur de conversion angle vers phase (cf. codes IDL)
290
+
291
+ for i in range(nlayer):
292
+ if (alt[i]>0) and (alt[i]<src_alt):
293
+ ze_z = 1/propagation_spherical_coord(alt[i], src_alt, backward=True)
294
+ ze = alt[i]*ze_z / np.cos(src_zenith)
295
+ u = np.pi*ze*wvl*freq**2
296
+ vk = vonkarmanshape(freq, lext=lext*ze_z, lint=lint*ze_z)
297
+ psd[i,...] = vk * k0**2 *0.033*cn2dh[i]*ze_z**(-5/3) * (2*np.pi)**(-2/3) # [eq A4]*dh
298
+ psd[i,...] = psd[i,...]/np.cos(src_zenith) # zenithal angle effect on cn2dh
299
+ FF[i,...] = 4*psd[i,...] * tfpup**2 * np.sin(u)**2 # [eq A3]
300
+ GG[i,...] = (2*np.pi*dpup)**2 * psd[i,...] * tfpup**2 * np.cos(u)**2 # [eq A7]
301
+ HH[i,...] = 2j*np.pi*dpup * psd[i,...] * tfpup**2 * np.sin(2*u) # [eq A9]/fconv car fconv porté par Dx
302
+ Dx[i,...][vldx] = 1j*fconv*np.sinc(ze*fy[vldx]*objsize)/(2*np.pi*ze*fx[vldx])*(np.cos(np.pi*ze*fx[vldx]*objsize)-np.sinc(ze*fx[vldx]*objsize))
303
+ Dy[i,...][vldy] = 1j*fconv*np.sinc(ze*fx[vldy]*objsize)/(2*np.pi*ze*fy[vldy])*(np.cos(np.pi*ze*fy[vldy]*objsize)-np.sinc(ze*fy[vldy]*objsize))
304
+ EE[i,...] = np.sinc(ze*fx*objsize)*np.sinc(ze*fy*objsize) - 1
305
+
306
+ psd_aa = {}
307
+ psd_aa["xx"] = np.sum(np.conjugate(Dx)*Dx*FF,axis=0)
308
+ psd_aa["xy"] = np.sum(np.conjugate(Dx)*Dy*FF,axis=0)
309
+ psd_aa["yy"] = np.sum(np.conjugate(Dy)*Dy*FF,axis=0)
310
+
311
+ psd_cc = {}
312
+ psd_cc["xx"] = np.sum(GG*np.abs(EE)**2,axis=0)*fx*fx
313
+ psd_cc["xy"] = np.sum(GG*np.abs(EE)**2,axis=0)*fx*fy
314
+ psd_cc["yy"] = np.sum(GG*np.abs(EE)**2,axis=0)*fy*fy
315
+
316
+ psd_acca = {}
317
+ psd_acca["xx"] = np.sum((fx*EE*np.conjugate(Dx)+fx*Dx*np.conjugate(EE))*HH,axis=0)
318
+ psd_acca["xy"] = np.sum((fx*EE*np.conjugate(Dy)+fy*Dx*np.conjugate(EE))*HH,axis=0)
319
+ psd_acca["yy"] = np.sum((fy*EE*np.conjugate(Dy)+fy*Dy*np.conjugate(EE))*HH,axis=0)
320
+
321
+ Mx = 2j*np.pi*fx*tfpup
322
+ My = 2j*np.pi*fy*tfpup
323
+
324
+ logging.debug('Measurement anisoplanetism has been scaled to match IDL codes.')
325
+ Mx *= dpup * np.pi/2 #FIXME : factor to match IDL codes
326
+ My *= dpup * np.pi/2 #FIXME : factor to match IDL codes
327
+
328
+ def reconstructor(psd):
329
+ """Slope PSD to phase PSD [eq B5]"""
330
+ denom = np.abs(Mx)**4 + np.abs(My)**4
331
+ vld = np.where(denom>0)
332
+ not_vld = np.where(denom==0)
333
+ psd_wfe = np.real(psd["xx"]*np.abs(Mx)**2 + psd["yy"]*np.abs(My)**2 + 2*np.conjugate(Mx)*My*psd["xy"])
334
+ psd_wfe[vld] /= denom[vld]
335
+ psd_wfe[not_vld] = 0
336
+ return psd_wfe
337
+
338
+ psd_wfe_aa = reconstructor(psd_aa)
339
+ psd_wfe_cc = reconstructor(psd_cc)
340
+ psd_wfe_acca = reconstructor(psd_acca)
341
+ psd_wfe_tot = psd_wfe_aa + psd_wfe_cc + psd_wfe_acca
342
+ if return_all:
343
+ return psd_wfe_tot, psd_wfe_aa, psd_wfe_cc, psd_wfe_acca
344
+ return psd_wfe_tot