Skip to content

Commit 1ed9ee2

Browse files
sawyerbfullerbnavigator
authored andcommitted
changes so that a default dt=0 passes unit tests. fixed code everywhere that combines systems with different timebases
1 parent 86401d6 commit 1ed9ee2

7 files changed

Lines changed: 116 additions & 182 deletions

File tree

control/config.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ def reset_defaults():
5959
from .statesp import _statesp_defaults
6060
defaults.update(_statesp_defaults)
6161

62+
from .iosys import _iosys_defaults
63+
defaults.update(_iosys_defaults)
64+
6265

6366
def _get_param(module, param, argval=None, defval=None, pop=False):
6467
"""Return the default value for a configuration option.
@@ -208,8 +211,13 @@ def use_legacy_defaults(version):
208211
# Go backwards through releases and reset defaults
209212
#
210213

211-
# Version 0.9.0: switched to 'array' as default for state space objects
214+
# Version 0.9.0:
212215
if major == 0 and minor < 9:
216+
# switched to 'array' as default for state space objects
213217
set_defaults('statesp', use_numpy_matrix=True)
218+
# switched to 0 (=continuous) as default timestep
219+
set_defaults('statesp', default_dt=None)
220+
set_defaults('xferfcn', default_dt=None)
221+
set_defaults('iosys', default_dt=None)
214222

215223
return (major, minor, patch)

control/iosys.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,16 @@
3838

3939
from .statesp import StateSpace, tf2ss
4040
from .timeresp import _check_convert_array
41-
from .lti import isctime, isdtime, _find_timebase
41+
from .lti import isctime, isdtime, common_timebase
42+
from . import config
4243

4344
__all__ = ['InputOutputSystem', 'LinearIOSystem', 'NonlinearIOSystem',
4445
'InterconnectedSystem', 'input_output_response', 'find_eqpt',
4546
'linearize', 'ss2io', 'tf2io']
4647

48+
# Define module default parameter values
49+
_iosys_defaults = {
50+
'iosys.default_dt': 0}
4751

4852
class InputOutputSystem(object):
4953
"""A class for representing input/output systems.
@@ -118,7 +122,7 @@ def name_or_default(self, name=None):
118122
return name
119123

120124
def __init__(self, inputs=None, outputs=None, states=None, params={},
121-
dt=None, name=None):
125+
name=None, **kwargs):
122126
"""Create an input/output system.
123127
124128
The InputOutputSystem contructor is used to create an input/output
@@ -163,7 +167,7 @@ def __init__(self, inputs=None, outputs=None, states=None, params={},
163167
"""
164168
# Store the input arguments
165169
self.params = params.copy() # default parameters
166-
self.dt = dt # timebase
170+
self.dt = kwargs.get('dt', config.defaults['iosys.default_dt']) # timebase
167171
self.name = self.name_or_default(name) # system name
168172

169173
# Parse and store the number of inputs, outputs, and states
@@ -210,9 +214,7 @@ def __mul__(sys2, sys1):
210214
"inputs and outputs")
211215

212216
# Make sure timebase are compatible
213-
dt = _find_timebase(sys1, sys2)
214-
if dt is False:
215-
raise ValueError("System timebases are not compabile")
217+
dt = common_timebase(sys1.dt, sys2.dt)
216218

217219
inplist = [(0,i) for i in range(sys1.ninputs)]
218220
outlist = [(1,i) for i in range(sys2.noutputs)]
@@ -464,12 +466,11 @@ def feedback(self, other=1, sign=-1, params={}):
464466
"inputs and outputs")
465467

466468
# Make sure timebases are compatible
467-
dt = _find_timebase(self, other)
468-
if dt is False:
469-
raise ValueError("System timebases are not compabile")
469+
dt = common_timebase(self.dt, other.dt)
470470

