Performing analyses#

Being able to define and build eye models is nice, but being able to analyze them is at least as important. Visisipy provides a suite of analyses than can be performed on eye models in the visisipy.analysis module.

Analyses are functions with the following signature:

analysis(model: EyeModel, [parameters], return_raw_result: bool, backend: type[BaseBackend]) -> AnalysisResult
  • model is the eye model to be analyzed. This parameter is optional. If not provided, the eye model that is currently built in the backend is used.

  • parameters are the parameters to be passed to the analysis. These parameters are specific to each analysis.

  • return_raw_result is a boolean that indicates whether to return the raw result from the backend. This parameter is optional and defaults to False.

  • backend is the backend to be used for the analysis. This parameter is optional and defaults to the currently selected backend. If no backend has been initialized, the default backend is initialized first.

The example below shows how to calculate the refraction of the eye model using the refraction analysis.

import matplotlib.pyplot as plt

import visisipy

# Perform all calculations in Optiland
visisipy.set_backend("optiland")
visisipy.update_settings(fields=[(0, 0), (0, 30)])

# Use the Navarro model
model = visisipy.EyeModel()

# Build the model in the backend
model.build()

# Perform the refraction analysis
refraction = visisipy.analysis.refraction()

print(refraction)
FourierPowerVectorRefraction(M=np.float64(-0.021794140116020003), J0=np.float64(3.0889018667587853e-11), J45=np.float64(1.2045769555389102e-12))

Alternatively, the model can be defined without building it, and passed to the analysis to build it:

myopic_model = visisipy.EyeModel(visisipy.create_geometry(axial_length=26.5))

refraction_myopic = visisipy.analysis.refraction(myopic_model)

print(refraction_myopic)
FourierPowerVectorRefraction(M=np.float64(-7.495269032132664), J0=np.float64(3.332908328025218e-11), J45=np.float64(1.0208252807601943e-12))

Analyses available in Visisipy#

Warning

The output of analyses can be influenced by the settings of the backend. For example, the ray aiming method and its parameters can affect the results of analyses that involve ray tracing. You can find an overview of the backend settings here.

Cardinal points#

Calculates the cardinal points of the eye.

# Revert to the emmetropic model
model.build()

visisipy.analysis.cardinal_points()
CardinalPointsResult(focal_lengths=CardinalPoints(object=-16.461645005582017, image=22.021982081505797), focal_points=CardinalPoints(object=-14.878911063889863, image=-0.006670375453102611), principal_points=CardinalPoints(object=1.5827339416921546, image=-22.0286524569589), anti_principal_points=CardinalPoints(object=-31.34055606947188, image=22.015311706052696), nodal_points=CardinalPoints(object=7.143071017615934, image=-16.46831538103512), anti_nodal_points=CardinalPoints(object=-36.900893145395656, image=16.454974630128916))

FFT Modulation Transfer Function (MTF)#

Calculates the modulation transfer function (MTF) of the eye using the fast Fourier transform (FFT).

fft_mtf = visisipy.analysis.fft_mtf()

plt.figure()

for field, mtf in fft_mtf.items():
    plt.plot(mtf.tangential.index, mtf.tangential, label=f"Tangential {field}")
    plt.plot(mtf.sagittal.index, mtf.sagittal, ls="--", label=f"Sagittal {field}")

plt.xlabel("Spatial Frequency [cycles/mm]")
plt.ylabel("MTF")
plt.grid()
plt.legend();
../_images/27943612f3fccbe4eeb3cd7f8a1e5883837e9c0fe7a6677c283fef538cbf86df.png

FFT Point Spread Function#

Calculates the point spread function (PSF) of the eye using the Fast Fourier Transform (FFT). Compared to the Huygens PSF, the FFT PSF is faster to compute, but may be inaccurate for certain eye models.

fft_psf = visisipy.analysis.fft_psf(field_coordinate=(0, 0))

