The optimal control problem object: structure and usage

In this manual, we'll first recall the main functionalities you can use when working with an optimal control problem (OCP). This includes essential operations like:

  • Solving an OCP: How to find the optimal solution for your defined problem.
  • Computing flows from an OCP: Understanding the dynamics and trajectories derived from the optimal solution.
  • Printing an OCP: How to display a summary of your problem's definition.

After covering these core functionalities, we'll delve into the structure of an OCP. Since an OCP is structured as a OptimalControl.Model struct, we'll first explain how to access its underlying attributes, such as the problem's dynamics, costs, and constraints. Following this, we'll shift our focus to the simple properties inherent to an OCP, learning how to determine aspects like whether the problem:

  • Is autonomous: Does its dynamics depend explicitly on time?
  • Has a fixed or free initial/final time: Is the duration of the control problem predetermined or not?

Content


Main functionalities

Let's define a basic optimal control problem.

using OptimalControl

t0 = 0
tf = 1
x0 = [-1, 0]

ocp = @def begin
    t ∈ [ t0, tf ], time
    x = (q, v) ∈ R², state
    u ∈ R, control
    x(t0) == x0
    x(tf) == [0, 0]
    ẋ(t)  == [v(t), u(t)]
    0.5∫( u(t)^2 ) → min
end
nothing # hide
<< @example-block not executed in draft mode >>

To print it, simply:

ocp
<< @example-block not executed in draft mode >>

We can now solve the problem (for more details, visit the solve manual):

using NLPModelsIpopt
solve(ocp)
nothing # hide
<< @example-block not executed in draft mode >>

You can also compute flows (for more details, see the flow manual) from the optimal control problem, providing a control law in feedback form. The pseudo-Hamiltonian of this problem is

\[ H(x, p, u) = p_q\, v + p_v\, u + p^0 \frac{u^2}{2},\]

where $p^0 = -1$ since we are in the normal case. From the Pontryagin maximum principle, the maximising control is given in feedback form by

\[u(x, p) = p_v\]

since $\partial^2_{uu} H = p^0 = - 1 < 0$.

u = (x, p) -> p[2]          # control law in feedback form

using OrdinaryDiffEq        # needed to import numerical integrators
f = Flow(ocp, u)            # compute the Hamiltonian flow function

p0 = [12, 6]                # initial covector solution
xf, pf = f(t0, x0, p0, tf)  # flow from (x0, p0) at time t0 to tf
xf                          # should be (0, 0)
<< @example-block not executed in draft mode >>
Note

A more advanced feature allows for the discretization of the optimal control problem. From the discretized version, you can obtain a Nonlinear Programming problem (or optimization problem) and solve it using any appropriate NLP solver. For more details, visit the NLP manipulation tutorial.

Model struct

The optimal control problem ocp is a OptimalControl.Model struct.

CTModels.OCP.ModelType
struct Model{TD<:CTModels.OCP.TimeDependence, TimesModelType<:CTModels.OCP.AbstractTimesModel, StateModelType<:CTModels.OCP.AbstractStateModel, ControlModelType<:CTModels.OCP.AbstractControlModel, VariableModelType<:CTModels.OCP.AbstractVariableModel, DynamicsModelType<:Function, ObjectiveModelType<:CTModels.OCP.AbstractObjectiveModel, ConstraintsModelType<:CTModels.OCP.AbstractConstraintsModel, DefinitionType<:CTModels.OCP.AbstractDefinition, BuildExaModelType<:Union{Nothing, Function}} <: CTModels.OCP.AbstractModel

Immutable optimal control problem model containing all problem components.

A Model is created from a PreModel once all required fields have been set. It is parameterised by the time dependence type (Autonomous or NonAutonomous) and the types of all its components.

Fields

  • times::TimesModelType: Initial and final time specification.
  • state::StateModelType: State variable structure (name, components).
  • control::ControlModelType: Control variable structure (name, components).
  • variable::VariableModelType: Optimisation variable structure (may be empty).
  • dynamics::DynamicsModelType: System dynamics function (t, x, u, v) -> ẋ.
  • objective::ObjectiveModelType: Cost functional (Mayer, Lagrange, or Bolza).
  • constraints::ConstraintsModelType: All problem constraints. Box constraints satisfy the per-component uniqueness invariant: each component appears at most once in the stored tuples, bounds are the intersection of all declared bounds, and every declared label is preserved in the aliases field of the box tuples (see ConstraintsModel).
  • definition::DefinitionType: Original symbolic definition of the problem, stored as a subtype of AbstractDefinition (Definition when set, EmptyDefinition otherwise).
  • build_examodel::BuildExaModelType: Optional ExaModels builder function.

