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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmatoolbox
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Python port of FMAToolbox
5
5
  Author: Pietro Bozzo
6
6
  License-Expression: GPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fmatoolbox"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Python port of FMAToolbox"
9
9
  authors = [{name = "Pietro Bozzo"}]
10
10
  readme = "README.md"
@@ -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
- firing_rate = sp.ndimage.gaussian_filter(firing_rate,smooth,axes=0)
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
- ''' Session data handling functions for FMAToolbox '''
2
- from typing import Any, Callable
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
- def loadSpikeTimes(session:str, output:str='dict', anat_file:str=None, return_elec:bool=False, return_loc:bool=False, reload:bool=False):
16
- # load spikes from a session
17
- #
18
- # arguments:
19
- # session string, path to session .xml file, spike files must be in session folder
20
- # output string = None, determines output type, can be:
21
- # 'dict', dictionary of spike times per unit
22
- # 'compact', (:,2) array of [timestamps, unit ids]
23
- # 'full', (:,2) array of [timestamps, electrode groups, clusters]
24
- # 'regions', dict of {region id: dict}, entries are {'spikes': region spikes, 'units': region units}
25
- # anat_file str = None, DESCRIBE
26
- # return_elec bool = False, if true, return cluster_loc
27
- # return_loc bool = False, if true, return cluster_loc
28
- # reload bool = False, load original files, bypassing Regions/<basename>_spikes.npz backup (for 'output' = 'regions')
29
- #
30
- # output:
31
- # spikes (see 'output')
32
- # electrodes (n) float, optional, electrode id per unit
33
- # cluster_loc (n) float, optional, index of max spike-amplitude cluster per unit
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])] # keep rat of interest, deduced from file name
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.array(s,ndmin=2)
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=20000, output:str='dict', anat_file:str=None, reload:bool=False):
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 .res and .clu files
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: str, compact: bool = False):
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 from a session
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: str):
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 loadLFP(session: str):
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: str):
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
- axtick = 9 if format == 'paper' else 14
30
- axfont = 10 if format == 'paper' else 18
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=axtick)
40
- ax.xaxis.label.set_fontsize(axfont)
41
- ax.yaxis.label.set_fontsize(axfont)
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 = [None] * len(x)
461
- if label is None:
462
- label = [None] * len(x)
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
- this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid
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
- this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid in log-space
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
- this_grid = np.linspace(0,2*np.pi,n_points) # linear grid in [0,2*pi]
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
- self.session = pathlib.Path(session).parent
38
- self.basename = pathlib.Path(session).stem
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 = np.concatenate([list(self.region[r]['e_group'].keys()) for r in self.ids])
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
- # get pooled list of electrode groups for regions
300
- #
301
- # arguments:
302
- # regs (:) string = None, electrode of these regions are returned as an array, default is all regions
303
- #
304
- # output:
305
- # electrodes (:) int, sorted by value
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=False,
380
- window=None, step=None, smooth=None, norm=False):
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=False,window=None,step=None,smooth=None):
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=False,thresh=30,window=0.05,step=1,smooth=None,return_fr=False,norm=False):
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmatoolbox
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Python port of FMAToolbox
5
5
  Author: Pietro Bozzo
6
6
  License-Expression: GPL-3.0-or-later
File without changes
File without changes
File without changes