OpenReservoirComputing 0.1.0__py3-none-any.whl
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.
- openreservoircomputing-0.1.0.dist-info/METADATA +168 -0
- openreservoircomputing-0.1.0.dist-info/RECORD +28 -0
- openreservoircomputing-0.1.0.dist-info/WHEEL +5 -0
- openreservoircomputing-0.1.0.dist-info/licenses/LICENSE +201 -0
- openreservoircomputing-0.1.0.dist-info/top_level.txt +1 -0
- orc/__init__.py +29 -0
- orc/classifier/__init__.py +6 -0
- orc/classifier/base.py +6 -0
- orc/classifier/models.py +6 -0
- orc/classifier/train.py +6 -0
- orc/control/__init__.py +15 -0
- orc/control/base.py +362 -0
- orc/control/models.py +139 -0
- orc/control/train.py +90 -0
- orc/data/__init__.py +27 -0
- orc/data/integrators.py +753 -0
- orc/drivers.py +851 -0
- orc/embeddings.py +459 -0
- orc/forecaster/__init__.py +20 -0
- orc/forecaster/base.py +487 -0
- orc/forecaster/models.py +416 -0
- orc/forecaster/train.py +301 -0
- orc/readouts.py +742 -0
- orc/tuning/__init__.py +6 -0
- orc/utils/__init__.py +6 -0
- orc/utils/numerics.py +115 -0
- orc/utils/regressions.py +73 -0
- orc/utils/visualization.py +193 -0
orc/tuning/__init__.py
ADDED
orc/utils/__init__.py
ADDED
orc/utils/numerics.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Additional utility functions for ORC."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
|
|
5
|
+
import jax
|
|
6
|
+
import jax.numpy as jnp
|
|
7
|
+
from jaxtyping import Array
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@functools.partial(jax.jit, static_argnames=["max_iters"])
|
|
11
|
+
def _arnoldi_iteration(A, max_iters=200, seed=0):
|
|
12
|
+
"""Perform Arnoldi iteration to find an orthonormal basis for the Krylov subspace.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
A : Array
|
|
17
|
+
The input matrix (m x m) for which the Krylov subspace is computed.
|
|
18
|
+
max_iters : int, optional
|
|
19
|
+
The maximum number of Arnoldi iterations to perform. Default is 200. If the
|
|
20
|
+
number of iterations exceeds the size of the matrix, it will be capped at m.
|
|
21
|
+
seed : int, optional
|
|
22
|
+
Random seed for initializing the starting vector. Default is 0.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
Q : Array
|
|
27
|
+
An orthonormal basis for the Krylov subspace (m x n).
|
|
28
|
+
H : Array
|
|
29
|
+
The upper Hessenberg matrix (n x n) representing the coefficients of the Krylov
|
|
30
|
+
subspace.
|
|
31
|
+
"""
|
|
32
|
+
# A is m x m; n is the size of the krylov basis
|
|
33
|
+
m = A.shape[0]
|
|
34
|
+
n = max_iters
|
|
35
|
+
|
|
36
|
+
# choose a random vector to start iterating on
|
|
37
|
+
key = jax.random.PRNGKey(seed)
|
|
38
|
+
b = jax.random.normal(key, (m,))
|
|
39
|
+
q0 = b / jnp.linalg.norm(b)
|
|
40
|
+
|
|
41
|
+
# init krylov basis Q and hessenberg matrix H
|
|
42
|
+
Q = jnp.zeros((m, n + 1), dtype=A.dtype)
|
|
43
|
+
H = jnp.zeros((n + 1, n), dtype=A.dtype)
|
|
44
|
+
Q = Q.at[:, 0].set(q0)
|
|
45
|
+
|
|
46
|
+
# modified gs step to form orth krylov basis
|
|
47
|
+
def gs_step(carry, j):
|
|
48
|
+
v, Q, mask = carry
|
|
49
|
+
# Only apply when j is less than or equal to k (mask is True)
|
|
50
|
+
h_jk = jnp.where(mask[j], jnp.dot(Q[:, j], v), 0.0)
|
|
51
|
+
v = jnp.where(mask[j], v - h_jk * Q[:, j], v)
|
|
52
|
+
return (v, Q, mask), h_jk
|
|
53
|
+
|
|
54
|
+
def arnoldi_step(carry, k):
|
|
55
|
+
A, Q, H = carry
|
|
56
|
+
|
|
57
|
+
# new candidate vector
|
|
58
|
+
v = A @ Q[:, k]
|
|
59
|
+
|
|
60
|
+
# Create a mask for valid indices (0 to k)
|
|
61
|
+
idx_mask = jnp.arange(n + 1) <= k
|
|
62
|
+
|
|
63
|
+
# run modified gs with fixed-size loop and masking
|
|
64
|
+
final_carry_gs, h_jk_vals = jax.lax.scan(
|
|
65
|
+
gs_step,
|
|
66
|
+
(v, Q, idx_mask),
|
|
67
|
+
jnp.arange(n + 1), # fixed size scan
|
|
68
|
+
)
|
|
69
|
+
v = final_carry_gs[0] # orthogonalized candidate vector
|
|
70
|
+
|
|
71
|
+
# Calculate subdiagonal
|
|
72
|
+
h_kplus1k = jnp.linalg.norm(v)
|
|
73
|
+
|
|
74
|
+
# Update H column k using a mask
|
|
75
|
+
col_indices = jnp.arange(n + 1)
|
|
76
|
+
mask = col_indices <= k # For the first k+1 elements
|
|
77
|
+
|
|
78
|
+
# Apply h_jk_vals for the first k+1 elements using the mask
|
|
79
|
+
H_col = jnp.where(mask, h_jk_vals, H[:, k])
|
|
80
|
+
|
|
81
|
+
# Set the k+1 element to h_kplus1k
|
|
82
|
+
H_col = H_col.at[k + 1].set(h_kplus1k)
|
|
83
|
+
|
|
84
|
+
# Update the k-th column of H
|
|
85
|
+
H = H.at[:, k].set(H_col)
|
|
86
|
+
|
|
87
|
+
# Update Q[:, k+1]
|
|
88
|
+
Q = Q.at[:, k + 1].set(v / (h_kplus1k))
|
|
89
|
+
|
|
90
|
+
return (A, Q, H), None
|
|
91
|
+
|
|
92
|
+
final_carry_arnoldi, _ = jax.lax.scan(arnoldi_step, (A, Q, H), jnp.arange(n))
|
|
93
|
+
_, Q, H = final_carry_arnoldi
|
|
94
|
+
|
|
95
|
+
return Q[:, :n], H[:n, :n]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@functools.partial(jax.jit, static_argnames=("max_iters",))
|
|
99
|
+
def max_eig_arnoldi(A: Array, max_iters: int = 200, seed: int = 0):
|
|
100
|
+
"""Compute the maximum eigenvalue of a matrix using the Arnoldi iteration method.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
A : Array
|
|
105
|
+
The input matrix (m x m) for which the maximum eigenvalue is computed.
|
|
106
|
+
max_iters : int, optional
|
|
107
|
+
The maximum number of Arnoldi iterations to perform. Default is 200. If the
|
|
108
|
+
number of iterations exceeds the size of the matrix, it will be capped at m.
|
|
109
|
+
seed : int, optional
|
|
110
|
+
Random seed for initializing the starting vector. Default is 0.
|
|
111
|
+
"""
|
|
112
|
+
_, H = _arnoldi_iteration(A, max_iters, seed)
|
|
113
|
+
eigvals = jnp.linalg.eigvals(H)
|
|
114
|
+
lambda_max = eigvals[jnp.argmax(jnp.abs(eigvals))]
|
|
115
|
+
return lambda_max
|
orc/utils/regressions.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Implements common regressions used to train RC models."""
|
|
2
|
+
|
|
3
|
+
import equinox as eqx
|
|
4
|
+
import jax
|
|
5
|
+
import jax.numpy as jnp
|
|
6
|
+
from jaxtyping import Array
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def ridge_regression(res_seq: Array, target_seq: Array, beta: float = 1e-7):
|
|
10
|
+
"""Solve a single matrix ridge regression problem.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
res_seq : Array
|
|
15
|
+
Sequence of training reservoir states, (shape=(seq_len, res_dim)).
|
|
16
|
+
target_seq : Array
|
|
17
|
+
Sequence of training targe states, (shape=(seq_len, out_dim)).
|
|
18
|
+
beta : float
|
|
19
|
+
Tikhonov regularization parameter.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
Array
|
|
24
|
+
Solution to ridge regression s.t. cmat @ res_seq = target_seq.
|
|
25
|
+
"""
|
|
26
|
+
lhs = res_seq.T @ res_seq + beta * jnp.eye(res_seq.shape[1], dtype=res_seq.dtype)
|
|
27
|
+
rhs = res_seq.T @ target_seq
|
|
28
|
+
cmat = jax.scipy.linalg.solve(lhs, rhs, assume_a="sym").T
|
|
29
|
+
return cmat
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_solve_all_ridge_reg = eqx.filter_vmap(ridge_regression, in_axes=eqx.if_array(1))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _solve_all_ridge_reg_batched(
|
|
36
|
+
res_seq_train: Array, target_seq: Array, beta: float, batch_size: int
|
|
37
|
+
) -> Array:
|
|
38
|
+
"""Solve ridge regression for all parallel reservoirs using batched vmap.
|
|
39
|
+
|
|
40
|
+
This function processes the parallel reservoirs in batches to reduce memory
|
|
41
|
+
usage for large numbers of parallel reservoirs.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
res_seq_train : Array
|
|
46
|
+
Training reservoir states, shape=(seq_len, chunks, res_dim).
|
|
47
|
+
target_seq : Array
|
|
48
|
+
Target sequence, shape=(seq_len, chunks, out_dim).
|
|
49
|
+
beta : float
|
|
50
|
+
Tikhonov regularization parameter.
|
|
51
|
+
batch_size : int
|
|
52
|
+
Number of parallel reservoirs to process in each batch.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
Array
|
|
57
|
+
Ridge regression solution for all chunks, shape=(chunks, out_dim, res_dim).
|
|
58
|
+
"""
|
|
59
|
+
chunks = res_seq_train.shape[1]
|
|
60
|
+
|
|
61
|
+
if batch_size >= chunks:
|
|
62
|
+
return _solve_all_ridge_reg(res_seq_train, target_seq, beta)
|
|
63
|
+
|
|
64
|
+
results = []
|
|
65
|
+
for i in range(0, chunks, batch_size):
|
|
66
|
+
end_idx = min(i + batch_size, chunks)
|
|
67
|
+
batch_res = res_seq_train[:, i:end_idx, :]
|
|
68
|
+
batch_target = target_seq[:, i:end_idx, :]
|
|
69
|
+
|
|
70
|
+
batch_result = _solve_all_ridge_reg(batch_res, batch_target, beta)
|
|
71
|
+
results.append(batch_result)
|
|
72
|
+
|
|
73
|
+
return jnp.concatenate(results, axis=0)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Visualization utilities for plotting time series and spatiotemporal data."""
|
|
2
|
+
|
|
3
|
+
import jax.numpy as jnp
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def plot_time_series(
|
|
9
|
+
U_lst,
|
|
10
|
+
t=None,
|
|
11
|
+
time_series_labels=None,
|
|
12
|
+
line_formats=None,
|
|
13
|
+
state_var_names=None,
|
|
14
|
+
t_lim=None,
|
|
15
|
+
figsize=(20, 8),
|
|
16
|
+
x_label=r"$t$",
|
|
17
|
+
title=None,
|
|
18
|
+
**plot_kwargs,
|
|
19
|
+
):
|
|
20
|
+
"""Plot time series data with separate panels for each state variable.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
U_lst : 2D array or list of 2D arrays
|
|
25
|
+
If a 2D array, shape should be (Nt, Nu) where Nu is the number of state
|
|
26
|
+
variables and Nt is the number of time points.If a list of 2D arrays,
|
|
27
|
+
each array should have shape (Nt, Nu) and represent different time series.
|
|
28
|
+
t : 1D array, optional
|
|
29
|
+
1D array of time points. If None, the time points will be assumed to be
|
|
30
|
+
evenly spaced from 0 to Nt-1.
|
|
31
|
+
time_series_labels : list of strings, optional
|
|
32
|
+
List of strings containing the labels for each time series to be shown in
|
|
33
|
+
a legend. If None, no labels will be shown.
|
|
34
|
+
line_formats : list of strings, optional
|
|
35
|
+
List of strings containing the line formats for each time series. If None,
|
|
36
|
+
default line format will be used.
|
|
37
|
+
state_var_names : list of strings, optional
|
|
38
|
+
List of strings containing the names of the state variables. If None,
|
|
39
|
+
no y-axis labels will be shown.
|
|
40
|
+
t_lim : tuple, optional
|
|
41
|
+
Limit for the x-axis. If None, the x-axis will be set to the full
|
|
42
|
+
range of time points.
|
|
43
|
+
figsize : tuple, optional
|
|
44
|
+
Size of the figure to be created. Default is (20, 8).
|
|
45
|
+
x_label : string, optional
|
|
46
|
+
Label for the x-axis. Default is r'$t$'.
|
|
47
|
+
title : string, optional
|
|
48
|
+
Title of the plot. If None, no title is shown.
|
|
49
|
+
plot_kwargs : dict, optional
|
|
50
|
+
Additional arguments to pass to the plot function.
|
|
51
|
+
"""
|
|
52
|
+
# Input validation
|
|
53
|
+
if not isinstance(U_lst, list):
|
|
54
|
+
if not isinstance(U_lst, jnp.ndarray | np.ndarray) or U_lst.ndim != 2:
|
|
55
|
+
raise TypeError(
|
|
56
|
+
"U_lst must be a 2D JAX or NumPy array or a list of \
|
|
57
|
+
2D JAX/NumPy arrays."
|
|
58
|
+
)
|
|
59
|
+
U_lst = [U_lst]
|
|
60
|
+
else:
|
|
61
|
+
if not all(
|
|
62
|
+
isinstance(U, jnp.ndarray | np.ndarray) and U.ndim == 2 for U in U_lst
|
|
63
|
+
):
|
|
64
|
+
raise TypeError("All elements in U_lst must be 2D JAX or NumPy arrays.")
|
|
65
|
+
if not all(U.shape == U_lst[0].shape for U in U_lst):
|
|
66
|
+
raise ValueError("All arrays in U_lst must have the same shape.")
|
|
67
|
+
|
|
68
|
+
Nu = U_lst[0].shape[1]
|
|
69
|
+
Nt = U_lst[0].shape[0]
|
|
70
|
+
|
|
71
|
+
if t is not None:
|
|
72
|
+
if not isinstance(t, jnp.ndarray | np.ndarray) or t.ndim != 1:
|
|
73
|
+
raise TypeError("t must be a 1D JAX or NumPy array.")
|
|
74
|
+
if len(t) != Nt:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Length of t ({len(t)}) must match the number of time\
|
|
77
|
+
points in U_lst ({Nt})."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if time_series_labels is not None:
|
|
81
|
+
if not isinstance(time_series_labels, list):
|
|
82
|
+
raise TypeError("time_series_labels must be a list of strings.")
|
|
83
|
+
if len(time_series_labels) != len(U_lst):
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"Length of time_series_labels ({len(time_series_labels)})\
|
|
86
|
+
must match the number of time series ({len(U_lst)})."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if line_formats is not None:
|
|
90
|
+
if not isinstance(line_formats, list):
|
|
91
|
+
raise TypeError("line_formats must be a list of strings.")
|
|
92
|
+
if len(line_formats) != len(U_lst):
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"Length of line_formats ({len(line_formats)}) must \
|
|
95
|
+
match the number of time series ({len(U_lst)})."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if state_var_names is not None:
|
|
99
|
+
if not isinstance(state_var_names, list):
|
|
100
|
+
raise TypeError("state_var_names must be a list of strings.")
|
|
101
|
+
if len(state_var_names) != Nu:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"Length of state_var_names ({len(state_var_names)}) \
|
|
104
|
+
must match the number of state variables ({Nu})."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if t_lim is not None and not isinstance(t_lim, int | float):
|
|
108
|
+
raise TypeError("t_lim must be a number (int or float).")
|
|
109
|
+
|
|
110
|
+
# defaults
|
|
111
|
+
plot_kwargs.setdefault("linewidth", 2)
|
|
112
|
+
|
|
113
|
+
# setup time vectors
|
|
114
|
+
if t is None:
|
|
115
|
+
t = jnp.arange(Nt)
|
|
116
|
+
if t_lim is None:
|
|
117
|
+
t_lim = t[-1]
|
|
118
|
+
|
|
119
|
+
# handle optional inputs
|
|
120
|
+
if time_series_labels is None:
|
|
121
|
+
time_series_labels = [None for _ in range(len(U_lst))]
|
|
122
|
+
if line_formats is None:
|
|
123
|
+
line_formats = ["-" for _ in range(len(U_lst))]
|
|
124
|
+
|
|
125
|
+
# plot
|
|
126
|
+
fig, axs = plt.subplots(Nu, figsize=figsize)
|
|
127
|
+
# Ensure axs is always iterable, even if Nu=1
|
|
128
|
+
if Nu == 1:
|
|
129
|
+
axs = [axs]
|
|
130
|
+
for i in range(Nu):
|
|
131
|
+
for j, Y in enumerate(U_lst):
|
|
132
|
+
axs[i].plot(
|
|
133
|
+
t, Y[:, i], line_formats[j], label=time_series_labels[j], **plot_kwargs
|
|
134
|
+
)
|
|
135
|
+
axs[i].set_xlim([0, t_lim])
|
|
136
|
+
if state_var_names is not None:
|
|
137
|
+
axs[i].set(ylabel=state_var_names[i])
|
|
138
|
+
if time_series_labels[0] is not None:
|
|
139
|
+
axs[0].legend(loc="upper right")
|
|
140
|
+
axs[-1].set(xlabel=x_label)
|
|
141
|
+
if title is not None:
|
|
142
|
+
axs[0].set_title(title, fontsize=14)
|
|
143
|
+
plt.show()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def imshow_1D_spatiotemp(
|
|
147
|
+
U, tN, domain=(0, 1), figsize=(20, 6), title=None, x_label=r"$t$", **imshow_kwargs
|
|
148
|
+
):
|
|
149
|
+
"""
|
|
150
|
+
Plot 1D spatiotemporal data using imshow.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
U: 2D array
|
|
155
|
+
Data to be plotted, shape should be (Nt, Nx) where Nt is the number of time
|
|
156
|
+
points and Nx is the number of spatial points
|
|
157
|
+
tN: float
|
|
158
|
+
Final time of the simulation
|
|
159
|
+
domain: tuple of length 2
|
|
160
|
+
Bounds of the spatial domain, default is (0, 1)
|
|
161
|
+
figsize: tuple
|
|
162
|
+
Size of the figure to be created, default is (20, 6)
|
|
163
|
+
title: string, optional
|
|
164
|
+
Title of the plot, if None no title is shown
|
|
165
|
+
x_label: string, optional
|
|
166
|
+
Label for the x-axis, default is r'$t$'
|
|
167
|
+
**imshow_kwargs: additional arguments to pass to imshow
|
|
168
|
+
"""
|
|
169
|
+
# Input validation
|
|
170
|
+
if not isinstance(U, jnp.ndarray | np.ndarray) or U.ndim != 2:
|
|
171
|
+
raise TypeError("U must be a 2D JAX or NumPy array.")
|
|
172
|
+
if not isinstance(domain, tuple) or len(domain) != 2:
|
|
173
|
+
raise TypeError("domain must be a tuple of length 2.")
|
|
174
|
+
if not all(isinstance(x, int | float) for x in domain):
|
|
175
|
+
raise TypeError("Both elements of domain must be numbers (int or float).")
|
|
176
|
+
|
|
177
|
+
# set defaults for imshow
|
|
178
|
+
imshow_kwargs.setdefault("aspect", "auto")
|
|
179
|
+
imshow_kwargs.setdefault("origin", "lower")
|
|
180
|
+
imshow_kwargs.setdefault("cmap", "RdGy")
|
|
181
|
+
imshow_kwargs.setdefault("extent", [0, tN, domain[0], domain[1]])
|
|
182
|
+
|
|
183
|
+
plt.figure(figsize=figsize, dpi=200)
|
|
184
|
+
plt.imshow(U.T, **imshow_kwargs)
|
|
185
|
+
plt.ylabel("x")
|
|
186
|
+
plt.xlabel(x_label)
|
|
187
|
+
if title is not None:
|
|
188
|
+
plt.title(title)
|
|
189
|
+
plt.colorbar(pad=0.01, label=r"$u$")
|
|
190
|
+
plt.show()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# TODO: add plot_attrator function to visualize 2D/3D attractors in state space
|