Пример #1
0
def test_non_siso_manipulation():
    sys = Alpha(0.1)
    A, B, C, D = sys.ss

    SIMO = LinearSystem((A, B, np.eye(len(A)), [[0], [0]]))
    assert not SIMO.is_SISO
    assert SIMO.size_in == 1
    assert SIMO.size_out == 2
    assert SIMO.shape == (2, 1)
    assert not SIMO.has_passthrough
    assert ss_equal(_eval(SIMO), SIMO)
    assert isinstance(str(SIMO), str)
    assert ss_equal(canonical(SIMO), SIMO)
    for sub1, sub2 in zip(sys, SIMO):
        assert ss_equal(sub1, sub2)

    MISO = LinearSystem((A, [[1, 1]], C, [[0, 1]]))
    assert not MISO.is_SISO
    assert MISO.size_in == 2
    assert MISO.size_out == 1
    assert MISO.shape == (1, 2)
    assert MISO.has_passthrough
    assert ss_equal(_eval(MISO), MISO)
    assert isinstance(str(MISO), str)

    MIMO = LinearSystem((A, [[1, 1]], np.eye(len(A)), np.zeros((2, 2))))
    assert not MIMO.is_SISO
    assert MIMO.size_in == MIMO.size_out == 2
    assert MIMO.shape == (2, 2)
    assert not MIMO.has_passthrough
    assert ss_equal(_eval(MIMO), MIMO)
    assert isinstance(str(MIMO), str)
    for sub1, sub2 in zip(MISO, MIMO):
        assert ss_equal(sub1, sub2)
Пример #2
0
def test_filt():
    u = np.asarray([1.0, 0, 0])
    dt = 0.1
    num, den = [1], [1, 2, 1]
    sys1 = nengo.LinearFilter(num, den)
    sys2 = LinearSystem((num, den))  # uses a different make_step
    y1 = sys1.filt(u, dt=dt, y0=0)
    y2 = sys2.filt(u, dt=dt, y0=0)
    assert np.allclose(y1, y2)
Пример #3
0
def test_filt_issue_nengo938():
    # Testing related to nengo issue #938
    # test combinations of _apply_filter / filt on nengo / nengolib
    # using a passthrough / (strictly) proper and y0=0 / y0=None
    # ... in an **ideal** world all of these would produce the same results
    # but we assert behaviour here so that it is at least documented
    # and so that we are alerted to any changes in these differences
    # https://github.com/nengo/nengo/issues/938
    # https://github.com/nengo/nengo/issues/1124

    sys_prop_nengo = nengo.LinearFilter([1], [1, 0])
    sys_prop_nglib = LinearSystem(([1], [1, 0]))
    sys_pass_nengo = nengo.LinearFilter([1e-9, 1], [1, 0])
    sys_pass_nglib = LinearSystem(([1e-9, 1], [1, 0]))

    u = np.asarray([1.0, 0.5, 0])
    dt = 0.001

    def filt_scipy(sys):
        return _apply_filter(sys, dt=dt, u=u)

    def filt_nengo(sys, y0):
        return sys.filt(u, dt=dt, y0=y0)

    # Strictly proper transfer function
    prop_nengo_apply = filt_scipy(sys_prop_nengo)
    prop_nglib_apply = filt_scipy(sys_prop_nglib)
    prop_nengo_filt0 = filt_nengo(sys_prop_nengo, y0=0)
    prop_nglib_filt0 = filt_nengo(sys_prop_nglib, y0=0)
    prop_nengo_filtN = filt_nengo(sys_prop_nengo, y0=None)
    prop_nglib_filtN = filt_nengo(sys_prop_nglib, y0=None)

    # => two equivalence classes
    _transclose(prop_nengo_apply, prop_nglib_apply)
    _transclose(prop_nengo_filt0, prop_nglib_filt0, prop_nglib_filtN)

    # One-step delay difference between these two classes
    _transclose(prop_nengo_apply[1:], prop_nengo_filt0[:-1])

    # Passthrough transfer functions
    pass_nengo_apply = filt_scipy(sys_pass_nengo)
    pass_nglib_apply = filt_scipy(sys_pass_nglib)
    pass_nengo_filt0 = filt_nengo(sys_pass_nengo, y0=0)
    pass_nglib_filt0 = filt_nengo(sys_pass_nglib, y0=0)
    pass_nengo_filtN = filt_nengo(sys_pass_nengo, y0=None)
    pass_nglib_filtN = filt_nengo(sys_pass_nglib, y0=None)

    # => almost all are equivalent (except nengo with y0=None)
    _transclose(pass_nengo_apply, pass_nglib_apply, pass_nengo_filt0,
                pass_nglib_filt0, pass_nglib_filtN)
    assert not np.allclose(prop_nengo_filtN, pass_nengo_filtN)

    # And belongs to the same equivalence class as the very first
    _transclose(prop_nengo_apply, pass_nengo_apply)
