Skip to content

[Utilities] add eval_variables support for ScalarNonlinearFunction #2219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Bridges/Constraint/bridges/quad_to_soc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,8 @@ function MOI.set(
# | U * x |
# where we compute `x'Qx/2` and `U * x` using the starting values of the variable.
soc = MOI.get(model, MOI.ConstraintFunction(), bridge.soc)
Ux = MOI.Utilities.eval_variables(MOI.Utilities.eachscalar(soc)[3:end]) do v
f = MOI.Utilities.eachscalar(soc)[3:end]
Ux = MOI.Utilities.eval_variables(model, f) do v
return _primal_start_or_error(model, attr, v)
end
if bridge.less_than
Expand Down
2 changes: 1 addition & 1 deletion src/Bridges/Objective/bridges/slack.jl
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ function MOI.set(
end
MOI.set(model, MOI.VariablePrimalStart(), b.slack, zero(T))
f = MOI.get(model, MOI.ConstraintFunction(), b.constraint)
f_val = MOI.Utilities.eval_variables(f) do v
f_val = MOI.Utilities.eval_variables(model, f) do v
return MOI.get(model, MOI.VariablePrimalStart(), v)
end
f_val -= MOI.constant(MOI.get(model, MOI.ConstraintSet(), b.constraint))
Expand Down
69 changes: 69 additions & 0 deletions src/Nonlinear/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -895,3 +895,72 @@ function eval_comparison_function(
return lhs > rhs
end
end

# This method is implmented here because it needs the OperatorRegistry type from
# the Nonlinear, which doesn't exist when the Utilities submodule is defined.

function MOI.Utilities.eval_variables(
value_fn::F,
model::MOI.ModelLike,
f::MOI.ScalarNonlinearFunction,
) where {F}
registry = OperatorRegistry()
return _evaluate_expr(registry, value_fn, model, f)
end

function _evaluate_expr(
::OperatorRegistry,
value_fn::Function,
model::MOI.ModelLike,
f::MOI.AbstractFunction,
)
return MOI.Utilities.eval_variables(value_fn, model, f)
end

function _evaluate_expr(
::OperatorRegistry,
::Function,
::MOI.ModelLike,
f::Number,
)
return f
end

function _evaluate_expr(
registry::OperatorRegistry,
value_fn::Function,
model::MOI.ModelLike,
expr::MOI.ScalarNonlinearFunction,
)
op = expr.head
if !_is_registered(registry, op, length(expr.args))
udf = MOI.get(model, MOI.UserDefinedFunction(op, length(expr.args)))
if udf === nothing
throw(MOI.UnsupportedNonlinearOperator(op))
end
args = map(expr.args) do arg
return _evaluate_expr(registry, value_fn, model, arg)
end
return first(udf)(args...)
end
if length(expr.args) == 1 && haskey(registry.univariate_operator_to_id, op)
arg = _evaluate_expr(registry, value_fn, model, expr.args[1])
return eval_univariate_function(registry, op, arg)
elseif haskey(registry.multivariate_operator_to_id, op)
args = map(expr.args) do arg
return _evaluate_expr(registry, value_fn, model, arg)
end
return eval_multivariate_function(registry, op, args)
elseif haskey(registry.logic_operator_to_id, op)
@assert length(expr.args) == 2
x = _evaluate_expr(registry, value_fn, model, expr.args[1])
y = _evaluate_expr(registry, value_fn, model, expr.args[2])
return eval_logic_function(registry, op, x, y)
else
@assert haskey(registry.comparison_operator_to_id, op)
@assert length(expr.args) == 2
x = _evaluate_expr(registry, value_fn, model, expr.args[1])
y = _evaluate_expr(registry, value_fn, model, expr.args[2])
return eval_comparison_function(registry, op, x, y)
end
end
35 changes: 33 additions & 2 deletions src/Utilities/functions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Returns the output type that results if a function of type `F` is evaluated
using variables with numeric type `T`.

In other words, this is the return type for
`MOI.Utilities.eval_variables(value_fn::Function, f::F)` for a function
`MOI.Utilities.eval_variables(value_fn::Function, model, f::F)` for a function
`value_fn(::MOI.VariableIndex)::T`.
"""
function value_type end
Expand Down Expand Up @@ -107,7 +107,13 @@ Returns the value of function `f` if each variable index `vi` is evaluated as

Note that `value_fn` must return a Number. See [`substitute_variables`](@ref)
for a similar function where `value_fn` returns an
[`MOI.AbstractScalarFunction`](@ref).
[`MOI.AbstractScalarFunction`](@ref).

!!! warning
The two-argument version of `eval_variables` is deprecated and may be
removed in MOI v2.0.0. Use the three-argument method
`eval_variables(::Function, ::MOI.ModelLike, ::MOI.AbstractFunction)`
instead.
"""
function eval_variables end

Expand Down Expand Up @@ -164,6 +170,31 @@ function eval_variables(value_fn::Function, f::MOI.VectorQuadraticFunction)
return out
end

"""
eval_variables(
value_fn::Function,
model::MOI.ModelLike,
f::MOI.AbstractFunction,
)

Returns the value of function `f` if each variable index `vi` is evaluated as
`value_fn(vi)`.

Note that `value_fn` must return a Number. See [`substitute_variables`](@ref)
for a similar function where `value_fn` returns an
[`MOI.AbstractScalarFunction`](@ref).
"""
function eval_variables(
value_fn::F,
model::MOI.ModelLike,
f::MOI.AbstractFunction,
) where {F}
return eval_variables(value_fn, f)
end

# The `eval_variables(::F, ::MOI.ModelLike, ::MOI.ScalarNonlinearFunction)`
# method is defined in the MOI.Nonlinear submodule.

"""
map_indices(index_map::Function, attr::MOI.AnyAttribute, x::X)::X where {X}

Expand Down
9 changes: 4 additions & 5 deletions src/Utilities/results.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ function get_fallback(model::MOI.ModelLike, attr::MOI.ObjectiveValue)
MOI.check_result_index_bounds(model, attr)
F = MOI.get(model, MOI.ObjectiveFunctionType())
f = MOI.get(model, MOI.ObjectiveFunction{F}())
obj = eval_variables(
vi -> MOI.get(model, MOI.VariablePrimal(attr.result_index), vi),
f,
)
obj = eval_variables(model, f) do vi
return MOI.get(model, MOI.VariablePrimal(attr.result_index), vi)
end
if is_ray(MOI.get(model, MOI.PrimalStatus()))
# Dual infeasibiltiy certificates do not include the primal objective
# constant.
Expand Down Expand Up @@ -187,7 +186,7 @@ function get_fallback(
)
MOI.check_result_index_bounds(model, attr)
f = MOI.get(model, MOI.ConstraintFunction(), idx)
c = eval_variables(f) do vi
c = eval_variables(model, f) do vi
return MOI.get(model, MOI.VariablePrimal(attr.result_index), vi)
end
if is_ray(MOI.get(model, MOI.PrimalStatus()))
Expand Down
36 changes: 36 additions & 0 deletions test/Utilities/functions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,42 @@ function test_eval_variables()
return
end

function test_eval_variables_scalar_nonlinear_function()
model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
x = MOI.add_variable(model)
f = MOI.ScalarNonlinearFunction(:log, Any[x])
@test MOI.Utilities.eval_variables(xi -> 0.5, model, f) ≈ log(0.5)
f = MOI.ScalarNonlinearFunction(:*, Any[x, 2.0*x, 1.0*x+2.0])
@test MOI.Utilities.eval_variables(xi -> 0.5, model, f) ≈
0.5 * (2.0 * 0.5) * (1.0 * 0.5 + 2.0)
f = MOI.ScalarNonlinearFunction(
:ifelse,
Any[MOI.ScalarNonlinearFunction(:<, Any[x, 1.0]), 0.0, x],
)
@test MOI.Utilities.eval_variables(xi -> 0.5, model, f) ≈ 0.0
@test MOI.Utilities.eval_variables(xi -> 1.5, model, f) ≈ 1.5
f = MOI.ScalarNonlinearFunction(
:||,
Any[
MOI.ScalarNonlinearFunction(:<, Any[x, 0.0]),
MOI.ScalarNonlinearFunction(:>, Any[x, 1.0]),
],
)
@test MOI.Utilities.eval_variables(xi -> 0.5, model, f) ≈ 0.0
@test MOI.Utilities.eval_variables(xi -> 1.5, model, f) ≈ 1.0
@test MOI.Utilities.eval_variables(xi -> -0.5, model, f) ≈ 1.0
my_square(x, y) = (x - y)^2
MOI.set(model, MOI.UserDefinedFunction(:my_square, 2), (my_square,))
f = MOI.ScalarNonlinearFunction(:my_square, Any[x, 1.0])
@test MOI.Utilities.eval_variables(xi -> 0.5, model, f) ≈ (0.5 - 1.0)^2
f = MOI.ScalarNonlinearFunction(:bad_f, Any[x, 1.0])
@test_throws(
MOI.UnsupportedNonlinearOperator(:bad_f),
MOI.Utilities.eval_variables(xi -> 0.5, model, f)
)
return
end

function test_substitute_variables()
# We do tests twice to make sure the function is not modified
subs = Dict(w => 1.0y + 1.0z, x => 2.0y + 1.0, y => 1.0y, z => -1.0w)
Expand Down