From 29c9432b198a17dc62dc8f793ed17dfc3245a2ca Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Fri, 2 Jun 2023 10:59:45 -0700 Subject: [PATCH] warn if prewarp-frequency is not used and tests, make c2d an identical copy to sample_system instead of a separate function --- control/dtime.py | 71 +++++++--------------------------- control/statesp.py | 15 ++++--- control/tests/discrete_test.py | 49 ++++++++++++++++------- control/tests/kwargs_test.py | 3 +- control/xferfcn.py | 12 ++++-- 5 files changed, 70 insertions(+), 80 deletions(-) diff --git a/control/dtime.py b/control/dtime.py index 6197ae8af..38fcf8056 100644 --- a/control/dtime.py +++ b/control/dtime.py @@ -72,22 +72,12 @@ def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None, 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') - name : string, optional - Set the name of the sampled system. If not specified and - if `copy_names` is `False`, a generic name is generated - with a unique integer id. If `copy_names` is `True`, the new system - name is determined by adding the prefix and suffix strings in - config.defaults['namedio.sampled_system_name_prefix'] and - config.defaults['namedio.sampled_system_name_suffix'], with the - default being to add the suffix '$sampled'. - copy_names : bool, Optional - If True, copy the names of the input signals, output - signals, and states to the sampled system. + time system's magnitude and phase (only valid for method='bilinear', + 'tustin', or 'gbt' with alpha=0.5) Returns ------- - sysd : linsys + sysd : LTI of the same class (:class:`StateSpace` or :class:`TransferFunction`) Discrete time system, with sampling rate Ts Other Parameters @@ -101,6 +91,17 @@ def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None, states : int, list of str, or None, optional Description of the system states. Same format as `inputs`. Only available if the system is :class:`StateSpace`. + name : string, optional + Set the name of the sampled system. If not specified and + if `copy_names` is `False`, a generic name is generated + with a unique integer id. If `copy_names` is `True`, the new system + name is determined by adding the prefix and suffix strings in + config.defaults['namedio.sampled_system_name_prefix'] and + config.defaults['namedio.sampled_system_name_suffix'], with the + default being to add the suffix '$sampled'. + copy_names : bool, Optional + If True, copy the names of the input signals, output + signals, and states to the sampled system. Notes ----- @@ -126,46 +127,4 @@ def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None, method=method, alpha=alpha, prewarp_frequency=prewarp_frequency, name=name, copy_names=copy_names, **kwargs) - -def c2d(sysc, Ts, method='zoh', prewarp_frequency=None): - """ - Convert a continuous time system to discrete time by sampling - - Parameters - ---------- - sysc : LTI (:class:`StateSpace` or :class:`TransferFunction`) - Continuous time system to be converted - Ts : float > 0 - Sampling period - method : string - Method to use for conversion, e.g. 'bilinear', 'zoh' (default) - prewarp_frequency : real 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') - - Returns - ------- - sysd : LTI of the same class - Discrete time system, with sampling rate Ts - - Notes - ----- - See :meth:`StateSpace.sample` or :meth:`TransferFunction.sample`` for - further details. - - Examples - -------- - >>> Gc = ct.tf([1], [1, 2, 1]) - >>> Gc.isdtime() - False - >>> Gd = ct.sample_system(Gc, 1, method='bilinear') - >>> Gd.isdtime() - True - - """ - - # Call the sample_system() function to do the work - sysd = sample_system(sysc, Ts, - method=method, prewarp_frequency=prewarp_frequency) - - return sysd +c2d = sample_system \ No newline at end of file diff --git a/control/statesp.py b/control/statesp.py index 41f92ae21..4f80bdb2f 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -170,9 +170,9 @@ class StateSpace(LTI): The StateSpace class is used to represent state-space realizations of linear time-invariant (LTI) systems: - + .. math:: - + dx/dt &= A x + B u \\ y &= C x + D u @@ -1368,10 +1368,13 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, """ if not self.isctime(): raise ValueError("System must be continuous time system") - - if (method == 'bilinear' or (method == 'gbt' and alpha == 0.5)) and \ - prewarp_frequency is not None: - Twarp = 2 * np.tan(prewarp_frequency * Ts/2)/prewarp_frequency + if prewarp_frequency is not None: + if method in ('bilinear', 'tustin') or \ + (method == 'gbt' and alpha == 0.5): + Twarp = 2*np.tan(prewarp_frequency*Ts/2)/prewarp_frequency + else: + warn('prewarp_frequency ignored: incompatible conversion') + Twarp = Ts else: Twarp = Ts sys = (self.A, self.B, self.C, self.D) diff --git a/control/tests/discrete_test.py b/control/tests/discrete_test.py index 4cf28a21b..4415fac0c 100644 --- a/control/tests/discrete_test.py +++ b/control/tests/discrete_test.py @@ -376,28 +376,51 @@ def test_sample_system(self, tsys): @pytest.mark.parametrize("plantname", ["siso_ss1c", "siso_tf1c"]) - def test_sample_system_prewarp(self, tsys, plantname): + @pytest.mark.parametrize("wwarp", + [.1, 1, 3]) + @pytest.mark.parametrize("Ts", + [.1, 1]) + @pytest.mark.parametrize("discretization_type", + ['bilinear', 'tustin', 'gbt']) + def test_sample_system_prewarp(self, tsys, plantname, discretization_type, wwarp, Ts): """bilinear approximation with prewarping test""" - wwarp = 50 - Ts = 0.025 # test state space version plant = getattr(tsys, plantname) plant_fr = plant(wwarp * 1j) + alpha = 0.5 if discretization_type == 'gbt' else None - plant_d_warped = plant.sample(Ts, 'bilinear', prewarp_frequency=wwarp) + plant_d_warped = plant.sample(Ts, discretization_type, + prewarp_frequency=wwarp, alpha=alpha) dt = plant_d_warped.dt plant_d_fr = plant_d_warped(np.exp(wwarp * 1.j * dt)) np.testing.assert_array_almost_equal(plant_fr, plant_d_fr) - plant_d_warped = sample_system(plant, Ts, 'bilinear', - prewarp_frequency=wwarp) + plant_d_warped = sample_system(plant, Ts, discretization_type, + prewarp_frequency=wwarp, alpha=alpha) plant_d_fr = plant_d_warped(np.exp(wwarp * 1.j * dt)) np.testing.assert_array_almost_equal(plant_fr, plant_d_fr) - plant_d_warped = c2d(plant, Ts, 'bilinear', prewarp_frequency=wwarp) + plant_d_warped = c2d(plant, Ts, discretization_type, + prewarp_frequency=wwarp, alpha=alpha) plant_d_fr = plant_d_warped(np.exp(wwarp * 1.j * dt)) np.testing.assert_array_almost_equal(plant_fr, plant_d_fr) + @pytest.mark.parametrize("plantname", + ["siso_ss1c", + "siso_tf1c"]) + @pytest.mark.parametrize("discretization_type", + ['euler', 'backward_diff', 'zoh']) + def test_sample_system_prewarp_warning(self, tsys, plantname, discretization_type): + plant = getattr(tsys, plantname) + wwarp = 1 + Ts = 0.1 + with pytest.warns(UserWarning, match="prewarp_frequency ignored: incompatible conversion"): + plant_d_warped = plant.sample(Ts, discretization_type, prewarp_frequency=wwarp) + with pytest.warns(UserWarning, match="prewarp_frequency ignored: incompatible conversion"): + plant_d_warped = sample_system(plant, Ts, discretization_type, prewarp_frequency=wwarp) + with pytest.warns(UserWarning, match="prewarp_frequency ignored: incompatible conversion"): + plant_d_warped = c2d(plant, Ts, discretization_type, prewarp_frequency=wwarp) + def test_sample_system_errors(self, tsys): # Check errors with pytest.raises(ValueError): @@ -446,11 +469,11 @@ def test_discrete_bode(self, tsys): np.testing.assert_array_almost_equal(omega, omega_out) np.testing.assert_array_almost_equal(mag_out, np.absolute(H_z)) np.testing.assert_array_almost_equal(phase_out, np.angle(H_z)) - + def test_signal_names(self, tsys): "test that signal names are preserved in conversion to discrete-time" - ssc = StateSpace(tsys.siso_ss1c, - inputs='u', outputs='y', states=['a', 'b', 'c']) + ssc = StateSpace(tsys.siso_ss1c, + inputs='u', outputs='y', states=['a', 'b', 'c']) ssd = ssc.sample(0.1) tfc = TransferFunction(tsys.siso_tf1c, inputs='u', outputs='y') tfd = tfc.sample(0.1) @@ -467,7 +490,7 @@ def test_signal_names(self, tsys): assert ssd.output_labels == ['y'] assert tfd.input_labels == ['u'] assert tfd.output_labels == ['y'] - + # system names and signal name override sysc = StateSpace(1.1, 1, 1, 1, inputs='u', outputs='y', states='a') @@ -488,14 +511,14 @@ def test_signal_names(self, tsys): assert sysd_nocopy.find_state('a') is None # if signal names are provided, they should override those of sysc - sysd_newnames = sample_system(sysc, 0.1, + sysd_newnames = sample_system(sysc, 0.1, inputs='v', outputs='x', states='b') assert sysd_newnames.find_input('v') == 0 assert sysd_newnames.find_input('u') is None assert sysd_newnames.find_output('x') == 0 assert sysd_newnames.find_output('y') is None assert sysd_newnames.find_state('b') == 0 - assert sysd_newnames.find_state('a') is None + assert sysd_newnames.find_state('a') is None # test just one name sysd_newnames = sample_system(sysc, 0.1, inputs='v') assert sysd_newnames.find_input('v') == 0 diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index f94009549..83026391c 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -190,6 +190,7 @@ def test_matplotlib_kwargs(function, nsysargs, moreargs, kwargs, mplcleanup): 'tf2io' : test_unrecognized_kwargs, 'tf2ss' : test_unrecognized_kwargs, 'sample_system' : test_unrecognized_kwargs, + 'c2d' : test_unrecognized_kwargs, 'zpk': test_unrecognized_kwargs, 'flatsys.point_to_point': flatsys_test.TestFlatSys.test_point_to_point_errors, @@ -210,7 +211,7 @@ def test_matplotlib_kwargs(function, nsysargs, moreargs, kwargs, mplcleanup): 'NonlinearIOSystem.__init__': interconnect_test.test_interconnect_exceptions, 'StateSpace.__init__': test_unrecognized_kwargs, - 'StateSpace.sample': test_unrecognized_kwargs, + 'StateSpace.sample': test_unrecognized_kwargs, 'TimeResponseData.__call__': trdata_test.test_response_copy, 'TransferFunction.__init__': test_unrecognized_kwargs, 'TransferFunction.sample': test_unrecognized_kwargs, diff --git a/control/xferfcn.py b/control/xferfcn.py index a6a00c5d7..8ef7e1084 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1134,7 +1134,7 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, Method to use for sampling: * gbt: generalized bilinear transformation - * bilinear: Tustin's approximation ("gbt" with alpha=0.5) + * bilinear or tustin: Tustin's approximation ("gbt" with alpha=0.5) * euler: Euler (or forward difference) method ("gbt" with alpha=0) * backward_diff: Backwards difference ("gbt" with alpha=1.0) * zoh: zero-order hold (default) @@ -1192,9 +1192,13 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, if method == "matched": return _c2d_matched(self, Ts) sys = (self.num[0][0], self.den[0][0]) - if (method == 'bilinear' or (method == 'gbt' and alpha == 0.5)) and \ - prewarp_frequency is not None: - Twarp = 2*np.tan(prewarp_frequency*Ts/2)/prewarp_frequency + if prewarp_frequency is not None: + if method in ('bilinear', 'tustin') or \ + (method == 'gbt' and alpha == 0.5): + Twarp = 2*np.tan(prewarp_frequency*Ts/2)/prewarp_frequency + else: + warn('prewarp_frequency ignored: incompatible conversion') + Twarp = Ts else: Twarp = Ts numd, dend, _ = cont2discrete(sys, Twarp, method, alpha)