Skip to content

Inspect a problem

Once a problem is built — via @def or the functional API — every part of it can be read back: dimensions, names, dynamics, costs, constraints, traits. This page is about reading a model, not solving it: for that, see Solve overview; for the indirect/PMP route, see Flows overview.

Signatures and is_* aliases

Every accessor on this page is listed with its full signature and return type in the Problem API reference. Many predicates also have an equivalent is_* alias — has_control / is_control_free, has_variable / is_variable / is_nonvariable, is_autonomous / is_nonautonomous, has_fixed_final_time / is_final_time_fixed (and the three other time variants), has_mayer_cost / is_mayer_cost_defined, has_lagrange_cost / is_lagrange_cost_defined, has_abstract_definition / is_abstractly_defined. Only the has_* / is_autonomous forms are shown below.

julia
using OptimalControl

A model prints as a readable summary:

julia
ocp = @def begin
    t  [0, 1], time
    x = (q, v)  R², state
    u  R, control
    x(0) == [-1, 0]
    x(1) == [0, 0]
(t)  == [v(t), u(t)]
    0.5∫( u(t)^2 )  min
end
ocp
Abstract definition:

    t ∈ [0, 1], time
    x = ((q, v) ∈ R², state)
    u ∈ R, control
    x(0) == [-1, 0]
    x(1) == [0, 0]
    ẋ(t) == [v(t), u(t)]
    0.5 * ∫(u(t) ^ 2) → min

The (autonomous) optimal control problem is of the form:

    minimize  J(x, u) = ∫ f⁰(x(t), u(t)) dt, over [0, 1]

    subject to

        ẋ(t) = f(x(t), u(t)), t in [0, 1] a.e.,

        ϕ₋ ≤ ϕ(x(0), x(1)) ≤ ϕ₊, 

    where x(t) = (q(t), v(t)) ∈ R² and u(t) ∈ R.

To illustrate the accessors below, we use a richer problem: free final time, a variable, and several kinds of constraints.

julia
ocp = @def begin
    v = (w, tf)  R²,   variable
    s  [0, tf],        time
    q = (x, y)  R²,    state
    u  R,              control
    0 tf  2,         (1)
    u(s)  0,           (cons_u)
    x(s) + u(s)  10,   (cons_mixed)
    w == 0
    x(0) == -1
    y(0) - tf == 0,     (cons_bound)
    q(tf) == [0, 0]
(s) == [y(s)+w, u(s)]
    0.5∫( u(s)^2 )  min
end

Times

Times model

julia
times(ocp)  # the TimesModel struct
TimesModel
├─ initial: FixedTimeModel(name=0, time=0)
├─ final: FreeTimeModel(name=tf, index=2)
└─ time_name: s

Initial and final times separately. The final time needs the variable value when it is free:

julia
julia> initial_time(ocp)
0

julia> final_time(ocp, [1, 2])   # w = 1, tf = 2
2

Asking for a free final time without the variable errors:

julia> final_time(ocp)
ERROR: PreconditionError

  Cannot get final time with this function

  Reason   This model type does not support direct final time access

  Context  final_time on AbstractModel
  Hint     Use final_time(ocp) on a Model with FixedTimeModel or use final_time(ocp, variable) for variable final time
└─

Time variable names

julia
julia> time_name(ocp)           # "s"
"s"

julia> initial_time_name(ocp)   # "0" — fixed
"0"

julia> final_time_name(ocp)     # "tf" — a variable
"tf"

Time fixedness predicates

julia
julia> has_fixed_initial_time(ocp)
true

julia> has_free_initial_time(ocp)
false

julia> has_fixed_final_time(ocp)
false

julia> has_free_final_time(ocp)
true

Autonomy

julia
is_autonomous(ocp)  # false if dynamics or Lagrange cost depend on t
true

See Time dependence below.

State

State component information

julia
julia> state_name(ocp)
"q"

julia> state_dimension(ocp)
2

julia> state_components(ocp)
2-element Vector{String}:
 "x"
 "y"

Note

The component names are used when plotting the solution. See Plot.

State box constraints

julia
state_constraints_box(ocp)
(Float64[], Int64[], Float64[], Symbol[], Vector{Symbol}[])

Tuple structures

Box-constraint accessors (state_constraints_box, control_constraints_box, variable_constraints_box) return (lb, indices, ub, labels, aliases):

  • lb, ub — vectors of lower / upper bounds

  • indices — component indices (1-based)

  • labels — constraint labels

  • aliases — for each component, every label that declared it

The nonlinear-constraint accessors (path_constraints_nl, boundary_constraints_nl) return (lb, f!, ub, labels) instead, with f! in-place: f!(val, t, x, u, v) for path constraints, f!(val, x0, xf, v) for boundary constraints.

julia
dim_state_constraints_box(ocp)
0

Control

Control component information

julia
julia> control_name(ocp)
"u"

julia> control_dimension(ocp)
1

julia> control_components(ocp)
1-element Vector{String}:
 "u"

Control box constraints

julia
julia> control_constraints_box(ocp)
([0.0], [1], [Inf], [:cons_u], [[:cons_u]])

julia> dim_control_constraints_box(ocp)
1

Control presence

julia
has_control(ocp)  # true if the problem has a control input
true

is_control_free(ocp) ≡ !has_control(ocp) — see No control.

Variable

Variable component information

julia
julia> variable_name(ocp)
"v"

julia> variable_dimension(ocp)
2

