From ca8e67025c1b429ef8fb57541eb449059eddd8a5 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Fri, 5 Nov 2021 13:00:54 -0700 Subject: [PATCH 1/4] misc bugfixes: fixed prewarp not working in c2d and sample_system, incorrect order of return arguments in margin, typos and changed to ControlMIMONotImplemented error where needed. --- control/dtime.py | 6 ++++-- control/margins.py | 8 ++++---- control/tests/margin_test.py | 20 ++++++++++---------- control/tests/matlab_test.py | 6 +++--- control/xferfcn.py | 12 +++++++----- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/control/dtime.py b/control/dtime.py index 8c0fe53e9..8f3e00071 100644 --- a/control/dtime.py +++ b/control/dtime.py @@ -89,7 +89,8 @@ def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None): if not isctime(sysc): raise ValueError("First argument must be continuous time system") - return sysc.sample(Ts, method, alpha, prewarp_frequency) + return sysc.sample(Ts, + method=method, alpha=alpha, prewarp_frequency=prewarp_frequency) def c2d(sysc, Ts, method='zoh', prewarp_frequency=None): @@ -126,6 +127,7 @@ def c2d(sysc, Ts, method='zoh', prewarp_frequency=None): """ # Call the sample_system() function to do the work - sysd = sample_system(sysc, Ts, method, prewarp_frequency) + sysd = sample_system(sysc, Ts, + method=method, prewarp_frequency=prewarp_frequency) return sysd diff --git a/control/margins.py b/control/margins.py index 0b53f26ed..e3c5ab14a 100644 --- a/control/margins.py +++ b/control/margins.py @@ -283,7 +283,7 @@ def stability_margins(sysdata, returnall=False, epsw=0.0, method='best'): ------- gm : float or array_like Gain margin - pm : float or array_loke + pm : float or array_like Phase margin sm : float or array_like Stability margin, the minimum distance from the Nyquist plot to -1 @@ -522,10 +522,10 @@ def margin(*args): Gain margin pm : float Phase margin (in degrees) - wpc : float or array_like - Phase crossover frequency (where phase crosses -180 degrees) wgc : float or array_like Gain crossover frequency (where gain crosses 1) + wpc : float or array_like + Phase crossover frequency (where phase crosses -180 degrees) Margins are calculated for a SISO open-loop system. @@ -548,4 +548,4 @@ def margin(*args): raise ValueError("Margin needs 1 or 3 arguments; received %i." % len(args)) - return margin[0], margin[1], margin[3], margin[4] + return margin[0], margin[1], margin[4], margin[3] diff --git a/control/tests/margin_test.py b/control/tests/margin_test.py index a1246103f..8c91ade29 100644 --- a/control/tests/margin_test.py +++ b/control/tests/margin_test.py @@ -102,7 +102,7 @@ def test_margin_sys(tsys): sys, refout, refoutall = tsys """Test margin() function with system input""" out = margin(sys) - assert_allclose(out, np.array(refout)[[0, 1, 3, 4]], atol=1.5e-3) + assert_allclose(out, np.array(refout)[[0, 1, 4, 3]], atol=1.5e-3) def test_margin_3input(tsys): sys, refout, refoutall = tsys @@ -110,7 +110,7 @@ def test_margin_3input(tsys): omega = np.logspace(-2, 2, 2000) mag, phase, omega_ = sys.frequency_response(omega) out = margin((mag, phase*180/np.pi, omega_)) - assert_allclose(out, np.array(refout)[[0, 1, 3, 4]], atol=1.5e-3) + assert_allclose(out, np.array(refout)[[0, 1, 4, 3]], atol=1.5e-3) @pytest.mark.parametrize( @@ -276,23 +276,23 @@ def tsys_zmore(request, tsys_zmoresystems): @pytest.mark.parametrize( 'tsys_zmore', [dict(sysname='typem1', K=2.0, atol=1.5e-3, - result=(float('Inf'), -120.0007, float('NaN'), 0.5774)), + result=(float('Inf'), -120.0007, 0.5774, float('NaN'))), dict(sysname='type0', K=0.8, atol=1.5e-3, - result=(10.0014, float('inf'), 1.7322, float('nan'))), + result=(10.0014, float('inf'), float('nan'), 1.7322)), dict(sysname='type0', K=2.0, atol=1e-2, - result=(4.000, 67.6058, 1.7322, 0.7663)), + result=(4.000, 67.6058, 0.7663, 1.7322)), dict(sysname='type1', K=1.0, atol=1e-4, - result=(float('Inf'), 144.9032, float('NaN'), 0.3162)), + result=(float('Inf'), 144.9032, 0.3162, float('NaN'))), dict(sysname='type2', K=1.0, atol=1e-4, - result=(float('Inf'), 44.4594, float('NaN'), 0.7907)), + result=(float('Inf'), 44.4594, 0.7907, float('NaN'))), dict(sysname='type3', K=1.0, atol=1.5e-3, - result=(0.0626, 37.1748, 0.1119, 0.7951)), + result=(0.0626, 37.1748, 0.7951, 0.1119)), dict(sysname='example21', K=1.0, atol=1e-2, result=(0.0100, -14.5640, 0, 0.0022)), dict(sysname='example21', K=1000.0, atol=1e-2, - result=(0.1793, 22.5215, 0.0243, 0.0630)), + result=(0.1793, 22.5215, 0.0630, 0.0243)), dict(sysname='example21', K=5000.0, atol=1.5e-3, - result=(4.5596, 21.2101, 0.4385, 0.1868)), + result=(4.5596, 21.2101, 0.1868, 0.4385)), ], indirect=True) def test_zmore_margin(tsys_zmore): diff --git a/control/tests/matlab_test.py b/control/tests/matlab_test.py index 6957e0bfe..7d51e7fbe 100644 --- a/control/tests/matlab_test.py +++ b/control/tests/matlab_test.py @@ -361,7 +361,7 @@ def testMargin(self, siso): gm, pm, wg, wp = margin(siso.ss2) gm, pm, wg, wp = margin(siso.ss2 * siso.ss2 * 2) np.testing.assert_array_almost_equal( - [gm, pm, wg, wp], [1.5451, 75.9933, 1.2720, 0.6559], decimal=3) + [gm, pm, wg, wp], [1.5451, 75.9933, 0.6559, 1.2720], decimal=3) def testDcgain(self, siso): """Test dcgain() for SISO system""" @@ -785,8 +785,8 @@ def testCombi01(self): # print("%f %f %f %f" % (gm, pm, wg, wp)) np.testing.assert_allclose(gm, 3.32065569155) np.testing.assert_allclose(pm, 46.9740430224) - np.testing.assert_allclose(wg, 0.176469728448) - np.testing.assert_allclose(wp, 0.0616288455466) + np.testing.assert_allclose(wg, 0.0616288455466) + np.testing.assert_allclose(wp, 0.176469728448) def test_tf_string_args(self): """Make sure s and z are defined properly""" diff --git a/control/xferfcn.py b/control/xferfcn.py index cb3bb4d41..dc6672b33 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -64,6 +64,7 @@ from itertools import chain from re import sub from .lti import LTI, common_timebase, isdtime, _process_frequency_response +from .exception import ControlMIMONotImplemented from . import config __all__ = ['TransferFunction', 'tf', 'ss2tf', 'tfdata'] @@ -793,9 +794,9 @@ def feedback(self, other=1, sign=-1): if (self.ninputs > 1 or self.noutputs > 1 or other.ninputs > 1 or other.noutputs > 1): # TODO: MIMO feedback - raise NotImplementedError( - "TransferFunction.feedback is currently only implemented " - "for SISO functions.") + raise ControlMIMONotImplemented( + "TransferFunction.feedback is currently not implemented for " + "MIMO systems.") dt = common_timebase(self.dt, other.dt) num1 = self.num[0][0] @@ -1117,7 +1118,7 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): if not self.isctime(): raise ValueError("System must be continuous time system") if not self.issiso(): - raise NotImplementedError("MIMO implementation not available") + raise ControlMIMONotImplemented("Not implemented for MIMO systems") if method == "matched": return _c2d_matched(self, Ts) sys = (self.num[0][0], self.den[0][0]) @@ -1373,7 +1374,8 @@ def _convert_to_transfer_function(sys, **kw): except ImportError: # If slycot is not available, use signal.lti (SISO only) if sys.ninputs != 1 or sys.noutputs != 1: - raise TypeError("No support for MIMO without slycot.") + raise ControlMIMONotImplemented("Not implemented for " + + "MIMO systems without slycot.") # Do the conversion using sp.signal.ss2tf # Note that this returns a 2D array for the numerator From bc5079bfa940559397da4402acf7eaaf72359a8e Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Fri, 5 Nov 2021 14:17:11 -0700 Subject: [PATCH 2/4] reverted mistaken margin argument rearrangement and clarified definitions in docstring of margin --- control/margins.py | 14 ++++++++------ control/tests/margin_test.py | 20 ++++++++++---------- control/tests/matlab_test.py | 20 ++++++++++---------- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/control/margins.py b/control/margins.py index e3c5ab14a..c602d3627 100644 --- a/control/margins.py +++ b/control/margins.py @@ -522,10 +522,12 @@ def margin(*args): Gain margin pm : float Phase margin (in degrees) - wgc : float or array_like - Gain crossover frequency (where gain crosses 1) - wpc : float or array_like - Phase crossover frequency (where phase crosses -180 degrees) + wcg : float or array_like + Crossover frequency associated with gain margin (phase crossover + frequency), where phase crosses below -180 degrees. + wcp : float or array_like + Crossover frequency associated with phase margin (gain crossover + frequency), where gain crosses below 1. Margins are calculated for a SISO open-loop system. @@ -536,7 +538,7 @@ def margin(*args): Examples -------- >>> sys = tf(1, [1, 2, 1, 0]) - >>> gm, pm, wg, wp = margin(sys) + >>> gm, pm, wcg, wcp = margin(sys) """ if len(args) == 1: @@ -548,4 +550,4 @@ def margin(*args): raise ValueError("Margin needs 1 or 3 arguments; received %i." % len(args)) - return margin[0], margin[1], margin[4], margin[3] + return margin[0], margin[1], margin[3], margin[4] diff --git a/control/tests/margin_test.py b/control/tests/margin_test.py index 8c91ade29..a1246103f 100644 --- a/control/tests/margin_test.py +++ b/control/tests/margin_test.py @@ -102,7 +102,7 @@ def test_margin_sys(tsys): sys, refout, refoutall = tsys """Test margin() function with system input""" out = margin(sys) - assert_allclose(out, np.array(refout)[[0, 1, 4, 3]], atol=1.5e-3) + assert_allclose(out, np.array(refout)[[0, 1, 3, 4]], atol=1.5e-3) def test_margin_3input(tsys): sys, refout, refoutall = tsys @@ -110,7 +110,7 @@ def test_margin_3input(tsys): omega = np.logspace(-2, 2, 2000) mag, phase, omega_ = sys.frequency_response(omega) out = margin((mag, phase*180/np.pi, omega_)) - assert_allclose(out, np.array(refout)[[0, 1, 4, 3]], atol=1.5e-3) + assert_allclose(out, np.array(refout)[[0, 1, 3, 4]], atol=1.5e-3) @pytest.mark.parametrize( @@ -276,23 +276,23 @@ def tsys_zmore(request, tsys_zmoresystems): @pytest.mark.parametrize( 'tsys_zmore', [dict(sysname='typem1', K=2.0, atol=1.5e-3, - result=(float('Inf'), -120.0007, 0.5774, float('NaN'))), + result=(float('Inf'), -120.0007, float('NaN'), 0.5774)), dict(sysname='type0', K=0.8, atol=1.5e-3, - result=(10.0014, float('inf'), float('nan'), 1.7322)), + result=(10.0014, float('inf'), 1.7322, float('nan'))), dict(sysname='type0', K=2.0, atol=1e-2, - result=(4.000, 67.6058, 0.7663, 1.7322)), + result=(4.000, 67.6058, 1.7322, 0.7663)), dict(sysname='type1', K=1.0, atol=1e-4, - result=(float('Inf'), 144.9032, 0.3162, float('NaN'))), + result=(float('Inf'), 144.9032, float('NaN'), 0.3162)), dict(sysname='type2', K=1.0, atol=1e-4, - result=(float('Inf'), 44.4594, 0.7907, float('NaN'))), + result=(float('Inf'), 44.4594, float('NaN'), 0.7907)), dict(sysname='type3', K=1.0, atol=1.5e-3, - result=(0.0626, 37.1748, 0.7951, 0.1119)), + result=(0.0626, 37.1748, 0.1119, 0.7951)), dict(sysname='example21', K=1.0, atol=1e-2, result=(0.0100, -14.5640, 0, 0.0022)), dict(sysname='example21', K=1000.0, atol=1e-2, - result=(0.1793, 22.5215, 0.0630, 0.0243)), + result=(0.1793, 22.5215, 0.0243, 0.0630)), dict(sysname='example21', K=5000.0, atol=1.5e-3, - result=(4.5596, 21.2101, 0.1868, 0.4385)), + result=(4.5596, 21.2101, 0.4385, 0.1868)), ], indirect=True) def test_zmore_margin(tsys_zmore): diff --git a/control/tests/matlab_test.py b/control/tests/matlab_test.py index 7d51e7fbe..8b2a0951e 100644 --- a/control/tests/matlab_test.py +++ b/control/tests/matlab_test.py @@ -355,13 +355,13 @@ def testLsim_mimo(self, mimo): def testMargin(self, siso): """Test margin()""" #! TODO: check results to make sure they are OK - gm, pm, wg, wp = margin(siso.tf1) - gm, pm, wg, wp = margin(siso.tf2) - gm, pm, wg, wp = margin(siso.ss1) - gm, pm, wg, wp = margin(siso.ss2) - gm, pm, wg, wp = margin(siso.ss2 * siso.ss2 * 2) + gm, pm, wcg, wcp = margin(siso.tf1) + gm, pm, wcg, wcp = margin(siso.tf2) + gm, pm, wcg, wcp = margin(siso.ss1) + gm, pm, wcg, wcp = margin(siso.ss2) + gm, pm, wcg, wcp = margin(siso.ss2 * siso.ss2 * 2) np.testing.assert_array_almost_equal( - [gm, pm, wg, wp], [1.5451, 75.9933, 0.6559, 1.2720], decimal=3) + [gm, pm, wcg, wcp], [1.5451, 75.9933, 1.2720, 0.6559], decimal=3) def testDcgain(self, siso): """Test dcgain() for SISO system""" @@ -781,12 +781,12 @@ def testCombi01(self): # total open loop Hol = Hc*Hno*Hp - gm, pm, wg, wp = margin(Hol) - # print("%f %f %f %f" % (gm, pm, wg, wp)) + gm, pm, wcg, wcp = margin(Hol) + # print("%f %f %f %f" % (gm, pm, wcg, wcp)) np.testing.assert_allclose(gm, 3.32065569155) np.testing.assert_allclose(pm, 46.9740430224) - np.testing.assert_allclose(wg, 0.0616288455466) - np.testing.assert_allclose(wp, 0.176469728448) + np.testing.assert_allclose(wcg, 0.176469728448) + np.testing.assert_allclose(wcp, 0.0616288455466) def test_tf_string_args(self): """Make sure s and z are defined properly""" From cfb6e86e76a908d3cbac79bea7e45fff9bd08db8 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Fri, 5 Nov 2021 14:19:25 -0700 Subject: [PATCH 3/4] clarified docstring in stability-margins --- control/margins.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/control/margins.py b/control/margins.py index c602d3627..48e0c6cc2 100644 --- a/control/margins.py +++ b/control/margins.py @@ -288,9 +288,11 @@ def stability_margins(sysdata, returnall=False, epsw=0.0, method='best'): sm : float or array_like Stability margin, the minimum distance from the Nyquist plot to -1 wpc : float or array_like - Phase crossover frequency (where phase crosses -180 degrees) + Phase crossover frequency (where phase crosses -180 degrees), which is + associated with the gain margin. wgc : float or array_like - Gain crossover frequency (where gain crosses 1) + Gain crossover frequency (where gain crosses 1), which is associated + with the phase margin. wms : float or array_like Stability margin frequency (where Nyquist plot is closest to -1) From 4c66fb1ea47f462941b9f382756274895211a8ff Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 8 Nov 2021 11:04:53 -0800 Subject: [PATCH 4/4] test prewarp in c2d and sample_system --- control/dtime.py | 25 ++++++++++++++----------- control/tests/discrete_test.py | 18 ++++++++++++++---- control/xferfcn.py | 6 ++---- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/control/dtime.py b/control/dtime.py index 8f3e00071..c60778d00 100644 --- a/control/dtime.py +++ b/control/dtime.py @@ -5,6 +5,7 @@ Routines in this module: sample_system() +c2d() """ """Copyright (c) 2012 by California Institute of Technology @@ -58,16 +59,19 @@ def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None): Parameters ---------- - sysc : LTI (StateSpace or TransferFunction) + sysc : LTI (:class:`StateSpace` or :class:`TransferFunction`) Continuous time system to be converted - Ts : real > 0 + Ts : float > 0 Sampling period method : string Method to use for conversion, e.g. 'bilinear', 'zoh' (default) - - prewarp_frequency : real within [0, infinity) + alpha : float within [0, 1] + The generalized bilinear transformation weighting parameter, which + should only be specified with method="gbt", and is ignored + otherwise. See :func:`scipy.signal.cont2discrete`. + prewarp_frequency : float within [0, infinity) The frequency [rad/s] at which to match with the input continuous- - time system's magnitude and phase + time system's magnitude and phase (only valid for method='bilinear') Returns ------- @@ -76,7 +80,7 @@ def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None): Notes ----- - See :meth:`StateSpace.sample` or :meth:`TransferFunction.sample`` for + See :meth:`StateSpace.sample` or :meth:`TransferFunction.sample` for further details. Examples @@ -99,20 +103,19 @@ def c2d(sysc, Ts, method='zoh', prewarp_frequency=None): Parameters ---------- - sysc : LTI (StateSpace or TransferFunction) + sysc : LTI (:class:`StateSpace` or :class:`TransferFunction`) Continuous time system to be converted - Ts : real > 0 + Ts : float > 0 Sampling period method : string Method to use for conversion, e.g. 'bilinear', 'zoh' (default) - prewarp_frequency : real within [0, infinity) The frequency [rad/s] at which to match with the input continuous- - time system's magnitude and phase + time system's magnitude and phase (only valid for method='bilinear') Returns ------- - sysd : linsys + sysd : LTI of the same class Discrete time system, with sampling rate Ts Notes diff --git a/control/tests/discrete_test.py b/control/tests/discrete_test.py index 379098ff2..5a1a367ab 100644 --- a/control/tests/discrete_test.py +++ b/control/tests/discrete_test.py @@ -7,8 +7,8 @@ import pytest from control import (StateSpace, TransferFunction, bode, common_timebase, - evalfr, feedback, forced_response, impulse_response, - isctime, isdtime, rss, sample_system, step_response, + feedback, forced_response, impulse_response, + isctime, isdtime, rss, c2d, sample_system, step_response, timebase) @@ -382,10 +382,20 @@ def test_sample_system_prewarp(self, tsys, plantname): Ts = 0.025 # test state space version plant = getattr(tsys, plantname) + plant_fr = plant(wwarp * 1j) + plant_d_warped = plant.sample(Ts, 'bilinear', prewarp_frequency=wwarp) - plant_fr = evalfr(plant, wwarp * 1j) dt = plant_d_warped.dt - plant_d_fr = evalfr(plant_d_warped, np.exp(wwarp * 1.j * dt)) + plant_d_fr = plant_d_warped(np.exp(wwarp * 1.j * dt)) + np.testing.assert_array_almost_equal(plant_fr, plant_d_fr) + + plant_d_warped = sample_system(plant, Ts, 'bilinear', + prewarp_frequency=wwarp) + plant_d_fr = plant_d_warped(np.exp(wwarp * 1.j * dt)) + np.testing.assert_array_almost_equal(plant_fr, plant_d_fr) + + plant_d_warped = c2d(plant, Ts, 'bilinear', prewarp_frequency=wwarp) + plant_d_fr = plant_d_warped(np.exp(wwarp * 1.j * dt)) np.testing.assert_array_almost_equal(plant_fr, plant_d_fr) def test_sample_system_errors(self, tsys): diff --git a/control/xferfcn.py b/control/xferfcn.py index dc6672b33..356bf0e18 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1086,12 +1086,10 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): * euler: Euler (or forward difference) method ("gbt" with alpha=0) * backward_diff: Backwards difference ("gbt" with alpha=1.0) * zoh: zero-order hold (default) - alpha : float within [0, 1] The generalized bilinear transformation weighting parameter, which should only be specified with method="gbt", and is ignored - otherwise. - + otherwise. See :func:`scipy.signal.cont2discrete`. prewarp_frequency : float within [0, infinity) The frequency [rad/s] at which to match with the input continuous- time system's magnitude and phase (the gain=1 crossover frequency, @@ -1101,7 +1099,7 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): Returns ------- sysd : TransferFunction system - Discrete time system, with sampling rate Ts + Discrete time system, with sample period Ts Notes ----- 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