JuliaCon 2026 · Optimal Control

Optimizing race car track times in Dyad.


Sebastian Micluța-Câmpeanu — JuliaHub · University of Bucharest
Rajeev Voleti — JuliaHub

JuliaCon 2026 · Aug 12 · Alte Mensa, Atrium Maximum

Problem

Drive one lap, as fast as possible

  • Nonlinear optimal control with free final time: minimize lap time T
  • Two controls: thrust and steering angle
  • Subject to
    • vehicle dynamics
    • tire force saturation
    • engine power limit
    • periodic boundary: end the lap the way you started
  • Reference implementation: the Dymos (NASA / OpenMDAO) racecar example
Model

A symbolic DAE, authored in Dyad

  • Dyad components compile to ModelingToolkit systems — the analysis below is plain SciML all the way down
  • Longitudinal + lateral dynamics, load transfer, nonlinear tire saturation
  • Track geometry: curvature κ(s) is an interpolation object stored as a parameter
  • mtkcompile(sys; inputs, outputs) — controls stay symbolic inputs, constrained quantities stay addressable outputs

After compilation

states9
control inputs (thrust, steering)2
bounded outputs (tires ×4, power)5
collocation nodes51

Costs and constraints are symbolic too: costs = [-EvalAt(te)(…)], point constraints via EvalAt, path bounds from variable metadata.

Attempt 1

The direct formulation, time as the independent variable

97
iterations, JuMP + RK4
(converges, barely conditioned)
Fail
InfiniteOpt finite differences
(oscillates, dual-inf → 3·10⁷)
Fail
InfiniteOpt orthogonal collocation
(never reaches feasibility)
Reparameterize

Drive on arc length, not on the clock

  • One call: change_independent_variable(sys, s), the s-parametrization used in everything that follows
  • Grid becomes fixed: s ∈ [0, track length]
  • Time turns into a passive lap-timer state, the objective is just t(L)
  • No free multiplier on the dynamics ⇒ well-scaled NLP
  • Same trick Dymos uses, but here it is a symbolic transformation of the same model, not a rewrite
Interface

From a System to a solver, four ways

System
mtkcompile, inputs + outputs
DynamicOptProblem
costs, constraints, bounds, guesses
JuMP InfiniteOpt CasADi Pyomo
Results · Accuracy

Direct collocation, three ways into Ipopt

TranscriptionStackLap timeIterations
Gauss–Lobatto (50, 3), L-BFGSDymos (Python) 22.27 s79
RK4 defects, analytic HessianJuMPDynamicOptProblem 22.83 s39
Orthogonal collocation (4)InfiniteOptDynamicOptProblem 22.00 s37
One symbolic model, one solve per backend — the transcription is a keyword, not a rewrite.
Results · Runtime

Runtime performance benchmarks

1.43 s
Dyad via MTK & InfiniteOpt wall time
(37 iterations)
6.3 s
Dyad via MTK & JuMP wall time
(39 iterations)
242.6 s
Dymos wall time
(79 iterations)
The speedup is structural: it's what an analytic sparse Hessian buys when the modeling layer can generate one.
Results · Validation

Same racing line, two independent stacks

Dymos GL(50,3)
MTK · JuMP RK4
MTK · InfOpt OC(4)

Identical apexes (n saturating at ±4 m), braking points and speed envelope (39–80 m/s). The only divergence is on the straights, where lateral position is nearly cost-free.

Results · Racing line

One lap, drawn from the solution data

Dymos · 22.27 s
MTK · InfOpt 22.00 s

Exact segment geometry · lateral offset n to scale (|n| ≤ 4 m) · draw-in paced by the optimal speed profile

Journey · 01

What it actually took, pt. 1: scaling is not optional

These are exactly the things a modeling language can automate: nominal-value metadata → automatic residual & constraint scaling.
Journey · 02

What it actually took, pt. 2: the track is a parameter you call

Interpolated lookup data in optimal control, with no manual operator plumbing. PR #4909 · merged
Results · Summary

The journey, in one table

FormulationBackendLap timeItersIpopt wallVerdict
time domain, free tfJuMP RK422.41 s97converges, ill-conditioned
time domain, free tfInfiniteOpt (FD / OC4)fails, intrinsically
s-parametrizationJuMP RK422.83 s396.3 srobust
s-parametrizationInfiniteOpt OC(4)22.00 s371.43 sbest honest lap, fastest solve
s-parametrization, lifted DAE-BVPMIRK4 + Ipopt21.84 s119302 sleakage-flattered; honest 22.02 s
referenceDymos GL(50,3)22.27 s79242.6 sbrackets the same optimum
Upstream

All of this has landed upstream

merged ModelingToolkit.jl

  • #4399 parameter bindings & observed substitution
  • #4406 inputs(sys) for optimal control
  • #4407 variable bounds → backend bounds
  • #4425 nominal-value metadata
  • #4702 input-aware codegen on compiled systems
  • #4839 functional initial guesses (trajectories)
  • #4840 observed-variable bounds lifting
  • #4841 residual scaling of dynamics
  • #4909 callable-parameter registration
  • #4930 InfiniteOpt verbose-flag fix
  • #4931 control-aware BVProblem dynamics

open in review now

  • BVDE #521 mass-matrix MIRK/FIRK
  • BVDE #589 OC constraint Jacobian
  • PreallocationTools #198 typed tracer workspaces

All split from the #4394 umbrella — reviewable pieces, each validated against this benchmark.

ModelingToolkitBase 1.65.0 ships all of them — the collocation workflow needs no patch branches.
Roadmap

Where this is going

The gap between “a model you simulate” and “a model you optimize over” is closing — symbolically.
Wrap-up

One model, three transcriptions, a racing line.

MTK dynamic optimization tutorial QR code
DYNAMIC OPTIMIZATION TUTORIAL
docs.sciml.ai/ModelingToolkit/
stable/tutorials/dynamic_optimization
Dyad documentation QR code
DYAD DOCUMENTATION
help.juliahub.com/dyad/stable
Backup

Backup: the BVP's 21.84 s was a leak

MIRK bounds live on mesh nodes — so at s ≈ 0 the lifted BVP fires thrust ≈ 40 (normal ±2), hits 115 m/s and swings power to −4000 kW.

Dymos 22.27 s
BVP unbounded 21.84 s
BVP honest 22.02 s