Skip to content

Commit d700ad7

Browse files
authored
Merge pull request #1071 from murrayrm/create_statefbk-29Nov2024
add reference gain design pattern to create_statefbk_iosystem
2 parents da55654 + bb52b70 commit d700ad7

5 files changed

Lines changed: 249 additions & 92 deletions

File tree

‎control/statefbk.py‎

Lines changed: 122 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,25 @@
3939
#
4040
# $Id$
4141

42-
# External packages and modules
42+
import warnings
43+
4344
import numpy as np
4445
import scipy as sp
45-
import warnings
4646

4747
from . import statesp
48-
from .mateqn import care, dare, _check_shape
49-
from .statesp import StateSpace, _ssmatrix, _convert_to_statespace, ss
48+
from .config import _process_legacy_keyword
49+
from .exception import ControlArgument, ControlDimension, \
50+
ControlNotImplemented, ControlSlycot
51+
from .iosys import _process_indices, _process_labels, isctime, isdtime
5052
from .lti import LTI
51-
from .iosys import isdtime, isctime, _process_indices, _process_labels
53+
from .mateqn import _check_shape, care, dare
5254
from .nlsys import NonlinearIOSystem, interconnect
53-
from .exception import ControlSlycot, ControlArgument, ControlDimension, \
54-
ControlNotImplemented
55-
from .config import _process_legacy_keyword
55+
from .statesp import StateSpace, _convert_to_statespace, _ssmatrix, ss
5656

5757
# Make sure we have access to the right slycot routines
5858
try:
5959
from slycot import sb03md57
60+
6061
# wrap without the deprecation warning
6162
def sb03md(n, C, A, U, dico, job='X',fact='N',trana='N',ldwork=None):
6263
ret = sb03md57(A, U, C, dico, job, fact, trana, ldwork)
@@ -581,8 +582,9 @@ def dlqr(*args, **kwargs):
581582

