Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add static typing with Python type annotations #35

Merged
merged 6 commits into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
@@ -1,35 +1,41 @@
name: Python Tests

on:
pull_request:
branches: [ master ]

jobs:
test:
test-and-typecheck:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4

- name: Set up Miniconda
uses: conda-incubator/setup-miniconda@v3
with:
auto-update-conda: true
python-version: ${{ matrix.python-version }}

- name: Set up Miniconda
uses: conda-incubator/setup-miniconda@v3
with:
auto-update-conda: true
python-version: ${{ matrix.python-version }}
- name: Install dependencies
shell: bash -l {0}
run: |
conda create -n tracepyci python=${{ matrix.python-version }} --yes
conda activate tracepyci
conda install --yes numpy scipy matplotlib scikit-learn pandas pytest pytest-cov
pip install mypy types-PyYAML pandas-stubs
pip install .

- name: Install dependencies
shell: bash -l {0}
run: |
conda create -n tracepyci python=${{ matrix.python-version }} --yes
conda activate tracepyci
conda install --yes numpy scipy matplotlib scikit-learn pandas pytest pytest-cov
pip install .
- name: Run tests
shell: bash -l {0}
run: |
conda activate tracepyci
pytest tests/

- name: Run tests
shell: bash -l {0}
run: |
conda activate tracepyci
pytest tests/
- name: Run type checking
shell: bash -l {0}
run: |
conda activate tracepyci
mypy tracepy/ --ignore-missing-imports
2 changes: 1 addition & 1 deletion tests/test_hyperbolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@
def test_rms_hyperbolic():
geo = [back_lens, lens, stop]
ray_group = tp.ray_plane(geo, [0., 0., 0.], 1.1, d=[0.,0.,1.], nrays=100)
rms = tp.spotdiagram(geo, ray_group, optimizer=True)
rms = tp.spot_rms(geo, ray_group)
assert rms == 0.

2 changes: 1 addition & 1 deletion tests/test_parabolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
def test_rms_parabolic():
geo = [mirror, stop]
ray_group = tp.ray_plane(geo, [0., 0., -1.5], 1.1, d=[0.,0.,1.], nrays=100)
rms = tp.spotdiagram(geo, ray_group, optimizer=True)
rms = tp.spot_rms(geo, ray_group)
assert rms == 0.
10 changes: 5 additions & 5 deletions tracepy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from .geometry import geometry
from .optimize import optimize
from .geoplot import plotxz, plotyz, plot2d
from .optplot import spotdiagram, plotobject, rayaberration
from .iotables import *
from .transforms import *
from .raygroup import *
from.index import *
from .optplot import spotdiagram, plotobject, rayaberration, spot_rms
from .iotables import save_optics
from .raygroup import ray_plane
from .index import cauchy_two_term, glass_index
from .utils import gen_rot
12 changes: 12 additions & 0 deletions tracepy/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Constants used by optimizer
SURV_CONST = 100 # Weight of failed propagation.
MAX_RMS = 999 # Maximum RMS penalty for trace error.

# Constants used for ray objects
MAX_INTERSECTION_ITERATIONS = 1e4 # Max iter before failed intersection search.
MAX_REFRACTION_ITERATIONS = 1e5 # Max iter before failed refraction.
INTERSECTION_CONVERGENCE_TOLERANCE = 1e-6 # Tolerance for intersection search.
REFRACTION_CONVERGENCE_TOLERANCE = 1e-15 # Tolerance for refraction.

# Constants used for plotting
PLOT_ROUNDING_ACC = 14 # Rounding accuracy for spot diagrams and plots.
3 changes: 3 additions & 0 deletions tracepy/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ class NotOnSurfaceError(Exception):

class TraceError(Exception):
""" Custom error for lens systems where no rays survive being traced. """

class InvalidGeometry(Exception):
""" Invalid parameters were given to define a geometry object. """
81 changes: 41 additions & 40 deletions tracepy/geometry.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import numpy as np

from .transforms import gen_rot
from .exceptions import NotOnSurfaceError
from .utils import gen_rot
from .exceptions import NotOnSurfaceError, InvalidGeometry
from .index import glass_index

from typing import Dict, List, Tuple, Union, Optional

class geometry:
"""Class for the different surfaces in an optical system.

Expand Down Expand Up @@ -39,55 +41,48 @@ class geometry:
If c is 0 then the surface is planar.
name (optional): str
Name of the surface, used for optimization
R (generated): np.matrix((3,3))
R (generated): np.array((3,3))
Rotation matrix for the surface from rotation angles D.

"""

