Skip to content

API Reference

This section documents the Python API for rclib. The C++ core is wrapped efficiently to provide a seamless experience.

Reservoirs

rclib.reservoirs

Reservoir configurations.

RandomSparse

Random Sparse Reservoir configuration.

Source code in python/rclib/reservoirs.py
class RandomSparse:
    """Random Sparse Reservoir configuration."""

    def __init__(
        self,
        n_neurons: int,
        spectral_radius: float,
        sparsity: float = 0.1,
        leak_rate: float = 1.0,
        input_scaling: float = 1.0,
        *,
        include_bias: bool = False,
        seed: int = 42,
        spectral_radius_method: str = "power_iteration",
    ) -> None:
        """Initialize the Random Sparse Reservoir.

        Args:
            n_neurons: Number of neurons in the reservoir.
            spectral_radius: Spectral radius of the reservoir weight matrix.
            sparsity: Sparsity of the reservoir weight matrix (0.0 to 1.0).
            leak_rate: Leaking rate of the neurons.
            input_scaling: Scaling factor for the input weights.
            include_bias: Whether to include a bias term.
            seed: Random seed for weights initialization.
            spectral_radius_method: How the spectral radius of the random weight matrix is
                found before it is scaled to ``spectral_radius``. "power_iteration" (default)
                estimates it by seeded power iteration, typically to within 0.3%. "dense"
                computes it exactly from all eigenvalues of the matrix, which costs
                O(n_neurons^3) time and O(n_neurons^2) memory, so it is meant for small
                reservoirs.
        """
        if n_neurons <= 0:
            msg = "n_neurons must be positive."
            raise ValueError(msg)
        # Chained range checks reject NaN on their own; one-sided bounds also need isfinite.
        if not math.isfinite(spectral_radius) or spectral_radius < 0:
            msg = "spectral_radius must be finite and non-negative."
            raise ValueError(msg)
        if not 0 <= sparsity <= 1:
            msg = "sparsity must be in [0, 1]."
            raise ValueError(msg)
        if not 0 < leak_rate <= 1:
            msg = "leak_rate must be in (0, 1]."
            raise ValueError(msg)
        if not math.isfinite(input_scaling) or input_scaling < 0:
            msg = "input_scaling must be finite and non-negative."
            raise ValueError(msg)
        if spectral_radius_method not in {"power_iteration", "dense"}:
            msg = "spectral_radius_method must be 'power_iteration' or 'dense'."
            raise ValueError(msg)

        self.n_neurons = n_neurons
        self.spectral_radius = spectral_radius
        self.sparsity = sparsity
        self.leak_rate = leak_rate
        self.input_scaling = input_scaling
        self.include_bias = include_bias
        self.seed = seed
        self.spectral_radius_method = spectral_radius_method

__init__(n_neurons, spectral_radius, sparsity=0.1, leak_rate=1.0, input_scaling=1.0, *, include_bias=False, seed=42, spectral_radius_method='power_iteration')

Initialize the Random Sparse Reservoir.

Args: n_neurons: Number of neurons in the reservoir. spectral_radius: Spectral radius of the reservoir weight matrix. sparsity: Sparsity of the reservoir weight matrix (0.0 to 1.0). leak_rate: Leaking rate of the neurons. input_scaling: Scaling factor for the input weights. include_bias: Whether to include a bias term. seed: Random seed for weights initialization. spectral_radius_method: How the spectral radius of the random weight matrix is found before it is scaled to spectral_radius. "power_iteration" (default) estimates it by seeded power iteration, typically to within 0.3%. "dense" computes it exactly from all eigenvalues of the matrix, which costs O(n_neurons^3) time and O(n_neurons^2) memory, so it is meant for small reservoirs.

Source code in python/rclib/reservoirs.py
def __init__(
    self,
    n_neurons: int,
    spectral_radius: float,
    sparsity: float = 0.1,
    leak_rate: float = 1.0,
    input_scaling: float = 1.0,
    *,
    include_bias: bool = False,
    seed: int = 42,
    spectral_radius_method: str = "power_iteration",
) -> None:
    """Initialize the Random Sparse Reservoir.

    Args:
        n_neurons: Number of neurons in the reservoir.
        spectral_radius: Spectral radius of the reservoir weight matrix.
        sparsity: Sparsity of the reservoir weight matrix (0.0 to 1.0).
        leak_rate: Leaking rate of the neurons.
        input_scaling: Scaling factor for the input weights.
        include_bias: Whether to include a bias term.
        seed: Random seed for weights initialization.
        spectral_radius_method: How the spectral radius of the random weight matrix is
            found before it is scaled to ``spectral_radius``. "power_iteration" (default)
            estimates it by seeded power iteration, typically to within 0.3%. "dense"
            computes it exactly from all eigenvalues of the matrix, which costs
            O(n_neurons^3) time and O(n_neurons^2) memory, so it is meant for small
            reservoirs.
    """
    if n_neurons <= 0:
        msg = "n_neurons must be positive."
        raise ValueError(msg)
    # Chained range checks reject NaN on their own; one-sided bounds also need isfinite.
    if not math.isfinite(spectral_radius) or spectral_radius < 0:
        msg = "spectral_radius must be finite and non-negative."
        raise ValueError(msg)
    if not 0 <= sparsity <= 1:
        msg = "sparsity must be in [0, 1]."
        raise ValueError(msg)
    if not 0 < leak_rate <= 1:
        msg = "leak_rate must be in (0, 1]."
        raise ValueError(msg)
    if not math.isfinite(input_scaling) or input_scaling < 0:
        msg = "input_scaling must be finite and non-negative."
        raise ValueError(msg)
    if spectral_radius_method not in {"power_iteration", "dense"}:
        msg = "spectral_radius_method must be 'power_iteration' or 'dense'."
        raise ValueError(msg)

    self.n_neurons = n_neurons
    self.spectral_radius = spectral_radius
    self.sparsity = sparsity
    self.leak_rate = leak_rate
    self.input_scaling = input_scaling
    self.include_bias = include_bias
    self.seed = seed
    self.spectral_radius_method = spectral_radius_method

