Added pyrate as a direct dependency.
This commit is contained in:
0
pyrate/tests/act/__init__.py
Normal file
0
pyrate/tests/act/__init__.py
Normal file
66
pyrate/tests/act/test_lqr.py
Normal file
66
pyrate/tests/act/test_lqr.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""This module asserts correct runtime behaviour of the pyrate.act.pid module."""
|
||||
|
||||
# Test environment
|
||||
from unittest import TestCase
|
||||
|
||||
# Mathematics
|
||||
from numpy import array
|
||||
from numpy import eye
|
||||
from numpy import Inf
|
||||
from numpy.linalg import norm
|
||||
from numpy import vstack
|
||||
|
||||
# Package under test
|
||||
from pyrate.act.control import Lqr
|
||||
|
||||
|
||||
class TestLqr(TestCase):
|
||||
|
||||
"""Test for correct runtime behaviour in pyrate.act.control.lqr."""
|
||||
|
||||
# In this context, we reproduce a common controller notation
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Setup the LQR specification for testing."""
|
||||
|
||||
# Model specification
|
||||
self.A = array([[0, 1], [0, 0]])
|
||||
self.B = array([0, 1])[:, None]
|
||||
self.C = array([1, 0])[None, :]
|
||||
self.dt = 0.5
|
||||
|
||||
# Cost matrix specification
|
||||
self.Q = eye(2)
|
||||
self.R = array([[1.0]])
|
||||
|
||||
# State and target
|
||||
# Target is already reached in this example
|
||||
self.state = vstack([0.0, 0.0])
|
||||
self.desired = vstack([0.0])
|
||||
|
||||
def test_process_tracking(self) -> None:
|
||||
"""Assert that a pandas.DataFrame with process data is created."""
|
||||
|
||||
# Initialize PID controller
|
||||
lqr = Lqr(self.A, self.B, self.C, self.Q, self.R, self.dt, keep_trace=True)
|
||||
|
||||
# Execute a few control steps
|
||||
state = self.state.copy()
|
||||
previous_error = Inf
|
||||
for _ in range(10):
|
||||
control_signal = lqr.control(desired=self.desired, state=self.state)
|
||||
state += self.A.dot(state) + self.B.dot(control_signal)
|
||||
error = norm(state - self.desired).item() # Convert from numpy scalar to Python float
|
||||
assert error < previous_error or error == 0, "Error did not get lower."
|
||||
previous_error = error
|
||||
|
||||
# Assert correct process tracing with LQR controller
|
||||
assert lqr.process is not None, "LQR did not keep trace of process"
|
||||
assert len(lqr.process.index) == 10, "LQR has not traced enough steps"
|
||||
|
||||
# Reset PID
|
||||
lqr.reset()
|
||||
|
||||
# Assert correct reset to initial state
|
||||
assert len(lqr.process.index) == 0, "LQR has not dropped process trace properly"
|
117
pyrate/tests/act/test_lqr_integral.py
Normal file
117
pyrate/tests/act/test_lqr_integral.py
Normal file
@ -0,0 +1,117 @@
|
||||
"""This module asserts correct runtime behaviour of the pyrate.act.pid module."""
|
||||
|
||||
# Test environment
|
||||
from unittest import TestCase
|
||||
|
||||
# Mathematics
|
||||
from numpy import array
|
||||
from numpy import eye
|
||||
from numpy.linalg import eig
|
||||
from numpy import vstack
|
||||
|
||||
# Package under test
|
||||
from pyrate.act.control import AntiWindupLqr
|
||||
|
||||
|
||||
class TestAntiWindupLqr(TestCase):
|
||||
|
||||
"""Test for correct runtime behaviour in pyrate.act.control.anti_windup_lqr."""
|
||||
|
||||
# In this context, we reproduce a common controller notation
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Setup the LQR specification for testing."""
|
||||
|
||||
# Model specification
|
||||
self.A = array([[0, 1], [0, 0]])
|
||||
self.B = array([0, 1])[:, None]
|
||||
self.C = array([1, 0])[None, :]
|
||||
self.max_control = array([1.0])
|
||||
self.dt = 0.5
|
||||
|
||||
# Time discrete model
|
||||
self.Ad = self.dt * eye(2)
|
||||
self.Bd = self.B + self.A @ self.B * self.dt
|
||||
|
||||
# Cost matrix specification
|
||||
self.Q = eye(3)
|
||||
self.R = array([[1.0]])
|
||||
|
||||
# State and target
|
||||
# Target is already reached in this example
|
||||
self.wrong_state = vstack([1.0, 0.0])
|
||||
self.state_small_negative = vstack([-0.001, 0.0])
|
||||
self.state = vstack([0.0, 0.0])
|
||||
self.desired = vstack([0.0])
|
||||
self.desired1 = vstack([1.0])
|
||||
|
||||
def test_lqr_design(self) -> None:
|
||||
"""Assert stable controller dynamics"""
|
||||
|
||||
# Initialize controller
|
||||
lqr = AntiWindupLqr(
|
||||
self.A, self.B, self.C, self.Q, self.R, self.max_control, self.dt, keep_trace=True
|
||||
)
|
||||
# check continous time eigen_values
|
||||
eigen_values, _ = eig(lqr.A - lqr.B @ lqr.K)
|
||||
assert all(ev.real < 0 for ev in eigen_values), "instable controller"
|
||||
|
||||
def test_anti_windup(self) -> None:
|
||||
"""Assert control signal in allowed range, LQR responsive by limited integral part."""
|
||||
|
||||
# Initialize controller
|
||||
lqr = AntiWindupLqr(
|
||||
self.A, self.B, self.C, self.Q, self.R, self.max_control, self.dt, keep_trace=True
|
||||
)
|
||||
|
||||
# Execute a few control steps
|
||||
for _ in range(10):
|
||||
control_signal = lqr.control(desired=self.desired, state=self.wrong_state)
|
||||
assert abs(control_signal) <= self.max_control, "control limits are not applied"
|
||||
|
||||
assert control_signal == -self.max_control, f"control limits not reached {control_signal}"
|
||||
|
||||
# test stationary summed error
|
||||
summed_error = lqr.summed_error
|
||||
control_signal = lqr.control(desired=self.desired, state=self.wrong_state)
|
||||
assert abs(lqr.summed_error - summed_error) < 1e-6, "summed error changes in saturation"
|
||||
|
||||
# test reactiveness
|
||||
control_signal_back = lqr.control(desired=self.desired, state=self.state_small_negative)
|
||||
assert abs(control_signal_back) < self.max_control, "anti wind up is not working"
|
||||
|
||||
def test_process_tracking(self) -> None:
|
||||
"""Assert useful controller behavior and that a pandas.DataFrame with process data is created."""
|
||||
|
||||
# Initialize controller
|
||||
lqr = AntiWindupLqr(
|
||||
self.A, self.B, self.C, self.Q, self.R, self.max_control, self.dt, keep_trace=True
|
||||
)
|
||||
|
||||
# Check zero control
|
||||
control_signal = lqr.control(desired=self.desired, state=self.state)
|
||||
assert control_signal == 0, "Control signal not zero when desired value 0 reached"
|
||||
control_signal = lqr.control(desired=self.desired1, state=self.wrong_state)
|
||||
assert control_signal == 0, "Control signal not zero when desired value 0 reached"
|
||||
lqr.reset()
|
||||
|
||||
# Execute a few control steps
|
||||
state = self.wrong_state.copy()
|
||||
initial_error = abs(self.C @ state - self.desired)
|
||||
for _ in range(10):
|
||||
control_signal = lqr.control(desired=self.desired, state=state)
|
||||
state = self.Ad.dot(state) + self.Bd.dot(control_signal)
|
||||
error = abs(self.C @ state - self.desired)
|
||||
assert error < initial_error or error == 0, "Error exceeds initial - instable controller?"
|
||||
|
||||
# Assert correct process tracing with LQR controller
|
||||
assert lqr.process is not None, "LQR did not keep trace of process"
|
||||
assert len(lqr.process.index) == 10, "LQR has not traced enough steps"
|
||||
|
||||
# Reset controller
|
||||
lqr.reset()
|
||||
|
||||
# Assert correct reset to initial state
|
||||
assert lqr.summed_error == 0.0, "Integral did not reset summed error properly"
|
||||
assert len(lqr.process.index) == 0, "LQR has not dropped process trace properly"
|
65
pyrate/tests/act/test_pid.py
Normal file
65
pyrate/tests/act/test_pid.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""This module asserts correct runtime behaviour of the pyrate.act.pid module."""
|
||||
|
||||
# Test environment
|
||||
from unittest import TestCase
|
||||
|
||||
# Mathematics
|
||||
from numpy import array
|
||||
from numpy import Inf
|
||||
from numpy.linalg import norm
|
||||
from numpy import vstack
|
||||
|
||||
# Package under test
|
||||
from pyrate.act.control import Pid
|
||||
|
||||
|
||||
class TestPid(TestCase):
|
||||
|
||||
"""Test for correct runtime behaviour in pyrate.act.control.pid."""
|
||||
|
||||
# In this context, we reproduce a common PID notation
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Setup the PID specification for testing."""
|
||||
|
||||
# PID specification
|
||||
self.P = array([[1.0]])
|
||||
self.I = array([[1.0]]) # noqa: 741
|
||||
self.D = array([[1.0]])
|
||||
self.dt = 0.5
|
||||
|
||||
# State and target
|
||||
# Target is already reached in this example
|
||||
self.state = vstack([0.0])
|
||||
self.state_derivative = vstack([0.0])
|
||||
self.desired = vstack([0.0])
|
||||
|
||||
def test_process_tracking(self) -> None:
|
||||
"""Assert that a pandas.DataFrame with process data is created."""
|
||||
|
||||
# Initialize PID controller
|
||||
pid = Pid(self.P, self.I, self.D, self.dt, keep_trace=True)
|
||||
|
||||
# Execute a few control steps
|
||||
state = self.state.copy()
|
||||
previous_error = Inf
|
||||
for _ in range(10):
|
||||
control_signal = pid.control(
|
||||
desired=self.desired, state=self.state, state_derivative=self.state_derivative
|
||||
)
|
||||
state += control_signal
|
||||
error = norm(state - self.desired).item() # Convert from numpy scalar to Python float
|
||||
assert error < previous_error or error == 0, "Error did not get lower."
|
||||
previous_error = error
|
||||
|
||||
# Assert correct process tracing with PID controller
|
||||
assert pid.process is not None, "PID did not keep trace of process"
|
||||
assert len(pid.process.index) == 10, "PID has not traced enough steps"
|
||||
|
||||
# Reset PID
|
||||
pid.reset()
|
||||
|
||||
# Assert correct reset to initial state
|
||||
assert pid.summed_error == 0.0, "PID did not reset summed error properly"
|
||||
assert len(pid.process.index) == 0, "PID has not dropped process trace properly"
|
88
pyrate/tests/act/test_pid_anti_windup.py
Normal file
88
pyrate/tests/act/test_pid_anti_windup.py
Normal file
@ -0,0 +1,88 @@
|
||||
"""This module asserts correct runtime behaviour of the pyrate.act.pid module."""
|
||||
|
||||
# Test environment
|
||||
from unittest import TestCase
|
||||
|
||||
# Mathematics
|
||||
from numpy import array
|
||||
from numpy import Inf
|
||||
from numpy.linalg import norm
|
||||
from numpy import vstack
|
||||
|
||||
# Package under test
|
||||
from pyrate.act.control import AntiWindupPid
|
||||
|
||||
|
||||
class TestAntiWindupPid(TestCase):
|
||||
|
||||
"""Test for correct runtime behaviour in pyrate.act.control.anti_windup_pid."""
|
||||
|
||||
# In this context, we reproduce a common PID notation
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Set up the PID specification for testing."""
|
||||
|
||||
# PID specification
|
||||
self.P = array([[1.0]])
|
||||
self.I = array([[1.0]]) # noqa: 741
|
||||
self.D = array([[1.0]])
|
||||
self.max_control = 1.0
|
||||
self.dt = 0.5
|
||||
|
||||
# State and target
|
||||
# Target is already reached in this example
|
||||
self.state = vstack([0.0])
|
||||
self.state_large = vstack([0.5])
|
||||
self.state_small_neg = vstack([-0.01])
|
||||
self.state_derivative = vstack([0.0])
|
||||
self.desired = vstack([0.0])
|
||||
|
||||
def test_anti_windup(self) -> None:
|
||||
"""Assert control signal in allowed range, PID responsive by limited integral part."""
|
||||
# Initialize PID controller
|
||||
pid = AntiWindupPid(self.P, self.I, self.D, self.max_control, self.dt, keep_trace=True)
|
||||
|
||||
# Execute a few control steps
|
||||
for _ in range(10):
|
||||
control_signal = pid.control(
|
||||
desired=self.desired, state=self.state_large, state_derivative=self.state_derivative
|
||||
)
|
||||
assert abs(control_signal) <= self.max_control, "control limits are not applied"
|
||||
assert abs(control_signal) == self.max_control, "control limits not reached"
|
||||
|
||||
# test reactiveness
|
||||
control_signal_back = pid.control(
|
||||
desired=self.desired, state=self.state_small_neg, state_derivative=self.state_derivative
|
||||
)
|
||||
assert abs(control_signal_back) < self.max_control + 1e-4, "anti wind up is not working"
|
||||
|
||||
def test_process_tracking(self) -> None:
|
||||
"""Assert that a pandas.DataFrame with process data is created."""
|
||||
|
||||
# Initialize PID controller
|
||||
pid = AntiWindupPid(self.P, self.I, self.D, self.max_control, self.dt, keep_trace=True)
|
||||
|
||||
# Execute a few control steps
|
||||
# Execute a few control steps
|
||||
state = self.state.copy()
|
||||
previous_error = Inf
|
||||
for _ in range(10):
|
||||
control_signal = pid.control(
|
||||
desired=self.desired, state=self.state, state_derivative=self.state_derivative
|
||||
)
|
||||
state += control_signal
|
||||
error = norm(state - self.desired).item() # Convert from numpy scalar to Python float
|
||||
assert error < previous_error or error == 0, "Error did not decrease"
|
||||
previous_error = error
|
||||
|
||||
# Assert correct process tracing with PID controller
|
||||
assert pid.process is not None, "PID did not keep trace of process"
|
||||
assert len(pid.process.index) == 10, "PID has not traced enough steps"
|
||||
|
||||
# Reset PID
|
||||
pid.reset()
|
||||
|
||||
# Assert correct reset to initial state
|
||||
assert pid.summed_error == 0.0, "PID did not reset summed error properly"
|
||||
assert len(pid.process.index) == 0, "PID has not dropped process trace properly"
|
Reference in New Issue
Block a user