|
| 1 | +import NLPModels |
| 2 | +import SolverCore |
| 3 | + |
| 4 | +export IntervalOptimiser |
| 5 | + |
| 6 | +""" |
| 7 | + IntervalOptimiser <: SolverCore.AbstractOptimizationSolver |
| 8 | +
|
| 9 | +A global optimization solver based on the Moore-Skelboe interval branch-and-bound algorithm. |
| 10 | +Can be used with NLPModelsJuMP to solve JuMP models. |
| 11 | +
|
| 12 | +# Usage with JuMP and NLPModelsJuMP |
| 13 | +
|
| 14 | +```julia |
| 15 | +using JuMP, NLPModelsJuMP, IntervalOptimisation |
| 16 | +
|
| 17 | +model = Model(NLPModelsJuMP.Optimizer) |
| 18 | +set_attribute(model, "solver", IntervalOptimiser) |
| 19 | +set_attribute(model, "tol", 1e-5) |
| 20 | +
|
| 21 | +@variable(model, -10 <= x <= 10) |
| 22 | +@objective(model, Min, (x - 3)^2 + 1) |
| 23 | +optimize!(model) |
| 24 | +``` |
| 25 | +
|
| 26 | +The solver stores the interval enclosure of the global minimum and the list of minimizer |
| 27 | +boxes in the `solver_specific` fields `:global_min_interval` and `:minimizers`. |
| 28 | +""" |
| 29 | +mutable struct IntervalOptimiser <: SolverCore.AbstractOptimizationSolver |
| 30 | +end |
| 31 | + |
| 32 | +function IntervalOptimiser(nlp::NLPModels.AbstractNLPModel; kwargs...) |
| 33 | + return IntervalOptimiser() |
| 34 | +end |
| 35 | + |
| 36 | +""" |
| 37 | + _eval_moi_expr(expr, x) |
| 38 | +
|
| 39 | +Recursively evaluate a Julia `Expr` returned by `MOI.objective_expr` using the |
| 40 | +variable values in `x`. Works with any numeric type including intervals. |
| 41 | +""" |
| 42 | +function _eval_moi_expr(expr::Expr, x) |
| 43 | + if expr.head == :ref && expr.args[1] == :x |
| 44 | + # x[MOI.VariableIndex(i)] → x[i] |
| 45 | + vi = expr.args[2] |
| 46 | + return x[vi.value] |
| 47 | + elseif expr.head == :call |
| 48 | + op = expr.args[1] |
| 49 | + evaluated_args = [_eval_moi_expr(a, x) for a in @view(expr.args[2:end])] |
| 50 | + if op isa Symbol |
| 51 | + return _call_op(Val(op), evaluated_args) |
| 52 | + else |
| 53 | + return op(evaluated_args...) |
| 54 | + end |
| 55 | + else |
| 56 | + error("Unsupported expression head: $(expr.head)") |
| 57 | + end |
| 58 | +end |
| 59 | + |
| 60 | +_eval_moi_expr(v::Number, _) = v |
| 61 | + |
| 62 | +# Arithmetic |
| 63 | +_call_op(::Val{:+}, args) = length(args) == 1 ? +args[1] : +(args...) |
| 64 | +_call_op(::Val{:-}, args) = length(args) == 1 ? -args[1] : args[1] - args[2] |
| 65 | +_call_op(::Val{:*}, args) = *(args...) |
| 66 | +_call_op(::Val{:/}, args) = args[1] / args[2] |
| 67 | +_call_op(::Val{:^}, args) = args[1] ^ args[2] |
| 68 | +# Common math functions |
| 69 | +_call_op(::Val{:log}, args) = log(args[1]) |
| 70 | +_call_op(::Val{:log2}, args) = log2(args[1]) |
| 71 | +_call_op(::Val{:log10}, args) = log10(args[1]) |
| 72 | +_call_op(::Val{:exp}, args) = exp(args[1]) |
| 73 | +_call_op(::Val{:sqrt}, args) = sqrt(args[1]) |
| 74 | +_call_op(::Val{:abs}, args) = abs(args[1]) |
| 75 | +_call_op(::Val{:sin}, args) = sin(args[1]) |
| 76 | +_call_op(::Val{:cos}, args) = cos(args[1]) |
| 77 | +_call_op(::Val{:tan}, args) = tan(args[1]) |
| 78 | +_call_op(::Val{:asin}, args) = asin(args[1]) |
| 79 | +_call_op(::Val{:acos}, args) = acos(args[1]) |
| 80 | +_call_op(::Val{:atan}, args) = atan(args...) |
| 81 | +_call_op(::Val{:min}, args) = min(args...) |
| 82 | +_call_op(::Val{:max}, args) = max(args...) |
| 83 | +# Fallback: try to find the function in Base/Main |
| 84 | +_call_op(::Val{op}, args) where {op} = getfield(Base, op)(args...) |
| 85 | + |
| 86 | +function SolverCore.solve!( |
| 87 | + solver::IntervalOptimiser, |
| 88 | + nlp::NLPModels.AbstractNLPModel, |
| 89 | + stats::SolverCore.GenericExecutionStats; |
| 90 | + tol::Real = 1e-3, |
| 91 | + structure = HeapedVector, |
| 92 | + kwargs..., |
| 93 | +) |
| 94 | + start_time = time() |
| 95 | + |
| 96 | + # Only box-constrained or unconstrained problems are supported |
| 97 | + if nlp.meta.ncon > 0 |
| 98 | + SolverCore.set_status!(stats, :exception) |
| 99 | + SolverCore.set_time!(stats, time() - start_time) |
| 100 | + error("IntervalOptimiser only supports unconstrained or box-constrained problems") |
| 101 | + end |
| 102 | + |
| 103 | + nvar = nlp.meta.nvar |
| 104 | + lvar = nlp.meta.lvar |
| 105 | + uvar = nlp.meta.uvar |
| 106 | + |
| 107 | + # Check that all variables have finite bounds |
| 108 | + for i in 1:nvar |
| 109 | + if !isfinite(lvar[i]) || !isfinite(uvar[i]) |
| 110 | + SolverCore.set_status!(stats, :exception) |
| 111 | + SolverCore.set_time!(stats, time() - start_time) |
| 112 | + error("IntervalOptimiser requires finite bounds on all variables") |
| 113 | + end |
| 114 | + end |
| 115 | + |
| 116 | + # Build the interval search box |
| 117 | + X = [interval(lvar[i], uvar[i]) for i in 1:nvar] |
| 118 | + |
| 119 | + # Create the objective function for interval evaluation |
| 120 | + f = _make_objective(nlp, nvar) |
| 121 | + |
| 122 | + # Solve |
| 123 | + dom = nvar == 1 ? X[1] : X |
| 124 | + if nlp.meta.minimize |
| 125 | + global_opt, optimizer_boxes = minimise(f, dom; structure = structure, tol = tol) |
| 126 | + else |
| 127 | + global_opt, optimizer_boxes = maximise(f, dom; structure = structure, tol = tol) |
| 128 | + end |
| 129 | + |
| 130 | + # Extract solution (midpoint of the first minimizer box) |
| 131 | + if nvar == 1 |
| 132 | + solution = [mid(optimizer_boxes[1])] |
| 133 | + else |
| 134 | + solution = mid.(optimizer_boxes[1]) |
| 135 | + end |
| 136 | + objective = mid(global_opt) |
| 137 | + |
| 138 | + SolverCore.set_status!(stats, :first_order) |
| 139 | + SolverCore.set_solution!(stats, solution) |
| 140 | + SolverCore.set_objective!(stats, objective) |
| 141 | + SolverCore.set_iter!(stats, length(optimizer_boxes)) |
| 142 | + SolverCore.set_time!(stats, time() - start_time) |
| 143 | + SolverCore.set_solver_specific!(stats, :global_min_interval, global_opt) |
| 144 | + SolverCore.set_solver_specific!(stats, :minimizers, optimizer_boxes) |
| 145 | + |
| 146 | + return stats |
| 147 | +end |
| 148 | + |
| 149 | +# Default: use NLPModels.obj directly (works for linear/quadratic objectives) |
| 150 | +function _make_objective(nlp::NLPModels.AbstractNLPModel, nvar) |
| 151 | + if nvar == 1 |
| 152 | + return x -> NLPModels.obj(nlp, [x]) |
| 153 | + else |
| 154 | + return x -> NLPModels.obj(nlp, x) |
| 155 | + end |
| 156 | +end |
0 commit comments