Nvar

NVAR Reservoir configuration.

Source code in python/rclib/reservoirs.py
class Nvar:
    """NVAR Reservoir configuration."""

    def __init__(self, num_lags: int, polynomial_order: int = 1) -> None:
        """Initialize the NVAR Reservoir.

        Args:
            num_lags: Number of time lags to include.
            polynomial_order: Maximum monomial degree to include. The default
                of 1 preserves a linear delay embedding.
        """
        if num_lags <= 0:
            msg = "num_lags must be positive."
            raise ValueError(msg)
        if polynomial_order <= 0:
            msg = "polynomial_order must be positive."
            raise ValueError(msg)
        if polynomial_order > MAX_POLYNOMIAL_ORDER:
            msg = f"polynomial_order exceeds the supported maximum ({MAX_POLYNOMIAL_ORDER})."
            raise ValueError(msg)

        self.num_lags = num_lags
        self.polynomial_order = polynomial_order

__init__(num_lags, polynomial_order=1)

Initialize the NVAR Reservoir.

Args: num_lags: Number of time lags to include. polynomial_order: Maximum monomial degree to include. The default of 1 preserves a linear delay embedding.

Source code in python/rclib/reservoirs.py
def __init__(self, num_lags: int, polynomial_order: int = 1) -> None:
    """Initialize the NVAR Reservoir.

    Args:
        num_lags: Number of time lags to include.
        polynomial_order: Maximum monomial degree to include. The default
            of 1 preserves a linear delay embedding.
    """
    if num_lags <= 0:
        msg = "num_lags must be positive."
        raise ValueError(msg)
    if polynomial_order <= 0:
        msg = "polynomial_order must be positive."
        raise ValueError(msg)
    if polynomial_order > MAX_POLYNOMIAL_ORDER:
        msg = f"polynomial_order exceeds the supported maximum ({MAX_POLYNOMIAL_ORDER})."
        raise ValueError(msg)

    self.num_lags = num_lags
    self.polynomial_order = polynomial_order

Readouts

rclib.readouts

Readout configurations.

Ridge

Ridge Regression Readout configuration.

Source code in python/rclib/readouts.py
class Ridge:
    """Ridge Regression Readout configuration."""

    def __init__(
        self,
        alpha: float,
        *,
        include_bias: bool,
        solver: str = "auto",
        tolerance: float = 1e-10,
    ) -> None:
        """Initialize the Ridge Readout.

        Args:
            alpha: Regularization parameter.
            include_bias: Whether to include a bias term.
            solver: Solver to use ("auto", "cholesky", "dual_cholesky",
                "conjugate_gradient", "conjugate_gradient_implicit").
            tolerance: Convergence tolerance for iterative solvers (CG).
        """
        if not math.isfinite(alpha) or alpha < 0:
            msg = "alpha must be finite and non-negative."
            raise ValueError(msg)
        if not math.isfinite(tolerance) or tolerance <= 0:
            msg = "tolerance must be finite and positive."
            raise ValueError(msg)

        self.alpha = alpha
        self.include_bias = include_bias
        self.solver = solver
        self.tolerance = tolerance

__init__(alpha, *, include_bias, solver='auto', tolerance=1e-10)

Initialize the Ridge Readout.

Args: alpha: Regularization parameter. include_bias: Whether to include a bias term. solver: Solver to use ("auto", "cholesky", "dual_cholesky", "conjugate_gradient", "conjugate_gradient_implicit"). tolerance: Convergence tolerance for iterative solvers (CG).

Source code in python/rclib/readouts.py
def __init__(
    self,
    alpha: float,
    *,
    include_bias: bool,
    solver: str = "auto",
    tolerance: float = 1e-10,
) -> None:
    """Initialize the Ridge Readout.

    Args:
        alpha: Regularization parameter.
        include_bias: Whether to include a bias term.
        solver: Solver to use ("auto", "cholesky", "dual_cholesky",
            "conjugate_gradient", "conjugate_gradient_implicit").
        tolerance: Convergence tolerance for iterative solvers (CG).
    """
    if not math.isfinite(alpha) or alpha < 0:
        msg = "alpha must be finite and non-negative."
        raise ValueError(msg)
    if not math.isfinite(tolerance) or tolerance <= 0:
        msg = "tolerance must be finite and positive."
        raise ValueError(msg)

    self.alpha = alpha
    self.include_bias = include_bias
    self.solver = solver
    self.tolerance = tolerance

Rls

Recursive Least Squares (RLS) Readout configuration.

Source code in python/rclib/readouts.py
class Rls:
    """Recursive Least Squares (RLS) Readout configuration."""

    def __init__(
        self,
        lambda_: float,
        delta: float,
        *,
        include_bias: bool,
        solver: str = "rank1_update",
    ) -> None:
        """Initialize the RLS Readout.

        Args:
            lambda_: Forgetting factor (0.0 to 1.0).
            delta: Initial value for the covariance matrix diagonal.
            include_bias: Whether to include a bias term.
            solver: Solver type ("rank1_update" or "rank_k_update").
                "rank1_update" is traditional sequential RLS.
                "rank_k_update" is optimized for mini-batches using Woodbury identity.
        """
        if not 0 < lambda_ <= 1:
            msg = "lambda_ must be in (0, 1]."
            raise ValueError(msg)
        if not math.isfinite(delta) or delta <= 0:
            msg = "delta must be finite and positive."
            raise ValueError(msg)

        self.lambda_ = lambda_
        self.delta = delta
        self.include_bias = include_bias
        self.solver = solver

