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/readouts.py ADDED
@@ -0,0 +1,742 @@
1
+ """Define base class for readout layers and implement common architectures."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Callable
5
+
6
+ import equinox as eqx
7
+ import jax.numpy as jnp
8
+ from jaxtyping import Array, Float
9
+
10
+
11
+ class ReadoutBase(eqx.Module, ABC):
12
+ """
13
+ Base class dictating API for all implemented readout layers.
14
+
15
+ Attributes
16
+ ----------
17
+ out_dim : int
18
+ Dimension of reservoir output.
19
+ res_dim : int
20
+ Reservoir dimension.
21
+ dtype : Float
22
+ Dtype of JAX arrays, jnp.float32 or jnp.float64.
23
+
24
+ Methods
25
+ -------
26
+ readout(res_state)
27
+ Map from reservoir state to output state.
28
+ batch_readout(res_state)
29
+ Map from reservoir states to output states.
30
+ """
31
+
32
+ out_dim: int
33
+ res_dim: int
34
+ dtype: Float
35
+
36
+ def __init__(self, out_dim, res_dim, dtype=jnp.float64):
37
+ """Ensure in dim, res dim, and dtype are correct type."""
38
+ self.res_dim = res_dim
39
+ self.out_dim = out_dim
40
+ self.dtype = dtype
41
+ if not isinstance(res_dim, int):
42
+ raise TypeError("Reservoir dimension res_dim must be an integer.")
43
+ if not isinstance(out_dim, int):
44
+ raise TypeError("Reservoir dimension res_dim must be an integer.")
45
+ self.dtype = dtype
46
+ if not (dtype == jnp.float64 or dtype == jnp.float32):
47
+ raise TypeError("dtype must be jnp.float64 of jnp.float32.")
48
+
49
+ @abstractmethod
50
+ def readout(
51
+ self,
52
+ res_state: Array,
53
+ ) -> Array:
54
+ """Readout from reservoir state.
55
+
56
+ Parameters
57
+ ----------
58
+ res_state : Array
59
+ Reservoir state.
60
+
61
+ Returns
62
+ -------
63
+ Array
64
+ Output from reservoir state.
65
+ """
66
+ pass
67
+
68
+ def batch_readout(
69
+ self,
70
+ res_state: Array,
71
+ ) -> Array:
72
+ """Batch apply readout from reservoir states.
73
+
74
+ Parameters
75
+ ----------
76
+ res_state : Array
77
+ Reservoir state.
78
+
79
+ Returns
80
+ -------
81
+ Array
82
+ Output from reservoir states.
83
+ """
84
+ return eqx.filter_vmap(self.readout)(res_state)
85
+
86
+ def __call__(
87
+ self,
88
+ res_state: Array,
89
+ ) -> Array:
90
+ """Call either readout or batch_readout depending on dimensions.
91
+
92
+ Parameters
93
+ ----------
94
+ res_state : Array
95
+ Reservoir state, (shape=(chunks, res_dim) or
96
+ shape=(seq_len, chunks, res_dim)).
97
+
98
+ Returns
99
+ -------
100
+ Array
101
+ Output state, (out_dim,) or shape=(seq_len, out_dim)).
102
+ """
103
+ if self.chunks > 0:
104
+ if len(res_state.shape) == 2:
105
+ to_ret = self.readout(res_state)
106
+ elif len(res_state.shape) == 3:
107
+ to_ret = self.batch_readout(res_state)
108
+ else:
109
+ if len(res_state.shape) == 1:
110
+ to_ret = self.readout(res_state)
111
+ elif len(res_state.shape) == 2:
112
+ to_ret = self.batch_readout(res_state)
113
+ return to_ret
114
+
115
+
116
+ class ParallelLinearReadout(ReadoutBase):
117
+ """Linear readout layer.
118
+
119
+ Attributes
120
+ ----------
121
+ out_dim : int
122
+ Dimension of reservoir output.
123
+ res_dim : int
124
+ Reservoir dimension.
125
+ chunks : int
126
+ Number of parallel reservoirs.
127
+ wout : Array
128
+ Output matrix.
129
+ dtype : Float
130
+ Dtype, default jnp.float64.
131
+
132
+ Methods
133
+ -------
134
+ readout(res_state)
135
+ Map from reservoir state to output state.
136
+ """
137
+
138
+ out_dim: int
139
+ res_dim: int
140
+ wout: Array
141
+ chunks: int
142
+ dtype: Float
143
+
144
+ def __init__(
145
+ self,
146
+ out_dim: int,
147
+ res_dim: int,
148
+ chunks: int = 1,
149
+ dtype: Float = jnp.float64,
150
+ *,
151
+ seed: int = 0,
152
+ ) -> None:
153
+ """Initialize readout layer to zeros.
154
+
155
+ Parameters
156
+ ----------
157
+ out_dim : int
158
+ Dimension of reservoir output.
159
+ res_dim : int
160
+ Reservoir dimension.
161
+ chunks : int
162
+ Number of parallel resrevoirs.
163
+ dtype : Float
164
+ Dtype, default jnp.float64.
165
+ seed : int
166
+ Not used for ParallelLinearReadout, here to maintain consistent
167
+ interface.
168
+ """
169
+ super().__init__(out_dim=out_dim, res_dim=res_dim, dtype=dtype)
170
+ self.out_dim = out_dim
171
+ self.res_dim = res_dim
172
+ self.wout = jnp.zeros((chunks, int(out_dim / chunks), res_dim), dtype=dtype)
173
+ self.dtype = dtype
174
+ self.chunks = chunks
175
+
176
+ @eqx.filter_jit
177
+ def readout(self, res_state: Array) -> Array:
178
+ """Readout from reservoir state.
179
+
180
+ Parameters
181
+ ----------
182
+ res_state : Array
183
+ Reservoir state, (shape=(chunks, res_dim,)).
184
+
185
+ Returns
186
+ -------
187
+ Array
188
+ Output from reservoir, (shape=(out_dim,)).
189
+ """
190
+ if res_state.shape[1] != self.res_dim:
191
+ raise ValueError(
192
+ "Incorrect reservoir dimension for instantiated output map."
193
+ )
194
+ return jnp.ravel(eqx.filter_vmap(jnp.matmul)(self.wout, res_state))
195
+
196
+
197
+ class LinearReadout(ParallelLinearReadout):
198
+ """Linear readout layer.
199
+
200
+ Attributes
201
+ ----------
202
+ out_dim : int
203
+ Dimension of reservoir output.
204
+ res_dim : int
205
+ Reservoir dimension.
206
+ wout : Array
207
+ Output matrix.
208
+ dtype : Float
209
+ Dtype, default jnp.float64.
210
+
211
+ Methods
212
+ -------
213
+ readout(res_state)
214
+ Map from reservoir state to output state.
215
+ """
216
+
217
+ def __init__(
218
+ self,
219
+ out_dim: int,
220
+ res_dim: int,
221
+ dtype: Float = jnp.float64,
222
+ *,
223
+ seed: int = 0,
224
+ ) -> None:
225
+ """Initialize readout layer to zeros.
226
+
227
+ Parameters
228
+ ----------
229
+ out_dim : int
230
+ Dimension of reservoir output.
231
+ res_dim : int
232
+ Reservoir dimension.
233
+ chunks : int
234
+ Number of parallel resrevoirs.
235
+ dtype : Float
236
+ Dtype, default jnp.float64.
237
+ seed : int
238
+ Not used for ParallelLinearReadout, here to maintain consistent
239
+ interface.
240
+ """
241
+ super().__init__(
242
+ out_dim=out_dim,
243
+ res_dim=res_dim,
244
+ chunks=1,
245
+ dtype=dtype,
246
+ seed=seed,
247
+ )
248
+
249
+ @eqx.filter_jit
250
+ def readout(self, res_state: Array) -> Array:
251
+ """Readout from reservoir state.
252
+
253
+ Parameters
254
+ ----------
255
+ res_state : Array
256
+ Reservoir state, (shape=(res_dim,)).
257
+
258
+ Returns
259
+ -------
260
+ Array
261
+ Output from reservoir, (shape=(out_dim,)).
262
+ """
263
+ return super().readout(res_state.reshape(1, -1)).reshape(-1)
264
+
265
+ def __call__(self, res_state: Array) -> Array:
266
+ """Call either readout or batch_readout depending on dimensions.
267
+
268
+ Parameters
269
+ ----------
270
+ res_state : Array
271
+ Reservoir state, (shape=(res_dim) or
272
+ shape=(seq_len, res_dim)).
273
+
274
+ Returns
275
+ -------
276
+ Array
277
+ Output state, (out_dim,) or shape=(seq_len, out_dim)).
278
+ """
279
+ return jnp.squeeze(super().__call__(res_state[..., None, :]))
280
+
281
+
282
+ class ParallelNonlinearReadout(ReadoutBase):
283
+ """Readout layer with user specified nonlinearities.
284
+
285
+ Attributes
286
+ ----------
287
+ out_dim : int
288
+ Dimension of reservoir output.
289
+ res_dim : int
290
+ Reservoir dimension.
291
+ chunks : int
292
+ Number of parallel reservoirs.
293
+ wout : Array
294
+ Output matrix.
295
+ nonlin_list : list
296
+ List containing user specified nonlinearities.
297
+ dtype : Float
298
+ Dtype, default jnp.float64.
299
+
300
+ Methods
301
+ -------
302
+ nonlinear_transform(res_state)
303
+ Nonlinear transform that acts entrywise on reservoir state.
304
+ readout(res_state)
305
+ Map from reservoir state to output state.
306
+ __call__(res_state)
307
+ Map from reservoir state to output state, handles batch and single outputs.
308
+ """
309
+
310
+ out_dim: int
311
+ res_dim: int
312
+ wout: Array
313
+ chunks: int
314
+ nonlin_list: list
315
+ dtype: Float
316
+
317
+ def __init__(
318
+ self,
319
+ out_dim: int,
320
+ res_dim: int,
321
+ nonlin_list: list[Callable],
322
+ chunks: int = 1,
323
+ dtype: Float = jnp.float64,
324
+ *,
325
+ seed: int = 0,
326
+ ) -> None:
327
+ """Initialize readout layer to zeros.
328
+
329
+ Parameters
330
+ ----------
331
+ out_dim : int
332
+ Dimension of reservoir output.
333
+ res_dim : int
334
+ Reservoir dimension.
335
+ nonlin_list : list[Callable]
336
+ List containing user specified entrywise nonlinearities. Each entry should
337
+ be a function mapping a scalar value to another scalar value, e.g.
338
+ lambda x : x ** 2 or lambda x : jnp.sin(x).
339
+ chunks : int
340
+ Number of parallel reservoirs.
341
+ dtype : Float
342
+ Dtype, default jnp.float64.
343
+ seed : int
344
+ Not used for ParallelNonlinearReadout, here to maintain consistent
345
+ interface.
346
+ """
347
+ super().__init__(out_dim=out_dim, res_dim=res_dim, dtype=dtype)
348
+ self.out_dim = out_dim
349
+ self.res_dim = res_dim
350
+ self.wout = jnp.zeros((chunks, int(out_dim / chunks), res_dim), dtype=dtype)
351
+ self.dtype = dtype
352
+ self.chunks = chunks
353
+ self.nonlin_list = nonlin_list
354
+
355
+ def nonlinear_transform(self, res_state: Array) -> Array:
356
+ """Perform nonlinear transformation on reservoir state.
357
+
358
+ Let tot_list be the list consisting of nonlin_list prepended by the identity
359
+ mapping. Let n be the length of tot_list. Then, nonlinear_transform acts such
360
+ that for all 0 <= k < chunks and 0 <= j < j * n:
361
+ res_state[k, j * n] <- res_state[k, j*n]
362
+ res_state[k, j * n + 1] <- f_0(res_state[k, j * n + 1])
363
+ ...
364
+ res_state[k, j * n + n - 1] <- f_{n-1}(res_state[k, j * n + n - 1])
365
+ where f_i is the i-th entry of nonlin_list.
366
+
367
+ Parameters
368
+ ----------
369
+ res_state : Array
370
+ Reservoir state, (shape=(..., res_dim,)).
371
+
372
+ Returns
373
+ -------
374
+ Array
375
+ Transformed reservoir state.
376
+ """
377
+ num_nonlins = len(self.nonlin_list)
378
+ transformed_res_state = res_state
379
+ for idx in range(num_nonlins):
380
+ transformed_res_state = transformed_res_state.at[
381
+ ..., idx + 1 :: num_nonlins + 1
382
+ ].set(
383
+ self.nonlin_list[idx](
384
+ transformed_res_state[..., idx + 1 :: num_nonlins + 1]
385
+ )
386
+ )
387
+ return transformed_res_state
388
+
389
+ @eqx.filter_jit
390
+ def readout(self, res_state: Array) -> Array:
391
+ """Readout from reservoir state.
392
+
393
+ Parameters
394
+ ----------
395
+ res_state : Array
396
+ Reservoir state, (shape=(chunks, res_dim,)).
397
+
398
+ Returns
399
+ -------
400
+ Array
401
+ Output from reservoir, (shape=(out_dim,)).
402
+ """
403
+ if res_state.shape[1] != self.res_dim:
404
+ raise ValueError(
405
+ "Incorrect reservoir dimension for instantiated output map."
406
+ )
407
+ transformed_res_state = self.nonlinear_transform(res_state)
408
+ return jnp.ravel(eqx.filter_vmap(jnp.matmul)(self.wout, transformed_res_state))
409
+
410
+
411
+ class NonlinearReadout(ParallelNonlinearReadout):
412
+ """Readout layer with user specified nonlinearities.
413
+
414
+ Attributes
415
+ ----------
416
+ out_dim : int
417
+ Dimension of reservoir output.
418
+ res_dim : int
419
+ Reservoir dimension.
420
+ chunks : int
421
+ Number of parallel reservoirs.
422
+ wout : Array
423
+ Output matrix.
424
+ nonlin_list : list
425
+ List containing user specified nonlinearities.
426
+ dtype : Float
427
+ Dtype, default jnp.float64.
428
+
429
+ Methods
430
+ -------
431
+ nonlinear_transform(res_state)
432
+ Nonlinear transform that acts entrywise on reservoir state.
433
+ readout(res_state)
434
+ Map from reservoir state to output state.
435
+ __call__(res_state)
436
+ Map from reservoir state to output state, handles batch and single outputs.
437
+ """
438
+
439
+ def __init__(
440
+ self,
441
+ out_dim: int,
442
+ res_dim: int,
443
+ nonlin_list: list[Callable],
444
+ dtype: Float = jnp.float64,
445
+ *,
446
+ seed: int = 0,
447
+ ) -> None:
448
+ """Initialize readout layer to zeros.
449
+
450
+ Parameters
451
+ ----------
452
+ out_dim : int
453
+ Dimension of reservoir output.
454
+ res_dim : int
455
+ Reservoir dimension.
456
+ nonlin_list : list[Callable]
457
+ List containing user specified entrywise nonlinearities. Each entry should
458
+ be a function mapping a scalar value to another scalar value, e.g.
459
+ lambda x : x ** 2 or lambda x : jnp.sin(x).
460
+ dtype : Float
461
+ Dtype, default jnp.float64.
462
+ seed : int
463
+ Not used for ParallelNonlinearReadout, here to maintain consistent
464
+ interface.
465
+ """
466
+ super().__init__(
467
+ out_dim=out_dim,
468
+ res_dim=res_dim,
469
+ nonlin_list=nonlin_list,
470
+ chunks=1,
471
+ dtype=dtype,
472
+ seed=seed,
473
+ )
474
+
475
+ def nonlinear_transform(self, res_state: Array) -> Array:
476
+ """Perform nonlinear transformation on reservoir state.
477
+
478
+ Let tot_list be the list consisting of nonlin_list prepended by the identity
479
+ mapping. Let n be the length of tot_list. Then, nonlinear_transform acts such
480
+ that for all 0 <= k < chunks and 0 <= j < j * n:
481
+ res_state[k, j * n] <- res_state[k, j*n]
482
+ res_state[k, j * n + 1] <- f_0(res_state[k, j * n + 1])
483
+ ...
484
+ res_state[k, j * n + n - 1] <- f_{n-1}(res_state[k, j * n + n - 1])
485
+ where f_i is the i-th entry of nonlin_list.
486
+
487
+ Parameters
488
+ ----------
489
+ res_state : Array
490
+ Reservoir state, (shape=(..., res_dim,)).
491
+
492
+ Returns
493
+ -------
494
+ Array
495
+ Transformed reservoir state.
496
+ """
497
+ num_nonlins = len(self.nonlin_list)
498
+ transformed_res_state = res_state
499
+ for idx in range(num_nonlins):
500
+ transformed_res_state = transformed_res_state.at[
501
+ ..., idx + 1 :: num_nonlins + 1
502
+ ].set(
503
+ self.nonlin_list[idx](
504
+ transformed_res_state[..., idx + 1 :: num_nonlins + 1]
505
+ )
506
+ )
507
+ return transformed_res_state
508
+
509
+ @eqx.filter_jit
510
+ def readout(self, res_state: Array) -> Array:
511
+ """Readout from reservoir state.
512
+
513
+ Parameters
514
+ ----------
515
+ res_state : Array
516
+ Reservoir state, (shape=(res_dim,)).
517
+
518
+ Returns
519
+ -------
520
+ Array
521
+ Output from reservoir, (shape=(out_dim,)).
522
+ """
523
+ return super().readout(res_state.reshape(1, -1)).reshape(-1)
524
+
525
+ def __call__(self, res_state: Array) -> Array:
526
+ """Call either readout or batch_readout depending on dimensions.
527
+
528
+ Parameters
529
+ ----------
530
+ res_state : Array
531
+ Reservoir state, (shape=(res_dim) or
532
+ shape=(seq_len, res_dim)).
533
+
534
+ Returns
535
+ -------
536
+ Array
537
+ Output state, (out_dim,) or shape=(seq_len, out_dim)).
538
+ """
539
+ return jnp.squeeze(super().__call__(res_state[..., None, :]))
540
+
541
+
542
+ class ParallelQuadraticReadout(ParallelNonlinearReadout):
543
+ """Quadratic readout layer.
544
+
545
+ Attributes
546
+ ----------
547
+ out_dim : int
548
+ Dimension of reservoir output.
549
+ res_dim : int
550
+ Reservoir dimension.
551
+ chunks : int
552
+ Number of parallel reservoirs.
553
+ wout : Array
554
+ Output matrix.
555
+ dtype : Float
556
+ Dtype, default jnp.float64.
557
+
558
+ Methods
559
+ -------
560
+ nonlinear_transform(res_state)
561
+ Quadratic transform that acts entrywise on reservoir state.
562
+ readout(res_state)
563
+ Map from reservoir state to output state with quadratic nonlinearity.
564
+ __call__(res_state)
565
+ Map from reservoir state to output state with quadratic nonlinearity,
566
+ handles batch and single outputs.
567
+ """
568
+
569
+ out_dim: int
570
+ res_dim: int
571
+ wout: Array
572
+ chunks: int
573
+ dtype: Float
574
+
575
+ def __init__(
576
+ self,
577
+ out_dim: int,
578
+ res_dim: int,
579
+ chunks: int = 1,
580
+ dtype: Float = jnp.float64,
581
+ *,
582
+ seed: int = 0,
583
+ ) -> None:
584
+ """Initialize readout layer to zeros.
585
+
586
+ Parameters
587
+ ----------
588
+ out_dim : int
589
+ Dimension of reservoir output.
590
+ res_dim : int
591
+ Reservoir dimension.
592
+ chunks : int
593
+ Number of parallel resrevoirs.
594
+ dtype : Float
595
+ Dtype, default jnp.float64.
596
+ seed : int
597
+ Not used for ParallelQuadraticReadout, here to maintain consistent
598
+ interface.
599
+ """
600
+ super().__init__(
601
+ out_dim=out_dim,
602
+ res_dim=res_dim,
603
+ dtype=dtype,
604
+ nonlin_list=[lambda x: x**2],
605
+ chunks=chunks,
606
+ )
607
+
608
+
609
+ class QuadraticReadout(NonlinearReadout):
610
+ """Quadratic readout layer.
611
+
612
+ Attributes
613
+ ----------
614
+ out_dim : int
615
+ Dimension of reservoir output.
616
+ res_dim : int
617
+ Reservoir dimension.
618
+ wout : Array
619
+ Output matrix.
620
+ dtype : Float
621
+ Dtype, default jnp.float64.
622
+
623
+ Methods
624
+ -------
625
+ nonlinear_transform(res_state)
626
+ Quadratic transform that acts entrywise on reservoir state.
627
+ readout(res_state)
628
+ Map from reservoir state to output state with quadratic nonlinearity.
629
+ __call__(res_state)
630
+ Map from reservoir state to output state with quadratic nonlinearity,
631
+ handles batch and single outputs.
632
+ """
633
+
634
+ def __init__(
635
+ self,
636
+ out_dim: int,
637
+ res_dim: int,
638
+ dtype: Float = jnp.float64,
639
+ *,
640
+ seed: int = 0,
641
+ ) -> None:
642
+ """Initialize readout layer to zeros.
643
+
644
+ Parameters
645
+ ----------
646
+ out_dim : int
647
+ Dimension of reservoir output.
648
+ res_dim : int
649
+ Reservoir dimension.
650
+ dtype : Float
651
+ Dtype, default jnp.float64.
652
+ seed : int
653
+ Not used for ParallelQuadraticReadout, here to maintain consistent
654
+ interface.
655
+ """
656
+ super().__init__(
657
+ out_dim=out_dim,
658
+ res_dim=res_dim,
659
+ dtype=dtype,
660
+ nonlin_list=[lambda x: x**2],
661
+ )
662
+
663
+
664
+ class EnsembleLinearReadout(ReadoutBase):
665
+ """Esembled linear readout layer.
666
+
667
+ Attributes
668
+ ----------
669
+ out_dim : int
670
+ Dimension of reservoir output.
671
+ res_dim : int
672
+ Reservoir dimension.
673
+ chunks : int
674
+ Number of parallel reservoirs.
675
+ wout : Array
676
+ Output matrix.
677
+ dtype : Float
678
+ Dtype, default jnp.float64.
679
+
680
+ Methods
681
+ -------
682
+ readout(res_state)
683
+ Map from reservoir state to output state.
684
+ """
685
+
686
+ out_dim: int
687
+ res_dim: int
688
+ wout: Array
689
+ chunks: int
690
+ dtype: Float
691
+
692
+ def __init__(
693
+ self,
694
+ out_dim: int,
695
+ res_dim: int,
696
+ chunks: int = 5,
697
+ dtype: Float = jnp.float64,
698
+ *,
699
+ seed: int = 0,
700
+ ) -> None:
701
+ """Initialize readout layer to zeros.
702
+
703
+ Parameters
704
+ ----------
705
+ out_dim : int
706
+ Dimension of reservoir output.
707
+ res_dim : int
708
+ Reservoir dimension.
709
+ chunks : int
710
+ Number of parallel resrevoirs.
711
+ dtype : Float
712
+ Dtype, default jnp.float64.
713
+ seed : int
714
+ Not used for ParallelLinearReadout, here to maintain consistent
715
+ interface.
716
+ """
717
+ super().__init__(out_dim=out_dim, res_dim=res_dim, dtype=dtype)
718
+ self.out_dim = out_dim
719
+ self.res_dim = res_dim
720
+ self.wout = jnp.zeros((chunks, out_dim, res_dim), dtype=dtype)
721
+ self.dtype = dtype
722
+ self.chunks = chunks
723
+
724
+ @eqx.filter_jit
725
+ def readout(self, res_state: Array) -> Array:
726
+ """Readout from reservoir state.
727
+
728
+ Parameters
729
+ ----------
730
+ res_state : Array
731
+ Reservoir state, (shape=(chunks, res_dim,)).
732
+
733
+ Returns
734
+ -------
735
+ Array
736
+ Output from reservoir, (shape=(out_dim,)).
737
+ """
738
+ if res_state.shape[1] != self.res_dim:
739
+ raise ValueError(
740
+ "Incorrect reservoir dimension for instantiated output map."
741
+ )
742
+ return jnp.mean(eqx.filter_vmap(jnp.matmul)(self.wout, res_state), axis=0)