lbm_suite2p_python 3.0.7__tar.gz → 3.1.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 (32) hide show
  1. {lbm_suite2p_python-3.0.7/lbm_suite2p_python.egg-info → lbm_suite2p_python-3.1.0}/PKG-INFO +2 -2
  2. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/__init__.py +6 -0
  3. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/cli.py +6 -0
  4. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/conversion.py +2 -2
  5. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/db_settings.py +114 -26
  6. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/postprocessing.py +78 -7
  7. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/run_lsp.py +111 -35
  8. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/utils.py +10 -1
  9. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/volume.py +35 -24
  10. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/zplane.py +97 -85
  11. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0/lbm_suite2p_python.egg-info}/PKG-INFO +2 -2
  12. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python.egg-info/requires.txt +1 -1
  13. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/pyproject.toml +106 -106
  14. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/tests/test_pipeline_parameters.py +29 -0
  15. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/tests/test_refactored_pipeline.py +4 -2
  16. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/LICENSE.md +0 -0
  17. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/MANIFEST.in +0 -0
  18. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/README.md +0 -0
  19. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/__main__.py +0 -0
  20. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/_benchmarking.py +0 -0
  21. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/cellpose.py +0 -0
  22. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/default_ops.py +0 -0
  23. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/grid_search.py +0 -0
  24. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/gui.py +0 -0
  25. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python/merging.py +0 -0
  26. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python.egg-info/SOURCES.txt +0 -0
  27. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python.egg-info/dependency_links.txt +0 -0
  28. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python.egg-info/entry_points.txt +0 -0
  29. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/lbm_suite2p_python.egg-info/top_level.txt +0 -0
  30. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/setup.cfg +0 -0
  31. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.0}/tests/test_frame_count_aliases.py +0 -0
  32. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.1.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: 3.0.7
3
+ Version: 3.1.0
4
4
  Summary: Calcium Imaging Pipeline built with Suite2p, Cellpose and Rastermap
5
5
  License-Expression: BSD-3-Clause
6
6
  Project-URL: homepage, https://github.com/MillerBrainObservatory/LBM-Suite2p-Python
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
11
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>=3.0.3
14
+ Requires-Dist: mbo_utilities>=3.1.0
15
15
  Requires-Dist: suite2p>=1.0.0.1
16
16
  Requires-Dist: setuptools<81
17
17
  Provides-Extra: rastermap
@@ -70,6 +70,9 @@ from lbm_suite2p_python.postprocessing import (
70
70
  load_ops,
71
71
  load_planar_results,
72
72
  dff_rolling_percentile,
73
+ dff_median_filter,
74
+ zscore_trace,
75
+ baseline_percentile_dff,
73
76
  dff_shot_noise,
74
77
  compute_roi_stats,
75
78
  )
@@ -131,6 +134,9 @@ __all__ = [
131
134
  "load_ops",
132
135
  "load_planar_results",
133
136
  "dff_rolling_percentile",
137
+ "dff_median_filter",
138
+ "zscore_trace",
139
+ "baseline_percentile_dff",
134
140
  "dff_shot_noise",
135
141
  "compute_roi_stats",
136
142
 
@@ -207,6 +207,11 @@ Examples:
207
207
  "--dff-smooth", type=int, dest="dff_smooth_window",
208
208
  help="smoothing window for dF/F"
209
209
  )
210
+ dff.add_argument(
211
+ "--norm-method", choices=["dff", "zscore"], default="zscore",
212
+ dest="norm_method",
213
+ help="normalization for norm_traces.npy (default: zscore)"
214
+ )
210
215
  dff.add_argument(
211
216
  "--correct-neuropil", dest="correct_neuropil",
212
217
  action=argparse.BooleanOptionalAction, default=True,
@@ -570,6 +575,7 @@ def main():
570
575
  dff_window_size=args.dff_window_size,
571
576
  dff_percentile=args.dff_percentile,
572
577
  dff_smooth_window=args.dff_smooth_window,
578
+ norm_method=args.norm_method,
573
579
  correct_neuropil=args.correct_neuropil,
574
580
  cell_filters=cell_filters,
575
581
  accept_all_cells=args.accept_all_cells,
@@ -871,7 +871,7 @@ def get_results(path, include_traces=True):
871
871
  "F": None,
872
872
  "Fneu": None,
873
873
  "spks": None,
874
- "dff": None,
874
+ "norm_traces": None,
875
875
  "ops": None,
876
876
  "seg_file": None,
877
877
  }
