Skip to content

Commit 256d0e2

Browse files
authored
Return informative TerminationStatus on failure (#36)
1 parent e70836c commit 256d0e2

12 files changed

+113
-54
lines changed

src/MultiObjectiveAlgorithms.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,10 @@ function _compute_point(
500500
return X, Y
501501
end
502502

503+
function _is_scalar_status_optimal(status::MOI.TerminationStatusCode)
504+
return status == MOI.OPTIMAL || status == MOI.LOCALLY_SOLVED
505+
end
506+
503507
for file in readdir(joinpath(@__DIR__, "algorithms"))
504508
include(joinpath(@__DIR__, "algorithms", file))
505509
end

src/algorithms/EpsilonConstraint.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@ function optimize_multiobjective!(
7676
alg = Hierarchical()
7777
MOI.set.(Ref(alg), ObjectivePriority.(1:2), [1, 0])
7878
status, solution_1 = optimize_multiobjective!(alg, model)
79-
if status != MOI.OPTIMAL
80-
return MOI.OTHER_ERROR, nothing
79+
if !_is_scalar_status_optimal(status)
80+
return status, nothing
8181
end
8282
MOI.set(alg, ObjectivePriority(2), 2)
8383
status, solution_2 = optimize_multiobjective!(alg, model)
84-
if status != MOI.OPTIMAL
85-
return MOI.OTHER_ERROR, nothing
84+
if !_is_scalar_status_optimal(status)
85+
return status, nothing
8686
end
8787
a, b = solution_1[1].y[1], solution_2[1].y[1]
8888
left, right = min(a, b), max(a, b)

src/algorithms/Hierarchical.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,9 @@ function optimize_multiobjective!(algorithm::Hierarchical, model::Optimizer)
100100
new_f = _scalarise(new_vector_f, weights[indices])
101101
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(new_f)}(), new_f)
102102
MOI.optimize!(model.inner)
103-
if MOI.get(model.inner, MOI.TerminationStatus()) != MOI.OPTIMAL
104-
return MOI.OTHER_ERROR, nothing
103+
status = MOI.get(model.inner, MOI.TerminationStatus())
104+
if !_is_scalar_status_optimal(status)
105+
return status, nothing
105106
end
106107
if round == length(objective_subsets)
107108
break

src/algorithms/KirlikSayin.jl

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ function optimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer)
103103
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f_i)}(), f_i)
104104
MOI.set(model.inner, MOI.ObjectiveSense(), sense)
105105
MOI.optimize!(model.inner)
106-
if MOI.get(model.inner, MOI.TerminationStatus()) != MOI.OPTIMAL
107-
return MOI.OTHER_ERROR, nothing
106+
status = MOI.get(model.inner, MOI.TerminationStatus())
107+
if !_is_scalar_status_optimal(status)
108+
return status, nothing
108109
end
109110
_, Y = _compute_point(model, variables, f_i)
110111
yI[i] = Y
@@ -114,8 +115,13 @@ function optimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer)
114115
sense == MOI.MIN_SENSE ? MOI.MAX_SENSE : MOI.MIN_SENSE,
115116
)
116117
MOI.optimize!(model.inner)
117-
if MOI.get(model.inner, MOI.TerminationStatus()) != MOI.OPTIMAL
118-
return MOI.OTHER_ERROR, nothing
118+
status = MOI.get(model.inner, MOI.TerminationStatus())
119+
if !_is_scalar_status_optimal(status)
120+
@warn(
121+
"Unable to solve problem using `KirlikSayin()` because " *
122+
"objective $i does not have a finite domain.",
123+
)
124+
return status, nothing
119125
end
120126
_, Y = _compute_point(model, variables, f_i)
121127
yN[i] = Y

