From 0dc8099a3b89b8d0f2668e21f2c3eb99252a488b Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 6 Dec 2024 09:58:05 -0500 Subject: [PATCH 01/41] NonLinearProgram --- src/NonLinearProgram/NonLinearProgram.jl | 499 +++++++++++++++++++++++ src/NonLinearProgram/nlp_utilities.jl | 431 ++++++++++++++++++++ 2 files changed, 930 insertions(+) create mode 100644 src/NonLinearProgram/NonLinearProgram.jl create mode 100644 src/NonLinearProgram/nlp_utilities.jl diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl new file mode 100644 index 00000000..aae15f01 --- /dev/null +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -0,0 +1,499 @@ +# Copyright (c) 2020: Andrew Rosemberg and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +# NonLinearProgram.jl +module NonLinearProgram + +import DiffOpt +import JuMP +import MathOptInterface as MOI +using SparseArrays + +include("nlp_utilities.jl") + +Base.@kwdef struct Cache + primal_vars::Vector{Float64} + params::Vector{Float64} + evaluator + cons +end + +Base.@kwdef struct ForwCache + Δs::Matrix{Float64} + sp::Vector{Float64} +end + +Base.@kwdef struct ReverseCache + Δs_T::Matrix{Float64} + sp_T::Vector{Float64} +end + +""" + DiffOpt.NonLinearProgram.Model <: DiffOpt.AbstractModel + +Model to differentiate nonlinear programs. + +The forward differentiation computes the Jacobian product for selected variables +with respect to specified parameters. + +The reverse differentiation computes the Jacobian transpose product for dual and primal +variables with respect to the parameters. + +# Key Components +- Forward differentiation: Partial derivatives of variables of interest. +- Reverse differentiation: Transpose Jacobian computation. + +""" +mutable struct Model <: DiffOpt.AbstractModel + model::JuMP.Model # JuMP optimization model + cache::Union{Nothing, Cache} # Caches to hold evaluator and constraints + forw_grad_cache::Union{Nothing, ForwCache} # Cache for forward sensitivity results + back_grad_cache::Union{Nothing, ReverseCache} # Cache for reverse sensitivity results + diff_time::Float64 +end + +function Model(model::JuMP.Model) + return Model( + model, + nothing, + nothing, + nothing, + NaN, + ) +end + +function MOI.is_empty(model::Model) + return model.cache === nothing +end + +function MOI.empty!(model::Model) + model.cache = nothing + model.forw_grad_cache = nothing + model.back_grad_cache = nothing + model.diff_time = NaN + return +end + +MOI.get(model::Model, ::DiffOpt.DifferentiateTimeSec) = model.diff_time + +function _cache_evaluator!(model::Model, primal_vars, params) + if model.cache !== nothing + return model.cache + end + evaluator, cons = create_evaluator(model.model; x=[primal_vars; params]) + model.cache = Cache(primal_vars=primal_vars, params=params, evaluator=evaluator, cons=cons) + return model.cache +end + +function DiffOpt.forward_differentiate!(model::Model; focus_vars, focus_duals, params) + model.diff_time = @elapsed begin + # Retrieve primal variables and cache the evaluator + primal_vars = all_primal_vars(model.model) + vars_idx = [findall(x -> x == i, primal_vars)[1] for i in focus_vars] + + cache = _cache_evaluator!(model, primal_vars, params) + + dual_index = [findall(x -> x == i, cache.cons)[1] for i in focus_duals] + leq_locations, geq_locations = find_inequealities(cache.cons) + num_ineq = length(leq_locations) + length(geq_locations) + num_primal = length(primal_vars) + + # Compute sensitivities + Δs, sp = compute_sensitivity(cache.evaluator, cache.cons, Δp; primal_vars=primal_vars, params=params) + Δs_focus = Δs[vars_idx, :] + Δs_duals_focus = Δs[num_primal + num_ineq + dual_index, :] + + model.forw_grad_cache = ForwCache(Δs=[Δs_focus; Δs_duals_focus], sp=sp) + end + return nothing +end + +function DiffOpt.reverse_differentiate!(model::Model) + model.diff_time = @elapsed begin + # Not implemented, placeholder for reverse sensitivity logic + # Use transpose Jacobian logic based on evaluator and derivatives + throw(NotImplementedError("Reverse differentiation not yet implemented for NonLinearProgram")) + end + return nothing +end + +function MOI.get( + model::Model, + ::DiffOpt.ForwardVariablePrimal, + vi::MOI.VariableIndex, +) + if model.forw_grad_cache === nothing + error("Forward differentiation has not been performed yet.") + end + Δs = model.forw_grad_cache.Δs + return Δs[vi.value] +end + +end # module NonLinearProgram + + +# module NonLinearProgram + +# using JuMP +# using MathOptInterface +# import MathOptInterface: ConstraintSet, CanonicalConstraintFunction +# using SparseArrays +# using LinearAlgebra +# import JuMP.index +# include("nlp_utilities.jl") + +# Base.@kwdef struct Cache +# K::SparseArrays.SparseMatrixCSC{Float64,Int} +# evaluator::MOI.Nonlinear.Model +# rows::Vector{ConstraintRef} +# end + +# # TODO: What is the forward cache when we want the sentsitivities wrt primal,dual,slack and bound dual variables? +# Base.@kwdef struct ForwCache +# du::Vector{Float64} +# dv::Vector{Float64} +# dw::Vector{Float64} +# end + +# # TODO: What is the reverse cache when we want the sentsitivities wrt primal,dual,slack and bound dual variables? +# Base.@kwdef struct ReverseCache +# g::Vector{Float64} +# πz::Vector{Float64} +# end + +# # TODO: What models are supported? +# function MOI.supports( +# ::MOI.Utilities.GenericModel{T}, +# ::MOI.ObjectiveFunction{F}, +# ) where {T<:MOI.Utilities.ModelLike,F<:MOI.AbstractFunction} +# return F === MOI.ScalarNonlinearFunction{T} +# end + +# """ +# Diffopt.NonLinearProgram.Model <: DiffOpt.AbstractModel + +# Model to differentiate conic programs. + +# The forward differentiation computes the product of the derivative (Jacobian) at +# the non-linear program parameters `p`, to the perturbations `dp`. + +# The reverse differentiation computes the product of the transpose of the +# derivative (Jacobian) at the non-linear program parameters `p`, to the +# perturbations `dx`, `dy`, `ds`, `dv`. + +# For theoretical background, refer to XXX. +# """ +# mutable struct Model <: DiffOpt.AbstractModel +# # storage for problem data in matrix form +# model::Form{Float64} +# # includes maps from matrix indices to problem data held in `optimizer` +# # also includes KKT matrices +# # also includes the solution +# gradient_cache::Union{Nothing,Cache} + +# # caches for sensitivity output +# # result from solving KKT/residualmap linear systems +# # this allows keeping the same `gradient_cache` +# # if only sensitivy input changes +# forw_grad_cache::Union{Nothing,ForwCache} +# back_grad_cache::Union{Nothing,ReverseCache} + +# # sensitivity input cache using MOI like sparse format +# input_cache::DiffOpt.InputCache + +# x::Vector{Float64} # Primal +# s::Vector{Float64} # Slack +# y::Vector{Float64} # Dual +# v::Vector{Float64} # Bound Dual +# diff_time::Float64 +# end + +# function Model() +# return Model( +# Form{Float64}(), +# nothing, +# nothing, +# nothing, +# DiffOpt.InputCache(), +# Float64[], +# Float64[], +# Float64[], +# Float64[], +# NaN, +# ) +# end + +# function MOI.is_empty(model::Model) +# return MOI.is_empty(model.model) +# end + +# function MOI.empty!(model::Model) +# MOI.empty!(model.model) +# model.gradient_cache = nothing +# model.forw_grad_cache = nothing +# model.back_grad_cache = nothing +# empty!(model.input_cache) +# empty!(model.x) +# empty!(model.s) +# empty!(model.y) +# model.diff_time = NaN +# return +# end + +# MOI.get(model::Model, ::DiffOpt.DifferentiateTimeSec) = model.diff_time + +# # TODO: what constraints are supported? +# function MOI.supports_constraint( +# model::Model, +# F::Type{MOI.VectorAffineFunction{Float64}}, +# ::Type{S}, +# ) where {S<:MOI.AbstractVectorSet} +# if DiffOpt.add_set_types(model.model.constraints.sets, S) +# push!(model.model.constraints.caches, Tuple{F,S}[]) +# push!(model.model.constraints.are_indices_mapped, BitSet()) +# end +# return MOI.supports_constraint(model.model, F, S) +# end + +# # TODO: what is this? +# function MOI.set( +# model::Model, +# ::MOI.ConstraintPrimalStart, +# ci::MOI.ConstraintIndex, +# value, +# ) +# MOI.throw_if_not_valid(model, ci) +# return DiffOpt._enlarge_set( +# model.s, +# MOI.Utilities.rows(model.model.constraints, ci), +# value, +# ) +# end + +# # TODO: what is this? +# function MOI.set( +# model::Model, +# ::MOI.ConstraintDualStart, +# ci::MOI.ConstraintIndex, +# value, +# ) +# MOI.throw_if_not_valid(model, ci) +# return DiffOpt._enlarge_set( +# model.y, +# MOI.Utilities.rows(model.model.constraints, ci), +# value, +# ) +# end + +# function _gradient_cache(model::Model) +# if model.gradient_cache !== nothing +# return model.gradient_cache +# end + +# evaluator, cons = create_evaluator(model; x=[primal_vars; params]) + +# model.gradient_cache = +# Cache(; M = M, vp = vp, Dπv = Dπv, A = A, b = b, c = c) + +# return model.gradient_cache +# end + +# function DiffOpt.forward_differentiate!(model::Model) +# model.diff_time = @elapsed begin +# gradient_cache = _gradient_cache(model) +# M = gradient_cache.M +# vp = gradient_cache.vp +# Dπv = gradient_cache.Dπv +# x = model.x +# y = model.y +# s = model.s +# A = gradient_cache.A +# b = gradient_cache.b +# c = gradient_cache.c + +# objective_function = DiffOpt._convert( +# MOI.ScalarAffineFunction{Float64}, +# model.input_cache.objective, +# ) +# sparse_array_obj = DiffOpt.sparse_array_representation( +# objective_function, +# length(c), +# ) +# dc = sparse_array_obj.terms + +# db = zeros(length(b)) +# DiffOpt._fill( +# S -> false, +# gradient_cache, +# model.input_cache, +# model.model.constraints.sets, +# db, +# ) +# (lines, cols) = size(A) +# nz = SparseArrays.nnz(A) +# dAi = zeros(Int, 0) +# dAj = zeros(Int, 0) +# dAv = zeros(Float64, 0) +# sizehint!(dAi, nz) +# sizehint!(dAj, nz) +# sizehint!(dAv, nz) +# DiffOpt._fill( +# S -> false, +# gradient_cache, +# model.input_cache, +# model.model.constraints.sets, +# dAi, +# dAj, +# dAv, +# ) +# dA = SparseArrays.sparse(dAi, dAj, dAv, lines, cols) + +# m = size(A, 1) +# n = size(A, 2) +# N = m + n + 1 +# # NOTE: w = 1 systematically since we asserted the primal-dual pair is optimal +# (u, v, w) = (x, y - s, 1.0) + +# # g = dQ * Π(z/|w|) = dQ * [u, vp, 1.0] +# RHS = [ +# dA' * vp + dc +# -dA * u + db +# -LinearAlgebra.dot(dc, u) - LinearAlgebra.dot(db, vp) +# ] + +# dz = if LinearAlgebra.norm(RHS) <= 1e-400 # TODO: parametrize or remove +# RHS .= 0 # because M is square +# else +# IterativeSolvers.lsqr(M, RHS) +# end + +# du, dv, dw = dz[1:n], dz[n+1:n+m], dz[n+m+1] +# model.forw_grad_cache = ForwCache(du, dv, [dw]) +# end +# return nothing +# # dx = du - x * dw +# # dy = Dπv * dv - y * dw +# # ds = Dπv * dv - dv - s * dw +# # return -dx, -dy, -ds +# end + +# function DiffOpt.reverse_differentiate!(model::Model) +# model.diff_time = @elapsed begin +# gradient_cache = _gradient_cache(model) +# M = gradient_cache.M +# vp = gradient_cache.vp +# Dπv = gradient_cache.Dπv +# x = model.x +# y = model.y +# s = model.s +# A = gradient_cache.A +# b = gradient_cache.b +# c = gradient_cache.c + +# dx = zeros(length(c)) +# for (vi, value) in model.input_cache.dx +# dx[vi.value] = value +# end +# dy = zeros(length(b)) +# ds = zeros(length(b)) + +# m = size(A, 1) +# n = size(A, 2) +# N = m + n + 1 +# # NOTE: w = 1 systematically since we asserted the primal-dual pair is optimal +# (u, v, w) = (x, y - s, 1.0) + +# # dz = D \phi (z)^T (dx,dy,dz) +# dz = [ +# dx +# Dπv' * (dy + ds) - ds +# -x' * dx - y' * dy - s' * ds +# ] + +# g = if LinearAlgebra.norm(dz) <= 1e-4 # TODO: parametrize or remove +# dz .= 0 # because M is square +# else +# IterativeSolvers.lsqr(M, dz) +# end + +# πz = [ +# u +# vp +# 1.0 +# ] + +# # TODO: very important +# # contrast with: +# # http://reports-archive.adm.cs.cmu.edu/anon/2019/CMU-CS-19-109.pdf +# # pg 97, cap 7.4.2 + +# model.back_grad_cache = ReverseCache(g, πz) +# end +# return nothing +# # dQ = - g * πz' +# # dA = - dQ[1:n, n+1:n+m]' + dQ[n+1:n+m, 1:n] +# # db = - dQ[n+1:n+m, end] + dQ[end, n+1:n+m]' +# # dc = - dQ[1:n, end] + dQ[end, 1:n]' +# # return dA, db, dc +# end + +# function MOI.get(model::Model, ::DiffOpt.ReverseObjectiveFunction) +# g = model.back_grad_cache.g +# πz = model.back_grad_cache.πz +# dc = DiffOpt.lazy_combination(-, πz, g, length(g), eachindex(model.x)) +# return DiffOpt.VectorScalarAffineFunction(dc, 0.0) +# end + +# function MOI.get( +# model::Model, +# ::DiffOpt.ForwardVariablePrimal, +# vi::MOI.VariableIndex, +# ) +# i = vi.value +# du = model.forw_grad_cache.du +# dw = model.forw_grad_cache.dw +# return -(du[i] - model.x[i] * dw[]) +# end + +# function DiffOpt._get_db( +# model::Model, +# ci::MOI.ConstraintIndex{F,S}, +# ) where {F<:MOI.AbstractVectorFunction,S} +# i = MOI.Utilities.rows(model.model.constraints, ci) # vector +# # i = ci.value +# n = length(model.x) # columns in A +# # Since `b` in https://arxiv.org/pdf/1904.09043.pdf is the constant in the right-hand side and +# # `b` in MOI is the constant on the left-hand side, we have the opposite sign here +# # db = - dQ[n+1:n+m, end] + dQ[end, n+1:n+m]' +# g = model.back_grad_cache.g +# πz = model.back_grad_cache.πz +# # `g[end] * πz[n .+ i] - πz[end] * g[n .+ i]` +# return DiffOpt.lazy_combination(-, πz, g, length(g), n .+ i) +# end + +# function DiffOpt._get_dA( +# model::Model, +# ci::MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, +# ) +# i = MOI.Utilities.rows(model.model.constraints, ci) # vector +# # i = ci.value +# n = length(model.x) # columns in A +# m = length(model.y) # lines in A +# # dA = - dQ[1:n, n+1:n+m]' + dQ[n+1:n+m, 1:n] +# g = model.back_grad_cache.g +# πz = model.back_grad_cache.πz +# #return DiffOpt.lazy_combination(-, g, πz, n .+ i, 1:n) +# return g[n.+i] * πz[1:n]' - πz[n.+i] * g[1:n]' +# end + +# function MOI.get( +# model::Model, +# attr::MOI.ConstraintFunction, +# ci::MOI.ConstraintIndex, +# ) +# return MOI.get(model.model, attr, ci) +# end + +# end diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl new file mode 100644 index 00000000..bb8b8077 --- /dev/null +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -0,0 +1,431 @@ +""" + create_nlp_model(model::JuMP.Model) + +Create a Nonlinear Programming (NLP) model from a JuMP model. +""" +function create_nlp_model(model::JuMP.Model) + rows = Vector{ConstraintRef}(undef, 0) + nlp = MOI.Nonlinear.Model() + for (F, S) in list_of_constraint_types(model) + if F <: VariableRef && !(S <: MathOptInterface.EqualTo{Float64}) + continue # Skip variable bounds + end + for ci in all_constraints(model, F, S) + push!(rows, ci) + object = constraint_object(ci) + MOI.Nonlinear.add_constraint(nlp, object.func, object.set) + end + end + MOI.Nonlinear.set_objective(nlp, objective_function(model)) + return nlp, rows +end + +""" + fill_off_diagonal(H) + +Filling the off-diagonal elements of a sparse matrix to make it symmetric. +""" +function fill_off_diagonal(H) + ret = H + H' + row_vals = SparseArrays.rowvals(ret) + non_zeros = SparseArrays.nonzeros(ret) + for col in 1:size(ret, 2) + for i in SparseArrays.nzrange(ret, col) + if col == row_vals[i] + non_zeros[i] /= 2 + end + end + end + return ret +end + +sense_mult(x) = JuMP.objective_sense(owner_model(x)) == MOI.MIN_SENSE ? 1.0 : -1.0 +sense_mult(x::Vector) = sense_mult(x[1]) + +""" + compute_optimal_hessian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{ConstraintRef}, x::Vector{VariableRef}) + +Compute the optimal Hessian of the Lagrangian. +""" +function compute_optimal_hessian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{ConstraintRef}, x::Vector{VariableRef}) + sense_multiplier = objective_sense(owner_model(x[1])) == MOI.MIN_SENSE ? 1.0 : -1.0 + hessian_sparsity = MOI.hessian_lagrangian_structure(evaluator) + I = [i for (i, _) in hessian_sparsity] + J = [j for (_, j) in hessian_sparsity] + V = zeros(length(hessian_sparsity)) + # The signals are being sdjusted to match the Ipopt convention (inner.mult_g) + # but we don't know if we need to adjust the objective function multiplier + MOI.eval_hessian_lagrangian(evaluator, V, value.(x), 1.0, - sense_multiplier * dual.(rows)) + H = SparseArrays.sparse(I, J, V, length(x), length(x)) + return fill_off_diagonal(H) +end + +""" + compute_optimal_jacobian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{ConstraintRef}, x::Vector{VariableRef}) + +Compute the optimal Jacobian of the constraints. +""" +function compute_optimal_jacobian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{ConstraintRef}, x::Vector{VariableRef}) + jacobian_sparsity = MOI.jacobian_structure(evaluator) + I = [i for (i, _) in jacobian_sparsity] + J = [j for (_, j) in jacobian_sparsity] + V = zeros(length(jacobian_sparsity)) + MOI.eval_constraint_jacobian(evaluator, V, value.(x)) + A = SparseArrays.sparse(I, J, V, length(rows), length(x)) + return A +end + +""" + compute_optimal_hess_jac(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{ConstraintRef}, x::Vector{VariableRef}) + +Compute the optimal Hessian of the Lagrangian and Jacobian of the constraints. +""" +function compute_optimal_hess_jac(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{ConstraintRef}, x::Vector{VariableRef}) + hessian = compute_optimal_hessian(evaluator, rows, x) + jacobian = compute_optimal_jacobian(evaluator, rows, x) + + return hessian, jacobian +end + +""" + all_primal_vars(model::Model) + +Get all the primal variables in the model. +""" +all_primal_vars(model::Model) = filter(x -> !is_parameter(x), all_variables(model)) + +""" + all_params(model::Model) + +Get all the parameters in the model. +""" +all_params(model::Model) = filter(x -> is_parameter(x), all_variables(model)) + +""" + create_evaluator(model::Model; x=all_variables(model)) + +Create an evaluator for the model. +""" +JuMP.index(x::JuMP.Containers.DenseAxisArray) = index.(x).data + +function create_evaluator(model::Model; x=all_variables(model)) + nlp, rows = create_nlp_model(model) + backend = MOI.Nonlinear.SparseReverseMode() + evaluator = MOI.Nonlinear.Evaluator(nlp, backend, vcat(index.(x)...)) + MOI.initialize(evaluator, [:Hess, :Jac]) + return evaluator, rows +end + + +function is_less_inequality(con::ConstraintRef) + set_type = typeof(MOI.get(owner_model(con), MOI.ConstraintSet(), con)) + return set_type <: MOI.LessThan +end + +function is_greater_inequality(con::ConstraintRef) + set_type = typeof(MOI.get(owner_model(con), MOI.ConstraintSet(), con)) + return set_type <: MOI.GreaterThan +end + +""" + find_inequealities(cons::Vector{ConstraintRef}) + +Find the indices of the inequality constraints. +""" +function find_inequealities(cons::Vector{C}) where C<:ConstraintRef + leq_locations = zeros(length(cons)) + geq_locations = zeros(length(cons)) + for i in 1:length(cons) + if is_less_inequality(cons[i]) + leq_locations[i] = true + end + if is_greater_inequality(cons[i]) + geq_locations[i] = true + end + end + return findall(x -> x ==1, leq_locations), findall(x -> x ==1, geq_locations) +end + +""" + get_slack_inequality(con::ConstraintRef) + +Get the reference to the canonical function that is equivalent to the slack variable of the inequality constraint. +""" +function get_slack_inequality(con::ConstraintRef) + set_type = typeof(MOI.get(owner_model(con), MOI.ConstraintSet(), con)) + obj = constraint_object(con) + if set_type <: MOI.LessThan + # c(x) <= b --> slack = c(x) - b | slack <= 0 + return obj.func - obj.set.upper + end + # c(x) >= b --> slack = c(x) - b | slack >= 0 + return obj.func - obj.set.lower +end + +""" + compute_solution_and_bounds(primal_vars::Vector{VariableRef}, cons::Vector{C}) where C<:ConstraintRef + +Compute the solution and bounds of the primal variables. +""" +function compute_solution_and_bounds(primal_vars::Vector{VariableRef}, cons::Vector{C}) where {C<:ConstraintRef} + sense_multiplier = sense_mult(primal_vars) + num_vars = length(primal_vars) + leq_locations, geq_locations = find_inequealities(cons) + ineq_locations = vcat(geq_locations, leq_locations) + num_leq = length(leq_locations) + num_geq = length(geq_locations) + num_ineq = num_leq + num_geq + slack_vars = [get_slack_inequality(cons[i]) for i in ineq_locations] + has_up = findall(x -> has_upper_bound(x), primal_vars) + has_low = findall(x -> has_lower_bound(x), primal_vars) + + # Primal solution + X = value.([primal_vars; slack_vars]) + + # value and dual of the lower bounds + V_L = spzeros(num_vars+num_ineq) + X_L = spzeros(num_vars+num_ineq) + for (i, j) in enumerate(has_low) + V_L[i] = dual.(LowerBoundRef(primal_vars[j])) * sense_multiplier + # + if sense_multiplier == 1.0 + V_L[i] <= -1e-6 && @info "Dual of lower bound must be positive" i V_L[i] + else + V_L[i] >= 1e-6 && @info "Dual of lower bound must be negative" i V_L[i] + end + # + X_L[i] = JuMP.lower_bound(primal_vars[j]) + end + for (i, con) in enumerate(cons[geq_locations]) + # By convention jump dual will allways be positive for geq constraints + # but for ipopt it will be positive if min problem and negative if max problem + V_L[num_vars+i] = dual.(con) * (sense_multiplier) + # + if sense_multiplier == 1.0 + V_L[num_vars+i] <= -1e-6 && @info "Dual of geq constraint must be positive" i V_L[num_vars+i] + else + V_L[num_vars+i] >= 1e-6 && @info "Dual of geq constraint must be negative" i V_L[num_vars+i] + end + end + # value and dual of the upper bounds + V_U = spzeros(num_vars+num_ineq) + X_U = spzeros(num_vars+num_ineq) + for (i, j) in enumerate(has_up) + V_U[i] = dual.(UpperBoundRef(primal_vars[j])) * (- sense_multiplier) + # + if sense_multiplier == 1.0 + V_U[i] <= -1e-6 && @info "Dual of upper bound must be positive" i V_U[i] + else + V_U[i] >= 1e-6 && @info "Dual of upper bound must be negative" i V_U[i] + end + # + X_U[i] = JuMP.upper_bound(primal_vars[j]) + end + for (i, con) in enumerate(cons[leq_locations]) + # By convention jump dual will allways be negative for leq constraints + # but for ipopt it will be positive if min problem and negative if max problem + V_U[num_vars+i] = dual.(con) * (- sense_multiplier) + # + if sense_multiplier == 1.0 + V_U[num_vars+i] <= -1e-6 && @info "Dual of leq constraint must be positive" i V_U[num_vars+i] + else + V_U[num_vars+i] >= 1e-6 && @info "Dual of leq constraint must be negative" i V_U[num_vars+i] + end + end + return X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, vcat(has_up, collect(num_vars+num_geq+1:num_vars+num_geq+num_leq)), vcat(has_low, collect(num_vars+1:num_vars+num_geq)) +end + +""" + build_M_N(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{ConstraintRef}, + primal_vars::Vector{VariableRef}, params::Vector{VariableRef}, + _X::Vector, _V_L::Vector, _X_L::Vector, _V_U::Vector, _X_U::Vector, ineq_locations::Vector{Z}, + has_up::Vector{Z}, has_low::Vector{Z} +) where {Z<:Integer} + +Build the M (KKT Jacobian w.r.t. solution) and N (KKT Jacobian w.r.t. parameters) matrices for the sensitivity analysis. +""" +function build_M_N(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{ConstraintRef}, + primal_vars::Vector{VariableRef}, params::Vector{VariableRef}, + _X::AbstractVector, _V_L::AbstractVector, _X_L::AbstractVector, _V_U::AbstractVector, _X_U::AbstractVector, leq_locations::Vector{Z}, geq_locations::Vector{Z}, ineq_locations::Vector{Z}, + has_up::Vector{Z}, has_low::Vector{Z} +) where {Z<:Integer} + @assert all(x -> is_parameter(x), params) "All parameters must be parameters" + + # Setting + num_vars = length(primal_vars) + num_parms = length(params) + num_cons = length(cons) + num_ineq = length(ineq_locations) + all_vars = [primal_vars; params] + num_low = length(has_low) + num_up = length(has_up) + + # Primal solution + X_lb = spzeros(num_low, num_low) + X_ub = spzeros(num_up, num_up) + V_L = spzeros(num_low, num_vars + num_ineq) + V_U = spzeros(num_up, num_vars + num_ineq) + I_L = spzeros(num_vars + num_ineq, num_low) + I_U = spzeros(num_vars + num_ineq, num_up) + + # value and dual of the lower bounds + for (i, j) in enumerate(has_low) + V_L[i, j] = _V_L[j] + X_lb[i, i] = _X[j] - _X_L[j] + I_L[j, i] = -1 + end + # value and dual of the upper bounds + for (i, j) in enumerate(has_up) + V_U[i, j] = _V_U[j] + X_ub[i, i] = _X_U[j] - _X[j] + I_U[j, i] = 1 + end + + # Function Derivatives + hessian, jacobian = compute_optimal_hess_jac(evaluator, cons, all_vars) + + # Hessian of the lagrangian wrt the primal variables + W = spzeros(num_vars + num_ineq, num_vars + num_ineq) + W[1:num_vars, 1:num_vars] = hessian[1:num_vars, 1:num_vars] + # Jacobian of the constraints + A = spzeros(num_cons, num_vars + num_ineq) + # A is the Jacobian of: c(x) = b and c(x) <= b and c(x) >= b, possibly all mixed up. + # Each of the will be re-written as: + # c(x) - b = 0 + # c(x) - b - su = 0, su <= 0 + # c(x) - b - sl = 0, sl >= 0 + # Jacobian of the constraints wrt the primal variables + A[:, 1:num_vars] = jacobian[:, 1:num_vars] + # Jacobian of the constraints wrt the slack variables + for (i,j) in enumerate(geq_locations) + A[j, num_vars+i] = -1 + end + for (i,j) in enumerate(leq_locations) + A[j, num_vars+i] = -1 + end + # Partial second derivative of the lagrangian wrt primal solution and parameters + ∇ₓₚL = spzeros(num_vars + num_ineq, num_parms) + ∇ₓₚL[1:num_vars, :] = hessian[1:num_vars, num_vars+1:end] + # Partial derivative of the equality constraintswith wrt parameters + ∇ₚC = jacobian[:, num_vars+1:end] + + # M matrix + # M = [ + # [W A' -I I]; + # [A 0 0 0]; + # [V_L 0 (X - X_L) 0] + # [V_U 0 0 0 (X_U - X)] + # ] + len_w = num_vars + num_ineq + M = spzeros(len_w + num_cons + num_low + num_up, len_w + num_cons + num_low + num_up) + + M[1:len_w, 1:len_w] = W + M[1:len_w, len_w + 1 : len_w + num_cons] = A' + M[len_w+1:len_w+num_cons, 1:len_w] = A + M[1:len_w, len_w+num_cons+1:len_w+num_cons+num_low] = I_L + M[len_w+num_cons+1:len_w+num_cons+num_low, 1:len_w] = V_L + M[len_w+num_cons+1:len_w+num_cons+num_low, len_w+num_cons+1:len_w+num_cons+num_low] = X_lb + M[len_w+num_cons+num_low+1:len_w+num_cons+num_low+num_up, 1:len_w] = V_U + M[len_w+num_cons+num_low+1:len_w+num_cons+num_low+num_up, len_w+num_cons+num_low+1:len_w+num_cons+num_low+num_up] = X_ub + M[1:len_w, len_w+num_cons+num_low+1:end] = I_U + + # N matrix + # N = [∇ₓₚL ; ∇ₚC; zeros(num_low + num_up, num_parms)] + N = spzeros(len_w + num_cons + num_low + num_up, num_parms) + N[1:len_w, :] = ∇ₓₚL + N[len_w+1:len_w+num_cons, :] = ∇ₚC + + return M, N +end + +function inertia_corrector_factorization(M::SparseMatrixCSC, num_w, num_cons; st=1e-6, max_corrections=50) + # Factorization + K = lu(M; check=false) + # Inertia correction + status = K.status + num_c = 0 + diag_mat = ones(size(M, 1)) + diag_mat[num_w+1:num_w+num_cons] .= -1 + diag_mat = sparse(diagm(diag_mat)) + while status == 1 && num_c < max_corrections + println("Inertia correction") + M = M + st * diag_mat + K = lu(M; check=false) + status = K.status + num_c += 1 + end + if status != 0 + @warn "Inertia correction failed" + return nothing + end + return K +end + +function inertia_corrector_factorization(M; st=1e-6, max_corrections=50) + num_c = 0 + if cond(M) > 1/st + @warn "Inertia correction" + M = M + st * I(size(M, 1)) + num_c += 1 + end + while cond(M) > 1/st && num_c < max_corrections + M = M + st * I(size(M, 1)) + num_c += 1 + end + if num_c == max_corrections + @warn "Inertia correction failed" + return nothing + end + return lu(M) +end + +""" + compute_derivatives_no_relax(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{ConstraintRef}, + primal_vars::Vector{VariableRef}, params::Vector{VariableRef}, + _X::Vector, _V_L::Vector, _X_L::Vector, _V_U::Vector, _X_U::Vector, ineq_locations::Vector{Z}, + has_up::Vector{Z}, has_low::Vector{Z} + ) where {Z<:Integer} + +Compute the derivatives of the solution w.r.t. the parameters without accounting for active set changes. +""" +function compute_derivatives_no_relax(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{ConstraintRef}, + primal_vars::Vector{VariableRef}, params::Vector{VariableRef}, + _X::AbstractVector, _V_L::AbstractVector, _X_L::AbstractVector, _V_U::AbstractVector, _X_U::AbstractVector, leq_locations::Vector{Z}, geq_locations::Vector{Z}, ineq_locations::Vector{Z}, + has_up::Vector{Z}, has_low::Vector{Z} +) where {Z<:Integer} + num_bounds = length(has_up) + length(has_low) + M, N = build_M_N(evaluator, cons, primal_vars, params, _X, _V_L, _X_L, _V_U, _X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low) + + # Sesitivity of the solution (primal-dual_constraints-dual_bounds) w.r.t. the parameters + K = inertia_corrector_factorization(M, length(primal_vars) + length(ineq_locations), length(cons)) # Factorization + if isnothing(K) + return zeros(size(M, 1), size(N, 2)), K, N + end + ∂s = zeros(size(M, 1), size(N, 2)) + # ∂s = - (K \ N) # Sensitivity + ldiv!(∂s, K, N) + ∂s = - ∂s + + return ∂s, K, N +end + +""" + compute_sensitivity(model::Model; primal_vars=all_primal_vars(model), params=all_params(model)) + +Compute the sensitivity of the solution given sensitivity of the parameters (Δp). +""" +function compute_sensitivity(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{ConstraintRef}; primal_vars=all_primal_vars(model), params=all_params(model), tol=1e-6 +) + ismin = sense_mult(primal_vars) == 1.0 + sense_multiplier = sense_mult(primal_vars) + num_cons = length(cons) + num_var = length(primal_vars) + # Solution and bounds + X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low = compute_solution_and_bounds(primal_vars, cons) + # Compute derivatives + num_w = length(X) + num_lower = length(has_low) + # ∂s = [∂x; ∂λ; ∂ν_L; ∂ν_U] + ∂s, K, N = compute_derivatives_no_relax(evaluator, cons, primal_vars, params, X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low) + return ∂s +end From 4b16e13e7e8e240d45f7b069533f864aeb6382f6 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 9 Dec 2024 18:02:47 -0500 Subject: [PATCH 02/41] index by MOI index --- src/NonLinearProgram/NonLinearProgram.jl | 513 ++++------------------- 1 file changed, 93 insertions(+), 420 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index aae15f01..93eab420 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -1,9 +1,3 @@ -# Copyright (c) 2020: Andrew Rosemberg and contributors -# -# Use of this source code is governed by an MIT-style license that can be found -# in the LICENSE.md file or at https://opensource.org/licenses/MIT. - -# NonLinearProgram.jl module NonLinearProgram import DiffOpt @@ -14,20 +8,22 @@ using SparseArrays include("nlp_utilities.jl") Base.@kwdef struct Cache - primal_vars::Vector{Float64} - params::Vector{Float64} - evaluator - cons + primal_vars::Vector{JuMP.VariableRef} # Sorted primal variables + dual_mapping::Dict{MOI.ConstraintIndex, Int} # Unified mapping for constraints and bounds + params::Vector{JuMP.VariableRef} # VariableRefs for parameters + index_duals::Vector{Int} # Indices for dual variables + evaluator # Cached evaluator for derivative computation + cons # Cached constraints from evaluator end Base.@kwdef struct ForwCache - Δs::Matrix{Float64} - sp::Vector{Float64} + primal_Δs::Matrix{Float64} # Sensitivity for primal variables (excluding slacks) + dual_Δs::Matrix{Float64} # Sensitivity for constraints and bounds (indexed by ConstraintIndex) end Base.@kwdef struct ReverseCache - Δs_T::Matrix{Float64} - sp_T::Vector{Float64} + primal_Δs_T::Matrix{Float64} + dual_Δs_T::Matrix{Float64} end """ @@ -35,20 +31,12 @@ end Model to differentiate nonlinear programs. -The forward differentiation computes the Jacobian product for selected variables -with respect to specified parameters. - -The reverse differentiation computes the Jacobian transpose product for dual and primal -variables with respect to the parameters. - -# Key Components -- Forward differentiation: Partial derivatives of variables of interest. -- Reverse differentiation: Transpose Jacobian computation. - +Supports forward and reverse differentiation, caching sensitivity data +for primal variables, constraints, and bounds, excluding slack variables. """ mutable struct Model <: DiffOpt.AbstractModel model::JuMP.Model # JuMP optimization model - cache::Union{Nothing, Cache} # Caches to hold evaluator and constraints + cache::Union{Nothing, Cache} # Cache for evaluator and mappings forw_grad_cache::Union{Nothing, ForwCache} # Cache for forward sensitivity results back_grad_cache::Union{Nothing, ReverseCache} # Cache for reverse sensitivity results diff_time::Float64 @@ -76,45 +64,79 @@ function MOI.empty!(model::Model) return end -MOI.get(model::Model, ::DiffOpt.DifferentiateTimeSec) = model.diff_time - -function _cache_evaluator!(model::Model, primal_vars, params) - if model.cache !== nothing - return model.cache +function _cache_evaluator!(model::Model; params=nothing) + if params !== nothing || model.cache === nothing + # Retrieve and sort primal variables by index + primal_vars = sort(all_primal_vars(model.model), by=x -> x.index.value) + num_primal = length(primal_vars) + params = params === nothing ? sort(all_params(model.model), by=x -> x.index.value) : params + + # Create evaluator and constraints + evaluator, cons = create_evaluator(model.model; x=[primal_vars; params]) + num_constraints = length(cons) + # Analyze constraints and bounds + leq_locations, geq_locations = find_inequealities(cons) + num_leq = length(leq_locations) + num_geq = length(geq_locations) + has_up = findall(x -> has_upper_bound(x), primal_vars) + has_low = findall(x -> has_lower_bound(x), primal_vars) + num_low = length(has_low) + num_up = length(has_up) + + # Create unified dual mapping + dual_mapping = Vector{Int}(undef, num_constraints + num_low + num_up) + for (i, ci) in enumerate(cons) + dual_mapping[ci.index.value] = i + end + + # Add bounds to dual mapping + for (i, var_idx) in enumerate(has_low) + lb = MOI.LowerBoundRef(primal_vars[var_idx]) + dual_mapping[lb.index] = num_constraints + i + end + offset += num_low + for (i, var_idx) in enumerate(has_up) + ub = MOI.UpperBoundRef(primal_vars[var_idx]) + dual_mapping[ub.index] = offset + i + end + + num_slacks = num_leq + num_geq + num_w = num_primal + num_slacks + index_duals = [num_w+1:num_w+num_constraints; num_w+num_constraints+1:num_w+num_constraints+num_low; num_w+num_constraints+num_low+num_geq+1:num_w+num_constraints+num_low+num_geq+num_up] + + model.cache = Cache( + primal_vars=primal_vars, + dual_mapping=dual_mapping, + params=params, + index_duals=index_duals, + evaluator=evaluator, + cons=cons, + ) end - evaluator, cons = create_evaluator(model.model; x=[primal_vars; params]) - model.cache = Cache(primal_vars=primal_vars, params=params, evaluator=evaluator, cons=cons) return model.cache end -function DiffOpt.forward_differentiate!(model::Model; focus_vars, focus_duals, params) +function DiffOpt.forward_differentiate!(model::Model; params=nothing) model.diff_time = @elapsed begin - # Retrieve primal variables and cache the evaluator - primal_vars = all_primal_vars(model.model) - vars_idx = [findall(x -> x == i, primal_vars)[1] for i in focus_vars] - - cache = _cache_evaluator!(model, primal_vars, params) - - dual_index = [findall(x -> x == i, cache.cons)[1] for i in focus_duals] - leq_locations, geq_locations = find_inequealities(cache.cons) - num_ineq = length(leq_locations) + length(geq_locations) - num_primal = length(primal_vars) - + cache = _cache_evaluator!(model; params=params) + # Compute sensitivities - Δs, sp = compute_sensitivity(cache.evaluator, cache.cons, Δp; primal_vars=primal_vars, params=params) - Δs_focus = Δs[vars_idx, :] - Δs_duals_focus = Δs[num_primal + num_ineq + dual_index, :] - - model.forw_grad_cache = ForwCache(Δs=[Δs_focus; Δs_duals_focus], sp=sp) + Δs = compute_sensitivity(cache.evaluator, cache.cons; primal_vars=cache.primal_vars, params=cache.params) + + primal_Δs = Δs[1:cache.num_primal, :] # Exclude slacks + dual_Δs = Δs[cache.index_duals, :] # Includes constraints and bounds + + model.forw_grad_cache = ForwCache( + primal_Δs=primal_Δs, + dual_Δs=dual_Δs, + ) end return nothing end -function DiffOpt.reverse_differentiate!(model::Model) +function DiffOpt.reverse_differentiate!(model::Model; params=nothing) model.diff_time = @elapsed begin - # Not implemented, placeholder for reverse sensitivity logic - # Use transpose Jacobian logic based on evaluator and derivatives - throw(NotImplementedError("Reverse differentiation not yet implemented for NonLinearProgram")) + # TODO: Implement reverse differentiation end return nothing end @@ -127,373 +149,24 @@ function MOI.get( if model.forw_grad_cache === nothing error("Forward differentiation has not been performed yet.") end - Δs = model.forw_grad_cache.Δs - return Δs[vi.value] + idx = vi.value # Direct mapping via sorted primal variables + return model.forw_grad_cache.primal_Δs[idx, :] end -end # module NonLinearProgram - - -# module NonLinearProgram - -# using JuMP -# using MathOptInterface -# import MathOptInterface: ConstraintSet, CanonicalConstraintFunction -# using SparseArrays -# using LinearAlgebra -# import JuMP.index -# include("nlp_utilities.jl") - -# Base.@kwdef struct Cache -# K::SparseArrays.SparseMatrixCSC{Float64,Int} -# evaluator::MOI.Nonlinear.Model -# rows::Vector{ConstraintRef} -# end - -# # TODO: What is the forward cache when we want the sentsitivities wrt primal,dual,slack and bound dual variables? -# Base.@kwdef struct ForwCache -# du::Vector{Float64} -# dv::Vector{Float64} -# dw::Vector{Float64} -# end - -# # TODO: What is the reverse cache when we want the sentsitivities wrt primal,dual,slack and bound dual variables? -# Base.@kwdef struct ReverseCache -# g::Vector{Float64} -# πz::Vector{Float64} -# end - -# # TODO: What models are supported? -# function MOI.supports( -# ::MOI.Utilities.GenericModel{T}, -# ::MOI.ObjectiveFunction{F}, -# ) where {T<:MOI.Utilities.ModelLike,F<:MOI.AbstractFunction} -# return F === MOI.ScalarNonlinearFunction{T} -# end - -# """ -# Diffopt.NonLinearProgram.Model <: DiffOpt.AbstractModel - -# Model to differentiate conic programs. - -# The forward differentiation computes the product of the derivative (Jacobian) at -# the non-linear program parameters `p`, to the perturbations `dp`. - -# The reverse differentiation computes the product of the transpose of the -# derivative (Jacobian) at the non-linear program parameters `p`, to the -# perturbations `dx`, `dy`, `ds`, `dv`. - -# For theoretical background, refer to XXX. -# """ -# mutable struct Model <: DiffOpt.AbstractModel -# # storage for problem data in matrix form -# model::Form{Float64} -# # includes maps from matrix indices to problem data held in `optimizer` -# # also includes KKT matrices -# # also includes the solution -# gradient_cache::Union{Nothing,Cache} - -# # caches for sensitivity output -# # result from solving KKT/residualmap linear systems -# # this allows keeping the same `gradient_cache` -# # if only sensitivy input changes -# forw_grad_cache::Union{Nothing,ForwCache} -# back_grad_cache::Union{Nothing,ReverseCache} - -# # sensitivity input cache using MOI like sparse format -# input_cache::DiffOpt.InputCache - -# x::Vector{Float64} # Primal -# s::Vector{Float64} # Slack -# y::Vector{Float64} # Dual -# v::Vector{Float64} # Bound Dual -# diff_time::Float64 -# end - -# function Model() -# return Model( -# Form{Float64}(), -# nothing, -# nothing, -# nothing, -# DiffOpt.InputCache(), -# Float64[], -# Float64[], -# Float64[], -# Float64[], -# NaN, -# ) -# end - -# function MOI.is_empty(model::Model) -# return MOI.is_empty(model.model) -# end - -# function MOI.empty!(model::Model) -# MOI.empty!(model.model) -# model.gradient_cache = nothing -# model.forw_grad_cache = nothing -# model.back_grad_cache = nothing -# empty!(model.input_cache) -# empty!(model.x) -# empty!(model.s) -# empty!(model.y) -# model.diff_time = NaN -# return -# end - -# MOI.get(model::Model, ::DiffOpt.DifferentiateTimeSec) = model.diff_time - -# # TODO: what constraints are supported? -# function MOI.supports_constraint( -# model::Model, -# F::Type{MOI.VectorAffineFunction{Float64}}, -# ::Type{S}, -# ) where {S<:MOI.AbstractVectorSet} -# if DiffOpt.add_set_types(model.model.constraints.sets, S) -# push!(model.model.constraints.caches, Tuple{F,S}[]) -# push!(model.model.constraints.are_indices_mapped, BitSet()) -# end -# return MOI.supports_constraint(model.model, F, S) -# end - -# # TODO: what is this? -# function MOI.set( -# model::Model, -# ::MOI.ConstraintPrimalStart, -# ci::MOI.ConstraintIndex, -# value, -# ) -# MOI.throw_if_not_valid(model, ci) -# return DiffOpt._enlarge_set( -# model.s, -# MOI.Utilities.rows(model.model.constraints, ci), -# value, -# ) -# end - -# # TODO: what is this? -# function MOI.set( -# model::Model, -# ::MOI.ConstraintDualStart, -# ci::MOI.ConstraintIndex, -# value, -# ) -# MOI.throw_if_not_valid(model, ci) -# return DiffOpt._enlarge_set( -# model.y, -# MOI.Utilities.rows(model.model.constraints, ci), -# value, -# ) -# end - -# function _gradient_cache(model::Model) -# if model.gradient_cache !== nothing -# return model.gradient_cache -# end - -# evaluator, cons = create_evaluator(model; x=[primal_vars; params]) - -# model.gradient_cache = -# Cache(; M = M, vp = vp, Dπv = Dπv, A = A, b = b, c = c) - -# return model.gradient_cache -# end - -# function DiffOpt.forward_differentiate!(model::Model) -# model.diff_time = @elapsed begin -# gradient_cache = _gradient_cache(model) -# M = gradient_cache.M -# vp = gradient_cache.vp -# Dπv = gradient_cache.Dπv -# x = model.x -# y = model.y -# s = model.s -# A = gradient_cache.A -# b = gradient_cache.b -# c = gradient_cache.c - -# objective_function = DiffOpt._convert( -# MOI.ScalarAffineFunction{Float64}, -# model.input_cache.objective, -# ) -# sparse_array_obj = DiffOpt.sparse_array_representation( -# objective_function, -# length(c), -# ) -# dc = sparse_array_obj.terms - -# db = zeros(length(b)) -# DiffOpt._fill( -# S -> false, -# gradient_cache, -# model.input_cache, -# model.model.constraints.sets, -# db, -# ) -# (lines, cols) = size(A) -# nz = SparseArrays.nnz(A) -# dAi = zeros(Int, 0) -# dAj = zeros(Int, 0) -# dAv = zeros(Float64, 0) -# sizehint!(dAi, nz) -# sizehint!(dAj, nz) -# sizehint!(dAv, nz) -# DiffOpt._fill( -# S -> false, -# gradient_cache, -# model.input_cache, -# model.model.constraints.sets, -# dAi, -# dAj, -# dAv, -# ) -# dA = SparseArrays.sparse(dAi, dAj, dAv, lines, cols) - -# m = size(A, 1) -# n = size(A, 2) -# N = m + n + 1 -# # NOTE: w = 1 systematically since we asserted the primal-dual pair is optimal -# (u, v, w) = (x, y - s, 1.0) - -# # g = dQ * Π(z/|w|) = dQ * [u, vp, 1.0] -# RHS = [ -# dA' * vp + dc -# -dA * u + db -# -LinearAlgebra.dot(dc, u) - LinearAlgebra.dot(db, vp) -# ] - -# dz = if LinearAlgebra.norm(RHS) <= 1e-400 # TODO: parametrize or remove -# RHS .= 0 # because M is square -# else -# IterativeSolvers.lsqr(M, RHS) -# end - -# du, dv, dw = dz[1:n], dz[n+1:n+m], dz[n+m+1] -# model.forw_grad_cache = ForwCache(du, dv, [dw]) -# end -# return nothing -# # dx = du - x * dw -# # dy = Dπv * dv - y * dw -# # ds = Dπv * dv - dv - s * dw -# # return -dx, -dy, -ds -# end - -# function DiffOpt.reverse_differentiate!(model::Model) -# model.diff_time = @elapsed begin -# gradient_cache = _gradient_cache(model) -# M = gradient_cache.M -# vp = gradient_cache.vp -# Dπv = gradient_cache.Dπv -# x = model.x -# y = model.y -# s = model.s -# A = gradient_cache.A -# b = gradient_cache.b -# c = gradient_cache.c - -# dx = zeros(length(c)) -# for (vi, value) in model.input_cache.dx -# dx[vi.value] = value -# end -# dy = zeros(length(b)) -# ds = zeros(length(b)) - -# m = size(A, 1) -# n = size(A, 2) -# N = m + n + 1 -# # NOTE: w = 1 systematically since we asserted the primal-dual pair is optimal -# (u, v, w) = (x, y - s, 1.0) - -# # dz = D \phi (z)^T (dx,dy,dz) -# dz = [ -# dx -# Dπv' * (dy + ds) - ds -# -x' * dx - y' * dy - s' * ds -# ] - -# g = if LinearAlgebra.norm(dz) <= 1e-4 # TODO: parametrize or remove -# dz .= 0 # because M is square -# else -# IterativeSolvers.lsqr(M, dz) -# end - -# πz = [ -# u -# vp -# 1.0 -# ] - -# # TODO: very important -# # contrast with: -# # http://reports-archive.adm.cs.cmu.edu/anon/2019/CMU-CS-19-109.pdf -# # pg 97, cap 7.4.2 - -# model.back_grad_cache = ReverseCache(g, πz) -# end -# return nothing -# # dQ = - g * πz' -# # dA = - dQ[1:n, n+1:n+m]' + dQ[n+1:n+m, 1:n] -# # db = - dQ[n+1:n+m, end] + dQ[end, n+1:n+m]' -# # dc = - dQ[1:n, end] + dQ[end, 1:n]' -# # return dA, db, dc -# end - -# function MOI.get(model::Model, ::DiffOpt.ReverseObjectiveFunction) -# g = model.back_grad_cache.g -# πz = model.back_grad_cache.πz -# dc = DiffOpt.lazy_combination(-, πz, g, length(g), eachindex(model.x)) -# return DiffOpt.VectorScalarAffineFunction(dc, 0.0) -# end - -# function MOI.get( -# model::Model, -# ::DiffOpt.ForwardVariablePrimal, -# vi::MOI.VariableIndex, -# ) -# i = vi.value -# du = model.forw_grad_cache.du -# dw = model.forw_grad_cache.dw -# return -(du[i] - model.x[i] * dw[]) -# end - -# function DiffOpt._get_db( -# model::Model, -# ci::MOI.ConstraintIndex{F,S}, -# ) where {F<:MOI.AbstractVectorFunction,S} -# i = MOI.Utilities.rows(model.model.constraints, ci) # vector -# # i = ci.value -# n = length(model.x) # columns in A -# # Since `b` in https://arxiv.org/pdf/1904.09043.pdf is the constant in the right-hand side and -# # `b` in MOI is the constant on the left-hand side, we have the opposite sign here -# # db = - dQ[n+1:n+m, end] + dQ[end, n+1:n+m]' -# g = model.back_grad_cache.g -# πz = model.back_grad_cache.πz -# # `g[end] * πz[n .+ i] - πz[end] * g[n .+ i]` -# return DiffOpt.lazy_combination(-, πz, g, length(g), n .+ i) -# end - -# function DiffOpt._get_dA( -# model::Model, -# ci::MOI.ConstraintIndex{<:MOI.AbstractVectorFunction}, -# ) -# i = MOI.Utilities.rows(model.model.constraints, ci) # vector -# # i = ci.value -# n = length(model.x) # columns in A -# m = length(model.y) # lines in A -# # dA = - dQ[1:n, n+1:n+m]' + dQ[n+1:n+m, 1:n] -# g = model.back_grad_cache.g -# πz = model.back_grad_cache.πz -# #return DiffOpt.lazy_combination(-, g, πz, n .+ i, 1:n) -# return g[n.+i] * πz[1:n]' - πz[n.+i] * g[1:n]' -# end - -# function MOI.get( -# model::Model, -# attr::MOI.ConstraintFunction, -# ci::MOI.ConstraintIndex, -# ) -# return MOI.get(model.model, attr, ci) -# end +function MOI.get( + model::Model, + ::DiffOpt.ForwardConstraintDual, + ci::MOI.ConstraintIndex, +) + if model.forw_grad_cache === nothing + error("Forward differentiation has not been performed yet.") + end + try + idx = model.cache.dual_mapping[ci.value] + catch + error("ConstraintIndex not found in dual mapping.") + end + return model.forw_grad_cache.dual_Δs[idx, :] +end -# end +end # module NonLinearProgram From db0c0ce1a8bb4f865052e2932b5f67fdfd1e983d Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 11 Dec 2024 11:59:55 -0500 Subject: [PATCH 03/41] only cache gradient --- src/NonLinearProgram/NonLinearProgram.jl | 128 ++++++++++++++--------- 1 file changed, 76 insertions(+), 52 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 93eab420..4b4bed96 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -26,6 +26,16 @@ Base.@kwdef struct ReverseCache dual_Δs_T::Matrix{Float64} end +struct ForwardParameter <: MOI.AbstractVariableAttribute end + +MOI.is_set_by_optimize(::ForwardParameter) = true + +struct ReverseParameter <: MOI.AbstractVariableAttribute end + +MOI.is_set_by_optimize(::ReverseParameter) = true + +struct ForwardConstraintDual <: MOI.AbstractConstraintAttribute end + """ DiffOpt.NonLinearProgram.Model <: DiffOpt.AbstractModel @@ -64,55 +74,52 @@ function MOI.empty!(model::Model) return end -function _cache_evaluator!(model::Model; params=nothing) - if params !== nothing || model.cache === nothing - # Retrieve and sort primal variables by index - primal_vars = sort(all_primal_vars(model.model), by=x -> x.index.value) - num_primal = length(primal_vars) - params = params === nothing ? sort(all_params(model.model), by=x -> x.index.value) : params - - # Create evaluator and constraints - evaluator, cons = create_evaluator(model.model; x=[primal_vars; params]) - num_constraints = length(cons) - # Analyze constraints and bounds - leq_locations, geq_locations = find_inequealities(cons) - num_leq = length(leq_locations) - num_geq = length(geq_locations) - has_up = findall(x -> has_upper_bound(x), primal_vars) - has_low = findall(x -> has_lower_bound(x), primal_vars) - num_low = length(has_low) - num_up = length(has_up) - - # Create unified dual mapping - dual_mapping = Vector{Int}(undef, num_constraints + num_low + num_up) - for (i, ci) in enumerate(cons) - dual_mapping[ci.index.value] = i - end - - # Add bounds to dual mapping - for (i, var_idx) in enumerate(has_low) - lb = MOI.LowerBoundRef(primal_vars[var_idx]) - dual_mapping[lb.index] = num_constraints + i - end - offset += num_low - for (i, var_idx) in enumerate(has_up) - ub = MOI.UpperBoundRef(primal_vars[var_idx]) - dual_mapping[ub.index] = offset + i - end - - num_slacks = num_leq + num_geq - num_w = num_primal + num_slacks - index_duals = [num_w+1:num_w+num_constraints; num_w+num_constraints+1:num_w+num_constraints+num_low; num_w+num_constraints+num_low+num_geq+1:num_w+num_constraints+num_low+num_geq+num_up] - - model.cache = Cache( - primal_vars=primal_vars, - dual_mapping=dual_mapping, - params=params, - index_duals=index_duals, - evaluator=evaluator, - cons=cons, - ) +function _cache_evaluator!(model::Model; params=sort(all_params(model.model), by=x -> x.index.value)) + # Retrieve and sort primal variables by index + primal_vars = sort(all_primal_vars(model.model), by=x -> x.index.value) + num_primal = length(primal_vars) + + # Create evaluator and constraints + evaluator, cons = create_evaluator(model.model; x=[primal_vars; params]) + num_constraints = length(cons) + # Analyze constraints and bounds + leq_locations, geq_locations = find_inequealities(cons) + num_leq = length(leq_locations) + num_geq = length(geq_locations) + has_up = findall(x -> has_upper_bound(x), primal_vars) + has_low = findall(x -> has_lower_bound(x), primal_vars) + num_low = length(has_low) + num_up = length(has_up) + + # Create unified dual mapping + dual_mapping = Vector{Int}(undef, num_constraints + num_low + num_up) + for (i, ci) in enumerate(cons) + dual_mapping[ci.index.value] = i + end + + # Add bounds to dual mapping + for (i, var_idx) in enumerate(has_low) + lb = MOI.LowerBoundRef(primal_vars[var_idx]) + dual_mapping[lb.index] = num_constraints + i end + offset += num_low + for (i, var_idx) in enumerate(has_up) + ub = MOI.UpperBoundRef(primal_vars[var_idx]) + dual_mapping[ub.index] = offset + i + end + + num_slacks = num_leq + num_geq + num_w = num_primal + num_slacks + index_duals = [num_w+1:num_w+num_constraints; num_w+num_constraints+1:num_w+num_constraints+num_low; num_w+num_constraints+num_low+num_geq+1:num_w+num_constraints+num_low+num_geq+num_up] + + model.cache = Cache( + primal_vars=primal_vars, + dual_mapping=dual_mapping, + params=params, + index_duals=index_duals, + evaluator=evaluator, + cons=cons, + ) return model.cache end @@ -120,11 +127,14 @@ function DiffOpt.forward_differentiate!(model::Model; params=nothing) model.diff_time = @elapsed begin cache = _cache_evaluator!(model; params=params) - # Compute sensitivities + Δp = [MOI.get(model, DiffOpt.ForwardParameter(), p.index) for p in cache.params] + + # Compute Jacobian Δs = compute_sensitivity(cache.evaluator, cache.cons; primal_vars=cache.primal_vars, params=cache.params) - primal_Δs = Δs[1:cache.num_primal, :] # Exclude slacks - dual_Δs = Δs[cache.index_duals, :] # Includes constraints and bounds + # Extract primal and dual sensitivities + primal_Δs = Δs[1:cache.num_primal, :] * Δp # Exclude slacks + dual_Δs = Δs[cache.index_duals, :] * Δp # Includes constraints and bounds model.forw_grad_cache = ForwCache( primal_Δs=primal_Δs, @@ -136,7 +146,19 @@ end function DiffOpt.reverse_differentiate!(model::Model; params=nothing) model.diff_time = @elapsed begin - # TODO: Implement reverse differentiation + cache = _cache_evaluator!(model; params=params) + + # Compute Jacobian + Δs = compute_sensitivity(cache.evaluator, cache.cons; primal_vars=cache.primal_vars, params=cache.params) + + # Extract primal and dual sensitivities + primal_Δs_T = Δs[1:cache.num_primal, :]' + dual_Δs_T = Δs[cache.index_duals, :]' + + model.back_grad_cache = ReverseCache( + primal_Δs_T=primal_Δs_T, + dual_Δs_T=dual_Δs_T, + ) end return nothing end @@ -169,4 +191,6 @@ function MOI.get( return model.forw_grad_cache.dual_Δs[idx, :] end +# TODO: get for the reverse mode + end # module NonLinearProgram From d8b38f457048e278bb8bf0f8390d9a7b485f1a41 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 13 Dec 2024 09:46:06 -0500 Subject: [PATCH 04/41] update API --- src/DiffOpt.jl | 2 + src/NonLinearProgram/NonLinearProgram.jl | 10 - src/NonLinearProgram/nlp_utilities.jl | 64 ++--- src/diff_opt.jl | 56 ++++ src/moi_wrapper.jl | 8 + test/data/nlp_problems.jl | 340 +++++++++++++++++++++++ test/nlp_program.jl | 252 +++++++++++++++++ 7 files changed, 690 insertions(+), 42 deletions(-) create mode 100644 test/data/nlp_problems.jl create mode 100644 test/nlp_program.jl diff --git a/src/DiffOpt.jl b/src/DiffOpt.jl index 9ab20101..b2d0d2f4 100644 --- a/src/DiffOpt.jl +++ b/src/DiffOpt.jl @@ -25,6 +25,7 @@ include("bridges.jl") include("QuadraticProgram/QuadraticProgram.jl") include("ConicProgram/ConicProgram.jl") +include("NonLinearProgram/NonLinearProgram.jl") """ add_all_model_constructors(model) @@ -35,6 +36,7 @@ Add all constructors of [`AbstractModel`](@ref) defined in this package to function add_all_model_constructors(model) add_model_constructor(model, QuadraticProgram.Model) add_model_constructor(model, ConicProgram.Model) + add_model_constructor(model, NonLinearProgram.Model) return end diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 4b4bed96..20a9d4a6 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -26,16 +26,6 @@ Base.@kwdef struct ReverseCache dual_Δs_T::Matrix{Float64} end -struct ForwardParameter <: MOI.AbstractVariableAttribute end - -MOI.is_set_by_optimize(::ForwardParameter) = true - -struct ReverseParameter <: MOI.AbstractVariableAttribute end - -MOI.is_set_by_optimize(::ReverseParameter) = true - -struct ForwardConstraintDual <: MOI.AbstractConstraintAttribute end - """ DiffOpt.NonLinearProgram.Model <: DiffOpt.AbstractModel diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index bb8b8077..966ba8d6 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -4,10 +4,10 @@ Create a Nonlinear Programming (NLP) model from a JuMP model. """ function create_nlp_model(model::JuMP.Model) - rows = Vector{ConstraintRef}(undef, 0) + rows = Vector{JuMP.ConstraintRef}(undef, 0) nlp = MOI.Nonlinear.Model() for (F, S) in list_of_constraint_types(model) - if F <: VariableRef && !(S <: MathOptInterface.EqualTo{Float64}) + if F <: JuMP.VariableRef && !(S <: MathOptInterface.EqualTo{Float64}) continue # Skip variable bounds end for ci in all_constraints(model, F, S) @@ -43,11 +43,11 @@ sense_mult(x) = JuMP.objective_sense(owner_model(x)) == MOI.MIN_SENSE ? 1.0 : -1 sense_mult(x::Vector) = sense_mult(x[1]) """ - compute_optimal_hessian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{ConstraintRef}, x::Vector{VariableRef}) + compute_optimal_hessian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{JuMP.ConstraintRef}, x::Vector{JuMP.VariableRef}) Compute the optimal Hessian of the Lagrangian. """ -function compute_optimal_hessian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{ConstraintRef}, x::Vector{VariableRef}) +function compute_optimal_hessian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{JuMP.ConstraintRef}, x::Vector{JuMP.VariableRef}) sense_multiplier = objective_sense(owner_model(x[1])) == MOI.MIN_SENSE ? 1.0 : -1.0 hessian_sparsity = MOI.hessian_lagrangian_structure(evaluator) I = [i for (i, _) in hessian_sparsity] @@ -61,11 +61,11 @@ function compute_optimal_hessian(evaluator::MOI.Nonlinear.Evaluator, rows::Vecto end """ - compute_optimal_jacobian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{ConstraintRef}, x::Vector{VariableRef}) + compute_optimal_jacobian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{JuMP.ConstraintRef}, x::Vector{JuMP.VariableRef}) Compute the optimal Jacobian of the constraints. """ -function compute_optimal_jacobian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{ConstraintRef}, x::Vector{VariableRef}) +function compute_optimal_jacobian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{JuMP.ConstraintRef}, x::Vector{JuMP.VariableRef}) jacobian_sparsity = MOI.jacobian_structure(evaluator) I = [i for (i, _) in jacobian_sparsity] J = [j for (_, j) in jacobian_sparsity] @@ -76,11 +76,11 @@ function compute_optimal_jacobian(evaluator::MOI.Nonlinear.Evaluator, rows::Vect end """ - compute_optimal_hess_jac(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{ConstraintRef}, x::Vector{VariableRef}) + compute_optimal_hess_jac(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{JuMP.ConstraintRef}, x::Vector{JuMP.VariableRef}) Compute the optimal Hessian of the Lagrangian and Jacobian of the constraints. """ -function compute_optimal_hess_jac(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{ConstraintRef}, x::Vector{VariableRef}) +function compute_optimal_hess_jac(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{JuMP.ConstraintRef}, x::Vector{JuMP.VariableRef}) hessian = compute_optimal_hessian(evaluator, rows, x) jacobian = compute_optimal_jacobian(evaluator, rows, x) @@ -88,27 +88,27 @@ function compute_optimal_hess_jac(evaluator::MOI.Nonlinear.Evaluator, rows::Vect end """ - all_primal_vars(model::Model) + all_primal_vars(model::JuMP.Model) Get all the primal variables in the model. """ -all_primal_vars(model::Model) = filter(x -> !is_parameter(x), all_variables(model)) +all_primal_vars(model::JuMP.Model) = filter(x -> !is_parameter(x), all_variables(model)) """ - all_params(model::Model) + all_params(model::JuMP.Model) Get all the parameters in the model. """ -all_params(model::Model) = filter(x -> is_parameter(x), all_variables(model)) +all_params(model::JuMP.Model) = filter(x -> is_parameter(x), all_variables(model)) """ - create_evaluator(model::Model; x=all_variables(model)) + create_evaluator(model::JuMP.Model; x=all_variables(model)) Create an evaluator for the model. """ JuMP.index(x::JuMP.Containers.DenseAxisArray) = index.(x).data -function create_evaluator(model::Model; x=all_variables(model)) +function create_evaluator(model::JuMP.Model; x=all_variables(model)) nlp, rows = create_nlp_model(model) backend = MOI.Nonlinear.SparseReverseMode() evaluator = MOI.Nonlinear.Evaluator(nlp, backend, vcat(index.(x)...)) @@ -117,22 +117,22 @@ function create_evaluator(model::Model; x=all_variables(model)) end -function is_less_inequality(con::ConstraintRef) +function is_less_inequality(con::JuMP.ConstraintRef) set_type = typeof(MOI.get(owner_model(con), MOI.ConstraintSet(), con)) return set_type <: MOI.LessThan end -function is_greater_inequality(con::ConstraintRef) +function is_greater_inequality(con::JuMP.ConstraintRef) set_type = typeof(MOI.get(owner_model(con), MOI.ConstraintSet(), con)) return set_type <: MOI.GreaterThan end """ - find_inequealities(cons::Vector{ConstraintRef}) + find_inequealities(cons::Vector{JuMP.ConstraintRef}) Find the indices of the inequality constraints. """ -function find_inequealities(cons::Vector{C}) where C<:ConstraintRef +function find_inequealities(cons::Vector{C}) where C<:JuMP.ConstraintRef leq_locations = zeros(length(cons)) geq_locations = zeros(length(cons)) for i in 1:length(cons) @@ -147,11 +147,11 @@ function find_inequealities(cons::Vector{C}) where C<:ConstraintRef end """ - get_slack_inequality(con::ConstraintRef) + get_slack_inequality(con::JuMP.ConstraintRef) Get the reference to the canonical function that is equivalent to the slack variable of the inequality constraint. """ -function get_slack_inequality(con::ConstraintRef) +function get_slack_inequality(con::JuMP.ConstraintRef) set_type = typeof(MOI.get(owner_model(con), MOI.ConstraintSet(), con)) obj = constraint_object(con) if set_type <: MOI.LessThan @@ -163,11 +163,11 @@ function get_slack_inequality(con::ConstraintRef) end """ - compute_solution_and_bounds(primal_vars::Vector{VariableRef}, cons::Vector{C}) where C<:ConstraintRef + compute_solution_and_bounds(primal_vars::Vector{JuMP.VariableRef}, cons::Vector{C}) where C<:JuMP.ConstraintRef Compute the solution and bounds of the primal variables. """ -function compute_solution_and_bounds(primal_vars::Vector{VariableRef}, cons::Vector{C}) where {C<:ConstraintRef} +function compute_solution_and_bounds(primal_vars::Vector{JuMP.VariableRef}, cons::Vector{C}) where {C<:JuMP.ConstraintRef} sense_multiplier = sense_mult(primal_vars) num_vars = length(primal_vars) leq_locations, geq_locations = find_inequealities(cons) @@ -236,16 +236,16 @@ function compute_solution_and_bounds(primal_vars::Vector{VariableRef}, cons::Vec end """ - build_M_N(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{ConstraintRef}, - primal_vars::Vector{VariableRef}, params::Vector{VariableRef}, + build_M_N(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{JuMP.ConstraintRef}, + primal_vars::Vector{JuMP.VariableRef}, params::Vector{JuMP.VariableRef}, _X::Vector, _V_L::Vector, _X_L::Vector, _V_U::Vector, _X_U::Vector, ineq_locations::Vector{Z}, has_up::Vector{Z}, has_low::Vector{Z} ) where {Z<:Integer} Build the M (KKT Jacobian w.r.t. solution) and N (KKT Jacobian w.r.t. parameters) matrices for the sensitivity analysis. """ -function build_M_N(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{ConstraintRef}, - primal_vars::Vector{VariableRef}, params::Vector{VariableRef}, +function build_M_N(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{JuMP.ConstraintRef}, + primal_vars::Vector{JuMP.VariableRef}, params::Vector{JuMP.VariableRef}, _X::AbstractVector, _V_L::AbstractVector, _X_L::AbstractVector, _V_U::AbstractVector, _X_U::AbstractVector, leq_locations::Vector{Z}, geq_locations::Vector{Z}, ineq_locations::Vector{Z}, has_up::Vector{Z}, has_low::Vector{Z} ) where {Z<:Integer} @@ -380,16 +380,16 @@ function inertia_corrector_factorization(M; st=1e-6, max_corrections=50) end """ - compute_derivatives_no_relax(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{ConstraintRef}, - primal_vars::Vector{VariableRef}, params::Vector{VariableRef}, + compute_derivatives_no_relax(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{JuMP.ConstraintRef}, + primal_vars::Vector{JuMP.VariableRef}, params::Vector{JuMP.VariableRef}, _X::Vector, _V_L::Vector, _X_L::Vector, _V_U::Vector, _X_U::Vector, ineq_locations::Vector{Z}, has_up::Vector{Z}, has_low::Vector{Z} ) where {Z<:Integer} Compute the derivatives of the solution w.r.t. the parameters without accounting for active set changes. """ -function compute_derivatives_no_relax(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{ConstraintRef}, - primal_vars::Vector{VariableRef}, params::Vector{VariableRef}, +function compute_derivatives_no_relax(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{JuMP.ConstraintRef}, + primal_vars::Vector{JuMP.VariableRef}, params::Vector{JuMP.VariableRef}, _X::AbstractVector, _V_L::AbstractVector, _X_L::AbstractVector, _V_U::AbstractVector, _X_U::AbstractVector, leq_locations::Vector{Z}, geq_locations::Vector{Z}, ineq_locations::Vector{Z}, has_up::Vector{Z}, has_low::Vector{Z} ) where {Z<:Integer} @@ -410,11 +410,11 @@ function compute_derivatives_no_relax(evaluator::MOI.Nonlinear.Evaluator, cons:: end """ - compute_sensitivity(model::Model; primal_vars=all_primal_vars(model), params=all_params(model)) + compute_sensitivity(model::JuMP.Model; primal_vars=all_primal_vars(model), params=all_params(model)) Compute the sensitivity of the solution given sensitivity of the parameters (Δp). """ -function compute_sensitivity(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{ConstraintRef}; primal_vars=all_primal_vars(model), params=all_params(model), tol=1e-6 +function compute_sensitivity(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{JuMP.ConstraintRef}; primal_vars=all_primal_vars(model), params=all_params(model), tol=1e-6 ) ismin = sense_mult(primal_vars) == 1.0 sense_multiplier = sense_mult(primal_vars) diff --git a/src/diff_opt.jl b/src/diff_opt.jl index 77ba7d96..2c0c7129 100644 --- a/src/diff_opt.jl +++ b/src/diff_opt.jl @@ -124,6 +124,52 @@ MOI.set(model, DiffOpt.ReverseVariablePrimal(), x) """ struct ReverseVariablePrimal <: MOI.AbstractVariableAttribute end +""" + ForwardParameter <: MOI.AbstractVariableAttribute + +A `MOI.AbstractVariableAttribute` to set input data to forward differentiation, +that is, problem input data. + +For instance, to set the tangent of the variable of index `vi`, do the +following: +```julia +MOI.set(model, DiffOpt.ForwardParameter(), x) +``` +""" +struct ForwardParameter <: MOI.AbstractVariableAttribute end + +MOI.is_set_by_optimize(::ForwardParameter) = true + +""" + ReverseParameter <: MOI.AbstractVariableAttribute + +A `MOI.AbstractVariableAttribute` to get output data from reverse differentiation, +that is, problem input data. + +For instance, to get the tangent of the variable of index `vi` corresponding to +the tangents given to `ReverseVariablePrimal`, do the following: +```julia +MOI.get(model, DiffOpt.ReverseParameter(), vi) +``` +""" +struct ReverseParameter <: MOI.AbstractVariableAttribute end + +MOI.is_set_by_optimize(::ReverseParameter) = true + +""" + ForwardConstraintDual <: MOI.AbstractConstraintAttribute + +A `MOI.AbstractConstraintAttribute` to get output data from forward differentiation for the dual variable. + +For instance, to get the sensitivity of the dual of constraint of index `ci` with respect to the parameter perturbation, do the following: + +```julia +MOI.get(model, DiffOpt.ForwardConstraintDual(), ci) +``` +""" + +struct ForwardConstraintDual <: MOI.AbstractConstraintAttribute end + """ ReverseObjectiveFunction <: MOI.AbstractModelAttribute @@ -290,6 +336,16 @@ function MOI.set( return end +function MOI.set( + model::AbstractModel, + ::ForwardParameter, + pi::MOI.VariableIndex, + val, +) + model.input_cache.dx[pi] = val + return +end + function MOI.set( model::AbstractModel, ::ForwardConstraintFunction, diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index 3570d94d..7cce0dd2 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -662,6 +662,14 @@ function MOI.supports( return true end +function MOI.supports( + ::Optimizer, + ::ForwardParameter, + ::Type{MOI.VariableIndex}, +) + return true +end + function MOI.get( model::Optimizer, ::ReverseVariablePrimal, diff --git a/test/data/nlp_problems.jl b/test/data/nlp_problems.jl new file mode 100644 index 00000000..c1f5760d --- /dev/null +++ b/test/data/nlp_problems.jl @@ -0,0 +1,340 @@ +using JuMP +using Ipopt + +################################################ +#= +From JuMP Tutorial for Querying Hessians: +https://github.com/jump-dev/JuMP.jl/blob/301d46e81cb66c74c6e22cd89fb89ced740f157b/docs/src/tutorials/nonlinear/querying_hessians.jl#L67-L72 +=# +################################################ +function create_nonlinear_jump_model(;ismin=true) + model = Model(Ipopt.Optimizer) + set_silent(model) + @variable(model, p ∈ MOI.Parameter(1.0)) + @variable(model, p2 ∈ MOI.Parameter(2.0)) + @variable(model, p3 ∈ MOI.Parameter(100.0)) + @variable(model, x[i = 1:2], start = -i) + @constraint(model, g_1, x[1]^2 <= p) + @constraint(model, g_2, p * (x[1] + x[2])^2 <= p2) + if ismin + @objective(model, Min, (1 - x[1])^2 + p3 * (x[2] - x[1]^2)^2) + else + @objective(model, Max, -(1 - x[1])^2 - p3 * (x[2] - x[1]^2)^2) + end + + return model, x, [g_1; g_2], [p; p2; p3] +end + + +################################################ +#= +From sIpopt paper: https://optimization-online.org/2011/04/3008/ +=# +################################################ + +function create_nonlinear_jump_model_sipopt(;ismin = true) + model = Model(Ipopt.Optimizer) + set_silent(model) + @variable(model, p1 ∈ MOI.Parameter(4.5)) + @variable(model, p2 ∈ MOI.Parameter(1.0)) + @variable(model, x[i = 1:3] >= 0, start = -i) + @constraint(model, g_1, 6 * x[1] + 3 * x[2] + 2 * x[3] - p1 == 0) + @constraint(model, g_2, p2 * x[1] + x[2] - x[3] - 1 == 0) + if ismin + @objective(model, Min, x[1]^2 + x[2]^2 + x[3]^2) + else + @objective(model, Max, -x[1]^2 - x[2]^2 - x[3]^2) + end + return model, x, [g_1; g_2], [p1; p2] +end + +################################################ +#= +Simple Problems +=# +################################################ + + +function create_jump_model_1(p_val = [1.5]) + model = Model(Ipopt.Optimizer) + set_silent(model) + + # Parameters + @variable(model, p ∈ MOI.Parameter(p_val[1])) + + # Variables + @variable(model, x) + + # Constraints + @constraint(model, con1, x >= p) + @constraint(model, con2, x >= 2) + @objective(model, Min, x^2) + + return model, [x], [con1; con2], [p] +end + +function create_jump_model_2(p_val = [1.5]) + model = Model(Ipopt.Optimizer) + set_silent(model) + + # Parameters + @variable(model, p ∈ MOI.Parameter(p_val[1])) + + # Variables + @variable(model, x >= 2.0) + + # Constraints + @constraint(model, con1, x >= p) + @objective(model, Min, x^2) + + return model, [x], [con1], [p] +end + +function create_jump_model_3(p_val = [-1.5]) + model = Model(Ipopt.Optimizer) + set_silent(model) + + # Parameters + @variable(model, p ∈ MOI.Parameter(p_val[1])) + + # Variables + @variable(model, x) + + # Constraints + @constraint(model, con1, x <= p) + @constraint(model, con2, x <= -2) + @objective(model, Min, -x) + + return model, [x], [con1; con2], [p] +end + +function create_jump_model_4(p_val = [1.5]) + model = Model(Ipopt.Optimizer) + set_silent(model) + + # Parameters + @variable(model, p ∈ MOI.Parameter(p_val[1])) + + # Variables + @variable(model, x) + + # Constraints + @constraint(model, con1, x <= p) + @constraint(model, con2, x <= 2) + @objective(model, Max, x) + + return model, [x], [con1; con2], [p] +end + +function create_jump_model_5(p_val = [1.5]) + model = Model(Ipopt.Optimizer) + set_silent(model) + + # Parameters + @variable(model, p ∈ MOI.Parameter(p_val[1])) + + # Variables + @variable(model, x) + + # Constraints + @constraint(model, con1, x >= p) + @constraint(model, con2, x >= 2) + @objective(model, Max, -x) + + return model, [x], [con1; con2], [p] +end + +# Softmax model +h(y) = - sum(y .* log.(y)) +softmax(x) = exp.(x) / sum(exp.(x)) +function create_jump_model_6(p_a = collect(1.0:0.1:2.0)) + model = Model(Ipopt.Optimizer) + set_silent(model) + + # Parameters + @variable(model, x[i=1:length(p_a)] ∈ MOI.Parameter.(p_a)) + + # Variables + @variable(model, y[1:length(p_a)] >= 0.0) + + # Constraints + @constraint(model, con1, sum(y) == 1) + @constraint(model, con2[i=1:length(x)], y[i] <= 1) + + # Objective + @objective(model, Max, dot(x, y) + h(y)) + + return model, y, [con1; con2], x +end + +function create_jump_model_7(p_val = [1.5], g = sin) + model = Model(Ipopt.Optimizer) + set_silent(model) + + # Parameters + @variable(model, p ∈ MOI.Parameter(p_val[1])) + + # Variables + @variable(model, x) + + # Constraints + @constraint(model, con1, x * g(p) == 1) + @objective(model, Min, 0) + + return model, [x], [con1], [p] +end + +################################################ +#= +Non Linear Problems +=# +################################################ + + +function create_nonlinear_jump_model_1(p_val = [1.0; 2.0; 100]; ismin = true) + model = Model(Ipopt.Optimizer) + set_silent(model) + + # Parameters + @variable(model, p[i=1:3] ∈ MOI.Parameter.(p_val)) + + # Variables + @variable(model, x) + @variable(model, y) + + # Constraints + @constraint(model, con1, y >= p[1]*sin(x)) # NLP Constraint + @constraint(model, con2, x + y == p[1]) + @constraint(model, con3, p[2] * x >= 0.1) + if ismin + @objective(model, Min, (1 - x)^2 + p[3] * (y - x^2)^2) # NLP Objective + else + @objective(model, Max, -(1 - x)^2 - p[3] * (y - x^2)^2) # NLP Objective + end + + return model, [x; y], [con1; con2; con3], p +end + +function create_nonlinear_jump_model_2(p_val = [3.0; 2.0; 10]; ismin = true) + model = Model(Ipopt.Optimizer) + set_silent(model) + + # Parameters + @variable(model, p[i=1:3] ∈ MOI.Parameter.(p_val)) + + # Variables + @variable(model, x <= 10) + @variable(model, y) + + # Constraints + @constraint(model, con1, y >= p[1]*sin(x)) # NLP Constraint + @constraint(model, con2, x + y == p[1]) + @constraint(model, con3, p[2] * x >= 0.1) + if ismin + @objective(model, Min, (1 - x)^2 + p[3] * (y - x^2)^2) # NLP Objective + else + @objective(model, Max, -(1 - x)^2 - p[3] * (y - x^2)^2) # NLP Objective + end + + return model, [x; y], [con1; con2; con3], p +end + +function create_nonlinear_jump_model_3(p_val = [3.0; 2.0; 10]; ismin = true) + model = Model(Ipopt.Optimizer) + set_silent(model) + + # Parameters + @variable(model, p[i=1:3] ∈ MOI.Parameter.(p_val)) + + # Variables + @variable(model, x <= 10) + @variable(model, y) + + # Constraints + @constraint(model, con1, y >= p[1]*sin(x)) # NLP Constraint + @constraint(model, con2, x + y == p[1]) + @constraint(model, con3, p[2] * x >= 0.1) + if ismin + @objective(model, Min, (1 - x)^2 + p[3] * (y - x^2)^2) # NLP Objective + else + @objective(model, Max, -(1 - x)^2 - p[3] * (y - x^2)^2) # NLP Objective + end + return model, [x; y], [con1; con2; con3], p +end + +function create_nonlinear_jump_model_4(p_val = [1.0; 2.0; 100]; ismin = true) + model = Model(Ipopt.Optimizer) + set_silent(model) + + # Parameters + @variable(model, p[i=1:3] ∈ MOI.Parameter.(p_val)) + + # Variables + @variable(model, x) + @variable(model, y) + + # Constraints + @constraint(model, con0, x == p[1] - 0.5) + @constraint(model, con1, y >= p[1]*sin(x)) # NLP Constraint + @constraint(model, con2, x + y == p[1]) + @constraint(model, con3, p[2] * x >= 0.1) + if ismin + @objective(model, Min, (1 - x)^2 + p[3] * (y - x^2)^2) # NLP Objective + else + @objective(model, Max, -(1 - x)^2 - p[3] * (y - x^2)^2) # NLP Objective + end + + return model, [x; y], [con1; con2; con3], p +end + +function create_nonlinear_jump_model_5(p_val = [1.0; 2.0; 100]; ismin = true) + model = Model(Ipopt.Optimizer) + set_silent(model) + + # Parameters + @variable(model, p[i=1:3] ∈ MOI.Parameter.(p_val)) + + # Variables + @variable(model, x) + @variable(model, y) + + # Constraints + fix(x, 0.5) + con0 = JuMP.FixRef(x) + @constraint(model, con1, y >= p[1]*sin(x)) # NLP Constraint + @constraint(model, con2, x + y == p[1]) + @constraint(model, con3, p[2] * x >= 0.1) + if ismin + @objective(model, Min, (1 - x)^2 + p[3] * (y - x^2)^2) # NLP Objective + else + @objective(model, Max, -(1 - x)^2 - p[3] * (y - x^2)^2) # NLP Objective + end + + return model, [x; y], [con0; con1; con2; con3], p +end + +function create_nonlinear_jump_model_6(p_val = [100.0; 200.0]; ismin = true) + model = Model(Ipopt.Optimizer) + set_silent(model) + + # Parameters + @variable(model, p[i=1:2] ∈ MOI.Parameter.(p_val)) + + # Variables + @variable(model, x[i=1:2]) + @variable(model, z) # >= 2.0) + @variable(model, w) # <= 3.0) + # @variable(model, f[1:2]) + + # Constraints + @constraint(model, con1, x[2] - 0.0001 * x[1]^2 - 0.2 * z^2 - 0.3 * w^2 >= p[1] + 1) + @constraint(model, con2, x[1] + 0.001 * x[2]^2 + 0.5 * w^2 + 0.4 * z^2 <= 10 * p[1] + 2) + @constraint(model, con3, z^2 + w^2 == 13) + if ismin + @objective(model, Min, x[2] - x[1] + z - w) + else + @objective(model, Max, -x[2] + x[1] - z + w) + end + + return model, [x; z; w], [con2; con3], p +end \ No newline at end of file diff --git a/test/nlp_program.jl b/test/nlp_program.jl new file mode 100644 index 00000000..702fdc23 --- /dev/null +++ b/test/nlp_program.jl @@ -0,0 +1,252 @@ +using JuMP +using Ipopt +using Test +using FiniteDiff + +include("test/data/nlp_problems.jl") + +################################################ +#= +# Test JuMP Hessian and Jacobian + +From JuMP Tutorial for Querying Hessians: +https://github.com/jump-dev/JuMP.jl/blob/301d46e81cb66c74c6e22cd89fb89ced740f157b/docs/src/tutorials/nonlinear/querying_hessians.jl#L67-L72 +=# +################################################ + +function analytic_hessian(x, σ, μ, p) + g_1_H = [2.0 0.0; 0.0 0.0] + g_2_H = p[1] * [2.0 2.0; 2.0 2.0] + f_H = zeros(2, 2) + f_H[1, 1] = 2.0 + p[3] * 12.0 * x[1]^2 - p[3] * 4.0 * x[2] + f_H[1, 2] = f_H[2, 1] = -p[3] * 4.0 * x[1] + f_H[2, 2] = p[3] * 2.0 + return σ * f_H + μ' * [g_1_H, g_2_H] +end + +function analytic_jacobian(x, p) + g_1_J = [ + 2.0 * x[1], # ∂g_1/∂x_1 + 0.0, # ∂g_1/∂x_2 + -1.0, # ∂g_1/∂p_1 + 0.0, # ∂g_1/∂p_2 + 0.0 # ∂g_1/∂p_3 + ] + g_2_J = [ + p[1] * 2.0 * (x[1] + x[2]), # ∂g_2/∂x_1 + 2.0 * (x[1] + x[2]), # ∂g_2/∂x_2 + (x[1] + x[2])^2, # ∂g_2/∂p_1 + -1.0, # ∂g_2/∂p_2 + 0.0 # ∂g_2/∂p_3 + ] + return hcat(g_2_J, g_1_J)'[:,:] +end + +function test_create_evaluator(model, x) + @testset "Create NLP model" begin + nlp, rows = create_nlp_model(model) + @test nlp isa MOI.Nonlinear.Model + @test rows isa Vector{ConstraintRef} + end + @testset "Create Evaluator" begin + evaluator, rows = create_evaluator(model; x = x) + @test evaluator isa MOI.Nonlinear.Evaluator + @test rows isa Vector{ConstraintRef} + end +end + +function test_compute_optimal_hess_jacobian() + @testset "Compute Optimal Hessian and Jacobian" begin + # Model + model, x, cons, params = create_nonlinear_jump_model() + # Optimize + optimize!(model) + @assert is_solved_and_feasible(model) + # Create evaluator + test_create_evaluator(model, [x; params]) + evaluator, rows = create_evaluator(model; x = [x; params]) + # Compute Hessian and Jacobian + num_var = length(x) + full_hessian, full_jacobian = compute_optimal_hess_jac(evaluator, rows, [x; params]) + hessian = full_hessian[1:num_var, 1:num_var] + # Check Hessian + @test all(hessian .≈ analytic_hessian(value.(x), 1.0, -dual.(cons), value.(params))) + # TODO: Test hessial of parameters + # Check Jacobian + @test all(full_jacobian .≈ analytic_jacobian(value.(x), value.(params))) + end +end + +################################################ +#= +# Test Sensitivity through analytical +=# +################################################ + + +# f(x, p) = 0 +# x = g(p) +# ∂x/∂p = ∂g/∂p + +DICT_PROBLEMS_Analytical_no_cc = Dict( + "geq no impact" => (p_a=[1.5], Δp=[0.2], Δs_a=[0.0; -0.2; 0.0; 0.0; 0.0; 0.0; 0.0], model_generator=create_jump_model_1), + "geq impact" => (p_a=[2.1], Δp=[0.2], Δs_a=[0.2; 0.0; 0.2; 0.4; 0.0; 0.4; 0.0], model_generator=create_jump_model_1), + "geq bound impact" => (p_a=[2.1], Δp=[0.2], Δs_a=[0.2; 0.0; 0.4; 0.0; 0.4], model_generator=create_jump_model_2), + "leq no impact" => (p_a=[-1.5], Δp=[-0.2], Δs_a=[0.0; 0.2; 0.0; 0.0; 0.0; 0.0; 0.0], model_generator=create_jump_model_3), + "leq impact" => (p_a=[-2.1], Δp=[-0.2], Δs_a=[-0.2; 0.0; -0.2], model_generator=create_jump_model_3), + "leq no impact max" => (p_a=[2.1], Δp=[0.2], Δs_a=[0.0; -0.2; 0.0; 0.0; 0.0], model_generator=create_jump_model_4), + "leq impact max" => (p_a=[1.5], Δp=[0.2], Δs_a=[0.2; 0.0; 0.2], model_generator=create_jump_model_4), + "geq no impact max" => (p_a=[1.5], Δp=[0.2], Δs_a=[0.0; -0.2; 0.0; 0.0; 0.0], model_generator=create_jump_model_5), + "geq impact max" => (p_a=[2.1], Δp=[0.2], Δs_a=[0.2; 0.0; 0.2], model_generator=create_jump_model_5), +) + +function test_compute_derivatives_Analytical(DICT_PROBLEMS) + @testset "Compute Derivatives Analytical: $problem_name" for (problem_name, (p_a, Δp, Δs_a, model_generator)) in DICT_PROBLEMS + # OPT Problem + model, primal_vars, cons, params = model_generator() + set_parameter_value.(params, p_a) + optimize!(model) + @assert is_solved_and_feasible(model) + MOI.set.(model, DiffOpt.ForwardParameter(), params[1].index, Δp) + DiffOpt.forward_differentiate!(model::Model; params=params) + # Compute derivatives + (Δs, sp_approx), evaluator, cons = compute_sensitivity(model, Δp; primal_vars, params) + # Check sensitivities + @test all(isapprox.(Δs[1:length(Δs_a)], Δs_a; atol = 1e-4)) + end +end + +################################################ +#= +# Test Sensitivity through finite differences +=# +################################################ + +function eval_model_jump(model, primal_vars, cons, params, p_val) + set_parameter_value.(params, p_val) + optimize!(model) + @assert is_solved_and_feasible(model) + return value.(primal_vars), dual.(cons), [dual.(LowerBoundRef(v)) for v in primal_vars if has_lower_bound(v)], [dual.(UpperBoundRef(v)) for v in primal_vars if has_upper_bound(v)] +end + +function stack_solution(cons, leq_locations, geq_locations, x, _λ, ν_L, ν_U) + ineq_locations = vcat(geq_locations, leq_locations) + return Float64[x; value.(get_slack_inequality.(cons[ineq_locations])); _λ; ν_L; _λ[geq_locations]; ν_U; _λ[leq_locations]] +end + +function print_wrong_sensitive(Δs, Δs_fd, primal_vars, cons, leq_locations, geq_locations) + ineq_locations = vcat(geq_locations, leq_locations) + println("Some sensitivities are not correct: \n") + # primal vars + num_primal_vars = length(primal_vars) + for (i, v) in enumerate(primal_vars) + if !isapprox(Δs[i], Δs_fd[i]; atol = 1e-6) + println("Primal var: ", v, " | Δs: ", Δs[i], " | Δs_fd: ", Δs_fd[i]) + end + end + # slack vars + num_slack_vars = length(ineq_locations) + num_w = num_slack_vars + num_primal_vars + for (i, c) in enumerate(cons[ineq_locations]) + if !isapprox(Δs[i + num_primal_vars], Δs_fd[i + num_primal_vars] ; atol = 1e-6) + println("Slack var: ", c, " | Δs: ", Δs[i + num_primal_vars], " | Δs_fd: ", Δs_fd[i + num_primal_vars]) + end + end + # dual vars + num_cons = length(cons) + for (i, c) in enumerate(cons) + if !isapprox(Δs[i + num_w], Δs_fd[i + num_w] ; atol = 1e-6) + println("Dual var: ", c, " | Δs: ", Δs[i + num_w], " | Δs_fd: ", Δs_fd[i + num_w]) + end + end + # dual lower bound primal vars + var_lower = [v for v in primal_vars if has_lower_bound(v)] + num_lower_bounds = length(var_lower) + for (i, v) in enumerate(var_lower) + if !isapprox(Δs[i + num_w + num_cons], Δs_fd[i + num_w + num_cons] ; atol = 1e-6) + lower_bound_ref = LowerBoundRef(v) + println("lower bound dual: ", lower_bound_ref, " | Δs: ", Δs[i + num_w + num_cons], " | Δs_fd: ", Δs_fd[i + num_w + num_cons]) + end + end + # dual lower bound slack vars + for (i, c) in enumerate(cons[geq_locations]) + if !isapprox(Δs[i + num_w + num_cons + num_lower_bounds], Δs_fd[i + num_w + num_cons + num_lower_bounds] ; atol = 1e-6) + println("lower bound slack dual: ", c, " | Δs: ", Δs[i + num_w + num_cons + num_lower_bounds], " | Δs_fd: ", Δs_fd[i + num_w + num_cons + num_lower_bounds]) + end + end + for (i, c) in enumerate(cons[leq_locations]) + if !isapprox(Δs[i + num_w + num_cons + num_lower_bounds + length(geq_locations)], Δs_fd[i + num_w + num_cons + num_lower_bounds + length(geq_locations)] ; atol = 1e-6) + println("upper bound slack dual: ", c, " | Δs: ", Δs[i + num_w + num_cons + num_lower_bounds + length(geq_locations)], " | Δs_fd: ", Δs_fd[i + num_w + num_cons + num_lower_bounds + length(geq_locations)]) + end + end + # dual upper bound primal vars + var_upper = [v for v in primal_vars if has_upper_bound(v)] + for (i, v) in enumerate(var_upper) + if !isapprox(Δs[i + num_w + num_cons + num_lower_bounds + num_slack_vars], Δs_fd[i + num_w + num_cons + num_lower_bounds + num_slack_vars] ; atol = 1e-6) + upper_bound_ref = UpperBoundRef(v) + println("upper bound dual: ", upper_bound_ref, " | Δs: ", Δs[i + num_w + num_cons + num_lower_bounds + num_slack_vars], " | Δs_fd: ", Δs_fd[i + num_w + num_cons + num_lower_bounds + num_slack_vars]) + end + end +end + +DICT_PROBLEMS_no_cc = Dict( + "QP_sIpopt" => (p_a=[4.5; 1.0], Δp=[0.001; 0.0], model_generator=create_nonlinear_jump_model_sipopt), + "NLP_1" => (p_a=[3.0; 2.0; 200], Δp=[0.001; 0.0; 0.0], model_generator=create_nonlinear_jump_model_1), + "NLP_1_2" => (p_a=[3.0; 2.0; 200], Δp=[0.0; 0.001; 0.0], model_generator=create_nonlinear_jump_model_1), + "NLP_1_3" => (p_a=[3.0; 2.0; 200], Δp=[0.0; 0.0; 0.001], model_generator=create_nonlinear_jump_model_1), + "NLP_1_4" => (p_a=[3.0; 2.0; 200], Δp=[0.1; 0.5; 0.5], model_generator=create_nonlinear_jump_model_1), + "NLP_1_4" => (p_a=[3.0; 2.0; 200], Δp=[0.5; -0.5; 0.1], model_generator=create_nonlinear_jump_model_1), + "NLP_2" => (p_a=[3.0; 2.0; 10], Δp=[0.01; 0.0; 0.0], model_generator=create_nonlinear_jump_model_2), + "NLP_2_2" => (p_a=[3.0; 2.0; 10], Δp=[-0.1; 0.0; 0.0], model_generator=create_nonlinear_jump_model_2), + "NLP_3" => (p_a=[3.0; 2.0; 10], Δp=[0.001; 0.0; 0.0], model_generator=create_nonlinear_jump_model_3), + "NLP_3_2" => (p_a=[3.0; 2.0; 10], Δp=[0.0; 0.001; 0.0], model_generator=create_nonlinear_jump_model_3), + "NLP_3_3" => (p_a=[3.0; 2.0; 10], Δp=[0.0; 0.0; 0.001], model_generator=create_nonlinear_jump_model_3), + "NLP_3_4" => (p_a=[3.0; 2.0; 10], Δp=[0.5; 0.001; 0.5], model_generator=create_nonlinear_jump_model_3), + "NLP_3_5" => (p_a=[3.0; 2.0; 10], Δp=[0.1; 0.3; 0.1], model_generator=create_nonlinear_jump_model_3), + "NLP_3_6" => (p_a=[3.0; 2.0; 10], Δp=[0.1; 0.2; -0.5], model_generator=create_nonlinear_jump_model_3), + "NLP_4" => (p_a=[1.0; 2.0; 100], Δp=[0.001; 0.0; 0.0], model_generator=create_nonlinear_jump_model_4), + "NLP_5" => (p_a=[1.0; 2.0; 100], Δp=[0.0; 0.001; 0.0], model_generator=create_nonlinear_jump_model_5), + "NLP_6" => (p_a=[100.0; 200.0], Δp=[0.2; 0.5], model_generator=create_nonlinear_jump_model_6), +) + + +DICT_PROBLEMS_cc = Dict( + "QP_JuMP" => (p_a=[1.0; 2.0; 100.0], Δp=[-0.5; 0.5; 0.1], model_generator=create_nonlinear_jump_model), + "QP_sIpopt2" => (p_a=[5.0; 1.0], Δp=[-0.5; 0.0], model_generator=create_nonlinear_jump_model_sipopt), +) + +function test_compute_derivatives_Finite_Diff(DICT_PROBLEMS, iscc=false) + # @testset "Compute Derivatives: $problem_name" + for (problem_name, (p_a, Δp, model_generator)) in DICT_PROBLEMS, ismin in [true, false] + # OPT Problem + model, primal_vars, cons, params = model_generator(;ismin=ismin) + eval_model_jump(model, primal_vars, cons, params, p_a) + println("$problem_name: ", model) + # Compute derivatives + # Δp = [0.001; 0.0; 0.0] + p_b = p_a .+ Δp + (Δs, sp_approx), evaluator, cons = compute_sensitivity(model, Δp; primal_vars, params) + leq_locations, geq_locations = find_inequealities(cons) + sa = stack_solution(cons, leq_locations, geq_locations, eval_model_jump(model, primal_vars, cons, params, p_a)...) + # Check derivatives using finite differences + ∂s_fd = FiniteDiff.finite_difference_jacobian((p) -> stack_solution(cons, leq_locations, geq_locations, eval_model_jump(model, primal_vars, cons, params, p)...), p_a) + Δs_fd = ∂s_fd * Δp + # actual solution + sp = stack_solution(cons, leq_locations, geq_locations, eval_model_jump(model, primal_vars, cons, params, p_b)...) + # Check sensitivities + num_important = length(primal_vars) + length(cons) + test_derivatives = all(isapprox.(Δs, Δs_fd; rtol = 1e-5, atol=1e-6)) + test_approx = all(isapprox.(sp[1:num_important], sp_approx[1:num_important]; rtol = 1e-5, atol=1e-6)) + if test_derivatives || (iscc && test_approx) + println("All sensitivities are correct") + elseif iscc && !test_approx + @show Δp + println("Fail Approximations") + print_wrong_sensitive(Δs, sp.-sa, primal_vars, cons, leq_locations, geq_locations) + else + @show Δp + print_wrong_sensitive(Δs, Δs_fd, primal_vars, cons, leq_locations, geq_locations) + end + println("--------------------") + end +end \ No newline at end of file From eaa4c69ad342a0f476a71be358d5b09bcf17aedc Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 13 Dec 2024 10:07:42 -0500 Subject: [PATCH 05/41] start reverse mode --- src/NonLinearProgram/NonLinearProgram.jl | 17 ++++++++++++-- src/diff_opt.jl | 29 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 20a9d4a6..c601f9cd 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -82,6 +82,7 @@ function _cache_evaluator!(model::Model; params=sort(all_params(model.model), by num_up = length(has_up) # Create unified dual mapping + # TODO: This assumes that these are all possible constraints available. We should change to either a dict or a sparse array dual_mapping = Vector{Int}(undef, num_constraints + num_low + num_up) for (i, ci) in enumerate(cons) dual_mapping[ci.index.value] = i @@ -141,9 +142,13 @@ function DiffOpt.reverse_differentiate!(model::Model; params=nothing) # Compute Jacobian Δs = compute_sensitivity(cache.evaluator, cache.cons; primal_vars=cache.primal_vars, params=cache.params) + Δx = [MOI.get(model, DiffOpt.ReverseVariablePrimal(), x.index) for x in cache.primal_vars] + Δλ = [MOI.get(model, DiffOpt.ReverseConstraintDual(), c.index) for c in cache.cons] + Δvdown = [MOI.get(model, DiffOpt.ReverseVariableDual(), MOI.LowerBoundRef(x)) for x in cache.primal_vars if has_lower_bound(x)] + Δvup = [MOI.get(model, DiffOpt.ReverseVariableDual(), MOI.UpperBoundRef(x)) for x in cache.primal_vars if has_upper_bound(x)] # Extract primal and dual sensitivities - primal_Δs_T = Δs[1:cache.num_primal, :]' - dual_Δs_T = Δs[cache.index_duals, :]' + primal_Δs_T = Δs[1:cache.num_primal, :]' * Δx + dual_Δs_T = Δs[cache.index_duals, :]' * [Δλ; Δvdown; Δvup] model.back_grad_cache = ReverseCache( primal_Δs_T=primal_Δs_T, @@ -182,5 +187,13 @@ function MOI.get( end # TODO: get for the reverse mode +function MOI.get( + model::Model, + ::DiffOpt.ReverseParameter, + pi::MOI.VariableIndex, +) + +return NaN +end end # module NonLinearProgram diff --git a/src/diff_opt.jl b/src/diff_opt.jl index 2c0c7129..608fdaa8 100644 --- a/src/diff_opt.jl +++ b/src/diff_opt.jl @@ -140,6 +140,35 @@ struct ForwardParameter <: MOI.AbstractVariableAttribute end MOI.is_set_by_optimize(::ForwardParameter) = true +""" + ReverseConstraintDual <: MOI.AbstractConstraintAttribute + +A `MOI.AbstractConstraintAttribute` to set input data from reverse differentiation. + +For instance, to set the sensitivity wrt the dual of constraint of index `ci` do the following: +```julia +MOI.set(model, DiffOpt.ReverseConstraintDual(), x) +``` +""" +struct ReverseConstraintDual <: MOI.AbstractConstraintAttribute end + +MOI.is_set_by_optimize(::ReverseConstraintDual) = true + +""" + ReverseVariableDual <: MOI.AbstractVariableAttribute + +A `MOI.AbstractVariableAttribute` to set input data from reverse differentiation. + +For instance, to set the tangent of the variable of index `vi`, do the +following: +```julia +MOI.set(model, DiffOpt.ReverseVariableDual(), x) +``` +""" +struct ReverseVariableDual <: MOI.AbstractVariableAttribute end + +MOI.is_set_by_optimize(::ReverseVariableDual) = true + """ ReverseParameter <: MOI.AbstractVariableAttribute From e17033965a56956d093f86d40bd437c9783dadfb Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 13 Dec 2024 12:43:03 -0500 Subject: [PATCH 06/41] add overloads --- src/NonLinearProgram/NonLinearProgram.jl | 5 ++-- src/diff_opt.jl | 6 ----- src/jump_moi_overloads.jl | 9 +++++++ test/data/nlp_problems.jl | 30 ++++++++++++------------ test/nlp_program.jl | 2 +- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index c601f9cd..29eb5ac8 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -83,6 +83,7 @@ function _cache_evaluator!(model::Model; params=sort(all_params(model.model), by # Create unified dual mapping # TODO: This assumes that these are all possible constraints available. We should change to either a dict or a sparse array + # TODO: Check that the variable equal to works - Perhaps use bridge to change from equal to <= and >= dual_mapping = Vector{Int}(undef, num_constraints + num_low + num_up) for (i, ci) in enumerate(cons) dual_mapping[ci.index.value] = i @@ -147,12 +148,12 @@ function DiffOpt.reverse_differentiate!(model::Model; params=nothing) Δvdown = [MOI.get(model, DiffOpt.ReverseVariableDual(), MOI.LowerBoundRef(x)) for x in cache.primal_vars if has_lower_bound(x)] Δvup = [MOI.get(model, DiffOpt.ReverseVariableDual(), MOI.UpperBoundRef(x)) for x in cache.primal_vars if has_upper_bound(x)] # Extract primal and dual sensitivities + # TODO: multiply everyone together before indexing primal_Δs_T = Δs[1:cache.num_primal, :]' * Δx dual_Δs_T = Δs[cache.index_duals, :]' * [Δλ; Δvdown; Δvup] model.back_grad_cache = ReverseCache( - primal_Δs_T=primal_Δs_T, - dual_Δs_T=dual_Δs_T, + Δp=primal_Δs_T, # TODO: change ) end return nothing diff --git a/src/diff_opt.jl b/src/diff_opt.jl index 608fdaa8..2159b32c 100644 --- a/src/diff_opt.jl +++ b/src/diff_opt.jl @@ -138,8 +138,6 @@ MOI.set(model, DiffOpt.ForwardParameter(), x) """ struct ForwardParameter <: MOI.AbstractVariableAttribute end -MOI.is_set_by_optimize(::ForwardParameter) = true - """ ReverseConstraintDual <: MOI.AbstractConstraintAttribute @@ -152,8 +150,6 @@ MOI.set(model, DiffOpt.ReverseConstraintDual(), x) """ struct ReverseConstraintDual <: MOI.AbstractConstraintAttribute end -MOI.is_set_by_optimize(::ReverseConstraintDual) = true - """ ReverseVariableDual <: MOI.AbstractVariableAttribute @@ -167,8 +163,6 @@ MOI.set(model, DiffOpt.ReverseVariableDual(), x) """ struct ReverseVariableDual <: MOI.AbstractVariableAttribute end -MOI.is_set_by_optimize(::ReverseVariableDual) = true - """ ReverseParameter <: MOI.AbstractVariableAttribute diff --git a/src/jump_moi_overloads.jl b/src/jump_moi_overloads.jl index 9459f840..c6113336 100644 --- a/src/jump_moi_overloads.jl +++ b/src/jump_moi_overloads.jl @@ -39,6 +39,15 @@ function MOI.set( return MOI.set(model, attr, con_ref, JuMP.AffExpr(func)) end +function MOI.get( + model::JuMP.Model, + attr::ForwardParameter, + param::JuMP.VariableRef, +) + JuMP.check_belongs_to_model(param, model) + return MOI.get(model, attr, JuMP.index(param)) +end + function MOI.get(model::JuMP.Model, attr::ReverseObjectiveFunction) func = MOI.get(JuMP.backend(model), attr) return JuMP.jump_function(model, func) diff --git a/test/data/nlp_problems.jl b/test/data/nlp_problems.jl index c1f5760d..13c7e533 100644 --- a/test/data/nlp_problems.jl +++ b/test/data/nlp_problems.jl @@ -8,7 +8,7 @@ https://github.com/jump-dev/JuMP.jl/blob/301d46e81cb66c74c6e22cd89fb89ced740f157 =# ################################################ function create_nonlinear_jump_model(;ismin=true) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) @variable(model, p ∈ MOI.Parameter(1.0)) @variable(model, p2 ∈ MOI.Parameter(2.0)) @@ -33,7 +33,7 @@ From sIpopt paper: https://optimization-online.org/2011/04/3008/ ################################################ function create_nonlinear_jump_model_sipopt(;ismin = true) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) @variable(model, p1 ∈ MOI.Parameter(4.5)) @variable(model, p2 ∈ MOI.Parameter(1.0)) @@ -56,7 +56,7 @@ Simple Problems function create_jump_model_1(p_val = [1.5]) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters @@ -74,7 +74,7 @@ function create_jump_model_1(p_val = [1.5]) end function create_jump_model_2(p_val = [1.5]) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters @@ -91,7 +91,7 @@ function create_jump_model_2(p_val = [1.5]) end function create_jump_model_3(p_val = [-1.5]) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters @@ -109,7 +109,7 @@ function create_jump_model_3(p_val = [-1.5]) end function create_jump_model_4(p_val = [1.5]) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters @@ -127,7 +127,7 @@ function create_jump_model_4(p_val = [1.5]) end function create_jump_model_5(p_val = [1.5]) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters @@ -148,7 +148,7 @@ end h(y) = - sum(y .* log.(y)) softmax(x) = exp.(x) / sum(exp.(x)) function create_jump_model_6(p_a = collect(1.0:0.1:2.0)) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters @@ -168,7 +168,7 @@ function create_jump_model_6(p_a = collect(1.0:0.1:2.0)) end function create_jump_model_7(p_val = [1.5], g = sin) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters @@ -192,7 +192,7 @@ Non Linear Problems function create_nonlinear_jump_model_1(p_val = [1.0; 2.0; 100]; ismin = true) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters @@ -216,7 +216,7 @@ function create_nonlinear_jump_model_1(p_val = [1.0; 2.0; 100]; ismin = true) end function create_nonlinear_jump_model_2(p_val = [3.0; 2.0; 10]; ismin = true) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters @@ -240,7 +240,7 @@ function create_nonlinear_jump_model_2(p_val = [3.0; 2.0; 10]; ismin = true) end function create_nonlinear_jump_model_3(p_val = [3.0; 2.0; 10]; ismin = true) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters @@ -263,7 +263,7 @@ function create_nonlinear_jump_model_3(p_val = [3.0; 2.0; 10]; ismin = true) end function create_nonlinear_jump_model_4(p_val = [1.0; 2.0; 100]; ismin = true) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters @@ -288,7 +288,7 @@ function create_nonlinear_jump_model_4(p_val = [1.0; 2.0; 100]; ismin = true) end function create_nonlinear_jump_model_5(p_val = [1.0; 2.0; 100]; ismin = true) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters @@ -314,7 +314,7 @@ function create_nonlinear_jump_model_5(p_val = [1.0; 2.0; 100]; ismin = true) end function create_nonlinear_jump_model_6(p_val = [100.0; 200.0]; ismin = true) - model = Model(Ipopt.Optimizer) + model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters diff --git a/test/nlp_program.jl b/test/nlp_program.jl index 702fdc23..1e2f7f0a 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -107,7 +107,7 @@ function test_compute_derivatives_Analytical(DICT_PROBLEMS) set_parameter_value.(params, p_a) optimize!(model) @assert is_solved_and_feasible(model) - MOI.set.(model, DiffOpt.ForwardParameter(), params[1].index, Δp) + MOI.set.(model, DiffOpt.ForwardParameter(), params[1], Δp) DiffOpt.forward_differentiate!(model::Model; params=params) # Compute derivatives (Δs, sp_approx), evaluator, cons = compute_sensitivity(model, Δp; primal_vars, params) From b006b57c447eb0a5d2e78dec488c434202bd96d1 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 16 Dec 2024 16:58:06 -0500 Subject: [PATCH 07/41] update MOI wrapper --- src/NonLinearProgram/NonLinearProgram.jl | 195 ++++++++++++++++++++++- src/moi_wrapper.jl | 2 + 2 files changed, 190 insertions(+), 7 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 29eb5ac8..37c9d57f 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -26,6 +26,136 @@ Base.@kwdef struct ReverseCache dual_Δs_T::Matrix{Float64} end +mutable struct Form <: MOI.ModelLike + model::MOI.Nonlinear.Model + num_variables::Int + num_constraints::Int + sense::MOI.OptimizationSense + list_of_constraint::MOI.Utilities.DoubleDicts.IndexDoubleDict +end + +Form() = Form(MOI.Nonlinear.Model(), 0, 0, MOI.MIN_SENSE, MOI.Utilities.DoubleDicts.IndexDoubleDict()) + +function MOI.is_valid(model::Form, ref::MOI.VariableIndex) + return ref.value <= model.num_variables +end + +function MOI.is_valid(model::Form, ref::MOI.ConstraintIndex) + return ref.value <= model.num_constraints +end + +function MOI.add_variable(form::Form) + form.num_variables += 1 + return MOI.VariableIndex(form.num_variables) +end + +function MOI.add_variables(form::Form, n) + idxs = Vector{MOI.VariableIndex}(undef, n) + for i in 1:n + idxs[i] = MOI.add_variable(form) + end + return idxs +end + +function MOI.supports( + form::Form, + attribute, + val, +) + return MOI.supports(form.model, attribute, val) +end + +function MOI.supports_constraint( + ::Form, + ::Type{F}, + ::Type{S}, +) where { + F<:Union{MOI.ScalarNonlinearFunction, + MOI.ScalarQuadraticFunction{Float64}, + MOI.ScalarAffineFunction{Float64} + }, + S<:Union{ + MOI.GreaterThan{Float64}, + MOI.LessThan{Float64}, + MOI.Interval{Float64}, + MOI.EqualTo{Float64}, + } +} + return true +end + +function MOI.add_constraint( + form::Form, + func::F, + set::S +) where { + F<:Union{MOI.ScalarNonlinearFunction, + MOI.ScalarQuadraticFunction{Float64}, + MOI.ScalarAffineFunction{Float64} + }, + S<:Union{ + MOI.GreaterThan{Float64}, + MOI.LessThan{Float64}, + MOI.Interval{Float64}, + MOI.EqualTo{Float64}, + } +} + form.num_constraints += 1 + MOI.Nonlinear.add_constraint(form.model, func, set) + idx = MOI.ConstraintIndex{F, S}(form.num_constraints) + form.list_of_constraint[idx] = idx + return idx +end + +function MOI.get(form::Form, ::MOI.ListOfConstraintTypesPresent) + return collect(MOI.Utilities.DoubleDicts.outer_keys(form.list_of_constraint)) +end + +function MOI.get(form::Form, ::MOI.NumberOfConstraints{F,S}) where {F,S} + return length(form.list_of_constraint[F,S]) +end + +function MOI.get(form::Form, ::MOI.ConstraintPrimalStart) + return +end + +function MOI.supports(::Form, ::MOI.ObjectiveSense) + return true +end + +# function MOI.supports(::Form, ::MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}) +# return true +# end + +function MOI.supports(::Form, ::MOI.ObjectiveFunction) + return true +end + +function MOI.set( + form::Form, + ::MOI.ObjectiveSense, + sense::MOI.OptimizationSense +) + form.sense = sense + return +end + +function MOI.get( + form::Form, + ::MOI.ObjectiveSense, +) + return form.sense +end + +function MOI.set( + form::Form, + ::MOI.ObjectiveFunction, + func #::MOI.ScalarNonlinearFunction +) + MOI.Nonlinear.set_objective(form.model, func) + return +end + """ DiffOpt.NonLinearProgram.Model <: DiffOpt.AbstractModel @@ -35,20 +165,68 @@ Supports forward and reverse differentiation, caching sensitivity data for primal variables, constraints, and bounds, excluding slack variables. """ mutable struct Model <: DiffOpt.AbstractModel - model::JuMP.Model # JuMP optimization model + model::Form cache::Union{Nothing, Cache} # Cache for evaluator and mappings forw_grad_cache::Union{Nothing, ForwCache} # Cache for forward sensitivity results back_grad_cache::Union{Nothing, ReverseCache} # Cache for reverse sensitivity results diff_time::Float64 + x::Vector{Float64} + y::Vector{Float64} + s::Vector{Float64} end -function Model(model::JuMP.Model) +function Model() return Model( - model, + Form(), nothing, nothing, nothing, NaN, + [], + [], + [], + ) +end + +function MOI.set( + model::Model, + ::MOI.ConstraintPrimalStart, + ci::MOI.ConstraintIndex, + value, +) + MOI.throw_if_not_valid(model, ci) + return DiffOpt._enlarge_set( + model.s, + ci.value, + value, + ) +end + +function MOI.set( + model::Model, + ::MOI.ConstraintDualStart, + ci::MOI.ConstraintIndex, + value, +) + MOI.throw_if_not_valid(model, ci) + return DiffOpt._enlarge_set( + model.y, + ci.value, + value, + ) +end + +function MOI.set( + model::Model, + ::MOI.VariablePrimalStart, + vi::MOI.VariableIndex, + value, +) + MOI.throw_if_not_valid(model, vi) + return DiffOpt._enlarge_set( + model.x, + vi.value, + value, ) end @@ -64,8 +242,9 @@ function MOI.empty!(model::Model) return end -function _cache_evaluator!(model::Model; params=sort(all_params(model.model), by=x -> x.index.value)) +function _cache_evaluator!(model::Model) # Retrieve and sort primal variables by index + params = params=sort(all_params(model.model), by=x -> x.index.value) primal_vars = sort(all_primal_vars(model.model), by=x -> x.index.value) num_primal = length(primal_vars) @@ -149,11 +328,13 @@ function DiffOpt.reverse_differentiate!(model::Model; params=nothing) Δvup = [MOI.get(model, DiffOpt.ReverseVariableDual(), MOI.UpperBoundRef(x)) for x in cache.primal_vars if has_upper_bound(x)] # Extract primal and dual sensitivities # TODO: multiply everyone together before indexing - primal_Δs_T = Δs[1:cache.num_primal, :]' * Δx - dual_Δs_T = Δs[cache.index_duals, :]' * [Δλ; Δvdown; Δvup] + dual_Δ = zeros(size(Δs, 2)) + dual_Δ[1:cache.num_primal] = Δx + dual_Δ[cache.index_duals] = [Δλ; Δvdown; Δvup] + Δp = Δs' * dual_Δ model.back_grad_cache = ReverseCache( - Δp=primal_Δs_T, # TODO: change + Δp=Δp, ) end return nothing diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index 7cce0dd2..04bc9a45 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -573,10 +573,12 @@ function _diff(model::Optimizer) if isnothing(model_constructor) model.diff = nothing for constructor in model.model_constructors + println("Trying $constructor") model.diff = _instantiate_with_bridges(constructor) try model.index_map = MOI.copy_to(model.diff, model.optimizer) catch err + @show err if err isa MOI.UnsupportedConstraint || err isa MOI.UnsupportedAttribute model.diff = nothing From 5ade750ea33d3ec9a7f9558aafbbcaf34b9885cd Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 17 Dec 2024 16:00:08 -0500 Subject: [PATCH 08/41] update code for DiffOpt API --- src/NonLinearProgram/NonLinearProgram.jl | 146 +++++++++++---- src/NonLinearProgram/nlp_utilities.jl | 224 ++++++++++++----------- src/moi_wrapper.jl | 2 - 3 files changed, 222 insertions(+), 150 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 37c9d57f..fe587763 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -5,15 +5,14 @@ import JuMP import MathOptInterface as MOI using SparseArrays -include("nlp_utilities.jl") - Base.@kwdef struct Cache - primal_vars::Vector{JuMP.VariableRef} # Sorted primal variables - dual_mapping::Dict{MOI.ConstraintIndex, Int} # Unified mapping for constraints and bounds - params::Vector{JuMP.VariableRef} # VariableRefs for parameters + primal_vars::Vector{MOI.VariableIndex} # Sorted primal variables + dual_mapping::Vector{Int} # Unified mapping for constraints and bounds + params::Vector{MOI.VariableIndex} # VariableRefs for parameters index_duals::Vector{Int} # Indices for dual variables + leq_locations::Vector{Int} # Locations of <= constraints + geq_locations::Vector{Int} # Locations of >= constraints evaluator # Cached evaluator for derivative computation - cons # Cached constraints from evaluator end Base.@kwdef struct ForwCache @@ -32,9 +31,24 @@ mutable struct Form <: MOI.ModelLike num_constraints::Int sense::MOI.OptimizationSense list_of_constraint::MOI.Utilities.DoubleDicts.IndexDoubleDict -end - -Form() = Form(MOI.Nonlinear.Model(), 0, 0, MOI.MIN_SENSE, MOI.Utilities.DoubleDicts.IndexDoubleDict()) + var2param::Dict{MOI.VariableIndex, MOI.Nonlinear.ParameterIndex} + upper_bounds::Dict{Int, Float64} + lower_bounds::Dict{Int, Float64} + constraint_upper_bounds::Dict{Int, MOI.ConstraintIndex} + constraint_lower_bounds::Dict{Int, MOI.ConstraintIndex} + constraints_2_nlp_index::Dict{MOI.ConstraintIndex, MOI.Nonlinear.ConstraintIndex} + nlp_index_2_constraint::Dict{MOI.Nonlinear.ConstraintIndex, MOI.ConstraintIndex} +end + +Form() = Form( + MOI.Nonlinear.Model(), 0, 0, MOI.MIN_SENSE, + MOI.Utilities.DoubleDicts.IndexDoubleDict(), + Dict{MOI.VariableIndex, MOI.Nonlinear.ParameterIndex}(), + Dict{Int, Float64}(), Dict{Int, Float64}(), + Dict{Int, MOI.ConstraintIndex}(), Dict{Int, MOI.ConstraintIndex}(), + Dict{MOI.ConstraintIndex, MOI.Nonlinear.ConstraintIndex}(), + Dict{MOI.Nonlinear.ConstraintIndex, MOI.ConstraintIndex}() +) function MOI.is_valid(model::Form, ref::MOI.VariableIndex) return ref.value <= model.num_variables @@ -72,13 +86,15 @@ function MOI.supports_constraint( ) where { F<:Union{MOI.ScalarNonlinearFunction, MOI.ScalarQuadraticFunction{Float64}, - MOI.ScalarAffineFunction{Float64} + MOI.ScalarAffineFunction{Float64}, + MOI.VariableIndex }, S<:Union{ MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, MOI.Interval{Float64}, MOI.EqualTo{Float64}, + MOI.Parameter{Float64} } } return true @@ -91,7 +107,7 @@ function MOI.add_constraint( ) where { F<:Union{MOI.ScalarNonlinearFunction, MOI.ScalarQuadraticFunction{Float64}, - MOI.ScalarAffineFunction{Float64} + MOI.ScalarAffineFunction{Float64}, }, S<:Union{ MOI.GreaterThan{Float64}, @@ -101,9 +117,47 @@ function MOI.add_constraint( } } form.num_constraints += 1 - MOI.Nonlinear.add_constraint(form.model, func, set) + idx_nlp = MOI.Nonlinear.add_constraint(form.model, func, set) idx = MOI.ConstraintIndex{F, S}(form.num_constraints) form.list_of_constraint[idx] = idx + form.constraints_2_nlp_index[idx] = idx_nlp + form.nlp_index_2_constraint[idx_nlp] = idx + return idx +end + +function MOI.add_constraint( + form::Form, + idx::F, + set::S +) where {F<:MOI.VariableIndex, S<:MOI.Parameter{Float64}} + form.num_constraints += 1 + p = MOI.Nonlinear.add_parameter(form.model, set.value) + form.var2param[idx] = p + idx = MOI.ConstraintIndex{F, S}(form.num_constraints) + return idx +end + +function MOI.add_constraint( + form::Form, + var_idx::F, + set::S +) where {F<:MOI.VariableIndex, S<:MOI.GreaterThan} + form.num_constraints += 1 + form.lower_bounds[var_idx.value] = set.lower + idx = MOI.ConstraintIndex{F, S}(form.num_constraints) + form.constraint_lower_bounds[var_idx.value] = idx + return idx +end + +function MOI.add_constraint( + form::Form, + var_idx::F, + set::S +) where {F<:MOI.VariableIndex, S<:MOI.LessThan} + form.num_constraints += 1 + form.upper_bounds[var_idx.value] = set.upper + idx = MOI.ConstraintIndex{F, S}(form.num_constraints) + form.constraint_upper_bounds[var_idx.value] = idx return idx end @@ -123,10 +177,6 @@ function MOI.supports(::Form, ::MOI.ObjectiveSense) return true end -# function MOI.supports(::Form, ::MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}) -# return true -# end - function MOI.supports(::Form, ::MOI.ObjectiveFunction) return true end @@ -188,6 +238,9 @@ function Model() ) end +objective_sense(form::Form) = form.sense +objective_sense(model::Model) = objective_sense(model.model) + function MOI.set( model::Model, ::MOI.ConstraintPrimalStart, @@ -242,41 +295,59 @@ function MOI.empty!(model::Model) return end +include("nlp_utilities.jl") + +all_variables(form::Form) = MOI.VariableIndex.(1:form.num_variables) +all_variables(model::Model) = all_variables(model.model) +all_params(form::Form) = keys(form.var2param) +all_params(model::Model) = all_params(model.model) +all_primal_vars(form::Form) = setdiff(all_variables(form), all_params(form)) +all_primal_vars(model::Model) = all_primal_vars(model.model) + +get_num_constraints(form::Form) = length(form.list_of_constraint) +get_num_constraints(model::Model) = get_num_constraints(model.model) +get_num_primal_vars(form::Form) = length(all_primal_vars(form)) +get_num_primal_vars(model::Model) = get_num_primal_vars(model.model) +get_num_params(form::Form) = length(all_params(form)) +get_num_params(model::Model) = get_num_params(model.model) + function _cache_evaluator!(model::Model) + form = model.model # Retrieve and sort primal variables by index - params = params=sort(all_params(model.model), by=x -> x.index.value) - primal_vars = sort(all_primal_vars(model.model), by=x -> x.index.value) + params = params=sort(all_params(form), by=x -> x.index.value) + primal_vars = sort(all_primal_vars(form), by=x -> x.index.value) num_primal = length(primal_vars) # Create evaluator and constraints - evaluator, cons = create_evaluator(model.model; x=[primal_vars; params]) - num_constraints = length(cons) + evaluator = create_evaluator(form) + num_constraints = get_num_constraints(form) # Analyze constraints and bounds - leq_locations, geq_locations = find_inequealities(cons) + leq_locations, geq_locations = find_inequealities(form) num_leq = length(leq_locations) num_geq = length(geq_locations) - has_up = findall(x -> has_upper_bound(x), primal_vars) - has_low = findall(x -> has_lower_bound(x), primal_vars) + has_up = sort(keys(form.upper_bounds)) + has_low = sort(keys(form.lower_bounds)) num_low = length(has_low) num_up = length(has_up) # Create unified dual mapping # TODO: This assumes that these are all possible constraints available. We should change to either a dict or a sparse array # TODO: Check that the variable equal to works - Perhaps use bridge to change from equal to <= and >= - dual_mapping = Vector{Int}(undef, num_constraints + num_low + num_up) - for (i, ci) in enumerate(cons) - dual_mapping[ci.index.value] = i + dual_mapping = Vector{Int}(undef, form.num_constraints) + for (ci, cni) in form.constraints_2_nlp_index + dual_mapping[ci.value] = cni.value end # Add bounds to dual mapping + offset = num_constraints for (i, var_idx) in enumerate(has_low) - lb = MOI.LowerBoundRef(primal_vars[var_idx]) - dual_mapping[lb.index] = num_constraints + i + # offset + i + dual_mapping[form.constraint_lower_bounds[var_idx].value] = offset + i end offset += num_low for (i, var_idx) in enumerate(has_up) - ub = MOI.UpperBoundRef(primal_vars[var_idx]) - dual_mapping[ub.index] = offset + i + # offset + i + dual_mapping[form.constraint_upper_bounds[var_idx].value] = offset + i end num_slacks = num_leq + num_geq @@ -288,20 +359,21 @@ function _cache_evaluator!(model::Model) dual_mapping=dual_mapping, params=params, index_duals=index_duals, + leq_locations=leq_locations, + geq_locations=geq_locations, evaluator=evaluator, - cons=cons, ) return model.cache end -function DiffOpt.forward_differentiate!(model::Model; params=nothing) +function DiffOpt.forward_differentiate!(model::Model) model.diff_time = @elapsed begin - cache = _cache_evaluator!(model; params=params) + cache = _cache_evaluator!(model) - Δp = [MOI.get(model, DiffOpt.ForwardParameter(), p.index) for p in cache.params] + Δp = [MOI.get(model, DiffOpt.ForwardParameter(), p) for p in cache.params] # Compute Jacobian - Δs = compute_sensitivity(cache.evaluator, cache.cons; primal_vars=cache.primal_vars, params=cache.params) + Δs = compute_sensitivity(model) # Extract primal and dual sensitivities primal_Δs = Δs[1:cache.num_primal, :] * Δp # Exclude slacks @@ -315,9 +387,9 @@ function DiffOpt.forward_differentiate!(model::Model; params=nothing) return nothing end -function DiffOpt.reverse_differentiate!(model::Model; params=nothing) +function DiffOpt.reverse_differentiate!(model::Model) model.diff_time = @elapsed begin - cache = _cache_evaluator!(model; params=params) + cache = _cache_evaluator!(model) # Compute Jacobian Δs = compute_sensitivity(cache.evaluator, cache.cons; primal_vars=cache.primal_vars, params=cache.params) diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 966ba8d6..7cd18d3b 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -3,22 +3,22 @@ Create a Nonlinear Programming (NLP) model from a JuMP model. """ -function create_nlp_model(model::JuMP.Model) - rows = Vector{JuMP.ConstraintRef}(undef, 0) - nlp = MOI.Nonlinear.Model() - for (F, S) in list_of_constraint_types(model) - if F <: JuMP.VariableRef && !(S <: MathOptInterface.EqualTo{Float64}) - continue # Skip variable bounds - end - for ci in all_constraints(model, F, S) - push!(rows, ci) - object = constraint_object(ci) - MOI.Nonlinear.add_constraint(nlp, object.func, object.set) - end - end - MOI.Nonlinear.set_objective(nlp, objective_function(model)) - return nlp, rows -end +# function create_nlp_model(model::JuMP.Model) +# rows = Vector{JuMP.ConstraintRef}(undef, 0) +# nlp = MOI.Nonlinear.Model() +# for (F, S) in list_of_constraint_types(model) +# if F <: JuMP.VariableRef && !(S <: MathOptInterface.EqualTo{Float64}) +# continue # Skip variable bounds +# end +# for ci in all_constraints(model, F, S) +# push!(rows, ci) +# object = constraint_object(ci) +# MOI.Nonlinear.add_constraint(nlp, object.func, object.set) +# end +# end +# MOI.Nonlinear.set_objective(nlp, objective_function(model)) +# return nlp, rows +# end """ fill_off_diagonal(H) @@ -47,15 +47,17 @@ sense_mult(x::Vector) = sense_mult(x[1]) Compute the optimal Hessian of the Lagrangian. """ -function compute_optimal_hessian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{JuMP.ConstraintRef}, x::Vector{JuMP.VariableRef}) - sense_multiplier = objective_sense(owner_model(x[1])) == MOI.MIN_SENSE ? 1.0 : -1.0 +function compute_optimal_hessian(model::Model, rows::Vector{MOI.Nonlinear.ConstraintIndex}) + sense_multiplier = sense_mult(model) + evaluator = model.cache.evaluator + y = [model.y[model.form.nlp_index_2_constraint[row].value] for row in rows] hessian_sparsity = MOI.hessian_lagrangian_structure(evaluator) I = [i for (i, _) in hessian_sparsity] J = [j for (_, j) in hessian_sparsity] V = zeros(length(hessian_sparsity)) # The signals are being sdjusted to match the Ipopt convention (inner.mult_g) # but we don't know if we need to adjust the objective function multiplier - MOI.eval_hessian_lagrangian(evaluator, V, value.(x), 1.0, - sense_multiplier * dual.(rows)) + MOI.eval_hessian_lagrangian(evaluator, V, model.x, 1.0, - sense_multiplier * y) H = SparseArrays.sparse(I, J, V, length(x), length(x)) return fill_off_diagonal(H) end @@ -65,13 +67,14 @@ end Compute the optimal Jacobian of the constraints. """ -function compute_optimal_jacobian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{JuMP.ConstraintRef}, x::Vector{JuMP.VariableRef}) +function compute_optimal_jacobian(model::Model, rows::Vector{MOI.Nonlinear.ConstraintIndex}) + evaluator = model.cache.evaluator jacobian_sparsity = MOI.jacobian_structure(evaluator) I = [i for (i, _) in jacobian_sparsity] J = [j for (_, j) in jacobian_sparsity] V = zeros(length(jacobian_sparsity)) - MOI.eval_constraint_jacobian(evaluator, V, value.(x)) - A = SparseArrays.sparse(I, J, V, length(rows), length(x)) + MOI.eval_constraint_jacobian(evaluator, V, model.x) + A = SparseArrays.sparse(I, J, V, length(rows), length(model.x)) return A end @@ -80,26 +83,26 @@ end Compute the optimal Hessian of the Lagrangian and Jacobian of the constraints. """ -function compute_optimal_hess_jac(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{JuMP.ConstraintRef}, x::Vector{JuMP.VariableRef}) - hessian = compute_optimal_hessian(evaluator, rows, x) - jacobian = compute_optimal_jacobian(evaluator, rows, x) +function compute_optimal_hess_jac(model::Model, rows::Vector{MOI.Nonlinear.ConstraintIndex}) + hessian = compute_optimal_hessian(model, rows) + jacobian = compute_optimal_jacobian(model, rows) return hessian, jacobian end -""" - all_primal_vars(model::JuMP.Model) +# """ +# all_primal_vars(model::JuMP.Model) -Get all the primal variables in the model. -""" -all_primal_vars(model::JuMP.Model) = filter(x -> !is_parameter(x), all_variables(model)) +# Get all the primal variables in the model. +# """ +# all_primal_vars(model::JuMP.Model) = filter(x -> !is_parameter(x), all_variables(model)) -""" - all_params(model::JuMP.Model) +# """ +# all_params(model::JuMP.Model) -Get all the parameters in the model. -""" -all_params(model::JuMP.Model) = filter(x -> is_parameter(x), all_variables(model)) +# Get all the parameters in the model. +# """ +# all_params(model::JuMP.Model) = filter(x -> is_parameter(x), all_variables(model)) """ create_evaluator(model::JuMP.Model; x=all_variables(model)) @@ -108,23 +111,28 @@ Create an evaluator for the model. """ JuMP.index(x::JuMP.Containers.DenseAxisArray) = index.(x).data -function create_evaluator(model::JuMP.Model; x=all_variables(model)) - nlp, rows = create_nlp_model(model) +# function create_evaluator(model::JuMP.Model; x=all_variables(model)) +# nlp, rows = create_nlp_model(model) +# backend = MOI.Nonlinear.SparseReverseMode() +# evaluator = MOI.Nonlinear.Evaluator(nlp, backend, vcat(index.(x)...)) +# MOI.initialize(evaluator, [:Hess, :Jac]) +# return evaluator, rows +# end + +function create_evaluator(form::Form) + nlp = form.model backend = MOI.Nonlinear.SparseReverseMode() - evaluator = MOI.Nonlinear.Evaluator(nlp, backend, vcat(index.(x)...)) + evaluator = MOI.Nonlinear.Evaluator(nlp, backend, MOI.VariableIndex.(1:form.num_variables)) MOI.initialize(evaluator, [:Hess, :Jac]) - return evaluator, rows + return evaluator end - -function is_less_inequality(con::JuMP.ConstraintRef) - set_type = typeof(MOI.get(owner_model(con), MOI.ConstraintSet(), con)) - return set_type <: MOI.LessThan +function is_less_inequality(::MOI.ConstraintIndex{F, MOI.LessThan}) where {F} + return true end -function is_greater_inequality(con::JuMP.ConstraintRef) - set_type = typeof(MOI.get(owner_model(con), MOI.ConstraintSet(), con)) - return set_type <: MOI.GreaterThan +function is_greater_inequality(::MOI.ConstraintIndex{F, MOI.GreaterThan}) where {F} + return true end """ @@ -132,15 +140,16 @@ end Find the indices of the inequality constraints. """ -function find_inequealities(cons::Vector{C}) where C<:JuMP.ConstraintRef - leq_locations = zeros(length(cons)) - geq_locations = zeros(length(cons)) - for i in 1:length(cons) - if is_less_inequality(cons[i]) - leq_locations[i] = true +function find_inequealities(model::Form) + num_cons = length(model.list_of_constraint) + leq_locations = zeros(num_cons) + geq_locations = zeros(num_cons) + for con in keys(model.list_of_constraint) + if is_less_inequality(con) + leq_locations[constraints_2_nlp_index[con].value] = true end - if is_greater_inequality(cons[i]) - geq_locations[i] = true + if is_greater_inequality(con) + geq_locations[constraints_2_nlp_index[con].value] = true end end return findall(x -> x ==1, leq_locations), findall(x -> x ==1, geq_locations) @@ -151,72 +160,74 @@ end Get the reference to the canonical function that is equivalent to the slack variable of the inequality constraint. """ -function get_slack_inequality(con::JuMP.ConstraintRef) - set_type = typeof(MOI.get(owner_model(con), MOI.ConstraintSet(), con)) - obj = constraint_object(con) - if set_type <: MOI.LessThan - # c(x) <= b --> slack = c(x) - b | slack <= 0 - return obj.func - obj.set.upper - end - # c(x) >= b --> slack = c(x) - b | slack >= 0 - return obj.func - obj.set.lower -end +# function get_slack_inequality(con::JuMP.ConstraintRef) +# set_type = typeof(MOI.get(owner_model(con), MOI.ConstraintSet(), con)) +# obj = constraint_object(con) +# if set_type <: MOI.LessThan +# # c(x) <= b --> slack = c(x) - b | slack <= 0 +# return obj.func - obj.set.upper +# end +# # c(x) >= b --> slack = c(x) - b | slack >= 0 +# return obj.func - obj.set.lower +# end """ compute_solution_and_bounds(primal_vars::Vector{JuMP.VariableRef}, cons::Vector{C}) where C<:JuMP.ConstraintRef Compute the solution and bounds of the primal variables. """ -function compute_solution_and_bounds(primal_vars::Vector{JuMP.VariableRef}, cons::Vector{C}) where {C<:JuMP.ConstraintRef} - sense_multiplier = sense_mult(primal_vars) - num_vars = length(primal_vars) - leq_locations, geq_locations = find_inequealities(cons) +function compute_solution_and_bounds(model::Model; tol=1e-6) + sense_multiplier = sense_mult(model) + num_vars = get_num_primal_vars(model) + form = model.form + leq_locations = model.cache.leq_locations + geq_locations = model.cache.geq_locations ineq_locations = vcat(geq_locations, leq_locations) num_leq = length(leq_locations) num_geq = length(geq_locations) num_ineq = num_leq + num_geq - slack_vars = [get_slack_inequality(cons[i]) for i in ineq_locations] - has_up = findall(x -> has_upper_bound(x), primal_vars) - has_low = findall(x -> has_lower_bound(x), primal_vars) - - # Primal solution - X = value.([primal_vars; slack_vars]) + has_up = sort(keys(form.upper_bounds)) + has_low = sort(keys(form.lower_bounds)) + cons = sort(keys(form.nlp_index_2_constraint), by=x->x.value) + # Primal solution: value.([primal_vars; slack_vars]) + X = [model.x; model.s] # value and dual of the lower bounds V_L = spzeros(num_vars+num_ineq) X_L = spzeros(num_vars+num_ineq) for (i, j) in enumerate(has_low) - V_L[i] = dual.(LowerBoundRef(primal_vars[j])) * sense_multiplier + V_L[i] = model.y[form.constraint_lower_bounds[j].value] * sense_multiplier + #dual.(LowerBoundRef(primal_vars[j])) * sense_multiplier # if sense_multiplier == 1.0 - V_L[i] <= -1e-6 && @info "Dual of lower bound must be positive" i V_L[i] + V_L[i] <= -tol && @info "Dual of lower bound must be positive" i V_L[i] else - V_L[i] >= 1e-6 && @info "Dual of lower bound must be negative" i V_L[i] + V_L[i] >= tol && @info "Dual of lower bound must be negative" i V_L[i] end # - X_L[i] = JuMP.lower_bound(primal_vars[j]) + X_L[i] = form.lower_bounds[j] end for (i, con) in enumerate(cons[geq_locations]) # By convention jump dual will allways be positive for geq constraints # but for ipopt it will be positive if min problem and negative if max problem - V_L[num_vars+i] = dual.(con) * (sense_multiplier) + V_L[num_vars+i] = model.y[form.nlp_index_2_constraint[con]] * sense_multiplier # if sense_multiplier == 1.0 - V_L[num_vars+i] <= -1e-6 && @info "Dual of geq constraint must be positive" i V_L[num_vars+i] + V_L[num_vars+i] <= -tol && @info "Dual of geq constraint must be positive" i V_L[num_vars+i] else - V_L[num_vars+i] >= 1e-6 && @info "Dual of geq constraint must be negative" i V_L[num_vars+i] + V_L[num_vars+i] >= tol && @info "Dual of geq constraint must be negative" i V_L[num_vars+i] end end # value and dual of the upper bounds V_U = spzeros(num_vars+num_ineq) X_U = spzeros(num_vars+num_ineq) for (i, j) in enumerate(has_up) - V_U[i] = dual.(UpperBoundRef(primal_vars[j])) * (- sense_multiplier) - # + V_U[i] = model.y[form.constraint_upper_bounds[j].value] * (- sense_multiplier) + # dual.(UpperBoundRef(primal_vars[j])) * (- sense_multiplier) if sense_multiplier == 1.0 - V_U[i] <= -1e-6 && @info "Dual of upper bound must be positive" i V_U[i] + V_U[i] <= -tol && @info "Dual of upper bound must be positive" i V_U[i] else - V_U[i] >= 1e-6 && @info "Dual of upper bound must be negative" i V_U[i] + V_U[i] >= tol && @info "Dual of upper bound must be negative" i V_U[i] end # X_U[i] = JuMP.upper_bound(primal_vars[j]) @@ -224,15 +235,15 @@ function compute_solution_and_bounds(primal_vars::Vector{JuMP.VariableRef}, cons for (i, con) in enumerate(cons[leq_locations]) # By convention jump dual will allways be negative for leq constraints # but for ipopt it will be positive if min problem and negative if max problem - V_U[num_vars+i] = dual.(con) * (- sense_multiplier) - # + V_U[num_vars+i] = model.y[form.nlp_index_2_constraint[con]] * (- sense_multiplier) + # dual.(con) * (- sense_multiplier) if sense_multiplier == 1.0 - V_U[num_vars+i] <= -1e-6 && @info "Dual of leq constraint must be positive" i V_U[num_vars+i] + V_U[num_vars+i] <= -tol && @info "Dual of leq constraint must be positive" i V_U[num_vars+i] else - V_U[num_vars+i] >= 1e-6 && @info "Dual of leq constraint must be negative" i V_U[num_vars+i] + V_U[num_vars+i] >= tol && @info "Dual of leq constraint must be negative" i V_U[num_vars+i] end end - return X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, vcat(has_up, collect(num_vars+num_geq+1:num_vars+num_geq+num_leq)), vcat(has_low, collect(num_vars+1:num_vars+num_geq)) + return X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, vcat(has_up, collect(num_vars+num_geq+1:num_vars+num_geq+num_leq)), vcat(has_low, collect(num_vars+1:num_vars+num_geq)), cons end """ @@ -244,19 +255,17 @@ end Build the M (KKT Jacobian w.r.t. solution) and N (KKT Jacobian w.r.t. parameters) matrices for the sensitivity analysis. """ -function build_M_N(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{JuMP.ConstraintRef}, - primal_vars::Vector{JuMP.VariableRef}, params::Vector{JuMP.VariableRef}, +function build_M_N(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, _X::AbstractVector, _V_L::AbstractVector, _X_L::AbstractVector, _V_U::AbstractVector, _X_U::AbstractVector, leq_locations::Vector{Z}, geq_locations::Vector{Z}, ineq_locations::Vector{Z}, has_up::Vector{Z}, has_low::Vector{Z} ) where {Z<:Integer} @assert all(x -> is_parameter(x), params) "All parameters must be parameters" # Setting - num_vars = length(primal_vars) - num_parms = length(params) - num_cons = length(cons) + num_vars = get_num_primal_vars(model) + num_parms = get_num_params(model) + num_cons = get_num_constraints(model) num_ineq = length(ineq_locations) - all_vars = [primal_vars; params] num_low = length(has_low) num_up = length(has_up) @@ -282,7 +291,7 @@ function build_M_N(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{JuMP.Constra end # Function Derivatives - hessian, jacobian = compute_optimal_hess_jac(evaluator, cons, all_vars) + hessian, jacobian = compute_optimal_hess_jac(model, cons) # Hessian of the lagrangian wrt the primal variables W = spzeros(num_vars + num_ineq, num_vars + num_ineq) @@ -388,13 +397,11 @@ end Compute the derivatives of the solution w.r.t. the parameters without accounting for active set changes. """ -function compute_derivatives_no_relax(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{JuMP.ConstraintRef}, - primal_vars::Vector{JuMP.VariableRef}, params::Vector{JuMP.VariableRef}, +function compute_derivatives_no_relax(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, _X::AbstractVector, _V_L::AbstractVector, _X_L::AbstractVector, _V_U::AbstractVector, _X_U::AbstractVector, leq_locations::Vector{Z}, geq_locations::Vector{Z}, ineq_locations::Vector{Z}, has_up::Vector{Z}, has_low::Vector{Z} ) where {Z<:Integer} - num_bounds = length(has_up) + length(has_low) - M, N = build_M_N(evaluator, cons, primal_vars, params, _X, _V_L, _X_L, _V_U, _X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low) + M, N = build_M_N(model, cons, _X, _V_L, _X_L, _V_U, _X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low) # Sesitivity of the solution (primal-dual_constraints-dual_bounds) w.r.t. the parameters K = inertia_corrector_factorization(M, length(primal_vars) + length(ineq_locations), length(cons)) # Factorization @@ -409,23 +416,18 @@ function compute_derivatives_no_relax(evaluator::MOI.Nonlinear.Evaluator, cons:: return ∂s, K, N end +sense_mult(model::Model) = objective_sense(model) == MOI.MIN_SENSE ? 1.0 : -1.0 + """ compute_sensitivity(model::JuMP.Model; primal_vars=all_primal_vars(model), params=all_params(model)) Compute the sensitivity of the solution given sensitivity of the parameters (Δp). """ -function compute_sensitivity(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{JuMP.ConstraintRef}; primal_vars=all_primal_vars(model), params=all_params(model), tol=1e-6 -) - ismin = sense_mult(primal_vars) == 1.0 - sense_multiplier = sense_mult(primal_vars) - num_cons = length(cons) - num_var = length(primal_vars) +function compute_sensitivity(model::Model, tol=1e-6) # Solution and bounds - X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low = compute_solution_and_bounds(primal_vars, cons) + X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low, cons = compute_solution_and_bounds(model; tol=tol) # Compute derivatives - num_w = length(X) - num_lower = length(has_low) # ∂s = [∂x; ∂λ; ∂ν_L; ∂ν_U] - ∂s, K, N = compute_derivatives_no_relax(evaluator, cons, primal_vars, params, X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low) + ∂s, K, N = compute_derivatives_no_relax(model, cons, X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low) return ∂s end diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index 04bc9a45..7cce0dd2 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -573,12 +573,10 @@ function _diff(model::Optimizer) if isnothing(model_constructor) model.diff = nothing for constructor in model.model_constructors - println("Trying $constructor") model.diff = _instantiate_with_bridges(constructor) try model.index_map = MOI.copy_to(model.diff, model.optimizer) catch err - @show err if err isa MOI.UnsupportedConstraint || err isa MOI.UnsupportedAttribute model.diff = nothing From ff1052f841f12fdd438f78ec2b5d96cf316c3d65 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 17 Dec 2024 17:52:44 -0500 Subject: [PATCH 09/41] working code --- src/NonLinearProgram/NonLinearProgram.jl | 35 ++++++++++------- src/NonLinearProgram/nlp_utilities.jl | 50 ++++++++++++++---------- src/diff_opt.jl | 4 +- src/moi_wrapper.jl | 15 +++++++ 4 files changed, 68 insertions(+), 36 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index fe587763..25e210ce 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -4,6 +4,7 @@ import DiffOpt import JuMP import MathOptInterface as MOI using SparseArrays +using LinearAlgebra Base.@kwdef struct Cache primal_vars::Vector{MOI.VariableIndex} # Sorted primal variables @@ -12,17 +13,19 @@ Base.@kwdef struct Cache index_duals::Vector{Int} # Indices for dual variables leq_locations::Vector{Int} # Locations of <= constraints geq_locations::Vector{Int} # Locations of >= constraints + has_up::Vector{Int} # Variables with upper bounds + has_low::Vector{Int} # Variables with lower bounds evaluator # Cached evaluator for derivative computation end Base.@kwdef struct ForwCache - primal_Δs::Matrix{Float64} # Sensitivity for primal variables (excluding slacks) - dual_Δs::Matrix{Float64} # Sensitivity for constraints and bounds (indexed by ConstraintIndex) + primal_Δs::Dict{MOI.VariableIndex, Float64} # Sensitivity for primal variables (indexed by VariableIndex) + dual_Δs::Vector{Float64} # Sensitivity for constraints and bounds (indexed by ConstraintIndex) end Base.@kwdef struct ReverseCache - primal_Δs_T::Matrix{Float64} - dual_Δs_T::Matrix{Float64} + primal_Δs_T::Vector{Float64} + dual_Δs_T::Vector{Float64} end mutable struct Form <: MOI.ModelLike @@ -220,6 +223,7 @@ mutable struct Model <: DiffOpt.AbstractModel forw_grad_cache::Union{Nothing, ForwCache} # Cache for forward sensitivity results back_grad_cache::Union{Nothing, ReverseCache} # Cache for reverse sensitivity results diff_time::Float64 + input_cache::DiffOpt.InputCache x::Vector{Float64} y::Vector{Float64} s::Vector{Float64} @@ -232,6 +236,7 @@ function Model() nothing, nothing, NaN, + DiffOpt.InputCache(), [], [], [], @@ -299,7 +304,7 @@ include("nlp_utilities.jl") all_variables(form::Form) = MOI.VariableIndex.(1:form.num_variables) all_variables(model::Model) = all_variables(model.model) -all_params(form::Form) = keys(form.var2param) +all_params(form::Form) = collect(keys(form.var2param)) all_params(model::Model) = all_params(model.model) all_primal_vars(form::Form) = setdiff(all_variables(form), all_params(form)) all_primal_vars(model::Model) = all_primal_vars(model.model) @@ -314,8 +319,8 @@ get_num_params(model::Model) = get_num_params(model.model) function _cache_evaluator!(model::Model) form = model.model # Retrieve and sort primal variables by index - params = params=sort(all_params(form), by=x -> x.index.value) - primal_vars = sort(all_primal_vars(form), by=x -> x.index.value) + params = params=sort(all_params(form), by=x -> x.value) + primal_vars = sort(all_primal_vars(form), by=x -> x.value) num_primal = length(primal_vars) # Create evaluator and constraints @@ -325,8 +330,8 @@ function _cache_evaluator!(model::Model) leq_locations, geq_locations = find_inequealities(form) num_leq = length(leq_locations) num_geq = length(geq_locations) - has_up = sort(keys(form.upper_bounds)) - has_low = sort(keys(form.lower_bounds)) + has_up = findall(i-> haskey(form.upper_bounds, i), primal_vars) + has_low = findall(i-> haskey(form.lower_bounds, i), primal_vars) num_low = length(has_low) num_up = length(has_up) @@ -361,6 +366,8 @@ function _cache_evaluator!(model::Model) index_duals=index_duals, leq_locations=leq_locations, geq_locations=geq_locations, + has_up=has_up, + has_low=has_low, evaluator=evaluator, ) return model.cache @@ -369,18 +376,17 @@ end function DiffOpt.forward_differentiate!(model::Model) model.diff_time = @elapsed begin cache = _cache_evaluator!(model) - - Δp = [MOI.get(model, DiffOpt.ForwardParameter(), p) for p in cache.params] + Δp = [model.input_cache.dp[i] for i in cache.params] # Compute Jacobian Δs = compute_sensitivity(model) # Extract primal and dual sensitivities - primal_Δs = Δs[1:cache.num_primal, :] * Δp # Exclude slacks + primal_Δs = Δs[1:length(model.cache.primal_vars), :] * Δp # Exclude slacks dual_Δs = Δs[cache.index_duals, :] * Δp # Includes constraints and bounds model.forw_grad_cache = ForwCache( - primal_Δs=primal_Δs, + primal_Δs=Dict(model.cache.primal_vars .=> primal_Δs), dual_Δs=dual_Δs, ) end @@ -420,8 +426,7 @@ function MOI.get( if model.forw_grad_cache === nothing error("Forward differentiation has not been performed yet.") end - idx = vi.value # Direct mapping via sorted primal variables - return model.forw_grad_cache.primal_Δs[idx, :] + return model.forw_grad_cache.primal_Δs[vi] end function MOI.get( diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 7cd18d3b..a7a03844 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -50,7 +50,7 @@ Compute the optimal Hessian of the Lagrangian. function compute_optimal_hessian(model::Model, rows::Vector{MOI.Nonlinear.ConstraintIndex}) sense_multiplier = sense_mult(model) evaluator = model.cache.evaluator - y = [model.y[model.form.nlp_index_2_constraint[row].value] for row in rows] + y = [model.y[model.model.nlp_index_2_constraint[row].value] for row in rows] hessian_sparsity = MOI.hessian_lagrangian_structure(evaluator) I = [i for (i, _) in hessian_sparsity] J = [j for (_, j) in hessian_sparsity] @@ -58,7 +58,8 @@ function compute_optimal_hessian(model::Model, rows::Vector{MOI.Nonlinear.Constr # The signals are being sdjusted to match the Ipopt convention (inner.mult_g) # but we don't know if we need to adjust the objective function multiplier MOI.eval_hessian_lagrangian(evaluator, V, model.x, 1.0, - sense_multiplier * y) - H = SparseArrays.sparse(I, J, V, length(x), length(x)) + num_vars = length(model.x) + H = SparseArrays.sparse(I, J, V, num_vars, num_vars) return fill_off_diagonal(H) end @@ -131,6 +132,14 @@ function is_less_inequality(::MOI.ConstraintIndex{F, MOI.LessThan}) where {F} return true end +function is_less_inequality(::MOI.ConstraintIndex{F, S}) where {F, S} + return false +end + +function is_greater_inequality(::MOI.ConstraintIndex{F, S}) where {F, S} + return false +end + function is_greater_inequality(::MOI.ConstraintIndex{F, MOI.GreaterThan}) where {F} return true end @@ -179,16 +188,16 @@ Compute the solution and bounds of the primal variables. function compute_solution_and_bounds(model::Model; tol=1e-6) sense_multiplier = sense_mult(model) num_vars = get_num_primal_vars(model) - form = model.form + form = model.model leq_locations = model.cache.leq_locations geq_locations = model.cache.geq_locations ineq_locations = vcat(geq_locations, leq_locations) num_leq = length(leq_locations) num_geq = length(geq_locations) num_ineq = num_leq + num_geq - has_up = sort(keys(form.upper_bounds)) - has_low = sort(keys(form.lower_bounds)) - cons = sort(keys(form.nlp_index_2_constraint), by=x->x.value) + has_up = model.cache.has_up + has_low = model.cache.has_low + cons = sort(collect(keys(form.nlp_index_2_constraint)), by=x->x.value) # Primal solution: value.([primal_vars; slack_vars]) X = [model.x; model.s] @@ -196,16 +205,16 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) V_L = spzeros(num_vars+num_ineq) X_L = spzeros(num_vars+num_ineq) for (i, j) in enumerate(has_low) - V_L[i] = model.y[form.constraint_lower_bounds[j].value] * sense_multiplier + V_L[j] = model.y[form.constraint_lower_bounds[j].value] * sense_multiplier #dual.(LowerBoundRef(primal_vars[j])) * sense_multiplier # if sense_multiplier == 1.0 - V_L[i] <= -tol && @info "Dual of lower bound must be positive" i V_L[i] + V_L[j] <= -tol && @info "Dual of lower bound must be positive" i V_L[j] else - V_L[i] >= tol && @info "Dual of lower bound must be negative" i V_L[i] + V_L[j] >= tol && @info "Dual of lower bound must be negative" i V_L[j] end # - X_L[i] = form.lower_bounds[j] + X_L[j] = form.lower_bounds[j] end for (i, con) in enumerate(cons[geq_locations]) # By convention jump dual will allways be positive for geq constraints @@ -222,25 +231,25 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) V_U = spzeros(num_vars+num_ineq) X_U = spzeros(num_vars+num_ineq) for (i, j) in enumerate(has_up) - V_U[i] = model.y[form.constraint_upper_bounds[j].value] * (- sense_multiplier) + V_U[j] = model.y[form.constraint_upper_bounds[j].value] * (- sense_multiplier) # dual.(UpperBoundRef(primal_vars[j])) * (- sense_multiplier) if sense_multiplier == 1.0 - V_U[i] <= -tol && @info "Dual of upper bound must be positive" i V_U[i] + V_U[j] <= -tol && @info "Dual of upper bound must be positive" i V_U[i] else - V_U[i] >= tol && @info "Dual of upper bound must be negative" i V_U[i] + V_U[j] >= tol && @info "Dual of upper bound must be negative" i V_U[i] end # - X_U[i] = JuMP.upper_bound(primal_vars[j]) + X_U[j] = form.upper_bounds[j] end for (i, con) in enumerate(cons[leq_locations]) # By convention jump dual will allways be negative for leq constraints # but for ipopt it will be positive if min problem and negative if max problem - V_U[num_vars+i] = model.y[form.nlp_index_2_constraint[con]] * (- sense_multiplier) + V_U[num_vars+num_geq+i] = model.y[form.nlp_index_2_constraint[con]] * (- sense_multiplier) # dual.(con) * (- sense_multiplier) if sense_multiplier == 1.0 - V_U[num_vars+i] <= -tol && @info "Dual of leq constraint must be positive" i V_U[num_vars+i] + V_U[num_vars+num_geq+i] <= -tol && @info "Dual of leq constraint must be positive" i V_U[num_vars+i] else - V_U[num_vars+i] >= tol && @info "Dual of leq constraint must be negative" i V_U[num_vars+i] + V_U[num_vars+num_geq+i] >= tol && @info "Dual of leq constraint must be negative" i V_U[num_vars+i] end end return X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, vcat(has_up, collect(num_vars+num_geq+1:num_vars+num_geq+num_leq)), vcat(has_low, collect(num_vars+1:num_vars+num_geq)), cons @@ -259,8 +268,6 @@ function build_M_N(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, _X::AbstractVector, _V_L::AbstractVector, _X_L::AbstractVector, _V_U::AbstractVector, _X_U::AbstractVector, leq_locations::Vector{Z}, geq_locations::Vector{Z}, ineq_locations::Vector{Z}, has_up::Vector{Z}, has_low::Vector{Z} ) where {Z<:Integer} - @assert all(x -> is_parameter(x), params) "All parameters must be parameters" - # Setting num_vars = get_num_primal_vars(model) num_parms = get_num_params(model) @@ -404,7 +411,10 @@ function compute_derivatives_no_relax(model::Model, cons::Vector{MOI.Nonlinear.C M, N = build_M_N(model, cons, _X, _V_L, _X_L, _V_U, _X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low) # Sesitivity of the solution (primal-dual_constraints-dual_bounds) w.r.t. the parameters - K = inertia_corrector_factorization(M, length(primal_vars) + length(ineq_locations), length(cons)) # Factorization + num_vars = get_num_primal_vars(model) + num_cons = get_num_constraints(model) + num_ineq = length(ineq_locations) + K = inertia_corrector_factorization(M, num_vars + num_ineq, num_cons) # Factorization if isnothing(K) return zeros(size(M, 1), size(N, 2)), K, N end diff --git a/src/diff_opt.jl b/src/diff_opt.jl index 2159b32c..56e5acf8 100644 --- a/src/diff_opt.jl +++ b/src/diff_opt.jl @@ -13,6 +13,7 @@ const MOIDD = MOI.Utilities.DoubleDicts Base.@kwdef mutable struct InputCache dx::Dict{MOI.VariableIndex,Float64} = Dict{MOI.VariableIndex,Float64}()# dz for QP + dp::Dict{MOI.VariableIndex,Float64} = Dict{MOI.VariableIndex,Float64}() # ds # dy #= [d\lambda, d\nu] for QP # FIXME Would it be possible to have a DoubleDict where the value depends @@ -29,6 +30,7 @@ end function Base.empty!(cache::InputCache) empty!(cache.dx) + empty!(cache.dp) empty!(cache.scalar_constraints) empty!(cache.vector_constraints) cache.objective = nothing @@ -365,7 +367,7 @@ function MOI.set( pi::MOI.VariableIndex, val, ) - model.input_cache.dx[pi] = val + model.input_cache.dp[pi] = val return end diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index 7cce0dd2..229dc05f 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -549,6 +549,11 @@ function forward_differentiate!(model::Optimizer) model.input_cache.vector_constraints[F, S], ) end + if model.input_cache.dp !== nothing + for (vi, value) in model.input_cache.dp + MOI.set(diff, ForwardParameter(), model.index_map[vi], value) + end + end return forward_differentiate!(diff) end @@ -670,6 +675,16 @@ function MOI.supports( return true end +function MOI.set( + model::Optimizer, + ::ForwardParameter, + vi::MOI.VariableIndex, + val, +) + model.input_cache.dp[vi] = val + return +end + function MOI.get( model::Optimizer, ::ReverseVariablePrimal, From 24fb23063e25dfb8a1bdbb3cd0ba6b93a813ec97 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 17 Dec 2024 18:02:03 -0500 Subject: [PATCH 10/41] usage example --- test/nlp_program.jl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/nlp_program.jl b/test/nlp_program.jl index 1e2f7f0a..4cffc6c0 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -5,6 +5,28 @@ using FiniteDiff include("test/data/nlp_problems.jl") +# Example usage +# using Revise +# using DiffOpt +# using JuMP +# using Ipopt +# using Test + +# model, vars, cons, params = create_jump_model_1() +# set_parameter_value.(params, [2.1]) +# JuMP.optimize!(model) + +# # d_iff = DiffOpt._diff(model.moi_backend.optimizer.model) + +# # set parameter pertubations +# MOI.set(model, DiffOpt.ForwardParameter(), params[1], 0.2) + +# # forward differentiate +# DiffOpt.forward_differentiate!(model) + +# # get sensitivities +# MOI.get(model, DiffOpt.ForwardVariablePrimal(), vars[1]) + ################################################ #= # Test JuMP Hessian and Jacobian From 56a4d1eab9c2fbe5229e72b6acd7569d83d69379 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 18 Dec 2024 16:11:07 -0500 Subject: [PATCH 11/41] add reverse diff --- src/NonLinearProgram/NonLinearProgram.jl | 62 ++++++++++++++++++------ src/NonLinearProgram/nlp_utilities.jl | 19 ++++---- src/diff_opt.jl | 28 +++++------ src/jump_moi_overloads.jl | 8 +-- src/moi_wrapper.jl | 38 +++++++++++++++ test/nlp_program.jl | 13 ++++- 6 files changed, 125 insertions(+), 43 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 25e210ce..b361aa21 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -15,7 +15,8 @@ Base.@kwdef struct Cache geq_locations::Vector{Int} # Locations of >= constraints has_up::Vector{Int} # Variables with upper bounds has_low::Vector{Int} # Variables with lower bounds - evaluator # Cached evaluator for derivative computation + evaluator::MOI.Nonlinear.Evaluator # Evaluator for the NLP + cons::Vector{MOI.Nonlinear.ConstraintIndex} # Constraints index for the NLP end Base.@kwdef struct ForwCache @@ -24,8 +25,7 @@ Base.@kwdef struct ForwCache end Base.@kwdef struct ReverseCache - primal_Δs_T::Vector{Float64} - dual_Δs_T::Vector{Float64} + Δp::Vector{Float64} # Sensitivity for parameters end mutable struct Form <: MOI.ModelLike @@ -358,6 +358,7 @@ function _cache_evaluator!(model::Model) num_slacks = num_leq + num_geq num_w = num_primal + num_slacks index_duals = [num_w+1:num_w+num_constraints; num_w+num_constraints+1:num_w+num_constraints+num_low; num_w+num_constraints+num_low+num_geq+1:num_w+num_constraints+num_low+num_geq+num_up] + cons = sort(collect(keys(form.nlp_index_2_constraint)), by=x->x.value) model.cache = Cache( primal_vars=primal_vars, @@ -369,6 +370,7 @@ function _cache_evaluator!(model::Model) has_up=has_up, has_low=has_low, evaluator=evaluator, + cons=cons, ) return model.cache end @@ -396,20 +398,50 @@ end function DiffOpt.reverse_differentiate!(model::Model) model.diff_time = @elapsed begin cache = _cache_evaluator!(model) + form = model.model # Compute Jacobian - Δs = compute_sensitivity(cache.evaluator, cache.cons; primal_vars=cache.primal_vars, params=cache.params) - - Δx = [MOI.get(model, DiffOpt.ReverseVariablePrimal(), x.index) for x in cache.primal_vars] - Δλ = [MOI.get(model, DiffOpt.ReverseConstraintDual(), c.index) for c in cache.cons] - Δvdown = [MOI.get(model, DiffOpt.ReverseVariableDual(), MOI.LowerBoundRef(x)) for x in cache.primal_vars if has_lower_bound(x)] - Δvup = [MOI.get(model, DiffOpt.ReverseVariableDual(), MOI.UpperBoundRef(x)) for x in cache.primal_vars if has_upper_bound(x)] + Δs = compute_sensitivity(model) + num_primal = length(cache.primal_vars) + Δx = zeros(num_primal) + # [model.input_cache.dx[i] for i in cache.primal_vars] + for (i, var_idx) in enumerate(cache.primal_vars) + if haskey(model.input_cache.dx, var_idx) + Δx[i] = model.input_cache.dx[var_idx] + end + end + # ReverseConstraintDual + num_constraints = length(cache.cons) + num_up = length(cache.has_up) + num_low = length(cache.has_low) + Δdual = zeros(num_constraints + num_up + num_low) + # Δdual[1:num_constraints] = [model.input_cache.dy[form.nlp_index_2_constraint[nlp_ci]] for nlp_ci in cache.cons] + # Δdual[num_constraints+1:num_constraints+num_low] = [model.input_cache.dy[form.constraint_lower_bounds[form.primal_vars[i]].value] for i in cache.has_low] + # Δdual[num_constraints+num_low+1:end] = [model.input_cache.dy[form.constraint_upper_bounds[form.primal_vars[i]].value] for i in cache.has_up] + for (i, ci) in enumerate(cache.cons) + idx = form.nlp_index_2_constraint[ci] + if haskey(model.input_cache.dy, idx) + Δdual[i] = model.input_cache.dy[idx] + end + end + for (i, var_idx) in enumerate(cache.has_low) + idx = form.constraint_lower_bounds[var_idx].value + if haskey(model.input_cache.dy, idx) + Δdual[num_constraints + i] = model.input_cache.dy[idx] + end + end + for (i, var_idx) in enumerate(cache.has_up) + idx = form.constraint_upper_bounds[var_idx].value + if haskey(model.input_cache.dy, idx) + Δdual[num_constraints + num_low + i] = model.input_cache.dy[idx] + end + end # Extract primal and dual sensitivities # TODO: multiply everyone together before indexing - dual_Δ = zeros(size(Δs, 2)) - dual_Δ[1:cache.num_primal] = Δx - dual_Δ[cache.index_duals] = [Δλ; Δvdown; Δvup] - Δp = Δs' * dual_Δ + Δw = zeros(size(Δs, 1)) + Δw[1:num_primal] = Δx + Δw[cache.index_duals] = Δdual + Δp = Δs' * Δw model.back_grad_cache = ReverseCache( Δp=Δp, @@ -451,8 +483,8 @@ function MOI.get( ::DiffOpt.ReverseParameter, pi::MOI.VariableIndex, ) - -return NaN + form = model.model + return model.back_grad_cache.Δp[form.var2param[pi].value] end end # module NonLinearProgram diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index a7a03844..792e6b2c 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -128,7 +128,7 @@ function create_evaluator(form::Form) return evaluator end -function is_less_inequality(::MOI.ConstraintIndex{F, MOI.LessThan}) where {F} +function is_less_inequality(::MOI.ConstraintIndex{F, S}) where {F, S<:MOI.LessThan} return true end @@ -140,7 +140,7 @@ function is_greater_inequality(::MOI.ConstraintIndex{F, S}) where {F, S} return false end -function is_greater_inequality(::MOI.ConstraintIndex{F, MOI.GreaterThan}) where {F} +function is_greater_inequality(::MOI.ConstraintIndex{F, S}) where {F, S<:MOI.GreaterThan} return true end @@ -155,10 +155,10 @@ function find_inequealities(model::Form) geq_locations = zeros(num_cons) for con in keys(model.list_of_constraint) if is_less_inequality(con) - leq_locations[constraints_2_nlp_index[con].value] = true + leq_locations[model.constraints_2_nlp_index[con].value] = true end if is_greater_inequality(con) - geq_locations[constraints_2_nlp_index[con].value] = true + geq_locations[model.constraints_2_nlp_index[con].value] = true end end return findall(x -> x ==1, leq_locations), findall(x -> x ==1, geq_locations) @@ -197,9 +197,10 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) num_ineq = num_leq + num_geq has_up = model.cache.has_up has_low = model.cache.has_low - cons = sort(collect(keys(form.nlp_index_2_constraint)), by=x->x.value) + cons = model.cache.cons #sort(collect(keys(form.nlp_index_2_constraint)), by=x->x.value) + model_cons = [form.nlp_index_2_constraint[con] for con in cons] # Primal solution: value.([primal_vars; slack_vars]) - X = [model.x; model.s] + X = [model.x; [model.s[con.value] for con in model_cons]] # value and dual of the lower bounds V_L = spzeros(num_vars+num_ineq) @@ -219,7 +220,7 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) for (i, con) in enumerate(cons[geq_locations]) # By convention jump dual will allways be positive for geq constraints # but for ipopt it will be positive if min problem and negative if max problem - V_L[num_vars+i] = model.y[form.nlp_index_2_constraint[con]] * sense_multiplier + V_L[num_vars+i] = model.y[form.nlp_index_2_constraint[con].value] * sense_multiplier # if sense_multiplier == 1.0 V_L[num_vars+i] <= -tol && @info "Dual of geq constraint must be positive" i V_L[num_vars+i] @@ -244,7 +245,7 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) for (i, con) in enumerate(cons[leq_locations]) # By convention jump dual will allways be negative for leq constraints # but for ipopt it will be positive if min problem and negative if max problem - V_U[num_vars+num_geq+i] = model.y[form.nlp_index_2_constraint[con]] * (- sense_multiplier) + V_U[num_vars+num_geq+i] = model.y[form.nlp_index_2_constraint[con].value] * (- sense_multiplier) # dual.(con) * (- sense_multiplier) if sense_multiplier == 1.0 V_U[num_vars+num_geq+i] <= -tol && @info "Dual of leq constraint must be positive" i V_U[num_vars+i] @@ -433,7 +434,7 @@ sense_mult(model::Model) = objective_sense(model) == MOI.MIN_SENSE ? 1.0 : -1.0 Compute the sensitivity of the solution given sensitivity of the parameters (Δp). """ -function compute_sensitivity(model::Model, tol=1e-6) +function compute_sensitivity(model::Model; tol=1e-6) # Solution and bounds X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low, cons = compute_solution_and_bounds(model; tol=tol) # Compute derivatives diff --git a/src/diff_opt.jl b/src/diff_opt.jl index 56e5acf8..2331a7b8 100644 --- a/src/diff_opt.jl +++ b/src/diff_opt.jl @@ -14,6 +14,7 @@ const MOIDD = MOI.Utilities.DoubleDicts Base.@kwdef mutable struct InputCache dx::Dict{MOI.VariableIndex,Float64} = Dict{MOI.VariableIndex,Float64}()# dz for QP dp::Dict{MOI.VariableIndex,Float64} = Dict{MOI.VariableIndex,Float64}() + dy::Dict{MOI.ConstraintIndex,Float64} = Dict{MOI.ConstraintIndex,Float64}() # ds # dy #= [d\lambda, d\nu] for QP # FIXME Would it be possible to have a DoubleDict where the value depends @@ -31,6 +32,7 @@ end function Base.empty!(cache::InputCache) empty!(cache.dx) empty!(cache.dp) + empty!(cache.dy) empty!(cache.scalar_constraints) empty!(cache.vector_constraints) cache.objective = nothing @@ -152,19 +154,6 @@ MOI.set(model, DiffOpt.ReverseConstraintDual(), x) """ struct ReverseConstraintDual <: MOI.AbstractConstraintAttribute end -""" - ReverseVariableDual <: MOI.AbstractVariableAttribute - -A `MOI.AbstractVariableAttribute` to set input data from reverse differentiation. - -For instance, to set the tangent of the variable of index `vi`, do the -following: -```julia -MOI.set(model, DiffOpt.ReverseVariableDual(), x) -``` -""" -struct ReverseVariableDual <: MOI.AbstractVariableAttribute end - """ ReverseParameter <: MOI.AbstractVariableAttribute @@ -192,9 +181,10 @@ For instance, to get the sensitivity of the dual of constraint of index `ci` wit MOI.get(model, DiffOpt.ForwardConstraintDual(), ci) ``` """ - struct ForwardConstraintDual <: MOI.AbstractConstraintAttribute end +MOI.is_set_by_optimize(::ForwardConstraintDual) = true + """ ReverseObjectiveFunction <: MOI.AbstractModelAttribute @@ -361,6 +351,16 @@ function MOI.set( return end +function MOI.set( + model::AbstractModel, + ::ReverseConstraintDual, + vi::MOI.ConstraintIndex, + val, +) + model.input_cache.dy[vi] = val + return +end + function MOI.set( model::AbstractModel, ::ForwardParameter, diff --git a/src/jump_moi_overloads.jl b/src/jump_moi_overloads.jl index c6113336..7ff4b364 100644 --- a/src/jump_moi_overloads.jl +++ b/src/jump_moi_overloads.jl @@ -41,11 +41,11 @@ end function MOI.get( model::JuMP.Model, - attr::ForwardParameter, - param::JuMP.VariableRef, + attr::ReverseParameter, + var_ref::JuMP.VariableRef, ) - JuMP.check_belongs_to_model(param, model) - return MOI.get(model, attr, JuMP.index(param)) + JuMP.check_belongs_to_model(var_ref, model) + return _moi_get_result(JuMP.backend(model), attr, JuMP.index(var_ref)) end function MOI.get(model::JuMP.Model, attr::ReverseObjectiveFunction) diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index 229dc05f..ae737cdb 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -659,6 +659,18 @@ function MOI.get( ) end +function MOI.get( + model::Optimizer, + attr::ReverseParameter, + vi::MOI.VariableIndex, +) + return MOI.get( + _checked_diff(model, attr, :reverse_differentiate!), + attr, + model.index_map[vi], + ) +end + function MOI.supports( ::Optimizer, ::ReverseVariablePrimal, @@ -685,6 +697,14 @@ function MOI.set( return end +function MOI.get( + model::Optimizer, + ::ForwardParameter, + vi::MOI.VariableIndex, +) + return get(model.input_cache.dp, vi, 0.0) +end + function MOI.get( model::Optimizer, ::ReverseVariablePrimal, @@ -703,6 +723,24 @@ function MOI.set( return end +function MOI.set( + model::Optimizer, + ::ReverseConstraintDual, + vi::MOI.ConstraintIndex, + val, +) + model.input_cache.dy[vi] = val + return +end + +function MOI.get( + model::Optimizer, + ::ReverseConstraintDual, + vi::MOI.ConstraintIndex, +) + return get(model.input_cache.dy, vi, 0.0) +end + function MOI.get( model::Optimizer, attr::ReverseConstraintFunction, diff --git a/test/nlp_program.jl b/test/nlp_program.jl index 4cffc6c0..a34b6dc5 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -16,7 +16,7 @@ include("test/data/nlp_problems.jl") # set_parameter_value.(params, [2.1]) # JuMP.optimize!(model) -# # d_iff = DiffOpt._diff(model.moi_backend.optimizer.model) +# ### Forward differentiation # # set parameter pertubations # MOI.set(model, DiffOpt.ForwardParameter(), params[1], 0.2) @@ -27,6 +27,17 @@ include("test/data/nlp_problems.jl") # # get sensitivities # MOI.get(model, DiffOpt.ForwardVariablePrimal(), vars[1]) +# ### Reverse differentiation + +# # set variable pertubations +# MOI.set(model, DiffOpt.ReverseVariablePrimal(), vars[1], 1.0) + +# # reverse differentiate +# DiffOpt.reverse_differentiate!(model) + +# # get sensitivities +# dp = MOI.get(model, DiffOpt.ReverseParameter(), params[1]) + ################################################ #= # Test JuMP Hessian and Jacobian From b9781ba031c5ffa7b0a5471a415e50cd4cd3d3c0 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 20 Dec 2024 14:13:07 -0500 Subject: [PATCH 12/41] update code --- src/NonLinearProgram/NonLinearProgram.jl | 25 +++++++++++-- src/NonLinearProgram/nlp_utilities.jl | 8 +++-- test/nlp_program.jl | 45 ++++++++++++------------ 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index b361aa21..84422d51 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -41,6 +41,8 @@ mutable struct Form <: MOI.ModelLike constraint_lower_bounds::Dict{Int, MOI.ConstraintIndex} constraints_2_nlp_index::Dict{MOI.ConstraintIndex, MOI.Nonlinear.ConstraintIndex} nlp_index_2_constraint::Dict{MOI.Nonlinear.ConstraintIndex, MOI.ConstraintIndex} + leq_values::Dict{MOI.ConstraintIndex, Float64} + geq_values::Dict{MOI.ConstraintIndex, Float64} end Form() = Form( @@ -50,7 +52,9 @@ Form() = Form( Dict{Int, Float64}(), Dict{Int, Float64}(), Dict{Int, MOI.ConstraintIndex}(), Dict{Int, MOI.ConstraintIndex}(), Dict{MOI.ConstraintIndex, MOI.Nonlinear.ConstraintIndex}(), - Dict{MOI.Nonlinear.ConstraintIndex, MOI.ConstraintIndex}() + Dict{MOI.Nonlinear.ConstraintIndex, MOI.ConstraintIndex}(), + Dict{MOI.ConstraintIndex, Float64}(), + Dict{MOI.ConstraintIndex, Float64}() ) function MOI.is_valid(model::Form, ref::MOI.VariableIndex) @@ -95,7 +99,7 @@ function MOI.supports_constraint( S<:Union{ MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, - MOI.Interval{Float64}, + # MOI.Interval{Float64}, MOI.EqualTo{Float64}, MOI.Parameter{Float64} } @@ -103,6 +107,20 @@ function MOI.supports_constraint( return true end +function add_leq_geq(form::Form, idx::MOI.ConstraintIndex, set::MOI.GreaterThan) + form.geq_values[idx] = set.lower + return +end + +function add_leq_geq(form::Form, idx::MOI.ConstraintIndex, set::MOI.LessThan) + form.leq_values[idx] = set.upper + return +end + +function add_leq_geq(::Form, ::MOI.ConstraintIndex, ::MOI.EqualTo) + return +end + function MOI.add_constraint( form::Form, func::F, @@ -115,13 +133,14 @@ function MOI.add_constraint( S<:Union{ MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, - MOI.Interval{Float64}, + # MOI.Interval{Float64}, MOI.EqualTo{Float64}, } } form.num_constraints += 1 idx_nlp = MOI.Nonlinear.add_constraint(form.model, func, set) idx = MOI.ConstraintIndex{F, S}(form.num_constraints) + add_leq_geq(form, idx, set) form.list_of_constraint[idx] = idx form.constraints_2_nlp_index[idx] = idx_nlp form.nlp_index_2_constraint[idx_nlp] = idx diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 792e6b2c..73a50c49 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -198,9 +198,13 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) has_up = model.cache.has_up has_low = model.cache.has_low cons = model.cache.cons #sort(collect(keys(form.nlp_index_2_constraint)), by=x->x.value) - model_cons = [form.nlp_index_2_constraint[con] for con in cons] # Primal solution: value.([primal_vars; slack_vars]) - X = [model.x; [model.s[con.value] for con in model_cons]] + model_cons_leq = [form.nlp_index_2_constraint[con] for con in cons[leq_locations]] + model_cons_geq = [form.nlp_index_2_constraint[con] for con in cons[geq_locations]] + s_leq = [model.s[con.value] for con in model_cons_leq] - [form.leq_values[con] for con in model_cons_leq] + s_geq = [model.s[con.value] for con in model_cons_geq] - [form.geq_values[con] for con in model_cons_geq] + primal_idx = [i.value for i in model.cache.primal_vars] + X = [model.x[primal_idx]; s_leq; s_geq] # value and dual of the lower bounds V_L = spzeros(num_vars+num_ineq) diff --git a/test/nlp_program.jl b/test/nlp_program.jl index a34b6dc5..326c90b2 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -75,16 +75,11 @@ function analytic_jacobian(x, p) return hcat(g_2_J, g_1_J)'[:,:] end -function test_create_evaluator(model, x) - @testset "Create NLP model" begin - nlp, rows = create_nlp_model(model) - @test nlp isa MOI.Nonlinear.Model - @test rows isa Vector{ConstraintRef} - end +function test_create_evaluator(nlp_model) @testset "Create Evaluator" begin - evaluator, rows = create_evaluator(model; x = x) - @test evaluator isa MOI.Nonlinear.Evaluator - @test rows isa Vector{ConstraintRef} + cache = DiffOpt.NonLinearProgram._cache_evaluator!(nlp_model) + @test cache.evaluator isa MOI.Nonlinear.Evaluator + @test cache.cons isa Vector{MOI.Nonlinear.ConstraintIndex} end end @@ -96,20 +91,22 @@ function test_compute_optimal_hess_jacobian() optimize!(model) @assert is_solved_and_feasible(model) # Create evaluator - test_create_evaluator(model, [x; params]) - evaluator, rows = create_evaluator(model; x = [x; params]) - # Compute Hessian and Jacobian - num_var = length(x) - full_hessian, full_jacobian = compute_optimal_hess_jac(evaluator, rows, [x; params]) - hessian = full_hessian[1:num_var, 1:num_var] + nlp_model = DiffOpt._diff(model.moi_backend.optimizer.model).model + test_create_evaluator(nlp_model) + cons = nlp_model.cache.cons + y = [nlp_model.y[nlp_model.model.nlp_index_2_constraint[row].value] for row in cons] + hessian, jacobian = DiffOpt.NonLinearProgram.compute_optimal_hess_jac(nlp_model, cons) # Check Hessian - @test all(hessian .≈ analytic_hessian(value.(x), 1.0, -dual.(cons), value.(params))) - # TODO: Test hessial of parameters + primal_idx = [i.value for i in nlp_model.cache.primal_vars] + params_idx = [i.value for i in nlp_model.cache.params] + @test all(isapprox(hessian[primal_idx,primal_idx], analytic_hessian(nlp_model.x[primal_idx], 1.0, -y, nlp_model.x[params_idx]); atol = 1)) # Check Jacobian - @test all(full_jacobian .≈ analytic_jacobian(value.(x), value.(params))) + @test all(isapprox(jacobian[:,[primal_idx; params_idx]], analytic_jacobian(nlp_model.x[primal_idx], nlp_model.x[params_idx]))) end end +test_compute_optimal_hess_jacobian() + ################################################ #= # Test Sensitivity through analytical @@ -122,7 +119,7 @@ end # ∂x/∂p = ∂g/∂p DICT_PROBLEMS_Analytical_no_cc = Dict( - "geq no impact" => (p_a=[1.5], Δp=[0.2], Δs_a=[0.0; -0.2; 0.0; 0.0; 0.0; 0.0; 0.0], model_generator=create_jump_model_1), + "geq no impact" => (p_a=[1.5], Δp=[0.2], Δx=[0.0], Δy=[0.0; 0.0], Δv=[], model_generator=create_jump_model_1), "geq impact" => (p_a=[2.1], Δp=[0.2], Δs_a=[0.2; 0.0; 0.2; 0.4; 0.0; 0.4; 0.0], model_generator=create_jump_model_1), "geq bound impact" => (p_a=[2.1], Δp=[0.2], Δs_a=[0.2; 0.0; 0.4; 0.0; 0.4], model_generator=create_jump_model_2), "leq no impact" => (p_a=[-1.5], Δp=[-0.2], Δs_a=[0.0; 0.2; 0.0; 0.0; 0.0; 0.0; 0.0], model_generator=create_jump_model_3), @@ -134,16 +131,18 @@ DICT_PROBLEMS_Analytical_no_cc = Dict( ) function test_compute_derivatives_Analytical(DICT_PROBLEMS) - @testset "Compute Derivatives Analytical: $problem_name" for (problem_name, (p_a, Δp, Δs_a, model_generator)) in DICT_PROBLEMS + @testset "Compute Derivatives Analytical: $problem_name" for (problem_name, (p_a, Δp, Δx, Δy, Δv, model_generator)) in DICT_PROBLEMS # OPT Problem model, primal_vars, cons, params = model_generator() set_parameter_value.(params, p_a) optimize!(model) @assert is_solved_and_feasible(model) - MOI.set.(model, DiffOpt.ForwardParameter(), params[1], Δp) - DiffOpt.forward_differentiate!(model::Model; params=params) + # Set pertubations + MOI.set.(model, DiffOpt.ForwardParameter(), params, Δp) # Compute derivatives - (Δs, sp_approx), evaluator, cons = compute_sensitivity(model, Δp; primal_vars, params) + DiffOpt.forward_differentiate!(model) + # test sensitivities primal_vars + @test all(isapprox.([MOI.get(model, DiffOpt.ForwardVariablePrimal(), var) for var in primal_vars], Δx; atol = 1e-4)) # Check sensitivities @test all(isapprox.(Δs[1:length(Δs_a)], Δs_a; atol = 1e-4)) end From 1e949965154195284e8154390c42debba6a5c53e Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 22 Dec 2024 18:05:17 -0500 Subject: [PATCH 13/41] update tests --- src/NonLinearProgram/NonLinearProgram.jl | 3 +- src/NonLinearProgram/nlp_utilities.jl | 11 ++++--- src/jump_moi_overloads.jl | 10 ++++++ src/moi_wrapper.jl | 13 ++++++++ test/nlp_program.jl | 39 ++++++++++++++++++++---- 5 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 84422d51..6fc1a71c 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -490,13 +490,12 @@ function MOI.get( end try idx = model.cache.dual_mapping[ci.value] + return model.forw_grad_cache.dual_Δs[idx] catch error("ConstraintIndex not found in dual mapping.") end - return model.forw_grad_cache.dual_Δs[idx, :] end -# TODO: get for the reverse mode function MOI.get( model::Model, ::DiffOpt.ReverseParameter, diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 73a50c49..2a9f53b9 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -304,10 +304,11 @@ function build_M_N(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, # Function Derivatives hessian, jacobian = compute_optimal_hess_jac(model, cons) - + primal_idx = [i.value for i in model.cache.primal_vars] + params_idx = [i.value for i in model.cache.params] # Hessian of the lagrangian wrt the primal variables W = spzeros(num_vars + num_ineq, num_vars + num_ineq) - W[1:num_vars, 1:num_vars] = hessian[1:num_vars, 1:num_vars] + W[1:num_vars, 1:num_vars] = hessian[primal_idx, primal_idx] # Jacobian of the constraints A = spzeros(num_cons, num_vars + num_ineq) # A is the Jacobian of: c(x) = b and c(x) <= b and c(x) >= b, possibly all mixed up. @@ -316,7 +317,7 @@ function build_M_N(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, # c(x) - b - su = 0, su <= 0 # c(x) - b - sl = 0, sl >= 0 # Jacobian of the constraints wrt the primal variables - A[:, 1:num_vars] = jacobian[:, 1:num_vars] + A[:, 1:num_vars] = jacobian[:, primal_idx] # Jacobian of the constraints wrt the slack variables for (i,j) in enumerate(geq_locations) A[j, num_vars+i] = -1 @@ -326,9 +327,9 @@ function build_M_N(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, end # Partial second derivative of the lagrangian wrt primal solution and parameters ∇ₓₚL = spzeros(num_vars + num_ineq, num_parms) - ∇ₓₚL[1:num_vars, :] = hessian[1:num_vars, num_vars+1:end] + ∇ₓₚL[1:num_vars, :] = hessian[primal_idx, params_idx] # Partial derivative of the equality constraintswith wrt parameters - ∇ₚC = jacobian[:, num_vars+1:end] + ∇ₚC = jacobian[:, params_idx] # M matrix # M = [ diff --git a/src/jump_moi_overloads.jl b/src/jump_moi_overloads.jl index 7ff4b364..714139f4 100644 --- a/src/jump_moi_overloads.jl +++ b/src/jump_moi_overloads.jl @@ -48,6 +48,16 @@ function MOI.get( return _moi_get_result(JuMP.backend(model), attr, JuMP.index(var_ref)) end +function MOI.get( + model::JuMP.Model, + attr::ForwardConstraintDual, + con_ref::JuMP.ConstraintRef, +) + JuMP.check_belongs_to_model(con_ref, model) + moi_func = MOI.get(JuMP.backend(model), attr, JuMP.index(con_ref)) + return JuMP.jump_function(model, moi_func) +end + function MOI.get(model::JuMP.Model, attr::ReverseObjectiveFunction) func = MOI.get(JuMP.backend(model), attr) return JuMP.jump_function(model, func) diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index ae737cdb..d8933c5a 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -671,6 +671,19 @@ function MOI.get( ) end +function MOI.get( + model::Optimizer, + attr::ForwardConstraintDual, + ci::MOI.ConstraintIndex, +) + return MOI.get( + _checked_diff(model, attr, :reverse_differentiate!), + attr, + model.index_map[ci], + ) + +end + function MOI.supports( ::Optimizer, ::ReverseVariablePrimal, diff --git a/test/nlp_program.jl b/test/nlp_program.jl index 326c90b2..e4f7ab14 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -119,8 +119,8 @@ test_compute_optimal_hess_jacobian() # ∂x/∂p = ∂g/∂p DICT_PROBLEMS_Analytical_no_cc = Dict( - "geq no impact" => (p_a=[1.5], Δp=[0.2], Δx=[0.0], Δy=[0.0; 0.0], Δv=[], model_generator=create_jump_model_1), - "geq impact" => (p_a=[2.1], Δp=[0.2], Δs_a=[0.2; 0.0; 0.2; 0.4; 0.0; 0.4; 0.0], model_generator=create_jump_model_1), + "geq no impact" => (p_a=[1.5], Δp=[0.2], Δx=[0.0], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_1), + "geq impact" => (p_a=[2.1], Δp=[0.2], Δx=[0.2], Δy=[0.4; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_1), "geq bound impact" => (p_a=[2.1], Δp=[0.2], Δs_a=[0.2; 0.0; 0.4; 0.0; 0.4], model_generator=create_jump_model_2), "leq no impact" => (p_a=[-1.5], Δp=[-0.2], Δs_a=[0.0; 0.2; 0.0; 0.0; 0.0; 0.0; 0.0], model_generator=create_jump_model_3), "leq impact" => (p_a=[-2.1], Δp=[-0.2], Δs_a=[-0.2; 0.0; -0.2], model_generator=create_jump_model_3), @@ -130,6 +130,20 @@ DICT_PROBLEMS_Analytical_no_cc = Dict( "geq impact max" => (p_a=[2.1], Δp=[0.2], Δs_a=[0.2; 0.0; 0.2], model_generator=create_jump_model_5), ) +using Revise +using DiffOpt +using JuMP +using Ipopt +using Test +include("test/data/nlp_problems.jl") +p_a=[2.1] +Δp=[0.2] +Δx=[0.2] +Δy=[0.4; 0.0] +Δvu=[] +Δvl=[] +model_generator=create_jump_model_1 + function test_compute_derivatives_Analytical(DICT_PROBLEMS) @testset "Compute Derivatives Analytical: $problem_name" for (problem_name, (p_a, Δp, Δx, Δy, Δv, model_generator)) in DICT_PROBLEMS # OPT Problem @@ -141,10 +155,23 @@ function test_compute_derivatives_Analytical(DICT_PROBLEMS) MOI.set.(model, DiffOpt.ForwardParameter(), params, Δp) # Compute derivatives DiffOpt.forward_differentiate!(model) - # test sensitivities primal_vars - @test all(isapprox.([MOI.get(model, DiffOpt.ForwardVariablePrimal(), var) for var in primal_vars], Δx; atol = 1e-4)) - # Check sensitivities - @test all(isapprox.(Δs[1:length(Δs_a)], Δs_a; atol = 1e-4)) + # Test sensitivities primal_vars + if !isempty(Δx) + @test all(isapprox.([MOI.get(model, DiffOpt.ForwardVariablePrimal(), var) for var in primal_vars], Δx; atol = 1e-4)) + end + # Test sensitivities cons + if !isempty(Δy) + @test all(isapprox.([MOI.get(model, DiffOpt.ForwardConstraintDual(), con) for con in cons], Δy; atol = 1e-4)) + end + # Test sensitivities dual vars + if !isempty(Δvu) #TODO: check sign + primal_vars_upper = [v for v in primal_vars if has_upper_bound(v)] + @test all(isapprox.([MOI.get(model, DiffOpt.ForwardConstraintDual(), UpperBoundRef(var)) for var in primal_vars_upper], Δvu; atol = 1e-4)) + end + if !isempty(Δvl) + primal_vars_lower = [v for v in primal_vars if has_lower_bound(v)] + @test all(isapprox.([MOI.get(model, DiffOpt.ForwardConstraintDual(), LowerBoundRef(var)) for var in primal_vars_lower], Δvl; atol = 1e-4)) + end end end From 48e7b19b6abd2d9e21f883edca036a93dd3be578 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 23 Dec 2024 13:04:06 -0500 Subject: [PATCH 14/41] update tests --- src/NonLinearProgram/NonLinearProgram.jl | 46 ++++++++++++++---------- src/NonLinearProgram/nlp_utilities.jl | 32 +++++++++++++---- test/nlp_program.jl | 34 ++++++------------ 3 files changed, 64 insertions(+), 48 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 6fc1a71c..90f3c105 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -167,6 +167,7 @@ function MOI.add_constraint( form.num_constraints += 1 form.lower_bounds[var_idx.value] = set.lower idx = MOI.ConstraintIndex{F, S}(form.num_constraints) + form.list_of_constraint[idx] = idx form.constraint_lower_bounds[var_idx.value] = idx return idx end @@ -179,6 +180,7 @@ function MOI.add_constraint( form.num_constraints += 1 form.upper_bounds[var_idx.value] = set.upper idx = MOI.ConstraintIndex{F, S}(form.num_constraints) + form.list_of_constraint[idx] = idx form.constraint_upper_bounds[var_idx.value] = idx return idx end @@ -191,7 +193,7 @@ function MOI.get(form::Form, ::MOI.NumberOfConstraints{F,S}) where {F,S} return length(form.list_of_constraint[F,S]) end -function MOI.get(form::Form, ::MOI.ConstraintPrimalStart) +function MOI.get(::Form, ::MOI.ConstraintPrimalStart) return end @@ -279,6 +281,19 @@ function MOI.set( ) end +function MOI.supports( + ::Model, + ::MOI.ConstraintDualStart, + ::Type{MOI.ConstraintIndex{MOI.VariableIndex,S}}, +) where {S<:Union{ + MOI.GreaterThan{Float64}, + MOI.LessThan{Float64}, + MOI.EqualTo{Float64}, + MOI.Interval{Float64}, + }} + return true +end + function MOI.set( model::Model, ::MOI.ConstraintDualStart, @@ -328,7 +343,7 @@ all_params(model::Model) = all_params(model.model) all_primal_vars(form::Form) = setdiff(all_variables(form), all_params(form)) all_primal_vars(model::Model) = all_primal_vars(model.model) -get_num_constraints(form::Form) = length(form.list_of_constraint) +get_num_constraints(form::Form) = length(form.constraints_2_nlp_index) get_num_constraints(model::Model) = get_num_constraints(model.model) get_num_primal_vars(form::Form) = length(all_primal_vars(form)) get_num_primal_vars(model::Model) = get_num_primal_vars(model.model) @@ -349,14 +364,12 @@ function _cache_evaluator!(model::Model) leq_locations, geq_locations = find_inequealities(form) num_leq = length(leq_locations) num_geq = length(geq_locations) - has_up = findall(i-> haskey(form.upper_bounds, i), primal_vars) - has_low = findall(i-> haskey(form.lower_bounds, i), primal_vars) + has_up = findall(i-> haskey(form.upper_bounds, i.value), primal_vars) + has_low = findall(i-> haskey(form.lower_bounds, i.value), primal_vars) num_low = length(has_low) num_up = length(has_up) # Create unified dual mapping - # TODO: This assumes that these are all possible constraints available. We should change to either a dict or a sparse array - # TODO: Check that the variable equal to works - Perhaps use bridge to change from equal to <= and >= dual_mapping = Vector{Int}(undef, form.num_constraints) for (ci, cni) in form.constraints_2_nlp_index dual_mapping[ci.value] = cni.value @@ -364,14 +377,14 @@ function _cache_evaluator!(model::Model) # Add bounds to dual mapping offset = num_constraints - for (i, var_idx) in enumerate(has_low) + for (i, var_idx) in enumerate(primal_vars[has_low]) # offset + i - dual_mapping[form.constraint_lower_bounds[var_idx].value] = offset + i + dual_mapping[form.constraint_lower_bounds[var_idx.value].value] = offset + i end offset += num_low - for (i, var_idx) in enumerate(has_up) + for (i, var_idx) in enumerate(primal_vars[has_up]) # offset + i - dual_mapping[form.constraint_upper_bounds[var_idx].value] = offset + i + dual_mapping[form.constraint_upper_bounds[var_idx.value].value] = offset + i end num_slacks = num_leq + num_geq @@ -423,7 +436,6 @@ function DiffOpt.reverse_differentiate!(model::Model) Δs = compute_sensitivity(model) num_primal = length(cache.primal_vars) Δx = zeros(num_primal) - # [model.input_cache.dx[i] for i in cache.primal_vars] for (i, var_idx) in enumerate(cache.primal_vars) if haskey(model.input_cache.dx, var_idx) Δx[i] = model.input_cache.dx[var_idx] @@ -434,29 +446,25 @@ function DiffOpt.reverse_differentiate!(model::Model) num_up = length(cache.has_up) num_low = length(cache.has_low) Δdual = zeros(num_constraints + num_up + num_low) - # Δdual[1:num_constraints] = [model.input_cache.dy[form.nlp_index_2_constraint[nlp_ci]] for nlp_ci in cache.cons] - # Δdual[num_constraints+1:num_constraints+num_low] = [model.input_cache.dy[form.constraint_lower_bounds[form.primal_vars[i]].value] for i in cache.has_low] - # Δdual[num_constraints+num_low+1:end] = [model.input_cache.dy[form.constraint_upper_bounds[form.primal_vars[i]].value] for i in cache.has_up] for (i, ci) in enumerate(cache.cons) idx = form.nlp_index_2_constraint[ci] if haskey(model.input_cache.dy, idx) Δdual[i] = model.input_cache.dy[idx] end end - for (i, var_idx) in enumerate(cache.has_low) - idx = form.constraint_lower_bounds[var_idx].value + for (i, var_idx) in enumerate(cache.primal_vars[cache.has_low]) + idx = form.constraint_lower_bounds[var_idx.value].value if haskey(model.input_cache.dy, idx) Δdual[num_constraints + i] = model.input_cache.dy[idx] end end - for (i, var_idx) in enumerate(cache.has_up) - idx = form.constraint_upper_bounds[var_idx].value + for (i, var_idx) in enumerate(cache.primal_vars[cache.has_up]) + idx = form.constraint_upper_bounds[var_idx.value].value if haskey(model.input_cache.dy, idx) Δdual[num_constraints + num_low + i] = model.input_cache.dy[idx] end end # Extract primal and dual sensitivities - # TODO: multiply everyone together before indexing Δw = zeros(size(Δs, 1)) Δw[1:num_primal] = Δx Δw[cache.index_duals] = Δdual diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 2a9f53b9..c3271c21 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -128,7 +128,10 @@ function create_evaluator(form::Form) return evaluator end -function is_less_inequality(::MOI.ConstraintIndex{F, S}) where {F, S<:MOI.LessThan} +function is_less_inequality(::MOI.ConstraintIndex{F, S}) where {F<:Union{MOI.ScalarNonlinearFunction, + MOI.ScalarQuadraticFunction{Float64}, + MOI.ScalarAffineFunction{Float64}, +}, S<:MOI.LessThan} return true end @@ -140,7 +143,10 @@ function is_greater_inequality(::MOI.ConstraintIndex{F, S}) where {F, S} return false end -function is_greater_inequality(::MOI.ConstraintIndex{F, S}) where {F, S<:MOI.GreaterThan} +function is_greater_inequality(::MOI.ConstraintIndex{F, S}) where {F<:Union{MOI.ScalarNonlinearFunction, + MOI.ScalarQuadraticFunction{Float64}, + MOI.ScalarAffineFunction{Float64}, +}, S<:MOI.GreaterThan} return true end @@ -197,6 +203,7 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) num_ineq = num_leq + num_geq has_up = model.cache.has_up has_low = model.cache.has_low + primal_vars = model.cache.primal_vars cons = model.cache.cons #sort(collect(keys(form.nlp_index_2_constraint)), by=x->x.value) # Primal solution: value.([primal_vars; slack_vars]) model_cons_leq = [form.nlp_index_2_constraint[con] for con in cons[leq_locations]] @@ -210,7 +217,7 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) V_L = spzeros(num_vars+num_ineq) X_L = spzeros(num_vars+num_ineq) for (i, j) in enumerate(has_low) - V_L[j] = model.y[form.constraint_lower_bounds[j].value] * sense_multiplier + V_L[j] = model.y[form.constraint_lower_bounds[primal_vars[j].value].value] * sense_multiplier #dual.(LowerBoundRef(primal_vars[j])) * sense_multiplier # if sense_multiplier == 1.0 @@ -219,7 +226,7 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) V_L[j] >= tol && @info "Dual of lower bound must be negative" i V_L[j] end # - X_L[j] = form.lower_bounds[j] + X_L[j] = form.lower_bounds[primal_vars[j].value] end for (i, con) in enumerate(cons[geq_locations]) # By convention jump dual will allways be positive for geq constraints @@ -236,7 +243,7 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) V_U = spzeros(num_vars+num_ineq) X_U = spzeros(num_vars+num_ineq) for (i, j) in enumerate(has_up) - V_U[j] = model.y[form.constraint_upper_bounds[j].value] * (- sense_multiplier) + V_U[j] = model.y[form.constraint_upper_bounds[primal_vars[j].value].value] * (- sense_multiplier) # dual.(UpperBoundRef(primal_vars[j])) * (- sense_multiplier) if sense_multiplier == 1.0 V_U[j] <= -tol && @info "Dual of upper bound must be positive" i V_U[i] @@ -244,7 +251,7 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) V_U[j] >= tol && @info "Dual of upper bound must be negative" i V_U[i] end # - X_U[j] = form.upper_bounds[j] + X_U[j] = form.upper_bounds[primal_vars[j].value] end for (i, con) in enumerate(cons[leq_locations]) # By convention jump dual will allways be negative for leq constraints @@ -445,5 +452,18 @@ function compute_sensitivity(model::Model; tol=1e-6) # Compute derivatives # ∂s = [∂x; ∂λ; ∂ν_L; ∂ν_U] ∂s, K, N = compute_derivatives_no_relax(model, cons, X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low) + ## Adjust signs based on JuMP convention + num_vars = get_num_primal_vars(model) + num_cons = get_num_constraints(model) + num_ineq = length(ineq_locations) + num_w = num_vars + num_ineq + num_lower = length(has_low) + sense_multiplier = sense_mult(model) + # Duals + ∂s[num_w+1:num_w+num_cons, :] *= -sense_multiplier + # Dual bounds lower + ∂s[num_w+num_cons+1:num_w+num_cons+num_lower, :] *= sense_multiplier + # Dual bounds upper + ∂s[num_w+num_cons+num_lower+1:end, :] *= -sense_multiplier return ∂s end diff --git a/test/nlp_program.jl b/test/nlp_program.jl index e4f7ab14..fb5b99bc 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -121,31 +121,17 @@ test_compute_optimal_hess_jacobian() DICT_PROBLEMS_Analytical_no_cc = Dict( "geq no impact" => (p_a=[1.5], Δp=[0.2], Δx=[0.0], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_1), "geq impact" => (p_a=[2.1], Δp=[0.2], Δx=[0.2], Δy=[0.4; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_1), - "geq bound impact" => (p_a=[2.1], Δp=[0.2], Δs_a=[0.2; 0.0; 0.4; 0.0; 0.4], model_generator=create_jump_model_2), - "leq no impact" => (p_a=[-1.5], Δp=[-0.2], Δs_a=[0.0; 0.2; 0.0; 0.0; 0.0; 0.0; 0.0], model_generator=create_jump_model_3), - "leq impact" => (p_a=[-2.1], Δp=[-0.2], Δs_a=[-0.2; 0.0; -0.2], model_generator=create_jump_model_3), - "leq no impact max" => (p_a=[2.1], Δp=[0.2], Δs_a=[0.0; -0.2; 0.0; 0.0; 0.0], model_generator=create_jump_model_4), - "leq impact max" => (p_a=[1.5], Δp=[0.2], Δs_a=[0.2; 0.0; 0.2], model_generator=create_jump_model_4), - "geq no impact max" => (p_a=[1.5], Δp=[0.2], Δs_a=[0.0; -0.2; 0.0; 0.0; 0.0], model_generator=create_jump_model_5), - "geq impact max" => (p_a=[2.1], Δp=[0.2], Δs_a=[0.2; 0.0; 0.2], model_generator=create_jump_model_5), + "geq bound impact" => (p_a=[2.1], Δp=[0.2], Δx=[0.2], Δy=[0.4], Δvu=[], Δvl=[0.0], model_generator=create_jump_model_2), + "leq no impact" => (p_a=[-1.5], Δp=[-0.2], Δx=[0.0], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_3), + "leq impact" => (p_a=[-2.1], Δp=[-0.2], Δx=[-0.2], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_3), + "leq no impact max" => (p_a=[2.1], Δp=[0.2], Δx=[0.0], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_4), + "leq impact max" => (p_a=[1.5], Δp=[0.2], Δx=[0.2], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_4), + "geq no impact max" => (p_a=[1.5], Δp=[0.2], Δx=[0.0], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_5), + "geq impact max" => (p_a=[2.1], Δp=[0.2], Δx=[0.2], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_5), ) -using Revise -using DiffOpt -using JuMP -using Ipopt -using Test -include("test/data/nlp_problems.jl") -p_a=[2.1] -Δp=[0.2] -Δx=[0.2] -Δy=[0.4; 0.0] -Δvu=[] -Δvl=[] -model_generator=create_jump_model_1 - function test_compute_derivatives_Analytical(DICT_PROBLEMS) - @testset "Compute Derivatives Analytical: $problem_name" for (problem_name, (p_a, Δp, Δx, Δy, Δv, model_generator)) in DICT_PROBLEMS + @testset "Compute Derivatives Analytical: $problem_name" for (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) @@ -164,7 +150,7 @@ function test_compute_derivatives_Analytical(DICT_PROBLEMS) @test all(isapprox.([MOI.get(model, DiffOpt.ForwardConstraintDual(), con) for con in cons], Δy; atol = 1e-4)) end # Test sensitivities dual vars - if !isempty(Δvu) #TODO: check sign + if !isempty(Δvu) primal_vars_upper = [v for v in primal_vars if has_upper_bound(v)] @test all(isapprox.([MOI.get(model, DiffOpt.ForwardConstraintDual(), UpperBoundRef(var)) for var in primal_vars_upper], Δvu; atol = 1e-4)) end @@ -175,6 +161,8 @@ function test_compute_derivatives_Analytical(DICT_PROBLEMS) end end +test_compute_derivatives_Analytical(DICT_PROBLEMS_Analytical_no_cc) + ################################################ #= # Test Sensitivity through finite differences From 501fe83a55c3fd77d8ab679e21442917408abc6d Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 23 Dec 2024 14:07:56 -0500 Subject: [PATCH 15/41] add forward_differentiate! tests --- src/NonLinearProgram/NonLinearProgram.jl | 15 +++ test/nlp_program.jl | 117 ++++------------------- 2 files changed, 35 insertions(+), 97 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 90f3c105..11761d0e 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -147,6 +147,21 @@ function MOI.add_constraint( return idx end +function MOI.add_constraint( + form::Form, + func::F, + set::S +) where {F<:MOI.VariableIndex, S<:MOI.EqualTo} + form.num_constraints += 1 + idx_nlp = MOI.Nonlinear.add_constraint(form.model, func, set) + idx = MOI.ConstraintIndex{F, S}(form.num_constraints) + add_leq_geq(form, idx, set) + form.list_of_constraint[idx] = idx + form.constraints_2_nlp_index[idx] = idx_nlp + form.nlp_index_2_constraint[idx_nlp] = idx + return idx +end + function MOI.add_constraint( form::Form, idx::F, diff --git a/test/nlp_program.jl b/test/nlp_program.jl index fb5b99bc..a2297240 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -169,71 +169,11 @@ test_compute_derivatives_Analytical(DICT_PROBLEMS_Analytical_no_cc) =# ################################################ -function eval_model_jump(model, primal_vars, cons, params, p_val) - set_parameter_value.(params, p_val) +function stack_solution(model, p_a, params, primal_vars, cons) + set_parameter_value.(params, p_a) optimize!(model) @assert is_solved_and_feasible(model) - return value.(primal_vars), dual.(cons), [dual.(LowerBoundRef(v)) for v in primal_vars if has_lower_bound(v)], [dual.(UpperBoundRef(v)) for v in primal_vars if has_upper_bound(v)] -end - -function stack_solution(cons, leq_locations, geq_locations, x, _λ, ν_L, ν_U) - ineq_locations = vcat(geq_locations, leq_locations) - return Float64[x; value.(get_slack_inequality.(cons[ineq_locations])); _λ; ν_L; _λ[geq_locations]; ν_U; _λ[leq_locations]] -end - -function print_wrong_sensitive(Δs, Δs_fd, primal_vars, cons, leq_locations, geq_locations) - ineq_locations = vcat(geq_locations, leq_locations) - println("Some sensitivities are not correct: \n") - # primal vars - num_primal_vars = length(primal_vars) - for (i, v) in enumerate(primal_vars) - if !isapprox(Δs[i], Δs_fd[i]; atol = 1e-6) - println("Primal var: ", v, " | Δs: ", Δs[i], " | Δs_fd: ", Δs_fd[i]) - end - end - # slack vars - num_slack_vars = length(ineq_locations) - num_w = num_slack_vars + num_primal_vars - for (i, c) in enumerate(cons[ineq_locations]) - if !isapprox(Δs[i + num_primal_vars], Δs_fd[i + num_primal_vars] ; atol = 1e-6) - println("Slack var: ", c, " | Δs: ", Δs[i + num_primal_vars], " | Δs_fd: ", Δs_fd[i + num_primal_vars]) - end - end - # dual vars - num_cons = length(cons) - for (i, c) in enumerate(cons) - if !isapprox(Δs[i + num_w], Δs_fd[i + num_w] ; atol = 1e-6) - println("Dual var: ", c, " | Δs: ", Δs[i + num_w], " | Δs_fd: ", Δs_fd[i + num_w]) - end - end - # dual lower bound primal vars - var_lower = [v for v in primal_vars if has_lower_bound(v)] - num_lower_bounds = length(var_lower) - for (i, v) in enumerate(var_lower) - if !isapprox(Δs[i + num_w + num_cons], Δs_fd[i + num_w + num_cons] ; atol = 1e-6) - lower_bound_ref = LowerBoundRef(v) - println("lower bound dual: ", lower_bound_ref, " | Δs: ", Δs[i + num_w + num_cons], " | Δs_fd: ", Δs_fd[i + num_w + num_cons]) - end - end - # dual lower bound slack vars - for (i, c) in enumerate(cons[geq_locations]) - if !isapprox(Δs[i + num_w + num_cons + num_lower_bounds], Δs_fd[i + num_w + num_cons + num_lower_bounds] ; atol = 1e-6) - println("lower bound slack dual: ", c, " | Δs: ", Δs[i + num_w + num_cons + num_lower_bounds], " | Δs_fd: ", Δs_fd[i + num_w + num_cons + num_lower_bounds]) - end - end - for (i, c) in enumerate(cons[leq_locations]) - if !isapprox(Δs[i + num_w + num_cons + num_lower_bounds + length(geq_locations)], Δs_fd[i + num_w + num_cons + num_lower_bounds + length(geq_locations)] ; atol = 1e-6) - println("upper bound slack dual: ", c, " | Δs: ", Δs[i + num_w + num_cons + num_lower_bounds + length(geq_locations)], " | Δs_fd: ", Δs_fd[i + num_w + num_cons + num_lower_bounds + length(geq_locations)]) - end - end - # dual upper bound primal vars - var_upper = [v for v in primal_vars if has_upper_bound(v)] - for (i, v) in enumerate(var_upper) - if !isapprox(Δs[i + num_w + num_cons + num_lower_bounds + num_slack_vars], Δs_fd[i + num_w + num_cons + num_lower_bounds + num_slack_vars] ; atol = 1e-6) - upper_bound_ref = UpperBoundRef(v) - println("upper bound dual: ", upper_bound_ref, " | Δs: ", Δs[i + num_w + num_cons + num_lower_bounds + num_slack_vars], " | Δs_fd: ", Δs_fd[i + num_w + num_cons + num_lower_bounds + num_slack_vars]) - end - end + return [value.(primal_vars); dual.(cons)] end DICT_PROBLEMS_no_cc = Dict( @@ -256,44 +196,27 @@ DICT_PROBLEMS_no_cc = Dict( "NLP_6" => (p_a=[100.0; 200.0], Δp=[0.2; 0.5], model_generator=create_nonlinear_jump_model_6), ) - -DICT_PROBLEMS_cc = Dict( - "QP_JuMP" => (p_a=[1.0; 2.0; 100.0], Δp=[-0.5; 0.5; 0.1], model_generator=create_nonlinear_jump_model), - "QP_sIpopt2" => (p_a=[5.0; 1.0], Δp=[-0.5; 0.0], model_generator=create_nonlinear_jump_model_sipopt), -) - function test_compute_derivatives_Finite_Diff(DICT_PROBLEMS, iscc=false) # @testset "Compute Derivatives: $problem_name" for (problem_name, (p_a, Δp, model_generator)) in DICT_PROBLEMS, ismin in [true, false] # OPT Problem model, primal_vars, cons, params = model_generator(;ismin=ismin) - eval_model_jump(model, primal_vars, cons, params, p_a) - println("$problem_name: ", model) + set_parameter_value.(params, p_a) + optimize!(model) + @assert is_solved_and_feasible(model) + # Set pertubations + MOI.set.(model, DiffOpt.ForwardParameter(), params, Δp) # Compute derivatives - # Δp = [0.001; 0.0; 0.0] - p_b = p_a .+ Δp - (Δs, sp_approx), evaluator, cons = compute_sensitivity(model, Δp; primal_vars, params) - leq_locations, geq_locations = find_inequealities(cons) - sa = stack_solution(cons, leq_locations, geq_locations, eval_model_jump(model, primal_vars, cons, params, p_a)...) - # Check derivatives using finite differences - ∂s_fd = FiniteDiff.finite_difference_jacobian((p) -> stack_solution(cons, leq_locations, geq_locations, eval_model_jump(model, primal_vars, cons, params, p)...), p_a) - Δs_fd = ∂s_fd * Δp - # actual solution - sp = stack_solution(cons, leq_locations, geq_locations, eval_model_jump(model, primal_vars, cons, params, p_b)...) - # Check sensitivities - num_important = length(primal_vars) + length(cons) - test_derivatives = all(isapprox.(Δs, Δs_fd; rtol = 1e-5, atol=1e-6)) - test_approx = all(isapprox.(sp[1:num_important], sp_approx[1:num_important]; rtol = 1e-5, atol=1e-6)) - if test_derivatives || (iscc && test_approx) - println("All sensitivities are correct") - elseif iscc && !test_approx - @show Δp - println("Fail Approximations") - print_wrong_sensitive(Δs, sp.-sa, primal_vars, cons, leq_locations, geq_locations) - else - @show Δp - print_wrong_sensitive(Δs, Δs_fd, primal_vars, cons, leq_locations, geq_locations) - end - println("--------------------") + DiffOpt.forward_differentiate!(model) + Δx = [MOI.get(model, DiffOpt.ForwardVariablePrimal(), var) for var in primal_vars] + Δy = [MOI.get(model, DiffOpt.ForwardConstraintDual(), con) for con in cons] + # Compute derivatives using finite differences + ∂s_fd = FiniteDiff.finite_difference_jacobian((p) -> stack_solution(model, p, params, primal_vars, cons), p_a) * Δp + # Test sensitivities primal_vars + @test all(isapprox.(Δx, ∂s_fd[1:length(primal_vars)]; atol = 1e-4)) + # Test sensitivities cons + @test all(isapprox.(Δy, ∂s_fd[length(primal_vars)+1:end]; atol = 1e-4)) end -end \ No newline at end of file +end + +test_compute_derivatives_Finite_Diff(DICT_PROBLEMS_no_cc) \ No newline at end of file From 28ec54fefa044b9fc220d2352a50a2fbf7388701 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 23 Dec 2024 15:08:16 -0500 Subject: [PATCH 16/41] add reverse_differentiate! tests --- Project.toml | 2 +- src/NonLinearProgram/NonLinearProgram.jl | 7 ++ src/NonLinearProgram/nlp_utilities.jl | 111 ++++++-------------- test/Project.toml | 1 + test/nlp_program.jl | 124 +++++++++++++++-------- 5 files changed, 122 insertions(+), 123 deletions(-) diff --git a/Project.toml b/Project.toml index d73a81b4..6108c122 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "DiffOpt" uuid = "930fe3bc-9c6b-11ea-2d94-6184641e85e7" authors = ["Akshay Sharma", "Mathieu Besançon", "Joaquim Dias Garcia", "Benoît Legat", "Oscar Dowson"] -version = "0.4.2" +version = "0.5.0" [deps] BlockDiagonals = "0a1fb500-61f7-11e9-3c65-f5ef3456f9f0" diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 11761d0e..36f0a237 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -1,3 +1,8 @@ +# Copyright (c) 2020: Andrew Rosemberg and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + module NonLinearProgram import DiffOpt @@ -28,6 +33,7 @@ Base.@kwdef struct ReverseCache Δp::Vector{Float64} # Sensitivity for parameters end +# Define the form of the NLP mutable struct Form <: MOI.ModelLike model::MOI.Nonlinear.Model num_variables::Int @@ -404,6 +410,7 @@ function _cache_evaluator!(model::Model) num_slacks = num_leq + num_geq num_w = num_primal + num_slacks + # Create index for dual variables index_duals = [num_w+1:num_w+num_constraints; num_w+num_constraints+1:num_w+num_constraints+num_low; num_w+num_constraints+num_low+num_geq+1:num_w+num_constraints+num_low+num_geq+num_up] cons = sort(collect(keys(form.nlp_index_2_constraint)), by=x->x.value) diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index c3271c21..4e623346 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -1,24 +1,7 @@ -""" - create_nlp_model(model::JuMP.Model) - -Create a Nonlinear Programming (NLP) model from a JuMP model. -""" -# function create_nlp_model(model::JuMP.Model) -# rows = Vector{JuMP.ConstraintRef}(undef, 0) -# nlp = MOI.Nonlinear.Model() -# for (F, S) in list_of_constraint_types(model) -# if F <: JuMP.VariableRef && !(S <: MathOptInterface.EqualTo{Float64}) -# continue # Skip variable bounds -# end -# for ci in all_constraints(model, F, S) -# push!(rows, ci) -# object = constraint_object(ci) -# MOI.Nonlinear.add_constraint(nlp, object.func, object.set) -# end -# end -# MOI.Nonlinear.set_objective(nlp, objective_function(model)) -# return nlp, rows -# end +# Copyright (c) 2020: Andrew Rosemberg and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. """ fill_off_diagonal(H) @@ -39,9 +22,6 @@ function fill_off_diagonal(H) return ret end -sense_mult(x) = JuMP.objective_sense(owner_model(x)) == MOI.MIN_SENSE ? 1.0 : -1.0 -sense_mult(x::Vector) = sense_mult(x[1]) - """ compute_optimal_hessian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{JuMP.ConstraintRef}, x::Vector{JuMP.VariableRef}) @@ -91,35 +71,11 @@ function compute_optimal_hess_jac(model::Model, rows::Vector{MOI.Nonlinear.Const return hessian, jacobian end -# """ -# all_primal_vars(model::JuMP.Model) - -# Get all the primal variables in the model. -# """ -# all_primal_vars(model::JuMP.Model) = filter(x -> !is_parameter(x), all_variables(model)) - -# """ -# all_params(model::JuMP.Model) - -# Get all the parameters in the model. -# """ -# all_params(model::JuMP.Model) = filter(x -> is_parameter(x), all_variables(model)) - """ - create_evaluator(model::JuMP.Model; x=all_variables(model)) + create_evaluator(form::Form) -Create an evaluator for the model. +Create the evaluator for the NLP. """ -JuMP.index(x::JuMP.Containers.DenseAxisArray) = index.(x).data - -# function create_evaluator(model::JuMP.Model; x=all_variables(model)) -# nlp, rows = create_nlp_model(model) -# backend = MOI.Nonlinear.SparseReverseMode() -# evaluator = MOI.Nonlinear.Evaluator(nlp, backend, vcat(index.(x)...)) -# MOI.initialize(evaluator, [:Hess, :Jac]) -# return evaluator, rows -# end - function create_evaluator(form::Form) nlp = form.model backend = MOI.Nonlinear.SparseReverseMode() @@ -128,6 +84,11 @@ function create_evaluator(form::Form) return evaluator end +""" + is_less_inequality(con::MOI.ConstraintIndex{F, S}) where {F, S} + +Check if the constraint is a less than inequality. +""" function is_less_inequality(::MOI.ConstraintIndex{F, S}) where {F<:Union{MOI.ScalarNonlinearFunction, MOI.ScalarQuadraticFunction{Float64}, MOI.ScalarAffineFunction{Float64}, @@ -143,6 +104,11 @@ function is_greater_inequality(::MOI.ConstraintIndex{F, S}) where {F, S} return false end +""" + is_greater_inequality(con::MOI.ConstraintIndex{F, S}) where {F, S} + +Check if the constraint is a greater than inequality. +""" function is_greater_inequality(::MOI.ConstraintIndex{F, S}) where {F<:Union{MOI.ScalarNonlinearFunction, MOI.ScalarQuadraticFunction{Float64}, MOI.ScalarAffineFunction{Float64}, @@ -171,23 +137,7 @@ function find_inequealities(model::Form) end """ - get_slack_inequality(con::JuMP.ConstraintRef) - -Get the reference to the canonical function that is equivalent to the slack variable of the inequality constraint. -""" -# function get_slack_inequality(con::JuMP.ConstraintRef) -# set_type = typeof(MOI.get(owner_model(con), MOI.ConstraintSet(), con)) -# obj = constraint_object(con) -# if set_type <: MOI.LessThan -# # c(x) <= b --> slack = c(x) - b | slack <= 0 -# return obj.func - obj.set.upper -# end -# # c(x) >= b --> slack = c(x) - b | slack >= 0 -# return obj.func - obj.set.lower -# end - -""" - compute_solution_and_bounds(primal_vars::Vector{JuMP.VariableRef}, cons::Vector{C}) where C<:JuMP.ConstraintRef + compute_solution_and_bounds(model::Model; tol=1e-6) Compute the solution and bounds of the primal variables. """ @@ -204,7 +154,7 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) has_up = model.cache.has_up has_low = model.cache.has_low primal_vars = model.cache.primal_vars - cons = model.cache.cons #sort(collect(keys(form.nlp_index_2_constraint)), by=x->x.value) + cons = model.cache.cons # Primal solution: value.([primal_vars; slack_vars]) model_cons_leq = [form.nlp_index_2_constraint[con] for con in cons[leq_locations]] model_cons_geq = [form.nlp_index_2_constraint[con] for con in cons[geq_locations]] @@ -268,11 +218,7 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) end """ - build_M_N(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{JuMP.ConstraintRef}, - primal_vars::Vector{JuMP.VariableRef}, params::Vector{JuMP.VariableRef}, - _X::Vector, _V_L::Vector, _X_L::Vector, _V_U::Vector, _X_U::Vector, ineq_locations::Vector{Z}, - has_up::Vector{Z}, has_low::Vector{Z} -) where {Z<:Integer} + build_M_N(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, _X::AbstractVector, _V_L::AbstractVector, _X_L::AbstractVector, _V_U::AbstractVector, _X_U::AbstractVector, leq_locations::Vector{Z}, geq_locations::Vector{Z}, ineq_locations::Vector{Z}, has_up::Vector{Z}, has_low::Vector{Z}) Build the M (KKT Jacobian w.r.t. solution) and N (KKT Jacobian w.r.t. parameters) matrices for the sensitivity analysis. """ @@ -367,6 +313,11 @@ function build_M_N(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, return M, N end +""" + inertia_corrector_factorization(M::SparseMatrixCSC, num_w, num_cons; st=1e-6, max_corrections=50) + +Inertia correction for the factorization of the KKT matrix. Sparse version. +""" function inertia_corrector_factorization(M::SparseMatrixCSC, num_w, num_cons; st=1e-6, max_corrections=50) # Factorization K = lu(M; check=false) @@ -390,6 +341,11 @@ function inertia_corrector_factorization(M::SparseMatrixCSC, num_w, num_cons; st return K end +""" + inertia_corrector_factorization(M::Matrix, num_w, num_cons; st=1e-6, max_corrections=50) + +Inertia correction for the factorization of the KKT matrix. Dense version. +""" function inertia_corrector_factorization(M; st=1e-6, max_corrections=50) num_c = 0 if cond(M) > 1/st @@ -409,11 +365,10 @@ function inertia_corrector_factorization(M; st=1e-6, max_corrections=50) end """ - compute_derivatives_no_relax(evaluator::MOI.Nonlinear.Evaluator, cons::Vector{JuMP.ConstraintRef}, - primal_vars::Vector{JuMP.VariableRef}, params::Vector{JuMP.VariableRef}, - _X::Vector, _V_L::Vector, _X_L::Vector, _V_U::Vector, _X_U::Vector, ineq_locations::Vector{Z}, + compute_derivatives_no_relax(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, + _X::AbstractVector, _V_L::AbstractVector, _X_L::AbstractVector, _V_U::AbstractVector, _X_U::AbstractVector, leq_locations::Vector{Z}, geq_locations::Vector{Z}, ineq_locations::Vector{Z}, has_up::Vector{Z}, has_low::Vector{Z} - ) where {Z<:Integer} + ) Compute the derivatives of the solution w.r.t. the parameters without accounting for active set changes. """ @@ -442,7 +397,7 @@ end sense_mult(model::Model) = objective_sense(model) == MOI.MIN_SENSE ? 1.0 : -1.0 """ - compute_sensitivity(model::JuMP.Model; primal_vars=all_primal_vars(model), params=all_params(model)) + compute_sensitivity(model::Model; tol=1e-6) Compute the sensitivity of the solution given sensitivity of the parameters (Δp). """ diff --git a/test/Project.toml b/test/Project.toml index fdb058f2..e7c5a5b8 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -16,6 +16,7 @@ SCS = "c946c3f1-0d1f-5ce8-9dea-7daa1f7e2d13" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +FiniteDiff = "6a86dc24-6348-571c-b903-95158fe2bd41" [compat] HiGHS = "1" diff --git a/test/nlp_program.jl b/test/nlp_program.jl index a2297240..1a48581d 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -1,42 +1,24 @@ +module TestNLPProgram + +using DiffOpt using JuMP using Ipopt using Test using FiniteDiff +import DelimitedFiles -include("test/data/nlp_problems.jl") - -# Example usage -# using Revise -# using DiffOpt -# using JuMP -# using Ipopt -# using Test - -# model, vars, cons, params = create_jump_model_1() -# set_parameter_value.(params, [2.1]) -# JuMP.optimize!(model) - -# ### Forward differentiation - -# # set parameter pertubations -# MOI.set(model, DiffOpt.ForwardParameter(), params[1], 0.2) - -# # forward differentiate -# DiffOpt.forward_differentiate!(model) - -# # get sensitivities -# MOI.get(model, DiffOpt.ForwardVariablePrimal(), vars[1]) - -# ### Reverse differentiation +include(joinpath(@__DIR__, "data/nlp_problems.jl")) -# # set variable pertubations -# MOI.set(model, DiffOpt.ReverseVariablePrimal(), vars[1], 1.0) - -# # reverse differentiate -# DiffOpt.reverse_differentiate!(model) - -# # get sensitivities -# dp = MOI.get(model, DiffOpt.ReverseParameter(), params[1]) +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$name", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end ################################################ #= @@ -75,7 +57,7 @@ function analytic_jacobian(x, p) return hcat(g_2_J, g_1_J)'[:,:] end -function test_create_evaluator(nlp_model) +function _test_create_evaluator(nlp_model) @testset "Create Evaluator" begin cache = DiffOpt.NonLinearProgram._cache_evaluator!(nlp_model) @test cache.evaluator isa MOI.Nonlinear.Evaluator @@ -92,7 +74,7 @@ function test_compute_optimal_hess_jacobian() @assert is_solved_and_feasible(model) # Create evaluator nlp_model = DiffOpt._diff(model.moi_backend.optimizer.model).model - test_create_evaluator(nlp_model) + _test_create_evaluator(nlp_model) cons = nlp_model.cache.cons y = [nlp_model.y[nlp_model.model.nlp_index_2_constraint[row].value] for row in cons] hessian, jacobian = DiffOpt.NonLinearProgram.compute_optimal_hess_jac(nlp_model, cons) @@ -105,8 +87,6 @@ function test_compute_optimal_hess_jacobian() end end -test_compute_optimal_hess_jacobian() - ################################################ #= # Test Sensitivity through analytical @@ -130,7 +110,7 @@ DICT_PROBLEMS_Analytical_no_cc = Dict( "geq impact max" => (p_a=[2.1], Δp=[0.2], Δx=[0.2], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_5), ) -function test_compute_derivatives_Analytical(DICT_PROBLEMS) +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 # OPT Problem model, primal_vars, cons, params = model_generator() @@ -161,8 +141,6 @@ function test_compute_derivatives_Analytical(DICT_PROBLEMS) end end -test_compute_derivatives_Analytical(DICT_PROBLEMS_Analytical_no_cc) - ################################################ #= # Test Sensitivity through finite differences @@ -196,9 +174,8 @@ DICT_PROBLEMS_no_cc = Dict( "NLP_6" => (p_a=[100.0; 200.0], Δp=[0.2; 0.5], model_generator=create_nonlinear_jump_model_6), ) -function test_compute_derivatives_Finite_Diff(DICT_PROBLEMS, iscc=false) - # @testset "Compute Derivatives: $problem_name" - for (problem_name, (p_a, Δp, model_generator)) in DICT_PROBLEMS, ismin in [true, false] +function test_compute_derivatives_Finite_Diff(;DICT_PROBLEMS=DICT_PROBLEMS_no_cc) + @testset "Compute Derivatives FiniteDiff: $problem_name" for (problem_name, (p_a, Δp, model_generator)) in DICT_PROBLEMS, ismin in [true, false] # OPT Problem model, primal_vars, cons, params = model_generator(;ismin=ismin) set_parameter_value.(params, p_a) @@ -219,4 +196,63 @@ function test_compute_derivatives_Finite_Diff(DICT_PROBLEMS, iscc=false) end end -test_compute_derivatives_Finite_Diff(DICT_PROBLEMS_no_cc) \ No newline at end of file +################################################ +#= +# Test Sensitivity through Reverse Mode +=# +################################################ + +# Copied from test/jump.jl and adapated for nlp interface +function test_differentiating_non_trivial_convex_qp_jump() + nz = 10 + nineq_le = 25 + neq = 10 + # read matrices from files + names = ["P", "q", "G", "h", "A", "b"] + matrices = [] + for name in names + filename = joinpath(@__DIR__, "data", "$name.txt") + push!(matrices, DelimitedFiles.readdlm(filename, ' ', Float64, '\n')) + end + Q, q, G, h, A, b = matrices + q = vec(q) + h = vec(h) + b = vec(b) + model = JuMP.Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) + MOI.set(model, MOI.Silent(), true) + @variable(model, x[1:nz]) + @variable(model, p_le[1:nineq_le] ∈ MOI.Parameter.(0.0)) + @variable(model, p_eq[1:neq] ∈ MOI.Parameter.(0.0)) + @objective(model, Min, x' * Q * x + q' * x) + @constraint(model, c_le, G * x .<= h + p_le) + @constraint(model, c_eq, A * x .== b + p_eq) + optimize!(model) + MOI.set.(model, DiffOpt.ReverseVariablePrimal(), x, 1.0) + # compute gradients + DiffOpt.reverse_differentiate!(model) + # read gradients from files + param_names = ["dP", "dq", "dG", "dh", "dA", "db"] + grads_actual = [] + for name in param_names + filename = joinpath(@__DIR__, "data", "$(name).txt") + push!( + grads_actual, + DelimitedFiles.readdlm(filename, ' ', Float64, '\n'), + ) + end + dh = grads_actual[4] + db = grads_actual[6] + + for (i, ci) in enumerate(c_le) + @test -dh[i] ≈ -MOI.get(model, DiffOpt.ReverseParameter(), p_le[i]) atol = 1e-2 rtol = 1e-2 + end + for (i, ci) in enumerate(c_eq) + @test -db[i] ≈ -MOI.get(model, DiffOpt.ReverseParameter(), p_eq[i]) atol = 1e-2 rtol = 1e-2 + end + + return +end + +end # module + +TestNLPProgram.runtests() \ No newline at end of file From d3563dd7f6130a2257241738014aa95c725266d2 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 23 Dec 2024 15:20:17 -0500 Subject: [PATCH 17/41] update docs --- docs/src/index.md | 8 ++++---- docs/src/manual.md | 13 ++++++++----- docs/src/usage.md | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index 0eb47b63..aece6932 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,10 +1,10 @@ # DiffOpt.jl -[DiffOpt.jl](https://github.com/jump-dev/DiffOpt.jl) is a package for differentiating convex optimization program ([JuMP.jl](https://github.com/jump-dev/JuMP.jl) or [MathOptInterface.jl](https://github.com/jump-dev/MathOptInterface.jl) models) with respect to program parameters. Note that this package does not contain any solver. +[DiffOpt.jl](https://github.com/jump-dev/DiffOpt.jl) is a package for differentiating convex and non-convex optimization program ([JuMP.jl](https://github.com/jump-dev/JuMP.jl) or [MathOptInterface.jl](https://github.com/jump-dev/MathOptInterface.jl) models) with respect to program parameters. Note that this package does not contain any solver. This package has two major backends, available via the `reverse_differentiate!` and `forward_differentiate!` methods, to differentiate models (quadratic or conic) with optimal solutions. !!! note - Currently supports *linear programs* (LP), *convex quadratic programs* (QP) and *convex conic programs* (SDP, SOCP, exponential cone constraints only). + Currently supports *linear programs* (LP), *convex quadratic programs* (QP), *convex conic programs* (SDP, SOCP, exponential cone constraints only), and *general nonlinear programs* (NLP). ## Installation @@ -16,8 +16,8 @@ DiffOpt can be installed through the Julia package manager: ## Why are Differentiable optimization problems important? -Differentiable optimization is a promising field of convex optimization and has many potential applications in game theory, control theory and machine learning (specifically deep learning - refer [this video](https://www.youtube.com/watch?v=NrcaNnEXkT8) for more). -Recent work has shown how to differentiate specific subclasses of convex optimization problems. But several applications remain unexplored (refer section 8 of this [really good thesis](https://github.com/bamos/thesis)). With the help of automatic differentiation, differentiable optimization can have a significant impact on creating end-to-end differentiable systems to model neural networks, stochastic processes, or a game. +Differentiable optimization is a promising field of constrained optimization and has many potential applications in game theory, control theory and machine learning (specifically deep learning - refer [this video](https://www.youtube.com/watch?v=NrcaNnEXkT8) for more). +Recent work has shown how to differentiate specific subclasses of constrained optimization problems. But several applications remain unexplored (refer section 8 of this [really good thesis](https://github.com/bamos/thesis)). With the help of automatic differentiation, differentiable optimization can have a significant impact on creating end-to-end differentiable systems to model neural networks, stochastic processes, or a game. ## Contributing diff --git a/docs/src/manual.md b/docs/src/manual.md index 40f68eb2..279c7820 100644 --- a/docs/src/manual.md +++ b/docs/src/manual.md @@ -1,9 +1,5 @@ # Manual -!!! note - As of now, this package only works for optimization models that can be written either in convex conic form or convex quadratic form. - - ## Supported objectives & constraints - scheme 1 For `QPTH`/`OPTNET` style backend, the package supports following `Function-in-Set` constraints: @@ -16,6 +12,12 @@ For `QPTH`/`OPTNET` style backend, the package supports following `Function-in-S | `ScalarAffineFunction` | `GreaterThan` | | `ScalarAffineFunction` | `LessThan` | | `ScalarAffineFunction` | `EqualTo` | +| `ScalarQuadraticFunction` | `GreaterThan` | +| `ScalarQuadraticFunction` | `LessThan` | +| `ScalarQuadraticFunction` | `EqualTo` | +| `ScalarNonlinearFunction` | `GreaterThan` | +| `ScalarNonlinearFunction` | `LessThan` | +| `ScalarNonlinearFunction` | `EqualTo` | and the following objective types: @@ -24,6 +26,7 @@ and the following objective types: | `VariableIndex` | | `ScalarAffineFunction` | | `ScalarQuadraticFunction` | +| `ScalarNonlinearFunction` | ## Supported objectives & constraints - scheme 2 @@ -71,7 +74,7 @@ DiffOpt requires taking projections and finding projection gradients of vectors ## Conic problem formulation !!! note - As of now, the package is using `SCS` geometric form for affine expressions in cones. + As of now, when defining a conic or convex quadratic problem, the package is using `SCS` geometric form for affine expressions in cones. Consider a convex conic optimization problem in its primal (P) and dual (D) forms: ```math diff --git a/docs/src/usage.md b/docs/src/usage.md index 2ad2aa5a..7a453983 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -56,3 +56,38 @@ MOI.set(model, DiffOpt.ForwardObjectiveFunction(), ones(2) ⋅ x) DiffOpt.forward_differentiate!(model) grad_x = MOI.get.(model, DiffOpt.ForwardVariablePrimal(), x) ``` + +3. To differentiate a general nonlinear program, we can use the `forward_differentiate!` method with perturbations in the objective function and constraints through perturbations in the problem parameters. For example, consider the following nonlinear program: +```julia +model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) +@variable(model, p ∈ MOI.Parameter(0.1)) +@variable(model, x >= p) +@variable(model, y >= 0) +@objective(model, Min, x^2 + y^2) +@constraint(model, con, x + y >= 1) + +# Solve +JuMP.optimize!(model) + +# Set parameter pertubations +MOI.set(model, DiffOpt.ForwardParameter(), params[1], 0.2) + +# forward differentiate +DiffOpt.forward_differentiate!(model) + +# Retrieve sensitivities +dx = MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) +dy = MOI.get(model, DiffOpt.ForwardVariablePrimal(), y) +``` + +or we can use the `reverse_differentiate!` method: +```julia +# Set Primal Pertubations +MOI.set(model, DiffOpt.ReverseVariablePrimal(), x, 1.0) + +# Reverse differentiation +DiffOpt.reverse_differentiate!(model) + +# Retrieve reverse sensitivities (example usage) +dp= MOI.get(model, DiffOpt.ReverseParameter(), p) +``` \ No newline at end of file From 7867ece73512de848bcb9a0befbe86fc7ed19bfd Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 23 Dec 2024 15:29:41 -0500 Subject: [PATCH 18/41] format --- src/NonLinearProgram/NonLinearProgram.jl | 224 ++++++++-------- src/NonLinearProgram/nlp_utilities.jl | 262 +++++++++++++----- src/moi_wrapper.jl | 15 +- test/data/nlp_problems.jl | 77 +++--- test/nlp_program.jl | 326 +++++++++++++++++++---- 5 files changed, 631 insertions(+), 273 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 36f0a237..f63d596d 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -25,7 +25,7 @@ Base.@kwdef struct Cache end Base.@kwdef struct ForwCache - primal_Δs::Dict{MOI.VariableIndex, Float64} # Sensitivity for primal variables (indexed by VariableIndex) + primal_Δs::Dict{MOI.VariableIndex,Float64} # Sensitivity for primal variables (indexed by VariableIndex) dual_Δs::Vector{Float64} # Sensitivity for constraints and bounds (indexed by ConstraintIndex) end @@ -40,30 +40,43 @@ mutable struct Form <: MOI.ModelLike num_constraints::Int sense::MOI.OptimizationSense list_of_constraint::MOI.Utilities.DoubleDicts.IndexDoubleDict - var2param::Dict{MOI.VariableIndex, MOI.Nonlinear.ParameterIndex} - upper_bounds::Dict{Int, Float64} - lower_bounds::Dict{Int, Float64} - constraint_upper_bounds::Dict{Int, MOI.ConstraintIndex} - constraint_lower_bounds::Dict{Int, MOI.ConstraintIndex} - constraints_2_nlp_index::Dict{MOI.ConstraintIndex, MOI.Nonlinear.ConstraintIndex} - nlp_index_2_constraint::Dict{MOI.Nonlinear.ConstraintIndex, MOI.ConstraintIndex} - leq_values::Dict{MOI.ConstraintIndex, Float64} - geq_values::Dict{MOI.ConstraintIndex, Float64} -end - -Form() = Form( - MOI.Nonlinear.Model(), 0, 0, MOI.MIN_SENSE, - MOI.Utilities.DoubleDicts.IndexDoubleDict(), - Dict{MOI.VariableIndex, MOI.Nonlinear.ParameterIndex}(), - Dict{Int, Float64}(), Dict{Int, Float64}(), - Dict{Int, MOI.ConstraintIndex}(), Dict{Int, MOI.ConstraintIndex}(), - Dict{MOI.ConstraintIndex, MOI.Nonlinear.ConstraintIndex}(), - Dict{MOI.Nonlinear.ConstraintIndex, MOI.ConstraintIndex}(), - Dict{MOI.ConstraintIndex, Float64}(), - Dict{MOI.ConstraintIndex, Float64}() -) + var2param::Dict{MOI.VariableIndex,MOI.Nonlinear.ParameterIndex} + upper_bounds::Dict{Int,Float64} + lower_bounds::Dict{Int,Float64} + constraint_upper_bounds::Dict{Int,MOI.ConstraintIndex} + constraint_lower_bounds::Dict{Int,MOI.ConstraintIndex} + constraints_2_nlp_index::Dict{ + MOI.ConstraintIndex, + MOI.Nonlinear.ConstraintIndex, + } + nlp_index_2_constraint::Dict{ + MOI.Nonlinear.ConstraintIndex, + MOI.ConstraintIndex, + } + leq_values::Dict{MOI.ConstraintIndex,Float64} + geq_values::Dict{MOI.ConstraintIndex,Float64} +end + +function Form() + return Form( + MOI.Nonlinear.Model(), + 0, + 0, + MOI.MIN_SENSE, + MOI.Utilities.DoubleDicts.IndexDoubleDict(), + Dict{MOI.VariableIndex,MOI.Nonlinear.ParameterIndex}(), + Dict{Int,Float64}(), + Dict{Int,Float64}(), + Dict{Int,MOI.ConstraintIndex}(), + Dict{Int,MOI.ConstraintIndex}(), + Dict{MOI.ConstraintIndex,MOI.Nonlinear.ConstraintIndex}(), + Dict{MOI.Nonlinear.ConstraintIndex,MOI.ConstraintIndex}(), + Dict{MOI.ConstraintIndex,Float64}(), + Dict{MOI.ConstraintIndex,Float64}(), + ) +end -function MOI.is_valid(model::Form, ref::MOI.VariableIndex) +function MOI.is_valid(model::Form, ref::MOI.VariableIndex) return ref.value <= model.num_variables end @@ -84,11 +97,7 @@ function MOI.add_variables(form::Form, n) return idxs end -function MOI.supports( - form::Form, - attribute, - val, -) +function MOI.supports(form::Form, attribute, val) return MOI.supports(form.model, attribute, val) end @@ -97,18 +106,19 @@ function MOI.supports_constraint( ::Type{F}, ::Type{S}, ) where { - F<:Union{MOI.ScalarNonlinearFunction, + F<:Union{ + MOI.ScalarNonlinearFunction, MOI.ScalarQuadraticFunction{Float64}, MOI.ScalarAffineFunction{Float64}, - MOI.VariableIndex + MOI.VariableIndex, }, S<:Union{ MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, # MOI.Interval{Float64}, MOI.EqualTo{Float64}, - MOI.Parameter{Float64} - } + MOI.Parameter{Float64}, + }, } return true end @@ -130,9 +140,10 @@ end function MOI.add_constraint( form::Form, func::F, - set::S + set::S, ) where { - F<:Union{MOI.ScalarNonlinearFunction, + F<:Union{ + MOI.ScalarNonlinearFunction, MOI.ScalarQuadraticFunction{Float64}, MOI.ScalarAffineFunction{Float64}, }, @@ -141,11 +152,11 @@ function MOI.add_constraint( MOI.LessThan{Float64}, # MOI.Interval{Float64}, MOI.EqualTo{Float64}, - } + }, } form.num_constraints += 1 idx_nlp = MOI.Nonlinear.add_constraint(form.model, func, set) - idx = MOI.ConstraintIndex{F, S}(form.num_constraints) + idx = MOI.ConstraintIndex{F,S}(form.num_constraints) add_leq_geq(form, idx, set) form.list_of_constraint[idx] = idx form.constraints_2_nlp_index[idx] = idx_nlp @@ -156,11 +167,11 @@ end function MOI.add_constraint( form::Form, func::F, - set::S -) where {F<:MOI.VariableIndex, S<:MOI.EqualTo} + set::S, +) where {F<:MOI.VariableIndex,S<:MOI.EqualTo} form.num_constraints += 1 idx_nlp = MOI.Nonlinear.add_constraint(form.model, func, set) - idx = MOI.ConstraintIndex{F, S}(form.num_constraints) + idx = MOI.ConstraintIndex{F,S}(form.num_constraints) add_leq_geq(form, idx, set) form.list_of_constraint[idx] = idx form.constraints_2_nlp_index[idx] = idx_nlp @@ -171,23 +182,23 @@ end function MOI.add_constraint( form::Form, idx::F, - set::S -) where {F<:MOI.VariableIndex, S<:MOI.Parameter{Float64}} + set::S, +) where {F<:MOI.VariableIndex,S<:MOI.Parameter{Float64}} form.num_constraints += 1 p = MOI.Nonlinear.add_parameter(form.model, set.value) form.var2param[idx] = p - idx = MOI.ConstraintIndex{F, S}(form.num_constraints) + idx = MOI.ConstraintIndex{F,S}(form.num_constraints) return idx end function MOI.add_constraint( form::Form, var_idx::F, - set::S -) where {F<:MOI.VariableIndex, S<:MOI.GreaterThan} + set::S, +) where {F<:MOI.VariableIndex,S<:MOI.GreaterThan} form.num_constraints += 1 form.lower_bounds[var_idx.value] = set.lower - idx = MOI.ConstraintIndex{F, S}(form.num_constraints) + idx = MOI.ConstraintIndex{F,S}(form.num_constraints) form.list_of_constraint[idx] = idx form.constraint_lower_bounds[var_idx.value] = idx return idx @@ -196,26 +207,28 @@ end function MOI.add_constraint( form::Form, var_idx::F, - set::S -) where {F<:MOI.VariableIndex, S<:MOI.LessThan} + set::S, +) where {F<:MOI.VariableIndex,S<:MOI.LessThan} form.num_constraints += 1 form.upper_bounds[var_idx.value] = set.upper - idx = MOI.ConstraintIndex{F, S}(form.num_constraints) + idx = MOI.ConstraintIndex{F,S}(form.num_constraints) form.list_of_constraint[idx] = idx form.constraint_upper_bounds[var_idx.value] = idx return idx end function MOI.get(form::Form, ::MOI.ListOfConstraintTypesPresent) - return collect(MOI.Utilities.DoubleDicts.outer_keys(form.list_of_constraint)) + return collect( + MOI.Utilities.DoubleDicts.outer_keys(form.list_of_constraint), + ) end function MOI.get(form::Form, ::MOI.NumberOfConstraints{F,S}) where {F,S} - return length(form.list_of_constraint[F,S]) + return length(form.list_of_constraint[F, S]) end function MOI.get(::Form, ::MOI.ConstraintPrimalStart) - return + return end function MOI.supports(::Form, ::MOI.ObjectiveSense) @@ -226,26 +239,19 @@ function MOI.supports(::Form, ::MOI.ObjectiveFunction) return true end -function MOI.set( - form::Form, - ::MOI.ObjectiveSense, - sense::MOI.OptimizationSense -) +function MOI.set(form::Form, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense) form.sense = sense return end -function MOI.get( - form::Form, - ::MOI.ObjectiveSense, -) +function MOI.get(form::Form, ::MOI.ObjectiveSense) return form.sense end function MOI.set( form::Form, ::MOI.ObjectiveFunction, - func #::MOI.ScalarNonlinearFunction + func, #::MOI.ScalarNonlinearFunction ) MOI.Nonlinear.set_objective(form.model, func) return @@ -261,9 +267,9 @@ for primal variables, constraints, and bounds, excluding slack variables. """ mutable struct Model <: DiffOpt.AbstractModel model::Form - cache::Union{Nothing, Cache} # Cache for evaluator and mappings - forw_grad_cache::Union{Nothing, ForwCache} # Cache for forward sensitivity results - back_grad_cache::Union{Nothing, ReverseCache} # Cache for reverse sensitivity results + cache::Union{Nothing,Cache} # Cache for evaluator and mappings + forw_grad_cache::Union{Nothing,ForwCache} # Cache for forward sensitivity results + back_grad_cache::Union{Nothing,ReverseCache} # Cache for reverse sensitivity results diff_time::Float64 input_cache::DiffOpt.InputCache x::Vector{Float64} @@ -295,23 +301,21 @@ function MOI.set( value, ) MOI.throw_if_not_valid(model, ci) - return DiffOpt._enlarge_set( - model.s, - ci.value, - value, - ) + return DiffOpt._enlarge_set(model.s, ci.value, value) end function MOI.supports( ::Model, ::MOI.ConstraintDualStart, ::Type{MOI.ConstraintIndex{MOI.VariableIndex,S}}, -) where {S<:Union{ +) where { + S<:Union{ MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, MOI.EqualTo{Float64}, MOI.Interval{Float64}, - }} + }, +} return true end @@ -322,11 +326,7 @@ function MOI.set( value, ) MOI.throw_if_not_valid(model, ci) - return DiffOpt._enlarge_set( - model.y, - ci.value, - value, - ) + return DiffOpt._enlarge_set(model.y, ci.value, value) end function MOI.set( @@ -336,11 +336,7 @@ function MOI.set( value, ) MOI.throw_if_not_valid(model, vi) - return DiffOpt._enlarge_set( - model.x, - vi.value, - value, - ) + return DiffOpt._enlarge_set(model.x, vi.value, value) end function MOI.is_empty(model::Model) @@ -374,8 +370,8 @@ get_num_params(model::Model) = get_num_params(model.model) function _cache_evaluator!(model::Model) form = model.model # Retrieve and sort primal variables by index - params = params=sort(all_params(form), by=x -> x.value) - primal_vars = sort(all_primal_vars(form), by=x -> x.value) + params = params = sort(all_params(form); by = x -> x.value) + primal_vars = sort(all_primal_vars(form); by = x -> x.value) num_primal = length(primal_vars) # Create evaluator and constraints @@ -385,8 +381,8 @@ function _cache_evaluator!(model::Model) leq_locations, geq_locations = find_inequealities(form) num_leq = length(leq_locations) num_geq = length(geq_locations) - has_up = findall(i-> haskey(form.upper_bounds, i.value), primal_vars) - has_low = findall(i-> haskey(form.lower_bounds, i.value), primal_vars) + has_up = findall(i -> haskey(form.upper_bounds, i.value), primal_vars) + has_low = findall(i -> haskey(form.lower_bounds, i.value), primal_vars) num_low = length(has_low) num_up = length(has_up) @@ -400,31 +396,37 @@ function _cache_evaluator!(model::Model) offset = num_constraints for (i, var_idx) in enumerate(primal_vars[has_low]) # offset + i - dual_mapping[form.constraint_lower_bounds[var_idx.value].value] = offset + i + dual_mapping[form.constraint_lower_bounds[var_idx.value].value] = + offset + i end offset += num_low for (i, var_idx) in enumerate(primal_vars[has_up]) # offset + i - dual_mapping[form.constraint_upper_bounds[var_idx.value].value] = offset + i + dual_mapping[form.constraint_upper_bounds[var_idx.value].value] = + offset + i end num_slacks = num_leq + num_geq num_w = num_primal + num_slacks # Create index for dual variables - index_duals = [num_w+1:num_w+num_constraints; num_w+num_constraints+1:num_w+num_constraints+num_low; num_w+num_constraints+num_low+num_geq+1:num_w+num_constraints+num_low+num_geq+num_up] - cons = sort(collect(keys(form.nlp_index_2_constraint)), by=x->x.value) - - model.cache = Cache( - primal_vars=primal_vars, - dual_mapping=dual_mapping, - params=params, - index_duals=index_duals, - leq_locations=leq_locations, - geq_locations=geq_locations, - has_up=has_up, - has_low=has_low, - evaluator=evaluator, - cons=cons, + index_duals = [ + num_w+1:num_w+num_constraints + num_w+num_constraints+1:num_w+num_constraints+num_low + num_w+num_constraints+num_low+num_geq+1:num_w+num_constraints+num_low+num_geq+num_up + ] + cons = sort(collect(keys(form.nlp_index_2_constraint)); by = x -> x.value) + + model.cache = Cache(; + primal_vars = primal_vars, + dual_mapping = dual_mapping, + params = params, + index_duals = index_duals, + leq_locations = leq_locations, + geq_locations = geq_locations, + has_up = has_up, + has_low = has_low, + evaluator = evaluator, + cons = cons, ) return model.cache end @@ -439,11 +441,11 @@ function DiffOpt.forward_differentiate!(model::Model) # Extract primal and dual sensitivities primal_Δs = Δs[1:length(model.cache.primal_vars), :] * Δp # Exclude slacks - dual_Δs = Δs[cache.index_duals, :] * Δp # Includes constraints and bounds + dual_Δs = Δs[cache.index_duals, :] * Δp # Includes constraints and bounds - model.forw_grad_cache = ForwCache( - primal_Δs=Dict(model.cache.primal_vars .=> primal_Δs), - dual_Δs=dual_Δs, + model.forw_grad_cache = ForwCache(; + primal_Δs = Dict(model.cache.primal_vars .=> primal_Δs), + dual_Δs = dual_Δs, ) end return nothing @@ -477,13 +479,13 @@ function DiffOpt.reverse_differentiate!(model::Model) for (i, var_idx) in enumerate(cache.primal_vars[cache.has_low]) idx = form.constraint_lower_bounds[var_idx.value].value if haskey(model.input_cache.dy, idx) - Δdual[num_constraints + i] = model.input_cache.dy[idx] + Δdual[num_constraints+i] = model.input_cache.dy[idx] end end for (i, var_idx) in enumerate(cache.primal_vars[cache.has_up]) idx = form.constraint_upper_bounds[var_idx.value].value if haskey(model.input_cache.dy, idx) - Δdual[num_constraints + num_low + i] = model.input_cache.dy[idx] + Δdual[num_constraints+num_low+i] = model.input_cache.dy[idx] end end # Extract primal and dual sensitivities @@ -492,9 +494,7 @@ function DiffOpt.reverse_differentiate!(model::Model) Δw[cache.index_duals] = Δdual Δp = Δs' * Δw - model.back_grad_cache = ReverseCache( - Δp=Δp, - ) + model.back_grad_cache = ReverseCache(; Δp = Δp) end return nothing end @@ -518,7 +518,7 @@ function MOI.get( if model.forw_grad_cache === nothing error("Forward differentiation has not been performed yet.") end - try + try idx = model.cache.dual_mapping[ci.value] return model.forw_grad_cache.dual_Δs[idx] catch diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 4e623346..18fe4fbf 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -27,7 +27,10 @@ end Compute the optimal Hessian of the Lagrangian. """ -function compute_optimal_hessian(model::Model, rows::Vector{MOI.Nonlinear.ConstraintIndex}) +function compute_optimal_hessian( + model::Model, + rows::Vector{MOI.Nonlinear.ConstraintIndex}, +) sense_multiplier = sense_mult(model) evaluator = model.cache.evaluator y = [model.y[model.model.nlp_index_2_constraint[row].value] for row in rows] @@ -37,7 +40,13 @@ function compute_optimal_hessian(model::Model, rows::Vector{MOI.Nonlinear.Constr V = zeros(length(hessian_sparsity)) # The signals are being sdjusted to match the Ipopt convention (inner.mult_g) # but we don't know if we need to adjust the objective function multiplier - MOI.eval_hessian_lagrangian(evaluator, V, model.x, 1.0, - sense_multiplier * y) + MOI.eval_hessian_lagrangian( + evaluator, + V, + model.x, + 1.0, + -sense_multiplier * y, + ) num_vars = length(model.x) H = SparseArrays.sparse(I, J, V, num_vars, num_vars) return fill_off_diagonal(H) @@ -48,7 +57,10 @@ end Compute the optimal Jacobian of the constraints. """ -function compute_optimal_jacobian(model::Model, rows::Vector{MOI.Nonlinear.ConstraintIndex}) +function compute_optimal_jacobian( + model::Model, + rows::Vector{MOI.Nonlinear.ConstraintIndex}, +) evaluator = model.cache.evaluator jacobian_sparsity = MOI.jacobian_structure(evaluator) I = [i for (i, _) in jacobian_sparsity] @@ -64,10 +76,13 @@ end Compute the optimal Hessian of the Lagrangian and Jacobian of the constraints. """ -function compute_optimal_hess_jac(model::Model, rows::Vector{MOI.Nonlinear.ConstraintIndex}) +function compute_optimal_hess_jac( + model::Model, + rows::Vector{MOI.Nonlinear.ConstraintIndex}, +) hessian = compute_optimal_hessian(model, rows) jacobian = compute_optimal_jacobian(model, rows) - + return hessian, jacobian end @@ -79,7 +94,11 @@ Create the evaluator for the NLP. function create_evaluator(form::Form) nlp = form.model backend = MOI.Nonlinear.SparseReverseMode() - evaluator = MOI.Nonlinear.Evaluator(nlp, backend, MOI.VariableIndex.(1:form.num_variables)) + evaluator = MOI.Nonlinear.Evaluator( + nlp, + backend, + MOI.VariableIndex.(1:form.num_variables), + ) MOI.initialize(evaluator, [:Hess, :Jac]) return evaluator end @@ -89,18 +108,24 @@ end Check if the constraint is a less than inequality. """ -function is_less_inequality(::MOI.ConstraintIndex{F, S}) where {F<:Union{MOI.ScalarNonlinearFunction, - MOI.ScalarQuadraticFunction{Float64}, - MOI.ScalarAffineFunction{Float64}, -}, S<:MOI.LessThan} +function is_less_inequality( + ::MOI.ConstraintIndex{F,S}, +) where { + F<:Union{ + MOI.ScalarNonlinearFunction, + MOI.ScalarQuadraticFunction{Float64}, + MOI.ScalarAffineFunction{Float64}, + }, + S<:MOI.LessThan, +} return true end -function is_less_inequality(::MOI.ConstraintIndex{F, S}) where {F, S} +function is_less_inequality(::MOI.ConstraintIndex{F,S}) where {F,S} return false end -function is_greater_inequality(::MOI.ConstraintIndex{F, S}) where {F, S} +function is_greater_inequality(::MOI.ConstraintIndex{F,S}) where {F,S} return false end @@ -109,10 +134,16 @@ end Check if the constraint is a greater than inequality. """ -function is_greater_inequality(::MOI.ConstraintIndex{F, S}) where {F<:Union{MOI.ScalarNonlinearFunction, - MOI.ScalarQuadraticFunction{Float64}, - MOI.ScalarAffineFunction{Float64}, -}, S<:MOI.GreaterThan} +function is_greater_inequality( + ::MOI.ConstraintIndex{F,S}, +) where { + F<:Union{ + MOI.ScalarNonlinearFunction, + MOI.ScalarQuadraticFunction{Float64}, + MOI.ScalarAffineFunction{Float64}, + }, + S<:MOI.GreaterThan, +} return true end @@ -133,7 +164,8 @@ function find_inequealities(model::Form) geq_locations[model.constraints_2_nlp_index[con].value] = true end end - return findall(x -> x ==1, leq_locations), findall(x -> x ==1, geq_locations) + return findall(x -> x == 1, leq_locations), + findall(x -> x == 1, geq_locations) end """ @@ -141,7 +173,7 @@ end Compute the solution and bounds of the primal variables. """ -function compute_solution_and_bounds(model::Model; tol=1e-6) +function compute_solution_and_bounds(model::Model; tol = 1e-6) sense_multiplier = sense_mult(model) num_vars = get_num_primal_vars(model) form = model.model @@ -156,24 +188,32 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) primal_vars = model.cache.primal_vars cons = model.cache.cons # Primal solution: value.([primal_vars; slack_vars]) - model_cons_leq = [form.nlp_index_2_constraint[con] for con in cons[leq_locations]] - model_cons_geq = [form.nlp_index_2_constraint[con] for con in cons[geq_locations]] - s_leq = [model.s[con.value] for con in model_cons_leq] - [form.leq_values[con] for con in model_cons_leq] - s_geq = [model.s[con.value] for con in model_cons_geq] - [form.geq_values[con] for con in model_cons_geq] + model_cons_leq = + [form.nlp_index_2_constraint[con] for con in cons[leq_locations]] + model_cons_geq = + [form.nlp_index_2_constraint[con] for con in cons[geq_locations]] + s_leq = + [model.s[con.value] for con in model_cons_leq] - [form.leq_values[con] for con in model_cons_leq] + s_geq = + [model.s[con.value] for con in model_cons_geq] - [form.geq_values[con] for con in model_cons_geq] primal_idx = [i.value for i in model.cache.primal_vars] X = [model.x[primal_idx]; s_leq; s_geq] # value and dual of the lower bounds - V_L = spzeros(num_vars+num_ineq) - X_L = spzeros(num_vars+num_ineq) + V_L = spzeros(num_vars + num_ineq) + X_L = spzeros(num_vars + num_ineq) for (i, j) in enumerate(has_low) - V_L[j] = model.y[form.constraint_lower_bounds[primal_vars[j].value].value] * sense_multiplier + V_L[j] = + model.y[form.constraint_lower_bounds[primal_vars[j].value].value] * + sense_multiplier #dual.(LowerBoundRef(primal_vars[j])) * sense_multiplier # if sense_multiplier == 1.0 - V_L[j] <= -tol && @info "Dual of lower bound must be positive" i V_L[j] + V_L[j] <= -tol && + @info "Dual of lower bound must be positive" i V_L[j] else - V_L[j] >= tol && @info "Dual of lower bound must be negative" i V_L[j] + V_L[j] >= tol && + @info "Dual of lower bound must be negative" i V_L[j] end # X_L[j] = form.lower_bounds[primal_vars[j].value] @@ -181,24 +221,31 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) for (i, con) in enumerate(cons[geq_locations]) # By convention jump dual will allways be positive for geq constraints # but for ipopt it will be positive if min problem and negative if max problem - V_L[num_vars+i] = model.y[form.nlp_index_2_constraint[con].value] * sense_multiplier + V_L[num_vars+i] = + model.y[form.nlp_index_2_constraint[con].value] * sense_multiplier # if sense_multiplier == 1.0 - V_L[num_vars+i] <= -tol && @info "Dual of geq constraint must be positive" i V_L[num_vars+i] + V_L[num_vars+i] <= -tol && + @info "Dual of geq constraint must be positive" i V_L[num_vars+i] else - V_L[num_vars+i] >= tol && @info "Dual of geq constraint must be negative" i V_L[num_vars+i] + V_L[num_vars+i] >= tol && + @info "Dual of geq constraint must be negative" i V_L[num_vars+i] end end # value and dual of the upper bounds - V_U = spzeros(num_vars+num_ineq) - X_U = spzeros(num_vars+num_ineq) + V_U = spzeros(num_vars + num_ineq) + X_U = spzeros(num_vars + num_ineq) for (i, j) in enumerate(has_up) - V_U[j] = model.y[form.constraint_upper_bounds[primal_vars[j].value].value] * (- sense_multiplier) + V_U[j] = + model.y[form.constraint_upper_bounds[primal_vars[j].value].value] * + (-sense_multiplier) # dual.(UpperBoundRef(primal_vars[j])) * (- sense_multiplier) if sense_multiplier == 1.0 - V_U[j] <= -tol && @info "Dual of upper bound must be positive" i V_U[i] + V_U[j] <= -tol && + @info "Dual of upper bound must be positive" i V_U[i] else - V_U[j] >= tol && @info "Dual of upper bound must be negative" i V_U[i] + V_U[j] >= tol && + @info "Dual of upper bound must be negative" i V_U[i] end # X_U[j] = form.upper_bounds[primal_vars[j].value] @@ -206,15 +253,29 @@ function compute_solution_and_bounds(model::Model; tol=1e-6) for (i, con) in enumerate(cons[leq_locations]) # By convention jump dual will allways be negative for leq constraints # but for ipopt it will be positive if min problem and negative if max problem - V_U[num_vars+num_geq+i] = model.y[form.nlp_index_2_constraint[con].value] * (- sense_multiplier) + V_U[num_vars+num_geq+i] = + model.y[form.nlp_index_2_constraint[con].value] * + (-sense_multiplier) # dual.(con) * (- sense_multiplier) if sense_multiplier == 1.0 - V_U[num_vars+num_geq+i] <= -tol && @info "Dual of leq constraint must be positive" i V_U[num_vars+i] + V_U[num_vars+num_geq+i] <= -tol && + @info "Dual of leq constraint must be positive" i V_U[num_vars+i] else - V_U[num_vars+num_geq+i] >= tol && @info "Dual of leq constraint must be negative" i V_U[num_vars+i] + V_U[num_vars+num_geq+i] >= tol && + @info "Dual of leq constraint must be negative" i V_U[num_vars+i] end end - return X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, vcat(has_up, collect(num_vars+num_geq+1:num_vars+num_geq+num_leq)), vcat(has_low, collect(num_vars+1:num_vars+num_geq)), cons + return X, + V_L, + X_L, + V_U, + X_U, + leq_locations, + geq_locations, + ineq_locations, + vcat(has_up, collect(num_vars+num_geq+1:num_vars+num_geq+num_leq)), + vcat(has_low, collect(num_vars+1:num_vars+num_geq)), + cons end """ @@ -222,9 +283,19 @@ end Build the M (KKT Jacobian w.r.t. solution) and N (KKT Jacobian w.r.t. parameters) matrices for the sensitivity analysis. """ -function build_M_N(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, - _X::AbstractVector, _V_L::AbstractVector, _X_L::AbstractVector, _V_U::AbstractVector, _X_U::AbstractVector, leq_locations::Vector{Z}, geq_locations::Vector{Z}, ineq_locations::Vector{Z}, - has_up::Vector{Z}, has_low::Vector{Z} +function build_M_N( + model::Model, + cons::Vector{MOI.Nonlinear.ConstraintIndex}, + _X::AbstractVector, + _V_L::AbstractVector, + _X_L::AbstractVector, + _V_U::AbstractVector, + _X_U::AbstractVector, + leq_locations::Vector{Z}, + geq_locations::Vector{Z}, + ineq_locations::Vector{Z}, + has_up::Vector{Z}, + has_low::Vector{Z}, ) where {Z<:Integer} # Setting num_vars = get_num_primal_vars(model) @@ -239,8 +310,8 @@ function build_M_N(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, X_ub = spzeros(num_up, num_up) V_L = spzeros(num_low, num_vars + num_ineq) V_U = spzeros(num_up, num_vars + num_ineq) - I_L = spzeros(num_vars + num_ineq, num_low) - I_U = spzeros(num_vars + num_ineq, num_up) + I_L = spzeros(num_vars + num_ineq, num_low) + I_U = spzeros(num_vars + num_ineq, num_up) # value and dual of the lower bounds for (i, j) in enumerate(has_low) @@ -272,10 +343,10 @@ function build_M_N(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, # Jacobian of the constraints wrt the primal variables A[:, 1:num_vars] = jacobian[:, primal_idx] # Jacobian of the constraints wrt the slack variables - for (i,j) in enumerate(geq_locations) + for (i, j) in enumerate(geq_locations) A[j, num_vars+i] = -1 end - for (i,j) in enumerate(leq_locations) + for (i, j) in enumerate(leq_locations) A[j, num_vars+i] = -1 end # Partial second derivative of the lagrangian wrt primal solution and parameters @@ -292,16 +363,25 @@ function build_M_N(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, # [V_U 0 0 0 (X_U - X)] # ] len_w = num_vars + num_ineq - M = spzeros(len_w + num_cons + num_low + num_up, len_w + num_cons + num_low + num_up) + M = spzeros( + len_w + num_cons + num_low + num_up, + len_w + num_cons + num_low + num_up, + ) M[1:len_w, 1:len_w] = W - M[1:len_w, len_w + 1 : len_w + num_cons] = A' + M[1:len_w, len_w+1:len_w+num_cons] = A' M[len_w+1:len_w+num_cons, 1:len_w] = A M[1:len_w, len_w+num_cons+1:len_w+num_cons+num_low] = I_L M[len_w+num_cons+1:len_w+num_cons+num_low, 1:len_w] = V_L - M[len_w+num_cons+1:len_w+num_cons+num_low, len_w+num_cons+1:len_w+num_cons+num_low] = X_lb + M[ + len_w+num_cons+1:len_w+num_cons+num_low, + len_w+num_cons+1:len_w+num_cons+num_low, + ] = X_lb M[len_w+num_cons+num_low+1:len_w+num_cons+num_low+num_up, 1:len_w] = V_U - M[len_w+num_cons+num_low+1:len_w+num_cons+num_low+num_up, len_w+num_cons+num_low+1:len_w+num_cons+num_low+num_up] = X_ub + M[ + len_w+num_cons+num_low+1:len_w+num_cons+num_low+num_up, + len_w+num_cons+num_low+1:len_w+num_cons+num_low+num_up, + ] = X_ub M[1:len_w, len_w+num_cons+num_low+1:end] = I_U # N matrix @@ -318,9 +398,15 @@ end Inertia correction for the factorization of the KKT matrix. Sparse version. """ -function inertia_corrector_factorization(M::SparseMatrixCSC, num_w, num_cons; st=1e-6, max_corrections=50) +function inertia_corrector_factorization( + M::SparseMatrixCSC, + num_w, + num_cons; + st = 1e-6, + max_corrections = 50, +) # Factorization - K = lu(M; check=false) + K = lu(M; check = false) # Inertia correction status = K.status num_c = 0 @@ -330,7 +416,7 @@ function inertia_corrector_factorization(M::SparseMatrixCSC, num_w, num_cons; st while status == 1 && num_c < max_corrections println("Inertia correction") M = M + st * diag_mat - K = lu(M; check=false) + K = lu(M; check = false) status = K.status num_c += 1 end @@ -346,14 +432,14 @@ end Inertia correction for the factorization of the KKT matrix. Dense version. """ -function inertia_corrector_factorization(M; st=1e-6, max_corrections=50) +function inertia_corrector_factorization(M; st = 1e-6, max_corrections = 50) num_c = 0 - if cond(M) > 1/st + if cond(M) > 1 / st @warn "Inertia correction" M = M + st * I(size(M, 1)) num_c += 1 end - while cond(M) > 1/st && num_c < max_corrections + while cond(M) > 1 / st && num_c < max_corrections M = M + st * I(size(M, 1)) num_c += 1 end @@ -372,11 +458,34 @@ end Compute the derivatives of the solution w.r.t. the parameters without accounting for active set changes. """ -function compute_derivatives_no_relax(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, - _X::AbstractVector, _V_L::AbstractVector, _X_L::AbstractVector, _V_U::AbstractVector, _X_U::AbstractVector, leq_locations::Vector{Z}, geq_locations::Vector{Z}, ineq_locations::Vector{Z}, - has_up::Vector{Z}, has_low::Vector{Z} +function compute_derivatives_no_relax( + model::Model, + cons::Vector{MOI.Nonlinear.ConstraintIndex}, + _X::AbstractVector, + _V_L::AbstractVector, + _X_L::AbstractVector, + _V_U::AbstractVector, + _X_U::AbstractVector, + leq_locations::Vector{Z}, + geq_locations::Vector{Z}, + ineq_locations::Vector{Z}, + has_up::Vector{Z}, + has_low::Vector{Z}, ) where {Z<:Integer} - M, N = build_M_N(model, cons, _X, _V_L, _X_L, _V_U, _X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low) + M, N = build_M_N( + model, + cons, + _X, + _V_L, + _X_L, + _V_U, + _X_U, + leq_locations, + geq_locations, + ineq_locations, + has_up, + has_low, + ) # Sesitivity of the solution (primal-dual_constraints-dual_bounds) w.r.t. the parameters num_vars = get_num_primal_vars(model) @@ -389,8 +498,8 @@ function compute_derivatives_no_relax(model::Model, cons::Vector{MOI.Nonlinear.C ∂s = zeros(size(M, 1), size(N, 2)) # ∂s = - (K \ N) # Sensitivity ldiv!(∂s, K, N) - ∂s = - ∂s - + ∂s = -∂s + return ∂s, K, N end @@ -401,12 +510,35 @@ sense_mult(model::Model) = objective_sense(model) == MOI.MIN_SENSE ? 1.0 : -1.0 Compute the sensitivity of the solution given sensitivity of the parameters (Δp). """ -function compute_sensitivity(model::Model; tol=1e-6) +function compute_sensitivity(model::Model; tol = 1e-6) # Solution and bounds - X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low, cons = compute_solution_and_bounds(model; tol=tol) + X, + V_L, + X_L, + V_U, + X_U, + leq_locations, + geq_locations, + ineq_locations, + has_up, + has_low, + cons = compute_solution_and_bounds(model; tol = tol) # Compute derivatives # ∂s = [∂x; ∂λ; ∂ν_L; ∂ν_U] - ∂s, K, N = compute_derivatives_no_relax(model, cons, X, V_L, X_L, V_U, X_U, leq_locations, geq_locations, ineq_locations, has_up, has_low) + ∂s, K, N = compute_derivatives_no_relax( + model, + cons, + X, + V_L, + X_L, + V_U, + X_U, + leq_locations, + geq_locations, + ineq_locations, + has_up, + has_low, + ) ## Adjust signs based on JuMP convention num_vars = get_num_primal_vars(model) num_cons = get_num_constraints(model) diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index d8933c5a..da054f75 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -677,11 +677,10 @@ function MOI.get( ci::MOI.ConstraintIndex, ) return MOI.get( - _checked_diff(model, attr, :reverse_differentiate!), - attr, - model.index_map[ci], - ) - + _checked_diff(model, attr, :reverse_differentiate!), + attr, + model.index_map[ci], + ) end function MOI.supports( @@ -710,11 +709,7 @@ function MOI.set( return end -function MOI.get( - model::Optimizer, - ::ForwardParameter, - vi::MOI.VariableIndex, -) +function MOI.get(model::Optimizer, ::ForwardParameter, vi::MOI.VariableIndex) return get(model.input_cache.dp, vi, 0.0) end diff --git a/test/data/nlp_problems.jl b/test/data/nlp_problems.jl index 13c7e533..923b48c4 100644 --- a/test/data/nlp_problems.jl +++ b/test/data/nlp_problems.jl @@ -7,7 +7,7 @@ From JuMP Tutorial for Querying Hessians: https://github.com/jump-dev/JuMP.jl/blob/301d46e81cb66c74c6e22cd89fb89ced740f157b/docs/src/tutorials/nonlinear/querying_hessians.jl#L67-L72 =# ################################################ -function create_nonlinear_jump_model(;ismin=true) +function create_nonlinear_jump_model(; ismin = true) model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) @variable(model, p ∈ MOI.Parameter(1.0)) @@ -21,18 +21,17 @@ function create_nonlinear_jump_model(;ismin=true) else @objective(model, Max, -(1 - x[1])^2 - p3 * (x[2] - x[1]^2)^2) end - + return model, x, [g_1; g_2], [p; p2; p3] end - ################################################ #= From sIpopt paper: https://optimization-online.org/2011/04/3008/ =# ################################################ -function create_nonlinear_jump_model_sipopt(;ismin = true) +function create_nonlinear_jump_model_sipopt(; ismin = true) model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) @variable(model, p1 ∈ MOI.Parameter(4.5)) @@ -54,7 +53,6 @@ Simple Problems =# ################################################ - function create_jump_model_1(p_val = [1.5]) model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) @@ -63,7 +61,7 @@ function create_jump_model_1(p_val = [1.5]) @variable(model, p ∈ MOI.Parameter(p_val[1])) # Variables - @variable(model, x) + @variable(model, x) # Constraints @constraint(model, con1, x >= p) @@ -81,7 +79,7 @@ function create_jump_model_2(p_val = [1.5]) @variable(model, p ∈ MOI.Parameter(p_val[1])) # Variables - @variable(model, x >= 2.0) + @variable(model, x >= 2.0) # Constraints @constraint(model, con1, x >= p) @@ -98,7 +96,7 @@ function create_jump_model_3(p_val = [-1.5]) @variable(model, p ∈ MOI.Parameter(p_val[1])) # Variables - @variable(model, x) + @variable(model, x) # Constraints @constraint(model, con1, x <= p) @@ -116,7 +114,7 @@ function create_jump_model_4(p_val = [1.5]) @variable(model, p ∈ MOI.Parameter(p_val[1])) # Variables - @variable(model, x) + @variable(model, x) # Constraints @constraint(model, con1, x <= p) @@ -134,7 +132,7 @@ function create_jump_model_5(p_val = [1.5]) @variable(model, p ∈ MOI.Parameter(p_val[1])) # Variables - @variable(model, x) + @variable(model, x) # Constraints @constraint(model, con1, x >= p) @@ -145,21 +143,21 @@ function create_jump_model_5(p_val = [1.5]) end # Softmax model -h(y) = - sum(y .* log.(y)) +h(y) = -sum(y .* log.(y)) softmax(x) = exp.(x) / sum(exp.(x)) function create_jump_model_6(p_a = collect(1.0:0.1:2.0)) model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters - @variable(model, x[i=1:length(p_a)] ∈ MOI.Parameter.(p_a)) + @variable(model, x[i = 1:length(p_a)] ∈ MOI.Parameter.(p_a)) # Variables @variable(model, y[1:length(p_a)] >= 0.0) # Constraints @constraint(model, con1, sum(y) == 1) - @constraint(model, con2[i=1:length(x)], y[i] <= 1) + @constraint(model, con2[i = 1:length(x)], y[i] <= 1) # Objective @objective(model, Max, dot(x, y) + h(y)) @@ -175,7 +173,7 @@ function create_jump_model_7(p_val = [1.5], g = sin) @variable(model, p ∈ MOI.Parameter(p_val[1])) # Variables - @variable(model, x) + @variable(model, x) # Constraints @constraint(model, con1, x * g(p) == 1) @@ -190,20 +188,19 @@ Non Linear Problems =# ################################################ - function create_nonlinear_jump_model_1(p_val = [1.0; 2.0; 100]; ismin = true) model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) set_silent(model) # Parameters - @variable(model, p[i=1:3] ∈ MOI.Parameter.(p_val)) + @variable(model, p[i = 1:3] ∈ MOI.Parameter.(p_val)) # Variables - @variable(model, x) + @variable(model, x) @variable(model, y) # Constraints - @constraint(model, con1, y >= p[1]*sin(x)) # NLP Constraint + @constraint(model, con1, y >= p[1] * sin(x)) # NLP Constraint @constraint(model, con2, x + y == p[1]) @constraint(model, con3, p[2] * x >= 0.1) if ismin @@ -220,14 +217,14 @@ function create_nonlinear_jump_model_2(p_val = [3.0; 2.0; 10]; ismin = true) set_silent(model) # Parameters - @variable(model, p[i=1:3] ∈ MOI.Parameter.(p_val)) + @variable(model, p[i = 1:3] ∈ MOI.Parameter.(p_val)) # Variables - @variable(model, x <= 10) + @variable(model, x <= 10) @variable(model, y) # Constraints - @constraint(model, con1, y >= p[1]*sin(x)) # NLP Constraint + @constraint(model, con1, y >= p[1] * sin(x)) # NLP Constraint @constraint(model, con2, x + y == p[1]) @constraint(model, con3, p[2] * x >= 0.1) if ismin @@ -235,7 +232,7 @@ function create_nonlinear_jump_model_2(p_val = [3.0; 2.0; 10]; ismin = true) else @objective(model, Max, -(1 - x)^2 - p[3] * (y - x^2)^2) # NLP Objective end - + return model, [x; y], [con1; con2; con3], p end @@ -244,14 +241,14 @@ function create_nonlinear_jump_model_3(p_val = [3.0; 2.0; 10]; ismin = true) set_silent(model) # Parameters - @variable(model, p[i=1:3] ∈ MOI.Parameter.(p_val)) + @variable(model, p[i = 1:3] ∈ MOI.Parameter.(p_val)) # Variables - @variable(model, x <= 10) + @variable(model, x <= 10) @variable(model, y) # Constraints - @constraint(model, con1, y >= p[1]*sin(x)) # NLP Constraint + @constraint(model, con1, y >= p[1] * sin(x)) # NLP Constraint @constraint(model, con2, x + y == p[1]) @constraint(model, con3, p[2] * x >= 0.1) if ismin @@ -267,15 +264,15 @@ function create_nonlinear_jump_model_4(p_val = [1.0; 2.0; 100]; ismin = true) set_silent(model) # Parameters - @variable(model, p[i=1:3] ∈ MOI.Parameter.(p_val)) + @variable(model, p[i = 1:3] ∈ MOI.Parameter.(p_val)) # Variables - @variable(model, x) + @variable(model, x) @variable(model, y) # Constraints @constraint(model, con0, x == p[1] - 0.5) - @constraint(model, con1, y >= p[1]*sin(x)) # NLP Constraint + @constraint(model, con1, y >= p[1] * sin(x)) # NLP Constraint @constraint(model, con2, x + y == p[1]) @constraint(model, con3, p[2] * x >= 0.1) if ismin @@ -292,16 +289,16 @@ function create_nonlinear_jump_model_5(p_val = [1.0; 2.0; 100]; ismin = true) set_silent(model) # Parameters - @variable(model, p[i=1:3] ∈ MOI.Parameter.(p_val)) + @variable(model, p[i = 1:3] ∈ MOI.Parameter.(p_val)) # Variables - @variable(model, x) + @variable(model, x) @variable(model, y) # Constraints fix(x, 0.5) con0 = JuMP.FixRef(x) - @constraint(model, con1, y >= p[1]*sin(x)) # NLP Constraint + @constraint(model, con1, y >= p[1] * sin(x)) # NLP Constraint @constraint(model, con2, x + y == p[1]) @constraint(model, con3, p[2] * x >= 0.1) if ismin @@ -318,17 +315,25 @@ function create_nonlinear_jump_model_6(p_val = [100.0; 200.0]; ismin = true) set_silent(model) # Parameters - @variable(model, p[i=1:2] ∈ MOI.Parameter.(p_val)) + @variable(model, p[i = 1:2] ∈ MOI.Parameter.(p_val)) # Variables - @variable(model, x[i=1:2]) + @variable(model, x[i = 1:2]) @variable(model, z) # >= 2.0) @variable(model, w) # <= 3.0) # @variable(model, f[1:2]) # Constraints - @constraint(model, con1, x[2] - 0.0001 * x[1]^2 - 0.2 * z^2 - 0.3 * w^2 >= p[1] + 1) - @constraint(model, con2, x[1] + 0.001 * x[2]^2 + 0.5 * w^2 + 0.4 * z^2 <= 10 * p[1] + 2) + @constraint( + model, + con1, + x[2] - 0.0001 * x[1]^2 - 0.2 * z^2 - 0.3 * w^2 >= p[1] + 1 + ) + @constraint( + model, + con2, + x[1] + 0.001 * x[2]^2 + 0.5 * w^2 + 0.4 * z^2 <= 10 * p[1] + 2 + ) @constraint(model, con3, z^2 + w^2 == 13) if ismin @objective(model, Min, x[2] - x[1] + z - w) @@ -337,4 +342,4 @@ function create_nonlinear_jump_model_6(p_val = [100.0; 200.0]; ismin = true) end return model, [x; z; w], [con2; con3], p -end \ No newline at end of file +end diff --git a/test/nlp_program.jl b/test/nlp_program.jl index 1a48581d..7005c949 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -40,21 +40,21 @@ function analytic_hessian(x, σ, μ, p) end function analytic_jacobian(x, p) - g_1_J = [ + g_1_J = [ 2.0 * x[1], # ∂g_1/∂x_1 0.0, # ∂g_1/∂x_2 -1.0, # ∂g_1/∂p_1 0.0, # ∂g_1/∂p_2 - 0.0 # ∂g_1/∂p_3 + 0.0, # ∂g_1/∂p_3 ] g_2_J = [ p[1] * 2.0 * (x[1] + x[2]), # ∂g_2/∂x_1 2.0 * (x[1] + x[2]), # ∂g_2/∂x_2 (x[1] + x[2])^2, # ∂g_2/∂p_1 -1.0, # ∂g_2/∂p_2 - 0.0 # ∂g_2/∂p_3 + 0.0, # ∂g_2/∂p_3 ] - return hcat(g_2_J, g_1_J)'[:,:] + return hcat(g_2_J, g_1_J)'[:, :] end function _test_create_evaluator(nlp_model) @@ -76,14 +76,37 @@ function test_compute_optimal_hess_jacobian() nlp_model = DiffOpt._diff(model.moi_backend.optimizer.model).model _test_create_evaluator(nlp_model) cons = nlp_model.cache.cons - y = [nlp_model.y[nlp_model.model.nlp_index_2_constraint[row].value] for row in cons] - hessian, jacobian = DiffOpt.NonLinearProgram.compute_optimal_hess_jac(nlp_model, cons) + y = [ + nlp_model.y[nlp_model.model.nlp_index_2_constraint[row].value] + for row in cons + ] + hessian, jacobian = + DiffOpt.NonLinearProgram.compute_optimal_hess_jac(nlp_model, cons) # Check Hessian primal_idx = [i.value for i in nlp_model.cache.primal_vars] params_idx = [i.value for i in nlp_model.cache.params] - @test all(isapprox(hessian[primal_idx,primal_idx], analytic_hessian(nlp_model.x[primal_idx], 1.0, -y, nlp_model.x[params_idx]); atol = 1)) + @test all( + isapprox( + hessian[primal_idx, primal_idx], + analytic_hessian( + nlp_model.x[primal_idx], + 1.0, + -y, + nlp_model.x[params_idx], + ); + atol = 1, + ), + ) # Check Jacobian - @test all(isapprox(jacobian[:,[primal_idx; params_idx]], analytic_jacobian(nlp_model.x[primal_idx], nlp_model.x[params_idx]))) + @test all( + isapprox( + jacobian[:, [primal_idx; params_idx]], + analytic_jacobian( + nlp_model.x[primal_idx], + nlp_model.x[params_idx], + ), + ), + ) end end @@ -93,25 +116,101 @@ end =# ################################################ - # f(x, p) = 0 # x = g(p) # ∂x/∂p = ∂g/∂p DICT_PROBLEMS_Analytical_no_cc = Dict( - "geq no impact" => (p_a=[1.5], Δp=[0.2], Δx=[0.0], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_1), - "geq impact" => (p_a=[2.1], Δp=[0.2], Δx=[0.2], Δy=[0.4; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_1), - "geq bound impact" => (p_a=[2.1], Δp=[0.2], Δx=[0.2], Δy=[0.4], Δvu=[], Δvl=[0.0], model_generator=create_jump_model_2), - "leq no impact" => (p_a=[-1.5], Δp=[-0.2], Δx=[0.0], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_3), - "leq impact" => (p_a=[-2.1], Δp=[-0.2], Δx=[-0.2], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_3), - "leq no impact max" => (p_a=[2.1], Δp=[0.2], Δx=[0.0], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_4), - "leq impact max" => (p_a=[1.5], Δp=[0.2], Δx=[0.2], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_4), - "geq no impact max" => (p_a=[1.5], Δp=[0.2], Δx=[0.0], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_5), - "geq impact max" => (p_a=[2.1], Δp=[0.2], Δx=[0.2], Δy=[0.0; 0.0], Δvu=[], Δvl=[], model_generator=create_jump_model_5), + "geq no impact" => ( + p_a = [1.5], + Δp = [0.2], + Δx = [0.0], + Δy = [0.0; 0.0], + Δvu = [], + Δvl = [], + model_generator = create_jump_model_1, + ), + "geq impact" => ( + p_a = [2.1], + Δp = [0.2], + Δx = [0.2], + Δy = [0.4; 0.0], + Δvu = [], + Δvl = [], + model_generator = create_jump_model_1, + ), + "geq bound impact" => ( + p_a = [2.1], + Δp = [0.2], + Δx = [0.2], + Δy = [0.4], + Δvu = [], + Δvl = [0.0], + model_generator = create_jump_model_2, + ), + "leq no impact" => ( + p_a = [-1.5], + Δp = [-0.2], + Δx = [0.0], + Δy = [0.0; 0.0], + Δvu = [], + Δvl = [], + model_generator = create_jump_model_3, + ), + "leq impact" => ( + p_a = [-2.1], + Δp = [-0.2], + Δx = [-0.2], + Δy = [0.0; 0.0], + Δvu = [], + Δvl = [], + model_generator = create_jump_model_3, + ), + "leq no impact max" => ( + p_a = [2.1], + Δp = [0.2], + Δx = [0.0], + Δy = [0.0; 0.0], + Δvu = [], + Δvl = [], + model_generator = create_jump_model_4, + ), + "leq impact max" => ( + p_a = [1.5], + Δp = [0.2], + Δx = [0.2], + Δy = [0.0; 0.0], + Δvu = [], + Δvl = [], + model_generator = create_jump_model_4, + ), + "geq no impact max" => ( + p_a = [1.5], + Δp = [0.2], + Δx = [0.0], + Δy = [0.0; 0.0], + Δvu = [], + Δvl = [], + model_generator = create_jump_model_5, + ), + "geq impact max" => ( + p_a = [2.1], + Δp = [0.2], + Δx = [0.2], + Δy = [0.0; 0.0], + Δvu = [], + Δvl = [], + model_generator = create_jump_model_5, + ), ) -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 +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 # OPT Problem model, primal_vars, cons, params = model_generator() set_parameter_value.(params, p_a) @@ -123,20 +222,62 @@ function test_compute_derivatives_Analytical(;DICT_PROBLEMS=DICT_PROBLEMS_Analyt DiffOpt.forward_differentiate!(model) # Test sensitivities primal_vars if !isempty(Δx) - @test all(isapprox.([MOI.get(model, DiffOpt.ForwardVariablePrimal(), var) for var in primal_vars], Δx; atol = 1e-4)) + @test all( + isapprox.( + [ + MOI.get(model, DiffOpt.ForwardVariablePrimal(), var) for + var in primal_vars + ], + Δx; + atol = 1e-4, + ), + ) end # Test sensitivities cons if !isempty(Δy) - @test all(isapprox.([MOI.get(model, DiffOpt.ForwardConstraintDual(), con) for con in cons], Δy; atol = 1e-4)) + @test all( + isapprox.( + [ + MOI.get(model, DiffOpt.ForwardConstraintDual(), con) for + con in cons + ], + Δy; + atol = 1e-4, + ), + ) end # Test sensitivities dual vars if !isempty(Δvu) primal_vars_upper = [v for v in primal_vars if has_upper_bound(v)] - @test all(isapprox.([MOI.get(model, DiffOpt.ForwardConstraintDual(), UpperBoundRef(var)) for var in primal_vars_upper], Δvu; atol = 1e-4)) + @test all( + isapprox.( + [ + MOI.get( + model, + DiffOpt.ForwardConstraintDual(), + UpperBoundRef(var), + ) for var in primal_vars_upper + ], + Δvu; + atol = 1e-4, + ), + ) end if !isempty(Δvl) primal_vars_lower = [v for v in primal_vars if has_lower_bound(v)] - @test all(isapprox.([MOI.get(model, DiffOpt.ForwardConstraintDual(), LowerBoundRef(var)) for var in primal_vars_lower], Δvl; atol = 1e-4)) + @test all( + isapprox.( + [ + MOI.get( + model, + DiffOpt.ForwardConstraintDual(), + LowerBoundRef(var), + ) for var in primal_vars_lower + ], + Δvl; + atol = 1e-4, + ), + ) end end end @@ -155,29 +296,103 @@ function stack_solution(model, p_a, params, primal_vars, cons) end DICT_PROBLEMS_no_cc = Dict( - "QP_sIpopt" => (p_a=[4.5; 1.0], Δp=[0.001; 0.0], model_generator=create_nonlinear_jump_model_sipopt), - "NLP_1" => (p_a=[3.0; 2.0; 200], Δp=[0.001; 0.0; 0.0], model_generator=create_nonlinear_jump_model_1), - "NLP_1_2" => (p_a=[3.0; 2.0; 200], Δp=[0.0; 0.001; 0.0], model_generator=create_nonlinear_jump_model_1), - "NLP_1_3" => (p_a=[3.0; 2.0; 200], Δp=[0.0; 0.0; 0.001], model_generator=create_nonlinear_jump_model_1), - "NLP_1_4" => (p_a=[3.0; 2.0; 200], Δp=[0.1; 0.5; 0.5], model_generator=create_nonlinear_jump_model_1), - "NLP_1_4" => (p_a=[3.0; 2.0; 200], Δp=[0.5; -0.5; 0.1], model_generator=create_nonlinear_jump_model_1), - "NLP_2" => (p_a=[3.0; 2.0; 10], Δp=[0.01; 0.0; 0.0], model_generator=create_nonlinear_jump_model_2), - "NLP_2_2" => (p_a=[3.0; 2.0; 10], Δp=[-0.1; 0.0; 0.0], model_generator=create_nonlinear_jump_model_2), - "NLP_3" => (p_a=[3.0; 2.0; 10], Δp=[0.001; 0.0; 0.0], model_generator=create_nonlinear_jump_model_3), - "NLP_3_2" => (p_a=[3.0; 2.0; 10], Δp=[0.0; 0.001; 0.0], model_generator=create_nonlinear_jump_model_3), - "NLP_3_3" => (p_a=[3.0; 2.0; 10], Δp=[0.0; 0.0; 0.001], model_generator=create_nonlinear_jump_model_3), - "NLP_3_4" => (p_a=[3.0; 2.0; 10], Δp=[0.5; 0.001; 0.5], model_generator=create_nonlinear_jump_model_3), - "NLP_3_5" => (p_a=[3.0; 2.0; 10], Δp=[0.1; 0.3; 0.1], model_generator=create_nonlinear_jump_model_3), - "NLP_3_6" => (p_a=[3.0; 2.0; 10], Δp=[0.1; 0.2; -0.5], model_generator=create_nonlinear_jump_model_3), - "NLP_4" => (p_a=[1.0; 2.0; 100], Δp=[0.001; 0.0; 0.0], model_generator=create_nonlinear_jump_model_4), - "NLP_5" => (p_a=[1.0; 2.0; 100], Δp=[0.0; 0.001; 0.0], model_generator=create_nonlinear_jump_model_5), - "NLP_6" => (p_a=[100.0; 200.0], Δp=[0.2; 0.5], model_generator=create_nonlinear_jump_model_6), + "QP_sIpopt" => ( + p_a = [4.5; 1.0], + Δp = [0.001; 0.0], + model_generator = create_nonlinear_jump_model_sipopt, + ), + "NLP_1" => ( + p_a = [3.0; 2.0; 200], + Δp = [0.001; 0.0; 0.0], + model_generator = create_nonlinear_jump_model_1, + ), + "NLP_1_2" => ( + p_a = [3.0; 2.0; 200], + Δp = [0.0; 0.001; 0.0], + model_generator = create_nonlinear_jump_model_1, + ), + "NLP_1_3" => ( + p_a = [3.0; 2.0; 200], + Δp = [0.0; 0.0; 0.001], + model_generator = create_nonlinear_jump_model_1, + ), + "NLP_1_4" => ( + p_a = [3.0; 2.0; 200], + Δp = [0.1; 0.5; 0.5], + model_generator = create_nonlinear_jump_model_1, + ), + "NLP_1_4" => ( + p_a = [3.0; 2.0; 200], + Δp = [0.5; -0.5; 0.1], + model_generator = create_nonlinear_jump_model_1, + ), + "NLP_2" => ( + p_a = [3.0; 2.0; 10], + Δp = [0.01; 0.0; 0.0], + model_generator = create_nonlinear_jump_model_2, + ), + "NLP_2_2" => ( + p_a = [3.0; 2.0; 10], + Δp = [-0.1; 0.0; 0.0], + model_generator = create_nonlinear_jump_model_2, + ), + "NLP_3" => ( + p_a = [3.0; 2.0; 10], + Δp = [0.001; 0.0; 0.0], + model_generator = create_nonlinear_jump_model_3, + ), + "NLP_3_2" => ( + p_a = [3.0; 2.0; 10], + Δp = [0.0; 0.001; 0.0], + model_generator = create_nonlinear_jump_model_3, + ), + "NLP_3_3" => ( + p_a = [3.0; 2.0; 10], + Δp = [0.0; 0.0; 0.001], + model_generator = create_nonlinear_jump_model_3, + ), + "NLP_3_4" => ( + p_a = [3.0; 2.0; 10], + Δp = [0.5; 0.001; 0.5], + model_generator = create_nonlinear_jump_model_3, + ), + "NLP_3_5" => ( + p_a = [3.0; 2.0; 10], + Δp = [0.1; 0.3; 0.1], + model_generator = create_nonlinear_jump_model_3, + ), + "NLP_3_6" => ( + p_a = [3.0; 2.0; 10], + Δp = [0.1; 0.2; -0.5], + model_generator = create_nonlinear_jump_model_3, + ), + "NLP_4" => ( + p_a = [1.0; 2.0; 100], + Δp = [0.001; 0.0; 0.0], + model_generator = create_nonlinear_jump_model_4, + ), + "NLP_5" => ( + p_a = [1.0; 2.0; 100], + Δp = [0.0; 0.001; 0.0], + model_generator = create_nonlinear_jump_model_5, + ), + "NLP_6" => ( + p_a = [100.0; 200.0], + Δp = [0.2; 0.5], + model_generator = create_nonlinear_jump_model_6, + ), ) -function test_compute_derivatives_Finite_Diff(;DICT_PROBLEMS=DICT_PROBLEMS_no_cc) - @testset "Compute Derivatives FiniteDiff: $problem_name" for (problem_name, (p_a, Δp, model_generator)) in DICT_PROBLEMS, ismin in [true, false] +function test_compute_derivatives_Finite_Diff(; + DICT_PROBLEMS = DICT_PROBLEMS_no_cc, +) + @testset "Compute Derivatives FiniteDiff: $problem_name" for ( + problem_name, + (p_a, Δp, model_generator), + ) in DICT_PROBLEMS, + ismin in [true, false] # OPT Problem - model, primal_vars, cons, params = model_generator(;ismin=ismin) + model, primal_vars, cons, params = model_generator(; ismin = ismin) set_parameter_value.(params, p_a) optimize!(model) @assert is_solved_and_feasible(model) @@ -185,10 +400,19 @@ function test_compute_derivatives_Finite_Diff(;DICT_PROBLEMS=DICT_PROBLEMS_no_cc MOI.set.(model, DiffOpt.ForwardParameter(), params, Δp) # Compute derivatives DiffOpt.forward_differentiate!(model) - Δx = [MOI.get(model, DiffOpt.ForwardVariablePrimal(), var) for var in primal_vars] - Δy = [MOI.get(model, DiffOpt.ForwardConstraintDual(), con) for con in cons] + Δx = [ + MOI.get(model, DiffOpt.ForwardVariablePrimal(), var) for + var in primal_vars + ] + Δy = [ + MOI.get(model, DiffOpt.ForwardConstraintDual(), con) for con in cons + ] # Compute derivatives using finite differences - ∂s_fd = FiniteDiff.finite_difference_jacobian((p) -> stack_solution(model, p, params, primal_vars, cons), p_a) * Δp + ∂s_fd = + FiniteDiff.finite_difference_jacobian( + (p) -> stack_solution(model, p, params, primal_vars, cons), + p_a, + ) * Δp # Test sensitivities primal_vars @test all(isapprox.(Δx, ∂s_fd[1:length(primal_vars)]; atol = 1e-4)) # Test sensitivities cons @@ -244,10 +468,12 @@ function test_differentiating_non_trivial_convex_qp_jump() db = grads_actual[6] for (i, ci) in enumerate(c_le) - @test -dh[i] ≈ -MOI.get(model, DiffOpt.ReverseParameter(), p_le[i]) atol = 1e-2 rtol = 1e-2 + @test -dh[i] ≈ -MOI.get(model, DiffOpt.ReverseParameter(), p_le[i]) atol = + 1e-2 rtol = 1e-2 end for (i, ci) in enumerate(c_eq) - @test -db[i] ≈ -MOI.get(model, DiffOpt.ReverseParameter(), p_eq[i]) atol = 1e-2 rtol = 1e-2 + @test -db[i] ≈ -MOI.get(model, DiffOpt.ReverseParameter(), p_eq[i]) atol = + 1e-2 rtol = 1e-2 end return @@ -255,4 +481,4 @@ end end # module -TestNLPProgram.runtests() \ No newline at end of file +TestNLPProgram.runtests() From 4868748ccf11e317f520a99b22effb1af3af6598 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 23 Dec 2024 15:59:50 -0500 Subject: [PATCH 19/41] update API reference --- docs/src/reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/reference.md b/docs/src/reference.md index 4688643a..a1e1b25a 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -4,5 +4,5 @@ ``` ```@autodocs -Modules = [DiffOpt, DiffOpt.QuadraticProgram, DiffOpt.ConicProgram] +Modules = [DiffOpt, DiffOpt.QuadraticProgram, DiffOpt.ConicProgram, DiffOpt.NonlinearProgram] ``` From 1d5dd4a7ed15cc398f3f9006d11f57accb6bfe5e Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 6 Jan 2025 12:25:05 -0500 Subject: [PATCH 20/41] fix typos --- src/NonLinearProgram/NonLinearProgram.jl | 2 +- src/NonLinearProgram/nlp_utilities.jl | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index f63d596d..547dc258 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -378,7 +378,7 @@ function _cache_evaluator!(model::Model) evaluator = create_evaluator(form) num_constraints = get_num_constraints(form) # Analyze constraints and bounds - leq_locations, geq_locations = find_inequealities(form) + leq_locations, geq_locations = find_inequalities(form) num_leq = length(leq_locations) num_geq = length(geq_locations) has_up = findall(i -> haskey(form.upper_bounds, i.value), primal_vars) diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 18fe4fbf..9f409616 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -25,7 +25,7 @@ end """ compute_optimal_hessian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{JuMP.ConstraintRef}, x::Vector{JuMP.VariableRef}) -Compute the optimal Hessian of the Lagrangian. +Compute the Hessian of the Lagrangian calculated at the optimal solution. """ function compute_optimal_hessian( model::Model, @@ -38,7 +38,7 @@ function compute_optimal_hessian( I = [i for (i, _) in hessian_sparsity] J = [j for (_, j) in hessian_sparsity] V = zeros(length(hessian_sparsity)) - # The signals are being sdjusted to match the Ipopt convention (inner.mult_g) + # The signals are being adjusted to match the Ipopt convention (inner.mult_g) # but we don't know if we need to adjust the objective function multiplier MOI.eval_hessian_lagrangian( evaluator, @@ -55,7 +55,7 @@ end """ compute_optimal_jacobian(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{JuMP.ConstraintRef}, x::Vector{JuMP.VariableRef}) -Compute the optimal Jacobian of the constraints. +Compute the Jacobian of the constraints calculated at the optimal solution. """ function compute_optimal_jacobian( model::Model, @@ -74,7 +74,7 @@ end """ compute_optimal_hess_jac(evaluator::MOI.Nonlinear.Evaluator, rows::Vector{JuMP.ConstraintRef}, x::Vector{JuMP.VariableRef}) -Compute the optimal Hessian of the Lagrangian and Jacobian of the constraints. +Compute the Hessian of the Lagrangian and Jacobian of the constraints calculated at the optimal solution. """ function compute_optimal_hess_jac( model::Model, @@ -148,11 +148,11 @@ function is_greater_inequality( end """ - find_inequealities(cons::Vector{JuMP.ConstraintRef}) + find_inequalities(cons::Vector{JuMP.ConstraintRef}) Find the indices of the inequality constraints. """ -function find_inequealities(model::Form) +function find_inequalities(model::Form) num_cons = length(model.list_of_constraint) leq_locations = zeros(num_cons) geq_locations = zeros(num_cons) @@ -279,11 +279,11 @@ function compute_solution_and_bounds(model::Model; tol = 1e-6) end """ - build_M_N(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, _X::AbstractVector, _V_L::AbstractVector, _X_L::AbstractVector, _V_U::AbstractVector, _X_U::AbstractVector, leq_locations::Vector{Z}, geq_locations::Vector{Z}, ineq_locations::Vector{Z}, has_up::Vector{Z}, has_low::Vector{Z}) + build_sensitivity_matrices(model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, _X::AbstractVector, _V_L::AbstractVector, _X_L::AbstractVector, _V_U::AbstractVector, _X_U::AbstractVector, leq_locations::Vector{Z}, geq_locations::Vector{Z}, ineq_locations::Vector{Z}, has_up::Vector{Z}, has_low::Vector{Z}) Build the M (KKT Jacobian w.r.t. solution) and N (KKT Jacobian w.r.t. parameters) matrices for the sensitivity analysis. """ -function build_M_N( +function build_sensitivity_matrices( model::Model, cons::Vector{MOI.Nonlinear.ConstraintIndex}, _X::AbstractVector, @@ -472,7 +472,7 @@ function compute_derivatives_no_relax( has_up::Vector{Z}, has_low::Vector{Z}, ) where {Z<:Integer} - M, N = build_M_N( + M, N = build_sensitivity_matrices( model, cons, _X, @@ -487,7 +487,7 @@ function compute_derivatives_no_relax( has_low, ) - # Sesitivity of the solution (primal-dual_constraints-dual_bounds) w.r.t. the parameters + # Sensitivity of the solution (primal-dual_constraints-dual_bounds) w.r.t. the parameters num_vars = get_num_primal_vars(model) num_cons = get_num_constraints(model) num_ineq = length(ineq_locations) @@ -498,7 +498,7 @@ function compute_derivatives_no_relax( ∂s = zeros(size(M, 1), size(N, 2)) # ∂s = - (K \ N) # Sensitivity ldiv!(∂s, K, N) - ∂s = -∂s + ∂s = -∂s # multiply by -1 since we used ∂s as an auxilary variable to calculate K \ N return ∂s, K, N end From 89d34eaaf67ff99fcee9ce03f878e9657163166c Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 6 Jan 2025 12:28:47 -0500 Subject: [PATCH 21/41] update reference --- docs/src/reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/reference.md b/docs/src/reference.md index a1e1b25a..9bc2fd82 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -4,5 +4,5 @@ ``` ```@autodocs -Modules = [DiffOpt, DiffOpt.QuadraticProgram, DiffOpt.ConicProgram, DiffOpt.NonlinearProgram] +Modules = [DiffOpt, DiffOpt.QuadraticProgram, DiffOpt.ConicProgram, DiffOpt.NonLinearProgram] ``` From d8a56916fcac5f618a7eb54757471f8a1107ae9d Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 3 Feb 2025 18:09:26 -0500 Subject: [PATCH 22/41] update spdiagm --- src/NonLinearProgram/nlp_utilities.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 9f409616..62cb7303 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -412,7 +412,7 @@ function inertia_corrector_factorization( num_c = 0 diag_mat = ones(size(M, 1)) diag_mat[num_w+1:num_w+num_cons] .= -1 - diag_mat = sparse(diagm(diag_mat)) + diag_mat = SparseArrays.spdiagm(diag_mat) while status == 1 && num_c < max_corrections println("Inertia correction") M = M + st * diag_mat From 4074055657e611c524eca794ef028ab783a2d63d Mon Sep 17 00:00:00 2001 From: mzagorowska <7868389+mzagorowska@users.noreply.github.com> Date: Tue, 23 Jul 2024 19:14:33 +0200 Subject: [PATCH 23/41] Typo "acutal" to "actual" (#258) Correcting typo "acutal" to "actual" --- docs/src/manual.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/manual.md b/docs/src/manual.md index 279c7820..7a3aeb09 100644 --- a/docs/src/manual.md +++ b/docs/src/manual.md @@ -107,6 +107,6 @@ In the light of above, DiffOpt differentiates program variables ``x``, ``s``, `` - OptNet: Differentiable Optimization as a Layer in Neural Networks ### Backward Pass vector -One possible point of confusion in finding Jacobians is the role of the backward pass vector - above eqn (7), *OptNet: Differentiable Optimization as a Layer in Neural Networks*. While differentiating convex programs, it is often the case that we don't want to find the acutal derivatives, rather we might be interested in computing the product of Jacobians with a *backward pass vector*, often used in backprop in machine learning/automatic differentiation. This is what happens in scheme 1 of `DiffOpt` backend. +One possible point of confusion in finding Jacobians is the role of the backward pass vector - above eqn (7), *OptNet: Differentiable Optimization as a Layer in Neural Networks*. While differentiating convex programs, it is often the case that we don't want to find the actual derivatives, rather we might be interested in computing the product of Jacobians with a *backward pass vector*, often used in backprop in machine learning/automatic differentiation. This is what happens in scheme 1 of `DiffOpt` backend. But, for the conic system (scheme 2), we provide perturbations in conic data (`dA`, `db`, `dc`) to compute pertubations (`dx`, `dy`, `dz`) in input variables. Unlike the quadratic case, these perturbations are actual derivatives, not the product with a backward pass vector. This is an important distinction between the two schemes of differential optimization. From b1f009267c162e3761a238c99867e779c068227a Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 6 Jan 2025 11:08:00 +1300 Subject: [PATCH 24/41] Fix GitHub actions badge in README (#263) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 36541bab..a90b9a9b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![stable docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://jump.dev/DiffOpt.jl/stable) [![development docs](https://img.shields.io/badge/docs-dev-blue.svg)](https://jump.dev/DiffOpt.jl/dev) -[![Build Status](https://github.com/jump-dev/DiffOpt.jl/workflows/CI/badge.svg?branch=master)](https://github.com/jump-dev/DiffOpt.jl/actions?query=workflow%3ACI) +[![Build Status](https://github.com/jump-dev/DiffOpt.jl/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/jump-dev/DiffOpt.jl/actions?query=workflow%3ACI) [![Coverage](https://codecov.io/gh/jump-dev/DiffOpt.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/jump-dev/DiffOpt.jl) [DiffOpt.jl](https://github.com/jump-dev/DiffOpt.jl) is a package for From 614b0268179b295e5f74aee69b5b6e9bc776aa06 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 7 Jan 2025 13:30:51 +1300 Subject: [PATCH 25/41] Implement MOI.Utilities.scalar_type for (Matrix|Sparse)VectorAffineFunction (#264) --- src/utils.jl | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 7e0b738c..dad12f7a 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -239,6 +239,7 @@ struct MatrixVectorAffineFunction{AT,VT} <: MOI.AbstractVectorFunction terms::AT constants::VT end + MOI.constant(func::MatrixVectorAffineFunction) = func.constants function Base.convert( ::Type{MOI.VectorAffineFunction{T}}, @@ -269,14 +270,22 @@ function MOIU.isapprox_zero( return MOIU.isapprox_zero(standard_form(func), tol) end -_scalar(::Type{<:MatrixVectorAffineFunction}) = VectorScalarAffineFunction -_scalar(::Type{<:SparseVectorAffineFunction}) = SparseScalarAffineFunction +function MOI.Utilities.scalar_type(::Type{<:MatrixVectorAffineFunction}) + return VectorScalarAffineFunction +end + +function MOI.Utilities.scalar_type(::Type{<:SparseVectorAffineFunction}) + return SparseScalarAffineFunction +end function Base.getindex( it::MOI.Utilities.ScalarFunctionIterator{F}, output_index::Integer, ) where {F<:Union{MatrixVectorAffineFunction,SparseVectorAffineFunction}} - return _scalar(F)(it.f.terms[output_index, :], it.f.constants[output_index]) + return MOI.Utilities.scalar_type(F)( + it.f.terms[output_index, :], + it.f.constants[output_index], + ) end function _index_map_to_oneto!(index_map, v::MOI.VariableIndex) From 39adba2bc037d1279353f409996d169d5d907b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 8 Jan 2025 22:53:54 +0100 Subject: [PATCH 26/41] Use SlackBridgePrimalDualStart (#253) * Use SlackBridgePrimalDualStart * Update src/copy_dual.jl * Remove test_broken * Add supports * Add comment * Move to AbstractModel --- Project.toml | 2 +- src/copy_dual.jl | 79 +++--------------------------------------------- src/diff_opt.jl | 19 ++++++++++++ test/jump.jl | 2 +- 4 files changed, 26 insertions(+), 76 deletions(-) diff --git a/Project.toml b/Project.toml index 6108c122..7bebbe8d 100644 --- a/Project.toml +++ b/Project.toml @@ -20,6 +20,6 @@ ChainRulesCore = "1" IterativeSolvers = "0.9" JuMP = "1" LazyArrays = "0.21, 0.22, 1" -MathOptInterface = "1.14.1" +MathOptInterface = "1.18" MathOptSetDistances = "0.2.7" julia = "1.6" diff --git a/src/copy_dual.jl b/src/copy_dual.jl index acb203ff..a7c24d23 100644 --- a/src/copy_dual.jl +++ b/src/copy_dual.jl @@ -11,45 +11,6 @@ struct ObjectiveFunctionAttribute{A,F} <: MOI.AbstractModelAttribute attr::A end -""" - struct ObjectiveDualStart <: MOI.AbstractModelAttribute end - -If the objective function had a dual, it would be `-1` for the Lagrangian -function to be the same. -When the `MOI.Bridges.Objective.SlackBridge` is used, it creates a constraint. -The dual of this constraint is therefore `-1` as well. -When setting this attribute, it allows to set the constraint dual of this -constraint. -""" -struct ObjectiveDualStart <: MOI.AbstractModelAttribute end -# Defining it for `MOI.set` leads to ambiguity -function MOI.throw_set_error_fallback( - ::MOI.ModelLike, - ::ObjectiveDualStart, - value, -) - return nothing -end - -""" - struct ObjectiveSlackGapPrimalStart <: MOI.AbstractModelAttribute end - -If the objective function had a dual, it would be `-1` for the Lagrangian -function to be the same. -When the `MOI.Bridges.Objective.SlackBridge` is used, it creates a constraint. -The dual of this constraint is therefore `-1` as well. -When setting this attribute, it allows to set the constraint dual of this -constraint. -""" -struct ObjectiveSlackGapPrimalStart <: MOI.AbstractModelAttribute end -function MOI.throw_set_error_fallback( - ::MOI.ModelLike, - ::ObjectiveSlackGapPrimalStart, - value, -) - return nothing -end - function MOI.get( b::MOI.Bridges.AbstractBridgeOptimizer, attr::ObjectiveFunctionAttribute{A,F}, @@ -100,11 +61,7 @@ end function MOI.set( b::MOI.Bridges.AbstractBridgeOptimizer, - attr::Union{ - ObjectiveDualStart, - ObjectiveSlackGapPrimalStart, - ForwardObjectiveFunction, - }, + attr::ForwardObjectiveFunction, value, ) if MOI.Bridges.is_objective_bridged(b) @@ -121,34 +78,6 @@ function MOI.set( end end -function MOI.set( - model::MOI.ModelLike, - ::ObjectiveFunctionAttribute{ObjectiveDualStart}, - b::MOI.Bridges.Objective.SlackBridge, - value, -) - return MOI.set(model, MOI.ConstraintDualStart(), b.constraint, value) -end - -function MOI.set( - model::MOI.ModelLike, - ::ObjectiveFunctionAttribute{ObjectiveSlackGapPrimalStart}, - b::MOI.Bridges.Objective.SlackBridge{T}, - value, -) where {T} - # `f(x) - slack = value` so `slack = f(x) - value` - fun = MOI.get(model, MOI.ConstraintFunction(), b.constraint) - set = MOI.get(model, MOI.ConstraintSet(), b.constraint) - MOI.Utilities.operate!(-, T, fun, MOI.constant(set)) - # `fun = f - slack` so we remove the term `-slack` to get `f` - f = MOI.Utilities.remove_variable(fun, b.slack) - f_val = MOI.Utilities.eval_variables(f) do v - return MOI.get(model, MOI.VariablePrimalStart(), v) - end - MOI.set(model, MOI.VariablePrimalStart(), b.slack, f_val - value) - return MOI.set(model, MOI.ConstraintPrimalStart(), b.constraint, value) -end - function _copy_dual(dest::MOI.ModelLike, src::MOI.ModelLike, index_map) vis_src = MOI.get(src, MOI.ListOfVariableIndices()) MOI.set( @@ -173,8 +102,10 @@ function _copy_dual(dest::MOI.ModelLike, src::MOI.ModelLike, index_map) MOI.ConstraintDual(), ) end - MOI.set(dest, ObjectiveDualStart(), -1.0) - return MOI.set(dest, ObjectiveSlackGapPrimalStart(), 0.0) + # Same as in `JuMP.set_start_values` + # Needed for models which bridge `min f(x)` into `min t such that t >= f(x)`. + MOI.set(dest, MOI.Bridges.Objective.SlackBridgePrimalDualStart(), nothing) + return end function _copy_constraint_start( diff --git a/src/diff_opt.jl b/src/diff_opt.jl index 2331a7b8..a8d802cf 100644 --- a/src/diff_opt.jl +++ b/src/diff_opt.jl @@ -318,6 +318,25 @@ function _enlarge_set(vec::Vector, idx, value) return end +# The following `supports` methods are needed because +# `MOI.set(::MOI.ModelLike, ::SlackBridgePrimalDualStart, ::SlackBridge, ::Nothing)` +# checks that the model supports these starting value attributes. +function MOI.supports( + ::AbstractModel, + ::MOI.VariablePrimalStart, + ::Type{<:MOI.VariableIndex}, +) + return true +end + +function MOI.supports( + ::AbstractModel, + ::Union{MOI.ConstraintDualStart,MOI.ConstraintPrimalStart}, + ::Type{<:MOI.ConstraintIndex}, +) + return true +end + function MOI.get( model::AbstractModel, ::MOI.VariablePrimalStart, diff --git a/test/jump.jl b/test/jump.jl index cacb27ae..8055f1f1 100644 --- a/test/jump.jl +++ b/test/jump.jl @@ -465,7 +465,7 @@ function test_differentiating_simple_socp() db = zeros(5) dc = zeros(3) MOI.set.(model, DiffOpt.ReverseVariablePrimal(), vv, 1.0) - @test_broken DiffOpt.reverse_differentiate!(model) + DiffOpt.reverse_differentiate!(model) # TODO add tests return end From 8526ac477da9882bb1ad197ea85df2f5bb080ff1 Mon Sep 17 00:00:00 2001 From: Joaquim Date: Fri, 31 Jan 2025 15:58:49 -0800 Subject: [PATCH 27/41] Integrate with POI to improve UX (#262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [WIP] Integrate with POI to improve UX * add missing import * temp change to proj toml * format * simplify method setting to sue model constructor * add possible fix to scalarize bridge error * add pkg to project * format * improvements * remove jump wrapper * clean tests * fix readme * use intermediary API * format * Apply suggestions from code review Co-authored-by: Benoît Legat * add suggestion * use Parameter set * todo was fixed * format * update docs for newer Flux * format * kwargs * remove diff model * suggestions * format * fix examples --------- Co-authored-by: Benoît Legat --- Project.toml | 4 +- README.md | 71 ++- docs/src/examples/custom-relu.jl | 29 +- docs/src/examples/polyhedral_project.jl | 29 +- docs/src/manual.md | 19 +- src/DiffOpt.jl | 5 + src/bridges.jl | 19 + src/diff_opt.jl | 11 + src/jump_moi_overloads.jl | 55 +- src/moi_wrapper.jl | 52 +- src/parameters.jl | 579 ++++++++++++++++++++ src/utils.jl | 17 + test/parameters.jl | 676 ++++++++++++++++++++++++ test/utils.jl | 14 +- 14 files changed, 1512 insertions(+), 68 deletions(-) create mode 100644 src/parameters.jl create mode 100644 test/parameters.jl diff --git a/Project.toml b/Project.toml index 7bebbe8d..726e1f6e 100644 --- a/Project.toml +++ b/Project.toml @@ -12,6 +12,7 @@ LazyArrays = "5078a376-72f3-5289-bfd5-ec5146d43c02" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" MathOptSetDistances = "3b969827-a86c-476c-9527-bb6f1a8fbad5" +ParametricOptInterface = "0ce4ce61-57bf-432b-a095-efac525d185e" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] @@ -21,5 +22,6 @@ IterativeSolvers = "0.9" JuMP = "1" LazyArrays = "0.21, 0.22, 1" MathOptInterface = "1.18" -MathOptSetDistances = "0.2.7" +MathOptSetDistances = "0.2.9" +ParametricOptInterface = "0.9.0" julia = "1.6" diff --git a/README.md b/README.md index a90b9a9b..2af52446 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,76 @@ examples, tutorials, and an API reference. ## Use with JuMP -Use DiffOpt with JuMP by following this brief example: +### DiffOpt-JuMP API with `Parameters` + +```julia +using JuMP, DiffOpt, HiGHS + +model = Model( + () -> DiffOpt.diff_optimizer( + HiGHS.Optimizer; + with_parametric_opt_interface = true, + ), +) +set_silent(model) + +p_val = 4.0 +pc_val = 2.0 +@variable(model, x) +@variable(model, p in Parameter(p_val)) +@variable(model, pc in Parameter(pc_val)) +@constraint(model, cons, pc * x >= 3 * p) +@objective(model, Min, 2x) +optimize!(model) +@show value(x) == 3 * p_val / pc_val + +# the function is +# x(p, pc) = 3p / pc +# hence, +# dx/dp = 3 / pc +# dx/dpc = -3p / pc^2 + +# First, try forward mode AD + +# differentiate w.r.t. p +direction_p = 3.0 +MOI.set(model, DiffOpt.ForwardConstraintSet(), ParameterRef(p), Parameter(direction_p)) +DiffOpt.forward_differentiate!(model) +@show MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) == direction_p * 3 / pc_val + +# update p and pc +p_val = 2.0 +pc_val = 6.0 +set_parameter_value(p, p_val) +set_parameter_value(pc, pc_val) +# re-optimize +optimize!(model) +# check solution +@show value(x) ≈ 3 * p_val / pc_val + +# stop differentiating with respect to p +DiffOpt.empty_input_sensitivities!(model) +# differentiate w.r.t. pc +direction_pc = 10.0 +MOI.set(model, DiffOpt.ForwardConstraintSet(), ParameterRef(pc), Parameter(direction_pc)) +DiffOpt.forward_differentiate!(model) +@show abs(MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) - + -direction_pc * 3 * p_val / pc_val^2) < 1e-5 + +# always a good practice to clear previously set sensitivities +DiffOpt.empty_input_sensitivities!(model) +# Now, reverse model AD +direction_x = 10.0 +MOI.set(model, DiffOpt.ReverseVariablePrimal(), x, direction_x) +DiffOpt.reverse_differentiate!(model) +@show MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(p)) == MOI.Parameter(direction_x * 3 / pc_val) +@show abs(MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(pc)).value - + -direction_x * 3 * p_val / pc_val^2) < 1e-5 +``` + +### Low level DiffOpt-JuMP API: + +A brief example: ```julia using JuMP, DiffOpt, HiGHS diff --git a/docs/src/examples/custom-relu.jl b/docs/src/examples/custom-relu.jl index 13e795e5..ce68ec5a 100644 --- a/docs/src/examples/custom-relu.jl +++ b/docs/src/examples/custom-relu.jl @@ -32,7 +32,7 @@ function matrix_relu( @variable(model, x[1:layer_size, 1:batch_size] >= 0) @objective(model, Min, x[:]'x[:] - 2y[:]'x[:]) optimize!(model) - return value.(x) + return Float32.(value.(x)) end # Define the reverse differentiation rule, for the function we defined above. @@ -42,9 +42,9 @@ function ChainRulesCore.rrule(::typeof(matrix_relu), y::Matrix{T}) where {T} function pullback_matrix_relu(dl_dx) ## some value from the backpropagation (e.g., loss) is denoted by `l` ## so `dl_dy` is the derivative of `l` wrt `y` - x = model[:x] # load decision variable `x` into scope - dl_dy = zeros(T, size(dl_dx)) - dl_dq = zeros(T, size(dl_dx)) + x = model[:x]::Matrix{JuMP.VariableRef} # load decision variable `x` into scope + dl_dy = zeros(T, size(x)) + dl_dq = zeros(T, size(x)) ## set sensitivities MOI.set.(model, DiffOpt.ReverseVariablePrimal(), x[:], dl_dx[:]) ## compute grad @@ -76,13 +76,13 @@ m = Flux.Chain( N = 1000 # batch size ## Preprocessing train data -imgs = MLDatasets.MNIST.traintensor(1:N) -labels = MLDatasets.MNIST.trainlabels(1:N) +imgs = MLDatasets.MNIST(; split = :train).features[:, :, 1:N] +labels = MLDatasets.MNIST(; split = :train).targets[1:N] train_X = float.(reshape(imgs, size(imgs, 1) * size(imgs, 2), N)) # stack images train_Y = Flux.onehotbatch(labels, 0:9); ## Preprocessing test data -test_imgs = MLDatasets.MNIST.testtensor(1:N) -test_labels = MLDatasets.MNIST.testlabels(1:N) +test_imgs = MLDatasets.MNIST(; split = :test).features[:, :, 1:N] +test_labels = MLDatasets.MNIST(; split = :test).targets[1:N]; test_X = float.(reshape(test_imgs, size(test_imgs, 1) * size(test_imgs, 2), N)) test_Y = Flux.onehotbatch(test_labels, 0:9); @@ -97,19 +97,12 @@ dataset = repeated((train_X, train_Y), epochs); # ## Network training # training loss function, Flux optimizer -custom_loss(x, y) = Flux.crossentropy(m(x), y) -opt = Flux.Adam() -evalcb = () -> @show(custom_loss(train_X, train_Y)) +custom_loss(m, x, y) = Flux.crossentropy(m(x), y) +opt = Flux.setup(Flux.Adam(), m) # Train to optimize network parameters -@time Flux.train!( - custom_loss, - Flux.params(m), - dataset, - opt, - cb = Flux.throttle(evalcb, 5), -); +@time Flux.train!(custom_loss, m, dataset, opt); # Although our custom implementation takes time, it is able to reach similar # accuracy as the usual ReLU function implementation. diff --git a/docs/src/examples/polyhedral_project.jl b/docs/src/examples/polyhedral_project.jl index e1013b94..ae2421cd 100644 --- a/docs/src/examples/polyhedral_project.jl +++ b/docs/src/examples/polyhedral_project.jl @@ -54,7 +54,7 @@ function (polytope::Polytope{N})( ) @objective(model, Min, dot(x - y, x - y)) optimize!(model) - return JuMP.value.(x) + return Float32.(JuMP.value.(x)) end # The `@functor` macro from Flux implements auxiliary functions for collecting the parameters of @@ -75,12 +75,12 @@ function ChainRulesCore.rrule( model = direct_model(DiffOpt.diff_optimizer(Ipopt.Optimizer)) xv = polytope(y; model = model) function pullback_matrix_projection(dl_dx) - layer_size, batch_size = size(dl_dx) dl_dx = ChainRulesCore.unthunk(dl_dx) ## `dl_dy` is the derivative of `l` wrt `y` - x = model[:x] + x = model[:x]::Matrix{JuMP.VariableRef} + layer_size, batch_size = size(x) ## grad wrt input parameters - dl_dy = zeros(size(dl_dx)) + dl_dy = zeros(size(x)) ## grad wrt layer parameters dl_dw = zero.(polytope.w) dl_db = zero(polytope.b) @@ -122,13 +122,13 @@ m = Flux.Chain( M = 500 # batch size ## Preprocessing train data -imgs = MLDatasets.MNIST.traintensor(1:M) -labels = MLDatasets.MNIST.trainlabels(1:M); +imgs = MLDatasets.MNIST(; split = :train).features[:, :, 1:M] +labels = MLDatasets.MNIST(; split = :train).targets[1:M] train_X = float.(reshape(imgs, size(imgs, 1) * size(imgs, 2), M)) # stack images train_Y = Flux.onehotbatch(labels, 0:9); ## Preprocessing test data -test_imgs = MLDatasets.MNIST.testtensor(1:M) -test_labels = MLDatasets.MNIST.testlabels(1:M) +test_imgs = MLDatasets.MNIST(; split = :test).features[:, :, 1:M] +test_labels = MLDatasets.MNIST(; split = :test).targets[1:M] test_X = float.(reshape(test_imgs, size(test_imgs, 1) * size(test_imgs, 2), M)) test_Y = Flux.onehotbatch(test_labels, 0:9); @@ -142,19 +142,12 @@ dataset = repeated((train_X, train_Y), epochs); # ## Network training # training loss function, Flux optimizer -custom_loss(x, y) = Flux.crossentropy(m(x), y) -opt = Flux.ADAM() -evalcb = () -> @show(custom_loss(train_X, train_Y)) +custom_loss(m, x, y) = Flux.crossentropy(m(x), y) +opt = Flux.setup(Flux.Adam(), m) # Train to optimize network parameters -@time Flux.train!( - custom_loss, - Flux.params(m), - dataset, - opt, - cb = Flux.throttle(evalcb, 5), -); +@time Flux.train!(custom_loss, m, dataset, opt); # Although our custom implementation takes time, it is able to reach similar # accuracy as the usual ReLU function implementation. diff --git a/docs/src/manual.md b/docs/src/manual.md index 7a3aeb09..bd761fe9 100644 --- a/docs/src/manual.md +++ b/docs/src/manual.md @@ -2,7 +2,7 @@ ## Supported objectives & constraints - scheme 1 -For `QPTH`/`OPTNET` style backend, the package supports following `Function-in-Set` constraints: +For `QuadraticProgram` backend, the package supports following `Function-in-Set` constraints: | MOI Function | MOI Set | |:-------|:---------------| @@ -29,9 +29,9 @@ and the following objective types: | `ScalarNonlinearFunction` | -## Supported objectives & constraints - scheme 2 +## Supported objectives & constraints - `ConicProgram` backend -For `DiffCP`/`CVXPY` style backend, the package supports following `Function-in-Set` constraints: +For the `ConicProgram` backend, the package supports following `Function-in-Set` constraints: | MOI Function | MOI Set | |:-------|:---------------| @@ -53,19 +53,16 @@ and the following objective types: | `VariableIndex` | | `ScalarAffineFunction` | +Other conic sets such as `RotatedSecondOrderCone` and `PositiveSemidefiniteConeSquare` are supported through bridges. -## Creating a differentiable optimizer + +## Creating a differentiable MOI optimizer You can create a differentiable optimizer over an existing MOI solver by using the `diff_optimizer` utility. ```@docs diff_optimizer ``` -## Adding new sets and constraints - -The DiffOpt `Optimizer` behaves similarly to other MOI Optimizers -and implements the `MOI.AbstractOptimizer` API. - ## Projections on cone sets DiffOpt requires taking projections and finding projection gradients of vectors while computing the jacobians. For this purpose, we use [MathOptSetDistances.jl](https://github.com/matbesancon/MathOptSetDistances.jl), which is a dedicated package for computing set distances, projections and projection gradients. @@ -107,6 +104,4 @@ In the light of above, DiffOpt differentiates program variables ``x``, ``s``, `` - OptNet: Differentiable Optimization as a Layer in Neural Networks ### Backward Pass vector -One possible point of confusion in finding Jacobians is the role of the backward pass vector - above eqn (7), *OptNet: Differentiable Optimization as a Layer in Neural Networks*. While differentiating convex programs, it is often the case that we don't want to find the actual derivatives, rather we might be interested in computing the product of Jacobians with a *backward pass vector*, often used in backprop in machine learning/automatic differentiation. This is what happens in scheme 1 of `DiffOpt` backend. - -But, for the conic system (scheme 2), we provide perturbations in conic data (`dA`, `db`, `dc`) to compute pertubations (`dx`, `dy`, `dz`) in input variables. Unlike the quadratic case, these perturbations are actual derivatives, not the product with a backward pass vector. This is an important distinction between the two schemes of differential optimization. +One possible point of confusion in finding Jacobians is the role of the backward pass vector - above eqn (7), *OptNet: Differentiable Optimization as a Layer in Neural Networks*. While differentiating convex programs, it is often the case that we don't want to find the actual derivatives, rather we might be interested in computing the product of Jacobians with a *backward pass vector*, often used in backpropagation in machine learning/automatic differentiation. This is what happens in `DiffOpt` backends. diff --git a/src/DiffOpt.jl b/src/DiffOpt.jl index b2d0d2f4..3733bf8b 100644 --- a/src/DiffOpt.jl +++ b/src/DiffOpt.jl @@ -12,12 +12,14 @@ import LazyArrays import LinearAlgebra import MathOptInterface as MOI import MathOptSetDistances as MOSD +import ParametricOptInterface as POI import SparseArrays include("utils.jl") include("product_of_sets.jl") include("diff_opt.jl") include("moi_wrapper.jl") +include("parameters.jl") include("jump_moi_overloads.jl") include("copy_dual.jl") @@ -42,4 +44,7 @@ end export diff_optimizer +# TODO +# add precompilation statements + end # module diff --git a/src/bridges.jl b/src/bridges.jl index 02299d65..2b0eb46b 100644 --- a/src/bridges.jl +++ b/src/bridges.jl @@ -43,6 +43,25 @@ function MOI.get( MOI.get(model, attr, bridge.vector_constraint), )[1] end + +function MOI.set( + model::MOI.ModelLike, + attr::ForwardConstraintFunction, + bridge::MOI.Bridges.Constraint.ScalarizeBridge, + value, +) + MOI.set.(model, attr, bridge.scalar_constraints, value) + return +end + +function MOI.get( + model::MOI.ModelLike, + attr::ReverseConstraintFunction, + bridge::MOI.Bridges.Constraint.ScalarizeBridge, +) + return _vectorize(MOI.get.(model, attr, bridge.scalar_constraints)) +end + function MOI.get( model::MOI.ModelLike, attr::DiffOpt.ReverseConstraintFunction, diff --git a/src/diff_opt.jl b/src/diff_opt.jl index a8d802cf..9e8ddaf6 100644 --- a/src/diff_opt.jl +++ b/src/diff_opt.jl @@ -62,6 +62,17 @@ The output solution differentials can be queried with the attribute """ function forward_differentiate! end +""" + empty_input_sensitivities!(model::MOI.ModelLike) + +Empty the input sensitivities of the model. +Sets to zero all the sensitivities set by the user with method such as: +- `MOI.set(model, DiffOpt.ReverseVariablePrimal(), variable_index, value)` +- `MOI.set(model, DiffOpt.ForwardObjectiveFunction(), expression)` +- `MOI.set(model, DiffOpt.ForwardConstraintFunction(), index, expression)` +""" +function empty_input_sensitivities! end + """ ForwardObjectiveFunction <: MOI.AbstractModelAttribute diff --git a/src/jump_moi_overloads.jl b/src/jump_moi_overloads.jl index 714139f4..4b100e2d 100644 --- a/src/jump_moi_overloads.jl +++ b/src/jump_moi_overloads.jl @@ -3,6 +3,15 @@ # Use of this source code is governed by an MIT-style license that can be found # in the LICENSE.md file or at https://opensource.org/licenses/MIT. +# FIXME +# Some function in this file are overloads to skip JuMP dirty state. +# Workaround for https://github.com/jump-dev/JuMP.jl/issues/2797 +# This workaround is necessary because once some attributes are set the JuMP +# model changes to a dirty state, then getting some attributes is blocked. +# However, getting and setting forward and backward sensitivities is +# done after the model is optimized, so we add function to bypass the +# dirty state. + function MOI.set( model::JuMP.Model, attr::ForwardObjectiveFunction, @@ -73,7 +82,7 @@ function MOI.get( return JuMP.jump_function(model, moi_func) end -# FIXME Workaround for https://github.com/jump-dev/JuMP.jl/issues/2797 +# see FIXME comment in the top of the file function _moi_get_result(model::MOI.ModelLike, args...) if MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED throw(OptimizeNotCalled()) @@ -99,6 +108,35 @@ function MOI.get( return _moi_get_result(JuMP.backend(model), attr, JuMP.index(var_ref)) end +function MOI.get( + model::JuMP.Model, + attr::ReverseConstraintSet, + var_ref::JuMP.ConstraintRef, +) + JuMP.check_belongs_to_model(var_ref, model) + return _moi_get_result(JuMP.backend(model), attr, JuMP.index(var_ref)) +end + +function MOI.set( + model::JuMP.Model, + attr::ForwardConstraintSet, + con_ref::JuMP.ConstraintRef, + set::MOI.AbstractScalarSet, +) + JuMP.check_belongs_to_model(con_ref, model) + return MOI.set(JuMP.backend(model), attr, JuMP.index(con_ref), set) +end + +function MOI.set( + model::JuMP.Model, + attr::ForwardConstraintSet, + con_ref::JuMP.ConstraintRef, + set::JuMP.AbstractScalarSet, +) + JuMP.check_belongs_to_model(con_ref, model) + return MOI.set(model, attr, con_ref, JuMP.moi_set(set)) +end + """ abstract type AbstractLazyScalarFunction <: MOI.AbstractScalarFunction end @@ -326,6 +364,11 @@ function forward_differentiate!(model::JuMP.Model) return forward_differentiate!(JuMP.backend(model)) end +function empty_input_sensitivities!(model::JuMP.Model) + empty_input_sensitivities!(JuMP.backend(model)) + return +end + # MOI.Utilities function reverse_differentiate!(model::MOI.Utilities.CachingOptimizer) @@ -336,6 +379,11 @@ function forward_differentiate!(model::MOI.Utilities.CachingOptimizer) return forward_differentiate!(model.optimizer) end +function empty_input_sensitivities!(model::MOI.Utilities.CachingOptimizer) + empty_input_sensitivities!(model.optimizer) + return +end + # MOIB function reverse_differentiate!(model::MOI.Bridges.AbstractBridgeOptimizer) @@ -345,3 +393,8 @@ end function forward_differentiate!(model::MOI.Bridges.AbstractBridgeOptimizer) return forward_differentiate!(model.model) end + +function empty_input_sensitivities!(model::MOI.Bridges.AbstractBridgeOptimizer) + empty_input_sensitivities!(model.model) + return +end diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index da054f75..98fc6b00 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -4,7 +4,7 @@ # in the LICENSE.md file or at https://opensource.org/licenses/MIT. """ - diff_optimizer(optimizer_constructor)::Optimizer + diff_optimizer(optimizer_constructor) Creates a `DiffOpt.Optimizer`, which is an MOI layer with an internal optimizer and other utility methods. Results (primal, dual and slack values) are obtained @@ -17,23 +17,38 @@ One define a differentiable model by using any solver of choice. Example: julia> import DiffOpt, HiGHS julia> model = DiffOpt.diff_optimizer(HiGHS.Optimizer) +julia> set_attribute(model, DiffOpt.ModelConstructor, DiffOpt.QuadraticProgram.Model) # optional selection of diff method julia> x = model.add_variable(model) julia> model.add_constraint(model, ...) ``` """ -function diff_optimizer(optimizer_constructor)::Optimizer - optimizer = - MOI.instantiate(optimizer_constructor; with_bridge_type = Float64) +function diff_optimizer( + optimizer_constructor; + with_parametric_opt_interface::Bool = false, + with_bridge_type = Float64, + with_cache::Bool = true, +) + optimizer = MOI.instantiate(optimizer_constructor; with_bridge_type) # When we do `MOI.copy_to(diff, optimizer)` we need to efficiently `MOI.get` # the model information from `optimizer`. However, 1) `optimizer` may not # implement some getters or it may be inefficient and 2) the getters may be # unimplemented or inefficient through some bridges. # For this reason we add a cache layer, the same cache JuMP adds. - caching_opt = MOI.Utilities.CachingOptimizer( - MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), - optimizer, - ) - return Optimizer(caching_opt) + caching_opt = if with_cache + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback( + MOI.Utilities.Model{with_bridge_type}(), + ), + optimizer, + ) + else + optimizer + end + if with_parametric_opt_interface + return POI.Optimizer(Optimizer(caching_opt)) + else + return Optimizer(caching_opt) + end end mutable struct Optimizer{OT<:MOI.ModelLike} <: MOI.AbstractOptimizer @@ -476,6 +491,14 @@ end Determines which subtype of [`DiffOpt.AbstractModel`](@ref) to use for differentiation. When set to `nothing`, the first one out of `model.model_constructors` that support the problem is used. + +Examples: + +```julia +julia> MOI.set(model, DiffOpt.ModelConstructor(), DiffOpt.QuadraticProgram.Model) + +julia> MOI.set(model, DiffOpt.ModelConstructor(), DiffOpt.ConicProgram.Model) +``` """ struct ModelConstructor <: MOI.AbstractOptimizerAttribute end @@ -557,6 +580,11 @@ function forward_differentiate!(model::Optimizer) return forward_differentiate!(diff) end +function empty_input_sensitivities!(model::Optimizer) + empty!(model.input_cache) + return +end + function _instantiate_with_bridges(model_constructor) model = MOI.Bridges.LazyBridgeOptimizer(MOI.instantiate(model_constructor)) # We don't add any variable bridge here because: @@ -595,7 +623,11 @@ function _diff(model::Optimizer) end if isnothing(model.diff) error( - "No differentiation model supports the problem. If you believe it should be supported, say by `DiffOpt.QuadraticProgram.Model`, use `MOI.set(model, DiffOpt.ModelConstructor, DiffOpt.QuadraticProgram.Model)` and try again to see an error indicating why it is not supported.", + "No differentiation model supports the problem. If you " * + "believe it should be supported, say by " * + "`DiffOpt.QuadraticProgram.Model`, use " * + "`MOI.set(model, DiffOpt.ModelConstructor, DiffOpt.QuadraticProgram.Model)`" * + "and try again to see an error indicating why it is not supported.", ) end else diff --git a/src/parameters.jl b/src/parameters.jl new file mode 100644 index 00000000..65798949 --- /dev/null +++ b/src/parameters.jl @@ -0,0 +1,579 @@ +# Copyright (c) 2020: Akshay Sharma and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +# block other methods + +MOI.supports(::POI.Optimizer, ::ForwardObjectiveFunction) = false + +function MOI.set(::POI.Optimizer, ::ForwardObjectiveFunction, _) + return error( + "Forward objective function is not supported when " * + "`with_parametric_opt_interface` is set to `true` in " * + "`diff_optimizer`." * + "Use parameters to set the forward sensitivity.", + ) +end + +MOI.supports(::POI.Optimizer, ::ForwardConstraintFunction) = false + +function MOI.set( + ::POI.Optimizer, + ::ForwardConstraintFunction, + ::MOI.ConstraintIndex, + _, +) + return error( + "Forward constraint function is not supported when " * + "`with_parametric_opt_interface` is set to `true` in " * + "`diff_optimizer`." * + "Use parameters to set the forward sensitivity.", + ) +end + +MOI.supports(::POI.Optimizer, ::ReverseObjectiveFunction) = false + +function MOI.get(::POI.Optimizer, ::ReverseObjectiveFunction) + return error( + "Reverse objective function is not supported when " * + "`with_parametric_opt_interface` is set to `true` in " * + "`diff_optimizer`." * + "Use parameters to get the reverse sensitivity.", + ) +end + +MOI.supports(::POI.Optimizer, ::ReverseConstraintFunction) = false + +function MOI.get( + ::POI.Optimizer, + ::ReverseConstraintFunction, + ::MOI.ConstraintIndex, +) + return error( + "Reverse constraint function is not supported when " * + "`with_parametric_opt_interface` is set to `true` in " * + "`diff_optimizer`." * + "Use parameters to get the reverse sensitivity.", + ) +end + +# functions to be used with ParametricOptInterface.jl + +struct ForwardConstraintSet <: MOI.AbstractConstraintAttribute end + +struct ReverseConstraintSet <: MOI.AbstractConstraintAttribute end + +mutable struct SensitivityData{T} + parameter_input_forward::Dict{MOI.VariableIndex,T} + parameter_output_backward::Dict{MOI.VariableIndex,T} +end + +function SensitivityData{T}() where {T} + return SensitivityData{T}( + Dict{MOI.VariableIndex,T}(), + Dict{MOI.VariableIndex,T}(), + ) +end + +const _SENSITIVITY_DATA = :_sensitivity_data + +function _get_sensitivity_data( + model::POI.Optimizer{T}, +)::SensitivityData{T} where {T} + _initialize_sensitivity_data!(model) + return model.ext[_SENSITIVITY_DATA]::SensitivityData{T} +end + +function _initialize_sensitivity_data!(model::POI.Optimizer{T}) where {T} + if !haskey(model.ext, _SENSITIVITY_DATA) + model.ext[_SENSITIVITY_DATA] = SensitivityData{T}() + end + return +end + +# forward mode + +function _constraint_set_forward!( + model::POI.Optimizer{T}, + affine_constraint_cache_dict, + ::Type{P}, +) where {T,P<:POI.ParametricAffineFunction} + sensitivity_data = _get_sensitivity_data(model) + for (inner_ci, pf) in affine_constraint_cache_dict + cte = zero(T) + terms = MOI.ScalarAffineTerm{T}[] + sizehint!(terms, 0) + for term in POI.affine_parameter_terms(pf) + p = term.variable + sensitivity = get(sensitivity_data.parameter_input_forward, p, 0.0) + cte += sensitivity * term.coefficient + end + if !iszero(cte) + MOI.set( + model.optimizer, + ForwardConstraintFunction(), + inner_ci, + MOI.ScalarAffineFunction{T}(terms, cte), + ) + end + end + return +end + +function _constraint_set_forward!( + model::POI.Optimizer{T}, + vector_affine_constraint_cache_dict, + ::Type{P}, +) where {T,P<:POI.ParametricVectorAffineFunction} + sensitivity_data = _get_sensitivity_data(model) + for (inner_ci, pf) in vector_affine_constraint_cache_dict + cte = zeros(T, length(pf.c)) + terms = MOI.VectorAffineTerm{T}[] + sizehint!(terms, 0) + 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 + if !iszero(cte) + MOI.set( + model.optimizer, + ForwardConstraintFunction(), + inner_ci, + MOI.VectorAffineFunction{T}(terms, cte), + ) + end + end + return +end + +function _constraint_set_forward!( + model::POI.Optimizer{T}, + quadratic_constraint_cache_dict, + ::Type{P}, +) where {T,P<:POI.ParametricQuadraticFunction} + sensitivity_data = _get_sensitivity_data(model) + for (inner_ci, pf) in quadratic_constraint_cache_dict + cte = zero(T) + terms = MOI.ScalarAffineTerm{T}[] + for term in POI.affine_parameter_terms(pf) + p = term.variable + sensitivity = get(sensitivity_data.parameter_input_forward, p, 0.0) + cte += sensitivity * term.coefficient + end + for term in POI.quadratic_parameter_parameter_terms(pf) + p_1 = term.variable_1 + p_2 = 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 += + sensitivity_1 * + term.coefficient * + MOI.get(model, MOI.VariablePrimal(), p_2) / + ifelse(term.variable_1 === term.variable_2, 2, 1) + cte += + sensitivity_2 * + term.coefficient * + MOI.get(model, MOI.VariablePrimal(), p_1) / + ifelse(term.variable_1 === term.variable_2, 2, 1) + end + sizehint!(terms, length(POI.quadratic_parameter_variable_terms(pf))) + for term in POI.quadratic_parameter_variable_terms(pf) + p = term.variable_1 + sensitivity = get(sensitivity_data.parameter_input_forward, p, NaN) + if !isnan(sensitivity) + push!( + terms, + MOI.ScalarAffineTerm{T}( + sensitivity * term.coefficient, + term.variable_2, + ), + ) + end + end + if !iszero(cte) || !isempty(terms) + MOI.set( + model.optimizer, + ForwardConstraintFunction(), + inner_ci, + MOI.ScalarAffineFunction{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}[] + pf = model.affine_objective_cache + sizehint!(terms, 0) + sensitivity_data = _get_sensitivity_data(model) + for term in POI.affine_parameter_terms(pf) + p = term.variable + sensitivity = get(sensitivity_data.parameter_input_forward, p, 0.0) + cte += sensitivity * term.coefficient + end + if !iszero(cte) + MOI.set( + model.optimizer, + ForwardObjectiveFunction(), + MOI.ScalarAffineFunction{T}(terms, cte), + ) + end + return +end + +function _quadratic_objective_set_forward!(model::POI.Optimizer{T}) where {T} + cte = zero(T) + pf = MOI.get( + model, + POI.ParametricObjectiveFunction{POI.ParametricQuadraticFunction{T}}(), + ) + sensitivity_data = _get_sensitivity_data(model) + for term in POI.affine_parameter_terms(pf) + p = term.variable + sensitivity = get(sensitivity_data.parameter_input_forward, p, 0.0) + cte += sensitivity * term.coefficient + end + for term in POI.quadratic_parameter_parameter_terms(pf) + p_1 = term.variable_1 + p_2 = 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 += + sensitivity_1 * + term.coefficient * + MOI.get(model, MOI.VariablePrimal(), p_2) / + ifelse(term.variable_1 === term.variable_2, 2, 1) + cte += sensitivity_2 * term.coefficient + MOI.get(model, MOI.VariablePrimal(), p_1) / + ifelse(term.variable_1 === term.variable_2, 2, 1) + end + terms = MOI.ScalarAffineTerm{T}[] + sizehint!(terms, length(POI.quadratic_parameter_variable_terms(pf))) + for term in POI.quadratic_parameter_variable_terms(pf) + p = term.variable_1 + sensitivity = get(sensitivity_data.parameter_input_forward, p, NaN) + if !isnan(sensitivity) + push!( + terms, + MOI.ScalarAffineTerm{T}( + sensitivity * term.coefficient, + term.variable_2, + ), + ) + end + end + if !iszero(cte) || !isempty(terms) + MOI.set( + model.optimizer, + ForwardObjectiveFunction(), + MOI.ScalarAffineFunction{T}(terms, cte), + ) + end + return +end + +function empty_input_sensitivities!(model::POI.Optimizer{T}) where {T} + empty_input_sensitivities!(model.optimizer) + model.ext[_SENSITIVITY_DATA] = SensitivityData{T}() + return +end + +function forward_differentiate!(model::POI.Optimizer{T}) where {T} + empty_input_sensitivities!(model.optimizer) + ctr_types = MOI.get(model, POI.ListOfParametricConstraintTypesPresent()) + for (F, S, P) in ctr_types + dict = MOI.get( + model, + POI.DictOfParametricConstraintIndicesAndFunctions{F,S,P}(), + ) + _constraint_set_forward!(model, dict, P) + end + obj_type = MOI.get(model, POI.ParametricObjectiveType()) + if obj_type <: POI.ParametricAffineFunction + _affine_objective_set_forward!(model) + elseif obj_type <: POI.ParametricQuadraticFunction + _quadratic_objective_set_forward!(model) + end + forward_differentiate!(model.optimizer) + return +end + +function MOI.set( + model::POI.Optimizer, + ::ForwardConstraintSet, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}, + set::MOI.Parameter, +) where {T} + variable = MOI.VariableIndex(ci.value) + if _is_variable(model, variable) + error("Trying to set a forward parameter sensitivity for a variable") + end + sensitivity_data = _get_sensitivity_data(model) + sensitivity_data.parameter_input_forward[variable] = set.value + return +end + +function MOI.get( + model::POI.Optimizer, + attr::ForwardVariablePrimal, + variable::MOI.VariableIndex, +) + if _is_parameter(model, variable) + error("Trying to get a forward variable sensitivity for a parameter") + end + return MOI.get(model.optimizer, attr, model.variables[variable]) +end + +# reverse mode + +function _constraint_get_reverse!( + model::POI.Optimizer{T}, + affine_constraint_cache_dict, + ::Type{P}, +) where {T,P<:POI.ParametricAffineFunction} + sensitivity_data = _get_sensitivity_data(model) + for (inner_ci, pf) in affine_constraint_cache_dict + terms = POI.affine_parameter_terms(pf) + if isempty(terms) + continue + end + grad_pf_cte = MOI.constant( + MOI.get(model.optimizer, ReverseConstraintFunction(), inner_ci), + ) + for term in terms + p = term.variable + value = get!(sensitivity_data.parameter_output_backward, p, 0.0) + sensitivity_data.parameter_output_backward[p] = + value + term.coefficient * grad_pf_cte + end + end + return +end + +function _constraint_get_reverse!( + model::POI.Optimizer{T}, + vector_affine_constraint_cache_dict, + ::Type{P}, +) where {T,P<:POI.ParametricVectorAffineFunction} + sensitivity_data = _get_sensitivity_data(model) + for (inner_ci, pf) in vector_affine_constraint_cache_dict + terms = POI.vector_affine_parameter_terms(pf) + if isempty(terms) + continue + end + grad_pf_cte = MOI.constant( + MOI.get(model.optimizer, ReverseConstraintFunction(), inner_ci), + ) + for term in 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 + end + return +end + +function _constraint_get_reverse!( + model::POI.Optimizer{T}, + quadratic_constraint_cache_dict, + ::Type{P}, +) where {T,P<:POI.ParametricQuadraticFunction} + sensitivity_data = _get_sensitivity_data(model) + for (inner_ci, pf) in quadratic_constraint_cache_dict + p_terms = POI.affine_parameter_terms(pf) + pp_terms = POI.quadratic_parameter_parameter_terms(pf) + pv_terms = POI.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.variable + value = get!(sensitivity_data.parameter_output_backward, p, 0.0) + sensitivity_data.parameter_output_backward[p] = + value + term.coefficient * grad_pf_cte + end + for term in pp_terms + p_1 = term.variable_1 + p_2 = 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) + # TODO: why there is no factor of 2 here???? + # ANS: probably because it was SET + sensitivity_data.parameter_output_backward[p_1] = + value_1 + + term.coefficient * + grad_pf_cte * + MOI.get(model, MOI.VariablePrimal(), p_2) / + ifelse(term.variable_1 === term.variable_2, 1, 1) + sensitivity_data.parameter_output_backward[p_2] = + value_2 + + term.coefficient * + grad_pf_cte * + MOI.get(model, MOI.VariablePrimal(), p_1) / + ifelse(term.variable_1 === term.variable_2, 1, 1) + end + for term in pv_terms + p = term.variable_1 + v = term.variable_2 # check if inner or outer (should be inner) + value = get!(sensitivity_data.parameter_output_backward, p, 0.0) + sensitivity_data.parameter_output_backward[p] = + value + term.coefficient * JuMP.coefficient(grad_pf, v) # * fixed value of the parameter ? + end + end + return +end + +function _affine_objective_get_reverse!(model::POI.Optimizer{T}) where {T} + pf = MOI.get( + model, + POI.ParametricObjectiveFunction{POI.ParametricAffineFunction{T}}(), + ) + terms = POI.affine_parameter_terms(pf) + if isempty(terms) + return + end + sensitivity_data = _get_sensitivity_data(model) + grad_pf = MOI.get(model.optimizer, ReverseObjectiveFunction()) + grad_pf_cte = MOI.constant(grad_pf) + for term in terms + p = term.variable + value = get!(sensitivity_data.parameter_output_backward, p, 0.0) + sensitivity_data.parameter_output_backward[p] = + value + term.coefficient * grad_pf_cte + end + return +end +function _quadratic_objective_get_reverse!(model::POI.Optimizer{T}) where {T} + pf = MOI.get( + model, + POI.ParametricObjectiveFunction{POI.ParametricQuadraticFunction{T}}(), + ) + p_terms = POI.affine_parameter_terms(pf) + pp_terms = POI.quadratic_parameter_parameter_terms(pf) + pv_terms = POI.quadratic_parameter_variable_terms(pf) + if isempty(p_terms) && isempty(pp_terms) && isempty(pv_terms) + return + end + sensitivity_data = _get_sensitivity_data(model) + grad_pf = MOI.get(model.optimizer, ReverseObjectiveFunction()) + grad_pf_cte = MOI.constant(grad_pf) + for term in p_terms + p = term.variable + value = get!(sensitivity_data.parameter_output_backward, p, 0.0) + sensitivity_data.parameter_output_backward[p] = + value + term.coefficient * grad_pf_cte + end + for term in pp_terms + p_1 = term.variable_1 + p_2 = 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.coefficient * + grad_pf_cte * + MOI.get(model, MOI.VariablePrimal(), p_2) / + ifelse(term.variable_1 === term.variable_2, 2, 1) + sensitivity_data.parameter_output_backward[p_2] = + value_2 + + term.coefficient * + grad_pf_cte * + MOI.get(model, MOI.VariablePrimal(), p_1) / + ifelse(term.variable_1 === term.variable_2, 2, 1) + end + for term in pv_terms + p = term.variable_1 + v = term.variable_2 # check if inner or outer (should be inner) + value = get!(sensitivity_data.parameter_output_backward, p, 0.0) + sensitivity_data.parameter_output_backward[p] = + value + term.coefficient * JuMP.coefficient(grad_pf, v) # * fixed value of the parameter ? + end + return +end + +function reverse_differentiate!(model::POI.Optimizer) + reverse_differentiate!(model.optimizer) + sensitivity_data = _get_sensitivity_data(model) + empty!(sensitivity_data.parameter_output_backward) + sizehint!( + sensitivity_data.parameter_output_backward, + length(model.parameters), + ) + ctr_types = MOI.get(model, POI.ListOfParametricConstraintTypesPresent()) + for (F, S, P) in ctr_types + dict = MOI.get( + model, + POI.DictOfParametricConstraintIndicesAndFunctions{F,S,P}(), + ) + _constraint_get_reverse!(model, dict, P) + end + obj_type = MOI.get(model, POI.ParametricObjectiveType()) + if obj_type <: POI.ParametricAffineFunction + _affine_objective_get_reverse!(model) + elseif obj_type <: POI.ParametricQuadraticFunction + _quadratic_objective_get_reverse!(model) + end + return +end + +function _is_parameter( + model::POI.Optimizer{T}, + variable::MOI.VariableIndex, +) where {T} + return MOI.is_valid( + model, + MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}(variable.value), + ) +end + +function _is_variable( + model::POI.Optimizer{T}, + variable::MOI.VariableIndex, +) where {T} + return MOI.is_valid(model, variable) && + !MOI.is_valid( + model, + MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}(variable.value), + ) +end + +function MOI.set( + model::POI.Optimizer, + attr::ReverseVariablePrimal, + variable::MOI.VariableIndex, + value::Number, +) + if _is_parameter(model, variable) + error("Trying to set a backward variable sensitivity for a parameter") + end + MOI.set(model.optimizer, attr, variable, value) + return +end + +MOI.is_set_by_optimize(::ReverseConstraintSet) = true + +function MOI.get( + model::POI.Optimizer, + ::ReverseConstraintSet, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}, +) where {T} + variable = MOI.VariableIndex(ci.value) + if _is_variable(model, variable) + error("Trying to get a backward parameter sensitivity for a variable") + end + sensitivity_data = _get_sensitivity_data(model) + return MOI.Parameter{T}( + get(sensitivity_data.parameter_output_backward, variable, 0.0), + ) +end diff --git a/src/utils.jl b/src/utils.jl index dad12f7a..d6764c1f 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -3,6 +3,8 @@ # Use of this source code is governed by an MIT-style license that can be found # in the LICENSE.md file or at https://opensource.org/licenses/MIT. +# Helpers for objective function + # Representation of MOI functions using SparseArrays # Might be able to replace in the future by a function in MOI, see # https://github.com/jump-dev/MathOptInterface.jl/pull/1238 @@ -152,6 +154,8 @@ function sparse_array_representation( ) end +# Helpers for scalar constraints + # In the future, we could replace by https://github.com/jump-dev/MathOptInterface.jl/pull/1238 """ VectorScalarAffineFunction{T, VT} <: MOI.AbstractScalarFunction @@ -228,6 +232,8 @@ function JuMP.coefficient( return JuMP.coefficient(func.affine, vi) end +# Helpers for Vector constraints + """ MatrixVectorAffineFunction{T, VT} <: MOI.AbstractVectorFunction @@ -261,6 +267,17 @@ function standard_form(func::MatrixVectorAffineFunction{T}) where {T} return convert(MOI.VectorAffineFunction{T}, func) end +_get_terms(f::VectorScalarAffineFunction) = f.terms +_get_constant(f::VectorScalarAffineFunction) = f.constant +function _vectorize(vec::Vector{T}) where {T<:VectorScalarAffineFunction} + terms = LazyArrays.@~ LazyArrays.ApplyArray(hcat, _get_terms.(vec)...)' + constants = LazyArrays.ApplyArray(vcat, _get_constant.(vec)...) + AT = typeof(terms) + VT = typeof(constants) + ret = MatrixVectorAffineFunction{AT,VT}(terms, constants) + return ret +end + # Only used for testing at the moment so performance is not critical so # converting to standard form is ok function MOIU.isapprox_zero( diff --git a/test/parameters.jl b/test/parameters.jl new file mode 100644 index 00000000..294fa1e8 --- /dev/null +++ b/test/parameters.jl @@ -0,0 +1,676 @@ +# Copyright (c) 2020: Akshay Sharma and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +# using Revise + +module TestParameters + +using Test +using JuMP +import DiffOpt +import MathOptInterface as MOI +import HiGHS +import SCS + +function Base.isapprox(x::MOI.Parameter, y::MOI.Parameter; atol = 1e-10) + return isapprox(x.value, y.value; atol = atol) +end + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$name", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +function test_diff_rhs() + model = Model( + () -> DiffOpt.diff_optimizer( + HiGHS.Optimizer; + with_parametric_opt_interface = true, + ), + ) + set_silent(model) + @variable(model, x) + @variable(model, p in Parameter(3.0)) + @constraint(model, cons, x >= 3 * p) + @objective(model, Min, 2x) + optimize!(model) + @test value(x) ≈ 9 + # the function is + # x(p) = 3p, hence x'(p) = 3 + # differentiate w.r.t. p + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(1.0), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ 3 + # again with different "direction" + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(2.0), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ 6 + # + set_parameter_value(p, 2.0) + optimize!(model) + @test value(x) ≈ 6 + # differentiate w.r.t. p + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(1.0), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ 3 + # again with different "direction" + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(2.0), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ 6 + # + # test reverse mode + # + MOI.set(model, DiffOpt.ReverseVariablePrimal(), x, 1) + DiffOpt.reverse_differentiate!(model) + @test MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(p)) ≈ + MOI.Parameter(3.0) + # again with different "direction" + MOI.set(model, DiffOpt.ReverseVariablePrimal(), x, 2) + DiffOpt.reverse_differentiate!(model) + @test MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(p)) ≈ + MOI.Parameter(6.0) + return +end + +function test_diff_vector_rhs() + model = direct_model( + DiffOpt.diff_optimizer( + SCS.Optimizer; + with_parametric_opt_interface = true, + ), + ) + set_silent(model) + @variable(model, x) + @variable(model, p in Parameter(3.0)) + @constraint(model, cons, [x - 3 * p] in MOI.Zeros(1)) + + # FIXME + @constraint(model, fake_soc, [0, 0, 0] in SecondOrderCone()) + + @objective(model, Min, 2x) + optimize!(model) + @test isapprox(value(x), 9, atol = 1e-3) + # the function is + # x(p) = 3p, hence x'(p) = 3 + # differentiate w.r.t. p + for p_val in 0:3 + set_parameter_value(p, p_val) + optimize!(model) + @test isapprox(value(x), 3 * p_val, atol = 1e-3) + for direction in 0.0:3.0 + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(direction), + ) + DiffOpt.forward_differentiate!(model) + @test isapprox( + MOI.get(model, DiffOpt.ForwardVariablePrimal(), x), + direction * 3, + atol = 1e-3, + ) + # reverse mode + MOI.set(model, DiffOpt.ReverseVariablePrimal(), x, direction) + DiffOpt.reverse_differentiate!(model) + @test isapprox( + MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(p)), + MOI.Parameter(direction * 3), + atol = 1e-3, + ) + end + end + return +end + +function test_affine_changes() + model = Model( + () -> DiffOpt.diff_optimizer( + HiGHS.Optimizer; + with_parametric_opt_interface = true, + ), + ) + set_silent(model) + p_val = 3.0 + pc_val = 1.0 + @variable(model, x) + @variable(model, p in Parameter(p_val)) + @variable(model, pc in Parameter(pc_val)) + @constraint(model, cons, pc * x >= 3 * p) + @objective(model, Min, 2x) + optimize!(model) + @test value(x) ≈ 3 * p_val / pc_val + # the function is + # x(p, pc) = 3p / pc, hence dx/dp = 3 / pc, dx/dpc = -3p / pc^2 + # differentiate w.r.t. p + for direction_p in 1.0:2.0 + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(direction_p), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ + direction_p * 3 / pc_val + end + # update p + p_val = 2.0 + set_parameter_value(p, p_val) + optimize!(model) + @test value(x) ≈ 3 * p_val / pc_val + # differentiate w.r.t. p + for direction_p in 1.0:2.0 + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(direction_p), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ + direction_p * 3 / pc_val + end + # differentiate w.r.t. pc + # stop differentiating with respect to p + direction_p = 0.0 + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(direction_p), + ) + for direction_pc in 1.0:2.0 + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(pc), + Parameter(direction_pc), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ + -direction_pc * 3 * p_val / pc_val^2 + end + # update pc + pc_val = 2.0 + set_parameter_value(pc, pc_val) + optimize!(model) + @test value(x) ≈ 3 * p_val / pc_val + for direction_pc in 1.0:2.0 + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(pc), + Parameter(direction_pc), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ + -direction_pc * 3 * p_val / pc_val^2 + end + # test combines directions + for direction_pc in 1:2, direction_p in 1:2 + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(direction_p), + ) + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(pc), + Parameter(direction_pc), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ + -direction_pc * 3 * p_val / pc_val^2 + direction_p * 3 / pc_val + end + return +end + +function test_affine_changes_compact() + model = Model( + () -> DiffOpt.diff_optimizer( + HiGHS.Optimizer; + with_parametric_opt_interface = true, + ), + ) + set_silent(model) + p_val = 3.0 + pc_val = 1.0 + @variable(model, x) + @variable(model, p in Parameter(p_val)) + @variable(model, pc in Parameter(pc_val)) + @constraint(model, cons, pc * x >= 3 * p) + @objective(model, Min, 2x) + # the function is + # x(p, pc) = 3p / pc, hence dx/dp = 3 / pc, dx/dpc = -3p / pc^2 + for p_val in 1:3, pc_val in 1:3 + set_parameter_value(p, p_val) + set_parameter_value(pc, pc_val) + optimize!(model) + @test value(x) ≈ 3 * p_val / pc_val + for direction_pc in 0.0:2.0, direction_p in 0.0:2.0 + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(direction_p), + ) + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(pc), + Parameter(direction_pc), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ + -direction_pc * 3 * p_val / pc_val^2 + + direction_p * 3 / pc_val + end + for direction_x in 0:2 + MOI.set(model, DiffOpt.ReverseVariablePrimal(), x, direction_x) + DiffOpt.reverse_differentiate!(model) + @test MOI.get( + model, + DiffOpt.ReverseConstraintSet(), + ParameterRef(p), + ) ≈ MOI.Parameter(direction_x * 3 / pc_val) + @test MOI.get( + model, + DiffOpt.ReverseConstraintSet(), + ParameterRef(pc), + ) ≈ MOI.Parameter(-direction_x * 3 * p_val / pc_val^2) + end + end + return +end + +function test_quadratic_rhs_changes() + model = Model( + () -> DiffOpt.diff_optimizer( + HiGHS.Optimizer; + with_parametric_opt_interface = true, + ), + ) + set_silent(model) + p_val = 2.0 + q_val = 2.0 + r_val = 2.0 + s_val = 2.0 + t_val = 2.0 + @variable(model, x) + @variable(model, p in Parameter(p_val)) + @variable(model, q in Parameter(q_val)) + @variable(model, r in Parameter(r_val)) + @variable(model, s in Parameter(s_val)) + @variable(model, t in Parameter(t_val)) + @constraint(model, cons, 11 * t * x >= 1 + 3 * p * q + 5 * r^2 + 7 * s) + @objective(model, Min, 2x) + # the function is + # x(p, q, r, s, t) = (1 + 3pq + 5r^2 + 7s) / (11t) + # hence + # dx/dp = 3q / (11t) + # dx/dq = 3p / (11t) + # dx/dr = 10r / (11t) + # dx/ds = 7 / (11t) + # dx/dt = - (1 + 3pq + 5r^2 + 7s) / (11t^2) + optimize!(model) + @test value(x) ≈ + (1 + 3 * p_val * q_val + 5 * r_val^2 + 7 * s_val) / (11 * t_val) + for p_val in 2:3, q_val in 2:3, r_val in 2:3, s_val in 2:3, t_val in 2:3 + set_parameter_value(p, p_val) + set_parameter_value(q, q_val) + set_parameter_value(r, r_val) + set_parameter_value(s, s_val) + set_parameter_value(t, t_val) + optimize!(model) + @test value(x) ≈ + (1 + 3 * p_val * q_val + 5 * r_val^2 + 7 * s_val) / (11 * t_val) + for dir_p in 0.0:2.0, + dir_q in 0.0:2.0, + dir_r in 0.0:2.0, + dir_s in 0.0:2.0, + dir_t in 0.0:2.0 + + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(dir_p), + ) + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(q), + Parameter(dir_q), + ) + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(r), + Parameter(dir_r), + ) + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(s), + Parameter(dir_s), + ) + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(t), + Parameter(dir_t), + ) + DiffOpt.forward_differentiate!(model) + @test isapprox( + MOI.get(model, DiffOpt.ForwardVariablePrimal(), x), + dir_p * 3 * q_val / (11 * t_val) + + dir_q * 3 * p_val / (11 * t_val) + + dir_r * 10 * r_val / (11 * t_val) + + dir_s * 7 / (11 * t_val) + + dir_t * ( + -(1 + 3 * p_val * q_val + 5 * r_val^2 + 7 * s_val) / + (11 * t_val^2) + ), + atol = 1e-10, + ) + end + for dir_x in 0:3 + MOI.set(model, DiffOpt.ReverseVariablePrimal(), x, dir_x) + DiffOpt.reverse_differentiate!(model) + @test isapprox( + MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(p)), + MOI.Parameter(dir_x * 3 * q_val / (11 * t_val)), + atol = 1e-10, + ) + @test isapprox( + MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(q)), + MOI.Parameter(dir_x * 3 * p_val / (11 * t_val)), + atol = 1e-10, + ) + @test isapprox( + MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(r)), + MOI.Parameter(dir_x * 10 * r_val / (11 * t_val)), + atol = 1e-10, + ) + @test isapprox( + MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(s)), + MOI.Parameter(dir_x * 7 / (11 * t_val)), + atol = 1e-10, + ) + @test isapprox( + MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(t)), + MOI.Parameter( + dir_x * ( + -(1 + 3 * p_val * q_val + 5 * r_val^2 + 7 * s_val) / + (11 * t_val^2) + ), + ), + atol = 1e-10, + ) + end + end + return +end + +function test_affine_changes_compact_max() + model = Model( + () -> DiffOpt.diff_optimizer( + HiGHS.Optimizer; + with_parametric_opt_interface = true, + ), + ) + set_silent(model) + p_val = 3.0 + pc_val = 1.0 + @variable(model, x) + @variable(model, p in Parameter(p_val)) + @variable(model, pc in Parameter(pc_val)) + @constraint(model, cons, pc * x >= 3 * p) + @objective(model, Max, -2x) + # the function is + # x(p, pc) = 3p / pc, hence dx/dp = 3 / pc, dx/dpc = -3p / pc^2 + for p_val in 1:3, pc_val in 1:3 + set_parameter_value(p, p_val) + set_parameter_value(pc, pc_val) + optimize!(model) + @test value(x) ≈ 3 * p_val / pc_val + for direction_pc in 0.0:2.0, direction_p in 0.0:2.0 + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(direction_p), + ) + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(pc), + Parameter(direction_pc), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ + -direction_pc * 3 * p_val / pc_val^2 + + direction_p * 3 / pc_val + end + end + return +end + +function test_diff_affine_objective() + model = Model( + () -> DiffOpt.diff_optimizer( + HiGHS.Optimizer; + with_parametric_opt_interface = true, + ), + ) + set_silent(model) + p_val = 3.0 + @variable(model, x) + @variable(model, p in Parameter(p_val)) + @constraint(model, cons, x >= 3) + @objective(model, Min, 2x + 3p) + # x(p, pc) = 3, hence dx/dp = 0 + for p_val in 1:2 + set_parameter_value(p, p_val) + optimize!(model) + @test value(x) ≈ 3 + for direction_p in 0.0:2.0 + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(direction_p), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ 0.0 + # reverse mode + MOI.set(model, DiffOpt.ReverseVariablePrimal(), x, direction_p) + DiffOpt.reverse_differentiate!(model) + @test MOI.get( + model, + DiffOpt.ReverseConstraintSet(), + ParameterRef(p), + ) ≈ MOI.Parameter(0.0) + end + end + return +end + +function test_diff_quadratic_objective() + model = Model( + () -> DiffOpt.diff_optimizer( + HiGHS.Optimizer; + with_parametric_opt_interface = true, + ), + ) + set_silent(model) + p_val = 3.0 + @variable(model, x) + @variable(model, p in Parameter(p_val)) + @constraint(model, cons, x >= 3) + @objective(model, Min, p * x) + # x(p, pc) = 3, hence dx/dp = 0 + for p_val in 1:2 + set_parameter_value(p, p_val) + optimize!(model) + @test value(x) ≈ 3 + for direction_p in 0.0:2.0 + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(direction_p), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ 0.0 + # reverse mode + MOI.set(model, DiffOpt.ReverseVariablePrimal(), x, direction_p) + DiffOpt.reverse_differentiate!(model) + @test MOI.get( + model, + DiffOpt.ReverseConstraintSet(), + ParameterRef(p), + ) ≈ MOI.Parameter(0.0) + end + end + return +end + +function test_quadratic_objective_qp() + model = Model( + () -> DiffOpt.diff_optimizer( + HiGHS.Optimizer; + with_parametric_opt_interface = true, + ), + ) + set_silent(model) + p_val = 3.0 + @variable(model, x) + @variable(model, p in Parameter(p_val)) + @constraint(model, cons, x >= -10) + @objective(model, Min, 3 * p * x + x * x + 5 * p + 7 * p^2) + # 2x + 3p = 0, hence x = -3p/2 + # hence dx/dp = -3/2 + for p_val in 3:3 + set_parameter_value(p, p_val) + optimize!(model) + @test value(x) ≈ -3p_val / 2 atol = 1e-4 + for direction_p in 0.0:2.0 + MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(p), + Parameter(direction_p), + ) + DiffOpt.forward_differentiate!(model) + @test MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) ≈ + direction_p * (-3 / 2) atol = 1e-4 + # reverse mode + MOI.set(model, DiffOpt.ReverseVariablePrimal(), x, direction_p) + DiffOpt.reverse_differentiate!(model) + @test MOI.get( + model, + DiffOpt.ReverseConstraintSet(), + ParameterRef(p), + ) ≈ MOI.Parameter(direction_p * (-3 / 2)) atol = 1e-4 + end + end + return +end + +function test_diff_errors() + model = Model( + () -> DiffOpt.diff_optimizer( + HiGHS.Optimizer; + with_parametric_opt_interface = true, + ), + ) + set_silent(model) + @variable(model, x) + @variable(model, p in Parameter(3.0)) + @constraint(model, cons, x >= 3 * p) + @objective(model, Min, 2x) + optimize!(model) + @test value(x) ≈ 9 + + @test_throws ErrorException MOI.set( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef(x), + Parameter(1.0), + ) + @test_throws ErrorException MOI.set( + model, + DiffOpt.ReverseVariablePrimal(), + p, + 1, + ) + @test_throws ErrorException MOI.get( + model, + DiffOpt.ForwardVariablePrimal(), + p, + ) + @test_throws ErrorException MOI.get( + model, + DiffOpt.ReverseConstraintSet(), + ParameterRef(x), + ) + + @test_throws ErrorException MOI.set( + model, + DiffOpt.ForwardObjectiveFunction(), + 3 * x, + ) + @test_throws ErrorException MOI.set( + model, + DiffOpt.ForwardConstraintFunction(), + cons, + 1 + 7 * x, + ) + @test_throws ErrorException MOI.get( + model, + DiffOpt.ReverseObjectiveFunction(), + ) + @test_throws ErrorException MOI.get( + model, + DiffOpt.ReverseConstraintFunction(), + cons, + ) + + return +end + +end # module + +TestParameters.runtests() diff --git a/test/utils.jl b/test/utils.jl index 10e37eac..49cfac38 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -36,7 +36,7 @@ PMLR, 2017. https://arxiv.org/pdf/1703.00443.pdf """ function qp_test( solver, - diff_model, + _diff_model, lt::Bool, set_zero::Bool, canonicalize::Bool; @@ -88,7 +88,7 @@ function qp_test( atol = ATOL, rtol = RTOL, ) - is_conic_qp = !all(iszero, Q) && diff_model == DiffOpt.ConicProgram.Model + is_conic_qp = !all(iszero, Q) && _diff_model == DiffOpt.ConicProgram.Model n = length(q) @assert n == LinearAlgebra.checksquare(Q) @assert n == size(A, 2) @@ -162,7 +162,7 @@ function qp_test( end @_test(convert(Vector{Float64}, _λ), λ) - MOI.set(model, DiffOpt.ModelConstructor(), diff_model) + MOI.set(model, DiffOpt.ModelConstructor(), _diff_model) #dobjb = v' * (dQb / 2.0) * v + dqb' * v # TODO, it should .- @@ -344,7 +344,7 @@ function qp_test( return end -function qp_test(solver, diff_model; kws...) +function qp_test(solver, _diff_model; kws...) @testset "With $(lt ? "LessThan" : "GreaterThan") constraints" for lt in [ true, #false, @@ -359,7 +359,7 @@ function qp_test(solver, diff_model; kws...) true, #false, ] - qp_test(solver, diff_model, lt, set_zero, canonicalize; kws...) + qp_test(solver, _diff_model, lt, set_zero, canonicalize; kws...) end end end @@ -367,11 +367,11 @@ function qp_test(solver, diff_model; kws...) end function qp_test(solver; kws...) - @testset "With $diff_model" for diff_model in [ + @testset "With $_diff_model" for _diff_model in [ DiffOpt.QuadraticProgram.Model, DiffOpt.ConicProgram.Model, ] - qp_test(solver, diff_model; kws...) + qp_test(solver, _diff_model; kws...) end return end From 066aef63f5ca3f2a255aff2eac27f2fa5af35bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 1 Feb 2025 18:16:15 +0100 Subject: [PATCH 28/41] Add error for missing starting value (#269) --- src/ConicProgram/ConicProgram.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/ConicProgram/ConicProgram.jl b/src/ConicProgram/ConicProgram.jl index 69df7e09..89a79183 100644 --- a/src/ConicProgram/ConicProgram.jl +++ b/src/ConicProgram/ConicProgram.jl @@ -183,6 +183,18 @@ function _gradient_cache(model::Model) ) b = model.model.constraints.constants + if any(isnan, model.y) || length(model.y) < length(b) + error( + "Some constraints are missing a value for the `ConstraintDualStart` attribute.", + ) + end + + if any(isnan, model.s) || length(model.s) < length(b) + error( + "Some constraints are missing a value for the `ConstraintPrimalStart` attribute.", + ) + end + if MOI.get(model, MOI.ObjectiveSense()) == MOI.FEASIBILITY_SENSE c = SparseArrays.spzeros(size(A, 2)) else From 61123b93eb1ae07839fab6c5a239a96705ec64c7 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 5 Feb 2025 11:47:15 -0500 Subject: [PATCH 29/41] update API --- Project.toml | 2 +- src/NonLinearProgram/NonLinearProgram.jl | 25 +++++++++----- src/NonLinearProgram/nlp_utilities.jl | 2 +- src/diff_opt.jl | 42 ++---------------------- src/jump_moi_overloads.jl | 9 ----- src/moi_wrapper.jl | 28 ++++------------ src/parameters.jl | 14 +++++--- test/nlp_program.jl | 10 +++--- 8 files changed, 44 insertions(+), 88 deletions(-) diff --git a/Project.toml b/Project.toml index 726e1f6e..a3689608 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "DiffOpt" uuid = "930fe3bc-9c6b-11ea-2d94-6184641e85e7" -authors = ["Akshay Sharma", "Mathieu Besançon", "Joaquim Dias Garcia", "Benoît Legat", "Oscar Dowson"] +authors = ["Akshay Sharma", "Mathieu Besançon", "Joaquim Dias Garcia", "Benoît Legat", "Oscar Dowson", "Andrew Rosemberg"] version = "0.5.0" [deps] diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 547dc258..3469bb4d 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -1,4 +1,4 @@ -# Copyright (c) 2020: Andrew Rosemberg and contributors +# Copyright (c) 2025: Andrew Rosemberg and contributors # # Use of this source code is governed by an MIT-style license that can be found # in the LICENSE.md file or at https://opensource.org/licenses/MIT. @@ -41,6 +41,7 @@ mutable struct Form <: MOI.ModelLike sense::MOI.OptimizationSense list_of_constraint::MOI.Utilities.DoubleDicts.IndexDoubleDict var2param::Dict{MOI.VariableIndex,MOI.Nonlinear.ParameterIndex} + var2ci::Dict{MOI.VariableIndex,MOI.ConstraintIndex} upper_bounds::Dict{Int,Float64} lower_bounds::Dict{Int,Float64} constraint_upper_bounds::Dict{Int,MOI.ConstraintIndex} @@ -65,6 +66,7 @@ function Form() MOI.MIN_SENSE, MOI.Utilities.DoubleDicts.IndexDoubleDict(), Dict{MOI.VariableIndex,MOI.Nonlinear.ParameterIndex}(), + Dict{MOI.VariableIndex,MOI.ConstraintIndex}(), Dict{Int,Float64}(), Dict{Int,Float64}(), Dict{Int,MOI.ConstraintIndex}(), @@ -187,8 +189,9 @@ function MOI.add_constraint( form.num_constraints += 1 p = MOI.Nonlinear.add_parameter(form.model, set.value) form.var2param[idx] = p - idx = MOI.ConstraintIndex{F,S}(form.num_constraints) - return idx + idx_ci = MOI.ConstraintIndex{F,S}(form.num_constraints) + form.var2ci[idx] = idx_ci + return idx_ci end function MOI.add_constraint( @@ -434,7 +437,8 @@ end function DiffOpt.forward_differentiate!(model::Model) model.diff_time = @elapsed begin cache = _cache_evaluator!(model) - Δp = [model.input_cache.dp[i] for i in cache.params] + form = model.model + Δp = [model.input_cache.dp[form.var2ci[i]] for i in cache.params] # Compute Jacobian Δs = compute_sensitivity(model) @@ -494,6 +498,10 @@ function DiffOpt.reverse_differentiate!(model::Model) Δw[cache.index_duals] = Δdual Δp = Δs' * Δw + # Order by ConstraintIndex + varorder = sort(collect(keys(form.var2ci)); by = x -> form.var2ci[x].value) + Δp = [Δp[form.var2param[var_idx].value] for var_idx in varorder] + model.back_grad_cache = ReverseCache(; Δp = Δp) end return nothing @@ -528,11 +536,10 @@ end function MOI.get( model::Model, - ::DiffOpt.ReverseParameter, - pi::MOI.VariableIndex, -) - form = model.model - return model.back_grad_cache.Δp[form.var2param[pi].value] + ::DiffOpt.ReverseConstraintSet, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}, +) where {T} + return MOI.Parameter{T}(model.back_grad_cache.Δp[ci.value],) end end # module NonLinearProgram diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 62cb7303..0e1c8d37 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -1,4 +1,4 @@ -# Copyright (c) 2020: Andrew Rosemberg and contributors +# Copyright (c) 2025: Andrew Rosemberg and contributors # # Use of this source code is governed by an MIT-style license that can be found # in the LICENSE.md file or at https://opensource.org/licenses/MIT. diff --git a/src/diff_opt.jl b/src/diff_opt.jl index 9e8ddaf6..6fc9daef 100644 --- a/src/diff_opt.jl +++ b/src/diff_opt.jl @@ -13,7 +13,7 @@ const MOIDD = MOI.Utilities.DoubleDicts Base.@kwdef mutable struct InputCache dx::Dict{MOI.VariableIndex,Float64} = Dict{MOI.VariableIndex,Float64}()# dz for QP - dp::Dict{MOI.VariableIndex,Float64} = Dict{MOI.VariableIndex,Float64}() + dp::Dict{MOI.ConstraintIndex,Float64} = Dict{MOI.ConstraintIndex,Float64}() dy::Dict{MOI.ConstraintIndex,Float64} = Dict{MOI.ConstraintIndex,Float64}() # ds # dy #= [d\lambda, d\nu] for QP @@ -139,19 +139,9 @@ MOI.set(model, DiffOpt.ReverseVariablePrimal(), x) """ struct ReverseVariablePrimal <: MOI.AbstractVariableAttribute end -""" - ForwardParameter <: MOI.AbstractVariableAttribute +struct ForwardConstraintSet <: MOI.AbstractConstraintAttribute end -A `MOI.AbstractVariableAttribute` to set input data to forward differentiation, -that is, problem input data. - -For instance, to set the tangent of the variable of index `vi`, do the -following: -```julia -MOI.set(model, DiffOpt.ForwardParameter(), x) -``` -""" -struct ForwardParameter <: MOI.AbstractVariableAttribute end +struct ReverseConstraintSet <: MOI.AbstractConstraintAttribute end """ ReverseConstraintDual <: MOI.AbstractConstraintAttribute @@ -165,22 +155,6 @@ MOI.set(model, DiffOpt.ReverseConstraintDual(), x) """ struct ReverseConstraintDual <: MOI.AbstractConstraintAttribute end -""" - ReverseParameter <: MOI.AbstractVariableAttribute - -A `MOI.AbstractVariableAttribute` to get output data from reverse differentiation, -that is, problem input data. - -For instance, to get the tangent of the variable of index `vi` corresponding to -the tangents given to `ReverseVariablePrimal`, do the following: -```julia -MOI.get(model, DiffOpt.ReverseParameter(), vi) -``` -""" -struct ReverseParameter <: MOI.AbstractVariableAttribute end - -MOI.is_set_by_optimize(::ReverseParameter) = true - """ ForwardConstraintDual <: MOI.AbstractConstraintAttribute @@ -391,16 +365,6 @@ function MOI.set( return end -function MOI.set( - model::AbstractModel, - ::ForwardParameter, - pi::MOI.VariableIndex, - val, -) - model.input_cache.dp[pi] = val - return -end - function MOI.set( model::AbstractModel, ::ForwardConstraintFunction, diff --git a/src/jump_moi_overloads.jl b/src/jump_moi_overloads.jl index 4b100e2d..5119d6dd 100644 --- a/src/jump_moi_overloads.jl +++ b/src/jump_moi_overloads.jl @@ -48,15 +48,6 @@ function MOI.set( return MOI.set(model, attr, con_ref, JuMP.AffExpr(func)) end -function MOI.get( - model::JuMP.Model, - attr::ReverseParameter, - var_ref::JuMP.VariableRef, -) - JuMP.check_belongs_to_model(var_ref, model) - return _moi_get_result(JuMP.backend(model), attr, JuMP.index(var_ref)) -end - function MOI.get( model::JuMP.Model, attr::ForwardConstraintDual, diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index 98fc6b00..6092cc6e 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -574,7 +574,7 @@ function forward_differentiate!(model::Optimizer) end if model.input_cache.dp !== nothing for (vi, value) in model.input_cache.dp - MOI.set(diff, ForwardParameter(), model.index_map[vi], value) + diff.model.input_cache.dp[model.index_map[vi]] = value end end return forward_differentiate!(diff) @@ -693,13 +693,13 @@ end function MOI.get( model::Optimizer, - attr::ReverseParameter, - vi::MOI.VariableIndex, + attr::ReverseConstraintSet, + ci::MOI.ConstraintIndex, ) return MOI.get( _checked_diff(model, attr, :reverse_differentiate!), attr, - model.index_map[vi], + model.index_map[ci], ) end @@ -725,26 +725,12 @@ end function MOI.supports( ::Optimizer, - ::ForwardParameter, - ::Type{MOI.VariableIndex}, -) + ::ForwardConstraintSet, + ::Type{MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}}, +) where {T} return true end -function MOI.set( - model::Optimizer, - ::ForwardParameter, - vi::MOI.VariableIndex, - val, -) - model.input_cache.dp[vi] = val - return -end - -function MOI.get(model::Optimizer, ::ForwardParameter, vi::MOI.VariableIndex) - return get(model.input_cache.dp, vi, 0.0) -end - function MOI.get( model::Optimizer, ::ReverseVariablePrimal, diff --git a/src/parameters.jl b/src/parameters.jl index 65798949..68ff54a4 100644 --- a/src/parameters.jl +++ b/src/parameters.jl @@ -60,10 +60,6 @@ end # functions to be used with ParametricOptInterface.jl -struct ForwardConstraintSet <: MOI.AbstractConstraintAttribute end - -struct ReverseConstraintSet <: MOI.AbstractConstraintAttribute end - mutable struct SensitivityData{T} parameter_input_forward::Dict{MOI.VariableIndex,T} parameter_output_backward::Dict{MOI.VariableIndex,T} @@ -319,6 +315,16 @@ function MOI.set( return end +function MOI.set( + model::Optimizer, + ::ForwardConstraintSet, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}, + set::MOI.Parameter, +) where {T} + model.input_cache.dp[ci] = set.value + return +end + function MOI.get( model::POI.Optimizer, attr::ForwardVariablePrimal, diff --git a/test/nlp_program.jl b/test/nlp_program.jl index 7005c949..fc5a2cfc 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -217,7 +217,8 @@ function test_compute_derivatives_Analytical(; optimize!(model) @assert is_solved_and_feasible(model) # Set pertubations - MOI.set.(model, DiffOpt.ForwardParameter(), params, Δp) + # MOI.set.(model, DiffOpt.ForwardParameter(), params, Δp) # TODO: DELETE LINE + MOI.set.(model, DiffOpt.ForwardConstraintSet(), ParameterRef.(params), Parameter.(Δp)) # Compute derivatives DiffOpt.forward_differentiate!(model) # Test sensitivities primal_vars @@ -397,7 +398,8 @@ function test_compute_derivatives_Finite_Diff(; optimize!(model) @assert is_solved_and_feasible(model) # Set pertubations - MOI.set.(model, DiffOpt.ForwardParameter(), params, Δp) + # MOI.set.(model, DiffOpt.ForwardParameter(), params, Δp) # TODO: DELETE LINE + MOI.set.(model, DiffOpt.ForwardConstraintSet(), ParameterRef.(params), Parameter.(Δp)) # Compute derivatives DiffOpt.forward_differentiate!(model) Δx = [ @@ -468,11 +470,11 @@ function test_differentiating_non_trivial_convex_qp_jump() db = grads_actual[6] for (i, ci) in enumerate(c_le) - @test -dh[i] ≈ -MOI.get(model, DiffOpt.ReverseParameter(), p_le[i]) atol = + @test -dh[i] ≈ -MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(p_le[i])).value atol = 1e-2 rtol = 1e-2 end for (i, ci) in enumerate(c_eq) - @test -db[i] ≈ -MOI.get(model, DiffOpt.ReverseParameter(), p_eq[i]) atol = + @test -db[i] ≈ -MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(p_eq[i])).value atol = 1e-2 rtol = 1e-2 end From 65d4224ba850de0fa421e8a2081b6f9c32bffe33 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 5 Feb 2025 13:23:56 -0500 Subject: [PATCH 30/41] expose kwargs --- src/NonLinearProgram/NonLinearProgram.jl | 8 ++++---- src/NonLinearProgram/nlp_utilities.jl | 25 +++++++++++++++++------- src/diff_opt.jl | 4 ++-- src/jump_moi_overloads.jl | 24 +++++++++++------------ src/moi_wrapper.jl | 8 ++++---- test/nlp_program.jl | 2 -- 6 files changed, 40 insertions(+), 31 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 3469bb4d..c561bdd4 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -434,14 +434,14 @@ function _cache_evaluator!(model::Model) return model.cache end -function DiffOpt.forward_differentiate!(model::Model) +function DiffOpt.forward_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max_corrections = 50, allow_intertia_correction = true) model.diff_time = @elapsed begin cache = _cache_evaluator!(model) form = model.model Δp = [model.input_cache.dp[form.var2ci[i]] for i in cache.params] # Compute Jacobian - Δs = compute_sensitivity(model) + Δs = compute_sensitivity(model; tol = tol, st = st, max_corrections = max_corrections, allow_intertia_correction = allow_intertia_correction) # Extract primal and dual sensitivities primal_Δs = Δs[1:length(model.cache.primal_vars), :] * Δp # Exclude slacks @@ -455,13 +455,13 @@ function DiffOpt.forward_differentiate!(model::Model) return nothing end -function DiffOpt.reverse_differentiate!(model::Model) +function DiffOpt.reverse_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max_corrections = 50, allow_intertia_correction = true) model.diff_time = @elapsed begin cache = _cache_evaluator!(model) form = model.model # Compute Jacobian - Δs = compute_sensitivity(model) + Δs = compute_sensitivity(model; tol = tol, st = st, max_corrections = max_corrections, allow_intertia_correction = allow_intertia_correction) num_primal = length(cache.primal_vars) Δx = zeros(num_primal) for (i, var_idx) in enumerate(cache.primal_vars) diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 0e1c8d37..89cb53ba 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -404,6 +404,7 @@ function inertia_corrector_factorization( num_cons; st = 1e-6, max_corrections = 50, + allow_intertia_correction = true, ) # Factorization K = lu(M; check = false) @@ -413,8 +414,11 @@ function inertia_corrector_factorization( diag_mat = ones(size(M, 1)) diag_mat[num_w+1:num_w+num_cons] .= -1 diag_mat = SparseArrays.spdiagm(diag_mat) + if status == 1 + @assert allow_intertia_correction "Inertia correction needed but not allowed" + @info "Inertia correction needed" + end while status == 1 && num_c < max_corrections - println("Inertia correction") M = M + st * diag_mat K = lu(M; check = false) status = K.status @@ -432,10 +436,11 @@ end Inertia correction for the factorization of the KKT matrix. Dense version. """ -function inertia_corrector_factorization(M; st = 1e-6, max_corrections = 50) +function inertia_corrector_factorization(M; st = 1e-6, max_corrections = 50, allow_intertia_correction = true) num_c = 0 if cond(M) > 1 / st - @warn "Inertia correction" + @assert allow_intertia_correction "Inertia correction needed but not allowed" + @info "Inertia correction needed" M = M + st * I(size(M, 1)) num_c += 1 end @@ -470,7 +475,8 @@ function compute_derivatives_no_relax( geq_locations::Vector{Z}, ineq_locations::Vector{Z}, has_up::Vector{Z}, - has_low::Vector{Z}, + has_low::Vector{Z}; + st = 1e-6, max_corrections = 50, allow_intertia_correction = true ) where {Z<:Integer} M, N = build_sensitivity_matrices( model, @@ -491,7 +497,10 @@ function compute_derivatives_no_relax( num_vars = get_num_primal_vars(model) num_cons = get_num_constraints(model) num_ineq = length(ineq_locations) - K = inertia_corrector_factorization(M, num_vars + num_ineq, num_cons) # Factorization + K = inertia_corrector_factorization(M, num_vars + num_ineq, num_cons; + st = st, max_corrections = max_corrections, + allow_intertia_correction = allow_intertia_correction + ) # Factorization if isnothing(K) return zeros(size(M, 1), size(N, 2)), K, N end @@ -510,7 +519,7 @@ sense_mult(model::Model) = objective_sense(model) == MOI.MIN_SENSE ? 1.0 : -1.0 Compute the sensitivity of the solution given sensitivity of the parameters (Δp). """ -function compute_sensitivity(model::Model; tol = 1e-6) +function compute_sensitivity(model::Model; tol = 1e-6, st = 1e-6, max_corrections = 50, allow_intertia_correction = true) # Solution and bounds X, V_L, @@ -537,7 +546,9 @@ function compute_sensitivity(model::Model; tol = 1e-6) geq_locations, ineq_locations, has_up, - has_low, + has_low; + st = st, max_corrections = max_corrections, + allow_intertia_correction = allow_intertia_correction ) ## Adjust signs based on JuMP convention num_vars = get_num_primal_vars(model) diff --git a/src/diff_opt.jl b/src/diff_opt.jl index 6fc9daef..1e9ecdee 100644 --- a/src/diff_opt.jl +++ b/src/diff_opt.jl @@ -40,7 +40,7 @@ function Base.empty!(cache::InputCache) end """ - reverse_differentiate!(model::MOI.ModelLike) + reverse_differentiate!(model::MOI.ModelLike; kwargs...) Wrapper method for the backward pass / reverse differentiation. This method will consider as input a currently solved problem and differentials @@ -51,7 +51,7 @@ attributes [`ReverseObjectiveFunction`](@ref) and [`ReverseConstraintFunction`]( function reverse_differentiate! end """ - forward_differentiate!(model::Optimizer) + forward_differentiate!(model::Optimizer; kwargs...) Wrapper method for the forward pass. This method will consider as input a currently solved problem and diff --git a/src/jump_moi_overloads.jl b/src/jump_moi_overloads.jl index 5119d6dd..150a9302 100644 --- a/src/jump_moi_overloads.jl +++ b/src/jump_moi_overloads.jl @@ -347,12 +347,12 @@ end # JuMP -function reverse_differentiate!(model::JuMP.Model) - return reverse_differentiate!(JuMP.backend(model)) +function reverse_differentiate!(model::JuMP.Model; kwargs...) + return reverse_differentiate!(JuMP.backend(model); kwargs...) end -function forward_differentiate!(model::JuMP.Model) - return forward_differentiate!(JuMP.backend(model)) +function forward_differentiate!(model::JuMP.Model; kwargs...) + return forward_differentiate!(JuMP.backend(model); kwargs...) end function empty_input_sensitivities!(model::JuMP.Model) @@ -362,12 +362,12 @@ end # MOI.Utilities -function reverse_differentiate!(model::MOI.Utilities.CachingOptimizer) - return reverse_differentiate!(model.optimizer) +function reverse_differentiate!(model::MOI.Utilities.CachingOptimizer; kwargs...) + return reverse_differentiate!(model.optimizer; kwargs...) end -function forward_differentiate!(model::MOI.Utilities.CachingOptimizer) - return forward_differentiate!(model.optimizer) +function forward_differentiate!(model::MOI.Utilities.CachingOptimizer; kwargs...) + return forward_differentiate!(model.optimizer; kwargs...) end function empty_input_sensitivities!(model::MOI.Utilities.CachingOptimizer) @@ -377,12 +377,12 @@ end # MOIB -function reverse_differentiate!(model::MOI.Bridges.AbstractBridgeOptimizer) - return reverse_differentiate!(model.model) +function reverse_differentiate!(model::MOI.Bridges.AbstractBridgeOptimizer; kwargs...) + return reverse_differentiate!(model.model; kwargs...) end -function forward_differentiate!(model::MOI.Bridges.AbstractBridgeOptimizer) - return forward_differentiate!(model.model) +function forward_differentiate!(model::MOI.Bridges.AbstractBridgeOptimizer; kwargs...) + return forward_differentiate!(model.model; kwargs...) end function empty_input_sensitivities!(model::MOI.Bridges.AbstractBridgeOptimizer) diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index 6092cc6e..d2e975b6 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -512,7 +512,7 @@ function MOI.set(model::Optimizer, ::ModelConstructor, model_constructor) return end -function reverse_differentiate!(model::Optimizer) +function reverse_differentiate!(model::Optimizer; kwargs...) st = MOI.get(model.optimizer, MOI.TerminationStatus()) if !in(st, (MOI.LOCALLY_SOLVED, MOI.OPTIMAL)) error( @@ -523,7 +523,7 @@ function reverse_differentiate!(model::Optimizer) for (vi, value) in model.input_cache.dx MOI.set(diff, ReverseVariablePrimal(), model.index_map[vi], value) end - return reverse_differentiate!(diff) + return reverse_differentiate!(diff; kwargs...) end function _copy_forward_in_constraint(diff, index_map, con_map, constraints) @@ -538,7 +538,7 @@ function _copy_forward_in_constraint(diff, index_map, con_map, constraints) return end -function forward_differentiate!(model::Optimizer) +function forward_differentiate!(model::Optimizer; kwargs...) st = MOI.get(model.optimizer, MOI.TerminationStatus()) if !in(st, (MOI.LOCALLY_SOLVED, MOI.OPTIMAL)) error( @@ -577,7 +577,7 @@ function forward_differentiate!(model::Optimizer) diff.model.input_cache.dp[model.index_map[vi]] = value end end - return forward_differentiate!(diff) + return forward_differentiate!(diff; kwargs...) end function empty_input_sensitivities!(model::Optimizer) diff --git a/test/nlp_program.jl b/test/nlp_program.jl index fc5a2cfc..4f761f09 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -217,7 +217,6 @@ function test_compute_derivatives_Analytical(; optimize!(model) @assert is_solved_and_feasible(model) # Set pertubations - # MOI.set.(model, DiffOpt.ForwardParameter(), params, Δp) # TODO: DELETE LINE MOI.set.(model, DiffOpt.ForwardConstraintSet(), ParameterRef.(params), Parameter.(Δp)) # Compute derivatives DiffOpt.forward_differentiate!(model) @@ -398,7 +397,6 @@ function test_compute_derivatives_Finite_Diff(; optimize!(model) @assert is_solved_and_feasible(model) # Set pertubations - # MOI.set.(model, DiffOpt.ForwardParameter(), params, Δp) # TODO: DELETE LINE MOI.set.(model, DiffOpt.ForwardConstraintSet(), ParameterRef.(params), Parameter.(Δp)) # Compute derivatives DiffOpt.forward_differentiate!(model) From 36b01705132e536177fb841c7076b1bed298ebd7 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 5 Feb 2025 13:40:23 -0500 Subject: [PATCH 31/41] restrict hessian type --- src/NonLinearProgram/nlp_utilities.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 89cb53ba..db9818c8 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -8,7 +8,7 @@ Filling the off-diagonal elements of a sparse matrix to make it symmetric. """ -function fill_off_diagonal(H) +function fill_off_diagonal(H::SparseMatrixCSC) ret = H + H' row_vals = SparseArrays.rowvals(ret) non_zeros = SparseArrays.nonzeros(ret) @@ -49,7 +49,7 @@ function compute_optimal_hessian( ) num_vars = length(model.x) H = SparseArrays.sparse(I, J, V, num_vars, num_vars) - return fill_off_diagonal(H) + return H end """ From 859ddeaea2f01d542ebb23e9646111ab69baeea1 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 5 Feb 2025 13:51:39 -0500 Subject: [PATCH 32/41] reverse wrong change --- src/NonLinearProgram/nlp_utilities.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index db9818c8..33e9e810 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -49,7 +49,7 @@ function compute_optimal_hessian( ) num_vars = length(model.x) H = SparseArrays.sparse(I, J, V, num_vars, num_vars) - return H + return fill_off_diagonal(H) end """ From 475a02f3101aab5ce1a145938b45a62cbc8c6a73 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 5 Feb 2025 13:56:39 -0500 Subject: [PATCH 33/41] update usage --- docs/src/usage.md | 85 +++++++++++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/docs/src/usage.md b/docs/src/usage.md index 7a453983..1ecfa085 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -57,37 +57,64 @@ DiffOpt.forward_differentiate!(model) grad_x = MOI.get.(model, DiffOpt.ForwardVariablePrimal(), x) ``` -3. To differentiate a general nonlinear program, we can use the `forward_differentiate!` method with perturbations in the objective function and constraints through perturbations in the problem parameters. For example, consider the following nonlinear program: -```julia -model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) -@variable(model, p ∈ MOI.Parameter(0.1)) -@variable(model, x >= p) -@variable(model, y >= 0) -@objective(model, Min, x^2 + y^2) -@constraint(model, con, x + y >= 1) - -# Solve -JuMP.optimize!(model) - -# Set parameter pertubations -MOI.set(model, DiffOpt.ForwardParameter(), params[1], 0.2) - -# forward differentiate -DiffOpt.forward_differentiate!(model) +3. To differentiate a general nonlinear program, have to use the API for Parameterized JuMP models. For example, consider the following nonlinear program: -# Retrieve sensitivities -dx = MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) -dy = MOI.get(model, DiffOpt.ForwardVariablePrimal(), y) -``` - -or we can use the `reverse_differentiate!` method: ```julia -# Set Primal Pertubations -MOI.set(model, DiffOpt.ReverseVariablePrimal(), x, 1.0) +using JuMP, DiffOpt, HiGHS -# Reverse differentiation +model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) +set_silent(model) + +p_val = 4.0 +pc_val = 2.0 +@variable(model, x) +@variable(model, p in Parameter(p_val)) +@variable(model, pc in Parameter(pc_val)) +@constraint(model, cons, pc * x >= 3 * p) +@objective(model, Min, 2x) +optimize!(model) +@show value(x) == 3 * p_val / pc_val + +# the function is +# x(p, pc) = 3p / pc +# hence, +# dx/dp = 3 / pc +# dx/dpc = -3p / pc^2 + +# First, try forward mode AD + +# differentiate w.r.t. p +direction_p = 3.0 +MOI.set(model, DiffOpt.ForwardConstraintSet(), ParameterRef(p), Parameter(direction_p)) +DiffOpt.forward_differentiate!(model) +@show MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) == direction_p * 3 / pc_val + +# update p and pc +p_val = 2.0 +pc_val = 6.0 +set_parameter_value(p, p_val) +set_parameter_value(pc, pc_val) +# re-optimize +optimize!(model) +# check solution +@show value(x) ≈ 3 * p_val / pc_val + +# stop differentiating with respect to p +DiffOpt.empty_input_sensitivities!(model) +# differentiate w.r.t. pc +direction_pc = 10.0 +MOI.set(model, DiffOpt.ForwardConstraintSet(), ParameterRef(pc), Parameter(direction_pc)) +DiffOpt.forward_differentiate!(model) +@show abs(MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) - + -direction_pc * 3 * p_val / pc_val^2) < 1e-5 + +# always a good practice to clear previously set sensitivities +DiffOpt.empty_input_sensitivities!(model) +# Now, reverse model AD +direction_x = 10.0 +MOI.set(model, DiffOpt.ReverseVariablePrimal(), x, direction_x) DiffOpt.reverse_differentiate!(model) - -# Retrieve reverse sensitivities (example usage) -dp= MOI.get(model, DiffOpt.ReverseParameter(), p) +@show MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(p)) == MOI.Parameter(direction_x * 3 / pc_val) +@show abs(MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(pc)).value - + -direction_x * 3 * p_val / pc_val^2) < 1e-5 ``` \ No newline at end of file From dffdf8da98c683271b1c8c03832fda95426b55b3 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 5 Feb 2025 18:09:35 -0500 Subject: [PATCH 34/41] fix mad merge --- src/jump_moi_overloads.jl | 15 --------------- src/moi_wrapper.jl | 5 ----- 2 files changed, 20 deletions(-) diff --git a/src/jump_moi_overloads.jl b/src/jump_moi_overloads.jl index 62c7b70d..150a9302 100644 --- a/src/jump_moi_overloads.jl +++ b/src/jump_moi_overloads.jl @@ -360,11 +360,6 @@ function empty_input_sensitivities!(model::JuMP.Model) return end -function empty_input_sensitivities!(model::JuMP.Model) - empty_input_sensitivities!(JuMP.backend(model)) - return -end - # MOI.Utilities function reverse_differentiate!(model::MOI.Utilities.CachingOptimizer; kwargs...) @@ -380,11 +375,6 @@ function empty_input_sensitivities!(model::MOI.Utilities.CachingOptimizer) return end -function empty_input_sensitivities!(model::MOI.Utilities.CachingOptimizer) - empty_input_sensitivities!(model.optimizer) - return -end - # MOIB function reverse_differentiate!(model::MOI.Bridges.AbstractBridgeOptimizer; kwargs...) @@ -399,8 +389,3 @@ function empty_input_sensitivities!(model::MOI.Bridges.AbstractBridgeOptimizer) empty_input_sensitivities!(model.model) return end - -function empty_input_sensitivities!(model::MOI.Bridges.AbstractBridgeOptimizer) - empty_input_sensitivities!(model.model) - return -end diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index c3074d19..d2e975b6 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -585,11 +585,6 @@ function empty_input_sensitivities!(model::Optimizer) return end -function empty_input_sensitivities!(model::Optimizer) - empty!(model.input_cache) - return -end - function _instantiate_with_bridges(model_constructor) model = MOI.Bridges.LazyBridgeOptimizer(MOI.instantiate(model_constructor)) # We don't add any variable bridge here because: From bf4ab5d845a63b6464cb4b521d40f098f9e98dd0 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 5 Feb 2025 18:11:42 -0500 Subject: [PATCH 35/41] fix typo --- src/NonLinearProgram/NonLinearProgram.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index c561bdd4..fbda69b8 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -373,7 +373,7 @@ get_num_params(model::Model) = get_num_params(model.model) function _cache_evaluator!(model::Model) form = model.model # Retrieve and sort primal variables by index - params = params = sort(all_params(form); by = x -> x.value) + params = sort(all_params(form); by = x -> x.value) primal_vars = sort(all_primal_vars(form); by = x -> x.value) num_primal = length(primal_vars) From b7ef541d8e01ed36556c93f3258df2a5a30fb5d6 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 5 Feb 2025 21:45:50 -0500 Subject: [PATCH 36/41] fix typo --- src/NonLinearProgram/NonLinearProgram.jl | 8 ++++---- src/NonLinearProgram/nlp_utilities.jl | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index fbda69b8..9500db24 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -434,14 +434,14 @@ function _cache_evaluator!(model::Model) return model.cache end -function DiffOpt.forward_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max_corrections = 50, allow_intertia_correction = true) +function DiffOpt.forward_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max_corrections = 50, allow_inertia_correction = true) model.diff_time = @elapsed begin cache = _cache_evaluator!(model) form = model.model Δp = [model.input_cache.dp[form.var2ci[i]] for i in cache.params] # Compute Jacobian - Δs = compute_sensitivity(model; tol = tol, st = st, max_corrections = max_corrections, allow_intertia_correction = allow_intertia_correction) + Δs = compute_sensitivity(model; tol = tol, st = st, max_corrections = max_corrections, allow_inertia_correction = allow_inertia_correction) # Extract primal and dual sensitivities primal_Δs = Δs[1:length(model.cache.primal_vars), :] * Δp # Exclude slacks @@ -455,13 +455,13 @@ function DiffOpt.forward_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max return nothing end -function DiffOpt.reverse_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max_corrections = 50, allow_intertia_correction = true) +function DiffOpt.reverse_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max_corrections = 50, allow_inertia_correction = true) model.diff_time = @elapsed begin cache = _cache_evaluator!(model) form = model.model # Compute Jacobian - Δs = compute_sensitivity(model; tol = tol, st = st, max_corrections = max_corrections, allow_intertia_correction = allow_intertia_correction) + Δs = compute_sensitivity(model; tol = tol, st = st, max_corrections = max_corrections, allow_inertia_correction = allow_inertia_correction) num_primal = length(cache.primal_vars) Δx = zeros(num_primal) for (i, var_idx) in enumerate(cache.primal_vars) diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 33e9e810..7ced377e 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -404,7 +404,7 @@ function inertia_corrector_factorization( num_cons; st = 1e-6, max_corrections = 50, - allow_intertia_correction = true, + allow_inertia_correction = true, ) # Factorization K = lu(M; check = false) @@ -415,7 +415,7 @@ function inertia_corrector_factorization( diag_mat[num_w+1:num_w+num_cons] .= -1 diag_mat = SparseArrays.spdiagm(diag_mat) if status == 1 - @assert allow_intertia_correction "Inertia correction needed but not allowed" + @assert allow_inertia_correction "Inertia correction needed but not allowed" @info "Inertia correction needed" end while status == 1 && num_c < max_corrections @@ -436,10 +436,10 @@ end Inertia correction for the factorization of the KKT matrix. Dense version. """ -function inertia_corrector_factorization(M; st = 1e-6, max_corrections = 50, allow_intertia_correction = true) +function inertia_corrector_factorization(M; st = 1e-6, max_corrections = 50, allow_inertia_correction = true) num_c = 0 if cond(M) > 1 / st - @assert allow_intertia_correction "Inertia correction needed but not allowed" + @assert allow_inertia_correction "Inertia correction needed but not allowed" @info "Inertia correction needed" M = M + st * I(size(M, 1)) num_c += 1 @@ -476,7 +476,7 @@ function compute_derivatives_no_relax( ineq_locations::Vector{Z}, has_up::Vector{Z}, has_low::Vector{Z}; - st = 1e-6, max_corrections = 50, allow_intertia_correction = true + st = 1e-6, max_corrections = 50, allow_inertia_correction = true ) where {Z<:Integer} M, N = build_sensitivity_matrices( model, @@ -499,7 +499,7 @@ function compute_derivatives_no_relax( num_ineq = length(ineq_locations) K = inertia_corrector_factorization(M, num_vars + num_ineq, num_cons; st = st, max_corrections = max_corrections, - allow_intertia_correction = allow_intertia_correction + allow_inertia_correction = allow_inertia_correction ) # Factorization if isnothing(K) return zeros(size(M, 1), size(N, 2)), K, N @@ -519,7 +519,7 @@ sense_mult(model::Model) = objective_sense(model) == MOI.MIN_SENSE ? 1.0 : -1.0 Compute the sensitivity of the solution given sensitivity of the parameters (Δp). """ -function compute_sensitivity(model::Model; tol = 1e-6, st = 1e-6, max_corrections = 50, allow_intertia_correction = true) +function compute_sensitivity(model::Model; tol = 1e-6, st = 1e-6, max_corrections = 50, allow_inertia_correction = true) # Solution and bounds X, V_L, @@ -548,7 +548,7 @@ function compute_sensitivity(model::Model; tol = 1e-6, st = 1e-6, max_correction has_up, has_low; st = st, max_corrections = max_corrections, - allow_intertia_correction = allow_intertia_correction + allow_inertia_correction = allow_inertia_correction ) ## Adjust signs based on JuMP convention num_vars = get_num_primal_vars(model) From 19dcda4d2390ab54e8019fd5d0779965cd023dd4 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 6 Feb 2025 15:16:11 -0500 Subject: [PATCH 37/41] fix wrong index --- src/NonLinearProgram/NonLinearProgram.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 9500db24..1b922254 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -438,7 +438,8 @@ function DiffOpt.forward_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max model.diff_time = @elapsed begin cache = _cache_evaluator!(model) form = model.model - Δp = [model.input_cache.dp[form.var2ci[i]] for i in cache.params] + param2var = Dict(value => key for (key, value) in form.var2param) + Δp = [model.input_cache.dp[form.var2ci[param2var[i]]] for i in cache.params] # Compute Jacobian Δs = compute_sensitivity(model; tol = tol, st = st, max_corrections = max_corrections, allow_inertia_correction = allow_inertia_correction) From df90d9769aee833af7f05f3d49b39abc31b37130 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 6 Feb 2025 15:21:00 -0500 Subject: [PATCH 38/41] reverse index --- src/NonLinearProgram/NonLinearProgram.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 1b922254..9500db24 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -438,8 +438,7 @@ function DiffOpt.forward_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max model.diff_time = @elapsed begin cache = _cache_evaluator!(model) form = model.model - param2var = Dict(value => key for (key, value) in form.var2param) - Δp = [model.input_cache.dp[form.var2ci[param2var[i]]] for i in cache.params] + Δp = [model.input_cache.dp[form.var2ci[i]] for i in cache.params] # Compute Jacobian Δs = compute_sensitivity(model; tol = tol, st = st, max_corrections = max_corrections, allow_inertia_correction = allow_inertia_correction) From 622732eb716a42ed90ca35182551c9c9b47a96fa Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 6 Feb 2025 19:25:45 -0500 Subject: [PATCH 39/41] allow user to just set relevat sensitivities --- src/NonLinearProgram/NonLinearProgram.jl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 9500db24..7d63fe2e 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -438,7 +438,14 @@ function DiffOpt.forward_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max model.diff_time = @elapsed begin cache = _cache_evaluator!(model) form = model.model - Δp = [model.input_cache.dp[form.var2ci[i]] for i in cache.params] + # Δp = [model.input_cache.dp[form.var2ci[i]] for i in cache.params] + Δp = zeros(length(cache.params)) + for (i, var_idx) in enumerate(cache.params) + ky = form.var2ci[var_idx] + if haskey(model.input_cache.dp, ky) + Δp[i] = model.input_cache.dp[ky] + end + end # Compute Jacobian Δs = compute_sensitivity(model; tol = tol, st = st, max_corrections = max_corrections, allow_inertia_correction = allow_inertia_correction) From bce230345e544e7838908abd67c8a57ee454b07c Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 7 Feb 2025 11:38:30 -0500 Subject: [PATCH 40/41] fix copy reverse sensitivity dual --- src/moi_wrapper.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/moi_wrapper.jl b/src/moi_wrapper.jl index d2e975b6..a9661c35 100644 --- a/src/moi_wrapper.jl +++ b/src/moi_wrapper.jl @@ -523,6 +523,9 @@ function reverse_differentiate!(model::Optimizer; kwargs...) for (vi, value) in model.input_cache.dx MOI.set(diff, ReverseVariablePrimal(), model.index_map[vi], value) end + for (vi, value) in model.input_cache.dy + MOI.set(diff, ReverseConstraintDual(), model.index_map[vi], value) + end return reverse_differentiate!(diff; kwargs...) end From a3fe85a84fa0d254fedb6130eb70d62d84b738c1 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 7 Feb 2025 15:17:24 -0500 Subject: [PATCH 41/41] format --- src/NonLinearProgram/NonLinearProgram.jl | 37 ++++++++++++++++++++---- src/NonLinearProgram/nlp_utilities.jl | 34 +++++++++++++++++----- src/jump_moi_overloads.jl | 20 ++++++++++--- test/nlp_program.jl | 30 +++++++++++++++---- 4 files changed, 97 insertions(+), 24 deletions(-) diff --git a/src/NonLinearProgram/NonLinearProgram.jl b/src/NonLinearProgram/NonLinearProgram.jl index 7d63fe2e..b090056d 100644 --- a/src/NonLinearProgram/NonLinearProgram.jl +++ b/src/NonLinearProgram/NonLinearProgram.jl @@ -434,7 +434,13 @@ function _cache_evaluator!(model::Model) return model.cache end -function DiffOpt.forward_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max_corrections = 50, allow_inertia_correction = true) +function DiffOpt.forward_differentiate!( + model::Model; + tol = 1e-6, + st = 1e-6, + max_corrections = 50, + allow_inertia_correction = true, +) model.diff_time = @elapsed begin cache = _cache_evaluator!(model) form = model.model @@ -448,7 +454,13 @@ function DiffOpt.forward_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max end # Compute Jacobian - Δs = compute_sensitivity(model; tol = tol, st = st, max_corrections = max_corrections, allow_inertia_correction = allow_inertia_correction) + Δs = compute_sensitivity( + model; + tol = tol, + st = st, + max_corrections = max_corrections, + allow_inertia_correction = allow_inertia_correction, + ) # Extract primal and dual sensitivities primal_Δs = Δs[1:length(model.cache.primal_vars), :] * Δp # Exclude slacks @@ -462,13 +474,25 @@ function DiffOpt.forward_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max return nothing end -function DiffOpt.reverse_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max_corrections = 50, allow_inertia_correction = true) +function DiffOpt.reverse_differentiate!( + model::Model; + tol = 1e-6, + st = 1e-6, + max_corrections = 50, + allow_inertia_correction = true, +) model.diff_time = @elapsed begin cache = _cache_evaluator!(model) form = model.model # Compute Jacobian - Δs = compute_sensitivity(model; tol = tol, st = st, max_corrections = max_corrections, allow_inertia_correction = allow_inertia_correction) + Δs = compute_sensitivity( + model; + tol = tol, + st = st, + max_corrections = max_corrections, + allow_inertia_correction = allow_inertia_correction, + ) num_primal = length(cache.primal_vars) Δx = zeros(num_primal) for (i, var_idx) in enumerate(cache.primal_vars) @@ -506,7 +530,8 @@ function DiffOpt.reverse_differentiate!(model::Model; tol = 1e-6, st = 1e-6, max Δp = Δs' * Δw # Order by ConstraintIndex - varorder = sort(collect(keys(form.var2ci)); by = x -> form.var2ci[x].value) + varorder = + sort(collect(keys(form.var2ci)); by = x -> form.var2ci[x].value) Δp = [Δp[form.var2param[var_idx].value] for var_idx in varorder] model.back_grad_cache = ReverseCache(; Δp = Δp) @@ -546,7 +571,7 @@ function MOI.get( ::DiffOpt.ReverseConstraintSet, ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}, ) where {T} - return MOI.Parameter{T}(model.back_grad_cache.Δp[ci.value],) + return MOI.Parameter{T}(model.back_grad_cache.Δp[ci.value]) end end # module NonLinearProgram diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 7ced377e..55fd0f8f 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -436,7 +436,12 @@ end Inertia correction for the factorization of the KKT matrix. Dense version. """ -function inertia_corrector_factorization(M; st = 1e-6, max_corrections = 50, allow_inertia_correction = true) +function inertia_corrector_factorization( + M; + st = 1e-6, + max_corrections = 50, + allow_inertia_correction = true, +) num_c = 0 if cond(M) > 1 / st @assert allow_inertia_correction "Inertia correction needed but not allowed" @@ -476,7 +481,9 @@ function compute_derivatives_no_relax( ineq_locations::Vector{Z}, has_up::Vector{Z}, has_low::Vector{Z}; - st = 1e-6, max_corrections = 50, allow_inertia_correction = true + st = 1e-6, + max_corrections = 50, + allow_inertia_correction = true, ) where {Z<:Integer} M, N = build_sensitivity_matrices( model, @@ -497,9 +504,13 @@ function compute_derivatives_no_relax( num_vars = get_num_primal_vars(model) num_cons = get_num_constraints(model) num_ineq = length(ineq_locations) - K = inertia_corrector_factorization(M, num_vars + num_ineq, num_cons; - st = st, max_corrections = max_corrections, - allow_inertia_correction = allow_inertia_correction + K = inertia_corrector_factorization( + M, + num_vars + num_ineq, + num_cons; + st = st, + max_corrections = max_corrections, + allow_inertia_correction = allow_inertia_correction, ) # Factorization if isnothing(K) return zeros(size(M, 1), size(N, 2)), K, N @@ -519,7 +530,13 @@ sense_mult(model::Model) = objective_sense(model) == MOI.MIN_SENSE ? 1.0 : -1.0 Compute the sensitivity of the solution given sensitivity of the parameters (Δp). """ -function compute_sensitivity(model::Model; tol = 1e-6, st = 1e-6, max_corrections = 50, allow_inertia_correction = true) +function compute_sensitivity( + model::Model; + tol = 1e-6, + st = 1e-6, + max_corrections = 50, + allow_inertia_correction = true, +) # Solution and bounds X, V_L, @@ -547,8 +564,9 @@ function compute_sensitivity(model::Model; tol = 1e-6, st = 1e-6, max_correction ineq_locations, has_up, has_low; - st = st, max_corrections = max_corrections, - allow_inertia_correction = allow_inertia_correction + st = st, + max_corrections = max_corrections, + allow_inertia_correction = allow_inertia_correction, ) ## Adjust signs based on JuMP convention num_vars = get_num_primal_vars(model) diff --git a/src/jump_moi_overloads.jl b/src/jump_moi_overloads.jl index 150a9302..e003a032 100644 --- a/src/jump_moi_overloads.jl +++ b/src/jump_moi_overloads.jl @@ -362,11 +362,17 @@ end # MOI.Utilities -function reverse_differentiate!(model::MOI.Utilities.CachingOptimizer; kwargs...) +function reverse_differentiate!( + model::MOI.Utilities.CachingOptimizer; + kwargs..., +) return reverse_differentiate!(model.optimizer; kwargs...) end -function forward_differentiate!(model::MOI.Utilities.CachingOptimizer; kwargs...) +function forward_differentiate!( + model::MOI.Utilities.CachingOptimizer; + kwargs..., +) return forward_differentiate!(model.optimizer; kwargs...) end @@ -377,11 +383,17 @@ end # MOIB -function reverse_differentiate!(model::MOI.Bridges.AbstractBridgeOptimizer; kwargs...) +function reverse_differentiate!( + model::MOI.Bridges.AbstractBridgeOptimizer; + kwargs..., +) return reverse_differentiate!(model.model; kwargs...) end -function forward_differentiate!(model::MOI.Bridges.AbstractBridgeOptimizer; kwargs...) +function forward_differentiate!( + model::MOI.Bridges.AbstractBridgeOptimizer; + kwargs..., +) return forward_differentiate!(model.model; kwargs...) end diff --git a/test/nlp_program.jl b/test/nlp_program.jl index 4f761f09..fd71e0f0 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -217,7 +217,12 @@ function test_compute_derivatives_Analytical(; optimize!(model) @assert is_solved_and_feasible(model) # Set pertubations - MOI.set.(model, DiffOpt.ForwardConstraintSet(), ParameterRef.(params), Parameter.(Δp)) + MOI.set.( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef.(params), + Parameter.(Δp), + ) # Compute derivatives DiffOpt.forward_differentiate!(model) # Test sensitivities primal_vars @@ -397,7 +402,12 @@ function test_compute_derivatives_Finite_Diff(; optimize!(model) @assert is_solved_and_feasible(model) # Set pertubations - MOI.set.(model, DiffOpt.ForwardConstraintSet(), ParameterRef.(params), Parameter.(Δp)) + MOI.set.( + model, + DiffOpt.ForwardConstraintSet(), + ParameterRef.(params), + Parameter.(Δp), + ) # Compute derivatives DiffOpt.forward_differentiate!(model) Δx = [ @@ -468,12 +478,20 @@ function test_differentiating_non_trivial_convex_qp_jump() db = grads_actual[6] for (i, ci) in enumerate(c_le) - @test -dh[i] ≈ -MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(p_le[i])).value atol = - 1e-2 rtol = 1e-2 + @test -dh[i] ≈ + -MOI.get( + model, + DiffOpt.ReverseConstraintSet(), + ParameterRef(p_le[i]), + ).value atol = 1e-2 rtol = 1e-2 end for (i, ci) in enumerate(c_eq) - @test -db[i] ≈ -MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(p_eq[i])).value atol = - 1e-2 rtol = 1e-2 + @test -db[i] ≈ + -MOI.get( + model, + DiffOpt.ReverseConstraintSet(), + ParameterRef(p_eq[i]), + ).value atol = 1e-2 rtol = 1e-2 end return