Skip to content

v2.1.0-beta: dependency upgrade, CTLie integration, test rework - #826

Merged
ocots merged 21 commits into
mainfrom
feat/upgrade-v2.1.0-beta
Jul 30, 2026
Merged

v2.1.0-beta: dependency upgrade, CTLie integration, test rework#826
ocots merged 21 commits into
mainfrom
feat/upgrade-v2.1.0-beta

Conversation

@ocots

@ocots ocots commented Jul 29, 2026

Copy link
Copy Markdown
Member

Closes the load failure described in #815 and brings OptimalControl onto the restructured CTx stack.

Status: all phases (A–K) done and green. Full local suite: 2167 passed / 2 broken (expected) / 0 failed in 25m48s.

Context

OptimalControl was pinned at CTBase = "=0.18.8" / CTModels = "=0.10.1" while the ecosystem was reorganised around the Handbook modules.md rule. The package did not loadUndefVarError: AbstractDiscretizer not defined in CTDirect.

Three symbol families physically changed package:

From To What
CTFlows CTBase.Data the type vocabulary Flow dispatches on
CTFlows CTLie ad (was Lie), Lift, Poisson, ∂ₜ, @Lie
CTSolvers CTBase.{Strategies,Options,Orchestration} the whole strategy/option layer
CTDirect CTSolvers.DOCP AbstractDiscretizer, discretize

Full specification: .reports/upgrade-v2.1.0-beta.md

Commits, one per phase

