kernels.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. # -*- coding: utf-8 -*-
  2. """
  3. Definition of a hierarchy of classes for kernel functions to be used
  4. in convolution, e.g., for data smoothing (low pass filtering) or
  5. firing rate estimation.
  6. Examples of usage:
  7. >>> kernel1 = kernels.GaussianKernel(sigma=100*ms)
  8. >>> kernel2 = kernels.ExponentialKernel(sigma=8*mm, invert=True)
  9. :copyright: Copyright 2016 by the Elephant team, see AUTHORS.txt.
  10. :license: Modified BSD, see LICENSE.txt for details.
  11. """
  12. import quantities as pq
  13. import numpy as np
  14. import scipy.special
  15. def inherit_docstring(fromfunc, sep=""):
  16. """
  17. Decorator: Copy the docstring of `fromfunc`
  18. based on:
  19. http://stackoverflow.com/questions/13741998/
  20. is-there-a-way-to-let-classes-inherit-the-documentation-of-their-superclass-with
  21. """
  22. def _decorator(func):
  23. parent_doc = fromfunc.__doc__
  24. if func.__doc__ is None:
  25. func.__doc__ = parent_doc
  26. else:
  27. func.__doc__ = sep.join([parent_doc, func.__doc__])
  28. return func
  29. return _decorator
  30. class Kernel(object):
  31. """
  32. This is the base class for commonly used kernels.
  33. General definition of kernel:
  34. A function :math:`K(x, y)` is called a kernel function if
  35. :math:`\\int K(x, y) g(x) g(y)\\ \\textrm{d}x\\ \\textrm{d}y
  36. \\ \\geq 0\\ \\ \\ \\forall\\ g \\in L_2`
  37. Currently implemented kernels are:
  38. - rectangular
  39. - triangular
  40. - epanechnikovlike
  41. - gaussian
  42. - laplacian
  43. - exponential (asymmetric)
  44. - alpha function (asymmetric)
  45. In neuroscience a popular application of kernels is in performing smoothing
  46. operations via convolution. In this case, the kernel has the properties of
  47. a probability density, i.e., it is positive and normalized to one. Popular
  48. choices are the rectangular or Gaussian kernels.
  49. Exponential and alpha kernels may also be used to represent the postynaptic
  50. current / potentials in a linear (current-based) model.
  51. Parameters
  52. ----------
  53. sigma : Quantity scalar
  54. Standard deviation of the kernel.
  55. invert: bool, optional
  56. If true, asymmetric kernels (e.g., exponential
  57. or alpha kernels) are inverted along the time axis.
  58. Default: False
  59. """
  60. def __init__(self, sigma, invert=False):
  61. if not (isinstance(sigma, pq.Quantity)):
  62. raise TypeError("sigma must be a quantity!")
  63. if sigma.magnitude < 0:
  64. raise ValueError("sigma cannot be negative!")
  65. if not isinstance(invert, bool):
  66. raise ValueError("invert must be bool!")
  67. self.sigma = sigma
  68. self.invert = invert
  69. def __call__(self, t):
  70. """
  71. Evaluates the kernel at all points in the array `t`.
  72. Parameter
  73. ---------
  74. t : Quantity 1D
  75. Interval on which the kernel is evaluated, not necessarily
  76. a time interval.
  77. Returns
  78. -------
  79. Quantity 1D
  80. The result of the kernel evaluations.
  81. """
  82. if not (isinstance(t, pq.Quantity)):
  83. raise TypeError("The argument of the kernel callable must be "
  84. "of type quantity!")
  85. if t.dimensionality.simplified != self.sigma.dimensionality.simplified:
  86. raise TypeError("The dimensionality of sigma and the input array "
  87. "to the callable kernel object must be the same. "
  88. "Otherwise a normalization to 1 of the kernel "
  89. "cannot be performed.")
  90. self._sigma_scaled = self.sigma.rescale(t.units)
  91. # A hidden variable _sigma_scaled is introduced here in order to avoid
  92. # accumulation of floating point errors of sigma upon multiple
  93. # usages of the __call__ - function for the same Kernel instance.
  94. return self._evaluate(t)
  95. def _evaluate(self, t):
  96. """
  97. Evaluates the kernel.
  98. Parameter
  99. ---------
  100. t : Quantity 1D
  101. Interval on which the kernel is evaluated, not necessarily
  102. a time interval.
  103. Returns
  104. -------
  105. Quantity 1D
  106. The result of the kernel evaluation.
  107. """
  108. raise NotImplementedError("The Kernel class should not be used directly, "
  109. "instead the subclasses for the single kernels.")
  110. def boundary_enclosing_area_fraction(self, fraction):
  111. """
  112. Calculates the boundary :math:`b` so that the integral from
  113. :math:`-b` to :math:`b` encloses a certain fraction of the
  114. integral over the complete kernel. By definition the returned value
  115. of the method boundary_enclosing_area_fraction is hence non-negative,
  116. even if the whole probability mass of the kernel is concentrated over
  117. negative support for inverted kernels.
  118. Parameter
  119. ---------
  120. fraction : float
  121. Fraction of the whole area which has to be enclosed.
  122. Returns
  123. -------
  124. Quantity scalar
  125. Boundary of the kernel containing area `fraction` under the
  126. kernel density.
  127. """
  128. self._check_fraction(fraction)
  129. sigma_division = 500 # arbitrary choice
  130. interval = self.sigma / sigma_division
  131. self._sigma_scaled = self.sigma
  132. area = 0
  133. counter = 0
  134. while area < fraction:
  135. area += (self._evaluate((counter + 1) * interval) +
  136. self._evaluate(counter * interval)) * interval / 2
  137. area += (self._evaluate(-1 * (counter + 1) * interval) +
  138. self._evaluate(-1 * counter * interval)) * interval / 2
  139. counter += 1
  140. if(counter > 250000):
  141. raise ValueError("fraction was chosen too close to one such "
  142. "that in combination with integral "
  143. "approximation errors the calculation of a "
  144. "boundary was not possible.")
  145. return counter * interval
  146. def _check_fraction(self, fraction):
  147. """
  148. Checks the input variable of the method boundary_enclosing_area_fraction
  149. for validity of type and value.
  150. Parameter
  151. ---------
  152. fraction : float or int
  153. Fraction of the area under the kernel function.
  154. """
  155. if not isinstance(fraction, (float, int)):
  156. raise TypeError("`fraction` must be float or integer!")
  157. if not 0 <= fraction < 1:
  158. raise ValueError("`fraction` must be in the interval [0, 1)!")
  159. def median_index(self, t):
  160. """
  161. Estimates the index of the Median of the kernel.
  162. This parameter is not mandatory for symmetrical kernels but it is
  163. required when asymmetrical kernels have to be aligned at their median.
  164. Parameter
  165. ---------
  166. t : Quantity 1D
  167. Interval on which the kernel is evaluated,
  168. Returns
  169. -------
  170. int
  171. Index of the estimated value of the kernel median.
  172. Remarks
  173. -------
  174. The formula in this method using retrieval of the sampling interval
  175. from t only works for t with equidistant intervals!
  176. The formula calculates the Median slightly wrong by the potentially
  177. ignored probability in the distribution corresponding to lower values
  178. than the minimum in the array t.
  179. """
  180. return np.nonzero(self(t).cumsum() *
  181. (t[len(t) - 1] - t[0]) / (len(t) - 1) >= 0.5)[0].min()
  182. def is_symmetric(self):
  183. """
  184. In the case of symmetric kernels, this method is overwritten in the
  185. class SymmetricKernel, where it returns 'True', hence leaving the
  186. here returned value 'False' for the asymmetric kernels.
  187. """
  188. return False
  189. class SymmetricKernel(Kernel):
  190. """
  191. Base class for symmetric kernels.
  192. Derived from:
  193. """
  194. __doc__ += Kernel.__doc__
  195. def is_symmetric(self):
  196. return True
  197. class RectangularKernel(SymmetricKernel):
  198. """
  199. Class for rectangular kernels
  200. .. math::
  201. K(t) = \\left\\{\\begin{array}{ll} \\frac{1}{2 \\tau}, & |t| < \\tau \\\\
  202. 0, & |t| \\geq \\tau \\end{array} \\right.
  203. with :math:`\\tau = \\sqrt{3} \\sigma` corresponding to the half width
  204. of the kernel.
  205. Besides the standard deviation `sigma`, for consistency of interfaces the
  206. parameter `invert` needed for asymmetric kernels also exists without
  207. having any effect in the case of symmetric kernels.
  208. Derived from:
  209. """
  210. __doc__ += SymmetricKernel.__doc__
  211. @property
  212. def min_cutoff(self):
  213. min_cutoff = np.sqrt(3.0)
  214. return min_cutoff
  215. @inherit_docstring(Kernel._evaluate)
  216. def _evaluate(self, t):
  217. return (0.5 / (np.sqrt(3.0) * self._sigma_scaled)) * \
  218. (np.absolute(t) < np.sqrt(3.0) * self._sigma_scaled)
  219. @inherit_docstring(Kernel.boundary_enclosing_area_fraction)
  220. def boundary_enclosing_area_fraction(self, fraction):
  221. self._check_fraction(fraction)
  222. return np.sqrt(3.0) * self.sigma * fraction
  223. class TriangularKernel(SymmetricKernel):
  224. """
  225. Class for triangular kernels
  226. .. math::
  227. K(t) = \\left\\{ \\begin{array}{ll} \\frac{1}{\\tau} (1
  228. - \\frac{|t|}{\\tau}), & |t| < \\tau \\\\
  229. 0, & |t| \\geq \\tau \\end{array} \\right.
  230. with :math:`\\tau = \\sqrt{6} \\sigma` corresponding to the half width of
  231. the kernel.
  232. Besides the standard deviation `sigma`, for consistency of interfaces the
  233. parameter `invert` needed for asymmetric kernels also exists without
  234. having any effect in the case of symmetric kernels.
  235. Derived from:
  236. """
  237. __doc__ += SymmetricKernel.__doc__
  238. @property
  239. def min_cutoff(self):
  240. min_cutoff = np.sqrt(6.0)
  241. return min_cutoff
  242. @inherit_docstring(Kernel._evaluate)
  243. def _evaluate(self, t):
  244. return (1.0 / (np.sqrt(6.0) * self._sigma_scaled)) * np.maximum(
  245. 0.0,
  246. (1.0 - (np.absolute(t) /
  247. (np.sqrt(6.0) * self._sigma_scaled)).magnitude))
  248. @inherit_docstring(Kernel.boundary_enclosing_area_fraction)
  249. def boundary_enclosing_area_fraction(self, fraction):
  250. self._check_fraction(fraction)
  251. return np.sqrt(6.0) * self.sigma * (1 - np.sqrt(1 - fraction))
  252. class EpanechnikovLikeKernel(SymmetricKernel):
  253. """
  254. Class for epanechnikov-like kernels
  255. .. math::
  256. K(t) = \\left\\{\\begin{array}{ll} (3 /(4 d)) (1 - (t / d)^2),
  257. & |t| < d \\\\
  258. 0, & |t| \\geq d \\end{array} \\right.
  259. with :math:`d = \\sqrt{5} \\sigma` being the half width of the kernel.
  260. The Epanechnikov kernel under full consideration of its axioms has a half
  261. width of :math:`\\sqrt{5}`. Ignoring one axiom also the respective kernel
  262. with half width = 1 can be called Epanechnikov kernel.
  263. ( https://de.wikipedia.org/wiki/Epanechnikov-Kern )
  264. However, arbitrary width of this type of kernel is here preferred to be
  265. called 'Epanechnikov-like' kernel.
  266. Besides the standard deviation `sigma`, for consistency of interfaces the
  267. parameter `invert` needed for asymmetric kernels also exists without
  268. having any effect in the case of symmetric kernels.
  269. Derived from:
  270. """
  271. __doc__ += SymmetricKernel.__doc__
  272. @property
  273. def min_cutoff(self):
  274. min_cutoff = np.sqrt(5.0)
  275. return min_cutoff
  276. @inherit_docstring(Kernel._evaluate)
  277. def _evaluate(self, t):
  278. return (3.0 / (4.0 * np.sqrt(5.0) * self._sigma_scaled)) * np.maximum(
  279. 0.0,
  280. 1 - (t / (np.sqrt(5.0) * self._sigma_scaled)).magnitude ** 2)
  281. @inherit_docstring(Kernel.boundary_enclosing_area_fraction)
  282. def boundary_enclosing_area_fraction(self, fraction):
  283. """
  284. For Epanechnikov-like kernels, integration of its density within
  285. the boundaries 0 and :math:`b`, and then solving for :math:`b` leads
  286. to the problem of finding the roots of a polynomial of third order.
  287. The implemented formulas are based on the solution of this problem
  288. given in https://en.wikipedia.org/wiki/Cubic_function,
  289. where the following 3 solutions are given:
  290. - :math:`u_1 = 1`: Solution on negative side
  291. - :math:`u_2 = \\frac{-1 + i\\sqrt{3}}{2}`: Solution for larger
  292. values than zero crossing of the density
  293. - :math:`u_3 = \\frac{-1 - i\\sqrt{3}}{2}`: Solution for smaller
  294. values than zero crossing of the density
  295. The solution :math:`u_3` is the relevant one for the problem at hand,
  296. since it involves only positive area contributions.
  297. """
  298. self._check_fraction(fraction)
  299. # Python's complex-operator cannot handle quantities, hence the
  300. # following construction on quantities is necessary:
  301. Delta_0 = complex(1.0 / (5.0 * self.sigma.magnitude**2), 0) / \
  302. self.sigma.units**2
  303. Delta_1 = complex(2.0 * np.sqrt(5.0) * fraction /
  304. (25.0 * self.sigma.magnitude**3), 0) / \
  305. self.sigma.units**3
  306. C = ((Delta_1 + (Delta_1**2.0 - 4.0 * Delta_0**3.0)**(1.0 / 2.0)) /
  307. 2.0)**(1.0 / 3.0)
  308. u_3 = complex(-1.0 / 2.0, -np.sqrt(3.0) / 2.0)
  309. b = -5.0 * self.sigma**2 * (u_3 * C + Delta_0 / (u_3 * C))
  310. return b.real
  311. class GaussianKernel(SymmetricKernel):
  312. """
  313. Class for gaussian kernels
  314. .. math::
  315. K(t) = (\\frac{1}{\\sigma \\sqrt{2 \\pi}})
  316. \\exp(-\\frac{t^2}{2 \\sigma^2})
  317. with :math:`\\sigma` being the standard deviation.
  318. Besides the standard deviation `sigma`, for consistency of interfaces the
  319. parameter `invert` needed for asymmetric kernels also exists without
  320. having any effect in the case of symmetric kernels.
  321. Derived from:
  322. """
  323. __doc__ += SymmetricKernel.__doc__
  324. @property
  325. def min_cutoff(self):
  326. min_cutoff = 3.0
  327. return min_cutoff
  328. @inherit_docstring(Kernel._evaluate)
  329. def _evaluate(self, t):
  330. return (1.0 / (np.sqrt(2.0 * np.pi) * self._sigma_scaled)) * np.exp(
  331. -0.5 * (t / self._sigma_scaled).magnitude ** 2)
  332. @inherit_docstring(Kernel.boundary_enclosing_area_fraction)
  333. def boundary_enclosing_area_fraction(self, fraction):
  334. self._check_fraction(fraction)
  335. return self.sigma * np.sqrt(2.0) * scipy.special.erfinv(fraction)
  336. class LaplacianKernel(SymmetricKernel):
  337. """
  338. Class for laplacian kernels
  339. .. math::
  340. K(t) = \\frac{1}{2 \\tau} \\exp(-|\\frac{t}{\\tau}|)
  341. with :math:`\\tau = \\sigma / \\sqrt{2}`.
  342. Besides the standard deviation `sigma`, for consistency of interfaces the
  343. parameter `invert` needed for asymmetric kernels also exists without
  344. having any effect in the case of symmetric kernels.
  345. Derived from:
  346. """
  347. __doc__ += SymmetricKernel.__doc__
  348. @property
  349. def min_cutoff(self):
  350. min_cutoff = 3.0
  351. return min_cutoff
  352. @inherit_docstring(Kernel._evaluate)
  353. def _evaluate(self, t):
  354. return (1 / (np.sqrt(2.0) * self._sigma_scaled)) * np.exp(
  355. -(np.absolute(t) * np.sqrt(2.0) / self._sigma_scaled).magnitude)
  356. @inherit_docstring(Kernel.boundary_enclosing_area_fraction)
  357. def boundary_enclosing_area_fraction(self, fraction):
  358. self._check_fraction(fraction)
  359. return -self.sigma * np.log(1.0 - fraction) / np.sqrt(2.0)
  360. # Potential further symmetric kernels from Wiki Kernels (statistics):
  361. # Quartic (biweight), Triweight, Tricube, Cosine, Logistics, Silverman
  362. class ExponentialKernel(Kernel):
  363. """
  364. Class for exponential kernels
  365. .. math::
  366. K(t) = \\left\\{\\begin{array}{ll} (1 / \\tau) \\exp{(-t / \\tau)},
  367. & t > 0 \\\\
  368. 0, & t \\leq 0 \\end{array} \\right.
  369. with :math:`\\tau = \\sigma`.
  370. Derived from:
  371. """
  372. __doc__ += Kernel.__doc__
  373. @property
  374. def min_cutoff(self):
  375. min_cutoff = 3.0
  376. return min_cutoff
  377. @inherit_docstring(Kernel._evaluate)
  378. def _evaluate(self, t):
  379. if not self.invert:
  380. kernel = (t >= 0) * (1. / self._sigma_scaled.magnitude) *\
  381. np.exp((-t / self._sigma_scaled).magnitude) / t.units
  382. elif self.invert:
  383. kernel = (t <= 0) * (1. / self._sigma_scaled.magnitude) *\
  384. np.exp((t / self._sigma_scaled).magnitude) / t.units
  385. return kernel
  386. @inherit_docstring(Kernel.boundary_enclosing_area_fraction)
  387. def boundary_enclosing_area_fraction(self, fraction):
  388. self._check_fraction(fraction)
  389. return -self.sigma * np.log(1.0 - fraction)
  390. class AlphaKernel(Kernel):
  391. """
  392. Class for alpha kernels
  393. .. math::
  394. K(t) = \\left\\{\\begin{array}{ll} (1 / \\tau^2)
  395. \\ t\\ \\exp{(-t / \\tau)}, & t > 0 \\\\
  396. 0, & t \\leq 0 \\end{array} \\right.
  397. with :math:`\\tau = \\sigma / \\sqrt{2}`.
  398. For the alpha kernel an analytical expression for the boundary of the
  399. integral as a function of the area under the alpha kernel function
  400. cannot be given. Hence in this case the value of the boundary is
  401. determined by kernel-approximating numerical integration, inherited
  402. from the Kernel class.
  403. Derived from:
  404. """
  405. __doc__ += Kernel.__doc__
  406. @property
  407. def min_cutoff(self):
  408. min_cutoff = 3.0
  409. return min_cutoff
  410. @inherit_docstring(Kernel._evaluate)
  411. def _evaluate(self, t):
  412. if not self.invert:
  413. kernel = (t >= 0) * 2. * (t / self._sigma_scaled**2).magnitude *\
  414. np.exp((
  415. -t * np.sqrt(2.) / self._sigma_scaled).magnitude) / t.units
  416. elif self.invert:
  417. kernel = (t <= 0) * -2. * (t / self._sigma_scaled**2).magnitude *\
  418. np.exp((
  419. t * np.sqrt(2.) / self._sigma_scaled).magnitude) / t.units
  420. return kernel