Skip to content

Commit a1791c9

Browse files
authored
Merge pull request #1092 from lkies/main
Add aliases of selected functions as member functions to LTI
2 parents 93c4c8d + 566627b commit a1791c9

File tree

5 files changed

+82
-2
lines changed

5 files changed

+82
-2
lines changed

control/__init__.py

+9
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,14 @@
118118
except ImportError:
119119
__version__ = "dev"
120120

121+
# patch the LTI class with function aliases for convenience functions
122+
# this needs to be done after the fact since the modules that contain the functions
123+
# all heavily depend on the LTI class
124+
LTI.to_ss = ss
125+
LTI.to_tf = tf
126+
LTI.bode_plot = bode_plot
127+
LTI.nyquist_plot = nyquist_plot
128+
LTI.nichols_plot = nichols_plot
129+
121130
# Initialize default parameter values
122131
reset_defaults()

control/lti.py

+38-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from warnings import warn
1212
from . import config
1313
from .iosys import InputOutputSystem
14+
import control
15+
from typing import Callable
1416

1517
__all__ = ['poles', 'zeros', 'damp', 'evalfr', 'frequency_response',
1618
'freqresp', 'dcgain', 'bandwidth', 'LTI']
@@ -76,9 +78,10 @@ def frequency_response(self, omega=None, squeeze=None):
7678
7779
Parameters
7880
----------
79-
omega : float or 1D array_like
81+
omega : float or 1D array_like, optional
8082
A list, tuple, array, or scalar value of frequencies in
81-
radians/sec at which the system will be evaluated.
83+
radians/sec at which the system will be evaluated. If None (default),
84+
a set of frequencies is computed based on the system dynamics.
8285
squeeze : bool, optional
8386
If squeeze=True, remove single-dimensional entries from the shape
8487
of the output even if the system is not SISO. If squeeze=False,
@@ -107,6 +110,11 @@ def frequency_response(self, omega=None, squeeze=None):
107110
"""
108111
from .frdata import FrequencyResponseData
109112

113+
if omega is None:
114+
# Use default frequency range
115+
from .freqplot import _default_frequency_range
116+
omega = _default_frequency_range(self)
117+
110118
omega = np.sort(np.array(omega, ndmin=1))
111119
if self.isdtime(strict=True):
112120
# Convert the frequency to discrete time
@@ -202,6 +210,34 @@ def ispassive(self):
202210
# importing here prevents circular dependancy
203211
from control.passivity import ispassive
204212
return ispassive(self)
213+
214+
def feedback(self, other=1, sign=-1):
215+
raise NotImplementedError(f"feedback not implemented for base {self.__class__.__name__} objects")
216+
217+
# convenience aliases
218+
# most function are only forward declaraed and patched in the __init__.py to avoid circular imports
219+
220+
# conversions
221+
#: Convert to :class:`StateSpace` representation; see :func:`ss`
222+
to_ss: Callable
223+
#: Convert to :class:`TransferFunction` representation; see :func:`tf`
224+
to_tf: Callable
225+
226+
# freq domain plotting
227+
#: Bode plot; see :func:`bode_plot`
228+
bode_plot: Callable
229+
#: Nyquist plot; see :func:`nyquist_plot`
230+
nyquist_plot: Callable
231+
#: Nichols plot; see :func:`nichols_plot`
232+
nichols_plot: Callable
233+
234+
# time domain simulation
235+
#: Forced response; see :func:`forced_response`
236+
forced_response = control.timeresp.forced_response
237+
#: Impulse response; see :func:`impulse_response`
238+
impulse_response = control.timeresp.impulse_response
239+
#: Step response; see :func:`step_response`
240+
step_response = control.timeresp.step_response
205241

206242

207243
def poles(sys):

control/statesp.py

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
common_timebase, isdtime, issiso
3737
from .lti import LTI, _process_frequency_response
3838
from .nlsys import InterconnectedSystem, NonlinearIOSystem
39+
import control
3940

4041
try:
4142
from slycot import ab13dd
@@ -1427,6 +1428,9 @@ def output(self, t, x, u=None, params=None):
14271428
raise ValueError("len(u) must be equal to number of inputs")
14281429
return (self.C @ x).reshape((-1,)) \
14291430
+ (self.D @ u).reshape((-1,)) # return as row vector
1431+
1432+
# convenience aliase, import needs to go over the submodule to avoid circular imports
1433+
initial_response = control.timeresp.initial_response
14301434

14311435

14321436
class LinearICSystem(InterconnectedSystem, StateSpace):

control/tests/kwargs_test.py

+3
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo):
244244
'append': test_unrecognized_kwargs,
245245
'bode': test_response_plot_kwargs,
246246
'bode_plot': test_response_plot_kwargs,
247+
'LTI.bode_plot': test_response_plot_kwargs, # alias for bode_plot and tested via bode_plot
247248
'create_estimator_iosystem': stochsys_test.test_estimator_errors,
248249
'create_statefbk_iosystem': statefbk_test.TestStatefbk.test_statefbk_errors,
249250
'describing_function_plot': test_matplotlib_kwargs,
@@ -267,11 +268,13 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo):
267268
'lqr': test_unrecognized_kwargs,
268269
'negate': test_unrecognized_kwargs,
269270
'nichols_plot': test_matplotlib_kwargs,
271+
'LTI.nichols_plot': test_matplotlib_kwargs, # alias for nichols_plot and tested via nichols_plot
270272
'nichols': test_matplotlib_kwargs,
271273
'nlsys': test_unrecognized_kwargs,
272274
'nyquist': test_matplotlib_kwargs,
273275
'nyquist_response': test_response_plot_kwargs,
274276
'nyquist_plot': test_matplotlib_kwargs,
277+
'LTI.nyquist_plot': test_matplotlib_kwargs, # alias for nyquist_plot and tested via nyquist_plot
275278
'phase_plane_plot': test_matplotlib_kwargs,
276279
'parallel': test_unrecognized_kwargs,
277280
'pole_zero_plot': test_unrecognized_kwargs,

control/tests/statesp_test.py

+28
Original file line numberDiff line numberDiff line change
@@ -1281,3 +1281,31 @@ def test_tf2ss_mimo():
12811281
else:
12821282
with pytest.raises(ct.ControlMIMONotImplemented):
12831283
sys_ss = ct.ss(sys_tf)
1284+
1285+
def test_convenience_aliases():
1286+
sys = ct.StateSpace(1, 1, 1, 1)
1287+
1288+
# test that all the aliases point to the correct function
1289+
# .__funct__ a bound methods underlying function
1290+
assert sys.to_ss.__func__ == ct.ss
1291+
assert sys.to_tf.__func__ == ct.tf
1292+
assert sys.bode_plot.__func__ == ct.bode_plot
1293+
assert sys.nyquist_plot.__func__ == ct.nyquist_plot
1294+
assert sys.nichols_plot.__func__ == ct.nichols_plot
1295+
assert sys.forced_response.__func__ == ct.forced_response
1296+
assert sys.impulse_response.__func__ == ct.impulse_response
1297+
assert sys.step_response.__func__ == ct.step_response
1298+
assert sys.initial_response.__func__ == ct.initial_response
1299+
1300+
# make sure the functions can be used as member function ie they support
1301+
# an instance of StateSpace as the first argument and that they at least return
1302+
# the correct type
1303+
assert isinstance(sys.to_ss(), StateSpace)
1304+
assert isinstance(sys.to_tf(), TransferFunction)
1305+
assert isinstance(sys.bode_plot(), ct.ControlPlot)
1306+
assert isinstance(sys.nyquist_plot(), ct.ControlPlot)
1307+
assert isinstance(sys.nichols_plot(), ct.ControlPlot)
1308+
assert isinstance(sys.forced_response([0, 1], [1, 1]), (ct.TimeResponseData, ct.TimeResponseList))
1309+
assert isinstance(sys.impulse_response(), (ct.TimeResponseData, ct.TimeResponseList))
1310+
assert isinstance(sys.step_response(), (ct.TimeResponseData, ct.TimeResponseList))
1311+
assert isinstance(sys.initial_response(X0=1), (ct.TimeResponseData, ct.TimeResponseList))

0 commit comments

Comments
 (0)