Skip to content

Commit b337c9c

Browse files
committed
consolidate time/frequency response processing for easier updates
1 parent ad33109 commit b337c9c

File tree

5 files changed

+53
-73
lines changed

5 files changed

+53
-73
lines changed

control/iosys.py

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from warnings import warn
3838

3939
from .statesp import StateSpace, tf2ss
40-
from .timeresp import _check_convert_array
40+
from .timeresp import _check_convert_array, _process_time_response
4141
from .lti import isctime, isdtime, common_timebase
4242
from . import config
4343

@@ -1353,7 +1353,7 @@ def __init__(self, io_sys, ss_sys=None):
13531353

13541354

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

13581358
"""Compute the output response of a system to a given input.
13591359
@@ -1398,9 +1398,6 @@ def input_output_response(sys, T, U=0., X0=0, params={}, method='RK45',
13981398
if not isinstance(sys, InputOutputSystem):
13991399
raise TypeError("System of type ", type(sys), " not valid")
14001400

1401-
if squeeze is None:
1402-
squeeze = config.defaults['control.squeeze']
1403-
14041401
# Compute the time interval and number of steps
14051402
T0, Tf = T[0], T[-1]
14061403
n_steps = len(T)
@@ -1423,12 +1420,8 @@ def input_output_response(sys, T, U=0., X0=0, params={}, method='RK45',
14231420
for i in range(len(T)):
14241421
u = U[i] if len(U.shape) == 1 else U[:, i]
14251422
y[:, i] = sys._out(T[i], [], u)
1426-
if squeeze and y.shape[0] == 1:
1427-
y = y[0]
1428-
if return_x:
1429-
return T, y, []
1430-
else:
1431-
return T, y
1423+
return _process_time_response(sys, T, y, [], transpose=transpose,
1424+
return_x=return_x, squeeze=squeeze)
14321425

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

1506-
# Get rid of extra dimensions in the output, of desired
1507-
if squeeze and y.shape[0] == 1:
1508-
y = y[0]
1509-
1510-
if return_x:
1511-
return soln.t, y, soln.y
1512-
else:
1513-
return soln.t, y
1499+
return _process_time_response(sys, soln.t, y, soln.y, transpose=transpose,
1500+
return_x=return_x, squeeze=squeeze)
15141501

15151502

15161503
def find_eqpt(sys, x0, u0=[], y0=None, t=0, params={},

control/lti.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import numpy as np
1616
from numpy import absolute, real, angle, abs
1717
from warnings import warn
18+
from . import config
1819

1920
__all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual',
2021
'isdtime', 'isctime', 'pole', 'zero', 'damp', 'evalfr',
@@ -596,3 +597,18 @@ def dcgain(sys):
596597
at the origin
597598
"""
598599
return sys.dcgain()
600+
601+
602+
# Process frequency responses in a uniform way
603+
def _process_frequency_response(sys, omega, out, squeeze=None):
604+
if not hasattr(omega, '__len__'):
605+
# received a scalar x, squeeze down the array along last dim
606+
out = np.squeeze(out, axis=2)
607+
608+
# Get rid of unneeded dimensions
609+
if squeeze is None:
610+
squeeze = config.defaults['control.squeeze']
611+
if squeeze and sys.issiso():
612+
return out[0][0]
613+
else:
614+
return out

control/statesp.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
from scipy.signal import cont2discrete
6363
from scipy.signal import StateSpace as signalStateSpace
6464
from warnings import warn
65-
from .lti import LTI, common_timebase, isdtime
65+
from .lti import LTI, common_timebase, isdtime, _process_frequency_response
6666
from . import config
6767
from copy import deepcopy
6868

@@ -671,19 +671,9 @@ def __call__(self, x, squeeze=None):
671671
only if system is SISO and ``squeeze=True``.
672672
673673
"""
674-
# Set value of squeeze argument if not set
675-
if squeeze is None:
676-
squeeze = config.defaults['control.squeeze']
677-
678674
# Use Slycot if available
679675
out = self.horner(x)
680-
if not hasattr(x, '__len__'):
681-
# received a scalar x, squeeze down the array along last dim
682-
out = np.squeeze(out, axis=2)
683-
if squeeze and self.issiso():
684-
return out[0][0]
685-
else:
686-
return out
676+
return _process_frequency_response(self, x, out, squeeze=squeeze)
687677

688678
def slycot_laub(self, x):
689679
"""Evaluate system's transfer function at complex frequency