src/algorithms/Lexicographic.jl

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ function optimize_multiobjective!(algorithm::Lexicographic, model::Optimizer)
4949
solutions = SolutionPoint[]
5050
for sequence in Combinatorics.permutations(sequence)
5151
status, solution = _solve_in_sequence(algorithm, model, sequence)
52-
if solution !== nothing
53-
push!(solutions, solution[1])
52+
if !_is_scalar_status_optimal(status)
53+
return status, nothing
5454
end
55+
push!(solutions, solution[1])
5556
end
5657
sense = MOI.get(model.inner, MOI.ObjectiveSense())
5758
return MOI.OPTIMAL, filter_nondominated(sense, solutions)
@@ -69,8 +70,9 @@ function _solve_in_sequence(
6970
f = scalars[i]
7071
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f)}(), f)
7172
MOI.optimize!(model.inner)
72-
if MOI.get(model.inner, MOI.TerminationStatus()) != MOI.OPTIMAL
73-
return MOI.OTHER_ERROR, nothing
73+
status = MOI.get(model.inner, MOI.TerminationStatus())
74+
if !_is_scalar_status_optimal(status)
75+
return status, nothing
7476
end
7577
X, Y = _compute_point(model, variables, f)
7678
rtol = MOI.get(algorithm, ObjectiveRelativeTolerance(i))

src/algorithms/NISE.jl

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ function _solve_weighted_sum(model::Optimizer, ::NISE, weights::Vector{Float64})
4242
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f)}(), f)
4343
MOI.optimize!(model.inner)
4444
status = MOI.get(model.inner, MOI.TerminationStatus())
45-
if status != MOI.OPTIMAL
45+
status = MOI.get(model.inner, MOI.TerminationStatus())
46+
if !_is_scalar_status_optimal(status)
4647
return status, nothing
4748
end
4849
variables = MOI.get(model.inner, MOI.ListOfVariableIndices())
@@ -56,13 +57,13 @@ function optimize_multiobjective!(algorithm::NISE, model::Optimizer)
5657
end
5758
if MOI.output_dimension(model.f) == 1
5859
status, solution = _solve_weighted_sum(model, algorithm, [1.0])
59-
return MOI.OPTIMAL, [solution]
60+
return status, [solution]
6061
end
6162
solutions = Dict{Float64,SolutionPoint}()
6263
for w in (0.0, 1.0)
6364
status, solution = _solve_weighted_sum(model, algorithm, w)
64-
if status != MOI.OPTIMAL
65-
return MOI.OTHER_ERROR, nothing
65+
if !_is_scalar_status_optimal(status)
66+
return status, nothing
6667
end
6768
solutions[w] = solution
6869
end
@@ -76,9 +77,9 @@ function optimize_multiobjective!(algorithm::NISE, model::Optimizer)
7677
y_d = solutions[a].y .- solutions[b].y
7778
w = y_d[2] / (y_d[2] - y_d[1])
7879
status, solution = _solve_weighted_sum(model, algorithm, w)
79-
if status != MOI.OPTIMAL
80+
if !_is_scalar_status_optimal(status)
8081
# Exit the solve with some error.
81-
return MOI.OTHER_ERROR, nothing
82+
return status, nothing
8283
elseif solution solutions[a] || solution solutions[b]
8384
# We have found an existing solution. We're free to prune (a, b)
8485
# from the search space.

test/algorithms/EpsilonConstraint.jl

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ function test_infeasible()
241241
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
242242
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
243243
MOI.optimize!(model)
244-
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OTHER_ERROR
244+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
245245
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
246246
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
247247
return
@@ -257,7 +257,24 @@ function test_unbounded()
257257
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
258258
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
259259
MOI.optimize!(model)
260-
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OTHER_ERROR
260+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE
261+
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
262+
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
263+
return
264+
end
265+
266+
function test_unbounded_second()
267+
model = MOA.Optimizer(HiGHS.Optimizer)
268+
MOI.set(model, MOA.Algorithm(), MOA.EpsilonConstraint())
269+
MOI.set(model, MOI.Silent(), true)
270+
x = MOI.add_variables(model, 2)
271+
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
272+
MOI.add_constraint(model, x[1], MOI.LessThan(1.0))
273+
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
274+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
275+
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
276+
MOI.optimize!(model)
277+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE
261278
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
262279
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
263280
return

