Exemple #1
0
class Solver(with_metaclass(DocstringInheritor, FrozenObject)):
    """Decoder or weight solver."""

    weights = BoolParam('weights')

    def __init__(self, weights=False):
        super(Solver, self).__init__()
        self.weights = weights

    def __call__(self, A, Y, rng=None, E=None):
        """Call the solver.

        Parameters
        ----------
        A : (n_eval_points, n_neurons) array_like
            Matrix of the neurons' activities at the evaluation points
        Y : (n_eval_points, dimensions) array_like
            Matrix of the target decoded values for each of the D dimensions,
            at each of the evaluation points.
        rng : `numpy.random.RandomState`, optional (Default: None)
            A random number generator to use as required. If None,
            the ``numpy.random`` module functions will be used.
        E : (dimensions, post.n_neurons) array_like, optional (Default: None)
            Array of post-population encoders. Providing this tells the solver
            to return an array of connection weights rather than decoders.

        Returns
        -------
        X :  (n_neurons, dimensions) or (n_neurons, post.n_neurons) ndarray
            (n_neurons, dimensions) array of decoders (if ``solver.weights``
            is False) or (n_neurons, post.n_neurons) array of weights
            (if ``'solver.weights`` is True).
        info : dict
            A dictionary of information about the solver. All dictionaries have
            an ``'rmses'`` key that contains RMS errors of the solve.
            Other keys are unique to particular solvers.
        """
        raise NotImplementedError("Solvers must implement '__call__'")

    def mul_encoders(self, Y, E, copy=False):
        """Helper function that projects signal ``Y`` onto encoders ``E``.

        Parameters
        ----------
        Y : ndarray
            The signal of interest.
        E : (dimensions, n_neurons) array_like or None
            Array of encoders. If None, ``Y`` will be returned unchanged.
        copy : bool, optional (Default: False)
            Whether a copy of ``Y`` should be returned if ``E`` is None.
        """
        if self.weights and E is None:
            raise ValidationError(
                "Encoders must be provided for weight solver", attr='E')
        if not self.weights and E is not None:
            raise ValidationError(
                "Encoders must be 'None' for decoder solver", attr='E')

        return np.dot(Y, E) if E is not None else Y.copy() if copy else Y
Exemple #2
0
class ObjectProxy(with_metaclass(ObjectProxyMeta)):
    """A transparent object proxy for creating decorator descriptors.

    This is used in lieu of ``functools.update_wrapper``, which copies
    a number of properties of the wrapped function in the wrapper.
    Copying properties can be expensive though, so this is used instead
    to make the wrapper act like the wrapped function in all cases
    except ``__call__``.
    """

    __slots__ = '__wrapped__'

    def __init__(self, wrapped):
        object.__setattr__(self, '__wrapped__', wrapped)

        # Python 3 has the __qualname__ attribute, but it does not
        # allow it to be overridden using a property and it must instead
        # be an actual string object instead.
        try:
            object.__setattr__(self, '__qualname__', wrapped.__qualname__)
        except AttributeError:
            pass

    @property
    def __annotations__(self):
        return self.__wrapped__.__anotations__

    @property
    def __name__(self):
        return self.__wrapped__.__name__

    @property
    def __class__(self):
        return self.__wrapped__.__class__

    def __dir__(self):
        return dir(self.__wrapped__)

    def __getattr__(self, key):
        return getattr(self.__wrapped__, key)

    def __hash__(self):
        return hash(self.__wrapped__)

    def __setattr__(self, key, value):
        setattr(self.__wrapped__, key, value)

    def __str__(self):
        return str(self.__wrapped__)

    def __repr__(self):
        return '<%s at 0x%x for %s at 0x%x>' % (
            type(self).__name__, id(self),
            type(self.__wrapped__).__name__,
            id(self.__wrapped__))

    def __unicode__(self):
        return unicode(self.__wrapped__)
Exemple #3
0
class NengoObject(with_metaclass(NetworkMember)):
    """A base class for Nengo objects.

    This defines some functions that the Network requires
    for correct operation. In particular, list membership
    and object comparison require each object to have a unique ID.
    """
    def _str(self, include_id):
        return "<%s%s%s>" % (
            self.__class__.__name__, "" if not hasattr(self, 'label') else
            " (unlabeled)" if self.label is None else ' "%s"' % self.label,
            " at 0x%x" % id(self) if include_id else "")

    def __str__(self):
        return self._str(
            include_id=not hasattr(self, 'label') or self.label is None)

    def __repr__(self):
        return self._str(include_id=True)

    def __setattr__(self, name, val):
        if hasattr(self, '_initialized') and not hasattr(self, name):
            warnings.warn(
                "Creating new attribute '%s' on '%s'. "
                "Did you mean to change an existing attribute?" % (name, self),
                SyntaxWarning)
        if val is Default:
            val = Config.default(type(self), name)
        try:
            super(NengoObject, self).__setattr__(name, val)
        except Exception as e:
            arg0 = '' if len(e.args) == 0 else e.args[0]
            arg0 = ("Validation error when setting '%s.%s': %s" %
                    (self.__class__.__name__, name, arg0))
            e.args = (arg0, ) + e.args[1:]
            raise

    def __getstate__(self):
        raise NotImplementedError("Nengo objects do not support pickling")

    def __setstate__(self, state):
        raise NotImplementedError("Nengo objects do not support pickling")

    @classmethod
    def param_list(cls):
        """Returns a list of parameter names that can be set."""
        return (attr for attr in dir(cls) if is_param(getattr(cls, attr)))

    @property
    def params(self):
        """Returns a list of parameter names that can be set."""
        return self.param_list()
