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 ADDED
@@ -0,0 +1,4 @@
1
+ from .types import *
2
+ from .utils import *
3
+ from .units import *
4
+ from .loader import *
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