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.
orc/tuning/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Hyperparameter tuning utilities for Reservoir Computers.
2
+
3
+ This module is currently a placeholder for future tuning implementations.
4
+ """
5
+
6
+ __all__ = []
orc/utils/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Common utility functions for numerics and visualization."""
2
+
3
+ from orc.utils import numerics, regressions, visualization
4
+ from orc.utils.numerics import max_eig_arnoldi
5
+
6
+ __all__ = ["numerics", "max_eig_arnoldi", "visualization", "regressions"]
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
@@ -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