LSR/env/lib/python3.6/site-packages/control/tests/iosys_test.py
2020-06-04 17:24:47 +02:00

981 lines
42 KiB
Python

#!/usr/bin/env python
#
# iosys_test.py - test input/output system oeprations
# RMM, 17 Apr 2019
#
# This test suite checks to make sure that basic input/output class
# operations are working. It doesn't do exhaustive testing of
# operations on input/output systems. Separate unit tests should be
# created for that purpose.
from __future__ import print_function
import unittest
import warnings
import numpy as np
import scipy as sp
import control as ct
import control.iosys as ios
from distutils.version import StrictVersion
class TestIOSys(unittest.TestCase):
def setUp(self):
# Turn off numpy matrix warnings
import warnings
warnings.simplefilter('ignore', category=PendingDeprecationWarning)
# Create a single input/single output linear system
self.siso_linsys = ct.StateSpace(
[[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[0]])
# Create a multi input/multi output linear system
self.mimo_linsys1 = ct.StateSpace(
[[-1, 1], [0, -2]], [[1, 0], [0, 1]],
[[1, 0], [0, 1]], np.zeros((2,2)))
# Create a multi input/multi output linear system
self.mimo_linsys2 = ct.StateSpace(
[[-1, 1], [0, -2]], [[0, 1], [1, 0]],
[[1, 0], [0, 1]], np.zeros((2,2)))
# Create simulation parameters
self.T = np.linspace(0, 10, 100)
self.U = np.sin(self.T)
self.X0 = [0, 0]
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_linear_iosys(self):
# Create an input/output system from the linear system
linsys = self.siso_linsys
iosys = ios.LinearIOSystem(linsys)
# Make sure that the right hand side matches linear system
for x, u in (([0, 0], 0), ([1, 0], 0), ([0, 1], 0), ([0, 0], 1)):
np.testing.assert_array_almost_equal(
np.reshape(iosys._rhs(0, x, u), (-1,1)),
linsys.A * np.reshape(x, (-1, 1)) + linsys.B * u)
# Make sure that simulations also line up
T, U, X0 = self.T, self.U, self.X0
lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0)
ios_t, ios_y = ios.input_output_response(iosys, T, U, X0)
np.testing.assert_array_almost_equal(lti_t, ios_t)
np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3)
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_tf2io(self):
# Create a transfer function from the state space system
linsys = self.siso_linsys
tfsys = ct.ss2tf(linsys)
iosys = ct.tf2io(tfsys)
# Verify correctness via simulation
T, U, X0 = self.T, self.U, self.X0
lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0)
ios_t, ios_y = ios.input_output_response(iosys, T, U, X0)
np.testing.assert_array_almost_equal(lti_t, ios_t)
np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3)
def test_ss2io(self):
# Create an input/output system from the linear system
linsys = self.siso_linsys
iosys = ct.ss2io(linsys)
np.testing.assert_array_equal(linsys.A, iosys.A)
np.testing.assert_array_equal(linsys.B, iosys.B)
np.testing.assert_array_equal(linsys.C, iosys.C)
np.testing.assert_array_equal(linsys.D, iosys.D)
# Try adding names to things
iosys_named = ct.ss2io(linsys, inputs='u', outputs='y',
states=['x1', 'x2'], name='iosys_named')
self.assertEqual(iosys_named.find_input('u'), 0)
self.assertEqual(iosys_named.find_input('x'), None)
self.assertEqual(iosys_named.find_output('y'), 0)
self.assertEqual(iosys_named.find_output('u'), None)
self.assertEqual(iosys_named.find_state('x0'), None)
self.assertEqual(iosys_named.find_state('x1'), 0)
self.assertEqual(iosys_named.find_state('x2'), 1)
np.testing.assert_array_equal(linsys.A, iosys_named.A)
np.testing.assert_array_equal(linsys.B, iosys_named.B)
np.testing.assert_array_equal(linsys.C, iosys_named.C)
np.testing.assert_array_equal(linsys.D, iosys_named.D)
# Make sure unspecified inputs/outputs/states are handled properly
def test_iosys_unspecified(self):
# System with unspecified inputs and outputs
sys = ios.NonlinearIOSystem(secord_update, secord_output)
np.testing.assert_raises(TypeError, sys.__mul__, sys)
# Make sure we can print various types of I/O systems
def test_iosys_print(self):
# Send the output to /dev/null
import os
f = open(os.devnull,"w")
# Simple I/O system
iosys = ct.ss2io(self.siso_linsys)
print(iosys, file=f)
# I/O system without ninputs, noutputs
ios_unspecified = ios.NonlinearIOSystem(secord_update, secord_output)
print(ios_unspecified, file=f)
# I/O system with derived inputs and outputs
ios_linearized = ios.linearize(ios_unspecified, [0, 0], [0])
print(ios_linearized, file=f)
f.close()
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_nonlinear_iosys(self):
# Create a simple nonlinear I/O system
nlsys = ios.NonlinearIOSystem(predprey)
T = self.T
# Start by simulating from an equilibrium point
X0 = [0, 0]
ios_t, ios_y = ios.input_output_response(nlsys, T, 0, X0)
np.testing.assert_array_almost_equal(ios_y, np.zeros(np.shape(ios_y)))
# Now simulate from a nonzero point
X0 = [0.5, 0.5]
ios_t, ios_y = ios.input_output_response(nlsys, T, 0, X0)
#
# Simulate a linear function as a nonlinear function and compare
#
# Create a single input/single output linear system
linsys = self.siso_linsys
# Create a nonlinear system with the same dynamics
nlupd = lambda t, x, u, params: \
np.reshape(linsys.A * np.reshape(x, (-1, 1)) + linsys.B * u, (-1,))
nlout = lambda t, x, u, params: \
np.reshape(linsys.C * np.reshape(x, (-1, 1)) + linsys.D * u, (-1,))
nlsys = ios.NonlinearIOSystem(nlupd, nlout)
# Make sure that simulations also line up
T, U, X0 = self.T, self.U, self.X0
lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0)
ios_t, ios_y = ios.input_output_response(nlsys, T, U, X0)
np.testing.assert_array_almost_equal(lti_t, ios_t)
np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3)
def test_linearize(self):
# Create a single input/single output linear system
linsys = self.siso_linsys
iosys = ios.LinearIOSystem(linsys)
# Linearize it and make sure we get back what we started with
linearized = iosys.linearize([0, 0], 0)
np.testing.assert_array_almost_equal(linsys.A, linearized.A)
np.testing.assert_array_almost_equal(linsys.B, linearized.B)
np.testing.assert_array_almost_equal(linsys.C, linearized.C)
np.testing.assert_array_almost_equal(linsys.D, linearized.D)
# Create a simple nonlinear system to check (kinematic car)
def kincar_update(t, x, u, params):
return np.array([np.cos(x[2]) * u[0], np.sin(x[2]) * u[0], u[1]])
def kincar_output(t, x, u, params):
return np.array([x[0], x[1]])
iosys = ios.NonlinearIOSystem(kincar_update, kincar_output)
linearized = iosys.linearize([0, 0, 0], [0, 0])
np.testing.assert_array_almost_equal(linearized.A, np.zeros((3,3)))
np.testing.assert_array_almost_equal(
linearized.B, [[1, 0], [0, 0], [0, 1]])
np.testing.assert_array_almost_equal(
linearized.C, [[1, 0, 0], [0, 1, 0]])
np.testing.assert_array_almost_equal(linearized.D, np.zeros((2,2)))
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_connect(self):
# Define a couple of (linear) systems to interconnection
linsys1 = self.siso_linsys
iosys1 = ios.LinearIOSystem(linsys1)
linsys2 = self.siso_linsys
iosys2 = ios.LinearIOSystem(linsys2)
# Connect systems in different ways and compare to StateSpace
linsys_series = linsys2 * linsys1
iosys_series = ios.InterconnectedSystem(
(iosys1, iosys2), # systems
((1, 0),), # interconnection (series)
0, # input = first system
1 # output = second system
)
# Run a simulation and compare to linear response
T, U = self.T, self.U
X0 = np.concatenate((self.X0, self.X0))
ios_t, ios_y, ios_x = ios.input_output_response(
iosys_series, T, U, X0, return_x=True)
lti_t, lti_y, lti_x = ct.forced_response(linsys_series, T, U, X0)
np.testing.assert_array_almost_equal(lti_t, ios_t)
np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3)
# Connect systems with different timebases
linsys2c = self.siso_linsys
linsys2c.dt = 0 # Reset the timebase
iosys2c = ios.LinearIOSystem(linsys2c)
iosys_series = ios.InterconnectedSystem(
(iosys1, iosys2c), # systems
((1, 0),), # interconnection (series)
0, # input = first system
1 # output = second system
)
self.assertTrue(ct.isctime(iosys_series, strict=True))
ios_t, ios_y, ios_x = ios.input_output_response(
iosys_series, T, U, X0, return_x=True)
lti_t, lti_y, lti_x = ct.forced_response(linsys_series, T, U, X0)
np.testing.assert_array_almost_equal(lti_t, ios_t)
np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3)
# Feedback interconnection
linsys_feedback = ct.feedback(linsys1, linsys2)
iosys_feedback = ios.InterconnectedSystem(
(iosys1, iosys2), # systems
((1, 0), # input of sys2 = output of sys1
(0, (1, 0, -1))), # input of sys1 = -output of sys2
0, # input = first system
0 # output = first system
)
ios_t, ios_y, ios_x = ios.input_output_response(
iosys_feedback, T, U, X0, return_x=True)
lti_t, lti_y, lti_x = ct.forced_response(linsys_feedback, T, U, X0)
np.testing.assert_array_almost_equal(lti_t, ios_t)
np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3)
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_static_nonlinearity(self):
# Linear dynamical system
linsys = self.siso_linsys
ioslin = ios.LinearIOSystem(linsys)
# Nonlinear saturation
sat = lambda u: u if abs(u) < 1 else np.sign(u)
sat_output = lambda t, x, u, params: sat(u)
nlsat = ios.NonlinearIOSystem(None, sat_output, inputs=1, outputs=1)
# Set up parameters for simulation
T, U, X0 = self.T, 2 * self.U, self.X0
Usat = np.vectorize(sat)(U)
# Make sure saturation works properly by comparing linear system with
# saturated input to nonlinear system with saturation composition
lti_t, lti_y, lti_x = ct.forced_response(linsys, T, Usat, X0)
ios_t, ios_y, ios_x = ios.input_output_response(
ioslin * nlsat, T, U, X0, return_x=True)
np.testing.assert_array_almost_equal(lti_t, ios_t)
np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=2)
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_algebraic_loop(self):
# Create some linear and nonlinear systems to play with
linsys = self.siso_linsys
lnios = ios.LinearIOSystem(linsys)
nlios = ios.NonlinearIOSystem(None, \
lambda t, x, u, params: u*u, inputs=1, outputs=1)
nlios1 = nlios.copy()
nlios2 = nlios.copy()
# Set up parameters for simulation
T, U, X0 = self.T, self.U, self.X0
# Single nonlinear system - no states
ios_t, ios_y = ios.input_output_response(nlios, T, U, X0)
np.testing.assert_array_almost_equal(ios_y, U*U, decimal=3)
# Composed nonlinear system (series)
ios_t, ios_y = ios.input_output_response(nlios1 * nlios2, T, U, X0)
np.testing.assert_array_almost_equal(ios_y, U**4, decimal=3)
# Composed nonlinear system (parallel)
ios_t, ios_y = ios.input_output_response(nlios1 + nlios2, T, U, X0)
np.testing.assert_array_almost_equal(ios_y, 2*U**2, decimal=3)
# Nonlinear system composed with LTI system (series)
ios_t, ios_y = ios.input_output_response(
nlios * lnios * nlios, T, U, X0)
lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U*U, X0)
np.testing.assert_array_almost_equal(ios_y, lti_y*lti_y, decimal=3)
# Nonlinear system in feeback loop with LTI system
iosys = ios.InterconnectedSystem(
(lnios, nlios), # linear system w/ nonlinear feedback
((1,), # feedback interconnection (sig to 0)
(0, (1, 0, -1))),
0, # input to linear system
0 # output from linear system
)
ios_t, ios_y = ios.input_output_response(iosys, T, U, X0)
# No easy way to test the result
# Algebraic loop from static nonlinear system in feedback
# (error will be due to no states)
iosys = ios.InterconnectedSystem(
(nlios1, nlios2), # two copies of a static nonlinear system
((0, 1), # feedback interconnection
(1, (0, 0, -1))),
0, 0
)
args = (iosys, T, U, X0)
self.assertRaises(RuntimeError, ios.input_output_response, *args)
# Algebraic loop due to feedthrough term
linsys = ct.StateSpace(
[[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[1]])
lnios = ios.LinearIOSystem(linsys)
iosys = ios.InterconnectedSystem(
(nlios, lnios), # linear system w/ nonlinear feedback
((0, 1), # feedback interconnection
(1, (0, 0, -1))),
0, 0
)
args = (iosys, T, U, X0)
# ios_t, ios_y = ios.input_output_response(iosys, T, U, X0)
self.assertRaises(RuntimeError, ios.input_output_response, *args)
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_summer(self):
# Construct a MIMO system for testing
linsys = self.mimo_linsys1
linio = ios.LinearIOSystem(linsys)
linsys_parallel = linsys + linsys
iosys_parallel = linio + linio
# Set up parameters for simulation
T = self.T
U = [np.sin(T), np.cos(T)]
X0 = 0
lin_t, lin_y, lin_x = ct.forced_response(linsys_parallel, T, U, X0)
ios_t, ios_y = ios.input_output_response(iosys_parallel, T, U, X0)
np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3)
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_rmul(self):
# Test right multiplication
# TODO: replace with better tests when conversions are implemented
# Set up parameters for simulation
T, U, X0 = self.T, self.U, self.X0
# Linear system with input and output nonlinearities
# Also creates a nested interconnected system
ioslin = ios.LinearIOSystem(self.siso_linsys)
nlios = ios.NonlinearIOSystem(None, \
lambda t, x, u, params: u*u, inputs=1, outputs=1)
sys1 = nlios * ioslin
sys2 = ios.InputOutputSystem.__rmul__(nlios, sys1)
# Make sure we got the right thing (via simulation comparison)
ios_t, ios_y = ios.input_output_response(sys2, T, U, X0)
lti_t, lti_y, lti_x = ct.forced_response(ioslin, T, U*U, X0)
np.testing.assert_array_almost_equal(ios_y, lti_y*lti_y, decimal=3)
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_neg(self):
"""Test negation of a system"""
# Set up parameters for simulation
T, U, X0 = self.T, self.U, self.X0
# Static nonlinear system
nlios = ios.NonlinearIOSystem(None, \
lambda t, x, u, params: u*u, inputs=1, outputs=1)
ios_t, ios_y = ios.input_output_response(-nlios, T, U, X0)
np.testing.assert_array_almost_equal(ios_y, -U*U, decimal=3)
# Linear system with input nonlinearity
# Also creates a nested interconnected system
ioslin = ios.LinearIOSystem(self.siso_linsys)
sys = (ioslin) * (-nlios)
# Make sure we got the right thing (via simulation comparison)
ios_t, ios_y = ios.input_output_response(sys, T, U, X0)
lti_t, lti_y, lti_x = ct.forced_response(ioslin, T, U*U, X0)
np.testing.assert_array_almost_equal(ios_y, -lti_y, decimal=3)
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_feedback(self):
# Set up parameters for simulation
T, U, X0 = self.T, self.U, self.X0
# Linear system with constant feedback (via "nonlinear" mapping)
ioslin = ios.LinearIOSystem(self.siso_linsys)
nlios = ios.NonlinearIOSystem(None, \
lambda t, x, u, params: u, inputs=1, outputs=1)
iosys = ct.feedback(ioslin, nlios)
linsys = ct.feedback(self.siso_linsys, 1)
ios_t, ios_y = ios.input_output_response(iosys, T, U, X0)
lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0)
np.testing.assert_array_almost_equal(ios_y, lti_y, decimal=3)
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_bdalg_functions(self):
"""Test block diagram functions algebra on I/O systems"""
# Set up parameters for simulation
T = self.T
U = [np.sin(T), np.cos(T)]
X0 = 0
# Set up systems to be composed
linsys1 = self.mimo_linsys1
linio1 = ios.LinearIOSystem(linsys1)
linsys2 = self.mimo_linsys2
linio2 = ios.LinearIOSystem(linsys2)
# Series interconnection
linsys_series = ct.series(linsys1, linsys2)
iosys_series = ct.series(linio1, linio2)
lin_t, lin_y, lin_x = ct.forced_response(linsys_series, T, U, X0)
ios_t, ios_y = ios.input_output_response(iosys_series, T, U, X0)
np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3)
# Make sure that systems don't commute
linsys_series = ct.series(linsys2, linsys1)
lin_t, lin_y, lin_x = ct.forced_response(linsys_series, T, U, X0)
self.assertFalse((np.abs(lin_y - ios_y) < 1e-3).all())
# Parallel interconnection
linsys_parallel = ct.parallel(linsys1, linsys2)
iosys_parallel = ct.parallel(linio1, linio2)
lin_t, lin_y, lin_x = ct.forced_response(linsys_parallel, T, U, X0)
ios_t, ios_y = ios.input_output_response(iosys_parallel, T, U, X0)
np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3)
# Negation
linsys_negate = ct.negate(linsys1)
iosys_negate = ct.negate(linio1)
lin_t, lin_y, lin_x = ct.forced_response(linsys_negate, T, U, X0)
ios_t, ios_y = ios.input_output_response(iosys_negate, T, U, X0)
np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3)
# Feedback interconnection
linsys_feedback = ct.feedback(linsys1, linsys2)
iosys_feedback = ct.feedback(linio1, linio2)
lin_t, lin_y, lin_x = ct.forced_response(linsys_feedback, T, U, X0)
ios_t, ios_y = ios.input_output_response(iosys_feedback, T, U, X0)
np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3)
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_nonsquare_bdalg(self):
# Set up parameters for simulation
T = self.T
U2 = [np.sin(T), np.cos(T)]
U3 = [np.sin(T), np.cos(T), T]
X0 = 0
# Set up systems to be composed
linsys_2i3o = ct.StateSpace(
[[-1, 1, 0], [0, -2, 0], [0, 0, -3]], [[1, 0], [0, 1], [1, 1]],
[[1, 0, 0], [0, 1, 0], [0, 0, 1]], np.zeros((3, 2)))
iosys_2i3o = ios.LinearIOSystem(linsys_2i3o)
linsys_3i2o = ct.StateSpace(
[[-1, 1, 0], [0, -2, 0], [0, 0, -3]],
[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
[[1, 0, 1], [0, 1, -1]], np.zeros((2, 3)))
iosys_3i2o = ios.LinearIOSystem(linsys_3i2o)
# Multiplication
linsys_multiply = linsys_3i2o * linsys_2i3o
iosys_multiply = iosys_3i2o * iosys_2i3o
lin_t, lin_y, lin_x = ct.forced_response(linsys_multiply, T, U2, X0)
ios_t, ios_y = ios.input_output_response(iosys_multiply, T, U2, X0)
np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3)
linsys_multiply = linsys_2i3o * linsys_3i2o
iosys_multiply = iosys_2i3o * iosys_3i2o
lin_t, lin_y, lin_x = ct.forced_response(linsys_multiply, T, U3, X0)
ios_t, ios_y = ios.input_output_response(iosys_multiply, T, U3, X0)
np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3)
# Right multiplication
# TODO: add real tests once conversion from other types is supported
iosys_multiply = ios.InputOutputSystem.__rmul__(iosys_3i2o, iosys_2i3o)
ios_t, ios_y = ios.input_output_response(iosys_multiply, T, U3, X0)
np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3)
# Feedback
linsys_multiply = ct.feedback(linsys_3i2o, linsys_2i3o)
iosys_multiply = iosys_3i2o.feedback(iosys_2i3o)
lin_t, lin_y, lin_x = ct.forced_response(linsys_multiply, T, U3, X0)
ios_t, ios_y = ios.input_output_response(iosys_multiply, T, U3, X0)
np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3)
# Mismatch should generate exception
args = (iosys_3i2o, iosys_3i2o)
self.assertRaises(ValueError, ct.series, *args)
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_discrete(self):
"""Test discrete time functionality"""
# Create some linear and nonlinear systems to play with
linsys = ct.StateSpace(
[[-1, 1], [0, -2]], [[0], [1]], [[1, 0]], [[0]], True)
lnios = ios.LinearIOSystem(linsys)
# Set up parameters for simulation
T, U, X0 = self.T, self.U, self.X0
# Simulate and compare to LTI output
ios_t, ios_y = ios.input_output_response(lnios, T, U, X0)
lin_t, lin_y, lin_x = ct.forced_response(linsys, T, U, X0)
np.testing.assert_array_almost_equal(ios_t, lin_t, decimal=3)
np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3)
# Test MIMO system, converted to discrete time
linsys = ct.StateSpace(self.mimo_linsys1)
linsys.dt = self.T[1] - self.T[0]
lnios = ios.LinearIOSystem(linsys)
# Set up parameters for simulation
T = self.T
U = [np.sin(T), np.cos(T)]
X0 = 0
# Simulate and compare to LTI output
ios_t, ios_y = ios.input_output_response(lnios, T, U, X0)
lin_t, lin_y, lin_x = ct.forced_response(linsys, T, U, X0)
np.testing.assert_array_almost_equal(ios_t, lin_t, decimal=3)
np.testing.assert_array_almost_equal(ios_y, lin_y, decimal=3)
def test_find_eqpts(self):
"""Test find_eqpt function"""
# Simple equilibrium point with no inputs
nlsys = ios.NonlinearIOSystem(predprey)
xeq, ueq, result = ios.find_eqpt(
nlsys, [1.6, 1.2], None, return_result=True)
self.assertTrue(result.success)
np.testing.assert_array_almost_equal(xeq, [1.64705879, 1.17923874])
np.testing.assert_array_almost_equal(
nlsys._rhs(0, xeq, ueq), np.zeros((2,)))
# Ducted fan dynamics with output = velocity
nlsys = ios.NonlinearIOSystem(pvtol, lambda t, x, u, params: x[0:2])
# Make sure the origin is a fixed point
xeq, ueq, result = ios.find_eqpt(
nlsys, [0, 0, 0, 0], [0, 4*9.8], return_result=True)
self.assertTrue(result.success)
np.testing.assert_array_almost_equal(
nlsys._rhs(0, xeq, ueq), np.zeros((4,)))
np.testing.assert_array_almost_equal(xeq, [0, 0, 0, 0])
# Use a small lateral force to cause motion
xeq, ueq, result = ios.find_eqpt(
nlsys, [0, 0, 0, 0], [0.01, 4*9.8], return_result=True)
self.assertTrue(result.success)
np.testing.assert_array_almost_equal(
nlsys._rhs(0, xeq, ueq), np.zeros((4,)), decimal=5)
# Equilibrium point with fixed output
xeq, ueq, result = ios.find_eqpt(
nlsys, [0, 0, 0, 0], [0.01, 4*9.8],
y0=[0.1, 0.1], return_result=True)
self.assertTrue(result.success)
np.testing.assert_array_almost_equal(
nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5)
np.testing.assert_array_almost_equal(
nlsys._rhs(0, xeq, ueq), np.zeros((4,)), decimal=5)
# Specify outputs to constrain (replicate previous)
xeq, ueq, result = ios.find_eqpt(
nlsys, [0, 0, 0, 0], [0.01, 4*9.8], y0=[0.1, 0.1],
iy = [0, 1], return_result=True)
self.assertTrue(result.success)
np.testing.assert_array_almost_equal(
nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5)
np.testing.assert_array_almost_equal(
nlsys._rhs(0, xeq, ueq), np.zeros((4,)), decimal=5)
# Specify inputs to constrain (replicate previous), w/ no result
xeq, ueq = ios.find_eqpt(
nlsys, [0, 0, 0, 0], [0.01, 4*9.8], y0=[0.1, 0.1], iu = [])
np.testing.assert_array_almost_equal(
nlsys._out(0, xeq, ueq), [0.1, 0.1], decimal=5)
np.testing.assert_array_almost_equal(
nlsys._rhs(0, xeq, ueq), np.zeros((4,)), decimal=5)
# Now solve the problem with the original PVTOL variables
# Constrain the output angle and x velocity
nlsys_full = ios.NonlinearIOSystem(pvtol_full, None)
xeq, ueq, result = ios.find_eqpt(
nlsys_full, [0, 0, 0, 0, 0, 0], [0.01, 4*9.8],
y0=[0, 0, 0.1, 0.1, 0, 0], iy = [2, 3],
idx=[2, 3, 4, 5], ix=[0, 1], return_result=True)
self.assertTrue(result.success)
np.testing.assert_array_almost_equal(
nlsys_full._out(0, xeq, ueq)[[2, 3]], [0.1, 0.1], decimal=5)
np.testing.assert_array_almost_equal(
nlsys_full._rhs(0, xeq, ueq)[-4:], np.zeros((4,)), decimal=5)
# Fix one input and vary the other
nlsys_full = ios.NonlinearIOSystem(pvtol_full, None)
xeq, ueq, result = ios.find_eqpt(
nlsys_full, [0, 0, 0, 0, 0, 0], [0.01, 4*9.8],
y0=[0, 0, 0.1, 0.1, 0, 0], iy=[3], iu=[1],
idx=[2, 3, 4, 5], ix=[0, 1], return_result=True)
self.assertTrue(result.success)
np.testing.assert_almost_equal(ueq[1], 4*9.8, decimal=5)
np.testing.assert_array_almost_equal(
nlsys_full._out(0, xeq, ueq)[[3]], [0.1], decimal=5)
np.testing.assert_array_almost_equal(
nlsys_full._rhs(0, xeq, ueq)[-4:], np.zeros((4,)), decimal=5)
# PVTOL with output = y velocity
xeq, ueq, result = ios.find_eqpt(
nlsys_full, [0, 0, 0, 0.1, 0, 0], [0.01, 4*9.8],
y0=[0, 0, 0, 0.1, 0, 0], iy=[3],
dx0=[0.1, 0, 0, 0, 0, 0], idx=[1, 2, 3, 4, 5],
ix=[0, 1], return_result=True)
self.assertTrue(result.success)
np.testing.assert_array_almost_equal(
nlsys_full._out(0, xeq, ueq)[-3:], [0.1, 0, 0], decimal=5)
np.testing.assert_array_almost_equal(
nlsys_full._rhs(0, xeq, ueq)[-5:], np.zeros((5,)), decimal=5)
# Unobservable system
linsys = ct.StateSpace(
[[-1, 1], [0, -2]], [[0], [1]], [[0, 0]], [[0]])
lnios = ios.LinearIOSystem(linsys)
# If result is returned, user has to check
xeq, ueq, result = ios.find_eqpt(
lnios, [0, 0], [0], y0=[1], return_result=True)
self.assertFalse(result.success)
# If result is not returned, find_eqpt should return None
xeq, ueq = ios.find_eqpt(lnios, [0, 0], [0], y0=[1])
self.assertEqual(xeq, None)
self.assertEqual(ueq, None)
@unittest.skipIf(StrictVersion(sp.__version__) < "1.0",
"requires SciPy 1.0 or greater")
def test_params(self):
# Start with the default set of parameters
ios_secord_default = ios.NonlinearIOSystem(
secord_update, secord_output, inputs=1, outputs=1, states=2)
lin_secord_default = ios.linearize(ios_secord_default, [0, 0], [0])
w_default, v_default = np.linalg.eig(lin_secord_default.A)
# New copy, with modified parameters
ios_secord_update = ios.NonlinearIOSystem(
secord_update, secord_output, inputs=1, outputs=1, states=2,
params={'omega0':2, 'zeta':0})
# Make sure the default parameters haven't changed
lin_secord_check = ios.linearize(ios_secord_default, [0, 0], [0])
w, v = np.linalg.eig(lin_secord_check.A)
np.testing.assert_array_almost_equal(np.sort(w), np.sort(w_default))
# Make sure updated system parameters got set correctly
lin_secord_update = ios.linearize(ios_secord_update, [0, 0], [0])
w, v = np.linalg.eig(lin_secord_update.A)
np.testing.assert_array_almost_equal(np.sort(w), np.sort([2j, -2j]))
# Change the parameters of the default sys just for the linearization
lin_secord_local = ios.linearize(ios_secord_default, [0, 0], [0],
params={'zeta':0})
w, v = np.linalg.eig(lin_secord_local.A)
np.testing.assert_array_almost_equal(np.sort(w), np.sort([1j, -1j]))
# Change the parameters of the updated sys just for the linearization
lin_secord_local = ios.linearize(ios_secord_update, [0, 0], [0],
params={'zeta':0, 'omega0':3})
w, v = np.linalg.eig(lin_secord_local.A)
np.testing.assert_array_almost_equal(np.sort(w), np.sort([3j, -3j]))
# Make sure that changes propagate through interconnections
ios_series_default_local = ios_secord_default * ios_secord_update
lin_series_default_local = ios.linearize(
ios_series_default_local, [0, 0, 0, 0], [0])
w, v = np.linalg.eig(lin_series_default_local.A)
np.testing.assert_array_almost_equal(
np.sort(w), np.sort(np.concatenate((w_default, [2j, -2j]))))
# Show that we can change the parameters at linearization
lin_series_override = ios.linearize(
ios_series_default_local, [0, 0, 0, 0], [0],
params={'zeta':0, 'omega0':4})
w, v = np.linalg.eig(lin_series_override.A)
np.testing.assert_array_almost_equal(w, [4j, -4j, 4j, -4j])
# Check for warning if we try to set params for LinearIOSystem
linsys = self.siso_linsys
iosys = ios.LinearIOSystem(linsys)
T, U, X0 = self.T, self.U, self.X0
lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U, X0)
with warnings.catch_warnings(record=True) as warnval:
# Turn off deprecation warnings
warnings.simplefilter("ignore", category=DeprecationWarning)
warnings.simplefilter("ignore", category=PendingDeprecationWarning)
# Trigger a warning
ios_t, ios_y = ios.input_output_response(
iosys, T, U, X0, params={'something':0})
# Verify that we got a warning
self.assertEqual(len(warnval), 1)
self.assertTrue(issubclass(warnval[-1].category, UserWarning))
self.assertTrue("LinearIOSystem" in str(warnval[-1].message))
self.assertTrue("ignored" in str(warnval[-1].message))
# Check to make sure results are OK
np.testing.assert_array_almost_equal(lti_t, ios_t)
np.testing.assert_array_almost_equal(lti_y, ios_y, decimal=3)
def test_named_signals(self):
sys1 = ios.NonlinearIOSystem(
updfcn = lambda t, x, u, params: np.array(
np.dot(self.mimo_linsys1.A, np.reshape(x, (-1, 1))) \
+ np.dot(self.mimo_linsys1.B, np.reshape(u, (-1, 1)))
).reshape(-1,),
outfcn = lambda t, x, u, params: np.array(
self.mimo_linsys1.C * np.reshape(x, (-1, 1)) \
+ self.mimo_linsys1.D * np.reshape(u, (-1, 1))
).reshape(-1,),
inputs = ('u[0]', 'u[1]'),
outputs = ('y[0]', 'y[1]'),
states = self.mimo_linsys1.states,
name = 'sys1')
sys2 = ios.LinearIOSystem(self.mimo_linsys2,
inputs = ('u[0]', 'u[1]'),
outputs = ('y[0]', 'y[1]'),
name = 'sys2')
# Series interconnection (sys1 * sys2) using __mul__
ios_mul = sys1 * sys2
ss_series = self.mimo_linsys1 * self.mimo_linsys2
lin_series = ct.linearize(ios_mul, 0, 0)
for M, N in ((ss_series.A, lin_series.A), (ss_series.B, lin_series.B),
(ss_series.C, lin_series.C), (ss_series.D, lin_series.D)):
np.testing.assert_array_almost_equal(M, N)
# Series interconnection (sys1 * sys2) using series
ios_series = ct.series(sys2, sys1)
ss_series = ct.series(self.mimo_linsys2, self.mimo_linsys1)
lin_series = ct.linearize(ios_series, 0, 0)
for M, N in ((ss_series.A, lin_series.A), (ss_series.B, lin_series.B),
(ss_series.C, lin_series.C), (ss_series.D, lin_series.D)):
np.testing.assert_array_almost_equal(M, N)
# Series interconnection (sys1 * sys2) using named + mixed signals
ios_connect = ios.InterconnectedSystem(
(sys2, sys1),
connections=(
(('sys1', 'u[0]'), 'sys2.y[0]'),
('sys1.u[1]', 'sys2.y[1]')
),
inplist=('sys2.u[0]', ('sys2', 1)),
outlist=((1, 'y[0]'), 'sys1.y[1]')
)
lin_series = ct.linearize(ios_connect, 0, 0)
for M, N in ((ss_series.A, lin_series.A), (ss_series.B, lin_series.B),
(ss_series.C, lin_series.C), (ss_series.D, lin_series.D)):
np.testing.assert_array_almost_equal(M, N)
# Make sure that we can use input signal names as system outputs
ios_connect = ios.InterconnectedSystem(
(sys1, sys2),
connections=(
('sys2.u[0]', 'sys1.y[0]'), ('sys2.u[1]', 'sys1.y[1]'),
('sys1.u[0]', '-sys2.y[0]'), ('sys1.u[1]', '-sys2.y[1]')
),
inplist=('sys1.u[0]', 'sys1.u[1]'),
outlist=('sys2.u[0]', 'sys2.u[1]') # = sys1.y[0], sys1.y[1]
)
ss_feedback = ct.feedback(self.mimo_linsys1, self.mimo_linsys2)
lin_feedback = ct.linearize(ios_connect, 0, 0)
np.testing.assert_array_almost_equal(ss_feedback.A, lin_feedback.A)
np.testing.assert_array_almost_equal(ss_feedback.B, lin_feedback.B)
np.testing.assert_array_almost_equal(ss_feedback.C, lin_feedback.C)
np.testing.assert_array_almost_equal(ss_feedback.D, lin_feedback.D)
def test_lineariosys_statespace(self):
"""Make sure that a LinearIOSystem is also a StateSpace object"""
iosys_siso = ct.LinearIOSystem(self.siso_linsys)
self.assertTrue(isinstance(iosys_siso, ct.StateSpace))
# Make sure that state space functions work for LinearIOSystems
np.testing.assert_array_equal(
iosys_siso.pole(), self.siso_linsys.pole())
omega = np.logspace(.1, 10, 100)
mag_io, phase_io, omega_io = iosys_siso.freqresp(omega)
mag_ss, phase_ss, omega_ss = self.siso_linsys.freqresp(omega)
np.testing.assert_array_equal(mag_io, mag_ss)
np.testing.assert_array_equal(phase_io, phase_ss)
np.testing.assert_array_equal(omega_io, omega_ss)
# LinearIOSystem methods should override StateSpace methods
io_mul = iosys_siso * iosys_siso
self.assertTrue(isinstance(io_mul, ct.InputOutputSystem))
# But also retain linear structure
self.assertTrue(isinstance(io_mul, ct.StateSpace))
# And make sure the systems match
ss_series = self.siso_linsys * self.siso_linsys
np.testing.assert_array_equal(io_mul.A, ss_series.A)
np.testing.assert_array_equal(io_mul.B, ss_series.B)
np.testing.assert_array_equal(io_mul.C, ss_series.C)
np.testing.assert_array_equal(io_mul.D, ss_series.D)
# Make sure that series does the same thing
io_series = ct.series(iosys_siso, iosys_siso)
self.assertTrue(isinstance(io_series, ct.InputOutputSystem))
self.assertTrue(isinstance(io_series, ct.StateSpace))
np.testing.assert_array_equal(io_series.A, ss_series.A)
np.testing.assert_array_equal(io_series.B, ss_series.B)
np.testing.assert_array_equal(io_series.C, ss_series.C)
np.testing.assert_array_equal(io_series.D, ss_series.D)
# Test out feedback as well
io_feedback = ct.feedback(iosys_siso, iosys_siso)
self.assertTrue(isinstance(io_series, ct.InputOutputSystem))
# But also retain linear structure
self.assertTrue(isinstance(io_series, ct.StateSpace))
# And make sure the systems match
ss_feedback = ct.feedback(self.siso_linsys, self.siso_linsys)
np.testing.assert_array_equal(io_feedback.A, ss_feedback.A)
np.testing.assert_array_equal(io_feedback.B, ss_feedback.B)
np.testing.assert_array_equal(io_feedback.C, ss_feedback.C)
np.testing.assert_array_equal(io_feedback.D, ss_feedback.D)
def test_duplicates(self):
nlios = ios.NonlinearIOSystem(None, \
lambda t, x, u, params: u*u, inputs=1, outputs=1)
# Turn off deprecation warnings
warnings.simplefilter("ignore", category=DeprecationWarning)
warnings.simplefilter("ignore", category=PendingDeprecationWarning)
# Duplicate objects
with warnings.catch_warnings(record=True) as warnval:
# Trigger a warning
ios_series = nlios * nlios
# Verify that we got a warning
self.assertEqual(len(warnval), 1)
self.assertTrue(issubclass(warnval[-1].category, UserWarning))
self.assertTrue("Duplicate object" in str(warnval[-1].message))
# Nonduplicate objects
nlios1 = nlios.copy()
nlios2 = nlios.copy()
with warnings.catch_warnings(record=True) as warnval:
ios_series = nlios1 * nlios2
self.assertEqual(len(warnval), 0)
# Duplicate names
iosys_siso = ct.LinearIOSystem(self.siso_linsys)
nlios1 = ios.NonlinearIOSystem(None, \
lambda t, x, u, params: u*u, inputs=1, outputs=1, name="sys")
nlios2 = ios.NonlinearIOSystem(None, \
lambda t, x, u, params: u*u, inputs=1, outputs=1, name="sys")
with warnings.catch_warnings(record=True) as warnval:
# Trigger a warning
iosys = ct.InterconnectedSystem(
(nlios1, iosys_siso, nlios2), inputs=0, outputs=0, states=0)
# Verify that we got a warning
self.assertEqual(len(warnval), 1)
self.assertTrue(issubclass(warnval[-1].category, UserWarning))
self.assertTrue("Duplicate name" in str(warnval[-1].message))
# Same system, different names => everything should be OK
nlios1 = ios.NonlinearIOSystem(None, \
lambda t, x, u, params: u*u, inputs=1, outputs=1, name="nlios1")
nlios2 = ios.NonlinearIOSystem(None, \
lambda t, x, u, params: u*u, inputs=1, outputs=1, name="nlios2")
with warnings.catch_warnings(record=True) as warnval:
iosys = ct.InterconnectedSystem(
(nlios1, iosys_siso, nlios2), inputs=0, outputs=0, states=0)
self.assertEqual(len(warnval), 0)
def suite():
return unittest.TestLoader().loadTestsFromTestCase(TestTimeresp)
# Predator prey dynamics
def predprey(t, x, u, params={}):
r = params.get('r', 2)
d = params.get('d', 0.7)
b = params.get('b', 0.3)
k = params.get('k', 10)
a = params.get('a', 8)
c = params.get('c', 4)
# Dynamics for the system
dx0 = r * x[0] * (1 - x[0]/k) - a * x[1] * x[0]/(c + x[0])
dx1 = b * a * x[1] * x[0] / (c + x[0]) - d * x[1]
return np.array([dx0, dx1])
# Reduced planar vertical takeoff and landing dynamics
def pvtol(t, x, u, params={}):
from math import sin, cos
m = params.get('m', 4.) # kg, system mass
J = params.get('J', 0.0475) # kg m^2, system inertia
r = params.get('r', 0.25) # m, thrust offset
g = params.get('g', 9.8) # m/s, gravitational constant
c = params.get('c', 0.05) # N s/m, rotational damping
l = params.get('c', 0.1) # m, pivot location
return np.array([
x[3],
-c/m * x[1] + 1/m * cos(x[0]) * u[0] - 1/m * sin(x[0]) * u[1],
-g - c/m * x[2] + 1/m * sin(x[0]) * u[0] + 1/m * cos(x[0]) * u[1],
-l/J * sin(x[0]) + r/J * u[0]
])
def pvtol_full(t, x, u, params={}):
from math import sin, cos
m = params.get('m', 4.) # kg, system mass
J = params.get('J', 0.0475) # kg m^2, system inertia
r = params.get('r', 0.25) # m, thrust offset
g = params.get('g', 9.8) # m/s, gravitational constant
c = params.get('c', 0.05) # N s/m, rotational damping
l = params.get('c', 0.1) # m, pivot location
return np.array([
x[3], x[4], x[5],
-c/m * x[3] + 1/m * cos(x[2]) * u[0] - 1/m * sin(x[2]) * u[1],
-g - c/m * x[4] + 1/m * sin(x[2]) * u[0] + 1/m * cos(x[2]) * u[1],
-l/J * sin(x[2]) + r/J * u[0]
])
# Second order system dynamics
def secord_update(t, x, u, params={}):
omega0 = params.get('omega0', 1.)
zeta = params.get('zeta', 0.5)
u = np.array(u, ndmin=1)
return np.array([
x[1],
-2 * zeta * omega0 * x[1] - omega0*omega0 * x[0] + u[0]
])
def secord_output(t, x, u, params={}):
return np.array([x[0]])
if __name__ == '__main__':
unittest.main()