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.
Files changed (42) hide show
  1. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/.github/workflows/ci.yml +2 -2
  2. {toolsandogh-0.4.0/src/toolsandogh.egg-info → toolsandogh-0.4.2}/PKG-INFO +1 -1
  3. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_locate.py +56 -51
  4. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_locate.py +9 -5
  5. {toolsandogh-0.4.0 → toolsandogh-0.4.2/src/toolsandogh.egg-info}/PKG-INFO +1 -1
  6. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh.egg-info/scm_version.json +2 -2
  7. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/.github/workflows/release.yml +0 -0
  8. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/.gitignore +0 -0
  9. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/.pre-commit-config.yaml +0 -0
  10. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/DESIGN.md +0 -0
  11. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/LICENSE +0 -0
  12. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/README.md +0 -0
  13. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/benchmarks/bench_locate.py +0 -0
  14. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/demos/demo_iscat_50nm_bead.py +0 -0
  15. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/pyproject.toml +0 -0
  16. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/setup.cfg +0 -0
  17. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/__init__.py +0 -0
  18. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_canonicalize_video.py +0 -0
  19. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_generate_video.py +0 -0
  20. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_link.py +0 -0
  21. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_load_video.py +0 -0
  22. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_rolling.py +0 -0
  23. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_rvt.py +0 -0
  24. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_simulate_particles.py +0 -0
  25. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_simulate_psf.py +0 -0
  26. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_store_video.py +0 -0
  27. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/_validate_video.py +0 -0
  28. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/__init__.py +0 -0
  29. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/conftest.py +0 -0
  30. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_canonicalize_video.py +0 -0
  31. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_io.py +0 -0
  32. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_link.py +0 -0
  33. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_rolling.py +0 -0
  34. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_simulate_particles.py +0 -0
  35. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/test_simulate_psf.py +0 -0
  36. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh/tests/testfile.tiff +0 -0
  37. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh.egg-info/SOURCES.txt +0 -0
  38. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh.egg-info/dependency_links.txt +0 -0
  39. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh.egg-info/requires.txt +0 -0
  40. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh.egg-info/scm_file_list.json +0 -0
  41. {toolsandogh-0.4.0 → toolsandogh-0.4.2}/src/toolsandogh.egg-info/top_level.txt +0 -0
  42. {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: 8
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: 8
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: toolsandogh
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: A collection of Python scripts for iSCAT microscopy data analysis
5
5
  Author-email: Marco Heisig <marco.heisig@mpl.mpg.de>
6
6
  License-Expression: GPL-3.0-or-later
@@ -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 robustly from each chunk
122
- via the median absolute deviation of second differences. The
123
- value normalises the ``chi2``, ``reduced_chi2`` and ``snr`` output
124
- columns; supplying a known value yields statistics that are
125
- comparable across runs and devices. The default is ``None``.
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 chunk as described under the
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 robustly from the
347
- chunk's active frames via the median absolute deviation of
348
- second differences. The value normalises the ``chi2``,
349
- ``reduced_chi2`` and ``snr`` output columns. The default is
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-pixel noise standard deviation: either the value
417
- # supplied by the caller or a robust estimate from the chunk's active
418
- # frames. It is kept as a device-resident scalar so no extra host sync
419
- # is needed before the batched fit.
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
- noise_sigma_arr = _estimate_noise_sigma(chunk, n_active_frames)
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.asarray(noise_sigma, dtype=chunk.dtype)
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=noise_sigma_arr,
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-pixel noise standard deviation used to
644
- normalise each emitter's ``chi2``, ``reduced_chi2`` and ``snr``
645
- outputs. It is a scalar shared across the batch and is passed to
646
- the per-emitter closure unbatched (``in_axes=None``) so it is not
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, None),
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
- A second-difference filter (taps ``[1, -2, 1]``) cancels smooth
981
- signal and constant background, so its output is dominated by noise.
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
- * ``hf_gain`` -- the L2 gain of the high-pass filter, computed as
990
- ``sqrt(sum(taps**2))``; for ``[1, -2, 1]`` this is ``sqrt(6)``.
991
- * ``mad_to_sigma`` -- the MAD-to-standard-deviation factor for a
992
- normal distribution, ``1 / norm.ppf(0.75)`` (about 1.4826).
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
- :func:`_fit_outputs` dominates in practice.
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.asarray(jnp.finfo(active.dtype).tiny, dtype=active.dtype)
1016
- d = jnp.concatenate(diffs)
1017
- med = jnp.median(d)
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 robust estimator recovers a known noise std from pure noise."""
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 = float(np.asarray(_estimate_noise_sigma(noise, 1)))
836
- # The MAD estimate from ~8k second-difference samples is accurate to
837
- # a few percent; allow a generous band.
838
- assert 0.27 < est < 0.33
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: toolsandogh
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: A collection of Python scripts for iSCAT microscopy data analysis
5
5
  Author-email: Marco Heisig <marco.heisig@mpl.mpg.de>
6
6
  License-Expression: GPL-3.0-or-later
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "0.4.0",
2
+ "tag": "0.4.2",
3
3
  "distance": 0,
4
- "node": "g0cab170e9bfd47fb365802f94b979ddfe24c933d",
4
+ "node": "ga8d187143c3b6e231689d85e1d056ab4e5179ff8",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-07-16"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes