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/shwfs.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shack-Hartmann WFS
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from aopera.aopsd import aomask_in
|
|
7
|
+
from aopera.turbulence import piston_filter
|
|
8
|
+
from aopera.readconfig import read_config_file, read_config_tiptop, set_attribute
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
#%% SH BASIC FUNCTIONS
|
|
12
|
+
|
|
13
|
+
def shwfs_sensitivity(fx, fy, dpup):
|
|
14
|
+
"""
|
|
15
|
+
Compute the sensitivity map of a SH-WFS in the (fx,fy) domain.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
fx : np.array
|
|
20
|
+
Array of X frequencies [1/m].
|
|
21
|
+
fy : np.array
|
|
22
|
+
Array of Y frequencies [1/m].
|
|
23
|
+
dpup : float (>0)
|
|
24
|
+
Sub-aperture size [m].
|
|
25
|
+
|
|
26
|
+
References
|
|
27
|
+
----------
|
|
28
|
+
Rigaut, 1998, SPIE proceedings
|
|
29
|
+
Neichel, 2008, PhD thesis, pp 138-139, eq 6.10
|
|
30
|
+
Olivier Martin, https://github.com/oliviermartin-lam/P3/blob/main/aoSystem/fourierModel.py
|
|
31
|
+
"""
|
|
32
|
+
tfpup = np.sinc(fx*dpup)*np.sinc(fy*dpup) # ok with definition: np.sinc(x)=sinc(pi*x)
|
|
33
|
+
#TODO: check 'dpup' normalisation factor on 'sx' and 'sy'
|
|
34
|
+
logging.debug('Check SH-WFS sensitivity.')
|
|
35
|
+
sx = 2j*np.pi*fx * dpup * tfpup
|
|
36
|
+
sy = 2j*np.pi*fy * dpup * tfpup
|
|
37
|
+
return sx, sy # angle in rad? lambda/dpup ?
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def shwfs_reconstructor(*args, thresh=1e-2):
|
|
41
|
+
"""
|
|
42
|
+
Compute the reconstructor map of a SH-WFS in the (fx,fy) domain.
|
|
43
|
+
See also: shwfs_sensitivity
|
|
44
|
+
"""
|
|
45
|
+
sx,sy = shwfs_sensitivity(*args)
|
|
46
|
+
sensi2 = np.abs(sx)**2 + np.abs(sy)**2
|
|
47
|
+
vld = np.where(sensi2 > thresh**2) # filter frequencies if sensitivity is too low
|
|
48
|
+
recx = np.zeros(sx.shape, dtype=complex)
|
|
49
|
+
recy = np.zeros(sy.shape, dtype=complex)
|
|
50
|
+
recx[vld] = np.conj(sx[vld])/sensi2[vld]
|
|
51
|
+
recy[vld] = np.conj(sy[vld])/sensi2[vld]
|
|
52
|
+
return recx, recy
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def shwfs_visibility(*args, thresh=1e-2):
|
|
56
|
+
"""
|
|
57
|
+
Compute the SH-WFS visible frequencies.
|
|
58
|
+
See also: shwfs_sensitivity, shwfs_reconstructor
|
|
59
|
+
"""
|
|
60
|
+
sx,sy = shwfs_sensitivity(*args)
|
|
61
|
+
rx,ry = shwfs_reconstructor(*args, thresh=thresh)
|
|
62
|
+
return np.real(rx*sx+ry*sy)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def shwfs_spot_fwhm(r0, pitch, wvl, src_size, samp):
|
|
66
|
+
"""
|
|
67
|
+
Compute Shack-Hartmann spot FWHM (in units of wvl/D).
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
r0 : float (>0)
|
|
72
|
+
Fried parameter at WFS wavelength [m].
|
|
73
|
+
pitch : float (>0)
|
|
74
|
+
DM actuator pitch [m].
|
|
75
|
+
wvl : float (>0)
|
|
76
|
+
WFS wavelength [m].
|
|
77
|
+
src_size : float (>=0)
|
|
78
|
+
Source physical extension [rad].
|
|
79
|
+
samp : float (>0)
|
|
80
|
+
SH detector sampling.
|
|
81
|
+
"""
|
|
82
|
+
SNd2 = (src_size*pitch/wvl)**2 # taille source sur diffraction d'une sous-pup (au carré)
|
|
83
|
+
NpixNd2 = (1.0/samp)**2 # (D*pix)/(2*wvl*F) = taille pixel sur la diffraction d'une sous-pup (au carré)
|
|
84
|
+
NturbNd2 = (pitch/r0)**2 # taille turbulence sur la diffraction d'une sous-pup (au carré)
|
|
85
|
+
return np.sqrt(SNd2 + NpixNd2 + NturbNd2 + 1) # le +1 compte pour la taille diffraction divisée par elle même (au carré)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
#%% ANALYTICAL ERROR BREAKDOWN
|
|
89
|
+
|
|
90
|
+
def shwfs_var_ron_subpupil(pix_size, ron_nphot, npix_cog=10):
|
|
91
|
+
"""
|
|
92
|
+
Compute the AO variance due to the readout-noise on one subpupil
|
|
93
|
+
"""
|
|
94
|
+
return pix_size**2 * np.pi**2 / 3 * ron_nphot**2 * npix_cog**2
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def shwfs_var_ron(pix_size, ron_nphot, nradial, bandwidth, freq, npix_cog=10):
|
|
98
|
+
"""
|
|
99
|
+
Compute the AO variance due to the readout-noise (propagated in the loop).
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
pix_size : float (>0)
|
|
104
|
+
SH pixel size (in units of wvl/D).
|
|
105
|
+
ron_nphot : float (>=0)
|
|
106
|
+
Pixel read-out noise over number of photons per subaperture per frame.
|
|
107
|
+
nradial : int (>0)
|
|
108
|
+
Number of radial corrected modes.
|
|
109
|
+
bandwidth : float (>0)
|
|
110
|
+
AO loop correction bandwidth [Hz].
|
|
111
|
+
freq : float (>0)
|
|
112
|
+
AO loop camera frequency (framerate) [Hz].
|
|
113
|
+
npix_cog : int (>0)
|
|
114
|
+
Total number of pixels used in the CoG measurement.
|
|
115
|
+
|
|
116
|
+
Return
|
|
117
|
+
------
|
|
118
|
+
the AO error variance [rad²].
|
|
119
|
+
The variance is given at the measurement wvl !!!
|
|
120
|
+
To get it at the scientific wvl, it should be multiplied by (wvl_wfs/wvl_sci)**2.
|
|
121
|
+
|
|
122
|
+
References
|
|
123
|
+
----------
|
|
124
|
+
Thierry Fusco (ONERA), year?, internal document, "Budget OA.docx".
|
|
125
|
+
VERSO team (ONERA), 2021, internal document, "RF-VERSOBC3.docx".
|
|
126
|
+
Magalie Nicolle, 2007, PhD thesis, "Analyse de front d'onde pour les OA de nouvelle génération". Section 10.2.1.
|
|
127
|
+
"""
|
|
128
|
+
var_ron = shwfs_var_ron_subpupil(pix_size, ron_nphot, npix_cog=npix_cog)
|
|
129
|
+
spatial_fltr = sum([0.2/(n+1) for n in range(1,nradial+1)])
|
|
130
|
+
tempo_fltr = 2*bandwidth/freq
|
|
131
|
+
return var_ron * spatial_fltr * tempo_fltr
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def shwfs_var_photon_subpupil(spot_size, nphot, emccd=False, weight=None):
|
|
135
|
+
"""
|
|
136
|
+
Compute the AO variance due to the photon-noise on one subpupil
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
spot_size : float (>0)
|
|
141
|
+
SH spot size (in units of wvl/Dpup).
|
|
142
|
+
nphot : float (>=0)
|
|
143
|
+
Number of photons per subaperture per frame.
|
|
144
|
+
|
|
145
|
+
Keywords
|
|
146
|
+
--------
|
|
147
|
+
emccd : boolean
|
|
148
|
+
Multiply result by excess factor if camera is EMCCD
|
|
149
|
+
weight : None or float
|
|
150
|
+
Weighting CoG (in units of wvl/Dpup)
|
|
151
|
+
|
|
152
|
+
Reference
|
|
153
|
+
---------
|
|
154
|
+
Magalie Nicolle, 2006, PhD thesis, p.234
|
|
155
|
+
"""
|
|
156
|
+
var_phot = spot_size**2 * np.pi**2/(2*np.log(2)*nphot)
|
|
157
|
+
if weight is not None:
|
|
158
|
+
var_phot *= ((spot_size**2 + weight**2)/(2*spot_size**2 + weight**2))**2
|
|
159
|
+
if emccd:
|
|
160
|
+
var_phot *= 2 # excess factor squared
|
|
161
|
+
return var_phot
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def shwfs_var_photon(spot_size, nphot, nradial, bandwidth, freq, **kwargs):
|
|
165
|
+
"""
|
|
166
|
+
Compute the variance of the AO photon-noise error (propagated in the loop).
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
spot_size : float (>0)
|
|
171
|
+
SH spot size (in units of wvl/Dpup).
|
|
172
|
+
nphot : float (>=0)
|
|
173
|
+
Number of photons per subaperture per frame.
|
|
174
|
+
nradial : int (>0)
|
|
175
|
+
Number of radial corrected modes.
|
|
176
|
+
bandwidth : float (>0)
|
|
177
|
+
AO loop correction bandwidth [Hz].
|
|
178
|
+
freq : float (>0)
|
|
179
|
+
AO loop camera frequency (framerate) [Hz].
|
|
180
|
+
|
|
181
|
+
Keywords
|
|
182
|
+
--------
|
|
183
|
+
See `shwfs_var_photon_subpupil`
|
|
184
|
+
|
|
185
|
+
Return
|
|
186
|
+
------
|
|
187
|
+
the AO error variance [rad²].
|
|
188
|
+
The variance is given at the measurement wvl !!!
|
|
189
|
+
To get it at the scientific wvl, it should be multiplied by (wvl_wfs/wvl_sci)**2.
|
|
190
|
+
|
|
191
|
+
References
|
|
192
|
+
----------
|
|
193
|
+
Thierry Fusco (ONERA), year?, internal document, "Budget OA.docx".
|
|
194
|
+
VERSO team (ONERA), 2021, internal document, "RF-VERSOBC3.docx".
|
|
195
|
+
Magalie Nicolle, 2007, PhD thesis, "Analyse de front d'onde pour les OA de nouvelle génération". Section 10.2.1, Annexe D.2.1.
|
|
196
|
+
Jean-Marc Conan, 1994, PhD thesis, section 2.2.4.3
|
|
197
|
+
"""
|
|
198
|
+
var_phot = shwfs_var_photon_subpupil(spot_size, nphot, **kwargs)
|
|
199
|
+
spatial_fltr = sum([0.2/(n+1) for n in range(1,nradial+1)])
|
|
200
|
+
tempo_fltr = 2*bandwidth/freq
|
|
201
|
+
return var_phot * spatial_fltr * tempo_fltr
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
#%% POWER SPECTRAL DENSITIES
|
|
205
|
+
|
|
206
|
+
def shwfs_psd_aliasing(fx, fy, dpup, psd_input):
|
|
207
|
+
"""
|
|
208
|
+
Aliasing PSD of a Shack-Hartmann
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
fx : np.array
|
|
213
|
+
The spatial frequencies on X [1/m].
|
|
214
|
+
fy : np.array
|
|
215
|
+
The spatial frequencies on Y [1/m].
|
|
216
|
+
dpup : float
|
|
217
|
+
Size of the subaperture on sky [m].
|
|
218
|
+
psd_input : function
|
|
219
|
+
The input PSD to be aliased by the SH. Should be called as: psd=psd_input(fx,fy)
|
|
220
|
+
"""
|
|
221
|
+
rx, ry = shwfs_reconstructor(fx, fy, dpup)
|
|
222
|
+
psd_alias = np.zeros(fx.shape)
|
|
223
|
+
fcut = 1/(2*dpup)
|
|
224
|
+
nshift = 3
|
|
225
|
+
for i in range(-nshift,nshift+1):
|
|
226
|
+
for j in range(-nshift,nshift+1):
|
|
227
|
+
if (i!=0) or (j!=0):
|
|
228
|
+
fx_shift = fx - i*2*fcut
|
|
229
|
+
fy_shift = fy - j*2*fcut
|
|
230
|
+
psd_atmo_shift = psd_input(fx_shift,fy_shift)
|
|
231
|
+
sx_shift, sy_shift = shwfs_sensitivity(fx_shift, fy_shift, dpup)
|
|
232
|
+
psd_rec = psd_atmo_shift * np.abs(sx_shift*rx+sy_shift*ry)**2
|
|
233
|
+
psd_alias += psd_rec
|
|
234
|
+
return psd_alias
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def shwfs_psd_noise(fx, fy, aocutoff, var, df, D=None, **kwargs):
|
|
238
|
+
"""
|
|
239
|
+
Compute the noise PSD, assuming a f^(-2) power law.
|
|
240
|
+
Valid for a SH-WFS.
|
|
241
|
+
TODO: use a more accurate formula.
|
|
242
|
+
"""
|
|
243
|
+
logging.debug('Is SH PSD noise accurate enough ?')
|
|
244
|
+
msk = aomask_in(fx, fy, aocutoff, **kwargs)
|
|
245
|
+
f2 = fx**2 + fy**2
|
|
246
|
+
fnull = np.where(f2==0)
|
|
247
|
+
f2[fnull] = 1 # avoid numerical issue in f=0
|
|
248
|
+
noiseshape = msk / f2
|
|
249
|
+
if D is not None:
|
|
250
|
+
noiseshape *= piston_filter(np.sqrt(fx**2+fy**2), D)
|
|
251
|
+
noiseshape[fnull] = 0
|
|
252
|
+
return var*noiseshape/np.sum(noiseshape*df**2)
|
|
253
|
+
|
|
254
|
+
#%% SHWFS CLASS
|
|
255
|
+
class SHWFS:
|
|
256
|
+
def __init__(self, dictionary):
|
|
257
|
+
set_attribute(self, dictionary, 'shwfs')
|
|
258
|
+
|
|
259
|
+
def __repr__(self):
|
|
260
|
+
s = 'aopera.SHWFS\n'
|
|
261
|
+
s += '-------------\n'
|
|
262
|
+
s += 'lenslet : %u \n'%self.lenslet
|
|
263
|
+
s += 'ron : %.1f e-\n'%self.ron
|
|
264
|
+
s += 'EMCCD : %s\n'%self.emccd
|
|
265
|
+
s += 'sampling: %.1f\n'%self.samp
|
|
266
|
+
s += 'npix CoG: %u\n'%self.npix_cog
|
|
267
|
+
return s
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
def from_file(filepath, category='shwfs'):
|
|
271
|
+
return SHWFS(read_config_file(filepath, category))
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def from_file_tiptop(filepath):
|
|
275
|
+
return SHWFS(read_config_tiptop(filepath)[2])
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def from_oopao(wfs, npix_cog):
|
|
279
|
+
return SHWFS({'lenslet':wfs.nSubap,
|
|
280
|
+
'ron':wfs.cam.readoutNoise,
|
|
281
|
+
'emccd':wfs.cam.sensor=='EMCCD',
|
|
282
|
+
'samp':1.0+wfs.shannon_sampling*1.0,
|
|
283
|
+
'npix_cog':npix_cog})
|
|
284
|
+
|
|
285
|
+
def lenslet_valid(self, occ=0):
|
|
286
|
+
return round(np.pi/4*(self.lenslet**2)*(1-occ**2))
|
|
287
|
+
|
|
288
|
+
def sensitivity(self, fx, fy, diameter):
|
|
289
|
+
return shwfs_sensitivity(fx, fy, diameter/self.lenslet)
|
|
290
|
+
|
|
291
|
+
def reconstructor(self, fx, fy, diameter):
|
|
292
|
+
return shwfs_reconstructor(fx, fy, diameter/self.lenslet)
|
|
293
|
+
|
|
294
|
+
def visibility(self, fx, fy, diameter, thresh=1e-2):
|
|
295
|
+
return shwfs_visibility(fx, fy, diameter/self.lenslet, thresh=thresh)
|
|
296
|
+
|
|
297
|
+
def spot_fwhm(self, r0, diameter, wvl, src_size=0):
|
|
298
|
+
return shwfs_spot_fwhm(r0, diameter/self.lenslet, wvl, src_size, self.samp)
|
|
299
|
+
|
|
300
|
+
def var_ron_subpupil(self, pix_size, nphot):
|
|
301
|
+
return shwfs_var_ron_subpupil(pix_size, self.ron/nphot, npix_cog=self.npix_cog)
|
|
302
|
+
|
|
303
|
+
def var_ron(self, nphot, nradial, bandwidth, freq):
|
|
304
|
+
return shwfs_var_ron(1/self.samp, self.ron/nphot, nradial, bandwidth, freq, npix_cog=self.npix_cog)
|
|
305
|
+
|
|
306
|
+
def var_photon_subpupil(self, spot_size, nphot):
|
|
307
|
+
return shwfs_var_photon_subpupil(spot_size, nphot, emccd=self.emccd, weight=self.weight)
|
|
308
|
+
|
|
309
|
+
def var_photon(self, spot_size, nphot, nradial, bandwidth, freq):
|
|
310
|
+
return shwfs_var_photon(spot_size, nphot, nradial, bandwidth, freq, emccd=self.emccd, weight=self.weight)
|
|
311
|
+
|
|
312
|
+
def psd_aliasing(self, fx, fy, diameter, psd_input):
|
|
313
|
+
return shwfs_psd_aliasing(fx, fy, diameter/self.lenslet, psd_input)
|
|
314
|
+
|
|
315
|
+
def psd_noise(self, fx, fy, aocutoff, var, df, D=None, **kwargs):
|
|
316
|
+
return shwfs_psd_noise(fx, fy, aocutoff, var, df, D=D, **kwargs)
|
aopera/simulation.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gathers all classes from the library to run a simulation
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy.signal import fftconvolve
|
|
8
|
+
import logging
|
|
9
|
+
from aopera.utils import arcsec2rad, print_std
|
|
10
|
+
from aopera.aopsd import psd_temporal, psd_anisoplanetism, psd_anisoplanetism_extended_object, piston_filter, psd_ncpa, psd_chromatic_filter, air_refractive_index
|
|
11
|
+
from aopera.turbulence import Atmosphere
|
|
12
|
+
from aopera.otfpsf import Pupil, psd2otf, otf2psf
|
|
13
|
+
from aopera.photometry import SourceScience, SourceWFS
|
|
14
|
+
from aopera.shwfs import SHWFS
|
|
15
|
+
from aopera.ffwfs import PWFS
|
|
16
|
+
from aopera.control import RTC
|
|
17
|
+
from aopera.readconfig import read_config_tiptop_nx, read_config_tiptop_samp
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TiptopBaseSimulation:
|
|
21
|
+
"""Implement baseSimulation from Tiptop"""
|
|
22
|
+
def __init__(self, path, parametersFile, verbose=False):
|
|
23
|
+
self.path = path
|
|
24
|
+
self.parametersFile = parametersFile
|
|
25
|
+
self.verbose = verbose
|
|
26
|
+
|
|
27
|
+
def doOverallSimulation(self):
|
|
28
|
+
file_ini = os.path.join(self.path, self.parametersFile + '.ini')
|
|
29
|
+
src_sci = SourceScience.from_file_tiptop(file_ini)
|
|
30
|
+
src_wfs = SourceWFS.from_file_tiptop(file_ini)
|
|
31
|
+
atm = Atmosphere.from_file_tiptop(file_ini)
|
|
32
|
+
pupil = Pupil.from_file_tiptop(file_ini)
|
|
33
|
+
rtc = RTC.from_file_tiptop(file_ini)
|
|
34
|
+
try:
|
|
35
|
+
wfs = PWFS.from_file_tiptop(file_ini)
|
|
36
|
+
except:
|
|
37
|
+
wfs = SHWFS.from_file_tiptop(file_ini)
|
|
38
|
+
nx = read_config_tiptop_nx(file_ini)
|
|
39
|
+
samp = read_config_tiptop_samp(file_ini)
|
|
40
|
+
psd, var, param = simulation(nx, samp, pupil, atm, rtc, wfs, src_sci, src_wfs, verbose=self.verbose)
|
|
41
|
+
psd_total = sum([psd[k] for k in psd.keys()])
|
|
42
|
+
wvl_ratio = src_sci.wvl/src_wfs.wvl
|
|
43
|
+
psf_ao_sci = pupil.psf_ao(psd_total, samp, wvl_scale=wvl_ratio)
|
|
44
|
+
psf_diff_sci = pupil.psf_diffraction(nx, samp*wvl_ratio)
|
|
45
|
+
|
|
46
|
+
df = pupil.frequency_step(samp)
|
|
47
|
+
self.PSDstep = df
|
|
48
|
+
self.N = nx
|
|
49
|
+
self.PSD = psd_total * (src_wfs.wvl_nm/(2*np.pi))**2 * df**2 # Tiptop PSD has units of [nm²]
|
|
50
|
+
self.sr = [np.max(psf_ao_sci)/np.max(psf_diff_sci)]
|
|
51
|
+
self.cubeResultsArray = np.zeros((1, nx, nx))
|
|
52
|
+
self.cubeResultsArray[0,...] = psf_ao_sci
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def cubeResults(self):
|
|
56
|
+
try:
|
|
57
|
+
return list(self.cubeResultsArray)
|
|
58
|
+
except: # not computed yet
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def computeMetrics(self):
|
|
62
|
+
raise NotImplementedError()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def simulation(nx, samp, *args, **kwargs):
|
|
67
|
+
"""Run a SH-WFS or a P-WFS simulation"""
|
|
68
|
+
for a in args:
|
|
69
|
+
if isinstance(a, SHWFS):
|
|
70
|
+
return simulation_shwfs(nx, samp, *args, **kwargs)
|
|
71
|
+
if isinstance(a, PWFS):
|
|
72
|
+
return simulation_ffwfs(nx, samp, *args, **kwargs)
|
|
73
|
+
raise ValueError('Your simulation must include a SHWFS or a PWFS')
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def simulation_shwfs(nx, samp, *args, verbose=False):
|
|
77
|
+
"""
|
|
78
|
+
Run a full SH-WFS simulation
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
nx : int : number of pixels
|
|
83
|
+
samp : float : sampling.
|
|
84
|
+
*args : must include a Pupil, Atmosphere, SHWFS, RTC, SourceWFS, SourceScience
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
dictionary : the PSD list [rad²m²]
|
|
89
|
+
dictionary : the variance list [rad²]
|
|
90
|
+
dictionary : some intermediate values
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
### PARSE INPUTS, whatever the order
|
|
94
|
+
for a in args:
|
|
95
|
+
if isinstance(a, Atmosphere):
|
|
96
|
+
atm = a
|
|
97
|
+
if isinstance(a, Pupil):
|
|
98
|
+
tel = a
|
|
99
|
+
if isinstance(a, SourceScience):
|
|
100
|
+
src_sci = a
|
|
101
|
+
if isinstance(a, SourceWFS):
|
|
102
|
+
src_wfs = a
|
|
103
|
+
if isinstance(a, SHWFS):
|
|
104
|
+
wfs = a
|
|
105
|
+
if isinstance(a, RTC):
|
|
106
|
+
rtc = a
|
|
107
|
+
|
|
108
|
+
for v in ['atm','tel','src_sci','src_wfs','wfs','rtc']:
|
|
109
|
+
if not v in locals():
|
|
110
|
+
raise ValueError('You forgot to define one of the arguments')
|
|
111
|
+
|
|
112
|
+
### INIT SOME STUFF
|
|
113
|
+
df = tel.frequency_step(samp) # numerical frequency step [1/m]
|
|
114
|
+
aocutoff = tel.cutoff_frequency(wfs.lenslet) # system AO cutoff [1/m]
|
|
115
|
+
fx,fy = tel.frequency_grid(nx, samp)
|
|
116
|
+
freq = np.sqrt(fx**2+fy**2)
|
|
117
|
+
pstflt = piston_filter(freq, tel.diameter) # filter low freq
|
|
118
|
+
aomskin = wfs.visibility(fx, fy, tel.diameter, thresh=1e-8) * tel.controllability(fx, fy)
|
|
119
|
+
r0_wfs = atm.r0(src_wfs.wvl, zenith=src_sci.zenith_rad)
|
|
120
|
+
nphot_subap = src_wfs.flux * tel.surface / rtc.freq / wfs.lenslet_valid(occ=tel.occultation)
|
|
121
|
+
psdatmo = atm.phase_psd(freq, src_wfs.wvl, zenith=src_sci.zenith_rad)
|
|
122
|
+
psd = {}
|
|
123
|
+
|
|
124
|
+
## PSD FITTING
|
|
125
|
+
psd['fitting'] = psdatmo * (1-aomskin) * pstflt
|
|
126
|
+
|
|
127
|
+
### PSD ALIASING
|
|
128
|
+
psd_to_alias = lambda fx,fy : atm.phase_psd(np.sqrt(fx**2+fy**2), src_wfs.wvl, zenith=src_sci.zenith_rad)
|
|
129
|
+
psd['aliasing'] = wfs.psd_aliasing(fx, fy, tel.diameter, psd_to_alias) * aomskin
|
|
130
|
+
|
|
131
|
+
## PSD TEMPORAL
|
|
132
|
+
psd['temporal'] = np.zeros((nx,nx))
|
|
133
|
+
logging.debug('Use better implementation of LQG.')
|
|
134
|
+
for j in range(atm.nlayer):
|
|
135
|
+
wnd_angle = atm.wind_direction[j]*np.pi/180
|
|
136
|
+
psd_temp = aomskin * psd_temporal(fx, fy, atm.wind_speed[j], psdatmo, rtc.closed_loop_transfer, wnd_angle=wnd_angle)
|
|
137
|
+
psd['temporal'] += psd_temp * atm.cn2dh_ratio[j] * rtc.predictive_factor #TODO: better implementation of LQG
|
|
138
|
+
|
|
139
|
+
## PSD RON NOISE
|
|
140
|
+
var_ron = wfs.var_ron(nphot_subap, tel.radial_max, rtc.bandwidth_noise, rtc.freq)
|
|
141
|
+
psd['ron'] = wfs.psd_noise(fx, fy, aocutoff, var_ron, df, D=tel.diameter)
|
|
142
|
+
|
|
143
|
+
### PSD PHOTON NOISE
|
|
144
|
+
spot_size = wfs.spot_fwhm(r0_wfs, tel.diameter, src_wfs.wvl, src_size=arcsec2rad(src_wfs.size))
|
|
145
|
+
var_photon = wfs.var_photon(spot_size, nphot_subap, tel.radial_max, rtc.bandwidth_noise, rtc.freq)
|
|
146
|
+
psd['photon'] = wfs.psd_noise(fx, fy, aocutoff, var_photon, df, D=tel.diameter)
|
|
147
|
+
|
|
148
|
+
## PSD ANISOPLANETISM MEASURE
|
|
149
|
+
if (src_wfs.size > 0) and (max(atm.altitude) > 0):
|
|
150
|
+
aniso_size = arcsec2rad(src_wfs.size) / (0.5*(np.sqrt(2)+1)) # avg between circle and square
|
|
151
|
+
psd['aniso-mes'] = psd_anisoplanetism_extended_object(fx, fy, atm.cn2dh, atm.altitude, tel.pitch_ao(wfs.lenslet), src_wfs.wvl, aniso_size, src_alt=np.inf, src_zenith=src_sci.zenith_rad, lext=atm.lext, lint=0)
|
|
152
|
+
|
|
153
|
+
## PSD ANISOPLANETISM SEPARATION
|
|
154
|
+
if src_wfs.separation > 0:
|
|
155
|
+
sep_x_rad = arcsec2rad(src_wfs.separation_x)
|
|
156
|
+
sep_y_rad = arcsec2rad(src_wfs.separation_y)
|
|
157
|
+
psd_aniso = psd_anisoplanetism(fx, fy, atm.cn2dh, atm.altitude, src_wfs.wvl, sep_x_rad, sep_y_rad, lext=atm.lext, zenith=src_sci.zenith_rad)
|
|
158
|
+
psd['aniso-dist'] = psd_aniso * aomskin
|
|
159
|
+
|
|
160
|
+
## NCPA SCIENCE vs WFS
|
|
161
|
+
rad_to_nm = src_wfs.wvl*1e9/(2*np.pi)
|
|
162
|
+
if tel.ncpa > 0:
|
|
163
|
+
psd['ncpa'] = psd_ncpa(tel.ncpa / rad_to_nm, freq, df, tel.diameter)
|
|
164
|
+
|
|
165
|
+
## DIFFERENTIAL CHROMATIC INDEX
|
|
166
|
+
if src_sci.wvl != src_wfs.wvl:
|
|
167
|
+
psd['chromatic'] = psdatmo * aomskin * psd_chromatic_filter(src_wfs.wvl_nm, src_sci.wvl_nm)
|
|
168
|
+
theta_refraction = (air_refractive_index(src_sci.wvl_nm*1e-3)-air_refractive_index(src_wfs.wvl_nm*1e-3))*np.tan(src_sci.zenith_rad)
|
|
169
|
+
psd['refraction'] = psd_anisoplanetism(fx, fy, atm.cn2dh, atm.altitude, src_wfs.wvl, theta_refraction, 0, lext=atm.lext, zenith=src_sci.zenith_rad) * aomskin
|
|
170
|
+
|
|
171
|
+
### GET VARIANCES
|
|
172
|
+
var = {}
|
|
173
|
+
for k in psd.keys():
|
|
174
|
+
if k in ['ncpa', 'photon', 'ron']:
|
|
175
|
+
filt = 1 # these have already been filtered
|
|
176
|
+
else:
|
|
177
|
+
filt = pstflt
|
|
178
|
+
var[k] = np.sum(filt*psd[k]*df**2)
|
|
179
|
+
|
|
180
|
+
if verbose:
|
|
181
|
+
rad_to_nm = src_wfs.wvl*1e9/(2*np.pi)
|
|
182
|
+
std_nm = {k:rad_to_nm*np.sqrt(var[k]) for k in var.keys()}
|
|
183
|
+
print()
|
|
184
|
+
print(' WAVEFRONT ERROR [nm RMS]')
|
|
185
|
+
print_std(std_nm)
|
|
186
|
+
|
|
187
|
+
### RETURN
|
|
188
|
+
param = {'r0_wfs':r0_wfs,
|
|
189
|
+
'spot_fwhm':spot_size,
|
|
190
|
+
'psd_atmo':psdatmo}
|
|
191
|
+
return psd, var, param
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def simulation_ffwfs(nx, samp, *args, verbose=False, is_modu_sky=True, nb_iter_og=4):
|
|
196
|
+
"""
|
|
197
|
+
Run a full FF-WFS simulation
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
nx : int : number of pixels.
|
|
202
|
+
samp : float : sampling of the PSF at the WFS wavelength.
|
|
203
|
+
*args : must include a Pupil, Atmosphere, PWFS, RTC, SourceWFS, SourceScience.
|
|
204
|
+
|
|
205
|
+
Keywords
|
|
206
|
+
--------
|
|
207
|
+
verbose = False : bool : activate verbose mode.
|
|
208
|
+
is_modu_sky = True : bool : activate modulation on-sky.
|
|
209
|
+
nb_iter_og = 4 : int : number of computations for OG to converge
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
dictionary : the PSD list [rad²m²]
|
|
214
|
+
dictionary : the variance list [rad²]
|
|
215
|
+
dictionary : some intermediate values
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
### PARSE INPUTS, whatever the order
|
|
219
|
+
for a in args:
|
|
220
|
+
if isinstance(a, Atmosphere):
|
|
221
|
+
atm = a
|
|
222
|
+
if isinstance(a, Pupil):
|
|
223
|
+
tel = a
|
|
224
|
+
if isinstance(a, SourceScience):
|
|
225
|
+
src_sci = a
|
|
226
|
+
if isinstance(a, SourceWFS):
|
|
227
|
+
src_wfs = a
|
|
228
|
+
if isinstance(a, PWFS):
|
|
229
|
+
wfs = a
|
|
230
|
+
if isinstance(a, RTC):
|
|
231
|
+
rtc = a
|
|
232
|
+
|
|
233
|
+
for v in ['atm','tel','src_sci','src_wfs','wfs','rtc']:
|
|
234
|
+
if not v in locals():
|
|
235
|
+
raise ValueError('You forgot to define one of the arguments')
|
|
236
|
+
|
|
237
|
+
otf_diff = tel.otf_diffraction(nx, samp)
|
|
238
|
+
psf_diff = tel.psf_diffraction(nx, samp)
|
|
239
|
+
obj = src_wfs.image(nx, tel.diameter, samp)
|
|
240
|
+
nphot = src_wfs.flux * tel.surface / rtc.freq
|
|
241
|
+
|
|
242
|
+
df = tel.frequency_step(samp) # numerical frequency step [1/m]
|
|
243
|
+
fx,fy = tel.frequency_grid(nx, samp)
|
|
244
|
+
freq = np.sqrt(fx**2+fy**2)
|
|
245
|
+
pstflt = piston_filter(freq, tel.diameter) # filter low freq
|
|
246
|
+
aomskin = wfs.visibility(samp, psf_diff, thresh=0.01) * tel.controllability(fx, fy)
|
|
247
|
+
ki0 = rtc.ki
|
|
248
|
+
psdatmo = atm.phase_psd(freq, src_wfs.wvl, zenith=src_sci.zenith_rad)
|
|
249
|
+
psd = {}
|
|
250
|
+
|
|
251
|
+
## PSD FITTING and ALIASING
|
|
252
|
+
psd['fitting'] = psdatmo * (1-aomskin) * pstflt
|
|
253
|
+
|
|
254
|
+
psd_to_alias = lambda fx,fy : atm.phase_psd(np.sqrt(fx**2+fy**2), src_wfs.wvl, zenith=src_sci.zenith_rad)
|
|
255
|
+
ffwfsrec = wfs.reconstructor(samp, psf_diff, thresh=0.01)
|
|
256
|
+
psd['aliasing'] = wfs.psd_aliasing(fx, fy, psd_to_alias, ffwfsrec, tel.diameter, nact=tel.nact) * aomskin
|
|
257
|
+
|
|
258
|
+
## PSD TEMPORAL and NOISE (affected by OG)
|
|
259
|
+
og = np.ones((nx,nx))
|
|
260
|
+
psf_ao = np.copy(psf_diff)
|
|
261
|
+
|
|
262
|
+
if nb_iter_og < 1:
|
|
263
|
+
raise ValueError('You must ensure `nb_iter_og >= 1`')
|
|
264
|
+
|
|
265
|
+
for i_og in range(nb_iter_og):
|
|
266
|
+
|
|
267
|
+
## APPLY OG to LOOP GAIN
|
|
268
|
+
rtc.ki = ki0 * og # apply WFS OG to loop gain
|
|
269
|
+
|
|
270
|
+
if wfs.og_compensation:
|
|
271
|
+
modal_gain_compensation = og # (1+og)/2 # specific matrix approximation
|
|
272
|
+
rtc.ki = rtc.ki / modal_gain_compensation
|
|
273
|
+
|
|
274
|
+
### PSD TEMPORAL
|
|
275
|
+
psd['temporal'] = np.zeros((nx,nx))
|
|
276
|
+
logging.debug('Use better implementation of LQG.')
|
|
277
|
+
for i in range(atm.nlayer):
|
|
278
|
+
wnd_angle = atm.wind_direction[i]*np.pi/180
|
|
279
|
+
psd_temp = aomskin * psd_temporal(fx, fy, atm.wind_speed[i], psdatmo, rtc.closed_loop_transfer, wnd_angle=wnd_angle)
|
|
280
|
+
psd['temporal'] += psd_temp * atm.cn2dh_ratio[i] * rtc.predictive_factor #TODO: better implementation of LQG
|
|
281
|
+
|
|
282
|
+
## PSD NOISE
|
|
283
|
+
og_avg = np.mean(og[aomskin>0.5])
|
|
284
|
+
rtc.ki = ki0 * og_avg
|
|
285
|
+
if wfs.og_compensation:
|
|
286
|
+
rtc.ki = rtc.ki / np.mean(modal_gain_compensation[aomskin>0.5])
|
|
287
|
+
freq_temp = np.linspace(0.01, rtc.freq/2, num=3000) # Could also use np.logspace to compute freq_temp!
|
|
288
|
+
ntf2_int = 2/rtc.freq * np.trapezoid(np.abs(rtc.noise_transfer(freq_temp))**2, freq_temp)
|
|
289
|
+
if (src_wfs.size==0):
|
|
290
|
+
psf_onsky = psf_ao
|
|
291
|
+
else:
|
|
292
|
+
psf_onsky = fftconvolve(psf_ao, obj, mode='same')
|
|
293
|
+
psd['ron'] = wfs.psd_noise_ron(samp, nphot, psf_onsky, psf_diff) * ntf2_int * aomskin
|
|
294
|
+
psd['photon'] = wfs.psd_noise_photon(samp, nphot, psf_onsky, psf_diff) * ntf2_int * aomskin
|
|
295
|
+
|
|
296
|
+
## PSD ANISOPLANETISM MEASURE
|
|
297
|
+
if (src_wfs.size > 0) and (max(atm.altitude) > 0):
|
|
298
|
+
aniso_size = arcsec2rad(src_wfs.size) / (0.5*(np.sqrt(2)+1)) # avg between circle and square
|
|
299
|
+
psd['aniso-mes'] = psd_anisoplanetism_extended_object(fx, fy, atm.cn2dh, atm.altitude, tel.pitch_ao(wfs.lenslet), src_wfs.wvl, aniso_size, src_alt=np.inf, src_zenith=src_sci.zenith_rad, lext=atm.lext, lint=0) * np.mean(og[aomskin>0])
|
|
300
|
+
|
|
301
|
+
### COMPUTE PSD TOTAL, PSF and OG
|
|
302
|
+
psd_total = sum([psd[k] for k in psd.keys()])
|
|
303
|
+
psf_ao = otf2psf(psd2otf(psd_total, df)*otf_diff)
|
|
304
|
+
if i_og < (nb_iter_og-1): # compute OG for next iteration
|
|
305
|
+
og = wfs.optical_gain(samp, psf_diff, psf_ao, obj_sky=obj, modu_sky=is_modu_sky)
|
|
306
|
+
|
|
307
|
+
### VERBOSE
|
|
308
|
+
if verbose:
|
|
309
|
+
rad_to_nm = src_wfs.wvl*1e9/(2*np.pi)
|
|
310
|
+
wfe = np.sqrt(np.sum(pstflt*psd_total)*df**2)*rad_to_nm
|
|
311
|
+
print('[OG iter. %u/%u] OG = %.2f WFE = %3u nm'%(i_og+1, nb_iter_og, og_avg, wfe))
|
|
312
|
+
|
|
313
|
+
## SET AGAIN CORRECT KI BEFORE EXITING
|
|
314
|
+
rtc.ki = ki0
|
|
315
|
+
|
|
316
|
+
## PSD ANISOPLANETISM SEPARATION
|
|
317
|
+
## after OG computation since it affects only science!
|
|
318
|
+
if src_wfs.separation > 0:
|
|
319
|
+
sep_x_rad = arcsec2rad(src_wfs.separation_x)
|
|
320
|
+
sep_y_rad = arcsec2rad(src_wfs.separation_y)
|
|
321
|
+
psd_aniso = psd_anisoplanetism(fx, fy, atm.cn2dh, atm.altitude, src_wfs.wvl, sep_x_rad, sep_y_rad, lext=atm.lext, zenith=src_sci.zenith_rad)
|
|
322
|
+
psd['aniso-dist'] = psd_aniso * aomskin
|
|
323
|
+
|
|
324
|
+
## NCPA SCIENCE vs WFS
|
|
325
|
+
## after OG computation since it affects only science!
|
|
326
|
+
rad_to_nm = src_wfs.wvl*1e9/(2*np.pi)
|
|
327
|
+
if tel.ncpa > 0:
|
|
328
|
+
psd['ncpa'] = psd_ncpa(tel.ncpa / rad_to_nm, freq, df, tel.diameter)
|
|
329
|
+
|
|
330
|
+
## DIFFERENTIAL CHROMATIC INDEX
|
|
331
|
+
## after OG computation since it affects only science!
|
|
332
|
+
if src_sci.wvl != src_wfs.wvl:
|
|
333
|
+
psd['chromatic'] = psdatmo * aomskin * psd_chromatic_filter(src_wfs.wvl_nm, src_sci.wvl_nm)
|
|
334
|
+
theta_refraction = (air_refractive_index(src_sci.wvl_nm*1e-3)-air_refractive_index(src_wfs.wvl_nm*1e-3))*np.tan(src_sci.zenith_rad)
|
|
335
|
+
psd['refraction'] = psd_anisoplanetism(fx, fy, atm.cn2dh, atm.altitude, src_wfs.wvl, theta_refraction, 0, lext=atm.lext, zenith=src_sci.zenith_rad) * aomskin
|
|
336
|
+
|
|
337
|
+
### GET VARIANCES
|
|
338
|
+
var = {}
|
|
339
|
+
for k in psd.keys():
|
|
340
|
+
if k in ['ncpa', 'photon', 'ron']:
|
|
341
|
+
filt = 1 # these have already been filtered
|
|
342
|
+
else:
|
|
343
|
+
filt = pstflt
|
|
344
|
+
var[k] = np.sum(filt*psd[k]*df**2)
|
|
345
|
+
|
|
346
|
+
if verbose:
|
|
347
|
+
rad_to_nm = src_wfs.wvl*1e9/(2*np.pi)
|
|
348
|
+
std_nm = {k:rad_to_nm*np.sqrt(var[k]) for k in var.keys()}
|
|
349
|
+
print()
|
|
350
|
+
print(' WAVEFRONT ERROR [nm RMS]')
|
|
351
|
+
print_std(std_nm)
|
|
352
|
+
|
|
353
|
+
### RETURN
|
|
354
|
+
param = {'r0_wfs':atm.r0(src_wfs.wvl,zenith=src_sci.zenith_rad),
|
|
355
|
+
'og_avg':og_avg,
|
|
356
|
+
'psf':psf_ao,
|
|
357
|
+
'psd_atmo':psdatmo}
|
|
358
|
+
return psd, var, param
|