Skip to content

Migrating to v2.1 ​

This page does not execute

Unlike every other page on this site, the code blocks here deliberately show spellings that error — that is the whole point. They are plain fences, not @example blocks, and are not run when the docs build. Every error message quoted below was verified by running it against the current package; treat the page as accurate reference, not as tested output.

OptimalControl.jl v2.1.0-beta reorganised the ecosystem so that every name has exactly one owner. Most of the fallout is mechanical — a Lie becomes an ad, a success becomes a successful — and every mechanical rename fails loudly with a message naming its replacement, instead of a bare UndefVarError. A few changes are semantic and will not announce themselves; those get their own warnings below.

The informative messages below need 2.2.0-beta, not just v2.1.0-beta

Every spelling on this page stopped working in v2.1.0-beta, tagged 2026-07-30 — that is when each old name was removed. The shims that turn the mistake into a named, actionable PreconditionError (the block under What you get instead of an error) shipped two weeks later, in 2.2.0-beta (src/deprecated.jl, #855). A copy pinned to exactly v2.1.0-beta gets a bare UndefVarError or MethodError for every one of them — the same mistake, none of the guidance below. Use 2.2.0-beta or newer.

The full technical record of every breaking change is BREAKING.md in the repository root — this page is its user-facing rendering, organised around "what do I do about it" rather than "what changed and why." Read BREAKING.md for the reasoning; read this page to fix a script.

Start here: Flow needs an integrator ​

This is the single most common first failure. SciML is no longer a hard dependency of the stack — the user chooses and loads an ODE integrator explicitly:

julia
using OptimalControl
using OrdinaryDiffEqTsit5   # ← new, and required before any Flow(...)

f = Flow(ocp, (x, p) -> p[2])

Without it, Flow fails with a clean ExtensionError naming OrdinaryDiffEqTsit5 (or OrdinaryDiffEq/DifferentialEquations) as the missing package — not a cryptic dispatch error. This is by design: it keeps the install cost of the direct (solve) path off users who never write a flow. See Installation and Flows overview.

What was renamed ​

v2.0v2.1.0-betaNotes
OptimalControl.VectorField, OptimalControl.Hamiltonian, …VectorField, Hamiltonian, …exported at top level since v2.1.0-beta; the qualified form still works, it's just no longer necessary
Lie(X, f)ad(X, f)renamed; Lie(...) now throws
X ⋅ fad(X, f)removed, no operator alias; X ⋅ f now throws
HamiltonianLiftLift(f) to build; OptimalControl.LiftedHamiltonianFunction to name the typerenamed and re-parented — see What changed meaning silently
autonomous=, variable=, inplace= (constructor keywords)is_autonomous=, is_variable=, is_inplace=prefixed, on VectorField, Hamiltonian, @Lie, and friends
Flow(f) with f::FunctionFlow(VectorField(f)), Flow(Hamiltonian(f)), Flow(HamiltonianVectorField(f))the bare function no longer says what kind of flow to build
Flow(ocp, u, g, μ) (3 positional)Flow(ocp, u; constraint=g, multiplier=μ)keywords, and they come as a pair
f(t0, x0, p0, tf, λ) / f(t0, x0, tf, λ)f(t0, x0, p0, tf; variable=λ) / f(t0, x0, tf; variable=λ)variable= is a mandatory keyword on a NonFixed problem, never positional
augment=truevariable_costate=truerenamed; returns (xf, pf, pvf) instead of (xf, pf)
CTSolvers.Modelers.ADNLP(), CTDirect.Collocation()OptimalControl.ADNLP(), OptimalControl.Collocation()neither module name is re-exported; write against OptimalControl
time(ocp), time(sol)times(ocp), time_grid(sol)time is no longer re-exported (it's Base.time)
success(sol)successful(sol)success is no longer re-exported (it's Base.success)

What changed shape ​

Three call conventions changed signature, not just spelling.

The flow call. There is no positional slot for the variable any more:

julia
f(t0, x0, p0, tf, λ)                                  
f(t0, x0, p0, tf; augment=true)                       
f(t0, x0, p0, tf; variable=λ)                         
f(t0, x0, p0, tf; variable=λ, variable_costate=true)  

variable= is mandatory on a NonFixed problem — omitting it does not silently default, it throws naming exactly what to pass. New in v2.1.0-beta: unsafe=false — with unsafe=true the ODE return code is not checked and integration failures don't throw, useful inside a shooting loop where an intermediate failure should surface through the residual instead. Also new: per-call integrator options (saveat=, abstol=, reltol=, alg=, …) no longer exist — the call signature only accepts variable, unsafe, variable_costate. Pass integrator options at construction time instead, where they still work exactly as before:

julia
# after — set once, at construction, not per call
f = Flow(ocp, (x, p) -> p[2]; abstol=1e-8)
f(t0, x0, p0, tf)

Constrained flows. Positional arguments became a keyword pair:

julia
fb = Flow(ocp, u, g, μ)                         # 3 positional
fb = Flow(ocp, u; constraint=g, multiplier=μ)   # paired keywords

The two keywords are a pair: one without the other is an IncorrectArgument. constraint= gained a capability along the way — besides a plain Function or a Data.PathConstraint, it now also accepts a Symbol naming a :path constraint already declared in the OCP: constraint=:vmax.

Constructor keywords take an is_ prefix, on VectorField, Hamiltonian, @Lie, and every sibling Data constructor:

julia
VectorField(f; autonomous=false, variable=true)        
@Lie [X, Y] autonomous=false
VectorField(f; is_autonomous=false, is_variable=true)   
@Lie [X, Y] is_autonomous=false

The old spelling on @Lie raises an IncorrectArgument at macro-expansion time — loud, not silently ignored. On the plain Data constructors it is not shimmed (see below): Julia cannot dispatch on a keyword name, and a workaround would need to duplicate CTBase's own trait detection across 14 entry points.

What changed meaning silently ​

Three changes will not announce themselves as an error. Each is confirmed by running it against the current package.

Lift(f::Function) is no longer an AbstractHamiltonian

In v2.0, lifting a plain function and lifting a typed VectorField produced the same kind of object. In v2.1.0-beta only the typed path does:

julia
H = Lift(f)          # f::Function
H isa AbstractHamiltonian   # false in v2.1.0-beta, true in v2.0

H is now an OptimalControl.LiftedHamiltonianFunction, <: Function only. Any isa or <: test against the old hierarchy is quietly wrong. Lift(X::AbstractVectorField) is unaffected — only the bare-function overload changed. See Lift.

OpenLoop rejects the wrong arity only when the flow runs, not when it's built

An open-loop control must be a function of time, u(t) (or u(t, v)). A bare constant closure constructs without error and only fails once the flow actually runs:

julia
bad_law = OpenLoop(() -> 1.0)                          # constructs fine — the trap
Flow(ControlledVectorField(fc), bad_law)(t0, x0, tf)   # MethodError here, not above

The MethodError points at the call site, far from the actual mistake at construction. is_autonomous is not a real choice for OpenLoop either way — passing it emits a @warn that it has no effect, rather than silently doing nothing. See Simulation.

The old 4-positional state-flow call silently misreads on an OptimalControlFlow

v2.0's state-flow convention was f(t0, x0, tf, λ) — 4 positional arguments. v2.1.0-beta's genuine (non-deprecated) call for a Fixed-time OptimalControlFlow is also 4 positional: f(t0, x0, p0, tf). If a v2.0 script calls the old convention against an OptimalControlFlow with a real number in the 4th slot, it matches the new signature instead of erroring — tf silently becomes p0 and λ silently becomes tf:

julia
f(t0, x0, tf, λ)   # intended: state flow, tf and lambda as before
# actually runs as f(t0, x0, p0=tf, tf=λ)  — arguments shifted by one position

It throws only when the 4th argument's type doesn't match p0's expected shape — so whether this is caught depends entirely on what happens to be in that slot. There is no shim for this: the deprecated 4-positional signature is disjoint on AbstractStateFlow (a plain vector-field flow, unaffected) but genuinely ambiguous on AbstractHamiltonianFlow, where it collides with the real v2.1.0-beta call. This one is on the caller to catch by eye.

What you get instead of an error ​

Every removed spelling below throws CTBase.Exceptions.PreconditionError naming its replacement — confirmed by running each one against 2.2.0-beta (the shims are not in the bare v2.1.0-beta tag; see the note near the top of this page):

julia
julia> Lie(X, f)
ERROR: PreconditionError: `Lie(X, f) / Lie(X, Y)` is deprecated
Reason  this spelling was removed in v2.1.0-beta
Hint    use ad(X, f) or ad(X, Y)

julia> X ⋅ f
ERROR: PreconditionError: `X \cdot f` is deprecated
Reason  this spelling was removed in v2.1.0-beta
Hint    use ad(X, f)

julia> HamiltonianLift(f)
ERROR: PreconditionError: `HamiltonianLift` is deprecated
Reason  this spelling was removed in v2.1.0-beta
Hint    use Lift(f) for a plain function, or CTLie.LiftedHamiltonianFunction

julia> time(ocp)
ERROR: PreconditionError: `time(ocp)` is deprecated
Reason  this spelling was removed in v2.1.0-beta
Hint    use times(ocp)

julia> time(sol)
ERROR: PreconditionError: `time(sol)` is deprecated
Reason  this spelling was removed in v2.1.0-beta
Hint    use time_grid(sol)

julia> success(sol)
ERROR: PreconditionError: `success(sol)` is deprecated
Reason  this spelling was removed in v2.1.0-beta
Hint    use successful(sol)

julia> Flow(f)   # f::Function
ERROR: PreconditionError: `Flow(f::Function)` is deprecated
Reason  this spelling was removed in v2.1.0-beta
Hint    use Flow(VectorField(f)), Flow(Hamiltonian(f)), or Flow(HamiltonianVectorField(f))

julia> f(t0, x0, p0, tf, λ)   # 5-positional, on a Hamiltonian flow
ERROR: PreconditionError: `f(t0, x0, p0, tf, lambda)` is deprecated
Reason  this spelling was removed in v2.1.0-beta
Hint    use f(t0, x0, p0, tf; variable=lambda)

julia> f(t0, x0, tf, λ)   # 4-positional, on a plain state flow
ERROR: PreconditionError: `f(t0, x0, tf, lambda)` is deprecated
Reason  this spelling was removed in v2.1.0-beta
Hint    use f(t0, x0, tf; variable=lambda)

The last row is the disjoint, safe half of the previous section's warning: on a plain AbstractStateFlow (not an OptimalControlFlow), the 4-positional call is unambiguous and shimmed cleanly. The hazard is specific to OptimalControlFlow/AbstractHamiltonianFlow.

What could not be shimmed, and why ​

Julia dispatches on argument types and arity, not on keyword names or "this call used to mean something else" — a few removed spellings can't be intercepted without either overwriting a method CTFlows still needs, or duplicating trait-detection logic that belongs upstream.

SpellingWhat actually happens todayWhy it isn't shimmed
Flow(ocp, u, g, μ) (3 positional)PreconditionError, thrown by CTFlows (not by OptimalControl), naming the keyword form: Flow(ocp, u; constraint=g, multiplier=μ)A 4-arity shim here would overwrite CTFlows' own method for this exact signature — and it need not, CTFlows guards it directly (CTFlows#401, fixed).
augment=trueBare MethodError, not caught anywhereSame positional signature as the still-valid call — a shim would overwrite CTFlows' own OptimalControlFlow method and breaks precompilation (Method overwriting is not permitted). Filed as CTFlows#402.
CTSolvers.Modelers.ADNLP(), CTDirect.Collocation()UndefVarError on CTSolvers / CTDirect — the module name is not bound under using OptimalControlCTSolvers, CTDirect and CTParser are implementation packages and are deliberately not re-exported (unlike CTBase, CTModels, CTFlows, CTLie), so there is no bound name to attach a throwing method to. The constructors themselves are re-exported: write OptimalControl.ADNLP() / OptimalControl.Collocation().
Per-call integrator options (saveat=, abstol=, reltol=, alg=, …)Bare MethodErrorSame reason: the call signature is closed to variable/unsafe/variable_costate only; a shim would overwrite CTFlows' own call method. Unlike the two rows above, this looks like a deliberate design choice (options belong at construction, baked into the flow's type), not a bug — not filed upstream. Construction-time options are unaffected.
@Lie [X, Y] autonomous=falseIncorrectArgument, raised at macro-expansion timeAlready thrown upstream in CTLie — nothing to add.
autonomous=/variable=/inplace= on VectorField, Hamiltonian, and other Data constructorsSilently ignored or MethodError, depending on the constructorJulia cannot dispatch on a keyword's name; a shim would mean re-detecting the trait by hand at all 14 entry points, duplicating logic CTBase already owns.

Legacy initial guesses ​

The @init macro (Initial guess) is the documented way to build an initial guess in v2.1.0-beta. The older direct NamedTuple construction still works — it was never removed — it's just no longer shown anywhere in the current docs, so there is no way to discover it still works without this page:

julia
# still works, not the documented path
sol = solve(ocp; init=(state=[-0.2, 0.1], control=-0.2))

# component labels work too
sol = solve(ocp; init=(q=-1.0, v=0.0, u=0.1, tf=2.0))

Prefer @init for new code — it validates labels against the problem and reads closer to the mathematics. There is still no way to provide an initial guess for the costate/multipliers directly; that remains true in both forms.

v1.x → v2.0 ​

For the older v1.x → v2.0 migration (the direct-mode API rewrite, option routing, the methods system), see the second section of BREAKING.md directly — it predates this page and isn't duplicated here.