Source code for devices.xylo.syns61201.xylo_graph_modules

"""
Xylo graph modules for use with tracing and mapping
"""

import warnings

from rockpool.graph import (
    GenericNeurons,
    GraphModule,
    LIFNeuronWithSynsRealValue,
    replace_module,
)

import numpy as np

from typing import List, Optional, Union
from rockpool.typehints import IntVector, FloatVector

from dataclasses import dataclass, field

__all__ = ["Xylo2Neurons", "Xylo2HiddenNeurons", "Xylo2OutputNeurons"]


@dataclass(eq=False, repr=False)
class Xylo2Neurons(GenericNeurons):
    """
    Base class for all Xylo graph module classes
    """

    hw_ids: Union[IntVector, FloatVector] = field(default_factory=list)
    """ IntVector: The HW neuron IDs allocated to this graph module ``(N,)``. Empty means than no HW IDs have been allocated."""

    threshold: Union[IntVector, FloatVector] = field(default_factory=list)
    """ IntVector: The threshold parameters for each neuron ``(N,)`` """

    bias: Union[IntVector, FloatVector] = field(default_factory=list)
    """ IntVector: The bias parameters for each neuron ``(N,)`` """

    dash_mem: Union[IntVector, FloatVector] = field(default_factory=list)
    """ IntVector: The membrane decay parameters for each neuron ``(N,)`` """

    dash_syn: Union[IntVector, FloatVector] = field(default_factory=list)
    """ IntVector: The synapse decay parameters for each neuron. Either ``(N,)`` if only one synapse is used per neuron, or ``(2N,)`` if two synapses are used for each neuron (i.e. syn2). In this case, elements ``dash_syn[0:1]`` refer to the synapses of neuron ``0``, and so on. """

    dt: Optional[float] = None
    """ float: The ``dt`` time step used for this neuron module """

    @classmethod
    def _convert_from(cls, mod: GraphModule) -> GraphModule:
        if isinstance(mod, cls):
            # - No need to do anything
            return mod

        elif isinstance(mod, LIFNeuronWithSynsRealValue):
            # - Convert from a real-valued LIF neuron
            # - Get a value for `dt` to use in the conversion
            if mod.dt is None:
                raise ValueError(
                    f"Graph module of type {type(mod).__name__} with name {mod.name} has no `dt` set, so cannot convert time constants when converting to {cls.__name__}."
                )

            # - Check values of time constants
            if np.any(np.array(mod.tau_mem) <= mod.dt) or np.any(
                np.array(mod.tau_syn) <= mod.dt
            ):
                raise ValueError(
                    f"Graph module of type {type(mod).__name__} with name {mod.name} has one or more time constants <= `dt`, which therefore cannot be converted to `dash` parameters."
                )

            # - Convert TCs to dash parameters
            dash_mem = np.log2(np.array(mod.tau_mem) / mod.dt).tolist()
            dash_syn = np.log2(np.array(mod.tau_syn) / mod.dt).flatten().tolist()

            # - Get thresholds
            thresholds = np.array(mod.threshold).tolist()

            # - Get biases
            bias = np.array(mod.bias).tolist()

            # - Build a new neurons module to insert into the graph
            neurons = cls._factory(
                len(mod.input_nodes),
                len(mod.output_nodes),
                mod.name,
                mod.computational_module,
                [],  # Empty list for HW IDs
                thresholds,
                bias,
                dash_mem,
                dash_syn,
                mod.dt,
            )

            # - Replace the target module and return
            replace_module(mod, neurons)
            return neurons

        elif isinstance(mod, GenericNeurons):
            # - Try to convert as a `GenericNeurons` base class
            if type(mod) != GenericNeurons:
                # - Warn if `mod` is actually some other derived class
                #   We might be missing an explicit conversion rule in this case
                warnings.warn(
                    f"Converting module {mod} as a GenericNeurons module to {cls.__name__} . No explicit conversion rule was found for class {type(mod).__name__}."
                )

            # - Make a new module
            neurons = cls._factory(
                len(mod.input_nodes),
                len(mod.output_nodes),
                mod.name,
            )

            # - Replace the target module
            replace_module(mod, neurons)

            # - Try to set attributes of the new module
            for attr in neurons.__dataclass_fields__.keys():
                if hasattr(mod, attr):
                    setattr(neurons, attr, getattr(mod, attr))

            return neurons

        else:
            raise ValueError(
                f"Graph module of type {type(mod).__name__} cannot be converted to a {cls.__name__}"
            )


[docs]@dataclass(eq=False, repr=False) class Xylo2HiddenNeurons(Xylo2Neurons): """ A :py:class:`.graph.GraphModule` encapsulating Xylo v2 hidden neurons """ def __post_init__(self, *args, **kwargs): if len(self.input_nodes) != len(self.output_nodes): if len(self.input_nodes) != 2 * len(self.output_nodes): raise ValueError( "Number of input nodes must be 1* or 2* number of output nodes" ) super().__post_init__(self, *args, **kwargs)
[docs]@dataclass(eq=False, repr=False) class Xylo2OutputNeurons(Xylo2Neurons): """ A :py:class:`.graph.GraphModule` encapsulating Xylo V1 output neurons """ def __post_init__(self, *args, **kwargs): if len(self.input_nodes) != len(self.output_nodes): raise ValueError( "Number of input nodes must be equal to number of output nodes" ) super().__post_init__(self, *args, **kwargs)