lbm_suite2p_python 2.5.4__tar.gz → 3.0.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 (35) hide show
  1. {lbm_suite2p_python-2.5.4/lbm_suite2p_python.egg-info → lbm_suite2p_python-3.0.0}/PKG-INFO +6 -6
  2. lbm_suite2p_python-3.0.0/lbm_suite2p_python/_padding_shim.py +140 -0
  3. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/cellpose.py +16 -11
  4. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/cli.py +529 -529
  5. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/conversion.py +1 -1
  6. lbm_suite2p_python-3.0.0/lbm_suite2p_python/db_settings.py +441 -0
  7. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/default_ops.py +5 -5
  8. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/grid_search.py +24 -15
  9. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/gui.py +1 -1
  10. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/postprocessing.py +25 -11
  11. lbm_suite2p_python-3.0.0/lbm_suite2p_python/run_lsp.py +2671 -0
  12. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/utils.py +12 -5
  13. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/zplane.py +411 -290
  14. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0/lbm_suite2p_python.egg-info}/PKG-INFO +6 -6
  15. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python.egg-info/SOURCES.txt +3 -0
  16. lbm_suite2p_python-3.0.0/lbm_suite2p_python.egg-info/requires.txt +16 -0
  17. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/pyproject.toml +6 -22
  18. lbm_suite2p_python-3.0.0/tests/test_frame_count_aliases.py +317 -0
  19. lbm_suite2p_python-2.5.4/lbm_suite2p_python/run_lsp.py +0 -1625
  20. lbm_suite2p_python-2.5.4/lbm_suite2p_python.egg-info/requires.txt +0 -17
  21. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/LICENSE.md +0 -0
  22. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/MANIFEST.in +0 -0
  23. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/README.md +0 -0
  24. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/__init__.py +0 -0
  25. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/__main__.py +0 -0
  26. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/_benchmarking.py +0 -0
  27. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/merging.py +0 -0
  28. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python/volume.py +0 -0
  29. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python.egg-info/dependency_links.txt +0 -0
  30. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python.egg-info/entry_points.txt +0 -0
  31. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/lbm_suite2p_python.egg-info/top_level.txt +0 -0
  32. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/setup.cfg +0 -0
  33. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/tests/test_pipeline_parameters.py +0 -0
  34. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/tests/test_refactored_pipeline.py +0 -0
  35. {lbm_suite2p_python-2.5.4 → lbm_suite2p_python-3.0.0}/tests/test_run_volume.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lbm_suite2p_python
3
- Version: 2.5.4
3
+ Version: 3.0.0
4
4
  Summary: Light Beads Microscopy Pipeline using Suite2p
5
5
  License-Expression: BSD-3-Clause
6
6
  Project-URL: homepage, https://github.com/MillerBrainObservatory/LBM-Suite2p-Python
@@ -8,12 +8,12 @@ Keywords: Pipeline,Numpy,Microscopy,ScanImage,Suite2p,tiff
8
8
  Classifier: Development Status :: 3 - Alpha
9
9
  Classifier: Intended Audience :: Science/Research
10
10
  Classifier: Programming Language :: Python :: 3 :: Only
11
- Requires-Python: <3.12.10,>=3.12.7
11
+ Requires-Python: <3.14,>=3.12.7
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE.md
14
- Requires-Dist: mbo_utilities>=2.6.0
15
- Provides-Extra: suite2p
16
- Requires-Dist: suite2p_mbo>=2.0.1; extra == "suite2p"
14
+ Requires-Dist: mbo_utilities>=2.7.7
15
+ Requires-Dist: suite2p>=1.0.0.1
16
+ Requires-Dist: setuptools<81
17
17
  Provides-Extra: rastermap
18
18
  Requires-Dist: rastermap; extra == "rastermap"
19
19
  Provides-Extra: cellpose
@@ -22,7 +22,7 @@ Provides-Extra: torch
22
22
  Requires-Dist: torch>=2.7.0; extra == "torch"
23
23
  Requires-Dist: torchvision>=0.22.0; extra == "torch"
24
24
  Provides-Extra: all
25
- Requires-Dist: lbm_suite2p_python[cellpose,rastermap,suite2p,torch]; extra == "all"
25
+ Requires-Dist: lbm_suite2p_python[cellpose,rastermap,torch]; extra == "all"
26
26
  Dynamic: license-file
27
27
 
28
28
  <p align="center">
