|
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,39 @@ def _dcgain(self, warn_infinite):
|
202 | 202 | else:
|
203 | 203 | return zeroresp
|
204 | 204 |
|
| 205 | + def bandwidth(self, dbdrop=-3): |
| 206 | + """Return the bandwidth""" |
| 207 | + raise NotImplementedError("bandwidth not implemented for %s objects" % |
| 208 | + str(self.__class__)) |
| 209 | + |
| 210 | + def _bandwidth(self, dbdrop=-3): |
| 211 | + # check if system is SISO and dbdrop is a negative scalar |
| 212 | + if (not self.issiso()) and (dbdrop >= 0): |
| 213 | + raise ValueError("NOT sure what to raise #TODO ") |
| 214 | + |
| 215 | + # result = scipy.optimize.root(lambda w: np.abs(self(w*1j)) - np.abs(self.dcgain())*10**(dbdrop/20), x0=1) |
| 216 | + # # this will probabily fail if there is a resonant frequency larger than the bandwidth, the initial guess can be around that peak |
| 217 | + |
| 218 | + # use bodeplot to identify the 0-crossing bracket |
| 219 | + from control.freqplot import _default_frequency_range |
| 220 | + omega = _default_frequency_range(self) |
| 221 | + mag, phase, omega = self.frequency_response(omega) |
| 222 | + |
| 223 | + dcgain = self.dcgain() |
| 224 | + idx_out = np.nonzero(mag - dcgain*10**(dbdrop/20) < 0)[0][0] |
| 225 | + |
| 226 | + # solve for the bandwidth, use scipy.optimize.root_scalar() to solve using bisection |
| 227 | + import scipy |
| 228 | + result = scipy.optimize.root_scalar(lambda w: np.abs(self(w*1j)) - np.abs(dcgain)*10**(dbdrop/20), |
| 229 | + bracket=[omega[idx_out-1], omega[idx_out]], |
| 230 | + method='bisect') |
| 231 | + |
| 232 | + # check solution |
| 233 | + if result.converged: |
| 234 | + return np.abs(result.root) |
| 235 | + else: |
| 236 | + raise Exception(result.message) |
| 237 | + |
205 | 238 | def ispassive(self):
|
206 | 239 | # importing here prevents circular dependancy
|
207 | 240 | from control.passivity import ispassive
|
@@ -499,6 +532,33 @@ def dcgain(sys):
|
499 | 532 | return sys.dcgain()
|
500 | 533 |
|
501 | 534 |
|
| 535 | +def bandwidth(sys, dbdrop=-3): |
| 536 | + """Return the first freqency where the gain drop by dbdrop of the system. |
| 537 | +
|
| 538 | + Parameters |
| 539 | + ---------- |
| 540 | + sys: StateSpace or TransferFunction |
| 541 | + Linear system |
| 542 | + dbdrop : float, optional |
| 543 | + By how much the gain drop in dB (default = -3) that defines the |
| 544 | + bandwidth. Should be a negative scalar |
| 545 | +
|
| 546 | + Returns |
| 547 | + ------- |
| 548 | + bandwidth : #TODO data-type |
| 549 | + The first frequency where the gain drops below dbdrop of the dc gain |
| 550 | + of the system. |
| 551 | +
|
| 552 | + Example |
| 553 | + ------- |
| 554 | + >>> G = ct.tf([1], [1, 2]) |
| 555 | + >>> ct.bandwidth(G) |
| 556 | + 0.9976 |
| 557 | +
|
| 558 | + """ |
| 559 | + return sys.bandwidth(dbdrop) |
| 560 | + |
| 561 | + |
502 | 562 | # Process frequency responses in a uniform way
|
503 | 563 | def _process_frequency_response(sys, omega, out, squeeze=None):
|
504 | 564 | # Set value of squeeze argument if not set
|
|
0 commit comments