diff --git a/Project.toml b/Project.toml index 3931da940..952991e34 100644 --- a/Project.toml +++ b/Project.toml @@ -4,33 +4,28 @@ authors = ["Joshua Pulsipher and Weiqi Zhang"] version = "0.5.8" [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" +JuMP = "1.5" 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/docs/Project.toml b/docs/Project.toml index 36ec0ebaa..091907c4a 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -10,15 +10,16 @@ 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" Distributions = "0.25" Documenter = "0.27" InfiniteOpt = "0.5" -JuMP = "^1.11.1" -Ipopt = "1" +Ipopt = "1.4" +HiGHS = "1" +julia = "1.6" +JuMP = "1.15" Literate = "2.14" Plots = "1" -julia = "1.6" SpecialFunctions = "2" diff --git a/docs/src/develop/extensions.md b/docs/src/develop/extensions.md index acf473c85..02fa3358d 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,29 +928,14 @@ 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 + 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 @@ -975,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) 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 41a2e26ec..06a0efc7e 100644 --- a/docs/src/guide/expression.md +++ b/docs/src/guide/expression.md @@ -4,32 +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 handled differently in `InfiniteOpt` vs `JuMP`. 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; @@ -41,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)); @@ -57,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]); @@ -65,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)) @@ -76,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 @@ -128,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]) @@ -159,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: @@ -184,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 @@ -206,13 +207,13 @@ 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} ``` -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: @@ -221,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 @@ -233,169 +234,97 @@ 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 +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) -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. - -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: + 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 +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)); 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)) + The legacy `@NLexpression`, `@NLobjective`, and `@NLconstraint` macros in `JuMP` + are not supported by `InfiniteOpt`. -julia> InfiniteOpt.ifelse(y == 0, y^2.3, exp(y)) -ifelse(y(t) == 0, y(t)^2.3, exp(y(t))) - -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 +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]); 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 -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); 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 +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 [Adding 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 @@ -408,96 +337,89 @@ 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 -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 - 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 `.>=` +We can also set vectorized constraints using the `.==`, `.<=`, and `.>=` operators: ```jldoctest nlp 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 + ((+(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 ``` -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: +### 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 +type `Real`): ```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> h(a, b) = a * b^2; # an overly simple example operator -julia> optimize!(model) -ERROR: TranscriptionOpt does not support vector constraints of general nonlinear expressions because this is not yet supported by JuMP. -``` - -### 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`): -```jldoctest nlp -julia> h(a, b) = a * b^2; # an overly simple example user-defined function +julia> @operator(model, op_h, 2, h); -julia> @register(model, h(a, b)); - -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(f, :op_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 -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) @@ -511,47 +433,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 -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> my_eta(a) = eta(a); - -julia> @register(model, my_eta(a)); +julia> @operator(model, op_eta, 1, eta) +NonlinearOperator(eta, :op_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: +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; -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 +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; @@ -561,85 +478,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(w, :op_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 +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(-) - -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); + We do not currently support vector inputs or vector valued functions + directly, since typically `JuMP` optimizer model backends don't support them. -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/index.md b/docs/src/index.md index 1d27f73cf..ce6e442a2 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -6,6 +6,10 @@ A `JuMP` extension for expressing and solving infinite-dimensional optimization problems. +!!! note + `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 infinite-dimensional optimization problems (i.e., problems with decision diff --git a/docs/src/manual/expression.md b/docs/src/manual/expression.md index a8701a61a..524bc36f9 100644 --- a/docs/src/manual/expression.md +++ b/docs/src/manual/expression.md @@ -39,28 +39,23 @@ JuMP.delete(::InfiniteModel, ::ParameterFunctionRef) ## [Nonlinear Expressions](@id nlp_manual) ### DataTypes ```@docs -NodeData -NLPExpr -RegisteredFunction +NLPOperator ``` -### Methods/Macros +### Methods ```@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 +JuMP.add_nonlinear_operator +all_nonlinear_operators +name_to_operator +added_nonlinear_operators +add_operators_to_jump ``` ## Expression Methods ```@docs -parameter_refs(::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr}) +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 ec7678559..79c67e13b 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 @@ -38,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") @@ -48,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") @@ -62,6 +56,13 @@ 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 +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/TranscriptionOpt/model.jl b/src/TranscriptionOpt/model.jl index 44d4c43e5..6cd542287 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.GenericNonlinearExpr}; label::Type{<:InfiniteOpt.AbstractSupportLabel} = InfiniteOpt.PublicLabel, ndarray::Bool = false ) @@ -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 @@ -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.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, InfiniteOpt.NLPExpr}, + 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 9e03b36ce..ccaf430f2 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.GenericNonlinearExpr}, 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 @@ -576,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 @@ -598,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 @@ -616,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 @@ -627,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 @@ -638,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 @@ -649,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 @@ -660,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 @@ -670,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]) || @@ -681,46 +651,6 @@ function _support_in_restrictions( return true 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 - -## Helper for adding constrints correctly via dispatch -# New function is NonlinearExpression -function _add_constraint(model, func::JuMP.NonlinearExpression, set, name) - return JuMP.add_nonlinear_constraint(model, _make_constr_ast(func, set)) -end - -# New expression is not nonlinear -function _add_constraint(model, func::JuMP.AbstractJuMPScalar, set, name) - trans_constr = JuMP.build_constraint(error, func, set) - return JuMP.add_constraint(model, trans_constr, name) -end - - ## Process constraint objects at a particular support value and make the new ## transcribed version # JuMP.ScalarConstraint @@ -733,20 +663,8 @@ function _process_constraint( name::String ) new_func = transcription_expression(trans_model, func, raw_supp) - return _add_constraint(trans_model, new_func, set, name) -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 _add_constraint(trans_model, nlp_ref, set, name) + trans_constr = JuMP.build_constraint(error, new_func, set) + return JuMP.add_constraint(trans_model, trans_constr, name) end # JuMP.VectorConstraint @@ -759,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.NonlinearExpression, 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) @@ -800,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 @@ -884,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) @@ -938,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) @@ -952,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 0b099fd6c..3a71ef158 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -1189,6 +1189,73 @@ mutable struct ConstraintData{C <: JuMP.AbstractConstraint} <: AbstractDataObjec is_info_constraint::Bool end +################################################################################ +# NONLINEAR OPERATORS +################################################################################ +""" + NLPOperator{F <: Function, G <: Union{Function, Nothing}, + H <: Union{Function, Nothing}} + +A type for storing new nonlinear operators and their information +that is ultimately for automatic differentiation. The constructor is of the form: +```julia + NLPOperator(name::Symbol, dim::Int, f::Function, + [∇f::Function, ∇²f::Function]) +``` + +**Fields** +- `name::Symbol`: The name of the operator that is used. +- `dim::Int`: The number of function arguments. +- `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 NLPOperator{F <: Function, G, H} + name::Symbol + dim::Int + f::F + ∇f::G + ∇²f::H + + # Constructors + function NLPOperator( + name::Symbol, + dim::Int, + f::F + ) where {F <: Function} + return new{F, Nothing, Nothing}(name, dim, f, nothing, nothing) + end + function NLPOperator( + name::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}(name, dim, f, ∇f, nothing) + end + function NLPOperator( + name::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}(name, dim, f, ∇f, ∇²f) + end +end + ################################################################################ # INFINITE MODEL ################################################################################ @@ -1197,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{RegisteredFunction}`: The nonlinear registered functions. -- `Dict{Tuple{Symbol, Int}, Function}`: Map a name and number of arguments to a registered function. -- `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 @@ -1285,9 +1301,9 @@ mutable struct InfiniteModel <: JuMP.AbstractModel objective_function::JuMP.AbstractJuMPScalar objective_has_measures::Bool - # Function Registration - registrations::Vector{Any} - func_lookup::Dict{Tuple{Symbol, Int}, Function} + # Operator Registration + operators::Vector{NLPOperator} + op_lookup::Dict{Symbol, Tuple{Function, Int}} # Objects obj_dict::Dict{Symbol, Any} @@ -1377,8 +1393,8 @@ function InfiniteModel(; zero(JuMP.GenericAffExpr{Float64, GeneralVariableRef}), false, # registration - RegisteredFunction[], - Dict{Tuple{Symbol, Int}, Function}(), + NLPOperator[], + Dict{Symbol, Tuple{Function, Int}}(), # Object dictionary Dict{Symbol, Any}(), # Optimize data @@ -1467,7 +1483,8 @@ 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) model.ready_to_optimize = false diff --git a/src/derivative_evaluations.jl b/src/derivative_evaluations.jl index df3fbc070..5accd84ad 100644 --- a/src/derivative_evaluations.jl +++ b/src/derivative_evaluations.jl @@ -228,10 +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)) + 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 @@ -247,10 +246,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 @_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 @@ -265,10 +263,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 @_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/src/derivatives.jl b/src/derivatives.jl index 0a41bd696..0ae29e172 100644 --- a/src/derivatives.jl +++ b/src/derivatives.jl @@ -381,13 +381,13 @@ end # AffExpr function _build_deriv_expr(aff::JuMP.GenericAffExpr, pref) - return _MA.@rewrite(sum(c * _build_deriv_expr(v, pref) + 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)) diff --git a/src/expressions.jl b/src/expressions.jl index 4897d07c2..25caf30aa 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.GenericNonlinearExpr) + stack = Vector{Any}[nlp.args] + while !isempty(stack) + args = pop!(stack) + for arg in args + if arg isa JuMP.GenericNonlinearExpr + 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.GenericNonlinearExpr, AbstractArray}) vref_set = Set{GeneralVariableRef}() _interrogate_variables(v -> push!(vref_set, v), f) return collect(vref_set) @@ -644,66 +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 - -# 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 - 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 ################################################################################ @@ -727,29 +634,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.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)) + end + while !isempty(stack) + arr, idx = pop!(stack) + expr = arr[idx] + 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 + else + arr[idx] = _remove_variable_from_leaf(expr, vref) + end end return end @@ -776,38 +691,122 @@ 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) + # flatten needed in case expression becomes nonlinear with transform + return JuMP.flatten!(@_expr(sum(c * transform(v) for (c, v) in JuMP.linear_terms(aff)) + - JuMP.constant(aff)) + JuMP.constant(aff))) 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!(@_expr(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))) +end + +# 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.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.GenericNonlinearExpr + # new_expr = JuMP.GenericNonlinearExpr{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.GenericNonlinearExpr(nlp.head, Any[map_expression(transform, arg) for arg in nlp.args]) end -# Helper methods for mapping NLP trees -function _process_node(expr::NLPExpr) - return expr.tree_root +""" + 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 -function _process_node(expr) - return _LCRST.Node(NodeData(expr)) + +# Constant +function map_expression_to_ast(::Function, ::Function, c) + return c end -function _map_expr_node(transform, data::JuMP.AbstractJuMPScalar) - return _process_node(map_expression(transform, data)) + +# GeneralVariableRef +function map_expression_to_ast( + var_mapper::Function, + ::Function, + vref::GeneralVariableRef + ) + return var_mapper(vref) end -function _map_expr_node(transform, data) - return _LCRST.Node(NodeData(data)) + +# 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 -# NLPExpr -function map_expression(transform::Function, nlp::NLPExpr) - return NLPExpr(_map_tree(n -> _map_expr_node(transform, _node_value(n.data)), - nlp.tree_root)) +# 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 ################################################################################ @@ -818,7 +817,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 +827,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 +841,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 +864,7 @@ end function _affine_coefficient( func::GeneralVariableRef, var::GeneralVariableRef - )::Float64 + ) return isequal(func, var) ? 1.0 : 0.0 end @@ -873,7 +872,7 @@ end function _affine_coefficient( func::GenericAffExpr, var::GeneralVariableRef - )::Float64 + ) return get(func.terms, var, 0.0) end @@ -881,7 +880,7 @@ end function _affine_coefficient( func::GenericQuadExpr, var::GeneralVariableRef - )::Float64 + ) return get(func.aff.terms, var, 0.0) end @@ -924,9 +923,9 @@ julia> parameter_refs(my_expr) ``` """ function parameter_refs( - expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, NLPExpr} - )::Tuple - model = _model_from_expr(expr) + expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, JuMP.GenericNonlinearExpr} + ) + model = JuMP.owner_model(expr) if isnothing(model) return () else 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 1b94f3737..663b42855 100644 --- a/src/measure_expansions.jl +++ b/src/measure_expansions.jl @@ -241,13 +241,13 @@ 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))) # 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))) end @@ -266,7 +266,7 @@ 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))) # treat variable as constant if doesn't have measure parameter @@ -277,7 +277,7 @@ 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))) # make semi-infinite variables if the variable contains other parameters @@ -290,7 +290,7 @@ 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))) end @@ -328,7 +328,7 @@ 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))) @@ -338,7 +338,7 @@ function expand_measure(rvref::GeneralVariableRef, 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))) end @@ -386,7 +386,7 @@ 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))) @@ -405,7 +405,7 @@ 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))) end @@ -497,8 +497,9 @@ 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) + 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 # GenericAffExpr (Multi DiscreteMeasureData) @@ -512,8 +513,9 @@ 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) + 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 # GenericQuadExpr (1D DiscreteMeasureData) @@ -534,11 +536,12 @@ 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 = @_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)) + return JuMP.flatten!(new_ex) # just in case we have nested measures producing a NonlinearExpr end # GenericQuadExpr(Multi DiscreteMeasureData) @@ -559,16 +562,17 @@ 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 = @_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)) + return JuMP.flatten!(new_ex) # just in case we have nested measures producing a NonlinearExpr end -# NLPExpr (1D DiscreteMeasureData) +# NonlinearExpr (1D DiscreteMeasureData) function expand_measure( - expr::NLPExpr, + expr::JuMP.GenericNonlinearExpr, data::DiscreteMeasureData{GeneralVariableRef, 1}, write_model::JuMP.AbstractModel ) @@ -584,14 +588,15 @@ 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 -# NLPExpr (Multi DiscreteMeasureData) +# NonlinearExpr (Multi DiscreteMeasureData) function expand_measure( - expr::NLPExpr, + expr::JuMP.GenericNonlinearExpr, data::DiscreteMeasureData{Vector{GeneralVariableRef}, 2}, write_model::JuMP.AbstractModel ) @@ -607,9 +612,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 @@ -835,7 +841,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/measures.jl b/src/measures.jl index 6d0a6980c..10d688253 100644 --- a/src/measures.jl +++ b/src/measures.jl @@ -756,11 +756,10 @@ an finite variable parameter bounds of finite variables that are included in the measure. """ function build_measure( - expr::T, - data::D; - )::Measure{T, D} where {T <: JuMP.AbstractJuMPScalar, D <: AbstractMeasureData} + expr::JuMP.AbstractJuMPScalar, + data::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 afdc748fc..0055ead83 100644 --- a/src/nlp.jl +++ b/src/nlp.jl @@ -1,1537 +1,130 @@ ################################################################################ -# DATATYPES +# USER OPERATORS ################################################################################ -# 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 +# Keep track of the predefined functions in MOI +const _NativeNLPOperators = append!(copy(MOI.Nonlinear.DEFAULT_UNIVARIATE_OPERATORS), + MOI.Nonlinear.DEFAULT_MULTIVARIATE_OPERATORS) +append!(_NativeNLPOperators, (:&&, :||, :<=, :(==), :>=, :<, :>)) """ - 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}} + JuMP.add_nonlinear_operator( + model::InfiniteModel, + dim::Int, + f::Function, + [∇f::Function,] + [∇²f::Function]; + [name::Symbol = Symbol(f)] ) - 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) => ^ -) +Extend `add_nonlinear_operator` for `InfiniteModel`s. -# 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 -) +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) +can be used for a more convenient syntax. -# 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 +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. -# Setup the ifelse function -_NativeNLPFunctions[(:ifelse, 3)] = Core.ifelse +```julia-repl +julia> @variable(model, y); -""" - InfiniteOpt.ifelse(cond::NLPExpr, v1::Union{AbstractInfOptExpr, Real}, - v2::Union{AbstractInfOptExpr, Real})::NLPExpr +julia> g(x) = x^2; -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`. +julia> new_op = add_nonlinear_operator(model, 1, g) +NonlinearOperator(g, :g) -**Example** -```julia -julia> InfiniteOpt.ifelse(x >= y, 0, y^3) -ifelse(x >= y, 0, y^3) +julia> @expression(model, new_op(y)) +g(y) ``` """ -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 +function JuMP.add_nonlinear_operator( + model::InfiniteModel, + dim::Int, + f::Function, + funcs::Vararg{Function, N}; + name::Symbol = Symbol(f) + ) where {N} + if name in _NativeNLPOperators || name in keys(model.op_lookup) + error("An operator with name `$name` arguments is already " * + "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.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(f, name) end -################################################################################ -# USER FUNCTIONS -################################################################################ """ - RegisteredFunction{F <: Function, G <: Union{Function, Nothing}, - H <: Union{Function, Nothing}} + name_to_operator(model::InfiniteModel, name::Symbol)::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(name::Symbol, num_args::Int, func::Function, - [gradient::Function, hessian::Function]) -``` +Return the nonlinear operator that corresponds to `name`. +Returns `nothing` if no such operator exists. -**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. +!!! warning + Currently, this does not return functions for default operators. """ -struct RegisteredFunction{F <: Function, G, H} - name::Symbol - num_args::Int - func::F - gradient::G - hessian::H - - # Constructors - function RegisteredFunction( - name::Symbol, - num_args::Int, - func::F - ) where {F <: Function} - return new{F, Nothing, Nothing}(name, num_args, func, nothing, nothing) - end - function RegisteredFunction( - name::Symbol, - num_args::Int, - func::F, - gradient::G - ) where {F <: Function, G <: Function} - if isone(num_args) && !hasmethod(gradient, 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)...}) - error("Invalid multi-variate gradient function form, see the docs for details.") - end - return new{F, G, Nothing}(name, num_args, func, gradient, nothing) - end - function RegisteredFunction( - name::Symbol, - num_args::Int, - func::F, - gradient::G, - hessian::H - ) where {F <: Function, G <: Function, H <: Function} - if isone(num_args) && !hasmethod(gradient, Tuple{Real}) - error("Invalid gradient function form, see the docs for details.") - elseif isone(num_args) && !hasmethod(hessian, Tuple{Real}) - error("Invalid hessian function form, see the docs for details.") - end - return new{F, G, H}(name, num_args, func, gradient, hessian) - end -end - -# Helper function for @register -function _register( - _error::Function, - call_mod::Module, - model::InfiniteModel, - name::Symbol, - num_args::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 " * - "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 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] +function name_to_operator(model::InfiniteModel, name::Symbol) + haskey(model.op_lookup, name) && return model.op_lookup[name][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.") - 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) -end - -""" - name_to_function(model::InfiniteModel, name::Symbol, num_args::Int)::Union{Function, Nothing} - -Return the registered function that corresponds to `name` with `num_args`. -Returns `nothing` if no such registered function exists. This helps retrieve the -functions of function names stored in `NLPExpr`s. -""" -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 -end - """ - all_registered_functions(model::InfiniteModel)::Vector{Function} + all_nonlinear_operators(model::InfiniteModel)::Vector{Symbol} -Retrieve all the functions that are currently registered to `model`. +Retrieve all the operators that are currently added to `model`. """ -function all_registered_functions(model::InfiniteModel) - funcs = append!(collect(values(_NativeNLPFunctions)), (+, *)) - return append!(funcs, values(model.func_lookup)) +function all_nonlinear_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_defined_operators(model::InfiniteModel)::Vector{NLPOperator} -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 +added to `model`. Each is stored as a [`NLPOperator`](@ref). """ -function user_registered_functions(model::InfiniteModel) - return model.registrations +function added_nonlinear_operators(model::InfiniteModel) + return model.operators end -## Define helper function to add registered functions to JuMP +## Define helper function to add nonlinear operators to JuMP # No gradient or hessian -function _add_func_data_to_jump( - model::JuMP.Model, - data::RegisteredFunction{F, Nothing, Nothing} +function _add_op_data_to_jump( + model::JuMP.Model, + data::NLPOperator{F, Nothing, Nothing} ) where {F <: Function} - JuMP.register(model, data.name, data.num_args, data.func, autodiff = true) + JuMP.add_nonlinear_operator(model, data.dim, data.f, name = data.name) return end # Only gradient information -function _add_func_data_to_jump( - model::JuMP.Model, - data::RegisteredFunction{F, G, Nothing} +function _add_op_data_to_jump( + model::JuMP.Model, + data::NLPOperator{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_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) - 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) +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 functions 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_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) +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 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/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 6985e910e..6ef30839f 100644 --- a/src/results.jl +++ b/src/results.jl @@ -239,16 +239,15 @@ julia> value(my_infinite_expr) ``` """ function JuMP.value( - expr::AbstractInfOptExpr; + expr::Union{JuMP.GenericAffExpr{Float64, GeneralVariableRef}, JuMP.GenericQuadExpr{Float64, GeneralVariableRef}, JuMP.GenericNonlinearExpr{GeneralVariableRef}}; result::Int = 1, 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 NLPExpr && 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 @@ -333,9 +332,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 NLPExpr - 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) 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 59de8b25f..5b416043a 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 @@ -302,14 +301,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 @@ -324,12 +315,10 @@ end con = constraint_object(c7) func = jump_function(con) set = moi_set(con) - expected = Sys.iswindows() ? "subexpression[2] - 0.0 == 0" : "subexpression[2] - 0.0 = 0" - @test sprint(show, IOTO._process_constraint(tm, con, func, set, zeros(3), "test1")) == expected - expected = ["subexpression[2]: sin(z) ^ x(support: 1) - 0.0", - "subexpression[2]: sin(z) ^ x(support: 2) - 0.0"] - @test sprint(show, NonlinearExpression(tm, 2)) 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) @@ -342,8 +331,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") - tm.nlp_model = nothing + @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") @@ -358,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 @@ -374,9 +362,7 @@ 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 transcription_constraint(c8) isa NonlinearConstraintRef - @test length(keys(tm.nlp_model.constraints)) == 7 - @test length(tm.nlp_model.expressions) == 7 + @test transcription_constraint(c8) isa ConstraintRef # 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 @@ -453,8 +439,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) + @operator(m, gr, 1, g) + @constraint(m, c7, gr(z) == 2) @objective(m, Min, x0 + meas1) # test basic usage tm = optimizer_model(m) @@ -503,10 +489,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 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] @@ -524,7 +510,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/datatypes.jl b/test/datatypes.jl index 10f337e46..0165a7eb7 100644 --- a/test/datatypes.jl +++ b/test/datatypes.jl @@ -467,3 +467,43 @@ end @test ConstraintData <: AbstractDataObject @test ConstraintData(con, [1], "", MeasureIndex[], false) isa ConstraintData end + +# Test the operator constructor +@testset "Nonlinear Operators" 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 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 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/deletion.jl b/test/deletion.jl index 5035517e3..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)), sin(zero(NLPExpr))) + @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/derivatives.jl b/test/derivatives.jl index 1930ec7fe..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 * gvref * x) + @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/expressions.jl b/test/expressions.jl index 1faf2b103..da6d587b1 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 GenericNonlinearExpr + @testset "GenericNonlinearExpr" 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 "GenericNonlinearExpr" 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 GenericNonlinearExpr + @testset "GenericNonlinearExpr" 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 GenericNonlinearExpr + @testset "GenericNonlinearExpr" begin # make expressions nlp = sin(inf2) # test expressions @@ -599,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 NLPExpr - @testset "NLPExpr" begin - # make expressions - nlp1 = sin(hd) - nlp2 = zero(NLPExpr) - 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 @@ -687,19 +600,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 GenericNonlinearExpr + @testset "GenericNonlinearExpr" 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,13 +647,53 @@ 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 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 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 (@test_deprecated map_nlp_to_ast(vmap, nlp)) == :((sin($x) + (2.0 * $x + $w + 42.0)) ^ 3.4) + end +end + # Test _set_variable_coefficient! @testset "_set_variable_coefficient!" begin # initialize model and references diff --git a/test/extensions/optimizer_model.jl b/test/extensions/optimizer_model.jl index 91a98d398..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 @@ -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/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/measure_expansions.jl b/test/measure_expansions.jl index 2bf7e4d8d..06d66ce4e 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 "GenericNonlinearExpr (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 "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 8a99600bd..2fd3103f4 100644 --- a/test/nlp.jl +++ b/test/nlp.jl @@ -1,855 +1,8 @@ -# 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 - m = InfiniteModel() - @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 - @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 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 +# Test operator utilities +@testset "Operator 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 @@ -862,166 +15,87 @@ 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) - end - # test @register - @testset "@register" begin + 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_nonlinear_operator + @testset "JuMP.add_nonlinear_operator" begin + # test errors + @test_throws ErrorException add_nonlinear_operator(m, 1, f, name = :max) + m.op_lookup[:f] = (f, 1) + @test_throws ErrorException add_nonlinear_operator(m, 1, f) + empty!(m.op_lookup) + @test_throws ErrorException add_nonlinear_operator(m, 2, f) + # test normal + @test add_nonlinear_operator(m, 1, f).head == :f + @test m.op_lookup[:f] == (f, 1) + @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.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 + @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_nonlinear_operators + @testset "all_nonlinear_operators" begin + @test all_nonlinear_operators(m) isa Vector{Symbol} + end + # test added_nonlinear_operators + @testset "added_nonlinear_operators" begin + @test added_nonlinear_operators(m) isa Vector{NLPOperator} + end + empty!(m.operators) + empty!(m.op_lookup) + # test @operator + @testset "@operator" 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 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, q(a)) isa Function - @test q(x) isa NLPExpr + @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 - 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 + @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,) + @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 - -# 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..ec50b79d1 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -919,3 +919,81 @@ 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) + # prepare the test grid + aff = 2z - 2 + quad = y^2 + 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)] + 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 GenericNonlinearExpr + # @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/results.jl b/test/results.jl index a175d233d..b345686b7 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(GenericNonlinearExpr{GeneralVariableRef}(:sin, Any[0])) == 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. @@ -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 48ed63ad4..7886ed261 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,7 +2,7 @@ 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 @@ -16,7 +16,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 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 diff --git a/test/utilities.jl b/test/utilities.jl index a91a41675..580e13fe0 100644 --- a/test/utilities.jl +++ b/test/utilities.jl @@ -128,3 +128,9 @@ function _update_variable_param_refs(vref::InfiniteVariableRef, InfiniteOpt._set_core_variable_object(vref, new_var) return end + +function Base.isequal(nlp1::GenericNonlinearExpr, nlp2::GenericNonlinearExpr) + return nlp1.head == nlp2.head && isequal(nlp1.args, nlp2.args) +end + +