Skip to content

bandwidth feature #889

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 14 commits into from
May 15, 2023
109 changes: 108 additions & 1 deletion control/lti.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from .namedio import NamedIOSystem, isdtime

__all__ = ['poles', 'zeros', 'damp', 'evalfr', 'frequency_response',
'freqresp', 'dcgain', 'pole', 'zero']
'freqresp', 'dcgain', 'bandwidth', 'pole', 'zero']


class LTI(NamedIOSystem):
Expand Down Expand Up @@ -202,6 +202,68 @@ def _dcgain(self, warn_infinite):
else:
return zeroresp

def bandwidth(self, dbdrop=-3):
"""Evaluate the bandwidth of the LTI system for a given dB drop.

Evaluate the first frequency that the response magnitude is lower than
DC gain by dbdrop dB.

Parameters
----------
dpdrop : float, optional
A strictly negative scalar in dB (default = -3) defines the
amount of gain drop for deciding bandwidth.

Returns
-------
bandwidth : ndarray
The first frequency (rad/time-unit) where the gain drops below
dbdrop of the dc gain of the system, or nan if the system has
infinite dc gain, inf if the gain does not drop for all frequency

Raises
------
TypeError
if 'sys' is not an SISO LTI instance
ValueError
if 'dbdrop' is not a negative scalar
"""
# check if system is SISO and dbdrop is a negative scalar
if not self.issiso():
raise TypeError("system should be a SISO system")

if (not np.isscalar(dbdrop)) or dbdrop >= 0:
raise ValueError("expecting dbdrop be a negative scalar in dB")

dcgain = self.dcgain()
if np.isinf(dcgain):
# infinite dcgain, return np.nan
return np.nan

# use frequency range to identify the 0-crossing (dbdrop) bracket
from control.freqplot import _default_frequency_range
omega = _default_frequency_range(self)
mag, phase, omega = self.frequency_response(omega)
idx_dropped = np.nonzero(mag - dcgain*10**(dbdrop/20) < 0)[0]

if idx_dropped.shape[0] == 0:
# no frequency response is dbdrop below the dc gain, return np.inf
return np.inf
else:
# solve for the bandwidth, use scipy.optimize.root_scalar() to
# solve using bisection
import scipy
result = scipy.optimize.root_scalar(
lambda w: np.abs(self(w*1j)) - np.abs(dcgain)*10**(dbdrop/20),
bracket=[omega[idx_dropped[0] - 1], omega[idx_dropped[0]]],
method='bisect')

# check solution
if result.converged:
return np.abs(result.root)
else:
raise Exception(result.message)

def ispassive(self):
# importing here prevents circular dependancy
from control.passivity import ispassive
Expand Down Expand Up @@ -499,6 +561,51 @@ def dcgain(sys):
return sys.dcgain()


def bandwidth(sys, dbdrop=-3):
"""Return the first freqency where the gain drop by dbdrop of the system.

Parameters
----------
sys: StateSpace or TransferFunction
Linear system
dbdrop : float, optional
By how much the gain drop in dB (default = -3) that defines the
bandwidth. Should be a negative scalar

Returns
-------
bandwidth : ndarray
The first frequency (rad/time-unit) where the gain drops below dbdrop
of the dc gain of the system, or nan if the system has infinite dc
gain, inf if the gain does not drop for all frequency

Raises
------
TypeError
if 'sys' is not an SISO LTI instance
ValueError
if 'dbdrop' is not a negative scalar

Example
-------
>>> G = ct.tf([1], [1, 1])
>>> ct.bandwidth(G)
0.9976

>>> G1 = ct.tf(0.1, [1, 0.1])
>>> wn2 = 1
>>> zeta2 = 0.001
>>> G2 = ct.tf(wn2**2, [1, 2*zeta2*wn2, wn2**2])
>>> ct.bandwidth(G1*G2)
0.1018

"""
if not isinstance(sys, LTI):
raise TypeError("sys must be a LTI instance.")

return sys.bandwidth(dbdrop)


# Process frequency responses in a uniform way
def _process_frequency_response(sys, omega, out, squeeze=None):
# Set value of squeeze argument if not set
Expand Down
2 changes: 1 addition & 1 deletion control/matlab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@

== ========================== ============================================
\* :func:`dcgain` steady-state (D.C.) gain
\ lti/bandwidth system bandwidth
\* :func:`bandwidth` system bandwidth
\ lti/norm h2 and Hinfinity norms of LTI models
\* :func:`pole` system poles
\* :func:`zero` system (transmission) zeros
Expand Down
34 changes: 33 additions & 1 deletion control/tests/lti_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import control as ct
from control import c2d, tf, ss, tf2ss, NonlinearIOSystem
from control.lti import LTI, evalfr, damp, dcgain, zeros, poles
from control.lti import LTI, evalfr, damp, dcgain, zeros, poles, bandwidth
from control import common_timebase, isctime, isdtime, issiso, timebaseEqual
from control.tests.conftest import slycotonly
from control.exception import slycot_check
Expand Down Expand Up @@ -104,6 +104,38 @@ def test_dcgain(self):
np.testing.assert_allclose(sys.dcgain(), 42)
np.testing.assert_allclose(dcgain(sys), 42)

def test_bandwidth(self):
# test a first-order system, compared with matlab
sys1 = tf(0.1, [1, 0.1])
np.testing.assert_allclose(sys1.bandwidth(), 0.099762834511098)
np.testing.assert_allclose(bandwidth(sys1), 0.099762834511098)

# test a second-order system, compared with matlab
wn2 = 1
zeta2 = 0.001
sys2 = sys1 * tf(wn2**2, [1, 2*zeta2*wn2, wn2**2])
np.testing.assert_allclose(sys2.bandwidth(), 0.101848388240241)
np.testing.assert_allclose(bandwidth(sys2), 0.101848388240241)

# test constant gain, bandwidth should be infinity
sysAP = tf(1,1)
np.testing.assert_allclose(bandwidth(sysAP), np.inf)

# test integrator, bandwidth should return np.nan
sysInt = tf(1, [1, 0])
np.testing.assert_allclose(bandwidth(sysInt), np.nan)

# test exception for system other than LTI
np.testing.assert_raises(TypeError, bandwidth, 1)

# test exception for system other than SISO system
sysMIMO = tf([[[-1, 41], [1]], [[1, 2], [3, 4]]],
[[[1, 10], [1, 20]], [[1, 30], [1, 40]]])
np.testing.assert_raises(TypeError, bandwidth, sysMIMO)

# test if raise exception if dbdrop is positive scalar
np.testing.assert_raises(ValueError, bandwidth, sys1, 3)

@pytest.mark.parametrize("dt1, dt2, expected",
[(None, None, True),
(None, 0, True),
Expand Down
2 changes: 2 additions & 0 deletions control/xferfcn.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
'xferfcn.floating_point_format': '.4g'
}


def _float2str(value):
_num_format = config.defaults.get('xferfcn.floating_point_format', ':.4g')
return f"{value:{_num_format}}"
Expand Down Expand Up @@ -1407,6 +1408,7 @@ def _tf_factorized_polynomial_to_string(roots, gain=1, var='s'):

return multiplier + " ".join(factors)


def _tf_string_to_latex(thestr, var='s'):
""" make sure to superscript all digits in a polynomial string
and convert float coefficients in scientific notation
Expand Down