From a9266783a0a67d1575d5fb8c334f97881393a445 Mon Sep 17 00:00:00 2001 From: mtanneau Date: Sat, 10 Jan 2026 15:07:39 -0500 Subject: [PATCH 01/10] Support QP interface via MOI Signed-off-by: mtanneau --- Project.toml | 2 + src/MOI_wrapper.jl | 143 +++++++++++++++++++++++++++++++++++--------- test/MOI_wrapper.jl | 4 ++ 3 files changed, 122 insertions(+), 27 deletions(-) diff --git a/Project.toml b/Project.toml index 7d69f7f..ff9d8c4 100644 --- a/Project.toml +++ b/Project.toml @@ -23,10 +23,12 @@ version = "0.1.2" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] MathOptInterface = "1.34" PrecompileTools = "1" +SparseArrays = "1" Test = "1.6" julia = "1.6" diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index bf37839..29b07c0 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -18,6 +18,8 @@ # The HiGHS wrapper is released under an MIT license, a copy of which can be # found in `/thirdparty/THIRD_PARTY_LICENSES` or at https://opensource.org/licenses/MIT. +using SparseArrays: sparse + import MathOptInterface as MOI const CleverDicts = MOI.Utilities.CleverDicts @@ -621,8 +623,13 @@ end function MOI.supports( ::Optimizer, - ::Union{MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}}, -) + ::MOI.ObjectiveFunction{F}, +) where { + F<:Union{ + MOI.ScalarAffineFunction{Float64}, + MOI.ScalarQuadraticFunction{Float64}, + }, +} return true end @@ -651,6 +658,7 @@ function _check_input_data(dest::Optimizer, src::MOI.ModelLike) if attr in ( MOI.Name(), MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), MOI.ObjectiveSense(), ) continue @@ -903,17 +911,66 @@ function _get_objective_data( objective_sense = sense == MOI.MIN_SENSE ? CUOPT_MINIMIZE : CUOPT_MAXIMIZE - objective_coefficients = zeros(Float64, numcol) F = MOI.get(src, MOI.ObjectiveFunctionType()) f_obj = MOI.get(src, MOI.ObjectiveFunction{F}()) - for term in f_obj.terms - objective_coefficients[mapping[term.variable].value] += term.coefficient - end + objective_coefficients_linear = zeros(Float64, numcol) + objective_offset = 0.0 + + if F == MOI.ScalarAffineFunction{Float64} + for term in f_obj.terms + objective_coefficients_linear[mapping[term.variable].value] += term.coefficient + end + + objective_offset = f_obj.constant - objective_offset = f_obj.constant + # CSR of empty matrix + qobj_matrix_values = Float64[] + qobj_row_offsets = Int32[0] + qobj_col_indices = Int32[] - return objective_sense, objective_offset, objective_coefficients + elseif F == MOI.ScalarQuadraticFunction{Float64} + # Grab linear objective + for term in f_obj.affine_terms + objective_coefficients_linear[mapping[term.variable].value] += term.coefficient + end + # Grab quadratic objective + # cuOpt requires a CSR representation of Q... + # so we build a CSC representation of Qᵀ + Qtrows = Int32[] + Qtcols = Int32[] + Qtvals = Float64[] + sizehint!(Qtrows, length(f_obj.quadratic_terms)) + sizehint!(Qtcols, length(f_obj.quadratic_terms)) + sizehint!(Qtvals, length(f_obj.quadratic_terms)) + for qterm in f_obj.quadratic_terms + i = mapping[qterm.variable_1].value + j = mapping[qterm.variable_2].value + v = qterm.coefficient + if i == j + # Adjust diagonal coefficients to match cuOpt convention + v /= 2 + end + + # We are building a COO of Qᵀ --> swap i and j + push!(Qtrows, j) + push!(Qtcols, i) + push!(Qtvals, v) + end + # Retrieve CSR representation of Q, and revert to 0-based indexing + Qt = sparse(Qtrows, Qtcols, Qtvals, numcol, numcol) + qobj_matrix_values = Qt.nzval + # ⚠️ make sure row & column indices are Int32-valued + qobj_row_offsets = Qt.colptr .- Int32(1) + qobj_col_indices = Qt.rowval .- Int32(1) + + # Objective constant + objective_offset = f_obj.constant + else + throw(MOI.UnsupportedAttribute(MOI.ObjectiveFunction{F})) + end + + return objective_sense, objective_offset, objective_coefficients_linear, qobj_row_offsets, qobj_col_indices, qobj_matrix_values end function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) @@ -970,27 +1027,59 @@ function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) has_integrality = true end - objective_sense, objective_offset, objective_coefficients = + objective_sense, objective_offset, objective_coefficients, qobj_row_offsets, qobj_col_indices, qobj_matrix_values = _get_objective_data(dest, src, mapping, numcol) - ref_problem = Ref{cuOptOptimizationProblem}() - ret = cuOptCreateRangedProblem( - numrow, - numcol, - objective_sense, - objective_offset, - objective_coefficients, - constraint_matrix_row_offsets, - constraint_matrix_column_indices, - constraint_matrix_coefficients, - rowlower, - rowupper, - collower, - colupper, - var_type, - ref_problem, - ) - _check_ret(ret, "cuOptCreateRangedProblem") + # Is this a QP or an LP? + has_quadratic_objective = length(qobj_matrix_values) > 0 + if has_quadratic_objective && has_integrality + error("cuOpt does not support models with quadratic objectives _and_ integer variables") + end + + if has_quadratic_objective + # We have a QP + ref_problem = Ref{cuOptOptimizationProblem}() + ret = cuOptCreateQuadraticRangedProblem( + numrow, + numcol, + objective_sense, + objective_offset, + objective_coefficients, + qobj_row_offsets, + qobj_col_indices, + qobj_matrix_values, + constraint_matrix_row_offsets, + constraint_matrix_column_indices, + constraint_matrix_coefficients, + rowlower, + rowupper, + collower, + colupper, + ref_problem, + ) + _check_ret(ret, "cuOptCreateQuadraticRangedProblem") + else + # we have an LP + ref_problem = Ref{cuOptOptimizationProblem}() + ret = cuOptCreateRangedProblem( + numrow, + numcol, + objective_sense, + objective_offset, + objective_coefficients, + constraint_matrix_row_offsets, + constraint_matrix_column_indices, + constraint_matrix_coefficients, + rowlower, + rowupper, + collower, + colupper, + var_type, + ref_problem, + ) + _check_ret(ret, "cuOptCreateRangedProblem") + end + dest.cuopt_problem = ref_problem[] ref_settings = Ref{cuOptSolverSettings}() diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index df48a73..e61847c 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -60,6 +60,10 @@ function test_runtests_cache_optimizer() "test_constraint_ZeroOne_bounds_3", # Upstream bug: https://github.com/NVIDIA/cuopt/issues/112 "test_solve_TerminationStatus_DUAL_INFEASIBLE", + # Upstream bug: https://github.com/NVIDIA/cuopt/issues/759 + # (cuOpt crashes when given a QP with no linear constraints) + "test_objective_qp_ObjectiveFunction_zero_ofdiag", + "test_objective_qp_ObjectiveFunction_edge_cases", ], ) return From bbcb4f43add846fd5e588c65557d87bbff153f46 Mon Sep 17 00:00:00 2001 From: mtanneau Date: Sat, 10 Jan 2026 17:13:16 -0500 Subject: [PATCH 02/10] Move objective data extraction to separate method Signed-off-by: mtanneau --- src/MOI_wrapper.jl | 98 +++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 29b07c0..6dad411 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -914,63 +914,61 @@ function _get_objective_data( F = MOI.get(src, MOI.ObjectiveFunctionType()) f_obj = MOI.get(src, MOI.ObjectiveFunction{F}()) + objective_offset, objective_coefficients_linear, qobj_row_offsets, qobj_col_indices, qobj_matrix_values = _get_objective_data(f_obj, mapping, numcol) + + return objective_sense, objective_offset, objective_coefficients_linear, qobj_row_offsets, qobj_col_indices, qobj_matrix_values +end + +function _get_objective_data(f::MOI.ScalarAffineFunction, mapping, numcol::Int32) + objective_offset = f.constant + objective_coefficients_linear = zeros(Float64, numcol) - objective_offset = 0.0 + for term in f.terms + i = mapping[term.variable].value + objective_coefficients_linear[i] += term.coefficient + end - if F == MOI.ScalarAffineFunction{Float64} - for term in f_obj.terms - objective_coefficients_linear[mapping[term.variable].value] += term.coefficient - end + return objective_offset, objective_coefficients_linear, Int32[0], Int32[], Float64[] +end - objective_offset = f_obj.constant +function _get_objective_data(f::MOI.ScalarQuadraticFunction, mapping, numcol::Int32) + objective_offset = f.constant - # CSR of empty matrix - qobj_matrix_values = Float64[] - qobj_row_offsets = Int32[0] - qobj_col_indices = Int32[] + objective_coefficients_linear = zeros(Float64, numcol) + for term in f.affine_terms + i = mapping[term.variable].value + objective_coefficients_linear[i] += term.coefficient + end - elseif F == MOI.ScalarQuadraticFunction{Float64} - # Grab linear objective - for term in f_obj.affine_terms - objective_coefficients_linear[mapping[term.variable].value] += term.coefficient - end - # Grab quadratic objective - # cuOpt requires a CSR representation of Q... - # so we build a CSC representation of Qᵀ - Qtrows = Int32[] - Qtcols = Int32[] - Qtvals = Float64[] - sizehint!(Qtrows, length(f_obj.quadratic_terms)) - sizehint!(Qtcols, length(f_obj.quadratic_terms)) - sizehint!(Qtvals, length(f_obj.quadratic_terms)) - for qterm in f_obj.quadratic_terms - i = mapping[qterm.variable_1].value - j = mapping[qterm.variable_2].value - v = qterm.coefficient - if i == j - # Adjust diagonal coefficients to match cuOpt convention - v /= 2 - end - - # We are building a COO of Qᵀ --> swap i and j - push!(Qtrows, j) - push!(Qtcols, i) - push!(Qtvals, v) + # Extract quadratic objective + Qtrows = Int32[] + Qtcols = Int32[] + Qtvals = Float64[] + sizehint!(Qtrows, length(f_obj.quadratic_terms)) + sizehint!(Qtcols, length(f_obj.quadratic_terms)) + sizehint!(Qtvals, length(f_obj.quadratic_terms)) + for qterm in f_obj.quadratic_terms + i = mapping[qterm.variable_1].value + j = mapping[qterm.variable_2].value + v = qterm.coefficient + if i == j + # Adjust diagonal coefficients to match cuOpt convention + v /= 2 end - # Retrieve CSR representation of Q, and revert to 0-based indexing - Qt = sparse(Qtrows, Qtcols, Qtvals, numcol, numcol) - qobj_matrix_values = Qt.nzval - # ⚠️ make sure row & column indices are Int32-valued - qobj_row_offsets = Qt.colptr .- Int32(1) - qobj_col_indices = Qt.rowval .- Int32(1) - - # Objective constant - objective_offset = f_obj.constant - else - throw(MOI.UnsupportedAttribute(MOI.ObjectiveFunction{F})) + + # We are building a COO of Qᵀ --> swap i and j + push!(Qtrows, j) + push!(Qtcols, i) + push!(Qtvals, v) end - - return objective_sense, objective_offset, objective_coefficients_linear, qobj_row_offsets, qobj_col_indices, qobj_matrix_values + # Retrieve CSR representation of Q, and revert to 0-based indexing + Qt = sparse(Qtrows, Qtcols, Qtvals, numcol, numcol) + qobj_matrix_values = Qt.nzval + # ⚠️ ensure row & column indices are Int32-valued + qobj_row_offsets = Qt.colptr .- Int32(1) + qobj_col_indices = Qt.rowval .- Int32(1) + + return objective_offset, objective_coefficients_linear, qobj_row_offsets, qobj_col_indices, qobj_matrix_values end function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) From ace38fd1500c11686163d9b8b7cdb3da3c88283c Mon Sep 17 00:00:00 2001 From: mtanneau Date: Sat, 10 Jan 2026 17:15:00 -0500 Subject: [PATCH 03/10] Format Signed-off-by: mtanneau --- src/MOI_wrapper.jl | 51 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 6dad411..9e8d989 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -914,12 +914,25 @@ function _get_objective_data( F = MOI.get(src, MOI.ObjectiveFunctionType()) f_obj = MOI.get(src, MOI.ObjectiveFunction{F}()) - objective_offset, objective_coefficients_linear, qobj_row_offsets, qobj_col_indices, qobj_matrix_values = _get_objective_data(f_obj, mapping, numcol) + objective_offset, + objective_coefficients_linear, + qobj_row_offsets, + qobj_col_indices, + qobj_matrix_values = _get_objective_data(f_obj, mapping, numcol) - return objective_sense, objective_offset, objective_coefficients_linear, qobj_row_offsets, qobj_col_indices, qobj_matrix_values + return objective_sense, + objective_offset, + objective_coefficients_linear, + qobj_row_offsets, + qobj_col_indices, + qobj_matrix_values end -function _get_objective_data(f::MOI.ScalarAffineFunction, mapping, numcol::Int32) +function _get_objective_data( + f::MOI.ScalarAffineFunction, + mapping, + numcol::Int32, +) objective_offset = f.constant objective_coefficients_linear = zeros(Float64, numcol) @@ -928,10 +941,18 @@ function _get_objective_data(f::MOI.ScalarAffineFunction, mapping, numcol::Int32 objective_coefficients_linear[i] += term.coefficient end - return objective_offset, objective_coefficients_linear, Int32[0], Int32[], Float64[] + return objective_offset, + objective_coefficients_linear, + Int32[0], + Int32[], + Float64[] end -function _get_objective_data(f::MOI.ScalarQuadraticFunction, mapping, numcol::Int32) +function _get_objective_data( + f::MOI.ScalarQuadraticFunction, + mapping, + numcol::Int32, +) objective_offset = f.constant objective_coefficients_linear = zeros(Float64, numcol) @@ -955,7 +976,7 @@ function _get_objective_data(f::MOI.ScalarQuadraticFunction, mapping, numcol::In # Adjust diagonal coefficients to match cuOpt convention v /= 2 end - + # We are building a COO of Qᵀ --> swap i and j push!(Qtrows, j) push!(Qtcols, i) @@ -968,7 +989,11 @@ function _get_objective_data(f::MOI.ScalarQuadraticFunction, mapping, numcol::In qobj_row_offsets = Qt.colptr .- Int32(1) qobj_col_indices = Qt.rowval .- Int32(1) - return objective_offset, objective_coefficients_linear, qobj_row_offsets, qobj_col_indices, qobj_matrix_values + return objective_offset, + objective_coefficients_linear, + qobj_row_offsets, + qobj_col_indices, + qobj_matrix_values end function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) @@ -1025,13 +1050,19 @@ function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) has_integrality = true end - objective_sense, objective_offset, objective_coefficients, qobj_row_offsets, qobj_col_indices, qobj_matrix_values = - _get_objective_data(dest, src, mapping, numcol) + objective_sense, + objective_offset, + objective_coefficients, + qobj_row_offsets, + qobj_col_indices, + qobj_matrix_values = _get_objective_data(dest, src, mapping, numcol) # Is this a QP or an LP? has_quadratic_objective = length(qobj_matrix_values) > 0 if has_quadratic_objective && has_integrality - error("cuOpt does not support models with quadratic objectives _and_ integer variables") + error( + "cuOpt does not support models with quadratic objectives _and_ integer variables", + ) end if has_quadratic_objective From a7ff4a23308adc5dfb32c00fd8f561a6266f0ba1 Mon Sep 17 00:00:00 2001 From: mtanneau Date: Sat, 10 Jan 2026 17:24:46 -0500 Subject: [PATCH 04/10] Fix typos Signed-off-by: mtanneau --- src/MOI_wrapper.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 9e8d989..c6a2353 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -929,14 +929,14 @@ function _get_objective_data( end function _get_objective_data( - f::MOI.ScalarAffineFunction, + f_obj::MOI.ScalarAffineFunction, mapping, numcol::Int32, ) - objective_offset = f.constant + objective_offset = f_obj.constant objective_coefficients_linear = zeros(Float64, numcol) - for term in f.terms + for term in f_obj.terms i = mapping[term.variable].value objective_coefficients_linear[i] += term.coefficient end @@ -949,14 +949,14 @@ function _get_objective_data( end function _get_objective_data( - f::MOI.ScalarQuadraticFunction, + f_obj::MOI.ScalarQuadraticFunction, mapping, numcol::Int32, ) - objective_offset = f.constant + objective_offset = f_obj.constant objective_coefficients_linear = zeros(Float64, numcol) - for term in f.affine_terms + for term in f_obj.affine_terms i = mapping[term.variable].value objective_coefficients_linear[i] += term.coefficient end From 7c6a48a59ed0457aef97cbe72754a52fae2c2275 Mon Sep 17 00:00:00 2001 From: mtanneau Date: Sat, 10 Jan 2026 22:47:56 -0500 Subject: [PATCH 05/10] In-place offset + remove comments Signed-off-by: mtanneau --- src/MOI_wrapper.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index c6a2353..f986752 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -982,12 +982,14 @@ function _get_objective_data( push!(Qtcols, i) push!(Qtvals, v) end - # Retrieve CSR representation of Q, and revert to 0-based indexing + # CSC of Qᵀ is CSR of Q Qt = sparse(Qtrows, Qtcols, Qtvals, numcol, numcol) qobj_matrix_values = Qt.nzval - # ⚠️ ensure row & column indices are Int32-valued - qobj_row_offsets = Qt.colptr .- Int32(1) - qobj_col_indices = Qt.rowval .- Int32(1) + qobj_row_offsets = Qt.colptr + qobj_col_indices = Qt.rowval + # Revert to 0-based indexing + qobj_row_offsets .-= Int32(1) + qobj_col_indices .-= Int32(1) return objective_offset, objective_coefficients_linear, @@ -1066,7 +1068,6 @@ function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) end if has_quadratic_objective - # We have a QP ref_problem = Ref{cuOptOptimizationProblem}() ret = cuOptCreateQuadraticRangedProblem( numrow, @@ -1088,7 +1089,6 @@ function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) ) _check_ret(ret, "cuOptCreateQuadraticRangedProblem") else - # we have an LP ref_problem = Ref{cuOptOptimizationProblem}() ret = cuOptCreateRangedProblem( numrow, From 067d973ad9cdf25c1b1acb2e953e2a6218d2166b Mon Sep 17 00:00:00 2001 From: mtanneau Date: Sun, 11 Jan 2026 18:43:48 -0500 Subject: [PATCH 06/10] Move import Signed-off-by: mtanneau --- src/MOI_wrapper.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index f986752..8834cbe 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -18,7 +18,7 @@ # The HiGHS wrapper is released under an MIT license, a copy of which can be # found in `/thirdparty/THIRD_PARTY_LICENSES` or at https://opensource.org/licenses/MIT. -using SparseArrays: sparse +import SparseArrays import MathOptInterface as MOI const CleverDicts = MOI.Utilities.CleverDicts @@ -983,7 +983,7 @@ function _get_objective_data( push!(Qtvals, v) end # CSC of Qᵀ is CSR of Q - Qt = sparse(Qtrows, Qtcols, Qtvals, numcol, numcol) + Qt = SparseArrays.sparse(Qtrows, Qtcols, Qtvals, numcol, numcol) qobj_matrix_values = Qt.nzval qobj_row_offsets = Qt.colptr qobj_col_indices = Qt.rowval From 184198089207d0732519f86f045e1efb4345b505 Mon Sep 17 00:00:00 2001 From: mtanneau Date: Mon, 12 Jan 2026 16:28:01 -0500 Subject: [PATCH 07/10] Bump cuopt compat version to 25.12 Signed-off-by: mtanneau --- src/cuOpt.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cuOpt.jl b/src/cuOpt.jl index 6a3d96b..bb260d2 100644 --- a/src/cuOpt.jl +++ b/src/cuOpt.jl @@ -46,7 +46,7 @@ function __init__() error("Failed to get cuOpt library version (status code: $status)") end version = VersionNumber(major[], minor[], patch[]) - min, max = v"25.08", v"25.13" + min, max = v"25.12", v"25.13" if !(min <= version < max) error( "Incompatible cuOpt library version. Got $version, but supported versions are [$min, $max)", From c10815118939f4a8f3fd2a7eaad71c6ff2d656b5 Mon Sep 17 00:00:00 2001 From: mtanneau Date: Mon, 12 Jan 2026 16:43:12 -0500 Subject: [PATCH 08/10] Explain MOI vs cuOpt convention re:quadratic functions Signed-off-by: mtanneau --- src/MOI_wrapper.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 8834cbe..d84bb24 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -972,8 +972,12 @@ function _get_objective_data( i = mapping[qterm.variable_1].value j = mapping[qterm.variable_2].value v = qterm.coefficient + # MOI stores quadratic functions as `¹/₂ xᵀQx + aᵀx + b`, with `Q` symmetric... + # (https://jump.dev/MathOptInterface.jl/stable/reference/standard_form/#MathOptInterface.ScalarQuadraticFunction) + # ... whereas cuOpt expects a QP objective of the form `¹xᵀQx + aᵀx + b`, + # where `Q` need not be symmetric + # --> we need to scale diagonal coeffs. by ¹/₂ to match cuOpt convention if i == j - # Adjust diagonal coefficients to match cuOpt convention v /= 2 end From b27e7d0967ed91d6d69567a749b4d532f676efe0 Mon Sep 17 00:00:00 2001 From: mtanneau Date: Tue, 13 Jan 2026 11:52:44 -0500 Subject: [PATCH 09/10] Add QP unit tests Signed-off-by: mtanneau --- test/MOI_wrapper.jl | 82 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index e61847c..44255e1 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -31,7 +31,7 @@ function runtests() return end -function test_runtests() +function _test_runtests() model = cuOpt.Optimizer() MOI.Test.runtests( model, @@ -41,7 +41,7 @@ function test_runtests() return end -function test_runtests_cache_optimizer() +function _test_runtests_cache_optimizer() model = MOI.instantiate(cuOpt.Optimizer; with_cache_type = Float64) MOI.Test.runtests( model, @@ -69,7 +69,7 @@ function test_runtests_cache_optimizer() return end -function test_air05() +function _test_air05() src = MOI.FileFormats.MPS.Model() MOI.read_from_file(src, joinpath(@__DIR__, "datasets", "air05.mps")) model = cuOpt.Optimizer() @@ -82,6 +82,82 @@ function test_air05() return end +function test_qp_objective() + model = MOI.instantiate(cuOpt.Optimizer; with_cache_type = Float64) + MOI.set(model, MOI.Silent(), true) + x, _ = MOI.add_constrained_variable(model, (MOI.GreaterThan(0.0), MOI.LessThan(1.0))) + y, _ = MOI.add_constrained_variable(model, (MOI.GreaterThan(0.0), MOI.LessThan(1.0))) + + # x + y == 1 + MOI.add_constraint(model, + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x), MOI.ScalarAffineTerm(1.0, y)], 0.0), + MOI.EqualTo(1.0) + ) + + F = MOI.ScalarQuadraticFunction{Float64} + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + + # 1. Homogeneous QP with diagonal objective + # Min ¹/₂ * (x² + y²) s.t. x+y == 1 + fobj = MOI.ScalarQuadraticFunction( + [ + MOI.ScalarQuadraticTerm(1.0, x, x), + MOI.ScalarQuadraticTerm(1.0, y, y), + ], + MOI.ScalarAffineTerm{Float64}[], + 0.0, + ) + MOI.set(model, MOI.ObjectiveFunction{F}(), fobj) + MOI.optimize!(model) + + @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), 0.5; atol=1e-4, rtol = 1e-4) + @test isapprox(MOI.get(model, MOI.VariablePrimal(), y), 0.5; atol=1e-4, rtol = 1e-4) + # ¹/₂ * (2 * 0.5² + 2*0.5²) == 0.5 + @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 0.25; atol=1e-4, rtol = 1e-4) + + # Change diagonal coefficients + # Min ¹/₂ * (2x² + 2y²) s.t. x+y == 1 + fobj = MOI.ScalarQuadraticFunction( + [ + MOI.ScalarQuadraticTerm(2.0, x, x), + MOI.ScalarQuadraticTerm(2.0, y, y), + ], + MOI.ScalarAffineTerm{Float64}[], + 0.0, + ) + MOI.set(model, MOI.ObjectiveFunction{F}(), fobj) + MOI.optimize!(model) + # Same solution, but different objective value + # ¹/₂ * (2 * 0.5² + 2*0.5²) == 0.5 + @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 0.5; atol=1e-4, rtol = 1e-4) + @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), 0.5; atol=1e-4, rtol = 1e-4) + @test isapprox(MOI.get(model, MOI.VariablePrimal(), y), 0.5; atol=1e-4, rtol = 1e-4) + + # QP with off-diagonal term + # (x - y - 2)² = x² - 2xy + y² - 4x + 4y + 4 + # = ¹/₂ (2x² - xy - yx + 2y²) + (-4x + 4y) + 4 + # Solution is (x, y) = (1, 0) + fobj = MOI.ScalarQuadraticFunction( + [ + MOI.ScalarQuadraticTerm(2.0, x, x), + MOI.ScalarQuadraticTerm(2.0, y, y), + MOI.ScalarQuadraticTerm(-1.0, x, y), + ], + [ + MOI.ScalarAffineTerm(-4.0, x), + MOI.ScalarAffineTerm(+4.0, y), + ], + 4.0, + ) + MOI.set(model, MOI.ObjectiveFunction{F}(), fobj) + MOI.optimize!(model) + @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 1; atol=1e-4, rtol = 1e-4) + @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), 1.0; atol=1e-4, rtol = 1e-4) + @test isapprox(MOI.get(model, MOI.VariablePrimal(), y), 0.0; atol=1e-4, rtol = 1e-4) + + return nothing +end + end # TestMOIWrapper TestMOIWrapper.runtests() From 2ee820daa6e4bd64cbb3912bc13a19a50f227f5f Mon Sep 17 00:00:00 2001 From: mtanneau Date: Thu, 15 Jan 2026 11:11:01 -0500 Subject: [PATCH 10/10] Fix typos + format Signed-off-by: mtanneau --- test/MOI_wrapper.jl | 94 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 21 deletions(-) diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 44255e1..4136402 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -31,7 +31,7 @@ function runtests() return end -function _test_runtests() +function test_runtests() model = cuOpt.Optimizer() MOI.Test.runtests( model, @@ -41,7 +41,7 @@ function _test_runtests() return end -function _test_runtests_cache_optimizer() +function test_runtests_cache_optimizer() model = MOI.instantiate(cuOpt.Optimizer; with_cache_type = Float64) MOI.Test.runtests( model, @@ -69,7 +69,7 @@ function _test_runtests_cache_optimizer() return end -function _test_air05() +function test_air05() src = MOI.FileFormats.MPS.Model() MOI.read_from_file(src, joinpath(@__DIR__, "datasets", "air05.mps")) model = cuOpt.Optimizer() @@ -85,13 +85,23 @@ end function test_qp_objective() model = MOI.instantiate(cuOpt.Optimizer; with_cache_type = Float64) MOI.set(model, MOI.Silent(), true) - x, _ = MOI.add_constrained_variable(model, (MOI.GreaterThan(0.0), MOI.LessThan(1.0))) - y, _ = MOI.add_constrained_variable(model, (MOI.GreaterThan(0.0), MOI.LessThan(1.0))) + x, _ = MOI.add_constrained_variable( + model, + (MOI.GreaterThan(0.0), MOI.LessThan(1.0)), + ) + y, _ = MOI.add_constrained_variable( + model, + (MOI.GreaterThan(0.0), MOI.LessThan(1.0)), + ) # x + y == 1 - MOI.add_constraint(model, - MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x), MOI.ScalarAffineTerm(1.0, y)], 0.0), - MOI.EqualTo(1.0) + MOI.add_constraint( + model, + MOI.ScalarAffineFunction( + [MOI.ScalarAffineTerm(1.0, x), MOI.ScalarAffineTerm(1.0, y)], + 0.0, + ), + MOI.EqualTo(1.0), ) F = MOI.ScalarQuadraticFunction{Float64} @@ -110,10 +120,25 @@ function test_qp_objective() MOI.set(model, MOI.ObjectiveFunction{F}(), fobj) MOI.optimize!(model) - @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), 0.5; atol=1e-4, rtol = 1e-4) - @test isapprox(MOI.get(model, MOI.VariablePrimal(), y), 0.5; atol=1e-4, rtol = 1e-4) + @test isapprox( + MOI.get(model, MOI.VariablePrimal(), x), + 0.5; + atol = 1e-4, + rtol = 1e-4, + ) + @test isapprox( + MOI.get(model, MOI.VariablePrimal(), y), + 0.5; + atol = 1e-4, + rtol = 1e-4, + ) # ¹/₂ * (2 * 0.5² + 2*0.5²) == 0.5 - @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 0.25; atol=1e-4, rtol = 1e-4) + @test isapprox( + MOI.get(model, MOI.ObjectiveValue()), + 0.25; + atol = 1e-4, + rtol = 1e-4, + ) # Change diagonal coefficients # Min ¹/₂ * (2x² + 2y²) s.t. x+y == 1 @@ -129,9 +154,24 @@ function test_qp_objective() MOI.optimize!(model) # Same solution, but different objective value # ¹/₂ * (2 * 0.5² + 2*0.5²) == 0.5 - @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 0.5; atol=1e-4, rtol = 1e-4) - @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), 0.5; atol=1e-4, rtol = 1e-4) - @test isapprox(MOI.get(model, MOI.VariablePrimal(), y), 0.5; atol=1e-4, rtol = 1e-4) + @test isapprox( + MOI.get(model, MOI.ObjectiveValue()), + 0.5; + atol = 1e-4, + rtol = 1e-4, + ) + @test isapprox( + MOI.get(model, MOI.VariablePrimal(), x), + 0.5; + atol = 1e-4, + rtol = 1e-4, + ) + @test isapprox( + MOI.get(model, MOI.VariablePrimal(), y), + 0.5; + atol = 1e-4, + rtol = 1e-4, + ) # QP with off-diagonal term # (x - y - 2)² = x² - 2xy + y² - 4x + 4y + 4 @@ -143,17 +183,29 @@ function test_qp_objective() MOI.ScalarQuadraticTerm(2.0, y, y), MOI.ScalarQuadraticTerm(-1.0, x, y), ], - [ - MOI.ScalarAffineTerm(-4.0, x), - MOI.ScalarAffineTerm(+4.0, y), - ], + [MOI.ScalarAffineTerm(-4.0, x), MOI.ScalarAffineTerm(+4.0, y)], 4.0, ) MOI.set(model, MOI.ObjectiveFunction{F}(), fobj) MOI.optimize!(model) - @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 1; atol=1e-4, rtol = 1e-4) - @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), 1.0; atol=1e-4, rtol = 1e-4) - @test isapprox(MOI.get(model, MOI.VariablePrimal(), y), 0.0; atol=1e-4, rtol = 1e-4) + @test isapprox( + MOI.get(model, MOI.ObjectiveValue()), + 1; + atol = 1e-4, + rtol = 1e-4, + ) + @test isapprox( + MOI.get(model, MOI.VariablePrimal(), x), + 1.0; + atol = 1e-4, + rtol = 1e-4, + ) + @test isapprox( + MOI.get(model, MOI.VariablePrimal(), y), + 0.0; + atol = 1e-4, + rtol = 1e-4, + ) return nothing end