471471
inplist = [(0,i) for i in range(self.ninputs)]
472472
outlist = [(0,i) for i in range(self.noutputs)]
473+
473474
# Return the series interconnection between the systems
474475
newsys = InterconnectedSystem((self, other), inplist=inplist, outlist=outlist,
475476
params=params, dt=dt)
@@ -650,7 +651,8 @@ class NonlinearIOSystem(InputOutputSystem):
650651
651652
"""
652653
def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None,
653-
states=None, params={}, dt=None, name=None):
654+
states=None, params={},
655+
name=None, **kwargs):
654656
"""Create a nonlinear I/O system given update and output functions.
655657
656658
Creates an `InputOutputSystem` for a nonlinear system by specifying a
@@ -722,6 +724,7 @@ def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None,
722724
self.outfcn = outfcn
723725

724726
# Initialize the rest of the structure
727+
dt = kwargs.get('dt', config.defaults['iosys.default_dt'])
725728
super(NonlinearIOSystem, self).__init__(
726729
inputs=inputs, outputs=outputs, states=states,
727730
params=params, dt=dt, name=name
@@ -888,20 +891,14 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
888891
# Check to make sure all systems are consistent
889892
self.syslist = syslist
890893
self.syslist_index = {}
891-
dt = None
892894
nstates = 0; self.state_offset = []
893895
ninputs = 0; self.input_offset = []
894896
noutputs = 0; self.output_offset = []
895897
sysobj_name_dct = {}
896898
sysname_count_dct = {}
897899
for sysidx, sys in enumerate(syslist):
898900
# Make sure time bases are consistent
899-
# TODO: Use lti._find_timebase() instead?
900-
if dt is None and sys.dt is not None:
901-
# Timebase was not specified; set to match this system
902-
dt = sys.dt
903-
elif dt != sys.dt:
904-
raise TypeError("System timebases are not compatible")
901+
dt = common_timebase(dt, sys.dt)
905902

906903
# Make sure number of inputs, outputs, states is given
907904
if sys.ninputs is None or sys.noutputs is None or \

control/lti.py

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
isdtime()
1010
isctime()
1111
timebase()
12-
timebaseEqual()
12+
common_timebase()
1313
"""
1414

1515
import numpy as np
1616
from numpy import absolute, real
1717

18-
__all__ = ['issiso', 'timebase', 'timebaseEqual', 'isdtime', 'isctime',
18+
__all__ = ['issiso', 'timebase', 'common_timebase', 'isdtime', 'isctime',
1919
'pole', 'zero', 'damp', 'evalfr', 'freqresp', 'dcgain']
2020

2121
class LTI:
@@ -157,48 +157,31 @@ def timebase(sys, strict=True):
157157

158158
return sys.dt
159159

160-
# Check to see if two timebases are equal
161-
def timebaseEqual(sys1, sys2):
162-
"""Check to see if two systems have the same timebase
163-
164-
timebaseEqual(sys1, sys2)
165-
166-
returns True if the timebases for the two systems are compatible. By
167-
default, systems with timebase 'None' are compatible with either
168-
discrete or continuous timebase systems. If two systems have a discrete
169-
timebase (dt > 0) then their timebases must be equal.
170-
"""
171-
172-
if (type(sys1.dt) == bool or type(sys2.dt) == bool):
173-
# Make sure both are unspecified discrete timebases
174-
return type(sys1.dt) == type(sys2.dt) and sys1.dt == sys2.dt
175-
elif (sys1.dt is None or sys2.dt is None):
176-
# One or the other is unspecified => the other can be anything
177-
return True
178-
else:
179-
return sys1.dt == sys2.dt
180-
181-
# Find a common timebase between two or more systems
182-
def _find_timebase(sys1, *sysn):
183-
"""Find the common timebase between systems, otherwise return False"""
184-
185-
# Create a list of systems to check
186-
syslist = [sys1]
187-
syslist.append(*sysn)
188-
189-
# Look for a common timebase
190-
dt = None
191-
192-
for sys in syslist:
193-
# Make sure time bases are consistent
194-
if (dt is None and sys.dt is not None) or \
195-
(dt is True and isdiscrete(sys)):
196-
# Timebase was not specified; set to match this system
197-
dt = sys.dt
198-
elif dt != sys.dt:
199-
return False
200-
return dt
201-
160+
def common_timebase(dt1, dt2):
161+
"""Find the common timebase when interconnecting systems."""
162+
# cases:
163+
# if either dt is None, they are compatible with anything
164+
# if either dt is True (discrete with unspecified time base),
165+
# use the timebase of the other, if it is also discrete
166+
# otherwise they must be equal (holds for both cont and discrete systems)
167+
if dt1 is None:
168+
return dt2
169+
elif dt2 is None:
170+
return dt1
171+
elif dt1 is True:
172+
if dt2 > 0:
173+
return dt2
174+
else:
175+
raise ValueError("Systems have incompatible timebases")
176+
elif dt2 is True:
177+
if dt1 > 0:
178+
return dt1
179+
else:
180+
raise ValueError("Systems have incompatible timebases")
181+
elif np.isclose(dt1, dt2):
182+
return dt1
183+
else:
184+
raise ValueError("Systems have incompatible timebases")
202185

203186
# Check to see if a system is a discrete time system
204187
def isdtime(sys, strict=False):

control/statesp.py

Lines changed: 25 additions & 46 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, timebase, timebaseEqual, isdtime
65+
from .lti import LTI, common_timebase, isdtime
6666
from . import config
6767
from copy import deepcopy
6868

@@ -180,7 +180,10 @@ def __init__(self, *args, **kw):
180180
if len(args) == 4:
181181
# The user provided A, B, C, and D matrices.
182182
(A, B, C, D) = args
183-
dt = config.defaults['statesp.default_dt']
183+
if _isstaticgain(A, B, C, D):
184+
dt = None
185+
else:
186+
dt = config.defaults['statesp.default_dt']
184187
elif len(args) == 5:
185188
# Discrete time system
186189
(A, B, C, D, dt) = args
@@ -196,9 +199,12 @@ def __init__(self, *args, **kw):
196199
try:
197200
dt = args[0].dt
198201
except NameError:
199-
dt = config.defaults['statesp.default_dt']
202+
if _isstaticgain(A, B, C, D):
203+
dt = None
204+
else:
205+
dt = config.defaults['statesp.default_dt']
200206
else:
201-
raise ValueError("Needs 1 or 4 arguments; received %i." % len(args))
207+
raise ValueError("Expected 1, 4, or 5 arguments; received %i." % len(args))
202208

203209
# Process keyword arguments
204210
remove_useless = kw.get('remove_useless',
@@ -330,14 +336,7 @@ def __add__(self, other):
330336
(self.outputs != other.outputs)):
331337
raise ValueError("Systems have different shapes.")
332338

333-
# Figure out the sampling time to use
334-
if self.dt is None and other.dt is not None:
335-
dt = other.dt # use dt from second argument
336-
elif (other.dt is None and self.dt is not None) or \
337-
(timebaseEqual(self, other)):
338-
dt = self.dt # use dt from first argument
339-
else:
340-
raise ValueError("Systems have different sampling times")
339+
dt = common_timebase(self.dt, other.dt)
341340

342341
# Concatenate the various arrays
343342
A = concatenate((
@@ -386,16 +385,8 @@ def __mul__(self, other):
386385
# Check to make sure the dimensions are OK
387386
if self.inputs != other.outputs:
388387
raise ValueError("C = A * B: A has %i column(s) (input(s)), \
389-
but B has %i row(s)\n(output(s))." % (self.inputs, other.outputs))
390-
391-
# Figure out the sampling time to use
392-
if (self.dt == None and other.dt != None):
393-
dt = other.dt # use dt from second argument
394-
elif (other.dt == None and self.dt != None) or \
395-
(timebaseEqual(self, other)):
396-
dt = self.dt # use dt from first argument
397-
else:
398-
raise ValueError("Systems have different sampling times")
388+
but B has %i row(s)\n(output(s))." % (self.inputs, other.outputs))
389+
dt = common_timebase(self.dt, other.dt)
399390

400391
# Concatenate the various arrays
401392
A = concatenate(
@@ -467,9 +458,8 @@ def _evalfr(self, omega):
467458
"""Evaluate a SS system's transfer function at a single frequency"""
468459
# Figure out the point to evaluate the transfer function
469460
if isdtime(self, strict=True):
470-
dt = timebase(self)
471-
s = exp(1.j * omega * dt)
472-
if omega * dt > math.pi:
461+
s = exp(1.j * omega * self.dt)
462+
if omega * self.dt > math.pi:
473463
warn("_evalfr: frequency evaluation above Nyquist frequency")
474464
else:
475465
s = omega * 1.j
@@ -526,9 +516,8 @@ def freqresp(self, omega):
526516
# axis (continuous time) or unit circle (discrete time).
527517
omega.sort()
528518
if isdtime(self, strict=True):
529-
dt = timebase(self)
530-
cmplx_freqs = exp(1.j * omega * dt)
531-
if max(np.abs(omega)) * dt > math.pi:
519+
cmplx_freqs = exp(1.j * omega * self.dt)
520+
if max(np.abs(omega)) * self.dt > math.pi:
532521
warn("freqresp: frequency evaluation above Nyquist frequency")
533522
else:
534523
cmplx_freqs = omega * 1.j
@@ -631,14 +620,7 @@ def feedback(self, other=1, sign=-1):
631620
if (self.inputs != other.outputs) or (self.outputs != other.inputs):
632621
raise ValueError("State space systems don't have compatible inputs/outputs for "
633622
"feedback.")
634-
635-
# Figure out the sampling time to use
636-
if self.dt is None and other.dt is not None:
637-
dt = other.dt # use dt from second argument
638-
elif other.dt is None and self.dt is not None or timebaseEqual(self, other):
639-
dt = self.dt # use dt from first argument
640-
else:
641-
raise ValueError("Systems have different sampling times")
623+
dt = common_timebase(self.dt, other.dt)
642624

643625
A1 = self.A
644626
B1 = self.B
@@ -708,14 +690,7 @@ def lft(self, other, nu=-1, ny=-1):
708690
# dimension check
709691
# TODO
710692

711-
# Figure out the sampling time to use
712-
if (self.dt == None and other.dt != None):
713-
dt = other.dt # use dt from second argument
714-
elif (other.dt == None and self.dt != None) or \
715-
timebaseEqual(self, other):
716-
dt = self.dt # use dt from first argument
717-
else:
718-
raise ValueError("Systems have different time bases")
693+
dt = common_timebase(self.dt, other.dt)
719694

720695
# submatrices
721696
A = self.A
@@ -858,8 +833,7 @@ def append(self, other):
858833
if not isinstance(other, StateSpace):
859834
other = _convertToStateSpace(other)
860835

861-
if self.dt != other.dt:
862-
raise ValueError("Systems must have the same time step")
836+
self.dt = common_timebase(self.dt, other.dt)
863837

864838
n = self.states + other.states
865839
m = self.inputs + other.inputs
@@ -1293,6 +1267,11 @@ def _mimo2simo(sys, input, warn_conversion=False):
12931267

12941268
return sys
12951269

1270+
def _isstaticgain(A, B, C, D):
1271+
"""returns True if and only if the system has no dynamics, that is,
1272+
if A and B are zero. """
1273+
return not np.any(np.matrix(A, dtype=float)) \
1274+
and not np.any(np.matrix(B, dtype=float))
12961275

12971276
def ss(*args):
12981277
"""ss(A, B, C, D[, dt])

control/tests/config_test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,8 @@ def test_change_default_dt(self, dt):
246246

247247
def test_change_default_dt_static(self):
248248
"""Test that static gain systems always have dt=None"""
249-
ct.set_defaults('control', default_dt=0)
249+
ct.set_defaults('xferfcn', default_dt=0)
250250
assert ct.tf(1, 1).dt is None
251+
ct.set_defaults('statesp', default_dt=0)
251252
assert ct.ss(0, 0, 0, 1).dt is None
252253
# TODO: add in test for static gain iosys

0 commit comments

Comments
 (0)