COMPASS  5.4.4
End-to-end AO simulation tool using GPU acceleration
dm_init.py
1 
37 
38 import shesha.config as conf
39 import shesha.constants as scons
40 
41 from shesha.constants import CONST
42 
43 from shesha.util import dm_util, influ_util, kl_util
44 from shesha.util import hdf5_util as h5u
45 
46 import numpy as np
47 
48 import pandas as pd
49 from scipy import interpolate
50 from shesha.sutra_wrap import carmaWrap_context, Dms
51 
52 from typing import List
53 
54 from rich.progress import track
55 
56 import os
57 try:
58  shesha_dm = os.environ['SHESHA_DM_ROOT']
59 except KeyError as err:
60  shesha_dm = os.environ['SHESHA_ROOT'] + "/data/dm-data"
61 
62 
63 def dm_init(context: carmaWrap_context, p_dms: List[conf.Param_dm],
64  p_tel: conf.Param_tel, p_geom: conf.Param_geom,
65  p_wfss: List[conf.Param_wfs] = None) -> Dms:
66  """Create and initialize a Dms object on the gpu
67 
68  Args:
69  context: (carmaWrap_context): context
70  p_dms: (list of Param_dms) : dms settings
71  p_tel: (Param_tel) : telescope settings
72  p_geom: (Param_geom) : geom settings
73  p_wfss: (list of Param_wfs) : wfs settings
74  :return:
75  Dms: (Dms): Dms object
76  """
77  max_extent = 0
78  if (p_wfss is not None):
79  xpos_wfs = []
80  ypos_wfs = []
81  for i in range(len(p_wfss)):
82  xpos_wfs.append(p_wfss[i].xpos)
83  ypos_wfs.append(p_wfss[i].ypos)
84  else:
85  xpos_wfs = [0]
86  ypos_wfs = [0]
87  if (len(p_dms) != 0):
88  dms = Dms()
89  types_dm = [p_dm.type for p_dm in p_dms]
90  if scons.DmType.TT in types_dm:
91  first_TT = types_dm.index(scons.DmType.TT)
92  if np.any(np.array(types_dm[first_TT:]) != scons.DmType.TT):
93  raise RuntimeError("TT must be defined at the end of the dms parameters")
94 
95  for i in range(len(p_dms)):
96  max_extent = _dm_init(context, dms, p_dms[i], xpos_wfs, ypos_wfs, p_geom,
97  p_tel.diam, p_tel.cobs, p_tel.pupangle, max_extent)
98 
99  return dms
100 
101 
102 def _dm_init(context: carmaWrap_context, dms: Dms, p_dm: conf.Param_dm, xpos_wfs: list,
103  ypos_wfs: list, p_geom: conf.Param_geom, diam: float, cobs: float,
104  pupAngle: float, max_extent: int):
105  """ inits a Dms object on the gpu
106 
107  Args:
108  context: (carmaWrap_context): context
109  dms: (Dms) : dm object
110 
111  p_dm: (Param_dms) : dm settings
112 
113  xpos_wfs: (list) : list of wfs xpos
114 
115  ypos_wfs: (list) : list of wfs ypos
116 
117  p_geom: (Param_geom) : geom settings
118 
119  diam: (float) : diameter of telescope
120 
121  cobs: (float) : cobs of telescope
122 
123  pupAngle: (float) : rotation/clocking angle of the pupil in degrees
124 
125  max_extent: (int) : maximum dimension of all dms
126 
127  :return:
128  max_extent: (int) : new maximum dimension of all dms
129 
130  """
131 
132  if (p_dm.pupoffset is not None):
133  p_dm._puppixoffset = p_dm.pupoffset / diam * p_geom.pupdiam
134  # For patchDiam
135  patchDiam = dm_util.dim_dm_patch(p_geom.pupdiam, diam, p_dm.type, p_dm.alt, xpos_wfs,
136  ypos_wfs)
137 
138  if (p_dm.type == scons.DmType.PZT):
139  if p_dm.file_influ_fits == None:
140  if p_dm._pitch is None:
141  p_dm._pitch = patchDiam / float(p_dm.nact - 1)
142  print(f"DM pitch = {p_dm._pitch:8.5f} pix = {p_dm._pitch*diam/p_geom.pupdiam:8.5f} m",
143  flush=True)
144  # + 2.5 pitch each side
145  extent = p_dm._pitch * (p_dm.nact + p_dm.pzt_extent)
146  p_dm._n1, p_dm._n2 = dm_util.dim_dm_support(p_geom.cent, extent,
147  p_geom.ssize)
148 
149  # calcul defaut influsize
150  make_pzt_dm(p_dm, p_geom, cobs, pupAngle)
151  else:
152  init_custom_dm(p_dm, p_geom, diam)
153 
154  # max_extent
155  max_extent = max(max_extent, p_dm._n2 - p_dm._n1 + 1)
156 
157  dim = max(p_dm._n2 - p_dm._n1 + 1, p_geom._mpupil.shape[0])
158  p_dm._dim_screen = dim
159  ninflupos = p_dm._influpos.size
160  n_npts = p_dm._ninflu.size #// 2
161  dms.add_dm(context, p_dm.type, p_dm.alt, dim, p_dm._ntotact, p_dm._influsize,
162  ninflupos, n_npts, p_dm.push4imat, 0, context.active_device)
163  #infludata = p_dm._influ.flatten()[p_dm._influpos]
164  dms.d_dms[-1].pzt_loadarrays(p_dm._influ, p_dm._influpos.astype(np.int32),
165  p_dm._ninflu, p_dm._influstart, p_dm._i1, p_dm._j1)
166 
167  elif (p_dm.type == scons.DmType.TT):
168 
169  if (p_dm.alt == 0) and (max_extent != 0):
170  extent = int(max_extent * 1.05)
171  if (extent % 2 != 0):
172  extent += 1
173  else:
174  extent = p_geom.pupdiam + 16
175  p_dm._n1, p_dm._n2 = dm_util.dim_dm_support(p_geom.cent, extent, p_geom.ssize)
176  # max_extent
177  max_extent = max(max_extent, p_dm._n2 - p_dm._n1 + 1)
178 
179  dim = p_dm._n2 - p_dm._n1 + 1
180  make_tiptilt_dm(p_dm, patchDiam, p_geom, diam)
181  p_dm._dim_screen = dim
182  dms.add_dm(context, p_dm.type, p_dm.alt, dim, 2, dim, 1, 1, p_dm.push4imat, 0,
183  context.active_device)
184  dms.d_dms[-1].tt_loadarrays(p_dm._influ)
185 
186  elif (p_dm.type == scons.DmType.KL):
187 
188  extent = p_geom.pupdiam + 16
189  p_dm._n1, p_dm._n2 = dm_util.dim_dm_support(p_geom.cent, extent, p_geom.ssize)
190  # max_extent
191  max_extent = max(max_extent, p_dm._n2 - p_dm._n1 + 1)
192 
193  dim = p_dm._n2 - p_dm._n1 + 1
194 
195  make_kl_dm(p_dm, patchDiam, p_geom, cobs)
196 
197  ninflu = p_dm.nkl
198  p_dm._dim_screen = dim
199 
200  dms.add_dm(context, p_dm.type, p_dm.alt, dim, p_dm.nkl, p_dm._ncp, p_dm._nr,
201  p_dm._npp, p_dm.push4imat, p_dm._ord.max(), context.active_device)
202 
203  dms.d_dms[-1].kl_loadarrays(p_dm._rabas, p_dm._azbas, p_dm._ord, p_dm._cr,
204  p_dm._cp)
205 
206  else:
207 
208  raise TypeError("This type of DM doesn't exist ")
209  # Verif
210  # res1 = pol2car(*y_dm(n)._klbas,gkl_sfi(*y_dm(n)._klbas, 1));
211  # res2 = yoga_getkl(g_dm,0.,1);
212 
213  return max_extent
214 
215 
216 def _dm_init_factorized(context: carmaWrap_context, dms: Dms, p_dm: conf.Param_dm,
217  xpos_wfs: list, ypos_wfs: list, p_geom: conf.Param_geom,
218  diam: float, cobs: float, pupAngle: float, max_extent: int):
219  """ inits a Dms object on the gpu
220  NOTE: This is the
221 
222  Args:
223  context: (carmaWrap_context): context
224 
225  dms: (Dms) : dm object
226 
227  p_dm: (Param_dms) : dm settings
228 
229  xpos_wfs: (list) : list of wfs xpos
230 
231  ypos_wfs: (list) : list of wfs ypos
232 
233  p_geom: (Param_geom) : geom settings
234 
235  diam: (float) : diameter of telescope
236 
237  cobs: (float) : cobs of telescope
238 
239  pupAngle: (float) : rotation/clocking angle of the pupil in degrees
240 
241  max_extent: (int) : maximum dimension of all dms
242 
243  :return:
244  max_extent: (int) : new maximum dimension of all dms
245 
246  """
247 
248  if (p_dm.pupoffset is not None):
249  p_dm._puppixoffset = p_dm.pupoffset / diam * p_geom.pupdiam
250  # For patchDiam
251  patchDiam = dm_util.dim_dm_patch(p_geom.pupdiam, diam, p_dm.type, p_dm.alt, xpos_wfs,
252  ypos_wfs)
253 
254  if (p_dm.type == scons.DmType.PZT) and p_dm.file_influ_fits is not None:
255  init_custom_dm(p_dm, p_geom, diam)
256  else:
257  if (p_dm.type == scons.DmType.PZT):
258  p_dm._pitch = patchDiam / float(p_dm.nact - 1)
259  # + 2.5 pitch each side
260  extent = p_dm._pitch * (p_dm.nact + p_dm.pzt_extent)
261 
262  # calcul defaut influsize
263  make_pzt_dm(p_dm, p_geom, cobs, pupAngle)
264 
265  elif (p_dm.type == scons.DmType.TT):
266  if (p_dm.alt == 0) and (max_extent != 0):
267  extent = int(max_extent * 1.05)
268  if (extent % 2 != 0):
269  extent += 1
270  else:
271  extent = p_geom.pupdiam + 16
272 
273  elif (p_dm.type == scons.DmType.KL):
274  extent = p_geom.pupdiam + 16
275  else:
276  raise TypeError("This type of DM doesn't exist ")
277 
278  # Verif
279  # res1 = pol2car(*y_dm(n)._klbas,gkl_sfi(*y_dm(n)._klbas, 1));
280  # res2 = yoga_getkl(g_dm,0.,1);
281 
282  p_dm._n1, p_dm._n2 = dm_util.dim_dm_support(p_geom.cent, extent, p_geom.ssize)
283 
284  # max_extent
285  max_extent = max(max_extent, p_dm._n2 - p_dm._n1 + 1)
286 
287  dim = max(p_dm._n2 - p_dm._n1 + 1, p_geom._mpupil.shape[0])
288 
289  if (p_dm.type == scons.DmType.PZT):
290  ninflupos = p_dm._influpos.size
291  n_npts = p_dm._ninflu.size #// 2
292  dms.add_dm(context, p_dm.type, p_dm.alt, dim, p_dm._ntotact, p_dm._influsize,
293  ninflupos, n_npts, p_dm.push4imat, 0, context.active_device)
294  #infludata = p_dm._influ.flatten()[p_dm._influpos]
295  dms.d_dms[-1].pzt_loadarrays(p_dm._influ, p_dm._influpos.astype(np.int32),
296  p_dm._ninflu, p_dm._influstart, p_dm._i1, p_dm._j1)
297  elif (p_dm.type == scons.DmType.TT):
298  make_tiptilt_dm(p_dm, patchDiam, p_geom, diam)
299  dms.add_dm(context, p_dm.type, p_dm.alt, dim, 2, dim, 1, 1, p_dm.push4imat, 0,
300  context.active_device)
301  dms.d_dms[-1].tt_loadarrays(p_dm._influ)
302  elif (p_dm.type == scons.DmType.KL):
303  make_kl_dm(p_dm, patchDiam, p_geom, cobs)
304  ninflu = p_dm.nkl
305 
306  dms.add_dm(context, p_dm.type, p_dm.alt, dim, p_dm.nkl, p_dm._ncp, p_dm._nr,
307  p_dm._npp, p_dm.push4imat, p_dm._ord.max(), context.active_device)
308  dms.d_dms[-1].kl_loadarrays(p_dm._rabas, p_dm._azbas, p_dm._ord, p_dm._cr,
309  p_dm._cp)
310 
311  return max_extent
312 
313 
314 def dm_init_standalone(context: carmaWrap_context, p_dms: list, p_geom: conf.Param_geom,
315  diam=1., cobs=0., pupAngle=0., wfs_xpos=[0], wfs_ypos=[0]):
316  """Create and initialize a Dms object on the gpu
317 
318  Args:
319  p_dms: (list of Param_dms) : dms settings
320 
321  p_geom: (Param_geom) : geom settings
322 
323  diam: (float) : diameter of telescope (default 1.)
324 
325  cobs: (float) : cobs of telescope (default 0.)
326 
327  pupAngle: (float) : pupil rotation angle (degrees, default 0.)
328 
329  wfs_xpos: (array) : guide star x position on sky (in arcsec).
330 
331  wfs_ypos: (array) : guide star y position on sky (in arcsec).
332 
333  """
334  max_extent = [0]
335  if (len(p_dms) != 0):
336  dms = Dms()
337  for i in range(len(p_dms)):
338  _dm_init(context, dms, p_dms[i], wfs_xpos, wfs_ypos, p_geom, diam, cobs,
339  pupAngle, max_extent)
340  return dms
341 
342 
343 def make_pzt_dm(p_dm: conf.Param_dm, p_geom: conf.Param_geom, cobs: float,
344  pupAngle: float):
345  """Compute the actuators positions and the influence functions for a pzt DM.
346  NOTE: if the DM is in altitude, central obstruction is forced to 0
347 
348  Args:
349  p_dm: (Param_dm) : dm parameters
350 
351  p_geom: (Param_geom) : geometry parameters
352 
353  cobs: (float) : telescope central obstruction
354 
355  pupAngle: (float) : rotation/clocking angle of the pupil in degrees
356 
357  Returns:
358  influ: (np.ndarray(dims=3, dtype=np.float64)) : cube of the IF for each actuator
359 
360  """
361  # Petal DM (segmentation of M4)
362  if (p_dm.influ_type == scons.InfluType.PETAL):
363  makePetalDm(p_dm, p_geom, pupAngle)
364  return
365 
366  # prepare to compute IF on partial (local) support of size <smallsize>
367  coupling = p_dm.coupling
368  pitch = p_dm._pitch # unit is pixels
369  smallsize = 0
370 
371  if (p_dm.influ_type == scons.InfluType.RADIALSCHWARTZ):
372  smallsize = influ_util.makeRadialSchwartz(pitch, coupling)
373  elif (p_dm.influ_type == scons.InfluType.SQUARESCHWARTZ):
374  smallsize = influ_util.makeSquareSchwartz(pitch, coupling)
375  elif (p_dm.influ_type == scons.InfluType.BLACKNUTT):
376  smallsize = influ_util.makeBlacknutt(pitch, coupling)
377  elif (p_dm.influ_type == scons.InfluType.GAUSSIAN):
378  smallsize = influ_util.makeGaussian(pitch, coupling)
379  elif (p_dm.influ_type == scons.InfluType.BESSEL):
380  smallsize = influ_util.makeBessel(pitch, coupling, p_dm.type_pattern)
381  elif (p_dm.influ_type == scons.InfluType.DEFAULT):
382  smallsize = influ_util.makeRigaut(pitch, coupling)
383  else:
384  print("ERROR influtype not recognized ")
385  p_dm._influsize = smallsize
386 
387  # compute location (x,y and i,j) of each actuator:
388  if p_dm.type_pattern is None:
389  p_dm.type_pattern = scons.PatternType.SQUARE
390 
391  if p_dm.type_pattern == scons.PatternType.HEXA:
392  print("Pattern type : hexa")
393  xypos = dm_util.createHexaPattern(pitch, p_geom.pupdiam * 1.1)
394  p_dm.keep_all_actu = True
395  elif p_dm.type_pattern == scons.PatternType.HEXAM4:
396  print("Pattern type : hexaM4")
397  p_dm.keep_all_actu = True
398  xypos = dm_util.createDoubleHexaPattern(pitch, p_geom.pupdiam * 1.1, pupAngle)
399  if p_dm.margin_out is not None:
400  print(f'p_dm.margin_out={p_dm.margin_out} is being '
401  'used for pupil-based actuator filtering')
402  pup_side = p_geom._ipupil.shape[0]
403  cub_off = dm_util.filterActuWithPupil(xypos + pup_side // 2 - 0.5,
404  p_geom._ipupil,
405  p_dm.margin_out * p_dm.get_pitch())
406  xypos = cub_off - pup_side // 2 + 0.5
407  p_dm.set_ntotact(xypos.shape[1])
408  elif p_dm.type_pattern == scons.PatternType.SQUARE:
409  print("Pattern type : square")
410  xypos = dm_util.createSquarePattern(pitch, p_dm.nact + 4)
411  else:
412  raise ValueError("This pattern does not exist for pzt dm")
413 
414  if p_dm.keep_all_actu:
415  inbigcirc = np.arange(xypos.shape[1])
416  else:
417  if (p_dm.alt > 0):
418  cobs = 0
419  inbigcirc = dm_util.select_actuators(xypos[0, :], xypos[1, :], p_dm.nact,
420  p_dm._pitch, cobs, p_dm.margin_in,
421  p_dm.margin_out, p_dm._ntotact)
422  p_dm._ntotact = inbigcirc.size
423 
424  # converting to array coordinates:
425  xypos += p_geom.cent
426 
427  # filtering actuators outside of a disk radius = rad (see above)
428  xypos = xypos[:, inbigcirc]
429  ntotact = xypos.shape[1]
430  xpos = xypos[0, :]
431  ypos = xypos[1, :]
432  i1t = (xpos - smallsize / 2 - 0.5 - p_dm._n1).astype(np.int32)
433  j1t = (ypos - smallsize / 2 - 0.5 - p_dm._n1).astype(np.int32)
434 
435  p_dm._xpos = xpos
436  p_dm._ypos = ypos
437  p_dm._i1 = i1t
438  p_dm._j1 = j1t
439 
440  # Allocate array of influence functions
441  influ = np.zeros((smallsize, smallsize, ntotact), dtype=np.float32)
442 
443  # Computation of influence function for each actuator
444  print("Computing Influence Function type : ", p_dm.influ_type)
445  for i in track(range(ntotact)):
446 
447  i1 = i1t[i]
448  x = np.tile(np.arange(i1, i1 + smallsize, dtype=np.float32),
449  (smallsize, 1)).T # pixel coords in ref frame "dm support"
450  # pixel coords in ref frame "pupil support"
451  x += p_dm._n1
452  # pixel coords in local ref frame
453  x -= xpos[i]
454 
455  j1 = j1t[i]
456  y = np.tile(np.arange(j1, j1 + smallsize, dtype=np.float32),
457  (smallsize, 1)) # idem as X, in Y
458  y += p_dm._n1
459  y -= ypos[i]
460  # print("Computing Influence Function #%d/%d \r" % (i, ntotact), end=' ')
461 
462  if (p_dm.influ_type == scons.InfluType.RADIALSCHWARTZ):
463  influ[:, :, i] = influ_util.makeRadialSchwartz(pitch, coupling, x=x, y=y)
464  elif (p_dm.influ_type == scons.InfluType.SQUARESCHWARTZ):
465  influ[:, :, i] = influ_util.makeSquareSchwartz(pitch, coupling, x=x, y=y)
466  elif (p_dm.influ_type == scons.InfluType.BLACKNUTT):
467  influ[:, :, i] = influ_util.makeBlacknutt(pitch, coupling, x=x, y=y)
468  elif (p_dm.influ_type == scons.InfluType.GAUSSIAN):
469  influ[:, :, i] = influ_util.makeGaussian(pitch, coupling, x=x, y=y)
470  elif (p_dm.influ_type == scons.InfluType.BESSEL):
471  influ[:, :, i] = influ_util.makeBessel(pitch, coupling, x=x, y=y,
472  patternType=p_dm.type_pattern)
473  elif (p_dm.influ_type == scons.InfluType.DEFAULT):
474  influ[:, :, i] = influ_util.makeRigaut(pitch, coupling, x=x, y=y)
475  else:
476  print("ERROR influtype not recognized (defaut or gaussian or bessel)")
477 
478  if (p_dm._puppixoffset is not None):
479  xpos += p_dm._puppixoffset[0]
480  ypos += p_dm._puppixoffset[1]
481 
482  if p_dm.segmented_mirror:
483  # mpupil to ipupil shift
484  # Good centering assumptions
485  s = (p_geom._ipupil.shape[0] - p_geom._mpupil.shape[0]) // 2
486 
487  from skimage.morphology import label
488  k = 0
489  for i in track(range(ntotact)):
490  # Pupil area corresponding to influ data
491  i1, j1 = i1t[i] + s - smallsize // 2, j1t[i] + s - smallsize // 2
492  pupilSnapshot = p_geom._ipupil[i1:i1 + smallsize, j1:j1 + smallsize]
493  if np.all(pupilSnapshot): # We have at least one non-pupil pixel
494  continue
495  labels, num = label(pupilSnapshot, background=0, return_num=True)
496  if num <= 1:
497  continue
498  k += 1
499  maxPerArea = np.array([
500  (influ[:, :, i] * (labels == k).astype(np.float32)).max()
501  for k in range(1, num + 1)
502  ])
503  influ[:, :, i] *= (labels == np.argmax(maxPerArea) + 1).astype(np.float32)
504  print(f'{k} cross-spider influence functions trimmed.')
505 
506  influ = influ * float(p_dm.unitpervolt / np.max(influ))
507 
508  p_dm._influ = influ
509 
510  comp_dmgeom(p_dm, p_geom)
511 
512  dim = max(p_geom._mpupil.shape[0], p_dm._n2 - p_dm._n1 + 1)
513  off = (dim - p_dm._influsize) // 2
514 
515 
516 
517 def init_custom_dm(p_dm: conf.Param_dm, p_geom: conf.Param_geom, diam: float):
518  """Read Fits for influence pzt fonction and form
519 
520  Args:
521  p_dm: (Param_dm) : dm settings
522 
523  p_geom: (Param_geom) : geom settings
524 
525  diam: (float) : tel diameter
526 
527  Conversion. There are sereval coordinate systems.
528  Some are coming from the input fits file, others from compass.
529  Those systems differ by scales and offsets.
530  Coord systems from the input fits file:
531  - they all have the same scale: the coordinates are expressed in
532  pixels
533  - one system is the single common frame where fits data are described [i]
534  - one is local to the minimap [l]
535  Coord systems from compass:
536  - they all have the same scale (different from fits one) expressed in
537  pixels
538  - one system is attached to ipupil (i=image, largest support) [i]
539  - one system is attached to mpupil (m=medium, medium support) [m]
540  - one system is local to minimap [l)]
541 
542  Variables will be named using
543  f, c: define either fits or compass
544 
545  """
546  from astropy.io import fits as pfits
547 
548  # read fits file
549  file_name = p_dm.file_influ_fits
550  if(not os.path.isfile(file_name)):
551  file_name = shesha_dm + "/" + p_dm.file_influ_fits
552 
553  hdul = pfits.open(file_name)
554  print("Read influence function from fits file : ", file_name)
555 
556  dm_fits_version = hdul[0].header['VERSION']
557 
558  # read mandatory keywords from the FITS file
559  f_xC = hdul[0].header['XCENTER']
560  f_yC = hdul[0].header['YCENTER']
561  f_pixsize = hdul[0].header['PIXSIZE']
562  if(dm_fits_version < 1.2 ):
563  fi_i1 , fi_j1 = hdul[1].data
564  f_influ = hdul[2].data
565  f_xpos, f_ypos = hdul[3].data
566  else:
567  fi_i1, fi_j1 = hdul['I1_J1'].data
568  f_influ = hdul['INFLU'].data
569  f_xpos, f_ypos = hdul['XPOS_YPOS'].data
570 
571  # Analysis of the requirements set in the COMPASS configuration file
572  cases = [ p_dm.diam_dm is not None, p_dm._pitch is not None,
573  p_dm.diam_dm_proj is not None ]
574 
575  # Projecting the dm in the tel pupil plane with the desired factor
576  if cases == [False, False, False]:
577  f_pupm = hdul[0].header['PUPM']
578  scale = diam / f_pupm
579  print('Custom DM: stretching DM to fit PUPM (%f) to compass (%f)' % (f_pupm, diam))
580  elif cases == [True, False, False]:
581  scale = diam / p_dm.diam_dm
582  elif cases == [False, True, False]:
583  f_pitchm = hdul[0].header['PITCHM']
584  scale = p_dm._pitch / f_pitchm
585  elif cases == [False, False, True]:
586  f_pupm = hdul[0].header['PUPM']
587  scale = p_dm.diam_dm_proj / f_pupm
588  else:
589  err_msg = '''Invalid rescaling parameters
590  To set the scale of the custom dm, MAXIMUM ONE of the following parameters should be set:
591  - p_dm.set_pitch(val) : if the pitch in the tel pupil plane is known
592  - p_dm.set_diam_dm(val) : if the pupil diameter in the dm plane is known
593  - p_dm.set_diam_dm_proj(val) : if the dm pupil diameter projected in the tel pupil plane is known
594  If none of the above is set, the dm will be scaled so that the PUPM parameter in the fits file matches the tel pupil.
595  '''
596  raise RuntimeError(err_msg)
597 
598  f_pixsize *= scale
599  print("Custom dm scaling factor to pupil plane :", scale)
600 
601  # Scaling factor from fits to compass system. The float32() acts as a
602  # roundoff, required to avoid weird behavior when exporting/importing
603  # custom DMs from/to compass
604  scaleToCompass = np.float32(f_pixsize) / np.float32(p_geom._pixsize)
605 
606  # shift to add to coordinates from fits to compass
607  # Compass = Fits * scaleToCompass + offsetToCompass
608  # we have (compass - centreCompass) = (Fits-f_xC) * scaleToCompass
609  offsetXToCompass = p_geom.cent - f_xC * scaleToCompass
610  offsetYToCompass = p_geom.cent - f_yC * scaleToCompass
611 
612  # computing IF spport size in compass system
613  iSize, jSize, ntotact = f_influ.shape
614  if iSize != jSize:
615  raise ('Error')
616 
617  ess1 = np.ceil((fi_i1+iSize) * scaleToCompass + offsetXToCompass) \
618  - np.floor(fi_i1 * scaleToCompass + offsetXToCompass)
619  ess1 = np.max(ess1)
620 
621  ess2 = np.ceil((fi_j1+jSize) * scaleToCompass + offsetYToCompass) \
622  - np.floor(fi_j1 * scaleToCompass + offsetYToCompass)
623  ess2 = np.max(ess2)
624  smallsize = np.maximum(ess1, ess2).astype(int)
625 
626  # Allocate influence function maps and other arrays
627  p_dm._ntotact = ntotact
628  p_dm._influsize = np.int64(smallsize)
629  p_dm._i1 = np.zeros(ntotact, dtype=np.int32)
630  p_dm._j1 = np.zeros(ntotact, dtype=np.int32)
631  p_dm._xpos = np.zeros(ntotact, dtype=np.float32)
632  p_dm._ypos = np.zeros(ntotact, dtype=np.float32)
633  p_dm._influ = np.zeros((smallsize, smallsize, ntotact), dtype=np.float32)
634 
635  # loop on actuators
636  for i in range(ntotact):
637  # pix coordinate of the minimap in fits system
638  fi_x = np.arange(iSize) + fi_i1[i]
639  fi_y = np.arange(jSize) + fi_j1[i]
640 
641  # transfer to compass scale
642  ci_x = fi_x * scaleToCompass + offsetXToCompass
643  ci_y = fi_y * scaleToCompass + offsetYToCompass
644 
645  # Creation of compass coordinates
646  ci_i1 = fi_i1[i] * scaleToCompass + offsetXToCompass
647  ci_j1 = fi_j1[i] * scaleToCompass + offsetYToCompass
648  ci_i1 = np.floor(ci_i1).astype(np.int32)
649  ci_j1 = np.floor(ci_j1).astype(np.int32)
650  ci_xpix = ci_i1 + np.arange(smallsize)
651  ci_ypix = ci_j1 + np.arange(smallsize)
652  # WARNING: xpos and ypos are approximate !! For debug only ...
653  # p_dm._xpos[i] = ci_i1 + smallsize/2.0
654  # p_dm._ypos[i] = ci_j1 + smallsize/2.0
655 
656  f = interpolate.interp2d(ci_y, ci_x, f_influ[:, :, i], kind='cubic')
657  temp = f(ci_ypix, ci_xpix) * p_dm.unitpervolt
658  # temp[np.where(temp<1e-6)] = 0.
659  p_dm._influ[:, :, i] = temp
660 
661  # ....
662  p_dm._i1[i] = ci_i1
663  p_dm._j1[i] = ci_j1
664 
665  tmp = p_geom.ssize - (np.max(p_dm._i1 + smallsize))
666  margin_i = np.minimum(tmp, np.min(p_dm._i1))
667  tmp = p_geom.ssize - (np.max(p_dm._j1 + smallsize))
668  margin_j = np.minimum(tmp, np.min(p_dm._j1))
669 
670  p_dm._xpos = f_xpos * scaleToCompass + offsetXToCompass
671  p_dm._ypos = f_ypos * scaleToCompass + offsetYToCompass
672 
673  p_dm._n1 = int(np.minimum(margin_i, margin_j))
674  p_dm._n2 = p_geom.ssize - 1 - p_dm._n1
675 
676  p_dm._i1 -= p_dm._n1
677  p_dm._j1 -= p_dm._n1
678 
679  comp_dmgeom(p_dm, p_geom)
680 
681 
682 def make_tiptilt_dm(p_dm: conf.Param_dm, patchDiam: int, p_geom: conf.Param_geom,
683  diam: float):
684  """Compute the influence functions for a tip-tilt DM
685 
686  Args:
687  p_dm: (Param_dm) : dm settings
688 
689  patchDiam: (int) : patchDiam for dm size
690 
691  p_geom: (Param_geom) : geom settings
692 
693  diam: (float) : telescope diameter
694  :return:
695  influ: (np.ndarray(dims=3,dtype=np.float64)) : cube of the IF
696 
697  """
698  dim = max(p_dm._n2 - p_dm._n1 + 1, p_geom._mpupil.shape[0])
699  #norms = [np.linalg.norm([w.xpos, w.ypos]) for w in p_wfs]
700 
701  nzer = 2
702  influ = dm_util.make_zernike(nzer + 1, dim, patchDiam, p_geom.cent - p_dm._n1 - 0.5,
703  p_geom.cent - p_dm._n1 - 0.5, 1)[:, :, 1:]
704 
705  # normalization factor: one unit of tilt gives 1 arcsec:
706  current = influ[dim // 2 - 1, dim // 2 - 1, 0] - \
707  influ[dim // 2 - 2, dim // 2 - 2, 0]
708  fact = p_dm.unitpervolt * diam / p_geom.pupdiam * 4.848 / current
709 
710  influ = influ * fact
711  p_dm._ntotact = influ.shape[2]
712  p_dm._influsize = influ.shape[0]
713  p_dm._influ = influ
714 
715  return influ
716 
717 
718 def make_kl_dm(p_dm: conf.Param_dm, patchDiam: int, p_geom: conf.Param_geom,
719  cobs: float) -> None:
720  """Compute the influence function for a Karhunen-Loeve DM
721 
722  Args:
723  p_dm: (Param_dm) : dm settings
724 
725  patchDiam: (int) : patchDiam for dm size
726 
727  p_geom: (Param_geom) : geom settings
728 
729  cobs: (float) : telescope cobs
730 
731  """
732  dim = p_geom._mpupil.shape[0]
733 
734  print("KL type: ", p_dm.type_kl)
735 
736  if (p_dm.nkl < 13):
737  nr = np.int64(5.0 * np.sqrt(52)) # one point per degree
738  npp = np.int64(10.0 * nr)
739  else:
740  nr = np.int64(5.0 * np.sqrt(p_dm.nkl))
741  npp = np.int64(10.0 * nr)
742 
743  radp = kl_util.make_radii(cobs, nr)
744 
745  kers = kl_util.make_kernels(cobs, nr, radp, p_dm.type_kl, p_dm.outscl)
746 
747  evals, nord, npo, ordd, rabas = kl_util.gkl_fcom(kers, cobs, p_dm.nkl)
748 
749  azbas = kl_util.make_azimuth(nord, npp)
750 
751  ncp, ncmar, px, py, cr, cp, pincx, pincy, pincw, ap = kl_util.set_pctr(
752  patchDiam, nr, npp, p_dm.nkl, cobs, nord)
753 
754  p_dm._ntotact = p_dm.nkl
755  p_dm._nr = nr # number of radial points
756  p_dm._npp = npp # number of elements
757  p_dm._ord = ordd # the radial orders of the basis
758  p_dm._rabas = rabas # the radial array of the basis
759  p_dm._azbas = azbas # the azimuthal array of the basis
760  p_dm._ncp = ncp # dim of grid
761  p_dm._cr = cr # radial coord in cartesien grid
762  p_dm._cp = cp # phi coord in cartesien grid
763  p_dm._i1 = np.zeros((p_dm.nkl), dtype=np.int32) + \
764  (dim - patchDiam) // 2
765  p_dm._j1 = np.zeros((p_dm.nkl), dtype=np.int32) + \
766  (dim - patchDiam) // 2
767  p_dm._ntotact = p_dm.nkl
768  p_dm.ap = ap
769 
770 
771 def comp_dmgeom(p_dm: conf.Param_dm, p_geom: conf.Param_geom):
772  """Compute the geometry of a DM : positions of actuators and influence functions
773 
774  Args:
775  dm: (Param_dm) : dm settings
776 
777  geom: (Param_geom) : geom settings
778  """
779  smallsize = p_dm._influsize
780  nact = p_dm._ntotact
781  dm_dim = int(p_dm._n2 - p_dm._n1 + 1)
782  mpup_dim = p_geom._mpupil.shape[0]
783 
784  if (dm_dim < mpup_dim):
785  print('DM support is smaller than mpupil')
786  offs = (mpup_dim - dm_dim) // 2
787  else:
788  print('DM support is larger than mpupil')
789  offs = 0
790  mpup_dim = dm_dim
791 
792  indgen = np.tile(np.arange(smallsize, dtype=np.int32), (smallsize, 1))
793 
794  tmpx = np.tile(indgen, (nact, 1, 1))
795  tmpy = np.tile(indgen.T, (nact, 1, 1))
796 
797  tmpx += offs + p_dm._i1[:, None, None]
798  tmpy += offs + p_dm._j1[:, None, None]
799 
800  tmp = tmpx + mpup_dim * tmpy
801 
802  # bug in limit of def zone -10 destoe influpos for all actuator
803  tmp[tmpx < 0] = mpup_dim * mpup_dim + 10 # -10
804  tmp[tmpy < 0] = mpup_dim * mpup_dim + 10 # -10
805  tmp[tmpx > dm_dim - 1] = mpup_dim * mpup_dim + 10 # -10
806  tmp[tmpy > dm_dim - 1] = mpup_dim * mpup_dim + 10 # -10
807  itmps = np.argsort(tmp.flatten()).astype(np.int32)
808  tmps = tmp.flatten()[itmps].astype(np.int32)
809  itmps = itmps[np.where(itmps > -1)]
810 
811  istart = np.zeros((mpup_dim * mpup_dim), dtype=np.int32)
812  npts = np.zeros((mpup_dim * mpup_dim), dtype=np.int32)
813 
814  tmps_unique, cpt = np.unique(tmps, return_counts=True)
815  if (tmps_unique > npts.size - 1).any():
816  tmps_unique = tmps_unique[:-1]
817  cpt = cpt[:-1]
818 
819  for i in range(tmps_unique.size):
820  npts[tmps_unique[i]] = cpt[i]
821  istart[1:] = np.cumsum(npts[:-1])
822 
823  p_dm._influpos = itmps[:np.sum(npts)].astype(np.int32)
824  # infludata = p_dm._influ.flatten()[p_dm._influpos]
825  # p_dm._influ = infludata[:,None,None]
826  # p_dm._influpos = p_dm._influpos / (smallsize * smallsize)
827  p_dm._ninflu = npts.astype(np.int32)
828  p_dm._influstart = istart.astype(np.int32)
829 
830  p_dm._i1 += offs
831  p_dm._j1 += offs
832 
833  # ninflu = np.zeros((istart.size * 2))
834  # ninflu[::2] = istart.astype(np.int32)
835  # ninflu[1::2] = npts.astype(np.int32)
836 
837  # p_dm._ninflu = ninflu
838 
839 
840 def correct_dm(context, dms: Dms, p_dms: list, p_controller: conf.Param_controller,
841  p_geom: conf.Param_geom, imat: np.ndarray = None, dataBase: dict = {},
842  use_DB: bool = False):
843  """Correct the geometry of the DMs using the imat (filter unseen actuators)
844 
845  Args:
846  context: (carmaWrap_context): context
847  dms: (Dms) : Dms object
848  p_dms: (list of Param_dm) : dms settings
849  p_controller: (Param_controller) : controller settings
850  p_geom: (Param_geom) : geom settings
851  imat: (np.ndarray) : interaction matrix
852  dataBase: (dict): dictionary containing paths to files to load
853  use_DB: (bool): dataBase use flag
854  """
855  print("Filtering unseen actuators... ")
856  if imat is not None:
857  resp = np.sqrt(np.sum(imat**2, axis=0))
858 
859  ndm = p_controller.ndm.size
860  inds = 0
861 
862  for nmc in range(ndm):
863  nm = p_controller.ndm[nmc]
864  nactu_nm = p_dms[nm]._ntotact
865  # filter actuators only in stackarray mirrors:
866  if (p_dms[nm].type == scons.DmType.PZT):
867  if "dm" in dataBase:
868  influpos, ninflu, influstart, i1, j1, ok = h5u.load_dm_geom_from_dataBase(
869  dataBase, nmc)
870  p_dms[nm].set_ntotact(ok.shape[0])
871  p_dms[nm].set_influ(p_dms[nm]._influ[:, :, ok.tolist()])
872  p_dms[nm].set_xpos(p_dms[nm]._xpos[ok])
873  p_dms[nm].set_ypos(p_dms[nm]._ypos[ok])
874  p_dms[nm]._influpos = influpos
875  p_dms[nm]._ninflu = ninflu
876  p_dms[nm]._influstart = influstart
877  p_dms[nm]._i1 = i1
878  p_dms[nm]._j1 = j1
879  else:
880  tmp = resp[inds:inds + p_dms[nm]._ntotact]
881  ok = np.where(tmp > p_dms[nm].thresh * np.max(tmp))[0]
882  nok = np.where(tmp <= p_dms[nm].thresh * np.max(tmp))[0]
883 
884  p_dms[nm].set_ntotact(ok.shape[0])
885  p_dms[nm].set_influ(p_dms[nm]._influ[:, :, ok.tolist()])
886  p_dms[nm].set_xpos(p_dms[nm]._xpos[ok])
887  p_dms[nm].set_ypos(p_dms[nm]._ypos[ok])
888  p_dms[nm].set_i1(p_dms[nm]._i1[ok])
889  p_dms[nm].set_j1(p_dms[nm]._j1[ok])
890 
891  comp_dmgeom(p_dms[nm], p_geom)
892  if use_DB:
893  h5u.save_dm_geom_in_dataBase(nmc, p_dms[nm]._influpos,
894  p_dms[nm]._ninflu,
895  p_dms[nm]._influstart, p_dms[nm]._i1,
896  p_dms[nm]._j1, ok)
897 
898  dim = max(p_dms[nm]._n2 - p_dms[nm]._n1 + 1, p_geom._mpupil.shape[0])
899  ninflupos = p_dms[nm]._influpos.size
900  n_npts = p_dms[nm]._ninflu.size
901  dms.remove_dm(nm)
902  dms.insert_dm(context, p_dms[nm].type, p_dms[nm].alt, dim,
903  p_dms[nm]._ntotact, p_dms[nm]._influsize, ninflupos, n_npts,
904  p_dms[nm].push4imat, 0, p_dms[nm].dx / p_geom._pixsize,
905  p_dms[nm].dy / p_geom._pixsize, p_dms[nm].theta, p_dms[nm].G,
906  context.active_device, nm)
907  dms.d_dms[nm].pzt_loadarrays(p_dms[nm]._influ, p_dms[nm]._influpos.astype(
908  np.int32), p_dms[nm]._ninflu, p_dms[nm]._influstart, p_dms[nm]._i1,
909  p_dms[nm]._j1)
910 
911  inds += nactu_nm
912  print("Done")
913 
914 
915 def makePetalDm(p_dm, p_geom, pupAngleDegree):
916  '''
917  makePetalDm(p_dm, p_geom, pupAngleDegree)
918 
919  The function builds a DM, segmented in petals according to the pupil
920  shape. The petals will be adapted to the EELT case only.
921 
922  <p_geom> : compass object p_geom. The function requires the object p_geom
923  in order to know what is the pupil mask, and what is the mpupil.
924  <p_dm> : compass petal dm object p_dm to be created. The function will
925  transform/modify in place the attributes of the object p_dm.
926  <pupAngleDegree> : rotation/clocking angle of the pupil in degrees
927 
928 
929  '''
930  p_dm._n1 = p_geom._n1
931  p_dm._n2 = p_geom._n2
932  influ, i1, j1, smallsize, nbSeg = make_petal_dm_core(p_geom._mpupil, pupAngleDegree)
933  p_dm._influsize = smallsize
934  p_dm.set_ntotact(nbSeg)
935  p_dm._i1 = i1
936  p_dm._j1 = j1
937  p_dm._xpos = i1 + smallsize / 2 + p_dm._n1
938  p_dm._ypos = j1 + smallsize / 2 + p_dm._n1
939  p_dm._influ = influ
940 
941  # generates the arrays of indexes for the GPUs
942  comp_dmgeom(p_dm, p_geom)
943 
944 
945 def make_petal_dm_core(pupImage, pupAngleDegree):
946  """
947  <pupImage> : image of the pupil
948  <pupAngleDegree> : rotation angle of the pupil in degrees
949 
950  La fonction renvoie des fn d'influence en forme de petale d'apres
951  une image de la pupille, qui est supposee etre segmentee.
952 
953 
954  influ, i1, j1, smallsize, nbSeg = make_petal_dm_core(pupImage, 0.0)
955  """
956  # Splits the pupil into connex areas.
957  # <segments> is the map of the segments, <nbSeg> in their number.
958  # binary_opening() allows us to suppress individual pixels that could
959  # be identified as relevant connex areas
960  from scipy.ndimage import label
961  from scipy.ndimage.morphology import binary_opening
962  s = np.ones((2, 2), dtype=bool)
963  segments, nbSeg = label(binary_opening(pupImage, s))
964 
965  # Faut trouver le plus petit support commun a tous les
966  # petales : on determine <smallsize>
967  smallsize = 0
968  i1t = [] # list of starting indexes of influ functions
969  j1t = []
970  i2t = [] # list of ending indexes of influ functions
971  j2t = []
972  for i in range(nbSeg):
973  petal = segments == (i + 1) # identification (boolean) of a given segment
974  profil = np.sum(petal, axis=1) != 0
975  extent = np.sum(profil).astype(np.int32)
976  i1t.append(np.min(np.where(profil)[0]))
977  i2t.append(np.max(np.where(profil)[0]))
978  if extent > smallsize:
979  smallsize = extent
980 
981  profil = np.sum(petal, axis=0) != 0
982  extent = np.sum(profil).astype(np.int32)
983  j1t.append(np.min(np.where(profil)[0]))
984  j2t.append(np.max(np.where(profil)[0]))
985  if extent > smallsize:
986  smallsize = extent
987 
988  # extension de la zone minimale pour avoir un peu de marge
989  smallsize += 2
990 
991  # Allocate array of influence functions
992  influ = np.zeros((smallsize, smallsize, nbSeg), dtype=np.float32)
993 
994  npt = pupImage.shape[0]
995  i0 = j0 = npt / 2 - 0.5
996  petalMap = build_petals(nbSeg, pupAngleDegree, i0, j0, npt)
997  ii1 = np.zeros(nbSeg)
998  jj1 = np.zeros(nbSeg)
999  for i in range(nbSeg):
1000  ip = (smallsize - i2t[i] + i1t[i] - 1) // 2
1001  jp = (smallsize - j2t[i] + j1t[i] - 1) // 2
1002  i1 = np.maximum(i1t[i] - ip, 0)
1003  j1 = np.maximum(j1t[i] - jp, 0)
1004  if (j1 + smallsize) > npt:
1005  j1 = npt - smallsize
1006  if (i1 + smallsize) > npt:
1007  i1 = npt - smallsize
1008  #petal = segments==(i+1) # determine le segment pupille veritable
1009  k = petalMap[i1 + smallsize // 2, j1 + smallsize // 2]
1010  petal = (petalMap == k)
1011  influ[:, :, k] = petal[i1:i1 + smallsize, j1:j1 + smallsize]
1012  ii1[k] = i1
1013  jj1[k] = j1
1014 
1015  return influ, ii1, jj1, int(smallsize), nbSeg
1016 
1017 
1018 def build_petals(nbSeg, pupAngleDegree, i0, j0, npt):
1019  """
1020  Makes an image npt x npt of <nbSeg> regularly spaced angular segments
1021  centred on (i0, j0).
1022  Origin of angles is set by <pupAngleDegree>.
1023 
1024  The segments are oriented as defined in document "Standard Coordinates
1025  and Basic Conventions", ESO-193058.
1026  This document states that the X axis lies in the middle of a petal, i.e.
1027  that the axis Y is along the spider.
1028  The separation angle between segments are [-30, 30, 90, 150, -150, -90].
1029  For this reason, an <esoOffsetAngle> = -pi/6 is introduced in the code.
1030 
1031  nbSeg = 6
1032  pupAngleDegree = 5.0 # pupil rotation/clocking angle
1033  i0 = j0 = 112.3
1034  npt = 222
1035  p = build_petals(nbSeg, pupAngleDegree, i0, j0, npt)
1036  """
1037  # conversion to radians
1038  rot = pupAngleDegree * np.pi / 180.0
1039 
1040  # building coordinate maps
1041  esoOffsetAngle = -np.pi / 6 # -30°, ESO definition.
1042  x = np.arange(npt) - i0
1043  y = np.arange(npt) - j0
1044  X, Y = np.meshgrid(x, y, indexing='ij')
1045  theta = (np.arctan2(Y, X) - rot + 2 * np.pi - esoOffsetAngle) % (2 * np.pi)
1047  # Compute separation angle between segments: start and end.
1048  angleStep = 2 * np.pi / nbSeg
1049  startAngle = np.arange(nbSeg) * angleStep
1050  endAngle = np.roll(startAngle, -1)
1051  endAngle[-1] = 2 * np.pi # last angle is 0.00 and must be replaced by 2.pi
1052  petalMap = np.zeros((npt, npt), dtype=int)
1053  for i in range(nbSeg):
1054  nn = np.where(np.logical_and(theta >= startAngle[i], theta < endAngle[i]))
1055  petalMap[nn] = i
1056  return petalMap
Parameter classes for COMPASS.
Numerical constants for shesha and config enumerations for safe-typing.
Definition: constants.py:1
def make_pzt_dm(conf.Param_dm p_dm, conf.Param_geom p_geom, float cobs, float pupAngle)
Compute the actuators positions and the influence functions for a pzt DM.
Definition: dm_init.py:364
def makePetalDm(p_dm, p_geom, pupAngleDegree)
Definition: dm_init.py:935
def make_petal_dm_core(pupImage, pupAngleDegree)
<pupImage> : image of the pupil <pupAngleDegree> : rotation angle of the pupil in degrees
Definition: dm_init.py:963
def comp_dmgeom(conf.Param_dm p_dm, conf.Param_geom p_geom)
Compute the geometry of a DM : positions of actuators and influence functions.
Definition: dm_init.py:782
Dms dm_init(carmaWrap_context context, List[conf.Param_dm] p_dms, conf.Param_tel p_tel, conf.Param_geom p_geom, List[conf.Param_wfs] p_wfss=None)
Create and initialize a Dms object on the gpu.
Definition: dm_init.py:76
def build_petals(nbSeg, pupAngleDegree, i0, j0, npt)
Makes an image npt x npt of <nbSeg> regularly spaced angular segments centred on (i0,...
Definition: dm_init.py:1046
None make_kl_dm(conf.Param_dm p_dm, int patchDiam, conf.Param_geom p_geom, float cobs)
Compute the influence function for a Karhunen-Loeve DM.
Definition: dm_init.py:735
def init_custom_dm(conf.Param_dm p_dm, conf.Param_geom p_geom, float diam)
Read Fits for influence pzt fonction and form.
Definition: dm_init.py:549
def make_tiptilt_dm(conf.Param_dm p_dm, int patchDiam, conf.Param_geom p_geom, float diam)
Compute the influence functions for a tip-tilt DM.
Definition: dm_init.py:701
def dm_init_standalone(carmaWrap_context context, list p_dms, conf.Param_geom p_geom, diam=1., cobs=0., pupAngle=0., wfs_xpos=[0], wfs_ypos=[0])
Create and initialize a Dms object on the gpu.
Definition: dm_init.py:337
def correct_dm(context, Dms dms, list p_dms, conf.Param_controller p_controller, conf.Param_geom p_geom, np.ndarray imat=None, dict dataBase={}, bool use_DB=False)
Correct the geometry of the DMs using the imat (filter unseen actuators)
Definition: dm_init.py:858
Utilities functions.
Definition: util/__init__.py:1