Chronological, newest first. Each CHANGELOG.md entry links back here for the full technical
record; this file never duplicates a release's own compat table, only what a caller must
change or be aware of.
- Unreleased
- Non-breaking notes (2.2.1-beta)
- Non-breaking notes (2.2.0-beta)
- Breaking changes: v2.0 → v2.1.0-beta
- Breaking changes: v1.x → v2.0
using OptimalControl used to bring the modules CTModels, ExaModels, CTBase,
CTFlows and ADNLPModels into scope, only so that the code generated by @def and
@init could name them. CTParser 0.9.6 made these macros hygienic, so the exports are gone
(#967), as for CTLie
below. @def and @init now work after import OptimalControl or
using OptimalControl: @def, @init as well. The names a user needs are reached through
OptimalControl: exported, or qualified as OptimalControl.X, like OptimalControl.ADNLP.
Two names change at the same time:
OptimalControl.DifferentiationInterfaceis now the AD backend of the differential geometry (it wasCTBase.Differentiation.DifferentiationInterface), not the package of the same name;systemandintegrator, the accessors of a flow, are exported (they wereCTFlows.Flows.systemandCTFlows.Flows.integrator).
Migration: only code that wrote one of these module names after using OptimalControl
is affected.
# Before
using OptimalControl
err isa CTBase.Exceptions.IncorrectArgument
backend = CTBase.Differentiation.DifferentiationInterface(; ad_backend=AutoForwardDiff())
CTFlows.Flows.integrator(f)
# After
using OptimalControl
err isa OptimalControl.IncorrectArgument
backend = OptimalControl.DifferentiationInterface(; ad_backend=AutoForwardDiff())
integrator(f)
# a module you still need: load it yourself (it must be in your environment)
using CTModels: CTModelsusing OptimalControl used to bring the module CTLie into scope, only so that the code
generated by @Lie could name it. CTLie 0.3 made @Lie hygienic, so the export is gone
(#966). The functions
and the macro (ad, Lift, Poisson, ∂ₜ, @Lie, dg_ad_backend, dg_ad_backend!) are
still exported, and @Lie now works after import OptimalControl or
using OptimalControl: @Lie as well.
Migration: only code that wrote CTLie.… after using OptimalControl is affected.
Load the module yourself, or qualify it through OptimalControl:
# Before
using OptimalControl
CTLie.LiftedHamiltonianFunction
# After
using OptimalControl
using CTLie: CTLie # CTLie in your environment
CTLie.LiftedHamiltonianFunction
# or, without adding CTLie to your environment
OptimalControl.CTLie.LiftedHamiltonianFunctionThe dependencies move to CTModels 0.21, CTSolvers 0.7 and CTDirect 2, which apply the sign
convention of ct-conventions SGN1–SGN3 to the multipliers. What dual(sol, ocp, :label) and
costate(sol) return changes in three cases:
- a constraint with two finite bounds, box or path, has μ = μ_ub − μ_lb: positive where the upper bound is active, negative where the lower bound is active (a box returned μ_lb − μ_ub);
- a constraint with a single finite bound has a non-negative multiplier;
- a
→ maxproblem has the costate and the duals of the equivalent→ minproblem (min Jandmax -Jgive the same values), the opposite of what the previous releases returned. A→ minproblem is unchanged apart from the two cases above.
Migration: drop any sign compensation.
# Before: two finite bounds, the upper bound active
dual(sol, ocp, :u_box)(t) < 0
# After
dual(sol, ocp, :u_box)(t) > 0
# Before: a max problem gave the opposite of the equivalent min problem
costate(sol_max)(t) == -costate(sol_min)(t)
# After
costate(sol_max)(t) == costate(sol_min)(t)With the :exa modeler, dual(sol, ocp, :label) of a path or boundary constraint throws a
PreconditionError; it returned 0, a value that was never computed
(CTParser#414).
Migration:
# Before: a 0 with :exa
d = dual(sol, ocp, :c)
# After: solve with :adnlp to read the multipliers
sol = solve(ocp, :adnlp)
d = dual(sol, ocp, :c)adandLifton aHamiltonianVectorField, andadand∂ₜon an in-place vector field, throwIncorrectArgumentinstead ofNotImplemented(CTLie0.3). Code that caughtNotImplementedfor these calls catchesIncorrectArgument.- The pins of ct-conventions and ct-agents and the
Releasecaller are documentation and CI only.
Documentation infrastructure only. Nothing to migrate — OptimalControl's API and runtime behaviour are unchanged, and the change is invisible to anyone who only uses the package.
For contributors who build the documentation locally:
docs/make.jlnow defaults todraft = false, so every page executes its@example/@replblocks. Setdraft = trueinmake.jlfor a fast local build that skips execution; opt a single page out of execution withDraft = truein its@metablock.- The docs build has no
warnonlyfallback any more: an unresolved@ref/@extref, or a broken@example/@repl, now fails the build rather than warning. - The docs environment moves to CTBase
0.30.4-betaand CTModels0.19.4-beta.
ExaModels 0.12 added objective and constraint to its export list (oracle builders); OptimalControl exports both names as accessors. Loading ExaModels unqualified next to OptimalControl makes objective(sol) / constraint(ocp, :label) throw UndefVarError — Julia refuses to resolve the ambiguous binding.
No migration required for the common case. The :exa modeler needs no using ExaModels: the module is bound through using OptimalControl and solve(ocp, :exa) works as-is. Only import ExaModels if you genuinely need its own API, and then import it qualified:
# Before — shadows the accessors
using ExaModels
# After
using ExaModels: ExaModels
ExaModels.some_function(...)No breaking change to OptimalControl's API: objective / constraint are unchanged. See #882, and the same fix one layer down in CTParser#230.
Explicit-mode solve used to silently drop a keyword argument that did not match any known option — a typo in a strategy option, or an option passed to the wrong strategy, ran without warning. It now raises an actionable error naming the option (#853).
# Before — silently ignored, ran with defaults
solve(ocp, :adnlp, :ipopt; toll=1e-8) # typo for tol=, dropped without a word
# After — raises, names the option
solve(ocp, :adnlp, :ipopt; toll=1e-8)
# ERROR: unconsumed option(s): tollStrategy-specific options must be configured at construction time, not passed loose to solve; see the explicit-mode documentation. Near-breaking: a script that relied on a mistyped or misplaced option being silently ignored will now raise where it previously ran. No option that was ever actually read changes behaviour — this only catches ones nothing was reading.
This section describes the breaking changes when migrating from OptimalControl.jl v2.0.5-beta to v2.1.0-beta. For the v1.x → v2.0 migration, see the section below.
Three symbol families physically changed package in this release, as the ecosystem was reorganised so that each name has exactly one owner. Most of the fallout is mechanical, but three items are semantic and will not announce themselves as import errors.
Every example's preamble changes. SciML is no longer a hard dependency of the stack — the user chooses and loads an integrator:
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. This is by design: it keeps the install cost of the direct path off users who never write a flow.
Lift, Poisson, ∂ₜ and @Lie are re-exported from the same names as before, so most code is unaffected. Two changes are not cosmetic:
| v2.0 | v2.1.0-beta | Notes |
|---|---|---|
Lie(X, f) |
ad(X, f) |
renamed; Lie(...) now throws PreconditionError |
X ⋅ f |
ad(X, f) |
removed; X ⋅ f now throws PreconditionError |
HamiltonianLift |
CTLie.LiftedHamiltonianFunction |
renamed; HamiltonianLift(...) now throws PreconditionError |
LiftedHamiltonianFunction is <: Function, no longer <: AbstractHamiltonian. Any isa or <: test against the old hierarchy is now wrong:
H = Lift(F) # F::Function
H isa AbstractHamiltonian # false in v2.1.0-beta, true in v2.0Note that Lift is overloaded on its input: Lift(X::AbstractVectorField) still returns a Hamiltonian. Only the plain-Function overload changed.
The signature changed, not just the spelling.
# before
f(t0, x0, p0, tf, λ)
f(t0, x0, p0, tf; augment=true)
# after
f(t0, x0, p0, tf; variable=λ)
f(t0, x0, p0, tf; variable=λ, variable_costate=true)-
There is no positional slot for the variable any more, and
variable=is mandatory on aNonFixedproblem. The old positional spellingsf(t0, x0, p0, tf, λ)andf(t0, x0, tf, λ)raise aPreconditionErrorsuggestingvariable=λ; omitting it raises aPreconditionErrorwhose suggestion is literally "Passvariable=vwhen calling the flow" — it does not silently default. -
augment=true→variable_costate=true. It integrates the augmented adjointṗᵥ = -∂H/∂vand returns(xf, pf, pvf)instead of(xf, pf). The old spelling is not shimmed — it fails with a bareMethodErrortoday, not aPreconditionError— because a shim here would mean overwriting CTFlows' own method; filed as CTFlows#402 instead. -
New
unsafe=false. Withunsafe=truethe ODE retcode is not checked and failures do not throw — useful inside a shooting loop, where an intermediate failure should surface through the residual. -
Integrator options can no longer be overridden per call. In v2.0, keywords like
saveat=,abstol=,reltol=,alg=were forwarded straight through to OrdinaryDiffEq.jl at call time:# before — worked at call time f(t0, x0, p0, tf; abstol=1e-8) f((t0, tf), x0, p0; saveat=range(t0, tf, 100))
The call signature now only accepts
variable,unsafeandvariable_costate; anything else is a bareMethodError, not aPreconditionError— this spelling is not shimmed (it would mean overwriting CTFlows' own call method, the same reasonFlow(ocp, u, g, μ)above andaugment=are not shimmed either). Pass integrator options at construction time instead, where they still work exactly as before:# after — set once, at construction f = Flow(ocp, (x, p) -> p[2]; abstol=1e-8) f(t0, x0, p0, tf)
# before
fb = Flow(ocp, u, g, μ) # 3 positional
# after
fb = Flow(ocp, u; constraint=g, multiplier=μ) # paired keywordsThe old positional form already raises a PreconditionError today, but with a misleading suggestion — fix filed as CTFlows#401.
The two are a pair: one without the other is an IncorrectArgument.
constraint now accepts three spellings, which is a capability gain rather than a rename — a plain Function, a Data.PathConstraint, or a Symbol naming a :path constraint already declared in the OCP:
fb = Flow(ocp, u; constraint=:vmax, multiplier=μ) # reuse the model's own constraint# before
VectorField(f; autonomous=false, variable=true)
@Lie [X, Y] autonomous=false
# after
VectorField(f; is_autonomous=false, is_variable=true)
@Lie [X, Y] is_autonomous=falseThe old spelling on @Lie raises an IncorrectArgument at macro-expansion time rather than being ignored.
| Name | Why |
|---|---|
time |
It is Base.time. time(ocp) and time(sol) now throw PreconditionError; use times(ocp) or time_grid(sol). |
success |
Base.success is the name. success(sol) now throws PreconditionError; use successful(sol). |
The full CTBase.Data type vocabulary is now available without reaching into the package by hand — Flow dispatches on these, so building a flow explicitly needed them:
VectorField, Hamiltonian, HamiltonianVectorField, ComposedHamiltonian, PseudoHamiltonian, ControlLaw, OpenLoop, ClosedLoop, DynClosedLoop, PathConstraint, StateConstraint, ControlConstraint, MixedConstraint, Multiplier, and their abstract supertypes.
OpenLoop, ClosedLoop, DynClosedLoop and the constraint kinds are factory functions, not types. They all build a ControlLaw{F,Kind,…} / PathConstraint{F,Kind,…}; the kind is a trait parameter, so OpenLoop <: AbstractControlLaw is a TypeError. Dispatch on the trait.
Also new: CTLie.dg_ad_backend / dg_ad_backend! (global AD-backend control), CTFlows.MultiPhase (n_phases, get_flow, get_switching_time, …), and CTSolvers.Integrators (SciML, final_state, evaluate_at).
An open-loop control depends only on time — u(t) (or u(t, v)) — never on the state or costate, and autonomy is a property of the OCP, not of the control law itself. OpenLoop therefore does not offer is_autonomous as a real choice, unlike ClosedLoop/DynClosedLoop:
OpenLoop(t -> 1.0) # the only spelling — always u(t), or u(t, v) with is_variable=true
OpenLoop(() -> 1.0) # wrong: constructs silently, MethodError once the flow is runis_autonomous is kept as a misuse-detector keyword only: passing it (true or false) emits a @warn explaining that it has no effect, rather than silently doing nothing or being treated as a real choice. ClosedLoop and DynClosedLoop are unaffected — is_autonomous still governs their arity exactly as before.
See control-toolbox/CTBase.jl#515.
If you define your own AbstractStrategy, you must now implement parameter:
CTBase.Strategies.parameter(::Type{<:MyStrategy}) = nothing # non-parameterized
CTBase.Strategies.parameter(::Type{MyStrategy{P}}) where {P} = P # parameterizedThis is not a rename of CTSolvers.Strategies.get_parameter_type, which returned nothing by default. The CTBase generic throws NotImplemented instead, and option routing calls it — so a strategy that omits it fails at solve time rather than being treated as non-parameterized.
A caller that cannot guarantee a third-party strategy implements the contract should reach for the non-throwing CTBase.Strategies.parameter(strategy_type, default) — the get(dict, key, default)-style 2-arg accessor (CTBase ≥ 0.28.8-beta) — rather than writing its own try/catch around NotImplemented. OptimalControl's own display code does exactly this, and warns once per strategy type when the fallback is taken.
describe(:id) previously only knew the solve registry (discretizer, NLP modeler, NLP solver). The AD backend and the ODE integrator are strategies in the same sense — describe(:di) and describe(:sciml) now work from the same single entry point, which merges the solve registry with CTFlows' flow registry (Base.merge(::CTBase.Strategies.StrategyRegistry...), CTBase ≥ 0.28.8-beta).
This document describes the breaking changes when migrating from OptimalControl.jl v1.1.6 (last stable release) to v2.0.0.
!!! note "v2.0.5-beta Compatibility"
v2.0.5-beta is fully backward compatible with v2.0.4. It adds a Paderborn tutorial with Literate + Binder setup, narrows CTBase to =0.18.8 and CTModels to =0.10.1 for stricter version pinning, and includes README updates and code formatting, with no breaking changes.
!!! note "v2.0.4 Compatibility" v2.0.4 is fully backward compatible with v2.0.3. It contains documentation improvements (scalar/vector convention warning, :exa modeler incompatibility note) and build system changes (logger filter) with no breaking changes.
!!! note "v2.0.3 Compatibility" v2.0.3 is fully backward compatible with v2.0.2. It adds the functional API (macro-free) for defining optimal control problems programmatically and updates CTModels to v0.10, with no breaking changes to existing code.
!!! note "v2.0.2 Compatibility" v2.0.2 is fully backward compatible with v2.0.1. It contains a dependency update (UnoSolver v0.2 → v0.3) with no breaking changes.
!!! note "v2.0.1 Compatibility"
v2.0.1 is fully backward compatible with v2.0.0. It contains documentation improvements and an export change (build_initial_guess is now explicitly reexported) with no breaking changes.
Version 2.0.0 represents a major architectural redesign of OptimalControl.jl, introducing:
- Complete solve architecture redesign with descriptive and explicit modes
- GPU/CPU parameter system for heterogeneous computing
- Advanced option routing with introspection and disambiguation tools
- New solver integrations (Uno, MadNCL)
- Control-free problems support with augmented Hamiltonian approach
- CTFlows enhancements with
augment=trueand direct OCP flow creation - Modernized reexport system using
@reexport import
The following functions from v1.1.6 have been removed and replaced:
| v1.1.6 Function | v2.0.0 Replacement | Notes |
|---|---|---|
direct_transcription |
discretize |
New function from CTDirect.jl |
set_initial_guess |
@init macro |
Use the @init macro for initial guess construction |
build_OCP_solution |
ocp_solution |
New function from CTSolvers.jl |
Migration example:
# v1.1.6
docp = direct_transcription(ocp, grid_size=100)
set_initial_guess(docp, x_init, u_init)
sol = build_OCP_solution(docp, nlp_sol)
# v2.0.0
docp = discretize(ocp, Collocation(); grid_size=100)
init = @init ocp begin
x = x_init
u = u_init
end
sol = ocp_solution(docp, nlp_sol)Removed exports:
IncorrectMethodIncorrectOutputUnauthorizedCall
Added exports:
PreconditionError
These exceptions are still available via CTBase.IncorrectMethod, etc., but are no longer re-exported by OptimalControl.jl.
The following types are no longer exported (but still available via qualified access):
VectorField→ useOptimalControl.VectorFieldorCTFlows.VectorFieldHamiltonian→ useOptimalControl.HamiltonianorCTFlows.HamiltonianHamiltonianLift→ useOptimalControl.HamiltonianLiftorCTFlows.HamiltonianLiftHamiltonianVectorField→ useOptimalControl.HamiltonianVectorFieldorCTFlows.HamiltonianVectorField
Migration example:
# v1.1.6
X = VectorField(f)
# v2.0.0
X = OptimalControl.VectorField(f)
# or
using CTFlows: VectorField
X = VectorField(f)The solve function has been completely redesigned with two modes:
# Specify strategies using symbols
sol = solve(ocp, :collocation, :adnlp, :ipopt, :cpu)
# Partial specification (auto-completed)
sol = solve(ocp, :ipopt) # Uses first matching method
sol = solve(ocp, :gpu) # Uses first GPU method# Specify strategies using typed components
sol = solve(ocp;
discretizer=Collocation(),
modeler=ADNLP(),
solver=Ipopt()
)The methods() function now returns 4-tuples instead of 3-tuples:
# v1.1.6
methods() # Returns (discretizer, modeler, solver)
# v2.0.0
methods() # Returns (discretizer, modeler, solver, parameter)
# Example: (:collocation, :adnlp, :ipopt, :cpu)The 4th element is the parameter (:cpu or :gpu) for execution backend.
v2.0.0 introduces automatic option routing with new introspection tools:
describe(strategy)— Display available options for a strategyroute_to(strategy=value)— Disambiguate shared optionsbypass(option=value)— Pass undeclared options to strategies
Example:
# Inspect available options
describe(:ipopt)
describe(:collocation)
# Disambiguate shared options
sol = solve(ocp, :ipopt;
max_iter=100, # Shared option (auto-routed)
route_to(solver=:print_level=>0) # Explicitly route to solver
)
# Pass undeclared options
sol = solve(ocp, :ipopt;
bypass(solver=:custom_option=>42)
)v2.0.0 introduces the @init macro for constructing initial guesses:
# v2.0.0
init = @init ocp begin
u = 0.5
x = [1.0, 2.0]
end
sol = solve(ocp; initial_guess=init)
# or using alias
sol = solve(ocp; init=init)The old functional approach is no longer supported.
These features are new in v2.0.0 but don't break existing code:
Support for optimal control problems without control variables:
ocp = @def begin
tf ∈ R, variable
t ∈ [0, tf], time
x ∈ R², state
ẋ(t) == f(x(t)) # No control
∫(L(x(t))) → min
end- Uno: CPU-only nonlinear optimization solver
- MadNCL: GPU-capable solver
Total of 5 solvers: Ipopt, MadNLP, Uno, MadNCL, Knitro
Basic schemes:
:trapeze— Trapezoidal rule:midpoint— Midpoint rule:euler/:euler_explicit/:euler_forward— Explicit Euler:euler_implicit/:euler_backward— Implicit Euler
ADNLP-only schemes:
:gauss_legendre_2— 2-point Gauss-Legendre collocation:gauss_legendre_3— 3-point Gauss-Legendre collocation
Explicit GPU/CPU selection via parameter:
# CPU execution (default)
sol = solve(ocp, :collocation, :adnlp, :ipopt, :cpu)
# GPU execution (requires ExaModels + MadNLP/MadNCL)
using CUDA, MadNLPGPU
sol = solve(ocp, :collocation, :exa, :madnlp, :gpu)v2.0.0 introduces comprehensive support for control-free problems (optimal control without control variables) with enhanced CTFlows integration:
Augmented Hamiltonian approach:
# v1.1.6: Manual augmented Hamiltonian construction
function H_aug(t, x_, p_)
x, λ = x_
p, _ = p_
return H(t, x, p, λ)
end
f = Flow(Hamiltonian(H_aug))
# v2.0.0: Direct OCP flow creation
f = Flow(ocp)Automatic costate computation:
# v2.0.0: augment=true automatically computes p_λ(tf)
function shoot!(s, p0, λ)
_, px_tf, pλ_tf = f(t0, x0, p0, tf, λ; augment=true)
s[1] = px_tf # p(tf) = 0
s[2] = pλ_tf # p_λ(tf) = 0
endMathematical framework:
- Complete augmented system dynamics with proper transversality conditions
- Automatic handling of Lagrange costs:
$p_\lambda(t_f) = 0$ - Automatic handling of Mayer costs:
$p_\omega(t_f) = -2\omega$ - Initial conditions:
$p_\lambda(t_0) = 0$ by construction
v2.0.0 requires updated versions of CTX packages:
| Package | v1.1.6 | v2.0.0 |
|---|---|---|
| CTBase | 0.16-0.17 | 0.18 |
| CTModels | 0.6 | 0.9 |
| CTDirect | 0.x | 1.0 |
| CTSolvers | N/A | 0.4 |
| CTParser | 0.7-0.8 | 0.8 |
New dependency: CTSolvers.jl (handles NLP modeling and solving)
The main breaking changes are:
- Removed functions:
direct_transcription,set_initial_guess,build_OCP_solution - Changed exports: Some CTBase exceptions and CTFlows types no longer exported
- New solve architecture: Descriptive/explicit modes with 4-tuple methods
- Initial guess: Use
@initmacro instead of functional approach
For detailed usage examples, see the documentation.