• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NeuralEnsemble / elephant / 8388038380

22 Mar 2024 09:16AM UTC coverage: 88.231% (-0.004%) from 88.235%
8388038380

push

github

web-flow
[FIX] failing unitests for neo_tools with Neo 0.13.0 (#617)

* fix failing unitests for neo_tools

* fix unit test in spike_train_synchrony

* fix unittest test_correct_transfer_of_spiketrain_attributes

* fix pep8

* remove cast to list

* edit inline comments for clarity

* add same object to tests by getting length of list like object in advance

* fix pep8

* remove space

* remove workaround for Neo Issue #1405

* remove space

* add version cap for numpy <2

* undo numpy requiremnts, unintentional commit

---------

Co-authored-by: Moritz-Alexander-Kern <moritz.kern@ymail.com>

1 of 1 new or added line in 1 file covered. (100.0%)

25 existing lines in 3 files now uncovered.

6612 of 7494 relevant lines covered (88.23%)

4.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.67
/elephant/current_source_density_src/icsd.py
1
# -*- coding: utf-8 -*-
2
"""
5✔
3
py-iCSD toolbox!
4
Translation of the core functionality of the CSDplotter MATLAB package
5
to python.
6

7
The methods were originally developed by Klas H. Pettersen, as described in:
8
Klas H. Pettersen, Anna Devor, Istvan Ulbert, Anders M. Dale, Gaute T. Einevoll,
9
Current-source density estimation based on inversion of electrostatic forward
10
solution: Effects of finite extent of neuronal activity and conductivity
11
discontinuities, Journal of Neuroscience Methods, Volume 154, Issues 1-2,
12
30 June 2006, Pages 116-133, ISSN 0165-0270,
13
http://dx.doi.org/10.1016/j.jneumeth.2005.12.005.
14
(http://www.sciencedirect.com/science/article/pii/S0165027005004541)
15

16
The method themselves are implemented as callable subclasses of the base
17
CSD class object, which sets some common attributes,
18
and a basic function for calculating the iCSD, and a generic spatial filter
19
implementation.
20

21
The raw- and filtered CSD estimates are returned as Quantity arrays.
22

23
Requires pylab environment to work, i.e numpy+scipy+matplotlib, with the
24
addition of quantities (http://pythonhosted.org/quantities) and
25
neo (https://pythonhosted.org/neo)-
26

27
Original implementation from CSDplotter-0.1.1
28
(http://software.incf.org/software/csdplotter) by Klas. H. Pettersen 2005.
29

30
Written by:
31
- Espen.Hagen@umb.no, 2010,
32
- e.hagen@fz-juelich.de, 2015-2016
33

34
"""
35

36
import numpy as np
5✔
37
import scipy.integrate as si
5✔
38
import scipy.signal as ss
5✔
39
import quantities as pq
5✔
40

41

42
class CSD(object):
5✔
43
    """Base iCSD class"""
44
    def __init__(self, lfp, f_type='gaussian', f_order=(3, 1)):
5✔
45
        """Initialize parent class iCSD
46

47
        Parameters
48
        ----------
49
        lfp : np.ndarray * quantity.Quantity
50
            LFP signal of shape (# channels, # time steps)
51
        f_type : str
52
            type of spatial filter, must be a scipy.signal filter design method
53
        f_order : list
54
            settings for spatial filter, arg passed to  filter design function
55
        """
56
        self.name = 'CSD estimate parent class'
5✔
57
        self.lfp = lfp
5✔
58
        self.f_matrix = np.eye(lfp.shape[0]) * pq.m**3 / pq.S
5✔
59
        self.f_type = f_type
5✔
60
        self.f_order = f_order
5✔
61

62
    def get_csd(self, ):
5✔
63
        """
64
        Perform the CSD estimate from the LFP and forward matrix F, i.e as
65
        CSD=F**-1*LFP
66

67
        Arguments
68
        ---------
69

70
        Returns
71
        -------
72
        csd : np.ndarray * quantity.Quantity
73
            Array with the csd estimate
74
        """
75
        csd = np.linalg.solve(self.f_matrix, self.lfp)
5✔
76

77
        return csd * (self.f_matrix.units**-1 * self.lfp.units).simplified
5✔
78

79
    def filter_csd(self, csd, filterfunction='convolve'):
5✔
80
        """
81
        Spatial filtering of the CSD estimate, using an N-point filter
82

83
        Arguments
84
        ---------
85
        csd : np.ndarrray * quantity.Quantity
86
            Array with the csd estimate
87
        filterfunction : str
88
            'filtfilt' or 'convolve'. Apply spatial filter using
89
            scipy.signal.filtfilt or scipy.signal.convolve.
90
        """
91
        if self.f_type == 'gaussian':
5✔
92
            try:
5✔
93
                assert(len(self.f_order) == 2)
5✔
94
            except AssertionError as ae:
×
95
                raise ae('filter order f_order must be a tuple of length 2')
×
96
        else:
97
            try:
×
98
                assert (self.f_order > 0 and isinstance(self.f_order, int))
×
99
            except AssertionError as ae:
×
100
                raise ae('Filter order must be int > 0!')
×
101
        try:
5✔
102
            assert (filterfunction in ['filtfilt', 'convolve'])
5✔
103
        except AssertionError as ae:
×
104
            raise ae("{} not equal to 'filtfilt' or \
×
105
                     'convolve'".format(filterfunction))
106

107
        if self.f_type == 'boxcar':
5✔
108
            num = ss.windows.boxcar(self.f_order)
×
109
            denom = np.array([num.sum()])
×
110
        elif self.f_type == 'hamming':
5✔
111
            num = ss.windows.hamming(self.f_order)
×
112
            denom = np.array([num.sum()])
×
113
        elif self.f_type == 'triangular':
5✔
114
            num = ss.windows.triang(self.f_order)
×
115
            denom = np.array([num.sum()])
×
116
        elif self.f_type == 'gaussian':
5✔
117
            num = ss.windows.gaussian(self.f_order[0], self.f_order[1])
5✔
118
            denom = np.array([num.sum()])
5✔
119
        elif self.f_type == 'identity':
×
120
            num = np.array([1.])
×
121
            denom = np.array([1.])
×
122
        else:
123
            print('%s Wrong filter type!' % self.f_type)
×
124
            raise
×
125

126
        num_string = '[ '
5✔
127
        for i in num:
5✔
128
            num_string = num_string + '%.3f ' % i
5✔
129
        num_string = num_string + ']'
5✔
130
        denom_string = '[ '
5✔
131
        for i in denom:
5✔
132
            denom_string = denom_string + '%.3f ' % i
5✔
133
        denom_string = denom_string + ']'
5✔
134

135
        print(('discrete filter coefficients: \nb = {}, \
5✔
136
               \na = {}'.format(num_string, denom_string)))
137

138
        if filterfunction == 'filtfilt':
5✔
139
            return ss.filtfilt(num, denom, csd, axis=0) * csd.units
×
140
        elif filterfunction == 'convolve':
5✔
141
            csdf = csd / csd.units
5✔
142
            for i in range(csdf.shape[1]):
5✔
143
                csdf[:, i] = ss.convolve(csdf[:, i], num / denom.sum(), 'same')
5✔
144
            return csdf * csd.units
5✔
145

146

147
class StandardCSD(CSD):
5✔
148
    """
149
    Standard CSD method with and without Vaknin electrodes
150
    """
151

152
    def __init__(self, lfp, coord_electrode, **kwargs):
5✔
153
        """
154
        Initialize standard CSD method class with & without Vaknin electrodes.
155

156
        Parameters
157
        ----------
158
        lfp : np.ndarray * quantity.Quantity
159
            LFP signal of shape (# channels, # time steps) in units of V
160
        coord_electrode : np.ndarray * quantity.Quantity
161
            depth of evenly spaced electrode contact points of shape
162
            (# contacts, ) in units of m, must be monotonously increasing
163
        sigma : float * quantity.Quantity
164
            conductivity of tissue in units of S/m or 1/(ohm*m)
165
            Defaults to 0.3 S/m
166
        vaknin_el : bool
167
            flag for using method of Vaknin to endpoint electrodes
168
            Defaults to True
169
        f_type : str
170
            type of spatial filter, must be a scipy.signal filter design method
171
            Defaults to 'gaussian'
172
        f_order : list
173
            settings for spatial filter, arg passed to  filter design function
174
            Defaults to (3,1) for the gaussian
175
        """
176
        self.parameters(**kwargs)
5✔
177
        CSD.__init__(self, lfp, self.f_type, self.f_order)
5✔
178

179
        diff_diff_coord = np.diff(np.diff(coord_electrode)).magnitude
5✔
180
        zeros_ddc = np.zeros_like(diff_diff_coord)
5✔
181
        try:
5✔
182
            assert(np.all(np.isclose(diff_diff_coord, zeros_ddc, atol=1e-12)))
5✔
183
        except AssertionError as ae:
×
184
            print('coord_electrode not monotonously varying')
×
185
            raise ae
×
186

187
        if self.vaknin_el:
5✔
188
            # extend lfps array by duplicating potential at endpoint contacts
189
            if lfp.ndim == 1:
5✔
190
                self.lfp = np.empty((lfp.shape[0] + 2,))
5✔
191
            else:
192
                self.lfp = np.empty((lfp.shape[0] + 2, lfp.shape[1]))
5✔
193
            self.lfp[0,] = lfp[0,]
5✔
194
            self.lfp[1:-1, ] = lfp
5✔
195
            self.lfp[-1,] = lfp[-1,]
5✔
196
            self.lfp = self.lfp * lfp.units
5✔
197
        else:
198
            self.lfp = lfp
×
199

200
        self.name = 'Standard CSD method'
5✔
201
        self.coord_electrode = coord_electrode
5✔
202

203
        self.f_inv_matrix = self.get_f_inv_matrix()
5✔
204

205
    def parameters(self, **kwargs):
5✔
206
        """Defining the default values of the method passed as kwargs
207
        Parameters
208
        ----------
209
        **kwargs
210
            Same as those passed to initialize the Class
211
        """
212
        self.sigma = kwargs.pop('sigma', 0.3 * pq.S / pq.m)
5✔
213
        self.vaknin_el = kwargs.pop('vaknin_el', True)
5✔
214
        self.f_type = kwargs.pop('f_type', 'gaussian')
5✔
215
        self.f_order = kwargs.pop('f_order', (3, 1))
5✔
216
        if kwargs:
5✔
217
            raise TypeError('Invalid keyword arguments:', kwargs.keys())
×
218

219
    def get_f_inv_matrix(self):
5✔
220
        """Calculate the inverse F-matrix for the standard CSD method"""
221
        h_val = abs(np.diff(self.coord_electrode)[0])
5✔
222
        f_inv = -np.eye(self.lfp.shape[0])
5✔
223

224
        # Inner matrix elements  is just the discrete laplacian coefficients
225
        for j in range(1, f_inv.shape[0] - 1):
5✔
226
            f_inv[j, j - 1: j + 2] = np.array([1., -2., 1.])
5✔
227
        return f_inv * -self.sigma / h_val
5✔
228

229
    def get_csd(self):
5✔
230
        """
231
        Perform the iCSD calculation, i.e: iCSD=F_inv*LFP
232

233
        Returns
234
        -------
235
        csd : np.ndarray * quantity.Quantity
236
            Array with the csd estimate
237
        """
238
        csd = np.dot(self.f_inv_matrix, self.lfp)[1:-1, ]
5✔
239
        # `np.dot()` does not return correct units, so the units of `csd` must
240
        # be assigned manually
241
        csd_units = (self.f_inv_matrix.units * self.lfp.units).simplified
5✔
242
        csd = csd.magnitude * csd_units
5✔
243

244
        return csd
5✔
245

246

247
class DeltaiCSD(CSD):
5✔
248
    """
249
    delta-iCSD method
250
    """
251
    def __init__(self, lfp, coord_electrode, **kwargs):
5✔
252
        """
253
        Initialize the delta-iCSD method class object
254

255
        Parameters
256
        ----------
257
        lfp : np.ndarray * quantity.Quantity
258
            LFP signal of shape (# channels, # time steps) in units of V
259
        coord_electrode : np.ndarray * quantity.Quantity
260
            depth of evenly spaced electrode contact points of shape
261
            (# contacts, ) in units of m
262
        diam : float * quantity.Quantity
263
            diamater of the assumed circular planar current sources centered
264
            at each contact
265
            Defaults to 500E-6 meters
266
        sigma : float * quantity.Quantity
267
            conductivity of tissue in units of S/m or 1/(ohm*m)
268
            Defaults to 0.3 S / m
269
        sigma_top : float * quantity.Quantity
270
            conductivity on top of tissue in units of S/m or 1/(ohm*m)
271
            Defaults to 0.3 S / m
272
        f_type : str
273
            type of spatial filter, must be a scipy.signal filter design method
274
            Defaults to 'gaussian'
275
        f_order : list
276
            settings for spatial filter, arg passed to  filter design function
277
            Defaults to (3,1) for gaussian
278
        """
279
        self.parameters(**kwargs)
5✔
280
        CSD.__init__(self, lfp, self.f_type, self.f_order)
5✔
281

282
        try:  # Should the class not take care of this?!
5✔
283
            assert(self.diam.units == coord_electrode.units)
5✔
284
        except AssertionError as ae:
×
285
            print('units of coord_electrode ({}) and diam ({}) differ'
×
286
                  .format(coord_electrode.units, self.diam.units))
287
            raise ae
×
288

289
        try:
5✔
290
            assert(np.all(np.diff(coord_electrode) > 0))
5✔
291
        except AssertionError as ae:
×
292
            print('values of coord_electrode not continously increasing')
×
293
            raise ae
×
294

295
        try:
5✔
296
            assert(self.diam.size == 1 or self.diam.size == coord_electrode.size)
5✔
297
            if self.diam.size == coord_electrode.size:
5✔
298
                assert(np.all(self.diam > 0 * self.diam.units))
5✔
299
            else:
300
                assert(self.diam > 0 * self.diam.units)
5✔
301
        except AssertionError as ae:
×
302
            print('diam must be positive scalar or of same shape \
×
303
                   as coord_electrode')
304
            raise ae
×
305
        if self.diam.size == 1:
5✔
306
            self.diam = np.ones(coord_electrode.size) * self.diam
5✔
307

308
        self.name = 'delta-iCSD method'
5✔
309
        self.coord_electrode = coord_electrode
5✔
310

311
        # initialize F- and iCSD-matrices
312
        self.f_matrix = np.empty((self.coord_electrode.size,
5✔
313
                                  self.coord_electrode.size))
314
        self.f_matrix = self.get_f_matrix()
5✔
315

316
    def parameters(self, **kwargs):
5✔
317
        """Defining the default values of the method passed as kwargs
318
        Parameters
319
        ----------
320
        **kwargs
321
            Same as those passed to initialize the Class
322
        """
323
        self.diam = kwargs.pop('diam', 500E-6 * pq.m)
5✔
324
        self.sigma = kwargs.pop('sigma', 0.3 * pq.S / pq.m)
5✔
325
        self.sigma_top = kwargs.pop('sigma_top', 0.3 * pq.S / pq.m)
5✔
326
        self.f_type = kwargs.pop('f_type', 'gaussian')
5✔
327
        self.f_order = kwargs.pop('f_order', (3, 1))
5✔
328
        if kwargs:
5✔
329
            raise TypeError('Invalid keyword arguments:', kwargs.keys())
×
330

331
    def get_f_matrix(self):
5✔
332
        """Calculate the F-matrix"""
333
        f_matrix = np.empty((self.coord_electrode.size,
5✔
334
                             self.coord_electrode.size)) * self.coord_electrode.units
335
        for j in range(self.coord_electrode.size):
5✔
336
            for i in range(self.coord_electrode.size):
5✔
337
                f_matrix[j, i] = ((np.sqrt((self.coord_electrode[j] -
5✔
338
                                            self.coord_electrode[i])**2 +
339
                    (self.diam[j] / 2)**2) - abs(self.coord_electrode[j] -
340
                                                 self.coord_electrode[i])) +
341
                    (self.sigma - self.sigma_top) / (self.sigma +
342
                                                     self.sigma_top) *
343
                    (np.sqrt((self.coord_electrode[j] +
344
                              self.coord_electrode[i])**2 + (self.diam[j] / 2)**2)-
345
                    abs(self.coord_electrode[j] + self.coord_electrode[i])))
346

347
        f_matrix /= (2 * self.sigma)
5✔
348
        return f_matrix
5✔
349

350

351
class StepiCSD(CSD):
5✔
352
    """step-iCSD method"""
353
    def __init__(self, lfp, coord_electrode, **kwargs):
5✔
354

355
        """
356
        Initializing step-iCSD method class object
357

358
        Parameters
359
        ----------
360
        lfp : np.ndarray * quantity.Quantity
361
            LFP signal of shape (# channels, # time steps) in units of V
362
        coord_electrode : np.ndarray * quantity.Quantity
363
            depth of evenly spaced electrode contact points of shape
364
            (# contacts, ) in units of m
365
        diam : float or np.ndarray * quantity.Quantity
366
            diameter(s) of the assumed circular planar current sources centered
367
            at each contact
368
            Defaults to 500E-6 meters
369
        h : float or np.ndarray * quantity.Quantity
370
            assumed thickness of the source cylinders at all or each contact
371
            Defaults to np.ones(15) * 100E-6 * pq.m
372
        sigma : float * quantity.Quantity
373
            conductivity of tissue in units of S/m or 1/(ohm*m)
374
            Defaults to 0.3 S / m
375
        sigma_top : float * quantity.Quantity
376
            conductivity on top of tissue in units of S/m or 1/(ohm*m)
377
            Defaults to 0.3 S / m
378
        tol : float
379
            tolerance of numerical integration
380
            Defaults 1e-6
381
        f_type : str
382
            type of spatial filter, must be a scipy.signal filter design method
383
            Defaults to 'gaussian'
384
        f_order : list
385
            settings for spatial filter, arg passed to  filter design function
386
            Defaults to (3,1) for the gaussian
387
        """
388
        self.parameters(**kwargs)
5✔
389
        CSD.__init__(self, lfp, self.f_type, self.f_order)
5✔
390

391
        try:  # Should the class not take care of this?
5✔
392
            assert(self.diam.units == coord_electrode.units)
5✔
393
        except AssertionError as ae:
×
394
            print('units of coord_electrode ({}) and diam ({}) differ'
×
395
                  .format(coord_electrode.units, self.diam.units))
396
            raise ae
×
397
        try:
5✔
398
            assert(np.all(np.diff(coord_electrode) > 0))
5✔
399
        except AssertionError as ae:
×
400
            print('values of coord_electrode not continously increasing')
×
401
            raise ae
×
402

403
        try:
5✔
404
            assert(self.diam.size == 1 or self.diam.size == coord_electrode.size)
5✔
405
            if self.diam.size == coord_electrode.size:
5✔
406
                assert(np.all(self.diam > 0 * self.diam.units))
5✔
407
            else:
408
                assert(self.diam > 0 * self.diam.units)
5✔
409
        except AssertionError as ae:
×
410
            print('diam must be positive scalar or of same shape \
×
411
                   as coord_electrode')
412
            raise ae
×
413
        if self.diam.size == 1:
5✔
414
            self.diam = np.ones(coord_electrode.size) * self.diam
5✔
415
        try:
5✔
416
            assert(self.h.size == 1 or self.h.size == coord_electrode.size)
5✔
417
            if self.h.size == coord_electrode.size:
5✔
418
                assert(np.all(self.h > 0 * self.h.units))
5✔
419
        except AssertionError as ae:
5✔
420
            print('h must be scalar or of same shape as coord_electrode')
5✔
421
            raise ae
5✔
422
        if self.h.size == 1:
5✔
423
            self.h = np.ones(coord_electrode.size) * self.h
×
424

425
        self.name = 'step-iCSD method'
5✔
426
        self.coord_electrode = coord_electrode
5✔
427

428
        # compute forward-solution matrix
429
        self.f_matrix = self.get_f_matrix()
5✔
430

431
    def parameters(self, **kwargs):
5✔
432
        """Defining the default values of the method passed as kwargs
433
        Parameters
434
        ----------
435
        **kwargs
436
            Same as those passed to initialize the Class
437
        """
438

439
        self.diam = kwargs.pop('diam', 500E-6 * pq.m)
5✔
440
        self.h = kwargs.pop('h', np.ones(23) * 100E-6 * pq.m)
5✔
441
        self.sigma = kwargs.pop('sigma', 0.3 * pq.S / pq.m)
5✔
442
        self.sigma_top = kwargs.pop('sigma_top', 0.3 * pq.S / pq.m)
5✔
443
        self.tol = kwargs.pop('tol', 1e-6)
5✔
444
        self.f_type = kwargs.pop('f_type', 'gaussian')
5✔
445
        self.f_order = kwargs.pop('f_order', (3, 1))
5✔
446
        if kwargs:
5✔
447
            raise TypeError('Invalid keyword arguments:', kwargs.keys())
×
448

449
    def get_f_matrix(self):
5✔
450
        """Calculate F-matrix for step iCSD method"""
451
        el_len = self.coord_electrode.size
5✔
452
        f_matrix = np.zeros((el_len, el_len))
5✔
453
        for j in range(el_len):
5✔
454
            for i in range(el_len):
5✔
455
                lower_int = self.coord_electrode[i] - self.h[j] / 2
5✔
456
                if lower_int < 0:
5✔
457
                    lower_int = self.h[j].units
5✔
458
                upper_int = self.coord_electrode[i] + self.h[j] / 2
5✔
459

460
                # components of f_matrix object
461
                f_cyl0 = si.quad(self._f_cylinder,
5✔
462
                                 a=lower_int, b=upper_int,
463
                                 args=(float(self.coord_electrode[j]),
464
                                       float(self.diam[j]),
465
                                       float(self.sigma)),
466
                                 epsabs=self.tol)[0]
467
                f_cyl1 = si.quad(self._f_cylinder, a=lower_int, b=upper_int,
5✔
468
                                 args=(-float(self.coord_electrode[j]),
469
                                       float(self.diam[j]), float(self.sigma)),
470
                                 epsabs=self.tol)[0]
471

472
                # method of images coefficient
473
                mom = (self.sigma - self.sigma_top) / (self.sigma + self.sigma_top)
5✔
474

475
                f_matrix[j, i] = f_cyl0 + mom * f_cyl1
5✔
476

477
        # assume si.quad trash the units
478
        return f_matrix * self.h.units**2 / self.sigma.units
5✔
479

480
    def _f_cylinder(self, zeta, z_val, diam, sigma):
5✔
481
        """function used by class method"""
482
        f_cyl = 1. / (2. * sigma) * \
5✔
483
            (np.sqrt((diam / 2)**2 + ((z_val - zeta))**2) - abs(z_val - zeta))
484
        return f_cyl
5✔
485

486

487
class SplineiCSD(CSD):
5✔
488
    """spline iCSD method"""
489
    def __init__(self, lfp, coord_electrode, **kwargs):
5✔
490

491
        """
492
        Initializing spline-iCSD method class object
493

494
        Parameters
495
        ----------
496
        lfp : np.ndarray * quantity.Quantity
497
            LFP signal of shape (# channels, # time steps) in units of V
498
        coord_electrode : np.ndarray * quantity.Quantity
499
            depth of evenly spaced electrode contact points of shape
500
            (# contacts, ) in units of m
501
        diam : float * quantity.Quantity
502
            diamater of the assumed circular planar current sources centered
503
            at each contact
504
            Defaults to 500E-6 meters
505
        sigma : float * quantity.Quantity
506
            conductivity of tissue in units of S/m or 1/(ohm*m)
507
            Defaults to 0.3 S / m
508
        sigma_top : float * quantity.Quantity
509
            conductivity on top of tissue in units of S/m or 1/(ohm*m)
510
            Defaults to 0.3 S / m
511
        tol : float
512
            tolerance of numerical integration
513
            Defaults 1e-6
514
        f_type : str
515
            type of spatial filter, must be a scipy.signal filter design method
516
            Defaults to 'gaussian'
517
        f_order : list
518
            settings for spatial filter, arg passed to  filter design function
519
            Defaults to (3,1) for the gaussian
520
        num_steps : int
521
            number of data points for the spatially upsampled LFP/CSD data
522
            Defaults to 200
523
        """
524
        self.parameters(**kwargs)
5✔
525
        CSD.__init__(self, lfp, self.f_type, self.f_order)
5✔
526

527
        try:  # Should the class not take care of this?!
5✔
528
            assert(self.diam.units == coord_electrode.units)
5✔
529
        except AssertionError as ae:
×
530
            print('units of coord_electrode ({}) and diam ({}) differ'
×
531
                  .format(coord_electrode.units, self.diam.units))
532
            raise
×
533
        try:
5✔
534
            assert(np.all(np.diff(coord_electrode) > 0))
5✔
535
        except AssertionError as ae:
×
536
            print('values of coord_electrode not continously increasing')
×
537
            raise ae
×
538

539
        try:
5✔
540
            assert(self.diam.size == 1 or self.diam.size == coord_electrode.size)
5✔
541
            if self.diam.size == coord_electrode.size:
5✔
542
                assert(np.all(self.diam > 0 * self.diam.units))
5✔
543
        except AssertionError as ae:
×
544
            print('diam must be scalar or of same shape as coord_electrode')
×
545
            raise ae
×
546
        if self.diam.size == 1:
5✔
547
            self.diam = np.ones(coord_electrode.size) * self.diam
5✔
548

549
        self.name = 'spline-iCSD method'
5✔
550
        self.coord_electrode = coord_electrode
5✔
551

552
        # compute stuff
553
        self.f_matrix = self.get_f_matrix()
5✔
554

555
    def parameters(self, **kwargs):
5✔
556
        """Defining the default values of the method passed as kwargs
557
        Parameters
558
        ----------
559
        **kwargs
560
            Same as those passed to initialize the Class
561
        """
562
        self.diam = kwargs.pop('diam', 500E-6 * pq.m)
5✔
563
        self.sigma = kwargs.pop('sigma', 0.3 * pq.S / pq.m)
5✔
564
        self.sigma_top = kwargs.pop('sigma_top', 0.3 * pq.S / pq.m)
5✔
565
        self.tol = kwargs.pop('tol', 1e-6)
5✔
566
        self.num_steps = kwargs.pop('num_steps', 200)
5✔
567
        self.f_type = kwargs.pop('f_type', 'gaussian')
5✔
568
        self.f_order = kwargs.pop('f_order', (3, 1))
5✔
569
        if kwargs:
5✔
570
            raise TypeError('Invalid keyword arguments:', kwargs.keys())
×
571

572
    def get_f_matrix(self):
5✔
573
        """Calculate the F-matrix for cubic spline iCSD method"""
574
        el_len = self.coord_electrode.size
5✔
575
        z_js = np.zeros(el_len + 1)
5✔
576
        z_js[:-1] = np.array(self.coord_electrode)
5✔
577
        z_js[-1] = z_js[-2] + float(np.diff(self.coord_electrode).mean())
5✔
578

579
        # Define integration matrixes
580
        f_mat0 = np.zeros((el_len, el_len + 1))
5✔
581
        f_mat1 = np.zeros((el_len, el_len + 1))
5✔
582
        f_mat2 = np.zeros((el_len, el_len + 1))
5✔
583
        f_mat3 = np.zeros((el_len, el_len + 1))
5✔
584

585
        # Calc. elements
586
        for j in range(el_len):
5✔
587
            for i in range(el_len):
5✔
588
                f_mat0[j, i] = si.quad(self._f_mat0, a=z_js[i], b=z_js[i + 1],
5✔
589
                                       args=(z_js[j + 1],
590
                                             float(self.sigma),
591
                                             float(self.diam[j])),
592
                                       epsabs=self.tol)[0]
593
                f_mat1[j, i] = si.quad(self._f_mat1, a=z_js[i], b=z_js[i + 1],
5✔
594
                                       args=(z_js[j + 1], z_js[i],
595
                                             float(self.sigma),
596
                                             float(self.diam[j])),
597
                                       epsabs=self.tol)[0]
598
                f_mat2[j, i] = si.quad(self._f_mat2, a=z_js[i], b=z_js[i + 1],
5✔
599
                                       args=(z_js[j + 1], z_js[i],
600
                                             float(self.sigma),
601
                                             float(self.diam[j])),
602
                                       epsabs=self.tol)[0]
603
                f_mat3[j, i] = si.quad(self._f_mat3, a=z_js[i], b=z_js[i + 1],
5✔
604
                                       args=(z_js[j + 1], z_js[i],
605
                                             float(self.sigma),
606
                                             float(self.diam[j])),
607
                                       epsabs=self.tol)[0]
608

609
                # image technique if conductivity not constant:
610
                if self.sigma != self.sigma_top:
5✔
611
                    f_mat0[j, i] = f_mat0[j, i] + (self.sigma-self.sigma_top) / \
5✔
612
                                                (self.sigma + self.sigma_top) * \
613
                            si.quad(self._f_mat0, a=z_js[i], b=z_js[i+1], \
614
                                    args=(-z_js[j+1],
615
                                          float(self.sigma), float(self.diam[j])), \
616
                                    epsabs=self.tol)[0]
617
                    f_mat1[j, i] = f_mat1[j, i] + (self.sigma-self.sigma_top) / \
5✔
618
                        (self.sigma + self.sigma_top) * \
619
                            si.quad(self._f_mat1, a=z_js[i], b=z_js[i+1], \
620
                                args=(-z_js[j+1], z_js[i], float(self.sigma),
621
                                      float(self.diam[j])), epsabs=self.tol)[0]
622
                    f_mat2[j, i] = f_mat2[j, i] + (self.sigma-self.sigma_top) / \
5✔
623
                        (self.sigma + self.sigma_top) * \
624
                            si.quad(self._f_mat2, a=z_js[i], b=z_js[i+1], \
625
                                args=(-z_js[j+1], z_js[i], float(self.sigma),
626
                                      float(self.diam[j])), epsabs=self.tol)[0]
627
                    f_mat3[j, i] = f_mat3[j, i] + (self.sigma-self.sigma_top) / \
5✔
628
                        (self.sigma + self.sigma_top) * \
629
                            si.quad(self._f_mat3, a=z_js[i], b=z_js[i+1], \
630
                                args=(-z_js[j+1], z_js[i], float(self.sigma),
631
                                      float(self.diam[j])), epsabs=self.tol)[0]
632

633
        e_mat0, e_mat1, e_mat2, e_mat3 = self._calc_e_matrices()
5✔
634

635
        # Calculate the F-matrix
636
        f_matrix = np.eye(el_len + 2)
5✔
637
        f_matrix[1:-1, :] = np.dot(f_mat0, e_mat0) + \
5✔
638
                            np.dot(f_mat1, e_mat1) + \
639
                            np.dot(f_mat2, e_mat2) + \
640
                            np.dot(f_mat3, e_mat3)
641

642
        return f_matrix * self.coord_electrode.units**2 / self.sigma.units
5✔
643

644
    def get_csd(self):
5✔
645
        """
646
        Calculate the iCSD using the spline iCSD method
647

648
        Returns
649
        -------
650
        csd : np.ndarray * quantity.Quantity
651
            Array with csd estimate
652

653

654
        """
655
        e_mat = self._calc_e_matrices()
5✔
656

657
        el_len = self.coord_electrode.size
5✔
658

659
        # padding the lfp with zeros on top/bottom
660
        if self.lfp.ndim == 1:
5✔
661
            cs_lfp = np.r_[[0], np.asarray(self.lfp), [0]].reshape(1, -1).T
5✔
662
            csd = np.zeros(self.num_steps)
5✔
663
        else:
664
            cs_lfp = np.vstack((np.zeros(self.lfp.shape[1]),
5✔
665
                                np.asarray(self.lfp),
666
                                np.zeros(self.lfp.shape[1])))
667
            csd = np.zeros((self.num_steps, self.lfp.shape[1]))
5✔
668
        cs_lfp *= self.lfp.units
5✔
669

670
        # CSD coefficients
671
        csd_coeff = np.linalg.solve(self.f_matrix, cs_lfp)
5✔
672

673
        # The cubic spline polynomial coefficients
674
        a_mat0 = np.dot(e_mat[0], csd_coeff)
5✔
675
        a_mat1 = np.dot(e_mat[1], csd_coeff)
5✔
676
        a_mat2 = np.dot(e_mat[2], csd_coeff)
5✔
677
        a_mat3 = np.dot(e_mat[3], csd_coeff)
5✔
678

679
        # Extend electrode coordinates in both end by min contact interdistance
680
        h = np.diff(self.coord_electrode).min()
5✔
681
        z_js = np.zeros(el_len + 2)
5✔
682
        z_js[0] = self.coord_electrode[0] - h
5✔
683
        z_js[1: -1] = self.coord_electrode
5✔
684
        z_js[-1] = self.coord_electrode[-1] + h
5✔
685

686
        # create high res spatial grid
687
        out_zs = np.linspace(z_js[1], z_js[-2], self.num_steps)
5✔
688

689
        # Calculate iCSD estimate on grid from polynomial coefficients.
690
        i = 0
5✔
691
        for j in range(self.num_steps):
5✔
692
            if out_zs[j] >= z_js[i + 1]:
5✔
693
                i += 1
5✔
694
            csd[j] = (a_mat0[i, :] + a_mat1[i, :] *
5✔
695
                      (out_zs[j] - z_js[i]) +
696
                      a_mat2[i, :] * (out_zs[j] - z_js[i])**2 +
697
                      a_mat3[i, :] * (out_zs[j] - z_js[i])**3).item()
698

699
        csd_unit = (self.f_matrix.units**-1 * self.lfp.units).simplified
5✔
700

701
        return csd * csd_unit
5✔
702

703
    def _f_mat0(self, zeta, z_val, sigma, diam):
5✔
704
        """0'th order potential function"""
705
        return 1. / (2. * sigma) * \
5✔
706
            (np.sqrt((diam / 2)**2 + ((z_val - zeta))**2) - abs(z_val - zeta))
707

708
    def _f_mat1(self, zeta, z_val, zi_val, sigma, diam):
5✔
709
        """1'th order potential function"""
710
        return (zeta - zi_val) * self._f_mat0(zeta, z_val, sigma, diam)
5✔
711

712
    def _f_mat2(self, zeta, z_val, zi_val, sigma, diam):
5✔
713
        """2'nd order potential function"""
714
        return (zeta - zi_val)**2 * self._f_mat0(zeta, z_val, sigma, diam)
5✔
715

716
    def _f_mat3(self, zeta, z_val, zi_val, sigma, diam):
5✔
717
        """3'rd order potential function"""
718
        return (zeta - zi_val)**3 * self._f_mat0(zeta, z_val, sigma, diam)
5✔
719

720
    def _calc_k_matrix(self):
5✔
721
        """Calculate the K-matrix used by to calculate E-matrices"""
722
        el_len = self.coord_electrode.size
5✔
723
        h = float(np.diff(self.coord_electrode).min())
5✔
724

725
        c_jm1 = np.eye(el_len + 2, k=0) / h
5✔
726
        c_jm1[0, 0] = 0
5✔
727

728
        c_j0 = np.eye(el_len + 2) / h
5✔
729
        c_j0[-1, -1] = 0
5✔
730

731
        c_jall = c_j0
5✔
732
        c_jall[0, 0] = 1
5✔
733
        c_jall[-1, -1] = 1
5✔
734

735
        tjp1 = np.eye(el_len + 2, k=1)
5✔
736
        tjm1 = np.eye(el_len + 2, k=-1)
5✔
737

738
        tj0 = np.eye(el_len + 2)
5✔
739
        tj0[0, 0] = 0
5✔
740
        tj0[-1, -1] = 0
5✔
741

742
        # Defining K-matrix used to calculate e_mat1-3
743
        return np.dot(np.linalg.inv(np.dot(c_jm1, tjm1) +
5✔
744
                                    2 * np.dot(c_jm1, tj0) +
745
                                    2 * c_jall +
746
                                    np.dot(c_j0, tjp1)),
747
                      3 * (np.dot(np.dot(c_jm1, c_jm1), tj0) -
748
                           np.dot(np.dot(c_jm1, c_jm1), tjm1) +
749
                           np.dot(np.dot(c_j0, c_j0), tjp1) -
750
                           np.dot(np.dot(c_j0, c_j0), tj0)))
751

752
    def _calc_e_matrices(self):
5✔
753
        """Calculate the E-matrices used by cubic spline iCSD method"""
754
        el_len = self.coord_electrode.size
5✔
755
        # expanding electrode grid
756
        h = float(np.diff(self.coord_electrode).min())
5✔
757

758
        # Define transformation matrices
759
        c_mat3 = np.eye(el_len + 1) / h
5✔
760

761
        # Get K-matrix
762
        k_matrix = self._calc_k_matrix()
5✔
763

764
        # Define matrixes for C to A transformation:
765
        tja = np.eye(el_len + 2)[:-1, ]
5✔
766
        tjp1a = np.eye(el_len + 2, k=1)[:-1, ]
5✔
767

768
        # Define spline coefficients
769
        e_mat0 = tja
5✔
770
        e_mat1 = np.dot(tja, k_matrix)
5✔
771
        e_mat2 = 3 * np.dot(c_mat3**2, (tjp1a - tja)) - \
5✔
772
                            np.dot(np.dot(c_mat3, (tjp1a + 2 * tja)), k_matrix)
773
        e_mat3 = 2 * np.dot(c_mat3**3, (tja - tjp1a)) + \
5✔
774
                            np.dot(np.dot(c_mat3**2, (tjp1a + tja)), k_matrix)
775

776
        return e_mat0, e_mat1, e_mat2, e_mat3
5✔
777

778

779
if __name__ == '__main__':
5✔
780
    from scipy.io import loadmat
×
781
    import matplotlib.pyplot as plt
×
782

783
    
784
    #loading test data
785
    test_data = loadmat('test_data.mat')
×
786
    
787
    #prepare lfp data for use, by changing the units to SI and append quantities,
788
    #along with electrode geometry, conductivities and assumed source geometry
789
    lfp_data = test_data['pot1'] * 1E-6 * pq.V        # [uV] -> [V]
×
790
    z_data = np.linspace(100E-6, 2300E-6, 23) * pq.m  # [m]
×
791
    diam = 500E-6 * pq.m                              # [m]
×
792
    h = 100E-6 * pq.m                                 # [m]
×
793
    sigma = 0.3 * pq.S / pq.m                         # [S/m] or [1/(ohm*m)]
×
794
    sigma_top = 0.3 * pq.S / pq.m                     # [S/m] or [1/(ohm*m)]
×
795
    
796
    # Input dictionaries for each method
797
    delta_input = {
×
798
        'lfp' : lfp_data,
799
        'coord_electrode' : z_data,
800
        'diam' : diam,          # source diameter
801
        'sigma' : sigma,        # extracellular conductivity
802
        'sigma_top' : sigma,    # conductivity on top of cortex
803
        'f_type' : 'gaussian',  # gaussian filter
804
        'f_order' : (3, 1),     # 3-point filter, sigma = 1.
805
    }
806
    step_input = {
×
807
        'lfp' : lfp_data,
808
        'coord_electrode' : z_data,
809
        'diam' : diam,
810
        'h' : h,                # source thickness
811
        'sigma' : sigma,
812
        'sigma_top' : sigma,
813
        'tol' : 1E-12,          # Tolerance in numerical integration
814
        'f_type' : 'gaussian',
815
        'f_order' : (3, 1),
816
    }
817
    spline_input = {
×
818
        'lfp' : lfp_data,
819
        'coord_electrode' : z_data,
820
        'diam' : diam,
821
        'sigma' : sigma,
822
        'sigma_top' : sigma,
823
        'num_steps' : 201,      # Spatial CSD upsampling to N steps
824
        'tol' : 1E-12,
825
        'f_type' : 'gaussian',
826
        'f_order' : (20, 5),
827
    }
828
    std_input = {
×
829
        'lfp' : lfp_data,
830
        'coord_electrode' : z_data,
831
        'sigma' : sigma,
832
        'f_type' : 'gaussian',
833
        'f_order' : (3, 1),
834
    }
835
    
836
    
837
    #Create the different CSD-method class instances. We use the class methods
838
    #get_csd() and filter_csd() below to get the raw and spatially filtered
839
    #versions of the current-source density estimates.
840
    csd_dict = dict(
×
841
        delta_icsd = DeltaiCSD(**delta_input),
842
        step_icsd = StepiCSD(**step_input),
843
        spline_icsd = SplineiCSD(**spline_input),
844
        std_csd = StandardCSD(**std_input),
845
    )
846
    
847
    #plot
848
    for method, csd_obj in list(csd_dict.items()):
×
849
        fig, axes = plt.subplots(3,1, figsize=(8,8))
×
850
    
851
        #plot LFP signal
852
        ax = axes[0]
×
853
        im = ax.imshow(np.array(lfp_data), origin='upper', vmin=-abs(lfp_data).max(), \
×
854
                  vmax=abs(lfp_data).max(), cmap='jet_r', interpolation='nearest')
855
        ax.axis(ax.axis('tight'))
×
856
        cb = plt.colorbar(im, ax=ax)
×
857
        cb.set_label('LFP (%s)' % lfp_data.dimensionality.string)
×
858
        ax.set_xticklabels([])
×
859
        ax.set_title('LFP')
×
860
        ax.set_ylabel('ch #')
×
861
    
862
        #plot raw csd estimate
863
        csd = csd_obj.get_csd()
×
864
        ax = axes[1]
×
865
        im = ax.imshow(np.array(csd), origin='upper', vmin=-abs(csd).max(), \
×
866
              vmax=abs(csd).max(), cmap='jet_r', interpolation='nearest')
867
        ax.axis(ax.axis('tight'))
×
868
        ax.set_title(csd_obj.name)
×
869
        cb = plt.colorbar(im, ax=ax)
×
870
        cb.set_label('CSD (%s)' % csd.dimensionality.string)
×
871
        ax.set_xticklabels([])
×
872
        ax.set_ylabel('ch #')
×
873
    
874
        #plot spatially filtered csd estimate
875
        ax = axes[2]
×
876
        csd = csd_obj.filter_csd(csd)
×
877
        im = ax.imshow(np.array(csd), origin='upper', vmin=-abs(csd).max(), \
×
878
              vmax=abs(csd).max(), cmap='jet_r', interpolation='nearest')
879
        ax.axis(ax.axis('tight'))
×
880
        ax.set_title(csd_obj.name + ', filtered')
×
881
        cb = plt.colorbar(im, ax=ax)
×
882
        cb.set_label('CSD (%s)' % csd.dimensionality.string)
×
883
        ax.set_ylabel('ch #')
×
884
        ax.set_xlabel('timestep')
×
885
    
886
    
887
    plt.show()
×
888

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc