labdata 0.1.19__tar.gz → 0.1.20__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 (65) hide show
  1. {labdata-0.1.19 → labdata-0.1.20}/PKG-INFO +1 -1
  2. {labdata-0.1.19 → labdata-0.1.20}/labdata/__init__.py +1 -1
  3. labdata-0.1.20/labdata/compute/caiman.py +498 -0
  4. {labdata-0.1.19 → labdata-0.1.20}/labdata/compute/pose.py +10 -7
  5. {labdata-0.1.19 → labdata-0.1.20}/labdata/compute/utils.py +79 -39
  6. labdata-0.1.20/labdata/dashboard/ephys.py +656 -0
  7. labdata-0.1.20/labdata/dashboard/imaging.py +233 -0
  8. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/index.py +14 -0
  9. labdata-0.1.20/labdata/dashboard/reads.py +386 -0
  10. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/segmentation.py +138 -2
  11. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/video.py +122 -115
  12. labdata-0.1.20/labdata/ephys_io.py +321 -0
  13. labdata-0.1.20/labdata/ephys_signal.py +219 -0
  14. {labdata-0.1.19 → labdata-0.1.20}/labdata/schema/ephys.py +173 -9
  15. {labdata-0.1.19 → labdata-0.1.20}/labdata/schema/general.py +186 -0
  16. {labdata-0.1.19 → labdata-0.1.20}/labdata/schema/histology.py +12 -11
  17. {labdata-0.1.19 → labdata-0.1.20}/labdata/schema/onephoton.py +12 -36
  18. {labdata-0.1.19 → labdata-0.1.20}/labdata/schema/twophoton.py +7 -21
  19. {labdata-0.1.19 → labdata-0.1.20}/labdata/schema/video.py +190 -80
  20. {labdata-0.1.19 → labdata-0.1.20}/labdata/stacks.py +191 -14
  21. {labdata-0.1.19 → labdata-0.1.20}/labdata/utils.py +218 -14
  22. {labdata-0.1.19 → labdata-0.1.20}/labdata.egg-info/PKG-INFO +1 -1
  23. {labdata-0.1.19 → labdata-0.1.20}/labdata.egg-info/SOURCES.txt +5 -0
  24. labdata-0.1.19/labdata/compute/caiman.py +0 -394
  25. {labdata-0.1.19 → labdata-0.1.20}/LICENSE +0 -0
  26. {labdata-0.1.19 → labdata-0.1.20}/README.md +0 -0
  27. {labdata-0.1.19 → labdata-0.1.20}/labdata/cli.py +0 -0
  28. {labdata-0.1.19 → labdata-0.1.20}/labdata/compute/__init__.py +0 -0
  29. {labdata-0.1.19 → labdata-0.1.20}/labdata/compute/ec2.py +0 -0
  30. {labdata-0.1.19 → labdata-0.1.20}/labdata/compute/ephys.py +0 -0
  31. {labdata-0.1.19 → labdata-0.1.20}/labdata/compute/schedulers.py +0 -0
  32. {labdata-0.1.19 → labdata-0.1.20}/labdata/compute/singularity.py +0 -0
  33. {labdata-0.1.19 → labdata-0.1.20}/labdata/compute/suite2p.py +0 -0
  34. {labdata-0.1.19 → labdata-0.1.20}/labdata/copy.py +0 -0
  35. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/__init__.py +0 -0
  36. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/activity.py +0 -0
  37. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/admin.py +0 -0
  38. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/ai_utils.py +0 -0
  39. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/compute.py +0 -0
  40. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/console.py +0 -0
  41. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/decisiontask.py +0 -0
  42. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/docstrings.py +0 -0
  43. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/drift.py +0 -0
  44. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/notes.py +0 -0
  45. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/procedures.py +0 -0
  46. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/schema_graph.py +0 -0
  47. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/sorting.py +0 -0
  48. {labdata-0.1.19 → labdata-0.1.20}/labdata/dashboard/writes.py +0 -0
  49. {labdata-0.1.19 → labdata-0.1.20}/labdata/export.py +0 -0
  50. {labdata-0.1.19 → labdata-0.1.20}/labdata/rules/__init__.py +0 -0
  51. {labdata-0.1.19 → labdata-0.1.20}/labdata/rules/ephys.py +0 -0
  52. {labdata-0.1.19 → labdata-0.1.20}/labdata/rules/imaging.py +0 -0
  53. {labdata-0.1.19 → labdata-0.1.20}/labdata/rules/utils.py +0 -0
  54. {labdata-0.1.19 → labdata-0.1.20}/labdata/s3.py +0 -0
  55. {labdata-0.1.19 → labdata-0.1.20}/labdata/schema/__init__.py +0 -0
  56. {labdata-0.1.19 → labdata-0.1.20}/labdata/schema/procedures.py +0 -0
  57. {labdata-0.1.19 → labdata-0.1.20}/labdata/schema/tasks.py +0 -0
  58. {labdata-0.1.19 → labdata-0.1.20}/labdata/schema/utils.py +0 -0
  59. {labdata-0.1.19 → labdata-0.1.20}/labdata/widgets.py +0 -0
  60. {labdata-0.1.19 → labdata-0.1.20}/labdata.egg-info/dependency_links.txt +0 -0
  61. {labdata-0.1.19 → labdata-0.1.20}/labdata.egg-info/entry_points.txt +0 -0
  62. {labdata-0.1.19 → labdata-0.1.20}/labdata.egg-info/requires.txt +0 -0
  63. {labdata-0.1.19 → labdata-0.1.20}/labdata.egg-info/top_level.txt +0 -0
  64. {labdata-0.1.19 → labdata-0.1.20}/pyproject.toml +0 -0
  65. {labdata-0.1.19 → labdata-0.1.20}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labdata
