Skip to content

warn if prewarp-frequency is not used #900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 15 additions & 56 deletions control/dtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <sys[id]> 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
Expand All @@ -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 <sys[id]> 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
-----
Expand All @@ -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
15 changes: 9 additions & 6 deletions control/statesp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
49 changes: 36 additions & 13 deletions control/tests/discrete_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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')

Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion control/tests/kwargs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
12 changes: 8 additions & 4 deletions control/xferfcn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down