diff --git a/ext/CTSolversIpopt.jl b/ext/CTSolversIpopt.jl index fe9fb119..b9f886e7 100644 --- a/ext/CTSolversIpopt.jl +++ b/ext/CTSolversIpopt.jl @@ -8,6 +8,7 @@ module CTSolversIpopt import DocStringExtensions: TYPEDSIGNATURES import CTSolvers.Solvers +import CommonSolve import CTBase.Strategies import CTBase.Options import CTBase.Core @@ -591,8 +592,8 @@ Solve an NLP problem using Ipopt. # Returns - `SolverCore.GenericExecutionStats`: Solver execution statistics """ -function (solver::Solvers.Ipopt)( - nlp::NLPModels.AbstractNLPModel; display::Bool=true +function CommonSolve.solve( + nlp::NLPModels.AbstractNLPModel, solver::Solvers.Ipopt; display::Bool=true )::SolverCore.GenericExecutionStats options = Strategies.options_dict(solver) options[:print_level] = display ? options[:print_level] : 0 diff --git a/ext/CTSolversKnitro.jl b/ext/CTSolversKnitro.jl index 2d59085c..51475e2a 100644 --- a/ext/CTSolversKnitro.jl +++ b/ext/CTSolversKnitro.jl @@ -8,6 +8,7 @@ module CTSolversKnitro import DocStringExtensions: TYPEDSIGNATURES import CTSolvers.Solvers +import CommonSolve import CTBase.Strategies import CTBase.Options import CTBase.Exceptions @@ -244,8 +245,8 @@ Solve an NLP problem using Knitro. # Returns - `SolverCore.GenericExecutionStats`: Solver execution statistics """ -function (solver::Solvers.Knitro)( - nlp::NLPModels.AbstractNLPModel; display::Bool=true +function CommonSolve.solve( + nlp::NLPModels.AbstractNLPModel, solver::Solvers.Knitro; display::Bool=true )::SolverCore.GenericExecutionStats options = Strategies.options_dict(solver) options[:outlev] = display ? options[:outlev] : 0 diff --git a/ext/CTSolversMadNCL.jl b/ext/CTSolversMadNCL.jl index e1b76873..1bb68442 100644 --- a/ext/CTSolversMadNCL.jl +++ b/ext/CTSolversMadNCL.jl @@ -8,6 +8,7 @@ module CTSolversMadNCL import DocStringExtensions: TYPEDSIGNATURES import CTSolvers.Solvers +import CommonSolve import CTBase.Strategies import CTBase.Options import CTBase.Core @@ -402,8 +403,8 @@ Solve an NLP problem using MadNCL. # Returns - `MadNCL.NCLStats`: MadNCL execution statistics """ -function (solver::Solvers.MadNCL)( - nlp::NLPModels.AbstractNLPModel; display::Bool=true +function CommonSolve.solve( + nlp::NLPModels.AbstractNLPModel, solver::Solvers.MadNCL; display::Bool=true )::MadNCL.NCLStats options = Strategies.options_dict(solver) options[:print_level] = display ? options[:print_level] : MadNLP.ERROR diff --git a/ext/CTSolversMadNLP.jl b/ext/CTSolversMadNLP.jl index dd68d901..98f13ec5 100644 --- a/ext/CTSolversMadNLP.jl +++ b/ext/CTSolversMadNLP.jl @@ -9,6 +9,7 @@ module CTSolversMadNLP import DocStringExtensions: TYPEDSIGNATURES import CTSolvers.Optimization import CTSolvers.Solvers +import CommonSolve import CTBase.Strategies import CTBase.Options import CTBase.Core @@ -502,8 +503,8 @@ Solve an NLP problem using MadNLP. # Returns - `MadNLP.MadNLPExecutionStats`: MadNLP execution statistics """ -function (solver::Solvers.MadNLP)( - nlp::NLPModels.AbstractNLPModel; display::Bool=true +function CommonSolve.solve( + nlp::NLPModels.AbstractNLPModel, solver::Solvers.MadNLP; display::Bool=true )::MadNLP.MadNLPExecutionStats options = Strategies.options_dict(solver) options[:print_level] = display ? options[:print_level] : MadNLP.ERROR diff --git a/ext/CTSolversUno.jl b/ext/CTSolversUno.jl index 3d01d2a2..313af7f4 100644 --- a/ext/CTSolversUno.jl +++ b/ext/CTSolversUno.jl @@ -9,6 +9,7 @@ module CTSolversUno import DocStringExtensions: TYPEDSIGNATURES import CTSolvers.Optimization import CTSolvers.Solvers +import CommonSolve import CTBase.Strategies import CTBase.Options import CTBase.Core @@ -338,8 +339,8 @@ Solve an NLP problem using Uno. See also: [`solve_with_uno`](@ref), [`Optimization.extract_solver_infos`](@ref) """ -function (solver::Solvers.Uno)( - nlp::NLPModels.AbstractNLPModel; display::Bool=true +function CommonSolve.solve( + nlp::NLPModels.AbstractNLPModel, solver::Solvers.Uno; display::Bool=true )::UnoSolver.UnoExecutionStats options = Strategies.options_dict(solver) options[:logger] = display ? options[:logger] : "SILENT" diff --git a/src/DOCP/DOCP.jl b/src/DOCP/DOCP.jl index 1641fe54..70319dd7 100644 --- a/src/DOCP/DOCP.jl +++ b/src/DOCP/DOCP.jl @@ -20,19 +20,27 @@ import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES using NLPModels: NLPModels using SolverCore: SolverCore using CTModels: CTModels +import CTBase.Core +import CTBase.Exceptions + +# CTBase generic infrastructure +using CTBase.Strategies # Internal CTSolvers API using ..CTSolvers.Optimization using ..CTSolvers.Modelers # Submodules +include(joinpath(@__DIR__, "abstract_discretizer.jl")) include(joinpath(@__DIR__, "types.jl")) -include(joinpath(@__DIR__, "contract_impl.jl")) +include(joinpath(@__DIR__, "contract.jl")) include(joinpath(@__DIR__, "accessors.jl")) include(joinpath(@__DIR__, "building.jl")) # Public API +export AbstractDiscretizer export DiscretizedModel +export discretize export ocp_model export nlp_model, ocp_solution diff --git a/src/DOCP/abstract_discretizer.jl b/src/DOCP/abstract_discretizer.jl new file mode 100644 index 00000000..724e23d1 --- /dev/null +++ b/src/DOCP/abstract_discretizer.jl @@ -0,0 +1,19 @@ +# DOCP abstract discretizer +# +# Defines the `AbstractDiscretizer` strategy contract. Concrete discretizers +# (collocation, direct shooting, ...) are implemented in the packages providing +# the transcription methods (e.g. CTDirect). + +""" +$(TYPEDEF) + +Abstract base type for all discretization strategies. + +Concrete subtypes implement specific transcription methods (collocation, direct +shooting, etc.) and are defined in the package providing the method. A discretizer +is a `Strategies.AbstractStrategy`: it carries validated options and drives +`discretize` to turn an optimal control problem into a [`DiscretizedModel`](@ref). + +See also: `DiscretizedModel`, `discretize`. +""" +abstract type AbstractDiscretizer <: Strategies.AbstractStrategy end diff --git a/src/DOCP/accessors.jl b/src/DOCP/accessors.jl index 96b2b1ad..2dac47f3 100644 --- a/src/DOCP/accessors.jl +++ b/src/DOCP/accessors.jl @@ -20,4 +20,4 @@ ocp = ocp_model(docp) See also: `DiscretizedModel` """ -ocp_model(docp::DiscretizedModel) = docp.optimal_control_problem +ocp_model(docp::DiscretizedModel) = docp.ocp diff --git a/src/DOCP/contract.jl b/src/DOCP/contract.jl new file mode 100644 index 00000000..313e33ca --- /dev/null +++ b/src/DOCP/contract.jl @@ -0,0 +1,34 @@ +# DOCP contract +# +# Generic entry points implemented (by multiple dispatch) in the package that +# provides the discretizer (e.g. CTDirect). The methods here are stubs that throw +# `NotImplemented` so that a missing implementation fails with a clear message. + +""" +$(TYPEDSIGNATURES) + +Discretize an optimal control problem into a [`DiscretizedModel`](@ref). + +# Contract +Must be implemented in the package providing `discretizer`, dispatching on its +concrete type, e.g. `CTSolvers.discretize(ocp, ::Collocation)` in CTDirect. + +# Arguments +- `ocp::CTModels.AbstractModel`: The optimal control problem. +- `discretizer::AbstractDiscretizer`: The discretization strategy. + +# Returns +- A [`DiscretizedModel`](@ref) with a populated cache. + +See also: `build_model`, `build_solution`. +""" +function discretize(ocp::CTModels.AbstractModel, discretizer::AbstractDiscretizer) + throw( + Exceptions.NotImplemented( + "discretize not implemented"; + required_method="CTSolvers.discretize(ocp, ::$(typeof(discretizer)))", + suggestion="Implement it in the package providing $(typeof(discretizer))", + context="DOCP.discretize - required method implementation", + ), + ) +end diff --git a/src/DOCP/contract_impl.jl b/src/DOCP/contract_impl.jl deleted file mode 100644 index 08fcae93..00000000 --- a/src/DOCP/contract_impl.jl +++ /dev/null @@ -1,104 +0,0 @@ -# DOCP contract implementation -# -# Implements the `Optimization.AbstractOptimizationProblem` contract for -# `DiscretizedModel`. - -""" -$(TYPEDSIGNATURES) - -Get the ADNLPModels model builder from a DiscretizedModel. - -This implements the `Optimization.AbstractOptimizationProblem` contract. - -# Arguments -- `prob::DiscretizedModel`: The discretized problem - -# Returns -- `AbstractModelBuilder`: The ADNLP model builder - -# Example -```julia -builder = Optimization.get_adnlp_model_builder(docp) -nlp = builder(initial_guess) -``` - -See also: `Optimization.get_exa_model_builder` -""" -function Optimization.get_adnlp_model_builder(prob::DiscretizedModel) - return prob.adnlp_model_builder -end - -""" -$(TYPEDSIGNATURES) - -Get the ExaModels model builder from a DiscretizedModel. - -This implements the `Optimization.AbstractOptimizationProblem` contract. - -# Arguments -- `prob::DiscretizedModel`: The discretized problem - -# Returns -- `AbstractModelBuilder`: The ExaModel builder - -# Example -```julia -builder = Optimization.get_exa_model_builder(docp) -nlp = builder(Float64, initial_guess) -``` - -See also: `Optimization.get_adnlp_model_builder` -""" -function Optimization.get_exa_model_builder(prob::DiscretizedModel) - return prob.exa_model_builder -end - -""" -$(TYPEDSIGNATURES) - -Get the ADNLPModels solution builder from a DiscretizedModel. - -This implements the `Optimization.AbstractOptimizationProblem` contract. - -# Arguments -- `prob::DiscretizedModel`: The discretized problem - -# Returns -- `AbstractSolutionBuilder`: The ADNLP solution builder - -# Example -```julia -builder = Optimization.get_adnlp_solution_builder(docp) -sol = builder(nlp_stats) -``` - -See also: `Optimization.get_exa_solution_builder` -""" -function Optimization.get_adnlp_solution_builder(prob::DiscretizedModel) - return prob.adnlp_solution_builder -end - -""" -$(TYPEDSIGNATURES) - -Get the ExaModels solution builder from a DiscretizedModel. - -This implements the `Optimization.AbstractOptimizationProblem` contract. - -# Arguments -- `prob::DiscretizedModel`: The discretized problem - -# Returns -- `AbstractSolutionBuilder`: The ExaModel solution builder - -# Example -```julia -builder = Optimization.get_exa_solution_builder(docp) -sol = builder(nlp_stats) -``` - -See also: `Optimization.get_adnlp_solution_builder` -""" -function Optimization.get_exa_solution_builder(prob::DiscretizedModel) - return prob.exa_solution_builder -end diff --git a/src/DOCP/types.jl b/src/DOCP/types.jl index 8f35cf18..d12e9b1c 100644 --- a/src/DOCP/types.jl +++ b/src/DOCP/types.jl @@ -1,47 +1,38 @@ # DOCP types # -# Defines the `DiscretizedModel` type. Builder types live in the -# `CTSolvers.Optimization` module. +# Defines the `DiscretizedModel` type: a thin pairing of an optimal control +# problem with the discretizer that produced it, plus an optional backend cache. """ $(TYPEDEF) Discretized optimal control problem ready for NLP solving. -Wraps an optimal control problem together with builders for the supported NLP backends. -This type implements the `Optimization.AbstractOptimizationProblem` contract. +A thin pairing of an optimal control problem with the discretizer that produced +it, plus a backend cache. The actual NLP model and OCP solution are produced by +multiple dispatch on `(DiscretizedModel, modeler)` through the `build_model` / +`build_solution` contract, implemented in the package providing the discretizer +(e.g. CTDirect). This mirrors `Flow{system, integrator}` on the ODE side. # Fields -- `optimal_control_problem::TO`: The original optimal control problem -- `adnlp_model_builder::TAMB`: Builder for ADNLPModels -- `exa_model_builder::TEMB`: Builder for ExaModels -- `adnlp_solution_builder::TASB`: Builder for ADNLP solutions -- `exa_solution_builder::TESB`: Builder for ExaModel solutions +- `ocp::TO`: The original optimal control problem. +- `discretizer::TD`: The discretization strategy used. +- `cache::TC`: Backend cache (`<: CTBase.Core.AbstractCache`), opaque to CTSolvers, + populated by the implementing package (e.g. CTDirect's `DOCPCache`). -# Example -```julia -# Conceptual usage pattern -docp = DiscretizedModel( - ocp, - adnlp_model_builder, - exa_model_builder, - adnlp_solution_builder, - exa_solution_builder, -) -``` +# Type parameters +- `TO <: CTModels.AbstractModel` +- `TD <: AbstractDiscretizer` +- `TC <: CTBase.Core.AbstractCache` -See also: `ocp_model`, `nlp_model`, `ocp_solution` +See also: `ocp_model`, `discretize`, `build_model`, `build_solution`. """ struct DiscretizedModel{ TO<:CTModels.AbstractModel, - TAMB<:AbstractModelBuilder, - TEMB<:AbstractModelBuilder, - TASB<:AbstractSolutionBuilder, - TESB<:AbstractSolutionBuilder, + TD<:AbstractDiscretizer, + TC<:Core.AbstractCache, } <: AbstractOptimizationProblem - optimal_control_problem::TO - adnlp_model_builder::TAMB - exa_model_builder::TEMB - adnlp_solution_builder::TASB - exa_solution_builder::TESB + ocp::TO + discretizer::TD + cache::TC end diff --git a/src/Modelers/abstract_modeler.jl b/src/Modelers/abstract_modeler.jl index 4f03a3e9..2b359784 100644 --- a/src/Modelers/abstract_modeler.jl +++ b/src/Modelers/abstract_modeler.jl @@ -15,8 +15,10 @@ interfaces for model and solution building. # Implementation Requirements All concrete modeler strategies must: - Implement the `Strategies.AbstractStrategy` contract -- Implement `(modeler)(prob::Optimization.AbstractOptimizationProblem, initial_guess)` -- Implement `(modeler)(prob::Optimization.AbstractOptimizationProblem, stats::SolverCore.AbstractExecutionStats)` +- 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)` # Example ```julia @@ -26,10 +28,8 @@ end Strategies.id(::Type{<:MyModeler}) = :my_modeler -function (modeler::MyModeler)( - prob::Optimization.AbstractOptimizationProblem, - initial_guess -) +# In the package providing the concrete problem type: +function Optimization.build_model(prob::MyProblem, initial_guess, ::MyModeler) # Build NLP model from problem and initial guess return nlp_model end @@ -38,61 +38,3 @@ end See also: `Strategies.AbstractStrategy`, `Optimization.build_model`, `Optimization.build_solution` """ abstract type AbstractNLPModeler <: Strategies.AbstractStrategy end - -""" -$(TYPEDSIGNATURES) - -Build an NLP model from a discretized optimal control problem and initial guess. - -# Arguments -- `modeler::AbstractNLPModeler`: The modeler strategy instance -- `prob::Optimization.AbstractOptimizationProblem`: The discretized optimal control problem -- `initial_guess`: Initial guess for optimization variables - -# Returns -- An NLP model compatible with the target backend (e.g., ADNLPModel, ExaModel) - -# Throws -- `CTBase.Exceptions.NotImplemented`: If not implemented by concrete type -""" -function (modeler::AbstractNLPModeler)( - ::Optimization.AbstractOptimizationProblem, initial_guess -) - throw( - Exceptions.NotImplemented( - "Model building not implemented"; - required_method="(modeler::$(typeof(modeler)))(prob::Optimization.AbstractOptimizationProblem, initial_guess)", - suggestion="Implement the callable method for $(typeof(modeler)) to build NLP models", - context="AbstractNLPModeler - required method implementation", - ), - ) -end - -""" -$(TYPEDSIGNATURES) - -Build a solution object from a discretized optimal control problem and NLP solution. - -# Arguments -- `modeler::AbstractNLPModeler`: The modeler strategy instance -- `prob::Optimization.AbstractOptimizationProblem`: The discretized optimal control problem -- `nlp_solution::SolverCore.AbstractExecutionStats`: Solution from NLP solver - -# Returns -- A solution object appropriate for the problem type - -# Throws -- `CTBase.Exceptions.NotImplemented`: If not implemented by concrete type -""" -function (modeler::AbstractNLPModeler)( - ::Optimization.AbstractOptimizationProblem, ::SolverCore.AbstractExecutionStats -) - throw( - Exceptions.NotImplemented( - "Solution building not implemented"; - required_method="(modeler::$(typeof(modeler)))(prob::Optimization.AbstractOptimizationProblem, nlp_solution::SolverCore.AbstractExecutionStats)", - suggestion="Implement the callable method for $(typeof(modeler)) to build solution objects", - context="AbstractNLPModeler - required method implementation", - ), - ) -end diff --git a/src/Modelers/adnlp.jl b/src/Modelers/adnlp.jl index 8edc33d0..d8e80d4d 100644 --- a/src/Modelers/adnlp.jl +++ b/src/Modelers/adnlp.jl @@ -468,89 +468,9 @@ function Modelers.ADNLP(; mode::Symbol=:strict, kwargs...) return Modelers.ADNLP{P}(; mode=mode, kwargs...) end -# Model building interface -""" -$(TYPEDSIGNATURES) - -Build an ADNLPModel from a discretized optimal control problem. - -# Arguments -- `modeler::Modelers.ADNLP`: Configured modeler instance -- `prob::AbstractOptimizationProblem`: Discretized optimal control problem -- `initial_guess`: Initial guess for optimization variables - -# Returns -- `ADNLPModels.ADNLPModel`: Built NLP model - -# Examples -```julia -# Create modeler -modeler = Modelers.ADNLP(backend=:optimized) - -# Build model from problem -nlp = modeler(problem, initial_guess) - -# Solve the model -stats = solve(nlp, solver) -``` - -# See also - -- `Modelers.ADNLP`: Type documentation -- `build_model`: Generic model building interface -- `ADNLPModels.ADNLPModel`: NLP model type -""" -function (modeler::Modelers.ADNLP)( - prob::AbstractOptimizationProblem, initial_guess -)::ADNLPModels.ADNLPModel - # Get the appropriate builder for this problem type - builder = get_adnlp_model_builder(prob) - - # Extract options as Dict - options = Strategies.options_dict(modeler) - - # Build the ADNLP model passing all options generically - return builder(initial_guess; options...) -end - -# Solution building interface -""" -$(TYPEDSIGNATURES) - -Build a solution object from NLP solver statistics. - -# Arguments -- `modeler::Modelers.ADNLP`: Configured modeler instance -- `prob::AbstractOptimizationProblem`: Original optimization problem -- `nlp_solution::SolverCore.AbstractExecutionStats`: NLP solver statistics - -# Returns -- Solution object appropriate for the problem type - -# Examples -```julia -# Create modeler and solve -modeler = Modelers.ADNLP() -nlp = modeler(problem, initial_guess) -stats = solve(nlp, solver) - -# Build solution object -solution = modeler(problem, stats) -``` - -# See also - -- `Modelers.ADNLP`: Type documentation -- `SolverCore.AbstractExecutionStats`: Solver statistics type -- `solve`: Generic solve interface -""" -function (modeler::Modelers.ADNLP)( - prob::AbstractOptimizationProblem, nlp_solution::SolverCore.AbstractExecutionStats -) - # Get the appropriate solution builder for this problem type - builder = get_adnlp_solution_builder(prob) - return builder(nlp_solution) -end +# Model building / solution building are implemented by multiple dispatch on +# `(prob, ::Modelers.ADNLP)` in the package providing the problem type (e.g. +# CTDirect), via `Optimization.build_model` / `Optimization.build_solution`. # ============================================================================ # Backend validation factory diff --git a/src/Modelers/exa.jl b/src/Modelers/exa.jl index d4d7c0f5..0c48191f 100644 --- a/src/Modelers/exa.jl +++ b/src/Modelers/exa.jl @@ -379,90 +379,6 @@ function Modelers.Exa(; mode::Symbol=:strict, kwargs...) return Modelers.Exa{P}(; mode=mode, kwargs...) end -# Model building interface -""" -$(TYPEDSIGNATURES) - -Build an ExaModel from a discretized optimal control problem. - -# Arguments -- `modeler::Modelers.Exa{P}`: Configured modeler instance -- `prob::AbstractOptimizationProblem`: Discretized optimal control problem -- `initial_guess`: Initial guess for optimization variables - -# Returns -- `ExaModels.ExaModel`: Built NLP model - -# Examples -```julia -# Create modeler -modeler = Modelers.Exa(base_type=Float64) - -# Build model from problem -nlp = modeler(problem, initial_guess) - -# Solve the model -stats = solve(nlp, solver) -``` - -# See also - -- `Modelers.Exa`: Type documentation -- `build_model`: Generic model building interface -- `ExaModels.ExaModel`: NLP model type -""" -function (modeler::Modelers.Exa{P})( - prob::AbstractOptimizationProblem, initial_guess -)::ExaModels.ExaModel where {P<:AbstractStrategyParameter} - # Get the appropriate builder for this problem type - builder = get_exa_model_builder(prob) - - # Extract options as Dict - options = Strategies.options_dict(modeler) - - # Extract BaseType and remove it from options to avoid passing it as named argument - BaseType = options[:base_type] - delete!(options, :base_type) - - # Build the ExaModel passing BaseType as first argument and remaining options as named arguments - return builder(BaseType, initial_guess; options...) -end - -# Solution building interface -""" -$(TYPEDSIGNATURES) - -Build a solution object from NLP solver statistics. - -# Arguments -- `modeler::Modelers.Exa{P}`: Configured modeler instance -- `prob::AbstractOptimizationProblem`: Original optimization problem -- `nlp_solution::SolverCore.AbstractExecutionStats`: NLP solver statistics - -# Returns -- Solution object appropriate for the problem type - -# Examples -```julia -# Create modeler and solve -modeler = Modelers.Exa() -nlp = modeler(problem, initial_guess) -stats = solve(nlp, solver) - -# Build solution object -solution = modeler(problem, stats) -``` - -# See also - -- `Modelers.Exa`: Type documentation -- `SolverCore.AbstractExecutionStats`: Solver statistics type -- `solve`: Generic solve interface -""" -function (modeler::Modelers.Exa{P})( - prob::AbstractOptimizationProblem, nlp_solution::SolverCore.AbstractExecutionStats -) where {P<:AbstractStrategyParameter} - # Get the appropriate solution builder for this problem type - builder = get_exa_solution_builder(prob) - return builder(nlp_solution) -end +# Model building / solution building are implemented by multiple dispatch on +# `(prob, ::Modelers.Exa)` in the package providing the problem type (e.g. +# CTDirect), via `Optimization.build_model` / `Optimization.build_solution`. diff --git a/src/Optimization/Optimization.jl b/src/Optimization/Optimization.jl index 1467bdf9..8657aaad 100644 --- a/src/Optimization/Optimization.jl +++ b/src/Optimization/Optimization.jl @@ -7,13 +7,10 @@ Optimization module. This module defines the abstract optimization problem interface -(`AbstractOptimizationProblem`) together with the builder pattern used by -modelers: -- model builders construct backend NLP models from an initial guess -- solution builders convert solver statistics into domain-level solutions - -The functions `build_model` and `build_solution` provide a backend-agnostic API -delegating the actual work to the selected modeler strategy. +(`AbstractOptimizationProblem`) and the backend-agnostic model/solution building +contract (`build_model` / `build_solution`). Concrete problem types (e.g. +`DiscretizedModel`) and the packages providing them implement these by multiple +dispatch on `(problem, modeler)`. """ module Optimization @@ -25,23 +22,11 @@ using SolverCore: SolverCore # Submodules include(joinpath(@__DIR__, "abstract_types.jl")) -include(joinpath(@__DIR__, "builders.jl")) -include(joinpath(@__DIR__, "contract.jl")) include(joinpath(@__DIR__, "building.jl")) include(joinpath(@__DIR__, "solver_info.jl")) # Public API - Abstract types export AbstractOptimizationProblem -export AbstractBuilder, AbstractModelBuilder, AbstractSolutionBuilder -export AbstractOCPSolutionBuilder - -# Public API - Concrete builder types -export ADNLPModelBuilder, ExaModelBuilder -export ADNLPSolutionBuilder, ExaSolutionBuilder - -# Public API - Contract functions -export get_adnlp_model_builder, get_exa_model_builder -export get_adnlp_solution_builder, get_exa_solution_builder # Public API - Model building functions export build_model, build_solution diff --git a/src/Optimization/abstract_types.jl b/src/Optimization/abstract_types.jl index efa38b76..56dc760c 100644 --- a/src/Optimization/abstract_types.jl +++ b/src/Optimization/abstract_types.jl @@ -11,8 +11,9 @@ This is a general type that represents any optimization problem, not necessarily tied to optimal control. Subtypes can represent various problem formulations including discretized optimal control problems, general NLP problems, etc. -Subtypes are typically paired with AbstractModelBuilder and AbstractSolutionBuilder -implementations that know how to construct and interpret NLP back-end models and solutions. +Subtypes implement the `build_model` / `build_solution` contract (by multiple +dispatch on `(problem, modeler)`) to construct and interpret NLP back-end models +and solutions. # Example ```julia diff --git a/src/Optimization/builders.jl b/src/Optimization/builders.jl deleted file mode 100644 index ce6b14c3..00000000 --- a/src/Optimization/builders.jl +++ /dev/null @@ -1,206 +0,0 @@ -# Optimization builders -# -# Abstract builder types and concrete builder implementations. - -""" -$(TYPEDEF) - -Abstract base type for all builders in the optimization system. - -This provides a common interface for model builders and solution builders -that work with optimization problems. -""" -abstract type AbstractBuilder end - -""" -$(TYPEDEF) - -Abstract base type for builders that construct NLP back-end models from -an AbstractOptimizationProblem. - -Concrete subtypes are callable objects that encapsulate the logic for building -a model for a specific NLP back-end. -""" -abstract type AbstractModelBuilder <: AbstractBuilder end - -""" -$(TYPEDEF) - -Abstract base type for builders that transform NLP solutions into other -representations (for example, solutions of an optimal control problem). - -Subtypes are callable objects that convert NLP solver results into -problem-specific solution formats. -""" -abstract type AbstractSolutionBuilder <: AbstractBuilder end - -""" -$(TYPEDEF) - -Abstract base type for builders that transform NLP solutions into OCP solutions. - -Concrete implementations should define the exact call signature and behavior -for specific solution types. -""" -abstract type AbstractOCPSolutionBuilder <: AbstractSolutionBuilder end - -# ============================================================================ # -# Concrete Builder Implementations -# ============================================================================ # - -""" -$(TYPEDEF) - -Builder for constructing ADNLPModels-based NLP models. - -This is a callable object that wraps a function for building ADNLPModels. -The wrapped function should accept an initial guess and keyword arguments. - -# Fields -- `f::T`: A callable that builds the ADNLPModel when invoked - -# Example -```julia -builder = ADNLPModelBuilder(build_adnlp_model) -nlp = builder(initial_guess; show_time=false, backend=:optimized) -``` -""" -struct ADNLPModelBuilder{T<:Function} <: AbstractModelBuilder - f::T -end - -""" -$(TYPEDSIGNATURES) - -Invoke the ADNLPModels model builder to construct an NLP model from an initial guess. - -# Arguments -- `builder::ADNLPModelBuilder`: The builder instance -- `initial_guess`: Initial guess for optimization variables -- `kwargs...`: Additional options passed to the builder function - -# Returns -- `ADNLPModels.ADNLPModel`: The constructed NLP model -""" -function (builder::ADNLPModelBuilder)(initial_guess; kwargs...) - return builder.f(initial_guess; kwargs...) -end - -""" -$(TYPEDEF) - -Builder for constructing ExaModels-based NLP models. - -This is a callable object that wraps a function for building ExaModels. -The wrapped function should accept a base type, initial guess, and keyword arguments. - -# Fields -- `f::T`: A callable that builds the ExaModel when invoked - -# Example -```julia -builder = ExaModelBuilder(build_exa_model) -nlp = builder(Float64, initial_guess; backend=nothing, minimize=true) -``` -""" -struct ExaModelBuilder{T<:Function} <: AbstractModelBuilder - f::T -end - -""" -$(TYPEDSIGNATURES) - -Invoke the ExaModels model builder to construct an NLP model from an initial guess. - -The `BaseType` parameter specifies the floating-point type for the model. - -# Arguments -- `builder::ExaModelBuilder`: The builder instance -- `BaseType::Type{<:AbstractFloat}`: Floating-point type for the model -- `initial_guess`: Initial guess for optimization variables -- `kwargs...`: Additional options passed to the builder function - -# Returns -- `ExaModels.ExaModel{BaseType}`: The constructed NLP model -""" -function (builder::ExaModelBuilder)( - ::Type{BaseType}, initial_guess; kwargs... -) where {BaseType<:AbstractFloat} - return builder.f(BaseType, initial_guess; kwargs...) -end - -""" -$(TYPEDEF) - -Builder for constructing OCP solutions from ADNLP solver results. - -This is a callable object that wraps a function for converting NLP solver -statistics into optimal control solutions. - -# Fields -- `f::T`: A callable that builds the solution when invoked - -# Example -```julia -builder = ADNLPSolutionBuilder(build_adnlp_solution) -sol = builder(nlp_stats) -``` -""" -struct ADNLPSolutionBuilder{T<:Function} <: AbstractOCPSolutionBuilder - f::T -end - -""" -$(TYPEDSIGNATURES) - -Invoke the ADNLPModels solution builder to convert NLP execution statistics -into an optimal control solution. - -# Arguments -- `builder::ADNLPSolutionBuilder`: The builder instance -- `nlp_solution`: NLP solver execution statistics - -# Returns -- Optimal control solution (type depends on the wrapped function) -""" -function (builder::ADNLPSolutionBuilder)(nlp_solution) - return builder.f(nlp_solution) -end - -""" -$(TYPEDEF) - -Builder for constructing OCP solutions from ExaModels solver results. - -This is a callable object that wraps a function for converting NLP solver -statistics into optimal control solutions. - -# Fields -- `f::T`: A callable that builds the solution when invoked - -# Example -```julia -builder = ExaSolutionBuilder(build_exa_solution) -sol = builder(nlp_stats) -``` -""" -struct ExaSolutionBuilder{T<:Function} <: AbstractOCPSolutionBuilder - f::T -end - -""" -$(TYPEDSIGNATURES) - -Invoke the ExaModels solution builder to convert NLP execution statistics -into an optimal control solution. - -# Arguments -- `builder::ExaSolutionBuilder`: The builder instance -- `nlp_solution`: NLP solver execution statistics - -# Returns -- Optimal control solution (type depends on the wrapped function) -""" -function (builder::ExaSolutionBuilder)(nlp_solution) - return builder.f(nlp_solution) -end diff --git a/src/Optimization/building.jl b/src/Optimization/building.jl index 25a20cb1..38de0af3 100644 --- a/src/Optimization/building.jl +++ b/src/Optimization/building.jl @@ -26,8 +26,15 @@ nlp = build_model(prob, initial_guess, modeler) See also: `build_solution` """ -function build_model(prob, initial_guess, modeler) - return modeler(prob, initial_guess) +function build_model(prob::AbstractOptimizationProblem, initial_guess, modeler) + throw( + Exceptions.NotImplemented( + "Model building not implemented"; + required_method="build_model(prob::$(typeof(prob)), initial_guess, modeler::$(typeof(modeler)))", + suggestion="Implement build_model for this (problem, modeler) pair in the package providing the problem", + context="Optimization.build_model - required method implementation", + ), + ) end """ @@ -53,6 +60,13 @@ sol = build_solution(prob, nlp_stats, modeler) See also: `build_model` """ -function build_solution(prob, model_solution, modeler) - return modeler(prob, model_solution) +function build_solution(prob::AbstractOptimizationProblem, model_solution, modeler) + throw( + Exceptions.NotImplemented( + "Solution building not implemented"; + required_method="build_solution(prob::$(typeof(prob)), 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", + ), + ) end diff --git a/src/Optimization/contract.jl b/src/Optimization/contract.jl deleted file mode 100644 index 2972d3d2..00000000 --- a/src/Optimization/contract.jl +++ /dev/null @@ -1,156 +0,0 @@ -# Optimization problem contract -# -# Contract methods required for an `AbstractOptimizationProblem` to interact with -# the model building and solution building pipeline. - -""" -$(TYPEDSIGNATURES) - -Get the ADNLPModels model builder for an optimization problem. - -This is part of the `AbstractOptimizationProblem` contract. Concrete problem types -must implement this method to provide a builder that constructs ADNLPModels from -the problem. - -# Arguments -- `prob::AbstractOptimizationProblem`: The optimization problem - -# Returns -- `AbstractModelBuilder`: A callable builder that constructs ADNLPModels - -# Throws - -- `CTBase.Exceptions.NotImplemented`: If the problem type does not support the ADNLPModels backend - -# Example -```julia -builder = get_adnlp_model_builder(prob) -nlp = builder(initial_guess; show_time=false, backend=:optimized) -``` - -See also: `get_exa_model_builder`, `build_model` -""" -function get_adnlp_model_builder(prob::AbstractOptimizationProblem) - throw( - Exceptions.NotImplemented( - "ADNLP model builder not implemented"; - required_method="get_adnlp_model_builder(prob::$(typeof(prob)))", - suggestion="Implement get_adnlp_model_builder for $(typeof(prob)) to support ADNLPModels backend", - context="AbstractOptimizationProblem.get_adnlp_model_builder - required method implementation", - ), - ) -end - -""" -$(TYPEDSIGNATURES) - -Get the ExaModels model builder for an optimization problem. - -This is part of the `AbstractOptimizationProblem` contract. Concrete problem types -must implement this method to provide a builder that constructs ExaModels from -the problem. - -# Arguments -- `prob::AbstractOptimizationProblem`: The optimization problem - -# Returns -- `AbstractModelBuilder`: A callable builder that constructs ExaModels - -# Throws - -- `CTBase.Exceptions.NotImplemented`: If the problem type does not support the ExaModels backend - -# Example -```julia -builder = get_exa_model_builder(prob) -nlp = builder(Float64, initial_guess; backend=nothing, minimize=true) -``` - -See also: `get_adnlp_model_builder`, `build_model` -""" -function get_exa_model_builder(prob::AbstractOptimizationProblem) - throw( - Exceptions.NotImplemented( - "ExaModel builder not implemented"; - required_method="get_exa_model_builder(prob::$(typeof(prob)))", - suggestion="Implement get_exa_model_builder for $(typeof(prob)) to support ExaModels backend", - context="AbstractOptimizationProblem.get_exa_model_builder - required method implementation", - ), - ) -end - -""" -$(TYPEDSIGNATURES) - -Get the ADNLPModels solution builder for an optimization problem. - -This is part of the `AbstractOptimizationProblem` contract. Concrete problem types -must implement this method to provide a builder that converts NLP solver results -into problem-specific solutions. - -# Arguments -- `prob::AbstractOptimizationProblem`: The optimization problem - -# Returns -- `AbstractSolutionBuilder`: A callable builder that constructs solutions from NLP results - -# Throws - -- `CTBase.Exceptions.NotImplemented`: If the problem type does not support the ADNLPModels backend - -# Example -```julia -builder = get_adnlp_solution_builder(prob) -sol = builder(nlp_stats) -``` - -See also: `get_exa_solution_builder`, `build_solution` -""" -function get_adnlp_solution_builder(prob::AbstractOptimizationProblem) - throw( - Exceptions.NotImplemented( - "ADNLP solution builder not implemented"; - required_method="get_adnlp_solution_builder(prob::$(typeof(prob)))", - suggestion="Implement get_adnlp_solution_builder for $(typeof(prob)) to support ADNLPModels backend", - context="AbstractOptimizationProblem.get_adnlp_solution_builder - required method implementation", - ), - ) -end - -""" -$(TYPEDSIGNATURES) - -Get the ExaModels solution builder for an optimization problem. - -This is part of the `AbstractOptimizationProblem` contract. Concrete problem types -must implement this method to provide a builder that converts NLP solver results -into problem-specific solutions. - -# Arguments -- `prob::AbstractOptimizationProblem`: The optimization problem - -# Returns -- `AbstractSolutionBuilder`: A callable builder that constructs solutions from NLP results - -# Throws - -- `CTBase.Exceptions.NotImplemented`: If the problem type does not support the ExaModels backend - -# Example -```julia -builder = get_exa_solution_builder(prob) -sol = builder(nlp_stats) -``` - -See also: `get_adnlp_solution_builder`, `build_solution` -""" -function get_exa_solution_builder(prob::AbstractOptimizationProblem) - throw( - Exceptions.NotImplemented( - "ExaSolution builder not implemented"; - required_method="get_exa_solution_builder(prob::$(typeof(prob)))", - suggestion="Implement get_exa_solution_builder for $(typeof(prob)) to support ExaModels backend", - context="AbstractOptimizationProblem.get_exa_solution_builder - required method implementation", - ), - ) -end diff --git a/src/Solvers/abstract_solver.jl b/src/Solvers/abstract_solver.jl index 4549004c..bb802d91 100644 --- a/src/Solvers/abstract_solver.jl +++ b/src/Solvers/abstract_solver.jl @@ -9,8 +9,8 @@ All concrete solver types must: - `Strategies.id(::Type{<:MySolver})` - Return unique Symbol identifier - `Strategies.metadata(::Type{<:MySolver})` - Return StrategyMetadata with options - Have an `options::Strategies.StrategyOptions` field -3. Implement the callable interface: - - `(solver::MySolver)(nlp; display=Bool)` - Solve the NLP problem +3. Implement the solve method (typically in a backend extension): + - `CommonSolve.solve(nlp::NLPModels.AbstractNLPModel, solver::MySolver; display=Bool)` # Solver Types - `Solvers.Ipopt` - Interior point optimizer (Ipopt backend) @@ -20,55 +20,16 @@ All concrete solver types must: # Example ```julia +using CommonSolve + # Create solver with options solver = Solvers.Ipopt(max_iter=1000, tol=1e-8) # Solve an NLP problem nlp = ADNLPModel(x -> sum(x.^2), zeros(10)) -stats = solver(nlp, display=true) +stats = solve(nlp, solver; display=true) ``` -See also: `Solvers.Ipopt`, `Solvers.MadNLP`, `Solvers.MadNCL`, `Solvers.Knitro` +See also: `Solvers.Ipopt`, `Solvers.MadNLP`, `Solvers.MadNCL`, `Solvers.Knitro`, `CommonSolve.solve` """ abstract type AbstractNLPSolver <: Strategies.AbstractStrategy end - -""" -$(TYPEDSIGNATURES) - -Callable interface for optimization solvers. - -Solves the given NLP problem and returns execution statistics. - -# Arguments -- `nlp`: NLP problem to solve (typically `NLPModels.AbstractNLPModel`) -- `display::Bool`: Whether to display solver output (default: true) - -# Returns -- `SolverCore.AbstractExecutionStats`: Solver execution statistics - -# Throws -- `CTBase.Exceptions.NotImplemented`: If not implemented by a concrete solver type - -# Implementation -Concrete solver types must implement this method. The default implementation -throws a `NotImplemented` error with helpful guidance. - -# Example -```julia -solver = Solvers.Ipopt(max_iter=100) -nlp = ADNLPModel(x -> sum(x.^2), zeros(5)) -stats = solver(nlp, display=false) -``` - -See also: `AbstractNLPSolver`, `CommonSolve.solve` -""" -function (solver::AbstractNLPSolver)(nlp; display::Bool=true) - throw( - Exceptions.NotImplemented( - "Solver callable not implemented"; - required_method="(solver::$(typeof(solver)))(nlp; display=Bool)", - suggestion="Implement the callable method for $(typeof(solver))", - context="AbstractNLPSolver - required method", - ), - ) -end diff --git a/src/Solvers/common_solve_api.jl b/src/Solvers/common_solve_api.jl index e48ce305..f4b617d6 100644 --- a/src/Solvers/common_solve_api.jl +++ b/src/Solvers/common_solve_api.jl @@ -64,7 +64,13 @@ end """ $(TYPEDSIGNATURES) -Mid-level solve: Solve NLP problem directly. +Mid-level solve: Solve an NLP problem directly. + +# Contract +Concrete solvers implement this method, typically in a backend extension, +dispatching on the solver type, e.g. +`CommonSolve.solve(nlp::NLPModels.AbstractNLPModel, solver::Ipopt; display)` in +the `CTSolversIpopt` extension. This generic stub throws `NotImplemented`. # Arguments - `nlp::NLPModels.AbstractNLPModel`: The NLP problem to solve @@ -74,40 +80,17 @@ Mid-level solve: Solve NLP problem directly. # Returns - `SolverCore.AbstractExecutionStats`: Solver execution statistics -# Example -```julia -# Conceptual usage pattern -# nlp = ADNLPModel(x -> sum(x.^2), zeros(10)) -# solver = Solvers.Ipopt() -# stats = solve(nlp, solver, display=false) -``` - See also: `AbstractNLPSolver` """ function CommonSolve.solve( nlp::NLPModels.AbstractNLPModel, solver::AbstractNLPSolver; display::Bool=__display() -)::SolverCore.AbstractExecutionStats - return solver(nlp; display=display) -end - -""" -$(TYPEDSIGNATURES) - -Flexible solve: Allow user freedom with any compatible types. - -This method provides flexibility for users to pass different types -that may be compatible with the solver's callable interface. - -# Arguments -- `nlp`: Problem to solve (any type compatible with solver) -- `solver::AbstractNLPSolver`: Solver to use -- `display::Bool`: Whether to show solver output (default: true) - -# Returns -- Result from solver (type depends on solver implementation) - -See also: `CommonSolve.solve`, `AbstractNLPSolver` -""" -function CommonSolve.solve(nlp, solver::AbstractNLPSolver; display::Bool=__display()) - return solver(nlp; display=display) +) + throw( + Exceptions.NotImplemented( + "Solve not implemented for this solver"; + required_method="CommonSolve.solve(nlp::NLPModels.AbstractNLPModel, solver::$(typeof(solver)); display)", + suggestion="Load the backend extension providing $(typeof(solver)) (e.g. `using NLPModelsIpopt`)", + context="Solvers.solve - required method implementation", + ), + ) end diff --git a/test/problems/elec.jl b/test/problems/elec.jl index 51b7e05c..65abab75 100644 --- a/test/problems/elec.jl +++ b/test/problems/elec.jl @@ -74,12 +74,7 @@ function Elec(; np::Int=5, seed::Int=2713) return ExaModels.ExaModel(m) end - prob = OptimizationProblem( - CTSolvers.ADNLPModelBuilder(build_adnlp_model), - CTSolvers.ExaModelBuilder(build_exa_model), - ADNLPSolutionBuilder(), - ExaSolutionBuilder(), - ) + prob = OptimizationProblem(build_adnlp_model, build_exa_model) theta = (2π) .* rand(np) phi = π .* rand(np) diff --git a/test/problems/max1minusx2.jl b/test/problems/max1minusx2.jl index adfe75a6..cd99fa85 100644 --- a/test/problems/max1minusx2.jl +++ b/test/problems/max1minusx2.jl @@ -40,12 +40,7 @@ function Max1MinusX2() return ExaModels.ExaModel(m) end - prob = OptimizationProblem( - CTSolvers.ADNLPModelBuilder(build_adnlp_model), - CTSolvers.ExaModelBuilder(build_exa_model), - ADNLPSolutionBuilder(), - ExaSolutionBuilder(), - ) + prob = OptimizationProblem(build_adnlp_model, build_exa_model) init = [2.0] sol = [0.0] diff --git a/test/problems/problems_definition.jl b/test/problems/problems_definition.jl index 08c4f237..7c18084e 100644 --- a/test/problems/problems_definition.jl +++ b/test/problems/problems_definition.jl @@ -1,39 +1,38 @@ -# Helper optimization problem and solution-builder types used by benchmark test problems. -# Helper types -abstract type AbstractNLPSolutionBuilder <: CTSolvers.AbstractSolutionBuilder end -struct ADNLPSolutionBuilder <: AbstractNLPSolutionBuilder end -struct ExaSolutionBuilder <: AbstractNLPSolutionBuilder end - +# Helper optimization problem type used by benchmark test problems. # -struct OptimizationProblem <: CTSolvers.AbstractOptimizationProblem - build_adnlp_model::CTSolvers.ADNLPModelBuilder - build_exa_model::CTSolvers.ExaModelBuilder - adnlp_solution_builder::ADNLPSolutionBuilder - exa_solution_builder::ExaSolutionBuilder -end - -function CTSolvers.get_adnlp_model_builder(prob::OptimizationProblem) - return prob.build_adnlp_model -end +# `OptimizationProblem` wraps the per-backend model builders as plain functions +# and implements the CTSolvers `build_model` / `build_solution` contract by +# multiple dispatch on the modeler type. -function CTSolvers.get_exa_model_builder(prob::OptimizationProblem) - return prob.build_exa_model -end +import CTSolvers.Optimization +import CTSolvers.Modelers -function (builder::ADNLPSolutionBuilder)(nlp_solution::SolverCore.AbstractExecutionStats) - return nlp_solution +struct OptimizationProblem{A,E} <: CTSolvers.AbstractOptimizationProblem + build_adnlp_model::A + build_exa_model::E end -function (builder::ExaSolutionBuilder)(nlp_solution::SolverCore.AbstractExecutionStats) - return nlp_solution +# Build the ADNLP model from the wrapped builder. +function Optimization.build_model( + prob::OptimizationProblem, initial_guess, ::Modelers.ADNLP +) + return prob.build_adnlp_model(initial_guess) end -function CTSolvers.get_adnlp_solution_builder(prob::OptimizationProblem) - return prob.adnlp_solution_builder +# 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) end -function CTSolvers.get_exa_solution_builder(prob::OptimizationProblem) - return prob.exa_solution_builder +# These benchmark problems return the raw NLP solver statistics as the solution. +function Optimization.build_solution( + ::OptimizationProblem, + nlp_solution::SolverCore.AbstractExecutionStats, + ::Modelers.AbstractNLPModeler, +) + return nlp_solution end # diff --git a/test/problems/rosenbrock.jl b/test/problems/rosenbrock.jl index 5586e619..7fb666ff 100644 --- a/test/problems/rosenbrock.jl +++ b/test/problems/rosenbrock.jl @@ -37,12 +37,7 @@ function Rosenbrock() return ExaModels.ExaModel(m) end - prob = OptimizationProblem( - CTSolvers.ADNLPModelBuilder(build_adnlp_model), - CTSolvers.ExaModelBuilder(build_exa_model), - ADNLPSolutionBuilder(), - ExaSolutionBuilder(), - ) + prob = OptimizationProblem(build_adnlp_model, build_exa_model) init = [-1.2; 1.0] sol = [1.0; 1.0] diff --git a/test/suite/docp/test_docp.jl b/test/suite/docp/test_docp.jl index e4daf33d..75d41bc8 100644 --- a/test/suite/docp/test_docp.jl +++ b/test/suite/docp/test_docp.jl @@ -6,6 +6,7 @@ using CTSolvers: CTSolvers import CTSolvers.DOCP import CTSolvers.Optimization import CTSolvers.Modelers +import CTBase using NLPModels: NLPModels using SolverCore: SolverCore using ADNLPModels: ADNLPModels @@ -27,6 +28,16 @@ struct FakeOCP <: CTModels.AbstractModel name::String end +""" +Fake discretizer for testing DiscretizedModel construction and dispatch. +""" +struct FakeDiscretizer <: DOCP.AbstractDiscretizer end + +""" +Fake backend cache attached to a DiscretizedModel. +""" +struct FakeCache <: CTBase.Core.AbstractCache end + """ Mock execution statistics for testing. """ @@ -38,35 +49,46 @@ mutable struct MockExecutionStats <: SolverCore.AbstractExecutionStats end """ -Fake modeler for testing building functions. -Subtypes Modelers.AbstractNLPModeler to satisfy the type annotation on nlp_model/ocp_solution. +Fake modeler for testing the build_model / build_solution contract. """ struct FakeModelerDOCP <: Modelers.AbstractNLPModeler backend::Symbol end -function (modeler::FakeModelerDOCP)(prob::DOCP.DiscretizedModel, initial_guess) +# Contract implementation by dispatch on (DiscretizedModel{<:FakeDiscretizer}, FakeModelerDOCP) +function Optimization.build_model( + ::DOCP.DiscretizedModel{<:Any,<:FakeDiscretizer}, + initial_guess, + modeler::FakeModelerDOCP, +) if modeler.backend == :adnlp - builder = Optimization.get_adnlp_model_builder(prob) - return builder(initial_guess) + return ADNLPModels.ADNLPModel(z -> sum(z .^ 2), initial_guess) else - builder = Optimization.get_exa_model_builder(prob) - return builder(Float64, initial_guess) + 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) end end -function (modeler::FakeModelerDOCP)( - prob::DOCP.DiscretizedModel, nlp_solution::SolverCore.AbstractExecutionStats +function Optimization.build_solution( + ::DOCP.DiscretizedModel{<:Any,<:FakeDiscretizer}, + nlp_solution::SolverCore.AbstractExecutionStats, + ::FakeModelerDOCP, ) - if modeler.backend == :adnlp - builder = Optimization.get_adnlp_solution_builder(prob) - return builder(nlp_solution) - else - builder = Optimization.get_exa_solution_builder(prob) - return builder(nlp_solution) - end + return ( + objective=nlp_solution.objective, + iter=nlp_solution.iter, + status=nlp_solution.status, + success=(nlp_solution.status == :first_order || nlp_solution.status == :acceptable), + ) end +# Helper: build a DiscretizedModel around the fake OCP / discretizer / cache. +fake_docp(name::String="test_ocp") = + DOCP.DiscretizedModel(FakeOCP(name), FakeDiscretizer(), FakeCache()) + # ============================================================================ # TEST FUNCTION # ============================================================================ @@ -79,15 +101,13 @@ function test_docp() # ==================================================================== Test.@testset "Exports verification" begin - # Test that DOCP module is available Test.@testset "DOCP Module" begin Test.@test isdefined(CTSolvers, :DOCP) Test.@test CTSolvers.DOCP isa Module end - # Test exported types Test.@testset "Exported Types" begin - for T in (DiscretizedModel,) + for T in (DiscretizedModel, AbstractDiscretizer) Test.@testset "$(nameof(T))" begin Test.@test isdefined(DOCP, nameof(T)) Test.@test isdefined(CurrentModule, nameof(T)) @@ -96,9 +116,8 @@ function test_docp() end end - # Test exported functions Test.@testset "Exported Functions" begin - for f in (:ocp_model, :nlp_model, :ocp_solution) + for f in (:ocp_model, :nlp_model, :ocp_solution, :discretize) Test.@testset "$f" begin Test.@test isdefined(DOCP, f) Test.@test isdefined(CurrentModule, f) @@ -114,140 +133,23 @@ function test_docp() Test.@testset "DOCP.DiscretizedModel Type" begin Test.@testset "Construction" begin - # Create builders - adnlp_builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) - ) - exa_builder = Optimization.ExaModelBuilder( - (T, x) -> begin - m = ExaModels.ExaCore(T; concrete=Val(true)) - ExaModels.@add_var(m, x_var, length(x); start=x) - # Define objective using ExaModels syntax (like Rosenbrock) - obj_func(v) = sum(v[i]^2 for i in 1:length(x)) - ExaModels.@add_obj(m, obj_func(x_var)) - ExaModels.ExaModel(m) - end - ) - adnlp_sol_builder = Optimization.ADNLPSolutionBuilder( - s -> (objective=s.objective,) - ) - exa_sol_builder = Optimization.ExaSolutionBuilder( - s -> (objective=s.objective,) - ) - - # Create fake OCP ocp = FakeOCP("test_ocp") - - # Create DOCP - docp = DOCP.DiscretizedModel( - ocp, adnlp_builder, exa_builder, adnlp_sol_builder, exa_sol_builder - ) + disc = FakeDiscretizer() + cache = FakeCache() + docp = DOCP.DiscretizedModel(ocp, disc, cache) Test.@test docp isa DOCP.DiscretizedModel Test.@test docp isa Optimization.AbstractOptimizationProblem - Test.@test docp.optimal_control_problem === ocp - Test.@test docp.adnlp_model_builder === adnlp_builder - Test.@test docp.exa_model_builder === exa_builder - Test.@test docp.adnlp_solution_builder === adnlp_sol_builder - Test.@test docp.exa_solution_builder === exa_sol_builder + Test.@test docp.ocp === ocp + Test.@test docp.discretizer === disc + Test.@test docp.cache === cache end Test.@testset "Type parameters" begin - ocp = FakeOCP("test") - adnlp_builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) - ) - exa_builder = Optimization.ExaModelBuilder( - (T, x) -> begin - m = ExaModels.ExaCore(T; concrete=Val(true)) - ExaModels.@add_var(m, x_var, length(x); start=x) - # Define objective using ExaModels syntax (like Rosenbrock) - obj_func(v) = sum(v[i]^2 for i in 1:length(x)) - ExaModels.@add_obj(m, obj_func(x_var)) - ExaModels.ExaModel(m) - end - ) - adnlp_sol_builder = Optimization.ADNLPSolutionBuilder( - s -> (objective=s.objective,) - ) - exa_sol_builder = Optimization.ExaSolutionBuilder( - s -> (objective=s.objective,) - ) - - docp = DOCP.DiscretizedModel( - ocp, adnlp_builder, exa_builder, adnlp_sol_builder, exa_sol_builder - ) - - Test.@test typeof(docp.optimal_control_problem) == FakeOCP - Test.@test typeof(docp.adnlp_model_builder) <: - Optimization.ADNLPModelBuilder - Test.@test typeof(docp.exa_model_builder) <: Optimization.ExaModelBuilder - Test.@test typeof(docp.adnlp_solution_builder) <: - Optimization.ADNLPSolutionBuilder - Test.@test typeof(docp.exa_solution_builder) <: - Optimization.ExaSolutionBuilder - end - end - - # ==================================================================== - # UNIT TESTS - Contract Implementation - # ==================================================================== - - Test.@testset "Contract Implementation" begin - # Setup - ocp = FakeOCP("test_ocp") - adnlp_builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) - ) - exa_builder = Optimization.ExaModelBuilder( - (T, x) -> begin - n = length(x) - m = ExaModels.ExaCore(T; concrete=Val(true)) - ExaModels.@add_var(m, x_var, n; start=x) - ExaModels.@add_obj(m, sum(x_var[i]^2 for i in 1:n)) - ExaModels.ExaModel(m) - end - ) - adnlp_sol_builder = Optimization.ADNLPSolutionBuilder( - s -> (objective=s.objective,) - ) - exa_sol_builder = Optimization.ExaSolutionBuilder(s -> (objective=s.objective,)) - - docp = DOCP.DiscretizedModel( - ocp, adnlp_builder, exa_builder, adnlp_sol_builder, exa_sol_builder - ) - - Test.@testset "Optimization.get_adnlp_model_builder" begin - builder = Optimization.get_adnlp_model_builder(docp) - Test.@test builder === adnlp_builder - Test.@test builder isa Optimization.ADNLPModelBuilder - end - - Test.@testset "Optimization.get_exa_model_builder" begin - builder = Optimization.get_exa_model_builder(docp) - Test.@test builder === exa_builder - Test.@test builder isa Optimization.ExaModelBuilder - end - - Test.@testset "Optimization.get_adnlp_solution_builder" begin - builder = Optimization.get_adnlp_solution_builder(docp) - Test.@test builder === adnlp_sol_builder - Test.@test builder isa Optimization.ADNLPSolutionBuilder - end - - Test.@testset "Optimization.get_exa_solution_builder" begin - builder = Optimization.get_exa_solution_builder(docp) - Test.@test builder === exa_sol_builder - Test.@test builder isa Optimization.ExaSolutionBuilder - end - - Test.@testset "Type stability" begin - Test.@test_nowarn Test.@inferred Optimization.get_adnlp_model_builder(docp) - Test.@test_nowarn Test.@inferred Optimization.get_exa_model_builder(docp) - Test.@test_nowarn Test.@inferred Optimization.get_adnlp_solution_builder( - docp - ) - Test.@test_nowarn Test.@inferred Optimization.get_exa_solution_builder(docp) + docp = fake_docp() + Test.@test typeof(docp.ocp) == FakeOCP + Test.@test typeof(docp.discretizer) <: DOCP.AbstractDiscretizer + Test.@test typeof(docp.cache) <: CTBase.Core.AbstractCache end end @@ -258,69 +160,23 @@ function test_docp() Test.@testset "Accessors" begin Test.@testset "ocp_model" begin ocp = FakeOCP("my_ocp") - adnlp_builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) - ) - exa_builder = Optimization.ExaModelBuilder( - (T, x) -> begin - m = ExaModels.ExaCore(T; concrete=Val(true)) - ExaModels.@add_var(m, x_var, length(x); start=x) - # Define objective using ExaModels syntax (like Rosenbrock) - obj_func(v) = sum(v[i]^2 for i in 1:length(x)) - ExaModels.@add_obj(m, obj_func(x_var)) - ExaModels.ExaModel(m) - end - ) - adnlp_sol_builder = Optimization.ADNLPSolutionBuilder( - s -> (objective=s.objective,) - ) - exa_sol_builder = Optimization.ExaSolutionBuilder( - s -> (objective=s.objective,) - ) - - docp = DOCP.DiscretizedModel( - ocp, adnlp_builder, exa_builder, adnlp_sol_builder, exa_sol_builder - ) + docp = DOCP.DiscretizedModel(ocp, FakeDiscretizer(), FakeCache()) retrieved_ocp = DOCP.ocp_model(docp) Test.@test retrieved_ocp === ocp Test.@test retrieved_ocp.name == "my_ocp" - Test.@test_nowarn Test.@inferred DOCP.ocp_model(docp) end end # ==================================================================== - # UNIT TESTS - Building Functions + # UNIT TESTS - Building Functions (build_model / build_solution) # ==================================================================== Test.@testset "Building Functions" begin - # Setup - ocp = FakeOCP("test_ocp") - adnlp_builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) - ) - exa_builder = Optimization.ExaModelBuilder( - (T, x) -> begin - n = length(x) - m = ExaModels.ExaCore(T; concrete=Val(true)) - ExaModels.@add_var(m, x_var, n; start=x) - ExaModels.@add_obj(m, sum(x_var[i]^2 for i in 1:n)) - ExaModels.ExaModel(m) - end - ) - adnlp_sol_builder = Optimization.ADNLPSolutionBuilder( - s -> (objective=s.objective, status=s.status) - ) - exa_sol_builder = Optimization.ExaSolutionBuilder( - s -> (objective=s.objective, iter=s.iter) - ) - - docp = DOCP.DiscretizedModel( - ocp, adnlp_builder, exa_builder, adnlp_sol_builder, exa_sol_builder - ) - - Test.@testset "nlp_model with ADNLP" begin + docp = fake_docp() + + Test.@testset "nlp_model with ADNLP backend" begin modeler = FakeModelerDOCP(:adnlp) x0 = [1.0, 2.0] @@ -334,7 +190,7 @@ function test_docp() Test.@test NLPModels.obj(nlp2, x0) ≈ 5.0 end - Test.@testset "nlp_model with Exa" begin + Test.@testset "nlp_model with Exa backend" begin modeler = FakeModelerDOCP(:exa) x0 = [1.0, 2.0] @@ -348,20 +204,21 @@ function test_docp() Test.@test NLPModels.obj(nlp2, x0) ≈ 5.0 end - Test.@testset "ocp_solution with ADNLP" begin + Test.@testset "ocp_solution with ADNLP backend" begin modeler = FakeModelerDOCP(:adnlp) stats = MockExecutionStats(1.23, 10, 1e-6, :first_order) sol = DOCP.ocp_solution(docp, 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) Test.@test sol2.objective ≈ 1.23 Test.@test sol2.status == :first_order end - Test.@testset "ocp_solution with Exa" begin + Test.@testset "ocp_solution with Exa backend" begin modeler = FakeModelerDOCP(:exa) stats = MockExecutionStats(2.34, 15, 1e-5, :acceptable) @@ -376,156 +233,64 @@ function test_docp() end # ==================================================================== - # INTEGRATION TESTS + # UNIT TESTS - Contract stubs (NotImplemented) # ==================================================================== - Test.@testset "Integration Tests" begin - Test.@testset "Complete DOCP workflow - ADNLP" begin - # Create OCP - ocp = FakeOCP("integration_test_ocp") + Test.@testset "Contract stubs" begin + docp = fake_docp() - # Create builders - adnlp_builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) - ) - exa_builder = Optimization.ExaModelBuilder( - (T, x) -> begin - m = ExaModels.ExaCore(T) - x_var = ExaModels.variable(m, length(x); start=x) - # Define objective using ExaModels syntax (like Rosenbrock) - obj_func(v) = sum(v[i]^2 for i in 1:length(x)) - ExaModels.objective(m, obj_func(x_var)) - ExaModels.ExaModel(m) - end - ) - adnlp_sol_builder = Optimization.ADNLPSolutionBuilder( - s -> ( - objective=s.objective, - iterations=s.iter, - status=s.status, - success=(s.status == :first_order || s.status == :acceptable), - ), - ) - exa_sol_builder = Optimization.ExaSolutionBuilder( - s -> (objective=s.objective, iter=s.iter) + Test.@testset "build_model NotImplemented for unknown modeler" begin + # A modeler with no (problem, modeler) method falls back to the + # generic stub, which throws NotImplemented. + Test.@test_throws CTBase.Exceptions.NotImplemented Optimization.build_model( + docp, [1.0], Modelers.ADNLP() ) + end - # Create DOCP - docp = DOCP.DiscretizedModel( - ocp, adnlp_builder, exa_builder, adnlp_sol_builder, exa_sol_builder + Test.@testset "discretize NotImplemented for fake discretizer" begin + Test.@test_throws CTBase.Exceptions.NotImplemented DOCP.discretize( + FakeOCP("x"), FakeDiscretizer() ) + end + end + + # ==================================================================== + # INTEGRATION TESTS + # ==================================================================== - # Verify OCP retrieval - Test.@test DOCP.ocp_model(docp) === ocp + Test.@testset "Integration Tests" begin + Test.@testset "Complete DOCP workflow - ADNLP" begin + docp = fake_docp("integration_test_ocp") + Test.@test DOCP.ocp_model(docp).name == "integration_test_ocp" - # Build NLP model 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 - # Build solution stats = MockExecutionStats(14.0, 20, 1e-8, :first_order) sol = DOCP.ocp_solution(docp, stats, modeler) - Test.@test sol.objective ≈ 14.0 - Test.@test sol.iterations == 20 + Test.@test sol.iter == 20 Test.@test sol.status == :first_order Test.@test sol.success === true end Test.@testset "Complete DOCP workflow - Exa" begin - # Create OCP - ocp = FakeOCP("integration_test_exa") - - # Create builders - adnlp_builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) - ) - exa_builder = Optimization.ExaModelBuilder( - (T, x) -> begin - m = ExaModels.ExaCore(T; concrete=Val(true)) - ExaModels.@add_var(m, x_var, length(x); start=x) - # Define objective using ExaModels syntax (like Rosenbrock) - obj_func(v) = sum(v[i]^2 for i in 1:length(x)) - ExaModels.@add_obj(m, obj_func(x_var)) - ExaModels.ExaModel(m) - end - ) - adnlp_sol_builder = Optimization.ADNLPSolutionBuilder( - s -> (objective=s.objective,) - ) - exa_sol_builder = Optimization.ExaSolutionBuilder( - s -> (objective=s.objective, iterations=s.iter, status=s.status) - ) - - # Create DOCP - docp = DOCP.DiscretizedModel( - ocp, adnlp_builder, exa_builder, adnlp_sol_builder, exa_sol_builder - ) - - # Verify OCP retrieval - Test.@test DOCP.ocp_model(docp) === ocp - - # Build NLP model + 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 - # Build solution stats = MockExecutionStats(14.0, 25, 1e-7, :acceptable) sol = DOCP.ocp_solution(docp, stats, modeler) - Test.@test sol.objective ≈ 14.0 - Test.@test sol.iterations == 25 + Test.@test sol.iter == 25 Test.@test sol.status == :acceptable end - - Test.@testset "DOCP with different base types" begin - ocp = FakeOCP("base_type_test") - - # Create builders - adnlp_builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) - ) - exa_builder = Optimization.ExaModelBuilder( - (T, x) -> begin - m = ExaModels.ExaCore(T; concrete=Val(true)) - ExaModels.@add_var(m, x_var, length(x); start=x) - # Define objective using ExaModels syntax (like Rosenbrock) - obj_func(v) = sum(v[i]^2 for i in 1:length(x)) - ExaModels.@add_obj(m, obj_func(x_var)) - ExaModels.ExaModel(m) - end - ) - adnlp_sol_builder = Optimization.ADNLPSolutionBuilder( - s -> (objective=s.objective,) - ) - exa_sol_builder = Optimization.ExaSolutionBuilder( - s -> (objective=s.objective,) - ) - - docp = DOCP.DiscretizedModel( - ocp, adnlp_builder, exa_builder, adnlp_sol_builder, exa_sol_builder - ) - - # Test with Float64 - builder64 = Optimization.get_exa_model_builder(docp) - x0_64 = [1.0, 2.0] - nlp64 = builder64(Float64, x0_64) - Test.@test nlp64 isa ExaModels.ExaModel{Float64} - - # Test with Float32 - builder32 = Optimization.get_exa_model_builder(docp) - x0_32 = Float32[1.0, 2.0] - nlp32 = builder32(Float32, x0_32) - Test.@test nlp32 isa ExaModels.ExaModel{Float32} - end end end end diff --git a/test/suite/extensions/test_ipopt_extension.jl b/test/suite/extensions/test_ipopt_extension.jl index b118622c..9c74382f 100644 --- a/test/suite/extensions/test_ipopt_extension.jl +++ b/test/suite/extensions/test_ipopt_extension.jl @@ -131,8 +131,8 @@ function test_ipopt_extension() # Note: We can't easily test the internal behavior without actually solving, # but we can verify the solver accepts the display parameter - Test.@test_nowarn solver_verbose(nlp; display=false) - Test.@test_nowarn solver_verbose(nlp; display=true) + Test.@test_nowarn CommonSolve.solve(nlp, solver_verbose; display=false) + Test.@test_nowarn CommonSolve.solve(nlp, solver_verbose; display=true) end # ==================================================================== @@ -143,7 +143,7 @@ function test_ipopt_extension() ros = TestProblems.Rosenbrock() # Build NLP model from problem - adnlp_builder = CTSolvers.get_adnlp_model_builder(ros.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(ros.init) # Create solver with appropriate options @@ -157,7 +157,7 @@ function test_ipopt_extension() ) # Solve the problem - stats = solver(nlp; display=false) + stats = CommonSolve.solve(nlp, solver; display=false) # Check convergence Test.@test stats.status == :first_order @@ -169,12 +169,12 @@ function test_ipopt_extension() elec = TestProblems.Elec() # Build NLP model - adnlp_builder = CTSolvers.get_adnlp_model_builder(elec.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(elec.init) solver = Solvers.Ipopt(max_iter=1000, tol=1e-6, print_level=0) - stats = solver(nlp; display=false) + stats = CommonSolve.solve(nlp, solver; display=false) # Just check it converges Test.@test stats.status == :first_order @@ -184,12 +184,12 @@ function test_ipopt_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP model - adnlp_builder = CTSolvers.get_adnlp_model_builder(max_prob.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(max_prob.init) solver = Solvers.Ipopt(max_iter=1000, tol=1e-6, print_level=0) - stats = solver(nlp; display=false) + stats = CommonSolve.solve(nlp, solver; display=false) # Check convergence Test.@test stats.status == :first_order @@ -230,11 +230,11 @@ function test_ipopt_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP models - nlp1 = CTSolvers.get_adnlp_model_builder(ros.prob)(ros.init) - nlp2 = CTSolvers.get_adnlp_model_builder(max_prob.prob)(max_prob.init) + nlp1 = Optimization.build_model(ros.prob, ros.init, Modelers.ADNLP()) + nlp2 = Optimization.build_model(max_prob.prob, max_prob.init, Modelers.ADNLP()) - stats1 = solver(nlp1; display=false) - stats2 = solver(nlp2; display=false) + stats1 = CommonSolve.solve(nlp1, solver; display=false) + stats2 = CommonSolve.solve(nlp2, solver; display=false) Test.@test stats1.status == :first_order Test.@test stats2.status == :first_order @@ -483,7 +483,7 @@ function test_ipopt_extension() Test.@testset "Pass-through Verification" begin ros = TestProblems.Rosenbrock() - adnlp_builder = CTSolvers.get_adnlp_model_builder(ros.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(ros.init) # Test derivative_test="first-order" @@ -496,7 +496,7 @@ function test_ipopt_extension() redirect_stdout(devnull) do redirect_stderr(devnull) do # Just check it runs - Test.@test_nowarn solver(nlp; display=false) + Test.@test_nowarn CommonSolve.solve(nlp, solver; display=false) end end @@ -504,7 +504,7 @@ function test_ipopt_extension() solver_lbfgs = Solvers.Ipopt( max_iter=10, hessian_approximation="limited-memory", print_level=0, sb="yes" ) - Test.@test_nowarn solver_lbfgs(nlp; display=false) + Test.@test_nowarn CommonSolve.solve(nlp, solver_lbfgs; display=false) end # ==================================================================== @@ -513,7 +513,7 @@ function test_ipopt_extension() Test.@testset "Exhaustive Options Validation" begin ros = TestProblems.Rosenbrock() - adnlp_builder = CTSolvers.get_adnlp_model_builder(ros.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(ros.init) # Define all options with valid values to check for typos in names @@ -552,7 +552,7 @@ function test_ipopt_extension() solver = Solvers.Ipopt(; exhaustive_options...) # This should NOT throw any ErrorException about unknown options - Test.@test_nowarn solver(nlp; display=false) + Test.@test_nowarn CommonSolve.solve(nlp, solver; display=false) end end end diff --git a/test/suite/extensions/test_madncl_extension.jl b/test/suite/extensions/test_madncl_extension.jl index e7d536f4..19eb1b7d 100644 --- a/test/suite/extensions/test_madncl_extension.jl +++ b/test/suite/extensions/test_madncl_extension.jl @@ -209,7 +209,7 @@ function test_madncl_extension() Test.@testset "MadNLP Option Pass-through" begin # Create a simple dummy problem ros = TestProblems.Rosenbrock() - adnlp_builder = CTSolvers.get_adnlp_model_builder(ros.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(ros.init) # checking that it runs without error with these options @@ -218,7 +218,7 @@ function test_madncl_extension() ) # Just ensure the call works and options are accepted - Test.@test_nowarn solver(nlp, display=false) + Test.@test_nowarn CommonSolve.solve(nlp, solver; display=false) end # ==================================================================== @@ -229,7 +229,7 @@ function test_madncl_extension() # MadNCL requires problems with constraints # Using Elec problem which has constraints elec = TestProblems.Elec() - adnlp_builder = CTSolvers.get_adnlp_model_builder(elec.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(elec.init) # Test with display=false sets print_level=MadNLP.ERROR @@ -249,12 +249,12 @@ function test_madncl_extension() ros = TestProblems.Rosenbrock() # Build NLP model - adnlp_builder = CTSolvers.get_adnlp_model_builder(ros.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(ros.init) solver = Solvers.MadNCL(max_iter=1000, tol=1e-6, print_level=MadNLP.ERROR) - stats = solver(nlp; display=false) + stats = CommonSolve.solve(nlp, solver; display=false) # Just check it converges Test.@test Symbol(stats.status) in @@ -265,12 +265,12 @@ function test_madncl_extension() elec = TestProblems.Elec() # Build NLP model - adnlp_builder = CTSolvers.get_adnlp_model_builder(elec.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(elec.init) solver = Solvers.MadNCL(max_iter=3000, tol=1e-6, print_level=MadNLP.ERROR) - stats = solver(nlp; display=false) + stats = CommonSolve.solve(nlp, solver; display=false) # Just check it converges Test.@test Symbol(stats.status) in @@ -281,12 +281,12 @@ function test_madncl_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP model - adnlp_builder = CTSolvers.get_adnlp_model_builder(max_prob.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(max_prob.init) solver = Solvers.MadNCL(max_iter=1000, tol=1e-6, print_level=MadNLP.ERROR) - stats = solver(nlp; display=false) + stats = CommonSolve.solve(nlp, solver; display=false) # Check convergence Test.@test Symbol(stats.status) in @@ -351,14 +351,14 @@ function test_madncl_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP models - adnlp_builder1 = CTSolvers.get_adnlp_model_builder(elec.prob) + adnlp_builder1 = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()) nlp1 = adnlp_builder1(elec.init) - adnlp_builder2 = CTSolvers.get_adnlp_model_builder(max_prob.prob) + adnlp_builder2 = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) nlp2 = adnlp_builder2(max_prob.init) - stats1 = solver(nlp1; display=false) - stats2 = solver(nlp2; display=false) + stats1 = CommonSolve.solve(nlp1, solver; display=false) + stats2 = CommonSolve.solve(nlp2, solver; display=false) Test.@test Symbol(stats1.status) in (:SOLVE_SUCCEEDED, :SOLVED_TO_ACCEPTABLE_LEVEL) diff --git a/test/suite/extensions/test_madncl_extract_solver_infos.jl b/test/suite/extensions/test_madncl_extract_solver_infos.jl index d486e97e..0a7ef964 100644 --- a/test/suite/extensions/test_madncl_extract_solver_infos.jl +++ b/test/suite/extensions/test_madncl_extract_solver_infos.jl @@ -3,6 +3,7 @@ module TestExtMadNCL using Test: Test using CTSolvers: CTSolvers import CTSolvers.Optimization +import CTSolvers.Modelers using MadNCL: MadNCL using MadNLP: MadNLP using NLPModels: NLPModels @@ -34,7 +35,7 @@ function test_madncl_extract_solver_infos() ros = TestProblems.Rosenbrock() # Build NLP model - adnlp_builder = CTSolvers.get_adnlp_model_builder(ros.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(ros.init) # Configure MadNCL options @@ -69,7 +70,7 @@ function test_madncl_extract_solver_infos() max_prob = TestProblems.Max1MinusX2() # Build NLP model - adnlp_builder = CTSolvers.get_adnlp_model_builder(max_prob.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(max_prob.init) # Verify it's a maximization problem @@ -122,7 +123,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 = CTSolvers.get_adnlp_model_builder(ros.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(ros.init) # Configure MadNCL options @@ -152,7 +153,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 = CTSolvers.get_adnlp_model_builder(ros.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(ros.init) # Configure MadNCL options @@ -184,7 +185,7 @@ function test_madncl_extract_solver_infos() # Test with maximization problem for contract compliance max_prob = TestProblems.Max1MinusX2() - adnlp_builder_max = CTSolvers.get_adnlp_model_builder(max_prob.prob) + adnlp_builder_max = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) nlp_max = adnlp_builder_max(max_prob.init) # Configure MadNCL options @@ -219,7 +220,7 @@ function test_madncl_extract_solver_infos() # Test with minimization (Rosenbrock) ros = TestProblems.Rosenbrock() - adnlp_builder = CTSolvers.get_adnlp_model_builder(ros.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(ros.init) # Configure MadNCL options @@ -255,7 +256,7 @@ function test_madncl_extract_solver_infos() # Test with maximization problem (Max1MinusX2) max_prob = TestProblems.Max1MinusX2() - adnlp_builder_max = CTSolvers.get_adnlp_model_builder(max_prob.prob) + adnlp_builder_max = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) nlp_max = adnlp_builder_max(max_prob.init) # Configure MadNCL options diff --git a/test/suite/extensions/test_madnlp_extension.jl b/test/suite/extensions/test_madnlp_extension.jl index 22f11e30..b27321dd 100644 --- a/test/suite/extensions/test_madnlp_extension.jl +++ b/test/suite/extensions/test_madnlp_extension.jl @@ -208,9 +208,9 @@ function test_madnlp_extension() solver_verbose = Solvers.MadNLP(max_iter=10, print_level=MadNLP.INFO) # Verify the solver accepts the display parameter - Test.@test_nowarn solver_verbose(nlp; display=false) + Test.@test_nowarn CommonSolve.solve(nlp, solver_verbose; display=false) redirect_stdout(devnull) do - Test.@test_nowarn solver_verbose(nlp; display=true) + Test.@test_nowarn CommonSolve.solve(nlp, solver_verbose; display=true) end end @@ -222,7 +222,7 @@ function test_madnlp_extension() ros = TestProblems.Rosenbrock() # Build NLP model - adnlp_builder = Optimization.get_adnlp_model_builder(ros.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(ros.init) solver = Solvers.MadNLP( @@ -232,7 +232,7 @@ function test_madnlp_extension() linear_solver=MadNLP.MumpsSolver, ) - stats = solver(nlp; display=false) + stats = CommonSolve.solve(nlp, solver; display=false) # Check convergence Test.@test stats isa MadNLP.MadNLPExecutionStats @@ -245,12 +245,12 @@ function test_madnlp_extension() elec = TestProblems.Elec() # Build NLP model - adnlp_builder = Optimization.get_adnlp_model_builder(elec.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(elec.init) solver = Solvers.MadNLP(max_iter=1000, tol=1e-6, print_level=MadNLP.ERROR) - stats = solver(nlp; display=false) + stats = CommonSolve.solve(nlp, solver; display=false) # Just check it converges Test.@test Symbol(stats.status) in @@ -261,12 +261,12 @@ function test_madnlp_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP model - adnlp_builder = Optimization.get_adnlp_model_builder(max_prob.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(max_prob.init) solver = Solvers.MadNLP(max_iter=1000, tol=1e-6, print_level=MadNLP.ERROR) - stats = solver(nlp; display=false) + stats = CommonSolve.solve(nlp, solver; display=false) # Check convergence Test.@test Symbol(stats.status) in @@ -508,14 +508,14 @@ function test_madnlp_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP models - adnlp_builder = Optimization.get_adnlp_model_builder(ros.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) nlp1 = adnlp_builder(ros.init) - adnlp_builder2 = Optimization.get_adnlp_model_builder(max_prob.prob) + adnlp_builder2 = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) nlp2 = adnlp_builder2(max_prob.init) - stats1 = solver(nlp1; display=false) - stats2 = solver(nlp2; display=false) + stats1 = CommonSolve.solve(nlp1, solver; display=false) + stats2 = CommonSolve.solve(nlp2, solver; display=false) Test.@test Symbol(stats1.status) in (:SOLVE_SUCCEEDED, :SOLVED_TO_ACCEPTABLE_LEVEL) diff --git a/test/suite/extensions/test_uno_extension.jl b/test/suite/extensions/test_uno_extension.jl index 749b979f..cab5a12c 100644 --- a/test/suite/extensions/test_uno_extension.jl +++ b/test/suite/extensions/test_uno_extension.jl @@ -185,8 +185,8 @@ function test_uno_extension() # Note: We can't easily test the internal behavior without actually solving, # but we can verify the solver accepts the display parameter - Test.@test_nowarn solver_verbose(nlp; display=false) - Test.@test_nowarn solver_verbose(nlp; display=true) + Test.@test_nowarn CommonSolve.solve(nlp, solver_verbose; display=false) + Test.@test_nowarn CommonSolve.solve(nlp, solver_verbose; display=true) end # ==================================================================== @@ -197,7 +197,7 @@ function test_uno_extension() ros = TestProblems.Rosenbrock() # Build NLP model from problem - adnlp_builder = CTSolvers.get_adnlp_model_builder(ros.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(ros.init) # Create solver with appropriate options @@ -210,7 +210,7 @@ function test_uno_extension() ) # Solve the problem - stats = solver(nlp; display=false) + stats = CommonSolve.solve(nlp, solver; display=false) # Check convergence (stats is now GenericExecutionStats) Test.@test stats.status in (:first_order, :acceptable) @@ -222,14 +222,14 @@ function test_uno_extension() elec = TestProblems.Elec() # Build NLP model - adnlp_builder = CTSolvers.get_adnlp_model_builder(elec.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(elec.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(elec.init) solver = Solvers.Uno( max_iterations=1000, primal_tolerance=1e-6, logger="SILENT" ) - stats = solver(nlp; display=false) + stats = CommonSolve.solve(nlp, solver; display=false) # Just check it converges (stats is now GenericExecutionStats) Test.@test stats.status in (:first_order, :acceptable) @@ -239,14 +239,14 @@ function test_uno_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP model - adnlp_builder = CTSolvers.get_adnlp_model_builder(max_prob.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(max_prob.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(max_prob.init) solver = Solvers.Uno( max_iterations=1000, primal_tolerance=1e-6, logger="SILENT" ) - stats = solver(nlp; display=false) + stats = CommonSolve.solve(nlp, solver; display=false) # Check convergence (stats is now GenericExecutionStats) Test.@test stats.status in (:first_order, :acceptable) @@ -289,11 +289,11 @@ function test_uno_extension() max_prob = TestProblems.Max1MinusX2() # Build NLP models - nlp1 = CTSolvers.get_adnlp_model_builder(ros.prob)(ros.init) - nlp2 = CTSolvers.get_adnlp_model_builder(max_prob.prob)(max_prob.init) + nlp1 = Optimization.build_model(ros.prob, ros.init, Modelers.ADNLP()) + nlp2 = Optimization.build_model(max_prob.prob, max_prob.init, Modelers.ADNLP()) - stats1 = solver(nlp1; display=false) - stats2 = solver(nlp2; display=false) + stats1 = CommonSolve.solve(nlp1, solver; display=false) + stats2 = CommonSolve.solve(nlp2, solver; display=false) # Stats are now GenericExecutionStats Test.@test stats1.status in (:first_order, :acceptable) @@ -524,7 +524,7 @@ function test_uno_extension() Test.@testset "Exhaustive Options Validation" begin ros = TestProblems.Rosenbrock() - adnlp_builder = CTSolvers.get_adnlp_model_builder(ros.prob) + adnlp_builder = (init; kwargs...) -> Optimization.build_model(ros.prob, init, Modelers.ADNLP()) nlp = adnlp_builder(ros.init) # Define all options with valid values to check for typos in names @@ -540,7 +540,7 @@ function test_uno_extension() solver = Solvers.Uno(; exhaustive_options...) # This should NOT throw any ErrorException about unknown options - Test.@test_nowarn solver(nlp; display=false) + Test.@test_nowarn CommonSolve.solve(nlp, solver; display=false) end end end diff --git a/test/suite/integration/test_end_to_end.jl b/test/suite/integration/test_end_to_end.jl index 92297682..02ff516b 100644 --- a/test/suite/integration/test_end_to_end.jl +++ b/test/suite/integration/test_end_to_end.jl @@ -41,7 +41,7 @@ function test_end_to_end() Test.@test modeler isa Modelers.AbstractNLPModeler # Step 4: Build NLP model - nlp = modeler(prob, ros.init) + nlp = Optimization.build_model(prob, ros.init, modeler) Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test nlp.meta.nvar == 2 Test.@test nlp.meta.ncon == 1 @@ -100,7 +100,7 @@ function test_end_to_end() Test.@test modeler isa Modelers.Exa # Step 3: Build NLP model - nlp = modeler(prob, ros.init) + nlp = Optimization.build_model(prob, ros.init, modeler) Test.@test nlp isa ExaModels.ExaModel Test.@test nlp.meta.nvar == 2 Test.@test nlp.meta.ncon == 1 @@ -133,7 +133,7 @@ function test_end_to_end() modeler = Modelers.Exa( base_type=Float32, minimize=true; mode=:permissive ) - nlp = modeler(prob, ros.init) + nlp = Optimization.build_model(prob, ros.init, modeler) Test.@test nlp isa ExaModels.ExaModel Test.@test eltype(nlp.meta.x0) == Float32 @@ -149,7 +149,7 @@ function test_end_to_end() modeler = Modelers.Exa( base_type=Float64, minimize=true; mode=:permissive ) - nlp = modeler(prob, ros.init) + nlp = Optimization.build_model(prob, ros.init, modeler) Test.@test nlp isa ExaModels.ExaModel Test.@test eltype(nlp.meta.x0) == Float64 @@ -172,7 +172,7 @@ function test_end_to_end() Test.@testset "Modelers.ADNLP - Simple" begin # Test without options (defaults) modeler = Modelers.ADNLP() - nlp = modeler(prob, ros.init) + nlp = Optimization.build_model(prob, ros.init, modeler) Test.@test nlp isa ADNLPModels.ADNLPModel obj = NLPModels.obj(nlp, ros.init) @@ -182,13 +182,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 = modeler(prob, ros.init) + nlp = Optimization.build_model(prob, ros.init, modeler) 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 = modeler_backend(prob, ros.init) + nlp_backend = Optimization.build_model(prob, ros.init, modeler_backend) Test.@test nlp_backend isa ADNLPModels.ADNLPModel obj = NLPModels.obj(nlp_backend, ros.init) @@ -200,7 +200,7 @@ function test_end_to_end() Test.@testset "Modelers.Exa - Simple" begin # Test without options (defaults) modeler = Modelers.Exa(base_type=Float64) - nlp = modeler(prob, ros.init) + nlp = Optimization.build_model(prob, ros.init, modeler) Test.@test nlp isa ExaModels.ExaModel obj = NLPModels.obj(nlp, ros.init) @@ -213,7 +213,7 @@ function test_end_to_end() modeler = Modelers.Exa( base_type=Float64, minimize=true, backend=nothing; mode=:permissive ) - nlp = modeler(prob, ros.init) + nlp = Optimization.build_model(prob, ros.init, modeler) Test.@test nlp isa ExaModels.ExaModel obj = NLPModels.obj(nlp, ros.init) @@ -232,7 +232,7 @@ function test_end_to_end() # Build with ADNLP modeler_adnlp = Modelers.ADNLP(show_time=false) - nlp_adnlp = modeler_adnlp(prob, ros.init) + nlp_adnlp = Optimization.build_model(prob, ros.init, modeler_adnlp) obj_adnlp = NLPModels.obj(nlp_adnlp, ros.init) # Build with Exa (permissive mode for minimize option) @@ -240,7 +240,7 @@ function test_end_to_end() modeler_exa = Modelers.Exa( base_type=Float64, minimize=true; mode=:permissive ) - nlp_exa = modeler_exa(prob, ros.init) + nlp_exa = Optimization.build_model(prob, ros.init, modeler_exa) obj_exa = NLPModels.obj(nlp_exa, Float64.(ros.init)) # Both should give same objective @@ -262,7 +262,7 @@ function test_end_to_end() prob = ros.prob modeler = Modelers.ADNLP(show_time=false) - nlp = modeler(prob, ros.init) + nlp = Optimization.build_model(prob, ros.init, modeler) Test.@testset "Gradient at initial point" begin grad = NLPModels.grad(nlp, ros.init) @@ -294,7 +294,7 @@ function test_end_to_end() prob = ros.prob modeler = Modelers.ADNLP(show_time=false) - nlp = modeler(prob, ros.init) + nlp = Optimization.build_model(prob, ros.init, modeler) Test.@testset "Constraint at initial point" begin cons = NLPModels.cons(nlp, ros.init) @@ -327,14 +327,14 @@ function test_end_to_end() modeler = Modelers.ADNLP(show_time=false) # Should be fast - t = @elapsed nlp = modeler(prob, ros.init) + t = @elapsed nlp = Optimization.build_model(prob, ros.init, modeler) 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 = modeler(prob, ros.init) + nlp = Optimization.build_model(prob, ros.init, modeler) # Objective evaluation should be fast t = @elapsed obj = NLPModels.obj(nlp, ros.init) diff --git a/test/suite/modelers/test_coverage_modelers.jl b/test/suite/modelers/test_coverage_modelers.jl index 91488e0d..f8fdab9a 100644 --- a/test/suite/modelers/test_coverage_modelers.jl +++ b/test/suite/modelers/test_coverage_modelers.jl @@ -36,11 +36,15 @@ function test_coverage_modelers() prob = CovFakeProblem() stats = CovFakeStats() - # Model building callable - NotImplemented - Test.@test_throws Exceptions.NotImplemented modeler(prob, [1.0, 2.0]) - - # Solution building callable - NotImplemented - Test.@test_throws Exceptions.NotImplemented modeler(prob, stats) + # Model building contract - NotImplemented + Test.@test_throws Exceptions.NotImplemented Optimization.build_model( + prob, [1.0, 2.0], modeler + ) + + # Solution building contract - NotImplemented + Test.@test_throws Exceptions.NotImplemented Optimization.build_solution( + prob, stats, modeler + ) end Test.@testset "AbstractNLPModeler - type hierarchy" begin diff --git a/test/suite/optimization/test_error_cases.jl b/test/suite/optimization/test_error_cases.jl index c25d9f2d..ed8d9769 100644 --- a/test/suite/optimization/test_error_cases.jl +++ b/test/suite/optimization/test_error_cases.jl @@ -9,8 +9,9 @@ using ExaModels: ExaModels const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true -# Import from Optimization module +# Import from CTSolvers import CTSolvers.Optimization +import CTSolvers.Modelers # ============================================================================ # FAKE TYPES FOR ERROR TESTING (TOP-LEVEL) @@ -22,13 +23,45 @@ Minimal problem that doesn't implement the contract. struct MinimalProblemForErrors <: Optimization.AbstractOptimizationProblem end """ -Problem with only partial contract implementation. +Problem with only partial contract implementation (ADNLP only, no Exa). """ struct PartialProblem <: Optimization.AbstractOptimizationProblem end -# Implement only ADNLP builder -function Optimization.get_adnlp_model_builder(::PartialProblem) - Optimization.ADNLPModelBuilder(x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x)) +function Optimization.build_model(::PartialProblem, initial_guess, ::Modelers.ADNLP) + return ADNLPModels.ADNLPModel(z -> sum(z .^ 2), initial_guess) +end + +""" +Problem whose model builder always fails (to test error propagation). +""" +struct FailingProblem <: Optimization.AbstractOptimizationProblem end + +function Optimization.build_model(::FailingProblem, initial_guess, ::Modelers.ADNLP) + return error("Intentional error") +end + +function Optimization.build_solution( + ::FailingProblem, ::SolverCore.AbstractExecutionStats, ::Modelers.ADNLP +) + return error("Intentional error") +end + +""" +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) +end + +function Optimization.build_model(::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) end """ @@ -47,14 +80,6 @@ function create_edge_case_stats( ) end -""" -Type test stats for testing. -""" -mutable struct TypeTestStats <: SolverCore.AbstractExecutionStats - objective::Float64 - status::Symbol -end - # ============================================================================ # TEST FUNCTION # ============================================================================ @@ -62,10 +87,7 @@ end """ test_error_cases() -Tests for error cases and edge cases in Optimization module. - -This function tests error handling, NotImplemented errors, and edge cases -to ensure the module fails gracefully with clear error messages. +Tests for error cases and edge cases in the Optimization contract. """ function test_error_cases() Test.@testset "Error Cases and Edge Cases" verbose=VERBOSE showtiming=SHOWTIMING begin @@ -77,27 +99,16 @@ function test_error_cases() Test.@testset "NotImplemented Errors" begin prob = MinimalProblemForErrors() - Test.@testset "get_adnlp_model_builder - NotImplemented" begin - Test.@test_throws Exceptions.NotImplemented Optimization.get_adnlp_model_builder( - prob + Test.@testset "build_model - NotImplemented" begin + Test.@test_throws Exceptions.NotImplemented Optimization.build_model( + prob, [1.0, 2.0], Modelers.ADNLP() ) end - Test.@testset "get_exa_model_builder - NotImplemented" begin - Test.@test_throws Exceptions.NotImplemented Optimization.get_exa_model_builder( - prob - ) - end - - Test.@testset "get_adnlp_solution_builder - NotImplemented" begin - Test.@test_throws Exceptions.NotImplemented Optimization.get_adnlp_solution_builder( - prob - ) - end - - Test.@testset "get_exa_solution_builder - NotImplemented" begin - Test.@test_throws Exceptions.NotImplemented Optimization.get_exa_solution_builder( - prob + Test.@testset "build_solution - NotImplemented" begin + stats = create_edge_case_stats(1.0, 1, 1e-6, :first_order) + Test.@test_throws Exceptions.NotImplemented Optimization.build_solution( + prob, stats, Modelers.ADNLP() ) end end @@ -109,62 +120,37 @@ function test_error_cases() Test.@testset "Partial Contract Implementation" begin prob = PartialProblem() - Test.@testset "Implemented builder works" begin - builder = Optimization.get_adnlp_model_builder(prob) - Test.@test builder isa Optimization.ADNLPModelBuilder - - # Can build model with implemented builder + Test.@testset "Implemented backend works" begin x0 = [1.0, 2.0] - nlp = builder(x0) + nlp = Optimization.build_model(prob, x0, Modelers.ADNLP()) Test.@test nlp isa ADNLPModels.ADNLPModel end - Test.@testset "Non-implemented builders throw NotImplemented" begin - Test.@test_throws Exceptions.NotImplemented Optimization.get_exa_model_builder( - prob - ) - Test.@test_throws Exceptions.NotImplemented Optimization.get_adnlp_solution_builder( - prob - ) - Test.@test_throws Exceptions.NotImplemented Optimization.get_exa_solution_builder( - prob + Test.@testset "Non-implemented backend throws NotImplemented" begin + Test.@test_throws Exceptions.NotImplemented Optimization.build_model( + prob, [1.0, 2.0], Modelers.Exa() ) end end # ==================================================================== - # BUILDER ERRORS + # BUILDER ERRORS (error propagation through the contract) # ==================================================================== Test.@testset "Builder Errors" begin - Test.@testset "ADNLPModelBuilder with failing function" begin - # Builder that throws an error - failing_builder = Optimization.ADNLPModelBuilder( - x -> error("Intentional error") - ) + prob = FailingProblem() - Test.@test_throws ErrorException failing_builder([1.0, 2.0]) - end - - Test.@testset "ExaModelBuilder with failing function" begin - # Builder that throws an error - failing_builder = Optimization.ExaModelBuilder( - (T, x) -> error("Intentional error") + Test.@testset "build_model with failing implementation" begin + Test.@test_throws ErrorException Optimization.build_model( + prob, [1.0, 2.0], Modelers.ADNLP() ) - - Test.@test_throws ErrorException failing_builder(Float64, [1.0, 2.0]) end - Test.@testset "ADNLPSolutionBuilder with failing function" begin - # Builder that throws an error - failing_builder = Optimization.ADNLPSolutionBuilder( - s -> error("Intentional error") - ) - - # Mock stats + Test.@testset "build_solution with failing implementation" begin stats = MockStats(1.0) - - Test.@test_throws ErrorException failing_builder(stats) + Test.@test_throws ErrorException Optimization.build_solution( + prob, stats, Modelers.ADNLP() + ) end end @@ -173,16 +159,11 @@ function test_error_cases() # ==================================================================== Test.@testset "Edge Cases" begin - # Note: Empty initial guess (nvar=0) is not supported by ADNLPModels - # ADNLPModels requires nvar > 0, so we skip this edge case + prob = SquaresProblem() Test.@testset "Single variable problem" begin - builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> z[1]^2, x) - ) - x0 = [1.0] - nlp = builder(x0) + nlp = Optimization.build_model(prob, x0, Modelers.ADNLP()) Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test nlp.meta.nvar == 1 Test.@test NLPModels.obj(nlp, x0) ≈ 1.0 @@ -190,35 +171,22 @@ function test_error_cases() Test.@testset "Large dimension problem" begin n = 1000 - builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) - ) - x0 = ones(n) - nlp = builder(x0) + nlp = Optimization.build_model(prob, x0, Modelers.ADNLP()) Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test nlp.meta.nvar == n end Test.@testset "Different numeric types" begin - # Float32 - builder32 = Optimization.ExaModelBuilder( - (T, x) -> begin - 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))) - ExaModels.ExaModel(m) - end, + nlp32 = Optimization.build_model( + prob, Float32[1.0, 2.0], Modelers.Exa(; base_type=Float32) ) - - x0_32 = Float32[1.0, 2.0] - nlp32 = builder32(Float32, x0_32) Test.@test nlp32 isa ExaModels.ExaModel{Float32} Test.@test eltype(nlp32.meta.x0) == Float32 - # Float64 - x0_64 = Float64[1.0, 2.0] - nlp64 = builder32(Float64, x0_64) + nlp64 = Optimization.build_model( + prob, Float64[1.0, 2.0], Modelers.Exa(; base_type=Float64) + ) Test.@test nlp64 isa ExaModels.ExaModel{Float64} Test.@test eltype(nlp64.meta.x0) == Float64 end @@ -231,7 +199,6 @@ 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( stats ) @@ -241,7 +208,6 @@ 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( stats ) @@ -251,7 +217,6 @@ 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( stats ) @@ -261,7 +226,6 @@ 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( stats ) @@ -275,29 +239,14 @@ function test_error_cases() # ==================================================================== Test.@testset "Type Stability" begin - Test.@testset "Builder return types" begin - adnlp_builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) - ) - x0 = [1.0, 2.0] + prob = SquaresProblem() - nlp = adnlp_builder(x0) + Test.@testset "build_model return type" begin + x0 = [1.0, 2.0] + nlp = Optimization.build_model(prob, x0, Modelers.ADNLP()) Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test typeof(nlp) <: ADNLPModels.ADNLPModel end - - Test.@testset "Solution builder return types" begin - sol_builder = Optimization.ADNLPSolutionBuilder( - s -> (obj=s.objective, status=s.status) - ) - - stats = TypeTestStats(1.0, :first_order) - - sol = sol_builder(stats) - Test.@test sol isa NamedTuple - Test.@test haskey(sol, :obj) - Test.@test haskey(sol, :status) - end end end end diff --git a/test/suite/optimization/test_optimization.jl b/test/suite/optimization/test_optimization.jl index 02aa685f..aa794aab 100644 --- a/test/suite/optimization/test_optimization.jl +++ b/test/suite/optimization/test_optimization.jl @@ -19,60 +19,41 @@ const CurrentModule = TestOptimization # ============================================================================ """ -Fake optimization problem for testing the contract interface. +Fake optimization problem implementing the build_model / build_solution contract. """ -struct FakeOptimizationProblem <: Optimization.AbstractOptimizationProblem - adnlp_builder::Optimization.ADNLPModelBuilder - exa_builder::Optimization.ExaModelBuilder - adnlp_solution_builder::Optimization.ADNLPSolutionBuilder - exa_solution_builder::Optimization.ExaSolutionBuilder -end - -# Implement contract for FakeOptimizationProblem -Optimization.get_adnlp_model_builder(prob::FakeOptimizationProblem) = prob.adnlp_builder -Optimization.get_exa_model_builder(prob::FakeOptimizationProblem) = prob.exa_builder -function Optimization.get_adnlp_solution_builder(prob::FakeOptimizationProblem) - prob.adnlp_solution_builder -end -function Optimization.get_exa_solution_builder(prob::FakeOptimizationProblem) - prob.exa_solution_builder -end +struct FakeOptimizationProblem <: Optimization.AbstractOptimizationProblem end """ -Minimal problem for testing NotImplemented errors. +Minimal problem for testing NotImplemented errors (no contract method defined). """ struct MinimalProblem <: Optimization.AbstractOptimizationProblem end """ -Fake modeler for testing building functions. +Fake modeler for testing building functions; carries a backend selector. """ struct FakeModeler backend::Symbol end -function (modeler::FakeModeler)( - prob::Optimization.AbstractOptimizationProblem, initial_guess +# Contract implementation by dispatch on (FakeOptimizationProblem, FakeModeler) +function Optimization.build_model( + ::FakeOptimizationProblem, initial_guess, modeler::FakeModeler ) if modeler.backend == :adnlp - builder = Optimization.get_adnlp_model_builder(prob) - return builder(initial_guess) + return ADNLPModels.ADNLPModel(z -> sum(z .^ 2), initial_guess) else - builder = Optimization.get_exa_model_builder(prob) - return builder(Float64, initial_guess) + 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) end end -function (modeler::FakeModeler)( - prob::Optimization.AbstractOptimizationProblem, - nlp_solution::SolverCore.AbstractExecutionStats, +function Optimization.build_solution( + ::FakeOptimizationProblem, nlp_solution::SolverCore.AbstractExecutionStats, ::FakeModeler ) - if modeler.backend == :adnlp - builder = Optimization.get_adnlp_solution_builder(prob) - return builder(nlp_solution) - else - builder = Optimization.get_exa_solution_builder(prob) - return builder(nlp_solution) - end + return (obj=nlp_solution.objective, iter=nlp_solution.iter, status=nlp_solution.status) end # TOP-LEVEL: Create GenericExecutionStats instances for testing @@ -91,14 +72,11 @@ end """ test_optimization() -Tests for Optimization module. - -This function tests the complete Optimization module including: -- Abstract types (AbstractOptimizationProblem, AbstractBuilder, etc.) -- Concrete builder types (ADNLPModelBuilder, ExaModelBuilder, etc.) -- Contract interface (get_*_builder functions) -- Building functions (build_model, build_solution) -- Solver utilities (extract_solver_infos) +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`) """ function test_optimization() Test.@testset "Optimization Module" verbose=VERBOSE showtiming=SHOWTIMING begin @@ -108,21 +86,13 @@ function test_optimization() # ==================================================================== Test.@testset "Exports verification" begin - # Test that Optimization module is available Test.@testset "Optimization Module" begin Test.@test isdefined(CTSolvers, :Optimization) Test.@test CTSolvers.Optimization isa Module end - # Test exported abstract types Test.@testset "Exported Abstract Types" begin - for T in ( - AbstractOptimizationProblem, - AbstractBuilder, - AbstractModelBuilder, - AbstractSolutionBuilder, - AbstractOCPSolutionBuilder, - ) + for T in (AbstractOptimizationProblem,) Test.@testset "$(nameof(T))" begin Test.@test isdefined(Optimization, nameof(T)) Test.@test isdefined(CurrentModule, nameof(T)) @@ -131,33 +101,8 @@ function test_optimization() end end - # Test exported concrete types - Test.@testset "Exported Concrete Types" begin - for T in ( - ADNLPModelBuilder, - ExaModelBuilder, - ADNLPSolutionBuilder, - ExaSolutionBuilder, - ) - Test.@testset "$(nameof(T))" begin - Test.@test isdefined(Optimization, nameof(T)) - Test.@test isdefined(CurrentModule, nameof(T)) - Test.@test T isa DataType || T isa UnionAll - end - end - end - - # Test exported functions Test.@testset "Exported Functions" begin - for f in ( - :get_adnlp_model_builder, - :get_exa_model_builder, - :get_adnlp_solution_builder, - :get_exa_solution_builder, - :build_model, - :build_solution, - :extract_solver_infos, - ) + for f in (:build_model, :build_solution, :extract_solver_infos) Test.@testset "$f" begin Test.@test isdefined(Optimization, f) Test.@test isdefined(CurrentModule, f) @@ -168,250 +113,53 @@ function test_optimization() end # ==================================================================== - # UNIT TESTS - Abstract Types - # ==================================================================== - - Test.@testset "Abstract Types" begin - Test.@testset "Type hierarchy" begin - Test.@test Optimization.AbstractOptimizationProblem <: Any - Test.@test Optimization.AbstractBuilder <: Any - Test.@test Optimization.AbstractModelBuilder <: Optimization.AbstractBuilder - Test.@test Optimization.AbstractSolutionBuilder <: - Optimization.AbstractBuilder - Test.@test Optimization.AbstractOCPSolutionBuilder <: - Optimization.AbstractSolutionBuilder - end - - Test.@testset "Contract interface - NotImplemented errors" begin - prob = MinimalProblem() - - Test.@test_throws Exceptions.NotImplemented Optimization.get_adnlp_model_builder( - prob - ) - Test.@test_throws Exceptions.NotImplemented Optimization.get_exa_model_builder( - prob - ) - Test.@test_throws Exceptions.NotImplemented Optimization.get_adnlp_solution_builder( - prob - ) - Test.@test_throws Exceptions.NotImplemented Optimization.get_exa_solution_builder( - prob - ) - end - end - - # ==================================================================== - # UNIT TESTS - Concrete Builder Types + # UNIT TESTS - Contract stubs (NotImplemented) # ==================================================================== - Test.@testset "Concrete Builder Types" begin - Test.@testset "ADNLPModelBuilder" begin - # Test construction - calls = Ref(0) - function test_builder(x; show_time=false) - calls[] += 1 - return ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x; show_time=show_time) - end - - builder = Optimization.ADNLPModelBuilder(test_builder) - Test.@test builder isa Optimization.ADNLPModelBuilder - Test.@test builder isa Optimization.AbstractModelBuilder - - # Test callable - x0 = [1.0, 2.0] - nlp = builder(x0) - Test.@test nlp isa ADNLPModels.ADNLPModel - Test.@test calls[] == 1 - Test.@test nlp.meta.x0 == x0 - - # Test with kwargs - redirect_stdout(devnull) do - nlp2 = builder(x0; show_time=true) - Test.@test calls[] == 2 - end - end - - Test.@testset "ExaModelBuilder" begin - # Test construction - calls = Ref(0) - function test_exa_builder(::Type{T}, x; backend=nothing) where {T} - calls[] += 1 - # Use correct ExaModels syntax (like in Rosenbrock) - m = ExaModels.ExaCore(T; concrete=Val(true), backend=backend) - 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) - end - - builder = Optimization.ExaModelBuilder(test_exa_builder) - Test.@test builder isa Optimization.ExaModelBuilder - Test.@test builder isa Optimization.AbstractModelBuilder - - # Test callable - x0 = [1.0, 2.0] - nlp = builder(Float64, x0) - Test.@test nlp isa ExaModels.ExaModel{Float64} - Test.@test calls[] == 1 - - # Test with different base type - nlp32 = builder(Float32, x0) - Test.@test nlp32 isa ExaModels.ExaModel{Float32} - Test.@test calls[] == 2 - end - - Test.@testset "ADNLPSolutionBuilder" begin - # Test construction - calls = Ref(0) - function test_solution_builder(stats) - calls[] += 1 - return (objective=stats.objective, status=stats.status) - end - - builder = Optimization.ADNLPSolutionBuilder(test_solution_builder) - Test.@test builder isa Optimization.ADNLPSolutionBuilder - Test.@test builder isa Optimization.AbstractOCPSolutionBuilder - - # Test callable - stats = create_mock_execution_stats(1.23, 10, 1e-6, :first_order) - sol = builder(stats) - Test.@test calls[] == 1 - Test.@test sol.objective ≈ 1.23 - Test.@test sol.status == :first_order - end - - Test.@testset "ExaSolutionBuilder" begin - # Test construction - calls = Ref(0) - function test_exa_solution_builder(stats) - calls[] += 1 - return (objective=stats.objective, iterations=stats.iter) - end - - builder = Optimization.ExaSolutionBuilder(test_exa_solution_builder) - Test.@test builder isa Optimization.ExaSolutionBuilder - Test.@test builder isa Optimization.AbstractOCPSolutionBuilder - - # Test callable - stats = create_mock_execution_stats(2.34, 15, 1e-5, :acceptable) - sol = builder(stats) - Test.@test calls[] == 1 - Test.@test sol.objective ≈ 2.34 - Test.@test sol.iterations == 15 - end - end - - # ==================================================================== - # UNIT TESTS - Contract Implementation - # ==================================================================== + Test.@testset "Contract stubs - NotImplemented errors" begin + prob = MinimalProblem() - Test.@testset "Contract Implementation" begin - # Create builders - adnlp_builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) + Test.@test_throws Exceptions.NotImplemented Optimization.build_model( + prob, [1.0], FakeModeler(:adnlp) ) - exa_builder = Optimization.ExaModelBuilder( - (T, x) -> begin - m = ExaModels.ExaCore(T; concrete=Val(true)) - ExaModels.@add_var(m, x_var, length(x); start=x) - # Define objective using ExaModels syntax (like Rosenbrock) - obj_func(v) = sum(v[i]^2 for i in 1:length(x)) - ExaModels.@add_obj(m, obj_func(x_var)) - ExaModels.ExaModel(m) - end - ) - adnlp_sol_builder = Optimization.ADNLPSolutionBuilder(s -> (obj=s.objective,)) - exa_sol_builder = Optimization.ExaSolutionBuilder(s -> (obj=s.objective,)) - - # Create fake problem - prob = FakeOptimizationProblem( - adnlp_builder, exa_builder, adnlp_sol_builder, exa_sol_builder + Test.@test_throws Exceptions.NotImplemented Optimization.build_solution( + prob, create_mock_execution_stats(1.0, 1, 1e-6, :first_order), FakeModeler(:adnlp) ) - - Test.@testset "get_adnlp_model_builder" begin - builder = Optimization.get_adnlp_model_builder(prob) - Test.@test builder === adnlp_builder - Test.@test builder isa Optimization.ADNLPModelBuilder - end - - Test.@testset "get_exa_model_builder" begin - builder = Optimization.get_exa_model_builder(prob) - Test.@test builder === exa_builder - Test.@test builder isa Optimization.ExaModelBuilder - end - - Test.@testset "get_adnlp_solution_builder" begin - builder = Optimization.get_adnlp_solution_builder(prob) - Test.@test builder === adnlp_sol_builder - Test.@test builder isa Optimization.ADNLPSolutionBuilder - end - - Test.@testset "get_exa_solution_builder" begin - builder = Optimization.get_exa_solution_builder(prob) - Test.@test builder === exa_sol_builder - Test.@test builder isa Optimization.ExaSolutionBuilder - end end # ==================================================================== - # UNIT TESTS - Building Functions + # UNIT TESTS - Building Functions (build_model / build_solution) # ==================================================================== Test.@testset "Building Functions" begin - # Setup - adnlp_builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) - ) - exa_builder = Optimization.ExaModelBuilder( - (T, x) -> begin - m = ExaModels.ExaCore(T; concrete=Val(true)) - ExaModels.@add_var(m, x_var, length(x); start=x) - # Define objective using ExaModels syntax (like Rosenbrock) - obj_func(v) = sum(v[i]^2 for i in 1:length(x)) - ExaModels.@add_obj(m, obj_func(x_var)) - ExaModels.ExaModel(m) - end - ) - adnlp_sol_builder = Optimization.ADNLPSolutionBuilder( - s -> (obj=s.objective, status=s.status) - ) - exa_sol_builder = Optimization.ExaSolutionBuilder( - s -> (obj=s.objective, iter=s.iter) - ) - - prob = FakeOptimizationProblem( - adnlp_builder, exa_builder, adnlp_sol_builder, exa_sol_builder - ) + prob = FakeOptimizationProblem() - Test.@testset "build_model with ADNLP" begin + Test.@testset "build_model with ADNLP backend" begin modeler = FakeModeler(:adnlp) x0 = [1.0, 2.0] - nlp = Optimization.build_model(prob, x0, modeler) Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test nlp.meta.x0 == x0 end - Test.@testset "build_model with Exa" begin + Test.@testset "build_model with Exa backend" begin modeler = FakeModeler(:exa) x0 = [1.0, 2.0] - nlp = Optimization.build_model(prob, x0, modeler) Test.@test nlp isa ExaModels.ExaModel{Float64} end - Test.@testset "build_solution with ADNLP" begin + Test.@testset "build_solution with ADNLP backend" begin modeler = FakeModeler(:adnlp) stats = create_mock_execution_stats(1.23, 10, 1e-6, :first_order) - sol = Optimization.build_solution(prob, stats, modeler) Test.@test sol.obj ≈ 1.23 Test.@test sol.status == :first_order end - Test.@testset "build_solution with Exa" begin + Test.@testset "build_solution with Exa backend" begin modeler = FakeModeler(:exa) stats = create_mock_execution_stats(2.34, 15, 1e-5, :acceptable) - sol = Optimization.build_solution(prob, stats, modeler) Test.@test sol.obj ≈ 2.34 Test.@test sol.iter == 15 @@ -425,47 +173,34 @@ 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( stats ) - Test.@test obj ≈ 1.23 Test.@test iter == 15 Test.@test viol ≈ 1.0e-6 Test.@test msg == "Ipopt/generic" Test.@test status == :first_order Test.@test success == true - Test.@test_nowarn Test.@inferred Optimization.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( stats ) - Test.@test obj ≈ 2.34 - Test.@test iter == 20 - Test.@test viol ≈ 1.0e-5 - Test.@test msg == "Ipopt/generic" Test.@test status == :acceptable Test.@test success == true end 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( stats ) - Test.@test obj ≈ 3.45 - Test.@test iter == 5 - Test.@test viol ≈ 1.0e-3 - Test.@test msg == "Ipopt/generic" Test.@test status == :max_iter Test.@test success == false end @@ -477,46 +212,18 @@ function test_optimization() Test.@testset "Integration Tests" begin Test.@testset "Complete workflow - ADNLP" begin - # Create builders - adnlp_builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) - ) - exa_builder = Optimization.ExaModelBuilder( - (T, x) -> begin - 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) - end, - ) - adnlp_sol_builder = Optimization.ADNLPSolutionBuilder( - s -> (objective=s.objective, status=s.status) - ) - exa_sol_builder = Optimization.ExaSolutionBuilder( - s -> (objective=s.objective, iter=s.iter) - ) - - # Create problem - prob = FakeOptimizationProblem( - adnlp_builder, exa_builder, adnlp_sol_builder, exa_sol_builder - ) - - # Build model + prob = FakeOptimizationProblem() modeler = FakeModeler(:adnlp) x0 = [1.0, 2.0] nlp = Optimization.build_model(prob, x0, modeler) - Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test NLPModels.obj(nlp, x0) ≈ 5.0 - # Build solution stats = create_mock_execution_stats(5.0, 10, 1e-6, :first_order) sol = Optimization.build_solution(prob, stats, modeler) - - Test.@test sol.objective ≈ 5.0 + Test.@test sol.obj ≈ 5.0 Test.@test sol.status == :first_order - # Extract solver info obj, iter, viol, msg, status, success = Optimization.extract_solver_infos( stats ) @@ -525,45 +232,16 @@ function test_optimization() end Test.@testset "Complete workflow - Exa" begin - # Create builders - adnlp_builder = Optimization.ADNLPModelBuilder( - x -> ADNLPModels.ADNLPModel(z -> sum(z .^ 2), x) - ) - exa_builder = Optimization.ExaModelBuilder( - (T, x) -> begin - n = length(x) - m = ExaModels.ExaCore(T; concrete=Val(true)) - ExaModels.@add_var(m, x_var, n; start=x) - # Define objective directly (like Rosenbrock does with F(x)) - ExaModels.@add_obj(m, sum(x_var[i]^2 for i in 1:n)) - ExaModels.ExaModel(m) - end - ) - adnlp_sol_builder = Optimization.ADNLPSolutionBuilder( - s -> (objective=s.objective, status=s.status) - ) - exa_sol_builder = Optimization.ExaSolutionBuilder( - s -> (objective=s.objective, iter=s.iter) - ) - - # Create problem - prob = FakeOptimizationProblem( - adnlp_builder, exa_builder, adnlp_sol_builder, exa_sol_builder - ) - - # Build model + prob = FakeOptimizationProblem() modeler = FakeModeler(:exa) x0 = [1.0, 2.0] nlp = Optimization.build_model(prob, x0, modeler) - Test.@test nlp isa ExaModels.ExaModel{Float64} Test.@test NLPModels.obj(nlp, x0) ≈ 5.0 - # Build solution stats = create_mock_execution_stats(5.0, 15, 1e-5, :acceptable) sol = Optimization.build_solution(prob, stats, modeler) - - Test.@test sol.objective ≈ 5.0 + Test.@test sol.obj ≈ 5.0 Test.@test sol.iter == 15 end end diff --git a/test/suite/optimization/test_real_problems.jl b/test/suite/optimization/test_real_problems.jl index e8ae83a9..f366f430 100644 --- a/test/suite/optimization/test_real_problems.jl +++ b/test/suite/optimization/test_real_problems.jl @@ -11,8 +11,9 @@ import .TestProblems const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true -# Import from Optimization module +# Import from CTSolvers import CTSolvers.Optimization +import CTSolvers.Modelers # ============================================================================ # TEST FUNCTION @@ -27,73 +28,48 @@ function test_real_problems() # ==================================================================== Test.@testset "Rosenbrock Problem" begin - # Load Rosenbrock problem from TestProblems module ros = TestProblems.Rosenbrock() - Test.@testset "ADNLPModelBuilder with Rosenbrock" begin - # Get the builder from the problem - builder = Optimization.get_adnlp_model_builder(ros.prob) - Test.@test builder isa Optimization.ADNLPModelBuilder - - # Build the NLP model - nlp = builder(ros.init; show_time=false) + Test.@testset "build_model (ADNLP) with Rosenbrock" begin + nlp = Optimization.build_model(ros.prob, ros.init, Modelers.ADNLP()) Test.@test nlp isa ADNLPModels.ADNLPModel Test.@test nlp.meta.x0 == ros.init Test.@test nlp.meta.minimize == true - # Test objective evaluation obj_val = NLPModels.obj(nlp, ros.init) - expected_obj = TestProblems.rosenbrock_objective(ros.init) - Test.@test obj_val ≈ expected_obj + Test.@test obj_val ≈ TestProblems.rosenbrock_objective(ros.init) - # Test constraint evaluation cons_val = NLPModels.cons(nlp, ros.init) - expected_cons = TestProblems.rosenbrock_constraint(ros.init) - Test.@test cons_val[1] ≈ expected_cons + Test.@test cons_val[1] ≈ TestProblems.rosenbrock_constraint(ros.init) end - Test.@testset "ExaModelBuilder with Rosenbrock" begin - # Get the builder from the problem - builder = Optimization.get_exa_model_builder(ros.prob) - Test.@test builder isa Optimization.ExaModelBuilder - - # Build the NLP model with Float64 - nlp64 = builder(Float64, ros.init) + Test.@testset "build_model (Exa, Float64) with Rosenbrock" begin + nlp64 = Optimization.build_model(ros.prob, ros.init, Modelers.Exa()) Test.@test nlp64 isa ExaModels.ExaModel{Float64} Test.@test nlp64.meta.x0 == Float64.(ros.init) Test.@test nlp64.meta.minimize == true - # Test objective evaluation obj_val = NLPModels.obj(nlp64, nlp64.meta.x0) - expected_obj = TestProblems.rosenbrock_objective(Float64.(ros.init)) - Test.@test obj_val ≈ expected_obj + Test.@test obj_val ≈ TestProblems.rosenbrock_objective(Float64.(ros.init)) - # Test constraint evaluation cons_val = NLPModels.cons(nlp64, nlp64.meta.x0) - expected_cons = TestProblems.rosenbrock_constraint(Float64.(ros.init)) - Test.@test cons_val[1] ≈ expected_cons + Test.@test cons_val[1] ≈ TestProblems.rosenbrock_constraint(Float64.(ros.init)) end - Test.@testset "ExaModelBuilder with Rosenbrock - Float32" begin - # Get the builder from the problem - builder = Optimization.get_exa_model_builder(ros.prob) - - # Build the NLP model with Float32 - nlp32 = builder(Float32, ros.init) + Test.@testset "build_model (Exa, Float32) with Rosenbrock" begin + nlp32 = Optimization.build_model( + ros.prob, ros.init, Modelers.Exa(; base_type=Float32) + ) Test.@test nlp32 isa ExaModels.ExaModel{Float32} Test.@test nlp32.meta.x0 == Float32.(ros.init) Test.@test eltype(nlp32.meta.x0) == Float32 Test.@test nlp32.meta.minimize == true - # Test objective evaluation obj_val = NLPModels.obj(nlp32, nlp32.meta.x0) - expected_obj = TestProblems.rosenbrock_objective(Float32.(ros.init)) - Test.@test obj_val ≈ expected_obj + Test.@test obj_val ≈ TestProblems.rosenbrock_objective(Float32.(ros.init)) - # Test constraint evaluation cons_val = NLPModels.cons(nlp32, nlp32.meta.x0) - expected_cons = TestProblems.rosenbrock_constraint(Float32.(ros.init)) - Test.@test cons_val[1] ≈ expected_cons + Test.@test cons_val[1] ≈ TestProblems.rosenbrock_constraint(Float32.(ros.init)) end end @@ -104,24 +80,14 @@ function test_real_problems() Test.@testset "Integration with Real Problems" begin Test.@testset "Complete workflow - Rosenbrock ADNLP" begin ros = TestProblems.Rosenbrock() - - # Get builder - builder = Optimization.get_adnlp_model_builder(ros.prob) - - # Build model - nlp = builder(ros.init; show_time=false) + nlp = Optimization.build_model(ros.prob, ros.init, Modelers.ADNLP()) Test.@test nlp isa ADNLPModels.ADNLPModel - - # Verify problem properties Test.@test nlp.meta.nvar == 2 Test.@test nlp.meta.ncon == 1 Test.@test nlp.meta.minimize == true - # Verify at initial point Test.@test NLPModels.obj(nlp, ros.init) ≈ TestProblems.rosenbrock_objective(ros.init) - - # Verify at solution Test.@test NLPModels.obj(nlp, ros.sol) ≈ TestProblems.rosenbrock_objective(ros.sol) Test.@test TestProblems.rosenbrock_objective(ros.sol) < @@ -130,24 +96,14 @@ function test_real_problems() Test.@testset "Complete workflow - Rosenbrock Exa" begin ros = TestProblems.Rosenbrock() - - # Get builder - builder = Optimization.get_exa_model_builder(ros.prob) - - # Build model - nlp = builder(Float64, ros.init) + nlp = Optimization.build_model(ros.prob, ros.init, Modelers.Exa()) Test.@test nlp isa ExaModels.ExaModel{Float64} - - # Verify problem properties Test.@test nlp.meta.nvar == 2 Test.@test nlp.meta.ncon == 1 Test.@test nlp.meta.minimize == true - # Verify at initial point Test.@test NLPModels.obj(nlp, Float64.(ros.init)) ≈ TestProblems.rosenbrock_objective(ros.init) - - # Verify at solution Test.@test NLPModels.obj(nlp, Float64.(ros.sol)) ≈ TestProblems.rosenbrock_objective(ros.sol) end diff --git a/test/suite/solvers/test_common_solve_api.jl b/test/suite/solvers/test_common_solve_api.jl index 499911bf..a1822677 100644 --- a/test/suite/solvers/test_common_solve_api.jl +++ b/test/suite/solvers/test_common_solve_api.jl @@ -26,9 +26,11 @@ end FakeSolver() = FakeSolver(Ref(0), Ref{Union{Nothing,Bool}}(nothing)) """ -Implement callable interface for FakeSolver. +Implement the CommonSolve.solve method for FakeSolver. """ -function (s::FakeSolver)(nlp::NLPModels.AbstractNLPModel; display::Bool=true) +function CommonSolve.solve( + nlp::NLPModels.AbstractNLPModel, s::FakeSolver; display::Bool=true +) s.calls[] += 1 s.display_flag[] = display # Return a valid GenericExecutionStats using the NLP model diff --git a/test/suite/solvers/test_coverage_solvers.jl b/test/suite/solvers/test_coverage_solvers.jl index b58851cf..b42cd500 100644 --- a/test/suite/solvers/test_coverage_solvers.jl +++ b/test/suite/solvers/test_coverage_solvers.jl @@ -21,15 +21,6 @@ struct CovUnimplementedSolver <: Solvers.AbstractNLPSolver options::Strategies.StrategyOptions end -struct CovCallableSolver <: Solvers.AbstractNLPSolver - options::Strategies.StrategyOptions -end - -# Implement callable for a non-NLPModel argument (covers generic solve overload) -function (s::CovCallableSolver)(nlp; display::Bool=true) - return (status=:ok, display=display) -end - function test_coverage_solvers() Test.@testset "Coverage: Solvers" verbose=VERBOSE showtiming=SHOWTIMING begin @@ -37,13 +28,15 @@ function test_coverage_solvers() # UNIT TESTS - AbstractNLPSolver callable (abstract_solver.jl) # ==================================================================== - Test.@testset "AbstractNLPSolver callable - NotImplemented" begin + Test.@testset "AbstractNLPSolver solve - NotImplemented" begin opts = Strategies.StrategyOptions() solver = CovUnimplementedSolver(opts) nlp = ADNLPModels.ADNLPModel(x -> sum(x .^ 2), [1.0]) - Test.@test_throws Exceptions.NotImplemented solver(nlp) - Test.@test_throws Exceptions.NotImplemented solver(nlp; display=false) + Test.@test_throws Exceptions.NotImplemented CommonSolve.solve(nlp, solver) + Test.@test_throws Exceptions.NotImplemented CommonSolve.solve( + nlp, solver; display=false + ) end # Note: Knitro tests removed as Knitro is not currently tested @@ -66,26 +59,6 @@ function test_coverage_solvers() Test.@test Strategies.id(Solvers.MadNCL) === :madncl Test.@test Strategies.id(Solvers.Uno) === :uno end - - # ==================================================================== - # UNIT TESTS - CommonSolve.solve(nlp, solver) generic overload - # (common_solve_api.jl:112-117) - # ==================================================================== - - Test.@testset "CommonSolve.solve(nlp, solver) generic" begin - opts = Strategies.StrategyOptions() - solver = CovCallableSolver(opts) - - # Use a plain NamedTuple as "nlp" to hit the generic overload - # (not AbstractNLPModel) - fake_nlp = (name="fake",) - result = CommonSolve.solve(fake_nlp, solver; display=false) - Test.@test result.status === :ok - Test.@test result.display === false - - result2 = CommonSolve.solve(fake_nlp, solver; display=true) - Test.@test result2.display === true - end end end