@@ -902,7 +902,7 @@ def get_results(path, include_traces=True):
902
902
 
903
903
  # Load traces if requested
904
904
  if include_traces and fmt == "suite2p":
905
- for name in ["F", "Fneu", "spks", "dff"]:
905
+ for name in ["F", "Fneu", "spks", "norm_traces"]:
906
906
  trace_file = path / f"{name}.npy"
907
907
  if trace_file.exists():
908
908
  result[name] = np.load(trace_file)
@@ -175,20 +175,121 @@ _FORK_TO_UPSTREAM_RENAMES: dict[str, str] = {
175
175
  "pretrained_model": "cellpose_model",
176
176
  }
177
177
 
178
- # fork's anatomical_only int (1-4) selects which image cellpose runs on.
179
- # upstream replaced this with a string in cellpose_settings.img. mapping:
180
- # 1 -> 'max_proj / meanImg' (log ratio)
181
- # 2 -> 'meanImg'
182
- # 3 -> enhanced_mean_img (REMOVED upstream — no equivalent string;
183
- # falls through to max_proj branch)
184
- # 4 -> 'max_proj' (anything not matching the two strings hits else: img=max_proj)
178
+ # fork's anatomical_only int selects which image cellpose runs on;
179
+ # upstream encodes this via cellpose_settings.img. 3 (enhanced mean) was
180
+ # removed from suite2p detection — reconcile_detection_keys maps it to 1.
185
181
  _ANATOMICAL_ONLY_TO_IMG: dict[int, str] = {
186
182
  1: "max_proj / meanImg",
187
183
  2: "meanImg",
188
184
  4: "max_proj",
189
185
  }
186
+
187
+ # Values suite2p actually accepts. Mirrors suite2p.parameters:
188
+ # detection.algorithm description -> ['sparsery', 'sourcery', 'cellpose'];
189
+ # cellpose_settings.img description -> ['max_proj / meanImg', 'meanImg',
190
+ # 'max_proj']. Hardcoded (the schema lists these only in a description
191
+ # string) and kept in sync with upstream.
192
+ VALID_DETECTION_ALGORITHMS = ("sparsery", "sourcery", "cellpose")
193
+ VALID_CELLPOSE_IMG = ("max_proj / meanImg", "meanImg", "max_proj")
194
+
195
+ # legacy / renamed cellpose image spellings -> current suite2p string. the
196
+ # old enhanced-mean image (meanImgE) was removed from suite2p detection, so
197
+ # it maps to suite2p's default; use spatial_hp_cp (highpass_spatial) > 0 to
198
+ # recover the sharpening.
199
+ _LEGACY_IMG_ALIASES = {
200
+ "meanimge": "max_proj / meanImg",
201
+ "enhanced_meanimg": "max_proj / meanImg",
202
+ "enhanced_mean_img": "max_proj / meanImg",
203
+ "mean_img": "meanImg",
204
+ "maximg": "max_proj",
205
+ "max_img": "max_proj",
206
+ "maxproj": "max_proj",
207
+ }
190
208
  _UPSTREAM_TO_FORK_RENAMES = {v: k for k, v in _FORK_TO_UPSTREAM_RENAMES.items()}
191
209
 
