Skip to content

Commit bd322e7

Browse files
committed
remove polytope dependence; implement MPC iosys w/ notebook example
1 parent 6f9c092 commit bd322e7

File tree

3 files changed

+328
-31
lines changed

3 files changed

+328
-31
lines changed

control/obc.py

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,8 @@ def __init__(
103103
# Initial guess
104104
#
105105
# We store an initial guess (zero input) in case it is not specified
106-
# later.
107-
#
108-
# TODO: add the ability to overwride this when calling the optimizer.
106+
# later. Note that create_mpc_iosystem() will reset the initial guess
107+
# based on the current state of the MPC controller.
109108
#
110109
self.initial_guess = np.zeros(
111110
self.system.ninputs * self.time_vector.size)
@@ -128,24 +127,25 @@ def cost_function(self, inputs):
128127
x = self.x
129128
inputs = inputs.reshape(
130129
(self.system.ninputs, self.time_vector.size))
131-
130+
132131
# Simulate the system to get the state
133132
_, _, states = ct.input_output_response(
134133
self.system, self.time_vector, inputs, x, return_x=True)
135-
134+
136135
# Trajectory cost
137136
# TODO: vectorize
138137
cost = 0
139138
for i, time in enumerate(self.time_vector):
140-
cost += self.integral_cost(states[:,i], inputs[:,i])
141-
139+
cost += self.integral_cost(states[:,i], inputs[:,i])
140+
142141
# Terminal cost
143142
if self.terminal_cost is not None:
144143
cost += self.terminal_cost(states[:,-1], inputs[:,-1])
145-
144+
146145
# Return the total cost for this input sequence
147146
return cost
148147

148+
149149
#
150150
# Constraints
151151
#
@@ -188,7 +188,7 @@ def constraint_function(self, inputs):
188188
x = self.x
189189
inputs = inputs.reshape(
190190
(self.system.ninputs, self.time_vector.size))
191-
191+
192192
# Simulate the system to get the state
193193
_, _, states = ct.input_output_response(
194194
self.system, self.time_vector, inputs, x, return_x=True)
@@ -234,15 +234,15 @@ def __call__(self, x, squeeze=None):
234234
def mpc(self, x, squeeze=None):
235235
"""Compute the optimal input at state x"""
236236
_, inputs = self.compute_trajectory(x, squeeze=squeeze)
237-
return None if inputs is None else inputs.transpose()[0]
237+
return None if inputs is None else inputs[:,0]
238238

239239
# Compute the optimal trajectory from the current state
240240
def compute_trajectory(
241241
self, x, squeeze=None, transpose=None, return_x=None):
242242
"""Compute the optimal input at state x"""
243243
# Store the initial state (for use in constraint_function)
244244
self.x = x
245-
245+
246246
# Call ScipPy optimizer
247247
res = sp.optimize.minimize(
248248
self.cost_function, self.initial_guess,
@@ -263,14 +263,33 @@ def compute_trajectory(
263263
self.system, self.time_vector, inputs, x, return_x=True)
264264
else:
265265
states=None
266-
266+
267267
return _process_time_response(
268268
self.system, self.time_vector, inputs, states,
269269
transpose=transpose, return_x=return_x, squeeze=squeeze)
270270

271+
# Create an input/output system implementing an MPC controller
272+
def create_mpc_iosystem(self, dt=True):
273+
def _update(t, x, u, params={}):
274+
inputs = x.reshape((self.system.ninputs, self.time_vector.size))
275+
self.initial_guess = np.hstack(
276+
[inputs[:,1:], inputs[:,-1:]]).reshape(-1)
277+
_, inputs = self.compute_trajectory(u)
278+
return inputs.reshape(-1)
279+
280+
def _output(t, x, u, params={}):
281+
inputs = x.reshape((self.system.ninputs, self.time_vector.size))
282+
return inputs[:,0]
283+
284+
return ct.NonlinearIOSystem(
285+
_update, _output, dt=dt,
286+
inputs=self.system.nstates,
287+
outputs=self.system.ninputs,
288+
states=self.system.ninputs * self.time_vector.size)
289+
271290

272291
#
273-
# Create a polytope constraint on the system state
292+
# Create a polytope constraint on the system state: A x <= b
274293
#
275294
# As in the cost function evaluation, the main "trick" in creating a constrain
276295
# on the state or input is to properly evaluate the constraint on the stacked
@@ -283,26 +302,48 @@ def compute_trajectory(
283302
# optimization methods LinearConstraint and NonlinearConstraint as "types" to
284303
# keep things consistent with the terminology in scipy.optimize.
285304
#
286-
def state_poly_constraint(sys, polytope):
305+
def state_poly_constraint(sys, A, b):
306+
"""Create state constraint from polytope"""
307+
# TODO: make sure the system and constraints are compatible
308+
309+
# Return a linear constraint object based on the polynomial
310+
return (opt.LinearConstraint,
311+
np.hstack([A, np.zeros((A.shape[0], sys.ninputs))]),
312+
np.full(A.shape[0], -np.inf), polytope.b)
313+
314+
315+
def state_range_constraint(sys, lb, ub):
287316
"""Create state constraint from polytope"""
288317
# TODO: make sure the system and constraints are compatible
289318

290319
# Return a linear constraint object based on the polynomial
291320
return (opt.LinearConstraint,
292321
np.hstack(
293-
[polytope.A, np.zeros((polytope.A.shape[0], sys.ninputs))]),
294-
np.full(polytope.A.shape[0], -np.inf), polytope.b)
322+
[np.eye(sys.nstates), np.zeros((sys.nstates, sys.ninputs))]),
323+
np.array(lb), np.array(ub))
324+
295325

296326
# Create a constraint polytope on the system input
297-
def input_poly_constraint(sys, polytope):
327+
def input_poly_constraint(sys, A, b):
328+
"""Create input constraint from polytope"""
329+
# TODO: make sure the system and constraints are compatible
330+
331+
# Return a linear constraint object based on the polynomial
332+
return (opt.LinearConstraint,
333+
np.hstack(
334+
[np.zeros((A.shape[0], sys.nstates)), A]),
335+
np.full(A.shape[0], -np.inf), b)
336+
337+
338+
def input_range_constraint(sys, lb, ub):
298339
"""Create input constraint from polytope"""
299340
# TODO: make sure the system and constraints are compatible
300341

301342
# Return a linear constraint object based on the polynomial
302343
return (opt.LinearConstraint,
303344
np.hstack(
304-
[np.zeros((polytope.A.shape[0], sys.nstates)), polytope.A]),
305-
np.full(polytope.A.shape[0], -np.inf), polytope.b)
345+
[np.zeros((sys.ninputs, sys.nstates)), np.eye(sys.ninputs)]),
346+
np.array(lb), np.array(ub))
306347

