Skip to content

Commit 0060618

Browse files
committed
fix handling of endpoint in discrete optimal
1 parent df6b352 commit 0060618

2 files changed

Lines changed: 40 additions & 27 deletions

File tree

control/optimal.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class OptimalControlProblem():
5858
extension of the time axis.
5959
log : bool, optional
6060
If `True`, turn on logging messages (using Python logging module).
61+
Use ``logging.basicConfig`` to enable logging output (e.g., to a file).
6162
kwargs : dict, optional
6263
Additional parameters (passed to :func:`scipy.optimal.minimize`).
6364
@@ -95,7 +96,7 @@ class OptimalControlProblem():
9596
trajectory generated by the proposed input. It does this by calling a
9697
user-defined function for the integral_cost given the current states and
9798
inputs at each point along the trajectory and then adding the value of a
98-
user-defined terminal cost at the final pint in the trajectory.
99+
user-defined terminal cost at the final point in the trajectory.
99100
100101
The `_constraint_function` method evaluates the constraint functions along
101102
the trajectory generated by the proposed input. As in the case of the
@@ -269,7 +270,6 @@ def _cost_function(self, coeffs):
269270
+ str(states))
270271

271272
# Trajectory cost
272-
# TODO: vectorize
273273
if ct.isctime(self.system):
274274
# Evaluate the costs
275275
costs = [self.integral_cost(states[:, i], inputs[:, i]) for
@@ -279,6 +279,7 @@ def _cost_function(self, coeffs):
279279
dt = np.diff(self.timepts)
280280

281281
# Integrate the cost
282+
# TODO: vectorize
282283
cost = 0
283284
for i in range(self.timepts.size-1):
284285
# Approximate the integral using trapezoidal rule
@@ -288,8 +289,8 @@ def _cost_function(self, coeffs):
288289
# Sum the integral cost over the time (second) indices
289290
# cost += self.integral_cost(states[:,i], inputs[:,i])
290291
cost = sum(map(
291-
self.integral_cost, np.transpose(states),
292-
np.transpose(inputs)))
292+
self.integral_cost, np.transpose(states[:, :-1]),
293+
np.transpose(inputs[:, :-1])))
293294

294295
# Terminal cost
295296
if self.terminal_cost is not None:
@@ -661,8 +662,8 @@ def _output(t, x, u, params={}):
661662
return ct.NonlinearIOSystem(
662663
_update, _output, dt=dt,
663664
inputs=self.system.nstates, outputs=self.system.ninputs,
664-
states=self.system.ninputs *
665-
(self.timepts.size if self.basis is None else self.basis.N))
665+
states=self.system.ninputs * \
666+
(self.timepts.size if self.basis is None else self.basis.N))
666667

