From b9baa2d4de4929f15fdb6d71b83cf5379fdc9ce0 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 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 118 insertions(+), 27 deletions(-) diff --git a/Project.toml b/Project.toml index 27927aa..f7bbd54 100644 --- a/Project.toml +++ b/Project.toml @@ -23,10 +23,12 @@ version = "0.2.0" 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 cf9e49b..9b93068 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 @@ -627,8 +629,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 @@ -657,6 +664,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 @@ -909,17 +917,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) @@ -976,27 +1033,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}() From efec7d552318ef4df327ca98bce7857291afe737 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 9b93068..4046634 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -920,63 +920,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 9e9cc63a736b1781b36d14669a9c9b9f0d4d4402 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 4046634..3f6855c 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -920,12 +920,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) @@ -934,10 +947,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) @@ -961,7 +982,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) @@ -974,7 +995,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) @@ -1031,13 +1056,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 5141b73d8f2504fcc72609ab7c8cb63cbb174a2c 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 3f6855c..b03f642 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -935,14 +935,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 @@ -955,14 +955,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 41822fe465809b4348fdceee81839157c05b7a21 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 b03f642..ad0623b 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -988,12 +988,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, @@ -1072,7 +1074,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, @@ -1094,7 +1095,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 9a66093b74a48670b94cb37fa9ade48b5164b155 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 ad0623b..c076f4c 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 @@ -989,7 +989,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 ef215836bcadd8e229cd257512e7d90548734f2e Mon Sep 17 00:00:00 2001 From: mtanneau Date: Mon, 12 Jan 2026 16:43:12 -0500 Subject: [PATCH 07/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 c076f4c..c7cd767 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -978,8 +978,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 1dfe6996fd750e62eb9d3d4fe8126916dd4b0b43 Mon Sep 17 00:00:00 2001 From: mtanneau Date: Tue, 13 Jan 2026 11:52:44 -0500 Subject: [PATCH 08/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 610ffdf..ac42d7f 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, @@ -60,7 +60,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() @@ -73,6 +73,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 335ce408bf69c6cdcf396982b3e45ac26b4387ec Mon Sep 17 00:00:00 2001 From: mtanneau Date: Thu, 15 Jan 2026 11:11:01 -0500 Subject: [PATCH 09/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 ac42d7f..16b522a 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, @@ -60,7 +60,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() @@ -76,13 +76,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} @@ -101,10 +111,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 @@ -120,9 +145,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 @@ -134,17 +174,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 From be427f7051810d6168ad3c39481db5e8ed4d94e6 Mon Sep 17 00:00:00 2001 From: mtanneau Date: Tue, 21 Apr 2026 11:00:00 -0400 Subject: [PATCH 10/10] Remove QP tests coverd by MOI Signed-off-by: mtanneau --- test/MOI_wrapper.jl | 128 -------------------------------------------- 1 file changed, 128 deletions(-) diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 16b522a..610ffdf 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -73,134 +73,6 @@ 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()