COMPASS  5.4.4
End-to-end AO simulation tool using GPU acceleration
slopesCovariance.py
1 import numpy as np
2 
3 """
4 ---------------------------------------------------------------------------------------------------------
5 File name: slopesCovariance.py
6 Authors: E. Gendron & F. Vidal
7 Python Version: 3.x
8 Re-written in Python from the original code in Yorick made in 2010 for the CANARY experiment.
9 
10 See also Vidal F., Gendron E. and Rousset G, "Tomography approach for multi-object adaptive optics
11 ", JOSA-A Vol. 27, Issue 11, pp. A253-A264 (2010)
12 
13 This file contains all the routines needed to compute slopes covariance matrices between 2 SH WFS and serves as
14 a basis for the Learn & Apply algorithm.
15 
16 The main function to be called should be: compute_Caa (see example in the function docstring)
17 compute_Caa is a low-level function.
18 
19 To be used properly, compute_Caa shall be called wrapped into several loops:
20 - 2 nested loops over all the WFSs of the experiment
21 - 1 other nested loop (with sum) over the layers
22 in order to fill the whole covariance matrix of the system and
23 integrate over the Cn2(h) profile.
24 
25 The fitting process (LEARN part) shall use a minimization algorithm (such as https://lmfit.github.io/lmfit-py/model.html)
26 to compute the minimum mean square error between the measured covariance slopes matrix from WFSs data
27 and the slopes covariance matrix model provided by this file to estimate the desired atmospheric parameters
28 (I.e fitted parameters are usually Cn2 parameters such as r_0 and altitude).
29 
30 A special care shall be taken in slopes order in the pupil (see example docstring in compute_Caa).
31 
32 ---------------------------------------------------------------------------------------------------------
33 """
34 
35 
36 
37 def KLmodes(x, y, L0, filterPiston):
38  '''
39  Return modes
40  <x, y> : x,y coordinates of the DM actuators.
41  Could come from the output of the function:
42  x, y, mask = generateActuCoordinates(nx, ny, Nactu)
43  OR can be any x,y DM coordinates.
44 
45  <L0> : outer scale, in the same units as x and y.
46  <filterPiston> : True/False, to filter the piston out or not
47 
48 
49  example of use:
50  L0 = 25
51 
52  nx, ny = 64, 64
53  Nactu = 3228
54  x, y, mask = generateActuCoordinates(nx, ny, Nactu)
55 
56  modes = KLmodes(x, y, L0, True)
57  '''
58  Nactu = len(x)
59  # Computation of the matrix of distances (squared !)
60  print('Computing matrix of distances')
61  mdist2 = (x[:, None] - x[None, :])**2 + (y[:, None] - y[None, :])**2
62  # normalisation of distances with respect to pupil diameter
63  D = x.ptp()
64  mdist2 /= D**2
65  print('Computing covariance matrix')
66  if L0 == 0:
67  kolmo = -0.5 * 6.88 * mdist2**(5. / 6)
68  else:
69  kolmo = -0.5 * rodconan(np.sqrt(mdist2), L0 / D)
70  if filterPiston:
71  print('Filtering piston out')
73  kolmo = P.dot(kolmo.dot(P))
74  print('Diagonalisation')
75  # L'unite des valuers propres est en radians^2 a la longueur d'onde ou
76  # est exprime r0, et sachant que tout est normalise pour (D/r0)=1.
77  #
78  l, U = np.linalg.eigh(kolmo)
79  l = l / Nactu
80  print('done')
81  return U[:, ::-1], l[::-1]
82 
83 
85  '''
86  Creates a sqaure matrix that removes (=filter out) piston mode.
87  <n> : size of the matrix
88 
89  P = computePistonFilteringMatrix(64)
90  '''
91  P = np.full((n, n), -1. / n)
92  for i in range(n):
93  P[i, i] += 1.0
94  return P
95 
96 
97 def compute_Caa(vv1, vv2, s1, s2, Cn2h, L0, alt1, alt2, altitude):
98  """
99 
100  This function computes the covariance matrix between TWO wavefront-sensors,
101  each pointing in some particular direction of a given Guide Star for a UNI turbulent
102  layer at a given altitude.
103 
104  ----------------
105  --- Warning! ---
106  ----------------
107 
108  The simulated guide star ---IS ALWAYS--- TT sensitive i.e if the GS is set at a finite altitude, it simulates a LGS with the perfect TT sensing capability.
109  If needed, the TT filtering should be performed afterwards on the covariance matrix by applying an averaging slopes matrix for the given WFS (see computeTTFiltMatrix for TT filtering or Filt_Zer for filtering other Zernikes modes)
110 
111  <vv1>, <vv2> is the array (N,2) of the subaperture coordinates projected
112  onto altitude layer. vv[:,0] is the x-coordinate, vv([:,1] is y.
113  vv1 is for WFS number 1, vv2 for the other. It's up to the user to
114  order the subapertures in the way he expects to find them in the
115  final cov matrix.
116  <s1>, <s2> are the sizes of the subapertures **in the pupil** (i.e.
117  at ground level, i.e. the real physical size in the telescope
118  pupil, not the one projected onto the layer)
119  <Cn2h> strengh of the layer: it must be equal to r0**(-5./3) with r0
120  in metres. Initially this parameter was r0 but the Learn & Apply fit was
121  non-linear. Now the Learn will be searching for <Cn2h> which makes
122  it a linear fit and convergence is faster.
123  <L0> the outer scale (in metres)
124  <alt1>, <alt2>: altitude of the source (the Guide Star) of the WFS (metres). A value
125  of alt==0 is interpreted as alt==infinity.
126  <altitude>: altitude of the layer (metres). Used to recompute the real ssp size at the given altitude.
127 
128  -------------------
129  Example of use:
130  -------------------
131 
132  # Generating Arbitrary ssp coordinates with a 14x14 SH on a 8m telescope:
133  Nssp = 14
134  x = np.linspace(-1,1,Nssp)
135  x, y = np.meshgrid(x,x)
136  r=np.sqrt(x*x+y*y)<1.1
137  vv1 = np.array([x[r].flatten(), y[r].flatten()]).T
138  vv1*=4
139  # We shift the second WFS by 0.5m
140  vv2 =vv1 + 0.5
141 
142  s1=0.5 # Physical size of the ssp in meters of wfs #1
143  s2=0.5 # Physical size of the ssp in meters of wfs #2
144  r0 = 0.5 # in meters
145  Cn2h = r0**(5/3)
146  L0 = 50 # in meters
147 
148  alt1 = 0 # GS altitude 0 meters == infinite GS == NGS
149  alt2 = 0 # GS altitude 0 meters == infinite GS == NGS
150 
151  altitude = 3500 # Altitude of the turbulence layer in meters
152 
153  caa = compute_Caa(vv1, vv2, s1, s2, Cn2h, L0, alt1, alt2, altitude) # Computes the WFS covariance matrix
154 
155  # --------------------------------------------
156  # Optional modes filtering:
157  # --------------------------------------------
158 
159  nbslopes = caa.shape[0] # x +y slopes = total nb of slopes
160  FiltMatrix = computeTTFiltMatrix(nbslopes) # ex with TT filtering (use Filt_Zer for other modes filtering)
161 
162  # Case of Covmat of NGS/LGS:
163  caaFilt = np.dot(filtTTMat, caa)
164 
165  # Case of Covmat of LGS/LGS:
166  caaFilt = np.dot(np.dot(filtTTMat, caa), filtTTMat.T)
167 
168  """
169 
170  # vv are the subaperture coordinates projected onto altitude layer
171  vx1 = vv1[:,0]
172  vy1 = vv1[:,1]
173  vx2 = vv2[:,0]
174  vy2 = vv2[:,1]
175 
176  # matrix of distances in x and y for all subapertures couples between 2 WFSs.
177  vx = vx1[:, None] - vx2[None, :]
178  vy = vy1[:, None] - vy2[None, :]
179  s1_pup = s1
180  s2_pup = s2
181  # rescale the subaperture size the projected size of a subaperture at a given altitude layer
182  if alt1 > 0:
183  s1 = s1*(alt1-altitude)/alt1
184  if alt2 > 0:
185  s2 = s2*(alt2-altitude)/alt2
186  nssp1 = vx1.shape[0] #number of sub-apertures of 1st WFS
187  nssp2 = vx2.shape[0] #number of sub-apertures of 2nd WFS
188  # test if the altitude layers is higher than the LGS altitude
189  if (s1 <= 0) or (s2 <= 0):
190  return np.zeros((2*nssp1, 2*nssp2))
191 
192  # Faut calculer la covariance en altitude et la ramener dans le plan pupille
193  ac = s1/2-s2/2
194  ad = s1/2+s2/2
195  bc = -s1/2-s2/2
196  bd = -s1/2+s2/2
197 
198  # Caa x-x
199  caa_xx = -DPHI(vx+ac,vy,L0) + DPHI(vx+ad,vy,L0) + DPHI(vx+bc,vy,L0) - DPHI(vx+bd,vy,L0)
200  caa_xx /= 2.
201 
202  # Caa y-y
203  caa_yy = -DPHI(vx,vy+ac,L0) + DPHI(vx,vy+ad,L0) + DPHI(vx,vy+bc,L0) - DPHI(vx,vy+bd,L0)
204  caa_yy /= 2.
205 
206  if False:
207  # Calcul du rico pour ameliorer l'exactitude du modele en xy
208  # Bon on desactive quand meme... c'est idem a 1e-5 pres
209  s0 = np.sqrt(s1**2+s2**2) # size of the subaperture equivalent to a convolution by s1 and s2
210  caa_xy = -DPHI(vx+s0/2,vy-s0/2,L0) + DPHI(vx+s0/2,vy+s0/2,L0) + DPHI(vx-s0/2,vy-s0/2,L0) - DPHI(vx-s0/2,vy+s0/2,L0)
211  caa_xy /= 2.
212  caa_xy /= 2.
213  # et la, calcul d'une correction car le modele est loin d etre parfait ...
214  r = np.max([s1,s2])/np.min([s1,s2])
215  coeff = 1./(1-(1-1/r)**2)
216  caa_xy /= coeff
217  else:
218  caa_xy = -DPHI(vx+s1/2,vy-s2/2,L0) + DPHI(vx+s1/2,vy+s2/2,L0) + DPHI(vx-s1/2,vy-s2/2,L0) - DPHI(vx-s1/2,vy+s2/2,L0)
219  caa_xy /= 2.
220 
221  caa = np.zeros((2*nssp1, 2*nssp2))
222  caa[:nssp1, :nssp2] = caa_xx
223  caa[:nssp1, nssp2:nssp2*2] = caa_xy
224  caa[nssp1:nssp1*2, :nssp2] = caa_xy
225  caa[nssp1:nssp1*2, nssp2:nssp2*2] = caa_yy
226  #units
227  k = 1.0/s1_pup/s2_pup
228  lambda2 = (206265*0.5e-6/2/np.pi)**2
229  caa *=k*lambda2*(np.abs(Cn2h))
230 
231  return caa
232 
233 
234 
235 
236 
237 def macdo_x56(x,k=10):
238  """
239  Computation of the Mc Donald function.
240 
241  f(x) = x**(5/6)*K_{5/6}(x)
242  using a series for the esimation of K_{5/6}, taken from Rod Conan thesis :
243  K_a(x)=1/2 \sum_{n=0}**\infty \frac{(-1)**n}{n!}
244  \left(\Gamma(-n-a) (x/2)**{2n+a} + \Gamma(-n+a) (x/2)**{2n-a} \right) ,
245  with a = 5/6.
246 
247  Setting x22 = (x/2)**2, setting uda = (1/2)**a, and multiplying by x**a,
248  this becomes :
249  x**a * Ka(x) = 0.5 $ -1**n / n! [ G(-n-a).uda x22**(n+a) + G(-n+a)/uda x22**n ]
250  Then we use the following recurrence formulae on the following quantities :
251  G(-(n+1)-a) = G(-n-a) / -a-n-1
252  G(-(n+1)+a) = G(-n+a) / a-n-1
253  (n+1)! = n! * (n+1)
254  x22**(n+1) = x22**n * x22
255  and at each iteration on n, one will use the values already computed
256  at step (n-1).
257  The values of G(a) and G(-a) are hardcoded instead of being computed.
258 
259  The first term of the series has also been skipped, as it
260  vanishes with another term in the expression of Dphi.
261 
262  """
263  x = np.array(x) # Safe check
264  a = 5./6.
265  fn = 1. # initialisation factorielle 0!=1
266  x2a = x**(2.*a)
267  x22 = x*x/4. # (x/2)**2
268  x2n = 0.5 # init (1/2) * x**0
269  Ga = 2.01126983599717856777 # Gamma(a) / (1/2)**a
270  Gma = -3.74878707653729348337 # Gamma(-a) * (1/2.)**a
271  s = np.zeros(x.shape)
272  for n in range(k+1):
273  dd = Gma * x2a
274  if n:
275  dd += Ga
276  dd *= x2n
277  dd /= fn
278  # addition to s, with multiplication by (-1)**n
279  if n%2:
280  s -= dd
281  else:
282  s += dd
283  # prepare recurrence iteration for next step
284  if n<k:
285  fn *= n+1 # factorial
286  Gma /= -a-n-1 # gamma function
287  Ga /= a-n-1 # idem
288  x2n *= x22 # x**n
289  return s
290 
291 
292 
293 def asymp_macdo(x):
294  """
295  Computes a term involved in the computation of the phase struct
296  function with a finite outer scale according to the Von-Karman
297  model. The term involves the MacDonald function (modified bessel
298  function of second kind) K_{5/6}(x), and the algorithm uses the
299  asymptotic form for x ~ infinity.
300  Warnings :
301  - This function makes a floating point interrupt for x=0
302  and should not be used in this case.
303  - Works only for x>0.
304 
305  """
306  x = np.array(x)
307  # k2 is the value for
308  # gamma_R(5./6)*2**(-1./6)
309  k2 = 1.00563491799858928388289314170833
310  k3 = 1.25331413731550012081 # sqrt(pi/2)
311  a1 = 0.22222222222222222222 # 2/9
312  a2 = -0.08641975308641974829 # -7/89
313  a3 = 0.08001828989483310284 # 175/2187
314  x_1 = 1./x
315  res = k2 - k3*np.exp(-x)*x**(1/3.)*(1.0 + x_1*(a1 + x_1*(a2 + x_1*a3)))
316  return res
317 
318 
319 
320 
321 def rodconan(r,L0,k=10):
322  """ DOCUMENT rodconan(r,L0,k=)
323  The phase structure function is computed from the expression
324  Dphi(r) = k1 * L0**(5./3) * (k2 - (2.pi.r/L0)**5/6 K_{5/6}(2.pi.r/L0))
325 
326  For small r, the expression is computed from a development of
327  K_5/6 near 0. The value of k2 is not used, as this same value
328  appears in the series and cancels with k2.
329  For large r, the expression is taken from an asymptotic form.
330 
331  """
332  # k1 is the value of :
333  # 2*gamma_R(11./6)*2**(-5./6)*pi**(-8./3)*(24*gamma_R(6./5)/5.)**(5./6)
334  k1 = 0.1716613621245709486
335  dprf0 = (2*np.pi/L0)*r
336  # k2 is the value for gamma_R(5./6)*2**(-1./6),
337  # but is now unused
338  # k2 = 1.0056349179985892838
339  res = np.zeros(r.shape)
340  Xlim = 0.75*2*np.pi
341  largeX = dprf0>Xlim
342 
343  res[largeX] = asymp_macdo(dprf0[largeX])
344  smallX = np.logical_not(largeX)
345  res[smallX] = -macdo_x56(dprf0[smallX], k=k)
346  return (k1 * L0**(5./3)) * res
347 
348 
349 
350 
351 
352 def DPHI(x,y,L0):
353  """
354  dphi = DPHI(x,y,L0) * r0**(-5./3)
355 
356  Computes the phase structure function for a separation (x,y).
357  The r0 is not taken into account : the final result of DPHI(x,y,L0)
358  has to be scaled with r0**-5/3, with r0 expressed in meters, to get
359  the right value.
360  """
361 
362  r = np.sqrt(x**2+y**2)
363 
364  """ BEFORE ...... when rod conan did not exist
365  fracDim = 5./3. # Can vary fracDim for those who do not believe in Kolmogorov...
366  r53 = r**(fracDim)
367  return 6.88*r53
368  """
369  # With L0 ......
370  return rodconan(r, L0)
371 
372 
373 
374 
375 def computeTTFiltMatrix(nbslopes):
376  """
377  Returns a matrix <p1> that filters the average TT on the vector of slopes <vec>
378  with nbslopes = x+y slopes
379 
380  ---------------
381  Example of use:
382  ---------------
383 
384  nbslopes = 300
385  p1 = computeTTFiltMatrix(nbslopes)
386  vec_filt = np.dot(p1,vec)
387 
388  """
389  p = filt_TTvec(nbslopes)
390  Id = np.identity(nbslopes)
391  p1 = Id-p
392  return p1
393 
394 
395 def filt_TTvec(nbslopes):
396  """
397  Returns a matrix p that averages the slopes over x and y.
398 
399  ---------------
400  Example of use:
401  ---------------
402 
403  nbslopes = 300
404  p = filt_TTvec(nbslopes)
405  vec_filtered = np.dot(p,vec)
406 
407  """
408  p = np.zeros((nbslopes, nbslopes))
409  p[0:nbslopes//2,0:nbslopes//2] = 1
410  p[nbslopes//2:,nbslopes//2:] = 1
411  p/=nbslopes/2
412 
413  return p
414 
415 
416 def Filt_Zer(modesfilt, miz):
417  """
418  MatFiltZer = Filt_Zer(modesfilt, miz);
419  This function generates a matrix that filters all the zernikes modes up to <modesfilt>
420  It requires a zernike interaction matrix <miz> of the given the WFS
421 
422  ---------------
423  Example of use:
424  ---------------
425 
426  modesfilt = 10
427  MatFiltZer = Filt_Zer(modesfilt, miz)
428 
429  The filtered vector can be computed using:
430  vec_filt = np.dot(MatFiltZer,miz)
431 
432  With <miz> computed using doit:
433  from hraa.tools.doit import doit
434  nssp = 14
435  obs = 0
436  nbzer = 100
437  miz = doit(nssp, obs, nbzer)
438 
439  """
440  mizFilt = miz[:,:modesfilt]
441  mrz = np.linalg.pinv(mizFilt)
442  MatFiltZer = np.identity(miz.shape[0]) - np.dot(mizFilt, mrz)
443  return MatFiltZer
def computeTTFiltMatrix(nbslopes)
Returns a matrix <p1> that filters the average TT on the vector of slopes <vec> with nbslopes = x+y s...
def rodconan(r, L0, k=10)
DOCUMENT rodconan(r,L0,k=) The phase structure function is computed from the expression Dphi(r) = k1 ...
def macdo_x56(x, k=10)
Computation of the Mc Donald function.
def compute_Caa(vv1, vv2, s1, s2, Cn2h, L0, alt1, alt2, altitude)
This function computes the covariance matrix between TWO wavefront-sensors, each pointing in some par...
def computePistonFilteringMatrix(n)
Creates a sqaure matrix that removes (=filter out) piston mode.
def filt_TTvec(nbslopes)
Returns a matrix p that averages the slopes over x and y.
def KLmodes(x, y, L0, filterPiston)
Return modes <x, y> : x,y coordinates of the DM actuators.
def asymp_macdo(x)
Computes a term involved in the computation of the phase struct function with a finite outer scale ac...
def Filt_Zer(modesfilt, miz)