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
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
"""Phase retrieval for generic linear measurement models: the Wirtinger-flow family.
|
|
2
|
+
|
|
3
|
+
Recover ``x`` (up to a global phase) from ``y = |A x|^2`` for a known linear
|
|
4
|
+
operator ``A`` (:mod:`solvephase.operators`). The algorithms are the gradient
|
|
5
|
+
methods catalogued by PhasePack (Chandra et al., Asilomar 2017), each started
|
|
6
|
+
from a spectral-type initialization:
|
|
7
|
+
|
|
8
|
+
Initializations
|
|
9
|
+
* spectral - leading eigenvector of ``A^H diag(y) A`` (Netrapalli, Jain &
|
|
10
|
+
Sanghavi, NeurIPS 2013; Candès, Li & Soltanolkotabi, IEEE Trans. Inf.
|
|
11
|
+
Theory 61, 1985, 2015);
|
|
12
|
+
* truncated spectral - drops measurements above ``alpha_y^2 mean(y)``
|
|
13
|
+
(Chen & Candès, Comm. Pure Appl. Math. 70, 822, 2017);
|
|
14
|
+
* orthogonality-promoting - the ``m/6`` largest amplitudes, unweighted
|
|
15
|
+
(Wang, Giannakis & Eldar, IEEE Trans. Inf. Theory 64, 773, 2018);
|
|
16
|
+
* weighted maximal correlation - the ``3m/13`` largest amplitudes weighted
|
|
17
|
+
by ``psi^(1/2)`` (Wang, Giannakis, Saad & Chen, IEEE Trans. Signal
|
|
18
|
+
Process. 66, 2818, 2018);
|
|
19
|
+
* optimal preprocessing - ``T(y) = (y - 1) / (y + sqrt(delta) - 1)`` with
|
|
20
|
+
``y`` normalized to unit mean and ``delta = m / n`` (Luo, Alghamdi & Lu,
|
|
21
|
+
IEEE Trans. Signal Process. 67, 2347, 2019);
|
|
22
|
+
* random.
|
|
23
|
+
|
|
24
|
+
Algorithms
|
|
25
|
+
* ``"wf"`` - Wirtinger Flow on the intensity loss with the step schedule
|
|
26
|
+
``mu_t = min(1 - exp(-t / tau0), mu_max)`` (Candès et al. 2015;
|
|
27
|
+
``tau0 = 330``; ``mu_max`` defaults to 0.1 because the paper's 0.2
|
|
28
|
+
diverges or stalls on many Gaussian instances with ``n >= 128``);
|
|
29
|
+
* ``"twf"`` - Truncated Wirtinger Flow on the Poisson likelihood (Chen &
|
|
30
|
+
Candès 2017);
|
|
31
|
+
* ``"taf"`` - Truncated Amplitude Flow (Wang, Giannakis & Eldar 2018);
|
|
32
|
+
* ``"raf"`` - Reweighted Amplitude Flow (Wang, Giannakis, Saad & Chen 2018);
|
|
33
|
+
* ``"lbfgs"`` - L-BFGS (:func:`solvephase.optimize.lbfgs`) on the amplitude
|
|
34
|
+
or intensity least-squares loss, using a real view of the complex vector.
|
|
35
|
+
|
|
36
|
+
Scaling. The literature states every rule for the unit-variance complex
|
|
37
|
+
Gaussian model (``E |A_kj|^2 = 1``). Internally the operator is rescaled to
|
|
38
|
+
that convention, ``A -> sqrt(s) A`` and ``y -> s y`` with
|
|
39
|
+
``s = m n / ||A||_F^2``, so the published step sizes and truncation thresholds
|
|
40
|
+
apply unchanged to any operator (for coded diffraction with a unitary FFT this
|
|
41
|
+
reproduces the unnormalized-FFT model of the CDP papers). Measurement vectors
|
|
42
|
+
are assumed to have nearly equal norms, which holds for the Gaussian and CDP
|
|
43
|
+
models; the row-norm normalizations of the TAF/RAF initializations are
|
|
44
|
+
therefore dropped.
|
|
45
|
+
|
|
46
|
+
Whitening. When the operator reports ``G = diag(A^H A)``
|
|
47
|
+
(:meth:`~solvephase.operators.LinearOperator.gram_diagonal`; every built-in
|
|
48
|
+
operator does), the power method runs on ``G^{-1/2} Y G^{-1/2}`` and maps the
|
|
49
|
+
eigenvector back with ``G^{1/2}``. For the Gaussian model ``G ~ m I`` and
|
|
50
|
+
nothing changes; for coded diffraction with octanary masks it removes
|
|
51
|
+
eigenvectors localized on pixels where most masks are strong, which otherwise
|
|
52
|
+
beat the signal eigenvector for images of about ``10^4`` pixels or more.
|
|
53
|
+
|
|
54
|
+
All iterations stay on the backend device; the residual is transferred to the
|
|
55
|
+
host only every ``check_every`` iterations.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
import math
|
|
61
|
+
import time
|
|
62
|
+
from collections.abc import Callable
|
|
63
|
+
from dataclasses import dataclass, field, replace
|
|
64
|
+
from typing import Any
|
|
65
|
+
|
|
66
|
+
import numpy as np
|
|
67
|
+
|
|
68
|
+
from ..backend import Backend, backend_of, to_numpy
|
|
69
|
+
from ..operators import LinearOperator, _norm
|
|
70
|
+
from ..optimize import lbfgs
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"GenericResult",
|
|
74
|
+
"init_optimal_spectral",
|
|
75
|
+
"init_orthogonality_promoting",
|
|
76
|
+
"init_random",
|
|
77
|
+
"init_spectral",
|
|
78
|
+
"init_truncated_spectral",
|
|
79
|
+
"init_weighted_correlation",
|
|
80
|
+
"relative_error",
|
|
81
|
+
"wirtinger",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
Callback = Callable[[int, Any, float], "bool | None"]
|
|
85
|
+
|
|
86
|
+
METHODS = ("wf", "twf", "taf", "raf", "lbfgs")
|
|
87
|
+
INITS = ("spectral", "truncated", "orthogonal", "weighted", "optimal", "random")
|
|
88
|
+
|
|
89
|
+
#: Initialization used by ``init="auto"``: each algorithm's own paper, except WF, whose
|
|
90
|
+
#: plain spectral start is replaced by the truncated one (identical on Gaussian data, and
|
|
91
|
+
#: the plain one is dominated by the heavy tail of ``y`` for coded diffraction).
|
|
92
|
+
DEFAULT_INIT = {
|
|
93
|
+
"wf": "truncated",
|
|
94
|
+
"twf": "truncated",
|
|
95
|
+
"taf": "orthogonal",
|
|
96
|
+
"raf": "weighted",
|
|
97
|
+
"lbfgs": "optimal",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#: Default parameters per method; ``step`` is ``mu`` (``mu_max`` for WF).
|
|
101
|
+
DEFAULT_OPTIONS: dict[str, dict[str, float]] = {
|
|
102
|
+
"wf": {"step": 0.1, "tau0": 330.0},
|
|
103
|
+
"twf": {"step": 0.2, "alpha_lb": 0.3, "alpha_ub": 5.0, "alpha_h": 5.0},
|
|
104
|
+
"taf": {"step": 1.0, "gamma": 0.7},
|
|
105
|
+
"raf": {"step": 4.0, "beta": 5.0},
|
|
106
|
+
"lbfgs": {"memory": 10},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# --------------------------------------------------------------------- result
|
|
111
|
+
@dataclass
|
|
112
|
+
class GenericResult:
|
|
113
|
+
"""Outcome of :func:`wirtinger`.
|
|
114
|
+
|
|
115
|
+
Attributes
|
|
116
|
+
----------
|
|
117
|
+
x:
|
|
118
|
+
Recovered vector, shape ``operator.in_shape`` (backend array), in the
|
|
119
|
+
units of the true signal (the global phase is arbitrary).
|
|
120
|
+
history:
|
|
121
|
+
Relative amplitude residual ``|| |A x| - sqrt(y) || / || sqrt(y) ||``
|
|
122
|
+
at the start and at every check.
|
|
123
|
+
times:
|
|
124
|
+
Wall-clock seconds since the call started, for each history entry
|
|
125
|
+
(the first entry includes the initialization).
|
|
126
|
+
n_iter:
|
|
127
|
+
Iterations performed.
|
|
128
|
+
converged:
|
|
129
|
+
Whether a tolerance (rather than the iteration limit) stopped the run.
|
|
130
|
+
message:
|
|
131
|
+
Why the run stopped.
|
|
132
|
+
elapsed:
|
|
133
|
+
Total wall-clock seconds, including the initialization.
|
|
134
|
+
device:
|
|
135
|
+
``"cpu"`` or ``"gpu"``.
|
|
136
|
+
method:
|
|
137
|
+
Algorithm used.
|
|
138
|
+
init:
|
|
139
|
+
Initialization used (``"given"`` for a user-supplied start).
|
|
140
|
+
options:
|
|
141
|
+
Method parameters used (``step`` and the method's options).
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
x: Any
|
|
145
|
+
history: list[float]
|
|
146
|
+
times: list[float]
|
|
147
|
+
n_iter: int
|
|
148
|
+
converged: bool
|
|
149
|
+
message: str
|
|
150
|
+
elapsed: float
|
|
151
|
+
device: str
|
|
152
|
+
method: str
|
|
153
|
+
init: str
|
|
154
|
+
options: dict[str, float] = field(default_factory=dict)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def residual(self) -> float:
|
|
158
|
+
"""Final relative amplitude residual."""
|
|
159
|
+
return self.history[-1]
|
|
160
|
+
|
|
161
|
+
def to_numpy(self) -> GenericResult:
|
|
162
|
+
"""A copy with :attr:`x` on the host as a NumPy array."""
|
|
163
|
+
return replace(self, x=to_numpy(self.x))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------- utilities
|
|
167
|
+
def relative_error(x: Any, truth: Any) -> float:
|
|
168
|
+
"""Distance to the truth modulo the global phase.
|
|
169
|
+
|
|
170
|
+
``min_phi ||x - exp(i phi) truth|| / ||truth||``, with the optimal phase
|
|
171
|
+
``exp(i phi) = truth^H x / |truth^H x|``. Works for NumPy or CuPy input
|
|
172
|
+
(computed on the device of ``x``).
|
|
173
|
+
"""
|
|
174
|
+
be = backend_of(x)
|
|
175
|
+
xp = be.xp
|
|
176
|
+
a = be.asarray(x, dtype="complex")
|
|
177
|
+
t = be.asarray(truth, dtype="complex")
|
|
178
|
+
c = complex(xp.sum(xp.conj(t) * a))
|
|
179
|
+
phase = c / abs(c) if c != 0 else 1.0
|
|
180
|
+
d = a - phase * t
|
|
181
|
+
return math.sqrt(be.dot(d, d) / max(be.dot(t, t), 1e-300))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _abs2(u: Any) -> Any:
|
|
185
|
+
return u.real * u.real + u.imag * u.imag
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class _Model:
|
|
189
|
+
"""The operator and data rescaled to the unit-variance Gaussian convention."""
|
|
190
|
+
|
|
191
|
+
def __init__(self, operator: LinearOperator, measurements: Any, magnitudes: bool) -> None:
|
|
192
|
+
if not isinstance(operator, LinearOperator):
|
|
193
|
+
raise TypeError(
|
|
194
|
+
f"operator must be a solvephase.operators.LinearOperator, got "
|
|
195
|
+
f"{type(operator).__name__}; wrap a dense matrix in MatrixOperator"
|
|
196
|
+
)
|
|
197
|
+
be = operator.backend
|
|
198
|
+
xp = be.xp
|
|
199
|
+
self.operator = operator
|
|
200
|
+
self.backend: Backend = be
|
|
201
|
+
if getattr(measurements, "dtype", np.dtype(float)).kind == "c":
|
|
202
|
+
raise ValueError(
|
|
203
|
+
"measurements must be real intensities (or magnitudes with magnitudes=True), "
|
|
204
|
+
"got a complex array"
|
|
205
|
+
)
|
|
206
|
+
data = be.asarray(measurements, dtype="real")
|
|
207
|
+
if tuple(data.shape) != tuple(operator.out_shape):
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"measurements have shape {tuple(data.shape)} but the operator produces "
|
|
210
|
+
f"{tuple(operator.out_shape)}"
|
|
211
|
+
)
|
|
212
|
+
if magnitudes:
|
|
213
|
+
psi = xp.abs(data)
|
|
214
|
+
y = psi * psi
|
|
215
|
+
else:
|
|
216
|
+
# Noise can make intensities negative; they carry no amplitude.
|
|
217
|
+
y = xp.maximum(data, 0)
|
|
218
|
+
psi = xp.sqrt(y)
|
|
219
|
+
self.m = operator.out_size
|
|
220
|
+
self.n = operator.in_size
|
|
221
|
+
gain = operator.frobenius_norm_squared() / self.n
|
|
222
|
+
if not gain > 0:
|
|
223
|
+
raise ValueError("the operator has zero Frobenius norm")
|
|
224
|
+
self.scale = self.m / gain
|
|
225
|
+
self.root = math.sqrt(self.scale)
|
|
226
|
+
self.y = y * self.scale
|
|
227
|
+
self.psi = psi * self.root
|
|
228
|
+
self.psi_norm = math.sqrt(be.dot(self.psi, self.psi))
|
|
229
|
+
if not self.psi_norm > 0:
|
|
230
|
+
raise ValueError("all measurements are zero: the signal is zero or unobservable")
|
|
231
|
+
# ||x||^2 estimate: E y_k = ||x||^2 in the Gaussian convention.
|
|
232
|
+
self.norm0 = self.psi_norm / math.sqrt(self.m)
|
|
233
|
+
self.tiny = float(np.finfo(be.real_dtype).tiny)
|
|
234
|
+
# Diagonal whitening G^{-1/2}, G = diag(A^H A)/m in the rescaled units (mean ~1).
|
|
235
|
+
self.whiten: Any = None
|
|
236
|
+
gram = operator.gram_diagonal()
|
|
237
|
+
if gram is not None:
|
|
238
|
+
gram = be.asarray(gram, dtype="real") * (self.scale / self.m)
|
|
239
|
+
if be.scalar(xp.min(gram)) > 0:
|
|
240
|
+
self.whiten = 1.0 / xp.sqrt(gram)
|
|
241
|
+
|
|
242
|
+
def forward(self, z: Any) -> Any:
|
|
243
|
+
return self.operator.forward(z) * self.root
|
|
244
|
+
|
|
245
|
+
def adjoint(self, v: Any) -> Any:
|
|
246
|
+
return self.operator.adjoint(v) * self.root
|
|
247
|
+
|
|
248
|
+
def residual_norm(self, u: Any) -> Any:
|
|
249
|
+
"""``|| |u| - psi ||`` (device 0-d array on the GPU, float on the CPU)."""
|
|
250
|
+
return _norm(self.backend, self.backend.xp.abs(u) - self.psi)
|
|
251
|
+
|
|
252
|
+
def random_vector(self, seed: Any) -> Any:
|
|
253
|
+
rng = self.backend.random(seed)
|
|
254
|
+
g = rng.standard_normal((2, *self.operator.in_shape))
|
|
255
|
+
v = self.backend.asarray(g[0] + 1j * g[1], dtype="complex")
|
|
256
|
+
return v / _norm(self.backend, v)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# --------------------------------------------------------- initializations
|
|
260
|
+
def _power(
|
|
261
|
+
model: _Model,
|
|
262
|
+
weights: Any,
|
|
263
|
+
*,
|
|
264
|
+
shift: float = 0.0,
|
|
265
|
+
iterations: int,
|
|
266
|
+
tol: float,
|
|
267
|
+
seed: Any,
|
|
268
|
+
check_every: int = 10,
|
|
269
|
+
) -> tuple[Any, float]:
|
|
270
|
+
"""Power method for ``M = (1/m) W A^H diag(weights) A W + shift I``.
|
|
271
|
+
|
|
272
|
+
``W = G^{-1/2}`` is the model's diagonal whitening (identity when the
|
|
273
|
+
operator does not report ``diag(A^H A)``). Returns the unit eigenvector
|
|
274
|
+
estimate (whitened coordinates) and its Rayleigh quotient.
|
|
275
|
+
"""
|
|
276
|
+
be = model.backend
|
|
277
|
+
wh = model.whiten
|
|
278
|
+
v = model.random_vector(seed)
|
|
279
|
+
inv_m = 1.0 / model.m
|
|
280
|
+
previous: float | None = None
|
|
281
|
+
value = 0.0
|
|
282
|
+
for it in range(1, iterations + 1):
|
|
283
|
+
w = model.adjoint(weights * model.forward(v if wh is None else wh * v)) * inv_m
|
|
284
|
+
if wh is not None:
|
|
285
|
+
w = wh * w
|
|
286
|
+
if shift:
|
|
287
|
+
w = w + shift * v
|
|
288
|
+
if it % check_every == 0 or it == iterations:
|
|
289
|
+
value = be.dot(v, w)
|
|
290
|
+
if previous is not None and abs(value - previous) <= tol * abs(value):
|
|
291
|
+
return w / _norm(be, w), value
|
|
292
|
+
previous = value
|
|
293
|
+
v = w / _norm(be, w)
|
|
294
|
+
return v, value
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _leading_eigenvector(
|
|
298
|
+
model: _Model,
|
|
299
|
+
weights: Any,
|
|
300
|
+
*,
|
|
301
|
+
shift: float = 0.0,
|
|
302
|
+
iterations: int,
|
|
303
|
+
tol: float,
|
|
304
|
+
seed: Any,
|
|
305
|
+
) -> Any:
|
|
306
|
+
"""Unit-norm leading eigenvector, mapped back from whitened coordinates."""
|
|
307
|
+
v, _ = _power(model, weights, shift=shift, iterations=iterations, tol=tol, seed=seed)
|
|
308
|
+
if model.whiten is not None:
|
|
309
|
+
v = v / model.whiten
|
|
310
|
+
v = v / _norm(model.backend, v)
|
|
311
|
+
return v
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _spectral(model: _Model, iterations: int, tol: float, seed: Any) -> Any:
|
|
315
|
+
v = _leading_eigenvector(model, model.y, iterations=iterations, tol=tol, seed=seed)
|
|
316
|
+
return v * model.norm0
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _truncated(model: _Model, iterations: int, tol: float, seed: Any, alpha_y: float = 3.0) -> Any:
|
|
320
|
+
y = model.y
|
|
321
|
+
weights = y * (y <= alpha_y**2 * model.norm0**2)
|
|
322
|
+
v = _leading_eigenvector(model, weights, iterations=iterations, tol=tol, seed=seed)
|
|
323
|
+
return v * model.norm0
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _top_fraction(model: _Model, fraction: float) -> Any:
|
|
327
|
+
"""Boolean mask of the ``ceil(fraction m)`` largest amplitudes."""
|
|
328
|
+
xp = model.backend.xp
|
|
329
|
+
if not 0.0 < fraction <= 1.0:
|
|
330
|
+
raise ValueError(f"fraction must be in (0, 1], got {fraction}")
|
|
331
|
+
k = max(1, min(model.m, math.ceil(fraction * model.m)))
|
|
332
|
+
flat = model.psi.ravel()
|
|
333
|
+
threshold = xp.partition(flat, model.m - k)[model.m - k]
|
|
334
|
+
return model.psi >= threshold
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _orthogonal(
|
|
338
|
+
model: _Model, iterations: int, tol: float, seed: Any, fraction: float = 1.0 / 6.0
|
|
339
|
+
) -> Any:
|
|
340
|
+
weights = _top_fraction(model, fraction).astype(model.y.dtype)
|
|
341
|
+
v = _leading_eigenvector(model, weights, iterations=iterations, tol=tol, seed=seed)
|
|
342
|
+
return v * model.norm0
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _weighted(
|
|
346
|
+
model: _Model,
|
|
347
|
+
iterations: int,
|
|
348
|
+
tol: float,
|
|
349
|
+
seed: Any,
|
|
350
|
+
fraction: float = 3.0 / 13.0,
|
|
351
|
+
exponent: float = 0.5,
|
|
352
|
+
) -> Any:
|
|
353
|
+
weights = _top_fraction(model, fraction) * model.psi**exponent
|
|
354
|
+
v = _leading_eigenvector(model, weights, iterations=iterations, tol=tol, seed=seed)
|
|
355
|
+
return v * model.norm0
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _optimal(model: _Model, iterations: int, tol: float, seed: Any) -> Any:
|
|
359
|
+
delta = model.m / model.n
|
|
360
|
+
if delta <= 1.0:
|
|
361
|
+
raise ValueError(
|
|
362
|
+
f"the optimal-preprocessing initialization needs more measurements than "
|
|
363
|
+
f"unknowns (m/n = {delta:.3g} <= 1); use init='spectral' or 'truncated'"
|
|
364
|
+
)
|
|
365
|
+
root_delta = math.sqrt(delta)
|
|
366
|
+
ybar = model.y / model.norm0**2
|
|
367
|
+
weights = (ybar - 1.0) / (ybar + (root_delta - 1.0))
|
|
368
|
+
# T >= -1/(sqrt(delta) - 1); shift by that times ||A W||^2/m so the matrix is PSD and
|
|
369
|
+
# the power method finds the largest algebraic eigenvalue.
|
|
370
|
+
_, gram_norm = _power(model, 1.0, iterations=100, tol=1e-4, seed=seed)
|
|
371
|
+
shift = 1.05 * gram_norm / (root_delta - 1.0)
|
|
372
|
+
v = _leading_eigenvector(model, weights, shift=shift, iterations=iterations, tol=tol, seed=seed)
|
|
373
|
+
return v * model.norm0
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _random(model: _Model, iterations: int, tol: float, seed: Any) -> Any:
|
|
377
|
+
return model.random_vector(seed) * model.norm0
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
_INIT_FUNCTIONS: dict[str, Callable[..., Any]] = {
|
|
381
|
+
"spectral": _spectral,
|
|
382
|
+
"truncated": _truncated,
|
|
383
|
+
"orthogonal": _orthogonal,
|
|
384
|
+
"weighted": _weighted,
|
|
385
|
+
"optimal": _optimal,
|
|
386
|
+
"random": _random,
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def init_spectral(
|
|
391
|
+
operator: LinearOperator,
|
|
392
|
+
measurements: Any,
|
|
393
|
+
*,
|
|
394
|
+
magnitudes: bool = False,
|
|
395
|
+
iterations: int = 100,
|
|
396
|
+
tol: float = 1e-6,
|
|
397
|
+
seed: Any = None,
|
|
398
|
+
) -> Any:
|
|
399
|
+
"""Spectral initialization (Netrapalli et al. 2013; Candès et al. 2015).
|
|
400
|
+
|
|
401
|
+
Leading eigenvector of ``Y = (1/m) A^H diag(y) A`` by the power method,
|
|
402
|
+
scaled to the norm estimate ``||x||^2 ~ mean(y)`` (in the unit-variance
|
|
403
|
+
convention; see the module docstring).
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
operator:
|
|
408
|
+
The measurement operator ``A``.
|
|
409
|
+
measurements:
|
|
410
|
+
Intensities ``y = |A x|^2`` (or magnitudes ``|A x|`` with
|
|
411
|
+
``magnitudes=True``), shape ``operator.out_shape``.
|
|
412
|
+
magnitudes:
|
|
413
|
+
Whether ``measurements`` are magnitudes rather than intensities.
|
|
414
|
+
iterations:
|
|
415
|
+
Maximum power iterations.
|
|
416
|
+
tol:
|
|
417
|
+
Relative change of the Rayleigh quotient that stops the power method.
|
|
418
|
+
seed:
|
|
419
|
+
Seed of the random power-method start (host RNG).
|
|
420
|
+
|
|
421
|
+
Returns
|
|
422
|
+
-------
|
|
423
|
+
array
|
|
424
|
+
Initial estimate of shape ``operator.in_shape`` on the operator's
|
|
425
|
+
backend.
|
|
426
|
+
"""
|
|
427
|
+
model = _Model(operator, measurements, magnitudes)
|
|
428
|
+
return _spectral(model, iterations, tol, seed)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def init_truncated_spectral(
|
|
432
|
+
operator: LinearOperator,
|
|
433
|
+
measurements: Any,
|
|
434
|
+
*,
|
|
435
|
+
magnitudes: bool = False,
|
|
436
|
+
alpha_y: float = 3.0,
|
|
437
|
+
iterations: int = 100,
|
|
438
|
+
tol: float = 1e-6,
|
|
439
|
+
seed: Any = None,
|
|
440
|
+
) -> Any:
|
|
441
|
+
"""Truncated spectral initialization (Chen & Candès 2017).
|
|
442
|
+
|
|
443
|
+
As :func:`init_spectral` but keeping only ``y_k <= alpha_y^2 mean(y)``,
|
|
444
|
+
which removes the heavy tail that biases the plain spectral estimate.
|
|
445
|
+
Parameters are those of :func:`init_spectral` plus ``alpha_y``
|
|
446
|
+
(dimensionless, default 3 as in the paper).
|
|
447
|
+
"""
|
|
448
|
+
model = _Model(operator, measurements, magnitudes)
|
|
449
|
+
return _truncated(model, iterations, tol, seed, alpha_y=alpha_y)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def init_orthogonality_promoting(
|
|
453
|
+
operator: LinearOperator,
|
|
454
|
+
measurements: Any,
|
|
455
|
+
*,
|
|
456
|
+
magnitudes: bool = False,
|
|
457
|
+
fraction: float = 1.0 / 6.0,
|
|
458
|
+
iterations: int = 100,
|
|
459
|
+
tol: float = 1e-6,
|
|
460
|
+
seed: Any = None,
|
|
461
|
+
) -> Any:
|
|
462
|
+
"""Orthogonality-promoting initialization (Wang, Giannakis & Eldar 2018).
|
|
463
|
+
|
|
464
|
+
Leading eigenvector of ``sum_{k in S} a_k a_k^*`` over the
|
|
465
|
+
``ceil(fraction m)`` largest amplitudes (the measurement vectors most
|
|
466
|
+
aligned with ``x``), scaled to ``sqrt(mean(y))``. Parameters are those of
|
|
467
|
+
:func:`init_spectral` plus ``fraction`` (default 1/6, as in TAF).
|
|
468
|
+
"""
|
|
469
|
+
model = _Model(operator, measurements, magnitudes)
|
|
470
|
+
return _orthogonal(model, iterations, tol, seed, fraction=fraction)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def init_weighted_correlation(
|
|
474
|
+
operator: LinearOperator,
|
|
475
|
+
measurements: Any,
|
|
476
|
+
*,
|
|
477
|
+
magnitudes: bool = False,
|
|
478
|
+
fraction: float = 3.0 / 13.0,
|
|
479
|
+
exponent: float = 0.5,
|
|
480
|
+
iterations: int = 100,
|
|
481
|
+
tol: float = 1e-6,
|
|
482
|
+
seed: Any = None,
|
|
483
|
+
) -> Any:
|
|
484
|
+
"""Weighted maximal correlation initialization (Wang, Giannakis, Saad & Chen 2018).
|
|
485
|
+
|
|
486
|
+
Leading eigenvector of ``sum_{k in S} psi_k^exponent a_k a_k^*`` over the
|
|
487
|
+
``ceil(fraction m)`` largest amplitudes ``psi = sqrt(y)``, scaled to
|
|
488
|
+
``sqrt(mean(y))``. Defaults (``3/13`` and ``1/2``) follow the RAF paper.
|
|
489
|
+
"""
|
|
490
|
+
model = _Model(operator, measurements, magnitudes)
|
|
491
|
+
return _weighted(model, iterations, tol, seed, fraction=fraction, exponent=exponent)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def init_optimal_spectral(
|
|
495
|
+
operator: LinearOperator,
|
|
496
|
+
measurements: Any,
|
|
497
|
+
*,
|
|
498
|
+
magnitudes: bool = False,
|
|
499
|
+
iterations: int = 200,
|
|
500
|
+
tol: float = 1e-6,
|
|
501
|
+
seed: Any = None,
|
|
502
|
+
) -> Any:
|
|
503
|
+
"""Spectral initialization with optimal preprocessing (Luo, Alghamdi & Lu 2019).
|
|
504
|
+
|
|
505
|
+
Leading (largest algebraic) eigenvector of ``(1/m) A^H diag(T(y)) A`` with
|
|
506
|
+
``T(y) = (y - 1) / (y + sqrt(delta) - 1)``, ``y`` normalized to unit mean
|
|
507
|
+
and ``delta = m / n > 1``. ``T`` is negative for small ``y``, so the power
|
|
508
|
+
method runs on the matrix shifted by ``||A||^2 / (m (sqrt(delta) - 1))``
|
|
509
|
+
(``||A||`` from :meth:`~solvephase.operators.LinearOperator.norm_estimate`),
|
|
510
|
+
which slows it; the default ``iterations`` is therefore larger. Asymptotically
|
|
511
|
+
this weighting minimizes the number of measurements needed for a
|
|
512
|
+
non-trivial correlation with ``x``.
|
|
513
|
+
"""
|
|
514
|
+
model = _Model(operator, measurements, magnitudes)
|
|
515
|
+
return _optimal(model, iterations, tol, seed)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def init_random(
|
|
519
|
+
operator: LinearOperator,
|
|
520
|
+
measurements: Any,
|
|
521
|
+
*,
|
|
522
|
+
magnitudes: bool = False,
|
|
523
|
+
seed: Any = None,
|
|
524
|
+
) -> Any:
|
|
525
|
+
"""Random complex Gaussian start scaled to the norm estimate ``sqrt(mean(y))``."""
|
|
526
|
+
model = _Model(operator, measurements, magnitudes)
|
|
527
|
+
return _random(model, 0, 0.0, seed)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
# --------------------------------------------------------------- iterations
|
|
531
|
+
def _wf_step(model: _Model, opts: dict[str, float], norm0_sq: float) -> Callable[..., Any]:
|
|
532
|
+
mu_max, tau0 = opts["step"], opts["tau0"]
|
|
533
|
+
scale = 1.0 / (model.m * norm0_sq)
|
|
534
|
+
|
|
535
|
+
def step(it: int, z: Any, u: Any) -> Any:
|
|
536
|
+
mu = min(1.0 - math.exp(-it / tau0), mu_max)
|
|
537
|
+
return z - (mu * scale) * model.adjoint((_abs2(u) - model.y) * u)
|
|
538
|
+
|
|
539
|
+
return step
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _twf_step(model: _Model, opts: dict[str, float]) -> Callable[..., Any]:
|
|
543
|
+
xp = model.backend.xp
|
|
544
|
+
mu, lb, ub, ah = opts["step"], opts["alpha_lb"], opts["alpha_ub"], opts["alpha_h"]
|
|
545
|
+
factor = 2.0 * mu / model.m
|
|
546
|
+
|
|
547
|
+
def step(it: int, z: Any, u: Any) -> Any:
|
|
548
|
+
au2 = _abs2(u)
|
|
549
|
+
au = xp.sqrt(au2)
|
|
550
|
+
r = model.y - au2
|
|
551
|
+
ar = xp.abs(r)
|
|
552
|
+
ratio = au / _norm(model.backend, z)
|
|
553
|
+
keep = (ratio >= lb) & (ratio <= ub) & (ar <= (ah * xp.mean(ar)) * ratio)
|
|
554
|
+
g = xp.where(keep, r / xp.maximum(au2, model.tiny), 0) * u
|
|
555
|
+
return z + factor * model.adjoint(g)
|
|
556
|
+
|
|
557
|
+
return step
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _amplitude_residual(model: _Model, u: Any) -> tuple[Any, Any]:
|
|
561
|
+
"""``(|u|, u - psi u / |u|)``."""
|
|
562
|
+
xp = model.backend.xp
|
|
563
|
+
au = xp.abs(u)
|
|
564
|
+
return au, u - (model.psi / xp.maximum(au, model.tiny)) * u
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _taf_step(model: _Model, opts: dict[str, float]) -> Callable[..., Any]:
|
|
568
|
+
xp = model.backend.xp
|
|
569
|
+
factor = opts["step"] / model.m
|
|
570
|
+
gamma1 = 1.0 + opts["gamma"]
|
|
571
|
+
|
|
572
|
+
def step(it: int, z: Any, u: Any) -> Any:
|
|
573
|
+
au, v = _amplitude_residual(model, u)
|
|
574
|
+
return z - factor * model.adjoint(xp.where(au * gamma1 >= model.psi, v, 0))
|
|
575
|
+
|
|
576
|
+
return step
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _raf_step(model: _Model, opts: dict[str, float]) -> Callable[..., Any]:
|
|
580
|
+
xp = model.backend.xp
|
|
581
|
+
factor = opts["step"] / model.m
|
|
582
|
+
beta = opts["beta"]
|
|
583
|
+
|
|
584
|
+
def step(it: int, z: Any, u: Any) -> Any:
|
|
585
|
+
au, v = _amplitude_residual(model, u)
|
|
586
|
+
w = au / xp.maximum(au + beta * model.psi, model.tiny)
|
|
587
|
+
return z - factor * model.adjoint(w * v)
|
|
588
|
+
|
|
589
|
+
return step
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
class _Monitor:
|
|
593
|
+
"""Shared stopping rules, history and callback handling.
|
|
594
|
+
|
|
595
|
+
Two tests run at every check: the relative amplitude residual ``r`` below
|
|
596
|
+
``tol`` (an exact fit), and its relative change since the previous check
|
|
597
|
+
below ``tol`` (a stationary point: near a minimizer with residual ``r*``
|
|
598
|
+
the change of ``r`` is quadratic in the distance to it, so this stops only
|
|
599
|
+
once the remaining optimization error is far below the noise-induced one,
|
|
600
|
+
and never during geometric convergence to an exact fit).
|
|
601
|
+
"""
|
|
602
|
+
|
|
603
|
+
def __init__(
|
|
604
|
+
self, model: _Model, tol: float, callback: Callback | None, t0: float, u0: Any
|
|
605
|
+
) -> None:
|
|
606
|
+
self.model = model
|
|
607
|
+
self.tol = max(tol, 8.0 * model.backend.eps)
|
|
608
|
+
self.callback = callback
|
|
609
|
+
self.t0 = t0
|
|
610
|
+
self.history = [float(model.residual_norm(u0)) / model.psi_norm]
|
|
611
|
+
self.times = [time.perf_counter() - t0]
|
|
612
|
+
self.message = "iteration limit reached"
|
|
613
|
+
self.converged = False
|
|
614
|
+
|
|
615
|
+
def check(self, it: int, z: Any, residual_norm: Any, *, final: bool = False) -> bool:
|
|
616
|
+
"""Record a check; return True to stop.
|
|
617
|
+
|
|
618
|
+
``final`` records the end state of a run that stopped by itself and
|
|
619
|
+
applies only the exact-fit test.
|
|
620
|
+
"""
|
|
621
|
+
r = float(residual_norm) / self.model.psi_norm
|
|
622
|
+
previous = self.history[-1]
|
|
623
|
+
self.history.append(r)
|
|
624
|
+
self.times.append(time.perf_counter() - self.t0)
|
|
625
|
+
if not math.isfinite(r):
|
|
626
|
+
self.message = "diverged (non-finite residual); reduce the step size"
|
|
627
|
+
return True
|
|
628
|
+
if not final and self.callback is not None and self.callback(it, z, r):
|
|
629
|
+
self.message, self.converged = "stopped by callback", True
|
|
630
|
+
return True
|
|
631
|
+
if r <= self.tol:
|
|
632
|
+
self.message, self.converged = "relative residual below tol", True
|
|
633
|
+
return True
|
|
634
|
+
if not final and abs(previous - r) <= self.tol * previous:
|
|
635
|
+
self.message, self.converged = "residual stagnated (stationary point)", True
|
|
636
|
+
return True
|
|
637
|
+
return False
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _iterate(
|
|
641
|
+
model: _Model,
|
|
642
|
+
z: Any,
|
|
643
|
+
step: Callable[..., Any],
|
|
644
|
+
monitor: _Monitor,
|
|
645
|
+
iterations: int,
|
|
646
|
+
check_every: int,
|
|
647
|
+
) -> tuple[Any, int]:
|
|
648
|
+
u = model.forward(z)
|
|
649
|
+
it = 0
|
|
650
|
+
for it in range(1, iterations + 1):
|
|
651
|
+
z = step(it, z, u)
|
|
652
|
+
u = model.forward(z)
|
|
653
|
+
if (it % check_every == 0 or it == iterations) and monitor.check(
|
|
654
|
+
it, z, model.residual_norm(u)
|
|
655
|
+
):
|
|
656
|
+
break
|
|
657
|
+
return z, it
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _run_lbfgs(
|
|
661
|
+
model: _Model,
|
|
662
|
+
z0: Any,
|
|
663
|
+
monitor: _Monitor,
|
|
664
|
+
iterations: int,
|
|
665
|
+
check_every: int,
|
|
666
|
+
loss: str,
|
|
667
|
+
memory: int,
|
|
668
|
+
) -> tuple[Any, int]:
|
|
669
|
+
be = model.backend
|
|
670
|
+
xp = be.xp
|
|
671
|
+
cdt, rdt = be.complex_dtype, be.real_dtype
|
|
672
|
+
inv_m = 1.0 / model.m
|
|
673
|
+
amplitude = loss == "amplitude"
|
|
674
|
+
|
|
675
|
+
def fun(xr: Any) -> tuple[float, Any]:
|
|
676
|
+
z = xr.view(cdt)
|
|
677
|
+
u = model.forward(z)
|
|
678
|
+
if amplitude:
|
|
679
|
+
au, v = _amplitude_residual(model, u)
|
|
680
|
+
d = au - model.psi
|
|
681
|
+
f = (0.5 * inv_m) * be.dot(d, d)
|
|
682
|
+
g = model.adjoint(v) * inv_m
|
|
683
|
+
else:
|
|
684
|
+
d = _abs2(u) - model.y
|
|
685
|
+
f = (0.25 * inv_m) * be.dot(d, d)
|
|
686
|
+
g = model.adjoint(d * u) * inv_m
|
|
687
|
+
return f, xp.ascontiguousarray(g).view(rdt)
|
|
688
|
+
|
|
689
|
+
def check(it: int, xr: Any, f: float) -> bool:
|
|
690
|
+
if it % check_every:
|
|
691
|
+
return False
|
|
692
|
+
z = xr.view(cdt)
|
|
693
|
+
if amplitude:
|
|
694
|
+
res = math.sqrt(max(2.0 * model.m * f, 0.0))
|
|
695
|
+
else:
|
|
696
|
+
res = model.residual_norm(model.forward(z))
|
|
697
|
+
return monitor.check(it, z, res)
|
|
698
|
+
|
|
699
|
+
x0 = xp.ascontiguousarray(z0).view(rdt)
|
|
700
|
+
out = lbfgs(
|
|
701
|
+
fun,
|
|
702
|
+
x0,
|
|
703
|
+
be,
|
|
704
|
+
max_iter=iterations,
|
|
705
|
+
memory=memory,
|
|
706
|
+
ftol=be.eps,
|
|
707
|
+
gtol=0.0,
|
|
708
|
+
callback=check,
|
|
709
|
+
)
|
|
710
|
+
z = out.x.view(cdt)
|
|
711
|
+
if out.message != "stopped by callback":
|
|
712
|
+
# The optimizer stopped by itself; record the final state.
|
|
713
|
+
monitor.check(out.n_iter, z, model.residual_norm(model.forward(z)), final=True)
|
|
714
|
+
if out.message != "iteration limit reached" and not monitor.converged:
|
|
715
|
+
monitor.message, monitor.converged = out.message, out.converged
|
|
716
|
+
return z, out.n_iter
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _options(method: str, options: dict[str, float] | None, step: float | None) -> dict[str, float]:
|
|
720
|
+
opts = dict(DEFAULT_OPTIONS[method])
|
|
721
|
+
for key, value in (options or {}).items():
|
|
722
|
+
if key not in opts:
|
|
723
|
+
raise ValueError(
|
|
724
|
+
f"unknown option {key!r} for method {method!r}; valid options: {sorted(opts)}"
|
|
725
|
+
)
|
|
726
|
+
opts[key] = float(value)
|
|
727
|
+
if step is not None:
|
|
728
|
+
if method == "lbfgs":
|
|
729
|
+
raise ValueError("method='lbfgs' chooses its steps by line search; omit step")
|
|
730
|
+
opts["step"] = float(step)
|
|
731
|
+
return opts
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def wirtinger(
|
|
735
|
+
operator: LinearOperator,
|
|
736
|
+
measurements: Any,
|
|
737
|
+
*,
|
|
738
|
+
method: str = "raf",
|
|
739
|
+
init: Any = "auto",
|
|
740
|
+
magnitudes: bool = False,
|
|
741
|
+
iterations: int = 1000,
|
|
742
|
+
tol: float = 1e-7,
|
|
743
|
+
check_every: int = 10,
|
|
744
|
+
step: float | None = None,
|
|
745
|
+
loss: str = "amplitude",
|
|
746
|
+
options: dict[str, float] | None = None,
|
|
747
|
+
power_iterations: int | None = None,
|
|
748
|
+
seed: Any = None,
|
|
749
|
+
callback: Callback | None = None,
|
|
750
|
+
) -> GenericResult:
|
|
751
|
+
"""Recover ``x`` from ``y = |A x|^2`` with a Wirtinger-flow-family solver.
|
|
752
|
+
|
|
753
|
+
Parameters
|
|
754
|
+
----------
|
|
755
|
+
operator:
|
|
756
|
+
Measurement operator ``A`` (:mod:`solvephase.operators`); its backend
|
|
757
|
+
sets the device and precision.
|
|
758
|
+
measurements:
|
|
759
|
+
Intensities ``y = |A x|^2``, shape ``operator.out_shape`` (host or
|
|
760
|
+
device array). Negative values (noise) are clipped to zero.
|
|
761
|
+
method:
|
|
762
|
+
``"raf"`` (Reweighted Amplitude Flow, default), ``"taf"`` (Truncated
|
|
763
|
+
Amplitude Flow), ``"twf"`` (Truncated Wirtinger Flow), ``"wf"``
|
|
764
|
+
(Wirtinger Flow) or ``"lbfgs"`` (L-BFGS on the ``loss``).
|
|
765
|
+
init:
|
|
766
|
+
``"auto"`` (the method's own initialization, see
|
|
767
|
+
:data:`DEFAULT_INIT`), ``"spectral"``, ``"truncated"``,
|
|
768
|
+
``"orthogonal"``, ``"weighted"``, ``"optimal"``, ``"random"``, or an
|
|
769
|
+
array of shape ``operator.in_shape`` to start from.
|
|
770
|
+
magnitudes:
|
|
771
|
+
Whether ``measurements`` are magnitudes ``|A x|`` instead of
|
|
772
|
+
intensities.
|
|
773
|
+
iterations:
|
|
774
|
+
Maximum iterations (each costs one ``forward`` and one ``adjoint``;
|
|
775
|
+
an L-BFGS iteration may cost more through its line search).
|
|
776
|
+
tol:
|
|
777
|
+
Stop when the relative amplitude residual falls below ``tol`` (exact
|
|
778
|
+
fit), or changes by less than ``tol`` (relative) between checks
|
|
779
|
+
(stationary point, e.g. with noisy data). It is raised to ``8 eps``
|
|
780
|
+
of the working precision (about ``1e-6`` in single precision).
|
|
781
|
+
check_every:
|
|
782
|
+
Iterations between convergence checks (the only host syncs).
|
|
783
|
+
step:
|
|
784
|
+
Step size ``mu`` overriding the method default (``mu_max`` for WF).
|
|
785
|
+
loss:
|
|
786
|
+
``"amplitude"`` or ``"intensity"`` least squares, for
|
|
787
|
+
``method="lbfgs"``.
|
|
788
|
+
options:
|
|
789
|
+
Method parameters overriding :data:`DEFAULT_OPTIONS`: ``tau0`` (WF);
|
|
790
|
+
``alpha_lb``, ``alpha_ub``, ``alpha_h`` (TWF); ``gamma`` (TAF);
|
|
791
|
+
``beta`` (RAF); ``memory`` (L-BFGS).
|
|
792
|
+
power_iterations:
|
|
793
|
+
Power-method iterations of the initialization (default 100, 200 for
|
|
794
|
+
``"optimal"``).
|
|
795
|
+
seed:
|
|
796
|
+
Seed for the initialization (host RNG; CPU and GPU agree).
|
|
797
|
+
callback:
|
|
798
|
+
``callback(iteration, x, residual)`` at each check; returning
|
|
799
|
+
``True`` stops the run.
|
|
800
|
+
|
|
801
|
+
Returns
|
|
802
|
+
-------
|
|
803
|
+
GenericResult
|
|
804
|
+
The estimate (global phase arbitrary; compare with
|
|
805
|
+
:func:`relative_error`) and the residual history.
|
|
806
|
+
"""
|
|
807
|
+
t0 = time.perf_counter()
|
|
808
|
+
method = str(method).lower()
|
|
809
|
+
if method not in METHODS:
|
|
810
|
+
raise ValueError(f"method must be one of {METHODS}, got {method!r}")
|
|
811
|
+
if loss not in ("amplitude", "intensity"):
|
|
812
|
+
raise ValueError(f"loss must be 'amplitude' or 'intensity', got {loss!r}")
|
|
813
|
+
if iterations < 0 or check_every < 1:
|
|
814
|
+
raise ValueError("iterations must be >= 0 and check_every >= 1")
|
|
815
|
+
opts = _options(method, options, step)
|
|
816
|
+
model = _Model(operator, measurements, magnitudes)
|
|
817
|
+
be = model.backend
|
|
818
|
+
xp = be.xp
|
|
819
|
+
|
|
820
|
+
if isinstance(init, str):
|
|
821
|
+
name = DEFAULT_INIT[method] if init == "auto" else init
|
|
822
|
+
if name not in _INIT_FUNCTIONS:
|
|
823
|
+
raise ValueError(f"init must be 'auto', one of {INITS}, or an array; got {init!r}")
|
|
824
|
+
n_power = power_iterations or (200 if name == "optimal" else 100)
|
|
825
|
+
z = _INIT_FUNCTIONS[name](model, n_power, 1e-6, seed)
|
|
826
|
+
else:
|
|
827
|
+
name = "given"
|
|
828
|
+
z = be.asarray(init, dtype="complex")
|
|
829
|
+
if tuple(z.shape) != tuple(operator.in_shape):
|
|
830
|
+
raise ValueError(
|
|
831
|
+
f"init has shape {tuple(z.shape)} but the operator expects "
|
|
832
|
+
f"{tuple(operator.in_shape)}"
|
|
833
|
+
)
|
|
834
|
+
z = xp.ascontiguousarray(z)
|
|
835
|
+
|
|
836
|
+
monitor = _Monitor(model, tol, callback, t0, model.forward(z))
|
|
837
|
+
if method == "lbfgs":
|
|
838
|
+
z, n_iter = _run_lbfgs(
|
|
839
|
+
model, z, monitor, iterations, check_every, loss, int(opts["memory"])
|
|
840
|
+
)
|
|
841
|
+
else:
|
|
842
|
+
if method == "wf":
|
|
843
|
+
norm0_sq = be.dot(z, z) or model.norm0**2
|
|
844
|
+
stepper = _wf_step(model, opts, norm0_sq)
|
|
845
|
+
elif method == "twf":
|
|
846
|
+
stepper = _twf_step(model, opts)
|
|
847
|
+
elif method == "taf":
|
|
848
|
+
stepper = _taf_step(model, opts)
|
|
849
|
+
else:
|
|
850
|
+
stepper = _raf_step(model, opts)
|
|
851
|
+
z, n_iter = _iterate(model, z, stepper, monitor, iterations, check_every)
|
|
852
|
+
be.synchronize()
|
|
853
|
+
return GenericResult(
|
|
854
|
+
x=z,
|
|
855
|
+
history=monitor.history,
|
|
856
|
+
times=monitor.times,
|
|
857
|
+
n_iter=n_iter,
|
|
858
|
+
converged=monitor.converged,
|
|
859
|
+
message=monitor.message,
|
|
860
|
+
elapsed=time.perf_counter() - t0,
|
|
861
|
+
device=be.device,
|
|
862
|
+
method=method,
|
|
863
|
+
init=name,
|
|
864
|
+
options=opts,
|
|
865
|
+
)
|