Skip to content

Commit e3ff9c9

Browse files
committed
update LTI convenience functions to allow proper documentation
1 parent a6b30b1 commit e3ff9c9

8 files changed

Lines changed: 160 additions & 71 deletions

File tree

control/__init__.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,5 @@
100100
except ImportError:
101101
__version__ = "dev"
102102

103-
# patch the LTI class with function aliases for convenience functions
104-
# this needs to be done after the fact since the modules that contain the functions
105-
# all heavily depend on the LTI class
106-
LTI.to_ss = ss
107-
LTI.to_tf = tf
108-
LTI.bode_plot = bode_plot
109-
LTI.nyquist_plot = nyquist_plot
110-
LTI.nichols_plot = nichols_plot
111-
112103
# Initialize default parameter values
113104
reset_defaults()

control/frdata.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,19 @@ def append(self, other):
845845
"""Append a second model to the present model.
846846
847847
The second model is converted to FRD if necessary, inputs and
848-
outputs are appended and their order is preserved"""
848+
outputs are appended and their order is preserved.
849+
850+
Parameters
851+
----------
852+
other : `LTI`
853+
System to be appended.
854+
855+
Returns
856+
-------
857+
sys : `FrequencyResponseData`
858+
System model with `other` appended to `self`.
859+
860+
"""
849861
other = _convert_to_frd(other, omega=self.omega, inputs=other.ninputs,
850862
outputs=other.noutputs)
851863

control/lti.py

Lines changed: 95 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,11 @@
88
"""
99

1010
import math
11-
from typing import Callable
1211
from warnings import warn
1312

1413
import numpy as np
1514
from numpy import abs, real
1615

17-
import control
18-
1916
from . import config
2017
from .iosys import InputOutputSystem
2118

@@ -64,7 +61,7 @@ def damp(self):
6461
return wn, zeta, poles
6562

