bayes-mef 0.2.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.
@@ -0,0 +1,23 @@
1
+ # some extensions
2
+ .vscode
3
+ .DS_Store
4
+ # jupyter notebook
5
+ .ipynb_checkpoints
6
+ */.ipynb_checkpoints/*
7
+
8
+ # Byte-compiled / optimized / DLL files
9
+ __pycache__/
10
+ *.py[cod]
11
+ *$py.class
12
+
13
+ # folders
14
+ data
15
+ dist
16
+ *.zip
17
+ # Python / uv
18
+ .venv/
19
+ __pycache__/
20
+ *.pyc
21
+ .pytest_cache/
22
+ .ruff_cache/
23
+ dist/
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, microscopic-image-analysis
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: bayes-mef
3
+ Version: 0.2.0
4
+ Summary: Bayesian multi-exposure image fusion for robust HDR imaging
5
+ Project-URL: Homepage, https://github.com/microscopic-image-analysis/bayes-mef
6
+ Project-URL: Repository, https://github.com/microscopic-image-analysis/bayes-mef
7
+ Project-URL: Issues, https://github.com/microscopic-image-analysis/bayes-mef/issues
8
+ Author-email: Shantanu Kodgirwar <shantanu.kodgirwar@uni-jena.de>, Michael Habeck <michael.habeck@uni-jena.de>
9
+ License-Expression: BSD-3-Clause
10
+ License-File: LICENSE
11
+ Keywords: HDR,expectation-maximization,image-fusion,jax,ptychography
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
16
+ Classifier: Topic :: Scientific/Engineering :: Physics
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: h5py>=3.9
19
+ Requires-Dist: jax>=0.8
20
+ Requires-Dist: numpy>=1.24
21
+ Requires-Dist: scipy>=1.11
22
+ Provides-Extra: cuda12
23
+ Requires-Dist: jax[cuda12]; extra == 'cuda12'
24
+ Provides-Extra: cuda13
25
+ Requires-Dist: jax[cuda13]; extra == 'cuda13'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Bayesian MEF
29
+ [![PyPI](https://img.shields.io/pypi/v/bayes_mef)](https://pypi.org/project/bayes_mef/)
30
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11+-green.svg)
31
+ [![DOI](https://img.shields.io/badge/DOI-10.1364%2FOE.524284-blue.svg)](https://doi.org/10.1364/OE.524284)
32
+ [![License](https://img.shields.io/badge/License-BSD_3--Clause-purple.svg)](https://opensource.org/licenses/BSD-3-Clause)
33
+
34
+ Bayesian multi-exposure image fusion (MEF) is a general-purpose algorithm for robust
35
+ high dynamic range (HDR) imaging under low SNR or varying illumination — in particular
36
+ for phase retrieval in coherent diffractive imaging. The paper,
37
+ ["Bayesian multi-exposure image fusion for robust high dynamic range ptychography"](https://doi.org/10.1364/OE.524284),
38
+ details the method and its benefits for ptychography ([reproducing the results](#reproducing-results)).
39
+
40
+ ![demo_mef](https://github.com/microscopic-image-analysis/bayes-mef/assets/64919085/d00a8c5e-5e53-4b7e-856b-381cc99523ba)
41
+
42
+ This small library is implemented in [JAX](https://docs.jax.dev), so the same code runs on CPU/GPU/TPU. Inputs may be NumPy arrays, Python lists or JAX arrays; results come back as NumPy arrays.
43
+
44
+ ```bash
45
+ pip install bayes_mef
46
+ ```
47
+
48
+ Optionally for GPU (CUDA 12 or 13)
49
+
50
+ ```bash
51
+ pip install "bayes_mef[cuda13]" # GPU with CUDA 13; `cuda12` flag for older GPUs
52
+ ```
53
+
54
+ After a GPU install, check the GPU is picked up with `bayes_mef check gpu`.
55
+
56
+ ## Usage
57
+
58
+ A minimal example, simulating some data
59
+ ([Colab](https://colab.research.google.com/github/microscopic-image-analysis/bayes-mef/blob/main/demo.ipynb)):
60
+
61
+ ```python
62
+ from bayes_mef import BayesianMEF, ConventionalMEF
63
+ from skimage.data import camera
64
+ import numpy as np
65
+
66
+ truth = camera()
67
+ background = 60
68
+ times = np.array([0.1, 1, 10]) # exposure times / flux factors
69
+ threshold = 1500 # detector limit
70
+
71
+ # overexposed Poisson data from the image-formation model
72
+ data = [np.random.poisson(t * truth + background) for t in times]
73
+ data_saturated = np.clip(data, None, threshold, dtype="float")
74
+
75
+ mef_em = BayesianMEF(data_saturated, threshold, times, background)
76
+ mef_em.run(n_iter=100)
77
+ fused_em = mef_em.fused_image.copy()
78
+
79
+ # ConventionalMEF is the paper's MLE baseline, with the same interface
80
+ mef_mle = ConventionalMEF(data_saturated, threshold, times, background)
81
+ mef_mle.mle()
82
+ fused_mle = mef_mle.fused_image.copy()
83
+ ```
84
+
85
+ ### Precision
86
+
87
+ Single precision by default (accurate for censoring thresholds up to ~4096). For 16-bit detectors, switch to double precision *before* creating any array; a warning flags the risk otherwise:
88
+
89
+ ```python
90
+ import bayes_mef
91
+ bayes_mef.enable_x64() # or set JAX_ENABLE_X64=1
92
+ ```
93
+
94
+ ### When the exposures are not known
95
+
96
+ Omit `times` (and `threshold`) and they are estimated from the data, initialised from the non-saturated pixels (`flux_init="matched"`). The estimate is only a starting point, so **pass `update_fluxes=True` with it** and let EM refine it:
97
+
98
+ ```python
99
+ mef_em = BayesianMEF(data_saturated, background=background, update_fluxes=True)
100
+ mef_em.run(n_iter=200)
101
+ ```
102
+
103
+ `update_fluxes` defaults to False — `times` are used exactly as given, estimated or not — and estimating them without it warns. On the simulation above, the unrefined estimate correlates 0.16 with the truth against 0.989 once refined. `flux_init="uncensored"` selects the v0.1.9 initialiser, whose ratios compress under heavy censoring.
104
+
105
+ Once the background dominates the signal, i.e., the weak, low-SNR regime the [paper](https://doi.org/10.1364/OE.524284) targets: summed counts carry almost no information about the exposures, and it is EM's iterative background handling that recovers the range; a warning is raised when the estimate comes out nearly flat. Estimated fluxes are relative, so the fused image is on a relative scale. **Supply the real exposure times whenever you know them.**
106
+
107
+ ### Fusing a 4D ptychogram dataset
108
+
109
+ For ptychography, we record multiple exposures per scan position. `LaunchMEF` fuses every scan position with a single vectorised program (CPU or GPU):
110
+
111
+ ```python
112
+ from bayes_mef import LaunchMEF
113
+
114
+ launch_mef = LaunchMEF(
115
+ ptychogram_stack, # (n_exposures, n_scans, dp_x, dp_y)
116
+ background, # a number, one dark frame, or one per exposure
117
+ times=None, # None -> estimated from the data
118
+ threshold=None, # None -> estimated automatically
119
+ update_fluxes=False, # True -> EM refines the fluxes; pair with estimated times
120
+ flux_init="matched",
121
+ )
122
+
123
+ # returns fused patterns (n_scans, dp_x, dp_y) and the flux factors
124
+ fused_ptyem_stack, em_flux_factors = launch_mef.run_em(n_iter=150)
125
+
126
+ # or the conventional MLE baseline over the whole stack (just the fused patterns)
127
+ fused_ptymle_stack = launch_mef.run_mle()
128
+ ```
129
+
130
+ Backgrounds are taken however they were recorded, on `LaunchMEF` and on the single stack classes alike: a scalar, one offset per exposure `(n_exposures,)`, a single dark frame `(dp_x, dp_y)`, one frame per exposure `(n_exposures, dp_x, dp_y)`, or one per image (the full stack shape). Each is given its exposure axis explicitly, so a per-exposure vector is never spread along the image columns, and a mismatched shape raises instead of broadcasting into something that quietly means the wrong thing.
131
+
132
+ The old `n_cpus` argument is accepted but ignored (it warns), since there are no worker processes to size any more.
133
+
134
+ Scans are fused in chunks sized to the device's free memory, so a stack larger than device memory still works. Set `batch_size` yourself if you hit a `MemoryError` or fuse alongside other work (results do not depend on it):
135
+
136
+ ```python
137
+ fused, fluxes = launch_mef.run_em(n_iter, batch_size=8)
138
+ ```
139
+
140
+ See [synthetic_mef.py](scripts/synthetic_mef.py) for detailed usage on synthetic
141
+ ptychography data, and [benchmarks/FINDINGS.md](benchmarks/FINDINGS.md) for a study
142
+ of when each method helps and for performance benchmarks.
143
+
144
+ ## Reproducing results
145
+
146
+ To reproduce the ptychographic reconstructions from the paper:
147
+
148
+ 1. Clone the repo:
149
+ ```bash
150
+ git clone https://github.com/microscopic-image-analysis/bayes-mef.git
151
+ cd bayes-mef
152
+ ```
153
+ 2. Install the pinned dependencies with [uv](https://docs.astral.sh/uv/), then prefix
154
+ commands with `uv run` (e.g. `uv run python scripts/synthetic_mef.py`):
155
+ ```bash
156
+ uv sync --locked --group scripts
157
+ ```
158
+ 3. Download the data from [Zenodo](https://zenodo.org/doi/10.5281/zenodo.10964222):
159
+ ```bash
160
+ ./download_data.sh
161
+ ```
162
+ 4. Optional: install [`cupy`](https://docs.cupy.dev/en/stable/install.html) for faster
163
+ GPU reconstructions.
164
+ 5. Run files from [scripts/](scripts) to plot the results.
165
+
166
+ ## Citation
167
+ If this algorithm or the publication was useful, please cite:
168
+ ```tex
169
+ @article{Kodgirwar:24,
170
+ author = {Shantanu Kodgirwar and Lars Loetgering and Chang Liu and Aleena Joseph and Leona Licht and Daniel S. Penagos Molina and Wilhelm Eschen and Jan Rothhardt and Michael Habeck},
171
+ journal = {Opt. Express},
172
+ number = {16},
173
+ pages = {28090--28099},
174
+ publisher = {Optica Publishing Group},
175
+ title = {Bayesian multi-exposure image fusion for robust high dynamic range ptychography},
176
+ volume = {32},
177
+ month = {Jul},
178
+ year = {2024},
179
+ url = {https://opg.optica.org/oe/abstract.cfm?URI=oe-32-16-28090},
180
+ doi = {10.1364/OE.524284},
181
+ }
182
+ ```
@@ -0,0 +1,155 @@
1
+ # Bayesian MEF
2
+ [![PyPI](https://img.shields.io/pypi/v/bayes_mef)](https://pypi.org/project/bayes_mef/)
3
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11+-green.svg)
4
+ [![DOI](https://img.shields.io/badge/DOI-10.1364%2FOE.524284-blue.svg)](https://doi.org/10.1364/OE.524284)
5
+ [![License](https://img.shields.io/badge/License-BSD_3--Clause-purple.svg)](https://opensource.org/licenses/BSD-3-Clause)
6
+
7
+ Bayesian multi-exposure image fusion (MEF) is a general-purpose algorithm for robust
8
+ high dynamic range (HDR) imaging under low SNR or varying illumination — in particular
9
+ for phase retrieval in coherent diffractive imaging. The paper,
10
+ ["Bayesian multi-exposure image fusion for robust high dynamic range ptychography"](https://doi.org/10.1364/OE.524284),
11
+ details the method and its benefits for ptychography ([reproducing the results](#reproducing-results)).
12
+
13
+ ![demo_mef](https://github.com/microscopic-image-analysis/bayes-mef/assets/64919085/d00a8c5e-5e53-4b7e-856b-381cc99523ba)
14
+
15
+ This small library is implemented in [JAX](https://docs.jax.dev), so the same code runs on CPU/GPU/TPU. Inputs may be NumPy arrays, Python lists or JAX arrays; results come back as NumPy arrays.
16
+
17
+ ```bash
18
+ pip install bayes_mef
19
+ ```
20
+
21
+ Optionally for GPU (CUDA 12 or 13)
22
+
23
+ ```bash
24
+ pip install "bayes_mef[cuda13]" # GPU with CUDA 13; `cuda12` flag for older GPUs
25
+ ```
26
+
27
+ After a GPU install, check the GPU is picked up with `bayes_mef check gpu`.
28
+
29
+ ## Usage
30
+
31
+ A minimal example, simulating some data
32
+ ([Colab](https://colab.research.google.com/github/microscopic-image-analysis/bayes-mef/blob/main/demo.ipynb)):
33
+
34
+ ```python
35
+ from bayes_mef import BayesianMEF, ConventionalMEF
36
+ from skimage.data import camera
37
+ import numpy as np
38
+
39
+ truth = camera()
40
+ background = 60
41
+ times = np.array([0.1, 1, 10]) # exposure times / flux factors
42
+ threshold = 1500 # detector limit
43
+
44
+ # overexposed Poisson data from the image-formation model
45
+ data = [np.random.poisson(t * truth + background) for t in times]
46
+ data_saturated = np.clip(data, None, threshold, dtype="float")
47
+
48
+ mef_em = BayesianMEF(data_saturated, threshold, times, background)
49
+ mef_em.run(n_iter=100)
50
+ fused_em = mef_em.fused_image.copy()
51
+
52
+ # ConventionalMEF is the paper's MLE baseline, with the same interface
53
+ mef_mle = ConventionalMEF(data_saturated, threshold, times, background)
54
+ mef_mle.mle()
55
+ fused_mle = mef_mle.fused_image.copy()
56
+ ```
57
+
58
+ ### Precision
59
+
60
+ Single precision by default (accurate for censoring thresholds up to ~4096). For 16-bit detectors, switch to double precision *before* creating any array; a warning flags the risk otherwise:
61
+
62
+ ```python
63
+ import bayes_mef
64
+ bayes_mef.enable_x64() # or set JAX_ENABLE_X64=1
65
+ ```
66
+
67
+ ### When the exposures are not known
68
+
69
+ Omit `times` (and `threshold`) and they are estimated from the data, initialised from the non-saturated pixels (`flux_init="matched"`). The estimate is only a starting point, so **pass `update_fluxes=True` with it** and let EM refine it:
70
+
71
+ ```python
72
+ mef_em = BayesianMEF(data_saturated, background=background, update_fluxes=True)
73
+ mef_em.run(n_iter=200)
74
+ ```
75
+
76
+ `update_fluxes` defaults to False — `times` are used exactly as given, estimated or not — and estimating them without it warns. On the simulation above, the unrefined estimate correlates 0.16 with the truth against 0.989 once refined. `flux_init="uncensored"` selects the v0.1.9 initialiser, whose ratios compress under heavy censoring.
77
+
78
+ Once the background dominates the signal, i.e., the weak, low-SNR regime the [paper](https://doi.org/10.1364/OE.524284) targets: summed counts carry almost no information about the exposures, and it is EM's iterative background handling that recovers the range; a warning is raised when the estimate comes out nearly flat. Estimated fluxes are relative, so the fused image is on a relative scale. **Supply the real exposure times whenever you know them.**
79
+
80
+ ### Fusing a 4D ptychogram dataset
81
+
82
+ For ptychography, we record multiple exposures per scan position. `LaunchMEF` fuses every scan position with a single vectorised program (CPU or GPU):
83
+
84
+ ```python
85
+ from bayes_mef import LaunchMEF
86
+
87
+ launch_mef = LaunchMEF(
88
+ ptychogram_stack, # (n_exposures, n_scans, dp_x, dp_y)
89
+ background, # a number, one dark frame, or one per exposure
90
+ times=None, # None -> estimated from the data
91
+ threshold=None, # None -> estimated automatically
92
+ update_fluxes=False, # True -> EM refines the fluxes; pair with estimated times
93
+ flux_init="matched",
94
+ )
95
+
96
+ # returns fused patterns (n_scans, dp_x, dp_y) and the flux factors
97
+ fused_ptyem_stack, em_flux_factors = launch_mef.run_em(n_iter=150)
98
+
99
+ # or the conventional MLE baseline over the whole stack (just the fused patterns)
100
+ fused_ptymle_stack = launch_mef.run_mle()
101
+ ```
102
+
103
+ Backgrounds are taken however they were recorded, on `LaunchMEF` and on the single stack classes alike: a scalar, one offset per exposure `(n_exposures,)`, a single dark frame `(dp_x, dp_y)`, one frame per exposure `(n_exposures, dp_x, dp_y)`, or one per image (the full stack shape). Each is given its exposure axis explicitly, so a per-exposure vector is never spread along the image columns, and a mismatched shape raises instead of broadcasting into something that quietly means the wrong thing.
104
+
105
+ The old `n_cpus` argument is accepted but ignored (it warns), since there are no worker processes to size any more.
106
+
107
+ Scans are fused in chunks sized to the device's free memory, so a stack larger than device memory still works. Set `batch_size` yourself if you hit a `MemoryError` or fuse alongside other work (results do not depend on it):
108
+
109
+ ```python
110
+ fused, fluxes = launch_mef.run_em(n_iter, batch_size=8)
111
+ ```
112
+
113
+ See [synthetic_mef.py](scripts/synthetic_mef.py) for detailed usage on synthetic
114
+ ptychography data, and [benchmarks/FINDINGS.md](benchmarks/FINDINGS.md) for a study
115
+ of when each method helps and for performance benchmarks.
116
+
117
+ ## Reproducing results
118
+
119
+ To reproduce the ptychographic reconstructions from the paper:
120
+
121
+ 1. Clone the repo:
122
+ ```bash
123
+ git clone https://github.com/microscopic-image-analysis/bayes-mef.git
124
+ cd bayes-mef
125
+ ```
126
+ 2. Install the pinned dependencies with [uv](https://docs.astral.sh/uv/), then prefix
127
+ commands with `uv run` (e.g. `uv run python scripts/synthetic_mef.py`):
128
+ ```bash
129
+ uv sync --locked --group scripts
130
+ ```
131
+ 3. Download the data from [Zenodo](https://zenodo.org/doi/10.5281/zenodo.10964222):
132
+ ```bash
133
+ ./download_data.sh
134
+ ```
135
+ 4. Optional: install [`cupy`](https://docs.cupy.dev/en/stable/install.html) for faster
136
+ GPU reconstructions.
137
+ 5. Run files from [scripts/](scripts) to plot the results.
138
+
139
+ ## Citation
140
+ If this algorithm or the publication was useful, please cite:
141
+ ```tex
142
+ @article{Kodgirwar:24,
143
+ author = {Shantanu Kodgirwar and Lars Loetgering and Chang Liu and Aleena Joseph and Leona Licht and Daniel S. Penagos Molina and Wilhelm Eschen and Jan Rothhardt and Michael Habeck},
144
+ journal = {Opt. Express},
145
+ number = {16},
146
+ pages = {28090--28099},
147
+ publisher = {Optica Publishing Group},
148
+ title = {Bayesian multi-exposure image fusion for robust high dynamic range ptychography},
149
+ volume = {32},
150
+ month = {Jul},
151
+ year = {2024},
152
+ url = {https://opg.optica.org/oe/abstract.cfm?URI=oe-32-16-28090},
153
+ doi = {10.1364/OE.524284},
154
+ }
155
+ ```
@@ -0,0 +1,54 @@
1
+ """Bayesian multi-exposure image fusion for robust HDR imaging."""
2
+
3
+ import importlib
4
+ from importlib.metadata import PackageNotFoundError, version
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ # Never executed. The public names are bound lazily below, which leaves type
9
+ # checkers and editors with nothing to resolve, so "go to definition" on
10
+ # `from bayes_mef import BayesianMEF` lands nowhere. These imports exist for
11
+ # them alone; at runtime the lazy path is still the only one.
12
+ from ._compat import enable_x64
13
+ from .algos import BayesianMEF, ConventionalMEF
14
+ from .mef_launcher import LaunchMEF
15
+ from .utils import set_fluxes, set_threshold
16
+
17
+ __all__ = [
18
+ "BayesianMEF",
19
+ "ConventionalMEF",
20
+ "LaunchMEF",
21
+ "enable_x64",
22
+ "set_fluxes",
23
+ "set_threshold",
24
+ ]
25
+
26
+ try:
27
+ __version__ = version("bayes-mef")
28
+ except PackageNotFoundError: # not installed, e.g. running from a source checkout
29
+ __version__ = "0.0.0"
30
+
31
+ _LAZY = {
32
+ "BayesianMEF": "algos",
33
+ "ConventionalMEF": "algos",
34
+ "LaunchMEF": "mef_launcher",
35
+ "set_fluxes": "utils",
36
+ "set_threshold": "utils",
37
+ "enable_x64": "_compat",
38
+ }
39
+
40
+
41
+ def __getattr__(name):
42
+ """Import submodules on first use, so `import bayes_mef` does not load JAX.
43
+
44
+ Keeps the import cheap, and leaves a window for callers to configure JAX
45
+ (precision, platform) before the backend initialises.
46
+ """
47
+ if name in _LAZY:
48
+ module = importlib.import_module(f".{_LAZY[name]}", __name__)
49
+ return getattr(module, name)
50
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
51
+
52
+
53
+ def __dir__():
54
+ return sorted(__all__)
@@ -0,0 +1,132 @@
1
+ """NumPy <-> JAX boundary: dtype policy, input coercion, precision control."""
2
+
3
+ import warnings
4
+
5
+ import jax
6
+ import jax.numpy as jnp
7
+ import numpy as np
8
+
9
+ _X64_WARNED = False
10
+
11
+
12
+ def default_dtype():
13
+ """Working float dtype: float64 when x64 is enabled, float32 otherwise."""
14
+ return jnp.float64 if jax.config.jax_enable_x64 else jnp.float32
15
+
16
+
17
+ def enable_x64():
18
+ """Run bayes_mef in double precision.
19
+
20
+ Must be called before any JAX array is created. Equivalent to setting
21
+ ``JAX_ENABLE_X64=1`` in the environment.
22
+ """
23
+ jax.config.update("jax_enable_x64", True)
24
+
25
+
26
+ def as_array(x, dtype=None):
27
+ """Coerce numpy arrays, Python lists/scalars and JAX arrays to a JAX array."""
28
+ return jnp.asarray(x, dtype=default_dtype() if dtype is None else dtype)
29
+
30
+
31
+ def as_background(x):
32
+ """Background may be None, a scalar, or a full array; all broadcast against data."""
33
+ return as_array(0.0 if x is None else x)
34
+
35
+
36
+ def align_background(background, stack_shape, *, scan_axis=False):
37
+ """Lay a recorded background out so it broadcasts against the stack.
38
+
39
+ Backgrounds arrive in whatever form the experiment produced: a number, one
40
+ offset per exposure, a single dark frame, one frame per exposure, or one per
41
+ image. All of them describe the same thing, but NumPy lines shapes up from
42
+ the trailing axis, so only some happen to broadcast correctly against a
43
+ stack -- a per-exposure vector silently spreads along the image columns
44
+ instead. Each form is therefore given its exposure axis explicitly here,
45
+ once, so nothing downstream has to reason about the layout again.
46
+
47
+ `stack_shape` is the stack as the caller holds it, exposures first:
48
+ (n_exposures, *image), or (n_exposures, n_scans, *image) with
49
+ `scan_axis=True`. Accepted backgrounds, in that same layout:
50
+
51
+ None, scalar, (1,) one offset for the whole stack
52
+ (n_exposures,) one offset per exposure
53
+ image one dark frame, shared by every exposure
54
+ (n_exposures, *image) one dark frame per exposure
55
+ stack_shape one dark frame per image (scan_axis only)
56
+
57
+ Anything of the right rank whose axes are 1 or the stack's own length is
58
+ accepted too, so a background already written as (n_exposures, 1, 1) or
59
+ (1, X, Y) passes through untouched. Rank is what disambiguates the forms,
60
+ so a shape that is merely broadcastable by NumPy's trailing-axis rule is
61
+ rejected rather than guessed at.
62
+
63
+ Returns a NumPy array in the caller's layout. A per-image background that
64
+ does not actually vary across scan positions -- `np.broadcast_to(frame,
65
+ stack.shape)`, the usual way of saying "this frame everywhere" -- collapses
66
+ to the per-exposure form, so the launcher uploads it once rather than
67
+ re-sending a stack-sized copy with every chunk.
68
+ """
69
+ bg = np.asarray(0.0 if background is None else background)
70
+ stack_shape = tuple(stack_shape)
71
+ n_exposures = stack_shape[0]
72
+ image = stack_shape[2:] if scan_axis else stack_shape[1:]
73
+
74
+ def fits(target):
75
+ return len(bg.shape) == len(target) and all(
76
+ b in (1, t) for b, t in zip(bg.shape, target, strict=True)
77
+ )
78
+
79
+ if bg.ndim == 0 or bg.shape == (1,):
80
+ return bg.reshape(())
81
+
82
+ if bg.shape == (n_exposures,):
83
+ return bg.reshape((n_exposures,) + (1,) * len(image))
84
+
85
+ if fits(image) or fits((n_exposures, *image)):
86
+ return bg
87
+
88
+ if scan_axis and fits(stack_shape):
89
+ # A singleton or zero-strided scan axis means the frame was broadcast,
90
+ # not measured per position, so the axis carries nothing worth keeping.
91
+ if bg.shape[1] == 1 or bg.strides[1] == 0:
92
+ return bg[:, 0]
93
+ return bg
94
+
95
+ accepted = [f"a scalar, {(n_exposures,)}, {image}, {(n_exposures, *image)}"]
96
+ if scan_axis:
97
+ accepted.append(f"or {stack_shape}")
98
+ raise ValueError(
99
+ f"background shape {bg.shape} does not fit a stack of shape "
100
+ f"{stack_shape}. Expected {' '.join(accepted)}."
101
+ )
102
+
103
+
104
+ def to_numpy(x):
105
+ """Public results are NumPy, so downstream `.flat`, h5py and in-place idioms work."""
106
+ return np.asarray(x)
107
+
108
+
109
+ # Above this censoring threshold, XLA's float32 igamma loses accuracy near
110
+ # rate ~ threshold -- exactly where the EM operates on censored pixels. Measured
111
+ # relative error at rate = 0.9 * threshold: 1e-3 at 4096, 2e-2 at 16383, 1e-1 at
112
+ # 65279. Below the bound the error stays at or under 2e-3.
113
+ X32_SAFE_THRESHOLD = 4096.0
114
+
115
+
116
+ def warn_if_precision_risk(threshold):
117
+ """Warn once when single precision is not accurate enough for this threshold."""
118
+ global _X64_WARNED
119
+ if _X64_WARNED or jax.config.jax_enable_x64:
120
+ return
121
+ if float(np.max(np.asarray(threshold))) <= X32_SAFE_THRESHOLD:
122
+ return
123
+ _X64_WARNED = True
124
+ warnings.warn(
125
+ f"censoring threshold exceeds {X32_SAFE_THRESHOLD:.0f}, where float32 "
126
+ "evaluation of the censored-Poisson expectation loses up to ~10% "
127
+ "accuracy for rates near the threshold. Call bayes_mef.enable_x64() "
128
+ "(or set JAX_ENABLE_X64=1) before creating any array to run in double "
129
+ "precision.",
130
+ UserWarning,
131
+ stacklevel=3,
132
+ )