582583
# Function to create an I/O sytems representing a state feedback controller
583584
def create_statefbk_iosystem(
584-
sys, gain, integral_action=None, estimator=None, controller_type=None,
585-
xd_labels=None, ud_labels=None, gainsched_indices=None,
585+
sys, gain, feedfwd_gain=None, integral_action=None, estimator=None,
586+
controller_type=None, xd_labels=None, ud_labels=None, ref_labels=None,
587+
feedfwd_pattern='trajgen', gainsched_indices=None,
586588
gainsched_method='linear', control_indices=None, state_indices=None,
587589
name=None, inputs=None, outputs=None, states=None, **kwargs):
588590
r"""Create an I/O system using a (full) state feedback controller.
@@ -592,7 +594,7 @@ def create_statefbk_iosystem(
592594
593595
.. math:: u = u_d - K_p (x - x_d) - K_i \int(C x - C x_d)
594596
595-
It can be called in the form::
597+
by calling
596598
597599
ctrl, clsys = ct.create_statefbk_iosystem(sys, K)
598600
@@ -608,6 +610,18 @@ def create_statefbk_iosystem(
608610
609611
where :math:`\mu` represents the scheduling variable.
610612
613+
Alternatively, a controller of the form
614+
615+
.. math:: u = k_f r - K_p x - K_i \int(C x - r)
616+
617+
can be created by calling
618+
619+
ctrl, clsys = ct.create_statefbk_iosystem(
620+
sys, K, kf, feedfwd_pattern='refgain')
621+
622+
In either form, an estimator can also be used to compute the estimated
623+
state from the input and output measurements.
624+
611625
Parameters
612626
----------
613627
sys : NonlinearIOSystem
@@ -631,14 +645,15 @@ def create_statefbk_iosystem(
631645
If an I/O system is given, the error e = x - xd is passed to the
632646
system and the output is used as the feedback compensation term.
633647
634-
xd_labels, ud_labels : str or list of str, optional
635-
Set the name of the signals to use for the desired state and
636-
inputs. If a single string is specified, it should be a format
637-
string using the variable `i` as an index. Otherwise, a list of
638-
strings matching the size of `x_d` and `u_d`, respectively, should
639-
be used. Default is "xd[{i}]" for xd_labels and "ud[{i}]" for
640-
ud_labels. These settings can also be overridden using the
641-
`inputs` keyword.
648+
feedfwd_gain : array_like, optional
649+
Specify the feedforward gain, `k_f`. Used only for the reference
650+
gain design pattern. If not given and if `sys` is a `StateSpace`
651+
(linear) system, will be computed as -1/(C (A-BK)^{-1}) B.
652+
653+
feedfwd_pattern : str, optional
654+
If set to 'refgain', the reference gain design pattern is used to
655+
create the controller instead of the trajectory generation
656+
('trajgen') pattern.
642657
643658
integral_action : ndarray, optional
644659
If this keyword is specified, the controller can include integral
@@ -680,17 +695,19 @@ def create_statefbk_iosystem(
680695
Returns
681696
-------
682697
ctrl : NonlinearIOSystem
683-
Input/output system representing the controller. This system
684-
takes as inputs the desired state `x_d`, the desired input
685-
`u_d`, and either the system state `x` or the estimated state
686-
`xhat`. It outputs the controller action `u` according to the
687-
formula `u = u_d - K(x - x_d)`. If the keyword
688-
`integral_action` is specified, then an additional set of
689-
integrators is included in the control system (with the gain
690-
matrix `K` having the integral gains appended after the state
691-
gains). If a gain scheduled controller is specified, the gain
692-
(proportional and integral) are evaluated using the scheduling
693-
variables specified by `gainsched_indices`.
698+
Input/output system representing the controller. For the 'trajgen'
699+
design pattern (default), this system takes as inputs the desired
700+
state `x_d`, the desired input `u_d`, and either the system state
701+
`x` or the estimated state `xhat`. It outputs the controller
702+
action `u` according to the formula `u = u_d - K(x - x_d)`. For
703+
the 'refgain' design pattern, the system takes as inputs the
704+
reference input `r` and the system or estimated state. If the
705+
keyword `integral_action` is specified, then an additional set of
706+
integrators is included in the control system (with the gain matrix
707+
`K` having the integral gains appended after the state gains). If
708+
a gain scheduled controller is specified, the gain (proportional
709+
and integral) are evaluated using the scheduling variables
710+
specified by `gainsched_indices`.
694711
695712
clsys : NonlinearIOSystem
696713
Input/output system representing the closed loop system. This
@@ -716,6 +733,15 @@ def create_statefbk_iosystem(
716733
specified as either integer offsets or as estimator/system output
717734
signal names. If not specified, defaults to the system states.
718735
736+
xd_labels, ud_labels, ref_labels : str or list of str, optional
737+
Set the name of the signals to use for the desired state and inputs
738+
or the reference inputs (for the 'refgain' design pattern). If a
739+
single string is specified, it should be a format string using the
740+
variable `i` as an index. Otherwise, a list of strings matching
741+
the size of `x_d` and `u_d`, respectively, should be used. Default
742+
is "xd[{i}]" for xd_labels and "ud[{i}]" for ud_labels. These
743+
settings can also be overridden using the `inputs` keyword.
744+
719745
inputs, outputs, states : str, or list of str, optional
720746
List of strings that name the individual signals of the transformed
721747
system. If not given, the inputs, outputs, and states are the same
@@ -753,6 +779,11 @@ def create_statefbk_iosystem(
753779
if kwargs:
754780
raise TypeError("unrecognized keywords: ", str(kwargs))
755781

782+
# Check for consistency of positional parameters
783+
if feedfwd_gain is not None and feedfwd_pattern != 'refgain':
784+
raise ControlArgument(
785+
"feedfwd_gain specified but feedfwd_pattern != 'refgain'")
786+
756787
# Figure out what inputs to the system to use
757788
control_indices = _process_indices(
758789
control_indices, 'control', sys.input_labels, sys.ninputs)
@@ -812,14 +843,18 @@ def create_statefbk_iosystem(
812843
# Check for gain scheduled controller
813844
if len(gain) != 2:
814845
raise ControlArgument("gain must be a 2-tuple for gain scheduling")
846+
elif feedfwd_pattern != 'trajgen':
847+
raise NotImplementedError(
848+
"Gain scheduling is not implemented for pattern "
849+
f"'{feedfwd_pattern}'")
815850
gains, points = gain[0:2]
816851

817852
# Stack gains and points if past as a list
818853
gains = np.stack(gains)
819854
points = np.stack(points)
820855
gainsched = True
821856

822-
elif isinstance(gain, NonlinearIOSystem):
857+
elif isinstance(gain, NonlinearIOSystem) and feedfwd_pattern != 'refgain':
823858
if controller_type not in ['iosystem', None]:
824859
raise ControlArgument(
825860
f"incompatible controller type '{controller_type}'")
@@ -841,20 +876,29 @@ def create_statefbk_iosystem(
841876
raise ControlArgument(f"unknown controller_type '{controller_type}'")
842877

843878
# Figure out the labels to use
844-
xd_labels = _process_labels(
845-
xd_labels, 'xd', ['xd[{i}]'.format(i=i) for i in range(sys_nstates)])
846-
ud_labels = _process_labels(
847-
ud_labels, 'ud', ['ud[{i}]'.format(i=i) for i in range(sys_ninputs)])
848-
849-
# Create the signal and system names
850-
if inputs is None:
851-
inputs = xd_labels + ud_labels + estimator.output_labels
879+
if feedfwd_pattern == 'trajgen':
880+
xd_labels = _process_labels(xd_labels, 'xd', [
881+
'xd[{i}]'.format(i=i) for i in range(sys_nstates)])
882+
ud_labels = _process_labels(ud_labels, 'ud', [
883+
'ud[{i}]'.format(i=i) for i in range(sys_ninputs)])
884+
885+
# Create the signal and system names
886+
if inputs is None:
887+
inputs = xd_labels + ud_labels + estimator.output_labels
888+
elif feedfwd_pattern == 'refgain':
889+
ref_labels = _process_labels(ref_labels, 'r', [
890+
f'r[{i}]' for i in range(sys_ninputs)])
891+
if inputs is None:
892+
inputs = ref_labels + estimator.output_labels
893+
else:
894+
raise NotImplementedError(f"unknown pattern '{feedfwd_pattern}'")
895+
852896
if outputs is None:
853897
outputs = [sys.input_labels[i] for i in control_indices]
854898
if states is None:
855899
states = nintegrators
856900

857-
# Process gainscheduling variables, if present
901+
# Process gain scheduling variables, if present
858902
if gainsched:
859903
# Create a copy of the scheduling variable indices (default = xd)
860904
gainsched_indices = _process_indices(
@@ -897,7 +941,7 @@ def _compute_gain(mu):
897941
return K
898942

899943
# Define the controller system
900-
if controller_type == 'nonlinear':
944+
if controller_type == 'nonlinear' and feedfwd_pattern == 'trajgen':
901945
# Create an I/O system for the state feedback gains
902946
def _control_update(t, states, inputs, params):
903947
# Split input into desired state, nominal input, and current state
@@ -931,7 +975,7 @@ def _control_output(t, states, inputs, params):
931975
_control_update, _control_output, name=name, inputs=inputs,
932976
outputs=outputs, states=states, params=params)
933977

934-
elif controller_type == 'iosystem':
978+
elif controller_type == 'iosystem' and feedfwd_pattern == 'trajgen':
935979
# Use the passed system to compute feedback compensation
936980
def _control_update(t, states, inputs, params):
937981
# Split input into desired state, nominal input, and current state
@@ -955,7 +999,7 @@ def _control_output(t, states, inputs, params):
955999
_control_update, _control_output, name=name, inputs=inputs,
9561000
outputs=outputs, states=fbkctrl.state_labels, dt=fbkctrl.dt)
9571001

958-
elif controller_type == 'linear' or controller_type is None:
1002+
elif controller_type in 'linear' and feedfwd_pattern == 'trajgen':
9591003
# Create the matrices implementing the controller
9601004
if isctime(sys):
9611005
# Continuous time: integrator
@@ -973,6 +1017,37 @@ def _control_output(t, states, inputs, params):
9731017
A_lqr, B_lqr, C_lqr, D_lqr, dt=sys.dt, name=name,
9741018
inputs=inputs, outputs=outputs, states=states)
9751019

1020+
elif feedfwd_pattern == 'refgain':
1021+
if controller_type not in ['linear', 'iosystem']:
1022+
raise ControlArgument(
1023+
"refgain design pattern only supports linear controllers")
1024+
1025+
if feedfwd_gain is None:
1026+
raise ControlArgument(
1027+
"'feedfwd_gain' required for reference gain pattern")
1028+
1029+
# Check to make sure the reference gain is valid
1030+
Kf = np.atleast_2d(feedfwd_gain)
1031+
if Kf.ndim != 2 or Kf.shape[0] != sys.ninputs or \
1032+
Kf.shape[1] != sys.ninputs:
1033+
raise ControlArgument("feedfwd_gain is not the right shape")
1034+
1035+
# Create the matrices implementing the controller
1036+
# [r, x]->[u]: u = k_f r - K_p x - K_i \int(C x - r)
1037+
if isctime(sys):
1038+
# Continuous time: integrator
1039+
A_lqr = np.zeros((C.shape[0], C.shape[0]))
1040+
else:
1041+
# Discrete time: summer
1042+
A_lqr = np.eye(C.shape[0])
1043+
B_lqr = np.hstack([-np.eye(C.shape[0], sys_ninputs), C])
1044+
C_lqr = -K[:, sys_nstates:] # integral gain (opt)
1045+
D_lqr = np.hstack([Kf, -K])
1046+
1047+
ctrl = ss(
1048+
A_lqr, B_lqr, C_lqr, D_lqr, dt=sys.dt, name=name,
1049+
inputs=inputs, outputs=outputs, states=states)
1050+
9761051
else:
9771052
raise ControlArgument(f"unknown controller_type '{controller_type}'")
9781053

@@ -1020,7 +1095,7 @@ def ctrb(A, B, t=None):
10201095
bmat = _ssmatrix(B)
10211096
n = np.shape(amat)[0]
10221097
m = np.shape(bmat)[1]
1023-
1098+
10241099
if t is None or t > n:
10251100
t = n
10261101

@@ -1042,7 +1117,7 @@ def obsv(A, C, t=None):
10421117
Dynamics and output matrix of the system
10431118
t : None or integer
10441119
maximum time horizon of the controllability matrix, max = A.shape[0]
1045-
1120+
10461121
Returns
10471122
-------
10481123
O : 2D array (or matrix)
@@ -1062,14 +1137,14 @@ def obsv(A, C, t=None):
10621137
cmat = _ssmatrix(C)
10631138
n = np.shape(amat)[0]
10641139
p = np.shape(cmat)[0]
1065-
1140+
10661141
if t is None or t > n:
10671142
t = n
10681143

10691144
# Construct the observability matrix
10701145
obsv = np.zeros((t * p, n))
10711146
obsv[:p, :] = cmat
1072-
1147+
10731148
for k in range(1, t):
10741149
obsv[k * p:(k + 1) * p, :] = np.dot(obsv[(k - 1) * p:k * p, :], amat)
10751150

‎control/statesp.py‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,12 @@ def __init__(self, *args, **kwargs):
252252
D = np.zeros((C.shape[0], B.shape[1]))
253253
D = _ssmatrix(D)
254254

255+
# If only direct term is present, adjust sizes of C and D if needed
256+
if D.size > 0 and B.size == 0:
257+
B = np.zeros((0, D.shape[1]))
258+
if D.size > 0 and C.size == 0:
259+
C = np.zeros((D.shape[0], 0))
260+
255261
# Matrices defining the linear system
256262
self.A = A
257263
self.B = B
@@ -268,7 +274,7 @@ def __init__(self, *args, **kwargs):
268274

269275
# Process iosys keywords
270276
defaults = args[0] if len(args) == 1 else \
271-
{'inputs': D.shape[1], 'outputs': D.shape[0],
277+
{'inputs': B.shape[1], 'outputs': C.shape[0],
272278
'states': A.shape[0]}
273279
name, inputs, outputs, states, dt = _process_iosys_keywords(
274280
kwargs, defaults, static=(A.size == 0))
@@ -295,16 +301,15 @@ def __init__(self, *args, **kwargs):
295301
# Check to make sure everything is consistent
296302
#
297303
# Check that the matrix sizes are consistent
298-
if A.shape[0] != A.shape[1] or self.nstates != A.shape[0]:
299-
raise ValueError("A must be square.")
300-
if self.nstates != B.shape[0]:
301-
raise ValueError("A and B must have the same number of rows.")
302-
if self.nstates != C.shape[1]:
303-
raise ValueError("A and C must have the same number of columns.")
304-
if self.ninputs != B.shape[1] or self.ninputs != D.shape[1]:
305-
raise ValueError("B and D must have the same number of columns.")
306-
if self.noutputs != C.shape[0] or self.noutputs != D.shape[0]:
307-
raise ValueError("C and D must have the same number of rows.")
304+
def _check_shape(matrix, expected, name):
305+
if matrix.shape != expected:
306+
raise ValueError(
307+
f"{name} is the wrong shape; "
308+
f"expected {expected} instead of {matrix.shape}")
309+
_check_shape(A, (self.nstates, self.nstates), "A")
310+
_check_shape(B, (self.nstates, self.ninputs), "B")
311+
_check_shape(C, (self.noutputs, self.nstates), "C")
312+
_check_shape(D, (self.noutputs, self.ninputs), "D")
308313

309314
#
310315
# Final processing

0 commit comments

Comments
 (0)