control/timeresp.py

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,6 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False,
277277
if not isinstance(sys, LTI):
278278
raise TypeError('Parameter ``sys``: must be a ``LTI`` object. '
279279
'(For example ``StateSpace`` or ``TransferFunction``)')
280-
if squeeze is None:
281-
squeeze = config.defaults['control.squeeze']
282280

283281
sys = _convert_to_statespace(sys)
284282
A, B, C, D = np.asarray(sys.A), np.asarray(sys.B), np.asarray(sys.C), \
@@ -433,7 +431,16 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False,
433431
xout = np.transpose(xout)
434432
yout = np.transpose(yout)
435433

434+
return _process_time_response(sys, tout, yout, xout, transpose=transpose,
435+
return_x=return_x, squeeze=squeeze)
436+
437+
438+
# Process time responses in a uniform way
439+
def _process_time_response(sys, tout, yout, xout, transpose=None,
440+
return_x=False, squeeze=None):
436441
# Get rid of unneeded dimensions
442+
if squeeze is None:
443+
squeeze = config.defaults['control.squeeze']
437444
if squeeze and yout.shape[0] == 1:
438445
yout = yout[0]
439446

@@ -443,10 +450,8 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False,
443450
yout = np.transpose(yout)
444451
xout = np.transpose(xout)
445452

446-
if return_x:
447-
return tout, yout, xout
448-
449-
return tout, yout
453+
# Return time, output, and (optionally) state
454+
return (tout, yout, xout) if return_x else (tout, yout)
450455

451456

