vscope 1.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.
- vscope/__init__.py +4 -0
- vscope/coherence.py +317 -0
- vscope/debleach.py +73 -0
- vscope/loader.py +222 -0
- vscope/localmotion.py +407 -0
- vscope/microcorrect.py +388 -0
- vscope/rois.py +270 -0
- vscope/types.py +363 -0
- vscope/units.py +210 -0
- vscope/utils.py +138 -0
- vscope/xyrra.py +88 -0
- vscope-1.1.0.dist-info/METADATA +13 -0
- vscope-1.1.0.dist-info/RECORD +15 -0
- vscope-1.1.0.dist-info/WHEEL +5 -0
- vscope-1.1.0.dist-info/top_level.txt +1 -0
vscope/__init__.py
ADDED
vscope/coherence.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import numbers
|
|
5
|
+
from scipy.signal.windows import dpss
|
|
6
|
+
|
|
7
|
+
from . import rois
|
|
8
|
+
from . import debleach
|
|
9
|
+
from . import utils
|
|
10
|
+
|
|
11
|
+
def pds_mtm(t, x, f_res):
|
|
12
|
+
'''PDS_MTM - Multi-taper spectral estimate
|
|
13
|
+
This is DW's adaptation of Adam Taylor's PDS_MTM code
|
|
14
|
+
ff,Pxx = PDS_MTM(tt,xx,fres) calculates one-side multi-taper
|
|
15
|
+
spectrogram.
|
|
16
|
+
|
|
17
|
+
TT [T] indicates time points.
|
|
18
|
+
XX [T] is the (optical) data.
|
|
19
|
+
FRES [scalar] is the half-width of the transform of the tapers used;
|
|
20
|
+
It must be in reciprocal units of those of TT.
|
|
21
|
+
|
|
22
|
+
FF [F] is the resulting (one-sided) frequency base.
|
|
23
|
+
Pxx [FxK] are the spectral estimates from each individual taper
|
|
24
|
+
|
|
25
|
+
Note that the nature of the beast is that the output Pxx has a
|
|
26
|
+
full width of 2*FRES even if the signal XX is perfectly sinusoidal.'''
|
|
27
|
+
|
|
28
|
+
# From Adam's comments:
|
|
29
|
+
# t is a col vector
|
|
30
|
+
# elements of t are evenly spaced and increasing
|
|
31
|
+
# x is a real matrix with the same number of rows as t
|
|
32
|
+
# f_res is the half-width of the transform of the tapers used
|
|
33
|
+
# it must be in reciprocal units of those of t
|
|
34
|
+
# N_fft is the length to which data is zero-padded before FFTing
|
|
35
|
+
# this works on the columns of x independently
|
|
36
|
+
#
|
|
37
|
+
# f is the frequncy base, which is one-sided
|
|
38
|
+
# Pxx's cols are the the one-sided spectral estimates of the cols of x
|
|
39
|
+
# Pxxs is 3D, (frequency samples)x(cols of x)x(tapers), gives the spectrum
|
|
40
|
+
# estimate for each taper
|
|
41
|
+
#
|
|
42
|
+
# we assume that x is real, and return the one-side periodogram
|
|
43
|
+
|
|
44
|
+
tapers = None
|
|
45
|
+
N_fft = 2**np.ceil(np.log2(len(t)))
|
|
46
|
+
|
|
47
|
+
N = len(t) # number of time samples
|
|
48
|
+
dt =(t[N]-t[1])/(N-1)
|
|
49
|
+
fs=1/dt
|
|
50
|
+
|
|
51
|
+
# compute nw and K
|
|
52
|
+
nw = N*dt*f_res
|
|
53
|
+
K = np.floor(2*nw-1)
|
|
54
|
+
|
|
55
|
+
tapers = dpss(N,nw,K)
|
|
56
|
+
tapers = np.reshape(tapers, [N, 1, K])
|
|
57
|
+
|
|
58
|
+
% zero-pad, taper, and do the FFT
|
|
59
|
+
x_tapered=repmat(x,[1 1 K]).*repmat(tapers,[1 N_signals 1]);
|
|
60
|
+
X=fft(x_tapered,N_fft);
|
|
61
|
+
|
|
62
|
+
% convert to PDSs by squaring and normalizing appropriately
|
|
63
|
+
Pxxs=(abs(X).^2)/fs;
|
|
64
|
+
|
|
65
|
+
% fold the positive and negative frequencies together
|
|
66
|
+
[Pxxs,f]=sum_pos_neg_freqs(Pxxs);
|
|
67
|
+
f=fs*f;
|
|
68
|
+
|
|
69
|
+
% average all the spectral estimates together
|
|
70
|
+
Pxx=mean(Pxxs,3);
|
|
71
|
+
|
|
72
|
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
73
|
+
function [Pxxs_os,f_os] = sum_pos_neg_freqs(Pxxs_ts)
|
|
74
|
+
|
|
75
|
+
% turns a two-sided PSD into a one-sided
|
|
76
|
+
% works on the the cols of Pxx_ts
|
|
77
|
+
% doesn't work for ndims>3
|
|
78
|
+
|
|
79
|
+
% get the dims of Pxx_ts
|
|
80
|
+
[N,N_signals,K]=size(Pxxs_ts);
|
|
81
|
+
|
|
82
|
+
% fold the positive and negative frequencies together
|
|
83
|
+
% hpfi = 'highest positive frequency index'
|
|
84
|
+
% also, generate frequency base
|
|
85
|
+
hpfi=ceil(N/2);
|
|
86
|
+
if mod(N,2)==0 % if N_fft is even
|
|
87
|
+
Pxxs_os=[Pxxs_ts(1:hpfi,:,:) ; zeros(1,N_signals,K) ]+...
|
|
88
|
+
[zeros(1,N_signals,K) ; flipdim(Pxxs_ts(hpfi+1:N,:,:),1) ];
|
|
89
|
+
f_os=(0:hpfi)'/N;
|
|
90
|
+
else
|
|
91
|
+
Pxxs_os=Pxxs_ts(1:hpfi,:,:)+...
|
|
92
|
+
[zeros(1,N_signals,K) ; flipdim(Pxxs_ts(hpfi+1:N_fft,:,:),1)];
|
|
93
|
+
f_os=(0:(hpfi-1))'/N;
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def psd(tt, yy, df=1/3):
|
|
98
|
+
'''PSD - Multitaper power spectrum estimate for vscope
|
|
99
|
+
f, p = PSD(t_sig, y_sig, df) calculates multitaper power
|
|
100
|
+
spectral density estimates for the signal YY (a.u.) sampled at
|
|
101
|
+
times TT (s), with a final frequency resolution of DF (Hz).
|
|
102
|
+
Returns:
|
|
103
|
+
- F: frequency vector (in Hz if t_sig is in seconds)
|
|
104
|
+
- P: power estimates at each frequency'''
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def globaltrend(x, cam, outsiderois=False):
|
|
108
|
+
'''GLOBALTREND - Extract global trend from VSD data
|
|
109
|
+
y = GLOBALTREND(x, cam) returns the global trend of the VSD image
|
|
110
|
+
sequence in the vscope data X (which must be a VScopeFile from LOAD)
|
|
111
|
+
for the given camera.
|
|
112
|
+
|
|
113
|
+
Optional arguments:
|
|
114
|
+
- OUTSIDEROIS: Set to True to calculate the trend based only on
|
|
115
|
+
those areas of the VSD image sequence that are
|
|
116
|
+
not in any ROIs.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
- Y: vector of global trend data
|
|
120
|
+
'''
|
|
121
|
+
dat = x.ccd.data[cam]
|
|
122
|
+
T, Y, X = dat.shape
|
|
123
|
+
KY = Y//32
|
|
124
|
+
KX = X//32
|
|
125
|
+
if outsiderois:
|
|
126
|
+
msk = np.logical_not(rois.allroimask(x, cam))
|
|
127
|
+
else:
|
|
128
|
+
msk = np.ones((Y,X), bool)
|
|
129
|
+
msk[:KY,:] = False
|
|
130
|
+
msk[Y-KY:,:] = False
|
|
131
|
+
msk[:,:KX] = False
|
|
132
|
+
msk[:,X-KX:] = False
|
|
133
|
+
msk = np.nonzero(msk)
|
|
134
|
+
res = np.zeros(T)
|
|
135
|
+
yy = np.zeros(T)
|
|
136
|
+
for t in range(T):
|
|
137
|
+
frm = dat[t,:,:]
|
|
138
|
+
yy[t] = np.mean(frm[msk])
|
|
139
|
+
return yy
|
|
140
|
+
|
|
141
|
+
class Reference:
|
|
142
|
+
def __init__(self):
|
|
143
|
+
self.typ = None
|
|
144
|
+
def instantiate(self, x, cam):
|
|
145
|
+
tt = utils.ccdtime(x, cam)
|
|
146
|
+
return np.zeros(len(tt))
|
|
147
|
+
|
|
148
|
+
class SineRef(Reference):
|
|
149
|
+
def __init__(self, freq_Hz, t0=0):
|
|
150
|
+
'''Construct a sinusoidal reference signal at given frequency.
|
|
151
|
+
By default, phase is aligned with t=0 in the source signal;
|
|
152
|
+
optional argument T0 changes that.'''
|
|
153
|
+
self.typ = 'sine'
|
|
154
|
+
self.freq_Hz = freq_Hz
|
|
155
|
+
self.t0 = t0
|
|
156
|
+
def instantiate(self, x, cam):
|
|
157
|
+
tt = utils.ccdtime(x, cam)
|
|
158
|
+
return np.sin(2*np.pi*(tt-self.t0)*self.freq_Hz)
|
|
159
|
+
|
|
160
|
+
class AnalogRef(Reference):
|
|
161
|
+
def __init__(self, channel, func=np.mean):
|
|
162
|
+
'''Construct a reference based on an analog input.
|
|
163
|
+
CHANNEL may be either the number or the name of an analog input
|
|
164
|
+
channel.
|
|
165
|
+
By default, analog samples are averaged across the duration of
|
|
166
|
+
a camera frame using NP.MEAN. Optional argument FUNC specifies
|
|
167
|
+
a function or lambda to change that.'''
|
|
168
|
+
self.typ = 'analog'
|
|
169
|
+
self.channel = channel
|
|
170
|
+
self.func = func
|
|
171
|
+
def instantiate(self, x, cam):
|
|
172
|
+
if isinstance(self.channel, numbers.Number):
|
|
173
|
+
idx = self.channel
|
|
174
|
+
else:
|
|
175
|
+
idx = x.analog.cids.index(self.channel)
|
|
176
|
+
return utils.ephysatccd(x, cam, x.analog.data[:,idx], self.func)
|
|
177
|
+
|
|
178
|
+
class OpticalRef(Reference):
|
|
179
|
+
def __init__(self, roi):
|
|
180
|
+
'''Construct a reference based on an ROI in the optical data.
|
|
181
|
+
ROI must be the name of an ROI that is visible on the camera
|
|
182
|
+
for which you will be calculating the coherence.'''
|
|
183
|
+
self.typ = 'optical'
|
|
184
|
+
self.roi = roi
|
|
185
|
+
def instantiate(self, x, cam):
|
|
186
|
+
return rois.extract(x, self.roi, cam)
|
|
187
|
+
class DirectRef(Reference):
|
|
188
|
+
def __init__(self, data):
|
|
189
|
+
'''Construct a reference based on direct data.
|
|
190
|
+
The DATA must have the same number of time points as the
|
|
191
|
+
total number of optical frames in the recording.'''
|
|
192
|
+
self.typ = 'direct'
|
|
193
|
+
self.data = data
|
|
194
|
+
def instantiate(self, x, cam):
|
|
195
|
+
return self.data
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def cohanalysis(x, cam, ref,
|
|
199
|
+
df_psd=1/3, df_coh=2/3,
|
|
200
|
+
f_star=None,
|
|
201
|
+
ci=1, pthresh=0.01,
|
|
202
|
+
t0=None, t1=None,
|
|
203
|
+
sig=None,
|
|
204
|
+
debleach=None,
|
|
205
|
+
trend=None):
|
|
206
|
+
'''ANALYSIS - Perform coherence analysis on VSD data
|
|
207
|
+
res = ANALYSIS(x, cam, ref) performs coherence analysis on a VScope
|
|
208
|
+
trial.
|
|
209
|
+
X must be a VScopeFile from LOAD.
|
|
210
|
+
CAM must be a camera ID.
|
|
211
|
+
REF must be a SineRef, an AnalogRef, an OpticalRef, or a DirectRef.
|
|
212
|
+
|
|
213
|
+
Optional arguments:
|
|
214
|
+
- DF_PSD: frequency resolution for power spectral density. Default
|
|
215
|
+
is 0.333 Hz.
|
|
216
|
+
- DF_COH: frequency resolution for coherence. Default is 0.667 Hz.
|
|
217
|
+
- F_STAR: override frequency at which to measure coherence. (Default
|
|
218
|
+
is to find peak in reference signal; ignored if comparing
|
|
219
|
+
to sine wave.)
|
|
220
|
+
- CI: confidence interval (units of sigma) for coherence
|
|
221
|
+
- PTHRESH: threshold for significance. Use None to omit calculations.
|
|
222
|
+
- T0: start time of analysis. Default to 3rd frame of optical trace.
|
|
223
|
+
- T1: end time of analysis. Default to penultimate frame of optical
|
|
224
|
+
trace.
|
|
225
|
+
- SIG: override what signals to use. Default is extracted ROI data.
|
|
226
|
+
- DEBLEACH: A subclass of Debleach, i.e., SalpaDebleach, PolyDebleach,
|
|
227
|
+
or ExpDebleach. Debleaching is never done on override signals.
|
|
228
|
+
- TREND - trend data to subtract, e.g., from GLOBALTREND.
|
|
229
|
+
|
|
230
|
+
Returns: RES: a dict with members:
|
|
231
|
+
- F: frequency at which results where computed (Hz)
|
|
232
|
+
- PSD: power spectrial densities of all signals at that frequency
|
|
233
|
+
- COH: complex coherence of each signal (dict with ROI numbers as keys)
|
|
234
|
+
- MAG: coherence magnitudes (dict)
|
|
235
|
+
- PHASE: coherence phases (-π to +π; dict)
|
|
236
|
+
- MAG_LO and MAG_HI: confidence limits on MAG (dicts)
|
|
237
|
+
- PHASE_LO and PHASE_HI: confidence limits on PHASE (dicts)
|
|
238
|
+
- THR: threshold for significance of MAG
|
|
239
|
+
- CC: color map for plotting (signals with MAG<THR will be gray; dict)
|
|
240
|
+
- EXTRA: dict containing more information
|
|
241
|
+
- FF: frequency vector (Hz)
|
|
242
|
+
- PSD: power spectral densities of all signals at all freqs
|
|
243
|
+
(dict of channel IDs to vectors)
|
|
244
|
+
- REFPSD: PSDs of reference at all frequencies
|
|
245
|
+
- TT: time vector
|
|
246
|
+
- SIG: detrended and debleached signals at times TT
|
|
247
|
+
(dict of channel IDs to vectors)
|
|
248
|
+
- REF: detrended reference at times TT
|
|
249
|
+
- TT0: time vector before clipping to T0...T1 (see below)
|
|
250
|
+
- SIG0: debleached but not detrended signals at times TT0
|
|
251
|
+
(dict of channel IDs to vectors)
|
|
252
|
+
- REF0: not detrended signals at times TT0
|
|
253
|
+
- IMG: raw data for first frame within the time window
|
|
254
|
+
- ROIS: original ROI coordinates, transformed into space of IMG
|
|
255
|
+
|
|
256
|
+
Since we usually do multiple comparisons, either pthresh should be
|
|
257
|
+
chosen conservatively, or, more cleverly, put in pthresh=-0.05 (or -p
|
|
258
|
+
in general), and we will automatically find the highest value of N
|
|
259
|
+
such that there are N signals significant at |pthresh|/N. In this
|
|
260
|
+
case, RES['pthr'] will end up being the p-value ultimately used.'''
|
|
261
|
+
|
|
262
|
+
t_on, t_off, ok = vscope.ccdtime(x, cam)
|
|
263
|
+
|
|
264
|
+
res = { 'extra': { } }
|
|
265
|
+
|
|
266
|
+
res['rois'] = {}
|
|
267
|
+
for k, roi in x.rois.items():
|
|
268
|
+
if cam in roi['cams']:
|
|
269
|
+
res['extra']['here'].add(k)
|
|
270
|
+
res['rois'][k] = rois.outline(x, k, cam)
|
|
271
|
+
|
|
272
|
+
ccd = x.ccd.data[cam]
|
|
273
|
+
T, Y, X = ccd.shape
|
|
274
|
+
|
|
275
|
+
if t0 is None:
|
|
276
|
+
res['extra']['img'] = ccd[max(2,T), :, :]
|
|
277
|
+
skip0 = 2
|
|
278
|
+
else:
|
|
279
|
+
idx = np.nonzero(t_on >= t0)
|
|
280
|
+
res['extra']['img'] = ccd[idx[0], :, :]
|
|
281
|
+
skip0 = np.sum(t_off<t0)
|
|
282
|
+
if t1 is None:
|
|
283
|
+
skip1 = 1
|
|
284
|
+
else:
|
|
285
|
+
skip1 = np.sum(t_on>t1)
|
|
286
|
+
|
|
287
|
+
if sig is None:
|
|
288
|
+
sig = rois.extractall(x, cam)
|
|
289
|
+
if trend is not None:
|
|
290
|
+
# Octave version does the equivalent of:
|
|
291
|
+
trend = trend - 1000
|
|
292
|
+
trend /= np.mean(trend)
|
|
293
|
+
# I do not know why
|
|
294
|
+
for k in sig:
|
|
295
|
+
sig[k] = sig[k] / trend
|
|
296
|
+
if debleach is not None:
|
|
297
|
+
sig = debleach.apply(sig, skip0, skip1)
|
|
298
|
+
|
|
299
|
+
idx = np.arange(skip0, T - skip1, int)
|
|
300
|
+
tt0 = (t_on + t_off)/2
|
|
301
|
+
tt = tt0[idx]
|
|
302
|
+
y_sig = {}
|
|
303
|
+
dblch = debleach.PolyDebleach(1)
|
|
304
|
+
for k, v in sig.items():
|
|
305
|
+
y_sig[k] = dblch.apply(v[idx])
|
|
306
|
+
y_ref = dblch.apply(ref[idx])
|
|
307
|
+
res['extra']['tt'] = tt
|
|
308
|
+
res['extra']['sig'] = y_sig
|
|
309
|
+
res['extra']['ref'] = y_ref
|
|
310
|
+
res['extra']['tt0'] = tt0
|
|
311
|
+
res['extra']['sig0'] = sig
|
|
312
|
+
res['extra']['ref'] = ref
|
|
313
|
+
|
|
314
|
+
print('Calculating PSDs')
|
|
315
|
+
refpsd = psd(tt, y_ref, df=df_psd)
|
|
316
|
+
if f_star is None:
|
|
317
|
+
f_star = refpsd.fstar
|
vscope/debleach.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+
from . import utils
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
class Debleach:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.typ = None
|
|
9
|
+
def apply(self, data, skipstart=0, skipend=0):
|
|
10
|
+
if type(data)==dict:
|
|
11
|
+
res = {}
|
|
12
|
+
for k, dat in data.items():
|
|
13
|
+
res[k] = self._apply(dat, skipstart, skipend)
|
|
14
|
+
return res
|
|
15
|
+
elif type(data)==np.ndarray:
|
|
16
|
+
data, S = utils.semiflatten(data, -1)
|
|
17
|
+
res = data.copy()
|
|
18
|
+
for n in range(data.shape[0]):
|
|
19
|
+
res[n,:] = self._apply(data[n,:], skipstart, skipend)
|
|
20
|
+
return utils.semiunflatten(res, S)
|
|
21
|
+
else:
|
|
22
|
+
raise ValueError(f'Unsupported type for debleach: {type(data)}')
|
|
23
|
+
|
|
24
|
+
def _apply(self, data, skipstart, skipend):
|
|
25
|
+
return data
|
|
26
|
+
|
|
27
|
+
class SalpaDebleach(Debleach):
|
|
28
|
+
def __init__(self, tau):
|
|
29
|
+
'''Construct a debleacher that uses SALPA.
|
|
30
|
+
TAU must be the TAU parameter for SALPA.'''
|
|
31
|
+
self.typ = 'salpa'
|
|
32
|
+
self.tau = tau
|
|
33
|
+
|
|
34
|
+
def _apply(self, data, skipstart, skipend):
|
|
35
|
+
import salpa
|
|
36
|
+
m0 = np.mean(data)
|
|
37
|
+
return salpa.salpa(data - m0, tau=self.tau,
|
|
38
|
+
t_ahead=0, t_blankdepeg=0) + m0
|
|
39
|
+
|
|
40
|
+
class PolyDebleach(Debleach):
|
|
41
|
+
def __init__(self, degree=2):
|
|
42
|
+
'''Construct a debleacher that uses polynomial subtraction.
|
|
43
|
+
By default, a second degree polynomial is removed.
|
|
44
|
+
Optional argument DEGREE changes that.'''
|
|
45
|
+
self.typ = 'poly'
|
|
46
|
+
self.degree = degree
|
|
47
|
+
|
|
48
|
+
def _apply(self, data, skipstart, skipend):
|
|
49
|
+
T = len(data)
|
|
50
|
+
tt = np.arange(T)
|
|
51
|
+
tidx = np.arange(skipstart, T-skipend, dtype=int)
|
|
52
|
+
tt = tt - np.mean(tt[tidx])
|
|
53
|
+
p = np.polyfit(tt[tidx], data[tidx], self.degree)
|
|
54
|
+
res = data.copy()
|
|
55
|
+
for k in np.arange(self.degree):
|
|
56
|
+
res -= p[k] * tt**(self.degree-k)
|
|
57
|
+
return res
|
|
58
|
+
|
|
59
|
+
class ExpDebleach(Debleach):
|
|
60
|
+
def __init__(self):
|
|
61
|
+
'''Construct a debleacher that removes an exponential trend
|
|
62
|
+
from the data.'''
|
|
63
|
+
self.typ = 'exp'
|
|
64
|
+
|
|
65
|
+
def _apply(self, data, skipstart, skipend):
|
|
66
|
+
import physfit
|
|
67
|
+
T = len(data)
|
|
68
|
+
tt = np.arange(T)
|
|
69
|
+
tidx = np.arange(skipstart, T-skipend, dtype=int)
|
|
70
|
+
tt = tt - np.mean(tt[tidx])
|
|
71
|
+
p = physfit.fit('expc', tt[tidx], data[tidx])
|
|
72
|
+
return data - p.apply(tt) + np.mean(data)
|
|
73
|
+
|
vscope/loader.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# vscope/loader.py - loading vscope data into python
|
|
2
|
+
|
|
3
|
+
# This module contains:
|
|
4
|
+
|
|
5
|
+
# - load
|
|
6
|
+
# - loadxml
|
|
7
|
+
# - loadanalog
|
|
8
|
+
# - loaddigital
|
|
9
|
+
# - loadccd
|
|
10
|
+
# - loadrois
|
|
11
|
+
|
|
12
|
+
import xml.etree.ElementTree as ET
|
|
13
|
+
import numpy as np
|
|
14
|
+
from .types import *
|
|
15
|
+
from . import utils
|
|
16
|
+
|
|
17
|
+
def loadxml(fn):
|
|
18
|
+
'''LOADXML - Load a vscope xml file
|
|
19
|
+
|
|
20
|
+
x = LOADXML(fn) loads the named XML file.'''
|
|
21
|
+
|
|
22
|
+
x = VScopeFile()
|
|
23
|
+
tree = ET.parse(fn)
|
|
24
|
+
root = tree.getroot()
|
|
25
|
+
x.parsexml(root)
|
|
26
|
+
return x
|
|
27
|
+
|
|
28
|
+
def loadrois(fn):
|
|
29
|
+
'''LOADROIS - Load ROI info from XML
|
|
30
|
+
rois = LOADROIS(fn), where FN is 'EXPT/TRIAL.xml' or 'EXPT/TRIAL-rois.xml'
|
|
31
|
+
loads the ROIs from the file.
|
|
32
|
+
Result is a dict where keys are ROI numbers and values are dicts
|
|
33
|
+
containing CAM, X, and Y. (The latter being numpy arrays.)
|
|
34
|
+
Note that ROI numbers count from 1, as in Octave.'''
|
|
35
|
+
if not fn.endswith('-rois.xml'):
|
|
36
|
+
if fn.endswith('.xml'):
|
|
37
|
+
fn = fn[:-4]
|
|
38
|
+
fn += '-rois.xml'
|
|
39
|
+
tree = ET.parse(fn)
|
|
40
|
+
root = tree.getroot()
|
|
41
|
+
top = root[0]
|
|
42
|
+
if top.tag!='rois':
|
|
43
|
+
raise KeyError('Expected rois tag')
|
|
44
|
+
rois = {}
|
|
45
|
+
for roi in top:
|
|
46
|
+
id = int(roi.attrib['id'])
|
|
47
|
+
rois[id] = {'cams': roi.attrib['cam'].split(':')}
|
|
48
|
+
if 'n' in roi.attrib:
|
|
49
|
+
n = int(roi.attrib['n'])
|
|
50
|
+
xx = np.zeros(n) + np.nan
|
|
51
|
+
yy = np.zeros(n) + np.nan
|
|
52
|
+
k = 0
|
|
53
|
+
for line in roi.text.split('\n'):
|
|
54
|
+
bits = line.split(' ')
|
|
55
|
+
if len(bits)==2:
|
|
56
|
+
xx[k] = float(bits[0])
|
|
57
|
+
yy[k] = float(bits[1])
|
|
58
|
+
k += 1
|
|
59
|
+
rois[id]['x'] = xx
|
|
60
|
+
rois[id]['y'] = yy
|
|
61
|
+
else:
|
|
62
|
+
rois[id]['x0'] = roi.attrib['x0']
|
|
63
|
+
rois[id]['y0'] = roi.attrib['y0']
|
|
64
|
+
rois[id]['R'] = roi.attrib['R']
|
|
65
|
+
rois[id]['r'] = roi.attrib['r']
|
|
66
|
+
rois[id]['a'] = roi.attrib['a']
|
|
67
|
+
return rois
|
|
68
|
+
|
|
69
|
+
def loadanalog(fn, ana):
|
|
70
|
+
'''LOADANALOG - Load analog data from VScope files
|
|
71
|
+
data = LOADANALOG(fn, ana), where FN is 'EXPT/TRIAL.xml' or
|
|
72
|
+
'EXPT/TRIAL-analog.dat' loads the analog data for the given trial.
|
|
73
|
+
ANA must be the 'analog' section from LOADXML().
|
|
74
|
+
Result is dict of channel names to vectors of T floats.
|
|
75
|
+
Voltages are converted to mV, currents to nA.'''
|
|
76
|
+
if not fn.endswith('-analog.dat'):
|
|
77
|
+
if fn.endswith('.xml'):
|
|
78
|
+
fn = fn[:-4]
|
|
79
|
+
fn += '-analog.dat'
|
|
80
|
+
if ana is None:
|
|
81
|
+
return np.zeros((0,0), dtype='float32')
|
|
82
|
+
|
|
83
|
+
with open(fn, 'rb') as f:
|
|
84
|
+
data = f.read()
|
|
85
|
+
data = np.frombuffer(data, dtype='int16')
|
|
86
|
+
T = int(ana.nscans)
|
|
87
|
+
C = int(ana.nchannels)
|
|
88
|
+
data = np.reshape(data, (T, C))
|
|
89
|
+
res = {}
|
|
90
|
+
for cid in ana.channels:
|
|
91
|
+
if 'scale' in ana.info[cid]:
|
|
92
|
+
scl = ana.info[cid]['scale']
|
|
93
|
+
else:
|
|
94
|
+
scl = '1 mV'
|
|
95
|
+
print(f'Caution: assuming scale for channel {c} is {scl}')
|
|
96
|
+
uni = scl[-1]
|
|
97
|
+
if 'offset' in ana.info[cid]:
|
|
98
|
+
off = ana.info[cid]['offset']
|
|
99
|
+
else:
|
|
100
|
+
off = '0 ' + uni
|
|
101
|
+
if uni=='V':
|
|
102
|
+
off = units.quantity(off)('mV')
|
|
103
|
+
scl = units.quantity(scl)('mV')
|
|
104
|
+
elif uni=='A':
|
|
105
|
+
off = units.quantity(off)('nA')
|
|
106
|
+
scl = units.quantity(scl)('nA')
|
|
107
|
+
else:
|
|
108
|
+
raise ValueError('Bad unit for channel %i' % c)
|
|
109
|
+
res[cid] = data[:,ana.info[cid]['idx']].astype(np.float32)*scl + off
|
|
110
|
+
return res
|
|
111
|
+
|
|
112
|
+
def loaddigital(fn, dig):
|
|
113
|
+
'''LOADDIGITAL - Load digital data from VScope files
|
|
114
|
+
data = LOADDIGITAL(fn, dig), where FN is 'EXPT/TRIAL.xml' or
|
|
115
|
+
'EXPT/TRIAL-digital.dat' loads the digital data for the given trial.
|
|
116
|
+
DIG must be the 'digital' section from LOADXML().
|
|
117
|
+
Result is a dict of line IDs to numpy array of bools.'''
|
|
118
|
+
if not fn.endswith('-digital.dat'):
|
|
119
|
+
if fn.endswith('.xml'):
|
|
120
|
+
fn = fn[:-4]
|
|
121
|
+
fn += '-digital.dat'
|
|
122
|
+
if dig is None:
|
|
123
|
+
return {}
|
|
124
|
+
|
|
125
|
+
with open(fn, 'rb') as f:
|
|
126
|
+
data = f.read()
|
|
127
|
+
data = np.frombuffer(data, dtype='uint32')
|
|
128
|
+
res = {}
|
|
129
|
+
for cid in dig.keys():
|
|
130
|
+
line = dig.info[cid]['line']
|
|
131
|
+
res[cid] = np.bitwise_and(data, 2**line) != 0
|
|
132
|
+
return res
|
|
133
|
+
|
|
134
|
+
def loadccd(fn, ccd):
|
|
135
|
+
'''LOADCCD - Load CCD imagery from VScope files
|
|
136
|
+
data = LOADCCD(fn, ccd), where FN is 'EXPT/TRIAL.xml' or
|
|
137
|
+
'EXPT/TRIAL-ccd.dat' loads the CCD data for the given trial.
|
|
138
|
+
CCD must be the 'ccd' section from LOADXML().
|
|
139
|
+
Result is a dict mapping camera names to TxYxX data (as uint16).'''
|
|
140
|
+
if not fn.endswith('-ccd.dat'):
|
|
141
|
+
if fn.endswith('.xml'):
|
|
142
|
+
fn = fn[:-4]
|
|
143
|
+
fn += '-ccd.dat'
|
|
144
|
+
if ccd is None:
|
|
145
|
+
return {}
|
|
146
|
+
|
|
147
|
+
with open(fn, 'rb') as f:
|
|
148
|
+
data = f.read()
|
|
149
|
+
data = np.frombuffer(data, dtype='uint16')
|
|
150
|
+
ncam = len(ccd)
|
|
151
|
+
frmw = np.zeros(ncam, dtype='int')
|
|
152
|
+
frmh = np.zeros(ncam, dtype='int')
|
|
153
|
+
nfrm = np.zeros(ncam, dtype='int')
|
|
154
|
+
for k in range(ncam):
|
|
155
|
+
frmw[k] = int(ccd.caminfo[k]['serpix'])
|
|
156
|
+
frmh[k] = int(ccd.caminfo[k]['parpix'])
|
|
157
|
+
nfrm[k] = int(ccd.caminfo[k]['frames'])
|
|
158
|
+
res = {}
|
|
159
|
+
offset = 0
|
|
160
|
+
for k in range(ncam):
|
|
161
|
+
dat = np.zeros((nfrm[k], frmh[k], frmw[k]), dtype=np.uint16)
|
|
162
|
+
for f in range(nfrm[k]):
|
|
163
|
+
n = frmw[k]*frmh[k]
|
|
164
|
+
frm = data[offset:offset+n]
|
|
165
|
+
offset += n
|
|
166
|
+
dat[f,:,:] = np.reshape(frm, (1, frmh[k], frmw[k]))
|
|
167
|
+
res[ccd.caminfo[k]['name']] = dat
|
|
168
|
+
return res
|
|
169
|
+
|
|
170
|
+
def _ccdframetimes(x):
|
|
171
|
+
'''_CCDFRAMETIMES - Find start and end times of CCD frames
|
|
172
|
+
(start_s, end_s) = _ccdframetimes(x), where X is a VScopeFile containing
|
|
173
|
+
both digital data and CCD information, returns a dict of camera names to
|
|
174
|
+
frame start times and frame end times.'''
|
|
175
|
+
start_s = {}
|
|
176
|
+
end_s = {}
|
|
177
|
+
|
|
178
|
+
for cam in x.ccd.keys():
|
|
179
|
+
frmid = 'Frame:' + cam
|
|
180
|
+
if x.digital and frmid in x.digital:
|
|
181
|
+
start_s[cam] = utils.rising(x.digital[frmid]) / x.digital.rate('Hz')
|
|
182
|
+
end_s[cam] = utils.falling(x.digital[frmid]) / x.digital.rate('Hz')
|
|
183
|
+
|
|
184
|
+
return (start_s, end_s)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def load(fn, loaddata=True, loadvsddata=True):
|
|
188
|
+
'''LOAD - Load all data for a VScope trial
|
|
189
|
+
x = LOAD(fn) where FN is 'EXPT/TRIAL.xml' loads the xml file and all
|
|
190
|
+
associated data files.
|
|
191
|
+
x = LOAD(fn, loaddata=False) loads only the information (incl. rois).
|
|
192
|
+
x = LOAD(fn, loadvsddata=False) loads only info and ephys
|
|
193
|
+
See also LOADXML, LOADROIS, LOADANALOG, LOADDIGITAL, LOADCCD to load
|
|
194
|
+
individual files.'''
|
|
195
|
+
res = loadxml(fn)
|
|
196
|
+
try:
|
|
197
|
+
res.rois = loadrois(fn)
|
|
198
|
+
except:
|
|
199
|
+
res.rois = None
|
|
200
|
+
if loaddata and res.analog is not None:
|
|
201
|
+
res.analog.data = loadanalog(fn, res.analog)
|
|
202
|
+
if loaddata and res.digital is not None:
|
|
203
|
+
res.digital.data = loaddigital(fn, res.digital)
|
|
204
|
+
if loaddata and res.ccd is not None:
|
|
205
|
+
if loadvsddata:
|
|
206
|
+
res.ccd.data = loadccd(fn, res.ccd)
|
|
207
|
+
(res.ccd.framestart_s, res.ccd.frameend_s) = _ccdframetimes(res)
|
|
208
|
+
for camno in range(len(res.ccd.caminfo)):
|
|
209
|
+
info = res.ccd.caminfo[camno]
|
|
210
|
+
camid = info.name
|
|
211
|
+
xform = info.transform
|
|
212
|
+
if xform.ax<0:
|
|
213
|
+
if loadvsddata:
|
|
214
|
+
res.ccd.data[camid] = np.flip(res.ccd.data[camid], 2)
|
|
215
|
+
res.ccd.caminfo[camno].transform.ax = -xform.ax
|
|
216
|
+
res.ccd.caminfo[camno].transform.bx -= xform.ax*info.serpix
|
|
217
|
+
if xform.ay<0:
|
|
218
|
+
if loadvsddata:
|
|
219
|
+
res.ccd.data[camid] = np.flip(res.ccd.data[camid], 1)
|
|
220
|
+
res.ccd.caminfo[camno].transform.ay = -xform.ay
|
|
221
|
+
res.ccd.caminfo[camno].transform.by -= xform.ay*info.parpix
|
|
222
|
+
return res
|