edmkit 0.0.12__tar.gz → 0.0.13__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.12 → edmkit-0.0.13}/PKG-INFO +3 -3
- {edmkit-0.0.12 → edmkit-0.0.13}/pyproject.toml +3 -3
- {edmkit-0.0.12 → edmkit-0.0.13}/pyproject.toml.orig +3 -3
- edmkit-0.0.13/src/edmkit/ccm.py +274 -0
- edmkit-0.0.13/src/edmkit/embedding.py +85 -0
- {edmkit-0.0.12 → edmkit-0.0.13}/src/edmkit/generate/__init__.py +1 -2
- edmkit-0.0.13/src/edmkit/metrics/__init__.py +35 -0
- edmkit-0.0.13/src/edmkit/metrics/mae.py +77 -0
- edmkit-0.0.13/src/edmkit/metrics/pearson_correlation.py +96 -0
- edmkit-0.0.13/src/edmkit/metrics/rmse.py +79 -0
- {edmkit-0.0.12 → edmkit-0.0.13}/src/edmkit/simplex_projection/__init__.py +1 -2
- {edmkit-0.0.12 → edmkit-0.0.13}/src/edmkit/simplex_projection/simplex_projection.py +26 -15
- {edmkit-0.0.12 → edmkit-0.0.13}/src/edmkit/simplex_projection/soft.py +13 -7
- {edmkit-0.0.12 → edmkit-0.0.13}/src/edmkit/smap.py +21 -15
- {edmkit-0.0.12 → edmkit-0.0.13}/src/edmkit/splits.py +3 -2
- edmkit-0.0.13/src/edmkit/theiler_window.py +34 -0
- {edmkit-0.0.12 → edmkit-0.0.13}/src/edmkit/util.py +1 -1
- edmkit-0.0.12/src/edmkit/ccm.py +0 -508
- edmkit-0.0.12/src/edmkit/embedding.py +0 -246
- edmkit-0.0.12/src/edmkit/metrics.py +0 -132
- edmkit-0.0.12/src/edmkit/simplex_projection/loo.py +0 -91
- {edmkit-0.0.12 → edmkit-0.0.13}/README.md +0 -0
- {edmkit-0.0.12 → edmkit-0.0.13}/src/edmkit/generate/double_pendulum.py +0 -0
- {edmkit-0.0.12 → edmkit-0.0.13}/src/edmkit/generate/lorenz.py +0 -0
- {edmkit-0.0.12 → edmkit-0.0.13}/src/edmkit/generate/mackey_glass.py +0 -0
- {edmkit-0.0.12 → edmkit-0.0.13}/src/edmkit/simplex_projection/knn.py +0 -0
- {edmkit-0.0.12 → edmkit-0.0.13}/src/edmkit/types.py +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: edmkit
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.13
|
|
4
4
|
Summary: Simple EDM (Empirical Dynamic Modeling) library
|
|
5
5
|
Author: FUJISHIGE TEMMA
|
|
6
6
|
Author-email: FUJISHIGE TEMMA <tenma.x0@gmail.com>
|
|
7
7
|
Requires-Dist: kdtree-rs>=0.3.1
|
|
8
8
|
Requires-Dist: numpy>=2.5.1
|
|
9
9
|
Requires-Dist: scipy>=1.17.1
|
|
10
|
-
Requires-Dist: tinygrad>=0.
|
|
11
|
-
Requires-Dist: tinysolve>=0.2.
|
|
10
|
+
Requires-Dist: tinygrad>=0.14.0
|
|
11
|
+
Requires-Dist: tinysolve>=0.2.1
|
|
12
12
|
Requires-Python: >=3.13
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "edmkit"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.13"
|
|
4
4
|
description = "Simple EDM (Empirical Dynamic Modeling) library"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">= 3.13"
|
|
@@ -8,8 +8,8 @@ dependencies = [
|
|
|
8
8
|
"kdtree-rs>=0.3.1",
|
|
9
9
|
"numpy>=2.5.1",
|
|
10
10
|
"scipy>=1.17.1",
|
|
11
|
-
"tinygrad>=0.
|
|
12
|
-
"tinysolve>=0.2.
|
|
11
|
+
"tinygrad>=0.14.0",
|
|
12
|
+
"tinysolve>=0.2.1",
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
[[project.authors]]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "edmkit"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.13"
|
|
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"
|
|
@@ -9,8 +9,8 @@ dependencies = [
|
|
|
9
9
|
"kdtree-rs>=0.3.1",
|
|
10
10
|
"numpy>=2.5.1",
|
|
11
11
|
"scipy>=1.17.1",
|
|
12
|
-
"tinygrad>=0.
|
|
13
|
-
"tinysolve>=0.2.
|
|
12
|
+
"tinygrad>=0.14.0",
|
|
13
|
+
"tinysolve>=0.2.1",
|
|
14
14
|
]
|
|
15
15
|
|
|
16
16
|
[dependency-groups]
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from edmkit.metrics import MetricFunc
|
|
6
|
+
from edmkit.types import PredictFunc
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AggregateFunc",
|
|
10
|
+
"SampleFunc",
|
|
11
|
+
"bootstrap",
|
|
12
|
+
"ccm",
|
|
13
|
+
"make_sample_func",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
type SampleFunc = Callable[[np.ndarray, int], np.ndarray]
|
|
17
|
+
"""SampleFunc is a function that takes (pool, size) and returns a sampled array."""
|
|
18
|
+
type AggregateFunc = Callable[[np.ndarray], float]
|
|
19
|
+
"""AggregateFunc is a function that takes an array of values and returns a single value."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def make_sample_func(seed: int | None = 42) -> SampleFunc:
|
|
23
|
+
"""Create a sample function with its own independent RNG."""
|
|
24
|
+
rng = np.random.default_rng(seed)
|
|
25
|
+
|
|
26
|
+
def sample_func(pool: np.ndarray, size: int) -> np.ndarray:
|
|
27
|
+
return rng.choice(pool, size=size, replace=True)
|
|
28
|
+
|
|
29
|
+
return sample_func
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def bootstrap(
|
|
33
|
+
X: np.ndarray,
|
|
34
|
+
Y: np.ndarray,
|
|
35
|
+
lib_sizes: np.ndarray,
|
|
36
|
+
predict_func: PredictFunc,
|
|
37
|
+
metric_func: MetricFunc,
|
|
38
|
+
n_samples: int = 20,
|
|
39
|
+
*,
|
|
40
|
+
library_pool: np.ndarray,
|
|
41
|
+
prediction_pool: np.ndarray,
|
|
42
|
+
sample_func: SampleFunc | None = None,
|
|
43
|
+
batch_size: int | None = 20,
|
|
44
|
+
) -> np.ndarray:
|
|
45
|
+
"""
|
|
46
|
+
Perform Convergent Cross Mapping and return per-sample scores.
|
|
47
|
+
|
|
48
|
+
Same as :func:`ccm` but returns the raw per-sample scores instead of
|
|
49
|
+
aggregating them.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
X : np.ndarray
|
|
54
|
+
Library delay vectors of shape ``(T,)`` or ``(T, E)`` (potential response).
|
|
55
|
+
Y : np.ndarray
|
|
56
|
+
Target values of shape ``(T,)`` or ``(T, E')`` (potential driver).
|
|
57
|
+
A 2D `Y` cross-maps to a delay vector; the metric scores all columns jointly.
|
|
58
|
+
lib_sizes : np.ndarray
|
|
59
|
+
Array of library sizes to test convergence.
|
|
60
|
+
predict_func : PredictFunc
|
|
61
|
+
Prediction function with signature (X, Y, Q) -> predictions.
|
|
62
|
+
metric_func : MetricFunc
|
|
63
|
+
Metric function with signature (predictions, observations) -> metric value.
|
|
64
|
+
n_samples : int, default 20
|
|
65
|
+
Number of random samples per library size for bootstrapping.
|
|
66
|
+
library_pool : np.ndarray
|
|
67
|
+
1-D array of integer indices from which library members are sampled.
|
|
68
|
+
prediction_pool : np.ndarray
|
|
69
|
+
1-D array of integer indices that are predicted.
|
|
70
|
+
sample_func : SampleFunc or None, default None
|
|
71
|
+
Function responsible for drawing a library sample of a given size.
|
|
72
|
+
When None, a fresh RNG-backed sampler is created per call.
|
|
73
|
+
batch_size : int or None, default 20
|
|
74
|
+
If specified, predictions are made in batches to limit memory usage.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
samples : np.ndarray
|
|
79
|
+
Per-sample skill scores of shape ``(n_samples, len(lib_sizes))``.
|
|
80
|
+
"""
|
|
81
|
+
if sample_func is None:
|
|
82
|
+
sample_func = make_sample_func()
|
|
83
|
+
|
|
84
|
+
if X.shape[0] != Y.shape[0]:
|
|
85
|
+
raise ValueError(f"X and Y must have same length, got {X.shape[0]} and {Y.shape[0]}")
|
|
86
|
+
if predict_func is None or not callable(predict_func):
|
|
87
|
+
raise ValueError(f"predict_func must be callable, got {type(predict_func)}")
|
|
88
|
+
if n_samples <= 0:
|
|
89
|
+
raise ValueError(f"n_samples must be positive, got {n_samples}")
|
|
90
|
+
|
|
91
|
+
if batch_size is None:
|
|
92
|
+
batch_size = n_samples
|
|
93
|
+
else:
|
|
94
|
+
batch_size = min(batch_size, n_samples)
|
|
95
|
+
|
|
96
|
+
if X.ndim == 1:
|
|
97
|
+
X = X[:, None]
|
|
98
|
+
if Y.ndim == 1:
|
|
99
|
+
Y = Y[:, None]
|
|
100
|
+
|
|
101
|
+
prediction_indices = np.tile(prediction_pool, (batch_size, 1))
|
|
102
|
+
Q = X[prediction_indices]
|
|
103
|
+
actual = Y[prediction_indices]
|
|
104
|
+
|
|
105
|
+
samples = np.zeros((n_samples, len(lib_sizes)))
|
|
106
|
+
|
|
107
|
+
for i, lib_size in enumerate(lib_sizes):
|
|
108
|
+
remaining = n_samples
|
|
109
|
+
while remaining > 0:
|
|
110
|
+
batch = min(batch_size, remaining)
|
|
111
|
+
|
|
112
|
+
library_indices = np.vstack([sample_func(library_pool, lib_size) for _ in range(batch)])
|
|
113
|
+
|
|
114
|
+
lib_X = X[library_indices]
|
|
115
|
+
lib_Y = Y[library_indices]
|
|
116
|
+
|
|
117
|
+
predictions = predict_func(lib_X, lib_Y, Q[:batch])
|
|
118
|
+
|
|
119
|
+
offset = n_samples - remaining
|
|
120
|
+
samples[offset : offset + batch, i] = metric_func(predictions, actual[:batch])
|
|
121
|
+
remaining -= batch
|
|
122
|
+
|
|
123
|
+
return samples
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def ccm(
|
|
127
|
+
X: np.ndarray,
|
|
128
|
+
Y: np.ndarray,
|
|
129
|
+
lib_sizes: np.ndarray,
|
|
130
|
+
predict_func: PredictFunc,
|
|
131
|
+
metric_func: MetricFunc,
|
|
132
|
+
n_samples: int = 20,
|
|
133
|
+
*,
|
|
134
|
+
library_pool: np.ndarray,
|
|
135
|
+
prediction_pool: np.ndarray,
|
|
136
|
+
sample_func: SampleFunc | None = None,
|
|
137
|
+
aggregate_func: AggregateFunc = np.mean,
|
|
138
|
+
batch_size: int | None = 20,
|
|
139
|
+
) -> np.ndarray:
|
|
140
|
+
"""
|
|
141
|
+
Perform Convergent Cross Mapping using a custom prediction function.
|
|
142
|
+
|
|
143
|
+
CCM tests for causality from X to Y by using the attractor reconstructed from Y
|
|
144
|
+
to predict values of X. If X causes Y, then Y's attractor contains information
|
|
145
|
+
about X, allowing cross-mapping from Y to X.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
X : np.ndarray
|
|
150
|
+
Library delay vectors of shape ``(T,)`` or ``(T, E)`` (potential response).
|
|
151
|
+
Y : np.ndarray
|
|
152
|
+
Target values of shape ``(T,)`` or ``(T, E')`` (potential driver).
|
|
153
|
+
A 2D `Y` cross-maps to a delay vector; the metric scores all columns jointly.
|
|
154
|
+
lib_sizes : np.ndarray
|
|
155
|
+
Array of library sizes to test convergence.
|
|
156
|
+
predict_func : PredictFunc
|
|
157
|
+
Prediction function with signature (X, Y, Q) -> predictions.
|
|
158
|
+
Can be `simplex_projection`, `smap` with partial application, or a custom function.
|
|
159
|
+
metric_func : MetricFunc
|
|
160
|
+
Metric function with signature (predictions, observations) -> metric value.
|
|
161
|
+
n_samples : int, default 100
|
|
162
|
+
Number of random samples per library size for bootstrapping.
|
|
163
|
+
library_pool : np.ndarray
|
|
164
|
+
1-D array of integer indices from which library members are sampled.
|
|
165
|
+
prediction_pool : np.ndarray
|
|
166
|
+
1-D array of integer indices that are predicted.
|
|
167
|
+
sample_func : SampleFunc or None, default None
|
|
168
|
+
Function responsible for drawing a library sample of a given size.
|
|
169
|
+
It receives ``(pool, size)`` and returns an array of indices.
|
|
170
|
+
When None, a fresh RNG-backed sampler is created per call.
|
|
171
|
+
aggregate_func : AggregateFunc, default np.mean
|
|
172
|
+
Reducer applied to the skill samples for each library size.
|
|
173
|
+
batch_size : int or None, default 20
|
|
174
|
+
If not specified, batch_size == n_samples.
|
|
175
|
+
If specified, predictions are made in batches to limit memory usage.
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
correlations : np.ndarray
|
|
179
|
+
Mean correlation coefficient for each library size.
|
|
180
|
+
|
|
181
|
+
Raises
|
|
182
|
+
------
|
|
183
|
+
ValueError
|
|
184
|
+
- If `X` and `Y` have different lengths.
|
|
185
|
+
- If `lib_sizes` contains non-positive values.
|
|
186
|
+
- If `predict_func` is not callable.
|
|
187
|
+
- If `n_samples` is not positive.
|
|
188
|
+
- If `aggregate_func` is not callable.
|
|
189
|
+
- If `library_pool` or `prediction_pool` is invalid.
|
|
190
|
+
|
|
191
|
+
Notes
|
|
192
|
+
-----
|
|
193
|
+
- Higher correlation at larger library sizes indicates convergence and suggests X influences Y (X -> Y causality)
|
|
194
|
+
- Convergence is the key signature of causality in CCM
|
|
195
|
+
- The method uses Y's attractor to predict X (cross-mapping)
|
|
196
|
+
|
|
197
|
+
Examples
|
|
198
|
+
--------
|
|
199
|
+
```python
|
|
200
|
+
from functools import partial
|
|
201
|
+
|
|
202
|
+
import numpy as np
|
|
203
|
+
|
|
204
|
+
from edmkit.ccm import ccm
|
|
205
|
+
from edmkit.embedding import embed
|
|
206
|
+
from edmkit.metrics import pearson_correlation
|
|
207
|
+
from edmkit.simplex_projection import simplex_projection
|
|
208
|
+
from edmkit.smap import smap
|
|
209
|
+
|
|
210
|
+
# Generate coupled logistic maps (X drives Y)
|
|
211
|
+
N = 1000
|
|
212
|
+
rx, ry, Bxy = 3.8, 3.5, 0.02
|
|
213
|
+
X = np.zeros(N)
|
|
214
|
+
Y = np.zeros(N)
|
|
215
|
+
X[0], Y[0] = 0.4, 0.2
|
|
216
|
+
for i in range(1, N):
|
|
217
|
+
X[i] = X[i - 1] * (rx - rx * X[i - 1])
|
|
218
|
+
Y[i] = Y[i - 1] * (ry - ry * Y[i - 1]) + Bxy * X[i - 1]
|
|
219
|
+
|
|
220
|
+
tau = 1
|
|
221
|
+
E = 2
|
|
222
|
+
|
|
223
|
+
# To test X -> Y causality, cross-map from Y's attractor to X
|
|
224
|
+
coordinates = np.array([[0, tau * j] for j in range(E)])
|
|
225
|
+
Y_embedding, _ = embed(coordinates, Y)
|
|
226
|
+
shift = tau * (E - 1)
|
|
227
|
+
X_aligned = X[shift:]
|
|
228
|
+
|
|
229
|
+
library_pool = np.arange(Y_embedding.shape[0] // 2)
|
|
230
|
+
prediction_pool = np.arange(Y_embedding.shape[0] // 2, Y_embedding.shape[0])
|
|
231
|
+
|
|
232
|
+
# logarithmic within range 10 to max library size
|
|
233
|
+
lib_sizes = np.logspace(np.log10(10), np.log10(library_pool[-1]), num=5, dtype=int)
|
|
234
|
+
|
|
235
|
+
# Using simplex projection
|
|
236
|
+
correlations = ccm(
|
|
237
|
+
Y_embedding,
|
|
238
|
+
X_aligned,
|
|
239
|
+
lib_sizes=lib_sizes,
|
|
240
|
+
predict_func=simplex_projection,
|
|
241
|
+
metric_func=pearson_correlation,
|
|
242
|
+
library_pool=library_pool,
|
|
243
|
+
prediction_pool=prediction_pool,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Using S-Map with partial application
|
|
247
|
+
correlations = ccm(
|
|
248
|
+
Y_embedding,
|
|
249
|
+
X_aligned,
|
|
250
|
+
lib_sizes=lib_sizes,
|
|
251
|
+
predict_func=partial(smap, theta=2.0, alpha=1e-10),
|
|
252
|
+
metric_func=pearson_correlation,
|
|
253
|
+
library_pool=library_pool,
|
|
254
|
+
prediction_pool=prediction_pool,
|
|
255
|
+
)
|
|
256
|
+
```
|
|
257
|
+
"""
|
|
258
|
+
if aggregate_func is None or not callable(aggregate_func):
|
|
259
|
+
raise ValueError("aggregate_func must be a callable")
|
|
260
|
+
|
|
261
|
+
samples = bootstrap(
|
|
262
|
+
X,
|
|
263
|
+
Y,
|
|
264
|
+
lib_sizes,
|
|
265
|
+
predict_func,
|
|
266
|
+
metric_func,
|
|
267
|
+
n_samples,
|
|
268
|
+
library_pool=library_pool,
|
|
269
|
+
prediction_pool=prediction_pool,
|
|
270
|
+
sample_func=sample_func,
|
|
271
|
+
batch_size=batch_size,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return np.array([aggregate_func(samples[:, i]) for i in range(samples.shape[1])])
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
__all__ = ["embed"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def embed(coordinates: np.ndarray, x: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
7
|
+
"""Delay vectors for an arbitrary choice of variables and lags.
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
coordinates : np.ndarray
|
|
12
|
+
Integer array of shape ``(E, 2)``. Row ``j`` is ``(variable, lag)``, selecting
|
|
13
|
+
observation ``x[t - lag, variable]`` for the delay vector at time ``t``.
|
|
14
|
+
A negative lag selects an observation taken after time ``t``.
|
|
15
|
+
x : np.ndarray
|
|
16
|
+
Observations of shape ``(T, d)``, or ``(T,)``.
|
|
17
|
+
|
|
18
|
+
Returns
|
|
19
|
+
-------
|
|
20
|
+
X : np.ndarray
|
|
21
|
+
Delay vectors of shape ``(N, E)``, one row per time in `t`.
|
|
22
|
+
t : np.ndarray
|
|
23
|
+
Times of shape ``(N,)`` at which every coordinate is available, that is
|
|
24
|
+
``lags.max() <= t <= T - 1 + lags.min()``. A time outside ``[0, T - 1]`` is
|
|
25
|
+
included when every coordinate still reads an observation inside `x`.
|
|
26
|
+
|
|
27
|
+
Raises
|
|
28
|
+
------
|
|
29
|
+
ValueError
|
|
30
|
+
- If `coordinates` is not an integer array of shape ``(E, 2)`` with ``E >= 1``.
|
|
31
|
+
- If `x` is not a 1D or 2D array.
|
|
32
|
+
- If a variable index lies outside `x`.
|
|
33
|
+
- If no time has every coordinate available.
|
|
34
|
+
|
|
35
|
+
Examples
|
|
36
|
+
--------
|
|
37
|
+
```python
|
|
38
|
+
import numpy as np
|
|
39
|
+
|
|
40
|
+
from edmkit.embedding import embed
|
|
41
|
+
|
|
42
|
+
x = np.arange(20).reshape(10, 2) # ten times, two variables
|
|
43
|
+
|
|
44
|
+
# Classical univariate delay coordinates.
|
|
45
|
+
tau, E = 2, 3
|
|
46
|
+
coordinates = np.array([[0, tau * j] for j in range(E)])
|
|
47
|
+
X, t = embed(coordinates, x)
|
|
48
|
+
print(X)
|
|
49
|
+
print(t)
|
|
50
|
+
# [[ 8 4 0]
|
|
51
|
+
# [10 6 2]
|
|
52
|
+
# [12 8 4]
|
|
53
|
+
# [14 10 6]
|
|
54
|
+
# [16 12 8]
|
|
55
|
+
# [18 14 10]]
|
|
56
|
+
# [4 5 6 7 8 9]
|
|
57
|
+
|
|
58
|
+
# One step ahead of the same coordinates, as iterated forecasting needs.
|
|
59
|
+
Y, t_ahead = embed(coordinates - [0, 1], x)
|
|
60
|
+
```
|
|
61
|
+
"""
|
|
62
|
+
coordinates = np.asarray(coordinates)
|
|
63
|
+
if coordinates.ndim != 2 or coordinates.shape[-1] != 2:
|
|
64
|
+
raise ValueError(f"coordinates must have shape (E, 2), got coordinates.shape={coordinates.shape}")
|
|
65
|
+
if coordinates.shape[0] < 1:
|
|
66
|
+
raise ValueError("coordinates must select at least one delay coordinate")
|
|
67
|
+
if not np.issubdtype(coordinates.dtype, np.integer):
|
|
68
|
+
raise ValueError(f"coordinates must be an integer array, got coordinates.dtype={coordinates.dtype}")
|
|
69
|
+
if x.ndim == 1:
|
|
70
|
+
x = x[:, None]
|
|
71
|
+
if x.ndim != 2:
|
|
72
|
+
raise ValueError(f"x must have shape (T,) or (T, d), got x.shape={x.shape}")
|
|
73
|
+
|
|
74
|
+
variables, lags = coordinates[:, 0], coordinates[:, 1]
|
|
75
|
+
if variables.min() < 0 or variables.max() >= x.shape[1]:
|
|
76
|
+
raise ValueError(f"variable indices must lie in [0, {x.shape[1] - 1}], got {variables.min()} to {variables.max()}")
|
|
77
|
+
|
|
78
|
+
# A delay vector at time t needs 0 <= t - lag <= T - 1 for every coordinate.
|
|
79
|
+
first = int(lags.max())
|
|
80
|
+
last = x.shape[0] - 1 + int(lags.min())
|
|
81
|
+
if last < first:
|
|
82
|
+
raise ValueError(f"no time has every coordinate available: lags span {int(lags.min())} to {int(lags.max())} for T={x.shape[0]}")
|
|
83
|
+
|
|
84
|
+
t = np.arange(first, last + 1)
|
|
85
|
+
return x[t[:, None] - lags[None, :], variables[None, :]], t
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
# ruff: noqa: F401
|
|
2
1
|
from .double_pendulum import double_pendulum, to_xy
|
|
3
2
|
from .lorenz import lorenz
|
|
4
3
|
from .mackey_glass import mackey_glass
|
|
5
4
|
|
|
6
|
-
__all__ = ["double_pendulum", "
|
|
5
|
+
__all__ = ["double_pendulum", "lorenz", "mackey_glass", "to_xy"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Protocol, overload
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from edmkit.metrics.mae import mae
|
|
8
|
+
from edmkit.metrics.pearson_correlation import pearson_correlation
|
|
9
|
+
from edmkit.metrics.rmse import rmse
|
|
10
|
+
|
|
11
|
+
__all__ = ["MetricFunc", "mae", "pearson_correlation", "rmse"]
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from tinygrad import Tensor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MetricFunc(Protocol):
|
|
18
|
+
"""Metric function protocol.
|
|
19
|
+
|
|
20
|
+
Accepts predictions and observations of the same shape and returns a metric value.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@overload
|
|
24
|
+
def __call__(self, predictions: np.ndarray, observations: np.ndarray) -> np.ndarray: ...
|
|
25
|
+
|
|
26
|
+
@overload
|
|
27
|
+
def __call__(self, predictions: Tensor, observations: Tensor) -> Tensor: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
func: MetricFunc
|
|
32
|
+
|
|
33
|
+
func = pearson_correlation
|
|
34
|
+
func = rmse
|
|
35
|
+
func = mae
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, overload
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
__all__ = ["mae"]
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from tinygrad import Tensor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@overload
|
|
14
|
+
def mae(predictions: np.ndarray, observations: np.ndarray) -> np.ndarray: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@overload
|
|
18
|
+
def mae(predictions: Tensor, observations: Tensor) -> Tensor: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def mae(predictions, observations):
|
|
22
|
+
"""Mean Absolute Error.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
predictions : np.ndarray or Tensor
|
|
27
|
+
``(N,)``, ``(N, D)``, or ``(B, N, D)``.
|
|
28
|
+
observations : np.ndarray or Tensor
|
|
29
|
+
Same shape as `predictions`.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
np.ndarray or Tensor
|
|
34
|
+
``()`` for 1D/2D input, ``(B,)`` for 3D input.
|
|
35
|
+
|
|
36
|
+
Raises
|
|
37
|
+
------
|
|
38
|
+
ValueError
|
|
39
|
+
- If `predictions` and `observations` have different shapes.
|
|
40
|
+
- If the inputs are not 1D, 2D, or 3D.
|
|
41
|
+
"""
|
|
42
|
+
if isinstance(predictions, np.ndarray):
|
|
43
|
+
return _numpy(predictions, observations)
|
|
44
|
+
|
|
45
|
+
return _tensor(predictions, observations)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _numpy(predictions: np.ndarray, observations: np.ndarray) -> np.ndarray:
|
|
49
|
+
if predictions.shape != observations.shape:
|
|
50
|
+
raise ValueError(f"Shape mismatch: predictions {predictions.shape} vs observations {observations.shape}")
|
|
51
|
+
if predictions.ndim not in (1, 2, 3):
|
|
52
|
+
raise ValueError(f"Expected 1D, 2D, or 3D arrays, got {predictions.ndim}D")
|
|
53
|
+
|
|
54
|
+
# treat a 1D series as a single target dimension
|
|
55
|
+
if predictions.ndim == 1:
|
|
56
|
+
predictions = predictions[:, None]
|
|
57
|
+
observations = observations[:, None]
|
|
58
|
+
|
|
59
|
+
# 2D: (N, D) -> (N,) -> ()
|
|
60
|
+
# 3D: (B, N, D) -> (B, N) -> (B,)
|
|
61
|
+
return np.abs(predictions - observations).mean(axis=-1).mean(axis=-1)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _tensor(predictions: Tensor, observations: Tensor) -> Tensor:
|
|
65
|
+
if predictions.shape != observations.shape:
|
|
66
|
+
raise ValueError(f"Shape mismatch: predictions {predictions.shape} vs observations {observations.shape}")
|
|
67
|
+
if predictions.ndim not in (1, 2, 3):
|
|
68
|
+
raise ValueError(f"Expected 1D, 2D, or 3D tensors, got {predictions.ndim}D")
|
|
69
|
+
|
|
70
|
+
# treat a 1D series as a single target dimension
|
|
71
|
+
if predictions.ndim == 1:
|
|
72
|
+
predictions = predictions.unsqueeze(-1)
|
|
73
|
+
observations = observations.unsqueeze(-1)
|
|
74
|
+
|
|
75
|
+
# 2D: (N, D) -> (N,) -> ()
|
|
76
|
+
# 3D: (B, N, D) -> (B, N) -> (B,)
|
|
77
|
+
return (predictions - observations).abs().mean(axis=-1).mean(axis=-1)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, overload
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
__all__ = ["pearson_correlation"]
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from tinygrad import Tensor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@overload
|
|
14
|
+
def pearson_correlation(predictions: np.ndarray, observations: np.ndarray) -> np.ndarray: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@overload
|
|
18
|
+
def pearson_correlation(predictions: Tensor, observations: Tensor) -> Tensor: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def pearson_correlation(predictions, observations):
|
|
22
|
+
"""Mean Pearson correlation over the target dimensions.
|
|
23
|
+
|
|
24
|
+
The correlation is computed per target dimension along the sample axis,
|
|
25
|
+
then averaged over the dimensions.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
predictions : np.ndarray or Tensor
|
|
30
|
+
``(N,)``, ``(N, D)``, or ``(B, N, D)``.
|
|
31
|
+
observations : np.ndarray or Tensor
|
|
32
|
+
Same shape as `predictions`.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
np.ndarray or Tensor
|
|
37
|
+
``()`` for 1D/2D input, ``(B,)`` for 3D input.
|
|
38
|
+
|
|
39
|
+
Raises
|
|
40
|
+
------
|
|
41
|
+
ValueError
|
|
42
|
+
- If `predictions` and `observations` have different shapes.
|
|
43
|
+
- If the inputs are not 1D, 2D, or 3D.
|
|
44
|
+
"""
|
|
45
|
+
if isinstance(predictions, np.ndarray):
|
|
46
|
+
return _numpy(predictions, observations)
|
|
47
|
+
|
|
48
|
+
return _tensor(predictions, observations)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _numpy(predictions: np.ndarray, observations: np.ndarray) -> np.ndarray:
|
|
52
|
+
if predictions.shape != observations.shape:
|
|
53
|
+
raise ValueError(f"Shape mismatch: predictions {predictions.shape} vs observations {observations.shape}")
|
|
54
|
+
if predictions.ndim not in (1, 2, 3):
|
|
55
|
+
raise ValueError(f"Expected 1D, 2D, or 3D arrays, got {predictions.ndim}D")
|
|
56
|
+
|
|
57
|
+
# treat a 1D series as a single target dimension
|
|
58
|
+
if predictions.ndim == 1:
|
|
59
|
+
predictions = predictions[:, None]
|
|
60
|
+
observations = observations[:, None]
|
|
61
|
+
|
|
62
|
+
predictions_centered = predictions - predictions.mean(axis=-2, keepdims=True)
|
|
63
|
+
observations_centered = observations - observations.mean(axis=-2, keepdims=True)
|
|
64
|
+
|
|
65
|
+
numerator = (predictions_centered * observations_centered).sum(axis=-2)
|
|
66
|
+
denominator = np.sqrt((predictions_centered**2).sum(axis=-2) * (observations_centered**2).sum(axis=-2))
|
|
67
|
+
|
|
68
|
+
denominator = np.where(denominator > 0, denominator, np.nan)
|
|
69
|
+
|
|
70
|
+
# 2D: (D,) -> ()
|
|
71
|
+
# 3D: (B, D) -> (B,)
|
|
72
|
+
return (numerator / denominator).mean(axis=-1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _tensor(predictions: Tensor, observations: Tensor) -> Tensor:
|
|
76
|
+
if predictions.shape != observations.shape:
|
|
77
|
+
raise ValueError(f"Shape mismatch: predictions {predictions.shape} vs observations {observations.shape}")
|
|
78
|
+
if predictions.ndim not in (1, 2, 3):
|
|
79
|
+
raise ValueError(f"Expected 1D, 2D, or 3D tensors, got {predictions.ndim}D")
|
|
80
|
+
|
|
81
|
+
# treat a 1D series as a single target dimension
|
|
82
|
+
if predictions.ndim == 1:
|
|
83
|
+
predictions = predictions.unsqueeze(-1)
|
|
84
|
+
observations = observations.unsqueeze(-1)
|
|
85
|
+
|
|
86
|
+
predictions_centered = predictions - predictions.mean(axis=-2, keepdim=True)
|
|
87
|
+
observations_centered = observations - observations.mean(axis=-2, keepdim=True)
|
|
88
|
+
|
|
89
|
+
numerator = (predictions_centered * observations_centered).sum(axis=-2)
|
|
90
|
+
denominator = predictions_centered.pow(2).sum(axis=-2) * observations_centered.pow(2).sum(axis=-2)
|
|
91
|
+
|
|
92
|
+
denominator = (denominator > 0).where(denominator.clamp(min_=1e-12).sqrt(), float("nan"))
|
|
93
|
+
|
|
94
|
+
# 2D: (D,) -> ()
|
|
95
|
+
# 3D: (B, D) -> (B,)
|
|
96
|
+
return (numerator / denominator).mean(axis=-1)
|