Skip to content

Commit e080243

Browse files
committed
fix: make free final time problems with constraints work
1 parent 56caf18 commit e080243

File tree

3 files changed

+68
-21
lines changed

3 files changed

+68
-21
lines changed

ext/MTKJuMPDynamicOptExt.jl

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ end
4242
Convert an ODESystem representing an optimal control system into a JuMP model
4343
for solving using optimization. Must provide either `dt`, the timestep between collocation
4444
points (which, along with the timespan, determines the number of points), or directly
45-
provide the number of points as `nsteps`.
45+
provide the number of points as `steps`.
4646
4747
The optimization variables:
4848
- a vector-of-vectors U representing the unknowns as an interpolation array
@@ -61,7 +61,7 @@ function MTK.JuMPDynamicOptProblem(sys::ODESystem, u0map, tspan, pmap;
6161
f, u0, p = MTK.process_SciMLProblem(ODEInputFunction, sys, _u0map, pmap;
6262
t = tspan !== nothing ? tspan[1] : tspan, kwargs...)
6363

64-
pmap = MTK.todict(pmap)
64+
pmap = Dict{Any, Any}(pmap)
6565
steps, is_free_t = MTK.process_tspan(tspan, dt, steps)
6666
model = init_model(sys, tspan, steps, u0map, pmap, u0; is_free_t)
6767

@@ -87,7 +87,7 @@ function MTK.InfiniteOptDynamicOptProblem(sys::ODESystem, u0map, tspan, pmap;
8787
f, u0, p = MTK.process_SciMLProblem(ODEInputFunction, sys, _u0map, pmap;
8888
t = tspan !== nothing ? tspan[1] : tspan, kwargs...)
8989

90-
pmap = MTK.todict(pmap)
90+
pmap = Dict{Any, Any}(pmap)
9191
steps, is_free_t = MTK.process_tspan(tspan, dt, steps)
9292
model = init_model(sys, tspan, steps, u0map, pmap, u0; is_free_t)
9393

@@ -103,21 +103,25 @@ function init_model(sys, tspan, steps, u0map, pmap, u0; is_free_t = false)
103103

104104
if is_free_t
105105
(ts_sym, te_sym) = tspan
106+
MTK.symbolic_type(ts_sym) !== MTK.NotSymbolic() && error("Free initial time problems are not currently supported.")
106107
@variable(model, tf, start=pmap[te_sym])
108+
set_lower_bound(tf, ts_sym)
107109
hasbounds(te_sym) && begin
108110
lo, hi = getbounds(te_sym)
109111
set_lower_bound(tf, lo)
110112
set_upper_bound(tf, hi)
111113
end
112-
pmap[ts_sym] = 0
113-
pmap[te_sym] = 1
114+
pmap[te_sym] = model[:tf]
114115
tspan = (0, 1)
115116
end
116117

117118
@infinite_parameter(model, t in [tspan[1], tspan[2]], num_supports=steps)
118119
@variable(model, U[i = 1:length(states)], Infinite(t), start=u0[i])
119120
c0 = MTK.value.([pmap[c] for c in ctrls])
120121
@variable(model, V[i = 1:length(ctrls)], Infinite(t), start=c0[i])
122+
for (i, ct) in enumerate(ctrls)
123+
pmap[ct] = model[:V][i]
124+
end
121125

122126
set_jump_bounds!(model, sys, pmap)
123127
add_jump_cost_function!(model, sys, (tspan[1], tspan[2]), pmap; is_free_t)
@@ -131,6 +135,13 @@ function init_model(sys, tspan, steps, u0map, pmap, u0; is_free_t = false)
131135
return model
132136
end
133137

138+
"""
139+
Modify the pmap by replacing controls with V[i](t), and tf with the model's final time variable for free final time problems.
140+
"""
141+
function modified_pmap(model, sys, pmap)
142+
pmap = Dict{Any, Any}(pmap)
143+
end
144+
134145
function set_jump_bounds!(model, sys, pmap)
135146
U = model[:U]
136147
for (i, u) in enumerate(unknowns(sys))
@@ -158,7 +169,7 @@ function add_jump_cost_function!(model::InfiniteModel, sys, tspan, pmap; is_free
158169
@objective(model, Min, 0)
159170
return
160171
end
161-
jcosts = substitute_jump_vars(model, sys, pmap, jcosts)
172+
jcosts = substitute_jump_vars(model, sys, pmap, jcosts; is_free_t)
162173
tₛ = is_free_t ? model[:tf] : 1
163174

164175
# Substitute integral
@@ -186,13 +197,14 @@ function add_user_constraints!(model::InfiniteModel, sys, pmap; is_free_t = fals
186197
for u in MTK.get_unknowns(conssys)
187198
x = MTK.operation(u)
188199
t = only(arguments(u))
189-
MTK.symbolic_type(t) === NotSymbolic() &&
190-
error("Provided specific time constraint in a free final time problem. This is not supported by the JuMP/InfiniteOpt collocation solvers. The offending variable is $u.")
200+
if (MTK.symbolic_type(t) === MTK.NotSymbolic())
201+
error("Provided specific time constraint in a free final time problem. This is not supported by the JuMP/InfiniteOpt collocation solvers. The offending variable is $u. Specific-time constraints can only be specified at the beginning or end of the timespan.")
202+
end
191203
end
192204
end
193205

194206
auxmap = Dict([u => MTK.default_toterm(MTK.value(u)) for u in unknowns(conssys)])
195-
jconstraints = substitute_jump_vars(model, sys, pmap, jconstraints; auxmap)
207+
jconstraints = substitute_jump_vars(model, sys, pmap, jconstraints; auxmap, is_free_t)
196208

197209
# Substitute to-term'd variables
198210
for (i, cons) in enumerate(jconstraints)
@@ -211,28 +223,33 @@ function add_initial_constraints!(model::InfiniteModel, u0, u0_idxs, ts)
211223
@constraint(model, initial[i in u0_idxs], U[i](ts)==u0[i])
212224
end
213225

214-
function substitute_jump_vars(model, sys, pmap, exprs; auxmap = Dict())
226+
function substitute_jump_vars(model, sys, pmap, exprs; auxmap = Dict(), is_free_t = false)
215227
iv = MTK.get_iv(sys)
216228
sts = unknowns(sys)
217229
cts = MTK.unbound_inputs(sys)
218230
U = model[:U]
219231
V = model[:V]
232+
x_ops = [MTK.operation(MTK.unwrap(st)) for st in sts]
233+
c_ops = [MTK.operation(MTK.unwrap(ct)) for ct in cts]
234+
220235
exprs = map(c -> Symbolics.fixpoint_sub(c, auxmap), exprs)
236+
exprs = map(c -> Symbolics.fixpoint_sub(c, Dict(pmap)), exprs)
237+
if is_free_t
238+
tf = model[:tf]
239+
free_t_map = Dict([[x(tf) => U[i](1) for (i, x) in enumerate(x_ops)]; [c(tf) => V[i](1) for (i, c) in enumerate(c_ops)]])
240+
exprs = map(c -> Symbolics.fixpoint_sub(c, free_t_map), exprs)
241+
end
221242

222243
# for variables like x(t)
223244
whole_interval_map = Dict([[v => U[i] for (i, v) in enumerate(sts)];
224245
[v => V[i] for (i, v) in enumerate(cts)]])
225246
exprs = map(c -> Symbolics.fixpoint_sub(c, whole_interval_map), exprs)
226247

227248
# for variables like x(1.0)
228-
x_ops = [MTK.operation(MTK.unwrap(st)) for st in sts]
229-
c_ops = [MTK.operation(MTK.unwrap(ct)) for ct in cts]
230249
fixed_t_map = Dict([[x_ops[i] => U[i] for i in 1:length(U)];
231250
[c_ops[i] => V[i] for i in 1:length(V)]])
232251

233252
exprs = map(c -> Symbolics.fixpoint_sub(c, fixed_t_map), exprs)
234-
235-
exprs = map(c -> Symbolics.fixpoint_sub(c, Dict(pmap)), exprs)
236253
exprs
237254
end
238255

@@ -246,8 +263,12 @@ function add_infopt_solve_constraints!(model::InfiniteModel, sys, pmap; is_free_
246263
diffsubmap = Dict([D(U[i]) => (U[i], t) for i in 1:length(U)])
247264
tₛ = is_free_t ? model[:tf] : 1
248265

266+
@show diff_equations(sys)
267+
@show pmap
249268
diff_eqs = substitute_jump_vars(model, sys, pmap, diff_equations(sys))
269+
@show diff_eqs
250270
diff_eqs = map(e -> Symbolics.substitute(e, diffsubmap), diff_eqs)
271+
@show diff_eqs
251272
@constraint(model, D[i = 1:length(diff_eqs)], diff_eqs[i].lhs==tₛ * diff_eqs[i].rhs)
252273

253274
# Algebraic equations

src/systems/optimal_control_interface.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ function process_tspan(tspan, dt, steps)
173173
elseif symbolic_type(tspan[1]) === ScalarSymbolic() ||
174174
symbolic_type(tspan[2]) === ScalarSymbolic()
175175
isnothing(steps) &&
176-
error("Free final time problems require specifying the number of steps, rather than dt.")
176+
error("Free final time problems require specifying the number of steps using the keyword arg `steps`, rather than dt.")
177177
isnothing(dt) ||
178178
@warn "Specified dt for free final time problem. This will be ignored; dt will be determined by the number of timesteps."
179179

test/downstream/jump_control.jl

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,22 +178,22 @@ end
178178

179179
(ts, te) = (0.0, 0.2)
180180
costs = [-h(te)]
181-
cons = [T(te) ~ 0]
181+
cons = [T(te) ~ 0, m(te) ~ m_c]
182182
@named rocket = ODESystem(eqs, t; costs, constraints = cons)
183183
rocket, input_idxs = structural_simplify(rocket, ([T(t)], []))
184184

185185
u0map = [h(t) => h₀, m(t) => m₀, v(t) => 0]
186186
pmap = [
187187
g₀ => 1, m₀ => 1.0, h_c => 500, c => 0.5 * (g₀ * h₀), D_c => 0.5 * 620 * m₀ / g₀,
188188
Tₘ => 3.5 * g₀ * m₀, T(t) => 0.0, h₀ => 1, m_c => 0.6]
189-
jprob = JuMPDynamicOptProblem(rocket, u0map, (ts, te), pmap; dt = 0.005, cse = false)
189+
jprob = JuMPDynamicOptProblem(rocket, u0map, (ts, te), pmap; dt = 0.001, cse = false)
190190
jsol = solve(jprob, Ipopt.Optimizer, :RadauIIA5, silent = true)
191191
@test jsol.sol.u[end][1] > 1.012
192192

193193
iprob = InfiniteOptDynamicOptProblem(
194-
rocket, u0map, (ts, te), pmap; dt = 0.005, cse = false)
194+
rocket, u0map, (ts, te), pmap; dt = 0.001, cse = false)
195195
isol = solve(iprob, Ipopt.Optimizer,
196-
derivative_method = InfiniteOpt.OrthogonalCollocation(3), silent = true)
196+
derivative_method = InfiniteOpt.OrthogonalCollocation(3), silent = true)
197197
@test isol.sol.u[end][1] > 1.012
198198

199199
# Test solution
@@ -204,11 +204,16 @@ end
204204
@mtkbuild rocket_ode = ODESystem(eqs, t)
205205
interpmap = Dict(T_interp => ctrl_to_spline(jsol.input_sol, CubicSpline))
206206
oprob = ODEProblem(rocket_ode, u0map, (ts, te), merge(Dict(pmap), interpmap))
207-
osol = solve(oprob, RadauIIA5(); adaptive = false, dt = 0.005)
207+
osol = solve(oprob, RadauIIA5(); adaptive = false, dt = 0.001)
208208
@test (jsol.sol.u, osol.u, rtol = 0.02)
209+
210+
interpmap1 = Dict(T_interp => ctrl_to_spline(isol.input_sol, CubicSpline))
211+
oprob1 = ODEProblem(rocket_ode, u0map, (ts, te), merge(Dict(pmap), interpmap1))
212+
osol1 = solve(oprob1, RadauIIA5(); adaptive = false, dt = 0.001)
213+
@test (isol.sol.u, osol1.u, rtol = 0.02)
209214
end
210215

211-
@testset "Free final time problem" begin
216+
@testset "Free final time problems" begin
212217
t = M.t_nounits
213218
D = M.D_nounits
214219

@@ -230,6 +235,27 @@ end
230235
iprob = InfiniteOptDynamicOptProblem(rocket, u0map, (0, tf), pmap; steps = 200)
231236
isol = solve(iprob, Ipopt.Optimizer)
232237
@test isapprox(isol.sol.t[end], 10.0, rtol = 1e-3)
238+
239+
@variables x(..) v(..)
240+
@variables u(..) [bounds = (-1.0, 1.0), input = true]
241+
@parameters tf
242+
constr = [v(tf) ~ 0, x(tf) ~ 0]
243+
cost = [tf] # Minimize time
244+
245+
@named block = ODESystem(
246+
[D(x(t)) ~ v(t), D(v(t)) ~ u(t)], t; costs = cost, constraints = constr)
247+
block, input_idxs = structural_simplify(block, ([u(t)], []))
248+
249+
u0map = [x(t) => 1.0, v(t) => 0.0]
250+
tspan = (0.0, tf)
251+
parammap = [u(t) => 0.0, tf => 1.0]
252+
jprob = JuMPDynamicOptProblem(block, u0map, tspan, parammap; steps = 51)
253+
jsol = solve(jprob, Ipopt.Optimizer, :Verner8, silent = true)
254+
@test isapprox(jsol.sol.t[end], 2.0, atol = 1e-5)
255+
256+
iprob = InfiniteOptDynamicOptProblem(block, u0map, tspan, parammap; steps = 51)
257+
isol = solve(iprob, Ipopt.Optimizer, silent = true)
258+
@test isapprox(isol.sol.t[end], 2.0, atol = 1e-5)
233259
end
234260

235261
@testset "Cart-pole problem" begin

0 commit comments

Comments
 (0)