plt.imshow(
    fft_psf, extent=(fft_psf.columns[0], fft_psf.columns[-1], fft_psf.index[-1], fft_psf.index[0]), origin="lower"
)
plt.xlabel("X [μm]")
plt.ylabel("Y [μm]");
../_images/24e3b15f2d29d12fa7010e30e6c351a67bf4504c2f2636d5b0310fea6cf697ed.png

Huygens Point Spread Function#

Calculates the Huygens point spread function (PSF) of the eye. This PSF is more accurate than the FFT PSF, but may be slower to compute.

huygens_psf = visisipy.analysis.huygens_psf(field_coordinate=(0, 0))

plt.imshow(
    huygens_psf,
    extent=(huygens_psf.columns[0], huygens_psf.columns[-1], huygens_psf.index[-1], huygens_psf.index[0]),
    origin="lower",
)

plt.xlabel("X [μm]")
plt.ylabel("Y [μm]");
../_images/8562a1978dda94dd85a87d89016d4f72aa067429cbc756b0fa34517767ca9c5f.png

Optical Path Difference Map#

Calculates the optical path difference (OPD) map at the exit pupil.

opd_map = visisipy.analysis.opd_map(field_coordinate=(0, 0))

plt.imshow(
    opd_map,
    extent=(opd_map.columns[0], opd_map.columns[-1], opd_map.index[-1], opd_map.index[0]),
    origin="lower",
)

plt.xlabel("PX [-]")
plt.ylabel("PY [μm]");
../_images/c876bcc36fa0a81326ad84adbf6b3782aba6b8b9dfb44d0ef1472cbbbaae99eb.png

Raytrace#

Performs one or more single ray traces through the eye model.

raytrace = visisipy.analysis.raytrace(coordinates=[(0, 0), (0, 30), (0, 60)], wavelengths=[0.543, 0.6328], pupil=(0, 0))

raytrace
/home/docs/checkouts/readthedocs.org/user_builds/visisipy/checkouts/latest/visisipy/optiland/analysis/raytrace.py:111: UserWarning: Field coordinate (0, 60) not found. Adding it to the system.
  normalized_field = set_field(backend, field, field_type)
/home/docs/checkouts/readthedocs.org/user_builds/visisipy/checkouts/latest/visisipy/optiland/analysis/raytrace.py:108: UserWarning: Wavelength 0.6328 not found. Adding it to the system.
  set_wavelength(backend, wavelength)
