Analysis

Zernike Decomposition

Decompose complex wavefronts into orthogonal polynomial modes.

AdvancedAberrationsNumPy backend10 min read

Introduction

This tutorial shows how to decompose the pupil using various Zernike types. Namely, we use "standard", "fringe", and "Noll" Zernike indices.

Core concepts used

wavefront.ZernikeOPD(...)
Fits Zernike polynomials to a raw Optical Path Difference (OPD) map.
zernike_type='fringe'
Uses the Fringe Zernike system, common in interferometry and older lens design software.
zernike_type='noll'
Uses the Noll indexing system, frequently used in atmospheric science and adaptive optics.
num_terms=37
Determines how many polynomial terms to compute. Higher numbers capture finer, high-frequency wavefront ripples.

Step-by-step build

1

Import wavefront module and sample eyepiece

python
import matplotlib.pyplot as plt

from optiland import wavefront
from optiland.samples.eyepieces import EyepieceErfle
2

Instantiate and draw the Erfle eyepiece

python
lens = EyepieceErfle()
lens.draw()
Step
3

View the on-axis OPD map

First, we'll view the wavefront.

python
opd = wavefront.OPD(lens, field=(0, 0), wavelength=0.55)
opd.view(projection="2d", num_points=512)
Step
4

Fit standard Zernike polynomials to the wavefront

We'll then find the Zernike coefficients of the wavefront.

python
zernike_standard = wavefront.ZernikeOPD(
 lens,
 field=(0, 0),
 wavelength=0.55,
 zernike_type="standard",
 num_terms=37,
)
5

View the Zernike-reconstructed OPD map

Let's view the Zernike fit and compare it to the nominal OPD map.

python
zernike_standard.view(projection="2d", num_points=512)
Step
6

Plot the standard Zernike coefficient bar chart

Qualitatively, we can see the Zernike fit well-represents the OPD map.

Let's see what the actual coefficients look like:

python
plt.bar(range(1, 38), zernike_standard.coeffs)
plt.axhline(color="k", linewidth=1, linestyle="--")
plt.xlabel("Zernike Term #")
plt.ylabel("Zernike Standard Coefficient")
plt.show()
Step
7

Decompose full-field wavefront using fringe Zernike indices

Let's decompose the wavefront using Zernike fringe indices and Zernike Noll indices. We'll use the field point at (0, 1).

python
zernike_fringe = wavefront.ZernikeOPD(
 lens,
 field=(0, 1),
 wavelength=0.55,
 zernike_type="fringe",
 num_terms=37,
)

plt.bar(range(1, 38), zernike_fringe.coeffs, color="C1")
plt.axhline(color="k", linewidth=1, linestyle="--")
plt.xlabel("Zernike Term #")
plt.ylabel("Zernike Fringe Coefficient")
plt.show()
Step
8

Decompose full-field wavefront using Noll Zernike indices

python
zernike_noll = wavefront.ZernikeOPD(
 lens,
 field=(0, 1),
 wavelength=0.55,
 zernike_type="noll",
 num_terms=37,
)

plt.bar(range(1, 38), zernike_noll.coeffs, color="C2")
plt.axhline(color="k", linewidth=1, linestyle="--")
plt.xlabel("Zernike Term #")
plt.ylabel("Zernike Noll Coefficient")
plt.show()
Step
Show full code listing
python
import matplotlib.pyplot as plt

from optiland import wavefront
from optiland.samples.eyepieces import EyepieceErfle

lens = EyepieceErfle()
lens.draw()

opd = wavefront.OPD(lens, field=(0, 0), wavelength=0.55)
opd.view(projection="2d", num_points=512)

zernike_standard = wavefront.ZernikeOPD(
  lens,
  field=(0, 0),
  wavelength=0.55,
  zernike_type="standard",
  num_terms=37,
)

zernike_standard.view(projection="2d", num_points=512)

plt.bar(range(1, 38), zernike_standard.coeffs)
plt.axhline(color="k", linewidth=1, linestyle="--")
plt.xlabel("Zernike Term #")
plt.ylabel("Zernike Standard Coefficient")
plt.show()

zernike_fringe = wavefront.ZernikeOPD(
  lens,
  field=(0, 1),
  wavelength=0.55,
  zernike_type="fringe",
  num_terms=37,
)

plt.bar(range(1, 38), zernike_fringe.coeffs, color="C1")
plt.axhline(color="k", linewidth=1, linestyle="--")
plt.xlabel("Zernike Term #")
plt.ylabel("Zernike Fringe Coefficient")
plt.show()

zernike_noll = wavefront.ZernikeOPD(
  lens,
  field=(0, 1),
  wavelength=0.55,
  zernike_type="noll",
  num_terms=37,
)

plt.bar(range(1, 38), zernike_noll.coeffs, color="C2")
plt.axhline(color="k", linewidth=1, linestyle="--")
plt.xlabel("Zernike Term #")
plt.ylabel("Zernike Noll Coefficient")
plt.show()

zernike = wavefront.ZernikeOPD(lens, (0, 1), 0.55, zernike_type="noll", num_terms=9)

for k in range(len(zernike.coeffs)):
  print(f"Z{k + 1}: {zernike.coeffs[k]:.8f}")

Conclusions

  • The wavefront.ZernikeOPD class fits an arbitrary number of Zernike polynomial terms to a sampled OPD map, providing a compact, analytically useful representation of complex wavefront errors.
  • Standard, fringe, and Noll indexing conventions are all supported via the zernike_type parameter, making it straightforward to match the convention used by external tools such as interferometers or adaptive-optics software.
  • Visualising the reconstructed OPD map alongside the raw map confirms that 37 terms capture the dominant wavefront structure of the Erfle eyepiece with high fidelity.
  • Bar charts of the coefficient vectors immediately reveal which polynomial modes — such as defocus, primary astigmatism, or higher-order coma — carry the largest contribution at a given field point.
  • Printing coefficients numerically with a reduced term count (e.g. 9 terms) offers a concise, machine-readable summary suitable for tolerance budgeting or further numerical processing.

Next tutorials