• 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

83.08
/elephant/current_source_density_src/KCSD.py
1
#!/usr/bin/env python
2
"""This script is used to generate Current Source Density Estimates, using the
5✔
3
kCSD method Jan et.al (2012).
4
This was written by :
5
[1]Chaitanya Chintaluri,
6
[2]Michal Czerwinski,
7
Laboratory of Neuroinformatics,
8
Nencki Institute of Exprimental Biology, Warsaw.
9
KCSD1D[1][2], KCSD2D[1], KCSD3D[1], MoIKCSD[1]
10
"""
11
from __future__ import division
5✔
12

13
import numpy as np
5✔
14
from scipy import special, integrate, interpolate
5✔
15
from scipy.spatial import distance
5✔
16
from numpy.linalg import LinAlgError
5✔
17

18
from . import utility_functions as utils
5✔
19
from . import basis_functions as basis
5✔
20

21
skmonaco_available = False
5✔
22

23

24
class CSD(object):
5✔
25
    """CSD - The base class for KCSD methods."""
26
    def __init__(self, ele_pos, pots):
5✔
27
        self.validate(ele_pos, pots)
5✔
28
        self.ele_pos = ele_pos
5✔
29
        self.pots = pots
5✔
30
        self.n_ele = self.ele_pos.shape[0]
5✔
31
        self.n_time = self.pots.shape[1]
5✔
32
        self.dim = self.ele_pos.shape[1]
5✔
33
        self.cv_error = None
5✔
34

35
    def validate(self, ele_pos, pots):
5✔
36
        """Basic checks to see if inputs are okay
37

38
        Parameters
39
        ----------
40
        ele_pos : numpy array
41
            positions of electrodes
42
        pots : numpy array
43
            potentials measured by electrodes
44
        """
45
        if ele_pos.shape[0] != pots.shape[0]:
5✔
46
            raise Exception("Number of measured potentials is not equal "
×
47
                            "to electrode number!")
48
        if ele_pos.shape[0] < 1+ele_pos.shape[1]: #Dim+1
5✔
49
            raise Exception("Number of electrodes must be at least :",
×
50
                            1+ele_pos.shape[1])
51
        if utils.contains_duplicated_electrodes(ele_pos):
5✔
52
            raise Exception("Error! Duplicated electrode!")
×
53

54
    def sanity(self, true_csd, pos_csd):
5✔
55
        """Useful for comparing TrueCSD with reconstructed CSD. Computes, the RMS error
56
        between the true_csd and the reconstructed csd at pos_csd using the
57
        method defined.
58
        Parameters
59
        ----------
60
        true_csd : csd values used to generate potentials
61
        pos_csd : csd estimatation from the method
62
        Returns
63
        -------
64
        RMSE : root mean squared difference
65
        """
66
        csd = self.values(pos_csd)
×
67
        RMSE = np.sqrt(np.mean(np.square(true_csd - csd)))
×
68
        return RMSE
×
69

70
class KCSD(CSD):
5✔
71
    """KCSD - The base class for all the KCSD variants.
72
    This estimates the Current Source Density, for a given configuration of
73
    electrod positions and recorded potentials, electrodes.
74
    The method implented here is based on the original paper
75
    by Jan Potworowski et.al. 2012.
76
    """
77
    def __init__(self, ele_pos, pots, **kwargs):
5✔
78
        super(KCSD, self).__init__(ele_pos, pots)
5✔
79
        self.parameters(**kwargs)
5✔
80
        self.estimate_at()
5✔
81
        self.place_basis()
5✔
82
        self.create_src_dist_tables()
5✔
83
        self.method()
5✔
84

85
    def parameters(self, **kwargs):
5✔
86
        """Defining the default values of the method passed as kwargs
87
        Parameters
88
        ----------
89
        **kwargs
90
            Same as those passed to initialize the Class
91
        """
92
        self.src_type = kwargs.pop('src_type', 'gauss')
5✔
93
        self.sigma = kwargs.pop('sigma', 1.0)
5✔
94
        self.h = kwargs.pop('h', 1.0)
5✔
95
        self.n_src_init = kwargs.pop('n_src_init', 1000)
5✔
96
        self.lambd = kwargs.pop('lambd', 0.0)
5✔
97
        self.R_init = kwargs.pop('R_init', 0.23)
5✔
98
        self.ext_x = kwargs.pop('ext_x', 0.0)
5✔
99
        self.xmin = kwargs.pop('xmin', np.min(self.ele_pos[:, 0]))
5✔
100
        self.xmax = kwargs.pop('xmax', np.max(self.ele_pos[:, 0]))
5✔
101
        self.gdx = kwargs.pop('gdx', 0.01*(self.xmax - self.xmin))
5✔
102
        if self.dim >= 2:
5✔
103
            self.ext_y = kwargs.pop('ext_y', 0.0)
5✔
104
            self.ymin = kwargs.pop('ymin', np.min(self.ele_pos[:, 1]))
5✔
105
            self.ymax = kwargs.pop('ymax', np.max(self.ele_pos[:, 1]))
5✔
106
            self.gdy = kwargs.pop('gdy', 0.01*(self.ymax - self.ymin))
5✔
107
        if self.dim == 3:
5✔
108
            self.ext_z = kwargs.pop('ext_z', 0.0)
5✔
109
            self.zmin = kwargs.pop('zmin', np.min(self.ele_pos[:, 2]))
5✔
110
            self.zmax = kwargs.pop('zmax', np.max(self.ele_pos[:, 2]))
5✔
111
            self.gdz = kwargs.pop('gdz', 0.01*(self.zmax - self.zmin))
5✔
112
        if kwargs:
5✔
113
            raise TypeError('Invalid keyword arguments:', kwargs.keys())
5✔
114

115
    def method(self):
5✔
116
        """Actual sequence of methods called for KCSD
117
        Defines:
118
        self.k_pot and self.k_interp_cross matrices
119
        Parameters
120
        ----------
121
        None
122
        """