__init__(lambda_, delta, *, include_bias, solver='rank1_update')

Initialize the RLS Readout.

Args: lambda_: Forgetting factor (0.0 to 1.0). delta: Initial value for the covariance matrix diagonal. include_bias: Whether to include a bias term. solver: Solver type ("rank1_update" or "rank_k_update"). "rank1_update" is traditional sequential RLS. "rank_k_update" is optimized for mini-batches using Woodbury identity.

Source code in python/rclib/readouts.py
def __init__(
    self,
    lambda_: float,
    delta: float,
    *,
    include_bias: bool,
    solver: str = "rank1_update",
) -> None:
    """Initialize the RLS Readout.

    Args:
        lambda_: Forgetting factor (0.0 to 1.0).
        delta: Initial value for the covariance matrix diagonal.
        include_bias: Whether to include a bias term.
        solver: Solver type ("rank1_update" or "rank_k_update").
            "rank1_update" is traditional sequential RLS.
            "rank_k_update" is optimized for mini-batches using Woodbury identity.
    """
    if not 0 < lambda_ <= 1:
        msg = "lambda_ must be in (0, 1]."
        raise ValueError(msg)
    if not math.isfinite(delta) or delta <= 0:
        msg = "delta must be finite and positive."
        raise ValueError(msg)

    self.lambda_ = lambda_
    self.delta = delta
    self.include_bias = include_bias
    self.solver = solver

Lms

Least Mean Squares (LMS) Readout configuration.

Source code in python/rclib/readouts.py
class Lms:
    """Least Mean Squares (LMS) Readout configuration."""

    def __init__(self, learning_rate: float, *, include_bias: bool) -> None:
        """Initialize the LMS Readout.

        Args:
            learning_rate: Learning rate for the LMS algorithm.
            include_bias: Whether to include a bias term.
        """
        if not math.isfinite(learning_rate) or learning_rate <= 0:
            msg = "learning_rate must be finite and positive."
            raise ValueError(msg)

        self.learning_rate = learning_rate
        self.include_bias = include_bias

__init__(learning_rate, *, include_bias)

Initialize the LMS Readout.

Args: learning_rate: Learning rate for the LMS algorithm. include_bias: Whether to include a bias term.

Source code in python/rclib/readouts.py
def __init__(self, learning_rate: float, *, include_bias: bool) -> None:
    """Initialize the LMS Readout.

    Args:
        learning_rate: Learning rate for the LMS algorithm.
        include_bias: Whether to include a bias term.
    """
    if not math.isfinite(learning_rate) or learning_rate <= 0:
        msg = "learning_rate must be finite and positive."
        raise ValueError(msg)

    self.learning_rate = learning_rate
    self.include_bias = include_bias

Model

rclib.model

Model module for Reservoir Computing.

ESN

Echo State Network (ESN) model.