Commit Checkpoint
feat(imports)!: migrate to the restructured CTx stack 67/67
test(reexport): assert ownership, not mere definedness 760/760
test(helpers,builders): re-point paths, consolidate CUDA checks 431 passed, 1 skip
test(solve): re-point paths, complete the strategy contract in mocks bypass 27/27 · canonical 252/252 · routing 75/75 · dispatch 102/102
test(indirect)!: migrate to the new flow calling convention Goddard shooting residual 1.19e-8
test(problems): dual-form library, declare direct/indirect capability 77 passed, 1 skip
test: form equivalence, hamiltonian_type, flow API, shape contract 323/323
fix: consume CTBase 0.28.9-beta — OpenLoop unconditionally non-autonomous (CTBase#515) full suite 2167/2169
ci: use ct-registry, add windows to the CPU matrix needed because this branch depends on beta packages not yet in General

Two traps worth flagging for review

Precompilation proves nothing here. Argument annotations are load-fatal, bodies fail only on first call — the package precompiles cleanly with half the migration missing. Every phase was verified by forcing the lazy bodies (registry, describe, print helpers) and running a real solve end to end.

isdefined is vacuously true for Base names. The reexport group asserted isdefined(OptimalControl, :foo), which passes via the implicit using Base for time, merge, status, name, value, describe, success. The group could go green while OptimalControl re-exported none of them. Replaced with ownership checks (test/helpers/reexport.jl).

Findings that were not in the plan

  • parameter is not a rename of get_parameter_type. The old one returned nothing for a strategy without a parameter; the new one throws NotImplemented. This broke option routing for every mock strategy in the suite, and made _extract_strategy_parameters crash on any third-party strategy that had not implemented the optional contract. Resolved via the new non-throwing CTBase.Strategies.parameter(strategy_type, default) accessor.
  • OpenLoop/ClosedLoop/DynClosedLoop are factory functions, not types. They all build a ControlLaw{F,Kind,…}; the kind is a trait parameter. Same for the three constraint kinds.
  • OpenLoop is now unconditionally non-autonomous — an open-loop control has nothing but time, so autonomy was never a real choice for it the way it is for ClosedLoop/DynClosedLoop. Raised neutrally as Should an autonomous OpenLoop control law take no argument at all? CTBase.jl#515, shipped in CTBase 0.28.9-beta (is_autonomous kept only as a warn-on-use misuse detector, no effect on OpenLoop). Companion fix for the 6 stale call sites in CTFlows: fix: OpenLoop is unconditionally non-autonomous (CTBase#515) CTFlows.jl#378.
  • Flow(ocp, ClosedLoop/OpenLoop) returns a state flow, not a PreconditionError as the report stated.
  • success is dead upstream. CTModels.Solutions exports the name but defines no method — success(sol) was always a MethodError. Dropped from the re-export; successful is the real accessor.
  • constraint/multiplier without its pair raises IncorrectArgument, not PreconditionError.
  • ocp_solution takes a BuiltModel, not a DiscretizedModel — the old assertion was wrong and passed by chance.
  • describe now covers the full strategy surface (:di, :sciml, …) via the new Base.merge(::StrategyRegistry...) in CTBase.

Test rework

Problems now build in two front-end forms (@def and the functional CTModels.Building API) and declare which solution methods they are fixtures for. The quadrotor has no exploitable extremal structure, so it is direct-only — and declaring :indirect without a reference p0 is rejected by the constructor, so the claim cannot rot.

Indirect test fixtures (Goddard, double integrator ×3) now carry their own shoot_builder closure, consumed generically by a shooting sweep and by the hamiltonian_type tests, rather than each problem re-deriving its own flows.

New groups cover what OptimalControl actually owns rather than duplicating upstream:

  • hamiltonian_type:total and :partial agree wherever the law is stationary for H̃, which is everywhere in the indirect suite. The discriminating case (a deliberately non-stationary law) shows they are not redundant, and why.
  • shape contract — deliberately not a copy of CTDirect's exhaustive per-scheme version. Ours is the contract surviving the whole solve stack, the same contract on the indirect path (CTFlows only has defensive isa Number ? … : …[1] guards, which tolerate either shape), and the two paths agreeing — the seam nothing upstream can check.
  • GPU routing — CPU-runnable dispatch assertions, with the actual device-tier checks marked @test_skip and gated behind the GPU-runner-required contract (kkt self-hosted runner).

Public API changes

See BREAKING.md. Lead item: Flow now requires an integrator using — it changes every example's preamble.

CI

Both CPU and GPU jobs now set use_ct_registry: true — this branch depends on beta versions (CTBase, CTFlows, CTSolvers, CTLie, ...) that only exist in the control-toolbox registry, not General. windows-latest added to the CPU runner matrix.

Out of scope, by design: docs/ (~24 stale pages, separate PR), the full 25-problem library behind a scheduled workflow, and GPU device-tier assertions (deferred to the GPU runner, marked @test_skip so they appear in the summary).

🤖 Generated with Claude Code

ocots and others added 5 commits July 29, 2026 10:15
The package did not load: `UndefVarError: AbstractDiscretizer not defined in
CTDirect`. Three symbol families physically changed package while
OptimalControl stayed pinned at CTBase 0.18.8 / CTModels 0.10.1.

Re-point every import at its *owning* submodule, per the Handbook `modules.md`
rule:

  CTFlows            → CTBase.Data      the type vocabulary `Flow` dispatches on
  CTFlows            → CTLie            ad / Lift / Poisson / ∂ₜ / @lie
  CTSolvers          → CTBase.Strategies, CTBase.Options, CTBase.Orchestration
  CTDirect           → CTSolvers.DOCP   AbstractDiscretizer, discretize
  CTModels           → Components / Models / Solutions / Building / Init

New `src/imports/{ctlie,adnlpmodels,ad}.jl`. The last two exist for their
imports alone: a `[deps]` entry arms no extension — Julia fires one when the
trigger package is *loaded*. ADNLPModels and DifferentiationInterface are in
`[deps]` precisely because CTSolvers' and CTBase's extensions need them, so we
must load them or ship a dead capability. `test/suite/extensions/` guards this.

Also re-points ~45 qualified sites across `src/helpers/` and `src/solve/`.
Argument annotations are load-fatal while bodies fail only on first call, so
precompilation proves nothing here; verified by forcing the lazy registry,
`describe`, the print helpers and a descriptive `solve` end to end.

Two API changes are not renames, and are handled rather than papered over:

- `CTSolvers.Strategies.get_parameter_type` → `CTBase.Strategies.parameter`
  returns `nothing` no longer: it throws `NotImplemented` when a strategy has
  not implemented the (optional) parameter contract. `print.jl` is display
  code and must not be what crashes on a third-party strategy, hence the new
  `_strategy_parameter` shim.
- `time` and `success` are dropped from the re-export surface. Both resolve to
  bare `Base` functions: `CTModels.Components` extends `Base.time` without
  exporting it, and `CTModels.Solutions` exports the *name* `success` while
  defining no method for it — `success(sol)` was always a `MethodError`. The
  real accessor is `successful`.

Refs #815

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Test.@test isdefined(OptimalControl, :foo)` is vacuously true for every name
that also exists in `Base` — `time`, `merge`, `status`, `name`, `value`,
`describe`, `success`. Julia resolves those through the implicit `using Base`,
so the whole reexport group could go green while OptimalControl re-exported
none of them. That is exactly the blind spot this migration walks through.

New `test/helpers/reexport.jl` with `reexports` / `imports` / `is_exported`,
which check the binding resolves to the object the owning submodule defines.

Rewrites `test_ctflows.jl` (nearly every assertion was stale) and
`test_ctsolvers.jl`, adds `test_ctlie.jl`, and reorganises `test_ctbase.jl` and
`test_ctmodels.jl` by owning submodule. `test_ctdirect.jl` is recentred on what
is still CTDirect's — the concrete discretizers — since the names it asserted
kept passing straight through the move to CTSolvers.DOCP without noticing it.

Three corrections to what the migration report assumed, found by running this:

- `OpenLoop`/`ClosedLoop`/`DynClosedLoop` and `StateConstraint`/
  `ControlConstraint`/`MixedConstraint` are *factory functions*, not types.
  They all build a `ControlLaw{F,Kind,…}` / `PathConstraint{F,Kind,…}`; the
  kind is a trait parameter. `OpenLoop <: AbstractControlLaw` is a `TypeError`.
- `Flow` is a `UnionAll`, not a `Function`.
- `ocp_solution` takes a `BuiltModel`, not a `DiscretizedModel`.

Refs #815

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mechanical re-pointing of ~113 stale qualified sites, plus the `CTBase` imports
the new paths need.

`is_parameter_type` is deprecated upstream in favour of `is_a_parameter`; switch
rather than emit depwarns.

The `parameter` assertions needed real work, not a rename: `parameter(ADNLP)`
throws `NotImplemented` where `get_parameter_type(ADNLP)` returned `nothing`. A
bare `UnionAll` genuinely does not determine a parameter — it is the
instantiated type that carries one. Assert both halves.

New `test/helpers/capabilities.jl`, mirroring the upstream `TestCapabilities`
pattern. `is_cuda_on` was defined three times independently (runtests.jl —
unused, test_canonical.jl, test_options_forwarding.jl); this is now the single
definition, and it separates two things that are easy to conflate:
`gpu_extension_armed()` (is the extension loaded — CPU-runnable) from
`is_cuda_on()` (is there a device).

The CUDA device tier now uses `Test.@test_skip` instead of a silent
`if is_cuda_on()`, so the skip appears in the summary rather than vanishing.

Refs #815

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mechanical re-pointing of ~152 stale qualified sites. The `const MOCK_REGISTRY`
in `test_descriptive_routing.jl` is load-fatal for the whole file, so it went
first.

The real find: every mock strategy was out of contract. `parameter` must now be
implemented by each strategy — the CTBase generic throws `NotImplemented` by
default, where the old `CTSolvers.Strategies.get_parameter_type` silently
returned `nothing`. `route_all_options` calls it, so option routing failed
outright and `solve_descriptive` errored on every bypass test. The real
strategies all implement it, which is why the end-to-end solve kept working and
the failure looked like a src regression.

`MockModelerParam`/`MockSolverParam` now report the parameter they actually
carry, rather than `nothing` — the old assertion only held because nothing
implemented the contract at all.

`test_canonical.jl` gains `using CUDSS`: since CTSolvers#189 the extension
trigger is `["MadNLPGPU", "CUDA", "CUDSS"]` — all three. Without it
`CTSolversMadNLPGPU` never loads and `MadNLP{GPU}` is not registered as a
strategy, silently. (CUDSS loads fine on macOS despite linux/windows-only
artifacts: the artifact is lazy.)

`test_explicit.jl` imported MadNLPGPU and CUDA but never used them; dropped.

Refs #815

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three user-visible breaks, exercised together on Goddard:

1. `Lie(X, f)` → `ad(X, f)`, from CTLie. `X ⋅ f` was dropped with no
   replacement. `Lift`/`@Lie` also moved to CTLie.
2. Constrained flows take *paired keywords*:
   `Flow(ocp, u; constraint=g, multiplier=μ)`. The three-positional form is a
   `MethodError`; one keyword without the other is an `IncorrectArgument`
   (the migration report said `PreconditionError` — it is not).
3. `variable=` is mandatory on a `NonFixed` flow. Goddard and the time-minimal
   double integrator both declare `tf ∈ R, variable`, so all four call sites
   need it; omitting it raises a `PreconditionError` rather than defaulting.

`Flow` also needs an integrator loaded now (Q7), hence the
`OrdinaryDiffEqTsit5` imports.

Goddard additionally needs `CTLie` and `CTBase` imported explicitly: `@Lie`
expands to bare `CTLie.*` and `CTBase.Traits.*` prefixes, and while
OptimalControl does re-export both, this file imports qualified
(`using OptimalControl: OptimalControl`), which brings in that one name only.
Same reason it used to carry `import CTFlows: CTFlows`.

Guard rails added for each break, so a regression fails loudly rather than
silently changing meaning.

Refs #815

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lity

The nine problems returned bare NamedTuples with **no common schema** — some
carried `F0`/`F1`, some `x0`/`xf`/`t0`/`tf`, some `p_expected`. Anything
generic over them had to guess.

`TestProblem` promotes the four fields every problem has and keeps the rest in
`data`, verbatim: no information was dropped, it moved one level down.

Each problem now builds two ways, `:abstract` (the `@def` DSL) and
`:functional` (the `CTModels.Building` API), which is what makes a
form-equivalence test possible at all. 18/18 build.

Also adds a `methods` field, prompted by review: not every problem can be
attacked both ways. The quadrotor has no exploitable extremal structure and no
reference costate, so it is a direct fixture only — and nothing said so. A
shooting sweep would have had to skip it by name, a list that rots.

The claim is self-enforcing rather than documentary: declaring `:indirect`
without a `p0` in `data` is rejected by the constructor. Selection goes through
`problems_for(:indirect)`.

    indirect: goddard, double_integrator_{time,energy,energy_constrained}
    direct:   all nine

The indirect tests now read `p0` and the switching times from `data` instead of
restating them, so the reference data cannot drift away from the model that is
supposed to satisfy it.

Two things the functional API insists on, both easy to get wrong:
`time_dependence!` is mandatory before `build`, and a control-free problem must
*omit* `control!` — `control!(pre, 0)` is rejected.

Refs #815

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ocots and others added 4 commits July 29, 2026 10:42
Four new groups, each covering a boundary OptimalControl owns rather than one
its dependencies already test.

**suite/problems/test_forms_equivalent.jl** — `@def` is sugar over
`CTModels.Building`, and if the sugar and the API drift apart one of them is
lying. One body over all nine problems: dimensions, horizon kind, cost,
dynamics at a sample point, constraint dimensions. The `definition` difference
is asserted *as* a difference rather than skipped.

**suite/problems/test_hamiltonian_type.jl** — `:total` and `:partial` agree
wherever the control law is stationary for H̃, which is everywhere in the
indirect suite, so it is easy to believe they are redundant. They are not, and
this pins down why: on the energy double integrator with u = p₂ + 1 (not the
minimiser),

    :partial → ẋ₂ = u = p₂ + 1        ("apply this feedback")
    :total   → ẋ₂ = u + (∂H̃/∂u)(∂u/∂p₂) = p₂   (the perturbation cancels)

Both correct for what they compute; picking the wrong one on a non-stationary
law silently integrates different dynamics. Also runs the stationary cases
across both front ends, including Goddard's constrained arc.

**suite/flows/test_flow_api.jl** — the constructor grid, the three `constraint`
spellings including the `Symbol` label that reuses the OCP's own declared
constraint, and the calling convention.

Two things found while writing it:

- `Flow(ocp, ClosedLoop/OpenLoop)` returns a *state* flow with no costate, not
  a rejection — the report had this as a `PreconditionError`.
- `is_autonomous` strips the time argument uniformly across law kinds, so an
  autonomous `OpenLoop` is called with **no arguments at all**. `OpenLoop(t ->
  0.0)` reads as correct and is a `MethodError` at integration time. The full
  arity table is now pinned; the design question is raised neutrally in
  control-toolbox/CTBase.jl#515.

**suite/shape/test_shape_contract.jl** — deliberately *not* a copy of
CTDirect's exhaustive per-scheme version. What is ours is the contract
surviving the whole `solve` stack, the same contract on the indirect path
(CTFlows has no assertion for it, only defensive `isa Number ? … : …[1]`
guards that tolerate either shape), and the two paths agreeing — the seam
nothing upstream can check.

`test/helpers/shooting.jl` ports `test_shooting` from CTFlows so the indirect
checks here ask the same two questions upstream does: residual at the known
solution (a wrong derivation still converges, just to the wrong thing), and
convergence from a perturbed guess.

Refs #815

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…v2.1.0-beta

`:cpu`/`:gpu` now means the same thing on both sides of the library, so the
routing is worth asserting on both — and all of it is CPU-runnable: resolving
a GPU strategy builds a parameterized type, it does not execute on a device.

Two things kept carefully apart, because conflating them is how a GPU suite
ends up green and inert:

  extension armed  — is `CTSolversMadNLPGPU` loaded? CPU-runnable, and the only
                     local evidence the GPU path is compiled in at all.
  device present   — is there a functional GPU? False on every local runner.

Since CTSolvers#189 the trigger is `["MadNLPGPU", "CUDA", "CUDSS"]` — all
three, or the extension stays inactive and `MadNLP{GPU}` is never registered,
silently. `MadNLPGPU.CUDSSSolver isa Type` is asserted outside any device gate.

The device tier has no honest local green, so it is `@test_skip` rather than an
elided `if is_cuda_on()` branch — a skip appears in the summary, a missing
branch does not.

One assertion here was self-sabotaging and is worth the comment it now carries:
`occursin("GPU", string(flow_type))` is vacuously true because the type string
embeds this module's own name, `TestGPURouting`. Matching `SciML{GPU` and
`DifferentiationInterface{GPU` instead also states the real claim — that one
token resolves *both* strategy families.

Docs cover the six user-visible breaks, leading with the one that changes every
example's preamble: `Flow` now requires an integrator `using`.

Refs #815

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adapts the runner contract from CTSolvers#190 (merged). Our device tier was
`@test_skip` unconditionally, so a `kkt` runner whose driver broke was
indistinguishable from a laptop: the GPU job would go green having run nothing.
`on_gpu_runner()` reads `RUNNER_NAME`, set by the Actions agent itself, and
turns the skip into an assertion on the one machine that must have a device.

Inert off the runner — local run unchanged at 31 passed, 1 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ocots and others added 2 commits July 29, 2026 20:53
…and :sciml

`describe(:id)` previously only knew the solve registry (discretizer, NLP
modeler, NLP solver) — `:di` and `:sciml`, from CTFlows' flow registry, were
unreachable through the single-argument convenience wrapper. The integrator
and the AD backend are strategies in the control-toolbox sense like any
other, so their options should be inspectable the same way (.reports item).

`get_full_strategy_registry()` merges the two registries rather than falling
back with try/catch: a fallback would swallow the genuine "unknown strategy"
error on a typo and surface whatever the second registry raised instead. The
merge is total here because the two registries are measurably disjoint — no
shared id, no shared family, `:cpu`/`:gpu` bound to the same types on both
sides — asserted in test_describe.jl rather than assumed.

Reaching into StrategyRegistry's two fields to build the union is deliberate
but temporary, and skips the validation create_registry performs within one
registry — exactly what a cross-registry union should re-check. Requested
upstream as CTBase#517 (Base.merge for StrategyRegistry); switch once
released.

Along the way: describe(:sciml) turns out to throw FieldError on any
registry that contains it, and takes describe(:cpu)/describe(:gpu) down with
it — CTBase's _strategy_base_name only handles strategies with at most two
type parameters, and SciML has four. Root cause and fix are upstream
(CTBase#516); CTSolvers#191 tracks the missing describe coverage for the
Integrators family that let it through. The three affected assertions here
are @test_broken, not skipped, so the fix landing flips them to unexpected
passes and fails the suite — the reminder to promote them.

Full suite: 23 passed + 3 broken (new file), no regressions in the
registry-dependent groups (helpers/print, helpers/registry, helpers/methods,
reexport/{ctbase,ctsolvers,optimalcontrol}: 552/552).

Refs: control-toolbox/CTBase.jl#516, control-toolbox/CTBase.jl#517,
control-toolbox/CTSolvers.jl#191

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Matches the actual call sites — typeof(discretizer)/typeof(modeler)/
typeof(solver) are always AbstractDiscretizer/AbstractNLPModeler/
AbstractNLPSolver, all <: CTBase.Strategies.AbstractStrategy.

Does not remove the need for the try/catch below it, and the docstring now
says so explicitly: `parameter` is optional-to-override on any
AbstractStrategy subtype, and Julia has no way to require an interface
method at the abstract type's definition site. A third-party strategy that
forgets the override still throws NotImplemented regardless of how tightly
T is bounded here — see helpers/print.jl#331-337.

test_print.jl: 87/87.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Not a silent absorption any more: a third-party strategy that never
implements `CTBase.Strategies.parameter` still falls back to `nothing`
(display must not crash the caller's solve over an optional, cosmetic
detail), but now says so once per type via `@warn ... maxlog=1`, keyed on
the strategy type so a solve loop doesn't spam.

Confirmed via the existing FakeDiscretizerNoParam/FakeModelerNoParam/
FakeSolverNoParam fixtures in test_print.jl, which already exercise this
exact path: one warning per type, test_print.jl still 87/87.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ocots and others added 2 commits July 30, 2026 08:48
Each indirect fixture (Goddard, the three double-integrator variants) now
carries its own shoot_builder — a closure of signature
`(; hamiltonian_type=:total) -> (shoot!, ξ_exact, ξ_guess)` — on TestProblem,
next to the problem itself. Quadrotor's stays `nothing`: no exploitable
extremal structure, and TestProblem's own constructor now enforces the claim
(`:indirect` requires shoot_builder !== nothing, not `data.p0`).

One generic suite/indirect/test_shooting_sweep.jl replaces three
hand-written files (test_goddard.jl, test_double_integrator_{time,energy}.jl),
looping over problems_for(:indirect) across both forms. It also adds the
Newton-from-a-perturbed-guess half none of the three ever exercised — only
the residual at the known reference was checked before.

Found along the way: the same derivations were ALSO written a second time in
suite/problems/test_hamiltonian_type.jl, independently. That file now
consumes the same shoot_builder(; hamiltonian_type=ht), keeping only what is
genuinely its own — sweeping :total/:partial and the "they agree/disagree"
assertions.

The one guard-rail assertion from the three deleted files that was not
already covered generically elsewhere (`variable=` mandatory when *omitted*
on a NonFixed flow) moved to test_flow_api.jl, next to its mirror image
("no variable on a Fixed flow") which already lived there. Everything else
they carried (constraint/multiplier pairing, trajectory form, Lift-vs-
Hamiltonian semantics) was redundant with existing generic coverage and is
gone rather than moved.

Full suite: 2166 passed, 5 broken (2 device/capability skips + 3 pre-existing
@test_broken in test_describe.jl, unrelated to this change), zero
regressions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eter

Three requests filed against CTBase while working on describe()/parameter()
support here (CTBase#516, #517, #518) all shipped in 0.28.8-beta. Consuming
them removes the temporary workarounds each one was standing in for:

- get_full_strategy_registry() now calls Base.merge(a::StrategyRegistry,
  bs::StrategyRegistry...) instead of reaching into .families/.parameters and
  merging the dicts by hand — the cross-registry validation create_registry
  already does within one registry now runs across the two being combined.

- _strategy_parameter() forwards to the new non-throwing
  Strategies.parameter(T, default), passing a dedicated sentinel
  (_PARAMETER_NOT_IMPLEMENTED) as default so the @warn path (a strategy that
  never implemented the contract) stays distinguishable from a strategy that
  legitimately declares no parameter — the one case the 2-arg accessor itself
  can't tell apart, since both collapse to `nothing`.

- describe(:sciml) no longer throws FieldError (CTBase#516 was
  _strategy_base_name breaking on strategies with 3+ type parameters). The
  three @test_broken in test_describe.jl — :sciml, :cpu, :gpu — are promoted
  to @test; WORKING/BROKEN collapses into a single ALL_STRATEGIES list.

CTBase compat was already "0.28" (minor-pin per the house rule), so no
Project.toml change; only the resolved version moved, 0.28.7-beta ->
0.28.8-beta.

Full suite: 2168 passed, 2 broken (the two legitimate device/capability
skips) — test_describe.jl 25/25, zero broken, down from 3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Consumes CTBase 0.28.9-beta: an open-loop control depends only on time,
u(t) (or u(t, v)) — autonomy is a property of the OCP, not of the control,
so OpenLoop no longer offers is_autonomous as a real choice the way
ClosedLoop/DynClosedLoop do. is_autonomous is kept as a misuse-detector
keyword that now warns rather than doing nothing.

test_flow_api.jl:
- "control-law kinds select different flows": drop the now-inert
  is_autonomous=false on the OpenLoop construction, rewrite the comment that
  described the old trap (an autonomous OpenLoop called with no arguments at
  all) since the trap no longer exists.
- "is_autonomous governs the law's arity": the OpenLoop row of the arity
  table loses its autonomous variant — there is only one spelling now. Added
  a dedicated regression test that the old zero-argument spelling
  (OpenLoop(() -> 0.0)) still fails, loudly, with MethodError at
  integration time rather than constructing silently.

BREAKING.md / CHANGELOG.md also completed with everything landed since the
first v2.1.0-beta entries were written: describe() now covers the full
strategy surface (:di, :sciml, merging the solve and flow registries via
CTBase's Base.merge), the non-throwing parameter(T, default) accessor
package authors can use instead of a hand-rolled try/catch, and this
OpenLoop change.

CTFlows' own six zero-argument OpenLoop test sites are fixed separately, on
their own branch/PR, not here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ocots
ocots marked this pull request as ready for review July 30, 2026 08:39
@ocots ocots added run ci kkt-runner Trigger CI on kkt runner run ci github-runner Trigger CI on GitHub runners labels Jul 30, 2026
Both jobs need use_ct_registry: true — this branch depends on beta versions
(CTBase, CTFlows, CTSolvers, CTLie, ...) that only exist in the
control-toolbox registry, not General. Drops the explicit julia `versions`
pin and adds windows-latest to the CPU job's runner matrix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ocots ocots removed the run ci kkt-runner Trigger CI on kkt runner label Jul 30, 2026
Julia 1.10's inference widens `_extract_kwarg`'s return type to
Union{Nothing,AbstractDiscretizer} instead of narrowing to the concrete
branch actually taken, making @inferred fail spuriously — a real
inference-precision difference between 1.10 and 1.11, not a bug in
_extract_kwarg. The allocation checks (@allocated) are unaffected and
still run unconditionally on both versions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Temporary — points the CPU job at fix/windows-pkg-cli-git instead of
main so this branch's Windows CI can confirm CTActions#67 before it
merges. Revert to @main once that PR lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ocots ocots added run ci kkt-runner Trigger CI on kkt runner run ci github-runner Trigger CI on GitHub runners and removed run ci github-runner Trigger CI on GitHub runners run ci kkt-runner Trigger CI on kkt runner labels Jul 30, 2026
ocots and others added 2 commits July 30, 2026 12:11
Windows CI confirmed green on fix/windows-pkg-cli-git (git_cli input on
julia-buildpkg). CTActions#67 is merged, so revert to @main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same cause as the kwarg-extraction @inferred guard: Julia 1.10 has a
measurably different allocation profile for the same code path on
Windows (25760 vs the 21648 this bound was set for; 128800 vs 120000
for the 5x loop) — not a regression, just version drift in these bounds.
Guard both assertions; the @allocated measurement itself still runs
unconditionally on both versions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ocots
ocots merged commit 82b2dee into main Jul 30, 2026
6 checks passed
@ocots
ocots deleted the feat/upgrade-v2.1.0-beta branch July 30, 2026 11:48
@ocots

ocots commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

Closes #815.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant