fmatoolbox 0.1.0__tar.gz → 0.1.2__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,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmatoolbox
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Python port of FMAToolbox
5
5
  Author: Pietro Bozzo
6
6
  License-Expression: GPL-3.0-or-later
7
7
  Project-URL: Homepage, https://github.com/pietro-bozzo/FMAToolboxPython
8
8
  Project-URL: Repository, https://github.com/pietro-bozzo/FMAToolboxPython
9
9
  Project-URL: Issues, https://github.com/pietro-bozzo/FMAToolboxPython/issues
10
- Requires-Python: >=3.9
10
+ Requires-Python: >=3.11
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
13
  Requires-Dist: numpy
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fmatoolbox"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Python port of FMAToolbox"
9
9
  authors = [{name = "Pietro Bozzo"}]
10
10
  readme = "README.md"
11
11
  license = "GPL-3.0-or-later"
12
12
 
13
- requires-python = ">=3.9"
13
+ requires-python = ">=3.11"
14
14
  dependencies = [
15
15
  "numpy",
16
16
  "scipy",
@@ -7,9 +7,86 @@ import sklearn.decomposition as skdc
7
7
  import skimage.filters as skif
8
8
  import joblib
9
9
  import statsmodels.stats.multitest
10
+ import warnings
10
11
  from typing import Callable
11
12
 
12
13
 
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
+ '''
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
+
18
+ arguments:
19
+ samples (n,:) float, every row is either [sample time (s)] or [sample time (s), process id]; if 1d, interpreted as (n,1)
20
+ start float = min(samples[:,0]) s, time to start count at
21
+ stop float = max(samples[:,0]) s, time to stop count at
22
+ bin float = 0.05 s, time bin to count samples
23
+ step int = 1, rate is computed in windows of length 'bin' and overlap 'bin' / 'step', default is no overlap
24
+ smooth float = None, gaussian kernel std to smooth rate over time
25
+ g_range (2) int, range of groups (process ids) to consider, default is [min(samples[:,1]), max(samples[:,1])]
26
+ (boundaries included, only for 2-columns 'samples')
27
+
28
+ output:
29
+ rate (:,g+1) float, every row is [time stamp, rates for g processes], g is 1 if 'samples' has just one column
30
+ '''
31
+
32
+ # validate input
33
+ samples = np.asarray(samples)
34
+ if bin is None: bin = 0.05
35
+ if step is None: step = 1
36
+ if step % 1 or step == 0:
37
+ raise ValueError("'step' must be a non-zero integer")
38
+
39
+ groups = []
40
+ if samples.ndim == 1:
41
+ times = samples
42
+ elif samples.shape[1] == 1:
43
+ times = samples.reshape(-1)
44
+ else:
45
+ times = samples[:,0]
46
+ groups = samples[:,1]
47
+ if g_range is None:
48
+ g_range = np.unique(groups).astype(int)[[0,-1]]
49
+ else:
50
+ g_range = np.array(g_range,dtype=int)
51
+ if start is None: start = times.min()
52
+ if stop is None: stop = times.max()
53
+
54
+ def histogramVectorized(x,nbins):
55
+ a, b = x.shape
56
+ # assign bin indeces
57
+ bin_idx = (x / bin).astype(int) # bin_idx = 0, bin_idx = n_bins + 1 after 'stop'
58
+ bin_idx = np.clip(bin_idx,0,nbins-1)
59
+ offset = np.arange(b)[None,:] * nbins
60
+ hist = np.bincount((bin_idx+offset).ravel(), minlength=nbins*b).reshape(b,nbins).T
61
+ return hist
62
+
63
+ # repeat 'times' by 'step' times
64
+ times = np.tile(times[:,None],(1,step)) # (times, step)
65
+ # shift by: 'start', 'bin' to add bin before 'start', overlap for each repetition
66
+ times = times - start + bin - np.arange(step)*(bin/step)
67
+ # parameters
68
+ n_bins = int((stop + bin - start) // bin)
69
+ n_bins2 = n_bins + 2 # add two bins: first for 'samples' before 'start', last for 'samples' after 'stop'
70
+ if len(groups) == 0:
71
+ hist = histogramVectorized(times,n_bins2)
72
+ rate = (hist[1:-1] / bin).ravel() # remove data outside [start, stop], convert to rate (Hz), flatten
73
+ else:
74
+ rate = []
75
+ for g in range(g_range[0],g_range[-1] + 1):
76
+ hist = histogramVectorized(times[groups==g],n_bins2)
77
+ rate.append((hist[1:-1]).ravel())
78
+ rate = np.array(rate).T / bin
79
+
80
+ # make time axis
81
+ t = bin/step/2 + np.linspace(0,(n_bins-1)*bin,n_bins*step)
82
+
83
+ # apply smoothing
84
+ if smooth is not None:
85
+ firing_rate = sp.ndimage.gaussian_filter(firing_rate,smooth,axes=0)
86
+
87
+ return np.column_stack((t,rate))
88
+
89
+
13
90
  def firingRate(spikes, start:float=None, stop:float=None, bin_size:float=None, step:int=None, smooth:float=None, u_range:tuple[int,int]=None):
14
91
  # estimate istantaneous firing rate from spike times
15
92
  #
@@ -26,6 +103,8 @@ def firingRate(spikes, start:float=None, stop:float=None, bin_size:float=None, s
26
103
  # output:
27
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
28
105
 
106
+ warnings.warn("firingRate is deprecated, use istantaneousRate instead!", DeprecationWarning)
107
+
29
108
  # validate input
30
109
  spikes = np.asarray(spikes)
31
110
  if bin_size is None: bin_size = 0.05
@@ -343,41 +422,35 @@ def reactivationStrength(raster, templates, threshold:float=5):
343
422
 
344
423
  # --- statistics functions ---
345
424
 
346
- def MCpValue(surrogate,real,alternative='two-sided'):
347
- """
348
- Compute Monte Carlo p-values comparing real statistics to surrogate distributions
349
-
350
- Parameters
351
- ----------
352
- surrogate : array_like, shape (n_surrogates, n_features)
353
- surrogate statistics
354
- real : array_like, shape (n_features,)
355
- observed statistics
356
- alternative : {"two-sided", "greater", "less"}
357
- direction of the test
358
-
359
- Returns
360
- -------
361
- pvals : ndarray, shape (n_features,)
362
- Monte Carlo p-values
363
- """
425
+ def MCpValue(surrogate,observed,alternative='two-sided'):
426
+ '''
427
+ compute Monte Carlo p-values comparing observed statistics to surrogate distributions
428
+
429
+ arguments:
430
+ surrogate (s,f) float, surrogate statistics; s: # surrogates, f: # features
431
+ observed (f,) float, observed statistics
432
+ alternative str = {"two-sided", "greater", "less"}, test direction
433
+
434
+ output:
435
+ pvals (f,) float, Monte Carlo p-values
436
+ '''
364
437
 
365
438
  surrogate = np.asarray(surrogate)
366
- real = np.asarray(real).ravel()
439
+ observed = np.asarray(observed).ravel()
367
440
  if surrogate.ndim == 1:
368
441
  surrogate = surrogate.reshape((-1,1))
369
- if surrogate.shape[1] != real.shape[0]:
370
- raise ValueError("real must have one element for every column of surrogates")
442
+ if surrogate.shape[1] != observed.shape[0]:
443
+ raise ValueError("'observed' must have one element for every column of 'surrogates'")
371
444
 
372
445
  if alternative == "greater":
373
- count = np.sum(surrogate >= real, axis=0)
446
+ count = np.sum(surrogate >= observed, axis=0)
374
447
 
375
448
  elif alternative == "less":
376
- count = np.sum(surrogate <= real, axis=0)
449
+ count = np.sum(surrogate <= observed, axis=0)
377
450
 
378
451
  elif alternative == "two-sided":
379
- greater = np.sum(surrogate >= real, axis=0)
380
- less = np.sum(surrogate <= real, axis=0)
452
+ greater = np.sum(surrogate >= observed, axis=0)
453
+ less = np.sum(surrogate <= observed, axis=0)
381
454
  count = 2 * np.minimum(greater, less)
382
455
 
383
456
  else:
@@ -388,26 +461,19 @@ def MCpValue(surrogate,real,alternative='two-sided'):
388
461
  return np.minimum(pvals, 1.0)
389
462
 
390
463
 
391
- def holmBonferroni(pvals,alpha=0.05,return_reject=False):
392
- """
464
+ def holmBonferroni(pvals, alpha:float=0.05, return_reject:bool=False):
465
+ '''
393
466
  Holm-Bonferroni correction for multiple tests
394
467
 
395
- Parameters
396
- ----------
397
- pvals : array-like
398
- array of p-values, NaNs are ignored in the correction procedure and propagated in output
399
- alpha : float = 0.05
400
- significance level
401
- return_reject : bool = False
402
- whether to also return rejection decisions
403
-
404
- Returns
405
- -------
406
- corrected : ndarray
407
- adjusted p-values, preserves input shape
408
- reject : ndarray of bool, optional
409
- true for hypothesis that can be rejected for given alpha, preserves input shape
410
- """
468
+ arguments:
469
+ pvals (n,) float, p values, NaNs are ignored in the correction procedure and propagated in output
470
+ alpha float = 0.05, significance level, must be in [0,1]
471
+ return_reject bool = False, return also rejection decisions
472
+
473
+ output:
474
+ corrected (n,) float, adjusted p values
475
+ reject (n,) bool, optional, true for hypothesis that can be rejected
476
+ '''
411
477
 
412
478
  pvals = np.asarray(pvals)
413
479
  original_shape = pvals.shape
@@ -430,45 +496,53 @@ def holmBonferroni(pvals,alpha=0.05,return_reject=False):
430
496
  return corrected
431
497
 
432
498
 
433
- def maxStatisticTest(data, surrogate, statistic=None, alpha:float=0.05, alternative:str='two-sided'):
434
- # conduct a max statistic test over time
435
- #
436
- # arguments:
437
- # data (n sessions, n times)
438
- # surrogate (n session, n times, n surrogates)
439
-
499
+ def maxStatisticTest(data, surrogate, statistic=None, group=None, alpha:float=0.05, alternative:str='two-sided'):
500
+ '''
501
+ conduct a max statistic test over time, assessing in which time points the null hypothesis about a statistic across sessions can be rejected
502
+
503
+ arguments:
504
+ data (sessions, times) float
505
+ surrogate (sessions, times, surrogates) float
506
+ group (sessions,) int, grouping variable used to aggregate sessions, the statistic is computed per group and then again over groups
507
+ alpha float = 0.05, significance level, must be in [0,1]
508
+ alternative str = {'two-sided','grater','less'}, test direction, defines the null hypothesis
509
+ '''
510
+
440
511
  data = np.array(data,ndmin=2)
441
- surrogate = np.array(surrogate,ndmin=2)
512
+ surrogate = np.array(surrogate,ndmin=3)
442
513
  if data.shape[:2] != surrogate.shape[:2]:
443
- raise ValueError("'data' and 'surrogate' must have the same first two dimensions")
444
- if surrogate.ndim != 3:
445
- raise ValueError("'surrogate' must have dimensions (sessions, times, surrogates)")
514
+ raise ValueError("'data' and 'surrogate' must have the same first two dimensions (sessions, times)")
446
515
  if statistic is None:
447
516
  statistic = lambda x : np.nanmean(x,axis=0)
448
517
  n_times = data.shape[1]
449
518
  n_surrogates = surrogate.shape[2]
450
519
 
451
520
  # statistic for real and surrogate data
452
- s_real = statistic(data) # (n_times,)
453
- s_surrogate = np.zeros((n_times,n_surrogates)) # (n_times,n_surrogates)
454
- for i in range(n_surrogates):
455
- s_surrogate[:,i] = statistic(surrogate[:,:,i])
521
+ if group is None:
522
+ s_real = statistic(data) # (times,)
523
+ s_surrogate = statistic(surrogate) # (times, surrogates)
524
+ else:
525
+ unique_groups = np.unique(group)
526
+ s_real = [statistic(data[group==g]) for g in unique_groups]
527
+ s_real = statistic(s_real)
528
+ s_surrogate = [statistic(surrogate[group==g]) for g in unique_groups]
529
+ s_surrogate = statistic(s_surrogate)
456
530
 
457
531
  # p-values per time point
458
532
  if alternative == 'greater':
459
- s_surrogate = np.min(s_surrogate,axis=0) # (n_surrogates,)
460
- p = MCpValue(np.tile(s_surrogate,(n_times,1)).T,s_real,alternative) # (n_times,)
533
+ s_surrogate = np.min(s_surrogate,axis=0) # (surrogates,)
534
+ p = MCpValue(np.tile(s_surrogate,(n_times,1)).T,s_real,alternative) # (times,)
461
535
  elif alternative == 'less':
462
536
  s_surrogate = np.max(s_surrogate,axis=0)
463
537
  p = MCpValue(np.tile(s_surrogate,(n_times,1)).T,s_real,alternative)
464
538
  elif alternative == 'two-sided':
465
539
  # standardize statistic to ensure proper two-tailed test
466
- mu = np.mean(s_surrogate,axis=1) # (n_times,)
540
+ mu = np.mean(s_surrogate,axis=1) # (times,)
467
541
  sigma = np.std(s_surrogate,axis=1,ddof=1)
468
542
  s_real = np.abs((s_real - mu) / sigma) # abs(z-score( ))
469
543
  s_surrogate = (s_surrogate - mu.reshape(-1,1)) / sigma.reshape(-1,1)
470
- s_surrogate = np.max(np.abs(s_surrogate),axis=0) # max_t(abs(z-score( ))), i.e., (n_surrogates,)
471
- p = MCpValue(np.tile(s_surrogate,(n_times,1)).T,s_real,"greater")
544
+ s_surrogate = np.max(np.abs(s_surrogate),axis=0) # max_t(abs(z-score( ))), i.e., (surrogates,)
545
+ p = MCpValue(np.tile(s_surrogate,(n_times,1)).T,s_real,'greater')
472
546
  else:
473
547
  raise ValueError("'alternative' must be 'two-sided', 'greater' or 'less'")
474
548
 
@@ -16,10 +16,7 @@ def consolidateIntervals(intervals, epsilon:float=0, duration:float=0):
16
16
  # output:
17
17
  # intervals (:,2) float, consolidated intervals (s)
18
18
 
19
- try:
20
- intervals = np.array(intervals,dtype=float,ndmin=2)
21
- except Exception as e:
22
- raise TypeError("'intervals' must be convertible to a NumPy array") from e
19
+ intervals = np.array(intervals,dtype=float,ndmin=2)
23
20
  if intervals.shape[1] != 2:
24
21
  raise ValueError("'intervals' must be a (n,2) array")
25
22
  if (intervals[:,0] > intervals[:,1]).any():
@@ -143,7 +140,7 @@ def intersectIntervals(intervals):
143
140
  return intersection
144
141
 
145
142
 
146
- def restrict(samples,intervals,shift=False,s_ind=False,i_ind=False):
143
+ def restrict(samples, intervals, shift:bool=False, s_ind:bool=False, i_ind:bool=False):
147
144
  # keep only samples falling in a set of intervals
148
145
  #
149
146
  # arguments:
@@ -172,9 +169,9 @@ def restrict(samples,intervals,shift=False,s_ind=False,i_ind=False):
172
169
 
173
170
  # assign and index to each sample time, only odd indeces belong to intervals
174
171
  ind = np.digitize(samples[:,0],intervals.flatten())
175
- is_ok = (ind % 2).astype(bool) # is_ok[i] is 1 iff samples[i] must be kept
176
- samples = samples[is_ok]
177
- Ii = (ind[is_ok] - 1) // 2 # Ii[j] indexes interval that contains samples[is_ok][j]
172
+ Is = (ind % 2).astype(bool) # Is[i] is 1 iff samples[i] must be kept
173
+ samples = samples[Is]
174
+ Ii = (ind[Is] - 1) // 2 # Ii[j] indexes interval that contains samples[Is][j]
178
175
 
179
176
  if shift:
180
177
  # cumulative shifts: distances between intervals
@@ -188,16 +185,16 @@ def restrict(samples,intervals,shift=False,s_ind=False,i_ind=False):
188
185
  samples = samples.reshape(-1)
189
186
 
190
187
  if not s_ind and not i_ind:
191
- return samples
192
- out = (samples, is_ok, Ii) # prepare tuple to return requested outputs
188
+ return samples
189
+ out = (samples, Is, Ii) # prepare tuple to return requested outputs
193
190
  return out[:2+i_ind:2-s_ind]
194
191
 
195
192
 
196
- def unshift(samples,intervals):
193
+ def unshift(samples, intervals):
197
194
  # perform the opposite operation of option 'shift' in restrict
198
195
  #
199
196
  # arguments:
200
- # samples (:,:,) float, rows are [time stamp, value1, value2, ...]; if 1d, reshaped to (:,1)
197
+ # samples (:,:) float, rows are [time stamp, value1, value2, ...]; if 1d, reshaped to (:,1)
201
198
  # intervals (:,2) float, rows are [start, stop] times (s) for an interval, previously used to shift `samples`
202
199
 
203
200
  samples = np.asarray(samples,dtype=float)
@@ -219,56 +216,106 @@ def unshift(samples,intervals):
219
216
  return samples
220
217
 
221
218
 
222
- def shuffleEvents(events,offset=0,intervals=None):
223
- # shuffle events preserving their inter-event interval
224
- #
225
- # arguments:
226
- # events (n,) or (:,n) float, every row is either [event time] or [event time, event id]; if 1d, interpreted as (:,1)
227
- # offset float = 0 s, reference time, necessary when events start at a time != 0, e.g., for a portion of a recording
228
- # in [1000 s, 3000 s]; must be smaller than first event time, ignored if 'intervals' are provided
229
- # intervals (:,2) float = None, rows are [start, stop] times of intervals; if 1d, reshaped to (1,2)
230
- #
231
- # output:
232
- # shuffled (n,) or (:,n) float, shuffled events
233
-
219
+ def shuffleEvents(events, offset:float=None, n:int=None, group=None, fast:bool=None, intervals=None):
220
+ '''
221
+ shuffle point-process events (i.e., time stamps) preserving their inter-event interval
222
+
223
+ arguments:
224
+ events (s,) | (s,f) float, samples to shuffle; m: # samples, f: # features, if 2d, first column contains time stamps
225
+ offset float = 0 s, reference time, necessary when events start at a time != 0, e.g., for a portion of a recording
226
+ in [1000 s, 3000 s]; must be smaller than first event time, ignored if 'intervals' are provided
227
+ n int = 1, number of independent repetitions of the shuffling procedure, changes output shape
228
+ group (s,) int, grouping variable to shuffle events separately per group, adds output 'group'
229
+ fast bool = False, if True, 'events' must be time sorted (to save computation time)
230
+ intervals (:,2) float = None, rows are [start, stop] times of intervals to restrict shuffle into; if 1d, reshaped to (1,2)
231
+
232
+ output:
233
+ shuffled (:,:,) float, shuffled events, shape depends on 'n':
234
+ - same as 'events' if 'n' = 1
235
+ - (s,f,n), i.e., with an extra dimension for 'n' shuffling repetitions, otherwise
236
+ group (:,) int, optional, grouping variable for shuffled events, only returned if 'group' is not None, shape depends on 'n':
237
+ - (s,) if 'n' = 1
238
+ - (s,n), otherwise
239
+ '''
240
+
241
+ # default values
234
242
  events = np.asarray(events)
243
+ n_dim = events.ndim
244
+ if offset is None: offset = 0
245
+ if n is None: n = 1
246
+ if group is not None: group = np.asarray(group,dtype=int)
235
247
  if intervals is not None:
236
- intervals = np.array(intervals,ndmin=2)
237
- n_dim = events.ndim
238
- events = restrict(events,intervals,shift=True)
248
+ events, valid = restrict(events,intervals,shift=True,s_ind=True)
239
249
  offset = 0
240
-
241
- # 1. single-column input (only timestamps)
242
- if events.ndim == 1 or events.shape[1] == 1:
243
-
244
- # compute and shuffle inter-event intervals
245
- inter_event_intervals = np.diff(events,prepend=offset,axis=0)
246
- rng = np.random.default_rng()
247
- inter_event_intervals = rng.permutation(inter_event_intervals)
248
- shuffled = offset + np.cumsum(inter_event_intervals,axis=0)
249
-
250
- # 2: multiple columns (also grouping ids)
250
+ if group is not None:
251
+ group = group[valid]
252
+
253
+ # zero-repetitions case
254
+ if n == 0:
255
+ if group is not None:
256
+ return np.zeros(events.shape+(0,)), np.zeros((events.shape[0],0))
257
+ return np.zeros(events.shape+(0,))
258
+
259
+ # make 'events' 3d by appending singleton dimensions
260
+ events = events.reshape(*events.shape, *(1,) * (3 - n_dim)) # (events, features, 1)
261
+
262
+ # sort by time
263
+ if not fast:
264
+ order = events[:,0,0].argsort(axis=0)
265
+ events = events[order]
266
+ if group is not None:
267
+ group = group[order]
268
+
269
+ # repeat events 'n' times
270
+ events = np.tile(events,(1,1,n)) # (events, features, shuffle repetitions)
271
+
272
+ # core f to shuffle events of a group
273
+ rng = np.random.default_rng()
274
+ def shuffleGroup(e):
275
+ # shuffle inter-event intervals (IEI)
276
+ iei = np.diff(e[:,0,:],prepend=offset,axis=0)
277
+ iei_shape = iei.shape
278
+ permutation_idx = np.argsort(rng.random(iei_shape),axis=0)
279
+ iei = iei[permutation_idx,np.arange(iei_shape[1])]
280
+ # reconstruct shuffled samples
281
+ shuffled = offset + np.cumsum(iei,axis=0) # shuffled times (events, shuffle repetitions)
282
+ features = e[permutation_idx[:,None,:], np.arange(1,e.shape[1])[None,:,None], np.arange(iei_shape[1])[None,None,:]]
283
+ shuffled = np.concatenate((shuffled[:,None],features),axis=1) # append shuffled features
284
+ return shuffled
285
+
286
+ # perform shuffling
287
+ if group is None:
288
+ shuffled = shuffleGroup(events)
251
289
  else:
252
-
253
- times = events[:,0].reshape((-1,1))
254
- ids = events[:,1:]
255
- unique_ids = np.unique(ids,axis=0)
256
-
257
290
  shuffled = []
258
- for i in unique_ids:
259
- # compute and shuffle inter-event intervals per group
260
- inter_event_intervals = np.diff(times[(ids==i).all(1)],axis=0,prepend=offset)
261
- inter_event_intervals = np.random.permutation(inter_event_intervals)
262
- shuffled.append(np.concatenate((offset+np.cumsum(inter_event_intervals,axis=0),np.repeat(i.reshape((1,-1)),len(inter_event_intervals),axis=0)),axis=1))
263
-
291
+ unique_groups, g_count = np.unique(group,return_counts=True)
292
+ for g in unique_groups:
293
+ shuffled.append(shuffleGroup(events[group==g]))
264
294
  # sort by time
265
- shuffled = np.concatenate(shuffled)
266
- shuffled = shuffled[shuffled[:,0].argsort()]
267
-
295
+ shuffled = np.concatenate(shuffled,axis=0)
296
+ order = shuffled[:,0,:].argsort(axis=0)
297
+ shuffled = shuffled[order[:,None,:], np.arange(shuffled.shape[1])[None,:,None], np.arange(shuffled.shape[2])[None,None,:]]
298
+ # build 'group' for output
299
+ group = np.repeat(unique_groups,g_count) # use unique output to sort
300
+ group = group[order] # sort as 'shuffled'
301
+
302
+ # unshift events back into intervals
268
303
  if intervals is not None:
269
- shuffled = unshift(shuffled,intervals)
270
- if n_dim == 1:
271
- shuffled = shuffled.ravel()
304
+ for i in range(n):
305
+ shuffled[:,:,i] = unshift(shuffled[:,:,i],intervals)
306
+
307
+ # restore shape
308
+ if n_dim == 1 and n == 1:
309
+ shuffled = shuffled.ravel()
310
+ elif n == 1:
311
+ shuffled = shuffled[:,:,0]
312
+
313
+ if group is not None:
314
+ if n_dim == 1 and n == 1:
315
+ group = group.ravel()
316
+ elif n == 1:
317
+ group = group[:,0]
318
+ return shuffled, group
272
319
 
273
320
  return shuffled
274
321
 
@@ -315,5 +362,7 @@ def subtractIntervals(a,b):
315
362
  c[new_even_ind] = a[even_ind]
316
363
  # return as interval set
317
364
  c = c.reshape((c.shape[0]//2,2))
365
+ # remove remaining empty intervals
366
+ c = c[(np.diff(c)!=0).ravel(),:]
318
367
 
319
368
  return c
@@ -12,7 +12,7 @@ from collections.abc import Iterable
12
12
  from typing import Literal, Callable
13
13
 
14
14
 
15
- def adjustAxes(axs:Iterable[mpla.Axes], format:Literal['paper','poster']='paper'):
15
+ def adjustAxes(axs:mpla.Axes|Iterable[mpla.Axes], format:Literal['paper','poster']='paper'):
16
16
  # adjust axes properties to emprove figure appearance
17
17
  #
18
18
  # arguments:
@@ -31,11 +31,11 @@ def adjustAxes(axs:Iterable[mpla.Axes], format:Literal['paper','poster']='paper'
31
31
 
32
32
  for ax in axs:
33
33
 
34
- # remove upper and right borders
35
- ax.spines[['right','top']].set_visible(False)
34
+ # remove upper and right borders in catesian axis
35
+ [ax.spines[spine].set_visible(False) for spine in ['right','top'] if spine in ax.spines]
36
36
 
37
37
  # adjust thickness and font size
38
- ax.spines[['bottom','left']].set_linewidth(lw)
38
+ [ax.spines[spine].set_linewidth(lw) for spine in ['bottom','left','polar'] if spine in ax.spines]
39
39
  ax.tick_params(width=axw,labelsize=axtick)
40
40
  ax.xaxis.label.set_fontsize(axfont)
41
41
  ax.yaxis.label.set_fontsize(axfont)
@@ -43,7 +43,7 @@ def adjustAxes(axs:Iterable[mpla.Axes], format:Literal['paper','poster']='paper'
43
43
  return
44
44
 
45
45
 
46
- def makeFigure(title:str=None, n:tuple[int,int]=[1,1], size:tuple[float,float]=[20,10], projection=None, format:Literal['paper','poster']='paper'):
46
+ def makeFigure(title:str=None, n:tuple[int,int]=[1,1], size:tuple[float,float]=[20,10], projection:str=None, format:Literal['paper','poster']='paper'):
47
47
  # make a figure
48
48
  #
49
49
  # arguments:
@@ -72,7 +72,7 @@ def makeFigure(title:str=None, n:tuple[int,int]=[1,1], size:tuple[float,float]=[
72
72
  return fig, axs
73
73
 
74
74
 
75
- def setProp(axs:Iterable[mpla.Axes], xlabelcolor:dict[int,mplt.ColorType]=None, xtickvisible:dict[int,bool]=None, **kwargs):
75
+ def setProp(axs:mpla.Axes|Iterable[mpla.Axes], xlabelcolor:dict[int,mplt.ColorType]=None, xtickvisible:dict[int,bool]=None, **kwargs):
76
76
  # set multiple axes properties at once
77
77
  #
78
78
  # arguments:
@@ -104,7 +104,7 @@ def setProp(axs:Iterable[mpla.Axes], xlabelcolor:dict[int,mplt.ColorType]=None,
104
104
  return
105
105
 
106
106
 
107
- def saveFigure(fig,fname,format):
107
+ def saveFigure(fig, fname:str, format:str):
108
108
 
109
109
  # promote single format to iterable
110
110
  if isinstance(format,str):
@@ -115,7 +115,7 @@ def saveFigure(fig,fname,format):
115
115
  return
116
116
 
117
117
 
118
- def plotXY(data,start=None,stop=None,color=None,label=None,ax=None):
118
+ def plotXY(data, start=None, stop=None, color:mplt.ColorType=None, label=None, ax:mpla.Axes=None):
119
119
  # plot columns of 'data', interpreting the first as the x axis and all others as y values
120
120
 
121
121
  data = np.array(data, ndmin=2)
@@ -141,23 +141,26 @@ def plotXY(data,start=None,stop=None,color=None,label=None,ax=None):
141
141
  return
142
142
 
143
143
 
144
- def plotColorMap(data: npt.NDArray[np.floating], vmin: float = None, vmax: float = None, zscore = None, sortby = None, sortax: int = 0,
145
- xzoom: float = None, yzoom: float = None, x = None, y = None, aspect: float = 3/4, ax = None):
146
- # plot a 2D array as a colormap with optional normalization, sorting, and resampling
147
- #
148
- # arguments:
149
- # data (n,m) float, data to visualize, rows correspond to first dimension
150
- # vmin, vmax float = None, lower / upper bound of the colormap, if None uses autoscale
151
- # zscore int | 'all' = None, if int, specifies axis along which to z-score `data`; if 'all', compute z-score over whole array,
152
- # if None, no normalization is applied
153
- # sortby callable | 'peak' = None, optional method used to sort `data` along `sortax` (after optional z-scoring), either:
154
- # - 'peak', sort rows or columns by the index of their maximum value along the opposite axis.
155
- # - callable, must have signature ``f(data) -> array_like`` and return a 1D array used to sort data along `sortax`
156
- # sortax int = 0, axis along which sorting is performed
157
- # xzoom, yzoom float = None, optional horizontal / vertical resampling factor passed to ``scipy.ndimage.zoom`` (after sorting)
158
- # x, y (:,) float, coordinates corresponding to columns and rows of `data`, defaults are range(m) and range(n)
159
- # aspect float = 3/4, image aspect ratio
160
- # ax matplotlib.axes.Axes = matplotlib.pyplot.gca(), axes to plot in
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=0,
145
+ xzoom:float=None, yzoom:float=None, x=None, y=None, aspect:float=3/4, ax:mpla.Axes=None):
146
+ '''
147
+ plot a 2D array as a colormap with optional normalization, sorting, and resampling
148
+
149
+ arguments:
150
+ data (n,m) float, data to visualize, rows correspond to first dimension
151
+ vmin, vmax float = None, lower / upper bound of the colormap, if None uses autoscale
152
+ zscore int | 'all' = None, if int, specifies axis along which to z-score `data`; if 'all', compute z-score over whole array,
153
+ if None, no normalization is applied
154
+ sortby (:,) int | callable | 'peak' = None, optional method used to sort `data` along `sortax` (after optional z-scoring), either:
155
+ - 'peak', sort rows or columns by the index of their maximum value along the opposite axis
156
+ - (:,) int, array of indeces to sort data along `sortax`
157
+ - callable, must have signature ``f(data) -> array_like`` and return a 1D array used to sort data along `sortax`
158
+ sortax int = 0, axis along which sorting is performed
159
+ xzoom, yzoom float = None, optional horizontal / vertical resampling factor passed to ``scipy.ndimage.zoom`` (after sorting)
160
+ x, y (:,) float, coordinates corresponding to columns and rows of `data`, defaults are range(m) and range(n)
161
+ aspect float = 3/4, image aspect ratio
162
+ ax matplotlib.axes.Axes = matplotlib.pyplot.gca(), axes to plot in
163
+ '''
161
164
 
162
165
  # store original shape in case data neds to be zoomed
163
166
  n_y, n_x = data.shape
@@ -165,12 +168,15 @@ def plotColorMap(data: npt.NDArray[np.floating], vmin: float = None, vmax: float
165
168
  if zscore is not None:
166
169
  if zscore == 'all':
167
170
  zscore = None
168
- data = spst.zscore(data,axis=zscore)
171
+ data = spst.zscore(data,axis=zscore,nan_policy='omit')
169
172
 
170
173
  if sortby is not None:
171
- if sortby == 'peak':
174
+ if isinstance(sortby,str) and sortby == 'peak':
172
175
  sortby = lambda x : np.argsort(np.argmax(x,1-sortax))
173
- sort_idx = sortby(data)
176
+ elif callable(sortby):
177
+ sort_idx = sortby(data)
178
+ else:
179
+ sort_idx = sortby
174
180
  data = data[sort_idx,:] if sortax == 0 else data[:,sort_idx]
175
181
 
176
182
  if xzoom is not None or yzoom is not None:
@@ -197,21 +203,26 @@ def plotColorMap(data: npt.NDArray[np.floating], vmin: float = None, vmax: float
197
203
  return im
198
204
 
199
205
 
200
- def semPlot(x, y, ci:Callable=None, zscore:bool|int=None, color=None, mode:Literal['area','error']='area', alpha:float=0.5, label:str=None, lprop:dict=None, aprop:dict=None, ax:mpla.Axes=None):
201
- # plot mean +/- confidence intervals of matrix data
202
- #
203
- # arguments:
204
- # x (n) float, x coordinates
205
- # y (:,n) float, data to plot, each column corresponds to a value of x
206
- # ci callable, used to compute confidence intervals for every column of y, must have signature: low, high = ci(y)
207
- # 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
208
- # color color = None
209
- # mode str = 'area' | 'error', plot 'ci' either as a shaded area or as error bars
210
- # alpha float = 0.5, shaded area transparency value (only for 'area' mode)
211
- # label str = None, legend label for line
212
- # lprop dict = {}, keyword arguments passed to matplotlib.pyplot.plot
213
- # aprop dict = {}, keyword arguments passed to matplotlib.pyplot.fill_between (only for 'area' mode)
214
- # ax matplotlib.axes.Axes = matplotlib.pyplot.gca(), axes to plot in
206
+ def semPlot(x, y, ci:str|Callable=None, zscore:bool|int=None, color:mplt.ColorType=None, mode:Literal['area','error']='area', alpha:float=0.5, label:str=None, lprop:dict=None, aprop:dict=None, ax:mpla.Axes=None):
207
+ '''
208
+ plot mean +/- confidence intervals of matrix data
209
+
210
+ arguments:
211
+ x (n) float, x coordinates
212
+ y (:,n) float, data to plot, each column corresponds to a value of x
213
+ ci callable | 'nansem', used to compute confidence intervals for every column of `y`, either:
214
+ - 'nansem', standard error of the mean (SEM) for each column of `y`, ignoring missing values
215
+ - callable, must have signature ``low, high = ci(y)``
216
+ 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
217
+ color color = None
218
+ mode str = 'area' | 'error', plot 'ci' either as a shaded area or as error bars
219
+ alpha float = 0.5, shaded area transparency value (only for 'area' mode)
220
+ label str = None, legend label for line
221
+ lprop dict = {}, keyword arguments passed to matplotlib.pyplot.plot
222
+ aprop dict = {}, keyword arguments passed to matplotlib.pyplot.fill_between (only for 'area' mode)
223
+ ax matplotlib.axes.Axes = matplotlib.pyplot.gca(), axes to plot in
224
+ '''
225
+ # NOTE: should maybe change y to match plt.plot: each row of 'y' corresponds to a values of 'x'
215
226
 
216
227
  y = np.array(y,ndmin=2)
217
228
  y = y[~np.isnan(y).all(axis=1)] # ŕemove full-nan rows
@@ -227,7 +238,11 @@ def semPlot(x, y, ci:Callable=None, zscore:bool|int=None, color=None, mode:Liter
227
238
  if not (set(['edgecolor','edgecolors','ec','facecolor','facecolors','fc','color']) & aprop.keys()): aprop['color'] = color
228
239
  aprop.setdefault('alpha',alpha)
229
240
  aprop.setdefault('lw',0)
230
- if ci is None:
241
+
242
+ if isinstance(ci,str) and ci == 'nansem':
243
+ ci = lambda x : (np.nanmean(x,axis=0) - np.nanstd(x,axis=0,ddof=1)/np.sqrt(np.sum(~np.isnan(x),axis=0)),
244
+ np.nanmean(x,axis=0) + np.nanstd(x,axis=0,ddof=1)/np.sqrt(np.sum(~np.isnan(x),axis=0)))
245
+ elif ci is None:
231
246
  if y.shape[0] == 1:
232
247
  ci = lambda x : (x.flatten(), x.flatten())
233
248
  elif y.shape[0] < 500:
@@ -268,7 +283,7 @@ def semPlot(x, y, ci:Callable=None, zscore:bool|int=None, color=None, mode:Liter
268
283
  return
269
284
 
270
285
 
271
- def boxPlot(data, x=None, color=None, label=None, ax:mpla.Axes=None):
286
+ def boxPlot(data, x=None, color:mplt.ColorType=None, label=None, ax:mpla.Axes=None):
272
287
 
273
288
  if ax is None:
274
289
  ax = plt.gca()
@@ -282,6 +297,7 @@ def boxPlot(data, x=None, color=None, label=None, ax:mpla.Axes=None):
282
297
  else:
283
298
  data = [np.array(d)[~np.isnan(d)] for d in data]
284
299
 
300
+ if color is None: color = 'b'
285
301
  try:
286
302
  color = mplc.to_rgba(color)
287
303
  color = [color] * len(data)
@@ -372,7 +388,7 @@ def pBar(p, x = None, alpha=0.05, dy=1, draw=(False,True,True,True), ax:mpla.Axe
372
388
  return
373
389
 
374
390
 
375
- def pHorzLine(p,t=None,dy=None,color=None,ax=None):
391
+ def pHorzLine(p, t=None, dy=None, color:mplt.ColorType=None, ax:mpla.Axes=None):
376
392
  # p: (n_times, n_cond)
377
393
 
378
394
  p = np.asarray(p).astype(float)
@@ -403,7 +419,7 @@ def pHorzLine(p,t=None,dy=None,color=None,ax=None):
403
419
  return
404
420
 
405
421
 
406
- def plotIntervals(intervals,alpha=0.3,color='gray',ax:mpla.Axes=None):
422
+ def plotIntervals(intervals, alpha=0.3, color:mplt.ColorType='gray', ax:mpla.Axes=None):
407
423
 
408
424
  intervals = np.array(intervals,ndmin=2)
409
425
  if intervals.ndim != 2 or intervals.shape[1] != 2:
@@ -416,12 +432,29 @@ def plotIntervals(intervals,alpha=0.3,color='gray',ax:mpla.Axes=None):
416
432
  ax.axvspan(start,stop,alpha=alpha,color=color)
417
433
 
418
434
 
419
- def plotPDF(x, log:bool=False, bandwidth:float|str=None, eps:float=1e-12, n_points:int=50, color=None, label=None, ax:mpla.Axes=None, **plot_kwargs):
420
-
421
- if bandwidth is None:
422
- bandwidth = 'scott'
423
- if ax is None:
424
- ax = plt.gca()
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):
436
+ '''
437
+ estimate and plot probability density function (PDF) of data
438
+
439
+ arguments:
440
+ x (n,) tuple | array, data to plot
441
+ mode str = {'normal','log','polar'}, DESCRIBE
442
+ log bool = False, plot log-transformed PDF, DEPRECATED
443
+ bandwidth float | str = 'scott', bandwidth for gaussian kernel
444
+ eps float = 1e-12, small value used to avoid log(0)
445
+ n_points int = 50, number of points used to evaluate PDF
446
+ norm str = {'density','max'}, normalization mode, 'density' computes PDF, 'max' normalizes its maximum to 1
447
+ color color = None, line color
448
+ label str = None, legend label for line
449
+ ax matplotlib.axes.Axes = matplotlib.pyplot.gca(), axes to plot in
450
+ '''
451
+
452
+ if mode is None: mode = 'normal'
453
+ if bandwidth is None: bandwidth = 0.05 if mode == 'polar' else 'scott'
454
+ if eps is None: eps = 1e-12
455
+ if n_points is None: n_points = 50
456
+ if norm is None: norm = 'density'
457
+ if ax is None: ax = plt.gca()
425
458
  if isinstance(x,tuple):
426
459
  if color is None:
427
460
  color = [None] * len(x)
@@ -437,30 +470,54 @@ def plotPDF(x, log:bool=False, bandwidth:float|str=None, eps:float=1e-12, n_poin
437
470
  for i, data in enumerate(x):
438
471
  # cast to array and validate
439
472
  data = np.asarray(data)
473
+ data = data[~np.isnan(data)] # always ravels input: loosing a capability of gaussian_kde?
474
+ #if data.ndim == 2 and data.shape[1] == 1:
475
+ # data = data.ravel()
440
476
  if data.size < 2:
441
477
  grid.append([])
442
478
  density.append([])
443
479
  continue
444
- data = data[~np.isnan(data)] # always ravels input: loosing a capability of gaussian_kde?
445
- #if data.ndim == 2 and data.shape[1] == 1:
446
- # data = data.ravel()
447
-
448
- if log:
449
- if data.min() <= 0:
450
- raise ValueError('log-transforming data requires positive values')
451
- data = np.log(data + eps) # log-transform data
452
- this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid in log-space
453
- jacobian = np.exp(this_grid) # jacobian term to transform density back to linear
454
- else:
455
- this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid
456
480
 
457
- kde = sp.stats.gaussian_kde(data,bw_method=bandwidth)
458
- if log:
459
- this_density = kde(this_grid) / jacobian
460
- ax.loglog(jacobian,this_density,color=color[i],label=label[i],**plot_kwargs)
461
- else:
462
- this_density = kde(this_grid)
463
- ax.plot(this_grid,this_density,color=color[i],label=label[i],**plot_kwargs)
481
+ match mode:
482
+ # 1. real-valued data using gaussian kernel density estimator
483
+ case 'normal':
484
+ this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid
485
+ kde = sp.stats.gaussian_kde(data,bw_method=bandwidth)
486
+ this_density = kde(this_grid)
487
+ if norm == 'max':
488
+ this_density /= this_density.max()
489
+ ax.plot(this_grid,this_density,color=color[i],label=label[i],**plot_kwargs)
490
+
491
+ # 2. log-transformed data using gaussian kernel density estimator
492
+ case 'log':
493
+ if data.min() <= 0:
494
+ raise ValueError('log-transforming data requires positive values')
495
+ data = np.log(data + eps) # log-transform data
496
+ this_grid = np.linspace(data.min(),data.max(),n_points) # linear grid in log-space
497
+ jacobian = np.exp(this_grid) # jacobian term to transform density back to linear
498
+ kde = sp.stats.gaussian_kde(data,bw_method=bandwidth)
499
+ this_density = kde(this_grid) / jacobian
500
+ if norm == 'max':
501
+ this_density /= this_density.max()
502
+ ax.loglog(jacobian,this_density,color=color[i],label=label[i],**plot_kwargs)
503
+
504
+ # 3. circular data using von-Mises kernel density estimator
505
+ case 'polar':
506
+ data = np.mod(data,2*np.pi)
507
+ this_grid = np.linspace(0,2*np.pi,n_points) # linear grid in [0,2*pi]
508
+ this_density = np.zeros_like(this_grid)
509
+ for theta in data:
510
+ this_density += spst.vonmises.pdf(this_grid-theta,1/bandwidth)
511
+ if norm == 'max':
512
+ this_density /= this_density.max()
513
+ else:
514
+ this_density /= len(data)
515
+ this_grid = np.concatenate((this_grid,this_grid+2*np.pi))
516
+ this_density = np.concatenate((this_density,this_density))
517
+ ax.plot(this_grid,this_density,color=color[i],label=label[i],**plot_kwargs)
518
+
519
+ grid.append(this_grid)
520
+ density.append(this_density)
464
521
 
465
522
  ax.set_yticks([])
466
523
 
@@ -57,11 +57,14 @@ class regions:
57
57
  # 2. assign session epochs
58
58
  matches = self._matchEvents(phase_names,phases) if phases else phase_names
59
59
  self.phases = {m: np.stack((loaded_events[m]['beginning'],loaded_events[m]['end']),axis=1) for m in matches}
60
+ epoch_intervals = np.concatenate(list(self.phases.values()))
60
61
 
61
62
  # 3. assign states
62
63
  self.states = {s: np.stack((loaded_events[s]['col0'],loaded_events[s]['col1']),axis=1) for s in states}
63
- # if session phases are available, use them to compute special states 'all' and 'other' SHOULD ALSO RESTRICT states AND events TO phases
64
64
  if phase_names:
65
+ # restrict states to session epochs
66
+ self.states = {s: fmatoolbox.general.restrict(self.states[s],epoch_intervals) for s in self.states}
67
+ # build special states 'all' and 'other'
65
68
  self.states['all'] = np.array([[self.phases[list(self.phases)[0]][0,0],self.phases[list(self.phases)[-1]][-1,-1]]])
66
69
  self.states['other'] = self.states['all']
67
70
  for name in states:
@@ -90,6 +93,12 @@ class regions:
90
93
  e['intervals'] = np.stack((e[start],e[end]),axis=1)
91
94
  e.pop(start)
92
95
  e.pop(end)
96
+
97
+ # restrict to session epochs
98
+ if phase_names:
99
+ _, valid = fmatoolbox.general.restrict(e['intervals'],epoch_intervals,s_ind=True)
100
+ e = {k: v[valid] for k, v in e.items()}
101
+
93
102
  self.events[name] = e
94
103
 
95
104
  if ids:
@@ -115,7 +124,7 @@ class regions:
115
124
  if not self.all_events:
116
125
  # restrict spikes to session phases
117
126
  for id in self.ids:
118
- self.region[id]['spikes'] = fmatoolbox.general.restrict(self.region[id]['spikes'],self.eventIntervals())
127
+ self.region[id]['spikes'] = fmatoolbox.general.restrict(self.region[id]['spikes'],epoch_intervals)
119
128
 
120
129
  return
121
130
 
@@ -369,23 +378,25 @@ class regions:
369
378
 
370
379
  def firingRate(self, regs:Iterable[str]=None, e_groups:Iterable[int]=None, states:Iterable[str]=None, when=None, shift=False,
371
380
  window=None, step=None, smooth=None, norm=False):
372
- # get region firing rate
373
- #
374
- # arguments:
375
- # regs (n) string = None, brain regions to compute firing rate of, default is all regions
376
- # e_groups (m) int = None, electrode groups (starting at 1) to compute firing rate of, default is none
377
- # states (:) string = None, behavioral states, default is all
378
- # when DESCRIBE, same input as eventIntervals
379
- # shift bool = False, shift epochs together in time after filtering by state
380
- # window float = 0.05, window size to count spikes
381
- # step int = 1, firing rate is computed in windows of length 'binSize' and overlap 'binSize' / 'step',
382
- # default is no overlap
383
- # smooth float = None, gaussian kernel std for smoothing over time
384
- # norm bool = False, normalize by neuron number per region
385
- #
386
- # output:
387
- # rate (:,n+m+1) float, every row is [time stamp, firing rates for n regions, firing rates for m electrodes],
388
- # input order of regions and electrodes is preserved
381
+ '''
382
+ get region firing rate
383
+
384
+ arguments:
385
+ regs (n) string = None, brain regions to compute firing rate of, default is all regions
386
+ e_groups (m) int = None, electrode groups (starting at 1) to compute firing rate of, default is none
387
+ states (:) string = None, behavioral states, default is all
388
+ when DESCRIBE, same input as eventIntervals
389
+ shift bool = False, shift epochs together in time after filtering by state
390
+ window float = 0.05, window size to count spikes
391
+ step int = 1, firing rate is computed in windows of length 'binSize' and overlap 'binSize' / 'step',
392
+ default is no overlap
393
+ smooth float = None, gaussian kernel std for smoothing over time
394
+ norm bool = False, normalize by neuron number per region
395
+
396
+ output:
397
+ rate (:,n+m+1) float, every row is [time stamp, firing rates for n regions, firing rates for m electrodes],
398
+ input order of regions and electrodes is preserved
399
+ '''
389
400
 
390
401
  if states is not None and when is not None:
391
402
  raise ValueError("'states' and 'when' cannot be specified at the same time")
@@ -394,7 +405,8 @@ class regions:
394
405
 
395
406
  regs, e_groups, states = self._checkIDs(regs=regs,e_groups=e_groups,states=states,fuse=True)
396
407
 
397
- # find big holes in 'when' intervals, to speed up computation, NOTE: might not be speeding it up after all...
408
+ # find big holes in 'when' intervals, to speed up computation
409
+ # NOTE: it is actually number of time bins which defines big! Also, this looks every element of spikes many times, maybe useless!
398
410
  when = self.eventIntervals(when)
399
411
  holes = when[1:,0] - when[:-1,1] > 5000 # s
400
412
  partition_idx = np.insert(np.cumsum(holes),0,0) # partition_idx[i] indexes partition to which when[i] belongs
@@ -410,12 +422,12 @@ class regions:
410
422
 
411
423
  if np.any(regs != None):
412
424
  for r in regs:
413
- fr = fmatoolbox.analysis.firingRate(self.spikes(regs=r)[:,0],np.floor(intervals[0,0]),intervals[-1,1],window,step,smooth)
425
+ fr = fmatoolbox.analysis.istantaneousRate(self.spikes(regs=r)[:,0],start=np.floor(intervals[0,0]),stop=intervals[-1,1],bin=window,step=step,smooth=smooth)
414
426
  fr_interv.append(fr[:,1])
415
427
 
416
428
  if np.any(e_groups != None):
417
429
  for e in e_groups:
418
- fr = fmatoolbox.analysis.firingRate(self.spikes(e_groups=e)[:,0],np.floor(intervals[0,0]),intervals[-1,1],window,step,smooth)
430
+ fr = fmatoolbox.analysis.istantaneousRate(self.spikes(e_groups=e)[:,0],start=np.floor(intervals[0,0]),stop=intervals[-1,1],bin=window,step=step,smooth=smooth)
419
431
  fr_interv.append(fr[:,1])
420
432
 
421
433
  firing_rate.append(np.stack(fr_interv,1))
@@ -428,7 +440,7 @@ class regions:
428
440
  # filter by state
429
441
  if np.any(states != 'all'):
430
442
  firing_rate = fmatoolbox.general.restrict(firing_rate,self.eventIntervals([states]),shift=shift)
431
- warnings.warn("option 'states' is deprecated, use 'when' instead")
443
+ warnings.warn("option 'states' is deprecated, use 'when' instead!")
432
444
 
433
445
  # normalize
434
446
  if norm:
@@ -468,7 +480,7 @@ class regions:
468
480
  firing_rate = np.zeros((n_times[-1],n_units[-1]))
469
481
  for i, interval in enumerate(phase_intervals):
470
482
  for j, r in enumerate(regs):
471
- fr = fmatoolbox.analysis.firingRate(self.spikes(r),interval[0],interval[1],window,step,smooth,u_range=self.units(r))
483
+ fr = fmatoolbox.analysis.istantaneousRate(self.spikes(r),start=interval[0],stop=interval[1],bin=window,step=step,smooth=smooth,g_range=self.units(r))
472
484
  firing_rate[n_times[i]:n_times[i+1],n_units[j]:n_units[j+1]] = fr[:,1:]
473
485
  firing_rate[n_times[i]:n_times[i+1],0] = fr[:,0]
474
486
 
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmatoolbox
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Python port of FMAToolbox
5
5
  Author: Pietro Bozzo
6
6
  License-Expression: GPL-3.0-or-later
7
7
  Project-URL: Homepage, https://github.com/pietro-bozzo/FMAToolboxPython
8
8
  Project-URL: Repository, https://github.com/pietro-bozzo/FMAToolboxPython
9
9
  Project-URL: Issues, https://github.com/pietro-bozzo/FMAToolboxPython/issues
10
- Requires-Python: >=3.9
10
+ Requires-Python: >=3.11
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
13
  Requires-Dist: numpy
File without changes
File without changes
File without changes