Source code in python/rclib/model.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
class ESN:
    """Echo State Network (ESN) model."""

    def __init__(self, connection_type: str = "serial") -> None:
        """Initialize the ESN model.

        Parameters
        ----------
        connection_type : str, optional
            The type of connection between reservoirs ("serial" or "parallel").
            Default is "serial".
        """
        if connection_type not in {"serial", "parallel"}:
            msg = "connection_type must be 'serial' or 'parallel'."
            raise ValueError(msg)

        self.connection_type = connection_type
        self._reservoirs_params: list[Any] = []  # Store parameters for Python-side reservoir objects
        self._readout_params: Any = None  # Store parameters for Python-side readout object
        self._cpp_model = _rclib.Model()  # Initialize the C++ Model object

    def add_reservoir(self, reservoir: Any) -> None:  # noqa: ANN401
        """Add a reservoir to the model.

        Parameters
        ----------
        reservoir : Any
            The reservoir object to add.

        Raises
        ------
        TypeError
            If the reservoir type is unsupported.
        ValueError
            If a RandomSparse reservoir has an unsupported spectral_radius_method.
        """
        # Create and add the C++ reservoir to the C++ model
        if isinstance(reservoir, reservoirs.RandomSparse):
            if reservoir.spectral_radius_method not in _SPECTRAL_RADIUS_METHODS:
                msg = f"Unsupported spectral_radius_method: {reservoir.spectral_radius_method}"
                raise ValueError(msg)
            cpp_res = _rclib.RandomSparseReservoir(
                reservoir.n_neurons,
                reservoir.spectral_radius,
                reservoir.sparsity,
                reservoir.leak_rate,
                reservoir.input_scaling,
                reservoir.include_bias,
                reservoir.seed,
                _SPECTRAL_RADIUS_METHODS[reservoir.spectral_radius_method],
            )
            self._cpp_model.addReservoir(cpp_res, self.connection_type)
        elif isinstance(reservoir, reservoirs.Nvar):
            cpp_res = _rclib.NvarReservoir(reservoir.num_lags, reservoir.polynomial_order)
            self._cpp_model.addReservoir(cpp_res, self.connection_type)
        # Add other reservoir types here as they are implemented
        else:
            msg = "Unsupported reservoir type"
            raise TypeError(msg)

        # Store the Python reservoir object's parameters only once the C++ model holds the
        # reservoir, so a rejected reservoir leaves both sides unchanged.
        self._reservoirs_params.append(reservoir)

        # Update readout in case it's using "auto" solver
        self._update_readout()

    def set_readout(self, readout: Any) -> None:  # noqa: ANN401
        """Set the readout for the model.

        Parameters
        ----------
        readout : Any
            The readout object to set.

        Raises
        ------
        TypeError
            If the readout type is unsupported.
        """
        # Store the Python readout object's parameters
        self._readout_params = readout
        self._update_readout()

    def _update_readout(self) -> None:
        """Instantiate or update the C++ readout based on current parameters."""
        if self._readout_params is None:
            return

        # Don't re-instantiate if already exists (to preserve online learning state)
        try:
            if self._cpp_model.getReadout() is not None:
                return
        except RuntimeError:
            # getReadout() throws if not set
            pass

        readout = self._readout_params

        # Create and set the C++ readout to the C++ model
        if isinstance(readout, readouts.Ridge):
            if readout.solver not in _RIDGE_SOLVERS:
                msg = f"Unsupported solver: {readout.solver}"
                raise ValueError(msg)

            cpp_readout = _rclib.RidgeReadout(
                readout.alpha, readout.include_bias, _RIDGE_SOLVERS[readout.solver], readout.tolerance
            )
            self._cpp_model.setReadout(cpp_readout)
        elif isinstance(readout, readouts.Rls):
            if readout.solver not in _RLS_SOLVERS:
                msg = f"Unsupported RLS solver: {readout.solver}"
                raise ValueError(msg)

            cpp_readout = _rclib.RlsReadout(
                readout.lambda_, readout.delta, readout.include_bias, _RLS_SOLVERS[readout.solver]
            )
            self._cpp_model.setReadout(cpp_readout)
        elif isinstance(readout, readouts.Lms):
            cpp_readout = _rclib.LmsReadout(readout.learning_rate, readout.include_bias)
            self._cpp_model.setReadout(cpp_readout)
        else:
            msg = "Unsupported readout type"
            raise TypeError(msg)

    def fit(self, x: ArrayLike, y: ArrayLike, washout_len: int = 0) -> None:
        """Fit the model to the data.

        Parameters
        ----------
        x : ArrayLike
            Input data.
        y : ArrayLike
            Target data.
        washout_len : int, optional
            Number of initial samples to discard. Default is 0.
        """
        # Ensure readout is correctly initialized (especially for "auto" solver)
        self._update_readout()
        # Call the C++ model's fit method
        self._cpp_model.fit(x, y, washout_len)

    def fit_sequences(self, inputs: Sequence[ArrayLike], targets: Sequence[ArrayLike], washout_len: int = 0) -> None:
        """Fit the readout once on several independent sequences, such as episodes.

        Each sequence starts from reset reservoirs and discards its own first
        ``washout_len`` samples. The remaining reservoir states and targets of all
        sequences are stacked and the readout is fitted once, so nothing carries over
        from one sequence to the next. With a single sequence this equals :meth:`fit`.

        Parameters
        ----------
        inputs : Sequence[ArrayLike]
            Input data, one 2-D array of shape (n_samples_i, n_inputs) per sequence,
            for example a list of arrays. Sequences may differ in length but not in
            width.
        targets : Sequence[ArrayLike]
            Target data, one 2-D array of shape (n_samples_i, n_outputs) per sequence,
            with as many rows as the matching input sequence.
        washout_len : int, optional
            Number of initial samples to discard from each sequence. Default is 0.

        Raises
        ------
        ValueError
            If a single 2-D array is passed instead of a sequence of arrays, if there
            are no sequences or unequal numbers of input and target sequences, or if a
            sequence is empty, its inputs and targets differ in length, its width
            differs from the first sequence's, or its input width differs from the one
            the reservoirs are locked to by earlier input. Messages name the sequence
            index.
        IndexError
            If ``washout_len`` is negative or not shorter than a sequence.
        RuntimeError
            If the model has no reservoir or no readout.

        Notes
        -----
        These checks run before the model changes. Afterwards the reservoirs hold
        their states at the end of the last sequence.
        """
        # A single 2-D array would otherwise be split into one-row sequences.
        for name, sequences in (("inputs", inputs), ("targets", targets)):
            if isinstance(sequences, np.ndarray) and sequences.ndim == 2:  # noqa: PLR2004
                msg = f"{name} must be a sequence of 2-D arrays, one per sequence, not a single 2-D array."
                raise ValueError(msg)
        self._update_readout()
        self._cpp_model.fitSequences(inputs, targets, washout_len)

    def predict(self, x: ArrayLike, *, reset_state_before_predict: bool = True) -> np.ndarray:
        """Predict using the trained model.

        Parameters
        ----------
        x : ArrayLike
            Input data.
        reset_state_before_predict : bool, optional
            Whether to reset the reservoir state before prediction. Default is True.

        Returns
        -------
        np.ndarray
            The predicted values.
        """
        # Call the C++ model's predict method
        return self._cpp_model.predict(x, reset_state_before_predict)

    def predict_online(self, x: ArrayLike) -> np.ndarray:
        """Predict in online mode (updating state).

        Parameters
        ----------
        x : ArrayLike
            Input data.

        Returns
        -------
        np.ndarray
            The predicted values.
        """
        # Call the C++ model's predictOnline method
        return self._cpp_model.predictOnline(x)

    def predict_generative(self, prime_data: ArrayLike, n_steps: int) -> np.ndarray:
        """Generative prediction.

        Advances the reservoirs through ``prime_data`` (if any), then generates
        ``n_steps`` outputs, feeding each output back as the next input. Every
        generated output, including the last, advances the reservoirs, so a following
        call with empty ``prime_data`` continues the sequence: generating ``a`` steps
        and then ``b`` steps equals generating ``a + b`` steps. With ``n_steps == 0``
        no output is fed back.

        Parameters
        ----------
        prime_data : ArrayLike
            Initial data to prime the reservoir.
        n_steps : int
            Number of steps to generate.

        Returns
        -------
        np.ndarray
            The generated data.

        Raises
        ------
        ValueError
            If a generated output cannot be fed back because its width differs from
            the input width the reservoirs are locked to (also for ``n_steps == 1``).
        """
        # Call the C++ model's predictGenerative method
        return self._cpp_model.predictGenerative(prime_data, n_steps)

    def get_reservoir(self, index: int) -> Any:  # noqa: ANN401
        """Get the reservoir object at the specified index.

        Parameters
        ----------
        index : int
            The index of the reservoir.

        Returns
        -------
        Any
            The C++ reservoir object.
        """
        # Return the C++ reservoir object
        return self._cpp_model.getReservoir(index)

    def get_readout(self) -> Any:  # noqa: ANN401
        """Get the readout object, such as a fitted ridge readout to read its weights from.

        Returns
        -------
        Any
            The C++ readout object.

        Raises
        ------
        RuntimeError
            If no readout is set.
        """
        return self._cpp_model.getReadout()

    def reset_reservoirs(self) -> None:
        """Reset the states of all reservoirs."""
        # Call the C++ model's resetReservoirs method
        self._cpp_model.resetReservoirs()

    def partial_fit(self, x: ArrayLike | None, y: ArrayLike) -> None:
        """Update the model with a single sample (online learning).

        Parameters
        ----------
        x : ArrayLike, optional
            Input data sample. If None, the reservoir state is not advanced
            (useful if predict_online was already called).
        y : ArrayLike
            Target data sample.

        Raises
        ------
        RuntimeError
            If no reservoir or readout is set.
        """
        # Assuming only one reservoir for simplicity in online learning for now.
        # If multiple reservoirs are present, the logic would need to be more complex
        # to handle how their states are combined before feeding to the readout.
        if not self._reservoirs_params:
            msg = "No reservoir added to the model."
            raise RuntimeError(msg)
        if not self._readout_params:
            msg = "No readout set for the model."
            raise RuntimeError(msg)

        # Ensure readout is correctly initialized
        self._update_readout()

        if x is None:
            cpp_readout = self._cpp_model.getReadout()
            states = [self._cpp_model.getReservoir(i).getState() for i in range(len(self._reservoirs_params))]
            if any(state.shape[1] == 0 for state in states):
                msg = "Reservoir state is uninitialized; call predict_online or partial_fit with input first."
                raise RuntimeError(msg)
            current_state = states[-1] if self.connection_type == "serial" else np.hstack(states)
            cpp_readout.partialFit(current_state, y)
        else:
            self._cpp_model.partialFit(x, y)

    def save(self, path: str | os.PathLike[str]) -> None:
        """Save the model to a file.

        The file holds the configuration, the trained weights and the current
        reservoir states, so a loaded model continues exactly where this one
        stopped. It can also be loaded from C++ with ``Model::load``. An existing
        file at ``path`` is replaced in one step and is left unchanged if saving
        fails.

        Parameters
        ----------
        path : str or os.PathLike
            Destination file.

        Raises
        ------
        SerializationError
            If the model lacks a reservoir or readout, is internally inconsistent,
            or the file cannot be written.
        """
        self._cpp_model.save(os.fspath(path))

    @classmethod
    def load(cls, path: str | os.PathLike[str]) -> ESN:
        """Load a model saved by :meth:`save` or by C++ ``Model::save``.

        Only load files from sources you trust: the format contains no executable
        code, but it is parsed by native code.

        Parameters
        ----------
        path : str or os.PathLike
            Model file to read.

        Returns
        -------
        ESN
            The restored model, including its reservoir states.

        Raises
        ------
        SerializationError
            If the file cannot be read or is not a valid model file.
        """
        return cls._from_cpp_model(_rclib.Model.load(os.fspath(path)))

    @classmethod
    def _from_cpp_model(cls, cpp_model: Any) -> ESN:  # noqa: ANN401
        """Wrap a loaded C++ model, rebuilding the Python configuration objects."""
        esn = cls(cpp_model.getConnectionType())
        esn._cpp_model = cpp_model
        # partial_fit and _update_readout read these configuration objects.
        try:
            esn._reservoirs_params = [
                _reservoir_config_from_cpp(cpp_model.getReservoir(i)) for i in range(cpp_model.getNumReservoirs())
            ]
            esn._readout_params = _readout_config_from_cpp(cpp_model.getReadout())
        except (TypeError, ValueError) as err:
            msg = f"Invalid model file: {err}"
            raise _rclib.SerializationError(msg) from err
        return esn

    def __getstate__(self) -> dict[str, Any]:
        """Support pickle and copy.deepcopy.

        Every instance attribute is kept, including ones added by users or
        subclasses; the C++ model is stored as bytes in the model file format.
        Unpickling can run arbitrary code, so only unpickle data you trust.
        """
        state = self.__dict__.copy()
        state["_cpp_model"] = self._cpp_model.dumps()
        return state

    def __setstate__(self, state: dict[str, Any]) -> None:
        """Restore a model pickled by __getstate__."""
        cpp_model = _rclib.Model.loads(state["_cpp_model"])  # before touching self, in case it fails
        self.__dict__.update(state)
        self._cpp_model = cpp_model

