Skip to content

Commit f524100

Browse files
authored
Fixed python 3.8/3.9 support (#2)
* some typing thing * trying to fix for python 3.8
1 parent 5d7b839 commit f524100

6 files changed

Lines changed: 110 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ jobs:
2525
pip install -e .[dev]
2626
- name: Run tests
2727
run: |
28-
pytest
28+
pytest --cov=src/aobasis --cov-fail-under=90

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ requires-python = ">=3.8"
2424
[project.optional-dependencies]
2525
dev = [
2626
"pytest",
27+
"pytest-cov",
2728
"imageio",
2829
]
2930

src/aobasis/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from abc import ABC, abstractmethod
22
import numpy as np
33
from pathlib import Path
4-
from typing import Tuple, Optional
4+
from typing import Tuple, Optional, Union
55
from .utils import plot_basis_modes
66

77
class BasisGenerator(ABC):
@@ -31,7 +31,7 @@ def generate(self, n_modes: int, **kwargs) -> np.ndarray:
3131
"""
3232
pass
3333

34-
def save(self, filepath: str | Path) -> None:
34+
def save(self, filepath: Union[str, Path]) -> None:
3535
"""
3636
Save the generated basis and actuator positions to a .npz file.
3737
"""
@@ -46,7 +46,7 @@ def save(self, filepath: str | Path) -> None:
4646
)
4747

4848
@classmethod
49-
def load(cls, filepath: str | Path) -> 'BasisGenerator':
49+
def load(cls, filepath: Union[str, Path]) -> 'BasisGenerator':
5050
"""
5151
Load a basis from a .npz file.
5252
Note: This returns a generic container or re-instantiates the specific class if possible.
@@ -62,7 +62,7 @@ def load(cls, filepath: str | Path) -> 'BasisGenerator':
6262
instance.modes = modes
6363
return instance
6464

65-
def plot(self, count: int = 6, outfile: Optional[str | Path] = None, **kwargs):
65+
def plot(self, count: int = 6, outfile: Optional[Union[str, Path]] = None, **kwargs):
6666
"""Plot the generated modes."""
6767
if self.modes is None:
6868
raise ValueError("No modes to plot.")

src/aobasis/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import numpy as np
22
import matplotlib.pyplot as plt
33
from pathlib import Path
4+
from typing import Union
45
import math
56
from scipy.interpolate import griddata
67

@@ -20,7 +21,7 @@ def plot_basis_modes(
2021
modes: np.ndarray,
2122
positions: np.ndarray,
2223
count: int = 6,
23-
outfile: Path | str | None = None,
24+
outfile: Union[Path, str, None] = None,
2425
cmap: str = "coolwarm",
2526
title_prefix: str = "Mode",
2627
interpolate: bool = False,

src/aobasis/zernike.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import numpy as np
22
import math
3+
from typing import Tuple
34
from .base import BasisGenerator
45

56
class ZernikeBasisGenerator(BasisGenerator):
@@ -62,7 +63,7 @@ def generate(self, n_modes: int, ignore_piston: bool = False, **kwargs) -> np.nd
6263
self.modes = np.column_stack(modes_list)
6364
return self.modes
6465

65-
def _noll_to_nm(self, j: int) -> tuple[int, int]:
66+
def _noll_to_nm(self, j: int) -> Tuple[int, int]:
6667
"""
6768
Convert Noll index j to radial order n and azimuthal frequency m.
6869
Based on Noll, J. Opt. Soc. Am. 66, 207 (1976).

tests/test_utils.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import numpy as np
2+
import pytest
3+
from pathlib import Path
4+
from unittest.mock import patch, MagicMock
5+
from aobasis.utils import make_circular_actuator_grid, make_concentric_actuator_grid, plot_basis_modes
6+
7+
def test_make_circular_actuator_grid():
8+
diameter = 10.0
9+
grid_size = 10
10+
positions = make_circular_actuator_grid(diameter, grid_size)
11+
12+
assert isinstance(positions, np.ndarray)
13+
assert positions.shape[1] == 2
14+
15+
# Check that all points are within the radius
16+
radius = diameter / 2
17+
distances = np.linalg.norm(positions, axis=1)
18+
assert np.all(distances <= radius * 1.0000001)
19+
20+
def test_make_concentric_actuator_grid():
21+
diameter = 10.0
22+
n_rings = 3
23+
n_points_innermost = 6
24+
positions = make_concentric_actuator_grid(diameter, n_rings, n_points_innermost)
25+
26+
assert isinstance(positions, np.ndarray)
27+
assert positions.shape[1] == 2
28+
29+
# Expected number of points: 1 (center) + 6*1 + 6*2 + 6*3 = 1 + 6 + 12 + 18 = 37
30+
expected_points = 1 + sum(n_points_innermost * i for i in range(1, n_rings + 1))
31+
assert positions.shape[0] == expected_points
32+
33+
@patch("aobasis.utils.plt")
34+
def test_plot_basis_modes(mock_plt):
35+
# Setup mock data
36+
n_actuators = 20
37+
n_modes = 5
38+
positions = np.random.rand(n_actuators, 2)
39+
modes = np.random.rand(n_actuators, n_modes)
40+
41+
# Configure mock to return a tuple
42+
mock_fig = MagicMock()
43+
mock_axes = MagicMock()
44+
mock_plt.subplots.return_value = (mock_fig, mock_axes)
45+
46+
# Test basic plotting
47+
plot_basis_modes(modes, positions, count=3)
48+
49+
assert mock_plt.subplots.called
50+
assert mock_plt.show.called
51+
52+
@patch("aobasis.utils.plt")
53+
def test_plot_basis_modes_save(mock_plt, tmp_path):
54+
# Setup mock data
55+
n_actuators = 20
56+
n_modes = 5
57+
positions = np.random.rand(n_actuators, 2)
58+
modes = np.random.rand(n_actuators, n_modes)
59+
outfile = tmp_path / "test_plot.png"
60+
61+
# Configure mock to return a tuple
62+
mock_fig = MagicMock()
63+
mock_axes = MagicMock()
64+
mock_plt.subplots.return_value = (mock_fig, mock_axes)
65+
66+
# Test saving to file
67+
plot_basis_modes(modes, positions, count=3, outfile=outfile)
68+
69+
assert mock_plt.subplots.called
70+
mock_plt.savefig.assert_called_with(outfile, dpi=150)
71+
assert mock_plt.close.called
72+
73+
@patch("aobasis.utils.plt")
74+
def test_plot_basis_modes_interpolate(mock_plt):
75+
# Setup mock data
76+
n_actuators = 20
77+
n_modes = 5
78+
positions = np.random.rand(n_actuators, 2)
79+
modes = np.random.rand(n_actuators, n_modes)
80+
81+
# Configure mock to return a tuple
82+
mock_fig = MagicMock()
83+
mock_axes = MagicMock()
84+
mock_plt.subplots.return_value = (mock_fig, mock_axes)
85+
86+
# Test interpolation
87+
plot_basis_modes(modes, positions, count=3, interpolate=True)
88+
89+
assert mock_plt.subplots.called
90+
# We can't easily check if imshow was called on the axes objects without more complex mocking,
91+
# but we can check that no errors were raised.
92+
93+
def test_plot_basis_modes_invalid_shape():
94+
n_actuators = 20
95+
n_modes = 5
96+
positions = np.random.rand(n_actuators, 2)
97+
modes = np.random.rand(n_actuators + 1, n_modes) # Mismatch
98+
99+
with pytest.raises(ValueError, match="Mode dimension 0"):
100+
plot_basis_modes(modes, positions)

0 commit comments

Comments
 (0)