6663
def feedback(self, other=1, sign=-1):
67-
"""Feedback interconnection between two input/output systems."""
64+
"""Feedback interconnection between two input/output systems.
6865
6966
Parameters
7067
----------
@@ -238,30 +235,100 @@ def ispassive(self):
238235
from control.passivity import ispassive
239236
return ispassive(self)
240237

241-
# convenience aliases
242-
# most function are only forward declaraed and patched in the __init__.py to avoid circular imports
243-
244-
# conversions
245-
#: Convert to :class:`StateSpace` representation; see :func:`ss`
246-
to_ss: Callable
247-
#: Convert to :class:`TransferFunction` representation; see :func:`tf`
248-
to_tf: Callable
249-
250-
# freq domain plotting
251-
#: Bode plot; see :func:`bode_plot`
252-
bode_plot: Callable
253-
#: Nyquist plot; see :func:`nyquist_plot`
254-
nyquist_plot: Callable
255-
#: Nichols plot; see :func:`nichols_plot`
256-
nichols_plot: Callable
257-
258-
# time domain simulation
259-
#: Forced response; see :func:`forced_response`
260-
forced_response = control.timeresp.forced_response
261-
#: Impulse response; see :func:`impulse_response`
262-
impulse_response = control.timeresp.impulse_response
263-
#: Step response; see :func:`step_response`
264-
step_response = control.timeresp.step_response
238+
#
239+
# Convenience aliases for conversion functions
240+
#
241+
# Allow conversion between state space and transfer function types
242+
# as methods. These are just pass throughs to factory functions.
243+
#
244+
# Note: in order for docstrings to created, these have to set these up
245+
# as independent methods, not just assigned to ss() and tf().
246+
#
247+
# Imports are done within the function to avoid circular imports.
248+
#
249+
def to_ss(self, *args, **kwargs):
250+
"""Convert to state space representation.
251+
252+
See `ss` for details.
253+
"""
254+
from .statesp import ss
255+
return ss(self, *args, **kwargs)
256+
257+
def to_tf(self, *args, **kwargs):
258+
"""Convert to transfer function representation.
259+
260+
See `tf` for details.
261+
"""
262+
from .xferfcn import tf
263+
return tf(self, *args, **kwargs)
264+
265+
#
266+
# Convenience aliases for plotting and response functions
267+
#
268+
# Allow standard plots to be generated directly from the system object
269+
# in addition to standalone plotting and response functions.
270+
#
271+
# Note: in order for docstrings to created, these have to set these up as
272+
# independent methods, not just assigned to plotting/response functions.
273+
#
274+
# Imports are done within the function to avoid circular imports.
275+
#
276+
277+
def bode_plot(self, *args, **kwargs):
278+
"""Generate a Bode plot for the system.
279+
280+
See `bode_plot` for more information.
281+
"""
282+
from .freqplot import bode_plot
283+
return bode_plot(self, *args, **kwargs)
284+
285+
def nichols_plot(self, *args, **kwargs):
286+
"""Generate a Nichols plot for the system.
287+
288+
See `nichols_plot` for more information.
289+
"""
290+
from .nichols import nichols_plot
291+
return nichols_plot(self, *args, **kwargs)
292+
293+
def nyquist_plot(self, *args, **kwargs):
294+
"""Generate a Nyquist plot for the system.
295+
296+
See `nyquist_plot` for more information.
297+
"""
298+
from .freqplot import nyquist_plot
299+
return nyquist_plot(self, *args, **kwargs)
300+
301+
def forced_response(self, *args, **kwargs):
302+
"""Generate the forced response for the system.
303+
304+
See `forced_response` for more information.
305+
"""
306+
from .timeresp import forced_response
307+
return forced_response(self, *args, **kwargs)
308+
309+
def impulse_response(self, *args, **kwargs):
310+
"""Generate the impulse response for the system.
311+
312+
See `impulse_response` for more information.
313+
"""
314+
from .timeresp import impulse_response
315+
return impulse_response(self, *args, **kwargs)
316+
317+
def initial_response(self, *args, **kwargs):
318+
"""Generate the initial response for the system.
319+
320+
See `initial_response` for more information.
321+
"""
322+
from .timeresp import initial_response
323+
return initial_response(self, *args, **kwargs)
324+
325+
def step_response(self, *args, **kwargs):
326+
"""Generate the step response for the system.
327+
328+
See `step_response` for more information.
329+
"""
330+
from .timeresp import step_response
331+
return step_response(self, *args, **kwargs)
265332

266333

267334
def poles(sys):

control/statesp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1250,7 +1250,7 @@ def append(self, other):
12501250
"""Append a second model to the present model.
12511251
12521252
The second model is converted to state-space if necessary, inputs and
1253-
outputs are appended and their order is preserved
1253+
outputs are appended and their order is preserved.
12541254
12551255
Parameters
12561256
----------

