flimkit 0.13.8__tar.gz → 0.14.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.
- {flimkit-0.13.8 → flimkit-0.14.0}/PKG-INFO +26 -3
- {flimkit-0.13.8 → flimkit-0.14.0}/README.md +22 -1
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/FLIM/fit_tools.py +84 -16
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/FLIM/fitters.py +357 -91
- flimkit-0.14.0/flimkit/FLIM/nb_freetau.py +188 -0
- flimkit-0.14.0/flimkit/FLIM/nb_freetau_cuda.py +274 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/GPU/__init__.py +59 -7
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/GPU/_base.py +17 -8
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/GPU/torch_backend.py +122 -8
- flimkit-0.14.0/flimkit/GPU/torch_lm.py +122 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/controller.py +3 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/expert_settings.py +20 -25
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/gui.py +405 -52
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/phasor_panel.py +1 -2
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/project_panel.py +29 -18
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/roi_tools.py +1 -4
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/_version.py +4 -2
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/BH/reader.py +27 -13
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/PS/reader.py +54 -31
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/PTU/reader.py +6 -3
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/PTU/stitch.py +13 -5
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/signal.py +2 -2
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/interactive.py +13 -1
- flimkit-0.14.0/flimkit/phasor/export.py +150 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/plugins/__init__.py +10 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/plugins/registry.py +78 -2
- flimkit-0.14.0/flimkit/utils/app_update.py +206 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/apply_settings.py +36 -6
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/export_png.py +2 -2
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/misc.py +33 -21
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/update_check.py +25 -29
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit.egg-info/PKG-INFO +26 -3
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit.egg-info/SOURCES.txt +5 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit.egg-info/requires.txt +4 -1
- {flimkit-0.13.8 → flimkit-0.14.0}/pyproject.toml +4 -1
- {flimkit-0.13.8 → flimkit-0.14.0}/LICENSE.md +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/FLIM/__init__.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/FLIM/assemble.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/FLIM/batch.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/FLIM/bg_tools.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/FLIM/irf_tools.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/FLIM/models.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/GPU/cuda.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/GPU/mlx_backend.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/GPU/mps.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/GPU/rocm.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/app_state.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/fit_help.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/fov_preview.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/icon.icns +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/icon.ico +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/icon.png +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/irf_widget.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/mode_controller.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/modes/__init__.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/modes/base.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/modes/batch_mode.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/modes/fov_mode.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/modes/irf_mode.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/modes/phasor_mode.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/modes/stitch_mode.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/progress_window.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/results_panel.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/UI/utils.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/__init__.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/cli.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/configs.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/dialogs.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/BH/__init__.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/BH/writer.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/ISS/__init__.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/ISS/fdflim.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/ISS/image.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/ISS/reader.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/PS/__init__.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/PTU/__init__.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/PTU/decode.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/PTU/phu.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/PTU/series.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/PTU/tools.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/__init__.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/flim_file.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/formats/phasor.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/image/tools.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/mpl_backend.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/phasor/__init__.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/phasor/filters.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/phasor/fret.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/phasor/interactive.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/phasor/peaks.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/phasor/signal.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/phasor_launcher.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/plugins/bindings.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/plugins/builtin/__init__.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/plugins/builtin/core_tools.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/plugins/loader.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/project.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/synth.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/__init__.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/batch_fit.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/config_manager.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/config_snapshot.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/crash_handler.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/display.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/enhanced_outputs.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/fancy.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/lifetime_image.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/ome_zarr.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/plotting.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/roi.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/session.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/xlsx_tools.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit/utils/xml_utils.py +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit.egg-info/dependency_links.txt +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit.egg-info/entry_points.txt +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/flimkit.egg-info/top_level.txt +0 -0
- {flimkit-0.13.8 → flimkit-0.14.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flimkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Fluorescence lifetime imaging analysis: reading, fitting, phasors and stitching
|
|
5
5
|
Author-email: Alex Hunt <alexander.hunt@ed.ac.uk>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,7 +29,7 @@ Requires-Dist: pandas==3.0.1
|
|
|
29
29
|
Requires-Dist: Pillow>=10.0.0
|
|
30
30
|
Requires-Dist: psutil
|
|
31
31
|
Requires-Dist: phasorpy==0.12
|
|
32
|
-
Requires-Dist: photonsfile
|
|
32
|
+
Requires-Dist: photonsfile>=2026.9.29.1
|
|
33
33
|
Requires-Dist: ptufile
|
|
34
34
|
Requires-Dist: PyWavelets>=1.4.0
|
|
35
35
|
Requires-Dist: scipy==1.17.1
|
|
@@ -54,6 +54,8 @@ Provides-Extra: mlx
|
|
|
54
54
|
Requires-Dist: mlx; extra == "mlx"
|
|
55
55
|
Provides-Extra: torch
|
|
56
56
|
Requires-Dist: torch; extra == "torch"
|
|
57
|
+
Provides-Extra: cuda
|
|
58
|
+
Requires-Dist: numba-cuda[cu12]; extra == "cuda"
|
|
57
59
|
Provides-Extra: test
|
|
58
60
|
Requires-Dist: pytest>=9.1.0; extra == "test"
|
|
59
61
|
Provides-Extra: all
|
|
@@ -98,6 +100,9 @@ to every release.
|
|
|
98
100
|
On macOS the app is self-signed, so the first launch needs right-click then
|
|
99
101
|
Open. A double-click will be refused.
|
|
100
102
|
|
|
103
|
+
To update it later, Help > Check for Updates downloads the new build and swaps
|
|
104
|
+
it in place. On macOS, move the app out of Downloads first.
|
|
105
|
+
|
|
101
106
|
This route gets the desktop application only. Everything below is for scripting,
|
|
102
107
|
the terminal, or GPU fitting.
|
|
103
108
|
|
|
@@ -105,6 +110,19 @@ the terminal, or GPU fitting.
|
|
|
105
110
|
|
|
106
111
|
Python ≥ 3.12 required (3.14 recommended, official builds use 3.14).
|
|
107
112
|
|
|
113
|
+
Make an environment first, with conda or uv:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
conda create -n flimkit -c conda-forge python=3.14 && conda activate flimkit
|
|
117
|
+
# or
|
|
118
|
+
uv venv --seed --python 3.14 flimkit-env && source flimkit-env/bin/activate
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
With uv, `uv pip install` works in place of `pip install` below. `--seed` adds
|
|
122
|
+
pip, which `install.py` needs. The
|
|
123
|
+
[installation docs](https://github.com/FLIMKit/FLIMKit/wiki/Requirements-and-Installation)
|
|
124
|
+
cover Windows and which of the two to pick.
|
|
125
|
+
|
|
108
126
|
For analysis in scripts, notebooks and the terminal:
|
|
109
127
|
|
|
110
128
|
```bash
|
|
@@ -131,6 +149,11 @@ For development work (PyInstaller + test dependencies):
|
|
|
131
149
|
python install.py --dev
|
|
132
150
|
```
|
|
133
151
|
|
|
152
|
+
To update a clone later, `python update.py` pulls the latest code and reruns
|
|
153
|
+
`install.py`. It also rebuilds an app you built into `dist/`, and
|
|
154
|
+
`--app-path /Applications/FLIMKit.app` copies that build over the one you open.
|
|
155
|
+
`--dry-run` shows what it would do without changing anything.
|
|
156
|
+
|
|
134
157
|
## Docker / TrueNAS SCALE
|
|
135
158
|
|
|
136
159
|
Pre-built images are on Docker Hub as `alex1075/flimkit`. They run FLIMKit with the [web UI](https://github.com/FLIMKit/flimkit-web-ui), so the whole app is used from a browser with nothing installed on the client.
|
|
@@ -289,7 +312,7 @@ Per-pixel fitting uses a batched matrix solver that runs on GPU when a supported
|
|
|
289
312
|
|
|
290
313
|
`python install.py` detects your hardware and installs the right backend automatically. No extra flags needed at runtime.
|
|
291
314
|
|
|
292
|
-
**Limitations:** `--free-tau-perpixel`
|
|
315
|
+
**Limitations:** `--free-tau-perpixel` with two or more components prepares the batch on the GPU but fits each pixel with SciPy on the CPU, so a GPU gains little there. `fit_summed` (single global fit) is always CPU, and fast enough not to matter.
|
|
293
316
|
|
|
294
317
|
**Compiled app and GPU:** The compiled app bundles whatever GPU libraries are installed on the *build* machine. A binary built on Apple Silicon will have MLX/MPS; one built on a CUDA machine will have CUDA. If you need GPU in the compiled app, build it yourself on the target hardware. See [Compiled App](#compiled-app-macos--windows--linux).
|
|
295
318
|
|
|
@@ -36,6 +36,9 @@ to every release.
|
|
|
36
36
|
On macOS the app is self-signed, so the first launch needs right-click then
|
|
37
37
|
Open. A double-click will be refused.
|
|
38
38
|
|
|
39
|
+
To update it later, Help > Check for Updates downloads the new build and swaps
|
|
40
|
+
it in place. On macOS, move the app out of Downloads first.
|
|
41
|
+
|
|
39
42
|
This route gets the desktop application only. Everything below is for scripting,
|
|
40
43
|
the terminal, or GPU fitting.
|
|
41
44
|
|
|
@@ -43,6 +46,19 @@ the terminal, or GPU fitting.
|
|
|
43
46
|
|
|
44
47
|
Python ≥ 3.12 required (3.14 recommended, official builds use 3.14).
|
|
45
48
|
|
|
49
|
+
Make an environment first, with conda or uv:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
conda create -n flimkit -c conda-forge python=3.14 && conda activate flimkit
|
|
53
|
+
# or
|
|
54
|
+
uv venv --seed --python 3.14 flimkit-env && source flimkit-env/bin/activate
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
With uv, `uv pip install` works in place of `pip install` below. `--seed` adds
|
|
58
|
+
pip, which `install.py` needs. The
|
|
59
|
+
[installation docs](https://github.com/FLIMKit/FLIMKit/wiki/Requirements-and-Installation)
|
|
60
|
+
cover Windows and which of the two to pick.
|
|
61
|
+
|
|
46
62
|
For analysis in scripts, notebooks and the terminal:
|
|
47
63
|
|
|
48
64
|
```bash
|
|
@@ -69,6 +85,11 @@ For development work (PyInstaller + test dependencies):
|
|
|
69
85
|
python install.py --dev
|
|
70
86
|
```
|
|
71
87
|
|
|
88
|
+
To update a clone later, `python update.py` pulls the latest code and reruns
|
|
89
|
+
`install.py`. It also rebuilds an app you built into `dist/`, and
|
|
90
|
+
`--app-path /Applications/FLIMKit.app` copies that build over the one you open.
|
|
91
|
+
`--dry-run` shows what it would do without changing anything.
|
|
92
|
+
|
|
72
93
|
## Docker / TrueNAS SCALE
|
|
73
94
|
|
|
74
95
|
Pre-built images are on Docker Hub as `alex1075/flimkit`. They run FLIMKit with the [web UI](https://github.com/FLIMKit/flimkit-web-ui), so the whole app is used from a browser with nothing installed on the client.
|
|
@@ -227,7 +248,7 @@ Per-pixel fitting uses a batched matrix solver that runs on GPU when a supported
|
|
|
227
248
|
|
|
228
249
|
`python install.py` detects your hardware and installs the right backend automatically. No extra flags needed at runtime.
|
|
229
250
|
|
|
230
|
-
**Limitations:** `--free-tau-perpixel`
|
|
251
|
+
**Limitations:** `--free-tau-perpixel` with two or more components prepares the batch on the GPU but fits each pixel with SciPy on the CPU, so a GPU gains little there. `fit_summed` (single global fit) is always CPU, and fast enough not to matter.
|
|
231
252
|
|
|
232
253
|
**Compiled app and GPU:** The compiled app bundles whatever GPU libraries are installed on the *build* machine. A binary built on Apple Silicon will have MLX/MPS; one built on a CUDA machine will have CUDA. If you need GPU in the compiled app, build it yourself on the target hardware. See [Compiled App](#compiled-app-macos--windows--linux).
|
|
233
254
|
|
|
@@ -2,12 +2,10 @@ import numpy as np
|
|
|
2
2
|
from scipy.ndimage import gaussian_filter1d
|
|
3
3
|
from scipy.optimize import curve_fit
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
def distribution_dof(n_fit, n_components=1, fit_tvb=False):
|
|
7
6
|
n_params = 3 * n_components + 1 + int(fit_tvb)
|
|
8
7
|
return max(int(n_fit) - n_params, 1)
|
|
9
8
|
|
|
10
|
-
|
|
11
9
|
def chi2_terms(data, model, axis=None, dtype=float):
|
|
12
10
|
data_arr, model_arr = np.broadcast_arrays(
|
|
13
11
|
np.asarray(data, dtype=dtype), np.asarray(model, dtype=dtype))
|
|
@@ -22,7 +20,6 @@ def chi2_terms(data, model, axis=None, dtype=float):
|
|
|
22
20
|
expected = np.sum(np.minimum(model_arr, one), axis=axis)
|
|
23
21
|
return numerator, expected, valid
|
|
24
22
|
|
|
25
|
-
|
|
26
23
|
def calibrated_from_terms(numerator, expected, valid):
|
|
27
24
|
if np.ndim(expected) == 0:
|
|
28
25
|
return (float(numerator / expected)
|
|
@@ -32,14 +29,12 @@ def calibrated_from_terms(numerator, expected, valid):
|
|
|
32
29
|
numerator, expected, out=result,
|
|
33
30
|
where=valid & (expected > 0) & np.isfinite(numerator))
|
|
34
31
|
|
|
35
|
-
|
|
36
32
|
def calibrated_chi2(data, model, axis=None):
|
|
37
33
|
numerator, expected, valid = chi2_terms(data, model, axis=axis)
|
|
38
34
|
if np.ndim(expected) == 0:
|
|
39
35
|
return float(numerator / expected) if valid and expected > 0 else np.nan
|
|
40
36
|
return calibrated_from_terms(numerator, expected, valid)
|
|
41
37
|
|
|
42
|
-
|
|
43
38
|
def coates_pileup_correction(decay: np.ndarray, n_sync: int):
|
|
44
39
|
m = np.asarray(decay, dtype=float)
|
|
45
40
|
n_s = float(n_sync or 0.0)
|
|
@@ -50,7 +45,6 @@ def coates_pileup_correction(decay: np.ndarray, n_sync: int):
|
|
|
50
45
|
f'n_sync={n_s:,.0f} for {total:,.0f} photons. N_sync must exceed the photon count.')
|
|
51
46
|
cumulative = np.concatenate([[0.0], np.cumsum(m[:-1])])
|
|
52
47
|
denom = n_s - cumulative
|
|
53
|
-
# Argument of log: 1 - M(t)/(N_s - C(t))
|
|
54
48
|
with np.errstate(divide='ignore', invalid='ignore'):
|
|
55
49
|
ratio = np.where(denom > 0, m / denom, 0.0)
|
|
56
50
|
arg = 1.0 - ratio
|
|
@@ -62,9 +56,6 @@ def bins_from_ns(value_ns, tcspc_res):
|
|
|
62
56
|
return int(round(float(value_ns) * 1e-9 / tcspc_res))
|
|
63
57
|
|
|
64
58
|
def build_fit_idx(fit_start, fit_end, n_bins, exclude_bins=None):
|
|
65
|
-
# explicit bin set the fit runs over: a contiguous window minus any excluded
|
|
66
|
-
# bands (eg a reflection peak mid-decay). Dropping bins keeps the fit linear
|
|
67
|
-
# in the amplitudes, so the per-pixel projection is unaffected.
|
|
68
59
|
idx = np.arange(max(0, int(fit_start)), min(int(n_bins), int(fit_end)), dtype=int)
|
|
69
60
|
if exclude_bins:
|
|
70
61
|
keep = np.ones(idx.shape, dtype=bool)
|
|
@@ -78,7 +69,6 @@ def build_fit_idx(fit_start, fit_end, n_bins, exclude_bins=None):
|
|
|
78
69
|
def find_irf_peak_bin(decay: np.ndarray, smooth_sigma: float = 1.5):
|
|
79
70
|
smoothed = gaussian_filter1d(decay.astype(float), sigma=smooth_sigma)
|
|
80
71
|
deriv = np.gradient(smoothed)
|
|
81
|
-
# Only search in the first half of the histogram (rising edge region)
|
|
82
72
|
half = len(decay) // 2
|
|
83
73
|
peak_bin = int(np.argmax(deriv[:half]))
|
|
84
74
|
return peak_bin
|
|
@@ -109,7 +99,6 @@ def find_fit_start(decay: np.ndarray, irf_prompt: np.ndarray,
|
|
|
109
99
|
fit_start = max(0, onset - pre_bins)
|
|
110
100
|
return fit_start
|
|
111
101
|
|
|
112
|
-
|
|
113
102
|
def find_tail_fit_start(decay, peak_bin, n_bins, frac=0.9):
|
|
114
103
|
d = np.asarray(decay, dtype=float)
|
|
115
104
|
peak_val = float(d[peak_bin]) if 0 <= peak_bin < n_bins else 0.0
|
|
@@ -165,8 +154,8 @@ def find_fit_end(decay, peak_bin, tau_max_s, tcspc_res, n_bins):
|
|
|
165
154
|
if len(spikes) > 0:
|
|
166
155
|
spike_abs = search_start + spikes[0]
|
|
167
156
|
if spike_abs < candidate:
|
|
168
|
-
print(f
|
|
169
|
-
f
|
|
157
|
+
print(f' Next-period artefact at bin {spike_abs} '
|
|
158
|
+
f'({spike_abs*tcspc_res*1e9:.2f} ns). Truncating fit window.')
|
|
170
159
|
candidate = spike_abs
|
|
171
160
|
return candidate
|
|
172
161
|
|
|
@@ -207,7 +196,6 @@ def _build_bounds_dist(n_components, tau_min, tau_max, decay_peak,
|
|
|
207
196
|
lo += [0.0]; hi += [_tvb_hi]
|
|
208
197
|
return lo, hi
|
|
209
198
|
|
|
210
|
-
|
|
211
199
|
def _pack_p0_dist(n_components, tau_min, tau_max, decay_peak,
|
|
212
200
|
fit_bg, fit_sigma, bg_init, fit_tvb=False, tvb_init=0.0):
|
|
213
201
|
tmin = max(tau_min, 1e-14) * 1.001
|
|
@@ -224,7 +212,6 @@ def _pack_p0_dist(n_components, tau_min, tau_max, decay_peak,
|
|
|
224
212
|
base = np.concatenate([base, [tvb_init]])
|
|
225
213
|
return base
|
|
226
214
|
|
|
227
|
-
|
|
228
215
|
def _pack_p0(n_exp, tau_min, tau_max, decay_peak,
|
|
229
216
|
has_tail, fit_bg, fit_sigma, bg_init,
|
|
230
217
|
tau_override=None, fit_tvb=False, tvb_init=0.0):
|
|
@@ -244,4 +231,85 @@ def _pack_p0(n_exp, tau_min, tau_max, decay_peak,
|
|
|
244
231
|
base = np.concatenate([base, [tvb_init]])
|
|
245
232
|
if has_tail:
|
|
246
233
|
base = np.concatenate([base, [0.5, 20.0]])
|
|
247
|
-
return base
|
|
234
|
+
return base
|
|
235
|
+
|
|
236
|
+
def _param_scale(p, lo, hi):
|
|
237
|
+
span = hi - lo
|
|
238
|
+
scale = np.where(np.abs(p) > 0, np.abs(p), np.where(np.isfinite(span), span, 1.0))
|
|
239
|
+
return np.maximum(scale, 1e-300)
|
|
240
|
+
|
|
241
|
+
def fit_uncertainties(model_fn, popt, fit_idx, lo=None, hi=None, rel_step=1e-4):
|
|
242
|
+
p = np.asarray(popt, dtype=float)
|
|
243
|
+
k = p.size
|
|
244
|
+
lo = np.full(k, -np.inf) if lo is None else np.asarray(lo, dtype=float)
|
|
245
|
+
hi = np.full(k, np.inf) if hi is None else np.asarray(hi, dtype=float)
|
|
246
|
+
span = hi - lo
|
|
247
|
+
tol = np.where(np.isfinite(span), 1e-6 * span, 0.0)
|
|
248
|
+
free = ~((p - lo <= tol) | (hi - p <= tol))
|
|
249
|
+
scale = _param_scale(p, lo, hi)
|
|
250
|
+
m0 = np.asarray(model_fn(p), dtype=float)[fit_idx]
|
|
251
|
+
jac = np.zeros((len(fit_idx), k))
|
|
252
|
+
for j in np.flatnonzero(free):
|
|
253
|
+
h = rel_step * scale[j]
|
|
254
|
+
up = p.copy()
|
|
255
|
+
dn = p.copy()
|
|
256
|
+
if hi[j] - p[j] >= h and p[j] - lo[j] >= h:
|
|
257
|
+
up[j] += h
|
|
258
|
+
dn[j] -= h
|
|
259
|
+
width = 2.0 * h
|
|
260
|
+
elif hi[j] - p[j] >= h:
|
|
261
|
+
up[j] += h
|
|
262
|
+
width = h
|
|
263
|
+
else:
|
|
264
|
+
dn[j] -= h
|
|
265
|
+
width = h
|
|
266
|
+
diff = np.asarray(model_fn(up), dtype=float)[fit_idx] - np.asarray(model_fn(dn), dtype=float)[fit_idx]
|
|
267
|
+
jac[:, j] = diff / width * scale[j]
|
|
268
|
+
live = free & np.any(jac != 0.0, axis=0)
|
|
269
|
+
idx = np.flatnonzero(live)
|
|
270
|
+
cov = np.full((k, k), np.nan)
|
|
271
|
+
if idx.size:
|
|
272
|
+
js = jac[:, idx]
|
|
273
|
+
weight = 1.0 / np.maximum(m0, 1.0)
|
|
274
|
+
fisher = js.T @ (js * weight[:, None])
|
|
275
|
+
try:
|
|
276
|
+
cov_s = np.linalg.inv(fisher)
|
|
277
|
+
except np.linalg.LinAlgError:
|
|
278
|
+
cov_s = np.linalg.pinv(fisher)
|
|
279
|
+
cov[np.ix_(idx, idx)] = cov_s * np.outer(scale[idx], scale[idx])
|
|
280
|
+
diag = np.diag(cov)
|
|
281
|
+
stderr = np.where(np.isfinite(diag) & (diag >= 0), np.sqrt(np.abs(diag)), np.nan)
|
|
282
|
+
with np.errstate(invalid='ignore', divide='ignore'):
|
|
283
|
+
corr = cov / np.outer(stderr, stderr)
|
|
284
|
+
return dict(cov=cov, stderr=stderr, corr=corr, free=live, scale=scale)
|
|
285
|
+
|
|
286
|
+
def propagate_uncertainty(fn, popt, unc, rel_step=1e-4):
|
|
287
|
+
p = np.asarray(popt, dtype=float)
|
|
288
|
+
idx = np.flatnonzero(unc['free'])
|
|
289
|
+
if idx.size == 0:
|
|
290
|
+
return np.nan
|
|
291
|
+
grad = np.zeros(idx.size)
|
|
292
|
+
for n, j in enumerate(idx):
|
|
293
|
+
h = rel_step * unc['scale'][j]
|
|
294
|
+
up = p.copy()
|
|
295
|
+
dn = p.copy()
|
|
296
|
+
up[j] += h
|
|
297
|
+
dn[j] -= h
|
|
298
|
+
grad[n] = (float(fn(up)) - float(fn(dn))) / (2.0 * h)
|
|
299
|
+
var = float(grad @ unc['cov'][np.ix_(idx, idx)] @ grad)
|
|
300
|
+
return float(np.sqrt(var)) if np.isfinite(var) and var >= 0 else np.nan
|
|
301
|
+
|
|
302
|
+
def uncertainty_warnings(taus_ns, taus_err_ns, tau_corr, rel_limit=0.1, corr_limit=0.95):
|
|
303
|
+
notes = []
|
|
304
|
+
taus_ns = np.atleast_1d(np.asarray(taus_ns, dtype=float))
|
|
305
|
+
errs = np.atleast_1d(np.asarray(taus_err_ns, dtype=float))
|
|
306
|
+
for i, (tau, err) in enumerate(zip(taus_ns, errs), start=1):
|
|
307
|
+
if np.isfinite(err) and tau > 0 and err / tau > rel_limit:
|
|
308
|
+
notes.append(f'τ{i} is poorly determined: ±{err:.3g} ns on {tau:.3g} ns ({100 * err / tau:.0f}%)')
|
|
309
|
+
corr = np.atleast_2d(np.asarray(tau_corr, dtype=float))
|
|
310
|
+
for i in range(corr.shape[0]):
|
|
311
|
+
for j in range(i + 1, corr.shape[1]):
|
|
312
|
+
if np.isfinite(corr[i, j]) and abs(corr[i, j]) > corr_limit:
|
|
313
|
+
notes.append(f'τ{i + 1} and τ{j + 1} are strongly correlated (r = {corr[i, j]:+.2f}): '
|
|
314
|
+
f'the data cannot separate them well')
|
|
315
|
+
return notes
|