/
current_source_density.py
364 lines (320 loc) · 14.2 KB
/
current_source_density.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# -*- coding: utf-8 -*-
"""
*\"Current Source Density analysis (CSD) is a class of methods of analysis of
extracellular electric potentials recorded at multiple sites leading to
estimates of current sources generating the measured potentials. It is usually
applied to low-frequency part of the potential (called the Local Field
Potential, LFP) and to simultaneous recordings or to recordings taken with
fixed time reference to the onset of specific stimulus (Evoked Potentials).\"*
(Definition by Prof.Daniel K. Wójcik for Encyclopedia of Computational
Neuroscience.)
CSD is also called as Source Localization or Source Imaging in the EEG circles.
Here are CSD methods for different types of electrode configurations.
- 1D -laminar probe like electrodes.
- 2D -Microelectrode Array like
- 3D -UtahArray or multiple laminar probes.
The following methods have been implemented so far
- 1D: StandardCSD, DeltaiCSD, SplineiCSD, StepiCSD, KCSD1D
- 2D: KCSD2D, MoIKCSD (Saline layer on top of slice)
- 3D: KCSD3D
Each listed method has certain advantages. The KCSD methods, for instance, can
handle broken or irregular electrode configurations electrode.
.. autosummary::
:toctree: _toctree/current_source_density
estimate_csd
generate_lfp
"""
from __future__ import division, print_function, unicode_literals
import neo
import numpy as np
import quantities as pq
from scipy.integrate import simpson
import elephant.current_source_density_src.utility_functions as utils
from elephant.current_source_density_src import KCSD, icsd
__all__ = [
"estimate_csd",
"generate_lfp"
]
utils.patch_quantities()
available_1d = ['StandardCSD', 'DeltaiCSD', 'StepiCSD', 'SplineiCSD', 'KCSD1D']
available_2d = ['KCSD2D', 'MoIKCSD']
available_3d = ['KCSD3D']
kernel_methods = ['KCSD1D', 'KCSD2D', 'KCSD3D', 'MoIKCSD']
icsd_methods = ['DeltaiCSD', 'StepiCSD', 'SplineiCSD']
py_iCSD_toolbox = ['StandardCSD'] + icsd_methods
def estimate_csd(lfp, coordinates='coordinates', method=None,
process_estimate=True, **kwargs):
"""
Function call to compute the current source density (CSD) from
extracellular potential recordings (local field potentials - LFP) using
laminar electrodes or multi-contact electrodes with 2D or 3D geometries.
Parameters
----------
lfp : :class:`neo.core.AnalogSignal`
Positions of electrodes can be added as an array annotation
coordinates : array-like Quantity or string
Specifies the corresponding spatial coordinates of the electrodes.
Coordinates can be directly supplied by a NxM array-like Quantity
with dimension of space, where M is the number of signals in 'lfp',
and N is equal to the dimensionality of the method.
Alternatively, if coordinates is a string, the function will fetch the
coordinates, supplied in the same format, as annotation of 'lfp' by
that name.
Default: 'coordinates'
method : string
Pick a method corresponding to the setup, in this implementation
For Laminar probe style (1D), use 'KCSD1D' or 'StandardCSD',
or 'DeltaiCSD' or 'StepiCSD' or 'SplineiCSD'
For MEA probe style (2D), use 'KCSD2D', or 'MoIKCSD'
For array of laminar probes (3D), use 'KCSD3D'
Defaults to None
process_estimate : bool
In the py_iCSD_toolbox this corresponds to the filter_csd -
the parameters are passed as kwargs here ie., f_type and f_order
In the kcsd methods this corresponds to cross_validate -
the parameters are passed as kwargs here ie., lambdas and Rs
Defaults to True
kwargs : parameters to each method
The parameters corresponding to the method chosen
See the documentation of the individual method
Default is {} - picks the best parameters,
Returns
-------
Estimated CSD
neo.AnalogSignal object
annotated with the spatial coordinates
Raises
------
AttributeError
No units specified for electrode spatial coordinates
ValueError
Invalid function arguments, wrong method name, or
mismatching coordinates
TypeError
Invalid cv_param argument passed
"""
if not isinstance(lfp, neo.AnalogSignal):
raise TypeError('Parameter `lfp` must be a neo.AnalogSignal object')
if isinstance(coordinates, str):
coordinates = lfp.annotations[coordinates]
# Scale all coordinates to mm as common basis
scaled_coords = []
for coord in coordinates:
try:
scaled_coords.append(coord.rescale(pq.mm))
except AttributeError:
raise AttributeError('No units given for electrode spatial \
coordinates')
coordinates = scaled_coords
if method is None:
raise ValueError('Must specify a method of CSD implementation')
if len(coordinates) != lfp.shape[1]:
raise ValueError('Number of signals and coords is not same')
for ii in coordinates: # CHECK for Dimensionality of electrodes
if len(ii) > 3:
raise ValueError('Invalid number of coordinate positions')
dim = len(coordinates[0]) # TODO : Generic co-ordinates!
if dim == 1 and (method not in available_1d):
raise ValueError('Invalid method, Available options are:',
available_1d)
if dim == 2 and (method not in available_2d):
raise ValueError('Invalid method, Available options are:',
available_2d)
if dim == 3 and (method not in available_3d):
raise ValueError('Invalid method, Available options are:',
available_3d)
if method in kernel_methods:
input_array = np.zeros((len(lfp), lfp[0].magnitude.shape[0]))
for ii, jj in enumerate(lfp):
input_array[ii, :] = jj.rescale(pq.mV).magnitude
kernel_method = getattr(KCSD, method) # fetch the class 'KCSD1D'
lambdas = kwargs.pop('lambdas', None)
Rs = kwargs.pop('Rs', None)
k = kernel_method(np.array(coordinates), input_array.T, **kwargs)
if process_estimate:
k.cross_validate(lambdas, Rs)
estm_csd = k.values()
estm_csd = np.rollaxis(estm_csd, -1, 0)
output = neo.AnalogSignal(estm_csd * pq.uA / pq.mm**3,
t_start=lfp.t_start,
sampling_rate=lfp.sampling_rate)
if dim == 1:
output.annotate(x_coords=k.estm_x)
elif dim == 2:
output.annotate(x_coords=k.estm_x, y_coords=k.estm_y)
elif dim == 3:
output.annotate(x_coords=k.estm_x, y_coords=k.estm_y,
z_coords=k.estm_z)
elif method in py_iCSD_toolbox:
coordinates = np.array(coordinates) * coordinates[0].units
if method in icsd_methods:
try:
coordinates = coordinates.rescale(kwargs['diam'].units)
except KeyError: # Then why specify as a default in icsd?
# All iCSD methods explicitly assume a source
# diameter in contrast to the stdCSD that
# implicitly assume infinite source radius
raise ValueError(f"Parameter diam must be specified for iCSD "
f"methods: {', '.join(icsd_methods)}")
if 'f_type' in kwargs:
if (kwargs['f_type'] != 'identity') and \
(kwargs['f_order'] is None):
raise ValueError(f"The order of {kwargs['f_type']} filter must"
f" be specified")
csd_method = getattr(icsd, method) # fetch class from icsd.py file
csd_estimator = csd_method(lfp=lfp.T.magnitude * lfp.units,
coord_electrode=coordinates.flatten(),
**kwargs)
csd_pqarr = csd_estimator.get_csd()
if process_estimate:
csd_pqarr_filtered = csd_estimator.filter_csd(csd_pqarr)
output = neo.AnalogSignal(csd_pqarr_filtered.T,
t_start=lfp.t_start,
sampling_rate=lfp.sampling_rate)
else:
output = neo.AnalogSignal(csd_pqarr.T, t_start=lfp.t_start,
sampling_rate=lfp.sampling_rate)
output.annotate(x_coords=coordinates)
return output
def generate_lfp(csd_profile, x_positions, y_positions=None, z_positions=None,
x_limits=[0., 1.], y_limits=[0., 1.], z_limits=[0., 1.],
resolution=50):
"""
Forward modelling for getting the potentials for testing Current Source
Density (CSD).
Parameters
----------
csd_profile : callable
A function that computes true CSD profile.
Available options are (see ./csd/utility_functions.py)
1D : gauss_1d_dipole
2D : large_source_2D and small_source_2D
3D : gauss_3d_dipole
x_positions : np.ndarray
A 2D column vector (N x 1 array) containing the positions of the x
coordinates of the electrodes
y_positions : np.ndarray, optional
A 2D column vector (N x 1 array) containing the positions of the y
coordinates of the electrodes
Defaults to None, use in 2D or 3D cases only
z_positions : np.ndarray, optional
A 2D column vector (N x 1 array) containing the positions of the z
coordinates of the electrodes
Defaults to None, use in 3D case only
x_limits : list, optional
A list of [start, end].
The starting spatial coordinate and the ending for integration
Defaults to [0.,1.]
y_limits : list, optional
A list of [start, end].
The starting spatial coordinate and the ending for integration
Defaults to [0.,1.], use only in 2D and 3D case
z_limits : list, optional
A list of [start, end].
The starting spatial coordinate and the ending for integration
Defaults to [0.,1.], use only in 3D case
resolution : int, optional
The resolution of the integration
Defaults to 50
Returns
-------
LFP : :class:`neo.core.AnalogSignal`
The potentials created by the csd profile at the electrode positions.
The electrode positions are attached as an annotation named
'coordinates'.
Examples
--------
>>> import numpy as np
>>> from elephant.current_source_density import generate_lfp, estimate_csd
>>> from elephant.current_source_density_src.utility_functions import gauss_1d_dipole # noqa
>>> # 1. Define an array xs to x coordinate values with a length of 2304
>>> xs=np.linspace(0, 10, 2304)
>>> # 2. Run generate_lfp(gauss_1d_dipole, xs)
>>> lfp = generate_lfp(gauss_1d_dipole, xs)
>>> # 3. Run estimate_csd(lfp, method="StandardCSD")
>>> csd = estimate_csd(lfp, method="StandardCSD") #doctest: +ELLIPSIS
discrete ...
>>> # 4. Print the results
>>> print(f"LFPs: {lfp}")
LFPs: [[-0.01483716 -0.01483396 -0.01483075 ... 0.01219233 0.0121911
0.01218986]] mV
>>> print(f"CSD estimate: {csd}") #doctest: +ELLIPSIS
CSD estimate: [[-1.00025592e-04 -6.06684588e-05 ...
"""
def integrate_1D(x0, csd_x, csd, h):
m = np.sqrt((csd_x - x0) ** 2 + h ** 2) - abs(csd_x - x0)
y = csd * m
I = simpson(y, x=csd_x)
return I
def integrate_2D(x, y, xlin, ylin, csd, h, X, Y):
x = np.reshape(x, (1, 1, len(x)))
y = np.reshape(y, (1, 1, len(y)))
X = np.expand_dims(X, axis=2)
Y = np.expand_dims(Y, axis=2)
csd = np.expand_dims(csd, axis=2)
m = np.sqrt((x - X) ** 2 + (y - Y) ** 2)
np.clip(m, a_min=0.0000001, a_max=None, out=m)
y = np.arcsinh(2 * h / m) * csd
I = simpson(y.T, x=ylin)
F = simpson(I, x=xlin)
return F
def integrate_3D(x, y, z, csd, xlin, ylin, zlin, X, Y, Z):
m = np.sqrt((x - X) ** 2 + (y - Y) ** 2 + (z - Z) ** 2)
np.clip(m, a_min=0.0000001, a_max=None, out=m)
z = csd / m
Iy = simpson(np.transpose(z, (1, 0, 2)), x=zlin)
Iy = simpson(Iy, x=ylin)
F = simpson(Iy, x=xlin)
return F
dim = 1
if z_positions is not None:
dim = 3
elif y_positions is not None:
dim = 2
x = np.linspace(x_limits[0], x_limits[1], resolution)
sigma = 1.0
h = 50.
if dim == 1:
# Handle one dimensional case,
# see https://github.com/NeuralEnsemble/elephant/issues/546
if len(x_positions.shape) == 1:
x_positions = np.expand_dims(x_positions, axis=1)
chrg_x = x
csd = csd_profile(chrg_x)
pots = integrate_1D(x_positions, chrg_x, csd, h)
pots /= 2. * sigma # eq.: 26 from Potworowski et al
ele_pos = x_positions
elif dim == 2:
y = np.linspace(y_limits[0], y_limits[1], resolution)
chrg_x = np.expand_dims(x, axis=1)
chrg_y = np.expand_dims(y, axis=0)
csd = csd_profile(chrg_x, chrg_y)
pots = integrate_2D(x_positions, y_positions,
x, y,
csd, h,
chrg_x, chrg_y)
pots /= 2 * np.pi * sigma
ele_pos = np.vstack((x_positions, y_positions)).T
elif dim == 3:
y = np.linspace(y_limits[0], y_limits[1], resolution)
z = np.linspace(z_limits[0], z_limits[1], resolution)
chrg_x, chrg_y, chrg_z = np.mgrid[
x_limits[0]: x_limits[1]: complex(0, resolution),
y_limits[0]: y_limits[1]: complex(0, resolution),
z_limits[0]: z_limits[1]: complex(0, resolution)
]
csd = csd_profile(chrg_x, chrg_y, chrg_z)
pots = np.zeros(len(x_positions))
for ii in range(len(x_positions)):
pots[ii] = integrate_3D(x_positions[ii], y_positions[ii],
z_positions[ii],
csd,
x, y, z,
chrg_x, chrg_y, chrg_z)
pots /= 4 * np.pi * sigma
ele_pos = np.vstack((x_positions, y_positions, z_positions)).T
ele_pos = ele_pos * pq.mm
asig = neo.AnalogSignal(np.expand_dims(pots, axis=0),
sampling_rate=pq.kHz, units='mV')
asig.annotate(coordinates=ele_pos)
return asig