210
+
211
+ def reconcile_detection_keys(ops: dict | None) -> dict | None:
212
+ """Make the flat detection keys consistent and valid for suite2p.
213
+
214
+ The fork spells detection as `anatomical_only` (int) / `sparse_mode`
215
+ (bool); suite2p uses `algorithm` (str). Users mix both. This keeps them
216
+ mutually consistent and validates them against what suite2p accepts, so
217
+ either spelling engages identical code paths. Mutates and returns `ops`
218
+ (no-op for non-dicts); idempotent.
219
+
220
+ - `algorithm="cellpose"` with no `anatomical_only` -> `anatomical_only=1`
221
+ (suite2p's default cellpose image), so the fork's anatomical paths run.
222
+ - `anatomical_only>0` with no `algorithm` -> `algorithm="cellpose"`.
223
+ - `algorithm` in {sparsery, sourcery} -> `anatomical_only=0`, `sparse_mode`.
224
+ - legacy `anatomical_only=3` (enhanced mean, removed upstream) -> 1 + warn.
225
+ - unknown `algorithm` / `img` -> warn and fall back to a valid value.
226
+ """
227
+ import warnings
228
+
229
+ if not isinstance(ops, dict):
230
+ return ops
231
+
232
+ algo = ops.get("algorithm")
233
+ if isinstance(algo, str):
234
+ algo = algo.strip()
235
+ if algo in VALID_DETECTION_ALGORITHMS:
236
+ ops["algorithm"] = algo
237
+ else:
238
+ warnings.warn(
239
+ f"Unknown detection algorithm {ops['algorithm']!r}; suite2p "
240
+ f"accepts {VALID_DETECTION_ALGORITHMS}. Ignoring it (falling "
241
+ "back to anatomical_only / suite2p default).",
242
+ stacklevel=2,
243
+ )
244
+ ops.pop("algorithm", None)
245
+ algo = None
246
+ else:
247
+ algo = None
248
+
249
+ anat = ops.get("anatomical_only")
250
+ try:
251
+ anat_int = int(anat) if anat is not None else None
252
+ except (TypeError, ValueError):
253
+ anat_int = None
254
+
255
+ if anat_int == 3:
256
+ warnings.warn(
257
+ "anatomical_only=3 (enhanced mean image) was removed from suite2p "
258
+ "detection; using anatomical_only=1 (suite2p default image). Set "
259
+ "spatial_hp_cp>0 to sharpen the detection image.",
260
+ stacklevel=2,
261
+ )
262
+ anat_int = 1
263
+ ops["anatomical_only"] = 1
264
+
265
+ if algo == "cellpose":
266
+ if not anat_int or anat_int <= 0:
267
+ ops["anatomical_only"] = 1 # suite2p's default cellpose image
268
+ elif algo in ("sparsery", "sourcery"):
269
+ ops["anatomical_only"] = 0
270
+ ops.setdefault("sparse_mode", algo == "sparsery")
271
+ elif algo is None and anat_int and anat_int > 0:
272
+ ops["algorithm"] = "cellpose"
273
+
274
+ img = ops.get("img")
275
+ if isinstance(img, str) and img not in VALID_CELLPOSE_IMG:
276
+ mapped = _LEGACY_IMG_ALIASES.get(img.strip().lower())
277
+ if mapped:
278
+ warnings.warn(
279
+ f"cellpose image {img!r} is legacy/renamed; using {mapped!r}.",
280
+ stacklevel=2,
281
+ )
282
+ ops["img"] = mapped
283
+ else:
284
+ warnings.warn(
285
+ f"Unknown cellpose image {img!r}; suite2p accepts "
286
+ f"{VALID_CELLPOSE_IMG}. Ignoring it (using suite2p default).",
287
+ stacklevel=2,
288
+ )
289
+ ops.pop("img", None)
290
+
291
+ return ops
292
+
192
293
  # per-section flat-key disambiguation. Several upstream sections share
193
294
  # an upstream key. When flattened to ops, the second iteration overwrites
194
295
  # the first; on the reverse derivation, the surviving value gets fanned
@@ -286,8 +387,9 @@ def ops_to_db_settings(ops: dict) -> tuple[dict, dict]:
286
387
  settings: dict[str, Any] = {}
287
388
 
288
389
  # resolve fork→upstream renames into a temporary lookup that
289
- # doesn't mutate the caller's ops
290
- lookup = dict(ops)
390
+ # doesn't mutate the caller's ops. reconcile first so algorithm /
391
+ # anatomical_only / img are consistent and valid before translation.
392
+ lookup = reconcile_detection_keys(dict(ops))
291
393
  for fork_name, upstream_name in _FORK_TO_UPSTREAM_RENAMES.items():
292
394
  if fork_name in lookup and upstream_name not in lookup:
293
395
  lookup[upstream_name] = lookup[fork_name]
@@ -377,11 +479,9 @@ def ops_to_db_settings(ops: dict) -> tuple[dict, dict]:
377
479
  if key in lookup:
378
480
  cellpose[key] = lookup[key]
379
481
 
380
- # fork's anatomical_only int picks which image cellpose runs on.
381
- # upstream encodes this via cellpose_settings.img; without this
382
- # mapping anatomical_only=4 silently degrades to anatomical_only=1
383
- # (the upstream default 'max_proj / meanImg' = log ratio image)
384
- # because the translator only sets algorithm='cellpose' otherwise.
482
+ # fork's anatomical_only int picks which image cellpose runs on;
483
+ # upstream encodes this via cellpose_settings.img. anatomical_only was
484
+ # already validated/normalized (incl. legacy 3) by reconcile above.
385
485
  anat = lookup.get("anatomical_only")
386
486
  if anat and "img" not in cellpose:
387
487
  try:
@@ -390,18 +490,6 @@ def ops_to_db_settings(ops: dict) -> tuple[dict, dict]:
390
490
  anat_int = None
391
491
  if anat_int in _ANATOMICAL_ONLY_TO_IMG:
392
492
  cellpose["img"] = _ANATOMICAL_ONLY_TO_IMG[anat_int]
393
- elif anat_int == 3:
394
- # upstream removed enhanced_mean_img — closest fallback is
395
- # 'meanImg' (same source family, no median-ratio enhancement).
396
- # warn the caller so they know something changed.
397
- import warnings
398
- warnings.warn(
399
- "anatomical_only=3 (enhanced_mean_img) is no longer supported "
400
- "upstream; falling back to cellpose_settings.img='meanImg'. "
401
- "Use anatomical_only in {1, 2, 4} to avoid this fallback.",
402
- stacklevel=2,
403
- )
404
- cellpose["img"] = "max_proj"
405
493
 
406
494
  if cellpose:
407
495
  detection["cellpose_settings"] = cellpose
@@ -1162,6 +1162,80 @@ def dff_median_filter(f_trace):
1162
1162
  return (f_trace - f0) / (f0 + 1e-6) # 1e-6 to avoid division by zero
1163
1163
 
1164
1164
 
1165
+ def zscore_trace(f_trace, smooth_window: int = None, fs: float = None, tau: float = None):
1166
+ """
1167
+ Z-score fluorescence traces per ROI: ``(f - mean) / std`` over time.
1168
+
1169
+ One of the ``norm_method`` options for ``norm_traces.npy``. Unlike ΔF/F,
1170
+ output is unitless, centered on 0, and can be negative.
1171
+
1172
+ Parameters
1173
+ ----------
1174
+ f_trace : np.ndarray
1175
+ (N_neurons, N_frames) fluorescence traces.
1176
+ smooth_window : int, optional
1177
+ Temporal smoothing window (frames) applied after z-scoring. If None
1178
+ and both ``tau`` and ``fs`` are given, set to ~0.5 * tau * fs;
1179
+ otherwise no smoothing. Set to 0 or 1 to disable.
1180
+ fs : float, optional
1181
+ Frame rate in Hz. Used with ``tau`` to auto-size ``smooth_window``.
1182
+ tau : float, optional
1183
+ Calcium indicator decay time constant in seconds. Used with ``fs``
1184
+ to auto-size ``smooth_window``.
1185
+
1186
+ Returns
1187
+ -------
1188
+ z : np.ndarray
1189
+ (N_neurons, N_frames) z-scored traces.
1190
+ """
1191
+ from scipy.ndimage import uniform_filter1d
1192
+
1193
+ if not isinstance(f_trace, np.ndarray):
1194
+ raise TypeError("f_trace must be a numpy array")
1195
+ if f_trace.ndim != 2:
1196
+ raise ValueError("f_trace must be a 2D array with shape (N_neurons, N_frames)")
1197
+ if f_trace.shape[0] == 0 or f_trace.shape[1] == 0:
1198
+ raise ValueError("f_trace must not be empty")
1199
+
1200
+ mean = np.mean(f_trace, axis=1, keepdims=True)
1201
+ std = np.std(f_trace, axis=1, keepdims=True)
1202
+ z = (f_trace - mean) / (std + 1e-6) # 1e-6 to avoid division by zero
1203
+
1204
+ if smooth_window is None and tau is not None and fs is not None:
1205
+ smooth_window = max(1, int(0.5 * tau * fs))
1206
+ if smooth_window is not None and smooth_window > 1:
1207
+ z = uniform_filter1d(z, size=smooth_window, axis=1, mode="nearest")
1208
+
1209
+ return z
1210
+
1211
+
1212
+ def baseline_percentile_dff(f_corr, percentile: int = 20):
1213
+ """
1214
+ ΔF/F using a static percentile baseline.
1215
+
1216
+ Used by the SNR, skew, and shot-noise metrics, which always require ΔF/F
1217
+ units regardless of the ``norm_method`` chosen for ``norm_traces.npy``.
1218
+ Kept separate from :func:`dff_rolling_percentile` and :func:`zscore_trace`
1219
+ so the quality metrics never follow the user-selected normalization.
1220
+
1221
+ Parameters
1222
+ ----------
1223
+ f_corr : np.ndarray
1224
+ (N_neurons, N_frames) fluorescence, already neuropil-corrected and
1225
+ rectified as needed by the caller.
1226
+ percentile : int, default 20
1227
+ Percentile for the static baseline F0.
1228
+
1229
+ Returns
1230
+ -------
1231
+ dff : np.ndarray
1232
+ (N_neurons, N_frames) ΔF/F traces.
1233
+ """
1234
+ baseline = np.percentile(f_corr, percentile, axis=1, keepdims=True)
1235
+ baseline = np.maximum(baseline, 1e-6)
1236
+ return (f_corr - baseline) / baseline
1237
+
1238
+
1165
1239
  def dff_shot_noise(dff, fr):
1166
1240
  """
1167
1241
  Estimate the shot noise level of calcium imaging traces.
@@ -1279,10 +1353,8 @@ def compute_trace_quality_score(
1279
1353
  F_corr = F
1280
1354
  F_corr = np.maximum(F_corr, 0)
1281
1355
 
1282
- # compute baseline and dF/F
1283
- baseline = np.percentile(F_corr, 20, axis=1, keepdims=True)
1284
- baseline = np.maximum(baseline, 1e-6)
1285
- dff = (F_corr - baseline) / baseline
1356
+ # static-baseline ΔF/F for quality metrics only (see baseline_percentile_dff)
1357
+ dff = baseline_percentile_dff(F_corr)
1286
1358
 
1287
1359
  # SNR
1288
1360
  signal = np.std(dff, axis=1)
@@ -1448,9 +1520,8 @@ def compute_roi_stats(plane_dir, fs=None):
1448
1520
  # neuropil correction, rectify negatives, and dF/F
1449
1521
  F_corr = F - 0.7 * Fneu
1450
1522
  F_corr = np.maximum(F_corr, 0)
1451
- baseline = np.percentile(F_corr, 20, axis=1, keepdims=True)
1452
- baseline = np.maximum(baseline, 1e-6)
1453
- dff = (F_corr - baseline) / baseline
1523
+ # static-baseline ΔF/F for quality metrics only (see baseline_percentile_dff)
1524
+ dff = baseline_percentile_dff(F_corr)
1454
1525
 
1455
1526
  # compute metrics
1456
1527
  signal = np.std(dff, axis=1)