Skip to content

Your first problem ​

The shortest complete story: define a problem, solve it, look at the result, and check it against the exact answer. No options, no theory: every line is explained, and the other pages go deeper.

The problem ​

A wagon moves along a rail. Its position is , its velocity , and we push it with a force (unit mass, no friction).

Starting at rest at   , we want it at rest at   one unit of time later, while spending as little energy as possible:

where   is the state. This is the double integrator, the "hello world" of optimal control.

Define it ​

We load OptimalControl to write the problem and NLPModelsIpopt to solve it (see Installation), then transcribe the formulation with @def:

julia
using OptimalControl
using NLPModelsIpopt

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

Each line mirrors the mathematics:

LineMeaning
t ∈ [0, 1], timethe time variable and its fixed interval
x = (q, v) ∈ R², statea two-dimensional state, whose components are named q and v
u ∈ R, controla scalar control
x(0) == [-1, 0], x(1) == [0, 0]the initial and final conditions
ẋ(t) == [v(t), u(t)]the dynamics
0.5∫(u(t)^2) → minthe cost to minimise

See Abstract syntax (@def) for every form these lines can take.

Solve it ​

solve discretises the problem in time and hands the resulting finite-dimensional problem to Ipopt:

julia
sol = solve(ocp)
▫ This is OptimalControl 2.2.1-beta, solving with: collocation → adnlp → ipopt (cpu)

  📦 Configuration:
   ├─ Discretizer: collocation
   ├─ Modeler: adnlp
   └─ Solver: ipopt

▫ This is Ipopt version 3.14.19, running with linear solver MUMPS 5.9.0.

Number of nonzeros in equality constraint Jacobian...:     1754
Number of nonzeros in inequality constraint Jacobian.:        0
Number of nonzeros in Lagrangian Hessian.............:      250

Total number of variables............................:      752
                     variables with only lower bounds:        0
                variables with lower and upper bounds:        0
                     variables with only upper bounds:        0
Total number of equality constraints.................:      504
Total number of inequality constraints...............:        0
        inequality constraints with only lower bounds:        0
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0

iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
   0  5.0000000e-03 1.10e+00 2.24e-14   0.0 0.00e+00    -  0.00e+00 0.00e+00   0
   1  6.0000960e+00 2.22e-16 1.78e-15 -11.0 6.08e+00    -  1.00e+00 1.00e+00h  1

Number of Iterations....: 1

                                   (scaled)                 (unscaled)
Objective...............:   6.0000960015360381e+00    6.0000960015360381e+00
Dual infeasibility......:   1.7763568394002505e-15    1.7763568394002505e-15
Constraint violation....:   2.2204460492503131e-16    2.2204460492503131e-16
Variable bound violation:   0.0000000000000000e+00    0.0000000000000000e+00
Complementarity.........:   0.0000000000000000e+00    0.0000000000000000e+00
Overall NLP error.......:   1.7763568394002505e-15    1.7763568394002505e-15


Number of objective function evaluations             = 2
Number of objective gradient evaluations             = 2
Number of equality constraint evaluations            = 2
Number of inequality constraint evaluations          = 0
Number of equality constraint Jacobian evaluations   = 2
Number of inequality constraint Jacobian evaluations = 0
Number of Lagrangian Hessian evaluations             = 1
Total seconds in IPOPT                               = 4.598

EXIT: Optimal Solution Found.

The first box names the method that ran: solve(ocp) was called without any method, so it used the default (:collocation, :adnlp, :ipopt, :cpu). The rest is Ipopt's log. EXIT: Optimal Solution Found is the line to look for. Ipopt needs a single iteration here because the dynamics are linear and the cost is quadratic.

Look at it ​

Plots draws the solution:

julia
using Plots
plot(sol)

The figure shows the state , the costate and the control . The wagon accelerates, then brakes: the velocity peaks at mid-time and the control decreases linearly from about to about . The costate comes from the multipliers of the discretised problem: is constant and coincides with the control, as the maximum principle predicts (see the energy example below).

The solution also carries the cost and a few solver diagnostics:

julia
objective(sol), iterations(sol), successful(sol)
(6.000096001536038, 1, true)

The state and the control are functions of time. Because the control is scalar, control(sol) returns a Number, not a length-1 vector (see the note on dimension one):

julia
state(sol)(0.5), control(sol)(0.25)
([-0.5000000000000006, 1.5000240003840084], 3.0000480007680177)

See Solution object for everything else a solution carries, and Plot for what else plot can show.

Check it ​

This problem can be solved by hand: the optimal control is   , the state at mid-time is   , and the minimal cost is . The numerical solution matches, up to the discretisation error of the default grid (250 steps):

julia
objective(sol) - 6
9.60015360380595e-5

The energy minimisation example derives this exact solution with the maximum principle and recovers it by indirect shooting.

Next ​

  • Choosing a method — pick a different solver, modeler, or grid.

  • Guided tour — the same problem in more depth, plus the indirect (Pontryagin) method and a second, harder problem.

  • Example gallery — worked problems covering singular arcs, state constraints, free variables, and more.