Example

julia> using CTModels

julia> # Models are typically created via the @def macro or PreModel
julia> ocp = CTModels.Model  # Type reference

Each field can be accessed directly (ocp.times, etc) or by a getter:

For instance, we can retrieve the times and definition values.

times(ocp)  # returns the TimesModel struct containing time information
<< @example-block not executed in draft mode >>
definition(ocp)
<< @example-block not executed in draft mode >>
Note

We refer to the CTModels documentation for more details about this struct and its fields.

To illustrate the various methods in the sections below, we define a more complex optimal control problem with free final time, variables, and various types of constraints:

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]
    q̇(s) == [y(s)+w, u(s)]
    0.5∫( u(s)^2 ) → min
end
nothing # hide
<< @example-block not executed in draft mode >>

Times

The time component defines the temporal domain of the optimal control problem.

Times model

Get the times model:

times(ocp)  # returns the TimesModel struct containing time information
<< @example-block not executed in draft mode >>

You can also access initial and final times separately:

initial_time(ocp)  # returns the initial time value
<< @example-block not executed in draft mode >>

For the final time, if it is free (part of the variable), you need to provide the variable value:

v = [1, 2]  # example variable values: w=1, tf=2
final_time(ocp, v)  # returns tf value from variable
<< @example-block not executed in draft mode >>

If you try to get the final time without providing the variable when it's free, an error occurs:

final_time(ocp)  # error: tf is free, need variable
<< @repl-block not executed in draft mode >>

Time variable names

Get the names of the time variable and time bounds:

time_name(ocp)  # returns "s" (the time variable name in this OCP)
<< @example-block not executed in draft mode >>
initial_time_name(ocp)  # returns "0" (initial time is fixed at 0)
<< @example-block not executed in draft mode >>
final_time_name(ocp)  # returns "tf" (final time is a variable)
<< @example-block not executed in draft mode >>

Time fixedness predicates

Check whether initial or final times are fixed or free:

has_fixed_initial_time(ocp)  # true if t0 is fixed
<< @example-block not executed in draft mode >>
has_free_initial_time(ocp)  # true if t0 is free (part of variable)
<< @example-block not executed in draft mode >>
Variant methods

Alternative methods with is_* prefix are also available and equivalent:

  • is_initial_time_fixed(ocp)has_fixed_initial_time(ocp)
  • is_initial_time_free(ocp)has_free_initial_time(ocp)
  • is_final_time_fixed(ocp)has_fixed_final_time(ocp)
  • is_final_time_free(ocp)has_free_final_time(ocp)

Similarly for final time:

has_fixed_final_time(ocp)  # false (tf is free in this OCP)
<< @example-block not executed in draft mode >>
has_free_final_time(ocp)  # true (tf is part of variable v)
<< @example-block not executed in draft mode >>

Autonomy

Check if the dynamics and Lagrange cost are autonomous (time-independent):

is_autonomous(ocp)  # false if dynamics or cost depend on time
<< @example-block not executed in draft mode >>

For more details on autonomy, see the Time dependence section below.

Summary table

MethodReturnsDescription
times(ocp)(Float64, Any)Time interval (t0, tf) or (t0, tf_name)
initial_time(ocp)Float64Initial time t0
final_time(ocp)Float64Final time tf (error if free)
final_time(ocp, v)Float64Final time tf from variable v
time_name(ocp)StringTime variable name
initial_time_name(ocp)StringInitial time name or value
final_time_name(ocp)StringFinal time name or value
has_fixed_initial_time(ocp)BoolTrue if t0 is fixed
has_free_initial_time(ocp)BoolTrue if t0 is free
has_fixed_final_time(ocp)BoolTrue if tf is fixed
has_free_final_time(ocp)BoolTrue if tf is free
is_autonomous(ocp)BoolTrue if time-independent

State

The state component represents the state variables of the optimal control problem.

State component information

Get the name, dimension, and component names of the state:

state_name(ocp)  # returns "q" (the state variable name)
<< @example-block not executed in draft mode >>
state_dimension(ocp)  # returns 2 (dimension of state)
<< @example-block not executed in draft mode >>
state_components(ocp)  # returns ["x", "y"] (component names)
<< @example-block not executed in draft mode >>
Note

The component names are used when plotting the solution. See the plot manual.

State box constraints

Get the box constraints on the state (lower and upper bounds):

state_constraints_box(ocp)  # returns box constraints if any
<< @example-block not executed in draft mode >>
Tuple structure

The returned tuple has the structure (lb, indices, ub, labels, aliases) where:

  • lb: vector of lower bounds
  • indices: vector of component indices (1-based)
  • ub: vector of upper bounds
  • labels: vector of constraint labels
  • aliases: vector of vectors containing all labels that declared each component

Get the dimension of state box constraints:

dim_state_constraints_box(ocp)  # returns number of box constraints on state
<< @example-block not executed in draft mode >>

Summary table

MethodReturnsDescription
state_name(ocp)StringState variable name
state_dimension(ocp)IntState dimension
state_components(ocp)Vector{String}State component names
state_constraints_box(ocp)Box constraintsState box constraints
dim_state_constraints_box(ocp)IntNumber of state box constraints

Control

The control component represents the control variables of the optimal control problem.

Control component information

Get the name, dimension, and component names of the control:

control_name(ocp)  # returns "u" (the control variable name)
<< @example-block not executed in draft mode >>
control_dimension(ocp)  # returns 1 (dimension of control)
<< @example-block not executed in draft mode >>
control_components(ocp)  # returns ["u"] (component names)
<< @example-block not executed in draft mode >>

Control box constraints

Get the box constraints on the control:

control_constraints_box(ocp)  # returns box constraints if any
<< @example-block not executed in draft mode >>
Tuple structure

The returned tuple has the structure (lb, indices, ub, labels, aliases) where:

  • lb: vector of lower bounds
  • indices: vector of component indices (1-based)
  • ub: vector of upper bounds
  • labels: vector of constraint labels
  • aliases: vector of vectors containing all labels that declared each component

Get the dimension of control box constraints:

dim_control_constraints_box(ocp)  # returns number of box constraints on control
<< @example-block not executed in draft mode >>

Control presence

Check whether the problem has a control input:

has_control(ocp)  # true if problem has a control input
<< @example-block not executed in draft mode >>
Variant method
  • is_control_free(ocp)!has_control(ocp)

Summary table

MethodReturnsDescription
control_name(ocp)StringControl variable name
control_dimension(ocp)IntControl dimension
control_components(ocp)Vector{String}Control component names
control_constraints_box(ocp)Box constraintsControl box constraints
dim_control_constraints_box(ocp)IntNumber of control box constraints
has_control(ocp)BoolTrue if problem has a control input
is_control_free(ocp)BoolTrue if problem has no control (≡ !has_control)

Variable

The variable component represents the optimization variables (parameters) of the optimal control problem.

Variable component information

Get the name, dimension, and component names of the variable:

variable_name(ocp)  # returns "v" (the variable name)
<< @example-block not executed in draft mode >>
variable_dimension(ocp)  # returns 2 (dimension of variable)
<< @example-block not executed in draft mode >>
variable_components(ocp)  # returns ["w", "tf"] (component names)
<< @example-block not executed in draft mode >>

Variable box constraints

Get the box constraints on the variable:

variable_constraints_box(ocp)  # returns box constraints if any
<< @example-block not executed in draft mode >>
Tuple structure

The returned tuple has the structure (lb, indices, ub, labels, aliases) where:

  • lb: vector of lower bounds
  • indices: vector of component indices (1-based)
  • ub: vector of upper bounds
  • labels: vector of constraint labels
  • aliases: vector of vectors containing all labels that declared each component

Get the dimension of variable box constraints:

dim_variable_constraints_box(ocp)  # returns number of box constraints on variable
<< @example-block not executed in draft mode >>

Variable presence

Check whether the problem has optimization variables:

has_variable(ocp)  # true if problem has optimization variables
<< @example-block not executed in draft mode >>
Variant methods
  • is_variable(ocp)has_variable(ocp)
  • is_nonvariable(ocp)!has_variable(ocp)