123
        self.create_lookup()                                #Look up table
5✔
124
        self.update_b_pot()                                 #update kernel
5✔
125
        self.update_b_src()                                 #update crskernel
5✔
126
        self.update_b_interp_pot()                          #update pot interp
5✔
127

128
    def create_lookup(self, dist_table_density=20):
5✔
129
        """Creates a table for easy potential estimation from CSD.
130
        Updates and Returns the potentials due to a
131
        given basis source like a lookup
132
        table whose shape=(dist_table_density,)
133
        Parameters
134
        ----------
135
        dist_table_density : int
136
            number of distance values at which potentials are computed.
137
            Default 100
138
        """
139
        xs = np.logspace(0., np.log10(self.dist_max+1.), dist_table_density)
5✔
140
        xs = xs - 1.0 #starting from 0
5✔
141
        dist_table = np.zeros(len(xs))
5✔
142
        for i, pos in enumerate(xs):
5✔
143
            dist_table[i] = self.forward_model(pos,
5✔
144
                                               self.R,
145
                                               self.h,
146
                                               self.sigma,
147
                                               self.basis)
148
        self.interpolate_pot_at = interpolate.interp1d(xs, dist_table, kind='cubic')
5✔
149

150
    def update_b_pot(self):
5✔
151
        """Updates the b_pot  - array is (#_basis_sources, #_electrodes)
152
        Updates the  k_pot - array is (#_electrodes, #_electrodes) K(x,x')
153
        Eq9,Jan2012
154
        Calculates b_pot - matrix containing the values of all
155
        the potential basis functions in all the electrode positions
156
        (essential for calculating the cross_matrix).
157
        Parameters
158
        ----------
159
        None
160
        """
161
        self.b_pot = self.interpolate_pot_at(self.src_ele_dists)
5✔
162
        self.k_pot = np.dot(self.b_pot.T, self.b_pot) #K(x,x') Eq9,Jan2012
5✔
163
        self.k_pot /= self.n_src
5✔
164

165
    def update_b_src(self):
5✔
166
        """Updates the b_src in the shape of (#_est_pts, #_basis_sources)
167
        Updates the k_interp_cross - K_t(x,y) Eq17
168
        Calculate b_src - matrix containing containing the values of
169
        all the source basis functions in all the points at which we want to
170
        calculate the solution (essential for calculating the cross_matrix)
171
        Parameters
172
        ----------
173
        None
174
        """
175
        self.b_src = self.basis(self.src_estm_dists, self.R).T
5✔
176
        self.k_interp_cross = np.dot(self.b_src, self.b_pot) #K_t(x,y) Eq17
5✔
177
        self.k_interp_cross /= self.n_src
5✔
178

179
    def update_b_interp_pot(self):
5✔
180
        """Compute the matrix of potentials generated by every source
181
        basis function at every position in the interpolated space.
182
        Updates b_interp_pot
183
        Updates k_interp_pot
184
        Parameters
185
        ----------
186
        None
187
        """
188
        self.b_interp_pot = self.interpolate_pot_at(self.src_estm_dists).T
5✔
189
        self.k_interp_pot = np.dot(self.b_interp_pot, self.b_pot)
5✔
190
        self.k_interp_pot /= self.n_src
5✔
191

192
    def values(self, estimate='CSD'):
5✔
193
        """Computes the values of the quantity of interest
194
        Parameters
195
        ----------
196
        estimate : 'CSD' or 'POT'
197
            What quantity is to be estimated
198
            Defaults to 'CSD'
199
        Returns
200
        -------
201
        estimation : np.array
202
            estimated quantity of shape (ngx, ngy, ngz, nt)
203
        """
204
        if estimate == 'CSD': #Maybe used for estimating the potentials also.
5✔
205
            estimation_table = self.k_interp_cross
5✔
206
        elif estimate == 'POT':
×
207
            estimation_table = self.k_interp_pot
×
208
        else:
209
            print('Invalid quantity to be measured, pass either CSD or POT')
×
210
        k_inv = np.linalg.inv(self.k_pot + self.lambd *
5✔
211
                              np.identity(self.k_pot.shape[0]))
212
        estimation = np.zeros((self.n_estm, self.n_time))
5✔
213
        for t in range(self.n_time):
5✔
214
            beta = np.dot(k_inv, self.pots[:, t])
5✔
215
            for i in range(self.n_ele):
5✔
216
                estimation[:, t] += estimation_table[:, i] *beta[i] # C*(x) Eq 18
5✔
217
        return self.process_estimate(estimation)
5✔
218

219
    def process_estimate(self, estimation):
5✔
220
        """Function used to rearrange estimation according to dimension, to be
221
        used by the fuctions values
222
        Parameters
223
        ----------
224
        estimation : np.array
225
        Returns
226
        -------
227
        estimation : np.array
228
            estimated quantity of shape (ngx, ngy, ngz, nt)
229
        """
230
        if self.dim == 1:
5✔
231
            estimation = estimation.reshape(self.ngx, self.n_time)
5✔
232
        elif self.dim == 2:
5✔
233
            estimation = estimation.reshape(self.ngx, self.ngy, self.n_time)
5✔
234
        elif self.dim == 3:
5✔
235
            estimation = estimation.reshape(self.ngx, self.ngy, self.ngz, self.n_time)
5✔
236
        return estimation
5✔
237

238
    def update_R(self, R):
5✔
239
        """Update the width of the basis fuction - Used in Cross validation
240
        Parameters
241
        ----------
242
        R : float
243
        """
244
        self.R = R
5✔
245
        self.dist_max = max(np.max(self.src_ele_dists),
5✔
246
                            np.max(self.src_estm_dists)) + self.R
247
        self.method()
5✔
248

249
    def update_lambda(self, lambd):
5✔
250
        """Update the lambda parameter of regularization, Used in Cross validation
251
        Parameters
252
        ----------
253
        lambd : float
254
        """