Exemple #4
0
class Solver(with_metaclass(DocstringInheritor, FrozenObject)):
    """Decoder or weight solver.

    A solver can be compositional or non-compositional. Non-compositional
    solvers must operate on the whole neuron-to-neuron weight matrix, while
    compositional solvers operate in the decoded state space, which is then
    combined with transform/encoders to generate the full weight matrix.
    See the solver's ``compositional`` class attribute to determine if it is
    compositional.
    """

    compositional = True

    weights = BoolParam('weights')

    def __init__(self, weights=False):
        super(Solver, self).__init__()
        self.weights = weights

    def __call__(self, A, Y, rng=np.random):
        """Call the solver.

        Parameters
        ----------
        A : (n_eval_points, n_neurons) array_like
            Matrix of the neurons' activities at the evaluation points
        Y : (n_eval_points, dimensions) array_like
            Matrix of the target decoded values for each of the D dimensions,
            at each of the evaluation points.
        rng : `numpy.random.RandomState`, optional (Default: ``numpy.random``)
            A random number generator to use as required.

        Returns
        -------
        X : (n_neurons, dimensions) or (n_neurons, post.n_neurons) ndarray
            (n_neurons, dimensions) array of decoders (if ``solver.weights``
            is False) or (n_neurons, post.n_neurons) array of weights
            (if ``'solver.weights`` is True).
        info : dict
            A dictionary of information about the solver. All dictionaries have
            an ``'rmses'`` key that contains RMS errors of the solve.
            Other keys are unique to particular solvers.
        """
        raise NotImplementedError("Solvers must implement '__call__'")
Exemple #5
0
class NengoObject(with_metaclass(NetworkMember)):
    """A base class for Nengo objects.

    This defines some functions that the Network requires
    for correct operation. In particular, list membership
    and object comparison require each object to have a unique ID.
    """
    def __str__(self):
        if hasattr(self, 'label') and self.label is not None:
            return "%s: %s" % (self.__class__.__name__, self.label)
        else:
            return "%s: id=%d" % (self.__class__.__name__, id(self))

    def __repr__(self):
        return str(self)

    def __setattr__(self, name, val):
        if hasattr(self, '_initialized') and not hasattr(self, name):
            warnings.warn(
                "Creating new attribute '%s' on '%s'. "
                "Did you mean to change an existing attribute?" % (name, self),
                SyntaxWarning)
        if val is Default:
            val = Config.default(type(self), name)
        try:
            super(NengoObject, self).__setattr__(name, val)
        except Exception as e:
            arg0 = '' if len(e.args) == 0 else e.args[0]
            arg0 = ("Validation error when setting '%s.%s': %s" %
                    (self.__class__.__name__, name, arg0))
            e.args = (arg0, ) + e.args[1:]
            raise

    @classmethod
    def param_list(cls):
        """Returns a list of parameter names that can be set."""
        return (attr for attr in dir(cls) if is_param(getattr(cls, attr)))

    @property
    def params(self):
        """Returns a list of parameter names that can be set."""
        return self.param_list()
Exemple #6
0
class NengoObject(with_metaclass(NetworkMember)):
    """A base class for Nengo objects.

    This defines some functions that the Network requires
    for correct operation. In particular, list membership
    and object comparison require each object to have a unique ID.
    """

    def __hash__(self):
        if self._key is None:
            return super(NengoObject, self).__hash__()
        return hash((self.__class__, self._key))

    def __eq__(self, other):
        return hash(self) == hash(other)

    def __str__(self):
        if hasattr(self, 'label') and self.label is not None:
            return "%s: %s" % (self.__class__.__name__, self.label)
        else:
            return "%s: key=%d" % (self.__class__.__name__, self._key)

    def __repr__(self):
        return str(self)
Exemple #7
0
class Solver(with_metaclass(DocstringInheritor)):
    """
    Decoder or weight solver.
    """
    def __call__(self, A, Y, rng=None, E=None):
        """Call the solver.

        Parameters
        ----------
        A : array_like (M, N)
            Matrix of the N neurons' activities at the M evaluation points
        Y : array_like (M, D)
            Matrix of the target decoded values for each of the D dimensions,
            at each of the M evaluation points.
        rng : numpy.RandomState, optional
            A random number generator to use as required. If none is provided,
            numpy.random will be used.
        E : array_like (D, N2), optional
            Array of post-population encoders. Providing this tells the solver
            to return an array of connection weights rather than decoders.

        Returns
        -------
        X : np.ndarray (N, D) or (N, N2)
            (N, D) array of decoders (if solver.weights == False) or
            (N, N2) array of weights (if solver.weights == True).
        info : dict
            A dictionary of information about the solve. All dictionaries have
            an 'rmses' key that contains RMS errors of the solve. Other keys
            are unique to particular solvers.
        """
        raise NotImplementedError("Solvers must implement '__call__'")

    def mul_encoders(self, Y, E):
        if self.weights:
            if E is None:
                raise ValueError("Encoders must be provided for weight solver")
            return np.dot(Y, E)
        else:
            if E is not None:
                raise ValueError("Encoders must be 'None' for decoder solver")
            return Y

    def __hash__(self):
        items = list(self.__dict__.items())
        items.sort(key=lambda item: item[0])

        hashes = []
        for k, v in items:
            if isinstance(v, np.ndarray):
                if v.size < 1e5:
                    a = v[:]
                    a.setflags(write=False)
                    hashes.append(hash(a))
                else:
                    raise ValueError("array is too large to hash")
            elif isinstance(v, collections.Iterable):
                hashes.append(hash(tuple(v)))
            elif isinstance(v, collections.Callable):
                hashes.append(hash(v.__code__))
            else:
                hashes.append(hash(v))

        return hash(tuple(hashes))

    def __str__(self):
        return "%s(%s)" % (self.__class__.__name__, ', '.join(
            "%s=%s" % (k, v) for k, v in iteritems(self.__dict__)))
