lbm_caiman_python 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,63 @@
1
+ from . import _version
2
+
3
+ # core pipeline api
4
+ from .default_ops import default_ops, mcorr_ops, cnmf_ops
5
+ from .run_lcp import (
6
+ pipeline,
7
+ run_volume,
8
+ run_plane,
9
+ add_processing_step,
10
+ generate_plane_dirname,
11
+ )
12
+ from .postprocessing import (
13
+ load_ops,
14
+ load_planar_results,
15
+ dff_rolling_percentile,
16
+ compute_roi_stats,
17
+ get_accepted_cells,
18
+ get_contours,
19
+ )
20
+
21
+ # existing utilities
22
+ from .collation import combine_z_planes
23
+ from .util.transform import vectorize, unvectorize, calculate_centers
24
+ from .util.quality import get_noise_fft, greedyROI
25
+ from .util.signal import smooth_data, norm_minmax
26
+ from .helpers import (
27
+ generate_patch_view,
28
+ get_single_patch_coords,
29
+ extract_center_square,
30
+ )
31
+
32
+ __version__ = _version.get_versions()['version']
33
+
34
+ __all__ = [
35
+ # core pipeline
36
+ "pipeline",
37
+ "run_volume",
38
+ "run_plane",
39
+ "default_ops",
40
+ "mcorr_ops",
41
+ "cnmf_ops",
42
+ "add_processing_step",
43
+ "generate_plane_dirname",
44
+
45
+ # postprocessing
46
+ "load_ops",
47
+ "load_planar_results",
48
+ "dff_rolling_percentile",
49
+ "compute_roi_stats",
50
+ "get_accepted_cells",
51
+ "get_contours",
52
+
53
+ # existing utilities
54
+ "combine_z_planes",
55
+ "generate_patch_view",
56
+ "get_noise_fft",
57
+ "calculate_centers",
58
+ "greedyROI",
59
+ "get_single_patch_coords",
60
+ "extract_center_square",
61
+ "smooth_data",
62
+ "norm_minmax",
63
+ ]
@@ -0,0 +1,302 @@
1
+ """
2
+ cli entry point for lbm-caiman-python.
3
+
4
+ usage:
5
+ lcp <input> <output> [options]
6
+ python -m lbm_caiman_python <input> <output> [options]
7
+ """
8
+
9
+ import argparse
10
+ import sys
11
+ from functools import partial
12
+ from pathlib import Path
13
+
14
+ print = partial(print, flush=True)
15
+
16
+
17
+ def add_args(parser: argparse.ArgumentParser):
18
+ """add command-line arguments to the parser."""
19
+ from lbm_caiman_python.default_ops import default_ops
20
+
21
+ defaults = default_ops()
22
+
23
+ # positional arguments
24
+ parser.add_argument(
25
+ "input",
26
+ type=str,
27
+ nargs="?",
28
+ default=None,
29
+ help="Input file or directory",
30
+ )
31
+ parser.add_argument(
32
+ "output",
33
+ type=str,
34
+ nargs="?",
35
+ default=None,
36
+ help="Output directory",
37
+ )
38
+
39
+ # processing flags
40
+ parser.add_argument(
41
+ "--planes",
42
+ type=int,
43
+ nargs="+",
44
+ default=None,
45
+ help="Planes to process (1-based)",
46
+ )
47
+ parser.add_argument(
48
+ "--no-mcorr",
49
+ action="store_true",
50
+ help="Skip motion correction",
51
+ )
52
+ parser.add_argument(
53
+ "--no-cnmf",
54
+ action="store_true",
55
+ help="Skip CNMF",
56
+ )
57
+ parser.add_argument(
58
+ "--force-mcorr",
59
+ action="store_true",
60
+ help="Force re-run motion correction",
61
+ )
62
+ parser.add_argument(
63
+ "--force-cnmf",
64
+ action="store_true",
65
+ help="Force re-run CNMF",
66
+ )
67
+ parser.add_argument(
68
+ "--num-frames",
69
+ type=int,
70
+ default=None,
71
+ help="Limit number of frames to process",
72
+ )
73
+
74
+ # motion correction parameters
75
+ parser.add_argument(
76
+ "--max-shifts",
77
+ type=int,
78
+ nargs=2,
79
+ default=None,
80
+ help=f"Maximum rigid shifts (default: {defaults['max_shifts']})",
81
+ )
82
+ parser.add_argument(
83
+ "--strides",
84
+ type=int,
85
+ nargs=2,
86
+ default=None,
87
+ help=f"Patch strides for piecewise-rigid (default: {defaults['strides']})",
88
+ )
89
+ parser.add_argument(
90
+ "--overlaps",
91
+ type=int,
92
+ nargs=2,
93
+ default=None,
94
+ help=f"Patch overlaps (default: {defaults['overlaps']})",
95
+ )
96
+ parser.add_argument(
97
+ "--gSig-filt",
98
+ type=int,
99
+ nargs=2,
100
+ default=None,
101
+ help=f"Gaussian filter size (default: {defaults['gSig_filt']})",
102
+ )
103
+ parser.add_argument(
104
+ "--no-pw-rigid",
105
+ action="store_true",
106
+ help="Disable piecewise-rigid correction (use rigid only)",
107
+ )
108
+
109
+ # cnmf parameters
110
+ parser.add_argument(
111
+ "--K",
112
+ type=int,
113
+ default=None,
114
+ help=f"Expected number of neurons (default: {defaults['K']})",
115
+ )
116
+ parser.add_argument(
117
+ "--gSig",
118
+ type=int,
119
+ nargs=2,
120
+ default=None,
121
+ help=f"Expected neuron half-width (default: {defaults['gSig']})",
122
+ )
123
+ parser.add_argument(
124
+ "--min-SNR",
125
+ type=float,
126
+ default=None,
127
+ help=f"Minimum SNR threshold (default: {defaults['min_SNR']})",
128
+ )
129
+ parser.add_argument(
130
+ "--rval-thr",
131
+ type=float,
132
+ default=None,
133
+ help=f"Correlation threshold (default: {defaults['rval_thr']})",
134
+ )
135
+ parser.add_argument(
136
+ "--merge-thresh",
137
+ type=float,
138
+ default=None,
139
+ help=f"Merging threshold (default: {defaults['merge_thresh']})",
140
+ )
141
+
142
+ # general parameters
143
+ parser.add_argument(
144
+ "--fr",
145
+ type=float,
146
+ default=None,
147
+ help=f"Frame rate in Hz (default: {defaults['fr']})",
148
+ )
149
+ parser.add_argument(
150
+ "--n-processes",
151
+ type=int,
152
+ default=None,
153
+ help="Number of parallel processes (default: auto)",
154
+ )
155
+
156
+ # utility flags
157
+ parser.add_argument(
158
+ "--version",
159
+ action="store_true",
160
+ help="Show version information",
161
+ )
162
+ parser.add_argument(
163
+ "--debug",
164
+ action="store_true",
165
+ help="Enable debug mode",
166
+ )
167
+
168
+ return parser
169
+
170
+
171
+ def build_ops_from_args(args) -> dict:
172
+ """build ops dictionary from command-line arguments."""
173
+ from lbm_caiman_python.default_ops import default_ops
174
+
175
+ ops = default_ops()
176
+
177
+ # map cli args to ops keys
178
+ arg_to_ops = {
179
+ "max_shifts": "max_shifts",
180
+ "strides": "strides",
181
+ "overlaps": "overlaps",
182
+ "gSig_filt": "gSig_filt",
183
+ "K": "K",
184
+ "gSig": "gSig",
185
+ "min_SNR": "min_SNR",
186
+ "rval_thr": "rval_thr",
187
+ "merge_thresh": "merge_thresh",
188
+ "fr": "fr",
189
+ "n_processes": "n_processes",
190
+ }
191
+
192
+ for arg_name, ops_key in arg_to_ops.items():
193
+ value = getattr(args, arg_name.replace("-", "_"), None)
194
+ if value is not None:
195
+ if isinstance(value, list) and len(value) == 2:
196
+ ops[ops_key] = tuple(value)
197
+ else:
198
+ ops[ops_key] = value
199
+
200
+ # handle boolean flags
201
+ if args.no_mcorr:
202
+ ops["do_motion_correction"] = False
203
+ if args.no_cnmf:
204
+ ops["do_cnmf"] = False
205
+ if args.no_pw_rigid:
206
+ ops["pw_rigid"] = False
207
+
208
+ return ops
209
+
210
+
211
+ def main():
212
+ """main cli entry point."""
213
+ print("\n")
214
+ print("--- LBM-CaImAn Pipeline ---")
215
+ print("\n")
216
+
217
+ parser = argparse.ArgumentParser(
218
+ description="LBM-CaImAn processing pipeline",
219
+ formatter_class=argparse.RawDescriptionHelpFormatter,
220
+ epilog="""
221
+ examples:
222
+ lcp data.tiff output/
223
+ lcp data/ results/ --planes 1 2 3
224
+ lcp movie.tif results/ --K 100 --gSig 5 5
225
+ lcp data.tiff output/ --no-mcorr # skip motion correction
226
+ """,
227
+ )
228
+ parser = add_args(parser)
229
+ args = parser.parse_args()
230
+
231
+ # handle version
232
+ if args.version:
233
+ import lbm_caiman_python
234
+ print(f"lbm_caiman_python v{lbm_caiman_python.__version__}")
235
+ return
236
+
237
+ # check required arguments
238
+ if args.input is None:
239
+ parser.print_help()
240
+ return
241
+
242
+ # setup logging
243
+ if args.debug:
244
+ import logging
245
+ logging.basicConfig(level=logging.DEBUG)
246
+ print("Debug mode enabled.")
247
+
248
+ # validate input
249
+ input_path = Path(args.input).expanduser().resolve()
250
+ if not input_path.exists():
251
+ print(f"Error: Input path does not exist: {input_path}")
252
+ sys.exit(1)
253
+
254
+ # setup output
255
+ if args.output is None:
256
+ output_path = input_path.parent / (input_path.stem + "_caiman_results")
257
+ else:
258
+ output_path = Path(args.output).expanduser().resolve()
259
+
260
+ print(f"Input: {input_path}")
261
+ print(f"Output: {output_path}")
262
+
263
+ # build ops from arguments
264
+ ops = build_ops_from_args(args)
265
+
266
+ # run pipeline
267
+ from lbm_caiman_python.run_lcp import pipeline
268
+
269
+ try:
270
+ results = pipeline(
271
+ input_data=input_path,
272
+ save_path=str(output_path),
273
+ ops=ops,
274
+ planes=args.planes,
275
+ force_mcorr=args.force_mcorr,
276
+ force_cnmf=args.force_cnmf,
277
+ num_timepoints=args.num_frames,
278
+ )
279
+
280
+ print("\n")
281
+ print("--- Processing Complete ---")
282
+ print(f"Results saved to: {output_path}")
283
+ print(f"Processed {len(results)} plane(s)")
284
+
285
+ # print summary
286
+ for ops_path in results:
287
+ from lbm_caiman_python.postprocessing import load_ops
288
+ ops_result = load_ops(ops_path)
289
+ plane = ops_result.get("plane", "?")
290
+ n_cells = ops_result.get("n_cells", 0)
291
+ print(f" Plane {plane}: {n_cells} cells")
292
+
293
+ except Exception as e:
294
+ print(f"\nError: {e}")
295
+ if args.debug:
296
+ import traceback
297
+ traceback.print_exc()
298
+ sys.exit(1)
299
+
300
+
301
+ if __name__ == "__main__":
302
+ main()
@@ -0,0 +1,8 @@
1
+ """version information for lbm_caiman_python."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+
6
+ def get_versions():
7
+ """return version dict for backwards compatibility."""
8
+ return {"version": __version__}
@@ -0,0 +1,188 @@
1
+ import re as regex
2
+ from pathlib import Path
3
+ from typing import Union
4
+ from contextlib import contextmanager
5
+ import pathlib
6
+ import lbm_mc as mc
7
+
8
+ COMPUTE_BACKEND_SUBPROCESS = "subprocess" #: subprocess backend
9
+ COMPUTE_BACKEND_SLURM = "slurm" #: SLURM backend
10
+ COMPUTE_BACKEND_LOCAL = "local"
11
+
12
+ COMPUTE_BACKENDS = [
13
+ COMPUTE_BACKEND_SUBPROCESS,
14
+ COMPUTE_BACKEND_SLURM,
15
+ COMPUTE_BACKEND_LOCAL,
16
+ ]
17
+
18
+ DATAFRAME_COLUMNS = [
19
+ "algo",
20
+ "item_name",
21
+ "input_movie_path",
22
+ "params",
23
+ "outputs",
24
+ "added_time",
25
+ "ran_time",
26
+ "algo_duration",
27
+ "comments",
28
+ "uuid",
29
+ ]
30
+
31
+
32
+ @contextmanager
33
+ def _set_posix_windows():
34
+ posix_backup = pathlib.PosixPath
35
+ try:
36
+ pathlib.PosixPath = pathlib.WindowsPath
37
+ yield
38
+ finally:
39
+ pathlib.PosixPath = posix_backup
40
+
41
+
42
+ @contextmanager
43
+ def _set_windows_posix():
44
+ """
45
+ Set the Path class to WindowsPath on a POSIX system.
46
+ """
47
+ windows_backup = pathlib.WindowsPath
48
+ try:
49
+ pathlib.WindowsPath = pathlib.PosixPath
50
+ yield
51
+ finally:
52
+ pathlib.WindowsPath = windows_backup
53
+
54
+
55
+ def load_batch(batch_path: str | Path):
56
+ """
57
+ Load a batch after transfering it from a Windows to a POSIX system or vice versa.
58
+
59
+ Parameters
60
+ ----------
61
+ batch_path : str, Path
62
+ The path to the batch file.
63
+
64
+ Returns
65
+ -------
66
+ pandas.DataFrame
67
+ The loaded batch.
68
+ """
69
+ try:
70
+ with _set_windows_posix():
71
+ return mc.load_batch(batch_path)
72
+ except Exception:
73
+ with _set_posix_windows():
74
+ return mc.load_batch(batch_path)
75
+
76
+
77
+ def clean_batch(df):
78
+ """
79
+ Clean a batch of DataFrame entries by removing unsuccessful df from storage.
80
+
81
+ This function iterates over the df of the given DataFrame, identifies
82
+ df where the 'outputs' column is either `None` or a dictionary containing
83
+ a 'success' key with a `False` value. For each such row, the corresponding
84
+ item is removed using the `df.caiman.remove_item()` method, and the removal
85
+ is saved to disk.
86
+
87
+ Parameters
88
+ ----------
89
+ df : pandas.DataFrame
90
+ The DataFrame to be cleaned. It must have a 'uuid' column for identification
91
+ and an 'outputs' column containing a dictionary with a 'success' key.
92
+
93
+ Returns
94
+ -------
95
+ pandas.DataFrame
96
+ The DataFrame reloaded from disk after unsuccessful items have been removed.
97
+
98
+ Notes
99
+ -----
100
+ - If 'outputs' is None or does not contain 'success' as a key with a value of
101
+ `False`, the row will be removed.
102
+
103
+ Examples
104
+ --------
105
+ >>> import pandas as pd
106
+ >>> df = pd.DataFrame({
107
+ ... 'uuid': ['123', '456', '789'],
108
+ ... 'outputs': [{'success': True}, {'success': False}, None]
109
+ ... })
110
+ >>> cleaned_df = clean_batch(df)
111
+ Removing unsuccessful batch row 1.
112
+ Row 1 deleted.
113
+ Removing unsuccessful batch row 2.
114
+ Row 2 deleted.
115
+ """
116
+ for index, row in df.iterrows():
117
+ # Check if 'outputs' is a dictionary and has 'success' key with value False
118
+ if isinstance(row["outputs"], dict) and row["outputs"].get("success") is False or row["outputs"] is None:
119
+ uuid = row["uuid"]
120
+ print(f"Removing unsuccessful batch row {row.index}.")
121
+ df.caiman.remove_item(uuid, remove_data=True, safe_removal=False)
122
+ print(f"Row {row.index} deleted.")
123
+ df.caiman.save_to_disk()
124
+ return df.caiman.reload_from_disk()
125
+
126
+
127
+ def delete_batch_rows(df, rows_delete, remove_data=False, safe_removal=True):
128
+ rows_delete = [rows_delete] if isinstance(rows_delete, int) else rows_delete
129
+ uuids_delete = [row.uuid for i, row in df.iterrows() if i in rows_delete]
130
+ for uuid in uuids_delete:
131
+ df.caiman.remove_item(uuid, remove_data=remove_data, safe_removal=safe_removal)
132
+ df.caiman.save_to_disk()
133
+ return df
134
+
135
+
136
+ def validate_path(path: Union[str, Path]):
137
+ if not regex.match("^[A-Za-z0-9@\/\\\:._-]*$", str(path)):
138
+ raise ValueError(
139
+ "Paths must only contain alphanumeric characters, "
140
+ "hyphens ( - ), underscores ( _ ) or periods ( . )"
141
+ )
142
+ return path
143
+
144
+
145
+ def drop_duplicates(df):
146
+ """
147
+ Remove duplicate items from a batch DataFrame.
148
+
149
+ Parameters
150
+ ----------
151
+ df : pandas.DataFrame
152
+ The batch DataFrame to remove duplicates from.
153
+
154
+ Returns
155
+ -------
156
+ None
157
+
158
+ """
159
+ import hashlib
160
+ df["hash"] = df.apply(lambda row: hashlib.sha256(row.mcorr.get_output().tobytes()).hexdigest(), axis=1)
161
+ uuids_to_remove = []
162
+ for _, group in df.groupby("hash"):
163
+ if len(group) > 1:
164
+ for idx in group.index[1:]:
165
+ uuid = df.loc[idx, "uuid"]
166
+ uuids_to_remove.append(uuid)
167
+ if not uuids_to_remove:
168
+ print("No duplicates found.")
169
+ return
170
+ for uuid in uuids_to_remove:
171
+ print(f"Removing duplicate item {uuid}.")
172
+ df.caiman.remove_item(uuid, remove_data=True, safe_removal=False)
173
+ df.drop(columns="hash", inplace=True)
174
+ df.caiman.save_to_disk()
175
+ return df
176
+
177
+
178
+ def get_batch_from_path(batch_path):
179
+ """
180
+ Load or create a batch at the given batch_path.
181
+ """
182
+ try:
183
+ df = mc.load_batch(batch_path)
184
+ print(f"Batch found at {batch_path}")
185
+ except (IsADirectoryError, FileNotFoundError):
186
+ print(f"Creating batch at {batch_path}")
187
+ df = mc.create_batch(batch_path)
188
+ return df
@@ -0,0 +1,125 @@
1
+ from pathlib import Path
2
+ import numpy as np
3
+ from scipy.sparse import hstack
4
+ import copy
5
+ import scipy.signal
6
+ import json
7
+
8
+
9
+ def combine_z_planes(results: dict):
10
+ """
11
+ Combines all z-planes in the results dictionary into a single estimates object.
12
+
13
+ Parameters
14
+ ----------
15
+ results (dict): Dictionary with estimates for each z-plane.
16
+
17
+ Returns
18
+ -------
19
+ estimates.Estimates: Combined estimates for all z-planes.
20
+ """
21
+ from caiman.source_extraction.cnmf import estimates
22
+ keys = sorted(results.keys())
23
+ e_list = [results[k].estimates for k in keys]
24
+
25
+ # Initialize lists to collect components
26
+ A_list = []
27
+ b_list = []
28
+ C_list = []
29
+ f_list = []
30
+ R_list = []
31
+
32
+ for e in e_list:
33
+ A_list.append(e.A)
34
+ b_list.append(e.b)
35
+ C_list.append(e.C)
36
+ f_list.append(e.f)
37
+ R_list.append(e.R)
38
+
39
+ # Combine the components
40
+ A_new = hstack(A_list).tocsr()
41
+ b_new = np.concatenate(b_list, axis=0)
42
+ C_new = np.concatenate(C_list, axis=0)
43
+ f_new = np.concatenate(f_list, axis=0)
44
+ R_new = np.concatenate(R_list, axis=0)
45
+
46
+ # Assuming all z-planes have the same spatial dimensions
47
+ dims_new = e_list[0].dims # e.g., (height, width)
48
+
49
+ # Create new estimates object
50
+ e_new = estimates.Estimates(
51
+ A=A_new,
52
+ C=C_new,
53
+ b=b_new,
54
+ f=f_new,
55
+ R=R_new,
56
+ dims=dims_new
57
+ )
58
+
59
+ return e_new
60
+
61
+
62
+ def calculate_interplane_shifts(volume, n_planes, params, json_logger=None):
63
+ """
64
+ """
65
+ interplane_shifts = np.zeros((n_planes - 1, 2), dtype=int)
66
+ accumulated_shifts = np.zeros((n_planes - 1, 2), dtype=int)
67
+
68
+ for i_plane in range(n_planes - 1):
69
+ im1_copy = copy.deepcopy(volume[0, :, :, i_plane])
70
+ im2_copy = copy.deepcopy(volume[0, :, :, i_plane + 1])
71
+
72
+ # Removing NaNs
73
+ nonan_mask = np.stack(
74
+ (~np.isnan(im1_copy), ~np.isnan(im2_copy)), axis=0
75
+ )
76
+ nonan_mask = np.all(nonan_mask, axis=0)
77
+ coord_nonan_pixels = np.where(nonan_mask)
78
+ min_x, max_x = np.min(coord_nonan_pixels[0]), np.max(coord_nonan_pixels[0])
79
+ min_y, max_y = np.min(coord_nonan_pixels[1]), np.max(coord_nonan_pixels[1])
80
+
81
+ im1_nonan = im1_copy[min_x:max_x + 1, min_y:max_y + 1]
82
+ im2_nonan = im2_copy[min_x:max_x + 1, min_y:max_y + 1]
83
+
84
+ # Normalize intensities
85
+ im1_nonan -= np.min(im1_nonan)
86
+ im2_nonan -= np.min(im2_nonan)
87
+
88
+ # Cross-correlation
89
+ cross_corr_img = scipy.signal.fftconvolve(
90
+ im1_nonan, im2_nonan[::-1, ::-1], mode="same"
91
+ )
92
+
93
+ # Find correlation peak
94
+ corr_img_peak_x, corr_img_peak_y = np.unravel_index(
95
+ np.argmax(cross_corr_img), cross_corr_img.shape
96
+ )
97
+ self_corr_peak_x, self_corr_peak_y = [
98
+ dim / 2 for dim in cross_corr_img.shape
99
+ ]
100
+ interplane_shift = [
101
+ corr_img_peak_x - self_corr_peak_x,
102
+ corr_img_peak_y - self_corr_peak_y,
103
+ ]
104
+
105
+ interplane_shifts[i_plane] = copy.deepcopy(interplane_shift)
106
+ accumulated_shifts[i_plane] = np.sum(
107
+ interplane_shifts, axis=0, dtype=int
108
+ )
109
+
110
+ # Normalize shifts to start at zero
111
+ min_accumulated_shift = np.min(accumulated_shifts, axis=0)
112
+ for xy in range(2):
113
+ accumulated_shifts[:, xy] -= min_accumulated_shift[xy]
114
+
115
+ # Optional JSON logging
116
+ if params.get("json_logging") and json_logger:
117
+ json_logger.info(
118
+ json.dumps({"accumulated_shifts": accumulated_shifts.tolist()})
119
+ )
120
+
121
+ return accumulated_shifts
122
+
123
+
124
+ if __name__ == "__main__":
125
+ pass