icsd.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887
  1. # -*- coding: utf-8 -*-
  2. '''
  3. py-iCSD toolbox!
  4. Translation of the core functionality of the CSDplotter MATLAB package
  5. to python.
  6. The methods were originally developed by Klas H. Pettersen, as described in:
  7. Klas H. Pettersen, Anna Devor, Istvan Ulbert, Anders M. Dale, Gaute T. Einevoll,
  8. Current-source density estimation based on inversion of electrostatic forward
  9. solution: Effects of finite extent of neuronal activity and conductivity
  10. discontinuities, Journal of Neuroscience Methods, Volume 154, Issues 1-2,
  11. 30 June 2006, Pages 116-133, ISSN 0165-0270,
  12. http://dx.doi.org/10.1016/j.jneumeth.2005.12.005.
  13. (http://www.sciencedirect.com/science/article/pii/S0165027005004541)
  14. The method themselves are implemented as callable subclasses of the base
  15. CSD class object, which sets some common attributes,
  16. and a basic function for calculating the iCSD, and a generic spatial filter
  17. implementation.
  18. The raw- and filtered CSD estimates are returned as Quantity arrays.
  19. Requires pylab environment to work, i.e numpy+scipy+matplotlib, with the
  20. addition of quantities (http://pythonhosted.org/quantities) and
  21. neo (https://pythonhosted.org/neo)-
  22. Original implementation from CSDplotter-0.1.1
  23. (http://software.incf.org/software/csdplotter) by Klas. H. Pettersen 2005.
  24. Written by:
  25. - Espen.Hagen@umb.no, 2010,
  26. - e.hagen@fz-juelich.de, 2015-2016
  27. '''
  28. import numpy as np
  29. import scipy.integrate as si
  30. import scipy.signal as ss
  31. import quantities as pq
  32. class CSD(object):
  33. '''Base iCSD class'''
  34. def __init__(self, lfp, f_type='gaussian', f_order=(3, 1)):
  35. '''Initialize parent class iCSD
  36. Parameters
  37. ----------
  38. lfp : np.ndarray * quantity.Quantity
  39. LFP signal of shape (# channels, # time steps)
  40. f_type : str
  41. type of spatial filter, must be a scipy.signal filter design method
  42. f_order : list
  43. settings for spatial filter, arg passed to filter design function
  44. '''
  45. self.name = 'CSD estimate parent class'
  46. self.lfp = lfp
  47. self.f_matrix = np.eye(lfp.shape[0]) * pq.m**3 / pq.S
  48. self.f_type = f_type
  49. self.f_order = f_order
  50. def get_csd(self, ):
  51. '''
  52. Perform the CSD estimate from the LFP and forward matrix F, i.e as
  53. CSD=F**-1*LFP
  54. Arguments
  55. ---------
  56. Returns
  57. -------
  58. csd : np.ndarray * quantity.Quantity
  59. Array with the csd estimate
  60. '''
  61. csd = np.linalg.solve(self.f_matrix, self.lfp)
  62. return csd * (self.f_matrix.units**-1 * self.lfp.units).simplified
  63. def filter_csd(self, csd, filterfunction='convolve'):
  64. '''
  65. Spatial filtering of the CSD estimate, using an N-point filter
  66. Arguments
  67. ---------
  68. csd : np.ndarrray * quantity.Quantity
  69. Array with the csd estimate
  70. filterfunction : str
  71. 'filtfilt' or 'convolve'. Apply spatial filter using
  72. scipy.signal.filtfilt or scipy.signal.convolve.
  73. '''
  74. if self.f_type == 'gaussian':
  75. try:
  76. assert(len(self.f_order) == 2)
  77. except AssertionError as ae:
  78. raise ae('filter order f_order must be a tuple of length 2')
  79. else:
  80. try:
  81. assert(self.f_order > 0 and isinstance(self.f_order, int))
  82. except AssertionError as ae:
  83. raise ae('Filter order must be int > 0!')
  84. try:
  85. assert(filterfunction in ['filtfilt', 'convolve'])
  86. except AssertionError as ae:
  87. raise ae("{} not equal to 'filtfilt' or \
  88. 'convolve'".format(filterfunction))
  89. if self.f_type == 'boxcar':
  90. num = ss.boxcar(self.f_order)
  91. denom = np.array([num.sum()])
  92. elif self.f_type == 'hamming':
  93. num = ss.hamming(self.f_order)
  94. denom = np.array([num.sum()])
  95. elif self.f_type == 'triangular':
  96. num = ss.triang(self.f_order)
  97. denom = np.array([num.sum()])
  98. elif self.f_type == 'gaussian':
  99. num = ss.gaussian(self.f_order[0], self.f_order[1])
  100. denom = np.array([num.sum()])
  101. elif self.f_type == 'identity':
  102. num = np.array([1.])
  103. denom = np.array([1.])
  104. else:
  105. print('%s Wrong filter type!' % self.f_type)
  106. raise
  107. num_string = '[ '
  108. for i in num:
  109. num_string = num_string + '%.3f ' % i
  110. num_string = num_string + ']'
  111. denom_string = '[ '
  112. for i in denom:
  113. denom_string = denom_string + '%.3f ' % i
  114. denom_string = denom_string + ']'
  115. print(('discrete filter coefficients: \nb = {}, \
  116. \na = {}'.format(num_string, denom_string)))
  117. if filterfunction == 'filtfilt':
  118. return ss.filtfilt(num, denom, csd, axis=0) * csd.units
  119. elif filterfunction == 'convolve':
  120. csdf = csd / csd.units
  121. for i in range(csdf.shape[1]):
  122. csdf[:, i] = ss.convolve(csdf[:, i], num / denom.sum(), 'same')
  123. return csdf * csd.units
  124. class StandardCSD(CSD):
  125. '''
  126. Standard CSD method with and without Vaknin electrodes
  127. '''
  128. def __init__(self, lfp, coord_electrode, **kwargs):
  129. '''
  130. Initialize standard CSD method class with & without Vaknin electrodes.
  131. Parameters
  132. ----------
  133. lfp : np.ndarray * quantity.Quantity
  134. LFP signal of shape (# channels, # time steps) in units of V
  135. coord_electrode : np.ndarray * quantity.Quantity
  136. depth of evenly spaced electrode contact points of shape
  137. (# contacts, ) in units of m, must be monotonously increasing
  138. sigma : float * quantity.Quantity
  139. conductivity of tissue in units of S/m or 1/(ohm*m)
  140. Defaults to 0.3 S/m
  141. vaknin_el : bool
  142. flag for using method of Vaknin to endpoint electrodes
  143. Defaults to True
  144. f_type : str
  145. type of spatial filter, must be a scipy.signal filter design method
  146. Defaults to 'gaussian'
  147. f_order : list
  148. settings for spatial filter, arg passed to filter design function
  149. Defaults to (3,1) for the gaussian
  150. '''
  151. self.parameters(**kwargs)
  152. CSD.__init__(self, lfp, self.f_type, self.f_order)
  153. diff_diff_coord = np.diff(np.diff(coord_electrode)).magnitude
  154. zeros_ddc = np.zeros_like(diff_diff_coord)
  155. try:
  156. assert(np.all(np.isclose(diff_diff_coord, zeros_ddc, atol=1e-12)))
  157. except AssertionError as ae:
  158. print('coord_electrode not monotonously varying')
  159. raise ae
  160. if self.vaknin_el:
  161. # extend lfps array by duplicating potential at endpoint contacts
  162. if lfp.ndim == 1:
  163. self.lfp = np.empty((lfp.shape[0] + 2, )) * lfp.units
  164. else:
  165. self.lfp = np.empty((lfp.shape[0] + 2, lfp.shape[1])) * lfp.units
  166. self.lfp[0, ] = lfp[0, ]
  167. self.lfp[1:-1, ] = lfp
  168. self.lfp[-1, ] = lfp[-1, ]
  169. else:
  170. self.lfp = lfp
  171. self.name = 'Standard CSD method'
  172. self.coord_electrode = coord_electrode
  173. self.f_inv_matrix = self.get_f_inv_matrix()
  174. def parameters(self, **kwargs):
  175. '''Defining the default values of the method passed as kwargs
  176. Parameters
  177. ----------
  178. **kwargs
  179. Same as those passed to initialize the Class
  180. '''
  181. self.sigma = kwargs.pop('sigma', 0.3 * pq.S / pq.m)
  182. self.vaknin_el = kwargs.pop('vaknin_el', True)
  183. self.f_type = kwargs.pop('f_type', 'gaussian')
  184. self.f_order = kwargs.pop('f_order', (3, 1))
  185. if kwargs:
  186. raise TypeError('Invalid keyword arguments:', kwargs.keys())
  187. def get_f_inv_matrix(self):
  188. '''Calculate the inverse F-matrix for the standard CSD method'''
  189. h_val = abs(np.diff(self.coord_electrode)[0])
  190. f_inv = -np.eye(self.lfp.shape[0])
  191. # Inner matrix elements is just the discrete laplacian coefficients
  192. for j in range(1, f_inv.shape[0] - 1):
  193. f_inv[j, j - 1: j + 2] = np.array([1., -2., 1.])
  194. return f_inv * -self.sigma / h_val
  195. def get_csd(self):
  196. '''
  197. Perform the iCSD calculation, i.e: iCSD=F_inv*LFP
  198. Returns
  199. -------
  200. csd : np.ndarray * quantity.Quantity
  201. Array with the csd estimate
  202. '''
  203. csd = np.dot(self.f_inv_matrix, self.lfp)[1:-1, ]
  204. # `np.dot()` does not return correct units, so the units of `csd` must
  205. # be assigned manually
  206. csd_units = (self.f_inv_matrix.units * self.lfp.units).simplified
  207. csd = csd.magnitude * csd_units
  208. return csd
  209. class DeltaiCSD(CSD):
  210. '''
  211. delta-iCSD method
  212. '''
  213. def __init__(self, lfp, coord_electrode, **kwargs):
  214. '''
  215. Initialize the delta-iCSD method class object
  216. Parameters
  217. ----------
  218. lfp : np.ndarray * quantity.Quantity
  219. LFP signal of shape (# channels, # time steps) in units of V
  220. coord_electrode : np.ndarray * quantity.Quantity
  221. depth of evenly spaced electrode contact points of shape
  222. (# contacts, ) in units of m
  223. diam : float * quantity.Quantity
  224. diamater of the assumed circular planar current sources centered
  225. at each contact
  226. Defaults to 500E-6 meters
  227. sigma : float * quantity.Quantity
  228. conductivity of tissue in units of S/m or 1/(ohm*m)
  229. Defaults to 0.3 S / m
  230. sigma_top : float * quantity.Quantity
  231. conductivity on top of tissue in units of S/m or 1/(ohm*m)
  232. Defaults to 0.3 S / m
  233. f_type : str
  234. type of spatial filter, must be a scipy.signal filter design method
  235. Defaults to 'gaussian'
  236. f_order : list
  237. settings for spatial filter, arg passed to filter design function
  238. Defaults to (3,1) for gaussian
  239. '''
  240. self.parameters(**kwargs)
  241. CSD.__init__(self, lfp, self.f_type, self.f_order)
  242. try: # Should the class not take care of this?!
  243. assert(self.diam.units == coord_electrode.units)
  244. except AssertionError as ae:
  245. print('units of coord_electrode ({}) and diam ({}) differ'
  246. .format(coord_electrode.units, self.diam.units))
  247. raise ae
  248. try:
  249. assert(np.all(np.diff(coord_electrode) > 0))
  250. except AssertionError as ae:
  251. print('values of coord_electrode not continously increasing')
  252. raise ae
  253. try:
  254. assert(self.diam.size == 1 or self.diam.size == coord_electrode.size)
  255. if self.diam.size == coord_electrode.size:
  256. assert(np.all(self.diam > 0 * self.diam.units))
  257. else:
  258. assert(self.diam > 0 * self.diam.units)
  259. except AssertionError as ae:
  260. print('diam must be positive scalar or of same shape \
  261. as coord_electrode')
  262. raise ae
  263. if self.diam.size == 1:
  264. self.diam = np.ones(coord_electrode.size) * self.diam
  265. self.name = 'delta-iCSD method'
  266. self.coord_electrode = coord_electrode
  267. # initialize F- and iCSD-matrices
  268. self.f_matrix = np.empty((self.coord_electrode.size,
  269. self.coord_electrode.size))
  270. self.f_matrix = self.get_f_matrix()
  271. def parameters(self, **kwargs):
  272. '''Defining the default values of the method passed as kwargs
  273. Parameters
  274. ----------
  275. **kwargs
  276. Same as those passed to initialize the Class
  277. '''
  278. self.diam = kwargs.pop('diam', 500E-6 * pq.m)
  279. self.sigma = kwargs.pop('sigma', 0.3 * pq.S / pq.m)
  280. self.sigma_top = kwargs.pop('sigma_top', 0.3 * pq.S / pq.m)
  281. self.f_type = kwargs.pop('f_type', 'gaussian')
  282. self.f_order = kwargs.pop('f_order', (3, 1))
  283. if kwargs:
  284. raise TypeError('Invalid keyword arguments:', kwargs.keys())
  285. def get_f_matrix(self):
  286. '''Calculate the F-matrix'''
  287. f_matrix = np.empty((self.coord_electrode.size,
  288. self.coord_electrode.size)) * self.coord_electrode.units
  289. for j in range(self.coord_electrode.size):
  290. for i in range(self.coord_electrode.size):
  291. f_matrix[j, i] = ((np.sqrt((self.coord_electrode[j] -
  292. self.coord_electrode[i])**2 +
  293. (self.diam[j] / 2)**2) - abs(self.coord_electrode[j] -
  294. self.coord_electrode[i])) +
  295. (self.sigma - self.sigma_top) / (self.sigma +
  296. self.sigma_top) *
  297. (np.sqrt((self.coord_electrode[j] +
  298. self.coord_electrode[i])**2 + (self.diam[j] / 2)**2)-
  299. abs(self.coord_electrode[j] + self.coord_electrode[i])))
  300. f_matrix /= (2 * self.sigma)
  301. return f_matrix
  302. class StepiCSD(CSD):
  303. '''step-iCSD method'''
  304. def __init__(self, lfp, coord_electrode, **kwargs):
  305. '''
  306. Initializing step-iCSD method class object
  307. Parameters
  308. ----------
  309. lfp : np.ndarray * quantity.Quantity
  310. LFP signal of shape (# channels, # time steps) in units of V
  311. coord_electrode : np.ndarray * quantity.Quantity
  312. depth of evenly spaced electrode contact points of shape
  313. (# contacts, ) in units of m
  314. diam : float or np.ndarray * quantity.Quantity
  315. diameter(s) of the assumed circular planar current sources centered
  316. at each contact
  317. Defaults to 500E-6 meters
  318. h : float or np.ndarray * quantity.Quantity
  319. assumed thickness of the source cylinders at all or each contact
  320. Defaults to np.ones(15) * 100E-6 * pq.m
  321. sigma : float * quantity.Quantity
  322. conductivity of tissue in units of S/m or 1/(ohm*m)
  323. Defaults to 0.3 S / m
  324. sigma_top : float * quantity.Quantity
  325. conductivity on top of tissue in units of S/m or 1/(ohm*m)
  326. Defaults to 0.3 S / m
  327. tol : float
  328. tolerance of numerical integration
  329. Defaults 1e-6
  330. f_type : str
  331. type of spatial filter, must be a scipy.signal filter design method
  332. Defaults to 'gaussian'
  333. f_order : list
  334. settings for spatial filter, arg passed to filter design function
  335. Defaults to (3,1) for the gaussian
  336. '''
  337. self.parameters(**kwargs)
  338. CSD.__init__(self, lfp, self.f_type, self.f_order)
  339. try: # Should the class not take care of this?
  340. assert(self.diam.units == coord_electrode.units)
  341. except AssertionError as ae:
  342. print('units of coord_electrode ({}) and diam ({}) differ'
  343. .format(coord_electrode.units, self.diam.units))
  344. raise ae
  345. try:
  346. assert(np.all(np.diff(coord_electrode) > 0))
  347. except AssertionError as ae:
  348. print('values of coord_electrode not continously increasing')
  349. raise ae
  350. try:
  351. assert(self.diam.size == 1 or self.diam.size == coord_electrode.size)
  352. if self.diam.size == coord_electrode.size:
  353. assert(np.all(self.diam > 0 * self.diam.units))
  354. else:
  355. assert(self.diam > 0 * self.diam.units)
  356. except AssertionError as ae:
  357. print('diam must be positive scalar or of same shape \
  358. as coord_electrode')
  359. raise ae
  360. if self.diam.size == 1:
  361. self.diam = np.ones(coord_electrode.size) * self.diam
  362. try:
  363. assert(self.h.size == 1 or self.h.size == coord_electrode.size)
  364. if self.h.size == coord_electrode.size:
  365. assert(np.all(self.h > 0 * self.h.units))
  366. except AssertionError as ae:
  367. print('h must be scalar or of same shape as coord_electrode')
  368. raise ae
  369. if self.h.size == 1:
  370. self.h = np.ones(coord_electrode.size) * self.h
  371. self.name = 'step-iCSD method'
  372. self.coord_electrode = coord_electrode
  373. # compute forward-solution matrix
  374. self.f_matrix = self.get_f_matrix()
  375. def parameters(self, **kwargs):
  376. '''Defining the default values of the method passed as kwargs
  377. Parameters
  378. ----------
  379. **kwargs
  380. Same as those passed to initialize the Class
  381. '''
  382. self.diam = kwargs.pop('diam', 500E-6 * pq.m)
  383. self.h = kwargs.pop('h', np.ones(23) * 100E-6 * pq.m)
  384. self.sigma = kwargs.pop('sigma', 0.3 * pq.S / pq.m)
  385. self.sigma_top = kwargs.pop('sigma_top', 0.3 * pq.S / pq.m)
  386. self.tol = kwargs.pop('tol', 1e-6)
  387. self.f_type = kwargs.pop('f_type', 'gaussian')
  388. self.f_order = kwargs.pop('f_order', (3, 1))
  389. if kwargs:
  390. raise TypeError('Invalid keyword arguments:', kwargs.keys())
  391. def get_f_matrix(self):
  392. '''Calculate F-matrix for step iCSD method'''
  393. el_len = self.coord_electrode.size
  394. f_matrix = np.zeros((el_len, el_len))
  395. for j in range(el_len):
  396. for i in range(el_len):
  397. lower_int = self.coord_electrode[i] - self.h[j] / 2
  398. if lower_int < 0:
  399. lower_int = self.h[j].units
  400. upper_int = self.coord_electrode[i] + self.h[j] / 2
  401. # components of f_matrix object
  402. f_cyl0 = si.quad(self._f_cylinder,
  403. a=lower_int, b=upper_int,
  404. args=(float(self.coord_electrode[j]),
  405. float(self.diam[j]),
  406. float(self.sigma)),
  407. epsabs=self.tol)[0]
  408. f_cyl1 = si.quad(self._f_cylinder, a=lower_int, b=upper_int,
  409. args=(-float(self.coord_electrode[j]),
  410. float(self.diam[j]), float(self.sigma)),
  411. epsabs=self.tol)[0]
  412. # method of images coefficient
  413. mom = (self.sigma - self.sigma_top) / (self.sigma + self.sigma_top)
  414. f_matrix[j, i] = f_cyl0 + mom * f_cyl1
  415. # assume si.quad trash the units
  416. return f_matrix * self.h.units**2 / self.sigma.units
  417. def _f_cylinder(self, zeta, z_val, diam, sigma):
  418. '''function used by class method'''
  419. f_cyl = 1. / (2. * sigma) * \
  420. (np.sqrt((diam / 2)**2 + ((z_val - zeta))**2) - abs(z_val - zeta))
  421. return f_cyl
  422. class SplineiCSD(CSD):
  423. '''spline iCSD method'''
  424. def __init__(self, lfp, coord_electrode, **kwargs):
  425. '''
  426. Initializing spline-iCSD method class object
  427. Parameters
  428. ----------
  429. lfp : np.ndarray * quantity.Quantity
  430. LFP signal of shape (# channels, # time steps) in units of V
  431. coord_electrode : np.ndarray * quantity.Quantity
  432. depth of evenly spaced electrode contact points of shape
  433. (# contacts, ) in units of m
  434. diam : float * quantity.Quantity
  435. diamater of the assumed circular planar current sources centered
  436. at each contact
  437. Defaults to 500E-6 meters
  438. sigma : float * quantity.Quantity
  439. conductivity of tissue in units of S/m or 1/(ohm*m)
  440. Defaults to 0.3 S / m
  441. sigma_top : float * quantity.Quantity
  442. conductivity on top of tissue in units of S/m or 1/(ohm*m)
  443. Defaults to 0.3 S / m
  444. tol : float
  445. tolerance of numerical integration
  446. Defaults 1e-6
  447. f_type : str
  448. type of spatial filter, must be a scipy.signal filter design method
  449. Defaults to 'gaussian'
  450. f_order : list
  451. settings for spatial filter, arg passed to filter design function
  452. Defaults to (3,1) for the gaussian
  453. num_steps : int
  454. number of data points for the spatially upsampled LFP/CSD data
  455. Defaults to 200
  456. '''
  457. self.parameters(**kwargs)
  458. CSD.__init__(self, lfp, self.f_type, self.f_order)
  459. try: # Should the class not take care of this?!
  460. assert(self.diam.units == coord_electrode.units)
  461. except AssertionError as ae:
  462. print('units of coord_electrode ({}) and diam ({}) differ'
  463. .format(coord_electrode.units, self.diam.units))
  464. raise
  465. try:
  466. assert(np.all(np.diff(coord_electrode) > 0))
  467. except AssertionError as ae:
  468. print('values of coord_electrode not continously increasing')
  469. raise ae
  470. try:
  471. assert(self.diam.size == 1 or self.diam.size == coord_electrode.size)
  472. if self.diam.size == coord_electrode.size:
  473. assert(np.all(self.diam > 0 * self.diam.units))
  474. except AssertionError as ae:
  475. print('diam must be scalar or of same shape as coord_electrode')
  476. raise ae
  477. if self.diam.size == 1:
  478. self.diam = np.ones(coord_electrode.size) * self.diam
  479. self.name = 'spline-iCSD method'
  480. self.coord_electrode = coord_electrode
  481. # compute stuff
  482. self.f_matrix = self.get_f_matrix()
  483. def parameters(self, **kwargs):
  484. '''Defining the default values of the method passed as kwargs
  485. Parameters
  486. ----------
  487. **kwargs
  488. Same as those passed to initialize the Class
  489. '''
  490. self.diam = kwargs.pop('diam', 500E-6 * pq.m)
  491. self.sigma = kwargs.pop('sigma', 0.3 * pq.S / pq.m)
  492. self.sigma_top = kwargs.pop('sigma_top', 0.3 * pq.S / pq.m)
  493. self.tol = kwargs.pop('tol', 1e-6)
  494. self.num_steps = kwargs.pop('num_steps', 200)
  495. self.f_type = kwargs.pop('f_type', 'gaussian')
  496. self.f_order = kwargs.pop('f_order', (3, 1))
  497. if kwargs:
  498. raise TypeError('Invalid keyword arguments:', kwargs.keys())
  499. def get_f_matrix(self):
  500. '''Calculate the F-matrix for cubic spline iCSD method'''
  501. el_len = self.coord_electrode.size
  502. z_js = np.zeros(el_len + 1)
  503. z_js[:-1] = np.array(self.coord_electrode)
  504. z_js[-1] = z_js[-2] + float(np.diff(self.coord_electrode).mean())
  505. # Define integration matrixes
  506. f_mat0 = np.zeros((el_len, el_len + 1))
  507. f_mat1 = np.zeros((el_len, el_len + 1))
  508. f_mat2 = np.zeros((el_len, el_len + 1))
  509. f_mat3 = np.zeros((el_len, el_len + 1))
  510. # Calc. elements
  511. for j in range(el_len):
  512. for i in range(el_len):
  513. f_mat0[j, i] = si.quad(self._f_mat0, a=z_js[i], b=z_js[i + 1],
  514. args=(z_js[j + 1],
  515. float(self.sigma),
  516. float(self.diam[j])),
  517. epsabs=self.tol)[0]
  518. f_mat1[j, i] = si.quad(self._f_mat1, a=z_js[i], b=z_js[i + 1],
  519. args=(z_js[j + 1], z_js[i],
  520. float(self.sigma),
  521. float(self.diam[j])),
  522. epsabs=self.tol)[0]
  523. f_mat2[j, i] = si.quad(self._f_mat2, a=z_js[i], b=z_js[i + 1],
  524. args=(z_js[j + 1], z_js[i],
  525. float(self.sigma),
  526. float(self.diam[j])),
  527. epsabs=self.tol)[0]
  528. f_mat3[j, i] = si.quad(self._f_mat3, a=z_js[i], b=z_js[i + 1],
  529. args=(z_js[j + 1], z_js[i],
  530. float(self.sigma),
  531. float(self.diam[j])),
  532. epsabs=self.tol)[0]
  533. # image technique if conductivity not constant:
  534. if self.sigma != self.sigma_top:
  535. f_mat0[j, i] = f_mat0[j, i] + (self.sigma-self.sigma_top) / \
  536. (self.sigma + self.sigma_top) * \
  537. si.quad(self._f_mat0, a=z_js[i], b=z_js[i+1], \
  538. args=(-z_js[j+1],
  539. float(self.sigma), float(self.diam[j])), \
  540. epsabs=self.tol)[0]
  541. f_mat1[j, i] = f_mat1[j, i] + (self.sigma-self.sigma_top) / \
  542. (self.sigma + self.sigma_top) * \
  543. si.quad(self._f_mat1, a=z_js[i], b=z_js[i+1], \
  544. args=(-z_js[j+1], z_js[i], float(self.sigma),
  545. float(self.diam[j])), epsabs=self.tol)[0]
  546. f_mat2[j, i] = f_mat2[j, i] + (self.sigma-self.sigma_top) / \
  547. (self.sigma + self.sigma_top) * \
  548. si.quad(self._f_mat2, a=z_js[i], b=z_js[i+1], \
  549. args=(-z_js[j+1], z_js[i], float(self.sigma),
  550. float(self.diam[j])), epsabs=self.tol)[0]
  551. f_mat3[j, i] = f_mat3[j, i] + (self.sigma-self.sigma_top) / \
  552. (self.sigma + self.sigma_top) * \
  553. si.quad(self._f_mat3, a=z_js[i], b=z_js[i+1], \
  554. args=(-z_js[j+1], z_js[i], float(self.sigma),
  555. float(self.diam[j])), epsabs=self.tol)[0]
  556. e_mat0, e_mat1, e_mat2, e_mat3 = self._calc_e_matrices()
  557. # Calculate the F-matrix
  558. f_matrix = np.eye(el_len + 2)
  559. f_matrix[1:-1, :] = np.dot(f_mat0, e_mat0) + \
  560. np.dot(f_mat1, e_mat1) + \
  561. np.dot(f_mat2, e_mat2) + \
  562. np.dot(f_mat3, e_mat3)
  563. return f_matrix * self.coord_electrode.units**2 / self.sigma.units
  564. def get_csd(self):
  565. '''
  566. Calculate the iCSD using the spline iCSD method
  567. Returns
  568. -------
  569. csd : np.ndarray * quantity.Quantity
  570. Array with csd estimate
  571. '''
  572. e_mat = self._calc_e_matrices()
  573. el_len = self.coord_electrode.size
  574. # padding the lfp with zeros on top/bottom
  575. if self.lfp.ndim == 1:
  576. cs_lfp = np.r_[[0], np.asarray(self.lfp), [0]].reshape(1, -1).T
  577. csd = np.zeros(self.num_steps)
  578. else:
  579. cs_lfp = np.vstack((np.zeros(self.lfp.shape[1]),
  580. np.asarray(self.lfp),
  581. np.zeros(self.lfp.shape[1])))
  582. csd = np.zeros((self.num_steps, self.lfp.shape[1]))
  583. cs_lfp *= self.lfp.units
  584. # CSD coefficients
  585. csd_coeff = np.linalg.solve(self.f_matrix, cs_lfp)
  586. # The cubic spline polynomial coefficients
  587. a_mat0 = np.dot(e_mat[0], csd_coeff)
  588. a_mat1 = np.dot(e_mat[1], csd_coeff)
  589. a_mat2 = np.dot(e_mat[2], csd_coeff)
  590. a_mat3 = np.dot(e_mat[3], csd_coeff)
  591. # Extend electrode coordinates in both end by min contact interdistance
  592. h = np.diff(self.coord_electrode).min()
  593. z_js = np.zeros(el_len + 2)
  594. z_js[0] = self.coord_electrode[0] - h
  595. z_js[1: -1] = self.coord_electrode
  596. z_js[-1] = self.coord_electrode[-1] + h
  597. # create high res spatial grid
  598. out_zs = np.linspace(z_js[1], z_js[-2], self.num_steps)
  599. # Calculate iCSD estimate on grid from polynomial coefficients.
  600. i = 0
  601. for j in range(self.num_steps):
  602. if out_zs[j] >= z_js[i + 1]:
  603. i += 1
  604. csd[j, ] = a_mat0[i, :] + a_mat1[i, :] * \
  605. (out_zs[j] - z_js[i]) + \
  606. a_mat2[i, :] * (out_zs[j] - z_js[i])**2 + \
  607. a_mat3[i, :] * (out_zs[j] - z_js[i])**3
  608. csd_unit = (self.f_matrix.units**-1 * self.lfp.units).simplified
  609. return csd * csd_unit
  610. def _f_mat0(self, zeta, z_val, sigma, diam):
  611. '''0'th order potential function'''
  612. return 1. / (2. * sigma) * \
  613. (np.sqrt((diam / 2)**2 + ((z_val - zeta))**2) - abs(z_val - zeta))
  614. def _f_mat1(self, zeta, z_val, zi_val, sigma, diam):
  615. '''1'th order potential function'''
  616. return (zeta - zi_val) * self._f_mat0(zeta, z_val, sigma, diam)
  617. def _f_mat2(self, zeta, z_val, zi_val, sigma, diam):
  618. '''2'nd order potential function'''
  619. return (zeta - zi_val)**2 * self._f_mat0(zeta, z_val, sigma, diam)
  620. def _f_mat3(self, zeta, z_val, zi_val, sigma, diam):
  621. '''3'rd order potential function'''
  622. return (zeta - zi_val)**3 * self._f_mat0(zeta, z_val, sigma, diam)
  623. def _calc_k_matrix(self):
  624. '''Calculate the K-matrix used by to calculate E-matrices'''
  625. el_len = self.coord_electrode.size
  626. h = float(np.diff(self.coord_electrode).min())
  627. c_jm1 = np.eye(el_len + 2, k=0) / h
  628. c_jm1[0, 0] = 0
  629. c_j0 = np.eye(el_len + 2) / h
  630. c_j0[-1, -1] = 0
  631. c_jall = c_j0
  632. c_jall[0, 0] = 1
  633. c_jall[-1, -1] = 1
  634. tjp1 = np.eye(el_len + 2, k=1)
  635. tjm1 = np.eye(el_len + 2, k=-1)
  636. tj0 = np.eye(el_len + 2)
  637. tj0[0, 0] = 0
  638. tj0[-1, -1] = 0
  639. # Defining K-matrix used to calculate e_mat1-3
  640. return np.dot(np.linalg.inv(np.dot(c_jm1, tjm1) +
  641. 2 * np.dot(c_jm1, tj0) +
  642. 2 * c_jall +
  643. np.dot(c_j0, tjp1)),
  644. 3 * (np.dot(np.dot(c_jm1, c_jm1), tj0) -
  645. np.dot(np.dot(c_jm1, c_jm1), tjm1) +
  646. np.dot(np.dot(c_j0, c_j0), tjp1) -
  647. np.dot(np.dot(c_j0, c_j0), tj0)))
  648. def _calc_e_matrices(self):
  649. '''Calculate the E-matrices used by cubic spline iCSD method'''
  650. el_len = self.coord_electrode.size
  651. # expanding electrode grid
  652. h = float(np.diff(self.coord_electrode).min())
  653. # Define transformation matrices
  654. c_mat3 = np.eye(el_len + 1) / h
  655. # Get K-matrix
  656. k_matrix = self._calc_k_matrix()
  657. # Define matrixes for C to A transformation:
  658. tja = np.eye(el_len + 2)[:-1, ]
  659. tjp1a = np.eye(el_len + 2, k=1)[:-1, ]
  660. # Define spline coefficients
  661. e_mat0 = tja
  662. e_mat1 = np.dot(tja, k_matrix)
  663. e_mat2 = 3 * np.dot(c_mat3**2, (tjp1a - tja)) - \
  664. np.dot(np.dot(c_mat3, (tjp1a + 2 * tja)), k_matrix)
  665. e_mat3 = 2 * np.dot(c_mat3**3, (tja - tjp1a)) + \
  666. np.dot(np.dot(c_mat3**2, (tjp1a + tja)), k_matrix)
  667. return e_mat0, e_mat1, e_mat2, e_mat3
  668. if __name__ == '__main__':
  669. from scipy.io import loadmat
  670. import matplotlib.pyplot as plt
  671. #loading test data
  672. test_data = loadmat('test_data.mat')
  673. #prepare lfp data for use, by changing the units to SI and append quantities,
  674. #along with electrode geometry, conductivities and assumed source geometry
  675. lfp_data = test_data['pot1'] * 1E-6 * pq.V # [uV] -> [V]
  676. z_data = np.linspace(100E-6, 2300E-6, 23) * pq.m # [m]
  677. diam = 500E-6 * pq.m # [m]
  678. h = 100E-6 * pq.m # [m]
  679. sigma = 0.3 * pq.S / pq.m # [S/m] or [1/(ohm*m)]
  680. sigma_top = 0.3 * pq.S / pq.m # [S/m] or [1/(ohm*m)]
  681. # Input dictionaries for each method
  682. delta_input = {
  683. 'lfp' : lfp_data,
  684. 'coord_electrode' : z_data,
  685. 'diam' : diam, # source diameter
  686. 'sigma' : sigma, # extracellular conductivity
  687. 'sigma_top' : sigma, # conductivity on top of cortex
  688. 'f_type' : 'gaussian', # gaussian filter
  689. 'f_order' : (3, 1), # 3-point filter, sigma = 1.
  690. }
  691. step_input = {
  692. 'lfp' : lfp_data,
  693. 'coord_electrode' : z_data,
  694. 'diam' : diam,
  695. 'h' : h, # source thickness
  696. 'sigma' : sigma,
  697. 'sigma_top' : sigma,
  698. 'tol' : 1E-12, # Tolerance in numerical integration
  699. 'f_type' : 'gaussian',
  700. 'f_order' : (3, 1),
  701. }
  702. spline_input = {
  703. 'lfp' : lfp_data,
  704. 'coord_electrode' : z_data,
  705. 'diam' : diam,
  706. 'sigma' : sigma,
  707. 'sigma_top' : sigma,
  708. 'num_steps' : 201, # Spatial CSD upsampling to N steps
  709. 'tol' : 1E-12,
  710. 'f_type' : 'gaussian',
  711. 'f_order' : (20, 5),
  712. }
  713. std_input = {
  714. 'lfp' : lfp_data,
  715. 'coord_electrode' : z_data,
  716. 'sigma' : sigma,
  717. 'f_type' : 'gaussian',
  718. 'f_order' : (3, 1),
  719. }
  720. #Create the different CSD-method class instances. We use the class methods
  721. #get_csd() and filter_csd() below to get the raw and spatially filtered
  722. #versions of the current-source density estimates.
  723. csd_dict = dict(
  724. delta_icsd = DeltaiCSD(**delta_input),
  725. step_icsd = StepiCSD(**step_input),
  726. spline_icsd = SplineiCSD(**spline_input),
  727. std_csd = StandardCSD(**std_input),
  728. )
  729. #plot
  730. for method, csd_obj in list(csd_dict.items()):
  731. fig, axes = plt.subplots(3,1, figsize=(8,8))
  732. #plot LFP signal
  733. ax = axes[0]
  734. im = ax.imshow(np.array(lfp_data), origin='upper', vmin=-abs(lfp_data).max(), \
  735. vmax=abs(lfp_data).max(), cmap='jet_r', interpolation='nearest')
  736. ax.axis(ax.axis('tight'))
  737. cb = plt.colorbar(im, ax=ax)
  738. cb.set_label('LFP (%s)' % lfp_data.dimensionality.string)
  739. ax.set_xticklabels([])
  740. ax.set_title('LFP')
  741. ax.set_ylabel('ch #')
  742. #plot raw csd estimate
  743. csd = csd_obj.get_csd()
  744. ax = axes[1]
  745. im = ax.imshow(np.array(csd), origin='upper', vmin=-abs(csd).max(), \
  746. vmax=abs(csd).max(), cmap='jet_r', interpolation='nearest')
  747. ax.axis(ax.axis('tight'))
  748. ax.set_title(csd_obj.name)
  749. cb = plt.colorbar(im, ax=ax)
  750. cb.set_label('CSD (%s)' % csd.dimensionality.string)
  751. ax.set_xticklabels([])
  752. ax.set_ylabel('ch #')
  753. #plot spatially filtered csd estimate
  754. ax = axes[2]
  755. csd = csd_obj.filter_csd(csd)
  756. im = ax.imshow(np.array(csd), origin='upper', vmin=-abs(csd).max(), \
  757. vmax=abs(csd).max(), cmap='jet_r', interpolation='nearest')
  758. ax.axis(ax.axis('tight'))
  759. ax.set_title(csd_obj.name + ', filtered')
  760. cb = plt.colorbar(im, ax=ax)
  761. cb.set_label('CSD (%s)' % csd.dimensionality.string)
  762. ax.set_ylabel('ch #')
  763. ax.set_xlabel('timestep')
  764. plt.show()