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 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