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