255
        self.lambd = lambd
5✔
256

257
    def cross_validate(self, lambdas=None, Rs=None):
5✔
258
        """Method defines the cross validation.
259
        By default only cross_validates over lambda,
260
        When no argument is passed, it takes
261
        lambdas = np.logspace(-2,-25,25,base=10.)
262
        and Rs = np.array(self.R).flatten()
263
        otherwise pass necessary numpy arrays
264
        Parameters
265
        ----------
266
        lambdas : numpy array
267
        Rs : numpy array
268
        Returns
269
        -------
270
        R : post cross validation
271
        Lambda : post cross validation
272
        """
273
        if lambdas is None:                           #when None
5✔
274
            print('No lambda given, using defaults')
5✔
275
            lambdas = np.logspace(-2,-25,25,base=10.) #Default multiple lambda
5✔
276
            lambdas = np.hstack((lambdas, np.array((0.0))))
5✔
277
        elif lambdas.size == 1:                       #resize when one entry
×
278
            lambdas = lambdas.flatten()
×
279
        if Rs is None:                                #when None
5✔
280
            Rs = np.array((self.R)).flatten()         #Default over one R value
5✔
281
        errs = np.zeros((Rs.size, lambdas.size))
5✔
282
        index_generator = []
5✔
283
        for ii in range(self.n_ele):
5✔
284
            idx_test = [ii]
5✔
285
            idx_train = list(range(self.n_ele))
5✔
286
            idx_train.remove(ii)                      #Leave one out
5✔
287
            index_generator.append((idx_train, idx_test))
5✔
288
        for R_idx,R in enumerate(Rs):                 #Iterate over R
5✔
289
            self.update_R(R)
5✔
290
            print('Cross validating R (all lambda) :', R)
5✔
291
            for lambd_idx,lambd in enumerate(lambdas): #Iterate over lambdas
5✔
292
                errs[R_idx, lambd_idx] = self.compute_cverror(lambd,
5✔
293
                                                              index_generator)
294
        err_idx = np.where(errs==np.min(errs))         #Index of the least error
5✔
295
        cv_R = Rs[err_idx[0]][0]      #First occurance of the least error's
5✔
296
        cv_lambda = lambdas[err_idx[1]][0]
5✔
297
        self.cv_error = np.min(errs)  #otherwise is None
5✔
298
        self.update_R(cv_R)           #Update solver
5✔
299
        self.update_lambda(cv_lambda)
5✔
300
        print('R, lambda :', cv_R, cv_lambda)
5✔
301
        return cv_R, cv_lambda
5✔
302

303
    def compute_cverror(self, lambd, index_generator):
5✔
304
        """Useful for Cross validation error calculations
305
        Parameters
306
        ----------
307
        lambd : float
308
        index_generator : list
309
        Returns
310
        -------
311
        err : float
312
            the sum of the error computed.
313
        """
314
        err = 0
5✔
315
        for idx_train, idx_test in index_generator:
5✔
316
            B_train = self.k_pot[np.ix_(idx_train, idx_train)]
5✔
317
            V_train = self.pots[idx_train]
5✔
318
            V_test = self.pots[idx_test]
5✔
319
            I_matrix = np.identity(len(idx_train))
5✔
320
            B_new = np.array(B_train) + (lambd * I_matrix)
5✔
321
            try:
5✔
322
                beta_new = np.linalg.inv(B_new) @ V_train
5✔
323
                B_test = self.k_pot[np.ix_(idx_test, idx_train)]
5✔
324
                V_est = np.zeros((len(idx_test), self.pots.shape[1]))
5✔
325
                for ii in range(len(idx_train)):
5✔
326
                    for tt in range(self.pots.shape[1]):
5✔
327
                        V_est[:, tt] += beta_new[ii, tt] * B_test[:, ii]
5✔
328
                err += np.linalg.norm(V_est - V_test)
5✔
329
            except np.linalg.LinAlgError:
×
330
                raise np.linalg.LinAlgError(
×
331
                    'Encountered Singular Matrix Error: try changing ele_pos slightly')
332
        return err
5✔
333

334
class KCSD1D(KCSD):
5✔
335
    """KCSD1D - The 1D variant for the Kernel Current Source Density method.
336
    This estimates the Current Source Density, for a given configuration of
337
    electrod positions and recorded potentials, in the case of 1D recording
338
    electrodes (laminar probes). The method implented here is based on the
339
    original paper by Jan Potworowski et.al. 2012.
340
    """
341
    def __init__(self, ele_pos, pots, **kwargs):
5✔
342
        """Initialize KCSD1D Class.
343
        Parameters
344
        ----------
345
        ele_pos : numpy array
346
            positions of electrodes
347
        pots : numpy array
348
            potentials measured by electrodes
349
        **kwargs
350
            configuration parameters, that may contain the following keys:
351
            src_type : str
352
                basis function type ('gauss', 'step', 'gauss_lim')
353
                Defaults to 'gauss'
354
            sigma : float
355
                space conductance of the tissue in S/m
356
                Defaults to 1 S/m
357
            n_src_init : int
358
                requested number of sources
359
                Defaults to 300
360
            R_init : float
361
                demanded thickness of the basis element
362
                Defaults to 0.23
363
            h : float
364
                thickness of analyzed cylindrical slice
365
                Defaults to 1.
366
            xmin, xmax : floats
367
                boundaries for CSD estimation space
368
                Defaults to min(ele_pos(x)), and max(ele_pos(x))
369
            ext_x : float
370
                length of space extension: x_min-ext_x ... x_max+ext_x
371
                Defaults to 0.
372
            gdx : float
373
                space increments in the estimation space
374
                Defaults to 0.01(xmax-xmin)
375
            lambd : float
376
                regularization parameter for ridge regression
377
                Defaults to 0.
378
        Raises
379
        ------
380
        LinAlgException
381
            If the matrix is not numerically invertible.
382
        KeyError
383
            Basis function (src_type) not implemented. See basis_functions.py for available
384
        """
