Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,84 @@ and provides migration guides for users upgrading between versions.

---

## v0.4.23-beta (2026-06-27)

**Breaking change:** `build_model` returns an immutable `BuiltModel` bundle (not a
bare NLP), `build_solution` dispatches on that bundle, and `extract_solver_infos`
moved from `Optimization` to `Solvers`.

### Summary - v0.4.23-beta

- `Optimization.build_model(prob, init, modeler)` returns
`Optimization.BuiltModel{problem, nlp, cache}` instead of a bare NLP model.
- `Optimization.build_solution(built, stats, modeler)` dispatches on the
`BuiltModel` (previously `build_solution(prob, stats, modeler)`).
- `extract_solver_infos` now lives in `Solvers` (`Solvers.extract_solver_infos`).
- `KernelAbstractions` is again a hard dependency, so `Modelers.Exa(...)` only needs
`using ExaModels` (no longer also `using KernelAbstractions`) — a relaxation of
the v0.4.22 requirement.

### Breaking Changes - v0.4.23-beta

#### 1. `build_model` returns a `BuiltModel`

**Before:**

```julia
nlp = Optimization.build_model(prob, init, modeler) # bare NLP
```

**After:**

```julia
built = Optimization.build_model(prob, init, modeler) # BuiltModel
nlp = built.nlp # or DOCP.nlp_model(prob, init, modeler)
```

#### 2. `build_solution` dispatches on the `BuiltModel`

**Before:**

```julia
sol = Optimization.build_solution(prob, stats, modeler)
```

**After:**

```julia
built = Optimization.build_model(prob, init, modeler)
stats = solve(built.nlp, solver)
sol = Optimization.build_solution(built, stats, modeler)
```

Packages implementing the contract (e.g. discretizers) must now have `build_model`
return `BuiltModel(prob, nlp, cache)` and define
`build_solution(built::BuiltModel{<:MyProblem}, stats, modeler)`.

#### 3. `extract_solver_infos` moved to `Solvers`

**Before:**

```julia
Optimization.extract_solver_infos(stats)
```

**After:**

```julia
Solvers.extract_solver_infos(stats) # CTSolvers.extract_solver_infos also resolves
```

### Migration - v0.4.23-beta

- Replace `build_model(...)` usages that expect a bare NLP with `build_model(...).nlp`
or `DOCP.nlp_model(...)`.
- Update `build_solution` callers and implementers to dispatch on the `BuiltModel`.
- Replace `Optimization.extract_solver_infos` with `Solvers.extract_solver_infos`.
- `Modelers.Exa(...)` no longer requires `using KernelAbstractions`.

---

## v0.4.22-beta (2026-06-27)

**Breaking change:** `NLPModels`, `ADNLPModels`, `ExaModels`, and `KernelAbstractions` are
Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [0.4.23-beta] - 2026-06-27

### Changed

- **Immutable `BuiltModel` bundle** — `Optimization.build_model` now returns an
immutable `BuiltModel{problem, nlp, cache}` (new type) instead of a bare NLP, and
`Optimization.build_solution` dispatches on that bundle. This removes the mutable
backend cache: the Exa getter — produced together with the `ExaModel` and
previously mutated into the shared `DiscretizedModel` — now travels immutably
inside the `BuiltModel`. It also fixes a latent bug where two builds on the same
`DiscretizedModel` could clobber each other's getter.
- New `Optimization.BuiltModel` and `Optimization.NoCache`
(`<: CTBase.Core.AbstractCache`).
- `DOCP.nlp_model` returns the bare NLP (`build_model(...).nlp`);
`DOCP.ocp_solution` realigned onto `BuiltModel`.
- **`extract_solver_infos` moved** from `Optimization` to `Solvers`
(`Solvers.extract_solver_infos`); `CTSolvers.extract_solver_infos` still resolves.
- **`KernelAbstractions` promoted back to a hard dependency** so the
`CTSolversExaModels` extension triggers on `ExaModels` alone (partially reverting
v0.4.22). Downstream packages (e.g. CTDirect) no longer need
`using KernelAbstractions` to use the Exa modeler; Aqua `stale_deps` ignores it.

### Breaking

- `Optimization.build_model` returns a `BuiltModel`, not a bare NLP — use
`build_model(...).nlp` (or `DOCP.nlp_model`) to obtain the NLP.
- `Optimization.build_solution` now dispatches on the `BuiltModel`:
`build_solution(built, stats, modeler)` instead of `build_solution(prob, stats, modeler)`.
- `Optimization.extract_solver_infos` moved to `Solvers.extract_solver_infos`.
- See `BREAKING.md` for the full migration guide.

---

## [0.4.22-beta] - 2026-06-27

### Changed
Expand Down
9 changes: 4 additions & 5 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
name = "CTSolvers"
uuid = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6"
version = "0.4.22-beta"
version = "0.4.23-beta"
authors = ["Olivier Cots <olivier.cots@toulouse-inp.fr>"]

[deps]
CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd"
CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d"
CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c"
SolverCore = "ff4d7338-4cf1-434d-91df-b86cb86fb843"