@@ -0,0 +1,140 @@
1
+ """
2
+ Padding-aware extraction shim for upstream suite2p.
3
+
4
+ Problem
5
+ -------
6
+ When LBM data is axially registered, mbo's writer zero-pads planes so every
7
+ plane sits in a common (Ly, Lx) coordinate system. `ops["yrange"]` /
8
+ `ops["xrange"]` mark the valid (non-padded) region.
9
+
10
+ Upstream's `suite2p.detection.*` is yrange/xrange-aware: `bin_movie` crops
11
+ before computing meanImg / max_proj / Vcorr / cellpose inputs, so padding
12
+ never enters detection. Upstream's `suite2p.extraction.masks` is NOT:
13
+ `create_cell_pix` allocates `np.zeros((Ly, Lx))`, and `create_neuropil_masks`
14
+ treats any pixel with `cell_pix < 0.5` as a valid neuropil candidate. Zero-
15
+ padded pixels satisfy that predicate, so neuropil donuts around
16
+ boundary-adjacent ROIs grow into the padding and pull zeros into `Fneu`.
17
+ The corrupted `Fneu` propagates into `F - neuropil_coefficient * Fneu` and
18
+ every dF/F derived from it.
19
+
20
+ Fix
21
+ ---
22
+ Monkeypatch `suite2p.extraction.masks.create_cell_pix` inside a context
23
+ manager scoped to the lbm call only. The replacement:
24
+
25
+ 1. Calls upstream's real implementation to produce the cell-pixel mask.
26
+ 2. Marks every pixel outside `ops["yrange"]` / `ops["xrange"]` as
27
+ "occupied" (True / 1.0), which makes `create_neuropil_masks`'s
28
+ `cell_pix[y,x] < 0.5` predicate return False for those pixels, so
29
+ donuts grow only into the valid region.
30
+
31
+ The patch is applied *only* for the duration of the `with` block and
32
+ restored on exit, regardless of whether the wrapped code succeeds or
33
+ raises. External users of suite2p see no change; only our call path is
34
+ affected.
35
+
36
+ Why this rather than rebuilding neuropil_masks externally and passing
37
+ them to `extraction_wrapper(neuropil_masks=...)`
38
+ -------------------------------------------------------------------
39
+ The external-build approach would require us to bypass upstream's
40
+ `pipeline()` and re-orchestrate detection → classification → extraction →
41
+ deconvolution ourselves, just to inject the `neuropil_masks=` kwarg.
42
+ Monkeypatching a single leaf function is ~15 lines, keeps us on the
43
+ upstream orchestration path, and is trivially reversible.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import contextlib
49
+ import numpy as np
50
+
51
+
52
+ @contextlib.contextmanager
53
+ def padding_aware_extraction(ops):
54
+ """
55
+ Context manager: inside the block, upstream's neuropil-mask builder
56
+ treats pixels outside ``ops["yrange"]`` / ``ops["xrange"]`` as
57
+ "occupied", preventing neuropil donuts from extending into the
58
+ zero-padded axial-alignment border.
59
+
60
+ Safe to call unconditionally: if no yrange/xrange are present on ops,
61
+ or if they cover the full frame, the patch is a no-op.
62
+
63
+ Parameters
64
+ ----------
65
+ ops : dict
66
+ Flat suite2p ops dict. Reads ``ops["yrange"]`` and
67
+ ``ops["xrange"]``; does not mutate.
68
+
69
+ Yields
70
+ ------
71
+ None
72
+
73
+ Examples
74
+ --------
75
+ >>> with padding_aware_extraction(ops):
76
+ ... reg_outputs, detect_outputs, stat, F, Fneu, ... = pipeline(
77
+ ... save_path=save_path, f_reg=f_reg, ..., settings=settings,
78
+ ... )
79
+ """
80
+ yrange = ops.get("yrange")
81
+ xrange = ops.get("xrange")
82
+ if not yrange or not xrange:
83
+ # no crop information — nothing to protect against
84
+ yield
85
+ return
86
+
87
+ try:
88
+ y0, y1 = int(yrange[0]), int(yrange[1])
89
+ x0, x1 = int(xrange[0]), int(xrange[1])
90
+ except (TypeError, ValueError):
91
+ yield
92
+ return
93
+
94
+ # import here so a missing suite2p doesn't break import of this module
95
+ try:
96
+ from suite2p.extraction import masks as _m
97
+ except ImportError:
98
+ yield
99
+ return
100
+
101
+ original_create_cell_pix = getattr(_m, "create_cell_pix", None)
102
+ if original_create_cell_pix is None:
103
+ # upstream moved or inlined create_cell_pix — no safe patch point.
104
+ # fall through without modification rather than silently corrupting.
105
+ yield
106
+ return
107
+
108
+ def _padding_aware_create_cell_pix(stats, Ly, Lx, lam_percentile=50.0):
109
+ """Drop-in replacement that adds the padding mask to upstream's result."""
110
+ cell_pix = original_create_cell_pix(stats, Ly, Lx, lam_percentile)
111
+ # upstream returns either bool array or float >0.5 mask; normalize
112
+ # to a bool copy so our additions don't alter any internal cache.
113
+ cell_pix = np.asarray(cell_pix).copy()
114
+ if cell_pix.dtype != bool:
115
+ cell_pix = cell_pix > 0.5
116
+
117
+ # mark the region OUTSIDE the valid window as occupied so the
118
+ # neuropil donut never extends into zero-padded pixels.
119
+ _y0 = max(0, min(y0, Ly))
120
+ _y1 = max(_y0, min(y1, Ly))
121
+ _x0 = max(0, min(x0, Lx))
122
+ _x1 = max(_x0, min(x1, Lx))
123
+ if _y0 > 0:
124
+ cell_pix[:_y0, :] = True
125
+ if _y1 < Ly:
126
+ cell_pix[_y1:, :] = True
127
+ if _x0 > 0:
128
+ cell_pix[:, :_x0] = True
129
+ if _x1 < Lx:
130
+ cell_pix[:, _x1:] = True
131
+ return cell_pix
132
+
133
+ _m.create_cell_pix = _padding_aware_create_cell_pix
134
+ try:
135
+ yield
136
+ finally:
137
+ # always restore, even on exception, so external callers of
138
+ # suite2p.extraction.masks.create_cell_pix see the unpatched
139
+ # upstream function.
140
+ _m.create_cell_pix = original_create_cell_pix
@@ -37,9 +37,10 @@ def _compute_projection(
37
37
  Parameters
38
38
  ----------
39
39
  arr : array-like
40
- Input array (T, Y, X) for 3D or (T, Z, Y, X) for 4D.
40
+ Input array. mbo_utilities arrays are 5D TCZYX.
41
+ Legacy 4D TZYX and 3D TYX inputs are also supported.
41
42
  plane_idx : int, optional
42
- For 4D arrays, which z-plane to extract (0-indexed).
43
+ For volumetric arrays, which z-plane to extract (0-indexed).
43
44
  If None, uses all planes for 3D segmentation.
44
45
  method : str
45
46
  Projection method: 'max', 'mean', 'std', or 'percentile'.
@@ -53,19 +54,23 @@ def _compute_projection(
53
54
  """
54
55
  ndim = len(arr.shape)
55
56
 
56
- if ndim == 4:
57
- # (T, Z, Y, X)
57
+ if ndim == 5:
58
+ # 5D TCZYX: use channel 0
59
+ if plane_idx is not None:
60
+ data = arr[:, 0, plane_idx, :, :] # (T, Y, X)
61
+ else:
62
+ data = arr[:, 0, :, :, :] # (T, Z, Y, X)
63
+ elif ndim == 4:
64
+ # legacy 4D TZYX
58
65
  if plane_idx is not None:
59
- # extract single plane -> (T, Y, X)
60
66
  data = arr[:, plane_idx, :, :]
61
67
  else:
62
- # keep all planes -> (T, Z, Y, X)
63
68
  data = arr[:]
64
69
  elif ndim == 3:
65
- # (T, Y, X)
70
+ # legacy 3D TYX
66
71
  data = arr[:]
67
72
  else:
68
- raise ValueError(f"Expected 3D or 4D array, got {ndim}D")
73
+ raise ValueError(f"Expected 3D, 4D, or 5D array, got {ndim}D")
69
74
 
70
75
  # convert to numpy if lazy
71
76
  if hasattr(data, "compute"):
@@ -577,18 +582,18 @@ def cellpose(
577
582
 
578
583
  # get array info
579
584
  shape = arr.shape
580
- ndim = len(shape)
581
585
  num_planes = _get_num_planes(arr)
582
586
  num_frames = shape[0]
587
+ is_volumetric = num_planes > 1
583
588
 
584
589
  print(f"\nDataset info:")
585
590
  print(f" Shape: {shape}")
586
591
  print(f" Frames: {num_frames}")
587
592
  print(f" Planes: {num_planes}")
588
- print(f" Data type: {'4D volumetric' if ndim == 4 else '3D planar'}")
593
+ print(f" Data type: {'volumetric' if is_volumetric else 'planar'}")
589
594
 
590
595
  # normalize planes to 0-indexed list
591
- if ndim == 4:
596
+ if is_volumetric:
592
597
  planes_to_process = _normalize_planes(planes, num_planes)
593
598
  else:
594
599
  planes_to_process = [None] # single plane data