You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
New repositories for the control-toolbox ecosystem
Two complementary repositories are proposed: one fills the most significant algorithmic
gap in the ecosystem, the other addresses long-term developer infrastructure. They are
independent and can be pursued in parallel.
1. CTIndirect.jl — indirect methods for optimal control
Motivation
CTFlows.jl already provides the building blocks for indirect methods: it can integrate
Hamiltonian flows given an initial costate. What is missing is the layer above it —
the formulation and resolution of the shooting equations, and the continuation machinery
needed to handle hard problems. These currently live in research scripts and tutorials,
not in a reusable, versioned library. The effect is that indirect methods are a
second-class citizen in the ecosystem, despite being a primary method in practice for
problems in space mechanics, quantum control, and the calculus of variations.
CTIndirect.jl would give indirect methods the same standing as direct methods
(CTDirect.jl), occupying a symmetric slot in the algorithms layer.
Scope
Simple shooting. A ShootingProblem type wraps an OCP and an initial costate
guess. The shoot() function integrates the Hamiltonian flow via CTFlows, evaluates
the boundary conditions on the adjoint, and returns a residual. The residual is passed
to NonlinearSolve.jl for root-finding. This is the canonical single-shooting approach
for problems with a known switching structure.
Multiple shooting. An interior time mesh is introduced; state and costate are
simultaneously integrated on each arc and matched at interior nodes. This improves
numerical conditioning for long-horizon problems and those with complex switching.
Homotopy / continuation. A HomotopyPath type deforms a tractable reference
problem (e.g., quadratic cost, no state constraints) toward the target problem through
a sequence of intermediate problems. Each intermediate solution is used as the warm-start
for the next. Arc-length continuation is supported for turning points. This is the
core technique for the hardest problems — minimum-time orbital transfers, problems with
bang-bang or singular arcs, constrained quantum control — and has no current home in
the toolbox.
Proposed interface
using OptimalControl, CTIndirect
# Simple shooting
prob =ShootingProblem(ocp, p0_guess)
sol =solve(prob, SimpleShooting(); nlp_solver=NewtonRaphson())
# Multiple shooting
prob =MultipleShootingProblem(ocp, p0_guess; nodes=10)
sol =solve(prob, MultipleShooting(); nlp_solver=NewtonRaphson())
# Homotopy
path =HomotopyPath(ocp_easy => ocp_hard; steps=20)
sol =solve(path)
The solve dispatch integrates with CTSolvers.jl so that indirect solvers appear
alongside direct ones in the unified solver registry. A combined workflow — indirect
warm-start from a direct solution, or vice versa — then requires no boilerplate.
2. CTDev — developer tooling for the multi-repo ecosystem
Context: the existing integration workflow
The multi-repo structure is already managed through a deliberate two-stage release
workflow. Beta versions of CTX packages are first registered in ct-registry, a LocalRegistry.jl-based Julia registry specific to the control-toolbox organization:
pkg> registry add https://github.com/control-toolbox/ct-registry.git
pkg> add CTDirect # resolves from ct-registry before General
This local registry acts as the integration gate: a new version of, say, CTModels
can be published there as a beta and immediately consumed by downstream packages for
testing, without touching Julia's General registry. Only once the cross-package
combination is validated does the release propagate to General.
The crux: cascading updates inside the CT hierarchy
The real difficulty is not releasing individual packages — it is coordinating a cascade of beta registrations triggered by a change anywhere in the dependency
hierarchy. The CT packages are not independent; they share a dependency graph with
several cross-edges:
A change to CTBase — a new type, a renamed function, a revised abstract interface —
requires a coordinated update of every package that depends on it, directly or
transitively. In practice this means:
Register CTBase v0.19.0-beta to ct-registry.
Update [compat] in CTModels to include the new beta → register CTModels beta.
Update [compat] in CTParser and CTFlows → register their betas.
Update [compat] in CTDirect, which depends on both CTModels and CTFlows →
register its beta.
Update [compat] in CTSolvers → register beta.
Update [compat] in OptimalControl → register beta.
Verify that the full combination works end-to-end.
When satisfied, re-register each package to General in topological order.
Done manually, this cascade is tedious and error-prone: it is easy to miss a compat
entry, register packages out of order, or lose track of which beta combination is
currently under test. This is the concrete problem CTDev is designed to solve.
Beta versions over branch-based development
The workflow described above deliberately favours pre-release registrations over the
alternative of using Pkg.develop() on local branches. This is the right choice,
for several reasons worth stating explicitly.
A registered beta — even one only in ct-registry — is an immutable, tagged
version. Julia's package resolver treats it like any other version: it checks compat
constraints, resolves transitive dependencies, and produces a reproducible Manifest.toml.
Every team member and every CI runner sees exactly the same thing by adding ct-registry.
A Pkg.develop() path is none of those things. It points to a mutable local
directory. It bypasses the package resolver's compat checks, so a compat violation
in a downstream package goes undetected until release time. It is invisible to CI
unless you reproduce the exact local filesystem layout on the runner. And it is
fragile in ways that are hard to debug: uncommitted changes, wrong branch, stale
precompilation cache. The only legitimate use of Pkg.develop() is rapid
within-session experimentation before you know what you want to commit — not as a
cross-package integration strategy.
Concretely: when working on a change that touches CTBase and CTDirect together,
the right workflow is to commit the CTBase change, tag and register a beta to ct-registry, then update CTDirect's compat entry and iterate. This is slightly
more ceremony than editing two local directories simultaneously, but it produces a
traceable, reproducible record of which combination was tested, and it exercises the
compat machinery that users will actually encounter.
Motivation for CTDev
What the existing workflow does not yet provide is a systematic, automated answer to
the question: is the current beta combination on ct-registry working end-to-end?
The local registry handles version pinning and staged release; CTBenchmarks.jl
tracks solver performance; CTActions centralizes CI workflow templates. But there
is no single repository that:
automates the cascade of compat bumps and beta registrations triggered by a
hierarchical change, keeping packages in dependency order and opening PRs
consistently;
runs cross-package integration tests (spanning the full parser → model → solver →
solution pipeline) automatically against the current beta combination;
provides a one-command developer environment bootstrap for contributors working
across multiple CTX packages.
CTDev fills that role. It is not a Julia package and is not registered on any
registry. It is a developer-facing repository whose subject is the ecosystem as a whole.
Scope
Cascade tooling. Given a release plan describing which packages change and by how
much (CTBase: minor, CTModels: patch, ...), a Julia script walks the dependency
graph in topological order, bumps versions, updates all downstream [compat] entries,
opens PRs against each affected repository, and registers the new betas to ct-registry
via LocalRegistry.register(). The dependency graph is encoded once in dependency_order.toml and reused for every release, rather than reconstructed from
memory.
Cross-package integration tests. A Project.toml that resolves against ct-registry (where betas live) and hosts end-to-end tests of the full pipeline: @def → parse → model → direct transcription → solve → solution extraction → plot,
as well as the Hamiltonian flow and GPU paths. These tests span multiple package
boundaries and cannot live inside any individual package's test suite. Running them
against ct-registry betas is exactly the validation step that currently happens
informally before each General release.
Nightly CI. A GitHub Actions workflow that resolves packages from ct-registry,
runs the integration suite, and reports failures per package. This makes the "does
the current beta combination work?" question answerable automatically, overnight,
rather than on each developer's machine before a release. Optionally triggered on
any push to ct-registry itself.
General promotion. Once the nightly suite is green on a beta combination, a
script triggers JuliaRegistrator for each package in topological order, respecting
the General registry's merge-delay convention.
Developer bootstrap. A script that clones all CTX repositories, adds ct-registry to the local Julia environment, and calls Pkg.develop() on all
packages, for contributors who need to work on multiple packages in the same session.
Even then, the bootstrap starts from ct-registry versions so that the local
dev environment is consistent with what CI sees.
Repository structure
CTDev/
├── integration/
│ ├── Project.toml # resolves from ct-registry
│ ├── Manifest.toml # pinned; updated after each beta registration
│ ├── test_direct.jl # @def → CTParser → CTModels → CTDirect → Ipopt
│ ├── test_indirect.jl # CTFlows → CTIndirect → BVP solve (once CTIndirect exists)
│ ├── test_gpu.jl # ExaModels → MadNLP/CUDA path
│ └── test_plot.jl # solution → RecipesBase rendering
├── release/
│ ├── dependency_order.toml # canonical topological order and cross-edges of CTX packages
│ ├── cascade.jl # bump versions, update all compat entries, open PRs
│ ├── register_beta.jl # register one or more packages to ct-registry
│ ├── register_general.jl # promote the validated beta combination to General
│ └── checklist.md # human steps (tag, announce, Discourse post)
├── dev/
│ ├── bootstrap.sh # clone all repos, add ct-registry, Pkg.develop() all
│ └── dev_env.jl # Julia-side of the bootstrap
├── .github/
│ └── workflows/
│ ├── nightly.yml # nightly integration suite against ct-registry
│ └── compat_check.yml # warn when compat entries drift from dependency_order
└── README.md
Key scripts in detail
cascade.jl — the central piece. Takes a release plan as input:
Walks dependency_order.toml, computes the new version numbers, updates every
affected [compat] entry in every downstream Project.toml, commits the changes,
and calls register_beta.jl for each package in order. The result is a fully
consistent beta combination on ct-registry with a single command, rather than
six manual steps prone to ordering errors.
nightly.yml — resolves all CTX packages from ct-registry, picking up the
latest registered betas, runs integration/, and reports failures per package. Also
updates integration/Manifest.toml as a timestamped record of exactly which versions
were tested.
register_general.jl — once the nightly suite is green, promotes each package
to General via JuliaRegistrator in topological order, with configurable delay
between registrations to respect the General registry's merge conventions.
What this is not
CTDev does not replace the per-package CI. Each CTX package continues to have
its own CI.yml running its own unit and regression tests. CTDev tests only
cross-package behaviour that cannot live inside any single package.
It is also not a monorepo. Each CTX package remains an independent repository with
its own history, issues, and release cycle. CTDev observes and coordinates them;
it does not own them.
Relationship between the two
The two repositories are orthogonal and can be pursued independently. If CTIndirect.jl
is developed, CTDev immediately gains a new node in dependency_order.toml and a
new integration test (test_indirect.jl). The cascade tooling then handles it
automatically on the next release. If CTDev is established first, it provides the
infrastructure that makes launching CTIndirect.jl smoother from day one: the beta
registration workflow, the cascade script, the nightly signal, and the bootstrap all
apply to any new CTX package.
A natural sequencing:
Bootstrap CTDev with the existing packages (direct, GPU, plot paths).
Develop CTIndirect.jl, registering development betas to ct-registry and
relying on the CTDev nightly suite as the integration signal.
Add test_indirect.jl to CTDev and a new entry to dependency_order.toml
as CTIndirect stabilizes toward a first release.
Both repositories follow the conventions of the ecosystem (CTAppTemplate, Blue
style, Documenter.jl, Aqua.jl, codecov) and would live under the control-toolbox GitHub organization.
Conclusion
The two proposals address distinct gaps. CTIndirect.jl closes the main algorithmic
asymmetry in the ecosystem: direct methods have a home (CTDirect), Hamiltonian flows
have a home (CTFlows), but shooting, multiple shooting, and homotopy — the workhorses
of hard optimal control problems — currently do not. CTDev closes the main operational
gap: the release cascade across eight interdependent packages is today a manual,
error-prone sequence, and the existing ct-registry / beta workflow, though sound in
principle, lacks the tooling to execute it reliably. Together, the two repositories
make the ecosystem stronger algorithmically and more robust to maintain, without
altering its multi-repo structure or its proven staged-release discipline.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
@ocots @PierreMartinon @gergaud
New repositories for the control-toolbox ecosystem
Two complementary repositories are proposed: one fills the most significant algorithmic
gap in the ecosystem, the other addresses long-term developer infrastructure. They are
independent and can be pursued in parallel.
1.
CTIndirect.jl— indirect methods for optimal controlMotivation
CTFlows.jlalready provides the building blocks for indirect methods: it can integrateHamiltonian flows given an initial costate. What is missing is the layer above it —
the formulation and resolution of the shooting equations, and the continuation machinery
needed to handle hard problems. These currently live in research scripts and tutorials,
not in a reusable, versioned library. The effect is that indirect methods are a
second-class citizen in the ecosystem, despite being a primary method in practice for
problems in space mechanics, quantum control, and the calculus of variations.
CTIndirect.jlwould give indirect methods the same standing as direct methods(
CTDirect.jl), occupying a symmetric slot in the algorithms layer.Scope
Simple shooting. A
ShootingProblemtype wraps an OCP and an initial costateguess. The
shoot()function integrates the Hamiltonian flow viaCTFlows, evaluatesthe boundary conditions on the adjoint, and returns a residual. The residual is passed
to
NonlinearSolve.jlfor root-finding. This is the canonical single-shooting approachfor problems with a known switching structure.
Multiple shooting. An interior time mesh is introduced; state and costate are
simultaneously integrated on each arc and matched at interior nodes. This improves
numerical conditioning for long-horizon problems and those with complex switching.
Homotopy / continuation. A
HomotopyPathtype deforms a tractable referenceproblem (e.g., quadratic cost, no state constraints) toward the target problem through
a sequence of intermediate problems. Each intermediate solution is used as the warm-start
for the next. Arc-length continuation is supported for turning points. This is the
core technique for the hardest problems — minimum-time orbital transfers, problems with
bang-bang or singular arcs, constrained quantum control — and has no current home in
the toolbox.
Proposed interface
The
solvedispatch integrates withCTSolvers.jlso that indirect solvers appearalongside direct ones in the unified solver registry. A combined workflow — indirect
warm-start from a direct solution, or vice versa — then requires no boilerplate.
Repository structure
Dependencies
CTBase.jlCTModels.jlCTFlows.jlNonlinearSolve.jlCTSolvers.jlNo new heavy dependency is introduced. The package is a pure Julia layer on top
of existing infrastructure.
Position in the ecosystem
2.
CTDev— developer tooling for the multi-repo ecosystemContext: the existing integration workflow
The multi-repo structure is already managed through a deliberate two-stage release
workflow. Beta versions of
CTXpackages are first registered inct-registry, aLocalRegistry.jl-based Julia registry specific to the control-toolbox organization:This local registry acts as the integration gate: a new version of, say,
CTModelscan be published there as a beta and immediately consumed by downstream packages for
testing, without touching Julia's General registry. Only once the cross-package
combination is validated does the release propagate to General.
The crux: cascading updates inside the CT hierarchy
The real difficulty is not releasing individual packages — it is coordinating a
cascade of beta registrations triggered by a change anywhere in the dependency
hierarchy. The CT packages are not independent; they share a dependency graph with
several cross-edges:
A change to
CTBase— a new type, a renamed function, a revised abstract interface —requires a coordinated update of every package that depends on it, directly or
transitively. In practice this means:
CTBase v0.19.0-betatoct-registry.[compat]inCTModelsto include the new beta → registerCTModelsbeta.[compat]inCTParserandCTFlows→ register their betas.[compat]inCTDirect, which depends on bothCTModelsandCTFlows→register its beta.
[compat]inCTSolvers→ register beta.[compat]inOptimalControl→ register beta.Done manually, this cascade is tedious and error-prone: it is easy to miss a compat
entry, register packages out of order, or lose track of which beta combination is
currently under test. This is the concrete problem
CTDevis designed to solve.Beta versions over branch-based development
The workflow described above deliberately favours pre-release registrations over the
alternative of using
Pkg.develop()on local branches. This is the right choice,for several reasons worth stating explicitly.
A registered beta — even one only in
ct-registry— is an immutable, taggedversion. Julia's package resolver treats it like any other version: it checks compat
constraints, resolves transitive dependencies, and produces a reproducible
Manifest.toml.Every team member and every CI runner sees exactly the same thing by adding
ct-registry.A
Pkg.develop()path is none of those things. It points to a mutable localdirectory. It bypasses the package resolver's compat checks, so a compat violation
in a downstream package goes undetected until release time. It is invisible to CI
unless you reproduce the exact local filesystem layout on the runner. And it is
fragile in ways that are hard to debug: uncommitted changes, wrong branch, stale
precompilation cache. The only legitimate use of
Pkg.develop()is rapidwithin-session experimentation before you know what you want to commit — not as a
cross-package integration strategy.
Concretely: when working on a change that touches
CTBaseandCTDirecttogether,the right workflow is to commit the
CTBasechange, tag and register a beta toct-registry, then updateCTDirect's compat entry and iterate. This is slightlymore ceremony than editing two local directories simultaneously, but it produces a
traceable, reproducible record of which combination was tested, and it exercises the
compat machinery that users will actually encounter.
Motivation for
CTDevWhat the existing workflow does not yet provide is a systematic, automated answer to
the question: is the current beta combination on
ct-registryworking end-to-end?The local registry handles version pinning and staged release;
CTBenchmarks.jltracks solver performance;
CTActionscentralizes CI workflow templates. But thereis no single repository that:
hierarchical change, keeping packages in dependency order and opening PRs
consistently;
solution pipeline) automatically against the current beta combination;
across multiple
CTXpackages.CTDevfills that role. It is not a Julia package and is not registered on anyregistry. It is a developer-facing repository whose subject is the ecosystem as a whole.
Scope
Cascade tooling. Given a release plan describing which packages change and by how
much (
CTBase: minor, CTModels: patch, ...), a Julia script walks the dependencygraph in topological order, bumps versions, updates all downstream
[compat]entries,opens PRs against each affected repository, and registers the new betas to
ct-registryvia
LocalRegistry.register(). The dependency graph is encoded once independency_order.tomland reused for every release, rather than reconstructed frommemory.
Cross-package integration tests. A
Project.tomlthat resolves againstct-registry(where betas live) and hosts end-to-end tests of the full pipeline:@def→ parse → model → direct transcription → solve → solution extraction → plot,as well as the Hamiltonian flow and GPU paths. These tests span multiple package
boundaries and cannot live inside any individual package's test suite. Running them
against
ct-registrybetas is exactly the validation step that currently happensinformally before each General release.
Nightly CI. A GitHub Actions workflow that resolves packages from
ct-registry,runs the integration suite, and reports failures per package. This makes the "does
the current beta combination work?" question answerable automatically, overnight,
rather than on each developer's machine before a release. Optionally triggered on
any push to
ct-registryitself.General promotion. Once the nightly suite is green on a beta combination, a
script triggers
JuliaRegistratorfor each package in topological order, respectingthe General registry's merge-delay convention.
Developer bootstrap. A script that clones all
CTXrepositories, addsct-registryto the local Julia environment, and callsPkg.develop()on allpackages, for contributors who need to work on multiple packages in the same session.
Even then, the bootstrap starts from
ct-registryversions so that the localdev environment is consistent with what CI sees.
Repository structure
Key scripts in detail
cascade.jl— the central piece. Takes a release plan as input:Walks
dependency_order.toml, computes the new version numbers, updates everyaffected
[compat]entry in every downstreamProject.toml, commits the changes,and calls
register_beta.jlfor each package in order. The result is a fullyconsistent beta combination on
ct-registrywith a single command, rather thansix manual steps prone to ordering errors.
nightly.yml— resolves allCTXpackages fromct-registry, picking up thelatest registered betas, runs
integration/, and reports failures per package. Alsoupdates
integration/Manifest.tomlas a timestamped record of exactly which versionswere tested.
register_general.jl— once the nightly suite is green, promotes each packageto General via
JuliaRegistratorin topological order, with configurable delaybetween registrations to respect the General registry's merge conventions.
What this is not
CTDevdoes not replace the per-package CI. EachCTXpackage continues to haveits own
CI.ymlrunning its own unit and regression tests.CTDevtests onlycross-package behaviour that cannot live inside any single package.
It is also not a monorepo. Each
CTXpackage remains an independent repository withits own history, issues, and release cycle.
CTDevobserves and coordinates them;it does not own them.
Relationship between the two
The two repositories are orthogonal and can be pursued independently. If
CTIndirect.jlis developed,
CTDevimmediately gains a new node independency_order.tomland anew integration test (
test_indirect.jl). The cascade tooling then handles itautomatically on the next release. If
CTDevis established first, it provides theinfrastructure that makes launching
CTIndirect.jlsmoother from day one: the betaregistration workflow, the cascade script, the nightly signal, and the bootstrap all
apply to any new
CTXpackage.A natural sequencing:
CTDevwith the existing packages (direct, GPU, plot paths).CTIndirect.jl, registering development betas toct-registryandrelying on the
CTDevnightly suite as the integration signal.test_indirect.jltoCTDevand a new entry todependency_order.tomlas
CTIndirectstabilizes toward a first release.Both repositories follow the conventions of the ecosystem (
CTAppTemplate, Bluestyle,
Documenter.jl,Aqua.jl, codecov) and would live under thecontrol-toolboxGitHub organization.Conclusion
The two proposals address distinct gaps.
CTIndirect.jlcloses the main algorithmicasymmetry in the ecosystem: direct methods have a home (
CTDirect), Hamiltonian flowshave a home (
CTFlows), but shooting, multiple shooting, and homotopy — the workhorsesof hard optimal control problems — currently do not.
CTDevcloses the main operationalgap: the release cascade across eight interdependent packages is today a manual,
error-prone sequence, and the existing
ct-registry/ beta workflow, though sound inprinciple, lacks the tooling to execute it reliably. Together, the two repositories
make the ecosystem stronger algorithmically and more robust to maintain, without
altering its multi-repo structure or its proven staged-release discipline.
Beta Was this translation helpful? Give feedback.
All reactions