Skip to content

Commit 3039293

Browse files
committed
Add error handling for JuMPDynamicOptProblem
When we have more collocation points than expected the constraints are wrong
1 parent a57a464 commit 3039293

File tree

5 files changed

+72
-15
lines changed

5 files changed

+72
-15
lines changed

ext/MTKCasADiDynamicOptExt.jl

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ mutable struct CasADiModel{T}
2828
P::T
2929
tₛ::MX
3030
is_free_final::Bool
31+
tsteps::LinRange{Float64, Int}
3132
solver_opti::Union{Nothing, Opti}
3233

33-
function CasADiModel(opti, U, V, P, tₛ, is_free_final, solver_opti = nothing)
34-
new{typeof(P)}(opti, U, V, P, tₛ, is_free_final, solver_opti)
34+
function CasADiModel(opti, U, V, P, tₛ, is_free_final, tsteps, solver_opti = nothing)
35+
new{typeof(P)}(opti, U, V, P, tₛ, is_free_final, tsteps, solver_opti)
3536
end
3637
end
3738

@@ -160,7 +161,10 @@ function add_solve_constraints!(prob::CasADiDynamicOptProblem, tableau)
160161
solver_opti = copy(model)
161162

162163
tsteps = U.t
163-
dt = tsteps[2] - tsteps[1]
164+
dt = (tsteps[end] - tsteps[1]) / (length(tsteps) - 1)
165+
166+
# CasADi uses linear interpolation for cost evaluations that are not on the collocation points
167+
@assert tsteps == wrapped_model.tsteps "Collocation points mismatch."
164168

165169
nᵤ = size(U.u, 1)
166170
nᵥ = size(V.u, 1)

ext/MTKInfiniteOptExt.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ struct InfiniteOptModel
1717
P::Vector{<:AbstractVariableRef}
1818
tₛ::AbstractVariableRef
1919
is_free_final::Bool
20+
tsteps::LinRange{Float64, Int}
2021
end
2122

2223
struct JuMPDynamicOptProblem{uType, tType, isinplace, P, F, K} <:
@@ -139,6 +140,10 @@ function add_solve_constraints!(prob::JuMPDynamicOptProblem, tableau)
139140
t = model[:t]
140141
tsteps = supports(t)
141142

143+
# InfiniteOpt can introduce additional collocation points
144+
# Make sure that the collocation points are correct.
145+
MTK.check_collocation_time_mismatch(prob.f.sys, wrapped_model.tsteps, tsteps)
146+
142147
dt = (tsteps[end] - tsteps[1]) / (length(tsteps) - 1)
143148

144149
nᵤ = length(U)

ext/MTKPyomoDynamicOptExt.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,17 @@ struct PyomoDynamicOptModel{T}
2626
P::T
2727
tₛ::PyomoVar
2828
is_free_final::Bool
29+
tsteps::LinRange{Float64, Int}
2930
solver_model::Union{Nothing, ConcreteModel}
3031
dU::PyomoVar
3132
model_sym::Union{Num, Symbolics.BasicSymbolic}
3233
t_sym::Union{Num, Symbolics.BasicSymbolic}
3334
dummy_sym::Union{Num, Symbolics.BasicSymbolic}
3435

