diff --git a/control/dtime.py b/control/dtime.py index 2ae482811..9b91eabd3 100644 --- a/control/dtime.py +++ b/control/dtime.py @@ -55,8 +55,7 @@ # Sample a continuous time system def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None, name=None, copy_names=True, **kwargs): - """ - Convert a continuous time system to discrete time by sampling. + """Convert a continuous time system to discrete time by sampling. Parameters ---------- @@ -67,9 +66,9 @@ def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None, method : string Method to use for conversion, e.g. 'bilinear', 'zoh' (default) alpha : float within [0, 1] - The generalized bilinear transformation weighting parameter, which - should only be specified with method="gbt", and is ignored - otherwise. See :func:`scipy.signal.cont2discrete`. + The generalized bilinear transformation weighting parameter, which + should only be specified with method="gbt", and is ignored + otherwise. See :func:`scipy.signal.cont2discrete`. prewarp_frequency : float within [0, infinity) The frequency [rad/s] at which to match with the input continuous- time system's magnitude and phase (only valid for method='bilinear', diff --git a/control/freqplot.py b/control/freqplot.py index 164ec28a2..533515415 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -894,8 +894,10 @@ def gen_zero_centered_series(val_min, val_max, period): # list of systems (e.g., "Step response for sys[1], sys[2]"). # - # Set the initial title for the data (unique system names) - sysnames = list(set([response.sysname for response in data])) + # Set the initial title for the data (unique system names, preserving order) + seen = set() + sysnames = [response.sysname for response in data \ + if not (response.sysname in seen or seen.add(response.sysname))] if title is None: if data[0].title is None: title = "Bode plot for " + ", ".join(sysnames) diff --git a/control/tests/discrete_test.py b/control/tests/discrete_test.py index 96777011e..cccb53708 100644 --- a/control/tests/discrete_test.py +++ b/control/tests/discrete_test.py @@ -5,11 +5,12 @@ import numpy as np import pytest +import cmath -from control import (StateSpace, TransferFunction, bode, common_timebase, - feedback, forced_response, impulse_response, - isctime, isdtime, rss, c2d, sample_system, step_response, - timebase) +import control as ct +from control import StateSpace, TransferFunction, bode, common_timebase, \ + feedback, forced_response, impulse_response, isctime, isdtime, rss, \ + c2d, sample_system, step_response, timebase class TestDiscrete: @@ -526,3 +527,33 @@ def test_signal_names(self, tsys): assert sysd_newnames.find_input('u') is None assert sysd_newnames.find_output('y') == 0 assert sysd_newnames.find_output('x') is None + + +@pytest.mark.parametrize("num, den", [ + ([1], [1, 1]), + ([1, 2], [1, 3]), + ([1, 2], [3, 4, 5]) +]) +@pytest.mark.parametrize("dt", [True, 0.1, 2]) +@pytest.mark.parametrize("method", ['zoh', 'bilinear', 'matched']) +def test_c2d_matched(num, den, dt, method): + sys_ct = ct.tf(num, den) + sys_dt = ct.sample_system(sys_ct, dt, method=method) + assert sys_dt.dt == dt # make sure sampling time is OK + assert cmath.isclose(sys_ct(0), sys_dt(1)) # check zero frequency gain + assert cmath.isclose( + sys_ct.dcgain(), sys_dt.dcgain()) # another way to check + + if method in ['zoh', 'matched']: + # Make sure that poles were properly matched + zpoles = sys_dt.poles() + for cpole in sys_ct.poles(): + zpole = zpoles[(np.abs(zpoles - cmath.exp(cpole * dt))).argmin()] + assert cmath.isclose(cmath.exp(cpole * dt), zpole) + + if method in ['matched']: + # Make sure that zeros were properly matched + zzeros = sys_dt.zeros() + for czero in sys_ct.zeros(): + zzero = zzeros[(np.abs(zzeros - cmath.exp(czero * dt))).argmin()] + assert cmath.isclose(cmath.exp(czero * dt), zzero) diff --git a/control/xferfcn.py b/control/xferfcn.py index b5334b7b8..206f0bc92 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1181,7 +1181,9 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, if not self.issiso(): raise ControlMIMONotImplemented("Not implemented for MIMO systems") if method == "matched": - return _c2d_matched(self, Ts) + if prewarp_frequency is not None: + warn('prewarp_frequency ignored: incompatible conversion') + return _c2d_matched(self, Ts, name=name, **kwargs) sys = (self.num[0][0], self.den[0][0]) if prewarp_frequency is not None: if method in ('bilinear', 'tustin') or \ @@ -1284,9 +1286,12 @@ def _isstatic(self): # c2d function contributed by Benjamin White, Oct 2012 -def _c2d_matched(sysC, Ts): +def _c2d_matched(sysC, Ts, **kwargs): + if not sysC.issiso(): + raise ControlMIMONotImplemented("Not implemented for MIMO systems") + # Pole-zero match method of continuous to discrete time conversion - szeros, spoles, sgain = tf2zpk(sysC.num[0][0], sysC.den[0][0]) + szeros, spoles, _ = tf2zpk(sysC.num[0][0], sysC.den[0][0]) zzeros = [0] * len(szeros) zpoles = [0] * len(spoles) pregainnum = [0] * len(szeros) @@ -1302,9 +1307,9 @@ def _c2d_matched(sysC, Ts): zpoles[idx] = z pregainden[idx] = 1 - z zgain = np.multiply.reduce(pregainnum) / np.multiply.reduce(pregainden) - gain = sgain / zgain + gain = sysC.dcgain() / zgain sysDnum, sysDden = zpk2tf(zzeros, zpoles, gain) - return TransferFunction(sysDnum, sysDden, Ts) + return TransferFunction(sysDnum, sysDden, Ts, **kwargs) # Utility function to convert a transfer function polynomial to a string
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: