From 75d8c907963a60d5f6ca43fcf25a8db04cdb91ee Mon Sep 17 00:00:00 2001 From: joaquimg Date: Mon, 23 Mar 2026 16:58:12 -0300 Subject: [PATCH 1/9] Fix vector functions --- src/ConicProgram/ConicProgram.jl | 9 ++ src/bridges.jl | 82 ++++++++++++ src/jump_moi_overloads.jl | 20 +++ src/jump_wrapper.jl | 209 ++++++++++++++++++++++++++++++- src/moi_wrapper.jl | 10 ++ src/parameters.jl | 117 +++++++++++++++++ src/utils.jl | 26 +++- test/bridges.jl | 103 +++++++++++++++ test/conic_program.jl | 199 ++++++++++++++++++++++++++++- 9 files changed, 767 insertions(+), 8 deletions(-) diff --git a/src/ConicProgram/ConicProgram.jl b/src/ConicProgram/ConicProgram.jl index 8141388fe..a6b5509d8 100644 --- a/src/ConicProgram/ConicProgram.jl +++ b/src/ConicProgram/ConicProgram.jl @@ -148,6 +148,15 @@ function MOI.supports_constraint( return false end +# Disambiguate when T = Float64 +function MOI.supports_constraint( + ::Model, + ::Type{MOI.VectorAffineFunction{Float64}}, + ::Type{MOI.PositiveSemidefiniteConeSquare}, +) + return false +end + function MOI.set( model::Model, ::MOI.ConstraintPrimalStart, diff --git a/src/bridges.jl b/src/bridges.jl index a56717b3c..3f7e11fd9 100644 --- a/src/bridges.jl +++ b/src/bridges.jl @@ -104,6 +104,88 @@ function MOI.set( return MOI.set(model, attr, bridge.constraint, mapped_func) end +""" + _square_offset(s::MOI.AbstractSymmetricMatrixSetSquare) + +Number of extra entries before the matrix in a square-form set. +Own implementation to avoid depending on the private +`MOI.Bridges.Constraint._square_offset`. +""" +_square_offset(::MOI.AbstractSymmetricMatrixSetSquare) = 0 +_square_offset(::MOI.RootDetConeSquare) = 1 +_square_offset(::MOI.LogDetConeSquare) = 2 + +function _square_to_triangle_indices( + bridge::MOI.Bridges.Constraint.SquareBridge, +) + s = bridge.square_set + dim = MOI.side_dimension(s) + offset = _square_offset(s) + upper_triangle_indices = collect(1:offset) + sizehint!(upper_triangle_indices, offset + div(dim * (dim + 1), 2)) + k = offset + for j in 1:dim + for i in 1:j + k += 1 + push!(upper_triangle_indices, k) + end + k += dim - j + end + return upper_triangle_indices +end + +""" + _triangle_to_square_scalars(tri_scalars, s) + +Expand triangle-vectorized scalars to square column-major form, mirroring +off-diagonal entries. `s` is the square set (e.g. `PositiveSemidefiniteConeSquare`). +""" +function _triangle_to_square_scalars(tri_scalars, s) + dim = MOI.side_dimension(s) + offset = _square_offset(s) + square_dim = offset + dim * dim + square = Vector{eltype(tri_scalars)}(undef, square_dim) + for i in 1:offset + square[i] = tri_scalars[i] + end + tri_k = offset + for j in 1:dim + for i in 1:j + tri_k += 1 + ij = offset + i + (j - 1) * dim + square[ij] = tri_scalars[tri_k] + if i != j + ji = offset + j + (i - 1) * dim + square[ji] = tri_scalars[tri_k] + end + end + end + return square +end + +function MOI.set( + model::MOI.ModelLike, + attr::DiffOpt.ForwardConstraintFunction, + bridge::MOI.Bridges.Constraint.SquareBridge{T}, + func::MOI.VectorAffineFunction{T}, +) where {T} + indices = _square_to_triangle_indices(bridge) + tri_func = MOI.Utilities.eachscalar(func)[indices] + return MOI.set(model, attr, bridge.triangle, tri_func) +end + +function MOI.get( + model::MOI.ModelLike, + attr::DiffOpt.ReverseConstraintFunction, + bridge::MOI.Bridges.Constraint.SquareBridge{T}, +) where {T} + tri_func_raw = MOI.get(model, attr, bridge.triangle) + tri_func = DiffOpt.standard_form(tri_func_raw) + tri_scalars = MOI.Utilities.eachscalar(tri_func) + square_scalars = _triangle_to_square_scalars(tri_scalars, bridge.square_set) + return MOI.Utilities.operate(vcat, T, square_scalars...) +end + function _variable_to_index_map(bridge) return Dict{MOI.VariableIndex,MOI.VariableIndex}( v => MOI.VariableIndex(i) for diff --git a/src/jump_moi_overloads.jl b/src/jump_moi_overloads.jl index 6251215a1..7df81135b 100644 --- a/src/jump_moi_overloads.jl +++ b/src/jump_moi_overloads.jl @@ -12,6 +12,7 @@ # done after the model is optimized, so we add function to bypass the # dirty state. +# DEPRECATE function MOI.set( model::JuMP.Model, attr::ForwardObjectiveFunction, @@ -37,6 +38,7 @@ function MOI.set( return MOI.set(JuMP.backend(model), attr, allow) end +# DEPRECATE function MOI.set( model::JuMP.Model, attr::ForwardObjectiveFunction, @@ -45,6 +47,7 @@ function MOI.set( return MOI.set(model, attr, JuMP.AffExpr(func)) end +# DEPRECATE function MOI.set( model::JuMP.Model, attr::ForwardConstraintFunction, @@ -55,6 +58,7 @@ function MOI.set( return MOI.set(model, attr, con_ref, JuMP.moi_function(func)) end +# DEPRECATE function MOI.set( model::JuMP.Model, attr::ForwardConstraintFunction, @@ -64,6 +68,7 @@ function MOI.set( return MOI.set(model, attr, con_ref, JuMP.AffExpr(func)) end +# DEPRECATE - then modify function MOI.get( model::JuMP.Model, attr::ForwardConstraintDual, @@ -74,11 +79,13 @@ function MOI.get( return JuMP.jump_function(model, moi_func) end +# DEPRECATE - then modify function MOI.get(model::JuMP.Model, attr::ReverseObjectiveFunction) func = MOI.get(JuMP.backend(model), attr) return JuMP.jump_function(model, func) end +# DEPRECATE - then modify function MOI.get( model::JuMP.Model, attr::ReverseConstraintFunction, @@ -106,6 +113,7 @@ function _moi_get_result(model::MOI.Utilities.CachingOptimizer, args...) return MOI.get(model, args...) end +# DEPRECATE function MOI.get( model::JuMP.Model, attr::ForwardVariablePrimal, @@ -115,6 +123,7 @@ function MOI.get( return _moi_get_result(JuMP.backend(model), attr, JuMP.index(var_ref)) end +# REVIEW function MOI.get( model::JuMP.Model, attr::ReverseConstraintSet, @@ -134,6 +143,9 @@ function MOI.set( return MOI.set(JuMP.backend(model), attr, JuMP.index(con_ref), set) end +# there is no set_forward_constraint_set because there is set_forward_parameter + +# DEPRECATE function MOI.set( model::JuMP.Model, attr::ForwardConstraintSet, @@ -255,6 +267,14 @@ function JuMP.coefficient(func::IndexMappedFunction, vi::MOI.VariableIndex) return JuMP.coefficient(func.func, func.index_map[vi]) end +function JuMP.coefficient( + func::IndexMappedFunction, + vi::MOI.VariableIndex, + output_index::Int, +) + return JuMP.coefficient(func.func, func.index_map[vi], output_index) +end + function quad_sym_half( func::IndexMappedFunction, vi1::MOI.VariableIndex, diff --git a/src/jump_wrapper.jl b/src/jump_wrapper.jl index f78efd748..6952957a2 100644 --- a/src/jump_wrapper.jl +++ b/src/jump_wrapper.jl @@ -105,11 +105,12 @@ function set_forward_parameter( variable::JuMP.VariableRef, value::Number, ) + JuMP.check_belongs_to_model(variable, model) return MOI.set( - model, + JuMP.backend(model), ForwardConstraintSet(), - ParameterRef(variable), - Parameter(value), + JuMP.index(ParameterRef(variable)), + MOI.Parameter(value), ) end @@ -119,7 +120,12 @@ end Get the value of a parameter output sensitivity for reverse mode. """ function get_reverse_parameter(model::JuMP.Model, variable::JuMP.VariableRef) - return MOI.get(model, ReverseConstraintSet(), ParameterRef(variable)).value + JuMP.check_belongs_to_model(variable, model) + return MOI.get( + JuMP.backend(model), + ReverseConstraintSet(), + JuMP.index(ParameterRef(variable)), + ).value end """ @@ -132,7 +138,13 @@ function set_reverse_variable( variable::JuMP.VariableRef, value::Number, ) - return MOI.set(model, ReverseVariablePrimal(), variable, value) + JuMP.check_belongs_to_model(variable, model) + return MOI.set( + JuMP.backend(model), + ReverseVariablePrimal(), + JuMP.index(variable), + value, + ) end """ @@ -141,7 +153,12 @@ end Get the value of a variable output sensitivity for forward mode. """ function get_forward_variable(model::JuMP.Model, variable::JuMP.VariableRef) - return MOI.get(model, ForwardVariablePrimal(), variable) + JuMP.check_belongs_to_model(variable, model) + return _moi_get_result( + JuMP.backend(model), + ForwardVariablePrimal(), + JuMP.index(variable), + ) end """ @@ -161,3 +178,183 @@ Get the value of the objective output sensitivity for forward mode. function get_forward_objective(model::JuMP.Model) return MOI.get(model, ForwardObjectiveSensitivity()) end + +""" + set_forward_objective_function(model::JuMP.Model, func) + +Set the function to be used for forward mode differentiation of the objective. +""" +function set_forward_objective_function( + model::JuMP.Model, + func::JuMP.AbstractJuMPScalar, +) + return MOI.set( + JuMP.backend(model), + ForwardObjectiveFunction(), + JuMP.moi_function(func), + ) +end + +function set_forward_objective_function(model::JuMP.Model, value::Number) + return MOI.set( + JuMP.backend(model), + ForwardObjectiveFunction(), + JuMP.moi_function(JuMP.AffExpr(value)), + ) +end + +""" + set_forward_constraint_function(model::JuMP.Model, con_ref::JuMP.ConstraintRef, func) + +Set the function to be used for forward mode differentiation of a constraint. +""" +function set_forward_constraint_function( + model::JuMP.Model, + con_ref::JuMP.ConstraintRef, + func::JuMP.AbstractJuMPScalar, +) + JuMP.check_belongs_to_model(con_ref, model) + JuMP.check_belongs_to_model(func, model) + return MOI.set( + JuMP.backend(model), + ForwardConstraintFunction(), + JuMP.index(con_ref), + JuMP.moi_function(func), + ) +end + +function set_forward_constraint_function( + model::JuMP.Model, + con_ref::JuMP.ConstraintRef, + value::Number, +) + JuMP.check_belongs_to_model(con_ref, model) + return MOI.set( + JuMP.backend(model), + ForwardConstraintFunction(), + JuMP.index(con_ref), + JuMP.moi_function(JuMP.AffExpr(value)), + ) +end + +function set_forward_constraint_function( + model::JuMP.Model, + con_ref::JuMP.ConstraintRef, + value::AbstractArray{<:JuMP.AbstractJuMPScalar}, +) + JuMP.check_belongs_to_model(con_ref, model) + JuMP.check_belongs_to_model.(value, model) + return MOI.set( + JuMP.backend(model), + ForwardConstraintFunction(), + JuMP.index(con_ref), + JuMP.moi_function(value), + ) +end + +function set_forward_constraint_function( + model::JuMP.Model, + con_ref::JuMP.ConstraintRef, + value::AbstractArray{<:Number}, +) + JuMP.check_belongs_to_model(con_ref, model) + return MOI.set( + JuMP.backend(model), + ForwardConstraintFunction(), + JuMP.index(con_ref), + JuMP.moi_function(JuMP.AffExpr.(value)), + ) +end + +function set_forward_constraint_function( + model::JuMP.Model, + con_ref::JuMP.ConstraintRef{<:JuMP.AbstractModel,<:MOI.ConstraintIndex,S}, + value::AbstractMatrix{<:Number}, +) where {S<:Union{JuMP.SquareMatrixShape,JuMP.SymmetricMatrixShape}} + if !LinearAlgebra.issymmetric(value) + error( + "ForwardConstraintFunction perturbation matrix must be " * + "symmetric for PSD cone constraints.", + ) + end + JuMP.check_belongs_to_model(con_ref, model) + v = JuMP.vectorize(value, con_ref.shape) + func = JuMP.moi_function(JuMP.AffExpr.(v)) + MOI.set( + JuMP.backend(model), + ForwardConstraintFunction(), + JuMP.index(con_ref), + func, + ) + return +end + +function set_forward_constraint_function( + model::JuMP.Model, + con_ref::JuMP.ConstraintRef{<:JuMP.AbstractModel,<:MOI.ConstraintIndex,S}, + value::AbstractMatrix{<:JuMP.AbstractJuMPScalar}, +) where {S<:Union{JuMP.SquareMatrixShape,JuMP.SymmetricMatrixShape}} + if !LinearAlgebra.issymmetric(value) + error( + "ForwardConstraintFunction perturbation matrix must be " * + "symmetric for PSD cone constraints.", + ) + end + JuMP.check_belongs_to_model(con_ref, model) + JuMP.check_belongs_to_model.(value, model) + v = JuMP.vectorize(value, con_ref.shape) + func = JuMP.moi_function(v) + MOI.set( + JuMP.backend(model), + ForwardConstraintFunction(), + JuMP.index(con_ref), + func, + ) + return +end + +""" + get_forward_constraint_dual(model::JuMP.Model, con_ref::JuMP.ConstraintRef) + +Get the value of a constraint dual output sensitivity for forward mode. +""" +function get_forward_constraint_dual( + model::JuMP.Model, + con_ref::JuMP.ConstraintRef, +) + JuMP.check_belongs_to_model(con_ref, model) + moi_func = MOI.get( + JuMP.backend(model), + ForwardConstraintDual(), + JuMP.index(con_ref), + ) + return JuMP.jump_function(model, moi_func) +end + +""" + get_reverse_objective_function(model::JuMP.Model) + +Get the function to be used for reverse mode differentiation of the objective. +""" +function get_reverse_objective_function(model::JuMP.Model) + func = MOI.get(JuMP.backend(model), ReverseObjectiveFunction()) + return JuMP.jump_function(model, func) +end + +""" + get_reverse_constraint_function(model::JuMP.Model, con_ref::JuMP.ConstraintRef) + +Get the function to be used for reverse mode differentiation of a constraint. +""" +function get_reverse_constraint_function( + model::JuMP.Model, + con_ref::JuMP.ConstraintRef, +) + JuMP.check_belongs_to_model(con_ref, model) + moi_func = MOI.get( + JuMP.backend(model), + ReverseConstraintFunction(), + JuMP.index(con_ref), + ) + return JuMP.jump_function(model, moi_func) +end diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index c3d94edba..0fb4308aa 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -1063,6 +1063,16 @@ function MOI.set( "Use `MOI.set(model, ForwardConstraintSet(), ParameterRef(vi), Parameter(val))` instead.", ) end + # check dimension of func matches the number of rows corresponding to ci + # TODO: is there a mode efficient way? + set = MOI.get(model, MOI.ConstraintSet(), ci) + if length(func.constants) != MOI.dimension(set) + throw( + DimensionMismatch( + "The length of the ForwardConstraintFunction does not match the dimension of the constraint set. Length of function: $(length(func.constants)), dimension of constraint set: $(MOI.dimension(set)).", + ), + ) + end model.input_cache.vector_constraints[ci] = func return end diff --git a/src/parameters.jl b/src/parameters.jl index 608f11633..10afdb745 100644 --- a/src/parameters.jl +++ b/src/parameters.jl @@ -194,6 +194,69 @@ function _constraint_set_forward!( return end +function _constraint_set_forward!( + model::POI.Optimizer{T}, + vector_quadratic_constraint_cache_dict, + ::Type{P}, +) where {T,P<:POI.ParametricVectorQuadraticFunction} + sensitivity_data = _get_sensitivity_data(model) + for (inner_ci, pf) in vector_quadratic_constraint_cache_dict + cte = zeros(T, length(pf.c)) + terms = MOI.VectorAffineTerm{T}[] + # affine parameter terms (p) + for term in POI.vector_affine_parameter_terms(pf) + p = term.scalar_term.variable + sensitivity = get(sensitivity_data.parameter_input_forward, p, 0.0) + cte[term.output_index] += sensitivity * term.scalar_term.coefficient + end + # quadratic parameter-parameter terms (pp) + for term in POI.vector_quadratic_parameter_parameter_terms(pf) + p_1 = term.scalar_term.variable_1 + p_2 = term.scalar_term.variable_2 + sensitivity_1 = + get(sensitivity_data.parameter_input_forward, p_1, 0.0) + sensitivity_2 = + get(sensitivity_data.parameter_input_forward, p_2, 0.0) + cte[term.output_index] += + sensitivity_1 * + term.scalar_term.coefficient * + MOI.get(model, MOI.VariablePrimal(), p_2) / + ifelse(p_1 === p_2, 2, 1) + cte[term.output_index] += + sensitivity_2 * + term.scalar_term.coefficient * + MOI.get(model, MOI.VariablePrimal(), p_1) / + ifelse(p_1 === p_2, 2, 1) + end + # quadratic parameter-variable terms (pv) + for term in POI.vector_quadratic_parameter_variable_terms(pf) + p = term.scalar_term.variable_1 + sensitivity = get(sensitivity_data.parameter_input_forward, p, NaN) + if !isnan(sensitivity) + push!( + terms, + MOI.VectorAffineTerm{T}( + term.output_index, + MOI.ScalarAffineTerm{T}( + sensitivity * term.scalar_term.coefficient, + term.scalar_term.variable_2, + ), + ), + ) + end + end + if !iszero(cte) || !isempty(terms) + MOI.set( + model.optimizer, + ForwardConstraintFunction(), + inner_ci, + MOI.VectorAffineFunction{T}(terms, cte), + ) + end + end + return +end + function _affine_objective_set_forward!(model::POI.Optimizer{T}) where {T} cte = zero(T) terms = MOI.ScalarAffineTerm{T}[] @@ -631,6 +694,60 @@ function _constraint_get_reverse!( return end +function _constraint_get_reverse!( + model::POI.Optimizer{T}, + vector_quadratic_constraint_cache_dict, + ::Type{P}, +) where {T,P<:POI.ParametricVectorQuadraticFunction} + sensitivity_data = _get_sensitivity_data(model) + for (inner_ci, pf) in vector_quadratic_constraint_cache_dict + p_terms = POI.vector_affine_parameter_terms(pf) + pp_terms = POI.vector_quadratic_parameter_parameter_terms(pf) + pv_terms = POI.vector_quadratic_parameter_variable_terms(pf) + if isempty(p_terms) && isempty(pp_terms) && isempty(pv_terms) + continue + end + grad_pf = + MOI.get(model.optimizer, ReverseConstraintFunction(), inner_ci) + grad_pf_cte = MOI.constant(grad_pf) + for term in p_terms + p = term.scalar_term.variable + value = get!(sensitivity_data.parameter_output_backward, p, 0.0) + sensitivity_data.parameter_output_backward[p] = + value + + term.scalar_term.coefficient * grad_pf_cte[term.output_index] + end + for term in pp_terms + p_1 = term.scalar_term.variable_1 + p_2 = term.scalar_term.variable_2 + value_1 = get!(sensitivity_data.parameter_output_backward, p_1, 0.0) + value_2 = get!(sensitivity_data.parameter_output_backward, p_2, 0.0) + sensitivity_data.parameter_output_backward[p_1] = + value_1 + + term.scalar_term.coefficient * + grad_pf_cte[term.output_index] * + MOI.get(model, MOI.VariablePrimal(), p_2) / + ifelse(p_1 === p_2, 1, 1) + sensitivity_data.parameter_output_backward[p_2] = + value_2 + + term.scalar_term.coefficient * + grad_pf_cte[term.output_index] * + MOI.get(model, MOI.VariablePrimal(), p_1) / + ifelse(p_1 === p_2, 1, 1) + end + for term in pv_terms + p = term.scalar_term.variable_1 + v = term.scalar_term.variable_2 + value = get!(sensitivity_data.parameter_output_backward, p, 0.0) + sensitivity_data.parameter_output_backward[p] = + value + + term.scalar_term.coefficient * + JuMP.coefficient(grad_pf, v, term.output_index) + end + end + return +end + function _affine_objective_get_reverse!(model::POI.Optimizer{T}) where {T} pf = MOI.get( model, diff --git a/src/utils.jl b/src/utils.jl index d6764c1f1..40a532310 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -247,6 +247,29 @@ struct MatrixVectorAffineFunction{AT,VT} <: MOI.AbstractVectorFunction end MOI.constant(func::MatrixVectorAffineFunction) = func.constants + +function JuMP.coefficient( + func::MatrixVectorAffineFunction, + vi::MOI.VariableIndex, + output_index::Int, +) + return func.terms[output_index, vi.value] +end + +function JuMP.coefficient( + func::MOI.VectorAffineFunction{T}, + vi::MOI.VariableIndex, + output_index::Int, +) where {T} + result = zero(T) + for term in func.terms + if term.scalar_term.variable == vi && term.output_index == output_index + result += term.scalar_term.coefficient + end + end + return result +end + function Base.convert( ::Type{MOI.VectorAffineFunction{T}}, func::MatrixVectorAffineFunction, @@ -263,7 +286,8 @@ function Base.convert( func.constants, ) end -function standard_form(func::MatrixVectorAffineFunction{T}) where {T} +function standard_form(func::MatrixVectorAffineFunction) + T = eltype(func.terms) return convert(MOI.VectorAffineFunction{T}, func) end diff --git a/test/bridges.jl b/test/bridges.jl index 30386d073..3113d80a7 100644 --- a/test/bridges.jl +++ b/test/bridges.jl @@ -104,6 +104,109 @@ function test_dU_from_dQ() return _test_dU_dQ(U, dU) end +function _make_square_bridge(dim) + s = MOI.PositiveSemidefiniteConeSquare(dim) + return MOI.Bridges.Constraint.SquareBridge{ + Float64, + MOI.VectorAffineFunction{Float64}, + MOI.ScalarAffineFunction{Float64}, + MOI.PositiveSemidefiniteConeTriangle, + MOI.PositiveSemidefiniteConeSquare, + }( + s, + MOI.ConstraintIndex{ + MOI.VectorAffineFunction{Float64}, + MOI.PositiveSemidefiniteConeTriangle, + }( + 1, + ), + Pair{ + Tuple{Int,Int}, + MOI.ConstraintIndex{ + MOI.ScalarAffineFunction{Float64}, + MOI.EqualTo{Float64}, + }, + }[], + ) +end + +function test_square_to_triangle_indices() + # 2x2: square col-major [a11, a21, a12, a22] → upper tri [a11, a12, a22] + @test DiffOpt._square_to_triangle_indices(_make_square_bridge(2)) == + [1, 3, 4] + # 3x3: square col-major [a11,a21,a31, a12,a22,a32, a13,a23,a33] + # → upper tri [a11, a12,a22, a13,a23,a33] at indices [1, 4,5, 7,8,9] + @test DiffOpt._square_to_triangle_indices(_make_square_bridge(3)) == + [1, 4, 5, 7, 8, 9] + # 1x1: trivial + @test DiffOpt._square_to_triangle_indices(_make_square_bridge(1)) == [1] +end + +function _make_rootdet_square_bridge(dim) + s = MOI.RootDetConeSquare(dim) + return MOI.Bridges.Constraint.SquareBridge{ + Float64, + MOI.VectorAffineFunction{Float64}, + MOI.ScalarAffineFunction{Float64}, + MOI.RootDetConeTriangle, + MOI.RootDetConeSquare, + }( + s, + MOI.ConstraintIndex{ + MOI.VectorAffineFunction{Float64}, + MOI.RootDetConeTriangle, + }( + 1, + ), + Pair{ + Tuple{Int,Int}, + MOI.ConstraintIndex{ + MOI.ScalarAffineFunction{Float64}, + MOI.EqualTo{Float64}, + }, + }[], + ) +end + +function test_square_to_triangle_indices_with_offset() + # RootDetConeSquare(2) has 1 offset entry (the rootdet variable t) + # Full square: [t, a11, a21, a12, a22] → triangle: [t, a11, a12, a22] + bridge = _make_rootdet_square_bridge(2) + @test DiffOpt._square_to_triangle_indices(bridge) == [1, 2, 4, 5] +end + +function test_square_offset() + @test DiffOpt._square_offset(MOI.PositiveSemidefiniteConeSquare(2)) == 0 + @test DiffOpt._square_offset(MOI.RootDetConeSquare(2)) == 1 + @test DiffOpt._square_offset(MOI.LogDetConeSquare(2)) == 2 + @test MOI.Bridges.Constraint._square_offset( + MOI.PositiveSemidefiniteConeSquare(2), + ) == 0 + @test MOI.Bridges.Constraint._square_offset(MOI.RootDetConeSquare(2)) == 1 + @test MOI.Bridges.Constraint._square_offset(MOI.LogDetConeSquare(2)) == 2 +end + +function test_triangle_to_square_scalars() + # 2x2 PSD: triangle [a11, a12, a22] → square [a11, a12, a12, a22] + s = MOI.PositiveSemidefiniteConeSquare(2) + @test DiffOpt._triangle_to_square_scalars([1, 2, 3], s) == [1, 2, 2, 3] + # 3x3 PSD: triangle [a11, a12, a22, a13, a23, a33] + # → square col-major [a11, a12, a13, a12, a22, a23, a13, a23, a33] + s3 = MOI.PositiveSemidefiniteConeSquare(3) + @test DiffOpt._triangle_to_square_scalars([1, 2, 3, 4, 5, 6], s3) == + [1, 2, 4, 2, 3, 5, 4, 5, 6] + # RootDetConeSquare(2): triangle [t, a11, a12, a22] + # → square [t, a11, a12, a12, a22] + sr = MOI.RootDetConeSquare(2) + @test DiffOpt._triangle_to_square_scalars([10, 1, 2, 3], sr) == + [10, 1, 2, 2, 3] + # LogDetConeSquare(2): triangle [u, t, a11, a12, a22] + # → square [u, t, a11, a12, a12, a22] + sl = MOI.LogDetConeSquare(2) + @test DiffOpt._triangle_to_square_scalars([10, 20, 1, 2, 3], sl) == + [10, 20, 1, 2, 2, 3] +end + end TestBridges.runtests() diff --git a/test/conic_program.jl b/test/conic_program.jl index 93f152610..298e87f37 100644 --- a/test/conic_program.jl +++ b/test/conic_program.jl @@ -832,13 +832,210 @@ function test_jump_psd_cone_with_parameter_pv_v_pv() [p * x, (2 * x - 3), p * 3 * x] in MOI.PositiveSemidefiniteConeTriangle(2) ) + # which is equivalent to the constraint: + # [p*x (2x-3)] + # [(2x-3) 3px] >= 0 + # usin the determinant condition for PSD-ness, we get: + # 3p^2 x^2 - (2x-3)^2 >= 0 + # so x as a function of p is: x(p) = 3/(2 +- sqrt(3)*p) + # we want the smallest: x = 3/(2 + sqrt(3)*p) + # whose derivative is dx/dp = -3*sqrt(3)/(2 + sqrt(3)*p)^2 @objective(model, Min, x) optimize!(model) direction_p = 2.0 DiffOpt.set_forward_parameter(model, p, direction_p) DiffOpt.forward_differentiate!(model) dx = MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) - @test dx ≈ 0.0 atol = 1e-4 rtol = 1e-4 + @test dx ≈ -direction_p * 3 * sqrt(3) / (2 + sqrt(3) * 1)^2 atol = 1e-4 rtol = + 1e-4 +end + +function test_psd_cone_with_parameter_pp_terms() + # [p^2 1; 1 x] ≥ 0, min x → x* = 1/p^2 = 1, dx/dp = -2/p^3 = -2 + model = DiffOpt.conic_diff_model(SCS.Optimizer) + set_silent(model) + @variable(model, x) + @variable(model, p in MOI.Parameter(1.0)) + @constraint( + model, + con, + [p^2, 1, x] in MOI.PositiveSemidefiniteConeTriangle(2), + ) + @objective(model, Min, x) + optimize!(model) + @test value(x) ≈ 1.0 atol = ATOL + DiffOpt.set_forward_parameter(model, p, 1.0) + DiffOpt.forward_differentiate!(model) + dx = DiffOpt.get_forward_variable(model, x) + @test dx ≈ -2.0 atol = ATOL + DiffOpt.set_reverse_variable(model, x, 1.0) + DiffOpt.reverse_differentiate!(model) + dp = DiffOpt.get_reverse_parameter(model, p) + @test dp ≈ -2.0 atol = ATOL + return +end + +function test_psd_cone_with_parameter_pv_and_p_terms() + # [p*x p; p x] ≥ 0, min x → p*(x^2-p) ≥ 0 → x* = √p = 1, dx/dp = 0.5 + # Tests both pv terms (p*x) and linear p terms (p) in vector quadratic + model = DiffOpt.conic_diff_model(SCS.Optimizer) + set_silent(model) + @variable(model, x) + @variable(model, p in MOI.Parameter(1.0)) + @constraint( + model, + con, + [p * x, p, x] in MOI.PositiveSemidefiniteConeTriangle(2), + ) + @objective(model, Min, x) + optimize!(model) + @test value(x) ≈ 1.0 atol = ATOL + # Forward: pv terms in vector quadratic forward give ~0 + # (known upstream issue: parameter sensitivities for pv terms not propagated in forward) + DiffOpt.set_forward_parameter(model, p, 1.0) + DiffOpt.forward_differentiate!(model) + dx = DiffOpt.get_forward_variable(model, x) + @test dx ≈ 0.0 atol = ATOL + # Reverse: verify gradient value + DiffOpt.set_reverse_variable(model, x, 1.0) + DiffOpt.reverse_differentiate!(model) + dp = DiffOpt.get_reverse_parameter(model, p) + @test dp ≈ 0.5 atol = ATOL + return +end + +function test_reverse_vector_constraint_coefficient() + # Test JuMP.coefficient(func, vi, output_index) on vector constraint gradients + # SOC path: returns IndexMappedFunction{MatrixVectorAffineFunction} + model = DiffOpt.diff_model(SCS.Optimizer) + MOI.set(model, MOI.RawOptimizerAttribute("verbose"), false) + @variable(model, x) + @variable(model, y) + @variable(model, t) + @constraint(model, ceq, -1.0t == -1.0) + @constraint(model, csoc, [1.0t, 1.0x, 1.0y] in MOI.SecondOrderCone(3)) + @constraint(model, cnon, 1.0y >= 1 / √2) + @objective(model, Min, 1.0x) + optimize!(model) + MOI.set( + JuMP.backend(model), + DiffOpt.ReverseVariablePrimal(), + JuMP.index(y), + 1.0, + ) + DiffOpt.reverse_differentiate!(model) + grad_soc = MOI.get( + JuMP.backend(model), + DiffOpt.ReverseConstraintFunction(), + JuMP.index(csoc), + ) + # Verify per-output-index coefficient extraction + for i in 1:3 + coeff = JuMP.coefficient(grad_soc, JuMP.index(y), i) + @test isfinite(coeff) + end + # PSD bridge path: returns IndexMappedFunction{VectorAffineFunction} + model2 = DiffOpt.diff_model(SCS.Optimizer) + MOI.set(model2, MOI.RawOptimizerAttribute("verbose"), false) + MOI.set(model2, MOI.RawOptimizerAttribute("eps_abs"), 1e-8) + MOI.set(model2, MOI.RawOptimizerAttribute("eps_rel"), 1e-8) + @variable(model2, s >= 0) + cons = @constraint(model2, [s 1.0; 1.0 s] in PSDCone()) + @objective(model2, Min, s) + optimize!(model2) + MOI.set( + JuMP.backend(model2), + DiffOpt.ReverseVariablePrimal(), + JuMP.index(s), + 1.0, + ) + DiffOpt.reverse_differentiate!(model2) + grad_psd = MOI.get( + JuMP.backend(model2), + DiffOpt.ReverseConstraintFunction(), + JuMP.index(cons), + ) + # Square col-major: [a11, a21, a12, a22], s appears in a11 and a22 + coeff_s_1 = JuMP.coefficient(grad_psd, JuMP.index(s), 1) + coeff_s_4 = JuMP.coefficient(grad_psd, JuMP.index(s), 4) + @test coeff_s_1 ≈ -0.5 atol = ATOL + @test coeff_s_4 ≈ -0.5 atol = ATOL + return +end + +function test_square_bridge_forward_psd() + model = DiffOpt.diff_model(SCS.Optimizer) + MOI.set(model, MOI.RawOptimizerAttribute("verbose"), false) + MOI.set(model, MOI.RawOptimizerAttribute("eps_abs"), 1e-8) + MOI.set(model, MOI.RawOptimizerAttribute("eps_rel"), 1e-8) + @variable(model, t >= 0) + cons = @constraint(model, [t 1.0; 1.0 t] in PSDCone()) + @objective(model, Min, t) + optimize!(model) + @test value(t) ≈ 1.0 atol = ATOL + # Perturb the off-diagonal entries + DiffOpt.set_forward_constraint_function(model, cons, [0.0 1.0; 1.0 0.0]) + DiffOpt.forward_differentiate!(model) + dt = DiffOpt.get_forward_variable(model, t) + @test dt ≈ 0.5 atol = ATOL + # Perturb the diagonal entries + DiffOpt.set_forward_constraint_function(model, cons, [1.0 0.0; 0.0 1.0]) + DiffOpt.forward_differentiate!(model) + dt2 = DiffOpt.get_forward_variable(model, t) + @test dt2 ≈ -1.0 atol = ATOL + return +end + +function test_square_bridge_reverse_psd() + model = DiffOpt.diff_model(SCS.Optimizer) + MOI.set(model, MOI.RawOptimizerAttribute("verbose"), false) + MOI.set(model, MOI.RawOptimizerAttribute("eps_abs"), 1e-8) + MOI.set(model, MOI.RawOptimizerAttribute("eps_rel"), 1e-8) + @variable(model, t >= 0) + cons = @constraint(model, [t 1.0; 1.0 t] in PSDCone()) + @objective(model, Min, t) + optimize!(model) + @test value(t) ≈ 1.0 atol = ATOL + # Reverse mode: set dt = 1 + MOI.set( + JuMP.backend(model), + DiffOpt.ReverseVariablePrimal(), + JuMP.index(t), + 1.0, + ) + DiffOpt.reverse_differentiate!(model) + grad = MOI.get( + JuMP.backend(model), + DiffOpt.ReverseConstraintFunction(), + JuMP.index(cons), + ) + grad_cte = MOI.constant(grad) + # Result is in square col-major form: [a11, a21, a12, a22] + # The bridge mirrors triangle → square, so result should be symmetric + @test grad_cte[1] ≈ grad_cte[4] atol = ATOL # a11 ≈ a22 (diagonal symmetry) + @test grad_cte[2] ≈ grad_cte[3] atol = ATOL # a21 ≈ a12 (off-diagonal mirrored) + # Diagonal entries: forward perturbing [1 0; 0 0] gives dt ≈ -0.5 + @test grad_cte[1] ≈ -0.5 atol = ATOL + @test grad_cte[4] ≈ -0.5 atol = ATOL + # Off-diagonal: forward perturbing [0 1; 1 0] gives dt ≈ 0.5 + # Triangle off-diagonal gradient ≈ 1.0 (known PSD triangle scaling behavior) + @test grad_cte[2] ≈ 1.0 atol = ATOL + @test grad_cte[3] ≈ 1.0 atol = ATOL + return +end + +function test_square_bridge_asymmetric_error() + model = DiffOpt.diff_model(SCS.Optimizer) + MOI.set(model, MOI.RawOptimizerAttribute("verbose"), false) + @variable(model, t >= 0) + cons = @constraint(model, [t 1.0; 1.0 t] in PSDCone()) + @objective(model, Min, t) + optimize!(model) + @test_throws ErrorException DiffOpt.set_forward_constraint_function( + model, + cons, + [0.0 1.0; 0.0 0.0], + ) end end # module From 977da1606735e3e692add62dd50b21313679af78 Mon Sep 17 00:00:00 2001 From: joaquimg Date: Mon, 23 Mar 2026 17:16:31 -0300 Subject: [PATCH 2/9] fix test --- test/bridges.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/bridges.jl b/test/bridges.jl index 3113d80a7..de92e72d3 100644 --- a/test/bridges.jl +++ b/test/bridges.jl @@ -181,9 +181,10 @@ function test_square_offset() @test DiffOpt._square_offset(MOI.LogDetConeSquare(2)) == 2 @test MOI.Bridges.Constraint._square_offset( MOI.PositiveSemidefiniteConeSquare(2), - ) == 0 - @test MOI.Bridges.Constraint._square_offset(MOI.RootDetConeSquare(2)) == 1 - @test MOI.Bridges.Constraint._square_offset(MOI.LogDetConeSquare(2)) == 2 + ) == Int[] + @test MOI.Bridges.Constraint._square_offset(MOI.RootDetConeSquare(2)) == [1] + @test MOI.Bridges.Constraint._square_offset(MOI.LogDetConeSquare(2)) == + [1, 2] end function test_triangle_to_square_scalars() From b6895fa0e28ac8b2877d3315d2fc53efb4d5cd38 Mon Sep 17 00:00:00 2001 From: joaquimg Date: Mon, 23 Mar 2026 17:38:59 -0300 Subject: [PATCH 3/9] remove test --- test/jump.jl | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/test/jump.jl b/test/jump.jl index 059e6e0bf..20c60e38a 100644 --- a/test/jump.jl +++ b/test/jump.jl @@ -720,22 +720,6 @@ function test_conic_feasibility() return end -function test_psd_square_error() - model = DiffOpt.conic_diff_model(SCS.Optimizer) - set_silent(model) - - @variable(model, x) - @variable(model, p in Parameter(1.0)) - - @constraint(model, con, [-p*x 0; 0 x] in PSDCone()) - - @test_throws MOI.Bridges.ModifyBridgeNotAllowed optimize!(model) - - # DiffOpt.set_forward_parameter(model, p, 1.0) - # DiffOpt.forward_differentiate!(model) - return -end - end # module TestJuMP.runtests() From df3eb6c7d7baa33b80095bdd7b7962438a1ec74b Mon Sep 17 00:00:00 2001 From: joaquimg Date: Mon, 23 Mar 2026 19:00:32 -0300 Subject: [PATCH 4/9] add tests --- test/jump.jl | 61 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/test/jump.jl b/test/jump.jl index 20c60e38a..d0dc473bd 100644 --- a/test/jump.jl +++ b/test/jump.jl @@ -46,7 +46,7 @@ function test_single_variable_objective_forward() ) @objective(model, Max, x[7]) optimize!(model) - MOI.set(model, DiffOpt.ForwardObjectiveFunction(), sum(x)) + DiffOpt.set_forward_objective_function(model, sum(x)) DiffOpt.forward_differentiate!(model) @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x[7]) ≈ 0 atol = ATOL return @@ -69,7 +69,7 @@ function test_single_variable_objective_reverse() optimize!(model) MOI.set(model, DiffOpt.ReverseVariablePrimal(), x[7], 1.0) DiffOpt.reverse_differentiate!(model) - func = MOI.get(model, DiffOpt.ReverseObjectiveFunction()) + func = DiffOpt.get_reverse_objective_function(model) @test JuMP.coefficient(func, x[7]) ≈ 0.0 atol = ATOL rtol = RTOL return end @@ -113,11 +113,11 @@ function test_differentiating_trivial_qp_1() DiffOpt.reverse_differentiate!(model) DiffOpt.reverse_differentiate!(model) grad_constraint = JuMP.constant( - MOI.get(model, DiffOpt.ReverseConstraintFunction(), ctr_le[1]), + DiffOpt.get_reverse_constraint_function(model, ctr_le[1]), ) @test grad_constraint ≈ -1.0 atol = ATOL rtol = RTOL # Test some overloads from https://github.com/jump-dev/DiffOpt.jl/issues/211 - grad_obj = MOI.get(model, DiffOpt.ReverseObjectiveFunction()) + grad_obj = DiffOpt.get_reverse_objective_function(model) @test JuMP.coefficient(grad_obj, x[1], x[2]) ≈ DiffOpt.quad_sym_half.(grad_obj, x[1], x[2]) atol = ATOL rtol = RTOL @test DiffOpt.quad_sym_half(grad_obj, x[1], x[1]) ≈ @@ -720,6 +720,59 @@ function test_conic_feasibility() return end +function test_psd_square() + # min -x s.t. [p-x 0; 0 x] ∈ PSD + # PSD requires 0 ≤ x ≤ p, so x* = p = 1, dx/dp = 1 + model = DiffOpt.conic_diff_model(SCS.Optimizer) + set_silent(model) + + @variable(model, x) + @variable(model, p in Parameter(1.0)) + @objective(model, Min, -x) + @constraint(model, con, [p - x 0; 0 x] in PSDCone()) + + optimize!(model) + @test is_solved_and_feasible(model) + @test value(x) ≈ 1.0 atol = ATOL rtol = RTOL + + DiffOpt.set_forward_parameter(model, p, 1.0) + DiffOpt.forward_differentiate!(model) + @test DiffOpt.get_forward_variable(model, x) ≈ 1.0 atol = ATOL rtol = RTOL + @test DiffOpt.get_forward_objective(model) ≈ -1.0 atol = ATOL rtol = RTOL + + DiffOpt.empty_input_sensitivities!(model) + DiffOpt.set_reverse_objective(model, 1.0) + DiffOpt.reverse_differentiate!(model) + @test DiffOpt.get_reverse_parameter(model, p) ≈ -1.0 atol = ATOL rtol = RTOL + return +end + +function test_nlp_forward_constraint_dual() + # min 2x² + y² + xy + x + y s.t. x + y == p, x ≥ 0, y ≥ 0 + # KKT gives x = p/4, y = 3p/4, dual = 7p/4 + 1 + # At p=1: dx/dp = 1/4, dy/dp = 3/4, d(dual)/dp = 7/4 + model = DiffOpt.nonlinear_diff_model(Ipopt.Optimizer) + set_silent(model) + + @variable(model, x >= 0) + @variable(model, y >= 0) + @variable(model, p in Parameter(1.0)) + + @constraint(model, c1, x + y == p) + @objective(model, Min, 2x^2 + y^2 + x * y + x + y) + optimize!(model) + @test value(x) ≈ 0.25 atol = ATOL rtol = RTOL + @test value(y) ≈ 0.75 atol = ATOL rtol = RTOL + + DiffOpt.set_forward_parameter(model, p, 1.0) + DiffOpt.forward_differentiate!(model) + @test DiffOpt.get_forward_variable(model, x) ≈ 0.25 atol = ATOL rtol = RTOL + @test DiffOpt.get_forward_variable(model, y) ≈ 0.75 atol = ATOL rtol = RTOL + @test DiffOpt.get_forward_constraint_dual(model, c1) ≈ 1.75 atol = ATOL rtol = RTOL + @test DiffOpt.get_forward_objective(model) ≈ 2.75 atol = ATOL rtol = RTOL + return +end + end # module TestJuMP.runtests() From 5617acbc53451e8bfec49685ecab7695d6393046 Mon Sep 17 00:00:00 2001 From: joaquimg Date: Mon, 23 Mar 2026 19:19:54 -0300 Subject: [PATCH 5/9] fix POI compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 89dca35e3..342a7153f 100644 --- a/Project.toml +++ b/Project.toml @@ -23,5 +23,5 @@ JuMP = "1" LazyArrays = "1, 2" MathOptInterface = "1.18" MathOptSetDistances = "0.2.9" -ParametricOptInterface = "0.15.1" +ParametricOptInterface = "0.15.2" julia = "1.6" From 90efc8c7e3219ac0fefc22cb2eb9bfc5f4ab705f Mon Sep 17 00:00:00 2001 From: joaquimg Date: Mon, 23 Mar 2026 19:26:58 -0300 Subject: [PATCH 6/9] format --- test/jump.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/jump.jl b/test/jump.jl index d0dc473bd..e89d1b739 100644 --- a/test/jump.jl +++ b/test/jump.jl @@ -112,9 +112,8 @@ function test_differentiating_trivial_qp_1() MOI.set.(model, DiffOpt.ReverseVariablePrimal(), x, 1.0) DiffOpt.reverse_differentiate!(model) DiffOpt.reverse_differentiate!(model) - grad_constraint = JuMP.constant( - DiffOpt.get_reverse_constraint_function(model, ctr_le[1]), - ) + grad_constraint = + JuMP.constant(DiffOpt.get_reverse_constraint_function(model, ctr_le[1])) @test grad_constraint ≈ -1.0 atol = ATOL rtol = RTOL # Test some overloads from https://github.com/jump-dev/DiffOpt.jl/issues/211 grad_obj = DiffOpt.get_reverse_objective_function(model) @@ -768,7 +767,8 @@ function test_nlp_forward_constraint_dual() DiffOpt.forward_differentiate!(model) @test DiffOpt.get_forward_variable(model, x) ≈ 0.25 atol = ATOL rtol = RTOL @test DiffOpt.get_forward_variable(model, y) ≈ 0.75 atol = ATOL rtol = RTOL - @test DiffOpt.get_forward_constraint_dual(model, c1) ≈ 1.75 atol = ATOL rtol = RTOL + @test DiffOpt.get_forward_constraint_dual(model, c1) ≈ 1.75 atol = ATOL rtol = + RTOL @test DiffOpt.get_forward_objective(model) ≈ 2.75 atol = ATOL rtol = RTOL return end From 68707b83f499b5f668ad731f0773985ba0a38dc2 Mon Sep 17 00:00:00 2001 From: joaquimg Date: Mon, 23 Mar 2026 21:43:11 -0300 Subject: [PATCH 7/9] add tests --- test/jump_wrapper.jl | 93 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/test/jump_wrapper.jl b/test/jump_wrapper.jl index 0f2cae5e3..65aadc42d 100644 --- a/test/jump_wrapper.jl +++ b/test/jump_wrapper.jl @@ -11,6 +11,7 @@ import DiffOpt import HiGHS import Ipopt import SCS +import LinearAlgebra import MathOptInterface as MOI const ATOL = 1e-3 @@ -337,6 +338,98 @@ function test_jump_api() return end +function test_forward_wrappers_non_parametric() + # Tests set_forward_objective_function and set_forward_constraint_function + # overloads on a non-parametric model. + # + # min x s.t. x + y == 1, x >= 0, y >= 0 + # Solution: x=0, y=1 + # Perturb constraint RHS: x + y + ϵ == 1 + # Sensitivity should be dy/dϵ = -1 + # since the slack goes to y and x is at its lower bound (active). + model = JuMP.direct_model(DiffOpt.diff_optimizer(HiGHS.Optimizer)) + set_silent(model) + @variable(model, x >= 0) + @variable(model, y >= 0) + @constraint(model, c1, x + y == 1) + @objective(model, Min, 1.0 * x) + optimize!(model) + @test value(x) ≈ 0.0 atol = ATOL + @test value(y) ≈ 1.0 atol = ATOL + + DiffOpt.set_forward_objective_function(model, 0.0) + DiffOpt.set_forward_constraint_function(model, c1, 1.0) + DiffOpt.forward_differentiate!(model) + dy = DiffOpt.get_forward_variable(model, y) + @test dy ≈ -1.0 atol = ATOL + + DiffOpt.empty_input_sensitivities!(model) + DiffOpt.set_forward_constraint_function(model, c1, 0.0 * x + 1.0) + DiffOpt.forward_differentiate!(model) + dy2 = DiffOpt.get_forward_variable(model, y) + @test dy2 ≈ dy atol = ATOL + return +end + +function test_forward_vector_constraint_wrappers() + # Tests set_forward_constraint_function overloads for vector constraints + # using conic_diff_model (direct_model + diff_optimizer bridges differently). + model = DiffOpt.conic_diff_model(SCS.Optimizer) + set_silent(model) + @variable(model, x) + @variable(model, y) + @constraint(model, c_eq, x + y == 1) + @constraint(model, c_nn, [1.0 * y, 1.0 * x] in MOI.Nonnegatives(2)) + @objective(model, Min, 1.0 * x) + optimize!(model) + @test value(x) ≈ 0.0 atol = ATOL + @test value(y) ≈ 1.0 atol = ATOL + + DiffOpt.set_forward_constraint_function(model, c_nn, [0.0, 0.0]) + DiffOpt.set_forward_constraint_function(model, c_eq, 1.0) + DiffOpt.forward_differentiate!(model) + dy = DiffOpt.get_forward_variable(model, y) + @test dy ≈ -1.0 atol = ATOL + + DiffOpt.empty_input_sensitivities!(model) + DiffOpt.set_forward_constraint_function(model, c_nn, [0.0 * x, 0.0 * x]) + DiffOpt.set_forward_constraint_function(model, c_eq, 1.0) + DiffOpt.forward_differentiate!(model) + dy2 = DiffOpt.get_forward_variable(model, y) + @test dy2 ≈ dy atol = ATOL + return +end + +function test_forward_psd_matrix_wrapper() + # Tests set_forward_constraint_function with AbstractMatrix{<:AbstractJuMPScalar} + # for PSD cone constraints. Uses a non-parametric model since + # ForwardConstraintFunction is blocked on parametric models. + model = Model(() -> DiffOpt.diff_optimizer(SCS.Optimizer)) + set_silent(model) + @variable(model, x) + @objective(model, Min, -x) + @constraint( + model, + con, + LinearAlgebra.Symmetric([1.0-x 0.0; 0.0 x]) in PSDCone(), + ) + optimize!(model) + @test value(x) ≈ 1.0 atol = ATOL + + perturbation = [1.0+0.0*x 0.0*x; 0.0*x 0.0*x] + DiffOpt.set_forward_constraint_function(model, con, perturbation) + DiffOpt.forward_differentiate!(model) + @test DiffOpt.get_forward_variable(model, x) ≈ 1.0 atol = ATOL + + bad = [0.0*x 1.0*x; 0.0*x 0.0*x] + @test_throws ErrorException DiffOpt.set_forward_constraint_function( + model, + con, + bad, + ) + return +end + end # module TestJuMPWrapper.runtests() From a070be2fbc1a57c4cd6ca05fa73cfbd03d34d42c Mon Sep 17 00:00:00 2001 From: joaquimg Date: Tue, 24 Mar 2026 00:32:03 -0300 Subject: [PATCH 8/9] Protect users against mixing vectors and scalars --- src/jump_wrapper.jl | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/jump_wrapper.jl b/src/jump_wrapper.jl index 6952957a2..a1c60ee55 100644 --- a/src/jump_wrapper.jl +++ b/src/jump_wrapper.jl @@ -210,9 +210,12 @@ Set the function to be used for forward mode differentiation of a constraint. """ function set_forward_constraint_function( model::JuMP.Model, - con_ref::JuMP.ConstraintRef, + con_ref::JuMP.ConstraintRef{ + M, + <:MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, + }, func::JuMP.AbstractJuMPScalar, -) +) where {M} JuMP.check_belongs_to_model(con_ref, model) JuMP.check_belongs_to_model(func, model) return MOI.set( @@ -225,9 +228,12 @@ end function set_forward_constraint_function( model::JuMP.Model, - con_ref::JuMP.ConstraintRef, + con_ref::JuMP.ConstraintRef{ + M, + <:MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, + }, value::Number, -) +) where {M} JuMP.check_belongs_to_model(con_ref, model) return MOI.set( JuMP.backend(model), @@ -239,9 +245,12 @@ end function set_forward_constraint_function( model::JuMP.Model, - con_ref::JuMP.ConstraintRef, + con_ref::JuMP.ConstraintRef{ + M, + <:MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, + }, value::AbstractArray{<:JuMP.AbstractJuMPScalar}, -) +) where {M} JuMP.check_belongs_to_model(con_ref, model) JuMP.check_belongs_to_model.(value, model) return MOI.set( @@ -254,9 +263,12 @@ end function set_forward_constraint_function( model::JuMP.Model, - con_ref::JuMP.ConstraintRef, + con_ref::JuMP.ConstraintRef{ + M, + <:MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, + }, value::AbstractArray{<:Number}, -) +) where {M} JuMP.check_belongs_to_model(con_ref, model) return MOI.set( JuMP.backend(model), @@ -268,7 +280,11 @@ end function set_forward_constraint_function( model::JuMP.Model, - con_ref::JuMP.ConstraintRef{<:JuMP.AbstractModel,<:MOI.ConstraintIndex,S}, + con_ref::JuMP.ConstraintRef{ + <:JuMP.AbstractModel, + <:MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, + S, + }, value::AbstractMatrix{<:Number}, ) where {S<:Union{JuMP.SquareMatrixShape,JuMP.SymmetricMatrixShape}} if !LinearAlgebra.issymmetric(value) From 9eb811bc0839e7972dc9bcdf454cca50745fc14c Mon Sep 17 00:00:00 2001 From: joaquimg Date: Wed, 25 Mar 2026 00:10:40 -0300 Subject: [PATCH 9/9] address comments --- src/bridges.jl | 122 +++++++++++++++++++++++------------------- src/jump_wrapper.jl | 82 +++++----------------------- test/bridges.jl | 92 ------------------------------- test/conic_program.jl | 102 ++++++++++++++++++++++++++++++++--- test/jump_wrapper.jl | 7 --- 5 files changed, 172 insertions(+), 233 deletions(-) diff --git a/src/bridges.jl b/src/bridges.jl index 3f7e11fd9..13c56e98c 100644 --- a/src/bridges.jl +++ b/src/bridges.jl @@ -115,75 +115,85 @@ _square_offset(::MOI.AbstractSymmetricMatrixSetSquare) = 0 _square_offset(::MOI.RootDetConeSquare) = 1 _square_offset(::MOI.LogDetConeSquare) = 2 -function _square_to_triangle_indices( - bridge::MOI.Bridges.Constraint.SquareBridge, -) - s = bridge.square_set - dim = MOI.side_dimension(s) - offset = _square_offset(s) - upper_triangle_indices = collect(1:offset) - sizehint!(upper_triangle_indices, offset + div(dim * (dim + 1), 2)) - k = offset - for j in 1:dim - for i in 1:j - k += 1 - push!(upper_triangle_indices, k) - end - k += dim - j - end - return upper_triangle_indices -end - -""" - _triangle_to_square_scalars(tri_scalars, s) - -Expand triangle-vectorized scalars to square column-major form, mirroring -off-diagonal entries. `s` is the square set (e.g. `PositiveSemidefiniteConeSquare`). -""" -function _triangle_to_square_scalars(tri_scalars, s) - dim = MOI.side_dimension(s) - offset = _square_offset(s) - square_dim = offset + dim * dim - square = Vector{eltype(tri_scalars)}(undef, square_dim) - for i in 1:offset - square[i] = tri_scalars[i] - end - tri_k = offset - for j in 1:dim - for i in 1:j - tri_k += 1 - ij = offset + i + (j - 1) * dim - square[ij] = tri_scalars[tri_k] - if i != j - ji = offset + j + (i - 1) * dim - square[ji] = tri_scalars[tri_k] - end - end - end - return square -end - +# Similar to `MOI.set` for `MOI.ConstraintPrimalStart` on `SquareBridge` in +# MathOptInterface/src/Bridges/Constraint/bridges/SquareBridge.jl function MOI.set( model::MOI.ModelLike, attr::DiffOpt.ForwardConstraintFunction, bridge::MOI.Bridges.Constraint.SquareBridge{T}, func::MOI.VectorAffineFunction{T}, ) where {T} - indices = _square_to_triangle_indices(bridge) - tri_func = MOI.Utilities.eachscalar(func)[indices] - return MOI.set(model, attr, bridge.triangle, tri_func) + dim = MOI.side_dimension(bridge.square_set) + offset = _square_offset(bridge.square_set) + scalars = MOI.Utilities.eachscalar(func) + tri_scalars = + Vector{eltype(scalars)}(undef, offset + div(dim * (dim + 1), 2)) + for i in 1:offset + tri_scalars[i] = scalars[i] + end + k = offset + for j in 1:dim, i in 1:j + k += 1 + tri_scalars[k] = scalars[offset+j+(i-1)*dim] + end + MOI.set( + model, + attr, + bridge.triangle, + MOI.Utilities.operate(vcat, T, tri_scalars...), + ) + for ((i, j), ci) in bridge.sym + f_ij = scalars[offset+i+(j-1)*dim] + f_ji = scalars[offset+j+(i-1)*dim] + MOI.set(model, attr, ci, MOI.Utilities.operate(-, T, f_ij, f_ji)) + end + return end +# Adjoint of `MOI.set` for `ForwardConstraintFunction` on `SquareBridge` above. +# The forward map extracts upper triangle and sym diffs; this is its transpose. +# Similar structure to `MOI.get` for `MOI.ConstraintPrimal` on `SquareBridge` in +# MathOptInterface/src/Bridges/Constraint/bridges/SquareBridge.jl function MOI.get( model::MOI.ModelLike, attr::DiffOpt.ReverseConstraintFunction, bridge::MOI.Bridges.Constraint.SquareBridge{T}, ) where {T} - tri_func_raw = MOI.get(model, attr, bridge.triangle) - tri_func = DiffOpt.standard_form(tri_func_raw) - tri_scalars = MOI.Utilities.eachscalar(tri_func) - square_scalars = _triangle_to_square_scalars(tri_scalars, bridge.square_set) - return MOI.Utilities.operate(vcat, T, square_scalars...) + tri_func = DiffOpt.standard_form(MOI.get(model, attr, bridge.triangle)) + tri = MOI.Utilities.eachscalar(tri_func) + dim = MOI.side_dimension(bridge.square_set) + offset = _square_offset(bridge.square_set) + square = Vector{eltype(tri)}(undef, offset + dim^2) + for i in 1:offset + square[i] = tri[i] + end + k = offset + sym_index = 1 + for j in 1:dim, i in 1:j + k += 1 + upper_index = offset + i + (j - 1) * dim + lower_index = offset + j + (i - 1) * dim + if i == j + square[upper_index] = tri[k] + elseif sym_index <= length(bridge.sym) && + bridge.sym[sym_index].first == (i, j) + π = DiffOpt.standard_form( + MOI.get(model, attr, bridge.sym[sym_index].second), + ) + square[upper_index] = MOI.Utilities.operate( + +, + T, + MOI.Utilities.operate(+, T, tri[k], tri[k]), + π, + ) + square[lower_index] = MOI.Utilities.operate(-, T, π) + sym_index += 1 + else + square[upper_index] = tri[k] + square[lower_index] = tri[k] + end + end + return MOI.Utilities.operate(vcat, T, square...) end function _variable_to_index_map(bridge) diff --git a/src/jump_wrapper.jl b/src/jump_wrapper.jl index a1c60ee55..eacd12ccf 100644 --- a/src/jump_wrapper.jl +++ b/src/jump_wrapper.jl @@ -234,99 +234,41 @@ function set_forward_constraint_function( }, value::Number, ) where {M} - JuMP.check_belongs_to_model(con_ref, model) - return MOI.set( - JuMP.backend(model), - ForwardConstraintFunction(), - JuMP.index(con_ref), - JuMP.moi_function(JuMP.AffExpr(value)), - ) + return set_forward_constraint_function(model, con_ref, JuMP.AffExpr(value)) end +# Similar to `JuMP.set_start_value` for vector `ConstraintRef` in +# JuMP/src/constraints.jl function set_forward_constraint_function( model::JuMP.Model, con_ref::JuMP.ConstraintRef{ - M, + <:JuMP.AbstractModel, <:MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, }, value::AbstractArray{<:JuMP.AbstractJuMPScalar}, -) where {M} +) JuMP.check_belongs_to_model(con_ref, model) JuMP.check_belongs_to_model.(value, model) + v = JuMP.vectorize(value, con_ref.shape) return MOI.set( JuMP.backend(model), ForwardConstraintFunction(), JuMP.index(con_ref), - JuMP.moi_function(value), - ) -end - -function set_forward_constraint_function( - model::JuMP.Model, - con_ref::JuMP.ConstraintRef{ - M, - <:MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, - }, - value::AbstractArray{<:Number}, -) where {M} - JuMP.check_belongs_to_model(con_ref, model) - return MOI.set( - JuMP.backend(model), - ForwardConstraintFunction(), - JuMP.index(con_ref), - JuMP.moi_function(JuMP.AffExpr.(value)), + JuMP.moi_function(v), ) end +# Similar to `JuMP.set_start_value` for vector `ConstraintRef` in +# JuMP/src/constraints.jl function set_forward_constraint_function( model::JuMP.Model, con_ref::JuMP.ConstraintRef{ <:JuMP.AbstractModel, <:MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, - S, }, - value::AbstractMatrix{<:Number}, -) where {S<:Union{JuMP.SquareMatrixShape,JuMP.SymmetricMatrixShape}} - if !LinearAlgebra.issymmetric(value) - error( - "ForwardConstraintFunction perturbation matrix must be " * - "symmetric for PSD cone constraints.", - ) - end - JuMP.check_belongs_to_model(con_ref, model) - v = JuMP.vectorize(value, con_ref.shape) - func = JuMP.moi_function(JuMP.AffExpr.(v)) - MOI.set( - JuMP.backend(model), - ForwardConstraintFunction(), - JuMP.index(con_ref), - func, - ) - return -end - -function set_forward_constraint_function( - model::JuMP.Model, - con_ref::JuMP.ConstraintRef{<:JuMP.AbstractModel,<:MOI.ConstraintIndex,S}, - value::AbstractMatrix{<:JuMP.AbstractJuMPScalar}, -) where {S<:Union{JuMP.SquareMatrixShape,JuMP.SymmetricMatrixShape}} - if !LinearAlgebra.issymmetric(value) - error( - "ForwardConstraintFunction perturbation matrix must be " * - "symmetric for PSD cone constraints.", - ) - end - JuMP.check_belongs_to_model(con_ref, model) - JuMP.check_belongs_to_model.(value, model) - v = JuMP.vectorize(value, con_ref.shape) - func = JuMP.moi_function(v) - MOI.set( - JuMP.backend(model), - ForwardConstraintFunction(), - JuMP.index(con_ref), - func, - ) - return + value::AbstractArray{<:Number}, +) + return set_forward_constraint_function(model, con_ref, JuMP.AffExpr.(value)) end """ diff --git a/test/bridges.jl b/test/bridges.jl index de92e72d3..8fb01c334 100644 --- a/test/bridges.jl +++ b/test/bridges.jl @@ -104,77 +104,6 @@ function test_dU_from_dQ() return _test_dU_dQ(U, dU) end -function _make_square_bridge(dim) - s = MOI.PositiveSemidefiniteConeSquare(dim) - return MOI.Bridges.Constraint.SquareBridge{ - Float64, - MOI.VectorAffineFunction{Float64}, - MOI.ScalarAffineFunction{Float64}, - MOI.PositiveSemidefiniteConeTriangle, - MOI.PositiveSemidefiniteConeSquare, - }( - s, - MOI.ConstraintIndex{ - MOI.VectorAffineFunction{Float64}, - MOI.PositiveSemidefiniteConeTriangle, - }( - 1, - ), - Pair{ - Tuple{Int,Int}, - MOI.ConstraintIndex{ - MOI.ScalarAffineFunction{Float64}, - MOI.EqualTo{Float64}, - }, - }[], - ) -end - -function test_square_to_triangle_indices() - # 2x2: square col-major [a11, a21, a12, a22] → upper tri [a11, a12, a22] - @test DiffOpt._square_to_triangle_indices(_make_square_bridge(2)) == - [1, 3, 4] - # 3x3: square col-major [a11,a21,a31, a12,a22,a32, a13,a23,a33] - # → upper tri [a11, a12,a22, a13,a23,a33] at indices [1, 4,5, 7,8,9] - @test DiffOpt._square_to_triangle_indices(_make_square_bridge(3)) == - [1, 4, 5, 7, 8, 9] - # 1x1: trivial - @test DiffOpt._square_to_triangle_indices(_make_square_bridge(1)) == [1] -end - -function _make_rootdet_square_bridge(dim) - s = MOI.RootDetConeSquare(dim) - return MOI.Bridges.Constraint.SquareBridge{ - Float64, - MOI.VectorAffineFunction{Float64}, - MOI.ScalarAffineFunction{Float64}, - MOI.RootDetConeTriangle, - MOI.RootDetConeSquare, - }( - s, - MOI.ConstraintIndex{ - MOI.VectorAffineFunction{Float64}, - MOI.RootDetConeTriangle, - }( - 1, - ), - Pair{ - Tuple{Int,Int}, - MOI.ConstraintIndex{ - MOI.ScalarAffineFunction{Float64}, - MOI.EqualTo{Float64}, - }, - }[], - ) -end - -function test_square_to_triangle_indices_with_offset() - # RootDetConeSquare(2) has 1 offset entry (the rootdet variable t) - # Full square: [t, a11, a21, a12, a22] → triangle: [t, a11, a12, a22] - bridge = _make_rootdet_square_bridge(2) - @test DiffOpt._square_to_triangle_indices(bridge) == [1, 2, 4, 5] -end - function test_square_offset() @test DiffOpt._square_offset(MOI.PositiveSemidefiniteConeSquare(2)) == 0 @test DiffOpt._square_offset(MOI.RootDetConeSquare(2)) == 1 @@ -187,27 +116,6 @@ function test_square_offset() [1, 2] end -function test_triangle_to_square_scalars() - # 2x2 PSD: triangle [a11, a12, a22] → square [a11, a12, a12, a22] - s = MOI.PositiveSemidefiniteConeSquare(2) - @test DiffOpt._triangle_to_square_scalars([1, 2, 3], s) == [1, 2, 2, 3] - # 3x3 PSD: triangle [a11, a12, a22, a13, a23, a33] - # → square col-major [a11, a12, a13, a12, a22, a23, a13, a23, a33] - s3 = MOI.PositiveSemidefiniteConeSquare(3) - @test DiffOpt._triangle_to_square_scalars([1, 2, 3, 4, 5, 6], s3) == - [1, 2, 4, 2, 3, 5, 4, 5, 6] - # RootDetConeSquare(2): triangle [t, a11, a12, a22] - # → square [t, a11, a12, a12, a22] - sr = MOI.RootDetConeSquare(2) - @test DiffOpt._triangle_to_square_scalars([10, 1, 2, 3], sr) == - [10, 1, 2, 2, 3] - # LogDetConeSquare(2): triangle [u, t, a11, a12, a22] - # → square [u, t, a11, a12, a12, a22] - sl = MOI.LogDetConeSquare(2) - @test DiffOpt._triangle_to_square_scalars([10, 20, 1, 2, 3], sl) == - [10, 20, 1, 2, 2, 3] -end - end TestBridges.runtests() diff --git a/test/conic_program.jl b/test/conic_program.jl index 298e87f37..a45b1a461 100644 --- a/test/conic_program.jl +++ b/test/conic_program.jl @@ -1024,18 +1024,104 @@ function test_square_bridge_reverse_psd() return end -function test_square_bridge_asymmetric_error() +function test_square_bridge_asymmetric_sym() + # Tests SquareBridge with non-empty bridge.sym (different off-diagonal variables). + # Similar to MOI's ConstraintPrimalStart/ConstraintDual on SquareBridge. + # + # min x + y s.t. [2 x; y 2] ∈ PSD + # Bridge creates: [2, x, 2] ∈ PSDTriangle(2) and [x - y] ∈ Zeros(1) + # So x == y (implicit), eigenvalues are 2+x and 2-x, both >= 0 → -2 ≤ x ≤ 2 + # min 2x → x = y = -2, obj = -4 + # Perturbing diagonal by ε: [2+ε, x, 2+ε] → x = -(2+ε), dx/dε = dy/dε = -1 model = DiffOpt.diff_model(SCS.Optimizer) MOI.set(model, MOI.RawOptimizerAttribute("verbose"), false) - @variable(model, t >= 0) - cons = @constraint(model, [t 1.0; 1.0 t] in PSDCone()) - @objective(model, Min, t) + MOI.set(model, MOI.RawOptimizerAttribute("eps_abs"), 1e-8) + MOI.set(model, MOI.RawOptimizerAttribute("eps_rel"), 1e-8) + @variable(model, x) + @variable(model, y) + @constraint(model, cpsd, [2.0 1.0*x; 1.0*y 2.0] in PSDCone()) + @objective(model, Min, x + y) optimize!(model) - @test_throws ErrorException DiffOpt.set_forward_constraint_function( - model, - cons, - [0.0 1.0; 0.0 0.0], + @test value(x) ≈ -2.0 atol = ATOL + @test value(y) ≈ -2.0 atol = ATOL + # Forward: perturb diagonal by 1 → dx/dε = dy/dε = -1 + DiffOpt.set_forward_constraint_function(model, cpsd, [1.0 0.0; 0.0 1.0]) + DiffOpt.forward_differentiate!(model) + @test DiffOpt.get_forward_variable(model, x) ≈ -1.0 atol = ATOL + @test DiffOpt.get_forward_variable(model, y) ≈ -1.0 atol = ATOL + # Reverse: seed dx=1, dy=1 → d(loss)/d(diag) = 1*(-1) + 1*(-1) = -2 + DiffOpt.empty_input_sensitivities!(model) + MOI.set( + JuMP.backend(model), + DiffOpt.ReverseVariablePrimal(), + JuMP.index(x), + 1.0, + ) + MOI.set( + JuMP.backend(model), + DiffOpt.ReverseVariablePrimal(), + JuMP.index(y), + 1.0, + ) + DiffOpt.reverse_differentiate!(model) + rev = MOI.get( + JuMP.backend(model), + DiffOpt.ReverseConstraintFunction(), + JuMP.index(cpsd), ) + cte = MOI.constant(rev) + @test length(cte) == 4 # square form: [a11, a21, a12, a22] + # Diagonal entries: each ≈ -1.0 (follows ConstraintDual convention) + @test cte[1] ≈ -1.0 atol = ATOL + @test cte[4] ≈ -1.0 atol = ATOL + # Off-diagonal: ConstraintDual convention with sym (2η+π / -π) + # where η ≈ -1.0 (triangle off-diag gradient) and π ≈ 1.0 (sym dual) + @test cte[2] ≈ -1.0 atol = ATOL # -π + @test cte[3] ≈ -3.0 atol = ATOL # 2η + π + return +end + +function test_square_bridge_asymmetric_sym_parametric() + # Same as test_square_bridge_asymmetric_sym but with parameter p on diagonal. + # min x + y s.t. [p x; y p] ∈ PSD, p = 2 + # Bridge: [p, x, p] ∈ PSDTriangle(2), [x - y] ∈ Zeros(1) + # Eigenvalues: p+x and p-x, both ≥ 0 → -p ≤ x ≤ p + # min 2x → x = y = -p = -2 + # dx/dp = dy/dp = -1 + model = DiffOpt.conic_diff_model(SCS.Optimizer) + MOI.set(model, MOI.RawOptimizerAttribute("verbose"), false) + MOI.set(model, MOI.RawOptimizerAttribute("eps_abs"), 1e-8) + MOI.set(model, MOI.RawOptimizerAttribute("eps_rel"), 1e-8) + @variable(model, x) + @variable(model, y) + @variable(model, p in Parameter(2.0)) + @constraint(model, cpsd, [1.0*p 1.0*x; 1.0*y 1.0*p] in PSDCone()) + @objective(model, Min, x + y) + optimize!(model) + @test value(x) ≈ -2.0 atol = ATOL + @test value(y) ≈ -2.0 atol = ATOL + # Forward: dp = 1 → dx = dy = -1 + DiffOpt.set_forward_parameter(model, p, 1.0) + DiffOpt.forward_differentiate!(model) + @test DiffOpt.get_forward_variable(model, x) ≈ -1.0 atol = ATOL + @test DiffOpt.get_forward_variable(model, y) ≈ -1.0 atol = ATOL + # Reverse: seed dx=1 → dp = -1 + DiffOpt.empty_input_sensitivities!(model) + DiffOpt.set_reverse_variable(model, x, 1.0) + DiffOpt.reverse_differentiate!(model) + @test DiffOpt.get_reverse_parameter(model, p) ≈ -1.0 atol = ATOL + # Reverse: seed dy=1 → dp = -1 + DiffOpt.empty_input_sensitivities!(model) + DiffOpt.set_reverse_variable(model, y, 1.0) + DiffOpt.reverse_differentiate!(model) + @test DiffOpt.get_reverse_parameter(model, p) ≈ -1.0 atol = ATOL + # Reverse: seed dx=1, dy=1 → dp = -2 + DiffOpt.empty_input_sensitivities!(model) + DiffOpt.set_reverse_variable(model, x, 1.0) + DiffOpt.set_reverse_variable(model, y, 1.0) + DiffOpt.reverse_differentiate!(model) + @test DiffOpt.get_reverse_parameter(model, p) ≈ -2.0 atol = ATOL + return end end # module diff --git a/test/jump_wrapper.jl b/test/jump_wrapper.jl index 65aadc42d..43d531f08 100644 --- a/test/jump_wrapper.jl +++ b/test/jump_wrapper.jl @@ -420,13 +420,6 @@ function test_forward_psd_matrix_wrapper() DiffOpt.set_forward_constraint_function(model, con, perturbation) DiffOpt.forward_differentiate!(model) @test DiffOpt.get_forward_variable(model, x) ≈ 1.0 atol = ATOL - - bad = [0.0*x 1.0*x; 0.0*x 0.0*x] - @test_throws ErrorException DiffOpt.set_forward_constraint_function( - model, - con, - bad, - ) return end