Course: Learn Laser Interferometry with Finesse

3. Gaussian Beams > 3. Mirror maps

Preprocessing Phase Maps

Author: Daniel Töyrä


The processing the phase maps before we use them in FINESSE is important to reduce the computational cost. We want to specify the radius of curvature, tilts, and position along the optical axis in FINESSE. For example, using a mirror map with a radius of curvature instead of specifying the radius of curvature in FINESSE will make FINESSE using a computational basis that is far from optimal, thus, many higher order modes need to be taken into account. Therefore, we want to remove the curvatures from the map and instead specify them in FINESSE. Tilts as well as surface offsets along the optical axis can easily be redefined, so those we just want to remove completely from the map.

Recommended notebooks before you start:

We recommend that you have looked through the Mirror Maps notebook that you can find in this folder. The link above only works if you started IPython/Jupyter Notebook in the top directory of this course.

Reading material and references:

[1] C. Bond, D. Brown, A. Freise and K. Strain, "Interferometer Techniques for Gravitational-Wave Detection", Living Reviews in Relativity 19, 3 (2010). - Living review article (more like a book) on laser interferometry in the frequency domain for detecting gravitational waves, and FINESSE.

[2] A. Freise, D. Brown, and C. Bond, "Finesse, Frequency domain INterferomEter Simulation SoftwarE". - FINESSE-manual

[3] FINESSE syntax reference - Useful online syntax reference for FINESSE. Also available in the Finesse manual [2], but this online version is updated more often.

After this session you will be able to...

  • Make a mirror map ready for being used in FINESSE

We start by loading PyKat and other Python packages that we need:

In [5]:
import numpy as np                         # Importing numpy
import matplotlib                          # For plotting
import matplotlib.pyplot as plt                   
from pykat import finesse                  # Importing the pykat.finesse package
from pykat.commands import *               # Importing all packages in pykat.commands.
from pykat.optics.maps import *            # Importing maps package
from IPython.display import display, HTML  # Allows us to display HTML.

# Telling the notebook to make plots inline.
%matplotlib inline      
# Initialises the PyKat plotting tool. Change dpi value 
# to change figure sizes on your screen.

Manual preparation

In this section we go through step by step the methods that can be used for processing a mirror map. If you just quickly want to know how to prepare a phase map, jump to section 3. We start by reading the map we want to process, in this case a LIGO ETM mirror.

In [6]:
smap = read_map('ETM08_S1_-power160.dat', mapFormat='metroPro')
fig = smap.plot()
/Users/adf/work/git/pykat/pykat/optics/ MatplotlibDeprecationWarning: 
The set_clim function was deprecated in Matplotlib 3.1 and will be removed in 3.3. Use ScalarMappable.set_clim instead.
  cbar.set_clim(zmin, zmax)

Cropping the map

The cropping is important for removing the rough edge of the raw mirror maps. Here, some large values usually are present, which we want to get rid of to not bias the pre-processing of the map. When cropping you can either specify a radius in meters, and everything further out from the mirror center will be removed. If you do not specify a radius, the distance from the mirror center to the closest NaN-element will be used as radius. For this we use the matrix surfacemap.notNan, which is a matrix of booleans keeping track of which elements in the matrix that are not NaN.

Without specifying a cropping radius

We can see that there is a rough edge around the mirror map in the figure above. By plotting the field notNan field, we can see that there are actually some NaN mixed in there as well.

In [7]:
smap1 = deepcopy(smap)

# Plotting the matrix notNan
fig = plt.figure()
pcm = plt.pcolormesh(smap1.x*100, smap1.y*100, smap1.notNan)
#cbar = plt.colorbar()
plt.xlabel('x [cm]')
plt.ylabel('y [cm]')

Here, 1 is equal to True (not NaN), and 0 is equal to False (NaN). So we see that there are some NaN-values in the edge. Cropping without specifying a cropping radius to use the matrix surfacemap.notNan to crop:

In [8]:
# Computing radius (max distance from center to a non-NaN element.)
r0 = smap1.find_radius(method = 'max', unit='meters')

# Cropping
# Showing unprocessed map
fig = smap1.plot()

# Computing radius (max distance from center to a non-NaN element.)
r1 = smap1.find_radius(method = 'max', unit='meters')
print('Radius before cropping:  {0:.5f} m'.format(r0))
print('Radius after cropping:   {0:.5f} m'.format(r1))
Radius before cropping:  0.16889 m
Radius after cropping:   0.16042 m

The printed out radii before and after the cropping reveals that we removed about 8.5 mm of radius from the mirror map. We can see that we got rid of most of the rough edge, but we still have some of it left, so we could remove some more.

With specifying a cropping radius

Here we crop the map with two different cropping radii, $r = 15.4$ cm and $r = 8.02$ cm. The latter is the radius used in LIGO's figure measurement reports.

In [9]:
smap2 = deepcopy(smap)
r0 = smap2.find_radius(method = 'max', unit='meters')
# The specified radius of this mirror
r1 = smap2.find_radius(method = 'max', unit='meters')
fig = smap2.plot()

