From 69a77eaa77aff3c80b9276ea2f1812eb3991b382 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sat, 27 Jun 2026 23:19:06 +0200 Subject: [PATCH 1/2] refactor: immutable BuiltModel bundle replacing the mutable DOCP cache build_model now returns an immutable BuiltModel{problem, nlp, cache} consumed by build_solution, eliminating the mutable backend cache (the Exa getter used to be mutated into the shared DiscretizedModel). This also removes a latent bug where two builds on the same DiscretizedModel could clobber each other's getter. - Optimization: add BuiltModel + NoCache; build_solution dispatches on the bundle; the orchestrator threads built.nlp into the solver. The solve(nlp, solver) stub stays untyped (NLPModels weak dep). - DOCP: nlp_model returns the bare NLP; ocp_solution realigned onto BuiltModel. - Move extract_solver_infos from Optimization to Solvers (solver-side utility). - KernelAbstractions promoted to a hard dep so the ExaModels extension triggers on ExaModels alone (CTDirect needn't load KernelAbstractions); Aqua stale_deps ignores it since the main module does not reference it. - Tests: import ADNLPModels/ExaModels in each modeler-using test file so the extensions trigger under per-file isolation; wrap fixtures in BuiltModel. Co-Authored-By: Claude Opus 4.8 --- Project.toml | 7 +-- ext/CTSolversMadNCL.jl | 2 +- ext/CTSolversMadNLP.jl | 2 +- ext/CTSolversUno.jl | 7 +-- src/DOCP/building.jl | 26 ++++---- src/Modelers/abstract_modeler.jl | 22 ++++--- src/Optimization/Optimization.jl | 11 ++-- src/Optimization/building.jl | 29 +++++---- src/Optimization/built_model.jl | 52 ++++++++++++++++ src/Solvers/Solvers.jl | 4 ++ src/Solvers/common_solve_api.jl | 8 +-- src/{Optimization => Solvers}/solver_info.jl | 0 test/problems/problems_definition.jl | 8 ++- test/suite/docp/test_docp.jl | 47 ++++++++------- test/suite/extensions/README.md | 60 ------------------- .../test_generic_extract_solver_infos.jl | 24 ++++---- test/suite/extensions/test_ipopt_extension.jl | 21 +++---- .../suite/extensions/test_madncl_extension.jl | 23 +++---- .../test_madncl_extract_solver_infos.jl | 34 ++++++----- .../suite/extensions/test_madnlp_extension.jl | 24 ++++---- .../test_madnlp_extract_solver_infos.jl | 30 +++++----- test/suite/extensions/test_uno_extension.jl | 22 +++---- .../test_comprehensive_validation.jl | 2 + test/suite/integration/test_end_to_end.jl | 33 +++++----- .../integration/test_real_strategies_mode.jl | 2 + .../test_route_to_comprehensive.jl | 2 + test/suite/meta/test_aqua.jl | 5 +- test/suite/modelers/test_adnlp_metadata.jl | 1 + .../test_adnlp_parameter_validation.jl | 2 + test/suite/modelers/test_coverage_modelers.jl | 4 +- test/suite/modelers/test_exa_gpu.jl | 1 + test/suite/optimization/test_error_cases.jl | 46 ++++++++------ test/suite/optimization/test_optimization.jl | 56 ++++++++++------- test/suite/optimization/test_real_problems.jl | 10 ++-- .../strategies/test_backward_compatibility.jl | 2 + .../test_cpu_only_parameters_integration.jl | 2 + .../strategies/test_describe_parameters.jl | 2 + .../strategies/test_describe_registry.jl | 2 + .../strategies/test_integration_parameters.jl | 2 + .../strategies/test_parameter_contract.jl | 2 + 40 files changed, 354 insertions(+), 285 deletions(-) create mode 100644 src/Optimization/built_model.jl rename src/{Optimization => Solvers}/solver_info.jl (100%) delete mode 100644 test/suite/extensions/README.md diff --git a/Project.toml b/Project.toml index 45445a09..ccb683fa 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,7 @@ 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] @@ -15,7 +16,6 @@ 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" @@ -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"] @@ -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" @@ -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"] diff --git a/ext/CTSolversMadNCL.jl b/ext/CTSolversMadNCL.jl index 1bb68442..641bc985 100644 --- a/ext/CTSolversMadNCL.jl +++ b/ext/CTSolversMadNCL.jl @@ -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 diff --git a/ext/CTSolversMadNLP.jl b/ext/CTSolversMadNLP.jl index 98f13ec5..b98b4afc 100644 --- a/ext/CTSolversMadNLP.jl +++ b/ext/CTSolversMadNLP.jl @@ -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 diff --git a/ext/CTSolversUno.jl b/ext/CTSolversUno.jl index 313af7f4..dc792b7d 100644 --- a/ext/CTSolversUno.jl +++ b/ext/CTSolversUno.jl @@ -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 @@ -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 @@ -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... @@ -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 diff --git a/src/DOCP/building.jl b/src/DOCP/building.jl index cd561a8d..04bccc8e 100644 --- a/src/DOCP/building.jl +++ b/src/DOCP/building.jl @@ -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 @@ -24,12 +25,12 @@ 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 """ @@ -37,12 +38,12 @@ $(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 @@ -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 diff --git a/src/Modelers/abstract_modeler.jl b/src/Modelers/abstract_modeler.jl index 2b359784..099c6fa8 100644 --- a/src/Modelers/abstract_modeler.jl +++ b/src/Modelers/abstract_modeler.jl @@ -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 @@ -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 ``` diff --git a/src/Optimization/Optimization.jl b/src/Optimization/Optimization.jl index fea87a9e..8870d9c1 100644 --- a/src/Optimization/Optimization.jl +++ b/src/Optimization/Optimization.jl @@ -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 diff --git a/src/Optimization/building.jl b/src/Optimization/building.jl index 38de0af3..2c210ec7 100644 --- a/src/Optimization/building.jl +++ b/src/Optimization/building.jl @@ -5,10 +5,12 @@ """ $(TYPEDSIGNATURES) -Build an NLP model from an optimization problem using the specified modeler. +Build a [`BuiltModel`](@ref) from an optimization problem using the specified modeler. This is a general function that works with any `AbstractOptimizationProblem`. -The modeler handles the conversion to the specific NLP backend. +The modeler handles the conversion to the specific NLP backend. The returned +[`BuiltModel`](@ref) carries the backend NLP model together with any immutable +build-time auxiliary needed later by [`build_solution`](@ref). # Arguments - `prob::AbstractOptimizationProblem`: The optimization problem @@ -16,15 +18,15 @@ The modeler handles the conversion to the specific NLP backend. - `modeler`: The modeler strategy (e.g., Modelers.ADNLP, Modelers.Exa) # Returns -- An NLP model suitable for the chosen backend +- A [`BuiltModel`](@ref) bundling the backend NLP model and its build-time cache # Example ```julia modeler = Modelers.ADNLP(show_time=false) -nlp = build_model(prob, initial_guess, modeler) +built = build_model(prob, initial_guess, modeler) ``` -See also: `build_solution` +See also: `build_solution`, `BuiltModel` """ function build_model(prob::AbstractOptimizationProblem, initial_guess, modeler) throw( @@ -42,11 +44,12 @@ $(TYPEDSIGNATURES) Build a solution from NLP execution statistics using the specified modeler. -This is a general function that works with any `AbstractOptimizationProblem`. -The modeler handles the conversion from NLP solution to problem-specific solution. +Dispatches on the [`BuiltModel`](@ref) returned by [`build_model`](@ref): it +carries both the problem (for problem-level data) and the immutable build-time +cache (e.g. an ExaModels getter) needed to reconstruct the solution. # Arguments -- `prob::AbstractOptimizationProblem`: The optimization problem +- `built::BuiltModel`: The built model bundle returned by `build_model` - `model_solution`: NLP solver output (execution statistics) - `modeler`: The modeler strategy used for building @@ -55,16 +58,18 @@ The modeler handles the conversion from NLP solution to problem-specific solutio # Example ```julia -sol = build_solution(prob, nlp_stats, modeler) +built = build_model(prob, initial_guess, modeler) +nlp_stats = solve(built.nlp, solver) +sol = build_solution(built, nlp_stats, modeler) ``` -See also: `build_model` +See also: `build_model`, `BuiltModel` """ -function build_solution(prob::AbstractOptimizationProblem, model_solution, modeler) +function build_solution(built::BuiltModel, model_solution, modeler) throw( Exceptions.NotImplemented( "Solution building not implemented"; - required_method="build_solution(prob::$(typeof(prob)), model_solution, modeler::$(typeof(modeler)))", + required_method="build_solution(built::BuiltModel{$(typeof(built.problem))}, model_solution, modeler::$(typeof(modeler)))", suggestion="Implement build_solution for this (problem, modeler) pair in the package providing the problem", context="Optimization.build_solution - required method implementation", ), diff --git a/src/Optimization/built_model.jl b/src/Optimization/built_model.jl new file mode 100644 index 00000000..548f65d8 --- /dev/null +++ b/src/Optimization/built_model.jl @@ -0,0 +1,52 @@ +# Built model +# +# Immutable bundle returned by `build_model` and consumed by `build_solution`. +# +# `build_model` produces two things that are both needed downstream: the backend +# NLP model (for the solver) and an optional build-time auxiliary (e.g. a getter +# produced together with an ExaModel). Both travel by value through `BuiltModel`, +# so no mutable cache is needed and the `build_model` -> `build_solution` coupling +# stays explicit and immutable. + +""" +$(TYPEDEF) + +Empty cache for backends whose `build_model` produces no auxiliary data. + +Used as the `cache` field of a [`BuiltModel`](@ref) when nothing besides the NLP +needs to be carried to `build_solution` (e.g. the ADNLP backend). Reusable by any +backend, including the future ODE side. +""" +struct NoCache <: Core.AbstractCache end + +""" +$(TYPEDEF) + +Immutable bundle produced by [`build_model`](@ref) and consumed by +[`build_solution`](@ref). + +It pairs the optimization problem with the backend NLP model and an optional, +immutable build-time cache. This replaces the previous pattern of mutating a +backend cache attached to the problem: any auxiliary produced while building the +NLP (e.g. an ExaModels getter) is stored here once, never mutated. + +# Fields +- `problem::TP`: The optimization problem (e.g. `DiscretizedModel`), giving access + to the original OCP, the discretizer, and the discretize-time cache (`docp`). +- `nlp::TN`: The backend NLP model. Left untyped because its package (e.g. + `NLPModels`) is a weak dependency. +- `cache::TC`: Immutable build-time auxiliary (`<: CTBase.Core.AbstractCache`), + populated by `build_model`. [`NoCache`](@ref) when the backend needs none. + +# Type parameters +- `TP <: AbstractOptimizationProblem` +- `TN` +- `TC <: CTBase.Core.AbstractCache` + +See also: [`build_model`](@ref), [`build_solution`](@ref), [`NoCache`](@ref). +""" +struct BuiltModel{TP<:AbstractOptimizationProblem,TN,TC<:Core.AbstractCache} + problem::TP + nlp::TN + cache::TC +end diff --git a/src/Solvers/Solvers.jl b/src/Solvers/Solvers.jl index b1718fbf..9c9482a4 100644 --- a/src/Solvers/Solvers.jl +++ b/src/Solvers/Solvers.jl @@ -65,9 +65,13 @@ include(joinpath(@__DIR__, "madnlpsuite.jl")) include(joinpath(@__DIR__, "knitro.jl")) include(joinpath(@__DIR__, "uno.jl")) include(joinpath(@__DIR__, "common_solve_api.jl")) +include(joinpath(@__DIR__, "solver_info.jl")) # Public API - abstract and concrete types export AbstractNLPSolver export Ipopt, MadNLP, MadNCL, Knitro, Uno +# Public API - solver utilities +export extract_solver_infos + end # module Solvers diff --git a/src/Solvers/common_solve_api.jl b/src/Solvers/common_solve_api.jl index 1bbb7995..b6833bb8 100644 --- a/src/Solvers/common_solve_api.jl +++ b/src/Solvers/common_solve_api.jl @@ -49,14 +49,14 @@ function CommonSolve.solve( solver::AbstractNLPSolver; display::Bool=__display(), ) - # Build NLP model - nlp = Optimization.build_model(problem, initial_guess, modeler) + # Build NLP model (bundled with its immutable build-time cache) + built = Optimization.build_model(problem, initial_guess, modeler) # Solve NLP - nlp_solution = CommonSolve.solve(nlp, solver; display=display) + nlp_solution = CommonSolve.solve(built.nlp, solver; display=display) # Build OCP solution - solution = Optimization.build_solution(problem, nlp_solution, modeler) + solution = Optimization.build_solution(built, nlp_solution, modeler) return solution end diff --git a/src/Optimization/solver_info.jl b/src/Solvers/solver_info.jl similarity index 100% rename from src/Optimization/solver_info.jl rename to src/Solvers/solver_info.jl diff --git a/test/problems/problems_definition.jl b/test/problems/problems_definition.jl index 7c18084e..0cdfb055 100644 --- a/test/problems/problems_definition.jl +++ b/test/problems/problems_definition.jl @@ -16,19 +16,21 @@ end function Optimization.build_model( prob::OptimizationProblem, initial_guess, ::Modelers.ADNLP ) - return prob.build_adnlp_model(initial_guess) + nlp = prob.build_adnlp_model(initial_guess) + return Optimization.BuiltModel(prob, nlp, Optimization.NoCache()) end # Build the Exa model from the wrapped builder, using the modeler base type. function Optimization.build_model( prob::OptimizationProblem, initial_guess, modeler::Modelers.Exa ) - return prob.build_exa_model(modeler[:base_type], initial_guess) + nlp = prob.build_exa_model(modeler[:base_type], initial_guess) + return Optimization.BuiltModel(prob, nlp, Optimization.NoCache()) end # These benchmark problems return the raw NLP solver statistics as the solution. function Optimization.build_solution( - ::OptimizationProblem, + ::Optimization.BuiltModel{<:OptimizationProblem}, nlp_solution::SolverCore.AbstractExecutionStats, ::Modelers.AbstractNLPModeler, ) diff --git a/test/suite/docp/test_docp.jl b/test/suite/docp/test_docp.jl index 75d41bc8..80ceb329 100644 --- a/test/suite/docp/test_docp.jl +++ b/test/suite/docp/test_docp.jl @@ -57,23 +57,24 @@ end # Contract implementation by dispatch on (DiscretizedModel{<:FakeDiscretizer}, FakeModelerDOCP) function Optimization.build_model( - ::DOCP.DiscretizedModel{<:Any,<:FakeDiscretizer}, + prob::DOCP.DiscretizedModel{<:Any,<:FakeDiscretizer}, initial_guess, modeler::FakeModelerDOCP, ) if modeler.backend == :adnlp - return ADNLPModels.ADNLPModel(z -> sum(z .^ 2), initial_guess) + nlp = ADNLPModels.ADNLPModel(z -> sum(z .^ 2), initial_guess) else n = length(initial_guess) m = ExaModels.ExaCore(Float64; concrete=Val(true)) ExaModels.@add_var(m, x_var, n; start=initial_guess) ExaModels.@add_obj(m, sum(x_var[i]^2 for i in 1:n)) - return ExaModels.ExaModel(m) + nlp = ExaModels.ExaModel(m) end + return Optimization.BuiltModel(prob, nlp, Optimization.NoCache()) end function Optimization.build_solution( - ::DOCP.DiscretizedModel{<:Any,<:FakeDiscretizer}, + ::Optimization.BuiltModel{<:DOCP.DiscretizedModel{<:Any,<:FakeDiscretizer}}, nlp_solution::SolverCore.AbstractExecutionStats, ::FakeModelerDOCP, ) @@ -185,9 +186,9 @@ function test_docp() Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test NLPModels.obj(nlp, x0) ≈ 5.0 - nlp2 = Optimization.build_model(docp, x0, modeler) - Test.@test nlp2 isa ADNLPModels.ADNLPModel - Test.@test NLPModels.obj(nlp2, x0) ≈ 5.0 + built2 = Optimization.build_model(docp, x0, modeler) + Test.@test built2.nlp isa ADNLPModels.ADNLPModel + Test.@test NLPModels.obj(built2.nlp, x0) ≈ 5.0 end Test.@testset "nlp_model with Exa backend" begin @@ -199,21 +200,22 @@ function test_docp() Test.@test nlp isa ExaModels.ExaModel{Float64} Test.@test NLPModels.obj(nlp, x0) ≈ 5.0 - nlp2 = Optimization.build_model(docp, x0, modeler) - Test.@test nlp2 isa ExaModels.ExaModel{Float64} - Test.@test NLPModels.obj(nlp2, x0) ≈ 5.0 + built2 = Optimization.build_model(docp, x0, modeler) + Test.@test built2.nlp isa ExaModels.ExaModel{Float64} + Test.@test NLPModels.obj(built2.nlp, x0) ≈ 5.0 end Test.@testset "ocp_solution with ADNLP backend" begin modeler = FakeModelerDOCP(:adnlp) stats = MockExecutionStats(1.23, 10, 1e-6, :first_order) + built = Optimization.BuiltModel(docp, nothing, Optimization.NoCache()) - sol = DOCP.ocp_solution(docp, stats, modeler) + sol = DOCP.ocp_solution(built, stats, modeler) Test.@test sol.objective ≈ 1.23 Test.@test sol.status == :first_order Test.@test sol.success === true - sol2 = Optimization.build_solution(docp, stats, modeler) + sol2 = Optimization.build_solution(built, stats, modeler) Test.@test sol2.objective ≈ 1.23 Test.@test sol2.status == :first_order end @@ -221,12 +223,13 @@ function test_docp() Test.@testset "ocp_solution with Exa backend" begin modeler = FakeModelerDOCP(:exa) stats = MockExecutionStats(2.34, 15, 1e-5, :acceptable) + built = Optimization.BuiltModel(docp, nothing, Optimization.NoCache()) - sol = DOCP.ocp_solution(docp, stats, modeler) + sol = DOCP.ocp_solution(built, stats, modeler) Test.@test sol.objective ≈ 2.34 Test.@test sol.iter == 15 - sol2 = Optimization.build_solution(docp, stats, modeler) + sol2 = Optimization.build_solution(built, stats, modeler) Test.@test sol2.objective ≈ 2.34 Test.@test sol2.iter == 15 end @@ -265,12 +268,12 @@ function test_docp() modeler = FakeModelerDOCP(:adnlp) x0 = [1.0, 2.0, 3.0] - nlp = DOCP.nlp_model(docp, x0, modeler) - Test.@test nlp isa ADNLPModels.ADNLPModel - Test.@test NLPModels.obj(nlp, x0) ≈ 14.0 + built = Optimization.build_model(docp, x0, modeler) + Test.@test built.nlp isa ADNLPModels.ADNLPModel + Test.@test NLPModels.obj(built.nlp, x0) ≈ 14.0 stats = MockExecutionStats(14.0, 20, 1e-8, :first_order) - sol = DOCP.ocp_solution(docp, stats, modeler) + sol = DOCP.ocp_solution(built, stats, modeler) Test.@test sol.objective ≈ 14.0 Test.@test sol.iter == 20 Test.@test sol.status == :first_order @@ -281,12 +284,12 @@ function test_docp() docp = fake_docp("integration_test_exa") modeler = FakeModelerDOCP(:exa) x0 = [1.0, 2.0, 3.0] - nlp = DOCP.nlp_model(docp, x0, modeler) - Test.@test nlp isa ExaModels.ExaModel{Float64} - Test.@test NLPModels.obj(nlp, x0) ≈ 14.0 + built = Optimization.build_model(docp, x0, modeler) + Test.@test built.nlp isa ExaModels.ExaModel{Float64} + Test.@test NLPModels.obj(built.nlp, x0) ≈ 14.0 stats = MockExecutionStats(14.0, 25, 1e-7, :acceptable) - sol = DOCP.ocp_solution(docp, stats, modeler) + sol = DOCP.ocp_solution(built, stats, modeler) Test.@test sol.objective ≈ 14.0 Test.@test sol.iter == 25 Test.@test sol.status == :acceptable diff --git a/test/suite/extensions/README.md b/test/suite/extensions/README.md deleted file mode 100644 index 1c0bc585..00000000 --- a/test/suite/extensions/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# Extension Tests - -These tests verify the functionality of solver extensions. They require optional packages to be installed. - -## Requirements - -Each extension test requires specific packages: - -### Ipopt Extension (`test_ipopt_extension.jl`) -```julia -using Pkg -Pkg.add("NLPModelsIpopt") -``` - -### Knitro Extension (`test_knitro_extension.jl`) - COMMENTED OUT -```julia -# using Pkg -# Pkg.add("NLPModelsKnitro") -``` -**Note**: Knitro is a commercial solver requiring a license - NOT AVAILABLE - -### MadNLP Extension (`test_madnlp_extension.jl`) -```julia -using Pkg -Pkg.add("MadNLP") -``` - -### MadNCL Extension (`test_madncl_extension.jl`) -```julia -using Pkg -Pkg.add("MadNCL") -Pkg.add("MadNLP") -``` - -## Running Extension Tests - -If the required packages are not installed, the tests will be skipped with a helpful message. - -To run all extension tests (with packages installed): -```bash -julia --project=@. test/runtests.jl suite/extensions/test_ipopt_extension -# julia --project=@. test/runtests.jl suite/extensions/test_knitro_extension # COMMENTED OUT - no license -julia --project=@. test/runtests.jl suite/extensions/test_madnlp_extension -julia --project=@. test/runtests.jl suite/extensions/test_madncl_extension -``` - -## Test Structure - -Each extension test follows the same pattern: - -1. **Check package availability** at runtime -2. **Skip tests** if packages are not available -3. **Unit tests**: Metadata, constructor, options extraction -4. **Integration tests**: Solve real problems (Rosenbrock, Elec, Max1MinusX2) - -All tests follow the testing rules in `.windsurf/rules/testing.md` with: -- Module wrapper for isolation -- Qualified calls (e.g., `Solvers.Ipopt`, `Strategies.metadata()`) -- Fake types at top-level when needed -- Clear separation between unit and integration tests diff --git a/test/suite/extensions/test_generic_extract_solver_infos.jl b/test/suite/extensions/test_generic_extract_solver_infos.jl index 0a3715d2..7e3d82cb 100644 --- a/test/suite/extensions/test_generic_extract_solver_infos.jl +++ b/test/suite/extensions/test_generic_extract_solver_infos.jl @@ -1,7 +1,7 @@ module TestExtGeneric using Test: Test -import CTSolvers.Optimization +import CTSolvers.Solvers using SolverCore: SolverCore using NLPModels: NLPModels using ADNLPModels: ADNLPModels @@ -74,7 +74,7 @@ function test_generic_extract_solver_infos() mock_stats = create_mock_stats(0.0, 10, 1e-8, :first_order) # Extract solver infos using generic function - objective, iterations, constraints_violation, message, status, successful = Optimization.extract_solver_infos( + objective, iterations, constraints_violation, message, status, successful = Solvers.extract_solver_infos( mock_stats ) @@ -92,7 +92,7 @@ function test_generic_extract_solver_infos() # Test successful status: :first_order stats_success = create_mock_stats(1.5, 5, 1e-6, :first_order) - obj, iter, viol, msg, stat, success = Optimization.extract_solver_infos( + obj, iter, viol, msg, stat, success = Solvers.extract_solver_infos( stats_success ) @@ -102,7 +102,7 @@ function test_generic_extract_solver_infos() # Test successful status: :acceptable stats_acceptable = create_mock_stats(1.5, 5, 1e-6, :acceptable) - _, _, _, _, stat2, success2 = Optimization.extract_solver_infos( + _, _, _, _, stat2, success2 = Solvers.extract_solver_infos( stats_acceptable ) @@ -111,14 +111,14 @@ function test_generic_extract_solver_infos() # Test unsuccessful status: :max_iter stats_max_iter = create_mock_stats(1.5, 100, 1e-2, :max_iter) - _, _, _, _, stat3, success3 = Optimization.extract_solver_infos(stats_max_iter) + _, _, _, _, stat3, success3 = Solvers.extract_solver_infos(stats_max_iter) Test.@test success3 == false Test.@test stat3 == :max_iter # Test unsuccessful status: :infeasible stats_infeasible = create_mock_stats(1.5, 50, 1e-1, :infeasible) - _, _, _, _, stat4, success4 = Optimization.extract_solver_infos( + _, _, _, _, stat4, success4 = Solvers.extract_solver_infos( stats_infeasible ) @@ -131,7 +131,7 @@ function test_generic_extract_solver_infos() # Test with minimization mock_stats_min = create_mock_stats(2.5, 15, 1e-7, :first_order) - objective, iterations, constraints_violation, message, status, successful = Optimization.extract_solver_infos( + objective, iterations, constraints_violation, message, status, successful = Solvers.extract_solver_infos( mock_stats_min ) @@ -144,13 +144,13 @@ function test_generic_extract_solver_infos() Test.@test successful isa Bool # Verify tuple structure - result = Optimization.extract_solver_infos(mock_stats_min) + result = Solvers.extract_solver_infos(mock_stats_min) Test.@test result isa Tuple Test.@test length(result) == 6 # Test with maximization (should not affect the generic implementation) mock_stats_max = create_mock_stats(2.5, 15, 1e-7, :first_order) - objective_max, iterations_max, constraints_violation_max, message_max, status_max, successful_max = Optimization.extract_solver_infos( + objective_max, iterations_max, constraints_violation_max, message_max, status_max, successful_max = Solvers.extract_solver_infos( mock_stats_max ) @@ -176,7 +176,7 @@ function test_generic_extract_solver_infos() # Test with minimization mock_stats_min = create_mock_stats(2.5, 15, 1e-7, :first_order) - objective, iterations, constraints_violation, message, status, successful = Optimization.extract_solver_infos( + objective, iterations, constraints_violation, message, status, successful = Solvers.extract_solver_infos( mock_stats_min ) @@ -202,7 +202,7 @@ function test_generic_extract_solver_infos() # Test with maximization mock_stats_max = create_mock_stats(3.14, 20, 1e-5, :acceptable) - objective_max, iterations_max, constraints_violation_max, message_max, status_max, successful_max = Optimization.extract_solver_infos( + objective_max, iterations_max, constraints_violation_max, message_max, status_max, successful_max = Solvers.extract_solver_infos( mock_stats_max ) @@ -245,7 +245,7 @@ function test_generic_extract_solver_infos() # Test that all 6 return values are present and have correct types mock_stats = create_mock_stats(3.14, 42, 1e-9, :acceptable) - result = Optimization.extract_solver_infos(mock_stats) + result = Solvers.extract_solver_infos(mock_stats) # Should return a 6-tuple Test.@test result isa Tuple diff --git a/test/suite/extensions/test_ipopt_extension.jl b/test/suite/extensions/test_ipopt_extension.jl index 9c74382f..8894a178 100644 --- a/test/suite/extensions/test_ipopt_extension.jl +++ b/test/suite/extensions/test_ipopt_extension.jl @@ -1,6 +1,7 @@ module TestIpoptExtension using Test: Test +import ExaModels: ExaModels # trigger CTSolversExaModels extension import CTBase.Exceptions using CTSolvers: CTSolvers import CTSolvers.Solvers @@ -143,7 +144,7 @@ function test_ipopt_extension() ros = TestProblems.Rosenbrock() # Build NLP model from problem - adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(ros.init) # Create solver with appropriate options @@ -169,7 +170,7 @@ function test_ipopt_extension() elec = TestProblems.Elec() # Build NLP model - adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(elec.init) solver = Solvers.Ipopt(max_iter=1000, tol=1e-6, print_level=0) @@ -184,7 +185,7 @@ function test_ipopt_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP model - adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(max_prob.init) solver = Solvers.Ipopt(max_iter=1000, tol=1e-6, print_level=0) @@ -230,8 +231,8 @@ function test_ipopt_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP models - nlp1 = Optimization.build_model(ros.prob, ros.init, Modelers.ADNLP()) - nlp2 = Optimization.build_model(max_prob.prob, max_prob.init, Modelers.ADNLP()) + nlp1 = Optimization.build_model(ros.prob, ros.init, Modelers.ADNLP()).nlp + nlp2 = Optimization.build_model(max_prob.prob, max_prob.init, Modelers.ADNLP()).nlp stats1 = CommonSolve.solve(nlp1, solver; display=false) stats2 = CommonSolve.solve(nlp2, solver; display=false) @@ -301,7 +302,7 @@ function test_ipopt_extension() ros = TestProblems.Rosenbrock() for (modeler, modeler_name) in zip(modelers, modelers_names) Test.@testset "$(modeler_name)" verbose=VERBOSE showtiming=SHOWTIMING begin - nlp = Optimization.build_model(ros.prob, ros.init, modeler) + nlp = Optimization.build_model(ros.prob, ros.init, modeler).nlp sol = CTSolversIpopt.solve_with_ipopt(nlp; ipopt_options...) Test.@test sol.status == :first_order Test.@test sol.solution ≈ ros.sol atol=1e-6 @@ -315,7 +316,7 @@ function test_ipopt_extension() elec = TestProblems.Elec() for (modeler, modeler_name) in zip(modelers, modelers_names) Test.@testset "$(modeler_name)" verbose=VERBOSE showtiming=SHOWTIMING begin - nlp = Optimization.build_model(elec.prob, elec.init, modeler) + nlp = Optimization.build_model(elec.prob, elec.init, modeler).nlp sol = CTSolversIpopt.solve_with_ipopt(nlp; ipopt_options...) Test.@test sol.status == :first_order end @@ -328,7 +329,7 @@ function test_ipopt_extension() Test.@testset "$(modeler_name)" verbose=VERBOSE showtiming=SHOWTIMING begin nlp = Optimization.build_model( max_prob.prob, max_prob.init, modeler - ) + ).nlp sol = CTSolversIpopt.solve_with_ipopt(nlp; ipopt_options...) Test.@test sol.status == :first_order Test.@test length(sol.solution) == 1 @@ -483,7 +484,7 @@ function test_ipopt_extension() Test.@testset "Pass-through Verification" begin ros = TestProblems.Rosenbrock() - adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(ros.init) # Test derivative_test="first-order" @@ -513,7 +514,7 @@ function test_ipopt_extension() Test.@testset "Exhaustive Options Validation" begin ros = TestProblems.Rosenbrock() - adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(ros.init) # Define all options with valid values to check for typos in names diff --git a/test/suite/extensions/test_madncl_extension.jl b/test/suite/extensions/test_madncl_extension.jl index 19eb1b7d..20c9318e 100644 --- a/test/suite/extensions/test_madncl_extension.jl +++ b/test/suite/extensions/test_madncl_extension.jl @@ -1,6 +1,7 @@ module TestMadNCLExtension using Test: Test +import ExaModels: ExaModels # trigger CTSolversExaModels extension import CTBase.Exceptions using CTSolvers: CTSolvers import CTSolvers.Solvers @@ -209,7 +210,7 @@ function test_madncl_extension() Test.@testset "MadNLP Option Pass-through" begin # Create a simple dummy problem ros = TestProblems.Rosenbrock() - adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(ros.init) # checking that it runs without error with these options @@ -229,7 +230,7 @@ function test_madncl_extension() # MadNCL requires problems with constraints # Using Elec problem which has constraints elec = TestProblems.Elec() - adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(elec.init) # Test with display=false sets print_level=MadNLP.ERROR @@ -249,7 +250,7 @@ function test_madncl_extension() ros = TestProblems.Rosenbrock() # Build NLP model - adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(ros.init) solver = Solvers.MadNCL(max_iter=1000, tol=1e-6, print_level=MadNLP.ERROR) @@ -265,7 +266,7 @@ function test_madncl_extension() elec = TestProblems.Elec() # Build NLP model - adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(elec.init) solver = Solvers.MadNCL(max_iter=3000, tol=1e-6, print_level=MadNLP.ERROR) @@ -281,7 +282,7 @@ function test_madncl_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP model - adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(max_prob.init) solver = Solvers.MadNCL(max_iter=1000, tol=1e-6, print_level=MadNLP.ERROR) @@ -351,10 +352,10 @@ function test_madncl_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP models - adnlp_builder1 = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()) + adnlp_builder1 = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()).nlp nlp1 = adnlp_builder1(elec.init) - adnlp_builder2 = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) + adnlp_builder2 = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()).nlp nlp2 = adnlp_builder2(max_prob.init) stats1 = CommonSolve.solve(nlp1, solver; display=false) @@ -432,7 +433,7 @@ function test_madncl_extension() for (linear_solver, linear_solver_name) in zip(linear_solvers, linear_solver_names) Test.@testset "$(modeler_name), $(linear_solver_name)" verbose=VERBOSE showtiming=SHOWTIMING begin - nlp = Optimization.build_model(elec.prob, elec.init, modeler) + nlp = Optimization.build_model(elec.prob, elec.init, modeler).nlp sol = CTSolversMadNCL.solve_with_madncl( nlp; linear_solver=linear_solver, madncl_options... ) @@ -450,7 +451,7 @@ function test_madncl_extension() Test.@testset "$(modeler_name), $(linear_solver_name)" verbose=VERBOSE showtiming=SHOWTIMING begin nlp = Optimization.build_model( max_prob.prob, max_prob.init, modeler - ) + ).nlp sol = CTSolversMadNCL.solve_with_madncl( nlp; linear_solver=linear_solver, madncl_options... ) @@ -520,7 +521,7 @@ function test_madncl_extension() Test.@testset "Elec - GPU" begin elec = TestProblems.Elec() - nlp = Optimization.build_model(elec.prob, elec.init, gpu_modeler) + nlp = Optimization.build_model(elec.prob, elec.init, gpu_modeler).nlp sol = CTSolversMadNCL.solve_with_madncl(nlp; madncl_options...) Test.@test sol.status == MadNLP.SOLVE_SUCCEEDED Test.@test isfinite(sol.objective) @@ -530,7 +531,7 @@ function test_madncl_extension() max_prob = TestProblems.Max1MinusX2() nlp = Optimization.build_model( max_prob.prob, max_prob.init, gpu_modeler - ) + ).nlp sol = CTSolversMadNCL.solve_with_madncl(nlp; madncl_options...) Test.@test sol.status == MadNLP.SOLVE_SUCCEEDED Test.@test length(sol.solution) == 1 diff --git a/test/suite/extensions/test_madncl_extract_solver_infos.jl b/test/suite/extensions/test_madncl_extract_solver_infos.jl index 0a7ef964..af964c26 100644 --- a/test/suite/extensions/test_madncl_extract_solver_infos.jl +++ b/test/suite/extensions/test_madncl_extract_solver_infos.jl @@ -1,8 +1,10 @@ module TestExtMadNCL using Test: Test +import ExaModels: ExaModels # trigger CTSolversExaModels extension using CTSolvers: CTSolvers import CTSolvers.Optimization +import CTSolvers.Solvers import CTSolvers.Modelers using MadNCL: MadNCL using MadNLP: MadNLP @@ -35,7 +37,7 @@ function test_madncl_extract_solver_infos() ros = TestProblems.Rosenbrock() # Build NLP model - adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(ros.init) # Configure MadNCL options @@ -48,7 +50,7 @@ function test_madncl_extract_solver_infos() stats = MadNCL.solve!(solver) # Extract solver infos using CTSolvers extension - objective, iterations, constraints_violation, message, status, successful = Optimization.extract_solver_infos( + objective, iterations, constraints_violation, message, status, successful = Solvers.extract_solver_infos( stats ) @@ -70,7 +72,7 @@ function test_madncl_extract_solver_infos() max_prob = TestProblems.Max1MinusX2() # Build NLP model - adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(max_prob.init) # Verify it's a maximization problem @@ -86,7 +88,7 @@ function test_madncl_extract_solver_infos() stats = MadNCL.solve!(solver) # Extract solver infos - objective_extracted, _, _, _, _, _ = Optimization.extract_solver_infos(stats) + objective_extracted, _, _, _, _, _ = Solvers.extract_solver_infos(stats) # The extracted objective should be the true maximization objective (≈ 1.0) expected_objective = TestProblems.max1minusx2_objective(max_prob.sol) @@ -123,7 +125,7 @@ function test_madncl_extract_solver_infos() # Unit test to verify that MadNCL does NOT flip the sign # (unlike MadNLP which has this bug) ros = TestProblems.Rosenbrock() - adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(ros.init) # Configure MadNCL options @@ -138,12 +140,12 @@ function test_madncl_extract_solver_infos() original_objective = stats.objective # Test case 1: minimization (should not flip) - obj_min, _, _, _, _, _ = Optimization.extract_solver_infos(stats) + obj_min, _, _, _, _, _ = Solvers.extract_solver_infos(stats) Test.@test obj_min ≈ original_objective atol=1e-10 # Test case 2: maximization (MadNCL returns correct sign, so we should NOT flip) # This is different from MadNLP! - obj_max, _, _, _, _, _ = Optimization.extract_solver_infos(stats) + obj_max, _, _, _, _, _ = Solvers.extract_solver_infos(stats) Test.@test obj_max ≈ original_objective atol=1e-10 # Same value, no flip # Verify: for MadNCL, both should be equal (no flip) @@ -153,7 +155,7 @@ function test_madncl_extract_solver_infos() Test.@testset "build_solution contract verification" begin # Test that extract_solver_infos returns types compatible with build_solution ros = TestProblems.Rosenbrock() - adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(ros.init) # Configure MadNCL options @@ -166,7 +168,7 @@ function test_madncl_extract_solver_infos() stats = MadNCL.solve!(solver) # Extract solver infos - objective, iterations, constraints_violation, message, status, successful = Optimization.extract_solver_infos( + objective, iterations, constraints_violation, message, status, successful = Solvers.extract_solver_infos( stats ) @@ -179,13 +181,13 @@ function test_madncl_extract_solver_infos() Test.@test successful isa Bool # Verify tuple structure - result = Optimization.extract_solver_infos(stats) + result = Solvers.extract_solver_infos(stats) Test.@test result isa Tuple Test.@test length(result) == 6 # Test with maximization problem for contract compliance max_prob = TestProblems.Max1MinusX2() - adnlp_builder_max = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) + adnlp_builder_max = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()).nlp nlp_max = adnlp_builder_max(max_prob.init) # Configure MadNCL options @@ -197,7 +199,7 @@ function test_madncl_extract_solver_infos() ) stats_max = MadNCL.solve!(solver_max) - objective_max, iterations_max, constraints_violation_max, message_max, status_max, successful_max = Optimization.extract_solver_infos( + objective_max, iterations_max, constraints_violation_max, message_max, status_max, successful_max = Solvers.extract_solver_infos( stats_max ) @@ -220,7 +222,7 @@ function test_madncl_extract_solver_infos() # Test with minimization (Rosenbrock) ros = TestProblems.Rosenbrock() - adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(ros.init) # Configure MadNCL options @@ -233,7 +235,7 @@ function test_madncl_extract_solver_infos() stats = MadNCL.solve!(solver) # Extract solver infos - objective, iterations, constraints_violation, message, status, successful = Optimization.extract_solver_infos( + objective, iterations, constraints_violation, message, status, successful = Solvers.extract_solver_infos( stats ) @@ -256,7 +258,7 @@ function test_madncl_extract_solver_infos() # Test with maximization problem (Max1MinusX2) max_prob = TestProblems.Max1MinusX2() - adnlp_builder_max = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) + adnlp_builder_max = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()).nlp nlp_max = adnlp_builder_max(max_prob.init) # Configure MadNCL options @@ -268,7 +270,7 @@ function test_madncl_extract_solver_infos() ) stats_max = MadNCL.solve!(solver_max) - objective_max, iterations_max, constraints_violation_max, message_max, status_max, successful_max = Optimization.extract_solver_infos( + objective_max, iterations_max, constraints_violation_max, message_max, status_max, successful_max = Solvers.extract_solver_infos( stats_max ) diff --git a/test/suite/extensions/test_madnlp_extension.jl b/test/suite/extensions/test_madnlp_extension.jl index b27321dd..eedc5006 100644 --- a/test/suite/extensions/test_madnlp_extension.jl +++ b/test/suite/extensions/test_madnlp_extension.jl @@ -222,7 +222,7 @@ function test_madnlp_extension() ros = TestProblems.Rosenbrock() # Build NLP model - adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(ros.init) solver = Solvers.MadNLP( @@ -245,7 +245,7 @@ function test_madnlp_extension() elec = TestProblems.Elec() # Build NLP model - adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(elec.init) solver = Solvers.MadNLP(max_iter=1000, tol=1e-6, print_level=MadNLP.ERROR) @@ -261,7 +261,7 @@ function test_madnlp_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP model - adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(max_prob.init) solver = Solvers.MadNLP(max_iter=1000, tol=1e-6, print_level=MadNLP.ERROR) @@ -292,7 +292,7 @@ function test_madnlp_extension() Test.@testset "Rosenbrock - GPU" begin ros = TestProblems.Rosenbrock() - nlp = Optimization.build_model(ros.prob, ros.init, gpu_modeler) + nlp = Optimization.build_model(ros.prob, ros.init, gpu_modeler).nlp sol = CommonSolve.solve( ros.prob, ros.init, gpu_modeler, gpu_solver; display=false ) @@ -508,10 +508,10 @@ function test_madnlp_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP models - adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()).nlp nlp1 = adnlp_builder(ros.init) - adnlp_builder2 = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) + adnlp_builder2 = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()).nlp nlp2 = adnlp_builder2(max_prob.init) stats1 = CommonSolve.solve(nlp1, solver; display=false) @@ -597,7 +597,7 @@ function test_madnlp_extension() for (linear_solver, linear_solver_name) in zip(linear_solvers, linear_solver_names) Test.@testset "$(modeler_name), $(linear_solver_name)" verbose=VERBOSE showtiming=SHOWTIMING begin - nlp = Optimization.build_model(ros.prob, ros.init, modeler) + nlp = Optimization.build_model(ros.prob, ros.init, modeler).nlp sol = CTSolversMadNLP.solve_with_madnlp( nlp; linear_solver=linear_solver, madnlp_options... ) @@ -616,7 +616,7 @@ function test_madnlp_extension() for (linear_solver, linear_solver_name) in zip(linear_solvers, linear_solver_names) Test.@testset "$(modeler_name), $(linear_solver_name)" verbose=VERBOSE showtiming=SHOWTIMING begin - nlp = Optimization.build_model(elec.prob, elec.init, modeler) + nlp = Optimization.build_model(elec.prob, elec.init, modeler).nlp sol = CTSolversMadNLP.solve_with_madnlp( nlp; linear_solver=linear_solver, madnlp_options... ) @@ -634,7 +634,7 @@ function test_madnlp_extension() Test.@testset "$(modeler_name), $(linear_solver_name)" verbose=VERBOSE showtiming=SHOWTIMING begin nlp = Optimization.build_model( max_prob.prob, max_prob.init, modeler - ) + ).nlp sol = CTSolversMadNLP.solve_with_madnlp( nlp; linear_solver=linear_solver, madnlp_options... ) @@ -665,7 +665,7 @@ function test_madnlp_extension() Test.@testset "Rosenbrock - GPU" begin ros = TestProblems.Rosenbrock() - nlp = Optimization.build_model(ros.prob, ros.init, gpu_modeler) + nlp = Optimization.build_model(ros.prob, ros.init, gpu_modeler).nlp sol = CTSolversMadNLP.solve_with_madnlp(nlp; madnlp_options...) Test.@test sol.status == MadNLP.SOLVE_SUCCEEDED Test.@test Array(sol.solution) ≈ ros.sol atol=1e-6 @@ -674,7 +674,7 @@ function test_madnlp_extension() Test.@testset "Elec - GPU" begin elec = TestProblems.Elec() - nlp = Optimization.build_model(elec.prob, elec.init, gpu_modeler) + nlp = Optimization.build_model(elec.prob, elec.init, gpu_modeler).nlp sol = CTSolversMadNLP.solve_with_madnlp(nlp; madnlp_options...) Test.@test sol.status == MadNLP.SOLVE_SUCCEEDED Test.@test isfinite(sol.objective) @@ -684,7 +684,7 @@ function test_madnlp_extension() max_prob = TestProblems.Max1MinusX2() nlp = Optimization.build_model( max_prob.prob, max_prob.init, gpu_modeler - ) + ).nlp sol = CTSolversMadNLP.solve_with_madnlp(nlp; madnlp_options...) Test.@test sol.status == MadNLP.SOLVE_SUCCEEDED Test.@test length(sol.solution) == 1 diff --git a/test/suite/extensions/test_madnlp_extract_solver_infos.jl b/test/suite/extensions/test_madnlp_extract_solver_infos.jl index f9db4049..c97fef58 100644 --- a/test/suite/extensions/test_madnlp_extract_solver_infos.jl +++ b/test/suite/extensions/test_madnlp_extract_solver_infos.jl @@ -1,7 +1,7 @@ module TestExtMadNLP using Test: Test -import CTSolvers.Optimization +import CTSolvers.Solvers using MadNLP: MadNLP using NLPModels: NLPModels using ADNLPModels: ADNLPModels @@ -64,7 +64,7 @@ function test_madnlp_extract_solver_infos() stats = MadNLP.solve!(solver) # Extract solver infos using CTSolvers extension - objective, iterations, constraints_violation, message, status, successful = Optimization.extract_solver_infos( + objective, iterations, constraints_violation, message, status, successful = Solvers.extract_solver_infos( stats ) @@ -92,7 +92,7 @@ function test_madnlp_extract_solver_infos() stats_min = MadNLP.solve!(solver_min) # Extract solver infos - objective_min, _, _, _, _, _ = Optimization.extract_solver_infos(stats_min) + objective_min, _, _, _, _, _ = Solvers.extract_solver_infos(stats_min) # For minimization, objective should equal stats.objective Test.@test objective_min ≈ stats_min.objective atol=1e-10 @@ -127,7 +127,7 @@ function test_madnlp_extract_solver_infos() nlp_min = ADNLPModels.ADNLPModel(obj, x0; minimize=true) solver_min = MadNLP.MadNLPSolver(nlp_min; print_level=MadNLP.ERROR) stats_min = MadNLP.solve!(solver_min) - obj_min, _, _, _, _, _ = Optimization.extract_solver_infos(stats_min) + obj_min, _, _, _, _, _ = Solvers.extract_solver_infos(stats_min) # For minimization, extracted objective should equal raw stats objective Test.@test obj_min ≈ stats_min.objective atol=1e-10 @@ -148,7 +148,7 @@ function test_madnlp_extract_solver_infos() solver = MadNLP.MadNLPSolver(nlp; print_level=MadNLP.ERROR) stats = MadNLP.solve!(solver) - _, _, _, _, status, _ = Optimization.extract_solver_infos(stats) + _, _, _, _, status, _ = Solvers.extract_solver_infos(stats) # Status should be a Symbol Test.@test status isa Symbol @@ -172,7 +172,7 @@ function test_madnlp_extract_solver_infos() solver = MadNLP.MadNLPSolver(nlp; print_level=MadNLP.ERROR, max_iter=100) stats = MadNLP.solve!(solver) - _, _, _, _, status, successful = Optimization.extract_solver_infos(stats) + _, _, _, _, status, successful = Solvers.extract_solver_infos(stats) # For a simple problem, should succeed Test.@test successful == true @@ -197,7 +197,7 @@ function test_madnlp_extract_solver_infos() solver = MadNLP.MadNLPSolver(nlp; print_level=MadNLP.ERROR) stats = MadNLP.solve!(solver) - result = Optimization.extract_solver_infos(stats) + result = Solvers.extract_solver_infos(stats) # Should return a 6-tuple Test.@test result isa Tuple @@ -232,7 +232,7 @@ function test_madnlp_extract_solver_infos() stats_max = MadNLP.solve!(solver_max) # Extract solver infos - objective_extracted, _, _, _, _, _ = Optimization.extract_solver_infos( + objective_extracted, _, _, _, _, _ = Solvers.extract_solver_infos( stats_max ) @@ -281,11 +281,11 @@ function test_madnlp_extract_solver_infos() original_objective = stats_min.objective # Test case 1: minimization (should not flip) - obj_min, _, _, _, _, _ = Optimization.extract_solver_infos(stats_min) + obj_min, _, _, _, _, _ = Solvers.extract_solver_infos(stats_min) Test.@test obj_min ≈ original_objective atol=1e-10 # Test case 2: maximization (should flip) - obj_max, _, _, _, _, _ = Optimization.extract_solver_infos(stats_min) + obj_max, _, _, _, _, _ = Solvers.extract_solver_infos(stats_min) Test.@test obj_max ≈ -original_objective atol=1e-10 # Verify the flip logic @@ -304,7 +304,7 @@ function test_madnlp_extract_solver_infos() stats = MadNLP.solve!(solver) # Extract solver infos - objective, iterations, constraints_violation, message, status, successful = Optimization.extract_solver_infos( + objective, iterations, constraints_violation, message, status, successful = Solvers.extract_solver_infos( stats ) @@ -317,7 +317,7 @@ function test_madnlp_extract_solver_infos() Test.@test successful isa Bool # Verify tuple structure - result = Optimization.extract_solver_infos(stats) + result = Solvers.extract_solver_infos(stats) Test.@test result isa Tuple Test.@test length(result) == 6 @@ -326,7 +326,7 @@ function test_madnlp_extract_solver_infos() solver_max = MadNLP.MadNLPSolver(nlp_max; print_level=MadNLP.ERROR) stats_max = MadNLP.solve!(solver_max) - objective_max, iterations_max, constraints_violation_max, message_max, status_max, successful_max = Optimization.extract_solver_infos( + objective_max, iterations_max, constraints_violation_max, message_max, status_max, successful_max = Solvers.extract_solver_infos( stats_max ) @@ -357,7 +357,7 @@ function test_madnlp_extract_solver_infos() stats = MadNLP.solve!(solver) # Extract solver infos - objective, iterations, constraints_violation, message, status, successful = Optimization.extract_solver_infos( + objective, iterations, constraints_violation, message, status, successful = Solvers.extract_solver_infos( stats ) @@ -382,7 +382,7 @@ function test_madnlp_extract_solver_infos() solver_max = MadNLP.MadNLPSolver(nlp_max; print_level=MadNLP.ERROR) stats_max = MadNLP.solve!(solver_max) - objective_max, iterations_max, constraints_violation_max, message_max, status_max, successful_max = Optimization.extract_solver_infos( + objective_max, iterations_max, constraints_violation_max, message_max, status_max, successful_max = Solvers.extract_solver_infos( stats_max ) diff --git a/test/suite/extensions/test_uno_extension.jl b/test/suite/extensions/test_uno_extension.jl index cab5a12c..4625be39 100644 --- a/test/suite/extensions/test_uno_extension.jl +++ b/test/suite/extensions/test_uno_extension.jl @@ -85,7 +85,7 @@ function test_uno_extension() Test.@test stats isa UnoSolver.UnoExecutionStats # Extract solver infos - obj, iter, viol, msg, status, successful = CTSolversUno.Optimization.extract_solver_infos( + obj, iter, viol, msg, status, successful = CTSolversUno.Solvers.extract_solver_infos( stats ) @@ -108,7 +108,7 @@ function test_uno_extension() stats_zero_iter = CTSolversUno.solve_with_uno( nlp; max_iterations=0, logger="SILENT" ) - obj_zero, iter_zero, viol_zero, msg_zero, status_zero, successful_zero = CTSolversUno.Optimization.extract_solver_infos( + obj_zero, iter_zero, viol_zero, msg_zero, status_zero, successful_zero = CTSolversUno.Solvers.extract_solver_infos( stats_zero_iter ) @@ -197,7 +197,7 @@ function test_uno_extension() ros = TestProblems.Rosenbrock() # Build NLP model from problem - adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(ros.init) # Create solver with appropriate options @@ -222,7 +222,7 @@ function test_uno_extension() elec = TestProblems.Elec() # Build NLP model - adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(elec.init) solver = Solvers.Uno( @@ -239,7 +239,7 @@ function test_uno_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP model - adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(max_prob.init) solver = Solvers.Uno( @@ -289,8 +289,8 @@ function test_uno_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP models - nlp1 = Optimization.build_model(ros.prob, ros.init, Modelers.ADNLP()) - nlp2 = Optimization.build_model(max_prob.prob, max_prob.init, Modelers.ADNLP()) + nlp1 = Optimization.build_model(ros.prob, ros.init, Modelers.ADNLP()).nlp + nlp2 = Optimization.build_model(max_prob.prob, max_prob.init, Modelers.ADNLP()).nlp stats1 = CommonSolve.solve(nlp1, solver; display=false) stats2 = CommonSolve.solve(nlp2, solver; display=false) @@ -363,7 +363,7 @@ function test_uno_extension() ros = TestProblems.Rosenbrock() for (modeler, modeler_name) in zip(modelers, modelers_names) Test.@testset "$(modeler_name)" verbose=VERBOSE showtiming=SHOWTIMING begin - nlp = Optimization.build_model(ros.prob, ros.init, modeler) + nlp = Optimization.build_model(ros.prob, ros.init, modeler).nlp sol = CTSolversUno.solve_with_uno(nlp; uno_options...) # solve_with_uno now returns GenericExecutionStats Test.@test sol.status in (:first_order, :acceptable) @@ -378,7 +378,7 @@ function test_uno_extension() elec = TestProblems.Elec() for (modeler, modeler_name) in zip(modelers, modelers_names) Test.@testset "$(modeler_name)" verbose=VERBOSE showtiming=SHOWTIMING begin - nlp = Optimization.build_model(elec.prob, elec.init, modeler) + nlp = Optimization.build_model(elec.prob, elec.init, modeler).nlp sol = CTSolversUno.solve_with_uno(nlp; uno_options...) # solve_with_uno now returns GenericExecutionStats Test.@test sol.status in (:first_order, :acceptable) @@ -392,7 +392,7 @@ function test_uno_extension() Test.@testset "$(modeler_name)" verbose=VERBOSE showtiming=SHOWTIMING begin nlp = Optimization.build_model( max_prob.prob, max_prob.init, modeler - ) + ).nlp sol = CTSolversUno.solve_with_uno(nlp; uno_options...) # solve_with_uno now returns GenericExecutionStats Test.@test sol.status in (:first_order, :acceptable) @@ -524,7 +524,7 @@ function test_uno_extension() Test.@testset "Exhaustive Options Validation" begin ros = TestProblems.Rosenbrock() - adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()).nlp nlp = adnlp_builder(ros.init) # Define all options with valid values to check for typos in names diff --git a/test/suite/integration/test_comprehensive_validation.jl b/test/suite/integration/test_comprehensive_validation.jl index a139cdb5..4066a4dd 100644 --- a/test/suite/integration/test_comprehensive_validation.jl +++ b/test/suite/integration/test_comprehensive_validation.jl @@ -14,6 +14,8 @@ Date: 2026-02-06 module TestComprehensiveValidation using Test: Test +import ADNLPModels: ADNLPModels # trigger CTSolversADNLPModels extension +import ExaModels: ExaModels # trigger CTSolversExaModels extension import CTBase.Exceptions import CTBase.Strategies import CTBase.Options diff --git a/test/suite/integration/test_end_to_end.jl b/test/suite/integration/test_end_to_end.jl index 02ff516b..6d643714 100644 --- a/test/suite/integration/test_end_to_end.jl +++ b/test/suite/integration/test_end_to_end.jl @@ -15,6 +15,7 @@ const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true # Import modules import CTSolvers.Modelers import CTSolvers.Optimization +import CTSolvers.Solvers import CTSolvers.DOCP # ============================================================================ @@ -41,7 +42,7 @@ function test_end_to_end() Test.@test modeler isa Modelers.AbstractNLPModeler # Step 4: Build NLP model - nlp = Optimization.build_model(prob, ros.init, modeler) + nlp = Optimization.build_model(prob, ros.init, modeler).nlp Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test nlp.meta.nvar == 2 Test.@test nlp.meta.ncon == 1 @@ -69,7 +70,7 @@ function test_end_to_end() result = MadNLP.solve!(solver) # Step 10: Extract solver info - obj, iter, viol, msg, status, success = Optimization.extract_solver_infos( + obj, iter, viol, msg, status, success = Solvers.extract_solver_infos( result ) @@ -100,7 +101,7 @@ function test_end_to_end() Test.@test modeler isa Modelers.Exa # Step 3: Build NLP model - nlp = Optimization.build_model(prob, ros.init, modeler) + nlp = Optimization.build_model(prob, ros.init, modeler).nlp Test.@test nlp isa ExaModels.ExaModel Test.@test nlp.meta.nvar == 2 Test.@test nlp.meta.ncon == 1 @@ -133,7 +134,7 @@ function test_end_to_end() modeler = Modelers.Exa( base_type=Float32, minimize=true; mode=:permissive ) - nlp = Optimization.build_model(prob, ros.init, modeler) + nlp = Optimization.build_model(prob, ros.init, modeler).nlp Test.@test nlp isa ExaModels.ExaModel Test.@test eltype(nlp.meta.x0) == Float32 @@ -149,7 +150,7 @@ function test_end_to_end() modeler = Modelers.Exa( base_type=Float64, minimize=true; mode=:permissive ) - nlp = Optimization.build_model(prob, ros.init, modeler) + nlp = Optimization.build_model(prob, ros.init, modeler).nlp Test.@test nlp isa ExaModels.ExaModel Test.@test eltype(nlp.meta.x0) == Float64 @@ -172,7 +173,7 @@ function test_end_to_end() Test.@testset "Modelers.ADNLP - Simple" begin # Test without options (defaults) modeler = Modelers.ADNLP() - nlp = Optimization.build_model(prob, ros.init, modeler) + nlp = Optimization.build_model(prob, ros.init, modeler).nlp Test.@test nlp isa ADNLPModels.ADNLPModel obj = NLPModels.obj(nlp, ros.init) @@ -182,13 +183,13 @@ function test_end_to_end() Test.@testset "Modelers.ADNLP - With Options" begin # Test with show_time option modeler = Modelers.ADNLP(show_time=false) - nlp = Optimization.build_model(prob, ros.init, modeler) + nlp = Optimization.build_model(prob, ros.init, modeler).nlp Test.@test nlp isa ADNLPModels.ADNLPModel # Test with different backends (all valid ADNLPModels backends) for backend in [:optimized, :generic, :default] modeler_backend = Modelers.ADNLP(backend=backend, show_time=false) - nlp_backend = Optimization.build_model(prob, ros.init, modeler_backend) + nlp_backend = Optimization.build_model(prob, ros.init, modeler_backend).nlp Test.@test nlp_backend isa ADNLPModels.ADNLPModel obj = NLPModels.obj(nlp_backend, ros.init) @@ -200,7 +201,7 @@ function test_end_to_end() Test.@testset "Modelers.Exa - Simple" begin # Test without options (defaults) modeler = Modelers.Exa(base_type=Float64) - nlp = Optimization.build_model(prob, ros.init, modeler) + nlp = Optimization.build_model(prob, ros.init, modeler).nlp Test.@test nlp isa ExaModels.ExaModel obj = NLPModels.obj(nlp, ros.init) @@ -213,7 +214,7 @@ function test_end_to_end() modeler = Modelers.Exa( base_type=Float64, minimize=true, backend=nothing; mode=:permissive ) - nlp = Optimization.build_model(prob, ros.init, modeler) + nlp = Optimization.build_model(prob, ros.init, modeler).nlp Test.@test nlp isa ExaModels.ExaModel obj = NLPModels.obj(nlp, ros.init) @@ -232,7 +233,7 @@ function test_end_to_end() # Build with ADNLP modeler_adnlp = Modelers.ADNLP(show_time=false) - nlp_adnlp = Optimization.build_model(prob, ros.init, modeler_adnlp) + nlp_adnlp = Optimization.build_model(prob, ros.init, modeler_adnlp).nlp obj_adnlp = NLPModels.obj(nlp_adnlp, ros.init) # Build with Exa (permissive mode for minimize option) @@ -240,7 +241,7 @@ function test_end_to_end() modeler_exa = Modelers.Exa( base_type=Float64, minimize=true; mode=:permissive ) - nlp_exa = Optimization.build_model(prob, ros.init, modeler_exa) + nlp_exa = Optimization.build_model(prob, ros.init, modeler_exa).nlp obj_exa = NLPModels.obj(nlp_exa, Float64.(ros.init)) # Both should give same objective @@ -262,7 +263,7 @@ function test_end_to_end() prob = ros.prob modeler = Modelers.ADNLP(show_time=false) - nlp = Optimization.build_model(prob, ros.init, modeler) + nlp = Optimization.build_model(prob, ros.init, modeler).nlp Test.@testset "Gradient at initial point" begin grad = NLPModels.grad(nlp, ros.init) @@ -294,7 +295,7 @@ function test_end_to_end() prob = ros.prob modeler = Modelers.ADNLP(show_time=false) - nlp = Optimization.build_model(prob, ros.init, modeler) + nlp = Optimization.build_model(prob, ros.init, modeler).nlp Test.@testset "Constraint at initial point" begin cons = NLPModels.cons(nlp, ros.init) @@ -327,14 +328,14 @@ function test_end_to_end() modeler = Modelers.ADNLP(show_time=false) # Should be fast - t = @elapsed nlp = Optimization.build_model(prob, ros.init, modeler) + t = @elapsed nlp = Optimization.build_model(prob, ros.init, modeler).nlp Test.@test t < 1.0 # Should take less than 1 second Test.@test nlp isa ADNLPModels.ADNLPModel end Test.@testset "Function evaluation time" begin modeler = Modelers.ADNLP(show_time=false) - nlp = Optimization.build_model(prob, ros.init, modeler) + nlp = Optimization.build_model(prob, ros.init, modeler).nlp # Objective evaluation should be fast t = @elapsed obj = NLPModels.obj(nlp, ros.init) diff --git a/test/suite/integration/test_real_strategies_mode.jl b/test/suite/integration/test_real_strategies_mode.jl index c4595120..22fb3fb4 100644 --- a/test/suite/integration/test_real_strategies_mode.jl +++ b/test/suite/integration/test_real_strategies_mode.jl @@ -7,6 +7,8 @@ Tests that the mode parameter works correctly with actual solver and modeler typ module TestRealStrategiesMode using Test: Test +import ADNLPModels: ADNLPModels # trigger CTSolversADNLPModels extension +import ExaModels: ExaModels # trigger CTSolversExaModels extension using CTSolvers: CTSolvers import CTBase.Strategies import CTBase.Options diff --git a/test/suite/integration/test_route_to_comprehensive.jl b/test/suite/integration/test_route_to_comprehensive.jl index e681d1de..bcf8ae09 100644 --- a/test/suite/integration/test_route_to_comprehensive.jl +++ b/test/suite/integration/test_route_to_comprehensive.jl @@ -16,6 +16,8 @@ Date: 2026-02-06 module TestRouteToComprehensive using Test: Test +import ADNLPModels: ADNLPModels # trigger CTSolversADNLPModels extension +import ExaModels: ExaModels # trigger CTSolversExaModels extension import CTBase.Exceptions import CTBase.Strategies import CTBase.Orchestration diff --git a/test/suite/meta/test_aqua.jl b/test/suite/meta/test_aqua.jl index 457e8d04..2155ffe1 100644 --- a/test/suite/meta/test_aqua.jl +++ b/test/suite/meta/test_aqua.jl @@ -11,7 +11,10 @@ function test_aqua() Aqua.test_all( CTSolvers; ambiguities=false, - #stale_deps=(ignore=[:SomePackage],), + # KernelAbstractions is a hard dep used only by the CTSolversExaModels + # extension (so ExaModels alone triggers it, and CTDirect needn't load + # KernelAbstractions); it is not referenced by the main module. + stale_deps=(ignore=[:KernelAbstractions],), deps_compat=(ignore=[:LinearAlgebra, :Unicode],), piracies=true, ) diff --git a/test/suite/modelers/test_adnlp_metadata.jl b/test/suite/modelers/test_adnlp_metadata.jl index 3530960c..001e7c5d 100644 --- a/test/suite/modelers/test_adnlp_metadata.jl +++ b/test/suite/modelers/test_adnlp_metadata.jl @@ -1,6 +1,7 @@ module TestADNLPMetadata using Test: Test +import ExaModels: ExaModels # trigger CTSolversExaModels extension import CTBase.Exceptions import CTSolvers.Modelers import CTBase.Strategies diff --git a/test/suite/modelers/test_adnlp_parameter_validation.jl b/test/suite/modelers/test_adnlp_parameter_validation.jl index 8598ed80..3ecfc917 100644 --- a/test/suite/modelers/test_adnlp_parameter_validation.jl +++ b/test/suite/modelers/test_adnlp_parameter_validation.jl @@ -1,4 +1,6 @@ module TestADNLPParameterValidation +import ADNLPModels: ADNLPModels # trigger CTSolversADNLPModels extension +import ExaModels: ExaModels # trigger CTSolversExaModels extension using Test import CTBase.Core diff --git a/test/suite/modelers/test_coverage_modelers.jl b/test/suite/modelers/test_coverage_modelers.jl index 9a4db1d8..f76bbbe4 100644 --- a/test/suite/modelers/test_coverage_modelers.jl +++ b/test/suite/modelers/test_coverage_modelers.jl @@ -1,6 +1,7 @@ module TestCoverageModelers using Test: Test +import ADNLPModels: ADNLPModels # trigger CTSolversADNLPModels extension import CTBase.Exceptions import CTSolvers.Modelers import CTBase.Strategies @@ -43,8 +44,9 @@ function test_coverage_modelers() ) # Solution building contract - NotImplemented + built = Optimization.BuiltModel(prob, nothing, Optimization.NoCache()) Test.@test_throws Exceptions.NotImplemented Optimization.build_solution( - prob, stats, modeler + built, stats, modeler ) end diff --git a/test/suite/modelers/test_exa_gpu.jl b/test/suite/modelers/test_exa_gpu.jl index bcf7aff6..7d5ae2ed 100644 --- a/test/suite/modelers/test_exa_gpu.jl +++ b/test/suite/modelers/test_exa_gpu.jl @@ -1,6 +1,7 @@ module TestExaGPU using Test: Test +import ADNLPModels: ADNLPModels # trigger CTSolversADNLPModels extension import CTBase.Exceptions import CTSolvers.Modelers import CTBase.Strategies diff --git a/test/suite/optimization/test_error_cases.jl b/test/suite/optimization/test_error_cases.jl index ed8d9769..f991f270 100644 --- a/test/suite/optimization/test_error_cases.jl +++ b/test/suite/optimization/test_error_cases.jl @@ -11,6 +11,7 @@ const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true # Import from CTSolvers import CTSolvers.Optimization +import CTSolvers.Solvers import CTSolvers.Modelers # ============================================================================ @@ -27,8 +28,9 @@ Problem with only partial contract implementation (ADNLP only, no Exa). """ struct PartialProblem <: Optimization.AbstractOptimizationProblem end -function Optimization.build_model(::PartialProblem, initial_guess, ::Modelers.ADNLP) - return ADNLPModels.ADNLPModel(z -> sum(z .^ 2), initial_guess) +function Optimization.build_model(prob::PartialProblem, initial_guess, ::Modelers.ADNLP) + nlp = ADNLPModels.ADNLPModel(z -> sum(z .^ 2), initial_guess) + return Optimization.BuiltModel(prob, nlp, Optimization.NoCache()) end """ @@ -41,7 +43,9 @@ function Optimization.build_model(::FailingProblem, initial_guess, ::Modelers.AD end function Optimization.build_solution( - ::FailingProblem, ::SolverCore.AbstractExecutionStats, ::Modelers.ADNLP + ::Optimization.BuiltModel{<:FailingProblem}, + ::SolverCore.AbstractExecutionStats, + ::Modelers.ADNLP, ) return error("Intentional error") end @@ -51,17 +55,19 @@ Sum-of-squares problem implementing both backends (for edge cases). """ struct SquaresProblem <: Optimization.AbstractOptimizationProblem end -function Optimization.build_model(::SquaresProblem, initial_guess, ::Modelers.ADNLP) - return ADNLPModels.ADNLPModel(z -> sum(z .^ 2), initial_guess) +function Optimization.build_model(prob::SquaresProblem, initial_guess, ::Modelers.ADNLP) + nlp = ADNLPModels.ADNLPModel(z -> sum(z .^ 2), initial_guess) + return Optimization.BuiltModel(prob, nlp, Optimization.NoCache()) end -function Optimization.build_model(::SquaresProblem, initial_guess, modeler::Modelers.Exa) +function Optimization.build_model(prob::SquaresProblem, initial_guess, modeler::Modelers.Exa) T = modeler[:base_type] x = T.(initial_guess) m = ExaModels.ExaCore(T; concrete=Val(true)) ExaModels.@add_var(m, x_var, length(x); start=x) ExaModels.@add_obj(m, sum(x_var[i]^2 for i in 1:length(x))) - return ExaModels.ExaModel(m) + nlp = ExaModels.ExaModel(m) + return Optimization.BuiltModel(prob, nlp, Optimization.NoCache()) end """ @@ -107,8 +113,9 @@ function test_error_cases() Test.@testset "build_solution - NotImplemented" begin stats = create_edge_case_stats(1.0, 1, 1e-6, :first_order) + built = Optimization.BuiltModel(prob, nothing, Optimization.NoCache()) Test.@test_throws Exceptions.NotImplemented Optimization.build_solution( - prob, stats, Modelers.ADNLP() + built, stats, Modelers.ADNLP() ) end end @@ -122,7 +129,7 @@ function test_error_cases() Test.@testset "Implemented backend works" begin x0 = [1.0, 2.0] - nlp = Optimization.build_model(prob, x0, Modelers.ADNLP()) + nlp = Optimization.build_model(prob, x0, Modelers.ADNLP()).nlp Test.@test nlp isa ADNLPModels.ADNLPModel end @@ -148,8 +155,9 @@ function test_error_cases() Test.@testset "build_solution with failing implementation" begin stats = MockStats(1.0) + built = Optimization.BuiltModel(prob, nothing, Optimization.NoCache()) Test.@test_throws ErrorException Optimization.build_solution( - prob, stats, Modelers.ADNLP() + built, stats, Modelers.ADNLP() ) end end @@ -163,7 +171,7 @@ function test_error_cases() Test.@testset "Single variable problem" begin x0 = [1.0] - nlp = Optimization.build_model(prob, x0, Modelers.ADNLP()) + nlp = Optimization.build_model(prob, x0, Modelers.ADNLP()).nlp Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test nlp.meta.nvar == 1 Test.@test NLPModels.obj(nlp, x0) ≈ 1.0 @@ -172,7 +180,7 @@ function test_error_cases() Test.@testset "Large dimension problem" begin n = 1000 x0 = ones(n) - nlp = Optimization.build_model(prob, x0, Modelers.ADNLP()) + nlp = Optimization.build_model(prob, x0, Modelers.ADNLP()).nlp Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test nlp.meta.nvar == n end @@ -180,13 +188,13 @@ function test_error_cases() Test.@testset "Different numeric types" begin nlp32 = Optimization.build_model( prob, Float32[1.0, 2.0], Modelers.Exa(; base_type=Float32) - ) + ).nlp Test.@test nlp32 isa ExaModels.ExaModel{Float32} Test.@test eltype(nlp32.meta.x0) == Float32 nlp64 = Optimization.build_model( prob, Float64[1.0, 2.0], Modelers.Exa(; base_type=Float64) - ) + ).nlp Test.@test nlp64 isa ExaModels.ExaModel{Float64} Test.@test eltype(nlp64.meta.x0) == Float64 end @@ -199,7 +207,7 @@ function test_error_cases() Test.@testset "Solver Info Edge Cases" begin Test.@testset "Zero iterations" begin stats = create_edge_case_stats(0.0, 0, 0.0, :first_order) - obj, iter, viol, msg, status, success = Optimization.extract_solver_infos( + obj, iter, viol, msg, status, success = Solvers.extract_solver_infos( stats ) Test.@test iter == 0 @@ -208,7 +216,7 @@ function test_error_cases() Test.@testset "Very large objective" begin stats = create_edge_case_stats(1e100, 10, 1e-6, :first_order) - obj, iter, viol, msg, status, success = Optimization.extract_solver_infos( + obj, iter, viol, msg, status, success = Solvers.extract_solver_infos( stats ) Test.@test obj ≈ 1e100 @@ -217,7 +225,7 @@ function test_error_cases() Test.@testset "Very small constraint violation" begin stats = create_edge_case_stats(1.0, 10, 1e-15, :first_order) - obj, iter, viol, msg, status, success = Optimization.extract_solver_infos( + obj, iter, viol, msg, status, success = Solvers.extract_solver_infos( stats ) Test.@test viol ≈ 1e-15 @@ -226,7 +234,7 @@ function test_error_cases() Test.@testset "Unknown status" begin stats = create_edge_case_stats(1.0, 10, 1e-6, :unknown_status) - obj, iter, viol, msg, status, success = Optimization.extract_solver_infos( + obj, iter, viol, msg, status, success = Solvers.extract_solver_infos( stats ) Test.@test status == :unknown_status @@ -243,7 +251,7 @@ function test_error_cases() Test.@testset "build_model return type" begin x0 = [1.0, 2.0] - nlp = Optimization.build_model(prob, x0, Modelers.ADNLP()) + nlp = Optimization.build_model(prob, x0, Modelers.ADNLP()).nlp Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test typeof(nlp) <: ADNLPModels.ADNLPModel end diff --git a/test/suite/optimization/test_optimization.jl b/test/suite/optimization/test_optimization.jl index aa794aab..0d7f9657 100644 --- a/test/suite/optimization/test_optimization.jl +++ b/test/suite/optimization/test_optimization.jl @@ -4,6 +4,7 @@ using Test: Test import CTBase.Exceptions using CTSolvers: CTSolvers import CTSolvers.Optimization +import CTSolvers.Solvers using NLPModels: NLPModels using SolverCore: SolverCore using ADNLPModels: ADNLPModels @@ -37,21 +38,24 @@ end # Contract implementation by dispatch on (FakeOptimizationProblem, FakeModeler) function Optimization.build_model( - ::FakeOptimizationProblem, initial_guess, modeler::FakeModeler + prob::FakeOptimizationProblem, initial_guess, modeler::FakeModeler ) if modeler.backend == :adnlp - return ADNLPModels.ADNLPModel(z -> sum(z .^ 2), initial_guess) + nlp = ADNLPModels.ADNLPModel(z -> sum(z .^ 2), initial_guess) else n = length(initial_guess) m = ExaModels.ExaCore(Float64; concrete=Val(true)) ExaModels.@add_var(m, x_var, n; start=initial_guess) ExaModels.@add_obj(m, sum(x_var[i]^2 for i in 1:n)) - return ExaModels.ExaModel(m) + nlp = ExaModels.ExaModel(m) end + return Optimization.BuiltModel(prob, nlp, Optimization.NoCache()) end function Optimization.build_solution( - ::FakeOptimizationProblem, nlp_solution::SolverCore.AbstractExecutionStats, ::FakeModeler + ::Optimization.BuiltModel{<:FakeOptimizationProblem}, + nlp_solution::SolverCore.AbstractExecutionStats, + ::FakeModeler, ) return (obj=nlp_solution.objective, iter=nlp_solution.iter, status=nlp_solution.status) end @@ -76,7 +80,7 @@ Tests for the Optimization module: - Abstract type (`AbstractOptimizationProblem`) - Building contract (`build_model`, `build_solution`) by multiple dispatch - NotImplemented stubs for unregistered (problem, modeler) pairs -- Solver utilities (`extract_solver_infos`) +- Solver utilities (`extract_solver_infos` — lives in `Solvers`, tested here for workflow integration) """ function test_optimization() Test.@testset "Optimization Module" verbose=VERBOSE showtiming=SHOWTIMING begin @@ -101,8 +105,8 @@ function test_optimization() end end - Test.@testset "Exported Functions" begin - for f in (:build_model, :build_solution, :extract_solver_infos) + Test.@testset "Exported Functions - Optimization" begin + for f in (:build_model, :build_solution) Test.@testset "$f" begin Test.@test isdefined(Optimization, f) Test.@test isdefined(CurrentModule, f) @@ -110,6 +114,11 @@ function test_optimization() end end end + + Test.@testset "Exported Functions - Solvers" begin + Test.@test isdefined(Solvers, :extract_solver_infos) + Test.@test Solvers.extract_solver_infos isa Function + end end # ==================================================================== @@ -122,8 +131,9 @@ function test_optimization() Test.@test_throws Exceptions.NotImplemented Optimization.build_model( prob, [1.0], FakeModeler(:adnlp) ) + built = Optimization.BuiltModel(prob, nothing, Optimization.NoCache()) Test.@test_throws Exceptions.NotImplemented Optimization.build_solution( - prob, create_mock_execution_stats(1.0, 1, 1e-6, :first_order), FakeModeler(:adnlp) + built, create_mock_execution_stats(1.0, 1, 1e-6, :first_order), FakeModeler(:adnlp) ) end @@ -137,7 +147,7 @@ function test_optimization() Test.@testset "build_model with ADNLP backend" begin modeler = FakeModeler(:adnlp) x0 = [1.0, 2.0] - nlp = Optimization.build_model(prob, x0, modeler) + nlp = Optimization.build_model(prob, x0, modeler).nlp Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test nlp.meta.x0 == x0 end @@ -145,22 +155,24 @@ function test_optimization() Test.@testset "build_model with Exa backend" begin modeler = FakeModeler(:exa) x0 = [1.0, 2.0] - nlp = Optimization.build_model(prob, x0, modeler) + nlp = Optimization.build_model(prob, x0, modeler).nlp Test.@test nlp isa ExaModels.ExaModel{Float64} end Test.@testset "build_solution with ADNLP backend" begin modeler = FakeModeler(:adnlp) + built = Optimization.BuiltModel(prob, nothing, Optimization.NoCache()) stats = create_mock_execution_stats(1.23, 10, 1e-6, :first_order) - sol = Optimization.build_solution(prob, stats, modeler) + sol = Optimization.build_solution(built, stats, modeler) Test.@test sol.obj ≈ 1.23 Test.@test sol.status == :first_order end Test.@testset "build_solution with Exa backend" begin modeler = FakeModeler(:exa) + built = Optimization.BuiltModel(prob, nothing, Optimization.NoCache()) stats = create_mock_execution_stats(2.34, 15, 1e-5, :acceptable) - sol = Optimization.build_solution(prob, stats, modeler) + sol = Optimization.build_solution(built, stats, modeler) Test.@test sol.obj ≈ 2.34 Test.@test sol.iter == 15 end @@ -173,7 +185,7 @@ function test_optimization() Test.@testset "Solver Info Extraction" begin Test.@testset "extract_solver_infos - first_order status" begin stats = create_mock_execution_stats(1.23, 15, 1.0e-6, :first_order) - obj, iter, viol, msg, status, success = Optimization.extract_solver_infos( + obj, iter, viol, msg, status, success = Solvers.extract_solver_infos( stats ) Test.@test obj ≈ 1.23 @@ -182,12 +194,12 @@ function test_optimization() Test.@test msg == "Ipopt/generic" Test.@test status == :first_order Test.@test success == true - Test.@test_nowarn Test.@inferred Optimization.extract_solver_infos(stats) + Test.@test_nowarn Test.@inferred Solvers.extract_solver_infos(stats) end Test.@testset "extract_solver_infos - acceptable status" begin stats = create_mock_execution_stats(2.34, 20, 1.0e-5, :acceptable) - obj, iter, viol, msg, status, success = Optimization.extract_solver_infos( + obj, iter, viol, msg, status, success = Solvers.extract_solver_infos( stats ) Test.@test obj ≈ 2.34 @@ -197,7 +209,7 @@ function test_optimization() Test.@testset "extract_solver_infos - failure status" begin stats = create_mock_execution_stats(3.45, 5, 1.0e-3, :max_iter) - obj, iter, viol, msg, status, success = Optimization.extract_solver_infos( + obj, iter, viol, msg, status, success = Solvers.extract_solver_infos( stats ) Test.@test obj ≈ 3.45 @@ -215,16 +227,17 @@ function test_optimization() prob = FakeOptimizationProblem() modeler = FakeModeler(:adnlp) x0 = [1.0, 2.0] - nlp = Optimization.build_model(prob, x0, modeler) + built = Optimization.build_model(prob, x0, modeler) + nlp = built.nlp Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test NLPModels.obj(nlp, x0) ≈ 5.0 stats = create_mock_execution_stats(5.0, 10, 1e-6, :first_order) - sol = Optimization.build_solution(prob, stats, modeler) + sol = Optimization.build_solution(built, stats, modeler) Test.@test sol.obj ≈ 5.0 Test.@test sol.status == :first_order - obj, iter, viol, msg, status, success = Optimization.extract_solver_infos( + obj, iter, viol, msg, status, success = Solvers.extract_solver_infos( stats ) Test.@test obj ≈ 5.0 @@ -235,12 +248,13 @@ function test_optimization() prob = FakeOptimizationProblem() modeler = FakeModeler(:exa) x0 = [1.0, 2.0] - nlp = Optimization.build_model(prob, x0, modeler) + built = Optimization.build_model(prob, x0, modeler) + nlp = built.nlp Test.@test nlp isa ExaModels.ExaModel{Float64} Test.@test NLPModels.obj(nlp, x0) ≈ 5.0 stats = create_mock_execution_stats(5.0, 15, 1e-5, :acceptable) - sol = Optimization.build_solution(prob, stats, modeler) + sol = Optimization.build_solution(built, stats, modeler) Test.@test sol.obj ≈ 5.0 Test.@test sol.iter == 15 end diff --git a/test/suite/optimization/test_real_problems.jl b/test/suite/optimization/test_real_problems.jl index f366f430..e0869e35 100644 --- a/test/suite/optimization/test_real_problems.jl +++ b/test/suite/optimization/test_real_problems.jl @@ -31,7 +31,7 @@ function test_real_problems() ros = TestProblems.Rosenbrock() Test.@testset "build_model (ADNLP) with Rosenbrock" begin - nlp = Optimization.build_model(ros.prob, ros.init, Modelers.ADNLP()) + nlp = Optimization.build_model(ros.prob, ros.init, Modelers.ADNLP()).nlp Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test nlp.meta.x0 == ros.init Test.@test nlp.meta.minimize == true @@ -44,7 +44,7 @@ function test_real_problems() end Test.@testset "build_model (Exa, Float64) with Rosenbrock" begin - nlp64 = Optimization.build_model(ros.prob, ros.init, Modelers.Exa()) + nlp64 = Optimization.build_model(ros.prob, ros.init, Modelers.Exa()).nlp Test.@test nlp64 isa ExaModels.ExaModel{Float64} Test.@test nlp64.meta.x0 == Float64.(ros.init) Test.@test nlp64.meta.minimize == true @@ -59,7 +59,7 @@ function test_real_problems() Test.@testset "build_model (Exa, Float32) with Rosenbrock" begin nlp32 = Optimization.build_model( ros.prob, ros.init, Modelers.Exa(; base_type=Float32) - ) + ).nlp Test.@test nlp32 isa ExaModels.ExaModel{Float32} Test.@test nlp32.meta.x0 == Float32.(ros.init) Test.@test eltype(nlp32.meta.x0) == Float32 @@ -80,7 +80,7 @@ function test_real_problems() Test.@testset "Integration with Real Problems" begin Test.@testset "Complete workflow - Rosenbrock ADNLP" begin ros = TestProblems.Rosenbrock() - nlp = Optimization.build_model(ros.prob, ros.init, Modelers.ADNLP()) + nlp = Optimization.build_model(ros.prob, ros.init, Modelers.ADNLP()).nlp Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test nlp.meta.nvar == 2 Test.@test nlp.meta.ncon == 1 @@ -96,7 +96,7 @@ function test_real_problems() Test.@testset "Complete workflow - Rosenbrock Exa" begin ros = TestProblems.Rosenbrock() - nlp = Optimization.build_model(ros.prob, ros.init, Modelers.Exa()) + nlp = Optimization.build_model(ros.prob, ros.init, Modelers.Exa()).nlp Test.@test nlp isa ExaModels.ExaModel{Float64} Test.@test nlp.meta.nvar == 2 Test.@test nlp.meta.ncon == 1 diff --git a/test/suite/strategies/test_backward_compatibility.jl b/test/suite/strategies/test_backward_compatibility.jl index 6afe8f27..65e59010 100644 --- a/test/suite/strategies/test_backward_compatibility.jl +++ b/test/suite/strategies/test_backward_compatibility.jl @@ -1,6 +1,8 @@ module TestBackwardCompatibility using Test: Test +import ADNLPModels: ADNLPModels # trigger CTSolversADNLPModels extension +import ExaModels: ExaModels # trigger CTSolversExaModels extension import CTBase.Exceptions import CTBase.Strategies import CTSolvers.Modelers diff --git a/test/suite/strategies/test_cpu_only_parameters_integration.jl b/test/suite/strategies/test_cpu_only_parameters_integration.jl index 4ca4e9e7..fff084d3 100644 --- a/test/suite/strategies/test_cpu_only_parameters_integration.jl +++ b/test/suite/strategies/test_cpu_only_parameters_integration.jl @@ -1,4 +1,6 @@ module TestCPUOnlyParametersIntegration +import ADNLPModels: ADNLPModels # trigger CTSolversADNLPModels extension +import ExaModels: ExaModels # trigger CTSolversExaModels extension using Test using CTSolvers diff --git a/test/suite/strategies/test_describe_parameters.jl b/test/suite/strategies/test_describe_parameters.jl index dfcf382d..b830bdae 100644 --- a/test/suite/strategies/test_describe_parameters.jl +++ b/test/suite/strategies/test_describe_parameters.jl @@ -5,6 +5,8 @@ module TestDescribeParameters using Test: Test +import ADNLPModels: ADNLPModels # trigger CTSolversADNLPModels extension +import ExaModels: ExaModels # trigger CTSolversExaModels extension import CTBase.Exceptions using CTSolvers: CTSolvers import CTBase.Strategies diff --git a/test/suite/strategies/test_describe_registry.jl b/test/suite/strategies/test_describe_registry.jl index ceb97fa7..0348b7c0 100644 --- a/test/suite/strategies/test_describe_registry.jl +++ b/test/suite/strategies/test_describe_registry.jl @@ -1,6 +1,8 @@ module TestDescribeRegistry using Test: Test +import ADNLPModels: ADNLPModels # trigger CTSolversADNLPModels extension +import ExaModels: ExaModels # trigger CTSolversExaModels extension import CTBase.Exceptions using CTSolvers: CTSolvers import CTBase.Strategies diff --git a/test/suite/strategies/test_integration_parameters.jl b/test/suite/strategies/test_integration_parameters.jl index f0ebc2d0..1c7806af 100644 --- a/test/suite/strategies/test_integration_parameters.jl +++ b/test/suite/strategies/test_integration_parameters.jl @@ -1,6 +1,8 @@ module TestIntegrationParameters using Test: Test +import ADNLPModels: ADNLPModels # trigger CTSolversADNLPModels extension +import ExaModels: ExaModels # trigger CTSolversExaModels extension import CTBase.Exceptions import CTBase.Strategies import CTSolvers.Modelers diff --git a/test/suite/strategies/test_parameter_contract.jl b/test/suite/strategies/test_parameter_contract.jl index 17065982..df8628ce 100644 --- a/test/suite/strategies/test_parameter_contract.jl +++ b/test/suite/strategies/test_parameter_contract.jl @@ -1,4 +1,6 @@ module TestParameterContract +import ADNLPModels: ADNLPModels # trigger CTSolversADNLPModels extension +import ExaModels: ExaModels # trigger CTSolversExaModels extension using Test using CTSolvers From 87f30460e31217b9ce3d8fd3cdf4d19b20c3ba53 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sat, 27 Jun 2026 23:32:09 +0200 Subject: [PATCH 2/2] docs: bump to 0.4.23-beta with CHANGELOG and BREAKING entries Documents the immutable BuiltModel refactor, the extract_solver_infos move to Solvers, and the KernelAbstractions return to a hard dependency. Co-Authored-By: Claude Opus 4.8 --- BREAKING.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 33 ++++++++++++++++++++++ Project.toml | 2 +- 3 files changed, 112 insertions(+), 1 deletion(-) diff --git a/BREAKING.md b/BREAKING.md index 08827527..6c2dff84 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index d0cc3d1f..9e30a1f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Project.toml b/Project.toml index ccb683fa..d05c90df 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "CTSolvers" uuid = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" -version = "0.4.22-beta" +version = "0.4.23-beta" authors = ["Olivier Cots "] [deps]