From ab8aa5496eff2eb6b6d943543f8c10f24b33e53d Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 2 Sep 2025 19:03:28 +1200 Subject: [PATCH 1/7] Sort based on optimization sense --- src/MultiObjectiveAlgorithms.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/MultiObjectiveAlgorithms.jl b/src/MultiObjectiveAlgorithms.jl index b9498da..37e88c9 100644 --- a/src/MultiObjectiveAlgorithms.jl +++ b/src/MultiObjectiveAlgorithms.jl @@ -42,14 +42,16 @@ function dominates( end end -_sort!(solutions::Vector{SolutionPoint}) = sort!(solutions; by = x -> x.y) +function _sort!(solutions::Vector{SolutionPoint}, sense::MOI.OptimizationSense) + return sort!(solutions; by = x -> x.y, rev = sense == MOI.MAX_SENSE) +end function filter_nondominated( sense, solutions::Vector{SolutionPoint}; atol::Float64 = 1e-6, ) - _sort!(solutions) + _sort!(solutions, sense) nondominated_solutions = SolutionPoint[] for candidate in solutions if any(test -> dominates(sense, test, candidate; atol), solutions) @@ -681,7 +683,7 @@ function _optimize!(model::Optimizer) model.termination_status = status if solutions !== nothing model.solutions = solutions - _sort!(model.solutions) + _sort!(model.solutions, MOI.get(model, MOI.ObjectiveSense())) end if MOI.get(model, ComputeIdealPoint()) _compute_ideal_point(model, start_time) From 35e05d091ade310b55416b1c26e5c83a08ce5399 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 2 Sep 2025 20:40:35 +1200 Subject: [PATCH 2/7] Add tests --- test/algorithms/Chalmet.jl | 1 + test/algorithms/Dichotomy.jl | 3 +++ test/algorithms/EpsilonConstraint.jl | 4 ++++ test/algorithms/Lexicographic.jl | 2 ++ test/algorithms/RandomWeighting.jl | 1 + test/problems.jl | 6 ++++-- 6 files changed, 15 insertions(+), 2 deletions(-) diff --git a/test/algorithms/Chalmet.jl b/test/algorithms/Chalmet.jl index ca2579a..5bb1f66 100644 --- a/test/algorithms/Chalmet.jl +++ b/test/algorithms/Chalmet.jl @@ -105,6 +105,7 @@ function test_knapsack_max() [0, 1, 1, 1, 1, 0, 1, 0, 1, 1] => [3043, 4627], [1, 0, 1, 1, 1, 0, 1, 1, 0, 1] => [3395, 3817], ] + reverse!(results) @test MOI.get(model, MOI.ResultCount()) == length(results) for (i, (x_sol, y_sol)) in enumerate(results) @test ≈(x_sol, MOI.get(model, MOI.VariablePrimal(i), x); atol = 1e-6) diff --git a/test/algorithms/Dichotomy.jl b/test/algorithms/Dichotomy.jl index 2fa8d07..9f26f86 100644 --- a/test/algorithms/Dichotomy.jl +++ b/test/algorithms/Dichotomy.jl @@ -98,6 +98,8 @@ function test_moi_bolp_1_maximize() @test MOI.get(model, MOI.ResultCount()) == 3 X = [[1.0, 0.25], [0.5, 0.5], [0.0, 1.0]] Y = [[-2.25, -1.25], [-1.5, -1.5], [-1.0, -2.5]] + reverse!(X) + reverse!(Y) for i in 1:3 @test MOI.get(model, MOI.PrimalStatus(i)) == MOI.FEASIBLE_POINT @test MOI.get(model, MOI.DualStatus(i)) == MOI.NO_SOLUTION @@ -227,6 +229,7 @@ function test_biobjective_knapsack() [948.0, 939.0] => [1, 2, 3, 5, 6, 8, 10, 11, 15, 16, 17], [955.0, 906.0] => [2, 3, 5, 6, 9, 10, 11, 14, 15, 16, 17], ] + reverse!(results) for i in 1:MOI.get(model, MOI.ResultCount()) x_sol = MOI.get(model, MOI.VariablePrimal(i), x) @test results[i][2] == findall(elt -> elt > 0.9, x_sol) diff --git a/test/algorithms/EpsilonConstraint.jl b/test/algorithms/EpsilonConstraint.jl index 0119113..747f25c 100644 --- a/test/algorithms/EpsilonConstraint.jl +++ b/test/algorithms/EpsilonConstraint.jl @@ -69,6 +69,7 @@ function test_biobjective_knapsack() [950, 915] => [1, 2, 5, 6, 8, 9, 10, 11, 15, 16, 17], [956, 906] => [2, 3, 5, 6, 9, 10, 11, 14, 15, 16, 17], ] + reverse!(results) @test MOI.get(model, MOI.ResultCount()) == 9 for i in 1:MOI.get(model, MOI.ResultCount()) x_sol = MOI.get(model, MOI.VariablePrimal(i), x) @@ -113,6 +114,7 @@ function test_biobjective_knapsack_atol() [949, 915] => [1, 2, 5, 6, 8, 9, 10, 11, 15, 16, 17], [955, 906] => [2, 3, 5, 6, 9, 10, 11, 14, 15, 16, 17], ] + reverse!(results) @test MOI.get(model, MOI.ResultCount()) == 9 for i in 1:MOI.get(model, MOI.ResultCount()) x_sol = MOI.get(model, MOI.VariablePrimal(i), x) @@ -154,6 +156,7 @@ function test_biobjective_knapsack_atol_large() [948, 939] => [1, 2, 3, 5, 6, 8, 10, 11, 15, 16, 17], [955, 906] => [2, 3, 5, 6, 9, 10, 11, 14, 15, 16, 17], ] + reverse!(results) @test MOI.get(model, MOI.ResultCount()) == 4 for i in 1:MOI.get(model, MOI.ResultCount()) x_sol = MOI.get(model, MOI.VariablePrimal(i), x) @@ -238,6 +241,7 @@ function test_biobjective_knapsack_min_solution_limit() [943, 940] => [2, 3, 5, 6, 8, 9, 10, 11, 15, 16, 17], [955, 906] => [2, 3, 5, 6, 9, 10, 11, 14, 15, 16, 17], ] + reverse!(results) @test MOI.get(model, MOI.ResultCount()) == 3 for i in 1:MOI.get(model, MOI.ResultCount()) x_sol = MOI.get(model, MOI.VariablePrimal(i), x) diff --git a/test/algorithms/Lexicographic.jl b/test/algorithms/Lexicographic.jl index 0cc4461..20b1d8c 100644 --- a/test/algorithms/Lexicographic.jl +++ b/test/algorithms/Lexicographic.jl @@ -94,6 +94,7 @@ function test_knapsack_default() [1, 0, 1] => [1, 0, 0, 1], [1, 1, 0] => [1, 1, 0, 0], ] + reverse!(results) @test MOI.get(model, MOI.ResultCount()) == 3 for i in 1:MOI.get(model, MOI.ResultCount()) X = round.(Int, MOI.get(model, MOI.VariablePrimal(i), x)) @@ -247,6 +248,7 @@ function test_knapsack_5_objectives() [1, 0, 1, 0, 2] => [1, 0, 1, 0], [1, 1, 0, 0, 2] => [1, 1, 0, 0], ] + reverse!(results) for i in 1:MOI.get(model, MOI.ResultCount()) X = round.(Int, MOI.get(model, MOI.VariablePrimal(i), x)) Y = round.(Int, MOI.get(model, MOI.ObjectiveValue(i))) diff --git a/test/algorithms/RandomWeighting.jl b/test/algorithms/RandomWeighting.jl index 5c3b9a0..ebc9ce4 100644 --- a/test/algorithms/RandomWeighting.jl +++ b/test/algorithms/RandomWeighting.jl @@ -124,6 +124,7 @@ function test_knapsack_max() [0, 1, 1, 1, 1, 0, 1, 0, 1, 1] => [3043, 4627], [1, 0, 1, 1, 1, 0, 1, 1, 0, 1] => [3395, 3817], ] + reverse!(results) @test MOI.get(model, MOI.ResultCount()) == length(results) for (i, (x_sol, y_sol)) in enumerate(results) @test ≈(x_sol, MOI.get(model, MOI.VariablePrimal(i), x); atol = 1e-6) diff --git a/test/problems.jl b/test/problems.jl index 1b64aff..b1a2776 100644 --- a/test/problems.jl +++ b/test/problems.jl @@ -113,6 +113,7 @@ function test_problem_knapsack_max_p3(model) [0, 1, 1, 1, 1, 0, 1, 0, 1, 1] => [3042, 4627, 3189], [1, 0, 1, 1, 1, 0, 1, 1, 0, 1] => [3394, 3817, 3408], ] + reverse!(results) N = MOI.get(model, MOI.ResultCount()) @assert N == length(results) for i in 1:length(results) @@ -223,6 +224,7 @@ function test_problem_knapsack_max_p4(model) [0, 1, 1, 0, 1, 1, 1, 1, 1, 0] => [3152, 3232, 3596, 3382], [1, 1, 1, 0, 1, 1, 1, 0, 0, 0] => [3269, 2320, 3059, 2891], ] + reverse!(results) @test MOI.get(model, MOI.ResultCount()) == length(results) for (i, (x_sol, y_sol)) in enumerate(results) @test ≈(x_sol, MOI.get(model, MOI.VariablePrimal(i), x); atol = 1e-6) @@ -368,7 +370,7 @@ function test_problem_assignment_max_p3(model) MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) MOI.optimize!(model) - results = reverse([ + results = [ [0 1 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 1] => [16, 61, 47], [0 0 1 0 0 0 1 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 1] => [17, 43, 71], [0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 1] => [18, 47, 67], @@ -390,7 +392,7 @@ function test_problem_assignment_max_p3(model) [0 1 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0 0 0 0 0 1 0] => [43, 51, 31], [0 1 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 1 0 0 1 0 0] => [45, 33, 34], [0 1 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 1 0 0 0 1 0 0] => [50, 40, 32], - ]) + ] @test MOI.get(model, MOI.ResultCount()) == length(results) @test MOI.get(model, MOA.SubproblemCount()) >= length(results) for (i, (x_sol, y_sol)) in enumerate(results) From 5e6e974008c74f5fe255938a638c1ad2a62b522c Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 2 Sep 2025 20:55:13 +1200 Subject: [PATCH 3/7] Update --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57ef58a..6193fe4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,8 +41,8 @@ jobs: - uses: julia-actions/cache@v1 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - with: - depwarn: error + # with: + # depwarn: error - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v4 with: From fc38d4df1f5013a40387c4762ae4770d558c1beb Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 3 Sep 2025 08:29:52 +1200 Subject: [PATCH 4/7] Update --- src/MultiObjectiveAlgorithms.jl | 2 +- test/test_utilities.jl | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/MultiObjectiveAlgorithms.jl b/src/MultiObjectiveAlgorithms.jl index 37e88c9..887ed5d 100644 --- a/src/MultiObjectiveAlgorithms.jl +++ b/src/MultiObjectiveAlgorithms.jl @@ -51,7 +51,6 @@ function filter_nondominated( solutions::Vector{SolutionPoint}; atol::Float64 = 1e-6, ) - _sort!(solutions, sense) nondominated_solutions = SolutionPoint[] for candidate in solutions if any(test -> dominates(sense, test, candidate; atol), solutions) @@ -62,6 +61,7 @@ function filter_nondominated( push!(nondominated_solutions, candidate) end end + _sort!(nondominated_solutions, sense) return nondominated_solutions end diff --git a/test/test_utilities.jl b/test/test_utilities.jl index 7ead00d..c3bfabe 100644 --- a/test/test_utilities.jl +++ b/test/test_utilities.jl @@ -25,7 +25,8 @@ function test_filter_nondominated() x = Dict{MOI.VariableIndex,Float64}() solutions = [MOA.SolutionPoint(x, [0, 1]), MOA.SolutionPoint(x, [1, 0])] @test MOA.filter_nondominated(MOI.MIN_SENSE, solutions) == solutions - @test MOA.filter_nondominated(MOI.MAX_SENSE, solutions) == solutions + @test MOA.filter_nondominated(MOI.MAX_SENSE, solutions) == + reverse(solutions) return end @@ -34,7 +35,7 @@ function test_filter_nondominated_sort_in_order() solutions = [MOA.SolutionPoint(x, [0, 1]), MOA.SolutionPoint(x, [1, 0])] r_solutions = reverse(solutions) @test MOA.filter_nondominated(MOI.MIN_SENSE, r_solutions) == solutions - @test MOA.filter_nondominated(MOI.MAX_SENSE, r_solutions) == solutions + @test MOA.filter_nondominated(MOI.MAX_SENSE, r_solutions) == r_solutions return end @@ -55,7 +56,7 @@ function test_filter_nondominated_weakly_dominated() MOA.SolutionPoint(x, [1, 0]), ] @test MOA.filter_nondominated(MOI.MIN_SENSE, solutions) == solutions[[1, 3]] - @test MOA.filter_nondominated(MOI.MAX_SENSE, solutions) == solutions[[2, 3]] + @test MOA.filter_nondominated(MOI.MAX_SENSE, solutions) == solutions[[3, 2]] solutions = [ MOA.SolutionPoint(x, [0, 1]), MOA.SolutionPoint(x, [0.5, 1]), @@ -67,7 +68,7 @@ function test_filter_nondominated_weakly_dominated() @test MOA.filter_nondominated(MOI.MIN_SENSE, solutions) == solutions[[1, 4, 6]] @test MOA.filter_nondominated(MOI.MAX_SENSE, solutions) == - solutions[[3, 5, 6]] + solutions[[6, 5, 3]] return end @@ -82,7 +83,7 @@ function test_filter_nondominated_knapsack() ] result = solutions[[1, 3, 4]] @test MOA.filter_nondominated(MOI.MIN_SENSE, solutions) == result - @test MOA.filter_nondominated(MOI.MAX_SENSE, solutions) == result + @test MOA.filter_nondominated(MOI.MAX_SENSE, solutions) == reverse(result) return end @@ -115,7 +116,7 @@ function test_filter_epsilon() solutions = [MOA.SolutionPoint(x, [1, 1 + 9e-5]), MOA.SolutionPoint(x, [2, 1])] new_solutions = MOA.filter_nondominated(MOI.MAX_SENSE, copy(solutions)) - @test new_solutions == solutions + @test new_solutions == reverse(solutions) solutions = [MOA.SolutionPoint(x, [-1, -1 - 1e-6]), MOA.SolutionPoint(x, [-2, -1])] new_solutions = MOA.filter_nondominated(MOI.MIN_SENSE, copy(solutions)) From 9efb372508bef9eb234d65de1221752cf0953e22 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 5 Sep 2025 11:40:07 +1200 Subject: [PATCH 5/7] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index b3aef5d..96d2f10 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,10 @@ Query the number of scalar subproblems that were solved using * `MOA.SubproblemCount()` +## Solution ordering + +Results are lexicograhically ordered by their objective vectors. The order depends on the objective sense. The first result is best. + ## Ideal point By default, MOA will compute the ideal point, which can be queried using the From c5c1583857bee36f47ef61f19a582337cbafb446 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 5 Sep 2025 12:03:47 +1200 Subject: [PATCH 6/7] Fix indentation for depwarn in CI workflow --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6193fe4..57ef58a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,8 +41,8 @@ jobs: - uses: julia-actions/cache@v1 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - # with: - # depwarn: error + with: + depwarn: error - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v4 with: From 20ae79e85b84645db38054e364cfd5e832b35b5b Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sat, 6 Sep 2025 10:17:56 +1200 Subject: [PATCH 7/7] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 96d2f10..25e45f6 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,8 @@ Query the number of scalar subproblems that were solved using ## Solution ordering -Results are lexicograhically ordered by their objective vectors. The order depends on the objective sense. The first result is best. +Results are lexicograhically ordered by their objective vectors. The order +depends on the objective sense. The first result is best. ## Ideal point