Skip to content

Commit 9ed4817

Browse files
committed
initial implementation, docstrings for refgain pattern
1 parent 391d29c commit 9ed4817

2 files changed

Lines changed: 104 additions & 35 deletions

File tree

control/statefbk.py

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ def dlqr(*args, **kwargs):
583583
# Function to create an I/O sytems representing a state feedback controller
584584
def create_statefbk_iosystem(
585585
sys, gain, feedfwd_gain=None, integral_action=None, estimator=None,
586-
controller_type=None, xd_labels=None, ud_labels=None,
586+
controller_type=None, xd_labels=None, ud_labels=None, ref_labels=None,
587587
feedfwd_pattern='trajgen', gainsched_indices=None,
588588
gainsched_method='linear', control_indices=None, state_indices=None,
589589
name=None, inputs=None, outputs=None, states=None, **kwargs):
@@ -645,24 +645,15 @@ def create_statefbk_iosystem(
645645
If an I/O system is given, the error e = x - xd is passed to the
646646
system and the output is used as the feedback compensation term.
647647
648-
xd_labels, ud_labels : str or list of str, optional
649-
Set the name of the signals to use for the desired state and
650-
inputs. If a single string is specified, it should be a format
651-
string using the variable `i` as an index. Otherwise, a list of
652-
strings matching the size of `x_d` and `u_d`, respectively, should
653-
be used. Default is "xd[{i}]" for xd_labels and "ud[{i}]" for
654-
ud_labels. These settings can also be overridden using the
655-
`inputs` keyword.
656-
657-
feedfwd_pattern : str, optional
658-
If set to 'refgain', the reference gain design pattern is used to
659-
create the controller instead of the trajectory generation pattern.
660-
661648
feedfwd_gain : array_like, optional
662649
Specify the feedforward gain, `k_f`. Used only for the reference
663650
gain design pattern. If not given and if `sys` is a `StateSpace`
664651
(linear) system, will be computed as -1/(C (A-BK)^{-1}) B.
665652
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 pattern.
656+
666657
integral_action : ndarray, optional
667658
If this keyword is specified, the controller can include integral
668659
action in addition to state feedback. The value of the
@@ -703,17 +694,19 @@ def create_statefbk_iosystem(
703694
Returns
704695
-------
705696
ctrl : NonlinearIOSystem
706-
Input/output system representing the controller. This system
707-
takes as inputs the desired state `x_d`, the desired input
708-
`u_d`, and either the system state `x` or the estimated state
709-
`xhat`. It outputs the controller action `u` according to the
710-
formula `u = u_d - K(x - x_d)`. If the keyword
711-
`integral_action` is specified, then an additional set of
712-
integrators is included in the control system (with the gain
713-
matrix `K` having the integral gains appended after the state
714-
gains). If a gain scheduled controller is specified, the gain
715-
(proportional and integral) are evaluated using the scheduling
716-
variables specified by `gainsched_indices`.
697+
Input/output system representing the controller. For the 'trajgen'
698+
design patter (default), this system takes as inputs the desired
699+
state `x_d`, the desired input `u_d`, and either the system state
700+
`x` or the estimated state `xhat`. It outputs the controller
701+
action `u` according to the formula `u = u_d - K(x - x_d)`. For
702+
the 'refgain' design patter, the system takes as inputs the
703+
reference input `r` and the system or estimated state. If the
704+
keyword `integral_action` is specified, then an additional set of
705+
integrators is included in the control system (with the gain matrix
706+
`K` having the integral gains appended after the state gains). If
707+
a gain scheduled controller is specified, the gain (proportional
708+
and integral) are evaluated using the scheduling variables
709+
specified by `gainsched_indices`.
717710
718711
clsys : NonlinearIOSystem
719712
Input/output system representing the closed loop system. This
@@ -739,6 +732,15 @@ def create_statefbk_iosystem(
739732
specified as either integer offsets or as estimator/system output
740733
signal names. If not specified, defaults to the system states.
741734
735+
xd_labels, ud_labels, ref_labels : str or list of str, optional
736+
Set the name of the signals to use for the desired state and inputs
737+
or the reference inputs (for the 'refgain' design pattern). If a
738+
single string is specified, it should be a format string using the
739+
variable `i` as an index. Otherwise, a list of strings matching
740+
the size of `x_d` and `u_d`, respectively, should be used. Default
741+
is "xd[{i}]" for xd_labels and "ud[{i}]" for ud_labels. These
742+
settings can also be overridden using the `inputs` keyword.
743+
742744
inputs, outputs, states : str, or list of str, optional
743745
List of strings that name the individual signals of the transformed
744746
system. If not given, the inputs, outputs, and states are the same
@@ -835,14 +837,18 @@ def create_statefbk_iosystem(
835837
# Check for gain scheduled controller
836838
if len(gain) != 2:
837839
raise ControlArgument("gain must be a 2-tuple for gain scheduling")
840+
elif feedfwd_pattern != 'trajgen':
841+
raise NotImplementedError(
842+
"Gain scheduling is not implemented for pattern "
843+
f"'{feedfwd_pattern}'")
838844
gains, points = gain[0:2]
839845

840846
# Stack gains and points if past as a list
841847
gains = np.stack(gains)
842848
points = np.stack(points)
843849
gainsched = True
844850

845-
elif isinstance(gain, NonlinearIOSystem):
851+
elif isinstance(gain, NonlinearIOSystem) and feedfwd_pattern != 'refgain':
846852
if controller_type not in ['iosystem', None]:
847853
raise ControlArgument(
848854
f"incompatible controller type '{controller_type}'")
@@ -874,7 +880,10 @@ def create_statefbk_iosystem(
874880
if inputs is None:
875881
inputs = xd_labels + ud_labels + estimator.output_labels
876882
elif feedfwd_pattern == 'refgain':
877-
raise NotImplementedError("reference gain pattern not yet implemented")
883+
ref_labels = _process_labels(ref_labels, 'r', [
884+
f'r[{i}]' for i in range(sys_ninputs)])
885+
if inputs is None:
886+
inputs = ref_labels + estimator.output_labels
878887
else:
879888
raise NotImplementedError(f"unknown pattern '{feedfwd_pattern}'")
880889

@@ -1006,7 +1015,32 @@ def _control_output(t, states, inputs, params):
10061015
if controller_type not in ['linear', 'iosystem']:
10071016
raise ControlArgument(
10081017
"refgain design pattern only supports linear controllers")
1009-
raise NotImplementedError("reference gain pattern not yet implemented")
1018+
1019+
if feedfwd_gain is None:
1020+
raise ControlArgument(
1021+
"'feedfwd_gain' required for reference gain pattern")
1022+
1023+
# Check to make sure the reference gain is valid
1024+
Kf = np.atleast_2d(feedfwd_gain)
1025+
if Kf.ndim != 2 or Kf.shape[0] != sys.ninputs or \
1026+
Kf.shape[1] != sys.ninputs:
1027+
raise ControlArgument("feedfwd_gain is not the right shape")
1028+
1029+
# Create the matrices implementing the controller
1030+
# [r, x]->[u]: u = k_f r - K_p x - K_i \int(C x - r)
1031+
if isctime(sys):
1032+
# Continuous time: integrator
1033+
A_lqr = np.zeros((C.shape[0], C.shape[0]))
1034+
else:
1035+
# Discrete time: summer
1036+
A_lqr = np.eye(C.shape[0])
1037+
B_lqr = np.hstack([-np.eye(C.shape[0], sys_ninputs), C])
1038+
C_lqr = -K[:, sys_nstates:] # integral gain (opt)
1039+
D_lqr = np.hstack([Kf, -K])
1040+
1041+
ctrl = ss(
1042+
A_lqr, B_lqr, C_lqr, D_lqr, dt=sys.dt, name=name,
1043+
inputs=inputs, outputs=outputs, states=states)
10101044

10111045
else:
10121046
raise ControlArgument(f"unknown controller_type '{controller_type}'")

control/tests/statefbk_test.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def testCtrbT(self):
5656
Wctrue = np.array([[5., 6.], [7., 8.]])
5757
Wc = ctrb(A, B, t=t)
5858
np.testing.assert_array_almost_equal(Wc, Wctrue)
59-
59+
6060
def testObsvSISO(self):
6161
A = np.array([[1., 2.], [3., 4.]])
6262
C = np.array([[5., 7.]])
@@ -70,7 +70,7 @@ def testObsvMIMO(self):
7070
Wotrue = np.array([[5., 6.], [7., 8.], [23., 34.], [31., 46.]])
7171
Wo = obsv(A, C)
7272
np.testing.assert_array_almost_equal(Wo, Wotrue)
73-
73+
7474
def testObsvT(self):
7575
A = np.array([[1., 2.], [3., 4.]])
7676
C = np.array([[5., 6.], [7., 8.]])
@@ -128,15 +128,14 @@ def testGramRc(self):
128128
C = np.array([[4., 5.], [6., 7.]])
129129
D = np.array([[13., 14.], [15., 16.]])
130130
sys = ss(A, B, C, D)
131-
Rctrue = np.array([[4.30116263, 5.6961343],
132-
[0., 0.23249528]])
131+
Rctrue = np.array([[4.30116263, 5.6961343], [0., 0.23249528]])
133132
Rc = gram(sys, 'cf')
134133
np.testing.assert_array_almost_equal(Rc, Rctrue)
135134
sysd = ct.c2d(sys, 0.2)
136135
Rctrue = np.array([[1.91488054, 2.53468814],
137136
[0. , 0.10290372]])
138137
Rc = gram(sysd, 'cf')
139-
np.testing.assert_array_almost_equal(Rc, Rctrue)
138+
np.testing.assert_array_almost_equal(Rc, Rctrue)
140139

141140
@slycotonly
142141
def testGramWo(self):
@@ -149,7 +148,7 @@ def testGramWo(self):
149148
Wo = gram(sys, 'o')
150149
np.testing.assert_array_almost_equal(Wo, Wotrue)
151150
sysd = ct.c2d(sys, 0.2)
152-
Wotrue = np.array([[ 1305.369179, -440.046414],
151+
Wotrue = np.array([[ 1305.369179, -440.046414],
153152
[ -440.046414, 333.034844]])
154153
Wo = gram(sysd, 'o')
155154
np.testing.assert_array_almost_equal(Wo, Wotrue)
@@ -184,7 +183,7 @@ def testGramRo(self):
184183
Rotrue = np.array([[ 36.12989315, -12.17956588],
185184
[ 0. , 13.59018097]])
186185
Ro = gram(sysd, 'of')
187-
np.testing.assert_array_almost_equal(Ro, Rotrue)
186+
np.testing.assert_array_almost_equal(Ro, Rotrue)
188187

189188
def testGramsys(self):
190189
sys = tf([1.], [1., 1., 1.])
@@ -1143,3 +1142,39 @@ def test_gainsched_errors(unicycle):
11431142
ctrl, clsys = ct.create_statefbk_iosystem(
11441143
unicycle, (gains, points),
11451144
gainsched_indices=[3, 2], gainsched_method='unknown')
1145+
1146+
1147+
@pytest.mark.parametrize("ninputs, Kf", [
1148+
(1, 1), (1, None),
1149+
(2, np.diag([1, 1])), (2, None),
1150+
])
1151+
def test_refgain_pattern(ninputs, Kf):
1152+
sys = ct.rss(2, 2, ninputs, strictly_proper=True)
1153+
sys.C = np.eye(2)
1154+
1155+
K, _, _ = ct.lqr(sys.A, sys.B, np.eye(sys.nstates), np.eye(sys.ninputs))
1156+
if Kf is None:
1157+
# Make sure we get an error if we don't specify Kf
1158+
with pytest.raises(ControlArgument, match="'feedfwd_gain' required"):
1159+
ctrl, clsys = ct.create_statefbk_iosystem(
1160+
sys, K, Kf, feedfwd_pattern='refgain')
1161+
1162+
# Now compute the gain to give unity zero frequency gain
1163+
C = np.eye(ninputs, sys.nstates)
1164+
Kf = -np.linalg.inv(
1165+
C @ np.linalg.inv(sys.A - sys.B @ K) @ sys.B)
1166+
ctrl, clsys = ct.create_statefbk_iosystem(
1167+
sys, K, Kf, feedfwd_pattern='refgain')
1168+
1169+
np.testing.assert_almost_equal(
1170+
C @ clsys(0)[0:sys.nstates], np.eye(ninputs))
1171+
1172+
else:
1173+
ctrl, clsys = ct.create_statefbk_iosystem(
1174+
sys, K, Kf, feedfwd_pattern='refgain')
1175+
1176+
manual = ct.feedback(sys, K) * Kf
1177+
np.testing.assert_almost_equal(clsys.A, manual.A)
1178+
np.testing.assert_almost_equal(clsys.B, manual.B)
1179+
np.testing.assert_almost_equal(clsys.C[:sys.nstates, :], manual.C)
1180+
np.testing.assert_almost_equal(clsys.D[:sys.nstates, :], manual.D)

0 commit comments

Comments
 (0)