# Radius used in the LIGO figure measurement reports
smap3 = deepcopy(smap)
r2 = smap3.find_radius(method = 'max', unit='meters')
fig = smap3.plot()

print('Radius before cropping:  {0:.5f} m'.format(r0))
print('Radius first cropping:   {0:.5f} m'.format(r1))
print('Radius second cropping:  {0:.5f} m'.format(r2))
Radius before cropping:  0.16889 m
Radius first cropping:   0.15400 m
Radius second cropping:  0.08020 m

By keeping track of the radii we can see that the algorithm is doing what we want.

Centering the map

Centering the map is very useful. Here, the origin (0,0) is defined as the place where the beam is supposed to hit the mirror, and this can be offset from the actual mirror center by setting the attribute surfacemap.xyOffet. We test this here:

In [10]:
smap4 = deepcopy(smap3)
# Setting xy-offset
smap4.xyOffset = (0.02,0.05)
# Storing center and xyoffset for printing
xy_offset1 = smap4.xyOffset
center1 =
# Plotting, center should be at (x,y) = (-0.02, -0.05) m.

# And now we recentering.
# Storing values for printing
xy_offset2 = smap4.xyOffset
center2 =
# Plotting, center should be at (x,y) = (0,0) m

print(('First center  (x, y) [points]: ({0[0]:.3f}, {0[1]:.3f}), ' +
      'first xy-offset  [m]: ({1[0]:.3f},{1[1]:.3f})').format(center1,xy_offset1))

print(('Second center (x, y) [points]: ({0[0]:.3f}, {0[1]:.3f}), ' +
      'second xy-offset [m]: ({1[0]:.3f},{1[1]:.3f})').format(center2,xy_offset2))
First center  (x, y) [points]: (254.441, 329.369), first xy-offset  [m]: (0.020,0.050)
Second center (x, y) [points]: (204.441, 204.369), second xy-offset [m]: (0.000,0.000)

We see that setting the xy-offset shifts the origin of the map, i.e., where the beam is supposed to hit the map. We can also see that the is specified in data points, while xy-offset is specified in meters. To get the center in meters, measured from th lower left corner of mirror map above, we can multiply with the attribute surfacemap.step_size:

In [11]:
center = ([0]*smap4.step_size[0],[1]*smap4.step_size[1])
print('Center (x,y): ({0[0]:.5f}, {0[1]:.5f}) m'.format(center))
Center (x,y): (0.08178, 0.08175) m

Preprocessing a phase map with Zernike-polynomials

Without weights, or with uniform weights, we take the whole mirror surface into account. The quickest way to do this is by convolving the mirror surface with Zernike-polynomials to get the amplitudes, and then remove the Zernike polynomials with the corresponding amplitudes from the map. We can chose which Zernike-polynomials we want to remove from the mirror map.

Removing radius of curvature by using the Z(2,0) mode

The method surfacemap.remove_curvature() returns and stores the information about what was removed. The Zernike-polynomial is also converted into an equivalent radius of curvature. This is an approximation, since Z(2,0) actually is parabolic, but for the large radii of curvatures used in the arm cavities of LIGO, the approximation is valid.

In [12]:
smap5 = deepcopy(smap4)
Rc, znm = smap5.remove_curvature(method='zernike', zModes = 'defocus')

fig5 = smap5.plot()
print('Returned removed values:')
print('Radius of curvature: {0:.2e} m'.format(Rc))
print('Zernike polynomial: {0}'.format(znm))
print('Stored removed values:')
print('Radius of curvature: {0:.2e} m'.format(smap5.RcRemoved))
print('Zernike polynomial: {0}'.format(smap5.zernikeRemoved))
Returned removed values:
Radius of curvature: -4.04e+08 m
Zernike polynomial: {'02': (0, 2, -0.00398168007798258)}

Stored removed values:
Radius of curvature: -4.04e+08 m
Zernike polynomial: {'02': (0, 2, -0.00398168007798258)}

We see above from the data printed to screen that the removed values are both returned and stored in the surfacemap-object. Note that barely nothing was removed. This map contains very little curvature as it has already been removed by LIGO.

Removing astigmatism using Zernike-polynomials

We could also remove the astigmatism of the mirror map by convolving with the two Zernike-polynomials Z(-2,2) and Z(2,2). We use the same method, but now using zModes = 'astigmatism'.

In [13]:
smap6 = deepcopy(smap4)
Rc, znm = smap6.remove_curvature(method='zernike', zModes = 'astigmatism')
fig6 = smap6.plot()

print('Returned removed values:')
print('Radius of curvature (x,y): ({0[0]:.2e}, {0[1]:.2e}) m'.format(Rc))
print('Zernike polynomial: {0}'.format(znm))
print('Stored removed values:')
print('Radius of curvature (x,y): ({0[0]:.2e}, {0[1]:.2e}) m'.format(smap6.RcRemoved))
print('Zernike polynomial: {0}'.format(smap6.zernikeRemoved))