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/control/base.py ADDED
@@ -0,0 +1,362 @@
1
+ """Defines base classes for Reservoir Computer Controllers."""
2
+
3
+ from abc import ABC
4
+
5
+ import equinox as eqx
6
+ import jax
7
+ import jax.numpy as jnp
8
+ import jax.scipy.optimize
9
+ from jaxtyping import Array, Float
10
+
11
+ from orc.drivers import DriverBase
12
+ from orc.embeddings import EmbedBase
13
+ from orc.readouts import ReadoutBase
14
+
15
+
16
+ class RCControllerBase(eqx.Module, ABC):
17
+ """Base class for reservoir computer controllers.
18
+
19
+ Defines the interface for the reservoir computer controller which includes
20
+ the driver, readout and embedding layers. Unlike the forecaster, the controller
21
+ handles an additional control input at each time step.
22
+
23
+ Attributes
24
+ ----------
25
+ driver : DriverBase
26
+ Driver layer of the reservoir computer.
27
+ readout : ReadoutBase
28
+ Readout layer of the reservoir computer.
29
+ embedding : EmbedBase
30
+ Embedding layer of the reservoir computer. Should accept concatenated
31
+ [input, control] vectors.
32
+ in_dim : int
33
+ Dimension of the system input data.
34
+ control_dim : int
35
+ Dimension of the control input.
36
+ out_dim : int
37
+ Dimension of the output data.
38
+ res_dim : int
39
+ Dimension of the reservoir.
40
+ dtype : type
41
+ Data type of the reservoir computer (jnp.float64 is highly recommended).
42
+ alpha_1 : float
43
+ Weight for trajectory deviation penalty in control optimization.
44
+ alpha_2 : float
45
+ Weight for control magnitude penalty in control optimization.
46
+ alpha_3 : float
47
+ Weight for control derivative penalty in control optimization.
48
+ seed : int
49
+ Random seed for generating the PRNG key for the reservoir computer.
50
+
51
+
52
+ Methods
53
+ -------
54
+ force(in_seq, control_seq, res_state)
55
+ Teacher forces the reservoir with input and control sequences.
56
+ apply_control(control_seq, res_state)
57
+ Apply a predefined control sequence in closed-loop.
58
+ set_readout(readout)
59
+ Replaces the readout layer of the reservoir computer.
60
+ set_embedding(embedding)
61
+ Replaces the embedding layer of the reservoir computer.
62
+ compute_penalty(control_seq, res_state, ref_traj)
63
+ Compute the control penalty for a given control sequence.
64
+ compute_control(control_seq, res_state, ref_traj)
65
+ Compute optimal control sequence to track a reference trajectory.
66
+ """
67
+
68
+ driver: DriverBase
69
+ readout: ReadoutBase
70
+ embedding: EmbedBase
71
+ in_dim: int
72
+ control_dim: int
73
+ out_dim: int
74
+ res_dim: int
75
+ dtype: Float = jnp.float64
76
+ alpha_1: float = 100
77
+ alpha_2: float = 1
78
+ alpha_3: float = 5
79
+ seed: int = 0
80
+
81
+ def __init__(
82
+ self,
83
+ driver: DriverBase,
84
+ readout: ReadoutBase,
85
+ embedding: EmbedBase,
86
+ in_dim: int,
87
+ control_dim: int,
88
+ dtype: Float = jnp.float64,
89
+ alpha_1: float = 100,
90
+ alpha_2: float = 1,
91
+ alpha_3: float = 5,
92
+ seed: int = 0,
93
+ ) -> None:
94
+ """Initialize RCController Base.
95
+
96
+ Parameters
97
+ ----------
98
+ driver : DriverBase
99
+ Driver layer of the reservoir computer.
100
+ readout : ReadoutBase
101
+ Readout layer of the reservoir computer.
102
+ embedding : EmbedBase
103
+ Embedding layer of the reservoir computer.
104
+ in_dim : int
105
+ Dimension of the system input data.
106
+ control_dim : int
107
+ Dimension of the control input.
108
+ dtype : type
109
+ Data type of the reservoir computer (jnp.float64 is highly recommended).
110
+ alpha_1 : float
111
+ Weight for trajectory deviation penalty in control optimization.
112
+ alpha_2 : float
113
+ Weight for control magnitude penalty in control optimization.
114
+ alpha_3 : float
115
+ Weight for control derivative penalty in control optimization.
116
+ seed : int
117
+ Random seed for generating the PRNG key for the reservoir computer.
118
+ """
119
+ self.driver = driver
120
+ self.readout = readout
121
+ self.embedding = embedding
122
+ self.in_dim = in_dim
123
+ self.control_dim = control_dim
124
+ self.out_dim = self.readout.out_dim
125
+ self.res_dim = self.driver.res_dim
126
+ self.dtype = dtype
127
+ self.alpha_1 = alpha_1
128
+ self.alpha_2 = alpha_2
129
+ self.alpha_3 = alpha_3
130
+ self.seed = seed
131
+
132
+ @eqx.filter_jit
133
+ def force(self, in_seq: Array, control_seq: Array, res_state: Array) -> Array:
134
+ """Teacher forces the reservoir with input and control sequences.
135
+
136
+ Parameters
137
+ ----------
138
+ in_seq: Array
139
+ Input sequence to force the reservoir, (shape=(seq_len, in_dim)).
140
+ control_seq: Array
141
+ Control sequence to force the reservoir, (shape=(seq_len, control_dim)).
142
+ res_state : Array
143
+ Initial reservoir state, (shape=(res_dim,)).
144
+
145
+ Returns
146
+ -------
147
+ Array
148
+ Forced reservoir sequence, (shape=(seq_len, res_dim)).
149
+ """
150
+
151
+ def scan_fn(state, in_vars):
152
+ in_state, control_state = in_vars
153
+ # Concatenate input and control for embedding
154
+ combined_input = jnp.concatenate([in_state, control_state])
155
+ proj_vars = self.embedding.embed(combined_input)
156
+ res_state = self.driver.advance(proj_vars, state)
157
+ return (res_state, res_state)
158
+
159
+ _, res_seq = jax.lax.scan(scan_fn, res_state, (in_seq, control_seq))
160
+ return res_seq
161
+
162
+ def __call__(self, in_seq: Array, control_seq: Array, res_state: Array) -> Array:
163
+ """Teacher forces the reservoir, wrapper for `force` method.
164
+
165
+ Parameters
166
+ ----------
167
+ in_seq: Array
168
+ Input sequence to force the reservoir, (shape=(seq_len, in_dim)).
169
+ control_seq: Array
170
+ Control sequence to force the reservoir, (shape=(seq_len, control_dim)).
171
+ res_state : Array
172
+ Initial reservoir state, (shape=(res_dim,)).
173
+
174
+ Returns
175
+ -------
176
+ Array
177
+ Forced reservoir sequence, (shape=(seq_len, res_dim)).
178
+ """
179
+ return self.force(in_seq, control_seq, res_state)
180
+
181
+ @eqx.filter_jit
182
+ def apply_control(
183
+ self, control_seq: Array, res_state: Array
184
+ ) -> tuple[Array, Array]:
185
+ """Apply a predefined control sequence in closed-loop.
186
+
187
+ The readout feeds back as the next input: u(t+1) = readout(x(t)).
188
+ Control c(t) comes from the provided control_seq.
189
+
190
+ Parameters
191
+ ----------
192
+ control_seq : Array
193
+ Control sequence to apply, (shape=(fcast_len, control_dim)).
194
+ res_state : Array
195
+ Initial reservoir state, (shape=(res_dim,)).
196
+
197
+ Returns
198
+ -------
199
+ Array
200
+ Controlled output trajectory with shape=(fcast_len, out_dim)).
201
+ """
202
+
203
+ def scan_fn(state, control_vars):
204
+ # Get output from current reservoir state
205
+ out_state = self.readout(state)
206
+ # Concatenate output (as next input) with control
207
+ combined_input = jnp.concatenate([out_state, control_vars])
208
+ # Embed and advance reservoir
209
+ proj_vars = self.embedding(combined_input)
210
+ next_res_state = self.driver(proj_vars, state)
211
+ return (next_res_state, self.readout(next_res_state))
212
+
213
+ res_state, state_seq = jax.lax.scan(scan_fn, res_state, control_seq)
214
+ return state_seq
215
+
216
+ def set_readout(self, readout: ReadoutBase) -> "RCControllerBase":
217
+ """Replace readout layer.
218
+
219
+ Parameters
220
+ ----------
221
+ readout : ReadoutBase
222
+ New readout layer.
223
+
224
+ Returns
225
+ -------
226
+ RCControllerBase
227
+ Updated model with new readout layer.
228
+ """
229
+
230
+ def where(m: "RCControllerBase"):
231
+ return m.readout
232
+
233
+ new_model = eqx.tree_at(where, self, readout)
234
+ return new_model
235
+
236
+ def set_embedding(self, embedding: EmbedBase) -> "RCControllerBase":
237
+ """Replace embedding layer.
238
+
239
+ Parameters
240
+ ----------
241
+ embedding : EmbedBase
242
+ New embedding layer.
243
+
244
+ Returns
245
+ -------
246
+ RCControllerBase
247
+ Updated model with new embedding layer.
248
+ """
249
+
250
+ def where(m: "RCControllerBase"):
251
+ return m.embedding
252
+
253
+ new_model = eqx.tree_at(where, self, embedding)
254
+ return new_model
255
+
256
+ def compute_penalty(
257
+ self,
258
+ control_seq: Array,
259
+ res_state: Array,
260
+ ref_traj: Array,
261
+ ) -> Float:
262
+ """Compute the control penalty for a given control sequence.
263
+
264
+ The penalty consists of three terms:
265
+ - Deviation penalty: squared error between forecast and reference trajectory
266
+ - Magnitude penalty: squared norm of control inputs
267
+ - Derivative penalty: squared norm of control input differences
268
+
269
+ Parameters
270
+ ----------
271
+ control_seq : Array
272
+ Control sequence to evaluate, (shape=(fcast_len, control_dim)).
273
+ res_state : Array
274
+ Initial reservoir state, (shape=(res_dim,)).
275
+ ref_traj : Array
276
+ Reference trajectory to track, (shape=(fcast_len, out_dim)).
277
+
278
+ Returns
279
+ -------
280
+ Float
281
+ Total penalty value (scalar).
282
+ """
283
+ fcast = self.apply_control(control_seq, res_state)
284
+ deviation = fcast - ref_traj
285
+ dev_penalty = jnp.sum(deviation**2) * self.alpha_1
286
+ mag_penalty = jnp.sum(control_seq**2) * self.alpha_2
287
+ deriv_penalty = jnp.sum(jnp.diff(control_seq, axis=0) ** 2) * self.alpha_3
288
+ return dev_penalty + mag_penalty + deriv_penalty
289
+
290
+ @eqx.filter_jit
291
+ def compute_control(
292
+ self,
293
+ control_seq: Array,
294
+ res_state: Array,
295
+ ref_traj: Array,
296
+ ) -> Array:
297
+ """Compute optimal control sequence to track a reference trajectory.
298
+
299
+ Uses BFGS optimization to find a control sequence that minimizes the
300
+ penalty function (deviation from reference + control magnitude + control
301
+ smoothness).
302
+
303
+ Parameters
304
+ ----------
305
+ control_seq : Array
306
+ Initial guess for control sequence, (shape=(fcast_len, control_dim)).
307
+ res_state : Array
308
+ Initial reservoir state, (shape=(res_dim,)).
309
+ ref_traj : Array
310
+ Reference trajectory to track, (shape=(fcast_len, out_dim)).
311
+
312
+ Returns
313
+ -------
314
+ Array
315
+ Optimized control sequence, (shape=(fcast_len, control_dim)).
316
+ """
317
+
318
+ def loss_fn(control_seq):
319
+ control_seq = control_seq.reshape(-1, self.control_dim)
320
+ return self.compute_penalty(control_seq, res_state, ref_traj)
321
+
322
+ # TODO: Implement optimization to allow finer grained control of tolerances
323
+ # linesearch = optax.scale_by_backtracking_linesearch(
324
+ # max_backtracking_steps=30,
325
+ # decrease_factor=0.5,
326
+ # )
327
+ # solver = optax.lbfgs(linesearch=linesearch)
328
+
329
+ # if not use_builtin_solver:
330
+ # solver = optax.lbfgs()
331
+
332
+ # @jax.jit
333
+ # def run_lbfgs(x0):
334
+ # value_and_grad_fn = jax.value_and_grad(loss_fn)
335
+
336
+ # def step(carry, _):
337
+ # x, state = carry
338
+ # value, grad = value_and_grad_fn(x)
339
+ # updates, state = solver.update(
340
+ # grad,
341
+ # state,
342
+ # x,
343
+ # value=value,
344
+ # grad=grad,
345
+ # value_fn=loss_fn)
346
+ # x = optax.apply_updates(x, updates)
347
+ # return (x, state), None
348
+
349
+ # init_state = solver.init(x0)
350
+ # (x_final, final_state), _ = jax.lax.scan(step, (x0, init_state),
351
+ # None,
352
+ # length=max_iter)
353
+ # return x_final, final_state
354
+
355
+ # control_opt, state = run_lbfgs(control_seq)
356
+
357
+ optimize_results = jax.scipy.optimize.minimize(
358
+ loss_fn, control_seq.reshape(-1), method="BFGS"
359
+ )
360
+ control_opt = optimize_results.x.reshape(-1, self.control_dim)
361
+
362
+ return control_opt
orc/control/models.py ADDED
@@ -0,0 +1,139 @@
1
+ """Discrete ESN controller implementation."""
2
+
3
+ import jax
4
+ import jax.numpy as jnp
5
+
6
+ from orc.control.base import RCControllerBase
7
+ from orc.drivers import ESNDriver
8
+ from orc.embeddings import LinearEmbedding
9
+ from orc.readouts import LinearReadout, QuadraticReadout
10
+
11
+ jax.config.update("jax_enable_x64", True)
12
+
13
+
14
+ class ESNController(RCControllerBase):
15
+ """
16
+ Basic implementation of ESN for control tasks.
17
+
18
+ Attributes
19
+ ----------
20
+ res_dim : int
21
+ Reservoir dimension.
22
+ data_dim : int
23
+ System input/output dimension.
24
+ control_dim : int
25
+ Control input dimension.
26
+ driver : ESNDriver
27
+ Driver implementing the Echo State Network dynamics.
28
+ readout : ReadoutBase
29
+ Trainable linear readout layer.
30
+ embedding : LinearEmbedding
31
+ Untrainable linear embedding layer for [input, control] concatenation.
32
+
33
+ Methods
34
+ -------
35
+ force(in_seq, control_seq, res_state)
36
+ Teacher forces the reservoir with input and control sequences.
37
+ apply_control(control_seq, fcast_len, res_state)
38
+ Apply a predefined control sequence in closed-loop.
39
+ set_readout(readout)
40
+ Replace readout layer.
41
+ set_embedding(embedding)
42
+ Replace embedding layer.
43
+ """
44
+
45
+ res_dim: int
46
+ data_dim: int
47
+ control_dim: int
48
+
49
+ def __init__(
50
+ self,
51
+ data_dim: int,
52
+ control_dim: int,
53
+ res_dim: int,
54
+ leak_rate: float = 0.6,
55
+ bias: float = 1.6,
56
+ embedding_scaling: float = 0.08,
57
+ Wr_density: float = 0.02,
58
+ Wr_spectral_radius: float = 0.8,
59
+ dtype: type = jnp.float64,
60
+ seed: int = 0,
61
+ quadratic: bool = False,
62
+ use_sparse_eigs: bool = True,
63
+ ) -> None:
64
+ """
65
+ Initialize the ESN controller.
66
+
67
+ Parameters
68
+ ----------
69
+ data_dim : int
70
+ Dimension of the system input/output data.
71
+ control_dim : int
72
+ Dimension of the control input.
73
+ res_dim : int
74
+ Dimension of the reservoir adjacency matrix Wr.
75
+ leak_rate : float
76
+ Integration leak rate of the reservoir dynamics.
77
+ bias : float
78
+ Bias term for the reservoir dynamics.
79
+ embedding_scaling : float
80
+ Scaling factor for the embedding layer.
81
+ Wr_density : float
82
+ Density of the reservoir adjacency matrix Wr.
83
+ Wr_spectral_radius : float
84
+ Largest eigenvalue of the reservoir adjacency matrix Wr.
85
+ dtype : type
86
+ Data type of the model (jnp.float64 is highly recommended).
87
+ seed : int
88
+ Random seed for generating the PRNG key for the reservoir computer.
89
+ quadratic : bool
90
+ Use quadratic nonlinearity in output, default False.
91
+ use_sparse_eigs : bool
92
+ Whether to use sparse eigensolver for setting the spectral radius of wr.
93
+ Default is True, which is recommended to save memory and compute time. If
94
+ False, will use dense eigensolver which may be more accurate.
95
+ """
96
+ # Initialize the random key and reservoir dimension
97
+ self.res_dim = res_dim
98
+ self.seed = seed
99
+ self.data_dim = data_dim
100
+ self.control_dim = control_dim
101
+ key = jax.random.PRNGKey(seed)
102
+ key_driver, key_readout, key_embedding = jax.random.split(key, 3)
103
+
104
+ # Embedding accepts concatenated [input, control] vectors
105
+ # Total input dimension is data_dim + control_dim
106
+ embedding = LinearEmbedding(
107
+ in_dim=data_dim + control_dim,
108
+ res_dim=res_dim,
109
+ seed=key_embedding[0],
110
+ scaling=embedding_scaling,
111
+ )
112
+ driver = ESNDriver(
113
+ res_dim=res_dim,
114
+ seed=key_driver[0],
115
+ leak=leak_rate,
116
+ bias=bias,
117
+ density=Wr_density,
118
+ spectral_radius=Wr_spectral_radius,
119
+ dtype=dtype,
120
+ use_sparse_eigs=use_sparse_eigs,
121
+ )
122
+ if quadratic:
123
+ readout = QuadraticReadout(
124
+ out_dim=data_dim, res_dim=res_dim, seed=key_readout[0]
125
+ )
126
+ else:
127
+ readout = LinearReadout(
128
+ out_dim=data_dim, res_dim=res_dim, seed=key_readout[0]
129
+ )
130
+
131
+ super().__init__(
132
+ driver=driver,
133
+ readout=readout,
134
+ embedding=embedding,
135
+ in_dim=data_dim,
136
+ control_dim=control_dim,
137
+ dtype=dtype,
138
+ seed=seed,
139
+ )
orc/control/train.py ADDED
@@ -0,0 +1,90 @@
1
+ """Training functions for reservoir computer controllers."""
2
+
3
+ import equinox as eqx
4
+ import jax.numpy as jnp
5
+ from jaxtyping import Array
6
+
7
+ from orc.control.models import ESNController
8
+ from orc.readouts import NonlinearReadout
9
+ from orc.utils.regressions import ridge_regression
10
+
11
+
12
+ def train_ESNController(
13
+ model: ESNController,
14
+ train_seq: Array,
15
+ control_seq: Array,
16
+ target_seq: Array = None,
17
+ spinup: int = 0,
18
+ initial_res_state: Array = None,
19
+ beta: float = 8e-8,
20
+ ) -> tuple[ESNController, Array]:
21
+ """Training function for ESNController.
22
+
23
+ Parameters
24
+ ----------
25
+ model : ESNController
26
+ ESNController model to train.
27
+ train_seq : Array
28
+ Training input sequence for reservoir, (shape=(seq_len, data_dim)).
29
+ control_seq : Array
30
+ Control input sequence for reservoir, (shape=(seq_len, control_dim)).
31
+ target_seq : Array
32
+ Target sequence for training reservoir, (shape=(seq_len, data_dim)).
33
+ If None, defaults to train_seq[1:].
34
+ initial_res_state : Array
35
+ Initial reservoir state, (shape=(res_dim,)).
36
+ spinup : int
37
+ Initial transient of reservoir states to discard.
38
+ beta : float
39
+ Tikhonov regularization parameter.
40
+
41
+ Returns
42
+ -------
43
+ model : ESNController
44
+ Trained ESN controller model.
45
+ res_seq : Array
46
+ Training sequence of reservoir states.
47
+ """
48
+ # Check that model is an ESN Controller
49
+ if not isinstance(model, ESNController):
50
+ raise TypeError("Model must be an ESNController.")
51
+
52
+ # check that train_seq and control_seq have the same length
53
+ if train_seq.shape[0] != control_seq.shape[0]:
54
+ raise ValueError("train_seq and control_seq must have the same length.")
55
+
56
+ # check that spinup is less than the length of the training sequence
57
+ if spinup >= train_seq.shape[0]:
58
+ raise ValueError(
59
+ "spinup must be less than the length of the training sequence."
60
+ )
61
+
62
+ if initial_res_state is None:
63
+ initial_res_state = jnp.zeros(model.res_dim, dtype=model.dtype)
64
+
65
+ if target_seq is None:
66
+ tot_seq = train_seq
67
+ tot_control_seq = control_seq
68
+ target_seq = train_seq[1:, :]
69
+ train_seq = train_seq[:-1, :]
70
+ control_seq = control_seq[:-1, :]
71
+ else:
72
+ tot_seq = jnp.vstack((train_seq, target_seq[-1:]))
73
+ tot_control_seq = control_seq
74
+
75
+ tot_res_seq = model.force(tot_seq, tot_control_seq, initial_res_state)
76
+ res_seq = tot_res_seq[:-1]
77
+ if isinstance(model.readout, NonlinearReadout):
78
+ res_seq_train = model.readout.nonlinear_transform(res_seq)
79
+ else:
80
+ res_seq_train = res_seq
81
+
82
+ cmat = ridge_regression(res_seq_train[spinup:], target_seq[spinup:], beta)
83
+ cmat = cmat.reshape(1, cmat.shape[0], cmat.shape[1])
84
+
85
+ def where(m):
86
+ return m.readout.wout
87
+
88
+ model = eqx.tree_at(where, model, cmat)
89
+
90
+ return model, tot_res_seq
orc/data/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """Implementations of data generation and handling functions."""
2
+
3
+ from orc.data import integrators
4
+ from orc.data.integrators import (
5
+ KS_1D,
6
+ colpitts,
7
+ double_pendulum,
8
+ hyper_lorenz63,
9
+ hyper_xu,
10
+ lorenz63,
11
+ lorenz96,
12
+ rossler,
13
+ sakaraya,
14
+ )
15
+
16
+ __all__ = [
17
+ "integrators",
18
+ "lorenz63",
19
+ "rossler",
20
+ "sakaraya",
21
+ "colpitts",
22
+ "hyper_lorenz63",
23
+ "hyper_xu",
24
+ "double_pendulum",
25
+ "lorenz96",
26
+ "KS_1D",
27
+ ]