385
        super(KCSD1D, self).__init__(ele_pos, pots, **kwargs)
5✔
386

387
    def estimate_at(self):
5✔
388
        """Defines locations where the estimation is wanted
389
        Defines:
390
        self.n_estm = self.estm_x.size
391
        self.ngx = self.estm_x.shape
392
        self.estm_x : Locations at which CSD is requested.
393
        Parameters
394
        ----------
395
        None
396
        """
397
        nx = (self.xmax - self.xmin)/self.gdx
5✔
398
        self.estm_x = np.mgrid[self.xmin:self.xmax:complex(0,nx)]
5✔
399
        self.n_estm = self.estm_x.size
5✔
400
        self.ngx = self.estm_x.shape[0]
5✔
401

402
    def place_basis(self):
5✔
403
        """Places basis sources of the defined type.
404
        Checks if a given source_type is defined, if so then defines it
405
        self.basis, This function gives locations of the basis sources,
406
        Defines
407
        source_type : basis_fuctions.basis_1D.keys()
408
        self.R based on R_init
409
        self.dist_max as maximum distance between electrode and basis
410
        self.nsx = self.src_x.shape
411
        self.src_x : Locations at which basis sources are placed.
412
        Parameters
413
        ----------
414
        None
415
        """
416
        source_type = self.src_type
5✔
417
        try:
5✔
418
            self.basis = basis.basis_1D[source_type]
5✔
419
        except KeyError:
5✔
420
            raise KeyError('Invalid source_type for basis! available are:',
5✔
421
                           basis.basis_1D.keys())
422
        (self.src_x, self.R) = utils.distribute_srcs_1D(self.estm_x,
5✔
423
                                                        self.n_src_init,
424
                                                        self.ext_x,
425
                                                        self.R_init )
426
        self.n_src = self.src_x.size
5✔
427
        self.nsx = self.src_x.shape
5✔
428

429
    def create_src_dist_tables(self):
5✔
430
        """Creates distance tables between sources, electrode and estm points
431
        Parameters
432
        ----------
433
        None
434
        """
435
        src_loc = np.array((self.src_x.ravel()))
5✔
436
        src_loc = src_loc.reshape((len(src_loc), 1))
5✔
437
        est_loc = np.array((self.estm_x.ravel()))
5✔
438
        est_loc = est_loc.reshape((len(est_loc), 1))
5✔
439
        self.src_ele_dists = distance.cdist(src_loc, self.ele_pos, 'euclidean')
5✔
440
        self.src_estm_dists = distance.cdist(src_loc, est_loc,  'euclidean')
5✔
441
        self.dist_max = max(np.max(self.src_ele_dists), np.max(self.src_estm_dists)) + self.R
5✔
442

443
    def forward_model(self, x, R, h, sigma, src_type):
5✔
444
        """FWD model functions
445
        Evaluates potential at point (x,0) by a basis source located at (0,0)
446
        Eq 26 kCSD by Jan,2012
447
        Parameters
448
        ----------
449
        x : float
450
        R : float
451
        h : float
452
        sigma : float
453
        src_type : basis_1D.key
454
        Returns
455
        -------
456
        pot : float
457
            value of potential at specified distance from the source
458
        """
459
        pot, err = integrate.quad(self.int_pot_1D,
5✔
460
                                  -R, R,
461
                                  args=(x, R, h, src_type))
462
        pot *= 1./(2.0*sigma)
5✔
463
        return pot
5✔
464

465
    def int_pot_1D(self, xp, x, R, h, basis_func):
5✔
466
        """FWD model function.
467
        Returns contribution of a point xp,yp, belonging to a basis source
468
        support centered at (0,0) to the potential measured at (x,0),
469
        integrated over xp,yp gives the potential generated by a
470
        basis source element centered at (0,0) at point (x,0)
471
        Eq 26 kCSD by Jan,2012
472

473
        Parameters
474
        ----------
475
        xp : floats or np.arrays
476
            point or set of points where function should be calculated
477
        x :  float
478
            position at which potential is being measured
479
        R : float
480
            The size of the basis function
481
        h : float
482
            thickness of slice
483
        basis_func : method
484
            Fuction of the basis source
485

486
        Returns
487
        -------
488
        pot : float
489
        """
490
        m = np.sqrt((x-xp)**2 + h**2) - abs(x-xp)
5✔
491
        m *= basis_func(abs(xp), R)  #xp is the distance
5✔
492
        return m
5✔
493

494
class KCSD2D(KCSD):
5✔
495
    """KCSD2D - The 2D variant for the Kernel Current Source Density method.
496
    This estimates the Current Source Density, for a given configuration of
497
    electrod positions and recorded potentials, in the case of 2D recording
498
    electrodes. The method implented here is based on the original paper
499
    by Jan Potworowski et.al. 2012.
500
    """
501
    def __init__(self, ele_pos, pots, **kwargs):
5✔
502
        """Initialize KCSD2D Class.
503
        Parameters
504
        ----------
505
        ele_pos : numpy array
506
            positions of electrodes
507
        pots : numpy array
508
            potentials measured by electrodes
509
        **kwargs
510
            configuration parameters, that may contain the following keys:
511
            src_type : str
512
                basis function type ('gauss', 'step', 'gauss_lim')
513
                Defaults to 'gauss'
514
            sigma : float
515
                space conductance of the tissue in S/m
516
                Defaults to 1 S/m
517
            n_src_init : int
518
                requested number of sources
519
                Defaults to 1000
520
            R_init : float
521
                demanded thickness of the basis element
522
                Defaults to 0.23
523
            h : float
524
                thickness of analyzed tissue slice
525
                Defaults to 1.
526
            xmin, xmax, ymin, ymax : floats
527
                boundaries for CSD estimation space
528
                Defaults to min(ele_pos(x)), and max(ele_pos(x))
529
                Defaults to min(ele_pos(y)), and max(ele_pos(y))
530
            ext_x, ext_y : float
531
                length of space extension: x_min-ext_x ... x_max+ext_x
532
                length of space extension: y_min-ext_y ... y_max+ext_y
533
                Defaults to 0.
534
            gdx, gdy : float
535
                space increments in the estimation space
536
                Defaults to 0.01(xmax-xmin)
537
                Defaults to 0.01(ymax-ymin)
538
            lambd : float
539
                regularization parameter for ridge regression
540
                Defaults to 0.
541
        Raises
542
        ------
543
        LinAlgError
544
            Could not invert the matrix, try changing the ele_pos slightly
545
        KeyError
546
            Basis function (src_type) not implemented. See basis_functions.py for available
547
        """
