Skip to content

Commit

Permalink
Merge pull request #3 from MREYE-LUMC/crnh/doc/readme
Browse files Browse the repository at this point in the history
Write README for public version
  • Loading branch information
crnh authored Aug 26, 2024
2 parents 853c24e + eb599d0 commit 02ac8df
Show file tree
Hide file tree
Showing 13 changed files with 362 additions and 258 deletions.
134 changes: 96 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
# EyeSimulator
# Visisipy: accessible vision simulations in Python

This project aims to provide a standardized method to perform optical (ray tracing) simulations on eye models. `EyeSimulator` is just a working name and should preferrably be replaced with something better before releasing this library.
Visisipy (pronounced `/ˌvɪsəˈsɪpi/`, like Mississippi but with a V) is a Python library for optical simulations of the eye.
It provides an easy-to-use interface to define and build eye models, and to perform common ophthalmic analyses on these models.

## Goals

1. Provide a uniform interface to define, build and analyze various types of eye models, using abstractions that make sense in a clinical context;
2. Provide an accessible interface to the most common analyses on these models;
3. Decouple the model definition interface as much as possible from the simulation software, i.e. Zemax OpticStudio;
4. Introduce this as a standardized method for optical simulations of the eye, that can be used by the broader physiological optics community.
2. Provide a collection of ready-to-use eye models, such as the Navarro model[^navarro], that can be customized at need;
3. Provide an accessible interface to clinically relevant analyses with these models.

All calculations are currently performed in OpticStudio through the [ZOSPy][zospy] library[^zospy], but visisipy is designed in a modular fashion to allow for other backends in the future.

## Contributing

