Skip to content

Commit 8710ee2

Browse files
committed
initial implementation of gain scheduling
1 parent f15fc0f commit 8710ee2

File tree

4 files changed

+290
-53
lines changed

4 files changed

+290
-53
lines changed

control/iosys.py

+18-8
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,10 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6,
536536
# functions.
537537
#
538538

539+
# If x0 and u0 are specified as lists, concatenate the elements
540+
x0 = _concatenate_list_elements(x0, 'x0')
541+
u0 = _concatenate_list_elements(u0, 'u0')
542+
539543
# Figure out dimensions if they were not specified.
540544
nstates = _find_size(self.nstates, x0)
541545
ninputs = _find_size(self.ninputs, u0)
@@ -1758,14 +1762,7 @@ def input_output_response(
17581762
ninputs = U.shape[0]
17591763

17601764
# If we were passed a list of initial states, concatenate them
1761-
if isinstance(X0, (tuple, list)):
1762-
X0_list = []
1763-
for i, x0 in enumerate(X0):
1764-
x0 = np.array(x0).reshape(-1) # convert everyting to 1D array
1765-
X0_list += x0.tolist() # add elements to initial state
1766-
1767-
# Save the newly created input vector
1768-
X0 = np.array(X0_list)
1765+
X0 = _concatenate_list_elements(X0, 'X0')
17691766

17701767
# If the initial state is too short, make it longer (NB: sys.nstates
17711768
# could be None if nstates comes from size of initial condition)
@@ -2377,6 +2374,19 @@ def ss(*args, **kwargs):
23772374
return sys
23782375

23792376

2377+
# Utility function to allow lists states, inputs
2378+
def _concatenate_list_elements(X, name='X'):
2379+
# If we were passed a list, concatenate the elements together
2380+
if isinstance(X, (tuple, list)):
2381+
X_list = []
2382+
for i, x in enumerate(X):
2383+
x = np.array(x).reshape(-1) # convert everyting to 1D array
2384+
X_list += x.tolist() # add elements to initial state
2385+
return np.array(X_list)
2386+
2387+
# Otherwise, do nothing
2388+
return X
2389+
23802390
def rss(states=1, outputs=1, inputs=1, strictly_proper=False, **kwargs):
23812391
"""Create a stable random state space object.
23822392

control/statefbk.py

+135-44
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
# External packages and modules
4343
import numpy as np
44+
import scipy as sp
4445

4546
from . import statesp
4647
from .mateqn import care, dare, _check_shape
@@ -71,7 +72,7 @@ def sb03md(n, C, A, U, dico, job='X',fact='N',trana='N',ldwork=None):
7172
sb03od = None
7273

7374

74-
__all__ = ['ctrb', 'obsv', 'gram', 'place', 'place_varga', 'lqr',
75+
__all__ = ['ctrb', 'obsv', 'gram', 'place', 'place_varga', 'lqr',
7576
'dlqr', 'acker', 'create_statefbk_iosystem']
7677

7778

@@ -600,8 +601,8 @@ def dlqr(*args, **kwargs):
600601

601602
# Function to create an I/O sytems representing a state feedback controller
602603
def create_statefbk_iosystem(
603-
sys, K, integral_action=None, xd_labels='xd[{i}]', ud_labels='ud[{i}]',
604-
estimator=None, type='linear'):
604+
sys, gain, integral_action=None, estimator=None, type=None,
605+
xd_labels='xd[{i}]', ud_labels='ud[{i}]', gainsched_indices=None):
605606
"""Create an I/O system using a (full) state feedback controller
606607
607608
This function creates an input/output system that implements a
@@ -617,26 +618,46 @@ def create_statefbk_iosystem(
617618
feedback gain (eg, from LQR). The function returns the controller
618619
``ctrl`` and the closed loop systems ``clsys``, both as I/O systems.
619620
621+
A gain scheduled controller can also be created, by passing a list of
622+
gains and a corresponding list of values of a set of scheduling
623+
variables. In this case, the controller has the form
624+
625+
u = ud - K_p(mu) (x - xd) - K_i(mu) integral(C x - C x_d)
626+
627+
where mu represents the scheduling variable.
628+
620629
Parameters
621630
----------
622631
sys : InputOutputSystem
623632
The I/O system that represents the process dynamics. If no estimator
624633
is given, the output of this system should represent the full state.
625634
626-
K : ndarray
627-
The state feedback gain. This matrix defines the gains to be
628-
applied to the system. If ``integral_action`` is None, then the
629-
dimensions of this array should be (sys.ninputs, sys.nstates). If
630-
`integral action` is set to a matrix or a function, then additional
631-
columns represent the gains of the integral states of the
632-
controller.
635+
gain : ndarray or tuple
636+
If a array is give, it represents the state feedback gain (K).
637+
This matrix defines the gains to be applied to the system. If
638+
``integral_action`` is None, then the dimensions of this array
639+
should be (sys.ninputs, sys.nstates). If `integral action` is
640+
set to a matrix or a function, then additional columns
641+
represent the gains of the integral states of the controller.
642+
643+
If a tuple is given, then it specifies a gain schedule. The
644+
tuple should be of the form
645+
646+
(gains, points[, method])
647+
648+
where gains is a list of gains :math:`K_j` and points is a list of
649+
values :math:`\\mu_j` at which the gains are computed. If `method`
650+
is specified, it is passed to :func:`scipy.interpolate.griddata` to
651+
specify the method of interpolation. Possible values include
652+
`linear`, `nearest`, and `cubic`.
633653
634654
xd_labels, ud_labels : str or list of str, optional
635655
Set the name of the signals to use for the desired state and inputs.
636656
If a single string is specified, it should be a format string using
637-
the variable ``i`` as an index. Otherwise, a list of strings matching
638-
the size of xd and ud, respectively, should be used. Default is
639-
``'xd[{i}]'`` for xd_labels and ``'xd[{i}]'`` for ud_labels.
657+
the variable ``i`` as an index. Otherwise, a list of strings
658+
matching the size of xd and ud, respectively, should be used.
659+
Default is ``'xd[{i}]'`` for xd_labels and ``'ud[{i}]'`` for
660+
ud_labels.
640661
641662
integral_action : None, ndarray, or func, optional
642663
If this keyword is specified, the controller can include integral
@@ -650,30 +671,48 @@ def create_statefbk_iosystem(
650671
``K`` matrix.
651672
652673
estimator : InputOutputSystem, optional
653-
If an estimator is provided, using the states of the estimator as
674+
If an estimator is provided, use the states of the estimator as
654675
the system inputs for the controller.
655676
656-
type : 'nonlinear' or 'linear', optional
657-
Set the type of controller to create. The default is a linear
658-
controller implementing the LQR regulator. If the type is 'nonlinear',
659-
a :class:NonlinearIOSystem is created instead, with the gain ``K`` as
660-
a parameter (allowing modifications of the gain at runtime).
677+
gainsched_indices : list of integers, optional
678+
If a gain scheduled controller is specified, specify the indices of
679+
the controller input to use for scheduling the gain. The input to
680+
the controller is the desired state xd, the desired input ud, and
681+
either the system state x or the system output y (if an estimator is
682+
given).
683+
684+
type : 'linear' or 'nonlinear', optional
685+
Set the type of controller to create. The default for a linear gain
686+
is a linear controller implementing the LQR regulator. If the type
687+
is 'nonlinear', a :class:NonlinearIOSystem is created instead, with
688+
the gain ``K`` as a parameter (allowing modifications of the gain at
689+
runtime). If the gain parameter is a tuple, the a nonlinear,
690+
gain-scheduled controller is created.
661691
662692
Returns
663693
-------
664694
ctrl : InputOutputSystem
665695
Input/output system representing the controller. This system takes
666-
as inputs the desired state xd, the desired input ud, and the system
667-
state x. It outputs the controller action u according to the
668-
formula u = ud - K(x - xd). If the keyword `integral_action` is
669-
specified, then an additional set of integrators is included in the
670-
control system (with the gain matrix K having the integral gains
671-
appended after the state gains).
696+
as inputs the desired state xd, the desired input ud, and either the
697+
system state x or the system output y (if an estimator is given).
698+
It outputs the controller action u according to the formula u = ud -
699+
K(x - xd). If the keyword `integral_action` is specified, then an
700+
additional set of integrators is included in the control system
701+
(with the gain matrix K having the integral gains appended after the
702+
state gains). If a gain scheduled controller is specified, the gain
703+
(proportional and integral) are evaluated using the input mu.
672704
673705
clsys : InputOutputSystem
674706
Input/output system representing the closed loop system. This
675-
systems takes as inputs the desired trajectory (xd, ud) and outputs
676-
the system state x and the applied input u (vertically stacked).
707+
systems takes as inputs the desired trajectory (xd, ud) along with
708+
any unassigned gain scheduling values mu and outputs the system
709+
state x and the applied input u (vertically stacked).
710+
711+
Notes
712+
-----
713+
1. If the gain scheduling variable labes are set to the names of system
714+
states, inputs, or outputs or desired states or inputs, then the
715+
scheduling variables are internally connected to those variables.
677716
678717
"""
679718
# Make sure that we were passed an I/O system as an input
@@ -709,52 +748,104 @@ def create_statefbk_iosystem(
709748
C = np.zeros((0, sys.nstates))
710749

711750
# Check to make sure that state feedback has the right shape
712-
if not isinstance(K, np.ndarray) or \
713-
K.shape != (sys.ninputs, estimator.noutputs + nintegrators):
751+
if isinstance(gain, np.ndarray):
752+
K = gain
753+
if K.shape != (sys.ninputs, estimator.noutputs + nintegrators):
754+
raise ControlArgument(
755+
f'Control gain must be an array of size {sys.ninputs}'
756+
f'x {sys.nstates}' +
757+
(f'+{nintegrators}' if nintegrators > 0 else ''))
758+
gainsched = False
759+
760+
elif isinstance(gain, tuple):
761+
# Check for gain scheduled controller
762+
gains, points = gain[0:2]
763+
method = 'nearest' if len(gain) < 3 else gain[2]
764+
765+
# Stack gains and points if past as a list
766+
gains = np.stack(gains)
767+
points = np.stack(points)
768+
gainsched=True
769+
770+
else:
771+
raise ControlArgument("gain must be an array or a tuple")
772+
773+
# Decide on the type of system to create
774+
if gainsched and type == 'linear':
714775
raise ControlArgument(
715-
f'Control gain must be an array of size {sys.ninputs}'
716-
f'x {sys.nstates}' +
717-
(f'+{nintegrators}' if nintegrators > 0 else ''))
776+
"type 'linear' not allowed for gain scheduled controller")
777+
elif type is None:
778+
type = 'nonlinear' if gainsched else 'linear'
779+
elif type not in {'linear', 'nonlinear'}:
780+
raise ControlArgument(f"unknown type '{type}'")
718781

719782
# Figure out the labels to use
720783
if isinstance(xd_labels, str):
721-
# Gnerate the list of labels using the argument as a format string
784+
# Generate the list of labels using the argument as a format string
722785
xd_labels = [xd_labels.format(i=i) for i in range(sys.nstates)]
723786

724787
if isinstance(ud_labels, str):
725-
# Gnerate the list of labels using the argument as a format string
788+
# Generate the list of labels using the argument as a format string
726789
ud_labels = [ud_labels.format(i=i) for i in range(sys.ninputs)]
727790

791+
# Process gainscheduling variables, if present
792+
if gainsched:
793+
# Create a copy of the scheduling variable indices (default = empty)
794+
gainsched_indices = [] if gainsched_indices is None \
795+
else list(gainsched_indices)
796+
797+
# Make sure the scheduling variable indices are the right length
798+
if len(gainsched_indices) != points.shape[1]:
799+
raise ControlArgument(
800+
"Length of gainsched_indices must match dimension of"
801+
" scheduling variables")
802+
803+
# TODO: Process scheduling variables
804+
728805
# Define the controller system
729806
if type == 'nonlinear':
730807
# Create an I/O system for the state feedback gains
731-
def _control_update(t, x, inputs, params):
808+
def _control_update(t, states, inputs, params):
732809
# Split input into desired state, nominal input, and current state
733810
xd_vec = inputs[0:sys.nstates]
734811
x_vec = inputs[-estimator.nstates:]
735812

736813
# Compute the integral error in the xy coordinates
737-
return C @ x_vec - C @ xd_vec
814+
return C @ (x_vec - xd_vec)
815+
816+
def _compute_gain(mu, gains_, points_):
817+
K = np.array([
818+
[sp.interpolate.griddata(
819+
points_, gains_[:, i, j], mu, method=method).item()
820+
for j in range(gains_.shape[2])]
821+
for i in range(gains_.shape[1])
822+
])
823+
return K
738824

739-
def _control_output(t, e, z, params):
740-
K = params.get('K')
825+
def _control_output(t, states, inputs, params):
826+
if gainsched:
827+
mu = inputs[gainsched_indices]
828+
K_ = _compute_gain(mu, gains, points)
829+
else:
830+
K_ = params.get('K')
741831

742832
# Split input into desired state, nominal input, and current state
743-
xd_vec = z[0:sys.nstates]
744-
ud_vec = z[sys.nstates:sys.nstates + sys.ninputs]
745-
x_vec = z[-sys.nstates:]
833+
xd_vec = inputs[0:sys.nstates]
834+
ud_vec = inputs[sys.nstates:sys.nstates + sys.ninputs]
835+
x_vec = inputs[-sys.nstates:]
746836

747837
# Compute the control law
748-
u = ud_vec - K[:, 0:sys.nstates] @ (x_vec - xd_vec)
838+
u = ud_vec - K_[:, 0:sys.nstates] @ (x_vec - xd_vec)
749839
if nintegrators > 0:
750-
u -= K[:, sys.nstates:] @ e
840+
u -= K_[:, sys.nstates:] @ states
751841

752842
return u
753843

844+
params = {} if gainsched else {'K': K}
754845
ctrl = NonlinearIOSystem(
755846
_control_update, _control_output, name='control',
756847
inputs=xd_labels + ud_labels + estimator.output_labels,
757-
outputs=list(sys.input_index.keys()), params={'K': K},
848+
outputs=list(sys.input_index.keys()), params=params,
758849
states=nintegrators)
759850

760851
elif type == 'linear' or type is None:

control/tests/iosys_test.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -1187,7 +1187,7 @@ def test_signals_naming_convention_0_8_4(self, tsys):
11871187
assert "copy of namedsys.x0" in same_name_series.state_index
11881188

11891189
def test_named_signals_linearize_inconsistent(self, tsys):
1190-
"""Mare sure that providing inputs or outputs not consistent with
1190+
"""Make sure that providing inputs or outputs not consistent with
11911191
updfcn or outfcn fail
11921192
"""
11931193

@@ -1232,6 +1232,17 @@ def outfcn(t, x, u, params):
12321232
with pytest.raises(ValueError):
12331233
sys2.linearize(x0, u0)
12341234

1235+
def test_linearize_concatenation(self, kincar):
1236+
# Create a simple nonlinear system to check (kinematic car)
1237+
iosys = kincar
1238+
linearized = iosys.linearize([0, np.array([0, 0])], [0, 0])
1239+
np.testing.assert_array_almost_equal(linearized.A, np.zeros((3,3)))
1240+
np.testing.assert_array_almost_equal(
1241+
linearized.B, [[1, 0], [0, 0], [0, 1]])
1242+
np.testing.assert_array_almost_equal(
1243+
linearized.C, [[1, 0, 0], [0, 1, 0]])
1244+
np.testing.assert_array_almost_equal(linearized.D, np.zeros((2,2)))
1245+
12351246
def test_lineariosys_statespace(self, tsys):
12361247
"""Make sure that a LinearIOSystem is also a StateSpace object"""
12371248
iosys_siso = ct.LinearIOSystem(tsys.siso_linsys, name='siso')

0 commit comments

Comments
 (0)