예제 #1
0
def test_invalid_discrete():
    dt = 0.001
    sys = cont2discrete(Lowpass(0.1), dt=dt)

    with pytest.raises(ValueError):
        discrete2cont(sys, dt=dt, method='gbt', alpha=1.1)

    with pytest.raises(ValueError):
        discrete2cont(sys, dt=0)

    with pytest.raises(ValueError):
        discrete2cont(sys, dt=dt, method=None)

    with pytest.raises(ValueError):
        discrete2cont(s, dt=dt)  # already continuous

    with pytest.raises(ValueError):
        cont2discrete(z, dt=dt)  # already discrete
예제 #2
0
def test_invalid_discrete():
    dt = 0.001
    sys = cont2discrete(Lowpass(0.1), dt=dt)

    with pytest.raises(ValueError):
        discrete2cont(sys, dt=dt, method='gbt', alpha=1.1)

    with pytest.raises(ValueError):
        discrete2cont(sys, dt=0)

    with pytest.raises(ValueError):
        discrete2cont(sys, dt=dt, method=None)

    with pytest.raises(ValueError):
        discrete2cont(s, dt=dt)  # already continuous

    with pytest.raises(ValueError):
        cont2discrete(z, dt=dt)  # already discrete
예제 #3
0
def ss2sim(sys, synapse, dt=None):
    """Maps an LTI system to the synaptic dynamics in state-space."""
    synapse = LinearSystem(synapse)
    if len(synapse) != 1 or not synapse.proper or not synapse.analog:
        raise ValueError("synapse (%s) must be first-order, proper, and "
                         "analog" % synapse)

    sys = LinearSystem(sys)
    if not sys.analog:
        raise ValueError("system (%s) must be analog" % sys)

    # TODO: put derivations into a notebook
    a, = synapse.num
    b1, b2 = synapse.den
    if np.allclose(b2, 0):  # scaled integrator
        # put synapse into form: gain / s, and handle gain at the end
        gain = a / b1
        if dt is None:
            A, B, C, D = sys.ss
        else:
            # discretized integrator is dt / (z - 1)
            A, B, C, D = cont2discrete(sys, dt=dt).ss
            A = 1./dt * (A - np.eye(len(sys)))
            B = 1./dt * B

    else:  # scaled lowpass
        # put synapse into form: gain / (tau*s + 1), and handle gain at the end
        gain, tau = a / b2, b1 / b2  # divide both polynomials by b2

        if dt is None:
            # Analog case (normal principle 3)
            A = tau * sys.A + np.eye(len(sys))
            B = tau * sys.B
            C = sys.C
            D = sys.D
        else:
            # Discretized case (derived from generalized principle 3)
            # discretized lowpass is (1 - a) / (z - a)
            A, B, C, D = cont2discrete(sys, dt=dt).ss
            a = np.exp(-dt/tau)
            A = 1./(1 - a) * (A - a * np.eye(len(A)))
            B = 1./(1 - a) * B

    return LinearSystem((A / gain, B / gain, C, D), analog=dt is None)
예제 #4
0
def test_discrete(sys):
    dt = 0.001
    alpha = 0.6
    for method in ('gbt', 'bilinear', 'tustin', 'euler', 'forward_diff',
                   'backward_diff', 'zoh'):
        dsys = cont2discrete(sys, dt=dt, method=method, alpha=alpha)
        assert not dsys.analog
        rsys = discrete2cont(dsys, dt=dt, method=method, alpha=alpha)
        assert rsys.analog

        assert ss_equal(sys, rsys, atol=1e-07)
예제 #5
0
def test_discrete(sys):
    dt = 0.001
    alpha = 0.6
    for method in ('gbt', 'bilinear', 'tustin', 'euler', 'forward_diff',
                   'backward_diff', 'zoh'):
        dsys = cont2discrete(sys, dt=dt, method=method, alpha=alpha)
        assert not dsys.analog
        rsys = discrete2cont(dsys, dt=dt, method=method, alpha=alpha)
        assert rsys.analog

        assert np.allclose(sys.ss[0], rsys.ss[0])
        assert np.allclose(sys.ss[1], rsys.ss[1])
        assert np.allclose(sys.ss[2], rsys.ss[2])
        assert np.allclose(sys.ss[3], rsys.ss[3])
예제 #6
0
def test_impulse():
    dt = 0.001
    tau = 0.005
    length = 500

    delta = np.zeros(length)  # TODO: turn into a little helper?
    delta[1] = 1. / dt  # starts at 1 to compensate for delay removed by nengo

    sys = Lowpass(tau)
    response = impulse(sys, dt, length)
    assert np.allclose(response[0], 0)

    # should give the same result as using filt
    assert np.allclose(response, sys.filt(delta, dt))

    # should also accept discrete systems
    dss = cont2discrete(sys, dt=dt)
    assert not dss.analog
    assert np.allclose(response, impulse(dss, dt=None, length=length) / dt)
