PAROS 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
PAROS/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """PARaxial Optical fundus Scaling (PAROS)
2
+
3
+ Paros is a method to calculate the magnification of fundus images based on the optical characteristics of the patient's
4
+ eye. The full method and validation are described in (article reference to be added upon acceptance).
5
+ """
6
+
7
+ from PAROS import data, eyephantom, fundscale
8
+
9
+ __all__ = ("data", "eyephantom", "fundscale")
10
+ __version__ = "0.1.0"
PAROS/data/__init__.py ADDED
@@ -0,0 +1,70 @@
1
+ """Calibration and lens data files."""
2
+
3
+ import importlib.resources
4
+ from pathlib import Path
5
+ from typing import cast
6
+
7
+ import pandas as pd
8
+
9
+ __all__ = ("get_camera_calibration_data", "get_lens_data")
10
+
11
+ calibration = importlib.resources.files("PAROS.data.calibration")
12
+ lenses = importlib.resources.files("PAROS.data.lenses")
13
+
14
+
15
+ def _find_available_manufacturers():
16
+ return [i.name for i in calibration.iterdir() if i.is_dir()]
17
+
18
+
19
+ _camera_manufacturers = _find_available_manufacturers()
20
+
21
+
22
+ def get_camera_calibration_data(manufacturer: str, camera: str) -> pd.DataFrame:
23
+ """Get camera calibration data for a given manufacturer and camera.
24
+
25
+ Reads the data file at `PAROS/data/calibration/{manufacturer}/{camera}.csv` and
26
+ returns it as a pandas DataFrame.
27
+
28
+ Parameters
29
+ ----------
30
+ manufacturer : str
31
+ Camera manufacturer name.
32
+ camera : str
33
+ Camera model name.
34
+
35
+ Returns
36
+ -------
37
+ pandas.DataFrame
38
+ Dataframe containing the camera calibration data.
39
+ """
40
+ if manufacturer not in _camera_manufacturers:
41
+ raise KeyError(f"Manufacturer {manufacturer} does not exist.")
42
+
43
+ try:
44
+ camera_data_file = cast(Path, calibration.joinpath(manufacturer, camera + ".csv"))
45
+
46
+ return pd.read_csv(camera_data_file)
47
+ except FileNotFoundError as e:
48
+ raise KeyError(f"Camera {camera} does not exist for manufacturer {manufacturer}.") from e
49
+
50
+
51
+ def get_lens_data(name: str) -> pd.DataFrame:
52
+ """Get lens data.
53
+
54
+ Reads the data file at `PAROS/data/lenses/{name}.csv` and returns it as a pandas DataFrame.
55
+
56
+ Parameters
57
+ ----------
58
+ name : str
59
+ Name of the lens data set.
60
+
61
+ Returns
62
+ -------
63
+ pandas.DataFrame
64
+ Dataframe containing the lens data.
65
+ """
66
+ try:
67
+ lens_data_file = cast(Path, lenses.joinpath(name + ".csv"))
68
+ return pd.read_csv(lens_data_file)
69
+ except FileNotFoundError as e:
70
+ raise KeyError(f"Lens data does not exist for name {name}.") from e
@@ -0,0 +1,10 @@
1
+ experiment_number,lens_1,lens_2,uncorrected_measurement,distance_camera_eye_mm,distance_camera_pinhole_mm,distance_pinhole_lens1_mm,distance_lens1_lens2_mm,distance_lens2_grid_mm,axial_length
2
+ 0,LB_1471,LB_1945,6.966666667,5.5,49.61969784,2.880302163,3.027080745,29.69261709,40.71969784
3
+ 1,LB_1471,LB_1945,6.726666667,5.5,49.61969784,2.880302163,3.027080745,31.69261709,42.71969784
4
+ 2,LB_1471,LB_1945,6.6,5.5,49.61969784,2.880302163,3.027080745,33.69261709,44.71969784
5
+ 3,LB_1471,LB_1945,6.37,5.5,49.61969784,2.880302163,3.027080745,35.69261709,46.71969784
6
+ 4,LB_1471,LB_1945,6.25,5.5,49.61969784,2.880302163,3.027080745,37.69261709,48.71969784
7
+ 5,LB_1471,LB_1945,6.086666667,5.5,49.61969784,2.880302163,3.027080745,39.69261709,50.71969784
8
+ 6,LB_1471,LB_1945,5.973333333,5.5,49.61969784,2.880302163,3.027080745,41.69261709,52.71969784
9
+ 7,LB_1471,LB_1945,5.746666667,5.5,49.61969784,2.880302163,3.027080745,43.69261709,54.71969784
10
+ 8,LB_1471,LB_1945,5.613333333,5.5,49.61969784,2.880302163,3.027080745,45.39261709,56.41969784
@@ -0,0 +1,11 @@
1
+ lens_1,lens_2,axial_length,distance_camera_eye_mm,uncorrected_measurement,distance_camera_pinhole_mm,distance_pinhole_lens1_mm,distance_lens1_lens2_mm,distance_lens2_grid_mm,experiment_number
2
+ LE_1156,LB_1945,83.9874534,5.5,3.31,50.2374534,2.262546596,6.344836312,71.54261709,0
3
+ LE_1156,LB_1945,90.7374534,5.5,3.13,50.2374534,2.262546596,6.344836312,78.29261709,1
4
+ LE_1156,LB_1945,78.0374534,5.5,3.46,50.2374534,2.262546596,6.344836312,65.59261709,2
5
+ LE_1156,LB_1945,134.6374534,5.5,2.24,50.2374534,2.262546596,6.344836312,122.1926171,3
6
+ LE_1156,LB_1945,107.9374534,5.5,2.69,50.2374534,2.262546596,6.344836312,95.49261709,4
7
+ LE_1156,LB_1945,72.0374534,5.5,3.71,50.2374534,2.262546596,6.344836312,59.59261709,5
8
+ LE_1156,LB_1945,63.6374534,5.5,4.08,50.2374534,2.262546596,6.344836312,51.19261709,6
9
+ LE_1234,LB_1056,62.51915876,5,4.28,45.31915876,2.180841236,5.205295461,51.1138633,7
10
+ LE_1234,LB_1056,75.91915876,5,3.66,45.31915876,2.180841236,5.205295461,64.5138633,8
11
+ LE_1234,LB_1056,96.51915876,5,2.98,45.31915876,2.180841236,5.205295461,85.1138633,9
@@ -0,0 +1 @@
1
+ """Camera calibration data files."""
@@ -0,0 +1 @@
1
+ """Lens data files."""
@@ -0,0 +1,13 @@
1
+ name,lens_type,thickness_centre,thickness_edge,diameter,power,front_radius,back_radius,refractive_index,power_calculated
2
+ LB_1409,biconvex,2.2,2.0,25.6,1.0,1029.8,-1029.8,1.515,1.0001942124684402
3
+ LB_1869,biconvex,2.3,2.0,25.6,2.0,514.7,-514.7,1.515,2.001165727608315
4
+ LB_1056_1,biconvex,2.6,2.0,25.6,4.0,257.1,-257.1,1.515,4.006223259432127
5
+ LE_1234,convex-concave,3.6,2.0,25.6,10.0,-82.2,-32.1,1.515,9.77840689451304
6
+ LE_1156,convex-concave,3.3,2.0,25.6,8.0,-106.9,-40.6,1.515,7.867142534572616
7
+ LE_1104,convex-concave,3.1,2.0,25.6,6.7,-131.6,-49.1,1.515,6.575424510489725
8
+ LE_1202,convex-concave,2.8,2.0,25.6,5.0,-182.2,-66.2,1.515,4.952891978205283
9
+ LB_1945,biconvex,2.8,2.0,25.6,5.0,205.6,-205.6,1.515,5.009727626459142
10
+ LB_1056,biconvex,2.6,2.0,25.6,4.0,257.1,-257.1,1.515,4.006223259432127
11
+ LB_1779,biconvex,2.5,2.0,25.6,3.3,308.6,-308.6,1.515,3.3376539209332456
12
+ LB_1471,biconvex,5.2,2.0,25.6,20.0,50.6,-50.6,1.515,20.355731225296400
13
+ LB_1757,biconvex,7.7,2.0,25.6,33.3,29.5,-29.5,1.515,34.91525423728813
PAROS/eyephantom.py ADDED
@@ -0,0 +1,197 @@
1
+ """Paraxial optical modelling of an eye phantom for fundus camera calibrations.
2
+
3
+ This module contains classes to model an eye phantom used to calibrate a fundus camera.
4
+ Lenses are specified using the `Lens` class. The full phantom is modeled with the `EyePhantom` class.
5
+ Lens and phantom data is included in `PAROS.data`.
6
+ This module can be used in conjunction with `PAROS.fundscale` to calculate the central magnification of the phantom.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Literal
12
+
13
+ import pandas as pd
14
+ import sympy as sp
15
+
16
+ from PAROS.fundscale import spherical_interface, uniform_medium
17
+
18
+ sp.init_printing(use_unicode=True)
19
+
20
+
21
+ class Lens:
22
+ """Ray transfer model of a lens."""
23
+
24
+ def __init__(
25
+ self,
26
+ name: str,
27
+ center_thickness: float,
28
+ edge_thickness: float,
29
+ back_curvature: float,
30
+ front_curvature: float,
31
+ refractive_index: float,
32
+ diameter: float,
33
+ power: float,
34
+ lens_type: Literal["biconvex", "convex-concave"],
35
+ ):
36
+ """Initialize a lens model.
37
+
38
+ Parameters
39
+ ----------
40
+ name : str
41
+ Name of the lens.
42
+ center_thickness : float
43
+ Central thickness of the lens, in millimeters.
44
+ edge_thickness : float
45
+ Edge thickness of the lens, in millimeters.
46
+ back_curvature : float
47
+ Radius of curvature of the back surface, in millimeters.
48
+ front_curvature : float
49
+ Radius of curvature of the front surface, in millimeters.
50
+ refractive_index : float
51
+ Refractive index of the lens.
52
+ diameter : float
53
+ Diameter of the lens, in millimeters.
54
+ power : float
55
+ Power according to the lens data sheet, in diopters.
56
+ lens_type : str
57
+ Lens type, one of ['biconvex', 'convex-concave'].
58
+ """
59
+ self.name = name
60
+ self.center_thickness = center_thickness
61
+ self.edge_thickness = edge_thickness
62
+ self.back_curvature = back_curvature
63
+ self.front_curvature = front_curvature
64
+ self.refractive_index = refractive_index
65
+ self.lens_type = lens_type
66
+ self.diameter = diameter
67
+ self.power = power
68
+
69
+ def __str__(self):
70
+ return f"{self.name}"
71
+
72
+ def ray_transfer_matrix(self) -> sp.Matrix:
73
+ """Calculate the ray transfer matrix for the lens.
74
+
75
+ Returns
76
+ -------
77
+ sympy.Matrix
78
+ Ray transfer matrix for the lens.
79
+ """
80
+ air_refractive_index = 1.0
81
+
82
+ return (
83
+ spherical_interface(self.refractive_index, air_refractive_index, self.back_curvature * 1e-3)
84
+ * uniform_medium(self.center_thickness * 1e-3)
85
+ * spherical_interface(air_refractive_index, self.refractive_index, self.front_curvature * 1e-3)
86
+ )
87
+
88
+
89
+ def make_lens(lens_file: pd.DataFrame, name: str) -> Lens:
90
+ """Create a `Lens` object for the lens `name` from `lens_file`.
91
+
92
+ Parameters
93
+ ----------
94
+ lens_file : pandas.DataFrame
95
+ DataFrame with lens specifications.
96
+ name : str
97
+ Name of the lens, must occur in `lens_file`.
98
+
99
+ Returns
100
+ -------
101
+ Lens
102
+ `Lens` object.
103
+ """
104
+ lens_info = lens_file[lens_file["name"] == name].iloc[0]
105
+
106
+ return Lens(
107
+ name=lens_info["name"], # 'name' is a property of pandas.Series
108
+ center_thickness=float(lens_info.thickness_centre),
109
+ edge_thickness=float(lens_info.thickness_edge),
110
+ back_curvature=float(lens_info.back_radius),
111
+ front_curvature=float(lens_info.front_radius),
112
+ refractive_index=float(lens_info.refractive_index),
113
+ diameter=float(lens_info.diameter),
114
+ power=float(lens_info.power),
115
+ lens_type=lens_info.lens_type,
116
+ )
117
+
118
+
119
+ class EyePhantom:
120
+ """Eye phantom consisting of two lenses and a grid.
121
+
122
+ Specification of an eye phantom consisting of two lenses and a grid.
123
+ The first lens models the cornea, the second lens the eye lens.
124
+
125
+ Attributes
126
+ ----------
127
+ name : str
128
+ Name of the phantom.
129
+ iol : Lens
130
+ `Lens` object for the eye lens.
131
+ cornea : Lens
132
+ `Lens` object for the cornea.
133
+ lens_to_lens_distance : float
134
+ Distance between the cornea and the lens.
135
+ lens_to_grid_distance : float
136
+ Distance between the lens and the grid.
137
+ camera_distance : float
138
+ Distance from the cornea front to the camera.
139
+ ray_transfer_matrix : sympy.Matrix
140
+ Ray transfer matrix for the phantom.
141
+ ray_transfer_matrix_to_camera : sympy.Matrix
142
+ Ray transfer matrix between the phantom and the camera.
143
+ """
144
+
145
+ def __init__(self, experiment: pd.Series = None, lens_file: pd.DataFrame = None):
146
+ """Initialize an eye phantom from a dataframe.
147
+
148
+ Currently only implemented for models of type 'set1'. If the dataframe
149
+ contains another model type, an empty model is created.
150
+
151
+ Parameters
152
+ ----------
153
+ experiment : pandas.Series
154
+ Series with experiment parameters.
155
+ lens_file : pandas.DataFrame
156
+ DataFrame with lens specifications.
157
+ """
158
+ if experiment is not None and lens_file is not None:
159
+ self.build_from_dataframe(experiment, lens_file)
160
+ else:
161
+ self.name = "unknown"
162
+ self.iol = None
163
+ self.cornea = None
164
+ self.lens_to_lens_distance = None
165
+ self.lens_to_grid_distance = None
166
+ self.camera_distance = None
167
+ self.ray_transfer_matrix = None
168
+ self.ray_transfer_matrix_to_camera = None
169
+
170
+ def __str__(self):
171
+ return f"{self.name}"
172
+
173
+ def build_from_dataframe(self, experiment: pd.Series, lens_file: pd.DataFrame) -> None:
174
+ """Initializes an `EyePhantom` instance from a pandas DataFrame.
175
+
176
+ Parameters
177
+ ----------
178
+ experiment : pandas.Series
179
+ Series with experiment parameters.
180
+ lens_file : pandas.DataFrame
181
+ DataFrame with lens specifications.
182
+ """
183
+ self.name: str = experiment.get("name")
184
+ self.iol: Lens = make_lens(lens_file, experiment.lens_2)
185
+ self.cornea: Lens = make_lens(lens_file, experiment.lens_1)
186
+ self.lens_to_lens_distance: float = float(experiment.distance_lens1_lens2_mm)
187
+ self.lens_to_grid_distance: float = float(experiment.distance_lens2_grid_mm)
188
+ self.camera_distance: float = experiment.distance_camera_eye_mm
189
+
190
+ matrix = (
191
+ self.cornea.ray_transfer_matrix()
192
+ * uniform_medium(self.lens_to_lens_distance * 1e-3)
193
+ * self.iol.ray_transfer_matrix()
194
+ * uniform_medium(self.lens_to_grid_distance * 1e-3)
195
+ )
196
+ self.ray_transfer_matrix: sp.Matrix = matrix
197
+ self.ray_transfer_matrix_to_camera = uniform_medium(self.camera_distance * 1e-3)
PAROS/fundscale.py ADDED
@@ -0,0 +1,667 @@
1
+ """Paraxial central scaling of fundus images.
2
+
3
+ This module implements a paraxial method to calculate the central scaling
4
+ of fundus images. The scaling is calculated by defining an eye using the
5
+ `Eye` class and a fundus camera using the `Camera` class. The magnification
6
+ can then be calculated using `calculate_magnification`.
7
+
8
+ Throughout this module, the following abbreviations are used for ocular
9
+ geometrical parameters:
10
+
11
+ - R_corF: cornea front radius
12
+ - R_corB: cornea back radius
13
+ - R_lensF: lens front radius
14
+ - R_lensB: lens back radius
15
+ - D_cor: cornea thickness
16
+ - D_ACD: anterior chamber depth
17
+ - D_lens: lens thickness
18
+ - D_vitr: vitreous thickness
19
+
20
+ Unless otherwise specified, sizes and distances are specified in meters.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import copy
26
+ import math
27
+ from typing import Literal, NamedTuple, Union
28
+ from warnings import warn
29
+
30
+ import numpy as np
31
+ import sympy as sp
32
+
33
+ sp.init_printing(use_unicode=True)
34
+
35
+ NumberOrSymbol = Union[int, float, sp.Symbol]
36
+ EyeModelType = Literal["Navarro", "VughtIOL"]
37
+
38
+
39
+ def _round_expr(expr: sp.Expr, num_digits: int = 3) -> sp.Expr:
40
+ return expr.xreplace({n: round(n, num_digits) for n in expr.atoms(sp.Number)})
41
+
42
+
43
+ def uniform_medium(thickness: float) -> sp.Matrix:
44
+ """Uniform medium without refraction.
45
+
46
+ Parameters
47
+ ----------
48
+ thickness : float
49
+ Thickness of the medium.
50
+
51
+ Returns
52
+ -------
53
+ sympy.Matrix
54
+ Ray transfer matrix for the uniform medium.
55
+ """
56
+ return sp.Matrix([[1, thickness], [0, 1]])
57
+
58
+
59
+ def medium_change(n_in: float, n_out: float) -> sp.Matrix:
60
+ """Change between media.
61
+
62
+ Parameters
63
+ ----------
64
+ n_in : float
65
+ Refractive index of the first medium.
66
+ n_out : float
67
+ Refractive index of the second medium.
68
+
69
+ Returns
70
+ -------
71
+ sympy.Matrix
72
+ Ray transfer matrix for the medium change.
73
+ """
74
+ return sp.Matrix([[1, 0], [0, n_in / n_out]])
75
+
76
+
77
+ def spherical_interface(n_in: float, n_out: float, curvature: float) -> sp.Matrix:
78
+ """Spherical interface between two media.
79
+
80
+ Parameters
81
+ ----------
82
+ n_in : float
83
+ Refractive index of the first medium.
84
+ n_out : float
85
+ Refractive index of the second medium.
86
+ curvature : float
87
+ Radius of curvature of the interface.
88
+
89
+ Returns
90
+ -------
91
+ sympy.Matrix
92
+ Ray transfer matrix for the spherical interface.
93
+ """
94
+ return sp.Matrix([[1, 0], [(n_in - n_out) / (n_out * curvature), (n_in / n_out)]])
95
+
96
+
97
+ def calculate_piol_curvature(piol_power: float, thickness: float = 0.2e-3, n_iol: float = 1.47) -> float:
98
+ """Calculate the radius of curvature for a phakic IOL.
99
+
100
+ Calculate the IOL radii for a pIOL with power `piol_power`. The front and back
101
+ curvatures of the IOL are equal.
102
+
103
+ The radii are calculated according to ISO 11979-2-2014 A.2.1:
104
+
105
+ .. math:: D_{iol} = 2 D_{front} - (t / n_{iol}) D_{front} ^ 2
106
+ .. math:: D_{front} = (n_{iol} - n_{medium}) / R_{front}
107
+ .. math:: n_{medium} = 1.336
108
+
109
+ Parameters
110
+ ----------
111
+ piol_power : float
112
+ Power of the pIOL, in diopters.
113
+ thickness : float
114
+ Thickness of the pIOL, in meters.
115
+ n_iol : float
116
+ Refractive index of the pIOL.
117
+
118
+ Returns
119
+ -------
120
+ float
121
+ Radius of curvature of the front and back pIOL surface.
122
+
123
+ Notes
124
+ -----
125
+ The calculation of the pIOL curvature seems to be in congruence with ANSI standard
126
+ and signs are correct.
127
+ """
128
+ n_medium = 1.336
129
+
130
+ A = -thickness / n_iol # noqa: N806
131
+ B = 2 # noqa: N806
132
+ C = -piol_power # noqa: N806
133
+
134
+ front_power = (-B + math.sqrt(B * B - 4 * A * C)) / (2 * A)
135
+
136
+ return (n_iol - n_medium) / front_power
137
+
138
+
139
+ def calculate_piol_matrix(
140
+ piol_power: NumberOrSymbol,
141
+ thickness: float,
142
+ n_iol: float,
143
+ distance_iol_lens: float,
144
+ n_aq: float,
145
+ ) -> sp.Matrix:
146
+ """Calculate the ray transfer matrix for a phakic IOL.
147
+
148
+ Parameters
149
+ ----------
150
+ piol_power : float
151
+ Power of the pIOL, in diopters.
152
+ thickness : float
153
+ Thickness of the pIOL, in meters.
154
+ n_iol : float
155
+ Refractive index of the pIOL.
156
+ distance_iol_lens : float
157
+ Distance between the pIOL and the crystalline lens, in m.
158
+ n_aq : float
159
+ Refractive index of the aqueous humor.
160
+
161
+ Returns
162
+ -------
163
+ sympy.Matrix
164
+ Ray transfer matrix for the pIOL.
165
+ """
166
+ piol_curvature = calculate_piol_curvature(piol_power, thickness, n_iol)
167
+
168
+ piol_matrix = (
169
+ spherical_interface(n_iol, n_aq, -1 * piol_curvature)
170
+ * uniform_medium(thickness)
171
+ * spherical_interface(n_aq, n_iol, piol_curvature)
172
+ )
173
+ distance_matrix = uniform_medium(distance_iol_lens)
174
+
175
+ return piol_matrix * distance_matrix
176
+
177
+
178
+ class PhakicIOL(NamedTuple):
179
+ """Parameters of a phakic IOL.
180
+
181
+ Attributes
182
+ ----------
183
+ power : NumberOrSymbol
184
+ Power in diopters.
185
+ thickness : NumberOrSymbol
186
+ Thickness in meters.
187
+ refractive_index : NumberOrSymbol
188
+ Refractive index of the pIOL material.
189
+ lens_distance : NumberOrSymbol
190
+ Distance to the crystalline lens in meters.
191
+ """
192
+
193
+ power: NumberOrSymbol
194
+ thickness: NumberOrSymbol
195
+ refractive_index: NumberOrSymbol
196
+ lens_distance: NumberOrSymbol
197
+
198
+
199
+ class Eye:
200
+ """Symbolic representation of an eye model."""
201
+
202
+ def __init__(
203
+ self,
204
+ name: str = "testEye",
205
+ geometry: dict[str, float] | None = None,
206
+ model_type: EyeModelType = "Navarro",
207
+ NType: EyeModelType = "Navarro", # noqa: N803
208
+ refraction: float | None = None,
209
+ pIOL: PhakicIOL | None = None, # noqa: N803
210
+ ) -> None:
211
+ """Initialize an eye model.
212
+
213
+ This class works with a symbolic representation of the eye model, in which the
214
+ numeric values of the model parameters as defined in `geometry` are substituted
215
+ when needed. The eye's geometry can be passed to this function as a dictionary.
216
+ If this is a partial dictionary, the geometrical parameters corresponding to
217
+ `model_type` are updated with these values.
218
+
219
+ Parameters
220
+ ----------
221
+ name : str
222
+ Name of the eye model.
223
+ geometry : dict[str, float]
224
+ Dictionary with numerical geometric parameters of the eye. If `None`, the
225
+ geometry is based on `model_type`. Can be a partial dictionary, in which
226
+ case missing values are based on the eye model specified by `model_type`.
227
+ model_type : EyeModelType
228
+ Standard eye model on which the eye geometry is based. One of
229
+ ["Navarro", "VughtIOL"].
230
+ NType : EyeModelType
231
+ Standard eye model from which the refractive indices are used. One of
232
+ ["Navarro", "VughtIOL"].
233
+ refraction : float
234
+ Spherical equivalent of refraction of the eye model. The lens back curvature
235
+ can be adjusted to obtain this refraction with `Eye.adjust_lens_back`.
236
+ pIOL : PhakicIOL
237
+ Properties of the pIOL, if present. Tuple of power, thickness, refractive
238
+ index, distance to lens.
239
+ """
240
+ self.name = name
241
+ self.spherical_equivalent = refraction
242
+ self.is_pseudophakic = model_type == "VughtIOL"
243
+ self.axial_length = None
244
+ self.pIOL = pIOL
245
+
246
+ self.geometry = self._build_geometry_dictionary(model_type, geometry)
247
+
248
+ if NType == "Navarro":
249
+ self.refractive_indices = {
250
+ "cor": 1.3777,
251
+ "aq": 1.3391,
252
+ "lens": 1.4222,
253
+ "vit": 1.3377,
254
+ }
255
+ elif NType == "VughtIOL":
256
+ self.refractive_indices = {
257
+ "cor": 1.3777,
258
+ "aq": 1.3391,
259
+ "lens": 1.47,
260
+ "vit": 1.3377,
261
+ }
262
+ else:
263
+ raise ValueError(f"Model type {NType} is undefined.")
264
+
265
+ self.R_corF, self.R_corB, self.R_lensF, self.R_lensB = sp.symbols("R_corF R_corB R_lensF R_lensB")
266
+ self.D_cor, self.D_ACD, self.D_lens, self.D_vitr = sp.symbols("D_cor D_ACD D_lens D_vitr")
267
+
268
+ self.axial_length = (
269
+ self.geometry["D_cor"] + self.geometry["D_ACD"] + self.geometry["D_lens"] + self.geometry["D_vitr"]
270
+ )
271
+ self.ray_transfer_matrix = self.calculate_ray_transfer_matrix()
272
+
273
+ def __str__(self) -> str:
274
+ return f"{self.name}"
275
+
276
+ @staticmethod
277
+ def _build_geometry_dictionary(model_type: EyeModelType, partial_geometry: dict[str, float] | None = None):
278
+ if model_type == "Navarro":
279
+ geometry = {
280
+ "R_corF": -7.72e-3,
281
+ "R_corB": -6.50e-3,
282
+ "R_lensF": -10.20e-3,
283
+ "R_lensB": +6.00e-3,
284
+ "D_cor": 0.55e-3,
285
+ "D_ACD": 3.05e-3,
286
+ "D_lens": 4.00e-3,
287
+ "D_vitr": 16.3203e-3,
288
+ }
289
+ elif model_type == "VughtIOL":
290
+ geometry = {
291
+ "R_corF": -7.72e-3,
292
+ "R_corB": -6.50e-3,
293
+ "R_lensF": -8.16e-3,
294
+ "R_lensB": +11.18e-3,
295
+ "D_cor": 0.55e-3,
296
+ "D_ACD": 3.05e-3,
297
+ "D_lens": 0.6896e-3,
298
+ "D_vitr": 19.3203e-3,
299
+ }
300
+ else:
301
+ raise ValueError(f"Model type {model_type} is undefined.")
302
+
303
+ if partial_geometry is not None:
304
+ # Estimate the cornea back curvature if it is unspecified and the cornea
305
+ # front curvature is specified
306
+ if "R_corF" in partial_geometry and "R_corB" not in partial_geometry:
307
+ partial_geometry["R_corB"] = 0.81 * partial_geometry["R_corF"]
308
+
309
+ # Update the geometry with the parameters specified in partial_geometry
310
+ geometry.update(partial_geometry)
311
+
312
+ return geometry
313
+
314
+ def calculate_ray_transfer_matrix(self) -> sp.Matrix:
315
+ """Calculate the eye's ray transfer matrix.
316
+
317
+ Returns
318
+ -------
319
+ sp.Matrix
320
+ Ray transfer matrix of the eye.
321
+ """
322
+
323
+ # Cornea
324
+ cornea = (
325
+ spherical_interface(self.refractive_indices["cor"], 1.0, self.R_corF)
326
+ * uniform_medium(self.D_cor)
327
+ * spherical_interface(
328
+ self.refractive_indices["aq"],
329
+ self.refractive_indices["cor"],
330
+ self.R_corB,
331
+ )
332
+ )
333
+
334
+ # Lens
335
+ lens = (
336
+ spherical_interface(
337
+ self.refractive_indices["lens"],
338
+ self.refractive_indices["aq"],
339
+ self.R_lensF,
340
+ )
341
+ * uniform_medium(self.D_lens)
342
+ * spherical_interface(
343
+ self.refractive_indices["vit"],
344
+ self.refractive_indices["lens"],
345
+ self.R_lensB,
346
+ )
347
+ )
348
+
349
+ # pIOL
350
+ if self.pIOL: # (Diol,tiol,Niol,d_iollens)
351
+ phakic_iol = calculate_piol_matrix(
352
+ self.pIOL[0],
353
+ self.pIOL[1],
354
+ self.pIOL[2],
355
+ self.pIOL[3],
356
+ self.refractive_indices["aq"],
357
+ )
358
+ else: # Identity matrix
359
+ phakic_iol = sp.Matrix([[1, 0], [0, 1]])
360
+
361
+ # Retina to cornea
362
+ return cornea * uniform_medium(self.D_ACD) * phakic_iol * lens * uniform_medium(self.D_vitr)
363
+
364
+ def adjust_lens_back(self, target_refraction: float, *, update_model: bool = False) -> tuple[float, float]:
365
+ """Fit the lens back curvature to the eye's refraction.
366
+
367
+ A corrective lens (glasses) for an eye with `target_refraction` is place in
368
+ front of the eye. The lens back surface is then solved for a focused image on
369
+ the retina. A vertex distance of 1.4 cm between the glasses and the eye is
370
+ assumed.
371
+
372
+ Parameters
373
+ ----------
374
+ target_refraction : float
375
+ Desired spherical of refraction of the eye in diopters.
376
+ update_model : bool
377
+ If `True`, the eye's lens back curvature is set to the calculated value.
378
+
379
+ Returns
380
+ -------
381
+ lens_back_curvature : float
382
+ Radius of curvature of the lens back surface.
383
+ r_glasses : float
384
+ Radius of curvature of the glasses.
385
+ """
386
+ n_glasses = 1.5
387
+ r_glasses = 2.0 * (n_glasses - 1.0) / (target_refraction + 0.0000000001)
388
+
389
+ m_glasses = spherical_interface(n_glasses, 1, -r_glasses) * spherical_interface(1, n_glasses, r_glasses)
390
+
391
+ # Reversed eye with glasses
392
+ # Assuming average vertex distance of 1.4cm
393
+ matrix_glasses_eye = m_glasses * uniform_medium(0.014) * self.ray_transfer_matrix
394
+ temp_geometry = copy.deepcopy(self.geometry)
395
+
396
+ temp_geometry.pop("R_lensB")
397
+
398
+ solve_output = sp.solveset(
399
+ matrix_glasses_eye.evalf(subs=temp_geometry)[1, 1], self.R_lensB
400
+ ) # parallel rays from glasses
401
+
402
+ if len(list(solve_output)) != 1:
403
+ warn(f"Multiple solutions found. {target_refraction=}, {solve_output=}")
404
+
405
+ lens_back_curvature = next(iter(solve_output))
406
+
407
+ if update_model:
408
+ self.geometry["R_lensB"] = lens_back_curvature
409
+
410
+ return lens_back_curvature, r_glasses
411
+
412
+ def calculate_refraction(self, *, display=True) -> tuple[float, float]:
413
+ """Calculate the refraction of the eye.
414
+
415
+ A corrective lens (glasses) for an eye with `target_refraction` is place in
416
+ front of the eye. Its radius of curvature is then solved for a focused image on
417
+ the retina. A vertex distance of 1.4 cm between the glasses and the eye is
418
+ assumed.
419
+
420
+ Parameters
421
+ ----------
422
+ display : bool
423
+ If `True`, the results are printed.
424
+
425
+ Returns
426
+ -------
427
+ glasses_curvature : float
428
+ Radius of curvature of the corrective lens, in meters.
429
+ glasses_power : float
430
+ Power of the corrective lens, in diopters.
431
+
432
+ Warnings
433
+ --------
434
+ When multiple solutions for the curvature are found, a warning is displayed.
435
+ """
436
+ n_glasses = 1.5
437
+ R = sp.symbols("R") # noqa: N806
438
+ glasses = spherical_interface(n_glasses, 1, -R * 10**-3) * spherical_interface(1, n_glasses, R * 10**-3)
439
+ eye = self.evaluate_matrix()
440
+
441
+ glasses_curvature_solutions = sp.solveset((glasses * uniform_medium(0.014) * eye)[1, 1], R)
442
+ if len(list(glasses_curvature_solutions)) != 1:
443
+ warn(f"Multiple solutions found: {glasses_curvature_solutions}")
444
+
445
+ glasses_curvature = next(iter(glasses_curvature_solutions)) * 1e-3
446
+ glasses_power = 2.0 * (n_glasses - 1.0) / (glasses_curvature)
447
+ if display:
448
+ print(f"{glasses_curvature=:.2f} m, {glasses_power=:.2f} D") # noqa: T201
449
+
450
+ return glasses_curvature, glasses_power
451
+
452
+ def evaluate_matrix(self) -> sp.Matrix:
453
+ """Evaluate the eye's ray transfer matrix using its geometrical parameters.
454
+
455
+ Returns
456
+ -------
457
+ sympy.Matrix
458
+ Ray transfer matrix for the eye model.
459
+ """
460
+ return self.ray_transfer_matrix.evalf(subs=self.geometry)
461
+
462
+ def calculate_camera_magnification(
463
+ self, camera: Camera, distance_eye_camera: float = 0.05
464
+ ) -> tuple[float, sp.Matrix, float]:
465
+ """Calculate the total magnification of the eye - camera system.
466
+
467
+ A structure on the central retina is depicted `magnification` times larger
468
+ on the camera sensor.
469
+
470
+ Parameters
471
+ ----------
472
+ camera : Camera
473
+ Camera model.
474
+ distance_eye_camera : float
475
+ Distance between eye and camera in meters, measured from the cornea front to
476
+ the camera lens front.
477
+
478
+ Returns
479
+ -------
480
+ magnification : float
481
+ Central magnification of the eye - camera system.
482
+ focused_system_matrix : sympy.Matrix
483
+ Ray transfer matrix for the full system.
484
+ focus_lens_radius : float
485
+ Radius of curvature of the camera's focal length, in meters.
486
+ """
487
+ eye_matrix = self.evaluate_matrix()
488
+ focused_system_matrix, focus_lens_radius = camera.focused_system_matrix(
489
+ eye_matrix, distance_eye_camera, return_focus_lens_curvature=True
490
+ )
491
+
492
+ magnification = focused_system_matrix[0, 0]
493
+
494
+ return magnification, focused_system_matrix, focus_lens_radius
495
+
496
+
497
+ class Camera:
498
+ def __init__(
499
+ self,
500
+ F_cond: NumberOrSymbol = None, # noqa: N803
501
+ a1: NumberOrSymbol = None,
502
+ camera_type: Literal["default"] = "default",
503
+ ) -> None:
504
+ self.camera_type = "lensTaylor"
505
+ self.F_cond = sp.Symbol("F_cond", real=True)
506
+ self.d_CCD = sp.symbols("d_CCD")
507
+ self.R_foc = sp.Symbol("R_foc", real=True) # in m
508
+ self.a1 = sp.Symbol("a1", real=True)
509
+
510
+ if camera_type == "default":
511
+ self.d_CCD = self.F_cond
512
+ else:
513
+ raise NotImplementedError("Custom CCD distances are not implemented.")
514
+
515
+ self.n_glas = 1.5
516
+ self.a1_value = a1
517
+
518
+ self.condenser_lens = spherical_interface(self.n_glas, 1.0, -self.F_cond) * spherical_interface(
519
+ 1.0, self.n_glas, self.F_cond
520
+ )
521
+ self.focus_lens = spherical_interface(self.n_glas, 1.0, -self.R_foc) * spherical_interface(
522
+ 1.0, self.n_glas, self.R_foc
523
+ )
524
+ self.correction_term = sp.Matrix([[1 + self.a1 / self.R_foc, 0], [0, 1.0 / (1 + self.a1 / self.R_foc)]])
525
+ self.ray_transfer_matrix = (
526
+ self.correction_term * uniform_medium(self.d_CCD) * self.focus_lens * self.condenser_lens
527
+ )
528
+
529
+ self.M_camera_alg = self.ray_transfer_matrix
530
+
531
+ if F_cond and (a1 is not None):
532
+ self.ray_transfer_matrix = self.M_camera_alg.evalf(subs={self.F_cond: F_cond, self.a1: a1})
533
+
534
+ def calculate_focus_lens_radius(self, eye_matrix: sp.Matrix, distance_eye_camera: float = 0.05) -> float:
535
+ """Calculate the radius of curvature of the focus lens.
536
+
537
+ Solves for the radius of curvature of the camera's focus lens, so that the image
538
+ of an object on the retina is in focus.
539
+
540
+ Parameters
541
+ ----------
542
+ eye_matrix : sympy.Matrix
543
+ Ray transfer matrix of the eye model.
544
+ distance_eye_camera : float
545
+ Distance between the eye and camera in meters, measured from the cornea
546
+ front to the camera lens front. Defaults to 0.05 m.
547
+
548
+ Returns
549
+ -------
550
+ float
551
+ Radius of curvature of the focus lens, in meters.
552
+ """
553
+ system_matrix = self.ray_transfer_matrix * uniform_medium(distance_eye_camera) * eye_matrix
554
+
555
+ # Ror contact cameras such as the Panoret fundus camera, the refractive index (of air) needs to be changed to
556
+ # that of the medium used between the eye and camera. This can be done by changing the use of uniform_medium()
557
+ # to medium_change().
558
+ # The camera should correct for the patients refraction, so the image should be
559
+ # focused, i.e. B = 0
560
+ B = system_matrix[0, 1] + 0.0000000001 # noqa: N806
561
+
562
+ if self.a1 in B.free_symbols:
563
+ possible_curvatures = list(sp.solve(B.evalf(subs={self.a1: 0}), self.R_foc))
564
+ else:
565
+ possible_curvatures = list(sp.solve(B, self.R_foc))
566
+
567
+ return np.max(np.abs(possible_curvatures))
568
+
569
+ def focused_system_matrix(
570
+ self,
571
+ eye_matrix: sp.Matrix,
572
+ distance_eye_camera: float = 0.05,
573
+ *,
574
+ return_focus_lens_curvature: bool = False,
575
+ ) -> sp.Matrix | tuple[sp.Matrix, float]:
576
+ system_matrix = self.ray_transfer_matrix * uniform_medium(distance_eye_camera) * eye_matrix
577
+
578
+ # For contact cameras such as the Panoret fundus camera, the refractive index (of air) needs to be changed to
579
+ # that of the medium use dbetween the eye and camera. This can be done by changing the use of uniform_medium()
580
+ # to medium_change().
581
+ focus_lens_curvature = self.calculate_focus_lens_radius(eye_matrix, distance_eye_camera)
582
+ if return_focus_lens_curvature:
583
+ return (
584
+ system_matrix.evalf(subs={self.R_foc: focus_lens_curvature}),
585
+ focus_lens_curvature,
586
+ )
587
+
588
+ return system_matrix.evalf(subs={self.R_foc: focus_lens_curvature})
589
+
590
+
591
+ # Maximum allowed difference between the calculated and specified refraction of the eye model
592
+ _MAXIMUM_REFRACTION_DEVIATION = 0.05
593
+
594
+ # Maximum allowed value of the B-element in a ray transfer matrix for a focused system
595
+ _MAXIMUM_FOCUS_DEVIATION = 0.0001
596
+
597
+
598
+ def calculate_magnification(
599
+ eye: Eye,
600
+ camera: Camera,
601
+ distance_eye_camera: float = 0.05,
602
+ focus_lens_radius: float | None = None,
603
+ *,
604
+ suppress_warnings: bool = False,
605
+ ) -> tuple[float, float, Eye]:
606
+ """Calculate the total magnification of the eye - camera system.
607
+
608
+ A structure on the central retina is depicted `magnification` times larger
609
+ on the camera sensor.
610
+
611
+ Parameters
612
+ ----------
613
+ eye : Eye
614
+ Eye model.
615
+ camera : Camera
616
+ Camera model.
617
+ distance_eye_camera : float
618
+ Distance between eye and camera in meters, measured from the cornea front to
619
+ the camera lens front.
620
+ focus_lens_radius : float
621
+ Optional radius of curvature of the focus lens. If not specified, the focus lens
622
+ curvature is determined using `Camera.calculate_focus_lens_radius`.
623
+ suppress_warnings : bool
624
+ If `True`, no warning is issued if the calculated glasses power differs
625
+ significantly from `eye.spherical_equivalent`.
626
+
627
+ Returns
628
+ -------
629
+ magnification : float
630
+ Central magnification of the eye - camera system.
631
+ glasses_power : float
632
+ Power of the corrective lens required to correct for the eye's refraction, in
633
+ diopters.
634
+ eye_model : Eye
635
+ Eye model used to calculate the magnification.
636
+
637
+ Warnings
638
+ --------
639
+ When the calculated glasses power differs significantly from
640
+ `eye.spherical_equivalent`, a warning is displayed.
641
+ """
642
+ glasses_curvature, glasses_power = eye.calculate_refraction(display=False)
643
+
644
+ if not suppress_warnings and abs(glasses_power - eye.spherical_equivalent) > _MAXIMUM_REFRACTION_DEVIATION:
645
+ warn(
646
+ f"model refraction {glasses_power:.2f} not matching clinical refraction"
647
+ f" {eye.spherical_equivalent:.2f} for {eye.name}"
648
+ )
649
+
650
+ # For contact cameras such as the Panoret fundus camera, this refractive index (of air) needs to be changed to that
651
+ # of the medium between the eye and camera. This can be done by changing the use of uniform_medium() to
652
+ # medium_change().
653
+ system_matrix = camera.ray_transfer_matrix * uniform_medium(distance_eye_camera) * eye.evaluate_matrix()
654
+
655
+ if focus_lens_radius is None:
656
+ # system is in focus so B=0
657
+ solutions = list(sp.solve(system_matrix[0, 1] + 0.00001, camera.R_foc))
658
+ focus_lens_radius = solutions[np.argmax(np.abs(np.array(solutions) + camera.a1_value))]
659
+
660
+ focused_system_matrix = system_matrix.evalf(subs=({camera.R_foc: focus_lens_radius}))
661
+
662
+ magnification: float = focused_system_matrix[0, 0]
663
+
664
+ if abs(focused_system_matrix[0, 1]) > _MAXIMUM_FOCUS_DEVIATION:
665
+ warn(f"focused_system_matrix not in focus for patient {eye.name}")
666
+
667
+ return magnification, glasses_power, eye
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.1
2
+ Name: PAROS
3
+ Version: 0.1.0
4
+ Summary: PARaxial Optical fundus Scaling (PAROS)
5
+ Author-email: "Lennart J. Pors" <l.j.pors@lumc.nl>, "Jan-Willem M. Beenakker" <j.w.m.beenakker@lumc.nl>, Corné Haasjes <c.haasjes@lumc.nl>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 Lennart Pors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Keywords: ophthalmology,paraxial optics
29
+ Classifier: Development Status :: 5 - Production/Stable
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Topic :: Scientific/Engineering :: Physics
32
+ Classifier: Typing :: Typed
33
+ Requires-Python: >=3.9
34
+ Requires-Dist: matplotlib
35
+ Requires-Dist: numpy
36
+ Requires-Dist: pandas
37
+ Requires-Dist: scipy
38
+ Requires-Dist: sympy
39
+ Description-Content-Type: text/markdown
40
+
41
+ # PARaxial Optical fundus Scaling (PAROS)
42
+
43
+ Paros is a method to calculate the magnification of fundus images based on the optical characteristics of the patient's eye. The full method and validation are described in (article reference to be added upon acceptance)
44
+
45
+ ## License
46
+
47
+ The code is provided as is, without any warranty, under the [MIT license](LICENSE). This license requires that reusers give credit to the creator. It allows reusers to distribute, remix, adapt, and build upon the material in any medium or format.
48
+
49
+ ## Basic usage
50
+
51
+ A basic version of PAROS can most easily be run online here: [Basic version](https://mreye-lumc.github.io/PAROS)
52
+
53
+ The full package is also available for download via PiP:
54
+
55
+ ```
56
+ pip install PAROS
57
+ ```
58
+
59
+ PAROS consists of two main functions:
60
+
61
+ - ‘fundus_camera_calibration.ipynb’: a method to calibrate camera using eye phantom measurements.
62
+ - ‘calculate_ocular_magnification.ipynb’: a method to calculate magnification fundus images of a subject.
63
+
64
+ ## Implementation
65
+
66
+ The implementation of PAROS in this repository is functional for the camera in our center, and with the specific software used at our center. Both have impact on the calculated magnification. We therefore recommend calibration of the camera and software using the method described in the article mentioned below before implementation for quantitative purposes.
67
+
68
+ ## Camera constants
69
+
70
+ Please find here a table of known camera calibration constants; this can be added upon by other contributors.
71
+ | Camera type | CCD type | Condenser lens power (D) | First order calibration term |
72
+ | --------------- | -------- | ------------------------ | ---------------------------- |
73
+ | Topcon TRC-50DX | | 38.0 | -0.011 |
74
+ | Topcon TRC-50IX | | 38.0 | -0.012 |
75
+
76
+ ## Referencing
77
+
78
+ When publishing results obtained with this package, please cite the paper that describes the full method and validation: Pors, L.J., et al. (2023). "Correction method for optical scaling of fundoscopy images: development, validation and first implementation" in press.
79
+
80
+ ## Contributing
81
+
82
+ Please read our [contribution guidelines](CONTRIBUTING.md) prior to opening a Pull Request.
83
+
84
+ ## Contact
85
+
86
+ Feel free to contact us for any inquiries:
87
+
88
+ - L.J. Pors ([email](mailto:l.j.pors@lumc.nl))
89
+ - J.W.M. Beenakker ([email](mailto:j.w.m.beenakker@lumc.nl))
@@ -0,0 +1,13 @@
1
+ PAROS/__init__.py,sha256=tDPXVwi1l7LKfd6ZxyuDcLCzs_mn64fEye2gIv1jQO4,386
2
+ PAROS/eyephantom.py,sha256=YpsKcKeKWi6bvRRQdTQK7cE-VqiMXo6nKL0_AqSSFy8,6836
3
+ PAROS/fundscale.py,sha256=ovYS3bzCcPuYyVyJJi6VGIOxKCr3uelOo6rOmng1zjM,23340
4
+ PAROS/data/__init__.py,sha256=k2WEcTh10TvSqee1H5xmgpW8RnP91gKCtfXhuCVT0_s,1982
5
+ PAROS/data/calibration/__init__.py,sha256=nHbgxHMZ76tUASGtaq18JeTs2TF9ATZHP118ppJAQ4A,37
6
+ PAROS/data/calibration/Topcon/TRC_50DX.csv,sha256=mfCCaJqjb_CNrlWIl3l6s1Vb6j-QtJenOcRpYQPSc2c,1016
7
+ PAROS/data/calibration/Topcon/TRC_50IX.csv,sha256=-GjlzBEufJVoQ2TekElZJKOwnOAfPAxm5MGB8J1Mu4o,1041
8
+ PAROS/data/lenses/__init__.py,sha256=D9Afgofn4S5yRS_2IsMVsvgXDP4snT9ttpkEdITxJdw,23
9
+ PAROS/data/lenses/phantom.csv,sha256=XpOnnU0gQiWO6ur_CMA3GaisAt7Y2hdRGt9vOLT1zjY,1000
10
+ paros-0.1.0.dist-info/METADATA,sha256=2PbZhXIgUxQbNluL_YRWnhjijUV50X4i1VGvm1Q4Dvc,4327
11
+ paros-0.1.0.dist-info/WHEEL,sha256=mRYSEL3Ih6g5a_CVMIcwiF__0Ae4_gLYh01YFNwiq1k,87
12
+ paros-0.1.0.dist-info/licenses/LICENSE,sha256=bdHn5SWJdOrfnRkSGDabdWMbgvwLsv2I60Ygzta2ySA,1069
13
+ paros-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.21.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Lennart Pors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.