35-
function PyomoDynamicOptModel(model, U, V, P, tₛ, is_free_final)
36+
function PyomoDynamicOptModel(model, U, V, P, tₛ, is_free_final, tsteps)
3637
@variables MODEL_SYM::Symbolics.symstruct(ConcreteModel) T_SYM DUMMY_SYM
3738
model.dU = dae.DerivativeVar(U, wrt = model.t, initialize = 0)
38-
new{typeof(P)}(model, U, V, P, tₛ, is_free_final, nothing,
39+
new{typeof(P)}(model, U, V, P, tₛ, is_free_final, tsteps, nothing,
3940
PyomoVar(model.dU), MODEL_SYM, T_SYM, DUMMY_SYM)
4041
end
4142
end

src/systems/optimal_control_interface.jl

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,56 @@ function process_tspan(tspan, dt, steps)
222222
end
223223
end
224224

225+
function get_discrete_time_evaluations(expr)
226+
vars = Symbolics.get_variables(expr)
227+
228+
# Group by base variable
229+
result = Dict()
230+
231+
for v in vars
232+
if iscall(v)
233+
args = arguments(v)
234+
if length(args) == 1 && args[1] isa Number
235+
base_var = operation(v)
236+
time_point = args[1]
237+
238+
if !haskey(result, base_var)
239+
result[base_var] = Float64[]
240+
end
241+
push!(result[base_var], time_point)
242+
end
243+
end
244+
end
245+
246+
# Sort and unique the time points
247+
for (var, times) in result
248+
result[var] = sort!(unique!(times))
249+
end
250+
251+
return result
252+
end
253+
254+
function check_collocation_time_mismatch(sys, expected_tsteps, tsteps)
255+
if collect(expected_tsteps)tsteps
256+
eval_times = get_discrete_time_evaluations(cost(sys))
257+
for (var, ts) in eval_times
258+
tdiff = setdiff(ts, expected_tsteps)
259+
@info tdiff
260+
if !isempty(tdiff)
261+
error("$var is evaluated inside the cost function at $(length(tdiff)) points " *
262+
"that are not in the $(length(expected_tsteps)) collocation points. " *
263+
"Cost evaluation points must align with the collocation grid. "*
264+
"Adjust the dt to match the time points used in the cost function.")
265+
end
266+
end
267+
if length(expected_tsteps) length(tsteps)
268+
error("Found extra $(abs(length(expected_tsteps) - length(tsteps))) collocation points.")
269+
elseif maximum(abs.(expected_tsteps .- tsteps)) > 1e-10
270+
error("The collocation points differ from the expected ones by more than 1e-10.")
271+
end
272+
end
273+
end
274+
225275
##########################
226276
### MODEL CONSTRUCTION ###
227277
##########################
@@ -274,7 +324,7 @@ function process_DynamicOptProblem(
274324
P_backend = needs_individual_tunables(model) ? P_syms : P
275325

276326
tₛ = generate_timescale!(model, get(pmap, tspan[2], tspan[2]), is_free_t)
277-
fullmodel = model_type(model, U, V, P_backend, tₛ, is_free_t)
327+
fullmodel = model_type(model, U, V, P_backend, tₛ, is_free_t, tsteps)
278328

279329
merge!(pmap, Dict(tunable_params .=> P_syms))
280330

@@ -427,7 +477,6 @@ function process_integral_bounds end
427477
function lowered_integral end
428478
function lowered_derivative end
429479
function lowered_var end
430-
function lowered_param end
431480
function fixed_t_map end
432481

433482
function add_user_constraints!(model, sys, tspan, pmap)

test/extensions/dynamic_optimization.jl

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -487,10 +487,8 @@ end
487487
# test with different time stepping
488488

489489
jprob = JuMPDynamicOptProblem(sys′, u0map, tspan; dt=1/120, tune_parameters=true)
490-
jsol = solve(jprob, JuMPCollocation(Ipopt.Optimizer, constructTsitouras5()))
491-
492-
@test jsol.sol.ps[δ] 1.8 rtol=1e-4 broken=true
493-
@test jsol.sol.ps[α] 2.5 rtol=1e-4 broken=true
490+
err_msg = "x is evaluated inside the cost function at 40 points that are not in the 121 collocation points."
491+
@test_throws err_msg solve(jprob, JuMPCollocation(Ipopt.Optimizer, constructTsitouras5()))
494492

495493
iprob = InfiniteOptDynamicOptProblem(sys′, u0map, tspan, dt = 1/50, tune_parameters=true)
496494
isol = solve(iprob, InfiniteOptCollocation(Ipopt.Optimizer, InfiniteOpt.OrthogonalCollocation(3)))
@@ -503,8 +501,8 @@ end
503501
iprob = InfiniteOptDynamicOptProblem(sys′, u0map, tspan, dt = 1/120, tune_parameters=true)
504502
isol = solve(iprob, InfiniteOptCollocation(Ipopt.Optimizer, InfiniteOpt.OrthogonalCollocation(3)))
505503

506-
@test isol.sol.ps[δ] 1.8 rtol=1e-3
507-
@test isol.sol.ps[α] 2.5 rtol=1e-3
504+
@test isol.sol.ps[δ] 1.8 rtol=1e-4
505+
@test isol.sol.ps[α] 2.5 rtol=1e-4
508506

509507
if ENABLE_CASADI
510508
cprob = CasADiDynamicOptProblem(sys′, u0map, tspan; dt = 1/50, tune_parameters=true)
@@ -516,8 +514,8 @@ end
516514

517515
cprob = CasADiDynamicOptProblem(sys′, u0map, tspan; dt = 1/120, tune_parameters=true)
518516
csol = solve(cprob, CasADiCollocation("ipopt", constructRK4()))
519-
@test csol.sol.ps[δ] 1.8 rtol=1e-4 broken=false
520-
@test csol.sol.ps[α] 2.5 rtol=1e-4 broken=true
517+
@test csol.sol.ps[δ] 1.8 rtol=1e-4
518+
@test csol.sol.ps[α] 2.5 rtol=1e-3
521519
end
522520

523521
pprob = PyomoDynamicOptProblem(sys′, u0map, tspan, dt = 1/50, tune_parameters=true)

0 commit comments

Comments
 (0)