예제 #7
0
    def __init__(self, systems, dt=None, elementwise=False, method='zoh'):
        if not is_iterable(systems) or isinstance(systems, LinearSystem):
            systems = [systems]
        self.systems = systems
        self.dt = dt
        self.elementwise = elementwise

        self.A = []
        self.B = []
        self.C = []
        self.D = []
        for sys in systems:
            sys = LinearSystem(sys)
            if dt is not None:
                sys = cont2discrete(sys, dt, method=method)
            elif sys.analog:
                raise ValueError(
                    "system (%s) must be digital if not given dt" % sys)

            A, B, C, D = sys.ss
            self.A.append(A)
            self.B.append(B)
            self.C.append(C)
            self.D.append(D)

        # TODO: If all of the synapses are single order, than A is diagonal
        # and so np.dot(self.A, self._x) is trivial. But perhaps
        # block_diag is already optimized for this.

        # Note: ideally we could put this into CCF to reduce the A mapping
        # to a single dot product and a shift operation. But in general
        # since this is MIMO it is not controllable from a single input.
        # Instead we might want to consider balanced reduction to
        # improve efficiency.
        self.A = block_diag(*self.A)
        self.B = block_diag(*self.B) if elementwise else np.vstack(self.B)
        self.C = block_diag(*self.C)
        self.D = block_diag(*self.D) if elementwise else np.vstack(self.D)
        # TODO: shape validation

        self._x = np.zeros(len(self.A))[:, None]
예제 #8
0
    def __init__(self, systems, dt=None, elementwise=False, method='zoh'):
        if not is_iterable(systems):
            systems = [systems]
        self.systems = systems
        self.dt = dt
        self.elementwise = elementwise

        self.A = []
        self.B = []
        self.C = []
        self.D = []
        for sys in systems:
            sys = LinearSystem(sys)
            if dt is not None:
                sys = cont2discrete(sys, dt, method=method)
            elif sys.analog:
                raise ValueError(
                    "system (%s) must be digital if not given dt" % sys)

            A, B, C, D = sys.ss
            self.A.append(A)
            self.B.append(B)
            self.C.append(C)
            self.D.append(D)

        # TODO: If all of the synapses are single order, than A is diagonal
        # and so np.dot(self.A, self._x) is trivial. But perhaps
        # block_diag is already optimized for this.

        # Note: ideally we could put this into CCF to reduce the A mapping
        # to a single dot product and a shift operation. But in general
        # since this is MIMO it is not controllable from a single input.
        # Instead we might want to consider balanced reduction to
        # improve efficiency.
        self.A = block_diag(*self.A)
        self.B = block_diag(*self.B) if elementwise else np.vstack(self.B)
        self.C = block_diag(*self.C)
        self.D = block_diag(*self.D) if elementwise else np.vstack(self.D)
        # TODO: shape validation

        self._x = np.zeros(len(self.A))[:, None]
