diff --git a/.gitignore b/.gitignore index 3b5ad793..b6ba4baa 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ tmp/ .devin/ .vscode/ .extras/ -.coverage/ \ No newline at end of file +.coverage/ +.claude/ \ No newline at end of file diff --git a/test/problems/problems_definition.jl b/test/problems/problems_definition.jl index 0cdfb055..904da9ec 100644 --- a/test/problems/problems_definition.jl +++ b/test/problems/problems_definition.jl @@ -6,25 +6,30 @@ import CTSolvers.Optimization import CTSolvers.Modelers +import CTBase.Strategies struct OptimizationProblem{A,E} <: CTSolvers.AbstractOptimizationProblem build_adnlp_model::A build_exa_model::E end -# Build the ADNLP model from the wrapped builder. +# Build the ADNLP model from the wrapped builder, forwarding all modeler options. function Optimization.build_model( - prob::OptimizationProblem, initial_guess, ::Modelers.ADNLP + prob::OptimizationProblem, initial_guess, modeler::Modelers.ADNLP ) - nlp = prob.build_adnlp_model(initial_guess) + options = Strategies.options_dict(modeler) + nlp = prob.build_adnlp_model(initial_guess; options...) return Optimization.BuiltModel(prob, nlp, Optimization.NoCache()) end -# Build the Exa model from the wrapped builder, using the modeler base type. +# Build the Exa model from the wrapped builder, using the modeler base type and +# forwarding all remaining options (e.g. backend). function Optimization.build_model( prob::OptimizationProblem, initial_guess, modeler::Modelers.Exa ) - nlp = prob.build_exa_model(modeler[:base_type], initial_guess) + options = Strategies.options_dict(modeler) + base_type = pop!(options, :base_type) + nlp = prob.build_exa_model(base_type, initial_guess; options...) return Optimization.BuiltModel(prob, nlp, Optimization.NoCache()) end diff --git a/test/suite/extensions/test_madnlp_extension.jl b/test/suite/extensions/test_madnlp_extension.jl index b9bff34c..dac0524a 100644 --- a/test/suite/extensions/test_madnlp_extension.jl +++ b/test/suite/extensions/test_madnlp_extension.jl @@ -286,8 +286,8 @@ function test_madnlp_extension() Test.@testset "GPU Tests" begin if is_cuda_on() && MadNLPGPU.CUDSSSolver isa Type - gpu_modeler = Modelers.Exa(backend=CUDA.CUDABackend()) - gpu_solver = Solvers.MadNLP( + gpu_modeler = Modelers.Exa{Strategies.GPU}() + gpu_solver = Solvers.MadNLP{Strategies.GPU}( max_iter=1000, tol=1e-6, print_level=MadNLP.ERROR, diff --git a/test/suite/optimization/test_problem_definition.jl b/test/suite/optimization/test_problem_definition.jl new file mode 100644 index 00000000..c7043523 --- /dev/null +++ b/test/suite/optimization/test_problem_definition.jl @@ -0,0 +1,95 @@ +module TestProblemDefinition + +using Test: Test +import CTSolvers.Optimization +import CTSolvers.Modelers +import CTBase.Strategies +using CUDA: CUDA + +include(joinpath(@__DIR__, "..", "..", "problems", "TestProblems.jl")) +import .TestProblems + +const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true + +""" + test_problem_definition() + +๐Ÿงช Contract test for `test/problems/problems_definition.jl`'s `Optimization.build_model` +methods: every modeler option (in particular `:backend`) must reach the wrapped +`build_adnlp_model`/`build_exa_model` closure, not just `:base_type`. + +Uses fake builder closures that record their kwargs instead of building a real NLP +model, so the forwarding contract is checked on CPU-only CI โ€” no functional CUDA +required โ€” unlike the end-to-end GPU solves in `test_madnlp_extension.jl` / +`test_madncl_extension.jl`, which are skipped unless real GPU hardware is present. +This is the regression test for the bug where `Modelers.Exa{GPU}`'s `:backend` +option was silently dropped, so GPU-strategy tests kept building CPU-resident +models without any test noticing on CPU-only CI. +""" +function test_problem_definition() + Test.@testset "Problem definition โ€” modeler option forwarding" verbose=VERBOSE showtiming=SHOWTIMING begin + + # ==================================================================== + # CONTRACT TESTS - Exa modeler + # ==================================================================== + + Test.@testset "Exa modeler forwards backend (GPU)" begin + received = Ref{Any}(nothing) + build_exa = (base_type, initial_guess; kwargs...) -> begin + received[] = (base_type, NamedTuple(kwargs)) + return initial_guess + end + prob = TestProblems.OptimizationProblem(nothing, build_exa) + + modeler = Modelers.Exa{Strategies.GPU}() + Optimization.build_model(prob, [1.0], modeler) + + base_type, kwargs = received[] + Test.@test base_type == Float64 + Test.@test haskey(kwargs, :backend) + Test.@test kwargs[:backend] === modeler[:backend] + Test.@test modeler[:backend] isa CUDA.CUDABackend + end + + Test.@testset "Exa modeler forwards backend (CPU)" begin + received = Ref{Any}(nothing) + build_exa = (base_type, initial_guess; kwargs...) -> begin + received[] = (base_type, NamedTuple(kwargs)) + return initial_guess + end + prob = TestProblems.OptimizationProblem(nothing, build_exa) + + modeler = Modelers.Exa{Strategies.CPU}() + Optimization.build_model(prob, [1.0], modeler) + + base_type, kwargs = received[] + Test.@test base_type == Float64 + Test.@test kwargs[:backend] === nothing + end + + # ==================================================================== + # CONTRACT TESTS - ADNLP modeler + # ==================================================================== + + Test.@testset "ADNLP modeler forwards its options" begin + received = Ref{Any}(nothing) + build_adnlp = (initial_guess; kwargs...) -> begin + received[] = NamedTuple(kwargs) + return initial_guess + end + prob = TestProblems.OptimizationProblem(build_adnlp, nothing) + + modeler = Modelers.ADNLP() + Optimization.build_model(prob, [1.0], modeler) + + kwargs = received[] + Test.@test haskey(kwargs, :backend) + Test.@test kwargs[:backend] === modeler[:backend] + end + end +end + +end # module + +test_problem_definition() = TestProblemDefinition.test_problem_definition()