Exemple #8
0
class NengoObject(with_metaclass(NetworkMember)):
    """A base class for Nengo objects.

    Parameters
    ----------
    label : string
        A descriptive label for the object.
    seed : int
        The seed used for random number generation.

    Attributes
    ----------
    label : string
        A descriptive label for the object.
    seed : int
        The seed used for random number generation.
    """

    label = StringParam('label', default=None, optional=True)
    seed = IntParam('seed', default=None, optional=True)

    def __init__(self, label, seed):
        self.label = label
        self.seed = seed

    def __getstate__(self):
        raise NotImplementedError("Nengo objects do not support pickling")

    def __setstate__(self, state):
        raise NotImplementedError("Nengo objects do not support pickling")

    def __setattr__(self, name, val):
        if hasattr(self, '_initialized') and not hasattr(self, name):
            warnings.warn(
                "Creating new attribute '%s' on '%s'. "
                "Did you mean to change an existing attribute?" % (name, self),
                SyntaxWarning)
        if val is Default:
            val = Config.default(type(self), name)

        if rc.getboolean('exceptions', 'simplified'):
            try:
                super(NengoObject, self).__setattr__(name, val)
            except ValidationError:
                exc_info = sys.exc_info()
                reraise(exc_info[0], exc_info[1], None)
        else:
            super(NengoObject, self).__setattr__(name, val)

    def __str__(self):
        return self._str(
            include_id=not hasattr(self, 'label') or self.label is None)

    def __repr__(self):
        return self._str(include_id=True)

    def _str(self, include_id):
        return "<%s%s%s>" % (
            self.__class__.__name__, "" if not hasattr(self, 'label') else
            " (unlabeled)" if self.label is None else ' "%s"' % self.label,
            " at 0x%x" % id(self) if include_id else "")

    @classmethod
    def param_list(cls):
        """Returns a list of parameter names that can be set."""
        return (attr for attr in dir(cls) if is_param(getattr(cls, attr)))

    @property
    def params(self):
        """Returns a list of parameter names that can be set."""
        return self.param_list()
Exemple #9
0
class Solver(with_metaclass(DocstringInheritor)):
    """Decoder or weight solver."""
    def __call__(self, A, Y, rng=None, E=None):
        """Call the solver.

        Parameters
        ----------
        A : (n_eval_points, n_neurons) array_like
            Matrix of the neurons' activities at the evaluation points
        Y : (n_eval_points, dimensions) array_like
            Matrix of the target decoded values for each of the D dimensions,
            at each of the evaluation points.
        rng : `numpy.random.RandomState`, optional (Default: None)
            A random number generator to use as required. If None,
            the ``numpy.random`` module functions will be used.
        E : (dimensions, post.n_neurons) array_like, optional (Default: None)
            Array of post-population encoders. Providing this tells the solver
            to return an array of connection weights rather than decoders.

        Returns
        -------
        X :  (n_neurons, dimensions) or (n_neurons, post.n_neurons) ndarray
            (n_neurons, dimensions) array of decoders (if ``solver.weights``
            is False) or (n_neurons, post.n_neurons) array of weights
            (if ``'solver.weights`` is True).
        info : dict
            A dictionary of information about the solver. All dictionaries have
            an ``'rmses'`` key that contains RMS errors of the solve.
            Other keys are unique to particular solvers.
        """
        raise NotImplementedError("Solvers must implement '__call__'")

    def __hash__(self):
        items = list(self.__dict__.items())
        items.sort(key=lambda item: item[0])

        hashes = []
        for k, v in items:
            if isinstance(v, np.ndarray):
                if v.size < 1e5:
                    a = v[:]
                    a.setflags(write=False)
                    hashes.append(hash(a))
                else:
                    raise ValidationError("array is too large to hash", attr=k)
            elif isinstance(v, collections.Iterable):
                hashes.append(hash(tuple(v)))
            elif isinstance(v, collections.Callable):
                hashes.append(hash(v.__code__))
            else:
                hashes.append(hash(v))

        return hash(tuple(hashes))

    def __str__(self):
        return "%s(%s)" % (self.__class__.__name__, ', '.join(
            "%s=%s" % (k, v) for k, v in iteritems(self.__dict__)))

    def mul_encoders(self, Y, E, copy=False):
        """Helper function that projects signal ``Y`` onto encoders ``E``.

        Parameters
        ----------
        Y : ndarray
            The signal of interest.
        E : (dimensions, n_neurons) array_like or None
            Array of encoders. If None, ``Y`` will be returned unchanged.
        copy : bool, optional (Default: False)
            Whether a copy of ``Y`` should be returned if ``E`` is None.
        """
        if self.weights:
            if E is None:
                raise ValidationError(
                    "Encoders must be provided for weight solver", attr='E')
            return np.dot(Y, E)
        else:
            if E is not None:
                raise ValidationError(
                    "Encoders must be 'None' for decoder solver", attr='E')
            return Y.copy() if copy else Y
