lbm_suite2p_python 3.0.4__tar.gz → 3.0.6__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.4/lbm_suite2p_python.egg-info → lbm_suite2p_python-3.0.6}/PKG-INFO +1 -1
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/cli.py +79 -2
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/db_settings.py +1 -1
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/run_lsp.py +525 -120
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6/lbm_suite2p_python.egg-info}/PKG-INFO +1 -1
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/pyproject.toml +1 -1
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/LICENSE.md +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/MANIFEST.in +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/README.md +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/__init__.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/__main__.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/_benchmarking.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/cellpose.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/conversion.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/default_ops.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/grid_search.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/gui.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/merging.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/postprocessing.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/utils.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/volume.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python/zplane.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python.egg-info/SOURCES.txt +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python.egg-info/dependency_links.txt +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python.egg-info/entry_points.txt +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python.egg-info/requires.txt +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python.egg-info/top_level.txt +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/setup.cfg +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/tests/test_frame_count_aliases.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/tests/test_pipeline_parameters.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/tests/test_refactored_pipeline.py +0 -0
- {lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/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.6
|
|
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
|
|
@@ -174,6 +174,24 @@ Examples:
|
|
|
174
174
|
"--accept-all-cells", action="store_true",
|
|
175
175
|
help="mark all detected ROIs as accepted cells"
|
|
176
176
|
)
|
|
177
|
+
pipeline.add_argument(
|
|
178
|
+
"--workers", type=int, default=4,
|
|
179
|
+
help="number of zplane worker processes (default: 4). "
|
|
180
|
+
"Use 1 for sequential, or 0 / negative for auto = "
|
|
181
|
+
"min(num_planes, cpu_count//2, 8). "
|
|
182
|
+
"Cellpose on GPU may OOM with multiple workers."
|
|
183
|
+
)
|
|
184
|
+
pipeline.add_argument(
|
|
185
|
+
"--skip-volumetric", action="store_true", dest="skip_volumetric",
|
|
186
|
+
help="skip merge_mrois, volume_stats, and volumetric plots after per-plane processing"
|
|
187
|
+
)
|
|
188
|
+
pipeline.add_argument(
|
|
189
|
+
"--threads-per-worker", type=int, dest="threads_per_worker", default=2,
|
|
190
|
+
help="cap BLAS / OMP / numba / torch threads per worker process "
|
|
191
|
+
"(default: 2). Total CPU load ~ workers * threads_per_worker. "
|
|
192
|
+
"Set to 0 or a negative value to use library defaults "
|
|
193
|
+
"(typically 1 thread per core, which oversubscribes when workers > 1)."
|
|
194
|
+
)
|
|
177
195
|
|
|
178
196
|
# dff options
|
|
179
197
|
dff = parser.add_argument_group("dff options")
|
|
@@ -324,6 +342,30 @@ def list_ops():
|
|
|
324
342
|
print(f" --{_snake_to_kebab(key):<22} {default_str:<20} {help_text}")
|
|
325
343
|
|
|
326
344
|
|
|
345
|
+
class _Tee:
|
|
346
|
+
"""Duplicate writes to multiple text streams (e.g., stdout + file)."""
|
|
347
|
+
|
|
348
|
+
def __init__(self, *streams):
|
|
349
|
+
self.streams = streams
|
|
350
|
+
|
|
351
|
+
def write(self, data):
|
|
352
|
+
for s in self.streams:
|
|
353
|
+
try:
|
|
354
|
+
s.write(data)
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
def flush(self):
|
|
359
|
+
for s in self.streams:
|
|
360
|
+
try:
|
|
361
|
+
s.flush()
|
|
362
|
+
except Exception:
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
def isatty(self):
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
|
|
327
369
|
def build_cell_filters(args) -> list | None:
|
|
328
370
|
"""build cell filters list from CLI args."""
|
|
329
371
|
filters = []
|
|
@@ -457,8 +499,28 @@ def main():
|
|
|
457
499
|
|
|
458
500
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
459
501
|
|
|
502
|
+
log_path = output_path / "log.txt"
|
|
503
|
+
log_file = open(log_path, "w", encoding="utf-8", buffering=1)
|
|
504
|
+
_orig_stdout, _orig_stderr = sys.stdout, sys.stderr
|
|
505
|
+
sys.stdout = _Tee(_orig_stdout, log_file)
|
|
506
|
+
sys.stderr = _Tee(_orig_stderr, log_file)
|
|
507
|
+
print(f"Logging to: {log_path}")
|
|
508
|
+
|
|
509
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
510
|
+
try:
|
|
511
|
+
_mbo_version = _pkg_version("mbo_utilities")
|
|
512
|
+
except PackageNotFoundError:
|
|
513
|
+
_mbo_version = "unknown"
|
|
514
|
+
try:
|
|
515
|
+
_s2p_version = _pkg_version("suite2p")
|
|
516
|
+
except PackageNotFoundError:
|
|
517
|
+
_s2p_version = "not installed"
|
|
518
|
+
|
|
460
519
|
print(f"\n{'='*60}")
|
|
461
520
|
print(f"LBM Suite2p Pipeline v{__version__}")
|
|
521
|
+
print(f" mbo_utilities v{_mbo_version}")
|
|
522
|
+
print(f" lbm_suite2p_python v{__version__}")
|
|
523
|
+
print(f" suite2p v{_s2p_version}")
|
|
462
524
|
print(f"{'='*60}")
|
|
463
525
|
print(f"Input: {input_path}")
|
|
464
526
|
print(f"Output: {output_path}")
|
|
@@ -491,6 +553,8 @@ def main():
|
|
|
491
553
|
print(f"\n{'='*60}\n")
|
|
492
554
|
|
|
493
555
|
# run pipeline
|
|
556
|
+
import time
|
|
557
|
+
_pipeline_start = time.time()
|
|
494
558
|
try:
|
|
495
559
|
lsp.pipeline(
|
|
496
560
|
input_data=input_path,
|
|
@@ -501,8 +565,8 @@ def main():
|
|
|
501
565
|
num_timepoints=args.num_timepoints,
|
|
502
566
|
keep_reg=args.keep_reg,
|
|
503
567
|
keep_raw=args.keep_raw,
|
|
504
|
-
force_reg=args.force_reg,
|
|
505
|
-
force_detect=args.force_detect,
|
|
568
|
+
force_reg=args.force_reg or args.overwrite,
|
|
569
|
+
force_detect=args.force_detect or args.overwrite,
|
|
506
570
|
dff_window_size=args.dff_window_size,
|
|
507
571
|
dff_percentile=args.dff_percentile,
|
|
508
572
|
dff_smooth_window=args.dff_smooth_window,
|
|
@@ -511,11 +575,18 @@ def main():
|
|
|
511
575
|
accept_all_cells=args.accept_all_cells,
|
|
512
576
|
save_json=args.save_json,
|
|
513
577
|
reader_kwargs=reader_kwargs if reader_kwargs else None,
|
|
578
|
+
workers=args.workers,
|
|
579
|
+
skip_volumetric=args.skip_volumetric,
|
|
580
|
+
threads_per_worker=args.threads_per_worker,
|
|
514
581
|
)
|
|
515
582
|
|
|
583
|
+
_elapsed = time.time() - _pipeline_start
|
|
584
|
+
_h, _rem = divmod(int(_elapsed), 3600)
|
|
585
|
+
_m, _s = divmod(_rem, 60)
|
|
516
586
|
print(f"\n{'='*60}")
|
|
517
587
|
print(f"Processing complete!")
|
|
518
588
|
print(f"Results saved to: {output_path}")
|
|
589
|
+
print(f"Total elapsed: {_h:02d}:{_m:02d}:{_s:02d} ({_elapsed:.1f}s)")
|
|
519
590
|
print(f"{'='*60}\n")
|
|
520
591
|
|
|
521
592
|
return 0
|
|
@@ -525,6 +596,12 @@ def main():
|
|
|
525
596
|
import traceback
|
|
526
597
|
traceback.print_exc()
|
|
527
598
|
return 1
|
|
599
|
+
finally:
|
|
600
|
+
sys.stdout, sys.stderr = _orig_stdout, _orig_stderr
|
|
601
|
+
try:
|
|
602
|
+
log_file.close()
|
|
603
|
+
except Exception:
|
|
604
|
+
pass
|
|
528
605
|
|
|
529
606
|
|
|
530
607
|
if __name__ == "__main__":
|
|
@@ -190,7 +190,7 @@ _ANATOMICAL_ONLY_TO_IMG: dict[int, str] = {
|
|
|
190
190
|
_UPSTREAM_TO_FORK_RENAMES = {v: k for k, v in _FORK_TO_UPSTREAM_RENAMES.items()}
|
|
191
191
|
|
|
192
192
|
# per-section flat-key disambiguation. Several upstream sections share
|
|
193
|
-
# an upstream key. When flattened to ops, the second iteration
|
|
193
|
+
# an upstream key. When flattened to ops, the second iteration overwrites
|
|
194
194
|
# the first; on the reverse derivation, the surviving value gets fanned
|
|
195
195
|
# back into BOTH sections — silently corrupting one of them.
|
|
196
196
|
# This map gives the colliding (section, upstream_key) pair its own
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import logging.handlers
|
|
3
|
+
import multiprocessing as mp
|
|
4
|
+
import os
|
|
5
|
+
import pickle
|
|
6
|
+
import sys
|
|
2
7
|
import time
|
|
8
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
3
9
|
from datetime import datetime
|
|
4
10
|
from pathlib import Path
|
|
5
11
|
import traceback
|
|
@@ -228,7 +234,7 @@ def _resolve_source_plane_dir(input_arr, input_path, target_plane, source_plane_
|
|
|
228
234
|
def _copy_if_needed(src: Path, dst: Path, *, overwrite_empty: bool = True) -> bool:
|
|
229
235
|
"""Copy src -> dst when dst is missing (or empty, if overwrite_empty).
|
|
230
236
|
|
|
231
|
-
Returns True when a copy actually happened. Never
|
|
237
|
+
Returns True when a copy actually happened. Never overwrites a
|
|
232
238
|
non-empty destination.
|
|
233
239
|
"""
|
|
234
240
|
import shutil
|
|
@@ -237,7 +243,6 @@ def _copy_if_needed(src: Path, dst: Path, *, overwrite_empty: bool = True) -> bo
|
|
|
237
243
|
if dst.exists():
|
|
238
244
|
if not (overwrite_empty and dst.stat().st_size == 0):
|
|
239
245
|
return False
|
|
240
|
-
print(f" Copying {src.name} from {src.parent} -> {dst.parent}")
|
|
241
246
|
shutil.copy2(src, dst)
|
|
242
247
|
return True
|
|
243
248
|
|
|
@@ -263,6 +268,7 @@ def _stage_source_into_plane_dir(
|
|
|
263
268
|
plane_dir = Path(plane_dir)
|
|
264
269
|
plane_dir.mkdir(parents=True, exist_ok=True)
|
|
265
270
|
|
|
271
|
+
print(f" Staging plane outputs: {src_dir.name} -> {plane_dir.name}")
|
|
266
272
|
_copy_if_needed(src_dir / "ops.npy", plane_dir / "ops.npy")
|
|
267
273
|
for fname in _DETECTION_OUTPUT_FILES:
|
|
268
274
|
_copy_if_needed(src_dir / fname, plane_dir / fname)
|
|
@@ -439,13 +445,37 @@ from lbm_suite2p_python.utils import _is_lazy_array, _get_num_planes
|
|
|
439
445
|
def _get_suite2p_version():
|
|
440
446
|
"""Get suite2p version string."""
|
|
441
447
|
try:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return getattr(suite2p, "__version__", "unknown")
|
|
445
|
-
except ImportError:
|
|
448
|
+
return version("suite2p")
|
|
449
|
+
except PackageNotFoundError:
|
|
446
450
|
return "not installed"
|
|
447
451
|
|
|
448
452
|
|
|
453
|
+
def _apply_thread_limits(threads_per_worker: int | None) -> None:
|
|
454
|
+
"""Cap BLAS / OMP / numba / torch thread counts per process.
|
|
455
|
+
|
|
456
|
+
Sets env vars so any child process spawned later inherits the cap,
|
|
457
|
+
and calls torch.set_num_threads for the current process (where the
|
|
458
|
+
BLAS env vars may have been read at import time and cannot be
|
|
459
|
+
changed without threadpoolctl). No-op when value is None or <= 0.
|
|
460
|
+
"""
|
|
461
|
+
if not threads_per_worker or threads_per_worker <= 0:
|
|
462
|
+
return
|
|
463
|
+
n = str(int(threads_per_worker))
|
|
464
|
+
for var in (
|
|
465
|
+
"OMP_NUM_THREADS",
|
|
466
|
+
"MKL_NUM_THREADS",
|
|
467
|
+
"OPENBLAS_NUM_THREADS",
|
|
468
|
+
"NUMEXPR_NUM_THREADS",
|
|
469
|
+
"NUMBA_NUM_THREADS",
|
|
470
|
+
):
|
|
471
|
+
os.environ[var] = n
|
|
472
|
+
try:
|
|
473
|
+
import torch
|
|
474
|
+
torch.set_num_threads(int(threads_per_worker))
|
|
475
|
+
except Exception:
|
|
476
|
+
pass
|
|
477
|
+
|
|
478
|
+
|
|
449
479
|
def _add_processing_step(
|
|
450
480
|
ops, step_name, input_files=None, duration_seconds=None, extra=None
|
|
451
481
|
):
|
|
@@ -523,6 +553,160 @@ def _extract_volumetric_rastermap_kwargs(rastermap_kwargs):
|
|
|
523
553
|
return _extract_rastermap_section(rastermap_kwargs, "volumetric")
|
|
524
554
|
|
|
525
555
|
|
|
556
|
+
def _resolve_input_source(input_arr, input_data):
|
|
557
|
+
"""Return a picklable source spec for parallel workers to imread().
|
|
558
|
+
|
|
559
|
+
Workers re-open the data themselves rather than receiving a pickled
|
|
560
|
+
lazy array (which may not survive spawn). Preference order:
|
|
561
|
+
1. Original input_data if it's a str/Path/list of paths.
|
|
562
|
+
2. input_arr.filenames for mbo lazy arrays backed by files.
|
|
563
|
+
Raises ValueError when neither is available.
|
|
564
|
+
"""
|
|
565
|
+
if isinstance(input_data, (str, Path)):
|
|
566
|
+
return str(input_data)
|
|
567
|
+
if isinstance(input_data, (list, tuple)) and all(
|
|
568
|
+
isinstance(p, (str, Path)) for p in input_data
|
|
569
|
+
):
|
|
570
|
+
return [str(p) for p in input_data]
|
|
571
|
+
if input_arr is not None and hasattr(input_arr, "filenames"):
|
|
572
|
+
fns = list(input_arr.filenames) if input_arr.filenames else []
|
|
573
|
+
if fns:
|
|
574
|
+
return [str(p) for p in fns]
|
|
575
|
+
raise ValueError(
|
|
576
|
+
"parallel mode (workers != 1) requires a path-based input. "
|
|
577
|
+
"Pass a file path, list of paths, or a lazy array loaded from disk, "
|
|
578
|
+
"or set workers=1 to process in-memory arrays sequentially."
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _resolve_worker_count(workers, num_planes):
|
|
583
|
+
"""Resolve `workers` kwarg into a concrete worker count.
|
|
584
|
+
|
|
585
|
+
workers=1: sequential (caller short-circuits before calling this).
|
|
586
|
+
workers in (None, <=0): auto = min(num_planes, cpu_count//2, 8).
|
|
587
|
+
workers>1: clamp to num_planes.
|
|
588
|
+
"""
|
|
589
|
+
if workers is None or (isinstance(workers, int) and workers <= 0):
|
|
590
|
+
cpu = os.cpu_count() or 1
|
|
591
|
+
return max(1, min(num_planes, cpu // 2 or 1, 8))
|
|
592
|
+
return max(1, min(int(workers), num_planes))
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class _QueueStreamRedirect:
|
|
596
|
+
"""File-like that forwards writes to a logger so print()/tqdm output
|
|
597
|
+
from a worker process flows through the multiprocessing log queue."""
|
|
598
|
+
|
|
599
|
+
def __init__(self, logger_, level=logging.INFO):
|
|
600
|
+
self._logger = logger_
|
|
601
|
+
self._level = level
|
|
602
|
+
self._buf = ""
|
|
603
|
+
|
|
604
|
+
def write(self, msg):
|
|
605
|
+
if not msg:
|
|
606
|
+
return
|
|
607
|
+
self._buf += msg
|
|
608
|
+
while "\n" in self._buf:
|
|
609
|
+
line, _, self._buf = self._buf.partition("\n")
|
|
610
|
+
if line:
|
|
611
|
+
self._logger.log(self._level, line)
|
|
612
|
+
|
|
613
|
+
def flush(self):
|
|
614
|
+
if self._buf:
|
|
615
|
+
self._logger.log(self._level, self._buf)
|
|
616
|
+
self._buf = ""
|
|
617
|
+
|
|
618
|
+
def isatty(self):
|
|
619
|
+
return False
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _plane_worker(input_source, current_ops, save_path, run_plane_kwargs,
|
|
623
|
+
reader_kwargs, log_queue):
|
|
624
|
+
"""Process one zplane in a child process. Returns (plane_num, ops_path).
|
|
625
|
+
|
|
626
|
+
Module-level so it pickles for ProcessPoolExecutor. The fully-prepared
|
|
627
|
+
`current_ops` is built in the main process; this worker only re-opens
|
|
628
|
+
the input via imread() and calls run_plane().
|
|
629
|
+
"""
|
|
630
|
+
plane_num = current_ops.get("plane")
|
|
631
|
+
|
|
632
|
+
root = logging.getLogger()
|
|
633
|
+
root.handlers.clear()
|
|
634
|
+
root.addHandler(logging.handlers.QueueHandler(log_queue))
|
|
635
|
+
root.setLevel(logging.INFO)
|
|
636
|
+
# suite2p / cellpose loggers set propagate=False, so route them directly.
|
|
637
|
+
for name in ("suite2p", "cellpose"):
|
|
638
|
+
lg = logging.getLogger(name)
|
|
639
|
+
lg.handlers.clear()
|
|
640
|
+
lg.addHandler(logging.handlers.QueueHandler(log_queue))
|
|
641
|
+
lg.setLevel(logging.INFO)
|
|
642
|
+
lg.propagate = False
|
|
643
|
+
global _external_logging_attached
|
|
644
|
+
_external_logging_attached = True
|
|
645
|
+
|
|
646
|
+
plane_logger = logging.getLogger(f"plane{plane_num:02d}")
|
|
647
|
+
plane_logger.setLevel(logging.INFO)
|
|
648
|
+
_orig_stdout, _orig_stderr = sys.stdout, sys.stderr
|
|
649
|
+
sys.stdout = _QueueStreamRedirect(plane_logger, logging.INFO)
|
|
650
|
+
sys.stderr = _QueueStreamRedirect(plane_logger, logging.INFO)
|
|
651
|
+
|
|
652
|
+
try:
|
|
653
|
+
from mbo_utilities import imread
|
|
654
|
+
arr = imread(input_source, **(reader_kwargs or {}))
|
|
655
|
+
ops_path = run_plane(
|
|
656
|
+
input_data=arr,
|
|
657
|
+
save_path=save_path,
|
|
658
|
+
ops=current_ops,
|
|
659
|
+
**run_plane_kwargs,
|
|
660
|
+
)
|
|
661
|
+
return plane_num, ops_path
|
|
662
|
+
finally:
|
|
663
|
+
try:
|
|
664
|
+
sys.stdout.flush()
|
|
665
|
+
sys.stderr.flush()
|
|
666
|
+
except Exception:
|
|
667
|
+
pass
|
|
668
|
+
sys.stdout, sys.stderr = _orig_stdout, _orig_stderr
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _prepare_plane_ops(*, base_ops, plane_idx, num_planes, input_arr,
|
|
672
|
+
volume_voxel, volume_source_meta, volume_source_shape,
|
|
673
|
+
z_selection, frame_indices):
|
|
674
|
+
"""Build a per-plane ops dict from base ops.
|
|
675
|
+
|
|
676
|
+
Centralizes the per-plane prep so both sequential and parallel paths
|
|
677
|
+
apply identical voxel-size propagation and reactive metadata. Mirrors
|
|
678
|
+
the inline prep that previously lived in run_volume's loop.
|
|
679
|
+
"""
|
|
680
|
+
plane_num = plane_idx + 1
|
|
681
|
+
current_ops = copy.deepcopy(load_ops(base_ops)) if base_ops else default_ops()
|
|
682
|
+
current_ops["plane"] = plane_num
|
|
683
|
+
current_ops["num_zplanes"] = num_planes
|
|
684
|
+
|
|
685
|
+
if input_arr is not None and hasattr(input_arr, "metadata"):
|
|
686
|
+
_source_idx = (input_arr.metadata or {}).get("selected_planes_0based")
|
|
687
|
+
if _source_idx is not None and plane_idx < len(_source_idx):
|
|
688
|
+
current_ops["source_plane_num"] = int(_source_idx[plane_idx]) + 1
|
|
689
|
+
|
|
690
|
+
if volume_voxel is not None:
|
|
691
|
+
if volume_voxel.dz is not None:
|
|
692
|
+
current_ops.setdefault("dz", volume_voxel.dz)
|
|
693
|
+
current_ops.setdefault("z_step", volume_voxel.dz)
|
|
694
|
+
if volume_voxel.dx != 1.0:
|
|
695
|
+
current_ops.setdefault("dx", volume_voxel.dx)
|
|
696
|
+
if volume_voxel.dy != 1.0:
|
|
697
|
+
current_ops.setdefault("dy", volume_voxel.dy)
|
|
698
|
+
|
|
699
|
+
_apply_reactive_metadata(
|
|
700
|
+
ops=current_ops,
|
|
701
|
+
source_metadata=volume_source_meta,
|
|
702
|
+
source_shape=volume_source_shape,
|
|
703
|
+
frame_indices=frame_indices,
|
|
704
|
+
plane_indices=z_selection,
|
|
705
|
+
logger=logger,
|
|
706
|
+
)
|
|
707
|
+
return current_ops
|
|
708
|
+
|
|
709
|
+
|
|
526
710
|
def pipeline(
|
|
527
711
|
input_data,
|
|
528
712
|
save_path: str | Path = None,
|
|
@@ -547,6 +731,9 @@ def pipeline(
|
|
|
547
731
|
save_json: bool = False,
|
|
548
732
|
reader_kwargs: dict = None,
|
|
549
733
|
writer_kwargs: dict = None,
|
|
734
|
+
workers: int | None = 1,
|
|
735
|
+
skip_volumetric: bool = False,
|
|
736
|
+
threads_per_worker: int | None = None,
|
|
550
737
|
# deprecated parameters
|
|
551
738
|
roi: int = None,
|
|
552
739
|
num_frames: int = None,
|
|
@@ -630,6 +817,19 @@ def pipeline(
|
|
|
630
817
|
Arguments passed to mbo_utilities.imread().
|
|
631
818
|
writer_kwargs : dict, optional
|
|
632
819
|
Arguments passed to binary writer (e.g., output_format).
|
|
820
|
+
workers : int or None, default 1
|
|
821
|
+
Number of zplane worker processes for volumetric input. ``1``
|
|
822
|
+
keeps the sequential code path. ``None`` (or any value <= 0)
|
|
823
|
+
auto-picks ``min(num_planes, cpu_count // 2, 8)``. Parallel mode
|
|
824
|
+
requires a path-based input; per-plane outputs go to disjoint
|
|
825
|
+
subdirectories. Cellpose on GPU may OOM with multiple workers —
|
|
826
|
+
reduce ``workers`` or switch cellpose to CPU when this happens.
|
|
827
|
+
Ignored for planar (single-plane) inputs.
|
|
828
|
+
skip_volumetric : bool, default False
|
|
829
|
+
When True, return per-plane ops_files immediately after the
|
|
830
|
+
per-plane loop, skipping merge_mrois, volume_stats, and
|
|
831
|
+
volumetric plots. Useful when farming planes across machines
|
|
832
|
+
and aggregating later.
|
|
633
833
|
plane_name : str, optional
|
|
634
834
|
Name for output directory when input is an array without
|
|
635
835
|
filenames. Passed via kwargs.
|
|
@@ -667,6 +867,8 @@ def pipeline(
|
|
|
667
867
|
|
|
668
868
|
_attach_external_loggers()
|
|
669
869
|
|
|
870
|
+
_apply_thread_limits(threads_per_worker)
|
|
871
|
+
|
|
670
872
|
# 1. Handle Deprecations
|
|
671
873
|
if roi is not None:
|
|
672
874
|
import warnings
|
|
@@ -747,9 +949,17 @@ def pipeline(
|
|
|
747
949
|
save_json=save_json,
|
|
748
950
|
reader_kwargs=reader_kwargs,
|
|
749
951
|
writer_kwargs=writer_kwargs,
|
|
952
|
+
workers=workers,
|
|
953
|
+
skip_volumetric=skip_volumetric,
|
|
954
|
+
threads_per_worker=threads_per_worker,
|
|
750
955
|
**kwargs,
|
|
751
956
|
)
|
|
752
957
|
else:
|
|
958
|
+
if workers != 1:
|
|
959
|
+
logger.warning(
|
|
960
|
+
"workers=%r ignored: input is planar (single zplane), no parallelism applicable",
|
|
961
|
+
workers,
|
|
962
|
+
)
|
|
753
963
|
# run_plane is planar-only — extract just the planar sub-dict
|
|
754
964
|
planar_kwargs = _extract_planar_rastermap_kwargs(rastermap_kwargs)
|
|
755
965
|
# run_plane returns a single Path, we wrap in list
|
|
@@ -947,6 +1157,9 @@ def run_volume(
|
|
|
947
1157
|
save_json: bool = False,
|
|
948
1158
|
reader_kwargs: dict = None,
|
|
949
1159
|
writer_kwargs: dict = None,
|
|
1160
|
+
workers: int | None = 1,
|
|
1161
|
+
skip_volumetric: bool = False,
|
|
1162
|
+
threads_per_worker: int | None = None,
|
|
950
1163
|
**kwargs,
|
|
951
1164
|
):
|
|
952
1165
|
"""
|
|
@@ -990,6 +1203,19 @@ def run_volume(
|
|
|
990
1203
|
See pipeline() for the full schema.
|
|
991
1204
|
save_json : bool, default False
|
|
992
1205
|
Save ops as JSON.
|
|
1206
|
+
workers : int or None, default 1
|
|
1207
|
+
Number of zplane worker processes. ``1`` (default) keeps the
|
|
1208
|
+
sequential path. ``None`` or any value <= 0 auto-picks
|
|
1209
|
+
``min(num_planes, cpu_count // 2, 8)``. Values > 1 run that many
|
|
1210
|
+
workers (clamped to num_planes). Parallel mode requires a
|
|
1211
|
+
path-based input. Workers reload input via ``imread`` and write
|
|
1212
|
+
to disjoint per-plane subdirectories.
|
|
1213
|
+
Caveat: cellpose on GPU may OOM with multiple workers; reduce
|
|
1214
|
+
``workers`` or switch cellpose to CPU when this happens.
|
|
1215
|
+
skip_volumetric : bool, default False
|
|
1216
|
+
When True, return per-plane ops_files immediately after the
|
|
1217
|
+
per-plane loop, skipping merge_mrois, volume_stats, and all
|
|
1218
|
+
volumetric plots. Useful when farming planes across machines.
|
|
993
1219
|
**kwargs
|
|
994
1220
|
Additional args passed to run_plane.
|
|
995
1221
|
|
|
@@ -1002,6 +1228,8 @@ def run_volume(
|
|
|
1002
1228
|
from mbo_utilities import imread
|
|
1003
1229
|
from lbm_suite2p_python.merging import merge_mrois
|
|
1004
1230
|
|
|
1231
|
+
_apply_thread_limits(threads_per_worker)
|
|
1232
|
+
|
|
1005
1233
|
# Handle input data
|
|
1006
1234
|
input_arr = None
|
|
1007
1235
|
input_paths = []
|
|
@@ -1072,6 +1300,7 @@ def run_volume(
|
|
|
1072
1300
|
else None
|
|
1073
1301
|
)
|
|
1074
1302
|
|
|
1303
|
+
_volume_start = time.time()
|
|
1075
1304
|
print(
|
|
1076
1305
|
f"Processing {len(planes_indices)} planes in volume (Total planes: {num_planes})"
|
|
1077
1306
|
)
|
|
@@ -1087,117 +1316,206 @@ def run_volume(
|
|
|
1087
1316
|
|
|
1088
1317
|
ops_files = []
|
|
1089
1318
|
|
|
1090
|
-
#
|
|
1091
|
-
|
|
1092
|
-
|
|
1319
|
+
# shared run_plane kwargs (identical for sequential and parallel paths)
|
|
1320
|
+
_run_plane_kwargs = dict(
|
|
1321
|
+
keep_reg=keep_reg,
|
|
1322
|
+
keep_raw=keep_raw,
|
|
1323
|
+
force_reg=force_reg,
|
|
1324
|
+
force_detect=force_detect,
|
|
1325
|
+
frame_indices=frame_indices,
|
|
1326
|
+
dff_window_size=dff_window_size,
|
|
1327
|
+
dff_percentile=dff_percentile,
|
|
1328
|
+
dff_smooth_window=dff_smooth_window,
|
|
1329
|
+
correct_neuropil=correct_neuropil,
|
|
1330
|
+
accept_all_cells=accept_all_cells,
|
|
1331
|
+
cell_filters=cell_filters,
|
|
1332
|
+
rastermap_kwargs=planar_rastermap_kwargs,
|
|
1333
|
+
save_json=save_json,
|
|
1334
|
+
reader_kwargs=reader_kwargs,
|
|
1335
|
+
writer_kwargs=writer_kwargs,
|
|
1336
|
+
**kwargs,
|
|
1337
|
+
)
|
|
1093
1338
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
total_planes=len(planes_indices),
|
|
1098
|
-
step="plane_start",
|
|
1099
|
-
message=f"Plane {plane_num}",
|
|
1100
|
-
)
|
|
1339
|
+
run_parallel = workers != 1
|
|
1340
|
+
if run_parallel:
|
|
1341
|
+
n_workers = _resolve_worker_count(workers, len(planes_indices))
|
|
1101
1342
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1343
|
+
if not run_parallel:
|
|
1344
|
+
# Iterate sequentially (original behavior)
|
|
1345
|
+
for i, plane_idx in enumerate(planes_indices):
|
|
1346
|
+
plane_num = plane_idx + 1
|
|
1347
|
+
|
|
1348
|
+
if progress_callback:
|
|
1349
|
+
progress_callback(
|
|
1350
|
+
plane=i,
|
|
1351
|
+
total_planes=len(planes_indices),
|
|
1352
|
+
step="plane_start",
|
|
1353
|
+
message=f"Plane {plane_num}",
|
|
1354
|
+
)
|
|
1355
|
+
|
|
1356
|
+
# Prepare input for run_plane
|
|
1357
|
+
if input_arr is not None:
|
|
1358
|
+
# Pass the whole array, run_plane handles extraction via ops['plane']
|
|
1359
|
+
current_input = input_arr
|
|
1111
1360
|
else:
|
|
1112
|
-
#
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
current_ops["source_plane_num"] = int(_source_idx[plane_idx]) + 1
|
|
1132
|
-
|
|
1133
|
-
# Propagate voxel size from volume metadata into per-plane ops
|
|
1134
|
-
if _volume_voxel is not None:
|
|
1135
|
-
if _volume_voxel.dz is not None:
|
|
1136
|
-
current_ops.setdefault("dz", _volume_voxel.dz)
|
|
1137
|
-
current_ops.setdefault("z_step", _volume_voxel.dz)
|
|
1138
|
-
if _volume_voxel.dx != 1.0:
|
|
1139
|
-
current_ops.setdefault("dx", _volume_voxel.dx)
|
|
1140
|
-
if _volume_voxel.dy != 1.0:
|
|
1141
|
-
current_ops.setdefault("dy", _volume_voxel.dy)
|
|
1142
|
-
|
|
1143
|
-
# Reactively scale dz (and fs if frame_indices are provided)
|
|
1144
|
-
# using the FULL plane selection. This MUST happen before
|
|
1145
|
-
# run_plane fires, because run_plane sees only its own plane
|
|
1146
|
-
# number and can't compute the implicit z-stride on its own.
|
|
1147
|
-
# The helper overwrites dz/dx/dy/fs in current_ops so the
|
|
1148
|
-
# `setdefault` calls above are effectively replaced when the
|
|
1149
|
-
# reactive value differs.
|
|
1150
|
-
_apply_reactive_metadata(
|
|
1151
|
-
ops=current_ops,
|
|
1152
|
-
source_metadata=_volume_source_meta,
|
|
1153
|
-
source_shape=_volume_source_shape,
|
|
1154
|
-
frame_indices=frame_indices,
|
|
1155
|
-
plane_indices=_volume_z_selection,
|
|
1156
|
-
logger=logger,
|
|
1157
|
-
)
|
|
1361
|
+
# List of files - map plane_num to file index (assuming 1-to-1 if no explicit mapping)
|
|
1362
|
+
# If input_paths corresponds to ALL planes, then plane_idx indexes into it
|
|
1363
|
+
if plane_idx < len(input_paths):
|
|
1364
|
+
current_input = input_paths[plane_idx]
|
|
1365
|
+
else:
|
|
1366
|
+
# Fallback or error? Assuming input_files length matches num_planes
|
|
1367
|
+
current_input = input_paths[0] # Should not happen if logic is correct
|
|
1368
|
+
|
|
1369
|
+
current_ops = _prepare_plane_ops(
|
|
1370
|
+
base_ops=ops,
|
|
1371
|
+
plane_idx=plane_idx,
|
|
1372
|
+
num_planes=num_planes,
|
|
1373
|
+
input_arr=input_arr,
|
|
1374
|
+
volume_voxel=_volume_voxel,
|
|
1375
|
+
volume_source_meta=_volume_source_meta,
|
|
1376
|
+
volume_source_shape=_volume_source_shape,
|
|
1377
|
+
z_selection=_volume_z_selection,
|
|
1378
|
+
frame_indices=frame_indices,
|
|
1379
|
+
)
|
|
1158
1380
|
|
|
1159
|
-
|
|
1381
|
+
# Call run_plane
|
|
1382
|
+
try:
|
|
1383
|
+
print(f"\n--- Volume Step: Plane {plane_num} ---")
|
|
1384
|
+
_plane_start = time.time()
|
|
1385
|
+
ops_file = run_plane(
|
|
1386
|
+
input_data=current_input,
|
|
1387
|
+
save_path=save_path,
|
|
1388
|
+
ops=current_ops,
|
|
1389
|
+
**_run_plane_kwargs,
|
|
1390
|
+
)
|
|
1391
|
+
ops_files.append(ops_file)
|
|
1392
|
+
print(
|
|
1393
|
+
f"--- Plane {plane_num} elapsed: {time.time() - _plane_start:.1f}s ---"
|
|
1394
|
+
)
|
|
1395
|
+
|
|
1396
|
+
if progress_callback:
|
|
1397
|
+
progress_callback(
|
|
1398
|
+
plane=i,
|
|
1399
|
+
total_planes=len(planes_indices),
|
|
1400
|
+
step="plane_done",
|
|
1401
|
+
message=f"Plane {plane_num} complete",
|
|
1402
|
+
)
|
|
1403
|
+
except Exception as e:
|
|
1404
|
+
print(f"ERROR processing plane {plane_num}: {e}")
|
|
1405
|
+
traceback.print_exc()
|
|
1406
|
+
else:
|
|
1407
|
+
# Parallel: resolve a picklable input source, pre-build all per-plane
|
|
1408
|
+
# ops in the main process, then run planes in a ProcessPoolExecutor.
|
|
1160
1409
|
try:
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1410
|
+
input_source = _resolve_input_source(input_arr, input_data)
|
|
1411
|
+
except ValueError:
|
|
1412
|
+
raise
|
|
1413
|
+
|
|
1414
|
+
prepared = [] # list of (plane_num, current_ops)
|
|
1415
|
+
for plane_idx in planes_indices:
|
|
1416
|
+
current_ops = _prepare_plane_ops(
|
|
1417
|
+
base_ops=ops,
|
|
1418
|
+
plane_idx=plane_idx,
|
|
1419
|
+
num_planes=num_planes,
|
|
1420
|
+
input_arr=input_arr,
|
|
1421
|
+
volume_voxel=_volume_voxel,
|
|
1422
|
+
volume_source_meta=_volume_source_meta,
|
|
1423
|
+
volume_source_shape=_volume_source_shape,
|
|
1424
|
+
z_selection=_volume_z_selection,
|
|
1170
1425
|
frame_indices=frame_indices,
|
|
1171
|
-
dff_window_size=dff_window_size,
|
|
1172
|
-
dff_percentile=dff_percentile,
|
|
1173
|
-
dff_smooth_window=dff_smooth_window,
|
|
1174
|
-
correct_neuropil=correct_neuropil,
|
|
1175
|
-
accept_all_cells=accept_all_cells,
|
|
1176
|
-
cell_filters=cell_filters,
|
|
1177
|
-
rastermap_kwargs=planar_rastermap_kwargs,
|
|
1178
|
-
save_json=save_json,
|
|
1179
|
-
reader_kwargs=reader_kwargs,
|
|
1180
|
-
writer_kwargs=writer_kwargs,
|
|
1181
|
-
**kwargs,
|
|
1182
1426
|
)
|
|
1183
|
-
|
|
1427
|
+
prepared.append((plane_idx + 1, current_ops))
|
|
1184
1428
|
|
|
1185
|
-
|
|
1429
|
+
# Eager picklability check on the first plane's ops so users see
|
|
1430
|
+
# a clear error at submission rather than a swallowed future.
|
|
1431
|
+
try:
|
|
1432
|
+
pickle.dumps(prepared[0][1])
|
|
1433
|
+
pickle.dumps(_run_plane_kwargs)
|
|
1434
|
+
except Exception as exc:
|
|
1435
|
+
raise RuntimeError(
|
|
1436
|
+
f"parallel mode cannot pickle the prepared per-plane ops or "
|
|
1437
|
+
f"run_plane kwargs ({exc!r}). Use workers=1 or remove the "
|
|
1438
|
+
f"unpicklable item."
|
|
1439
|
+
) from exc
|
|
1440
|
+
|
|
1441
|
+
print(
|
|
1442
|
+
f"Running {len(planes_indices)} planes across {n_workers} worker process(es)..."
|
|
1443
|
+
)
|
|
1444
|
+
|
|
1445
|
+
manager = mp.Manager()
|
|
1446
|
+
log_queue = manager.Queue()
|
|
1447
|
+
stdout_handler = logging.StreamHandler()
|
|
1448
|
+
stdout_handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
|
|
1449
|
+
listener = logging.handlers.QueueListener(
|
|
1450
|
+
log_queue, stdout_handler, respect_handler_level=False
|
|
1451
|
+
)
|
|
1452
|
+
listener.start()
|
|
1453
|
+
|
|
1454
|
+
if progress_callback:
|
|
1455
|
+
for i, (plane_num, _) in enumerate(prepared):
|
|
1186
1456
|
progress_callback(
|
|
1187
1457
|
plane=i,
|
|
1188
1458
|
total_planes=len(planes_indices),
|
|
1189
|
-
step="
|
|
1190
|
-
message=f"Plane {plane_num}
|
|
1459
|
+
step="plane_start",
|
|
1460
|
+
message=f"Plane {plane_num}",
|
|
1191
1461
|
)
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1462
|
+
|
|
1463
|
+
try:
|
|
1464
|
+
with ProcessPoolExecutor(max_workers=n_workers) as pool:
|
|
1465
|
+
future_to_plane = {}
|
|
1466
|
+
plane_start_times = {}
|
|
1467
|
+
for plane_num, current_ops in prepared:
|
|
1468
|
+
fut = pool.submit(
|
|
1469
|
+
_plane_worker,
|
|
1470
|
+
input_source,
|
|
1471
|
+
current_ops,
|
|
1472
|
+
save_path,
|
|
1473
|
+
_run_plane_kwargs,
|
|
1474
|
+
reader_kwargs,
|
|
1475
|
+
log_queue,
|
|
1476
|
+
)
|
|
1477
|
+
future_to_plane[fut] = plane_num
|
|
1478
|
+
plane_start_times[plane_num] = time.time()
|
|
1479
|
+
|
|
1480
|
+
done_count = 0
|
|
1481
|
+
for fut in as_completed(future_to_plane):
|
|
1482
|
+
plane_num = future_to_plane[fut]
|
|
1483
|
+
try:
|
|
1484
|
+
result_plane_num, ops_file = fut.result()
|
|
1485
|
+
ops_files.append((result_plane_num, ops_file))
|
|
1486
|
+
done_count += 1
|
|
1487
|
+
print(
|
|
1488
|
+
f"--- Plane {result_plane_num} elapsed: "
|
|
1489
|
+
f"{time.time() - plane_start_times[plane_num]:.1f}s ---"
|
|
1490
|
+
)
|
|
1491
|
+
if progress_callback:
|
|
1492
|
+
progress_callback(
|
|
1493
|
+
plane=done_count - 1,
|
|
1494
|
+
total_planes=len(planes_indices),
|
|
1495
|
+
step="plane_done",
|
|
1496
|
+
message=f"Plane {result_plane_num} complete",
|
|
1497
|
+
)
|
|
1498
|
+
except Exception as e:
|
|
1499
|
+
print(f"ERROR processing plane {plane_num}: {e}")
|
|
1500
|
+
traceback.print_exc()
|
|
1501
|
+
finally:
|
|
1502
|
+
listener.stop()
|
|
1503
|
+
|
|
1504
|
+
# Sort results back into z-order — volumetric stats/plots rely on
|
|
1505
|
+
# list order matching plane order.
|
|
1506
|
+
ops_files = [p for _, p in sorted(ops_files, key=lambda t: t[0])]
|
|
1195
1507
|
|
|
1196
1508
|
if not ops_files:
|
|
1197
1509
|
raise RuntimeError(
|
|
1198
1510
|
"run_volume failed: All planes resulted in exceptions during processing."
|
|
1199
1511
|
)
|
|
1200
1512
|
|
|
1513
|
+
if skip_volumetric:
|
|
1514
|
+
logger.info(
|
|
1515
|
+
"skip_volumetric=True; returning per-plane ops_files without aggregation"
|
|
1516
|
+
)
|
|
1517
|
+
return ops_files
|
|
1518
|
+
|
|
1201
1519
|
# Post-Loop: Merging and Volume Stats
|
|
1202
1520
|
|
|
1203
1521
|
# Check for multi-ROI merging using metadata (not filename heuristics)
|
|
@@ -1280,6 +1598,13 @@ def run_volume(
|
|
|
1280
1598
|
print(f"Warning: Volume statistics failed: {e}")
|
|
1281
1599
|
traceback.print_exc()
|
|
1282
1600
|
|
|
1601
|
+
_volume_elapsed = time.time() - _volume_start
|
|
1602
|
+
_h, _rem = divmod(int(_volume_elapsed), 3600)
|
|
1603
|
+
_m, _s = divmod(_rem, 60)
|
|
1604
|
+
print(
|
|
1605
|
+
f"\nVolume total elapsed: {_h:02d}:{_m:02d}:{_s:02d} "
|
|
1606
|
+
f"({_volume_elapsed:.1f}s) across {len(ops_files)} plane(s)"
|
|
1607
|
+
)
|
|
1283
1608
|
return ops_files
|
|
1284
1609
|
|
|
1285
1610
|
|
|
@@ -1388,13 +1713,25 @@ def _should_register(ops_path: str | Path) -> bool:
|
|
|
1388
1713
|
"""
|
|
1389
1714
|
Determine whether Suite2p registration still needs to be performed.
|
|
1390
1715
|
|
|
1391
|
-
Registration is considered complete if any of the following hold:
|
|
1392
|
-
- A reference image (refImg) exists and is a valid ndarray
|
|
1393
|
-
- meanImg exists (Suite2p always produces it post-registration)
|
|
1394
|
-
- Valid registration offsets (xoff/yoff) are present
|
|
1395
|
-
|
|
1396
1716
|
Returns True if registration *should* be run, False otherwise.
|
|
1717
|
+
|
|
1718
|
+
Decision rule:
|
|
1719
|
+
1. If the plane's data.bin (suite2p's registered output) is missing,
|
|
1720
|
+
registration has not run yet on this plane — return True
|
|
1721
|
+
unconditionally. ops.npy field state is not trusted here because
|
|
1722
|
+
write_ops merges the source array's metadata into a freshly-written
|
|
1723
|
+
ops.npy, and source metadata can carry suite2p-output-shaped keys
|
|
1724
|
+
(e.g. when imread of a directory pulls in a sibling ops.npy) that
|
|
1725
|
+
falsely advertise "registration done" on what is actually raw data.
|
|
1726
|
+
2. With data.bin present, fall back to ops.npy field inspection
|
|
1727
|
+
(refImg / meanImg / xoff / yoff / regDX / regPC) to decide whether
|
|
1728
|
+
the previous registration is complete enough to reuse.
|
|
1397
1729
|
"""
|
|
1730
|
+
ops_path = Path(ops_path)
|
|
1731
|
+
reg_bin = ops_path.parent / "data.bin"
|
|
1732
|
+
if not reg_bin.exists():
|
|
1733
|
+
return True
|
|
1734
|
+
|
|
1398
1735
|
ops = load_ops(ops_path)
|
|
1399
1736
|
|
|
1400
1737
|
has_ref = isinstance(ops.get("refImg"), np.ndarray)
|
|
@@ -1411,7 +1748,10 @@ def _should_register(ops_path: str | Path) -> bool:
|
|
|
1411
1748
|
return False
|
|
1412
1749
|
|
|
1413
1750
|
has_offsets = _has_valid_offsets("xoff") or _has_valid_offsets("yoff")
|
|
1414
|
-
has_metrics = any(
|
|
1751
|
+
has_metrics = any(
|
|
1752
|
+
isinstance(ops.get(k), np.ndarray) and ops[k].size > 0
|
|
1753
|
+
for k in ("regDX", "regPC", "regPC1", "regDX1")
|
|
1754
|
+
)
|
|
1415
1755
|
|
|
1416
1756
|
# registration done if any of these are true
|
|
1417
1757
|
registration_done = has_ref or has_mean or has_offsets or has_metrics
|
|
@@ -1456,7 +1796,7 @@ def run_plane_bin(ops) -> bool:
|
|
|
1456
1796
|
|
|
1457
1797
|
# resolve path keys. rewrite stale absolutes (ops was moved) but
|
|
1458
1798
|
# preserve an already-valid absolute path (staging points raw_file/
|
|
1459
|
-
# reg_file/chan2_file at the source dir —
|
|
1799
|
+
# reg_file/chan2_file at the source dir — overwriting those would
|
|
1460
1800
|
# break the link).
|
|
1461
1801
|
ops["save_path"] = str(ops_parent)
|
|
1462
1802
|
ops["ops_path"] = str(ops_parent / "ops.npy")
|
|
@@ -1609,7 +1949,7 @@ def run_plane_bin(ops) -> bool:
|
|
|
1609
1949
|
reg_file_chan2 = ops_parent / "data_chan2_reg.bin" if use_chan2 else None
|
|
1610
1950
|
|
|
1611
1951
|
# NOTE: previous versions hard-coded `ops["anatomical_red"] = False`
|
|
1612
|
-
# and `ops["chan2_thres"] = 0.1` here. Both were silently
|
|
1952
|
+
# and `ops["chan2_thres"] = 0.1` here. Both were silently overwriting
|
|
1613
1953
|
# the user's settings and the suite2p schema defaults
|
|
1614
1954
|
# (detection.chan2_threshold = 0.25). They've been removed — the
|
|
1615
1955
|
# user's chan2 threshold now flows through unchanged. If a downstream
|
|
@@ -2026,6 +2366,10 @@ def run_plane(
|
|
|
2026
2366
|
)
|
|
2027
2367
|
|
|
2028
2368
|
existing_ops = np.load(src_dir / "ops.npy", allow_pickle=True).item()
|
|
2369
|
+
# remember the original acquisition source before the merge below
|
|
2370
|
+
# overwrites data_path with the staged binary path. needed by
|
|
2371
|
+
# force_reg path when the source has no data_raw.bin.
|
|
2372
|
+
original_data_path = existing_ops.get("data_path")
|
|
2029
2373
|
metadata = {
|
|
2030
2374
|
k: v
|
|
2031
2375
|
for k, v in existing_ops.items()
|
|
@@ -2073,20 +2417,72 @@ def run_plane(
|
|
|
2073
2417
|
"data_path": str(input_path.resolve()),
|
|
2074
2418
|
}
|
|
2075
2419
|
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2420
|
+
if not force_reg:
|
|
2421
|
+
# registration writes to data.bin, but data.bin lives in the
|
|
2422
|
+
# source dir — running it would overwrite the user's source
|
|
2423
|
+
# binary. force detection-only; user can re-register from the
|
|
2424
|
+
# original tiff/zarr by passing force_reg=True (Force in the
|
|
2425
|
+
# GUI), which routes writes into the new plane_dir.
|
|
2426
|
+
if ops_user.get("do_registration", 1):
|
|
2427
|
+
logger.warning(
|
|
2428
|
+
"do_registration ignored: the source directory already "
|
|
2429
|
+
"contains a data.bin and running registration here would "
|
|
2430
|
+
"overwrite it. Forcing do_registration=0 (detection-only). "
|
|
2431
|
+
"Select Force in the Registration column (force_reg=True) "
|
|
2432
|
+
"to re-register from the original input into the new "
|
|
2433
|
+
"save_path."
|
|
2434
|
+
)
|
|
2435
|
+
ops["do_registration"] = 0
|
|
2088
2436
|
|
|
2089
2437
|
_stage_source_into_plane_dir(src_dir, plane_dir, ops)
|
|
2438
|
+
|
|
2439
|
+
if force_reg:
|
|
2440
|
+
# writes go to plane_dir/data.bin — discard the source pointer
|
|
2441
|
+
# that _stage_source_into_plane_dir set.
|
|
2442
|
+
ops.pop("reg_file", None)
|
|
2443
|
+
# existing_ops carries a `raw_file` path from the original
|
|
2444
|
+
# run; if keep_raw=False removed the file, the string is
|
|
2445
|
+
# still present but stale. existence-check, don't trust the
|
|
2446
|
+
# string alone.
|
|
2447
|
+
_raw = ops.get("raw_file")
|
|
2448
|
+
if not _raw or not Path(_raw).exists():
|
|
2449
|
+
import shutil
|
|
2450
|
+
target_raw = plane_dir / "data_raw.bin"
|
|
2451
|
+
# prefer the original acquisition (tiff/zarr) when it's
|
|
2452
|
+
# still on disk — avoids re-registering already-
|
|
2453
|
+
# registered data. Skip when it points at the same .bin
|
|
2454
|
+
# the user just loaded (no improvement, and imread of a
|
|
2455
|
+
# stale data_path could be wrong).
|
|
2456
|
+
use_original = (
|
|
2457
|
+
bool(original_data_path)
|
|
2458
|
+
and Path(original_data_path).exists()
|
|
2459
|
+
and Path(original_data_path).resolve() != input_path.resolve()
|
|
2460
|
+
)
|
|
2461
|
+
if use_original:
|
|
2462
|
+
print(
|
|
2463
|
+
f" Force registration: rewriting data_raw.bin from "
|
|
2464
|
+
f"{Path(original_data_path).name}"
|
|
2465
|
+
)
|
|
2466
|
+
file = imread(Path(original_data_path), **reader_kwargs)
|
|
2467
|
+
if hasattr(file, "metadata"):
|
|
2468
|
+
metadata = dict(file.metadata)
|
|
2469
|
+
else:
|
|
2470
|
+
metadata = get_metadata(Path(original_data_path))
|
|
2471
|
+
skip_imwrite = False
|
|
2472
|
+
else:
|
|
2473
|
+
# original acquisition is gone (or IS the input).
|
|
2474
|
+
# seed data_raw.bin from the staged data.bin so
|
|
2475
|
+
# registration has frames to work with. Re-registers
|
|
2476
|
+
# already-registered data (near-zero shifts) — fine
|
|
2477
|
+
# for detection / bad-frame re-runs.
|
|
2478
|
+
print(
|
|
2479
|
+
f" Force registration: original acquisition "
|
|
2480
|
+
f"unavailable; seeding data_raw.bin from "
|
|
2481
|
+
f"{input_path.name} (re-registering already-"
|
|
2482
|
+
f"registered data)"
|
|
2483
|
+
)
|
|
2484
|
+
shutil.copy2(input_path, target_raw)
|
|
2485
|
+
ops["raw_file"] = str(target_raw)
|
|
2090
2486
|
else:
|
|
2091
2487
|
skip_imwrite = False
|
|
2092
2488
|
|
|
@@ -2223,7 +2619,7 @@ def run_plane(
|
|
|
2223
2619
|
if src_fs is None and file is not None and hasattr(file, "metadata"):
|
|
2224
2620
|
src_fs = (file.metadata or {}).get("fs")
|
|
2225
2621
|
if src_fs is not None:
|
|
2226
|
-
# only override the lbm default — don't
|
|
2622
|
+
# only override the lbm default — don't overwrite a value the
|
|
2227
2623
|
# user explicitly set in their ops_user dict.
|
|
2228
2624
|
if ops.get("fs") in (None, 10.0) or "fs" not in ops_user:
|
|
2229
2625
|
ops["fs"] = float(src_fs)
|
|
@@ -2307,6 +2703,11 @@ def run_plane(
|
|
|
2307
2703
|
user_skip_detect = (not ops.get("roidetect", 1)) and not force_detect
|
|
2308
2704
|
|
|
2309
2705
|
stat_file = plane_dir / "stat.npy"
|
|
2706
|
+
# If we just wrote a fresh data_raw.bin in this run, any stat.npy /
|
|
2707
|
+
# ops.npy left in plane_dir from a prior run is stale and must not
|
|
2708
|
+
# short-circuit suite2p. fresh raw data needs registration and (unless
|
|
2709
|
+
# user-disabled) detection regardless of leftover artifacts.
|
|
2710
|
+
fresh_binary_written = (not skip_imwrite) and (file is not None) and should_write
|
|
2310
2711
|
|
|
2311
2712
|
if user_skip_reg and user_skip_detect:
|
|
2312
2713
|
needs_reg = False
|
|
@@ -2318,6 +2719,8 @@ def run_plane(
|
|
|
2318
2719
|
needs_detect = False
|
|
2319
2720
|
if force_detect:
|
|
2320
2721
|
needs_detect = True
|
|
2722
|
+
elif fresh_binary_written:
|
|
2723
|
+
needs_detect = not user_skip_detect
|
|
2321
2724
|
elif not stat_file.exists():
|
|
2322
2725
|
needs_detect = not user_skip_detect
|
|
2323
2726
|
elif ops.get("roidetect", 1):
|
|
@@ -2331,6 +2734,8 @@ def run_plane(
|
|
|
2331
2734
|
needs_reg = True
|
|
2332
2735
|
elif user_skip_reg:
|
|
2333
2736
|
needs_reg = False
|
|
2737
|
+
elif fresh_binary_written:
|
|
2738
|
+
needs_reg = True
|
|
2334
2739
|
elif not ops_file.exists():
|
|
2335
2740
|
needs_reg = True
|
|
2336
2741
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lbm_suite2p_python
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.6
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{lbm_suite2p_python-3.0.4 → lbm_suite2p_python-3.0.6}/lbm_suite2p_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|