control/tests/kwargs_test.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import control.tests.interconnect_test as interconnect_test
2727
import control.tests.iosys_test as iosys_test
2828
import control.tests.optimal_test as optimal_test
29+
import control.tests.statesp_test as statesp_test
2930
import control.tests.statefbk_test as statefbk_test
3031
import control.tests.stochsys_test as stochsys_test
3132
import control.tests.timeplot_test as timeplot_test
@@ -245,7 +246,7 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo):
245246
'append': test_unrecognized_kwargs,
246247
'bode': test_response_plot_kwargs,
247248
'bode_plot': test_response_plot_kwargs,
248-
'LTI.bode_plot': test_response_plot_kwargs, # alias for bode_plot and tested via bode_plot
249+
'LTI.bode_plot': test_response_plot_kwargs, # tested via bode_plot
249250
'combine_tf': test_unrecognized_kwargs,
250251
'create_estimator_iosystem': stochsys_test.test_estimator_errors,
251252
'create_statefbk_iosystem': statefbk_test.TestStatefbk.test_statefbk_errors,
@@ -268,15 +269,19 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo):
268269
'linearize': test_unrecognized_kwargs,
269270
'lqe': test_unrecognized_kwargs,
270271
'lqr': test_unrecognized_kwargs,
272+
'LTI.forced_response': statesp_test.test_convenience_aliases,
273+
'LTI.impulse_response': statesp_test.test_convenience_aliases,
274+
'LTI.initial_response': statesp_test.test_convenience_aliases,
275+
'LTI.step_response': statesp_test.test_convenience_aliases,
271276
'negate': test_unrecognized_kwargs,
272277
'nichols_plot': test_matplotlib_kwargs,
273-
'LTI.nichols_plot': test_matplotlib_kwargs, # alias for nichols_plot and tested via nichols_plot
278+
'LTI.nichols_plot': test_matplotlib_kwargs, # tested via nichols_plot
274279
'nichols': test_matplotlib_kwargs,
275280
'nlsys': test_unrecognized_kwargs,
276281
'nyquist': test_matplotlib_kwargs,
277282
'nyquist_response': test_response_plot_kwargs,
278283
'nyquist_plot': test_matplotlib_kwargs,
279-
'LTI.nyquist_plot': test_matplotlib_kwargs, # alias for nyquist_plot and tested via nyquist_plot
284+
'LTI.nyquist_plot': test_matplotlib_kwargs, # tested via nyquist_plot
280285
'phase_plane_plot': test_matplotlib_kwargs,
281286
'parallel': test_unrecognized_kwargs,
282287
'pole_zero_plot': test_unrecognized_kwargs,
@@ -289,11 +294,13 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo):
289294
'set_defaults': test_unrecognized_kwargs,
290295
'singular_values_plot': test_matplotlib_kwargs,
291296
'ss': test_unrecognized_kwargs,
297+
'LTI.to_ss': test_unrecognized_kwargs, # tested via 'ss'
292298
'ss2io': test_unrecognized_kwargs,
293299
'ss2tf': test_unrecognized_kwargs,
294300
'summing_junction': interconnect_test.test_interconnect_exceptions,
295301
'suptitle': freqplot_test.test_suptitle,
296302
'tf': test_unrecognized_kwargs,
303+
'LTI.to_tf': test_unrecognized_kwargs, # tested via 'ss'
297304
'tf2io' : test_unrecognized_kwargs,
298305
'tf2ss' : test_unrecognized_kwargs,
299306
'sample_system' : test_unrecognized_kwargs,

control/tests/statesp_test.py

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818
import control as ct
1919
from control.config import defaults
2020
from control.dtime import sample_system
21-
from control.lti import evalfr
22-
from control.statesp import StateSpace, _convert_to_statespace, tf2ss, \
23-
_statesp_defaults, _rss_generate, linfnorm, ss, rss, drss
21+
from control.lti import LTI, evalfr
22+
from control.statesp import StateSpace, _convert_to_statespace, \
23+
_rss_generate, _statesp_defaults, drss, linfnorm, rss, ss, tf2ss
2424
from control.xferfcn import TransferFunction, ss2tf
25-
from control.tests.conftest import assert_tf_close_coeff, editsdefaults, \
26-
slycotonly
25+
26+
from .conftest import assert_tf_close_coeff, editsdefaults, \
27+
ignore_future_warning, slycotonly
2728

2829

2930
class TestStateSpace:
@@ -1616,30 +1617,31 @@ def test_tf2ss_mimo():
16161617
def test_convenience_aliases():
16171618
sys = ct.StateSpace(1, 1, 1, 1)
16181619

