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)
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)
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)
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)
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)
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 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)
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)
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
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))
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)
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]]))
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])
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)
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)
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)
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]
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)
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)
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)
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)
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)
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)
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)
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)
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)
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]
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)
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)
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)