667668
# Compute the optimal trajectory from the current state
668669
def compute_trajectory(
@@ -755,7 +756,7 @@ def compute_mpc(self, x, squeeze=None):
755756
756757
"""
757758
res = self.compute_trajectory(x, squeeze=squeeze)
758-
return inputs[:, 0] if res.success else None
759+
return res.inputs[:, 0]
759760

760761

761762
# Optimal control result

control/tests/optimal_test.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ def test_finite_horizon_simple():
3939

4040
# Retrieve the full open-loop predictions
4141
res = opt.solve_ocp(
42-
sys, time, x0, cost, constraints, squeeze=True)
42+
sys, time, x0, cost, constraints, squeeze=True,
43+
terminal_cost=cost) # include to match MPT3 formulation
4344
t, u_openloop = res.time, res.inputs
4445
np.testing.assert_almost_equal(
4546
u_openloop, [-1, -1, 0.1393, 0.3361, -5.204e-16], decimal=4)
@@ -57,9 +58,7 @@ def test_finite_horizon_simple():
5758
#
5859
# The next unit test is intended to confirm that a finite horizon
5960
# optimal control problem with terminal cost set to LQR "cost to go"
60-
# gives the same answer as LQR. Unfortunately, it requires a discrete
61-
# time LQR function which is not yet availbale => for now this just
62-
# tests the interface a bit.
61+
# gives the same answer as LQR.
6362
#
6463
@slycotonly
6564
def test_discrete_lqr():
@@ -76,35 +75,43 @@ def test_discrete_lqr():
7675
# Include weights on states/inputs
7776
Q = np.eye(2)
7877
R = 1
79-
K, S, E = ct.lqr(A, B, Q, R) # note: *continuous* time LQR
78+
K, S, E = ct.dlqr(A, B, Q, R)
8079

8180
# Compute the integral and terminal cost
8281
integral_cost = opt.quadratic_cost(sys, Q, R)
8382
terminal_cost = opt.quadratic_cost(sys, S, None)
8483

85-
# Formulate finite horizon MPC problem
84+
# Solve the LQR problem
85+
lqr_sys = ct.ss2io(ct.ss(A - B @ K, B, C, D, 1))
86+
87+
# Generate a simulation of the LQR controller
8688
time = np.arange(0, 5, 1)
8789
x0 = np.array([1, 1])
90+
_, _, lqr_x = ct.input_output_response(
91+
lqr_sys, time, 0, x0, return_x=True)
92+
93+
# Use LQR input as initial guess to avoid convergence/precision issues
94+
lqr_u = -K @ lqr_x[0:time.size]
95+
96+
# Formulate the optimal control problem and compute optimal trajectory
8897
optctrl = opt.OptimalControlProblem(
89-
sys, time, integral_cost, terminal_cost=terminal_cost)
98+
sys, time, integral_cost, terminal_cost=terminal_cost,
99+
initial_guess=lqr_u)
90100
res1 = optctrl.compute_trajectory(x0, return_states=True)
91101

92-
with pytest.xfail("discrete LQR not implemented"):
93-
# Result should match LQR
94-
K, S, E = ct.dlqr(A, B, Q, R)
95-
lqr_sys = ct.ss2io(ct.ss(A - B @ K, B, C, D, 1))
96-
_, _, lqr_x = ct.input_output_response(
97-
lqr_sys, time, 0, x0, return_x=True)
98-
np.testing.assert_almost_equal(res1.states, lqr_x)
102+
# Compare to make sure results are the same
103+
np.testing.assert_almost_equal(res1.inputs, lqr_u[0])
104+
np.testing.assert_almost_equal(res1.states, lqr_x)
99105

100106
# Add state and input constraints
101107
trajectory_constraints = [
102-
(sp.optimize.LinearConstraint, np.eye(3), [-10, -10, -1], [10, 10, 1]),
108+
(sp.optimize.LinearConstraint, np.eye(3), [-5, -5, -.5], [5, 5, 0.5]),
103109
]
104110

105111
# Re-solve
106112
res2 = opt.solve_ocp(
107-
sys, time, x0, integral_cost, constraints, terminal_cost=terminal_cost)
113+
sys, time, x0, integral_cost, trajectory_constraints,
114+
terminal_cost=terminal_cost, initial_guess=lqr_u)
108115

109116
# Make sure we got a different solution
110117
assert np.any(np.abs(res1.inputs - res2.inputs) > 0.1)
@@ -205,7 +212,9 @@ def test_constraint_specification(constraint_list):
205212

206213
# Create a model predictive controller system
207214
time = np.arange(0, 5, 1)
208-
optctrl = opt.OptimalControlProblem(sys, time, cost, constraints)
215+
optctrl = opt.OptimalControlProblem(
216+
sys, time, cost, constraints,
217+
terminal_cost=cost) # include to match MPT3 formulation
209218

210219
# Compute optimal control and compare against MPT3 solution
211220
x0 = [4, 0]
@@ -223,7 +232,7 @@ def test_constraint_specification(constraint_list):
223232
([[1, 0], [0, 1]], np.eye(2), np.eye(2), 0, 1),
224233
id = "discrete, dt=1"),
225234
pytest.param(
226-
(np.zeros((2,2)), np.eye(2), np.eye(2), 0),
235+
(np.zeros((2, 2)), np.eye(2), np.eye(2), 0),
227236
id = "continuous"),
228237
])
229238
def test_terminal_constraints(sys_args):
@@ -274,8 +283,11 @@ def test_terminal_constraints(sys_args):
274283

275284
# Re-run using a basis function and see if we get the same answer
276285
res = opt.solve_ocp(sys, time, x0, cost, terminal_constraints=final_point,
277-
basis=flat.BezierFamily(4, Tf))
278-
np.testing.assert_almost_equal(res.inputs, u1, decimal=2)
286+
basis=flat.BezierFamily(8, Tf))
287+
288+
# Final point doesn't affect cost => don't need to test
289+
np.testing.assert_almost_equal(
290+
res.inputs[:, :-1], u1[:, :-1], decimal=2)
279291

280292
# Impose some cost on the state, which should change the path
281293
Q = np.eye(2)

0 commit comments

Comments
 (0)