Standard-Use Example 2
This example is also provided here as a Jupyter Notebook.
Using EAGO's Basic Optimizer With User-Defined Subroutines
In this section, we construct an optimizer that uses EAGO's basic NLP solution routine with user-defined lower and upper-bounding problems. The Optimizer
structure supplies a number of parameters and stored structures that advanced users may find useful for constructing specialized solution routines.
In this example, we'll forgo extensive integration into the Optimizer
and simply replace the lower and upper-bounding problems to construct a B&B routine that solves the following problem to global optimality using bounds obtained from interval arithmetic:
\[\begin{aligned} & \min_{\mathbf x \in X} \; \; \sin(x_{1}) x_{2}^{2} - \cos(x_{3}) / x_{4} \\ & X = [-10, 10] \times [-1, 1] \times [-10, 10] \times [2, 20]. \end{aligned}\]
We begin by importing EAGO, IntervalArithmetic [1], and JuMP [2].
using EAGO, IntervalArithmetic, JuMP
We now define the IntervalExt
struct as a subtype of the ExtensionType
.
struct IntervalExt <: EAGO.ExtensionType end
Define a Custom Lower-Bounding Problem
A valid lower bound is obtained from the lower bound of the natural interval extension using the IntervalArithmetic.jl [1] package. The LowerProblem
is dispatched using the new IntervalExt
structure and the GlobalOptimizer
structure, computes the bound using interval arithmetic, and stores the results to the appropriate field of the GlobalOptimizer
. Note that the problem is unconstrained on the domain so we can assume it is always feasible. Further, since the interval bound is constrained along the entire domain associated with a node, no additional cuts will be beneficial and thus we've disabled them using the _cut_add_flag
field.
import EAGO: lower_problem!
function lower_problem!(t::IntervalExt, x::EAGO.GlobalOptimizer)
# Retrieve bounds at current node
n = x._current_node
lower = n.lower_variable_bounds
upper = n.upper_variable_bounds
# Define X for the node and compute the interval extension
x_value = Interval.(lower, upper)
F = sin(x_value[1])*x_value[2]^2 - cos(x_value[3])/x_value[4]
x._lower_objective_value = F.lo
x._lower_solution = IntervalArithmetic.mid.(x_value)
x._lower_feasibility = true
x._cut_add_flag = false
return
end
Define a Custom Upper-Bounding Problem
Since the problem is unconstrained, any feasible point represents a valid upper bound. Thus, if we arbitrarily evaluate the function at the midpoint, we obtain a valid upper bound. This function constructs an upper bound in this manner then stores the results to the appropriate field of the GlobalOptimizer
.
import EAGO.upper_problem!
function EAGO.upper_problem!(t::IntervalExt, x::EAGO.GlobalOptimizer)
# Retrieve bounds at current node
n = x._current_node
lower = n.lower_variable_bounds
upper = n.upper_variable_bounds
# Compute midpoint value and evaluate at that point
x_value = 0.5*(upper + lower)
f_val = sin(x_value[1])*x_value[2]^2-cos(x_value[3])/x_value[4]
x._upper_objective_value = f_val
x._upper_solution = x_value
x._upper_feasibility = true
return
end
Disable Unnecessary Routines
It is entirely possible to disable domain reduction by manipulating keyword arguments supplied to the optimizer. However, for simplicity's sake we'll simply overload the default preprocessing and postprocessing methods and indicate that there are no conditions under which EAGO should cut the node.
import EAGO: preprocess!, postprocess!, cut_condition
function EAGO.preprocess!(t::IntervalExt, x::EAGO.GlobalOptimizer)
x._preprocess_feasibility = true
return
end
function EAGO.postprocess!(t::IntervalExt, x::EAGO.GlobalOptimizer)
x._postprocess_feasibility = true
return
end
EAGO.cut_condition(t::IntervalExt, x::EAGO.GlobalOptimizer) = false
Construct the JuMP Model and Optimize
We now add our optimizer to a JuMP [2] model, provide variable bounds, and optimize.
# Create a factory that specifies the interval extension in EAGO's SubSolver
factory = () -> EAGO.Optimizer(SubSolvers(; t = IntervalExt()))
# Create a JuMP model using the factory, and with the absolute tolerance set by keyword argument
m = Model(optimizer_with_attributes(factory,
"absolute_tolerance" => 0.001
))
# Add variables, bounds, and the objective function
x_L = [-10.0, -1.0, -10.0, 2.0]
x_U = [10.0, 1.0, 10.0, 20.0]
@variable(m, x_L[i] <= x[i=1:4] <= x_U[i])
@NLobjective(m, Min, sin(x[1])*x[2]^2 - cos(x[3])/x[4])
# Perform the optimization
optimize!(m)
Retrieve Results
The objective value, solution, termination status, and primal status can then be accessed via the standard JuMP interface.
fval = JuMP.objective_value(m)
xsol = JuMP.value.(x)
status_term = JuMP.termination_status(m)
status_prim = JuMP.primal_status(m)
println("EAGO terminated with a status of $status_term and a result code of $status_prim")
println("The optimal value is: $(round(fval, digits=3)), the solution found is $(round.(xsol, digits=4)).")
Advice for More Advanced Constructions
The default lower_problem!
and upper_problem!
should be used as templates for error handling and retrieving information from MOI models.
Essentially all of EAGO's subroutines stored to a field in the Optimizer
structure can be reset as user-defined functions.
References
- IntervalArithmetic.jl [Computer software] (2019). Retrieved from https://github.com/JuliaIntervals/IntervalArithmetic.jl
- Iain Dunning and Joey Huchette and Miles Lubin. JuMP: A Modeling Language for Mathematical Optimization, SIAM Review, SIAM 59 (2017), pp. 295-320.