qoraphase 0.1.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.
Files changed (48) hide show
  1. qoraphase-0.1.0/.gitignore +13 -0
  2. qoraphase-0.1.0/CITATION.cff +20 -0
  3. qoraphase-0.1.0/LICENSE +21 -0
  4. qoraphase-0.1.0/PKG-INFO +193 -0
  5. qoraphase-0.1.0/README.md +168 -0
  6. qoraphase-0.1.0/examples/diagnose.py +140 -0
  7. qoraphase-0.1.0/pyproject.toml +47 -0
  8. qoraphase-0.1.0/src/qoraphase/__init__.py +48 -0
  9. qoraphase-0.1.0/src/qoraphase/algorithms/__init__.py +0 -0
  10. qoraphase-0.1.0/src/qoraphase/algorithms/er.py +22 -0
  11. qoraphase-0.1.0/src/qoraphase/algorithms/hio.py +24 -0
  12. qoraphase-0.1.0/src/qoraphase/algorithms/raar.py +31 -0
  13. qoraphase-0.1.0/src/qoraphase/api.py +110 -0
  14. qoraphase-0.1.0/src/qoraphase/core/__init__.py +0 -0
  15. qoraphase-0.1.0/src/qoraphase/core/ensemble.py +77 -0
  16. qoraphase-0.1.0/src/qoraphase/core/problem.py +94 -0
  17. qoraphase-0.1.0/src/qoraphase/core/result.py +55 -0
  18. qoraphase-0.1.0/src/qoraphase/core/state.py +40 -0
  19. qoraphase-0.1.0/src/qoraphase/core/validate.py +47 -0
  20. qoraphase-0.1.0/src/qoraphase/diagnostics.py +200 -0
  21. qoraphase-0.1.0/src/qoraphase/metrics/__init__.py +0 -0
  22. qoraphase-0.1.0/src/qoraphase/metrics/errors.py +24 -0
  23. qoraphase-0.1.0/src/qoraphase/models/__init__.py +0 -0
  24. qoraphase-0.1.0/src/qoraphase/models/fft.py +24 -0
  25. qoraphase-0.1.0/src/qoraphase/operators/__init__.py +0 -0
  26. qoraphase-0.1.0/src/qoraphase/operators/modulus.py +34 -0
  27. qoraphase-0.1.0/src/qoraphase/operators/shrinkwrap.py +77 -0
  28. qoraphase-0.1.0/src/qoraphase/operators/support.py +51 -0
  29. qoraphase-0.1.0/src/qoraphase/postprocess.py +66 -0
  30. qoraphase-0.1.0/src/qoraphase/recipes/__init__.py +0 -0
  31. qoraphase-0.1.0/src/qoraphase/recipes/recipe.py +119 -0
  32. qoraphase-0.1.0/tests/conftest.py +102 -0
  33. qoraphase-0.1.0/tests/test_algorithms.py +137 -0
  34. qoraphase-0.1.0/tests/test_api.py +97 -0
  35. qoraphase-0.1.0/tests/test_detwin.py +178 -0
  36. qoraphase-0.1.0/tests/test_diagnostics.py +147 -0
  37. qoraphase-0.1.0/tests/test_ensemble.py +42 -0
  38. qoraphase-0.1.0/tests/test_fft.py +49 -0
  39. qoraphase-0.1.0/tests/test_forward_model.py +130 -0
  40. qoraphase-0.1.0/tests/test_jax_transforms.py +95 -0
  41. qoraphase-0.1.0/tests/test_metrics.py +36 -0
  42. qoraphase-0.1.0/tests/test_postprocess.py +121 -0
  43. qoraphase-0.1.0/tests/test_projections.py +82 -0
  44. qoraphase-0.1.0/tests/test_public_handles.py +52 -0
  45. qoraphase-0.1.0/tests/test_recipe.py +49 -0
  46. qoraphase-0.1.0/tests/test_shapes.py +36 -0
  47. qoraphase-0.1.0/tests/test_shrinkwrap.py +107 -0
  48. qoraphase-0.1.0/tests/test_validate.py +62 -0
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Design specs (local only)
13
+ QoraPhase 설계 명세서*.md
@@ -0,0 +1,20 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ type: software
4
+ title: "QoraPhase: A JAX-native, composable phase-retrieval framework for coherent diffraction imaging"
5
+ authors:
6
+ - family-names: Yong
7
+ given-names: Isaac
8
+ email: isaacyong@naver.com
9
+ affiliation: "Sogang University"
10
+ license: MIT
11
+ repository-code: "https://github.com/NaNIsaac/QoraPhase"
12
+ abstract: "A JAX-native, composable phase-retrieval framework for coherent diffraction imaging: an intensity array goes in, a reconstruction object comes out."
13
+ keywords:
14
+ - "phase retrieval"
15
+ - "coherent diffraction imaging"
16
+ - "CDI"
17
+ - "JAX"
18
+ - "X-ray"
19
+ version: "0.1.0"
20
+ date-released: "2026-08-28"
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Isaac Yong
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.
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.5
2
+ Name: qoraphase
3
+ Version: 0.1.0
4
+ Summary: A JAX-native, composable phase-retrieval framework for coherent diffraction imaging
5
+ Project-URL: Homepage, https://github.com/NaNIsaac/QoraPhase
6
+ Project-URL: Repository, https://github.com/NaNIsaac/QoraPhase
7
+ Project-URL: Issues, https://github.com/NaNIsaac/QoraPhase/issues
8
+ Author-email: Isaac Yong <isaacyong@naver.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: CDI,coherent diffraction imaging,jax,phase retrieval,x-ray imaging
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
19
+ Classifier: Topic :: Scientific/Engineering :: Physics
20
+ Requires-Python: >=3.12
21
+ Requires-Dist: jax>=0.11.1
22
+ Requires-Dist: matplotlib>=3.11.1
23
+ Requires-Dist: numpy>=2.5.2
24
+ Description-Content-Type: text/markdown
25
+
26
+ # QoraPhase
27
+
28
+ A JAX-native, composable phase-retrieval framework for coherent diffraction imaging.
29
+
30
+ **Array in → reconstruction out.** QoraPhase does not read files. You bring a
31
+ `numpy` or `jax` array; it gives you back an object.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install qoraphase
37
+ ```
38
+
39
+ ## Quick start
40
+
41
+ ```python
42
+ import qoraphase as qp
43
+
44
+ result = qp.reconstruct(intensity, support=support)
45
+ result.plot()
46
+ ```
47
+
48
+ ## Array conventions (read this first)
49
+
50
+ These three are the source of nearly every phase-retrieval bug, so they are part
51
+ of the API contract, not an implementation detail.
52
+
53
+ | | |
54
+ |---|---|
55
+ | **Origin** | Every public array is **center-origin**: index `n // 2` is q=0. `ifftshift` is internal; never call it yourself. |
56
+ | **`intensity`** | A measured **intensity**, not an amplitude. Internally `sqrt(clip(intensity, 0))`. |
57
+ | **`mask`** | `True` marks an **unusable** pixel (beamstop, hot, saturated). Masked pixels are left unconstrained — never forced to zero. |
58
+ | **`support`** | `True` is inside the object. Omit it and an autocorrelation support is derived (loose; pass a real one when you have it). |
59
+
60
+ ## Composing a reconstruction
61
+
62
+ Code order is execution order. There is no operator-overloading DSL.
63
+
64
+ ```python
65
+ problem = qp.CDI(intensity=intensity, support=support, mask=mask)
66
+
67
+ recipe = qp.Recipe(
68
+ qp.repeat(10, qp.RAAR(50, beta=0.9), qp.Shrinkwrap()),
69
+ qp.ER(200),
70
+ )
71
+
72
+ result = problem.reconstruct(recipe)
73
+ ```
74
+
75
+ The first positional argument of an algorithm is always its iteration count.
76
+
77
+ `qp.detwin(...)` runs its body against a support cut in half at the object's
78
+ centre of mass, then restores the full support. That cut is not centrosymmetric,
79
+ so it fits only one of the two conjugate solutions and biases the run into a
80
+ branch. The default recipe places it a quarter of the way through the RAAR
81
+ cycles, where PyNX puts it. It is measurably useful when the support is
82
+ symmetric and the run would otherwise stall, and harmless when it is not — but
83
+ a support with the right shape is worth far more than any detwinning.
84
+
85
+ `qp.Shrinkwrap(threshold, sigma, method)` takes `method="max"` (classic, relative
86
+ to the peak amplitude) or `method="rms"` (relative to the rms inside the current
87
+ support, less easily thrown off by a hot pixel). The thresholds are on different
88
+ scales: the classic `"max"` value is 0.2, PyNX uses 0.28 for `"rms"`.
89
+
90
+ The defaults are `0.3, 1.0` rather than the classic `0.2, 2.0`. Starting from an
91
+ autocorrelation support — the case that actually needs shrinkwrap — 0.2 / 2.0
92
+ never converged in testing and 0.3 / 1.0 did, on every object tried. That is a
93
+ measurement on synthetic data, not a law. **These two numbers are the first thing
94
+ to re-tune on a real dataset.**
95
+
96
+ ## Multi-start
97
+
98
+ Multi-start is a core feature, not an add-on — for a support with awkward
99
+ symmetry it is the difference between converging and not.
100
+
101
+ ```python
102
+ ensemble = problem.reconstruct(recipe, starts=32, batch=4)
103
+ best = ensemble.best
104
+ ```
105
+
106
+ `vmap` holds every start in memory at once. `batch` chunks that: lower it if a
107
+ large 3D volume runs out of GPU memory.
108
+
109
+ ## Swapping the forward model
110
+
111
+ Algorithms never name an FFT, so the physics is replaceable without touching
112
+ them:
113
+
114
+ ```python
115
+ qp.CDI(intensity, support=support, model=MyPropagator())
116
+ ```
117
+
118
+ A model needs `forward` / `adjoint` and should be unitary. A *wrong* model fails
119
+ silently — it can fit the data perfectly and return the wrong object — so this
120
+ is worth getting right. See `tests/test_forward_model.py`.
121
+
122
+ ## Before you trust a reconstruction
123
+
124
+ Check the measurement against the assumptions the library makes about it:
125
+
126
+ ```python
127
+ print(qp.diagnose(intensity, mask=mask, support=support))
128
+ ```
129
+
130
+ It reports the centring offset two ways (brightest pixel, and Friedel symmetry,
131
+ which resolves half a pixel and works on patterns with no dominant peak), how
132
+ much of the array the autocorrelation fills, the negative and masked fractions,
133
+ and warns about each. An off-centre pattern is the failure that costs the most
134
+ and shows the least.
135
+
136
+ `examples/diagnose.py` does the same from the command line, then reconstructs
137
+ and reports whether independent starts agree:
138
+
139
+ ```
140
+ python examples/diagnose.py scan.npz --key intensity --support sup.npy --starts 8
141
+ ```
142
+
143
+ A low Fourier error says a run fits the data. Agreement between starts, after
144
+ alignment, is what says the answer is determined — they are different claims and
145
+ only the second one is worth much.
146
+
147
+ ## Combining runs
148
+
149
+ A reconstruction is determined only up to a global phase, a translation, and a
150
+ conjugate flip. No iteration can remove those — they are exact symmetries of the
151
+ constraints — so they are resolved afterwards:
152
+
153
+ ```python
154
+ aligned = ensemble.align() # every run brought onto the best one
155
+ mean = ensemble.average() # coherent mean of the aligned runs
156
+
157
+ ```
158
+
159
+ **Never average runs without aligning them.** Each carries its own global phase,
160
+ so a raw mean interferes destructively — in practice it throws away about two
161
+ thirds of the amplitude. The direction survives, so a similarity score will not
162
+ warn you; only the amplitude shows it.
163
+
164
+ `align()` picks a *convention*, not the truth. It adopts whichever conjugate
165
+ branch the best run happens to be in — enough to combine runs, and no claim
166
+ about which branch is real. Nothing in the data distinguishes them: `conj(obj(-r))`
167
+ reproduces the measured intensity exactly. What breaks the tie is the support
168
+ you supply. Hand the solver a reflected support and every start converges just
169
+ as cleanly on the mirrored object, with a flawless error metric and no warning.
170
+ The handedness of your support is an assumption, not a result.
171
+
172
+ Alignment works to whole pixels.
173
+
174
+ `result.phase` still carries whatever linear phase the measurement geometry put
175
+ there, and QoraPhase does not remove it. Doing that properly needs the
176
+ diffraction geometry, which arrives with BCDI in v0.3; short of that, any
177
+ "correction" is a guess.
178
+
179
+ If the twin branch matters to you, break the symmetry during the run rather than
180
+ after it — see `qp.detwin` below.
181
+
182
+ ## What this does not do
183
+
184
+ - No phase-ramp removal, no sub-pixel alignment, no clustering, no PRTF.
185
+ - No positivity/reality constraint — the object is complex here.
186
+ - No file I/O, no BCDI geometry, no NUFFT, no strain, no autodiff optimisers.
187
+
188
+ ## Development
189
+
190
+ ```bash
191
+ uv sync --all-groups
192
+ uv run pytest
193
+ ```
@@ -0,0 +1,168 @@
1
+ # QoraPhase
2
+
3
+ A JAX-native, composable phase-retrieval framework for coherent diffraction imaging.
4
+
5
+ **Array in → reconstruction out.** QoraPhase does not read files. You bring a
6
+ `numpy` or `jax` array; it gives you back an object.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install qoraphase
12
+ ```
13
+
14
+ ## Quick start
15
+
16
+ ```python
17
+ import qoraphase as qp
18
+
19
+ result = qp.reconstruct(intensity, support=support)
20
+ result.plot()
21
+ ```
22
+
23
+ ## Array conventions (read this first)
24
+
25
+ These three are the source of nearly every phase-retrieval bug, so they are part
26
+ of the API contract, not an implementation detail.
27
+
28
+ | | |
29
+ |---|---|
30
+ | **Origin** | Every public array is **center-origin**: index `n // 2` is q=0. `ifftshift` is internal; never call it yourself. |
31
+ | **`intensity`** | A measured **intensity**, not an amplitude. Internally `sqrt(clip(intensity, 0))`. |
32
+ | **`mask`** | `True` marks an **unusable** pixel (beamstop, hot, saturated). Masked pixels are left unconstrained — never forced to zero. |
33
+ | **`support`** | `True` is inside the object. Omit it and an autocorrelation support is derived (loose; pass a real one when you have it). |
34
+
35
+ ## Composing a reconstruction
36
+
37
+ Code order is execution order. There is no operator-overloading DSL.
38
+
39
+ ```python
40
+ problem = qp.CDI(intensity=intensity, support=support, mask=mask)
41
+
42
+ recipe = qp.Recipe(
43
+ qp.repeat(10, qp.RAAR(50, beta=0.9), qp.Shrinkwrap()),
44
+ qp.ER(200),
45
+ )
46
+
47
+ result = problem.reconstruct(recipe)
48
+ ```
49
+
50
+ The first positional argument of an algorithm is always its iteration count.
51
+
52
+ `qp.detwin(...)` runs its body against a support cut in half at the object's
53
+ centre of mass, then restores the full support. That cut is not centrosymmetric,
54
+ so it fits only one of the two conjugate solutions and biases the run into a
55
+ branch. The default recipe places it a quarter of the way through the RAAR
56
+ cycles, where PyNX puts it. It is measurably useful when the support is
57
+ symmetric and the run would otherwise stall, and harmless when it is not — but
58
+ a support with the right shape is worth far more than any detwinning.
59
+
60
+ `qp.Shrinkwrap(threshold, sigma, method)` takes `method="max"` (classic, relative
61
+ to the peak amplitude) or `method="rms"` (relative to the rms inside the current
62
+ support, less easily thrown off by a hot pixel). The thresholds are on different
63
+ scales: the classic `"max"` value is 0.2, PyNX uses 0.28 for `"rms"`.
64
+
65
+ The defaults are `0.3, 1.0` rather than the classic `0.2, 2.0`. Starting from an
66
+ autocorrelation support — the case that actually needs shrinkwrap — 0.2 / 2.0
67
+ never converged in testing and 0.3 / 1.0 did, on every object tried. That is a
68
+ measurement on synthetic data, not a law. **These two numbers are the first thing
69
+ to re-tune on a real dataset.**
70
+
71
+ ## Multi-start
72
+
73
+ Multi-start is a core feature, not an add-on — for a support with awkward
74
+ symmetry it is the difference between converging and not.
75
+
76
+ ```python
77
+ ensemble = problem.reconstruct(recipe, starts=32, batch=4)
78
+ best = ensemble.best
79
+ ```
80
+
81
+ `vmap` holds every start in memory at once. `batch` chunks that: lower it if a
82
+ large 3D volume runs out of GPU memory.
83
+
84
+ ## Swapping the forward model
85
+
86
+ Algorithms never name an FFT, so the physics is replaceable without touching
87
+ them:
88
+
89
+ ```python
90
+ qp.CDI(intensity, support=support, model=MyPropagator())
91
+ ```
92
+
93
+ A model needs `forward` / `adjoint` and should be unitary. A *wrong* model fails
94
+ silently — it can fit the data perfectly and return the wrong object — so this
95
+ is worth getting right. See `tests/test_forward_model.py`.
96
+
97
+ ## Before you trust a reconstruction
98
+
99
+ Check the measurement against the assumptions the library makes about it:
100
+
101
+ ```python
102
+ print(qp.diagnose(intensity, mask=mask, support=support))
103
+ ```
104
+
105
+ It reports the centring offset two ways (brightest pixel, and Friedel symmetry,
106
+ which resolves half a pixel and works on patterns with no dominant peak), how
107
+ much of the array the autocorrelation fills, the negative and masked fractions,
108
+ and warns about each. An off-centre pattern is the failure that costs the most
109
+ and shows the least.
110
+
111
+ `examples/diagnose.py` does the same from the command line, then reconstructs
112
+ and reports whether independent starts agree:
113
+
114
+ ```
115
+ python examples/diagnose.py scan.npz --key intensity --support sup.npy --starts 8
116
+ ```
117
+
118
+ A low Fourier error says a run fits the data. Agreement between starts, after
119
+ alignment, is what says the answer is determined — they are different claims and
120
+ only the second one is worth much.
121
+
122
+ ## Combining runs
123
+
124
+ A reconstruction is determined only up to a global phase, a translation, and a
125
+ conjugate flip. No iteration can remove those — they are exact symmetries of the
126
+ constraints — so they are resolved afterwards:
127
+
128
+ ```python
129
+ aligned = ensemble.align() # every run brought onto the best one
130
+ mean = ensemble.average() # coherent mean of the aligned runs
131
+
132
+ ```
133
+
134
+ **Never average runs without aligning them.** Each carries its own global phase,
135
+ so a raw mean interferes destructively — in practice it throws away about two
136
+ thirds of the amplitude. The direction survives, so a similarity score will not
137
+ warn you; only the amplitude shows it.
138
+
139
+ `align()` picks a *convention*, not the truth. It adopts whichever conjugate
140
+ branch the best run happens to be in — enough to combine runs, and no claim
141
+ about which branch is real. Nothing in the data distinguishes them: `conj(obj(-r))`
142
+ reproduces the measured intensity exactly. What breaks the tie is the support
143
+ you supply. Hand the solver a reflected support and every start converges just
144
+ as cleanly on the mirrored object, with a flawless error metric and no warning.
145
+ The handedness of your support is an assumption, not a result.
146
+
147
+ Alignment works to whole pixels.
148
+
149
+ `result.phase` still carries whatever linear phase the measurement geometry put
150
+ there, and QoraPhase does not remove it. Doing that properly needs the
151
+ diffraction geometry, which arrives with BCDI in v0.3; short of that, any
152
+ "correction" is a guess.
153
+
154
+ If the twin branch matters to you, break the symmetry during the run rather than
155
+ after it — see `qp.detwin` below.
156
+
157
+ ## What this does not do
158
+
159
+ - No phase-ramp removal, no sub-pixel alignment, no clustering, no PRTF.
160
+ - No positivity/reality constraint — the object is complex here.
161
+ - No file I/O, no BCDI geometry, no NUFFT, no strain, no autodiff optimisers.
162
+
163
+ ## Development
164
+
165
+ ```bash
166
+ uv sync --all-groups
167
+ uv run pytest
168
+ ```
@@ -0,0 +1,140 @@
1
+ """Run QoraPhase against a real measurement and report whether to believe it.
2
+
3
+ python examples/diagnose.py scan.npz --key intensity --mask-key mask
4
+
5
+ The library never touches files, so the loading lives here. What the
6
+ script adds on top of ``qp.diagnose`` is the check that synthetic data can never
7
+ provide: whether independent random starts, once aligned, actually agree with
8
+ each other. A low Fourier error only says a run fits the data; agreement across
9
+ starts is what says the answer is determined.
10
+
11
+ --no-run stop after the pre-flight report
12
+ --plot out.png save amplitude/phase panels and the convergence curves
13
+ """
14
+
15
+ import argparse
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ import numpy as np
20
+
21
+ import qoraphase as qp
22
+
23
+
24
+ def load(path: Path, key: str | None):
25
+ """Read one array from .npy, .npz, .mat-style HDF5 or .h5/.cxi."""
26
+ suffix = path.suffix.lower()
27
+ if suffix == ".npy":
28
+ return np.load(path)
29
+ if suffix == ".npz":
30
+ with np.load(path) as archive:
31
+ names = list(archive.files)
32
+ if key is None:
33
+ if len(names) != 1:
34
+ raise SystemExit(f"{path} holds {names}; pick one with --key")
35
+ key = names[0]
36
+ return archive[key]
37
+ if suffix in (".h5", ".hdf5", ".cxi", ".nxs"):
38
+ try:
39
+ import h5py
40
+ except ImportError:
41
+ raise SystemExit("reading HDF5 needs h5py: uv pip install h5py") from None
42
+ if key is None:
43
+ raise SystemExit(f"--key is required for {suffix} files")
44
+ with h5py.File(path, "r") as handle:
45
+ return handle[key][()]
46
+ raise SystemExit(f"unsupported file type: {suffix}")
47
+
48
+
49
+ def agreement(ensemble):
50
+ """Pairwise similarity between runs, after resolving the ambiguities.
51
+
52
+ Each run is aligned onto the first, then compared by normalised overlap.
53
+ Runs that solved the same problem land on the same answer; runs that found
54
+ different local minima do not, however good their individual error looks.
55
+ """
56
+ reference = np.asarray(ensemble[0].object)
57
+ aligned = [np.asarray(qp.align_to(run.object, ensemble[0].object)) for run in ensemble]
58
+ scores = []
59
+ for i, a in enumerate(aligned):
60
+ for b in aligned[i + 1 :]:
61
+ overlap = abs(np.vdot(a, b)) / (np.linalg.norm(a) * np.linalg.norm(b))
62
+ scores.append(float(overlap))
63
+ return np.array(scores) if scores else np.array([1.0])
64
+
65
+
66
+ def plot(result, ensemble, path):
67
+ import matplotlib
68
+
69
+ matplotlib.use("Agg")
70
+ import matplotlib.pyplot as plt
71
+
72
+ figure = plt.figure(figsize=(14, 4))
73
+ panels = figure.subplots(1, 3)
74
+ result.plot(axes=panels[:2] if result.object.ndim == 2 else None)
75
+ for run in ensemble:
76
+ panels[2].semilogy(np.asarray(run.metrics), lw=0.8)
77
+ panels[2].set_title("Fourier error")
78
+ panels[2].set_xlabel("iteration")
79
+ figure.tight_layout()
80
+ figure.savefig(path, dpi=120)
81
+ print(f"\nwrote {path}")
82
+
83
+
84
+ def main(argv=None):
85
+ parser = argparse.ArgumentParser(description=__doc__)
86
+ parser.add_argument("path", type=Path)
87
+ parser.add_argument("--key", help="dataset name inside .npz / .h5")
88
+ parser.add_argument("--mask", type=Path, help="mask file; True marks bad pixels")
89
+ parser.add_argument("--mask-key")
90
+ parser.add_argument("--support", type=Path)
91
+ parser.add_argument("--support-key")
92
+ parser.add_argument("--starts", type=int, default=8)
93
+ parser.add_argument("--batch", type=int)
94
+ parser.add_argument("--seed", type=int, default=0)
95
+ parser.add_argument("--no-run", action="store_true")
96
+ parser.add_argument("--plot", type=Path)
97
+ args = parser.parse_args(argv)
98
+
99
+ intensity = load(args.path, args.key)
100
+ mask = None if args.mask is None else load(args.mask, args.mask_key).astype(bool)
101
+ support = (
102
+ None if args.support is None else load(args.support, args.support_key).astype(bool)
103
+ )
104
+
105
+ report = qp.diagnose(intensity, mask=mask, support=support)
106
+ print(report)
107
+ if args.no_run:
108
+ return 0
109
+ if report.warnings:
110
+ print("\ncontinuing anyway -- read the warnings above before trusting this")
111
+
112
+ print(f"\nreconstructing, {args.starts} starts ...")
113
+ ensemble = qp.reconstruct(
114
+ intensity,
115
+ support=support,
116
+ mask=mask,
117
+ starts=args.starts,
118
+ batch=args.batch,
119
+ seed=args.seed,
120
+ )
121
+ if args.starts == 1:
122
+ ensemble = qp.Ensemble((ensemble,))
123
+
124
+ errors = np.array([run.error for run in ensemble])
125
+ scores = agreement(ensemble)
126
+ print(f"final error best {errors.min():.3e} worst {errors.max():.3e}")
127
+ print(f"run agreement min {scores.min():.3f} median {np.median(scores):.3f}")
128
+ if scores.min() < 0.9:
129
+ print(
130
+ "!! the starts did not converge on the same object. A low error on a "
131
+ "single run means it fits the data, not that the answer is determined."
132
+ )
133
+
134
+ if args.plot:
135
+ plot(ensemble.best, ensemble, args.plot)
136
+ return 0
137
+
138
+
139
+ if __name__ == "__main__":
140
+ sys.exit(main())
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "qoraphase"
3
+ version = "0.1.0"
4
+ description = "A JAX-native, composable phase-retrieval framework for coherent diffraction imaging"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE*"]
8
+ authors = [{ name = "Isaac Yong", email = "isaacyong@naver.com" }]
9
+ requires-python = ">=3.12"
10
+ keywords = ["phase retrieval", "coherent diffraction imaging", "CDI", "jax", "x-ray imaging"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Science/Research",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Programming Language :: Python :: 3.14",
18
+ "Topic :: Scientific/Engineering :: Physics",
19
+ "Topic :: Scientific/Engineering :: Image Processing",
20
+ ]
21
+ dependencies = [
22
+ "jax>=0.11.1",
23
+ "matplotlib>=3.11.1",
24
+ "numpy>=2.5.2",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/NaNIsaac/QoraPhase"
29
+ Repository = "https://github.com/NaNIsaac/QoraPhase"
30
+ Issues = "https://github.com/NaNIsaac/QoraPhase/issues"
31
+
32
+ [build-system]
33
+ requires = ["hatchling>=1.27"]
34
+ build-backend = "hatchling.build"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/qoraphase"]
38
+
39
+ [tool.hatch.build.targets.sdist]
40
+ include = ["src/qoraphase", "tests", "examples", "README.md", "LICENSE", "CITATION.cff"]
41
+
42
+ [dependency-groups]
43
+ dev = ["pytest>=8"]
44
+
45
+ [tool.pytest.ini_options]
46
+ pythonpath = ["src"]
47
+ testpaths = ["tests"]
@@ -0,0 +1,48 @@
1
+ """QoraPhase -- a JAX-native, composable phase-retrieval framework for CDI.
2
+
3
+ Array in, reconstruction out::
4
+
5
+ import qoraphase as qp
6
+ result = qp.reconstruct(intensity, support=support)
7
+
8
+ All public arrays are center-origin, ``mask=True`` means an unusable pixel, and
9
+ ``intensity`` is a measured intensity (not an amplitude). See spec section 4.
10
+ """
11
+
12
+ from .algorithms.er import ER
13
+ from .algorithms.hio import HIO
14
+ from .algorithms.raar import RAAR
15
+ from .api import default_recipe, reconstruct
16
+ from .core.ensemble import Ensemble
17
+ from .core.problem import CDI
18
+ from .core.result import Reconstruction
19
+ from .core.state import ReconState
20
+ from .diagnostics import Diagnosis, diagnose
21
+ from .models.fft import FFT
22
+ from .operators.modulus import ModulusProjection
23
+ from .operators.shrinkwrap import Shrinkwrap
24
+ from .operators.support import SupportProjection
25
+ from .postprocess import align_to
26
+ from .recipes.recipe import Recipe, detwin, repeat
27
+
28
+ __all__ = [
29
+ "CDI",
30
+ "Diagnosis",
31
+ "ER",
32
+ "FFT",
33
+ "HIO",
34
+ "RAAR",
35
+ "Ensemble",
36
+ "ModulusProjection",
37
+ "ReconState",
38
+ "Reconstruction",
39
+ "Recipe",
40
+ "Shrinkwrap",
41
+ "SupportProjection",
42
+ "align_to",
43
+ "default_recipe",
44
+ "detwin",
45
+ "diagnose",
46
+ "reconstruct",
47
+ "repeat",
48
+ ]
File without changes
@@ -0,0 +1,22 @@
1
+ """Error Reduction."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import jax
6
+
7
+ from ..operators.modulus import modulus_project
8
+ from ..operators.support import support_project
9
+ from ..recipes.recipe import Iterative
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ER(Iterative):
14
+ """``x <- P_S P_M x``. Monotone in the Fourier error, and slow."""
15
+
16
+ def step(self, state, problem):
17
+ projected, err = modulus_project(state.obj, problem)
18
+ obj = support_project(projected, state.support)
19
+ return state._replace(obj=obj, iteration=state.iteration + 1), err
20
+
21
+
22
+ jax.tree_util.register_dataclass(ER, data_fields=[], meta_fields=["n"])