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)
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