Skip to content

Commit 5d7b839

Browse files
committed
Adding code to the repo
1 parent 480d2f3 commit 5d7b839

15 files changed

Lines changed: 1456 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.8","3.9", "3.10", "3.11", "3.12"]
15+
16+
steps:
17+
- uses: actions/checkout@v3
18+
- name: Set up Python ${{ matrix.python-version }}
19+
uses: actions/setup-python@v4
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
- name: Install dependencies
23+
run: |
24+
python -m pip install --upgrade pip
25+
pip install -e .[dev]
26+
- name: Run tests
27+
run: |
28+
pytest

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
__pycache__/
2+
*.pyc
3+
*.egg-info/
4+
dist/
5+
build/
6+
.pytest_cache/
7+
.DS_Store
8+
*.npz
9+
*.png
10+
*.gif

README.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# AO Basis (aobasis)
2+
3+
A Python package for generating various modal basis sets for Adaptive Optics (AO) systems. This tool allows you to easily create, visualize, and save basis sets for any deformable mirror geometry.
4+
5+
## Features
6+
7+
- **Karhunen-Loève (KL) Modes**: Optimized for atmospheric turbulence (Von Kármán spectrum).
8+
- **Zernike Polynomials**: Standard optical aberration modes (Noll indexing).
9+
- **Fourier Modes**: Sinusoidal basis sets.
10+
- **Zonal Basis**: Single actuator pokes (Identity).
11+
- **Hadamard Basis**: Orthogonal binary patterns for calibration.
12+
- **Flexible Geometry**: Works with arbitrary actuator positions (defaulting to circular grids).
13+
- **Piston Removal**: Option to exclude piston/DC modes from generation.
14+
- **Visualization**: Built-in plotting tools for quick inspection.
15+
- **Serialization**: Save and load basis sets to/from `.npz` files.
16+
17+
## Installation
18+
19+
### Prerequisites
20+
- Python 3.8 or higher
21+
22+
### Install from Source
23+
Clone the repository and install using pip:
24+
25+
```bash
26+
git clone https://github.com/jacotay7/aobasis.git
27+
cd aobasis
28+
pip install .
29+
```
30+
31+
For development (editable install with test dependencies):
32+
```bash
33+
pip install -e ".[dev]"
34+
```
35+
36+
## Quick Start
37+
38+
Here is a simple example of generating and plotting KL modes for a 10-meter telescope:
39+
40+
```python
41+
from aobasis import KLBasisGenerator, make_circular_actuator_grid
42+
43+
# 1. Define the actuator geometry
44+
positions = make_circular_actuator_grid(telescope_diameter=10.0, grid_size=20)
45+
46+
# 2. Initialize the generator
47+
kl_gen = KLBasisGenerator(positions, fried_parameter=0.16, outer_scale=30.0)
48+
49+
# 3. Generate modes (excluding piston)
50+
modes = kl_gen.generate(n_modes=50, ignore_piston=True)
51+
52+
# 4. Plot the first 6 modes
53+
kl_gen.plot(count=6, title_prefix="KL Mode")
54+
55+
# 5. Save to disk
56+
kl_gen.save("my_kl_basis.npz")
57+
```
58+
59+
## Performance
60+
61+
Generation times for 100 modes on a standard laptop (M1/M2 class):
62+
63+
| Basis | 16x16 Grid (~170 acts) | 32x32 Grid (~740 acts) | 64x64 Grid (~3100 acts) |
64+
|-------|------------------------|------------------------|-------------------------|
65+
| **KL** | 0.01s | 0.29s | 5.60s |
66+
| **Zernike** | 0.001s | 0.002s | 0.02s |
67+
| **Fourier** | 0.001s | 0.001s | 0.003s |
68+
| **Zonal** | <0.001s | <0.001s | 0.005s |
69+
| **Hadamard** | <0.001s | 0.004s | 0.09s |
70+
71+
*Note: KL basis generation is computationally intensive ($O(N^3)$) due to the dense covariance matrix diagonalization.*
72+
73+
## Tutorials
74+
75+
We provide Jupyter notebooks to help you get started.
76+
77+
1. **Getting Started**: `tutorials/getting_started.ipynb` covers all supported basis types and features.
78+
79+
To run the tutorials:
80+
```bash
81+
# Install Jupyter if you haven't already
82+
pip install jupyter
83+
84+
# Launch the notebook server
85+
jupyter notebook tutorials/getting_started.ipynb
86+
```
87+
88+
## Development & Testing
89+
90+
This project uses `pytest` for testing. To run the test suite:
91+
92+
```bash
93+
# Install dev dependencies
94+
pip install -e ".[dev]"
95+
96+
# Run tests
97+
pytest
98+
```
99+
100+
## Contributing
101+
102+
Contributions are welcome! Please feel free to submit a Pull Request.
103+
104+
1. Fork the repository.
105+
2. Create your feature branch (`git checkout -b feature/AmazingFeature`).
106+
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`).
107+
4. Push to the branch (`git push origin feature/AmazingFeature`).
108+
5. Open a Pull Request.
109+
110+
## Issues
111+
112+
If you encounter any bugs or have feature requests, please file an issue on the [GitHub Issues](https://github.com/jacotay7/aobasis/issues) page.
113+
114+
## Contact
115+
116+
For questions or support, please contact:
117+
118+
**User Name**
119+
Email: jtaylor@keck.hawaii.edu
120+
121+
## License
122+
123+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

benchmark.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import time
2+
import numpy as np
3+
from aobasis import (
4+
KLBasisGenerator,
5+
ZernikeBasisGenerator,
6+
FourierBasisGenerator,
7+
ZonalBasisGenerator,
8+
HadamardBasisGenerator,
9+
make_circular_actuator_grid
10+
)
11+
12+
def benchmark():
13+
# Grid sizes to test
14+
grid_sizes = [16, 32, 64]
15+
16+
generators = [
17+
("KL", KLBasisGenerator, {"fried_parameter": 0.16, "outer_scale": 30.0}),
18+
("Zernike", ZernikeBasisGenerator, {"pupil_radius": 5.0}),
19+
("Fourier", FourierBasisGenerator, {"pupil_diameter": 10.0}),
20+
("Zonal", ZonalBasisGenerator, {}),
21+
("Hadamard", HadamardBasisGenerator, {})
22+
]
23+
24+
print(f"Benchmarking Basis Generation (n_modes=n_acts)")
25+
print("=" * 80)
26+
print(f"{'Basis':<10} | {'Grid Size':<10} | {'Actuators':<10} | {'Time (s)':<10}")
27+
print("-" * 80)
28+
29+
results = {}
30+
31+
for name, GenClass, kwargs in generators:
32+
results[name] = []
33+
for size in grid_sizes:
34+
# Setup
35+
positions = make_circular_actuator_grid(10.0, size)
36+
n_act = positions.shape[0]
37+
38+
# Update kwargs based on size if needed (e.g. pupil radius is constant)
39+
gen = GenClass(positions, **kwargs)
40+
41+
# Timing
42+
start_time = time.perf_counter()
43+
gen.generate(n_modes=n_act)
44+
end_time = time.perf_counter()
45+
46+
duration = end_time - start_time
47+
print(f"{name:<10} | {size:<10} | {n_act:<10} | {duration:<10.4f}")
48+
results[name].append((size, n_act, duration))
49+
print("-" * 80)
50+
51+
if __name__ == "__main__":
52+
benchmark()

pyproject.toml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[build-system]
2+
requires = ["setuptools>=61.0"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "aobasis"
7+
version = "0.1.0"
8+
description = "A package for generating AO basis sets (KL, Zernike, Fourier)"
9+
readme = "README.md"
10+
authors = [{ name = "User", email = "user@example.com" }]
11+
license = { file = "LICENSE" }
12+
classifiers = [
13+
"Programming Language :: Python :: 3",
14+
"License :: OSI Approved :: MIT License",
15+
"Operating System :: OS Independent",
16+
]
17+
dependencies = [
18+
"numpy>=1.20",
19+
"scipy>=1.7",
20+
"matplotlib>=3.5",
21+
]
22+
requires-python = ">=3.8"
23+
24+
[project.optional-dependencies]
25+
dev = [
26+
"pytest",
27+
"imageio",
28+
]
29+
30+
[project.urls]
31+
"Homepage" = "https://github.com/example/aobasis"
32+
33+
[tool.setuptools.packages.find]
34+
where = ["src"]

src/aobasis/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from .base import BasisGenerator
2+
from .kl import KLBasisGenerator
3+
from .zernike import ZernikeBasisGenerator
4+
from .fourier import FourierBasisGenerator
5+
from .zonal import ZonalBasisGenerator
6+
from .hadamard import HadamardBasisGenerator
7+
from .utils import make_circular_actuator_grid, make_concentric_actuator_grid, plot_basis_modes
8+
9+
__all__ = [
10+
"BasisGenerator",
11+
"KLBasisGenerator",
12+
"ZernikeBasisGenerator",
13+
"FourierBasisGenerator",
14+
"ZonalBasisGenerator",
15+
"HadamardBasisGenerator",
16+
"make_circular_actuator_grid",
17+
"make_concentric_actuator_grid",
18+
"plot_basis_modes",
19+
]

src/aobasis/base.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from abc import ABC, abstractmethod
2+
import numpy as np
3+
from pathlib import Path
4+
from typing import Tuple, Optional
5+
from .utils import plot_basis_modes
6+
7+
class BasisGenerator(ABC):
8+
"""
9+
Abstract base class for AO basis generators.
10+
"""
11+
12+
def __init__(self, positions: np.ndarray):
13+
"""
14+
Args:
15+
positions: (N, 2) array of actuator coordinates (x, y) in meters.
16+
"""
17+
self.positions = np.array(positions)
18+
self.n_actuators = self.positions.shape[0]
19+
self.modes: Optional[np.ndarray] = None
20+
21+
@abstractmethod
22+
def generate(self, n_modes: int, **kwargs) -> np.ndarray:
23+
"""
24+
Generate the basis modes.
25+
26+
Args:
27+
n_modes: Number of modes to generate.
28+
29+
Returns:
30+
modes: (n_actuators, n_modes) matrix.
31+
"""
32+
pass
33+
34+
def save(self, filepath: str | Path) -> None:
35+
"""
36+
Save the generated basis and actuator positions to a .npz file.
37+
"""
38+
if self.modes is None:
39+
raise ValueError("No modes generated yet. Call generate() first.")
40+
41+
np.savez(
42+
filepath,
43+
modes=self.modes,
44+
positions=self.positions,
45+
basis_type=self.__class__.__name__
46+
)
47+
48+
@classmethod
49+
def load(cls, filepath: str | Path) -> 'BasisGenerator':
50+
"""
51+
Load a basis from a .npz file.
52+
Note: This returns a generic container or re-instantiates the specific class if possible.
53+
For simplicity here, we might just return the data or a generic wrapper.
54+
"""
55+
data = np.load(filepath)
56+
positions = data['positions']
57+
modes = data['modes']
58+
59+
# Create a generic instance to hold the data
60+
# In a more complex system, we might factory this based on basis_type
61+
instance = ConcreteBasis(positions)
62+
instance.modes = modes
63+
return instance
64+
65+
def plot(self, count: int = 6, outfile: Optional[str | Path] = None, **kwargs):
66+
"""Plot the generated modes."""
67+
if self.modes is None:
68+
raise ValueError("No modes to plot.")
69+
plot_basis_modes(self.modes, self.positions, count=count, outfile=outfile, **kwargs)
70+
71+
class ConcreteBasis(BasisGenerator):
72+
"""Helper class for loading existing bases."""
73+
def generate(self, n_modes: int, **kwargs) -> np.ndarray:
74+
if self.modes is None:
75+
raise NotImplementedError("This is a loaded basis container.")
76+
return self.modes[:, :n_modes]

0 commit comments

Comments
 (0)