Skip to content

add zpk() function #816

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion control/matlab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion control/statesp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
2 changes: 2 additions & 0 deletions control/tests/kwargs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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), {}),
Expand Down Expand Up @@ -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':
Expand Down
74 changes: 72 additions & 2 deletions control/tests/xferfcn_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')

Expand Down Expand Up @@ -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']
64 changes: 56 additions & 8 deletions control/xferfcn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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 <sys[id]> 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.
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions doc/control.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ System creation
ss
tf
frd
zpk
rss
drss
NonlinearIOSystem
Expand Down