Skip to content

Reference

Initialization

How frozen weights get their values: topology initializers for the square recurrent matrix, input/feedback initializers for the rectangular ones, the registries behind both, and the resolvers that accept names, tuples, callables, or configured objects interchangeably.

topology

Topology Initialization System

This module provides the interface between structure generators — NetworkX graphs or direct matrix builders — and PyTorch tensor initialization for reservoir recurrent weights.

CLASS DESCRIPTION
TopologyInitializer

Abstract base class for topology initializers.

GraphTopology

Concrete implementation using NetworkX graphs.

MatrixTopology

Concrete implementation wrapping any matrix-building callable.

FUNCTION DESCRIPTION
get_topology

Get a pre-configured topology by name.

show_topologies

List available topologies or get details.

register_graph_topology

Decorator to register graph-based topologies.

register_matrix_topology

Decorator to register matrix-builder topologies.

scale_to_spectral_radius

Rescale a square matrix to a target spectral radius.

estimate_spectral_radius

Estimate a matrix's largest absolute eigenvalue (power iteration / sparse eigs / tiny-N dense fallback).

Examples:

Using pre-registered topologies:

>>> from resdag.init.topology import get_topology
>>> topology = get_topology("erdos_renyi", p=0.1, seed=42)
>>> weight = torch.empty(100, 100)
>>> topology.initialize(weight, spectral_radius=0.9)

Any function that builds a matrix is a topology:

>>> def block_diagonal(n, blocks=4):
...     ...  # return an (n, n) tensor
>>> reservoir = ESNLayer(500, feedback_size=3, topology=block_diagonal)

Registering custom topologies:

>>> from resdag.init.topology import register_graph_topology, register_matrix_topology
>>> @register_graph_topology("custom", param=1.0)
... def my_custom_graph(n, param=1.0, seed=None):
...     G = nx.DiGraph()
...     # ... graph generation logic
...     return G
>>> @register_matrix_topology("block_diagonal", blocks=4)
... def block_diagonal(n, blocks=4, seed=None):
...     # ... matrix construction logic
...     return w
See Also

resdag.init.graphs : Graph generation functions. resdag.init.matrices : Direct matrix-construction functions. resdag.layers.ESNLayer : Uses topologies for weight initialization.

TopologyInitializer

Bases: ABC

Abstract base class for topology-based weight initialization.

Topology initializers convert graph structures into PyTorch weight tensors for reservoir layers. They extract the required size from the tensor shape, generate a graph, convert it to an adjacency matrix, and optionally apply spectral radius scaling.

Subclasses must implement the :meth:initialize method.

ATTRIBUTE DESCRIPTION
prescaled

Whether the topology bakes its own spectral structure into the recurrent matrix (graded radii, unit singular values, an analytically fixed spectral radius, ...). When True, :meth:initialize skips the outer :func:scale_to_spectral_radius rescale so that structure is preserved, and warns if a layer-level spectral_radius is also passed (the two collide). Defaults to False.

TYPE: bool

See Also

GraphTopology : Concrete implementation using NetworkX graphs. resdag.layers.ESNLayer : Uses topology initializers.

initialize abstractmethod

initialize(weight: Tensor, spectral_radius: float | None = None) -> Tensor

Initialize a weight tensor using graph topology.

PARAMETER DESCRIPTION
weight

The weight tensor to initialize, shape (n, n) for recurrent weights. This tensor is modified in-place.

TYPE: Tensor

spectral_radius

Target spectral radius for scaling. If None, no scaling is applied.

TYPE: float DEFAULT: None

RETURNS DESCRIPTION
Tensor

The initialized weight tensor (same as input, modified in-place).

Source code in src/resdag/init/topology/base.py
@abstractmethod
def initialize(
    self,
    weight: torch.Tensor,
    spectral_radius: float | None = None,
) -> torch.Tensor:
    """
    Initialize a weight tensor using graph topology.

    Parameters
    ----------
    weight : torch.Tensor
        The weight tensor to initialize, shape ``(n, n)`` for recurrent
        weights. This tensor is modified in-place.
    spectral_radius : float, optional
        Target spectral radius for scaling. If None, no scaling is applied.

    Returns
    -------
    torch.Tensor
        The initialized weight tensor (same as input, modified in-place).
    """
    pass

GraphTopology

GraphTopology(graph_func: Callable, graph_kwargs: dict[str, Any] | None = None, prescaled: bool = False)

Bases: TopologyInitializer

Topology initializer based on NetworkX graph functions.

This class wraps a graph generation function and converts it into a weight initializer. The graph function must accept n (number of nodes) as its first argument.

PARAMETER DESCRIPTION
graph_func

A function with signature (n: int, **kwargs) -> nx.Graph | nx.DiGraph. Must return a NetworkX graph with weighted edges.

TYPE: callable

graph_kwargs

Keyword arguments to pass to the graph function.

TYPE: dict DEFAULT: None

prescaled

