edmkit 0.0.15__tar.gz → 0.0.17__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 (52) hide show
  1. edmkit-0.0.17/PKG-INFO +77 -0
  2. edmkit-0.0.17/README.md +63 -0
  3. {edmkit-0.0.15 → edmkit-0.0.17}/pyproject.toml +23 -10
  4. {edmkit-0.0.15 → edmkit-0.0.17}/pyproject.toml.orig +20 -10
  5. edmkit-0.0.17/src/edmkit/ccm.py +168 -0
  6. edmkit-0.0.17/src/edmkit/generate/__init__.py +40 -0
  7. edmkit-0.0.17/src/edmkit/generate/chua.py +31 -0
  8. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/generate/ctrnn.py +2 -2
  9. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/generate/double_pendulum.py +3 -3
  10. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/generate/duffing.py +4 -3
  11. edmkit-0.0.17/src/edmkit/generate/euler_spiral.py +24 -0
  12. edmkit-0.0.17/src/edmkit/generate/fitzhugh_nagumo.py +30 -0
  13. edmkit-0.0.17/src/edmkit/generate/hindmarsh_rose.py +38 -0
  14. edmkit-0.0.17/src/edmkit/generate/kuramoto.py +26 -0
  15. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/generate/lorenz.py +4 -3
  16. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/generate/lorenz96.py +3 -3
  17. edmkit-0.0.17/src/edmkit/generate/lotka_volterra.py +27 -0
  18. edmkit-0.0.17/src/edmkit/generate/morris_lecar.py +70 -0
  19. edmkit-0.0.17/src/edmkit/generate/pyloric.py +105 -0
  20. edmkit-0.0.17/src/edmkit/generate/rk45.py +106 -0
  21. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/generate/rossler.py +4 -3
  22. edmkit-0.0.17/src/edmkit/generate/rulkov_map.py +39 -0
  23. edmkit-0.0.17/src/edmkit/generate/three_scroll.py +34 -0
  24. edmkit-0.0.17/src/edmkit/simplex_projection/knn.py +69 -0
  25. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/simplex_projection/linear_operator.py +20 -31
  26. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/simplex_projection/simplex_projection.py +19 -32
  27. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/simplex_projection/soft.py +29 -28
  28. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/simplex_projection/soft_linear_operator.py +25 -24
  29. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/smap/coefficients.py +35 -27
  30. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/smap/linear_operator.py +36 -27
  31. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/smap/smap.py +8 -7
  32. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/theiler_window.py +15 -8
  33. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/types.py +10 -3
  34. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/util.py +69 -2
  35. edmkit-0.0.17/src/edmkit/wavelet/__init__.py +4 -0
  36. edmkit-0.0.17/src/edmkit/wavelet/continuous.py +276 -0
  37. edmkit-0.0.17/src/edmkit/wavelet/discrete.py +365 -0
  38. edmkit-0.0.15/PKG-INFO +0 -48
  39. edmkit-0.0.15/README.md +0 -34
  40. edmkit-0.0.15/src/edmkit/ccm.py +0 -274
  41. edmkit-0.0.15/src/edmkit/generate/__init__.py +0 -10
  42. edmkit-0.0.15/src/edmkit/generate/rk45.py +0 -85
  43. edmkit-0.0.15/src/edmkit/simplex_projection/knn.py +0 -33
  44. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/embedding.py +0 -0
  45. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/generate/mackey_glass.py +0 -0
  46. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/metrics/__init__.py +0 -0
  47. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/metrics/mae.py +0 -0
  48. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/metrics/pearson_correlation.py +0 -0
  49. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/metrics/rmse.py +0 -0
  50. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/simplex_projection/__init__.py +0 -0
  51. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/smap/__init__.py +0 -0
  52. {edmkit-0.0.15 → edmkit-0.0.17}/src/edmkit/splits.py +0 -0