307348

308349
#
@@ -316,9 +357,9 @@ def input_poly_constraint(sys, polytope):
316357
# imposing a linear constraint:
317358
#
318359
# np.hstack(
319-
# [polytope.A @ sys.C, np.zeros((polytope.A.shape[0], sys.ninputs))])
360+
# [A @ sys.C, np.zeros((A.shape[0], sys.ninputs))])
320361
#
321-
def output_poly_constraint(sys, polytope):
362+
def output_poly_constraint(sys, A, b):
322363
"""Create output constraint from polytope"""
323364
# TODO: make sure the system and constraints are compatible
324365

@@ -329,12 +370,12 @@ def _evaluate_output_constraint(x):
329370
states = x[:sys.nstates]
330371
inputs = x[sys.nstates:]
331372
outputs = sys._out(0, states, inputs)
332-
return polytope.A @ outputs
373+
return A @ outputs
333374

334375
# Return a nonlinear constraint object based on the polynomial
335376
return (opt.NonlinearConstraint,
336377
_evaluate_output_constraint,
337-
np.full(polytope.A.shape[0], -np.inf), polytope.b)
378+
np.full(A.shape[0], -np.inf), b)
338379

339380

340381
#
@@ -345,8 +386,8 @@ def _evaluate_output_constraint(x):
345386
# evaluates to associted quadratic cost. This is compatible with the way that
346387
# the `cost_function` evaluates the cost at each point in the trajectory.
347388
#
348-
def quadratic_cost(sys, Q, R):
389+
def quadratic_cost(sys, Q, R, x0=0, u0=0):
349390
"""Create quadratic cost function"""
350391
Q = np.atleast_2d(Q)
351392
R = np.atleast_2d(R)
352-
return lambda x, u: x @ Q @ x + u @ R @ u
393+
return lambda x, u: ((x-x0) @ Q @ (x-x0) + (u-u0) @ R @ (u-u0)).item()

control/tests/obc_test.py

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import scipy as sp
1111
import control as ct
1212
import control.obc as obc
13-
import polytope as pc
13+
from control.tests.conftest import slycotonly
1414

1515
def test_finite_horizon_mpc_simple():
1616
# Define a linear system with constraints
@@ -21,8 +21,7 @@ def test_finite_horizon_mpc_simple():
2121

