From b32290101fd0962b7f0717734efef353b726dfae Mon Sep 17 00:00:00 2001 From: pulsipher Date: Fri, 28 Apr 2023 17:17:20 -0400 Subject: [PATCH 01/40] Update to use NonlinearExpr in src --- Project.toml | 9 +- src/InfiniteOpt.jl | 6 - src/TranscriptionOpt/model.jl | 6 +- src/TranscriptionOpt/transcribe.jl | 67 +- src/datatypes.jl | 6 +- src/expressions.jl | 185 ++-- src/measure_expansions.jl | 10 +- src/nlp.jl | 1524 ++-------------------------- src/results.jl | 8 +- test/runtests.jl | 5 +- 10 files changed, 200 insertions(+), 1626 deletions(-) diff --git a/Project.toml b/Project.toml index 4c61a995e..aea1db3c2 100644 --- a/Project.toml +++ b/Project.toml @@ -4,33 +4,28 @@ authors = ["Joshua Pulsipher and Weiqi Zhang"] version = "0.5.6" [deps] -AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" FastGaussQuadrature = "442a2c76-b920-505d-bb47-c5924d526838" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" -LeftChildRightSiblingTrees = "1d6d02ad-be62-4b6b-8a6d-2f90e265016e" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MutableArithmetics = "d8a4904e-b15c-11e9-3269-09a3773c0cb0" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" -SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" [compat] -AbstractTrees = "0.4" DataStructures = "0.14.2 - 0.18" Distributions = "0.19 - 0.25" FastGaussQuadrature = "0.3.2 - 0.4, 0.5" JuMP = "1.2" -LeftChildRightSiblingTrees = "0.2" MutableArithmetics = "1" Reexport = "0.2, 1" -SpecialFunctions = "0.8 - 0.10, 1, 2" julia = "^1.6" [extras] Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" [targets] -test = ["Test", "Random", "Suppressor"] +test = ["Test", "Random", "Suppressor", "Pkg"] diff --git a/src/InfiniteOpt.jl b/src/InfiniteOpt.jl index ec7678559..a43a60f34 100644 --- a/src/InfiniteOpt.jl +++ b/src/InfiniteOpt.jl @@ -8,19 +8,13 @@ Reexport.@reexport using JuMP import Distributions import DataStructures import FastGaussQuadrature -import AbstractTrees -import LeftChildRightSiblingTrees import LinearAlgebra import MutableArithmetics using Base.Meta -# Import and export SpecialFunctions for the NLP interface -Reexport.@reexport using SpecialFunctions - # Make useful aliases (note we get MOI and MOIU from JuMP) const JuMPC = JuMP.Containers const MOIUC = MOIU.CleverDicts -const _LCRST = LeftChildRightSiblingTrees const _MA = MutableArithmetics export JuMPC, MOIUC # this makes these accessible to the submodules diff --git a/src/TranscriptionOpt/model.jl b/src/TranscriptionOpt/model.jl index 44d4c43e5..70de1aadc 100644 --- a/src/TranscriptionOpt/model.jl +++ b/src/TranscriptionOpt/model.jl @@ -638,7 +638,7 @@ x(support: 1) - y """ function transcription_expression( model::JuMP.Model, - expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, InfiniteOpt.NLPExpr}; + expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr}; label::Type{<:InfiniteOpt.AbstractSupportLabel} = InfiniteOpt.PublicLabel, ndarray::Bool = false ) @@ -700,7 +700,7 @@ Proper extension of [`InfiniteOpt.optimizer_model_expression`](@ref) for `TranscriptionModel`s. This simply dispatches to [`transcription_expression`](@ref). """ function InfiniteOpt.optimizer_model_expression( - expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, InfiniteOpt.NLPExpr}, + expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr}, ::Val{:TransData}; label::Type{<:InfiniteOpt.AbstractSupportLabel} = InfiniteOpt.PublicLabel, ndarray::Bool = false) @@ -719,7 +719,7 @@ be transcribed. """ function InfiniteOpt.expression_supports( model::JuMP.Model, - expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, InfiniteOpt.NLPExpr}, + expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr}, key::Val{:TransData} = Val(:TransData); label::Type{<:InfiniteOpt.AbstractSupportLabel} = InfiniteOpt.PublicLabel, ndarray::Bool = false diff --git a/src/TranscriptionOpt/transcribe.jl b/src/TranscriptionOpt/transcribe.jl index 1220af30a..ff40b064d 100644 --- a/src/TranscriptionOpt/transcribe.jl +++ b/src/TranscriptionOpt/transcribe.jl @@ -481,34 +481,15 @@ function transcription_expression( return InfiniteOpt.parameter_value(vref) end -# AffExpr and QuadExpr +# AffExpr and QuadExpr and NonlinearExpr function transcription_expression( trans_model::JuMP.Model, - expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr}, + expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr}, support::Vector{Float64} ) - # TODO fix this temporary hack (need to handle NLP expressions better) - try - return InfiniteOpt.map_expression( - v -> transcription_expression(trans_model, v, support), - expr) - catch - return transcription_expression(trans_model, - convert(InfiniteOpt.NLPExpr, expr), - support) - end -end - -# NLPExpr -function transcription_expression( - trans_model::JuMP.Model, - nlp::InfiniteOpt.NLPExpr, - support::Vector{Float64} - ) - ast = InfiniteOpt.map_nlp_to_ast( + return InfiniteOpt.map_expression( v -> transcription_expression(trans_model, v, support), - nlp) - return JuMP.add_nonlinear_expression(trans_model, ast) + expr) end # Real Number @@ -697,46 +678,6 @@ function _process_constraint( return JuMP.add_constraint(trans_model, trans_constr, name) end -# MOI.LessThan expr -function _make_constr_ast(ref, set::MOI.LessThan) - return :($ref <= $(set.upper)) -end - -# MOI.LessGreat expr -function _make_constr_ast(ref, set::MOI.GreaterThan) - return :($ref >= $(set.lower)) -end - -# MOI.EqualTo expr -function _make_constr_ast(ref, set::MOI.EqualTo) - return :($ref == $(set.value)) -end - -# MOI.Interval expr -function _make_constr_ast(ref, set::MOI.Interval) - return :($(set.lower) <= $ref <= $(set.upper)) -end - -# MOI.Set fallback -function _make_constr_ast(ref, set) - error("TranscriptionOpt does not support constraint sets of type " * - "`$(typeof(set))` for general nonlinear constraints because this " * - "is not yet supported by JuMP.") -end - -# JuMP.ScalarConstraint with NLPExpr -function _process_constraint( - trans_model::JuMP.Model, - constr::JuMP.ScalarConstraint, - func::InfiniteOpt.NLPExpr, - set::MOI.AbstractScalarSet, - raw_supp::Vector{Float64}, - name::String - ) - nlp_ref = transcription_expression(trans_model, func, raw_supp) - return JuMP.add_nonlinear_constraint(trans_model, _make_constr_ast(nlp_ref, set)) -end - # JuMP.VectorConstraint function _process_constraint( trans_model::JuMP.Model, diff --git a/src/datatypes.jl b/src/datatypes.jl index 2158a4f93..d023e8646 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -1240,7 +1240,7 @@ model an optmization problem with an infinite-dimensional decision space. - `objective_function::JuMP.AbstractJuMPScalar`: Finite scalar function. - `objective_has_measures::Bool`: Does the objective contain measures? - `registrations::Vector{RegisteredFunction}`: The nonlinear registered functions. -- `Dict{Tuple{Symbol, Int}, Function}`: Map a name and number of arguments to a registered function. +- `Dict{Symbol, Tuple{Function, Int}}`: Map a name to a registered function and its dimension. - `obj_dict::Dict{Symbol, Any}`: Store Julia symbols used with `InfiniteModel` - `optimizer_constructor`: MOI optimizer constructor (e.g., Gurobi.Optimizer). - `optimizer_model::JuMP.Model`: Model used to solve `InfiniteModel` @@ -1285,7 +1285,7 @@ mutable struct InfiniteModel <: JuMP.AbstractModel # Function Registration registrations::Vector{Any} - func_lookup::Dict{Tuple{Symbol, Int}, Function} + func_lookup::Dict{Symbol, Tuple{Function, Int}} # Objects obj_dict::Dict{Symbol, Any} @@ -1376,7 +1376,7 @@ function InfiniteModel(; false, # registration RegisteredFunction[], - Dict{Tuple{Symbol, Int}, Function}(), + Dict{Symbol, Tuple{Function, Int}}(), # Object dictionary Dict{Symbol, Any}(), # Optimize data diff --git a/src/expressions.jl b/src/expressions.jl index 4897d07c2..737747287 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -475,47 +475,6 @@ function JuMP.delete(model::InfiniteModel, fref::ParameterFunctionRef)::Nothing return end -################################################################################ -# BASIC EXTENSIONS -################################################################################ -# Convert to NLPExpr -function Base.convert(::Type{NLPExpr}, expr) - return NLPExpr(_LCRST.Node(_process_child_input(expr))) -end -function Base.convert(::Type{NLPExpr}, expr::NLPExpr) - return expr -end - -# Redefine Base.isequal for UnorderedPair{GeneralVariableRef} -# This avoids symbolic conflicts with == -function Base.isequal( - p1::P, - p2::P - ) where {P <: JuMP.UnorderedPair{GeneralVariableRef}} - return (isequal(p1.a, p2.a) && isequal(p1.b, p2.b)) || - (isequal(p1.a, p2.b) && isequal(p1.b, p2.a)) -end - -# Define Base.isequal to avoid default to == -function Base.isequal( - v::Union{GeneralVariableRef, JuMP.GenericAffExpr, JuMP.GenericQuadExpr, NLPExpr}, - w - ) - return false -end -function Base.isequal( - w, - v::Union{GeneralVariableRef, JuMP.GenericAffExpr, JuMP.GenericQuadExpr, NLPExpr} - ) - return false -end -function Base.isequal( - w::Union{GeneralVariableRef, JuMP.GenericAffExpr, JuMP.GenericQuadExpr, NLPExpr}, - v::Union{GeneralVariableRef, JuMP.GenericAffExpr, JuMP.GenericQuadExpr, NLPExpr} - ) - return false # relies on underlying extension -end - ################################################################################ # VARIABLE ITERATION ################################################################################ @@ -558,10 +517,18 @@ function _interrogate_variables( return end -# NLPExpr -function _interrogate_variables(interrogator::Function, nlp::NLPExpr) - for n in AbstractTrees.Leaves(nlp.tree_root) - _interrogate_variables(interrogator, _node_value(n.data)) +# NonlinearExpr (avoid recursion to handle deeply nested expressions) +function _interrogate_variables(interrogator::Function, nlp::JuMP.NonlinearExpr) + stack = Vector{Any}[nlp.args] + while !isempty(stack) + args = pop!(stack) + for arg in args + if arg isa JuMP.NonlinearExpr + push!(stack, arg.args) + else + _interrogate_variables(interrogator, arg) + end + end end return end @@ -597,8 +564,8 @@ function _all_function_variables(f::JuMP.GenericQuadExpr) return collect(vref_set) end -# NLPExpr or array of expressions -function _all_function_variables(f::Union{NLPExpr, AbstractArray}) +# NonlinearExpr or array of expressions +function _all_function_variables(f::Union{JuMP.NonlinearExpr, AbstractArray}) vref_set = Set{GeneralVariableRef}() _interrogate_variables(v -> push!(vref_set, v), f) return collect(vref_set) @@ -679,12 +646,18 @@ function _model_from_expr(expr::JuMP.GenericQuadExpr) end end -# NLPExpr -function _model_from_expr(expr::NLPExpr) - for node in AbstractTrees.Leaves(expr.tree_root) - result = _model_from_expr(_node_value(node.data)) - if !isnothing(result) - return result +# NonlinearExpr (avoid recursion for deeply nested expressions) +function _model_from_expr(expr::JuMP.NonlinearExpr) + stack = Vector{Any}[nlp.args] + while !isempty(stack) + args = pop!(stack) + for arg in args + if arg isa JuMP.NonlinearExpr + push!(stack, arg.args) + else + result = _model_from_expr(expr) + isnothing(result) || return result + end end end return @@ -727,29 +700,37 @@ function _remove_variable(f::JuMP.GenericQuadExpr, vref::GeneralVariableRef) return end -# Helper functions for NLP variable deletion -function _remove_variable_from_node(node, c, vref) - return +# Helper functions for nonlinear variable deletion +function _remove_variable_from_leaf(c, vref) + return c end -function _remove_variable_from_node(node, n_vref::GeneralVariableRef, vref) - if isequal(n_vref, vref) - node.data = NodeData(0.0) - end - return +function _remove_variable_from_leaf(n_vref::GeneralVariableRef, vref) + return isequal(n_vref, vref) ? 0.0 : n_vref end -function _remove_variable_from_node( - node, +function _remove_variable_from_leaf( ex::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr}, vref - )::Nothing + ) _remove_variable(ex, vref) - return + return ex end -# NLPExpr -function _remove_variable(f::NLPExpr, vref::GeneralVariableRef) - for node in AbstractTrees.Leaves(f.tree_root) - _remove_variable_from_node(node, _node_value(node.data), vref) +# Nonlinear (avoid recursion to handle deeply nested expressions) +function _remove_variable(f::JuMP.NonlinearExpr, vref::GeneralVariableRef) + stack = Tuple{Vector{Any}, Int}[] + for i in eachindex(f.args) # should be reverse, but order doesn't matter + push!(stack, (f.args, i)) + end + while !isempty(stack) + arr, idx = pop!(stack) + expr = arr[idx] + if expr isa JuMP.NonlinearExpr + for i in eachindex(expr.args) # should be reverse, but order doesn't matter + push!(stack, (expr.args, i)) + end + else + arr[idx] = _remove_variable_from_leaf(expr, vref) + end end return end @@ -776,6 +757,11 @@ function map_expression(transform::Function, v::JuMP.AbstractVariableRef) return transform(v) end +# Constant +function map_expression(transform::Function, c::Number) + return c +end + # AffExpr function map_expression(transform::Function, aff::JuMP.GenericAffExpr) return _MA.@rewrite(sum(c * transform(v) @@ -790,24 +776,25 @@ function map_expression(transform::Function, quad::JuMP.GenericQuadExpr) map_expression(transform, quad.aff)) end -# Helper methods for mapping NLP trees -function _process_node(expr::NLPExpr) - return expr.tree_root -end -function _process_node(expr) - return _LCRST.Node(NodeData(expr)) -end -function _map_expr_node(transform, data::JuMP.AbstractJuMPScalar) - return _process_node(map_expression(transform, data)) -end -function _map_expr_node(transform, data) - return _LCRST.Node(NodeData(data)) -end - -# NLPExpr -function map_expression(transform::Function, nlp::NLPExpr) - return NLPExpr(_map_tree(n -> _map_expr_node(transform, _node_value(n.data)), - nlp.tree_root)) +# NonlinearExpr (avoid recursion to handle deeply nested expressions) +function map_expression(transform::Function, nlp::JuMP.NonlinearExpr) + stack = Tuple{Vector{Any}, Vector{Any}}[] + new_nlp = JuMP.NonlinearExpr(nlp.head, Any[]) + push!(stack, (nlp.args, new_nlp.args)) + while !isempty(stack) + args, cloned = pop!(stack) + for arg in args + if arg isa JuMP.NonlinearExpr + new_expr = JuMP.NonlinearExpr(arg.head, Any[]) + push!(stack, (arg.args, new_expr.args)) + else + new_expr = map_expression(transform, arg) + end + push!(cloned, new_expr) + end + end + return new_nlp + # return JuMP.NonlinearExpr(nlp.head, Any[map_expression(transform, arg) for arg in nlp.args]) # this recursion is still more efficient... end ################################################################################ @@ -818,7 +805,7 @@ end function _set_variable_coefficient!(expr::GeneralVariableRef, var::GeneralVariableRef, coeff::Real - )::JuMP.GenericAffExpr{Float64, GeneralVariableRef} + ) # Determine if variable is that of the expression and change accordingly if isequal(expr, var) return Float64(coeff) * var @@ -828,10 +815,10 @@ function _set_variable_coefficient!(expr::GeneralVariableRef, end # GenericAffExpr -function _set_variable_coefficient!(expr::JuMP.GenericAffExpr{C, V}, - var::V, +function _set_variable_coefficient!(expr::JuMP.GenericAffExpr, + var::GeneralVariableRef, coeff::Real - )::JuMP.GenericAffExpr{C, V} where {C, V <: GeneralVariableRef} + ) # Determine if variable is in the expression and change accordingly if haskey(expr.terms, var) expr.terms[var] = coeff @@ -842,10 +829,10 @@ function _set_variable_coefficient!(expr::JuMP.GenericAffExpr{C, V}, end # GenericQuadExpr -function _set_variable_coefficient!(expr::JuMP.GenericQuadExpr{C, V}, - var::V, +function _set_variable_coefficient!(expr::JuMP.GenericQuadExpr, + var::GeneralVariableRef, coeff::Real - )::JuMP.GenericQuadExpr{C, V} where {C, V <: GeneralVariableRef} + ) # Determine if variable is in the expression and change accordingly if haskey(expr.aff.terms, var) expr.aff.terms[var] = coeff @@ -865,7 +852,7 @@ end function _affine_coefficient( func::GeneralVariableRef, var::GeneralVariableRef - )::Float64 + ) return isequal(func, var) ? 1.0 : 0.0 end @@ -873,7 +860,7 @@ end function _affine_coefficient( func::GenericAffExpr, var::GeneralVariableRef - )::Float64 + ) return get(func.terms, var, 0.0) end @@ -881,7 +868,7 @@ end function _affine_coefficient( func::GenericQuadExpr, var::GeneralVariableRef - )::Float64 + ) return get(func.aff.terms, var, 0.0) end @@ -924,8 +911,8 @@ julia> parameter_refs(my_expr) ``` """ function parameter_refs( - expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, NLPExpr} - )::Tuple + expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr} + ) model = _model_from_expr(expr) if isnothing(model) return () diff --git a/src/measure_expansions.jl b/src/measure_expansions.jl index 1b94f3737..8703cb89d 100644 --- a/src/measure_expansions.jl +++ b/src/measure_expansions.jl @@ -566,9 +566,9 @@ function expand_measure( expand_measure(expr.aff, data, write_model)) end -# NLPExpr (1D DiscreteMeasureData) +# NonlinearExpr (1D DiscreteMeasureData) function expand_measure( - expr::NLPExpr, + expr::JuMP.NonlinearExpr, data::DiscreteMeasureData{GeneralVariableRef, 1}, write_model::JuMP.AbstractModel ) @@ -589,9 +589,9 @@ function expand_measure( for i in eachindex(supps)) end -# NLPExpr (Multi DiscreteMeasureData) +# NonlinearExpr (Multi DiscreteMeasureData) function expand_measure( - expr::NLPExpr, + expr::JuMP.NonlinearExpr, data::DiscreteMeasureData{Vector{GeneralVariableRef}, 2}, write_model::JuMP.AbstractModel ) @@ -835,7 +835,7 @@ end # Expressions function expand_measures( - expr::AbstractInfOptExpr, + expr::JuMP.AbstractJuMPScalar, write_model::JuMP.AbstractModel ) return map_expression(v -> expand_measures(v, write_model), expr) diff --git a/src/nlp.jl b/src/nlp.jl index afdc748fc..483065811 100644 --- a/src/nlp.jl +++ b/src/nlp.jl @@ -1,1080 +1,13 @@ -################################################################################ -# DATATYPES -################################################################################ -# Extend addchild to take the root of another graph as input -function _LCRST.addchild(parent::_LCRST.Node{T}, newc::_LCRST.Node{T}) where T - # copy the new node if it is not a root - # otherwise, we are just merging 2 graphs together - if !AbstractTrees.isroot(newc) - newc = copy(newc) - end - # add it on to the tree - newc.parent = parent - prevc = parent.child - if prevc == parent - parent.child = newc - else - prevc = _LCRST.lastsibling(prevc) - prevc.sibling = newc - end - return newc -end - -# Extend addchild with convenient nothing dispatch for empty previous child -function _LCRST.addchild( - parent::_LCRST.Node{T}, - oldc::Nothing, - newc::_LCRST.Node{T} - ) where T - return _LCRST.addchild(parent, newc) -end - -# Extend addchild to efficiently add multiple children if the previous is known -function _LCRST.addchild( - parent::_LCRST.Node{T}, - prevc::_LCRST.Node{T}, - data::T - ) where T - # add it on to the tree - newc = _LCRST.Node(data, parent) - prevc.sibling = newc - return newc -end - -# Extend addchild to efficiently add multiple children if the previous is known -function _LCRST.addchild( - parent::_LCRST.Node{T}, - prevc::_LCRST.Node{T}, - newc::_LCRST.Node{T} - ) where T - # check if the prev is actually a child of the parent - @assert prevc.parent === parent "Previous child doesn't belong to parent." - # copy the new node if it is not a root - # otherwise, we are just merging 2 graphs together - if !AbstractTrees.isroot(newc) - newc = copy(newc) - end - # add it on to the tree - newc.parent = parent - prevc.sibling = newc - return newc -end - -# Map a LCRST tree based by operating each node with a function -function _map_tree(map_func::Function, node::_LCRST.Node) - new_node = map_func(node) - prev = nothing - for child in node - prev = _LCRST.addchild(new_node, prev, _map_tree(map_func, child)) - end - return new_node -end - -# Extend copying for graph nodes -function Base.copy(node::_LCRST.Node) - return _map_tree(n -> _LCRST.Node(n.data), node) -end - -# Replace a node with its only child if it only has 1 child -function _merge_parent_and_child(node::_LCRST.Node) - if _LCRST.islastsibling(node.child) - child = node.child - node.data = child.data - for n in child - n.parent = node - end - node.child = child.child - child.child = child - child.parent = child - end - return node -end - -# This is ambiguous but faster than the concrete alternatives tested so far -# Even better than using Node{Any}... -""" - NodeData - -A `DataType` for storing values in an expression tree that is used in a -[`NLPExpr`](@ref). Acceptable value types include: -- `Real`: Constants -- `GeneralVariableRef`: Optimization variables -- `JuMP.GenericAffExpr{Float64, GeneralVariableRef}`: Affine expressions -- `JuMP.GenericQuadExpr{Float64, GeneralVariableRef}`: Quadratic expressions -- `Symbol`: Registered NLP function name. - -**Fields** -- `value`: The stored value. -""" -struct NodeData - value -end - -# Getter function for the node value (so it is easy to change later on if needed) -function _node_value(data::NodeData) - return data.value -end - -# Recursively determine if node is effectively zero -function _is_zero(node::_LCRST.Node{NodeData}) - raw = _node_value(node.data) - if isequal(raw, 0) - return true - elseif _LCRST.isleaf(node) - return false - elseif raw in (:+, :-) && all(_is_zero(n) for n in node) - return true - elseif raw == :* && any(_is_zero(n) for n in node) - return true - elseif raw in (:/, :^) && _is_zero(node.child) - return true - elseif all(_is_zero(n) for n in node) && iszero(get(_NativeNLPFunctions, (raw, length(collect(node))), (i...) -> true)((0.0 for n in node)...)) - return true - else - return false - end -end - -# Prone any nodes that are effectively zero -function _drop_zeros!(node::_LCRST.Node{NodeData}) - if _LCRST.isleaf(node) - return node - elseif _is_zero(node) - node.data = NodeData(0.0) - _LCRST.makeleaf!(node) - return node - end - raw = _node_value(node.data) - if raw == :+ - for n in node - if _is_zero(n) - _LCRST.prunebranch!(n) - end - end - _merge_parent_and_child(node) - elseif raw == :- - if _is_zero(node.child) - _LCRST.prunebranch!(node.child) - elseif _is_zero(node.child.sibling) - _LCRST.prunebranch!(node.child.sibling) - _merge_parent_and_child(node) - end - end - for n in node - _drop_zeros!(n) - end - return node -end - -# Extend Base.isequal for our node types -function Base.isequal(n1::_LCRST.Node{NodeData}, n2::_LCRST.Node{NodeData}) - isequal(_node_value(n1.data), _node_value(n2.data)) || return false - count(i -> true, n1) != count(i -> true, n2) && return false - for (c1, c2) in zip(n1, n2) - if !isequal(c1, c2) - return false - end - end - return true -end - -""" - NLPExpr <: JuMP.AbstractJuMPScalar - -A `DataType` for storing scalar nonlinear expressions. It stores the expression -algebraically via an expression tree where each node contains [`NodeData`](@ref) -that can store one of the following: -- a registered function name (stored as a `Symbol`) -- a constant -- a variable -- an affine expression -- a quadratic expression. -Specifically, it employs a left-child right-sibling tree -(from `LeftChildRightSiblingTrees.jl`) to represent the expression tree. - -**Fields** -- `tree_root::LeftChildRightSiblingTrees.Node{NodeData}`: The root node of the - expression tree. -""" -struct NLPExpr <: JuMP.AbstractJuMPScalar - tree_root::_LCRST.Node{NodeData} - - # Constructor - function NLPExpr(tree_root::_LCRST.Node{NodeData}) - return new(tree_root) - end -end - -# Extend basic functions -Base.broadcastable(nlp::NLPExpr) = Ref(nlp) -Base.copy(nlp::NLPExpr) = NLPExpr(copy(nlp.tree_root)) -Base.zero(::Type{NLPExpr}) = NLPExpr(_LCRST.Node(NodeData(0.0))) -Base.one(::Type{NLPExpr}) = NLPExpr(_LCRST.Node(NodeData(1.0))) -function Base.isequal(nlp1::NLPExpr, nlp2::NLPExpr) - return isequal(nlp1.tree_root, nlp2.tree_root) -end - -""" - JuMP.drop_zeros!(nlp::NLPExpr)::NLPExpr - -Removes the zeros (possibly introduced by deletion) from an nonlinear expression. -Note this only uses a few simple heuristics and will not remove more complex -relationships like `cos(π/2)`. - -**Example** -```julia-repl -julia> expr = x^2.3 * max(0, zero(NLPExpr)) - exp(1/x + 0) -x^2.3 * max(0, 0) - exp(1 / x + 0) - -julia> drop_zeros!(expr) --exp(1 / x) -``` -""" -function JuMP.drop_zeros!(nlp::NLPExpr) - _drop_zeros!(nlp.tree_root) # uses a basic simplification scheme - return nlp -end - -# Extend JuMP.isequal_canonical (uses some heuristics but is not perfect) -function JuMP.isequal_canonical(nlp1::NLPExpr, nlp2::NLPExpr) - n1 = _drop_zeros!(copy(nlp1.tree_root)) - n2 = _drop_zeros!(copy(nlp2.tree_root)) - return isequal(n1, n2) -end - -# Print the tree structure of the expression tree -function print_expression_tree(io::IO, nlp::NLPExpr) - return AbstractTrees.print_tree(io, nlp.tree_root) -end -print_expression_tree(io::IO, expr) = println(io, expr) - -""" - print_expression_tree(nlp::NLPExpr) - -Print a tree representation of the nonlinear expression `nlp`. - -**Example** -```julia-repl -julia> expr = (x * sin(x)^3) / 2 -(x * sin(x)^3) / 2 - -julia> print_expression_tree(expr) -/ -├─ * -│ ├─ x -│ └─ ^ -│ ├─ sin -│ │ └─ x -│ └─ 3 -└─ 2 -``` -""" -print_expression_tree(nlp::NLPExpr) = print_expression_tree(stdout::IO, nlp) - -# Convenient expression alias -const AbstractInfOptExpr = Union{ - NLPExpr, - JuMP.GenericQuadExpr{Float64, GeneralVariableRef}, - JuMP.GenericAffExpr{Float64, GeneralVariableRef}, - GeneralVariableRef -} - -## Dispatch function for ast mapping -# Constant -function _ast_process_node(map_func::Function, c) - return c -end - -# Variable -function _ast_process_node(map_func::Function, v::GeneralVariableRef) - return map_func(v) -end - -# AffExpr -function _ast_process_node(map_func::Function, aff::JuMP.GenericAffExpr) - ex = Expr(:call, :+) - for (v, c) in aff.terms - if isone(c) - push!(ex.args, map_func(v)) - else - push!(ex.args, Expr(:call, :*, c, map_func(v))) - end - end - if !iszero(aff.constant) - push!(ex.args, aff.constant) - end - return ex -end - -# QuadExpr -function _ast_process_node(map_func::Function, quad::JuMP.GenericQuadExpr) - ex = Expr(:call, :+) - for (xy, c) in quad.terms - if isone(c) - push!(ex.args, Expr(:call, :*, map_func(xy.a), map_func(xy.b))) - else - push!(ex.args, Expr(:call, :*, c, map_func(xy.a), map_func(xy.b))) - end - end - append!(ex.args, _ast_process_node(map_func, quad.aff).args[2:end]) - return ex -end - -# Map an expression tree to a Julia AST tree that is compatible with JuMP -function _tree_map_to_ast(map_func::Function, node::_LCRST.Node) - if _LCRST.isleaf(node) - return _ast_process_node(map_func, _node_value(node.data)) - else - ex = Expr(:call, _node_value(node.data)) # will be function symbol name - append!(ex.args, (_tree_map_to_ast(map_func, n) for n in node)) - return ex - end -end - -""" - map_nlp_to_ast(map_func::Function, nlp::NLPExpr)::Expr - -Map the nonlinear expression `nlp` to a Julia AST expression where each variable -is mapped via `map_func` and is directly interpolated into the AST expression. -This is intended as an internal method that can be helpful for developers that -wish to map a `NLPExpr` to a Julia AST expression that is compatible with -`JuMP.add_NL_expression`. -""" -function map_nlp_to_ast(map_func::Function, nlp::NLPExpr) - return _tree_map_to_ast(map_func, nlp.tree_root) -end - -################################################################################ -# EXPRESSION CREATION HELPERS -################################################################################ -## Make convenient dispatch methods for raw child input -# NLPExpr -function _process_child_input(nlp::NLPExpr) - return nlp.tree_root -end - -# An InfiniteOpt expression (not general nonlinear) -function _process_child_input(v::AbstractInfOptExpr) - return NodeData(v) -end - -# Function symbol -function _process_child_input(f::Symbol) - return NodeData(f) -end - -# A constant -function _process_child_input(c::Union{Real, Bool}) - return NodeData(c) -end - -# Fallback -function _process_child_input(v) - error("Unrecognized algebraic expression input `$v`.") -end - -# Generic graph builder -function _call_graph(func::Symbol, arg1, args...) - root = _LCRST.Node(NodeData(func)) - prevc = _LCRST.addchild(root, _process_child_input(arg1)) - for a in args - prevc = _LCRST.addchild(root, prevc, _process_child_input(a)) - end - return root -end - -################################################################################ -# SUMS AND PRODUCTS -################################################################################ -## Define helper functions for sum reductions -# Container of NLPExprs -function _reduce_by_first(::typeof(sum), first_itr::NLPExpr, itr, orig_itr; kws...) - for kw in kws - error("Unexpected keyword argument `$kw`.") - end - root = _LCRST.Node(NodeData(:+)) - prevc = _LCRST.addchild(root, first_itr.tree_root) - for ex in itr - prevc = _LCRST.addchild(root, prevc, _process_child_input(ex)) - end - return NLPExpr(root) -end - -# Container of InfiniteOpt exprs -function _reduce_by_first( - ::typeof(sum), - first_itr::JuMP.AbstractJuMPScalar, - itr, - orig_itr; - kws... - ) - for kw in kws - error("Unexpected keyword argument `$kw`.") - end - result = first_itr - for i in itr - result = _MA.operate!!(_MA.add_mul, result, i) - end - return result -end - -# Fallback -function _reduce_by_first(::typeof(sum), first_itr, itr, orig_itr; kws...) - return sum(identity, orig_itr; kws...) -end - -# Hyjack Base.sum for better efficiency on iterators --> this is type piracy... -function Base.sum(itr::Base.Generator; kws...) - isempty(itr) && return sum(identity, itr; kws...) - itr1, new_itr = Iterators.peel(itr) - return _reduce_by_first(sum, itr1, new_itr, itr; kws...) -end - -# Extend Base.sum for container of NLPExprs -function Base.sum(arr::AbstractArray{<:NLPExpr}; init = zero(NLPExpr)) - isempty(arr) && return init - itr1, new_itr = Iterators.peel(arr) - return _reduce_by_first(sum, itr1, new_itr, arr) -end - -# Extend Base.sum for container of InfiniteOpt exprs -function Base.sum( - arr::AbstractArray{<:AbstractInfOptExpr}; - init = zero(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) - ) - isempty(arr) && return init - result = _MA.Zero() - for i in arr - result = _MA.operate!!(_MA.add_mul, result, i) - end - return result -end - -## Define helper functions for reducing products -# Container of InfiniteOpt exprs -function _reduce_by_first(::typeof(prod), first_itr::AbstractInfOptExpr, itr, orig_itr; kws...) - for kw in kws - error("Unexpected keyword argument `$kw`.") - end - root = _LCRST.Node(NodeData(:*)) - prevc = _LCRST.addchild(root, _process_child_input(first_itr)) - for ex in itr - prevc = _LCRST.addchild(root, prevc, _process_child_input(ex)) - end - return NLPExpr(root) -end - -# Fallback -function _reduce_by_first(::typeof(prod), first_itr, itr, orig_itr; kws...) - return prod(identity, orig_itr; kws...) -end - -# Hyjack Base.prod for better efficiency on iterators --> this is type piracy... -function Base.prod(itr::Base.Generator; kws...) - isempty(itr) && return prod(identity, itr; kws...) - itr1, new_itr = Iterators.peel(itr) - return _reduce_by_first(prod, itr1, new_itr, itr; kws...) -end - -# Extend Base.prod for container of InfiniteOpt exprs -function Base.prod(arr::AbstractArray{<:AbstractInfOptExpr}; init = one(NLPExpr)) - isempty(arr) && return init - itr1, new_itr = Iterators.peel(arr) - return _reduce_by_first(prod, itr1, new_itr, arr) -end - -################################################################################ -# MULTIPLICATION OPERATORS -################################################################################ -# TODO more intelligently operate with constants - -# QuadExpr * expr -function Base.:*( - quad::JuMP.GenericQuadExpr{Float64, GeneralVariableRef}, - expr::AbstractInfOptExpr - ) - return NLPExpr(_call_graph(:*, quad, expr)) -end - -# expr * QuadExpr -function Base.:*( - expr::AbstractInfOptExpr, - quad::JuMP.GenericQuadExpr{Float64, GeneralVariableRef} - ) - return NLPExpr(_call_graph(:*, expr, quad)) -end - -# QuadExpr * QuadExpr -function Base.:*( - quad1::JuMP.GenericQuadExpr{Float64, GeneralVariableRef}, - quad2::JuMP.GenericQuadExpr{Float64, GeneralVariableRef} - ) - return NLPExpr(_call_graph(:*, quad1, quad2)) -end - -# NLPExpr * QuadExpr -function Base.:*( - nlp::NLPExpr, - quad::JuMP.GenericQuadExpr{Float64, GeneralVariableRef} - ) - return NLPExpr(_call_graph(:*, nlp, quad)) -end - -# QuadExpr * NLPExpr -function Base.:*( - quad::JuMP.GenericQuadExpr{Float64, GeneralVariableRef}, - nlp::NLPExpr - ) - return NLPExpr(_call_graph(:*, quad, nlp)) -end - -# NLPExpr * expr/constant -function Base.:*(nlp::NLPExpr, expr::Union{AbstractInfOptExpr, Real}) - return NLPExpr(_call_graph(:*, nlp, expr)) -end - -# expr/constant * NLPExpr -function Base.:*(expr::Union{AbstractInfOptExpr, Real}, nlp::NLPExpr) - return NLPExpr(_call_graph(:*, expr, nlp)) -end - -# NLPExpr * NLPExpr -function Base.:*(nlp1::NLPExpr, nlp2::NLPExpr) - return NLPExpr(_call_graph(:*, nlp1, nlp2)) -end - -# expr * expr * expr ... -function Base.:*( - expr1::AbstractInfOptExpr, - expr2::AbstractInfOptExpr, - expr3::AbstractInfOptExpr, - exprs::Vararg{AbstractInfOptExpr} - ) - return NLPExpr(_call_graph(:*, expr1, expr2, expr3, exprs...)) -end - -# *NLPExpr -function Base.:*(nlp::NLPExpr) - return nlp -end - -################################################################################ -# DIVISION OPERATORS -################################################################################ -# expr/constant / expr -function Base.:/( - expr1::Union{AbstractInfOptExpr, Real}, - expr2::AbstractInfOptExpr - ) - return NLPExpr(_call_graph(:/, expr1, expr2)) -end - -# NLPExpr / constant -function Base.:/(nlp::NLPExpr, c::Real) - if iszero(c) - error("Cannot divide by zero.") - elseif isone(c) - return nlp - else - return NLPExpr(_call_graph(:/, nlp, c)) - end -end - -################################################################################ -# POWER OPERATORS -################################################################################ -# expr ^ Integer -function Base.:^(expr::AbstractInfOptExpr, c::Integer) - if iszero(c) - return one(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) - elseif isone(c) - return expr - elseif c == 2 - return expr * expr - else - return NLPExpr(_call_graph(:^, expr, c)) - end -end - -# expr ^ Real -function Base.:^(expr::AbstractInfOptExpr, c::Real) - if iszero(c) - return one(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) - elseif isone(c) - return expr - elseif c == 2 - return expr * expr - else - return NLPExpr(_call_graph(:^, expr, c)) - end -end - -# NLPExpr ^ Integer -function Base.:^(expr::NLPExpr, c::Integer) - if iszero(c) - return one(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) - elseif isone(c) - return expr - else - return NLPExpr(_call_graph(:^, expr, c)) - end -end - -# NLPExpr ^ Real -function Base.:^(expr::NLPExpr, c::Real) - if iszero(c) - return one(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) - elseif isone(c) - return expr - else - return NLPExpr(_call_graph(:^, expr, c)) - end -end - -# expr/constant ^ expr -function Base.:^( - expr1::Union{AbstractInfOptExpr, Real}, - expr2::AbstractInfOptExpr - ) - return NLPExpr(_call_graph(:^, expr1, expr2)) -end - -################################################################################ -# SUBTRACTION OPERATORS -################################################################################ -# TODO more intelligently operate with constants - -# NLPExpr - expr/constant -function Base.:-(nlp::NLPExpr, expr::Union{AbstractInfOptExpr, Real}) - return NLPExpr(_call_graph(:-, nlp, expr)) -end - -# expr/constant - NLPExpr -function Base.:-(expr::Union{AbstractInfOptExpr, Real}, nlp::NLPExpr) - return NLPExpr(_call_graph(:-, expr, nlp)) -end - -# NLPExpr - NLPExpr -function Base.:-(nlp1::NLPExpr, nlp2::NLPExpr) - return NLPExpr(_call_graph(:-, nlp1, nlp2)) -end - -# -NLPExpr -function Base.:-(nlp::NLPExpr) - return NLPExpr(_call_graph(:-, nlp)) -end - -# Var - Var (to avoid using v == v) -function Base.:-(lhs::V, rhs::V) where {V<:GeneralVariableRef} - if isequal(lhs, rhs) - return zero(JuMP.GenericAffExpr{Float64,V}) - else - return JuMP.GenericAffExpr(0.0, - DataStructures.OrderedDict(lhs => 1.0, rhs => -1.0)) - end -end - -################################################################################ -# ADDITION OPERATORS -################################################################################ -# TODO more intelligently operate with constants - -# NLPExpr + expr/constant -function Base.:+(nlp::NLPExpr, expr::Union{AbstractInfOptExpr, Real}) - return NLPExpr(_call_graph(:+, nlp, expr)) -end - -# expr/constant + NLPExpr -function Base.:+(expr::Union{AbstractInfOptExpr, Real}, nlp::NLPExpr) - return NLPExpr(_call_graph(:+, expr, nlp)) -end - -# NLPExpr + NLPExpr -function Base.:+(nlp1::NLPExpr, nlp2::NLPExpr) - return NLPExpr(_call_graph(:+, nlp1, nlp2)) -end - -# +NLPExpr -function Base.:+(nlp::NLPExpr) - return nlp -end - -################################################################################ -# MUTABLE ARITHMETICS -################################################################################ -# Define NLPExpr as a mutable type for MA -_MA.mutability(::Type{NLPExpr}) = _MA.IsMutable() - -# Extend MA.promote_operation for bettered efficiency -for type in (:Real, :GeneralVariableRef, - :(JuMP.GenericAffExpr{Float64, GeneralVariableRef}), - :(JuMP.GenericQuadExpr{Float64, GeneralVariableRef})) - @eval begin - function _MA.promote_operation( - ::Union{typeof(+),typeof(-),typeof(*),typeof(/),typeof(^)}, - ::Type{<:$type}, - ::Type{NLPExpr} - ) - return NLPExpr - end - function _MA.promote_operation( - ::Union{typeof(+),typeof(-),typeof(*),typeof(/),typeof(^)}, - ::Type{NLPExpr}, - ::Type{<:$type} - ) - return NLPExpr - end - end -end -function _MA.promote_operation( - ::Union{typeof(+),typeof(-),typeof(*),typeof(/),typeof(^)}, - ::Type{NLPExpr}, - ::Type{NLPExpr} - ) - return NLPExpr -end -for type in (:GeneralVariableRef, - :(JuMP.GenericAffExpr{Float64, GeneralVariableRef})) - @eval begin - function _MA.promote_operation( - ::Union{typeof(*),typeof(/),typeof(^)}, - ::Type{<:$type}, - ::Type{JuMP.GenericQuadExpr{Float64, GeneralVariableRef}} - ) - return NLPExpr - end - function _MA.promote_operation( - ::Union{typeof(*),typeof(/),typeof(^)}, - ::Type{JuMP.GenericQuadExpr{Float64, GeneralVariableRef}}, - ::Type{<:$type} - ) - return NLPExpr - end - end -end -function _MA.promote_operation( - ::Union{typeof(*),typeof(/),typeof(^)}, - ::Type{<:JuMP.GenericQuadExpr{Float64, GeneralVariableRef}}, - ::Type{<:JuMP.GenericQuadExpr{Float64, GeneralVariableRef}} - ) - return NLPExpr -end -for type in (:GeneralVariableRef, - :(JuMP.GenericAffExpr{Float64, GeneralVariableRef}), - :(JuMP.GenericQuadExpr{Float64, GeneralVariableRef})) - @eval begin - function _MA.promote_operation( - ::Union{typeof(/),typeof(^)}, - ::Type{<:Real}, - ::Type{<:$type} - ) - return NLPExpr - end - end -end -for type in (:GeneralVariableRef, - :(JuMP.GenericAffExpr{Float64, GeneralVariableRef})) - @eval begin - function _MA.promote_operation( - ::Union{typeof(/),typeof(^)}, - ::Type{GeneralVariableRef}, - ::Type{<:$type} - ) - return NLPExpr - end - end -end -for type in (:GeneralVariableRef, - :(JuMP.GenericAffExpr{Float64, GeneralVariableRef})) - @eval begin - function _MA.promote_operation( - ::Union{typeof(/),typeof(^)}, - ::Type{JuMP.GenericAffExpr{Float64, GeneralVariableRef}}, - ::Type{<:$type} - ) - return NLPExpr - end - end -end - -# Extend MA.scaling in case an NLPExpr needs to be converted to a number -function _MA.scaling(nlp::NLPExpr) - c = _node_value(nlp.tree_root.data) - if !(c isa Real) - error("Cannot convert `$nlp` to `$Float64`.") - end - return _MA.scaling(c) -end - -# Extend MA.mutable_copy to avoid unnecessary copying -function _MA.mutable_copy(nlp::NLPExpr) - return nlp # we don't need to copy since we build from the leaves up -end - -# Extend MA.operate! as required -function _MA.operate!( - op::Union{typeof(zero), typeof(one)}, - ::NLPExpr - ) - return op(NLPExpr) # not actually mutable for safety and efficiency -end -function _MA.operate!( - op::Union{typeof(+), typeof(-), typeof(*), typeof(/), typeof(^)}, - nlp::NLPExpr, - v - ) - return op(nlp, v) -end -function _MA.operate!( - op::Union{typeof(+), typeof(-), typeof(*), typeof(/), typeof(^)}, - v, - nlp::NLPExpr - ) - return op(v, nlp) -end -function _MA.operate!( - op::typeof(+), - v::Union{JuMP.GenericAffExpr{Float64, GeneralVariableRef}, - JuMP.GenericQuadExpr{Float64, GeneralVariableRef}}, - nlp::NLPExpr - ) - return op(v, nlp) -end -function _MA.operate!( - op::typeof(-), - v::Union{JuMP.GenericAffExpr{Float64, GeneralVariableRef}, - JuMP.GenericQuadExpr{Float64, GeneralVariableRef}}, - nlp::NLPExpr - ) - return op(v, nlp) -end -function _MA.operate!( - op::Union{typeof(+), typeof(-), typeof(*), typeof(/), typeof(^)}, - nlp1::NLPExpr, - nlp2::NLPExpr - ) - return op(nlp1, nlp2) -end -function _MA.operate!(op::_MA.AddSubMul, nlp::NLPExpr, args...) - return _MA.add_sub_op(op)(nlp, *(args...)) -end - -# TODO maybe extend _MA.add_mul/_MA_.sub_mul as well - -################################################################################ -# NATIVE NLP FUNCTIONS -################################################################################ -# Store all of the native registered functions -const _NativeNLPFunctions = Dict{Tuple{Symbol, Int}, Function}( - (:-, 2) => -, - (:/, 2) => /, - (:^, 2) => ^ -) - -# List of 1 argument base functions to register -const _Base1ArgFuncList = ( - :sqrt => sqrt, - :cbrt => cbrt, - :abs => abs, - :abs2 => abs2, - :inv => inv, - :log => log, - :log10 => log10, - :log2 => log2, - :log1p => log1p, - :exp => exp, - :exp2 => exp2, - :expm1 => expm1, - :sin => sin, - :cos => cos, - :tan => tan, - :sec => sec, - :csc => csc, - :cot => cot, - :sind => sind, - :cosd => cosd, - :tand => tand, - :secd => secd, - :cscd => cscd, - :cotd => cotd, - :asin => asin, - :acos => acos, - :atan => atan, - :asec => asec, - :acsc => acsc, - :acot => acot, - :asind => asind, - :acosd => acosd, - :atand => atand, - :asecd => asecd, - :acscd => acscd, - :acotd => acotd, - :sinh => sinh, - :cosh => cosh, - :tanh => tanh, - :sech => sech, - :csch => csch, - :coth => coth, - :asinh => asinh, - :acosh => acosh, - :atanh => atanh, - :asech => asech, - :acsch => acsch, - :acoth => acoth, - :deg2rad => deg2rad, - :rad2deg => rad2deg -) - -# Setup the base 1 argument functions -for (name, func) in _Base1ArgFuncList - # add it to the main storage dict - _NativeNLPFunctions[(name, 1)] = func - # make an expression constructor - @eval begin - function Base.$name(v::AbstractInfOptExpr) - return NLPExpr(_call_graph($(quot(name)), v)) - end - end -end - -# Setup the ifelse function -_NativeNLPFunctions[(:ifelse, 3)] = Core.ifelse - -""" - InfiniteOpt.ifelse(cond::NLPExpr, v1::Union{AbstractInfOptExpr, Real}, - v2::Union{AbstractInfOptExpr, Real})::NLPExpr - -A symbolic version of `Core.ifelse` that can be used to establish symbolic -expressions with logic conditions. Note that is must be written -`InfiniteOpt.ifelse` since it conflicts with `Core.ifelse`. - -**Example** -```julia -julia> InfiniteOpt.ifelse(x >= y, 0, y^3) -ifelse(x >= y, 0, y^3) -``` -""" -function ifelse( - cond::NLPExpr, - v1::Union{AbstractInfOptExpr, Real}, - v2::Union{AbstractInfOptExpr, Real} - ) - return NLPExpr(_call_graph(:ifelse, cond, v1, v2)) -end -function ifelse( - cond::Bool, - v1::Union{AbstractInfOptExpr, Real}, - v2::Union{AbstractInfOptExpr, Real} - ) - return cond ? v1 : v2 -end - -# Setup the Base comparison functions -for (name, func) in (:< => Base.:(<), :(==) => Base.:(==), :> => Base.:(>), - :<= => Base.:(<=), :>= => Base.:(>=)) - # add it to the main storage dict - _NativeNLPFunctions[(name, 2)] = func - # make an expression constructor - @eval begin - function Base.$name(v::AbstractInfOptExpr, c::Real) - return NLPExpr(_call_graph($(quot(name)), v, c)) - end - function Base.$name(c::Real, v::AbstractInfOptExpr) - return NLPExpr(_call_graph($(quot(name)), c, v)) - end - function Base.$name(v1::AbstractInfOptExpr, v2::AbstractInfOptExpr) - return NLPExpr(_call_graph($(quot(name)), v1, v2)) - end - if $(quot(name)) in (:<, :>) - function Base.$name(v1::GeneralVariableRef, v2::GeneralVariableRef) - if isequal(v1, v2) - return false - else - return NLPExpr(_call_graph($(quot(name)), v1, v2)) - end - end - else - function Base.$name(v1::GeneralVariableRef, v2::GeneralVariableRef) - if isequal(v1, v2) - return true - else - return NLPExpr(_call_graph($(quot(name)), v1, v2)) - end - end - end - end -end - -# Setup the Base logical functions (we cannot extend && and || directly) -_NativeNLPFunctions[(:&&, 2)] = Base.:& -_NativeNLPFunctions[(:||, 2)] = Base.:| - -# Logical And -function Base.:&(v::Union{GeneralVariableRef, NLPExpr}, c::Bool) - return c ? v : false -end -function Base.:&(c::Bool, v::Union{GeneralVariableRef, NLPExpr}) - return c ? v : false -end -function Base.:&( - v1::Union{GeneralVariableRef, NLPExpr}, - v2::Union{GeneralVariableRef, NLPExpr} - ) - return NLPExpr(_call_graph(:&&, v1, v2)) -end - -# Logical Or -function Base.:|(v::Union{GeneralVariableRef, NLPExpr}, c::Bool) - return c ? true : v -end -function Base.:|(c::Bool, v::Union{GeneralVariableRef, NLPExpr}) - return c ? true : v -end -function Base.:|( - v1::Union{GeneralVariableRef, NLPExpr}, - v2::Union{GeneralVariableRef, NLPExpr}) - return NLPExpr(_call_graph(:||, v1, v2) - ) -end - -const _Special1ArgFuncList = ( - :erf => SpecialFunctions.erf, - :erfinv => SpecialFunctions.erfinv, - :erfc => SpecialFunctions.erfc, - :erfcinv => SpecialFunctions.erfcinv, - :erfi => SpecialFunctions.erfi, - :gamma => SpecialFunctions.gamma, - :lgamma => SpecialFunctions.lgamma, - :digamma => SpecialFunctions.digamma, - :invdigamma => SpecialFunctions.invdigamma, - :trigamma => SpecialFunctions.trigamma, - :airyai => SpecialFunctions.airyai, - :airybi => SpecialFunctions.airybi, - :airyaiprime => SpecialFunctions.airyaiprime, - :airybiprime => SpecialFunctions.airybiprime, - :besselj0 => SpecialFunctions.besselj0, - :besselj1 => SpecialFunctions.besselj1, - :bessely0 => SpecialFunctions.bessely0, - :bessely1 => SpecialFunctions.bessely1, - :erfcx => SpecialFunctions.erfcx, - :dawson => SpecialFunctions.dawson -) - -# Setup the SpecialFunctions 1 argument functions -for (name, func) in _Special1ArgFuncList - # add it to the main storage dict - _NativeNLPFunctions[(name, 1)] = func - # make an expression constructor - @eval begin - function SpecialFunctions.$name(v::AbstractInfOptExpr) - return NLPExpr(_call_graph($(quot(name)), v)) - end - end -end +# TODO add deprecation errors for old methods ################################################################################ # USER FUNCTIONS ################################################################################ +# Keep track of the predefined functions in MOI +const _NativeNLPFunctions = append!(copy(MOI.Nonlinear.DEFAULT_UNIVARIATE_OPERATORS), + MOI.Nonlinear.DEFAULT_MULTIVARIATE_OPERATORS) +append!(_NativeNLPFunctions, (:&&, :||, :<=, :(==), :>=, :<, :>)) + """ RegisteredFunction{F <: Function, G <: Union{Function, Nothing}, H <: Union{Function, Nothing}} @@ -1082,261 +15,130 @@ end A type for storing used defined registered functions and their information that is needed by JuMP for build an `NLPEvaluator`. The constructor is of the form: ```julia - RegisteredFunction(name::Symbol, num_args::Int, func::Function, - [gradient::Function, hessian::Function]) + RegisteredFunction(op::Symbol, dim::Int, f::Function, + [∇f::Function, ∇²f::Function]) ``` **Fields** -- `name::Symbol`: The name of the function that is used in `NLPExpr`s. -- `num_args::Int`: The number of function arguments. -- `func::F`: The function itself. -- `gradient::G`: The gradient function if one is given. -- `hessian::H`: The hessian function if one is given. +- `op::Symbol`: The name of the function that is used in `NLPExpr`s. +- `dim::Int`: The number of function arguments. +- `f::F`: The function itself. +- `∇f::G`: The gradient function if one is given. +- `∇²f::H`: The hessian function if one is given. """ struct RegisteredFunction{F <: Function, G, H} - name::Symbol - num_args::Int - func::F - gradient::G - hessian::H + op::Symbol + dim::Int + f::F + ∇f::G + ∇²f::H # Constructors function RegisteredFunction( - name::Symbol, - num_args::Int, - func::F + op::Symbol, + dim::Int, + f::F ) where {F <: Function} - return new{F, Nothing, Nothing}(name, num_args, func, nothing, nothing) + return new{F, Nothing, Nothing}(op, dim, f, nothing, nothing) end function RegisteredFunction( - name::Symbol, - num_args::Int, - func::F, - gradient::G + op::Symbol, + dim::Int, + f::F, + ∇f::G ) where {F <: Function, G <: Function} - if isone(num_args) && !hasmethod(gradient, Tuple{Real}) + if isone(dim) && !hasmethod(∇f, Tuple{Real}) error("Invalid gradient function form, see the docs for details.") - elseif !isone(num_args) && !hasmethod(gradient, Tuple{AbstractVector{Real}, ntuple(_->Real, num_args)...}) + elseif !isone(dim) && !hasmethod(∇f, Tuple{AbstractVector{Real}, ntuple(_->Real, dim)...}) error("Invalid multi-variate gradient function form, see the docs for details.") end - return new{F, G, Nothing}(name, num_args, func, gradient, nothing) + return new{F, G, Nothing}(op, dim, f, ∇f, nothing) end function RegisteredFunction( - name::Symbol, - num_args::Int, - func::F, - gradient::G, - hessian::H + op::Symbol, + dim::Int, + f::F, + ∇f::G, + ∇²f::H ) where {F <: Function, G <: Function, H <: Function} - if isone(num_args) && !hasmethod(gradient, Tuple{Real}) + if isone(dim) && !hasmethod(∇f, Tuple{Real}) error("Invalid gradient function form, see the docs for details.") - elseif isone(num_args) && !hasmethod(hessian, Tuple{Real}) + elseif isone(dim) && !hasmethod(∇²f, Tuple{Real}) error("Invalid hessian function form, see the docs for details.") end - return new{F, G, H}(name, num_args, func, gradient, hessian) + return new{F, G, H}(op, dim, f, ∇f, ∇²f) end end -# Helper function for @register -function _register( - _error::Function, - call_mod::Module, - model::InfiniteModel, - name::Symbol, - num_args::Int, +# TODO expand details in docstring +""" + JuMP.add_user_defined_function( + model::InfiniteModel, + op::Symbol, + dim::Int, + f::Function, + [∇f::Function,] + [∇²f::Function,] + ) + +Extend `JuMP.add_user_defined_function` for `InfiniteModel`s. + +Add a user-defined function with `dim` input arguments to `model` and associate +it with the operator `op`. + +The function `f` evaluates the function. The optional function `∇f` evaluates +the first derivative, and the optional function `∇²f` evaluates the second +derivative. `∇²f` may be provided only if `∇f` is also provided. +""" +function JuMP.add_user_defined_function( + model::InfiniteModel, + op::Symbol, + dim::Int, funcs... ) if !all(f -> f isa Function, funcs) - _error("Gradient and/or hessian must be functions.") - elseif haskey(_NativeNLPFunctions, (name, num_args)) || - haskey(model.func_lookup, (name, num_args)) - _error("A function with name `$name` and $num_args arguments is already " * + error("The gradient and/or hessian must be functions, but got argument(s) " * + "of type `" * join(Tuple(typeof(f) for f in funcs if !(f isa Function)), "`, `") * + "`.") + elseif isempty(funcs) + error("Tried to register `$op`, but no evaluation function was given.") + elseif op in _NativeNLPFunctions || op in keys(model.func_lookup) + error("A function with name `$op` arguments is already " * "registered. Please use a function with a different name.") - elseif !hasmethod(funcs[1], NTuple{num_args, Real}) - _error("The function `$name` is not defined for arguments of type `Real`.") + elseif !hasmethod(funcs[1], NTuple{dim, Real}) + error("The function `$op` is not defined for arguments of type `Real`.") elseif length(unique!([m.module for m in methods(funcs[1])])) > 1 || first(methods(funcs[1])).module !== call_mod - _error("Cannot register function names that are used by packages. Try " * - "wrapping `$(funcs[1])` in a user-defined function.") - end - push!(model.registrations, RegisteredFunction(name, num_args, funcs...)) - model.func_lookup[name, num_args] = funcs[1] - return -end - -# Helper function to check the inputs of created functions -function _check_function_args(model::InfiniteModel, f_name, args...) - for a in args - m = _model_from_expr(a) - if !isnothing(m) && m !== model - error("`$f_name` is a registered function in a different model than " * - "`$a` belongs to. Try registering `$f_name` to the current " * - "model.") - end - end - return -end - -""" - @register(model::InfiniteModel, func_expr, [gradient::Function], [hessian::Function]) - -Register a user-defined function in accordance with `func_expr` such that it can -be used in `NLPExpr`s that are used with `model` without being traced. - -**Argument Information** -Here `func_expr` is of the form: `myfunc(a, b)` where `myfunc` is the function -name and the number of arguments are given symbolically. Note that the choice -of argument symbols is arbitrary. Each function argument must support anything -of type `Real` to specified. - -Here we can also specify a gradient function `gradient` which for 1 argument -functions must taken in the 1 argument and return its derivative. For -multi-argument functions the gradient function must be of the form: -```julia -function gradient(g::AbstractVector{T}, args::T...) where {T <: Real} - # fill g vector with the gradient of the function -end -``` - -For 1 argument functions we can also specify a hessian function with takes that -argument and return the 2nd derivative. Hessians can ge specified for -multi-argument functions, but `JuMP` backends do not currently support this. - -If no gradient and/or hessian is given, the automatic differentation capabilities -of the backend (e.g., `JuMP`) will be used to determine them. Note that the -`JuMP` backend does not use Hessian's for user-defined multi-argument functions. - -**Notes** -- When possible, tracing is preferred over registering a function (see - [Function Tracing](@ref) for more info). -- Only user-defined functions can be specified. If the function is used by a - package then it can not be used directly. However, we can readily wrap it in a - new function `newfunc(a) = pkgfunc(a)`. -- We can only register functions in the same scope that they are defined in. -- Registered functions can only be used in or below the scope in which they are - registered. For instance, if we register some function inside of another - function then we can only use it inside that function (not outside of it). -- A function with a given name and number of arguments can only be registered - once in a particular model. - -**Examples** -```julia-repl -julia> @variable(model, x) -x - -julia> f(a) = a^3; - -julia> f(x) # user-function gets traced -x^3 - -julia> @register(model, f(a)) # register function -f (generic function with 2 methods) - -julia> f(x) # function is no longer traced and autodifferentiation will be used -f(x) - -julia> f2(a) = a^2; g2(a) = 2 * a; h2(a) = 2; - -julia> @register(model, f2(a), g2, h2) # register with explicit gradient and hessian -f2 (generic function with 2 methods) - -julia> f2(x) -f2(x) - -julia> f3(a, b) = a * b^2; - -julia> function g3(v, a, b) - v[1] = b^2 - v[2] = 2 * a * b - return - end; - -julia> @register(model, f3(a, b), g3) # register multi-argument function -f3 (generic function with 4 methods) - -julia> f3(42, x) -f3(42, x) -``` -""" -macro register(model, f, args...) - # define error message function - _error(str...) = _macro_error(:register, (f, args...), __source__, str...) - - # parse the arguments and check - pos_args, extra_kwargs, _, _ = _extract_kwargs(args) - if !isempty(extra_kwargs) - _error("Keyword arguments were given, but none are accepted.") - elseif length(pos_args) > 2 - _error("Too many position arguments given, should be of form " * - "`@register(myfunc(a), [gradient], [hessian])` where " * - "`gradient` and `hessian` are optional arguments.") + error("Cannot register function names that are used by packages. Try " * + "wrapping `$(funcs[1])` in a user-defined function.") end - - # process the function input - if isexpr(f, :call) && all(a -> a isa Symbol, f.args) - f_name = f.args[1] - f_args = f.args[2:end] - num_args = length(f_args) - else - _error("Unexpected function format, should be of form `myfunc(a, b)`.") - end - - # start creating the register code and register - code = Expr(:block) - push!(code.args, quote - $model isa InfiniteModel || $_error("Expected an `InfiniteModel`.") - end) - calling_mod = __module__ # get the module the macro is being called from - push!(code.args, quote - InfiniteOpt._register($_error, $calling_mod, $model, $(quot(f_name)), - $num_args, $(f_name), $(args...)) - end) - - # define the function overloads needed to create expressions - Ts = [Real, AbstractInfOptExpr] - type_combos = vec(collect(Iterators.product(ntuple(_->Ts, num_args)...))) - filter!(ts -> !all(T -> T == Real, ts), type_combos) # remove combo with only Reals - annotype(name, T) = :($name :: $T) - set_args(xs, vs) = (xs = map(annotype, xs, vs); xs) - for ts in type_combos - push!(code.args, quote - function $(f_name)($(set_args(f_args, ts)...)) - InfiniteOpt._check_function_args($model, $(quot(f_name)), $(f_args...)) - return NLPExpr(InfiniteOpt._call_graph($(quot(f_name)), $(f_args...))) - end - end) - end - - # return the code - return esc(code) + push!(model.registrations, RegisteredFunction(op, dim, funcs...)) + model.func_lookup[op] = (funcs[1], dim) + return JuMP.UserDefinedFunction(op) end """ - name_to_function(model::InfiniteModel, name::Symbol, num_args::Int)::Union{Function, Nothing} + name_to_function(model::InfiniteModel, op::Symbol)::Union{Function, Nothing} -Return the registered function that corresponds to `name` with `num_args`. +Return the registered function that corresponds to `op`. Returns `nothing` if no such registered function exists. This helps retrieve the -functions of function names stored in `NLPExpr`s. +functions of user-defined functions. + +!!! warning + Currently, this does not return functions for default operators. """ -function name_to_function(model::InfiniteModel, name::Symbol, num_args::Int) - if name == :+ - return + - elseif name == :* - return * - else - return get(_NativeNLPFunctions, (name, num_args), - get(model.func_lookup, (name, num_args), nothing)) - end +function name_to_function(model::InfiniteModel, op::Symbol) + haskey(model.func_lookup, op) && return model.func_lookup[op][1] + return end """ - all_registered_functions(model::InfiniteModel)::Vector{Function} + all_registered_functions(model::InfiniteModel)::Vector{Symbol} Retrieve all the functions that are currently registered to `model`. """ function all_registered_functions(model::InfiniteModel) - funcs = append!(collect(values(_NativeNLPFunctions)), (+, *)) - return append!(funcs, values(model.func_lookup)) + return append!(copy(_NativeNLPFunctions), map(first, values(model.func_lookup))) end """ @@ -1355,7 +157,7 @@ function _add_func_data_to_jump( model::JuMP.Model, data::RegisteredFunction{F, Nothing, Nothing} ) where {F <: Function} - JuMP.register(model, data.name, data.num_args, data.func, autodiff = true) + JuMP.add_user_defined_function(model, data.op, data.dim, data.f) return end @@ -1364,19 +166,13 @@ function _add_func_data_to_jump( model::JuMP.Model, data::RegisteredFunction{F, G, Nothing} ) where {F <: Function, G <: Function} - JuMP.register(model, data.name, data.num_args, data.func, data.gradient, - autodiff = isone(data.num_args)) + JuMP.add_user_defined_function(model, data.op, data.dim, data.f, data.∇f) return end # Gradient and hessian information function _add_func_data_to_jump(model::JuMP.Model, data::RegisteredFunction) - if data.num_args > 1 - error("JuMP does not support hessians for multi-argument registered " * - "functions.") - end - JuMP.register(model, data.name, data.num_args, data.func, data.gradient, - data.hessian) + JuMP.add_user_defined_function(model, data.op, data.dim, data.f, data.∇f, data.∇²f) return end @@ -1393,145 +189,3 @@ function add_registered_to_jump(opt_model::JuMP.Model, inf_model::InfiniteModel) end return end - -################################################################################ -# LINEAR ALGEBRA -################################################################################ -# Extend LinearAlgebra.dot for increased efficiency -LinearAlgebra.dot(lhs::AbstractInfOptExpr, rhs::AbstractInfOptExpr) = lhs * rhs -LinearAlgebra.dot(lhs::AbstractInfOptExpr, rhs::Real) = lhs * rhs -LinearAlgebra.dot(lhs::Real, rhs::AbstractInfOptExpr) = lhs * rhs - -# Implement promote_rule to help build better containers -function Base.promote_rule(::Type{NLPExpr}, ::Type{<:Real}) - return NLPExpr -end -function Base.promote_rule(::Type{NLPExpr}, ::Type{GeneralVariableRef}) - return NLPExpr -end -function Base.promote_rule(::Type{NLPExpr}, ::Type{<:JuMP.GenericAffExpr}) - return NLPExpr -end -function Base.promote_rule(::Type{NLPExpr}, ::Type{<:JuMP.GenericQuadExpr}) - return NLPExpr -end - -# TODO make proper MA extensions to enable efficient definition - -################################################################################ -# PRINTING -################################################################################ -# Define better printing for NodeData -function Base.show(io::IO, data::NodeData) - return print(io, string(_node_value(data))) -end - -# Map operators to their respective precedence (largest is highest priority) -const _Precedence = (; :^ => 6, Symbol("+u") => 5, Symbol("-u") => 5, :* => 4, - :/ => 4, :+ => 3, :- => 3, :(==) => 2, :<= => 2, :>= => 2, - :> => 2, :< => 2, :&& => 1, :|| => 1) - -## Make functions to determine the precedence of a leaf -# AffExpr -function _leaf_precedence(aff::JuMP.GenericAffExpr) - has_const = !iszero(JuMP.constant(aff)) - itr = JuMP.linear_terms(aff) - num_terms = length(itr) - if iszero(num_terms) - # we have only a constant - return 10 # will always have precedence - elseif has_const || num_terms > 1 - # we have an expr with multiple terms - return 3 - elseif isone(first(itr)[1]) - # we have a single variable - return 10 # will always have precedence - elseif first(itr)[1] == -1 - # we have a single unary negative variable - return 5 - else - # we have a single variable multiplied by some coefficient - return 4 - end -end - -# QuadExpr -function _leaf_precedence(quad::JuMP.GenericQuadExpr) - has_aff = !iszero(quad.aff) - itr = JuMP.quad_terms(quad) - num_terms = length(itr) - if iszero(num_terms) - # we have an affine expression - return _leaf_precedence(quad.aff) - elseif has_aff || num_terms > 1 - # we have a general quadratic expression - return 3 - else - # we only have a single quadratic term - return 4 - end -end - -# Other -function _leaf_precedence(v) - return 10 -end - -# Recursively build an expression string, starting with a root node -function _expr_string( - node::_LCRST.Node{NodeData}, - str::String = ""; - prev_prec = 0, - prev_comm = false - ) - # prepocess the raw value - raw_value = _node_value(node.data) - is_op = raw_value isa Symbol && haskey(_Precedence, raw_value) - data_str = _string_round(raw_value) - # make a string according to the node structure - if _LCRST.isleaf(node) && _leaf_precedence(raw_value) > prev_prec - # we have a leaf that doesn't require parentheses - return str * data_str - elseif _LCRST.isleaf(node) - # we have a leaf that requires parentheses - return str * string("(", data_str, ")") - elseif is_op && !_LCRST.islastsibling(node.child) - # we have a binary operator - curr_prec = _Precedence[raw_value] - has_prec = curr_prec > prev_prec || (prev_comm && curr_prec == prev_prec) - if !has_prec - str *= "(" - end - op_str = data_str == "^" ? data_str : string(" ", data_str, " ") - is_comm = raw_value == :* || raw_value == :+ - for child in node - str = string(_expr_string(child, str, prev_prec = curr_prec, - prev_comm = is_comm), op_str) - end - str = str[1:prevind(str, end, length(op_str))] - return has_prec ? str : str * ")" - elseif is_op - # we have a unary operator - curr_prec = _Precedence[Symbol(raw_value, :u)] - has_prec = curr_prec > prev_prec - if !has_prec - str *= "(" - end - str *= string(data_str, _expr_string(node.child, str, - prev_prec = curr_prec)) - return has_prec ? str : str * ")" - else - # we have a function - str *= string(data_str, "(") - for child in node - str = _expr_string(child, str) - str *= ", " - end - return str[1:prevind(str, end, 2)] * ")" - end -end - -# Extend JuMP.function_string for nonlinear expressions -function JuMP.function_string(mode, nlp::NLPExpr) - return _expr_string(nlp.tree_root) -end diff --git a/src/results.jl b/src/results.jl index 6985e910e..b2a4df416 100644 --- a/src/results.jl +++ b/src/results.jl @@ -239,7 +239,7 @@ julia> value(my_infinite_expr) ``` """ function JuMP.value( - expr::AbstractInfOptExpr; + expr::JuMP.AbstractJuMPScalar; result::Int = 1, kwargs... ) @@ -247,8 +247,8 @@ function JuMP.value( model = _model_from_expr(expr) # if no model then the expression only contains a constant if isnothing(model) - expr isa NLPExpr && error("Cannot evaluate the value of `$expr`,", - "because it doesn't have variables.") + expr isa JuMP.NonlinearExpr && error("Cannot evaluate the value of `$expr`,", + "because it doesn't have variables.") return JuMP.constant(expr) # otherwise let's call map_value else @@ -333,7 +333,7 @@ end # Default method that depends on optimizer_model_constraint --> making extensions easier function map_optimizer_index(cref::InfOptConstraintRef, key; kwargs...) - if JuMP.jump_function(JuMP.constraint_object(cref)) isa NLPExpr + if JuMP.jump_function(JuMP.constraint_object(cref)) isa JuMP.NonlinearExpr # TODO not sure if this is still true error("`optimizer_index` not defined for general nonlinear constraints.") end opt_cref = optimizer_model_constraint(cref, key; kwargs...) diff --git a/test/runtests.jl b/test/runtests.jl index 48ed63ad4..2bda91805 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,8 +1,11 @@ +import Pkg +Pkg.pkg"add JuMP#od/nlp-expr MathOptInterface#od/nlp-expr" + using InfiniteOpt: _domain_or_error using Test: Error # Load in the dependencies using InfiniteOpt, Distributions, Random, FastGaussQuadrature, DataStructures, -LeftChildRightSiblingTrees, AbstractTrees, Suppressor, LinearAlgebra +Suppressor, LinearAlgebra import MutableArithmetics # load the test module From 380fde555332dbf6b48e0b268be0029810d74ed8 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Fri, 28 Apr 2023 17:52:50 -0400 Subject: [PATCH 02/40] update optimizer_index for constraints --- src/results.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/results.jl b/src/results.jl index b2a4df416..10b360af6 100644 --- a/src/results.jl +++ b/src/results.jl @@ -333,9 +333,6 @@ end # Default method that depends on optimizer_model_constraint --> making extensions easier function map_optimizer_index(cref::InfOptConstraintRef, key; kwargs...) - if JuMP.jump_function(JuMP.constraint_object(cref)) isa JuMP.NonlinearExpr # TODO not sure if this is still true - error("`optimizer_index` not defined for general nonlinear constraints.") - end opt_cref = optimizer_model_constraint(cref, key; kwargs...) if opt_cref isa AbstractArray return map(c -> JuMP.optimizer_index(c), opt_cref) From 0a48881e62aa69fb2d590dc68966977cae2c07d5 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Fri, 28 Apr 2023 23:44:59 -0400 Subject: [PATCH 03/40] Begin updates for docs and tests --- docs/Project.toml | 1 + docs/make.jl | 3 ++ docs/src/guide/expression.md | 42 +++++------------- docs/src/index.md | 5 +-- docs/src/manual/expression.md | 9 +--- test/runtests.jl | 84 +++++++++++++++++------------------ 6 files changed, 61 insertions(+), 83 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index f13d1f534..e4ad066f9 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -9,6 +9,7 @@ Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" [compat] HiGHS = "1" diff --git a/docs/make.jl b/docs/make.jl index b1f63a23d..2fae33a1a 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,3 +1,6 @@ +import Pkg +Pkg.pkg"add JuMP#od/nlp-expr MathOptInterface#od/nlp-expr add Ipopt#od/nlp-expr" + using Documenter, InfiniteOpt, Distributions, Literate, Random if !@isdefined(EXAMPLE_DIR) diff --git a/docs/src/guide/expression.md b/docs/src/guide/expression.md index 14bca9486..82c4e98b0 100644 --- a/docs/src/guide/expression.md +++ b/docs/src/guide/expression.md @@ -9,8 +9,9 @@ used in `InfiniteOpt`. See the [technical manual](@ref expr_manual) for more details. !!! note - Nonlinear modeling is handled differently in `InfiniteOpt` vs `JuMP`. See - [Nonlinear Expressions](@ref nlp_guide) for more information. + Nonlinear modeling is now handled in `InfiniteOpt` via `JuMP`'s new + nonlinear interface. See [Nonlinear Expressions](@ref nlp_guide) for + more information. ## Overview Expressions in `InfiniteOpt` (also called functions) refer to mathematical @@ -261,39 +262,20 @@ More information can be found in the documentation for quadratic expressions in [`JuMP`](https://jump.dev/JuMP.jl/v1/reference/expressions/#Quadratic-expressions). ## [Nonlinear Expressions](@id nlp_guide) -General nonlinear expressions as generated via `JuMP.@NLexpression`, -`JuMP.@NLobjective`, and/or `JuMP.@NLconstraint` macros in `JuMP` are not -extendable for extension packages like `InfiniteOpt`. A fundamental -overhaul is planned to resolve this problem (check the status on -[GitHub](https://github.com/jump-dev/MathOptInterface.jl/issues/846)), but this -will likely require 1-3 years to resolve. +In this section, we walk you through the ins and out of working with general +nonlinear (i.e., not affine or quadratic) expressions in `InfiniteOpt`. !!! info - `JuMP-dev` has secured funding to overhaul their nonlinear interface and - hence the timeline for resolving many of the limitations should be expedited. - Check out their [announcement](https://jump.dev/announcements/2022/02/21/lanl/) - for more information. + Our previous `InfiniteOpt` specific nonlinear API as been deprecated in + favor of `JuMP`'s new and improved nonlinear interface. Thus, `InfiniteOpt` + now strictly uses the same expression structures as `JuMP`. -Thus, in the interim, we circumvent this problem in `InfiniteOpt` by implementing -our own general nonlinear expression API. However, we will see that our interface -treats nonlinear expressions as 1st class citizens and thus is generally more -convenient than using `JuMP`'s current legacy nonlinear modeling interface. -We discuss the ins and outs of this interface in the subsections below. - -!!! note - Unlike affine/quadratic expressions, our nonlinear interface differs from - that of `JuMP`. Thus, it is important to carefully review the sections - below to familiarize yourself with our syntax. - -!!! warning - Our new general nonlinear modeling interface is experimental and thus is - subject to change to address any unintended behavior. Please notify us on - GitHub if you encounter any unexpected behavior. + ### Basic Usage -In `InfiniteOpt` we can define nonlinear expressions in similar manner to how -affine/quadratic expressions are made in `JuMP`. For instance, we can make an -expression using normal Julia code outside a macro: +We can define nonlinear expressions in similar manner to how affine/quadratic +expressions are made in `JuMP`. For instance, we can make an expression using +normal Julia code outside a macro: ```jldoctest nlp; setup = :(using InfiniteOpt; model = InfiniteModel()) julia> @infinite_parameter(model, t ∈ [0, 1]); @variable(model, y, Infinite(t)); diff --git a/docs/src/index.md b/docs/src/index.md index d98b7e170..c970f4ce6 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -7,9 +7,8 @@ A `JuMP` extension for expressing and solving infinite-dimensional optimization problems. !!! note - `InfiniteOpt v0.5` introduces general nonlinear modeling to `InfiniteOpt`! - Thus, the possibilities for applications is vast. Please see - [Nonlinear Expressions](@ref nlp_guide) for more information. + `InfiniteOpt v0.6` introduces `JuMP`'s new general nonlinear modeling to `InfiniteOpt`! + Please see [Nonlinear Expressions](@ref nlp_guide) for more information. ## What is InfiniteOpt? `InfiniteOpt.jl` provides a general mathematical abstraction to express and solve diff --git a/docs/src/manual/expression.md b/docs/src/manual/expression.md index a8701a61a..48b56a803 100644 --- a/docs/src/manual/expression.md +++ b/docs/src/manual/expression.md @@ -39,27 +39,20 @@ JuMP.delete(::InfiniteModel, ::ParameterFunctionRef) ## [Nonlinear Expressions](@id nlp_manual) ### DataTypes ```@docs -NodeData -NLPExpr RegisteredFunction ``` ### Methods/Macros ```@docs -@register all_registered_functions name_to_function user_registered_functions -InfiniteOpt.ifelse -print_expression_tree(::NLPExpr) -JuMP.drop_zeros!(::NLPExpr) -map_nlp_to_ast add_registered_to_jump ``` ## Expression Methods ```@docs -parameter_refs(::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr}) +parameter_refs(::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr}) map_expression ``` diff --git a/test/runtests.jl b/test/runtests.jl index 2bda91805..b087412ef 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -34,7 +34,7 @@ println("----------------------------------------------------------------------- include("Collections/VectorTuple.jl") end println("") -@time @testset "Datatypes" begin include("datatypes.jl") end +# @time @testset "Datatypes" begin include("datatypes.jl") end println("") @time @testset "Utilities" begin include("utility_tests.jl") end println("") @@ -63,47 +63,47 @@ end println("") @time @testset "Derivative Methods" begin include("derivatives.jl") end println("") -@time @testset "Nonlinear" begin include("nlp.jl") end -println("") -@time @testset "Operators" begin include("operators.jl") end -println("") -@time @testset "Expression Methods" begin include("expressions.jl") end -println("") -@time @testset "Macro Expressions" begin include("macro_expressions.jl") end -println("") -@time @testset "Measure Methods" begin include("measures.jl") end -println("") -@time @testset "Measure Toolbox Methods" begin - @testset "Integrals" begin include("MeasureToolbox/integrals.jl") end - @testset "Expectations" begin include("MeasureToolbox/expectations.jl") end - @testset "Support Sums" begin include("MeasureToolbox/support_sums.jl") end -end -println("") -@time @testset "Objective Methods" begin include("objective.jl") end -println("") -@time @testset "Constraint Methods" begin include("constraints.jl") end -println("") -@time @testset "Printing Methods" begin include("show.jl") end -println("") -@time @testset "Deletion Methods" begin include("deletion.jl") end -println("") -@time @testset "Expansion Methods" begin include("measure_expansions.jl") end -println("") -@time @testset "Derivative Evaluation" begin include("derivative_evaluation.jl") end -println("") -@time @testset "TranscriptionOpt" begin - @testset "Model" begin include("TranscriptionOpt/model.jl") end - @testset "Measures" begin include("TranscriptionOpt/measure.jl") end - @testset "Transcribe" begin include("TranscriptionOpt/transcribe.jl") end - @testset "Optimize" begin include("TranscriptionOpt/optimize.jl") end -end -println("") -@time @testset "Solution Methods" begin include("optimizer.jl") end -println("") -@time @testset "Solution Queries" begin include("results.jl") end -println("") -@time @testset "Extensions" begin include("extensions.jl") end -println("") +# @time @testset "Nonlinear" begin include("nlp.jl") end +# println("") +# @time @testset "Operators" begin include("operators.jl") end +# println("") +# @time @testset "Expression Methods" begin include("expressions.jl") end +# println("") +# @time @testset "Macro Expressions" begin include("macro_expressions.jl") end +# println("") +# @time @testset "Measure Methods" begin include("measures.jl") end +# println("") +# @time @testset "Measure Toolbox Methods" begin +# @testset "Integrals" begin include("MeasureToolbox/integrals.jl") end +# @testset "Expectations" begin include("MeasureToolbox/expectations.jl") end +# @testset "Support Sums" begin include("MeasureToolbox/support_sums.jl") end +# end +# println("") +# @time @testset "Objective Methods" begin include("objective.jl") end +# println("") +# @time @testset "Constraint Methods" begin include("constraints.jl") end +# println("") +# @time @testset "Printing Methods" begin include("show.jl") end +# println("") +# @time @testset "Deletion Methods" begin include("deletion.jl") end +# println("") +# @time @testset "Expansion Methods" begin include("measure_expansions.jl") end +# println("") +# @time @testset "Derivative Evaluation" begin include("derivative_evaluation.jl") end +# println("") +# @time @testset "TranscriptionOpt" begin +# @testset "Model" begin include("TranscriptionOpt/model.jl") end +# @testset "Measures" begin include("TranscriptionOpt/measure.jl") end +# @testset "Transcribe" begin include("TranscriptionOpt/transcribe.jl") end +# @testset "Optimize" begin include("TranscriptionOpt/optimize.jl") end +# end +# println("") +# @time @testset "Solution Methods" begin include("optimizer.jl") end +# println("") +# @time @testset "Solution Queries" begin include("results.jl") end +# println("") +# @time @testset "Extensions" begin include("extensions.jl") end +# println("") println("----------------------------------------------------------------------------") println("-----------------------------TESTING COMPLETE!------------------------------") println("----------------------------------------------------------------------------") From 5b6c08c6cd5b19773aaea59b2857f3f0df8f23d7 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Wed, 3 May 2023 21:41:37 -0400 Subject: [PATCH 04/40] Update datatypes and tests --- src/datatypes.jl | 70 +++++- src/nlp.jl | 62 ----- test/datatypes.jl | 40 +++ test/nlp.jl | 608 ---------------------------------------------- test/runtests.jl | 3 +- 5 files changed, 110 insertions(+), 673 deletions(-) diff --git a/src/datatypes.jl b/src/datatypes.jl index d023e8646..107c5424d 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -1187,6 +1187,73 @@ mutable struct ConstraintData{C <: JuMP.AbstractConstraint} <: AbstractDataObjec is_info_constraint::Bool end +################################################################################ +# Function Registration +################################################################################ +""" + RegisteredFunction{F <: Function, G <: Union{Function, Nothing}, + H <: Union{Function, Nothing}} + +A type for storing used defined registered functions and their information that +is needed by JuMP for build an `NLPEvaluator`. The constructor is of the form: +```julia + RegisteredFunction(op::Symbol, dim::Int, f::Function, + [∇f::Function, ∇²f::Function]) +``` + +**Fields** +- `op::Symbol`: The name of the function that is used in `NLPExpr`s. +- `dim::Int`: The number of function arguments. +- `f::F`: The function itself. +- `∇f::G`: The gradient function if one is given. +- `∇²f::H`: The hessian function if one is given. +""" +struct RegisteredFunction{F <: Function, G, H} + op::Symbol + dim::Int + f::F + ∇f::G + ∇²f::H + + # Constructors + function RegisteredFunction( + op::Symbol, + dim::Int, + f::F + ) where {F <: Function} + return new{F, Nothing, Nothing}(op, dim, f, nothing, nothing) + end + function RegisteredFunction( + op::Symbol, + dim::Int, + f::F, + ∇f::G + ) where {F <: Function, G <: Function} + if isone(dim) && !hasmethod(∇f, Tuple{Real}) + error("Invalid gradient function form, see the docs for details.") + elseif !isone(dim) && !hasmethod(∇f, Tuple{AbstractVector{Real}, ntuple(_->Real, dim)...}) + error("Invalid multi-variate gradient function form, see the docs for details.") + end + return new{F, G, Nothing}(op, dim, f, ∇f, nothing) + end + function RegisteredFunction( + op::Symbol, + dim::Int, + f::F, + ∇f::G, + ∇²f::H + ) where {F <: Function, G <: Function, H <: Function} + if isone(dim) && !hasmethod(∇f, Tuple{Real}) + error("Invalid gradient function form, see the docs for details.") + elseif isone(dim) && !hasmethod(∇²f, Tuple{Real}) + error("Invalid hessian function form, see the docs for details.") + elseif !isone(dim) && !hasmethod(∇²f, Tuple{AbstractMatrix{Real}, ntuple(_->Real, dim)...}) + error("Invalid multi-variate hessian function form, see the docs for details.") + end + return new{F, G, H}(op, dim, f, ∇f, ∇²f) + end +end + ################################################################################ # INFINITE MODEL ################################################################################ @@ -1284,7 +1351,7 @@ mutable struct InfiniteModel <: JuMP.AbstractModel objective_has_measures::Bool # Function Registration - registrations::Vector{Any} + registrations::Vector{RegisteredFunction} func_lookup::Dict{Symbol, Tuple{Function, Int}} # Objects @@ -1466,6 +1533,7 @@ function Base.empty!(model::InfiniteModel)::InfiniteModel model.objective_has_measures = false # other stuff empty!(model.registrations) + empty!(model.func_lookup) empty!(model.obj_dict) empty!(model.optimizer_model) model.ready_to_optimize = false diff --git a/src/nlp.jl b/src/nlp.jl index 483065811..adb4092d4 100644 --- a/src/nlp.jl +++ b/src/nlp.jl @@ -8,68 +8,6 @@ const _NativeNLPFunctions = append!(copy(MOI.Nonlinear.DEFAULT_UNIVARIATE_OPERAT MOI.Nonlinear.DEFAULT_MULTIVARIATE_OPERATORS) append!(_NativeNLPFunctions, (:&&, :||, :<=, :(==), :>=, :<, :>)) -""" - RegisteredFunction{F <: Function, G <: Union{Function, Nothing}, - H <: Union{Function, Nothing}} - -A type for storing used defined registered functions and their information that -is needed by JuMP for build an `NLPEvaluator`. The constructor is of the form: -```julia - RegisteredFunction(op::Symbol, dim::Int, f::Function, - [∇f::Function, ∇²f::Function]) -``` - -**Fields** -- `op::Symbol`: The name of the function that is used in `NLPExpr`s. -- `dim::Int`: The number of function arguments. -- `f::F`: The function itself. -- `∇f::G`: The gradient function if one is given. -- `∇²f::H`: The hessian function if one is given. -""" -struct RegisteredFunction{F <: Function, G, H} - op::Symbol - dim::Int - f::F - ∇f::G - ∇²f::H - - # Constructors - function RegisteredFunction( - op::Symbol, - dim::Int, - f::F - ) where {F <: Function} - return new{F, Nothing, Nothing}(op, dim, f, nothing, nothing) - end - function RegisteredFunction( - op::Symbol, - dim::Int, - f::F, - ∇f::G - ) where {F <: Function, G <: Function} - if isone(dim) && !hasmethod(∇f, Tuple{Real}) - error("Invalid gradient function form, see the docs for details.") - elseif !isone(dim) && !hasmethod(∇f, Tuple{AbstractVector{Real}, ntuple(_->Real, dim)...}) - error("Invalid multi-variate gradient function form, see the docs for details.") - end - return new{F, G, Nothing}(op, dim, f, ∇f, nothing) - end - function RegisteredFunction( - op::Symbol, - dim::Int, - f::F, - ∇f::G, - ∇²f::H - ) where {F <: Function, G <: Function, H <: Function} - if isone(dim) && !hasmethod(∇f, Tuple{Real}) - error("Invalid gradient function form, see the docs for details.") - elseif isone(dim) && !hasmethod(∇²f, Tuple{Real}) - error("Invalid hessian function form, see the docs for details.") - end - return new{F, G, H}(op, dim, f, ∇f, ∇²f) - end -end - # TODO expand details in docstring """ JuMP.add_user_defined_function( diff --git a/test/datatypes.jl b/test/datatypes.jl index 022af7778..fd0a3e234 100644 --- a/test/datatypes.jl +++ b/test/datatypes.jl @@ -466,3 +466,43 @@ end @test ConstraintData <: AbstractDataObject @test ConstraintData(con, [1], "", MeasureIndex[], false) isa ConstraintData end + +# Test the function registration constructor +@testset "Registered Functions" begin + # Setup + f(a) = a^3 + g(a::Int) = 42 + h(a, b) = 42 + f1(a) = 32 + f2(a) = 10 + h1(a, b) = 13 + function hg(v::AbstractVector, a, b) + v[1] = 1 + v[2] = 2 + return + end + function ∇²h(H, x...) + H[1, 1] = 1200 * x[1]^2 - 400 * x[2] + 2 + H[2, 1] = -400 * x[1] + H[2, 2] = 200.0 + return + end + # Test errors + @test_throws ErrorException RegisteredFunction(:a, 1, f, g) + @test_throws ErrorException RegisteredFunction(:a, 2, f, g) + @test_throws ErrorException RegisteredFunction(:a, 1, f, g, f) + @test_throws ErrorException RegisteredFunction(:a, 1, f, f, g) + @test_throws ErrorException RegisteredFunction(:a, 2, h, hg, hg) + # Test regular builds + @test RegisteredFunction(:a, 1, f).op == :a + @test RegisteredFunction(:a, 1, f).dim == 1 + @test RegisteredFunction(:a, 1, f).f == f + @test RegisteredFunction(:a, 1, f, f) isa RegisteredFunction{typeof(f), typeof(f), Nothing} + @test RegisteredFunction(:a, 1, f, f).∇f == f + @test RegisteredFunction(:a, 1, f, f, f) isa RegisteredFunction{typeof(f), typeof(f), typeof(f)} + @test RegisteredFunction(:a, 1, f, f, f).∇²f == f + @test RegisteredFunction(:a, 2, h, hg) isa RegisteredFunction{typeof(h), typeof(hg), Nothing} + @test RegisteredFunction(:a, 2, h, hg).∇f == hg + @test RegisteredFunction(:a, 2, h, hg, ∇²h) isa RegisteredFunction{typeof(h), typeof(hg), typeof(∇²h)} + @test RegisteredFunction(:a, 2, h, hg, ∇²h).∇²f == ∇²h +end diff --git a/test/nlp.jl b/test/nlp.jl index 8a99600bd..a17beff3e 100644 --- a/test/nlp.jl +++ b/test/nlp.jl @@ -1,474 +1,3 @@ -# Test the extensions to LCRST -@testset "LCRST Extensions" begin - # test additions to addchild - @testset "addchild(parent, child)" begin - # setup nodes - p = Node(1) - c1 = Node(2) - c2 = Node(3) - # test first addition of first child - @test addchild(p, c1) == c1 - @test c1.parent == p - @test lastsibling(c1) == c1 - @test c1.child == c1 - @test p.child == c1 - # test first addition of 2nd child - @test addchild(p, c2) == c2 - @test c1.parent == p - @test c2.parent == p - @test c1.sibling == c2 - @test c2.sibling == c2 - @test c2.child == c2 - @test c1.child == c1 - @test p.child == c1 - # test subsequent addition of first child (invokes copy) - @test addchild(p, c1) !== c1 - @test c1.parent == p - @test lastsibling(c1) !== c2 && lastsibling(c1) !== c1 - @test c1.child == c1 - @test p.child == c1 - c3 = lastsibling(c1) - @test c3.parent == p - @test c2.sibling == c3 - @test c3.child == c3 - @test c3 !== c1 - @test c3.data == 2 - # test subsequent addition of child to a node with no children - p2 = Node(4) - @test addchild(p2, c1) !== c1 - c4 = p2.child - @test c4.parent == p2 - @test c4.data == 2 - @test c4.sibling == c4 - @test c4.child == c4 - end - @testset "addchild(parent, nothing, child)" begin - # setup nodes - p = Node(1) - c = Node(2) - # test first addition of first child - @test addchild(p, nothing, c) == c - @test c.parent == p - @test lastsibling(c) == c - @test c.child == c - @test p.child == c - # test subsequent addition of child to a node with no children - p2 = Node(4) - @test addchild(p2, nothing, c) !== c - c2 = p2.child - @test c2.parent == p2 - @test c2.data == 2 - @test c2.sibling == c2 - @test c2.child == c2 - end - @testset "addchild(parent, prevchild, data)" begin - # setup nodes - p = Node(1) - c1 = Node(2) - p.child = c1 - c1.parent = p - # test addition on first next sibling - @test addchild(p, c1, 3) isa Node - c2 = c1.sibling - @test c1 !== c2 - @test c2.data == 3 - @test c2.parent == p - @test c2.sibling == c2 - @test c2.child == c2 - @test c1.sibling == c2 - @test c1.parent == p - @test c1.child == c1 - # test adding more in for loop - prev = c2 - for i in 4:5 - prev = addchild(p, prev, i) - end - @test [n.data for n in p] == [2, 3, 4, 5] - c3 = c2.sibling - c4 = c3.sibling - @test c2 !== c3 && c3 !== c4 - @test c3.parent == p - @test c3.sibling == c4 - @test c3.child == c3 - @test c4.parent == p - @test c4.sibling == c4 - end - @testset "addchild(parent, prevchild, child)" begin - # setup nodes - p = Node(1) - c1 = Node(2) - p.child = c1 - c1.parent = p - c2 = Node(3) - # test invalid previous - @test_throws AssertionError addchild(p, c2, c1) - # test first addition of 2nd child - @test addchild(p, c1, c2) == c2 - @test c1.parent == p - @test c2.parent == p - @test c1.sibling == c2 - @test c2.sibling == c2 - @test c2.child == c2 - @test c1.child == c1 - @test p.child == c1 - # test subsequent addition of first child (invokes copy) - @test addchild(p, c2, c1) !== c1 - @test c1.parent == p - @test lastsibling(c1) !== c2 && lastsibling(c1) !== c1 - @test c1.child == c1 - @test p.child == c1 - c3 = lastsibling(c1) - @test c3.parent == p - @test c2.sibling == c3 - @test c3.child == c3 - @test c3 !== c1 - @test c3.data == 2 - # test subsequent double addition of a child to a root - p = Node(1) - c1 = Node(2) - p.child = c1 - c1.parent = p - @test addchild(p, c1, c1) !== c1 - @test [n.data for n in p] == [2, 2] - c2 = c1.sibling - @test c1 !== c2 - @test c1.parent == p - @test c2.parent == p - @test c1.sibling == c2 - @test c2.sibling == c2 - @test c2.child == c2 - @test c1.child == c1 - @test p.child == c1 - # test for loop addition of children - p = Node(1) - cs = [Node(2), Node(3), Node(4)] - prev = nothing - for i in 1:3 - prev = addchild(p, prev, cs[i]) - end - @test p.child == cs[1] - for i in 1:3 - @test cs[i].data == i + 1 - @test cs[i].parent == p - @test lastsibling(cs[i]) == cs[3] - end - end - # test _map_tree - @testset "_map_tree" begin - # setup tree - p = Node(1) - c1 = addchild(p, 2) - c2 = addchild(p, 3) - c3 = addchild(c2, 4) - # test simple map - str_p = InfiniteOpt._map_tree(n -> Node(string(n.data)), p) - @test isroot(str_p) - @test str_p.data == "1" - @test str_p.child.data == "2" - @test isleaf(str_p.child) - @test str_p.sibling == str_p - @test str_p.child.sibling.data == "3" - @test islastsibling(str_p.child.sibling) - @test isleaf(str_p.child.sibling.child) - @test str_p.child.sibling.child.data == "4" - # test empty root - p = Node(1) - str_p = InfiniteOpt._map_tree(n -> Node(string(n.data)), p) - @test isroot(str_p) - @test isleaf(str_p) - @test str_p.data == "1" - end - # test copy(Node) - @testset "Base.copy" begin - # setup tree - p = Node(1) - c1 = addchild(p, 2) - c2 = addchild(p, 3) - c3 = addchild(c2, 4) - # test simple copy - p2 = copy(p) - @test p2 !== p - @test isroot(p2) - @test p2.data == 1 - @test p2.child.data == 2 - @test isleaf(p2.child) - @test p2.child !== p.child - @test p2.sibling == p2 - @test p2.child.sibling.data == 3 - @test islastsibling(p2.child.sibling) - @test isleaf(p2.child.sibling.child) - @test p2.child.sibling.child.data == 4 - # test empty root - p = Node(1) - p2 = copy(p) - @test p !== p2 - @test isroot(p2) - @test isleaf(p2) - @test p2.data == 1 - end - # test _merge_parent_and_child - @testset "_merge_parent_and_child" begin - # setup tree - p = Node(1) - c1 = addchild(p, 2) - c2 = addchild(c1, 3) - c3 = addchild(c2, 4) - c4 = addchild(c2, 5) - # test normal - @test InfiniteOpt._merge_parent_and_child(c1) == c1 - @test c1.data == 3 - @test c1.child == c3 - @test c3.parent == c1 - @test c4.parent == c1 - @test c1.parent == p - # test at root node - @test InfiniteOpt._merge_parent_and_child(p) == p - @test p.data == 3 - @test p.child == c3 - @test c3.parent == p - @test c4.parent == p - @test p.parent == p - # test nothing happens - @test InfiniteOpt._merge_parent_and_child(p) == p - @test p.data == 3 - @test p.child == c3 - @test c3.parent == p - @test c4.parent == p - @test p.parent == p - end -end - -# Test the core NLP data structures and methods -@testset "Core Data Structures" begin - # setup model data - m = InfiniteModel() - @infinite_parameter(m, t in [0, 1]) - @variable(m, y, Infinite(t)) - @variable(m, z) - # test NodeData - @testset "NodeData" begin - @test NodeData(1).value == 1 - end - # test _node_value - @testset "_node_value" begin - @test InfiniteOpt._node_value(NodeData(1)) == 1 - end - # test _is_zero - @testset "_is_zero" begin - # test easy case - @test InfiniteOpt._is_zero(Node(NodeData(0))) - # test is leaf that isn't zero - @test !InfiniteOpt._is_zero(Node(NodeData(1))) - @test !InfiniteOpt._is_zero(Node(NodeData(y))) - # test addition block - p = Node(NodeData(:+)) - addchild(p, NodeData(y)) - addchild(p, NodeData(0)) - @test !InfiniteOpt._is_zero(p) - p.child.data = NodeData(0) - @test InfiniteOpt._is_zero(p) - # test multiplication block - p = Node(NodeData(:*)) - addchild(p, NodeData(y)) - addchild(p, NodeData(0)) - @test InfiniteOpt._is_zero(p) - p.child.sibling.data = NodeData(t) - @test !InfiniteOpt._is_zero(p) - # test power/divide - p = Node(NodeData(:^)) - addchild(p, NodeData(y)) - addchild(p, NodeData(2)) - @test !InfiniteOpt._is_zero(p) - p.child.data = NodeData(0) - @test InfiniteOpt._is_zero(p) - # test function - p = Node(NodeData(:abs)) - addchild(p, NodeData(0)) - @test InfiniteOpt._is_zero(p) - p.child.data = NodeData(y) - @test !InfiniteOpt._is_zero(p) - end - # test _drop_zeros! - @testset "_drop_zeros!" begin - # test simple case - n = Node(NodeData(0)) - @test InfiniteOpt._drop_zeros!(n) == n - @test n.data.value == 0 - # test addition with zeros - p = Node(NodeData(:+)) - addchild(p, NodeData(0)) - addchild(p, NodeData(y)) - @test InfiniteOpt._drop_zeros!(p) == p - @test isequal(p.data.value, y) - # test case with subtraction and zero function - p = Node(NodeData(:-)) - n1 = addchild(p, NodeData(:sin)) - n2 = addchild(p, NodeData(y)) - n3 = addchild(n1, NodeData(0)) - @test InfiniteOpt._drop_zeros!(p) == p - @test isequal(p.child.data.value, y) - # test case with substraction of zero - p = Node(NodeData(:-)) - n1 = addchild(p, NodeData(:sin)) - n2 = addchild(p, NodeData(0)) - n3 = addchild(n1, NodeData(y)) - @test InfiniteOpt._drop_zeros!(p) == p - @test p.data.value == :sin - @test isequal(p.child.data.value, y) - # test truncating leaf - p = Node(NodeData(:^)) - n1 = addchild(p, NodeData(y)) - n2 = addchild(p, NodeData(:*)) - n3 = addchild(n2, NodeData(0)) - n4 = addchild(n2, NodeData(t)) - @test InfiniteOpt._drop_zeros!(p) == p - @test p.child.sibling.data.value == 0 - end - # test isequal for Nodes - @testset "Base.isequal (Nodes)" begin - # test simple case - n1 = Node(NodeData(1)) - n2 = Node(NodeData(2)) - @test !isequal(n1, n2) - n2.data = NodeData(1) - @test isequal(n1, n2) - # test unequal count case - n1 = Node(NodeData(:+)) - addchild(n1, NodeData(1)) - n2 = Node(NodeData(:+)) - addchild(n2, NodeData(1)) - addchild(n2, NodeData(1)) - @test !isequal(n1, n2) - # test complicated isequal case - n1 = Node(NodeData(:+)) - addchild(n1, NodeData(1)) - addchild(n1, NodeData(y)) - n2 = Node(NodeData(:+)) - addchild(n2, NodeData(1)) - addchild(n2, NodeData(y)) - @test isequal(n1, n2) - # test more complicated not equal case - n2 = Node(NodeData(:+)) - addchild(n2, NodeData(1)) - addchild(n2, NodeData(t)) - @test !isequal(n1, n2) - end - # test NLPExpr - @testset "NLPExpr" begin - @test NLPExpr(Node(NodeData(0))).tree_root.data.value == 0 - end - # test Base basics - @testset "Base Basics (NLPExpr)" begin - # setup expr - p = Node(NodeData(:+)) - addchild(p, NodeData(y)) - addchild(p, NodeData(2)) - nlp = NLPExpr(p) - # test broadcastable - @test Base.broadcastable(nlp) isa Base.RefValue - # test copy - @test copy(nlp) !== nlp - @test isequal(copy(nlp), nlp) - # test zero and one - @test zero(NLPExpr).tree_root.data.value == 0 - @test one(NLPExpr).tree_root.data.value == 1 - # test isequal - p = Node(NodeData(:+)) - addchild(p, NodeData(y)) - addchild(p, NodeData(3)) - nlp2 = NLPExpr(p) - @test !isequal(nlp, nlp2) - p.child.sibling.data = NodeData(2) - @test isequal(nlp, nlp2) - end - # test drop_zeros! - @testset "drop_zeros!" begin - # setup expr - p = Node(NodeData(:^)) - n1 = addchild(p, NodeData(y)) - n2 = addchild(p, NodeData(:*)) - n3 = addchild(n2, NodeData(0)) - n4 = addchild(n2, NodeData(t)) - nlp = NLPExpr(p) - @test drop_zeros!(nlp) === nlp - @test nlp.tree_root.child.sibling.data.value == 0 - end - # test isequal_canonical - @testset "isequal_canonical" begin - # test that they are equal - p = Node(NodeData(:^)) - n1 = addchild(p, NodeData(y)) - n2 = addchild(p, NodeData(:*)) - n3 = addchild(n2, NodeData(0)) - n4 = addchild(n2, NodeData(t)) - nlp1 = NLPExpr(p) - p = Node(NodeData(:^)) - n1 = addchild(p, NodeData(y)) - n2 = addchild(p, NodeData(0)) - nlp2 = NLPExpr(p) - @test isequal_canonical(nlp1, nlp2) - # test that they are not equal - p = Node(NodeData(:^)) - n1 = addchild(p, NodeData(y)) - n2 = addchild(p, NodeData(t)) - nlp3 = NLPExpr(p) - @test !isequal_canonical(nlp1, nlp3) - @test !isequal_canonical(nlp2, nlp3) - end - # test tree printing - @testset "Tree Printing" begin - # setup - p = Node(NodeData(:^)) - n1 = addchild(p, NodeData(y)) - n2 = addchild(p, NodeData(2)) - nlp = NLPExpr(p) - # test with NLPExpre - expected = "^\n├─ y(t)\n└─ 2\n" - io_test(print_expression_tree, expected, nlp) - # test with other - io_test(print_expression_tree, "y(t)\n", y) - # test again - test_output = @capture_out print_expression_tree(nlp) - @test test_output == expected - end - # test ast mapping - @testset "ast mapping" begin - @variable(Model(), x) - # map tree of vars and constants - p = Node(NodeData(:^)) - n1 = addchild(p, NodeData(y)) - n2 = addchild(p, NodeData(:*)) - addchild(n2, NodeData(t)) - addchild(n2, NodeData(0.2)) - nlp = NLPExpr(p) - @test map_nlp_to_ast(v -> x, nlp) == :($x ^ ($x * 0.2)) - # map tree with affine expression - p = Node(NodeData(:^)) - n1 = addchild(p, NodeData(2y + t - 42)) - n2 = addchild(p, NodeData(:*)) - addchild(n2, NodeData(t)) - addchild(n2, NodeData(0.2)) - nlp = NLPExpr(p) - @test map_nlp_to_ast(v -> x, nlp) == :((2 * $x + $x + -42) ^ ($x * 0.2)) - # map tree with quadratic expression (no affine) - p = Node(NodeData(:^)) - n1 = addchild(p, NodeData(2y^2 + t^2)) - n2 = addchild(p, NodeData(:*)) - addchild(n2, NodeData(t)) - addchild(n2, NodeData(0.2)) - nlp = NLPExpr(p) - @test map_nlp_to_ast(v -> x, nlp) == :((2 * $x * $x + $x * $x) ^ ($x * 0.2)) - # map tree with quadratic expression (w/ affine) - p = Node(NodeData(:^)) - n1 = addchild(p, NodeData(2y^2 + t^2 - 42)) - n2 = addchild(p, NodeData(:*)) - addchild(n2, NodeData(t)) - addchild(n2, NodeData(0.2)) - nlp = NLPExpr(p) - @test map_nlp_to_ast(v -> x, nlp) == :((2 * $x * $x + $x * $x + -42) ^ ($x * 0.2)) - end -end - # Test the basic expression generation via operators @testset "Operator Definition" begin # setup model data @@ -476,42 +5,6 @@ end @infinite_parameter(m, t in [0, 1]) @variable(m, y, Infinite(t)) @variable(m, z) - # test _process_child_input - @testset "_process_child_input" begin - # nlp - p = Node(NodeData(:sin)) - addchild(p, NodeData(y)) - nlp = NLPExpr(p) - @test InfiniteOpt._process_child_input(nlp) == p - # expressions - @test InfiniteOpt._process_child_input(y) == NodeData(y) - @test isequal_canonical(InfiniteOpt._process_child_input(2y).value, 2y) - @test isequal_canonical(InfiniteOpt._process_child_input(y^2).value, y^2) - # function symbol - @test InfiniteOpt._process_child_input(:^) == NodeData(:^) - # constant - @test InfiniteOpt._process_child_input(true) == NodeData(true) - @test InfiniteOpt._process_child_input(3) == NodeData(3) - # fallback - @test_throws ErrorException InfiniteOpt._process_child_input("bad") - end - # test the basic graph builder - @testset "_call_graph" begin - # test simple - p = Node(NodeData(:*)) - addchild(p, NodeData(y)) - addchild(p, NodeData(t)) - addchild(p, NodeData(1)) - @test isequal(InfiniteOpt._call_graph(:*, y, t, 1), p) - # test with other expression - p = Node(NodeData(:sin)) - addchild(p, NodeData(y)) - nlp = NLPExpr(p) - p = Node(NodeData(:^)) - addchild(p, nlp.tree_root) - addchild(p, NodeData(y^2)) - @test isequal(InfiniteOpt._call_graph(:^, nlp, y^2), p) - end # test the sum @testset "sum" begin # test empty sums @@ -729,107 +222,6 @@ end end end -# Test the MutableArithmetics stuff -@testset "MutableArithmetics" begin - # setup model data - m = InfiniteModel() - @infinite_parameter(m, t in [0, 1]) - @variable(m, y, Infinite(t)) - @variable(m, z) - aff = 2z - 2 - quad = y^2 + y - nlp = sin(y) - # test MA.mutability - @testset "MA.mutability" begin - @test MA.mutability(NLPExpr) == MA.IsMutable() - end - # test MA.promote_operation - @testset "MA.promote_operation" begin - for i in (2.0, 2, y, aff, quad, nlp) - for f in (+, -, *, /, ^) - @test MA.promote_operation(f, typeof(i), NLPExpr) == NLPExpr - @test MA.promote_operation(f, NLPExpr, typeof(i)) == NLPExpr - end - end - for i in (y, aff, quad) - for f in (*, /, ^) - @test MA.promote_operation(f, typeof(i), typeof(quad)) == NLPExpr - @test MA.promote_operation(f, typeof(quad), typeof(i)) == NLPExpr - end - end - for i in (2, 2.0, y, aff, quad, nlp) - for j in (y, aff, quad, nlp) - for f in (/, ^) - @test MA.promote_operation(f, typeof(i), typeof(j)) == NLPExpr - end - end - end - end - # test MA.scaling - @testset "MA.scaling" begin - @test_throws ErrorException MA.scaling(nlp) - @test MA.scaling(one(NLPExpr)) == 1 - end - # test mutable_copy - @testset "MA.mutable_copy" begin - @test MA.mutable_copy(nlp) === nlp - end - # test operate! - @testset "MA.operate!" begin - # test zero and one - @test MA.operate!(zero, nlp) !== nlp - @test isequal(MA.operate!(zero, nlp), zero(NLPExpr)) - @test MA.operate!(one, nlp) !== nlp - @test isequal(MA.operate!(one, nlp), one(NLPExpr)) - # test operators - for i in (2.0, 2, y, aff, quad, nlp) - for f in (+, -, *, /, ^) - @test isequal(MA.operate!(f, nlp, i), f(nlp, i)) - @test isequal(MA.operate!(f, i, nlp), f(i, nlp)) - end - end - # test AddSubMul - @test isequal(MA.operate!(MA.add_mul, nlp, y, 2), nlp + y * 2) - @test isequal(MA.operate!(MA.sub_mul, nlp, y, 2), nlp - y * 2) - end -end - -# Test LinearAlgebra stuff -@testset "Linear Algebra" begin - # setup the model data - m = InfiniteModel() - @infinite_parameter(m, t in [0, 1]) - @variable(m, y, Infinite(t)) - @variable(m, z) - aff = 2z - 2 - quad = y^2 + y - nlp = sin(y) - # test promotions - @testset "Base.promote_rule" begin - for i in (3, y, aff, quad) - @test [nlp, i] isa Vector{NLPExpr} - end - # extra tests - @test promote_rule(NLPExpr, typeof(quad)) == NLPExpr - end - # test dot - @testset "LinearAlgebra.dot" begin - for i in (2, y, aff, quad, nlp) - for j in (2, y, aff, quad, nlp) - @test isequal(dot(i, j), i * j) - end - end - end - # test linear algebra operations - @testset "Operations" begin - # make variables - @variable(m, A[1:2, 1:3]) - @variable(m, x[1:2]) - @variable(m, w[1:3]) - # test expression - @test x' * A * w isa NLPExpr # TODO fix with improved MA extension - end -end # Test registration utilities @testset "Registration Methods" begin diff --git a/test/runtests.jl b/test/runtests.jl index b087412ef..78a27a135 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,7 +19,6 @@ const JuMPC = JuMP.Containers const MOIUC = MOIU.CleverDicts const FGQ = FastGaussQuadrature const IOMT = InfiniteOpt.MeasureToolbox -const LCRST = LeftChildRightSiblingTrees const MA = MutableArithmetics # Load in testing utilities @@ -34,7 +33,7 @@ println("----------------------------------------------------------------------- include("Collections/VectorTuple.jl") end println("") -# @time @testset "Datatypes" begin include("datatypes.jl") end +@time @testset "Datatypes" begin include("datatypes.jl") end println("") @time @testset "Utilities" begin include("utility_tests.jl") end println("") From 23f661fe26c77faffc0283cd0548137b211cfca2 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Wed, 3 May 2023 22:37:25 -0400 Subject: [PATCH 05/40] Add registration tests --- src/nlp.jl | 12 +- test/nlp.jl | 440 +++++++--------------------------------------- test/operators.jl | 225 ++++++++++++++++++++++++ test/runtests.jl | 2 +- 4 files changed, 289 insertions(+), 390 deletions(-) diff --git a/src/nlp.jl b/src/nlp.jl index adb4092d4..828d39134 100644 --- a/src/nlp.jl +++ b/src/nlp.jl @@ -34,21 +34,17 @@ function JuMP.add_user_defined_function( dim::Int, funcs... ) - if !all(f -> f isa Function, funcs) + if isempty(funcs) + error("Tried to register `$op`, but no evaluation function was given.") + elseif !all(f -> f isa Function, funcs) error("The gradient and/or hessian must be functions, but got argument(s) " * "of type `" * join(Tuple(typeof(f) for f in funcs if !(f isa Function)), "`, `") * "`.") - elseif isempty(funcs) - error("Tried to register `$op`, but no evaluation function was given.") elseif op in _NativeNLPFunctions || op in keys(model.func_lookup) error("A function with name `$op` arguments is already " * "registered. Please use a function with a different name.") elseif !hasmethod(funcs[1], NTuple{dim, Real}) error("The function `$op` is not defined for arguments of type `Real`.") - elseif length(unique!([m.module for m in methods(funcs[1])])) > 1 || - first(methods(funcs[1])).module !== call_mod - error("Cannot register function names that are used by packages. Try " * - "wrapping `$(funcs[1])` in a user-defined function.") end push!(model.registrations, RegisteredFunction(op, dim, funcs...)) model.func_lookup[op] = (funcs[1], dim) @@ -76,7 +72,7 @@ end Retrieve all the functions that are currently registered to `model`. """ function all_registered_functions(model::InfiniteModel) - return append!(copy(_NativeNLPFunctions), map(first, values(model.func_lookup))) + return append!(copy(_NativeNLPFunctions), map(v -> Symbol(first(v)), values(model.func_lookup))) end """ diff --git a/test/nlp.jl b/test/nlp.jl index a17beff3e..ce8d49da9 100644 --- a/test/nlp.jl +++ b/test/nlp.jl @@ -1,247 +1,8 @@ -# Test the basic expression generation via operators -@testset "Operator Definition" begin - # setup model data - m = InfiniteModel() - @infinite_parameter(m, t in [0, 1]) - @variable(m, y, Infinite(t)) - @variable(m, z) - # test the sum - @testset "sum" begin - # test empty sums - @test sum(i for i in Int[]) == 0 - @test isequal(sum(NLPExpr[]), zero(NLPExpr)) - @test isequal(sum(GeneralVariableRef[]), - zero(GenericAffExpr{Float64, GeneralVariableRef})) - # test NLP sums - p = Node(NodeData(:sin)) - addchild(p, NodeData(y)) - nlp = NLPExpr(p) - new = NLPExpr(InfiniteOpt._call_graph(:+, nlp, nlp)) - @test isequal(sum(i for i in [nlp, nlp]), new) - @test isequal(sum([nlp, nlp]), new) - @test_throws ErrorException sum(i for i in [nlp, nlp]; bad = 42) - @test_throws ErrorException sum(i for i in [y, t]; bad = 42) - # test other expressions - @test isequal(sum(i for i in [y, t]), y + t) - @test isequal(sum([y, t]), y + t) - # normal sums - @test sum(i for i in 1:3) == 6 - end - # test the product - @testset "prod" begin - # test empty sums - @test prod(i for i in Int[]) == 1 - @test isequal(prod(NLPExpr[]), one(NLPExpr)) - # test NLP sums - p = Node(NodeData(:sin)) - addchild(p, NodeData(y)) - nlp = NLPExpr(p) - new = NLPExpr(InfiniteOpt._call_graph(:*, nlp, nlp)) - @test isequal(prod(i for i in [nlp, nlp]), new) - @test isequal(prod([nlp, nlp]), new) - @test_throws ErrorException prod(i for i in [nlp, nlp]; bad = 42) - @test_throws ErrorException prod(i for i in [y, t, z]; bad = 42) - # test other expressions - new = NLPExpr(InfiniteOpt._call_graph(:*, y, t, z)) - @test isequal(prod(i for i in [y, t, z]), new) - @test isequal(prod([y, t, z]), new) - # test normal products - @test prod(i for i in 1:3) == 6 - end - # prepare the test grid - aff = 2z - 2 - quad = y^2 + y - p = Node(NodeData(:sin)) - addchild(p, NodeData(y)) - nlp = NLPExpr(p) - # test the multiplication operator - @testset "Multiplication Operator" begin - for (i, iorder) in [(42, 0), (y, 1), (aff, 1), (quad, 2), (nlp, 3)] - for (j, jorder) in [(42, 0), (y, 1), (aff, 1), (quad, 2), (nlp, 3)] - if iorder + jorder >= 3 - expected = NLPExpr(InfiniteOpt._call_graph(:*, i, j)) - @test isequal(i * j, expected) - end - end - end - expected = NLPExpr(InfiniteOpt._call_graph(:*, y, t, z,nlp)) - @test isequal(y * t * z * nlp, expected) - @test isequal(*(nlp), nlp) - end - # test division operator - @testset "Division Operator" begin - for i in [42, y, aff, quad, nlp] - for j in [y, aff, quad, nlp] - expected = NLPExpr(InfiniteOpt._call_graph(:/, i, j)) - @test isequal(i / j, expected) - end - end - expected = NLPExpr(InfiniteOpt._call_graph(:/, nlp, 42)) - @test isequal(nlp / 42, expected) - @test isequal(nlp / 1, nlp) - @test_throws ErrorException nlp / 0 - end - # test the power operator - @testset "Power Operator" begin - for i in [42, y, aff, quad, nlp] - for j in [y, aff, quad, nlp] - expected = NLPExpr(InfiniteOpt._call_graph(:^, i, j)) - @test isequal(i ^ j, expected) - end - end - one_aff = one(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) - for i in [y, aff, quad, nlp] - for f in [Float64, Int] - @test isequal(i^zero(f), one_aff) - @test isequal(i^one(f), i) - if i isa NLPExpr - expected = NLPExpr(InfiniteOpt._call_graph(:^, i, f(2))) - @test isequal(i ^ f(2), expected) - else - @test isequal(i^f(2), i * i) - end - expected = NLPExpr(InfiniteOpt._call_graph(:^, i, f(3))) - @test isequal(i ^ f(3), expected) - end - end - # extra tests - @test isequal(y^0, one_aff) - @test isequal(y^1, y) - @test isequal(y^2, y * y) - @test isequal(y^0.0, one_aff) - @test isequal(y^1.0, y) - @test isequal(y^2.0, y * y) - end - # test the subtraction operator - @testset "Subtraction Operator" begin - for i in [42, y, aff, quad, nlp] - expected = NLPExpr(InfiniteOpt._call_graph(:-, nlp, i)) - @test isequal(nlp - i, expected) - expected = NLPExpr(InfiniteOpt._call_graph(:-, i, nlp)) - @test isequal(i - nlp, expected) - end - expected = NLPExpr(InfiniteOpt._call_graph(:-, nlp)) - @test isequal(-nlp, expected) - @test isequal(y - y, zero(JuMP.GenericAffExpr{Float64, GeneralVariableRef})) - @test (y - z).constant == 0 - @test (y - z).terms[y] == 1 - @test (y - z).terms[z] == -1 - end - # test the addition operator - @testset "Addition Operator" begin - for i in [42, y, aff, quad, nlp] - expected = NLPExpr(InfiniteOpt._call_graph(:+, nlp, i)) - @test isequal(nlp + i, expected) - expected = NLPExpr(InfiniteOpt._call_graph(:+, i, nlp)) - @test isequal(i + nlp, expected) - end - @test isequal(+nlp, nlp) - end - # test the comparison operators - @testset "Comparison Operators" begin - for i in [42, y, aff, quad, nlp] - for j in [42, y, aff, quad, nlp] - for (n, f) in (:< => Base.:(<), :(==) => Base.:(==), - :> => Base.:(>), :<= => Base.:(<=), - :>= => Base.:(>=)) - if !(isequal(i, 42) && isequal(j, 42)) && !(isequal(i, y) && isequal(j, y)) - expected = NLPExpr(InfiniteOpt._call_graph(n, i, j)) - @test isequal(f(i, j), expected) - end - end - end - end - for (n, f) in (:(==) => Base.:(==), :<= => Base.:(<=), :>= => Base.:(>=)) - expected = NLPExpr(InfiniteOpt._call_graph(n, y, z)) - @test isequal(f(y, z), expected) - @test f(y, y) - end - for (n, f) in (:< => Base.:(<), :> => Base.:(>)) - expected = NLPExpr(InfiniteOpt._call_graph(n, y, z)) - @test isequal(f(y, z), expected) - @test !f(y, y) - end - # extra tests - @test (y == 0) isa NLPExpr - @test (y <= 0) isa NLPExpr - @test (y >= 0) isa NLPExpr - @test (y > 0) isa NLPExpr - @test (y < 0) isa NLPExpr - @test (0 == y) isa NLPExpr - @test (0 <= y) isa NLPExpr - @test (0 >= y) isa NLPExpr - @test (0 > y) isa NLPExpr - @test (0 < y) isa NLPExpr - end - # test the logic operators - @testset "Logic Operators" begin - for i in [y, nlp] - for j in [y, nlp] - for (n, f) in (:&& => Base.:&, :|| => Base.:|) - expected = NLPExpr(InfiniteOpt._call_graph(n, i, j)) - @test isequal(f(i, j), expected) - end - end - end - for i in [y, nlp] - @test !(i & false) - @test !(false & i) - @test isequal(i & true, i) - @test isequal(true & i, i) - @test (i | true) - @test (true | i) - @test isequal(i | false, i) - @test isequal(false | i, i) - end - end - # test ifelse - @testset "ifelse" begin - for i in [42, y, aff, quad, nlp] - for j in [42, y, aff, quad, nlp] - expected = NLPExpr(InfiniteOpt._call_graph(:ifelse, nlp, i, j)) - @test isequal(InfiniteOpt.ifelse(nlp, i, j), expected) - end - end - @test isequal(InfiniteOpt.ifelse(true, y, z), y) - @test isequal(InfiniteOpt.ifelse(false, y, z), z) - end - # test the default functions - @testset "Default Registered Functions" begin - for i in [y, aff, quad, nlp] - for (n, f) in InfiniteOpt._Base1ArgFuncList - expected = NLPExpr(InfiniteOpt._call_graph(n, i)) - @test isequal(f(i), expected) - end - end - for i in [y, aff, quad, nlp] - for (n, f) in InfiniteOpt._Special1ArgFuncList - expected = NLPExpr(InfiniteOpt._call_graph(n, i)) - @test isequal(f(i), expected) - end - end - end -end - - # Test registration utilities @testset "Registration Methods" begin # setup model data m = InfiniteModel() @variable(m, y) - # test name_to_function - @testset "name_to_function" begin - @test name_to_function(m, :tan, 1) == tan - @test name_to_function(m, :+, 10) == + - @test name_to_function(m, :*, 2) == * - end - # test all_registered_functions - @testset "all_registered_functions" begin - @test all_registered_functions(m) isa Vector{Function} - end - # test user_registered_functions - @testset "user_registered_functions" begin - @test user_registered_functions(m) == RegisteredFunction[] - end # define functions for tests f(a) = a^3 g(a::Int) = 42 @@ -254,113 +15,67 @@ end v[2] = 2 return end - # test creation helper errors - @testset "Registration Helpers" begin - @test_throws ErrorException RegisteredFunction(:a, 1, f, g) - @test_throws ErrorException RegisteredFunction(:a, 2, f, g) - @test_throws ErrorException RegisteredFunction(:a, 1, f, g, f) - @test_throws ErrorException RegisteredFunction(:a, 1, f, f, g) - @test_throws ErrorException InfiniteOpt._register(error, Main, m, :f, 1, 1) - @test_throws ErrorException InfiniteOpt._register(error, Main, m, :sin, 1, sin) - @test_throws ErrorException InfiniteOpt._register(error, Main, m, :g, 1, g) - @test_throws ErrorException InfiniteOpt._register(error, Main, m, :eta, 1, eta) + function ∇²h(H, x...) + H[1, 1] = 1200 * x[1]^2 - 400 * x[2] + 2 + H[2, 1] = -400 * x[1] + H[2, 2] = 200.0 + return + end + # test JuMP.add_user_defined_function + @testset "JuMP.add_user_defined_function" begin + # test errors + @test_throws ErrorException add_user_defined_function(m, :f, 1) + @test_throws ErrorException add_user_defined_function(m, :f, 1, f, 2) + @test_throws ErrorException add_user_defined_function(m, :max, 1, f) + m.func_lookup[:f] = (f, 1) + @test_throws ErrorException add_user_defined_function(m, :f, 1, f) + empty!(m.func_lookup) + @test_throws ErrorException add_user_defined_function(m, :f, 2, f) + # test normal + @test add_user_defined_function(m, :f, 1, f).head == :f + @test m.func_lookup[:f] == (f, 1) + @test last(m.registrations).f == f + @test last(m.registrations).dim == 1 + @test last(m.registrations).op == :f + @test add_user_defined_function(m, :h, 2, h, hg).head == :h + @test m.func_lookup[:h] == (h, 2) + @test last(m.registrations).f == h + @test last(m.registrations).dim == 2 + @test last(m.registrations).op == :h + @test last(m.registrations).∇f == hg + end + # test name_to_function + @testset "name_to_function" begin + @test name_to_function(m, :f) == f + @test name_to_function(m, :h) == h + @test name_to_function(m, :bad) isa Nothing + end + # test all_registered_functions + @testset "all_registered_functions" begin + @test all_registered_functions(m) isa Vector{Symbol} end + # test user_registered_functions + @testset "user_registered_functions" begin + @test user_registered_functions(m) isa Vector{RegisteredFunction} + end + empty!(m.registrations) + empty!(m.func_lookup) # test @register @testset "@register" begin # test errors - @test_macro_throws ErrorException @register(Model(), f(a)) - @test_macro_throws ErrorException @register(m, f(a), bad = 42) - @test_macro_throws ErrorException @register(m, f(a), g, h, 3) - @test_macro_throws ErrorException @register(m, f[i](a)) - @test_macro_throws ErrorException @register(m, f(a::Int)) - @test_macro_throws ErrorException @register(m, f(a), 2) - @test_macro_throws ErrorException @register(m, g(a)) - @test_macro_throws ErrorException @register(m, eta(a)) - @test_macro_throws ErrorException @register(m, f(a), g) - @test_macro_throws ErrorException @register(m, f(a), g, g) - @test_macro_throws ErrorException @register(m, f(a), f, g) - @test_macro_throws ErrorException @register(m, h(a, b), h) - @test_macro_throws ErrorException @register(m, sin(a)) - # test univariate function with no gradient or hessian - @test @register(m, f(a)) isa Function - @test f(y) isa NLPExpr - @test f(y).tree_root.data.value == :f - @test isequal(f(y).tree_root.child.data.value, y) - @test f(2) == 8 - @test length(user_registered_functions(m)) == 1 - @test user_registered_functions(m)[1].name == :f - @test user_registered_functions(m)[1].func == f - @test user_registered_functions(m)[1].num_args == 1 - @test user_registered_functions(m)[1].gradient isa Nothing - @test user_registered_functions(m)[1].hessian isa Nothing - @test name_to_function(m, :f, 1) == f - # test univariate function with gradient - @test @register(m, f1(a), f) isa Function - @test f1(y) isa NLPExpr - @test f1(y).tree_root.data.value == :f1 - @test isequal(f1(y).tree_root.child.data.value, y) - @test f1(2) == 32 - @test length(user_registered_functions(m)) == 2 - @test user_registered_functions(m)[2].name == :f1 - @test user_registered_functions(m)[2].func == f1 - @test user_registered_functions(m)[2].num_args == 1 - @test user_registered_functions(m)[2].gradient == f - @test user_registered_functions(m)[2].hessian isa Nothing - @test name_to_function(m, :f1, 1) == f1 - # test univariate function with gradient and hessian - @test @register(m, f2(a), f, f1) isa Function - @test f2(y) isa NLPExpr - @test f2(y).tree_root.data.value == :f2 - @test isequal(f2(y).tree_root.child.data.value, y) - @test f2(2) == 10 - @test length(user_registered_functions(m)) == 3 - @test user_registered_functions(m)[3].name == :f2 - @test user_registered_functions(m)[3].func == f2 - @test user_registered_functions(m)[3].num_args == 1 - @test user_registered_functions(m)[3].gradient == f - @test user_registered_functions(m)[3].hessian == f1 - @test name_to_function(m, :f2, 1) == f2 - # test multivariate function with no gradient - @test @register(m, h(a, b)) isa Function - @test h(2, y) isa NLPExpr - @test h(2, y).tree_root.data.value == :h - @test isequal(h(2, y).tree_root.child.data.value, 2) - @test isequal(h(2, y).tree_root.child.sibling.data.value, y) - @test h(y, y) isa NLPExpr - @test h(y, 2) isa NLPExpr - @test h(2, 2) == 42 - @test length(user_registered_functions(m)) == 4 - @test user_registered_functions(m)[4].name == :h - @test user_registered_functions(m)[4].func == h - @test user_registered_functions(m)[4].num_args == 2 - @test user_registered_functions(m)[4].gradient isa Nothing - @test user_registered_functions(m)[4].hessian isa Nothing - @test name_to_function(m, :h, 2) == h - # test multivariate function with gradient - @test @register(m, h1(a, b), hg) isa Function - @test h1(2, y) isa NLPExpr - @test h1(2, y).tree_root.data.value == :h1 - @test isequal(h1(2, y).tree_root.child.data.value, 2) - @test isequal(h1(2, y).tree_root.child.sibling.data.value, y) - @test h1(y, y) isa NLPExpr - @test h1(y, 2) isa NLPExpr - @test h1(2, 2) == 13 - @test length(user_registered_functions(m)) == 5 - @test user_registered_functions(m)[5].name == :h1 - @test user_registered_functions(m)[5].func == h1 - @test user_registered_functions(m)[5].num_args == 2 - @test user_registered_functions(m)[5].gradient == hg - @test user_registered_functions(m)[5].hessian isa Nothing - @test name_to_function(m, :h1, 2) == h1 - # test wrong model error - @test_throws ErrorException f(@variable(InfiniteModel())) + @test @register(m, f1, 1, f) isa UserDefinedFunction + @test @register(m, f2, 1, f, f) isa UserDefinedFunction + @test @register(m, f3, 1, f, f, f) isa UserDefinedFunction + @test @register(m, h1, 2, h) isa UserDefinedFunction + @test @register(m, h2, 2, h, hg) isa UserDefinedFunction + @test @register(m, h3, 2, h, hg, ∇²h) isa UserDefinedFunction # test functional registration function registration_test() mt = InfiniteModel() @variable(mt, x) q(a) = 1 - @test @register(mt, q(a)) isa Function - @test q(x) isa NLPExpr + @test @register(mt, q, 1, q) isa UserDefinedFunction + @test @expression(mt, q(x)) isa NonlinearExpr return end @test registration_test() isa Nothing @@ -371,49 +86,12 @@ end # test normal m1 = Model() @test add_registered_to_jump(m1, m) isa Nothing - r1 = m1.nlp_model.operators - @test length(r1.registered_univariate_operators) == 3 - @test [r1.registered_univariate_operators[i].f for i in 1:3] == [f, f1, f2] - @test [r1.registered_univariate_operators[i].f′ for i in 2:3] == [f, f] - @test r1.registered_univariate_operators[3].f′′ == f1 - @test length(r1.registered_multivariate_operators) == 2 - # test error - m2 = Model() - h2(a, b) = 3 - @test @register(m, h2(a, b), hg, f) isa Function - @test_throws ErrorException add_registered_to_jump(m2, m) isa Nothing - r2 = m2.nlp_model.operators - @test length(r2.registered_univariate_operators) == 3 - @test [r2.registered_univariate_operators[i].f for i in 1:3] == [f, f1, f2] - @test [r2.registered_univariate_operators[i].f′ for i in 2:3] == [f, f] - @test r2.registered_univariate_operators[3].f′′ == f1 - @test length(r2.registered_multivariate_operators) == 2 + # TODO update checks below + # r1 = m1.nlp_model.operators + # @test length(r1.registered_univariate_operators) == 3 + # @test [r1.registered_univariate_operators[i].f for i in 1:3] == [f, f1, f2] + # @test [r1.registered_univariate_operators[i].f′ for i in 2:3] == [f, f] + # @test r1.registered_univariate_operators[3].f′′ == f1 + # @test length(r1.registered_multivariate_operators) == 2 end end - -# Test string methods -@testset "String Methods" begin - # setup the model data - m = InfiniteModel() - @infinite_parameter(m, t in [0, 1]) - @variable(m, y, Infinite(t)) - @variable(m, z) - # test making strings - @testset "String Creation" begin - # test some simple ones - @test string(sin(y) + 2) == "sin(y(t)) + 2" - @test string((z*z) ^ 4) == "(z²)^4" - @test string((cos(z) + sin(z)) / y) == "(cos(z) + sin(z)) / y(t)" - @test string(-cos(z + y) * z^2.3) == "-cos(z + y(t)) * z^2.3" - @test string((-InfiniteOpt.ifelse(z == 0, 0, z)) ^ 3) == "(-(ifelse(z == 0, 0, z))^3" - # test AffExpr cases - aff0 = zero(GenericAffExpr{Float64, GeneralVariableRef}) - @test string(aff0^4 + (2z - 3y + 42) ^ (-1z)) == "0^4 + (2 z - 3 y(t) + 42)^(-z)" - @test string((1z) / (2.3 * y)) == "z / (2.3 y(t))" - # test QuadExpr cases - quad0 = zero(GenericQuadExpr{Float64, GeneralVariableRef}) - @test string(quad0 / (z^2 + y * z)) == "0 / (z² + y(t)*z)" - @test string((0z^2 + 42) * sin(y)) == "42 * sin(y(t))" - @test string((z^2) / y) == "(z²) / y(t)" - end -end diff --git a/test/operators.jl b/test/operators.jl index 86714bb22..709a4edd4 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -919,3 +919,228 @@ end @test (copy(quad4) - quad4).terms[pair] == 0 end end + +# TODO adapt for new NLP types +# Test the basic expression generation via operators +# @testset "Operator Definition" begin +# # setup model data +# m = InfiniteModel() +# @infinite_parameter(m, t in [0, 1]) +# @variable(m, y, Infinite(t)) +# @variable(m, z) +# # test the sum +# @testset "sum" begin +# # test empty sums +# @test sum(i for i in Int[]) == 0 +# @test isequal(sum(NLPExpr[]), zero(NLPExpr)) +# @test isequal(sum(GeneralVariableRef[]), +# zero(GenericAffExpr{Float64, GeneralVariableRef})) +# # test NLP sums +# p = Node(NodeData(:sin)) +# addchild(p, NodeData(y)) +# nlp = NLPExpr(p) +# new = NLPExpr(InfiniteOpt._call_graph(:+, nlp, nlp)) +# @test isequal(sum(i for i in [nlp, nlp]), new) +# @test isequal(sum([nlp, nlp]), new) +# @test_throws ErrorException sum(i for i in [nlp, nlp]; bad = 42) +# @test_throws ErrorException sum(i for i in [y, t]; bad = 42) +# # test other expressions +# @test isequal(sum(i for i in [y, t]), y + t) +# @test isequal(sum([y, t]), y + t) +# # normal sums +# @test sum(i for i in 1:3) == 6 +# end +# # test the product +# @testset "prod" begin +# # test empty sums +# @test prod(i for i in Int[]) == 1 +# @test isequal(prod(NLPExpr[]), one(NLPExpr)) +# # test NLP sums +# p = Node(NodeData(:sin)) +# addchild(p, NodeData(y)) +# nlp = NLPExpr(p) +# new = NLPExpr(InfiniteOpt._call_graph(:*, nlp, nlp)) +# @test isequal(prod(i for i in [nlp, nlp]), new) +# @test isequal(prod([nlp, nlp]), new) +# @test_throws ErrorException prod(i for i in [nlp, nlp]; bad = 42) +# @test_throws ErrorException prod(i for i in [y, t, z]; bad = 42) +# # test other expressions +# new = NLPExpr(InfiniteOpt._call_graph(:*, y, t, z)) +# @test isequal(prod(i for i in [y, t, z]), new) +# @test isequal(prod([y, t, z]), new) +# # test normal products +# @test prod(i for i in 1:3) == 6 +# end +# # prepare the test grid +# aff = 2z - 2 +# quad = y^2 + y +# p = Node(NodeData(:sin)) +# addchild(p, NodeData(y)) +# nlp = NLPExpr(p) +# # test the multiplication operator +# @testset "Multiplication Operator" begin +# for (i, iorder) in [(42, 0), (y, 1), (aff, 1), (quad, 2), (nlp, 3)] +# for (j, jorder) in [(42, 0), (y, 1), (aff, 1), (quad, 2), (nlp, 3)] +# if iorder + jorder >= 3 +# expected = NLPExpr(InfiniteOpt._call_graph(:*, i, j)) +# @test isequal(i * j, expected) +# end +# end +# end +# expected = NLPExpr(InfiniteOpt._call_graph(:*, y, t, z,nlp)) +# @test isequal(y * t * z * nlp, expected) +# @test isequal(*(nlp), nlp) +# end +# # test division operator +# @testset "Division Operator" begin +# for i in [42, y, aff, quad, nlp] +# for j in [y, aff, quad, nlp] +# expected = NLPExpr(InfiniteOpt._call_graph(:/, i, j)) +# @test isequal(i / j, expected) +# end +# end +# expected = NLPExpr(InfiniteOpt._call_graph(:/, nlp, 42)) +# @test isequal(nlp / 42, expected) +# @test isequal(nlp / 1, nlp) +# @test_throws ErrorException nlp / 0 +# end +# # test the power operator +# @testset "Power Operator" begin +# for i in [42, y, aff, quad, nlp] +# for j in [y, aff, quad, nlp] +# expected = NLPExpr(InfiniteOpt._call_graph(:^, i, j)) +# @test isequal(i ^ j, expected) +# end +# end +# one_aff = one(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) +# for i in [y, aff, quad, nlp] +# for f in [Float64, Int] +# @test isequal(i^zero(f), one_aff) +# @test isequal(i^one(f), i) +# if i isa NLPExpr +# expected = NLPExpr(InfiniteOpt._call_graph(:^, i, f(2))) +# @test isequal(i ^ f(2), expected) +# else +# @test isequal(i^f(2), i * i) +# end +# expected = NLPExpr(InfiniteOpt._call_graph(:^, i, f(3))) +# @test isequal(i ^ f(3), expected) +# end +# end +# # extra tests +# @test isequal(y^0, one_aff) +# @test isequal(y^1, y) +# @test isequal(y^2, y * y) +# @test isequal(y^0.0, one_aff) +# @test isequal(y^1.0, y) +# @test isequal(y^2.0, y * y) +# end +# # test the subtraction operator +# @testset "Subtraction Operator" begin +# for i in [42, y, aff, quad, nlp] +# expected = NLPExpr(InfiniteOpt._call_graph(:-, nlp, i)) +# @test isequal(nlp - i, expected) +# expected = NLPExpr(InfiniteOpt._call_graph(:-, i, nlp)) +# @test isequal(i - nlp, expected) +# end +# expected = NLPExpr(InfiniteOpt._call_graph(:-, nlp)) +# @test isequal(-nlp, expected) +# @test isequal(y - y, zero(JuMP.GenericAffExpr{Float64, GeneralVariableRef})) +# @test (y - z).constant == 0 +# @test (y - z).terms[y] == 1 +# @test (y - z).terms[z] == -1 +# end +# # test the addition operator +# @testset "Addition Operator" begin +# for i in [42, y, aff, quad, nlp] +# expected = NLPExpr(InfiniteOpt._call_graph(:+, nlp, i)) +# @test isequal(nlp + i, expected) +# expected = NLPExpr(InfiniteOpt._call_graph(:+, i, nlp)) +# @test isequal(i + nlp, expected) +# end +# @test isequal(+nlp, nlp) +# end +# # test the comparison operators +# @testset "Comparison Operators" begin +# for i in [42, y, aff, quad, nlp] +# for j in [42, y, aff, quad, nlp] +# for (n, f) in (:< => Base.:(<), :(==) => Base.:(==), +# :> => Base.:(>), :<= => Base.:(<=), +# :>= => Base.:(>=)) +# if !(isequal(i, 42) && isequal(j, 42)) && !(isequal(i, y) && isequal(j, y)) +# expected = NLPExpr(InfiniteOpt._call_graph(n, i, j)) +# @test isequal(f(i, j), expected) +# end +# end +# end +# end +# for (n, f) in (:(==) => Base.:(==), :<= => Base.:(<=), :>= => Base.:(>=)) +# expected = NLPExpr(InfiniteOpt._call_graph(n, y, z)) +# @test isequal(f(y, z), expected) +# @test f(y, y) +# end +# for (n, f) in (:< => Base.:(<), :> => Base.:(>)) +# expected = NLPExpr(InfiniteOpt._call_graph(n, y, z)) +# @test isequal(f(y, z), expected) +# @test !f(y, y) +# end +# # extra tests +# @test (y == 0) isa NLPExpr +# @test (y <= 0) isa NLPExpr +# @test (y >= 0) isa NLPExpr +# @test (y > 0) isa NLPExpr +# @test (y < 0) isa NLPExpr +# @test (0 == y) isa NLPExpr +# @test (0 <= y) isa NLPExpr +# @test (0 >= y) isa NLPExpr +# @test (0 > y) isa NLPExpr +# @test (0 < y) isa NLPExpr +# end +# # test the logic operators +# @testset "Logic Operators" begin +# for i in [y, nlp] +# for j in [y, nlp] +# for (n, f) in (:&& => Base.:&, :|| => Base.:|) +# expected = NLPExpr(InfiniteOpt._call_graph(n, i, j)) +# @test isequal(f(i, j), expected) +# end +# end +# end +# for i in [y, nlp] +# @test !(i & false) +# @test !(false & i) +# @test isequal(i & true, i) +# @test isequal(true & i, i) +# @test (i | true) +# @test (true | i) +# @test isequal(i | false, i) +# @test isequal(false | i, i) +# end +# end +# # test ifelse +# @testset "ifelse" begin +# for i in [42, y, aff, quad, nlp] +# for j in [42, y, aff, quad, nlp] +# expected = NLPExpr(InfiniteOpt._call_graph(:ifelse, nlp, i, j)) +# @test isequal(InfiniteOpt.ifelse(nlp, i, j), expected) +# end +# end +# @test isequal(InfiniteOpt.ifelse(true, y, z), y) +# @test isequal(InfiniteOpt.ifelse(false, y, z), z) +# end +# # test the default functions +# @testset "Default Registered Functions" begin +# for i in [y, aff, quad, nlp] +# for (n, f) in InfiniteOpt._Base1ArgFuncList +# expected = NLPExpr(InfiniteOpt._call_graph(n, i)) +# @test isequal(f(i), expected) +# end +# end +# for i in [y, aff, quad, nlp] +# for (n, f) in InfiniteOpt._Special1ArgFuncList +# expected = NLPExpr(InfiniteOpt._call_graph(n, i)) +# @test isequal(f(i), expected) +# end +# end +# end +# end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 78a27a135..822f0bda6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -62,7 +62,7 @@ end println("") @time @testset "Derivative Methods" begin include("derivatives.jl") end println("") -# @time @testset "Nonlinear" begin include("nlp.jl") end +@time @testset "Nonlinear" begin include("nlp.jl") end # println("") # @time @testset "Operators" begin include("operators.jl") end # println("") From 12faa3ae755dac8d0c156ccda9ef503659479a53 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Thu, 4 May 2023 15:37:03 -0400 Subject: [PATCH 06/40] Added more tests --- src/expressions.jl | 4 +- src/nlp.jl | 3 +- test/expressions.jl | 74 +++------- test/macro_expressions.jl | 20 +-- test/operators.jl | 297 ++++++++++---------------------------- test/runtests.jl | 38 ++--- test/utilities.jl | 4 + 7 files changed, 133 insertions(+), 307 deletions(-) diff --git a/src/expressions.jl b/src/expressions.jl index 737747287..e09163653 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -648,14 +648,14 @@ end # NonlinearExpr (avoid recursion for deeply nested expressions) function _model_from_expr(expr::JuMP.NonlinearExpr) - stack = Vector{Any}[nlp.args] + stack = Vector{Any}[expr.args] while !isempty(stack) args = pop!(stack) for arg in args if arg isa JuMP.NonlinearExpr push!(stack, arg.args) else - result = _model_from_expr(expr) + result = _model_from_expr(arg) isnothing(result) || return result end end diff --git a/src/nlp.jl b/src/nlp.jl index 828d39134..a282b160c 100644 --- a/src/nlp.jl +++ b/src/nlp.jl @@ -16,7 +16,7 @@ append!(_NativeNLPFunctions, (:&&, :||, :<=, :(==), :>=, :<, :>)) dim::Int, f::Function, [∇f::Function,] - [∇²f::Function,] + [∇²f::Function] ) Extend `JuMP.add_user_defined_function` for `InfiniteModel`s. @@ -48,6 +48,7 @@ function JuMP.add_user_defined_function( end push!(model.registrations, RegisteredFunction(op, dim, funcs...)) model.func_lookup[op] = (funcs[1], dim) + # TODO should we set the optimizer model to be out of date? return JuMP.UserDefinedFunction(op) end diff --git a/test/expressions.jl b/test/expressions.jl index 1faf2b103..f7df49edf 100644 --- a/test/expressions.jl +++ b/test/expressions.jl @@ -335,39 +335,6 @@ end end end -# Test the basic extensions -@testset "Base Extensions" begin - # setup model - m = InfiniteModel() - @variable(m, z) - @variable(m, y) - aff = 2z + 42 - quad = z^2 + 2z - nlp = sin(z) - # test convert - @testset "Base.convert (NLPExpr)" begin - @test isequal(convert(NLPExpr, 1), one(NLPExpr)) - @test isequal(convert(NLPExpr, z), NLPExpr(Node(NodeData(z)))) - @test isequal(convert(NLPExpr, aff), NLPExpr(Node(NodeData(aff)))) - @test isequal(convert(NLPExpr, quad), NLPExpr(Node(NodeData(quad)))) - @test convert(NLPExpr, nlp) === nlp - end - # test isequal for UnorderedPair - @testset "Base.isequal (JuMP.UnorderedPair)" begin - @test isequal(UnorderedPair(z, z), UnorderedPair(z, z)) - @test isequal(UnorderedPair(z, y), UnorderedPair(y, z)) - @test !isequal(UnorderedPair(z, y), UnorderedPair(z, z)) - end - # test isequal for expressions - @testset "Base.isequal (Expr Fallbacks)" begin - @test !isequal(z, 2) - @test !isequal(2, z) - @test !isequal(z, aff) - @test !isequal(z, quad) - @test !isequal(nlp, aff) - end -end - # Test _interrogate_variables @testset "_interrogate_variables" begin # setup model @@ -399,8 +366,8 @@ end @test InfiniteOpt._interrogate_variables(i -> push!(a, i), quad) isa Nothing @test isequal(a, [z, z, z]) end - # test NLPExpr - @testset "NLPExpr" begin + # test NonlinearExpr + @testset "NonlinearExpr" begin a = [] @test InfiniteOpt._interrogate_variables(i -> push!(a, i), nlp) isa Nothing @test isequal(a, [z, z]) @@ -470,7 +437,7 @@ end [pt, inf, meas])) end # test for Array of expressions - @testset "NLPExpr" begin + @testset "NonlinearExpr" begin # make expressions nlp = sin(pt) + inf / pt # test expressions @@ -532,10 +499,10 @@ end @test sort!(InfiniteOpt._object_numbers(quad1)) == [1, 2] @test InfiniteOpt._object_numbers(quad2) == [] end - # test for NLPExpr - @testset "NLPExpr" begin + # test for NonlinearExpr + @testset "NonlinearExpr" begin # make expressions - nlp = sin(inf) + nlp = sin(inf) / pt # test expressions @test InfiniteOpt._object_numbers(nlp) == [1] end @@ -590,8 +557,8 @@ end @test sort!(InfiniteOpt._parameter_numbers(quad1)) == [1, 2, 3] @test InfiniteOpt._parameter_numbers(quad2) == [] end - # test for NLPExpr - @testset "NLPExpr" begin + # test for NonlinearExpr + @testset "NonlinearExpr" begin # make expressions nlp = sin(inf2) # test expressions @@ -628,11 +595,11 @@ end @test InfiniteOpt._model_from_expr(quad2) isa Nothing @test InfiniteOpt._model_from_expr(quad3) === m end - # test for NLPExpr - @testset "NLPExpr" begin + # test for NonlinearExpr + @testset "NonlinearExpr" begin # make expressions nlp1 = sin(hd) - nlp2 = zero(NLPExpr) + nlp2 = NonlinearExpr(:sin, Any[0.0]) nlp3 = 2 + sin(hd^2) # test expressions @test InfiniteOpt._model_from_expr(nlp1) === m @@ -687,19 +654,20 @@ end @test !haskey(quad.terms, UnorderedPair{GeneralVariableRef}(pt, pt)) @test isa(InfiniteOpt._remove_variable(quad2, inf), Nothing) end - # test for NLPExpr - @testset "NLPExpr" begin + # test for NonlinearExpr + @testset "NonlinearExpr" begin # make expressions nlp1 = sin(3pt) - nlp2 = pt^2.3 + <=(inf, pt) + nlp2 = pt^2.3 + ^(inf, pt) nlp3 = cos(pt^2 + pt) / (2pt + 2inf) # test expressions @test InfiniteOpt._remove_variable(nlp1, pt) isa Nothing - @test isequal(nlp1, sin(zero(zero(GenericAffExpr{Float64, GeneralVariableRef})))) + @test isequal(nlp1, sin(zero(GenericAffExpr{Float64, GeneralVariableRef}))) @test InfiniteOpt._remove_variable(nlp2, pt) isa Nothing - @test isequal(nlp2, zero(NLPExpr)^2.3 + <=(inf, 0.0)) + @test nlp2.args[1].args == [0.0, 2.3] + @test nlp2.args[2].args == Any[inf, 0.0] @test InfiniteOpt._remove_variable(nlp3, inf) isa Nothing - @test isequal(nlp3, cos(pt^2 + pt) / (2pt)) + @test nlp3.args[2] == 2pt end # test for AbstractArray @testset "AbstractArray" begin @@ -733,9 +701,9 @@ end @testset "QuadExpr" begin @test isequal(map_expression(v -> x, quad), x^2 + 2x) end - # test NLPExpr - @testset "NLPExpr" begin - @test isequal(map_expression(v -> z, nlp), (sin(z) + (2z + 42)) ^ 3.4) + # test NonlinearExpr + @testset "NonlinearExpr" begin + @test isequal(map_expression(v -> y, nlp), (sin(y) + (2y + 42))^3.4) @test isequal(map_expression(v -> v^3, sin(y)), sin(y^3)) end end diff --git a/test/macro_expressions.jl b/test/macro_expressions.jl index 6162a090b..20bbf27e9 100644 --- a/test/macro_expressions.jl +++ b/test/macro_expressions.jl @@ -79,7 +79,7 @@ end # test nonlinear operations @testset "Nonlinear" begin - @test isequal(@expression(m, pt / inf), pt * (1 / inf)) + @test isequal(@expression(m, pt / inf), pt / inf) @test isequal(@expression(m, pt ^ inf), pt ^ inf) @test isequal(@expression(m, 2 ^ inf), 2 ^ inf) @test isequal(@expression(m, abs(pt)), abs(pt)) @@ -168,7 +168,7 @@ end end # test nonlinear operations @testset "Nonlinear" begin - @test isequal(@expression(m, pt / aff1), pt * (1 / aff1)) + @test isequal(@expression(m, pt / aff1), pt / aff1) @test isequal(@expression(m, pt ^ aff1), pt ^ aff1) @test isequal(@expression(m, abs(aff1)), abs(aff1)) end @@ -256,7 +256,7 @@ end end # test nonlinear operations @testset "Nonlinear" begin - @test isequal(@expression(m, aff1 / pt), aff1 * (1 / pt)) + @test isequal(@expression(m, aff1 / pt), aff1 / pt) @test isequal(@expression(m, aff1 ^ pt), aff1 ^ pt) end end @@ -405,7 +405,7 @@ end end # test nonlinear operations @testset "Nonlinear" begin - @test isequal(@expression(m, aff1 / aff1), aff1 * (1 / aff1)) + @test isequal(@expression(m, aff1 / aff1), aff1 / aff1) @test isequal(@expression(m, aff1 ^ aff1), aff1 ^ aff1) end end @@ -512,7 +512,7 @@ end # test nonlinear operations @testset "Nonlinear" begin @test isequal(@expression(m, quad1 * pt), quad1 * pt) - @test isequal(@expression(m, quad1 / pt), quad1 * (1 / pt)) + @test isequal(@expression(m, quad1 / pt), quad1 / pt) @test isequal(@expression(m, quad1 ^ pt), quad1 ^ pt) @test isequal(@expression(m, abs(quad1)), abs(quad1)) end @@ -619,7 +619,7 @@ end end @testset "Nonlinear" begin @test isequal(@expression(m, pt * quad1), pt * quad1) - @test isequal(@expression(m, pt / quad1), pt * (1 / quad1)) + @test isequal(@expression(m, pt / quad1), pt / quad1) @test isequal(@expression(m, pt ^ quad1), pt ^ quad1) end end @@ -737,7 +737,7 @@ end end @testset "Nonlinear" begin @test isequal(@expression(m, aff1 * quad1), aff1 * quad1) - @test isequal(@expression(m, aff1 / quad1), aff1 * (1 / quad1)) + @test isequal(@expression(m, aff1 / quad1), aff1 / quad1) @test isequal(@expression(m, aff1 ^ quad1), aff1 ^ quad1) end end @@ -856,7 +856,7 @@ end # test nonlinear operations @testset "Nonlinear" begin @test isequal(@expression(m, quad1 * aff1), quad1 * aff1) - @test isequal(@expression(m, quad1 / aff1), quad1 * (1 / aff1)) + @test isequal(@expression(m, quad1 / aff1), quad1 / aff1) @test isequal(@expression(m, quad1 ^ aff1), quad1 ^ aff1) end end @@ -961,7 +961,7 @@ end # test nonlinear operations @testset "Nonlinear" begin @test isequal(@expression(m, quad1 * quad1), quad1 * quad1) - @test isequal(@expression(m, quad1 / quad1), quad1 * (1 / quad1)) + @test isequal(@expression(m, quad1 / quad1), quad1 / quad1) @test isequal(@expression(m, quad1 ^ quad1), quad1 ^ quad1) end end @@ -986,7 +986,7 @@ end @test isequal(@expression(m, nlp ^ quad), nlp ^ quad) @test isequal(@expression(m, 2 ^ quad), 2 ^ quad) @test isequal(@expression(m, 2 ^ nlp), 2 ^ nlp) - @test isequal(@expression(m, nlp / y), nlp * (1 / y)) + @test isequal(@expression(m, nlp / y), nlp / y) @test isequal(@expression(m, nlp + aff), nlp + aff) end # test function calls diff --git a/test/operators.jl b/test/operators.jl index 709a4edd4..1ca5604a7 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -922,225 +922,78 @@ end # TODO adapt for new NLP types # Test the basic expression generation via operators -# @testset "Operator Definition" begin -# # setup model data -# m = InfiniteModel() -# @infinite_parameter(m, t in [0, 1]) -# @variable(m, y, Infinite(t)) -# @variable(m, z) -# # test the sum -# @testset "sum" begin -# # test empty sums -# @test sum(i for i in Int[]) == 0 -# @test isequal(sum(NLPExpr[]), zero(NLPExpr)) -# @test isequal(sum(GeneralVariableRef[]), -# zero(GenericAffExpr{Float64, GeneralVariableRef})) -# # test NLP sums -# p = Node(NodeData(:sin)) -# addchild(p, NodeData(y)) -# nlp = NLPExpr(p) -# new = NLPExpr(InfiniteOpt._call_graph(:+, nlp, nlp)) -# @test isequal(sum(i for i in [nlp, nlp]), new) -# @test isequal(sum([nlp, nlp]), new) -# @test_throws ErrorException sum(i for i in [nlp, nlp]; bad = 42) -# @test_throws ErrorException sum(i for i in [y, t]; bad = 42) -# # test other expressions -# @test isequal(sum(i for i in [y, t]), y + t) -# @test isequal(sum([y, t]), y + t) -# # normal sums -# @test sum(i for i in 1:3) == 6 -# end -# # test the product -# @testset "prod" begin -# # test empty sums -# @test prod(i for i in Int[]) == 1 -# @test isequal(prod(NLPExpr[]), one(NLPExpr)) -# # test NLP sums -# p = Node(NodeData(:sin)) -# addchild(p, NodeData(y)) -# nlp = NLPExpr(p) -# new = NLPExpr(InfiniteOpt._call_graph(:*, nlp, nlp)) -# @test isequal(prod(i for i in [nlp, nlp]), new) -# @test isequal(prod([nlp, nlp]), new) -# @test_throws ErrorException prod(i for i in [nlp, nlp]; bad = 42) -# @test_throws ErrorException prod(i for i in [y, t, z]; bad = 42) -# # test other expressions -# new = NLPExpr(InfiniteOpt._call_graph(:*, y, t, z)) -# @test isequal(prod(i for i in [y, t, z]), new) -# @test isequal(prod([y, t, z]), new) -# # test normal products -# @test prod(i for i in 1:3) == 6 -# end -# # prepare the test grid -# aff = 2z - 2 -# quad = y^2 + y -# p = Node(NodeData(:sin)) -# addchild(p, NodeData(y)) -# nlp = NLPExpr(p) -# # test the multiplication operator -# @testset "Multiplication Operator" begin -# for (i, iorder) in [(42, 0), (y, 1), (aff, 1), (quad, 2), (nlp, 3)] -# for (j, jorder) in [(42, 0), (y, 1), (aff, 1), (quad, 2), (nlp, 3)] -# if iorder + jorder >= 3 -# expected = NLPExpr(InfiniteOpt._call_graph(:*, i, j)) -# @test isequal(i * j, expected) -# end -# end -# end -# expected = NLPExpr(InfiniteOpt._call_graph(:*, y, t, z,nlp)) -# @test isequal(y * t * z * nlp, expected) -# @test isequal(*(nlp), nlp) -# end -# # test division operator -# @testset "Division Operator" begin -# for i in [42, y, aff, quad, nlp] -# for j in [y, aff, quad, nlp] -# expected = NLPExpr(InfiniteOpt._call_graph(:/, i, j)) -# @test isequal(i / j, expected) -# end -# end -# expected = NLPExpr(InfiniteOpt._call_graph(:/, nlp, 42)) -# @test isequal(nlp / 42, expected) -# @test isequal(nlp / 1, nlp) -# @test_throws ErrorException nlp / 0 -# end -# # test the power operator -# @testset "Power Operator" begin -# for i in [42, y, aff, quad, nlp] -# for j in [y, aff, quad, nlp] -# expected = NLPExpr(InfiniteOpt._call_graph(:^, i, j)) -# @test isequal(i ^ j, expected) -# end -# end -# one_aff = one(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) -# for i in [y, aff, quad, nlp] -# for f in [Float64, Int] -# @test isequal(i^zero(f), one_aff) -# @test isequal(i^one(f), i) -# if i isa NLPExpr -# expected = NLPExpr(InfiniteOpt._call_graph(:^, i, f(2))) -# @test isequal(i ^ f(2), expected) -# else -# @test isequal(i^f(2), i * i) -# end -# expected = NLPExpr(InfiniteOpt._call_graph(:^, i, f(3))) -# @test isequal(i ^ f(3), expected) -# end -# end -# # extra tests -# @test isequal(y^0, one_aff) -# @test isequal(y^1, y) -# @test isequal(y^2, y * y) -# @test isequal(y^0.0, one_aff) -# @test isequal(y^1.0, y) -# @test isequal(y^2.0, y * y) -# end -# # test the subtraction operator -# @testset "Subtraction Operator" begin -# for i in [42, y, aff, quad, nlp] -# expected = NLPExpr(InfiniteOpt._call_graph(:-, nlp, i)) -# @test isequal(nlp - i, expected) -# expected = NLPExpr(InfiniteOpt._call_graph(:-, i, nlp)) -# @test isequal(i - nlp, expected) -# end -# expected = NLPExpr(InfiniteOpt._call_graph(:-, nlp)) -# @test isequal(-nlp, expected) -# @test isequal(y - y, zero(JuMP.GenericAffExpr{Float64, GeneralVariableRef})) -# @test (y - z).constant == 0 -# @test (y - z).terms[y] == 1 -# @test (y - z).terms[z] == -1 -# end -# # test the addition operator -# @testset "Addition Operator" begin -# for i in [42, y, aff, quad, nlp] -# expected = NLPExpr(InfiniteOpt._call_graph(:+, nlp, i)) -# @test isequal(nlp + i, expected) -# expected = NLPExpr(InfiniteOpt._call_graph(:+, i, nlp)) -# @test isequal(i + nlp, expected) -# end -# @test isequal(+nlp, nlp) -# end -# # test the comparison operators -# @testset "Comparison Operators" begin -# for i in [42, y, aff, quad, nlp] -# for j in [42, y, aff, quad, nlp] -# for (n, f) in (:< => Base.:(<), :(==) => Base.:(==), -# :> => Base.:(>), :<= => Base.:(<=), -# :>= => Base.:(>=)) -# if !(isequal(i, 42) && isequal(j, 42)) && !(isequal(i, y) && isequal(j, y)) -# expected = NLPExpr(InfiniteOpt._call_graph(n, i, j)) -# @test isequal(f(i, j), expected) -# end -# end -# end -# end -# for (n, f) in (:(==) => Base.:(==), :<= => Base.:(<=), :>= => Base.:(>=)) -# expected = NLPExpr(InfiniteOpt._call_graph(n, y, z)) -# @test isequal(f(y, z), expected) -# @test f(y, y) -# end -# for (n, f) in (:< => Base.:(<), :> => Base.:(>)) -# expected = NLPExpr(InfiniteOpt._call_graph(n, y, z)) -# @test isequal(f(y, z), expected) -# @test !f(y, y) -# end -# # extra tests -# @test (y == 0) isa NLPExpr -# @test (y <= 0) isa NLPExpr -# @test (y >= 0) isa NLPExpr -# @test (y > 0) isa NLPExpr -# @test (y < 0) isa NLPExpr -# @test (0 == y) isa NLPExpr -# @test (0 <= y) isa NLPExpr -# @test (0 >= y) isa NLPExpr -# @test (0 > y) isa NLPExpr -# @test (0 < y) isa NLPExpr -# end -# # test the logic operators -# @testset "Logic Operators" begin -# for i in [y, nlp] -# for j in [y, nlp] -# for (n, f) in (:&& => Base.:&, :|| => Base.:|) -# expected = NLPExpr(InfiniteOpt._call_graph(n, i, j)) -# @test isequal(f(i, j), expected) -# end -# end -# end -# for i in [y, nlp] -# @test !(i & false) -# @test !(false & i) -# @test isequal(i & true, i) -# @test isequal(true & i, i) -# @test (i | true) -# @test (true | i) -# @test isequal(i | false, i) -# @test isequal(false | i, i) -# end -# end -# # test ifelse -# @testset "ifelse" begin -# for i in [42, y, aff, quad, nlp] -# for j in [42, y, aff, quad, nlp] -# expected = NLPExpr(InfiniteOpt._call_graph(:ifelse, nlp, i, j)) -# @test isequal(InfiniteOpt.ifelse(nlp, i, j), expected) -# end -# end -# @test isequal(InfiniteOpt.ifelse(true, y, z), y) -# @test isequal(InfiniteOpt.ifelse(false, y, z), z) -# end -# # test the default functions -# @testset "Default Registered Functions" begin -# for i in [y, aff, quad, nlp] -# for (n, f) in InfiniteOpt._Base1ArgFuncList -# expected = NLPExpr(InfiniteOpt._call_graph(n, i)) -# @test isequal(f(i), expected) -# end -# end -# for i in [y, aff, quad, nlp] -# for (n, f) in InfiniteOpt._Special1ArgFuncList -# expected = NLPExpr(InfiniteOpt._call_graph(n, i)) -# @test isequal(f(i), expected) -# end -# end -# end -# end \ No newline at end of file +@testset "Operator Definition" begin + # setup model data + m = InfiniteModel() + @infinite_parameter(m, t in [0, 1]) + @variable(m, y, Infinite(t)) + @variable(m, z) + # prepare the test grid + aff = 2z - 2 + quad = y^2 + y + nlp = NonlinearExpr(:sin, Any[y]) + # test the multiplication operator + @testset "Multiplication Operator" begin + for (i, iorder) in [(42, 0), (y, 1), (aff, 1), (quad, 2), (nlp, 3)] + for (j, jorder) in [(42, 0), (y, 1), (aff, 1), (quad, 2), (nlp, 3)] + if iorder + jorder >= 3 + @test isequal((i * j).args, Any[i, j]) + @test (i * j).head == :* + end + end + end + end + # test division operator + @testset "Division Operator" begin + for i in [42, y, aff, quad, nlp] + for j in [y, aff, quad, nlp] + @test isequal((i / j).args, Any[i, j]) + @test (i / j).head == :/ + end + end + @test isequal((nlp / 42).args, Any[nlp, 42]) + end + # test the power operator + @testset "Power Operator" begin + for i in [42, y, aff, quad, nlp] + for j in [y, aff, quad, nlp] + @test isequal((i ^ j).args, Any[i, j]) + end + end + # TODO update these once the behavior is settled in JuMP + # one_aff = one(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) + # for i in [y, aff, quad, nlp] + # for f in [Float64, Int] + # if i isa NonlinearExpr + # @test isequal((i ^ f(2)).args, Any[i, f(2)]) + # else + # @test isequal(i^f(2), i * i) + # end + # @test isequal((i ^ f(3)).args, Any[i, f(3)]) + # end + # end + # extra tests + # @test isequal(y^0, one_aff) + # @test isequal(y^1, y) + # @test isequal(y^2, y * y) + # @test isequal(y^0.0, one_aff) + # @test isequal(y^1.0, y) + # @test isequal(y^2.0, y * y) + end + # test the subtraction operator + @testset "Subtraction Operator" begin + for i in [42, y, aff, quad, nlp] + @test isequal((nlp - i).args, Any[nlp, i]) + @test isequal((i - nlp).args, Any[i, nlp]) + end + @test isequal((-nlp).args, Any[nlp]) + end + # test the addition operator + @testset "Addition Operator" begin + for i in [42, y, aff, quad, nlp] + @test isequal((nlp + i).args, Any[nlp, i]) + @test isequal((i + nlp).args, Any[i, nlp]) + end + @test isequal(+nlp, nlp) + end +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 822f0bda6..1e994013f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -63,25 +63,25 @@ println("") @time @testset "Derivative Methods" begin include("derivatives.jl") end println("") @time @testset "Nonlinear" begin include("nlp.jl") end -# println("") -# @time @testset "Operators" begin include("operators.jl") end -# println("") -# @time @testset "Expression Methods" begin include("expressions.jl") end -# println("") -# @time @testset "Macro Expressions" begin include("macro_expressions.jl") end -# println("") -# @time @testset "Measure Methods" begin include("measures.jl") end -# println("") -# @time @testset "Measure Toolbox Methods" begin -# @testset "Integrals" begin include("MeasureToolbox/integrals.jl") end -# @testset "Expectations" begin include("MeasureToolbox/expectations.jl") end -# @testset "Support Sums" begin include("MeasureToolbox/support_sums.jl") end -# end -# println("") -# @time @testset "Objective Methods" begin include("objective.jl") end -# println("") -# @time @testset "Constraint Methods" begin include("constraints.jl") end -# println("") +println("") +@time @testset "Operators" begin include("operators.jl") end +println("") +@time @testset "Expression Methods" begin include("expressions.jl") end +println("") +@time @testset "Macro Expressions" begin include("macro_expressions.jl") end +println("") +@time @testset "Measure Methods" begin include("measures.jl") end +println("") +@time @testset "Measure Toolbox Methods" begin + @testset "Integrals" begin include("MeasureToolbox/integrals.jl") end + @testset "Expectations" begin include("MeasureToolbox/expectations.jl") end + @testset "Support Sums" begin include("MeasureToolbox/support_sums.jl") end +end +println("") +@time @testset "Objective Methods" begin include("objective.jl") end +println("") +@time @testset "Constraint Methods" begin include("constraints.jl") end +println("") # @time @testset "Printing Methods" begin include("show.jl") end # println("") # @time @testset "Deletion Methods" begin include("deletion.jl") end diff --git a/test/utilities.jl b/test/utilities.jl index a91a41675..d32b2d107 100644 --- a/test/utilities.jl +++ b/test/utilities.jl @@ -128,3 +128,7 @@ function _update_variable_param_refs(vref::InfiniteVariableRef, InfiniteOpt._set_core_variable_object(vref, new_var) return end + +function Base.isequal(nlp1::NonlinearExpr, nlp2::NonlinearExpr) + return nlp1.head == nlp2.head && isequal(nlp1.args, nlp2.args) +end From 26b01d374384431426d1989faf095fbc2982ffc6 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Thu, 4 May 2023 17:31:27 -0400 Subject: [PATCH 07/40] Reimplement all tests --- src/TranscriptionOpt/transcribe.jl | 2 +- src/results.jl | 6 ++-- test/TranscriptionOpt/model.jl | 17 +++++------ test/TranscriptionOpt/transcribe.jl | 39 ++++++++---------------- test/deletion.jl | 2 +- test/extensions/optimizer_model.jl | 4 +-- test/measure_expansions.jl | 8 ++--- test/results.jl | 26 +++++++++------- test/runtests.jl | 46 ++++++++++++++--------------- 9 files changed, 70 insertions(+), 80 deletions(-) diff --git a/src/TranscriptionOpt/transcribe.jl b/src/TranscriptionOpt/transcribe.jl index ff40b064d..24fce9dfe 100644 --- a/src/TranscriptionOpt/transcribe.jl +++ b/src/TranscriptionOpt/transcribe.jl @@ -688,7 +688,7 @@ function _process_constraint( name::String ) new_func = map(f -> transcription_expression(trans_model, f, raw_supp), func) - if any(f -> f isa JuMP.NonlinearExpression, new_func) + if any(f -> f isa JuMP.NonlinearExpr, new_func) error("TranscriptionOpt does not support vector constraints of general " * "nonlinear expressions because this is not yet supported by JuMP.") end diff --git a/src/results.jl b/src/results.jl index 10b360af6..40e50701a 100644 --- a/src/results.jl +++ b/src/results.jl @@ -239,7 +239,7 @@ julia> value(my_infinite_expr) ``` """ function JuMP.value( - expr::JuMP.AbstractJuMPScalar; + expr::Union{JuMP.GenericAffExpr{Float64, GeneralVariableRef}, JuMP.GenericQuadExpr{Float64, GeneralVariableRef}}; # TODO fix result::Int = 1, kwargs... ) @@ -247,8 +247,8 @@ function JuMP.value( model = _model_from_expr(expr) # if no model then the expression only contains a constant if isnothing(model) - expr isa JuMP.NonlinearExpr && error("Cannot evaluate the value of `$expr`,", - "because it doesn't have variables.") + # expr isa JuMP.NonlinearExpr && error("Cannot evaluate the value of `$expr`,", + # "because it doesn't have variables.") return JuMP.constant(expr) # otherwise let's call map_value else diff --git a/test/TranscriptionOpt/model.jl b/test/TranscriptionOpt/model.jl index 56b66d138..be5893faa 100644 --- a/test/TranscriptionOpt/model.jl +++ b/test/TranscriptionOpt/model.jl @@ -506,14 +506,12 @@ end @test IOTO.transcription_expression(tm, expr, [1., 1., 1.]) == expected # test becomes a nonlinear expression expr = meas2 * x0 - expected = "subexpression[1]: +((-2.0 * a + b * b + d * d) * b)" - @test sprint(show, IOTO.transcription_expression(tm, expr, [1., 1., 1.])) == expected + expected = +((-2.0 * a + b * b + d * d) * b, 0.0) + @test isequal(IOTO.transcription_expression(tm, expr, [1., 1., 1.]), expected) end - # test transcription expression for NLPExprs with 3 args - @testset "transcription_expression (NLPExpr)" begin - expr = sin(y) - expected = "subexpression[2]: sin(a)" - @test sprint(show, IOTO.transcription_expression(tm, expr, [1., 1., 1.])) == expected + # test transcription expression for NonlinearExprs with 3 args + @testset "transcription_expression (NonlinearExpr)" begin + @test isequal(IOTO.transcription_expression(tm, sin(y), [1., 1., 1.]), sin(a)) end # test transcription expression for numbers with 3 args @testset "transcription_expression (Real)" begin @@ -535,9 +533,8 @@ end expected = 2b- a @test IOTO.transcription_expression(tm, expr) == expected @test IOTO.transcription_expression(tm, expr, ndarray = true) == [expected] - # test NLPExpr - expected = "subexpression[3]: sin(b)" - @test sprint(show, IOTO.transcription_expression(tm, sin(x0))) == expected + # test NonlinearExpr + @test isequal(IOTO.transcription_expression(tm, sin(x0)), sin(b)) end # test transcription expression for variables with 2 args @testset "transcription_expression (Variable 2 Args)" begin diff --git a/test/TranscriptionOpt/transcribe.jl b/test/TranscriptionOpt/transcribe.jl index 28a41bd39..5dabc5994 100644 --- a/test/TranscriptionOpt/transcribe.jl +++ b/test/TranscriptionOpt/transcribe.jl @@ -221,8 +221,7 @@ end @objective(m, Max, z^4) @test IOTO.transcribe_objective!(tm, m) isa Nothing @test objective_sense(tm) == MOI.MAX_SENSE - @test objective_function_string(MIME("text/plain"), tm) == "subexpression[1]" - @test sprint(show, NonlinearExpression(tm, 1)) == "subexpression[1]: z ^ 4.0" + @test isequal(objective_function(tm), tz^4) end end @@ -300,14 +299,6 @@ end @test !IOTO._support_in_restrictions([NaN, 0., 0.], [1, 2], [IntervalDomain(1, 1), IntervalDomain(1, 1)]) @test !IOTO._support_in_restrictions([NaN, 0., 2.], [1, 3], [IntervalDomain(1, 1), IntervalDomain(1, 1)]) end - # test _make_constr_ast - @testset "_make_constr_ast" begin - @test IOTO._make_constr_ast(xt, MOI.LessThan(1.0)) == :($xt <= 1.0) - @test IOTO._make_constr_ast(xt, MOI.GreaterThan(1.0)) == :($xt >= 1.0) - @test IOTO._make_constr_ast(xt, MOI.EqualTo(1.0)) == :($xt == 1.0) - @test IOTO._make_constr_ast(xt, MOI.Interval(0.0, 1.0)) == :(0.0 <= $xt <= 1.0) - @test_throws ErrorException IOTO._make_constr_ast(xt, MOI.Integer()) - end # test _process_constraint @testset "_process_constraint" begin # scalar constraint @@ -322,12 +313,10 @@ end con = constraint_object(c7) func = jump_function(con) set = moi_set(con) - expected = Sys.iswindows() ? "subexpression[1] - 0.0 == 0" : "subexpression[1] - 0.0 = 0" - @test sprint(show, IOTO._process_constraint(tm, con, func, set, zeros(3), "test1")) == expected - expected = ["subexpression[1]: sin(z) ^ x(support: 1) - 0.0", - "subexpression[1]: sin(z) ^ x(support: 2) - 0.0"] - @test sprint(show, NonlinearExpression(tm, 1)) in expected - tm.nlp_model = nothing + @test IOTO._process_constraint(tm, con, func, set, zeros(3), "test1") isa ConstraintRef + @test num_constraints(tm, typeof(func), typeof(set)) == 1 + cref = constraint_by_name(tm, "test1") + delete(tm, cref) # vector constraint con = constraint_object(c6) func = jump_function(con) @@ -341,7 +330,6 @@ end func = [sin(z)] set = MOI.Zeros(1) @test_throws ErrorException IOTO._process_constraint(tm, con, func, set, zeros(3), "test2") - tm.nlp_model = nothing # fallback @test_throws ErrorException IOTO._process_constraint(tm, :bad, func, set, zeros(3), "bad") @@ -372,8 +360,6 @@ end @test length(transcription_constraint(c6)) == 6 @test moi_set(constraint_object(first(transcription_constraint(c6)))) == MOI.Zeros(2) @test length(transcription_constraint(c7)) == 6 - @test length(keys(tm.nlp_model.constraints)) == 6 - @test length(tm.nlp_model.expressions) == 6 # test the info constraint supports expected = [([0., 0.], 0.5), ([0., 0.], 1.), ([1., 1.], 0.), ([1., 1.], 0.5), ([1., 1.], 1.)] @test sort(supports(LowerBoundRef(x))) == expected @@ -449,8 +435,8 @@ end @constraint(m, x + y == 83) @constraint(m, c6, [z, w] in MOI.Zeros(2)) g(a) = 42 - @register(m, g(a)) - @constraint(m, c7, g(z) == 2) + @register(m, gr, 1, g) + @constraint(m, c7, gr(z) == 2) @objective(m, Min, x0 + meas1) # test basic usage tm = optimizer_model(m) @@ -499,10 +485,10 @@ end @test length(d2t) == 2 @test upper_bound(d1t[1]) == 2 @test supports(d2) == [(0.,), (1.,)] - # test registration - r = tm.nlp_model.operators - @test length(r.registered_univariate_operators) == 1 - @test r.registered_univariate_operators[1].f == g + # test registration (TODO how to test this with new system) + # r = tm.nlp_model.operators + # @test length(r.registered_univariate_operators) == 1 + # @test r.registered_univariate_operators[1].f == g # test objective xt = transcription_variable(tm, x) @test objective_function(tm) == 2xt[1] + xt[2] - 2wt - d2t[1] - d2t[2] @@ -520,7 +506,8 @@ end @test name(transcription_constraint(c2)) == "c2(support: 1)" @test name(transcription_constraint(c1)) == "c1(support: 1)" @test supports(c1) == (0., [0., 0.]) - @test transcription_constraint(c7) isa NonlinearConstraintRef + @test transcription_constraint(c7) isa ConstraintRef + @test isequal(constraint_object(transcription_constraint(c7)).func, gr(zt) - 2.) # test info constraints @test transcription_constraint(LowerBoundRef(z)) == LowerBoundRef(zt) @test transcription_constraint(UpperBoundRef(z)) == UpperBoundRef(zt) diff --git a/test/deletion.jl b/test/deletion.jl index 5035517e3..c5eb6ff9e 100644 --- a/test/deletion.jl +++ b/test/deletion.jl @@ -482,7 +482,7 @@ end @test isequal_canonical(measure_function(meas1), y + par) @test isequal_canonical(jump_function(constraint_object(con1)), y + par) @test isequal_canonical(objective_function(m), y + 0) - @test isequal_canonical(jump_function(constraint_object(con4)), sin(zero(NLPExpr))) + @test isequal_canonical(jump_function(constraint_object(con4)), NonlinearExpr(:-, Any[NonlinearExpr(:sin, Any[0.0]), 0.0])) @test !is_valid(m, con3) @test !haskey(InfiniteOpt._data_dictionary(m, FiniteVariable), JuMP.index(x)) # test deletion of y diff --git a/test/extensions/optimizer_model.jl b/test/extensions/optimizer_model.jl index 91a98d398..1eb9eed51 100644 --- a/test/extensions/optimizer_model.jl +++ b/test/extensions/optimizer_model.jl @@ -120,7 +120,7 @@ end # Extend optimizer_model_expression if appropriate to enable expression related queries function InfiniteOpt.optimizer_model_expression( - expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, NLPExpr}, # POSSIBLY BREAK THESE UP INTO 3 SEPARATE FUNCTIONS + expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr}, # POSSIBLY BREAK THESE UP INTO 3 SEPARATE FUNCTIONS key::Val{OptKey}; my_kwarg::Bool = true # ADD KEY ARGS AS NEEDED ) @@ -172,7 +172,7 @@ end # If appropriate extend expression_supports (enables support queries of expressions) function InfiniteOpt.expression_supports( model::JuMP.Model, - expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, NLPExpr}, + expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr}, key::Val{OptKey}; my_kwarg::Bool = true # ADD KEY ARGS AS NEEDED ) diff --git a/test/measure_expansions.jl b/test/measure_expansions.jl index 2bf7e4d8d..ab1eedf41 100644 --- a/test/measure_expansions.jl +++ b/test/measure_expansions.jl @@ -458,8 +458,8 @@ end expected = x + 2 * x + 4 @test isequal_canonical(InfiniteOpt.expand_measure(expr, data3, m), expected) end - # test expand_measure (NLPExpr univariate) - @testset "NLPExpr (1D DiscreteMeasureData)" begin + # test expand_measure (NonlinearExpr univariate) + @testset "NonlinearExpr (1D DiscreteMeasureData)" begin # test simple expr = sin(inf1) expected = 0.5 * sin(inf1(1)) + 0.5 * sin(inf1(2)) @@ -469,8 +469,8 @@ end expected = 0.5 * (sin(inf1(1)) + 1) + 0.5 * (sin(inf1(2)) + 2) @test isequal(expand_measure(expr, data1, m), expected) end - # test expand_measure (NLPExpr multivariate) - @testset "NLPExpr (Multi DiscreteMeasureData)" begin + # test expand_measure (NonlinearExpr multivariate) + @testset "NonlinearExpr (Multi DiscreteMeasureData)" begin # test simple expr = sin(inf5) expected = 1 * sin(inf5([1, 1], pars2)) + 1 * sin(inf5([2, 2], pars2)) diff --git a/test/results.jl b/test/results.jl index a175d233d..693862094 100644 --- a/test/results.jl +++ b/test/results.jl @@ -259,11 +259,11 @@ end @test value(meas1, label = All) == 4. @test value(meas2, label = UserDefined) == [0., -3.] @test value(3g - 1) == 2. - @test value(inf^2 + g - 2) == [3., -1.] - @test value(inf^2 + g - 2, ndarray = true) == [3., -1.] + @test value(inf * inf + g - 2) == [3., -1.] + @test value(inf * inf + g - 2, ndarray = true) == [3., -1.] @test value(zero(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) - 42) == -42. - @test value(sin(g)) == sin(1) - @test_throws ErrorException value(zero(NLPExpr)) + # @test value(sin(g)) == sin(1) + # @test_throws ErrorException value(NonlinearExpr(:sin, Any[0])) end # test dual @testset "JuMP.dual" begin @@ -290,10 +290,12 @@ end gt = transcription_variable(g) c1t = transcription_constraint(c1) c2t = transcription_constraint(c2) + c3t = transcription_constraint(c3) + c4t = transcription_constraint(c4) # setup optimizer info mockoptimizer = JuMP.backend(tm).optimizer.model - block = MOI.get(tm, MOI.NLPBlock()) - MOI.initialize(block.evaluator, Symbol[]) + # block = MOI.get(tm, MOI.NLPBlock()) + # MOI.initialize(block.evaluator, Symbol[]) MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL) MOI.set(mockoptimizer, MOI.ResultCount(), 1) MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) @@ -304,7 +306,10 @@ end MOI.set(mockoptimizer, MOI.ConstraintDual(), JuMP.optimizer_index(c1t), -1.0) MOI.set(mockoptimizer, MOI.ConstraintDual(), JuMP.optimizer_index(c2t[1]), 0.0) MOI.set(mockoptimizer, MOI.ConstraintDual(), JuMP.optimizer_index(c2t[2]), 1.0) - MOI.set(mockoptimizer, MOI.NLPBlockDual(1), [4.0, 2., 3.]) + MOI.set(mockoptimizer, MOI.ConstraintDual(), JuMP.optimizer_index(c3t), 4.0) + MOI.set(mockoptimizer, MOI.ConstraintDual(), JuMP.optimizer_index(c4t[1]), 2.0) + MOI.set(mockoptimizer, MOI.ConstraintDual(), JuMP.optimizer_index(c4t[2]), 3.0) + # MOI.set(mockoptimizer, MOI.NLPBlockDual(1), [4.0, 2., 3.]) # test map_value @testset "map_value" begin @test InfiniteOpt.map_value(c1, Val(:TransData), 1) == 1. @@ -315,8 +320,8 @@ end @test value(c1) == 1. @test value(c2, label = UserDefined) == [-1., 0.] @test value(c2, label = UserDefined, ndarray = true) == [-1., 0.] - @test value(c3) == sin(1) - @test value(c4) == [sin(-1), sin(0)] + # @test value(c3) == sin(1) + # @test value(c4) == [sin(-1), sin(0)] end # test map_optimizer_index @testset "map_optimizer_index" begin @@ -328,7 +333,8 @@ end @test isa(optimizer_index(c1), MOI.ConstraintIndex) @test isa(optimizer_index(c2, label = All), Vector{<:MOI.ConstraintIndex}) @test isa(optimizer_index(c2, label = All, ndarray = true), Vector{<:MOI.ConstraintIndex}) - @test_throws ErrorException optimizer_index(c3) + @test isa(optimizer_index(c3), MOI.ConstraintIndex) + @test isa(optimizer_index(c4, label = All), Vector{<:MOI.ConstraintIndex}) end # test has_values @testset "JuMP.has_duals" begin diff --git a/test/runtests.jl b/test/runtests.jl index 1e994013f..51f84658d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,5 @@ -import Pkg -Pkg.pkg"add JuMP#od/nlp-expr MathOptInterface#od/nlp-expr" +# import Pkg +# Pkg.pkg"add JuMP#od/nlp-expr MathOptInterface#od/nlp-expr" using InfiniteOpt: _domain_or_error using Test: Error @@ -82,27 +82,27 @@ println("") println("") @time @testset "Constraint Methods" begin include("constraints.jl") end println("") -# @time @testset "Printing Methods" begin include("show.jl") end -# println("") -# @time @testset "Deletion Methods" begin include("deletion.jl") end -# println("") -# @time @testset "Expansion Methods" begin include("measure_expansions.jl") end -# println("") -# @time @testset "Derivative Evaluation" begin include("derivative_evaluation.jl") end -# println("") -# @time @testset "TranscriptionOpt" begin -# @testset "Model" begin include("TranscriptionOpt/model.jl") end -# @testset "Measures" begin include("TranscriptionOpt/measure.jl") end -# @testset "Transcribe" begin include("TranscriptionOpt/transcribe.jl") end -# @testset "Optimize" begin include("TranscriptionOpt/optimize.jl") end -# end -# println("") -# @time @testset "Solution Methods" begin include("optimizer.jl") end -# println("") -# @time @testset "Solution Queries" begin include("results.jl") end -# println("") -# @time @testset "Extensions" begin include("extensions.jl") end -# println("") +@time @testset "Printing Methods" begin include("show.jl") end +println("") +@time @testset "Deletion Methods" begin include("deletion.jl") end +println("") +@time @testset "Expansion Methods" begin include("measure_expansions.jl") end +println("") +@time @testset "Derivative Evaluation" begin include("derivative_evaluation.jl") end +println("") +@time @testset "TranscriptionOpt" begin + @testset "Model" begin include("TranscriptionOpt/model.jl") end + @testset "Measures" begin include("TranscriptionOpt/measure.jl") end + @testset "Transcribe" begin include("TranscriptionOpt/transcribe.jl") end + @testset "Optimize" begin include("TranscriptionOpt/optimize.jl") end +end +println("") +@time @testset "Solution Methods" begin include("optimizer.jl") end +println("") +@time @testset "Solution Queries" begin include("results.jl") end +println("") +@time @testset "Extensions" begin include("extensions.jl") end +println("") println("----------------------------------------------------------------------------") println("-----------------------------TESTING COMPLETE!------------------------------") println("----------------------------------------------------------------------------") From dbf1063c2ac4a2bdb7efdd9d5051d6cbcde3e228 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Fri, 19 May 2023 10:28:17 -0400 Subject: [PATCH 08/40] Update nlp types --- src/expressions.jl | 37 +++++++++++++++++++------------------ src/results.jl | 6 +++--- test/expressions.jl | 2 +- test/results.jl | 8 ++++---- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/expressions.jl b/src/expressions.jl index e09163653..2f40eec49 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -776,25 +776,26 @@ function map_expression(transform::Function, quad::JuMP.GenericQuadExpr) map_expression(transform, quad.aff)) end -# NonlinearExpr (avoid recursion to handle deeply nested expressions) +# NonlinearExpr function map_expression(transform::Function, nlp::JuMP.NonlinearExpr) - stack = Tuple{Vector{Any}, Vector{Any}}[] - new_nlp = JuMP.NonlinearExpr(nlp.head, Any[]) - push!(stack, (nlp.args, new_nlp.args)) - while !isempty(stack) - args, cloned = pop!(stack) - for arg in args - if arg isa JuMP.NonlinearExpr - new_expr = JuMP.NonlinearExpr(arg.head, Any[]) - push!(stack, (arg.args, new_expr.args)) - else - new_expr = map_expression(transform, arg) - end - push!(cloned, new_expr) - end - end - return new_nlp - # return JuMP.NonlinearExpr(nlp.head, Any[map_expression(transform, arg) for arg in nlp.args]) # this recursion is still more efficient... + # TODO: Figure out how to make the recursionless code work + # stack = Tuple{Vector{Any}, Vector{Any}}[] + # new_nlp = JuMP.NonlinearExpr{NewVrefType}(nlp.head, Any[]) # how to get `NewVrefType`? + # push!(stack, (nlp.args, new_nlp.args)) + # while !isempty(stack) + # args, cloned = pop!(stack) + # for arg in args + # if arg isa JuMP.NonlinearExpr + # new_expr = JuMP.NonlinearExpr{NewVrefType}(arg.head, Any[]) + # push!(stack, (arg.args, new_expr.args)) + # else + # new_expr = map_expression(transform, arg) + # end + # push!(cloned, new_expr) + # end + # end + # return new_nlp + return JuMP.NonlinearExpr(nlp.head, Any[map_expression(transform, arg) for arg in nlp.args]) end ################################################################################ diff --git a/src/results.jl b/src/results.jl index 40e50701a..25711b5c6 100644 --- a/src/results.jl +++ b/src/results.jl @@ -239,7 +239,7 @@ julia> value(my_infinite_expr) ``` """ function JuMP.value( - expr::Union{JuMP.GenericAffExpr{Float64, GeneralVariableRef}, JuMP.GenericQuadExpr{Float64, GeneralVariableRef}}; # TODO fix + expr::Union{JuMP.GenericAffExpr{Float64, GeneralVariableRef}, JuMP.GenericQuadExpr{Float64, GeneralVariableRef}, JuMP.NonlinearExpr{GeneralVariableRef}}; result::Int = 1, kwargs... ) @@ -247,8 +247,8 @@ function JuMP.value( model = _model_from_expr(expr) # if no model then the expression only contains a constant if isnothing(model) - # expr isa JuMP.NonlinearExpr && error("Cannot evaluate the value of `$expr`,", - # "because it doesn't have variables.") + expr isa JuMP.NonlinearExpr && error("Cannot evaluate the value of `$expr`,", + "because it doesn't have variables.") return JuMP.constant(expr) # otherwise let's call map_value else diff --git a/test/expressions.jl b/test/expressions.jl index f7df49edf..05e2439e8 100644 --- a/test/expressions.jl +++ b/test/expressions.jl @@ -599,7 +599,7 @@ end @testset "NonlinearExpr" begin # make expressions nlp1 = sin(hd) - nlp2 = NonlinearExpr(:sin, Any[0.0]) + nlp2 = NonlinearExpr{GeneralVariableRef}(:sin, Any[0.0]) nlp3 = 2 + sin(hd^2) # test expressions @test InfiniteOpt._model_from_expr(nlp1) === m diff --git a/test/results.jl b/test/results.jl index 693862094..d3f33cf3a 100644 --- a/test/results.jl +++ b/test/results.jl @@ -262,8 +262,8 @@ end @test value(inf * inf + g - 2) == [3., -1.] @test value(inf * inf + g - 2, ndarray = true) == [3., -1.] @test value(zero(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) - 42) == -42. - # @test value(sin(g)) == sin(1) - # @test_throws ErrorException value(NonlinearExpr(:sin, Any[0])) + @test value(sin(g)) == sin(1) + @test_throws ErrorException value(NonlinearExpr{GeneralVariableRef}(:sin, Any[0])) end # test dual @testset "JuMP.dual" begin @@ -320,8 +320,8 @@ end @test value(c1) == 1. @test value(c2, label = UserDefined) == [-1., 0.] @test value(c2, label = UserDefined, ndarray = true) == [-1., 0.] - # @test value(c3) == sin(1) - # @test value(c4) == [sin(-1), sin(0)] + @test value(c3) == sin(1) + @test value(c4) == [sin(-1), sin(0)] end # test map_optimizer_index @testset "map_optimizer_index" begin From d4615e6a4c065a80f0c82b088e0b9ce2841a2091 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Mon, 22 May 2023 16:59:42 -0400 Subject: [PATCH 09/40] added comment --- src/expressions.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/expressions.jl b/src/expressions.jl index 2f40eec49..71af84c3a 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -780,7 +780,7 @@ end function map_expression(transform::Function, nlp::JuMP.NonlinearExpr) # TODO: Figure out how to make the recursionless code work # stack = Tuple{Vector{Any}, Vector{Any}}[] - # new_nlp = JuMP.NonlinearExpr{NewVrefType}(nlp.head, Any[]) # how to get `NewVrefType`? + # new_nlp = JuMP.NonlinearExpr{NewVrefType}(nlp.head, Any[]) # Need to add `NewVrefType` arg throughout pkg # push!(stack, (nlp.args, new_nlp.args)) # while !isempty(stack) # args, cloned = pop!(stack) From 21885f06145110551aed1cdcfc4e1a3be1f3efb1 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Fri, 2 Jun 2023 11:16:07 -0400 Subject: [PATCH 10/40] Update package dependencies --- docs/Project.toml | 2 +- test/runtests.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index e4ad066f9..8494efebe 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -16,7 +16,7 @@ HiGHS = "1" Distributions = "0.25" Documenter = "0.27" InfiniteOpt = "0.5" -Ipopt = "1" +Ipopt = "1.4" Literate = "2.9" Plots = "1" julia = "1.6" diff --git a/test/runtests.jl b/test/runtests.jl index 51f84658d..12a59d81a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,5 @@ -# import Pkg -# Pkg.pkg"add JuMP#od/nlp-expr MathOptInterface#od/nlp-expr" +import Pkg +Pkg.pkg"add JuMP#od/nlp-expr MathOptInterface@1.17" using InfiniteOpt: _domain_or_error using Test: Error From 0c0518fcf97151cc7e70105ec6e6eac93612f6cb Mon Sep 17 00:00:00 2001 From: pulsipher Date: Wed, 21 Jun 2023 12:37:59 -0400 Subject: [PATCH 11/40] incorporate `JuMP.flatten` --- src/derivatives.jl | 6 +++-- src/expressions.jl | 11 +++++---- src/measure_expansions.jl | 48 +++++++++++++++++++++++---------------- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/derivatives.jl b/src/derivatives.jl index 0a41bd696..48c96c823 100644 --- a/src/derivatives.jl +++ b/src/derivatives.jl @@ -382,7 +382,8 @@ end # AffExpr function _build_deriv_expr(aff::JuMP.GenericAffExpr, pref) return _MA.@rewrite(sum(c * _build_deriv_expr(v, pref) - for (c, v) in JuMP.linear_terms(aff))) + for (c, v) in JuMP.linear_terms(aff)); + move_factors_into_sums = false) end # Quad Expr (implements product rule) @@ -390,7 +391,8 @@ function _build_deriv_expr(quad::JuMP.GenericQuadExpr, pref) return _MA.@rewrite(sum(c * (_build_deriv_expr(v1, pref) * v2 + v1 * _build_deriv_expr(v2, pref)) for (c, v1, v2) in JuMP.quad_terms(quad)) + - _build_deriv_expr(quad.aff, pref)) + _build_deriv_expr(quad.aff, pref); + move_factors_into_sums = false) end # Real number diff --git a/src/expressions.jl b/src/expressions.jl index 71af84c3a..ca6c0ffab 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -764,16 +764,19 @@ end # AffExpr function map_expression(transform::Function, aff::JuMP.GenericAffExpr) - return _MA.@rewrite(sum(c * transform(v) + # flatten needed in case expression becomes nonlinear with transform + return JuMP.flatten(_MA.@rewrite(sum(c * transform(v) for (c, v) in JuMP.linear_terms(aff)) + - JuMP.constant(aff)) + JuMP.constant(aff); move_factors_into_sums = false)) end # QuadExpr function map_expression(transform::Function, quad::JuMP.GenericQuadExpr) - return _MA.@rewrite(sum(c * transform(v1) * transform(v2) + # flatten needed in case expression becomes nonlinear with transform + return JuMP.flatten(_MA.@rewrite(sum(c * transform(v1) * transform(v2) for (c, v1, v2) in JuMP.quad_terms(quad)) + - map_expression(transform, quad.aff)) + map_expression(transform, quad.aff); + move_factors_into_sums = false)) end # NonlinearExpr diff --git a/src/measure_expansions.jl b/src/measure_expansions.jl index 8703cb89d..3dcd450c3 100644 --- a/src/measure_expansions.jl +++ b/src/measure_expansions.jl @@ -243,13 +243,13 @@ function expand_measure(ivref::GeneralVariableRef, elseif length(var_prefs) == 1 return _MA.@rewrite(sum(coeffs[i] * w(supps[i]) * make_point_variable_ref(write_model, ivref, [supps[i]]) - for i in eachindex(coeffs))) + for i in eachindex(coeffs)); move_factors_into_sums = false) # make semi-infinite variables if the variable contains other parameters else index = [findfirst(isequal(pref), var_prefs)] return _MA.@rewrite(sum(coeffs[i] * w(supps[i]) * make_semi_infinite_variable_ref(write_model, ivref, index, [supps[i]]) - for i in eachindex(coeffs))) + for i in eachindex(coeffs)); move_factors_into_sums = false) end end @@ -268,7 +268,7 @@ function expand_measure(ivref::GeneralVariableRef, if isequal(var_prefs, prefs) return _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * make_point_variable_ref(write_model, ivref, supps[:, i]) - for i in eachindex(coeffs))) + for i in eachindex(coeffs)); move_factors_into_sums = false) # treat variable as constant if doesn't have measure parameter elseif !any(any(isequal(pref), var_prefs) for pref in prefs) var_coef = sum(coeffs[i] * w(supps[:, i]) for i in eachindex(coeffs)) @@ -279,7 +279,7 @@ function expand_measure(ivref::GeneralVariableRef, new_supps = supps[indices, :] return _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * make_point_variable_ref(write_model, ivref, new_supps[:, i]) - for i in eachindex(coeffs))) + for i in eachindex(coeffs)); move_factors_into_sums = false) # make semi-infinite variables if the variable contains other parameters else # get indices of each pref to map properly @@ -292,7 +292,7 @@ function expand_measure(ivref::GeneralVariableRef, end return _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * make_semi_infinite_variable_ref(write_model, ivref, indices, supps[:, i]) - for i in eachindex(coeffs))) + for i in eachindex(coeffs)); move_factors_into_sums = false) end end @@ -331,7 +331,7 @@ function expand_measure(rvref::GeneralVariableRef, expr = _MA.@rewrite(sum(coeffs[i] * w(supps[i]) * make_point_variable_ref(write_model, ivref, _make_point_support(orig_prefs, eval_supps, index, supps[i])) - for i in eachindex(coeffs))) + for i in eachindex(coeffs)); move_factors_into_sums = false) # make semi-infinite variables if the variable contains other parameters else index = findfirst(isequal(pref), orig_prefs) @@ -340,7 +340,7 @@ function expand_measure(rvref::GeneralVariableRef, indices = push!(collected_indices, index) expr = _MA.@rewrite(sum(coeffs[i] * w(supps[i]) * make_semi_infinite_variable_ref(write_model, ivref, indices, vcat(vals, supps[i])) - for i in eachindex(coeffs))) + for i in eachindex(coeffs)); move_factors_into_sums = false) end return expr end @@ -389,7 +389,7 @@ function expand_measure(rvref::GeneralVariableRef, expr = _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * make_point_variable_ref(write_model, ivref, _make_point_support(orig_prefs, eval_supps, indices, supps[:, i])) - for i in eachindex(coeffs))) + for i in eachindex(coeffs)); move_factors_into_sums = false) # make semi-infinite variables if the variable contains other parameters else # get the indices of prefs in terms of the ivref @@ -407,7 +407,7 @@ function expand_measure(rvref::GeneralVariableRef, # make the expression expr = _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * make_semi_infinite_variable_ref(write_model, ivref, indices, vcat(vals, supps[:, i])) - for i in eachindex(coeffs))) + for i in eachindex(coeffs)); move_factors_into_sums = false) end return expr end @@ -497,8 +497,10 @@ function expand_measure(expr::JuMP.GenericAffExpr{C, GeneralVariableRef}, w = weight_function(data) # expand each variable independently and add all together constant_coef = sum(coeffs[i] * w(supps[i]) for i in eachindex(coeffs)) - return _MA.@rewrite(sum(coef * expand_measure(var, data, write_model) - for (var, coef) in expr.terms) + expr.constant * constant_coef) + new_ex = _MA.@rewrite(sum(coef * expand_measure(var, data, write_model) + for (var, coef) in expr.terms) + expr.constant * constant_coef; + move_factors_into_sums = false) + return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr end # GenericAffExpr (Multi DiscreteMeasureData) @@ -512,8 +514,10 @@ function expand_measure(expr::JuMP.GenericAffExpr{C, GeneralVariableRef}, w = weight_function(data) # expand each variable independently and add all together constant_coef = sum(coeffs[i] * w(supps[:, i]) for i in eachindex(coeffs)) - return _MA.@rewrite(sum(coef * expand_measure(var, data, write_model) - for (var, coef) in expr.terms) + expr.constant * constant_coef) + new_ex = _MA.@rewrite(sum(coef * expand_measure(var, data, write_model) + for (var, coef) in expr.terms) + expr.constant * constant_coef; + move_factors_into_sums = false) + return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr end # GenericQuadExpr (1D DiscreteMeasureData) @@ -534,11 +538,13 @@ function expand_measure( # make the expression simple_data = DiscreteMeasureData(pref, ones(1), ones(1), label, default_weight, lb, ub, is_expect) - return _MA.@rewrite(sum(sum(coeffs[i] * w(supps[i]) * c * + new_ex = _MA.@rewrite(sum(sum(coeffs[i] * w(supps[i]) * c * _map_variable(p.a, simple_data, supps[i], write_model) * _map_variable(p.b, simple_data, supps[i], write_model) for (p, c) in expr.terms) for i in eachindex(coeffs)) + - expand_measure(expr.aff, data, write_model)) + expand_measure(expr.aff, data, write_model); + move_factors_into_sums = false) + return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr end # GenericQuadExpr(Multi DiscreteMeasureData) @@ -559,11 +565,13 @@ function expand_measure( # make the expression simple_data = DiscreteMeasureData(prefs, ones(1), ones(length(prefs), 1), label, default_weight, lbs, ubs, is_expect) - return _MA.@rewrite(sum(sum(coeffs[i] * w(@view(supps[:, i])) * c * + new_ex = _MA.@rewrite(sum(sum(coeffs[i] * w(@view(supps[:, i])) * c * _map_variable(p.a, simple_data, @view(supps[:, i]), write_model) * _map_variable(p.b, simple_data, @view(supps[:, i]), write_model) for (p, c) in expr.terms) for i in eachindex(coeffs)) + - expand_measure(expr.aff, data, write_model)) + expand_measure(expr.aff, data, write_model); + move_factors_into_sums = false) + return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr end # NonlinearExpr (1D DiscreteMeasureData) @@ -584,9 +592,10 @@ function expand_measure( # make the expression simple_data = DiscreteMeasureData(pref, ones(1), ones(1), label, default_weight, lb, ub, is_expect) - return sum(coeffs[i] * w(supps[i]) * + new_ex = sum(coeffs[i] * w(supps[i]) * map_expression(v -> _map_variable(v, simple_data, supps[i], write_model), expr) for i in eachindex(supps)) + return JuMP.flatten(new_ex) # make expression flat over summation end # NonlinearExpr (Multi DiscreteMeasureData) @@ -607,9 +616,10 @@ function expand_measure( # make the expression simple_data = DiscreteMeasureData(prefs, ones(1), ones(length(prefs), 1), label, default_weight, lbs, ubs, is_expect) - return sum(coeffs[i] * w(@view(supps[:, i])) * + new_ex = sum(coeffs[i] * w(@view(supps[:, i])) * map_expression(v -> _map_variable(v, simple_data, @view(supps[:, i]), write_model), expr) for i in eachindex(coeffs)) + return JuMP.flatten(new_ex) # make expression flat over summation end From cb6dd6ae40fe0144b1dd75da0739ea6e305d5357 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Wed, 21 Jun 2023 13:03:50 -0400 Subject: [PATCH 12/40] fix @rewrite --- src/derivatives.jl | 4 ++-- src/expressions.jl | 4 ++-- src/measure_expansions.jl | 26 +++++++++++++------------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/derivatives.jl b/src/derivatives.jl index 48c96c823..9ae85feb1 100644 --- a/src/derivatives.jl +++ b/src/derivatives.jl @@ -382,7 +382,7 @@ end # AffExpr function _build_deriv_expr(aff::JuMP.GenericAffExpr, pref) return _MA.@rewrite(sum(c * _build_deriv_expr(v, pref) - for (c, v) in JuMP.linear_terms(aff)); + for (c, v) in JuMP.linear_terms(aff)), move_factors_into_sums = false) end @@ -391,7 +391,7 @@ function _build_deriv_expr(quad::JuMP.GenericQuadExpr, pref) return _MA.@rewrite(sum(c * (_build_deriv_expr(v1, pref) * v2 + v1 * _build_deriv_expr(v2, pref)) for (c, v1, v2) in JuMP.quad_terms(quad)) + - _build_deriv_expr(quad.aff, pref); + _build_deriv_expr(quad.aff, pref), move_factors_into_sums = false) end diff --git a/src/expressions.jl b/src/expressions.jl index ca6c0ffab..9be726cc4 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -767,7 +767,7 @@ function map_expression(transform::Function, aff::JuMP.GenericAffExpr) # flatten needed in case expression becomes nonlinear with transform return JuMP.flatten(_MA.@rewrite(sum(c * transform(v) for (c, v) in JuMP.linear_terms(aff)) + - JuMP.constant(aff); move_factors_into_sums = false)) + JuMP.constant(aff), move_factors_into_sums = false)) end # QuadExpr @@ -775,7 +775,7 @@ function map_expression(transform::Function, quad::JuMP.GenericQuadExpr) # flatten needed in case expression becomes nonlinear with transform return JuMP.flatten(_MA.@rewrite(sum(c * transform(v1) * transform(v2) for (c, v1, v2) in JuMP.quad_terms(quad)) + - map_expression(transform, quad.aff); + map_expression(transform, quad.aff), move_factors_into_sums = false)) end diff --git a/src/measure_expansions.jl b/src/measure_expansions.jl index 3dcd450c3..3f40b8839 100644 --- a/src/measure_expansions.jl +++ b/src/measure_expansions.jl @@ -243,13 +243,13 @@ function expand_measure(ivref::GeneralVariableRef, elseif length(var_prefs) == 1 return _MA.@rewrite(sum(coeffs[i] * w(supps[i]) * make_point_variable_ref(write_model, ivref, [supps[i]]) - for i in eachindex(coeffs)); move_factors_into_sums = false) + for i in eachindex(coeffs)), move_factors_into_sums = false) # make semi-infinite variables if the variable contains other parameters else index = [findfirst(isequal(pref), var_prefs)] return _MA.@rewrite(sum(coeffs[i] * w(supps[i]) * make_semi_infinite_variable_ref(write_model, ivref, index, [supps[i]]) - for i in eachindex(coeffs)); move_factors_into_sums = false) + for i in eachindex(coeffs)), move_factors_into_sums = false) end end @@ -268,7 +268,7 @@ function expand_measure(ivref::GeneralVariableRef, if isequal(var_prefs, prefs) return _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * make_point_variable_ref(write_model, ivref, supps[:, i]) - for i in eachindex(coeffs)); move_factors_into_sums = false) + for i in eachindex(coeffs)), move_factors_into_sums = false) # treat variable as constant if doesn't have measure parameter elseif !any(any(isequal(pref), var_prefs) for pref in prefs) var_coef = sum(coeffs[i] * w(supps[:, i]) for i in eachindex(coeffs)) @@ -279,7 +279,7 @@ function expand_measure(ivref::GeneralVariableRef, new_supps = supps[indices, :] return _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * make_point_variable_ref(write_model, ivref, new_supps[:, i]) - for i in eachindex(coeffs)); move_factors_into_sums = false) + for i in eachindex(coeffs)), move_factors_into_sums = false) # make semi-infinite variables if the variable contains other parameters else # get indices of each pref to map properly @@ -292,7 +292,7 @@ function expand_measure(ivref::GeneralVariableRef, end return _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * make_semi_infinite_variable_ref(write_model, ivref, indices, supps[:, i]) - for i in eachindex(coeffs)); move_factors_into_sums = false) + for i in eachindex(coeffs)), move_factors_into_sums = false) end end @@ -331,7 +331,7 @@ function expand_measure(rvref::GeneralVariableRef, expr = _MA.@rewrite(sum(coeffs[i] * w(supps[i]) * make_point_variable_ref(write_model, ivref, _make_point_support(orig_prefs, eval_supps, index, supps[i])) - for i in eachindex(coeffs)); move_factors_into_sums = false) + for i in eachindex(coeffs)), move_factors_into_sums = false) # make semi-infinite variables if the variable contains other parameters else index = findfirst(isequal(pref), orig_prefs) @@ -340,7 +340,7 @@ function expand_measure(rvref::GeneralVariableRef, indices = push!(collected_indices, index) expr = _MA.@rewrite(sum(coeffs[i] * w(supps[i]) * make_semi_infinite_variable_ref(write_model, ivref, indices, vcat(vals, supps[i])) - for i in eachindex(coeffs)); move_factors_into_sums = false) + for i in eachindex(coeffs)), move_factors_into_sums = false) end return expr end @@ -389,7 +389,7 @@ function expand_measure(rvref::GeneralVariableRef, expr = _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * make_point_variable_ref(write_model, ivref, _make_point_support(orig_prefs, eval_supps, indices, supps[:, i])) - for i in eachindex(coeffs)); move_factors_into_sums = false) + for i in eachindex(coeffs)), move_factors_into_sums = false) # make semi-infinite variables if the variable contains other parameters else # get the indices of prefs in terms of the ivref @@ -407,7 +407,7 @@ function expand_measure(rvref::GeneralVariableRef, # make the expression expr = _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * make_semi_infinite_variable_ref(write_model, ivref, indices, vcat(vals, supps[:, i])) - for i in eachindex(coeffs)); move_factors_into_sums = false) + for i in eachindex(coeffs)), move_factors_into_sums = false) end return expr end @@ -498,7 +498,7 @@ function expand_measure(expr::JuMP.GenericAffExpr{C, GeneralVariableRef}, # expand each variable independently and add all together constant_coef = sum(coeffs[i] * w(supps[i]) for i in eachindex(coeffs)) new_ex = _MA.@rewrite(sum(coef * expand_measure(var, data, write_model) - for (var, coef) in expr.terms) + expr.constant * constant_coef; + for (var, coef) in expr.terms) + expr.constant * constant_coef, move_factors_into_sums = false) return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr end @@ -515,7 +515,7 @@ function expand_measure(expr::JuMP.GenericAffExpr{C, GeneralVariableRef}, # expand each variable independently and add all together constant_coef = sum(coeffs[i] * w(supps[:, i]) for i in eachindex(coeffs)) new_ex = _MA.@rewrite(sum(coef * expand_measure(var, data, write_model) - for (var, coef) in expr.terms) + expr.constant * constant_coef; + for (var, coef) in expr.terms) + expr.constant * constant_coef, move_factors_into_sums = false) return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr end @@ -542,7 +542,7 @@ function expand_measure( _map_variable(p.a, simple_data, supps[i], write_model) * _map_variable(p.b, simple_data, supps[i], write_model) for (p, c) in expr.terms) for i in eachindex(coeffs)) + - expand_measure(expr.aff, data, write_model); + expand_measure(expr.aff, data, write_model), move_factors_into_sums = false) return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr end @@ -569,7 +569,7 @@ function expand_measure( _map_variable(p.a, simple_data, @view(supps[:, i]), write_model) * _map_variable(p.b, simple_data, @view(supps[:, i]), write_model) for (p, c) in expr.terms) for i in eachindex(coeffs)) + - expand_measure(expr.aff, data, write_model); + expand_measure(expr.aff, data, write_model), move_factors_into_sums = false) return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr end From 6d3af168855de609845a0cc22e4b564cd7a96069 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Wed, 16 Aug 2023 17:16:28 -0400 Subject: [PATCH 13/40] Update w/ JuMP --- src/TranscriptionOpt/model.jl | 6 ++--- src/TranscriptionOpt/transcribe.jl | 39 +++++++++-------------------- src/expressions.jl | 26 +++++++++---------- src/measure_expansions.jl | 4 +-- src/results.jl | 5 ++-- test/TranscriptionOpt/transcribe.jl | 2 +- test/deletion.jl | 2 +- test/expressions.jl | 28 ++++++++++----------- test/measure_expansions.jl | 4 +-- test/nlp.jl | 2 +- test/operators.jl | 4 +-- test/results.jl | 2 +- test/utilities.jl | 2 +- 13 files changed, 55 insertions(+), 71 deletions(-) diff --git a/src/TranscriptionOpt/model.jl b/src/TranscriptionOpt/model.jl index 70de1aadc..ced6469d2 100644 --- a/src/TranscriptionOpt/model.jl +++ b/src/TranscriptionOpt/model.jl @@ -638,7 +638,7 @@ x(support: 1) - y """ function transcription_expression( model::JuMP.Model, - expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr}; + expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.GenericNonlinearExpr}; label::Type{<:InfiniteOpt.AbstractSupportLabel} = InfiniteOpt.PublicLabel, ndarray::Bool = false ) @@ -700,7 +700,7 @@ Proper extension of [`InfiniteOpt.optimizer_model_expression`](@ref) for `TranscriptionModel`s. This simply dispatches to [`transcription_expression`](@ref). """ function InfiniteOpt.optimizer_model_expression( - expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr}, + expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.GenericNonlinearExpr}, ::Val{:TransData}; label::Type{<:InfiniteOpt.AbstractSupportLabel} = InfiniteOpt.PublicLabel, ndarray::Bool = false) @@ -719,7 +719,7 @@ be transcribed. """ function InfiniteOpt.expression_supports( model::JuMP.Model, - expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr}, + expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.GenericNonlinearExpr}, key::Val{:TransData} = Val(:TransData); label::Type{<:InfiniteOpt.AbstractSupportLabel} = InfiniteOpt.PublicLabel, ndarray::Bool = false diff --git a/src/TranscriptionOpt/transcribe.jl b/src/TranscriptionOpt/transcribe.jl index 24fce9dfe..0e2fdc433 100644 --- a/src/TranscriptionOpt/transcribe.jl +++ b/src/TranscriptionOpt/transcribe.jl @@ -484,7 +484,7 @@ end # AffExpr and QuadExpr and NonlinearExpr function transcription_expression( trans_model::JuMP.Model, - expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr}, + expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.GenericNonlinearExpr}, support::Vector{Float64} ) return InfiniteOpt.map_expression( @@ -557,17 +557,6 @@ end ################################################################################ # OBJECTIVE TRANSCRIPTION METHODS ################################################################################ -## Dispatch functions for setting the objective -# Normal Expr -function _set_objective(trans_model, sense, expr) - return JuMP.set_objective(trans_model, sense, expr) -end - -# NonlinearExpression -function _set_objective(trans_model, sense, expr::JuMP.NonlinearExpression) - return JuMP.set_nonlinear_objective(trans_model, sense, expr) -end - """ transcribe_objective!(trans_model::JuMP.Model, inf_model::InfiniteOpt.InfiniteModel)::Nothing @@ -579,11 +568,11 @@ by transcripted first (e.g., via [`transcribe_infinite_variables!`](@ref)). function transcribe_objective!( trans_model::JuMP.Model, inf_model::InfiniteOpt.InfiniteModel - )::Nothing + ) expr = JuMP.objective_function(inf_model) sense = JuMP.objective_sense(inf_model) trans_expr = transcription_expression(trans_model, expr, Float64[]) - _set_objective(trans_model, sense, trans_expr) + JuMP.set_objective(trans_model, sense, trans_expr) return end @@ -597,7 +586,7 @@ function _get_info_constr_from_var( vref::InfiniteOpt.GeneralVariableRef, set::MOI.GreaterThan, support::Vector{Float64} - )::Union{JuMP.ConstraintRef, Nothing} + ) trans_vref = transcription_expression(trans_model, vref, support) return JuMP.has_lower_bound(trans_vref) ? JuMP.LowerBoundRef(trans_vref) : nothing end @@ -608,7 +597,7 @@ function _get_info_constr_from_var( vref::InfiniteOpt.GeneralVariableRef, set::MOI.LessThan, support::Vector{Float64} - )::Union{JuMP.ConstraintRef, Nothing} + ) trans_vref = transcription_expression(trans_model, vref, support) return JuMP.has_upper_bound(trans_vref) ? JuMP.UpperBoundRef(trans_vref) : nothing end @@ -619,7 +608,7 @@ function _get_info_constr_from_var( vref::InfiniteOpt.GeneralVariableRef, set::MOI.EqualTo, support::Vector{Float64} - )::Union{JuMP.ConstraintRef, Nothing} + ) trans_vref = transcription_expression(trans_model, vref, support) return JuMP.is_fixed(trans_vref) ? JuMP.FixRef(trans_vref) : nothing end @@ -630,7 +619,7 @@ function _get_info_constr_from_var( vref::InfiniteOpt.GeneralVariableRef, set::MOI.ZeroOne, support::Vector{Float64} - )::Union{JuMP.ConstraintRef, Nothing} + ) trans_vref = transcription_expression(trans_model, vref, support) return JuMP.is_binary(trans_vref) ? JuMP.BinaryRef(trans_vref) : nothing end @@ -641,7 +630,7 @@ function _get_info_constr_from_var( vref::InfiniteOpt.GeneralVariableRef, set::MOI.Integer, support::Vector{Float64} - )::Union{JuMP.ConstraintRef, Nothing} + ) trans_vref = transcription_expression(trans_model, vref, support) return JuMP.is_integer(trans_vref) ? JuMP.IntegerRef(trans_vref) : nothing end @@ -651,7 +640,7 @@ function _support_in_restrictions( support::Vector{Float64}, indices::Vector{Int}, domains::Vector{InfiniteOpt.IntervalDomain} - )::Bool + ) for i in eachindex(indices) s = support[indices[i]] if !isnan(s) && (s < JuMP.lower_bound(domains[i]) || @@ -688,10 +677,6 @@ function _process_constraint( name::String ) new_func = map(f -> transcription_expression(trans_model, f, raw_supp), func) - if any(f -> f isa JuMP.NonlinearExpr, new_func) - error("TranscriptionOpt does not support vector constraints of general " * - "nonlinear expressions because this is not yet supported by JuMP.") - end shape = JuMP.shape(constr) shaped_func = JuMP.reshape_vector(new_func, shape) shaped_set = JuMP.reshape_set(set, shape) @@ -729,7 +714,7 @@ the variables and measures must all first be transcripted (e.g., via function transcribe_constraints!( trans_model::JuMP.Model, inf_model::InfiniteOpt.InfiniteModel - )::Nothing + ) param_supps = parameter_supports(trans_model) for (idx, object) in inf_model.constraints # get the basic information @@ -813,7 +798,7 @@ the variables and measures must all first be transcripted (e.g., via function transcribe_derivative_evaluations!( trans_model::JuMP.Model, inf_model::InfiniteOpt.InfiniteModel - )::Nothing + ) for (idx, object) in InfiniteOpt._data_dictionary(inf_model, InfiniteOpt.Derivative) # get the basic variable information dref = InfiniteOpt._make_variable_ref(inf_model, idx) @@ -867,7 +852,7 @@ via `check_support_dims = false`. function build_transcription_model!( trans_model::JuMP.Model, inf_model::InfiniteOpt.InfiniteModel; check_support_dims::Bool = true - )::Nothing + ) # ensure there are supports to add and add them to the trans model InfiniteOpt.fill_in_supports!(inf_model, modify = false) set_parameter_supports(trans_model, inf_model) diff --git a/src/expressions.jl b/src/expressions.jl index 9be726cc4..714ae0bd1 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -518,12 +518,12 @@ function _interrogate_variables( end # NonlinearExpr (avoid recursion to handle deeply nested expressions) -function _interrogate_variables(interrogator::Function, nlp::JuMP.NonlinearExpr) +function _interrogate_variables(interrogator::Function, nlp::JuMP.GenericNonlinearExpr) stack = Vector{Any}[nlp.args] while !isempty(stack) args = pop!(stack) for arg in args - if arg isa JuMP.NonlinearExpr + if arg isa JuMP.GenericNonlinearExpr push!(stack, arg.args) else _interrogate_variables(interrogator, arg) @@ -565,7 +565,7 @@ function _all_function_variables(f::JuMP.GenericQuadExpr) end # NonlinearExpr or array of expressions -function _all_function_variables(f::Union{JuMP.NonlinearExpr, AbstractArray}) +function _all_function_variables(f::Union{JuMP.GenericNonlinearExpr, AbstractArray}) vref_set = Set{GeneralVariableRef}() _interrogate_variables(v -> push!(vref_set, v), f) return collect(vref_set) @@ -647,12 +647,12 @@ function _model_from_expr(expr::JuMP.GenericQuadExpr) end # NonlinearExpr (avoid recursion for deeply nested expressions) -function _model_from_expr(expr::JuMP.NonlinearExpr) +function _model_from_expr(expr::JuMP.GenericNonlinearExpr) stack = Vector{Any}[expr.args] while !isempty(stack) args = pop!(stack) for arg in args - if arg isa JuMP.NonlinearExpr + if arg isa JuMP.GenericNonlinearExpr push!(stack, arg.args) else result = _model_from_expr(arg) @@ -716,7 +716,7 @@ function _remove_variable_from_leaf( end # Nonlinear (avoid recursion to handle deeply nested expressions) -function _remove_variable(f::JuMP.NonlinearExpr, vref::GeneralVariableRef) +function _remove_variable(f::JuMP.GenericNonlinearExpr, vref::GeneralVariableRef) stack = Tuple{Vector{Any}, Int}[] for i in eachindex(f.args) # should be reverse, but order doesn't matter push!(stack, (f.args, i)) @@ -724,7 +724,7 @@ function _remove_variable(f::JuMP.NonlinearExpr, vref::GeneralVariableRef) while !isempty(stack) arr, idx = pop!(stack) expr = arr[idx] - if expr isa JuMP.NonlinearExpr + if expr isa JuMP.GenericNonlinearExpr for i in eachindex(expr.args) # should be reverse, but order doesn't matter push!(stack, (expr.args, i)) end @@ -780,16 +780,16 @@ function map_expression(transform::Function, quad::JuMP.GenericQuadExpr) end # NonlinearExpr -function map_expression(transform::Function, nlp::JuMP.NonlinearExpr) +function map_expression(transform::Function, nlp::JuMP.GenericNonlinearExpr) # TODO: Figure out how to make the recursionless code work # stack = Tuple{Vector{Any}, Vector{Any}}[] - # new_nlp = JuMP.NonlinearExpr{NewVrefType}(nlp.head, Any[]) # Need to add `NewVrefType` arg throughout pkg + # new_nlp = JuMP.GenericNonlinearExpr{NewVrefType}(nlp.head, Any[]) # Need to add `NewVrefType` arg throughout pkg # push!(stack, (nlp.args, new_nlp.args)) # while !isempty(stack) # args, cloned = pop!(stack) # for arg in args - # if arg isa JuMP.NonlinearExpr - # new_expr = JuMP.NonlinearExpr{NewVrefType}(arg.head, Any[]) + # if arg isa JuMP.GenericNonlinearExpr + # new_expr = JuMP.GenericNonlinearExpr{NewVrefType}(arg.head, Any[]) # push!(stack, (arg.args, new_expr.args)) # else # new_expr = map_expression(transform, arg) @@ -798,7 +798,7 @@ function map_expression(transform::Function, nlp::JuMP.NonlinearExpr) # end # end # return new_nlp - return JuMP.NonlinearExpr(nlp.head, Any[map_expression(transform, arg) for arg in nlp.args]) + return JuMP.GenericNonlinearExpr(nlp.head, Any[map_expression(transform, arg) for arg in nlp.args]) end ################################################################################ @@ -915,7 +915,7 @@ julia> parameter_refs(my_expr) ``` """ function parameter_refs( - expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr} + expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.GenericNonlinearExpr} ) model = _model_from_expr(expr) if isnothing(model) diff --git a/src/measure_expansions.jl b/src/measure_expansions.jl index 3f40b8839..ce3b0a101 100644 --- a/src/measure_expansions.jl +++ b/src/measure_expansions.jl @@ -576,7 +576,7 @@ end # NonlinearExpr (1D DiscreteMeasureData) function expand_measure( - expr::JuMP.NonlinearExpr, + expr::JuMP.GenericNonlinearExpr, data::DiscreteMeasureData{GeneralVariableRef, 1}, write_model::JuMP.AbstractModel ) @@ -600,7 +600,7 @@ end # NonlinearExpr (Multi DiscreteMeasureData) function expand_measure( - expr::JuMP.NonlinearExpr, + expr::JuMP.GenericNonlinearExpr, data::DiscreteMeasureData{Vector{GeneralVariableRef}, 2}, write_model::JuMP.AbstractModel ) diff --git a/src/results.jl b/src/results.jl index 25711b5c6..076ffabb6 100644 --- a/src/results.jl +++ b/src/results.jl @@ -239,7 +239,7 @@ julia> value(my_infinite_expr) ``` """ function JuMP.value( - expr::Union{JuMP.GenericAffExpr{Float64, GeneralVariableRef}, JuMP.GenericQuadExpr{Float64, GeneralVariableRef}, JuMP.NonlinearExpr{GeneralVariableRef}}; + expr::Union{JuMP.GenericAffExpr{Float64, GeneralVariableRef}, JuMP.GenericQuadExpr{Float64, GeneralVariableRef}, JuMP.GenericNonlinearExpr{GeneralVariableRef}}; result::Int = 1, kwargs... ) @@ -247,8 +247,7 @@ function JuMP.value( model = _model_from_expr(expr) # if no model then the expression only contains a constant if isnothing(model) - expr isa JuMP.NonlinearExpr && error("Cannot evaluate the value of `$expr`,", - "because it doesn't have variables.") + expr isa JuMP.GenericNonlinearExpr && return JuMP.value(identity, expr) return JuMP.constant(expr) # otherwise let's call map_value else diff --git a/test/TranscriptionOpt/transcribe.jl b/test/TranscriptionOpt/transcribe.jl index 5dabc5994..4e65e6a44 100644 --- a/test/TranscriptionOpt/transcribe.jl +++ b/test/TranscriptionOpt/transcribe.jl @@ -329,7 +329,7 @@ end con = VectorConstraint([sin(z)], MOI.Zeros(1)) func = [sin(z)] set = MOI.Zeros(1) - @test_throws ErrorException IOTO._process_constraint(tm, con, func, set, zeros(3), "test2") + @test IOTO._process_constraint(tm, con, func, set, zeros(3), "test2") isa ConstraintRef # fallback @test_throws ErrorException IOTO._process_constraint(tm, :bad, func, set, zeros(3), "bad") diff --git a/test/deletion.jl b/test/deletion.jl index c5eb6ff9e..f3a486365 100644 --- a/test/deletion.jl +++ b/test/deletion.jl @@ -482,7 +482,7 @@ end @test isequal_canonical(measure_function(meas1), y + par) @test isequal_canonical(jump_function(constraint_object(con1)), y + par) @test isequal_canonical(objective_function(m), y + 0) - @test isequal_canonical(jump_function(constraint_object(con4)), NonlinearExpr(:-, Any[NonlinearExpr(:sin, Any[0.0]), 0.0])) + @test isequal_canonical(jump_function(constraint_object(con4)), GenericNonlinearExpr{GeneralVariableRef}(:-, Any[GenericNonlinearExpr{GeneralVariableRef}(:sin, Any[0.0]), 0.0])) @test !is_valid(m, con3) @test !haskey(InfiniteOpt._data_dictionary(m, FiniteVariable), JuMP.index(x)) # test deletion of y diff --git a/test/expressions.jl b/test/expressions.jl index 05e2439e8..74352c60c 100644 --- a/test/expressions.jl +++ b/test/expressions.jl @@ -366,8 +366,8 @@ end @test InfiniteOpt._interrogate_variables(i -> push!(a, i), quad) isa Nothing @test isequal(a, [z, z, z]) end - # test NonlinearExpr - @testset "NonlinearExpr" begin + # test GenericNonlinearExpr + @testset "GenericNonlinearExpr" begin a = [] @test InfiniteOpt._interrogate_variables(i -> push!(a, i), nlp) isa Nothing @test isequal(a, [z, z]) @@ -437,7 +437,7 @@ end [pt, inf, meas])) end # test for Array of expressions - @testset "NonlinearExpr" begin + @testset "GenericNonlinearExpr" begin # make expressions nlp = sin(pt) + inf / pt # test expressions @@ -499,8 +499,8 @@ end @test sort!(InfiniteOpt._object_numbers(quad1)) == [1, 2] @test InfiniteOpt._object_numbers(quad2) == [] end - # test for NonlinearExpr - @testset "NonlinearExpr" begin + # test for GenericNonlinearExpr + @testset "GenericNonlinearExpr" begin # make expressions nlp = sin(inf) / pt # test expressions @@ -557,8 +557,8 @@ end @test sort!(InfiniteOpt._parameter_numbers(quad1)) == [1, 2, 3] @test InfiniteOpt._parameter_numbers(quad2) == [] end - # test for NonlinearExpr - @testset "NonlinearExpr" begin + # test for GenericNonlinearExpr + @testset "GenericNonlinearExpr" begin # make expressions nlp = sin(inf2) # test expressions @@ -595,11 +595,11 @@ end @test InfiniteOpt._model_from_expr(quad2) isa Nothing @test InfiniteOpt._model_from_expr(quad3) === m end - # test for NonlinearExpr - @testset "NonlinearExpr" begin + # test for GenericNonlinearExpr + @testset "GenericNonlinearExpr" begin # make expressions nlp1 = sin(hd) - nlp2 = NonlinearExpr{GeneralVariableRef}(:sin, Any[0.0]) + nlp2 = GenericNonlinearExpr{GeneralVariableRef}(:sin, Any[0.0]) nlp3 = 2 + sin(hd^2) # test expressions @test InfiniteOpt._model_from_expr(nlp1) === m @@ -654,8 +654,8 @@ end @test !haskey(quad.terms, UnorderedPair{GeneralVariableRef}(pt, pt)) @test isa(InfiniteOpt._remove_variable(quad2, inf), Nothing) end - # test for NonlinearExpr - @testset "NonlinearExpr" begin + # test for GenericNonlinearExpr + @testset "GenericNonlinearExpr" begin # make expressions nlp1 = sin(3pt) nlp2 = pt^2.3 + ^(inf, pt) @@ -701,8 +701,8 @@ end @testset "QuadExpr" begin @test isequal(map_expression(v -> x, quad), x^2 + 2x) end - # test NonlinearExpr - @testset "NonlinearExpr" begin + # test GenericNonlinearExpr + @testset "GenericNonlinearExpr" begin @test isequal(map_expression(v -> y, nlp), (sin(y) + (2y + 42))^3.4) @test isequal(map_expression(v -> v^3, sin(y)), sin(y^3)) end diff --git a/test/measure_expansions.jl b/test/measure_expansions.jl index ab1eedf41..06d66ce4e 100644 --- a/test/measure_expansions.jl +++ b/test/measure_expansions.jl @@ -459,7 +459,7 @@ end @test isequal_canonical(InfiniteOpt.expand_measure(expr, data3, m), expected) end # test expand_measure (NonlinearExpr univariate) - @testset "NonlinearExpr (1D DiscreteMeasureData)" begin + @testset "GenericNonlinearExpr (1D DiscreteMeasureData)" begin # test simple expr = sin(inf1) expected = 0.5 * sin(inf1(1)) + 0.5 * sin(inf1(2)) @@ -470,7 +470,7 @@ end @test isequal(expand_measure(expr, data1, m), expected) end # test expand_measure (NonlinearExpr multivariate) - @testset "NonlinearExpr (Multi DiscreteMeasureData)" begin + @testset "GenericNonlinearExpr (Multi DiscreteMeasureData)" begin # test simple expr = sin(inf5) expected = 1 * sin(inf5([1, 1], pars2)) + 1 * sin(inf5([2, 2], pars2)) diff --git a/test/nlp.jl b/test/nlp.jl index ce8d49da9..2cb4117a9 100644 --- a/test/nlp.jl +++ b/test/nlp.jl @@ -75,7 +75,7 @@ @variable(mt, x) q(a) = 1 @test @register(mt, q, 1, q) isa UserDefinedFunction - @test @expression(mt, q(x)) isa NonlinearExpr + @test @expression(mt, q(x)) isa GenericNonlinearExpr return end @test registration_test() isa Nothing diff --git a/test/operators.jl b/test/operators.jl index 1ca5604a7..ec50b79d1 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -931,7 +931,7 @@ end # prepare the test grid aff = 2z - 2 quad = y^2 + y - nlp = NonlinearExpr(:sin, Any[y]) + nlp = GenericNonlinearExpr{GeneralVariableRef}(:sin, Any[y]) # test the multiplication operator @testset "Multiplication Operator" begin for (i, iorder) in [(42, 0), (y, 1), (aff, 1), (quad, 2), (nlp, 3)] @@ -964,7 +964,7 @@ end # one_aff = one(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) # for i in [y, aff, quad, nlp] # for f in [Float64, Int] - # if i isa NonlinearExpr + # if i isa GenericNonlinearExpr # @test isequal((i ^ f(2)).args, Any[i, f(2)]) # else # @test isequal(i^f(2), i * i) diff --git a/test/results.jl b/test/results.jl index d3f33cf3a..b345686b7 100644 --- a/test/results.jl +++ b/test/results.jl @@ -263,7 +263,7 @@ end @test value(inf * inf + g - 2, ndarray = true) == [3., -1.] @test value(zero(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) - 42) == -42. @test value(sin(g)) == sin(1) - @test_throws ErrorException value(NonlinearExpr{GeneralVariableRef}(:sin, Any[0])) + @test value(GenericNonlinearExpr{GeneralVariableRef}(:sin, Any[0])) == 0 end # test dual @testset "JuMP.dual" begin diff --git a/test/utilities.jl b/test/utilities.jl index d32b2d107..ddbb47616 100644 --- a/test/utilities.jl +++ b/test/utilities.jl @@ -129,6 +129,6 @@ function _update_variable_param_refs(vref::InfiniteVariableRef, return end -function Base.isequal(nlp1::NonlinearExpr, nlp2::NonlinearExpr) +function Base.isequal(nlp1::GenericNonlinearExpr, nlp2::GenericNonlinearExpr) return nlp1.head == nlp2.head && isequal(nlp1.args, nlp2.args) end From 566fdb169369944e406f349155910a56f8e012be Mon Sep 17 00:00:00 2001 From: pulsipher Date: Fri, 18 Aug 2023 11:02:05 -0400 Subject: [PATCH 14/40] remove use of `MA.@rewrite` --- src/InfiniteOpt.jl | 3 +- src/derivative_evaluations.jl | 22 ++++++-------- src/derivatives.jl | 10 +++---- src/expressions.jl | 9 +++--- src/macros.jl | 15 ++++++++++ src/measure_expansions.jl | 56 ++++++++++++++++------------------- 6 files changed, 60 insertions(+), 55 deletions(-) diff --git a/src/InfiniteOpt.jl b/src/InfiniteOpt.jl index a43a60f34..4c7a7667e 100644 --- a/src/InfiniteOpt.jl +++ b/src/InfiniteOpt.jl @@ -32,6 +32,7 @@ include("semi_infinite_variables.jl") include("point_variables.jl") include("finite_variables.jl") include("nlp.jl") +include("macros.jl") include("expressions.jl") include("measures.jl") @@ -42,7 +43,7 @@ Reexport.@reexport using .MeasureToolbox # import more core methods include("derivatives.jl") include("constraints.jl") -include("macros.jl") +# include("macros.jl") include("objective.jl") include("measure_expansions.jl") include("derivative_evaluations.jl") diff --git a/src/derivative_evaluations.jl b/src/derivative_evaluations.jl index df3fbc070..c226f15d6 100644 --- a/src/derivative_evaluations.jl +++ b/src/derivative_evaluations.jl @@ -228,11 +228,9 @@ function _make_difference_expr( )::JuMP.AbstractJuMPScalar curr_value = ordered_supps[index] next_value = ordered_supps[index+1] - return _MA.@rewrite((next_value - curr_value) * - make_reduced_expr(dref, pref, curr_value, write_model) - - make_reduced_expr(vref, pref, next_value, write_model) + - make_reduced_expr(vref, pref, curr_value, write_model)) -end + return JuMP.GenericAffExpr(0.0, make_reduced_expr(dref, pref, curr_value, write_model) => (next_value - curr_value), + make_reduced_expr(vref, pref, next_value, write_model) => -1, + make_reduced_expr(vref, pref, curr_value, write_model) => 1) # Central function _make_difference_expr( @@ -247,10 +245,9 @@ function _make_difference_expr( prev_value = ordered_supps[index-1] next_value = ordered_supps[index+1] curr_value = ordered_supps[index] - return _MA.@rewrite((next_value - prev_value) * - make_reduced_expr(dref, pref, curr_value, write_model) - - make_reduced_expr(vref, pref, next_value, write_model) + - make_reduced_expr(vref, pref, prev_value, write_model)) + return JuMP.GenericAffExpr(0.0, make_reduced_expr(dref, pref, curr_value, write_model) => (next_value - prev_value), + make_reduced_expr(vref, pref, next_value, write_model) => -1, + make_reduced_expr(vref, pref, prev_value, write_model) => 1) end # Backward @@ -265,10 +262,9 @@ function _make_difference_expr( )::JuMP.AbstractJuMPScalar prev_value = ordered_supps[index-1] curr_value = ordered_supps[index] - return _MA.@rewrite((curr_value - prev_value) * - make_reduced_expr(dref, pref, curr_value, write_model) - - make_reduced_expr(vref, pref, curr_value, write_model) + - make_reduced_expr(vref, pref, prev_value, write_model)) + return JuMP.GenericAffExpr(0.0, make_reduced_expr(dref, pref, curr_value, write_model) => (curr_value - prev_value), + make_reduced_expr(vref, pref, curr_value, write_model) => -1, + make_reduced_expr(vref, pref, prev_value, write_model) => 1) end # Fallback diff --git a/src/derivatives.jl b/src/derivatives.jl index 9ae85feb1..0ae29e172 100644 --- a/src/derivatives.jl +++ b/src/derivatives.jl @@ -381,18 +381,16 @@ end # AffExpr function _build_deriv_expr(aff::JuMP.GenericAffExpr, pref) - return _MA.@rewrite(sum(c * _build_deriv_expr(v, pref) - for (c, v) in JuMP.linear_terms(aff)), - move_factors_into_sums = false) + return @_expr(sum(c * _build_deriv_expr(v, pref) + for (c, v) in JuMP.linear_terms(aff))) end # Quad Expr (implements product rule) function _build_deriv_expr(quad::JuMP.GenericQuadExpr, pref) - return _MA.@rewrite(sum(c * (_build_deriv_expr(v1, pref) * v2 + + return @_expr(sum(c * (_build_deriv_expr(v1, pref) * v2 + v1 * _build_deriv_expr(v2, pref)) for (c, v1, v2) in JuMP.quad_terms(quad)) + - _build_deriv_expr(quad.aff, pref), - move_factors_into_sums = false) + _build_deriv_expr(quad.aff, pref)) end # Real number diff --git a/src/expressions.jl b/src/expressions.jl index 714ae0bd1..4c8e4c7f8 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -765,18 +765,17 @@ end # AffExpr function map_expression(transform::Function, aff::JuMP.GenericAffExpr) # flatten needed in case expression becomes nonlinear with transform - return JuMP.flatten(_MA.@rewrite(sum(c * transform(v) + return JuMP.flatten(@_expr(sum(c * transform(v) for (c, v) in JuMP.linear_terms(aff)) + - JuMP.constant(aff), move_factors_into_sums = false)) + JuMP.constant(aff))) end # QuadExpr function map_expression(transform::Function, quad::JuMP.GenericQuadExpr) # flatten needed in case expression becomes nonlinear with transform - return JuMP.flatten(_MA.@rewrite(sum(c * transform(v1) * transform(v2) + return JuMP.flatten(@_expr(sum(c * transform(v1) * transform(v2) for (c, v1, v2) in JuMP.quad_terms(quad)) + - map_expression(transform, quad.aff), - move_factors_into_sums = false)) + map_expression(transform, quad.aff))) end # NonlinearExpr diff --git a/src/macros.jl b/src/macros.jl index 0f9d5d299..a3acb606f 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -780,3 +780,18 @@ macro parameter_function(model, args...) end return _finalize_macro(_error, esc_model, macro_code, __source__) end + +################################################################################ +# INTERNAL EXPRESSION BUILDER +################################################################################ +# Basic internal MA-based expression builder +# This avoids unnecssary elements of @expression (i.e., specifiing the model, traversing NLP trees, etc.) +macro _expr(expr) + new_expr, parse_expr = _MA.rewrite(expr; move_factors_into_sums = false) + return quote + let + $parse_expr + $new_expr + end + end +end diff --git a/src/measure_expansions.jl b/src/measure_expansions.jl index ce3b0a101..e1f986907 100644 --- a/src/measure_expansions.jl +++ b/src/measure_expansions.jl @@ -241,15 +241,15 @@ function expand_measure(ivref::GeneralVariableRef, return JuMP.GenericAffExpr{Float64, GeneralVariableRef}(0, ivref => var_coef) # make point variables if var_prefs = pref (it is the only dependence) elseif length(var_prefs) == 1 - return _MA.@rewrite(sum(coeffs[i] * w(supps[i]) * + return @_expr(sum(coeffs[i] * w(supps[i]) * make_point_variable_ref(write_model, ivref, [supps[i]]) - for i in eachindex(coeffs)), move_factors_into_sums = false) + for i in eachindex(coeffs))) # make semi-infinite variables if the variable contains other parameters else index = [findfirst(isequal(pref), var_prefs)] - return _MA.@rewrite(sum(coeffs[i] * w(supps[i]) * + return @_expr(sum(coeffs[i] * w(supps[i]) * make_semi_infinite_variable_ref(write_model, ivref, index, [supps[i]]) - for i in eachindex(coeffs)), move_factors_into_sums = false) + for i in eachindex(coeffs))) end end @@ -266,9 +266,9 @@ function expand_measure(ivref::GeneralVariableRef, w = weight_function(data) # var_prefs == prefs so let's make a point variable if isequal(var_prefs, prefs) - return _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * + return @_expr(sum(coeffs[i] * w(supps[:, i]) * make_point_variable_ref(write_model, ivref, supps[:, i]) - for i in eachindex(coeffs)), move_factors_into_sums = false) + for i in eachindex(coeffs))) # treat variable as constant if doesn't have measure parameter elseif !any(any(isequal(pref), var_prefs) for pref in prefs) var_coef = sum(coeffs[i] * w(supps[:, i]) for i in eachindex(coeffs)) @@ -277,9 +277,9 @@ function expand_measure(ivref::GeneralVariableRef, elseif all(any(isequal(pref), prefs) for pref in var_prefs) indices = [findfirst(isequal(pref), prefs) for pref in var_prefs] new_supps = supps[indices, :] - return _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * + return @_expr(sum(coeffs[i] * w(supps[:, i]) * make_point_variable_ref(write_model, ivref, new_supps[:, i]) - for i in eachindex(coeffs)), move_factors_into_sums = false) + for i in eachindex(coeffs))) # make semi-infinite variables if the variable contains other parameters else # get indices of each pref to map properly @@ -290,9 +290,9 @@ function expand_measure(ivref::GeneralVariableRef, indices = convert(Vector{Int}, deleteat!(indices, empty)) supps = supps[.!empty, :] end - return _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * + return @_expr(sum(coeffs[i] * w(supps[:, i]) * make_semi_infinite_variable_ref(write_model, ivref, indices, supps[:, i]) - for i in eachindex(coeffs)), move_factors_into_sums = false) + for i in eachindex(coeffs))) end end @@ -328,19 +328,19 @@ function expand_measure(rvref::GeneralVariableRef, # make point variables if var_prefs = pref (it is the only dependence) elseif length(var_prefs) == 1 index = findfirst(isequal(pref), orig_prefs) - expr = _MA.@rewrite(sum(coeffs[i] * w(supps[i]) * + expr = @_expr(sum(coeffs[i] * w(supps[i]) * make_point_variable_ref(write_model, ivref, _make_point_support(orig_prefs, eval_supps, index, supps[i])) - for i in eachindex(coeffs)), move_factors_into_sums = false) + for i in eachindex(coeffs))) # make semi-infinite variables if the variable contains other parameters else index = findfirst(isequal(pref), orig_prefs) collected_indices = collect(keys(eval_supps)) vals = map(k -> eval_supps[k], collected_indices) # a support will be appended on the fly indices = push!(collected_indices, index) - expr = _MA.@rewrite(sum(coeffs[i] * w(supps[i]) * + expr = @_expr(sum(coeffs[i] * w(supps[i]) * make_semi_infinite_variable_ref(write_model, ivref, indices, vcat(vals, supps[i])) - for i in eachindex(coeffs)), move_factors_into_sums = false) + for i in eachindex(coeffs))) end return expr end @@ -386,10 +386,10 @@ function expand_measure(rvref::GeneralVariableRef, # get the parameter indices of the variable parameters to be reduced indices = [findfirst(isequal(pref), orig_prefs) for pref in var_prefs] # make the expression - expr = _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * + expr = @_expr(sum(coeffs[i] * w(supps[:, i]) * make_point_variable_ref(write_model, ivref, _make_point_support(orig_prefs, eval_supps, indices, supps[:, i])) - for i in eachindex(coeffs)), move_factors_into_sums = false) + for i in eachindex(coeffs))) # make semi-infinite variables if the variable contains other parameters else # get the indices of prefs in terms of the ivref @@ -405,9 +405,9 @@ function expand_measure(rvref::GeneralVariableRef, vals = map(k -> eval_supps[k], collected_indices) # a support will be appended on the fly indices = append!(collected_indices, new_indices) # make the expression - expr = _MA.@rewrite(sum(coeffs[i] * w(supps[:, i]) * + expr = @_expr(sum(coeffs[i] * w(supps[:, i]) * make_semi_infinite_variable_ref(write_model, ivref, indices, vcat(vals, supps[:, i])) - for i in eachindex(coeffs)), move_factors_into_sums = false) + for i in eachindex(coeffs))) end return expr end @@ -497,9 +497,8 @@ function expand_measure(expr::JuMP.GenericAffExpr{C, GeneralVariableRef}, w = weight_function(data) # expand each variable independently and add all together constant_coef = sum(coeffs[i] * w(supps[i]) for i in eachindex(coeffs)) - new_ex = _MA.@rewrite(sum(coef * expand_measure(var, data, write_model) - for (var, coef) in expr.terms) + expr.constant * constant_coef, - move_factors_into_sums = false) + new_ex = @_expr(sum(coef * expand_measure(var, data, write_model) + for (var, coef) in expr.terms) + expr.constant * constant_coef) return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr end @@ -514,9 +513,8 @@ function expand_measure(expr::JuMP.GenericAffExpr{C, GeneralVariableRef}, w = weight_function(data) # expand each variable independently and add all together constant_coef = sum(coeffs[i] * w(supps[:, i]) for i in eachindex(coeffs)) - new_ex = _MA.@rewrite(sum(coef * expand_measure(var, data, write_model) - for (var, coef) in expr.terms) + expr.constant * constant_coef, - move_factors_into_sums = false) + new_ex = @_expr(sum(coef * expand_measure(var, data, write_model) + for (var, coef) in expr.terms) + expr.constant * constant_coef) return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr end @@ -538,12 +536,11 @@ function expand_measure( # make the expression simple_data = DiscreteMeasureData(pref, ones(1), ones(1), label, default_weight, lb, ub, is_expect) - new_ex = _MA.@rewrite(sum(sum(coeffs[i] * w(supps[i]) * c * + new_ex = @_expr(sum(sum(coeffs[i] * w(supps[i]) * c * _map_variable(p.a, simple_data, supps[i], write_model) * _map_variable(p.b, simple_data, supps[i], write_model) for (p, c) in expr.terms) for i in eachindex(coeffs)) + - expand_measure(expr.aff, data, write_model), - move_factors_into_sums = false) + expand_measure(expr.aff, data, write_model)) return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr end @@ -565,12 +562,11 @@ function expand_measure( # make the expression simple_data = DiscreteMeasureData(prefs, ones(1), ones(length(prefs), 1), label, default_weight, lbs, ubs, is_expect) - new_ex = _MA.@rewrite(sum(sum(coeffs[i] * w(@view(supps[:, i])) * c * + new_ex = @_expr(sum(sum(coeffs[i] * w(@view(supps[:, i])) * c * _map_variable(p.a, simple_data, @view(supps[:, i]), write_model) * _map_variable(p.b, simple_data, @view(supps[:, i]), write_model) for (p, c) in expr.terms) for i in eachindex(coeffs)) + - expand_measure(expr.aff, data, write_model), - move_factors_into_sums = false) + expand_measure(expr.aff, data, write_model)) return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr end From a8834f79c9557a6ec0d388227f223ffa3946ded6 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Fri, 18 Aug 2023 11:11:24 -0400 Subject: [PATCH 15/40] Update user defined functions --- src/InfiniteOpt.jl | 1 - src/nlp.jl | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/InfiniteOpt.jl b/src/InfiniteOpt.jl index 4c7a7667e..ff314cb4d 100644 --- a/src/InfiniteOpt.jl +++ b/src/InfiniteOpt.jl @@ -43,7 +43,6 @@ Reexport.@reexport using .MeasureToolbox # import more core methods include("derivatives.jl") include("constraints.jl") -# include("macros.jl") include("objective.jl") include("measure_expansions.jl") include("derivative_evaluations.jl") diff --git a/src/nlp.jl b/src/nlp.jl index a282b160c..adc852274 100644 --- a/src/nlp.jl +++ b/src/nlp.jl @@ -49,7 +49,7 @@ function JuMP.add_user_defined_function( push!(model.registrations, RegisteredFunction(op, dim, funcs...)) model.func_lookup[op] = (funcs[1], dim) # TODO should we set the optimizer model to be out of date? - return JuMP.UserDefinedFunction(op) + return JuMP.UserDefinedFunction(op, funcs[1]) end """ From 2a5cf06831bd5cf3fb403f10f2f14210239c3d4e Mon Sep 17 00:00:00 2001 From: pulsipher Date: Fri, 18 Aug 2023 14:42:25 -0400 Subject: [PATCH 16/40] Update registration tests --- test/nlp.jl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/nlp.jl b/test/nlp.jl index 2cb4117a9..f66ee3dea 100644 --- a/test/nlp.jl +++ b/test/nlp.jl @@ -74,7 +74,7 @@ mt = InfiniteModel() @variable(mt, x) q(a) = 1 - @test @register(mt, q, 1, q) isa UserDefinedFunction + @test @register(mt, my_q, 1, q) isa UserDefinedFunction # TODO update to use same name once JuMP is fixed @test @expression(mt, q(x)) isa GenericNonlinearExpr return end @@ -86,12 +86,13 @@ # test normal m1 = Model() @test add_registered_to_jump(m1, m) isa Nothing - # TODO update checks below - # r1 = m1.nlp_model.operators - # @test length(r1.registered_univariate_operators) == 3 - # @test [r1.registered_univariate_operators[i].f for i in 1:3] == [f, f1, f2] - # @test [r1.registered_univariate_operators[i].f′ for i in 2:3] == [f, f] - # @test r1.registered_univariate_operators[3].f′′ == f1 - # @test length(r1.registered_multivariate_operators) == 2 + attr_dict = backend(model).model_cache.modattr + @test length(attr_dict) == 6 + @test attr_dict[MOI.UserDefinedFunction(:f1, 1)] == (f,) + @test attr_dict[MOI.UserDefinedFunction(:f2, 1)] == (f, f) + @test attr_dict[MOI.UserDefinedFunction(:f3, 1)] == (f, f, f) + @test attr_dict(MOI.UserDefinedFunction(:h1, 2)) == (h,) + @test attr_dict(MOI.UserDefinedFunction(:h2, 2)) == (h, hg) + @test attr_dict(MOI.UserDefinedFunction(:h3, 2)) == (h, hg, ∇²h) end end From 1cfb843520836ecfe90aeef2a9fa36635fba7574 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Mon, 21 Aug 2023 10:39:58 -0400 Subject: [PATCH 17/40] `flatten` -> `flatten!` --- src/expressions.jl | 4 ++-- src/measure_expansions.jl | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/expressions.jl b/src/expressions.jl index 4c8e4c7f8..25cd346e0 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -765,7 +765,7 @@ end # AffExpr function map_expression(transform::Function, aff::JuMP.GenericAffExpr) # flatten needed in case expression becomes nonlinear with transform - return JuMP.flatten(@_expr(sum(c * transform(v) + return JuMP.flatten!(@_expr(sum(c * transform(v) for (c, v) in JuMP.linear_terms(aff)) + JuMP.constant(aff))) end @@ -773,7 +773,7 @@ end # QuadExpr function map_expression(transform::Function, quad::JuMP.GenericQuadExpr) # flatten needed in case expression becomes nonlinear with transform - return JuMP.flatten(@_expr(sum(c * transform(v1) * transform(v2) + return JuMP.flatten!(@_expr(sum(c * transform(v1) * transform(v2) for (c, v1, v2) in JuMP.quad_terms(quad)) + map_expression(transform, quad.aff))) end diff --git a/src/measure_expansions.jl b/src/measure_expansions.jl index e1f986907..663b42855 100644 --- a/src/measure_expansions.jl +++ b/src/measure_expansions.jl @@ -499,7 +499,7 @@ function expand_measure(expr::JuMP.GenericAffExpr{C, GeneralVariableRef}, constant_coef = sum(coeffs[i] * w(supps[i]) for i in eachindex(coeffs)) new_ex = @_expr(sum(coef * expand_measure(var, data, write_model) for (var, coef) in expr.terms) + expr.constant * constant_coef) - return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr + return JuMP.flatten!(new_ex) # just in case we have nested measures producing a NonlinearExpr end # GenericAffExpr (Multi DiscreteMeasureData) @@ -515,7 +515,7 @@ function expand_measure(expr::JuMP.GenericAffExpr{C, GeneralVariableRef}, constant_coef = sum(coeffs[i] * w(supps[:, i]) for i in eachindex(coeffs)) new_ex = @_expr(sum(coef * expand_measure(var, data, write_model) for (var, coef) in expr.terms) + expr.constant * constant_coef) - return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr + return JuMP.flatten!(new_ex) # just in case we have nested measures producing a NonlinearExpr end # GenericQuadExpr (1D DiscreteMeasureData) @@ -541,7 +541,7 @@ function expand_measure( _map_variable(p.b, simple_data, supps[i], write_model) for (p, c) in expr.terms) for i in eachindex(coeffs)) + expand_measure(expr.aff, data, write_model)) - return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr + return JuMP.flatten!(new_ex) # just in case we have nested measures producing a NonlinearExpr end # GenericQuadExpr(Multi DiscreteMeasureData) @@ -567,7 +567,7 @@ function expand_measure( _map_variable(p.b, simple_data, @view(supps[:, i]), write_model) for (p, c) in expr.terms) for i in eachindex(coeffs)) + expand_measure(expr.aff, data, write_model)) - return JuMP.flatten(new_ex) # just in case we have nested measures producing a NonlinearExpr + return JuMP.flatten!(new_ex) # just in case we have nested measures producing a NonlinearExpr end # NonlinearExpr (1D DiscreteMeasureData) @@ -591,7 +591,7 @@ function expand_measure( new_ex = sum(coeffs[i] * w(supps[i]) * map_expression(v -> _map_variable(v, simple_data, supps[i], write_model), expr) for i in eachindex(supps)) - return JuMP.flatten(new_ex) # make expression flat over summation + return JuMP.flatten!(new_ex) # make expression flat over summation end # NonlinearExpr (Multi DiscreteMeasureData) @@ -615,7 +615,7 @@ function expand_measure( new_ex = sum(coeffs[i] * w(@view(supps[:, i])) * map_expression(v -> _map_variable(v, simple_data, @view(supps[:, i]), write_model), expr) for i in eachindex(coeffs)) - return JuMP.flatten(new_ex) # make expression flat over summation + return JuMP.flatten!(new_ex) # make expression flat over summation end From 95f28bb99aa2bba5bda0f83e4e73e5aa7b7c9c9d Mon Sep 17 00:00:00 2001 From: pulsipher Date: Mon, 21 Aug 2023 12:52:07 -0400 Subject: [PATCH 18/40] Update registration tests --- test/nlp.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/nlp.jl b/test/nlp.jl index f66ee3dea..958993170 100644 --- a/test/nlp.jl +++ b/test/nlp.jl @@ -74,7 +74,8 @@ mt = InfiniteModel() @variable(mt, x) q(a) = 1 - @test @register(mt, my_q, 1, q) isa UserDefinedFunction # TODO update to use same name once JuMP is fixed + @test @register(mt, my_q, 1, q) isa UserDefinedFunction + q(x::JuMP.AbstractJuMPScalar) = GenericNonlinearExpr(:my_q, x) @test @expression(mt, q(x)) isa GenericNonlinearExpr return end From c1c73c4ef6df6cdb2172c2c52ba37c85270ba74a Mon Sep 17 00:00:00 2001 From: pulsipher Date: Mon, 21 Aug 2023 12:53:18 -0400 Subject: [PATCH 19/40] Prepare tests for CI --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 12a59d81a..c83572dc3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,5 @@ import Pkg -Pkg.pkg"add JuMP#od/nlp-expr MathOptInterface@1.17" +Pkg.pkg"add JuMP#od/nlp-expr" using InfiniteOpt: _domain_or_error using Test: Error From f37234c80550ae405e4282a002dd0134ae072160 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Mon, 21 Aug 2023 15:00:25 -0400 Subject: [PATCH 20/40] fix syntax bug --- src/derivative_evaluations.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/derivative_evaluations.jl b/src/derivative_evaluations.jl index c226f15d6..fa4925225 100644 --- a/src/derivative_evaluations.jl +++ b/src/derivative_evaluations.jl @@ -231,6 +231,7 @@ function _make_difference_expr( return JuMP.GenericAffExpr(0.0, make_reduced_expr(dref, pref, curr_value, write_model) => (next_value - curr_value), make_reduced_expr(vref, pref, next_value, write_model) => -1, make_reduced_expr(vref, pref, curr_value, write_model) => 1) +end # Central function _make_difference_expr( From 32f290d559f2268901e3afa4650cf8b0d0673155 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Mon, 21 Aug 2023 15:14:05 -0400 Subject: [PATCH 21/40] test fix --- test/derivatives.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/derivatives.jl b/test/derivatives.jl index 1930ec7fe..d25c79298 100644 --- a/test/derivatives.jl +++ b/test/derivatives.jl @@ -363,7 +363,7 @@ end @test isequal(InfiniteOpt._build_deriv_expr(2x^2 + 42, pref2), @expression(m, 0*x)) @test isequal(InfiniteOpt._build_deriv_expr(2pref2^2 -pref2 - 23, pref2), 4pref2 - 1) gvref = GeneralVariableRef(m, 1, DerivativeIndex) - @test isequal(InfiniteOpt._build_deriv_expr(-4x^2, pref), -8 * gvref * x) + @test isequal(InfiniteOpt._build_deriv_expr(-4x^2, pref), -8 * x * gvref) end # test "_build_deriv_expr (Number)" @testset "_build_deriv_expr (Number)" begin From 1f2245e056930470b671df5e3e589d710c967413 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Mon, 21 Aug 2023 16:38:10 -0400 Subject: [PATCH 22/40] minor fixes --- docs/Project.toml | 1 - docs/make.jl | 2 +- src/derivative_evaluations.jl | 18 +++++++++--------- test/derivatives.jl | 2 +- test/nlp.jl | 10 +++++----- test/runtests.jl | 4 ++-- test/utilities.jl | 2 ++ 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index 03c252678..e4a6dbff5 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -20,7 +20,6 @@ InfiniteOpt = "0.5" Ipopt = "1.4" Literate = "2.9" JuMP = "^1.11.1" -Ipopt = "1" Literate = "2.14" Plots = "1" julia = "1.6" diff --git a/docs/make.jl b/docs/make.jl index 2fae33a1a..b342255a0 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,5 +1,5 @@ import Pkg -Pkg.pkg"add JuMP#od/nlp-expr MathOptInterface#od/nlp-expr add Ipopt#od/nlp-expr" +Pkg.pkg"add JuMP#od/nlp-expr" using Documenter, InfiniteOpt, Distributions, Literate, Random diff --git a/src/derivative_evaluations.jl b/src/derivative_evaluations.jl index fa4925225..5accd84ad 100644 --- a/src/derivative_evaluations.jl +++ b/src/derivative_evaluations.jl @@ -228,9 +228,9 @@ function _make_difference_expr( )::JuMP.AbstractJuMPScalar curr_value = ordered_supps[index] next_value = ordered_supps[index+1] - return JuMP.GenericAffExpr(0.0, make_reduced_expr(dref, pref, curr_value, write_model) => (next_value - curr_value), - make_reduced_expr(vref, pref, next_value, write_model) => -1, - make_reduced_expr(vref, pref, curr_value, write_model) => 1) + return @_expr(make_reduced_expr(dref, pref, curr_value, write_model) * (next_value - curr_value) - + make_reduced_expr(vref, pref, next_value, write_model) + + make_reduced_expr(vref, pref, curr_value, write_model)) end # Central @@ -246,9 +246,9 @@ function _make_difference_expr( prev_value = ordered_supps[index-1] next_value = ordered_supps[index+1] curr_value = ordered_supps[index] - return JuMP.GenericAffExpr(0.0, make_reduced_expr(dref, pref, curr_value, write_model) => (next_value - prev_value), - make_reduced_expr(vref, pref, next_value, write_model) => -1, - make_reduced_expr(vref, pref, prev_value, write_model) => 1) + return @_expr(make_reduced_expr(dref, pref, curr_value, write_model) * (next_value - prev_value) - + make_reduced_expr(vref, pref, next_value, write_model) + + make_reduced_expr(vref, pref, prev_value, write_model)) end # Backward @@ -263,9 +263,9 @@ function _make_difference_expr( )::JuMP.AbstractJuMPScalar prev_value = ordered_supps[index-1] curr_value = ordered_supps[index] - return JuMP.GenericAffExpr(0.0, make_reduced_expr(dref, pref, curr_value, write_model) => (curr_value - prev_value), - make_reduced_expr(vref, pref, curr_value, write_model) => -1, - make_reduced_expr(vref, pref, prev_value, write_model) => 1) + return @_expr(make_reduced_expr(dref, pref, curr_value, write_model) * (curr_value - prev_value) - + make_reduced_expr(vref, pref, curr_value, write_model) + + make_reduced_expr(vref, pref, prev_value, write_model)) end # Fallback diff --git a/test/derivatives.jl b/test/derivatives.jl index d25c79298..af90021f6 100644 --- a/test/derivatives.jl +++ b/test/derivatives.jl @@ -363,7 +363,7 @@ end @test isequal(InfiniteOpt._build_deriv_expr(2x^2 + 42, pref2), @expression(m, 0*x)) @test isequal(InfiniteOpt._build_deriv_expr(2pref2^2 -pref2 - 23, pref2), 4pref2 - 1) gvref = GeneralVariableRef(m, 1, DerivativeIndex) - @test isequal(InfiniteOpt._build_deriv_expr(-4x^2, pref), -8 * x * gvref) + @test isequal(InfiniteOpt._build_deriv_expr(-4x^2, pref).terms, (-8 * x * gvref).terms) end # test "_build_deriv_expr (Number)" @testset "_build_deriv_expr (Number)" begin diff --git a/test/nlp.jl b/test/nlp.jl index 958993170..c81b5e614 100644 --- a/test/nlp.jl +++ b/test/nlp.jl @@ -75,7 +75,7 @@ @variable(mt, x) q(a) = 1 @test @register(mt, my_q, 1, q) isa UserDefinedFunction - q(x::JuMP.AbstractJuMPScalar) = GenericNonlinearExpr(:my_q, x) + q(x::GeneralVariableRef) = GenericNonlinearExpr{GeneralVariableRef}(:my_q, x) @test @expression(mt, q(x)) isa GenericNonlinearExpr return end @@ -87,13 +87,13 @@ # test normal m1 = Model() @test add_registered_to_jump(m1, m) isa Nothing - attr_dict = backend(model).model_cache.modattr + attr_dict = backend(m1).model_cache.modattr @test length(attr_dict) == 6 @test attr_dict[MOI.UserDefinedFunction(:f1, 1)] == (f,) @test attr_dict[MOI.UserDefinedFunction(:f2, 1)] == (f, f) @test attr_dict[MOI.UserDefinedFunction(:f3, 1)] == (f, f, f) - @test attr_dict(MOI.UserDefinedFunction(:h1, 2)) == (h,) - @test attr_dict(MOI.UserDefinedFunction(:h2, 2)) == (h, hg) - @test attr_dict(MOI.UserDefinedFunction(:h3, 2)) == (h, hg, ∇²h) + @test attr_dict[MOI.UserDefinedFunction(:h1, 2)] == (h,) + @test attr_dict[MOI.UserDefinedFunction(:h2, 2)] == (h, hg) + @test attr_dict[MOI.UserDefinedFunction(:h3, 2)] == (h, hg, ∇²h) end end diff --git a/test/runtests.jl b/test/runtests.jl index c83572dc3..c69a3bfb1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,5 @@ -import Pkg -Pkg.pkg"add JuMP#od/nlp-expr" +# import Pkg +# Pkg.pkg"add JuMP#od/nlp-expr" using InfiniteOpt: _domain_or_error using Test: Error diff --git a/test/utilities.jl b/test/utilities.jl index ddbb47616..580e13fe0 100644 --- a/test/utilities.jl +++ b/test/utilities.jl @@ -132,3 +132,5 @@ end function Base.isequal(nlp1::GenericNonlinearExpr, nlp2::GenericNonlinearExpr) return nlp1.head == nlp2.head && isequal(nlp1.args, nlp2.args) end + + From 1e961daaf04a9e6d7897878737329bb192f0c88d Mon Sep 17 00:00:00 2001 From: pulsipher Date: Mon, 21 Aug 2023 16:43:26 -0400 Subject: [PATCH 23/40] Minor doc fix --- docs/Project.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index e4a6dbff5..fb7125974 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -13,14 +13,13 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" [compat] -HiGHS = "1" Distributions = "0.25" Documenter = "0.27" InfiniteOpt = "0.5" Ipopt = "1.4" -Literate = "2.9" +HiGHS = "1" +julia = "1.6" JuMP = "^1.11.1" Literate = "2.14" Plots = "1" -julia = "1.6" SpecialFunctions = "2" From 8693a66b5762b404f62148e21446d7ea92607eef Mon Sep 17 00:00:00 2001 From: pulsipher Date: Mon, 21 Aug 2023 16:44:23 -0400 Subject: [PATCH 24/40] minor fix --- test/runtests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index c69a3bfb1..c83572dc3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,5 @@ -# import Pkg -# Pkg.pkg"add JuMP#od/nlp-expr" +import Pkg +Pkg.pkg"add JuMP#od/nlp-expr" using InfiniteOpt: _domain_or_error using Test: Error From ded4b4076ee1a136474deaa9cd6ee1a5db31436f Mon Sep 17 00:00:00 2001 From: pulsipher Date: Mon, 28 Aug 2023 12:35:34 -0400 Subject: [PATCH 25/40] Update to JuMP changes --- src/TranscriptionOpt/model.jl | 2 +- src/datatypes.jl | 48 ++++++++--------- src/expressions.jl | 68 +----------------------- src/measures.jl | 5 +- src/nlp.jl | 98 +++++++++++++++++------------------ src/optimize.jl | 4 +- src/results.jl | 2 +- test/datatypes.jl | 36 ++++++------- test/expressions.jl | 54 ------------------- test/nlp.jl | 68 ++++++++++++------------ 10 files changed, 132 insertions(+), 253 deletions(-) diff --git a/src/TranscriptionOpt/model.jl b/src/TranscriptionOpt/model.jl index ced6469d2..6cd542287 100644 --- a/src/TranscriptionOpt/model.jl +++ b/src/TranscriptionOpt/model.jl @@ -680,7 +680,7 @@ function transcription_expression( label::Type{<:InfiniteOpt.AbstractSupportLabel} = InfiniteOpt.PublicLabel, ndarray::Bool = false ) - model = InfiniteOpt._model_from_expr(expr) + model = JuMP.owner_model(expr) if isnothing(model) return zero(JuMP.AffExpr) + JuMP.constant(expr) else diff --git a/src/datatypes.jl b/src/datatypes.jl index 37ed463d7..f730a050a 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -1190,43 +1190,43 @@ mutable struct ConstraintData{C <: JuMP.AbstractConstraint} <: AbstractDataObjec end ################################################################################ -# Function Registration +# Operator Registration ################################################################################ """ - RegisteredFunction{F <: Function, G <: Union{Function, Nothing}, + RegisteredOperator{F <: Function, G <: Union{Function, Nothing}, H <: Union{Function, Nothing}} -A type for storing used defined registered functions and their information that -is needed by JuMP for build an `NLPEvaluator`. The constructor is of the form: +A type for storing used defined registered nonlinear operators and their information +that is needed by JuMP for build an `NLPEvaluator`. The constructor is of the form: ```julia - RegisteredFunction(op::Symbol, dim::Int, f::Function, + RegisteredOperator(name::Symbol, dim::Int, f::Function, [∇f::Function, ∇²f::Function]) ``` **Fields** -- `op::Symbol`: The name of the function that is used in `NLPExpr`s. +- `name::Symbol`: The name of the operator that is used. - `dim::Int`: The number of function arguments. -- `f::F`: The function itself. +- `f::F`: The function to evaluate the operator. - `∇f::G`: The gradient function if one is given. - `∇²f::H`: The hessian function if one is given. """ -struct RegisteredFunction{F <: Function, G, H} - op::Symbol +struct RegisteredOperator{F <: Function, G, H} + name::Symbol dim::Int f::F ∇f::G ∇²f::H # Constructors - function RegisteredFunction( - op::Symbol, + function RegisteredOperator( + name::Symbol, dim::Int, f::F ) where {F <: Function} - return new{F, Nothing, Nothing}(op, dim, f, nothing, nothing) + return new{F, Nothing, Nothing}(name, dim, f, nothing, nothing) end - function RegisteredFunction( - op::Symbol, + function RegisteredOperator( + name::Symbol, dim::Int, f::F, ∇f::G @@ -1236,10 +1236,10 @@ struct RegisteredFunction{F <: Function, G, H} elseif !isone(dim) && !hasmethod(∇f, Tuple{AbstractVector{Real}, ntuple(_->Real, dim)...}) error("Invalid multi-variate gradient function form, see the docs for details.") end - return new{F, G, Nothing}(op, dim, f, ∇f, nothing) + return new{F, G, Nothing}(name, dim, f, ∇f, nothing) end - function RegisteredFunction( - op::Symbol, + function RegisteredOperator( + name::Symbol, dim::Int, f::F, ∇f::G, @@ -1252,7 +1252,7 @@ struct RegisteredFunction{F <: Function, G, H} elseif !isone(dim) && !hasmethod(∇²f, Tuple{AbstractMatrix{Real}, ntuple(_->Real, dim)...}) error("Invalid multi-variate hessian function form, see the docs for details.") end - return new{F, G, H}(op, dim, f, ∇f, ∇²f) + return new{F, G, H}(name, dim, f, ∇f, ∇²f) end end @@ -1308,8 +1308,8 @@ model an optmization problem with an infinite-dimensional decision space. - `objective_sense::MOI.OptimizationSense`: Objective sense. - `objective_function::JuMP.AbstractJuMPScalar`: Finite scalar function. - `objective_has_measures::Bool`: Does the objective contain measures? -- `registrations::Vector{RegisteredFunction}`: The nonlinear registered functions. -- `Dict{Symbol, Tuple{Function, Int}}`: Map a name to a registered function and its dimension. +- `registrations::Vector{RegisteredOperator}`: The registered nonlinear operators. +- `op_lookup::Dict{Symbol, Tuple{Function, Int}}`: Map a name to a registered operator and its dimension. - `obj_dict::Dict{Symbol, Any}`: Store Julia symbols used with `InfiniteModel` - `optimizer_constructor`: MOI optimizer constructor (e.g., Gurobi.Optimizer). - `optimizer_model::JuMP.Model`: Model used to solve `InfiniteModel` @@ -1352,9 +1352,9 @@ mutable struct InfiniteModel <: JuMP.AbstractModel objective_function::JuMP.AbstractJuMPScalar objective_has_measures::Bool - # Function Registration - registrations::Vector{RegisteredFunction} - func_lookup::Dict{Symbol, Tuple{Function, Int}} + # Operator Registration + registrations::Vector{RegisteredOperator} + op_lookup::Dict{Symbol, Tuple{Function, Int}} # Objects obj_dict::Dict{Symbol, Any} @@ -1444,7 +1444,7 @@ function InfiniteModel(; zero(JuMP.GenericAffExpr{Float64, GeneralVariableRef}), false, # registration - RegisteredFunction[], + RegisteredOperator[], Dict{Symbol, Tuple{Function, Int}}(), # Object dictionary Dict{Symbol, Any}(), diff --git a/src/expressions.jl b/src/expressions.jl index 25cd346e0..51f8fd6cb 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -611,72 +611,6 @@ function _parameter_numbers(expr) return collect(param_nums) end -################################################################################ -# MODEL EXTRACTION METHODS -################################################################################ -## Get the model from an expression -# Constant -function _model_from_expr(::Union{Number, Bool}) - return -end - -# GeneralVariableRef -function _model_from_expr(expr::GeneralVariableRef) - return JuMP.owner_model(expr) -end - -# AffExpr -function _model_from_expr(expr::JuMP.GenericAffExpr) - if isempty(expr.terms) - return - else - return JuMP.owner_model(first(keys(expr.terms))) - end -end - -# QuadExpr -function _model_from_expr(expr::JuMP.GenericQuadExpr) - result = _model_from_expr(expr.aff) - if !isnothing(result) - return result - elseif isempty(expr.terms) - return - else - return JuMP.owner_model(first(keys(expr.terms)).a) - end -end - -# NonlinearExpr (avoid recursion for deeply nested expressions) -function _model_from_expr(expr::JuMP.GenericNonlinearExpr) - stack = Vector{Any}[expr.args] - while !isempty(stack) - args = pop!(stack) - for arg in args - if arg isa JuMP.GenericNonlinearExpr - push!(stack, arg.args) - else - result = _model_from_expr(arg) - isnothing(result) || return result - end - end - end - return -end - -# Vector{GeneralVariableRef} -function _model_from_expr(vrefs::Vector{GeneralVariableRef}) - if isempty(vrefs) - return - else - return JuMP.owner_model(first(vrefs)) - end -end - -# Fallback -function _model_from_expr(expr) - error("`_model_from_expr` not defined for expr of type $(typeof(expr)).") -end - ################################################################################ # VARIABLE REMOVAL BOUNDS ################################################################################ @@ -916,7 +850,7 @@ julia> parameter_refs(my_expr) function parameter_refs( expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.GenericNonlinearExpr} ) - model = _model_from_expr(expr) + model = JuMP.owner_model(expr) if isnothing(model) return () else diff --git a/src/measures.jl b/src/measures.jl index 6d0a6980c..b1fc94022 100644 --- a/src/measures.jl +++ b/src/measures.jl @@ -758,9 +758,8 @@ measure. function build_measure( expr::T, data::D; - )::Measure{T, D} where {T <: JuMP.AbstractJuMPScalar, D <: AbstractMeasureData} + ) vrefs = _all_function_variables(expr) - model = _model_from_expr(vrefs) expr_obj_nums = _object_numbers(expr) expr_param_nums = _parameter_numbers(expr) prefs = parameter_refs(data) @@ -1182,7 +1181,7 @@ function measure( data::AbstractMeasureData; name::String = "measure" )::GeneralVariableRef - model = _model_from_expr(expr) + model = JuMP.owner_model(expr) if isnothing(model) error("Expression contains no variables or parameters.") end diff --git a/src/nlp.jl b/src/nlp.jl index adc852274..815885449 100644 --- a/src/nlp.jl +++ b/src/nlp.jl @@ -1,126 +1,126 @@ # TODO add deprecation errors for old methods ################################################################################ -# USER FUNCTIONS +# USER OPERATORS ################################################################################ # Keep track of the predefined functions in MOI -const _NativeNLPFunctions = append!(copy(MOI.Nonlinear.DEFAULT_UNIVARIATE_OPERATORS), +const _NativeNLPOperators = append!(copy(MOI.Nonlinear.DEFAULT_UNIVARIATE_OPERATORS), MOI.Nonlinear.DEFAULT_MULTIVARIATE_OPERATORS) -append!(_NativeNLPFunctions, (:&&, :||, :<=, :(==), :>=, :<, :>)) +append!(_NativeNLPOperators, (:&&, :||, :<=, :(==), :>=, :<, :>)) # TODO expand details in docstring """ - JuMP.add_user_defined_function( + JuMP.register_nonlinear_operator( model::InfiniteModel, - op::Symbol, dim::Int, f::Function, [∇f::Function,] - [∇²f::Function] + [∇²f::Function]; + [name::Symbol = Symbol(f)] ) -Extend `JuMP.add_user_defined_function` for `InfiniteModel`s. +Extend `register_nonlinear_operator` for `InfiniteModel`s. -Add a user-defined function with `dim` input arguments to `model` and associate -it with the operator `op`. +Add a new nonlinear operator with `dim` input arguments to `model` and associate +it with the name `name`. -The function `f` evaluates the function. The optional function `∇f` evaluates +The function `f` evaluates the operator. The optional function `∇f` evaluates the first derivative, and the optional function `∇²f` evaluates the second derivative. `∇²f` may be provided only if `∇f` is also provided. """ -function JuMP.add_user_defined_function( +function JuMP.register_nonlinear_operator( model::InfiniteModel, - op::Symbol, dim::Int, - funcs... - ) + funcs::Vararg{Function, N}; + name::Symbol = Symbol(f) + ) where {N} if isempty(funcs) - error("Tried to register `$op`, but no evaluation function was given.") + error("Tried to register `$name`, but no evaluation function was given.") elseif !all(f -> f isa Function, funcs) error("The gradient and/or hessian must be functions, but got argument(s) " * "of type `" * join(Tuple(typeof(f) for f in funcs if !(f isa Function)), "`, `") * "`.") - elseif op in _NativeNLPFunctions || op in keys(model.func_lookup) - error("A function with name `$op` arguments is already " * - "registered. Please use a function with a different name.") - elseif !hasmethod(funcs[1], NTuple{dim, Real}) - error("The function `$op` is not defined for arguments of type `Real`.") + elseif name in _NativeNLPOperators || name in keys(model.op_lookup) + error("An operator with name `$op` arguments is already " * + "registered. Please use a operator with a different name.") + elseif !hasmethod(funcs[1], NTuple{dim, Float64}) + error("The operator `$op` is not defined for arguments of type `Float64`.") end - push!(model.registrations, RegisteredFunction(op, dim, funcs...)) - model.func_lookup[op] = (funcs[1], dim) + push!(model.registrations, RegisteredOperator(name, dim, funcs...)) + model.op_lookup[name] = (funcs[1], dim) # TODO should we set the optimizer model to be out of date? - return JuMP.UserDefinedFunction(op, funcs[1]) + return JuMP.NonlinearOperator(name, funcs[1]) end """ - name_to_function(model::InfiniteModel, op::Symbol)::Union{Function, Nothing} + name_to_operator(model::InfiniteModel, name::Symbol)::Union{Function, Nothing} -Return the registered function that corresponds to `op`. -Returns `nothing` if no such registered function exists. This helps retrieve the -functions of user-defined functions. +Return the registered operator that corresponds to `name`. +Returns `nothing` if no such registered operator exists. This helps retrieve the +functions of user-defined nonlinear operators. !!! warning Currently, this does not return functions for default operators. """ -function name_to_function(model::InfiniteModel, op::Symbol) - haskey(model.func_lookup, op) && return model.func_lookup[op][1] +function name_to_operator(model::InfiniteModel, name::Symbol) + haskey(model.op_lookup, name) && return model.op_lookup[op][1] return end """ - all_registered_functions(model::InfiniteModel)::Vector{Symbol} + all_registered_operators(model::InfiniteModel)::Vector{Symbol} -Retrieve all the functions that are currently registered to `model`. +Retrieve all the operators that are currently registered to `model`. """ -function all_registered_functions(model::InfiniteModel) - return append!(copy(_NativeNLPFunctions), map(v -> Symbol(first(v)), values(model.func_lookup))) +function all_registered_operators(model::InfiniteModel) + return append!(copy(_NativeNLPOperators), map(v -> Symbol(first(v)), values(model.op_lookup))) end """ - user_registered_functions(model::InfiniteModel)::Vector{RegisteredFunction} + user_registered_operators(model::InfiniteModel)::Vector{RegisteredOperator} -Return all the functions (and their associated information) that the user has -registered to `model`. Each is stored as a [`RegisteredFunction`](@ref). +Return all the operators (and their associated information) that the user has +registered to `model`. Each is stored as a [`RegisteredOperator`](@ref). """ -function user_registered_functions(model::InfiniteModel) +function user_registered_operators(model::InfiniteModel) return model.registrations end -## Define helper function to add registered functions to JuMP +## Define helper function to add registered operators to JuMP # No gradient or hessian -function _add_func_data_to_jump( +function _add_op_data_to_jump( model::JuMP.Model, - data::RegisteredFunction{F, Nothing, Nothing} + data::RegisteredOperator{F, Nothing, Nothing} ) where {F <: Function} - JuMP.add_user_defined_function(model, data.op, data.dim, data.f) + JuMP.register_nonlinear_operator(model, data.dim, data.f, name = data.name) return end # Only gradient information -function _add_func_data_to_jump( +function _add_op_data_to_jump( model::JuMP.Model, - data::RegisteredFunction{F, G, Nothing} + data::RegisteredOperator{F, G, Nothing} ) where {F <: Function, G <: Function} - JuMP.add_user_defined_function(model, data.op, data.dim, data.f, data.∇f) + JuMP.register_nonlinear_operator(model, data.dim, data.f, data.∇f, name = data.name) return end # Gradient and hessian information -function _add_func_data_to_jump(model::JuMP.Model, data::RegisteredFunction) - JuMP.add_user_defined_function(model, data.op, data.dim, data.f, data.∇f, data.∇²f) +function _add_op_data_to_jump(model::JuMP.Model, data::RegisteredOperator) + JuMP.register_nonlinear_operator(model, data.dim, data.f, data.∇f, data.∇²f, name = data.name) return end """ add_registered_to_jump(opt_model::JuMP.Model, inf_model::InfiniteModel)::Nothing -Add the user registered functions in `inf_model` to a `JuMP` model `opt_model`. +Add the user registered nonlinear operators in `inf_model` to a `JuMP` model `opt_model`. This is intended as an internal method, but it is provided for developers that extend `InfiniteOpt` to use other optimizer models. """ function add_registered_to_jump(opt_model::JuMP.Model, inf_model::InfiniteModel) - for data in user_registered_functions(inf_model) - _add_func_data_to_jump(opt_model, data) + for data in user_registered_operators(inf_model) + _add_op_data_to_jump(opt_model, data) end return end diff --git a/src/optimize.jl b/src/optimize.jl index 3e7b71a3a..11bda01cc 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -713,7 +713,7 @@ x(support: 1) - y ``` """ function optimizer_model_expression(expr::JuMP.AbstractJuMPScalar; kwargs...) - model = _model_from_expr(expr) + model = JuMP.owner_model(expr) if isnothing(model) return zero(JuMP.AffExpr) + JuMP.constant(expr) else @@ -776,7 +776,7 @@ julia> supports(cref) ``` """ function supports(expr::JuMP.AbstractJuMPScalar; kwargs...) - model = _model_from_expr(expr) + model = JuMP.owner_model(expr) if isnothing(model) return () else diff --git a/src/results.jl b/src/results.jl index 076ffabb6..6ef30839f 100644 --- a/src/results.jl +++ b/src/results.jl @@ -244,7 +244,7 @@ function JuMP.value( kwargs... ) # get the model - model = _model_from_expr(expr) + model = JuMP.owner_model(expr) # if no model then the expression only contains a constant if isnothing(model) expr isa JuMP.GenericNonlinearExpr && return JuMP.value(identity, expr) diff --git a/test/datatypes.jl b/test/datatypes.jl index 923ed4c2a..803a59491 100644 --- a/test/datatypes.jl +++ b/test/datatypes.jl @@ -468,8 +468,8 @@ end @test ConstraintData(con, [1], "", MeasureIndex[], false) isa ConstraintData end -# Test the function registration constructor -@testset "Registered Functions" begin +# Test the operator registration constructor +@testset "Registered Operators" begin # Setup f(a) = a^3 g(a::Int) = 42 @@ -489,21 +489,21 @@ end return end # Test errors - @test_throws ErrorException RegisteredFunction(:a, 1, f, g) - @test_throws ErrorException RegisteredFunction(:a, 2, f, g) - @test_throws ErrorException RegisteredFunction(:a, 1, f, g, f) - @test_throws ErrorException RegisteredFunction(:a, 1, f, f, g) - @test_throws ErrorException RegisteredFunction(:a, 2, h, hg, hg) + @test_throws ErrorException RegisteredOperator(:a, 1, f, g) + @test_throws ErrorException RegisteredOperator(:a, 2, f, g) + @test_throws ErrorException RegisteredOperator(:a, 1, f, g, f) + @test_throws ErrorException RegisteredOperator(:a, 1, f, f, g) + @test_throws ErrorException RegisteredOperator(:a, 2, h, hg, hg) # Test regular builds - @test RegisteredFunction(:a, 1, f).op == :a - @test RegisteredFunction(:a, 1, f).dim == 1 - @test RegisteredFunction(:a, 1, f).f == f - @test RegisteredFunction(:a, 1, f, f) isa RegisteredFunction{typeof(f), typeof(f), Nothing} - @test RegisteredFunction(:a, 1, f, f).∇f == f - @test RegisteredFunction(:a, 1, f, f, f) isa RegisteredFunction{typeof(f), typeof(f), typeof(f)} - @test RegisteredFunction(:a, 1, f, f, f).∇²f == f - @test RegisteredFunction(:a, 2, h, hg) isa RegisteredFunction{typeof(h), typeof(hg), Nothing} - @test RegisteredFunction(:a, 2, h, hg).∇f == hg - @test RegisteredFunction(:a, 2, h, hg, ∇²h) isa RegisteredFunction{typeof(h), typeof(hg), typeof(∇²h)} - @test RegisteredFunction(:a, 2, h, hg, ∇²h).∇²f == ∇²h + @test RegisteredOperator(:a, 1, f).name == :a + @test RegisteredOperator(:a, 1, f).dim == 1 + @test RegisteredOperator(:a, 1, f).f == f + @test RegisteredOperator(:a, 1, f, f) isa RegisteredOperator{typeof(f), typeof(f), Nothing} + @test RegisteredOperator(:a, 1, f, f).∇f == f + @test RegisteredOperator(:a, 1, f, f, f) isa RegisteredOperator{typeof(f), typeof(f), typeof(f)} + @test RegisteredOperator(:a, 1, f, f, f).∇²f == f + @test RegisteredOperator(:a, 2, h, hg) isa RegisteredOperator{typeof(h), typeof(hg), Nothing} + @test RegisteredOperator(:a, 2, h, hg).∇f == hg + @test RegisteredOperator(:a, 2, h, hg, ∇²h) isa RegisteredOperator{typeof(h), typeof(hg), typeof(∇²h)} + @test RegisteredOperator(:a, 2, h, hg, ∇²h).∇²f == ∇²h end diff --git a/test/expressions.jl b/test/expressions.jl index 74352c60c..e20cb6847 100644 --- a/test/expressions.jl +++ b/test/expressions.jl @@ -566,60 +566,6 @@ end end end -# Test _model_from_expr -@testset "_model_from_expr" begin - # initialize model and references - m = InfiniteModel() - @variable(m, hd) - # test for variable reference - @testset "Variable" begin - @test InfiniteOpt._model_from_expr(hd) === m - end - # test for GenericAffExpr - @testset "AffExpr" begin - # make expressions - aff1 = hd + 2 - aff2 = zero(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) - # test expressions - @test InfiniteOpt._model_from_expr(aff1) === m - @test InfiniteOpt._model_from_expr(aff2) isa Nothing - end - # test for GenericQuadExpr - @testset "QuadExpr" begin - # make expressions - quad1 = hd * hd + hd + 1 - quad2 = zero(JuMP.GenericQuadExpr{Float64, GeneralVariableRef}) - quad3 = hd * hd + 1 - # test expressions - @test InfiniteOpt._model_from_expr(quad1) === m - @test InfiniteOpt._model_from_expr(quad2) isa Nothing - @test InfiniteOpt._model_from_expr(quad3) === m - end - # test for GenericNonlinearExpr - @testset "GenericNonlinearExpr" begin - # make expressions - nlp1 = sin(hd) - nlp2 = GenericNonlinearExpr{GeneralVariableRef}(:sin, Any[0.0]) - nlp3 = 2 + sin(hd^2) - # test expressions - @test InfiniteOpt._model_from_expr(nlp1) === m - @test InfiniteOpt._model_from_expr(nlp2) isa Nothing - @test InfiniteOpt._model_from_expr(nlp3) === m - end - # test for Vector{GeneralVariableRef} - @testset "Vector{GeneralVariableRef}" begin - vrefs1 = GeneralVariableRef[] - vrefs2 = [hd] - # test expressions - @test InfiniteOpt._model_from_expr(vrefs1) isa Nothing - @test InfiniteOpt._model_from_expr(vrefs2) === m - end - # test Fallback - @testset "Fallback" begin - @test_throws ErrorException InfiniteOpt._model_from_expr(:bad) - end -end - # Test _remove_variable @testset "_remove_variable" begin # initialize model and references diff --git a/test/nlp.jl b/test/nlp.jl index c81b5e614..6985abd5a 100644 --- a/test/nlp.jl +++ b/test/nlp.jl @@ -21,60 +21,60 @@ H[2, 2] = 200.0 return end - # test JuMP.add_user_defined_function - @testset "JuMP.add_user_defined_function" begin + # test JuMP.register_nonlinear_operator + @testset "JuMP.register_nonlinear_operator" begin # test errors - @test_throws ErrorException add_user_defined_function(m, :f, 1) - @test_throws ErrorException add_user_defined_function(m, :f, 1, f, 2) - @test_throws ErrorException add_user_defined_function(m, :max, 1, f) - m.func_lookup[:f] = (f, 1) - @test_throws ErrorException add_user_defined_function(m, :f, 1, f) - empty!(m.func_lookup) - @test_throws ErrorException add_user_defined_function(m, :f, 2, f) + @test_throws ErrorException register_nonlinear_operator(m, 1) + @test_throws ErrorException register_nonlinear_operator(m, 1, f, 2) + @test_throws ErrorException register_nonlinear_operator(m, 1, f, name = :max) + m.op_lookup[:f] = (f, 1) + @test_throws ErrorException register_nonlinear_operator(m, 1, f) + empty!(m.op_lookup) + @test_throws ErrorException register_nonlinear_operator(m, 2, f) # test normal - @test add_user_defined_function(m, :f, 1, f).head == :f - @test m.func_lookup[:f] == (f, 1) + @test register_nonlinear_operator(m, 1, f).head == :f + @test m.op_lookup[:f] == (f, 1) @test last(m.registrations).f == f @test last(m.registrations).dim == 1 - @test last(m.registrations).op == :f - @test add_user_defined_function(m, :h, 2, h, hg).head == :h - @test m.func_lookup[:h] == (h, 2) + @test last(m.registrations).name == :f + @test register_nonlinear_operator(m, 2, h, hg).head == :h + @test m.op_lookup[:h] == (h, 2) @test last(m.registrations).f == h @test last(m.registrations).dim == 2 - @test last(m.registrations).op == :h + @test last(m.registrations).name == :h @test last(m.registrations).∇f == hg end - # test name_to_function - @testset "name_to_function" begin - @test name_to_function(m, :f) == f - @test name_to_function(m, :h) == h - @test name_to_function(m, :bad) isa Nothing + # test name_to_operator + @testset "name_to_operator" begin + @test name_to_operator(m, :f) == f + @test name_to_operator(m, :h) == h + @test name_to_operator(m, :bad) isa Nothing end - # test all_registered_functions - @testset "all_registered_functions" begin - @test all_registered_functions(m) isa Vector{Symbol} + # test all_registered_operators + @testset "all_registered_operators" begin + @test all_registered_operators(m) isa Vector{Symbol} end - # test user_registered_functions - @testset "user_registered_functions" begin - @test user_registered_functions(m) isa Vector{RegisteredFunction} + # test user_registered_operators + @testset "user_registered_operators" begin + @test user_registered_operators(m) isa Vector{RegisteredOperator} end empty!(m.registrations) - empty!(m.func_lookup) + empty!(m.op_lookup) # test @register @testset "@register" begin # test errors - @test @register(m, f1, 1, f) isa UserDefinedFunction - @test @register(m, f2, 1, f, f) isa UserDefinedFunction - @test @register(m, f3, 1, f, f, f) isa UserDefinedFunction - @test @register(m, h1, 2, h) isa UserDefinedFunction - @test @register(m, h2, 2, h, hg) isa UserDefinedFunction - @test @register(m, h3, 2, h, hg, ∇²h) isa UserDefinedFunction + @test @register(m, f1, 1, f) isa NonlinearOperator + @test @register(m, f2, 1, f, f) isa NonlinearOperator + @test @register(m, f3, 1, f, f, f) isa NonlinearOperator + @test @register(m, h1, 2, h) isa NonlinearOperator + @test @register(m, h2, 2, h, hg) isa NonlinearOperator + @test @register(m, h3, 2, h, hg, ∇²h) isa NonlinearOperator # test functional registration function registration_test() mt = InfiniteModel() @variable(mt, x) q(a) = 1 - @test @register(mt, my_q, 1, q) isa UserDefinedFunction + @test @register(mt, my_q, 1, q) isa NonlinearOperator q(x::GeneralVariableRef) = GenericNonlinearExpr{GeneralVariableRef}(:my_q, x) @test @expression(mt, q(x)) isa GenericNonlinearExpr return From 976f4ce8771f38ad0bd25dcfac5e455426a438f0 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Mon, 28 Aug 2023 13:11:59 -0400 Subject: [PATCH 26/40] fix `build_measure` --- src/measures.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/measures.jl b/src/measures.jl index b1fc94022..10d688253 100644 --- a/src/measures.jl +++ b/src/measures.jl @@ -756,8 +756,8 @@ an finite variable parameter bounds of finite variables that are included in the measure. """ function build_measure( - expr::T, - data::D; + expr::JuMP.AbstractJuMPScalar, + data::AbstractMeasureData ) vrefs = _all_function_variables(expr) expr_obj_nums = _object_numbers(expr) From bb1783056a104dae09c8532a33ce40bf1b050ea8 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Mon, 28 Aug 2023 13:21:51 -0400 Subject: [PATCH 27/40] Minor fixes --- docs/src/manual/expression.md | 9 +++++---- src/datatypes.jl | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/src/manual/expression.md b/docs/src/manual/expression.md index 48b56a803..cbe5e311d 100644 --- a/docs/src/manual/expression.md +++ b/docs/src/manual/expression.md @@ -39,14 +39,15 @@ JuMP.delete(::InfiniteModel, ::ParameterFunctionRef) ## [Nonlinear Expressions](@id nlp_manual) ### DataTypes ```@docs -RegisteredFunction +RegisteredOperator ``` ### Methods/Macros ```@docs -all_registered_functions -name_to_function -user_registered_functions +JuMP.register_nonlinear_operator +all_registered_operators +name_to_operator +user_registered_operator add_registered_to_jump ``` diff --git a/src/datatypes.jl b/src/datatypes.jl index f730a050a..aed0f684f 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -1535,7 +1535,7 @@ function Base.empty!(model::InfiniteModel)::InfiniteModel model.objective_has_measures = false # other stuff empty!(model.registrations) - empty!(model.func_lookup) + empty!(model.op_lookup) empty!(model.obj_dict) empty!(model.optimizer_model) model.ready_to_optimize = false From c09698d03f8c7d3595a7289f1826658905734dba Mon Sep 17 00:00:00 2001 From: pulsipher Date: Mon, 28 Aug 2023 14:13:32 -0400 Subject: [PATCH 28/40] fix tests --- src/nlp.jl | 23 +++++++++-------------- test/nlp.jl | 2 -- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/nlp.jl b/src/nlp.jl index 815885449..2c5846750 100644 --- a/src/nlp.jl +++ b/src/nlp.jl @@ -31,25 +31,20 @@ derivative. `∇²f` may be provided only if `∇f` is also provided. function JuMP.register_nonlinear_operator( model::InfiniteModel, dim::Int, + f::Function, funcs::Vararg{Function, N}; name::Symbol = Symbol(f) ) where {N} - if isempty(funcs) - error("Tried to register `$name`, but no evaluation function was given.") - elseif !all(f -> f isa Function, funcs) - error("The gradient and/or hessian must be functions, but got argument(s) " * - "of type `" * join(Tuple(typeof(f) for f in funcs if !(f isa Function)), "`, `") * - "`.") - elseif name in _NativeNLPOperators || name in keys(model.op_lookup) - error("An operator with name `$op` arguments is already " * + if name in _NativeNLPOperators || name in keys(model.op_lookup) + error("An operator with name `$name` arguments is already " * "registered. Please use a operator with a different name.") - elseif !hasmethod(funcs[1], NTuple{dim, Float64}) - error("The operator `$op` is not defined for arguments of type `Float64`.") + elseif !hasmethod(f, NTuple{dim, Float64}) + error("The operator evaluation function `$f` is not defined for arguments of type `Float64`.") end - push!(model.registrations, RegisteredOperator(name, dim, funcs...)) - model.op_lookup[name] = (funcs[1], dim) + push!(model.registrations, RegisteredOperator(name, dim, f, funcs...)) + model.op_lookup[name] = (f, dim) # TODO should we set the optimizer model to be out of date? - return JuMP.NonlinearOperator(name, funcs[1]) + return JuMP.NonlinearOperator(name, f) end """ @@ -63,7 +58,7 @@ functions of user-defined nonlinear operators. Currently, this does not return functions for default operators. """ function name_to_operator(model::InfiniteModel, name::Symbol) - haskey(model.op_lookup, name) && return model.op_lookup[op][1] + haskey(model.op_lookup, name) && return model.op_lookup[name][1] return end diff --git a/test/nlp.jl b/test/nlp.jl index 6985abd5a..2e044d215 100644 --- a/test/nlp.jl +++ b/test/nlp.jl @@ -24,8 +24,6 @@ # test JuMP.register_nonlinear_operator @testset "JuMP.register_nonlinear_operator" begin # test errors - @test_throws ErrorException register_nonlinear_operator(m, 1) - @test_throws ErrorException register_nonlinear_operator(m, 1, f, 2) @test_throws ErrorException register_nonlinear_operator(m, 1, f, name = :max) m.op_lookup[:f] = (f, 1) @test_throws ErrorException register_nonlinear_operator(m, 1, f) From 76be615c58875fbc91f06b93812a1700bdb4102e Mon Sep 17 00:00:00 2001 From: pulsipher Date: Thu, 31 Aug 2023 12:07:04 -0400 Subject: [PATCH 29/40] Update to JuMP's latest release --- docs/make.jl | 2 +- docs/src/develop/extensions.md | 36 +--- docs/src/guide/expression.md | 279 +++++++++------------------- docs/src/manual/expression.md | 12 +- src/InfiniteOpt.jl | 6 + src/TranscriptionOpt/transcribe.jl | 4 +- src/datatypes.jl | 75 ++------ src/nlp.jl | 69 ++++--- test/TranscriptionOpt/transcribe.jl | 10 +- test/datatypes.jl | 36 ++-- test/extensions/optimizer_model.jl | 8 +- test/nlp.jl | 82 ++++---- test/runtests.jl | 2 +- 13 files changed, 230 insertions(+), 391 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index b342255a0..a5f9bdf60 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,5 +1,5 @@ import Pkg -Pkg.pkg"add JuMP#od/nlp-expr" +Pkg.pkg"add JuMP#master" using Documenter, InfiniteOpt, Distributions, Literate, Random diff --git a/docs/src/develop/extensions.md b/docs/src/develop/extensions.md index acf473c85..734d6dc61 100644 --- a/docs/src/develop/extensions.md +++ b/docs/src/develop/extensions.md @@ -871,23 +871,19 @@ function _make_expression( return _make_expression(opt_model, measure_function(expr)) end # AffExpr/QuadExpr -function _make_expression(opt_model::Model, expr::Union{GenericAffExpr, GenericQuadExpr}) +function _make_expression(opt_model::Model, expr::Union{GenericAffExpr, GenericQuadExpr, GenericNonlinearExpr}) return map_expression(v -> _make_expression(opt_model, v), expr) end -# NLPExpr -function _make_expression(opt_model::Model, expr::NLPExpr) - return add_nonlinear_expression(opt_model, map_nlp_to_ast(v -> _make_expression(opt_model, v), expr)) -end # output -_make_expression (generic function with 8 methods) +_make_expression (generic function with 7 methods) ``` For simplicity in example, above we assume that only `DistributionDomain`s are used, there are not any `PointVariableRef`s, and all `MeasureRef`s correspond to expectations. Naturally, a full extension should include checks to enforce that -such assumptions hold. Notice that [`map_expression`](@ref) and -[`map_nlp_to_ast`](@ref) are useful for converting expressions. +such assumptions hold. Notice that [`map_expression`](@ref) is useful for +converting expressions. Now let's extend [`build_optimizer_model!`](@ref) for `DeterministicModel`s. Such extensions should build an optimizer model in place and in general should @@ -908,13 +904,13 @@ function InfiniteOpt.build_optimizer_model!( # clear the model for a build/rebuild determ_model = InfiniteOpt.clear_optimizer_model_build!(model) - # add the registered functions if there are any - add_registered_to_jump(determ_model, model) + # add user-defined nonlinear operators if there are any + add_operators_to_jump(determ_model, model) # add variables for vref in all_variables(model) if index(vref) isa InfiniteVariableIndex - start = NaN # easy hack + start = NaN # simple hack for sake of example else start = start_value(vref) start = isnothing(start) ? NaN : start @@ -932,26 +928,12 @@ function InfiniteOpt.build_optimizer_model!( # add the objective obj_func = _make_expression(determ_model, objective_function(model)) - if obj_func isa NonlinearExpression - set_nonlinear_objective(determ_model, objective_sense(model), obj_func) - else - set_objective(determ_model, objective_sense(model), obj_func) - end + set_objective(determ_model, objective_sense(model), obj_func) # add the constraints - for cref in all_constraints(model, Union{GenericAffExpr, GenericQuadExpr, NLPExpr}) + for cref in all_constraints(model, Union{GenericAffExpr, GenericQuadExpr, GenericNonlinearExpr}) constr = constraint_object(cref) new_func = _make_expression(determ_model, constr.func) - if new_func isa NonlinearExpression - if constr.set isa MOI.LessThan - ex = :($new_func <= $(constr.set.upper)) - elseif constr.set isa MOI.GreaterThan - ex = :($new_func >= $(constr.set.lower)) - else # assume it is MOI.EqualTo - ex = :($new_func == $(constr.set.value)) - end - new_cref = add_nonlinear_constraint(determ_model, ex) - else new_constr = build_constraint(error, new_func, constr.set) new_cref = add_constraint(determ_model, new_constr, name(cref)) end diff --git a/docs/src/guide/expression.md b/docs/src/guide/expression.md index 194af8e9c..a1a4f7364 100644 --- a/docs/src/guide/expression.md +++ b/docs/src/guide/expression.md @@ -266,12 +266,10 @@ In this section, we walk you through the ins and out of working with general nonlinear (i.e., not affine or quadratic) expressions in `InfiniteOpt`. !!! info - Our previous `InfiniteOpt` specific nonlinear API as been deprecated in + Our previous `InfiniteOpt` specific nonlinear API as been removed in favor of `JuMP`'s new and improved nonlinear interface. Thus, `InfiniteOpt` now strictly uses the same expression structures as `JuMP`. - - ### Basic Usage We can define nonlinear expressions in similar manner to how affine/quadratic expressions are made in `JuMP`. For instance, we can make an expression using @@ -280,72 +278,39 @@ normal Julia code outside a macro: julia> @infinite_parameter(model, t ∈ [0, 1]); @variable(model, y, Infinite(t)); julia> expr = exp(y^2.3) * y - 42 -exp(y(t)^2.3) * y(t) - 42 +(exp(y(t) ^ 2.3) * y(t)) - 42.0 julia> typeof(expr) -NLPExpr +GenericNonlinearExpr{GeneralVariableRef} ``` -Thus, the nonlinear expression `expr` of type [`NLPExpr`](@ref) is created can -be readily incorporated to other expressions, the objective, and/or constraints. -For macro-based definition, we simply use the `@expression`, `@objective`, and -`@constraint` macros (which in `JuMP` are only able to handle affine/quadratic -expressions): +Thus, the nonlinear expression `expr` of type +[`GenericNonlinearExpr`](https://jump.dev/JuMP.jl/v1/api/JuMP/#GenericNonlinearExpr) +is created and can be readily incorporated into other expressions, the objective, +and/or constraints. For macro-based definition, we simply use the `@expression`, +`@objective`, and `@constraint` macros as normal: ```jldoctest nlp julia> @expression(model, expr, exp(y^2.3) * y - 42) -exp(y(t)^2.3) * y(t) - 42 +(exp(y(t) ^ 2.3) * y(t)) - 42.0 julia> @objective(model, Min, ∫(0.3^cos(y^2), t)) -∫{t ∈ [0, 1]}[0.3^cos(y(t)²)] +∫{t ∈ [0, 1]}[0.3 ^ cos(y(t)²)] + julia> @constraint(model, constr, y^y * sin(y) + sum(y^i for i in 3:4) == 3) -constr : (y(t)^y(t) * sin(y(t)) + y(t)^3 + y(t)^4) - 3 = 0, ∀ t ∈ [0, 1] +constr : (((y(t) ^ y(t)) * sin(y(t))) + (y(t) ^ 3) + (y(t) ^ 4)) - 3.0 = 0, ∀ t ∈ [0, 1] ``` !!! note - The `@NLexpression`, `@NLobjective`, and `@NLconstraint` macros used by `JuMP` - are not supported by `InfiniteOpt`. Instead, we can more conveniently use the - `@expression`, `@objective`, and `@constraint` macros directly. - -Natively, we support all the same nonlinear functions/operators that `JuMP` -does. Note however that there are 3 caveats to this: -- Functions from [`SpecialFunctions.jl`](https://github.com/JuliaMath/SpecialFunctions.jl) - can only be used if `using SpecialFunctions` is included first -- The `ifelse` function must be specified [`InfiniteOpt.ifelse`](@ref) (because - the native `ifelse` is a core function that cannot be extended for our purposes) -- The logic operators `&` and `|` must be used instead of `&&` and `||` when - defining a nonlinear expression. - -Let's exemplify the above caveats: -```jldoctest nlp -julia> using SpecialFunctions - -julia> y^2.3 * gamma(y) -y(t)^2.3 * gamma(y(t)) - -julia> InfiniteOpt.ifelse(y == 0, y^2.3, exp(y)) -ifelse(y(t) == 0, y(t)^2.3, exp(y(t))) + The legacy `@NLexpression`, `@NLobjective`, and `@NLconstraint` macros in `JuMP` + are not supported by `InfiniteOpt`. -julia> InfiniteOpt.ifelse((y <= 0) | (y >= 3), y^2.3, exp(y)) -ifelse(y(t) <= 0 || y(t) >= 3, y(t)^2.3, exp(y(t))) -``` - -!!! warning - The logical comparison operator `==` will yield an `NLPExpr` instead of a - `Bool` when one side is a variable reference or an expression. Thus, for - creating Julia code that needs to determine if the Julia variables are equal - then `isequal` should be used instead: - ```julia-repl - julia> isequal(y, y) - true - - julia> y == t - y(t) == t - ``` +Natively, we support all the same nonlinear operators that `JuMP` +does. See [JuMP's documentation](https://jump.dev/JuMP.jl/v1/manual/nonlinear/#Supported-operators) +for more information. -We can interrogate which nonlinear functions/operators our model currently -supports by invoking [`all_registered_functions`](@ref). Moreover, we can add -additional functions via registration (see [Function Registration](@ref) for -more details). +We can interrogate which nonlinear operators our model currently +supports by invoking [`all_nonlinear_operators`](@ref). Moreover, we can add +additional operators (see [Adding Nonlinear Operators](@ref) for more details). Finally, we highlight that nonlinear expressions in `InfiniteOpt` support the same linear algebra operations as affine/quadratic expressions: @@ -353,7 +318,7 @@ same linear algebra operations as affine/quadratic expressions: julia> @variable(model, v[1:2]); @variable(model, Q[1:2, 1:2]); julia> @expression(model, v' * Q * v) -0 + (Q[1,1]*v[1] + Q[2,1]*v[2]) * v[1] + (Q[1,2]*v[1] + Q[2,2]*v[2]) * v[2] +0.0 + ((Q[1,2]*v[1] + Q[2,2]*v[2]) * v[2]) + ((Q[1,1]*v[1] + Q[2,1]*v[2]) * v[1]) ``` ### Function Tracing @@ -364,20 +329,22 @@ satisfy certain criteria. For instance: julia> myfunc(x) = sin(x^3) / tan(2^x); julia> expr = myfunc(y) -sin(y(t)^3) / tan(2^y(t)) +sin(y(t) ^ 3) / tan(2.0 ^ y(t)) ``` However, there are certain limitations as to what internal code these functions can contain. The following CANNOT be used: - loops (unless it only uses very simple operations) - if-statements (see workaround below) -- non-registered functions (if they cannot be traced). +- unrecognized operators (if they cannot be traced). !!! tip - If a particular function is not amendable for tracing, try registering it - instead. See [Function Registration](@ref) for details. + If a particular function is not amendable for tracing, try adding it + as a new nonlinear operator instead. See [Add Nonlinear Operators](@ref) + for details. -We can readily workaround the if-statement limitation using -[`InfiniteOpt.ifelse`](@ref). For example, the function: +We can readily work around the if-statement limitation using `op_ifelse` which +is a nonlinear operator version of `Base.ifelse` and follows the same syntax. +For example, the function: ```julia function mylogicfunc(x) if x >= 0 @@ -390,17 +357,20 @@ end is not amendable for function tracing, but we can rewrite it as: ```jldoctest nlp julia> function mylogicfunc(x) - return InfiniteOpt.ifelse(x >= 0, x^3, 0) + return op_ifelse(op_greater_than_or_equal_to(x, 0), x^3, 0) end mylogicfunc (generic function with 1 method) julia> mylogicfunc(y) -ifelse(y(t) >= 0, y(t)^3, 0) +ifelse(y(t) >= 0, y(t) ^ 3, 0) ``` -which is amendable for function tracing. +which is amendable for function tracing. Note that the basic logic operators +(e.g., `<=`) have special nonlinear operator analogues when used outside of a +macro. See [JuMP's documentation](https://jump.dev/JuMP.jl/v1/manual/nonlinear/#Limitations) +for more details. ### Linear Algebra -As described above in the Basic Usage Section, we support linear algebra +As described above in the Basic Usage Section, we support basic linear algebra operations with nonlinear expressions! This relies on our basic extensions of [`MutableArithmetics`](https://github.com/jump-dev/MutableArithmetics.jl), but admittedly this implementation is not perfect in terms of efficiency. @@ -411,10 +381,10 @@ admittedly this implementation is not perfect in terms of efficiency. used instead when efficiency is critical. ```jldoctest nlp julia> v' * Q * v # convenient linear algebra syntax - 0 + (Q[1,1]*v[1] + Q[2,1]*v[2]) * v[1] + (Q[1,2]*v[1] + Q[2,2]*v[2]) * v[2] + (+(0.0) + ((Q[1,1]*v[1] + Q[2,1]*v[2]) * v[1])) + ((Q[1,2]*v[1] + Q[2,2]*v[2]) * v[2]) julia> sum(v[i] * Q[i, j] * v[j] for i in 1:2, j in 1:2) # more efficient - v[1] * Q[1,1] * v[1] + v[2] * Q[2,1] * v[1] + v[1] * Q[1,2] * v[2] + v[2] * Q[2,2] * v[2] + ((((v[1]*Q[1,1]) * v[1]) + ((v[2]*Q[2,1]) * v[1])) + ((v[1]*Q[1,2]) * v[2])) + ((v[2]*Q[2,2]) * v[2]) ``` We can also set vectorized constraints using the `.==`, `.<=`, and `.>=` @@ -424,56 +394,46 @@ julia> @variable(model, W[1:2, 1:2]); julia> @constraint(model, W * Q * v .== 0) 2-element Vector{InfOptConstraintRef}: - (0 + (W[1,1]*Q[1,1] + W[1,2]*Q[2,1]) * v[1] + (W[1,1]*Q[1,2] + W[1,2]*Q[2,2]) * v[2]) - 0 = 0 - (0 + (W[2,1]*Q[1,1] + W[2,2]*Q[2,1]) * v[1] + (W[2,1]*Q[1,2] + W[2,2]*Q[2,2]) * v[2]) - 0 = 0 -``` - -However, it is important to note that although vector constraints can be -expressed in `InfiniteOpt`, they are not supported by `JuMP` and thus an error -is incurred if we try to solve an `InfiniteOpt` model using the -`TranscriptionOpt` backend: -```jldoctest nlp -julia> @constraint(model, W * Q * v in MOI.Zeros(2)) # will cause solution error -[0 + (W[1,1]*Q[1,1] + W[1,2]*Q[2,1]) * v[1] + (W[1,1]*Q[1,2] + W[1,2]*Q[2,2]) * v[2], 0 + (W[2,1]*Q[1,1] + W[2,2]*Q[2,1]) * v[1] + (W[2,1]*Q[1,2] + W[2,2]*Q[2,2]) * v[2]] in MathOptInterface.Zeros(2) - -julia> optimize!(model) -ERROR: TranscriptionOpt does not support vector constraints of general nonlinear expressions because this is not yet supported by JuMP. + ((+(0.0) + ((W[1,1]*Q[1,1] + W[1,2]*Q[2,1]) * v[1])) + ((W[1,1]*Q[1,2] + W[1,2]*Q[2,2]) * v[2])) - 0.0 = 0 + ((+(0.0) + ((W[2,1]*Q[1,1] + W[2,2]*Q[2,1]) * v[1])) + ((W[2,1]*Q[1,2] + W[2,2]*Q[2,2]) * v[2])) - 0.0 = 0 ``` -### Function Registration -In a similar spirit to `JuMP` and `Symbolics`, we can register user-defined -functions such that they can be directly incorporated into nonlinear expressions. -This is done via the [`@register`](@ref) macro. We can register any function -that takes scalar arguments (which can accept inputs of type `Real`): +### Add Nonlinear Operators +In a similar spirit to `JuMP` and `Symbolics`, we can add nonlinear operators +such that they can be directly incorporated into nonlinear expressions as atoms +(they will not be traced). This is done via the +[`@operator`](https://jump.dev/JuMP.jl/v1/api/JuMP/#@operator) macro. We can +register any operator that takes scalar arguments (which can accept inputs of +type `Real`): ```jldoctest nlp -julia> h(a, b) = a * b^2; # an overly simple example user-defined function +julia> h(a, b) = a * b^2; # an overly simple example operator -julia> @register(model, h(a, b)); +julia> @operator(model, op_h2, 2, h, name = :h); -julia> h(y, 42) -h(y(t), 42) +julia> op_h(y, 42) +op_h(y(t), 42) ``` !!! tip - Where possible it is preferred to use function tracing instead of function - registration. This improves performance and can prevent unintentional errors. + Where possible it is preferred to use function tracing instead. This improves + performance and can prevent unintentional errors. See [Function Tracing](@ref) for more details. -To highlight the difference between function tracing and function -registration consider the following example: +To highlight the difference between function tracing and operator definition +consider the following example: ```jldoctest nlp julia> f(a) = a^3; julia> f(y) # user-function gets traced -y(t)^3 +y(t) ^ 3 -julia> @register(model, f(a)) # register function -f (generic function with 2 methods) +julia> @operator(model, op_f, 1, f) # create nonlinear operator +NonlinearOperator(:op_f, f) -julia> f(y) # function is no longer traced -f(y(t)) +julia> op_f(y) # function is no longer traced +op_f(y(t)) ``` -Thus, registered functions are incorporated directly. This means that their +Thus, nonlinear operators are incorporated directly. This means that their gradients and hessians will need to determined as well (typically occurs behind the scenes via auto-differentiation with the selected optimizer model backend). However, again please note that in this case tracing is preferred @@ -493,47 +453,42 @@ julia> function g(a) return a end; -julia> @register(model, g(a)); +julia> @operator(model, op_g, 1, g); -julia> g(y) -g(y(t)) +julia> op_g(y) +op_g(y(t)) ``` Notice this example is a little contrived still, highlighting that in most cases -we can avoid registration. However, one exception to this trend, are functions +we can avoid adding operators. However, one exception to this trend, are functions from other packages that we might want to use. For example, perhaps we would like to use the `eta` function from `SpecialFunctions.jl` which is not natively supported: ```jldoctest nlp julia> using SpecialFunctions -julia> my_eta(a) = eta(a); - -julia> @register(model, my_eta(a)); +julia> @operator(model, op_eta, 1, eta) +NonlinearOperator(:op_eta, eta) -julia> my_eta(y) -my_eta(y(t)) +julia> op_eta(y) +op_eta(y(t)) ``` -Notice that we cannot register `SpecialFunctions.eta` directly due to -scoping limitations that are inherited in generating constructor functions on the -fly (which necessarily occurs behind the scenes with [`@register`](@ref)). Now in some cases we might wish to specify the gradient and hessian of a -univariate function we register to avoid the need for auto-differentiation. We -can do this, simply by adding them as additional arguments when we register: +univariate operator to avoid the need for auto-differentiation. We +can do this, simply by adding them as additional arguments in `@operator`: ```jldoctest nlp julia> my_squared(a) = a^2; gradient(a) = 2 * a; hessian(a) = 2; -julia> @register(model, my_squared(a), gradient, hessian); +julia> @operator(model, op_square, 1, my_squared, gradient, hessian); -julia> my_squared(y) -my_squared(y(t)) +julia> op_square(y) +op_square(y(t)) ``` Note the specification of the hessian is optional (it can separately be computed via auto-differentiation if need be). -For multivariate functions, we can specify the gradient (the hessian is not -currently supported by `JuMP` optimizer models) following the same gradient -function structure that `JuMP` uses: +For multivariate functions, we can specify the gradient following the same +gradient function structure that `JuMP` uses: ```jldoctest nlp julia> w(a, b) = a * b^2; @@ -543,85 +498,19 @@ julia> function wg(v, a, b) return end; -julia> @register(model, w(a, b), wg) # register multi-argument function -w (generic function with 4 methods) +julia> @operator(model, op_w, 2, w, wg) +NonlinearOperator(:op_w, w) -julia> w(42, y) -w(42, y(t)) +julia> op_w(42, y) +op_w(42, y(t)) ``` Note that the first argument of the gradient needs to accept an `AbstractVector{Real}` that is then filled in place. !!! note We do not currently support vector inputs or vector valued functions - directly, since typically `JuMP` optimizer model backends don't support them. - However, this limitation can readily be removed if there is a use case for it - (please reach out to us if such an addition is needed). - -### Expression Tree Abstraction -The nonlinear interface in `InfiniteOpt` is enabled through the [`NLPExpr`](@ref) -type which uses an intelligent expression tree structure. In particular, we use -a memory efficient [Left-Child Right-Sibling Tree](https://en.wikipedia.org/wiki/Left-child_right-sibling_binary_tree) -whose leaves (nodes with no children) can be: -- constants (i.e., `Int`, `Float64`, and/or `Bool`) -- variables ([`GeneralVariableRef`](@ref)s) -- affine expressions (`GenericAffExpr{Float64, GeneralVariableRef}`) -- quadratic expressions (`GenericQuadExpr{Float64, GeneralVariableRef}`) -Moreover, the internal tree nodes correspond to functions/operators which are -stored as `Symbol` names (which correspond to registered functions via -[`name_to_function`](@ref)). We accomplish this via -[`LeftChildRightSiblingTrees.jl`](https://github.com/JuliaCollections/LeftChildRightSiblingTrees.jl) -in combination with [`NodeData`](@ref) to store the content of each node. - -We can view the tree structure of an [`NLPExpr`](@ref) using -[`print_expression_tree`](@ref): -```jldoctest nlp -julia> expr = exp(y^2.3) * y - 42 -exp(y(t)^2.3) * y(t) - 42 - -julia> print_expression_tree(expr) -- -├─ * -│ ├─ exp -│ │ └─ ^ -│ │ ├─ y(t) -│ │ └─ 2.3 -│ └─ y(t) -└─ 42 -``` -Here, we can see the algebraic expression is decomposed into an expression -tree were the leaves contain the variables/constants (and can contain -affine/quadratic expressions) and the intermediate nodes contain function -names. Note that the top most node is called the root node and that is what -[`NLPExpr`](@ref) stores in its `tree_root` field: -```jldoctest nlp -julia> expr.tree_root -Node(-) + directly, since typically `JuMP` optimizer model backends don't support them. -julia> typeof(expr.tree_root) -LeftChildRightSiblingTrees.Node{NodeData} -``` -The rest of the tree can then be interrogated by traversing the tree as enabled -by the API of -[`LeftChildRightSiblingTrees.jl`](https://github.com/JuliaCollections/LeftChildRightSiblingTrees.jl). - -In addition to the API of `LeftChildRightSiblingTrees.jl`, we provide some -mapping functions that are useful for extensions. First, with -[`map_expression`](@ref) we can create a new `NLPExpr` based on an existing -`NLPExpr` where a transformation is applied to each variable: -```jldoctest nlp -julia> map_expression(v -> v^2, expr) -exp((y(t)²)^2.3) * (y(t)²) - 42 -``` -We also provide [`map_nlp_to_ast`](@ref) which can be used to map an `NLPExpr` to a -Julia Abstract Syntax Tree (AST) where a transformation is applied to each -variable: -```jldoctest nlp -julia> jump_model = Model(); @variable(jump_model, y_jump); - -julia> map_nlp_to_ast(v -> y_jump, expr) -:(exp(y_jump ^ 2.3) * y_jump - 42) -``` -This is useful for converting `NLPExpr`s into ASTs that can be used in `JuMP` -via its [`add_nonlinear_expression`](https://jump.dev/JuMP.jl/v1/manual/nlp/#Raw-expression-input) -API. +### More Details +For more details, please consult +[JuMP's Documentation](https://jump.dev/JuMP.jl/v1/manual/nonlinear/). diff --git a/docs/src/manual/expression.md b/docs/src/manual/expression.md index cbe5e311d..97c81a298 100644 --- a/docs/src/manual/expression.md +++ b/docs/src/manual/expression.md @@ -39,16 +39,16 @@ JuMP.delete(::InfiniteModel, ::ParameterFunctionRef) ## [Nonlinear Expressions](@id nlp_manual) ### DataTypes ```@docs -RegisteredOperator +NLPOperator ``` -### Methods/Macros +### Methods ```@docs -JuMP.register_nonlinear_operator -all_registered_operators +JuMP.add_nonlinear_operator +all_nonlinear_operators name_to_operator -user_registered_operator -add_registered_to_jump +added_nonlinear_operators +add_operators_to_jump ``` ## Expression Methods diff --git a/src/InfiniteOpt.jl b/src/InfiniteOpt.jl index ff314cb4d..90e7f82b3 100644 --- a/src/InfiniteOpt.jl +++ b/src/InfiniteOpt.jl @@ -56,6 +56,12 @@ include("general_variables.jl") include("TranscriptionOpt/TranscriptionOpt.jl") Reexport.@reexport using .TranscriptionOpt +# Add deprecation and old syntax methods +macro register(args...) + error("`@register` has now been replaced with `@operator`, see ", + "the nonlinear documenation page for details.") +end + # Define additional stuff that should not be exported const _EXCLUDE_SYMBOLS = [Symbol(@__MODULE__), :eval, :include] diff --git a/src/TranscriptionOpt/transcribe.jl b/src/TranscriptionOpt/transcribe.jl index 0e2fdc433..ccaf430f2 100644 --- a/src/TranscriptionOpt/transcribe.jl +++ b/src/TranscriptionOpt/transcribe.jl @@ -866,8 +866,8 @@ function build_transcription_model!( "and thus naive solution of the discretized problem may be slow. " * "This warning can be turned off via `check_support_dims = false`.") end - # register functions as needed - InfiniteOpt.add_registered_to_jump(trans_model, inf_model) + # add nonlinear operators as needed + InfiniteOpt.add_operators_to_jump(trans_model, inf_model) # define the variables transcribe_finite_variables!(trans_model, inf_model) transcribe_infinite_variables!(trans_model, inf_model) diff --git a/src/datatypes.jl b/src/datatypes.jl index aed0f684f..3a71ef158 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -1190,16 +1190,16 @@ mutable struct ConstraintData{C <: JuMP.AbstractConstraint} <: AbstractDataObjec end ################################################################################ -# Operator Registration +# NONLINEAR OPERATORS ################################################################################ """ - RegisteredOperator{F <: Function, G <: Union{Function, Nothing}, + NLPOperator{F <: Function, G <: Union{Function, Nothing}, H <: Union{Function, Nothing}} -A type for storing used defined registered nonlinear operators and their information -that is needed by JuMP for build an `NLPEvaluator`. The constructor is of the form: +A type for storing new nonlinear operators and their information +that is ultimately for automatic differentiation. The constructor is of the form: ```julia - RegisteredOperator(name::Symbol, dim::Int, f::Function, + NLPOperator(name::Symbol, dim::Int, f::Function, [∇f::Function, ∇²f::Function]) ``` @@ -1210,7 +1210,7 @@ that is needed by JuMP for build an `NLPEvaluator`. The constructor is of the fo - `∇f::G`: The gradient function if one is given. - `∇²f::H`: The hessian function if one is given. """ -struct RegisteredOperator{F <: Function, G, H} +struct NLPOperator{F <: Function, G, H} name::Symbol dim::Int f::F @@ -1218,14 +1218,14 @@ struct RegisteredOperator{F <: Function, G, H} ∇²f::H # Constructors - function RegisteredOperator( + function NLPOperator( name::Symbol, dim::Int, f::F ) where {F <: Function} return new{F, Nothing, Nothing}(name, dim, f, nothing, nothing) end - function RegisteredOperator( + function NLPOperator( name::Symbol, dim::Int, f::F, @@ -1238,7 +1238,7 @@ struct RegisteredOperator{F <: Function, G, H} end return new{F, G, Nothing}(name, dim, f, ∇f, nothing) end - function RegisteredOperator( + function NLPOperator( name::Symbol, dim::Int, f::F, @@ -1264,57 +1264,6 @@ end A `DataType` for storing all of the mathematical modeling information needed to model an optmization problem with an infinite-dimensional decision space. - -**Fields** -- `independent_params::MOIUC.CleverDict{IndependentParameterIndex, ScalarParameterData{IndependentParameter}}`: - The independent parameters and their mapping information. -- `dependent_params::MOIUC.CleverDict{DependentParametersIndex, MultiParameterData}`: - The dependent parameters and their mapping information. -- `finite_params::MOIUC.CleverDict{FiniteParameterIndex, ScalarParameterData{FiniteParameter}}`: - The finite parameters and their mapping information. -- `name_to_param::Union{Dict{String, AbstractInfOptIndex}, Nothing}`: - Field to help find a parameter given the name. -- `last_param_num::Int`: The last parameter number to be used. -- `param_object_indices::Vector{Union{IndependentParameterIndex, DependentParametersIndex}}`: - The collection of parameter object indices in creation order. -- `param_functions::MOIUC.CleverDict{ParameterFunctionIndex, ParameterFunctionData{ParameterFunction}}`: - The infinite parameter functions and their mapping information. -- `infinite_vars::MOIUC.CleverDict{InfiniteVariableIndex, <:VariableData{<:InfiniteVariable}}`: - The infinite variables and their mapping information. -- `semi_infinite_vars::MOIUC.CleverDict{SemiInfiniteVariableIndex, <:VariableData{<:SemiInfiniteVariable}}`: - The semi-infinite variables and their mapping information. -- `semi_lookup::Dict{<:Tuple, SemiInfiniteVariableIndex}`: Look-up if a variable already already exists. -- `point_vars::MOIUC.CleverDict{PointVariableIndex, <:VariableData{<:PointVariable}}`: - The point variables and their mapping information. -- `point_lookup::Dict{<:Tuple, PointVariableIndex}`: Look-up if a variable already exists. -- `finite_vars::MOIUC.CleverDict{FiniteVariableIndex, VariableData{JuMP.ScalarVariable{Float64, Float64, Float64, Float64}}}`: - The finite variables and their mapping information. -- `name_to_var::Union{Dict{String, AbstractInfOptIndex}, Nothing}`: - Field to help find a variable given the name. -- `derivatives::MOIUC.CleverDict{DerivativeIndex, <:VariableData{<:Derivative}}`: - The derivatives and their mapping information. -- `deriv_lookup::Dict{<:Tuple, DerivativeIndex}`: Map derivative variable-parameter - pairs to a derivative index to prevent duplicates. -- `measures::MOIUC.CleverDict{MeasureIndex, <:MeasureData}`: - The measures and their mapping information. -- `integral_defaults::Dict{Symbol}`: - The default keyword arguments for [`integral`](@ref). -- `constraints::MOIUC.CleverDict{InfOptConstraintIndex, <:ConstraintData}`: - The constraints and their mapping information. -- `constraint_restrictions::Dict{InfOptConstraintIndex, <:DomainRestrictions}` Map constraints - to their domain restrictions if they have any. -- `name_to_constr::Union{Dict{String, InfOptConstraintIndex}, Nothing}`: - Field to help find a constraint given the name. -- `objective_sense::MOI.OptimizationSense`: Objective sense. -- `objective_function::JuMP.AbstractJuMPScalar`: Finite scalar function. -- `objective_has_measures::Bool`: Does the objective contain measures? -- `registrations::Vector{RegisteredOperator}`: The registered nonlinear operators. -- `op_lookup::Dict{Symbol, Tuple{Function, Int}}`: Map a name to a registered operator and its dimension. -- `obj_dict::Dict{Symbol, Any}`: Store Julia symbols used with `InfiniteModel` -- `optimizer_constructor`: MOI optimizer constructor (e.g., Gurobi.Optimizer). -- `optimizer_model::JuMP.Model`: Model used to solve `InfiniteModel` -- `ready_to_optimize::Bool`: Is the optimizer_model up to date. -- `ext::Dict{Symbol, Any}`: Store arbitrary extension information. """ mutable struct InfiniteModel <: JuMP.AbstractModel # Parameter Data @@ -1353,7 +1302,7 @@ mutable struct InfiniteModel <: JuMP.AbstractModel objective_has_measures::Bool # Operator Registration - registrations::Vector{RegisteredOperator} + operators::Vector{NLPOperator} op_lookup::Dict{Symbol, Tuple{Function, Int}} # Objects @@ -1444,7 +1393,7 @@ function InfiniteModel(; zero(JuMP.GenericAffExpr{Float64, GeneralVariableRef}), false, # registration - RegisteredOperator[], + NLPOperator[], Dict{Symbol, Tuple{Function, Int}}(), # Object dictionary Dict{Symbol, Any}(), @@ -1534,7 +1483,7 @@ function Base.empty!(model::InfiniteModel)::InfiniteModel model.objective_function = zero(JuMP.GenericAffExpr{Float64, GeneralVariableRef}) model.objective_has_measures = false # other stuff - empty!(model.registrations) + empty!(model.operators) empty!(model.op_lookup) empty!(model.obj_dict) empty!(model.optimizer_model) diff --git a/src/nlp.jl b/src/nlp.jl index 2c5846750..df91db450 100644 --- a/src/nlp.jl +++ b/src/nlp.jl @@ -1,5 +1,3 @@ -# TODO add deprecation errors for old methods - ################################################################################ # USER OPERATORS ################################################################################ @@ -8,9 +6,8 @@ const _NativeNLPOperators = append!(copy(MOI.Nonlinear.DEFAULT_UNIVARIATE_OPERAT MOI.Nonlinear.DEFAULT_MULTIVARIATE_OPERATORS) append!(_NativeNLPOperators, (:&&, :||, :<=, :(==), :>=, :<, :>)) -# TODO expand details in docstring """ - JuMP.register_nonlinear_operator( + JuMP.add_nonlinear_operator( model::InfiniteModel, dim::Int, f::Function, @@ -19,16 +16,29 @@ append!(_NativeNLPOperators, (:&&, :||, :<=, :(==), :>=, :<, :>)) [name::Symbol = Symbol(f)] ) -Extend `register_nonlinear_operator` for `InfiniteModel`s. +Extend `add_nonlinear_operator` for `InfiniteModel`s. Add a new nonlinear operator with `dim` input arguments to `model` and associate -it with the name `name`. +it with the name `name`. Alternatively, [`@operator`](https://jump.dev/JuMP.jl/v1/api/JuMP/#@operator) +can be used for a more convenient syntax. The function `f` evaluates the operator. The optional function `∇f` evaluates the first derivative, and the optional function `∇²f` evaluates the second derivative. `∇²f` may be provided only if `∇f` is also provided. + +```julia-repl +julia> @variable(model, y); + +julia> g(x) = x^2; + +julia> new_op = add_nonlinear_operator(model, 1, g) +NonlinearOperator(:g, g) + +julia> @expression(model, new_op(y)) +g(y) +``` """ -function JuMP.register_nonlinear_operator( +function JuMP.add_nonlinear_operator( model::InfiniteModel, dim::Int, f::Function, @@ -37,11 +47,11 @@ function JuMP.register_nonlinear_operator( ) where {N} if name in _NativeNLPOperators || name in keys(model.op_lookup) error("An operator with name `$name` arguments is already " * - "registered. Please use a operator with a different name.") + "added. Please use a operator with a different name.") elseif !hasmethod(f, NTuple{dim, Float64}) error("The operator evaluation function `$f` is not defined for arguments of type `Float64`.") end - push!(model.registrations, RegisteredOperator(name, dim, f, funcs...)) + push!(model.operators, NLPOperator(name, dim, f, funcs...)) model.op_lookup[name] = (f, dim) # TODO should we set the optimizer model to be out of date? return JuMP.NonlinearOperator(name, f) @@ -50,9 +60,8 @@ end """ name_to_operator(model::InfiniteModel, name::Symbol)::Union{Function, Nothing} -Return the registered operator that corresponds to `name`. -Returns `nothing` if no such registered operator exists. This helps retrieve the -functions of user-defined nonlinear operators. +Return the nonlinear operator that corresponds to `name`. +Returns `nothing` if no such operator exists. !!! warning Currently, this does not return functions for default operators. @@ -63,58 +72,58 @@ function name_to_operator(model::InfiniteModel, name::Symbol) end """ - all_registered_operators(model::InfiniteModel)::Vector{Symbol} + all_nonlinear_operators(model::InfiniteModel)::Vector{Symbol} -Retrieve all the operators that are currently registered to `model`. +Retrieve all the operators that are currently added to `model`. """ -function all_registered_operators(model::InfiniteModel) +function all_nonlinear_operators(model::InfiniteModel) return append!(copy(_NativeNLPOperators), map(v -> Symbol(first(v)), values(model.op_lookup))) end """ - user_registered_operators(model::InfiniteModel)::Vector{RegisteredOperator} + user_defined_operators(model::InfiniteModel)::Vector{NLPOperator} Return all the operators (and their associated information) that the user has -registered to `model`. Each is stored as a [`RegisteredOperator`](@ref). +added to `model`. Each is stored as a [`NLPOperator`](@ref). """ -function user_registered_operators(model::InfiniteModel) - return model.registrations +function added_nonlinear_operators(model::InfiniteModel) + return model.operators end -## Define helper function to add registered operators to JuMP +## Define helper function to add nonlinear operators to JuMP # No gradient or hessian function _add_op_data_to_jump( model::JuMP.Model, - data::RegisteredOperator{F, Nothing, Nothing} + data::NLPOperator{F, Nothing, Nothing} ) where {F <: Function} - JuMP.register_nonlinear_operator(model, data.dim, data.f, name = data.name) + JuMP.add_nonlinear_operator(model, data.dim, data.f, name = data.name) return end # Only gradient information function _add_op_data_to_jump( model::JuMP.Model, - data::RegisteredOperator{F, G, Nothing} + data::NLPOperator{F, G, Nothing} ) where {F <: Function, G <: Function} - JuMP.register_nonlinear_operator(model, data.dim, data.f, data.∇f, name = data.name) + JuMP.add_nonlinear_operator(model, data.dim, data.f, data.∇f, name = data.name) return end # Gradient and hessian information -function _add_op_data_to_jump(model::JuMP.Model, data::RegisteredOperator) - JuMP.register_nonlinear_operator(model, data.dim, data.f, data.∇f, data.∇²f, name = data.name) +function _add_op_data_to_jump(model::JuMP.Model, data::NLPOperator) + JuMP.add_nonlinear_operator(model, data.dim, data.f, data.∇f, data.∇²f, name = data.name) return end """ - add_registered_to_jump(opt_model::JuMP.Model, inf_model::InfiniteModel)::Nothing + add_operators_to_jump(opt_model::JuMP.Model, inf_model::InfiniteModel)::Nothing -Add the user registered nonlinear operators in `inf_model` to a `JuMP` model `opt_model`. +Add the additional nonlinear operators in `inf_model` to a `JuMP` model `opt_model`. This is intended as an internal method, but it is provided for developers that extend `InfiniteOpt` to use other optimizer models. """ -function add_registered_to_jump(opt_model::JuMP.Model, inf_model::InfiniteModel) - for data in user_registered_operators(inf_model) +function add_operators_to_jump(opt_model::JuMP.Model, inf_model::InfiniteModel) + for data in added_nonlinear_operators(inf_model) _add_op_data_to_jump(opt_model, data) end return diff --git a/test/TranscriptionOpt/transcribe.jl b/test/TranscriptionOpt/transcribe.jl index efb81eb15..cc11c2aa9 100644 --- a/test/TranscriptionOpt/transcribe.jl +++ b/test/TranscriptionOpt/transcribe.jl @@ -439,7 +439,7 @@ end @constraint(m, x + y == 83) @constraint(m, c6, [z, w] in MOI.Zeros(2)) g(a) = 42 - @register(m, gr, 1, g) + @operator(m, gr, 1, g) @constraint(m, c7, gr(z) == 2) @objective(m, Min, x0 + meas1) # test basic usage @@ -489,10 +489,10 @@ end @test length(d2t) == 2 @test upper_bound(d1t[1]) == 2 @test supports(d2) == [(0.,), (1.,)] - # test registration (TODO how to test this with new system) - # r = tm.nlp_model.operators - # @test length(r.registered_univariate_operators) == 1 - # @test r.registered_univariate_operators[1].f == g + # test operators + attr_dict = backend(tm).model_cache.modattr + @test length(attr_dict) == 1 + @test attr_dict[MOI.UserDefinedFunction(:gr, 1)] == (g,) # test objective xt = transcription_variable(tm, x) @test objective_function(tm) == 2xt[1] + xt[2] - 2wt - d2t[1] - d2t[2] diff --git a/test/datatypes.jl b/test/datatypes.jl index 803a59491..0165a7eb7 100644 --- a/test/datatypes.jl +++ b/test/datatypes.jl @@ -468,8 +468,8 @@ end @test ConstraintData(con, [1], "", MeasureIndex[], false) isa ConstraintData end -# Test the operator registration constructor -@testset "Registered Operators" begin +# Test the operator constructor +@testset "Nonlinear Operators" begin # Setup f(a) = a^3 g(a::Int) = 42 @@ -489,21 +489,21 @@ end return end # Test errors - @test_throws ErrorException RegisteredOperator(:a, 1, f, g) - @test_throws ErrorException RegisteredOperator(:a, 2, f, g) - @test_throws ErrorException RegisteredOperator(:a, 1, f, g, f) - @test_throws ErrorException RegisteredOperator(:a, 1, f, f, g) - @test_throws ErrorException RegisteredOperator(:a, 2, h, hg, hg) + @test_throws ErrorException NLPOperator(:a, 1, f, g) + @test_throws ErrorException NLPOperator(:a, 2, f, g) + @test_throws ErrorException NLPOperator(:a, 1, f, g, f) + @test_throws ErrorException NLPOperator(:a, 1, f, f, g) + @test_throws ErrorException NLPOperator(:a, 2, h, hg, hg) # Test regular builds - @test RegisteredOperator(:a, 1, f).name == :a - @test RegisteredOperator(:a, 1, f).dim == 1 - @test RegisteredOperator(:a, 1, f).f == f - @test RegisteredOperator(:a, 1, f, f) isa RegisteredOperator{typeof(f), typeof(f), Nothing} - @test RegisteredOperator(:a, 1, f, f).∇f == f - @test RegisteredOperator(:a, 1, f, f, f) isa RegisteredOperator{typeof(f), typeof(f), typeof(f)} - @test RegisteredOperator(:a, 1, f, f, f).∇²f == f - @test RegisteredOperator(:a, 2, h, hg) isa RegisteredOperator{typeof(h), typeof(hg), Nothing} - @test RegisteredOperator(:a, 2, h, hg).∇f == hg - @test RegisteredOperator(:a, 2, h, hg, ∇²h) isa RegisteredOperator{typeof(h), typeof(hg), typeof(∇²h)} - @test RegisteredOperator(:a, 2, h, hg, ∇²h).∇²f == ∇²h + @test NLPOperator(:a, 1, f).name == :a + @test NLPOperator(:a, 1, f).dim == 1 + @test NLPOperator(:a, 1, f).f == f + @test NLPOperator(:a, 1, f, f) isa NLPOperator{typeof(f), typeof(f), Nothing} + @test NLPOperator(:a, 1, f, f).∇f == f + @test NLPOperator(:a, 1, f, f, f) isa NLPOperator{typeof(f), typeof(f), typeof(f)} + @test NLPOperator(:a, 1, f, f, f).∇²f == f + @test NLPOperator(:a, 2, h, hg) isa NLPOperator{typeof(h), typeof(hg), Nothing} + @test NLPOperator(:a, 2, h, hg).∇f == hg + @test NLPOperator(:a, 2, h, hg, ∇²h) isa NLPOperator{typeof(h), typeof(hg), typeof(∇²h)} + @test NLPOperator(:a, 2, h, hg, ∇²h).∇²f == ∇²h end diff --git a/test/extensions/optimizer_model.jl b/test/extensions/optimizer_model.jl index 1eb9eed51..c2af9f0b1 100644 --- a/test/extensions/optimizer_model.jl +++ b/test/extensions/optimizer_model.jl @@ -39,7 +39,7 @@ end const OptKey = :ReformData # REPLACE WITH A DESIRED UNIQUE KEY # Make a constructor for new optimizer model type (extension of JuMP.Model) -function NewReformModel(args...; kwargs...)::JuMP.Model # ADD EXPLICT ARGS AS NEEDED +function NewReformModel(args...; kwargs...) # ADD EXPLICT ARGS AS NEEDED # initialize the JuMP Model model = JuMP.Model(args...; kwargs...) @@ -51,7 +51,7 @@ function NewReformModel(args...; kwargs...)::JuMP.Model # ADD EXPLICT ARGS AS NE end # Make function for extracting the data from the model (optional) -function reform_data(model::JuMP.Model)::NewReformData +function reform_data(model::JuMP.Model) # UPDATE THE NOMENCLATURE AS NEEDED haskey(model.ext, OptKey) || error("Model is not a NewReformModel.") return model.ext[OptKey] @@ -66,8 +66,8 @@ function InfiniteOpt.build_optimizer_model!( # clear the model for a build/rebuild reform_model = clear_optimizer_model_build!(model) - # load in registered NLP functions - add_registered_to_jump(reform_model, model) + # load in nonlinear operators + add_operators_to_jump(reform_model, model) # IT MAY BE USEFUL TO CALL `expand_all_measures!` TO HANDLE MEASURES FIRST # otherwise can extend `add_measure_variable` and `delete_semi_infinite_variable` to diff --git a/test/nlp.jl b/test/nlp.jl index 2e044d215..2fd3103f4 100644 --- a/test/nlp.jl +++ b/test/nlp.jl @@ -1,5 +1,5 @@ -# Test registration utilities -@testset "Registration Methods" begin +# Test operator utilities +@testset "Operator Methods" begin # setup model data m = InfiniteModel() @variable(m, y) @@ -21,26 +21,26 @@ H[2, 2] = 200.0 return end - # test JuMP.register_nonlinear_operator - @testset "JuMP.register_nonlinear_operator" begin + # test JuMP.add_nonlinear_operator + @testset "JuMP.add_nonlinear_operator" begin # test errors - @test_throws ErrorException register_nonlinear_operator(m, 1, f, name = :max) + @test_throws ErrorException add_nonlinear_operator(m, 1, f, name = :max) m.op_lookup[:f] = (f, 1) - @test_throws ErrorException register_nonlinear_operator(m, 1, f) + @test_throws ErrorException add_nonlinear_operator(m, 1, f) empty!(m.op_lookup) - @test_throws ErrorException register_nonlinear_operator(m, 2, f) + @test_throws ErrorException add_nonlinear_operator(m, 2, f) # test normal - @test register_nonlinear_operator(m, 1, f).head == :f + @test add_nonlinear_operator(m, 1, f).head == :f @test m.op_lookup[:f] == (f, 1) - @test last(m.registrations).f == f - @test last(m.registrations).dim == 1 - @test last(m.registrations).name == :f - @test register_nonlinear_operator(m, 2, h, hg).head == :h + @test last(m.operators).f == f + @test last(m.operators).dim == 1 + @test last(m.operators).name == :f + @test add_nonlinear_operator(m, 2, h, hg).head == :h @test m.op_lookup[:h] == (h, 2) - @test last(m.registrations).f == h - @test last(m.registrations).dim == 2 - @test last(m.registrations).name == :h - @test last(m.registrations).∇f == hg + @test last(m.operators).f == h + @test last(m.operators).dim == 2 + @test last(m.operators).name == :h + @test last(m.operators).∇f == hg end # test name_to_operator @testset "name_to_operator" begin @@ -48,43 +48,47 @@ @test name_to_operator(m, :h) == h @test name_to_operator(m, :bad) isa Nothing end - # test all_registered_operators - @testset "all_registered_operators" begin - @test all_registered_operators(m) isa Vector{Symbol} + # test all_nonlinear_operators + @testset "all_nonlinear_operators" begin + @test all_nonlinear_operators(m) isa Vector{Symbol} end - # test user_registered_operators - @testset "user_registered_operators" begin - @test user_registered_operators(m) isa Vector{RegisteredOperator} + # test added_nonlinear_operators + @testset "added_nonlinear_operators" begin + @test added_nonlinear_operators(m) isa Vector{NLPOperator} end - empty!(m.registrations) + empty!(m.operators) empty!(m.op_lookup) - # test @register - @testset "@register" begin + # test @operator + @testset "@operator" begin # test errors - @test @register(m, f1, 1, f) isa NonlinearOperator - @test @register(m, f2, 1, f, f) isa NonlinearOperator - @test @register(m, f3, 1, f, f, f) isa NonlinearOperator - @test @register(m, h1, 2, h) isa NonlinearOperator - @test @register(m, h2, 2, h, hg) isa NonlinearOperator - @test @register(m, h3, 2, h, hg, ∇²h) isa NonlinearOperator - # test functional registration - function registration_test() + @test @operator(m, f1, 1, f) isa NonlinearOperator + @test @operator(m, f2, 1, f, f) isa NonlinearOperator + @test @operator(m, f3, 1, f, f, f) isa NonlinearOperator + @test @operator(m, h1, 2, h) isa NonlinearOperator + @test @operator(m, h2, 2, h, hg) isa NonlinearOperator + @test @operator(m, h3, 2, h, hg, ∇²h) isa NonlinearOperator + # test operator scoping + function scope_test() mt = InfiniteModel() @variable(mt, x) q(a) = 1 - @test @register(mt, my_q, 1, q) isa NonlinearOperator + @test @operator(mt, my_q, 1, q) isa NonlinearOperator q(x::GeneralVariableRef) = GenericNonlinearExpr{GeneralVariableRef}(:my_q, x) @test @expression(mt, q(x)) isa GenericNonlinearExpr return end - @test registration_test() isa Nothing - @test registration_test() isa Nothing + @test scope_test() isa Nothing + @test scope_test() isa Nothing + end + # test @register + @testset "@register" begin + @test_macro_throws ErrorException @register(m, f(a)) end - # test add_registered_to_jump - @testset "add_registered_to_jump" begin + # test add_operators_to_jump + @testset "add_operators_to_jump" begin # test normal m1 = Model() - @test add_registered_to_jump(m1, m) isa Nothing + @test add_operators_to_jump(m1, m) isa Nothing attr_dict = backend(m1).model_cache.modattr @test length(attr_dict) == 6 @test attr_dict[MOI.UserDefinedFunction(:f1, 1)] == (f,) diff --git a/test/runtests.jl b/test/runtests.jl index c83572dc3..54198a0b5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,5 @@ import Pkg -Pkg.pkg"add JuMP#od/nlp-expr" +Pkg.pkg"add JuMP#master" using InfiniteOpt: _domain_or_error using Test: Error From 7e8c4d865db84f5eab7d5ca7ed0457a4078aced0 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Thu, 31 Aug 2023 13:15:50 -0400 Subject: [PATCH 30/40] Fix doctests --- docs/src/develop/extensions.md | 5 ++--- docs/src/guide/derivative.md | 4 ++-- docs/src/guide/expression.md | 26 +++----------------------- 3 files changed, 7 insertions(+), 28 deletions(-) diff --git a/docs/src/develop/extensions.md b/docs/src/develop/extensions.md index 734d6dc61..a1b8ecae6 100644 --- a/docs/src/develop/extensions.md +++ b/docs/src/develop/extensions.md @@ -934,9 +934,8 @@ function InfiniteOpt.build_optimizer_model!( for cref in all_constraints(model, Union{GenericAffExpr, GenericQuadExpr, GenericNonlinearExpr}) constr = constraint_object(cref) new_func = _make_expression(determ_model, constr.func) - new_constr = build_constraint(error, new_func, constr.set) - new_cref = add_constraint(determ_model, new_constr, name(cref)) - end + new_constr = build_constraint(error, new_func, constr.set) + new_cref = add_constraint(determ_model, new_constr, name(cref)) deterministic_data(determ_model).infconstr_to_detconstr[cref] = new_cref end diff --git a/docs/src/guide/derivative.md b/docs/src/guide/derivative.md index 285b114f5..14a4752f6 100644 --- a/docs/src/guide/derivative.md +++ b/docs/src/guide/derivative.md @@ -53,7 +53,7 @@ julia> d3 = @∂(q, t^2) ∂/∂t[∂/∂t[q(t)]] julia> d_expr = @deriv(y * q - 2t, t) -∂/∂t[y(t, ξ)]*q(t) + ∂/∂t[q(t)]*y(t, ξ) - 2 +∂/∂t[y(t, ξ)]*q(t) + y(t, ξ)*∂/∂t[q(t)] - 2 ``` Thus, we can define derivatives in a variety of forms according to the problem at hand. The last example even shows how the product rule is correctly applied. @@ -216,7 +216,7 @@ defined up above with its alias name `dydt2`. This macro can also tackle complex expressions using the appropriate calculus such as: ```jldoctest deriv_basic julia> @deriv(∫(y, ξ) * q, t) -∂/∂t[∫{ξ ∈ [-1, 1]}[y(t, ξ)]]*q(t) + ∂/∂t[q(t)]*∫{ξ ∈ [-1, 1]}[y(t, ξ)] +∂/∂t[∫{ξ ∈ [-1, 1]}[y(t, ξ)]]*q(t) + ∫{ξ ∈ [-1, 1]}[y(t, ξ)]*∂/∂t[q(t)] ``` Thus, demonstrating the convenience of using `@deriv`. diff --git a/docs/src/guide/expression.md b/docs/src/guide/expression.md index a1a4f7364..180fb84dc 100644 --- a/docs/src/guide/expression.md +++ b/docs/src/guide/expression.md @@ -207,7 +207,7 @@ julia> expr = 2y^2 - z * y + 42t - 3 2 y(t)² - z*y(t) + 42 t - 3 julia> expr = @expression(model, 2y^2 - z * y + 42t - 3) -2 y(t)² - y(t)*z + 42 t - 3 +2 y(t)² - z*y(t) + 42 t - 3 julia> typeof(expr) GenericQuadExpr{Float64, GeneralVariableRef} @@ -234,30 +234,10 @@ GenericAffExpr{Float64, GeneralVariableRef} julia> expr.terms OrderedCollections.OrderedDict{UnorderedPair{GeneralVariableRef}, Float64} with 2 entries: UnorderedPair{GeneralVariableRef}(y(t), y(t)) => 2.0 - UnorderedPair{GeneralVariableRef}(y(t), z) => -1.0 + UnorderedPair{GeneralVariableRef}(z, y(t)) => -1.0 ``` Notice again that the ordered dictionary preserves the order. -!!! tip - Polynomial expressions can be represented by introducing dummy variables - and nested quadratic/affine expressions. For instance, ``z^3 + 2`` can be - expressed by introducing a dummy variable ``x = z^2``: - ```jldoctest affine - julia> @variable(model, x) - x - - julia> @constraint(model, x == z^2) - -z² + x = 0 - - julia> expr = @expression(model, z * x + 2) - z*x + 2 - ``` - Alternatively, can we can just use our nonlinear modeling interface: - ```jldoctest affine - julia> expr = @expression(model, z^3 + 2) - z^3 + 2 - ``` - More information can be found in the documentation for quadratic expressions in [`JuMP`](https://jump.dev/JuMP.jl/v1/api/JuMP/#JuMP.GenericQuadExpr). @@ -408,7 +388,7 @@ type `Real`): ```jldoctest nlp julia> h(a, b) = a * b^2; # an overly simple example operator -julia> @operator(model, op_h2, 2, h, name = :h); +julia> @operator(model, op_h, 2, h); julia> op_h(y, 42) op_h(y(t), 42) From 87c43246bfc2e91b5b7f0c225ab226f0217c19f0 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Thu, 31 Aug 2023 13:50:44 -0400 Subject: [PATCH 31/40] Another doctest fix --- docs/src/develop/extensions.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/src/develop/extensions.md b/docs/src/develop/extensions.md index a1b8ecae6..02fa3358d 100644 --- a/docs/src/develop/extensions.md +++ b/docs/src/develop/extensions.md @@ -956,13 +956,11 @@ print(optimizer_model(model)) # output Min z + y[1] + y[2] Subject to - 2 y[1] - z ≤ 42.0 + sin(z) - -1.0 ≥ 0 + 2 y[1] - z ≤ 42 y[2]² = 1.5 - y[1] ≥ 0.0 - y[2] ≥ 0.0 - subexpression[1] - 0.0 ≥ 0 -With NL expressions - subexpression[1]: sin(z) - -1.0 + y[1] ≥ 0 + y[2] ≥ 0 ``` Note that better variable naming could be used with the reformulated infinite variables. Moreover, in general extensions of [`build_optimizer_model!`](@ref) From ac40507515b47d1522f3c604cfceb181333ce75d Mon Sep 17 00:00:00 2001 From: pulsipher Date: Thu, 31 Aug 2023 14:11:44 -0400 Subject: [PATCH 32/40] Fix header link --- docs/src/guide/expression.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/guide/expression.md b/docs/src/guide/expression.md index 180fb84dc..8d4199ca1 100644 --- a/docs/src/guide/expression.md +++ b/docs/src/guide/expression.md @@ -319,7 +319,7 @@ can contain. The following CANNOT be used: !!! tip If a particular function is not amendable for tracing, try adding it - as a new nonlinear operator instead. See [Add Nonlinear Operators](@ref) + as a new nonlinear operator instead. See [Adding Nonlinear Operators](@ref) for details. We can readily work around the if-statement limitation using `op_ifelse` which @@ -378,7 +378,7 @@ julia> @constraint(model, W * Q * v .== 0) ((+(0.0) + ((W[2,1]*Q[1,1] + W[2,2]*Q[2,1]) * v[1])) + ((W[2,1]*Q[1,2] + W[2,2]*Q[2,2]) * v[2])) - 0.0 = 0 ``` -### Add Nonlinear Operators +### Adding Nonlinear Operators In a similar spirit to `JuMP` and `Symbolics`, we can add nonlinear operators such that they can be directly incorporated into nonlinear expressions as atoms (they will not be traced). This is done via the From 271100191fe4889514ecb3aa8f43cf309e428b7d Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sat, 9 Sep 2023 08:25:10 +1200 Subject: [PATCH 33/40] Update for NonlinearOperator change (#321) --- docs/src/guide/expression.md | 228 +++++++++++++++++------------------ src/nlp.jl | 26 ++-- 2 files changed, 127 insertions(+), 127 deletions(-) diff --git a/docs/src/guide/expression.md b/docs/src/guide/expression.md index 8d4199ca1..06a0efc7e 100644 --- a/docs/src/guide/expression.md +++ b/docs/src/guide/expression.md @@ -4,33 +4,33 @@ DocTestFilters = [r"≤|<=", r" == | = ", r" ∈ | in ", r"MathOptInterface|MOI" ``` # [Expressions](@id expr_docs) -A guide for the defining and understanding the variable expressions -used in `InfiniteOpt`. See the [technical manual](@ref expr_manual) for more +A guide for the defining and understanding the variable expressions +used in `InfiniteOpt`. See the [technical manual](@ref expr_manual) for more details. -!!! note - Nonlinear modeling is now handled in `InfiniteOpt` via `JuMP`'s new - nonlinear interface. See [Nonlinear Expressions](@ref nlp_guide) for - more information. +!!! note + Nonlinear modeling is now handled in `InfiniteOpt` via `JuMP`'s new + nonlinear interface. See [Nonlinear Expressions](@ref nlp_guide) for + more information. ## Overview -Expressions in `InfiniteOpt` (also called functions) refer to mathematical -statements involving variables and numbers. Thus, these comprise the -mathematical expressions used that are used in measures, objectives, and -constraints. Programmatically, `InfiniteOpt` simply extends `JuMP` expression -types and methods principally pertaining to affine and quadratic mathematical -expressions. A natively supported abstraction for general nonlinear expressions +Expressions in `InfiniteOpt` (also called functions) refer to mathematical +statements involving variables and numbers. Thus, these comprise the +mathematical expressions used that are used in measures, objectives, and +constraints. Programmatically, `InfiniteOpt` simply extends `JuMP` expression +types and methods principally pertaining to affine and quadratic mathematical +expressions. A natively supported abstraction for general nonlinear expressions is planned for development since that of `JuMP` is not readily extendable. ## [Parameter Functions](@id par_func_docs) -As described further below, InfiniteOpt.jl only supports affine and quadratic -expressions in its current rendition. However, there several use cases where we -might want to provide a more complex known function of infinite parameter(s) (e.g., -nonlinear setpoint tracking). Thus, we provide parameter function objects -that given a particular realization of infinite parameters will output a scalar -value. Note that this can be interpreted as an infinite variable that is -constrained to a particular known function. This is accomplished via -[`@parameter_function`](@ref) or [`parameter_function`](@ref) and is exemplified +As described further below, InfiniteOpt.jl only supports affine and quadratic +expressions in its current rendition. However, there several use cases where we +might want to provide a more complex known function of infinite parameter(s) (e.g., +nonlinear setpoint tracking). Thus, we provide parameter function objects +that given a particular realization of infinite parameters will output a scalar +value. Note that this can be interpreted as an infinite variable that is +constrained to a particular known function. This is accomplished via +[`@parameter_function`](@ref) or [`parameter_function`](@ref) and is exemplified by defining a parameter function `f(t)` that uses `sin(t)`: ```jldoctest param_func julia> using InfiniteOpt; @@ -42,9 +42,9 @@ julia> @infinite_parameter(model, t in [0, 10]); julia> @parameter_function(model, f == sin(t)) f(t) ``` -Here we created a parameter function object, added it to `model`, and -then created a Julia variable `f` that serves as a `GeneralVariableRef` that points -to it. From here we can treat `f` as a normal infinite variable and use it with +Here we created a parameter function object, added it to `model`, and +then created a Julia variable `f` that serves as a `GeneralVariableRef` that points +to it. From here we can treat `f` as a normal infinite variable and use it with measures, derivatives, and constraints. For example, we can do the following: ```jldoctest param_func julia> @variable(model, y, Infinite(t)); @@ -58,7 +58,7 @@ julia> meas = integral(y - f, t) julia> @constraint(model, y - f <= 0) y(t) - f(t) ≤ 0, ∀ t ∈ [0, 10] ``` -We can also define parameter functions that depend on multiple infinite +We can also define parameter functions that depend on multiple infinite parameters even use an anonymous function if preferred: ```jldoctest param_func julia> @infinite_parameter(model, x[1:2] in [-1, 1]); @@ -66,9 +66,9 @@ julia> @infinite_parameter(model, x[1:2] in [-1, 1]); julia> @parameter_function(model, myname == (t, x) -> t + sum(x)) myname(t, x) ``` -In many applications, we may also desire to define an array of parameter functions -that each use a different realization of some parent function by varying some -additional positional/keyword arguments. We readily support this behavior since +In many applications, we may also desire to define an array of parameter functions +that each use a different realization of some parent function by varying some +additional positional/keyword arguments. We readily support this behavior since parameter functions can be defined with additional known arguments: ```jldoctest param_func julia> @parameter_function(model, pfunc_alt[i = 1:3] == t -> mysin(t, as[i], b = 0)) @@ -77,24 +77,24 @@ julia> @parameter_function(model, pfunc_alt[i = 1:3] == t -> mysin(t, as[i], b = pfunc_alt[2](t) pfunc_alt[3](t) ``` -The main recommended use case for [`parameter_function`](@ref) is that it is -amenable to define complex anonymous functions via a do-block which is useful +The main recommended use case for [`parameter_function`](@ref) is that it is +amenable to define complex anonymous functions via a do-block which is useful for applications like defining a time-varied setpoint: ```jldoctest param_func julia> setpoint = parameter_function(t, name = "setpoint") do t_supp if t_supp <= 5 return 2.0 - else + else return 10.2 end end setpoint(t) ``` -Please consult the following links for more information about defining parameter +Please consult the following links for more information about defining parameter functions: [`@parameter_function`](@ref) and [`parameter_function`](@ref). -Beyond this, there are a number of query and modification methods that can be -employed for parameter functions and these are detailed in the +Beyond this, there are a number of query and modification methods that can be +employed for parameter functions and these are detailed in the [technical manual](@ref par_func_manual) Section below. ## Variable Hierarchy @@ -129,14 +129,14 @@ An affine expression pertains to a mathematical function of the form: ```math f_a(x) = a_1x_1 + ... + a_nx_n + b ``` -where ``x \in \mathbb{R}^n`` denote variables, ``a \in \mathbb{R}^n`` denote -coefficients, and ``b \in \mathbb{R}`` denotes a constant value. Such -expressions, are prevalent in any problem than involves linear constraints +where ``x \in \mathbb{R}^n`` denote variables, ``a \in \mathbb{R}^n`` denote +coefficients, and ``b \in \mathbb{R}`` denotes a constant value. Such +expressions, are prevalent in any problem than involves linear constraints and/or objectives. -In `InfiniteOpt`, affine expressions can be defined directly -using `Julia`'s arithmetic operators (i.e., `+`, `-`, `*`, etc.) or using -`@expression`. For example, let's define the expression +In `InfiniteOpt`, affine expressions can be defined directly +using `Julia`'s arithmetic operators (i.e., `+`, `-`, `*`, etc.) or using +`@expression`. For example, let's define the expression ``2y(t) + z - 3t`` noting that the following methods are equivalent: ```jldoctest affine; setup = :(using InfiniteOpt; model = InfiniteModel()) julia> @infinite_parameter(model, t in [0, 10]) @@ -160,15 +160,15 @@ julia> expr = @expression(model, 2y + z - 3t) julia> typeof(expr) GenericAffExpr{Float64, GeneralVariableRef} ``` -Notice that coefficients to variables can simply be put alongside variables -without having to use the `*` operator. Also, note that all of these expressions -are stored in a container referred to as a `GenericAffExpr` which is a `JuMP` +Notice that coefficients to variables can simply be put alongside variables +without having to use the `*` operator. Also, note that all of these expressions +are stored in a container referred to as a `GenericAffExpr` which is a `JuMP` object for storing affine expressions. !!! note - Where possible, it is preferable to use - [`@expression`](https://jump.dev/JuMP.jl/v1/api/JuMP/#JuMP.@expression) - for defining expressions as it is much more efficient than explicitly using + Where possible, it is preferable to use + [`@expression`](https://jump.dev/JuMP.jl/v1/api/JuMP/#JuMP.@expression) + for defining expressions as it is much more efficient than explicitly using the standard operators. `GenericAffExpr` objects contain 2 fields which are: @@ -185,10 +185,10 @@ OrderedCollections.OrderedDict{GeneralVariableRef, Float64} with 3 entries: julia> expr.constant 0.0 ``` -Notice that the ordered dictionary preserves the order in which the variables +Notice that the ordered dictionary preserves the order in which the variables appear in the expression. -More information can be found in the documentation for affine expressions in +More information can be found in the documentation for affine expressions in [`JuMP`](https://jump.dev/JuMP.jl/v1/api/JuMP/#JuMP.GenericAffExpr). ## Quadratic Expressions @@ -212,8 +212,8 @@ julia> expr = @expression(model, 2y^2 - z * y + 42t - 3) julia> typeof(expr) GenericQuadExpr{Float64, GeneralVariableRef} ``` -Again, notice that coefficients need not employ `*`. Also, the object used to -store the expression is a `GenericQuadExpr` which is a `JuMP` object used for +Again, notice that coefficients need not employ `*`. Also, the object used to +store the expression is a `GenericQuadExpr` which is a `JuMP` object used for storing quadratic expressions. `GenericQuadExpr` object contains 2 data fields which are: @@ -222,7 +222,7 @@ storing quadratic expressions. Here the `UnorderedPair` type is unique to `JuMP` and contains the fields: - `a::AbstractVariableRef` One variable in a quadratic pair - `b::AbstractVariableRef` The other variable in a quadratic pair. -Thus, this form can be used to store arbitrary quadratic expressions. For +Thus, this form can be used to store arbitrary quadratic expressions. For example, let's look at what these fields look like in the above example: ```jldoctest affine julia> expr.aff @@ -238,21 +238,21 @@ OrderedCollections.OrderedDict{UnorderedPair{GeneralVariableRef}, Float64} with ``` Notice again that the ordered dictionary preserves the order. -More information can be found in the documentation for quadratic expressions in +More information can be found in the documentation for quadratic expressions in [`JuMP`](https://jump.dev/JuMP.jl/v1/api/JuMP/#JuMP.GenericQuadExpr). ## [Nonlinear Expressions](@id nlp_guide) -In this section, we walk you through the ins and out of working with general +In this section, we walk you through the ins and out of working with general nonlinear (i.e., not affine or quadratic) expressions in `InfiniteOpt`. !!! info - Our previous `InfiniteOpt` specific nonlinear API as been removed in - favor of `JuMP`'s new and improved nonlinear interface. Thus, `InfiniteOpt` + Our previous `InfiniteOpt` specific nonlinear API as been removed in + favor of `JuMP`'s new and improved nonlinear interface. Thus, `InfiniteOpt` now strictly uses the same expression structures as `JuMP`. -### Basic Usage -We can define nonlinear expressions in similar manner to how affine/quadratic -expressions are made in `JuMP`. For instance, we can make an expression using +### Basic Usage +We can define nonlinear expressions in similar manner to how affine/quadratic +expressions are made in `JuMP`. For instance, we can make an expression using normal Julia code outside a macro: ```jldoctest nlp; setup = :(using InfiniteOpt; model = InfiniteModel()) julia> @infinite_parameter(model, t ∈ [0, 1]); @variable(model, y, Infinite(t)); @@ -263,10 +263,10 @@ julia> expr = exp(y^2.3) * y - 42 julia> typeof(expr) GenericNonlinearExpr{GeneralVariableRef} ``` -Thus, the nonlinear expression `expr` of type -[`GenericNonlinearExpr`](https://jump.dev/JuMP.jl/v1/api/JuMP/#GenericNonlinearExpr) -is created and can be readily incorporated into other expressions, the objective, -and/or constraints. For macro-based definition, we simply use the `@expression`, +Thus, the nonlinear expression `expr` of type +[`GenericNonlinearExpr`](https://jump.dev/JuMP.jl/v1/api/JuMP/#GenericNonlinearExpr) +is created and can be readily incorporated into other expressions, the objective, +and/or constraints. For macro-based definition, we simply use the `@expression`, `@objective`, and `@constraint` macros as normal: ```jldoctest nlp julia> @expression(model, expr, exp(y^2.3) * y - 42) @@ -284,15 +284,15 @@ constr : (((y(t) ^ y(t)) * sin(y(t))) + (y(t) ^ 3) + (y(t) ^ 4)) - 3.0 = 0, ∀ The legacy `@NLexpression`, `@NLobjective`, and `@NLconstraint` macros in `JuMP` are not supported by `InfiniteOpt`. -Natively, we support all the same nonlinear operators that `JuMP` -does. See [JuMP's documentation](https://jump.dev/JuMP.jl/v1/manual/nonlinear/#Supported-operators) +Natively, we support all the same nonlinear operators that `JuMP` +does. See [JuMP's documentation](https://jump.dev/JuMP.jl/v1/manual/nonlinear/#Supported-operators) for more information. -We can interrogate which nonlinear operators our model currently -supports by invoking [`all_nonlinear_operators`](@ref). Moreover, we can add -additional operators (see [Adding Nonlinear Operators](@ref) for more details). +We can interrogate which nonlinear operators our model currently +supports by invoking [`all_nonlinear_operators`](@ref). Moreover, we can add +additional operators (see [Adding Nonlinear Operators](@ref) for more details). -Finally, we highlight that nonlinear expressions in `InfiniteOpt` support the +Finally, we highlight that nonlinear expressions in `InfiniteOpt` support the same linear algebra operations as affine/quadratic expressions: ```jldoctest nlp julia> @variable(model, v[1:2]); @variable(model, Q[1:2, 1:2]); @@ -302,8 +302,8 @@ julia> @expression(model, v' * Q * v) ``` ### Function Tracing -In similar manner to `Symbolics.jl`, we support function tracing. This means -that we can create nonlinear modeling expression using Julia functions that +In similar manner to `Symbolics.jl`, we support function tracing. This means +that we can create nonlinear modeling expression using Julia functions that satisfy certain criteria. For instance: ```jldoctest nlp julia> myfunc(x) = sin(x^3) / tan(2^x); @@ -311,19 +311,19 @@ julia> myfunc(x) = sin(x^3) / tan(2^x); julia> expr = myfunc(y) sin(y(t) ^ 3) / tan(2.0 ^ y(t)) ``` -However, there are certain limitations as to what internal code these functions +However, there are certain limitations as to what internal code these functions can contain. The following CANNOT be used: - loops (unless it only uses very simple operations) - if-statements (see workaround below) - unrecognized operators (if they cannot be traced). !!! tip - If a particular function is not amendable for tracing, try adding it - as a new nonlinear operator instead. See [Adding Nonlinear Operators](@ref) + If a particular function is not amendable for tracing, try adding it + as a new nonlinear operator instead. See [Adding Nonlinear Operators](@ref) for details. -We can readily work around the if-statement limitation using `op_ifelse` which -is a nonlinear operator version of `Base.ifelse` and follows the same syntax. +We can readily work around the if-statement limitation using `op_ifelse` which +is a nonlinear operator version of `Base.ifelse` and follows the same syntax. For example, the function: ```julia function mylogicfunc(x) @@ -344,20 +344,20 @@ mylogicfunc (generic function with 1 method) julia> mylogicfunc(y) ifelse(y(t) >= 0, y(t) ^ 3, 0) ``` -which is amendable for function tracing. Note that the basic logic operators -(e.g., `<=`) have special nonlinear operator analogues when used outside of a -macro. See [JuMP's documentation](https://jump.dev/JuMP.jl/v1/manual/nonlinear/#Limitations) +which is amendable for function tracing. Note that the basic logic operators +(e.g., `<=`) have special nonlinear operator analogues when used outside of a +macro. See [JuMP's documentation](https://jump.dev/JuMP.jl/v1/manual/nonlinear/#Limitations) for more details. ### Linear Algebra -As described above in the Basic Usage Section, we support basic linear algebra -operations with nonlinear expressions! This relies on our basic extensions of -[`MutableArithmetics`](https://github.com/jump-dev/MutableArithmetics.jl), but -admittedly this implementation is not perfect in terms of efficiency. - -!!! tip - Using linear algebra operations with nonlinear expression provides user - convenience, but is less efficient than using `sum`s. Thus, `sum` should be +As described above in the Basic Usage Section, we support basic linear algebra +operations with nonlinear expressions! This relies on our basic extensions of +[`MutableArithmetics`](https://github.com/jump-dev/MutableArithmetics.jl), but +admittedly this implementation is not perfect in terms of efficiency. + +!!! tip + Using linear algebra operations with nonlinear expression provides user + convenience, but is less efficient than using `sum`s. Thus, `sum` should be used instead when efficiency is critical. ```jldoctest nlp julia> v' * Q * v # convenient linear algebra syntax @@ -367,7 +367,7 @@ admittedly this implementation is not perfect in terms of efficiency. ((((v[1]*Q[1,1]) * v[1]) + ((v[2]*Q[2,1]) * v[1])) + ((v[1]*Q[1,2]) * v[2])) + ((v[2]*Q[2,2]) * v[2]) ``` -We can also set vectorized constraints using the `.==`, `.<=`, and `.>=` +We can also set vectorized constraints using the `.==`, `.<=`, and `.>=` operators: ```jldoctest nlp julia> @variable(model, W[1:2, 1:2]); @@ -380,10 +380,10 @@ julia> @constraint(model, W * Q * v .== 0) ### Adding Nonlinear Operators In a similar spirit to `JuMP` and `Symbolics`, we can add nonlinear operators -such that they can be directly incorporated into nonlinear expressions as atoms -(they will not be traced). This is done via the -[`@operator`](https://jump.dev/JuMP.jl/v1/api/JuMP/#@operator) macro. We can -register any operator that takes scalar arguments (which can accept inputs of +such that they can be directly incorporated into nonlinear expressions as atoms +(they will not be traced). This is done via the +[`@operator`](https://jump.dev/JuMP.jl/v1/api/JuMP/#@operator) macro. We can +register any operator that takes scalar arguments (which can accept inputs of type `Real`): ```jldoctest nlp julia> h(a, b) = a * b^2; # an overly simple example operator @@ -395,11 +395,11 @@ op_h(y(t), 42) ``` !!! tip - Where possible it is preferred to use function tracing instead. This improves - performance and can prevent unintentional errors. + Where possible it is preferred to use function tracing instead. This improves + performance and can prevent unintentional errors. See [Function Tracing](@ref) for more details. -To highlight the difference between function tracing and operator definition +To highlight the difference between function tracing and operator definition consider the following example: ```jldoctest nlp julia> f(a) = a^3; @@ -408,18 +408,18 @@ julia> f(y) # user-function gets traced y(t) ^ 3 julia> @operator(model, op_f, 1, f) # create nonlinear operator -NonlinearOperator(:op_f, f) +NonlinearOperator(f, :op_f) julia> op_f(y) # function is no longer traced op_f(y(t)) ``` -Thus, nonlinear operators are incorporated directly. This means that their -gradients and hessians will need to determined as well (typically occurs -behind the scenes via auto-differentiation with the selected optimizer model -backend). However, again please note that in this case tracing is preferred -since `f` can be traced. +Thus, nonlinear operators are incorporated directly. This means that their +gradients and hessians will need to determined as well (typically occurs +behind the scenes via auto-differentiation with the selected optimizer model +backend). However, again please note that in this case tracing is preferred +since `f` can be traced. -Let's consider a more realistic example where the function is not amenable to +Let's consider a more realistic example where the function is not amenable to tracing: ```jldoctest nlp julia> function g(a) @@ -438,23 +438,23 @@ julia> @operator(model, op_g, 1, g); julia> op_g(y) op_g(y(t)) ``` -Notice this example is a little contrived still, highlighting that in most cases -we can avoid adding operators. However, one exception to this trend, are functions -from other packages that we might want to use. For example, perhaps we would -like to use the `eta` function from `SpecialFunctions.jl` which is not natively +Notice this example is a little contrived still, highlighting that in most cases +we can avoid adding operators. However, one exception to this trend, are functions +from other packages that we might want to use. For example, perhaps we would +like to use the `eta` function from `SpecialFunctions.jl` which is not natively supported: ```jldoctest nlp julia> using SpecialFunctions julia> @operator(model, op_eta, 1, eta) -NonlinearOperator(:op_eta, eta) +NonlinearOperator(eta, :op_eta) julia> op_eta(y) op_eta(y(t)) ``` -Now in some cases we might wish to specify the gradient and hessian of a -univariate operator to avoid the need for auto-differentiation. We +Now in some cases we might wish to specify the gradient and hessian of a +univariate operator to avoid the need for auto-differentiation. We can do this, simply by adding them as additional arguments in `@operator`: ```jldoctest nlp julia> my_squared(a) = a^2; gradient(a) = 2 * a; hessian(a) = 2; @@ -464,10 +464,10 @@ julia> @operator(model, op_square, 1, my_squared, gradient, hessian); julia> op_square(y) op_square(y(t)) ``` -Note the specification of the hessian is optional (it can separately be +Note the specification of the hessian is optional (it can separately be computed via auto-differentiation if need be). -For multivariate functions, we can specify the gradient following the same +For multivariate functions, we can specify the gradient following the same gradient function structure that `JuMP` uses: ```jldoctest nlp julia> w(a, b) = a * b^2; @@ -478,19 +478,19 @@ julia> function wg(v, a, b) return end; -julia> @operator(model, op_w, 2, w, wg) -NonlinearOperator(:op_w, w) +julia> @operator(model, op_w, 2, w, wg) +NonlinearOperator(w, :op_w) julia> op_w(42, y) op_w(42, y(t)) ``` -Note that the first argument of the gradient needs to accept an +Note that the first argument of the gradient needs to accept an `AbstractVector{Real}` that is then filled in place. !!! note - We do not currently support vector inputs or vector valued functions + We do not currently support vector inputs or vector valued functions directly, since typically `JuMP` optimizer model backends don't support them. ### More Details -For more details, please consult +For more details, please consult [JuMP's Documentation](https://jump.dev/JuMP.jl/v1/manual/nonlinear/). diff --git a/src/nlp.jl b/src/nlp.jl index df91db450..0055ead83 100644 --- a/src/nlp.jl +++ b/src/nlp.jl @@ -2,7 +2,7 @@ # USER OPERATORS ################################################################################ # Keep track of the predefined functions in MOI -const _NativeNLPOperators = append!(copy(MOI.Nonlinear.DEFAULT_UNIVARIATE_OPERATORS), +const _NativeNLPOperators = append!(copy(MOI.Nonlinear.DEFAULT_UNIVARIATE_OPERATORS), MOI.Nonlinear.DEFAULT_MULTIVARIATE_OPERATORS) append!(_NativeNLPOperators, (:&&, :||, :<=, :(==), :>=, :<, :>)) @@ -16,10 +16,10 @@ append!(_NativeNLPOperators, (:&&, :||, :<=, :(==), :>=, :<, :>)) [name::Symbol = Symbol(f)] ) -Extend `add_nonlinear_operator` for `InfiniteModel`s. +Extend `add_nonlinear_operator` for `InfiniteModel`s. Add a new nonlinear operator with `dim` input arguments to `model` and associate -it with the name `name`. Alternatively, [`@operator`](https://jump.dev/JuMP.jl/v1/api/JuMP/#@operator) +it with the name `name`. Alternatively, [`@operator`](https://jump.dev/JuMP.jl/v1/api/JuMP/#@operator) can be used for a more convenient syntax. The function `f` evaluates the operator. The optional function `∇f` evaluates @@ -32,7 +32,7 @@ julia> @variable(model, y); julia> g(x) = x^2; julia> new_op = add_nonlinear_operator(model, 1, g) -NonlinearOperator(:g, g) +NonlinearOperator(g, :g) julia> @expression(model, new_op(y)) g(y) @@ -54,13 +54,13 @@ function JuMP.add_nonlinear_operator( push!(model.operators, NLPOperator(name, dim, f, funcs...)) model.op_lookup[name] = (f, dim) # TODO should we set the optimizer model to be out of date? - return JuMP.NonlinearOperator(name, f) + return JuMP.NonlinearOperator(f, name) end """ - name_to_operator(model::InfiniteModel, name::Symbol)::Union{Function, Nothing} + name_to_operator(model::InfiniteModel, name::Symbol)::Union{Function, Nothing} -Return the nonlinear operator that corresponds to `name`. +Return the nonlinear operator that corresponds to `name`. Returns `nothing` if no such operator exists. !!! warning @@ -76,14 +76,14 @@ end Retrieve all the operators that are currently added to `model`. """ -function all_nonlinear_operators(model::InfiniteModel) +function all_nonlinear_operators(model::InfiniteModel) return append!(copy(_NativeNLPOperators), map(v -> Symbol(first(v)), values(model.op_lookup))) end """ user_defined_operators(model::InfiniteModel)::Vector{NLPOperator} -Return all the operators (and their associated information) that the user has +Return all the operators (and their associated information) that the user has added to `model`. Each is stored as a [`NLPOperator`](@ref). """ function added_nonlinear_operators(model::InfiniteModel) @@ -93,7 +93,7 @@ end ## Define helper function to add nonlinear operators to JuMP # No gradient or hessian function _add_op_data_to_jump( - model::JuMP.Model, + model::JuMP.Model, data::NLPOperator{F, Nothing, Nothing} ) where {F <: Function} JuMP.add_nonlinear_operator(model, data.dim, data.f, name = data.name) @@ -102,7 +102,7 @@ end # Only gradient information function _add_op_data_to_jump( - model::JuMP.Model, + model::JuMP.Model, data::NLPOperator{F, G, Nothing} ) where {F <: Function, G <: Function} JuMP.add_nonlinear_operator(model, data.dim, data.f, data.∇f, name = data.name) @@ -118,8 +118,8 @@ end """ add_operators_to_jump(opt_model::JuMP.Model, inf_model::InfiniteModel)::Nothing -Add the additional nonlinear operators in `inf_model` to a `JuMP` model `opt_model`. -This is intended as an internal method, but it is provided for developers that +Add the additional nonlinear operators in `inf_model` to a `JuMP` model `opt_model`. +This is intended as an internal method, but it is provided for developers that extend `InfiniteOpt` to use other optimizer models. """ function add_operators_to_jump(opt_model::JuMP.Model, inf_model::InfiniteModel) From 5ffb6ac53e99bd73f952b1271d799c896491638d Mon Sep 17 00:00:00 2001 From: pulsipher Date: Fri, 8 Sep 2023 17:15:42 -0400 Subject: [PATCH 34/40] test fix --- test/show.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/show.jl b/test/show.jl index 6fdaea7e2..6bca82467 100644 --- a/test/show.jl +++ b/test/show.jl @@ -717,7 +717,7 @@ end end # test Base.show (GeneralVariableRef in IJulia) @testset "Base.show (IJulia GeneralVariableRef)" begin - show_test(MIME("text/latex"), y, "\$\$ y \$\$") + show_test(MIME("text/latex"), y, "\$ y \$") end # test Base.show (GeneralVariableRef in REPL) @testset "Base.show (REPL GeneralVariableRef)" begin From 5d18d6fa2a149da862f91b47a4cfe1f2334e8992 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Thu, 14 Sep 2023 11:50:53 -0400 Subject: [PATCH 35/40] Add AST mapper --- docs/src/manual/expression.md | 1 + src/InfiniteOpt.jl | 1 + src/expressions.jl | 75 +++++++++++++++++++++++++++++++++++ test/expressions.jl | 40 +++++++++++++++++++ 4 files changed, 117 insertions(+) diff --git a/docs/src/manual/expression.md b/docs/src/manual/expression.md index 97c81a298..524bc36f9 100644 --- a/docs/src/manual/expression.md +++ b/docs/src/manual/expression.md @@ -55,6 +55,7 @@ add_operators_to_jump ```@docs parameter_refs(::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.NonlinearExpr}) map_expression +map_expression_to_ast ``` ## GeneralVariableRef User Methods diff --git a/src/InfiniteOpt.jl b/src/InfiniteOpt.jl index 90e7f82b3..79c67e13b 100644 --- a/src/InfiniteOpt.jl +++ b/src/InfiniteOpt.jl @@ -61,6 +61,7 @@ macro register(args...) error("`@register` has now been replaced with `@operator`, see ", "the nonlinear documenation page for details.") end +Base.@deprecate map_nlp_to_ast(f, expr) map_expression_to_ast(f, expr) # Define additional stuff that should not be exported const _EXCLUDE_SYMBOLS = [Symbol(@__MODULE__), :eval, :include] diff --git a/src/expressions.jl b/src/expressions.jl index 51f8fd6cb..62f9651b0 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -734,6 +734,81 @@ function map_expression(transform::Function, nlp::JuMP.GenericNonlinearExpr) return JuMP.GenericNonlinearExpr(nlp.head, Any[map_expression(transform, arg) for arg in nlp.args]) end +""" + map_expression_to_ast(var_mapper::Function, [op_mapper::Function,] expr::JuMP.AbstractJuMPScalar)::Expr + +Map the expression `expr` to a Julia AST expression where each variable is mapped +via `var_mapper` and is directly interpolated into the AST expression. Any nonlinear +operators can be mapped if needed via `op_mapper` and will be inserted into the +AST expression. This is only intended for developers and advanced users. +""" +function map_expression_to_ast(var_mapper::Function, expr) + return map_expression_to_ast(var_mapper, identity, expr) +end + +# Constant +function map_expression_to_ast(::Function, ::Function, c) + return c +end + +# GeneralVariableRef +function map_expression_to_ast( + var_mapper::Function, + ::Function, + vref::GeneralVariableRef + ) + return var_mapper(vref) +end + +# GenericAffExpr +function map_expression_to_ast( + var_mapper::Function, + ::Function, + aff::GenericAffExpr + ) + ex = Expr(:call, :+) + for (c, v) in JuMP.linear_terms(aff) + if isone(c) + push!(ex.args, var_mapper(v)) + else + push!(ex.args, Expr(:call, :*, c, var_mapper(v))) + end + end + if !iszero(aff.constant) + push!(ex.args, aff.constant) + end + return ex +end + +# GenericQuadExpr +function map_expression_to_ast( + var_mapper::Function, + op_mapper::Function, + quad::GenericQuadExpr + ) + ex = Expr(:call, :+) + for (c, v1, v2) in JuMP.quad_terms(quad) + if isone(c) + push!(ex.args, Expr(:call, :*, var_mapper(v1), var_mapper(v2))) + else + push!(ex.args, Expr(:call, :*, c, var_mapper(v1), var_mapper(v2))) + end + end + append!(ex.args, map_expression_to_ast(var_mapper, op_mapper, quad.aff).args[2:end]) + return ex +end + +# GenericNonlinearExpr +function map_expression_to_ast( + var_mapper::Function, + op_mapper::Function, + expr::JuMP.GenericNonlinearExpr + ) + ex = Expr(:call, op_mapper(expr.head)) + append!(ex.args, (map_expression_to_ast(var_mapper, op_mapper, arg) for arg in expr.args)) + return ex +end + ################################################################################ # COEFFICIENT METHODS ################################################################################ diff --git a/test/expressions.jl b/test/expressions.jl index e20cb6847..9962a644c 100644 --- a/test/expressions.jl +++ b/test/expressions.jl @@ -654,6 +654,46 @@ end end end +# Test map_expression_to_ast +@testset "map_expression_to_ast" begin + # setup model + m = InfiniteModel() + @variable(m, z) + @variable(m, y) + aff = 2z + y + 42 + quad = z^2 + 3 * z * y + 2z + nlp = (sin(z) + aff) ^ 3.4 + jm = Model() + @variable(jm, x) + @variable(jm, w) + vmap(v) = v == z ? x : w + omap(op) = :test + # test constant + @testset "Constant" begin + @test map_expression_to_ast(vmap, 42) == :(42) + end + # test variable + @testset "Variable" begin + @test map_expression_to_ast(vmap, z) == :($x) + end + # test AffExpr + @testset "AffExpr" begin + @test map_expression_to_ast(vmap, aff) == :(2 * $x + $w + 42) + end + # test QuadExpr + @testset "QuadExpr" begin + @test map_expression_to_ast(vmap, quad) == :($x * $x + 3 * $x * $w + 2 * $x) + end + # test GenericNonlinearExpr + @testset "GenericNonlinearExpr" begin + @test map_expression_to_ast(vmap, omap, nlp) == :(test(test(test($x), (2 * $x + $w + 42)), 3.4)) + end + # test deprecation + @testset "map_nlp_to_ast" begin + @test_deprecated map_nlp_to_ast(vmap, nlp) + end +end + # Test _set_variable_coefficient! @testset "_set_variable_coefficient!" begin # initialize model and references From 77779f929279672f8d6a34542cc4b2bf4a1f442e Mon Sep 17 00:00:00 2001 From: pulsipher Date: Thu, 14 Sep 2023 11:52:36 -0400 Subject: [PATCH 36/40] Update dep test --- test/expressions.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/expressions.jl b/test/expressions.jl index 9962a644c..7dab72b39 100644 --- a/test/expressions.jl +++ b/test/expressions.jl @@ -690,7 +690,7 @@ end end # test deprecation @testset "map_nlp_to_ast" begin - @test_deprecated map_nlp_to_ast(vmap, nlp) + @test (@test_deprecated map_nlp_to_ast(vmap, nlp)) == :(test(test(test($x), (2 * $x + $w + 42)), 3.4)) end end From 2fe7739816e9c0349c661025320e2a2b11b1d1d1 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Thu, 14 Sep 2023 12:14:46 -0400 Subject: [PATCH 37/40] fix tests --- test/TranscriptionOpt/transcribe.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/TranscriptionOpt/transcribe.jl b/test/TranscriptionOpt/transcribe.jl index cc11c2aa9..5b416043a 100644 --- a/test/TranscriptionOpt/transcribe.jl +++ b/test/TranscriptionOpt/transcribe.jl @@ -346,7 +346,7 @@ end @test length(transcription_constraint(LowerBoundRef(x))) == 5 @test transcription_constraint(FixRef(x0)) == FixRef(x0t) @test transcription_constraint(BinaryRef(x0)) == BinaryRef(x0t) - @test transcription_constraint(FixRef(y)) == FixRef.(yt)[1:2] + @test transcription_constraint(FixRef(y)) == [FixRef(yt[i]) for i in 1:2] @test transcription_constraint(UpperBoundRef(yf)) == UpperBoundRef(yft) @test transcription_constraint(BinaryRef(z)) == BinaryRef(zt) # test constraint transcriptions From 7dbc57ca581f6d6d5de841202def69e6fe921bbb Mon Sep 17 00:00:00 2001 From: pulsipher Date: Thu, 14 Sep 2023 12:23:53 -0400 Subject: [PATCH 38/40] minor fixes --- src/expressions.jl | 2 +- test/expressions.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/expressions.jl b/src/expressions.jl index 62f9651b0..25caf30aa 100644 --- a/src/expressions.jl +++ b/src/expressions.jl @@ -805,7 +805,7 @@ function map_expression_to_ast( expr::JuMP.GenericNonlinearExpr ) ex = Expr(:call, op_mapper(expr.head)) - append!(ex.args, (map_expression_to_ast(var_mapper, op_mapper, arg) for arg in expr.args)) + append!(ex.args, map_expression_to_ast(var_mapper, op_mapper, arg) for arg in expr.args) return ex end diff --git a/test/expressions.jl b/test/expressions.jl index 7dab72b39..bb05b0e7c 100644 --- a/test/expressions.jl +++ b/test/expressions.jl @@ -690,7 +690,7 @@ end end # test deprecation @testset "map_nlp_to_ast" begin - @test (@test_deprecated map_nlp_to_ast(vmap, nlp)) == :(test(test(test($x), (2 * $x + $w + 42)), 3.4)) + @test (@test_deprecated map_nlp_to_ast(vmap, nlp)) == :((sin(x) + (2.0 * x + w + 42.0)) ^ 3.4) end end From 66a784dcbb39e3eeaaf2f949316d725e027dddf8 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Thu, 14 Sep 2023 12:58:54 -0400 Subject: [PATCH 39/40] minor test fix --- test/expressions.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/expressions.jl b/test/expressions.jl index bb05b0e7c..da6d587b1 100644 --- a/test/expressions.jl +++ b/test/expressions.jl @@ -690,7 +690,7 @@ end end # test deprecation @testset "map_nlp_to_ast" begin - @test (@test_deprecated map_nlp_to_ast(vmap, nlp)) == :((sin(x) + (2.0 * x + w + 42.0)) ^ 3.4) + @test (@test_deprecated map_nlp_to_ast(vmap, nlp)) == :((sin($x) + (2.0 * $x + $w + 42.0)) ^ 3.4) end end From 38405b78210361215e390a83d7099e30172cf60f Mon Sep 17 00:00:00 2001 From: pulsipher Date: Thu, 14 Sep 2023 22:04:45 -0400 Subject: [PATCH 40/40] update to JuMP 1.15 --- Project.toml | 2 +- docs/Project.toml | 2 +- docs/make.jl | 3 --- test/runtests.jl | 3 --- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Project.toml b/Project.toml index 13107bdcf..952991e34 100644 --- a/Project.toml +++ b/Project.toml @@ -16,7 +16,7 @@ Reexport = "189a3867-3050-52da-a836-e630ba90ab69" DataStructures = "0.14.2 - 0.18" Distributions = "0.19 - 0.25" FastGaussQuadrature = "0.3.2 - 0.4, 0.5" -JuMP = "1.2" +JuMP = "1.5" MutableArithmetics = "1" Reexport = "0.2, 1" julia = "^1.6" diff --git a/docs/Project.toml b/docs/Project.toml index fb7125974..091907c4a 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -19,7 +19,7 @@ InfiniteOpt = "0.5" Ipopt = "1.4" HiGHS = "1" julia = "1.6" -JuMP = "^1.11.1" +JuMP = "1.15" Literate = "2.14" Plots = "1" SpecialFunctions = "2" diff --git a/docs/make.jl b/docs/make.jl index a5f9bdf60..b1f63a23d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,6 +1,3 @@ -import Pkg -Pkg.pkg"add JuMP#master" - using Documenter, InfiniteOpt, Distributions, Literate, Random if !@isdefined(EXAMPLE_DIR) diff --git a/test/runtests.jl b/test/runtests.jl index 54198a0b5..7886ed261 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,3 @@ -import Pkg -Pkg.pkg"add JuMP#master" - using InfiniteOpt: _domain_or_error using Test: Error # Load in the dependencies