Skip to content
Closed
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
39 changes: 36 additions & 3 deletions src/hull.jl
Original file line number Diff line number Diff line change
Expand Up @@ -281,14 +281,47 @@ function reformulate_disjunct_constraint(
reform_con = JuMP.build_constraint(error, new_func, con.set)
return [reform_con]
end
################################################################################
# NONLINEAR EXPRESSION AT ZERO
################################################################################

# Value of an expression with every variable set to zero, the constant
# f(0) of the perspective function. Checked here rather than through
# JuMP.value(f, expr), whose leaf needs a method per variable
# reference type, so any AbstractVariableRef works.
_evaluate_at_zero(c::Number) = convert(Float64, c)
_evaluate_at_zero(::JuMP.AbstractVariableRef) = 0.0
_evaluate_at_zero(aff::JuMP.GenericAffExpr) = convert(Float64, aff.constant)
_evaluate_at_zero(quad::JuMP.GenericQuadExpr) = _evaluate_at_zero(quad.aff)
function _evaluate_at_zero(nlp::JuMP.GenericNonlinearExpr)
registry = _MOI.Nonlinear.OperatorRegistry()
args = Real[_evaluate_at_zero(arg) for arg in nlp.args]
op = nlp.head
if length(args) == 1 && haskey(registry.univariate_operator_to_id, op)
return _MOI.Nonlinear.eval_univariate_function(registry, op, args[1])
elseif haskey(registry.multivariate_operator_to_id, op)
return _MOI.Nonlinear.eval_multivariate_function(registry, op, args)
elseif haskey(registry.logic_operator_to_id, op)
return _MOI.Nonlinear.eval_logic_function(registry, op, args...)
elseif haskey(registry.comparison_operator_to_id, op)
return _MOI.Nonlinear.eval_comparison_function(registry, op, args...)
end
# operators registered on the model
udf = _MOI.get(JuMP.owner_model(nlp),
_MOI.UserDefinedFunction(op, length(args)))
udf === nothing && error("Unable to evaluate nonlinear operator `$op` " *
"because it was not added as an operator.")
return first(udf)(args...)
end

function reformulate_disjunct_constraint(
model::JuMP.AbstractModel,
con::JuMP.ScalarConstraint{T, S},
bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr},
method::_Hull
) where {T <: JuMP.GenericNonlinearExpr, S <: Union{_MOI.LessThan, _MOI.GreaterThan, _MOI.EqualTo}}
con_func = _disaggregate_nl_expression(model, con.func, bvref, method)
con_func0 = JuMP.value(v -> 0.0, con.func)
con_func0 = _evaluate_at_zero(con.func)
if isinf(con_func0)
error("Operator `$(con.func.head)` is not defined at 0, causing the perspective function on the Hull reformulation to fail.")
end
Expand All @@ -307,7 +340,7 @@ function reformulate_disjunct_constraint(
con_func = JuMP.@expression(model, [i=1:con.set.dimension],
_disaggregate_nl_expression(model, con.func[i], bvref, method)
)
con_func0 = JuMP.value.(v -> 0.0, con.func)
con_func0 = _evaluate_at_zero.(con.func)
if any(isinf.(con_func0))
error("At least of of the operators `$([func.head for func in con.func])` is not defined at 0, causing the perspective function on the Hull reformulation to fail.")
end
Expand Down Expand Up @@ -338,7 +371,7 @@ function reformulate_disjunct_constraint(
method::_Hull
) where {T <: JuMP.GenericNonlinearExpr, S <: _MOI.Interval}
con_func = _disaggregate_nl_expression(model, con.func, bvref, method)
con_func0 = JuMP.value(v -> 0.0, con.func)
con_func0 = _evaluate_at_zero(con.func)
if isinf(con_func0)
error("Operator `$(con.func.head)` is not defined at 0, causing the perspective function on the Hull reformulation to fail.")
end
Expand Down
22 changes: 22 additions & 0 deletions test/constraints/hull.jl
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,27 @@ function test_vector_quadratic_hull_1sided(moiset)
@test ref[1].set == moiset(2)
end
#less than, greater than, equalto
function test_evaluate_at_zero()
model = GDPModel()
@variable(model, 1 <= x <= 10)
@variable(model, 1 <= y <= 10)
@test DP._evaluate_at_zero(3) === 3.0
@test DP._evaluate_at_zero(x) === 0.0
@test DP._evaluate_at_zero(2x + 1.5) === 1.5
@test DP._evaluate_at_zero(x * y + 2x + 1.5) === 1.5
@test DP._evaluate_at_zero(exp(x) - 1) === 0.0
@test DP._evaluate_at_zero(log(x + 1) * (y + 2)) === 0.0
@test DP._evaluate_at_zero((x - 1)^2 / (y + 4)) === 0.25
branch = NonlinearExpr(:ifelse,
Any[NonlinearExpr(:>=, Any[x, 1]), x, y + 3])
@test DP._evaluate_at_zero(branch) === 3.0
@test DP._evaluate_at_zero(log(x)) == -Inf
@operator(model, op_f, 1, v -> v^2 + 7)
@test DP._evaluate_at_zero(op_f(x)) === 7.0
@test_throws ErrorException DP._evaluate_at_zero(
NonlinearExpr(:op_missing, Any[x]))
end

function test_scalar_nonlinear_hull_1sided_error()
model = GDPModel()
@variable(model, 10 <= x <= 100)
Expand Down Expand Up @@ -678,6 +699,7 @@ end
test_scalar_quadratic_hull_1sided(s)
test_scalar_nonlinear_hull_1sided(s)
end
test_evaluate_at_zero()
test_scalar_nonlinear_hull_1sided_error()
for s in (MOI.Nonpositives, MOI.Nonnegatives, MOI.Zeros)
test_vector_var_hull_1sided(s)
Expand Down
27 changes: 27 additions & 0 deletions test/extensions/InfiniteDisjunctiveProgramming.jl
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,32 @@ function test_mbm_with_derivatives()
MOI.ALMOST_LOCALLY_SOLVED]
end

