toolsandogh 0.4.0__tar.gz → 0.4.2__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.
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/.github/workflows/ci.yml +2 -2
- {toolsandogh-0.4.0/src/toolsandogh.egg-info → toolsandogh-0.4.2}/PKG-INFO +1 -1
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_locate.py +56 -51
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_locate.py +9 -5
- {toolsandogh-0.4.0 → toolsandogh-0.4.2/src/toolsandogh.egg-info}/PKG-INFO +1 -1
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh.egg-info/scm_version.json +2 -2
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/.github/workflows/release.yml +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/.gitignore +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/.pre-commit-config.yaml +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/DESIGN.md +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/LICENSE +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/README.md +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/benchmarks/bench_locate.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/demos/demo_iscat_50nm_bead.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/pyproject.toml +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/setup.cfg +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/__init__.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_canonicalize_video.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_generate_video.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_link.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_load_video.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_rolling.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_rvt.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_simulate_particles.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_simulate_psf.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_store_video.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_validate_video.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/__init__.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/conftest.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_canonicalize_video.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_io.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_link.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_rolling.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_simulate_particles.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_simulate_psf.py +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/testfile.tiff +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh.egg-info/SOURCES.txt +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh.egg-info/dependency_links.txt +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh.egg-info/requires.txt +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh.egg-info/scm_file_list.json +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh.egg-info/top_level.txt +0 -0
- {toolsandogh-0.4.0 → toolsandogh-0.4.2}/uv.lock +0 -0
|
@@ -16,7 +16,7 @@ jobs:
|
|
|
16
16
|
# committed uv.lock via --frozen, so any lockfile drift fails loudly.
|
|
17
17
|
pre-commit:
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
|
-
timeout-minutes:
|
|
19
|
+
timeout-minutes: 15
|
|
20
20
|
env:
|
|
21
21
|
# Uniform cache path across OSes so the actions/cache `path` below
|
|
22
22
|
# matches what conftest.py writes (it honors this env var via
|
|
@@ -43,7 +43,7 @@ jobs:
|
|
|
43
43
|
# uv.lock), so the lowest-direct cell exercises the real lower bounds.
|
|
44
44
|
test:
|
|
45
45
|
runs-on: ${{ matrix.os }}
|
|
46
|
-
timeout-minutes:
|
|
46
|
+
timeout-minutes: 15
|
|
47
47
|
env:
|
|
48
48
|
JAX_COMPILATION_CACHE_DIR: ~/.cache/jax-compilation-cache
|
|
49
49
|
# Self-diagnosing safety net for hangs: dump all thread stacks on
|
|
@@ -17,7 +17,6 @@ from typing import Callable, Literal
|
|
|
17
17
|
import jax
|
|
18
18
|
import jax.numpy as jnp
|
|
19
19
|
import jax.scipy.signal as jsignal
|
|
20
|
-
import jax.scipy.stats as jstats
|
|
21
20
|
import numpy as np
|
|
22
21
|
import numpy.typing as npt
|
|
23
22
|
import polars
|
|
@@ -118,11 +117,12 @@ def locate(
|
|
|
118
117
|
``atol`` are marked ``converged=True``.
|
|
119
118
|
noise_sigma : float, optional
|
|
120
119
|
Standard deviation of the per-pixel Gaussian noise. When
|
|
121
|
-
``None`` (the default) it is estimated
|
|
122
|
-
|
|
123
|
-
value normalises the
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
``None`` (the default) it is estimated **per frame** from the
|
|
121
|
+
standard deviation of second differences (a single-pass
|
|
122
|
+
Laplacian-based noise estimator). The value normalises the
|
|
123
|
+
``chi2``, ``reduced_chi2`` and ``snr`` output columns; supplying
|
|
124
|
+
a known value yields statistics that are comparable across runs
|
|
125
|
+
and devices. The default is ``None``.
|
|
126
126
|
dtype : numpy dtype
|
|
127
127
|
Dtype used for the data during localization.
|
|
128
128
|
on_progress : callable, optional
|
|
@@ -153,7 +153,7 @@ def locate(
|
|
|
153
153
|
under a correct model and Gaussian noise. The ``reduced_chi2`` column
|
|
154
154
|
is ``chi2 / dof`` and has expectation 1. The ``snr`` column is the
|
|
155
155
|
fitted contrast divided by ``noise_sigma``. When ``noise_sigma`` is
|
|
156
|
-
not supplied, it is estimated per
|
|
156
|
+
not supplied, it is estimated per frame as described under the
|
|
157
157
|
``noise_sigma`` parameter.
|
|
158
158
|
"""
|
|
159
159
|
# Canonicalize the inputs. ``video`` may be any array-like (a raw
|
|
@@ -343,11 +343,10 @@ def _locate_in_chunk(
|
|
|
343
343
|
``atol`` are marked ``converged=True``.
|
|
344
344
|
noise_sigma : float, optional
|
|
345
345
|
Standard deviation of the per-pixel Gaussian noise. When
|
|
346
|
-
``None`` (the default) it is estimated
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
``None``.
|
|
346
|
+
``None`` (the default) it is estimated **per frame** from the
|
|
347
|
+
standard deviation of second differences. The value normalises
|
|
348
|
+
the ``chi2``, ``reduced_chi2`` and ``snr`` output columns. The
|
|
349
|
+
default is ``None``.
|
|
351
350
|
|
|
352
351
|
Returns
|
|
353
352
|
-------
|
|
@@ -413,14 +412,17 @@ def _locate_in_chunk(
|
|
|
413
412
|
# all emitters, instead of one eager pad+slice per emitter).
|
|
414
413
|
stamps = _extract_stamps_batch(chunk, b_idx, rows_z, rows_y, rows_x, (Pz, Py, Px))
|
|
415
414
|
|
|
416
|
-
# Resolve the per-
|
|
417
|
-
#
|
|
418
|
-
#
|
|
419
|
-
#
|
|
415
|
+
# Resolve the per-emitter noise standard deviation. When the caller
|
|
416
|
+
# does not supply one, it is estimated per frame from the standard
|
|
417
|
+
# deviation of second differences (a single-pass O(n) estimator) and
|
|
418
|
+
# gathered per emitter by frame index. A supplied scalar is broadcast
|
|
419
|
+
# across all emitters. The result is a (n_emitters,) device array
|
|
420
|
+
# kept resident so no extra host sync is needed before the batched fit.
|
|
420
421
|
if noise_sigma is None:
|
|
421
|
-
|
|
422
|
+
per_frame_sigma = _estimate_noise_sigma(chunk, n_active_frames) # (B,)
|
|
423
|
+
noise_sigma_arr = per_frame_sigma[jnp.asarray(b_idx)] # (n_emitters,)
|
|
422
424
|
else:
|
|
423
|
-
noise_sigma_arr = jnp.
|
|
425
|
+
noise_sigma_arr = jnp.full((n_emitters,), noise_sigma, dtype=chunk.dtype)
|
|
424
426
|
|
|
425
427
|
# 4. Batched Levenberg--Marquardt refinement. Stamps are processed in
|
|
426
428
|
# sub-batches whose size is the next power of two (capped at
|
|
@@ -447,6 +449,7 @@ def _locate_in_chunk(
|
|
|
447
449
|
n_iter_stacks: list[jax.Array] = []
|
|
448
450
|
for i in range(0, n_emitters, batch_size):
|
|
449
451
|
sub_stamps = stamps[i : i + batch_size]
|
|
452
|
+
sub_sigma = noise_sigma_arr[i : i + batch_size]
|
|
450
453
|
n_sub = sub_stamps.shape[0]
|
|
451
454
|
if n_sub < batch_size:
|
|
452
455
|
sub_stamps = jnp.pad(
|
|
@@ -454,12 +457,13 @@ def _locate_in_chunk(
|
|
|
454
457
|
((0, batch_size - n_sub), (0, 0), (0, 0), (0, 0)),
|
|
455
458
|
mode="constant",
|
|
456
459
|
)
|
|
460
|
+
sub_sigma = jnp.pad(sub_sigma, (0, batch_size - n_sub), mode="constant")
|
|
457
461
|
sub_fit = _fit_emitters_batch(
|
|
458
462
|
sub_stamps,
|
|
459
463
|
psf,
|
|
460
464
|
iterations=iterations,
|
|
461
465
|
atol=atol,
|
|
462
|
-
noise_sigma=
|
|
466
|
+
noise_sigma=sub_sigma,
|
|
463
467
|
)
|
|
464
468
|
sub_stack = jnp.stack([sub_fit[k] for k in fit_keys]) # (9, batch)
|
|
465
469
|
fit_stacks.append(sub_stack[:, :n_sub])
|
|
@@ -640,11 +644,10 @@ def _fit_emitters_batch(
|
|
|
640
644
|
through ``jax.vmap``. Convergence is decided afterwards by
|
|
641
645
|
comparing the final parameter update to ``atol``.
|
|
642
646
|
|
|
643
|
-
``noise_sigma`` is the per-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
replicated.
|
|
647
|
+
``noise_sigma`` is the per-emitter noise standard deviation (a
|
|
648
|
+
``(batch,)`` array, one value per emitter) used to normalise each
|
|
649
|
+
emitter's ``chi2``, ``reduced_chi2`` and ``snr`` outputs. It is
|
|
650
|
+
mapped alongside the stamps (``in_axes=(0, 0)``).
|
|
648
651
|
|
|
649
652
|
Returns a dictionary of per-emitter outputs:
|
|
650
653
|
``contrast``, ``background``, ``z_offset``, ``y_offset``,
|
|
@@ -662,7 +665,7 @@ def _fit_emitters_batch(
|
|
|
662
665
|
atol=atol,
|
|
663
666
|
noise_sigma=sig,
|
|
664
667
|
),
|
|
665
|
-
in_axes=(0,
|
|
668
|
+
in_axes=(0, 0),
|
|
666
669
|
)
|
|
667
670
|
return fit_one(stamps, sigma)
|
|
668
671
|
|
|
@@ -975,48 +978,50 @@ def _estimate_noise_sigma(
|
|
|
975
978
|
n_active_frames: int,
|
|
976
979
|
) -> jax.Array:
|
|
977
980
|
"""
|
|
978
|
-
Estimate the per-pixel noise standard deviation of ``chunk
|
|
981
|
+
Estimate the per-pixel noise standard deviation of ``chunk`` per frame.
|
|
979
982
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
Second differences are pooled over every spatial axis with at least
|
|
983
|
-
three samples, and the median absolute deviation (MAD) -- a robust
|
|
984
|
-
scale estimator tolerant of the sparse emitter pixels -- is
|
|
985
|
-
converted to a standard deviation.
|
|
986
|
-
|
|
987
|
-
Two derived (non hard-coded) constants appear:
|
|
983
|
+
Returns a ``(n_active_frames,)`` array with one sigma value per
|
|
984
|
+
frame.
|
|
988
985
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
986
|
+
A second-difference filter (taps ``[1, -2, 1]``) cancels smooth
|
|
987
|
+
signal and constant background, so its output is dominated by
|
|
988
|
+
noise. Second differences are computed along every spatial axis
|
|
989
|
+
with at least three samples, pooled per frame, and the per-frame
|
|
990
|
+
standard deviation is divided by the filter's L2 gain
|
|
991
|
+
``sqrt(sum(taps**2))`` (``sqrt(6)`` for ``[1, -2, 1]``) to recover
|
|
992
|
+
the underlying noise sigma.
|
|
993
|
+
|
|
994
|
+
This is a single-pass O(n) estimator (one fused reduction per
|
|
995
|
+
frame, no sort). It is non-robust to bright emitter pixels, but
|
|
996
|
+
emitters are sparse and this is a fallback for when the caller does
|
|
997
|
+
not supply a known ``noise_sigma``; a supplied value always takes
|
|
998
|
+
precedence. On pure Gaussian noise the standard deviation is a
|
|
999
|
+
lower-variance estimator than the median absolute deviation.
|
|
993
1000
|
|
|
994
1001
|
A tiny floor guards against noise-free data, where the estimate is
|
|
995
|
-
exactly zero; the per-emitter precision floor in
|
|
996
|
-
|
|
1002
|
+
exactly zero; the per-emitter precision floor in :func:`_fit_outputs`
|
|
1003
|
+
dominates in practice.
|
|
997
1004
|
"""
|
|
998
|
-
active = chunk[:n_active_frames]
|
|
1005
|
+
active = chunk[:n_active_frames] # (B, Z, Y, X)
|
|
1006
|
+
B = active.shape[0]
|
|
999
1007
|
Z, Y, X = active.shape[1], active.shape[2], active.shape[3]
|
|
1000
1008
|
taps = jnp.array([1.0, -2.0, 1.0], dtype=active.dtype)
|
|
1001
|
-
hf_gain = jnp.sqrt(jnp.sum(taps**2))
|
|
1002
|
-
mad_to_sigma = 1.0 / jstats.norm.ppf(0.75)
|
|
1009
|
+
hf_gain = jnp.sqrt(jnp.sum(taps**2)) # sqrt(6)
|
|
1003
1010
|
diffs: list[jax.Array] = []
|
|
1004
1011
|
if X >= 3:
|
|
1005
|
-
diffs.append((active[..., 2:] - 2 * active[..., 1:-1] + active[..., :-2]).reshape(-1))
|
|
1012
|
+
diffs.append((active[..., 2:] - 2 * active[..., 1:-1] + active[..., :-2]).reshape(B, -1))
|
|
1006
1013
|
if Y >= 3:
|
|
1007
1014
|
diffs.append(
|
|
1008
|
-
(active[:, :, 2:, :] - 2 * active[:, :, 1:-1, :] + active[:, :, :-2, :]).reshape(-1)
|
|
1015
|
+
(active[:, :, 2:, :] - 2 * active[:, :, 1:-1, :] + active[:, :, :-2, :]).reshape(B, -1)
|
|
1009
1016
|
)
|
|
1010
1017
|
if Z >= 3:
|
|
1011
1018
|
diffs.append(
|
|
1012
|
-
(active[:, 2:, :, :] - 2 * active[:, 1:-1, :, :] + active[:, :-2, :, :]).reshape(-1)
|
|
1019
|
+
(active[:, 2:, :, :] - 2 * active[:, 1:-1, :, :] + active[:, :-2, :, :]).reshape(B, -1)
|
|
1013
1020
|
)
|
|
1014
1021
|
if not diffs:
|
|
1015
|
-
return jnp.
|
|
1016
|
-
d = jnp.concatenate(diffs)
|
|
1017
|
-
|
|
1018
|
-
mad = jnp.median(jnp.abs(d - med))
|
|
1019
|
-
sigma = mad_to_sigma * mad / hf_gain
|
|
1022
|
+
return jnp.full((B,), jnp.finfo(active.dtype).tiny, dtype=active.dtype)
|
|
1023
|
+
d = jnp.concatenate(diffs, axis=1) # (B, n_samples)
|
|
1024
|
+
sigma = jnp.std(d, axis=1) / hf_gain # (B,)
|
|
1020
1025
|
floor = jnp.asarray(jnp.finfo(active.dtype).tiny, dtype=active.dtype)
|
|
1021
1026
|
return jnp.maximum(sigma, floor)
|
|
1022
1027
|
|
|
@@ -825,17 +825,21 @@ def test_locate_chi2_near_dof_for_good_fit() -> None:
|
|
|
825
825
|
|
|
826
826
|
|
|
827
827
|
def test_estimate_noise_sigma_recovers_known_std() -> None:
|
|
828
|
-
"""The
|
|
828
|
+
"""The per-frame estimator recovers a known noise std from pure noise."""
|
|
829
829
|
import jax
|
|
830
830
|
|
|
831
831
|
from toolsandogh._locate import _estimate_noise_sigma
|
|
832
832
|
|
|
833
833
|
key = jax.random.PRNGKey(0)
|
|
834
834
|
noise = 0.3 * jax.random.normal(key, (1, 1, 64, 64), dtype=np.float32)
|
|
835
|
-
est =
|
|
836
|
-
# The
|
|
837
|
-
|
|
838
|
-
|
|
835
|
+
est = np.asarray(_estimate_noise_sigma(noise, 1))
|
|
836
|
+
# The estimator returns one sigma per frame.
|
|
837
|
+
assert est.shape == (1,)
|
|
838
|
+
est = float(est[0])
|
|
839
|
+
# The std-of-second-differences estimator from ~8k samples recovers
|
|
840
|
+
# the true sigma to within a fraction of a percent; allow a small
|
|
841
|
+
# band for float32 reduction noise.
|
|
842
|
+
assert 0.285 < est < 0.315
|
|
839
843
|
|
|
840
844
|
|
|
841
845
|
def test_locate_noise_sigma_validation() -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|