fmatoolbox 0.1.0__py3-none-any.whl
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/__init__.py +5 -0
- fmatoolbox/analysis.py +475 -0
- fmatoolbox/data.py +546 -0
- fmatoolbox/exceptions.py +4 -0
- fmatoolbox/general.py +319 -0
- fmatoolbox/plotting.py +500 -0
- fmatoolbox/regions.py +524 -0
- fmatoolbox-0.1.0.dist-info/METADATA +31 -0
- fmatoolbox-0.1.0.dist-info/RECORD +12 -0
- fmatoolbox-0.1.0.dist-info/WHEEL +5 -0
- fmatoolbox-0.1.0.dist-info/licenses/LICENSE +7 -0
- fmatoolbox-0.1.0.dist-info/top_level.txt +1 -0
fmatoolbox/general.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
''' General-purpose data processing for FMAToolbox '''
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def consolidateIntervals(intervals, epsilon:float=0, duration:float=0):
|
|
7
|
+
# remove overlaps in a set of intervals, yielding its most compact description (the union of its elements)
|
|
8
|
+
# e.g., [[1,4],[2,6]] will become [1,6]
|
|
9
|
+
#
|
|
10
|
+
# arguments:
|
|
11
|
+
# intervals (:,2) float, rows are [start, stop] times for an interval (s); if 1d, reshaped to (1,2)
|
|
12
|
+
# epsilon float = 0, intervals with bounds closer than epsilon are also consolidated
|
|
13
|
+
# duration float = 0, trim consolidated set of intervals so that it has given total duration,
|
|
14
|
+
# if negative, duration is counted from the end and intervals are trimmed rightwards
|
|
15
|
+
#
|
|
16
|
+
# output:
|
|
17
|
+
# intervals (:,2) float, consolidated intervals (s)
|
|
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
|
|
23
|
+
if intervals.shape[1] != 2:
|
|
24
|
+
raise ValueError("'intervals' must be a (n,2) array")
|
|
25
|
+
if (intervals[:,0] > intervals[:,1]).any():
|
|
26
|
+
raise ValueError("rows of 'intervals' must be increasing")
|
|
27
|
+
|
|
28
|
+
# remove [nan nan] rows, handle empty input
|
|
29
|
+
intervals = intervals[~np.all(np.isnan(intervals),axis=1)]
|
|
30
|
+
if intervals.size == 0:
|
|
31
|
+
return intervals
|
|
32
|
+
|
|
33
|
+
# widen intervals
|
|
34
|
+
if epsilon:
|
|
35
|
+
intervals = intervals + np.array([-1,1])*epsilon
|
|
36
|
+
|
|
37
|
+
# sort by start time
|
|
38
|
+
intervals = intervals[intervals[:,0].argsort()]
|
|
39
|
+
|
|
40
|
+
# flatten and argsort to find overlaps
|
|
41
|
+
intervals = intervals.flatten()
|
|
42
|
+
ind = intervals.argsort()
|
|
43
|
+
|
|
44
|
+
# remove all ind which are followed by at least one smaller element
|
|
45
|
+
m = ind[-2:].min()
|
|
46
|
+
is_ok = [True,ind[-2] < ind[-1]]
|
|
47
|
+
for i in range(3,ind.shape[0]+1):
|
|
48
|
+
is_ok.append(ind[-i] < m)
|
|
49
|
+
m = min(ind[-i],m)
|
|
50
|
+
is_ok.reverse()
|
|
51
|
+
ind = ind[is_ok]
|
|
52
|
+
|
|
53
|
+
# remove consecutive odd elements
|
|
54
|
+
is_odd = (ind % 2).astype(bool)
|
|
55
|
+
ind = ind[np.concatenate((~is_odd[:-1] | ~is_odd[1:],[True]))]
|
|
56
|
+
|
|
57
|
+
# rebuild intervals
|
|
58
|
+
intervals = intervals[np.reshape(ind,(ind.shape[0]//2,2))]
|
|
59
|
+
|
|
60
|
+
# re-shorten intervals
|
|
61
|
+
if epsilon:
|
|
62
|
+
intervals = intervals + np.array([1,-1])*epsilon
|
|
63
|
+
|
|
64
|
+
if duration > 0:
|
|
65
|
+
cum_duration = np.cumsum(np.diff(intervals))
|
|
66
|
+
if cum_duration[-1] > duration:
|
|
67
|
+
idx = np.searchsorted(cum_duration,duration) + 1 # first interval where cumulative exceeds duration
|
|
68
|
+
intervals = intervals[:idx]
|
|
69
|
+
intervals[-1,1] -= cum_duration[idx-1] - duration
|
|
70
|
+
elif duration < 0:
|
|
71
|
+
cum_duration = np.cumsum(np.diff(intervals[::-1])) # backwards cumulative duration
|
|
72
|
+
if cum_duration[-1] > -duration:
|
|
73
|
+
idx = np.searchsorted(cum_duration,-duration) + 1
|
|
74
|
+
intervals = intervals[-idx:]
|
|
75
|
+
intervals[0,0] += cum_duration[idx-1] + duration
|
|
76
|
+
|
|
77
|
+
return intervals
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def intersectIntervals(intervals):
|
|
81
|
+
# intersect interval sets, obtaining the set of intervals which are contained in all inputs
|
|
82
|
+
#
|
|
83
|
+
# arguments:
|
|
84
|
+
# intervals (n) list of (:,2), every element is a set of [start time, stop time] intervals
|
|
85
|
+
#
|
|
86
|
+
# output:
|
|
87
|
+
# intersection (:,2) float, intersection between elements of input
|
|
88
|
+
|
|
89
|
+
# return single interval unchanged
|
|
90
|
+
if len(intervals) == 1:
|
|
91
|
+
return intervals[0]
|
|
92
|
+
|
|
93
|
+
# 1: more than 2 intervals, recursive call
|
|
94
|
+
if len(intervals) > 2:
|
|
95
|
+
intervals = [intervals[0],intersectIntervals(intervals[1:])]
|
|
96
|
+
|
|
97
|
+
# 2: intersect 2 interval sets
|
|
98
|
+
|
|
99
|
+
# consolidate every interval set
|
|
100
|
+
intervals = [consolidateIntervals(i) for i in intervals]
|
|
101
|
+
|
|
102
|
+
if intervals[0].size == 0:
|
|
103
|
+
return intervals[0]
|
|
104
|
+
|
|
105
|
+
# flatten both sets
|
|
106
|
+
a = intervals[0].flatten()
|
|
107
|
+
b = intervals[1].flatten()
|
|
108
|
+
|
|
109
|
+
# ind[i] is odd iff a[i] falls in an interval of b
|
|
110
|
+
ind = np.digitize(a,b)
|
|
111
|
+
|
|
112
|
+
# handle case when a ∋ [10,20] and b ∋ [20,35] : interval [20,20] must not be in result
|
|
113
|
+
end_nz = ((ind != 0) & (np.arange(ind.shape[0]) % 2)).astype(bool) # end_nz[i] is 1 iff ind[i] is not 0 and i is odd
|
|
114
|
+
change_ind = a[end_nz] == b[ind[end_nz]-1] # change_ind[j] is 1 iff corresponding interval must be shortened
|
|
115
|
+
find_end_nz = np.where(end_nz)[0]
|
|
116
|
+
ind[find_end_nz[change_ind]] -= 1
|
|
117
|
+
|
|
118
|
+
# if ind[2*i-1], ind[2*i] are equal and even, it's an interval of a to exclude entirely
|
|
119
|
+
keep_ind = ((ind[::2] % 2).astype(bool) | (ind[::2] != ind[1::2])).repeat(2)
|
|
120
|
+
ind = ind[keep_ind]
|
|
121
|
+
a = a[keep_ind]
|
|
122
|
+
if a.size == 0:
|
|
123
|
+
return np.zeros((0,2))
|
|
124
|
+
|
|
125
|
+
# odd_ind[i] is index of an odd element of ind, which will be replaced by a[odd_ind[i]]
|
|
126
|
+
odd_ind = np.where(ind % 2)[0]
|
|
127
|
+
# round start to lower even number and stop to lower odd number
|
|
128
|
+
start = ind[::2] - ind[::2] % 2
|
|
129
|
+
stop = ind[1::2] -1 + ind[1::2] % 2
|
|
130
|
+
# expand each start[i], stop[i] pair to include intervals in between
|
|
131
|
+
new_ind = np.concatenate([np.arange(start[i],stop[i]+1) for i in range(start.shape[0])]).astype(int)
|
|
132
|
+
# remap odd_ind to point elements of new_ind, which will have to be drawn from a
|
|
133
|
+
remapping = np.array([np.ones(start.shape),stop-start]).flatten('F').cumsum() - 1
|
|
134
|
+
new_odd_ind = remapping[odd_ind].astype(int)
|
|
135
|
+
|
|
136
|
+
# initialize intervals as [b[new_ind[i]],b[new_ind[i+1]]]
|
|
137
|
+
intersection = b[new_ind]
|
|
138
|
+
# when ind[j] was odd, replace corresponding element with a[odd_ind[j]]
|
|
139
|
+
intersection[new_odd_ind] = a[odd_ind]
|
|
140
|
+
# return as interval set
|
|
141
|
+
intersection = intersection.reshape((intersection.shape[0]//2,2))
|
|
142
|
+
|
|
143
|
+
return intersection
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def restrict(samples,intervals,shift=False,s_ind=False,i_ind=False):
|
|
147
|
+
# keep only samples falling in a set of intervals
|
|
148
|
+
#
|
|
149
|
+
# arguments:
|
|
150
|
+
# samples (:,:,) float, rows are [time stamp, value1, value2, ...]; if 1d, reshaped to (:,1)
|
|
151
|
+
# intervals (:,2) float, rows are [start, stop] times for an interval; if 1d, reshaped to (1,2)
|
|
152
|
+
# shift bool = False, if True, shift remaining epochs together in time
|
|
153
|
+
# s_ind bool = False, if True, return also Is
|
|
154
|
+
# i_ind bool = False, if True, return also Ii
|
|
155
|
+
#
|
|
156
|
+
# output:
|
|
157
|
+
# samples (p,m) float, restricted samples, i. e., rows of 'samples' whose first element falls into 'intervals'
|
|
158
|
+
# Is (n) bool, optional, indicese of original samples which were kept
|
|
159
|
+
# Ii (n) bool, optional, indicese of intervals which contain kept samples
|
|
160
|
+
|
|
161
|
+
samples = np.asarray(samples)
|
|
162
|
+
if samples.size == 0:
|
|
163
|
+
samples = np.array([])
|
|
164
|
+
|
|
165
|
+
# promote 1d arrays to 2d
|
|
166
|
+
squeeze = samples.ndim == 1
|
|
167
|
+
if squeeze:
|
|
168
|
+
samples = samples.reshape((-1,1))
|
|
169
|
+
|
|
170
|
+
# consolidate intervals to use vectorized algorithm
|
|
171
|
+
intervals = consolidateIntervals(intervals)
|
|
172
|
+
|
|
173
|
+
# assign and index to each sample time, only odd indeces belong to intervals
|
|
174
|
+
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]
|
|
178
|
+
|
|
179
|
+
if shift:
|
|
180
|
+
# cumulative shifts: distances between intervals
|
|
181
|
+
ii_distance = intervals[1:,0] - intervals[:-1,1]
|
|
182
|
+
cum_shift = np.concatenate(([0],np.cumsum(ii_distance)))
|
|
183
|
+
# assign cumulative shifts to samples
|
|
184
|
+
shifts = cum_shift[Ii]
|
|
185
|
+
samples[:,0] = samples[:,0] - shifts - intervals[0,0]
|
|
186
|
+
|
|
187
|
+
if squeeze:
|
|
188
|
+
samples = samples.reshape(-1)
|
|
189
|
+
|
|
190
|
+
if not s_ind and not i_ind:
|
|
191
|
+
return samples
|
|
192
|
+
out = (samples, is_ok, Ii) # prepare tuple to return requested outputs
|
|
193
|
+
return out[:2+i_ind:2-s_ind]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def unshift(samples,intervals):
|
|
197
|
+
# perform the opposite operation of option 'shift' in restrict
|
|
198
|
+
#
|
|
199
|
+
# arguments:
|
|
200
|
+
# samples (:,:,) float, rows are [time stamp, value1, value2, ...]; if 1d, reshaped to (:,1)
|
|
201
|
+
# intervals (:,2) float, rows are [start, stop] times (s) for an interval, previously used to shift `samples`
|
|
202
|
+
|
|
203
|
+
samples = np.asarray(samples,dtype=float)
|
|
204
|
+
intervals = np.array(intervals,ndmin=2)
|
|
205
|
+
# promote 1d arrays to 2d
|
|
206
|
+
if samples.ndim == 1:
|
|
207
|
+
samples = samples.reshape((-1, 1))
|
|
208
|
+
|
|
209
|
+
shifted_end = np.cumsum(np.diff(intervals,axis=1).flatten())
|
|
210
|
+
shifted_start = np.concatenate(([0],shifted_end[:-1]))
|
|
211
|
+
shifted_intervals = np.column_stack((shifted_start,shifted_end))
|
|
212
|
+
|
|
213
|
+
to_add = intervals[:,0] - shifted_start # offset to add to each sample
|
|
214
|
+
_, is_ok, int_ind = restrict(samples[:,0],shifted_intervals,s_ind=True,i_ind=True)
|
|
215
|
+
|
|
216
|
+
samples[~is_ok,0] = np.nan
|
|
217
|
+
samples[is_ok,0] += to_add[int_ind]
|
|
218
|
+
|
|
219
|
+
return samples
|
|
220
|
+
|
|
221
|
+
|
|
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
|
+
|
|
234
|
+
events = np.asarray(events)
|
|
235
|
+
if intervals is not None:
|
|
236
|
+
intervals = np.array(intervals,ndmin=2)
|
|
237
|
+
n_dim = events.ndim
|
|
238
|
+
events = restrict(events,intervals,shift=True)
|
|
239
|
+
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)
|
|
251
|
+
else:
|
|
252
|
+
|
|
253
|
+
times = events[:,0].reshape((-1,1))
|
|
254
|
+
ids = events[:,1:]
|
|
255
|
+
unique_ids = np.unique(ids,axis=0)
|
|
256
|
+
|
|
257
|
+
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
|
+
|
|
264
|
+
# sort by time
|
|
265
|
+
shuffled = np.concatenate(shuffled)
|
|
266
|
+
shuffled = shuffled[shuffled[:,0].argsort()]
|
|
267
|
+
|
|
268
|
+
if intervals is not None:
|
|
269
|
+
shuffled = unshift(shuffled,intervals)
|
|
270
|
+
if n_dim == 1:
|
|
271
|
+
shuffled = shuffled.ravel()
|
|
272
|
+
|
|
273
|
+
return shuffled
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def subtractIntervals(a,b):
|
|
277
|
+
# subtract from an interval set its intersection with a second set
|
|
278
|
+
#
|
|
279
|
+
# arguments:
|
|
280
|
+
# a, b (:,2) float, every row is [start time, stop time]
|
|
281
|
+
#
|
|
282
|
+
# output:
|
|
283
|
+
# c (:,2) float, a \ b
|
|
284
|
+
|
|
285
|
+
# consolidate and flatten
|
|
286
|
+
a = consolidateIntervals(a).flatten()
|
|
287
|
+
b = consolidateIntervals(b).flatten()
|
|
288
|
+
if a.size == 0:
|
|
289
|
+
return a
|
|
290
|
+
|
|
291
|
+
# ind[i] is odd iff a[i] falls in an interval of b
|
|
292
|
+
ind = np.digitize(a,b)
|
|
293
|
+
|
|
294
|
+
# if ind[2*i-1], ind[2*i] are equal and odd, it's an interval of a to exclude entirely
|
|
295
|
+
keep_ind = (~(ind[::2] % 2).astype(bool) | (ind[::2] != ind[1::2])).repeat(2)
|
|
296
|
+
ind = ind[keep_ind]
|
|
297
|
+
a = a[keep_ind]
|
|
298
|
+
|
|
299
|
+
# even_ind[i] is index of an even element of ind, which will be replaced by a[even_ind[i]]
|
|
300
|
+
even_ind = np.where(ind % 2 == 0)[0]
|
|
301
|
+
# round start to upper even number and stop to upper odd number
|
|
302
|
+
start = ind[::2] + ind[::2] % 2
|
|
303
|
+
stop = ind[1::2] + 1 - ind[1::2] % 2
|
|
304
|
+
# expand each start[i], stop[i] pair to include intervals in between
|
|
305
|
+
new_ind = np.concatenate([np.arange(start[i],stop[i]+1) for i in range(start.shape[0])]).astype(int)
|
|
306
|
+
# remap even_ind to point elements of new_ind, which will have to be drawn from a
|
|
307
|
+
remapping = np.array([np.ones(start.shape),stop-start]).flatten('F').cumsum() - 1
|
|
308
|
+
new_even_ind = remapping[even_ind].astype(int)
|
|
309
|
+
|
|
310
|
+
# initialize intervals as [b[new_ind[i]],b[new_ind[i+1]]]
|
|
311
|
+
new_ind -= 1
|
|
312
|
+
new_ind[new_even_ind] = 0 # place holder
|
|
313
|
+
c = b[new_ind]
|
|
314
|
+
# when ind[j] was even, replace corresponding element with a[even_ind[j]]
|
|
315
|
+
c[new_even_ind] = a[even_ind]
|
|
316
|
+
# return as interval set
|
|
317
|
+
c = c.reshape((c.shape[0]//2,2))
|
|
318
|
+
|
|
319
|
+
return c
|