3
- Version: 0.1.19
3
+ Version: 0.1.20
4
4
  Summary: Package to manage data in experimental neuroscience labs
5
5
  Author-email: Joao Couto <jpcouto@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/jcouto/labdata
@@ -1,4 +1,4 @@
1
- VERSION = "0.1.19"
1
+ VERSION = "0.1.20"
2
2
 
3
3
  from .utils import *
4
4
  from .copy import copy_to_upload_server
@@ -0,0 +1,498 @@
1
+ from ..utils import *
2
+ from .utils import BaseCompute
3
+ from .suite2p import Suite2pCompute
4
+ class CaimanCompute(Suite2pCompute):
5
+ '''Cell segmentation compute class using CaImAn.
6
+
7
+ Works for both one-photon (miniscope) and two-photon datasets. Runs
8
+ inside the ``labdata-caiman`` Apptainer container (CPU-only).
9
+
10
+ The compute pipeline:
11
+
12
+ 1. Identify files and determine dataset type (``Miniscope`` or ``TwoPhoton``).
13
+ 2. Copy the ``zarr.zip`` stack to the scratch directory.
14
+ 3. Run CaImAn CNMF/CNMF-E with the specified parameters.
15
+ 4. Delete memory-mapped intermediate files.
16
+ 5. Upload the HDF5 results file to S3 and insert into ``CellSegmentation``.
17
+
18
+ Miniscope recordings run CNMF-E; two-photon recordings run CNMF, one
19
+ plane at a time on the channel given with ``--channel`` (0 by default),
20
+ and the ROIs of each plane are stored under its ``plane_num``. A job
21
+ submitted with ``--multisession`` segments several sessions of a subject
22
+ together, as ``Suite2pCompute`` does: the frames are concatenated in
23
+ session order, one set of ROIs per plane is found on all of them, and
24
+ the traces, projections and motion shifts are stored per session under
25
+ the same ROI numbers, so cells can be followed across sessions.
26
+
27
+ Parameters are stored in ``CellSegmentationParams`` (shared with
28
+ ``Suite2pCompute`` using the same part tables).
29
+ '''
30
+ container = 'labdata-caiman'
31
+ cuda = False
32
+ name = 'caiman'
33
+ url = 'http://github.com/flatironinstitute/CaImAn'
34
+ def __init__(self, job_id, project=None, allow_s3=None, **kwargs):
35
+ super(Suite2pCompute,self).__init__(job_id, project = project, allow_s3 = allow_s3) # takes BaseCompute init
36
+ self.file_filters = ['.zarr.zip'] # this only runs on zarr.zip for the moment.
37
+ # default parameters
38
+ self.parameters = dict(algorithm_name = 'caiman')
39
+ # will only store these in CellSegmentationParams
40
+ self.parameter_keys = ['pw_rigid','p','gSig','gSig_filt','max_shifts','gSiz','merge_thr','rf','stride','tsub',
41
+ 'ssub','nb','K','min_corr','min_pnr','ssub_B','ring_size_factor',
42
+ 'min_SNR', 'rval_thr', 'use_cnn', 'detrendWin','quantileMin','denoise_dff']
43
+
44
+ self.parameter_set_num = None # identifier in CellSegmentationParams
45
+
46
+ self._init_job()
47
+ if not self.job_id is None:
48
+ self.add_parameter_key()
49
+
50
+ def _secondary_parse(self,arguments,parameter_number = None):
51
+ '''
52
+ Handles parsing the command line interface
53
+ '''
54
+ if not parameter_number is None:
55
+ self.parameters = ((self.schema.CellSegmentationParams() & f'parameter_set_num = {parameter_number}')).fetch(as_dict = True)
56
+ if not len(self.parameters):
57
+ raise(f'Could not find parameter {parameter_number} in CellSegmentationParams.')
58
+ self.parameters = self.parameters[0]
59
+ else:
60
+ import argparse
61
+ parser = argparse.ArgumentParser(
62
+ description = 'Segmentation of imaging datasets using CaImAn.',
63
+ usage = 'caiman -a <SUBJECT> -s <SESSION> -- <PARAMETERS>')
64
+ parser.add_argument('-i','--parameter_set',default = None, type=int, help = 'Parameter set number')
65
+ parser.add_argument('-m','--pwrigid',
66
+ action='store_true', default=False,
67
+ help = "Piecewise-rigid registration")
68
+ parser.add_argument('--max-allowed-shifts',
69
+ action='store', default=[20,20], type = int, nargs = 2,
70
+ help = "maximum allowed shift for motion correction")
71
+ parser.add_argument('-p',
72
+ action='store', default=1, type = int,
73
+ help = "Order of the autoregressive system")
74
+ parser.add_argument('-g','--gsig',
75
+ action='store', default=[6,6], type = int, nargs = 2,
76
+ help = "Expected halfwidth of the neurons in pixels")
77
+ parser.add_argument('--gsig-filt',
78
+ action='store', default=None, type = int, nargs = 2,
79
+ help = "Filter for 1photon data, will highlight the somas for motion correction (defaults to [6, 6] for 1p data).")
80
+ parser.add_argument('--nb',
81
+ action='store', default=None, type = int,
82
+ help = "number of background components (rank); 0 runs CNMF-E. Default 0 for miniscope and 2 for two-photon")
83
+ parser.add_argument('-k', '--k',
84
+ action='store', default=None, type = int,
85
+ help = "[two-photon] number of components per patch for the initialisation; default 4 (unused by CNMF-E)")
86
+ parser.add_argument('--channel',
87
+ action='store', default=0, type = int,
88
+ help = "[two-photon] channel of the stack to segment; default 0")
89
+ parser.add_argument('-r','--rf',
90
+ action='store', default=40, type = int,
91
+ help = "half-size of the patches in pixels. e.g., if rf=40, patches are 80x80")
92
+ parser.add_argument('--stride',
93
+ action='store', default=20, type = int,
94
+ help = "size of the overlap in pixels")
95
+ parser.add_argument('-s','--ring-size',
96
+ action='store', default=1.4, type = float,
97
+ help = "radius of ring is gSiz*ring_size_factor")
98
+ parser.add_argument('--merge-thr',
99
+ action='store', default=.7, type = float,
100
+ help = "merging threshold, max correlation allowed")
101
+ parser.add_argument('--tsub',
102
+ action='store', default=4, type = int,
103
+ help = "downsampling factor in time for initialization, increase if you have memory problems")
104
+ parser.add_argument('--ssub',
105
+ action='store', default=2, type = int,
106
+ help = "downsampling factor in space for initialization, increase if you have memory problems")
107
+ parser.add_argument('--ssub_b',
108
+ action='store', default=2, type = int,
109
+ help = "additional downsampling factor for the background")
110
+ parser.add_argument('--min-corr',
111
+ action='store', default=.8, type = float,
112
+ help = "min peak value from correlation image")
113
+ parser.add_argument('--min-pnr',
114
+ action='store', default=10, type = float,
115
+ help = "min peak to noise ratio")
116
+
117
+ parser.add_argument('--snr_thr',
118
+ action='store', default=7, type = float,
119
+ help = "[cell selection] signal to noise ratio threshold")
120
+ parser.add_argument('--rval-thr',
121
+ action='store', default=0.85, type = float,
122
+ help = "[cell selection] spatial correlation threshold")
123
+ parser.add_argument('--use-cnn',
124
+ action='store_true', default = False,
125
+ help = "[cell selection] use a CNN to help deciding good from bad units")
126
+ parser.add_argument('--denoise-dff',
127
+ action='store_true', default = False,
128
+ help = "[df/f] compute dff on the denoised data")
129
+
130
+ parser.add_argument('--quantile_min',
131
+ action='store', default=8, type = float,
132
+ help = "[df/f] Minimum quantile for df/f detrending")
133
+ parser.add_argument('--detrend_win',
134
+ action='store', default=250, type = int,
135
+ help = "[df/f] Number of frames for detrending")
136
+ parser.add_argument('--roi',
137
+ action='store', default=None, type = int, nargs = 4,
138
+ help = "ROI")
139
+
140
+ args = parser.parse_args(arguments[1:])
141
+
142
+ params = dict(pw_rigid = args.pwrigid,
143
+ max_shifts = args.max_allowed_shifts,
144
+ p = int(args.p),
145
+ gSig = [int(a) for a in args.gsig],
146
+ gSiz = [int(a) for a in 2*np.array(args.gsig) + 1],
147
+ gSig_filt = args.gsig_filt,
148
+ merge_thr = float(args.merge_thr),
149
+ rf = int(args.rf),
150
+ stride = int(args.stride),
151
+ tsub = int(args.tsub),
152
+ ssub = int(args.ssub),
153
+ nb = None if args.nb is None else int(args.nb),
154
+ K = None if args.k is None else int(args.k),
155
+ channel = int(args.channel),
156
+ min_corr = float(args.min_corr),
157
+ min_pnr = float(args.min_pnr),
158
+ ssub_B = int(args.ssub_b),
159
+ ring_size_factor = float(args.ring_size),
160
+ min_SNR = float(args.snr_thr),
161
+ rval_thr = float(args.rval_thr),
162
+ use_cnn = bool(args.use_cnn),
163
+ detrendWin = int(args.detrend_win),
164
+ denoise_dff = bool(args.denoise_dff),
165
+ quantileMin = float(args.quantile_min),
166
+ roi = args.roi)
167
+ if not args.gsig_filt is None:
168
+ params['gSig_filt'] = [int(a) for a in args.gsig_filt],
169
+
170
+ self.parameters = params
171
+
172
+ def _recordings(self):
173
+ """Return the recordings of the job in session order.
174
+
175
+ Returns ``(kind, keys, recordings, frame_rate, n_planes, n_channels)``
176
+ with ``kind`` 'miniscope' or 'two-photon', ``keys`` the dataset dicts
177
+ sorted by session name, which is the order the frames are concatenated
178
+ in, and ``recordings`` the matching table queries. Raises ValueError
179
+ when a dataset has no recording, the datasets mix kinds, or the
180
+ sessions differ in frame rate, planes or channels.
181
+ """
182
+ seskeys = sorted((dict(k) for k in self.dataset_key), key = lambda k: (k['subject_name'], k['session_name']))
183
+ kinds = []
184
+ for k in seskeys:
185
+ if len(self.schema.Miniscope() & k):
186
+ kinds.append('miniscope')
187
+ elif len(self.schema.TwoPhoton() & k):
188
+ kinds.append('two-photon')
189
+ else:
190
+ raise ValueError(f'No miniscope or two-photon recording on record for {k}.')
191
+ if len(set(kinds)) > 1:
192
+ raise ValueError(f'The datasets mix miniscope and two-photon recordings: {list(zip(kinds, seskeys))}.')
193
+ kind = kinds[0]
194
+ table = self.schema.Miniscope if kind == 'miniscope' else self.schema.TwoPhoton
195
+ recordings = [table() & k for k in seskeys]
196
+ rows = [r.fetch1() for r in recordings]
197
+ rates = [float(r['frame_rate']) for r in rows]
198
+ if max(rates) - min(rates) > 1e-3:
199
+ raise ValueError(f'The sessions have different frame rates and cannot be segmented together: {rates}.')
200
+ if kind == 'two-photon':
201
+ planes = {int(r['n_planes']) for r in rows}
202
+ channels = {int(r['n_channels']) for r in rows}
203
+ if len(planes) > 1 or len(channels) > 1:
204
+ raise ValueError(f'The sessions differ in planes {sorted(planes)} or channels {sorted(channels)}.')
205
+ n_planes, n_channels = planes.pop(), channels.pop()
206
+ else:
207
+ n_planes, n_channels = 1, 1
208
+ return kind, seskeys, recordings, rates[0], n_planes, n_channels
209
+
210
+ def _cnmf_parameters(self, kind, params):
211
+ """Return the CNMF parameters for a kind of recording, the stored parameter set on top of the defaults of the kind.
212
+
213
+ Miniscope recordings run CNMF-E (``corr_pnr`` initialisation, no
214
+ background rank, ring model); two-photon recordings run CNMF
215
+ (``greedy_roi`` initialisation, two background components). A stored
216
+ value of None for ``nb``, ``K`` or ``gSig_filt`` keeps the default of
217
+ the kind.
218
+ """
219
+ cnmfparams = dict(motion_correct = True,
220
+ del_duplicates = True, # whether to remove duplicates from initialization
221
+ update_background_components = True, # sometimes setting to False improve the results
222
+ method_deconvolution = 'oasis') # could use 'cvxpy' alternatively
223
+ if kind == 'miniscope':
224
+ cnmfparams.update(gSig_filt = [6,6], # 1p needs this for motion correction (spatial filter to register somas)
225
+ method_init = 'corr_pnr', # use this for 1 photon
226
+ K = None, # for 1p use None
227
+ nb = 0, # number of background components (rank), 0 for CNMFE
228
+ nb_patch = 0,
229
+ low_rank_background = None, # for 1p
230
+ normalize_init = False, # just leave as is
231
+ center_psf = True, # True for 1p
232
+ only_init = True) # set it to True to run CNMF-E
233
+ else:
234
+ cnmfparams.update(gSig_filt = None, # no spatial filter for 2p motion correction
235
+ method_init = 'greedy_roi', # 2p initialisation
236
+ K = 4, # components per patch
237
+ nb = 2, # background components
238
+ nb_patch = 1,
239
+ low_rank_background = True,
240
+ normalize_init = True,
241
+ center_psf = False,
242
+ only_init = False) # run the full CNMF after the initialisation
243
+ excluded = ['detrendWin','denoise_dff','quantileMin','roi','channel']
244
+ for k, v in params.items():
245
+ if k in excluded:
246
+ continue
247
+ if v is None and k in ('nb', 'K', 'gSig_filt'):
248
+ continue # keep the default of the kind
249
+ cnmfparams[k] = v
250
+ if kind == 'miniscope' and cnmfparams.get('gSig_filt') is None:
251
+ cnmfparams['gSig_filt'] = [6,6]
252
+ print('[caiman] Using default gSig_filt: (6,6) because this is 1photon.')
253
+ return cnmfparams
254
+
255
+ def _segment_plane(self, stacks, plane, channel, seskeys, cnmfparams, params, frame_rate, cluster, folder):
256
+ """Motion correct and segment one plane of the sessions' stacks, concatenated in order, and return the rows to store.
257
+
258
+ ``plane`` is None for a miniscope stack. The result is a dict with
259
+ ``n_rois`` and the lists of rows for the plane, motion correction,
260
+ projection, ROI, trace and selection tables, one entry per session
261
+ with that session's frames of the traces and shifts.
262
+ """
263
+ import time
264
+ from ..stacks import export_to_tiff, compute_projections, get_roi_pixels
265
+ from caiman.source_extraction.cnmf.params import CNMFParams
266
+ from caiman.motion_correction import MotionCorrect
267
+ from caiman import save_memmap, load_memmap
268
+ from caiman.source_extraction import cnmf
269
+ folder = Path(folder)
270
+ folder.mkdir(parents = True, exist_ok = True)
271
+ os.environ['CAIMAN_DATA'] = f'{folder}'
272
+ plane_num = 0 if plane is None else int(plane)
273
+ # one tiff sequence per session, in session order; caiman treats the files as one movie
274
+ paths, n_frames = [], []
275
+ for isession, dat in enumerate(stacks):
276
+ paths += export_to_tiff(dat, folder/f'session_{isession:02d}', crop_region = self.parameters.get('roi'),
277
+ plane = plane, channel = channel if plane is not None else None)
278
+ n_frames.append(int(dat.shape[0]))
279
+ offsets = np.cumsum([0, *n_frames])
280
+ kk = ['motion_correct','pw_rigid','max_shifts','gSig_filt']
281
+ parameters = CNMFParams(params_dict = dict({k:cnmfparams[k] for k in kk if k in cnmfparams},
282
+ fr = frame_rate))
283
+ pmotion = parameters.get_group('motion')
284
+
285
+ tstart = time.time()
286
+ mot_correct = MotionCorrect(paths, dview=cluster, **pmotion)
287
+ mot_correct.motion_correct(save_movie=True)
288
+ print(f'[caiman] Plane {plane_num}: done with motion correction in {(time.time() - tstart)/60.} min.')
289
+
290
+ [os.unlink(f) for f in paths] # clear raw converted tiff
291
+ fname_mc = mot_correct.fname_tot_els if cnmfparams['pw_rigid'] else mot_correct.fname_tot_rig
292
+ if pmotion['pw_rigid']:
293
+ bord_px = np.ceil(np.maximum(np.max(np.abs(mot_correct.x_shifts_els)),
294
+ np.max(np.abs(mot_correct.y_shifts_els)))).astype(int)
295
+ else:
296
+ bord_px = np.ceil(np.max(np.abs(mot_correct.shifts_rig))).astype(int)
297
+
298
+ bord_px = 0 if pmotion['border_nan'] == 'copy' else bord_px
299
+ fname_new = save_memmap(fname_mc, base_name='memmap_', order='C',
300
+ border_to_0 = bord_px)
301
+ [os.unlink(f) for f in fname_mc]
302
+ [os.unlink(f) for f in folder.rglob("*chunk_*order_F*")]; # clear motion correction temporary
303
+
304
+ Yr, dims, T = load_memmap(fname_new)
305
+ images = Yr.T.reshape((T,) + dims, order='F')
306
+ if T != offsets[-1]:
307
+ raise ValueError(f'The motion corrected movie has {T} frames; the sessions have {offsets[-1]}.')
308
+
309
+ tstart = time.time()
310
+ parameters.change_params(cnmfparams)
311
+ cnmfe_model = cnmf.CNMF(n_processes = DEFAULT_N_JOBS,
312
+ dview = cluster,
313
+ params = parameters)
314
+ cnmfe_model.fit(images);
315
+ print(f'[caiman] Plane {plane_num}: done with CNMF in {(time.time() - tstart)/60.} min.')
316
+
317
+ quality_params = {'min_SNR': params['min_SNR'],
318
+ 'rval_thr': params['rval_thr'],
319
+ 'use_cnn': params['use_cnn']}
320
+ cnmfe_model.params.change_params(params_dict=quality_params)
321
+ print(f"Evaluating components.")
322
+ cnmfe_model.estimates.evaluate_components(images, cnmfe_model.params, dview=cluster)
323
+ print(f"Computing df/f.")
324
+ cnmfe_model.estimates.detrend_df_f(quantileMin = params['quantileMin'],
325
+ frames_window = int(params['detrendWin']),
326
+ flag_auto = False,
327
+ use_residuals = not params['denoise_dff'])
328
+ print('*****')
329
+ print(f"Plane {plane_num}: total number of components: {len(cnmfe_model.estimates.C)}")
330
+ print(f"Number accepted: {len(cnmfe_model.estimates.idx_components)}")
331
+ print(f"Number rejected: {len(cnmfe_model.estimates.idx_components_bad)}")
332
+ roi_masks = np.array(cnmfe_model.estimates.A.todense()).reshape((*cnmfe_model.dims,-1))
333
+ roi_masks = roi_masks.transpose(2,1,0)
334
+ # the ROIs are the same in every session; each session gets its own frames of the traces and shifts
335
+ rois = []
336
+ for icell,roi in enumerate(roi_masks):
337
+ roi_pixels, roi_pixels_values = get_roi_pixels(roi)
338
+ if len(roi_pixels)>5000:
339
+ print(f"[CaimanCompute] {icell} had an roi with {len(roi_pixels)} pixels. Skipping.")
340
+ continue
341
+ rois.append(dict(roi_num = icell,
342
+ roi_pixels = roi_pixels.astype(int),
343
+ roi_pixels_values = roi_pixels_values.astype(np.float32),
344
+ selection = 1 if icell in cnmfe_model.estimates.idx_components else 0))
345
+ motion = None if params['pw_rigid'] else np.array(mot_correct.shifts_rig).astype(np.float32)
346
+ out = dict(n_rois = len(rois), planedicts = [], motiondicts = [], projdicts = [], roidict = [],
347
+ tracesdict = [], rawtracesdict = [], deconvtracesdict = [], selectiondict = [])
348
+ for key, on, off in zip(seskeys, offsets[:-1], offsets[1:]):
349
+ mean_proj,std_proj,max_proj,corr_proj = compute_projections(images[on:off])
350
+ planekey = dict(key,
351
+ parameter_set_num = self.parameter_set_num,
352
+ plane_num = plane_num)
353
+ out['planedicts'].append(dict(planekey,
354
+ plane_n_rois = len(rois),
355
+ dims = [a for a in mean_proj.shape]))
356
+ if motion is not None:
357
+ out['motiondicts'].append(dict(planekey,
358
+ motion_block_size = 0,
359
+ displacement = motion[on:off]))
360
+ out['projdicts'] += [dict(planekey,
361
+ proj_name = n,
362
+ proj_im = pi) for n,pi in zip(
363
+ ['mean','std','max','lcorr'],
364
+ [mean_proj,std_proj,max_proj,corr_proj])]
365
+ for roi in rois:
366
+ icell = roi['roi_num']
367
+ out['roidict'].append(dict(planekey,
368
+ roi_num = icell,
369
+ roi_pixels = roi['roi_pixels'],
370
+ roi_pixels_values = roi['roi_pixels_values']))
371
+ out['tracesdict'].append(dict(planekey,
372
+ roi_num = icell,
373
+ dff = cnmfe_model.estimates.F_dff[icell].astype(np.float32)[on:off]))
374
+ out['rawtracesdict'].append(dict(planekey,
375
+ roi_num = icell,
376
+ f_trace = cnmfe_model.estimates.C[icell].astype(np.float32)[on:off]))
377
+ out['deconvtracesdict'].append(dict(planekey,
378
+ roi_num = icell,
379
+ deconv = cnmfe_model.estimates.S[icell].astype(np.float32)[on:off]))
380
+ out['selectiondict'].append(dict(planekey,
381
+ roi_num = icell,
382
+ selection_method = 'auto',
383
+ selection = roi['selection']))
384
+ print(f'[caiman] Plane {plane_num}: projections computed.')
385
+ del images, Yr
386
+ if not self.keep_intermediate:
387
+ os.unlink(fname_new) # the motion corrected memmap of this plane
388
+ return out
389
+
390
+ def _compute(self):
391
+ # one dataset, or the sessions of a multisession job segmented together as suite2p does:
392
+ # the frames are concatenated in session order, one set of ROIs per plane is found on all
393
+ # of them, and the traces, projections and motion shifts are stored per session
394
+ import string
395
+ rand = ''.join(np.random.choice([s for s in string.ascii_lowercase + string.digits],9))
396
+ temporary_folder = Path(prefs['scratch_path'])/f'caiman_temporary_{rand}'
397
+ temporary_folder.mkdir(parents = True, exist_ok = True)
398
+ kind, seskeys, recordings, frame_rate, n_planes, n_channels = self._recordings()
399
+ channel = int(self.parameters.get('channel') or 0)
400
+ if not 0 <= channel < n_channels:
401
+ raise ValueError(f'The recording has {n_channels} channel(s); channel {channel} is out of range.')
402
+ parameters = (self.schema.CellSegmentationParams & f'parameter_set_num = {self.parameter_set_num}').fetch1()
403
+ params = json.loads(parameters['parameters_dict'])
404
+ cnmfparams = self._cnmf_parameters(kind, params)
405
+ print(f'[caiman] {kind}: {len(seskeys)} session(s), {n_planes} plane(s), channel {channel} of {n_channels}.', flush = True)
406
+ # downloaded files are removed at the end; files that were local stay
407
+ self.files_existed = all(len((self.schema.File & r).check_if_files_local()[1]) == 0 for r in recordings)
408
+ stacks = [r.open() for r in recordings]
409
+ if len(seskeys) > 1:
410
+ print(f'[caiman] Segmenting {len(seskeys)} sessions together, {sum(int(d.shape[0]) for d in stacks)} frames: ' +
411
+ ', '.join(f"{k['session_name']} ({d.shape[0]})" for k, d in zip(seskeys, stacks)), flush = True)
412
+
413
+ import logging
414
+ logger = logging.getLogger('caiman')
415
+ # Set to logging.INFO if you want more, not very useful output..
416
+ logger.setLevel(logging.WARNING)
417
+ handler = logging.StreamHandler()
418
+ logger.addHandler(handler)
419
+
420
+ n_cpus = DEFAULT_N_JOBS
421
+ cluster = setup_cluster(n_cpus)
422
+ totals = [0 for k in seskeys]
423
+ collected = dict(planedicts = [], motiondicts = [], projdicts = [], roidict = [],
424
+ tracesdict = [], rawtracesdict = [], deconvtracesdict = [], selectiondict = [])
425
+ try:
426
+ for iplane in range(n_planes):
427
+ plane = iplane if kind == 'two-photon' else None
428
+ out = self._segment_plane(stacks, plane, channel, seskeys, cnmfparams, params, frame_rate, cluster,
429
+ temporary_folder/f'plane_{iplane}')
430
+ totals = [t + out['n_rois'] for t in totals]
431
+ for name in collected:
432
+ collected[name] += out[name]
433
+ finally:
434
+ cluster.terminate()
435
+ if not self.keep_intermediate:
436
+ if not self.files_existed:
437
+ print('[caiman] Freeing up space by deleting raw data that was downloaded for this compute task.')
438
+ for r in recordings:
439
+ for f in (self.schema.File & r).get():
440
+ os.unlink(f)
441
+ import caiman
442
+ segmentation_datetime = datetime.now()
443
+ cellsegdicts = [dict(key,
444
+ parameter_set_num = self.parameter_set_num,
445
+ algorithm_version = f'caiman {caiman.__version__}',
446
+ n_rois = n_rois,
447
+ crop_region = self.parameters.get('roi'),
448
+ segmentation_datetime = segmentation_datetime) for key, n_rois in zip(seskeys, totals)]
449
+ self.schema.CellSegmentation.insert(cellsegdicts, allow_direct_insert = True)
450
+ self.schema.CellSegmentation.Plane.insert(collected['planedicts'],allow_direct_insert = True)
451
+ if collected['motiondicts']:
452
+ self.schema.CellSegmentation.MotionCorrection.insert(collected['motiondicts'], allow_direct_insert = True)
453
+ self.schema.CellSegmentation.Projection.insert(collected['projdicts'],allow_direct_insert = True)
454
+ self.schema.CellSegmentation.ROI.insert(collected['roidict'] ,allow_direct_insert = True)
455
+ # Insert traces in parallel to prevent timeout errors from mysql
456
+ parallel_insert(self.schema.schema_project,'CellSegmentation.Traces',collected['tracesdict'], n_jobs = DEFAULT_N_JOBS,
457
+ skip_duplicates = True, ignore_extra_fields = True)
458
+ parallel_insert(self.schema.schema_project,'CellSegmentation.RawTraces',collected['rawtracesdict'], n_jobs = DEFAULT_N_JOBS,
459
+ skip_duplicates = True, ignore_extra_fields = True)
460
+ parallel_insert(self.schema.schema_project,'CellSegmentation.Deconvolved',collected['deconvtracesdict'], n_jobs = DEFAULT_N_JOBS,
461
+ skip_duplicates = True, ignore_extra_fields = True)
462
+ parallel_insert(self.schema.schema_project,'CellSegmentation.Selection',collected['selectiondict'], n_jobs = DEFAULT_N_JOBS,
463
+ skip_duplicates = True, ignore_extra_fields = True)
464
+ if not self.keep_intermediate:
465
+ print(f'[{self.name} job] Removing the temporary folder.')
466
+ import shutil
467
+ shutil.rmtree(temporary_folder)
468
+ else:
469
+ print(f'[{self.name} job] Kept the temporary folder {temporary_folder}.')
470
+ for key in seskeys:
471
+ self.schema.CellSegmentationMetrics().populate(key, display_progress = True)
472
+
473
+ def setup_cluster(n_cpus, cluster=None):
474
+ '''Start (or restart) a CaImAn multiprocessing cluster.
475
+
476
+ If ``cluster`` is not ``None``, the existing cluster is stopped first.
477
+
478
+ Parameters
479
+ ----------
480
+ n_cpus : int
481
+ Number of parallel worker processes.
482
+ cluster : object or None, optional
483
+ Existing CaImAn cluster object to stop before starting a new one.
484
+
485
+ Returns
486
+ -------
487
+ object
488
+ The newly created CaImAn cluster (``dview``).
489
+ '''
490
+ from caiman import stop_server, cluster
491
+ #%% start a cluster for parallel processing (if a cluster already exists it will be closed and a new session will be opened)
492
+ if not cluster is None: # 'locals' contains list of current local variables
493
+ stop_server(dview=cluster)
494
+ _, cluster, n_processes = cluster.setup_cluster(backend='multiprocessing',
495
+ n_processes = n_cpus,
496
+ ignore_preexisting = False)
497
+ print(f"Set up parallelization with {n_processes} processes.")
498
+ return cluster
@@ -177,13 +177,14 @@ class PoseEstimationCompute(BaseCompute):
177
177
  container_name = self.container,