def __init__(self, params):
self.P = params['P']
self.D = np.array(params.get('D', [0., 0., 0.]))
self.action = params['action']
self.Diam = params['Diam']
self.N = params.get('N', 1.)
self.kappa = params.get('kappa', None)
self.diam = params.get('diam', 0.)
self.c = params.get('c', 0.)
self.name = params.get('name', None)
self.R = gen_rot(self.D)
def __init__(self, params: Dict):
P = params.get('P')
# Allow on axis integer for P.
if isinstance(P, float) or isinstance(P, int):
P = np.array([0., 0., P])
elif isinstance(P, List):
P = np.array(P)
else:
raise InvalidGeometry()
self.P: np.ndarray = P
self.D: np.ndarray = np.array(params.get('D', [0., 0., 0.]))
self.action: str = params['action']
self.Diam: Union[float, int] = params['Diam']
self.N: Union[float, int] = params.get('N', 1.)
self.kappa: Optional[Union[float, int]] = params.get('kappa')
self.diam: Union[float, int] = params.get('diam', 0.)
self.c: Union[float, int] = params.get('c', 0.)
self.name: str = params.get('name', None)
self.R: np.ndarray = gen_rot(self.D)
if params.get('glass'):
self.glass = glass_index(params.get('glass'))
self.check_params()

def __getitem__(self, item):
""" Return attribute of geometry. """
return getattr(self, item)

def __setitem__(self, item, value):
""" Set attribute of geometry. """
return setattr(self, item, value)

def check_params(self):
def check_params(self) -> None:
"""Check that required parameters are given and update needed parameters.

Summary
-------
If P is given as a float/int then it is converted to a np array
with that float/int in the Z direction. If c != 0 (in the case
of a conic) then kappa must be specified, and if kappa is greater
than 0 then the value of c is redundant by boundary conditions of
the conic equation. Lastly, if c == 0 in the case of a planar
surface the None value of kappa needs to be set to a dummy value
to avoid exceptions in calculating the conic equation. Note that
this does not affect the calculation since c is 0.
If c != 0 (in the case of a conic) then kappa must be specified,
and if kappa is greater than 0 then the value of c is redundant
by boundary conditions of the conic equation. Lastly, if c == 0 in
the case of a planar surface the None value of kappa needs to be set
to a dummy value to avoid exceptions in calculating the conic equation.
Note that this does not affect the calculation since c is 0.

"""

if isinstance(self.P, float) or isinstance(self.P, int):
#Allow on axis integer for P.
self.P = np.array([0., 0., self.P])
else:
self.P = np.array(self.P)
if self.c != 0:
if self.kappa is None:
raise Exception("Specify a kappa for this conic.")
Expand All @@ -98,15 +93,15 @@ def check_params(self):
#Used for planes, does not affect calculations.
self.kappa = 1.

def get_surface(self, point):
def get_surface(self, point: np.ndarray) -> Tuple[float, List[float]]:
""" Returns the function and derivitive of a surface for a point. """
return self.conics(point)

def get_surface_plot(self, points):
def get_surface_plot(self, points: np.ndarray) -> np.ndarray:
""" Returns the function value for an array of points. """
return self.conics_plot(points)

def conics(self, point):
def conics(self, point: np.ndarray) -> Tuple[float, List[float]]:
"""Returns function value and derivitive list for conics and sphere surfaces.

Note
Expand Down Expand Up @@ -137,14 +132,17 @@ def conics(self, point):
rho = np.sqrt(pow(X,2) + pow(Y, 2))
if rho > self.Diam/2. or rho < self.diam/2.:
raise NotOnSurfaceError()
# Ensure kappa is not None before using it in calculations
if self.kappa is None:
raise ValueError("kappa must not be None for conic calculations")
#Conic equation.
function = Z - self.c*pow(rho, 2)/(1 + pow((1-self.kappa*pow(self.c, 2)*pow(rho,2)), 0.5))
#See Spencer, Murty section on rotational surfaces for definition of E.
E = self.c / pow((1-self.kappa*pow(self.c, 2)*pow(rho,2)), 0.5)
derivitive = [-X*E, -Y*E, 1.]
return function, derivitive

def conics_plot(self, point):
def conics_plot(self, point: np.ndarray) -> np.ndarray:
"""Returns Z values for an array of points for plotting conics.

Parameters
Expand All @@ -166,5 +164,8 @@ def conics_plot(self, point):
nan_idx = (rho > self.Diam/2.) + (rho < self.diam/2.)
rho = np.sqrt(pow(X[~nan_idx],2) + pow(Y[~nan_idx], 2))
function[nan_idx] = np.nan
# Ensure kappa is not None before using it in calculations
if self.kappa is None:
raise ValueError("kappa must not be None for conic plot calculations")
function[~nan_idx] = self.c*pow(rho, 2)/(1 + pow((1-self.kappa*pow(self.c, 2)*pow(rho,2)), 0.5))
return function
Loading