labdata 0.1.18__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.
- {labdata-0.1.18 → labdata-0.1.20}/PKG-INFO +1 -1
- {labdata-0.1.18 → labdata-0.1.20}/labdata/__init__.py +1 -1
- {labdata-0.1.18 → labdata-0.1.20}/labdata/compute/__init__.py +2 -1
- labdata-0.1.20/labdata/compute/caiman.py +498 -0
- labdata-0.1.20/labdata/compute/pose.py +673 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/compute/utils.py +89 -49
- labdata-0.1.20/labdata/dashboard/activity.py +680 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/drift.py +46 -30
- labdata-0.1.20/labdata/dashboard/ephys.py +656 -0
- labdata-0.1.20/labdata/dashboard/imaging.py +233 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/index.py +29 -3
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/procedures.py +49 -60
- labdata-0.1.20/labdata/dashboard/reads.py +386 -0
- labdata-0.1.20/labdata/dashboard/segmentation.py +1510 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/sorting.py +20 -24
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/video.py +437 -55
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/writes.py +51 -14
- labdata-0.1.20/labdata/ephys_io.py +321 -0
- labdata-0.1.20/labdata/ephys_signal.py +219 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/s3.py +120 -1
- {labdata-0.1.18 → labdata-0.1.20}/labdata/schema/ephys.py +173 -9
- {labdata-0.1.18 → labdata-0.1.20}/labdata/schema/general.py +330 -48
- {labdata-0.1.18 → labdata-0.1.20}/labdata/schema/histology.py +12 -11
- {labdata-0.1.18 → labdata-0.1.20}/labdata/schema/onephoton.py +12 -36
- {labdata-0.1.18 → labdata-0.1.20}/labdata/schema/twophoton.py +7 -21
- labdata-0.1.20/labdata/schema/video.py +840 -0
- labdata-0.1.20/labdata/stacks.py +948 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/utils.py +237 -14
- {labdata-0.1.18 → labdata-0.1.20}/labdata.egg-info/PKG-INFO +1 -1
- {labdata-0.1.18 → labdata-0.1.20}/labdata.egg-info/SOURCES.txt +6 -0
- labdata-0.1.18/labdata/compute/caiman.py +0 -394
- labdata-0.1.18/labdata/compute/pose.py +0 -336
- labdata-0.1.18/labdata/dashboard/segmentation.py +0 -1014
- labdata-0.1.18/labdata/schema/video.py +0 -407
- labdata-0.1.18/labdata/stacks.py +0 -303
- {labdata-0.1.18 → labdata-0.1.20}/LICENSE +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/README.md +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/cli.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/compute/ec2.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/compute/ephys.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/compute/schedulers.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/compute/singularity.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/compute/suite2p.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/copy.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/__init__.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/admin.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/ai_utils.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/compute.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/console.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/decisiontask.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/docstrings.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/notes.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/dashboard/schema_graph.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/export.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/rules/__init__.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/rules/ephys.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/rules/imaging.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/rules/utils.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/schema/__init__.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/schema/procedures.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/schema/tasks.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/schema/utils.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata/widgets.py +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata.egg-info/dependency_links.txt +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata.egg-info/entry_points.txt +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata.egg-info/requires.txt +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/labdata.egg-info/top_level.txt +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/pyproject.toml +0 -0
- {labdata-0.1.18 → labdata-0.1.20}/setup.cfg +0 -0
|
@@ -6,6 +6,7 @@ Tasks can be scheduled and run on dedicated containers using job schedulers.
|
|
|
6
6
|
Available compute tasks:
|
|
7
7
|
- SpksCompute: Spike sorting using Kilosort/Phy via SPKS
|
|
8
8
|
- DeeplabcutCompute: Animal pose estimation using DeepLabCut
|
|
9
|
+
- LightningPoseCompute: Animal pose estimation using Lightning Pose
|
|
9
10
|
|
|
10
11
|
Each compute task can be:
|
|
11
12
|
- Scheduled to run on compute clusters via SLURM/PBS
|
|
@@ -24,6 +25,6 @@ The compute tasks handle:
|
|
|
24
25
|
from .utils import *
|
|
25
26
|
from .singularity import build_singularity_container
|
|
26
27
|
from .ephys import SpksCompute
|
|
27
|
-
from .pose import DeeplabcutCompute
|
|
28
|
+
from .pose import DeeplabcutCompute, LightningPoseCompute
|
|
28
29
|
from .caiman import CaimanCompute
|
|
29
30
|
from .suite2p import Suite2pCompute
|
|
@@ -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
|