From dd41b6ebaf0b94a62dd76635074d3b86a8f6fe30 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 12 Dec 2025 15:05:24 -0500 Subject: [PATCH 1/9] skip support check oracle parser_MOI --- src/utils.jl | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/utils.jl b/src/utils.jl index 84fe4a7..de7be9b 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -84,6 +84,17 @@ mutable struct QuadraticConstraints nnzh::Int end +""" + NonLinearStructure + +Structure containing Jacobian and Hessian structures of nonlinear constraints: +- jac_rows: row indices of the Jacobian in Coordinate format (COO) format +- jac_cols: column indices of the Jacobian in COO format +- nnzj: number of non-zero entries in the Jacobian +- hess_rows: row indices of the Hessian in COO format +- hess_cols: column indices of the Hessian in COO format +- nnzh: number of non-zero entries in the Hessian +""" mutable struct NonLinearStructure jac_rows::Vector{Int} jac_cols::Vector{Int} @@ -417,6 +428,10 @@ function parser_MOI(moimodel, index_map, nvar) contypes = MOI.get(moimodel, MOI.ListOfConstraintTypesPresent()) for (F, S) in contypes + # Ignore VectorNonlinearOracle here, we'll parse it separately + if F == MOI.VectorOfVariables && S <: MOI.VectorNonlinearOracle{Float64} + continue + end (F == VNF) && error( "The function $F is not supported. Please use `.<=`, `.==`, and `.>=` in your constraints to ensure compatibility with ScalarNonlinearFunction.", ) @@ -538,6 +553,12 @@ end parser_NL(nlp_data; hessian) Parse nonlinear constraints of an `nlp_data`. + +Returns: +- nnln: number of nonlinear constraints +- nlcon: NonLinearStructure containing Jacobian and Hessian structures +- nl_lcon: lower bounds of nonlinear constraints +- nl_ucon: upper bounds of nonlinear constraints """ function parser_NL(nlp_data; hessian::Bool = true) nnln = length(nlp_data.constraint_bounds) From 97bec14f3e4c66d5677d4601b60fae9fdf075506 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 12 Dec 2025 15:09:52 -0500 Subject: [PATCH 2/9] add parser oracles --- src/utils.jl | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/utils.jl b/src/utils.jl index de7be9b..77d2d90 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -581,6 +581,47 @@ function parser_NL(nlp_data; hessian::Bool = true) return nnln, nlcon, nl_lcon, nl_ucon end +""" + parser_oracles(moimodel, index_map) + +Parse nonlinear oracles of a `MOI.ModelLike`. +""" +function parser_oracles(moimodel, index_map) + oracles = Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}[] + l_oracle = Float64[] + u_oracle = Float64[] + + # We know this pair exists from ListOfConstraintTypesPresent + for ci in MOI.get( + moimodel, + MOI.ListOfConstraintIndices{MOI.VectorOfVariables, MOI.VectorNonlinearOracle{Float64}}(), + ) + f = MOI.get(moimodel, MOI.ConstraintFunction(), ci) # ::MOI.VectorOfVariables + set = MOI.get(moimodel, MOI.ConstraintSet(), ci) # ::MOI.VectorNonlinearOracle{Float64} + + cache = _VectorNonlinearOracleCache(set) + push!(oracles, (f, cache)) + + # Bounds: MOI.VectorNonlinearOracle stores them internally (l, u) + append!(l_oracle, set.l) + append!(u_oracle, set.u) + end + + # Sizes: number of scalar constraints represented by all oracles + noracle = length(l_oracle) + + # Sparsity: + nnzj_oracle = 0 + nnzh_oracle = 0 + for (_, cache) in oracles + nnzj_oracle += length(cache.set.jacobian_structure) + # there may or may not be Hessian info + nnzh_oracle += length(cache.set.hessian_lagrangian_structure) + end + + return oracles, noracle, l_oracle, u_oracle, nnzj_oracle, nnzh_oracle +end + """ parser_variables(model) From 167b38b70b988aa2678da1475a8f079fa9658fb6 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 12 Dec 2025 15:16:22 -0500 Subject: [PATCH 3/9] count number of elements considering oracles --- src/moi_nlp_model.jl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/moi_nlp_model.jl b/src/moi_nlp_model.jl index 3942558..762a8b0 100644 --- a/src/moi_nlp_model.jl +++ b/src/moi_nlp_model.jl @@ -36,6 +36,7 @@ function nlp_model(moimodel::MOI.ModelLike; hessian::Bool = true, name::String = nlp_data = _nlp_block(moimodel) nnln, nlcon, nl_lcon, nl_ucon = parser_NL(nlp_data, hessian = hessian) + oracles, noracle, oracle_lcon, oracle_ucon, nnzj_oracle, nnzh_oracle = parser_oracles(moimodel, index_map) oracles = Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}[] counters = Counters() λ = zeros(Float64, nnln) # Lagrange multipliers for hess_coord! and hprod! without y @@ -47,12 +48,12 @@ function nlp_model(moimodel::MOI.ModelLike; hessian::Bool = true, name::String = obj = parser_objective_MOI(moimodel, nvar, index_map) end - # Update ncon, lcon, ucon, nnzj and nnzh for the oracles - ncon = nlin + quadcon.nquad + nnln - lcon = vcat(lin_lcon, quad_lcon, nl_lcon) - ucon = vcat(lin_ucon, quad_ucon, nl_ucon) - nnzj = lincon.nnzj + quadcon.nnzj + nlcon.nnzj - nnzh = obj.nnzh + quadcon.nnzh + nlcon.nnzh + # Total counts + ncon = nlin + quadcon.nquad + nnln + noracle + lcon = vcat(lin_lcon, quad_lcon, nl_lcon, oracle_lcon) + ucon = vcat(lin_ucon, quad_ucon, nl_ucon, oracle_ucon) + nnzj = lincon.nnzj + quadcon.nnzj + nlcon.nnzj + nnzj_oracle + nnzh = obj.nnzh + quadcon.nnzh + nlcon.nnzh + nnzh_oracle meta = NLPModelMeta( nvar, @@ -67,7 +68,7 @@ function nlp_model(moimodel::MOI.ModelLike; hessian::Bool = true, name::String = nnzh = nnzh, lin = collect(1:nlin), lin_nnzj = lincon.nnzj, - nln_nnzj = quadcon.nnzj + nlcon.nnzj, + nln_nnzj = quadcon.nnzj + nlcon.nnzj + nnzj_oracle, minimize = MOI.get(moimodel, MOI.ObjectiveSense()) == MOI.MIN_SENSE, islp = (obj.type == "LINEAR") && (nnln == 0) && (quadcon.nquad == 0), name = name, From 6321fe76b3a82c42183d8206c77519718006774b Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 12 Dec 2025 15:42:30 -0500 Subject: [PATCH 4/9] update NLPModels.jac_nln_structure! --- src/moi_nlp_model.jl | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/moi_nlp_model.jl b/src/moi_nlp_model.jl index 762a8b0..1a4102e 100644 --- a/src/moi_nlp_model.jl +++ b/src/moi_nlp_model.jl @@ -205,14 +205,24 @@ function NLPModels.jac_nln_structure!( end if nlp.meta.nnln > nlp.quadcon.nquad offset = nlp.quadcon.nnzj - # for (f, s) in nlp.oracles - # for (i, j) in s.set.jacobian_structure - # ... - # offset += 1 - # end - # end - ind_nnln = (offset + 1):(nlp.quadcon.nnzj + nlp.nlcon.nnzj) - view(rows, ind_nnln) .= nlp.quadcon.nquad .+ nlp.nlcon.jac_rows + # structure of oracle Jacobians + for (k, (f, s)) in enumerate(nlp.oracles) + # Shift row index by quadcon.nquad + # plus previous oracle outputs. + row_offset = nlp.quadcon.nquad + for i in 1:(k - 1) + row_offset += nlp.oracles[i][2].set.output_dimension + end + + for (r, c) in s.set.jacobian_structure + offset += 1 + rows[offset] = row_offset + r + cols[offset] = f.variables[c].value + end + end + # non-oracle nonlinear constraints + ind_nnln = (offset + 1):(offset + nlp.nlcon.nnzj) + view(rows, ind_nnln) .= nlp.quadcon.nquad .+ sum(s.set.output_dimension for (_, s) in nlp.oracles) .+ nlp.nlcon.jac_rows view(cols, ind_nnln) .= nlp.nlcon.jac_cols end return rows, cols From dbd5cf1150be4f6df7c09592090345c2d154cf31 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 12 Dec 2025 15:46:13 -0500 Subject: [PATCH 5/9] update NLPModels.jac_nln_coord! --- src/moi_nlp_model.jl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/moi_nlp_model.jl b/src/moi_nlp_model.jl index 1a4102e..1b53088 100644 --- a/src/moi_nlp_model.jl +++ b/src/moi_nlp_model.jl @@ -301,15 +301,15 @@ function NLPModels.jac_nln_coord!(nlp::MathOptNLPModel, x::AbstractVector, vals: end if nlp.meta.nnln > nlp.quadcon.nquad offset = nlp.quadcon.nnzj - # for (f, s) in nlp.oracles - # for i in 1:s.set.input_dimension - # s.x[i] = x[f.variables[i].value] - # end - # nnz_oracle = length(s.set.jacobian_structure) - # s.set.eval_jacobian(view(values, (offset + 1):(oracle + nnz_oracle)), s.x) - # offset += nnz_oracle - # end - ind_nnln = (offset + 1):(nlp.quadcon.nnzj + nlp.nlcon.nnzj) + for (f, s) in nlp.oracles + for i in 1:s.set.input_dimension + s.x[i] = x[f.variables[i].value] + end + nnz_oracle = length(s.set.jacobian_structure) + s.set.eval_jacobian(view(values, (offset + 1):(offset + nnz_oracle)), s.x) + offset += nnz_oracle + end + ind_nnln = (offset + 1):(offset + nlp.nlcon.nnzj) MOI.eval_constraint_jacobian(nlp.eval, view(vals, ind_nnln), x) end return vals From ba11908e9f5a325d9edc241edd84a033072d216a Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 12 Dec 2025 15:47:17 -0500 Subject: [PATCH 6/9] update NLPModels.jac_coord! --- src/moi_nlp_model.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/moi_nlp_model.jl b/src/moi_nlp_model.jl index 1b53088..0f58dbe 100644 --- a/src/moi_nlp_model.jl +++ b/src/moi_nlp_model.jl @@ -348,14 +348,14 @@ function NLPModels.jac_coord!(nlp::MathOptNLPModel, x::AbstractVector, vals::Abs end if nlp.meta.nnln > nlp.quadcon.nquad offset = nlp.lincon.nnzj + nlp.quadcon.nnzj - # for (f, s) in nlp.oracles - # for i in 1:s.set.input_dimension - # s.x[i] = x[f.variables[i].value] - # end - # nnz_oracle = length(s.set.jacobian_structure) - # s.set.eval_jacobian(view(values, (offset + 1):(oracle + nnz_oracle)), s.x) - # offset += nnz_oracle - # end + for (f, s) in nlp.oracles + for i in 1:s.set.input_dimension + s.x[i] = x[f.variables[i].value] + end + nnz_oracle = length(s.set.jacobian_structure) + s.set.eval_jacobian(view(values, (offset + 1):(offset + nnz_oracle)), s.x) + offset += nnz_oracle + end ind_nnln = (offset + 1):(nlp.meta.nnzj) MOI.eval_constraint_jacobian(nlp.eval, view(vals, ind_nnln), x) end From 4ad64b1b1592b71cdbcea5ac1cf5d887df8ac2f2 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 12 Dec 2025 18:08:37 -0500 Subject: [PATCH 7/9] update NLPModels.hess_structure! --- src/moi_nlp_model.jl | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/moi_nlp_model.jl b/src/moi_nlp_model.jl index 0f58dbe..5bd6d50 100644 --- a/src/moi_nlp_model.jl +++ b/src/moi_nlp_model.jl @@ -527,12 +527,8 @@ function NLPModels.hess_structure!( view(rows, 1:(nlp.obj.nnzh)) .= nlp.obj.hessian.rows view(cols, 1:(nlp.obj.nnzh)) .= nlp.obj.hessian.cols end - if (nlp.obj.type == "NONLINEAR") || (nlp.meta.nnln > nlp.quadcon.nquad) - view(rows, (nlp.obj.nnzh + nlp.quadcon.nnzh + 1):(nlp.meta.nnzh)) .= nlp.nlcon.hess_rows - view(cols, (nlp.obj.nnzh + nlp.quadcon.nnzh + 1):(nlp.meta.nnzh)) .= nlp.nlcon.hess_cols - end + index = nlp.obj.nnzh if nlp.quadcon.nquad > 0 - index = nlp.obj.nnzh for i = 1:(nlp.quadcon.nquad) qcon = nlp.quadcon.constraints[i] view(rows, (index + 1):(index + qcon.nnzh)) .= qcon.A.rows @@ -540,13 +536,19 @@ function NLPModels.hess_structure!( index += qcon.nnzh end end - # if !isempty(nlp.oracles) - # for (f, s) in nlp.oracles - # for (i, j) in s.set.hessian_lagrangian_structure - # ... - # end - # end - # end + if !isempty(nlp.oracles) + for (f, s) in nlp.oracles + for (i_local, j_local) in s.set.hessian_lagrangian_structure + index += 1 + rows[index] = f.variables[i_local].value + cols[index] = f.variables[j_local].value + end + end + end + if (nlp.obj.type == "NONLINEAR") || (nlp.meta.nnln > nlp.quadcon.nquad) + view(rows, (index + 1):(index + nlp.nlcon.nnzh)) .= nlp.nlcon.hess_rows + view(cols, (index + 1):(index + nlp.nlcon.nnzh)) .= nlp.nlcon.hess_cols + end return rows, cols end From b49318583c02f38596e30281309a0bbfe0ca0746 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 15 Dec 2025 15:26:37 -0500 Subject: [PATCH 8/9] rearrange order nnl and oracle --- src/moi_nlp_model.jl | 95 ++++++++++++++++++++++---------------------- src/utils.jl | 33 ++++++++++++--- 2 files changed, 76 insertions(+), 52 deletions(-) diff --git a/src/moi_nlp_model.jl b/src/moi_nlp_model.jl index 5bd6d50..0146051 100644 --- a/src/moi_nlp_model.jl +++ b/src/moi_nlp_model.jl @@ -6,7 +6,7 @@ mutable struct MathOptNLPModel <: AbstractNLPModel{Float64, Vector{Float64}} lincon::LinearConstraints quadcon::QuadraticConstraints nlcon::NonLinearStructure - oracles::Vector{Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}} + oracles_data::OraclesData λ::Vector{Float64} hv::Vector{Float64} obj::Objective @@ -36,8 +36,7 @@ function nlp_model(moimodel::MOI.ModelLike; hessian::Bool = true, name::String = nlp_data = _nlp_block(moimodel) nnln, nlcon, nl_lcon, nl_ucon = parser_NL(nlp_data, hessian = hessian) - oracles, noracle, oracle_lcon, oracle_ucon, nnzj_oracle, nnzh_oracle = parser_oracles(moimodel, index_map) - oracles = Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}[] + oracles_data = parser_oracles(moimodel) counters = Counters() λ = zeros(Float64, nnln) # Lagrange multipliers for hess_coord! and hprod! without y hv = zeros(Float64, nvar) # workspace for ghjvprod! @@ -49,12 +48,11 @@ function nlp_model(moimodel::MOI.ModelLike; hessian::Bool = true, name::String = end # Total counts - ncon = nlin + quadcon.nquad + nnln + noracle - lcon = vcat(lin_lcon, quad_lcon, nl_lcon, oracle_lcon) - ucon = vcat(lin_ucon, quad_ucon, nl_ucon, oracle_ucon) - nnzj = lincon.nnzj + quadcon.nnzj + nlcon.nnzj + nnzj_oracle - nnzh = obj.nnzh + quadcon.nnzh + nlcon.nnzh + nnzh_oracle - + ncon = nlin + quadcon.nquad + nnln + oracles_data.noracle + lcon = vcat(lin_lcon, quad_lcon, nl_lcon, oracles_data.l_oracle) + ucon = vcat(lin_ucon, quad_ucon, nl_ucon, oracles_data.u_oracle) + nnzj = lincon.nnzj + quadcon.nnzj + nlcon.nnzj + oracles_data.nnzj_oracle + nnzh = obj.nnzh + quadcon.nnzh + nlcon.nnzh + oracles_data.nnzh_oracle meta = NLPModelMeta( nvar, x0 = x0, @@ -68,17 +66,17 @@ function nlp_model(moimodel::MOI.ModelLike; hessian::Bool = true, name::String = nnzh = nnzh, lin = collect(1:nlin), lin_nnzj = lincon.nnzj, - nln_nnzj = quadcon.nnzj + nlcon.nnzj + nnzj_oracle, + nln_nnzj = quadcon.nnzj + nlcon.nnzj + oracles_data.nnzj_oracle, minimize = MOI.get(moimodel, MOI.ObjectiveSense()) == MOI.MIN_SENSE, islp = (obj.type == "LINEAR") && (nnln == 0) && (quadcon.nquad == 0), name = name, - jprod_available = isempty(oracles), - jtprod_available = isempty(oracles), - hprod_available = isempty(oracles) && hessian, + jprod_available = isempty(oracles_data.oracles), + jtprod_available = isempty(oracles_data.oracles), + hprod_available = isempty(oracles_data.oracles) && hessian, hess_available = hessian, ) - return MathOptNLPModel(meta, nlp_data.evaluator, lincon, quadcon, nlcon, oracles, λ, hv, obj, counters), + return MathOptNLPModel(meta, nlp_data.evaluator, lincon, quadcon, nlcon, oracles_data, λ, hv, obj, counters), index_map end @@ -129,8 +127,9 @@ function NLPModels.cons_nln!(nlp::MathOptNLPModel, x::AbstractVector, c::Abstrac end end if nlp.meta.nnln > nlp.quadcon.nquad - offset = nlp.quadcon.nquad - for (f, s) in nlp.oracles + offset = nlp.quadcon.nquad + nlp.nlcon.nnln + MOI.eval_constraint(nlp.eval, view(c, (nlp.quadcon.nquad + 1):(offset)), x) + for (f, s) in nlp.oracles_data.oracles for i in 1:s.set.input_dimension s.x[i] = x[f.variables[i].value] end @@ -139,8 +138,6 @@ function NLPModels.cons_nln!(nlp::MathOptNLPModel, x::AbstractVector, c::Abstrac s.set.eval_f(c_oracle, s.x) offset += s.set.output_dimension end - index_nnln = (offset + 1):(nlp.meta.nnln) - MOI.eval_constraint(nlp.eval, view(c, index_nnln), x) end return c end @@ -159,8 +156,9 @@ function NLPModels.cons!(nlp::MathOptNLPModel, x::AbstractVector, c::AbstractVec end end if nlp.meta.nnln > nlp.quadcon.nquad - offset = nlp.meta.nlin + nlp.quadcon.nquad - for (f, s) in nlp.oracles + offset = nlp.meta.nlin + nlp.quadcon.nquad + nlp.nlcon.nnln + MOI.eval_constraint(nlp.eval, view(c, (nlp.meta.nlin + nlp.quadcon.nquad + 1):(offset)), x) + for (f, s) in nlp.oracles_data.oracles for i in 1:s.set.input_dimension s.x[i] = x[f.variables[i].value] end @@ -169,8 +167,6 @@ function NLPModels.cons!(nlp::MathOptNLPModel, x::AbstractVector, c::AbstractVec s.set.eval_f(c_oracle, s.x) offset += s.set.output_dimension end - index_nnln = (offset + 1):(nlp.meta.ncon) - MOI.eval_constraint(nlp.eval, view(c, index_nnln), x) end end return c @@ -205,13 +201,18 @@ function NLPModels.jac_nln_structure!( end if nlp.meta.nnln > nlp.quadcon.nquad offset = nlp.quadcon.nnzj + # non-oracle nonlinear constraints + ind_nnln = (offset + 1):(offset + nlp.nlcon.nnzj) + view(rows, ind_nnln) .= nlp.quadcon.nquad .+ sum(s.set.output_dimension for (_, s) in nlp.oracles_data.oracles) .+ nlp.nlcon.jac_rows + view(cols, ind_nnln) .= nlp.nlcon.jac_cols + offset += nlp.nlcon.nnzj # structure of oracle Jacobians - for (k, (f, s)) in enumerate(nlp.oracles) + for (k, (f, s)) in enumerate(nlp.oracles_data.oracles) # Shift row index by quadcon.nquad # plus previous oracle outputs. row_offset = nlp.quadcon.nquad for i in 1:(k - 1) - row_offset += nlp.oracles[i][2].set.output_dimension + row_offset += nlp.oracles_data.oracles[i][2].set.output_dimension end for (r, c) in s.set.jacobian_structure @@ -220,10 +221,6 @@ function NLPModels.jac_nln_structure!( cols[offset] = f.variables[c].value end end - # non-oracle nonlinear constraints - ind_nnln = (offset + 1):(offset + nlp.nlcon.nnzj) - view(rows, ind_nnln) .= nlp.quadcon.nquad .+ sum(s.set.output_dimension for (_, s) in nlp.oracles) .+ nlp.nlcon.jac_rows - view(cols, ind_nnln) .= nlp.nlcon.jac_cols end return rows, cols end @@ -252,15 +249,16 @@ function NLPModels.jac_structure!( end if nlp.meta.nnln > nlp.quadcon.nquad offset = nlp.lincon.nnzj + nlp.quadcon.nnzj - # for (f, s) in nlp.oracles - # for (i, j) in s.set.jacobian_structure - # ... - # offset += 1 - # end - # end - ind_nnln = (offset + 1):(nlp.meta.nnzj) + ind_nnln = (offset + 1):(offset + nlp.nlcon.nnzj) view(rows, ind_nnln) .= nlp.meta.nlin .+ nlp.quadcon.nquad .+ nlp.nlcon.jac_rows view(cols, ind_nnln) .= nlp.nlcon.jac_cols + for (f, s) in nlp.oracles_data.oracles + for (i, j) in s.set.jacobian_structure + offset += 1 + rows[offset] = nlp.meta.nlin + nlp.quadcon.nquad + s.set.output_dimension + i + cols[offset] = f.variables[j].value + end + end end end return rows, cols @@ -301,7 +299,10 @@ function NLPModels.jac_nln_coord!(nlp::MathOptNLPModel, x::AbstractVector, vals: end if nlp.meta.nnln > nlp.quadcon.nquad offset = nlp.quadcon.nnzj - for (f, s) in nlp.oracles + ind_nnln = (offset + 1):(offset + nlp.nlcon.nnzj) + MOI.eval_constraint_jacobian(nlp.eval, view(vals, ind_nnln), x) + offset += nlp.nlcon.nnzj + for (f, s) in nlp.oracles_data.oracles for i in 1:s.set.input_dimension s.x[i] = x[f.variables[i].value] end @@ -309,8 +310,6 @@ function NLPModels.jac_nln_coord!(nlp::MathOptNLPModel, x::AbstractVector, vals: s.set.eval_jacobian(view(values, (offset + 1):(offset + nnz_oracle)), s.x) offset += nnz_oracle end - ind_nnln = (offset + 1):(offset + nlp.nlcon.nnzj) - MOI.eval_constraint_jacobian(nlp.eval, view(vals, ind_nnln), x) end return vals end @@ -348,7 +347,10 @@ function NLPModels.jac_coord!(nlp::MathOptNLPModel, x::AbstractVector, vals::Abs end if nlp.meta.nnln > nlp.quadcon.nquad offset = nlp.lincon.nnzj + nlp.quadcon.nnzj - for (f, s) in nlp.oracles + ind_nnln = (offset + 1):(offset + nlp.nlcon.nnzj) + MOI.eval_constraint_jacobian(nlp.eval, view(vals, ind_nnln), x) + offset += nlp.nlcon.nnzj + for (f, s) in nlp.oracles_data.oracles for i in 1:s.set.input_dimension s.x[i] = x[f.variables[i].value] end @@ -356,8 +358,6 @@ function NLPModels.jac_coord!(nlp::MathOptNLPModel, x::AbstractVector, vals::Abs s.set.eval_jacobian(view(values, (offset + 1):(offset + nnz_oracle)), s.x) offset += nnz_oracle end - ind_nnln = (offset + 1):(nlp.meta.nnzj) - MOI.eval_constraint_jacobian(nlp.eval, view(vals, ind_nnln), x) end end return vals @@ -536,8 +536,13 @@ function NLPModels.hess_structure!( index += qcon.nnzh end end - if !isempty(nlp.oracles) - for (f, s) in nlp.oracles + if (nlp.obj.type == "NONLINEAR") || (nlp.meta.nnln > nlp.quadcon.nquad) + view(rows, (index + 1):(index + nlp.nlcon.nnzh)) .= nlp.nlcon.hess_rows + view(cols, (index + 1):(index + nlp.nlcon.nnzh)) .= nlp.nlcon.hess_cols + index += nlp.nlcon.nnzh + end + if !isempty(nlp.oracles_data.oracles) + for (f, s) in nlp.oracles_data.oracles for (i_local, j_local) in s.set.hessian_lagrangian_structure index += 1 rows[index] = f.variables[i_local].value @@ -545,10 +550,6 @@ function NLPModels.hess_structure!( end end end - if (nlp.obj.type == "NONLINEAR") || (nlp.meta.nnln > nlp.quadcon.nquad) - view(rows, (index + 1):(index + nlp.nlcon.nnzh)) .= nlp.nlcon.hess_rows - view(cols, (index + 1):(index + nlp.nlcon.nnzh)) .= nlp.nlcon.hess_cols - end return rows, cols end diff --git a/src/utils.jl b/src/utils.jl index 77d2d90..54f29c0 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -88,6 +88,7 @@ end NonLinearStructure Structure containing Jacobian and Hessian structures of nonlinear constraints: +- nnln: number of nonlinear constraints - jac_rows: row indices of the Jacobian in Coordinate format (COO) format - jac_cols: column indices of the Jacobian in COO format - nnzj: number of non-zero entries in the Jacobian @@ -96,6 +97,7 @@ Structure containing Jacobian and Hessian structures of nonlinear constraints: - nnzh: number of non-zero entries in the Hessian """ mutable struct NonLinearStructure + nnln::Int jac_rows::Vector{Int} jac_cols::Vector{Int} nnzj::Int @@ -118,6 +120,26 @@ mutable struct Objective nnzh::Int end +""" + OraclesData + +Structure containing nonlinear oracles data: +- oracles: vector of tuples (MOI.VectorOfVariables, _VectorNonlinearOracleCache) +- noracle: number of scalar constraints represented by all oracles +- l_oracle: lower bounds of oracle constraints +- u_oracle: upper bounds of oracle constraints +- nnzj_oracle: number of non-zero entries in the Jacobian of all oracles +- nnzh_oracle: number of non-zero entries in the Hessian of all oracles +""" +mutable struct OraclesData + oracles::Vector{Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}} + noracle::Int + l_oracle::Vector{Float64} + u_oracle::Vector{Float64} + nnzj_oracle::Int + nnzh_oracle::Int +end + """ replace!(ex, x) @@ -576,17 +598,17 @@ function parser_NL(nlp_data; hessian::Bool = true) hess_rows = hessian ? getindex.(hess, 1) : Int[] hess_cols = hessian ? getindex.(hess, 2) : Int[] nnzh = length(hess) - nlcon = NonLinearStructure(jac_rows, jac_cols, nnzj, hess_rows, hess_cols, nnzh) + nlcon = NonLinearStructure(nnln, jac_rows, jac_cols, nnzj, hess_rows, hess_cols, nnzh) return nnln, nlcon, nl_lcon, nl_ucon end """ - parser_oracles(moimodel, index_map) + parser_oracles(moimodel) Parse nonlinear oracles of a `MOI.ModelLike`. """ -function parser_oracles(moimodel, index_map) +function parser_oracles(moimodel) oracles = Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}[] l_oracle = Float64[] u_oracle = Float64[] @@ -619,7 +641,7 @@ function parser_oracles(moimodel, index_map) nnzh_oracle += length(cache.set.hessian_lagrangian_structure) end - return oracles, noracle, l_oracle, u_oracle, nnzj_oracle, nnzh_oracle + return OraclesData(oracles, noracle, l_oracle, u_oracle, nnzj_oracle, nnzh_oracle) end """ @@ -813,6 +835,7 @@ function parser_nonlinear_expression(cmodel, nvar, F; hessian::Bool = true) # Nonlinear least squares model F_is_array_of_containers = F isa Array{<:AbstractArray} + nnlnequ = 0 if F_is_array_of_containers nnlnequ = sum(sum(isa(Fi, NLE) for Fi in FF) for FF in F) if nnlnequ > 0 @@ -849,7 +872,7 @@ function parser_nonlinear_expression(cmodel, nvar, F; hessian::Bool = true) Fhess_cols = hessian && Feval ≠ nothing ? getindex.(Fhess, 2) : Int[] nl_Fnnzh = length(Fhess) - nlequ = NonLinearStructure(Fjac_rows, Fjac_cols, nl_Fnnzj, Fhess_rows, Fhess_cols, nl_Fnnzh) + nlequ = NonLinearStructure(nnlnequ, Fjac_rows, Fjac_cols, nl_Fnnzj, Fhess_rows, Fhess_cols, nl_Fnnzh) return Feval, nlequ, nnlnequ end From 6908d382bf14c3ab84173a8f46cd9a7fbc7a1188 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 15 Dec 2025 20:07:42 -0500 Subject: [PATCH 9/9] fix bug --- src/moi_nlp_model.jl | 108 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 15 deletions(-) diff --git a/src/moi_nlp_model.jl b/src/moi_nlp_model.jl index 0146051..19eb9b3 100644 --- a/src/moi_nlp_model.jl +++ b/src/moi_nlp_model.jl @@ -203,7 +203,7 @@ function NLPModels.jac_nln_structure!( offset = nlp.quadcon.nnzj # non-oracle nonlinear constraints ind_nnln = (offset + 1):(offset + nlp.nlcon.nnzj) - view(rows, ind_nnln) .= nlp.quadcon.nquad .+ sum(s.set.output_dimension for (_, s) in nlp.oracles_data.oracles) .+ nlp.nlcon.jac_rows + view(rows, ind_nnln) .= nlp.quadcon.nquad .+ nlp.nlcon.jac_rows view(cols, ind_nnln) .= nlp.nlcon.jac_cols offset += nlp.nlcon.nnzj # structure of oracle Jacobians @@ -248,16 +248,21 @@ function NLPModels.jac_structure!( end end if nlp.meta.nnln > nlp.quadcon.nquad + # non-oracle nonlinear constraints offset = nlp.lincon.nnzj + nlp.quadcon.nnzj ind_nnln = (offset + 1):(offset + nlp.nlcon.nnzj) view(rows, ind_nnln) .= nlp.meta.nlin .+ nlp.quadcon.nquad .+ nlp.nlcon.jac_rows view(cols, ind_nnln) .= nlp.nlcon.jac_cols + offset += nlp.nlcon.nnzj + # structure of oracle Jacobians + row_offset = nlp.meta.nlin + nlp.quadcon.nquad + nlp.nlcon.nnln for (f, s) in nlp.oracles_data.oracles for (i, j) in s.set.jacobian_structure offset += 1 - rows[offset] = nlp.meta.nlin + nlp.quadcon.nquad + s.set.output_dimension + i + rows[offset] = row_offset + i cols[offset] = f.variables[j].value end + row_offset += s.set.output_dimension end end end @@ -360,6 +365,7 @@ function NLPModels.jac_coord!(nlp::MathOptNLPModel, x::AbstractVector, vals::Abs end end end + @assert offset == nlp.meta.nnzj return vals end @@ -561,9 +567,16 @@ function NLPModels.hess_coord!( obj_weight::Float64 = 1.0, ) increment!(nlp, :neval_hess) + + # Running index over Hessian nonzeros we've filled so far. + index = 0 + + # Objective Hessian block if nlp.obj.type == "QUADRATIC" view(vals, 1:(nlp.obj.nnzh)) .= obj_weight .* nlp.obj.hessian.vals + index += nlp.obj.nnzh end + # Nonlinear objective Hessian block if (nlp.obj.type == "NONLINEAR") || (nlp.meta.nnln > nlp.quadcon.nquad) λ = view(y, (nlp.meta.nlin + nlp.quadcon.nquad + 1):(nlp.meta.ncon)) MOI.eval_hessian_lagrangian( @@ -574,23 +587,42 @@ function NLPModels.hess_coord!( λ, ) end + # Quadratic constraint Hessian blocks if nlp.quadcon.nquad > 0 - index = nlp.obj.nnzh for i = 1:(nlp.quadcon.nquad) qcon = nlp.quadcon.constraints[i] view(vals, (index + 1):(index + qcon.nnzh)) .= y[nlp.meta.nlin + i] .* qcon.A.vals index += qcon.nnzh end end - # if !isempty(nlp.oracles) - # for (f, s) in nlp.oracles - # for i in 1:s.set.input_dimension - # s.x[i] = x[f.variables[i].value] - # end - # H_nnz = length(s.set.hessian_lagrangian_structure) - # ... - # end - # end + # Oracle Hessian blocks + if !isempty(nlp.oracles_data.oracles) + λ_oracle_all = view( + y, + (nlp.meta.nlin + nlp.quadcon.nquad + nlp.nlcon.nnln + 1): + (nlp.meta.nlin + nlp.quadcon.nquad + nlp.nlcon.nnln + nlp.oracles_data.noracle), + ) + + λ_offset = 0 + for (f, s) in nlp.oracles_data.oracles + # build local x for this oracle + for i in 1:s.set.input_dimension + s.x[i] = x[f.variables[i].value] + end + + nout = s.set.output_dimension + μ = view(λ_oracle_all, (λ_offset + 1):(λ_offset + nout)) + + H_nnz = length(s.set.hessian_lagrangian_structure) + ind = (index + 1):(index + H_nnz) + s.set.eval_hessian_lagrangian(view(vals, ind), s.x, μ) + + index += H_nnz + λ_offset += nout + end + end + + @assert index == nlp.meta.nnzh return vals end @@ -645,9 +677,55 @@ function NLPModels.jth_hess_coord!( ) nlp.λ[j - nlp.meta.nlin - nlp.quadcon.nquad] = 0.0 end - # if !isempty(nlp.oracles) - # ... - # end + # check for oracle constraints + if !isempty(nlp.oracles_data.oracles) + n_nl = nlp.nlcon.nnln + noracle = nlp.oracles_data.noracle + + # check if j is an oracle constraint index + first_oracle = nlp.meta.nlin + nlp.quadcon.nquad + n_nl + 1 + last_oracle = first_oracle + noracle - 1 + + if first_oracle ≤ j ≤ last_oracle + # local oracle constraint index k ∈ 1: noracle + k = j - (nlp.meta.nlin + nlp.quadcon.nquad + n_nl) + + # starting index of this oracle's Hessian block + index = nlp.obj.nnzh + nlp.quadcon.nnzh + nlp.nlcon.nnzh + + # walk through oracles to find which one owns component k + offset_outputs = 0 + for (f, s) in nlp.oracles_data.oracles + nout = s.set.output_dimension + H_nnz = length(s.set.hessian_lagrangian_structure) + + if k > offset_outputs + nout + # skip this oracle's block + index += H_nnz + offset_outputs += nout + continue + end + + # this oracle owns constraint k; local index ℓ + ℓ = k - offset_outputs + + # build local x + for i in 1:s.set.input_dimension + s.x[i] = x[f.variables[i].value] + end + + # local multipliers μ: one-hot at ℓ + μ = zeros(eltype(x), nout) + μ[ℓ] = 1.0 + + # fill this oracle's Hessian block + ind = (index + 1):(index + H_nnz) + s.set.eval_hessian_lagrangian(view(vals, ind), s.x, μ) + + break + end + end + end return vals end