julia> variable_components(ocp)
2-element Vector{String}:
 "w"
 "tf"

Variable box constraints

julia
julia> variable_constraints_box(ocp)
([0.0, 0.0], [1, 2], [0.0, 2.0], [Symbol("label##12566"), :eq1], [[Symbol("label##12566")], [:eq1]])

julia> dim_variable_constraints_box(ocp)
2

Variable presence

julia
has_variable(ocp)  # true if the problem has optimisation variables
true

Dynamics

The dynamics are stored as an in-place function f!(dx, t, x, u, v)dx is mutated, the other arguments are time, state, control, variable:

julia
f! = dynamics(ocp)
s = 0.5; q = [0.0, 1.0]; u = 2.0; v = [1.0, 2.0]
dq = similar(q)
f!(dq, s, q, u, v)
dq  # the state derivative q̇
2-element Vector{Float64}:
 2.0
 2.0

Objective

julia
criterion(ocp)  # :min or :max
:min

The objective is in Mayer form , Lagrange form  , or Bolza form (both):

julia
julia> has_mayer_cost(ocp)
false

julia> has_lagrange_cost(ocp)
true

Get the cost functions — mayer has signature g(x0, xf, v) and errors when there is no Mayer cost; lagrange has signature f⁰(t, x, u, v):

julia> mayer(ocp)
ERROR: PreconditionError

  Cannot access Mayer cost

  Reason   This OCP has no Mayer objective defined

  Context  mayer accessor
  Hint     Define a Mayer objective using objective!(ocp, :min/:max, mayer=...) before accessing it
└─
julia
f⁰ = lagrange(ocp)
f⁰(0.5, [0.0, 1.0], 2.0, [1.0, 2.0])  # the integrand value
2.0

Constraints

Individual constraints

constraint(ocp, label) returns (type, f, lb, ub). The function signature is f(x0, xf, v) for :boundary and :variable constraints, f(t, x, u, v) for :control, :state and :mixed ones:

julia
x0 = [0, 1]; xf = [2, 3]; v = [1, 4]
s = 0.5; q = [1.0, 2.0]; u = 3.0

(type, f, lb, ub) = constraint(ocp, :eq1)
(type, f(x0, xf, v), lb, ub)
(:variable, 4, 0.0, 2.0)
julia
(type, f, lb, ub) = constraint(ocp, :cons_bound)   # a boundary constraint
(type, f(x0, xf, v))
(:boundary, -3.0)
julia
(type, f, lb, ub) = constraint(ocp, :cons_u)       # a control constraint
(type, f(s, q, u, v))
(:control, 3.0)
julia
(type, f, lb, ub) = constraint(ocp, :cons_mixed)  # a mixed path constraint
(type, f(s, q, u, v))
(:path, 4.0)

All constraints, and nonlinear ones

julia
constraints(ocp)
ConstraintsModel
├─ path nonlinear: 4
├─ boundary nonlinear: 4
├─ state box: 5
├─ control box: 5
└─ variable box: 5
julia> path_constraints_nl(ocp)
([-Inf], Main.var"#fun##12562#97"(), [10.0], [:cons_mixed])

julia> boundary_constraints_nl(ocp)
([-1.0, 0.0, 0.0, 0.0], CompositeConstraint{:boundary}(n=3, dims=[1, 1, 2]), [-1.0, 0.0, 0.0, 0.0], [Symbol("label##12568"), :cons_bound, Symbol("label##12579"), Symbol("label##12579")])

julia> dim_path_constraints_nl(ocp)
1

julia> dim_boundary_constraints_nl(ocp)
4

Note

To get the dual variable (Lagrange multiplier) of a constraint, use dual on a solution — see Solution object.

Problem definition

julia
definition(ocp)  # the OCP definition, an AbstractDefinition
Abstract definition:

    v = ((w, tf) ∈ R², variable)
    s ∈ [0, tf], time
    q = ((x, y) ∈ R², state)
    u ∈ R, control
    0 ≤ tf ≤ 2, 1
    u(s) ≥ 0, cons_u
    x(s) + u(s) ≤ 10, cons_mixed
    w == 0
    x(0) == -1
    y(0) - tf == 0, cons_bound
    q(tf) == [0, 0]
    q̇(s) == [y(s) + w, u(s)]
    0.5 * ∫(u(s) ^ 2) → min
julia
expr = expression(ocp)  # the Expr from the definition
julia
has_abstract_definition(ocp)  # false for a functional-API model
true

Time dependence

A problem is autonomous when neither the dynamics nor the Lagrange cost depends explicitly on the time variable, non-autonomous otherwise.

julia
ocp = @def begin
    t  [ 0, 1 ], time
    x  R, state
    u  R, control
(t)  == u(t)
    x(1) + 0.5∫( u(t)^2 )  min
end
is_autonomous(ocp)
true

Adding an explicit t to the dynamics (or to the Lagrange integrand) makes it non-autonomous:

julia
ocp = @def begin
    t  [ 0, 1 ], time
    x  R, state
    u  R, control
(t)  == u(t) + t                  # explicit dependence on t
    x(1) + 0.5∫( u(t)^2 )  min
end
is_autonomous(ocp)
false
julia
is_nonautonomous(ocp)  # the negation of is_autonomous
true

API trap: time is gone

time(ocp) is not the time accessor — time is Base.time (wall-clock time), extended but not exported. The accessor is times(ocp), shown above. Calling time(ocp) throws a migration error pointing here — see Migrating to v2.1.

See also