edmkit-0.0.17/PKG-INFO ADDED
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.3
2
+ Name: edmkit
3
+ Version: 0.0.17
4
+ Summary: Simple EDM (Empirical Dynamic Modeling) library
5
+ Author: FUJISHIGE TEMMA
6
+ Author-email: FUJISHIGE TEMMA <tenma.x0@gmail.com>
7
+ Requires-Dist: kdtree-rs>=0.3.1
8
+ Requires-Dist: numpy>=2.5.4
9
+ Requires-Dist: scipy>=1.18.1
10
+ Requires-Dist: tinygrad>=0.14.0
11
+ Requires-Dist: tinysolve>=0.3.0
12
+ Requires-Python: >=3.13
13
+ Description-Content-Type: text/markdown
14
+
15
+ # edmkit
16
+
17
+ This library is a collection of tools and utilities that are useful for Empirical Data Modeling (EDM) and related tasks. The library is designed to be fast and lightweight, and easy to use.
18
+
19
+ [benchmarks](./benchmarks)
20
+
21
+ ![](./benchmarks/benchmarks.png)
22
+
23
+ [compare.py](./benchmarks/compare.py)
24
+
25
+ ![](./benchmarks/compare.png)
26
+
27
+ | simplex N | edmkit [ms] | pyEDM [ms] | speedup |
28
+ |---:|---:|---:|---:|
29
+ | 500 | 0.2 | 1.4 | 8x |
30
+ | 1,000 | 0.4 | 2.3 | 5x |
31
+ | 2,000 | 0.9 | 4.2 | 5x |
32
+ | 5,000 | 2.3 | 10.7 | 5x |
33
+ | 10,000 | 4.9 | 22.8 | 5x |
34
+ | 20,000 | 10.3 | 46.8 | 5x |
35
+
36
+ | smap N | edmkit [ms] | pyEDM [ms] | speedup |
37
+ |---:|---:|---:|---:|
38
+ | 500 | 1.9 | 38.0 | 20x |
39
+ | 1,000 | 7.5 | 132.1 | 18x |
40
+ | 2,000 | 21.6 | 522.6 | 24x |
41
+ | 5,000 | 183.7 | 3,364 | 18x |
42
+
43
+ | ccm N | edmkit [ms] | pyEDM [ms] | speedup | edmkit parallel [ms] | pyEDM parallel [ms] | speedup |
44
+ |---:|---:|---:|---:|---:|---:|---:|
45
+ | 500 | 116.7 | 148.4 | 1.3x | 22.0 | 64.5 | 2.9x |
46
+ | 1,000 | 197.1 | 316.8 | 1.6x | 31.8 | 89.7 | 2.8x |
47
+ | 2,000 | 382.8 | 696.8 | 1.8x | 58.6 | 133.8 | 2.3x |
48
+ | 5,000 | 788.0 | 1,863 | 2.4x | 108.9 | 283.5 | 2.6x |
49
+ | 10,000 | 1,470 | 4,166 | 2.8x | 197.0 | 558.9 | 2.8x |
50
+
51
+
52
+ ## Installation
53
+
54
+ To install the library, you can use pip:
55
+
56
+ ```bash
57
+ pip install edmkit
58
+ ```
59
+
60
+ Or you can also use [uv](https://docs.astral.sh/uv/):
61
+
62
+ ```bash
63
+ uv add edmkit
64
+ ```
65
+
66
+ ## Testing
67
+
68
+ ```bash
69
+ # Run all tests
70
+ uv run pytest
71
+
72
+ # Skip slow tests
73
+ uv run pytest -m "not slow"
74
+
75
+ # Run with CI profile (more hypothesis examples)
76
+ HYPOTHESIS_PROFILE=ci uv run pytest
77
+ ```
@@ -0,0 +1,63 @@
1
+ # edmkit
2
+
3
+ This library is a collection of tools and utilities that are useful for Empirical Data Modeling (EDM) and related tasks. The library is designed to be fast and lightweight, and easy to use.
4
+
5
+ [benchmarks](./benchmarks)
6
+
7
+ ![](./benchmarks/benchmarks.png)
8
+
9
+ [compare.py](./benchmarks/compare.py)
10
+
11
+ ![](./benchmarks/compare.png)
12
+
13
+ | simplex N | edmkit [ms] | pyEDM [ms] | speedup |
14
+ |---:|---:|---:|---:|
15
+ | 500 | 0.2 | 1.4 | 8x |
16
+ | 1,000 | 0.4 | 2.3 | 5x |
17
+ | 2,000 | 0.9 | 4.2 | 5x |
18
+ | 5,000 | 2.3 | 10.7 | 5x |
19
+ | 10,000 | 4.9 | 22.8 | 5x |
20
+ | 20,000 | 10.3 | 46.8 | 5x |
21
+
22
+ | smap N | edmkit [ms] | pyEDM [ms] | speedup |
23
+ |---:|---:|---:|---:|
24
+ | 500 | 1.9 | 38.0 | 20x |
25
+ | 1,000 | 7.5 | 132.1 | 18x |
26
+ | 2,000 | 21.6 | 522.6 | 24x |
27
+ | 5,000 | 183.7 | 3,364 | 18x |
28
+
29
+ | ccm N | edmkit [ms] | pyEDM [ms] | speedup | edmkit parallel [ms] | pyEDM parallel [ms] | speedup |
30
+ |---:|---:|---:|---:|---:|---:|---:|
31
+ | 500 | 116.7 | 148.4 | 1.3x | 22.0 | 64.5 | 2.9x |
32
+ | 1,000 | 197.1 | 316.8 | 1.6x | 31.8 | 89.7 | 2.8x |
33
+ | 2,000 | 382.8 | 696.8 | 1.8x | 58.6 | 133.8 | 2.3x |
34
+ | 5,000 | 788.0 | 1,863 | 2.4x | 108.9 | 283.5 | 2.6x |
35
+ | 10,000 | 1,470 | 4,166 | 2.8x | 197.0 | 558.9 | 2.8x |
36
+
37
+
38
+ ## Installation
39
+
40
+ To install the library, you can use pip:
41
+
42
+ ```bash
43
+ pip install edmkit
44
+ ```
45
+
46
+ Or you can also use [uv](https://docs.astral.sh/uv/):
47
+
48
+ ```bash
49
+ uv add edmkit
50
+ ```
51
+
52
+ ## Testing
53
+
54
+ ```bash
55
+ # Run all tests
56
+ uv run pytest
57
+
58
+ # Skip slow tests
59
+ uv run pytest -m "not slow"
60
+
61
+ # Run with CI profile (more hypothesis examples)
62
+ HYPOTHESIS_PROFILE=ci uv run pytest
63
+ ```
@@ -1,15 +1,15 @@
1
1
  [project]
2
2
  name = "edmkit"
3
- version = "0.0.15"
3
+ version = "0.0.17"
4
4
  description = "Simple EDM (Empirical Dynamic Modeling) library"
5
5
  readme = "README.md"
6
6
  requires-python = ">= 3.13"
7
7
  dependencies = [
8
8
  "kdtree-rs>=0.3.1",
9
- "numpy>=2.5.1",
10
- "scipy>=1.17.1",
9
+ "numpy>=2.5.4",
10
+ "scipy>=1.18.1",
11
11
  "tinygrad>=0.14.0",
12
- "tinysolve>=0.2.1",
12
+ "tinysolve>=0.3.0",
13
13
  ]
14
14
 
15
15
  [[project.authors]]
@@ -17,13 +17,19 @@ name = "FUJISHIGE TEMMA"
17
17
  email = "tenma.x0@gmail.com"
18
18
 
19
19
  [dependency-groups]
20
+ benchmark = [
21
+ "pandas>=3.1.0",
22
+ "pyedm>=2.5.7",
23
+ ]
20
24
  dev = [
21
- "griffe2md>=1.5.0",
22
- "hypothesis>=6.151.9",
25
+ "griffe2md>=1.5.1",
26
+ "hypothesis>=6.170.14",
23
27
  "matplotlib>=3.11.2",
24
- "pytest>=9.1.1",
25
- "ruff>=0.15.20",
26
- "ty>=0.0.56",
28
+ "pytest>=9.2.0",
29
+ "pytest-benchmark>=5.3.1",
30
+ "pywavelets>=1.10.0",
31
+ "ruff>=0.17.3",
32
+ "ty>=0.0.95",
27
33
  ]
28
34
 
29
35
  [tool.uv]
@@ -35,12 +41,19 @@ namespace = true
35
41
 
36
42
  [tool.pytest.ini_options]
37
43
  addopts = "-v --tb=short"
44
+ testpaths = ["tests"]
45
+ python_files = [
46
+ "test_*.py",
47
+ "bench_*.py",
48
+ ]
38
49
  filterwarnings = ["error"]
39
50
  markers = [
40
51
  """slow: marks tests as slow (deselect with '-m "not slow"')""",
41
52
  """gpu: marks tests requiring tinygrad tensor backend (deselect with '-m "not gpu"')""",
53
+ "dense: marks benchmarks that build an (M, N) matrix",
54
+ """huge: marks benchmarks whose N is too large for an (M, N) matrix in memory (deselect with '-m "not (dense and huge)"')""",
42
55
  ]
43
56
 
44
57
  [build-system]
45
- requires = ["uv_build>=0.10.9,<0.11.0"]
58
+ requires = ["uv_build>=0.13.6,<0.14.0"]
46
59
  build-backend = "uv_build"
@@ -1,26 +1,32 @@
1
1
  [project]
2
2
  name = "edmkit"
3
- version = "0.0.15"
3
+ version = "0.0.17"
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"
7
7
  requires-python = ">= 3.13"
8
8
  dependencies = [
9
9
  "kdtree-rs>=0.3.1",
10
- "numpy>=2.5.1",
11
- "scipy>=1.17.1",
10
+ "numpy>=2.5.4",
11
+ "scipy>=1.18.1",
12
12
  "tinygrad>=0.14.0",
13
- "tinysolve>=0.2.1",
13
+ "tinysolve>=0.3.0",
14
14
  ]
15
15
 
16
16
  [dependency-groups]
17
+ benchmark = [
18
+ "pandas>=3.1.0",
19
+ "pyedm>=2.5.7",
20
+ ]
17
21
  dev = [
18
- "griffe2md>=1.5.0",
19
- "hypothesis>=6.151.9",
22
+ "griffe2md>=1.5.1",
23
+ "hypothesis>=6.170.14",
20
24
  "matplotlib>=3.11.2",
21
- "pytest>=9.1.1",
22
- "ruff>=0.15.20",
23
- "ty>=0.0.56",
25
+ "pytest>=9.2.0",
26
+ "pytest-benchmark>=5.3.1",
27
+ "pywavelets>=1.10.0",
28
+ "ruff>=0.17.3",
29
+ "ty>=0.0.95",
24
30
  ]
25
31
 
26
32
  [tool.uv]
@@ -30,15 +36,19 @@ required-environments = [
30
36
 
31
37
  [tool.pytest.ini_options]
32
38
  addopts = "-v --tb=short"
39
+ testpaths = ["tests"]
40
+ python_files = ["test_*.py", "bench_*.py"]
33
41
  filterwarnings = ["error"]
34
42
  markers = [
35
43
  "slow: marks tests as slow (deselect with '-m \"not slow\"')",
36
44
  "gpu: marks tests requiring tinygrad tensor backend (deselect with '-m \"not gpu\"')",
45
+ "dense: marks benchmarks that build an (M, N) matrix",
46
+ "huge: marks benchmarks whose N is too large for an (M, N) matrix in memory (deselect with '-m \"not (dense and huge)\"')",
37
47
  ]
38
48
 
39
49
 
40
50
  [build-system]
41
- requires = ["uv_build>=0.10.9,<0.11.0"]
51
+ requires = ["uv_build>=0.13.6,<0.14.0"]
42
52
  build-backend = "uv_build"
43
53
 
44
54
  [tool.uv.build-backend]
@@ -0,0 +1,168 @@
1
+ from collections.abc import Callable, Iterator
2
+ from functools import partial
3
+
4
+ import numpy as np
5
+
6
+ from edmkit.types import Mask, PredictFunc
7
+
8
+ __all__ = ["bootstrap"]
9
+
10
+
11
+ def bootstrap(
12
+ X: np.ndarray,
13
+ Y: np.ndarray,
14
+ Q: np.ndarray,
15
+ lib_sizes: np.ndarray,
16
+ *,
17
+ predict_func: PredictFunc,
18
+ n_samples: int = 20,
19
+ replace: bool = True,
20
+ seed: int | None = 42,
21
+ mask: Mask | None = None,
22
+ batch_size: int | None = 20,
23
+ ) -> Iterator[tuple[tuple[int, slice], Callable[[], np.ndarray]]]:
24
+ """
25
+ Iterate `predict_func(X[library], Y[library], Q)` for `n_samples` times for each library size in `lib_sizes`.
26
+
27
+ By gathering the results of the returned tasks, you can get `(len(lib_sizes), n_samples, M, E')` array of predictions.
28
+
29
+ Parameters
30
+ ----------
31
+ X : np.ndarray
32
+ The input data of shape (N,) or (N, E)
33
+ Y : np.ndarray
34
+ The target data of shape (N,) or (N, E'), sampled together with `X`.
35
+ Q : np.ndarray
36
+ The query points of shape (M,) or (M, E), shared by all sampled libraries as a read-only view.
37
+ lib_sizes : np.ndarray
38
+ The 1D array of library sizes, yielded in the given order. Sizes may exceed N only if `replace` is True.
39
+ predict_func : PredictFunc
40
+ The batched prediction function, such as `simplex_projection` or ``functools.partial(smap, theta=...)``.
41
+ n_samples : int, default 20
42
+ The number of libraries to sample for each size in `lib_sizes`.
43
+ replace : bool, default True
44
+ Whether to sample the library points with replacement.
45
+ seed : int or None, default 42
46
+ The seed of the random generator. If None, uses fresh entropy.
47
+ mask : Mask or None
48
+ ``mask(i, j)`` indicating, for each query in `Q`, which points of `X` to include. Use `theiler_window` for temporal exclusion.
49
+ batch_size : int or None, default 20
50
+ The maximum number of libraries per task. If None, uses `n_samples`.
51
+
52
+ Yields
53
+ ------
54
+ index : tuple[int, slice]
55
+ The position of the task's libraries in an array of shape (len(`lib_sizes`), `n_samples`), such as the scores.
56
+ samples : Callable[[], np.ndarray]
57
+ The task that returns the predictions on B sampled libraries, of shape (B, M, E'), where B <= `batch_size` and E' = 1 if `Y` is 1D.
58
+
59
+ Raises
60
+ ------
61
+ ValueError
62
+ - If `X`, `Y`, or `Q` is empty or not 1D or 2D.
63
+ - If the input arrays `X` and `Y` do not have the same number of points.
64
+ - If the dimension of `X` and `Q` do not match.
65
+ - If `lib_sizes` is not a nonempty 1D array of positive integers.
66
+ - If some size in `lib_sizes` exceeds N when `replace` is False.
67
+ - If `n_samples` or `batch_size` is not positive.
68
+ - If `predict_func` raises when a task is executed.
69
+
70
+ Examples
71
+ --------
72
+ ```python
73
+ from concurrent.futures import ThreadPoolExecutor
74
+
75
+ import numpy as np
76
+
77
+ from edmkit.ccm import bootstrap
78
+ from edmkit.embedding import embed
79
+ from edmkit.metrics import pearson_correlation
80
+ from edmkit.simplex_projection import simplex_projection
81
+
82
+ # Generate a simple time series (logistic map)
83
+ N = 300
84
+ x = np.zeros(N)
85
+ x[0] = 0.4
86
+ for i in range(1, N):
87
+ x[i] = 3.9 * x[i - 1] * (1 - x[i - 1])
88
+
89
+ tau = 2
90
+ E = 3
91
+
92
+ coordinates = np.array([[0, tau * j] for j in range(E)])
93
+ embedding, _ = embed(coordinates, x)
94
+ shift = tau * (E - 1)
95
+
96
+ pool_size = 200
97
+ Tp = 1
98
+ X = embedding[:pool_size - shift]
99
+ Y = x[shift + Tp : pool_size + Tp]
100
+ Q = embedding[pool_size - shift : -Tp]
101
+ actual = x[pool_size + Tp :, None]
102
+
103
+ lib_sizes = np.array([10, 50, 190])
104
+ n_samples = 100
105
+
106
+
107
+ def score(samples):
108
+ predictions = samples() # (B, M, 1)
109
+ return pearson_correlation(predictions, np.broadcast_to(actual, predictions.shape))
110
+
111
+
112
+ scores = np.empty((len(lib_sizes), n_samples))
113
+ tasks = bootstrap(X, Y, Q, lib_sizes, predict_func=simplex_projection, n_samples=n_samples)
114
+ indices, samples = zip(*tasks)
115
+ with ThreadPoolExecutor() as executor:
116
+ for index, result in zip(indices, executor.map(score, samples)):
117
+ scores[index] = result
118
+
119
+ for lib_size, correlation in zip(lib_sizes, scores.mean(axis=1)):
120
+ print(f"Correlation (L={lib_size}): {correlation:.3f}")
121
+ ```
122
+ """
123
+ if X.ndim == 1:
124
+ X = X[:, None]
125
+ if Y.ndim == 1:
126
+ Y = Y[:, None]
127
+ if Q.ndim == 1:
128
+ Q = Q[:, None]
129
+
130
+ if X.ndim != 2 or Y.ndim != 2 or Q.ndim != 2:
131
+ raise ValueError(f"X, Y, and Q must be 1D or 2D arrays, got X.ndim={X.ndim}, Y.ndim={Y.ndim}, Q.ndim={Q.ndim}")
132
+ if X.size == 0 or Y.size == 0 or Q.size == 0:
133
+ raise ValueError("X, Y, and Q must be nonempty")
134
+ if X.shape[0] != Y.shape[0]:
135
+ raise ValueError(f"X and Y must have the same length, got X.shape={X.shape}, Y.shape={Y.shape}")
136
+ if X.shape[1] != Q.shape[1]:
137
+ raise ValueError(f"X and Q must have the same dimension, got X.shape={X.shape}, Q.shape={Q.shape}")
138
+ if lib_sizes.ndim != 1 or lib_sizes.size == 0 or not np.issubdtype(lib_sizes.dtype, np.integer) or np.any(lib_sizes <= 0):
139
+ raise ValueError("lib_sizes must be a nonempty 1D integer array of positive sizes")
140
+ if not replace and np.any(lib_sizes > len(X)):
141
+ raise ValueError(f"lib_sizes cannot exceed N={len(X)} when replace is False")
142
+ batch_size = n_samples if batch_size is None else batch_size
143
+ if n_samples <= 0 or batch_size <= 0:
144
+ raise ValueError(f"n_samples and batch_size must be positive, got n_samples={n_samples}, batch_size={batch_size}")
145
+
146
+ rng = np.random.default_rng(seed)
147
+ for i, lib_size in enumerate(lib_sizes):
148
+ for start in range(0, n_samples, batch_size):
149
+ batch = min(batch_size, n_samples - start)
150
+ if replace:
151
+ indices = rng.choice(len(X), size=(batch, lib_size))
152
+ else:
153
+ indices = np.stack([rng.choice(len(X), size=lib_size, replace=False) for _ in range(batch)])
154
+ yield (i, slice(start, start + batch)), partial(samples, X, Y, Q, indices, predict_func=predict_func, mask=mask)
155
+
156
+
157
+ def samples(
158
+ X: np.ndarray,
159
+ Y: np.ndarray,
160
+ Q: np.ndarray,
161
+ indices: np.ndarray,
162
+ *,
163
+ predict_func: PredictFunc,
164
+ mask: Mask | None,
165
+ ) -> np.ndarray:
166
+ queries = np.broadcast_to(Q, (len(indices), *Q.shape))
167
+ # position j of library b is the point indices[b, j] of X
168
+ return predict_func(X[indices], Y[indices], queries, mask=None if mask is None else (lambda b, i, j: mask(i, indices[b, j])))
@@ -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