1619-
# test that all the aliases point to the correct function
1620-
# .__funct__ a bound methods underlying function
1621-
assert sys.to_ss.__func__ == ct.ss
1622-
assert sys.to_tf.__func__ == ct.tf
1623-
assert sys.bode_plot.__func__ == ct.bode_plot
1624-
assert sys.nyquist_plot.__func__ == ct.nyquist_plot
1625-
assert sys.nichols_plot.__func__ == ct.nichols_plot
1626-
assert sys.forced_response.__func__ == ct.forced_response
1627-
assert sys.impulse_response.__func__ == ct.impulse_response
1628-
assert sys.step_response.__func__ == ct.step_response
1629-
assert sys.initial_response.__func__ == ct.initial_response
1630-
1631-
# make sure the functions can be used as member function ie they support
1632-
# an instance of StateSpace as the first argument and that they at least return
1633-
# the correct type
1620+
# Make sure the functions can be used as member function: i.e. they
1621+
# support an instance of StateSpace as the first argument and that
1622+
# they at least return the correct type
16341623
assert isinstance(sys.to_ss(), StateSpace)
16351624
assert isinstance(sys.to_tf(), TransferFunction)
16361625
assert isinstance(sys.bode_plot(), ct.ControlPlot)
16371626
assert isinstance(sys.nyquist_plot(), ct.ControlPlot)
16381627
assert isinstance(sys.nichols_plot(), ct.ControlPlot)
1639-
assert isinstance(sys.forced_response([0, 1], [1, 1]), (ct.TimeResponseData, ct.TimeResponseList))
1640-
assert isinstance(sys.impulse_response(), (ct.TimeResponseData, ct.TimeResponseList))
1641-
assert isinstance(sys.step_response(), (ct.TimeResponseData, ct.TimeResponseList))
1642-
assert isinstance(sys.initial_response(X0=1), (ct.TimeResponseData, ct.TimeResponseList))
1628+
assert isinstance(sys.forced_response([0, 1], [1, 1]),
1629+
(ct.TimeResponseData, ct.TimeResponseList))
1630+
assert isinstance(sys.impulse_response(),
1631+
(ct.TimeResponseData, ct.TimeResponseList))
1632+
assert isinstance(sys.step_response(),
1633+
(ct.TimeResponseData, ct.TimeResponseList))
1634+
assert isinstance(sys.initial_response(X0=1),
1635+
(ct.TimeResponseData, ct.TimeResponseList))
1636+
1637+
# Make sure that unrecognized keywords for response functions are caught
1638+
for method in [LTI.impulse_response, LTI.initial_response,
1639+
LTI.step_response]:
1640+
with pytest.raises(TypeError, match="unexpected keyword"):
1641+
method(sys, unknown=True)
1642+
with pytest.raises(TypeError, match="unexpected keyword"):
1643+
LTI.forced_response(sys, [0, 1], [1, 1], unknown=True)
1644+
16431645

16441646
# Test LinearICSystem __call__
16451647
def test_linearic_call():

control/xferfcn.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -906,8 +906,20 @@ def feedback(self, other=1, sign=-1):
906906
def append(self, other):
907907
"""Append a second model to the present model.
908908
909-
The second model is converted to a transfer function if necessary,
910-
inputs and outputs are appended and their order is preserved"""
909+
The second model is converted to FRD if necessary, inputs and
910+
outputs are appended and their order is preserved.
911+
912+
Parameters
913+
----------
914+
other : `LTI`
915+
System to be appended.
916+
917+
Returns
918+
-------
919+
sys : `TransferFunction`
920+
System model with `other` appended to `self`.
921+
922+
"""
911923
other = _convert_to_transfer_function(other)
912924

913925
new_tf = bdalg.combine_tf([

doc/classes.rst

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ systems (both linear time-invariant and nonlinear). They are usually
1414
created from factory functions such as :func:`tf` and :func:`ss`, so the
1515
user should normally not need to instantiate these directly.
1616

17-
The following figure illustrates the relationship between the classes and
18-
some of the functions that can be used to convert objects from one class to
19-
another:
17+
The following figure illustrates the relationship between the classes.
2018

2119
.. image:: figures/classes.pdf
2220
:width: 800

0 commit comments

Comments
 (0)