Exemple #10
0
class Network(with_metaclass(NengoObjectContainer)):
    """A network contains ensembles, nodes, connections, and other networks.

    A network is primarily used for grouping together related
    objects and connections for visualization purposes.
    However, you can also use networks as a nice way to reuse
    network creation code.

    To group together related objects that you do not need to reuse,
    you can create a new ``Network`` and add objects in a ``with`` block.
    For example::

        network = nengo.Network()
        with network:
            with nengo.Network(label="Vision"):
                v1 = nengo.Ensemble(nengo.LIF(100), dimensions=2)
            with nengo.Network(label="Motor"):
                sma = nengo.Ensemble(nengo.LIF(100), dimensions=2)
            nengo.Connection(v1, sma)

    To reuse a group of related objects, you can create a new subclass
    of ``Network``, and add objects in the ``__init__`` method.
    For example::

        class OcularDominance(nengo.Network):
            def __init__(self):
                self.column = nengo.Ensemble(nengo.LIF(100), dimensions=2)
        network = nengo.Network()
        with network:
            left_eye = OcularDominance()
            right_eye = OcularDominance()
            nengo.Connection(left_eye.column, right_eye.column)

    For more information and advanced usage, please see the Nengo
    documentation at http://nengo.readthedocs.org/.

    Parameters
    ----------
    label : str, optional
        Name of the model. Defaults to None.
    seed : int, optional
        Random number seed that will be fed to the random number generator.
        Setting this seed makes the creation of the model
        a deterministic process; however, each new ensemble
        in the network advances the random number generator,
        so if the network creation code changes, the entire model changes.
    add_to_container : bool, optional
        Determines if this Network will be added to the current container.
        Defaults to true iff currently with a Network.

    Attributes
    ----------
    label : str
        Name of the Network.
    seed : int
        Random seed used by the Network.
    ensembles : list
        List of nengo.Ensemble objects in this Network.
    nodes : list
        List of nengo.Node objects in this Network.
    connections : list
        List of nengo.Connection objects in this Network.
    networks : list
        List of nengo.BaseNetwork objects in this Network.
    """
    def __new__(cls, *args, **kwargs):
        inst = super(Network, cls).__new__(cls)
        inst._config = cls.default_config()
        inst.objects = {
            Ensemble: [],
            Node: [],
            Connection: [],
            Network: [],
            Probe: [],
        }
        inst.ensembles = inst.objects[Ensemble]
        inst.nodes = inst.objects[Node]
        inst.connections = inst.objects[Connection]
        inst.networks = inst.objects[Network]
        inst.probes = inst.objects[Probe]
        return inst

    context = collections.deque(maxlen=100)  # static stack of Network objects

    @classmethod
    def add(cls, obj):
        """Add the passed object to the current Network.context."""
        if len(cls.context) == 0:
            raise RuntimeError("'%s' must either be created "
                               "inside a `with network:` block, or set "
                               "add_to_container=False in the object's "
                               "constructor." % obj)
        network = cls.context[-1]
        if not isinstance(network, Network):
            raise RuntimeError("Current context is not a network: %s" %
                               network)
        for cls in obj.__class__.__mro__:
            if cls in network.objects:
                network.objects[cls].append(obj)
                break
        else:
            raise TypeError("Objects of type '%s' cannot be added to "
                            "networks." % obj.__class__.__name__)

    @staticmethod
    def default_config():
        config = Config()
        config.configures(Connection)
        config.configures(Ensemble)
        config.configures(Network)
        config.configures(Node)
        config.configures(Probe)
        return config

    def _all_objects(self, object_type):
        """Returns a list of all objects of the specified type"""
        # Make a copy of this network's list
        objects = list(self.objects[object_type])
        for subnet in self.networks:
            objects.extend(subnet._all_objects(object_type))
        return objects

    @property
    def all_objects(self):
        """All objects in this network and its subnetworks"""
        objects = []
        for object_type in self.objects.keys():
            objects.extend(self._all_objects(object_type))
        return objects

    @property
    def all_ensembles(self):
        """All ensembles in this network and its subnetworks"""
        return self._all_objects(Ensemble)

    @property
    def all_nodes(self):
        """All nodes in this network and its subnetworks"""
        return self._all_objects(Node)

    @property
    def all_networks(self):
        """All networks in this network and its subnetworks"""
        return self._all_objects(Network)

    @property
    def all_connections(self):
        """All connections in this network and its subnetworks"""
        return self._all_objects(Connection)

    @property
    def all_probes(self):
        """All probes in this network and its subnetworks"""
        return self._all_objects(Probe)

    @property
    def config(self):
        return self._config

    @config.setter
    def config(self, dummy):
        raise AttributeError("config cannot be overwritten. See help("
                             "nengo.Config) for help on modifying configs.")

    def __enter__(self):
        Network.context.append(self)
        self._config.__enter__()
        return self

    def __exit__(self, dummy_exc_type, dummy_exc_value, dummy_tb):
        if len(Network.context) == 0:
            raise RuntimeError("Network.context in bad state; was empty when "
                               "exiting from a 'with' block.")

        config = Config.context[-1]
        if config is not self._config:
            raise RuntimeError("Config.context in bad state; was expecting "
                               "current context to be '%s' but instead got "
                               "'%s'." % (self._config, config))

        network = Network.context.pop()

        if network is not self:
            raise RuntimeError("Network.context in bad state; was expecting "
                               "current context to be '%s' but instead got "
                               "'%s'." % (self, network))
        self._config.__exit__(dummy_exc_type, dummy_exc_value, dummy_tb)

    def __str__(self):
        return "%s: %s" % (self.__class__.__name__, self.label
                           if self.label is not None else str(id(self)))

    def __repr__(self):
        return str(self)