Summary table

MethodReturnsDescription
variable_name(ocp)StringVariable name
variable_dimension(ocp)IntVariable dimension
variable_components(ocp)Vector{String}Variable component names
variable_constraints_box(ocp)Box constraintsVariable box constraints
dim_variable_constraints_box(ocp)IntNumber of variable box constraints
has_variable(ocp)BoolTrue if problem has optimization variables
is_variable(ocp)BoolAlias for has_variable
is_nonvariable(ocp)BoolTrue if problem has no variables (≡ !has_variable)

Dynamics

The dynamics component defines the differential equations governing the state evolution.

Dynamics function

The dynamics are stored as an in-place function of the form f!(dx, t, x, u, v):

f! = dynamics(ocp)
s = 0.5  # time
q = [0.0, 1.0]  # state
u = 2.0  # control
v = [1.0, 2.0]  # variable
dq = similar(q)
f!(dq, s, q, u, v)
dq  # returns the derivative q̇
<< @example-block not executed in draft mode >>

The first argument dx is mutated upon call and contains the state derivative. The other arguments are:

  • t: time
  • x: state
  • u: control
  • v: variable

Summary table

MethodReturnsDescription
dynamics(ocp)FunctionIn-place dynamics function f!(dx, t, x, u, v)

Objective

The objective component defines the cost function to minimize or maximize.

Criterion

The criterion indicates whether the problem is a minimization or maximization:

criterion(ocp)  # returns :min or :max
<< @example-block not executed in draft mode >>

Objective form

The objective function can be in Mayer form, Lagrange form, or Bolza form (combination of both):

  • Mayer: $g(x(t_0), x(t_f), v) \to \min$
  • Lagrange: $\int_{t_0}^{t_f} f^0(t, x(t), u(t), v)\, \mathrm{d}t \to \min$
  • Bolza: $g(x(t_0), x(t_f), v) + \int_{t_0}^{t_f} f^0(t, x(t), u(t), v)\, \mathrm{d}t \to \min$

Check which form is present:

has_mayer_cost(ocp)  # true if Mayer cost exists
<< @example-block not executed in draft mode >>
has_lagrange_cost(ocp)  # true if Lagrange cost exists
<< @example-block not executed in draft mode >>
Variant methods

Alternative methods are also available:

  • is_mayer_cost_defined(ocp)has_mayer_cost(ocp)
  • is_lagrange_cost_defined(ocp)has_lagrange_cost(ocp)

Mayer cost

Get the Mayer cost function with signature g(x0, xf, v):

g = mayer(ocp)  # error if no Mayer cost
<< @repl-block not executed in draft mode >>

Lagrange cost

Get the Lagrange cost function with signature f⁰(t, x, u, v):

f⁰ = lagrange(ocp)
s = 0.5
q = [0.0, 1.0]
u = 2.0
v = [1.0, 2.0]
f⁰(s, q, u, v)  # returns the integrand value
<< @example-block not executed in draft mode >>

Summary table

MethodReturnsDescription
criterion(ocp)Symbol:min or :max
has_mayer_cost(ocp)BoolTrue if Mayer cost exists
has_lagrange_cost(ocp)BoolTrue if Lagrange cost exists
mayer(ocp)FunctionMayer cost function g(x0, xf, v)
lagrange(ocp)FunctionLagrange cost function f⁰(t, x, u, v)

Constraints

The constraints component defines the constraints on the optimal control problem.

Individual constraints

Retrieve a specific constraint by its label using the constraint function. It returns a tuple (type, f, lb, ub):

(type, f, lb, ub) = constraint(ocp, :eq1)
println("type: ", type)
x0 = [0, 1]
xf = [2, 3]
v  = [1, 4]
println("val: ", f(x0, xf, v))
println("lb: ", lb)
println("ub: ", ub)
<< @example-block not executed in draft mode >>

The function signature depends on the constraint type:

  • For :boundary and :variable constraints: f(x0, xf, v)
  • For other constraints (:control, :state, :mixed): f(t, x, u, v)

Examples of different constraint types:

(type, f, lb, ub) = constraint(ocp, :cons_bound)
println("type: ", type)
println("val: ", f(x0, xf, v))
<< @example-block not executed in draft mode >>
(type, f, lb, ub) = constraint(ocp, :cons_u)
println("type: ", type)
s = 0.5
q = [1.0, 2.0]
u = 3.0
println("val: ", f(s, q, u, v))
<< @example-block not executed in draft mode >>
(type, f, lb, ub) = constraint(ocp, :cons_mixed)
println("type: ", type)
println("val: ", f(s, q, u, v))
<< @example-block not executed in draft mode >>

All constraints

Get all constraints as a collection:

constraints(ocp)  # returns all constraints
<< @example-block not executed in draft mode >>

Nonlinear constraints

Get nonlinear path and boundary constraints:

path_constraints_nl(ocp)  # returns nonlinear path constraints
<< @example-block not executed in draft mode >>
boundary_constraints_nl(ocp)  # returns nonlinear boundary constraints
<< @example-block not executed in draft mode >>
Tuple structure

The returned tuples have the structure (lb, f!, ub, labels) where:

  • lb: vector of lower bounds
  • f!: constraint function (in-place)
  • ub: vector of upper bounds
  • labels: vector of constraint labels

The constraint functions have the following signatures:

  • Path constraints: f!(val, t, x, u, v) where val is mutated
  • Boundary constraints: f!(val, x0, xf, v) where val is mutated

Get the dimensions of nonlinear constraints:

dim_path_constraints_nl(ocp)  # number of nonlinear path constraints
<< @example-block not executed in draft mode >>
dim_boundary_constraints_nl(ocp)  # number of nonlinear boundary constraints
<< @example-block not executed in draft mode >>
Note

To get the dual variable (or Lagrange multiplier) associated to a constraint, use the dual method on a solution.

Summary table

MethodReturnsDescription
constraint(ocp, label)(Symbol, Function, Real, Real)Get constraint by label
constraints(ocp)CollectionAll constraints
path_constraints_nl(ocp)ConstraintsNonlinear path constraints
boundary_constraints_nl(ocp)ConstraintsNonlinear boundary constraints
dim_path_constraints_nl(ocp)IntNumber of nonlinear path constraints
dim_boundary_constraints_nl(ocp)IntNumber of nonlinear boundary constraints

Problem definition

Get the problem definition as a string:

definition(ocp)  # returns the OCP definition as AbstractDefinition
<< @example-block not executed in draft mode >>

To extract the expression from the definition, use:

expr = expression(ocp)  # returns the Expr from the definition
nothing # hide
<< @example-block not executed in draft mode >>
Note

The definition is optional and can be EmptyDefinition. Use has_abstract_definition(ocp) to check if a definition is present.

Definition presence

Check whether the problem carries an abstract definition:

has_abstract_definition(ocp)  # true if definition is present (not EmptyDefinition)
<< @example-block not executed in draft mode >>
Variant method
  • is_abstractly_defined(ocp)has_abstract_definition(ocp)

Time dependence

Optimal control problems can be autonomous or non-autonomous. In an autonomous problem, neither the dynamics nor the Lagrange cost explicitly depends on the time variable.

The following problem is autonomous.

ocp = @def begin
    t ∈ [ 0, 1 ], time
    x ∈ R, state
    u ∈ R, control
    ẋ(t)  == u(t)                       # no explicit dependence on t
    x(1) + 0.5∫( u(t)^2 ) → min         # no explicit dependence on t
end
is_autonomous(ocp)
<< @example-block not executed in draft mode >>

The following problem is non-autonomous since the dynamics depends on t.

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)
<< @example-block not executed in draft mode >>

Finally, this last problem is non-autonomous because the Lagrange part of the cost depends on t.

ocp = @def begin
    t ∈ [ 0, 1 ], time
    x ∈ R, state
    u ∈ R, control
    ẋ(t)  == u(t)
    x(1) + 0.5∫( t + u(t)^2 ) → min     # explicit dependence on t
end
is_autonomous(ocp)
<< @example-block not executed in draft mode >>

The variant predicate is_nonautonomous is also available and returns the opposite of is_autonomous:

is_nonautonomous(ocp)  # true if dynamics or cost depend on time
<< @example-block not executed in draft mode >>
Variant method
  • is_nonautonomous(ocp)!is_autonomous(ocp)