Skip to content

standardize time response return values, return_x/squeeze keyword processing #511

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 3 commits into from
Jan 19, 2021
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
12 changes: 11 additions & 1 deletion control/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
# Package level default values
_control_defaults = {
'control.default_dt': 0,
'control.squeeze_frequency_response': None
'control.squeeze_frequency_response': None,
'control.squeeze_time_response': True,
'control.squeeze_time_response': None,
'forced_response.return_x': False,
}
defaults = dict(_control_defaults)

Expand Down Expand Up @@ -211,6 +214,7 @@ def use_legacy_defaults(version):
#
# Go backwards through releases and reset defaults
#
reset_defaults() # start from a clean slate

# Version 0.9.0:
if major == 0 and minor < 9:
Expand All @@ -230,4 +234,10 @@ def use_legacy_defaults(version):
# turned off _remove_useless_states
set_defaults('statesp', remove_useless_states=True)

# forced_response no longer returns x by default
set_defaults('forced_response', return_x=True)

# time responses are only squeezed if SISO
set_defaults('control', squeeze_time_response=True)

return (major, minor, patch)
40 changes: 19 additions & 21 deletions control/iosys.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from warnings import warn

from .statesp import StateSpace, tf2ss
from .timeresp import _check_convert_array
from .timeresp import _check_convert_array, _process_time_response
from .lti import isctime, isdtime, common_timebase
from . import config

Expand Down Expand Up @@ -450,6 +450,10 @@ def find_state(self, name):
"""Find the index for a state given its name (`None` if not found)"""
return self.state_index.get(name, None)

def issiso(self):
"""Check to see if a system is single input, single output"""
return self.ninputs == 1 and self.noutputs == 1

def feedback(self, other=1, sign=-1, params={}):
"""Feedback interconnection between two input/output systems

Expand Down Expand Up @@ -1353,7 +1357,7 @@ def __init__(self, io_sys, ss_sys=None):


def input_output_response(sys, T, U=0., X0=0, params={}, method='RK45',
return_x=False, squeeze=True):
transpose=False, return_x=False, squeeze=None):

"""Compute the output response of a system to a given input.

