lbm_suite2p_python 3.1.1__tar.gz → 3.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {lbm_suite2p_python-3.1.1/lbm_suite2p_python.egg-info → lbm_suite2p_python-3.2.1}/PKG-INFO +1 -1
  2. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/cellpose.py +6 -6
  3. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/cli.py +11 -1
  4. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/conversion.py +2 -2
  5. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/default_ops.py +96 -84
  6. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/run_lsp.py +161 -51
  7. lbm_suite2p_python-3.2.1/lbm_suite2p_python/utils.py +229 -0
  8. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1/lbm_suite2p_python.egg-info}/PKG-INFO +1 -1
  9. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/pyproject.toml +1 -1
  10. lbm_suite2p_python-3.1.1/lbm_suite2p_python/utils.py +0 -144
  11. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/LICENSE.md +0 -0
  12. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/MANIFEST.in +0 -0
  13. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/README.md +0 -0
  14. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/__init__.py +0 -0
  15. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/__main__.py +0 -0
  16. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/_benchmarking.py +0 -0
  17. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/db_settings.py +0 -0
  18. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/grid_search.py +0 -0
  19. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/gui.py +0 -0
  20. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/merging.py +0 -0
  21. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/postprocessing.py +0 -0
  22. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/volume.py +0 -0
  23. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python/zplane.py +0 -0
  24. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python.egg-info/SOURCES.txt +0 -0
  25. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python.egg-info/dependency_links.txt +0 -0
  26. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python.egg-info/entry_points.txt +0 -0
  27. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python.egg-info/requires.txt +0 -0
  28. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/lbm_suite2p_python.egg-info/top_level.txt +0 -0
  29. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/setup.cfg +0 -0
  30. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/tests/test_frame_count_aliases.py +0 -0
  31. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/tests/test_pipeline_parameters.py +0 -0
  32. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/tests/test_refactored_pipeline.py +0 -0
  33. {lbm_suite2p_python-3.1.1 → lbm_suite2p_python-3.2.1}/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.1.1
