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/embeddings.py ADDED
@@ -0,0 +1,459 @@
1
+ """Define base class for embedding layers and implement common architectures."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ import equinox as eqx
6
+ import jax
7
+ import jax.numpy as jnp
8
+ from jaxtyping import Array, Float
9
+
10
+ jax.config.update("jax_enable_x64", True)
11
+
12
+
13
+ class EmbedBase(eqx.Module, ABC):
14
+ """
15
+ Base class dictating API for all implemented embedding layers.
16
+
17
+ Attributes
18
+ ----------
19
+ in_dim : int
20
+ Input dimension.
21
+ res_dim : int
22
+ Reservoir dimension.
23
+ dtype : Float
24
+ Dtype of JAX arrays, jnp.float32 or jnp.float64.
25
+
26
+ Methods
27
+ -------
28
+ embed(in_state)
29
+ Embed input into reservoir dimension.
30
+ batch_embed(in_state)
31
+ Embed multiple inputs into reservoir dimension.
32
+ """
33
+
34
+ in_dim: int
35
+ res_dim: int
36
+ dtype: Float
37
+
38
+ def __init__(self, in_dim, res_dim, dtype=jnp.float64):
39
+ """Ensure in dim, res dim, and dtype are correct type."""
40
+ self.res_dim = res_dim
41
+ self.in_dim = in_dim
42
+ self.dtype = dtype
43
+ if not isinstance(res_dim, int):
44
+ raise TypeError("Reservoir dimension res_dim must be an integer.")
45
+ if not isinstance(in_dim, int):
46
+ raise TypeError("Reservoir dimension res_dim must be an integer.")
47
+ self.dtype = dtype
48
+ if not (dtype == jnp.float64 or dtype == jnp.float32):
49
+ raise TypeError("dtype must be jnp.float64 of jnp.float32.")
50
+
51
+ @abstractmethod
52
+ def embed(
53
+ self,
54
+ in_state: Array,
55
+ ) -> Array:
56
+ """Embed input signal to reservoir dimension.
57
+
58
+ Parameters
59
+ ----------
60
+ in_state : Array
61
+ Input state, (shape=(in_dim,)).
62
+
63
+ Returns
64
+ -------
65
+ Array
66
+ Embedded input state to reservoir dimension.
67
+ """
68
+ pass
69
+
70
+ @eqx.filter_jit
71
+ def batch_embed(
72
+ self,
73
+ in_state: Array,
74
+ ) -> Array:
75
+ """Batch apply embedding from input states.
76
+
77
+ Parameters
78
+ ----------
79
+ in_state : Array
80
+ Input states.
81
+
82
+ Returns
83
+ -------
84
+ Array
85
+ Embedded input states to reservoir, (shape=(batch_dim, res_dim,)).
86
+ """
87
+ return eqx.filter_vmap(self.embed)(in_state)
88
+
89
+ def __call__(
90
+ self,
91
+ in_state: Array,
92
+ ) -> Array:
93
+ """Embed state to reservoir dimensions.
94
+
95
+ Parameters
96
+ ----------
97
+ in_state : Array
98
+ Input state, (shape=(in_dim,) or shape=(seq_len, in_dim)).
99
+
100
+ Returns
101
+ -------
102
+ Array
103
+ Embedded input to reservoir, (shape=(chunks, res_dim,) or
104
+ shape=(seq_len, chunks, res_dim)).
105
+ """
106
+ if len(in_state.shape) == 1:
107
+ to_ret = self.embed(in_state)
108
+ elif len(in_state.shape) == 2:
109
+ to_ret = self.batch_embed(in_state)
110
+ return to_ret
111
+
112
+
113
+ class ParallelLinearEmbedding(EmbedBase):
114
+ """Linear embedding layer.
115
+
116
+ Attributes
117
+ ----------
118
+ in_dim : int
119
+ Reservoir input dimension.
120
+ res_dim : int
121
+ Reservoir dimension.
122
+ scaling : float
123
+ Min/max values of input matrix.
124
+ win : Array
125
+ Input matrix.
126
+ chunks : int
127
+ Number of parallel reservoirs.
128
+ locality : int
129
+ Adjacent reservoir overlap.
130
+ periodic : bool
131
+ Assume periodic BCs when decomposing the input state to parallel network
132
+ inputs. If False, the input is padded with boundary values at the edges
133
+ (i.e., edge values are extended to the locality region), which may not
134
+ match the true spatial dynamics. If True, the input is padded by connecting
135
+ smoothly the end and beginning of the signal ensuring continuity. Default is
136
+ True.
137
+
138
+ Methods
139
+ -------
140
+ __call__(in_state)
141
+ Embed input state to reservoir dimension.
142
+ localize(in_state, periodic=True)
143
+ Decompose input_state to parallel network inputs.
144
+ moving_window(a)
145
+ Helper function for localize.
146
+ embed(in_state)
147
+ Embed single input state to reservoir dimension.
148
+ """
149
+
150
+ in_dim: int
151
+ res_dim: int
152
+ scaling: float
153
+ win: Array
154
+ dtype: Float
155
+ chunks: int
156
+ locality: int
157
+ chunk_size: int
158
+ periodic: bool
159
+
160
+ def __init__(
161
+ self,
162
+ in_dim: int,
163
+ res_dim: int,
164
+ scaling: float,
165
+ dtype: Float = jnp.float64,
166
+ chunks: int = 1,
167
+ locality: int = 0,
168
+ periodic: bool = True,
169
+ *,
170
+ seed: int,
171
+ ) -> None:
172
+ """Instantiate linear embedding.
173
+
174
+ Parameters
175
+ ----------
176
+ in_dim : int
177
+ Input dimension to reservoir.
178
+ res_dim : int
179
+ Reservoir dimension.
180
+ scaling : float
181
+ Min/max values of input matrix.
182
+ seed : int
183
+ Random seed for generating the PRNG key for the reservoir computer.
184
+ dtype : Float
185
+ Dtype of model, jnp.float64 or jnp.float32.
186
+ periodic : bool
187
+ Assume periodic BCs when decomposing the input state to parallel network
188
+ inputs. If False, the input is padded with boundary values at the edges
189
+ (i.e., edge values are extended to the locality region), which may not
190
+ match the true spatial dynamics. If True, the input is padded by connecting
191
+ smoothly the end and beginning of the signal ensuring continuity. Default is
192
+ True.
193
+ """
194
+ super().__init__(in_dim=in_dim, res_dim=res_dim, dtype=dtype)
195
+ self.scaling = scaling
196
+ self.dtype = dtype
197
+ key = jax.random.key(seed)
198
+ self.chunk_size = int(in_dim / chunks)
199
+
200
+ if in_dim % chunks:
201
+ raise ValueError(
202
+ f"The number of chunks {chunks} must evenly divide in_dim {in_dim}."
203
+ )
204
+
205
+ self.win = jax.random.uniform(
206
+ key,
207
+ (chunks, res_dim, self.chunk_size + 2 * locality),
208
+ minval=-scaling,
209
+ maxval=scaling,
210
+ dtype=dtype,
211
+ )
212
+ self.locality = locality
213
+ self.chunks = chunks
214
+ self.periodic = periodic
215
+
216
+ @eqx.filter_jit
217
+ def moving_window(self, a):
218
+ """Generate window to compute localized states."""
219
+ size = int(self.in_dim / self.chunks + 2 * self.locality)
220
+ starts = jnp.arange(len(a) - size + 1)[: self.chunks] * int(
221
+ self.in_dim / self.chunks
222
+ )
223
+ return eqx.filter_vmap(
224
+ lambda start: jax.lax.dynamic_slice(a, (start,), (size,))
225
+ )(starts)
226
+
227
+ @eqx.filter_jit
228
+ def localize(self, in_state: Array) -> Array:
229
+ """Generate parallel reservoir inputs from input state.
230
+
231
+ Parameters
232
+ ----------
233
+ in_state : Array
234
+ Input state, (shape=(in_dim,))
235
+
236
+ Returns
237
+ -------
238
+ Array
239
+ Parallel reservoir inputs, (shape=(chunks, chunk_size + 2*locality))
240
+ """
241
+ if len(in_state.shape) != 1:
242
+ raise ValueError(
243
+ "Only 1-dimensional localization is currently supported, detected a "
244
+ f"{len(in_state.shape)}D field."
245
+ )
246
+ aug_state = jnp.hstack(
247
+ [in_state[-self.locality :], in_state, in_state[: self.locality]]
248
+ )
249
+ if not self.periodic:
250
+ aug_state = aug_state.at[: self.locality].set(aug_state[self.locality])
251
+ aug_state = aug_state.at[-self.locality :].set(aug_state[-self.locality])
252
+ return self.moving_window(aug_state)
253
+
254
+ @eqx.filter_jit
255
+ def embed(self, in_state: Array) -> Array:
256
+ """Embed single state to reservoir dimensions.
257
+
258
+ Parameters
259
+ ----------
260
+ in_state : Array
261
+ Input state, (shape=(in_dim,)).
262
+
263
+ Returns
264
+ -------
265
+ Array
266
+ Embedded input to reservoir, (shape=(chunks, res_dim,)).
267
+ """
268
+ if in_state.shape != (self.in_dim,):
269
+ raise ValueError("Incorrect dimension for input state.")
270
+ localized_states = self.localize(in_state)
271
+
272
+ return eqx.filter_vmap(jnp.matmul)(self.win, localized_states)
273
+
274
+
275
+ class LinearEmbedding(ParallelLinearEmbedding):
276
+ """Linear embedding layer.
277
+
278
+ Attributes
279
+ ----------
280
+ in_dim : int
281
+ Reservoir input dimension.
282
+ res_dim : int
283
+ Reservoir dimension.
284
+ scaling : float
285
+ Min/max values of input matrix.
286
+ win : Array
287
+ Input matrix.
288
+
289
+ Methods
290
+ -------
291
+ __call__(in_state)
292
+ Embed input state to reservoir dimension.
293
+ embed(in_state)
294
+ Embed single input state to reservoir dimension.
295
+ """
296
+
297
+ def __init__(
298
+ self,
299
+ in_dim: int,
300
+ res_dim: int,
301
+ scaling: float,
302
+ dtype: Float = jnp.float64,
303
+ *,
304
+ seed: int,
305
+ ) -> None:
306
+ """Instantiate linear embedding.
307
+
308
+ Parameters
309
+ ----------
310
+ in_dim : int
311
+ Input dimension to reservoir.
312
+ res_dim : int
313
+ Reservoir dimension.
314
+ scaling : float
315
+ Min/max values of input matrix.
316
+ seed : int
317
+ Random seed for generating the PRNG key for the reservoir computer.
318
+ dtype : Float
319
+ Dtype of model, jnp.float64 or jnp.float32.
320
+ periodic : bool
321
+ Assume periodic BCs when decomposing the input state to parallel network
322
+ inputs. If False, the input is padded with boundary values at the edges
323
+ (i.e., edge values are extended to the locality region), which may not
324
+ match the true spatial dynamics. If True, the input is padded by connecting
325
+ smoothly the end and beginning of the signal ensuring continuity. Default is
326
+ True.
327
+ """
328
+ super().__init__(
329
+ in_dim=in_dim,
330
+ res_dim=res_dim,
331
+ scaling=scaling,
332
+ dtype=dtype,
333
+ chunks=1,
334
+ locality=0,
335
+ periodic=True,
336
+ seed=seed,
337
+ )
338
+
339
+ @eqx.filter_jit
340
+ def embed(self, in_state: Array) -> Array:
341
+ """Embed single state to reservoir dimensions.
342
+
343
+ Parameters
344
+ ----------
345
+ in_state : Array
346
+ Input state, (shape=(in_dim,)).
347
+
348
+ Returns
349
+ -------
350
+ Array
351
+ Embedded input to reservoir, (shape=(res_dim,)).
352
+ """
353
+ return super().embed(in_state).reshape(-1)
354
+
355
+ def __call__(self, in_state: Array) -> Array:
356
+ """Embed state to reservoir dimensions.
357
+
358
+ Parameters
359
+ ----------
360
+ in_state : Array
361
+ Input state, (shape=(in_dim,) or shape=(seq_len, in_dim)).
362
+
363
+ Returns
364
+ -------
365
+ Array
366
+ Embedded input to reservoir, (shape=(res_dim,) or
367
+ shape=(seq_len, res_dim)).
368
+ """
369
+ return jnp.squeeze(super().__call__(in_state))
370
+
371
+
372
+ class EnsembleLinearEmbedding(EmbedBase):
373
+ """Ensemble linear embedding layer.
374
+
375
+ Attributes
376
+ ----------
377
+ in_dim : int
378
+ Reservoir input dimension.
379
+ res_dim : int
380
+ Reservoir dimension.
381
+ scaling : float
382
+ Min/max values of input matrix.
383
+ win : Array
384
+ Input matrix.
385
+ chunks : int
386
+ Number of parallel reservoirs.
387
+
388
+ Methods
389
+ -------
390
+ __call__(in_state)
391
+ Embed input state to reservoir dimension.
392
+ embed(in_state)
393
+ Embed single input state to reservoir dimension.
394
+ """
395
+
396
+ in_dim: int
397
+ res_dim: int
398
+ scaling: float
399
+ win: Array
400
+ dtype: Float
401
+ chunks: int
402
+
403
+ def __init__(
404
+ self,
405
+ in_dim: int,
406
+ res_dim: int,
407
+ scaling: float,
408
+ chunks: int = 5,
409
+ dtype: Float = jnp.float64,
410
+ *,
411
+ seed: int,
412
+ ) -> None:
413
+ """Instantiate linear embedding.
414
+
415
+ Parameters
416
+ ----------
417
+ in_dim : int
418
+ Input dimension to reservoir.
419
+ res_dim : int
420
+ Reservoir dimension.
421
+ scaling : float
422
+ Min/max values of input matrix.
423
+ seed : int
424
+ Random seed for generating the PRNG key for the reservoir computer.
425
+ dtype : Float
426
+ Dtype of model, jnp.float64 or jnp.float32.
427
+ """
428
+ super().__init__(in_dim=in_dim, res_dim=res_dim, dtype=dtype)
429
+ self.scaling = scaling
430
+ self.dtype = dtype
431
+ key = jax.random.key(seed)
432
+
433
+ self.win = jax.random.uniform(
434
+ key,
435
+ (chunks, res_dim, in_dim),
436
+ minval=-scaling,
437
+ maxval=scaling,
438
+ dtype=dtype,
439
+ )
440
+ self.chunks = chunks
441
+
442
+ @eqx.filter_jit
443
+ def embed(self, in_state: Array) -> Array:
444
+ """Embed single state to reservoir dimensions.
445
+
446
+ Parameters
447
+ ----------
448
+ in_state : Array
449
+ Input state, (shape=(in_dim,)).
450
+
451
+ Returns
452
+ -------
453
+ Array
454
+ Embedded input to reservoir, (shape=(chunks, res_dim,)).
455
+ """
456
+ if in_state.shape != (self.in_dim,):
457
+ raise ValueError("Incorrect dimension for input state.")
458
+
459
+ return self.win @ in_state
@@ -0,0 +1,20 @@
1
+ """Forecasting with Reservoir Computers."""
2
+
3
+ from orc.forecaster.base import CRCForecasterBase, RCForecasterBase
4
+ from orc.forecaster.models import CESNForecaster, EnsembleESNForecaster, ESNForecaster
5
+ from orc.forecaster.train import (
6
+ train_CESNForecaster,
7
+ train_EnsembleESNForecaster,
8
+ train_ESNForecaster,
9
+ )
10
+
11
+ __all__ = [
12
+ "RCForecasterBase",
13
+ "CRCForecasterBase",
14
+ "ESNForecaster",
15
+ "CESNForecaster",
16
+ "EnsembleESNForecaster",
17
+ "train_ESNForecaster",
18
+ "train_CESNForecaster",
19
+ "train_EnsembleESNForecaster",
20
+ ]