[weakdeps]
ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a"
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9"
ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2"
KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c"
MadNCL = "434a0bcb-5a7c-42b2-a9d3-9e3f760e7af0"
MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6"
MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598"
Expand All @@ -29,7 +29,7 @@ Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"
CTSolversADNLPModels = "ADNLPModels"
CTSolversCUDA = "CUDA"
CTSolversEnzyme = "Enzyme"
CTSolversExaModels = ["ExaModels", "KernelAbstractions"]
CTSolversExaModels = ["ExaModels"]
CTSolversIpopt = ["NLPModels", "NLPModelsIpopt"]
CTSolversKnitro = ["NLPModels", "NLPModelsKnitro"]
CTSolversMadNCL = ["MadNCL", "MadNLP", "NLPModels"]
Expand Down Expand Up @@ -71,7 +71,6 @@ BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9"
ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2"
KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c"
MadNCL = "434a0bcb-5a7c-42b2-a9d3-9e3f760e7af0"
MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6"
MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598"
Expand All @@ -85,4 +84,4 @@ UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05"
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"

[targets]
test = ["ADNLPModels", "Aqua", "BenchmarkTools", "CUDA", "Enzyme", "ExaModels", "KernelAbstractions", "MadNCL", "MadNLP", "MadNLPGPU", "NLPModels", "NLPModelsIpopt", "OrderedCollections", "Random", "Test", "UnoSolver", "Zygote"]
test = ["ADNLPModels", "Aqua", "BenchmarkTools", "CUDA", "Enzyme", "ExaModels", "MadNCL", "MadNLP", "MadNLPGPU", "NLPModels", "NLPModelsIpopt", "OrderedCollections", "Random", "Test", "UnoSolver", "Zygote"]
2 changes: 1 addition & 1 deletion ext/CTSolversMadNCL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ A 6-element tuple `(objective, iterations, constraints_violation, message, statu
- `status::Symbol`: Termination status from SolverCore
- `successful::Bool`: Whether the solver converged successfully
"""
function Optimization.extract_solver_infos(nlp_solution::MadNCL.NCLStats)
function Solvers.extract_solver_infos(nlp_solution::MadNCL.NCLStats)
objective = nlp_solution.objective
iterations = nlp_solution.iter
constraints_violation = nlp_solution.primal_feas
Expand Down
2 changes: 1 addition & 1 deletion ext/CTSolversMadNLP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ A 6-element tuple `(objective, iterations, constraints_violation, message, statu
- `status::Symbol`: Termination status from SolverCore
- `successful::Bool`: Whether the solver converged successfully
"""
function Optimization.extract_solver_infos(nlp_solution::MadNLP.MadNLPExecutionStats)
function Solvers.extract_solver_infos(nlp_solution::MadNLP.MadNLPExecutionStats)
objective = nlp_solution.objective
iterations = nlp_solution.iter
constraints_violation = nlp_solution.primal_feas
Expand Down
7 changes: 3 additions & 4 deletions ext/CTSolversUno.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Implements the complete Solvers.Uno functionality with proper option definitions
module CTSolversUno

import DocStringExtensions: TYPEDSIGNATURES
import CTSolvers.Optimization
import CTSolvers.Solvers
import CommonSolve
import CTBase.Strategies
Expand Down Expand Up @@ -337,7 +336,7 @@ Solve an NLP problem using Uno.
# Returns
- `UnoSolver.UnoExecutionStats`: Solver execution statistics

See also: [`solve_with_uno`](@ref), [`Optimization.extract_solver_infos`](@ref)
See also: [`solve_with_uno`](@ref), [`Solvers.extract_solver_infos`](@ref)
"""
function CommonSolve.solve(
nlp::NLPModels.AbstractNLPModel, solver::Solvers.Uno; display::Bool=true
Expand Down Expand Up @@ -365,7 +364,7 @@ Solves the NLP problem using UnoSolver backend.
# Returns
- `UnoSolver.UnoExecutionStats`: Solver execution statistics

See also: `Solvers.Uno`, `UnoSolver.uno`, [`Optimization.extract_solver_infos`](@ref)
See also: `Solvers.Uno`, `UnoSolver.uno`, [`Solvers.extract_solver_infos`](@ref)
"""
function solve_with_uno(
nlp::NLPModels.AbstractNLPModel; kwargs...
Expand Down Expand Up @@ -397,7 +396,7 @@ A 6-element tuple `(objective, iterations, constraints_violation, message, statu

See also: [`solve_with_uno`](@ref)
"""
function Optimization.extract_solver_infos(nlp_solution::UnoSolver.UnoExecutionStats)
function Solvers.extract_solver_infos(nlp_solution::UnoSolver.UnoExecutionStats)
objective = nlp_solution.objective
iterations = nlp_solution.iter
constraints_violation = nlp_solution.primal_feas
Expand Down
26 changes: 14 additions & 12 deletions src/DOCP/building.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ $(TYPEDSIGNATURES)

Build an NLP model from a discretized optimal control problem.

This is a convenience wrapper around `build_model` that provides explicit
typing for `DiscretizedModel`.
This is a convenience wrapper around `build_model` that returns only the backend
NLP model (the `nlp` field of the [`BuiltModel`](@ref)). Use `build_model`
directly when the build-time cache is needed (e.g. before `build_solution`).

# Arguments
- `prob::DiscretizedModel`: The discretized OCP
Expand All @@ -24,25 +25,25 @@ typing for `DiscretizedModel`.
nlp = nlp_model(docp, initial_guess, modeler)
```

See also: `ocp_solution`, `Optimization.build_model`
See also: `ocp_solution`, `Optimization.build_model`, `Optimization.BuiltModel`
"""
function nlp_model(
prob::DiscretizedModel, initial_guess, modeler::Modelers.AbstractNLPModeler
)
return build_model(prob, initial_guess, modeler)
return build_model(prob, initial_guess, modeler).nlp
end

"""
$(TYPEDSIGNATURES)

Build an optimal control solution from NLP execution statistics.

This is a convenience wrapper around `build_solution` that provides explicit
typing for `DiscretizedModel` and ensures the return type
is an optimal control solution.
This is a convenience wrapper around `build_solution` that dispatches on the
[`BuiltModel`](@ref) returned by `build_model` and ensures the return type is an
optimal control solution.

# Arguments
- `docp::DiscretizedModel`: The discretized OCP
- `built::BuiltModel`: The built model bundle returned by `build_model`
- `model_solution::SolverCore.AbstractExecutionStats`: NLP solver output
- `modeler`: The modeler used for building

Expand All @@ -51,15 +52,16 @@ is an optimal control solution.

# Example
```julia
sol = ocp_solution(docp, nlp_stats, modeler)
built = build_model(docp, initial_guess, modeler)
sol = ocp_solution(built, nlp_stats, modeler)
```

See also: `nlp_model`, `Optimization.build_solution`
See also: `nlp_model`, `Optimization.build_solution`, `Optimization.BuiltModel`
"""
function ocp_solution(
docp::DiscretizedModel,
built::Optimization.BuiltModel,
model_solution::SolverCore.AbstractExecutionStats,
modeler::Modelers.AbstractNLPModeler,
)
return build_solution(docp, model_solution, modeler)
return build_solution(built, model_solution, modeler)
end
22 changes: 14 additions & 8 deletions src/Modelers/abstract_modeler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ Abstract base type for all modeler strategies.

Modeler strategies are responsible for converting discretized optimization
problems (`Optimization.AbstractOptimizationProblem`) into NLP backend models.
They implement the `Strategies.AbstractStrategy` contract and provide callable
interfaces for model and solution building.
They implement the `Strategies.AbstractStrategy` contract together with named
model- and solution-building methods.

# Implementation Requirements
All concrete modeler strategies must:
- Implement the `Strategies.AbstractStrategy` contract
- Have the package providing the problem implement, by multiple dispatch on
`(prob, modeler)`:
- `Optimization.build_model(prob, initial_guess, modeler)`
- `Optimization.build_solution(prob, nlp_solution, modeler)`
- Have the package providing the problem implement, by multiple dispatch:
- `Optimization.build_model(prob, initial_guess, modeler)` returning a
`Optimization.BuiltModel`
- `Optimization.build_solution(built::Optimization.BuiltModel, nlp_solution, modeler)`

# Example
```julia
Expand All @@ -29,9 +29,15 @@ end
Strategies.id(::Type{<:MyModeler}) = :my_modeler

# In the package providing the concrete problem type:
function Optimization.build_model(prob::MyProblem, initial_guess, ::MyModeler)
function Optimization.build_model(prob::MyProblem, initial_guess, modeler::MyModeler)
# Build NLP model from problem and initial guess
return nlp_model
nlp = ...
return Optimization.BuiltModel(prob, nlp, Optimization.NoCache())
end

function Optimization.build_solution(built::Optimization.BuiltModel{<:MyProblem}, nlp_solution, ::MyModeler)
# Reconstruct the problem-level solution from built and nlp_solution
return solution
end
```

Expand Down
11 changes: 7 additions & 4 deletions src/Optimization/Optimization.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,29 @@ This module defines the abstract optimization problem interface
contract (`build_model` / `build_solution`). Concrete problem types (e.g.
`DiscretizedModel`) and the packages providing them implement these by multiple
dispatch on `(problem, modeler)`.

Solver-side utilities (e.g. `extract_solver_infos`) live in `Solvers`.
"""
module Optimization

# Imports
import CTBase.Core
import CTBase.Exceptions
import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES
using SolverCore: SolverCore

# Submodules
include(joinpath(@__DIR__, "abstract_types.jl"))
include(joinpath(@__DIR__, "built_model.jl"))
include(joinpath(@__DIR__, "building.jl"))
include(joinpath(@__DIR__, "solver_info.jl"))

# Public API - Abstract types
export AbstractOptimizationProblem

# Public API - Built model bundle
export BuiltModel, NoCache

# Public API - Model building functions
export build_model, build_solution

# Public API - Solver utilities
export extract_solver_infos

end # module Optimization
Loading