548
        super(KCSD2D, self).__init__(ele_pos, pots, **kwargs)
5✔
549

550
    def estimate_at(self):
5✔
551
        """Defines locations where the estimation is wanted
552
        Defines:
553
        self.n_estm = self.estm_x.size
554
        self.ngx, self.ngy = self.estm_x.shape
555
        self.estm_x, self.estm_y : Locations at which CSD is requested.
556
        Parameters
557
        ----------
558
        None
559
        """
560
        nx = (self.xmax - self.xmin)/self.gdx
5✔
561
        ny = (self.ymax - self.ymin)/self.gdy
5✔
562
        self.estm_x, self.estm_y = np.mgrid[self.xmin:self.xmax:complex(0,nx),
5✔
563
                                            self.ymin:self.ymax:complex(0,ny)]
564
        self.n_estm = self.estm_x.size
5✔
565
        self.ngx, self.ngy = self.estm_x.shape
5✔
566

567
    def place_basis(self):
5✔
568
        """Places basis sources of the defined type.
569
        Checks if a given source_type is defined, if so then defines it
570
        self.basis, This function gives locations of the basis sources,
571
        Defines
572
        source_type : basis_fuctions.basis_2D.keys()
573
        self.R based on R_init
574
        self.dist_max as maximum distance between electrode and basis
575
        self.nsx, self.nsy = self.src_x.shape
576
        self.src_x, self.src_y : Locations at which basis sources are placed.
577
        Parameters
578
        ----------
579
        None
580
        """
581
        source_type = self.src_type
5✔
582
        try:
5✔
583
            self.basis = basis.basis_2D[source_type]
5✔
584
        except KeyError:
5✔
585
            raise KeyError('Invalid source_type for basis! available are:',
5✔
586
                           basis.basis_2D.keys())
587
        (self.src_x, self.src_y, self.R) = utils.distribute_srcs_2D(self.estm_x,
5✔
588
                                                                    self.estm_y,
589
                                                                    self.n_src_init,
590
                                                                    self.ext_x,
591
                                                                    self.ext_y,
592
                                                                    self.R_init )
593
        self.n_src = self.src_x.size
5✔
594
        self.nsx, self.nsy = self.src_x.shape
5✔
595

596
    def create_src_dist_tables(self):
5✔
597
        """Creates distance tables between sources, electrode and estm points
598
        Parameters
599
        ----------
600
        None
601
        """
602
        src_loc = np.array((self.src_x.ravel(), self.src_y.ravel()))
5✔
603
        est_loc = np.array((self.estm_x.ravel(), self.estm_y.ravel()))
5✔
604
        self.src_ele_dists = distance.cdist(src_loc.T, self.ele_pos, 'euclidean')
5✔
605
        self.src_estm_dists = distance.cdist(src_loc.T, est_loc.T,  'euclidean')
5✔
606
        self.dist_max = max(np.max(self.src_ele_dists), np.max(self.src_estm_dists)) + self.R
5✔
607

608
    def forward_model(self, x, R, h, sigma, src_type):
5✔
609
        """FWD model functions
610
        Evaluates potential at point (x,0) by a basis source located at (0,0)
611
        Eq 22 kCSD by Jan,2012
612
        Parameters
613
        ----------
614
        x : float
615
        R : float
616
        h : float
617
        sigma : float
618
        src_type : basis_2D.key
619
        Returns
620
        -------
621
        pot : float
622
            value of potential at specified distance from the source
623
        """
624
        pot, err = integrate.dblquad(self.int_pot_2D,
5✔
625
                                     -R, R,
626
                                     lambda x: -R,
627
                                     lambda x: R,
628
                                     args=(x, R, h, src_type))
629
        pot *= 1./(2.0*np.pi*sigma)  #Potential basis functions bi_x_y
5✔
630
        return pot
5✔
631

632
    def int_pot_2D(self, xp, yp, x, R, h, basis_func):
5✔
633
        """FWD model function.
634
        Returns contribution of a point xp,yp, belonging to a basis source
635
        support centered at (0,0) to the potential measured at (x,0),
636
        integrated over xp,yp gives the potential generated by a
637
        basis source element centered at (0,0) at point (x,0)
638
        Parameters
639
        ----------
640
        xp, yp : floats or np.arrays
641
            point or set of points where function should be calculated
642
        x :  float
643
            position at which potential is being measured
644
        R : float
645
            The size of the basis function
646
        h : float
647
            thickness of slice
648
        basis_func : method
649
            Fuction of the basis source
650
        Returns
651
        -------
652
        pot : float
653
        """
654
        y = ((x-xp)**2 + yp**2)**(0.5)
5✔
655
        if y < 0.00001:
5✔
656
            y = 0.00001
5✔
657
        dist = np.sqrt(xp**2 + yp**2)
5✔
658
        pot = np.arcsinh(h/y)*basis_func(dist, R)
5✔
659
        return pot
5✔
660

661
class MoIKCSD(KCSD2D):
5✔
662
    """MoIKCSD - CSD while including the forward modeling effects of saline.
663

664
    This estimates the Current Source Density, for a given configuration of
665
    electrod positions and recorded potentials, in the case of 2D recording
666
    electrodes from an MEA electrode plane using the Method of Images.
667
    The method implented here is based on kCSD method by Jan Potworowski
668
    et.al. 2012, which was extended in Ness, Chintaluri 2015 for MEA.
669
    """
670
    def __init__(self, ele_pos, pots, **kwargs):
5✔
671
        """Initialize MoIKCSD Class.
672
        Parameters
673
        ----------
674
        ele_pos : numpy array
675
            positions of electrodes
676
        pots : numpy array
677
            potentials measured by electrodes
678
        **kwargs
679
            configuration parameters, that may contain the following keys:
680
            src_type : str
681
                basis function type ('gauss', 'step', 'gauss_lim')
682
                Defaults to 'gauss'
683
            sigma : float
684
                space conductance of the tissue in S/m
685
                Defaults to 1 S/m
686
            sigma_S : float
687
                conductance of the saline (medium) in S/m
688
                Default is 5 S/m (5 times more conductive)
689
            n_src_init : int
690
                requested number of sources
691
                Defaults to 1000
692
            R_init : float
693
                demanded thickness of the basis element
694
                Defaults to 0.23
695
            h : float
696
                thickness of analyzed tissue slice
697
                Defaults to 1.
698
            xmin, xmax, ymin, ymax : floats
699
                boundaries for CSD estimation space
700
                Defaults to min(ele_pos(x)), and max(ele_pos(x))
701
                Defaults to min(ele_pos(y)), and max(ele_pos(y))
702
            ext_x, ext_y : float
703
                length of space extension: x_min-ext_x ... x_max+ext_x
704
                length of space extension: y_min-ext_y ... y_max+ext_y
705
                Defaults to 0.
706
            gdx, gdy : float
707
                space increments in the estimation space
708
                Defaults to 0.01(xmax-xmin)
709
                Defaults to 0.01(ymax-ymin)
710
            lambd : float
711
                regularization parameter for ridge regression
712
                Defaults to 0.
713
            MoI_iters : int
714
                Number of interations in method of images.
715
                Default is 20
716
        """
717
        self.MoI_iters = kwargs.pop('MoI_iters', 20)
5✔
718
        self.sigma_S = kwargs.pop('sigma_S', 5.0)
5✔
719
        self.sigma = kwargs.pop('sigma', 1.0)
5✔
720
        W_TS = (self.sigma - self.sigma_S) / (self.sigma + self.sigma_S)
5✔
721
        self.iters = np.arange(self.MoI_iters) + 1  #Eq 6, Ness (2015)
5✔
722
        self.iter_factor = W_TS**self.iters
5✔
723
        super(MoIKCSD, self).__init__(ele_pos, pots, **kwargs)
5✔
724

725
    def forward_model(self, x, R, h, sigma, src_type):
5✔
726
        """FWD model functions
727
        Evaluates potential at point (x,0) by a basis source located at (0,0)
728
        Eq 22 kCSD by Jan,2012
729
        Parameters
730
        ----------
731
        x : float
732
        R : float
733
        h : float
734
        sigma : float
735
        src_type : basis_2D.key
736
        Returns
737
        -------
738
        pot : float
739
            value of potential at specified distance from the source
740
        """
741
        pot, err = integrate.dblquad(self.int_pot_2D_moi, -R, R,
5✔
742
                                     lambda x: -R,
743
                                     lambda x: R,
744
                                     args=(x, R, h, src_type))
745
        pot *= 1./(2.0*np.pi*sigma)
5✔
746
        return pot
5✔
747

748
    def int_pot_2D_moi(self, xp, yp, x, R, h, basis_func):
5✔
749
        """FWD model function. Incorporates the Method of Images.
750
        Returns contribution of a point xp,yp, belonging to a basis source
751
        support centered at (0,0) to the potential measured at (x,0),
752
        integrated over xp,yp gives the potential generated by a
753
        basis source element centered at (0,0) at point (x,0)
754
        #Eq 20, Ness(2015)
755
        Parameters
756
        ----------
757
        xp, yp : floats or np.arrays
758
            point or set of points where function should be calculated
759
        x :  float
760
            position at which potential is being measured
761
        R : float
762
            The size of the basis function
763
        h : float
764
            thickness of slice
765
        basis_func : method
766
            Fuction of the basis source
767
        Returns
768
        -------
769
        pot : float
770
        """
771
        L = ((x-xp)**2 + yp**2)**(0.5)
5✔
772
        if L < 0.00001:
5✔
773
            L = 0.00001
5✔
774
        correction = np.arcsinh((h-(2*h*self.iters))/L) + np.arcsinh((h+(2*h*self.iters))/L)
5✔
775
        pot = np.arcsinh(h/L) + np.sum(self.iter_factor*correction)
5✔
776
        dist = np.sqrt(xp**2 + yp**2)
5✔
777
        pot *= basis_func(dist, R) #Eq 20, Ness et.al.
5✔
778
        return pot
5✔
779

780
class KCSD3D(KCSD):
5✔
781
    """KCSD3D - The 3D variant for the Kernel Current Source Density method.
782
    This estimates the Current Source Density, for a given configuration of
783
    electrod positions and recorded potentials, in the case of 2D recording
784
    electrodes. The method implented here is based on the original paper
785
    by Jan Potworowski et.al. 2012.
786
    """
787
    def __init__(self, ele_pos, pots, **kwargs):
