# What is the difference between nonlinear "functions with vector outputs" and nonlinear "operators with vector outputs"?

The JuMP documentation and this answer suggests that nonlinear user-defined functions with vector outputs need to be defined using many `@operators`. The documentation actually says “User-defined operators …”.

This post suggest that nonlinear functions can be used directly. The date on that post and syntax suggest an old version of JuMP.

The example below also suggest that nonlinear functions can be used directly.

This works

``````import Pkg; Pkg.activate(temp=true)
Pkg.instantiate();

import JuMP
import Ipopt

model = JuMP.Model(Ipopt.Optimizer)

function dy(x)
return [cos(x[1])*x[2]; sin(x[1]) + sin(x[2])]
end

x0 = [.1;.1]
xc = [1;0.5]

JuMP.@variable(model, x[i=1:2], start = x0[i])
JuMP.@constraint(model, dy(x) == xc)
JuMP.optimize!(model)

print("x: ", JuMP.value.(x))
print("dy(x): ", dy(JuMP.value.(x)))
``````
``````...
EXIT: Optimal Solution Found.
x: [-0.39325899095511446, 1.0826434588266454]dy(x): [0.9999999999993564, 0.5000000000001374]
``````

So does this

``````import Pkg; Pkg.activate(temp=true)
Pkg.instantiate();

import JuMP
import Ipopt

function dy(x)
return [cos(x[1])*x[2]; sin(x[1]) + sin(x[2])]
end

x0 = [.1;.1]
xc = [1;0.5]

model = JuMP.Model(Ipopt.Optimizer)
JuMP.@variable(model, x[i=1:2], start = x0[i])

JuMP.@operator(model, op_f1, 2, (x...) -> dy(collect(x))[1])
JuMP.@operator(model, op_f2, 2, (x...) -> dy(collect(x))[2])

JuMP.@constraint(model, op_f1(x...) == xc[1])
JuMP.@constraint(model, op_f2(x...) == xc[2])

JuMP.optimize!(model)

print("x: ", JuMP.value.(x))
print("dy(x): ", dy(JuMP.value.(x)))
``````
``````...
EXIT: Optimal Solution Found.
x: [-0.39325899095511446, 1.0826434588266454]dy(x): [0.9999999999993564, 0.5000000000001374]
``````

Is there a preferred syntax?

Is there a reason to prefer the `@operator` macro over the user-defined function?

If we have a nice conclusion I will open a PR to add to the documentation

PS:
This for nonlinear and this for linear are possibly related.

It is possible that my confusion lies entirely with when to use an operator and is unrelated to “vector outputs”. The below example illustrates the need (?) for an `@operator` in the case of taking a derivative.

This does not work

``````import Pkg; Pkg.activate(temp=true)
Pkg.instantiate();

import JuMP
import Ipopt
import ForwardDiff as FD

f(x) = FD.derivative(sin,x)

model = JuMP.Model(Ipopt.Optimizer)
JuMP.@variable(model, x, start = 0.4)
JuMP.@constraint(model, f(x) == 0.5)
JuMP.optimize!(model)

print("x: ", JuMP.value.(x))
``````
``````MethodError: no method matching derivative(::typeof(sin), ::JuMP.VariableRef)

Closest candidates are:
derivative(::F, ::R) where {F, R<:Real}
@ ForwardDiff ~/.julia/packages/ForwardDiff/PcZ48/src/derivative.jl:12
derivative(::Any, ::AbstractArray)
@ ForwardDiff ~/.julia/packages/ForwardDiff/PcZ48/src/derivative.jl:72
derivative(::Any, ::AbstractArray, ::Real)
@ ForwardDiff ~/.julia/packages/ForwardDiff/PcZ48/src/derivative.jl:25
...

Stacktrace:
[1] f(x::JuMP.VariableRef)
@ Main ./In[2]:11
[2] macro expansion
@ ~/.julia/packages/MutableArithmetics/K9YPJ/src/rewrite.jl:321 [inlined]
[3] macro expansion
@ ~/.julia/packages/JuMP/ToPd2/src/macros.jl:717 [inlined]
[4] top-level scope
@ In[2]:15
``````

This works

``````import Pkg; Pkg.activate(temp=true)
Pkg.instantiate();

import JuMP
import Ipopt
import ForwardDiff as FD

f(x) = FD.derivative(sin,x)

model = JuMP.Model(Ipopt.Optimizer)
JuMP.@operator(model, op_f, 1, f)

JuMP.@variable(model, x, start = 0.4)
JuMP.@constraint(model, op_f(x) == 0.5)
JuMP.optimize!(model)

print("x: ", JuMP.value.(x))
``````
``````...
EXIT: Optimal Solution Found.
x: 1.0471975562573468
``````

The macro and struct suggests we need to use an operator " When called with non-JuMP types, the struct returns the evaluation of `func(args...)` .".

So the answer to question might be.

1. vector outputs are fine to use as is for JuMP types.
2. use `@operators` when multiple dispatch has not been implemented for JuMP types.

Or I could be wrong again

1 Like

You’re on the right track.

For some simple algebraic functions, like your `dy`, you can call it directly, including vector valued outputs. Under the hood, JuMP traces the function to create a vector of nonlinear expressions. (Try calling the function in the REPL.)

For functions that we cannot trace, like your gradient function, you must define it as an operator. We either need to be able to automatically differentiate it, or you need to provide the gradient. User defined operators must return a scalar.

If you have any suggestions for how we could improve the documentation please let me know. Its always hard for me to know what bits are confusing

1 Like