lbm_suite2p_python 3.0.7__tar.gz → 3.0.8__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.0.8}/PKG-INFO +2 -2
  2. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/__init__.py +4 -0
  3. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/cli.py +6 -0
  4. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/conversion.py +2 -2
  5. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/postprocessing.py +78 -7
  6. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/run_lsp.py +78 -19
  7. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/volume.py +35 -24
  8. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/zplane.py +97 -85
  9. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8/lbm_suite2p_python.egg-info}/PKG-INFO +2 -2
  10. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python.egg-info/requires.txt +1 -1
  11. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/pyproject.toml +2 -2
  12. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/tests/test_pipeline_parameters.py +29 -0
  13. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/tests/test_refactored_pipeline.py +4 -2
  14. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/LICENSE.md +0 -0
  15. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/MANIFEST.in +0 -0
  16. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/README.md +0 -0
  17. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/__main__.py +0 -0
  18. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/_benchmarking.py +0 -0
  19. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/cellpose.py +0 -0
  20. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/db_settings.py +0 -0
  21. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/default_ops.py +0 -0
  22. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/grid_search.py +0 -0
  23. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/gui.py +0 -0
  24. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/merging.py +0 -0
  25. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/utils.py +0 -0
  26. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python.egg-info/SOURCES.txt +0 -0
  27. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python.egg-info/dependency_links.txt +0 -0
  28. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python.egg-info/entry_points.txt +0 -0
  29. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python.egg-info/top_level.txt +0 -0
  30. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/setup.cfg +0 -0
  31. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/tests/test_frame_count_aliases.py +0 -0
  32. {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/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.0.8
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.0.7
15
15
  Requires-Dist: suite2p>=1.0.0.1
16
16
  Requires-Dist: setuptools<81
17
17
  Provides-Extra: rastermap
@@ -70,6 +70,8 @@ from lbm_suite2p_python.postprocessing import (
70
70
  load_ops,
71
71
  load_planar_results,
72
72
  dff_rolling_percentile,
73
+ zscore_trace,
74
+ baseline_percentile_dff,
73
75
  dff_shot_noise,
74
76
  compute_roi_stats,
75
77
  )
@@ -131,6 +133,8 @@ __all__ = [
131
133
  "load_ops",
132
134
  "load_planar_results",
133
135
  "dff_rolling_percentile",
136
+ "zscore_trace",
137
+ "baseline_percentile_dff",
134
138
  "dff_shot_noise",
135
139
  "compute_roi_stats",
136
140
 
@@ -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="dff",
212
+ dest="norm_method",
213
+ help="normalization for norm_traces.npy (default: dff)"
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)
@@ -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)
@@ -19,6 +19,7 @@ from lbm_suite2p_python.postprocessing import (
19
19
  ops_to_json,
20
20
  load_ops,
21
21
  dff_rolling_percentile,
22
+ zscore_trace,
22
23
  apply_filters,
23
24
  )
24
25
 
@@ -163,7 +164,7 @@ def _set_frame_count_aliases(ops: dict, n: int) -> None:
163
164
  # user reruns against a different save_path.