# Hull on a non-quadratic nonlinear disjunct constraint of an infinite
# variable: the perspective constant f(0) is evaluated without a
# JuMP.value method for GeneralVariableRef. Optimum x = 0 in disjunct 1.
function test_hull_infinite_nonlinear()
model = InfiniteGDPModel()
@infinite_parameter(model, t ∈ [0, 1], num_supports = 5)
@variable(model, 0 <= x <= 10, Infinite(t))
@variable(model, Y[1:2], InfiniteLogical(t))
@constraint(model, exp(x) - 1 <= 2, Disjunct(Y[1]))
@constraint(model, x >= 5, Disjunct(Y[2]))
@disjunction(model, Y)
@objective(model, Min, ∫(x, t))
@test DP.reformulate_model(model, Hull()) isa Nothing
juniper = JuMP.optimizer_with_attributes(
Juniper.Optimizer,
"nl_solver" => JuMP.optimizer_with_attributes(
Ipopt.Optimizer, "print_level" => 0),
"log_levels" => Symbol[])
set_optimizer(model, juniper)
@test optimize!(model, gdp_method = Hull()) isa Nothing
@test termination_status(model) in
[MOI.OPTIMAL, MOI.LOCALLY_SOLVED]
@test objective_value(model) ≈ 0.0 atol = 1e-4
@test all(value(Y[1]))
end

function test_CuttingPlanes_infinite_simple()
model = InfiniteGDPModel(HiGHS.Optimizer)
set_silent(model)
Expand Down Expand Up @@ -1203,6 +1229,7 @@ end
@testset "Integration" begin
test_infiniteopt_extension()
test_methods()
test_hull_infinite_nonlinear()
end

@testset "Cutting Planes" begin
Expand Down
Loading