Project: s41467-024-44900-4 | Function: simulate (python)

system: You are an expert Python programmer implementing algorithms from scientific papers.
Complete the masked function based on the algorithm description.
Follow Python best practices and maintain consistent code style.
Return **only** the complete Python function code.

user: Complete this Python function:

**DESCRIPTION:** Core reservoir computing simulation that propagates external input through recurrent neural network dynamics with nonlinear activation functions

**KEY TERMS:** {
  "ext_input": "external input signal",
  "w": "reservoir connectivity matrix",
  "w_in": "input connectivity matrix",
  "_state": "reservoir activation states",
  "synap_input": "synaptic input",
  "activation_function": "nonlinear transformation"
}

**CONTEXT CODE:** """
Functionality for simulating reservoirs
"""
from abc import ABCMeta, abstractmethod
import numpy as np
from numpy.linalg import pinv
from . import utils


class Reservoir(metaclass=ABCMeta):
    """
    Class that represents a general Reservoir object

    ...

    Attributes
    ----------
    w : numpy.ndarray
        reservoir connectivity matrix (source, target)
    _state : numpy.ndarray
        reservoir activation states
    n_nodes : int
        dimension of the reservoir

    Methods
    ----------
    # TODO

    simulate

    add_washout_time

    """

    def __init__(self, w):
        """
        Constructor class for general Reservoir Networks

        Parameters
        ----------
        w : (N, N) numpy.ndarray
            reservoir connectivity matrix (source, target)
            N: number of nodes in the network. If w is directed, then rows
            (columns) should correspond to source (target) nodes.
        """
        self.w = w
        self._state = None
        self.n_nodes = len(self.w)

    @abstractmethod
    def simulate(self, *args, **kwargs):
        pass

    def add_washout_time(self, *args, idx_washout=0):
        """
        Add washout time to reservoir states and corresponding arrays (e.g., label, sample weight)
        'ext_input'

        Parameters
        ----------
        idx_washout: int
            index up to which the values of arrays should be deleted
        args: numpy.ndarray
            reservoir states and any additional arrays where washout is to be applied

        Returns
        -------
        args: numpy.ndarray
            same arrays as in args but after washout is applied
        """

        # delete initial indexes of arrays in args
        argout = tuple(a[idx_washout:] for a in args)

        return argout


class EchoStateNetwork(Reservoir):
    """
    Class that represents an Echo State Network

    ...

    Attributes
    ----------
    w : numpy.ndarray
        reservoir connectivity matrix (source, target)
    _state : numpy.ndarray
        reservoir activation states
    n_nodes : int
        dimension of the reservoir
    activation_function : {'tanh', 'piecewise'}
        type of activation function

    Methods
    -------
    # TODO

    simulate

    set_activation_function

    """

    def __init__(self, *args, activation_function='tanh', leak_rate=None, **kwargs):
        """
        Constructor class for Echo State Networks

        Parameters
        ----------
        w: (N, N) numpy.ndarray
            Reservoir connectivity matrix (source, target)
            N: number of nodes in the network. If w is directed, then rows
            (columns) should correspond to source (target) nodes.
        activation_function: str {'linear', 'elu', 'relu', 'leaky_relu',
            'sigmoid', 'tanh', 'step'}, default 'tanh'
            Activation function (nonlinearity of the system's units)
        leak_rate: float in (0, 1], default None
            A leaky integrator is used if leak_rate is not None.
            The leak rate forms a time constant to control the dynamics.
        """

        super().__init__(*args, **kwargs)

        # activation function
        self.activation_function = self.set_activation_function(
            activation_function)
        
        # leak rate
        self.leak_rate = leak_rate

    def simulate(
        self, ext_input, w_in, ic=None, output_nodes=None,
        return_states=True, **kwargs
    ):
        """
        Simulates reservoir dynamics given an external input signal
        'ext_input' and an input connectivity matrix 'w_in'

        Parameters
        ----------
        ext_input : (time, N_inputs) numpy.ndarray
            External input signal
            N_inputs: number of external input signals
        w_in : (N_inputs, N) numpy.ndarray
            Input connectivity matrix (source, target)
            N_inputs: number of external input signals
            N: number of nodes in the network
        ic : (N,) numpy.ndarray, optional
            Initial conditions


Complete the function implementation replacing "raise NotImplementedError".