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
CTDirect (v1.0.12) transcribes a continuous optimal control problem (OCP, from CTModels) into a nonlinear program (NLP) solved through CTSolvers (ADNLPModels / ExaModels backends). The current code is a flat module with two discretizers (Collocation, DirectShooting) plugged into the CTSolvers.Strategies machinery, a central DOCP struct, and per-scheme files in src/ode/ (Euler, midpoint, trapeze, generic IRK, stagewise IRK).
A detailed constructive code review exists in discussion #594 (two passes: code-level and SOLID/architecture). This roadmap absorbs its findings and re-scopes them together with the open issues and the Handbook conventions into a coherent redesign plan, in the spirit of what was done for CTFlows (roadmap-v4).
The central theme: abstract the founding ingredients of a transcription so that discretization methods become recipes — generic on abstract types (e.g. driven by an arbitrary Butcher tableau), yet overridable with optimized specializations (e.g. midpoint avoiding duplicate evaluations). Everything else (Butcher-tableau input, ExaModels ownership, grid refinement, multiple shooting, impulsive problems) follows from getting this abstraction right.
Work already underway: branch refactor/docp-dispatch
The branch refactor/docp-dispatch (3 commits, not yet merged) already lands part of §1/§2:
CTSolvers dispatch contract: Collocation / DirectShooting now subtype CTSolvers.AbstractDiscretizer and implement CTSolvers.discretize / CTSolvers.build_model / CTSolvers.build_solution via multiple dispatch on (discretizer, modeler) instead of returning closure bundles.
Immutable caches: DOCPCache (discretize-time, holds the DOCP) and ExaBuildCache (build-time, carries the Exa getter inside the returned CTSolvers.BuiltModel) — this resolves the exa_getter closure-mutation issue (Constructive Code Review #594 §1.4) cleanly.
CTBase.Strategies / CTBase.Options / CTBase.Core / CTBase.Exceptions are now the imports (previously reached through CTSolvers re-exports).
Still to improve on/after that branch: the rest of §2 (scheme symbol dispatch, layout interfaces, typed extractors) and everything in §3+. This roadmap assumes the branch is the new baseline.
Ground rule: rewrite from scratch is on the table
Given the depth of §1–§3, a greenfield rewrite (new submodule tree built alongside, legacy deleted at the end) is an acceptable — possibly preferable — alternative to an in-place migration. The precondition is a regression safety net written first:
Golden integration tests over the test/problems/ suite: for each (problem × scheme × backend) combination, record objective value (rtol), convergence status, and iteration count from the current release; the rewrite must reproduce them.
Benchmark baseline (test/benchmark.jl success counts and timings) recorded on main before any rewrite; midpoint/trapeze fast paths are the performance sentinels.
These tests are written against the public surface only (solve-level via CTSolvers/OptimalControl), so they survive any internal reorganization.
With that net in place, the incremental-vs-greenfield choice becomes tactical, not risky; §14 items 2–3 (new-style unit/contract tests) then accompany the new code rather than the legacy one.
Current pain points (evidence)
Symbol if-else scheme dispatch in the DOCP constructor (DOCP_data.jl:307-349) — closed to extension; the scheme Symbol travels far beyond the API boundary (primitive obsession).
Direct field access everywhere: docp.time.steps, docp.dims.NLP_x, disc._step_variables_block, hand-computed offsets in every getter/setter and sparsity pattern (ode/common.jl:113-170) — no layout abstraction, no interface.
Runtime symbol dispatch in the post-processing getter(nlp_solution, docp; val::Symbol) (ode/common.jl:7-104), with fragile occursin("_l", …) string matching.
backend == :manual conflates two orthogonal choices (provide sparsity patterns vs. pick AD backends) (collocation.jl:112-134).
The ExaModels transcription does not live in CTDirect: it is generated at parse time by CTParser's :exa backend (onepass.jl) and only invoked here via CTModels.get_build_examodel(ocp) — a full duplicate of the discretization logic, owned by the wrong package (§5).
Duplicated scheme code: Midpoint vs Gauss_Legendre_1, Gauss_Legendre_2 vs Gauss_Legendre_2_Stagewise — each scheme re-implements layout, bounds, initial guess, integral, constraints and both sparsity patterns.
Half-finished parts: ode/variable.jl excluded from includes with debug remnants; DirectShooting Exa builders empty.
Tests do not follow the Handbook testing template; docs still on plain Documenter.
1. Architecture overhaul — submodules per responsibility
Reference:Handbook modules.md. Status: started on refactor/docp-dispatch (dispatch contract); submodule split not started. This is the structural half of the redesign; §2–§3 are the conceptual half. Do them together, incrementally.
Goal: move from the flat src/*.jl layout to one submodule per responsibility, src/<Name>/<Name>.jl, exporting its public API, with the top-level CTDirect.jl exporting nothing (only include + using .Submodule), qualified imports everywhere, and a strict dependency DAG.
Proposed decomposition (to refine while implementing §3)
ode/{euler,midpoint,trapeze,irk,irk_stagewise}.jl scheme data
Layouts
NLP variable & constraint layout: what the unknowns are, where each block lives, index ranges
the _step_variables_block / offset arithmetic scattered in ode/*
Transcriptions
the generic recipe: assemble objective, constraints, bounds, initial guess from (OCP, scheme, layout, grid) — for both NLP emission targets (callables for ADNLP, generators for Exa, §5)
Jacobian/Hessian pattern construction utilities and per-recipe patterns
add_nonzero_block!, DOCP_*_pattern
Grids
time grids (uniform, custom, free-time reconstruction), later refinement/rebalancing
DOCPtime, get_time_grid
Discretizers
the CTSolvers-facing strategies (Collocation, DirectShooting) and the discretize/build_model/build_solution contract methods
collocation.jl, direct_shooting.jl
Solutions
rebuild the functional OCP solution from the NLP solution (typed extractors)
build_OCP_solution, getter, get_time_grid_exa
Rules to enforce (from the Handbook): naming conventions symbol / _helper / __default, no bare using Pkg, qualification at call sites, one-way DAG (Schemes → Layouts → Transcriptions → Discretizers, with Sparsity, Grids, Solutions as leaves/mid-levels).
Migration strategy: two admissible paths, both gated by the regression safety net (see Context ground rule): (a) incremental — introduce the submodules around the new abstractions of §3 while the legacy flat code keeps working, migrate scheme by scheme (start with Euler — simplest layout — then trapeze/midpoint, then IRK), delete the legacy path last; or (b) greenfield — build the new submodule tree from scratch next to the legacy code and switch over once the golden tests pass. In both cases benchmarks (test/benchmark.jl) are run against the recorded baseline at each step: the current midpoint/trapeze implementations are performance-tuned and the redesign must not regress them.
2. Types, traits, interfaces — kill symbol dispatch and field access
The user-facing Symbol (:midpoint, :gauss_legendre_2) is kept only at the API boundary and converted once, via a registry (scheme_type(::Symbol) or the CTBase.Strategies registry — see the cross-cutting section), into a scheme type/instance. The DOCPif-else chain (DOCP_data.jl:307-349) disappears; adding a scheme = adding one file + one registration, no core edit (OCP principle). This also opens the door to passing a scheme object directly (a ButcherTableau, §4).
2.2 Traits for scheme properties
Properties currently encoded as booleans/fields (_final_control, explicit vs implicit Euler flag, constant-control vs stagewise IRK) become traits / type parameters with extractors, dispatched via the Holy-trait pattern:
keep traits in the type where they change layout/signatures (structural axes), as extracted values where they only tune internals.
2.3 Interfaces instead of field access
No consumer reads docp.time.steps or disc._step_variables_block directly. Define accessor interfaces (steps(grid), state_dim(dims), state_range(layout, i), control_range(layout, i, j), stage_range(layout, i, j), …) with NotImplemented stubs on the abstract types so a new scheme that forgets a required method fails loudly. This is what makes sparsity patterns (§6) and typed extractors (§9) writable generically: the index ranges come from the layout object, used both by evaluation getters and by pattern builders (this is exactly the "Utils: discretization dependent getters that return the indices range" item of #383).
2.4 Typed accessors for post-processing
Replace getter(nlp_solution, docp; val::Symbol) by dispatch on small singleton types (StateVal, ControlVal, CostateVal, PathMultVal, …) or plain named functions, with backend (ADNLP / Exa) selected by a wrapper type — eliminating the isnothing(exa_getter) guards and the occursin string matching.
DOCP immutable (DOCPbounds vectors already carry the mutation).
exa_getter closure mutation — done on refactor/docp-dispatch: the getter travels in the immutable ExaBuildCache inside CTSolvers.BuiltModel; the builder closures are replaced by the CTSolvers.discretize/build_model/build_solution dispatch contract.
Abstract-level error fallbacks for unsupported backend/discretizer combinations (LSP): DirectShooting + Exa must fail loudly, not return nothing (with the dispatch contract, this is now a missing-method / explicit-stub story on CTSolvers.build_model).
Remove/park ode/variable.jl debug remnants; hide Gauss_Legendre_1 (test-only) from the public surface.
build_OCP_solution decomposed (_extract_primal, _extract_dual); dead code and docstring arities fixed.
3. The transcription recipe — abstract the founding ingredients
Status: the heart of the roadmap. References:#583, #525, CTFlows' _core_invoke_flow as the style model.
Goal: identify and reify the ingredients of a direct transcription so that building a DOCP is a generic recipe over abstract types, analogous to CTFlows:
Unknowns — what the NLP variables are. Today: states at steps + controls (+ stage slopes K_i^j for IRK) + static variable v. #583 explicitly asks to investigate the alternative parametrization with stage statesx_ij as unknowns instead of slopes f_ij. Make "the set of unknowns" a first-class concept so both parametrizations are recipes.
Storage / layout — where each unknown lives in xu and each equation lives in c (§2.3). One layout object per (scheme × control-parametrization), queried by index-range accessors; the offset arithmetic exists in exactly one place.
Reconstruction at time points — how to recompute state/control (and costate) at the t_i, at stage times t_i^j, and at arbitrary t. #525 asks to standardize getters with both discrete (step index) and continuous (time value) variants; Stage-wise controls for IRK methods #583 raises the question of which control the solution reports (averaged vs stage controls) and where path constraints are evaluated (step times with first-stage control, averaged control, or stage times). These are evaluation policies — small strategy objects, not hard-coded choices.
Defect / dynamics constraints — the scheme equations (x_{i+1} = x_i + h_i Σ b_j K_i^j, stage equations). Generic version driven by the Butcher tableau; specialized versions may override (see 3.3).
Cost quadrature — the running-cost integral, consistent with the scheme's quadrature (b, c), reusing dynamics work where possible (the trapeze integral/work-array inconsistency noted in Constructive Code Review #594 §3.3).
Initial guess injection — writing a functional/vector initial guess into the layout (ties into §8).
Work arrays — per-recipe preallocation contract (setWorkArray generalized; fix the per-call allocations in the IRK integral, Constructive Code Review #594 §3.1).
Plus one dimension, not an ingredient but a target: the recipe must be expressible toward two NLP emission backends — in-place Julia callables (f, c!) for ADNLPModels, and generator expressions for ExaModels (§5). The design note must treat "how a recipe emits its NLP" as an explicit axis from day one, otherwise the Exa repatriation (§5) will not fit afterwards.
3.2 Generic recipe on abstract types
Write the constraints/objective/bounds/pattern code once, against the layout + scheme interfaces (a generic IRK with tableau (A, b, c) covers Euler explicit/implicit, midpoint, GL2, GL3, and any user tableau). The existing GenericIRK / GenericIRKStagewise are the seed of this; the generic path becomes the default, not a variant.
3.3 Optimized specializations
The generic path must remain overridable per scheme through ordinary dispatch: midpoint can avoid computing things twice (single stage, x_s = (x_i+x_{i+1})/2 shared between defect and quadrature — this is why midpoint.jl is "much faster than the version using stage variables"); trapeze can reuse endpoint dynamics across neighbouring steps. Rule: specialize a method of the recipe (defect!, integral, setWorkArray, patterns), never fork a whole parallel implementation. Benchmarks guard the specializations.
3.4 Deliverable order
Design note (in .reports/) fixing the ingredient interfaces (names, signatures, contracts) — including the ADNLP/Exa emission axis — reviewed before code.
Generic tableau-driven recipe (validate against current GL2/GL3 results and benchmarks).
Migrate midpoint/trapeze/Euler as specializations; delete duplicated code (Gauss_Legendre_1, constant-control IRK variants become tableau instances with a StepwiseControl policy).
Status: not started; blocked by §3 (needs the generic recipe). Goal: the user can pass a Butcher tableau directly to the discretizer:
tableau = Schemes.ButcherTableau(A, b, c) # + optional name/order metadataCollocation(scheme = tableau) # direct objectCollocation(scheme =:gauss_legendre_2) # symbols still work (registry)
ButcherTableau lives in Schemes with validation (sizes, c = A·1 check optional/warn, explicit/implicit detection, stiffly-accurate detection, order if provided).
Named schemes become constructors returning tableaux (gauss_legendre(2), radau_iia(3), lobatto_iiia(2), midpoint(), …) — the catalogue is data, not code.
Dispatch picks the optimized recipe when the tableau matches a specialized scheme (or when the user passes the named object), the generic recipe otherwise. The scheme option type in Strategies.metadata widens from Symbol to Union{Symbol, Schemes.AbstractScheme}.
Control parametrization is an orthogonal option (control = :stepwise | :stagewise), replacing the ad-hoc :gauss_legendre_2_constant_control symbol aliases.
5. Own the ExaModels transcription in CTDirect (move it out of CTParser)
Status: design item, not started; important. Where it lives today: CTParser's :exa parsing backend (onepass.jl) generates the whole ExaModels transcription at @def-parse time — variables with grid layout (p_state_exa!, p_control_exa!), per-scheme dynamics defects (p_dynamics_exa!: :euler, :euler_implicit, :midpoint, :trapeze hard-coded if scheme == … chains at onepass.jl:918-941), Lagrange quadrature (p_lagrange_exa!), constraints. The generated builder is stored in the model and CTDirect merely calls CTModels.get_build_examodel(ocp) (collocation.jl:209).
Why this is wrong: the discretization of an OCP is CTDirect's raison d'être, yet for the Exa backend it is implemented — a second time, with different scheme semantics — inside the parser. Consequences:
Duplicated transcription logic that can silently drift (final-control conventions already differ, cf. the "see with JB" notes in collocation.jl:198-208).
Restricted feature set on Exa: only 4 schemes (no IRK/stagewise), no custom time_grid, no path/boundary-constraint multipliers in solutions, DirectShooting unsupported.
Exa is only available for @def-parsed problems — a functional OCP built directly against CTModels has no ExaModel.
CTParser cannot benefit from §3/§4 (tableau-driven recipes) without re-implementing them in generated code.
Goal: CTDirect builds the ExaModels.ExaModel itself, as the second emission target of the §3 recipe; CTParser's :exa backend is reduced (then retired) once parity is reached.
The design question: how CTDirect emits Exa generators
ExaModels wants generator expressions over index sets (SIMD-friendly), not opaque closures — that is precisely why the codegen ended up in the parser. Two candidate routes, to be settled in the §3 design note:
Function tracing: call the user's dynamics/cost/constraint functions on ExaModels variable objects (Node types) inside generators. Works when the functions are written generically (type-generic, no Vector{Float64} assumptions); the "1-D = scalar" convention (§7) and the in-place buffer contract need special care (Exa is functional-style, not in-place). Likely requires an out-of-place variant of the dynamics from CTModels, or per-coordinate access.
Expression-level IR: CTParser stores a neutral expression representation of the problem in the model (it already manipulates the expressions), and CTDirect generates the Exa transcription from that IR with its own scheme/layout knowledge. Keeps codegen, but moves the discretization decisions to CTDirect; the parser only describes the problem.
Route 1 is the cleaner target (no codegen, works for non-parsed OCPs); route 2 is the pragmatic fallback if tracing hits ExaModels limitations (control-flow in user functions, non-scalar indexing). Prototype route 1 on a small problem (double integrator, then Goddard) early, since the outcome shapes the §3 emission axis.
Work items (Exa repatriation)
Feasibility prototype: trace a CTModels functional OCP's dynamics/costs into ExaModels.constraint/objective generators for Euler and trapeze; compare against the CTParser-generated model (same NLP dimensions, same solution).
Decide route 1 vs 2 (with JB — this is a cross-package decision with CTParser and CTModels: what get_build_examodel becomes, what IR or function variants CTModels must carry).
Implement the Exa emission of the §3 recipe for the scheme catalogue; wire it in CTSolvers.build_model(dm, ig, ::Modelers.Exa); reuse ExaBuildCache.
Solution parity: constraint multipliers, box multipliers, time grids (closes the "not yet implemented for exa" gaps of §9).
Deprecate CTParser's :exa backend once golden tests pass on both paths; keep it only as a comparison oracle during the transition.
GPU regression tests (test_gpu.jl) run on the new path before the switch.
6. Sparsity & AD: separate "pattern provision" from "backend choice"
Status: not started; small and independent (can be done before §3, then re-based on it). Issues:#383, #385.
Today collocation.jl:112-134: backend == :manual simultaneously (a) injects the hand-built Jacobian/Hessian patterns and (b) hard-picks ReverseDiffADGradient / SparseADJacobian / SparseReverseADHessian. Two orthogonal axes, one knob.
Work items (sparsity/AD)
Two options on the discretizer/modeler: sparsity = :manual | :detected (who provides the patterns: CTDirect's structural knowledge vs. SparseConnectivityTracer detection) × ad_backend = :default | :optimized | … (ADNLPModels preset or explicit backend types). Any combination valid; document defaults. Coordinate with CTSolvers, which owns the backend option surface.
True OCP-function sparsity in manual patterns (#383): the manual patterns currently declare full dense blocks per (constraint, variable-block) pair; use the actual sparsity of the OCP functions (dynamics, costs, constraints — from CTModels) to shrink them. The layout index-range accessors (§2.3/§3.1) are the prerequisite; start with Euler. Hessian first (most impactful), then Jacobian.
Coloring order experiment (#385): benchmark alternative column orders for coloring, as suggested by A. Montoison.
Fix add_nonzero_block! duplicate symmetric-diagonal entries (Constructive Code Review #594 §4.2) while moving it into Sparsity.
CTDirect is a caller of user OCP functions: it must pass scalars for 1-D state/control/variable to dynamics, costs and constraints, with coercion driven by the declared dimension (only when dim == 1, identity otherwise), never re-wrapping a scalar into a 1-vector; the in-place derivative buffer stays a length-n vector. CTFlows shipped this ("1-D = scalar" refactor, _coerce_state / _dim_coerce at the CTModels boundary) — reuse the same approach and vocabulary.
Work items (shape audit)
Audit every call boundary into CTModels.dynamics/lagrange/mayer/*_constraints_nl — apply dimension-driven coercion on xi, ui, v (currently raw views).
Solution side: 1-D state/control/costate reported as scalars in the built Solution (coordinate with CTModels build_solution).
Type-recording contract tests (fake dynamics recording argument types) as prescribed by the Handbook: scalar for 1-D problems, AbstractVector for n-D, under AD too (Duals are Numbers).
The Exa path has its own boundary (functional tracing or generated code, §5): the scalar contract applies at the OCP-function call sites there too — align it during the §5 repatriation.
8. Initial guess — pluggable and comparable strategies
Issue:#398. Status: open. Today: flat 0.1 for everything (DOCP_variables.jl:126, magic constant flagged in #594 §2.6).
Design split (two levels):
CTModels (OCP level):problem-informed default guesses, since all needed data (boxes, boundary conditions, dynamics) live in the model: median of two-sided boxes for controls/states; linear-in-time state interpolation between initial and final conditions when both exist, constant when one exists; heuristics/hints for free times (e.g. (x_f − x_0)/v_0-style estimates, probably user-hinted rather than automated). This extends the existing CTModels.AbstractInitialGuess / build_initial_guess machinery.
CTDirect (NLP level): injecting a functional guess into the NLP vector for all unknowns of the layout — including internal unknowns (stage slopes K_i^j: e.g. initialize with f(t_i^j, x̂_ij, û_ij, v̂) evaluated on the interpolated guess, instead of 0.1), via the layout interface (§3.1, ingredient 7).
Comparability requirement: initial-guess strategies must be first-class values (strategy objects or named policies) so different choices can be benchmarked against each other (iterations to convergence over the test/problems/ suite). Add a small comparison harness to the benchmark tooling; keep :constant (current behaviour) as the baseline.
9. Solution building & outputs
Issues:#405 (path-constraint duals ÷ step length — the ADNLP path now normalizes in DOCP_data.jl:596-602; verify sign/convention against the reference test and close), plus #594 §2.1/§2.5/DIP. Status: partially done, needs consolidation.
Work items (solutions)
Typed extractor layer (§2.4) shared by ADNLP and Exa paths; single RawNLPResult-style adapter isolating the SolverCore field-layout dependency.
Exa parity: path/boundary constraint multipliers and box multipliers for the ExaModels path (currently "+++ not yet implemented for exa") — becomes tractable once CTDirect owns the Exa layout (§5).
Stagewise outputs: decide and implement the reported control for stagewise IRK (averaged with weights b_j — current — vs. stage controls on the stage time grid; the new multi-grid CTModels.build_solution supports distinct grids, so exposing stage controls is feasible). Follows the evaluation-policy decision of §3.1.
Costate grid: currently P on T[1:end-1]; revisit conventions (per-scheme costate reconstruction is an "algorithm/pattern" in the sense of Stage-wise controls for IRK methods #583 — e.g. midpoint duals live at midpoints).
10. Direct multiple shooting — control/state parametrization decoupled from the ODE scheme
Issue:#525. Status: basic prototype done (fixed-step schemes with control_steps sub-controls per step); rework pending, re-based on §3.
The prototype revealed exactly the abstractions §3 must provide: control_steps leaked into DOCPtime, Midpoint.integral grew a two-branch special case, and the control time grid is rebuilt by hand in build_OCP_solution. After §3:
Control parametrization is an ingredient (piecewise-constant with k pieces per shooting interval, later piecewise-linear/polynomial), independent of the integration scheme used inside the interval.
State parametrization: shooting-node states as unknowns, interior integration by the scheme — a different unknowns choice (§3.1 ingredient 1), same recipe framework.
Discrete + continuous getters (step index / time value) standardized — shared with §3.1 ingredient 3.
Variable-step ODE integration inside intervals with proper AD (ref. ADNLPModels#280); revive/replace ode/variable.jl behind this design instead of its current half-finished state.
ExaModels support for DirectShooting (after §5; until then, an explicit loud "unsupported" — never silent nothing).
Status: new design item, not in the issue tracker yet. Blocked by §3 (grids and layouts must be first-class).
Two related capabilities, both to be formulated as generic recipes:
Free interior nodes / rebalancing: when some t_i are unknowns (beyond free t0/tf), the grid itself enters the NLP (with monotonicity constraints and step-size bounds). Grids must support a grid whose nodes are (partially) decision variables; the recipe reads h_i through the grid interface, so the same defect/quadrature code serves fixed and free grids. Natural applications: switching-time optimization, impulsive problems (§12).
Mesh refinement: outer loop — solve on a coarse grid, estimate local error (defect-based or curvature-based), refine/redistribute nodes, re-inject the previous solution as initial guess (uses the continuous getters of §3.1/§10 and the initial-guess injection of §8), resolve. This is a meta-recipe on top of the transcription; design it as a strategy so refinement policies are swappable and comparable.
Deliverable: a design note first (interfaces between Grids, the refinement loop, and the warm start), then a prototype on a 1-D problem with a known solution structure (e.g. bang-bang switch localization).
12. Impulsive case
Issue:#358 (refs OptimalControl#386). Status: open, not started; needs CTModels support first (how an impulsive OCP is even modelled: jump maps / impulse controls at (free) instants).
From the CTDirect side, an impulsive transcription = state jumps at selected nodes: x_{i+1}^+ = x_i^- + Δ(x_i, w_i) alongside the continuous defects — i.e. a per-node jump ingredient in the recipe, plus free jump instants via §11's free-node machinery. Scope for this roadmap: (a) coordinate the modeling layer with CTModels, (b) prototype a fixed-impulse-time transcription as a recipe extension, (c) free impulse times after §11.
Reference:Handbook VITEPRESS-DOC.md. Status: not started (docs are plain Documenter with index.md + API reference).
Mechanical migration to DocumenterVitepress following the Handbook steps (deps, make.jl format swap, generate_template, config.mts patches, CI workflow update, LiveServer preview).
Content — explain finely what is done (the real gap): a "Transcription" guide deriving, for each scheme family, the exact NLP produced — unknowns and layout, defect equations, quadrature of the running cost, where path constraints are evaluated, what the costate/duals mean and how they are rescaled (#405), reported control for stagewise schemes, and how the same recipe emits ADNLP callables vs Exa generators (§5). Written against the §3 abstractions so docs and code share vocabulary (ingredients / recipes / policies).
Guides for: passing a Butcher tableau (§4), sparsity/AD options (§6), initial-guess strategies (§8), direct shooting (§10), grid refinement (§11) as they land.
Reference:Handbook testing.md. Status: not compliant. Current layout: test/ci/*.jl scripts included from runtests.jl, plus archives/, tmp/, jump/ folders, .jld2/.json fixtures at the root.
Adopt the file template: one module per test file, qualified imports, single test_<name>() entry redefined at outer scope, top-level fakes, VERBOSE/SHOWTIMING from Main.TestOptions.
Organize by functionality (not by src/ layout): suite/schemes/ (tableau validation, order checks), suite/layouts/ (index-range accessors — pure unit tests, no solver), suite/transcription/ (objective/constraints values on hand-checkable problems, ADNLP/Exa cross-checks), suite/sparsity/ (patterns ⊇ detected patterns; manual vs detected equivalence of solutions), suite/solutions/ (extractors, duals, grids), suite/discretizers/ (end-to-end problem suite — the current test/problems/ convergence tests remain the integration layer, extended into the golden regression harness of the Context ground rule), suite/shape/ (1-D = scalar contract tests, §7).
Contract tests with fakes: a FakeScheme/FakeLayout implementing only the required interface methods validates the §2.3 contracts and error stubs (NotImplemented for missing methods, loud errors for unsupported backend combos).
Determinism & hygiene: remove archives//tmp/ from the repo or move out of test/; make GPU tests a guarded suite as today but with the same file template; keep the benchmark harness separate from correctness tests, extended with the initial-guess comparison runs (§8).
In control-toolbox vocabulary, every swappable choice is a strategy in the broad sense — there is no separate "algorithm" concept. The actual decision, to be made once, up front, is one of machinery granularity: which variation points get the full CTBase.Strategies treatment (subtype of AbstractStrategy, Strategies.metadata with validated OptionDefinitions, registry entry, describe introspection) and which remain lightweight strategy types — plain singleton/policy types or type parameters dispatched internally (Holy traits), with no metadata/registry because nothing about them is user-tunable. The refactor/docp-dispatch branch already commits to CTBase.Strategies as the machinery; the question is how far down it goes, and the answer shapes §3–§11.
Proposed criterion: full CTBase.Strategies machinery when the choice is (a) user-selectable from solve(...), (b) carries its own options needing validation/defaults/aliases, or (c) forms an extension boundary (weak dep, third-party addition). Lightweight type otherwise — an internal dispatch axis of the recipe that no user ever names.
Variation point
Machinery
Rationale
Discretizer (Collocation, DirectShooting)
full Strategies (already is)
user-facing, own options, CTSolvers contract
Scheme (:midpoint, tableau, §4)
full Strategies candidate — or strategy-owned option carrying a type
user-selectable and extensible (a user tableau is an extension); today an option of Collocation. Registering schemes in a Strategies registry gives symbol→type resolution and describe of the catalogue for free; the alternative is widening the option to Union{Symbol, Schemes.AbstractScheme}. Decide in the design note
NLP emission target (ADNLP / Exa, §5)
full Strategies (already: CTSolvers modelers)
the recipe dispatches on Modelers.ADNLP / Modelers.Exa; CTDirect adds methods, not knobs
Sparsity provision / AD backend (§6)
options (on discretizer/modeler)
two orthogonal knobs, no behaviourally rich object behind them; coordinate with CTSolvers' modeler options
Initial-guess policy (§8)
full Strategies
user-selectable, swappable and benchmarkable/comparable; may carry options (e.g. interpolation kind); lives partly in CTModels
Mesh refinement (§11)
full Strategies
outer loop with its own options (tolerance, max passes, estimator); natural extension boundary
pure recipe internals; dispatch on scheme/layout types; never named by a user
A lightweight type can be promoted later to a full strategy if it grows options or becomes user-facing — the criterion is not identity ("is it a strategy?" — everything is) but machinery cost/benefit. Err toward the full machinery when in doubt: registration is cheap and describe documents the catalogue.
Use more of the existing CTBase.Strategies surface than today: the registry (create_registry, type_from_id) should replace the scheme symbol→type lookup of §2.1 rather than a hand-rolled Dict; describe gives free introspection/documentation of available schemes and discretizers; RoutedOption/route_to covers options that must flow from OptimalControl down to the discretizer or modeler. A short design note should pin this table down (with JB/PM) before §3 implementation starts, so strategy boundaries and recipe interfaces are decided together.
Priority summary
Priority 0 (before anything else):
Regression safety net — golden integration tests (problems × schemes × backends, public API only) + benchmark baseline recorded on main. Precondition for either migration path, and the enabler of a from-scratch rewrite.
Merge/finish refactor/docp-dispatch — the dispatch-contract baseline this roadmap builds on.
High priority (the core redesign):
Strategy-granularity design note (cross-cutting table above: which variation points get the full CTBase.Strategies machinery) + §3 ingredient interfaces design note (including the ADNLP/Exa emission axis) — write and review together before code.
§5 Exa feasibility prototype (trace-based Exa emission on a small problem) — early, because its outcome shapes the §3 design.
§1/§2 architecture & dispatch overhaul — implemented with §3 (incremental or greenfield, per the ground rule), benchmark-guarded.
§6 sparsity/AD option separation — small, immediate user value; item 1 (two options) can even precede §3.
Medium priority:
§4 Butcher tableau input — first visible payoff of the generic recipe.
Constructive Code Review #594 review items not explicitly sectioned above (dead code, docstring fixes, Gauss_Legendre_1 visibility, IRK allocation fixes, trapeze work-array reuse) are absorbed into §1–§3 migration steps — treat the Constructive Code Review #594 summary tables as the checklist during migration.
Benchmark discipline:test/benchmark.jl results (success count / timings) recorded before and after each migration step; the midpoint/trapeze fast paths are the regression sentinels.
CTModels coordination points: initial-guess strategies (§8), scalar solutions (§7), stage-control grids in build_solution (§9), out-of-place dynamics variant or expression IR for Exa tracing (§5), impulsive modeling (§12).
CTParser coordination points: fate of the :exa parsing backend and get_build_examodel (§5) — parser describes the problem, CTDirect owns the discretization.
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.
-
CTDirect Roadmap v1
Last updated: July 7, 2026
Context
CTDirect (v1.0.12) transcribes a continuous optimal control problem (OCP, from CTModels) into a nonlinear program (NLP) solved through CTSolvers (ADNLPModels / ExaModels backends). The current code is a flat module with two discretizers (
Collocation,DirectShooting) plugged into theCTSolvers.Strategiesmachinery, a centralDOCPstruct, and per-scheme files insrc/ode/(Euler, midpoint, trapeze, generic IRK, stagewise IRK).A detailed constructive code review exists in discussion #594 (two passes: code-level and SOLID/architecture). This roadmap absorbs its findings and re-scopes them together with the open issues and the Handbook conventions into a coherent redesign plan, in the spirit of what was done for CTFlows (roadmap-v4).
The central theme: abstract the founding ingredients of a transcription so that discretization methods become recipes — generic on abstract types (e.g. driven by an arbitrary Butcher tableau), yet overridable with optimized specializations (e.g. midpoint avoiding duplicate evaluations). Everything else (Butcher-tableau input, ExaModels ownership, grid refinement, multiple shooting, impulsive problems) follows from getting this abstraction right.
Work already underway: branch
refactor/docp-dispatchThe branch
refactor/docp-dispatch(3 commits, not yet merged) already lands part of §1/§2:Collocation/DirectShootingnow subtypeCTSolvers.AbstractDiscretizerand implementCTSolvers.discretize/CTSolvers.build_model/CTSolvers.build_solutionvia multiple dispatch on (discretizer, modeler) instead of returning closure bundles.DOCPCache(discretize-time, holds theDOCP) andExaBuildCache(build-time, carries the Exa getter inside the returnedCTSolvers.BuiltModel) — this resolves theexa_getterclosure-mutation issue (Constructive Code Review #594 §1.4) cleanly.CTBase.Strategies/CTBase.Options/CTBase.Core/CTBase.Exceptionsare now the imports (previously reached through CTSolvers re-exports).Still to improve on/after that branch: the rest of §2 (scheme symbol dispatch, layout interfaces, typed extractors) and everything in §3+. This roadmap assumes the branch is the new baseline.
Ground rule: rewrite from scratch is on the table
Given the depth of §1–§3, a greenfield rewrite (new submodule tree built alongside, legacy deleted at the end) is an acceptable — possibly preferable — alternative to an in-place migration. The precondition is a regression safety net written first:
test/problems/suite: for each (problem × scheme × backend) combination, record objective value (rtol), convergence status, and iteration count from the current release; the rewrite must reproduce them.test/benchmark.jlsuccess counts and timings) recorded onmainbefore any rewrite; midpoint/trapeze fast paths are the performance sentinels.solve-level via CTSolvers/OptimalControl), so they survive any internal reorganization.With that net in place, the incremental-vs-greenfield choice becomes tactical, not risky; §14 items 2–3 (new-style unit/contract tests) then accompany the new code rather than the legacy one.
Current pain points (evidence)
if-elsescheme dispatch in theDOCPconstructor (DOCP_data.jl:307-349) — closed to extension; the schemeSymboltravels far beyond the API boundary (primitive obsession).docp.time.steps,docp.dims.NLP_x,disc._step_variables_block, hand-computed offsets in every getter/setter and sparsity pattern (ode/common.jl:113-170) — no layout abstraction, no interface.getter(nlp_solution, docp; val::Symbol)(ode/common.jl:7-104), with fragileoccursin("_l", …)string matching.backend == :manualconflates two orthogonal choices (provide sparsity patterns vs. pick AD backends) (collocation.jl:112-134).:exabackend (onepass.jl) and only invoked here viaCTModels.get_build_examodel(ocp)— a full duplicate of the discretization logic, owned by the wrong package (§5).MidpointvsGauss_Legendre_1,Gauss_Legendre_2vsGauss_Legendre_2_Stagewise— each scheme re-implements layout, bounds, initial guess, integral, constraints and both sparsity patterns.ode/variable.jlexcluded from includes with debug remnants;DirectShootingExa builders empty.1. Architecture overhaul — submodules per responsibility
Reference: Handbook modules.md. Status: started on
refactor/docp-dispatch(dispatch contract); submodule split not started. This is the structural half of the redesign; §2–§3 are the conceptual half. Do them together, incrementally.Goal: move from the flat
src/*.jllayout to one submodule per responsibility,src/<Name>/<Name>.jl, exporting its public API, with the top-levelCTDirect.jlexporting nothing (onlyinclude+using .Submodule), qualified imports everywhere, and a strict dependency DAG.Proposed decomposition (to refine while implementing §3)
Schemesode/{euler,midpoint,trapeze,irk,irk_stagewise}.jlscheme dataLayouts_step_variables_block/ offset arithmetic scattered inode/*TranscriptionsDOCP_functions.jl,DOCP_variables.jl, per-schemestepStateConstraints!/integral, CTParser's:exacodegenSparsityadd_nonzero_block!,DOCP_*_patternGridsDOCPtime,get_time_gridDiscretizersCollocation,DirectShooting) and thediscretize/build_model/build_solutioncontract methodscollocation.jl,direct_shooting.jlSolutionsbuild_OCP_solution,getter,get_time_grid_exaRules to enforce (from the Handbook): naming conventions
symbol/_helper/__default, no bareusing Pkg, qualification at call sites, one-way DAG (Schemes→Layouts→Transcriptions→Discretizers, withSparsity,Grids,Solutionsas leaves/mid-levels).Migration strategy: two admissible paths, both gated by the regression safety net (see Context ground rule): (a) incremental — introduce the submodules around the new abstractions of §3 while the legacy flat code keeps working, migrate scheme by scheme (start with Euler — simplest layout — then trapeze/midpoint, then IRK), delete the legacy path last; or (b) greenfield — build the new submodule tree from scratch next to the legacy code and switch over once the golden tests pass. In both cases benchmarks (
test/benchmark.jl) are run against the recorded baseline at each step: the current midpoint/trapeze implementations are performance-tuned and the redesign must not regress them.2. Types, traits, interfaces — kill symbol dispatch and field access
Reference: Handbook types-traits-interfaces.md. Status: not started. Feeds directly into §3.
2.1 Scheme identity is a type, not a
SymbolThe user-facing
Symbol(:midpoint,:gauss_legendre_2) is kept only at the API boundary and converted once, via a registry (scheme_type(::Symbol)or theCTBase.Strategiesregistry — see the cross-cutting section), into a scheme type/instance. TheDOCPif-elsechain (DOCP_data.jl:307-349) disappears; adding a scheme = adding one file + one registration, no core edit (OCP principle). This also opens the door to passing a scheme object directly (aButcherTableau, §4).2.2 Traits for scheme properties
Properties currently encoded as booleans/fields (
_final_control, explicit vs implicit Euler flag, constant-control vs stagewise IRK) become traits / type parameters with extractors, dispatched via the Holy-trait pattern:control_parametrization(scheme)→StepwiseControl()/StagewiseControl()/ (laterPiecewiseLinearControl(), …)has_final_control(scheme),is_explicit(scheme),stage_count(scheme),order(scheme)2.3 Interfaces instead of field access
No consumer reads
docp.time.stepsordisc._step_variables_blockdirectly. Define accessor interfaces (steps(grid),state_dim(dims),state_range(layout, i),control_range(layout, i, j),stage_range(layout, i, j), …) withNotImplementedstubs on the abstract types so a new scheme that forgets a required method fails loudly. This is what makes sparsity patterns (§6) and typed extractors (§9) writable generically: the index ranges come from the layout object, used both by evaluation getters and by pattern builders (this is exactly the "Utils: discretization dependent getters that return the indices range" item of #383).2.4 Typed accessors for post-processing
Replace
getter(nlp_solution, docp; val::Symbol)by dispatch on small singleton types (StateVal,ControlVal,CostateVal,PathMultVal, …) or plain named functions, with backend (ADNLP / Exa) selected by a wrapper type — eliminating theisnothing(exa_getter)guards and theoccursinstring matching.2.5 Cleanups from #594 folded in here
DOCPimmutable (DOCPboundsvectors already carry the mutation).— done onexa_getterclosure mutationrefactor/docp-dispatch: the getter travels in the immutableExaBuildCacheinsideCTSolvers.BuiltModel; the builder closures are replaced by theCTSolvers.discretize/build_model/build_solutiondispatch contract.errorfallbacks for unsupported backend/discretizer combinations (LSP):DirectShooting+ Exa must fail loudly, not returnnothing(with the dispatch contract, this is now a missing-method / explicit-stub story onCTSolvers.build_model).ode/variable.jldebug remnants; hideGauss_Legendre_1(test-only) from the public surface.build_OCP_solutiondecomposed (_extract_primal,_extract_dual); dead code and docstring arities fixed.3. The transcription recipe — abstract the founding ingredients
Status: the heart of the roadmap. References: #583, #525, CTFlows'
_core_invoke_flowas the style model.Goal: identify and reify the ingredients of a direct transcription so that building a DOCP is a generic recipe over abstract types, analogous to CTFlows:
3.1 The ingredients to abstract
K_i^jfor IRK) + static variablev. #583 explicitly asks to investigate the alternative parametrization with stage statesx_ijas unknowns instead of slopesf_ij. Make "the set of unknowns" a first-class concept so both parametrizations are recipes.xuand each equation lives inc(§2.3). One layout object per (scheme × control-parametrization), queried by index-range accessors; the offset arithmetic exists in exactly one place.t_i, at stage timest_i^j, and at arbitraryt. #525 asks to standardize getters with both discrete (step index) and continuous (time value) variants; Stage-wise controls for IRK methods #583 raises the question of which control the solution reports (averaged vs stage controls) and where path constraints are evaluated (step times with first-stage control, averaged control, or stage times). These are evaluation policies — small strategy objects, not hard-coded choices.x_{i+1} = x_i + h_i Σ b_j K_i^j, stage equations). Generic version driven by the Butcher tableau; specialized versions may override (see 3.3).b,c), reusing dynamics work where possible (the trapezeintegral/work-array inconsistency noted in Constructive Code Review #594 §3.3).setWorkArraygeneralized; fix the per-call allocations in the IRKintegral, Constructive Code Review #594 §3.1).Plus one dimension, not an ingredient but a target: the recipe must be expressible toward two NLP emission backends — in-place Julia callables (
f,c!) for ADNLPModels, and generator expressions for ExaModels (§5). The design note must treat "how a recipe emits its NLP" as an explicit axis from day one, otherwise the Exa repatriation (§5) will not fit afterwards.3.2 Generic recipe on abstract types
Write the constraints/objective/bounds/pattern code once, against the layout + scheme interfaces (a generic IRK with tableau
(A, b, c)covers Euler explicit/implicit, midpoint, GL2, GL3, and any user tableau). The existingGenericIRK/GenericIRKStagewiseare the seed of this; the generic path becomes the default, not a variant.3.3 Optimized specializations
The generic path must remain overridable per scheme through ordinary dispatch: midpoint can avoid computing things twice (single stage,
x_s = (x_i+x_{i+1})/2shared between defect and quadrature — this is why midpoint.jl is "much faster than the version using stage variables"); trapeze can reuse endpoint dynamics across neighbouring steps. Rule: specialize a method of the recipe (defect!,integral,setWorkArray, patterns), never fork a whole parallel implementation. Benchmarks guard the specializations.3.4 Deliverable order
.reports/) fixing the ingredient interfaces (names, signatures, contracts) — including the ADNLP/Exa emission axis — reviewed before code.Gauss_Legendre_1, constant-control IRK variants become tableau instances with aStepwiseControlpolicy).4. Butcher tableau as user input
Status: not started; blocked by §3 (needs the generic recipe). Goal: the user can pass a Butcher tableau directly to the discretizer:
ButcherTableaulives inSchemeswith validation (sizes,c = A·1check optional/warn, explicit/implicit detection, stiffly-accurate detection, order if provided).gauss_legendre(2),radau_iia(3),lobatto_iiia(2),midpoint(), …) — the catalogue is data, not code.schemeoption type inStrategies.metadatawidens fromSymboltoUnion{Symbol, Schemes.AbstractScheme}.control = :stepwise | :stagewise), replacing the ad-hoc:gauss_legendre_2_constant_controlsymbol aliases.5. Own the ExaModels transcription in CTDirect (move it out of CTParser)
Status: design item, not started; important. Where it lives today: CTParser's
:exaparsing backend (onepass.jl) generates the whole ExaModels transcription at@def-parse time — variables with grid layout (p_state_exa!,p_control_exa!), per-scheme dynamics defects (p_dynamics_exa!::euler,:euler_implicit,:midpoint,:trapezehard-codedif scheme == …chains at onepass.jl:918-941), Lagrange quadrature (p_lagrange_exa!), constraints. The generated builder is stored in the model and CTDirect merely callsCTModels.get_build_examodel(ocp)(collocation.jl:209).Why this is wrong: the discretization of an OCP is CTDirect's raison d'être, yet for the Exa backend it is implemented — a second time, with different scheme semantics — inside the parser. Consequences:
time_grid, no path/boundary-constraint multipliers in solutions,DirectShootingunsupported.@def-parsed problems — a functional OCP built directly against CTModels has no ExaModel.Goal: CTDirect builds the
ExaModels.ExaModelitself, as the second emission target of the §3 recipe; CTParser's:exabackend is reduced (then retired) once parity is reached.The design question: how CTDirect emits Exa generators
ExaModels wants generator expressions over index sets (SIMD-friendly), not opaque closures — that is precisely why the codegen ended up in the parser. Two candidate routes, to be settled in the §3 design note:
Nodetypes) inside generators. Works when the functions are written generically (type-generic, noVector{Float64}assumptions); the "1-D = scalar" convention (§7) and the in-place buffer contract need special care (Exa is functional-style, not in-place). Likely requires an out-of-place variant of the dynamics from CTModels, or per-coordinate access.Route 1 is the cleaner target (no codegen, works for non-parsed OCPs); route 2 is the pragmatic fallback if tracing hits ExaModels limitations (control-flow in user functions, non-scalar indexing). Prototype route 1 on a small problem (double integrator, then Goddard) early, since the outcome shapes the §3 emission axis.
Work items (Exa repatriation)
ExaModels.constraint/objectivegenerators for Euler and trapeze; compare against the CTParser-generated model (same NLP dimensions, same solution).get_build_examodelbecomes, what IR or function variants CTModels must carry).CTSolvers.build_model(dm, ig, ::Modelers.Exa); reuseExaBuildCache.:exabackend once golden tests pass on both paths; keep it only as a comparison oracle during the transition.test_gpu.jl) run on the new path before the switch.6. Sparsity & AD: separate "pattern provision" from "backend choice"
Status: not started; small and independent (can be done before §3, then re-based on it). Issues: #383, #385.
Today collocation.jl:112-134:
backend == :manualsimultaneously (a) injects the hand-built Jacobian/Hessian patterns and (b) hard-picksReverseDiffADGradient/SparseADJacobian/SparseReverseADHessian. Two orthogonal axes, one knob.Work items (sparsity/AD)
sparsity = :manual | :detected(who provides the patterns: CTDirect's structural knowledge vs. SparseConnectivityTracer detection) ×ad_backend = :default | :optimized | …(ADNLPModels preset or explicit backend types). Any combination valid; document defaults. Coordinate with CTSolvers, which owns thebackendoption surface.add_nonzero_block!duplicate symmetric-diagonal entries (Constructive Code Review #594 §4.2) while moving it intoSparsity.7. Dimension and shape — "1-D = scalar"
Reference: Handbook dimension-and-shape.md. Status: not compliant (getters return views/vectors regardless of dimension).
CTDirect is a caller of user OCP functions: it must pass scalars for 1-D state/control/variable to dynamics, costs and constraints, with coercion driven by the declared dimension (
onlywhendim == 1,identityotherwise), never re-wrapping a scalar into a 1-vector; the in-place derivative buffer stays a length-nvector. CTFlows shipped this ("1-D = scalar" refactor,_coerce_state/_dim_coerceat the CTModels boundary) — reuse the same approach and vocabulary.Work items (shape audit)
CTModels.dynamics/lagrange/mayer/*_constraints_nl— apply dimension-driven coercion onxi,ui,v(currently raw views).Solution(coordinate with CTModelsbuild_solution).AbstractVectorfor n-D, under AD too (Duals areNumbers).8. Initial guess — pluggable and comparable strategies
Issue: #398. Status: open. Today: flat
0.1for everything (DOCP_variables.jl:126, magic constant flagged in #594 §2.6).Design split (two levels):
(x_f − x_0)/v_0-style estimates, probably user-hinted rather than automated). This extends the existingCTModels.AbstractInitialGuess/build_initial_guessmachinery.K_i^j: e.g. initialize withf(t_i^j, x̂_ij, û_ij, v̂)evaluated on the interpolated guess, instead of0.1), via the layout interface (§3.1, ingredient 7).Comparability requirement: initial-guess strategies must be first-class values (strategy objects or named policies) so different choices can be benchmarked against each other (iterations to convergence over the
test/problems/suite). Add a small comparison harness to the benchmark tooling; keep:constant(current behaviour) as the baseline.9. Solution building & outputs
Issues: #405 (path-constraint duals ÷ step length — the ADNLP path now normalizes in DOCP_data.jl:596-602; verify sign/convention against the reference test and close), plus #594 §2.1/§2.5/DIP. Status: partially done, needs consolidation.
Work items (solutions)
RawNLPResult-style adapter isolating theSolverCorefield-layout dependency.b_j— current — vs. stage controls on the stage time grid; the new multi-gridCTModels.build_solutionsupports distinct grids, so exposing stage controls is feasible). Follows the evaluation-policy decision of §3.1.PonT[1:end-1]; revisit conventions (per-scheme costate reconstruction is an "algorithm/pattern" in the sense of Stage-wise controls for IRK methods #583 — e.g. midpoint duals live at midpoints).10. Direct multiple shooting — control/state parametrization decoupled from the ODE scheme
Issue: #525. Status: basic prototype done (fixed-step schemes with
control_stepssub-controls per step); rework pending, re-based on §3.The prototype revealed exactly the abstractions §3 must provide:
control_stepsleaked intoDOCPtime,Midpoint.integralgrew a two-branch special case, and the control time grid is rebuilt by hand inbuild_OCP_solution. After §3:kpieces per shooting interval, later piecewise-linear/polynomial), independent of the integration scheme used inside the interval.ode/variable.jlbehind this design instead of its current half-finished state.DirectShooting(after §5; until then, an explicit loud "unsupported" — never silentnothing).11. Free node times, grid rebalancing, mesh refinement
Status: new design item, not in the issue tracker yet. Blocked by §3 (grids and layouts must be first-class).
Two related capabilities, both to be formulated as generic recipes:
t_iare unknowns (beyond freet0/tf), the grid itself enters the NLP (with monotonicity constraints and step-size bounds).Gridsmust support a grid whose nodes are (partially) decision variables; the recipe readsh_ithrough the grid interface, so the same defect/quadrature code serves fixed and free grids. Natural applications: switching-time optimization, impulsive problems (§12).Deliverable: a design note first (interfaces between
Grids, the refinement loop, and the warm start), then a prototype on a 1-D problem with a known solution structure (e.g. bang-bang switch localization).12. Impulsive case
Issue: #358 (refs OptimalControl#386). Status: open, not started; needs CTModels support first (how an impulsive OCP is even modelled: jump maps / impulse controls at (free) instants).
From the CTDirect side, an impulsive transcription = state jumps at selected nodes:
x_{i+1}^+ = x_i^- + Δ(x_i, w_i)alongside the continuous defects — i.e. a per-node jump ingredient in the recipe, plus free jump instants via §11's free-node machinery. Scope for this roadmap: (a) coordinate the modeling layer with CTModels, (b) prototype a fixed-impulse-time transcription as a recipe extension, (c) free impulse times after §11.13. Documentation — Vitepress migration + transcription explained
Reference: Handbook VITEPRESS-DOC.md. Status: not started (docs are plain Documenter with
index.md+ API reference).make.jlformat swap,generate_template,config.mtspatches, CI workflow update, LiveServer preview).CTDirect, fixed docstring examples (DOCPdimsarity,DOCPtimefields — Constructive Code Review #594 §2.2/§5.1); consider doctests in CI.14. Test suite restructuring
Reference: Handbook testing.md. Status: not compliant. Current layout:
test/ci/*.jlscripts included fromruntests.jl, plusarchives/,tmp/,jump/folders,.jld2/.jsonfixtures at the root.test_<name>()entry redefined at outer scope, top-level fakes,VERBOSE/SHOWTIMINGfromMain.TestOptions.src/layout):suite/schemes/(tableau validation, order checks),suite/layouts/(index-range accessors — pure unit tests, no solver),suite/transcription/(objective/constraints values on hand-checkable problems, ADNLP/Exa cross-checks),suite/sparsity/(patterns ⊇ detected patterns; manual vs detected equivalence of solutions),suite/solutions/(extractors, duals, grids),suite/discretizers/(end-to-end problem suite — the currenttest/problems/convergence tests remain the integration layer, extended into the golden regression harness of the Context ground rule),suite/shape/(1-D = scalar contract tests, §7).FakeScheme/FakeLayoutimplementing only the required interface methods validates the §2.3 contracts and error stubs (NotImplementedfor missing methods, loud errors for unsupported backend combos).archives//tmp/from the repo or move out oftest/; make GPU tests a guarded suite as today but with the same file template; keep the benchmark harness separate from correctness tests, extended with the initial-guess comparison runs (§8).Cross-cutting decision: strategy granularity (
CTBase.Strategies)In control-toolbox vocabulary, every swappable choice is a strategy in the broad sense — there is no separate "algorithm" concept. The actual decision, to be made once, up front, is one of machinery granularity: which variation points get the full
CTBase.Strategiestreatment (subtype ofAbstractStrategy,Strategies.metadatawith validatedOptionDefinitions, registry entry,describeintrospection) and which remain lightweight strategy types — plain singleton/policy types or type parameters dispatched internally (Holy traits), with no metadata/registry because nothing about them is user-tunable. Therefactor/docp-dispatchbranch already commits toCTBase.Strategiesas the machinery; the question is how far down it goes, and the answer shapes §3–§11.Proposed criterion: full
CTBase.Strategiesmachinery when the choice is (a) user-selectable fromsolve(...), (b) carries its own options needing validation/defaults/aliases, or (c) forms an extension boundary (weak dep, third-party addition). Lightweight type otherwise — an internal dispatch axis of the recipe that no user ever names.Collocation,DirectShooting):midpoint, tableau, §4)Collocation. Registering schemes in aStrategiesregistry gives symbol→type resolution anddescribeof the catalogue for free; the alternative is widening the option toUnion{Symbol, Schemes.AbstractScheme}. Decide in the design noteModelers.ADNLP/Modelers.Exa; CTDirect adds methods, not knobsA lightweight type can be promoted later to a full strategy if it grows options or becomes user-facing — the criterion is not identity ("is it a strategy?" — everything is) but machinery cost/benefit. Err toward the full machinery when in doubt: registration is cheap and
describedocuments the catalogue.Use more of the existing
CTBase.Strategiessurface than today: the registry (create_registry,type_from_id) should replace the scheme symbol→type lookup of §2.1 rather than a hand-rolledDict;describegives free introspection/documentation of available schemes and discretizers;RoutedOption/route_tocovers options that must flow from OptimalControl down to the discretizer or modeler. A short design note should pin this table down (with JB/PM) before §3 implementation starts, so strategy boundaries and recipe interfaces are decided together.Priority summary
Priority 0 (before anything else):
main. Precondition for either migration path, and the enabler of a from-scratch rewrite.refactor/docp-dispatch— the dispatch-contract baseline this roadmap builds on.High priority (the core redesign):
CTBase.Strategiesmachinery) + §3 ingredient interfaces design note (including the ADNLP/Exa emission axis) — write and review together before code.Medium priority:
:exadeprecation.Lower priority (unblocked later):
Dependency graph
Carried over / recorded
Gauss_Legendre_1visibility, IRK allocation fixes, trapeze work-array reuse) are absorbed into §1–§3 migration steps — treat the Constructive Code Review #594 summary tables as the checklist during migration.test/benchmark.jlresults (success count / timings) recorded before and after each migration step; the midpoint/trapeze fast paths are the regression sentinels.build_modelper modeler), backend/sparsity option surface,BuiltModelcaches.build_solution(§9), out-of-place dynamics variant or expression IR for Exa tracing (§5), impulsive modeling (§12).:exaparsing backend andget_build_examodel(§5) — parser describes the problem, CTDirect owns the discretization.Beta Was this translation helpful? Give feedback.
All reactions