Пример #4
0
def test_zerodim_system():
    sys = LinearSystem(1)
    assert len(sys) == 0
    assert ss_equal(sys, (0, 0, 0, 1))

    # However, this following system could have dimension 0 or 1
    # depending on whether we're before or after scipy 0.18
    # see https://github.com/scipy/scipy/issues/5760
    # TODO: ideally it would stay 0, but documenting this weirdness for now
    ss_sys = LinearSystem(sys.ss)
    assert len(ss_sys) in (0, 1)
Пример #5
0
def test_check_sys_equal():
    assert not sys_equal(np.ones(2), np.ones(3))

    assert s != z
    assert not z == s
    assert LinearSystem(5, analog=True) != LinearSystem(5, analog=False)

    with pytest.raises(ValueError):
        sys_equal(s, z)

    with pytest.raises(ValueError):
        ss_equal(s, z)
Пример #6
0
 def __init__(self, sys, process, n_steps=10000, dt=None,
              **run_steps_kwargs):
     super(EvalPoints, self).__init__()
     self.sys = LinearSystem(sys)
     if not isinstance(process, Process):
         raise ValidationError(
             "process (%s) must be a Process" % (process,),
             attr='process', obj=self)
     self.process = process
     self.n_steps = n_steps
     if dt is None:
         dt = self.process.default_dt  # 0.001
     self.dt = dt
     self.run_steps_kwargs = run_steps_kwargs
Пример #7
0
def balance(sys):
    """Transforms a linear system to its balanced realization.

    Parameters
    ----------
    sys : :data:`linear_system_like`
       Linear system representation.

    Returns
    -------
    balanced_sys : :class:`.LinearSystem`
       Balanced linear system in state-space form.

    See Also
    --------
    :func:`.balred`
    :func:`.balanced_transformation`
    :class:`.Balanced`

    References
    ----------
    .. [#] https://www.mathworks.com/help/control/ref/balreal.html

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

    >>> from nengolib.signal import balance, s
    >>> before = 10 / ((s + 10) * (s + 20) * (s + 30) * (s + 40))
    >>> after = balance(before)

    Effect of balancing some arbitrary system:

    >>> import matplotlib.pyplot as plt
    >>> length = 500
    >>> plt.subplot(211)
    >>> plt.title("Impulse - Before")
    >>> plt.plot(before.ntrange(length), before.X.impulse(length))
    >>> plt.subplot(212)
    >>> plt.title("Impulse - After")
    >>> plt.plot(after.ntrange(length), after.X.impulse(length))
    >>> plt.xlabel("Time (s)")
    >>> plt.show()
    """

    sys = LinearSystem(sys)
    T, Tinv, _ = balanced_transformation(sys)
    return sys.transform(T, Tinv=Tinv)
Пример #8
0
def _pade_delay(p, q, c):
    """Numerically evaluated state-space using Pade approximants.

    This may have numerical issues for large values of p or q.
    """
    i = np.arange(1, p + q + 1, dtype=np.float64)
    taylor = np.append([1.0], (-c)**i / factorial(i))
    num, den = pade(taylor, q)
    return LinearSystem((num, den), analog=True)
Пример #9
0
def test_is_stable():
    sys = Lowpass(0.1)
    assert sys.is_stable

    assert not (~s).is_stable  # integrator

    assert LinearSystem(1).is_stable

    assert (~(z * (z - 0.5))).is_stable
    assert not (z / (z - 1)).is_stable  # discrete integrator
Пример #10
0
def _realize(sys, radii, T, Tinv=None):
    """Helper function for producing a RealizerResult."""
    sys = LinearSystem(sys)
    r = np.asarray(radii, dtype=np.float64)
    if r.ndim == 0:
        r = np.ones(len(sys)) * r
    elif r.ndim > 1:
        raise ValueError("radii (%s) must be a 1-dim array or scalar" %
                         (radii, ))
    elif len(r) != len(sys):
        raise ValueError("radii (%s) length must match state dimension %d" %
                         (radii, len(sys)))

    T = T * r[None, :]
    if Tinv is None:  # this needs to be computed eventually anyways
        Tinv = inv(T)
    else:
        Tinv = Tinv / r[:, None]

    return RealizerResult(sys, T, Tinv, sys.transform(T, Tinv))
Пример #11
0
 def __call__(self, sys, radii=1):
     """Produces a :class:`.RealizerResult` scaled by the ``radii``."""
     # TODO: this also recomputes many subcalculations in l1_norm
     sys = LinearSystem(sys)
     T = np.diag(
         np.atleast_1d(
             np.squeeze([
                 l1_norm(sub, rtol=self.rtol, max_length=self.max_length)[0]
                 for sub in sys
             ])))
     return _realize(sys, radii, T)
