edmkit 0.0.14__tar.gz → 0.0.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {edmkit-0.0.14 → edmkit-0.0.16}/PKG-INFO +1 -1
  2. {edmkit-0.0.14 → edmkit-0.0.16}/pyproject.toml +3 -2
  3. {edmkit-0.0.14 → edmkit-0.0.16}/pyproject.toml.orig +3 -2
  4. edmkit-0.0.16/src/edmkit/generate/__init__.py +40 -0
  5. edmkit-0.0.16/src/edmkit/generate/chua.py +31 -0
  6. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/ctrnn.py +2 -2
  7. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/double_pendulum.py +3 -3
  8. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/duffing.py +4 -3
  9. edmkit-0.0.16/src/edmkit/generate/euler_spiral.py +24 -0
  10. edmkit-0.0.16/src/edmkit/generate/fitzhugh_nagumo.py +30 -0
  11. edmkit-0.0.16/src/edmkit/generate/hindmarsh_rose.py +38 -0
  12. edmkit-0.0.16/src/edmkit/generate/kuramoto.py +26 -0
  13. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/lorenz.py +4 -3
  14. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/lorenz96.py +3 -3
  15. edmkit-0.0.16/src/edmkit/generate/lotka_volterra.py +27 -0
  16. edmkit-0.0.16/src/edmkit/generate/morris_lecar.py +70 -0
  17. edmkit-0.0.16/src/edmkit/generate/pyloric.py +105 -0
  18. edmkit-0.0.16/src/edmkit/generate/rk45.py +106 -0
  19. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/rossler.py +4 -3
  20. edmkit-0.0.16/src/edmkit/generate/rulkov_map.py +39 -0
  21. edmkit-0.0.16/src/edmkit/generate/three_scroll.py +34 -0
  22. edmkit-0.0.16/src/edmkit/simplex_projection/__init__.py +7 -0
  23. edmkit-0.0.16/src/edmkit/simplex_projection/linear_operator.py +206 -0
  24. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/simplex_projection/simplex_projection.py +18 -10
  25. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/simplex_projection/soft.py +24 -15
  26. edmkit-0.0.16/src/edmkit/simplex_projection/soft_linear_operator.py +203 -0
  27. edmkit-0.0.16/src/edmkit/smap/__init__.py +5 -0
  28. edmkit-0.0.14/src/edmkit/smap.py → edmkit-0.0.16/src/edmkit/smap/coefficients.py +71 -171
  29. edmkit-0.0.16/src/edmkit/smap/linear_operator.py +234 -0
  30. edmkit-0.0.16/src/edmkit/smap/smap.py +193 -0
  31. edmkit-0.0.16/src/edmkit/wavelet/__init__.py +4 -0
  32. edmkit-0.0.16/src/edmkit/wavelet/continuous.py +276 -0
  33. edmkit-0.0.16/src/edmkit/wavelet/discrete.py +365 -0
  34. edmkit-0.0.14/src/edmkit/generate/__init__.py +0 -10
  35. edmkit-0.0.14/src/edmkit/generate/rk45.py +0 -85
  36. edmkit-0.0.14/src/edmkit/simplex_projection/__init__.py +0 -5
  37. {edmkit-0.0.14 → edmkit-0.0.16}/README.md +0 -0
  38. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/ccm.py +0 -0
  39. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/embedding.py +0 -0
  40. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/mackey_glass.py +0 -0
  41. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/metrics/__init__.py +0 -0
  42. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/metrics/mae.py +0 -0
  43. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/metrics/pearson_correlation.py +0 -0
  44. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/metrics/rmse.py +0 -0
  45. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/simplex_projection/knn.py +0 -0
  46. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/splits.py +0 -0
  47. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/theiler_window.py +0 -0
  48. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/types.py +0 -0
  49. {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: edmkit
3
- Version: 0.0.14
3
+ Version: 0.0.16
4
4
  Summary: Simple EDM (Empirical Dynamic Modeling) library
5
5
  Author: FUJISHIGE TEMMA
6
6
  Author-email: FUJISHIGE TEMMA <tenma.x0@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "edmkit"
3
- version = "0.0.14"
3
+ version = "0.0.16"
4
4
  description = "Simple EDM (Empirical Dynamic Modeling) library"
5
5
  readme = "README.md"
6
6
  requires-python = ">= 3.13"
@@ -22,6 +22,7 @@ dev = [
22
22
  "hypothesis>=6.151.9",
23
23
  "matplotlib>=3.11.2",
24
24
  "pytest>=9.1.1",
25
+ "pywavelets>=1.10.0",
25
26
  "ruff>=0.15.20",
26
27
  "ty>=0.0.56",
27
28
  ]
@@ -42,5 +43,5 @@ markers = [
42
43
  ]
43
44
 
44
45
  [build-system]
45
- requires = ["uv_build>=0.10.9,<0.11.0"]
46
+ requires = ["uv_build>=0.13.6,<0.14.0"]
46
47
  build-backend = "uv_build"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "edmkit"
3
- version = "0.0.14"
3
+ version = "0.0.16"
4
4
  description = "Simple EDM (Empirical Dynamic Modeling) library"
5
5
  authors = [{ name = "FUJISHIGE TEMMA", email = "tenma.x0@gmail.com" }]
6
6
  readme = "README.md"
@@ -19,6 +19,7 @@ dev = [
19
19
  "hypothesis>=6.151.9",
20
20
  "matplotlib>=3.11.2",
21
21
  "pytest>=9.1.1",
22
+ "pywavelets>=1.10.0",
22
23
  "ruff>=0.15.20",
23
24
  "ty>=0.0.56",
24
25
  ]
@@ -38,7 +39,7 @@ markers = [
38
39
 
39
40
 
40
41
  [build-system]
41
- requires = ["uv_build>=0.10.9,<0.11.0"]
42
+ requires = ["uv_build>=0.13.6,<0.14.0"]
42
43
  build-backend = "uv_build"
43
44
 
44
45
  [tool.uv.build-backend]
@@ -0,0 +1,40 @@
1
+ from .chua import chua
2
+ from .ctrnn import ctrnn
3
+ from .double_pendulum import double_pendulum, to_xy
4
+ from .duffing import duffing
5
+ from .euler_spiral import euler_spiral
6
+ from .fitzhugh_nagumo import fitzhugh_nagumo
7
+ from .hindmarsh_rose import hindmarsh_rose
8
+ from .kuramoto import kuramoto
9
+ from .lorenz import lorenz
10
+ from .lorenz96 import lorenz96
11
+ from .lotka_volterra import lotka_volterra
12
+ from .mackey_glass import mackey_glass
13
+ from .morris_lecar import morris_lecar
14
+ from .pyloric import pyloric
15
+ from .rk45 import rk45
16
+ from .rossler import rossler
17
+ from .rulkov_map import rulkov_map
18
+ from .three_scroll import three_scroll
19
+
20
+ __all__ = [
21
+ "chua",
22
+ "ctrnn",
23
+ "double_pendulum",
24
+ "duffing",
25
+ "euler_spiral",
26
+ "fitzhugh_nagumo",
27
+ "hindmarsh_rose",
28
+ "kuramoto",
29
+ "lorenz",
30
+ "lorenz96",
31
+ "lotka_volterra",
32
+ "mackey_glass",
33
+ "morris_lecar",
34
+ "pyloric",
35
+ "rk45",
36
+ "rossler",
37
+ "rulkov_map",
38
+ "three_scroll",
39
+ "to_xy",
40
+ ]
@@ -0,0 +1,31 @@
1
+ import numpy as np
2
+
3
+ __all__ = ["chua"]
4
+
5
+
6
+ def chua(alpha: float, beta: float, m0: float, m1: float):
7
+ """Vector field of Chua's circuit, with the piecewise-linear diode ``h(x) = m1 x + (m0 - m1) (|x + 1| - |x - 1|) / 2``.
8
+
9
+ Parameters
10
+ ----------
11
+ alpha : float
12
+ Ratio of capacitances (typical: 15.6).
13
+ beta : float
14
+ Ratio of inductance to resistance (typical: 28).
15
+ m0 : float
16
+ Diode slope for ``|x| < 1`` (typical: -8/7).
17
+ m1 : float
18
+ Diode slope for ``|x| > 1`` (typical: -5/7).
19
+
20
+ Returns
21
+ -------
22
+ f : Callable[[float, np.ndarray], np.ndarray]
23
+ Right-hand side ``f(t, x)`` for a state ``(x, y, z)`` of shape ``(3,)``, or ``(B, 3)`` for a batch.
24
+ """
25
+
26
+ def f(t: float, s: np.ndarray):
27
+ x, y, z = np.moveaxis(s, -1, 0)
28
+ h = m1 * x + (m0 - m1) * np.clip(x, -1, 1) # (|x + 1| - |x - 1|) / 2 is clip(x, -1, 1)
29
+ return np.stack([alpha * (y - x - h), x - y + z, -beta * y], axis=-1)
30
+
31
+ return f
@@ -16,10 +16,10 @@ def ctrnn(g: float, J: np.ndarray):
16
16
  Returns
17
17
  -------
18
18
  f : Callable[[float, np.ndarray], np.ndarray]
19
- Right-hand side ``f(t, x)`` for a state of shape ``(N,)``.
19
+ Right-hand side ``f(t, x)`` for a state of shape ``(N,)``, or ``(B, N)`` for a batch.
20
20
  """
21
21
 
22
22
  def f(t: float, x: np.ndarray):
23
- return -x + g * J @ np.tanh(x)
23
+ return -x + g * np.tanh(x) @ J.T
24
24
 
25
25
  return f
@@ -22,11 +22,11 @@ def double_pendulum(m1: float, m2: float, L1: float, L2: float, g: float):
22
22
  Returns
23
23
  -------
24
24
  f : Callable[[float, np.ndarray], np.ndarray]
25
- Right-hand side ``f(t, x)`` for a state ``(theta1, theta2, omega1, omega2)`` of shape ``(4,)``.
25
+ Right-hand side ``f(t, x)`` for a state ``(theta1, theta2, omega1, omega2)`` of shape ``(4,)``, or ``(B, 4)`` for a batch.
26
26
  """
27
27
 
28
28
  def f(t: float, x: np.ndarray):
29
- theta1, theta2, omega1, omega2 = x
29
+ theta1, theta2, omega1, omega2 = np.moveaxis(x, -1, 0)
30
30
  delta = theta1 - theta2
31
31
 
32
32
  denom = 2 * m1 + m2 - m2 * np.cos(2 * delta)
@@ -44,7 +44,7 @@ def double_pendulum(m1: float, m2: float, L1: float, L2: float, g: float):
44
44
  L2 * denom
45
45
  )
46
46
 
47
- return np.array([dtheta1_dt, dtheta2_dt, domega1_dt, domega2_dt])
47
+ return np.stack([dtheta1_dt, dtheta2_dt, domega1_dt, domega2_dt], axis=-1)
48
48
 
49
49
  return f
50
50
 
@@ -22,10 +22,11 @@ def duffing(alpha: float, beta: float, delta: float, gamma: float, omega: float)
22
22
  Returns
23
23
  -------
24
24
  f : Callable[[float, np.ndarray], np.ndarray]
25
- Right-hand side ``f(t, x)`` for a state ``(x, v)`` of shape ``(2,)``.
25
+ Right-hand side ``f(t, x)`` for a state ``(x, v)`` of shape ``(2,)``, or ``(B, 2)`` for a batch.
26
26
  """
27
27
 
28
- def f(t: float, x: np.ndarray):
29
- return np.array([[0, 1], [-alpha - beta * x[0] ** 2, -delta]]) @ x + np.array([0, gamma * np.cos(omega * t)])
28
+ def f(t: float, s: np.ndarray):
29
+ x, v = np.moveaxis(s, -1, 0)
30
+ return np.stack([v, (-alpha - beta * x**2) * x - delta * v + gamma * np.cos(omega * t)], axis=-1)
30
31
 
31
32
  return f
@@ -0,0 +1,24 @@
1
+ import numpy as np
2
+
3
+ __all__ = ["euler_spiral"]
4
+
5
+
6
+ def euler_spiral(a: float):
7
+ """Vector field of the Euler spiral (clothoid).
8
+
9
+ Parameters
10
+ ----------
11
+ a : float
12
+ Rate at which the curvature grows (typical: 1).
13
+
14
+ Returns
15
+ -------
16
+ f : Callable[[float, np.ndarray], np.ndarray]
17
+ Right-hand side ``f(t, x)`` for a state ``(x, y, theta)`` of shape ``(3,)``, or ``(B, 3)`` for a batch.
18
+ """
19
+
20
+ def f(t: float, x: np.ndarray):
21
+ theta = x[..., 2]
22
+ return np.stack([np.cos(theta), np.sin(theta), np.full_like(theta, a * t)], axis=-1)
23
+
24
+ return f
@@ -0,0 +1,30 @@
1
+ import numpy as np
2
+
3
+ __all__ = ["fitzhugh_nagumo"]
4
+
5
+
6
+ def fitzhugh_nagumo(a: float, b: float, epsilon: float, I: float):
7
+ """Vector field of the FitzHugh-Nagumo neuron model.
8
+
9
+ Parameters
10
+ ----------
11
+ a : float
12
+ Offset of the recovery variable (typical: 0.7).
13
+ b : float
14
+ Damping of the recovery variable (typical: 0.8).
15
+ epsilon : float
16
+ Time scale of the recovery variable (typical: 0.08).
17
+ I : float
18
+ Applied current (typical: 0.5 for spiking).
19
+
20
+ Returns
21
+ -------
22
+ f : Callable[[float, np.ndarray], np.ndarray]
23
+ Right-hand side ``f(t, x)`` for a state ``(v, w)`` of shape ``(2,)``, or ``(B, 2)`` for a batch.
24
+ """
25
+
26
+ def f(t: float, x: np.ndarray):
27
+ v, w = np.moveaxis(x, -1, 0)
28
+ return np.stack([v - v**3 / 3 - w + I, epsilon * (v + a - b * w)], axis=-1)
29
+
30
+ return f
@@ -0,0 +1,38 @@
1
+ import numpy as np
2
+
3
+ __all__ = ["hindmarsh_rose"]
4
+
5
+
6
+ def hindmarsh_rose(a: float, b: float, c: float, d: float, r: float, s: float, x_R: float, I: float):
7
+ """Vector field of the Hindmarsh-Rose neuron model.
8
+
9
+ Parameters
10
+ ----------
11
+ a : float
12
+ Cubic coefficient of the membrane potential (typical: 1).
13
+ b : float
14
+ Quadratic coefficient of the membrane potential (typical: 3).
15
+ c : float
16
+ Constant drive of the recovery variable (typical: 1).
17
+ d : float
18
+ Quadratic coupling of the potential into the recovery variable (typical: 5).
19
+ r : float
20
+ Time scale of the slow adaptation current (typical: 0.006).
21
+ s : float
22
+ Adaptation strength (typical: 4).
23
+ x_R : float
24
+ Resting potential (typical: -1.6).
25
+ I : float
26
+ Applied current (typical: 3.25 for chaotic bursting).
27
+
28
+ Returns
29
+ -------
30
+ f : Callable[[float, np.ndarray], np.ndarray]
31
+ Right-hand side ``f(t, x)`` for a state ``(x, y, z)`` of shape ``(3,)``, or ``(B, 3)`` for a batch: membrane potential, recovery variable and adaptation current.
32
+ """
33
+
34
+ def f(t: float, x: np.ndarray):
35
+ v, w, u = np.moveaxis(x, -1, 0)
36
+ return np.stack([w - a * v**3 + b * v**2 - u + I, c - d * v**2 - w, r * (s * (v - x_R) - u)], axis=-1)
37
+
38
+ return f
@@ -0,0 +1,26 @@
1
+ import numpy as np
2
+
3
+ __all__ = ["kuramoto"]
4
+
5
+
6
+ def kuramoto(omega: np.ndarray, K: np.ndarray):
7
+ """Vector field of the Kuramoto model ``dθ_i/dt = ω_i + Σ_j K_ij sin(θ_j - θ_i)``.
8
+
9
+ Parameters
10
+ ----------
11
+ omega : np.ndarray
12
+ Natural frequencies of shape ``(N,)``.
13
+ K : np.ndarray
14
+ Coupling of shape ``(N, N)``; ``K[i, j]`` is the strength with which oscillator ``j`` drives oscillator ``i`` (typical: ``k / N`` everywhere).
15
+
16
+ Returns
17
+ -------
18
+ f : Callable[[float, np.ndarray], np.ndarray]
19
+ Right-hand side ``f(t, x)`` for unwrapped phases of shape ``(N,)``, or ``(B, N)`` for a batch.
20
+ """
21
+
22
+ def f(t: float, x: np.ndarray):
23
+ s, c = np.sin(x), np.cos(x)
24
+ return omega + c * (s @ K.T) - s * (c @ K.T)
25
+
26
+ return f
@@ -18,10 +18,11 @@ def lorenz(sigma: float, rho: float, beta: float):
18
18
  Returns
19
19
  -------
20
20
  f : Callable[[float, np.ndarray], np.ndarray]
21
- Right-hand side ``f(t, x)`` for a state ``(x, y, z)`` of shape ``(3,)``.
21
+ Right-hand side ``f(t, x)`` for a state ``(x, y, z)`` of shape ``(3,)``, or ``(B, 3)`` for a batch.
22
22
  """
23
23
 
24
- def f(t: float, x: np.ndarray):
25
- return np.array([[-sigma, sigma, 0], [rho, -1, -x[0]], [0, x[0], -beta]]) @ x
24
+ def f(t: float, s: np.ndarray):
25
+ x, y, z = np.moveaxis(s, -1, 0)
26
+ return np.stack([sigma * (y - x), rho * x - y - x * z, x * y - beta * z], axis=-1)
26
27
 
27
28
  return f
@@ -14,12 +14,12 @@ def lorenz96(F: float):
14
14
  Returns
15
15
  -------
16
16
  f : Callable[[float, np.ndarray], np.ndarray]
17
- Right-hand side ``f(t, x)`` for a state of shape ``(D,)`` with ``D >= 4``.
17
+ Right-hand side ``f(t, x)`` for a state of shape ``(D,)``, or ``(B, D)`` for a batch, with ``D >= 4``.
18
18
  """
19
19
 
20
20
  def f(t: float, x: np.ndarray):
21
21
  # p[i + 2] is x[i] on the ring, so this is (x[i + 1] - x[i - 2]) * x[i - 1] - x[i] + F
22
- p = np.concatenate([x[-2:], x, x[:1]])
23
- return (p[3:] - p[:-3]) * p[1:-2] - x + F
22
+ p = np.concatenate([x[..., -2:], x, x[..., :1]], axis=-1)
23
+ return (p[..., 3:] - p[..., :-3]) * p[..., 1:-2] - x + F
24
24
 
25
25
  return f
@@ -0,0 +1,27 @@
1
+ import numpy as np
2
+
3
+ __all__ = ["lotka_volterra"]
4
+
5
+
6
+ def lotka_volterra(rho: np.ndarray, sigma: float | np.ndarray, S: float | np.ndarray):
7
+ """Vector field of the generalized Lotka-Volterra system ``dx/dt = x (sigma - rho x) + S``.
8
+
9
+ Parameters
10
+ ----------
11
+ rho : np.ndarray
12
+ Interaction matrix of shape ``(N, N)``; ``rho[i, j]`` is the effect of species ``j`` on species ``i``.
13
+ sigma : float | np.ndarray
14
+ Growth rate, scalar or of shape ``(N,)`` (typical: 1).
15
+ S : float | np.ndarray
16
+ Constant input, scalar or of shape ``(N,)`` (typical: 0; small positive values keep winnerless competition switching).
17
+
18
+ Returns
19
+ -------
20
+ f : Callable[[float, np.ndarray], np.ndarray]
21
+ Right-hand side ``f(t, x)`` for a state of shape ``(N,)``, or ``(B, N)`` for a batch.
22
+ """
23
+
24
+ def f(t: float, x: np.ndarray):
25
+ return x * (sigma - x @ rho.T) + S
26
+
27
+ return f
@@ -0,0 +1,70 @@
1
+ import numpy as np
2
+
3
+ __all__ = ["morris_lecar"]
4
+
5
+
6
+ def morris_lecar(
7
+ C: float,
8
+ g_L: float,
9
+ g_Ca: float,
10
+ g_K: float,
11
+ V_L: float,
12
+ V_Ca: float,
13
+ V_K: float,
14
+ V1: float,
15
+ V2: float,
16
+ V3: float,
17
+ V4: float,
18
+ phi: float,
19
+ I: float,
20
+ ):
21
+ """Vector field of the Morris-Lecar neuron model.
22
+
23
+ Parameters
24
+ ----------
25
+ C : float
26
+ Membrane capacitance (typical: 20).
27
+ g_L : float
28
+ Leak conductance (typical: 2).
29
+ g_Ca : float
30
+ Calcium conductance (typical: 4.4).
31
+ g_K : float
32
+ Potassium conductance (typical: 8).
33
+ V_L : float
34
+ Leak reversal potential (typical: -60).
35
+ V_Ca : float
36
+ Calcium reversal potential (typical: 120).
37
+ V_K : float
38
+ Potassium reversal potential (typical: -84).
39
+ V1 : float
40
+ Half-activation voltage of the calcium channel (typical: -1.2).
41
+ V2 : float
42
+ Slope of the calcium activation (typical: 18).
43
+ V3 : float
44
+ Half-activation voltage of the potassium channel (typical: 2).
45
+ V4 : float
46
+ Slope of the potassium activation (typical: 30).
47
+ phi : float
48
+ Rate of the potassium activation (typical: 0.04).
49
+ I : float
50
+ Applied current (typical: 100 for spiking).
51
+
52
+ Returns
53
+ -------
54
+ f : Callable[[float, np.ndarray], np.ndarray]
55
+ Right-hand side ``f(t, x)`` for a state ``(V, n)`` of shape ``(2,)``, or ``(B, 2)`` for a batch: membrane potential and potassium activation.
56
+ """
57
+
58
+ def f(t: float, x: np.ndarray):
59
+ V, n = np.moveaxis(x, -1, 0)
60
+ m_inf = 0.5 * (1 + np.tanh((V - V1) / V2))
61
+ n_inf = 0.5 * (1 + np.tanh((V - V3) / V4))
62
+ return np.stack(
63
+ [
64
+ (I - g_L * (V - V_L) - g_Ca * m_inf * (V - V_Ca) - g_K * n * (V - V_K)) / C,
65
+ phi * (n_inf - n) * np.cosh((V - V3) / (2 * V4)),
66
+ ],
67
+ axis=-1,
68
+ )
69
+
70
+ return f
@@ -0,0 +1,105 @@
1
+ import numpy as np
2
+
3
+ __all__ = ["pyloric"]
4
+
5
+ AREA = 0.628e-3 # membrane area in cm^2 (specific capacitance 1 uF/cm^2, i.e. C = 0.628 nF)
6
+ RT_2F = 12.2 # RT / 2F in mV at 10 C, for the Nernst potential of Ca
7
+ E_REV = np.array([50.0, 0.0, 0.0, -80.0, -80.0, -80.0, -20.0, -50.0]) # Na, CaT, CaS, A, KCa, Kd, H, leak; Ca is replaced by Nernst
8
+ POWER = np.array([3, 1, 3, 1, 3, 1, 3, 1, 4, 4, 1])[:, None] # exponents of the 11 gates, see the state layout
9
+ E_SYN = np.array([-70.0, -80.0])[:, None] # reversal potentials of the glutamatergic and cholinergic synapses
10
+ K_SYN = np.array([1 / 40, 1 / 100])[:, None] # decay rates of the glutamatergic and cholinergic synapses
11
+
12
+
13
+ def pyloric(g_mem: np.ndarray, g_glu: np.ndarray, g_ach: np.ndarray):
14
+ """Vector field of the three-neuron pyloric circuit of the crustacean stomatogastric ganglion (Prinz, Bucher & Marder 2004), in ms and mV.
15
+
16
+ Parameters
17
+ ----------
18
+ g_mem : np.ndarray
19
+ Maximal conductances in mS/cm^2 of shape ``(3, 8)``: rows are AB/PD, LP, PY and columns are Na, CaT, CaS, A, KCa, Kd, H, leak
20
+ (typical: ``[[400, 2.5, 6, 50, 10, 100, 0.01, 0], [100, 0, 8, 40, 5, 75, 0.05, 0.02], [100, 2.5, 2, 50, 0, 125, 0.05, 0.01]]``).
21
+ g_glu : np.ndarray
22
+ Fast glutamatergic synapse strengths in nS of shape ``(3, 3)``; ``g_glu[i, j]`` is the synapse from neuron ``j`` onto neuron ``i``
23
+ (typical: ``g_glu[1, 0] = 30`` (AB to LP), ``g_glu[2, 0] = 3`` (AB to PY), ``g_glu[0, 1] = 10`` (LP to AB/PD), ``g_glu[2, 1] = 3`` (LP to PY),
24
+ ``g_glu[1, 2] = 3`` (PY to LP), the rest 0, for a PD, LP, PY rhythm of period about 1.7 s).
25
+ g_ach : np.ndarray
26
+ Slow cholinergic synapse strengths in nS of shape ``(3, 3)``, indexed like ``g_glu`` (typical: ``g_ach[1, 0] = 3`` (PD to LP), ``g_ach[2, 0] = 30`` (PD to PY), the rest 0).
27
+
28
+ Returns
29
+ -------
30
+ f : Callable[[float, np.ndarray], np.ndarray]
31
+ Right-hand side ``f(t, x)`` for a state of shape ``(45,)``, or ``(B, 45)`` for a batch; ``x.reshape(15, 3)`` has one column per neuron and rows: membrane potential,
32
+ intracellular [Ca] in uM, the gates m and h of Na, m and h of CaT, m and h of CaS, m and h of A, m of KCa, n of Kd, m of H, and the glutamatergic
33
+ and cholinergic synaptic activations of the neuron as presynaptic cell
34
+ (typical initial state: V = -50, [Ca] = 0.05, all gates and synaptic activations 0, discard the first 3 s as transient).
35
+ """
36
+ g_mem = np.asarray(g_mem, dtype=float)
37
+ g_syn = np.stack([g_glu, g_ach]) * 1e-6 / AREA # nS to uA/cm^2 per mV
38
+
39
+ def f(t: float, x: np.ndarray):
40
+ batch = x.shape[:-1]
41
+ V, Ca = x[..., :3], x[..., 3:6]
42
+ gate = x[..., 6:39].reshape(*batch, 11, 3)
43
+ s = x[..., 39:].reshape(*batch, 2, 3)
44
+
45
+ # steady states and time constants of the 11 gates
46
+ def sig(a, b):
47
+ return 1 / (1 + np.exp((V + a) / b))
48
+
49
+ def tau(a, b, c, d):
50
+ return a - b / (1 + np.exp((V + c) / -d))
51
+
52
+ inf = np.stack(
53
+ [
54
+ sig(25.5, -5.29),
55
+ sig(48.9, 5.18),
56
+ sig(27.1, -7.2),
57
+ sig(32.1, 5.5),
58
+ sig(33.0, -8.1),
59
+ sig(60.0, 6.2),
60
+ sig(27.2, -8.7),
61
+ sig(56.9, 4.9),
62
+ Ca / (Ca + 3) * sig(28.3, -12.6),
63
+ sig(12.3, -11.8),
64
+ sig(75.0, 5.5),
65
+ ],
66
+ axis=-2,
67
+ )
68
+ tau_g = np.stack(
69
+ [
70
+ tau(2.64, 2.52, 120, 25),
71
+ 1.34 / (1 + np.exp((V + 62.9) / -10)) * (1.5 + 1 / (1 + np.exp((V + 34.9) / 3.6))),
72
+ tau(43.4, 42.6, 68.1, 20.5),
73
+ tau(210, 179.6, 55, 16.9),
74
+ 2.8 + 14 / (np.exp((V + 27) / 10) + np.exp((V + 70) / -13)),
75
+ 120 + 300 / (np.exp((V + 55) / 9) + np.exp((V + 65) / -16)),
76
+ tau(23.2, 20.8, 32.9, 15.2),
77
+ tau(77.2, 58.4, 38.9, 26.5),
78
+ tau(180.6, 150.2, 46, 22.7),
79
+ tau(14.4, 12.8, 28.3, 19.2),
80
+ 2 / (np.exp((V + 169.7) / -11.6) + np.exp((V - 26.7) / 14.3)),
81
+ ],
82
+ axis=-2,
83
+ )
84
+
85
+ # membrane currents in uA/cm^2
86
+ m_Na, h_Na, m_CaT, h_CaT, m_CaS, h_CaS, m_A, h_A, m_KCa, n_Kd, m_H = np.moveaxis(gate**POWER, -2, 0)
87
+ activation = np.stack([m_Na * h_Na, m_CaT * h_CaT, m_CaS * h_CaS, m_A * h_A, m_KCa, n_Kd, m_H, np.ones_like(V)], axis=-2)
88
+ drive = V[..., None, :] - E_REV[:, None]
89
+ drive[..., 1:3, :] = (V - RT_2F * np.log(3000 / Ca))[..., None, :]
90
+ I_ion = g_mem.T * activation * drive
91
+ # sum over presynaptic cells first: g_s[p, i] = sum_j g_syn[p, i, j] s[p, j]
92
+ g_s = (g_syn @ s[..., None])[..., 0]
93
+ I_syn = (g_s * (V[..., None, :] - E_SYN)).sum(axis=-2)
94
+
95
+ # I_ion[1:3] in uA/cm^2 times area is uA, times 1e3 is nA, times f = 14.96 uM/nA gives the Ca influx
96
+ dCa = (0.05 - Ca - 14.96 * 1e3 * AREA * I_ion[..., 1:3, :].sum(axis=-2)) / 200
97
+ s_inf = sig(35.0, -5.0)[..., None, :]
98
+ # tau_s = (1 - s_inf) / k, floored at 1e-4 / k to keep the spikes from stiffening the system
99
+ ds = (s_inf - s) * K_SYN / np.maximum(1 - s_inf, 1e-4)
100
+
101
+ dV = -(I_ion.sum(axis=-2) + I_syn)
102
+ dgate = (inf - gate) / tau_g
103
+ return np.concatenate([dV, dCa, dgate.reshape(*batch, -1), ds.reshape(*batch, -1)], axis=-1)
104
+
105
+ return f
@@ -0,0 +1,106 @@
1
+ from collections.abc import Callable
2
+
3
+ import numpy as np
4
+
5
+ __all__ = ["rk45"]
6
+
7
+ # Dormand-Prince 5(4)
8
+ A = np.array(
9
+ [
10
+ [0, 0, 0, 0, 0, 0],
11
+ [1 / 5, 0, 0, 0, 0, 0],
12
+ [3 / 40, 9 / 40, 0, 0, 0, 0],
13
+ [44 / 45, -56 / 15, 32 / 9, 0, 0, 0],
14
+ [19372 / 6561, -25360 / 2187, 64448 / 6561, -212 / 729, 0, 0],
15
+ [9017 / 3168, -355 / 33, 46732 / 5247, 49 / 176, -5103 / 18656, 0],
16
+ [35 / 384, 0, 500 / 1113, 125 / 192, -2187 / 6784, 11 / 84],
17
+ ]
18
+ )
19
+ C = A.sum(axis=1)
20
+ E = np.array([71 / 57600, 0, -71 / 16695, 71 / 1920, -17253 / 339200, 22 / 525, -1 / 40])
21
+ # quartic dense output (Shampine 1986): x(s + theta h) = x + h sum_j K_j sum_k P[j, k] theta^(k + 1)
22
+ P = np.array(
23
+ [
24
+ [1, -8048581381 / 2820520608, 8663915743 / 2820520608, -12715105075 / 11282082432],
25
+ [0, 0, 0, 0],
26
+ [0, 131558114200 / 32700410799, -68118460800 / 10900136933, 87487479700 / 32700410799],
27
+ [0, -1754552775 / 470086768, 14199869525 / 1410260304, -10690763975 / 1880347072],
28
+ [0, 127303824393 / 49829197408, -318862633887 / 49829197408, 701980252875 / 199316789632],
29
+ [0, -282668133 / 205662961, 2019193451 / 616988883, -1453857185 / 822651844],
30
+ [0, 40617522 / 29380423, -110615467 / 29380423, 69997945 / 29380423],
31
+ ]
32
+ )
33
+ THETA_POWERS = np.arange(1, 5)
34
+
35
+
36
+ def rk45(
37
+ f: Callable[[float, np.ndarray], np.ndarray],
38
+ X0: np.ndarray,
39
+ dt: float,
40
+ t_max: float,
41
+ rtol: float = 1e-6,
42
+ atol: float = 1e-9,
43
+ ):
44
+ """Integrate ``dx/dt = f(t, x)`` via the adaptive Dormand-Prince RK45 method.
45
+
46
+ Steps are chosen by the error alone, and the uniform grid of spacing ``dt`` is filled in by the method's dense output.
47
+ A batch of initial conditions is integrated together, with steps that keep every member within the tolerance.
48
+
49
+ Parameters
50
+ ----------
51
+ f : Callable[[float, np.ndarray], np.ndarray]
52
+ Right-hand side of the system, called with states of the shape of ``X0``.
53
+ X0 : np.ndarray
54
+ Initial condition of shape ``(D,)``, or ``(B, D)`` for a batch.
55
+ dt : float
56
+ Sampling time step.
57
+ t_max : float
58
+ End time, excluded from the grid.
59
+ rtol : float
60
+ Relative tolerance of the local error.
61
+ atol : float
62
+ Absolute tolerance of the local error.
63
+
64
+ Returns
65
+ -------
66
+ t : np.ndarray
67
+ Time array of shape ``(N,)``.
68
+ X : np.ndarray
69
+ Trajectory of shape ``(N, D)``, or ``(B, N, D)`` for a batch.
70
+
71
+ Raises
72
+ ------
73
+ ValueError
74
+ If ``X0`` is not of shape ``(D,)`` or ``(B, D)``.
75
+ """
76
+ X0 = np.asarray(X0, dtype=float)
77
+ if X0.ndim not in (1, 2):
78
+ raise ValueError(f"X0 must be of shape (D,) or (B, D), got X0.shape={X0.shape}")
79
+
80
+ t = np.arange(0, t_max, dt)
81
+ X = np.empty((len(t), *X0.shape))
82
+ X[0] = X0
83
+
84
+ K = np.empty((7, *X0.shape))
85
+ Kf = K.reshape(7, -1) # flat view of K, so the stage sums are plain matrix products
86
+ K[0] = f(0.0, X0)
87
+ s, x, h, i = 0.0, X0, dt, 1
88
+
89
+ while i < len(t):
90
+ step = min(h, t[-1] - s)
91
+ for j in range(1, 7):
92
+ # the last stage is evaluated at x_new, so an accepted step hands K[6] on as the next K[0]
93
+ x_new = x + step * (A[j, :j] @ Kf[:j]).reshape(x.shape)
94
+ K[j] = f(s + C[j] * step, x_new)
95
+ error = np.max(np.abs(step * (E @ Kf).reshape(x.shape)) / (atol + rtol * np.maximum(np.abs(x), np.abs(x_new))))
96
+
97
+ if error <= 1:
98
+ n = np.searchsorted(t, s + step, side="right")
99
+ if n > i:
100
+ theta = (t[i:n] - s) / step
101
+ X[i:n] = x + step * (theta[:, None] ** THETA_POWERS @ (P.T @ Kf)).reshape(n - i, *x.shape)
102
+ s, x, i = s + step, x_new, n
103
+ K[0] = K[6]
104
+ h = step * (10 if error == 0 else min(10, max(0.2, 0.9 * error**-0.2)))
105
+
106
+ return t, np.moveaxis(X, 0, -2)
@@ -18,10 +18,11 @@ def rossler(a: float, b: float, c: float):
18
18
  Returns
19
19
  -------
20
20
  f : Callable[[float, np.ndarray], np.ndarray]
21
- Right-hand side ``f(t, x)`` for a state ``(x, y, z)`` of shape ``(3,)``.
21
+ Right-hand side ``f(t, x)`` for a state ``(x, y, z)`` of shape ``(3,)``, or ``(B, 3)`` for a batch.
22
22
  """
23
23
 
24
- def f(t: float, x: np.ndarray):
25
- return np.array([[0, -1, -1], [1, a, 0], [0, 0, x[0] - c]]) @ x + np.array([0, 0, b])
24
+ def f(t: float, s: np.ndarray):
25
+ x, y, z = np.moveaxis(s, -1, 0)
26
+ return np.stack([-y - z, x + a * y, b + (x - c) * z], axis=-1)
26
27
 
27
28
  return f