fmatoolbox 0.1.2__tar.gz → 0.1.4__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.
- {fmatoolbox-0.1.2/src/fmatoolbox.egg-info → fmatoolbox-0.1.4}/PKG-INFO +5 -3
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/pyproject.toml +6 -1
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/src/fmatoolbox/analysis.py +220 -99
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/src/fmatoolbox/data.py +167 -38
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/src/fmatoolbox/general.py +1 -4
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/src/fmatoolbox/plotting.py +46 -25
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/src/fmatoolbox/regions.py +73 -24
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4/src/fmatoolbox.egg-info}/PKG-INFO +5 -3
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/src/fmatoolbox.egg-info/requires.txt +3 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/LICENSE +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/README.md +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/setup.cfg +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/src/fmatoolbox/__init__.py +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/src/fmatoolbox/exceptions.py +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/src/fmatoolbox.egg-info/SOURCES.txt +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/src/fmatoolbox.egg-info/dependency_links.txt +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.4}/src/fmatoolbox.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fmatoolbox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Python port of FMAToolbox
|
|
5
5
|
Author: Pietro Bozzo
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -11,11 +11,13 @@ Requires-Python: >=3.11
|
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Requires-Dist: numpy
|
|
14
|
+
Requires-Dist: numba
|
|
14
15
|
Requires-Dist: scipy
|
|
15
16
|
Requires-Dist: matplotlib
|
|
16
17
|
Requires-Dist: statsmodels
|
|
17
|
-
|
|
18
|
-
Requires-Dist: scikit-
|
|
18
|
+
Provides-Extra: assemblies
|
|
19
|
+
Requires-Dist: scikit-learn; extra == "assemblies"
|
|
20
|
+
Requires-Dist: scikit-image; extra == "assemblies"
|
|
19
21
|
Dynamic: license-file
|
|
20
22
|
|
|
21
23
|
# FMAToolbox (Python Port)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fmatoolbox"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Python port of FMAToolbox"
|
|
9
9
|
authors = [{name = "Pietro Bozzo"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -13,9 +13,14 @@ license = "GPL-3.0-or-later"
|
|
|
13
13
|
requires-python = ">=3.11"
|
|
14
14
|
dependencies = [
|
|
15
15
|
"numpy",
|
|
16
|
+
"numba",
|
|
16
17
|
"scipy",
|
|
17
18
|
"matplotlib",
|
|
18
19
|
"statsmodels",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
assemblies = [
|
|
19
24
|
"scikit-learn",
|
|
20
25
|
"scikit-image",
|
|
21
26
|
]
|
|
@@ -3,20 +3,19 @@
|
|
|
3
3
|
import fmatoolbox.general
|
|
4
4
|
import numpy as np
|
|
5
5
|
import scipy as sp
|
|
6
|
-
import sklearn.decomposition as skdc
|
|
7
|
-
import skimage.filters as skif
|
|
8
6
|
import joblib
|
|
9
7
|
import statsmodels.stats.multitest
|
|
10
8
|
import warnings
|
|
9
|
+
from numba import njit
|
|
11
10
|
from typing import Callable
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
def istantaneousRate(samples, start:float=None, stop:float=None, bin:float=None, step:int=None, smooth:float=None, g_range:tuple[int,int]=None):
|
|
15
|
-
|
|
14
|
+
"""
|
|
16
15
|
estimate the istantaneous rate of a point process from a realization of its time stamps, e.g., the firing rate from spike times
|
|
17
16
|
|
|
18
17
|
arguments:
|
|
19
|
-
samples (n
|
|
18
|
+
samples (n,) float | (n,2) float, every row is either [sample time (s)] or [sample time (s), process id]; if 1d, interpreted as (n,1)
|
|
20
19
|
start float = min(samples[:,0]) s, time to start count at
|
|
21
20
|
stop float = max(samples[:,0]) s, time to stop count at
|
|
22
21
|
bin float = 0.05 s, time bin to count samples
|
|
@@ -27,7 +26,7 @@ def istantaneousRate(samples, start:float=None, stop:float=None, bin:float=None,
|
|
|
27
26
|
|
|
28
27
|
output:
|
|
29
28
|
rate (:,g+1) float, every row is [time stamp, rates for g processes], g is 1 if 'samples' has just one column
|
|
30
|
-
|
|
29
|
+
"""
|
|
31
30
|
|
|
32
31
|
# validate input
|
|
33
32
|
samples = np.asarray(samples)
|
|
@@ -78,107 +77,41 @@ def istantaneousRate(samples, start:float=None, stop:float=None, bin:float=None,
|
|
|
78
77
|
rate = np.array(rate).T / bin
|
|
79
78
|
|
|
80
79
|
# make time axis
|
|
81
|
-
t = bin/step/2 + np.linspace(0,(n_bins-1)*bin,n_bins*step)
|
|
80
|
+
t = start + bin/step/2 + np.linspace(0,(n_bins-1)*bin,n_bins*step)
|
|
82
81
|
|
|
83
82
|
# apply smoothing
|
|
84
83
|
if smooth is not None:
|
|
85
|
-
|
|
84
|
+
rate = sp.ndimage.gaussian_filter(rate,smooth,axes=0)
|
|
86
85
|
|
|
87
86
|
return np.column_stack((t,rate))
|
|
88
87
|
|
|
89
88
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# arguments:
|
|
94
|
-
# spikes (n,:) float, every row is either [spike time] (s) or [spike time, unit id]
|
|
95
|
-
# start float = min(spike_times) s, time to start count at
|
|
96
|
-
# stop float = max(spike_times) s, time to stop count at
|
|
97
|
-
# bin_size float = 0.05 s, time bin to count spikes
|
|
98
|
-
# step int = 1, firing rate is computed in windows of length 'bin_size' and overlap 'bin_size' / 'step', default is no overlap
|
|
99
|
-
# smooth float = None, gaussian kernel std for smoothing over time
|
|
100
|
-
# u_range (2) int, range of units to consider in computation, default is [min(spikes[:,1]), max(spikes[:,1])]
|
|
101
|
-
# (boundaries included, only for 2-columns 'spikes')
|
|
102
|
-
#
|
|
103
|
-
# output:
|
|
104
|
-
# firing_rate (:,m+1) float, every row is [time stamp, firing rates for m units], m is 1 if spikes has just one column
|
|
105
|
-
|
|
106
|
-
warnings.warn("firingRate is deprecated, use istantaneousRate instead!", DeprecationWarning)
|
|
107
|
-
|
|
108
|
-
# validate input
|
|
109
|
-
spikes = np.asarray(spikes)
|
|
110
|
-
if bin_size is None: bin_size = 0.05
|
|
111
|
-
if step is None: step = 1
|
|
112
|
-
if step % 1 or step == 0:
|
|
113
|
-
raise ValueError("'step' must be a non-zero integer")
|
|
114
|
-
|
|
115
|
-
units = []
|
|
116
|
-
if spikes.ndim == 1:
|
|
117
|
-
times = spikes
|
|
118
|
-
elif spikes.shape[1] == 1:
|
|
119
|
-
times = spikes.reshape(-1)
|
|
120
|
-
else:
|
|
121
|
-
times = spikes[:,0]
|
|
122
|
-
units = spikes[:,1]
|
|
123
|
-
if u_range is None:
|
|
124
|
-
u_range = np.unique(units).astype(int)[[0,-1]]
|
|
125
|
-
else:
|
|
126
|
-
u_range = np.array(u_range,dtype=int)
|
|
127
|
-
|
|
128
|
-
# build time bins, overlapping if requested
|
|
129
|
-
if start is None:
|
|
130
|
-
start = times.min()
|
|
131
|
-
if stop is None:
|
|
132
|
-
stop = times.max()
|
|
133
|
-
time_bins = [np.arange(start,stop+bin_size,bin_size) + i*bin_size/step for i in range(step)]
|
|
134
|
-
|
|
135
|
-
if len(units) == 0:
|
|
136
|
-
# compute firing rate once
|
|
137
|
-
firing_rate = [np.histogram(times,bins=tb)[0] for tb in time_bins]
|
|
138
|
-
# flatten and convert to Hz
|
|
139
|
-
firing_rate = np.array(firing_rate).reshape((-1,1),order='F') / bin_size
|
|
140
|
-
else:
|
|
141
|
-
# compute firing rate once per unit and stack into a matrix
|
|
142
|
-
firing_rate = []
|
|
143
|
-
for u in range(u_range[0],u_range[-1]+1):
|
|
144
|
-
fr = [np.histogram(times[units==u],bins=tb)[0] for tb in time_bins]
|
|
145
|
-
firing_rate.append(np.array(fr).flatten('F'))
|
|
146
|
-
firing_rate = np.array(firing_rate).T / bin_size
|
|
147
|
-
|
|
148
|
-
# center times into time bins
|
|
149
|
-
time_bins = [(tb[:-1] + tb[1:]) / 2 for tb in time_bins]
|
|
150
|
-
time_bins = np.array(time_bins).reshape((-1,1),order='F')
|
|
151
|
-
|
|
152
|
-
# apply smoothing
|
|
153
|
-
if smooth is not None:
|
|
154
|
-
firing_rate = sp.ndimage.gaussian_filter(firing_rate,smooth,axes=0)
|
|
155
|
-
|
|
156
|
-
return np.concatenate((time_bins,firing_rate),1)
|
|
89
|
+
def PETH(samples, events, groups=None, g_range:tuple[int,int]=None, limits:tuple[float,float]=None, n_bins:int=None, bin:float=None, step:int=None, smooth:float=None, fast:bool=False):
|
|
90
|
+
'''
|
|
91
|
+
compute peri-event time histogram of a signal relative to synchronizing events
|
|
157
92
|
|
|
93
|
+
arguments:
|
|
94
|
+
samples float, either:
|
|
95
|
+
- (n) array of time stamps (s), describing a point process
|
|
96
|
+
- (n,:) array, where each row is [time stamp (s), value1, ...], describing one or more continous signals
|
|
97
|
+
events (m) float, synchronizing events' times, their order is maintained in the output 'mat'
|
|
98
|
+
groups (n) int, grouping indeces for samples, to compute separate PETHs (only for point process 'samples')
|
|
99
|
+
g_range (2) int = [0,max(groups)], min and max group id
|
|
100
|
+
groups (:) int, grouping indeces for samples, to compute separate PETHs (only for point process 'samples')
|
|
101
|
+
limits (2) float = [-0.5,0.5] (s), defines a window around events, divided into 'n_bins' time bins to compute PETH
|
|
102
|
+
n_bins float = 101, number of time bins around event times
|
|
103
|
+
bin float = None (s), bin size, can be given instead of 'n_bins', which will be deduced from 'bin' and 'limits'
|
|
104
|
+
step int = 1, only for point-process 'samples', for values higher than 1, time bins inside a window will overlap, yielding:
|
|
105
|
+
- bin_size of (limit[1]-limit[0]) / n_bins, unchanged
|
|
106
|
+
- time resolution of bin_size / step
|
|
107
|
+
smooth float = None, gaussian kernel std to smooth mean output 'm' over time, has no effect on 'mat'
|
|
108
|
+
fast bool = False, if True, 'samples' must be time sorted to save computation time (only for point process 'samples')
|
|
158
109
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# - (n) array of time stamps (s), describing a point process
|
|
165
|
-
# - (n,:) array, where each row is [time stamp (s), value1, ...], describing one or more continous signals
|
|
166
|
-
# events (m) float, synchronizing events' times, their order is maintained in the output 'mat'
|
|
167
|
-
# groups (n) int, grouping indeces for samples, to compute separate PETHs (only for point process 'samples')
|
|
168
|
-
# g_range (2) int = [0,max(groups)], min and max group id
|
|
169
|
-
# groups (:) int, grouping indeces for samples, to compute separate PETHs (only for point process 'samples')
|
|
170
|
-
# limits (2) float = [-0.5,0.5] (s), defines a window around events, divided into 'n_bins' time bins to compute PETH
|
|
171
|
-
# n_bins float = 101, number of time bins around event times
|
|
172
|
-
# bin float = None (s), bin size, can be given instead of 'n_bins', which will be deduced from 'bin' and 'limits'
|
|
173
|
-
# step int = 1, only for point-process 'samples', for values higher than 1, time bins inside a window will overlap, yielding:
|
|
174
|
-
# - bin_size of (limit[1]-limit[0]) / n_bins, unchanged
|
|
175
|
-
# - time resolution of bin_size / step
|
|
176
|
-
# fast bool = False, if True, 'samples' must be time sorted to save computation time (only for point process 'samples')
|
|
177
|
-
#
|
|
178
|
-
# output:
|
|
179
|
-
# mat (m,n_bins) float, every row corresponds to samples centered on an event
|
|
180
|
-
# t (n_bins) float, times (s)
|
|
181
|
-
# m (n_bins) float, average samples across events
|
|
110
|
+
output:
|
|
111
|
+
mat (m,n_bins) float, every row corresponds to samples centered on an event
|
|
112
|
+
t (n_bins) float, times (s)
|
|
113
|
+
m (n_bins) float, average of 'samples' across events
|
|
114
|
+
'''
|
|
182
115
|
|
|
183
116
|
# default values
|
|
184
117
|
samples = np.array(samples,ndmin=1)
|
|
@@ -233,7 +166,7 @@ def PETH(samples, events, groups=None, g_range:tuple[int,int]=None, limits:tuple
|
|
|
233
166
|
valid = counts > 0
|
|
234
167
|
# repeat event indices according to how many samples they match
|
|
235
168
|
event_idx = np.repeat(np.arange(len(events))[valid],counts[valid])
|
|
236
|
-
sample_idx = np.concatenate([np.arange(l,r) for l, r in zip(left[valid],right[valid])])
|
|
169
|
+
sample_idx = np.concatenate([np.arange(l,r) for l, r in zip(left[valid],right[valid])]) if np.any(valid) else []
|
|
237
170
|
# build lists of matches
|
|
238
171
|
e_rep = events[event_idx]
|
|
239
172
|
s_sel = samples[sample_idx]
|
|
@@ -258,11 +191,193 @@ def PETH(samples, events, groups=None, g_range:tuple[int,int]=None, limits:tuple
|
|
|
258
191
|
if squeeze:
|
|
259
192
|
mat = mat.reshape(mat.shape[:2])
|
|
260
193
|
|
|
261
|
-
m = np.
|
|
194
|
+
m = np.nanmean(mat,axis=0)
|
|
195
|
+
if smooth is not None:
|
|
196
|
+
m = sp.ndimage.gaussian_filter(m,smooth,axes=0)
|
|
262
197
|
|
|
263
198
|
return mat, t, m
|
|
264
199
|
|
|
265
200
|
|
|
201
|
+
def jointPETH(samples, events, bin:float=None, step:int=None, n_bins=None, limits=None, return_peths:bool=None, smooth:float=None):
|
|
202
|
+
'''
|
|
203
|
+
compute joint peri-event time histogram of two signals A and B relative to synchronizing events, i.e., the co-occurrence rate (Hz)
|
|
204
|
+
for different time-lag pairs applied to A and B
|
|
205
|
+
|
|
206
|
+
arguments:
|
|
207
|
+
samples (2,) tuple, whose elements can be either:
|
|
208
|
+
- (n) float of time stamps (s), describing a point process
|
|
209
|
+
- (n,:) float, where each row is [time stamp (s), value1, ...], describing one or more continous signals
|
|
210
|
+
events (m) float, synchronizing events' times, their order is maintained in the output 'mat' /!\
|
|
211
|
+
bin float = 0.05 (s), bin size to compute PETHs
|
|
212
|
+
n_bins int | (2,) int = 101, number of time bins around event times, either one for both PETHS or two values
|
|
213
|
+
limits (2,) float | (2,2) float = None, can be given instead of 'n_bins', which will be deduced from 'bin' and 'limits', and
|
|
214
|
+
defines a [start, stop] window (in s) around events, divided into time bins of size 'bin' to compute PETHs; can be a
|
|
215
|
+
single tuple for both PETHS or a tuple of tuples
|
|
216
|
+
step int = 1, only for point-process 'samples', for values higher than 1, time bins inside each window will overlap, yielding:
|
|
217
|
+
- bin_size of (limit[1]-limit[0]) / n_bins, unchanged
|
|
218
|
+
- time resolution of bin_size / step
|
|
219
|
+
smooth float = None, gaussian kernel std to smooth mean output 'peth' over time, has no effect on other outputs
|
|
220
|
+
ARGS YET TO TEST
|
|
221
|
+
groups (n) int, grouping indeces for samples, to compute separate PETHs (only for point process 'samples')
|
|
222
|
+
g_range (2) int = [0,max(groups)], min and max group id
|
|
223
|
+
groups (:) int, grouping indeces for samples, to compute separate PETHs (only for point process 'samples')
|
|
224
|
+
fast bool = False, if True, 'samples' must be time sorted to save computation time (only for point process 'samples')
|
|
225
|
+
|
|
226
|
+
output:
|
|
227
|
+
joint (n_bins0,n_bins1) float, lagged co-occurrency rate (Hz), dimensions correspond to time lags w.r.t. 'events' for
|
|
228
|
+
A and B, respectively
|
|
229
|
+
null (n_bins0,n_bins1) float, null model for 'joint' in case A and B occurr independently, conditioned on 'events';
|
|
230
|
+
i.e., their conditioned co-occurrence probability is the product of their average event PETHs
|
|
231
|
+
difference (n_bins0,n_bins1) float, 'joint' - 'null', co-occurrence rate (Hz) unexplained by the null model, values far from 0 suggest
|
|
232
|
+
that conditioning on 'events' is not sufficient to explain the co-occurrence of the two signals
|
|
233
|
+
'''
|
|
234
|
+
|
|
235
|
+
# 1. ensure PETHs are produced with the same bin size
|
|
236
|
+
isscalar = lambda x: x is None or np.isscalar(x)
|
|
237
|
+
if bin is None: bin = 0.05
|
|
238
|
+
# duplicate single-peth inputs for two peths
|
|
239
|
+
if isscalar(n_bins):
|
|
240
|
+
n_bins = [n_bins,n_bins]
|
|
241
|
+
else:
|
|
242
|
+
n_bins = list(n_bins)
|
|
243
|
+
if limits is None:
|
|
244
|
+
limits = [None,None]
|
|
245
|
+
else:
|
|
246
|
+
limits = [list(limits),list(limits)] if np.isscalar(limits[0]) and np.isscalar(limits[1]) else [l if isscalar(l) else list(l) for l in limits]
|
|
247
|
+
# deduce 'n_bins' or 'limits'
|
|
248
|
+
for i in [0,1]:
|
|
249
|
+
if n_bins[i] is None:
|
|
250
|
+
if limits[i] is None:
|
|
251
|
+
n_bins[i] = 101
|
|
252
|
+
limits[i] = (-bin * n_bins[i] / 2, bin * n_bins[i] / 2)
|
|
253
|
+
else:
|
|
254
|
+
window = np.diff(limits[i]).item()
|
|
255
|
+
n_bins[i] = np.ceil(window / bin).astype(int)
|
|
256
|
+
extra = n_bins[i] * bin - window
|
|
257
|
+
limits[i][0] -= extra / 2
|
|
258
|
+
limits[i][1] += extra / 2
|
|
259
|
+
else:
|
|
260
|
+
limits[i] = (-bin * n_bins[i] / 2, bin * n_bins[i] / 2)
|
|
261
|
+
|
|
262
|
+
# 2. PETHs
|
|
263
|
+
mat0, t0, mean0 = PETH(samples[0],events,limits=limits[0],n_bins=n_bins[0],bin=bin,step=step)
|
|
264
|
+
mat1, t1, mean1 = PETH(samples[1],events,limits=limits[1],n_bins=n_bins[1],bin=bin,step=step)
|
|
265
|
+
dt = t0[1] - t0[0]
|
|
266
|
+
if not np.isclose(dt, t1[1]-t1[0]):
|
|
267
|
+
raise ValueError('time resolution of the two PETHs must coincide')
|
|
268
|
+
|
|
269
|
+
# observed co-occurrence between 'samples0' and 'samples1' at every time bin
|
|
270
|
+
joint = (mat0.T @ mat1) / len(events)
|
|
271
|
+
# expected co-occurrence if the signals were independent: product
|
|
272
|
+
null = np.outer(mean0, mean1)
|
|
273
|
+
# convert to Hz
|
|
274
|
+
joint = np.sqrt(joint) / dt
|
|
275
|
+
null = np.sqrt(null) / dt
|
|
276
|
+
# difference(i,j) = sqrt[ mean_e( mat0(e,i) * mat1(e,j) ) ] - sqrt[ mean0(i) * mean1(j) ]
|
|
277
|
+
difference = joint - null
|
|
278
|
+
|
|
279
|
+
if return_peths:
|
|
280
|
+
if smooth is not None:
|
|
281
|
+
# recompute with smoothing
|
|
282
|
+
_, _, mean0 = PETH(samples[0],events,limits=limits[0],n_bins=n_bins[0],bin=bin,step=step,smooth=smooth)
|
|
283
|
+
_, _, mean1 = PETH(samples[1],events,limits=limits[1],n_bins=n_bins[1],bin=bin,step=step,smooth=smooth)
|
|
284
|
+
mean0 /= dt
|
|
285
|
+
mean1 /= dt
|
|
286
|
+
return joint, null, difference, t0, t1, mean0, mean1
|
|
287
|
+
return joint, null, difference, t0, t1
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@njit
|
|
291
|
+
def _ccg_numba(times, proc, nproc, bin_width, lag_start, lag_stop):
|
|
292
|
+
# ccg: (reference process, target process, lag bin)
|
|
293
|
+
|
|
294
|
+
n = len(times)
|
|
295
|
+
nbins = int(np.ceil((lag_stop - lag_start) / bin_width))
|
|
296
|
+
inv_bin = nbins / (lag_stop - lag_start) # faster than dividing at every iteration
|
|
297
|
+
|
|
298
|
+
n_samples = np.zeros(nproc,dtype=np.int64)
|
|
299
|
+
ccg = np.zeros((nproc,nproc,nbins),dtype=np.int64)
|
|
300
|
+
for i in range(n):
|
|
301
|
+
ti = times[i]
|
|
302
|
+
pi = proc[i]
|
|
303
|
+
n_samples[pi] += 1
|
|
304
|
+
|
|
305
|
+
# find first event that can contribute, starting from next event
|
|
306
|
+
j0 = i + 1
|
|
307
|
+
while j0 < n and times[j0] - ti < lag_start:
|
|
308
|
+
j0 += 1
|
|
309
|
+
j = j0
|
|
310
|
+
|
|
311
|
+
while j < n:
|
|
312
|
+
dt = times[j] - ti
|
|
313
|
+
if dt >= lag_stop:
|
|
314
|
+
break
|
|
315
|
+
pj = proc[j]
|
|
316
|
+
|
|
317
|
+
# positive lag contribution
|
|
318
|
+
if dt >= lag_start:
|
|
319
|
+
b = int((dt - lag_start) * inv_bin)
|
|
320
|
+
if 0 <= b < nbins:
|
|
321
|
+
ccg[pi,pj,b] += 1
|
|
322
|
+
|
|
323
|
+
# negative lag contribution (swap reference and target)
|
|
324
|
+
if -dt >= lag_start and -dt < lag_stop:
|
|
325
|
+
b = int((-dt - lag_start) * inv_bin)
|
|
326
|
+
if 0 <= b < nbins:
|
|
327
|
+
ccg[pj,pi,b] += 1
|
|
328
|
+
|
|
329
|
+
j += 1
|
|
330
|
+
|
|
331
|
+
return ccg, n_samples
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def CCG(samples, bin:float=None, limits:tuple[float,float]=None, fast:bool=None, norm:str=None):
|
|
335
|
+
'''
|
|
336
|
+
compute cross-correlograms from time stamps generated by point processes
|
|
337
|
+
|
|
338
|
+
arguments:
|
|
339
|
+
samples (:,) float | (:,2) float, every row is either [sample time (s)] or [sample time (s), process id]; if 1d, interpreted as (n,1)
|
|
340
|
+
bin float = 0.05 s, time bin to count samples
|
|
341
|
+
limits (2) float = [-0.5,0.5] (s), defines a window around time stamps, divided into time bins to compute CCG
|
|
342
|
+
fast bool = False, if True, 'samples' must be time sorted to save computation time
|
|
343
|
+
norm str = {'rate','count'}, determines unit of measurement of 'ccg'
|
|
344
|
+
|
|
345
|
+
output:
|
|
346
|
+
ccg DESCRIBE
|
|
347
|
+
t DESCRIBE
|
|
348
|
+
'''
|
|
349
|
+
|
|
350
|
+
if bin is None: bin = 0.05
|
|
351
|
+
if limits is None: limits = (-0.5,0.5)
|
|
352
|
+
if norm is None: norm = 'rate'
|
|
353
|
+
|
|
354
|
+
samples = np.asarray(samples)
|
|
355
|
+
if samples.ndim == 1:
|
|
356
|
+
times = samples.astype(np.float64)
|
|
357
|
+
id = np.zeros(len(times),dtype=np.int64)
|
|
358
|
+
elif samples.shape[1] == 2:
|
|
359
|
+
times = samples[:,0].astype(np.float64)
|
|
360
|
+
id = samples[:,1].astype(np.int64)
|
|
361
|
+
else:
|
|
362
|
+
raise ValueError("'samples' must be (n,) or (n,2)")
|
|
363
|
+
nproc = id.max() + 1
|
|
364
|
+
|
|
365
|
+
# sort by time
|
|
366
|
+
if not fast:
|
|
367
|
+
order = np.argsort(times)
|
|
368
|
+
times = times[order]
|
|
369
|
+
id = id[order]
|
|
370
|
+
|
|
371
|
+
ccg, n_samples = _ccg_numba(times, id, nproc, float(bin), float(limits[0]), float(limits[1]))
|
|
372
|
+
edges = np.linspace(limits[0],limits[1],ccg.shape[2]+1)
|
|
373
|
+
t = (edges[1:] + edges[:-1]) / 2
|
|
374
|
+
|
|
375
|
+
if norm == 'rate':
|
|
376
|
+
ccg = ccg.astype(float) / (n_samples[:,None,None] * bin)
|
|
377
|
+
|
|
378
|
+
return ccg, t
|
|
379
|
+
|
|
380
|
+
|
|
266
381
|
def avalanchesFromProfile(x, threshold:float, time_step:float, t0:float=0):
|
|
267
382
|
# compute avalanches' sizes and [start,stop] intervals from a time series
|
|
268
383
|
#
|
|
@@ -304,6 +419,12 @@ def avalanchesFromProfile(x, threshold:float, time_step:float, t0:float=0):
|
|
|
304
419
|
def cellAssembliesICA(spikes, window:float=None, when=None, drop_mix:bool=False):
|
|
305
420
|
# detect assemblies from spike trains with PCA + ICA
|
|
306
421
|
|
|
422
|
+
try:
|
|
423
|
+
import sklearn.decomposition as skdc
|
|
424
|
+
import skimage.filters as skif
|
|
425
|
+
except ImportError as e:
|
|
426
|
+
raise ImportError('cellAssembliesICA requires scikit-learn, did you do: pip install "fmatoolbox[assemblies]" ?') from e
|
|
427
|
+
|
|
307
428
|
if window is None: window = 0.025
|
|
308
429
|
|
|
309
430
|
raster = firingRate(spikes,bin_size=window)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
'''
|
|
2
|
-
|
|
1
|
+
''' Functions to load data for a recording session '''
|
|
2
|
+
|
|
3
3
|
import pathlib
|
|
4
4
|
import scipy.io
|
|
5
5
|
import numpy as np
|
|
@@ -10,27 +10,32 @@ import re
|
|
|
10
10
|
import fmatoolbox.exceptions
|
|
11
11
|
import collections
|
|
12
12
|
import concurrent.futures
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
13
|
+
import xml.etree.ElementTree
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
from os import PathLike
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def loadSpikeTimes(session:str|PathLike[str], output:str='dict', anat_file:str=None, return_elec:bool=False, return_loc:bool=False, reload:bool=False):
|
|
19
|
+
"""
|
|
20
|
+
load spikes for a session
|
|
21
|
+
|
|
22
|
+
arguments:
|
|
23
|
+
session file, path to session .xml file, spike files must be in session folder
|
|
24
|
+
output string = None, determines output type, either:
|
|
25
|
+
- 'dict', dictionary of spike times per unit
|
|
26
|
+
- 'compact', (:,2) array of [timestamps, unit ids]
|
|
27
|
+
- 'full', (:,2) array of [timestamps, electrode groups, clusters]
|
|
28
|
+
- 'regions', dict of {region id: dict}, entries are {'spikes': region spikes, 'units': region units}
|
|
29
|
+
anat_file str = None, DESCRIBE
|
|
30
|
+
return_elec bool = False, if true, return cluster_loc
|
|
31
|
+
return_loc bool = False, if true, return cluster_loc
|
|
32
|
+
reload bool = False, load original files, bypassing Regions/<basename>_spikes.npz backup (for 'output' = 'regions')
|
|
33
|
+
|
|
34
|
+
output:
|
|
35
|
+
spikes (see 'output')
|
|
36
|
+
electrodes (n) float, optional, electrode id per unit
|
|
37
|
+
cluster_loc (n) float, optional, index of max spike-amplitude cluster per unit
|
|
38
|
+
"""
|
|
34
39
|
|
|
35
40
|
file_root = pathlib.Path(session).with_suffix('')
|
|
36
41
|
cell_metrics_file = file_root.with_suffix('.cell_metrics.cellinfo.mat')
|
|
@@ -50,7 +55,7 @@ def loadSpikeTimes(session:str, output:str='dict', anat_file:str=None, return_el
|
|
|
50
55
|
return out[:2+return_loc:2-return_elec]
|
|
51
56
|
|
|
52
57
|
|
|
53
|
-
def loadCellMetricsFile(session:str, output:str='dict', anat_file:str=None, return_extra:bool=False, reload:bool=False):
|
|
58
|
+
def loadCellMetricsFile(session:str|PathLike[str], output:str='dict', anat_file:str=None, return_extra:bool=False, reload:bool=False):
|
|
54
59
|
|
|
55
60
|
file_root = pathlib.Path(session).with_suffix('')
|
|
56
61
|
|
|
@@ -92,7 +97,7 @@ def loadCellMetricsFile(session:str, output:str='dict', anat_file:str=None, retu
|
|
|
92
97
|
if anat_file is None:
|
|
93
98
|
raise ValueError("'anat_file' must be provided when 'output' = 'regions'")
|
|
94
99
|
anat = loadAnatomyFile(anat_file)
|
|
95
|
-
anat = anat[anat['rat'] == int(file_root.stem[3:6])]
|
|
100
|
+
anat = anat[anat['rat'] == int(file_root.stem[3:6])] # keep rat of interest, deduced from file name
|
|
96
101
|
ids = np.unique(anat['region'])
|
|
97
102
|
spikes = {str(id): {} for id in ids}
|
|
98
103
|
for id in ids:
|
|
@@ -108,7 +113,7 @@ def loadCellMetricsFile(session:str, output:str='dict', anat_file:str=None, retu
|
|
|
108
113
|
s = np.concatenate(s)
|
|
109
114
|
spikes[id]['spikes'] = s[s[:,0].argsort()]
|
|
110
115
|
else:
|
|
111
|
-
spikes[id]['spikes'] = np.
|
|
116
|
+
spikes[id]['spikes'] = np.empty((0,2))
|
|
112
117
|
# save
|
|
113
118
|
saveRegionSpikes(spike_file,spikes)
|
|
114
119
|
|
|
@@ -120,13 +125,19 @@ def loadCellMetricsFile(session:str, output:str='dict', anat_file:str=None, retu
|
|
|
120
125
|
return spikes
|
|
121
126
|
|
|
122
127
|
|
|
123
|
-
def loadCluFiles(session:str, rate:float=
|
|
128
|
+
def loadCluFiles(session:str|PathLike[str], rate:float=None, output:str='dict', anat_file:str=None, reload:bool=False):
|
|
124
129
|
|
|
125
130
|
if output not in ['dict','compact','full','regions']:
|
|
126
131
|
raise ValueError("'output' must be 'dict', 'compact', 'full', or 'regions'")
|
|
127
132
|
|
|
128
|
-
# load
|
|
133
|
+
# load parameters n_channels from .xml
|
|
129
134
|
froot = pathlib.Path(session).with_suffix('')
|
|
135
|
+
if rate is None:
|
|
136
|
+
tree = xml.etree.ElementTree.parse(froot.with_suffix(".xml"))
|
|
137
|
+
root = tree.getroot()
|
|
138
|
+
rate = float(root.find(".//samplingRate").text)
|
|
139
|
+
|
|
140
|
+
# load .res and .clu files
|
|
130
141
|
res_files = {int(p.name[len(froot.name)+5:]): p for p in froot.parent.iterdir()
|
|
131
142
|
if p.is_file() and p.name.startswith(froot.name + '.res.') and p.name[len(froot.name)+5:].isdigit()}
|
|
132
143
|
clu_files = {int(p.name[len(froot.name)+5:]): p for p in froot.parent.iterdir()
|
|
@@ -203,7 +214,7 @@ def loadCluFiles(session:str, rate:float=20000, output:str='dict', anat_file:str
|
|
|
203
214
|
return spikes
|
|
204
215
|
|
|
205
216
|
|
|
206
|
-
def loadAnatomyFile(file_path):
|
|
217
|
+
def loadAnatomyFile(file_path:str|PathLike[str]):
|
|
207
218
|
# load .anat file, whose columns must be [rat, electrode, brain region] (comma separated)
|
|
208
219
|
#
|
|
209
220
|
# arguments:
|
|
@@ -212,7 +223,7 @@ def loadAnatomyFile(file_path):
|
|
|
212
223
|
return np.genfromtxt(file_path,delimiter=",",comments="%",dtype=[("rat",int),("electrode",int),("region","U50")])
|
|
213
224
|
|
|
214
225
|
|
|
215
|
-
def loadRegionSpikes(file):
|
|
226
|
+
def loadRegionSpikes(file:str|PathLike[str]):
|
|
216
227
|
spike_npz = np.load(file)
|
|
217
228
|
spikes = {r[3:]: {'spikes': spike_npz[r]} for r in spike_npz.files if r.startswith('sp_')}
|
|
218
229
|
for reg in spikes:
|
|
@@ -221,7 +232,7 @@ def loadRegionSpikes(file):
|
|
|
221
232
|
return spikes
|
|
222
233
|
|
|
223
234
|
|
|
224
|
-
def saveRegionSpikes(file,spikes):
|
|
235
|
+
def saveRegionSpikes(file:str|PathLike[str], spikes):
|
|
225
236
|
file = pathlib.Path(file)
|
|
226
237
|
if not file.parent.exists():
|
|
227
238
|
pathlib.Path.mkdir(file.parent)
|
|
@@ -231,7 +242,7 @@ def saveRegionSpikes(file,spikes):
|
|
|
231
242
|
return
|
|
232
243
|
|
|
233
244
|
|
|
234
|
-
def loadEventFile(filename:
|
|
245
|
+
def loadEventFile(filename:str|PathLike[str], compact:bool=False):
|
|
235
246
|
# load events from a .evt file
|
|
236
247
|
#
|
|
237
248
|
# arguments:
|
|
@@ -289,8 +300,8 @@ def loadEventFile(filename: str, compact: bool = False):
|
|
|
289
300
|
return events
|
|
290
301
|
|
|
291
302
|
|
|
292
|
-
def loadEvents(session:str, extra:str|list[str]):
|
|
293
|
-
# load event files
|
|
303
|
+
def loadEvents(session:str|PathLike[str], extra:str|list[str]):
|
|
304
|
+
# load event files for a session
|
|
294
305
|
#
|
|
295
306
|
# arguments:
|
|
296
307
|
# session string, path to session .xml file, event files must be in session folder
|
|
@@ -340,7 +351,7 @@ def loadEvents(session:str, extra:str|list[str]):
|
|
|
340
351
|
return events, cat_names
|
|
341
352
|
|
|
342
353
|
|
|
343
|
-
def loadSpikeWaveforms(session:
|
|
354
|
+
def loadSpikeWaveforms(session:str|PathLike[str]):
|
|
344
355
|
|
|
345
356
|
file_root = pathlib.Path(session).with_suffix('')
|
|
346
357
|
data = scipy.io.loadmat(file_root.with_suffix('.cell_metrics.cellinfo.mat'),simplify_cells=True)['cell_metrics']
|
|
@@ -351,7 +362,98 @@ def loadSpikeWaveforms(session: str):
|
|
|
351
362
|
return
|
|
352
363
|
|
|
353
364
|
|
|
354
|
-
def
|
|
365
|
+
def loadWideband(session:str|PathLike[str], dtype=None, channels=None, intervals=None, skip:int=None, cat:bool=None, trim:bool=None):
|
|
366
|
+
'''
|
|
367
|
+
load wideband data for a session, which is organized in records: chunks of data containing one sample for each channel
|
|
368
|
+
|
|
369
|
+
arguments:
|
|
370
|
+
session file, path to session .xml file, spike files must be in session folder
|
|
371
|
+
dtype type = np.int16
|
|
372
|
+
channels (:,) int = None, channels to load, if None, load all channels; counted base 0 as NeuroScope
|
|
373
|
+
intervals (:,2) float = None, time intervals to load, if None, load all data
|
|
374
|
+
skip int = 0, number of records to skip after each record is read (to subsample data)
|
|
375
|
+
cat bool = True, concatenate loaded data into a single array; if False, return a list of arrays for each interval
|
|
376
|
+
trim bool = False, trim each loaded chunk of data so that they all have the same number of time points (useful if
|
|
377
|
+
'intervals' are of the same length)
|
|
378
|
+
|
|
379
|
+
output:
|
|
380
|
+
data (records, channels) dtype, data for each channel, each row is a record; list of arrays if 'cat' is False
|
|
381
|
+
t (records,) float, time of each record; list of arrays if 'cat' is False (contrary to FMAT standard 't' returned
|
|
382
|
+
separately from 'data' to preserve different data types)
|
|
383
|
+
'''
|
|
384
|
+
|
|
385
|
+
file_root = pathlib.Path(session).with_suffix("")
|
|
386
|
+
dat_file = file_root.with_suffix(".dat")
|
|
387
|
+
|
|
388
|
+
# load parameters n_channels from .xml
|
|
389
|
+
tree = xml.etree.ElementTree.parse(file_root.with_suffix(".xml"))
|
|
390
|
+
root = tree.getroot()
|
|
391
|
+
frequency = float(root.find(".//samplingRate").text)
|
|
392
|
+
n_channels = int(root.find(".//nChannels").text)
|
|
393
|
+
|
|
394
|
+
if dtype is None: dtype = np.int16
|
|
395
|
+
if channels is not None: channels = np.array(channels, ndmin=1)
|
|
396
|
+
if intervals is None: intervals = [[0,np.inf]]
|
|
397
|
+
intervals = np.array(intervals,ndmin=2)
|
|
398
|
+
if skip is None: skip = 0
|
|
399
|
+
if cat is None: cat = True
|
|
400
|
+
|
|
401
|
+
sample_size = np.dtype(dtype).itemsize
|
|
402
|
+
file_size = dat_file.stat().st_size
|
|
403
|
+
results = []
|
|
404
|
+
t = []
|
|
405
|
+
min_len = np.inf
|
|
406
|
+
with open(dat_file,'rb') as f:
|
|
407
|
+
for start, stop in intervals:
|
|
408
|
+
|
|
409
|
+
# locate 'start' in file
|
|
410
|
+
record_offset = int(np.floor(start * frequency))
|
|
411
|
+
byte_offset = record_offset * n_channels * sample_size
|
|
412
|
+
f.seek(byte_offset)
|
|
413
|
+
|
|
414
|
+
# copute number of records to read
|
|
415
|
+
if np.isinf(stop):
|
|
416
|
+
remaining_bytes = file_size - byte_offset
|
|
417
|
+
n_records = remaining_bytes // (n_channels * sample_size)
|
|
418
|
+
else:
|
|
419
|
+
n_records = int(np.floor((stop-start) * frequency))
|
|
420
|
+
max_records = (file_size - byte_offset) // (n_channels * sample_size)
|
|
421
|
+
n_records = min(n_records,max_records)
|
|
422
|
+
|
|
423
|
+
raw = np.fromfile(f,dtype=dtype,count=n_records*n_channels).reshape((-1,n_channels))
|
|
424
|
+
if raw.size == 0:
|
|
425
|
+
break
|
|
426
|
+
|
|
427
|
+
# select requested channels
|
|
428
|
+
if channels is not None:
|
|
429
|
+
raw = raw[:,channels]
|
|
430
|
+
# apply skipping
|
|
431
|
+
if skip > 0:
|
|
432
|
+
raw = raw[::skip+1]
|
|
433
|
+
|
|
434
|
+
results.append(raw)
|
|
435
|
+
t.append(np.linspace(start, start+raw.shape[0]/frequency*(skip+1), raw.shape[0]))
|
|
436
|
+
min_len = min(min_len,raw.shape[0])
|
|
437
|
+
|
|
438
|
+
# handle empty output
|
|
439
|
+
if len(results) == 0:
|
|
440
|
+
if channels is None: channels = np.arange(n_channels)
|
|
441
|
+
results = [np.empty((0,len(channels)),dtype=dtype)]
|
|
442
|
+
t = [np.empty(0)]
|
|
443
|
+
|
|
444
|
+
if trim:
|
|
445
|
+
results = [r[:min_len,:] for r in results]
|
|
446
|
+
t = [_t[:min_len] for _t in t]
|
|
447
|
+
|
|
448
|
+
# concatenate results from intervals
|
|
449
|
+
if cat:
|
|
450
|
+
results = np.vstack(results)
|
|
451
|
+
t = np.concatenate(t)
|
|
452
|
+
|
|
453
|
+
return results, t
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def loadLFP(session:str|PathLike[str]):
|
|
355
457
|
|
|
356
458
|
print('implement!')
|
|
357
459
|
|
|
@@ -365,9 +467,36 @@ def saveMatrix():
|
|
|
365
467
|
|
|
366
468
|
return
|
|
367
469
|
|
|
470
|
+
|
|
471
|
+
def printXML(session:str|PathLike[str]):
|
|
472
|
+
"""
|
|
473
|
+
simple utility to print contents of a session's .xml file
|
|
474
|
+
"""
|
|
475
|
+
|
|
476
|
+
def print_xml(element,level=0):
|
|
477
|
+
indent = " " * level
|
|
478
|
+
# print tag and attributes
|
|
479
|
+
print(f"{indent}{element.tag}",end="")
|
|
480
|
+
if element.attrib:
|
|
481
|
+
print(f" {element.attrib}",end="")
|
|
482
|
+
# print text if present
|
|
483
|
+
text = element.text.strip() if element.text else ""
|
|
484
|
+
if text:
|
|
485
|
+
print(f" -> {text}",end="")
|
|
486
|
+
print()
|
|
487
|
+
# recurse into children
|
|
488
|
+
for child in element:
|
|
489
|
+
print_xml(child,level+1)
|
|
490
|
+
|
|
491
|
+
tree = xml.etree.ElementTree.parse(pathlib.Path(session).with_suffix(".xml"))
|
|
492
|
+
root = tree.getroot()
|
|
493
|
+
print_xml(root)
|
|
494
|
+
|
|
495
|
+
return
|
|
496
|
+
|
|
368
497
|
# functions to run batch
|
|
369
498
|
|
|
370
|
-
def readBatchFile(file_path:
|
|
499
|
+
def readBatchFile(file_path:str|PathLike[str]):
|
|
371
500
|
# read batch file
|
|
372
501
|
#
|
|
373
502
|
# arguments:
|
|
@@ -425,7 +554,7 @@ def _batchWorker(payload):
|
|
|
425
554
|
return -i-1, e
|
|
426
555
|
|
|
427
556
|
|
|
428
|
-
def runBatch(batch_file:str, func:Callable, args:list[list[Any]]=None, rnd_seed:str=None, kwargs:dict|list[dict]=None, ignore_args:bool=False,
|
|
557
|
+
def runBatch(batch_file:str|PathLike[str], func:Callable, args:list[list[Any]]=None, rnd_seed:str=None, kwargs:dict|list[dict]=None, ignore_args:bool=False,
|
|
429
558
|
sessions:list[int]=None, parallel:bool|int=False, verbose:bool=True) -> tuple[list, ...]:
|
|
430
559
|
# run a routine on multiple sessions
|
|
431
560
|
#
|
|
@@ -154,12 +154,9 @@ def restrict(samples, intervals, shift:bool=False, s_ind:bool=False, i_ind:bool=
|
|
|
154
154
|
# samples (p,m) float, restricted samples, i. e., rows of 'samples' whose first element falls into 'intervals'
|
|
155
155
|
# Is (n) bool, optional, indicese of original samples which were kept
|
|
156
156
|
# Ii (n) bool, optional, indicese of intervals which contain kept samples
|
|
157
|
-
|
|
158
|
-
samples = np.asarray(samples)
|
|
159
|
-
if samples.size == 0:
|
|
160
|
-
samples = np.array([])
|
|
161
157
|
|
|
162
158
|
# promote 1d arrays to 2d
|
|
159
|
+
samples = np.asarray(samples)
|
|
163
160
|
squeeze = samples.ndim == 1
|
|
164
161
|
if squeeze:
|
|
165
162
|
samples = samples.reshape((-1,1))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
''' Plotting utilities for publication grade figures '''
|
|
2
2
|
|
|
3
|
+
import fmatoolbox
|
|
3
4
|
import numpy as np
|
|
4
5
|
import numpy.typing as npt
|
|
5
6
|
import matplotlib.axes as mpla
|
|
@@ -9,11 +10,13 @@ import matplotlib.typing as mplt
|
|
|
9
10
|
import scipy.stats as spst
|
|
10
11
|
import scipy as sp
|
|
11
12
|
from collections.abc import Iterable
|
|
13
|
+
from networkx.algorithms.centrality import flow_matrix
|
|
12
14
|
from typing import Literal, Callable
|
|
15
|
+
import matplotlib as mpl
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
def adjustAxes(axs:mpla.Axes|Iterable[mpla.Axes], format:Literal['paper','poster']='paper'):
|
|
16
|
-
# adjust axes properties to
|
|
19
|
+
# adjust axes properties to improve figure appearance
|
|
17
20
|
#
|
|
18
21
|
# arguments:
|
|
19
22
|
# axs iterable of matplotlib.axes.Axes
|
|
@@ -26,8 +29,11 @@ def adjustAxes(axs:mpla.Axes|Iterable[mpla.Axes], format:Literal['paper','poster
|
|
|
26
29
|
|
|
27
30
|
lw = 1 if format == 'paper' else 2
|
|
28
31
|
axw = 1.3 if format == 'paper' else 2.1
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
ax_label_fs = 9 if format == 'paper' else 18
|
|
33
|
+
ax_label_pad = 0.1 if format == 'paper' else 1
|
|
34
|
+
ax_tick_fs = 8 if format == 'paper' else 14
|
|
35
|
+
ax_tick_l = 2 if format == 'paper' else 5
|
|
36
|
+
ax_tick_pad = 1 if format == 'paper' else 2
|
|
31
37
|
|
|
32
38
|
for ax in axs:
|
|
33
39
|
|
|
@@ -36,9 +42,11 @@ def adjustAxes(axs:mpla.Axes|Iterable[mpla.Axes], format:Literal['paper','poster
|
|
|
36
42
|
|
|
37
43
|
# adjust thickness and font size
|
|
38
44
|
[ax.spines[spine].set_linewidth(lw) for spine in ['bottom','left','polar'] if spine in ax.spines]
|
|
39
|
-
ax.tick_params(width=axw,labelsize=
|
|
40
|
-
ax.xaxis.label.set_fontsize(
|
|
41
|
-
ax.yaxis.label.set_fontsize(
|
|
45
|
+
ax.tick_params(width=axw,labelsize=ax_tick_fs,pad=ax_tick_pad,length=ax_tick_l)
|
|
46
|
+
ax.xaxis.label.set_fontsize(ax_label_fs)
|
|
47
|
+
ax.yaxis.label.set_fontsize(ax_label_fs)
|
|
48
|
+
ax.xaxis.labelpad = ax_label_pad
|
|
49
|
+
ax.yaxis.labelpad = ax_label_pad
|
|
42
50
|
|
|
43
51
|
return
|
|
44
52
|
|
|
@@ -141,8 +149,8 @@ def plotXY(data, start=None, stop=None, color:mplt.ColorType=None, label=None, a
|
|
|
141
149
|
return
|
|
142
150
|
|
|
143
151
|
|
|
144
|
-
def plotColorMap(data:npt.NDArray[np.floating], vmin:float=None, vmax:float=None, zscore=None, sortby:npt.NDArray[np.floating]|Callable|str=None, sortax:int=
|
|
145
|
-
xzoom:float=None, yzoom:float=None, x=None, y=None, aspect:float=
|
|
152
|
+
def plotColorMap(data:npt.NDArray[np.floating], vmin:float=None, vmax:float=None, zscore=None, sortby:npt.NDArray[np.floating]|Callable|str=None, sortax:int=None,
|
|
153
|
+
xzoom:float=None, yzoom:float=None, x=None, y=None, aspect:float=None, bar:str=None, ax:mpla.Axes=None):
|
|
146
154
|
'''
|
|
147
155
|
plot a 2D array as a colormap with optional normalization, sorting, and resampling
|
|
148
156
|
|
|
@@ -159,6 +167,7 @@ def plotColorMap(data:npt.NDArray[np.floating], vmin:float=None, vmax:float=None
|
|
|
159
167
|
xzoom, yzoom float = None, optional horizontal / vertical resampling factor passed to ``scipy.ndimage.zoom`` (after sorting)
|
|
160
168
|
x, y (:,) float, coordinates corresponding to columns and rows of `data`, defaults are range(m) and range(n)
|
|
161
169
|
aspect float = 3/4, image aspect ratio
|
|
170
|
+
bar str = None, if given, draw colorbar next to `ax` with label specified by `bar`
|
|
162
171
|
ax matplotlib.axes.Axes = matplotlib.pyplot.gca(), axes to plot in
|
|
163
172
|
'''
|
|
164
173
|
|
|
@@ -170,6 +179,7 @@ def plotColorMap(data:npt.NDArray[np.floating], vmin:float=None, vmax:float=None
|
|
|
170
179
|
zscore = None
|
|
171
180
|
data = spst.zscore(data,axis=zscore,nan_policy='omit')
|
|
172
181
|
|
|
182
|
+
if sortax is None: sortax = 0
|
|
173
183
|
if sortby is not None:
|
|
174
184
|
if isinstance(sortby,str) and sortby == 'peak':
|
|
175
185
|
sortby = lambda x : np.argsort(np.argmax(x,1-sortax))
|
|
@@ -197,8 +207,11 @@ def plotColorMap(data:npt.NDArray[np.floating], vmin:float=None, vmax:float=None
|
|
|
197
207
|
if ax is None:
|
|
198
208
|
ax = plt.gca()
|
|
199
209
|
|
|
210
|
+
if aspect is None: aspect = 3 / 4
|
|
200
211
|
ax.set_aspect(aspect)
|
|
201
212
|
im = ax.imshow(data,aspect='auto',vmin=vmin,vmax=vmax,origin='lower',extent=[x[0]-dx,x[-1]+dx,y[0]-dy,y[-1]+dy])
|
|
213
|
+
if bar is not None:
|
|
214
|
+
plt.colorbar(im,label=bar,ax=ax)
|
|
202
215
|
|
|
203
216
|
return im
|
|
204
217
|
|
|
@@ -214,6 +227,7 @@ def semPlot(x, y, ci:str|Callable=None, zscore:bool|int=None, color:mplt.ColorTy
|
|
|
214
227
|
- 'nansem', standard error of the mean (SEM) for each column of `y`, ignoring missing values
|
|
215
228
|
- callable, must have signature ``low, high = ci(y)``
|
|
216
229
|
zscore bool = False | int, if True (or 1), z-score w.r.t. average of y, if 2, z-score each row of y independently
|
|
230
|
+
smooth float = None, gaussian kernel std for smoothing over time, NOT IMPLEMENTED
|
|
217
231
|
color color = None
|
|
218
232
|
mode str = 'area' | 'error', plot 'ci' either as a shaded area or as error bars
|
|
219
233
|
alpha float = 0.5, shaded area transparency value (only for 'area' mode)
|
|
@@ -419,30 +433,28 @@ def pHorzLine(p, t=None, dy=None, color:mplt.ColorType=None, ax:mpla.Axes=None):
|
|
|
419
433
|
return
|
|
420
434
|
|
|
421
435
|
|
|
422
|
-
def plotIntervals(intervals,
|
|
423
|
-
|
|
424
|
-
intervals = np.array(intervals,ndmin=2)
|
|
425
|
-
if intervals.ndim != 2 or intervals.shape[1] != 2:
|
|
426
|
-
raise ValueError("'intervals' must have shape (n,2)")
|
|
436
|
+
def plotIntervals(intervals, color:mplt.ColorType='gray', alpha=0.3, label:str=None, ax:mpla.Axes=None, **plot_kwargs):
|
|
427
437
|
|
|
438
|
+
intervals = fmatoolbox.general.consolidateIntervals(intervals)
|
|
428
439
|
if ax is None:
|
|
429
440
|
ax = plt.gca()
|
|
430
441
|
|
|
431
|
-
|
|
432
|
-
|
|
442
|
+
ax.axvspan(intervals[0,0],intervals[0,1],color=color,alpha=alpha,label=label,**plot_kwargs)
|
|
443
|
+
for start, stop in intervals[1:]:
|
|
444
|
+
ax.axvspan(start,stop,color=color,alpha=alpha,**plot_kwargs)
|
|
433
445
|
|
|
434
446
|
|
|
435
|
-
def plotPDF(x, mode:str=None, bandwidth:float|str=None, eps:float=None, n_points:int=None, norm=None, color:mplt.ColorType=None, label=None, ax:mpla.Axes=None, **plot_kwargs):
|
|
447
|
+
def plotPDF(x, mode:str=None, bandwidth:float|str=None, eps:float=None, n_points:int=None, bins=None, norm=None, color:mplt.ColorType=None, label=None, ax:mpla.Axes=None, **plot_kwargs):
|
|
436
448
|
'''
|
|
437
449
|
estimate and plot probability density function (PDF) of data
|
|
438
450
|
|
|
439
451
|
arguments:
|
|
440
452
|
x (n,) tuple | array, data to plot
|
|
441
453
|
mode str = {'normal','log','polar'}, DESCRIBE
|
|
442
|
-
log bool = False, plot log-transformed PDF, DEPRECATED
|
|
443
454
|
bandwidth float | str = 'scott', bandwidth for gaussian kernel
|
|
444
455
|
eps float = 1e-12, small value used to avoid log(0)
|
|
445
456
|
n_points int = 50, number of points used to evaluate PDF
|
|
457
|
+
bins (:,) float = None, bin edges, if None, bins are linearly spaced between min and max of x
|
|
446
458
|
norm str = {'density','max'}, normalization mode, 'density' computes PDF, 'max' normalizes its maximum to 1
|
|
447
459
|
color color = None, line color
|
|
448
460
|
label str = None, legend label for line
|
|
@@ -452,14 +464,14 @@ def plotPDF(x, mode:str=None, bandwidth:float|str=None, eps:float=None, n_points
|
|
|
452
464
|
if mode is None: mode = 'normal'
|
|
453
465
|
if bandwidth is None: bandwidth = 0.05 if mode == 'polar' else 'scott'
|
|
454
466
|
if eps is None: eps = 1e-12
|
|
455
|
-
if n_points is None: n_points = 50
|
|
467
|
+
if n_points is None and bins is None: n_points = 50
|
|
456
468
|
if norm is None: norm = 'density'
|
|
457
469
|
if ax is None: ax = plt.gca()
|
|
458
470
|
if isinstance(x,tuple):
|
|
459
|
-
if color is None:
|
|
460
|
-
color = [
|
|
461
|
-
if label is None:
|
|
462
|
-
label = [
|
|
471
|
+
if color is None or isinstance(color,str):
|
|
472
|
+
color = [color] * len(x)
|
|
473
|
+
if label is None or isinstance(label,str):
|
|
474
|
+
label = [label] * len(x)
|
|
463
475
|
else:
|
|
464
476
|
x = (x,)
|
|
465
477
|
color = [color]
|
|
@@ -481,7 +493,10 @@ def plotPDF(x, mode:str=None, bandwidth:float|str=None, eps:float=None, n_points
|
|
|
481
493
|
match mode:
|
|
482
494
|
# 1. real-valued data using gaussian kernel density estimator
|
|
483
495
|
case 'normal':
|
|
484
|
-
|
|
496
|
+
if n_points is None:
|
|
497
|
+
this_grid = bins
|
|
498
|
+
else:
|
|
499
|
+
this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid
|
|
485
500
|
kde = sp.stats.gaussian_kde(data,bw_method=bandwidth)
|
|
486
501
|
this_density = kde(this_grid)
|
|
487
502
|
if norm == 'max':
|
|
@@ -493,7 +508,10 @@ def plotPDF(x, mode:str=None, bandwidth:float|str=None, eps:float=None, n_points
|
|
|
493
508
|
if data.min() <= 0:
|
|
494
509
|
raise ValueError('log-transforming data requires positive values')
|
|
495
510
|
data = np.log(data + eps) # log-transform data
|
|
496
|
-
|
|
511
|
+
if n_points is None:
|
|
512
|
+
this_grid = bins
|
|
513
|
+
else:
|
|
514
|
+
this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid in log-space
|
|
497
515
|
jacobian = np.exp(this_grid) # jacobian term to transform density back to linear
|
|
498
516
|
kde = sp.stats.gaussian_kde(data,bw_method=bandwidth)
|
|
499
517
|
this_density = kde(this_grid) / jacobian
|
|
@@ -504,7 +522,10 @@ def plotPDF(x, mode:str=None, bandwidth:float|str=None, eps:float=None, n_points
|
|
|
504
522
|
# 3. circular data using von-Mises kernel density estimator
|
|
505
523
|
case 'polar':
|
|
506
524
|
data = np.mod(data,2*np.pi)
|
|
507
|
-
|
|
525
|
+
if n_points is None:
|
|
526
|
+
this_grid = bins
|
|
527
|
+
else:
|
|
528
|
+
this_grid = np.linspace(0,2*np.pi,n_points) # linear grid in [0,2*pi]
|
|
508
529
|
this_density = np.zeros_like(this_grid)
|
|
509
530
|
for theta in data:
|
|
510
531
|
this_density += spst.vonmises.pdf(this_grid-theta,1/bandwidth)
|
|
@@ -7,6 +7,7 @@ import re
|
|
|
7
7
|
import fmatoolbox.analysis
|
|
8
8
|
import fmatoolbox.data
|
|
9
9
|
import warnings
|
|
10
|
+
import xml.etree.ElementTree
|
|
10
11
|
from collections.abc import Iterable
|
|
11
12
|
|
|
12
13
|
def _regionDataPath():
|
|
@@ -34,8 +35,9 @@ class regions:
|
|
|
34
35
|
# reload bool = False, load spikes from original files, bypassing Regions/<basename>_spikes.npz backup
|
|
35
36
|
# anat_file string = None, DESCRIBE
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
self.
|
|
38
|
+
session = pathlib.Path(session)
|
|
39
|
+
self.session = session.parent
|
|
40
|
+
self.basename = session.stem
|
|
39
41
|
self.rat = self.basename[3:6]
|
|
40
42
|
|
|
41
43
|
# 1. load events
|
|
@@ -97,10 +99,12 @@ class regions:
|
|
|
97
99
|
# restrict to session epochs
|
|
98
100
|
if phase_names:
|
|
99
101
|
_, valid = fmatoolbox.general.restrict(e['intervals'],epoch_intervals,s_ind=True)
|
|
100
|
-
e = {k: v[valid] for k, v in e.items()}
|
|
102
|
+
e = {k: v[valid] for k, v in e.items() if len(v) == len(valid)}
|
|
101
103
|
|
|
102
104
|
self.events[name] = e
|
|
103
105
|
|
|
106
|
+
if isinstance(ids,str):
|
|
107
|
+
ids = [ids]
|
|
104
108
|
if ids:
|
|
105
109
|
self.ids = list(dict.fromkeys(ids))
|
|
106
110
|
self.region = {id : {} for id in ids}
|
|
@@ -109,13 +113,13 @@ class regions:
|
|
|
109
113
|
self.region = {}
|
|
110
114
|
|
|
111
115
|
# 4. load spikes and store them per region
|
|
116
|
+
if anat_file is None:
|
|
117
|
+
anat_file = next(_regionDataPath().glob('*.anat'), None)
|
|
118
|
+
else:
|
|
119
|
+
anat_file = pathlib.Path(anat_file)
|
|
120
|
+
if not anat_file.exists():
|
|
121
|
+
anat_file = _regionDataPath() / anat_file
|
|
112
122
|
if load_spikes:
|
|
113
|
-
if anat_file is None:
|
|
114
|
-
anat_file = next(_regionDataPath().glob('*.anat'), None)
|
|
115
|
-
else:
|
|
116
|
-
anat_file = pathlib.Path(anat_file)
|
|
117
|
-
if not anat_file.exists():
|
|
118
|
-
anat_file = _regionDataPath() / anat_file
|
|
119
123
|
self.region = fmatoolbox.data.loadSpikeTimes(session,output='regions',anat_file=anat_file,reload=reload)
|
|
120
124
|
if ids:
|
|
121
125
|
self.region = {r: self.region[r] for r in ids}
|
|
@@ -126,6 +130,25 @@ class regions:
|
|
|
126
130
|
for id in self.ids:
|
|
127
131
|
self.region[id]['spikes'] = fmatoolbox.general.restrict(self.region[id]['spikes'],epoch_intervals)
|
|
128
132
|
|
|
133
|
+
# 5. load electrode groups and channels per region
|
|
134
|
+
try:
|
|
135
|
+
anat = fmatoolbox.data.loadAnatomyFile(anat_file)
|
|
136
|
+
anat = anat[anat['rat'] == int(self.rat)] # keep rat of interest, deduced from file name
|
|
137
|
+
anat_ids = np.unique(anat['region'])
|
|
138
|
+
tree = xml.etree.ElementTree.parse(session.with_suffix(".xml"))
|
|
139
|
+
root = tree.getroot()
|
|
140
|
+
channel_groups = root.find(".//channelGroups").findall("group")
|
|
141
|
+
channel_groups = [[int(element.text) for element in c.findall(".//channel")] for c in channel_groups]
|
|
142
|
+
if len(self.region):
|
|
143
|
+
for id in self.ids:
|
|
144
|
+
self.region[id]['channels'] = {int(e): channel_groups[e-1] for e in anat[anat['region'] == id]['electrode']}
|
|
145
|
+
else:
|
|
146
|
+
self.ids = anat_ids.tolist()
|
|
147
|
+
for id in self.ids:
|
|
148
|
+
self.region[id] = {'channels': {int(e): channel_groups[e-1] for e in anat[anat['region'] == id]['electrode']} }
|
|
149
|
+
except FileNotFoundError:
|
|
150
|
+
pass
|
|
151
|
+
|
|
129
152
|
return
|
|
130
153
|
|
|
131
154
|
|
|
@@ -166,7 +189,8 @@ class regions:
|
|
|
166
189
|
regs = regs[np.sort(idx)]
|
|
167
190
|
|
|
168
191
|
# default: all electrode groups
|
|
169
|
-
all_e_groups =
|
|
192
|
+
all_e_groups = [list(self.region[r]['e_group'].keys()) for r in self.ids if 'e_group' in self.region[r]]
|
|
193
|
+
if len(all_e_groups): all_e_groups = np.concatenate(all_e_groups)
|
|
170
194
|
if np.all(e_groups == None):
|
|
171
195
|
if exclusive and np.any(regs != None):
|
|
172
196
|
e_groups = np.array([None])
|
|
@@ -296,17 +320,36 @@ class regions:
|
|
|
296
320
|
|
|
297
321
|
|
|
298
322
|
def electrodes(self,regs=None):
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
323
|
+
"""
|
|
324
|
+
get pooled list of electrode groups for regions
|
|
325
|
+
|
|
326
|
+
arguments:
|
|
327
|
+
regs (:) string = None, electrode of these regions are returned as an array, default is all regions
|
|
328
|
+
|
|
329
|
+
output:
|
|
330
|
+
electrodes (:) int, sorted by value
|
|
331
|
+
"""
|
|
306
332
|
|
|
307
333
|
regs, _, _ = self._checkIDs(regs=regs)
|
|
308
334
|
|
|
309
335
|
return np.concatenate([list(self.region[r]['e_group'].keys()) for r in regs])
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def channels(self,regs=None):
|
|
339
|
+
"""
|
|
340
|
+
get pooled list of recording channels for regions
|
|
341
|
+
|
|
342
|
+
arguments:
|
|
343
|
+
regs (:) string = None, channels of these regions are returned as an array, default is all regions
|
|
344
|
+
|
|
345
|
+
output:
|
|
346
|
+
channels (:) int
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
regs, _, _ = self._checkIDs(regs=regs)
|
|
350
|
+
values = [list(self.region[r]['channels'].values()) for r in regs]
|
|
351
|
+
|
|
352
|
+
return np.array([x for outer in values for inner in outer for x in inner])
|
|
310
353
|
|
|
311
354
|
|
|
312
355
|
def units(self,regs=None,e_groups=None):
|
|
@@ -376,8 +419,8 @@ class regions:
|
|
|
376
419
|
|
|
377
420
|
## functions to compute quantities ##
|
|
378
421
|
|
|
379
|
-
def firingRate(self, regs:Iterable[str]=None, e_groups:Iterable[int]=None, states:Iterable[str]=None, when=None, shift=
|
|
380
|
-
window=None, step=None, smooth=None, norm=
|
|
422
|
+
def firingRate(self, regs:Iterable[str]=None, e_groups:Iterable[int]=None, states:Iterable[str]=None, when=None, shift:bool=None,
|
|
423
|
+
window=None, step:int=None, smooth=None, norm:bool=None):
|
|
381
424
|
'''
|
|
382
425
|
get region firing rate
|
|
383
426
|
|
|
@@ -390,7 +433,7 @@ class regions:
|
|
|
390
433
|
window float = 0.05, window size to count spikes
|
|
391
434
|
step int = 1, firing rate is computed in windows of length 'binSize' and overlap 'binSize' / 'step',
|
|
392
435
|
default is no overlap
|
|
393
|
-
smooth float = None, gaussian kernel std
|
|
436
|
+
smooth float = None, gaussian kernel std to smooth rate over time
|
|
394
437
|
norm bool = False, normalize by neuron number per region
|
|
395
438
|
|
|
396
439
|
output:
|
|
@@ -400,8 +443,10 @@ class regions:
|
|
|
400
443
|
|
|
401
444
|
if states is not None and when is not None:
|
|
402
445
|
raise ValueError("'states' and 'when' cannot be specified at the same time")
|
|
446
|
+
if shift is None: shift = False
|
|
403
447
|
if window is None: window = 0.05
|
|
404
448
|
if step is None: step = 1
|
|
449
|
+
if norm is None: norm = False
|
|
405
450
|
|
|
406
451
|
regs, e_groups, states = self._checkIDs(regs=regs,e_groups=e_groups,states=states,fuse=True)
|
|
407
452
|
|
|
@@ -450,7 +495,7 @@ class regions:
|
|
|
450
495
|
return firing_rate
|
|
451
496
|
|
|
452
497
|
|
|
453
|
-
def unitFiringRate(self,regs=None,states=None,when=None,shift=
|
|
498
|
+
def unitFiringRate(self, regs=None, states=None, when=None, shift:bool=None, window=None, step:int=None, smooth:bool=None):
|
|
454
499
|
# get units' firing rate
|
|
455
500
|
#
|
|
456
501
|
# arguments:
|
|
@@ -461,13 +506,14 @@ class regions:
|
|
|
461
506
|
# window float = 0.05 s, window size to count spikes
|
|
462
507
|
# step int = 1, firing rate is computed in windows of length 'binSize' and overlap 'binSize' / 'step',
|
|
463
508
|
# default is no overlap
|
|
464
|
-
# smooth float = None, gaussian kernel std
|
|
509
|
+
# smooth float = None, gaussian kernel std to smooth rate over time
|
|
465
510
|
#
|
|
466
511
|
# output:
|
|
467
512
|
# rate (:,n+1) float, every row is [time stamp, firing rates for n units]
|
|
468
513
|
|
|
469
514
|
if states is not None and when is not None:
|
|
470
515
|
raise ValueError("'states' and 'when' cannot be specified at the same time")
|
|
516
|
+
if shift is None: shift = False
|
|
471
517
|
if window is None: window = 0.05
|
|
472
518
|
if step is None: step = 1
|
|
473
519
|
|
|
@@ -498,7 +544,7 @@ class regions:
|
|
|
498
544
|
return firing_rate
|
|
499
545
|
|
|
500
546
|
|
|
501
|
-
def avalanches(self,regs=None,states=None,when=None,shift=
|
|
547
|
+
def avalanches(self, regs=None, states=None, when=None, shift:bool=None, thresh:float=None, window:float=None, step:int=None, smooth:bool=None, norm:bool=None, return_fr:bool=None):
|
|
502
548
|
# compute avalanches per region from population firing rate
|
|
503
549
|
#
|
|
504
550
|
# arguments:
|
|
@@ -510,7 +556,7 @@ class regions:
|
|
|
510
556
|
# window float = 0.05 s, window size to count spikes
|
|
511
557
|
# step float = 1, firing rate is computed in windows of length 'binSize' and overlap 'binSize' / 'step',
|
|
512
558
|
# default is no overlap
|
|
513
|
-
# smooth float = None, gaussian kernel std
|
|
559
|
+
# smooth float = None, gaussian kernel std to smooth rate over time before finding avalanches
|
|
514
560
|
# norm bool = False, normalize by neuron number per region
|
|
515
561
|
#
|
|
516
562
|
# output:
|
|
@@ -519,6 +565,9 @@ class regions:
|
|
|
519
565
|
# size_t (m) float, size over time, in which every avalanche is separated by a 0
|
|
520
566
|
# fr (:,r+1) float, every row is [time stamp, firing rates for r regions], optional
|
|
521
567
|
|
|
568
|
+
if thresh is None: thresh = 30
|
|
569
|
+
if return_fr is None: return_fr = False
|
|
570
|
+
|
|
522
571
|
none_state = states is None
|
|
523
572
|
regs, _, states = self._checkIDs(regs=regs,states=states,fuse=True)
|
|
524
573
|
if none_state:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fmatoolbox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Python port of FMAToolbox
|
|
5
5
|
Author: Pietro Bozzo
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -11,11 +11,13 @@ Requires-Python: >=3.11
|
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Requires-Dist: numpy
|
|
14
|
+
Requires-Dist: numba
|
|
14
15
|
Requires-Dist: scipy
|
|
15
16
|
Requires-Dist: matplotlib
|
|
16
17
|
Requires-Dist: statsmodels
|
|
17
|
-
|
|
18
|
-
Requires-Dist: scikit-
|
|
18
|
+
Provides-Extra: assemblies
|
|
19
|
+
Requires-Dist: scikit-learn; extra == "assemblies"
|
|
20
|
+
Requires-Dist: scikit-image; extra == "assemblies"
|
|
19
21
|
Dynamic: license-file
|
|
20
22
|
|
|
21
23
|
# FMAToolbox (Python Port)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|