|
56 | 56 | from . import xferfcn |
57 | 57 | from .lti import issiso, evalfr |
58 | 58 | from . import frdata |
| 59 | +from . import freqplot |
59 | 60 | from .exception import ControlMIMONotImplemented |
60 | 61 |
|
61 | 62 | __all__ = ['stability_margins', 'phase_crossover_frequencies', 'margin'] |
@@ -206,6 +207,17 @@ def fun(wdt): |
206 | 207 |
|
207 | 208 | return z, w |
208 | 209 |
|
| 210 | +def _numerical_inaccuracy(sys): |
| 211 | + # crude, conservative check for if |
| 212 | + # num(z)*num(1/z) << den(z)*den(1/z) for DT systems |
| 213 | + num, den, num_inv_zp, den_inv_zq, p_q, dt = _poly_z_invz(sys) |
| 214 | + p1 = np.polymul(num, num_inv_zp) |
| 215 | + p2 = np.polymul(den, den_inv_zq) |
| 216 | + if p_q < 0: |
| 217 | + # * z**(-p_q) |
| 218 | + x = [1] + [0] * (-p_q) |
| 219 | + p1 = np.polymul(p1, x) |
| 220 | + return np.linalg.norm(p1) < 1e-3 * np.linalg.norm(p2) |
209 | 221 |
|
210 | 222 | # Took the framework for the old function by |
211 | 223 | # Sawyer B. Fuller <minster@uw.edu>, removed a lot of the innards |
@@ -237,25 +249,34 @@ def fun(wdt): |
237 | 249 | # systems |
238 | 250 |
|
239 | 251 |
|
240 | | -def stability_margins(sysdata, returnall=False, epsw=0.0): |
| 252 | +def stability_margins(sysdata, returnall=False, epsw=0.0, method='best'): |
241 | 253 | """Calculate stability margins and associated crossover frequencies. |
242 | 254 |
|
243 | 255 | Parameters |
244 | 256 | ---------- |
245 | | - sysdata: LTI system or (mag, phase, omega) sequence |
| 257 | + sysdata : LTI system or (mag, phase, omega) sequence |
246 | 258 | sys : LTI system |
247 | 259 | Linear SISO system representing the loop transfer function |
248 | 260 | mag, phase, omega : sequence of array_like |
249 | 261 | Arrays of magnitudes (absolute values, not dB), phases (degrees), |
250 | 262 | and corresponding frequencies. Crossover frequencies returned are |
251 | 263 | in the same units as those in `omega` (e.g., rad/sec or Hz). |
252 | | - returnall: bool, optional |
| 264 | + returnall : bool, optional |
253 | 265 | If true, return all margins found. If False (default), return only the |
254 | 266 | minimum stability margins. For frequency data or FRD systems, only |
255 | 267 | margins in the given frequency region can be found and returned. |
256 | | - epsw: float, optional |
| 268 | + epsw : float, optional |
257 | 269 | Frequencies below this value (default 0.0) are considered static gain, |
258 | 270 | and not returned as margin. |
| 271 | + method : string, optional |
| 272 | + Method to use (default is 'best'): |
| 273 | + 'poly': use polynomial method if passed a :class:`LTI` system. |
| 274 | + 'frd': calculate crossover frequencies using numerical interpolation |
| 275 | + of a :class:`FrequencyResponseData` representation of the system if |
| 276 | + passed a :class:`LTI` system. |
| 277 | + 'best': use the 'poly' method if possible, reverting to 'frd' if it is |
| 278 | + detected that numerical inaccuracy is likey to arise in the 'poly' |
| 279 | + method for for discrete-time systems. |
259 | 280 |
|
260 | 281 | Returns |
261 | 282 | ------- |
@@ -300,6 +321,27 @@ def stability_margins(sysdata, returnall=False, epsw=0.0): |
300 | 321 | raise ControlMIMONotImplemented( |
301 | 322 | "Can only do margins for SISO system") |
302 | 323 |
|
| 324 | + if method == 'frd': |
| 325 | + # convert to FRD if we got a transfer function |
| 326 | + if isinstance(sys, xferfcn.TransferFunction): |
| 327 | + omega_sys = freqplot._default_frequency_range(sys) |
| 328 | + if sys.isctime(): |
| 329 | + sys = frdata.FRD(sys, omega_sys) |
| 330 | + else: |
| 331 | + omega_sys = omega_sys[omega_sys < np.pi / sys.dt] |
| 332 | + sys = frdata.FRD(sys(np.exp(1j*sys.dt*omega_sys)), omega_sys, |
| 333 | + smooth=True) |
| 334 | + elif method == 'best': |
| 335 | + # convert to FRD if anticipated numerical issues |
| 336 | + if isinstance(sys, xferfcn.TransferFunction) and not sys.isctime(): |
| 337 | + if _numerical_inaccuracy(sys): |
| 338 | + omega_sys = freqplot._default_frequency_range(sys) |
| 339 | + omega_sys = omega_sys[omega_sys < np.pi / sys.dt] |
| 340 | + sys = frdata.FRD(sys(np.exp(1j*sys.dt*omega_sys)), omega_sys, |
| 341 | + smooth=True) |
| 342 | + elif method != 'poly': |
| 343 | + raise ValueError("method " + method + " unknown") |
| 344 | + |
303 | 345 | if isinstance(sys, xferfcn.TransferFunction): |
304 | 346 | if sys.isctime(): |
305 | 347 | num_iw, den_iw = _poly_iw(sys) |
|
0 commit comments