__getstate__()

Support pickle and copy.deepcopy.

Every instance attribute is kept, including ones added by users or subclasses; the C++ model is stored as bytes in the model file format. Unpickling can run arbitrary code, so only unpickle data you trust.

Source code in python/rclib/model.py
def __getstate__(self) -> dict[str, Any]:
    """Support pickle and copy.deepcopy.

    Every instance attribute is kept, including ones added by users or
    subclasses; the C++ model is stored as bytes in the model file format.
    Unpickling can run arbitrary code, so only unpickle data you trust.
    """
    state = self.__dict__.copy()
    state["_cpp_model"] = self._cpp_model.dumps()
    return state

__init__(connection_type='serial')

Initialize the ESN model.

Parameters:

Name Type Description Default
connection_type str

The type of connection between reservoirs ("serial" or "parallel"). Default is "serial".

'serial'
Source code in python/rclib/model.py
def __init__(self, connection_type: str = "serial") -> None:
    """Initialize the ESN model.

    Parameters
    ----------
    connection_type : str, optional
        The type of connection between reservoirs ("serial" or "parallel").
        Default is "serial".
    """
    if connection_type not in {"serial", "parallel"}:
        msg = "connection_type must be 'serial' or 'parallel'."
        raise ValueError(msg)

    self.connection_type = connection_type
    self._reservoirs_params: list[Any] = []  # Store parameters for Python-side reservoir objects
    self._readout_params: Any = None  # Store parameters for Python-side readout object
    self._cpp_model = _rclib.Model()  # Initialize the C++ Model object

