diff --git a/src/ConicProgram/ConicProgram.jl b/src/ConicProgram/ConicProgram.jl index 415ad968..ee5492b0 100644 --- a/src/ConicProgram/ConicProgram.jl +++ b/src/ConicProgram/ConicProgram.jl @@ -477,10 +477,8 @@ end Method not supported for `DiffOpt.ConicProgram.Model` directly. However, a fallback is provided in `DiffOpt`. """ -function MOI.set(::Model, ::DiffOpt.ReverseObjectiveSensitivity, val) - return throw( - MOI.UnsupportedAttribute(DiffOpt.ReverseObjectiveSensitivity()), - ) +function MOI.set(::Model, ::DiffOpt.ReverseObjectiveValue, val) + return throw(MOI.UnsupportedAttribute(DiffOpt.ReverseObjectiveValue())) end end diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 9b1bf313..9d813b8b 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -434,7 +434,7 @@ function _lu_with_inertia_correction( return K end -_all_variables(form::Form) = MOI.VariableIndex.(1:form.num_variables) +_all_variables(form::Form) = MOI.VariableIndex.(1:(form.num_variables)) _all_variables(model::Model) = _all_variables(model.model) _all_params(form::Form) = collect(keys(form.var2param)) _all_params(model::Model) = _all_params(model.model) @@ -650,7 +650,7 @@ function MOI.get(model::Model, ::DiffOpt.ForwardObjectiveSensitivity) return model.forw_grad_cache.objective_sensitivity_p end -function MOI.set(model::Model, ::DiffOpt.ReverseObjectiveSensitivity, val) +function MOI.set(model::Model, ::DiffOpt.ReverseObjectiveValue, val) model.input_cache.dobj = val return end diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 12e639fa..0aa97c09 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -109,7 +109,7 @@ function _create_evaluator(form::Form) evaluator = MOI.Nonlinear.Evaluator( nlp, backend, - MOI.VariableIndex.(1:form.num_variables), + MOI.VariableIndex.(1:(form.num_variables)), ) MOI.initialize(evaluator, [:Hess, :Jac, :Grad]) return evaluator diff --git a/src/QuadraticProgram/QuadraticProgram.jl b/src/QuadraticProgram/QuadraticProgram.jl index b52901da..8273308e 100644 --- a/src/QuadraticProgram/QuadraticProgram.jl +++ b/src/QuadraticProgram/QuadraticProgram.jl @@ -519,10 +519,8 @@ end Method not supported for `DiffOpt.QuadraticProgram.Model` directly. However, a fallback is provided in `DiffOpt`. """ -function MOI.set(::Model, ::DiffOpt.ReverseObjectiveSensitivity, val) - return throw( - MOI.UnsupportedAttribute(DiffOpt.ReverseObjectiveSensitivity()), - ) +function MOI.set(::Model, ::DiffOpt.ReverseObjectiveValue, val) + return throw(MOI.UnsupportedAttribute(DiffOpt.ReverseObjectiveValue())) end end diff --git a/src/diff_opt.jl b/src/diff_opt.jl index e018b791..b6188442 100644 --- a/src/diff_opt.jl +++ b/src/diff_opt.jl @@ -156,6 +156,23 @@ Currently, this only works for the set `MOI.Parameter`. """ struct ForwardConstraintSet <: MOI.AbstractConstraintAttribute end +""" + ForwardParameterValue <: MOI.AbstractVariableAttribute + +A JuMP-level shortcut for forward differentiation with respect to a parameter +variable. Equivalent to setting +`ForwardConstraintSet()` on `ParameterRef(p)` with a wrapped `MOI.Parameter`: + +```julia +set_attribute(p, DiffOpt.ForwardParameterValue(), value) +# same as +set_attribute( + ParameterRef(p), DiffOpt.ForwardConstraintSet(), MOI.Parameter(value), +) +``` +""" +struct ForwardParameterValue <: MOI.AbstractVariableAttribute end + """ ForwardVariablePrimal <: MOI.AbstractVariableAttribute @@ -201,7 +218,7 @@ MOI.set(model, DiffOpt.ReverseConstraintDual(), ci, value) struct ReverseConstraintDual <: MOI.AbstractConstraintAttribute end """ - ReverseObjectiveSensitivity <: MOI.AbstractModelAttribute + ReverseObjectiveValue <: MOI.AbstractModelAttribute A `MOI.AbstractModelAttribute` to set input data for reverse differentiation. @@ -209,10 +226,12 @@ For instance, to set the sensitivity of the parameter perturbation with respect objective function perturbation, do the following: ```julia -MOI.set(model, DiffOpt.ReverseObjectiveSensitivity(), value) +MOI.set(model, DiffOpt.ReverseObjectiveValue(), value) ``` """ -struct ReverseObjectiveSensitivity <: MOI.AbstractModelAttribute end +struct ReverseObjectiveValue <: MOI.AbstractModelAttribute end + +Base.@deprecate_binding ReverseObjectiveSensitivity ReverseObjectiveValue """ ForwardConstraintDual <: MOI.AbstractConstraintAttribute @@ -310,6 +329,23 @@ struct ReverseConstraintSet <: MOI.AbstractConstraintAttribute end MOI.is_set_by_optimize(::ReverseConstraintSet) = true +""" + ReverseParameterValue <: MOI.AbstractVariableAttribute + +A JuMP-level shortcut for reverse differentiation output with respect to a +parameter variable. Equivalent to reading `ReverseConstraintSet()` on +`ParameterRef(p)` and unwrapping `MOI.Parameter`: + +```julia +get_attribute(p, DiffOpt.ReverseParameterValue()) +# same as +get_attribute(ParameterRef(p), DiffOpt.ReverseConstraintSet()).value +``` +""" +struct ReverseParameterValue <: MOI.AbstractVariableAttribute end + +MOI.is_set_by_optimize(::ReverseParameterValue) = true + """ DifferentiateTimeSec() diff --git a/src/jump_moi_overloads.jl b/src/jump_moi_overloads.jl index 3d13a680..f0d22008 100644 --- a/src/jump_moi_overloads.jl +++ b/src/jump_moi_overloads.jl @@ -12,7 +12,6 @@ # done after the model is optimized, so we add function to bypass the # dirty state. -# DEPRECATE function MOI.set( model::JuMP.Model, attr::ForwardObjectiveFunction, @@ -38,7 +37,6 @@ function MOI.set( return MOI.set(JuMP.backend(model), attr, allow) end -# DEPRECATE function MOI.set( model::JuMP.Model, attr::ForwardObjectiveFunction, @@ -47,28 +45,71 @@ function MOI.set( return MOI.set(model, attr, JuMP.AffExpr(func)) end -# DEPRECATE function MOI.set( model::JuMP.Model, attr::ForwardConstraintFunction, - con_ref::JuMP.ConstraintRef, + con_ref::JuMP.ConstraintRef{ + <:JuMP.AbstractModel, + <:MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, + }, func::JuMP.AbstractJuMPScalar, ) + JuMP.check_belongs_to_model(con_ref, model) JuMP.check_belongs_to_model(func, model) - return MOI.set(model, attr, con_ref, JuMP.moi_function(func)) + return MOI.set( + JuMP.backend(model), + attr, + JuMP.index(con_ref), + JuMP.moi_function(func), + ) end -# DEPRECATE function MOI.set( model::JuMP.Model, attr::ForwardConstraintFunction, - con_ref::JuMP.ConstraintRef, + con_ref::JuMP.ConstraintRef{ + <:JuMP.AbstractModel, + <:MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}, + }, func::Number, ) return MOI.set(model, attr, con_ref, JuMP.AffExpr(func)) end -# DEPRECATE - then modify +# Similar to `JuMP.set_start_value` for vector `ConstraintRef` in +# JuMP/src/constraints.jl +function MOI.set( + model::JuMP.Model, + attr::ForwardConstraintFunction, + con_ref::JuMP.ConstraintRef{ + <:JuMP.AbstractModel, + <:MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, + }, + value::AbstractArray{<:JuMP.AbstractJuMPScalar}, +) + 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), + attr, + JuMP.index(con_ref), + JuMP.moi_function(v), + ) +end + +function MOI.set( + model::JuMP.Model, + attr::ForwardConstraintFunction, + con_ref::JuMP.ConstraintRef{ + <:JuMP.AbstractModel, + <:MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, + }, + value::AbstractArray{<:Number}, +) + return MOI.set(model, attr, con_ref, JuMP.AffExpr.(value)) +end + function MOI.get( model::JuMP.Model, attr::ForwardConstraintDual, @@ -79,13 +120,11 @@ 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, @@ -113,7 +152,6 @@ function _moi_get_result(model::MOI.Utilities.CachingOptimizer, args...) return MOI.get(model, args...) end -# DEPRECATE function MOI.get( model::JuMP.Model, attr::ForwardVariablePrimal, @@ -123,7 +161,6 @@ 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, @@ -143,9 +180,30 @@ function MOI.set( return MOI.set(JuMP.backend(model), attr, JuMP.index(con_ref), set) end +function MOI.set( + model::JuMP.Model, + ::ForwardParameterValue, + p::JuMP.VariableRef, + value::Number, +) + return MOI.set( + model, + ForwardConstraintSet(), + JuMP.ParameterRef(p), + MOI.Parameter(value), + ) +end + +function MOI.get( + model::JuMP.Model, + ::ReverseParameterValue, + p::JuMP.VariableRef, +) + return MOI.get(model, ReverseConstraintSet(), JuMP.ParameterRef(p)).value +end + # there is no set_forward_constraint_set because there is set_forward_parameter -# DEPRECATE function MOI.set( model::JuMP.Model, attr::ForwardConstraintSet, diff --git a/src/jump_wrapper.jl b/src/jump_wrapper.jl index eacd12cc..d30c2628 100644 --- a/src/jump_wrapper.jl +++ b/src/jump_wrapper.jl @@ -99,6 +99,8 @@ end set_forward_parameter(model::JuMP.Model, variable::JuMP.VariableRef, value::Number) Set the value of a parameter input sensitivity for forward mode. + +Equivalent to `set_attribute(variable, DiffOpt.ForwardParameterValue(), value)`. """ function set_forward_parameter( model::JuMP.Model, @@ -118,6 +120,8 @@ end get_reverse_parameter(model::JuMP.Model, variable::JuMP.VariableRef) Get the value of a parameter output sensitivity for reverse mode. + +Equivalent to `get_attribute(variable, DiffOpt.ReverseParameterValue())`. """ function get_reverse_parameter(model::JuMP.Model, variable::JuMP.VariableRef) JuMP.check_belongs_to_model(variable, model) @@ -132,6 +136,8 @@ end set_reverse_variable(model::JuMP.Model, variable::JuMP.VariableRef, value::Number) Set the value of a variable input sensitivity for reverse mode. + +Equivalent to `set_attribute(variable, DiffOpt.ReverseVariablePrimal(), value)`. """ function set_reverse_variable( model::JuMP.Model, @@ -147,10 +153,33 @@ function set_reverse_variable( ) end +""" + set_reverse_constraint_dual(model::JuMP.Model, con_ref::JuMP.ConstraintRef, value::Number) + +Set the value of a constraint dual input sensitivity for reverse mode. + +Equivalent to `set_attribute(con_ref, DiffOpt.ReverseConstraintDual(), value)`. +""" +function set_reverse_constraint_dual( + model::JuMP.Model, + con_ref::JuMP.ConstraintRef, + value::Number, +) + JuMP.check_belongs_to_model(con_ref, model) + return MOI.set( + JuMP.backend(model), + ReverseConstraintDual(), + JuMP.index(con_ref), + value, + ) +end + """ get_forward_variable(model::JuMP.Model, variable::JuMP.VariableRef) Get the value of a variable output sensitivity for forward mode. + +Equivalent to `get_attribute(variable, DiffOpt.ForwardVariablePrimal())`. """ function get_forward_variable(model::JuMP.Model, variable::JuMP.VariableRef) JuMP.check_belongs_to_model(variable, model) @@ -165,15 +194,19 @@ end set_reverse_objective(model::JuMP.Model, value::Number) Set the value of the objective input sensitivity for reverse mode. + +Equivalent to `set_attribute(model, DiffOpt.ReverseObjectiveValue(), value)`. """ function set_reverse_objective(model::JuMP.Model, value::Number) - return MOI.set(model, ReverseObjectiveSensitivity(), value) + return MOI.set(model, ReverseObjectiveValue(), value) end """ get_forward_objective(model::JuMP.Model) Get the value of the objective output sensitivity for forward mode. + +Equivalent to `get_attribute(model, DiffOpt.ForwardObjectiveSensitivity())`. """ function get_forward_objective(model::JuMP.Model) return MOI.get(model, ForwardObjectiveSensitivity()) @@ -183,6 +216,8 @@ end set_forward_objective_function(model::JuMP.Model, func) Set the function to be used for forward mode differentiation of the objective. + +Equivalent to `set_attribute(model, DiffOpt.ForwardObjectiveFunction(), func)`. """ function set_forward_objective_function( model::JuMP.Model, @@ -207,6 +242,8 @@ 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. + +Equivalent to `set_attribute(con_ref, DiffOpt.ForwardConstraintFunction(), func)`. """ function set_forward_constraint_function( model::JuMP.Model, @@ -258,8 +295,6 @@ function set_forward_constraint_function( ) 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{ @@ -275,6 +310,8 @@ end get_forward_constraint_dual(model::JuMP.Model, con_ref::JuMP.ConstraintRef) Get the value of a constraint dual output sensitivity for forward mode. + +Equivalent to `get_attribute(con_ref, DiffOpt.ForwardConstraintDual())`. """ function get_forward_constraint_dual( model::JuMP.Model, @@ -293,6 +330,8 @@ end get_reverse_objective_function(model::JuMP.Model) Get the function to be used for reverse mode differentiation of the objective. + +Equivalent to `get_attribute(model, DiffOpt.ReverseObjectiveFunction())`. """ function get_reverse_objective_function(model::JuMP.Model) func = MOI.get(JuMP.backend(model), ReverseObjectiveFunction()) @@ -303,6 +342,8 @@ 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. + +Equivalent to `get_attribute(con_ref, DiffOpt.ReverseConstraintFunction())`. """ function get_reverse_constraint_function( model::JuMP.Model, diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index 022e3815..d46c3f7a 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -588,7 +588,7 @@ function MOI.set(model::Optimizer, attr::ReverseDifferentiate, value) end if !iszero(model.input_cache.dobj) try - MOI.set(diff, ReverseObjectiveSensitivity(), model.input_cache.dobj) + MOI.set(diff, ReverseObjectiveValue(), model.input_cache.dobj) catch e if e isa MOI.UnsupportedAttribute _fallback_set_reverse_objective_sensitivity( @@ -1006,7 +1006,7 @@ function MOI.set( return end -function MOI.set(model::Optimizer, ::ReverseObjectiveSensitivity, val) +function MOI.set(model::Optimizer, ::ReverseObjectiveValue, val) model.input_cache.dobj = val return end diff --git a/test/jump_wrapper.jl b/test/jump_wrapper.jl index 97302999..a41f40b1 100644 --- a/test/jump_wrapper.jl +++ b/test/jump_wrapper.jl @@ -427,6 +427,206 @@ function test_forward_psd_matrix_wrapper() return end +function test_set_get_attribute() + # Smoke test for the `set_attribute` / `get_attribute` syntax with every + # DiffOpt forward and reverse attribute. The wrapper functions are the + # documented entry points; this covers the MOI attribute path. + + # Parametric NLP via Ipopt — supports every Forward/Reverse attribute, + # including ForwardConstraintDual which is not implemented on the + # auto-selected QuadraticProgram backend. + 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) + + # Forward via attributes: ForwardParameterValue (set), + # ForwardVariablePrimal (get), ForwardObjectiveSensitivity (get), + # ForwardConstraintDual (get). + DiffOpt.empty_input_sensitivities!(model) + set_attribute(p, DiffOpt.ForwardParameterValue(), 1.0) + DiffOpt.forward_differentiate!(model) + @test get_attribute(x, DiffOpt.ForwardVariablePrimal()) ≈ 0.25 atol = ATOL rtol = + RTOL + @test get_attribute(y, DiffOpt.ForwardVariablePrimal()) ≈ 0.75 atol = ATOL rtol = + RTOL + @test get_attribute(c1, DiffOpt.ForwardConstraintDual()) ≈ 1.75 atol = ATOL rtol = + RTOL + @test get_attribute(model, DiffOpt.ForwardObjectiveSensitivity()) ≈ 2.75 atol = + ATOL rtol = RTOL + + # Reverse via attributes: ReverseVariablePrimal (set), + # ReverseParameterValue (get), ReverseConstraintSet (get on ParameterRef). + DiffOpt.empty_input_sensitivities!(model) + set_attribute(x, DiffOpt.ReverseVariablePrimal(), 1.0) + DiffOpt.reverse_differentiate!(model) + dp = get_attribute(p, DiffOpt.ReverseParameterValue()) + @test isfinite(dp) + rcs = get_attribute(ParameterRef(p), DiffOpt.ReverseConstraintSet()).value + @test rcs ≈ dp atol = ATOL + + # Reverse from the objective seed: ReverseObjectiveValue (set). + DiffOpt.empty_input_sensitivities!(model) + set_attribute(model, DiffOpt.ReverseObjectiveValue(), 1.0) + DiffOpt.reverse_differentiate!(model) + @test isfinite(get_attribute(p, DiffOpt.ReverseParameterValue())) + + # Non-parametric model — exercise ForwardObjectiveFunction (set), + # ForwardConstraintFunction (set, scalar), ReverseObjectiveFunction (get), + # ReverseConstraintFunction (get), ReverseConstraintDual (set) via the + # attribute syntax. + direct = JuMP.direct_model(DiffOpt.diff_optimizer(HiGHS.Optimizer)) + set_silent(direct) + @variable(direct, x2 >= 0) + @variable(direct, y2 >= 0) + @constraint(direct, c2, x2 + y2 == 1) + @objective(direct, Min, 1.0 * x2) + optimize!(direct) + + set_attribute(direct, DiffOpt.ForwardObjectiveFunction(), 0.0) + set_attribute(c2, DiffOpt.ForwardConstraintFunction(), 1.0) + DiffOpt.forward_differentiate!(direct) + @test get_attribute(y2, DiffOpt.ForwardVariablePrimal()) ≈ -1.0 atol = ATOL + + DiffOpt.empty_input_sensitivities!(direct) + set_attribute(y2, DiffOpt.ReverseVariablePrimal(), 1.0) + DiffOpt.reverse_differentiate!(direct) + @test get_attribute(direct, DiffOpt.ReverseObjectiveFunction()) isa + JuMP.AbstractJuMPScalar + @test get_attribute(c2, DiffOpt.ReverseConstraintFunction()) isa + JuMP.AbstractJuMPScalar + + DiffOpt.empty_input_sensitivities!(direct) + set_attribute(c2, DiffOpt.ReverseConstraintDual(), 1.0) + DiffOpt.reverse_differentiate!(direct) + @test get_attribute(direct, DiffOpt.ReverseObjectiveFunction()) isa + JuMP.AbstractJuMPScalar + + # Vector-form ForwardConstraintFunction via attributes on a conic model. + cmodel = DiffOpt.conic_diff_model(SCS.Optimizer) + set_silent(cmodel) + @variable(cmodel, xv) + @variable(cmodel, yv) + @constraint(cmodel, c_eq, xv + yv == 1) + @constraint(cmodel, c_nn, [1.0 * yv, 1.0 * xv] in MOI.Nonnegatives(2)) + @objective(cmodel, Min, 1.0 * xv) + optimize!(cmodel) + set_attribute(c_nn, DiffOpt.ForwardConstraintFunction(), [0.0, 0.0]) + set_attribute(c_eq, DiffOpt.ForwardConstraintFunction(), 1.0) + DiffOpt.forward_differentiate!(cmodel) + @test get_attribute(yv, DiffOpt.ForwardVariablePrimal()) ≈ -1.0 atol = ATOL + + # ForwardConstraintSet on a ParameterRef (the underlying mechanism behind + # ForwardParameterValue). + model2 = DiffOpt.diff_model(HiGHS.Optimizer) + set_silent(model2) + @variable(model2, xx) + @variable(model2, pp in Parameter(1.0)) + @constraint(model2, xx == 2 * pp) + @objective(model2, Min, xx) + optimize!(model2) + set_attribute( + ParameterRef(pp), + DiffOpt.ForwardConstraintSet(), + MOI.Parameter(1.0), + ) + DiffOpt.forward_differentiate!(model2) + @test get_attribute(xx, DiffOpt.ForwardVariablePrimal()) ≈ 2.0 atol = ATOL + return +end + +function test_attribute_types() + @test DiffOpt.ForwardParameterValue() isa MOI.AbstractVariableAttribute + @test DiffOpt.ReverseParameterValue() isa MOI.AbstractVariableAttribute + @test DiffOpt.ForwardVariablePrimal() isa MOI.AbstractVariableAttribute + @test DiffOpt.ReverseVariablePrimal() isa MOI.AbstractVariableAttribute + @test DiffOpt.ForwardObjectiveFunction() isa MOI.AbstractModelAttribute + @test DiffOpt.ReverseObjectiveFunction() isa MOI.AbstractModelAttribute + @test DiffOpt.ForwardObjectiveSensitivity() isa MOI.AbstractModelAttribute + @test DiffOpt.ReverseObjectiveValue() isa MOI.AbstractModelAttribute + @test DiffOpt.ForwardConstraintFunction() isa + MOI.AbstractConstraintAttribute + @test DiffOpt.ReverseConstraintFunction() isa + MOI.AbstractConstraintAttribute + @test DiffOpt.ForwardConstraintDual() isa MOI.AbstractConstraintAttribute + @test DiffOpt.ReverseConstraintDual() isa MOI.AbstractConstraintAttribute + return +end + +function test_parameter_wrappers() + # Parametric NLP `min 2x² + y² + xy + x + y s.t. x + y == p, x,y ≥ 0` + # has unique solution x* = 0.25, y* = 0.75 at p = 1, with constraint dual + # 1.75 and objective value 2.75 per unit of p. + 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) + + # Forward: seed dp = 1, read every forward getter wrapper. + DiffOpt.empty_input_sensitivities!(model) + 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_constraint_dual(model, c1) ≈ 1.75 atol = ATOL rtol = + RTOL + @test DiffOpt.get_forward_objective(model) ≈ 2.75 atol = ATOL rtol = RTOL + + # Reverse with variable seed: adjoint identity gives back dx/dp = 0.25. + DiffOpt.empty_input_sensitivities!(model) + DiffOpt.set_reverse_variable(model, x, 1.0) + DiffOpt.reverse_differentiate!(model) + @test DiffOpt.get_reverse_parameter(model, p) ≈ 0.25 atol = ATOL rtol = RTOL + + # Reverse with objective seed: gives df/dp = 2.75, matching the forward + # objective sensitivity above. + DiffOpt.empty_input_sensitivities!(model) + DiffOpt.set_reverse_objective(model, 1.0) + DiffOpt.reverse_differentiate!(model) + @test DiffOpt.get_reverse_parameter(model, p) ≈ 2.75 atol = ATOL rtol = RTOL + return +end + +function test_function_getter_wrappers() + # QP `min x² + 2y² s.t. x + y == 1` has x* = 2/3, y* = 1/3 with non- + # degenerate sensitivities, so the wrapper getters return informative + # affine/quadratic expressions instead of zeros. + model = JuMP.direct_model(DiffOpt.diff_optimizer(Ipopt.Optimizer)) + set_silent(model) + @variable(model, x) + @variable(model, y) + @constraint(model, c, x + y == 1) + @objective(model, Min, x^2 + 2y^2) + optimize!(model) + @test value(x) ≈ 2 / 3 atol = ATOL rtol = RTOL + @test value(y) ≈ 1 / 3 atol = ATOL rtol = RTOL + + # Seed dy = 1; expected closed-form sensitivities derived from KKT (see + # `test_function_getter_wrappers` analytical notes above): + # dy/d(linear coef of x in obj) = 1/6, dy/d(linear coef of y) = -1/6 + # dy/d(coef of x in c) = -4/9, dy/d(coef of y in c) = 1/9 + # dy/d(rhs of c) = 1/3 (stored constant has the opposite sign) + DiffOpt.set_reverse_variable(model, y, 1.0) + DiffOpt.reverse_differentiate!(model) + + obj_grad = DiffOpt.get_reverse_objective_function(model) + @test JuMP.coefficient(obj_grad, x) ≈ 1 / 6 atol = ATOL rtol = RTOL + @test JuMP.coefficient(obj_grad, y) ≈ -1 / 6 atol = ATOL rtol = RTOL + + con_grad = DiffOpt.get_reverse_constraint_function(model, c) + @test JuMP.coefficient(con_grad, x) ≈ -4 / 9 atol = ATOL rtol = RTOL + @test JuMP.coefficient(con_grad, y) ≈ 1 / 9 atol = ATOL rtol = RTOL + @test JuMP.constant(con_grad) ≈ -1 / 3 atol = ATOL rtol = RTOL + return +end + end # module TestJuMPWrapper.runtests() diff --git a/test/nlp_program.jl b/test/nlp_program.jl index cc208a0a..b68180d5 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -412,9 +412,9 @@ function test_compute_derivatives_Analytical(; DICT_PROBLEMS = DICT_PROBLEMS_Analytical_no_cc, ) @testset "Compute Derivatives Analytical: $problem_name" for ( - problem_name, - (p_a, Δp, Δx, Δy, Δvu, Δvl, model_generator), - ) in DICT_PROBLEMS + problem_name, + (p_a, Δp, Δx, Δy, Δvu, Δvl, model_generator), + ) in DICT_PROBLEMS # OPT Problem model, primal_vars, cons, params = model_generator() set_parameter_value.(params, p_a)