Exemple #11
0
class NengoObject(with_metaclass(NetworkMember, SupportDefaultsMixin)):
    """A base class for Nengo objects.

    Parameters
    ----------
    label : string
        A descriptive label for the object.
    seed : int
        The seed used for random number generation.

    Attributes
    ----------
    label : string
        A descriptive label for the object.
    seed : int
        The seed used for random number generation.
    """

    # Order in which parameters have to be initialized.
    # Missing parameters will be initialized last in an undefined order.
    # This is needed for pickling and copying of Nengo objects when the
    # parameter initialization order matters.
    _param_init_order = []

    label = StringParam('label', default=None, optional=True)
    seed = IntParam('seed', default=None, optional=True)

    def __init__(self, label, seed):
        super(NengoObject, self).__init__()
        self.label = label
        self.seed = seed

    def __getstate__(self):
        state = self.__dict__.copy()
        del state['_initialized']

        for attr in self.params:
            param = getattr(type(self), attr)
            if self in param:
                state[attr] = getattr(self, attr)

        return state

    def __setstate__(self, state):
        for attr in self._param_init_order:
            setattr(self, attr, state.pop(attr))

        for attr in self.params:
            if attr in state:
                setattr(self, attr, state.pop(attr))

        for k, v in iteritems(state):
            setattr(self, k, v)

        self._initialized = True
        if len(nengo.Network.context) > 0:
            warnings.warn(NotAddedToNetworkWarning(self))

    def __setattr__(self, name, val):
        if hasattr(self, '_initialized') and not hasattr(self, name):
            warnings.warn(
                "Creating new attribute '%s' on '%s'. "
                "Did you mean to change an existing attribute?" % (name, self),
                SyntaxWarning)
        super(NengoObject, self).__setattr__(name, val)

    def __str__(self):
        return self._str(
            include_id=not hasattr(self, 'label') or self.label is None)

    def __repr__(self):
        return self._str(include_id=True)

    def _str(self, include_id):
        return "<%s%s%s>" % (
            type(self).__name__, "" if not hasattr(self, 'label') else
            " (unlabeled)" if self.label is None else ' "%s"' % self.label,
            " at 0x%x" % id(self) if include_id else "")

    @property
    def params(self):
        """Returns a list of parameter names that can be set."""
        return list(iter_params(self))

    def copy(self, add_to_container=True):
        with warnings.catch_warnings():
            # We warn when copying since we can't change add_to_container.
            # However, we deal with it here, so we ignore the warning.
            warnings.simplefilter('ignore', category=NotAddedToNetworkWarning)
            c = copy(self)
        if add_to_container:
            nengo.Network.add(c)
        return c
