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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ tmp/
.devin/
.vscode/
.extras/
.coverage/
.coverage/
.claude/
15 changes: 10 additions & 5 deletions test/problems/problems_definition.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions test/suite/extensions/test_madnlp_extension.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
95 changes: 95 additions & 0 deletions test/suite/optimization/test_problem_definition.jl
Original file line number Diff line number Diff line change
@@ -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()
Loading