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