예제 #9
0
def l1_norm(sys, rtol=1e-6, max_length=2**18):
    """Returns the L1-norm of a linear system within a relative tolerance.

    The L1-norm of a (BIBO stable) linear system is the integral of the
    absolute value of its impulse response. For unstable systems this will be
    infinite. The L1-norm is important because it bounds the worst-case
    output of the system for arbitrary inputs within [-1, 1]. In fact,
    this worst-case output is achieved by reversing the input which alternates
    between -1 and 1 during the intervals where the impulse response is
    negative or positive, respectively (in the limit as T -> infinity).

    Algorithm adapted from [1]_ following the methods of [2]_. This works by
    iteratively refining lower and upper bounds using progressively longer
    simulations and smaller timesteps. The lower bound is given by the
    absolute values of the discretized response. The upper bound is given by
    refining the time-step intervals where zero-crossings may have occurred.

    References:
        [1] http://www.mathworks.com/matlabcentral/fileexchange/41587-system-l1-norm/content/l1norm.m  # noqa: E501
            J.F. Whidborne (April 28, 1995).

        [2] Rutland, Neil K., and Paul G. Lane. "Computing the 1-norm of the
            impulse response of linear time-invariant systems."
            Systems & control letters 26.3 (1995): 211-221.
    """
    sys = LinearSystem(sys)
    if not sys.analog:
        raise ValueError("system (%s) must be analog" % sys)

    # Setup state-space system and check stability/conditioning
    # we will subtract out D and add it back in at the end
    A, B, C, D = sys.ss
    alpha = np.max(eig(A)[0].real)  # eq (28)
    if alpha >= 0:
        raise ValueError("system (%s) has unstable eigenvalue: %s" % (
            sys, alpha))

    # Compute a suitable lower-bound for the L1-norm
    # using the steady state response, which is equivalent to the
    # L1-norm without an absolute value (i.e. just an integral)
    G0 = sys.dcgain - sys.D  # -C.inv(A).B

    # Compute a suitable upper-bound for the L1-norm
    # Note this should be tighter than 2*sum(abs(hankel(sys)))
    def _normtail(sig, A, x, C):
        # observability gramiam when A perturbed by sig
        W = solve_lyapunov(A.T + sig*np.eye(len(A)), -C.T.dot(C))
        return np.sqrt(x.dot(W).dot(x.T) / 2 / sig)  # eq (39)

    xtol = -alpha * 1e-4
    _, fopt, _, _ = fminbound(
        _normtail, 0, -alpha, (A, B.T, C), xtol=xtol, full_output=True)

    # Setup parameters for iterative optimization
    L, U = abs(G0), fopt
    N = 2**4
    T = -1 / alpha

    while N <= max_length and .5 * (U - L) / L >= rtol:  # eq (25)

        # Step 1. Improve the lower bound by simulating more.
        dt = T / N
        dsys = cont2discrete((A, B, C, 0), dt=dt)
        Phi = dsys.A

        y = impulse(dsys, dt=None, length=N)
        abs_y = abs(y[1:])
        L_impulse = np.sum(abs_y)

        # bound the missing response from t > T from below
        L_tail = abs(G0 - np.sum(y))  # eq (33)
        L = max(L, L_impulse + L_tail)

        # Step 2. Improve the upper bound using refined interval method.
        x = _state_impulse(Phi, x0=B, k=N, delay=0)  # eq (38)
        abs_e = np.squeeze(abs(C.dot(x.T)))
        x = x[:-1]  # compensate for computing where thresholds crossed

        # find intervals that could have zero-crossings and adjust their
        # upper bounds (the lower bound is exact for the other intervals)
        CTC = C.T.dot(C)
        W = solve_lyapunov(A.T, Phi.T.dot(CTC).dot(Phi) - CTC)  # eq (36)
        AWA = A.T.dot(W.dot(A))
        thresh = np.squeeze(  # eq (41)
            np.sqrt(dt * np.sum(x.dot(AWA) * x, axis=1)))
        cross = np.maximum(abs_e[:-1], abs_e[1:]) <= thresh  # eq (20)
        abs_y[cross] = np.sqrt(  # eq (22, 37)
            dt * np.sum(x[cross].dot(W) * x[cross], axis=1))

        # bound the missing response from t > T from above
        _, U_tail, _, _ = fminbound(
            _normtail, 0, -alpha, (A, x[-1], C), xtol=xtol, full_output=True)
        U_impulse = np.sum(abs_y)
        U = max(min(U, U_impulse + U_tail), L)

        N *= 2
        if U_impulse - L_impulse < U_tail - L_tail:  # eq (26)
            T *= 2

    return (U + L) / 2 + D, .5 * (U - L) / L