452457
def _get_ss_simo(sys, input=None, output=None):
@@ -640,7 +645,7 @@ def step_info(sys, T=None, T_num=None, SettlingTimeThreshold=0.02,
640645
'SettlingMin': yout[tr_upper_index:].min(),
641646
'SettlingMax': yout.max(),
642647
'Overshoot': 100. * (yout.max() - InfValue) / (InfValue - yout[0]),
643-
'Undershoot': yout.min(), # not very confident about this
648+
'Undershoot': yout.min(), # not very confident about this
644649
'Peak': yout[PeakIndex],
645650
'PeakTime': T[PeakIndex],
646651
'SteadyStateValue': InfValue
@@ -728,13 +733,8 @@ def initial_response(sys, T=None, X0=0., input=0, output=None, T_num=None,
728733
T = _default_time_vector(sys, N=T_num, tfinal=T, is_step=False)
729734
U = np.zeros_like(T)
730735

731-
T, yout, _xout = forced_response(sys, T, U, X0, transpose=transpose,
732-
return_x=True, squeeze=squeeze)
733-
734-
if return_x:
735-
return T, yout, _xout
736-
737-
return T, yout
736+
return forced_response(sys, T, U, X0, transpose=transpose,
737+
return_x=return_x, squeeze=squeeze)
738738

739739

740740
def impulse_response(sys, T=None, X0=0., input=None, output=None, T_num=None,
@@ -841,15 +841,11 @@ def impulse_response(sys, T=None, X0=0., input=None, output=None, T_num=None,
841841
new_X0 = B + X0
842842
else:
843843
new_X0 = X0
844-
U[0] = 1./sys.dt # unit area impulse
844+
U[0] = 1./sys.dt # unit area impulse
845845

846-
T, yout, _xout = forced_response(sys, T, U, new_X0, transpose=transpose,
847-
return_x=True, squeeze=squeeze)
846+
return forced_response(sys, T, U, new_X0, transpose=transpose,
847+
return_x=return_x, squeeze=squeeze)
848848

849-
if return_x:
850-
return T, yout, _xout
851-
852-
return T, yout
853849

854850
# utility function to find time period and time increment using pole locations
855851
def _ideal_tfinal_and_dt(sys, is_step=True):
@@ -949,7 +945,7 @@ def _ideal_tfinal_and_dt(sys, is_step=True):
949945

950946
if p_int.size > 0:
951947
tfinal = tfinal * 5
952-
else: # cont time
948+
else: # cont time
953949
sys_ss = _convert_to_statespace(sys)
954950
# Improve conditioning via balancing and zeroing tiny entries
955951
# See <w,v> for [[1, 2, 0], [9, 1, 0.01], [1, 2, 10*np.pi]]
@@ -977,7 +973,7 @@ def _ideal_tfinal_and_dt(sys, is_step=True):
977973
dc = np.zeros_like(p, dtype=float)
978974
# well-conditioned nonzero poles, np.abs just in case
979975
ok = np.abs(eig_sens) <= 1/sqrt_eps
980-
# the averaged t->inf response of each simple eigval on each i/o channel
976+
# averaged t->inf response of each simple eigval on each i/o channel
981977
# See, A = [[-1, k], [0, -2]], response sizes are k-dependent (that is
982978
# R/L eigenvector dependent)
983979
dc[ok] = norm(v[ok, :], axis=1)*norm(w[:, ok], axis=0)*eig_sens[ok]
@@ -1003,7 +999,7 @@ def _ideal_tfinal_and_dt(sys, is_step=True):
1003999
texp_mode = log_decay_percent / np.abs(psub[~iw & ~ints].real)
10041000
tfinal += texp_mode.tolist()
10051001
dt += minimum(texp_mode / 50,
1006-
(2 * np.pi / pts_per_cycle / wnsub[~iw & ~ints])).tolist()
1002+
(2*np.pi / pts_per_cycle / wnsub[~iw & ~ints])).tolist()
10071003

10081004
# All integrators?
10091005
if len(tfinal) == 0:
@@ -1014,30 +1010,32 @@ def _ideal_tfinal_and_dt(sys, is_step=True):
10141010

10151011
return tfinal, dt
10161012

1013+
10171014
def _default_time_vector(sys, N=None, tfinal=None, is_step=True):
10181015
"""Returns a time vector that has a reasonable number of points.
10191016
if system is discrete-time, N is ignored """
10201017

10211018
N_max = 5000
1022-
N_min_ct = 100 # min points for cont time systems
1023-
N_min_dt = 20 # more common to see just a few samples in discrete-time
1019+
N_min_ct = 100 # min points for cont time systems
1020+
N_min_dt = 20 # more common to see fewer samples in discrete-time
10241021

10251022
ideal_tfinal, ideal_dt = _ideal_tfinal_and_dt(sys, is_step=is_step)
10261023

10271024
if isdtime(sys, strict=True):
10281025
# only need to use default_tfinal if not given; N is ignored.
10291026
if tfinal is None:
10301027
# for discrete time, change from ideal_tfinal if N too large/small
1031-
N = int(np.clip(ideal_tfinal/sys.dt, N_min_dt, N_max))# [N_min, N_max]
1028+
N = int(np.clip(ideal_tfinal/sys.dt, N_min_dt, N_max))
10321029
tfinal = sys.dt * N
10331030
else:
10341031
N = int(tfinal/sys.dt)
1035-
tfinal = N * sys.dt # make tfinal an integer multiple of sys.dt
1032+
tfinal = N * sys.dt # make tfinal an integer multiple of sys.dt
10361033
else:
10371034
if tfinal is None:
10381035
# for continuous time, simulate to ideal_tfinal but limit N
10391036
tfinal = ideal_tfinal
10401037
if N is None:
1041-
N = int(np.clip(tfinal/ideal_dt, N_min_ct, N_max)) # N<-[N_min, N_max]
1038+
# N <- [N_min, N_max]
1039+
N = int(np.clip(tfinal/ideal_dt, N_min_ct, N_max))
10421040

10431041
return np.linspace(0, tfinal, N, endpoint=False)

control/xferfcn.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
from warnings import warn
6464
from itertools import chain
6565
from re import sub
66-
from .lti import LTI, common_timebase, isdtime
66+
from .lti import LTI, common_timebase, isdtime, _process_frequency_response
6767
from . import config
6868

6969
__all__ = ['TransferFunction', 'tf', 'ss2tf', 'tfdata']
@@ -265,19 +265,8 @@ def __call__(self, x, squeeze=None):
265265
only if system is SISO and ``squeeze=True``.
266266
267267
"""
268-
# Set value of squeeze argument if not set
269-
if squeeze is None:
270-
squeeze = config.defaults['control.squeeze']
271-
272268
out = self.horner(x)
273-
if not hasattr(x, '__len__'):
274-
# received a scalar x, squeeze down the array along last dim
275-
out = np.squeeze(out, axis=2)
276-
if squeeze and self.issiso():
277-
# return a scalar/1d array of outputs
278-
return out[0][0]
279-
else:
280-
return out
269+
return _process_frequency_response(self, x, out, squeeze=squeeze)
281270

282271
def horner(self, x):
283272
"""Evaluate system's transfer function at complex frequency

0 commit comments

Comments
 (0)