Skip to content

Commit 15ed92c

Browse files
authored
Merge pull request #889 from SCLiao47/feature_bandwidth
bandwidth feature
2 parents c06a205 + ab68562 commit 15ed92c

File tree

4 files changed

+144
-3
lines changed

4 files changed

+144
-3
lines changed

control/lti.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from .namedio import NamedIOSystem, isdtime
2121

2222
__all__ = ['poles', 'zeros', 'damp', 'evalfr', 'frequency_response',
23-
'freqresp', 'dcgain', 'pole', 'zero']
23+
'freqresp', 'dcgain', 'bandwidth', 'pole', 'zero']
2424

2525

2626
class LTI(NamedIOSystem):
@@ -202,6 +202,68 @@ def _dcgain(self, warn_infinite):
202202
else:
203203
return zeroresp
204204

205+
def bandwidth(self, dbdrop=-3):
206+
"""Evaluate the bandwidth of the LTI system for a given dB drop.
207+
208+
Evaluate the first frequency that the response magnitude is lower than
209+
DC gain by dbdrop dB.
210+
211+
Parameters
212+
----------
213+
dpdrop : float, optional
214+
A strictly negative scalar in dB (default = -3) defines the
215+
amount of gain drop for deciding bandwidth.
216+
217+
Returns
218+
-------
219+
bandwidth : ndarray
220+
The first frequency (rad/time-unit) where the gain drops below
221+
dbdrop of the dc gain of the system, or nan if the system has
222+
infinite dc gain, inf if the gain does not drop for all frequency
223+
224+
Raises
225+
------
226+
TypeError
227+
if 'sys' is not an SISO LTI instance
228+
ValueError
229+
if 'dbdrop' is not a negative scalar
230+
"""
231+
# check if system is SISO and dbdrop is a negative scalar
232+
if not self.issiso():
233+
raise TypeError("system should be a SISO system")
234+
235+
if (not np.isscalar(dbdrop)) or dbdrop >= 0:
236+
raise ValueError("expecting dbdrop be a negative scalar in dB")
237+
238+
dcgain = self.dcgain()
239+
if np.isinf(dcgain):
240+
# infinite dcgain, return np.nan
241+
return np.nan
242+
243+
# use frequency range to identify the 0-crossing (dbdrop) bracket
244+
from control.freqplot import _default_frequency_range
245+
omega = _default_frequency_range(self)
246+
mag, phase, omega = self.frequency_response(omega)
247+
idx_dropped = np.nonzero(mag - dcgain*10**(dbdrop/20) < 0)[0]
248+
249+
if idx_dropped.shape[0] == 0:
250+
# no frequency response is dbdrop below the dc gain, return np.inf
251+
return np.inf
252+
else:
253+
# solve for the bandwidth, use scipy.optimize.root_scalar() to
254+
# solve using bisection
255+
import scipy
256+
result = scipy.optimize.root_scalar(
257+
lambda w: np.abs(self(w*1j)) - np.abs(dcgain)*10**(dbdrop/20),
258+
bracket=[omega[idx_dropped[0] - 1], omega[idx_dropped[0]]],
259+
method='bisect')
260+
261+
# check solution
262+
if result.converged:
263+
return np.abs(result.root)
264+
else:
265+
raise Exception(result.message)
266+
205267
def ispassive(self):
206268
# importing here prevents circular dependancy
207269
from control.passivity import ispassive
@@ -499,6 +561,51 @@ def dcgain(sys):
499561
return sys.dcgain()
500562

501563

564+
def bandwidth(sys, dbdrop=-3):
565+
"""Return the first freqency where the gain drop by dbdrop of the system.
566+
567+
Parameters
568+
----------
569+
sys: StateSpace or TransferFunction
570+
Linear system
571+
dbdrop : float, optional
572+
By how much the gain drop in dB (default = -3) that defines the
573+
bandwidth. Should be a negative scalar
574+
575+
Returns
576+
-------
577+
bandwidth : ndarray
578+
The first frequency (rad/time-unit) where the gain drops below dbdrop
579+
of the dc gain of the system, or nan if the system has infinite dc
580+
gain, inf if the gain does not drop for all frequency
581+
582+
Raises
583+
------
584+
TypeError
585+
if 'sys' is not an SISO LTI instance
586+
ValueError
587+
if 'dbdrop' is not a negative scalar
588+
589+
Example
590+
-------
591+
>>> G = ct.tf([1], [1, 1])
592+
>>> ct.bandwidth(G)
593+
0.9976
594+
595+
>>> G1 = ct.tf(0.1, [1, 0.1])
596+
>>> wn2 = 1
597+
>>> zeta2 = 0.001
598+
>>> G2 = ct.tf(wn2**2, [1, 2*zeta2*wn2, wn2**2])
599+
>>> ct.bandwidth(G1*G2)
600+
0.1018
601+
602+
"""
603+
if not isinstance(sys, LTI):
604+
raise TypeError("sys must be a LTI instance.")
605+
606+
return sys.bandwidth(dbdrop)
607+
608+
502609
# Process frequency responses in a uniform way
503610
def _process_frequency_response(sys, omega, out, squeeze=None):
504611
# Set value of squeeze argument if not set

control/matlab/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@
189189
190190
== ========================== ============================================
191191
\* :func:`dcgain` steady-state (D.C.) gain
192-
\ lti/bandwidth system bandwidth
192+
\* :func:`bandwidth` system bandwidth
193193
\ lti/norm h2 and Hinfinity norms of LTI models
194194
\* :func:`pole` system poles
195195
\* :func:`zero` system (transmission) zeros

control/tests/lti_test.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import control as ct
88
from control import c2d, tf, ss, tf2ss, NonlinearIOSystem
9-
from control.lti import LTI, evalfr, damp, dcgain, zeros, poles
9+
from control.lti import LTI, evalfr, damp, dcgain, zeros, poles, bandwidth
1010
from control import common_timebase, isctime, isdtime, issiso, timebaseEqual
1111
from control.tests.conftest import slycotonly
1212
from control.exception import slycot_check
@@ -104,6 +104,38 @@ def test_dcgain(self):
104104
np.testing.assert_allclose(sys.dcgain(), 42)
105105
np.testing.assert_allclose(dcgain(sys), 42)
106106

107+
def test_bandwidth(self):
108+
# test a first-order system, compared with matlab
109+
sys1 = tf(0.1, [1, 0.1])
110+
np.testing.assert_allclose(sys1.bandwidth(), 0.099762834511098)
111+
np.testing.assert_allclose(bandwidth(sys1), 0.099762834511098)
112+
113+
# test a second-order system, compared with matlab
114+
wn2 = 1
115+
zeta2 = 0.001
116+
sys2 = sys1 * tf(wn2**2, [1, 2*zeta2*wn2, wn2**2])
117+
np.testing.assert_allclose(sys2.bandwidth(), 0.101848388240241)
118+
np.testing.assert_allclose(bandwidth(sys2), 0.101848388240241)
119+
120+
# test constant gain, bandwidth should be infinity
121+
sysAP = tf(1,1)
122+
np.testing.assert_allclose(bandwidth(sysAP), np.inf)
123+
124+
# test integrator, bandwidth should return np.nan
125+
sysInt = tf(1, [1, 0])
126+
np.testing.assert_allclose(bandwidth(sysInt), np.nan)
127+
128+
# test exception for system other than LTI
129+
np.testing.assert_raises(TypeError, bandwidth, 1)
130+
131+
# test exception for system other than SISO system
132+
sysMIMO = tf([[[-1, 41], [1]], [[1, 2], [3, 4]]],
133+
[[[1, 10], [1, 20]], [[1, 30], [1, 40]]])
134+
np.testing.assert_raises(TypeError, bandwidth, sysMIMO)
135+
136+
# test if raise exception if dbdrop is positive scalar
137+
np.testing.assert_raises(ValueError, bandwidth, sys1, 3)
138+
107139
@pytest.mark.parametrize("dt1, dt2, expected",
108140
[(None, None, True),
109141
(None, 0, True),

control/xferfcn.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
'xferfcn.floating_point_format': '.4g'
7575
}
7676

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

14081409
return multiplier + " ".join(factors)
14091410

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

0 commit comments

Comments
 (0)
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