164
165
  _DETECTION_OUTPUT_FILES = (
165
166
  "stat.npy", "iscell.npy", "F.npy", "Fneu.npy",
166
- "spks.npy", "dff.npy", "redcell.npy", "zcorr.npy",
167
+ "spks.npy", "norm_traces.npy", "redcell.npy", "zcorr.npy",
167
168
  "reg_outputs.npy", "detect_outputs.npy",
168
169
  "F_chan2.npy", "Fneu_chan2.npy",
169
170
  "roi_stats.npy",
@@ -496,6 +497,31 @@ def _get_suite2p_version():
496
497
  return "not installed"
497
498
 
498
499
 
500
+ def _resolve_gpu_env() -> None:
501
+ """Honor MBO_GPU -> CUDA_VISIBLE_DEVICES before torch/cupy/cellpose init.
502
+
503
+ Lets ``MBO_GPU=0 lsp ...`` (or programmatic use) force CPU across suite2p
504
+ and cellpose without per-call device args; ``MBO_GPU=N`` pins a device.
505
+ No-op when MBO_GPU is unset. Entry points call this first so the env is
506
+ set before torch initializes CUDA, and before workers are spawned (they
507
+ inherit it).
508
+ """
509
+ raw = os.environ.get("MBO_GPU")
510
+ if raw is None:
511
+ return
512
+ try:
513
+ from mbo_utilities.gpu import apply_gpu_policy
514
+ apply_gpu_policy(raw)
515
+ return
516
+ except Exception:
517
+ pass
518
+ token = raw.strip().lower()
519
+ if token in ("0", "off", "false", "no", "cpu", "none"):
520
+ os.environ["CUDA_VISIBLE_DEVICES"] = ""
521
+ elif token and token.replace(",", "").isdigit():
522
+ os.environ["CUDA_VISIBLE_DEVICES"] = token
523
+
524
+
499
525
  def _apply_thread_limits(threads_per_worker: int | None) -> None:
500
526
  """Cap BLAS / OMP / numba / torch thread counts per process.
501
527
 
@@ -770,6 +796,7 @@ def pipeline(
770
796
  dff_window_size: int = None,
771
797
  dff_percentile: int = 20,
772
798
  dff_smooth_window: int = None,
799
+ norm_method: str = "dff",
773
800
  correct_neuropil: bool = True,
774
801
  cell_filters: list = None,
775
802
  accept_all_cells: bool = False,
@@ -835,9 +862,15 @@ def pipeline(
835
862
  dff_smooth_window : int, optional
836
863
  Temporal smoothing window for dF/F traces (frames).
837
864
  If None, auto-calculated. Set to 1 to disable.
865
+ norm_method : str, default "dff"
866
+ Normalization for the saved norm_traces.npy. "dff" uses the rolling
867
+ percentile ΔF/F (dff_window_size / dff_percentile / dff_smooth_window
868
+ apply); "zscore" uses per-ROI (F - mean) / std. Quality metrics
869
+ (SNR / skew / shot-noise) always use ΔF/F regardless of this setting.
838
870
  correct_neuropil : bool, default True
839
- If True, dF/F is computed on F - 0.7 * Fneu. If False, on raw F.
840
- Affects dff.npy, the dF/F trace plots, and trace-quality scoring.
871
+ If True, the norm trace is computed on F - 0.7 * Fneu. If False, on
872
+ raw F. Affects norm_traces.npy, the trace plots, and trace-quality
873
+ scoring.
841
874
  cell_filters : list, optional
842
875
  Filters to apply to detected ROIs. Default is no filters (off).
843
876
  Currently supports diameter bounds in microns or pixels.
@@ -913,6 +946,7 @@ def pipeline(
913
946
 
914
947
  _attach_external_loggers()
915
948
 
949
+ _resolve_gpu_env()
916
950
  _apply_thread_limits(threads_per_worker)
917
951
 
918
952
  # 1. Handle Deprecations
@@ -988,6 +1022,7 @@ def pipeline(
988
1022
  dff_window_size=dff_window_size,
989
1023
  dff_percentile=dff_percentile,
990
1024
  dff_smooth_window=dff_smooth_window,
1025
+ norm_method=norm_method,
991
1026
  correct_neuropil=correct_neuropil,
992
1027
  accept_all_cells=accept_all_cells,
993
1028
  cell_filters=cell_filters,
@@ -1026,6 +1061,7 @@ def pipeline(
1026
1061
  dff_window_size=dff_window_size,
1027
1062
  dff_percentile=dff_percentile,
1028
1063
  dff_smooth_window=dff_smooth_window,
1064
+ norm_method=norm_method,
1029
1065
  correct_neuropil=correct_neuropil,
1030
1066
  accept_all_cells=accept_all_cells,
1031
1067
  cell_filters=cell_filters,
@@ -1196,6 +1232,7 @@ def run_volume(
1196
1232
  dff_window_size: int = None,
1197
1233
  dff_percentile: int = 20,
1198
1234
  dff_smooth_window: int = None,
1235
+ norm_method: str = "dff",
1199
1236
  correct_neuropil: bool = True,
1200
1237
  accept_all_cells: bool = False,
1201
1238
  cell_filters: list = None,
@@ -1237,7 +1274,9 @@ def run_volume(
1237
1274
  frame_indices : list, default None
1238
1275
  List of frame indices to process.
1239
1276
  dff_window_size, dff_percentile, dff_smooth_window : optional
1240
- dF/F calculation parameters.
1277
+ dF/F calculation parameters (used when norm_method="dff").
1278
+ norm_method : str, default "dff"
1279
+ Normalization for norm_traces.npy: "dff" or "zscore". See pipeline().
1241
1280
  accept_all_cells : bool, default False
1242
1281
  Mark all ROIs as accepted.
1243
1282
  cell_filters : list, optional
@@ -1274,6 +1313,7 @@ def run_volume(
1274
1313
  from mbo_utilities import imread
1275
1314
  from lbm_suite2p_python.merging import merge_mrois
1276
1315
 
1316
+ _resolve_gpu_env()
1277
1317
  _apply_thread_limits(threads_per_worker)
1278
1318
 
1279
1319
  # Handle input data
@@ -1372,6 +1412,7 @@ def run_volume(
1372
1412
  dff_window_size=dff_window_size,
1373
1413
  dff_percentile=dff_percentile,
1374
1414
  dff_smooth_window=dff_smooth_window,
1415
+ norm_method=norm_method,
1375
1416
  correct_neuropil=correct_neuropil,
1376
1417
  accept_all_cells=accept_all_cells,
1377
1418
  cell_filters=cell_filters,
@@ -1639,6 +1680,7 @@ def run_volume(
1639
1680
  save_path,
1640
1681
  rastermap_kwargs=volumetric_rastermap_kwargs,
1641
1682
  correct_neuropil=correct_neuropil,
1683
+ norm_method=norm_method,
1642
1684
  )
1643
1685
  except Exception as e:
1644
1686
  print(f"Warning: Volume trace figures failed: {e}")
@@ -2220,6 +2262,7 @@ def run_plane(
2220
2262
  dff_window_size: int = None,
2221
2263
  dff_percentile: int = 20,
2222
2264
  dff_smooth_window: int = None,
2265
+ norm_method: str = "dff",
2223
2266
  correct_neuropil: bool = True,
2224
2267
  accept_all_cells: bool = False,
2225
2268
  cell_filters: list = None,
@@ -2261,6 +2304,10 @@ def run_plane(
2261
2304
  Percentile for baseline F0.
2262
2305
  dff_smooth_window : int, optional
2263
2306
  Smoothing window for dF/F. Default: auto-calculated.
2307
+ norm_method : str, default "dff"
2308
+ Normalization for norm_traces.npy: "dff" (rolling percentile ΔF/F)
2309
+ or "zscore" (per-ROI (F - mean) / std). Quality metrics always use
2310
+ ΔF/F regardless of this setting.
2264
2311
  accept_all_cells : bool, default False
2265
2312
  If True, mark all detected ROIs as accepted cells.
2266
2313
  cell_filters : list[dict], optional
@@ -2298,6 +2345,8 @@ def run_plane(
2298
2345
  from mbo_utilities import imread, imwrite
2299
2346
  from mbo_utilities.metadata import get_metadata
2300
2347
 
2348
+ _resolve_gpu_env()
2349
+
2301
2350
  progress_callback = kwargs.pop("progress_callback", None)
2302
2351
 
2303
2352
  if "debug" in kwargs:
@@ -3004,12 +3053,12 @@ def run_plane(
3004
3053
  except Exception as e:
3005
3054
  print(f" Warning: Cell filtering failed: {e}")
3006
3055
 
3007
- # 3. dF/F Calculation
3056
+ # 3. Normalized-trace calculation (norm_traces.npy)
3008
3057
  F_file = plane_dir / "F.npy"
3009
3058
  Fneu_file = plane_dir / "Fneu.npy"
3010
3059
  if F_file.exists() and Fneu_file.exists():
3011
- print(" Computing dF/F...")
3012
- dff_start = time.time()
3060
+ print(f" Computing norm_traces ({norm_method})...")
3061
+ norm_start = time.time()
3013
3062
  F = np.load(F_file)
3014
3063
  Fneu = np.load(Fneu_file)
3015
3064
  if correct_neuropil:
@@ -3018,21 +3067,29 @@ def run_plane(
3018
3067
  F_corr = F
3019
3068
 
3020
3069
  current_ops = load_ops(ops_file)
3021
- dff = dff_rolling_percentile(
3022
- F_corr,
3023
- window_size=dff_window_size,
3024
- percentile=dff_percentile,
3025
- smooth_window=dff_smooth_window,
3026
- fs=current_ops.get("fs", 30.0),
3027
- tau=current_ops.get("tau", 1.0),
3028
- )
3029
- np.save(plane_dir / "dff.npy", dff)
3070
+ if norm_method == "zscore":
3071
+ norm = zscore_trace(
3072
+ F_corr,
3073
+ smooth_window=dff_smooth_window,
3074
+ fs=current_ops.get("fs", 30.0),
3075
+ tau=current_ops.get("tau", 1.0),
3076
+ )
3077
+ else:
3078
+ norm = dff_rolling_percentile(
3079
+ F_corr,
3080
+ window_size=dff_window_size,
3081
+ percentile=dff_percentile,
3082
+ smooth_window=dff_smooth_window,
3083
+ fs=current_ops.get("fs", 30.0),
3084
+ tau=current_ops.get("tau", 1.0),
3085
+ )
3086
+ np.save(plane_dir / "norm_traces.npy", norm)
3030
3087
 
3031
3088
  _add_processing_step(
3032
3089
  current_ops,
3033
- "dff_calculation",
3034
- duration_seconds=time.time() - dff_start,
3035
- extra={"percentile": dff_percentile},
3090
+ "norm_calculation",
3091
+ duration_seconds=time.time() - norm_start,
3092
+ extra={"method": norm_method, "percentile": dff_percentile},
3036
3093
  )
3037
3094
  save_ops_db_settings(ops_file, current_ops)
3038
3095
 
@@ -3050,6 +3107,7 @@ def run_plane(
3050
3107
  _post_ops["dff_window_size"] = dff_window_size
3051
3108
  _post_ops["dff_percentile"] = dff_percentile
3052
3109
  _post_ops["dff_smooth_window"] = dff_smooth_window
3110
+ _post_ops["norm_method"] = norm_method
3053
3111
  _post_ops["correct_neuropil"] = bool(correct_neuropil)
3054
3112
  _post_ops["accept_all_cells"] = bool(accept_all_cells)
3055
3113
  _post_ops["save_json"] = bool(save_json)
@@ -3080,6 +3138,7 @@ def run_plane(
3080
3138
  dff_percentile=dff_percentile,
3081
3139
  dff_window_size=dff_window_size,
3082
3140
  dff_smooth_window=dff_smooth_window,
3141
+ norm_method=norm_method,
3083
3142
  correct_neuropil=correct_neuropil,
3084
3143
  run_rastermap=rastermap_kwargs is not None,
3085
3144
  rastermap_kwargs=rastermap_kwargs,
@@ -7,7 +7,7 @@ import numpy as np
7
7
  from matplotlib import pyplot as plt
8
8
 
9
9
  from lbm_suite2p_python.utils import get_common_path
10
- from lbm_suite2p_python.postprocessing import load_ops
10
+ from lbm_suite2p_python.postprocessing import load_ops, baseline_percentile_dff
11
11
 
12
12
 
13
13
  def update_ops_paths(ops_files: str | list):
@@ -777,10 +777,9 @@ def plot_volume_diagnostics(
777
777
  n_rejected = int(np.sum(~iscell))
778
778
 
779
779
  # Compute SNR for accepted cells
780
+ # static-baseline ΔF/F for quality metrics only (see baseline_percentile_dff)
780
781
  F_corr = F - 0.7 * Fneu
781
- baseline = np.percentile(F_corr, 20, axis=1, keepdims=True)
782
- baseline = np.maximum(baseline, 1e-6)
783
- dff = (F_corr - baseline) / baseline
782
+ dff = baseline_percentile_dff(F_corr)
784
783
 
785
784
  signal = np.std(dff, axis=1)
786
785
  noise = np.median(np.abs(np.diff(dff, axis=1)), axis=1) / 0.6745
@@ -1356,10 +1355,9 @@ def plot_3d_roi_map(
1356
1355
  if F_file.exists():
1357
1356
  F = np.load(F_file, allow_pickle=True)
1358
1357
  Fneu = np.load(Fneu_file, allow_pickle=True) if Fneu_file.exists() else np.zeros_like(F)
1358
+ # static-baseline ΔF/F for quality metrics only (see baseline_percentile_dff)
1359
1359
  F_corr = F - 0.7 * Fneu
1360
- baseline = np.percentile(F_corr, 20, axis=1, keepdims=True)
1361
- baseline = np.maximum(baseline, 1e-6)
1362
- dff = (F_corr - baseline) / baseline
1360
+ dff = baseline_percentile_dff(F_corr)
1363
1361
  signal = np.std(dff, axis=1)
1364
1362
  noise = np.median(np.abs(np.diff(dff, axis=1)), axis=1) / 0.6745
1365
1363
  color_vals = signal / (noise + 1e-6)
@@ -1893,6 +1891,7 @@ def plot_volume_trace_figures(
1893
1891
  dff_percentile: int = 8,
1894
1892
  dff_window_size: int = None,
1895
1893
  dff_smooth_window: int = None,
1894
+ norm_method: str = "dff",
1896
1895
  correct_neuropil: bool = True,
1897
1896
  ):
1898
1897
  """
@@ -1905,9 +1904,9 @@ def plot_volume_trace_figures(
1905
1904
 
1906
1905
  - ``volume_trace_analysis.png`` — :func:`plot_trace_analysis` 6-panel
1907
1906
  extremes by SNR / shot noise / skewness, drawn from the volume.
1908
- - ``volume_traces_raw_{N}.png`` and ``volume_traces_dff_{N}.png`` for
1907
+ - ``volume_traces_raw_{N}.png`` and ``volume_traces_norm_{N}.png`` for
1909
1908
  each ``N`` in ``cell_counts`` — top-N accepted cells by quality
1910
- score, raw and rolling-percentile dF/F.
1909
+ score, raw and normalized (norm_method) traces.
1911
1910
  - ``rastermap.png`` — sorted-activity heatmap, if either a saved
1912
1911
  ``rastermap_model.npy`` exists in ``save_path`` (written by
1913
1912
  :func:`plot_3d_rastermap_clusters`) or ``rastermap_kwargs`` is
@@ -1926,13 +1925,16 @@ def plot_volume_trace_figures(
1926
1925
  kwargs (merged over count-aware defaults). If None and no cached
1927
1926
  model exists, the rastermap heatmap is skipped.
1928
1927
  dff_percentile, dff_window_size, dff_smooth_window
1929
- Forwarded to :func:`dff_rolling_percentile`.
1928
+ Forwarded to :func:`dff_rolling_percentile` (when norm_method="dff").
1929
+ norm_method : str, default "dff"
1930
+ Normalization for the norm-trace plots: "dff" or "zscore".
1930
1931
  """
1931
1932
  from types import SimpleNamespace
1932
1933
  from lbm_suite2p_python.postprocessing import (
1933
1934
  load_planar_results,
1934
1935
  compute_trace_quality_score,
1935
1936
  dff_rolling_percentile,
1937
+ zscore_trace,
1936
1938
  )
1937
1939
  from lbm_suite2p_python.zplane import (
1938
1940
  plot_traces,
@@ -2004,15 +2006,24 @@ def plot_volume_trace_figures(
2004
2006
  stat_acc = [s for s, m in zip(stat, iscell_mask) if m]
2005
2007
 
2006
2008
  try:
2007
- F_for_dff = (F_acc - 0.7 * Fneu_acc) if correct_neuropil else F_acc
2008
- dffp = dff_rolling_percentile(
2009
- F_for_dff,
2010
- percentile=dff_percentile,
2011
- window_size=dff_window_size,
2012
- smooth_window=dff_smooth_window,
2013
- fs=fs,
2014
- tau=tau,
2015
- ) * 100
2009
+ F_for_norm = (F_acc - 0.7 * Fneu_acc) if correct_neuropil else F_acc
2010
+ if norm_method == "zscore":
2011
+ norm = zscore_trace(
2012
+ F_for_norm, smooth_window=dff_smooth_window, fs=fs, tau=tau
2013
+ )
2014
+ norm_unit = "z-score"
2015
+ norm_label = "Z-Score"
2016
+ else:
2017
+ norm = dff_rolling_percentile(
2018
+ F_for_norm,
2019
+ percentile=dff_percentile,
2020
+ window_size=dff_window_size,
2021
+ smooth_window=dff_smooth_window,
2022
+ fs=fs,
2023
+ tau=tau,
2024
+ ) * 100
2025
+ norm_unit = r"% $\Delta$F/F$_0$"
2026
+ norm_label = r"$\Delta$F/F"
2016
2027
 
2017
2028
  quality = compute_trace_quality_score(
2018
2029
  F_acc,
@@ -2022,17 +2033,17 @@ def plot_volume_trace_figures(
2022
2033
  )
2023
2034
  sort_idx = quality["sort_idx"]
2024
2035
  F_sorted = F_acc[sort_idx]
2025
- dffp_sorted = dffp[sort_idx]
2036
+ norm_sorted = norm[sort_idx]
2026
2037
 
2027
2038
  for n_cells in cell_counts:
2028
2039
  n = min(int(n_cells), n_accepted)
2029
2040
  plot_traces(
2030
- dffp_sorted,
2031
- save_path=save_path / f"volume_traces_dff_{n_cells}.png",
2041
+ norm_sorted,
2042
+ save_path=save_path / f"volume_traces_norm_{n_cells}.png",
2032
2043
  num_neurons=n,
2033
2044
  fps=fs,
2034
- scale_bar_unit=r"% $\Delta$F/F$_0$",
2035
- title=rf"Volume Top {n} $\Delta$F/F Traces by Quality (n={n_accepted} total)",
2045
+ scale_bar_unit=norm_unit,
2046
+ title=rf"Volume Top {n} {norm_label} Traces by Quality (n={n_accepted} total)",
2036
2047
  )
2037
2048
  plot_traces(
2038
2049
  F_sorted,
@@ -21,6 +21,8 @@ from lbm_suite2p_python.postprocessing import (
21
21
  load_ops,
22
22
  load_planar_results,
23
23
  dff_rolling_percentile,
24
+ zscore_trace,
25
+ baseline_percentile_dff,
24
26
  dff_shot_noise,
25
27
  compute_trace_quality_score,
26
28
  )
@@ -35,9 +37,10 @@ def infer_units(f: np.ndarray) -> str:
35
37
  Infer calcium imaging signal type from array values:
36
38
  - 'raw': values in hundreds or thousands
37
39
  - 'dff': unitless ΔF/F₀, typically ~0–1
38
- - 'dff-percentile': ΔF/F₀ in percent, typically ~10–100
40
+ - 'dffp': ΔF/F₀ in percent, typically ~10–100
41
+ - 'zscore': centered on 0 with negatives, unit-scale std
39
42
 
40
- Returns one of: 'raw', 'dff', 'dff-percentile'
43
+ Returns one of: 'raw', 'dff', 'dffp', 'zscore', 'unknown'
41
44
  """
42
45
  f = np.asarray(f)
43
46
  if np.issubdtype(f.dtype, np.integer):
@@ -51,6 +54,8 @@ def infer_units(f: np.ndarray) -> str:
51
54
  return "dffp"
52
55
  elif 0.1 < p1 < 0.2 < p50 < 0.5 < p99 < 1.0:
53
56
  return "dff"
57
+ elif p1 < 0 and abs(p50) < 1 and p99 > 0:
58
+ return "zscore"
54
59
  else:
55
60
  return "unknown"
56
61
 
@@ -314,7 +319,7 @@ def plot_traces(
314
319
  ----------
315
320
  f : ndarray or str or Path
316
321
  2d array of fluorescence traces (n_neurons x n_timepoints),
317
- or path to Suite2p plane directory containing dff.npy/F.npy.
322
+ or path to Suite2p plane directory containing norm_traces.npy/F.npy.
318
323
  save_path : str, optional
319
324
  Path to save the output plot.
320
325
  fps : float
@@ -345,22 +350,25 @@ def plot_traces(
345
350
  if isinstance(f, (str, Path)):
346
351
  plane_dir = Path(f)
347
352
  if plane_dir.is_dir():
348
- # Try to load dff.npy first, fall back to F.npy
349
- dff_path = plane_dir / "dff.npy"
353
+ # Try to load norm_traces.npy first, fall back to F.npy
354
+ norm_path = plane_dir / "norm_traces.npy"
350
355
  f_path = plane_dir / "F.npy"
351
356
  iscell_path = plane_dir / "iscell.npy"
352
357
  ops_path = plane_dir / "ops.npy"
353
358
 
354
- if dff_path.exists():
355
- f = np.load(dff_path)
359
+ if norm_path.exists():
360
+ f = np.load(norm_path)
356
361
  if scale_bar_unit is None:
357
- scale_bar_unit = r"% $\Delta$F/F$_0$"
362
+ scale_bar_unit = (
363
+ "z-score" if infer_units(f) == "zscore"
364
+ else r"% $\Delta$F/F$_0$"
365
+ )
358
366
  elif f_path.exists():
359
367
  f = np.load(f_path)
360
368
  if scale_bar_unit is None:
361
369
  scale_bar_unit = "a.u."
362
370
  else:
363
- raise FileNotFoundError(f"No dff.npy or F.npy found in {plane_dir}")
371
+ raise FileNotFoundError(f"No norm_traces.npy or F.npy found in {plane_dir}")
364
372
 
365
373
  # Filter to accepted cells if iscell exists and no cell_indices provided
366
374
  if cell_indices is None and iscell_path.exists():
@@ -2161,11 +2169,9 @@ def plot_trace_analysis(
2161
2169
  plane_nums = np.array([s.get("iplane", 0) for s in stat_acc])
2162
2170
  fs = ops.get("fs", 30.0)
2163
2171
 
2164
- # Compute ΔF/F
2172
+ # static-baseline ΔF/F for quality metrics only (see baseline_percentile_dff)
2165
2173
  F_corrected = F_acc - 0.7 * Fneu_acc
2166
- baseline = np.percentile(F_corrected, 20, axis=1, keepdims=True)
2167
- baseline = np.maximum(baseline, 1e-6)
2168
- dff = (F_corrected - baseline) / baseline
2174
+ dff = baseline_percentile_dff(F_corrected)
2169
2175
 
2170
2176
  # Compute metrics
2171
2177
  # SNR: signal / noise
@@ -2356,10 +2362,9 @@ def create_volume_summary_table(
2356
2362
  if F is not None and Fneu is not None:
2357
2363
  F_acc = F[accepted]
2358
2364
  Fneu_acc = Fneu[accepted]
2365
+ # static-baseline ΔF/F for quality metrics only (see baseline_percentile_dff)
2359
2366
  F_corrected = F_acc - 0.7 * Fneu_acc
2360
- baseline = np.percentile(F_corrected, 20, axis=1, keepdims=True)
2361
- baseline = np.maximum(baseline, 1e-6)
2362
- dff = (F_corrected - baseline) / baseline
2367
+ dff = baseline_percentile_dff(F_corrected)
2363
2368
  signal = np.std(dff, axis=1)
2364
2369
  noise = np.median(np.abs(np.diff(dff, axis=1)), axis=1) / 0.6745
2365
2370
  snr = signal / (noise + 1e-6)
@@ -2657,10 +2662,9 @@ def plot_plane_diagnostics(
2657
2662
  n_rejected = int((~accepted).sum())
2658
2663
 
2659
2664
  # Compute metrics for ALL ROIs (not just accepted)
2665
+ # static-baseline ΔF/F for quality metrics only (see baseline_percentile_dff)
2660
2666
  F_corr = F - 0.7 * Fneu
2661
- baseline = np.percentile(F_corr, 20, axis=1, keepdims=True)
2662
- baseline = np.maximum(baseline, 1e-6)
2663
- dff = (F_corr - baseline) / baseline
2667
+ dff = baseline_percentile_dff(F_corr)
2664
2668
 
2665
2669
  # SNR calculation for all ROIs
2666
2670
  signal = np.std(dff, axis=1)
@@ -3052,7 +3056,8 @@ def mask_dead_zones_in_ops(ops, threshold=0.01):
3052
3056
 
3053
3057
  def plot_zplane_figures(
3054
3058
  plane_dir, dff_percentile=20, dff_window_size=None, dff_smooth_window=None,
3055
- correct_neuropil=True, run_rastermap=False, rastermap_kwargs=None, **kwargs
3059
+ norm_method="dff", correct_neuropil=True, run_rastermap=False,
3060
+ rastermap_kwargs=None, **kwargs
3056
3061
  ):
3057
3062
  """
3058
3063
  Re-generate Suite2p figures for a merged plane.
@@ -3062,7 +3067,7 @@ def plot_zplane_figures(
3062
3067
  plane_dir : Path
3063
3068
  Path to the planeXX output directory (with ops.npy, stat.npy, etc.).
3064
3069
  dff_percentile : int, optional
3065
- Percentile used for ΔF/F baseline.
3070
+ Percentile used for ΔF/F baseline (when norm_method="dff").
3066
3071
  dff_window_size : int, optional
3067
3072
  Window size for ΔF/F rolling baseline. If None, auto-calculated
3068
3073
  as ~10 × tau × fs based on ops values.
@@ -3070,6 +3075,8 @@ def plot_zplane_figures(
3070
3075
  Temporal smoothing window for dF/F traces (in frames).
3071
3076
  If None, auto-calculated as ~0.5 × tau × fs to emphasize
3072
3077
  transients while reducing noise. Set to 1 to disable.
3078
+ norm_method : str, default "dff"
3079
+ Normalization used for the norm-trace plots: "dff" or "zscore".
3073
3080
  run_rastermap : bool, optional
3074
3081
  If True, compute and plot rastermap sorting of cells.
3075
3082
  rastermap_kwargs : dict, optional
@@ -3106,9 +3113,9 @@ def plot_zplane_figures(
3106
3113
  "traces_raw_20": plane_dir / "07a_traces_raw_20.png",
3107
3114
  "traces_raw_50": plane_dir / "07b_traces_raw_50.png",
3108
3115
  "traces_raw_100": plane_dir / "07c_traces_raw_100.png",
3109
- "traces_dff_20": plane_dir / "08a_traces_dff_20.png",
3110
- "traces_dff_50": plane_dir / "08b_traces_dff_50.png",
3111
- "traces_dff_100": plane_dir / "08c_traces_dff_100.png",
3116
+ "traces_norm_20": plane_dir / "08a_traces_norm_20.png",
3117
+ "traces_norm_50": plane_dir / "08b_traces_norm_50.png",
3118
+ "traces_norm_100": plane_dir / "08c_traces_norm_100.png",
3112
3119
  "traces_rejected": plane_dir / "09_traces_rejected.png",
3113
3120
  # Noise distributions
3114
3121
  "noise_acc": plane_dir / "10_shot_noise_accepted.png",
@@ -3138,9 +3145,9 @@ def plot_zplane_figures(
3138
3145
  "traces_raw_20",
3139
3146
  "traces_raw_50",
3140
3147
  "traces_raw_100",
3141
- "traces_dff_20",
3142
- "traces_dff_50",
3143
- "traces_dff_100",
3148
+ "traces_norm_20",
3149
+ "traces_norm_50",
3150
+ "traces_norm_100",
3144
3151
  "traces_rejected",
3145
3152
  "noise_acc",
3146
3153
  "noise_rej",
@@ -3166,8 +3173,8 @@ def plot_zplane_figures(
3166
3173
 
3167
3174
  # F (raw) feeds the "raw" trace plots and compute_trace_quality_score
3168
3175
  # (which optionally subtracts Fneu internally). F_for_dff is the input
3169
- # to dff_rolling_percentile — neuropil-corrected only when the toggle
3170
- # is on, matching dff.npy.
3176
+ # to the norm-trace recompute — neuropil-corrected only when the
3177
+ # toggle is on, matching norm_traces.npy.
3171
3178
  if correct_neuropil and Fneu is not None:
3172
3179
  F_for_dff = F - 0.7 * Fneu
3173
3180
  else:
@@ -3431,9 +3438,8 @@ def plot_zplane_figures(
3431
3438
  fs = output_ops.get("fs", 1.0)
3432
3439
  tau = output_ops.get("tau", 1.0)
3433
3440
 
3434
- # resolve auto-calculated dF/F window sizes the same way
3435
- # dff_rolling_percentile does, so the param footer reflects
3436
- # the values actually used
3441
+ # resolve auto-calculated window sizes the same way the trace
3442
+ # functions do, so the param footer reflects the values used
3437
3443
  _resolved_window = (
3438
3444
  int(dff_window_size) if dff_window_size is not None
3439
3445
  else (int(10 * tau * fs) if (fs and tau) else 300)
@@ -3442,56 +3448,62 @@ def plot_zplane_figures(
3442
3448
  int(dff_smooth_window) if dff_smooth_window is not None
3443
3449
  else (max(1, int(0.5 * tau * fs)) if (fs and tau) else 1)
3444
3450
  )
3445
- dff_param_text = (
3446
- f"dff_percentile={dff_percentile} "
3447
- f"window={_resolved_window}f ({_resolved_window / fs:.1f}s) "
3448
- f"smooth={_resolved_smooth}f ({_resolved_smooth / fs:.2f}s) "
3449
- f"fs={fs:.2f}Hz tau={tau:.2f}s "
3450
- f"neuropil={'on' if correct_neuropil else 'off'}"
3451
- )
3451
+ # method-aware labels for the norm-trace plots
3452
+ if norm_method == "zscore":
3453
+ norm_unit = "z-score"
3454
+ norm_label = "Z-Score"
3455
+ norm_param_text = (
3456
+ f"norm=zscore "
3457
+ f"smooth={_resolved_smooth}f ({_resolved_smooth / fs:.2f}s) "
3458
+ f"fs={fs:.2f}Hz tau={tau:.2f}s "
3459
+ f"neuropil={'on' if correct_neuropil else 'off'}"
3460
+ )
3461
+ else:
3462
+ norm_unit = r"% $\Delta$F/F$_0$"
3463
+ norm_label = r"$\Delta$F/F"
3464
+ norm_param_text = (
3465
+ f"dff_percentile={dff_percentile} "
3466
+ f"window={_resolved_window}f ({_resolved_window / fs:.1f}s) "
3467
+ f"smooth={_resolved_smooth}f ({_resolved_smooth / fs:.2f}s) "
3468
+ f"fs={fs:.2f}Hz tau={tau:.2f}s "
3469
+ f"neuropil={'on' if correct_neuropil else 'off'}"
3470
+ )
3452
3471
 
3453
- # unsmoothed dF/F for shot noise
3454
- if n_accepted > 0:
3455
- dffp_acc_unsmoothed = dff_rolling_percentile(
3456
- F_dff_accepted,
3457
- percentile=dff_percentile,
3458
- window_size=dff_window_size,
3459
- smooth_window=1,
3460
- fs=fs,
3461
- tau=tau,
3462
- ) * 100
3463
- dffp_acc = dff_rolling_percentile(
3464
- F_dff_accepted,
3472
+ def _norm_for_display(F_in):
3473
+ # display trace, follows norm_method
3474
+ if F_in.shape[0] == 0:
3475
+ return np.zeros((0, F.shape[1]))
3476
+ if norm_method == "zscore":
3477
+ return zscore_trace(
3478
+ F_in, smooth_window=dff_smooth_window, fs=fs, tau=tau
3479
+ )
3480
+ return dff_rolling_percentile(
3481
+ F_in,
3465
3482
  percentile=dff_percentile,
3466
3483
  window_size=dff_window_size,
3467
3484
  smooth_window=dff_smooth_window,
3468
3485
  fs=fs,
3469
3486
  tau=tau,
3470
3487
  ) * 100
3471
- else:
3472
- dffp_acc_unsmoothed = np.zeros((0, F.shape[1]))
3473
- dffp_acc = np.zeros((0, F.shape[1]))
3474
3488
 
3475
- if n_rejected > 0:
3476
- dffp_rej_unsmoothed = dff_rolling_percentile(
3477
- F_dff_rejected,
3489
+ def _dff_unsmoothed(F_in):
3490
+ # always ΔF/F (%): shot noise is defined on ΔF/F regardless
3491
+ # of the selected norm_method
3492
+ if F_in.shape[0] == 0:
3493
+ return np.zeros((0, F.shape[1]))
3494
+ return dff_rolling_percentile(
3495
+ F_in,
3478
3496
  percentile=dff_percentile,
3479
3497
  window_size=dff_window_size,
3480
3498
  smooth_window=1,
3481
3499
  fs=fs,
3482
3500
  tau=tau,
3483
3501
  ) * 100
3484
- dffp_rej = dff_rolling_percentile(
3485
- F_dff_rejected,
3486
- percentile=dff_percentile,
3487
- window_size=dff_window_size,
3488
- smooth_window=dff_smooth_window,
3489
- fs=fs,
3490
- tau=tau,
3491
- ) * 100
3492
- else:
3493
- dffp_rej_unsmoothed = np.zeros((0, F.shape[1]))
3494
- dffp_rej = np.zeros((0, F.shape[1]))
3502
+
3503
+ norm_acc = _norm_for_display(F_dff_accepted)
3504
+ norm_rej = _norm_for_display(F_dff_rejected)
3505
+ dff_acc_unsmoothed = _dff_unsmoothed(F_dff_accepted)
3506
+ dff_rej_unsmoothed = _dff_unsmoothed(F_dff_rejected)
3495
3507
 
3496
3508
  if n_accepted > 0:
3497
3509
  stat_accepted = [s for s, m in zip(res["stat"], iscell_mask) if m]
@@ -3504,19 +3516,19 @@ def plot_zplane_figures(
3504
3516
  fs=fs,
3505
3517
  )
3506
3518
  quality_sort_idx = quality["sort_idx"]
3507
- dffp_acc_sorted = dffp_acc[quality_sort_idx]
3519
+ norm_acc_sorted = norm_acc[quality_sort_idx]
3508
3520
  F_accepted_sorted = F_accepted[quality_sort_idx]
3509
3521
 
3510
3522
  cell_counts = [20, 50, 100]
3511
3523
  for n_cells in cell_counts:
3512
3524
  if n_accepted >= n_cells:
3513
3525
  plot_traces(
3514
- dffp_acc_sorted,
3515
- save_path=expected_files[f"traces_dff_{n_cells}"],
3526
+ norm_acc_sorted,
3527
+ save_path=expected_files[f"traces_norm_{n_cells}"],
3516
3528
  num_neurons=n_cells,
3517
- scale_bar_unit=r"% $\Delta$F/F$_0$",
3518
- title=rf"Top {n_cells} $\Delta$F/F Traces by Quality (n={n_accepted} total)",
3519
- fig_text=dff_param_text,
3529
+ scale_bar_unit=norm_unit,
3530
+ title=rf"Top {n_cells} {norm_label} Traces by Quality (n={n_accepted} total)",
3531
+ fig_text=norm_param_text,
3520
3532
  )
3521
3533
  plot_traces(
3522
3534
  F_accepted_sorted,
@@ -3527,12 +3539,12 @@ def plot_zplane_figures(
3527
3539
  )
3528
3540
  elif n_cells == 20:
3529
3541
  plot_traces(
3530
- dffp_acc_sorted,
3531
- save_path=expected_files["traces_dff_20"],
3542
+ norm_acc_sorted,
3543
+ save_path=expected_files["traces_norm_20"],
3532
3544
  num_neurons=min(20, n_accepted),
3533
- scale_bar_unit=r"% $\Delta$F/F$_0$",
3534
- title=rf"Top {min(20, n_accepted)} $\Delta$F/F Traces by Quality (n={n_accepted} total)",
3535
- fig_text=dff_param_text,
3545
+ scale_bar_unit=norm_unit,
3546
+ title=rf"Top {min(20, n_accepted)} {norm_label} Traces by Quality (n={n_accepted} total)",
3547
+ fig_text=norm_param_text,
3536
3548
  )
3537
3549
  plot_traces(
3538
3550
  F_accepted_sorted,
@@ -3546,19 +3558,19 @@ def plot_zplane_figures(
3546
3558
 
3547
3559
  if n_rejected > 0:
3548
3560
  plot_traces(
3549
- dffp_rej,
3561
+ norm_rej,
3550
3562
  save_path=expected_files["traces_rejected"],
3551
3563
  num_neurons=min(20, n_rejected),
3552
- scale_bar_unit=r"% $\Delta$F/F$_0$",
3553
- title=rf"$\Delta$F/F Traces - Rejected ROIs (n={n_rejected})",
3554
- fig_text=dff_param_text,
3564
+ scale_bar_unit=norm_unit,
3565
+ title=rf"{norm_label} Traces - Rejected ROIs (n={n_rejected})",
3566
+ fig_text=norm_param_text,
3555
3567
  )
3556
3568
  else:
3557
3569
  print(" No rejected ROIs - skipping rejected trace plots")
3558
3570
 
3559
3571
  # noise distributions
3560
3572
  if n_accepted > 0:
3561
- dff_noise_acc = dff_shot_noise(dffp_acc_unsmoothed, fs)
3573
+ dff_noise_acc = dff_shot_noise(dff_acc_unsmoothed, fs)
3562
3574
  plot_noise_distribution(
3563
3575
  dff_noise_acc,
3564
3576
  output_filename=expected_files["noise_acc"],
@@ -3566,7 +3578,7 @@ def plot_zplane_figures(
3566
3578
  )
3567
3579
 
3568
3580
  if n_rejected > 0:
3569
- dff_noise_rej = dff_shot_noise(dffp_rej_unsmoothed, fs)
3581
+ dff_noise_rej = dff_shot_noise(dff_rej_unsmoothed, fs)
3570
3582
  plot_noise_distribution(
3571
3583
  dff_noise_rej,
3572
3584
  output_filename=expected_files["noise_rej"],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lbm_suite2p_python
3
- Version: 3.0.7
3
+ Version: 3.0.8
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.0.7
15
15
  Requires-Dist: suite2p>=1.0.0.1
16
16
  Requires-Dist: setuptools<81
17
17
  Provides-Extra: rastermap
@@ -1,4 +1,4 @@
1
- mbo_utilities>=3.0.3
1
+ mbo_utilities>=3.0.7
2
2
  suite2p>=1.0.0.1
3
3
  setuptools<81
4
4
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lbm_suite2p_python"
7
- version = "3.0.7"
7
+ version = "3.0.8"
8
8
  description = "Calcium Imaging Pipeline built with Suite2p, Cellpose and Rastermap"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -18,7 +18,7 @@ classifiers=[
18
18
  ]
19
19
 
20
20
  dependencies = [
21
- "mbo_utilities>=3.0.3",
21
+ "mbo_utilities>=3.0.7",
22
22
  "suite2p>=1.0.0.1",
23
23
  "setuptools<81",
24
24
  ]
@@ -522,6 +522,35 @@ class TestDFFCalculation:
522
522
  time.time() - start, {"error": str(e)})
523
523
  raise
524
524
 
525
+ def test_zscore_trace(self):
526
+ """Test zscore_trace function."""
527
+ from lbm_suite2p_python.postprocessing import zscore_trace
528
+
529
+ start = time.time()
530
+
531
+ try:
532
+ F = self._get_test_traces()
533
+
534
+ z = zscore_trace(F)
535
+
536
+ assert z.shape == F.shape, "z-score shape mismatch"
537
+ assert not np.any(np.isnan(z)), "z-score contains NaN values"
538
+ assert not np.any(np.isinf(z)), "z-score contains Inf values"
539
+
540
+ # per-ROI mean ~ 0 and std ~ 1
541
+ assert np.allclose(np.mean(z, axis=1), 0, atol=1e-4), "z-score not zero-mean"
542
+ assert np.allclose(np.std(z, axis=1), 1, atol=1e-2), "z-score not unit-std"
543
+
544
+ duration = time.time() - start
545
+ record_result(self.summary, "test_zscore_trace", "passed", duration, {
546
+ "shape": list(z.shape),
547
+ })
548
+
549
+ except Exception as e:
550
+ record_result(self.summary, "test_zscore_trace", "failed",
551
+ time.time() - start, {"error": str(e)})
552
+ raise
553
+
525
554
  def test_dff_window_size_effect(self):
526
555
  """Test that different window sizes produce different results."""
527
556
  from lbm_suite2p_python.postprocessing import dff_rolling_percentile
@@ -65,15 +65,17 @@ def test_pipeline_delegates_to_run_volume_with_planes_arg(mock_imread, mock_run_
65
65
  @patch("lbm_suite2p_python.run_lsp.run_volume")
66
66
  def test_pipeline_passes_parameters(mock_run_volume, mock_4d_array):
67
67
  pipeline(
68
- mock_4d_array,
68
+ mock_4d_array,
69
69
  save_path="test_output",
70
70
  dff_percentile=30,
71
+ norm_method="zscore",
71
72
  accept_all_cells=True,
72
73
  cell_filters=[{"name": "test"}]
73
74
  )
74
-
75
+
75
76
  kwargs = mock_run_volume.call_args.kwargs
76
77
  assert kwargs['dff_percentile'] == 30
78
+ assert kwargs['norm_method'] == "zscore"
77
79
  assert kwargs['accept_all_cells'] is True
78
80
  assert kwargs['cell_filters'] == [{"name": "test"}]
79
81