Пример #12
0
def test_canonical():
    sys = ([1], [1], [1], [0])
    assert sys_equal(canonical(sys), sys)

    sys = ([[1, 0], [1, 0]], [[1], [0]], [[1, 1]], [0])
    assert sys_equal(canonical(sys), sys)

    sys = ([[1, 0], [0, 1]], [[0], [1]], [[1, 1]], [0])
    assert sys_equal(canonical(sys), sys)

    sys = ([[1, 0], [0, 1]], [[1], [0]], [[1, 1]], [0])
    assert sys_equal(canonical(sys), sys)

    sys = ([[1, 0], [0, 0]], [[1], [0]], [[1, 1]], [0])
    assert sys_equal(canonical(sys), sys)

    sys = ([[1, 0], [0, 0]], [[0], [1]], [[1, 1]], [0])
    assert sys_equal(canonical(sys), sys)

    sys = ([[1, 0, 1], [0, 1, 1], [1, 0, 0]], [[0], [1], [-1]],
           [[1, 1, 1]], [0])
    assert sys_equal(canonical(sys), sys)

    sys = nengo.Alpha(0.1)
    csys = canonical(sys, controllable=True)
    osys = canonical(sys, controllable=False)

    assert ss_equal(csys, LinearSystem(sys).controllable)
    assert ss_equal(osys, LinearSystem(sys).observable)

    assert sys_equal(csys, osys)
    assert not ss_equal(csys, osys)  # different state-space realizations

    A, B, C, D = csys.ss
    assert sys_equal(csys, sys)
    assert ss_equal(csys,
                    ([[-20, -100], [1, 0]], [[1], [0]], [[0, 100]], [[0]]))

    assert sys_equal(osys, sys)
    assert ss_equal(osys,
                    ([[-20, 1], [-100, 0]], [[0], [100]], [[1, 0]], [[0]]))
Пример #13
0
def test_non_siso_filtering(rng):
    sys = PadeDelay(0.1, order=4)
    length = 1000

    SIMO = sys.X
    assert not SIMO.is_SISO
    assert SIMO.size_in == 1
    assert SIMO.size_out == len(sys)

    x = SIMO.impulse(length)
    for i, (sub1, sub2) in enumerate(zip(sys, SIMO)):
        assert sub1 == sub2
        y1 = sub1.impulse(length)
        y2 = sub2.impulse(length)
        _transclose(shift(y1), shift(y2), x[:, i])

    B = np.asarray([[1, 2, 3], [0, 0, 0], [0, 0, 0], [0, 0, 0]]) * sys.B
    u = rng.randn(length, 3)

    Bu = u.dot([1, 2, 3])
    assert Bu.shape == (length,)
    MISO = LinearSystem((sys.A, B, sys.C, np.zeros((1, 3))), analog=True)
    assert not MISO.is_SISO
    assert MISO.size_in == 3
    assert MISO.size_out == 1

    y = cont2discrete(MISO, dt=0.001).filt(u)
    assert y.shape == (length,)
    assert np.allclose(shift(sys.filt(Bu)), y)

    MIMO = MISO.X
    assert not MIMO.is_SISO
    assert MIMO.size_in == 3
    assert MIMO.size_out == 4

    y = MIMO.filt(u)
    I = np.eye(len(sys))
    for i, sub1 in enumerate(MIMO):
        sub2 = LinearSystem((sys.A, B, I[i:i+1], np.zeros((1, 3))))
        _transclose(sub1.filt(u), sub2.filt(u), y[:, i])
Пример #14
0
def _passthrough_delay(m, c):
    """Analytically derived state-space when p = q = m.

    We use this because it is numerically stable for high m.
    """
    j = np.arange(1, m + 1, dtype=np.float64)
    u = (m + j) * (m - j + 1) / (c * j)

    A = np.zeros((m, m))
    B = np.zeros((m, 1))
    C = np.zeros((1, m))
    D = np.zeros((1, ))

    A[0, :] = B[0, 0] = -u[0]
    A[1:, :-1][np.diag_indices(m - 1)] = u[1:]
    D[0] = (-1)**m
    C[0, np.arange(m) % 2 == 0] = 2 * D[0]
    return LinearSystem((A, B, C, D), analog=True)
Пример #15
0
def pole_zero_cancel(sys, tol=1e-8):
    """Pole/zero cancellation within a given tolerance.

    Sometimes referred to as the minimal realization in state-space. [#]_
    This (greedily) finds pole-zero pairs within a given tolerance, and
    removes them from the transfer function representation.

    Parameters
    ----------
    sys : :data:`linear_system_like`
       Linear system representation.
    tol : ``float``, optional
       Absolute tolerance to identify pole-zero pairs. Defaults to ``1e-8``.

    Returns
    -------
    reduced_sys : :class:`.LinearSystem`
       Reduced linear system in zero-pole-gain form.

    References
    ----------
    .. [#] http://www.mathworks.com/help/control/ref/minreal.html

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

    >>> from nengolib.signal import pole_zero_cancel, s
    >>> sys = (s - 1) / ((s - 1) * (s + 1))
    >>> assert pole_zero_cancel(sys) == 1 / (s + 1)
    """

    z, p, k = sys2zpk(sys)
    mz = np.ones(len(z), dtype=bool)  # start with all zeros
    mp = np.zeros(len(p), dtype=bool)  # and no poles
    for i, pole in enumerate(p):
        # search among the remaining zeros
        bad = np.where((np.abs(pole - z) <= tol) & mz)[0]
        if len(bad):  # cancel this pole with one of the zeros
            mz[bad[0]] = False
        else:  # include this pole
            mp[i] = True
    return LinearSystem((z[mz], p[mp], k), analog=sys.analog)
