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/variance.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analytical formulas for AO terms variances
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
def var_noll(r0,D):
|
|
9
|
+
"""
|
|
10
|
+
Compute the Noll phase variance [rad²], corresponding to the
|
|
11
|
+
expected error without correction.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
r0 : float (>0)
|
|
16
|
+
Fried parameter [m].
|
|
17
|
+
D : float (>0)
|
|
18
|
+
Pupil diameter [m].
|
|
19
|
+
|
|
20
|
+
References
|
|
21
|
+
----------
|
|
22
|
+
Robert J. Noll, "Zernike polynomials and atmospheric turbulence*,"
|
|
23
|
+
J. Opt. Soc. Am. 66, 207-211 (1976)
|
|
24
|
+
"""
|
|
25
|
+
return 1.03*(D/r0)**(5./3.)
|
|
26
|
+
|
|
27
|
+
def var_fitting(r0, freq_cutoff, dmtype='continuous', lint=0):
|
|
28
|
+
"""
|
|
29
|
+
Compute the variance [rad²] of the AO fitting error.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
r0 : float (>0)
|
|
34
|
+
Fried parameter [m].
|
|
35
|
+
freq_cutoff : float (>0)
|
|
36
|
+
AO cutoff frequency [1/m], typically 1/(2*pitch).
|
|
37
|
+
dmtype : string ('continuous','piston','ptt')
|
|
38
|
+
DM segmentation type.
|
|
39
|
+
lint : float (>=0)
|
|
40
|
+
Turbulence internal scale [m].
|
|
41
|
+
|
|
42
|
+
References
|
|
43
|
+
----------
|
|
44
|
+
Thierry Fusco (ONERA), year?, internal document, "Budget OA.docx".
|
|
45
|
+
VERSO team (ONERA), 2021, internal document, "RF-VERSOBC3.docx".
|
|
46
|
+
"""
|
|
47
|
+
#TODO: use actuator geometry (Neichel PhD, p148, sec 6.6.3)
|
|
48
|
+
# coef = 0.232 (square) or 0.275 (round) or 0.2 (hexa)
|
|
49
|
+
# below: dmCoef['continuous'] * 2**(5/3) corresponds to round shape
|
|
50
|
+
dmCoef = {'continuous':0.023*6*np.pi/5,
|
|
51
|
+
'ptt':0.18 * 2**(-5./3.),
|
|
52
|
+
'piston':1.26 * 2**(-5./3.)}
|
|
53
|
+
if dmtype not in dmCoef.keys():
|
|
54
|
+
msg = 'Requested <dmtype> has not been implemented'
|
|
55
|
+
logging.error(msg)
|
|
56
|
+
raise ValueError(msg)
|
|
57
|
+
var = dmCoef[dmtype] * (r0*freq_cutoff)**(-5./3.) # fittign error is due to the minimal cutoff between wfs and dm
|
|
58
|
+
var -= dmCoef[dmtype] * (lint/r0)**(5./3.) # remove internal scale
|
|
59
|
+
return var
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def var_aliasing(*args, aliasing=0.35, **kwargs):
|
|
63
|
+
"""
|
|
64
|
+
Compute the variance [rad²] of the AO aliasing error.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
*args : see `aovar_fitting`
|
|
69
|
+
aliasing : float (>=0)
|
|
70
|
+
The variance aliasing factor (typically=0.35 for SH).
|
|
71
|
+
**kwargs : see `aovar_fitting`
|
|
72
|
+
|
|
73
|
+
References
|
|
74
|
+
----------
|
|
75
|
+
Thierry Fusco (ONERA), year?, internal document, "Budget OA.docx".
|
|
76
|
+
VERSO team (ONERA), 2021, internal document, "RF-VERSOBC3.docx".
|
|
77
|
+
"""
|
|
78
|
+
return aliasing * var_fitting(*args, **kwargs)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def var_temporal(D, r0, nradial, windspeed, bandwidth):
|
|
82
|
+
"""
|
|
83
|
+
Compute the variance [rad²] of the AO temporal error.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
D : float (>0)
|
|
88
|
+
Pupil diameter [m].
|
|
89
|
+
r0 : float (>0)
|
|
90
|
+
Fried parameter [m].
|
|
91
|
+
nradial : int (>0)
|
|
92
|
+
Number of radial modes corrected by the AO system.
|
|
93
|
+
windspeed : float (>0)
|
|
94
|
+
turbulence equivalent windspeed [m/s].
|
|
95
|
+
bandwidth : float (>0)
|
|
96
|
+
AO loop cutoff bandwidth [Hz].
|
|
97
|
+
|
|
98
|
+
References
|
|
99
|
+
----------
|
|
100
|
+
Thierry Fusco (ONERA), year?, internal document, "Budget OA.docx".
|
|
101
|
+
Thierry Fusco (ONERA), 2005, internal document, "AO temporal behavior".
|
|
102
|
+
Jean-Marc Conan, 1994, PhD thesis, section 2.2.4.3
|
|
103
|
+
|
|
104
|
+
Note
|
|
105
|
+
----
|
|
106
|
+
This formula does not account for frequency-domain overshoot and might slightly under-estimate the variance
|
|
107
|
+
"""
|
|
108
|
+
if (3*bandwidth)<(0.32*(nradial+1)*windspeed/D):
|
|
109
|
+
logging.warning('AO bandwidth is too low, temporal error might be unconsistent')
|
|
110
|
+
s = sum([(n+1)**(-2./3.) for n in range(1,nradial+1)])
|
|
111
|
+
var = 0.045*s*(windspeed/(D*bandwidth))**2 * (D/r0)**(5./3.)
|
|
112
|
+
return var
|
aopera/zernike.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Computation of Zernike polynomials.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy.special import binom, jv
|
|
7
|
+
from aopera.utils import polar
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ansi2nm(j):
|
|
11
|
+
"""Convert Zernike J (ANSI) indexing to (n,m) indexing."""
|
|
12
|
+
j = np.array(j)
|
|
13
|
+
n = np.int_(np.sqrt(8*j+1)-1)//2
|
|
14
|
+
m = 2*j-n*(n+2)
|
|
15
|
+
return n,m
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def nm2ansi(n, m):
|
|
19
|
+
"""Convert (n,m) indexing to J ANSI indexing."""
|
|
20
|
+
n = np.array(n)
|
|
21
|
+
m = np.array(m)
|
|
22
|
+
return np.int_((n*(n+2)+m)//2)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def nm2noll(n, m):
|
|
26
|
+
"""Convert (n,m) indexing to J Noll indexing."""
|
|
27
|
+
n = np.array(n)
|
|
28
|
+
m = np.array(m)
|
|
29
|
+
n23 = np.logical_or((n%4)==2, (n%4)==3)
|
|
30
|
+
b = np.logical_or((m>=0) * n23, (m<=0) * np.logical_not(n23))
|
|
31
|
+
return n*(n+1)//2 + np.abs(m) + b
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def noll2nm(j, abs_m=False):
|
|
35
|
+
"""Convert from Noll indexing (j) to radian-azimuthal indexing (n, m)."""
|
|
36
|
+
j = np.asarray(j, dtype=int)
|
|
37
|
+
n = np.sqrt(8*(j-1) + 1).astype(int)//2 - 1
|
|
38
|
+
p = j - (n*(n + 1))//2
|
|
39
|
+
k = n%2
|
|
40
|
+
m = 2*((p+k)//2) - k
|
|
41
|
+
m = m*(m != 0)*(1 - 2*(j%2))
|
|
42
|
+
if abs_m:
|
|
43
|
+
m = np.abs(m)
|
|
44
|
+
return n, m
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def ansi2noll(jansi):
|
|
48
|
+
"""Convert ANSI to Noll indexing"""
|
|
49
|
+
return nm2noll(*ansi2nm(jansi))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def noll2ansi(jnoll):
|
|
53
|
+
"""Convert Noll to ANSI indexing"""
|
|
54
|
+
return nm2ansi(*noll2nm(jnoll))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ansi_name(j):
|
|
58
|
+
"""Return the Zernike usual name associated to ANSI index."""
|
|
59
|
+
znames = ["piston","tilt vertical","tilt horizontal","astigmatism x","defocus","astigmatism +","trefoil +","coma vertical","coma horizontal","trefoil x","quadrafoil x","secondary astig. x","primary spherical","secondary astig. +","quadrafoil +"]
|
|
60
|
+
if j<len(znames):
|
|
61
|
+
return znames[j]
|
|
62
|
+
return "high order"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def noll_name(j):
|
|
66
|
+
"""Return the Zernike usual name associated to Noll index"""
|
|
67
|
+
return ansi_name(noll2ansi(j))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def radial_poly(n, m, rho, outside=0):
|
|
71
|
+
"""Compute the radial contribution of a Zernike polynomial.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
n : int, radial order.
|
|
76
|
+
m : int, azimuthal order.
|
|
77
|
+
rho : ndarray, the radial values where to compute the polynomial. Must be normalized to a unit circle.
|
|
78
|
+
|
|
79
|
+
Keywords
|
|
80
|
+
--------
|
|
81
|
+
outside : float or np.nan, the value to fill the array for ``rho > 1``. Default: ``np.nan``.
|
|
82
|
+
"""
|
|
83
|
+
nmm = (n - np.abs(m)) / 2
|
|
84
|
+
if nmm<0:
|
|
85
|
+
raise ValueError('Zernike azimuthal order cannot be greater than radial order')
|
|
86
|
+
if nmm%1:
|
|
87
|
+
raise ValueError('Zernike `n-|m|` must be even')
|
|
88
|
+
aperture = rho <= 1.0
|
|
89
|
+
rr = np.zeros(rho.shape)
|
|
90
|
+
rr[~aperture] = outside
|
|
91
|
+
for k in range(0, int(nmm) + 1):
|
|
92
|
+
rr[aperture] += ((-1)**k * binom(n - k, k) * binom(n - 2 * k, nmm - k) * rho[aperture]**(n - 2 * k))
|
|
93
|
+
return rr
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def azimuthal_poly(m, theta):
|
|
97
|
+
"""Compute the azimuthal contribution of a Zernike polynomial.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
m : int, azimuthal order.
|
|
102
|
+
theta : array, angles [rad] where to compute the polynomial.
|
|
103
|
+
"""
|
|
104
|
+
if m >= 0:
|
|
105
|
+
return np.cos(m * theta)
|
|
106
|
+
else:
|
|
107
|
+
return np.sin(np.abs(m) * theta)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def nollnorm(n, m):
|
|
111
|
+
"""Compute the Noll Zernike polynomials normalization factor.
|
|
112
|
+
sum(Zi*Zj)/sum(disk) = delta_ij * pi
|
|
113
|
+
"""
|
|
114
|
+
neumann = 2 - (m != 0)
|
|
115
|
+
return np.sqrt((2*n + 2) / neumann)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def ansinorm(n, m):
|
|
119
|
+
"""Compute the ANSI Zernike polynomials normalization factor.
|
|
120
|
+
sum(Zi*Zj)/sum(disk) = delta_ij
|
|
121
|
+
"""
|
|
122
|
+
return nollnorm(n, m) / np.sqrt(np.pi)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def zernike(n, m, npix, samp=1, norm="noll", outside=0):
|
|
126
|
+
"""
|
|
127
|
+
Return the (radial=n, azimut=m) Zernike polynomial.
|
|
128
|
+
|
|
129
|
+
The default norm (`noll`) verifies the normalization:
|
|
130
|
+
|
|
131
|
+
.. math::
|
|
132
|
+
\\frac{1}{\\pi}\\iint_P|Z(x,y)|^2dxdy \\simeq \\text{Var}(Z_{k,l}[P_{k,l}]) = 1
|
|
133
|
+
|
|
134
|
+
where `P` denotes a circular non-obstructed pupil.
|
|
135
|
+
|
|
136
|
+
Warning: the piston mode behaves differently due to its not null average.
|
|
137
|
+
Its L2 norm equals 1, but the variance over the pupil equals 0.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
n : int, Zernike radial index.
|
|
142
|
+
m : int, Zernike azimuthal index.
|
|
143
|
+
npix : int, size of the output array is (npix, npix).
|
|
144
|
+
|
|
145
|
+
Keywords
|
|
146
|
+
--------
|
|
147
|
+
samp : float, samp of the Zernike disk
|
|
148
|
+
norm : "ansi" or "noll"
|
|
149
|
+
outside : float or np.nan, the value to fill the array for rho>1.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
rho, theta = polar(npix)
|
|
153
|
+
dx = samp / (npix/2)
|
|
154
|
+
rho = dx * rho
|
|
155
|
+
Z = radial_poly(n, m, rho, outside=outside) * azimuthal_poly(m, theta)
|
|
156
|
+
|
|
157
|
+
if norm.lower() == "noll":
|
|
158
|
+
norm_coef = nollnorm(n, m)
|
|
159
|
+
elif norm.lower() == "ansi":
|
|
160
|
+
norm_coef = ansinorm(n, m)
|
|
161
|
+
else:
|
|
162
|
+
raise ValueError("``norm`` must be either 'noll' or 'ansi'.")
|
|
163
|
+
|
|
164
|
+
return Z * norm_coef
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def zernike_fourier(n, m, npix, samp=1, norm="ansi"):
|
|
168
|
+
"""
|
|
169
|
+
Compute the Fourier transform of a Zernike polynomial.
|
|
170
|
+
|
|
171
|
+
The default norm (`ansi`) verifies the normalization:
|
|
172
|
+
|
|
173
|
+
.. math::
|
|
174
|
+
\\iint |\\hat{Z}(f_x,f_y)|^2 df_xdf_y \\simeq \\sum_{k,l}{|\\hat{Z}_{k,l}|^2 \\delta f^2} = 1
|
|
175
|
+
|
|
176
|
+
with the frequency step:
|
|
177
|
+
|
|
178
|
+
.. math::
|
|
179
|
+
\\delta f = \\frac{1}{L} = \\frac{1}{N_p \\delta x} = \\frac{1}{2\\times\\text{samp}}
|
|
180
|
+
"""
|
|
181
|
+
rho, theta = polar(npix)
|
|
182
|
+
df = 1/(2*samp)
|
|
183
|
+
rho = rho * df
|
|
184
|
+
rho[np.where(rho==0)] = 1e-8
|
|
185
|
+
norm_coef = (-1+0j)**(n/2-np.abs(m))*np.sqrt(n+1)
|
|
186
|
+
if m!=0:
|
|
187
|
+
norm_coef *= np.sqrt(2)
|
|
188
|
+
if norm.lower()=="ansi":
|
|
189
|
+
norm_coef = norm_coef / np.sqrt(np.pi)
|
|
190
|
+
ztf = azimuthal_poly(m, theta)*jv(n+1, 2*np.pi*rho)/rho
|
|
191
|
+
return ztf * norm_coef
|
|
192
|
+
|
|
193
|
+
|