예제 #10
0
def ss2sim(sys, synapse, dt):
    """Maps a linear system onto a synapse in state-space form.

    This implements a generalization of Principle 3 from the Neural Engineering
    Framework (NEF). [#]_
    Intuitively, this routine compensates for the change in dynamics
    that occurs when the integrator that usually forms the basis for any
    linear system is replaced by the given synapse.
    This is needed because in neural systems we don't have access to a
    perfect integrator; instead the synapse model becomes the
    "dynamical primitive".

    Parameters
    ----------
    sys : :data:`linear_system_like`
        Linear system representation of desired dynamical system.
        Requires ``sys.analog == synapse.analog``.
    synapse : :data:`linear_system_like`
        Linear system representation of the synapse providing the dynamics.
        Requires ``sys.analog == synapse.analog``.
    dt : ``float`` or ``None``
        Time-step of simulation. If not ``None``, then both ``sys`` and
        ``synapse`` are discretized using the ``'zoh'`` method.
        In either case, if ``sys`` is now digital, then the digital
        generalization of Principle 3 will be applied --- otherwise the analog
        version will be applied.

    Returns
    -------
    mapped_sys : :class:`.LinearSystem`
        Linear system whose state-space matrices yield the desired
        dynamics when using the synapse model instead of an integrator.

    See Also
    --------
    :class:`.LinearNetwork`
    :class:`.LinearSystem`
    :func:`.cont2discrete`

    Notes
    -----
    This routine is called automatically by :class:`.LinearNetwork`.

    Principle 3 is a special case of this routine when called with
    a continuous :func:`Lowpass` synapse and ``dt=None``. However, specifying
    the ``dt`` (or providing digital systems) will improve the accuracy in
    digital simulation.

    For higher-order synapses, this makes a zero-order hold (ZOH) assumption
    to avoid requiring the input derivatives. In this case, the mapping is
    not perfect. If the input derivatives are known, then the accuracy can be
    made perfect again. See references for details.

    References
    ----------
    .. [#] A. R. Voelker and C. Eliasmith, "Improving spiking dynamical
       networks: Accurate delays, higher-order synapses, and time cells",
       2017, Submitted. [`URL <https://github.com/arvoelke/delay2017>`__]

    Examples
    --------
    See :doc:`notebooks/research/discrete_comparison` for a notebook example.

    >>> from nengolib.synapses import ss2sim, PadeDelay

    Map the state of a balanced :func:`PadeDelay` onto a lowpass synapse:

    >>> import nengo
    >>> from nengolib.signal import balance
    >>> sys = balance(PadeDelay(.05, order=6))
    >>> synapse = nengo.Lowpass(.1)
    >>> mapped = ss2sim(sys, synapse, synapse.default_dt)
    >>> assert np.allclose(sys.C, mapped.C)
    >>> assert np.allclose(sys.D, mapped.D)

    Simulate the mapped system directly (without neurons):

    >>> process = nengo.processes.WhiteSignal(1, high=10, y0=0)
    >>> with nengo.Network() as model:
    >>>     stim = nengo.Node(output=process)
    >>>     x = nengo.Node(size_in=len(sys))
    >>>     nengo.Connection(stim, x, transform=mapped.B, synapse=synapse)
    >>>     nengo.Connection(x, x, transform=mapped.A, synapse=synapse)
    >>>     p_stim = nengo.Probe(stim)
    >>>     p_actual = nengo.Probe(x)
    >>> with nengo.Simulator(model) as sim:
    >>>     sim.run(.5)

    The desired dynamics are implemented perfectly:

    >>> target = sys.X.filt(sim.data[p_stim])
    >>> assert np.allclose(target, sim.data[p_actual])

    >>> import matplotlib.pyplot as plt
    >>> plt.plot(sim.trange(), target, linestyle='--', lw=4)
    >>> plt.plot(sim.trange(), sim.data[p_actual], alpha=.5)
    >>> plt.show()
    """

    synapse = LinearSystem(synapse)
    if synapse.analog and synapse.order_num > 0:
        raise ValueError("analog synapses (%s) must have order zero in the "
                         "numerator" % synapse)

    sys = LinearSystem(sys)
    if sys.analog != synapse.analog:
        raise ValueError("system (%s) and synapse (%s) must both be analog "
                         "or both be digital" % (sys, synapse))

    if dt is not None:
        if not sys.analog:  # sys is digital
            raise ValueError("system (%s) must be analog if dt is not None" %
                             sys)
        sys = cont2discrete(sys, dt=dt)
        synapse = cont2discrete(synapse, dt=dt)

    # If the synapse was discretized, then its numerator may now have multiple
    #   coefficients. By summing them together, we are implicitly assuming that
    #   the output of the synapse will stay constant across
    #   synapse.order_num + 1 time-steps. This is also related to:
    #   http://dsp.stackexchange.com/questions/33510/difference-between-convolving-before-after-discretizing-lti-systems  # noqa: E501
    # For example, if we have H = Lowpass(0.1), then the only difference
    #   between sys1 = cont2discrete(H*H, dt) and
    #           sys2 = cont2discrete(H, dt)*cont2discrete(H, dt), is that
    #   np.sum(sys1.num) == sys2.num (while sys1.den == sys2.den)!
    gain = np.sum(synapse.num)
    c = synapse.den / gain

    A, B, C, D = sys.ss
    k = len(synapse)
    powA = [matrix_power(A, i) for i in range(k + 1)]
    AH = np.sum([c[i] * powA[i] for i in range(k + 1)], axis=0)

    if sys.analog:
        BH = np.dot(
            np.sum([c[i] * powA[i - 1] for i in range(1, k + 1)], axis=0), B)

    else:
        BH = np.dot(
            np.sum([
                c[i] * powA[i - j - 1] for j in range(k)
                for i in range(j + 1, k + 1)
            ],
                   axis=0), B)

    return LinearSystem((AH, BH, C, D), analog=sys.analog)