5✔
788
        """Initialize KCSD3D Class.
789
        Parameters
790
        ----------
791
        ele_pos : numpy array
792
            positions of electrodes
793
        pots : numpy array
794
            potentials measured by electrodes
795
        **kwargs
796
            configuration parameters, that may contain the following keys:
797
            src_type : str
798
                basis function type ('gauss', 'step', 'gauss_lim')
799
                Defaults to 'gauss'
800
            sigma : float
801
                space conductance of the tissue in S/m
802
                Defaults to 1 S/m
803
            n_src_init : int
804
                requested number of sources
805
                Defaults to 1000
806
            R_init : float
807
                demanded thickness of the basis element
808
                Defaults to 0.23
809
            h : float
810
                thickness of analyzed tissue slice
811
                Defaults to 1.
812
            xmin, xmax, ymin, ymax, zmin, zmax : floats
813
                boundaries for CSD estimation space
814
                Defaults to min(ele_pos(x)), and max(ele_pos(x))
815
                Defaults to min(ele_pos(y)), and max(ele_pos(y))
816
                Defaults to min(ele_pos(z)), and max(ele_pos(z))
817
            ext_x, ext_y, ext_z : float
818
                length of space extension: xmin-ext_x ... xmax+ext_x
819
                length of space extension: ymin-ext_y ... ymax+ext_y
820
                length of space extension: zmin-ext_z ... zmax+ext_z
821
                Defaults to 0.
822
            gdx, gdy, gdz : float
823
                space increments in the estimation space
824
                Defaults to 0.01(xmax-xmin)
825
                Defaults to 0.01(ymax-ymin)
826
                Defaults to 0.01(zmax-zmin)
827
            lambd : float
828
                regularization parameter for ridge regression
829
                Defaults to 0.
830
        Raises
831
        ------
832
        LinAlgError
833
            Could not invert the matrix, try changing the ele_pos slightly
834
        KeyError
835
            Basis function (src_type) not implemented. See basis_functions.py for available
836
        """
837
        super(KCSD3D, self).__init__(ele_pos, pots, **kwargs)
5✔
838

839
    def estimate_at(self):
5✔
840
        """Defines locations where the estimation is wanted
841
        Defines:
842
        self.n_estm = self.estm_x.size
843
        self.ngx, self.ngy, self.ngz = self.estm_x.shape
844
        self.estm_x, self.estm_y, self.estm_z : Pts. at which CSD is requested
845
        Parameters
846
        ----------
847
        None
848
        """
849
        nx = (self.xmax - self.xmin)/self.gdx
5✔
850
        ny = (self.ymax - self.ymin)/self.gdy
5✔
851
        nz = (self.zmax - self.zmin)/self.gdz
5✔
852
        self.estm_x, self.estm_y, self.estm_z = np.mgrid[self.xmin:self.xmax:complex(0,nx),
5✔
853
                                                         self.ymin:self.ymax:complex(0,ny),
854
                                                         self.zmin:self.zmax:complex(0,nz)]
855
        self.n_estm = self.estm_x.size
5✔
856
        self.ngx, self.ngy, self.ngz = self.estm_x.shape
5✔
857

858
    def place_basis(self):
5✔
859
        """Places basis sources of the defined type.
860
        Checks if a given source_type is defined, if so then defines it
861
        self.basis, This function gives locations of the basis sources,
862
        Defines
863
        source_type : basis_fuctions.basis_2D.keys()
864
        self.R based on R_init
865
        self.dist_max as maximum distance between electrode and basis
866
        self.nsx, self.nsy, self.nsz = self.src_x.shape
867
        self.src_x, self.src_y, self.src_z : Locations at which basis sources are placed.
868
        Parameters
869
        ----------
870
        None
871
        """
872
        source_type = self.src_type
5✔
873
        try:
5✔
874
            self.basis = basis.basis_3D[source_type]
5✔
875
        except KeyError:
5✔
876
            raise KeyError('Invalid source_type for basis! available are:',
5✔
877
                           basis.basis_3D.keys())
878
        (self.src_x, self.src_y, self.src_z, self.R) = utils.distribute_srcs_3D(self.estm_x,
5✔
879
                                                                                self.estm_y,
880
                                                                                self.estm_z,
881
                                                                                self.n_src_init,
882
                                                                                self.ext_x,
883
                                                                                self.ext_y,
884
                                                                                self.ext_z,
885
                                                                                self.R_init)
886

887
        self.n_src = self.src_x.size
5✔
888
        self.nsx, self.nsy, self.nsz = self.src_x.shape
5✔
889

890
    def create_src_dist_tables(self):
5✔
891
        """Creates distance tables between sources, electrode and estm points
892
        Parameters
893
        ----------
894
        None
895
        """
896
        src_loc = np.array((self.src_x.ravel(),
5✔
897
                            self.src_y.ravel(),
898
                            self.src_z.ravel()))
899
        est_loc = np.array((self.estm_x.ravel(),
5✔
900
                            self.estm_y.ravel(),
901
                            self.estm_z.ravel()))
902
        self.src_ele_dists = distance.cdist(src_loc.T, self.ele_pos, 'euclidean')
5✔
903
        self.src_estm_dists = distance.cdist(src_loc.T, est_loc.T,  'euclidean')
5✔
904
        self.dist_max = max(np.max(self.src_ele_dists), np.max(self.src_estm_dists)) + self.R
5✔
905

906
    def forward_model(self, x, R, h, sigma, src_type):
5✔
907
        """FWD model functions
908
        Evaluates potential at point (x,0) by a basis source located at (0,0)
909
        Utlizies sk monaco monte carlo method if available, otherwise defaults
910
        to scipy integrate
911
        Parameters
912
        ----------
913
        x : float
914
        R : float
915
        h : float
916
        sigma : float
917
        src_type : basis_3D.key
918
        Returns
919
        -------
920
        pot : float
921
            value of potential at specified distance from the source
922
        """
923
        if src_type.__name__ == "gauss_3D":
5✔
924
            if x == 0: x=0.0001
×
925
            pot = special.erf(x/(np.sqrt(2)*R/3.0)) / x
×
926
        elif src_type.__name__ == "gauss_lim_3D":
5✔
927
            if x == 0: x=0.0001
×
928
            d = R/3.
×
929
            if x < R:
×
930
                e = np.exp(-(x/ (np.sqrt(2)*d))**2)
×
931
                erf = special.erf(x / (np.sqrt(2)*d))
×
932
                pot = 4* np.pi * ( (d**2)*(e - np.exp(-4.5)) +
×
933
                                   (1/x)*((np.sqrt(np.pi/2)*(d**3)*erf) - x*(d**2)*e))
934
            else:
935
                pot = 15.28828*(d)**3 / x
×
936
            pot /= (np.sqrt(2*np.pi)*d)**3
×
937
        elif src_type.__name__ == "step_3D":
5✔
938
            Q = 4.*np.pi*(R**3)/3.
5✔
939
            if x < R:
5✔
940
                pot = (Q * (3 - (x/R)**2)) / (2.*R)
5✔
941
            else:
942
                pot = Q / x
5✔
943
            pot *= 3/(4*np.pi*R**3)
5✔
944
        else:
945
            if skmonaco_available:
×
946
                pot, err = mcmiser(self.int_pot_3D_mc,
×
947
                                   npoints=1e5,
948
                                   xl=[-R, -R, -R],
949
                                   xu=[R, R, R],
950
                                   seed=42,
951
                                   nprocs=num_cores,
952
                                   args=(x, R, h, src_type))
953
            else:
954
                pot, err = integrate.tplquad(self.int_pot_3D,
×
955
                                             -R,
956
                                             R,
957
                                             lambda x: -R,
958
                                             lambda x: R,
959
                                             lambda x, y: -R,
960
                                             lambda x, y: R,
961
                                             args=(x, R, h, src_type))
962
        pot *= 1./(4.0*np.pi*sigma)
5✔
963
        return pot
5✔
964

965
    def int_pot_3D(self, xp, yp, zp, x, R, h, basis_func):
5✔
966
        """FWD model function.
967
        Returns contribution of a point xp,yp, belonging to a basis source
968
        support centered at (0,0) to the potential measured at (x,0),
969
        integrated over xp,yp gives the potential generated by a
970
        basis source element centered at (0,0) at point (x,0)
971
        Parameters
972
        ----------
973
        xp, yp, zp : floats or np.arrays
974
            point or set of points where function should be calculated
975
        x :  float
976
            position at which potential is being measured
977
        R : float
978
            The size of the basis function
979
        h : float
980
            thickness of slice
981
        basis_func : method
982
            Fuction of the basis source
983
        Returns
984
        -------
985
        pot : float
986
        """
987
        y = ((x-xp)**2 + yp**2 + zp**2)**0.5
×
988
        if y < 0.00001:
×
989
            y = 0.00001
×
990
        dist = np.sqrt(xp**2 + yp**2 + zp**2)
×
991
        pot = 1.0/y
×
992
        pot *= basis_func(dist, R)
×
993
        return pot
×
994

995
    def int_pot_3D_mc(self, xyz, x, R, h, basis_func):
5✔
996
        """
997
        The same as int_pot_3D, just different input: x,y,z <-- xyz (tuple)
998
        FWD model function, using Monte Carlo Method of integration
999
        Returns contribution of a point xp,yp, belonging to a basis source
1000
        support centered at (0,0) to the potential measured at (x,0),
1001
        integrated over xp,yp gives the potential generated by a
1002
        basis source element centered at (0,0) at point (x,0)
1003
        Parameters
1004
        ----------
1005
        xp, yp, zp : floats or np.arrays
1006
            point or set of points where function should be calculated
1007
        x :  float
1008
            position at which potential is being measured
1009
        R : float
1010
            The size of the basis function
1011
        h : float
1012
            thickness of slice
1013
        basis_func : method
1014
            Fuction of the basis source
1015
        Returns
1016
        -------
1017
        pot : float
1018
        """
1019
        xp, yp, zp = xyz
×
1020
        return self.int_pot_3D(xp, yp, zp, x, R, h, basis_func)
×
1021

1022
if __name__ == '__main__':
5✔
1023
    print('Checking 1D')
×
1024
    ele_pos = np.array(([-0.1],[0], [0.5], [1.], [1.4], [2.], [2.3]))
×
1025
    pots = np.array([[-1], [-1], [-1], [0], [0], [1], [-1.5]])
×
1026
    k = KCSD1D(ele_pos, pots,
×
1027
               gdx=0.01, n_src_init=300,
1028
               ext_x=0.0, src_type='gauss')
1029
    k.cross_validate()
×
1030
    print(k.values())
×
1031

1032
    print('Checking 2D')
×
1033
    ele_pos = np.array([[-0.2, -0.2],[0, 0], [0, 1], [1, 0], [1,1], [0.5, 0.5],
×
1034
                        [1.2, 1.2]])
1035
    pots = np.array([[-1], [-1], [-1], [0], [0], [1], [-1.5]])
×
1036
    k = KCSD2D(ele_pos, pots,
×
1037
               gdx=0.05, gdy=0.05,
1038
               xmin=-2.0, xmax=2.0,
1039
               ymin=-2.0, ymax=2.0,
1040
               src_type='gauss')
1041
    k.cross_validate()
×
1042
    print(k.values())
×
1043

1044
    print('Checking MoIKCSD')
×
1045
    k = MoIKCSD(ele_pos, pots,
×
1046
                gdx=0.05, gdy=0.05,
1047
                xmin=-2.0, xmax=2.0,
1048
                ymin=-2.0, ymax= 2.0)
1049
    k.cross_validate()
×
1050

1051
    print('Checking KCSD3D')
×
1052
    ele_pos = np.array([(0, 0, 0), (0, 0, 1), (0, 1, 0), (1, 0, 0),
×
1053
                        (0, 1, 1), (1, 1, 0), (1, 0, 1), (1, 1, 1),
1054
                        (0.5, 0.5, 0.5)])
1055
    pots = np.array([[-0.5], [0], [-0.5], [0], [0], [0.2], [0], [0], [1]])
×
1056
    k = KCSD3D(ele_pos, pots,
×
1057
               gdx=0.02, gdy=0.02, gdz=0.02,
1058
               n_src_init=1000, src_type='gauss_lim')
1059
    k.cross_validate()
×
1060
    print(k.values())
×
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