__setstate__(state)

Restore a model pickled by getstate.

Source code in python/rclib/model.py
def __setstate__(self, state: dict[str, Any]) -> None:
    """Restore a model pickled by __getstate__."""
    cpp_model = _rclib.Model.loads(state["_cpp_model"])  # before touching self, in case it fails
    self.__dict__.update(state)
    self._cpp_model = cpp_model

add_reservoir(reservoir)

Add a reservoir to the model.

Parameters:

Name Type Description Default
reservoir Any

The reservoir object to add.

required

Raises:

Type Description
TypeError

If the reservoir type is unsupported.

ValueError

If a RandomSparse reservoir has an unsupported spectral_radius_method.

Source code in python/rclib/model.py
def add_reservoir(self, reservoir: Any) -> None:  # noqa: ANN401
    """Add a reservoir to the model.

    Parameters
    ----------
    reservoir : Any
        The reservoir object to add.

    Raises
    ------
    TypeError
        If the reservoir type is unsupported.
    ValueError
        If a RandomSparse reservoir has an unsupported spectral_radius_method.
    """
    # Create and add the C++ reservoir to the C++ model
    if isinstance(reservoir, reservoirs.RandomSparse):
        if reservoir.spectral_radius_method not in _SPECTRAL_RADIUS_METHODS:
            msg = f"Unsupported spectral_radius_method: {reservoir.spectral_radius_method}"
            raise ValueError(msg)
        cpp_res = _rclib.RandomSparseReservoir(
            reservoir.n_neurons,
            reservoir.spectral_radius,
            reservoir.sparsity,
            reservoir.leak_rate,
            reservoir.input_scaling,
            reservoir.include_bias,
            reservoir.seed,
            _SPECTRAL_RADIUS_METHODS[reservoir.spectral_radius_method],
        )
        self._cpp_model.addReservoir(cpp_res, self.connection_type)
    elif isinstance(reservoir, reservoirs.Nvar):
        cpp_res = _rclib.NvarReservoir(reservoir.num_lags, reservoir.polynomial_order)
        self._cpp_model.addReservoir(cpp_res, self.connection_type)
    # Add other reservoir types here as they are implemented
    else:
        msg = "Unsupported reservoir type"
        raise TypeError(msg)

    # Store the Python reservoir object's parameters only once the C++ model holds the
    # reservoir, so a rejected reservoir leaves both sides unchanged.
    self._reservoirs_params.append(reservoir)

    # Update readout in case it's using "auto" solver
    self._update_readout()

fit(x, y, washout_len=0)

Fit the model to the data.

Parameters:

Name Type Description Default
x ArrayLike

Input data.

required
y ArrayLike

Target data.

required
washout_len int

Number of initial samples to discard. Default is 0.

0
Source code in python/rclib/model.py
def fit(self, x: ArrayLike, y: ArrayLike, washout_len: int = 0) -> None:
    """Fit the model to the data.

    Parameters
    ----------
    x : ArrayLike
        Input data.
    y : ArrayLike
        Target data.
    washout_len : int, optional
        Number of initial samples to discard. Default is 0.
    """
    # Ensure readout is correctly initialized (especially for "auto" solver)
    self._update_readout()
    # Call the C++ model's fit method
    self._cpp_model.fit(x, y, washout_len)

fit_sequences(inputs, targets, washout_len=0)

Fit the readout once on several independent sequences, such as episodes.

Each sequence starts from reset reservoirs and discards its own first washout_len samples. The remaining reservoir states and targets of all sequences are stacked and the readout is fitted once, so nothing carries over from one sequence to the next. With a single sequence this equals :meth:fit.

Parameters:

Name Type Description Default
inputs Sequence[ArrayLike]

Input data, one 2-D array of shape (n_samples_i, n_inputs) per sequence, for example a list of arrays. Sequences may differ in length but not in width.

required
targets Sequence[ArrayLike]

Target data, one 2-D array of shape (n_samples_i, n_outputs) per sequence, with as many rows as the matching input sequence.

required
washout_len int

Number of initial samples to discard from each sequence. Default is 0.

0

Raises:

Type Description
ValueError

If a single 2-D array is passed instead of a sequence of arrays, if there are no sequences or unequal numbers of input and target sequences, or if a sequence is empty, its inputs and targets differ in length, its width differs from the first sequence's, or its input width differs from the one the reservoirs are locked to by earlier input. Messages name the sequence index.

IndexError

If washout_len is negative or not shorter than a sequence.

RuntimeError

If the model has no reservoir or no readout.

Notes

These checks run before the model changes. Afterwards the reservoirs hold their states at the end of the last sequence.

Source code in python/rclib/model.py
def fit_sequences(self, inputs: Sequence[ArrayLike], targets: Sequence[ArrayLike], washout_len: int = 0) -> None:
    """Fit the readout once on several independent sequences, such as episodes.

    Each sequence starts from reset reservoirs and discards its own first
    ``washout_len`` samples. The remaining reservoir states and targets of all
    sequences are stacked and the readout is fitted once, so nothing carries over
    from one sequence to the next. With a single sequence this equals :meth:`fit`.

    Parameters
    ----------
    inputs : Sequence[ArrayLike]
        Input data, one 2-D array of shape (n_samples_i, n_inputs) per sequence,
        for example a list of arrays. Sequences may differ in length but not in
        width.
    targets : Sequence[ArrayLike]
        Target data, one 2-D array of shape (n_samples_i, n_outputs) per sequence,
        with as many rows as the matching input sequence.
    washout_len : int, optional
        Number of initial samples to discard from each sequence. Default is 0.

    Raises
    ------
    ValueError
        If a single 2-D array is passed instead of a sequence of arrays, if there
        are no sequences or unequal numbers of input and target sequences, or if a
        sequence is empty, its inputs and targets differ in length, its width
        differs from the first sequence's, or its input width differs from the one
        the reservoirs are locked to by earlier input. Messages name the sequence
        index.
    IndexError
        If ``washout_len`` is negative or not shorter than a sequence.
    RuntimeError
        If the model has no reservoir or no readout.

    Notes
    -----
    These checks run before the model changes. Afterwards the reservoirs hold
    their states at the end of the last sequence.
    """
    # A single 2-D array would otherwise be split into one-row sequences.
    for name, sequences in (("inputs", inputs), ("targets", targets)):
        if isinstance(sequences, np.ndarray) and sequences.ndim == 2:  # noqa: PLR2004
            msg = f"{name} must be a sequence of 2-D arrays, one per sequence, not a single 2-D array."
            raise ValueError(msg)
    self._update_readout()
    self._cpp_model.fitSequences(inputs, targets, washout_len)

get_readout()

Get the readout object, such as a fitted ridge readout to read its weights from.

Returns:

Type Description
Any

The C++ readout object.

Raises:

Type Description
RuntimeError

If no readout is set.

Source code in python/rclib/model.py
def get_readout(self) -> Any:  # noqa: ANN401
    """Get the readout object, such as a fitted ridge readout to read its weights from.

    Returns
    -------
    Any
        The C++ readout object.

    Raises
    ------
    RuntimeError
        If no readout is set.
    """
    return self._cpp_model.getReadout()

get_reservoir(index)

Get the reservoir object at the specified index.

Parameters:

Name Type Description Default
index int

The index of the reservoir.

required

Returns:

Type Description
Any

The C++ reservoir object.

Source code in python/rclib/model.py
def get_reservoir(self, index: int) -> Any:  # noqa: ANN401
    """Get the reservoir object at the specified index.

    Parameters
    ----------
    index : int
        The index of the reservoir.

    Returns
    -------
    Any
        The C++ reservoir object.
    """
    # Return the C++ reservoir object
    return self._cpp_model.getReservoir(index)

load(path) classmethod

Load a model saved by :meth:save or by C++ Model::save.

Only load files from sources you trust: the format contains no executable code, but it is parsed by native code.

Parameters:

Name Type Description Default
path str or PathLike

Model file to read.

required

Returns:

Type Description
ESN

The restored model, including its reservoir states.

Raises:

Type Description
SerializationError

If the file cannot be read or is not a valid model file.

Source code in python/rclib/model.py
@classmethod
def load(cls, path: str | os.PathLike[str]) -> ESN:
    """Load a model saved by :meth:`save` or by C++ ``Model::save``.

    Only load files from sources you trust: the format contains no executable
    code, but it is parsed by native code.

    Parameters
    ----------
    path : str or os.PathLike
        Model file to read.

    Returns
    -------
    ESN
        The restored model, including its reservoir states.

    Raises
    ------
    SerializationError
        If the file cannot be read or is not a valid model file.
    """
    return cls._from_cpp_model(_rclib.Model.load(os.fspath(path)))

partial_fit(x, y)

Update the model with a single sample (online learning).

Parameters:

Name Type Description Default
x ArrayLike

Input data sample. If None, the reservoir state is not advanced (useful if predict_online was already called).

required
y ArrayLike

Target data sample.

required

Raises:

Type Description
RuntimeError

If no reservoir or readout is set.

Source code in python/rclib/model.py
def partial_fit(self, x: ArrayLike | None, y: ArrayLike) -> None:
    """Update the model with a single sample (online learning).

    Parameters
    ----------
    x : ArrayLike, optional
        Input data sample. If None, the reservoir state is not advanced
        (useful if predict_online was already called).
    y : ArrayLike
        Target data sample.

    Raises
    ------
    RuntimeError
        If no reservoir or readout is set.
    """
    # Assuming only one reservoir for simplicity in online learning for now.
    # If multiple reservoirs are present, the logic would need to be more complex
    # to handle how their states are combined before feeding to the readout.
    if not self._reservoirs_params:
        msg = "No reservoir added to the model."
        raise RuntimeError(msg)
    if not self._readout_params:
        msg = "No readout set for the model."
        raise RuntimeError(msg)

    # Ensure readout is correctly initialized
    self._update_readout()

    if x is None:
        cpp_readout = self._cpp_model.getReadout()
        states = [self._cpp_model.getReservoir(i).getState() for i in range(len(self._reservoirs_params))]
        if any(state.shape[1] == 0 for state in states):
            msg = "Reservoir state is uninitialized; call predict_online or partial_fit with input first."
            raise RuntimeError(msg)
        current_state = states[-1] if self.connection_type == "serial" else np.hstack(states)
        cpp_readout.partialFit(current_state, y)
    else:
        self._cpp_model.partialFit(x, y)

