Skip to content

Commit ccbd7ac

Browse files
authored
fix discete time simulation time step issue #332 (#356)
1 parent ed4a963 commit ccbd7ac

File tree

2 files changed

+46
-4
lines changed

2 files changed

+46
-4
lines changed

control/tests/timeresp_test.py

+39
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,45 @@ def test_time_vector(self):
471471
squeeze=False)
472472
self.assertTrue(isinstance(context.exception, ValueError))
473473

474+
def test_discrete_time_steps(self):
475+
"""Make sure rounding errors in sample time are handled properly"""
476+
# See https://github.com/python-control/python-control/issues/332)
477+
#
478+
# These tests play around with the input time vector to make sure that
479+
# small rounding errors don't generate spurious errors.
480+
481+
# Discrete time system to use for simulation
482+
# self.siso_dtf2 = TransferFunction([1], [1, 1, 0.25], 0.2)
483+
484+
# Set up a time range and simulate
485+
T = np.arange(0, 100, 0.2)
486+
tout1, yout1 = step_response(self.siso_dtf2, T)
487+
488+
# Simulate every other time step
489+
T = np.arange(0, 100, 0.4)
490+
tout2, yout2 = step_response(self.siso_dtf2, T)
491+
np.testing.assert_array_almost_equal(tout1[::2], tout2)
492+
np.testing.assert_array_almost_equal(yout1[::2], yout2)
493+
494+
# Add a small error into some of the time steps
495+
T = np.arange(0, 100, 0.2)
496+
T[1:-2:2] -= 1e-12 # tweak second value and a few others
497+
tout3, yout3 = step_response(self.siso_dtf2, T)
498+
np.testing.assert_array_almost_equal(tout1, tout3)
499+
np.testing.assert_array_almost_equal(yout1, yout3)
500+
501+
# Add a small error into some of the time steps (w/ skipping)
502+
T = np.arange(0, 100, 0.4)
503+
T[1:-2:2] -= 1e-12 # tweak second value and a few others
504+
tout4, yout4 = step_response(self.siso_dtf2, T)
505+
np.testing.assert_array_almost_equal(tout2, tout4)
506+
np.testing.assert_array_almost_equal(yout2, yout4)
507+
508+
# Make sure larger errors *do* generate an error
509+
T = np.arange(0, 100, 0.2)
510+
T[1:-2:2] -= 1e-3 # change second value and a few others
511+
self.assertRaises(ValueError, step_response, self.siso_dtf2, T)
512+
474513
def test_time_series_data_convention(self):
475514
"""Make sure time series data matches documentation conventions"""
476515
# SISO continuous time

control/timeresp.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,8 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False,
365365
# Make sure that the time increment is a multiple of sampling time
366366

367367
# First make sure that time increment is bigger than sampling time
368-
if dt < sys.dt:
368+
# (with allowance for small precision errors)
369+
if dt < sys.dt and not np.isclose(dt, sys.dt):
369370
raise ValueError("Time steps ``T`` must match sampling time")
370371

371372
# Now check to make sure it is a multiple (with check against
@@ -374,19 +375,21 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False,
374375
np.isclose(dt % sys.dt, sys.dt)):
375376
raise ValueError("Time steps ``T`` must be multiples of "
376377
"sampling time")
378+
sys_dt = sys.dt
379+
377380
else:
378-
sys.dt = dt # For unspecified sampling time, use time incr
381+
sys_dt = dt # For unspecified sampling time, use time incr
379382

380383
# Discrete time simulation using signal processing toolbox
381-
dsys = (A, B, C, D, sys.dt)
384+
dsys = (A, B, C, D, sys_dt)
382385

383386
# Use signal processing toolbox for the discrete time simulation
384387
# Transpose the input to match toolbox convention
385388
tout, yout, xout = sp.signal.dlsim(dsys, np.transpose(U), T, X0)
386389

387390
if not interpolate:
388391
# If dt is different from sys.dt, resample the output
389-
inc = int(round(dt / sys.dt))
392+
inc = int(round(dt / sys_dt))
390393
tout = T # Return exact list of time steps
391394
yout = yout[::inc, :]
392395
xout = xout[::inc, :]

0 commit comments

Comments
 (0)