Exemple #12
0
class LinearSystem(with_metaclass(LinearSystemType, NengoLinearFilterMixin)):
    """Generic linear system representation.

    This extends :class:`nengo.LinearFilter` to unify a variety of
    representations (`transfer function`, `state-space`, `zero-pole gain`)
    in continuous (i.e., analog) and discrete (i.e., digital) time-domains.
    Instances provide access to a number of common attributes and methods
    that are core to a variety of routines and networks throughout nengolib.

    This can be used anywhere a :class:`nengo.synapses.Synapse` (or
    :class:`nengo.LinearFilter`) object is expected within Nengo. For
    instance, this can be passed as a ``synapse`` parameter to
    :class:`nengo.Connection`.
    If the system is analog, then it will be automatically discretized using
    the simulation time-step (see :func:`.cont2discrete`). We advocate for
    using this class to represent and manipulate linear systems whenever
    possible (e.g., to create synapse objects, and to specify dynamical
    systems, whenever modelling within the NEF).

    The objects :attr:`.s` and :attr:`.z` are instances of
    :class:`.LinearSystem` that form the basic building blocks for analog or
    digital systems respectively (see examples). These objects respect the
    usual interpretation of differentiation and time-shifting in the Laplace--
    and ``z``--domains respectively.

    Parameters
    ----------
    sys : :data:`linear_system_like`
       Linear system representation.
    analog : ``boolean``, optional
       Continuous or discrete time-domain. Defaults to ``sys.analog`` if
       ``isinstance(sys,`` :class:`nengo.LinearFilter`), otherwise ``True``.
       If specified, it must not contradict ``sys.analog``.

    See Also
    --------
    :attr:`.s`
    :attr:`.z`
    :class:`.LinearNetwork`
    :func:`.ss2sim`
    :mod:`.synapses`

    Notes
    -----
    Instances of this class are intended to be **immutable**.

    Currently, support is focused primarily on SISO systems.
    There is some limited support for SIMO, MISO, and MIMO systems within
    state-space representations, but the functionality of such systems is
    currently experimental / limited as they must remain in state-space form.

    State-space representations must be causal (proper) and finite.
    Transfer functions must also be finite (Padé approximants may help here)
    but may be acausal (not necessarily proper) and must remain SISO.

    Conversions between representations are cached within the object itself.
    Redundantly casting a :class:`.LinearSystem` to itself returns the same
    underlying object, in order to persist this cache whenever possible.
    This is done `not` as a performance measure, but to guard against
    numerical issues that would result from accidentally converting back and
    forth between the same formats.

    Examples
    --------
    A simple continuous-time integrator:

    >>> from nengolib.signal import s
    >>> integrator = 1/s
    >>> assert integrator == ~s == s**(-1)
    >>> t = integrator.trange(2.)
    >>> step = np.ones_like(t)
    >>> cosine = np.cos(t)

    >>> import matplotlib.pyplot as plt
    >>> plt.subplot(211)
    >>> plt.title("Integrating a Step Function")
    >>> plt.plot(t, step, label="Step Input")
    >>> plt.plot(t, integrator.filt(step), label="Ramping Output")
    >>> plt.legend(loc='lower center')
    >>> plt.subplot(212)
    >>> plt.title("Integrating a Cosine Wave")
    >>> plt.plot(t, cosine, label="Cosine Input")
    >>> plt.plot(t, integrator.filt(cosine), label="Sine Output")
    >>> plt.xlabel("Time (s)")
    >>> plt.legend(loc='lower center')
    >>> plt.show()

    Building up higher-order continuous systems:

    >>> sys1 = 1000/(s**2 + 2*s + 1000)   # Bandpass filtering
    >>> sys2 = 500/(s**2 + s + 500)       # Bandpass filtering
    >>> sys3 = .5*sys1 + .5*sys2          # Mixture of two bandpass
    >>> assert len(sys1) == 2  # sys1.order_den
    >>> assert len(sys2) == 2  # sys2.order_den
    >>> assert len(sys3) == 4  # sys3.order_den

    >>> plt.subplot(311)
    >>> plt.title("sys1.impulse")
    >>> plt.plot(t, sys1.impulse(len(t)), label="sys1")
    >>> plt.subplot(312)
    >>> plt.title("sys2.impulse")
    >>> plt.plot(t, sys2.impulse(len(t)), label="sys2")
    >>> plt.subplot(313)
    >>> plt.title("sys3.impulse")
    >>> plt.plot(t, sys3.impulse(len(t)), label="sys3")
    >>> plt.xlabel("Time (s)")
    >>> plt.show()

    Plotting a linear transformation of the state-space from sys3.impulse:

    >>> from nengolib.signal import balance
    >>> plt.title("balance(sys3).X.impulse")
    >>> plt.plot(t, balance(sys3).X.impulse(len(t)))
    >>> plt.xlabel("Time (s)")
    >>> plt.show()

    A discrete trajectory:

    >>> from nengolib.signal import z
    >>> trajectory = 1 - .5/z + 2/z**3 + .5/z**4
    >>> t = np.arange(7)
    >>> y = trajectory.impulse(len(t))

    >>> plt.title("trajectory.impulse")
    >>> plt.step(t, y, where='post')
    >>> plt.fill_between(t, np.zeros_like(y), y, step='post', alpha=.3)
    >>> plt.xticks(t)
    >>> plt.xlabel("Step")
    >>> plt.show()
    """

    # Reuse the underlying system whenever it is an instance of the same
    # class. This allows us to avoid recomputing the tf/ss for the same
    # instance, i.e.
    #    sys1 = LinearSystem(...)
    #    tf1 = sys1.tf  # computes once
    #    sys2 = LinearSystem(sys1)
    #    assert sys1 is sys2  # reuses underlying instance
    #    tf2 = sys2.tf  # already been computed

    _tf = None
    _ss = None
    _zpk = None

    def __init__(self, sys, analog=None):
        assert not isinstance(sys, LinearSystem)  # guaranteed by metaclass
        assert analog is not None  # guaranteed by metaclass
        self._sys = sys
        self._analog = analog
        # HACK: Don't initialize superclass, so that it uses this num/den

    @property
    def analog(self):
        """Boolean indicating whether system is analog or digital."""
        return self._analog

    @property
    def tf(self):
        """Transfer function representation ``(num, den)``."""
        if self._tf is None:
            self._tf = sys2tf(self._sys)
        return self._tf

    @property
    def ss(self):
        """State-space representation ``(A, B, C, D)``."""
        if self._ss is None:
            self._ss = sys2ss(self._sys)
            # TODO: throw nicer error if system is acausal
        return self._ss

    @property
    def zpk(self):
        """Zero-pole gain representation ``(zeros, poles, gain)``."""
        if self._zpk is None:
            self._zpk = sys2zpk(self._sys)
        return self._zpk

    @property
    def is_tf(self):
        """Boolean indicating whether transfer function has been computed."""
        return _sys2form(self._sys) == _TF or self._tf is not None

    @property
    def is_ss(self):
        """Boolean indicating whether state-space has been computed."""
        return _sys2form(self._sys) == _SS or self._ss is not None

    @property
    def is_zpk(self):
        """Boolean indicating whether zero-pole gain has been computed."""
        return _sys2form(self._sys) == _ZPK or self._zpk is not None

    @property
    def size_in(self):
        """Input dimensionality (this equals 1 for SISO or SIMO)."""
        if self.is_ss:
            return self.B.shape[1]
        return 1

    @property
    def size_out(self):
        """Output dimensionality (this equals 1 for SISO or MISO)."""
        if self.is_ss:
            return self.C.shape[0]
        return 1

    @property
    def shape(self):
        """Short-hand for ``(.size_in, .size_out)``."""
        return (self.size_out, self.size_in)

    @property
    def is_SISO(self):
        """Boolean indicating whether system is SISO."""
        return self.shape == (1, 1)

    @property
    def A(self):
        """A matrix from state-space representation."""
        return self.ss[0]

    @property
    def B(self):
        """B matrix from state-space representation."""
        return self.ss[1]

    @property
    def C(self):
        """C matrix from state-space representation."""
        return self.ss[2]

    @property
    def D(self):
        """D matrix from state-space representation."""
        return self.ss[3]

    @property
    def zeros(self):
        """Zeros from zero-pole gain representation."""
        return self.zpk[0]

    @property
    def poles(self):
        """Poles from zero-pole gain representation."""
        return self.zpk[1]

    @property
    def gain(self):
        """Gain from zero-pole gain representation."""
        return self.zpk[2]

    @property
    def num(self):
        """Numerator of transfer function."""
        return self.tf[0]

    @property
    def den(self):
        """Denominator of transfer function."""
        return self.tf[1]

    @property
    def order_num(self):
        """Order of transfer function's numerator."""
        return len(self.num.coeffs) - 1

    @property
    def order_den(self):
        """Order of transfer function's denominator (i.e., state dimension).

        Equivalent to ``__len__`` method.
        """
        if self.is_ss:
            return len(self.A)  # avoids conversion to transfer function
        return len(self.den.coeffs) - 1

    @property
    def causal(self):
        """Boolean indicating if the system is causal / proper."""
        return self.order_num <= self.order_den

    @property
    def has_passthrough(self):
        """Boolean indicating if the system has a passthrough."""
        # Note there may be numerical issues for values close to 0
        # since scipy routines occasionally "normalize "those to 0
        if self.is_ss:
            return np.any(self.D != 0)
        return self.num[self.order_den] != 0

    @property
    def strictly_proper(self):
        """Boolean indicating if the system is strictly proper."""
        return self.causal and not self.has_passthrough

    @property
    def dcgain(self):
        """Steady-state response to unit step input."""
        # http://www.mathworks.com/help/control/ref/dcgain.html
        # TODO: only works for SISO
        return self(0 if self.analog else 1)

    @property
    def is_stable(self):
        """Boolean indicating if system is exponentially stable."""
        w = self.poles  # eig(A)
        if not len(w):
            assert len(self) == 0  # only a passthrough
            return True
        if not self.analog:
            return np.max(abs(w)) < 1  # within unit circle
        return np.max(w.real) < 0  # within left half-plane

    def __call__(self, s):
        """Evaluate the transfer function at the given complex value(s)."""
        return self.num(s) / self.den(s)

    def __len__(self):
        """Dimensionality of state vector."""
        return self.order_den

    def __invert__(self):
        """Reciprocal of transfer function."""
        return self.__pow__(-1)

    def __repr__(self):
        return "%s(sys=%r, analog=%r)" % (
            type(self).__name__, self._sys, self.analog)

    def __str__(self):
        if self.is_ss:
            return "(A=%s, B=%s, C=%s, D=%s, analog=%s)" % (
                self.A, self.B, self.C, self.D, self.analog)
        return "(num=%s, den=%s, analog=%s)" % (
            np.asarray(self.num), np.asarray(self.den), self.analog)

    def _check_other(self, other):
        if isinstance(other, LinearFilter):  # base class containing analog
            if self.analog != other.analog:
                raise ValueError("incompatible %s objects: %s, %s; both must "
                                 "be analog or digital" % (
                                     type(self).__name__, self, other))

    def __neg__(self):
        n, d = self.tf
        return LinearSystem((-n, d), self.analog)

    def __pow__(self, other):
        if not is_integer(other):
            return NotImplemented
        n, d = self.tf
        if other > 0:
            return LinearSystem(normalize(n**other, d**other), self.analog)
        elif other < 0:
            return LinearSystem(normalize(d**-other, n**-other), self.analog)
        else:
            assert other == 0
            return LinearSystem(1., self.analog)

    def __add__(self, other):
        self._check_other(other)
        n1, d1 = self.tf
        n2, d2 = LinearSystem(other, self.analog).tf
        if len(d1) == len(d2) and np.allclose(d1, d2):
            # short-cut to avoid needing pole-zero cancellation
            return LinearSystem((n1 + n2, d1), self.analog)
        return LinearSystem(normalize(n1*d2 + n2*d1, d1*d2), self.analog)

    def __radd__(self, other):
        return self.__add__(other)

    def __sub__(self, other):
        return self.__add__(-other)

    def __rsub__(self, other):
        return (-self).__add__(other)

    def __mul__(self, other):
        self._check_other(other)
        n1, d1 = self.tf
        n2, d2 = LinearSystem(other, self.analog).tf
        return LinearSystem(normalize(n1*n2, d1*d2), self.analog)

    def __rmul__(self, other):
        return self.__mul__(other)

    def __div__(self, other):
        self._check_other(other)
        return self.__mul__(~LinearSystem(other, self.analog))

    def __rdiv__(self, other):
        self._check_other(other)
        return (~self).__mul__(LinearSystem(other, self.analog))

    def __truediv__(self, other):
        return self.__div__(other)

    def __rtruediv__(self, other):
        return self.__rdiv__(other)

    def __eq__(self, other):
        if isinstance(other, LinearFilter):  # base class containing analog
            if self.analog != other.analog:
                return False
        return sys_equal(self, other)

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        num, den = normalize(*self.tf)
        return hash((tuple(num), tuple(den), self.analog))

    @property
    def controllable(self):
        """Returns a new system in controllable canonical form."""
        return canonical(self, controllable=True)

    @property
    def observable(self):
        """Returns a new system in observable canonical form."""
        return canonical(self, controllable=False)  # observable

    def transform(self, T, Tinv=None):
        """Changes basis of state-space matrices to ``T``.

        Then ``dot(T, x_new(t)) = x_old(t)`` after this transformation.
        """
        A, B, C, D = self.ss
        if Tinv is None:
            Tinv = inv(T)
        TA = np.dot(Tinv, np.dot(A, T))
        TB = np.dot(Tinv, B)
        TC = np.dot(C, T)
        TD = D
        return LinearSystem((TA, TB, TC, TD), analog=self.analog)

    def __iter__(self):
        """Yields a new system corresponding to each state."""
        I = np.eye(len(self))
        for i in range(len(self)):
            sys = (self.A, self.B, I[i:i+1, :], np.zeros((1, self.size_in)))
            yield LinearSystem(sys, analog=self.analog)

    @property
    def X(self):
        """Returns the multiple-output system for the state-space vector."""
        C = np.eye(len(self))
        D = np.zeros((len(self), self.size_in))
        return LinearSystem((self.A, self.B, C, D), analog=self.analog)

    def filt(self, u, dt=None, axis=0, y0=0, copy=True, filtfilt=False):
        """Filter the input using this linear system."""
        # Defaults y0=0 because y0=None has strange behaviour;
        # see unit test: test_system.py -k test_filt_issue_nengo938
        u = np.asarray(u)  # nengo PR 1123
        if not self.is_SISO:
            # Work-in-progress for issue # 106
            # TODO: relax all of these constraints
            if u.ndim == 1:
                u = u[:, None]
            if u.shape[1] != self.size_in:
                raise ValueError("Filtering with non-SISO systems requires "
                                 "the second dimension of x (%s) to equal "
                                 "the system's size_in (%s)." %
                                 (u.shape[1], self.size_in))

            if axis != 0 or y0 is None or not np.allclose(y0, 0) or \
               not copy or filtfilt:
                raise ValueError("Filtering with non-SISO systems requires "
                                 "axis=0, y0=0, copy=True, filtfilt=False.")

            warnings.warn("Filtering with non-SISO systems is an "
                          "experimental feature that may not behave as "
                          "expected.", UserWarning)

            if self.analog:
                dt = self.default_dt if dt is None else dt
                A, B, C, D, _ = cont2discrete(self.ss, dt, method='zoh')
            else:
                A, B, C, D = self.ss

            x = np.zeros(len(self), dtype=u.dtype)
            if self.size_out > 1:
                shape_out = (len(u), self.size_out)
            else:
                shape_out = (len(u),)
            y = np.empty(shape_out, dtype=u.dtype)
            for i, u_i in enumerate(u):
                y[i] = np.dot(C, x) + np.dot(D, u_i)
                x = np.dot(A, x) + np.dot(B, u_i)
            return y

        return super(LinearSystem, self).filt(u, dt, axis, y0, copy, filtfilt)

    def impulse(self, length, dt=None):
        """Impulse response with ``length`` timesteps and width ``dt``."""
        if dt is None:
            if self.analog:
                h = 1. / self.default_dt
            else:
                h = 1.
        else:
            h = 1. / dt
        delta = np.zeros(length)
        delta[0] = h
        return self.filt(delta, dt=dt, y0=0)
