From f25e6254ef1ca5527ee2d8a10bfbded42758331d Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 20 Nov 2024 20:45:12 -0800 Subject: [PATCH 1/2] fix timebase processing in frd, zpk --- control/frdata.py | 17 +++++++++++++---- control/tests/docstrings_test.py | 1 + control/tests/timebase_test.py | 22 ++++++++++++++++++++++ control/xferfcn.py | 4 ++-- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index c5018babb..ab392f636 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -124,13 +124,22 @@ def __init__(self, *args, **kwargs): To construct frequency response data for an existing LTI object, other than an FRD, call FRD(sys, omega). + The timebase for the frequency response can be provided using an + optional third argument or the 'dt' keyword. + """ - # TODO: discrete-time FRD systems? smooth = kwargs.pop('smooth', False) # # Process positional arguments # + if len(args) == 3: + # Discrete time transfer function + if 'dt' in kwargs: + raise TypeError( + "timebase specified as positional argument and keyword") + kwargs['dt'] = args.pop() + if len(args) == 2: if not isinstance(args[0], FRD) and isinstance(args[0], LTI): # not an FRD, but still a system, second argument should be @@ -200,11 +209,11 @@ def __init__(self, *args, **kwargs): # Process iosys keywords defaults = { - 'inputs': self.fresp.shape[1], 'outputs': self.fresp.shape[0], - 'dt': None} + 'inputs': self.fresp.shape[1], 'outputs': self.fresp.shape[0]} + if arg_dt is not None: + defaults['dt'] = arg_dt # choose compatible timebase name, inputs, outputs, states, dt = _process_iosys_keywords( kwargs, defaults, end=True) - dt = common_timebase(dt, arg_dt) # choose compatible timebase # Process signal names InputOutputSystem.__init__( diff --git a/control/tests/docstrings_test.py b/control/tests/docstrings_test.py index 2b1368aeb..27ced105f 100644 --- a/control/tests/docstrings_test.py +++ b/control/tests/docstrings_test.py @@ -54,6 +54,7 @@ control.bode_plot: ['sharex', 'sharey', 'margin_info'], # deprecated control.eigensys_realization: ['arg'], # quasi-positional control.find_operating_point: ['method'], # internal use + control.zpk: ['args'] # 'dt' (manual) } # Decide on the level of verbosity (use -rP when running pytest) diff --git a/control/tests/timebase_test.py b/control/tests/timebase_test.py index 79b1492d7..bb77282b2 100644 --- a/control/tests/timebase_test.py +++ b/control/tests/timebase_test.py @@ -97,3 +97,25 @@ def test_composition_override(dt): with pytest.raises(ValueError, match="incompatible timebases"): sys3 = ct.interconnect( [sys1, sys2], inputs='u1', outputs='y2', dt=dt) + + +# Make sure all system creation functions treat timebases uniformly +@pytest.mark.parametrize( + "fcn, args", [ + (ct.ss, [-1, 1, 1, 1]), + (ct.tf, [[1, 2], [3, 4, 5]]), + (ct.zpk, [[-1], [-2, -3], 1]), + (ct.frd, [[1, 1, 1], [1, 2, 3]]), + (ct.nlsys, [lambda t, x, u, params: -x, None]), + ]) +@pytest.mark.parametrize( + "kwargs, expected", [ + ({}, 0), + ({'dt': 0}, 0), + ({'dt': 0.1}, 0.1), + ({'dt': True}, True), + ({'dt': None}, None), + ]) +def test_default(fcn, args, kwargs, expected): + sys = fcn(*args, **kwargs) + assert sys.dt == expected diff --git a/control/xferfcn.py b/control/xferfcn.py index ee41cbd2b..499359cbc 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1677,7 +1677,7 @@ def tf(*args, **kwargs): raise ValueError("Needs 1 or 2 arguments; received %i." % len(args)) -def zpk(zeros, poles, gain, dt=None, **kwargs): +def zpk(zeros, poles, gain, *args, **kwargs): """zpk(zeros, poles, gain[, dt]) Create a transfer function from zeros, poles, gain. @@ -1732,7 +1732,7 @@ def zpk(zeros, poles, gain, dt=None, **kwargs): """ num, den = zpk2tf(zeros, poles, gain) - return TransferFunction(num, den, dt=dt, **kwargs) + return TransferFunction(num, den, *args, **kwargs) def ss2tf(*args, **kwargs): From 8b29e45d71182a3b0ff60d1f1634fb0b63a5b715 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 21 Nov 2024 16:28:17 -0800 Subject: [PATCH 2/2] make frd timebase duplication testing consistent + unit tests --- control/frdata.py | 8 +++++--- control/tests/timebase_test.py | 9 +++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index ab392f636..42ecee0d9 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -135,10 +135,12 @@ def __init__(self, *args, **kwargs): # if len(args) == 3: # Discrete time transfer function + dt = args[-1] if 'dt' in kwargs: - raise TypeError( - "timebase specified as positional argument and keyword") - kwargs['dt'] = args.pop() + warn("received multiple dt arguments, " + "using positional arg dt = %s" % dt) + kwargs['dt'] = dt + args = args[:-1] if len(args) == 2: if not isinstance(args[0], FRD) and isinstance(args[0], LTI): diff --git a/control/tests/timebase_test.py b/control/tests/timebase_test.py index bb77282b2..c0e02d3b8 100644 --- a/control/tests/timebase_test.py +++ b/control/tests/timebase_test.py @@ -119,3 +119,12 @@ def test_composition_override(dt): def test_default(fcn, args, kwargs, expected): sys = fcn(*args, **kwargs) assert sys.dt == expected + + # Some commands allow dt via extra argument + if fcn in [ct.ss, ct.tf, ct.zpk, ct.frd] and kwargs.get('dt'): + sys = fcn(*args, kwargs['dt']) + assert sys.dt == expected + + # Make sure an error is generated if dt is redundant + with pytest.warns(UserWarning, match="received multiple dt"): + sys = fcn(*args, kwargs['dt'], **kwargs) 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