Skip to content

Commit c765811

Browse files
authored
fix time series inconsistency between continuous and discrete time simulations (#295)
* fix time series inconsistency between continuous and discrete time * code cleanup in response to review
1 parent d8d0a9c commit c765811

3 files changed

Lines changed: 149 additions & 49 deletions

File tree

control/tests/timeresp_test.py

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
import unittest
1212
import numpy as np
13-
# import scipy as sp
1413
from control.timeresp import *
1514
from control.statesp import *
1615
from control.xferfcn import TransferFunction, _convert_to_transfer_function
@@ -309,7 +308,7 @@ def check(u, x0, xtrue):
309308
def test_discrete_initial(self):
310309
h1 = TransferFunction([1.], [1., 0.], 1.)
311310
t, yout = impulse_response(h1, np.arange(4))
312-
np.testing.assert_array_equal(yout[0], [0., 1., 0., 0.])
311+
np.testing.assert_array_equal(yout, [0., 1., 0., 0.])
313312

314313
@unittest.skipIf(not slycot_check(), "slycot not installed")
315314
def test_step_robustness(self):
@@ -345,27 +344,32 @@ def test_time_vector(self):
345344
# No timebase in system => output should match input
346345
#
347346
# Initial response
348-
tout, yout = initial_response(self.siso_dtf1, Tin2, siso_x0)
347+
tout, yout = initial_response(self.siso_dtf1, Tin2, siso_x0,
348+
squeeze=False)
349349
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
350350
np.testing.assert_array_equal(tout, Tin2)
351351

352352
# Impulse response
353-
tout, yout = impulse_response(self.siso_dtf1, Tin2)
353+
tout, yout = impulse_response(self.siso_dtf1, Tin2,
354+
squeeze=False)
354355
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
355356
np.testing.assert_array_equal(tout, Tin2)
356357

357358
# Step response
358-
tout, yout = step_response(self.siso_dtf1, Tin2)
359+
tout, yout = step_response(self.siso_dtf1, Tin2,
360+
squeeze=False)
359361
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
360362
np.testing.assert_array_equal(tout, Tin2)
361363

362364
# Forced response with specified time vector
363-
tout, yout, xout = forced_response(self.siso_dtf1, Tin2, np.sin(Tin2))
365+
tout, yout, xout = forced_response(self.siso_dtf1, Tin2, np.sin(Tin2),
366+
squeeze=False)
364367
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
365368
np.testing.assert_array_equal(tout, Tin2)
366369

367370
# Forced response with no time vector, no sample time (should use 1)
368-
tout, yout, xout = forced_response(self.siso_dtf1, None, np.sin(Tin1))
371+
tout, yout, xout = forced_response(self.siso_dtf1, None, np.sin(Tin1),
372+
squeeze=False)
369373
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
370374
np.testing.assert_array_equal(tout, Tin1)
371375

@@ -380,49 +384,58 @@ def test_time_vector(self):
380384
# Matching timebase in system => output should match input
381385
#
382386
# Initial response
383-
tout, yout = initial_response(self.siso_dtf2, Tin2, siso_x0)
387+
tout, yout = initial_response(self.siso_dtf2, Tin2, siso_x0,
388+
squeeze=False)
384389
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
385390
np.testing.assert_array_equal(tout, Tin2)
386391

387392
# Impulse response
388-
tout, yout = impulse_response(self.siso_dtf2, Tin2)
393+
tout, yout = impulse_response(self.siso_dtf2, Tin2,
394+
squeeze=False)
389395
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
390396
np.testing.assert_array_equal(tout, Tin2)
391397

392398
# Step response
393-
tout, yout = step_response(self.siso_dtf2, Tin2)
399+
tout, yout = step_response(self.siso_dtf2, Tin2,
400+
squeeze=False)
394401
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
395402
np.testing.assert_array_equal(tout, Tin2)
396403

397404
# Forced response
398-
tout, yout, xout = forced_response(self.siso_dtf2, Tin2, np.sin(Tin2))
405+
tout, yout, xout = forced_response(self.siso_dtf2, Tin2, np.sin(Tin2),
406+
squeeze=False)
399407
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
400408
np.testing.assert_array_equal(tout, Tin2)
401409

402410
# Forced response with no time vector, use sample time
403-
tout, yout, xout = forced_response(self.siso_dtf2, None, np.sin(Tin2))
411+
tout, yout, xout = forced_response(self.siso_dtf2, None, np.sin(Tin2),
412+
squeeze=False)
404413
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
405414
np.testing.assert_array_equal(tout, Tin2)
406415

407416
# Compatible timebase in system => output should match input
408417
#
409418
# Initial response
410-
tout, yout = initial_response(self.siso_dtf2, Tin1, siso_x0)
419+
tout, yout = initial_response(self.siso_dtf2, Tin1, siso_x0,
420+
squeeze=False)
411421
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
412422
np.testing.assert_array_equal(tout, Tin1)
413423

414424
# Impulse response
415-
tout, yout = impulse_response(self.siso_dtf2, Tin1)
425+
tout, yout = impulse_response(self.siso_dtf2, Tin1,
426+
squeeze=False)
416427
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
417428
np.testing.assert_array_equal(tout, Tin1)
418429

419430
# Step response
420-
tout, yout = step_response(self.siso_dtf2, Tin1)
431+
tout, yout = step_response(self.siso_dtf2, Tin1,
432+
squeeze=False)
421433
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
422434
np.testing.assert_array_equal(tout, Tin1)
423435

424436
# Forced response
425-
tout, yout, xout = forced_response(self.siso_dtf2, Tin1, np.sin(Tin1))
437+
tout, yout, xout = forced_response(self.siso_dtf2, Tin1, np.sin(Tin1),
438+
squeeze=False)
426439
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
427440
np.testing.assert_array_equal(tout, Tin1)
428441

@@ -431,7 +444,8 @@ def test_time_vector(self):
431444
#
432445
# Initial response
433446
tout, yout, xout = forced_response(self.siso_dtf2, Tin1,
434-
np.sin(Tin1), interpolate=True)
447+
np.sin(Tin1), interpolate=True,
448+
squeeze=False)
435449
self.assertEqual(np.shape(tout), np.shape(yout[0,:]))
436450
self.assertTrue(np.allclose(tout[1:] - tout[:-1], self.siso_dtf2.dt))
437451

@@ -442,9 +456,62 @@ def test_time_vector(self):
442456
#
443457
# Initial response
444458
with self.assertRaises(Exception) as context:
445-
tout, yout = initial_response(self.siso_dtf2, Tin3, siso_x0)
459+
tout, yout = initial_response(self.siso_dtf2, Tin3, siso_x0,
460+
squeeze=False)
446461
self.assertTrue(isinstance(context.exception, ValueError))
447462

463+
def test_time_series_data_convention(self):
464+
"""Make sure time series data matches documentation conventions"""
465+
# SISO continuous time
466+
t, y = step_response(self.siso_ss1)
467+
self.assertTrue(isinstance(t, np.ndarray)
468+
and not isinstance(t, np.matrix))
469+
self.assertTrue(len(t.shape) == 1)
470+
self.assertTrue(len(y.shape) == 1) # SISO returns "scalar" output
471+
self.assertTrue(len(t) == len(y)) # Allows direct plotting of output
472+
473+
# SISO discrete time
474+
t, y = step_response(self.siso_dss1)
475+
self.assertTrue(isinstance(t, np.ndarray)
476+
and not isinstance(t, np.matrix))
477+
self.assertTrue(len(t.shape) == 1)
478+
self.assertTrue(len(y.shape) == 1) # SISO returns "scalar" output
479+
self.assertTrue(len(t) == len(y)) # Allows direct plotting of output
480+
481+
# MIMO continuous time
482+
tin = np.linspace(0, 10, 100)
483+
uin = [np.sin(tin), np.cos(tin)]
484+
t, y, x = forced_response(self.mimo_ss1, tin, uin)
485+
self.assertTrue(isinstance(t, np.ndarray)
486+
and not isinstance(t, np.matrix))
487+
self.assertTrue(len(t.shape) == 1)
488+
self.assertTrue(len(y[0].shape) == 1)
489+
self.assertTrue(len(y[1].shape) == 1)
490+
self.assertTrue(len(t) == len(y[0]))
491+
self.assertTrue(len(t) == len(y[1]))
492+
493+
# MIMO discrete time
494+
tin = np.linspace(0, 10, 100)
495+
uin = [np.sin(tin), np.cos(tin)]
496+
t, y, x = forced_response(self.mimo_dss1, tin, uin)
497+
self.assertTrue(isinstance(t, np.ndarray)
498+
and not isinstance(t, np.matrix))
499+
self.assertTrue(len(t.shape) == 1)
500+
self.assertTrue(len(y[0].shape) == 1)
501+
self.assertTrue(len(y[1].shape) == 1)
502+
self.assertTrue(len(t) == len(y[0]))
503+
self.assertTrue(len(t) == len(y[1]))
504+
505+
# Allow input time as 2D array (output should be 1D)
506+
tin = np.array(np.linspace(0, 10, 100), ndmin=2)
507+
t, y = step_response(self.siso_ss1, tin)
508+
self.assertTrue(isinstance(t, np.ndarray)
509+
and not isinstance(t, np.matrix))
510+
self.assertTrue(len(t.shape) == 1)
511+
self.assertTrue(len(y.shape) == 1) # SISO returns "scalar" output
512+
self.assertTrue(len(t) == len(y)) # Allows direct plotting of output
513+
514+
448515
def suite():
449516
return unittest.TestLoader().loadTestsFromTestCase(TestTimeresp)
450517

0 commit comments

Comments
 (0)