From 65988d12cbab5d895767da195a3767d922b1bb6e Mon Sep 17 00:00:00 2001 From: ShihChi Date: Fri, 28 Apr 2023 00:41:26 -0400 Subject: [PATCH 01/13] solve bandwidth by bisection for zero-crossing --- control/lti.py | 62 +++++++++++++++++++++++++++++++++++++++++++++- control/statesp.py | 4 +++ control/xferfcn.py | 4 +++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/control/lti.py b/control/lti.py index 6dc3bc62c..27cf3019d 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,39 @@ def _dcgain(self, warn_infinite): else: return zeroresp + def bandwidth(self, dbdrop=-3): + """Return the bandwidth""" + raise NotImplementedError("bandwidth not implemented for %s objects" % + str(self.__class__)) + + def _bandwidth(self, dbdrop=-3): + # check if system is SISO and dbdrop is a negative scalar + if (not self.issiso()) and (dbdrop >= 0): + raise ValueError("NOT sure what to raise #TODO ") + + # result = scipy.optimize.root(lambda w: np.abs(self(w*1j)) - np.abs(self.dcgain())*10**(dbdrop/20), x0=1) + # # this will probabily fail if there is a resonant frequency larger than the bandwidth, the initial guess can be around that peak + + # use bodeplot to identify the 0-crossing bracket + from control.freqplot import _default_frequency_range + omega = _default_frequency_range(self) + mag, phase, omega = self.frequency_response(omega) + + dcgain = self.dcgain() + idx_out = np.nonzero(mag - dcgain*10**(dbdrop/20) < 0)[0][0] + + # 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_out-1], omega[idx_out]], + 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 +532,33 @@ 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 : #TODO data-type + The first frequency where the gain drops below dbdrop of the dc gain + of the system. + + Example + ------- + >>> G = ct.tf([1], [1, 2]) + >>> ct.bandwidth(G) + 0.9976 + + """ + 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/statesp.py b/control/statesp.py index 41f92ae21..0a72f487c 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1424,6 +1424,10 @@ def dcgain(self, warn_infinite=False): """ return self._dcgain(warn_infinite) + def bandwidth(self, dbdrop=-3): + """Return the bandwith""" + return self._bandwidth(dbdrop) + def dynamics(self, t, x, u=None, params=None): """Compute the dynamics of the system diff --git a/control/xferfcn.py b/control/xferfcn.py index 89e2546f8..cfc9c9a5e 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1246,6 +1246,10 @@ def dcgain(self, warn_infinite=False): """ return self._dcgain(warn_infinite) + + def bandwidth(self, dbdrop=-3): + """Return the bandwith""" + return self._bandwidth(dbdrop) def _isstatic(self): """returns True if and only if all of the numerator and denominator From 86ff95c47f648dc859cfab61c8972eeccbd1039b Mon Sep 17 00:00:00 2001 From: ShihChi Date: Sun, 30 Apr 2023 22:05:51 -0400 Subject: [PATCH 02/13] testing suggested method by Kreijstal --- control/lti.py | 26 +++++++++++++++++++++----- control/matlab/__init__.py | 2 +- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/control/lti.py b/control/lti.py index 27cf3019d..f43039c44 100644 --- a/control/lti.py +++ b/control/lti.py @@ -210,10 +210,19 @@ def bandwidth(self, dbdrop=-3): def _bandwidth(self, dbdrop=-3): # check if system is SISO and dbdrop is a negative scalar if (not self.issiso()) and (dbdrop >= 0): - raise ValueError("NOT sure what to raise #TODO ") - + raise ValueError("#TODO ") + + # # # this will probabily fail if there is a resonant frequency larger than the bandwidth, the initial guess can be around that peak + # G1 = ct.tf(0.1, [1, 0.1]) + # wn2 = 0.9 + # zeta2 = 0.001 + # G2 = ct.tf(wn2**2, [1, 2*zeta2*wn2, wn2**2]) + # ct.bandwidth(G1*G2) + # import scipy # result = scipy.optimize.root(lambda w: np.abs(self(w*1j)) - np.abs(self.dcgain())*10**(dbdrop/20), x0=1) - # # this will probabily fail if there is a resonant frequency larger than the bandwidth, the initial guess can be around that peak + + # if result.success: + # return np.abs(result.x)[0] # use bodeplot to identify the 0-crossing bracket from control.freqplot import _default_frequency_range @@ -221,12 +230,12 @@ def _bandwidth(self, dbdrop=-3): mag, phase, omega = self.frequency_response(omega) dcgain = self.dcgain() - idx_out = np.nonzero(mag - dcgain*10**(dbdrop/20) < 0)[0][0] + idx_dropped = np.nonzero(mag - dcgain*10**(dbdrop/20) < 0)[0][0] # 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_out-1], omega[idx_out]], + bracket=[omega[idx_dropped-1], omega[idx_dropped]], method='bisect') # check solution @@ -555,6 +564,13 @@ def bandwidth(sys, dbdrop=-3): >>> 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 + """ return sys.bandwidth(dbdrop) 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 From 70e912f031574167eef683f090dbb59317dff6ac Mon Sep 17 00:00:00 2001 From: ShihChi Date: Sun, 30 Apr 2023 23:00:59 -0400 Subject: [PATCH 03/13] Implemented and passed nominal test --- control/lti.py | 9 ++++++--- control/tests/lti_test.py | 23 ++++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/control/lti.py b/control/lti.py index f43039c44..d43040084 100644 --- a/control/lti.py +++ b/control/lti.py @@ -209,8 +209,11 @@ def bandwidth(self, dbdrop=-3): def _bandwidth(self, dbdrop=-3): # check if system is SISO and dbdrop is a negative scalar - if (not self.issiso()) and (dbdrop >= 0): - raise ValueError("#TODO ") + 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") # # # this will probabily fail if there is a resonant frequency larger than the bandwidth, the initial guess can be around that peak # G1 = ct.tf(0.1, [1, 0.1]) @@ -560,7 +563,7 @@ def bandwidth(sys, dbdrop=-3): Example ------- - >>> G = ct.tf([1], [1, 2]) + >>> G = ct.tf([1], [1, 1]) >>> ct.bandwidth(G) 0.9976 diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index 8e45ea482..bd35e25a6 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,27 @@ 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 if raise exception given 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), From 0c81cb2b9812932096fd7d68c80b1457a02ef083 Mon Sep 17 00:00:00 2001 From: ShihChi Date: Mon, 1 May 2023 00:03:40 -0400 Subject: [PATCH 04/13] Handle integrator, all-pass filters --- control/lti.py | 64 +++++++++++++++++++++------------------ control/tests/lti_test.py | 13 +++++++- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/control/lti.py b/control/lti.py index d43040084..ee3f57f5e 100644 --- a/control/lti.py +++ b/control/lti.py @@ -215,37 +215,31 @@ def _bandwidth(self, dbdrop=-3): if not(np.isscalar(dbdrop)) or dbdrop >= 0: raise ValueError("expecting dbdrop be a negative scalar in dB") - # # # this will probabily fail if there is a resonant frequency larger than the bandwidth, the initial guess can be around that peak - # G1 = ct.tf(0.1, [1, 0.1]) - # wn2 = 0.9 - # zeta2 = 0.001 - # G2 = ct.tf(wn2**2, [1, 2*zeta2*wn2, wn2**2]) - # ct.bandwidth(G1*G2) - # import scipy - # result = scipy.optimize.root(lambda w: np.abs(self(w*1j)) - np.abs(self.dcgain())*10**(dbdrop/20), x0=1) - - # if result.success: - # return np.abs(result.x)[0] - - # use bodeplot to identify the 0-crossing bracket + dcgain = self.dcgain() + if np.isinf(dcgain): + 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] - dcgain = self.dcgain() - idx_dropped = np.nonzero(mag - dcgain*10**(dbdrop/20) < 0)[0][0] - - # 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-1], omega[idx_dropped]], - method='bisect') - - # check solution - if result.converged: - return np.abs(result.root) + if idx_dropped.shape[0] == 0: + # no frequency response is dbdrop below the dc gain. + return np.inf else: - raise Exception(result.message) + # 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 @@ -557,10 +551,17 @@ def bandwidth(sys, dbdrop=-3): Returns ------- - bandwidth : #TODO data-type - The first frequency where the gain drops below dbdrop of the dc gain - of the system. - + 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]) @@ -575,6 +576,9 @@ def bandwidth(sys, dbdrop=-3): 0.1018 """ + if not isinstance(sys, LTI): + raise TypeError("sys must be a LTI instance.") + return sys.bandwidth(dbdrop) diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index bd35e25a6..e0f7f35bf 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -117,7 +117,18 @@ def test_bandwidth(self): np.testing.assert_allclose(sys2.bandwidth(), 0.101848388240241) np.testing.assert_allclose(bandwidth(sys2), 0.101848388240241) - # test if raise exception given other than SISO system + # 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) From d59d19a299d2455e35826f6206715bba6197274c Mon Sep 17 00:00:00 2001 From: ShihChi Date: Mon, 1 May 2023 00:09:45 -0400 Subject: [PATCH 05/13] adjust for PEP8 coding style --- control/lti.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/control/lti.py b/control/lti.py index ee3f57f5e..2cf54cac2 100644 --- a/control/lti.py +++ b/control/lti.py @@ -211,12 +211,13 @@ def _bandwidth(self, dbdrop=-3): # 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: + + 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 @@ -226,14 +227,16 @@ def _bandwidth(self, dbdrop=-3): 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. + # 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 + # 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') + 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: @@ -552,8 +555,9 @@ def bandwidth(sys, dbdrop=-3): 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 + 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 ------ @@ -561,7 +565,7 @@ def bandwidth(sys, dbdrop=-3): if 'sys' is not an SISO LTI instance ValueError if 'dbdrop' is not a negative scalar - + Example ------- >>> G = ct.tf([1], [1, 1]) From bc2ddff637169cea8010794cae7b2488ffa16006 Mon Sep 17 00:00:00 2001 From: ShihChi Date: Fri, 28 Apr 2023 00:41:26 -0400 Subject: [PATCH 06/13] solve bandwidth by bisection for zero-crossing --- control/lti.py | 62 +++++++++++++++++++++++++++++++++++++++++++++- control/statesp.py | 4 +++ control/xferfcn.py | 4 +++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/control/lti.py b/control/lti.py index 6dc3bc62c..27cf3019d 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,39 @@ def _dcgain(self, warn_infinite): else: return zeroresp + def bandwidth(self, dbdrop=-3): + """Return the bandwidth""" + raise NotImplementedError("bandwidth not implemented for %s objects" % + str(self.__class__)) + + def _bandwidth(self, dbdrop=-3): + # check if system is SISO and dbdrop is a negative scalar + if (not self.issiso()) and (dbdrop >= 0): + raise ValueError("NOT sure what to raise #TODO ") + + # result = scipy.optimize.root(lambda w: np.abs(self(w*1j)) - np.abs(self.dcgain())*10**(dbdrop/20), x0=1) + # # this will probabily fail if there is a resonant frequency larger than the bandwidth, the initial guess can be around that peak + + # use bodeplot to identify the 0-crossing bracket + from control.freqplot import _default_frequency_range + omega = _default_frequency_range(self) + mag, phase, omega = self.frequency_response(omega) + + dcgain = self.dcgain() + idx_out = np.nonzero(mag - dcgain*10**(dbdrop/20) < 0)[0][0] + + # 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_out-1], omega[idx_out]], + 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 +532,33 @@ 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 : #TODO data-type + The first frequency where the gain drops below dbdrop of the dc gain + of the system. + + Example + ------- + >>> G = ct.tf([1], [1, 2]) + >>> ct.bandwidth(G) + 0.9976 + + """ + 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/statesp.py b/control/statesp.py index 41f92ae21..0a72f487c 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1424,6 +1424,10 @@ def dcgain(self, warn_infinite=False): """ return self._dcgain(warn_infinite) + def bandwidth(self, dbdrop=-3): + """Return the bandwith""" + return self._bandwidth(dbdrop) + def dynamics(self, t, x, u=None, params=None): """Compute the dynamics of the system diff --git a/control/xferfcn.py b/control/xferfcn.py index 89e2546f8..cfc9c9a5e 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1246,6 +1246,10 @@ def dcgain(self, warn_infinite=False): """ return self._dcgain(warn_infinite) + + def bandwidth(self, dbdrop=-3): + """Return the bandwith""" + return self._bandwidth(dbdrop) def _isstatic(self): """returns True if and only if all of the numerator and denominator From 409d0c62dd7c940d1d5575af229a31fc526e5f90 Mon Sep 17 00:00:00 2001 From: ShihChi Date: Sun, 30 Apr 2023 22:05:51 -0400 Subject: [PATCH 07/13] testing suggested method by Kreijstal --- control/lti.py | 26 +++++++++++++++++++++----- control/matlab/__init__.py | 2 +- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/control/lti.py b/control/lti.py index 27cf3019d..f43039c44 100644 --- a/control/lti.py +++ b/control/lti.py @@ -210,10 +210,19 @@ def bandwidth(self, dbdrop=-3): def _bandwidth(self, dbdrop=-3): # check if system is SISO and dbdrop is a negative scalar if (not self.issiso()) and (dbdrop >= 0): - raise ValueError("NOT sure what to raise #TODO ") - + raise ValueError("#TODO ") + + # # # this will probabily fail if there is a resonant frequency larger than the bandwidth, the initial guess can be around that peak + # G1 = ct.tf(0.1, [1, 0.1]) + # wn2 = 0.9 + # zeta2 = 0.001 + # G2 = ct.tf(wn2**2, [1, 2*zeta2*wn2, wn2**2]) + # ct.bandwidth(G1*G2) + # import scipy # result = scipy.optimize.root(lambda w: np.abs(self(w*1j)) - np.abs(self.dcgain())*10**(dbdrop/20), x0=1) - # # this will probabily fail if there is a resonant frequency larger than the bandwidth, the initial guess can be around that peak + + # if result.success: + # return np.abs(result.x)[0] # use bodeplot to identify the 0-crossing bracket from control.freqplot import _default_frequency_range @@ -221,12 +230,12 @@ def _bandwidth(self, dbdrop=-3): mag, phase, omega = self.frequency_response(omega) dcgain = self.dcgain() - idx_out = np.nonzero(mag - dcgain*10**(dbdrop/20) < 0)[0][0] + idx_dropped = np.nonzero(mag - dcgain*10**(dbdrop/20) < 0)[0][0] # 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_out-1], omega[idx_out]], + bracket=[omega[idx_dropped-1], omega[idx_dropped]], method='bisect') # check solution @@ -555,6 +564,13 @@ def bandwidth(sys, dbdrop=-3): >>> 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 + """ return sys.bandwidth(dbdrop) 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 From 723ddc88781912e858887cdec93154d8c9e26466 Mon Sep 17 00:00:00 2001 From: ShihChi Date: Sun, 30 Apr 2023 23:00:59 -0400 Subject: [PATCH 08/13] Implemented and passed nominal test --- control/lti.py | 9 ++++++--- control/tests/lti_test.py | 23 ++++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/control/lti.py b/control/lti.py index f43039c44..d43040084 100644 --- a/control/lti.py +++ b/control/lti.py @@ -209,8 +209,11 @@ def bandwidth(self, dbdrop=-3): def _bandwidth(self, dbdrop=-3): # check if system is SISO and dbdrop is a negative scalar - if (not self.issiso()) and (dbdrop >= 0): - raise ValueError("#TODO ") + 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") # # # this will probabily fail if there is a resonant frequency larger than the bandwidth, the initial guess can be around that peak # G1 = ct.tf(0.1, [1, 0.1]) @@ -560,7 +563,7 @@ def bandwidth(sys, dbdrop=-3): Example ------- - >>> G = ct.tf([1], [1, 2]) + >>> G = ct.tf([1], [1, 1]) >>> ct.bandwidth(G) 0.9976 diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index 8e45ea482..bd35e25a6 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,27 @@ 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 if raise exception given 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), From 6b5143236212bf5d9ab4c3fdf82c6299a3e27e55 Mon Sep 17 00:00:00 2001 From: ShihChi Date: Mon, 1 May 2023 00:03:40 -0400 Subject: [PATCH 09/13] Handle integrator, all-pass filters --- control/lti.py | 64 +++++++++++++++++++++------------------ control/tests/lti_test.py | 13 +++++++- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/control/lti.py b/control/lti.py index d43040084..ee3f57f5e 100644 --- a/control/lti.py +++ b/control/lti.py @@ -215,37 +215,31 @@ def _bandwidth(self, dbdrop=-3): if not(np.isscalar(dbdrop)) or dbdrop >= 0: raise ValueError("expecting dbdrop be a negative scalar in dB") - # # # this will probabily fail if there is a resonant frequency larger than the bandwidth, the initial guess can be around that peak - # G1 = ct.tf(0.1, [1, 0.1]) - # wn2 = 0.9 - # zeta2 = 0.001 - # G2 = ct.tf(wn2**2, [1, 2*zeta2*wn2, wn2**2]) - # ct.bandwidth(G1*G2) - # import scipy - # result = scipy.optimize.root(lambda w: np.abs(self(w*1j)) - np.abs(self.dcgain())*10**(dbdrop/20), x0=1) - - # if result.success: - # return np.abs(result.x)[0] - - # use bodeplot to identify the 0-crossing bracket + dcgain = self.dcgain() + if np.isinf(dcgain): + 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] - dcgain = self.dcgain() - idx_dropped = np.nonzero(mag - dcgain*10**(dbdrop/20) < 0)[0][0] - - # 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-1], omega[idx_dropped]], - method='bisect') - - # check solution - if result.converged: - return np.abs(result.root) + if idx_dropped.shape[0] == 0: + # no frequency response is dbdrop below the dc gain. + return np.inf else: - raise Exception(result.message) + # 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 @@ -557,10 +551,17 @@ def bandwidth(sys, dbdrop=-3): Returns ------- - bandwidth : #TODO data-type - The first frequency where the gain drops below dbdrop of the dc gain - of the system. - + 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]) @@ -575,6 +576,9 @@ def bandwidth(sys, dbdrop=-3): 0.1018 """ + if not isinstance(sys, LTI): + raise TypeError("sys must be a LTI instance.") + return sys.bandwidth(dbdrop) diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index bd35e25a6..e0f7f35bf 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -117,7 +117,18 @@ def test_bandwidth(self): np.testing.assert_allclose(sys2.bandwidth(), 0.101848388240241) np.testing.assert_allclose(bandwidth(sys2), 0.101848388240241) - # test if raise exception given other than SISO system + # 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) From 9f86b41c21e9d81b93921892f875d5940e9da4c8 Mon Sep 17 00:00:00 2001 From: ShihChi Date: Mon, 1 May 2023 00:09:45 -0400 Subject: [PATCH 10/13] adjust for PEP8 coding style --- control/lti.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/control/lti.py b/control/lti.py index ee3f57f5e..2cf54cac2 100644 --- a/control/lti.py +++ b/control/lti.py @@ -211,12 +211,13 @@ def _bandwidth(self, dbdrop=-3): # 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: + + 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 @@ -226,14 +227,16 @@ def _bandwidth(self, dbdrop=-3): 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. + # 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 + # 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') + 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: @@ -552,8 +555,9 @@ def bandwidth(sys, dbdrop=-3): 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 + 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 ------ @@ -561,7 +565,7 @@ def bandwidth(sys, dbdrop=-3): if 'sys' is not an SISO LTI instance ValueError if 'dbdrop' is not a negative scalar - + Example ------- >>> G = ct.tf([1], [1, 1]) From a370cdb65e7038cae6ffe88f881dba00ff87b7d9 Mon Sep 17 00:00:00 2001 From: ShihChi Date: Wed, 3 May 2023 23:28:59 -0400 Subject: [PATCH 11/13] remove _bandwidth method in lti.py --- control/lti.py | 5 ----- control/statesp.py | 4 ---- control/xferfcn.py | 4 ---- 3 files changed, 13 deletions(-) diff --git a/control/lti.py b/control/lti.py index 2cf54cac2..ec552e9ae 100644 --- a/control/lti.py +++ b/control/lti.py @@ -203,11 +203,6 @@ def _dcgain(self, warn_infinite): return zeroresp def bandwidth(self, dbdrop=-3): - """Return the bandwidth""" - raise NotImplementedError("bandwidth not implemented for %s objects" % - str(self.__class__)) - - def _bandwidth(self, dbdrop=-3): # check if system is SISO and dbdrop is a negative scalar if not self.issiso(): raise TypeError("system should be a SISO system") diff --git a/control/statesp.py b/control/statesp.py index 0a72f487c..41f92ae21 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1424,10 +1424,6 @@ def dcgain(self, warn_infinite=False): """ return self._dcgain(warn_infinite) - def bandwidth(self, dbdrop=-3): - """Return the bandwith""" - return self._bandwidth(dbdrop) - def dynamics(self, t, x, u=None, params=None): """Compute the dynamics of the system diff --git a/control/xferfcn.py b/control/xferfcn.py index cfc9c9a5e..96be243df 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1247,10 +1247,6 @@ def dcgain(self, warn_infinite=False): """ return self._dcgain(warn_infinite) - def bandwidth(self, dbdrop=-3): - """Return the bandwith""" - return self._bandwidth(dbdrop) - def _isstatic(self): """returns True if and only if all of the numerator and denominator polynomials of the (possibly MIMO) transfer function are zeroth order, From 02172b7a2238adc0b4c49bfefdfb475368984d7e Mon Sep 17 00:00:00 2001 From: ShihChi Date: Sun, 7 May 2023 19:27:23 -0400 Subject: [PATCH 12/13] resolving format issues in xfrefcn.py --- control/xferfcn.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/control/xferfcn.py b/control/xferfcn.py index 96be243df..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}}" @@ -1246,7 +1247,7 @@ def dcgain(self, warn_infinite=False): """ return self._dcgain(warn_infinite) - + def _isstatic(self): """returns True if and only if all of the numerator and denominator polynomials of the (possibly MIMO) transfer function are zeroth order, @@ -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 From ab685623a40853d66d2ac617545307e75ef50a80 Mon Sep 17 00:00:00 2001 From: ShihChi Date: Sun, 7 May 2023 20:29:30 -0400 Subject: [PATCH 13/13] add docstring to lti.bandwidth --- control/lti.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/control/lti.py b/control/lti.py index ec552e9ae..216332e91 100644 --- a/control/lti.py +++ b/control/lti.py @@ -203,6 +203,31 @@ def _dcgain(self, warn_infinite): 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") pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy