fmatoolbox 0.1.2__tar.gz → 0.1.3__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.3}/PKG-INFO +1 -1
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/pyproject.toml +1 -1
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/src/fmatoolbox/analysis.py +4 -4
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/src/fmatoolbox/data.py +156 -38
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/src/fmatoolbox/general.py +1 -4
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/src/fmatoolbox/plotting.py +30 -15
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/src/fmatoolbox/regions.py +67 -20
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3/src/fmatoolbox.egg-info}/PKG-INFO +1 -1
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/LICENSE +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/README.md +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/setup.cfg +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/src/fmatoolbox/__init__.py +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/src/fmatoolbox/exceptions.py +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/src/fmatoolbox.egg-info/SOURCES.txt +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/src/fmatoolbox.egg-info/dependency_links.txt +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/src/fmatoolbox.egg-info/requires.txt +0 -0
- {fmatoolbox-0.1.2 → fmatoolbox-0.1.3}/src/fmatoolbox.egg-info/top_level.txt +0 -0
|
@@ -12,7 +12,7 @@ from typing import Callable
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
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
|
-
|
|
15
|
+
"""
|
|
16
16
|
estimate the istantaneous rate of a point process from a realization of its time stamps, e.g., the firing rate from spike times
|
|
17
17
|
|
|
18
18
|
arguments:
|
|
@@ -27,7 +27,7 @@ def istantaneousRate(samples, start:float=None, stop:float=None, bin:float=None,
|
|
|
27
27
|
|
|
28
28
|
output:
|
|
29
29
|
rate (:,g+1) float, every row is [time stamp, rates for g processes], g is 1 if 'samples' has just one column
|
|
30
|
-
|
|
30
|
+
"""
|
|
31
31
|
|
|
32
32
|
# validate input
|
|
33
33
|
samples = np.asarray(samples)
|
|
@@ -78,11 +78,11 @@ def istantaneousRate(samples, start:float=None, stop:float=None, bin:float=None,
|
|
|
78
78
|
rate = np.array(rate).T / bin
|
|
79
79
|
|
|
80
80
|
# make time axis
|
|
81
|
-
t = bin/step/2 + np.linspace(0,(n_bins-1)*bin,n_bins*step)
|
|
81
|
+
t = start + bin/step/2 + np.linspace(0,(n_bins-1)*bin,n_bins*step)
|
|
82
82
|
|
|
83
83
|
# apply smoothing
|
|
84
84
|
if smooth is not None:
|
|
85
|
-
|
|
85
|
+
rate = sp.ndimage.gaussian_filter(rate,smooth,axes=0)
|
|
86
86
|
|
|
87
87
|
return np.column_stack((t,rate))
|
|
88
88
|
|
|
@@ -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,87 @@ 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=None, cat=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
|
+
|
|
377
|
+
output:
|
|
378
|
+
data (records, channels) dtype, data for each channel, each row is a record
|
|
379
|
+
t (records,) float, time of each record (not stacked as first column of 'data' to preserve data types)
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
file_root = pathlib.Path(session).with_suffix("")
|
|
383
|
+
dat_file = file_root.with_suffix(".dat")
|
|
384
|
+
|
|
385
|
+
# load parameters n_channels from .xml
|
|
386
|
+
tree = xml.etree.ElementTree.parse(file_root.with_suffix(".xml"))
|
|
387
|
+
root = tree.getroot()
|
|
388
|
+
frequency = float(root.find(".//samplingRate").text)
|
|
389
|
+
n_channels = int(root.find(".//nChannels").text)
|
|
390
|
+
|
|
391
|
+
if dtype is None: dtype = np.int16
|
|
392
|
+
if skip is None: skip = 0
|
|
393
|
+
if intervals is None: intervals = [[0,np.inf]]
|
|
394
|
+
intervals = np.array(intervals,ndmin=2)
|
|
395
|
+
|
|
396
|
+
sample_size = np.dtype(dtype).itemsize
|
|
397
|
+
file_size = dat_file.stat().st_size
|
|
398
|
+
results = []
|
|
399
|
+
t = []
|
|
400
|
+
with open(dat_file,'rb') as f:
|
|
401
|
+
for start, stop in intervals:
|
|
402
|
+
|
|
403
|
+
# locate 'start' in file
|
|
404
|
+
record_offset = int(np.floor(start * frequency))
|
|
405
|
+
byte_offset = record_offset * n_channels * sample_size
|
|
406
|
+
f.seek(byte_offset)
|
|
407
|
+
|
|
408
|
+
# copute number of records to read
|
|
409
|
+
if np.isinf(stop):
|
|
410
|
+
remaining_bytes = file_size - byte_offset
|
|
411
|
+
n_records = remaining_bytes // (n_channels * sample_size)
|
|
412
|
+
else:
|
|
413
|
+
n_records = int(np.floor((stop-start) * frequency))
|
|
414
|
+
max_records = (file_size - byte_offset) // (n_channels * sample_size)
|
|
415
|
+
n_records = min(n_records,max_records)
|
|
416
|
+
|
|
417
|
+
raw = np.fromfile(f,dtype=dtype,count=n_records*n_channels).reshape((-1,n_channels))
|
|
418
|
+
if raw.size == 0:
|
|
419
|
+
break
|
|
420
|
+
|
|
421
|
+
# select requested channels
|
|
422
|
+
if channels is not None:
|
|
423
|
+
raw = raw[:,channels]
|
|
424
|
+
# apply skipping
|
|
425
|
+
if skip > 0:
|
|
426
|
+
raw = raw[::skip+1]
|
|
427
|
+
|
|
428
|
+
results.append(raw)
|
|
429
|
+
t.append(np.linspace(start, start+raw.shape[0]/frequency, raw.shape[0]))
|
|
430
|
+
|
|
431
|
+
# handle empty output
|
|
432
|
+
if len(results) == 0:
|
|
433
|
+
if channels is None: channels = np.arange(n_channels)
|
|
434
|
+
results = [np.empty((0,len(channels)),dtype=dtype)]
|
|
435
|
+
t = [np.empty(0)]
|
|
436
|
+
|
|
437
|
+
# concatenate results from intervals
|
|
438
|
+
if cat:
|
|
439
|
+
results = np.vstack(results)
|
|
440
|
+
t = np.concatenate(t)
|
|
441
|
+
|
|
442
|
+
return results, t
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def loadLFP(session:str|PathLike[str]):
|
|
355
446
|
|
|
356
447
|
print('implement!')
|
|
357
448
|
|
|
@@ -365,9 +456,36 @@ def saveMatrix():
|
|
|
365
456
|
|
|
366
457
|
return
|
|
367
458
|
|
|
459
|
+
|
|
460
|
+
def printXML(session:str|PathLike[str]):
|
|
461
|
+
"""
|
|
462
|
+
simple utility to print contents of a session's .xml file
|
|
463
|
+
"""
|
|
464
|
+
|
|
465
|
+
def print_xml(element,level=0):
|
|
466
|
+
indent = " " * level
|
|
467
|
+
# print tag and attributes
|
|
468
|
+
print(f"{indent}{element.tag}",end="")
|
|
469
|
+
if element.attrib:
|
|
470
|
+
print(f" {element.attrib}",end="")
|
|
471
|
+
# print text if present
|
|
472
|
+
text = element.text.strip() if element.text else ""
|
|
473
|
+
if text:
|
|
474
|
+
print(f" -> {text}",end="")
|
|
475
|
+
print()
|
|
476
|
+
# recurse into children
|
|
477
|
+
for child in element:
|
|
478
|
+
print_xml(child,level+1)
|
|
479
|
+
|
|
480
|
+
tree = xml.etree.ElementTree.parse(pathlib.Path(session).with_suffix(".xml"))
|
|
481
|
+
root = tree.getroot()
|
|
482
|
+
print_xml(root)
|
|
483
|
+
|
|
484
|
+
return
|
|
485
|
+
|
|
368
486
|
# functions to run batch
|
|
369
487
|
|
|
370
|
-
def readBatchFile(file_path:
|
|
488
|
+
def readBatchFile(file_path:str|PathLike[str]):
|
|
371
489
|
# read batch file
|
|
372
490
|
#
|
|
373
491
|
# arguments:
|
|
@@ -425,7 +543,7 @@ def _batchWorker(payload):
|
|
|
425
543
|
return -i-1, e
|
|
426
544
|
|
|
427
545
|
|
|
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,
|
|
546
|
+
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
547
|
sessions:list[int]=None, parallel:bool|int=False, verbose:bool=True) -> tuple[list, ...]:
|
|
430
548
|
# run a routine on multiple sessions
|
|
431
549
|
#
|
|
@@ -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))
|
|
@@ -10,6 +10,7 @@ import scipy.stats as spst
|
|
|
10
10
|
import scipy as sp
|
|
11
11
|
from collections.abc import Iterable
|
|
12
12
|
from typing import Literal, Callable
|
|
13
|
+
import matplotlib as mpl
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def adjustAxes(axs:mpla.Axes|Iterable[mpla.Axes], format:Literal['paper','poster']='paper'):
|
|
@@ -26,8 +27,11 @@ def adjustAxes(axs:mpla.Axes|Iterable[mpla.Axes], format:Literal['paper','poster
|
|
|
26
27
|
|
|
27
28
|
lw = 1 if format == 'paper' else 2
|
|
28
29
|
axw = 1.3 if format == 'paper' else 2.1
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
ax_label_fs = 9 if format == 'paper' else 18
|
|
31
|
+
ax_label_pad = 0.1 if format == 'paper' else 1
|
|
32
|
+
ax_tick_fs = 8 if format == 'paper' else 14
|
|
33
|
+
ax_tick_l = 2 if format == 'paper' else 5
|
|
34
|
+
ax_tick_pad = 1 if format == 'paper' else 2
|
|
31
35
|
|
|
32
36
|
for ax in axs:
|
|
33
37
|
|
|
@@ -36,9 +40,11 @@ def adjustAxes(axs:mpla.Axes|Iterable[mpla.Axes], format:Literal['paper','poster
|
|
|
36
40
|
|
|
37
41
|
# adjust thickness and font size
|
|
38
42
|
[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(
|
|
43
|
+
ax.tick_params(width=axw,labelsize=ax_tick_fs,pad=ax_tick_pad,length=ax_tick_l)
|
|
44
|
+
ax.xaxis.label.set_fontsize(ax_label_fs)
|
|
45
|
+
ax.yaxis.label.set_fontsize(ax_label_fs)
|
|
46
|
+
ax.xaxis.labelpad = ax_label_pad
|
|
47
|
+
ax.yaxis.labelpad = ax_label_pad
|
|
42
48
|
|
|
43
49
|
return
|
|
44
50
|
|
|
@@ -432,17 +438,17 @@ def plotIntervals(intervals, alpha=0.3, color:mplt.ColorType='gray', ax:mpla.Axe
|
|
|
432
438
|
ax.axvspan(start,stop,alpha=alpha,color=color)
|
|
433
439
|
|
|
434
440
|
|
|
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):
|
|
441
|
+
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
442
|
'''
|
|
437
443
|
estimate and plot probability density function (PDF) of data
|
|
438
444
|
|
|
439
445
|
arguments:
|
|
440
446
|
x (n,) tuple | array, data to plot
|
|
441
447
|
mode str = {'normal','log','polar'}, DESCRIBE
|
|
442
|
-
log bool = False, plot log-transformed PDF, DEPRECATED
|
|
443
448
|
bandwidth float | str = 'scott', bandwidth for gaussian kernel
|
|
444
449
|
eps float = 1e-12, small value used to avoid log(0)
|
|
445
450
|
n_points int = 50, number of points used to evaluate PDF
|
|
451
|
+
bins (:,) float = None, bin edges, if None, bins are linearly spaced between min and max of x
|
|
446
452
|
norm str = {'density','max'}, normalization mode, 'density' computes PDF, 'max' normalizes its maximum to 1
|
|
447
453
|
color color = None, line color
|
|
448
454
|
label str = None, legend label for line
|
|
@@ -452,14 +458,14 @@ def plotPDF(x, mode:str=None, bandwidth:float|str=None, eps:float=None, n_points
|
|
|
452
458
|
if mode is None: mode = 'normal'
|
|
453
459
|
if bandwidth is None: bandwidth = 0.05 if mode == 'polar' else 'scott'
|
|
454
460
|
if eps is None: eps = 1e-12
|
|
455
|
-
if n_points is None: n_points = 50
|
|
461
|
+
if n_points is None and bins is None: n_points = 50
|
|
456
462
|
if norm is None: norm = 'density'
|
|
457
463
|
if ax is None: ax = plt.gca()
|
|
458
464
|
if isinstance(x,tuple):
|
|
459
|
-
if color is None:
|
|
460
|
-
color = [
|
|
461
|
-
if label is None:
|
|
462
|
-
label = [
|
|
465
|
+
if color is None or isinstance(color,str):
|
|
466
|
+
color = [color] * len(x)
|
|
467
|
+
if label is None or isinstance(label,str):
|
|
468
|
+
label = [label] * len(x)
|
|
463
469
|
else:
|
|
464
470
|
x = (x,)
|
|
465
471
|
color = [color]
|
|
@@ -481,7 +487,10 @@ def plotPDF(x, mode:str=None, bandwidth:float|str=None, eps:float=None, n_points
|
|
|
481
487
|
match mode:
|
|
482
488
|
# 1. real-valued data using gaussian kernel density estimator
|
|
483
489
|
case 'normal':
|
|
484
|
-
|
|
490
|
+
if n_points is None:
|
|
491
|
+
this_grid = bins
|
|
492
|
+
else:
|
|
493
|
+
this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid
|
|
485
494
|
kde = sp.stats.gaussian_kde(data,bw_method=bandwidth)
|
|
486
495
|
this_density = kde(this_grid)
|
|
487
496
|
if norm == 'max':
|
|
@@ -493,7 +502,10 @@ def plotPDF(x, mode:str=None, bandwidth:float|str=None, eps:float=None, n_points
|
|
|
493
502
|
if data.min() <= 0:
|
|
494
503
|
raise ValueError('log-transforming data requires positive values')
|
|
495
504
|
data = np.log(data + eps) # log-transform data
|
|
496
|
-
|
|
505
|
+
if n_points is None:
|
|
506
|
+
this_grid = bins
|
|
507
|
+
else:
|
|
508
|
+
this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid in log-space
|
|
497
509
|
jacobian = np.exp(this_grid) # jacobian term to transform density back to linear
|
|
498
510
|
kde = sp.stats.gaussian_kde(data,bw_method=bandwidth)
|
|
499
511
|
this_density = kde(this_grid) / jacobian
|
|
@@ -504,7 +516,10 @@ def plotPDF(x, mode:str=None, bandwidth:float|str=None, eps:float=None, n_points
|
|
|
504
516
|
# 3. circular data using von-Mises kernel density estimator
|
|
505
517
|
case 'polar':
|
|
506
518
|
data = np.mod(data,2*np.pi)
|
|
507
|
-
|
|
519
|
+
if n_points is None:
|
|
520
|
+
this_grid = bins
|
|
521
|
+
else:
|
|
522
|
+
this_grid = np.linspace(0,2*np.pi,n_points) # linear grid in [0,2*pi]
|
|
508
523
|
this_density = np.zeros_like(this_grid)
|
|
509
524
|
for theta in data:
|
|
510
525
|
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
|
|
@@ -109,13 +111,13 @@ class regions:
|
|
|
109
111
|
self.region = {}
|
|
110
112
|
|
|
111
113
|
# 4. load spikes and store them per region
|
|
114
|
+
if anat_file is None:
|
|
115
|
+
anat_file = next(_regionDataPath().glob('*.anat'), None)
|
|
116
|
+
else:
|
|
117
|
+
anat_file = pathlib.Path(anat_file)
|
|
118
|
+
if not anat_file.exists():
|
|
119
|
+
anat_file = _regionDataPath() / anat_file
|
|
112
120
|
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
121
|
self.region = fmatoolbox.data.loadSpikeTimes(session,output='regions',anat_file=anat_file,reload=reload)
|
|
120
122
|
if ids:
|
|
121
123
|
self.region = {r: self.region[r] for r in ids}
|
|
@@ -126,6 +128,25 @@ class regions:
|
|
|
126
128
|
for id in self.ids:
|
|
127
129
|
self.region[id]['spikes'] = fmatoolbox.general.restrict(self.region[id]['spikes'],epoch_intervals)
|
|
128
130
|
|
|
131
|
+
# 5. load electrode groups and channels per region
|
|
132
|
+
try:
|
|
133
|
+
anat = fmatoolbox.data.loadAnatomyFile(anat_file)
|
|
134
|
+
anat = anat[anat['rat'] == int(self.rat)] # keep rat of interest, deduced from file name
|
|
135
|
+
anat_ids = np.unique(anat['region'])
|
|
136
|
+
tree = xml.etree.ElementTree.parse(session.with_suffix(".xml"))
|
|
137
|
+
root = tree.getroot()
|
|
138
|
+
channel_groups = root.find(".//channelGroups").findall("group")
|
|
139
|
+
channel_groups = [[int(element.text) for element in c.findall(".//channel")] for c in channel_groups]
|
|
140
|
+
if len(self.region):
|
|
141
|
+
for id in self.ids:
|
|
142
|
+
self.region[id]['channels'] = {int(e): channel_groups[e-1] for e in anat[anat['region'] == id]['electrode']}
|
|
143
|
+
else:
|
|
144
|
+
self.ids = anat_ids.tolist()
|
|
145
|
+
for id in self.ids:
|
|
146
|
+
self.region[id] = {'channels': {int(e): channel_groups[e-1] for e in anat[anat['region'] == id]['electrode']} }
|
|
147
|
+
except FileNotFoundError:
|
|
148
|
+
pass
|
|
149
|
+
|
|
129
150
|
return
|
|
130
151
|
|
|
131
152
|
|
|
@@ -166,7 +187,8 @@ class regions:
|
|
|
166
187
|
regs = regs[np.sort(idx)]
|
|
167
188
|
|
|
168
189
|
# default: all electrode groups
|
|
169
|
-
all_e_groups =
|
|
190
|
+
all_e_groups = [list(self.region[r]['e_group'].keys()) for r in self.ids if 'e_group' in self.region[r]]
|
|
191
|
+
if len(all_e_groups): all_e_groups = np.concatenate(all_e_groups)
|
|
170
192
|
if np.all(e_groups == None):
|
|
171
193
|
if exclusive and np.any(regs != None):
|
|
172
194
|
e_groups = np.array([None])
|
|
@@ -296,17 +318,36 @@ class regions:
|
|
|
296
318
|
|
|
297
319
|
|
|
298
320
|
def electrodes(self,regs=None):
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
321
|
+
"""
|
|
322
|
+
get pooled list of electrode groups for regions
|
|
323
|
+
|
|
324
|
+
arguments:
|
|
325
|
+
regs (:) string = None, electrode of these regions are returned as an array, default is all regions
|
|
326
|
+
|
|
327
|
+
output:
|
|
328
|
+
electrodes (:) int, sorted by value
|
|
329
|
+
"""
|
|
306
330
|
|
|
307
331
|
regs, _, _ = self._checkIDs(regs=regs)
|
|
308
332
|
|
|
309
333
|
return np.concatenate([list(self.region[r]['e_group'].keys()) for r in regs])
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def channels(self,regs=None):
|
|
337
|
+
"""
|
|
338
|
+
get pooled list of recording channels for regions
|
|
339
|
+
|
|
340
|
+
arguments:
|
|
341
|
+
regs (:) string = None, channels of these regions are returned as an array, default is all regions
|
|
342
|
+
|
|
343
|
+
output:
|
|
344
|
+
channels (:) int
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
regs, _, _ = self._checkIDs(regs=regs)
|
|
348
|
+
values = [list(self.region[r]['channels'].values()) for r in regs]
|
|
349
|
+
|
|
350
|
+
return np.array([x for outer in values for inner in outer for x in inner])
|
|
310
351
|
|
|
311
352
|
|
|
312
353
|
def units(self,regs=None,e_groups=None):
|
|
@@ -376,8 +417,8 @@ class regions:
|
|
|
376
417
|
|
|
377
418
|
## functions to compute quantities ##
|
|
378
419
|
|
|
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=
|
|
420
|
+
def firingRate(self, regs:Iterable[str]=None, e_groups:Iterable[int]=None, states:Iterable[str]=None, when=None, shift:bool=None,
|
|
421
|
+
window=None, step:int=None, smooth=None, norm:bool=None):
|
|
381
422
|
'''
|
|
382
423
|
get region firing rate
|
|
383
424
|
|
|
@@ -400,8 +441,10 @@ class regions:
|
|
|
400
441
|
|
|
401
442
|
if states is not None and when is not None:
|
|
402
443
|
raise ValueError("'states' and 'when' cannot be specified at the same time")
|
|
444
|
+
if shift is None: shift = False
|
|
403
445
|
if window is None: window = 0.05
|
|
404
446
|
if step is None: step = 1
|
|
447
|
+
if norm is None: norm = False
|
|
405
448
|
|
|
406
449
|
regs, e_groups, states = self._checkIDs(regs=regs,e_groups=e_groups,states=states,fuse=True)
|
|
407
450
|
|
|
@@ -450,7 +493,7 @@ class regions:
|
|
|
450
493
|
return firing_rate
|
|
451
494
|
|
|
452
495
|
|
|
453
|
-
def unitFiringRate(self,regs=None,states=None,when=None,shift=
|
|
496
|
+
def unitFiringRate(self, regs=None, states=None, when=None, shift:bool=None, window=None, step:int=None, smooth:bool=None):
|
|
454
497
|
# get units' firing rate
|
|
455
498
|
#
|
|
456
499
|
# arguments:
|
|
@@ -468,6 +511,7 @@ class regions:
|
|
|
468
511
|
|
|
469
512
|
if states is not None and when is not None:
|
|
470
513
|
raise ValueError("'states' and 'when' cannot be specified at the same time")
|
|
514
|
+
if shift is None: shift = False
|
|
471
515
|
if window is None: window = 0.05
|
|
472
516
|
if step is None: step = 1
|
|
473
517
|
|
|
@@ -498,7 +542,7 @@ class regions:
|
|
|
498
542
|
return firing_rate
|
|
499
543
|
|
|
500
544
|
|
|
501
|
-
def avalanches(self,regs=None,states=None,when=None,shift=
|
|
545
|
+
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
546
|
# compute avalanches per region from population firing rate
|
|
503
547
|
#
|
|
504
548
|
# arguments:
|
|
@@ -519,6 +563,9 @@ class regions:
|
|
|
519
563
|
# size_t (m) float, size over time, in which every avalanche is separated by a 0
|
|
520
564
|
# fr (:,r+1) float, every row is [time stamp, firing rates for r regions], optional
|
|
521
565
|
|
|
566
|
+
if thresh is None: thresh = 30
|
|
567
|
+
if return_fr is None: return_fr = False
|
|
568
|
+
|
|
522
569
|
none_state = states is None
|
|
523
570
|
regs, _, states = self._checkIDs(regs=regs,states=states,fuse=True)
|
|
524
571
|
if none_state:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|