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
modelis 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.parametersare the parameters to be passed to the analysis. These parameters are specific to each analysis.return_raw_resultis a boolean that indicates whether to return the raw result from the backend. This parameter is optional and defaults toFalse.backendis 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();
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]");
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]");
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]");
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>