2222
# State and input constraints
2323
constraints = [
24-
obc.state_poly_constraint(sys, pc.box2poly([[-5, 5], [-5, 5]])),
25-
obc.input_poly_constraint(sys, pc.box2poly([[-1, 1]])),
24+
(sp.optimize.LinearConstraint, np.eye(3), [-5, -5, -1], [5, 5, 1]),
2625
]
2726

2827
# Quadratic state and input penalty
@@ -48,10 +47,12 @@ def test_finite_horizon_mpc_simple():
4847
# Convert controller to an explicit form (not implemented yet)
4948
# mpc_explicit = obc.explicit_mpc();
5049

51-
# Test explicit controller
50+
# Test explicit controller
5251
# u_explicit = mpc_explicit(x0)
5352
# np.testing.assert_array_almost_equal(u_openloop, u_explicit)
5453

54+
55+
@slycotonly
5556
def test_finite_horizon_mpc_oscillator():
5657
# oscillator model defined in 2D
5758
# Source: https://www.mpt3.org/UI/RegulationProblem
@@ -65,8 +66,7 @@ def test_finite_horizon_mpc_oscillator():
6566

6667
# state and input constraints
6768
trajectory_constraints = [
68-
obc.output_poly_constraint(sys, pc.box2poly([[-10, 10]])),
69-
obc.input_poly_constraint(sys, pc.box2poly([[-1, 1]]))
69+
(sp.optimize.LinearConstraint, np.eye(3), [-10, -10, -1], [10, 10, 1]),
7070
]
7171

7272
# Include weights on states/inputs
@@ -85,3 +85,57 @@ def test_finite_horizon_mpc_oscillator():
8585

8686
# Add tests to make sure everything works
8787
t, u_openloop = optctrl.compute_trajectory([1, 1])
88+
89+
90+
def test_mpc_iosystem():
91+
# model of an aircraft discretized with 0.2s sampling time
92+
# Source: https://www.mpt3.org/UI/RegulationProblem
93+
A = [[0.99, 0.01, 0.18, -0.09, 0],
94+
[ 0, 0.94, 0, 0.29, 0],
95+
[ 0, 0.14, 0.81, -0.9, 0],
96+
[ 0, -0.2, 0, 0.95, 0],
97+
[ 0, 0.09, 0, 0, 0.9]]
98+
B = [[ 0.01, -0.02],
99+
[-0.14, 0],
100+
[ 0.05, -0.2],
101+
[ 0.02, 0],
102+
[-0.01, 0]]
103+
C = [[0, 1, 0, 0, -1],
104+
[0, 0, 1, 0, 0],
105+
[0, 0, 0, 1, 0],
106+
[1, 0, 0, 0, 0]]
107+
model = ct.ss2io(ct.ss(A, B, C, 0, 0.2))
108+
109+
# For the simulation we need the full state output
110+
sys = ct.ss2io(ct.ss(A, B, np.eye(5), 0, 0.2))
111+
112+
# compute the steady state values for a particular value of the input
113+
ud = np.array([0.8, -0.3])
114+
xd = np.linalg.inv(np.eye(5) - A) @ B @ ud
115+
yd = C @ xd
116+
117+
# provide constraints on the system signals
118+
constraints = [obc.input_range_constraint(sys, [-5, -6], [5, 6])]
119+
120+
# provide penalties on the system signals
121+
Q = model.C.transpose() @ np.diag([10, 10, 10, 10]) @ model.C
122+
R = np.diag([3, 2])
123+
cost = obc.quadratic_cost(model, Q, R, x0=xd, u0=ud)
124+
125+
# online MPC controller object is constructed with a horizon 6
126+
optctrl = obc.OptimalControlProblem(
127+
model, np.arange(0, 6) * 0.2, cost, constraints)
128+
129+
# Define an I/O system implementing model predictive control
130+
ctrl = optctrl.create_mpc_iosystem()
131+
loop = ct.feedback(sys, ctrl, 1)
132+
133+
# Choose a nearby initial condition to speed up computation
134+
X0 = np.hstack([xd, np.kron(ud, np.ones(6))]) * 0.99
135+
136+
Nsim = 10
137+
tout, xout = ct.input_output_response(
138+
loop, np.arange(0, Nsim) * 0.2, 0, X0)
139+
140+
# Make sure the system converged to the desired state
141+
np.testing.assert_almost_equal(xout[0:sys.nstates, -1], xd, decimal=1)

0 commit comments

Comments
 (0)