diff --git a/control/lti.py b/control/lti.py index 6dc3bc62c..216332e91 100644 --- a/control/lti.py +++ b/control/lti.py @@ -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): @@ -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 @@ -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 diff --git a/control/matlab/__init__.py b/control/matlab/__init__.py index 1a524b33f..ef14248c0 100644 --- a/control/matlab/__init__.py +++ b/control/matlab/__init__.py @@ -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 diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index 8e45ea482..e0f7f35bf 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -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 @@ -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), diff --git a/control/xferfcn.py b/control/xferfcn.py index 89e2546f8..a6a00c5d7 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -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}}" @@ -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
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: