|
14 | 14 |
|
15 | 15 | from control import ControlMIMONotImplemented, FrequencyResponseData, \
|
16 | 16 | StateSpace, TransferFunction, margin, phase_crossover_frequencies, \
|
17 |
| - stability_margins |
| 17 | + stability_margins, disk_margins, tf, ss |
| 18 | +from control.exception import slycot_check |
18 | 19 |
|
19 | 20 | s = TransferFunction.s
|
20 | 21 |
|
@@ -372,3 +373,106 @@ def test_stability_margins_discrete(cnum, cden, dt,
|
372 | 373 | else:
|
373 | 374 | out = stability_margins(tf)
|
374 | 375 | assert_allclose(out, ref, rtol=rtol)
|
| 376 | + |
| 377 | +def test_siso_disk_margin(): |
| 378 | + # Frequencies of interest |
| 379 | + omega = np.logspace(-1, 2, 1001) |
| 380 | + |
| 381 | + # Loop transfer function |
| 382 | + L = tf(25, [1, 10, 10, 10]) |
| 383 | + |
| 384 | + # Balanced (S - T) disk-based stability margins |
| 385 | + DM, DGM, DPM = disk_margins(L, omega, skew=0.0) |
| 386 | + assert_allclose([DM], [0.46], atol=0.1) # disk margin of 0.46 |
| 387 | + assert_allclose([DGM], [4.05], atol=0.1) # disk-based gain margin of 4.05 dB |
| 388 | + assert_allclose([DPM], [25.8], atol=0.1) # disk-based phase margin of 25.8 deg |
| 389 | + |
| 390 | + # For SISO systems, the S-based (S) disk margin should match the third output |
| 391 | + # of existing library "stability_margins", i.e., minimum distance from the |
| 392 | + # Nyquist plot to -1. |
| 393 | + _, _, SM = stability_margins(L)[:3] |
| 394 | + DM = disk_margins(L, omega, skew=1.0)[0] |
| 395 | + assert_allclose([DM], [SM], atol=0.01) |
| 396 | + |
| 397 | +def test_mimo_disk_margin(): |
| 398 | + # Frequencies of interest |
| 399 | + omega = np.logspace(-1, 3, 1001) |
| 400 | + |
| 401 | + # Loop transfer gain |
| 402 | + P = ss([[0, 10], [-10, 0]], np.eye(2), [[1, 10], [-10, 1]], 0) # plant |
| 403 | + K = ss([], [], [], [[1, -2], [0, 1]]) # controller |
| 404 | + Lo = P * K # loop transfer function, broken at plant output |
| 405 | + Li = K * P # loop transfer function, broken at plant input |
| 406 | + |
| 407 | + if slycot_check(): |
| 408 | + # Balanced (S - T) disk-based stability margins at plant output |
| 409 | + DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0) |
| 410 | + assert_allclose([DMo], [0.3754], atol=0.1) # disk margin of 0.3754 |
| 411 | + assert_allclose([DGMo], [3.3], atol=0.1) # disk-based gain margin of 3.3 dB |
| 412 | + assert_allclose([DPMo], [21.26], atol=0.1) # disk-based phase margin of 21.26 deg |
| 413 | + |
| 414 | + # Balanced (S - T) disk-based stability margins at plant input |
| 415 | + DMi, DGMi, DPMi = disk_margins(Li, omega, skew=0.0) |
| 416 | + assert_allclose([DMi], [0.3754], atol=0.1) # disk margin of 0.3754 |
| 417 | + assert_allclose([DGMi], [3.3], atol=0.1) # disk-based gain margin of 3.3 dB |
| 418 | + assert_allclose([DPMi], [21.26], atol=0.1) # disk-based phase margin of 21.26 deg |
| 419 | + else: |
| 420 | + # Slycot not installed. Should throw exception. |
| 421 | + with pytest.raises(ControlMIMONotImplemented,\ |
| 422 | + match="Need slycot to compute MIMO disk_margins"): |
| 423 | + DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0) |
| 424 | + |
| 425 | +def test_siso_disk_margin_return_all(): |
| 426 | + # Frequencies of interest |
| 427 | + omega = np.logspace(-1, 2, 1001) |
| 428 | + |
| 429 | + # Loop transfer function |
| 430 | + L = tf(25, [1, 10, 10, 10]) |
| 431 | + |
| 432 | + # Balanced (S - T) disk-based stability margins |
| 433 | + DM, DGM, DPM = disk_margins(L, omega, skew=0.0, returnall=True) |
| 434 | + assert_allclose([omega[np.argmin(DM)]], [1.94],\ |
| 435 | + atol=0.01) # sensitivity peak at 1.94 rad/s |
| 436 | + assert_allclose([min(DM)], [0.46], atol=0.1) # disk margin of 0.46 |
| 437 | + assert_allclose([DGM[np.argmin(DM)]], [4.05],\ |
| 438 | + atol=0.1) # disk-based gain margin of 4.05 dB |
| 439 | + assert_allclose([DPM[np.argmin(DM)]], [25.8],\ |
| 440 | + atol=0.1) # disk-based phase margin of 25.8 deg |
| 441 | + |
| 442 | +def test_mimo_disk_margin_return_all(): |
| 443 | + # Frequencies of interest |
| 444 | + omega = np.logspace(-1, 3, 1001) |
| 445 | + |
| 446 | + # Loop transfer gain |
| 447 | + P = ss([[0, 10], [-10, 0]], np.eye(2),\ |
| 448 | + [[1, 10], [-10, 1]], 0) # plant |
| 449 | + K = ss([], [], [], [[1, -2], [0, 1]]) # controller |
| 450 | + Lo = P * K # loop transfer function, broken at plant output |
| 451 | + Li = K * P # loop transfer function, broken at plant input |
| 452 | + |
| 453 | + if slycot_check(): |
| 454 | + # Balanced (S - T) disk-based stability margins at plant output |
| 455 | + DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0, returnall=True) |
| 456 | + assert_allclose([omega[np.argmin(DMo)]], [omega[0]],\ |
| 457 | + atol=0.01) # sensitivity peak at 0 rad/s (or smallest provided) |
| 458 | + assert_allclose([min(DMo)], [0.3754], atol=0.1) # disk margin of 0.3754 |
| 459 | + assert_allclose([DGMo[np.argmin(DMo)]], [3.3],\ |
| 460 | + atol=0.1) # disk-based gain margin of 3.3 dB |
| 461 | + assert_allclose([DPMo[np.argmin(DMo)]], [21.26],\ |
| 462 | + atol=0.1) # disk-based phase margin of 21.26 deg |
| 463 | + |
| 464 | + # Balanced (S - T) disk-based stability margins at plant input |
| 465 | + DMi, DGMi, DPMi = disk_margins(Li, omega, skew=0.0, returnall=True) |
| 466 | + assert_allclose([omega[np.argmin(DMi)]], [omega[0]],\ |
| 467 | + atol=0.01) # sensitivity peak at 0 rad/s (or smallest provided) |
| 468 | + assert_allclose([min(DMi)], [0.3754],\ |
| 469 | + atol=0.1) # disk margin of 0.3754 |
| 470 | + assert_allclose([DGMi[np.argmin(DMi)]], [3.3],\ |
| 471 | + atol=0.1) # disk-based gain margin of 3.3 dB |
| 472 | + assert_allclose([DPMi[np.argmin(DMi)]], [21.26],\ |
| 473 | + atol=0.1) # disk-based phase margin of 21.26 deg |
| 474 | + else: |
| 475 | + # Slycot not installed. Should throw exception. |
| 476 | + with pytest.raises(ControlMIMONotImplemented,\ |
| 477 | + match="Need slycot to compute MIMO disk_margins"): |
| 478 | + DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0, returnall=True) |
0 commit comments