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.
- {edmkit-0.0.14 → edmkit-0.0.16}/PKG-INFO +1 -1
- {edmkit-0.0.14 → edmkit-0.0.16}/pyproject.toml +3 -2
- {edmkit-0.0.14 → edmkit-0.0.16}/pyproject.toml.orig +3 -2
- edmkit-0.0.16/src/edmkit/generate/__init__.py +40 -0
- edmkit-0.0.16/src/edmkit/generate/chua.py +31 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/ctrnn.py +2 -2
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/double_pendulum.py +3 -3
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/duffing.py +4 -3
- edmkit-0.0.16/src/edmkit/generate/euler_spiral.py +24 -0
- edmkit-0.0.16/src/edmkit/generate/fitzhugh_nagumo.py +30 -0
- edmkit-0.0.16/src/edmkit/generate/hindmarsh_rose.py +38 -0
- edmkit-0.0.16/src/edmkit/generate/kuramoto.py +26 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/lorenz.py +4 -3
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/lorenz96.py +3 -3
- edmkit-0.0.16/src/edmkit/generate/lotka_volterra.py +27 -0
- edmkit-0.0.16/src/edmkit/generate/morris_lecar.py +70 -0
- edmkit-0.0.16/src/edmkit/generate/pyloric.py +105 -0
- edmkit-0.0.16/src/edmkit/generate/rk45.py +106 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/rossler.py +4 -3
- edmkit-0.0.16/src/edmkit/generate/rulkov_map.py +39 -0
- edmkit-0.0.16/src/edmkit/generate/three_scroll.py +34 -0
- edmkit-0.0.16/src/edmkit/simplex_projection/__init__.py +7 -0
- edmkit-0.0.16/src/edmkit/simplex_projection/linear_operator.py +206 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/simplex_projection/simplex_projection.py +18 -10
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/simplex_projection/soft.py +24 -15
- edmkit-0.0.16/src/edmkit/simplex_projection/soft_linear_operator.py +203 -0
- edmkit-0.0.16/src/edmkit/smap/__init__.py +5 -0
- edmkit-0.0.14/src/edmkit/smap.py → edmkit-0.0.16/src/edmkit/smap/coefficients.py +71 -171
- edmkit-0.0.16/src/edmkit/smap/linear_operator.py +234 -0
- edmkit-0.0.16/src/edmkit/smap/smap.py +193 -0
- edmkit-0.0.16/src/edmkit/wavelet/__init__.py +4 -0
- edmkit-0.0.16/src/edmkit/wavelet/continuous.py +276 -0
- edmkit-0.0.16/src/edmkit/wavelet/discrete.py +365 -0
- edmkit-0.0.14/src/edmkit/generate/__init__.py +0 -10
- edmkit-0.0.14/src/edmkit/generate/rk45.py +0 -85
- edmkit-0.0.14/src/edmkit/simplex_projection/__init__.py +0 -5
- {edmkit-0.0.14 → edmkit-0.0.16}/README.md +0 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/ccm.py +0 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/embedding.py +0 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/generate/mackey_glass.py +0 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/metrics/__init__.py +0 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/metrics/mae.py +0 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/metrics/pearson_correlation.py +0 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/metrics/rmse.py +0 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/simplex_projection/knn.py +0 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/splits.py +0 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/theiler_window.py +0 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/types.py +0 -0
- {edmkit-0.0.14 → edmkit-0.0.16}/src/edmkit/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "edmkit"
|
|
3
|
-
version = "0.0.
|
|
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.
|
|
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.
|
|
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.
|
|
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 *
|
|
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.
|
|
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,
|
|
29
|
-
|
|
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,
|
|
25
|
-
|
|
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,
|
|
25
|
-
|
|
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
|