predict(x, *, reset_state_before_predict=True)

Predict using the trained model.

Parameters:

Name Type Description Default
x ArrayLike

Input data.

required
reset_state_before_predict bool

Whether to reset the reservoir state before prediction. Default is True.

True

Returns:

Type Description
ndarray

The predicted values.

Source code in python/rclib/model.py
def predict(self, x: ArrayLike, *, reset_state_before_predict: bool = True) -> np.ndarray:
    """Predict using the trained model.

    Parameters
    ----------
    x : ArrayLike
        Input data.
    reset_state_before_predict : bool, optional
        Whether to reset the reservoir state before prediction. Default is True.

    Returns
    -------
    np.ndarray
        The predicted values.
    """
    # Call the C++ model's predict method
    return self._cpp_model.predict(x, reset_state_before_predict)

predict_generative(prime_data, n_steps)

Generative prediction.

Advances the reservoirs through prime_data (if any), then generates n_steps outputs, feeding each output back as the next input. Every generated output, including the last, advances the reservoirs, so a following call with empty prime_data continues the sequence: generating a steps and then b steps equals generating a + b steps. With n_steps == 0 no output is fed back.

Parameters:

Name Type Description Default
prime_data ArrayLike

Initial data to prime the reservoir.

required
n_steps int

Number of steps to generate.

required

Returns:

Type Description
ndarray

The generated data.

Raises:

Type Description
ValueError

If a generated output cannot be fed back because its width differs from the input width the reservoirs are locked to (also for n_steps == 1).

Source code in python/rclib/model.py
def predict_generative(self, prime_data: ArrayLike, n_steps: int) -> np.ndarray:
    """Generative prediction.

    Advances the reservoirs through ``prime_data`` (if any), then generates
    ``n_steps`` outputs, feeding each output back as the next input. Every
    generated output, including the last, advances the reservoirs, so a following
    call with empty ``prime_data`` continues the sequence: generating ``a`` steps
    and then ``b`` steps equals generating ``a + b`` steps. With ``n_steps == 0``
    no output is fed back.

    Parameters
    ----------
    prime_data : ArrayLike
        Initial data to prime the reservoir.
    n_steps : int
        Number of steps to generate.

    Returns
    -------
    np.ndarray
        The generated data.

    Raises
    ------
    ValueError
        If a generated output cannot be fed back because its width differs from
        the input width the reservoirs are locked to (also for ``n_steps == 1``).
    """
    # Call the C++ model's predictGenerative method
    return self._cpp_model.predictGenerative(prime_data, n_steps)

predict_online(x)

Predict in online mode (updating state).

Parameters:

Name Type Description Default
x ArrayLike

Input data.

required

Returns:

Type Description
ndarray

The predicted values.

Source code in python/rclib/model.py
def predict_online(self, x: ArrayLike) -> np.ndarray:
    """Predict in online mode (updating state).

    Parameters
    ----------
    x : ArrayLike
        Input data.

    Returns
    -------
    np.ndarray
        The predicted values.
    """
    # Call the C++ model's predictOnline method
    return self._cpp_model.predictOnline(x)

reset_reservoirs()

Reset the states of all reservoirs.

Source code in python/rclib/model.py
def reset_reservoirs(self) -> None:
    """Reset the states of all reservoirs."""
    # Call the C++ model's resetReservoirs method
    self._cpp_model.resetReservoirs()

save(path)

Save the model to a file.

The file holds the configuration, the trained weights and the current reservoir states, so a loaded model continues exactly where this one stopped. It can also be loaded from C++ with Model::load. An existing file at path is replaced in one step and is left unchanged if saving fails.

Parameters:

Name Type Description Default
path str or PathLike

Destination file.

required

Raises:

Type Description
SerializationError

If the model lacks a reservoir or readout, is internally inconsistent, or the file cannot be written.

Source code in python/rclib/model.py
def save(self, path: str | os.PathLike[str]) -> None:
    """Save the model to a file.

    The file holds the configuration, the trained weights and the current
    reservoir states, so a loaded model continues exactly where this one
    stopped. It can also be loaded from C++ with ``Model::load``. An existing
    file at ``path`` is replaced in one step and is left unchanged if saving
    fails.

    Parameters
    ----------
    path : str or os.PathLike
        Destination file.

    Raises
    ------
    SerializationError
        If the model lacks a reservoir or readout, is internally inconsistent,
        or the file cannot be written.
    """
    self._cpp_model.save(os.fspath(path))

set_readout(readout)

Set the readout for the model.

Parameters:

Name Type Description Default
readout Any

The readout object to set.

required

Raises:

Type Description
TypeError

If the readout type is unsupported.

Source code in python/rclib/model.py
def set_readout(self, readout: Any) -> None:  # noqa: ANN401
    """Set the readout for the model.

    Parameters
    ----------
    readout : Any
        The readout object to set.

    Raises
    ------
    TypeError
        If the readout type is unsupported.
    """
    # Store the Python readout object's parameters
    self._readout_params = readout
    self._update_readout()