3
+ Version: 3.2.1
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
@@ -390,7 +390,7 @@ def cellpose(
390
390
  # cellpose eval parameters
391
391
  diameter: float = None,
392
392
  flow_threshold: float = 0.0,
393
- cellprob_threshold: float = -6.0,
393
+ cellprob_threshold: float = -4.0,
394
394
  min_size: int = 2,
395
395
  max_size: int = None,
396
396
  max_size_fraction: float = None,
@@ -445,9 +445,9 @@ def cellpose(
445
445
  Use GPU if available.
446
446
  diameter : float, optional
447
447
  Expected cell diameter in pixels. If None, Cellpose auto-estimates.
448
- flow_threshold : float, default 0.4
449
- Maximum allowed error of flows for each mask.
450
- cellprob_threshold : float, default 0.0
448
+ flow_threshold : float, default 0.0
449
+ Maximum allowed error of flows for each mask (0 = flow check off).
450
+ cellprob_threshold : float, default -4.0
451
451
  Probability threshold for cell detection. Lower = more cells.
452
452
  min_size : int, default 2
453
453
  Minimum number of pixels per mask.
@@ -932,8 +932,8 @@ def save_gui_results(
932
932
  flows: tuple = None,
933
933
  styles: np.ndarray = None,
934
934
  diameter: float = None,
935
- cellprob_threshold: float = 0.0,
936
- flow_threshold: float = 0.4,
935
+ cellprob_threshold: float = -4.0,
936
+ flow_threshold: float = 0.0,
937
937
  name: str = None,
938
938
  ) -> Path:
939
939
  """
@@ -134,13 +134,21 @@ Examples:
134
134
  "--planes", nargs="*", type=int, dest="planes",
135
135
  help="z-planes to process (1-indexed, e.g., --planes 1 2 3)"
136
136
  )
137
+ pipeline.add_argument(
138
+ "--timepoints", nargs="*", type=int, dest="timepoints",
139
+ help="timepoints to process (1-indexed, e.g., --timepoints 1 50 100)"
140
+ )
137
141
  pipeline.add_argument(
138
142
  "--roi-mode", "--roi", type=int, dest="roi_mode",
139
143
  help="ROI mode: None=stitch, 0=split all, N=specific ROI"
140
144
  )
141
145
  pipeline.add_argument(
142
146
  "--num-timepoints", "--frames", type=int, dest="num_timepoints",
143
- help="number of frames/timepoints to process (for quick testing)"
147
+ help="number of timepoints to process (first N, for quick testing)"
148
+ )
149
+ pipeline.add_argument(
150
+ "--num-zplanes", type=int, dest="num_zplanes",
151
+ help="number of z-planes to process (first N)"
144
152
  )
145
153
  pipeline.add_argument(
146
154
  "--overwrite", action="store_true",
@@ -566,8 +574,10 @@ def main():
566
574
  save_path=output_path,
567
575
  ops=ops,
568
576
  planes=args.planes,
577
+ timepoints=args.timepoints,
569
578
  roi_mode=args.roi_mode,
570
579
  num_timepoints=args.num_timepoints,
580
+ num_zplanes=args.num_zplanes,
571
581
  keep_reg=args.keep_reg,
572
582
  keep_raw=args.keep_raw,
573
583
  force_reg=args.force_reg or args.overwrite,
@@ -622,8 +622,8 @@ def export_for_gui(suite2p_dir, output_path=None, name=None):
622
622
  "filename": str(proj_path),
623
623
  "flows": None,
624
624
  "est_diam": ops.get("diameter"),
625
- "cellprob_threshold": ops.get("cellprob_threshold", 0.0),
626
- "flow_threshold": ops.get("flow_threshold", 0.4),
625
+ "cellprob_threshold": ops.get("cellprob_threshold", -4.0),
626
+ "flow_threshold": ops.get("flow_threshold", 0.0),
627
627
  }
628
628
 
629
629
  seg_file = output_dir / f"{name}_seg.npy"
@@ -1,84 +1,96 @@
1
- """Default ops for the LBM pipeline.
2
-
3
- Historically this module hard-coded a flat ops dict whose values
4
- diverged from suite2p's own defaults in a handful of fields
5
- (`batch_size=500`, `chan2_thres=0.65`, `tau=1.3`, `diameter=4`, etc.).
6
- That divergence made the round-trip through `settings.npy` confusing —
7
- on-disk values that matched the lsp default were flagged as "modified"
8
- against suite2p's schema, and vice versa.
9
-
10
- This module now exposes exactly suite2p's defaults (the values from
11
- `suite2p.default_settings()` + `suite2p.default_db()`), flattened to the
12
- fork's flat-ops shape via the same `db_settings_to_ops` translation
13
- that the rest of lsp uses. There is now ONE source of truth for "what
14
- default means": suite2p's parameter schema. Any LBM-specific tweaks
15
- (diameter, tau, etc.) belong in user code, not in the default.
16
- """
17
-
18
- from mbo_utilities.metadata import get_param, get_voxel_size
19
-
20
-
21
- def s2p_ops() -> dict:
22
- """Suite2p's default ops in flat (fork-style) form.
23
-
24
- Composed from `suite2p.default_settings()` + `suite2p.default_db()`
25
- via `db_settings_to_ops`, so the rename map and per-section
26
- flat-key disambiguation (e.g. extraction.batch_size
27
- extract_batch_size) are applied consistently.
28
- """
29
- from suite2p import default_settings, default_db
30
- from lbm_suite2p_python.db_settings import db_settings_to_ops
31
-
32
- return db_settings_to_ops(default_db(), default_settings())
33
-
34
-
35
- def default_ops(metadata: dict | None = None, ops: dict | None = None) -> dict:
36
- """Return default ops for the LBM Suite2p pipeline.
37
-
38
- Parameters
39
- ----------
40
- metadata : dict, optional
41
- Source-data metadata. When provided, `fs` and the (`dx`, `dy`)
42
- voxel-size pair are pulled in from the metadata and overlaid
43
- onto the suite2p defaults.
44
- ops : dict, optional
45
- A user-supplied ops dict to start from. When None, starts from
46
- `s2p_ops()` (suite2p's defaults).
47
-
48
- Returns
49
- -------
50
- dict
51
- Flat ops dict ready to be passed to `pipeline()` / `run_plane()`.
52
-
53
- Notes
54
- -----
55
- `nplanes=1` and `nchannels=1` are forced at the end. The lsp
56
- pipeline always processes one plane at a time, so these stay fixed
57
- regardless of caller input.
58
-
59
- Examples
60
- --------
61
- >>> import lbm_suite2p_python as lsp
62
- >>> ops = lsp.default_ops()
63
- >>> lsp.run_plane(
64
- ... ops=ops,
65
- ... input_tiff="D:/demo/raw_data/raw_file_00001.tif",
66
- ... save_path="D:/demo/results",
67
- ... save_folder="v1",
68
- ... )
69
- """
70
- if ops is None:
71
- ops = s2p_ops()
72
-
73
- if metadata is not None:
74
- fs = get_param(metadata, "fs")
75
- if fs is not None:
76
- ops["fs"] = fs
77
- voxel = get_voxel_size(metadata)
78
- if voxel.dx != 1.0 or voxel.dy != 1.0:
79
- ops["dx"] = voxel.dx
80
- ops["dy"] = voxel.dy
81
-
82
- ops["nplanes"] = 1
83
- ops["nchannels"] = 1
84
- return ops
1
+ """Default ops for the LBM pipeline.
2
+
3
+ Historically this module hard-coded a flat ops dict whose values
4
+ diverged from suite2p's own defaults in a handful of fields
5
+ (`batch_size=500`, `chan2_thres=0.65`, `tau=1.3`, `diameter=4`, etc.).
6
+ That divergence made the round-trip through `settings.npy` confusing —
7
+ on-disk values that matched the lsp default were flagged as "modified"
8
+ against suite2p's schema, and vice versa.
9
+
10
+ `s2p_ops()` exposes exactly suite2p's defaults (the values from
11
+ `suite2p.default_settings()` + `suite2p.default_db()`), flattened to the
12
+ fork's flat-ops shape via the same `db_settings_to_ops` translation
13
+ that the rest of lsp uses. `default_ops()` then applies a small set of
14
+ LBM detection defaults (`_LBM_DETECTION_DEFAULTS`) on top, so a notebook,
15
+ a bare `pipeline()` / `run_plane()` call, and the GUI all start from the
16
+ same place. An explicit caller-supplied `ops=` is respected, not overlaid.
17
+ """
18
+
19
+ from mbo_utilities.metadata import get_param, get_voxel_size
20
+
21
+
22
+ # LBM detection defaults that intentionally differ from suite2p's schema.
23
+ # default_ops() applies these on top of the suite2p mirror so notebook,
24
+ # function-call, and GUI runs share one set of defaults.
25
+ _LBM_DETECTION_DEFAULTS = {
26
+ "do_regmetrics": False, # PC reg-quality metrics: ~40s/plane on >=1500 frames
27
+ "cellprob_threshold": -4.0, # more permissive than suite2p's 0.0 for LBM data
28
+ "flow_threshold": 0.0, # flow check disabled
29
+ }
30
+
31
+
32
+ def s2p_ops() -> dict:
33
+ """Suite2p's default ops in flat (fork-style) form.
34
+
35
+ Composed from `suite2p.default_settings()` + `suite2p.default_db()`
36
+ via `db_settings_to_ops`, so the rename map and per-section
37
+ flat-key disambiguation (e.g. extraction.batch_size →
38
+ extract_batch_size) are applied consistently.
39
+ """
40
+ from suite2p import default_settings, default_db
41
+ from lbm_suite2p_python.db_settings import db_settings_to_ops
42
+
43
+ return db_settings_to_ops(default_db(), default_settings())
44
+
45
+
46
+ def default_ops(metadata: dict | None = None, ops: dict | None = None) -> dict:
47
+ """Return default ops for the LBM Suite2p pipeline.
48
+
49
+ Parameters
50
+ ----------
51
+ metadata : dict, optional
52
+ Source-data metadata. When provided, `fs` and the (`dx`, `dy`)
53
+ voxel-size pair are pulled in from the metadata and overlaid
54
+ onto the suite2p defaults.
55
+ ops : dict, optional
56
+ A user-supplied ops dict to start from. When None, starts from
57
+ `s2p_ops()` (suite2p's defaults).
58
+
59
+ Returns
60
+ -------
61
+ dict
62
+ Flat ops dict ready to be passed to `pipeline()` / `run_plane()`.
63
+
64
+ Notes
65
+ -----
66
+ `nplanes=1` and `nchannels=1` are forced at the end. The lsp
67
+ pipeline always processes one plane at a time, so these stay fixed
68
+ regardless of caller input.
69
+
70
+ Examples
71
+ --------
72
+ >>> import lbm_suite2p_python as lsp
73
+ >>> ops = lsp.default_ops()
74
+ >>> lsp.run_plane(
75
+ ... ops=ops,
76
+ ... input_tiff="D:/demo/raw_data/raw_file_00001.tif",
77
+ ... save_path="D:/demo/results",
78
+ ... save_folder="v1",
79
+ ... )
80
+ """
81
+ if ops is None:
82
+ ops = s2p_ops()
83
+ ops.update(_LBM_DETECTION_DEFAULTS)
84
+
85
+ if metadata is not None:
86
+ fs = get_param(metadata, "fs")
87
+ if fs is not None:
88
+ ops["fs"] = fs
89
+ voxel = get_voxel_size(metadata)
90
+ if voxel.dx != 1.0 or voxel.dy != 1.0:
91
+ ops["dx"] = voxel.dx
92
+ ops["dy"] = voxel.dy
93
+
94
+ ops["nplanes"] = 1
95
+ ops["nchannels"] = 1
96
+ return ops
@@ -446,14 +446,15 @@ def _is_valid_torch_checkpoint(path) -> bool:
446
446
  def _prewarm_cellpose_model(ops) -> None:
447
447
  """Download the cellpose model once, in the parent, before workers fan out.
448
448
 
449
- cellpose's cache_CPSAM_model_path downloads to a temp file then renames
450
- with no cross-process lock. Multiple workers hitting an empty cache at once
449
+ cellpose's model cache downloads to a temp file then renames with no
450
+ cross-process lock. Multiple workers hitting an empty cache at once
451
451
  race: one wins the rename, the rest fail (Windows WinError 32/183) or read a
452
452
  half-written file (PytorchStreamReader miniz error). Warming here serializes
453
453
  the download so workers only ever read a complete file. A corrupt leftover
454
454
  from a prior failed run is removed and re-downloaded.
455
455
  """
456
- if not (ops.get("roidetect", True) and ops.get("anatomical_only", 0) > 0):
456
+ if not (ops.get("roidetect", True)
457
+ and (ops.get("anatomical_only", 0) > 0 or ops.get("algorithm") == "cellpose")):
457
458
  return
458
459
  try:
459
460
  from cellpose import models as cp_models
@@ -467,7 +468,11 @@ def _prewarm_cellpose_model(ops) -> None:
467
468
  except OSError:
468
469
  pass
469
470
  try:
470
- cp_models.cache_CPSAM_model_path()
471
+ # cellpose 4.x renamed cache_CPSAM_model_path() -> cache_model_path(backbone)
472
+ if hasattr(cp_models, "cache_model_path"):
473
+ cp_models.cache_model_path("cpsam")
474
+ else:
475
+ cp_models.cache_CPSAM_model_path()
471
476
  except Exception as exc:
472
477
  print(
473
478
  f"Warning: could not pre-download cellpose model ({exc}); "
@@ -780,6 +785,38 @@ def _prepare_plane_ops(*, base_ops, plane_idx, num_planes, input_arr,
780
785
  return current_ops
781
786
 
782
787
 
788
+ def _resolve_timepoints(timepoints=None, frames=None, frame_indices=None):
789
+ """Resolve the canonical 1-based ``timepoints`` selection.
790
+
791
+ ``frames`` (1-based) and ``frame_indices`` (0-based) are deprecated
792
+ aliases and emit a DeprecationWarning. Returns a 1-based list, or
793
+ None for all timepoints.
794
+ """
795
+ import warnings
796
+
797
+ if frames is not None:
798
+ warnings.warn(
799
+ "'frames' is deprecated, use 'timepoints' (1-based)",
800
+ DeprecationWarning,
801
+ stacklevel=3,
802
+ )
803
+ if timepoints is None:
804
+ timepoints = frames
805
+ if frame_indices is not None:
806
+ warnings.warn(
807
+ "'frame_indices' is deprecated, use 'timepoints' (1-based)",
808
+ DeprecationWarning,
809
+ stacklevel=3,
810
+ )
811
+ if timepoints is None:
812
+ timepoints = [int(i) + 1 for i in frame_indices]
813
+ if timepoints is None:
814
+ return None
815
+ if isinstance(timepoints, (int, np.integer)):
816
+ return [int(timepoints)]
817
+ return [int(t) for t in timepoints]
818
+
819
+
783
820
  def pipeline(
784
821
  input_data,
785
822
  save_path: str | Path = None,
@@ -792,7 +829,10 @@ def pipeline(
792
829
  keep_raw: bool = False,
793
830
  force_reg: bool = False,
794
831
  force_detect: bool = False,
832
+ replot: bool = True,
833
+ timepoints: list | int | None = None,
795
834
  num_timepoints: int = None,
835
+ num_zplanes: int = None,
796
836
  frame_indices: list | None = None,
797
837
  dff_window_size: int = None,
798
838
  dff_percentile: int = 20,
@@ -854,16 +894,25 @@ def pipeline(
854
894
  Force re-registration even if already complete.
855
895
  force_detect : bool, default False
856
896
  Force ROI detection even if stat.npy exists.
857
- num_timepoints : int, optional
858
- Limit processing to first N frames (truncation only). For an
859
- explicit set of frames or a strided selection, use
860
- ``frame_indices`` instead.
861
- frame_indices : list[int], optional
862
- Explicit 0-based frame indices to process. Supports stride
863
- (e.g. ``list(range(0, 1574, 2))`` for every other frame).
897
+ replot : bool, default True
898
+ Regenerate per-plane figures. Set False to skip per-plane figure
899
+ regeneration (e.g. the volumetric aggregate over already-plotted
900
+ planes); suite2p and the volumetric plots are unaffected.
901
+ timepoints : list[int] or int, optional
902
+ Explicit 1-based timepoints to process. Supports stride
903
+ (e.g. ``list(range(1, 1575, 2))`` for every other timepoint).
864
904
  When provided, the implicit stride is used by `OutputMetadata`
865
905
  to reactively scale `fs` (e.g. stride of 2 → `fs / 2` in the
866
906
  output ops.npy). Takes precedence over ``num_timepoints``.
907
+ num_timepoints : int, optional
908
+ Limit processing to first N timepoints (truncation only). For an
909
+ explicit set or a strided selection, use ``timepoints`` instead.
910
+ num_zplanes : int, optional
911
+ Limit processing to the first N z-planes. Shortcut for
912
+ ``planes=[1..N]``; ignored when ``planes`` is given.
913
+ frame_indices : list[int], optional
914
+ Deprecated alias for ``timepoints`` (0-based). Emits a
915
+ DeprecationWarning.
867
916
  dff_window_size : int, optional
868
917
  Window size for rolling percentile dF/F baseline (frames).
869
918
  If None, auto-calculated as ~10 * tau * fs.
@@ -976,7 +1025,15 @@ def pipeline(
976
1025
  DeprecationWarning,
977
1026
  stacklevel=2,
978
1027
  )
979
- num_timepoints = num_frames
1028
+ if num_timepoints is None:
1029
+ num_timepoints = num_frames
1030
+
1031
+ # canonical 1-based timepoint selection (frames/frame_indices deprecated).
1032
+ timepoints = _resolve_timepoints(timepoints, kwargs.pop("frames", None), frame_indices)
1033
+ frame_indices = None
1034
+ # num_zplanes is a count shortcut for planes=[1..N].
1035
+ if num_zplanes is not None and planes is None:
1036
+ planes = list(range(1, int(num_zplanes) + 1))
980
1037
 
981
1038
  # flatten (db, settings) into ops so downstream run_volume / run_plane
982
1039
  # don't each need to forward the pair. explicit ops keys still win.
@@ -986,14 +1043,10 @@ def pipeline(
986
1043
 
987
1044
  reader_kwargs = reader_kwargs or {}
988
1045
  writer_kwargs = writer_kwargs or {}
1046
+ # num_timepoints truncation reaches the writer; an explicit `timepoints`
1047
+ # selection is forwarded as a param and rebuilt by run_plane.
989
1048
  if num_timepoints is not None:
990
- writer_kwargs["num_frames"] = num_timepoints
991
-
992
- # 1-based frame numbers.
993
- if frame_indices is not None:
994
- writer_kwargs["frames"] = [int(i) + 1 for i in frame_indices]
995
- # don't double-pass num_frames; len(frame_indices) is implicit
996
- writer_kwargs.pop("num_frames", None)
1049
+ writer_kwargs["num_timepoints"] = num_timepoints
997
1050
 
998
1051
  # Always load array to check dimensions and ensure downstream functions have the array shape
999
1052
  # If input is already array, this is fast. If path or list of paths, it loads lazy array.
@@ -1028,7 +1081,8 @@ def pipeline(
1028
1081
  keep_raw=keep_raw,
1029
1082
  force_reg=force_reg,
1030
1083
  force_detect=force_detect,
1031
- frame_indices=frame_indices,
1084
+ replot=replot,
1085
+ timepoints=timepoints,
1032
1086
  dff_window_size=dff_window_size,
1033
1087
  dff_percentile=dff_percentile,
1034
1088
  dff_smooth_window=dff_smooth_window,
@@ -1067,7 +1121,8 @@ def pipeline(
1067
1121
  keep_raw=keep_raw,
1068
1122
  force_reg=force_reg,
1069
1123
  force_detect=force_detect,
1070
- frame_indices=frame_indices,
1124
+ replot=replot,
1125
+ timepoints=timepoints,
1071
1126
  dff_window_size=dff_window_size,
1072
1127
  dff_percentile=dff_percentile,
1073
1128
  dff_smooth_window=dff_smooth_window,
@@ -1238,6 +1293,10 @@ def run_volume(
1238
1293
  keep_raw: bool = False,
1239
1294
  force_reg: bool = False,
1240
1295
  force_detect: bool = False,
1296
+ replot: bool = True,
1297
+ timepoints: list | int | None = None,
1298
+ num_timepoints: int = None,
1299
+ num_zplanes: int = None,
1241
1300
  frame_indices: list | None = None,
1242
1301
  dff_window_size: int = None,
1243
1302
  dff_percentile: int = 20,
@@ -1281,6 +1340,9 @@ def run_volume(
1281
1340
  Force re-registration.
1282
1341
  force_detect : bool, default False
1283
1342
  Force detection.
1343
+ replot : bool, default True
1344
+ Regenerate per-plane figures (passed to run_plane). Set False to skip
1345
+ per-plane figure regeneration during a volumetric aggregate.
1284
1346
  frame_indices : list, default None
1285
1347
  List of frame indices to process.
1286
1348
  dff_window_size, dff_percentile, dff_smooth_window : optional
@@ -1326,6 +1388,17 @@ def run_volume(
1326
1388
  _resolve_gpu_env()
1327
1389
  _apply_thread_limits(threads_per_worker)
1328
1390
 
1391
+ # canonical 1-based timepoints (frames/frame_indices deprecated); keep a
1392
+ # 0-based frame_indices for this function's reactive-metadata plumbing,
1393
+ # and forward `timepoints` to run_plane.
1394
+ timepoints = _resolve_timepoints(timepoints, kwargs.pop("frames", None), frame_indices)
1395
+ frame_indices = [int(t) - 1 for t in timepoints] if timepoints is not None else None
1396
+ if num_zplanes is not None and planes is None:
1397
+ planes = list(range(1, int(num_zplanes) + 1))
1398
+ writer_kwargs = dict(writer_kwargs or {})
1399
+ if num_timepoints is not None:
1400
+ writer_kwargs.setdefault("num_timepoints", num_timepoints)
1401
+
1329
1402
  # Handle input data
1330
1403
  input_arr = None
1331
1404
  input_paths = []
@@ -1418,7 +1491,8 @@ def run_volume(
1418
1491
  keep_raw=keep_raw,
1419
1492
  force_reg=force_reg,
1420
1493
  force_detect=force_detect,
1421
- frame_indices=frame_indices,
1494
+ replot=replot,
1495
+ timepoints=timepoints,
1422
1496
  dff_window_size=dff_window_size,
1423
1497
  dff_percentile=dff_percentile,
1424
1498
  dff_smooth_window=dff_smooth_window,
@@ -2270,6 +2344,9 @@ def run_plane(
2270
2344
  keep_reg: bool = True,
2271
2345
  force_reg: bool = False,
2272
2346
  force_detect: bool = False,
2347
+ replot: bool = True,
2348
+ timepoints: list | int | None = None,
2349
+ num_timepoints: int = None,
2273
2350
  frame_indices: list | None = None,
2274
2351
  dff_window_size: int = None,
2275
2352
  dff_percentile: int = 20,
@@ -2314,6 +2391,9 @@ def run_plane(
2314
2391
  If True, force a new registration.
2315
2392
  force_detect : bool, default False
2316
2393
  If True, force ROI detection.
2394
+ replot : bool, default True
2395
+ Generate per-plane figures. Set False to skip figure generation
2396
+ (keeps ROI stats; only the figures are skipped).
2317
2397
  dff_window_size : int, optional
2318
2398
  Frames for rolling percentile baseline. Default: auto-calculated (~10*tau*fs).
2319
2399
  dff_percentile : int, default 20
@@ -2337,13 +2417,18 @@ def run_plane(
2337
2417
  Example: {"n_clusters": 50, "n_PCs": 64}.
2338
2418
  save_json : bool, default False
2339
2419
  Save ops as JSON.
2340
- frame_indices : list[int], optional
2341
- Explicit 0-based timepoint indices. Supports stride
2342
- (e.g. ``list(range(0, 1574, 2))`` for every other frame).
2420
+ timepoints : list[int] or int, optional
2421
+ Explicit 1-based timepoints. Supports stride
2422
+ (e.g. ``list(range(1, 1575, 2))`` for every other timepoint).
2343
2423
  When provided, the binary on disk contains exactly these
2344
- frames, and `OutputMetadata` reactively scales `fs` in ops.npy
2345
- based on the implicit stride. Takes precedence over any
2346
- ``num_frames`` in ``writer_kwargs``.
2424
+ timepoints, and `OutputMetadata` reactively scales `fs` in ops.npy
2425
+ based on the implicit stride. Takes precedence over
2426
+ ``num_timepoints``.
2427
+ num_timepoints : int, optional
2428
+ Limit processing to first N timepoints (truncation only).
2429
+ frame_indices : list[int], optional
2430
+ Deprecated alias for ``timepoints`` (0-based). Emits a
2431
+ DeprecationWarning.
2347
2432
  plane_name : str, optional
2348
2433
  Custom name for the plane subdirectory.
2349
2434
  reader_kwargs : dict, optional
@@ -2363,6 +2448,14 @@ def run_plane(
2363
2448
 
2364
2449
  _resolve_gpu_env()
2365
2450
 
2451
+ # canonical 1-based timepoints (frames/frame_indices deprecated); convert to
2452
+ # the 0-based frame_indices this function consumes internally.
2453
+ timepoints = _resolve_timepoints(timepoints, kwargs.pop("frames", None), frame_indices)
2454
+ frame_indices = [int(t) - 1 for t in timepoints] if timepoints is not None else None
2455
+ writer_kwargs = dict(writer_kwargs or {})
2456
+ if num_timepoints is not None:
2457
+ writer_kwargs.setdefault("num_timepoints", num_timepoints)
2458
+
2366
2459
  progress_callback = kwargs.pop("progress_callback", None)
2367
2460
 
2368
2461
  if "debug" in kwargs:
@@ -2621,7 +2714,8 @@ def run_plane(
2621
2714
  else:
2622
2715
  # prefer the user-specified frame limit over raw array shape
2623
2716
  nframes_hint = (
2624
- writer_kwargs.get("num_frames")
2717
+ writer_kwargs.get("num_timepoints")
2718
+ or writer_kwargs.get("num_frames")
2625
2719
  or ops.get("nframes")
2626
2720
  )
2627
2721
  if not nframes_hint and input_arr is not None and hasattr(input_arr, "shape"):
@@ -2650,8 +2744,8 @@ def run_plane(
2650
2744
  ops_file = plane_dir / "ops.npy"
2651
2745
 
2652
2746
  # extract expected dims from input for cache validation
2653
- # honors writer_kwargs["num_frames"] limit if user requested fewer frames
2654
- exp_nframes = writer_kwargs.get("num_frames")
2747
+ # honors num_timepoints truncation if user requested fewer frames
2748
+ exp_nframes = writer_kwargs.get("num_timepoints") or writer_kwargs.get("num_frames")
2655
2749
  exp_ly = exp_lx = None
2656
2750
  if input_arr is not None and hasattr(input_arr, "shape"):
2657
2751
  if exp_nframes is None:
@@ -2780,12 +2874,13 @@ def run_plane(
2780
2874
  write_planes = [plane] if _get_num_planes(file) > 1 else None
2781
2875
 
2782
2876
  write_kw = dict(writer_kwargs)
2783
- # If the caller gave us explicit frame indices, pass them as
2784
- # `frames=` (1-based) to imwrite. This wins over any stale
2785
- # `num_frames` truncation in writer_kwargs — strided semantics
2786
- # require an explicit index list, not a count.
2877
+ # If the caller gave us explicit timepoints, pass them as
2878
+ # `timepoints=` (1-based) to imwrite. This wins over any stale
2879
+ # truncation count in writer_kwargs — strided semantics require an
2880
+ # explicit index list, not a count.
2787
2881
  if frame_indices is not None:
2788
- write_kw["frames"] = [int(i) + 1 for i in frame_indices]
2882
+ write_kw["timepoints"] = [int(i) + 1 for i in frame_indices]
2883
+ write_kw.pop("num_timepoints", None)
2789
2884
  write_kw.pop("num_frames", None)
2790
2885
 
2791
2886
  imwrite(
@@ -3143,7 +3238,7 @@ def run_plane(
3143
3238
  except Exception as _e:
3144
3239
  print(f" Warning: persisting post-processing kwargs failed: {_e}")
3145
3240
 
3146
- # 3b. ROI statistics
3241
+ # 3b. ROI statistics (cheap; feeds the volumetric aggregate, so always run).
3147
3242
  try:
3148
3243
  from lbm_suite2p_python.postprocessing import compute_roi_stats
3149
3244
 
@@ -3152,20 +3247,35 @@ def run_plane(
3152
3247
  except Exception as e:
3153
3248
  print(f" Warning: ROI stats computation failed: {e}")
3154
3249
 
3155
- # 4. Plots and Cleanup
3156
- try:
3157
- plot_zplane_figures(
3158
- plane_dir,
3159
- dff_percentile=dff_percentile,
3160
- dff_window_size=dff_window_size,
3161
- dff_smooth_window=dff_smooth_window,
3162
- norm_method=norm_method,
3163
- correct_neuropil=correct_neuropil,
3164
- run_rastermap=rastermap_kwargs is not None,
3165
- rastermap_kwargs=rastermap_kwargs,
3166
- )
3167
- except Exception as e:
3168
- print(f" Warning: Plot generation failed: {e}")
3250
+ # 4. Per-plane figures. Timed as the "plots" step (recorded in
3251
+ # processing_history). Skipped when replot=False — e.g. the volumetric
3252
+ # aggregate re-running already-plotted planes, where regenerating the
3253
+ # per-plane figures is the dominant redundant cost.
3254
+ if replot:
3255
+ plot_start = time.time()
3256
+ try:
3257
+ plot_zplane_figures(
3258
+ plane_dir,
3259
+ dff_percentile=dff_percentile,
3260
+ dff_window_size=dff_window_size,
3261
+ dff_smooth_window=dff_smooth_window,
3262
+ norm_method=norm_method,
3263
+ correct_neuropil=correct_neuropil,
3264
+ run_rastermap=rastermap_kwargs is not None,
3265
+ rastermap_kwargs=rastermap_kwargs,
3266
+ )
3267
+ except Exception as e:
3268
+ print(f" Warning: Plot generation failed: {e}")
3269
+
3270
+ if ops_file.exists():
3271
+ try:
3272
+ _t_ops = load_ops(ops_file)
3273
+ _add_processing_step(_t_ops, "plots", duration_seconds=time.time() - plot_start)
3274
+ save_ops_db_settings(ops_file, _t_ops)
3275
+ except Exception as _e:
3276
+ print(f" Warning: recording plot timing failed: {_e}")
3277
+ else:
3278
+ print(" replot=False: keeping existing per-plane figures")
3169
3279
 
3170
3280
  if save_json:
3171
3281
  ops_to_json(ops_file)
@@ -0,0 +1,229 @@
1
+ import os
2
+ import numpy as np
3
+ from pathlib import Path
4
+
5
+
6
+ # mbo_utilities >= 4.0 exposes a single LazyArray base; isinstance covers
7
+ # every built-in and any third-party plugin. The class-name tuple is the
8
+ # fallback for mbo_utilities < 4.0, which has no shared base.
9
+ try:
10
+ from mbo_utilities import LazyArray as _LazyArray
11
+ except ImportError: # mbo_utilities < 4.0
12
+ _LazyArray = None
13
+
14
+ _LAZY_ARRAY_TYPES = (
15
+ "ScanImageArray",
16
+ "LBMArray",
17
+ "PiezoArray",
18
+ "SinglePlaneArray",
19
+ "Suite2pArray",
20
+ "MBOTiffArray",
21
+ "MboRawArray",
22
+ "TiffArray",
23
+ "ZarrArray",
24
+ "H5Array",
25
+ "NumpyArray",
26
+ "BinArray",
27
+ )
28
+
29
+
30
+ def _is_lazy_array(obj):
31
+ """Check if obj is an mbo_utilities lazy array type."""
32
+ if _LazyArray is not None and isinstance(obj, _LazyArray):
33
+ return True
34
+ return type(obj).__name__ in _LAZY_ARRAY_TYPES
35
+
36
+
37
+ def _get_num_planes(arr):
38
+ """
39
+ Get number of z-planes from a lazy array.
40
+
41
+ mbo_utilities arrays are always 5D TCZYX, so Z is at shape[2].
42
+ Falls back to legacy 4D TZYX (Z at shape[1]) and other heuristics
43
+ for non-mbo arrays.
44
+
45
+ Parameters
46
+ ----------
47
+ arr : array-like
48
+ Input array, typically from mbo_utilities.
49
+
50
+ Returns
51
+ -------
52
+ int
53
+ Number of z-planes (1 if no Z dimension).
54
+ """
55
+ # mbo_utilities Shape5DMixin
56
+ if hasattr(arr, "nz"):
57
+ return arr.nz
58
+ if hasattr(arr, "num_planes") and arr.num_planes is not None:
59
+ return arr.num_planes
60
+ shape = arr.shape
61
+ if len(shape) == 5:
62
+ return shape[2] # 5D TCZYX
63
+ if len(shape) == 4:
64
+ return shape[1] # legacy 4D TZYX
65
+ return 1
66
+
67
+
68
+ def _resize_masks_fit_crop(mask, target_shape):
69
+ """Centers a mask within the target shape, cropping if too large or padding if too small."""
70
+ sy, sx = mask.shape
71
+ ty, tx = target_shape
72
+
73
+ # If mask is larger, crop it
74
+ if sy > ty or sx > tx:
75
+ start_y = (sy - ty) // 2
76
+ start_x = (sx - tx) // 2
77
+ return mask[start_y : start_y + ty, start_x : start_x + tx]
78
+
79
+ # If mask is smaller, pad it
80
+ resized_mask = np.zeros(target_shape, dtype=mask.dtype)
81
+ start_y = (ty - sy) // 2
82
+ start_x = (tx - sx) // 2
83
+ resized_mask[start_y : start_y + sy, start_x : start_x + sx] = mask
84
+ return resized_mask
85
+
86
+
87
+ def get_common_path(ops_files: list | tuple):
88
+ """
89
+ Find the common parent path of all files.
90
+
91
+ Parameters
92
+ ----------
93
+ ops_files : list or tuple
94
+ List of file paths.
95
+
96
+ Returns
97
+ -------
98
+ Path
99
+ Common parent directory of all files.
100
+ """
101
+ if not isinstance(ops_files, (list, tuple)):
102
+ ops_files = [ops_files]
103
+ if len(ops_files) == 1:
104
+ path = Path(ops_files[0]).parent
105
+ while (
106
+ path.exists() and len(list(path.iterdir())) <= 1
107
+ ): # Traverse up if only one item exists
108
+ path = path.parent
109
+ return path
110
+ else:
111
+ return Path(os.path.commonpath(ops_files))
112
+
113
+
114
+ def estimate_peak_memory(ops, Ly, Lx, n_frames, device="cuda", workers=1):
115
+ """
116
+ Estimate peak memory for one Suite2p plane from its parameters.
117
+
118
+ Registration and detection run sequentially in suite2p's pipeline, so
119
+ the peak for a plane is ``max(registration, detection)``, not the sum.
120
+ The two stages load different pools:
121
+
122
+ - Registration compute runs on ``device``. When ``device`` is cuda the
123
+ per-batch float32/FFT buffers live in VRAM, scaled by
124
+ ``batch_size * Ly * Lx``. The reference-image correlation
125
+ (``pick_initial_reference``) and the binary read stay on host.
126
+ - Detection's binned movie is a host numpy array, and the default
127
+ ``sparsery`` / ``sourcery`` detectors run on CPU. ``device`` is only
128
+ used by the cellpose path, which sees the 2D meanImg / max_proj, not
129
+ the movie. So the binned movie never enters VRAM.
130
+
131
+ Host RAM therefore peaks during detection (binned movie plus the
132
+ high-pass / sparsery copies, ~2.5x); VRAM peaks during registration
133
+ (or cellpose inference, when enabled). Neither VRAM term scales with
134
+ ``n_frames``; host detection plateaus once ``n_frames // bin_size``
135
+ exceeds ``nbins``.
136
+
137
+ Parameters
138
+ ----------
139
+ ops : dict
140
+ Flat Suite2p ops. Reads ``nimg_init``, ``batch_size`` (registration
141
+ batch), ``nbins``, ``bin_size``, ``tau``, ``fs``, ``nchannels``, and
142
+ ``anatomical_only``. Missing keys fall back to suite2p defaults.
143
+ Ly, Lx : int
144
+ Frame height and width in pixels. The detection crop (yrange/xrange)
145
+ is unknown before registration, so full ``Ly``/``Lx`` are used as an
146
+ upper bound.
147
+ n_frames : int
148
+ Number of frames in the plane.
149
+ device : str, optional (default "cuda")
150
+ Torch device. VRAM terms are reported only when this starts with
151
+ "cuda".
152
+ workers : int, optional (default 1)
153
+ Concurrent plane workers. Per-plane peaks are multiplied by this for
154
+ the ``*_total`` fields.
155
+
156
+ Returns
157
+ -------
158
+ dict
159
+ Bytes for ``host_per_plane``, ``vram_per_plane``, ``host_total``,
160
+ ``vram_total``. VRAM fields are 0 when ``device`` is not cuda.
161
+
162
+ Notes
163
+ -----
164
+ The 2.5x detection multiplier and the cpsam VRAM constant are rough; the
165
+ real values depend on data and hardware. Calibrate with
166
+ ``torch.cuda.max_memory_allocated()`` and an RSS sample around the
167
+ registration / detection calls for tight bounds.
168
+ """
169
+ cuda = str(device).startswith("cuda")
170
+
171
+ nimg_init = min(int(ops.get("nimg_init", 400)), n_frames)
172
+ reg_host = nimg_init * Ly * Lx * 10 # int16 frames + float64 ref corr (CPU)
173
+
174
+ nbins = int(ops.get("nbins", 5000))
175
+ bin_size = ops.get("bin_size") or max(
176
+ 1, n_frames // nbins, round(ops.get("tau", 1.0) * ops.get("fs", 10.0))
177
+ )
178
+ nbinned = min(nbins, n_frames // bin_size)
179
+ detect_host = int(2.5 * nbinned * Ly * Lx * 4) # binned movie + hp/sparsery copies (CPU)
180
+
181
+ host_peak = max(reg_host, detect_host)
182
+
183
+ vram_peak = 0
184
+ if cuda:
185
+ reg_batch = int(ops.get("batch_size", 100))
186
+ nchan = int(ops.get("nchannels", 1))
187
+ reg_vram = nchan * 8 * reg_batch * Ly * Lx * 4 # ~8 float32/FFT buffers per batch
188
+ cpsam_vram = 1_500_000_000 if ops.get("anatomical_only", 0) else 0 # cpsam weights + activations
189
+ vram_peak = max(reg_vram, cpsam_vram)
190
+
191
+ return {
192
+ "host_per_plane": host_peak,
193
+ "vram_per_plane": vram_peak,
194
+ "host_total": host_peak * max(1, workers),
195
+ "vram_total": vram_peak * max(1, workers),
196
+ }
197
+
198
+
199
+ def bin1d(X, bin_size, axis=0):
200
+ """
201
+ Mean bin over `axis` of `X` with bin `bin_size`.
202
+
203
+ Parameters
204
+ ----------
205
+ X : np.ndarray
206
+ Input array to be binned.
207
+ bin_size : int
208
+ Size of the bin. If <=0, no binning is performed.
209
+ axis : int, optional
210
+ Axis along which to bin. Default is 0.
211
+
212
+ Returns
213
+ -------
214
+ np.ndarray
215
+ Binned array with reduced size along the specified axis.
216
+ """
217
+ if bin_size > 0:
218
+ size = list(X.shape)
219
+ Xb = X.swapaxes(0, axis)
220
+ size_new = Xb.shape
221
+ Xb = (
222
+ Xb[: size[axis] // bin_size * bin_size]
223
+ .reshape((size[axis] // bin_size, bin_size, *size_new[1:]))
224
+ .mean(axis=1)
225
+ )
226
+ Xb = Xb.swapaxes(axis, 0)
227
+ return Xb
228
+ else:
229
+ return X
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lbm_suite2p_python
3
- Version: 3.1.1
3
+ Version: 3.2.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lbm_suite2p_python"
7
- version = "3.1.1"
7
+ version = "3.2.1"
8
8
  description = "Calcium Imaging Pipeline built with Suite2p, Cellpose and Rastermap"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -1,144 +0,0 @@
1
- import os
2
- import numpy as np
3
- from pathlib import Path
4
-
5
-
6
- # mbo_utilities >= 4.0 exposes a single LazyArray base; isinstance covers
7
- # every built-in and any third-party plugin. The class-name tuple is the
8
- # fallback for mbo_utilities < 4.0, which has no shared base.
9
- try:
10
- from mbo_utilities import LazyArray as _LazyArray
11
- except ImportError: # mbo_utilities < 4.0
12
- _LazyArray = None
13
-
14
- _LAZY_ARRAY_TYPES = (
15
- "ScanImageArray",
16
- "LBMArray",
17
- "PiezoArray",
18
- "SinglePlaneArray",
19
- "Suite2pArray",
20
- "MBOTiffArray",
21
- "MboRawArray",
22
- "TiffArray",
23
- "ZarrArray",
24
- "H5Array",
25
- "NumpyArray",
26
- "BinArray",
27
- )
28
-
29
-
30
- def _is_lazy_array(obj):
31
- """Check if obj is an mbo_utilities lazy array type."""
32
- if _LazyArray is not None and isinstance(obj, _LazyArray):
33
- return True
34
- return type(obj).__name__ in _LAZY_ARRAY_TYPES
35
-
36
-
37
- def _get_num_planes(arr):
38
- """
39
- Get number of z-planes from a lazy array.
40
-
41
- mbo_utilities arrays are always 5D TCZYX, so Z is at shape[2].
42
- Falls back to legacy 4D TZYX (Z at shape[1]) and other heuristics
43
- for non-mbo arrays.
44
-
45
- Parameters
46
- ----------
47
- arr : array-like
48
- Input array, typically from mbo_utilities.
49
-
50
- Returns
51
- -------
52
- int
53
- Number of z-planes (1 if no Z dimension).
54
- """
55
- # mbo_utilities Shape5DMixin
56
- if hasattr(arr, "nz"):
57
- return arr.nz
58
- if hasattr(arr, "num_planes") and arr.num_planes is not None:
59
- return arr.num_planes
60
- shape = arr.shape
61
- if len(shape) == 5:
62
- return shape[2] # 5D TCZYX
63
- if len(shape) == 4:
64
- return shape[1] # legacy 4D TZYX
65
- return 1
66
-
67
-
68
- def _resize_masks_fit_crop(mask, target_shape):
69
- """Centers a mask within the target shape, cropping if too large or padding if too small."""
70
- sy, sx = mask.shape
71
- ty, tx = target_shape
72
-
73
- # If mask is larger, crop it
74
- if sy > ty or sx > tx:
75
- start_y = (sy - ty) // 2
76
- start_x = (sx - tx) // 2
77
- return mask[start_y : start_y + ty, start_x : start_x + tx]
78
-
79
- # If mask is smaller, pad it
80
- resized_mask = np.zeros(target_shape, dtype=mask.dtype)
81
- start_y = (ty - sy) // 2
82
- start_x = (tx - sx) // 2
83
- resized_mask[start_y : start_y + sy, start_x : start_x + sx] = mask
84
- return resized_mask
85
-
86
-
87
- def get_common_path(ops_files: list | tuple):
88
- """
89
- Find the common parent path of all files.
90
-
91
- Parameters
92
- ----------
93
- ops_files : list or tuple
94
- List of file paths.
95
-
96
- Returns
97
- -------
98
- Path
99
- Common parent directory of all files.
100
- """
101
- if not isinstance(ops_files, (list, tuple)):
102
- ops_files = [ops_files]
103
- if len(ops_files) == 1:
104
- path = Path(ops_files[0]).parent
105
- while (
106
- path.exists() and len(list(path.iterdir())) <= 1
107
- ): # Traverse up if only one item exists
108
- path = path.parent
109
- return path
110
- else:
111
- return Path(os.path.commonpath(ops_files))
112
-
113
-
114
- def bin1d(X, bin_size, axis=0):
115
- """
116
- Mean bin over `axis` of `X` with bin `bin_size`.
117
-
118
- Parameters
119
- ----------
120
- X : np.ndarray
121
- Input array to be binned.
122
- bin_size : int
123
- Size of the bin. If <=0, no binning is performed.
124
- axis : int, optional
125
- Axis along which to bin. Default is 0.
126
-
127
- Returns
128
- -------
129
- np.ndarray
130
- Binned array with reduced size along the specified axis.
131
- """
132
- if bin_size > 0:
133
- size = list(X.shape)
134
- Xb = X.swapaxes(0, axis)
135
- size_new = Xb.shape
136
- Xb = (
137
- Xb[: size[axis] // bin_size * bin_size]
138
- .reshape((size[axis] // bin_size, bin_size, *size_new[1:]))
139
- .mean(axis=1)
140
- )
141
- Xb = Xb.swapaxes(axis, 0)
142
- return Xb
143
- else:
144
- return X