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/trajectory.py ADDED
@@ -0,0 +1,120 @@
1
+
2
+ import numpy as np
3
+
4
+ def satellite_trajectory(altitude, max_elevation, npoints=4000, debut=0, fin=0, signe_debut=-1, signe_fin=1):
5
+ """
6
+ Compute satellite trajectory.
7
+
8
+ Authors
9
+ -------
10
+ Pierre-Louis Mayeur (ONERA), adapted from Cyril Petit (ONERA).
11
+ """
12
+ # Constantes
13
+ Rt = 6378.15e3 # Rayon de la Terre en mètres
14
+ Cste_grav = 6.6743e-11 # Constante gravitationnelle
15
+ Mt = 5.97e24 # Masse de la Terre
16
+ c = 299792458 # Vitesse de la lumière
17
+
18
+ # Calculs préliminaires
19
+ Rs = Rt + altitude
20
+ vsat = np.sqrt(Cste_grav * Mt / Rs) # Vitesse du satellite
21
+ thetapoint = vsat /Rs
22
+ # beta_max = np.degrees(np.arccos(Rt/Rs))
23
+
24
+ beta = np.arccos(Rt/Rs*np.cos(np.radians(max_elevation))) -np.radians(max_elevation) # au signe pres evidemment mais on s'en fiche
25
+ theta_max = np.arccos(Rt/(Rs*np.cos(beta))) # angle max de visibilité
26
+ theta_full = np.linspace(-theta_max, theta_max, npoints+1) # angle rotation satellite
27
+
28
+ #temps écoulé en seconde
29
+ time_full = (theta_full - np.min(theta_full)) / thetapoint
30
+
31
+ #distance à la cible. formule validée par rapport à la litterature.
32
+ distance_full = np.sqrt(Rs**2 + Rt**2 - 2 * Rs * Rt * np.cos(theta_full) * np.cos(beta))
33
+
34
+ # elevation check formule.
35
+ value = (Rs * np.cos(theta_full) * np.cos(beta) - Rt) / distance_full
36
+ clipped_value = np.clip(value, -1., 1.) # Assure la valeur est dans [-1, 1]
37
+ elev_full = np.arcsin(clipped_value)
38
+
39
+
40
+ # ici on se laisse la possibilité de voir une trajectoire seulement à la montée ou descente.
41
+ if signe_debut == -1:
42
+ # Trouve les indices où la condition est vraie pour les valeurs négatives de theta_full
43
+ idx_theta_neg = np.where(np.sign(theta_full) == signe_debut)[0]
44
+
45
+ # Trouve les indices où l'élévation est supérieure ou égale au debut (en radians)
46
+ idx_elev_sup_debut = np.where(elev_full[idx_theta_neg] >= debut * np.pi / 180)[0]
47
+
48
+ # Si des indices sont trouvés, calcule indice_debut
49
+ if len(idx_elev_sup_debut) > 0:
50
+ indice_debut = idx_theta_neg[np.min(idx_elev_sup_debut)] + np.min(np.where(np.sign(theta_full) == signe_debut)[0])
51
+ else:
52
+ print("Erreur elevation de debut jamais atteinte : sortie")
53
+ else:
54
+ # Trouve les indices où la condition est vraie pour les valeurs positives de theta_full
55
+ idx_theta_pos = np.where(np.sign(theta_full) == signe_debut)[0]
56
+
57
+ # Trouve les indices où l'élévation est supérieure ou égale au debut (en radians)
58
+ idx_elev_sup_debut = np.where(elev_full[idx_theta_pos] >= debut * np.pi / 180)[0]
59
+
60
+ # Si des indices sont trouvés, calcule indice_debut
61
+ if len(idx_elev_sup_debut) > 0:
62
+ indice_debut = idx_theta_pos[np.max(idx_elev_sup_debut)] + np.min(np.where(np.sign(theta_full) == signe_debut)[0])
63
+ else:
64
+ print("Erreur elevation de debut jamais atteinte : sortie")
65
+
66
+ if signe_fin == -1:
67
+ # Trouve les indices où la condition est vraie pour les valeurs négatives de theta_full
68
+ idx_theta_neg = np.where(np.sign(theta_full) == signe_fin)[0]
69
+
70
+ # Trouve les indices où l'élévation est supérieure ou égale à la fin (en radians)
71
+ idx_elev_sup_fin = np.where(elev_full[idx_theta_neg] >= fin * np.pi / 180)[0]
72
+
73
+ # Si des indices sont trouvés, calcule indice_fin
74
+ if len(idx_elev_sup_fin) > 0:
75
+ indice_fin = idx_theta_neg[np.min(idx_elev_sup_fin)] + np.min(np.where(np.sign(theta_full) == signe_fin)[0])
76
+ else:
77
+ print("Erreur elevation de fin jamais atteinte : sortie")
78
+ else:
79
+ # Trouve les indices où la condition est vraie pour les valeurs positives de theta_full
80
+ idx_theta_pos = np.where(np.sign(theta_full) == signe_fin)[0]
81
+
82
+ # Trouve les indices où l'élévation est supérieure ou égale à la fin (en radians)
83
+ idx_elev_sup_fin = np.where(elev_full[idx_theta_pos] >= fin * np.pi / 180)[0]
84
+
85
+ # Si des indices sont trouvés, calcule indice_fin
86
+ if len(idx_elev_sup_fin) > 0:
87
+ indice_fin = idx_theta_pos[np.max(idx_elev_sup_fin)] + np.min(np.where(np.sign(theta_full) == signe_fin)[0])
88
+ else:
89
+ print("Erreur elevation de fin jamais atteinte : sortie")
90
+
91
+ if indice_fin < indice_debut:
92
+ print("Problème: la position de fin est avant la position début : sortie")
93
+
94
+ time = time_full[indice_debut:indice_fin+1]
95
+ elevation = elev_full[indice_debut:indice_fin+1]
96
+ theta = theta_full[indice_debut:indice_fin+1]
97
+ distance = distance_full[indice_debut:indice_fin+1]
98
+ npoints = len(time)
99
+
100
+ azim = np.arccos(np.sin(beta) / np.sqrt(np.sin(beta)**2 + (np.sin(theta)**2) * (np.cos(beta)**2))) * np.sign(theta)
101
+ if beta == 0:
102
+ test = np.where(theta == 0)[0]
103
+ if len(test) > 0:
104
+ azim[test] = 0.0
105
+
106
+ Vpar = -vsat / distance * Rt * np.sin(theta) * np.cos(beta)
107
+ Vorth = vsat * np.sqrt(1.0 - (Rt * np.sin(theta) * np.cos(beta) / distance)**2)
108
+ # norm_v = np.sqrt(Vpar**2 + Vorth**2)
109
+ slew_rate = Vorth / distance
110
+
111
+ vazim = vsat * (np.sin(theta) * np.sin(beta)*np.sin(azim) + np.cos(theta)*np.cos(azim) + np.sin(theta)*np.cos(beta)*0)
112
+ velev = vsat * (np.sin(theta) * np.sin(beta)*np.sin(elevation)*np.cos(azim) - np.cos(theta)*np.sin(elevation)*np.sin(azim) - np.sin(theta)*np.cos(beta)*np.cos(elevation))
113
+ slew_rate_elev = velev / distance
114
+ slew_rate_azim = vazim / (distance * np.cos(elevation))
115
+
116
+ paa = np.zeros((2, len(velev))) # Crée un tableau de zéros avec la bonne forme
117
+ paa[0, :] = 2 * distance * slew_rate / c * vazim / Vorth
118
+ paa[1, :] = 2 * distance * slew_rate / c * velev / Vorth
119
+
120
+ return time, distance, elevation, azim, Vpar, Vorth, slew_rate_elev,slew_rate_azim, velev,vazim, paa
aopera/turbulence.py ADDED
@@ -0,0 +1,445 @@
1
+ """
2
+ Set of functions related to turbulence
3
+ """
4
+
5
+ import numpy as np
6
+ import logging
7
+ from scipy.special import j1
8
+ from aopera.utils import rad2arcsec, arcsec2rad
9
+ from aopera.readconfig import read_config_file, read_config_tiptop, set_attribute, INFO_ATMO_CN2DH
10
+
11
+
12
+ WVL_REF_SEEING = 500e-9
13
+
14
+
15
+ def random_noise(shp):
16
+ """Compute a complex noise for a given shp=(nx,ny). See also `random_sample`."""
17
+ # fft2(randn(*sh)) / npix * np.sqrt(2) # other method
18
+ return np.random.randn(*shp)+1j*np.random.randn(*shp)
19
+
20
+
21
+ def random_sample(psd, L, noise=None, t_vxy=None):
22
+ """
23
+ Generate a random screen from a given PSD.
24
+
25
+ Parameters
26
+ ----------
27
+ psd : np.array
28
+ The PSD array, with null frequency at the center of the array.
29
+ L : float
30
+ Physical extent of the array.
31
+
32
+ Keywords
33
+ --------
34
+ noise : None or np.array
35
+ Define an array if you want to use the same generating noise.
36
+ See also `random_noise`.
37
+ t_vxy : (float,float)
38
+ The list of (time*Vx,time*Vy) to get frozen flow translation.
39
+ """
40
+ #np.random.seed(seed=seed) # if you want to use same seed...
41
+ if noise is None:
42
+ noise = random_noise(np.shape(psd))
43
+ tab = np.fft.fftshift(np.sqrt(psd)) * noise / L
44
+ if t_vxy is not None:
45
+ if max(t_vxy)>(L/2):
46
+ logging.warning('Time exceeds array size, random sample will suffer circular roll.')
47
+ nx,ny = np.shape(psd)
48
+ xx,yy = np.mgrid[0:nx,0:ny] * 1.0
49
+ xx = xx - nx//2
50
+ yy = yy - ny//2
51
+ tab *= np.exp(2j*np.pi*(t_vxy[0]*xx+t_vxy[1]*yy)/L)
52
+ tab = np.real(np.fft.ifft2(tab)) * np.size(psd)
53
+ return tab
54
+
55
+
56
+ def propagation_spherical_coord(h, L, backward=False):
57
+ """
58
+ Compute the spherical reduced coordinate
59
+
60
+ If the distance is np.inf (plane wave), then return an array of ones like `h`.
61
+
62
+ Parameters
63
+ ----------
64
+ h : np.array
65
+ Array of the coordinates along the propagation path.
66
+ L : float
67
+ Distance between source and observer.
68
+
69
+ Keywords
70
+ --------
71
+ backward : bool (default=False)
72
+ If False, coordinates are increasing from source to observer.
73
+ If True, coordinates are increasing from observer to the source (e.g. altitudes).
74
+
75
+ Reference
76
+ ---------
77
+ R. Sasiela, 1995, Electromagnetic wave propagation in turbulence (Chapter 2)
78
+ """
79
+ if L==np.inf:
80
+ return np.ones_like(h)
81
+ if backward:
82
+ u = (L-h)/L
83
+ else:
84
+ u = h/L
85
+ if (np.min(u)<0) or (np.max(u)>1):
86
+ raise ValueError("Incompatible values between array of coordinates and source distance. You must ensure 0<=h<=L")
87
+ return u
88
+
89
+
90
+ def cn2dh_to_r0sph(alt, cn2dh, wvl, src_alt, zenith=0):
91
+ """
92
+ Compute the spherical r0 from a Cn²*dh profile.
93
+
94
+ Parameters
95
+ ----------
96
+ alt : np.array
97
+ Values of altitudes corresponding to the `cn2dh` [m].
98
+ cn2dh : np.array
99
+ Values of the Cn²*dh profile at the different altitudes [m^(1/3)].
100
+ wvl : float
101
+ Observing wavelength [m].
102
+ src_alt : float, int
103
+ Altitude of the source [m].
104
+
105
+ Keywords
106
+ --------
107
+ zenith : float (default=0 for zenith)
108
+ Zenital angle in the range [-pi/2,pi/2] radians.
109
+
110
+ Reference
111
+ ---------
112
+ T. Fusco, 2000, PhD thesis, eq.1.10 (for plane wave only)
113
+ """
114
+ cosz = np.cos(zenith)
115
+ if cosz<=0:
116
+ raise ValueError("Zenital angle must be between -pi/2 and pi/2")
117
+ if len(cn2dh)!=len(alt):
118
+ raise ValueError("`cn2dh` and `alt` must have same number of elements")
119
+ k2 = (2*np.pi/wvl)**2.0
120
+ u = propagation_spherical_coord(alt, src_alt, backward=True)
121
+ return (k2*0.42/cosz*np.sum(cn2dh * u**(5./3.)))**(-3./5.)
122
+
123
+
124
+ def cn2dh_to_r0(cn2dh, wvl, zenith=0):
125
+ """
126
+ Compute the r0 from a Cn²*dh profile.
127
+
128
+ Parameters
129
+ ----------
130
+ cn2dh : float, list, tuple or np.array
131
+ Values of the Cn²*dh profile at the different altitudes [m^(1/3)].
132
+ wvl : float
133
+ Observing wavelength [m].
134
+
135
+ Keywords
136
+ --------
137
+ zenith : float (default=0 for zenith)
138
+ Zenital angle in the range [-pi/2,pi/2] radians.
139
+
140
+ Note
141
+ ----
142
+ This function is just a wrapper around `cn2dh_to_r0sph` for a source
143
+ located at infinite altitude.
144
+ """
145
+ src_alt = np.inf
146
+ alt = np.zeros_like(cn2dh)
147
+ return cn2dh_to_r0sph(alt, cn2dh, wvl, src_alt, zenith=zenith)
148
+
149
+
150
+ def equivalent_altitude(cn2dh, altitude):
151
+ """Get the equivalent altitude from a profile"""
152
+ return (np.sum(cn2dh*(altitude)**(5/3))/np.sum(cn2dh))**(3/5)
153
+
154
+
155
+ def equivalent_wind_speed(cn2dh, wind_speed):
156
+ """Get the equivalent wind speed from a profile"""
157
+ return (np.sum(cn2dh*(wind_speed)**(5/3))/np.sum(cn2dh))**(3/5)
158
+
159
+
160
+ def coherence_time(r0, wspd):
161
+ """Coherence time of turbulence (often called tau_0)"""
162
+ return 0.314*r0/wspd
163
+
164
+
165
+ def isoplanetic_angle(r0, alt):
166
+ """Isoplanetic angle of turbulence (often called theta_0)"""
167
+ return 0.314*r0/alt
168
+
169
+
170
+ def r0_to_cn2dh(r0, wvl, zenith=0):
171
+ """
172
+ Compute the equivalent mono-layer Cn²*dh from the r0 value.
173
+
174
+ Parameters
175
+ ----------
176
+ r0 : float
177
+ Fried parameter [m].
178
+ wvl : float
179
+ Observing wavelength [m].
180
+
181
+ Keywords
182
+ --------
183
+ zenith : float (default=0 for zenith)
184
+ Zenital angle in the range [-pi/2,pi/2] radians.
185
+ """
186
+ cosz = np.cos(zenith)
187
+ if cosz<=0:
188
+ raise ValueError("Zenital angle must be between -pi/2 and pi/2")
189
+ k2 = (2*np.pi/wvl)**2.0
190
+ return r0**(-5./3.) * cosz/0.42/k2
191
+
192
+
193
+ def seeing_to_cn2dh(seeing):
194
+ """Compute Cn2*dh (mono-layer) for a given seeing [arcsec]"""
195
+ r0_ref = WVL_REF_SEEING / arcsec2rad(seeing)
196
+ return r0_to_cn2dh(r0_ref, WVL_REF_SEEING)
197
+
198
+
199
+ def cn2dh_to_seeing(cn2dh):
200
+ """Compute the seeing [arcsec] from a Cn2*dh profile"""
201
+ r0_ref = cn2dh_to_r0(cn2dh, WVL_REF_SEEING)
202
+ return rad2arcsec(WVL_REF_SEEING/r0_ref)
203
+
204
+
205
+ def r0_to_seeing(r0, wvl, zenith=0):
206
+ """Convert r0 [m] to seeing [arcsec]"""
207
+ cn2dh = r0_to_cn2dh(r0, wvl, zenith=zenith)
208
+ return cn2dh_to_seeing([cn2dh])
209
+
210
+
211
+ def piston_filter(freq, D):
212
+ """
213
+ Filter piston mode from a PSD.
214
+ Assumes a non-obstructed circular pupil.
215
+
216
+ Parameters
217
+ ----------
218
+ freq : np.array
219
+ The spatial frequencies [1/m].
220
+ D : float
221
+ Diameter of the telescope [m].
222
+
223
+ Reference
224
+ ---------
225
+ https://github.com/oliviermartin-lam/P3/blob/main/aoSystem/FourierUtils.py
226
+ """
227
+ ff = np.pi*D*freq
228
+ out = np.zeros_like(ff)
229
+ idx = (ff!=0)
230
+ out[idx] = 1 - 4*(j1(ff[idx])/ff[idx])**2
231
+ return out
232
+
233
+
234
+ def vonkarmanshape(freq, lext=np.inf, lint=0.0, diameter=None):
235
+ """
236
+ Return the Von-Karman shape without normalisation multiplicative factors.
237
+
238
+ If you provide `k=2*pi*f` instead of an array of frequencies, the `lext` and `lint`
239
+ parameters must be divided by (2*pi) since they are often given in frequency units
240
+ and not pulsation units.
241
+
242
+ Parameters
243
+ ----------
244
+ freq : np.array
245
+ The spatial frequencies [1/m].
246
+
247
+ Keywords
248
+ --------
249
+ lext : float
250
+ Von-Karman external scale [m].
251
+ lint : float
252
+ Modified Von-Karman internal scale [m].
253
+ diameter : None or float
254
+ Pupil diameter, to filter piston from Von-Karman PSD
255
+ """
256
+ if lext<lint:
257
+ raise ValueError("Von-Karman external scale cannot be smaller than internal scale")
258
+ idx_null = np.where(freq==0)
259
+ freq[idx_null] = np.finfo(float).eps
260
+ vks = (freq**2 + (1/lext)**2)**(-11./6.) * np.exp(-(freq*lint)**2.0)
261
+ freq[idx_null] = 0
262
+ vks[idx_null] = 0
263
+ if diameter is not None:
264
+ vks *= piston_filter(freq, diameter)
265
+ return vks
266
+
267
+
268
+ def phase_psd(freq, r0, **kwargs):
269
+ """
270
+ Compute the turbulent phase spatial PSD.
271
+ By default, the Kolmogorov spectrum (lext=inf,lint=0) is generated.
272
+
273
+ Parameters
274
+ ----------
275
+ freq : float, np.array
276
+ Spatial frequencies for which to compute the phase PSD [1/m].
277
+ r0 : float
278
+ Fried parameter [m].
279
+
280
+ Keywords
281
+ --------
282
+ See 'vonkarmanshape'
283
+ """
284
+ return 0.023*r0**(-5./3.) * vonkarmanshape(freq, **kwargs)
285
+
286
+
287
+ def logamp_psd(freq, alt, cn2dh, wvl, lext=np.inf, lint=0.0, src_alt=np.inf, zenith=0):
288
+ """
289
+ Compute the log-amplitude spatial PSD.
290
+ By default, the Kolmogorov spectrum (lext=inf,lint=0) is generated.
291
+
292
+ Parameters
293
+ ----------
294
+ freq : float, np.array
295
+ Spatial frequencies for which to compute the phase PSD [1/m].
296
+ alt : list, tuple or np.array
297
+ Values of altitudes corresponding to the `cn2dh` [m].
298
+ cn2dh : np.array
299
+ Values of the Cn²*dh profile at the different altitudes [m^(1/3)].
300
+ wvl : float
301
+ Observing wavelength [m].
302
+
303
+ Keywords
304
+ --------
305
+ lext : float, np.inf (default=np.inf)
306
+ External scale [m].
307
+ lint : float (default=0.0)
308
+ Internal scale [m].
309
+ zenith : float (default=0)
310
+ Zenith angle [rad].
311
+ """
312
+ #if src_alt<_np.inf: raise ValueError("The code has not been debugged for spherical wave (src_alt<np.inf). Error in the Mahe SPIE article according to JM.Conan")
313
+ omega = 2*np.pi*freq
314
+ if len(alt)!=len(cn2dh):
315
+ raise ValueError("Arrays `cn2dh` and `alt` must have same number of elements")
316
+ k0 = 2*np.pi/wvl
317
+ factor = 0.207 * (k0**2) * (4*np.pi**2) # last factor to transform pulsation to frequency
318
+ res = np.zeros_like(omega)
319
+ u = propagation_spherical_coord(alt, src_alt, backward=True)
320
+ if src_alt==np.inf:
321
+ vonk = vonkarmanshape(omega,lext=lext/(2*np.pi),lint=lint/(2*np.pi)) # compute once to save time
322
+ for i in range(len(alt)):
323
+ if u[i]!=0: # not at the source because of terms in 1/u, but PSD tends mathematically to zero
324
+ if src_alt<np.inf:
325
+ vonk = vonkarmanshape(omega/u[i],lext=lext/(2*np.pi),lint=lint/(2*np.pi)) # compute in loop
326
+ sin2 = np.sin((alt[i]/np.cos(zenith)/u[i])*omega**2/(2*k0))**2
327
+ res += cn2dh[i] * vonk / (u[i]**2) * sin2
328
+ return factor * res
329
+
330
+
331
+ def logamp_variance(alt, cn2dh, wvl, src_alt=np.inf, zenith=0):
332
+ """
333
+ Log-amplitude variance, assuming Kolmogorov spectrum.
334
+
335
+ Reference
336
+ ---------
337
+ Sasiela, Electromagnetic wave propagation in turbulence, Eq. 2.128
338
+ """
339
+ k0 = 2*np.pi/wvl
340
+ u = propagation_spherical_coord(alt, src_alt, backward=True)
341
+ return 0.5631 * k0**(7/6) * np.sum(cn2dh*(u*alt/np.cos(zenith))**(5/6))
342
+
343
+
344
+ def check_psd_resolution(samp, nx, D, lext=np.inf, lint=0):
345
+ """Check if Von-Karman PSD is numerically consistent."""
346
+ error = False
347
+ df = 1/(samp*D) # numerical frequency step [1/m]
348
+
349
+ if (1/lext < df) and (1/D < df):
350
+ logging.warning('The numerical `df` step is the external scale. Consider increasing `samp`.')
351
+
352
+ if lint < 1/(df*nx/2):
353
+ logging.warning('The array size is the internal scale. Consider increasing `nx` or decreasing `samp`.')
354
+
355
+ return error
356
+
357
+ #%% ATMOSPHERE CLASS
358
+ class Atmosphere:
359
+ """
360
+ Representation of a turbulent atmosphere.
361
+ So far, it can only compute parameters for a source located at infinity.
362
+ """
363
+
364
+ def __init__(self, dictionary):
365
+ self.lint = 0
366
+ if not 'seeing' in dictionary.keys():
367
+ cn2dh = INFO_ATMO_CN2DH['cn2dh'][0](dictionary['cn2dh'])
368
+ dictionary['seeing'] = str(cn2dh_to_seeing(cn2dh))
369
+ dictionary['cn2dh_ratio'] = str(list(cn2dh/np.sum(cn2dh)))
370
+ set_attribute(self, dictionary, 'atmosphere_seeing')
371
+ if np.abs(np.sum(self.cn2dh_ratio)-1) > 0.01:
372
+ raise ValueError('sum(cn2dh_ratio) should be close to unity.')
373
+
374
+ def __repr__(self):
375
+ s = 'aopera.ATMOSPHERE\n'
376
+ s += '------------------\n'
377
+ s += 'seeing : %.2f arcsec\n'%self.seeing
378
+ s += 'nb layer : %u\n'%self.nlayer
379
+ s += 'altitude : %.1f km\n'%(self.equivalent_altitude*1e-3)
380
+ s += 'wind speed: %.1f m/s\n'%self.equivalent_wind_speed
381
+ return s
382
+
383
+ @staticmethod
384
+ def from_file(filepath, category='atmosphere'):
385
+ return Atmosphere(read_config_file(filepath, category))
386
+
387
+ @staticmethod
388
+ def from_file_tiptop(filepath):
389
+ return Atmosphere(read_config_tiptop(filepath)[1])
390
+
391
+ @staticmethod
392
+ def from_oopao(atm):
393
+ return Atmosphere({'lext':atm.L0,
394
+ 'altitude':np.array(atm.altitude),
395
+ 'wind_speed':np.array(atm.windSpeed),
396
+ 'wind_direction':np.array(atm.windDirection),
397
+ 'seeing':r0_to_seeing(atm.r0, atm.wavelength, zenith=0),
398
+ 'cn2dh_ratio':np.array(atm.fractionalR0)})
399
+
400
+ @property
401
+ def nlayer(self):
402
+ return len(self.cn2dh_ratio)
403
+
404
+ @property
405
+ def cn2dh(self):
406
+ return seeing_to_cn2dh(self.seeing) * self.cn2dh_ratio
407
+
408
+ @property
409
+ def equivalent_altitude(self):
410
+ return equivalent_altitude(self.cn2dh, self.altitude)
411
+
412
+ @property
413
+ def equivalent_wind_speed(self):
414
+ return equivalent_wind_speed(self.cn2dh, self.wind_speed)
415
+
416
+ @property
417
+ def squeeze_layers(self):
418
+ veq = self.equivalent_wind_speed
419
+ heq = self.equivalent_altitude
420
+ self.cn2dh_ratio = np.array([1.0])
421
+ self.wind_speed = np.array([veq])
422
+ self.wind_direction = np.array([0])
423
+ self.altitude = np.array([heq])
424
+
425
+ def r0(self, wvl, zenith=0):
426
+ return cn2dh_to_r0(self.cn2dh, wvl, zenith=zenith)
427
+
428
+ def coherence_time(self, *args, **kwargs):
429
+ r0 = self.r0(*args, **kwargs)
430
+ return coherence_time(r0, self.equivalent_wind_speed)
431
+
432
+ def isoplanetic_angle(self, *args, **kwargs):
433
+ r0 = self.r0(*args, **kwargs)
434
+ return isoplanetic_angle(r0, self.equivalent_altitude)
435
+
436
+ def phase_psd(self, freq, wvl, zenith=0):
437
+ r0 = self.r0(wvl, zenith=zenith)
438
+ return phase_psd(freq, r0, lext=self.lext, lint=self.lint)
439
+
440
+ def opd_psd(self, freq, zenith=0):
441
+ wvl = 500e-9 # whatever!
442
+ return self.phase_psd(freq, wvl, zenith=zenith) * (wvl/(2*np.pi))**2
443
+
444
+ def logamp_psd(self, freq, wvl, zenith=0):
445
+ return logamp_psd(freq, self.altitude, self.cn2dh, wvl, lext=self.lext, lint=self.lint, zenith=zenith)
aopera/utils.py ADDED
@@ -0,0 +1,142 @@
1
+ """
2
+ Some useful functions to run the library
3
+ """
4
+
5
+ import numpy as np
6
+
7
+
8
+ _RAD2ARCSEC = 180/np.pi * 3600
9
+
10
+
11
+ def rad2arcsec(rad):
12
+ """Convert radians to arcsec"""
13
+ return rad * _RAD2ARCSEC
14
+
15
+
16
+ def arcsec2rad(arcsec):
17
+ """Convert arcsec to radians"""
18
+ return arcsec / _RAD2ARCSEC
19
+
20
+
21
+ def polar(npix, center=None):
22
+ """Compute polar coordinates (rho[pix], theta[rad])"""
23
+ if center is None:
24
+ center = npix//2
25
+ xx,yy = (np.mgrid[0:npix,0:npix] - center)
26
+ return np.sqrt(xx**2 + yy**2), np.arctan2(yy, xx)
27
+
28
+
29
+ def polar_radius(*args, **kwargs):
30
+ """Compute the polar radius coordinate rho [pix]."""
31
+ return polar(*args, **kwargs)[0]
32
+
33
+
34
+ def circavg(tab, center=None, rmax=None):
35
+ """Compute the circular average of a given array
36
+
37
+ Parameters
38
+ ----------
39
+ tab : numpy.ndarray (dim=2)
40
+ Two-dimensional array to compute its circular average
41
+
42
+ Returns
43
+ -------
44
+ vec : numpy.ndarray (dim=1)
45
+ Vector containing the circular average from center
46
+ """
47
+ if tab.ndim != 2:
48
+ raise ValueError("Input `tab` should be a 2D array")
49
+ rr = polar_radius(max(tab.shape), center=center)
50
+ if rmax is None:
51
+ rmax = rr.max()
52
+ avg = np.zeros(int(rmax), dtype=tab.dtype)
53
+ for i in range(int(rmax)):
54
+ index = np.where((rr >= i) * (rr < (i + 1)))
55
+ avg[i] = tab[index[0], index[1]].sum() / index[0].size
56
+ return avg
57
+
58
+
59
+ def circsum(tab, center=None, rmax=None, backward=False):
60
+ """Compute circular sum of a 2D array"""
61
+ if tab.ndim != 2:
62
+ raise ValueError("Input `tab` should be a 2D array")
63
+ rr = polar_radius(max(tab.shape), center=center)
64
+ if rmax is None:
65
+ rmax = rr.max()
66
+ csum = np.zeros(int(rmax), dtype=tab.dtype)
67
+ for i in range(int(rmax)):
68
+ if not backward:
69
+ index = np.where(rr < (i+1))
70
+ else:
71
+ index = np.where(rr >= (i+1))
72
+ csum[i] = tab[index[0], index[1]].sum()
73
+ return csum
74
+
75
+
76
+ def aperture(npix, samp=1, occ=0, center=None):
77
+ """
78
+ Create a circular aperture
79
+
80
+ Parameters
81
+ ----------
82
+ npix : float
83
+ The size of the output array will be (npix,npix).
84
+
85
+ Keywords
86
+ --------
87
+ samp : float
88
+ Sampling required for the computations.
89
+ samp=1 means a disc tangent to the array.
90
+ default = 1.
91
+ occ : float
92
+ Occultation ratio (0<=occ<=1).
93
+ default = 0.
94
+ center : int, float, None
95
+ Position of the center of the array.
96
+ """
97
+ rho = polar_radius(npix, center=center)
98
+ aper = rho <= (npix/2/samp)
99
+ if occ>0:
100
+ aper *= rho >= (npix/2/samp*occ)
101
+ return aper
102
+
103
+
104
+ def print_var(var):
105
+ """
106
+ Formatted console print of AO variances budget
107
+
108
+ Parameter
109
+ ---------
110
+ var : dict
111
+ Dictionary of variances
112
+ """
113
+ var_total = sum([var[j] for j in var.keys()])
114
+ nsq = 20
115
+ print('-'*(16+2+nsq))
116
+ for k in sorted(var.keys(), key=lambda k:-var[k]):
117
+ nbstar = int(round(nsq*var[k]/var_total))
118
+ print('%8s %4.2f'%(k,var[k]) + ' |' + '\u25a0'*nbstar + ' '*(nsq-nbstar)+'|')
119
+ print()
120
+ print('%8s %4.2f'%('total',var_total))
121
+ print('-'*(16+2+nsq))
122
+
123
+
124
+ def print_std(std):
125
+ """
126
+ Formatted console print of AO WFE budget
127
+
128
+ Parameter
129
+ ---------
130
+ std : dict
131
+ Dictionary of std of WFE
132
+ """
133
+ var_total = sum([std[j]**2 for j in std.keys()])
134
+ nsq = 23
135
+ #print('-'*(17+2+nsq))
136
+ print('-'*17 + '\u25bc' + ('-'*(nsq//4) + '\u25bc')*4 )
137
+ for k in sorted(std.keys(), key=lambda k:-std[k]):
138
+ nbstar = int(round(nsq*std[k]**2/var_total)) # weighting is given by variance, and not std!
139
+ print('%10s %4u'%(k,round(std[k])) + ' |' + '\u25a0'*nbstar + ' '*(nsq-nbstar)+'|')
140
+ print()
141
+ print('%10s %4u'%('total',round(np.sqrt(var_total))))
142
+ print('-'*(17+2+nsq))