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