Visisipy aims to be a community-driven project and warmly accepts contributions. If you want to contribute, please email us ([email protected]) or [open a new discussion](https://github.com/MREYE-LUMC/visisipy/discussions).
Visisipy aims to be a community-driven project and warmly accepts contributions.
If you want to contribute, please email us ([email protected]) or [open a new discussion](https://github.com/MREYE-LUMC/visisipy/discussions).

## Installation

## Current implementation
Visisipy can be installed through `pip`:

In its current state, `EyeSimulator` provides
```bash
pip install git+https://github.com/MREYE-LUMC/visisipy.git
```

- Classes to define models in terms of clinically relevant and measurable parameters (`EyeGeometry`);
- An abstract base class `BaseEye` and a simple implementation `Eye`, which allows to build and analyze these eye models;
- A `Surface` class, which acts as a bridge between the abstractions used in this library and Zemax OpticStudio. This allows for seamless integration of OpticStudio models in our `Eye` class.
Visisipy will be made available through PyPI and Conda as soon as possible.

## Example

Expand All @@ -35,7 +40,11 @@ model = visisipy.EyeModel()
model.build()

# Perform a raytrace analysis
raytrace = visisipy.analysis.raytrace(coordinates=zip([0] * 5, range(0, 60, 10)))
coordinates = [(0, 0), (0, 10), (0, 20), (0, 30), (0, 40)]
raytrace = visisipy.analysis.raytrace(coordinates=coordinates)

# Alternatively, the model can be built and analyzed in one go:
# raytrace = visisipy.analysis.raytrace(model, coordinates=zip([0] * 5, range(0, 60, 10)))

# Visualize the model
fig, ax = plt.subplots()
Expand All @@ -49,42 +58,91 @@ sns.lineplot(raytrace, x="z", y="y", hue="field", ax=ax)
plt.show()
```

### Configure the backend

Visisipy uses OpticStudio as a backend for calculations; this is currently the only supported backend.
This backend is automatically started and managed in the background, but can also be configured manually.

```python
import zospy as zp
from visisipy import EyeModel, NavarroGeometry
from visisipy.opticstudio import OpticStudioEye
import visisipy

# Use OpticStudio in standalone mode (default)
visisipy.set_backend("opticstudio")

# Use OpticStudio in extension mode
visisipy.set_backend("opticstudio", mode="extension")

# Use OpticStudio in extension mode with ray aiming enabled
visisipy.set_backend("opticstudio", mode="extension", ray_aiming="real")

# Get the OpticStudioSystem from visisipy to interact with it manually
# This only works when the backend is set to "opticstudio"
# See https://zospy.readthedocs.io/en/latest/api/zospy.zpcore.OpticStudioSystem.html for documentation of this object
oss = visisipy.backend.get_oss()
```

# Initialize ZOSPy
zos = zp.ZOS()
oss = zos.connect()
### Create a custom eye model from clinical parameters

# Initialize an eye, using a slightly modified Navarro model
eye_model = EyeModel(NavarroGeometry(iris_radius=0.5))
eye = OpticStudioEye(eye_model)
An eye model in visispy consists of two parts: the geometry and the material properties.
The geometry is defined by `visisipy.models.EyeGeometry`, and the material properties are defined by `visisipy.models.Materials`.
They are combined in `visisipy.EyeModel` to constitute a complete eye model.

# Update a parameter of one of the eye's surfaces
eye.lens_front.refractive_index += 1.0
```python
import visisipy

geometry = visisipy.models.create_geometry(
axial_length=20,
cornea_thickness=0.5,
anterior_chamber_depth=3,
lens_thickness=4,
cornea_front_radius=7,
cornea_front_asphericity=0,
cornea_back_radius=6,
cornea_back_asphericity=0,
lens_front_radius=10,
lens_front_asphericity=0,
lens_back_radius=-6,
lens_back_asphericity=0,
retina_radius=-12,
retina_asphericity=0,
pupil_radius=1.0,
)

# Use this geometry together with the refractive indices of the Navarro model
model = visisipy.EyeModel(geometry=geometry, materials=visisipy.models.materials.NavarroMaterials())

# NavarroMaterials is the default, so this is equivalent:
model = visisipy.EyeModel(geometry=geometry)
```

# Build the eye in OpticStudio
eye.build(oss)
### Interact with the eye model in OpticStudio

# Change the lens's refractive index back
eye.lens_front.refractive_index -= 1.0
```python
import visisipy

# Insert a new (dummy) surface in OpticStudio
oss.LDE.InsertNewSurfaceAt(1).Comment = "input beam"
# Just use the default Navarro model
model = visisipy.EyeModel()

# Relink the surfaces of the eye, because a new surface was added
eye.relink_surfaces(oss)
# Build the model in OpticStudio
built_model: visisipy.opticstudio.OpticStudioEye = model.build()

# Check if the relinking succeeded
assert eye.lens_back.radius == eye_model.geometry.lens_back_curvature
# Update the lens front radius
built_model.lens_front.radius = 10.5
```

## Planned functions

- Generation of realistic randomized eye models using the method proposed by Rozema et al.[^rozema]

## Future ideas

- Provide (customizable) geometry definitions for various standard eye models, e.g. `NavarroGeometry`, `GullstrandGeometry`.
- Most likely as child classes of `EyeGeometry`. The default values currently used in `EyeGeometry` will then be removed.
- The same applies for the `EyeMaterials` class.
- Add more implementations of `BaseEye`, e.g. `ReverseEye`, a reversed version of `Eye`.
- Integrate the eye plot functions defined in [chaasjes/utilities](https://git.lumc.nl/chaasjes/utilities/-/tree/main/utilities/plots/eye) in this library for easy visualization of eye models.
- Provide (customizable) geometry definitions for other standard eye models, e.g. `GullstrandGeometry`.
- Add support for reversed eyes.
- Add support for other (open source) ray tracing backends.

[zospy]: https://zospy.readthedocs.io/

[//]: # (References)
[^navarro]: Escudero-Sanz, I., & Navarro, R. (1999). Off-axis aberrations of a wide-angle schematic eye model. JOSA A, 16(8), 1881–1891. https://doi.org/10.1364/JOSAA.16.001881
[^rozema]: Rozema, J. J., Rodriguez, P., Navarro, R., & Tassignon, M.-J. (2016). SyntEyes: A Higher-Order Statistical Eye Model for Healthy Eyes. Investigative Ophthalmology & Visual Science, 57(2), 683–691. https://doi.org/10.1167/iovs.15-18067
[^zospy]: Vught, L. van, Haasjes, C., & Beenakker, J.-W. M. (2024). ZOSPy: Optical ray tracing in Python through OpticStudio. Journal of Open Source Software, 9(96), 5756. https://doi.org/10.21105/joss.05756
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from __future__ import annotations

from typing import NamedTuple
from typing import TYPE_CHECKING, NamedTuple

import numpy as np
import pandas as pd

if TYPE_CHECKING:
import pandas as pd


def get_ray_output_angle(
df: pd.DataFrame,
reference_point: tuple[float, float] = (0, 0),
coordinate="y",
df: pd.DataFrame,
reference_point: tuple[float, float] = (0, 0),
coordinate="y",
):
"""Calculate the output angle of a ray with respect to the optical axis and a reference point."""
x0, y0 = reference_point
Expand All @@ -30,22 +32,18 @@ class InputOutputAngles(NamedTuple):

@classmethod
def from_ray_trace_result(
cls,
raytrace_result: pd.DataFrame,
np2: float,
np2_navarro: float = None,
retina_center: float = None,
patient: int = None,
coordinate="y",
) -> "InputOutputAngles":
cls,
raytrace_result: pd.DataFrame,
np2: float,
np2_navarro: float | None = None,
retina_center: float | None = None,
patient: int | None = None,
coordinate="y",
) -> InputOutputAngles:
return cls(
input_angle_field=raytrace_result.field[0][1],
output_angle_pupil=get_ray_output_angle(
raytrace_result, reference_point=(0, 0), coordinate=coordinate
),
output_angle_np2=get_ray_output_angle(
raytrace_result, reference_point=(np2, 0), coordinate=coordinate
),
output_angle_pupil=get_ray_output_angle(raytrace_result, reference_point=(0, 0), coordinate=coordinate),
output_angle_np2=get_ray_output_angle(raytrace_result, reference_point=(np2, 0), coordinate=coordinate),
output_angle_retina_center=(
get_ray_output_angle(
raytrace_result,
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ extend-ignore = [
]

[tool.ruff.lint.extend-per-file-ignores]
"examples/*" = [ "INP001" ]
"tests/*" = [ "ARG001", "ARG002" ]
"tests/opticstudio/*" = [ "SLF001" ]

Expand Down
47 changes: 32 additions & 15 deletions tests/opticstudio/test_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,32 @@ def opticstudio_analysis(opticstudio_backend):


class TestCardinalPointsAnalysis:
@pytest.mark.parametrize("surface_1,surface_2,expectation", [
(None, None, does_not_raise()),
(1, 6, does_not_raise()),
(2, 4, does_not_raise()),
(-1, 7, pytest.raises(ValueError, match="surface_1 and surface_2 must be between 1 and the number of surfaces in the system")),
(1, 8, pytest.raises(ValueError, match="surface_1 and surface_2 must be between 1 and the number of surfaces in the system")),
(3, 3, pytest.raises(ValueError, match="surface_1 must be less than surface_2")),
(4, 2, pytest.raises(ValueError, match="surface_1 must be less than surface_2")),
])
@pytest.mark.parametrize(
"surface_1,surface_2,expectation",
[
(None, None, does_not_raise()),
(1, 6, does_not_raise()),
(2, 4, does_not_raise()),
(
-1,
7,
pytest.raises(
ValueError,
match="surface_1 and surface_2 must be between 1 and the number of surfaces in the system",
),
),
(
1,
8,
pytest.raises(
ValueError,
match="surface_1 and surface_2 must be between 1 and the number of surfaces in the system",
),
),
(3, 3, pytest.raises(ValueError, match="surface_1 must be less than surface_2")),
(4, 2, pytest.raises(ValueError, match="surface_1 must be less than surface_2")),
],
)
def test_cardinal_points(self, opticstudio_backend, opticstudio_analysis, surface_1, surface_2, expectation):
opticstudio_backend.build_model(EyeModel())

Expand Down Expand Up @@ -96,12 +113,12 @@ class TestRefractionAnalysis:
],
)
def test_refraction(
self,
opticstudio_analysis,
use_higher_order_aberrations,
wavelength,
pupil_diameter,
monkeypatch,
self,
opticstudio_analysis,
use_higher_order_aberrations,
wavelength,
pupil_diameter,
monkeypatch,
):
monkeypatch.setattr(opticstudio_analysis._backend, "model", MockOpticstudioModel())

Expand Down
Loading

0 comments on commit 02ac8df

Please sign in to comment.