index field wavelength surface comment x y z
0 0 (0.0, 0.0) 0.5430 0 0.0 0.000000 -3.055765
1 1 (0.0, 0.0) 0.5430 1 cornea front 0.0 0.000000 0.000000
2 2 (0.0, 0.0) 0.5430 2 cornea back / aqueous 0.0 0.000000 0.550000
3 3 (0.0, 0.0) 0.5430 3 pupil 0.0 0.000000 3.600000
4 4 (0.0, 0.0) 0.5430 4 lens front 0.0 0.000000 3.600000
5 5 (0.0, 0.0) 0.5430 5 lens back / vitreous 0.0 0.000000 7.600000
6 6 (0.0, 0.0) 0.5430 6 retina 0.0 0.000000 23.920300
7 0 (0.0, 30.0) 0.5430 0 0.0 -3.519361 -3.055765
8 1 (0.0, 30.0) 0.5430 1 cornea front 0.0 -1.652164 0.178315
9 2 (0.0, 30.0) 0.5430 2 cornea back / aqueous 0.0 -1.406544 0.704007
10 3 (0.0, 30.0) 0.5430 3 pupil 0.0 -0.030747 3.600000
11 4 (0.0, 30.0) 0.5430 4 lens front 0.0 -0.030725 3.600046
12 5 (0.0, 30.0) 0.5430 5 lens back / vitreous 0.0 1.638180 7.376364
13 6 (0.0, 30.0) 0.5430 6 retina 0.0 7.823641 21.019236
14 0 (0.0, 60.0) 0.5430 0 0.0 -10.558084 -3.055765
15 1 (0.0, 60.0) 0.5430 1 cornea front 0.0 -3.678054 0.916422
16 2 (0.0, 60.0) 0.5430 2 cornea back / aqueous 0.0 -3.141121 1.359362
17 3 (0.0, 60.0) 0.5430 3 pupil 0.0 -0.360947 3.600000
18 4 (0.0, 60.0) 0.5430 4 lens front 0.0 -0.353357 3.606117
19 5 (0.0, 60.0) 0.5430 5 lens back / vitreous 0.0 3.112116 6.792894
20 6 (0.0, 60.0) 0.5430 6 retina 0.0 11.746714 14.372793
21 0 (0.0, 0.0) 0.6328 0 0.0 0.000000 -3.055765
22 1 (0.0, 0.0) 0.6328 1 cornea front 0.0 0.000000 0.000000
23 2 (0.0, 0.0) 0.6328 2 cornea back / aqueous 0.0 0.000000 0.550000
24 3 (0.0, 0.0) 0.6328 3 pupil 0.0 0.000000 3.600000
25 4 (0.0, 0.0) 0.6328 4 lens front 0.0 0.000000 3.600000
26 5 (0.0, 0.0) 0.6328 5 lens back / vitreous 0.0 0.000000 7.600000
27 6 (0.0, 0.0) 0.6328 6 retina 0.0 0.000000 23.920300
28 0 (0.0, 30.0) 0.6328 0 0.0 -3.519361 -3.055765
29 1 (0.0, 30.0) 0.6328 1 cornea front 0.0 -1.652164 0.178315
30 2 (0.0, 30.0) 0.6328 2 cornea back / aqueous 0.0 -1.406246 0.703940
31 3 (0.0, 30.0) 0.6328 3 pupil 0.0 -0.028409 3.600000
32 4 (0.0, 30.0) 0.6328 4 lens front 0.0 -0.028390 3.600040
33 5 (0.0, 30.0) 0.6328 5 lens back / vitreous 0.0 1.643125 7.375012
34 6 (0.0, 30.0) 0.6328 6 retina 0.0 7.834953 21.009497
35 0 (0.0, 60.0) 0.6328 0 0.0 -10.558084 -3.055765
36 1 (0.0, 60.0) 0.6328 1 cornea front 0.0 -3.678054 0.916422
37 2 (0.0, 60.0) 0.6328 2 cornea back / aqueous 0.0 -3.140492 1.359015
38 3 (0.0, 60.0) 0.6328 3 pupil 0.0 -0.353868 3.600000
39 4 (0.0, 60.0) 0.6328 4 lens front 0.0 -0.346552 3.605884
40 5 (0.0, 60.0) 0.6328 5 lens back / vitreous 0.0 3.122446 6.787527
41 6 (0.0, 60.0) 0.6328 6 retina 0.0 11.752176 14.346482

Refraction#

Calculates the spherical equivalent of refraction of the eye model. The refraction is calculated from Zernike standard coefficients and represented in Fourier power vector form.

# Calculate the refraction at an eccentricity of 30°
refraction = visisipy.analysis.refraction(field_coordinate=(0, 30), use_higher_order_aberrations=True)

print(refraction)
FourierPowerVectorRefraction(M=np.float64(0.7928086781754915), J0=np.float64(1.0982686296377036), J45=np.float64(1.5467912748513715e-12))

You can convert the refraction from Fourier power vector form to other representations:

print(refraction.to_sphero_cylindrical())
print(refraction.to_polar_power_vectors())
SpheroCylindricalRefraction(sphere=np.float64(1.891077307813195), cylinder=np.float64(-2.196537259275407), axis=np.float64(4.0347420223538186e-11))
PolarPowerVectorRefraction(M=np.float64(0.7928086781754915), J=np.float64(1.0982686296377036), axis=np.float64(4.0347420223538186e-11))

Strehl Ratio#

The Strehl ratio is a measure of the quality of the optical system. For a perfectly unaberrated optical system, the Strehl ratio is 1.0. The Strehl ratio is calculated from the point spread function (PSF) of the eye model.

visisipy.analysis.strehl_ratio(field_coordinate=(0, 0))
0.6039494317090162

Zernike coefficients#

Calculates the Zernike standard coefficients of the eye model at the retina surface.

zernike_coefficients = visisipy.analysis.zernike_standard_coefficients(sampling=128, maximum_term=15)

for term, coefficient in zernike_coefficients.items():
    print(f"Z{term:>2}: {coefficient:.4f}")
Z 1: 0.0708
Z 2: -0.0000
Z 3: -0.0000
Z 4: 0.0586
Z 5: -0.0000
Z 6: -0.0000
Z 7: 0.0000
Z 8: 0.0000
Z 9: -0.0000
Z10: 0.0000
Z11: 0.0138
Z12: 0.0000
Z13: 0.0000
Z14: 0.0000
Z15: -0.0000

Adding fields and wavelengths on the fly#

Most analyses require a field coordinate and/or a wavelength to be specified. If the requested field or wavelength is not already defined in the backend, it will be added to the backend before performing the analysis. This is convenient, but for some analyses in the OpticStudio backend the result may be influenced by the presence of other fields. It is therefore recommended to add all required fields and wavelengths before running any analysis.

Warning

Fields and wavelengths that are added during an analysis are not persisted in the backend’s settings. To persistently add fields and wavelengths to the backend, use visisipy’s update_settings method:

visisipy.update_settings(fields=[(0, 0)], wavelengths=[550])

Accuracy of analyses for both backends#

The accuracy of the analyses depends on the backend used. The table below summarizes to what extent the OpticStudio and Optiland backends provide similar results. As Optiland is a relatively new software package, we advise caution when its results differ from OpticStudio, the current gold standard for optical simulations. See also Comparison between the OpticStudio and Optiland backends for an in-depth comparison of the two backends.

Analysis

OpticStudio

Optiland

Cardinal points

FFT MTF

⚠️

FFT PSF

⚠️

Huygens PSF

⚠️

OPD map

⚠️

Raytrace

Refraction

RMS HOA

Strehl ratio

⚠️

Zernike coefficients

Optiland FFT PSF, MTF and Strehl ratio

The Optiland FFT PSF analysis has been validated against the OpticStudio FFT PSF analysis, and the results are very similar for on-axis fields. However, there are some minor differences in the sampling strategy and pupil shape correction, which results in differences of a few percent for off-axis fields. This also affects the FFT MTF and Strehl ratio calculation, which are based on the PSF.

Optiland Huygens PSF

OpticStudio and Optiland use different sampling strategies and image sizes for the Huygens PSF calculation. This makes comparing results between the two backends difficult. For central fields, the results are very similar, but for off-axis fields, the results may differ significantly. Furthermore, due to the different sampling strategies, the Optiland Huygens Strehl ratio tends to be lower than the OpticStudio Huygens Strehl ratio.

Optiland OPD map

The OPD map calculated by Optiland may differ from the OPD map calculated by OpticStudio, especially for off-axis fields. This is caused by several implementation choices in Optiland regarding tilt removal and correction for the exit pupil shape.

Visualizing eye models#

Visisipy comes with basic model visualization tools in visisipy.plots:

fig, ax = plt.subplots()

# Plot the raytrace results
for f, r in raytrace.query("wavelength == 0.543").groupby("field"):
    ax.plot(r.z, r.y, label=f"{f[1]}°")

visisipy.plots.plot_eye(geometry=model.geometry, ax=ax, lens_edge_thickness=0.5)

ax.legend()
<matplotlib.legend.Legend at 0x7fd8a8515e50>
../_images/98aa6ef2f18b6b014ffeaf36918bbceaf505644efa0c9138792d49dea9634eea.png