solvephase 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.
- solvephase/__about__.py +3 -0
- solvephase/__init__.py +110 -0
- solvephase/algorithms/__init__.py +4 -0
- solvephase/algorithms/cdi.py +967 -0
- solvephase/algorithms/fast_furious.py +494 -0
- solvephase/algorithms/gerchberg_saxton.py +255 -0
- solvephase/algorithms/lift.py +553 -0
- solvephase/algorithms/phase_diversity.py +658 -0
- solvephase/algorithms/tie.py +693 -0
- solvephase/algorithms/wirtinger.py +865 -0
- solvephase/api.py +228 -0
- solvephase/backend.py +24 -0
- solvephase/basis.py +516 -0
- solvephase/cli.py +131 -0
- solvephase/focal.py +367 -0
- solvephase/interop.py +173 -0
- solvephase/losses.py +169 -0
- solvephase/metrics.py +5 -0
- solvephase/operators.py +391 -0
- solvephase/optimize.py +404 -0
- solvephase/propagation.py +26 -0
- solvephase/pupil.py +5 -0
- solvephase/py.typed +0 -0
- solvephase/result.py +228 -0
- solvephase/retrieval.py +627 -0
- solvephase/simulate.py +113 -0
- solvephase/unwrap.py +5 -0
- solvephase-0.1.0.dist-info/METADATA +198 -0
- solvephase-0.1.0.dist-info/RECORD +32 -0
- solvephase-0.1.0.dist-info/WHEEL +4 -0
- solvephase-0.1.0.dist-info/entry_points.txt +2 -0
- solvephase-0.1.0.dist-info/licenses/LICENSE +21 -0
solvephase/api.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""One-call focal-plane phase retrieval: :func:`retrieve`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from .algorithms.gerchberg_saxton import gerchberg_saxton
|
|
13
|
+
from .backend import BackendLike, to_numpy
|
|
14
|
+
from .basis import Basis
|
|
15
|
+
from .focal import FocalPlaneModel, zernike_diversity
|
|
16
|
+
from .losses import Loss
|
|
17
|
+
from .pupil import Pupil
|
|
18
|
+
from .result import Result
|
|
19
|
+
from .retrieval import FocalPlaneProblem, solve
|
|
20
|
+
|
|
21
|
+
__all__ = ["retrieve"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _as_pupil(pupil: Pupil | int, diameter: float | None) -> Pupil:
|
|
25
|
+
if isinstance(pupil, Pupil):
|
|
26
|
+
return pupil
|
|
27
|
+
if isinstance(pupil, (int, np.integer)):
|
|
28
|
+
return Pupil.circular(int(pupil), diameter or 1.0)
|
|
29
|
+
raise TypeError("pupil must be a Pupil or an int (grid size of a circular pupil)")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _as_diversity(pupil: Pupil, diversity: Any, n_images: int) -> np.ndarray | None:
|
|
33
|
+
if diversity is None:
|
|
34
|
+
if n_images > 1:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"{n_images} images were given but no diversity: give one OPD map (or one "
|
|
37
|
+
"defocus RMS in metres) per image"
|
|
38
|
+
)
|
|
39
|
+
return None
|
|
40
|
+
if isinstance(diversity, dict):
|
|
41
|
+
noll = int(diversity.get("noll", 4))
|
|
42
|
+
return zernike_diversity(pupil, noll, diversity["amounts"])
|
|
43
|
+
if np.ndim(diversity) == 1 and all(np.isscalar(v) for v in diversity):
|
|
44
|
+
return zernike_diversity(pupil, 4, [float(v) for v in diversity])
|
|
45
|
+
maps = np.asarray(diversity, dtype=np.float64)
|
|
46
|
+
if maps.ndim == 2:
|
|
47
|
+
maps = maps[None]
|
|
48
|
+
return maps
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _as_basis(pupil: Pupil, basis: Any) -> Basis | None:
|
|
52
|
+
if basis is None or (isinstance(basis, str) and basis.lower() == "zonal"):
|
|
53
|
+
return None
|
|
54
|
+
if isinstance(basis, Basis):
|
|
55
|
+
return basis
|
|
56
|
+
if isinstance(basis, (int, np.integer)):
|
|
57
|
+
return Basis.zernike(pupil, int(basis))
|
|
58
|
+
raise TypeError("basis must be a Basis, an int (number of Zernike modes) or 'zonal'")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def retrieve(
|
|
62
|
+
images: Any,
|
|
63
|
+
pupil: Pupil | int,
|
|
64
|
+
wavelength: float | Sequence[float],
|
|
65
|
+
*,
|
|
66
|
+
sampling: float | None = None,
|
|
67
|
+
pixel_scale: float | None = None,
|
|
68
|
+
diversity: Any = None,
|
|
69
|
+
basis: Basis | int | str | None = 36,
|
|
70
|
+
zonal_refinement: bool = False,
|
|
71
|
+
method: str = "auto",
|
|
72
|
+
loss: str | Loss = "poisson",
|
|
73
|
+
read_noise: float = 0.0,
|
|
74
|
+
weights: Any = None,
|
|
75
|
+
fit_background: bool = True,
|
|
76
|
+
fit_tilt: bool = False,
|
|
77
|
+
spectral_weights: Sequence[float] | None = None,
|
|
78
|
+
oversample: int = 1,
|
|
79
|
+
diameter: float | None = None,
|
|
80
|
+
start: Any = None,
|
|
81
|
+
max_iter: int | None = None,
|
|
82
|
+
device: BackendLike = "cpu",
|
|
83
|
+
precision: str | None = None,
|
|
84
|
+
) -> Result:
|
|
85
|
+
"""Retrieve a pupil wavefront from focal-plane images in one call.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
images:
|
|
90
|
+
``(K, my, mx)`` images (or one ``(my, mx)`` image), ideally in
|
|
91
|
+
photo-electrons, background included.
|
|
92
|
+
pupil:
|
|
93
|
+
A :class:`~solvephase.Pupil`, or an int ``n`` for an ``n x n``
|
|
94
|
+
circular pupil of ``diameter`` (default 1 m).
|
|
95
|
+
wavelength:
|
|
96
|
+
Wavelength in metres, or an array sampling the band.
|
|
97
|
+
sampling, pixel_scale:
|
|
98
|
+
Detector sampling in pixels per lambda/D, or pixel scale in radians
|
|
99
|
+
(give one).
|
|
100
|
+
diversity:
|
|
101
|
+
Known per-image aberrations: ``(K, ny, nx)`` OPD maps in metres; a
|
|
102
|
+
list of ``K`` numbers, read as defocus (Noll 4) RMS in metres per
|
|
103
|
+
image; or ``{"noll": j, "amounts": [...]}``. Required for more than one
|
|
104
|
+
image.
|
|
105
|
+
basis:
|
|
106
|
+
Wavefront parameterization: number of Zernike modes (default 36,
|
|
107
|
+
tip/tilt upward), a :class:`~solvephase.Basis`, or ``"zonal"``.
|
|
108
|
+
zonal_refinement:
|
|
109
|
+
After the modal solve, refine pixel by pixel (L-BFGS).
|
|
110
|
+
method:
|
|
111
|
+
``"auto"`` (robust multi-start: Levenberg-Marquardt from a flat start
|
|
112
|
+
and from Gerchberg-Saxton, best kept, then a maximum-likelihood
|
|
113
|
+
polish), ``"lm"``, ``"lbfgs"``, ``"gs"`` (Gerchberg-Saxton/Misell only)
|
|
114
|
+
or ``"gs+lm"``.
|
|
115
|
+
loss:
|
|
116
|
+
Final data term: ``"poisson"`` (default; images in photo-electrons),
|
|
117
|
+
``"gaussian"`` or ``"amplitude"``.
|
|
118
|
+
read_noise:
|
|
119
|
+
Read noise in electrons (Poisson loss).
|
|
120
|
+
weights:
|
|
121
|
+
Per-pixel weights; zero masks bad pixels.
|
|
122
|
+
fit_background:
|
|
123
|
+
Fit a constant background per image.
|
|
124
|
+
fit_tilt:
|
|
125
|
+
Fit a registration tip/tilt per image relative to the first.
|
|
126
|
+
spectral_weights:
|
|
127
|
+
Relative photon flux per wavelength for broadband data.
|
|
128
|
+
oversample:
|
|
129
|
+
Model pixels per detector pixel per axis (pixel integration).
|
|
130
|
+
diameter:
|
|
131
|
+
Diameter in metres when ``pupil`` is an int.
|
|
132
|
+
start:
|
|
133
|
+
Initial guess: OPD map, coefficients or a previous :class:`Result`.
|
|
134
|
+
max_iter:
|
|
135
|
+
Iteration limit of the final stage.
|
|
136
|
+
device, precision:
|
|
137
|
+
``"cpu"``, ``"gpu"`` or ``"auto"``; ``"single"`` or ``"double"``.
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
Result
|
|
142
|
+
``result.opd`` (metres), ``result.coefficients``, model images,
|
|
143
|
+
flux/background, convergence history. ``result.extra["stages"]``
|
|
144
|
+
records each stage's loss and time.
|
|
145
|
+
|
|
146
|
+
Examples
|
|
147
|
+
--------
|
|
148
|
+
>>> import solvephase as sp
|
|
149
|
+
>>> pupil = sp.Pupil.circular(64, 8.0, obscuration=0.14)
|
|
150
|
+
>>> result = sp.retrieve(images, pupil, 1.6e-6, sampling=2.0,
|
|
151
|
+
... diversity=[0.0, 0.4e-6]) # doctest: +SKIP
|
|
152
|
+
>>> result.opd, result.coefficients # doctest: +SKIP
|
|
153
|
+
"""
|
|
154
|
+
t0 = time.perf_counter()
|
|
155
|
+
pupil = _as_pupil(pupil, diameter)
|
|
156
|
+
data = np.asarray(to_numpy(images), dtype=np.float64)
|
|
157
|
+
if data.ndim == 2:
|
|
158
|
+
data = data[None]
|
|
159
|
+
if data.ndim != 3:
|
|
160
|
+
raise ValueError(f"images must be (my, mx) or (K, my, mx), got shape {data.shape}")
|
|
161
|
+
div = _as_diversity(pupil, diversity, data.shape[0])
|
|
162
|
+
if div is not None and div.shape[0] != data.shape[0]:
|
|
163
|
+
raise ValueError(f"{div.shape[0]} diversity maps for {data.shape[0]} images")
|
|
164
|
+
model = FocalPlaneModel(
|
|
165
|
+
pupil,
|
|
166
|
+
wavelength,
|
|
167
|
+
data.shape[-2:],
|
|
168
|
+
sampling=sampling,
|
|
169
|
+
pixel_scale=pixel_scale,
|
|
170
|
+
weights=spectral_weights,
|
|
171
|
+
diversity=div,
|
|
172
|
+
oversample=oversample,
|
|
173
|
+
device=device,
|
|
174
|
+
precision=precision,
|
|
175
|
+
)
|
|
176
|
+
modal = _as_basis(pupil, basis)
|
|
177
|
+
common: dict[str, Any] = {
|
|
178
|
+
"weights": weights,
|
|
179
|
+
"fit_background": fit_background,
|
|
180
|
+
"fit_tilt": fit_tilt and data.shape[0] > 1,
|
|
181
|
+
"read_noise": read_noise,
|
|
182
|
+
}
|
|
183
|
+
final = FocalPlaneProblem(model, data, basis=modal, loss=loss, **common)
|
|
184
|
+
stages: list[dict[str, Any]] = []
|
|
185
|
+
|
|
186
|
+
def record(name: str, result: Result) -> Result:
|
|
187
|
+
stages.append(
|
|
188
|
+
{
|
|
189
|
+
"stage": name,
|
|
190
|
+
"loss": float(final.value(final.initial(result))),
|
|
191
|
+
"time": result.elapsed,
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
method = method.lower()
|
|
197
|
+
if method == "gs":
|
|
198
|
+
result = gerchberg_saxton(model, data, basis=modal, weights=weights)
|
|
199
|
+
best = record("gs", result)
|
|
200
|
+
elif method in ("lm", "lbfgs"):
|
|
201
|
+
if method == "lm" and modal is None:
|
|
202
|
+
raise ValueError("method='lm' needs a modal basis; use 'lbfgs' for a zonal solve")
|
|
203
|
+
best = record(method, solve(final, method=method, start=start, max_iter=max_iter))
|
|
204
|
+
elif method in ("auto", "gs+lm"):
|
|
205
|
+
explorer = modal if modal is not None else Basis.zernike(pupil, 36)
|
|
206
|
+
capture = FocalPlaneProblem(model, data, basis=explorer, loss="amplitude", **common)
|
|
207
|
+
candidates: list[Result] = []
|
|
208
|
+
if method == "auto" or start is not None:
|
|
209
|
+
candidates.append(record("lm-amplitude", solve(capture, method="lm", start=start)))
|
|
210
|
+
gs = gerchberg_saxton(model, data, iterations=200, weights=weights, start=start)
|
|
211
|
+
candidates.append(record("gs->lm-amplitude", solve(capture, method="lm", start=gs)))
|
|
212
|
+
best = min(candidates, key=lambda r: final.value(final.initial(r)))
|
|
213
|
+
polish_method = "lm" if modal is not None else "lbfgs"
|
|
214
|
+
best = record("polish", solve(final, method=polish_method, start=best, max_iter=max_iter))
|
|
215
|
+
else:
|
|
216
|
+
raise ValueError(f"unknown method {method!r}; use 'auto', 'lm', 'lbfgs', 'gs' or 'gs+lm'")
|
|
217
|
+
|
|
218
|
+
if zonal_refinement and modal is not None:
|
|
219
|
+
zonal = FocalPlaneProblem(model, data, basis=None, loss=loss, **common)
|
|
220
|
+
refined = solve(zonal, method="lbfgs", start=best, max_iter=max_iter or 300)
|
|
221
|
+
stages.append({"stage": "zonal", "loss": refined.loss, "time": refined.elapsed})
|
|
222
|
+
best = refined
|
|
223
|
+
best.elapsed = time.perf_counter() - t0
|
|
224
|
+
best.extra["stages"] = stages
|
|
225
|
+
best.extra["model"] = model
|
|
226
|
+
if not math.isfinite(best.loss):
|
|
227
|
+
best.loss = float(final.value(final.initial(best)))
|
|
228
|
+
return best
|
solvephase/backend.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Array backends: re-exported from :mod:`aocore.backend` (the stack's shared backend).
|
|
2
|
+
|
|
3
|
+
See :mod:`aocore.backend` for the full documentation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from aocore.backend import (
|
|
7
|
+
Backend,
|
|
8
|
+
BackendLike,
|
|
9
|
+
_cpu_workers,
|
|
10
|
+
backend_of,
|
|
11
|
+
get_backend,
|
|
12
|
+
gpu_available,
|
|
13
|
+
to_numpy,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Backend",
|
|
18
|
+
"BackendLike",
|
|
19
|
+
"_cpu_workers",
|
|
20
|
+
"backend_of",
|
|
21
|
+
"get_backend",
|
|
22
|
+
"gpu_available",
|
|
23
|
+
"to_numpy",
|
|
24
|
+
]
|