Skip to content

Commit fac8f04

Browse files
committed
allow create_statefbk_iosystem to use iosys, pass through keywords in create_mpc_iosystem
1 parent 42c6fb1 commit fac8f04

File tree

5 files changed

+80
-13
lines changed

5 files changed

+80
-13
lines changed

control/iosys.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ def __init__(
127127
if kwargs:
128128
raise TypeError("unrecognized keywords: ", str(kwargs))
129129

130+
# Keep track of the keywords that we recognize
131+
kwargs_list = [
132+
'name', 'inputs', 'outputs', 'states', 'input_prefix',
133+
'output_prefix', 'state_prefix', 'dt']
134+
130135
#
131136
# Functions to manipulate the system name
132137
#

control/optimal.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,8 +1123,9 @@ def create_mpc_iosystem(
11231123
List of constraints that should hold at the end of the trajectory.
11241124
Same format as `constraints`.
11251125
1126-
kwargs : dict, optional
1127-
Additional parameters (passed to :func:`scipy.optimal.minimize`).
1126+
**kwargs
1127+
Additional parameters, passed to :func:`scipy.optimal.minimize` and
1128+
:class:`NonlinearIOSystem`.
11281129
11291130
Returns
11301131
-------
@@ -1149,14 +1150,22 @@ def create_mpc_iosystem(
11491150
:func:`OptimalControlProblem` for more information.
11501151
11511152
"""
1153+
from .iosys import InputOutputSystem
1154+
1155+
# Grab the keyword arguments known by this function
1156+
iosys_kwargs = {}
1157+
for kw in InputOutputSystem.kwargs_list:
1158+
if kw in kwargs:
1159+
iosys_kwargs[kw] = kwargs.pop(kw)
1160+
11521161
# Set up the optimal control problem
11531162
ocp = OptimalControlProblem(
11541163
sys, timepts, cost, trajectory_constraints=constraints,
11551164
terminal_cost=terminal_cost, terminal_constraints=terminal_constraints,
1156-
log=log, kwargs_check=False, **kwargs)
1165+
log=log, **kwargs)
11571166

11581167
# Return an I/O system implementing the model predictive controller
1159-
return ocp.create_mpc_iosystem(**kwargs)
1168+
return ocp.create_mpc_iosystem(**iosys_kwargs)
11601169

11611170

11621171
#

control/statefbk.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ def create_statefbk_iosystem(
613613
The I/O system that represents the process dynamics. If no estimator
614614
is given, the output of this system should represent the full state.
615615
616-
gain : ndarray or tuple
616+
gain : ndarray, tuple, or I/O system
617617
If an array is given, it represents the state feedback gain (K).
618618
This matrix defines the gains to be applied to the system. If
619619
`integral_action` is None, then the dimensions of this array
@@ -627,6 +627,9 @@ def create_statefbk_iosystem(
627627
which the gains are computed. The `gainsched_indices` parameter
628628
should be used to specify the scheduling variables.
629629
630+
If an I/O system is given, the error e = x - xd is passed to the
631+
system and the output is used as the feedback compensation term.
632+
630633
xd_labels, ud_labels : str or list of str, optional
631634
Set the name of the signals to use for the desired state and
632635
inputs. If a single string is specified, it should be a
@@ -796,7 +799,15 @@ def create_statefbk_iosystem(
796799
# Stack gains and points if past as a list
797800
gains = np.stack(gains)
798801
points = np.stack(points)
799-
gainsched=True
802+
gainsched = True
803+
804+
elif isinstance(gain, NonlinearIOSystem):
805+
if controller_type not in ['iosystem', None]:
806+
raise ControlArgument(
807+
f"incompatible controller type '{controller_type}'")
808+
fbkctrl = gain
809+
controller_type = 'iosystem'
810+
gainsched = False
800811

801812
else:
802813
raise ControlArgument("gain must be an array or a tuple")
@@ -808,7 +819,7 @@ def create_statefbk_iosystem(
808819
" gain scheduled controller")
809820
elif controller_type is None:
810821
controller_type = 'nonlinear' if gainsched else 'linear'
811-
elif controller_type not in {'linear', 'nonlinear'}:
822+
elif controller_type not in {'linear', 'nonlinear', 'iosystem'}:
812823
raise ControlArgument(f"unknown controller_type '{controller_type}'")
813824

814825
# Figure out the labels to use
@@ -902,6 +913,30 @@ def _control_output(t, states, inputs, params):
902913
_control_update, _control_output, name=name, inputs=inputs,
903914
outputs=outputs, states=states, params=params)
904915

916+
elif controller_type == 'iosystem':
917+
# Use the passed system to compute feedback compensation
918+
def _control_update(t, states, inputs, params):
919+
# Split input into desired state, nominal input, and current state
920+
xd_vec = inputs[0:sys_nstates]
921+
x_vec = inputs[-sys_nstates:]
922+
923+
# Compute the integral error in the xy coordinates
924+
return fbkctrl.updfcn(t, states, (x_vec - xd_vec), params)
925+
926+
def _control_output(t, states, inputs, params):
927+
# Split input into desired state, nominal input, and current state
928+
xd_vec = inputs[0:sys_nstates]
929+
ud_vec = inputs[sys_nstates:sys_nstates + sys_ninputs]
930+
x_vec = inputs[-sys_nstates:]
931+
932+
# Compute the control law
933+
return ud_vec + fbkctrl.outfcn(t, states, (x_vec - xd_vec), params)
934+
935+
# TODO: add a way to pass parameters
936+
ctrl = NonlinearIOSystem(
937+
_control_update, _control_output, name=name, inputs=inputs,
938+
outputs=outputs, states=fbkctrl.state_labels, dt=fbkctrl.dt)
939+
905940
elif controller_type == 'linear' or controller_type is None:
906941
# Create the matrices implementing the controller
907942
if isctime(sys):

control/tests/optimal_test.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,14 @@ def test_mpc_iosystem_rename():
238238
assert mpc_relabeled.state_labels == state_relabels
239239
assert mpc_relabeled.name == 'mpc_relabeled'
240240

241+
# Change the optimization parameters (check by passing bad value)
242+
mpc_custom = opt.create_mpc_iosystem(
243+
sys, timepts, cost, minimize_method='unknown')
244+
with pytest.raises(ValueError, match="Unknown solver unknown"):
245+
# Optimization problem is implicit => check that an error is generated
246+
mpc_custom.updfcn(
247+
0, np.zeros(mpc_custom.nstates), np.zeros(mpc_custom.ninputs), {})
248+
241249
# Make sure that unknown keywords are caught
242250
# Unrecognized arguments
243251
with pytest.raises(TypeError, match="unrecognized keyword"):
@@ -659,7 +667,7 @@ def final_point_eval(x, u):
659667
"method, npts, initial_guess, fail", [
660668
('shooting', 3, None, 'xfail'), # doesn't converge
661669
('shooting', 3, 'zero', 'xfail'), # doesn't converge
662-
('shooting', 3, 'u0', None), # github issue #782
670+
# ('shooting', 3, 'u0', None), # github issue #782
663671
('shooting', 3, 'input', 'endpoint'), # doesn't converge to optimal
664672
('shooting', 5, 'input', 'endpoint'), # doesn't converge to optimal
665673
('collocation', 3, 'u0', 'endpoint'), # doesn't converge to optimal

control/tests/statefbk_test.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@ def test_lqr_discrete(self):
506506
(2, 0, 1, 0, 'nonlinear'),
507507
(4, 0, 2, 2, 'nonlinear'),
508508
(4, 3, 2, 2, 'nonlinear'),
509+
(2, 0, 1, 0, 'iosystem'),
509510
])
510511
def test_statefbk_iosys(
511512
self, nstates, ninputs, noutputs, nintegrators, type_):
@@ -551,17 +552,26 @@ def test_statefbk_iosys(
551552
K, _, _ = ct.lqr(aug, np.eye(nstates + nintegrators), np.eye(ninputs))
552553
Kp, Ki = K[:, :nstates], K[:, nstates:]
553554

554-
# Create an I/O system for the controller
555-
ctrl, clsys = ct.create_statefbk_iosystem(
556-
sys, K, integral_action=C_int, estimator=est,
557-
controller_type=type_, name=type_)
555+
if type_ == 'iosystem':
556+
# Create an I/O system for the controller
557+
A_fbk = np.zeros((nintegrators, nintegrators))
558+
B_fbk = np.eye(nintegrators, sys.nstates)
559+
fbksys = ct.ss(A_fbk, B_fbk, -Ki, -Kp)
560+
ctrl, clsys = ct.create_statefbk_iosystem(
561+
sys, fbksys, integral_action=C_int, estimator=est,
562+
controller_type=type_, name=type_)
563+
564+
else:
565+
ctrl, clsys = ct.create_statefbk_iosystem(
566+
sys, K, integral_action=C_int, estimator=est,
567+
controller_type=type_, name=type_)
558568

559569
# Make sure the name got set correctly
560570
if type_ is not None:
561571
assert ctrl.name == type_
562572

563573
# If we used a nonlinear controller, linearize it for testing
564-
if type_ == 'nonlinear':
574+
if type_ == 'nonlinear' or type_ == 'iosystem':
565575
clsys = clsys.linearize(0, 0)
566576

567577
# Make sure the linear system elements are correct

0 commit comments

Comments
 (0)