Пример #16
0
def _proper_delay(q, c):
    """Analytically derived state-space when p = q - 1.

    We use this because it is numerically stable for high q
    and doesn't have a passthrough.
    """
    j = np.arange(q, dtype=np.float64)
    u = (q + j) * (q - j) / (c * (j + 1))

    A = np.zeros((q, q))
    B = np.zeros((q, 1))
    C = np.zeros((1, q))
    D = np.zeros((1, ))

    A[0, :] = -u[0]
    B[0, 0] = u[0]
    A[1:, :-1][np.diag_indices(q - 1)] = u[1:]
    C[0, :] = (j + 1) / float(q) * (-1)**(q - 1 - j)
    return LinearSystem((A, B, C, D), analog=True)
Пример #17
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]
Пример #18
0
def balred(sys, order, method='del'):
    """Reduces a linear system to given order using balance and modred.

    Parameters
    ----------
    sys : :data:`linear_system_like`
       Linear system representation.
    order : ``integer``
       Number of dimensions to keep.
    method : ``string``, optional
       Model order reduction method passed to :func:`.modred`.

    Returns
    -------
    reduced_sys : :class:`.LinearSystem`
       Balanced and reduced linear system in state-space form.

    See Also
    --------
    :func:`.balance`
    :func:`.modred`

    References
    ----------
    .. [#] https://www.mathworks.com/help/control/ref/balred.html

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

    sys = LinearSystem(sys)
    if order < 1:
        raise ValueError("Invalid order (%s), must be at least 1" % (order, ))
    if order >= len(sys):
        warnings.warn("Model is already of given order")
        return sys
    sys = balance(sys)
    keep_states = np.arange(order)  # keep largest eigenvalues
    return modred(sys, keep_states, method)
Пример #19
0
    def __init__(self, sys, n_neurons_per_ensemble, synapse, dt, radii=1.0,
                 input_synapse=None, output_synapse=None,
                 realizer=Hankel(), solver=Default,
                 label=None, seed=None, add_to_container=None, **ens_kwargs):
        super(LinearNetwork, self).__init__(label, seed, add_to_container)

        # Parameter checking
        self.sys = LinearSystem(sys)
        self.n_neurons_per_ensemble = n_neurons_per_ensemble
        self.synapse = synapse
        self.dt = dt
        self.radii = radii
        self.input_synapse = input_synapse
        self.output_synapse = output_synapse
        self.realizer = realizer

        if solver is not Default:
            # https://github.com/nengo/nengo/issues/1044
            solver._hack = random()

        if len(self.sys) == 0:
            raise ValueError("system (%s) is zero order" % self.sys)

        if self.sys.has_passthrough and self.output_synapse is None:
            # the user shouldn't filter the output node themselves. an
            # output synapse should be given so we can do it before the
            # passthrough.
            warnings.warn("output_synapse should be given if the system has "
                          "a passthrough, otherwise filtering the output will "
                          "also filter the passthrough")

        if not self.sys.is_stable:
            # This means certain normalizers won't work, because the worst-case
            # output is now unbounded.
            warnings.warn("system (%s) is not exponentially stable" % self.sys)

        # Obtain state-space transformation and realization
        self._realizer_result = self.realizer(self.sys, self.radii)

        # Map the system onto the synapse
        self._mapped = ss2sim(self.realization, self.synapse, self.dt)

        with self:
            # Create internal Nengo objects
            self._input = nengo.Node(size_in=self.size_in, label="input")
            self._output = nengo.Node(size_in=self.size_out, label="output")

            x_input, x_output = self._make_core(solver, **ens_kwargs)

            # Connect everything up using (A, B, C, D)
            nengo.Connection(
                x_output, x_input, transform=self.A,
                synapse=self.synapse)
            nengo.Connection(
                self.input, x_input, transform=self.B,
                synapse=self.input_synapse)
            nengo.Connection(
                x_output, self.output, transform=self.C,
                synapse=self.output_synapse)

            if not np.allclose(self.D, 0):
                logging.info("Passthrough (%s) on LinearNetwork with sys=%s",
                             self.D, self.sys)
                nengo.Connection(
                    self.input, self.output, transform=self.D,
                    synapse=None)
Пример #20
0
def LegendreDelay(theta, order):
    """PadeDelay(theta, order) realizing the shifted Legendre basis.

    The transfer function is equivalent to :func:`.PadeDelay`, but its
    canonical state-space realization represents the window of history
    by the shifted Legendre polnomials:

    .. math::

       P_i(2 \\theta' \\theta^{-1} - 1)

    where ``i`` is the zero-based index into the state-vector.

    Parameters
    ----------
    theta : ``float``
        Length of time-delay in seconds.
    order : ``integer``
        Order of approximation in the denominator
        (dimensionality of resulting system).

    Returns
    -------
    sys : :class:`.LinearSystem`
        Finite-order approximation of a pure time-delay.

    See Also
    --------
    :func:`.PadeDelay`
    :func:`.pade_delay_error`
    :class:`.RollingWindow`

    Examples
    --------
    >>> from nengolib.synapses import LegendreDelay

    Delay 15 Hz band-limited white noise by 100 ms using various orders of
    approximations:

    >>> from nengolib.signal import z
    >>> from nengo.processes import WhiteSignal
    >>> import matplotlib.pyplot as plt
    >>> process = WhiteSignal(10., high=15, y0=0)
    >>> u = process.run_steps(500)
    >>> t = process.ntrange(len(u))
    >>> plt.plot(t, (z**-100).filt(u), linestyle='--', lw=4, label="Ideal")
    >>> for order in list(range(4, 9)):
    >>>     sys = LegendreDelay(.1, order=order)
    >>>     assert len(sys) == order
    >>>     plt.plot(t, sys.filt(u), label="order=%s" % order)
    >>> plt.xlabel("Time (s)")
    >>> plt.legend()
    >>> plt.show()
    """

    q = _check_order(order)

    Q = np.arange(q, dtype=np.float64)
    R = (2 * Q + 1)[:, None] / theta
    j, i = np.meshgrid(Q, Q)

    A = np.where(i < j, -1, (-1.)**(i - j + 1)) * R
    B = (-1.)**Q[:, None] * R
    C = np.ones((1, q))
    D = np.zeros((1, ))

    return LinearSystem((A, B, C, D), analog=True)
Пример #21
0
 def __call__(self, sys, radii=1):
     """Produces a :class:`.RealizerResult` scaled by the ``radii``."""
     sys = LinearSystem(sys)
     I = np.eye(len(sys))
     return _realize(sys, radii, I, I)
Пример #22
0
        sub2 = LinearSystem((sys.A, B, I[i:i+1], np.zeros((1, 3))))
        _transclose(sub1.filt(u), sub2.filt(u), y[:, i])


def test_bad_filt():
    sys = PadeDelay(0.1, order=4).X
    with pytest.raises(ValueError):
        sys.filt(np.ones((4, 4)))
    with pytest.raises(ValueError):
        sys.filt(np.ones((4, 1)), filtfilt=True)
    with pytest.raises(ValueError):
        sys.filt(np.ones((4,)), copy=False)


@pytest.mark.parametrize("sys", [
    Lowpass(0.01), Alpha(0.2), LinearSystem(([1, 1], [0.01, 1]))])
def test_simulation(sys, Simulator, plt, seed):
    assert isinstance(sys, LinearSystem)
    old_sys = nengo.LinearFilter(sys.num, sys.den)
    assert sys == old_sys

    with Network() as model:
        stim = nengo.Node(output=nengo.processes.WhiteSignal(
            1.0, high=10, seed=seed))
        out_new = nengo.Node(size_in=2)
        out_old = nengo.Node(size_in=2)
        nengo.Connection(stim, out_new, transform=[[1], [-1]], synapse=sys)
        nengo.Connection(stim, out_old, transform=[[1], [-1]], synapse=old_sys)
        p_new = nengo.Probe(out_new)
        p_old = nengo.Probe(out_old)
Пример #23
0
def test_linear_system():
    tau = 0.05
    sys = Lowpass(tau)
    dsys = (1 - np.exp(-1)) * z / (1 - np.exp(-1) * z)

    # Test attributes before state-space/zpk computed
    assert sys.is_tf
    assert not sys.is_ss
    assert not sys.is_zpk

    # Test representations
    assert sys == (1, [tau, 1])
    assert sys_equal(sys.tf, sys)
    assert sys_equal(sys.ss, sys)
    assert sys_equal(sys.zpk, sys)

    # Test attributes after state-space/zpk computed
    assert sys.is_tf
    assert sys.is_ss
    assert sys.is_zpk

    # Size in/out-related properties
    assert sys.is_SISO
    assert dsys.is_SISO
    assert sys.size_in == sys.size_out == dsys.size_in == dsys.size_out == 1

    # Test attributes
    assert np.allclose(sys.num, (1/tau,))
    assert np.allclose(sys.den, (1, 1/tau))
    assert sys.causal
    assert sys.strictly_proper
    assert not sys.has_passthrough
    assert not (sys/s).has_passthrough
    assert (sys*s).has_passthrough
    assert (sys*s).causal
    assert not (sys*s).strictly_proper
    assert not (sys*s*s).has_passthrough and not (sys*s*s).causal
    assert (sys*s*s + sys*s).has_passthrough

    assert np.allclose(sys.A, -1/tau)
    assert np.allclose(sys.B, 1)
    assert np.allclose(sys.C, 1/tau)
    assert np.allclose(sys.D, 0)

    assert np.allclose(sys.zeros, [0])
    assert np.allclose(sys.poles, [-1/tau])
    assert np.allclose(sys.gain, 1/tau)
    assert np.allclose(sys.zpk[0], np.array([]))
    assert np.allclose(sys.zpk[1], np.array([-1/tau]))
    assert np.allclose(sys.zpk[2], 1/tau)

    assert sys.order_num == 0
    assert sys.order_den == 1
    assert len(sys) == 1  # order_den
    assert len(LinearSystem(sys.ss)) == 1  # uses state-space rep

    # Test dcgain and __call__
    assert np.allclose(sys.dcgain, 1)
    assert np.allclose(dsys.dcgain, 1)
    assert np.allclose((s*sys)(1e12), 1.0 / tau)  # initial value theorem
    assert np.allclose((s*sys)(0), 0)  # final value theorem
    assert np.allclose(((1 - ~z)*dsys)(1), 0)  # final value theorem

    # Test multiplication and squaring
    assert sys*2 == 2*sys
    assert (0.4*sys) + (0.6*sys) == sys
    assert sys + sys == 2*sys
    assert sys * sys == sys**2
    assert sys_equal(sys + sys*sys, sys*sys + sys)

    # Test pow
    with pytest.raises(TypeError):
        sys**0.5
    assert sys**0 == LinearSystem(1)

    # Test inversion
    inv = ~sys
    assert inv == ([tau, 1], 1)
    assert not inv.causal

    assert inv == 1 / sys
    assert inv == sys**(-1)

    # Test repr/str
    copy = _eval(sys)
    assert copy == sys

    # Test addition/subtraction
    assert sys + 2 == ((2*tau, 3), (tau, 1))
    assert 3 + sys == (-sys)*(-1) + 3
    assert (4 - sys) + 2 == (-sys) + 6
    assert np.allclose((sys - sys).num, 0)

    # Test division
    assert sys / 2 == sys * 0.5
    assert 2 / sys == 2 * inv

    cancel = sys / sys
    assert np.allclose(cancel.num, cancel.den)

    # Test inequality
    assert sys != (sys*2)

    # Test usage of differential building block
    assert sys == 1 / (tau*s + 1)
Пример #24
0
def cont2discrete(sys, dt, method='zoh', alpha=None):
    """Convert linear system from continuous to discrete time-domain.

    This is a wrapper around :func:`scipy.signal.cont2discrete`, with the
    same interface (apart from the type of the first parameter).

    Parameters
    ----------
    sys : :data:`linear_system_like`
       Linear system representation.
    dt : ``float``
       Time-step for discrete simulation of target system.
    method : ``string``, optional
       Method of discretization. Defaults to zero-order hold discretization
       (``'zoh'``), which assumes that the input signal is held constant over
       each discrete time-step. [#]_
    alpha : ``float`` or ``None``, optional
       Weighting parameter for use with ``method='gbt'``.

    Returns
    -------
    discrete_sys : :class:`.LinearSystem`
       Discretized linear system (``analog=False``).

    See Also
    --------
    :func:`.discrete2cont`
    :func:`scipy.signal.cont2discrete`

    Notes
    -----
    Discretization is often performed automatically whenever needed;
    usually it is unnecessary to deal with this routine directly. One
    exception is when combining systems across domains (see example).

    References
    ----------
    .. [#] https://en.wikipedia.org/wiki/Discretization

    Examples
    --------
    Simulating an alpha synapse with a pure transmission delay:

    >>> from nengolib.signal import z, cont2discrete
    >>> from nengolib import Alpha
    >>> sys = Alpha(0.003)
    >>> dsys = z**(-20) * cont2discrete(sys, dt=sys.default_dt)
    >>> y = dsys.impulse(50)

    >>> assert np.allclose(np.sum(y), 1, atol=1e-3)
    >>> t = dsys.ntrange(len(y))

    >>> import matplotlib.pyplot as plt
    >>> plt.step(t, y, where='post')
    >>> plt.fill_between(t, np.zeros_like(y), y, step='post', alpha=.3)
    >>> plt.xlabel("Time (s)")
    >>> plt.show()
    """

    sys = LinearSystem(sys)
    if not sys.analog:
        raise ValueError("system (%s) is already discrete" % sys)
    return LinearSystem(_cont2discrete(sys.ss,
                                       dt=dt,
                                       method=method,
                                       alpha=alpha)[:-1],
                        analog=False)
Пример #25
0
def discrete2cont(sys, dt, method='zoh', alpha=None):
    """Convert linear system from discrete to continuous time-domain.

    This is the inverse of :func:`.cont2discrete`. This will not work in
    general, for instance with the ZOH method when the system has discrete
    poles at ``0`` (e.g., systems with pure time-delay elements).

    Parameters
    ----------
    sys : :data:`linear_system_like`
       Linear system representation.
    dt : ``float``
       Time-step used to *undiscretize* ``sys``.
    method : ``string``, optional
       Method of discretization. Defaults to zero-order hold discretization
       (``'zoh'``), which assumes that the input signal is held constant over
       each discrete time-step.
    alpha : ``float`` or ``None``, optional
       Weighting parameter for use with ``method='gbt'``.

    Returns
    -------
    continuous_sys : :class:`.LinearSystem`
       Continuous linear system (``analog=True``).

    See Also
    --------
    :func:`.cont2discrete`
    :func:`scipy.signal.cont2discrete`

    Examples
    --------
    Converting a double-exponential synapse back and forth between domains:

    >>> from nengolib.signal import discrete2cont, cont2discrete
    >>> from nengolib import DoubleExp
    >>> sys = DoubleExp(0.005, 0.2)
    >>> assert dsys == discrete2cont(cont2discrete(sys, dt=0.1), dt=0.1)
    """

    sys = LinearSystem(sys)
    if sys.analog:
        raise ValueError("system (%s) is already continuous" % sys)

    if dt <= 0:
        raise ValueError("dt (%s) must be positive" % (dt, ))

    ad, bd, cd, dd = sys.ss
    n = ad.shape[0]
    m = n + bd.shape[1]

    if method == 'gbt':
        if alpha is None or alpha < 0 or alpha > 1:
            raise ValueError("alpha (%s) must be in range [0, 1]" % (alpha, ))

        I = np.eye(n)
        ar = linalg.solve(alpha * dt * ad.T + (1 - alpha) * dt * I, ad.T - I).T
        M = I - alpha * dt * ar

        br = np.dot(M, bd) / dt
        cr = np.dot(cd, M)
        dr = dd - alpha * np.dot(cr, bd)

    elif method in ('bilinear', 'tustin'):
        return discrete2cont(sys, dt, method='gbt', alpha=0.5)

    elif method in ('euler', 'forward_diff'):
        return discrete2cont(sys, dt, method='gbt', alpha=0.0)

    elif method == 'backward_diff':
        return discrete2cont(sys, dt, method='gbt', alpha=1.0)

    elif method == 'zoh':
        M = np.zeros((m, m))
        M[:n, :n] = ad
        M[:n, n:] = bd
        M[n:, n:] = np.eye(bd.shape[1])
        E = linalg.logm(M) / dt

        ar = E[:n, :n]
        br = E[:n, n:]
        cr = cd
        dr = dd

    else:
        raise ValueError("invalid method: '%s'" % (method, ))

    return LinearSystem((ar, br, cr, dr), analog=True)
Пример #26
0
 def __call__(self, sys, radii=1):
     """Produces a :class:`.RealizerResult` scaled by the ``radii``."""
     sys = LinearSystem(sys)
     T = np.diag(state_norm(sys, 'H2'))
     return _realize(sys, radii, T)
Пример #27
0
class EvalPoints(Distribution):
    """Samples the output of a LinearSystem given some input process.

    This can be used to sample the evaluation points according to some
    filtered process. Used by :class:`.RollingWindow`.

    Parameters
    ----------
    sys : :data:`linear_system_like`
       Linear system representation.
    process : :class:`nengo.Process`
       Nengo process to simulate.
    n_steps : ``integer``, optional
       Number of steps to simulate the process. Defaults to ``10000``.
    dt : ``float``, optional
       Process and system simulation time-step.
       Defaults to ``process.default_dt``.
    **run_steps_kwargs : ``dictionary``, optional
       Additional keyword arguments for ``process.run_steps``.

    See Also
    --------
    :class:`.Encoders`
    :class:`.Callable`
    :class:`.RollingWindow`
    :class:`nengo.Ensemble`
    :class:`nengo.dists.Distribution`

    Notes
    -----
    For ideal sampling, the given ``process`` should be aperiodic across the
    interval of time specified by ``n_steps`` and ``dt``, and moreover
    the sampled ``num`` (number of evaluation points) should not
    exceed ``n_steps``.

    Examples
    --------
    >>> from nengolib.signal import EvalPoints

    Sampling from the state-space of an alpha synapse given band-limited
    white noise:

    >>> from nengolib import Alpha
    >>> from nengo.processes import WhiteSignal
    >>> eval_points = EvalPoints(Alpha(.5).X, WhiteSignal(10, high=20))

    >>> import matplotlib.pyplot as plt
    >>> from seaborn import jointplot
    >>> jointplot(*eval_points.sample(1000, 2).T, kind='kde')
    >>> plt.show()
    """

    def __init__(self, sys, process, n_steps=10000, dt=None,
                 **run_steps_kwargs):
        super(EvalPoints, self).__init__()
        self.sys = LinearSystem(sys)
        if not isinstance(process, Process):
            raise ValidationError(
                "process (%s) must be a Process" % (process,),
                attr='process', obj=self)
        self.process = process
        self.n_steps = n_steps
        if dt is None:
            dt = self.process.default_dt  # 0.001
        self.dt = dt
        self.run_steps_kwargs = run_steps_kwargs

    def __repr__(self):
        return ("%s(sys=%r, process=%r, n_steps=%r, dt=%r, **%r)" %
                (type(self).__name__, self.sys, self.process, self.n_steps,
                 self.dt, self.run_steps_kwargs))

    def _sample(self, d, rng):
        if self.sys.size_out != d:
            raise ValidationError(
                "sys.size_out (%d) must equal sample d (%s)" %
                (self.sys.size_out, d), attr='sys', obj=self)
        u = self.process.run_steps(
            self.n_steps, d=self.sys.size_in, dt=self.dt, rng=rng,
            **self.run_steps_kwargs)
        return self.sys.filt(u, dt=self.dt)

    def sample(self, num, d=1, rng=np.random):
        """Samples ``n`` points in ``d`` dimensions."""
        y = self._sample(d, rng)
        choices = rng.choice(len(y), size=num, replace=True)
        return y[choices]
Пример #28
0
def test_linear_system_type():
    # Test that sys1 is reused by sys2
    sys1 = LinearSystem(1)
    sys2 = LinearSystem(sys1)
    sys3 = LinearSystem(1)

    assert sys1 is sys2
    assert sys1 is not sys3

    # Test that sys1 is still reused even with weird arg/kwarg ordering
    sys4 = LinearSystem(sys1, analog=True)
    sys5 = LinearSystem(sys=sys1, analog=True)
    sys6 = LinearSystem(analog=True, sys=sys1)

    assert sys1 is sys4
    assert sys1 is sys5
    assert sys1 is sys6

    # Test that analog argument gets inherited properly
    assert LinearSystem(s).analog
    assert LinearSystem(s, analog=True).analog
    assert not LinearSystem(z).analog
    assert not LinearSystem(z, analog=False).analog
    assert LinearSystem(nengo.Lowpass(0.1)).analog
    assert not LinearSystem(LinearSystem(([1], [1]), analog=False)).analog

    # Test that analog argument must match
    with pytest.raises(TypeError):
        LinearSystem(sys1, analog=False)

    with pytest.raises(TypeError):
        LinearSystem(sys1, False)

    with pytest.raises(TypeError):
        LinearSystem(sys1, analog=False)

    with pytest.raises(TypeError):
        LinearSystem(s, analog=False)

    with pytest.raises(TypeError):
        LinearSystem(z, analog=True)

    with pytest.raises(TypeError):
        LinearSystem(LinearSystem(([1], [1]), analog=True), analog=False)

    with pytest.raises(TypeError):
        LinearSystem(LinearSystem(([1], [1]), analog=False), analog=True)
Пример #29
0
def modred(sys, keep_states, method='del'):
    """Reduces model order by eliminating a subset of states.

    Parameters
    ----------
    sys : :data:`linear_system_like`
       Linear system representation.
    keep_states : ``array_like``
       Subset of dimensions (integer indices between ``0`` and
       ``len(sys)-1``, inclusive) to keep.
    method : ``string``, optional
       Defaults to ``'del'``. Must be one of:

       * ``'del'`` : Delete the states entirely.

       * ``'dc'`` : Transform the remaining states to maintain the same
         DC gain. [#]_

    Returns
    -------
    reduced_sys : :class:`.LinearSystem`
       Reduced linear system in state-space form.

    See Also
    --------
    :func:`.balred`

    References
    ----------
    .. [#] http://www.mathworks.com/help/control/ref/modred.html

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

    sys = LinearSystem(sys)
    A, B, C, D = sys.ss
    if not sys.analog:
        raise NotImplementedError("model reduction of digital filters not "
                                  "supported")

    mask = np.zeros(len(A), dtype=bool)
    mask[np.asarray(keep_states)] = True

    grm = np.where(mask)[0]
    brm = np.where(~mask)[0]
    glm = grm[:, None]
    blm = brm[:, None]

    A11 = A[glm, grm]
    A12 = A[glm, brm]
    A21 = A[blm, grm]
    A22 = A[blm, brm]
    B1 = B[mask, :]
    B2 = B[~mask, :]
    C1 = C[:, mask]
    C2 = C[:, ~mask]

    if method == 'del':
        RA = A11
        RB = B1
        RC = C1
        RD = D

    elif method == 'dc':
        A22I = inv(A22)
        RA = A11 - np.dot(A12, np.dot(A22I, A21))
        RB = B1 - np.dot(A12, np.dot(A22I, B2))
        RC = C1 - np.dot(C2, np.dot(A22I, A21))
        RD = D - np.dot(C2, np.dot(A22I, B2))
        # TODO: for discrete case, simply replace (-A22I) with inv(I - A22)

    else:
        raise ValueError("invalid method: '%s'" % (method, ))

    return LinearSystem((RA, RB, RC, RD), analog=sys.analog)
Пример #30
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)