From 0aa6a21a87e570ed9866be6d515edfaec58bf2ed Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 16 Jun 2025 12:15:43 +1200 Subject: [PATCH 1/2] Fix comparison for non-dominated to account for tolerances --- src/MultiObjectiveAlgorithms.jl | 32 ++++++++++++++++++++++---------- test/test_utilities.jl | 29 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/MultiObjectiveAlgorithms.jl b/src/MultiObjectiveAlgorithms.jl index b0d269e..5f69cec 100644 --- a/src/MultiObjectiveAlgorithms.jl +++ b/src/MultiObjectiveAlgorithms.jl @@ -20,29 +20,41 @@ end Base.:(==)(a::SolutionPoint, b::SolutionPoint) = a.y == b.y """ - dominates(sense, a::SolutionPoint, b::SolutionPoint) + dominates(sense, a::SolutionPoint, b::SolutionPoint; atol::Float64) Returns `true` if point `a` dominates point `b`. """ -function dominates(sense, a::SolutionPoint, b::SolutionPoint) - if a.y == b.y - return false - elseif sense == MOI.MIN_SENSE - return all(a.y .<= b.y) +function dominates( + sense::MOI.OptimizationSense, + a::SolutionPoint, + b::SolutionPoint; + atol::Float64 = 1e-6, +) + l, u = extrema(a.y - b.y) + if sense == MOI.MIN_SENSE + # At least one element must be strictly better => l < -atol + # No element can be structly worse => u <= atol + return l < -atol && u <= atol else - return all(a.y .>= b.y) + # At least one element must be strictly better => u > atol + # No element can be structly worse => l >= -atol + return u > atol && l >= -atol end end _sort!(solutions::Vector{SolutionPoint}) = sort!(solutions; by = x -> x.y) -function filter_nondominated(sense, solutions::Vector{SolutionPoint}) +function filter_nondominated( + sense, + solutions::Vector{SolutionPoint}; + atol::Float64 = 1e-6, +) _sort!(solutions) nondominated_solutions = SolutionPoint[] for candidate in solutions - if any(test -> dominates(sense, test, candidate), solutions) + if any(test -> dominates(sense, test, candidate; atol), solutions) # Point is dominated. Don't add - elseif any(test -> test.y ≈ candidate.y, nondominated_solutions) + elseif any(test -> ≈(test.y, candidate.y; atol), nondominated_solutions) # Point already added to nondominated solutions. Don't add else push!(nondominated_solutions, candidate) diff --git a/test/test_utilities.jl b/test/test_utilities.jl index b65be5f..83cd8ed 100644 --- a/test/test_utilities.jl +++ b/test/test_utilities.jl @@ -106,6 +106,35 @@ function test_filter_nondominated_triple() return end +function test_filter_epsilon() + x = Dict{MOI.VariableIndex,Float64}() + solutions = [ + MOA.SolutionPoint(x, [1, 1 + 1e-6]), + MOA.SolutionPoint(x, [2, 1]), + ] + new_solutions = MOA.filter_nondominated(MOI.MAX_SENSE, copy(solutions)) + @test new_solutions == solutions[2:2] + 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 + solutions = [ + MOA.SolutionPoint(x, [-1, -1 - 1e-6]), + MOA.SolutionPoint(x, [-2, -1]), + ] + new_solutions = MOA.filter_nondominated(MOI.MIN_SENSE, copy(solutions)) + @test new_solutions == solutions[2:2] + solutions = [ + MOA.SolutionPoint(x, [-1, -1 - 9e-5]), + MOA.SolutionPoint(x, [-2, -1]), + ] + new_solutions = MOA.filter_nondominated(MOI.MIN_SENSE, copy(solutions)) + @test new_solutions == reverse(solutions) + return +end + end TestUtilities.run_tests() From 69e18f9ea937cccc0f884d7af92d7b534b83d33f Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 16 Jun 2025 13:14:55 +1200 Subject: [PATCH 2/2] Update --- test/test_utilities.jl | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/test/test_utilities.jl b/test/test_utilities.jl index 83cd8ed..7ead00d 100644 --- a/test/test_utilities.jl +++ b/test/test_utilities.jl @@ -108,28 +108,20 @@ end function test_filter_epsilon() x = Dict{MOI.VariableIndex,Float64}() - solutions = [ - MOA.SolutionPoint(x, [1, 1 + 1e-6]), - MOA.SolutionPoint(x, [2, 1]), - ] + solutions = + [MOA.SolutionPoint(x, [1, 1 + 1e-6]), MOA.SolutionPoint(x, [2, 1])] new_solutions = MOA.filter_nondominated(MOI.MAX_SENSE, copy(solutions)) @test new_solutions == solutions[2:2] - solutions = [ - MOA.SolutionPoint(x, [1, 1 + 9e-5]), - MOA.SolutionPoint(x, [2, 1]), - ] + 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 - solutions = [ - MOA.SolutionPoint(x, [-1, -1 - 1e-6]), - MOA.SolutionPoint(x, [-2, -1]), - ] + solutions = + [MOA.SolutionPoint(x, [-1, -1 - 1e-6]), MOA.SolutionPoint(x, [-2, -1])] new_solutions = MOA.filter_nondominated(MOI.MIN_SENSE, copy(solutions)) @test new_solutions == solutions[2:2] - solutions = [ - MOA.SolutionPoint(x, [-1, -1 - 9e-5]), - MOA.SolutionPoint(x, [-2, -1]), - ] + solutions = + [MOA.SolutionPoint(x, [-1, -1 - 9e-5]), MOA.SolutionPoint(x, [-2, -1])] new_solutions = MOA.filter_nondominated(MOI.MIN_SENSE, copy(solutions)) @test new_solutions == reverse(solutions) return