178
178
  code_link = self.url)
179
179
  elif self.parameters['mode'] == 'infer':
180
- datasets = (self.schema.File() & (self.schema.DatasetVideo.File() & self.dataset_key)).fetch(as_dict = True)
180
+ dataset_key = self.one_dataset()
181
+ datasets = (self.schema.File() & (self.schema.DatasetVideo.File() & dataset_key)).fetch(as_dict = True)
181
182
  if not len(datasets):
182
- raise(ValueError(f"Could not find {self.dataset_key}"))
183
+ raise(ValueError(f"Could not find {dataset_key}"))
183
184
  if len(datasets) > 1:
184
185
  # select the video to analyse
185
186
  datasets = (self.schema.File() & (self.schema.DatasetVideo.File() & dict(
186
- self.dataset_key,
187
+ dataset_key,
187
188
  video_name = self.parameters['video_name']))).fetch(as_dict = True)
188
189
  localfiles = self.get_files(datasets)
189
190
  if len(localfiles) > 1:
@@ -196,7 +197,7 @@ class PoseEstimationCompute(BaseCompute):
196
197
  # Save the results to PoseEstimation()
197
198
  toinsert = []
198
199
  for i,b in enumerate(bodyparts):
199
- toinsert.append(dict(self.dataset_key,
200
+ toinsert.append(dict(dataset_key,
200
201
  video_name = video_name,
201
202
  model_num = self.parameters["model_num"],
202
203
  label_name = b,
@@ -521,10 +522,11 @@ def create_lightning_pose_project(parameters, model_num, schema):
521
522
  from natsort import natsorted
522
523
  print(f'Creating model {model_num} from pose_label_set_num {parameters["label_set"]}')
523
524
  label_set = (schema.PoseEstimationLabelSet() & f'pose_label_set_num = {parameters["label_set"]}')
524
- data_path, frames, frame_labels = label_set.export_labeling(model_num = model_num, export_only_labeled = True)
525
+ data_path, frames, frame_labels = label_set.export_labeling(model_num = model_num, export_only_labeled = True, overwrite = True)
525
526
  project_path = data_path.parent.parent
527
+ from ..schema.video import read_labeling_file
526
528
  h5 = next(data_path.glob('CollectedData_*.h5'))
527
- labels = pd.read_hdf(h5)
529
+ labels = read_labeling_file(h5)
528
530
  # the DeepLabCut layout: three header rows and, in the first column, the
529
531
  # image path relative to the project
530
532
  labels.index = ['/'.join(str(v) for v in idx) if isinstance(idx, tuple) else str(idx) for idx in labels.index]
@@ -590,7 +592,8 @@ def create_project(parameters, model_num, schema):
590
592
 
591
593
  project_path,frames,frames_labels = (schema.PoseEstimationLabelSet & parameter_set_key).export_labeling(
592
594
  model_num = model_num,
593
- export_only_labeled = True)
595
+ export_only_labeled = True,
596
+ overwrite = True)
594
597
  print(f'Exporting to {project_path}.')
595
598
  F = frames['frame'].iloc[0]
596
599
  project_path = project_path.parent.parent