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.
- {lbm_suite2p_python-3.0.7/lbm_suite2p_python.egg-info → lbm_suite2p_python-3.0.8}/PKG-INFO +2 -2
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/__init__.py +4 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/cli.py +6 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/conversion.py +2 -2
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/postprocessing.py +78 -7
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/run_lsp.py +78 -19
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/volume.py +35 -24
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/zplane.py +97 -85
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8/lbm_suite2p_python.egg-info}/PKG-INFO +2 -2
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python.egg-info/requires.txt +1 -1
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/pyproject.toml +2 -2
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/tests/test_pipeline_parameters.py +29 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/tests/test_refactored_pipeline.py +4 -2
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/LICENSE.md +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/MANIFEST.in +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/README.md +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/__main__.py +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/_benchmarking.py +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/cellpose.py +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/db_settings.py +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/default_ops.py +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/grid_search.py +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/gui.py +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/merging.py +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python/utils.py +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python.egg-info/SOURCES.txt +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python.egg-info/dependency_links.txt +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python.egg-info/entry_points.txt +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python.egg-info/top_level.txt +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/setup.cfg +0 -0
- {lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/tests/test_frame_count_aliases.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
"
|
|
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", "
|
|
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
|
-
#
|
|
1283
|
-
|
|
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
|
|
1452
|
-
|
|
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", "
|
|
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,
|
|
840
|
-
Affects
|
|
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.
|
|
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
|
|
3012
|
-
|
|
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
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
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
|
-
"
|
|
3034
|
-
duration_seconds=time.time() -
|
|
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
|
-
|
|
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
|
-
|
|
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 ``
|
|
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
|
|
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
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2031
|
-
save_path=save_path / f"
|
|
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=
|
|
2035
|
-
title=rf"Volume Top {n}
|
|
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
|
-
- '
|
|
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', '
|
|
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
|
|
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
|
|
349
|
-
|
|
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
|
|
355
|
-
f = np.load(
|
|
359
|
+
if norm_path.exists():
|
|
360
|
+
f = np.load(norm_path)
|
|
356
361
|
if scale_bar_unit is None:
|
|
357
|
-
scale_bar_unit =
|
|
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
|
|
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
|
-
#
|
|
2172
|
+
# static-baseline ΔF/F for quality metrics only (see baseline_percentile_dff)
|
|
2165
2173
|
F_corrected = F_acc - 0.7 * Fneu_acc
|
|
2166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
"
|
|
3110
|
-
"
|
|
3111
|
-
"
|
|
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
|
-
"
|
|
3142
|
-
"
|
|
3143
|
-
"
|
|
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
|
|
3170
|
-
# is on, matching
|
|
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
|
|
3435
|
-
#
|
|
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
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
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
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
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
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
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
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3515
|
-
save_path=expected_files[f"
|
|
3526
|
+
norm_acc_sorted,
|
|
3527
|
+
save_path=expected_files[f"traces_norm_{n_cells}"],
|
|
3516
3528
|
num_neurons=n_cells,
|
|
3517
|
-
scale_bar_unit=
|
|
3518
|
-
title=rf"Top {n_cells}
|
|
3519
|
-
fig_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
|
-
|
|
3531
|
-
save_path=expected_files["
|
|
3542
|
+
norm_acc_sorted,
|
|
3543
|
+
save_path=expected_files["traces_norm_20"],
|
|
3532
3544
|
num_neurons=min(20, n_accepted),
|
|
3533
|
-
scale_bar_unit=
|
|
3534
|
-
title=rf"Top {min(20, n_accepted)}
|
|
3535
|
-
fig_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
|
-
|
|
3561
|
+
norm_rej,
|
|
3550
3562
|
save_path=expected_files["traces_rejected"],
|
|
3551
3563
|
num_neurons=min(20, n_rejected),
|
|
3552
|
-
scale_bar_unit=
|
|
3553
|
-
title=rf"
|
|
3554
|
-
fig_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(
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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
|
+
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.
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{lbm_suite2p_python-3.0.7 → lbm_suite2p_python-3.0.8}/lbm_suite2p_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|