test/algorithms/Hierarchical.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function test_infeasible()
8585
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
8686
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
8787
MOI.optimize!(model)
88-
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OTHER_ERROR
88+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
8989
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
9090
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
9191
return
@@ -101,7 +101,7 @@ function test_unbounded()
101101
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
102102
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
103103
MOI.optimize!(model)
104-
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OTHER_ERROR
104+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE
105105
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
106106
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
107107
return

test/algorithms/KirlikSayin.jl

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ function test_infeasible()
509509
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
510510
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
511511
MOI.optimize!(model)
512-
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OTHER_ERROR
512+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
513513
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
514514
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
515515
return
@@ -525,7 +525,23 @@ function test_unbounded()
525525
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
526526
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
527527
MOI.optimize!(model)
528-
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OTHER_ERROR
528+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE
529+
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
530+
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
531+
return
532+
end
533+
534+
function test_no_bounding_box()
535+
model = MOA.Optimizer(HiGHS.Optimizer)
536+
MOI.set(model, MOA.Algorithm(), MOA.KirlikSayin())
537+
MOI.set(model, MOI.Silent(), true)
538+
x = MOI.add_variables(model, 2)
539+
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
540+
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
541+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
542+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
543+
@test_logs (:warn,) MOI.optimize!(model)
544+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE
529545
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
530546
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
531547
return

test/algorithms/Lexicographic.jl

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -112,34 +112,46 @@ function test_knapsack_default()
112112
end
113113

114114
function test_infeasible()
115-
model = MOA.Optimizer(HiGHS.Optimizer)
116-
MOI.set(model, MOA.Algorithm(), MOA.Lexicographic())
117-
MOI.set(model, MOI.Silent(), true)
118-
x = MOI.add_variables(model, 2)
119-
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
120-
MOI.add_constraint(model, 1.0 * x[1] + 1.0 * x[2], MOI.LessThan(-1.0))
121-
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
122-
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
123-
MOI.optimize!(model)
124-
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OTHER_ERROR
125-
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
126-
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
115+
for flag in (true, false)
116+
model = MOA.Optimizer(HiGHS.Optimizer)
117+
MOI.set(
118+
model,
119+
MOA.Algorithm(),
120+
MOA.Lexicographic(; all_permutations = flag),
121+
)
122+
MOI.set(model, MOI.Silent(), true)
123+
x = MOI.add_variables(model, 2)
124+
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
125+
MOI.add_constraint(model, 1.0 * x[1] + 1.0 * x[2], MOI.LessThan(-1.0))
126+
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
127+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
128+
MOI.optimize!(model)
129+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
130+
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
131+
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
132+
end
127133
return
128134
end
129135

130136
function test_unbounded()
131-
model = MOA.Optimizer(HiGHS.Optimizer)
132-
MOI.set(model, MOA.Algorithm(), MOA.Lexicographic())
133-
MOI.set(model, MOI.Silent(), true)
134-
x = MOI.add_variables(model, 2)
135-
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
136-
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
137-
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
138-
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
139-
MOI.optimize!(model)
140-
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OTHER_ERROR
141-
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
142-
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
137+
for flag in (true, false)
138+
model = MOA.Optimizer(HiGHS.Optimizer)
139+
MOI.set(
140+
model,
141+
MOA.Algorithm(),
142+
MOA.Lexicographic(; all_permutations = flag),
143+
)
144+
MOI.set(model, MOI.Silent(), true)
145+
x = MOI.add_variables(model, 2)
146+
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
147+
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
148+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
149+
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
150+
MOI.optimize!(model)
151+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE
152+
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
153+
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
154+
end
143155
return
144156
end
145157

0 commit comments

Comments
 (0)