diff --git a/control/dtime.py b/control/dtime.py index 8c0fe53e9..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 @@ -89,7 +93,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): @@ -98,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 @@ -126,6 +130,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..48e0c6cc2 100644 --- a/control/margins.py +++ b/control/margins.py @@ -283,14 +283,16 @@ 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 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) @@ -522,10 +524,12 @@ 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) + 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 +540,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: 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/tests/matlab_test.py b/control/tests/matlab_test.py index 6957e0bfe..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, 1.2720, 0.6559], 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.176469728448) - np.testing.assert_allclose(wp, 0.0616288455466) + 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""" diff --git a/control/xferfcn.py b/control/xferfcn.py index cb3bb4d41..356bf0e18 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] @@ -1085,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, @@ -1100,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 ----- @@ -1117,7 +1116,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 +1372,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