aobasis 1.0.0__tar.gz
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.
- aobasis-1.0.0/LICENSE +21 -0
- aobasis-1.0.0/PKG-INFO +221 -0
- aobasis-1.0.0/README.md +178 -0
- aobasis-1.0.0/pyproject.toml +35 -0
- aobasis-1.0.0/setup.cfg +4 -0
- aobasis-1.0.0/src/aobasis/__init__.py +26 -0
- aobasis-1.0.0/src/aobasis/base.py +76 -0
- aobasis-1.0.0/src/aobasis/fourier.py +82 -0
- aobasis-1.0.0/src/aobasis/hadamard.py +51 -0
- aobasis-1.0.0/src/aobasis/kl.py +212 -0
- aobasis-1.0.0/src/aobasis/utils.py +135 -0
- aobasis-1.0.0/src/aobasis/zernike.py +129 -0
- aobasis-1.0.0/src/aobasis/zonal.py +26 -0
- aobasis-1.0.0/src/aobasis.egg-info/PKG-INFO +221 -0
- aobasis-1.0.0/src/aobasis.egg-info/SOURCES.txt +18 -0
- aobasis-1.0.0/src/aobasis.egg-info/dependency_links.txt +1 -0
- aobasis-1.0.0/src/aobasis.egg-info/requires.txt +8 -0
- aobasis-1.0.0/src/aobasis.egg-info/top_level.txt +1 -0
- aobasis-1.0.0/tests/test_generators.py +251 -0
- aobasis-1.0.0/tests/test_utils.py +213 -0
aobasis-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jacob Taylor
|
|
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.
|
aobasis-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aobasis
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A package for generating AO basis sets (KL, Zernike, Fourier)
|
|
5
|
+
Author-email: Jacob Taylor <jtaylor@keck.hawaii.edu>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Jacob Taylor
|
|
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
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/jacotay7/aobasis
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/jacotay7/aobasis/issues
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: Operating System :: OS Independent
|
|
32
|
+
Requires-Python: >=3.8
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
License-File: LICENSE
|
|
35
|
+
Requires-Dist: numpy>=1.20
|
|
36
|
+
Requires-Dist: scipy>=1.7
|
|
37
|
+
Requires-Dist: matplotlib>=3.5
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: pytest; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
41
|
+
Requires-Dist: imageio; extra == "dev"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# AO Basis (aobasis)
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- **Karhunen-Loève (KL) Modes**: Optimized for atmospheric turbulence (Von Kármán spectrum).
|
|
51
|
+
- Optional GPU acceleration available for large systems (requires CuPy).
|
|
52
|
+
- **Zernike Polynomials**: Standard optical aberration modes (Noll indexing).
|
|
53
|
+
- **Fourier Modes**: Sinusoidal basis sets.
|
|
54
|
+
- **Zonal Basis**: Single actuator pokes (Identity).
|
|
55
|
+
- **Hadamard Basis**: Orthogonal binary patterns for calibration.
|
|
56
|
+
- **Flexible Geometry**: Works with arbitrary actuator positions (defaulting to circular grids).
|
|
57
|
+
- **Piston Removal**: Option to exclude piston/DC modes from generation.
|
|
58
|
+
- **Visualization**: Built-in plotting tools for quick inspection.
|
|
59
|
+
- **Serialization**: Save and load basis sets to/from `.npz` files.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
### Prerequisites
|
|
64
|
+
- Python 3.8 or higher
|
|
65
|
+
- (Optional) For GPU-accelerated KL generation: CUDA-compatible GPU and CuPy
|
|
66
|
+
|
|
67
|
+
### Install from Source
|
|
68
|
+
Clone the repository and install using pip:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git clone https://github.com/jacotay7/aobasis.git
|
|
72
|
+
cd aobasis
|
|
73
|
+
pip install .
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For development (editable install with test dependencies):
|
|
77
|
+
```bash
|
|
78
|
+
pip install -e ".[dev]"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### GPU Acceleration (Optional)
|
|
82
|
+
To enable GPU acceleration for KL basis generation, you need to install CuPy and ensure you have a CUDA-compatible GPU.
|
|
83
|
+
|
|
84
|
+
#### Requirements
|
|
85
|
+
- NVIDIA GPU with CUDA support
|
|
86
|
+
- CUDA Toolkit (version 11.x or 12.x)
|
|
87
|
+
|
|
88
|
+
#### Installation via Conda (Recommended)
|
|
89
|
+
This method automatically handles CUDA dependencies:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Create a new conda environment (optional but recommended)
|
|
93
|
+
conda create -n aobasis python=3.12
|
|
94
|
+
conda activate aobasis
|
|
95
|
+
|
|
96
|
+
# Install CuPy from conda-forge (auto-detects CUDA version)
|
|
97
|
+
conda install -c conda-forge cupy
|
|
98
|
+
|
|
99
|
+
# Install CUDA toolkit if not already present
|
|
100
|
+
conda install -c nvidia cuda-toolkit
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Installation via Pip
|
|
104
|
+
If you prefer pip and already have CUDA installed on your system:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# For CUDA 12.x
|
|
108
|
+
pip install cupy-cuda12x
|
|
109
|
+
|
|
110
|
+
# For CUDA 11.x
|
|
111
|
+
pip install cupy-cuda11x
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Verify Installation
|
|
115
|
+
Test that CuPy is working correctly:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import cupy as cp
|
|
119
|
+
print(f"CuPy version: {cp.__version__}")
|
|
120
|
+
print(f"CUDA available: {cp.cuda.is_available()}")
|
|
121
|
+
|
|
122
|
+
# Simple test
|
|
123
|
+
a = cp.array([1, 2, 3])
|
|
124
|
+
b = cp.array([4, 5, 6])
|
|
125
|
+
print(f"Sum: {cp.asnumpy(a + b)}") # Should print [5, 7, 9]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
If you encounter any issues, consult the [CuPy installation guide](https://docs.cupy.dev/en/stable/install.html).
|
|
129
|
+
|
|
130
|
+
## Quick Start
|
|
131
|
+
|
|
132
|
+
Here is a simple example of generating and plotting KL modes for a 10-meter telescope:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from aobasis import KLBasisGenerator, make_circular_actuator_grid
|
|
136
|
+
|
|
137
|
+
# 1. Define the actuator geometry
|
|
138
|
+
positions = make_circular_actuator_grid(telescope_diameter=10.0, grid_size=20)
|
|
139
|
+
|
|
140
|
+
# 2. Initialize the generator (use_gpu=True for GPU acceleration if available)
|
|
141
|
+
kl_gen = KLBasisGenerator(positions, fried_parameter=0.16, outer_scale=30.0, use_gpu=False)
|
|
142
|
+
|
|
143
|
+
# 3. Generate modes (excluding piston)
|
|
144
|
+
modes = kl_gen.generate(n_modes=50, ignore_piston=True)
|
|
145
|
+
|
|
146
|
+
# 4. Plot the first 6 modes
|
|
147
|
+
kl_gen.plot(count=6, title_prefix="KL Mode")
|
|
148
|
+
|
|
149
|
+
# 5. Save to disk
|
|
150
|
+
kl_gen.save("my_kl_basis.npz")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Performance
|
|
154
|
+
|
|
155
|
+
Generation times for 100 modes benchmarked on the following system:
|
|
156
|
+
- **CPU**: AMD Ryzen 9 9950X3D (16-core, 32-thread)
|
|
157
|
+
- **GPU**: NVIDIA GeForce RTX 5090 (32 GB)
|
|
158
|
+
- **OS**: Linux (Ubuntu)
|
|
159
|
+
|
|
160
|
+
| Basis | 16x16 Grid (~170 acts) | 32x32 Grid (~740 acts) | 64x64 Grid (~3100 acts) |
|
|
161
|
+
|-------|------------------------|------------------------|-------------------------|
|
|
162
|
+
| **KL (CPU)** | 0.010s | 0.170s | 3.008s |
|
|
163
|
+
| **KL (GPU)** | 0.005s | 0.019s | 0.202s |
|
|
164
|
+
| **Zernike** | 0.001s | 0.002s | 0.005s |
|
|
165
|
+
| **Fourier** | <0.001s | 0.001s | 0.003s |
|
|
166
|
+
| **Zonal** | <0.001s | <0.001s | 0.003s |
|
|
167
|
+
| **Hadamard** | <0.001s | 0.001s | 0.031s |
|
|
168
|
+
|
|
169
|
+
*Note: KL basis generation is computationally intensive ($O(N^3)$) due to the dense covariance matrix diagonalization. GPU acceleration provides significant speedup (8-15x) for larger grids.*
|
|
170
|
+
|
|
171
|
+
## Tutorials
|
|
172
|
+
|
|
173
|
+
We provide Jupyter notebooks to help you get started.
|
|
174
|
+
|
|
175
|
+
1. **Getting Started**: `tutorials/getting_started.ipynb` covers all supported basis types and features.
|
|
176
|
+
|
|
177
|
+
To run the tutorials:
|
|
178
|
+
```bash
|
|
179
|
+
# Install Jupyter if you haven't already
|
|
180
|
+
pip install jupyter
|
|
181
|
+
|
|
182
|
+
# Launch the notebook server
|
|
183
|
+
jupyter notebook tutorials/getting_started.ipynb
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Development & Testing
|
|
187
|
+
|
|
188
|
+
This project uses `pytest` for testing. To run the test suite:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# Install dev dependencies
|
|
192
|
+
pip install -e ".[dev]"
|
|
193
|
+
|
|
194
|
+
# Run tests
|
|
195
|
+
pytest
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Contributing
|
|
199
|
+
|
|
200
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
201
|
+
|
|
202
|
+
1. Fork the repository.
|
|
203
|
+
2. Create your feature branch (`git checkout -b feature/AmazingFeature`).
|
|
204
|
+
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`).
|
|
205
|
+
4. Push to the branch (`git push origin feature/AmazingFeature`).
|
|
206
|
+
5. Open a Pull Request.
|
|
207
|
+
|
|
208
|
+
## Issues
|
|
209
|
+
|
|
210
|
+
If you encounter any bugs or have feature requests, please file an issue on the [GitHub Issues](https://github.com/jacotay7/aobasis/issues) page.
|
|
211
|
+
|
|
212
|
+
## Contact
|
|
213
|
+
|
|
214
|
+
For questions or support, please contact:
|
|
215
|
+
|
|
216
|
+
**User Name**
|
|
217
|
+
Email: jtaylor@keck.hawaii.edu
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
aobasis-1.0.0/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
- Optional GPU acceleration available for large systems (requires CuPy).
|
|
9
|
+
- **Zernike Polynomials**: Standard optical aberration modes (Noll indexing).
|
|
10
|
+
- **Fourier Modes**: Sinusoidal basis sets.
|
|
11
|
+
- **Zonal Basis**: Single actuator pokes (Identity).
|
|
12
|
+
- **Hadamard Basis**: Orthogonal binary patterns for calibration.
|
|
13
|
+
- **Flexible Geometry**: Works with arbitrary actuator positions (defaulting to circular grids).
|
|
14
|
+
- **Piston Removal**: Option to exclude piston/DC modes from generation.
|
|
15
|
+
- **Visualization**: Built-in plotting tools for quick inspection.
|
|
16
|
+
- **Serialization**: Save and load basis sets to/from `.npz` files.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
### Prerequisites
|
|
21
|
+
- Python 3.8 or higher
|
|
22
|
+
- (Optional) For GPU-accelerated KL generation: CUDA-compatible GPU and CuPy
|
|
23
|
+
|
|
24
|
+
### Install from Source
|
|
25
|
+
Clone the repository and install using pip:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/jacotay7/aobasis.git
|
|
29
|
+
cd aobasis
|
|
30
|
+
pip install .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For development (editable install with test dependencies):
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e ".[dev]"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### GPU Acceleration (Optional)
|
|
39
|
+
To enable GPU acceleration for KL basis generation, you need to install CuPy and ensure you have a CUDA-compatible GPU.
|
|
40
|
+
|
|
41
|
+
#### Requirements
|
|
42
|
+
- NVIDIA GPU with CUDA support
|
|
43
|
+
- CUDA Toolkit (version 11.x or 12.x)
|
|
44
|
+
|
|
45
|
+
#### Installation via Conda (Recommended)
|
|
46
|
+
This method automatically handles CUDA dependencies:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Create a new conda environment (optional but recommended)
|
|
50
|
+
conda create -n aobasis python=3.12
|
|
51
|
+
conda activate aobasis
|
|
52
|
+
|
|
53
|
+
# Install CuPy from conda-forge (auto-detects CUDA version)
|
|
54
|
+
conda install -c conda-forge cupy
|
|
55
|
+
|
|
56
|
+
# Install CUDA toolkit if not already present
|
|
57
|
+
conda install -c nvidia cuda-toolkit
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### Installation via Pip
|
|
61
|
+
If you prefer pip and already have CUDA installed on your system:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# For CUDA 12.x
|
|
65
|
+
pip install cupy-cuda12x
|
|
66
|
+
|
|
67
|
+
# For CUDA 11.x
|
|
68
|
+
pip install cupy-cuda11x
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### Verify Installation
|
|
72
|
+
Test that CuPy is working correctly:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import cupy as cp
|
|
76
|
+
print(f"CuPy version: {cp.__version__}")
|
|
77
|
+
print(f"CUDA available: {cp.cuda.is_available()}")
|
|
78
|
+
|
|
79
|
+
# Simple test
|
|
80
|
+
a = cp.array([1, 2, 3])
|
|
81
|
+
b = cp.array([4, 5, 6])
|
|
82
|
+
print(f"Sum: {cp.asnumpy(a + b)}") # Should print [5, 7, 9]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If you encounter any issues, consult the [CuPy installation guide](https://docs.cupy.dev/en/stable/install.html).
|
|
86
|
+
|
|
87
|
+
## Quick Start
|
|
88
|
+
|
|
89
|
+
Here is a simple example of generating and plotting KL modes for a 10-meter telescope:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from aobasis import KLBasisGenerator, make_circular_actuator_grid
|
|
93
|
+
|
|
94
|
+
# 1. Define the actuator geometry
|
|
95
|
+
positions = make_circular_actuator_grid(telescope_diameter=10.0, grid_size=20)
|
|
96
|
+
|
|
97
|
+
# 2. Initialize the generator (use_gpu=True for GPU acceleration if available)
|
|
98
|
+
kl_gen = KLBasisGenerator(positions, fried_parameter=0.16, outer_scale=30.0, use_gpu=False)
|
|
99
|
+
|
|
100
|
+
# 3. Generate modes (excluding piston)
|
|
101
|
+
modes = kl_gen.generate(n_modes=50, ignore_piston=True)
|
|
102
|
+
|
|
103
|
+
# 4. Plot the first 6 modes
|
|
104
|
+
kl_gen.plot(count=6, title_prefix="KL Mode")
|
|
105
|
+
|
|
106
|
+
# 5. Save to disk
|
|
107
|
+
kl_gen.save("my_kl_basis.npz")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Performance
|
|
111
|
+
|
|
112
|
+
Generation times for 100 modes benchmarked on the following system:
|
|
113
|
+
- **CPU**: AMD Ryzen 9 9950X3D (16-core, 32-thread)
|
|
114
|
+
- **GPU**: NVIDIA GeForce RTX 5090 (32 GB)
|
|
115
|
+
- **OS**: Linux (Ubuntu)
|
|
116
|
+
|
|
117
|
+
| Basis | 16x16 Grid (~170 acts) | 32x32 Grid (~740 acts) | 64x64 Grid (~3100 acts) |
|
|
118
|
+
|-------|------------------------|------------------------|-------------------------|
|
|
119
|
+
| **KL (CPU)** | 0.010s | 0.170s | 3.008s |
|
|
120
|
+
| **KL (GPU)** | 0.005s | 0.019s | 0.202s |
|
|
121
|
+
| **Zernike** | 0.001s | 0.002s | 0.005s |
|
|
122
|
+
| **Fourier** | <0.001s | 0.001s | 0.003s |
|
|
123
|
+
| **Zonal** | <0.001s | <0.001s | 0.003s |
|
|
124
|
+
| **Hadamard** | <0.001s | 0.001s | 0.031s |
|
|
125
|
+
|
|
126
|
+
*Note: KL basis generation is computationally intensive ($O(N^3)$) due to the dense covariance matrix diagonalization. GPU acceleration provides significant speedup (8-15x) for larger grids.*
|
|
127
|
+
|
|
128
|
+
## Tutorials
|
|
129
|
+
|
|
130
|
+
We provide Jupyter notebooks to help you get started.
|
|
131
|
+
|
|
132
|
+
1. **Getting Started**: `tutorials/getting_started.ipynb` covers all supported basis types and features.
|
|
133
|
+
|
|
134
|
+
To run the tutorials:
|
|
135
|
+
```bash
|
|
136
|
+
# Install Jupyter if you haven't already
|
|
137
|
+
pip install jupyter
|
|
138
|
+
|
|
139
|
+
# Launch the notebook server
|
|
140
|
+
jupyter notebook tutorials/getting_started.ipynb
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Development & Testing
|
|
144
|
+
|
|
145
|
+
This project uses `pytest` for testing. To run the test suite:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Install dev dependencies
|
|
149
|
+
pip install -e ".[dev]"
|
|
150
|
+
|
|
151
|
+
# Run tests
|
|
152
|
+
pytest
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Contributing
|
|
156
|
+
|
|
157
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
158
|
+
|
|
159
|
+
1. Fork the repository.
|
|
160
|
+
2. Create your feature branch (`git checkout -b feature/AmazingFeature`).
|
|
161
|
+
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`).
|
|
162
|
+
4. Push to the branch (`git push origin feature/AmazingFeature`).
|
|
163
|
+
5. Open a Pull Request.
|
|
164
|
+
|
|
165
|
+
## Issues
|
|
166
|
+
|
|
167
|
+
If you encounter any bugs or have feature requests, please file an issue on the [GitHub Issues](https://github.com/jacotay7/aobasis/issues) page.
|
|
168
|
+
|
|
169
|
+
## Contact
|
|
170
|
+
|
|
171
|
+
For questions or support, please contact:
|
|
172
|
+
|
|
173
|
+
**User Name**
|
|
174
|
+
Email: jtaylor@keck.hawaii.edu
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aobasis"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A package for generating AO basis sets (KL, Zernike, Fourier)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "Jacob Taylor", email = "jtaylor@keck.hawaii.edu" }]
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"numpy>=1.20",
|
|
18
|
+
"scipy>=1.7",
|
|
19
|
+
"matplotlib>=3.5",
|
|
20
|
+
]
|
|
21
|
+
requires-python = ">=3.8"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest",
|
|
26
|
+
"pytest-cov",
|
|
27
|
+
"imageio",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
"Homepage" = "https://github.com/jacotay7/aobasis"
|
|
32
|
+
"Bug Tracker" = "https://github.com/jacotay7/aobasis/issues"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
aobasis-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
__version__ = version("aobasis")
|
|
5
|
+
except PackageNotFoundError:
|
|
6
|
+
__version__ = "unknown"
|
|
7
|
+
|
|
8
|
+
from .base import BasisGenerator
|
|
9
|
+
from .kl import KLBasisGenerator
|
|
10
|
+
from .zernike import ZernikeBasisGenerator
|
|
11
|
+
from .fourier import FourierBasisGenerator
|
|
12
|
+
from .zonal import ZonalBasisGenerator
|
|
13
|
+
from .hadamard import HadamardBasisGenerator
|
|
14
|
+
from .utils import make_circular_actuator_grid, make_concentric_actuator_grid, plot_basis_modes
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"BasisGenerator",
|
|
18
|
+
"KLBasisGenerator",
|
|
19
|
+
"ZernikeBasisGenerator",
|
|
20
|
+
"FourierBasisGenerator",
|
|
21
|
+
"ZonalBasisGenerator",
|
|
22
|
+
"HadamardBasisGenerator",
|
|
23
|
+
"make_circular_actuator_grid",
|
|
24
|
+
"make_concentric_actuator_grid",
|
|
25
|
+
"plot_basis_modes",
|
|
26
|
+
]
|
|
@@ -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, Union
|
|
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: Union[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: Union[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[Union[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,0 +1,82 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .base import BasisGenerator
|
|
3
|
+
|
|
4
|
+
class FourierBasisGenerator(BasisGenerator):
|
|
5
|
+
"""
|
|
6
|
+
Generates Fourier modes (sine/cosine) on the actuator grid.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, positions: np.ndarray, pupil_diameter: float):
|
|
10
|
+
super().__init__(positions)
|
|
11
|
+
self.pupil_diameter = pupil_diameter
|
|
12
|
+
|
|
13
|
+
def generate(self, n_modes: int, ignore_piston: bool = False, **kwargs) -> np.ndarray:
|
|
14
|
+
"""
|
|
15
|
+
Generate Fourier modes.
|
|
16
|
+
We generate pairs of sin/cos for increasing spatial frequencies.
|
|
17
|
+
"""
|
|
18
|
+
x = self.positions[:, 0]
|
|
19
|
+
y = self.positions[:, 1]
|
|
20
|
+
|
|
21
|
+
modes_list = []
|
|
22
|
+
|
|
23
|
+
# Piston
|
|
24
|
+
if not ignore_piston:
|
|
25
|
+
modes_list.append(np.ones_like(x))
|
|
26
|
+
|
|
27
|
+
# Loop through spatial frequencies
|
|
28
|
+
# kx, ky integers
|
|
29
|
+
# We'll spiral out or just loop kx, ky
|
|
30
|
+
# Simple ordering: by magnitude of k vector
|
|
31
|
+
|
|
32
|
+
k_pairs = []
|
|
33
|
+
# Generate a pool of k-vectors
|
|
34
|
+
k_max = int(np.sqrt(n_modes)) + 2
|
|
35
|
+
for kx in range(-k_max, k_max + 1):
|
|
36
|
+
for ky in range(-k_max, k_max + 1):
|
|
37
|
+
if kx == 0 and ky == 0:
|
|
38
|
+
continue
|
|
39
|
+
# We only need half the plane for real Fourier basis (sin/cos)
|
|
40
|
+
# But simpler to just generate sin/cos pairs for positive k's?
|
|
41
|
+
# Let's stick to standard real Fourier series expansion logic
|
|
42
|
+
# cos(2pi(ux + vy)), sin(2pi(ux + vy))
|
|
43
|
+
k_pairs.append((kx, ky))
|
|
44
|
+
|
|
45
|
+
# Sort by spatial frequency magnitude
|
|
46
|
+
k_pairs.sort(key=lambda k: k[0]**2 + k[1]**2)
|
|
47
|
+
|
|
48
|
+
# Filter duplicates/redundancies for real basis
|
|
49
|
+
# We want unique spatial frequencies.
|
|
50
|
+
# For each (kx, ky), we can have cos and sin.
|
|
51
|
+
# But (kx, ky) and (-kx, -ky) are redundant.
|
|
52
|
+
# So we keep only "positive" half plane.
|
|
53
|
+
|
|
54
|
+
unique_ks = []
|
|
55
|
+
seen = set()
|
|
56
|
+
for kx, ky in k_pairs:
|
|
57
|
+
if (kx, ky) in seen or (-kx, -ky) in seen:
|
|
58
|
+
continue
|
|
59
|
+
seen.add((kx, ky))
|
|
60
|
+
unique_ks.append((kx, ky))
|
|
61
|
+
|
|
62
|
+
# Generate modes
|
|
63
|
+
# Fundamental frequency base: 1 cycle per pupil diameter
|
|
64
|
+
f0 = 1.0 / self.pupil_diameter
|
|
65
|
+
|
|
66
|
+
for kx, ky in unique_ks:
|
|
67
|
+
if len(modes_list) >= n_modes:
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
arg = 2 * np.pi * f0 * (kx * x + ky * y)
|
|
71
|
+
|
|
72
|
+
# Cosine component
|
|
73
|
+
modes_list.append(np.cos(arg))
|
|
74
|
+
|
|
75
|
+
if len(modes_list) >= n_modes:
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
# Sine component
|
|
79
|
+
modes_list.append(np.sin(arg))
|
|
80
|
+
|
|
81
|
+
self.modes = np.column_stack(modes_list)
|
|
82
|
+
return self.modes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.linalg import hadamard
|
|
3
|
+
from .base import BasisGenerator
|
|
4
|
+
|
|
5
|
+
class HadamardBasisGenerator(BasisGenerator):
|
|
6
|
+
"""
|
|
7
|
+
Generates Hadamard modes.
|
|
8
|
+
Useful for interaction matrix calibration (multiplexing).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def generate(self, n_modes: int, **kwargs) -> np.ndarray:
|
|
12
|
+
"""
|
|
13
|
+
Generate Hadamard modes.
|
|
14
|
+
|
|
15
|
+
Since Hadamard matrices exist only for sizes 2^k (or multiples of 4),
|
|
16
|
+
we find the next power of 2 >= n_actuators, generate the Hadamard matrix,
|
|
17
|
+
and truncate it to the number of actuators (rows) and requested modes (columns).
|
|
18
|
+
"""
|
|
19
|
+
# Find next power of 2 covering the number of actuators
|
|
20
|
+
# We need at least n_actuators rows to define the pattern on the grid
|
|
21
|
+
# And we need enough columns for n_modes
|
|
22
|
+
|
|
23
|
+
# Usually for calibration, we want a square matrix that covers all actuators.
|
|
24
|
+
# So size >= n_actuators.
|
|
25
|
+
|
|
26
|
+
size = 1
|
|
27
|
+
while size < self.n_actuators:
|
|
28
|
+
size *= 2
|
|
29
|
+
|
|
30
|
+
# If n_modes is larger than this size, we might need a larger matrix?
|
|
31
|
+
# But usually we can't have more orthogonal modes than actuators (degrees of freedom).
|
|
32
|
+
# However, Hadamard patterns are defined by the full matrix.
|
|
33
|
+
|
|
34
|
+
if n_modes > size:
|
|
35
|
+
# If user asks for more modes than the natural Hadamard block size covering actuators,
|
|
36
|
+
# we might need to go bigger.
|
|
37
|
+
while size < n_modes:
|
|
38
|
+
size *= 2
|
|
39
|
+
|
|
40
|
+
H = hadamard(size)
|
|
41
|
+
|
|
42
|
+
# Truncate to actuators (rows) and modes (cols)
|
|
43
|
+
# Note: Truncated Hadamard is not necessarily orthogonal!
|
|
44
|
+
# But it is the standard way to project Hadamard patterns onto a smaller aperture.
|
|
45
|
+
|
|
46
|
+
self.modes = H[:self.n_actuators, :n_modes]
|
|
47
|
+
|
|
48
|
+
# Optional: Normalize? Hadamard entries are 1 and -1.
|
|
49
|
+
# Keeping them as is is standard.
|
|
50
|
+
|
|
51
|
+
return self.modes
|