From c3b5602dc9c557c306eea55767616e7ae479e5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Kof?= Date: Sat, 28 Jun 2025 22:21:00 +0300 Subject: [PATCH 01/12] Add Sandwiching --- Project.toml | 9 +- ext/PolyhedraExt.jl | 12 +++ src/algorithms/Sandwiching.jl | 145 +++++++++++++++++++++++++++++++++ test/algorithms/Sandwiching.jl | 84 +++++++++++++++++++ 4 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 ext/PolyhedraExt.jl create mode 100644 src/algorithms/Sandwiching.jl create mode 100644 test/algorithms/Sandwiching.jl diff --git a/Project.toml b/Project.toml index 95437f5..04b73b1 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,12 @@ version = "1.4.3" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +[weakdeps] +Polyhedra = "67491407-f73d-577b-9b50-8179a7c68029" + +[extensions] +PolyhedraExt = "Polyhedra" + [compat] Combinatorics = "1" HiGHS = "1" @@ -21,6 +27,7 @@ HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Polyhedra = "67491407-f73d-577b-9b50-8179a7c68029" [targets] -test = ["HiGHS", "Ipopt", "JSON", "Test"] +test = ["HiGHS", "Ipopt", "JSON", "Test", "Polyhedra"] diff --git a/ext/PolyhedraExt.jl b/ext/PolyhedraExt.jl new file mode 100644 index 0000000..4f44420 --- /dev/null +++ b/ext/PolyhedraExt.jl @@ -0,0 +1,12 @@ +module PolyhedraExt + +using MultiObjectiveAlgorithms +using Polyhedra + +function MultiObjectiveAlgorithms._halfspaces(IPS::Vector{Vector{Float64}}) + V = vrep(IPS) + H = halfspaces(doubledescription(V)) + return [(-H_i.a, -H_i.β) for H_i in H] +end + +end \ No newline at end of file diff --git a/src/algorithms/Sandwiching.jl b/src/algorithms/Sandwiching.jl new file mode 100644 index 0000000..5ce5a65 --- /dev/null +++ b/src/algorithms/Sandwiching.jl @@ -0,0 +1,145 @@ +function _halfspaces(IPS::Vector{Vector{Float64}}) + error("MOA.Sandwiching requires Polyhedra.jl to be loaded") +end + +mutable struct Sandwiching <: AbstractAlgorithm + precision::Float64 +end + +tol = 1e-3 + +function _compute_anchors(model::Optimizer) + anchors = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}() + n = MOI.output_dimension(model.f) + scalars = MOI.Utilities.scalarize(model.f) + variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) + yI, yUB = zeros(n), zeros(n) + for (i, f_i) in enumerate(scalars) + MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f_i)}(), f_i) # ρ * sum(f_j for (j, f_j) in enumerate(model.f) if j != i) + MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.optimize!(model.inner) + # status check + X, Y = _compute_point(model, variables, model.f) + model.ideal_point[i] = Y[i] + yI[i] = Y[i] + anchors[Y] = X + MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) + MOI.optimize!(model.inner) + # status check + _, Y = _compute_point(model, variables, f_i) + yUB[i] = Y + end + MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) + return yI, yUB, anchors +end + +@enum DistanceMeasure SUB CUR SOL + +function _distance(w̄, b̄, OPS, model) + n = MOI.output_dimension(model.f) + optimizer = typeof(model.inner.optimizer) + δ_optimizer = optimizer() + MOI.set(δ_optimizer, MOI.Silent(), true) + x = MOI.add_variables(δ_optimizer, n) + for (w, b) in OPS + MOI.add_constraint( + δ_optimizer, + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w, x), 0.0), + MOI.GreaterThan(b), + ) + end + MOI.set( + δ_optimizer, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w̄, x), 0.0), + ) + MOI.set(δ_optimizer, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.optimize!(δ_optimizer) + δ = b̄ - MOI.get(δ_optimizer, MOI.ObjectiveValue()) + return δ +end + +function _select_next_halfspace(H, OPS, model) + distances = [_distance(w, b, OPS, model) for (w, b) in H] + @info "Distances: $(Dict(zip(H, distances)))" + index = argmax(distances) + w, b = H[index] + return distances[index], w, b +end + +function optimize_multiobjective!(algorithm::Sandwiching, model::Optimizer) + @assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE + ε = algorithm.precision + start_time = time() + solutions = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}() + variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) + n = MOI.output_dimension(model.f) + scalars = MOI.Utilities.scalarize(model.f) + yI, yUB, anchors = _compute_anchors(model) + merge!(solutions, anchors) + @info "yI: $(yI)" + @info "yUB: $(yUB)" + IPS = [yUB, keys(anchors)...] + OPS = Tuple{Vector{Float64}, Float64}[] + for i in 1:n + e_i = Float64.(1:n .== i) + push!(OPS, (e_i, yI[i])) # e_i' * y >= yI_i + push!(OPS, (-e_i, -yUB[i])) # -e_i' * y >= -yUB_i ⟹ e_i' * y <= yUB_i + end + @info "IPS: $(IPS)" + @info "OPS: $(OPS)" + u = MOI.add_variables(model.inner, n) + u_constraints = [ # u_i >= 0 for all i = 1:n + MOI.add_constraint( + model.inner, + u_i, + MOI.GreaterThan{Float64}(0), + ) + for u_i in u + ] + f_constraints = [ # f_i + u_i <= yUB_i for all i = 1:n + MOI.Utilities.normalize_and_add_constraint( + model.inner, + scalars[i] + u[i], + MOI.LessThan(yUB[i]), + ) for i in 1:n + ] + H = _halfspaces(IPS) + + count = 0 + + while !isempty(H) + count += 1 + @info "-- Iteration #$(count) --" + @info "HalfSpaces: $(H)" + δ, w, b = _select_next_halfspace(H, OPS, model) + @info "Selected halfspace: w: $(w), b: $(b)" + @info "δ: $(δ)" + if δ - tol > ε # added some convergence tolerance + # would not terminate when precision is set to 0 + new_f = sum(w[i] * (scalars[i] + u[i]) for i in 1:n) # w' * (f(x) + u) + MOI.set(model.inner, MOI.ObjectiveFunction{typeof(new_f)}(), new_f) + MOI.optimize!(model.inner) + β̄ = MOI.get(model.inner, MOI.ObjectiveValue()) + @info "β̄: $(β̄)" + X, Y = _compute_point(model, variables, model.f) + @info "Y: $(Y)" + solutions[Y] = X + push!(OPS, (w, β̄)) + @info "Added halfspace w: $(w), b: $(β̄) to OPS" + IPS = push!(IPS, Y) + else + break + end + @info "IPS: $(IPS)" + @info "OPS: $(OPS)" + H = _halfspaces(IPS) + if count == 10 + break + end + end + MOI.delete.(model.inner, f_constraints) + MOI.delete.(model.inner, u_constraints) + MOI.delete.(model.inner, u) + return MOI.OPTIMAL, [SolutionPoint(X, Y) for (Y, X) in solutions] +end \ No newline at end of file diff --git a/test/algorithms/Sandwiching.jl b/test/algorithms/Sandwiching.jl new file mode 100644 index 0000000..88a9a1a --- /dev/null +++ b/test/algorithms/Sandwiching.jl @@ -0,0 +1,84 @@ +module TestSandwiching + +using Test + +import HiGHS +using Polyhedra +import MultiObjectiveAlgorithms as MOA +import MultiObjectiveAlgorithms: MOI + +function run_tests() + for name in names(@__MODULE__; all = true) + if startswith("$name", "test_") + @testset "$name" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + + +# From International Doctoral School Algorithmic Decision Theory: MCDA and MOO +# Lecture 2: Multiobjective Linear Programming +# Matthias Ehrgott +# Department of Engineering Science, The University of Auckland, New Zealand +# Laboratoire d’Informatique de Nantes Atlantique, CNRS, Universit´e de Nantes, France +function test_molp() + C = Float64[ + 3 1 + -1 -2 + ] + p = size(C, 1) + A = Float64[ + 0 1 + 3 -1 + ] + m, n = size(A) + b = Float64[3, 6] + model = MOI.instantiate(; with_bridge_type = Float64) do + return MOA.Optimizer(HiGHS.Optimizer) + end + MOI.set(model, MOA.Algorithm(), MOA.Sandwiching(0.0)) + MOI.set(model, MOI.Silent(), true) + x = MOI.add_variables(model, n) + MOI.add_constraint.(model, x, MOI.GreaterThan(0.0)) + for i in 1:m + MOI.add_constraint( + model, + MOI.ScalarAffineFunction( + [MOI.ScalarAffineTerm(A[i, j], x[j]) for j in 1:n], + 0.0, + ), + MOI.LessThan(b[i]), + ) + end + f = MOI.VectorAffineFunction( + [ + MOI.VectorAffineTerm(i, MOI.ScalarAffineTerm(C[i, j], x[j])) + for i in 1:p for j in 1:n + ], + zeros(p), + ) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + MOI.optimize!(model) + N = MOI.get(model, MOI.ResultCount()) + solutions = reverse([MOI.get(model, MOI.VariablePrimal(i), x) => MOI.get(model, MOI.ObjectiveValue(i)) for i in 1:N]) + results = reverse([ + [0., 0.] => [0., 0.], + [0., 3.] => [3., -6.], + [3., 3.] => [12., -9.], + ]) + @test length(solutions) == length(results) + for (sol, res) in zip(solutions, results) + x_sol, y_sol = sol + x_res, y_res = res + @test ≈(x_sol, x_res; atol = 1e-6) + @test ≈(y_sol, y_res; atol = 1e-6) + end +end + +end + +TestSandwiching.run_tests() \ No newline at end of file From b87d69045de916f043a36f9897a9a85a9465d7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Kof?= Date: Sun, 29 Jun 2025 09:04:49 +0300 Subject: [PATCH 02/12] Update and fix format --- ext/PolyhedraExt.jl | 2 +- src/algorithms/Sandwiching.jl | 18 +++++++----------- test/algorithms/Sandwiching.jl | 18 ++++++++++-------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/ext/PolyhedraExt.jl b/ext/PolyhedraExt.jl index 4f44420..c3fa357 100644 --- a/ext/PolyhedraExt.jl +++ b/ext/PolyhedraExt.jl @@ -9,4 +9,4 @@ function MultiObjectiveAlgorithms._halfspaces(IPS::Vector{Vector{Float64}}) return [(-H_i.a, -H_i.β) for H_i in H] end -end \ No newline at end of file +end diff --git a/src/algorithms/Sandwiching.jl b/src/algorithms/Sandwiching.jl index 5ce5a65..0484102 100644 --- a/src/algorithms/Sandwiching.jl +++ b/src/algorithms/Sandwiching.jl @@ -1,5 +1,5 @@ -function _halfspaces(IPS::Vector{Vector{Float64}}) - error("MOA.Sandwiching requires Polyhedra.jl to be loaded") +function _halfspaces(IPS) + return error("MOA.Sandwiching requires Polyhedra.jl to be loaded") end mutable struct Sandwiching <: AbstractAlgorithm @@ -80,7 +80,7 @@ function optimize_multiobjective!(algorithm::Sandwiching, model::Optimizer) @info "yI: $(yI)" @info "yUB: $(yUB)" IPS = [yUB, keys(anchors)...] - OPS = Tuple{Vector{Float64}, Float64}[] + OPS = Tuple{Vector{Float64},Float64}[] for i in 1:n e_i = Float64.(1:n .== i) push!(OPS, (e_i, yI[i])) # e_i' * y >= yI_i @@ -90,17 +90,13 @@ function optimize_multiobjective!(algorithm::Sandwiching, model::Optimizer) @info "OPS: $(OPS)" u = MOI.add_variables(model.inner, n) u_constraints = [ # u_i >= 0 for all i = 1:n - MOI.add_constraint( - model.inner, - u_i, - MOI.GreaterThan{Float64}(0), - ) + MOI.add_constraint(model.inner, u_i, MOI.GreaterThan{Float64}(0)) for u_i in u ] f_constraints = [ # f_i + u_i <= yUB_i for all i = 1:n MOI.Utilities.normalize_and_add_constraint( model.inner, - scalars[i] + u[i], + scalars[i] + u[i], MOI.LessThan(yUB[i]), ) for i in 1:n ] @@ -116,7 +112,7 @@ function optimize_multiobjective!(algorithm::Sandwiching, model::Optimizer) @info "Selected halfspace: w: $(w), b: $(b)" @info "δ: $(δ)" if δ - tol > ε # added some convergence tolerance - # would not terminate when precision is set to 0 + # would not terminate when precision is set to 0 new_f = sum(w[i] * (scalars[i] + u[i]) for i in 1:n) # w' * (f(x) + u) MOI.set(model.inner, MOI.ObjectiveFunction{typeof(new_f)}(), new_f) MOI.optimize!(model.inner) @@ -142,4 +138,4 @@ function optimize_multiobjective!(algorithm::Sandwiching, model::Optimizer) MOI.delete.(model.inner, u_constraints) MOI.delete.(model.inner, u) return MOI.OPTIMAL, [SolutionPoint(X, Y) for (Y, X) in solutions] -end \ No newline at end of file +end diff --git a/test/algorithms/Sandwiching.jl b/test/algorithms/Sandwiching.jl index 88a9a1a..76a87ca 100644 --- a/test/algorithms/Sandwiching.jl +++ b/test/algorithms/Sandwiching.jl @@ -18,7 +18,6 @@ function run_tests() return end - # From International Doctoral School Algorithmic Decision Theory: MCDA and MOO # Lecture 2: Multiobjective Linear Programming # Matthias Ehrgott @@ -55,8 +54,8 @@ function test_molp() end f = MOI.VectorAffineFunction( [ - MOI.VectorAffineTerm(i, MOI.ScalarAffineTerm(C[i, j], x[j])) - for i in 1:p for j in 1:n + MOI.VectorAffineTerm(i, MOI.ScalarAffineTerm(C[i, j], x[j])) for + i in 1:p for j in 1:n ], zeros(p), ) @@ -64,11 +63,14 @@ function test_molp() MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) MOI.optimize!(model) N = MOI.get(model, MOI.ResultCount()) - solutions = reverse([MOI.get(model, MOI.VariablePrimal(i), x) => MOI.get(model, MOI.ObjectiveValue(i)) for i in 1:N]) + solutions = reverse([ + MOI.get(model, MOI.VariablePrimal(i), x) => + MOI.get(model, MOI.ObjectiveValue(i)) for i in 1:N + ]) results = reverse([ - [0., 0.] => [0., 0.], - [0., 3.] => [3., -6.], - [3., 3.] => [12., -9.], + [0.0, 0.0] => [0.0, 0.0], + [0.0, 3.0] => [3.0, -6.0], + [3.0, 3.0] => [12.0, -9.0], ]) @test length(solutions) == length(results) for (sol, res) in zip(solutions, results) @@ -81,4 +83,4 @@ end end -TestSandwiching.run_tests() \ No newline at end of file +TestSandwiching.run_tests() From 12f04e5db2b605637de9c20ead61d59141836212 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 2 Jul 2025 09:21:39 +1200 Subject: [PATCH 03/12] Refactor --- .github/workflows/ci.yml | 4 +- Project.toml | 7 +- ext/MultiObjectiveAlgorithmsPolyhedraExt.jl | 147 +++++++++++++++++++ ext/PolyhedraExt.jl | 12 -- src/algorithms/Sandwiching.jl | 152 +++----------------- test/algorithms/Sandwiching.jl | 24 ++-- 6 files changed, 181 insertions(+), 165 deletions(-) create mode 100644 ext/MultiObjectiveAlgorithmsPolyhedraExt.jl delete mode 100644 ext/PolyhedraExt.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8369946..57ef58a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,10 +26,10 @@ jobs: - version: '1' # The latest point-release (Windows) os: windows-latest arch: x64 - - version: '1.6' # 1.6 LTS (64-bit Linux) + - version: '1.10' # 1.10 LTS (64-bit Linux) os: ubuntu-latest arch: x64 - - version: '1.6' # 1.6 LTS (32-bit Linux) + - version: '1.10' # 1.10 LTS (32-bit Linux) os: ubuntu-latest arch: x86 steps: diff --git a/Project.toml b/Project.toml index 04b73b1..23c221b 100644 --- a/Project.toml +++ b/Project.toml @@ -11,7 +11,7 @@ MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" Polyhedra = "67491407-f73d-577b-9b50-8179a7c68029" [extensions] -PolyhedraExt = "Polyhedra" +MultiObjectiveAlgorithmsPolyhedraExt = "Polyhedra" [compat] Combinatorics = "1" @@ -19,8 +19,9 @@ HiGHS = "1" Ipopt = "1" JSON = "0.21" MathOptInterface = "1.19" -Test = "<0.0.1, 1.6" -julia = "1.6" +Polyhedra = "0.8" +Test = "1" +julia = "1.10" [extras] HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" diff --git a/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl new file mode 100644 index 0000000..e93b443 --- /dev/null +++ b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl @@ -0,0 +1,147 @@ +# Copyright 2019, Oscar Dowson and contributors +# This Source Code Form is subject to the terms of the Mozilla Public License, +# v.2.0. If a copy of the MPL was not distributed with this file, You can +# obtain one at http://mozilla.org/MPL/2.0/. + +module MultiObjectiveAlgorithmsPolyhedraExt + +import MathOptInterface as MOI +import MultiObjectiveAlgorithms as MOA +import Polyhedra + +function _halfspaces(IPS::Vector{Vector{Float64}}) + V = Polyhedra.vrep(IPS) + H = Polyhedra.halfspaces(Polyhedra.doubledescription(V)) + return [(-H_i.a, -H_i.β) for H_i in H] +end + +function _compute_anchors(model::MOA.Optimizer) + anchors = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}() + n = MOI.output_dimension(model.f) + scalars = MOI.Utilities.scalarize(model.f) + variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) + yI, yUB = zeros(n), zeros(n) + for (i, f_i) in enumerate(scalars) + MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f_i)}(), f_i) + MOI.optimize!(model.inner) + # status check + X, Y = MOA._compute_point(model, variables, model.f) + model.ideal_point[i] = Y[i] + yI[i] = Y[i] + anchors[Y] = X + MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) + MOI.optimize!(model.inner) + # status check + _, Y = MOA._compute_point(model, variables, f_i) + yUB[i] = Y + MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) + end + + return yI, yUB, anchors +end + +function _distance(w̄, b̄, OPS, model) + n = MOI.output_dimension(model.f) + optimizer = typeof(model.inner.optimizer) + δ_optimizer = optimizer() + MOI.set(δ_optimizer, MOI.Silent(), true) + x = MOI.add_variables(δ_optimizer, n) + for (w, b) in OPS + MOI.add_constraint( + δ_optimizer, + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w, x), 0.0), + MOI.GreaterThan(b), + ) + end + MOI.set( + δ_optimizer, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w̄, x), 0.0), + ) + MOI.set(δ_optimizer, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.optimize!(δ_optimizer) + return b̄ - MOI.get(δ_optimizer, MOI.ObjectiveValue()) +end + +function _select_next_halfspace(H, OPS, model) + distances = [_distance(w, b, OPS, model) for (w, b) in H] + @info "Distances: $(Dict(zip(H, distances)))" + index = argmax(distances) + w, b = H[index] + return distances[index], w, b +end + +function MOA.minimize_multiobjective!( + algorithm::MOA.Sandwiching, + model::MOA.Optimizer, +) + @assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE + ε = algorithm.precision + start_time = time() + solutions = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}() + variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) + n = MOI.output_dimension(model.f) + scalars = MOI.Utilities.scalarize(model.f) + yI, yUB, anchors = _compute_anchors(model) + merge!(solutions, anchors) + @info "yI: $(yI)" + @info "yUB: $(yUB)" + IPS = [yUB, keys(anchors)...] + OPS = Tuple{Vector{Float64},Float64}[] + for i in 1:n + e_i = Float64.(1:n .== i) + push!(OPS, (e_i, yI[i])) # e_i' * y >= yI_i + push!(OPS, (-e_i, -yUB[i])) # -e_i' * y >= -yUB_i ⟹ e_i' * y <= yUB_i + end + @info "IPS: $(IPS)" + @info "OPS: $(OPS)" + u = MOI.add_variables(model.inner, n) + u_constraints = [ # u_i >= 0 for all i = 1:n + MOI.add_constraint(model.inner, u_i, MOI.GreaterThan{Float64}(0)) + for u_i in u + ] + f_constraints = [ # f_i + u_i <= yUB_i for all i = 1:n + MOI.Utilities.normalize_and_add_constraint( + model.inner, + scalars[i] + u[i], + MOI.LessThan(yUB[i]), + ) for i in 1:n + ] + H = _halfspaces(IPS) + count = 0 + while !isempty(H) + count += 1 + @info "-- Iteration #$(count) --" + @info "HalfSpaces: $(H)" + δ, w, b = _select_next_halfspace(H, OPS, model) + @info "Selected halfspace: w: $(w), b: $(b)" + @info "δ: $(δ)" + if δ - 1e-3 <= ε # added some convergence tolerance + break + end + # would not terminate when precision is set to 0 + new_f = sum(w[i] * (scalars[i] + u[i]) for i in 1:n) # w' * (f(x) + u) + MOI.set(model.inner, MOI.ObjectiveFunction{typeof(new_f)}(), new_f) + MOI.optimize!(model.inner) + β̄ = MOI.get(model.inner, MOI.ObjectiveValue()) + @info "β̄: $(β̄)" + X, Y = MOA._compute_point(model, variables, model.f) + @info "Y: $(Y)" + solutions[Y] = X + push!(OPS, (w, β̄)) + @info "Added halfspace w: $(w), b: $(β̄) to OPS" + IPS = push!(IPS, Y) + @info "IPS: $(IPS)" + @info "OPS: $(OPS)" + H = _halfspaces(IPS) + if count == 10 + break + end + end + MOI.delete.(model.inner, f_constraints) + MOI.delete.(model.inner, u_constraints) + MOI.delete.(model.inner, u) + return MOI.OPTIMAL, [MOA.SolutionPoint(X, Y) for (Y, X) in solutions] +end + +end # module MultiObjectiveAlgorithmsPolyhedraExt diff --git a/ext/PolyhedraExt.jl b/ext/PolyhedraExt.jl deleted file mode 100644 index c3fa357..0000000 --- a/ext/PolyhedraExt.jl +++ /dev/null @@ -1,12 +0,0 @@ -module PolyhedraExt - -using MultiObjectiveAlgorithms -using Polyhedra - -function MultiObjectiveAlgorithms._halfspaces(IPS::Vector{Vector{Float64}}) - V = vrep(IPS) - H = halfspaces(doubledescription(V)) - return [(-H_i.a, -H_i.β) for H_i in H] -end - -end diff --git a/src/algorithms/Sandwiching.jl b/src/algorithms/Sandwiching.jl index 0484102..8c3ad44 100644 --- a/src/algorithms/Sandwiching.jl +++ b/src/algorithms/Sandwiching.jl @@ -1,141 +1,23 @@ -function _halfspaces(IPS) - return error("MOA.Sandwiching requires Polyhedra.jl to be loaded") -end +# Copyright 2019, Oscar Dowson and contributors +# This Source Code Form is subject to the terms of the Mozilla Public License, +# v.2.0. If a copy of the MPL was not distributed with this file, You can +# obtain one at http://mozilla.org/MPL/2.0/. -mutable struct Sandwiching <: AbstractAlgorithm - precision::Float64 -end +""" + Sandwiching(precision::Float64) -tol = 1e-3 +An algorithm that implemennts the paper described in XXX. -function _compute_anchors(model::Optimizer) - anchors = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}() - n = MOI.output_dimension(model.f) - scalars = MOI.Utilities.scalarize(model.f) - variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) - yI, yUB = zeros(n), zeros(n) - for (i, f_i) in enumerate(scalars) - MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f_i)}(), f_i) # ρ * sum(f_j for (j, f_j) in enumerate(model.f) if j != i) - MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) - MOI.optimize!(model.inner) - # status check - X, Y = _compute_point(model, variables, model.f) - model.ideal_point[i] = Y[i] - yI[i] = Y[i] - anchors[Y] = X - MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) - MOI.optimize!(model.inner) - # status check - _, Y = _compute_point(model, variables, f_i) - yUB[i] = Y - end - MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) - return yI, yUB, anchors -end - -@enum DistanceMeasure SUB CUR SOL +## Compat -function _distance(w̄, b̄, OPS, model) - n = MOI.output_dimension(model.f) - optimizer = typeof(model.inner.optimizer) - δ_optimizer = optimizer() - MOI.set(δ_optimizer, MOI.Silent(), true) - x = MOI.add_variables(δ_optimizer, n) - for (w, b) in OPS - MOI.add_constraint( - δ_optimizer, - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w, x), 0.0), - MOI.GreaterThan(b), - ) - end - MOI.set( - δ_optimizer, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w̄, x), 0.0), - ) - MOI.set(δ_optimizer, MOI.ObjectiveSense(), MOI.MIN_SENSE) - MOI.optimize!(δ_optimizer) - δ = b̄ - MOI.get(δ_optimizer, MOI.ObjectiveValue()) - return δ -end +To use this algorithm you MUST first load the Polyhedra.jl Julia package: -function _select_next_halfspace(H, OPS, model) - distances = [_distance(w, b, OPS, model) for (w, b) in H] - @info "Distances: $(Dict(zip(H, distances)))" - index = argmax(distances) - w, b = H[index] - return distances[index], w, b -end - -function optimize_multiobjective!(algorithm::Sandwiching, model::Optimizer) - @assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE - ε = algorithm.precision - start_time = time() - solutions = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}() - variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) - n = MOI.output_dimension(model.f) - scalars = MOI.Utilities.scalarize(model.f) - yI, yUB, anchors = _compute_anchors(model) - merge!(solutions, anchors) - @info "yI: $(yI)" - @info "yUB: $(yUB)" - IPS = [yUB, keys(anchors)...] - OPS = Tuple{Vector{Float64},Float64}[] - for i in 1:n - e_i = Float64.(1:n .== i) - push!(OPS, (e_i, yI[i])) # e_i' * y >= yI_i - push!(OPS, (-e_i, -yUB[i])) # -e_i' * y >= -yUB_i ⟹ e_i' * y <= yUB_i - end - @info "IPS: $(IPS)" - @info "OPS: $(OPS)" - u = MOI.add_variables(model.inner, n) - u_constraints = [ # u_i >= 0 for all i = 1:n - MOI.add_constraint(model.inner, u_i, MOI.GreaterThan{Float64}(0)) - for u_i in u - ] - f_constraints = [ # f_i + u_i <= yUB_i for all i = 1:n - MOI.Utilities.normalize_and_add_constraint( - model.inner, - scalars[i] + u[i], - MOI.LessThan(yUB[i]), - ) for i in 1:n - ] - H = _halfspaces(IPS) - - count = 0 - - while !isempty(H) - count += 1 - @info "-- Iteration #$(count) --" - @info "HalfSpaces: $(H)" - δ, w, b = _select_next_halfspace(H, OPS, model) - @info "Selected halfspace: w: $(w), b: $(b)" - @info "δ: $(δ)" - if δ - tol > ε # added some convergence tolerance - # would not terminate when precision is set to 0 - new_f = sum(w[i] * (scalars[i] + u[i]) for i in 1:n) # w' * (f(x) + u) - MOI.set(model.inner, MOI.ObjectiveFunction{typeof(new_f)}(), new_f) - MOI.optimize!(model.inner) - β̄ = MOI.get(model.inner, MOI.ObjectiveValue()) - @info "β̄: $(β̄)" - X, Y = _compute_point(model, variables, model.f) - @info "Y: $(Y)" - solutions[Y] = X - push!(OPS, (w, β̄)) - @info "Added halfspace w: $(w), b: $(β̄) to OPS" - IPS = push!(IPS, Y) - else - break - end - @info "IPS: $(IPS)" - @info "OPS: $(OPS)" - H = _halfspaces(IPS) - if count == 10 - break - end - end - MOI.delete.(model.inner, f_constraints) - MOI.delete.(model.inner, u_constraints) - MOI.delete.(model.inner, u) - return MOI.OPTIMAL, [SolutionPoint(X, Y) for (Y, X) in solutions] +```julia +import MultiObjectiveAlgorithms as MOA +import Polyhedra +algorithm = MOA.Sandwiching(0.0) +``` +""" +mutable struct Sandwiching <: AbstractAlgorithm + precision::Float64 end diff --git a/test/algorithms/Sandwiching.jl b/test/algorithms/Sandwiching.jl index 76a87ca..e3a654f 100644 --- a/test/algorithms/Sandwiching.jl +++ b/test/algorithms/Sandwiching.jl @@ -1,11 +1,16 @@ +# Copyright 2019, Oscar Dowson and contributors +# This Source Code Form is subject to the terms of the Mozilla Public License, +# v.2.0. If a copy of the MPL was not distributed with this file, You can +# obtain one at http://mozilla.org/MPL/2.0/. + module TestSandwiching using Test import HiGHS -using Polyhedra import MultiObjectiveAlgorithms as MOA import MultiObjectiveAlgorithms: MOI +import Polyhedra function run_tests() for name in names(@__MODULE__; all = true) @@ -24,20 +29,12 @@ end # Department of Engineering Science, The University of Auckland, New Zealand # Laboratoire d’Informatique de Nantes Atlantique, CNRS, Universit´e de Nantes, France function test_molp() - C = Float64[ - 3 1 - -1 -2 - ] + C = Float64[3 1; -1 -2] p = size(C, 1) - A = Float64[ - 0 1 - 3 -1 - ] + A = Float64[0 1; 3 -1] m, n = size(A) b = Float64[3, 6] - model = MOI.instantiate(; with_bridge_type = Float64) do - return MOA.Optimizer(HiGHS.Optimizer) - end + model = MOA.Optimizer(HiGHS.Optimizer) MOI.set(model, MOA.Algorithm(), MOA.Sandwiching(0.0)) MOI.set(model, MOI.Silent(), true) x = MOI.add_variables(model, n) @@ -79,8 +76,9 @@ function test_molp() @test ≈(x_sol, x_res; atol = 1e-6) @test ≈(y_sol, y_res; atol = 1e-6) end + return end -end +end # TestSandwiching TestSandwiching.run_tests() From 6738219bd61e04d6f152db1ee823d578aceed79e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Kof?= Date: Thu, 3 Jul 2025 18:24:17 +0300 Subject: [PATCH 04/12] Update and add test --- ext/MultiObjectiveAlgorithmsPolyhedraExt.jl | 77 +++++++++++---------- test/algorithms/Sandwiching.jl | 56 ++++++++++----- 2 files changed, 78 insertions(+), 55 deletions(-) diff --git a/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl index e93b443..cbc4031 100644 --- a/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl +++ b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl @@ -15,31 +15,6 @@ function _halfspaces(IPS::Vector{Vector{Float64}}) return [(-H_i.a, -H_i.β) for H_i in H] end -function _compute_anchors(model::MOA.Optimizer) - anchors = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}() - n = MOI.output_dimension(model.f) - scalars = MOI.Utilities.scalarize(model.f) - variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) - yI, yUB = zeros(n), zeros(n) - for (i, f_i) in enumerate(scalars) - MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f_i)}(), f_i) - MOI.optimize!(model.inner) - # status check - X, Y = MOA._compute_point(model, variables, model.f) - model.ideal_point[i] = Y[i] - yI[i] = Y[i] - anchors[Y] = X - MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) - MOI.optimize!(model.inner) - # status check - _, Y = MOA._compute_point(model, variables, f_i) - yUB[i] = Y - MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) - end - - return yI, yUB, anchors -end - function _distance(w̄, b̄, OPS, model) n = MOI.output_dimension(model.f) optimizer = typeof(model.inner.optimizer) @@ -65,7 +40,6 @@ end function _select_next_halfspace(H, OPS, model) distances = [_distance(w, b, OPS, model) for (w, b) in H] - @info "Distances: $(Dict(zip(H, distances)))" index = argmax(distances) w, b = H[index] return distances[index], w, b @@ -75,24 +49,45 @@ function MOA.minimize_multiobjective!( algorithm::MOA.Sandwiching, model::MOA.Optimizer, ) - @assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE - ε = algorithm.precision + @assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE start_time = time() solutions = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}() variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) n = MOI.output_dimension(model.f) scalars = MOI.Utilities.scalarize(model.f) - yI, yUB, anchors = _compute_anchors(model) - merge!(solutions, anchors) - @info "yI: $(yI)" - @info "yUB: $(yUB)" - IPS = [yUB, keys(anchors)...] + status = MOI.OPTIMAL OPS = Tuple{Vector{Float64},Float64}[] - for i in 1:n + anchors = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}() + yI, yUB = zeros(n), zeros(n) + for (i, f_i) in enumerate(scalars) + MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f_i)}(), f_i) + MOI.optimize!(model.inner) + status = MOI.get(model.inner, MOI.TerminationStatus()) + if !MOA._is_scalar_status_optimal(model) + return status, nothing + end + X, Y = MOA._compute_point(model, variables, model.f) + model.ideal_point[i] = Y[i] + yI[i] = Y[i] + anchors[Y] = X + MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) + MOI.optimize!(model.inner) + status = MOI.get(model.inner, MOI.TerminationStatus()) + if !MOA._is_scalar_status_optimal(model) + MOA._warn_on_nonfinite_anti_ideal(algorithm, MOI.MIN_SENSE, i) + return status, nothing + end + _, Y = MOA._compute_point(model, variables, f_i) + yUB[i] = Y + MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) e_i = Float64.(1:n .== i) push!(OPS, (e_i, yI[i])) # e_i' * y >= yI_i push!(OPS, (-e_i, -yUB[i])) # -e_i' * y >= -yUB_i ⟹ e_i' * y <= yUB_i end + @info "yI: $(yI)" + @info "yUB: $(yUB)" + IPS = [yUB, keys(anchors)...] + merge!(solutions, anchors) @info "IPS: $(IPS)" @info "OPS: $(OPS)" u = MOI.add_variables(model.inner, n) @@ -110,19 +105,28 @@ function MOA.minimize_multiobjective!( H = _halfspaces(IPS) count = 0 while !isempty(H) + if MOA._time_limit_exceeded(model, start_time) + status = MOI.TIME_LIMIT + break + end count += 1 @info "-- Iteration #$(count) --" @info "HalfSpaces: $(H)" δ, w, b = _select_next_halfspace(H, OPS, model) @info "Selected halfspace: w: $(w), b: $(b)" @info "δ: $(δ)" - if δ - 1e-3 <= ε # added some convergence tolerance + if δ - 1e-3 <= algorithm.precision # added some convergence tolerance break end # would not terminate when precision is set to 0 new_f = sum(w[i] * (scalars[i] + u[i]) for i in 1:n) # w' * (f(x) + u) MOI.set(model.inner, MOI.ObjectiveFunction{typeof(new_f)}(), new_f) MOI.optimize!(model.inner) + status = MOI.get(model.inner, MOI.TerminationStatus()) + if !MOA._is_scalar_status_optimal(model) + MOA._warn_on_nonfinite_anti_ideal(algorithm, MOI.MIN_SENSE, i) + return status, nothing + end β̄ = MOI.get(model.inner, MOI.ObjectiveValue()) @info "β̄: $(β̄)" X, Y = MOA._compute_point(model, variables, model.f) @@ -134,9 +138,6 @@ function MOA.minimize_multiobjective!( @info "IPS: $(IPS)" @info "OPS: $(OPS)" H = _halfspaces(IPS) - if count == 10 - break - end end MOI.delete.(model.inner, f_constraints) MOI.delete.(model.inner, u_constraints) diff --git a/test/algorithms/Sandwiching.jl b/test/algorithms/Sandwiching.jl index e3a654f..57c8f4c 100644 --- a/test/algorithms/Sandwiching.jl +++ b/test/algorithms/Sandwiching.jl @@ -23,17 +23,9 @@ function run_tests() return end -# From International Doctoral School Algorithmic Decision Theory: MCDA and MOO -# Lecture 2: Multiobjective Linear Programming -# Matthias Ehrgott -# Department of Engineering Science, The University of Auckland, New Zealand -# Laboratoire d’Informatique de Nantes Atlantique, CNRS, Universit´e de Nantes, France -function test_molp() - C = Float64[3 1; -1 -2] +function _test_molp(C, A, b, results, sense) p = size(C, 1) - A = Float64[0 1; 3 -1] m, n = size(A) - b = Float64[3, 6] model = MOA.Optimizer(HiGHS.Optimizer) MOI.set(model, MOA.Algorithm(), MOA.Sandwiching(0.0)) MOI.set(model, MOI.Silent(), true) @@ -56,20 +48,15 @@ function test_molp() ], zeros(p), ) - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveSense(), sense) MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) MOI.optimize!(model) N = MOI.get(model, MOI.ResultCount()) - solutions = reverse([ + solutions = sort([ MOI.get(model, MOI.VariablePrimal(i), x) => MOI.get(model, MOI.ObjectiveValue(i)) for i in 1:N ]) - results = reverse([ - [0.0, 0.0] => [0.0, 0.0], - [0.0, 3.0] => [3.0, -6.0], - [3.0, 3.0] => [12.0, -9.0], - ]) - @test length(solutions) == length(results) + @test N == length(results) for (sol, res) in zip(solutions, results) x_sol, y_sol = sol x_res, y_res = res @@ -79,6 +66,41 @@ function test_molp() return end +# From International Doctoral School Algorithmic Decision Theory: MCDA and MOO +# Lecture 2: Multiobjective Linear Programming +# Matthias Ehrgott +# Department of Engineering Science, The University of Auckland, New Zealand +# Laboratoire d’Informatique de Nantes Atlantique, CNRS, Universit´e de Nantes, France +function test_molp_1() + C = Float64[3 1; -1 -2] + A = Float64[0 1; 3 -1] + b = Float64[3, 6] + results = sort([ + [0.0, 0.0] => [0.0, 0.0], + [0.0, 3.0] => [3.0, -6.0], + [3.0, 3.0] => [12.0, -9.0], + ]) + sense = MOI.MIN_SENSE + return _test_molp(C, A, b, results, sense) +end + +# From Civil and Environmental Systems Engineering +# Chapter 5 Exercise 5.A.3 A graphical Interpretation of Noninferiority +function test_molp_2() + C = Float64[3 -2; -1 2] + A = Float64[-4 -8; 3 -6; 4 -2; 1 0; -1 3; -2 4; -6 3] + b = Float64[-8, 6, 14, 6, 15, 18, 9] + results = sort([ + [1.0, 5.0] => [-7.0, 9.0], # not sure about this + [3.0, 6.0] => [-3.0, 9.0], + [4.0, 1.0] => [10.0, -2.0], + [6.0, 5.0] => [8.0, 4.0], + [6.0, 7.0] => [4.0, 8.0], + ]) + sense = MOI.MAX_SENSE + return _test_molp(C, A, b, results, sense) +end + end # TestSandwiching TestSandwiching.run_tests() From 9a5595ec79695a9113b1dc5824f07fc2bb06de44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Kof?= Date: Thu, 3 Jul 2025 18:26:38 +0300 Subject: [PATCH 05/12] Fix format --- ext/MultiObjectiveAlgorithmsPolyhedraExt.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl index cbc4031..193471e 100644 --- a/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl +++ b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl @@ -49,7 +49,7 @@ function MOA.minimize_multiobjective!( algorithm::MOA.Sandwiching, model::MOA.Optimizer, ) - @assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE + @assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE start_time = time() solutions = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}() variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) From f79d6a1e0779f80a07850745794b8b808ea3fa3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Kof?= Date: Thu, 3 Jul 2025 19:15:39 +0300 Subject: [PATCH 06/12] Add infeasible and unbounded tests --- test/algorithms/Sandwiching.jl | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/algorithms/Sandwiching.jl b/test/algorithms/Sandwiching.jl index 57c8f4c..b41b9dd 100644 --- a/test/algorithms/Sandwiching.jl +++ b/test/algorithms/Sandwiching.jl @@ -101,6 +101,39 @@ function test_molp_2() return _test_molp(C, A, b, results, sense) end +function test_infeasible() + model = MOA.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOA.Algorithm(), MOA.Sandwiching(0.0)) + MOI.set(model, MOI.Silent(), true) + x = MOI.add_variables(model, 2) + MOI.add_constraint.(model, x, MOI.GreaterThan(0.0)) + MOI.add_constraint(model, 1.0 * x[1] + 1.0 * x[2], MOI.LessThan(-1.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE + @test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION + @test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION + return +end + +function test_unbounded() + model = MOA.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOA.Algorithm(), MOA.Sandwiching(0.0)) + MOI.set(model, MOI.Silent(), true) + x = MOI.add_variables(model, 2) + MOI.add_constraint.(model, x, MOI.GreaterThan(0.0)) + f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE + @test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION + @test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION + return +end + end # TestSandwiching TestSandwiching.run_tests() From d8f6bf77898e3c10b027bea538d5e48148bccc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Kof?= Date: Thu, 3 Jul 2025 19:15:50 +0300 Subject: [PATCH 07/12] Remove loggings --- ext/MultiObjectiveAlgorithmsPolyhedraExt.jl | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl index 193471e..222d6f9 100644 --- a/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl +++ b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl @@ -84,12 +84,8 @@ function MOA.minimize_multiobjective!( push!(OPS, (e_i, yI[i])) # e_i' * y >= yI_i push!(OPS, (-e_i, -yUB[i])) # -e_i' * y >= -yUB_i ⟹ e_i' * y <= yUB_i end - @info "yI: $(yI)" - @info "yUB: $(yUB)" IPS = [yUB, keys(anchors)...] merge!(solutions, anchors) - @info "IPS: $(IPS)" - @info "OPS: $(OPS)" u = MOI.add_variables(model.inner, n) u_constraints = [ # u_i >= 0 for all i = 1:n MOI.add_constraint(model.inner, u_i, MOI.GreaterThan{Float64}(0)) @@ -110,11 +106,7 @@ function MOA.minimize_multiobjective!( break end count += 1 - @info "-- Iteration #$(count) --" - @info "HalfSpaces: $(H)" δ, w, b = _select_next_halfspace(H, OPS, model) - @info "Selected halfspace: w: $(w), b: $(b)" - @info "δ: $(δ)" if δ - 1e-3 <= algorithm.precision # added some convergence tolerance break end @@ -128,15 +120,10 @@ function MOA.minimize_multiobjective!( return status, nothing end β̄ = MOI.get(model.inner, MOI.ObjectiveValue()) - @info "β̄: $(β̄)" X, Y = MOA._compute_point(model, variables, model.f) - @info "Y: $(Y)" solutions[Y] = X push!(OPS, (w, β̄)) - @info "Added halfspace w: $(w), b: $(β̄) to OPS" IPS = push!(IPS, Y) - @info "IPS: $(IPS)" - @info "OPS: $(OPS)" H = _halfspaces(IPS) end MOI.delete.(model.inner, f_constraints) From 99dbc12956e0c6bc8cc53f34c62e208a9875845e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Kof?= Date: Thu, 3 Jul 2025 22:40:29 +0300 Subject: [PATCH 08/12] Added tests for no bounding box and time limit --- test/algorithms/Sandwiching.jl | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/algorithms/Sandwiching.jl b/test/algorithms/Sandwiching.jl index b41b9dd..5f6f64b 100644 --- a/test/algorithms/Sandwiching.jl +++ b/test/algorithms/Sandwiching.jl @@ -134,6 +134,61 @@ function test_unbounded() return end +function test_no_bounding_box() + model = MOA.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOA.Algorithm(), MOA.Sandwiching(0.0)) + MOI.set(model, MOI.Silent(), true) + x = MOI.add_variables(model, 2) + MOI.add_constraint.(model, x, MOI.GreaterThan(0.0)) + f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + @test_logs (:warn,) MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE + @test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION + @test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION + return +end + +function test_time_limit() + p = 3 + n = 10 + W = 2137.0 + C = Float64[ + 566 611 506 180 817 184 585 423 26 317 + 62 84 977 979 874 54 269 93 881 563 + 664 982 962 140 224 215 12 869 332 537 + ] + w = Float64[557, 898, 148, 63, 78, 964, 246, 662, 386, 272] + model = MOA.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOA.Algorithm(), MOA.Sandwiching(0.0)) + MOI.set(model, MOI.TimeLimitSec(), 0.0) + MOI.set(model, MOI.Silent(), true) + x = MOI.add_variables(model, n) + MOI.add_constraint.(model, x, MOI.ZeroOne()) + MOI.add_constraint( + model, + MOI.ScalarAffineFunction( + [MOI.ScalarAffineTerm(w[j], x[j]) for j in 1:n], + 0.0, + ), + MOI.LessThan(W), + ) + f = MOI.VectorAffineFunction( + [ + MOI.VectorAffineTerm(i, MOI.ScalarAffineTerm(-C[i, j], x[j])) + for i in 1:p for j in 1:n + ], + fill(0.0, p), + ) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.TIME_LIMIT + @test MOI.get(model, MOI.ResultCount()) == size(C, 1) # anchor points are already computed when the time limit is checked + return +end + end # TestSandwiching TestSandwiching.run_tests() From d744b8ab239fb82931e123db59ef58510ced3f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Kof?= Date: Thu, 3 Jul 2025 22:40:39 +0300 Subject: [PATCH 09/12] Added reference --- src/algorithms/Sandwiching.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/algorithms/Sandwiching.jl b/src/algorithms/Sandwiching.jl index 8c3ad44..a53af81 100644 --- a/src/algorithms/Sandwiching.jl +++ b/src/algorithms/Sandwiching.jl @@ -6,7 +6,7 @@ """ Sandwiching(precision::Float64) -An algorithm that implemennts the paper described in XXX. +An algorithm that implemennts the paper described in Koenen, M., Balvert, M., & Fleuren, H. A. (2023). A Renewed Take on Weighted Sum in Sandwich Algorithms: Modification of the Criterion Space. (Center Discussion Paper; Vol. 2023-012). CentER, Center for Economic Research. ## Compat From 27bb918b7cc5c3c090999c52b1b2d88b49e169f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Kof?= Date: Thu, 3 Jul 2025 22:40:56 +0300 Subject: [PATCH 10/12] Update --- ext/MultiObjectiveAlgorithmsPolyhedraExt.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl index 222d6f9..b5e377b 100644 --- a/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl +++ b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl @@ -116,7 +116,6 @@ function MOA.minimize_multiobjective!( MOI.optimize!(model.inner) status = MOI.get(model.inner, MOI.TerminationStatus()) if !MOA._is_scalar_status_optimal(model) - MOA._warn_on_nonfinite_anti_ideal(algorithm, MOI.MIN_SENSE, i) return status, nothing end β̄ = MOI.get(model.inner, MOI.ObjectiveValue()) @@ -129,7 +128,7 @@ function MOA.minimize_multiobjective!( MOI.delete.(model.inner, f_constraints) MOI.delete.(model.inner, u_constraints) MOI.delete.(model.inner, u) - return MOI.OPTIMAL, [MOA.SolutionPoint(X, Y) for (Y, X) in solutions] + return status, [MOA.SolutionPoint(X, Y) for (Y, X) in solutions] end end # module MultiObjectiveAlgorithmsPolyhedraExt From fa1019be4bdb355e69fb46e74ff4401eef4d1737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Kof?= Date: Fri, 4 Jul 2025 18:09:50 +0300 Subject: [PATCH 11/12] Update distance model and OPS --- ext/MultiObjectiveAlgorithmsPolyhedraExt.jl | 54 +++++++++++---------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl index b5e377b..b68034f 100644 --- a/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl +++ b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl @@ -15,31 +15,20 @@ function _halfspaces(IPS::Vector{Vector{Float64}}) return [(-H_i.a, -H_i.β) for H_i in H] end -function _distance(w̄, b̄, OPS, model) - n = MOI.output_dimension(model.f) - optimizer = typeof(model.inner.optimizer) - δ_optimizer = optimizer() - MOI.set(δ_optimizer, MOI.Silent(), true) - x = MOI.add_variables(δ_optimizer, n) - for (w, b) in OPS - MOI.add_constraint( - δ_optimizer, - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w, x), 0.0), - MOI.GreaterThan(b), - ) - end +function _distance(w̄, b̄, δ_OPS_optimizer) + y = MOI.get(δ_OPS_optimizer, MOI.ListOfVariableIndices()) MOI.set( - δ_optimizer, + δ_OPS_optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w̄, x), 0.0), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w̄, y), 0.0), ) - MOI.set(δ_optimizer, MOI.ObjectiveSense(), MOI.MIN_SENSE) - MOI.optimize!(δ_optimizer) - return b̄ - MOI.get(δ_optimizer, MOI.ObjectiveValue()) + MOI.set(δ_OPS_optimizer, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.optimize!(δ_OPS_optimizer) + return b̄ - MOI.get(δ_OPS_optimizer, MOI.ObjectiveValue()) end -function _select_next_halfspace(H, OPS, model) - distances = [_distance(w, b, OPS, model) for (w, b) in H] +function _select_next_halfspace(H, δ_OPS_optimizer) + distances = [_distance(w, b, δ_OPS_optimizer) for (w, b) in H] index = argmax(distances) w, b = H[index] return distances[index], w, b @@ -56,7 +45,10 @@ function MOA.minimize_multiobjective!( n = MOI.output_dimension(model.f) scalars = MOI.Utilities.scalarize(model.f) status = MOI.OPTIMAL - OPS = Tuple{Vector{Float64},Float64}[] + optimizer = typeof(model.inner.optimizer) + δ_OPS_optimizer = optimizer() + MOI.set(δ_OPS_optimizer, MOI.Silent(), true) + y = MOI.add_variables(δ_OPS_optimizer, n) anchors = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}() yI, yUB = zeros(n), zeros(n) for (i, f_i) in enumerate(scalars) @@ -81,8 +73,16 @@ function MOA.minimize_multiobjective!( yUB[i] = Y MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) e_i = Float64.(1:n .== i) - push!(OPS, (e_i, yI[i])) # e_i' * y >= yI_i - push!(OPS, (-e_i, -yUB[i])) # -e_i' * y >= -yUB_i ⟹ e_i' * y <= yUB_i + MOI.add_constraint( + δ_OPS_optimizer, + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(e_i, y), 0.0), + MOI.GreaterThan(yI[i]), + ) + MOI.add_constraint( + δ_OPS_optimizer, + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(e_i, y), 0.0), + MOI.LessThan(yUB[i]), + ) end IPS = [yUB, keys(anchors)...] merge!(solutions, anchors) @@ -106,7 +106,7 @@ function MOA.minimize_multiobjective!( break end count += 1 - δ, w, b = _select_next_halfspace(H, OPS, model) + δ, w, b = _select_next_halfspace(H, δ_OPS_optimizer) if δ - 1e-3 <= algorithm.precision # added some convergence tolerance break end @@ -121,7 +121,11 @@ function MOA.minimize_multiobjective!( β̄ = MOI.get(model.inner, MOI.ObjectiveValue()) X, Y = MOA._compute_point(model, variables, model.f) solutions[Y] = X - push!(OPS, (w, β̄)) + MOI.add_constraint( + δ_OPS_optimizer, + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w, y), 0.0), + MOI.GreaterThan(β̄), + ) IPS = push!(IPS, Y) H = _halfspaces(IPS) end From 303c51d9d0153e850bad2ac5743f364d45b69e73 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 8 Jul 2025 09:39:02 +1200 Subject: [PATCH 12/12] Update --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 826974c..818662e 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ The value must be one of the algorithms supported by MOA: * `MOA.KirlikSayin()` * `MOA.Lexicographic()` [default] * `MOA.RandomWeighting()` + * `MOA.Sandwiching()` * `MOA.TambyVanderpooten()` Consult their docstrings for details.