If True, the graph builder already bakes its own spectral structure into the adjacency matrix (e.g. spectral_cascade's graded per-clique radii). The outer :func:scale_to_spectral_radius rescale is then skipped in :meth:initialize, and a warning is emitted if a spectral_radius is nonetheless requested, since the two meanings of spectral_radius collide.

TYPE: bool DEFAULT: False

ATTRIBUTE DESCRIPTION
graph_func

The graph generation function.

TYPE: callable

graph_kwargs

Keyword arguments for the graph function.

TYPE: dict

prescaled

Whether the outer spectral-radius rescale is suppressed (see above).

TYPE: bool

Examples:

Using a registered graph function:

>>> from resdag.init.graphs import erdos_renyi_graph
>>> import torch
>>>
>>> topology = GraphTopology(erdos_renyi_graph, {"p": 0.1, "directed": True})
>>> weight = torch.empty(100, 100)
>>> topology.initialize(weight, spectral_radius=0.9)

With the registry helper:

>>> from resdag.init.topology import get_topology
>>> topology = get_topology("erdos_renyi", p=0.15)
>>> topology.initialize(weight, spectral_radius=0.95)
See Also

resdag.init.graphs : Available graph generation functions. get_topology : Get pre-configured topology by name.

Source code in src/resdag/init/topology/base.py
def __init__(
    self,
    graph_func: Callable,
    graph_kwargs: dict[str, Any] | None = None,
    prescaled: bool = False,
):
    self.graph_func = graph_func
    self.graph_kwargs = graph_kwargs or {}
    self.prescaled = prescaled

initialize

initialize(weight: Tensor, spectral_radius: float | None = None) -> Tensor

Initialize weight tensor from graph topology.

Generates a graph using the stored function, converts it to an adjacency matrix, and optionally scales to the target spectral radius.

When this topology is :attr:prescaled, the outer spectral-radius rescale is skipped (the builder's own spectral structure is kept) and a warning is emitted if spectral_radius is not None.

PARAMETER DESCRIPTION
weight

Square tensor to initialize, shape (n, n).

TYPE: Tensor

spectral_radius

Target spectral radius for the weight matrix. Ignored (with a warning) when the topology is :attr:prescaled.

TYPE: float DEFAULT: None

RETURNS DESCRIPTION
Tensor

Initialized weight tensor (modified in-place).

RAISES DESCRIPTION
ValueError

If weight is not 2D or not square.

Source code in src/resdag/init/topology/base.py
def initialize(
    self,
    weight: torch.Tensor,
    spectral_radius: float | None = None,
) -> torch.Tensor:
    """
    Initialize weight tensor from graph topology.

    Generates a graph using the stored function, converts it to an
    adjacency matrix, and optionally scales to the target spectral radius.

    When this topology is :attr:`prescaled`, the outer spectral-radius
    rescale is skipped (the builder's own spectral structure is kept) and a
    warning is emitted if ``spectral_radius`` is not ``None``.

    Parameters
    ----------
    weight : torch.Tensor
        Square tensor to initialize, shape ``(n, n)``.
    spectral_radius : float, optional
        Target spectral radius for the weight matrix. Ignored (with a
        warning) when the topology is :attr:`prescaled`.

    Returns
    -------
    torch.Tensor
        Initialized weight tensor (modified in-place).

    Raises
    ------
    ValueError
        If weight is not 2D or not square.
    """
    if weight.ndim != 2:
        raise ValueError(f"Weight must be 2D, got shape {weight.shape}")

    if weight.shape[0] != weight.shape[1]:
        raise ValueError(f"Weight must be square, got shape {weight.shape}")

    n = weight.shape[0]
    device = weight.device
    dtype = weight.dtype

    # Dense fast path: generators decorated with ``register_dense_adjacency``
    # advertise a vectorized NumPy builder, so the dense adjacency is built
    # directly and the (pure-overhead) NetworkX round-trip is skipped.
    adj_matrix = self._build_adjacency(n)

    # Convert to torch tensor
    weight_data = torch.from_numpy(adj_matrix).to(device=device, dtype=dtype)

    # Apply spectral radius scaling if requested.  Pre-scaled topologies bake
    # their own spectral structure into the adjacency matrix, so the outer
    # rescale is suppressed (and the collision is surfaced as a warning).
    if spectral_radius is not None:
        if self.prescaled:
            _warn_prescaled_override(
                getattr(self.graph_func, "__name__", repr(self.graph_func)),
                spectral_radius,
            )
        else:
            weight_data = self._scale_spectral_radius(weight_data, spectral_radius)

    # Copy into weight tensor
    with torch.no_grad():
        weight.copy_(weight_data)

    return weight

MatrixTopology

MatrixTopology(matrix_func: Callable, matrix_kwargs: dict[str, Any] | None = None, prescaled: bool = False, seed: int | None = None)

Bases: TopologyInitializer

Topology initializer wrapping any matrix-building callable.

This is the general-purpose escape hatch of the topology system: any function with logic for constructing a recurrent weight matrix becomes a topology, with full access to spectral-radius scaling, the registry, and the ESNLayer(topology=...) shorthand. No graph required.

Two calling conventions are supported, tried in order:

  1. Build stylefn(n, **kwargs) returning an (n, n) array-like: a torch.Tensor, a numpy.ndarray, or even a networkx graph (converted to its adjacency matrix).
  2. In-place stylefn(tensor, **kwargs) mutating a tensor in place, e.g. any torch.nn.init.*_ function.
PARAMETER DESCRIPTION
matrix_func

The matrix-building function (build style or in-place style).

TYPE: callable

matrix_kwargs

Keyword arguments bound to the function.

TYPE: dict DEFAULT: None

prescaled

If True, the builder already fixes the matrix's spectral structure (e.g. orthogonal's unit singular values, or fast_spectral_initialization's analytically targeted radius). The outer :func:scale_to_spectral_radius rescale is then skipped in :meth:initialize, and a warning is emitted if a spectral_radius is nonetheless requested.

TYPE: bool DEFAULT: False

seed

Reproducibility seed for in-place builders that take a generator= keyword (e.g. :func:torch.nn.init.orthogonal_). At :meth:initialize time a :class:torch.Generator seeded from seed on the weight's device is passed to the builder, so two topologies built with the same seed produce bit-identical recurrent matrices. Builders that take a seed argument directly receive it through matrix_kwargs instead and ignore this. Defaults to None (draws from torch's global RNG).

TYPE: int DEFAULT: None

Examples:

A plain function as a topology:

>>> import torch
>>> def block_diagonal(n, blocks=4):
...     w = torch.zeros(n, n)
...     size = n // blocks
...     for b in range(blocks):
...         s = b * size
...         w[s : s + size, s : s + size] = torch.randn(size, size)
...     return w
>>> topology = MatrixTopology(block_diagonal, {"blocks": 5})
>>> weight = torch.empty(100, 100)
>>> topology.initialize(weight, spectral_radius=0.9)

Bare callables passed to ESNLayer are wrapped automatically:

>>> from resdag.layers import ESNLayer
>>> layer = ESNLayer(100, feedback_size=3, topology=block_diagonal)
>>> layer = ESNLayer(100, feedback_size=3, topology=(block_diagonal, {"blocks": 2}))

torch.nn.init functions work directly (in-place style):

>>> layer = ESNLayer(100, feedback_size=3, topology=torch.nn.init.orthogonal_)
See Also

GraphTopology : Graph-based topologies. resdag.init.topology.register_matrix_topology : Register by name.

Source code in src/resdag/init/topology/base.py
def __init__(
    self,
    matrix_func: Callable,
    matrix_kwargs: dict[str, Any] | None = None,
    prescaled: bool = False,
    seed: int | None = None,
):
    self.matrix_func = matrix_func
    self.matrix_kwargs = matrix_kwargs or {}
    self.prescaled = prescaled
    self.seed = seed

initialize

initialize(weight: Tensor, spectral_radius: float | None = None) -> Tensor

Initialize a square weight tensor from the wrapped callable.

When this topology is :attr:prescaled, the outer spectral-radius rescale is skipped (the builder's own spectral structure is kept) and a warning is emitted if spectral_radius is not None.

PARAMETER DESCRIPTION
weight

Square tensor to initialize, shape (n, n). Modified in-place.

TYPE: Tensor

spectral_radius

Target spectral radius. If None, no scaling is applied. Ignored (with a warning) when the topology is :attr:prescaled.

TYPE: float DEFAULT: None

RETURNS DESCRIPTION
Tensor

Initialized weight tensor (modified in-place).

RAISES DESCRIPTION
ValueError

If weight is not 2-D square, if the callable matches neither calling convention, or if the built matrix has the wrong shape.

Source code in src/resdag/init/topology/base.py
def initialize(
    self,
    weight: torch.Tensor,
    spectral_radius: float | None = None,
) -> torch.Tensor:
    """
    Initialize a square weight tensor from the wrapped callable.

    When this topology is :attr:`prescaled`, the outer spectral-radius
    rescale is skipped (the builder's own spectral structure is kept) and a
    warning is emitted if ``spectral_radius`` is not ``None``.

    Parameters
    ----------
    weight : torch.Tensor
        Square tensor to initialize, shape ``(n, n)``. Modified in-place.
    spectral_radius : float, optional
        Target spectral radius. If None, no scaling is applied. Ignored
        (with a warning) when the topology is :attr:`prescaled`.

    Returns
    -------
    torch.Tensor
        Initialized weight tensor (modified in-place).

    Raises
    ------
    ValueError
        If ``weight`` is not 2-D square, if the callable matches neither
        calling convention, or if the built matrix has the wrong shape.
    """
    if weight.ndim != 2:
        raise ValueError(f"Weight must be 2D, got shape {weight.shape}")
    if weight.shape[0] != weight.shape[1]:
        raise ValueError(f"Weight must be square, got shape {weight.shape}")

    n = weight.shape[0]

    result = _call_matrix_builder(
        self.matrix_func, weight, (n,), self.matrix_kwargs, seed=self.seed
    )
    matrix = _coerce_to_matrix(result, (n, n), weight.device, weight.dtype)

    # Pre-scaled builders fix their own spectral structure (unit singular
    # values, analytic radius, ...), so the outer rescale is suppressed and
    # the collision is surfaced as a warning rather than silently applied.
    if spectral_radius is not None:
        if self.prescaled:
            _warn_prescaled_override(
                getattr(self.matrix_func, "__name__", repr(self.matrix_func)),
                spectral_radius,
            )
        else:
            matrix = scale_to_spectral_radius(matrix, spectral_radius)

    with torch.no_grad():
        weight.copy_(matrix)

    return weight

get_topology

get_topology(name: str, **override_kwargs: Any) -> TopologyInitializer

Get a pre-configured topology initializer by name.

PARAMETER DESCRIPTION
name

Name of the topology (e.g., "erdos_renyi", "watts_strogatz").

TYPE: str

**override_kwargs

Keyword arguments to override default graph parameters.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
TopologyInitializer

Configured topology initializer (:class:GraphTopology or :class:MatrixTopology, depending on how the name was registered).

RAISES DESCRIPTION
ValueError

If topology name is not registered.

Examples:

Basic usage:

>>> topology = get_topology("erdos_renyi", p=0.15, seed=42)
>>> weight = torch.empty(100, 100)
>>> topology.initialize(weight, spectral_radius=0.9)

With ESNLayer:

>>> from resdag.layers import ESNLayer
>>> reservoir = ESNLayer(
...     reservoir_size=500,
...     feedback_size=10,
...     topology=get_topology("watts_strogatz", k=4, p=0.3),
...     spectral_radius=0.95,
... )
See Also

show_topologies : List available topologies. register_graph_topology : Register new topologies.

Source code in src/resdag/init/topology/registry.py
def get_topology(
    name: str,
    **override_kwargs: Any,
) -> TopologyInitializer:
    """
    Get a pre-configured topology initializer by name.

    Parameters
    ----------
    name : str
        Name of the topology (e.g., ``"erdos_renyi"``, ``"watts_strogatz"``).
    **override_kwargs
        Keyword arguments to override default graph parameters.

    Returns
    -------
    TopologyInitializer
        Configured topology initializer (:class:`GraphTopology` or
        :class:`MatrixTopology`, depending on how the name was registered).

    Raises
    ------
    ValueError
        If topology name is not registered.

    Examples
    --------
    Basic usage:

    >>> topology = get_topology("erdos_renyi", p=0.15, seed=42)
    >>> weight = torch.empty(100, 100)
    >>> topology.initialize(weight, spectral_radius=0.9)

    With ESNLayer:

    >>> from resdag.layers import ESNLayer
    >>> reservoir = ESNLayer(
    ...     reservoir_size=500,
    ...     feedback_size=10,
    ...     topology=get_topology("watts_strogatz", k=4, p=0.3),
    ...     spectral_radius=0.95,
    ... )

    See Also
    --------
    show_topologies : List available topologies.
    register_graph_topology : Register new topologies.
    """
    if name not in _TOPOLOGY_REGISTRY:
        available = ", ".join(_TOPOLOGY_REGISTRY.keys())
        raise ValueError(f"Unknown topology '{name}'. Available topologies: {available}")

    builder_func, default_kwargs, wrapper_class, prescaled = _TOPOLOGY_REGISTRY[name]

    # Merge default kwargs with overrides
    kwargs = {**default_kwargs, **override_kwargs}

    return wrapper_class(builder_func, kwargs, prescaled=prescaled)

register_graph_topology

register_graph_topology(name: str, prescaled: bool = False, **default_kwargs: Any) -> Callable[[Callable], Callable]

Decorator to register a graph function as a topology.

Registers a graph generation function in the topology registry at definition time, making it available for use with :class:~resdag.layers.ESNLayer.

PARAMETER DESCRIPTION
name

Unique name for the topology.

TYPE: str

prescaled

Mark the topology as already carrying its own spectral structure (e.g. spectral_cascade's graded per-clique radii). When True the wrapper :class:~resdag.init.topology.GraphTopology skips the outer :func:~resdag.init.topology.scale_to_spectral_radius rescale and warns if a layer-level spectral_radius is also requested.

TYPE: bool DEFAULT: False

**default_kwargs

Default keyword arguments for the graph function.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
callable

Decorator function that registers and returns the graph function.

RAISES DESCRIPTION
ValueError

If a topology with the same name is already registered.

Examples:

>>> @register_graph_topology("my_graph", p=0.1, directed=True)
... def my_graph(n, p=0.1, directed=False, seed=None):
...     G = nx.DiGraph() if directed else nx.Graph()
...     # ... graph generation logic
...     return G
Notes
  • Graph functions must accept n (number of nodes) as first parameter.
  • Graph functions must return nx.Graph or nx.DiGraph with weighted edges.
  • Registered topologies can be accessed via :func:get_topology.
Source code in src/resdag/init/topology/registry.py
def register_graph_topology(
    name: str,
    prescaled: bool = False,
    **default_kwargs: Any,
) -> Callable[[Callable], Callable]:
    """
    Decorator to register a graph function as a topology.

    Registers a graph generation function in the topology registry at
    definition time, making it available for use with
    :class:`~resdag.layers.ESNLayer`.

    Parameters
    ----------
    name : str
        Unique name for the topology.
    prescaled : bool, default=False
        Mark the topology as already carrying its own spectral structure (e.g.
        ``spectral_cascade``'s graded per-clique radii). When ``True`` the
        wrapper :class:`~resdag.init.topology.GraphTopology` skips the outer
        :func:`~resdag.init.topology.scale_to_spectral_radius` rescale and warns
        if a layer-level ``spectral_radius`` is also requested.
    **default_kwargs
        Default keyword arguments for the graph function.

    Returns
    -------
    callable
        Decorator function that registers and returns the graph function.

    Raises
    ------
    ValueError
        If a topology with the same name is already registered.

    Examples
    --------
    >>> @register_graph_topology("my_graph", p=0.1, directed=True)
    ... def my_graph(n, p=0.1, directed=False, seed=None):
    ...     G = nx.DiGraph() if directed else nx.Graph()
    ...     # ... graph generation logic
    ...     return G

    Notes
    -----
    - Graph functions must accept ``n`` (number of nodes) as first parameter.
    - Graph functions must return ``nx.Graph`` or ``nx.DiGraph`` with weighted edges.
    - Registered topologies can be accessed via :func:`get_topology`.
    """

    def decorator(graph_func: Callable) -> Callable:
        if name in _TOPOLOGY_REGISTRY:
            raise ValueError(f"Topology '{name}' is already registered")
        _TOPOLOGY_REGISTRY[name] = (graph_func, default_kwargs, GraphTopology, prescaled)
        return graph_func

    return decorator

register_matrix_topology

register_matrix_topology(name: str, prescaled: bool = False, **default_kwargs: Any) -> Callable[[Callable], Callable]

Decorator to register a matrix-building function as a topology.

The complement of :func:register_graph_topology for builders that construct the recurrent weight matrix directly — no graph involved. Any logic that produces a square matrix qualifies.

PARAMETER DESCRIPTION
name

Unique name for the topology.

TYPE: str

prescaled

Mark the topology as already fixing its own spectral structure (e.g. orthogonal's unit singular values, or fast_spectral_initialization's analytically targeted radius). When True the wrapper :class:~resdag.init.topology.MatrixTopology skips the outer :func:~resdag.init.topology.scale_to_spectral_radius rescale and warns if a layer-level spectral_radius is also requested.

TYPE: bool DEFAULT: False

**default_kwargs

Default keyword arguments for the matrix function.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
callable

Decorator function that registers and returns the matrix function.

RAISES DESCRIPTION
ValueError

If a topology with the same name is already registered.

Examples:

>>> @register_matrix_topology("block_diagonal", blocks=4)
... def block_diagonal(n, blocks=4, seed=None):
...     w = torch.zeros(n, n)
...     size = n // blocks
...     for b in range(blocks):
...         s = b * size
...         w[s : s + size, s : s + size] = torch.randn(size, size)
...     return w
>>> reservoir = ESNLayer(500, feedback_size=3, topology="block_diagonal")
Notes
  • Matrix functions must accept n (matrix size) as first parameter and return an (n, n) torch.Tensor or numpy.ndarray.
  • Registered topologies are accessed via :func:get_topology or by name in ESNLayer(topology="..."), identically to graph topologies.
Source code in src/resdag/init/topology/registry.py
def register_matrix_topology(
    name: str,
    prescaled: bool = False,
    **default_kwargs: Any,
) -> Callable[[Callable], Callable]:
    """
    Decorator to register a matrix-building function as a topology.

    The complement of :func:`register_graph_topology` for builders that
    construct the recurrent weight matrix directly — no graph involved.
    Any logic that produces a square matrix qualifies.

    Parameters
    ----------
    name : str
        Unique name for the topology.
    prescaled : bool, default=False
        Mark the topology as already fixing its own spectral structure (e.g.
        ``orthogonal``'s unit singular values, or
        ``fast_spectral_initialization``'s analytically targeted radius). When
        ``True`` the wrapper :class:`~resdag.init.topology.MatrixTopology` skips
        the outer :func:`~resdag.init.topology.scale_to_spectral_radius` rescale
        and warns if a layer-level ``spectral_radius`` is also requested.
    **default_kwargs
        Default keyword arguments for the matrix function.

    Returns
    -------
    callable
        Decorator function that registers and returns the matrix function.

    Raises
    ------
    ValueError
        If a topology with the same name is already registered.

    Examples
    --------
    >>> @register_matrix_topology("block_diagonal", blocks=4)
    ... def block_diagonal(n, blocks=4, seed=None):
    ...     w = torch.zeros(n, n)
    ...     size = n // blocks
    ...     for b in range(blocks):
    ...         s = b * size
    ...         w[s : s + size, s : s + size] = torch.randn(size, size)
    ...     return w

    >>> reservoir = ESNLayer(500, feedback_size=3, topology="block_diagonal")

    Notes
    -----
    - Matrix functions must accept ``n`` (matrix size) as first parameter and
      return an ``(n, n)`` ``torch.Tensor`` or ``numpy.ndarray``.
    - Registered topologies are accessed via :func:`get_topology` or by name
      in ``ESNLayer(topology="...")``, identically to graph topologies.
    """

    def decorator(matrix_func: Callable) -> Callable:
        if name in _TOPOLOGY_REGISTRY:
            raise ValueError(f"Topology '{name}' is already registered")
        _TOPOLOGY_REGISTRY[name] = (matrix_func, default_kwargs, MatrixTopology, prescaled)
        return matrix_func

    return decorator

scale_to_spectral_radius

scale_to_spectral_radius(matrix: Tensor, target_radius: float) -> Tensor

Rescale a square matrix so its spectral radius equals target_radius.

The current spectral radius is obtained from :func:estimate_spectral_radius, which picks the exact dense eigvals (small N), scipy eigs (large N, sparse or dense), or a power-iteration fallback automatically. Returns the matrix unchanged when its current spectral radius is (numerically) zero — e.g. the zero topology or a nilpotent matrix.

This is the single shared rescale implementation used by both :class:GraphTopology/:class:MatrixTopology and :class:resdag.layers.cells.esn_cell.ESNCell.

PARAMETER DESCRIPTION
matrix

Square matrix to rescale.

TYPE: Tensor

target_radius

Desired largest absolute eigenvalue.

TYPE: float

RETURNS DESCRIPTION
Tensor

The rescaled matrix (new tensor).

Source code in src/resdag/init/topology/base.py
def scale_to_spectral_radius(matrix: torch.Tensor, target_radius: float) -> torch.Tensor:
    """Rescale a square matrix so its spectral radius equals ``target_radius``.

    The current spectral radius is obtained from
    :func:`estimate_spectral_radius`, which picks the exact dense ``eigvals``
    (small N), scipy ``eigs`` (large N, sparse or dense), or a power-iteration
    fallback automatically.  Returns the matrix unchanged when its current
    spectral radius is (numerically) zero — e.g. the ``zero`` topology or a
    nilpotent matrix.

    This is the single shared rescale implementation used by both
    :class:`GraphTopology`/:class:`MatrixTopology` and
    :class:`resdag.layers.cells.esn_cell.ESNCell`.

    Parameters
    ----------
    matrix : torch.Tensor
        Square matrix to rescale.
    target_radius : float
        Desired largest absolute eigenvalue.

    Returns
    -------
    torch.Tensor
        The rescaled matrix (new tensor).
    """
    current_radius = estimate_spectral_radius(matrix)

    if current_radius > 1e-8:
        matrix = matrix * (target_radius / current_radius)
    elif abs(target_radius) > 1e-8:
        # A nilpotent (all-eigenvalues-zero, e.g. the pure ``delay_line``) or
        # all-zero matrix cannot be rescaled to a nonzero radius — scaling a zero
        # spectrum leaves it zero. Warn rather than silently ignore the request,
        # so ``ESNLayer(topology="delay_line", spectral_radius=0.9)`` does not look
        # like it took effect. Control the scale through the topology's own weight
        # parameter instead.
        warnings.warn(
            f"spectral_radius={target_radius:g} was requested but the recurrent "
            f"matrix has a zero spectral radius (nilpotent or all-zero, e.g. the "
            f"'delay_line' / 'zeros' topologies); it cannot be rescaled to a nonzero "
            f"radius and is left unchanged. Set the scale via the topology's weight "
            f"parameter instead.",
            RuntimeWarning,
            stacklevel=2,
        )

    return matrix

show_topologies

show_topologies(name: str | None = None) -> list[str] | None

Show available topologies or details for a specific topology.

PARAMETER DESCRIPTION
name

Name of topology to inspect. If None, prints all registered topology names and returns them as a list.

TYPE: str DEFAULT: None

RETURNS DESCRIPTION
list of str or None

When name is None, returns the sorted list of registered topology names (in addition to printing them). When name is provided, returns None after printing the parameter table.

RAISES DESCRIPTION
ValueError

If the specified topology name is not registered.

Source code in src/resdag/init/topology/registry.py
def show_topologies(name: str | None = None) -> list[str] | None:
    """
    Show available topologies or details for a specific topology.

    Parameters
    ----------
    name : str, optional
        Name of topology to inspect. If None, prints all
        registered topology names *and* returns them as a list.

    Returns
    -------
    list of str or None
        When ``name is None``, returns the sorted list of registered
        topology names (in addition to printing them).  When ``name`` is
        provided, returns ``None`` after printing the parameter table.

    Raises
    ------
    ValueError
        If the specified topology name is not registered.
    """
    if name is None:
        names = sorted(_TOPOLOGY_REGISTRY)
        print("\nAvailable topologies:\n")
        for n in names:
            print(f"  - {n}")
        print(f"\nTotal: {len(names)}\n")
        return names

    if name not in _TOPOLOGY_REGISTRY:
        available = "\n".join(sorted(_TOPOLOGY_REGISTRY.keys()))
        raise ValueError(f"Unknown topology '{name}'.\nAvailable:\n{available}")

    builder_func, default_kwargs, wrapper_class, prescaled = _TOPOLOGY_REGISTRY[name]

    sig = inspect.signature(builder_func)

    kind = "graph" if wrapper_class is GraphTopology else "matrix"
    prescaled_note = ", pre-scaled" if prescaled else ""
    print(f"\nTopology: {name} ({kind}{prescaled_note})\n")
    print("Parameters:\n")

    for param_name, param in sig.parameters.items():
        if param_name == "n":
            continue

        # Type extraction
        if param.annotation is not inspect.Parameter.empty:
            origin = get_origin(param.annotation)
            if origin is None:
                type_str = param.annotation.__name__
            else:
                args = get_args(param.annotation)
                type_str = " | ".join(a.__name__ for a in args)
        else:
            type_str = "Any"

        # Default resolution
        if param.default is not inspect.Parameter.empty:
            default = param.default
        else:
            default = default_kwargs.get(param_name, "<required>")

        print(f"  - {param_name}")
        print(f"      type:    {type_str}")
        print(f"      default: {default}\n")

    return None

input_feedback

Input/Feedback Weight Initialization

This module contains initializers for rectangular weight matrices used in reservoir input and feedback connections.

For reservoirs: - Feedback weights: (reservoir_size, feedback_size) - Input weights: (reservoir_size, input_size)

CLASS DESCRIPTION
InputFeedbackInitializer

Abstract base class for all initializers.

RandomInputInitializer

Uniform random in [-1, 1] (baseline).

RandomBinaryInitializer

Binary {-1, +1} values.

NormalInputInitializer

Gaussian Normal(loc, scale) entries (reservoirpy mat_gen.normal).

UniformInputInitializer

Bounded Uniform(low, high) entries (reservoirpy mat_gen.uniform).

BernoulliInputInitializer

Bernoulli ±1 signs with density (reservoirpy mat_gen.bernoulli).

PseudoDiagonalInitializer

Structured block-diagonal pattern.

ChebyshevInitializer

Deterministic chaotic initialization.

LogisticMappingInitializer

Deterministic logistic-map initialization (RC.jl logistic_mapping).

MinimalInputInitializer

Rodan & Tiňo minimal-complexity dense input (RC.jl minimal_init).

WeightedInputInitializer

Block-diagonal random input (RC.jl weighted_init).

WeightedMinimalInitializer

Block-diagonal constant-magnitude input (RC.jl weighted_minimal).

InformedInputInitializer

State/model split for hybrid ESNs (RC.jl informed_init).

ChessboardInitializer

Alternating {-1, +1} pattern.

BinaryBalancedInitializer

Hadamard-based balanced initialization.

OppositeAnchorsInitializer

Opposite anchor points on ring.

DendrocycleInputInitializer

Specific to dendrocycle topology.

ChainOfNeuronsInputInitializer

Specific to chain-of-neurons topology.

RingWindowInputInitializer

Windowed inputs on ring topology.

ZeroInitializer

Sets all weights to zero.

FunctionInitializer

Wraps any matrix-building callable as an initializer (the input/feedback analog of a matrix-builder topology).

FUNCTION DESCRIPTION
get_input_feedback

Get an initializer by name.

show_input_initializers

List available initializers or get details.

register_input_feedback

Decorator to register new initializers.

Examples:

Using pre-registered initializers:

>>> from resdag.init.input_feedback import get_input_feedback
>>> initializer = get_input_feedback("random", input_scaling=0.5)
>>> weight = torch.empty(100, 10)
>>> initializer.initialize(weight)

Registering custom initializers:

>>> from resdag.init.input_feedback import register_input_feedback
>>> @register_input_feedback("my_init", scaling=0.5)
... class MyInitializer(InputFeedbackInitializer):
...     def __init__(self, scaling=0.5):
...         self.scaling = scaling
...     def initialize(self, weight, **kwargs):
...         torch.nn.init.uniform_(weight, -self.scaling, self.scaling)
...         return weight
See Also

resdag.layers.ESNLayer : Uses these initializers for weight matrices. resdag.init.topology : Topology initializers for recurrent weights.

InputFeedbackInitializer

InputFeedbackInitializer(input_scaling: float | None = None, connectivity: float | None = None, seed: SeedLike = None)

Bases: ABC

Abstract base class for input/feedback weight initialization.

All input/feedback weight initializers inherit from this class, implement :meth:initialize, and honor the shared scaling contract owned here. These initializers create weight matrices for:

  • Input connections: shape (reservoir_size, input_size)
  • Feedback connections: shape (reservoir_size, feedback_size)
The scaling contract

input_scaling is the single uniform magnitude knob:

  • None — no scaling; the natural-range matrix is returned as-is.
  • s (finite float) — multiply the whole matrix by s as the documented final transform, so the matrix's magnitude statistic (max|W| for elementwise initializers, per-channel L2 norm for the structured ring initializers) scales linearly with s.

connectivity is an optional density knob in (0, 1]: the fraction of nonzero entries kept per column. None leaves the produced sparsity untouched.

Per-call overrides

The contract parameters are bound at construction, but :meth:initialize (and the :meth:__call__ alias) accept per-call keyword overrides for the recognized contract parameters — input_scaling and connectivity. A recognized keyword passed to initialize takes precedence over the bound attribute for that call only, and is validated identically::

init = RandomInputInitializer(input_scaling=2.0)
init.initialize(weight)                         # uses input_scaling=2.0
init.initialize(weight, input_scaling=0.001)    # honored: uses 0.001
init.input_scaling                              # still 2.0 (unchanged)

This is the same contract :class:FunctionInitializer honors (per-call kwargs merged over the bound ones), so a given call shape behaves identically for class-based and function-wrapped initializers. Subclasses cooperate simply by threading their **kwargs into the contract helpers, which read the override from **kwargs and fall back to the bound attribute; unrecognized keys are ignored.

Subclasses must implement :meth:initialize, which builds the weight tensor and returns it (modified in-place). They opt into the contract via :meth:_apply_scaling (and optionally :meth:_apply_connectivity), forwarding the per-call **kwargs so recognized overrides are honored.

PARAMETER DESCRIPTION
input_scaling

Uniform multiplicative scaling applied as the final transform. None (the default) applies no scaling. A float s scales the matrix's magnitude statistic linearly (input_scaling=0.5 halves it).

TYPE: float DEFAULT: None

connectivity

Fraction of nonzero entries to keep per column, in (0, 1]. None (the default) leaves the produced sparsity pattern untouched.

TYPE: float DEFAULT: None

seed

Reproducibility seed for any subclass randomness (the uniform/binary draws of the random initializers) and the connectivity mask. Accepts a plain int, a :class:torch.Generator (whose initial_seed() is used so a generator and the equivalent int agree), or None (defer to the global RNG). The torch-native random initializers draw on the target tensor's device via :meth:_torch_generator_for, so the same seed is reproducible per device — including on CUDA.

TYPE: int, torch.Generator, or None DEFAULT: None

Examples:

Creating a custom initializer that honors the contract:

>>> class MyInitializer(InputFeedbackInitializer):
...     def initialize(self, weight, **kwargs):
...         values = torch.empty_like(weight).uniform_(-1, 1)
...         # Forward **kwargs so a per-call ``input_scaling`` override wins.
...         values = self._apply_scaling(values, **kwargs)
...         with torch.no_grad():
...             weight.copy_(values)
...         return weight
>>>
>>> initializer = MyInitializer(input_scaling=0.5)
>>> weight = torch.empty(100, 10)
>>> initializer(weight)

Using with ESNLayer:

>>> from resdag.layers import ESNLayer
>>> reservoir = ESNLayer(
...     reservoir_size=100,
...     feedback_size=10,
...     feedback_initializer=MyInitializer(input_scaling=0.5),
... )
See Also

resdag.init.input_feedback.registry : Get initializers by name. resdag.layers.ESNLayer : Uses these initializers.

Source code in src/resdag/init/input_feedback/base.py
def __init__(
    self,
    input_scaling: float | None = None,
    connectivity: float | None = None,
    seed: SeedLike = None,
) -> None:
    """Store and validate the shared scaling/connectivity parameters."""
    self.input_scaling = _validate_input_scaling(input_scaling)
    self.connectivity = _validate_connectivity(connectivity)
    self.seed = seed

initialize abstractmethod

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize a weight tensor.

PARAMETER DESCRIPTION
weight

2D tensor of shape (reservoir_size, input_size) to initialize. Modified in-place.

TYPE: Tensor

**kwargs

Per-call keyword overrides. The recognized contract parameters — input_scaling and connectivity — override the bound attributes for this call only (see the Per-call overrides section of :class:InputFeedbackInitializer); specific initializers may recognize additional keys. Unrecognized keys are ignored. Implementations honor the overrides by forwarding **kwargs into :meth:_apply_scaling / :meth:_apply_connectivity.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

The initialized weight tensor (same as input, modified in-place).

Source code in src/resdag/init/input_feedback/base.py
@abstractmethod
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """
    Initialize a weight tensor.

    Parameters
    ----------
    weight : torch.Tensor
        2D tensor of shape ``(reservoir_size, input_size)`` to initialize.
        Modified in-place.
    **kwargs
        Per-call keyword overrides. The recognized contract parameters —
        ``input_scaling`` and ``connectivity`` — override the bound
        attributes *for this call only* (see the **Per-call overrides**
        section of :class:`InputFeedbackInitializer`); specific
        initializers may recognize additional keys. Unrecognized keys are
        ignored. Implementations honor the overrides by forwarding
        ``**kwargs`` into :meth:`_apply_scaling` /
        :meth:`_apply_connectivity`.

    Returns
    -------
    torch.Tensor
        The initialized weight tensor (same as input, modified in-place).
    """
    pass

FunctionInitializer

FunctionInitializer(fn: Callable, **kwargs: Any)

Bases: InputFeedbackInitializer

Input/feedback initializer wrapping any matrix-building callable.

Lets plain functions act as initializers for the rectangular input and feedback weight matrices, without subclassing. Two calling conventions are supported, tried in order:

  1. Build stylefn(rows, cols, **kwargs) returning a (rows, cols) array-like (torch.Tensor or numpy.ndarray).
  2. In-place stylefn(tensor, **kwargs) mutating a tensor in place, e.g. any torch.nn.init.*_ function.
PARAMETER DESCRIPTION
fn

The matrix-building function (build style or in-place style).

TYPE: callable

**kwargs

Keyword arguments bound to the function.

TYPE: Any DEFAULT: {}

Examples:

A plain function as an initializer:

>>> import torch
>>> def first_neuron_only(rows, cols, scale=1.0):
...     w = torch.zeros(rows, cols)
...     w[0, :] = scale
...     return w
>>> init = FunctionInitializer(first_neuron_only, scale=0.5)
>>> weight = torch.empty(100, 3)
>>> init.initialize(weight)

Bare callables passed to ESNLayer are wrapped automatically:

>>> from resdag.layers import ESNLayer
>>> layer = ESNLayer(100, feedback_size=3, feedback_initializer=first_neuron_only)
>>> layer = ESNLayer(
...     100,
...     feedback_size=3,
...     feedback_initializer=(first_neuron_only, {"scale": 0.1}),
... )

torch.nn.init functions work directly (in-place style):

>>> layer = ESNLayer(100, feedback_size=3, feedback_initializer=torch.nn.init.xavier_uniform_)
See Also

InputFeedbackInitializer : Base class for class-based initializers. resdag.init.input_feedback.register_input_feedback : Register by name.

Source code in src/resdag/init/input_feedback/function.py
def __init__(self, fn: Callable, **kwargs: Any) -> None:
    self.fn = fn
    self.kwargs = kwargs

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize a rectangular weight tensor from the wrapped callable.

PARAMETER DESCRIPTION
weight

2D tensor of shape (rows, cols) to initialize. Modified in-place.

TYPE: Tensor

**kwargs

Per-call overrides merged over the bound keyword arguments (a recognized key wins for this call only). This is the same per-call contract the class-based initializers honor — see the Per-call overrides section of :class:~resdag.init.input_feedback.InputFeedbackInitializer.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

The initialized weight tensor (same as input, modified in-place).

RAISES DESCRIPTION
ValueError

If weight is not 2-D, if the callable matches neither calling convention, or if the built matrix has the wrong shape.

Source code in src/resdag/init/input_feedback/function.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """
    Initialize a rectangular weight tensor from the wrapped callable.

    Parameters
    ----------
    weight : torch.Tensor
        2D tensor of shape ``(rows, cols)`` to initialize. Modified
        in-place.
    **kwargs
        Per-call overrides merged over the bound keyword arguments (a
        recognized key wins for this call only). This is the same per-call
        contract the class-based initializers honor — see the **Per-call
        overrides** section of
        :class:`~resdag.init.input_feedback.InputFeedbackInitializer`.

    Returns
    -------
    torch.Tensor
        The initialized weight tensor (same as input, modified in-place).

    Raises
    ------
    ValueError
        If ``weight`` is not 2-D, if the callable matches neither
        calling convention, or if the built matrix has the wrong shape.
    """
    if weight.ndim != 2:
        raise ValueError(f"Weight must be 2D, got shape {weight.shape}")

    rows, cols = weight.shape
    call_kwargs = {**self.kwargs, **kwargs}

    result = _call_matrix_builder(self.fn, weight, (rows, cols), call_kwargs)
    matrix = _coerce_to_matrix(result, (rows, cols), weight.device, weight.dtype)

    with torch.no_grad():
        weight.copy_(matrix)

    return weight

BernoulliInputInitializer

BernoulliInputInitializer(p: float = 0.5, connectivity: float | None = None, input_scaling: float | None = None, seed: SeedLike = None)

Bases: InputFeedbackInitializer

Bernoulli sign initializer for input/feedback weight matrices.

Each entry is +1 with probability p and -1 otherwise — the canonical sparse-sign input matrix of the reservoir-computing literature. Combined with connectivity (the single most common input-matrix recipe: a fixed density of random ±1 signs per channel) and the shared input_scaling magnitude knob.

This is the migration target for reservoirpy's :func:reservoirpy.mat_gen.bernoulli: p matches its p (the probability of drawing +1), and connectivity matches its connectivity density argument.

PARAMETER DESCRIPTION
p

Probability that an entry is +1 (it is -1 with probability 1 - p). Must lie in [0, 1]. Defaults to 0.5 (a balanced, zero-mean sign matrix).

TYPE: float DEFAULT: 0.5

connectivity

Density knob from the shared scaling contract (see :class:~resdag.init.input_feedback.InputFeedbackInitializer): the fraction of nonzero entries kept per column (input channel), in (0, 1]. None (the default) leaves the dense ±1 matrix untouched; connectivity=0.1 keeps ~10% of each column's signs (at least one), zeroing the rest. The kept fraction is statistically exact: round(connectivity * reservoir_size) entries per column.

TYPE: float DEFAULT: None

input_scaling

Uniform magnitude knob from the shared scaling contract. None (the default) leaves entries in {-1, +1}; a float s multiplies every entry by s (entries become {-s, +s}), so max|W| scales linearly with s (input_scaling=0.5 halves it).

TYPE: float DEFAULT: None

seed

Reproducibility seed for the Bernoulli draw and the connectivity mask. Accepts a plain int, a :class:torch.Generator (whose initial_seed() is used, so a generator and the equivalent int agree), or None (defer to torch's global RNG). The value draw is device-native: it happens directly on the target weight's device via a torch generator, so the same seed is reproducible per device (CPU and CUDA each reproduce, though their RNG streams differ from each other).

TYPE: int, torch.Generator, or None DEFAULT: None

Notes

The produced matrix is a pure function of (p, connectivity, input_scaling, seed, shape, device): repeated calls on the same instance with equal shapes on the same device yield identical matrices. Pass seed=None for a draw tied to torch's global RNG (reproducible under torch.manual_seed).

Examples:

>>> from resdag.init.input_feedback import BernoulliInputInitializer
>>>
>>> # 10%-sparse balanced ±1 input matrix (a classic sparse input recipe).
>>> init = BernoulliInputInitializer(connectivity=0.1, seed=42)
>>> weight = torch.empty(100, 10)  # (reservoir_size, feedback_size)
>>> init.initialize(weight)
>>>
>>> # Use in ESNLayer.
>>> from resdag.layers import ESNLayer
>>> reservoir = ESNLayer(
...     reservoir_size=100,
...     feedback_size=10,
...     feedback_initializer=init,
... )
Source code in src/resdag/init/input_feedback/bernoulli.py
def __init__(
    self,
    p: float = 0.5,
    connectivity: float | None = None,
    input_scaling: float | None = None,
    seed: SeedLike = None,
) -> None:
    """Initialize the BernoulliInputInitializer."""
    super().__init__(
        input_scaling=input_scaling,
        connectivity=connectivity,
        seed=seed,
    )
    p = float(p)
    if not (0.0 <= p <= 1.0):
        raise ValueError(f"p must be in [0, 1]; got {p!r}.")
    self.p = p

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight with Bernoulli ±1 entries.

PARAMETER DESCRIPTION
weight

Weight tensor of shape (out_features, in_features). Modified in-place.

TYPE: Tensor

**kwargs

Per-call keyword overrides. Recognized input_scaling and connectivity keys override the bound attributes for this call only (the shared per-call contract — see :class:~resdag.init.input_feedback.InputFeedbackInitializer); other keys are ignored.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

The initialized weight tensor (same object, modified in-place).

Source code in src/resdag/init/input_feedback/bernoulli.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize ``weight`` with Bernoulli ``±1`` entries.

    Parameters
    ----------
    weight : torch.Tensor
        Weight tensor of shape ``(out_features, in_features)``. Modified
        in-place.
    **kwargs
        Per-call keyword overrides. Recognized ``input_scaling`` and
        ``connectivity`` keys override the bound attributes for this call
        only (the shared per-call contract — see
        :class:`~resdag.init.input_feedback.InputFeedbackInitializer`);
        other keys are ignored.

    Returns
    -------
    torch.Tensor
        The initialized weight tensor (same object, modified in-place).
    """
    out_features, in_features = _resolve_shape(weight)
    device = weight.device
    dtype = weight.dtype

    generator = self._torch_generator_for(device)

    # Draw uniform [0, 1) on-device, then threshold at ``1 - p`` so that an
    # entry is +1 with probability ``p`` and -1 otherwise.
    draws = torch.empty(out_features, in_features, device=device, dtype=dtype)
    draws.uniform_(0.0, 1.0, generator=generator)
    values = torch.where(
        draws < self.p,
        torch.ones_like(draws),
        -torch.ones_like(draws),
    )

    # Apply the shared connectivity then scaling contract, in that order
    # (zeroing then scaling is identical either way; this matches the base
    # class's documented composition ``_apply_scaling(_apply_connectivity)``).
    # Forward ``**kwargs`` so per-call ``connectivity`` / ``input_scaling``
    # overrides win over the bound values.
    values = self._apply_connectivity(values, **kwargs)
    values = self._apply_scaling(values, **kwargs)

    with torch.no_grad():
        weight.copy_(values)

    return weight

BinaryBalancedInitializer

BinaryBalancedInitializer(input_scaling: float | None = None, balance_global: bool = True, step: int | None = None, seed: int | None = None)

Bases: InputFeedbackInitializer

Deterministic binary balanced initializer using Walsh-Hadamard structure.

Generates a dense input matrix with entries in {-1, +1}, column-wise balance (sum close to zero), and low inter-column correlation via truncated Walsh-Hadamard structure.

The matrix is built without randomness (deterministic) and is suitable for ESN/RC setups where each column (reservoir unit) should receive a balanced mix of positive/negative signs from the input channels, and columns should be nearly orthogonal.

PARAMETER DESCRIPTION
input_scaling

Uniform magnitude knob from the shared scaling contract (see :class:~resdag.init.input_feedback.InputFeedbackInitializer). None (the default) leaves entries in {-1, +1}; a float s multiplies every entry by s (entries become {-s, +s}), so max|W| scales linearly with s (input_scaling=0.5 halves it).

TYPE: float DEFAULT: None

balance_global

When rows is odd, enforce near-equal global count of +1 vs -1 column-sums by flipping full columns.

TYPE: bool DEFAULT: True

step

Preferred column selection step. If None or not coprime with L, the initializer will choose the smallest odd step that is coprime with L.

TYPE: int DEFAULT: None

seed

Unused (kept for API compatibility). The initializer is fully deterministic.

TYPE: int DEFAULT: None

Notes
  • Column-wise balance: sum over rows per column is exactly 0 if rows even, else ±1.
  • Low correlation: columns originate from orthogonal Hadamard columns.
  • Deterministic: no RNG is used; the output is reproducible given the shape.

Examples:

>>> from resdag.init.input_feedback import BinaryBalancedInitializer
>>>
>>> init = BinaryBalancedInitializer(input_scaling=0.5, balance_global=True)
>>> weight = torch.empty(100, 10)  # (reservoir_size, input_dim)
>>> init.initialize(weight)
>>>
>>> # Each column will have sum close to 0 (balanced)
>>> column_sums = weight.sum(dim=0)
>>> print(column_sums)  # Close to zero
Source code in src/resdag/init/input_feedback/binary_balanced.py
def __init__(
    self,
    input_scaling: float | None = None,
    balance_global: bool = True,
    step: int | None = None,
    seed: int | None = None,  # Unused, for API consistency
) -> None:
    """Initialize the BinaryBalancedInitializer."""
    super().__init__(input_scaling=input_scaling, seed=seed)
    self.balance_global = balance_global
    self.step = step

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight tensor with binary balanced structure.

PARAMETER DESCRIPTION
weight

Weight tensor of shape (out_features, in_features)

TYPE: Tensor

**kwargs

Per-call keyword overrides. A recognized input_scaling key overrides the bound self.input_scaling for this call only (the shared per-call contract — see :class:~resdag.init.input_feedback.InputFeedbackInitializer); other keys are ignored.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

Initialized weight tensor

Source code in src/resdag/init/input_feedback/binary_balanced.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize weight tensor with binary balanced structure.

    Parameters
    ----------
    weight : torch.Tensor
        Weight tensor of shape (out_features, in_features)
    **kwargs
        Per-call keyword overrides. A recognized ``input_scaling`` key
        overrides the bound ``self.input_scaling`` for this call only (the
        shared per-call contract — see
        :class:`~resdag.init.input_feedback.InputFeedbackInitializer`);
        other keys are ignored.

    Returns
    -------
    torch.Tensor
        Initialized weight tensor
    """
    out_features, in_features = _resolve_shape(weight)
    device = weight.device
    dtype = weight.dtype
    compute_dtype = _numpy_compute_dtype(dtype)

    rows, cols = out_features, in_features

    if rows == 0 or cols == 0:
        weight.zero_()
        return weight

    # Work with even row count
    n_work = rows if (rows % 2 == 0) else rows + 1

    # Build Hadamard matrix
    L = self._next_pow2(max(n_work, cols + 1, 2))
    H = self._hadamard(L)

    s = self._choose_step(L, self.step)
    idxs = [1 + (j * s) % (L - 1) for j in range(cols)]  # Exclude DC column

    Vw = H[:n_work, idxs].copy()

    # Balance each column to sum zero
    self._balance_columns_zero_sum(Vw)

    # If rows is even, done; else delete one row and optionally balance
    if n_work == rows:
        V = Vw
    else:
        V = self._delete_least_bias_row(Vw)
        if self.balance_global:
            self._balance_global_column_counts(V)

    # Build at the target precision so a non-float32-representable
    # input_scaling survives a float64 weight intact.
    values = V.astype(compute_dtype)

    # Apply the shared uniform scaling contract as the documented final
    # transform. Forward ``**kwargs`` so a per-call ``input_scaling`` override
    # wins over the bound value.
    values = self._apply_scaling(values, **kwargs)

    # Convert to tensor and copy to weight
    weight_data = torch.from_numpy(values).to(device=device, dtype=dtype)

    with torch.no_grad():
        weight.copy_(weight_data)

    return weight

ChainOfNeuronsInputInitializer

ChainOfNeuronsInputInitializer(features: int | None = None, weights: float | Sequence[float] = 1.0)

Bases: InputFeedbackInitializer

Input initializer for chain-of-neurons reservoirs.

For reservoirs organized as multiple parallel chains, this initializer connects each input to the first neuron of its corresponding chain.

PARAMETER DESCRIPTION
features

Number of chains. Must equal the number of inputs (the weight's in_features). None (the default) infers it from the weight's column count at :meth:initialize time, so the bare-name registry path get_input_feedback("chain_of_neurons_input") works. When an explicit value is given it is validated against the weight shape.

TYPE: int DEFAULT: None

weights

Either a single float (same weight for all input→chain pairs) or a sequence of floats (one weight per input/chain).

TYPE: float or Sequence[float] DEFAULT: 1.0

Examples:

>>> from resdag.init.input_feedback import ChainOfNeuronsInputInitializer
>>>
>>> # Infer the number of chains from the weight (3 columns -> 3 chains)
>>> init = ChainOfNeuronsInputInitializer(weights=1.0)
>>> weight = torch.empty(150, 3)  # (reservoir_size, num_inputs)
>>> init.initialize(weight)
>>>
>>> # Or pin it explicitly (validated against the weight shape)
>>> init = ChainOfNeuronsInputInitializer(features=3, weights=1.0)
>>>
>>> # Each input connects only to the first neuron of its chain
RAISES DESCRIPTION
ValueError

If features is given but < 1.

Source code in src/resdag/init/input_feedback/chain_of_neurons_input.py
def __init__(
    self,
    features: int | None = None,
    weights: float | Sequence[float] = 1.0,
):
    """Initialize the ChainOfNeuronsInputInitializer.

    Raises
    ------
    ValueError
        If ``features`` is given but ``< 1``.
    """
    if features is not None and features < 1:
        raise ValueError(f"'features' must be >= 1, got {features}.")
    self.features = features
    self.weights = weights

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight tensor for chain-of-neurons topology.

PARAMETER DESCRIPTION
weight

Weight tensor of shape (out_features, in_features) out_features = reservoir_size (units) in_features = num_inputs (must equal features; when features is None it is inferred from this column count)

TYPE: Tensor

RETURNS DESCRIPTION
Tensor

Initialized weight tensor

RAISES DESCRIPTION
ValueError

If an explicit features does not equal in_features, or units is not a multiple of the number of chains.

Source code in src/resdag/init/input_feedback/chain_of_neurons_input.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize weight tensor for chain-of-neurons topology.

    Parameters
    ----------
    weight : torch.Tensor
        Weight tensor of shape (out_features, in_features)
        out_features = reservoir_size (units)
        in_features = num_inputs (must equal features; when ``features`` is
        ``None`` it is inferred from this column count)

    Returns
    -------
    torch.Tensor
        Initialized weight tensor

    Raises
    ------
    ValueError
        If an explicit ``features`` does not equal ``in_features``, or
        ``units`` is not a multiple of the number of chains.
    """
    units, input_dim = _resolve_shape(weight)
    device = weight.device
    dtype = weight.dtype
    compute_dtype = _numpy_compute_dtype(dtype)

    # ``features`` equals the number of chains, which equals the weight's
    # column count (one input per chain). Infer it from the weight when the
    # caller left it unset; otherwise validate the explicit value matches.
    if self.features is None:
        features = input_dim
    else:
        features = self.features
        if input_dim != features:
            raise ValueError(
                f"input_dim ({input_dim}) must equal 'features' ({features}) "
                "to have one input per chain."
            )

    if units % features != 0:
        raise ValueError(
            f"Number of units ({units}) must be a multiple of 'features' "
            f"({features}) to align chains with inputs."
        )

    # Build at the target precision so non-float32-representable per-input
    # weights survive a float64 weight intact.
    values = np.zeros((units, input_dim), dtype=compute_dtype)

    # Resolve per-input weights
    if isinstance(self.weights, (list, tuple, np.ndarray)):
        if len(self.weights) != input_dim:
            raise ValueError(
                "When 'weights' is a sequence, its length must equal input_dim; "
                f"got len(weights)={len(self.weights)}, input_dim={input_dim}."
            )
        in_weights = [float(w) for w in self.weights]
    else:
        # Reached only when ``self.weights`` is not a list/tuple/ndarray, i.e.
        # the scalar branch of the ``float | Sequence[float]`` union; cast for mypy.
        w = float(cast(float, self.weights))
        in_weights = [w] * input_dim

    block_len = units // features

    # Deterministic: input i → first unit of chain i
    for i in range(input_dim):
        start = i * block_len
        values[start, i] = in_weights[i]

    # Convert to tensor and copy to weight
    weight_data = torch.from_numpy(values).to(device=device, dtype=dtype)

    with torch.no_grad():
        weight.copy_(weight_data)

    return weight

ChebyshevInitializer

ChebyshevInitializer(p: float = 0.3, q: float = 5.9, k: float = 3.8, input_scaling: float | None = None)

Bases: InputFeedbackInitializer

Chebyshev mapping initializer for deterministic chaotic initialization.

This initializer constructs a weight matrix based on the Chebyshev polynomial map, ensuring structured, chaotic initialization while maintaining a controlled range.

The Chebyshev polynomial recurrence exhibits deterministic chaos, making it a structured alternative to purely random weight initialization. This enhances the richness of how the input signal is connected to the reservoir neurons.

The Chebyshev map is applied column-wise: - First column: W[:, 0] = p * sin((i / (rows+1)) * (π / q)) - Subsequent columns: W[:, j] = cos(k * arccos(W[:, j-1]))

where k controls chaotic behavior (optimal range: 2 < k < 4).

The seed column (column 0) lives on the small amplitude [-p, p] while the chaotic columns span [-1, 1], so the raw map systematically de-weights the first feedback dimension (its max|W| is ~p against ~1 elsewhere). To honor the shared scaling contract — whose magnitude statistic is max|W| per column for this elementwise initializer — each column is normalized to unit peak amplitude (divided by its own max|W|) before input_scaling applies. Every column then peaks at the same input_scaling (or 1 when input_scaling is None), and the 1D-feedback matrix peaks at input_scaling rather than the unscaled seed amplitude p.

PARAMETER DESCRIPTION
p

Scaling factor for the initial sinusoidal weights. Should be in (0, 1).

TYPE: float DEFAULT: 0.3

q

Parameter controlling the initial sinusoidal distribution.

TYPE: float DEFAULT: 5.9

k

Control parameter of the Chebyshev map. Must be in (2, 4) for chaotic behavior.

TYPE: float DEFAULT: 3.8

input_scaling

Uniform magnitude knob from the shared scaling contract (see :class:~resdag.init.input_feedback.InputFeedbackInitializer). None (the default) applies no scaling; a float s multiplies every entry by s as the documented final transform, so max|W| scales linearly with s (input_scaling=0.5 halves it).

TYPE: float DEFAULT: None

RAISES DESCRIPTION
ValueError

If k is not in the valid range (2, 4).

References

M. Xie, Q. Wang, and S. Yu, "Time Series Prediction of ESN Based on Chebyshev Mapping and Strongly Connected Topology," Neural Process Lett, vol. 56, no. 1, p. 30, Feb. 2024.

Examples:

>>> from resdag.init.input_feedback import ChebyshevInitializer
>>>
>>> init = ChebyshevInitializer(p=0.3, k=3.5, input_scaling=0.8)
>>> weight = torch.empty(100, 10)
>>> init.initialize(weight)
Source code in src/resdag/init/input_feedback/chebyshev.py
def __init__(
    self,
    p: float = 0.3,
    q: float = 5.9,
    k: float = 3.8,
    input_scaling: float | None = None,
) -> None:
    """Initialize the ChebyshevInitializer."""
    if not (2.0 < k < 4.0):
        raise ValueError(f"Parameter k={k} must be in range (2, 4) for chaotic behavior")

    super().__init__(input_scaling=input_scaling)
    self.p = p
    self.q = q
    self.k = k

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight tensor using Chebyshev mapping.

PARAMETER DESCRIPTION
weight

Weight tensor of shape (out_features, in_features)

TYPE: Tensor

**kwargs

Per-call keyword overrides. A recognized input_scaling key overrides the bound self.input_scaling for this call only (the shared per-call contract — see :class:~resdag.init.input_feedback.InputFeedbackInitializer); other keys are ignored.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

Initialized weight tensor with Chebyshev structure

Source code in src/resdag/init/input_feedback/chebyshev.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize weight tensor using Chebyshev mapping.

    Parameters
    ----------
    weight : torch.Tensor
        Weight tensor of shape (out_features, in_features)
    **kwargs
        Per-call keyword overrides. A recognized ``input_scaling`` key
        overrides the bound ``self.input_scaling`` for this call only (the
        shared per-call contract — see
        :class:`~resdag.init.input_feedback.InputFeedbackInitializer`);
        other keys are ignored.

    Returns
    -------
    torch.Tensor
        Initialized weight tensor with Chebyshev structure
    """
    out_features, in_features = _resolve_shape(weight)
    device = weight.device
    dtype = weight.dtype
    compute_dtype = _numpy_compute_dtype(dtype)

    # Initialize matrix at the target precision so float64 weights are not
    # silently truncated to float32 before the final widening copy.
    values = np.zeros((out_features, in_features), dtype=compute_dtype)

    # First column: sinusoidal initialization
    row_indices = np.arange(1, out_features + 1, dtype=compute_dtype)
    values[:, 0] = self.p * np.sin((row_indices / (out_features + 1)) * (np.pi / self.q))

    # Apply Chebyshev recurrence column-wise
    for j in range(1, in_features):
        values[:, j] = np.cos(self.k * np.arccos(np.clip(values[:, j - 1], -1.0, 1.0)))

    # Normalize each column to unit peak amplitude so the small-amplitude seed
    # column (~|p|) is no longer ~1/p weaker than the chaotic columns (~1). This
    # makes the magnitude statistic ``max|W|`` uniform across feedback dimensions
    # before the scaling contract applies. Columns that are entirely zero (e.g. a
    # degenerate p=0 seed) are left untouched to avoid dividing by zero.
    col_peak = np.max(np.abs(values), axis=0, keepdims=True)
    np.divide(values, col_peak, out=values, where=col_peak > 0.0)

    # Apply the shared uniform scaling contract as the documented final
    # transform. Forward ``**kwargs`` so a per-call ``input_scaling`` override
    # wins over the bound value.
    values = self._apply_scaling(values, **kwargs)

    # Convert to tensor and copy to weight
    weight_data = torch.from_numpy(values).to(device=device, dtype=dtype)

    with torch.no_grad():
        weight.copy_(weight_data)

    return weight

ChessboardInitializer

ChessboardInitializer(input_scaling: float | None = None)

Bases: InputFeedbackInitializer

Chessboard pattern initializer with alternating {-1, +1} values.

Creates a deterministic checkerboard pattern where adjacent elements alternate in sign. This creates a structured, high-frequency pattern that can be useful for certain reservoir dynamics.

The pattern is: W[i, j] = (-1)^(i+j)

PARAMETER DESCRIPTION
input_scaling

Uniform magnitude knob from the shared scaling contract (see :class:~resdag.init.input_feedback.InputFeedbackInitializer). None (the default) leaves entries in {-1, +1}; a float s multiplies every entry by s (entries become {-s, +s}), so max|W| scales linearly with s (input_scaling=0.5 halves it).

TYPE: float DEFAULT: None

Examples:

>>> from resdag.init.input_feedback import ChessboardInitializer
>>>
>>> init = ChessboardInitializer(input_scaling=0.5)
>>> weight = torch.empty(5, 10)
>>> init.initialize(weight)
>>>
>>> # Creates alternating pattern:
>>> # [[ 0.5, -0.5,  0.5, -0.5, ...],
>>> #  [-0.5,  0.5, -0.5,  0.5, ...],
>>> #  [ 0.5, -0.5,  0.5, -0.5, ...],
>>> #  ...]
Source code in src/resdag/init/input_feedback/chessboard.py
def __init__(self, input_scaling: float | None = None) -> None:
    """Initialize the ChessboardInitializer."""
    super().__init__(input_scaling=input_scaling)

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight tensor with chessboard pattern.

PARAMETER DESCRIPTION
weight

Weight tensor of shape (out_features, in_features)

TYPE: Tensor

**kwargs

Per-call keyword overrides. A recognized input_scaling key overrides the bound self.input_scaling for this call only (the shared per-call contract — see :class:~resdag.init.input_feedback.InputFeedbackInitializer); other keys are ignored.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

Initialized weight tensor with chessboard pattern

Source code in src/resdag/init/input_feedback/chessboard.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize weight tensor with chessboard pattern.

    Parameters
    ----------
    weight : torch.Tensor
        Weight tensor of shape (out_features, in_features)
    **kwargs
        Per-call keyword overrides. A recognized ``input_scaling`` key
        overrides the bound ``self.input_scaling`` for this call only (the
        shared per-call contract — see
        :class:`~resdag.init.input_feedback.InputFeedbackInitializer`);
        other keys are ignored.

    Returns
    -------
    torch.Tensor
        Initialized weight tensor with chessboard pattern
    """
    out_features, in_features = _resolve_shape(weight)
    device = weight.device
    dtype = weight.dtype
    compute_dtype = _numpy_compute_dtype(dtype)

    # Create chessboard pattern at the target precision so any scaling that
    # is not float32-representable survives a float64 weight intact.
    i = np.arange(out_features)[:, None]
    j = np.arange(in_features)[None, :]
    values = ((-1.0) ** (i + j)).astype(compute_dtype)

    # Apply the shared uniform scaling contract as the documented final
    # transform. Forward ``**kwargs`` so a per-call ``input_scaling`` override
    # wins over the bound value.
    values = self._apply_scaling(values, **kwargs)

    # Convert to tensor and copy to weight
    weight_data = torch.from_numpy(values).to(device=device, dtype=dtype)

    with torch.no_grad():
        weight.copy_(weight_data)

    return weight

DendrocycleInputInitializer

DendrocycleInputInitializer(c: float | None = None, C: int | None = None, draw_width: float = 1.0, input_scaling: float | None = None, connectivity: float | None = None, seed: int | None = None)

Bases: InputFeedbackInitializer

Input initializer for dendro-cycle reservoirs.

Generates a matrix where only the core (cycle) nodes receive input connections. All other entries are zero. This is specific to dendrocycle topologies where inputs should only connect to the core ring.

Each active connection draws a weight from U[-draw_width, draw_width] and is then scaled by the shared input_scaling contract. draw_width is the draw half-width (the spread of the raw distribution); input_scaling is the uniform final magnitude knob shared by every initializer. With input_scaling=0.5 every drawn weight is halved, so max|W| scales linearly with input_scaling (max|W| <= draw_width * input_scaling).

.. note:: Prior to the unified scaling contract, input_scaling here meant the draw half-width (defaulting to 1.0). That role is now draw_width; input_scaling is the uniform multiplicative transform shared with every other initializer and defaults to None (no scaling). To reproduce the old DendrocycleInputInitializer(input_scaling=s) draw, pass draw_width=s.

PARAMETER DESCRIPTION
c

Fraction of nodes forming the cycle (0 < c <= 1). Provide either c or C.

TYPE: float DEFAULT: None

C

Number of cycle (core) nodes. If provided, c is ignored.

TYPE: int DEFAULT: None

draw_width

Half-width of the uniform draw U[-draw_width, draw_width] for each active connection.

TYPE: float DEFAULT: 1.0

input_scaling

Uniform multiplicative scaling applied as the final transform. None (the default) applies no scaling. input_scaling=0.5 halves every weight, so max|W| is halved.

TYPE: float DEFAULT: None

connectivity

Unused by this initializer: dendrocycle defines its own (core-only) connectivity pattern, so this knob is accepted for API uniformity but ignored. Always None in practice.

TYPE: float DEFAULT: None

seed

Random seed for reproducibility.

TYPE: int DEFAULT: None

Examples:

>>> from resdag.init.input_feedback import DendrocycleInputInitializer
>>>
>>> # Initialize for dendrocycle with 20% core nodes, weights in U[-0.5, 0.5]
>>> init = DendrocycleInputInitializer(c=0.2, draw_width=0.5, seed=42)
>>> weight = torch.empty(100, 8)  # (reservoir_size, num_inputs)
>>> init.initialize(weight)
>>>
>>> # Only first 20 neurons (core) have non-zero weights
Source code in src/resdag/init/input_feedback/dendrocycle_input.py
def __init__(
    self,
    c: float | None = None,
    C: int | None = None,
    draw_width: float = 1.0,
    input_scaling: float | None = None,
    connectivity: float | None = None,
    seed: int | None = None,
) -> None:
    """Initialize the DendrocycleInputInitializer."""
    super().__init__(input_scaling=input_scaling, connectivity=connectivity, seed=seed)
    if (c is None) == (C is None):
        raise ValueError("Provide exactly one of c or C.")
    if not (np.isfinite(draw_width) and draw_width > 0):
        raise ValueError("draw_width must be a positive finite float.")
    self.c = c
    self.C = C
    self.draw_width = float(draw_width)

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight tensor for dendrocycle topology.

The RNG is constructed from self.seed on every call, so the produced matrix is a pure function of (seed, shape). Repeated calls on the same instance with equal shapes therefore yield identical matrices. Pass seed=None for a fresh draw on each call.

PARAMETER DESCRIPTION
weight

Weight tensor of shape (out_features, in_features) out_features = reservoir_size (N) in_features = num_inputs (M)

TYPE: Tensor

**kwargs

Per-call keyword overrides. A recognized input_scaling key overrides the bound self.input_scaling for this call only (the shared per-call contract — see :class:~resdag.init.input_feedback.InputFeedbackInitializer); other keys are ignored. connectivity is not honored here (this initializer defines its own core-only pattern).

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

Initialized weight tensor (only core nodes have non-zero weights)

Source code in src/resdag/init/input_feedback/dendrocycle_input.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize weight tensor for dendrocycle topology.

    The RNG is constructed from ``self.seed`` on every call, so the produced
    matrix is a pure function of ``(seed, shape)``. Repeated calls on the
    same instance with equal shapes therefore yield identical matrices.
    Pass ``seed=None`` for a fresh draw on each call.

    Parameters
    ----------
    weight : torch.Tensor
        Weight tensor of shape (out_features, in_features)
        out_features = reservoir_size (N)
        in_features = num_inputs (M)
    **kwargs
        Per-call keyword overrides. A recognized ``input_scaling`` key
        overrides the bound ``self.input_scaling`` for this call only (the
        shared per-call contract — see
        :class:`~resdag.init.input_feedback.InputFeedbackInitializer`);
        other keys are ignored. ``connectivity`` is not honored here (this
        initializer defines its own core-only pattern).

    Returns
    -------
    torch.Tensor
        Initialized weight tensor (only core nodes have non-zero weights)
    """
    N, M = _resolve_shape(weight)
    device = weight.device
    dtype = weight.dtype
    compute_dtype = _numpy_compute_dtype(dtype)

    rng = np.random.default_rng(coerce_seed_to_int(self.seed))

    C = self.C
    if C is None:
        # __init__ enforces exactly one of c/C is set, so C is None ⇒ c is not None.
        assert self.c is not None
        if not (0 < self.c <= 1):
            raise ValueError("c must be in (0, 1].")
        C = max(1, int(round(self.c * N)))
    if not (1 <= C <= N):
        raise ValueError("C must be in [1, N].")

    # Build at the target precision so the float64 ``rng.uniform`` draws are
    # not truncated to float32 in a float64 weight.
    values = np.zeros((N, M), dtype=compute_dtype)

    # Case 1: fewer inputs than cores
    if M <= C:
        mapping = [int(np.floor(i * M / C)) for i in range(C)]
        for core_idx, input_idx in enumerate(mapping):
            values[core_idx, input_idx] = rng.uniform(-self.draw_width, self.draw_width)
    # Case 2: more inputs than cores
    else:
        mapping = [int(np.floor(i * C / M)) for i in range(M)]
        for input_idx, core_idx in enumerate(mapping):
            values[core_idx, input_idx] = rng.uniform(-self.draw_width, self.draw_width)

    # Apply the shared uniform scaling contract as the documented final
    # transform. Forward ``**kwargs`` so a per-call ``input_scaling`` override
    # wins over the bound value.
    values = self._apply_scaling(values, **kwargs)

    # Convert to tensor and copy to weight
    weight_data = torch.from_numpy(values).to(device=device, dtype=dtype)

    with torch.no_grad():
        weight.copy_(weight_data)

    return weight

NormalInputInitializer

NormalInputInitializer(loc: float = 0.0, scale: float = 1.0, connectivity: float | None = None, input_scaling: float | None = None, seed: SeedLike = None)

Bases: InputFeedbackInitializer

Gaussian (normal) initializer for input/feedback weight matrices.

Draws every entry independently from Normal(loc, scale) — the bread-and-butter dense Gaussian input matrix of the reservoir-computing literature. Optionally sparsified per input channel via connectivity and scaled uniformly via the shared input_scaling contract.

This is the migration target for reservoirpy's :func:reservoirpy.mat_gen.normal (and the "normal" distribution of :func:reservoirpy.mat_gen.random_sparse).

PARAMETER DESCRIPTION
loc

Mean of the Gaussian draw. Defaults to 0.0.

TYPE: float DEFAULT: 0.0

scale

Standard deviation of the Gaussian draw (must be non-negative). Defaults to 1.0.

TYPE: float DEFAULT: 1.0

connectivity

Density knob from the shared scaling contract (see :class:~resdag.init.input_feedback.InputFeedbackInitializer): the fraction of nonzero entries kept per column (input channel), in (0, 1]. None (the default) leaves the dense matrix untouched; connectivity=0.1 keeps ~10% of each column's entries (at least one), zeroing the rest. The kept fraction is statistically exact: round(connectivity * reservoir_size) entries per column.

TYPE: float DEFAULT: None

input_scaling

Uniform magnitude knob from the shared scaling contract. None (the default) applies no scaling; a float s multiplies every entry by s, so max|W| scales linearly with s (input_scaling=0.5 halves it).

TYPE: float DEFAULT: None

seed

Reproducibility seed for the Gaussian draw and the connectivity mask. Accepts a plain int, a :class:torch.Generator (whose initial_seed() is used, so a generator and the equivalent int agree), or None (defer to torch's global RNG). The value draw is device-native: it happens directly on the target weight's device via a torch generator, so the same seed is reproducible per device (CPU and CUDA each reproduce, though their RNG streams differ from each other).

TYPE: int, torch.Generator, or None DEFAULT: None

Notes

The produced matrix is a pure function of (loc, scale, connectivity, input_scaling, seed, shape, device): repeated calls on the same instance with equal shapes on the same device yield identical matrices. Pass seed=None for a draw tied to torch's global RNG (reproducible under torch.manual_seed).

Examples:

>>> from resdag.init.input_feedback import NormalInputInitializer
>>>
>>> # 10%-sparse standard-normal input matrix.
>>> init = NormalInputInitializer(connectivity=0.1, seed=42)
>>> weight = torch.empty(100, 10)  # (reservoir_size, feedback_size)
>>> init.initialize(weight)
>>>
>>> # Use in ESNLayer.
>>> from resdag.layers import ESNLayer
>>> reservoir = ESNLayer(
...     reservoir_size=100,
...     feedback_size=10,
...     feedback_initializer=init,
... )
Source code in src/resdag/init/input_feedback/normal.py
def __init__(
    self,
    loc: float = 0.0,
    scale: float = 1.0,
    connectivity: float | None = None,
    input_scaling: float | None = None,
    seed: SeedLike = None,
) -> None:
    """Initialize the NormalInputInitializer."""
    super().__init__(
        input_scaling=input_scaling,
        connectivity=connectivity,
        seed=seed,
    )
    scale = float(scale)
    if scale < 0.0:
        raise ValueError(f"scale must be non-negative; got {scale!r}.")
    self.loc = float(loc)
    self.scale = scale

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight with Normal(loc, scale) entries.

PARAMETER DESCRIPTION
weight

Weight tensor of shape (out_features, in_features). Modified in-place.

TYPE: Tensor

**kwargs

Per-call keyword overrides. Recognized input_scaling and connectivity keys override the bound attributes for this call only (the shared per-call contract — see :class:~resdag.init.input_feedback.InputFeedbackInitializer); other keys are ignored.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

The initialized weight tensor (same object, modified in-place).

Source code in src/resdag/init/input_feedback/normal.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize ``weight`` with ``Normal(loc, scale)`` entries.

    Parameters
    ----------
    weight : torch.Tensor
        Weight tensor of shape ``(out_features, in_features)``. Modified
        in-place.
    **kwargs
        Per-call keyword overrides. Recognized ``input_scaling`` and
        ``connectivity`` keys override the bound attributes for this call
        only (the shared per-call contract — see
        :class:`~resdag.init.input_feedback.InputFeedbackInitializer`);
        other keys are ignored.

    Returns
    -------
    torch.Tensor
        The initialized weight tensor (same object, modified in-place).
    """
    out_features, in_features = _resolve_shape(weight)
    device = weight.device
    dtype = weight.dtype

    generator = self._torch_generator_for(device)

    # Draw Gaussian values directly on the target device.
    values = torch.empty(out_features, in_features, device=device, dtype=dtype)
    values.normal_(self.loc, self.scale, generator=generator)

    # Apply the shared connectivity then scaling contract, in that order
    # (zeroing then scaling is identical either way; this matches the base
    # class's documented composition ``_apply_scaling(_apply_connectivity)``).
    # Forward ``**kwargs`` so per-call ``connectivity`` / ``input_scaling``
    # overrides win over the bound values.
    values = self._apply_connectivity(values, **kwargs)
    values = self._apply_scaling(values, **kwargs)

    with torch.no_grad():
        weight.copy_(values)

    return weight

OppositeAnchorsInitializer

OppositeAnchorsInitializer(input_scaling: float | None = None, gain: float | None = None)

Bases: InputFeedbackInitializer

Initializer that connects each input to two opposite anchors on an n-node ring.

Each input channel connects to two anchor nodes on opposite sides of the ring, with equal-magnitude bipolar weights on both anchors (input_scaling normalized by sqrt(2), so the per-channel L2 norm equals input_scaling). If the two anchors coincide (n=1), all weight goes to that single node.

The first (positive) anchor of channel i is placed at round(i * n / m) % n, spreading the m anchors evenly around the full ring; the second (negative) anchor is the diametrically opposite node (anchor + n // 2) % n. Spreading over the full ring (rather than a semicircle) keeps the columns distinct for any in_features <= reservoir_size, so W_in/W_fb stays full column rank.

This is useful for ring/cycle topologies where you want inputs distributed evenly around the ring with bipolar activation patterns.

Scaling

For this structured initializer the magnitude statistic governed by the shared contract is the per-channel L2 norm, which equals input_scaling. Hence input_scaling=0.5 makes every channel's column have L2 norm 0.5 (and, since each column is two equal-magnitude bipolar entries, max|W| = 0.5/sqrt(2)). The per-channel L2 norm therefore scales linearly with input_scaling.

Capacity limit

The ring has only n = reservoir_size nodes, so at most n channels can be assigned distinct anchors. initialize raises ValueError when in_features > reservoir_size (more channels than nodes), since duplicate columns would be unavoidable.

PARAMETER DESCRIPTION
input_scaling

Per-channel L2 norm of each input column. Defaults to 1.0 when neither input_scaling nor the deprecated gain is given.

TYPE: float DEFAULT: None

gain

Deprecated alias for input_scaling (same meaning). Emits a DeprecationWarning; passing both input_scaling and gain raises.

TYPE: float DEFAULT: None

RAISES DESCRIPTION
ValueError

If the resolved scaling is not a positive finite float (at construction), if both input_scaling and gain are supplied, or if in_features > reservoir_size when :meth:initialize is called.

Examples:

>>> from resdag.init.input_feedback import OppositeAnchorsInitializer
>>>
>>> init = OppositeAnchorsInitializer(input_scaling=1.0)
>>> weight = torch.empty(100, 5)  # (reservoir_size, num_inputs)
>>> init.initialize(weight)
>>>
>>> # Each input connects to two opposite points on the ring
Source code in src/resdag/init/input_feedback/opposite_anchors.py
def __init__(
    self,
    input_scaling: float | None = None,
    gain: float | None = None,
) -> None:
    """Initialize the OppositeAnchorsInitializer."""
    resolved = _resolve_scaling_alias(input_scaling, gain, default=1.0)
    if resolved is None or not np.isfinite(resolved) or resolved <= 0:
        raise ValueError("input_scaling must be a positive finite float.")
    super().__init__(input_scaling=resolved)

gain property

gain: float | None

Deprecated alias for :attr:input_scaling.

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight tensor with opposite anchor pattern.

PARAMETER DESCRIPTION
weight

Weight tensor of shape (out_features, in_features) out_features = number of ring nodes (n) in_features = number of input channels (m)

TYPE: Tensor

**kwargs

Per-call keyword overrides. A recognized input_scaling key overrides the bound self.input_scaling (the per-channel L2-norm target) for this call only (the shared per-call contract — see :class:~resdag.init.input_feedback.InputFeedbackInitializer); other keys are ignored. The override must be a positive finite float (None is not accepted for this structured initializer).

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

Initialized weight tensor

RAISES DESCRIPTION
ValueError

If m or n is non-positive, if m > n (more input channels than ring nodes), which would force duplicate columns, or if a per-call input_scaling override is not a positive finite float.

Source code in src/resdag/init/input_feedback/opposite_anchors.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize weight tensor with opposite anchor pattern.

    Parameters
    ----------
    weight : torch.Tensor
        Weight tensor of shape (out_features, in_features)
        out_features = number of ring nodes (n)
        in_features = number of input channels (m)
    **kwargs
        Per-call keyword overrides. A recognized ``input_scaling`` key
        overrides the bound ``self.input_scaling`` (the per-channel L2-norm
        target) for this call only (the shared per-call contract — see
        :class:`~resdag.init.input_feedback.InputFeedbackInitializer`);
        other keys are ignored. The override must be a positive finite float
        (``None`` is not accepted for this structured initializer).

    Returns
    -------
    torch.Tensor
        Initialized weight tensor

    Raises
    ------
    ValueError
        If ``m`` or ``n`` is non-positive, if ``m > n`` (more input channels
        than ring nodes), which would force duplicate columns, or if a
        per-call ``input_scaling`` override is not a positive finite float.
    """
    n, m = _resolve_shape(weight)  # n=ring nodes, m=input channels
    device = weight.device
    dtype = weight.dtype
    compute_dtype = _numpy_compute_dtype(dtype)

    if m <= 0 or n <= 0:
        raise ValueError(f"m and n must be positive; received: (m={m}, n={n})")

    if m > n:
        raise ValueError(
            "opposite_anchors requires in_features <= reservoir_size: the ring has "
            f"only n={n} nodes, so at most {n} channels can be assigned distinct "
            f"anchors, but received in_features={m}. Reduce in_features or grow the "
            "reservoir."
        )

    # Build at the target precision so input_scaling / sqrt(2) keeps full
    # precision in a float64 weight instead of being truncated to float32.
    values = np.zeros((n, m), dtype=compute_dtype)
    half = n // 2

    # ``input_scaling`` is the per-channel L2 norm target. Resolve the
    # effective value from the per-call ``**kwargs`` (a recognized
    # ``input_scaling`` key wins) with the bound value as fallback, so a
    # per-call override is honored. The bound value is never None (the
    # constructor resolves the alias and defaults it to 1.0); a per-call
    # override must likewise be a positive finite float for this structured
    # initializer.
    scaling_resolved = self._resolve_input_scaling(kwargs)
    if scaling_resolved is None or not np.isfinite(scaling_resolved) or scaling_resolved <= 0:
        raise ValueError("input_scaling must be a positive finite float.")
    scaling = float(scaling_resolved)

    # Special case: n == 1
    if n == 1:
        # All weight on the single node; |w| == per-channel L2 norm.
        values[0, :] = scaling
    else:
        # Spread the positive anchors evenly around the *full* ring so distinct
        # channels never collide while m <= n. The negative anchor is the
        # diametrically opposite node.
        j0 = np.round(np.arange(m) * n / m).astype(int) % n
        j1 = (j0 + half) % n

        # Two equal-magnitude entries per column -> per-channel L2 norm is
        # sqrt(2) * w, so w = scaling / sqrt(2) makes the norm equal scaling.
        w = scaling / np.sqrt(2.0)
        values[j0, np.arange(m)] = w
        values[j1, np.arange(m)] = -w  # Negative for bipolar pattern

    # Convert to tensor and copy to weight
    weight_data = torch.from_numpy(values).to(device=device, dtype=dtype)

    with torch.no_grad():
        weight.copy_(weight_data)

    return weight

PseudoDiagonalInitializer

PseudoDiagonalInitializer(input_scaling: float | None = None, binarize: bool = False, seed: int | None = None)

Bases: InputFeedbackInitializer

Pseudo-diagonal initializer for structured input connections.

This initializer creates a structured input weight matrix where each input dimension connects to a contiguous block of reservoir neurons. This creates a "pseudo-diagonal" pattern that can improve input-to-reservoir mapping, especially when input dimensions have semantic meaning.

The connectivity pattern ensures:

  • Each reservoir neuron receives input from exactly one input dimension
  • Each input dimension connects to approximately N/D reservoir neurons (where N=reservoir size, D=input dimension)
  • Connections form contiguous blocks (not random)
PARAMETER DESCRIPTION
input_scaling

Uniform magnitude knob from the shared scaling contract (see :class:~resdag.init.input_feedback.InputFeedbackInitializer). None (the default) applies no scaling (nonzero entries stay in [-1, 1]); a float s multiplies every entry by s, so max|W| scales linearly with s (input_scaling=0.5 halves it).

TYPE: float DEFAULT: None

binarize

Whether to binarize weights to {-input_scaling, input_scaling} instead of uniform distribution.

TYPE: bool DEFAULT: False

seed

Random seed for reproducibility.

TYPE: int DEFAULT: None

Examples:

>>> from resdag.init.input_feedback import PseudoDiagonalInitializer
>>>
>>> init = PseudoDiagonalInitializer(input_scaling=1.0, binarize=False, seed=42)
>>> weight = torch.empty(200, 5)  # (reservoir_size, input_dim)
>>> init.initialize(weight)
>>>
>>> # Each of the 5 inputs connects to a contiguous block of ~40 neurons
Source code in src/resdag/init/input_feedback/pseudo_diagonal.py
def __init__(
    self,
    input_scaling: float | None = None,
    binarize: bool = False,
    seed: int | None = None,
) -> None:
    """Initialize the PseudoDiagonalInitializer."""
    super().__init__(input_scaling=input_scaling, seed=seed)
    self.binarize = binarize

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight tensor with pseudo-diagonal structure.

The RNG is constructed from self.seed on every call, so the produced matrix is a pure function of (seed, shape). Repeated calls on the same instance with equal shapes therefore yield identical matrices. Pass seed=None for a fresh draw on each call.

PARAMETER DESCRIPTION
weight

Weight tensor of shape (out_features, in_features)

TYPE: Tensor

**kwargs

Per-call keyword overrides. A recognized input_scaling key overrides the bound self.input_scaling for this call only (the shared per-call contract — see :class:~resdag.init.input_feedback.InputFeedbackInitializer); other keys are ignored.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

Initialized weight tensor with block structure

Source code in src/resdag/init/input_feedback/pseudo_diagonal.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize weight tensor with pseudo-diagonal structure.

    The RNG is constructed from ``self.seed`` on every call, so the produced
    matrix is a pure function of ``(seed, shape)``. Repeated calls on the
    same instance with equal shapes therefore yield identical matrices.
    Pass ``seed=None`` for a fresh draw on each call.

    Parameters
    ----------
    weight : torch.Tensor
        Weight tensor of shape (out_features, in_features)
    **kwargs
        Per-call keyword overrides. A recognized ``input_scaling`` key
        overrides the bound ``self.input_scaling`` for this call only (the
        shared per-call contract — see
        :class:`~resdag.init.input_feedback.InputFeedbackInitializer`);
        other keys are ignored.

    Returns
    -------
    torch.Tensor
        Initialized weight tensor with block structure
    """
    out_features, in_features = _resolve_shape(weight)
    device = weight.device
    dtype = weight.dtype
    compute_dtype = _numpy_compute_dtype(dtype)

    rng = np.random.default_rng(coerce_seed_to_int(self.seed))

    # Create sparse block-diagonal structure at the target precision so that
    # the float64 ``rng.uniform`` draws are not truncated to float32.
    values = np.zeros((out_features, in_features), dtype=compute_dtype)

    # Case 1: out_features >= in_features (typical for reservoirs)
    # Each input feature gets a contiguous block of output neurons
    if out_features >= in_features:
        block_size = out_features // in_features
        remainder = out_features % in_features

        start_row = 0
        for col in range(in_features):
            # Determine block size for this column
            end_row = start_row + block_size + (1 if col < remainder else 0)

            # Fill this block
            block_values = rng.uniform(-1.0, 1.0, size=(end_row - start_row,))
            if self.binarize:
                block_values = np.sign(block_values)

            values[start_row:end_row, col] = block_values
            start_row = end_row

    # Case 2: out_features < in_features (rare case)
    # Each output neuron gets input from a contiguous block of input features
    else:
        block_size = in_features // out_features
        remainder = in_features % out_features

        start_col = 0
        for row in range(out_features):
            # Determine block size for this row
            end_col = start_col + block_size + (1 if row < remainder else 0)

            # Fill this block
            block_values = rng.uniform(-1.0, 1.0, size=(end_col - start_col,))
            if self.binarize:
                block_values = np.sign(block_values)

            values[row, start_col:end_col] = block_values
            start_col = end_col

    # Apply the shared uniform scaling contract as the documented final
    # transform. Forward ``**kwargs`` so a per-call ``input_scaling`` override
    # wins over the bound value.
    values = self._apply_scaling(values, **kwargs)

    # Convert to tensor and copy to weight
    weight_data = torch.from_numpy(values).to(device=device, dtype=dtype)

    with torch.no_grad():
        weight.copy_(weight_data)

    return weight

RandomBinaryInitializer

RandomBinaryInitializer(input_scaling: float | None = None, seed: SeedLike = None)

Bases: InputFeedbackInitializer

Binary random initializer for input/feedback weight matrices.

This initializer creates binary weight matrices with values in {-1, +1}. Each weight is randomly chosen to be either -1 or +1, optionally scaled.

Binary weights can be advantageous for: - Memory efficiency (can be stored as bits) - Computational efficiency (multiplication becomes addition/subtraction) - Improved robustness in some cases - Easier interpretation

PARAMETER DESCRIPTION
input_scaling

Uniform magnitude knob from the shared scaling contract (see :class:~resdag.init.input_feedback.InputFeedbackInitializer). None (the default) leaves entries in {-1, +1}; a float s multiplies every entry by s (entries become {-s, +s}), so max|W| scales linearly with s (input_scaling=0.5 halves it).

TYPE: float DEFAULT: None

seed

Reproducibility seed for the binary draw. Accepts a plain int, a :class:torch.Generator (whose initial_seed() is used), or None (defer to torch's global RNG). The draw is device-native: it happens directly on the target weight's device via a torch generator, so the same seed is reproducible per device (CPU and CUDA each reproduce, though their RNG streams differ from each other).

TYPE: int, torch.Generator, or None DEFAULT: None

Examples:

>>> from resdag.init.input_feedback import RandomBinaryInitializer
>>>
>>> # Create binary initializer
>>> init = RandomBinaryInitializer(input_scaling=0.5, seed=42)
>>>
>>> # Initialize weights
>>> weight = torch.empty(100, 10)
>>> init.initialize(weight)
>>>
>>> # All values will be either -0.5 or +0.5
>>> unique_values = torch.unique(weight)
>>> print(unique_values)  # tensor([-0.5, 0.5])
Source code in src/resdag/init/input_feedback/random_binary.py
def __init__(
    self,
    input_scaling: float | None = None,
    seed: SeedLike = None,
) -> None:
    """Initialize the RandomBinaryInitializer."""
    super().__init__(input_scaling=input_scaling, seed=seed)

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight tensor with binary random values.

The draw uses a :class:torch.Generator built from self.seed on the target weight's device (no CPU build + copy), so the produced matrix is a pure function of (seed, shape, device). Repeated calls on the same instance with equal shapes on the same device therefore yield identical matrices. Pass seed=None for a draw tied to torch's global RNG (reproducible under torch.manual_seed).

PARAMETER DESCRIPTION
weight

Weight tensor of shape (out_features, in_features)

TYPE: Tensor

**kwargs

Per-call keyword overrides. A recognized input_scaling key overrides the bound self.input_scaling for this call only (the shared per-call contract — see :class:~resdag.init.input_feedback.InputFeedbackInitializer); other keys are ignored.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

Initialized weight tensor with binary values

Source code in src/resdag/init/input_feedback/random_binary.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize weight tensor with binary random values.

    The draw uses a :class:`torch.Generator` built from ``self.seed`` on the
    target weight's **device** (no CPU build + copy), so the produced matrix
    is a pure function of ``(seed, shape, device)``. Repeated calls on the
    same instance with equal shapes on the same device therefore yield
    identical matrices. Pass ``seed=None`` for a draw tied to torch's global
    RNG (reproducible under ``torch.manual_seed``).

    Parameters
    ----------
    weight : torch.Tensor
        Weight tensor of shape (out_features, in_features)
    **kwargs
        Per-call keyword overrides. A recognized ``input_scaling`` key
        overrides the bound ``self.input_scaling`` for this call only (the
        shared per-call contract — see
        :class:`~resdag.init.input_feedback.InputFeedbackInitializer`);
        other keys are ignored.

    Returns
    -------
    torch.Tensor
        Initialized weight tensor with binary values
    """
    out_features, in_features = _resolve_shape(weight)
    device = weight.device
    dtype = weight.dtype

    generator = self._torch_generator_for(device)

    # Draw bits {0, 1} on the target device, then map to {-1, +1}.
    bits = torch.randint(0, 2, (out_features, in_features), generator=generator, device=device)
    values = (bits.to(dtype) * 2.0) - 1.0

    # Apply the shared uniform scaling contract as the documented final
    # transform (torch-native, staying on-device). Forward ``**kwargs`` so a
    # per-call ``input_scaling`` override wins over the bound value.
    values = self._apply_scaling(values, **kwargs)

    with torch.no_grad():
        weight.copy_(values)

    return weight

RandomInputInitializer

RandomInputInitializer(input_scaling: float | None = None, seed: SeedLike = None)

Bases: InputFeedbackInitializer

Random initializer for feedback/input weight matrices.

This initializer creates random weight matrices connecting inputs (feedback or external) to reservoir neurons. Values are sampled uniformly from [-1, 1] and optionally scaled by an input scaling factor.

This is a simple, commonly used initializer for input connections. It provides random, unstructured connectivity from inputs to the reservoir.

PARAMETER DESCRIPTION
input_scaling

Uniform magnitude knob from the shared scaling contract (see :class:~resdag.init.input_feedback.InputFeedbackInitializer). None (the default) applies no scaling, leaving entries in [-1, 1]; a float s multiplies every entry by s, so max|W| scales linearly with s (input_scaling=0.5 halves it). Controls the strength of input signals entering the reservoir. Typical values: 0.1-5.0.

TYPE: float DEFAULT: None

seed

Reproducibility seed for the uniform draw. Accepts a plain int, a :class:torch.Generator (whose initial_seed() is used, so a generator and the equivalent int agree), or None (defer to torch's global RNG). The draw is device-native: it happens directly on the target weight's device via a torch generator, so the same seed is reproducible per device (CPU and CUDA each reproduce, though their RNG streams differ from each other). Ensures the same weight matrix is generated for the same seed, shape, and device.

TYPE: int, torch.Generator, or None DEFAULT: None

Notes

Input Scaling:

The input_scaling parameter controls how strongly input signals affect the reservoir: - Low scaling (0.1-0.5): Weak input influence, reservoir dynamics dominate - Moderate scaling (0.5-1.0): Balanced input and reservoir dynamics - High scaling (1.0-5.0): Strong input influence, input-driven dynamics

Usage:

This initializer is typically used for: - Feedback weights: How feedback signals enter the reservoir - Input weights: How external inputs enter the reservoir (if used)

Best Practices:

  • Start with input_scaling=1.0 and tune based on performance
  • Lower scaling often works better for chaotic systems
  • Higher scaling can help when inputs are weak or noisy
  • Use seed for reproducibility

Examples:

>>> from resdag.init.input_feedback import RandomInputInitializer
>>>
>>> # Create initializer
>>> init = RandomInputInitializer(input_scaling=1.0, seed=42)
>>>
>>> # Initialize a weight tensor
>>> weight = torch.empty(100, 10)  # (reservoir_size, feedback_size)
>>> init.initialize(weight)
>>>
>>> # Use in ESNLayer
>>> reservoir = ESNLayer(
...     reservoir_size=100,
...     feedback_size=10,
...     feedback_initializer=init
... )
Source code in src/resdag/init/input_feedback/random.py
def __init__(
    self,
    input_scaling: float | None = None,
    seed: SeedLike = None,
) -> None:
    """Initialize the RandomInputInitializer."""
    super().__init__(input_scaling=input_scaling, seed=seed)

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight tensor with uniform random values.

The draw uses a :class:torch.Generator built from self.seed on the target weight's device (no CPU build + copy), so the produced matrix is a pure function of (seed, shape, device). Repeated calls on the same instance with equal shapes on the same device therefore yield identical matrices. Pass seed=None for a draw tied to torch's global RNG (reproducible under torch.manual_seed).

PARAMETER DESCRIPTION
weight

Weight tensor of shape (out_features, in_features)

TYPE: Tensor

**kwargs

Per-call keyword overrides. A recognized input_scaling key overrides the bound self.input_scaling for this call only (the shared per-call contract — see :class:~resdag.init.input_feedback.InputFeedbackInitializer); other keys are ignored.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

Initialized weight tensor

Source code in src/resdag/init/input_feedback/random.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize weight tensor with uniform random values.

    The draw uses a :class:`torch.Generator` built from ``self.seed`` on the
    target weight's **device** (no CPU build + copy), so the produced matrix
    is a pure function of ``(seed, shape, device)``. Repeated calls on the
    same instance with equal shapes on the same device therefore yield
    identical matrices. Pass ``seed=None`` for a draw tied to torch's global
    RNG (reproducible under ``torch.manual_seed``).

    Parameters
    ----------
    weight : torch.Tensor
        Weight tensor of shape (out_features, in_features)
    **kwargs
        Per-call keyword overrides. A recognized ``input_scaling`` key
        overrides the bound ``self.input_scaling`` for this call only (the
        shared per-call contract — see
        :class:`~resdag.init.input_feedback.InputFeedbackInitializer`);
        other keys are ignored.

    Returns
    -------
    torch.Tensor
        Initialized weight tensor
    """
    out_features, in_features = _resolve_shape(weight)
    device = weight.device
    dtype = weight.dtype

    generator = self._torch_generator_for(device)

    # Draw uniform values in [-1, 1] directly on the target device.
    values = torch.empty(out_features, in_features, device=device, dtype=dtype)
    values.uniform_(-1.0, 1.0, generator=generator)

    # Apply the shared uniform scaling contract as the documented final
    # transform (torch-native, staying on-device). Forward ``**kwargs`` so a
    # per-call ``input_scaling`` override wins over the bound value.
    values = self._apply_scaling(values, **kwargs)

    with torch.no_grad():
        weight.copy_(values)

    return weight

RingWindowInputInitializer

RingWindowInputInitializer(c: float, window: int | float, taper: str = 'flat', signed: str = 'allpos', input_scaling: float | None = None, gain: float | None = None)

Bases: InputFeedbackInitializer

Deterministic windowed input initializer for ring-based topologies.

Feeds each input channel into a contiguous window on the core ring of a dendrocycle(+chords) reservoir. Only the first C = round(c * n) columns (core) receive nonzeros.

Scaling

For this structured initializer the magnitude statistic governed by the shared contract is the per-channel L2 norm, which equals input_scaling: each channel's window vector is renormalized so its L2 norm is exactly input_scaling. Hence input_scaling=0.5 makes every channel's column have L2 norm 0.5; the per-channel L2 norm scales linearly with input_scaling.

PARAMETER DESCRIPTION
c

Fraction of core ring nodes. First round(c*n) columns are core.

TYPE: float

window

If int >= 1: number of core nodes per channel window. If float in (0, 1]: fraction of C per channel window (rounded >= 1).

TYPE: int or float

taper

Weight profile within a channel's window centered on its center index.

TYPE: (flat, triangle, cosine) DEFAULT: "flat"

signed

Sign policy for weights.

TYPE: (allpos, alt_ring, alt_inputs) DEFAULT: "allpos"

input_scaling

Per-channel L2-norm after taper/sign are applied. Defaults to 1.0 when neither input_scaling nor the deprecated gain is given.

TYPE: float DEFAULT: None

gain

Deprecated alias for input_scaling (same meaning). Emits a DeprecationWarning; passing both input_scaling and gain raises.

TYPE: float DEFAULT: None

Examples:

>>> from resdag.init.input_feedback import RingWindowInputInitializer
>>>
>>> init = RingWindowInputInitializer(
...     c=0.5, window=10, taper="cosine", signed="alt_ring", input_scaling=1.0
... )
>>> weight = torch.empty(100, 5)  # (reservoir_size, num_inputs)
>>> init.initialize(weight)
>>>
>>> # Each input connects to a windowed region of the core ring
Source code in src/resdag/init/input_feedback/ring_window.py
def __init__(
    self,
    c: float,
    window: int | float,
    taper: str = "flat",
    signed: str = "allpos",
    input_scaling: float | None = None,
    gain: float | None = None,
) -> None:
    """Initialize the RingWindowInputInitializer."""
    if not (0 < c <= 1):
        raise ValueError("c must be in (0,1].")
    self.c = float(c)

    self.window: int | float
    if isinstance(window, int):
        if window < 1:
            raise ValueError("window int must be >= 1.")
        self.window = window
        self.window_is_frac = False
    elif isinstance(window, float):
        if not (0 < window <= 1):
            raise ValueError("window float must be in (0,1].")
        self.window = float(window)
        self.window_is_frac = True
    else:
        raise TypeError("window must be int or float.")

    if taper not in {"flat", "triangle", "cosine"}:
        raise ValueError("taper must be 'flat', 'triangle', or 'cosine'.")
    if signed not in {"allpos", "alt_ring", "alt_inputs"}:
        raise ValueError("signed must be 'allpos', 'alt_ring', or 'alt_inputs'.")

    resolved = _resolve_scaling_alias(input_scaling, gain, default=1.0)
    if resolved is None or not (np.isfinite(resolved) and resolved > 0):
        raise ValueError("input_scaling must be a positive finite float.")
    super().__init__(input_scaling=resolved)

    self.taper = taper
    self.signed = signed

gain property

gain: float | None

Deprecated alias for :attr:input_scaling.

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight tensor with ring window pattern.

PARAMETER DESCRIPTION
weight

Weight tensor of shape (out_features, in_features) out_features = reservoir_size (n) in_features = num_inputs (m)

TYPE: Tensor

**kwargs

Per-call keyword overrides. A recognized input_scaling key overrides the bound self.input_scaling (the per-channel L2-norm target) for this call only (the shared per-call contract — see :class:~resdag.init.input_feedback.InputFeedbackInitializer); other keys are ignored. The override must be a positive finite float (None is not accepted for this structured initializer).

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

Initialized weight tensor

Source code in src/resdag/init/input_feedback/ring_window.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize weight tensor with ring window pattern.

    Parameters
    ----------
    weight : torch.Tensor
        Weight tensor of shape (out_features, in_features)
        out_features = reservoir_size (n)
        in_features = num_inputs (m)
    **kwargs
        Per-call keyword overrides. A recognized ``input_scaling`` key
        overrides the bound ``self.input_scaling`` (the per-channel L2-norm
        target) for this call only (the shared per-call contract — see
        :class:`~resdag.init.input_feedback.InputFeedbackInitializer`);
        other keys are ignored. The override must be a positive finite float
        (``None`` is not accepted for this structured initializer).

    Returns
    -------
    torch.Tensor
        Initialized weight tensor
    """
    n, m = _resolve_shape(weight)
    device = weight.device
    dtype = weight.dtype
    compute_dtype = _numpy_compute_dtype(dtype)

    if m <= 0 or n <= 0:
        raise ValueError("m and n must be positive.")

    C = max(1, int(round(self.c * n)))  # core size
    W = self._window_size(C)

    # ``input_scaling`` is the per-channel L2 norm target. Resolve the
    # effective value from the per-call ``**kwargs`` (a recognized
    # ``input_scaling`` key wins) with the bound value as fallback, so a
    # per-call override is honored. The bound value is never None (the
    # constructor resolves the alias and defaults it to 1.0); a per-call
    # override must likewise be a positive finite float for this structured
    # initializer.
    scaling_resolved = self._resolve_input_scaling(kwargs)
    if scaling_resolved is None or not (np.isfinite(scaling_resolved) and scaling_resolved > 0):
        raise ValueError("input_scaling must be a positive finite float.")
    scaling = float(scaling_resolved)

    # Build at the target precision so the float64 taper/norm computation is
    # not truncated to float32 before reaching a float64 weight.
    values = np.zeros((n, m), dtype=compute_dtype)
    base = self._taper_vector(W, self.taper).astype(compute_dtype)

    def window_indices(center: int) -> np.ndarray:
        start = center - (W // 2)
        return (np.arange(start, start + W) % C).astype(int)

    for k in range(m):
        start = int(np.floor(k * C / m)) % C
        center = (start + (W - 1) // 2) % C

        cols_core = window_indices(center)
        row_vals = base.copy()

        signs: float | np.ndarray
        if self.signed == "allpos":
            signs = 1.0
        elif self.signed == "alt_ring":
            signs = (1.0 - 2.0 * (cols_core % 2)).astype(compute_dtype)
        else:  # "alt_inputs"
            signs = 1.0 if (k % 2 == 0) else -1.0

        row_vals = row_vals * signs
        norm = float(np.linalg.norm(row_vals))
        scaled = row_vals if norm == 0.0 else (scaling / norm) * row_vals

        values[cols_core, k] = scaled  # Only core; rest stay zero

    # Convert to tensor and copy to weight
    weight_data = torch.from_numpy(values).to(device=device, dtype=dtype)

    with torch.no_grad():
        weight.copy_(weight_data)

    return weight

UniformInputInitializer

UniformInputInitializer(low: float = -1.0, high: float = 1.0, connectivity: float | None = None, input_scaling: float | None = None, seed: SeedLike = None)

Bases: InputFeedbackInitializer

Bounded uniform initializer for input/feedback weight matrices.

Draws every entry independently from Uniform(low, high). Unlike the baseline random initializer (fixed [-1, 1]), the bounds are configurable, so asymmetric or narrow ranges are expressible directly. Optionally sparsified per input channel via connectivity and scaled uniformly via the shared input_scaling contract.

This is the migration target for reservoirpy's :func:reservoirpy.mat_gen.uniform (and the "uniform" distribution of :func:reservoirpy.mat_gen.random_sparse).

PARAMETER DESCRIPTION
low

Inclusive lower bound of the uniform draw. Defaults to -1.0. Must be strictly less than high.

TYPE: float DEFAULT: -1.0

high

Exclusive upper bound of the uniform draw. Defaults to 1.0.

TYPE: float DEFAULT: 1.0

connectivity

Density knob from the shared scaling contract (see :class:~resdag.init.input_feedback.InputFeedbackInitializer): the fraction of nonzero entries kept per column (input channel), in (0, 1]. None (the default) leaves the dense matrix untouched; connectivity=0.1 keeps ~10% of each column's entries (at least one), zeroing the rest. The kept fraction is statistically exact: round(connectivity * reservoir_size) entries per column.

TYPE: float DEFAULT: None

input_scaling

Uniform magnitude knob from the shared scaling contract. None (the default) applies no scaling; a float s multiplies every entry by s, so max|W| scales linearly with s (input_scaling=0.5 halves it). Applied after the [low, high] draw, so it rescales the bounds rather than replacing them.

TYPE: float DEFAULT: None

seed

Reproducibility seed for the uniform draw and the connectivity mask. Accepts a plain int, a :class:torch.Generator (whose initial_seed() is used, so a generator and the equivalent int agree), or None (defer to torch's global RNG). The value draw is device-native: it happens directly on the target weight's device via a torch generator, so the same seed is reproducible per device (CPU and CUDA each reproduce, though their RNG streams differ from each other).

TYPE: int, torch.Generator, or None DEFAULT: None

Notes

The produced matrix is a pure function of (low, high, connectivity, input_scaling, seed, shape, device): repeated calls on the same instance with equal shapes on the same device yield identical matrices. Pass seed=None for a draw tied to torch's global RNG (reproducible under torch.manual_seed).

Examples:

>>> from resdag.init.input_feedback import UniformInputInitializer
>>>
>>> # A narrow, asymmetric uniform input matrix.
>>> init = UniformInputInitializer(low=0.0, high=0.5, seed=42)
>>> weight = torch.empty(100, 10)  # (reservoir_size, feedback_size)
>>> init.initialize(weight)
>>>
>>> # Use in ESNLayer.
>>> from resdag.layers import ESNLayer
>>> reservoir = ESNLayer(
...     reservoir_size=100,
...     feedback_size=10,
...     feedback_initializer=init,
... )
Source code in src/resdag/init/input_feedback/uniform.py
def __init__(
    self,
    low: float = -1.0,
    high: float = 1.0,
    connectivity: float | None = None,
    input_scaling: float | None = None,
    seed: SeedLike = None,
) -> None:
    """Initialize the UniformInputInitializer."""
    super().__init__(
        input_scaling=input_scaling,
        connectivity=connectivity,
        seed=seed,
    )
    low = float(low)
    high = float(high)
    if not low < high:
        raise ValueError(
            f"low must be strictly less than high; got low={low!r}, high={high!r}."
        )
    self.low = low
    self.high = high

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight with Uniform(low, high) entries.

PARAMETER DESCRIPTION
weight

Weight tensor of shape (out_features, in_features). Modified in-place.

TYPE: Tensor

**kwargs

Per-call keyword overrides. Recognized input_scaling and connectivity keys override the bound attributes for this call only (the shared per-call contract — see :class:~resdag.init.input_feedback.InputFeedbackInitializer); other keys are ignored.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Tensor

The initialized weight tensor (same object, modified in-place).

Source code in src/resdag/init/input_feedback/uniform.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize ``weight`` with ``Uniform(low, high)`` entries.

    Parameters
    ----------
    weight : torch.Tensor
        Weight tensor of shape ``(out_features, in_features)``. Modified
        in-place.
    **kwargs
        Per-call keyword overrides. Recognized ``input_scaling`` and
        ``connectivity`` keys override the bound attributes for this call
        only (the shared per-call contract — see
        :class:`~resdag.init.input_feedback.InputFeedbackInitializer`);
        other keys are ignored.

    Returns
    -------
    torch.Tensor
        The initialized weight tensor (same object, modified in-place).
    """
    out_features, in_features = _resolve_shape(weight)
    device = weight.device
    dtype = weight.dtype

    generator = self._torch_generator_for(device)

    # Draw uniform values in [low, high) directly on the target device.
    values = torch.empty(out_features, in_features, device=device, dtype=dtype)
    values.uniform_(self.low, self.high, generator=generator)

    # Apply the shared connectivity then scaling contract, in that order
    # (zeroing then scaling is identical either way; this matches the base
    # class's documented composition ``_apply_scaling(_apply_connectivity)``).
    # Forward ``**kwargs`` so per-call ``connectivity`` / ``input_scaling``
    # overrides win over the bound values.
    values = self._apply_connectivity(values, **kwargs)
    values = self._apply_scaling(values, **kwargs)

    with torch.no_grad():
        weight.copy_(values)

    return weight

ZeroInitializer

ZeroInitializer()

Bases: InputFeedbackInitializer

Initializer that sets all weights to zero.

Source code in src/resdag/init/input_feedback/zero.py
def __init__(self) -> None:
    """Initialize the ZeroInitializer."""
    super().__init__()

initialize

initialize(weight: Tensor, **kwargs: Any) -> Tensor

Initialize weight tensor with zero values.

Source code in src/resdag/init/input_feedback/zero.py
def initialize(self, weight: torch.Tensor, **kwargs: Any) -> torch.Tensor:
    """Initialize weight tensor with zero values."""
    with torch.no_grad():
        weight.zero_()
    return weight

get_input_feedback

get_input_feedback(name: str, **override_kwargs: Any) -> InputFeedbackInitializer

Get a pre-configured input/feedback initializer by name.

PARAMETER DESCRIPTION
name

Name of the initializer (e.g., "random", "binary_balanced")

TYPE: str

**override_kwargs

Keyword arguments to override default initializer parameters

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
InputFeedbackInitializer

Initializer instance

RAISES DESCRIPTION
ValueError

If initializer name is not registered

Examples:

>>> initializer = get_input_feedback("binary_balanced", input_scaling=0.5)
>>> weight = torch.empty(100, 10)
>>> initializer.initialize(weight)
Source code in src/resdag/init/input_feedback/registry.py
def get_input_feedback(
    name: str,
    **override_kwargs: Any,
) -> InputFeedbackInitializer:
    """Get a pre-configured input/feedback initializer by name.

    Parameters
    ----------
    name : str
        Name of the initializer (e.g., "random", "binary_balanced")
    **override_kwargs
        Keyword arguments to override default initializer parameters

    Returns
    -------
    InputFeedbackInitializer
        Initializer instance

    Raises
    ------
    ValueError
        If initializer name is not registered

    Examples
    --------
    >>> initializer = get_input_feedback("binary_balanced", input_scaling=0.5)
    >>> weight = torch.empty(100, 10)
    >>> initializer.initialize(weight)
    """
    if name not in _INPUT_FEEDBACK_REGISTRY:
        available = ", ".join(_INPUT_FEEDBACK_REGISTRY.keys())
        raise ValueError(f"Unknown initializer '{name}'. Available initializers: {available}")

    entry, default_kwargs = _INPUT_FEEDBACK_REGISTRY[name]

    # Merge default kwargs with overrides
    kwargs = {**default_kwargs, **override_kwargs}

    if inspect.isclass(entry) and issubclass(entry, InputFeedbackInitializer):
        return entry(**kwargs)
    return FunctionInitializer(entry, **kwargs)

register_input_feedback

register_input_feedback(name: str, **default_kwargs: Any) -> Callable[[Callable], Callable]

Decorator to register an input/feedback initializer.

Registers either an :class:InputFeedbackInitializer subclass or a plain matrix-building function in the registry at definition time, making it available by name to ESNLayer and other components.

PARAMETER DESCRIPTION
name

Name for the initializer (must be unique)

TYPE: str

**default_kwargs

Default keyword arguments for the initializer constructor (classes) or for the function call (plain functions)

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
callable

Decorator function

Examples:

Registering a class:

>>> @register_input_feedback("my_init", scaling=0.5)
... class MyInitializer(InputFeedbackInitializer):
...     def __init__(self, scaling=1.0):
...         self.scaling = scaling
...
...     def initialize(self, weight, **kwargs):
...         # ... initialization logic
...         return weight

Registering a plain function (build style — return the matrix):

>>> @register_input_feedback("first_neuron", scale=1.0)
... def first_neuron(rows, cols, scale=1.0):
...     w = torch.zeros(rows, cols)
...     w[0, :] = scale
...     return w
Notes
  • Classes must inherit from InputFeedbackInitializer and implement initialize(weight, **kwargs).
  • Functions follow the :class:FunctionInitializer conventions: fn(rows, cols, **kwargs) -> matrix or in-place fn(tensor, **kwargs).
  • Registered initializers can be accessed via get_input_feedback(name).
Source code in src/resdag/init/input_feedback/registry.py
def register_input_feedback(
    name: str,
    **default_kwargs: Any,
) -> Callable[[Callable], Callable]:
    """Decorator to register an input/feedback initializer.

    Registers either an :class:`InputFeedbackInitializer` subclass **or a
    plain matrix-building function** in the registry at definition time,
    making it available by name to ``ESNLayer`` and other components.

    Parameters
    ----------
    name : str
        Name for the initializer (must be unique)
    **default_kwargs
        Default keyword arguments for the initializer constructor (classes)
        or for the function call (plain functions)

    Returns
    -------
    callable
        Decorator function

    Examples
    --------
    Registering a class:

    >>> @register_input_feedback("my_init", scaling=0.5)
    ... class MyInitializer(InputFeedbackInitializer):
    ...     def __init__(self, scaling=1.0):
    ...         self.scaling = scaling
    ...
    ...     def initialize(self, weight, **kwargs):
    ...         # ... initialization logic
    ...         return weight

    Registering a plain function (build style — return the matrix):

    >>> @register_input_feedback("first_neuron", scale=1.0)
    ... def first_neuron(rows, cols, scale=1.0):
    ...     w = torch.zeros(rows, cols)
    ...     w[0, :] = scale
    ...     return w

    Notes
    -----
    - Classes must inherit from ``InputFeedbackInitializer`` and implement
      ``initialize(weight, **kwargs)``.
    - Functions follow the :class:`FunctionInitializer` conventions:
      ``fn(rows, cols, **kwargs) -> matrix`` or in-place ``fn(tensor, **kwargs)``.
    - Registered initializers can be accessed via ``get_input_feedback(name)``.
    """

    def decorator(obj: Callable) -> Callable:
        if name in _INPUT_FEEDBACK_REGISTRY:
            raise ValueError(f"Input/feedback initializer '{name}' is already registered")
        _INPUT_FEEDBACK_REGISTRY[name] = (obj, default_kwargs)
        return obj

    return decorator

show_input_initializers

show_input_initializers(name: str | None = None) -> list[str] | None

Show available input/feedback initializers or details for a specific one.

PARAMETER DESCRIPTION
name

Name of initializer to inspect. If None, prints all initializers and returns them as a list.

TYPE: str DEFAULT: None

RETURNS DESCRIPTION
list of str or None

When name is None, returns the sorted list of registered initializer names (in addition to printing them). When name is provided, returns None after printing the parameter table.

RAISES DESCRIPTION
ValueError

If the specified initializer name is not registered.

Examples:

>>> show_input_initializers()
['binary_balanced', 'chebyshev', 'chessboard', ...]
Source code in src/resdag/init/input_feedback/registry.py
def show_input_initializers(name: str | None = None) -> list[str] | None:
    """Show available input/feedback initializers or details for a specific one.

    Parameters
    ----------
    name : str, optional
        Name of initializer to inspect. If None, prints all initializers
        *and* returns them as a list.

    Returns
    -------
    list of str or None
        When ``name is None``, returns the sorted list of registered
        initializer names (in addition to printing them).  When ``name``
        is provided, returns ``None`` after printing the parameter table.

    Raises
    ------
    ValueError
        If the specified initializer name is not registered.

    Examples
    --------
    >>> show_input_initializers()
    ['binary_balanced', 'chebyshev', 'chessboard', ...]
    """
    if name is None:
        names = sorted(_INPUT_FEEDBACK_REGISTRY)
        print("\nAvailable input/feedback initializers:\n")
        for n in names:
            print(f"  - {n}")
        print(f"\nTotal: {len(names)}\n")
        return names

    if name not in _INPUT_FEEDBACK_REGISTRY:
        available = "\n".join(sorted(_INPUT_FEEDBACK_REGISTRY.keys()))
        raise ValueError(f"Unknown initializer '{name}'.\nAvailable:\n{available}")

    entry, default_kwargs = _INPUT_FEEDBACK_REGISTRY[name]
    is_class = inspect.isclass(entry)
    sig = inspect.signature(entry.__init__) if is_class else inspect.signature(entry)

    print(f"\nInitializer: {name} ({'class' if is_class else 'function'})\n")
    print("Parameters:\n")

    # Skip non-hyperparameter params: self (classes) and the matrix
    # dimensions / target tensor (functions).
    skip = {"self"} if is_class else {"rows", "cols", "tensor", "weight"}
    for param_name, param in sig.parameters.items():
        if param_name in skip:
            continue

        # Type extraction
        if param.annotation is not inspect.Parameter.empty:
            origin = get_origin(param.annotation)
            if origin is None:
                type_str = param.annotation.__name__
            else:
                args = get_args(param.annotation)
                type_str = " | ".join(a.__name__ for a in args)
        else:
            type_str = "Any"

        # Default resolution
        if param.default is not inspect.Parameter.empty:
            default = param.default
        else:
            default = default_kwargs.get(param_name, "<required>")

        print(f"  - {param_name}")
        print(f"      type:    {type_str}")
        print(f"      default: {default}\n")

    return None

matrices

Matrix-Builder Topologies

Direct matrix-construction functions registered as topologies — the non-graph counterpart of :mod:resdag.init.graphs. Each function takes the matrix size n first, returns an (n, n) matrix, and is registered via :func:~resdag.init.topology.register_matrix_topology, making it available by name in ESNLayer(topology="...").

FUNCTION DESCRIPTION
orthogonal_matrix

Haar-random orthogonal matrix via QR decomposition ("orthogonal").

antisymmetric_matrix

Random antisymmetric (skew-symmetric) matrix A - A^T with purely imaginary eigenvalues ("antisymmetric").

fast_spectral_initialization

Recurrent matrix built at a target spectral radius analytically, with no eigendecomposition ("fast_spectral_initialization").

delay_line, delay_line_backward, simple_cycle, cycle_jumps, self_loop_cycle

Deterministic-minimal (Rodan & Tiňo) reservoirs — a pure delay line, a delay line with a backward connection, a unidirectional cycle, a cycle with regular jumps, and a self-loop cycle ("delay_line", "delay_line_backward", "simple_cycle", "cycle_jumps", "self_loop_cycle").

toeplitz, band, block_diagonal

Structured reservoirs — a Toeplitz (constant-per-diagonal) matrix, a banded matrix, and a block-diagonal matrix ("toeplitz", "band", "block_diagonal").

See Also

resdag.init.graphs : Graph-based topology generators. resdag.init.topology : Registry and initializer classes.

orthogonal_matrix

orthogonal_matrix(n: int, gain: float = 1.0, seed: int | None = None) -> Tensor

Build a random orthogonal matrix via QR decomposition.

Draws a standard Gaussian matrix, takes its QR decomposition, and fixes the signs so the result is drawn from the Haar (uniform) distribution over orthogonal matrices.

This topology is pre-scaled: an orthogonal matrix already has all its singular values equal to gain (norm-preserving for gain=1), which is the structural property it exists to provide. Because the layer-level spectral-radius rescale would collapse every singular value to the target radius and destroy that property, it is suppressed for this topology. A layer spectral_radius passed alongside this topology is therefore ignored (with a warning); control the scale through gain instead. The singular-values-equal-1 / norm-preservation property holds only in this regime (no outer rescale applied).

PARAMETER DESCRIPTION
n

Matrix size (number of reservoir units).

TYPE: int

gain

Scaling factor applied to the orthogonal matrix. With gain=1 all singular values are 1 (norm-preserving). Since the layer-level spectral_radius rescale is suppressed (pre-scaled topology), gain is the only knob on the overall scale.

TYPE: float DEFAULT: 1.0

seed

Seed for the Gaussian draw. None uses the global torch RNG.

TYPE: int DEFAULT: None

RETURNS DESCRIPTION
Tensor

Orthogonal matrix of shape (n, n) (times gain).

Examples:

>>> from resdag.layers import ESNLayer
>>> reservoir = ESNLayer(500, feedback_size=3, topology="orthogonal")
>>> reservoir = ESNLayer(
...     500, feedback_size=3, topology=("orthogonal", {"seed": 42})
... )
Source code in src/resdag/init/matrices/orthogonal.py
@register_matrix_topology("orthogonal", prescaled=True)
def orthogonal_matrix(n: int, gain: float = 1.0, seed: int | None = None) -> torch.Tensor:
    """Build a random orthogonal matrix via QR decomposition.

    Draws a standard Gaussian matrix, takes its QR decomposition, and fixes
    the signs so the result is drawn from the Haar (uniform) distribution
    over orthogonal matrices.

    This topology is **pre-scaled**: an orthogonal matrix already has all its
    singular values equal to ``gain`` (norm-preserving for ``gain=1``), which is
    the structural property it exists to provide. Because the layer-level
    spectral-radius rescale would collapse every singular value to the target
    radius and destroy that property, it is suppressed for this topology. A
    layer ``spectral_radius`` passed alongside this topology is therefore
    **ignored** (with a warning); control the scale through ``gain`` instead. The
    singular-values-equal-1 / norm-preservation property holds only in this
    regime (no outer rescale applied).

    Parameters
    ----------
    n : int
        Matrix size (number of reservoir units).
    gain : float, default=1.0
        Scaling factor applied to the orthogonal matrix. With ``gain=1`` all
        singular values are 1 (norm-preserving). Since the layer-level
        ``spectral_radius`` rescale is suppressed (pre-scaled topology),
        ``gain`` is the only knob on the overall scale.
    seed : int, optional
        Seed for the Gaussian draw. ``None`` uses the global torch RNG.

    Returns
    -------
    torch.Tensor
        Orthogonal matrix of shape ``(n, n)`` (times ``gain``).

    Examples
    --------
    >>> from resdag.layers import ESNLayer
    >>> reservoir = ESNLayer(500, feedback_size=3, topology="orthogonal")
    >>> reservoir = ESNLayer(
    ...     500, feedback_size=3, topology=("orthogonal", {"seed": 42})
    ... )
    """
    generator = None
    if seed is not None:
        generator = torch.Generator()
        generator.manual_seed(seed)

    gaussian = torch.randn(n, n, generator=generator, dtype=torch.float64)
    q, r = torch.linalg.qr(gaussian)
    # Sign correction: make the decomposition unique so Q is Haar-distributed.
    q = q * torch.sign(torch.diagonal(r))

    return cast(torch.Tensor, (gain * q).to(torch.float32))

utils

Initialization utility functions.

TopologySpec module-attribute

TopologySpec = None | str | Callable | tuple[str | Callable, dict[str, Any]] | TopologyInitializer

InitializerSpec module-attribute

InitializerSpec = None | str | Callable | tuple[str | Callable, dict[str, Any]] | InputFeedbackInitializer

resolve_topology

resolve_topology(spec: TopologySpec, seed: SeedLike = None) -> TopologyInitializer | None

Resolve a topology specification to a TopologyInitializer object.

Accepts six formats:

  • None — returns None (use default random initialization)
  • str — registry name, uses registered default parameters
  • tuple[str, dict] — registry name with parameter overrides
  • callable — any matrix builder fn(n, **kw) -> matrix | graph or in-place fn(tensor, **kw); wrapped in :class:MatrixTopology
  • tuple[callable, dict] — matrix builder with bound parameters
  • TopologyInitializer — already resolved, returned as-is
PARAMETER DESCRIPTION
spec

Topology specification in one of the accepted formats.

TYPE: TopologySpec

seed

Seed forwarded to the underlying builder for reproducibility. It is applied only to the str, tuple[str, dict], callable, and tuple[callable, dict] forms, only when the builder accepts a seed argument, and only when the spec did not already pin one (an explicit spec seed always wins). A :class:torch.Generator is reduced to its initial_seed() before use. Pre-resolved TopologyInitializer objects are returned untouched.

TYPE: int or Generator DEFAULT: None

RETURNS DESCRIPTION
TopologyInitializer or None

Resolved topology object, or None if spec was None.

RAISES DESCRIPTION
TypeError

If spec is not one of the accepted types.

Examples:

>>> resolve_topology("erdos_renyi")
GraphTopology(...)
>>> resolve_topology("erdos_renyi", seed=42)
GraphTopology(...)
>>> resolve_topology(("watts_strogatz", {"k": 6, "p": 0.1}))
GraphTopology(...)
>>> resolve_topology(my_matrix_fn)
MatrixTopology(...)
>>> resolve_topology((my_matrix_fn, {"blocks": 4}))
MatrixTopology(...)
>>> resolve_topology(get_topology("ring_chord"))
GraphTopology(...)
Source code in src/resdag/init/utils/resolve.py
def resolve_topology(
    spec: TopologySpec,
    seed: SeedLike = None,
) -> TopologyInitializer | None:
    """Resolve a topology specification to a TopologyInitializer object.

    Accepts six formats:

    - ``None`` — returns None (use default random initialization)
    - ``str`` — registry name, uses registered default parameters
    - ``tuple[str, dict]`` — registry name with parameter overrides
    - ``callable`` — any matrix builder ``fn(n, **kw) -> matrix | graph`` or
      in-place ``fn(tensor, **kw)``; wrapped in :class:`MatrixTopology`
    - ``tuple[callable, dict]`` — matrix builder with bound parameters
    - ``TopologyInitializer`` — already resolved, returned as-is

    Parameters
    ----------
    spec : TopologySpec
        Topology specification in one of the accepted formats.
    seed : int or torch.Generator, optional
        Seed forwarded to the underlying builder for reproducibility. It is
        applied only to the ``str``, ``tuple[str, dict]``, ``callable``, and
        ``tuple[callable, dict]`` forms, only when the builder accepts a
        ``seed`` argument, and only when the spec did not already pin one (an
        explicit spec seed always wins). A :class:`torch.Generator` is reduced
        to its ``initial_seed()`` before use. Pre-resolved
        ``TopologyInitializer`` objects are returned untouched.

    Returns
    -------
    TopologyInitializer or None
        Resolved topology object, or None if spec was None.

    Raises
    ------
    TypeError
        If spec is not one of the accepted types.

    Examples
    --------
    >>> resolve_topology("erdos_renyi")
    GraphTopology(...)

    >>> resolve_topology("erdos_renyi", seed=42)
    GraphTopology(...)

    >>> resolve_topology(("watts_strogatz", {"k": 6, "p": 0.1}))
    GraphTopology(...)

    >>> resolve_topology(my_matrix_fn)
    MatrixTopology(...)

    >>> resolve_topology((my_matrix_fn, {"blocks": 4}))
    MatrixTopology(...)

    >>> resolve_topology(get_topology("ring_chord"))
    GraphTopology(...)
    """
    seed = coerce_seed_to_int(seed)
    if spec is None:
        return None
    if isinstance(spec, TopologyInitializer):
        return spec
    if isinstance(spec, str):
        topology = get_topology(spec)
        builder = _topology_builder(topology)
        if builder is not None and _accepts_seed(builder) and seed is not None:
            return get_topology(spec, seed=seed)
        return topology
    if isinstance(spec, tuple):
        name, params = spec
        if callable(name):
            return MatrixTopology(
                name,
                _inject_seed(dict(params), name, seed),
                seed=_generator_seed(name, dict(params), seed),
            )
        builder = _topology_builder(get_topology(name))
        injected = (
            _inject_seed(dict(params), builder, seed) if builder is not None else dict(params)
        )
        return get_topology(name, **injected)
    if callable(spec):
        return MatrixTopology(
            spec, _inject_seed({}, spec, seed), seed=_generator_seed(spec, {}, seed)
        )
    raise TypeError(
        f"Invalid topology spec type: {type(spec).__name__}. "
        f"Expected str, callable, tuple[str | callable, dict], or TopologyInitializer."
    )

resolve_initializer

resolve_initializer(spec: InitializerSpec, seed: SeedLike = None) -> InputFeedbackInitializer | None

Resolve an initializer specification to an InputFeedbackInitializer object.

Accepts six formats:

  • None — returns None (use default random initialization)
  • str — registry name, uses registered default parameters
  • tuple[str, dict] — registry name with parameter overrides
  • callable — any matrix builder fn(rows, cols, **kw) -> matrix or in-place fn(tensor, **kw); wrapped in :class:FunctionInitializer
  • tuple[callable, dict] — matrix builder with bound parameters
  • InputFeedbackInitializer — already resolved, returned as-is
PARAMETER DESCRIPTION
spec

Initializer specification in one of the accepted formats.

TYPE: InitializerSpec

seed

Seed forwarded to the underlying initializer for reproducibility. It is applied only to the str, tuple[str, dict], callable, and tuple[callable, dict] forms, only when the initializer accepts a seed argument, and only when the spec did not already pin one (an explicit spec seed always wins). A :class:torch.Generator is reduced to its initial_seed() before use. Pre-resolved InputFeedbackInitializer objects are returned untouched.

TYPE: int or Generator DEFAULT: None

RETURNS DESCRIPTION
InputFeedbackInitializer or None

Resolved initializer object, or None if spec was None.

RAISES DESCRIPTION
TypeError

If spec is not one of the accepted types.

Examples:

>>> resolve_initializer("pseudo_diagonal")
PseudoDiagonalInitializer(...)
>>> resolve_initializer("random", seed=42)
RandomInputInitializer(...)
>>> resolve_initializer(("chebyshev", {"p": 0.5, "q": 3.0}))
ChebyshevInitializer(...)
>>> resolve_initializer(torch.nn.init.xavier_uniform_)
FunctionInitializer(...)
>>> resolve_initializer((my_matrix_fn, {"scale": 0.5}))
FunctionInitializer(...)
>>> resolve_initializer(get_input_feedback("random"))
RandomInitializer(...)
Source code in src/resdag/init/utils/resolve.py
def resolve_initializer(
    spec: InitializerSpec,
    seed: SeedLike = None,
) -> InputFeedbackInitializer | None:
    """Resolve an initializer specification to an InputFeedbackInitializer object.

    Accepts six formats:

    - ``None`` — returns None (use default random initialization)
    - ``str`` — registry name, uses registered default parameters
    - ``tuple[str, dict]`` — registry name with parameter overrides
    - ``callable`` — any matrix builder ``fn(rows, cols, **kw) -> matrix`` or
      in-place ``fn(tensor, **kw)``; wrapped in :class:`FunctionInitializer`
    - ``tuple[callable, dict]`` — matrix builder with bound parameters
    - ``InputFeedbackInitializer`` — already resolved, returned as-is

    Parameters
    ----------
    spec : InitializerSpec
        Initializer specification in one of the accepted formats.
    seed : int or torch.Generator, optional
        Seed forwarded to the underlying initializer for reproducibility. It
        is applied only to the ``str``, ``tuple[str, dict]``, ``callable``,
        and ``tuple[callable, dict]`` forms, only when the initializer accepts
        a ``seed`` argument, and only when the spec did not already pin one (an
        explicit spec seed always wins). A :class:`torch.Generator` is reduced
        to its ``initial_seed()`` before use. Pre-resolved
        ``InputFeedbackInitializer`` objects are returned untouched.

    Returns
    -------
    InputFeedbackInitializer or None
        Resolved initializer object, or None if spec was None.

    Raises
    ------
    TypeError
        If spec is not one of the accepted types.

    Examples
    --------
    >>> resolve_initializer("pseudo_diagonal")
    PseudoDiagonalInitializer(...)

    >>> resolve_initializer("random", seed=42)
    RandomInputInitializer(...)

    >>> resolve_initializer(("chebyshev", {"p": 0.5, "q": 3.0}))
    ChebyshevInitializer(...)

    >>> resolve_initializer(torch.nn.init.xavier_uniform_)
    FunctionInitializer(...)

    >>> resolve_initializer((my_matrix_fn, {"scale": 0.5}))
    FunctionInitializer(...)

    >>> resolve_initializer(get_input_feedback("random"))
    RandomInitializer(...)
    """
    seed = coerce_seed_to_int(seed)
    if spec is None:
        return None
    if isinstance(spec, InputFeedbackInitializer):
        return spec
    if isinstance(spec, str):
        initializer = get_input_feedback(spec)
        target = _initializer_seed_target(initializer)
        if _accepts_seed(target) and seed is not None:
            return get_input_feedback(spec, seed=seed)
        return initializer
    if isinstance(spec, tuple):
        name, params = spec
        if callable(name):
            return FunctionInitializer(name, **_inject_seed(dict(params), name, seed))
        target = _initializer_seed_target(get_input_feedback(name))
        injected = _inject_seed(dict(params), target, seed)
        return get_input_feedback(name, **injected)
    if callable(spec):
        return FunctionInitializer(spec, **_inject_seed({}, spec, seed))
    raise TypeError(
        f"Invalid initializer spec type: {type(spec).__name__}. "
        f"Expected str, callable, tuple[str | callable, dict], or InputFeedbackInitializer."
    )