Expand All @@ -1373,18 +1377,22 @@ def input_output_response(sys, T, U=0., X0=0, params={}, method='RK45',
return_x : bool, optional
If True, return the values of the state at each time (default = False).
squeeze : bool, optional
If True (default), squeeze unused dimensions out of the output
response. In particular, for a single output system, return a
vector of shape (nsteps) instead of (nsteps, 1).
If True and if the system has a single output, return the system
output as a 1D array rather than a 2D array. If False, return the
system output as a 2D array even if the system is SISO. Default value
set by config.defaults['control.squeeze_time_response'].

Returns
-------
T : array
Time values of the output.
yout : array
Response of the system.
Response of the system. If the system is SISO and squeeze is not
True, the array is 1D (indexed by time). If the system is not SISO or
squeeze is False, the array is 2D (indexed by the output number and
time).
xout : array
Time evolution of the state vector (if return_x=True)
Time evolution of the state vector (if return_x=True).

Raises
------
Expand Down Expand Up @@ -1420,12 +1428,8 @@ def input_output_response(sys, T, U=0., X0=0, params={}, method='RK45',
for i in range(len(T)):
u = U[i] if len(U.shape) == 1 else U[:, i]
y[:, i] = sys._out(T[i], [], u)
if squeeze:
y = np.squeeze(y)
if return_x:
return T, y, []
else:
return T, y
return _process_time_response(sys, T, y, [], transpose=transpose,
return_x=return_x, squeeze=squeeze)

# create X0 if not given, test if X0 has correct shape
X0 = _check_convert_array(X0, [(nstates,), (nstates, 1)],
Expand Down Expand Up @@ -1500,14 +1504,8 @@ def ivp_rhs(t, x): return sys._rhs(t, x, u(t))
else: # Neither ctime or dtime??
raise TypeError("Can't determine system type")

# Get rid of extra dimensions in the output, of desired
if squeeze:
y = np.squeeze(y)

if return_x:
return soln.t, y, soln.y
else:
return soln.t, y
return _process_time_response(sys, soln.t, y, soln.y, transpose=transpose,
return_x=return_x, squeeze=squeeze)


def find_eqpt(sys, x0, u0=[], y0=None, t=0, params={},
Expand Down
38 changes: 18 additions & 20 deletions control/matlab/timeresp.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,13 @@ def step(sys, T=None, X0=0., input=0, output=None, return_x=False):
'''
from ..timeresp import step_response

T, yout, xout = step_response(sys, T, X0, input, output,
transpose=True, return_x=True)
# Switch output argument order and transpose outputs
out = step_response(sys, T, X0, input, output,
transpose=True, return_x=return_x)
return (out[1], out[0], out[2]) if return_x else (out[1], out[0])

if return_x:
return yout, T, xout

return yout, T

def stepinfo(sys, T=None, SettlingTimeThreshold=0.02, RiseTimeLimits=(0.1, 0.9)):
def stepinfo(sys, T=None, SettlingTimeThreshold=0.02,
RiseTimeLimits=(0.1, 0.9)):
'''
Step response characteristics (Rise time, Settling Time, Peak and others).

Expand Down Expand Up @@ -110,6 +108,7 @@ def stepinfo(sys, T=None, SettlingTimeThreshold=0.02, RiseTimeLimits=(0.1, 0.9))
'''
from ..timeresp import step_info

# Call step_info with MATLAB defaults
S = step_info(sys, T, None, SettlingTimeThreshold, RiseTimeLimits)

return S
Expand Down Expand Up @@ -164,13 +163,11 @@ def impulse(sys, T=None, X0=0., input=0, output=None, return_x=False):
>>> yout, T = impulse(sys, T)
'''
from ..timeresp import impulse_response
T, yout, xout = impulse_response(sys, T, X0, input, output,
transpose = True, return_x=True)

if return_x:
return yout, T, xout

return yout, T
# Switch output argument order and transpose outputs
out = impulse_response(sys, T, X0, input, output,
transpose = True, return_x=return_x)
return (out[1], out[0], out[2]) if return_x else (out[1], out[0])

def initial(sys, T=None, X0=0., input=None, output=None, return_x=False):
'''
Expand Down Expand Up @@ -222,13 +219,12 @@ def initial(sys, T=None, X0=0., input=None, output=None, return_x=False):

'''
from ..timeresp import initial_response

# Switch output argument order and transpose outputs
T, yout, xout = initial_response(sys, T, X0, output=output,
transpose=True, return_x=True)
return (yout, T, xout) if return_x else (yout, T)

if return_x:
return yout, T, xout

return yout, T

def lsim(sys, U=0., T=None, X0=0.):
'''
Expand Down Expand Up @@ -273,5 +269,7 @@ def lsim(sys, U=0., T=None, X0=0.):
>>> yout, T, xout = lsim(sys, U, T, X0)
'''
from ..timeresp import forced_response
T, yout, xout = forced_response(sys, T, U, X0, transpose = True)
return yout, T, xout

# Switch output argument order and transpose outputs (and always return x)
out = forced_response(sys, T, U, X0, return_x=True, transpose=True)
return out[1], out[0], out[2]
11 changes: 7 additions & 4 deletions control/tests/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,15 @@ def test_legacy_defaults(self):
ct.use_legacy_defaults('0.8.3')
assert(isinstance(ct.ss(0, 0, 0, 1).D, np.matrix))
ct.reset_defaults()
assert(isinstance(ct.ss(0, 0, 0, 1).D, np.ndarray))
assert(not isinstance(ct.ss(0, 0, 0, 1).D, np.matrix))
assert isinstance(ct.ss(0, 0, 0, 1).D, np.ndarray)
assert not isinstance(ct.ss(0, 0, 0, 1).D, np.matrix)

ct.use_legacy_defaults('0.8.4')
assert ct.config.defaults['forced_response.return_x'] is True

ct.use_legacy_defaults('0.9.0')
assert(isinstance(ct.ss(0, 0, 0, 1).D, np.ndarray))
assert(not isinstance(ct.ss(0, 0, 0, 1).D, np.matrix))
assert isinstance(ct.ss(0, 0, 0, 1).D, np.ndarray)
assert not isinstance(ct.ss(0, 0, 0, 1).D, np.matrix)

# test that old versions don't raise a problem
ct.use_legacy_defaults('REL-0.1')
Expand Down
8 changes: 5 additions & 3 deletions control/tests/discrete_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,11 @@ def testSimulation(self, tsys):
tout, yout = step_response(tsys.siso_ss1d, T)
tout, yout = impulse_response(tsys.siso_ss1d)
tout, yout = impulse_response(tsys.siso_ss1d, T)
tout, yout, xout = forced_response(tsys.siso_ss1d, T, U, 0)
tout, yout, xout = forced_response(tsys.siso_ss2d, T, U, 0)
tout, yout, xout = forced_response(tsys.siso_ss3d, T, U, 0)
tout, yout = forced_response(tsys.siso_ss1d, T, U, 0)
tout, yout = forced_response(tsys.siso_ss2d, T, U, 0)
tout, yout = forced_response(tsys.siso_ss3d, T, U, 0)
tout, yout, xout = forced_response(tsys.siso_ss1d, T, U, 0,
return_x=True)

def test_sample_system(self, tsys):
# Make sure we can convert various types of systems
Expand Down
2 changes: 1 addition & 1 deletion control/tests/flatsys_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_double_integrator(self, xf, uf, Tf):
T = np.linspace(0, Tf, 100)
xd, ud = traj.eval(T)

t, y, x = ct.forced_response(sys, T, ud, x1)
t, y, x = ct.forced_response(sys, T, ud, x1, return_x=True)
np.testing.assert_array_almost_equal(x, xd, decimal=3)

def test_kinematic_car(self):
Expand Down
Loading