|
20 | 20 | from .namedio import NamedIOSystem, isdtime
|
21 | 21 |
|
22 | 22 | __all__ = ['poles', 'zeros', 'damp', 'evalfr', 'frequency_response',
|
23 |
| - 'freqresp', 'dcgain', 'pole', 'zero'] |
| 23 | + 'freqresp', 'dcgain', 'bandwidth', 'pole', 'zero'] |
24 | 24 |
|
25 | 25 |
|
26 | 26 | class LTI(NamedIOSystem):
|
@@ -202,6 +202,68 @@ def _dcgain(self, warn_infinite):
|
202 | 202 | else:
|
203 | 203 | return zeroresp
|
204 | 204 |
|
| 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 | + |
205 | 267 | def ispassive(self):
|
206 | 268 | # importing here prevents circular dependancy
|
207 | 269 | from control.passivity import ispassive
|
@@ -499,6 +561,51 @@ def dcgain(sys):
|
499 | 561 | return sys.dcgain()
|
500 | 562 |
|
501 | 563 |
|
| 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 | + |
502 | 609 | # Process frequency responses in a uniform way
|
503 | 610 | def _process_frequency_response(sys, omega, out, squeeze=None):
|
504 | 611 | # Set value of squeeze argument if not set
|
|
0 commit comments