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")

# 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)
Matplotlib is building the font cache; this may take a moment.
FourierPowerVectorRefraction(M=np.float64(-0.021794126920540887), J0=np.float64(5.168542791035303e-11), J45=np.float64(-6.751485584869772e-14))

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.495269018914356), J0=np.float64(4.874457721752864e-11), J45=np.float64(-9.246932145382768e-14))

Analyses available in Visisipy#

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=np.float64(-16.463955728853474), image=np.float64(22.021982081505744)), focal_points=CardinalPoints(object=np.float64(-7.250149109263879), image=np.float64(-0.006670375453129489)), principal_points=CardinalPoints(object=np.float64(9.213806619589594), image=np.float64(-22.028652456958874)), anti_principal_points=CardinalPoints(object=np.float64(-23.714104838117354), image=np.float64(22.015311706052614)), nodal_points=CardinalPoints(object=np.float64(14.771832972241864), image=np.float64(-16.470626104306604)), anti_nodal_points=CardinalPoints(object=np.float64(-29.272131190769624), image=np.float64(16.457285353400344)))

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/03bd07ef23c4b1052bdf2553861d979d0264d39e56173eeb9007da231cb5dc6e.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/7d78f11a721ed78edd605311b5aaadbd79198477097b523993d916978a05e458.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
index field wavelength surface comment x y z
0 0 (0.0, 0.0) 0.5430 0 0.0 0.000000 -3.055765e+00
1 1 (0.0, 0.0) 0.5430 1 cornea front 0.0 0.000000 4.440892e-16
2 2 (0.0, 0.0) 0.5430 2 cornea back / aqueous 0.0 0.000000 5.500000e-01
3 3 (0.0, 0.0) 0.5430 3 pupil 0.0 0.000000 3.600000e+00
4 4 (0.0, 0.0) 0.5430 4 lens front 0.0 0.000000 3.600000e+00
5 5 (0.0, 0.0) 0.5430 5 lens back / vitreous 0.0 0.000000 7.600000e+00
6 6 (0.0, 0.0) 0.5430 6 retina 0.0 0.000000 2.392030e+01
7 0 (0.0, 30.0) 0.5430 0 0.0 -3.519361 -3.055765e+00
8 1 (0.0, 30.0) 0.5430 1 cornea front 0.0 -1.652164 1.783145e-01
9 2 (0.0, 30.0) 0.5430 2 cornea back / aqueous 0.0 -1.406544 7.040066e-01
10 3 (0.0, 30.0) 0.5430 3 pupil 0.0 -0.030747 3.600000e+00
11 4 (0.0, 30.0) 0.5430 4 lens front 0.0 -0.030725 3.600046e+00
12 5 (0.0, 30.0) 0.5430 5 lens back / vitreous 0.0 1.638180 7.376364e+00
13 6 (0.0, 30.0) 0.5430 6 retina 0.0 7.823641 2.101924e+01
14 0 (0.0, 60.0) 0.5430 0 0.0 -10.558084 -3.055765e+00
15 1 (0.0, 60.0) 0.5430 1 cornea front 0.0 -3.678054 9.164217e-01
16 2 (0.0, 60.0) 0.5430 2 cornea back / aqueous 0.0 -3.141121 1.359362e+00
17 3 (0.0, 60.0) 0.5430 3 pupil 0.0 -0.360947 3.600000e+00
18 4 (0.0, 60.0) 0.5430 4 lens front 0.0 -0.353357 3.606117e+00
19 5 (0.0, 60.0) 0.5430 5 lens back / vitreous 0.0 3.112116 6.792894e+00
20 6 (0.0, 60.0) 0.5430 6 retina 0.0 11.746714 1.437279e+01
21 0 (0.0, 0.0) 0.6328 0 0.0 0.000000 -3.055765e+00
22 1 (0.0, 0.0) 0.6328 1 cornea front 0.0 0.000000 4.440892e-16
23 2 (0.0, 0.0) 0.6328 2 cornea back / aqueous 0.0 0.000000 5.500000e-01
24 3 (0.0, 0.0) 0.6328 3 pupil 0.0 0.000000 3.600000e+00
25 4 (0.0, 0.0) 0.6328 4 lens front 0.0 0.000000 3.600000e+00
26 5 (0.0, 0.0) 0.6328 5 lens back / vitreous 0.0 0.000000 7.600000e+00
27 6 (0.0, 0.0) 0.6328 6 retina 0.0 0.000000 2.392030e+01
28 0 (0.0, 30.0) 0.6328 0 0.0 -3.519361 -3.055765e+00
29 1 (0.0, 30.0) 0.6328 1 cornea front 0.0 -1.652164 1.783145e-01
30 2 (0.0, 30.0) 0.6328 2 cornea back / aqueous 0.0 -1.406246 7.039404e-01
31 3 (0.0, 30.0) 0.6328 3 pupil 0.0 -0.028409 3.600000e+00
32 4 (0.0, 30.0) 0.6328 4 lens front 0.0 -0.028390 3.600040e+00
33 5 (0.0, 30.0) 0.6328 5 lens back / vitreous 0.0 1.643125 7.375012e+00
34 6 (0.0, 30.0) 0.6328 6 retina 0.0 7.834953 2.100950e+01
35 0 (0.0, 60.0) 0.6328 0 0.0 -10.558084 -3.055765e+00
36 1 (0.0, 60.0) 0.6328 1 cornea front 0.0 -3.678054 9.164217e-01
37 2 (0.0, 60.0) 0.6328 2 cornea back / aqueous 0.0 -3.140492 1.359015e+00
38 3 (0.0, 60.0) 0.6328 3 pupil 0.0 -0.353868 3.600000e+00
39 4 (0.0, 60.0) 0.6328 4 lens front 0.0 -0.346552 3.605884e+00
40 5 (0.0, 60.0) 0.6328 5 lens back / vitreous 0.0 3.122446 6.787527e+00
41 6 (0.0, 60.0) 0.6328 6 retina 0.0 11.752176 1.434648e+01

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.7928086781783007), J0=np.float64(1.0982686296306978), J45=np.float64(2.333778747900814e-13))

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.8910773078089984), cylinder=np.float64(-2.1965372592613956), axis=np.float64(6.087566783046758e-12))
PolarPowerVectorRefraction(M=np.float64(0.7928086781783007), J=np.float64(1.0982686296306978), axis=np.float64(6.087566783046758e-12))

Strehl Ratio#

The Strehl ratio is a measure of the quality of the optical system. For a perfectly unabberated 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))
np.float64(0.21103246100834383)

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.1304
Z 2: 0.0000
Z 3: -0.0000
Z 4: 0.1078
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.0255
Z12: -0.0000
Z13: 0.0000
Z14: 0.0000
Z15: -0.0000

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 PSF

⚠️

Huygens PSF

⚠️

Raytrace

Refraction

RMS HOA

Strehl ratio

⚠️

Zernike coefficients

Optiland FFT PSF 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 notable differences with OpticStudio. These differences are currently being investigated and will be addressed in future releases of Optiland:

  • The PSF extent calculation ignores the image-space refractive index, resulting in incorrect PSF extents. This does not affect the PSF values.

  • PSF calculation of off-axis fields may result in incorrect values.

This also affects the Strehl ratio calculation, which is 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.

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 - (model.geometry.cornea_thickness + model.geometry.anterior_chamber_depth), r.y, label=f"{f[1]}°")

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

ax.set_xlim(-5, 25)
ax.set_ylim(-15, 15)
ax.set_aspect("equal")
ax.legend()
<matplotlib.legend.Legend at 0x7677ac52c980>
../_images/68944b942821057ee6aaf4ab54db9d52add864d0a36bd4eafd1138cb18c505f6.png