Exemple #13
0
class Network(with_metaclass(NengoObjectContainer)):
    """A network contains ensembles, nodes, connections, and other networks.

    A network is primarily used for grouping together related
    objects and connections for visualization purposes.
    However, you can also use networks as a nice way to reuse
    network creation code.

    To grouping together related objects that you do not need to reuse,
    you can create a new ``Network`` and add objects in a ``with`` block.
    For example::

        network = nengo.Network()
        with network:
            with nengo.Network(label="Vision"):
                v1 = nengo.Ensemble(nengo.LIF(100), dimensions=2)
            with nengo.Network(label="Motor"):
                sma = nengo.Ensemble(nengo.LIF(100), dimensions=2)
            nengo.Connection(v1, sma)

    To reuse a group of related objects, you can create a new subclass
    of ``Network``, and add objects in the ``__init__`` method.
    For example::

        class OcularDominance(nengo.Network):
            def __init__(self):
                self.column = nengo.Ensemble(nengo.LIF(100), dimensions=2)
        network = nengo.Network()
        with network:
            left_eye = OcularDominance()
            right_eye = OcularDominance()
            nengo.Connection(left_eye.column, right_eye.column)

    For more information and advanced usage, please see the Nengo
    documentation at http://nengo.readthedocs.org/.

    Parameters
    ----------
    label : str, optional
        Name of the model. Defaults to None.
    seed : int, optional
        Random number seed that will be fed to the random number generator.
        Setting this seed makes the creation of the model
        a deterministic process; however, each new ensemble
        in the network advances the random number generator,
        so if the network creation code changes, the entire model changes.
    add_to_container : bool, optional
        Determines if this Network will be added to the current container.
        Defaults to true iff currently with a Network.

    Attributes
    ----------
    label : str
        Name of the Network.
    seed : int
        Random seed used by the Network.
    ensembles : list
        List of nengo.Ensemble objects in this Network.
    nodes : list
        List of nengo.Node objects in this Network.
    connections : list
        List of nengo.Connection objects in this Network.
    networks : list
        List of nengo.BaseNetwork objects in this Network.
    """

    def __new__(cls, *args, **kwargs):
        inst = super(Network, cls).__new__(cls)
        inst.objects = {Ensemble: [], Node: [], Connection: [], Network: []}
        inst.ensembles = inst.objects[Ensemble]
        inst.nodes = inst.objects[Node]
        inst.connections = inst.objects[Connection]
        inst.networks = inst.objects[Network]
        return inst

    context = collections.deque(maxlen=100)  # static stack of Network objects

    @classmethod
    def add(cls, obj):
        """Add the passed object to the current Network.context."""
        if len(cls.context) == 0:
            raise RuntimeError("'%s' must either be created "
                               "inside a `with network:` block, or set "
                               "add_to_container=False in the object's "
                               "constructor." % obj)
        network = cls.context[-1]
        if not isinstance(network, Network):
            raise RuntimeError("Current context is not a network: %s" %
                               network)
        obj._key = network.generate_key()
        for cls in obj.__class__.__mro__:
            if cls in network.objects:
                network.objects[cls].append(obj)
                break
        else:
            raise TypeError("Objects of type '%s' cannot be added to "
                            "networks." % obj.__class__.__name__)

    def generate_key(self):
        """Returns a new key for a NengoObject to be added to this Network."""
        self._next_key += 1
        return self._next_key

    def save(self, fname, fmt=None):
        """Save this model to a file.

        So far, Pickle is the only implemented format.
        """
        if fmt is None:
            fmt = os.path.splitext(fname)[1]

        # Default to pickle
        with open(fname, 'wb') as f:
            pickle.dump(self, f)
            logger.info("Saved %s successfully.", fname)

    @classmethod
    def load(cls, fname, fmt=None):
        """Load a model from a file.

        So far, Pickle is the only implemented format.
        """
        if fmt is None:
            fmt = os.path.splitext(fname)[1]

        # Default to pickle
        with open(fname, 'rb') as f:
            return pickle.load(f)

        raise IOError("Could not load %s" % fname)

    def __eq__(self, other):
        return hash(self) == hash(other)

    def __enter__(self):
        Network.context.append(self)
        return self

    def __exit__(self, dummy_exc_type, dummy_exc_value, dummy_tb):
        if len(Network.context) == 0:
            raise RuntimeError("Network.context in bad state; was empty when "
                               "exiting from a 'with' block.")

        network = Network.context.pop()

        if network is not self:
            raise RuntimeError("Network.context in bad state; was expecting "
                               "current context to be '%s' but instead got "
                               "'%s'." % (self, network))

    def __hash__(self):
        return hash((self._key, self.label))

    def __str__(self):
        return "%s: %s" % (
            self.__class__.__name__,
            self.label if self.label is not None else str(self._key))

    def __repr__(self):
        return str(self)
Exemple #14
0
class ExposedToClient(with_metaclass(Bindable)):
    def __init__(self, client):
        self.client = client