diff --git a/control/matlab/__init__.py b/control/matlab/__init__.py index fc0cd445b..1a524b33f 100644 --- a/control/matlab/__init__.py +++ b/control/matlab/__init__.py @@ -115,7 +115,7 @@ == ========================== ============================================ \* :func:`tf` create transfer function (TF) models -\ zpk create zero/pole/gain (ZPK) models. +\* :func:`zpk` create zero/pole/gain (ZPK) models. \* :func:`ss` create state-space (SS) models \ dss create descriptor state-space models \ delayss create state-space models with delayed terms diff --git a/control/statesp.py b/control/statesp.py index 4524af396..aac4dd8bd 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1521,7 +1521,6 @@ def output(self, t, x, u=None, params=None): # TODO: add discrete time check -# TODO: copy signal names def _convert_to_statespace(sys): """Convert a system to state space form (if needed). diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 20a1c8e9c..8116f013a 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -96,6 +96,7 @@ def test_kwarg_search(module, prefix): (control.tf, 0, 0, ([1], [1, 1]), {}), (control.tf2io, 0, 1, (), {}), (control.tf2ss, 0, 1, (), {}), + (control.zpk, 0, 0, ([1], [2, 3], 4), {}), (control.InputOutputSystem, 0, 0, (), {'inputs': 1, 'outputs': 1, 'states': 1}), (control.InputOutputSystem.linearize, 1, 0, (0, 0), {}), @@ -184,6 +185,7 @@ def test_matplotlib_kwargs(function, nsysargs, moreargs, kwargs, mplcleanup): 'tf2io' : test_unrecognized_kwargs, 'tf2ss' : test_unrecognized_kwargs, 'sample_system' : test_unrecognized_kwargs, + 'zpk': test_unrecognized_kwargs, 'flatsys.point_to_point': flatsys_test.TestFlatSys.test_point_to_point_errors, 'flatsys.solve_flat_ocp': diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index e4a2b3ec0..6e1cf6ce2 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -8,7 +8,8 @@ import operator import control as ct -from control import StateSpace, TransferFunction, rss, ss2tf, evalfr +from control import StateSpace, TransferFunction, rss, evalfr +from control import ss, ss2tf, tf, tf2ss from control import isctime, isdtime, sample_system, defaults from control.statesp import _convert_to_statespace from control.xferfcn import _convert_to_transfer_function @@ -986,7 +987,7 @@ def test_repr(self, Hargs, ref): np.testing.assert_array_almost_equal(H.num[p][m], H2.num[p][m]) np.testing.assert_array_almost_equal(H.den[p][m], H2.den[p][m]) assert H.dt == H2.dt - + def test_sample_named_signals(self): sysc = ct.TransferFunction(1.1, (1, 2), inputs='u', outputs='y') @@ -1073,3 +1074,72 @@ def test_xferfcn_ndarray_precedence(op, tf, arr): # Apply the operator to the array and transfer function result = op(arr, tf) assert isinstance(result, ct.TransferFunction) + + +@pytest.mark.parametrize( + "zeros, poles, gain, args, kwargs", [ + ([], [-1], 1, [], {}), + ([1, 2], [-1, -2, -3], 5, [], {}), + ([1, 2], [-1, -2, -3], 5, [], {'name': "sys"}), + ([1, 2], [-1, -2, -3], 5, [], {'inputs': ["in"], 'outputs': ["out"]}), + ([1, 2], [-1, -2, -3], 5, [0.1], {}), + (np.array([1, 2]), np.array([-1, -2, -3]), 5, [], {}), +]) +def test_zpk(zeros, poles, gain, args, kwargs): + # Create the transfer function + sys = ct.zpk(zeros, poles, gain, *args, **kwargs) + + # Make sure the poles and zeros match + np.testing.assert_equal(sys.zeros().sort(), zeros.sort()) + np.testing.assert_equal(sys.poles().sort(), poles.sort()) + + # Check to make sure the gain is OK + np.testing.assert_almost_equal( + gain, sys(0) * np.prod(-sys.poles()) / np.prod(-sys.zeros())) + + # Check time base + if args: + assert sys.dt == args[0] + + # Check inputs, outputs, name + input_labels = kwargs.get('inputs', []) + for i, label in enumerate(input_labels): + assert sys.input_labels[i] == label + + output_labels = kwargs.get('outputs', []) + for i, label in enumerate(output_labels): + assert sys.output_labels[i] == label + + if kwargs.get('name'): + assert sys.name == kwargs.get('name') + +@pytest.mark.parametrize("create, args, kwargs, convert", [ + (StateSpace, ([-1], [1], [1], [0]), {}, ss2tf), + (StateSpace, ([-1], [1], [1], [0]), {}, ss), + (StateSpace, ([-1], [1], [1], [0]), {}, tf), + (StateSpace, ([-1], [1], [1], [0]), dict(inputs='i', outputs='o'), ss2tf), + (StateSpace, ([-1], [1], [1], [0]), dict(inputs=1, outputs=1), ss2tf), + (StateSpace, ([-1], [1], [1], [0]), dict(inputs='i', outputs='o'), ss), + (StateSpace, ([-1], [1], [1], [0]), dict(inputs='i', outputs='o'), tf), + (TransferFunction, ([1], [1, 1]), {}, tf2ss), + (TransferFunction, ([1], [1, 1]), {}, tf), + (TransferFunction, ([1], [1, 1]), {}, ss), + (TransferFunction, ([1], [1, 1]), dict(inputs='i', outputs='o'), tf2ss), + (TransferFunction, ([1], [1, 1]), dict(inputs=1, outputs=1), tf2ss), + (TransferFunction, ([1], [1, 1]), dict(inputs='i', outputs='o'), tf), + (TransferFunction, ([1], [1, 1]), dict(inputs='i', outputs='o'), ss), +]) +def test_copy_names(create, args, kwargs, convert): + # Convert a system with no renaming + sys = create(*args, **kwargs) + cpy = convert(sys) + + assert cpy.input_labels == sys.input_labels + assert cpy.input_labels == sys.input_labels + if cpy.nstates is not None and sys.nstates is not None: + assert cpy.state_labels == sys.state_labels + + # Relabel inputs and outputs + cpy = convert(sys, inputs='myin', outputs='myout') + assert cpy.input_labels == ['myin'] + assert cpy.output_labels == ['myout'] diff --git a/control/xferfcn.py b/control/xferfcn.py index 5ebd35c13..0bc84e096 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -65,7 +65,7 @@ from .frdata import FrequencyResponseData from . import config -__all__ = ['TransferFunction', 'tf', 'ss2tf', 'tfdata'] +__all__ = ['TransferFunction', 'tf', 'zpk', 'ss2tf', 'tfdata'] # Define module default parameter values @@ -796,7 +796,7 @@ def zeros(self): """Compute the zeros of a transfer function.""" if self.ninputs > 1 or self.noutputs > 1: raise NotImplementedError( - "TransferFunction.zero is currently only implemented " + "TransferFunction.zeros is currently only implemented " "for SISO systems.") else: # for now, just give zeros of a SISO tf @@ -1424,16 +1424,13 @@ def _convert_to_transfer_function(sys, inputs=1, outputs=1): num = squeeze(num) # Convert to 1D array den = squeeze(den) # Probably not needed - return TransferFunction( - num, den, sys.dt, inputs=sys.input_labels, - outputs=sys.output_labels) + return TransferFunction(num, den, sys.dt) elif isinstance(sys, (int, float, complex, np.number)): num = [[[sys] for j in range(inputs)] for i in range(outputs)] den = [[[1] for j in range(inputs)] for i in range(outputs)] - return TransferFunction( - num, den, inputs=inputs, outputs=outputs) + return TransferFunction(num, den) elif isinstance(sys, FrequencyResponseData): raise TypeError("Can't convert given FRD to TransferFunction system.") @@ -1577,8 +1574,54 @@ def tf(*args, **kwargs): else: raise ValueError("Needs 1 or 2 arguments; received %i." % len(args)) -# TODO: copy signal names + +def zpk(zeros, poles, gain, *args, **kwargs): + """zpk(zeros, poles, gain[, dt]) + + Create a transfer function from zeros, poles, gain. + + Given a list of zeros z_i, poles p_j, and gain k, return the transfer + function: + + .. math:: + H(s) = k \\frac{(s - z_1) (s - z_2) \\cdots (s - z_m)} + {(s - p_1) (s - p_2) \\cdots (s - p_n)} + + Parameters + ---------- + zeros : array_like + Array containing the location of zeros. + poles : array_like + Array containing the location of zeros. + gain : float + System gain + dt : None, True or float, optional + System timebase. 0 (default) indicates continuous + time, True indicates discrete time with unspecified sampling + time, positive number is discrete time with specified + sampling time, None indicates unspecified timebase (either + continuous or discrete time). + inputs, outputs, states : str, or list of str, optional + List of strings that name the individual signals. If this parameter + is not given or given as `None`, the signal names will be of the + form `s[i]` (where `s` is one of `u`, `y`, or `x`). See + :class:`InputOutputSystem` for more information. + name : string, optional + System name (used for specifying signals). If unspecified, a generic + name is generated with a unique integer id. + + Returns + ------- + out: :class:`TransferFunction` + Transfer function with given zeros, poles, and gain. + + """ + num, den = zpk2tf(zeros, poles, gain) + return TransferFunction(num, den, *args, **kwargs) + + def ss2tf(*args, **kwargs): + """ss2tf(sys) Transform a state space system to a transfer function. @@ -1658,6 +1701,11 @@ def ss2tf(*args, **kwargs): if len(args) == 1: sys = args[0] if isinstance(sys, StateSpace): + kwargs = kwargs.copy() + if not kwargs.get('inputs'): + kwargs['inputs'] = sys.input_labels + if not kwargs.get('outputs'): + kwargs['outputs'] = sys.output_labels return TransferFunction( _convert_to_transfer_function(sys), **kwargs) else: diff --git a/doc/control.rst b/doc/control.rst index 54e233746..79702dc6a 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -18,6 +18,7 @@ System creation ss tf frd + zpk rss drss NonlinearIOSystem