From 39f96966fce6bbbc3385c4b6f3deb523786cd878 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 11 Dec 2025 21:05:33 +1300 Subject: [PATCH 1/6] WIP: have customized logging for each algorithm --- Project.toml | 6 ++-- src/MultiObjectiveAlgorithms.jl | 46 ++++++++++++++++++++++++++++- src/algorithms/Chalmet.jl | 5 ++++ src/algorithms/Dichotomy.jl | 1 + src/algorithms/DominguezRios.jl | 3 ++ src/algorithms/EpsilonConstraint.jl | 1 + src/algorithms/Hierarchical.jl | 2 ++ src/algorithms/KirlikSayin.jl | 8 +++++ src/algorithms/Lexicographic.jl | 2 ++ src/algorithms/RandomWeighting.jl | 2 ++ src/algorithms/TambyVanderpooten.jl | 3 ++ 11 files changed, 76 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index c7005f7..53738ce 100644 --- a/Project.toml +++ b/Project.toml @@ -1,11 +1,12 @@ name = "MultiObjectiveAlgorithms" uuid = "0327d340-17cd-11ea-3e99-2fd5d98cecda" -authors = ["Oscar Dowson "] version = "1.7.0" +authors = ["Oscar Dowson "] [deps] Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" [weakdeps] Polyhedra = "67491407-f73d-577b-9b50-8179a7c68029" @@ -20,6 +21,7 @@ Ipopt = "1" JSON = "1" MathOptInterface = "1.19" Polyhedra = "0.8" +Printf = "1.11.0" Test = "1" julia = "1.10" @@ -27,8 +29,8 @@ julia = "1.10" 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" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] test = ["HiGHS", "Ipopt", "JSON", "Test", "Polyhedra"] diff --git a/src/MultiObjectiveAlgorithms.jl b/src/MultiObjectiveAlgorithms.jl index 8d25df0..24d9c29 100644 --- a/src/MultiObjectiveAlgorithms.jl +++ b/src/MultiObjectiveAlgorithms.jl @@ -170,6 +170,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer f::Union{Nothing,MOI.AbstractVectorFunction} solutions::Vector{SolutionPoint} termination_status::MOI.TerminationStatusCode + silent::Bool time_limit_sec::Union{Nothing,Float64} solve_time::Float64 ideal_point::Vector{Float64} @@ -178,12 +179,17 @@ mutable struct Optimizer <: MOI.AbstractOptimizer optimizer_factory::Any function Optimizer(optimizer_factory) + inner = MOI.instantiate(optimizer_factory; with_cache_type = Float64) + if MOI.supports(inner, MOI.Silent()) + MOI.set(inner, MOI.Silent(), true) + end return new( - MOI.instantiate(optimizer_factory; with_cache_type = Float64), + inner, nothing, nothing, SolutionPoint[], MOI.OPTIMIZE_NOT_CALLED, + false, nothing, NaN, Float64[], @@ -220,6 +226,17 @@ function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) return MOI.Utilities.default_copy_to(dest, src) end +### Silent + +MOI.supports(::Optimizer, ::MOI.Silent) = true + +MOI.get(model::Optimizer, ::MOI.Silent) = model.silent + +function MOI.set(model::Optimizer, ::MOI.Silent, value::Bool) + model.silent = value + return +end + ### TimeLimitSec function MOI.supports(model::Optimizer, attr::MOI.TimeLimitSec) @@ -628,6 +645,8 @@ function MOI.delete(model::Optimizer, ci::MOI.ConstraintIndex) return end +import Printf + """ optimize_inner!(model::Optimizer) @@ -646,6 +665,21 @@ function optimize_inner!(model::Optimizer) return end +function _log_solution(model::Optimizer, Y) + if model.silent + return + end + print(_format(model.subproblem_count)) + for y in Y + print(" ", _format(y)) + end + println() + return +end +_format(x::Int) = Printf.@sprintf("%5d", x) +_format(x::Float64) = Printf.@sprintf("% .5e", x) +_format(::Nothing) = " " + function _compute_ideal_point(model::Optimizer, start_time) for (i, f) in enumerate(MOI.Utilities.eachscalar(model.f)) if _check_premature_termination(model, start_time) !== nothing @@ -764,6 +798,13 @@ function _optimize!(model::Optimizer) empty!(model.ideal_point) return end + if !model.silent + print("Iter.") + for i in 1:MOI.output_dimension(model.f) + print(lpad("Obj. $i", 13)) + end + println() + end # We need to clear the ideal point prior to starting the solve. Algorithms # may update this during the solve, otherwise we will update it at the end. model.ideal_point = fill(NaN, MOI.output_dimension(model.f)) @@ -774,6 +815,9 @@ function _optimize!(model::Optimizer) model.solutions = solutions _sort!(model.solutions, MOI.get(model, MOI.ObjectiveSense())) end + if !model.silent + println("Found $(length(model.solutions)) solutions") + end if MOI.get(model, ComputeIdealPoint()) _compute_ideal_point(model, start_time) end diff --git a/src/algorithms/Chalmet.jl b/src/algorithms/Chalmet.jl index 4b8a662..adb753f 100644 --- a/src/algorithms/Chalmet.jl +++ b/src/algorithms/Chalmet.jl @@ -43,6 +43,7 @@ function _solve_constrained_model( end variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) X, Y = _compute_point(model, variables, model.f) + _log_solution(model, Y) MOI.delete.(model, c) return status, SolutionPoint(X, Y) end @@ -66,6 +67,7 @@ function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer) return status, solutions end _, y1[2] = _compute_point(model, variables, f2) + _log_solution(model, y1[2]) MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f1)}(), f1) y1_constraint = MOI.Utilities.normalize_and_add_constraint( model.inner, @@ -78,6 +80,7 @@ function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer) return status, solutions end x1, y1[1] = _compute_point(model, variables, f1) + _log_solution(model, y1[1]) MOI.delete(model.inner, y1_constraint) push!(solutions, SolutionPoint(x1, y1)) MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f1)}(), f1) @@ -87,6 +90,7 @@ function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer) return status, solutions end _, y2[1] = _compute_point(model, variables, f1) + _log_solution(model, y2[1]) if y2[1] ≈ solutions[1].y[1] return MOI.OPTIMAL, solutions end @@ -102,6 +106,7 @@ function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer) return status, solutions end x2, y2[2] = _compute_point(model, variables, f2) + _log_solution(model, y2[2]) MOI.delete(model.inner, y2_constraint) push!(solutions, SolutionPoint(x2, y2)) push!(Q, (1, 2)) diff --git a/src/algorithms/Dichotomy.jl b/src/algorithms/Dichotomy.jl index b55e244..2e4050a 100644 --- a/src/algorithms/Dichotomy.jl +++ b/src/algorithms/Dichotomy.jl @@ -74,6 +74,7 @@ function _solve_weighted_sum( end variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) X, Y = _compute_point(model, variables, model.f) + _log_solution(model, Y) return status, SolutionPoint(X, Y) end diff --git a/src/algorithms/DominguezRios.jl b/src/algorithms/DominguezRios.jl index 1e0d0d7..ee36cdd 100644 --- a/src/algorithms/DominguezRios.jl +++ b/src/algorithms/DominguezRios.jl @@ -166,6 +166,7 @@ function minimize_multiobjective!(algorithm::DominguezRios, model::Optimizer) return status, nothing end _, Y = _compute_point(model, variables, f_i) + _log_solution(model, Y) yI[i] = Y model.ideal_point[i] = Y MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) @@ -176,6 +177,7 @@ function minimize_multiobjective!(algorithm::DominguezRios, model::Optimizer) return status, nothing end _, Y = _compute_point(model, variables, f_i) + _log_solution(model, Y) yN[i] = Y + 1 end MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) @@ -216,6 +218,7 @@ function minimize_multiobjective!(algorithm::DominguezRios, model::Optimizer) optimize_inner!(model) if _is_scalar_status_optimal(model) X, Y = _compute_point(model, variables, model.f) + _log_solution(model, Y) obj = MOI.get(model.inner, MOI.ObjectiveValue()) # We need to undo the scaling of the scalar objective. There's no # need to unscale `Y` because we have evaluated this explicitly from diff --git a/src/algorithms/EpsilonConstraint.jl b/src/algorithms/EpsilonConstraint.jl index d98c043..044f20f 100644 --- a/src/algorithms/EpsilonConstraint.jl +++ b/src/algorithms/EpsilonConstraint.jl @@ -127,6 +127,7 @@ function minimize_multiobjective!( break end X, Y = _compute_point(model, variables, model.f) + _log_solution(model, Y) if isempty(solutions) || !(Y ≈ solutions[end].y) push!(solutions, SolutionPoint(X, Y)) end diff --git a/src/algorithms/Hierarchical.jl b/src/algorithms/Hierarchical.jl index 8684a24..004d99e 100644 --- a/src/algorithms/Hierarchical.jl +++ b/src/algorithms/Hierarchical.jl @@ -114,6 +114,7 @@ function minimize_multiobjective!(algorithm::Hierarchical, model::Optimizer) end # Add tolerance constraints X, Y = _compute_point(model, variables, new_vector_f) + _log_solution(model, Y) for (i, fi) in enumerate(MOI.Utilities.eachscalar(new_vector_f)) rtol = MOI.get(algorithm, ObjectiveRelativeTolerance(i)) set = MOI.LessThan(Y[i] + rtol * abs(Y[i])) @@ -122,6 +123,7 @@ function minimize_multiobjective!(algorithm::Hierarchical, model::Optimizer) end end X, Y = _compute_point(model, variables, model.f) + _log_solution(model, Y) # Remove tolerance constraints for c in constraints MOI.delete(model, c) diff --git a/src/algorithms/KirlikSayin.jl b/src/algorithms/KirlikSayin.jl index 05e080f..b913e82 100644 --- a/src/algorithms/KirlikSayin.jl +++ b/src/algorithms/KirlikSayin.jl @@ -96,6 +96,8 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) # This tolerance is really important! δ = 1.0 scalars = MOI.Utilities.scalarize(model.f) + printing = Vector{Union{Nothing,Float64}}(undef, n) + fill!(printing, nothing) # Ideal and Nadir point estimation for (i, f_i) in enumerate(scalars) # Ideal point @@ -106,6 +108,8 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) return status, nothing end _, Y = _compute_point(model, variables, f_i) + printing[i] = Y + _log_solution(model, printing) model.ideal_point[i] = yI[i] = Y # Nadir point MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) @@ -118,6 +122,9 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) return status, nothing end _, Y = _compute_point(model, variables, f_i) + printing[i] = Y + _log_solution(model, printing) + printing[i] = nothing yN[i] = Y + δ MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) end @@ -180,6 +187,7 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) continue end X, Y = _compute_point(model, variables, model.f) + _log_solution(model, Y) Y_proj = _project(Y, k) if !(Y in YN) push!(solutions, SolutionPoint(X, Y)) diff --git a/src/algorithms/Lexicographic.jl b/src/algorithms/Lexicographic.jl index d81b139..dac70a1 100644 --- a/src/algorithms/Lexicographic.jl +++ b/src/algorithms/Lexicographic.jl @@ -139,12 +139,14 @@ function _solve_in_sequence( primal_status = MOI.get(model.inner, MOI.PrimalStatus()) if _is_scalar_status_feasible_point(primal_status) X, Y = _compute_point(model, variables, model.f) + _log_solution(model, Y) solution = [SolutionPoint(X, Y)] end if !_is_scalar_status_optimal(status) break end X, Y = _compute_point(model, variables, f) + _log_solution(model, Y) rtol = MOI.get(algorithm, ObjectiveRelativeTolerance(i)) set = if MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE MOI.LessThan(Y + rtol * abs(Y)) diff --git a/src/algorithms/RandomWeighting.jl b/src/algorithms/RandomWeighting.jl index 68bf7fb..5daec00 100644 --- a/src/algorithms/RandomWeighting.jl +++ b/src/algorithms/RandomWeighting.jl @@ -54,6 +54,7 @@ function optimize_multiobjective!(algorithm::RandomWeighting, model::Optimizer) status = MOI.get(model.inner, MOI.TerminationStatus()) if _is_scalar_status_optimal(status) X, Y = _compute_point(model, variables, model.f) + _log_solution(model, Y) push!(solutions, SolutionPoint(X, Y)) else return status, nothing @@ -75,6 +76,7 @@ function optimize_multiobjective!(algorithm::RandomWeighting, model::Optimizer) status = MOI.get(model.inner, MOI.TerminationStatus()) if _is_scalar_status_optimal(status) X, Y = _compute_point(model, variables, model.f) + _log_solution(model, Y) push!(solutions, SolutionPoint(X, Y)) end end diff --git a/src/algorithms/TambyVanderpooten.jl b/src/algorithms/TambyVanderpooten.jl index ef89e03..0a0b949 100644 --- a/src/algorithms/TambyVanderpooten.jl +++ b/src/algorithms/TambyVanderpooten.jl @@ -112,6 +112,7 @@ function minimize_multiobjective!( return status, nothing end _, Y = _compute_point(model, variables, f_i) + _log_solution(model, Y) yI[i] = Y model.ideal_point[i] = Y MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) @@ -122,6 +123,7 @@ function minimize_multiobjective!( return status, nothing end _, Y = _compute_point(model, variables, f_i) + _log_solution(model, Y) yN[i] = Y + 1 end MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) @@ -186,6 +188,7 @@ function minimize_multiobjective!( return status, nothing end X, Y = _compute_point(model, variables, model.f) + _log_solution(model, Y) MOI.delete.(model, ε_constraints) MOI.delete(model, y_k_constraint) push!(V[k], (u, Y)) From 4e097b6f8c9471181293c34766ad9bb15946dd7f Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 12 Dec 2025 13:28:49 +1300 Subject: [PATCH 2/6] Update --- Project.toml | 2 +- ext/MultiObjectiveAlgorithmsPolyhedraExt.jl | 3 +- src/MultiObjectiveAlgorithms.jl | 65 ++++++++++++++++----- src/algorithms/Chalmet.jl | 12 ++-- src/algorithms/Dichotomy.jl | 5 +- src/algorithms/DominguezRios.jl | 8 +-- src/algorithms/EpsilonConstraint.jl | 3 +- src/algorithms/Hierarchical.jl | 5 +- src/algorithms/KirlikSayin.jl | 14 ++--- src/algorithms/Lexicographic.jl | 9 +-- src/algorithms/RandomWeighting.jl | 3 +- src/algorithms/TambyVanderpooten.jl | 8 +-- 12 files changed, 82 insertions(+), 55 deletions(-) diff --git a/Project.toml b/Project.toml index 53738ce..6d324f5 100644 --- a/Project.toml +++ b/Project.toml @@ -21,7 +21,7 @@ Ipopt = "1" JSON = "1" MathOptInterface = "1.19" Polyhedra = "0.8" -Printf = "1.11.0" +Printf = "1.10.0" Test = "1" julia = "1.10" diff --git a/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl index efaadc9..b6c1601 100644 --- a/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl +++ b/ext/MultiObjectiveAlgorithmsPolyhedraExt.jl @@ -39,7 +39,6 @@ function MOA.minimize_multiobjective!( model::MOA.Optimizer, ) @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) @@ -102,7 +101,7 @@ function MOA.minimize_multiobjective!( H = _halfspaces(IPS) count = 0 while !isempty(H) - ret = MOA._check_premature_termination(model, start_time) + ret = MOA._check_premature_termination(model) if ret !== nothing status = ret break diff --git a/src/MultiObjectiveAlgorithms.jl b/src/MultiObjectiveAlgorithms.jl index 24d9c29..e9d9255 100644 --- a/src/MultiObjectiveAlgorithms.jl +++ b/src/MultiObjectiveAlgorithms.jl @@ -172,6 +172,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer termination_status::MOI.TerminationStatusCode silent::Bool time_limit_sec::Union{Nothing,Float64} + start_time::Float64 solve_time::Float64 ideal_point::Vector{Float64} compute_ideal_point::Bool @@ -192,6 +193,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer false, nothing, NaN, + NaN, Float64[], _default(ComputeIdealPoint()), 0, @@ -206,6 +208,7 @@ function MOI.empty!(model::Optimizer) empty!(model.solutions) model.termination_status = MOI.OPTIMIZE_NOT_CALLED model.solve_time = NaN + model.start_time = NaN empty!(model.ideal_point) model.subproblem_count = 0 return @@ -665,24 +668,56 @@ function optimize_inner!(model::Optimizer) return end +""" + _log_solution(model::Optimizer, variables::Vector{MOI.VariableIndex}) + +Log the solution. We don't have a pre-computed point, so compute one from the +variable values. +""" +function _log_solution(model::Optimizer, variables::Vector{MOI.VariableIndex}) + if !model.silent + _, Y = _compute_point(model, variables, model.f) + _log_solution(model, Y) + end + return +end + +""" + _log_solution(model::Optimizer, variables::Vector{MOI.VariableIndex}) + +Log the solution. We have a pre-computed point. +""" function _log_solution(model::Optimizer, Y) - if model.silent - return + if !model.silent + print(_format(model.subproblem_count)) + for y in Y + print(" ", _format(y)) + end + println(" ", _format(time() - model.start_time)) end - print(_format(model.subproblem_count)) - for y in Y - print(" ", _format(y)) + return +end +""" + _log_solution(model::Optimizer, msg::String) + +Log the solution. Assume the subproblem failed to solve. +""" +function _log_solution(model::Optimizer, msg::String) + if !model.silent + print(_format(model.subproblem_count), " ") + print(rpad(msg, 13 * MOI.output_dimension(model.f) - 3)) + println(" ", _format(time() - model.start_time)) end - println() return end + _format(x::Int) = Printf.@sprintf("%5d", x) _format(x::Float64) = Printf.@sprintf("% .5e", x) _format(::Nothing) = " " -function _compute_ideal_point(model::Optimizer, start_time) +function _compute_ideal_point(model::Optimizer) for (i, f) in enumerate(MOI.Utilities.eachscalar(model.f)) - if _check_premature_termination(model, start_time) !== nothing + if _check_premature_termination(model) !== nothing return end if !isnan(model.ideal_point[i]) @@ -767,11 +802,11 @@ function _check_interrupt(f) end end -function _check_premature_termination(model::Optimizer, start_time::Float64) +function _check_premature_termination(model::Optimizer) return _check_interrupt() do time_limit = MOI.get(model, MOI.TimeLimitSec()) if time_limit !== nothing - time_remaining = time_limit - (time() - start_time) + time_remaining = time_limit - (time() - model.start_time) if time_remaining <= 0 return MOI.TIME_LIMIT end @@ -789,7 +824,7 @@ function MOI.optimize!(model::Optimizer) end function _optimize!(model::Optimizer) - start_time = time() + model.start_time = time() empty!(model.solutions) model.termination_status = MOI.OPTIMIZE_NOT_CALLED model.subproblem_count = 0 @@ -801,9 +836,9 @@ function _optimize!(model::Optimizer) if !model.silent print("Iter.") for i in 1:MOI.output_dimension(model.f) - print(lpad("Obj. $i", 13)) + print(lpad("Obj. $i ", 13)) end - println() + println(" Time ") end # We need to clear the ideal point prior to starting the solve. Algorithms # may update this during the solve, otherwise we will update it at the end. @@ -819,12 +854,12 @@ function _optimize!(model::Optimizer) println("Found $(length(model.solutions)) solutions") end if MOI.get(model, ComputeIdealPoint()) - _compute_ideal_point(model, start_time) + _compute_ideal_point(model) end if MOI.supports(model.inner, MOI.TimeLimitSec()) MOI.set(model.inner, MOI.TimeLimitSec(), nothing) end - model.solve_time = time() - start_time + model.solve_time = time() - model.start_time return end diff --git a/src/algorithms/Chalmet.jl b/src/algorithms/Chalmet.jl index adb753f..535591b 100644 --- a/src/algorithms/Chalmet.jl +++ b/src/algorithms/Chalmet.jl @@ -38,6 +38,7 @@ function _solve_constrained_model( optimize_inner!(model) status = MOI.get(model.inner, MOI.TerminationStatus()) if !_is_scalar_status_optimal(status) + _log_solution(model, "subproblem not optimal") MOI.delete.(model, c) return status, nothing end @@ -50,7 +51,6 @@ end function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer) @assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE - start_time = time() if MOI.output_dimension(model.f) != 2 error("Chalmet requires exactly two objectives") end @@ -67,7 +67,7 @@ function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer) return status, solutions end _, y1[2] = _compute_point(model, variables, f2) - _log_solution(model, y1[2]) + _log_solution(model, variables) MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f1)}(), f1) y1_constraint = MOI.Utilities.normalize_and_add_constraint( model.inner, @@ -80,7 +80,7 @@ function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer) return status, solutions end x1, y1[1] = _compute_point(model, variables, f1) - _log_solution(model, y1[1]) + _log_solution(model, y1) MOI.delete(model.inner, y1_constraint) push!(solutions, SolutionPoint(x1, y1)) MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f1)}(), f1) @@ -90,7 +90,7 @@ function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer) return status, solutions end _, y2[1] = _compute_point(model, variables, f1) - _log_solution(model, y2[1]) + _log_solution(model, variables) if y2[1] ≈ solutions[1].y[1] return MOI.OPTIMAL, solutions end @@ -106,13 +106,13 @@ function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer) return status, solutions end x2, y2[2] = _compute_point(model, variables, f2) - _log_solution(model, y2[2]) + _log_solution(model, y2) MOI.delete(model.inner, y2_constraint) push!(solutions, SolutionPoint(x2, y2)) push!(Q, (1, 2)) t = 3 while !isempty(Q) - if (ret = _check_premature_termination(model, start_time)) !== nothing + if (ret = _check_premature_termination(model)) !== nothing return ret, solutions end r, s = pop!(Q) diff --git a/src/algorithms/Dichotomy.jl b/src/algorithms/Dichotomy.jl index 2e4050a..78f6e61 100644 --- a/src/algorithms/Dichotomy.jl +++ b/src/algorithms/Dichotomy.jl @@ -79,12 +79,11 @@ function _solve_weighted_sum( end function optimize_multiobjective!(algorithm::Dichotomy, model::Optimizer) - start_time = time() if MOI.output_dimension(model.f) > 2 error("Only scalar or bi-objective problems supported.") end if MOI.output_dimension(model.f) == 1 - if (ret = _check_premature_termination(model, start_time)) !== nothing + if (ret = _check_premature_termination(model)) !== nothing return ret, nothing end status, solution = _solve_weighted_sum(model, algorithm, [1.0]) @@ -107,7 +106,7 @@ function optimize_multiobjective!(algorithm::Dichotomy, model::Optimizer) limit = MOI.get(algorithm, SolutionLimit()) status = MOI.OPTIMAL while length(queue) > 0 && length(solutions) < limit - if (ret = _check_premature_termination(model, start_time)) !== nothing + if (ret = _check_premature_termination(model)) !== nothing status = ret break end diff --git a/src/algorithms/DominguezRios.jl b/src/algorithms/DominguezRios.jl index ee36cdd..bc72b3c 100644 --- a/src/algorithms/DominguezRios.jl +++ b/src/algorithms/DominguezRios.jl @@ -150,7 +150,6 @@ end function minimize_multiobjective!(algorithm::DominguezRios, model::Optimizer) @assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE - start_time = time() n = MOI.output_dimension(model.f) L = [_DominguezRiosBox[] for i in 1:n] scalars = MOI.Utilities.scalarize(model.f) @@ -166,7 +165,7 @@ function minimize_multiobjective!(algorithm::DominguezRios, model::Optimizer) return status, nothing end _, Y = _compute_point(model, variables, f_i) - _log_solution(model, Y) + _log_solution(model, variables) yI[i] = Y model.ideal_point[i] = Y MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) @@ -177,7 +176,7 @@ function minimize_multiobjective!(algorithm::DominguezRios, model::Optimizer) return status, nothing end _, Y = _compute_point(model, variables, f_i) - _log_solution(model, Y) + _log_solution(model, variables) yN[i] = Y + 1 end MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) @@ -192,7 +191,7 @@ function minimize_multiobjective!(algorithm::DominguezRios, model::Optimizer) k = 0 status = MOI.OPTIMAL while any(!isempty(l) for l in L) - if (ret = _check_premature_termination(model, start_time)) !== nothing + if (ret = _check_premature_termination(model)) !== nothing status = ret break end @@ -236,6 +235,7 @@ function minimize_multiobjective!(algorithm::DominguezRios, model::Optimizer) # fail and return something like OTHER_ERROR (e.g., because the # numerics are challenging). Rather than error completely, let's # just skip this box. + _log_solution(model, "subproblem not optimal") deleteat!(L[k], i) end MOI.delete.(model.inner, constraints) diff --git a/src/algorithms/EpsilonConstraint.jl b/src/algorithms/EpsilonConstraint.jl index 044f20f..dde6580 100644 --- a/src/algorithms/EpsilonConstraint.jl +++ b/src/algorithms/EpsilonConstraint.jl @@ -76,7 +76,6 @@ function minimize_multiobjective!( model::Optimizer, ) @assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE - start_time = time() if MOI.output_dimension(model.f) != 2 error("EpsilonConstraint requires exactly two objectives") end @@ -117,7 +116,7 @@ function minimize_multiobjective!( bound -= constant status = MOI.OPTIMAL for _ in 3:n_points - if (ret = _check_premature_termination(model, start_time)) !== nothing + if (ret = _check_premature_termination(model)) !== nothing status = ret break end diff --git a/src/algorithms/Hierarchical.jl b/src/algorithms/Hierarchical.jl index 004d99e..8fd5542 100644 --- a/src/algorithms/Hierarchical.jl +++ b/src/algorithms/Hierarchical.jl @@ -112,9 +112,12 @@ function minimize_multiobjective!(algorithm::Hierarchical, model::Optimizer) if round == length(objective_subsets) break end + if !model.silent + X, Y = _compute_point(model, variables, model.f) + _log_solution(model, Y) + end # Add tolerance constraints X, Y = _compute_point(model, variables, new_vector_f) - _log_solution(model, Y) for (i, fi) in enumerate(MOI.Utilities.eachscalar(new_vector_f)) rtol = MOI.get(algorithm, ObjectiveRelativeTolerance(i)) set = MOI.LessThan(Y[i] + rtol * abs(Y[i])) diff --git a/src/algorithms/KirlikSayin.jl b/src/algorithms/KirlikSayin.jl index b913e82..9829036 100644 --- a/src/algorithms/KirlikSayin.jl +++ b/src/algorithms/KirlikSayin.jl @@ -83,7 +83,6 @@ end function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) @assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE - start_time = time() solutions = SolutionPoint[] # Problem with p objectives. # Set k = 1, meaning the nondominated points will get projected @@ -96,8 +95,6 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) # This tolerance is really important! δ = 1.0 scalars = MOI.Utilities.scalarize(model.f) - printing = Vector{Union{Nothing,Float64}}(undef, n) - fill!(printing, nothing) # Ideal and Nadir point estimation for (i, f_i) in enumerate(scalars) # Ideal point @@ -108,8 +105,7 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) return status, nothing end _, Y = _compute_point(model, variables, f_i) - printing[i] = Y - _log_solution(model, printing) + _log_solution(model, variables) model.ideal_point[i] = yI[i] = Y # Nadir point MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) @@ -122,16 +118,14 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) return status, nothing end _, Y = _compute_point(model, variables, f_i) - printing[i] = Y - _log_solution(model, printing) - printing[i] = nothing + _log_solution(model, variables) yN[i] = Y + δ MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) end L = [_Rectangle(_project(yI, k), _project(yN, k))] status = MOI.OPTIMAL while !isempty(L) - if (ret = _check_premature_termination(model, start_time)) !== nothing + if (ret = _check_premature_termination(model)) !== nothing status = ret break end @@ -159,6 +153,7 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) push!(ε_constraints, ci) end optimize_inner!(model) + _log_solution(model, "auxillary subproblem") if !_is_scalar_status_optimal(model) # If this fails, it likely means that the solver experienced a # numerical error with this box. Just skip it. @@ -179,6 +174,7 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) ) optimize_inner!(model) if !_is_scalar_status_optimal(model) + _log_solution(model, "subproblem not optimal") # If this fails, it likely means that the solver experienced a # numerical error with this box. Just skip it. MOI.delete.(model, ε_constraints) diff --git a/src/algorithms/Lexicographic.jl b/src/algorithms/Lexicographic.jl index dac70a1..ea8d9dd 100644 --- a/src/algorithms/Lexicographic.jl +++ b/src/algorithms/Lexicographic.jl @@ -69,11 +69,10 @@ function MOI.set(alg::Lexicographic, ::LexicographicAllPermutations, val::Bool) end function optimize_multiobjective!(algorithm::Lexicographic, model::Optimizer) - start_time = time() sequence = 1:MOI.output_dimension(model.f) perm = MOI.get(algorithm, LexicographicAllPermutations()) if !something(perm, _default(LexicographicAllPermutations())) - return _solve_in_sequence(algorithm, model, sequence, start_time) + return _solve_in_sequence(algorithm, model, sequence) end if perm === nothing && length(sequence) >= 5 o, n = length(sequence), factorial(length(sequence)) @@ -104,7 +103,7 @@ function optimize_multiobjective!(algorithm::Lexicographic, model::Optimizer) status = MOI.OPTIMAL for sequence in Combinatorics.permutations(sequence) status, solution = - _solve_in_sequence(algorithm, model, sequence, start_time) + _solve_in_sequence(algorithm, model, sequence) if !isempty(solution) push!(solutions, solution[1]) end @@ -120,7 +119,6 @@ function _solve_in_sequence( algorithm::Lexicographic, model::Optimizer, sequence::AbstractVector{Int}, - start_time::Float64, ) variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) constraints = Any[] @@ -128,7 +126,7 @@ function _solve_in_sequence( solution = SolutionPoint[] status = MOI.OPTIMAL for i in sequence - if (ret = _check_premature_termination(model, start_time)) !== nothing + if (ret = _check_premature_termination(model)) !== nothing status = ret break end @@ -146,7 +144,6 @@ function _solve_in_sequence( break end X, Y = _compute_point(model, variables, f) - _log_solution(model, Y) rtol = MOI.get(algorithm, ObjectiveRelativeTolerance(i)) set = if MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE MOI.LessThan(Y + rtol * abs(Y)) diff --git a/src/algorithms/RandomWeighting.jl b/src/algorithms/RandomWeighting.jl index 5daec00..716bb64 100644 --- a/src/algorithms/RandomWeighting.jl +++ b/src/algorithms/RandomWeighting.jl @@ -43,7 +43,6 @@ function optimize_multiobjective!(algorithm::RandomWeighting, model::Optimizer) algorithm.solution_limit === nothing error("At least `MOI.TimeLimitSec` or `MOA.SolutionLimit` must be set") end - start_time = time() solutions = SolutionPoint[] sense = MOI.get(model, MOI.ObjectiveSense()) P = MOI.output_dimension(model.f) @@ -65,7 +64,7 @@ function optimize_multiobjective!(algorithm::RandomWeighting, model::Optimizer) # * then the outer loop goes again while length(solutions) < MOI.get(algorithm, SolutionLimit()) while length(solutions) < MOI.get(algorithm, SolutionLimit()) - ret = _check_premature_termination(model, start_time) + ret = _check_premature_termination(model) if ret !== nothing return ret, filter_nondominated(sense, solutions) end diff --git a/src/algorithms/TambyVanderpooten.jl b/src/algorithms/TambyVanderpooten.jl index 0a0b949..b83fe5b 100644 --- a/src/algorithms/TambyVanderpooten.jl +++ b/src/algorithms/TambyVanderpooten.jl @@ -93,7 +93,6 @@ function minimize_multiobjective!( model::Optimizer, ) @assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE - start_time = time() warm_start_supported = false if MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex) warm_start_supported = true @@ -112,7 +111,7 @@ function minimize_multiobjective!( return status, nothing end _, Y = _compute_point(model, variables, f_i) - _log_solution(model, Y) + _log_solution(model, variables) yI[i] = Y model.ideal_point[i] = Y MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) @@ -123,7 +122,7 @@ function minimize_multiobjective!( return status, nothing end _, Y = _compute_point(model, variables, f_i) - _log_solution(model, Y) + _log_solution(model, variables) yN[i] = Y + 1 end MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) @@ -132,7 +131,7 @@ function minimize_multiobjective!( U_N[yN] = [[_get_child(yN, yI, k)] for k in 1:n] status = MOI.OPTIMAL while !isempty(U_N) - if (ret = _check_premature_termination(model, start_time)) !== nothing + if (ret = _check_premature_termination(model)) !== nothing status = ret break end @@ -167,6 +166,7 @@ function minimize_multiobjective!( end end optimize_inner!(model) + _log_solution(model, "auxillary subproblem") status = MOI.get(model.inner, MOI.TerminationStatus()) if !_is_scalar_status_optimal(status) MOI.delete.(model, ε_constraints) From 610d7a6334ef575dcb3b162ad7673aab22f53e10 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 12 Dec 2025 13:44:44 +1300 Subject: [PATCH 3/6] Update --- src/MultiObjectiveAlgorithms.jl | 23 ++++++++++++++++------- src/algorithms/Chalmet.jl | 2 ++ src/algorithms/Dichotomy.jl | 2 ++ src/algorithms/DominguezRios.jl | 2 ++ src/algorithms/EpsilonConstraint.jl | 2 ++ src/algorithms/Hierarchical.jl | 2 ++ src/algorithms/KirlikSayin.jl | 2 ++ src/algorithms/Lexicographic.jl | 5 +++-- src/algorithms/RandomWeighting.jl | 2 ++ src/algorithms/Sandwiching.jl | 2 ++ src/algorithms/TambyVanderpooten.jl | 2 ++ 11 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/MultiObjectiveAlgorithms.jl b/src/MultiObjectiveAlgorithms.jl index e9d9255..284c763 100644 --- a/src/MultiObjectiveAlgorithms.jl +++ b/src/MultiObjectiveAlgorithms.jl @@ -7,6 +7,7 @@ module MultiObjectiveAlgorithms import Combinatorics import MathOptInterface as MOI +import Printf """ struct SolutionPoint @@ -648,8 +649,6 @@ function MOI.delete(model::Optimizer, ci::MOI.ConstraintIndex) return end -import Printf - """ optimize_inner!(model::Optimizer) @@ -689,7 +688,7 @@ Log the solution. We have a pre-computed point. """ function _log_solution(model::Optimizer, Y) if !model.silent - print(_format(model.subproblem_count)) + print(_format(model.subproblem_count), " ") for y in Y print(" ", _format(y)) end @@ -704,8 +703,8 @@ Log the solution. Assume the subproblem failed to solve. """ function _log_solution(model::Optimizer, msg::String) if !model.silent - print(_format(model.subproblem_count), " ") - print(rpad(msg, 13 * MOI.output_dimension(model.f) - 3)) + print(_format(model.subproblem_count), " ") + print(rpad(msg, 13 * MOI.output_dimension(model.f))) println(" ", _format(time() - model.start_time)) end return @@ -834,11 +833,18 @@ function _optimize!(model::Optimizer) return end if !model.silent - print("Iter.") + rule = "-"^(7 + 13 * (MOI.output_dimension(model.f) + 1)) + println(rule) + println(" MultiObjectiveAlgorithms.jl") + println(rule) + println("Algorithm: ", _describe(model.algorithm)) + println(rule) + print("solve #") for i in 1:MOI.output_dimension(model.f) print(lpad("Obj. $i ", 13)) end println(" Time ") + println(rule) end # We need to clear the ideal point prior to starting the solve. Algorithms # may update this during the solve, otherwise we will update it at the end. @@ -851,7 +857,10 @@ function _optimize!(model::Optimizer) _sort!(model.solutions, MOI.get(model, MOI.ObjectiveSense())) end if !model.silent - println("Found $(length(model.solutions)) solutions") + println(rule) + println("Terminating with status: ", status) + println("Number of non-dominated solutions: ", length(model.solutions)) + println(rule) end if MOI.get(model, ComputeIdealPoint()) _compute_ideal_point(model) diff --git a/src/algorithms/Chalmet.jl b/src/algorithms/Chalmet.jl index 535591b..4359a01 100644 --- a/src/algorithms/Chalmet.jl +++ b/src/algorithms/Chalmet.jl @@ -25,6 +25,8 @@ This algorithm is restricted to problems with: """ struct Chalmet <: AbstractAlgorithm end +_describe(::Chalmet) = "Chalmet()" + function _solve_constrained_model( model::Optimizer, ::Chalmet, diff --git a/src/algorithms/Dichotomy.jl b/src/algorithms/Dichotomy.jl index 78f6e61..5b1c3a9 100644 --- a/src/algorithms/Dichotomy.jl +++ b/src/algorithms/Dichotomy.jl @@ -30,6 +30,8 @@ mutable struct Dichotomy <: AbstractAlgorithm Dichotomy() = new(nothing) end +_describe(::Dichotomy) = "Dichotomy()" + """ NISE() diff --git a/src/algorithms/DominguezRios.jl b/src/algorithms/DominguezRios.jl index bc72b3c..b2f0c84 100644 --- a/src/algorithms/DominguezRios.jl +++ b/src/algorithms/DominguezRios.jl @@ -26,6 +26,8 @@ This algorithm is restricted to problems with: """ struct DominguezRios <: AbstractAlgorithm end +_describe(::DominguezRios) = "DominguezRios()" + mutable struct _DominguezRiosBox l::Vector{Float64} u::Vector{Float64} diff --git a/src/algorithms/EpsilonConstraint.jl b/src/algorithms/EpsilonConstraint.jl index dde6580..b2bd1ca 100644 --- a/src/algorithms/EpsilonConstraint.jl +++ b/src/algorithms/EpsilonConstraint.jl @@ -36,6 +36,8 @@ mutable struct EpsilonConstraint <: AbstractAlgorithm EpsilonConstraint() = new(nothing, nothing) end +_describe(::EpsilonConstraint) = "EpsilonConstraint()" + MOI.supports(::EpsilonConstraint, ::SolutionLimit) = true function MOI.set(alg::EpsilonConstraint, ::SolutionLimit, value) diff --git a/src/algorithms/Hierarchical.jl b/src/algorithms/Hierarchical.jl index 8fd5542..5bc4046 100644 --- a/src/algorithms/Hierarchical.jl +++ b/src/algorithms/Hierarchical.jl @@ -37,6 +37,8 @@ mutable struct Hierarchical <: AbstractAlgorithm Hierarchical() = new(Int[], Float64[], Float64[]) end +_describe(::Hierarchical) = "Hierarchical()" + MOI.supports(::Hierarchical, ::ObjectivePriority) = true function MOI.get(alg::Hierarchical, attr::ObjectivePriority) diff --git a/src/algorithms/KirlikSayin.jl b/src/algorithms/KirlikSayin.jl index 9829036..fbf3fbb 100644 --- a/src/algorithms/KirlikSayin.jl +++ b/src/algorithms/KirlikSayin.jl @@ -31,6 +31,8 @@ This algorithm is restricted to problems with: """ struct KirlikSayin <: AbstractAlgorithm end +_describe(::KirlikSayin) = "KirlikSayin()" + struct _Rectangle l::Vector{Float64} u::Vector{Float64} diff --git a/src/algorithms/Lexicographic.jl b/src/algorithms/Lexicographic.jl index ea8d9dd..b667a25 100644 --- a/src/algorithms/Lexicographic.jl +++ b/src/algorithms/Lexicographic.jl @@ -43,6 +43,8 @@ mutable struct Lexicographic <: AbstractAlgorithm end end +_describe(::Lexicographic) = "Lexicographic()" + MOI.supports(::Lexicographic, ::ObjectiveRelativeTolerance) = true function MOI.get(alg::Lexicographic, attr::ObjectiveRelativeTolerance) @@ -102,8 +104,7 @@ function optimize_multiobjective!(algorithm::Lexicographic, model::Optimizer) solutions = SolutionPoint[] status = MOI.OPTIMAL for sequence in Combinatorics.permutations(sequence) - status, solution = - _solve_in_sequence(algorithm, model, sequence) + status, solution = _solve_in_sequence(algorithm, model, sequence) if !isempty(solution) push!(solutions, solution[1]) end diff --git a/src/algorithms/RandomWeighting.jl b/src/algorithms/RandomWeighting.jl index 716bb64..af1ed1a 100644 --- a/src/algorithms/RandomWeighting.jl +++ b/src/algorithms/RandomWeighting.jl @@ -27,6 +27,8 @@ mutable struct RandomWeighting <: AbstractAlgorithm RandomWeighting() = new(nothing) end +_describe(::RandomWeighting) = "RandomWeighting()" + MOI.supports(::RandomWeighting, ::SolutionLimit) = true function MOI.set(alg::RandomWeighting, ::SolutionLimit, value) diff --git a/src/algorithms/Sandwiching.jl b/src/algorithms/Sandwiching.jl index 0008e71..6ec3002 100644 --- a/src/algorithms/Sandwiching.jl +++ b/src/algorithms/Sandwiching.jl @@ -28,3 +28,5 @@ algorithm = MOA.Sandwiching(0.0) mutable struct Sandwiching <: AbstractAlgorithm precision::Float64 end + +_describe(alg::Sandwiching) = "Sandwiching($(alg.precision))" diff --git a/src/algorithms/TambyVanderpooten.jl b/src/algorithms/TambyVanderpooten.jl index b83fe5b..b0debd0 100644 --- a/src/algorithms/TambyVanderpooten.jl +++ b/src/algorithms/TambyVanderpooten.jl @@ -32,6 +32,8 @@ This algorithm is restricted to problems with: """ struct TambyVanderpooten <: AbstractAlgorithm end +_describe(::TambyVanderpooten) = "TambyVanderpooten()" + function _update_search_region( U_N::Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}}, y::Vector{Float64}, From ec09399ba7977e0a38001fc18f04528d401178ea Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 12 Dec 2025 14:52:47 +1300 Subject: [PATCH 4/6] Update --- src/MultiObjectiveAlgorithms.jl | 141 +++++++++++++++------------- src/algorithms/Chalmet.jl | 14 ++- src/algorithms/Dichotomy.jl | 4 +- src/algorithms/DominguezRios.jl | 14 ++- src/algorithms/EpsilonConstraint.jl | 4 +- src/algorithms/Hierarchical.jl | 6 +- src/algorithms/KirlikSayin.jl | 12 +-- src/algorithms/Lexicographic.jl | 4 +- src/algorithms/RandomWeighting.jl | 6 +- src/algorithms/Sandwiching.jl | 2 - src/algorithms/TambyVanderpooten.jl | 10 +- test/test_model.jl | 29 ++++++ 12 files changed, 135 insertions(+), 111 deletions(-) diff --git a/src/MultiObjectiveAlgorithms.jl b/src/MultiObjectiveAlgorithms.jl index 284c763..7e87697 100644 --- a/src/MultiObjectiveAlgorithms.jl +++ b/src/MultiObjectiveAlgorithms.jl @@ -667,53 +667,6 @@ function optimize_inner!(model::Optimizer) return end -""" - _log_solution(model::Optimizer, variables::Vector{MOI.VariableIndex}) - -Log the solution. We don't have a pre-computed point, so compute one from the -variable values. -""" -function _log_solution(model::Optimizer, variables::Vector{MOI.VariableIndex}) - if !model.silent - _, Y = _compute_point(model, variables, model.f) - _log_solution(model, Y) - end - return -end - -""" - _log_solution(model::Optimizer, variables::Vector{MOI.VariableIndex}) - -Log the solution. We have a pre-computed point. -""" -function _log_solution(model::Optimizer, Y) - if !model.silent - print(_format(model.subproblem_count), " ") - for y in Y - print(" ", _format(y)) - end - println(" ", _format(time() - model.start_time)) - end - return -end -""" - _log_solution(model::Optimizer, msg::String) - -Log the solution. Assume the subproblem failed to solve. -""" -function _log_solution(model::Optimizer, msg::String) - if !model.silent - print(_format(model.subproblem_count), " ") - print(rpad(msg, 13 * MOI.output_dimension(model.f))) - println(" ", _format(time() - model.start_time)) - end - return -end - -_format(x::Int) = Printf.@sprintf("%5d", x) -_format(x::Float64) = Printf.@sprintf("% .5e", x) -_format(::Nothing) = " " - function _compute_ideal_point(model::Optimizer) for (i, f) in enumerate(MOI.Utilities.eachscalar(model.f)) if _check_premature_termination(model) !== nothing @@ -822,6 +775,82 @@ function MOI.optimize!(model::Optimizer) return end +function _print_header(io::IO, model::Optimizer) + rule = "-"^(7 + 13 * (MOI.output_dimension(model.f) + 1)) + println(io, rule) + println(io, " MultiObjectiveAlgorithms.jl") + println(io, rule) + println( + io, + "Algorithm: ", + replace( + string(typeof(model.algorithm)), + "MultiObjectiveAlgorithms." => "", + ), + ) + println(io, rule) + print(io, "solve #") + for i in 1:MOI.output_dimension(model.f) + print(io, lpad("Obj. $i ", 13)) + end + println(io, " Time ") + println(io, rule) + return +end + +function _print_footer(io::IO, model::Optimizer) + rule = "-"^(7 + 13 * (MOI.output_dimension(model.f) + 1)) + println(io, rule) + println(io, "TerminationStatus: ", model.termination_status) + println(io, "ResultCount: ", length(model.solutions)) + println(io, rule) + return +end + +""" + _log_subproblem_solve(model::Optimizer, variables::Vector{MOI.VariableIndex}) + +Log the solution. We don't have a pre-computed point, so compute one from the +variable values. +""" +function _log_subproblem_solve(model::Optimizer, arg) + if !model.silent + _log_subproblem_inner(model, arg) + end + return +end + +# Variables; compute the associated Y +function _log_subproblem_inner(model::Optimizer, x::Vector{MOI.VariableIndex}) + _, Y = _compute_point(model, x, model.f) + _log_subproblem_solve(model, Y) + return +end + +# We have a pre-computed point. +function _log_subproblem_inner(model::Optimizer, Y::Vector) + print(_format(model.subproblem_count), " ") + for y in Y + print(" ", _format(y)) + end + println(" ", _format(time() - model.start_time)) + return +end + +# Assume the subproblem failed to solve. +function _log_subproblem_inner(model::Optimizer, msg::String) + print(_format(model.subproblem_count), " ") + print(rpad(msg, 13 * MOI.output_dimension(model.f))) + println(" ", _format(time() - model.start_time)) + return +end + +_format(x::Int) = Printf.@sprintf("%5d", x) + +_format(x::Float64) = Printf.@sprintf("% .5e", x) + +_format(::Nothing) = " " + function _optimize!(model::Optimizer) model.start_time = time() empty!(model.solutions) @@ -833,18 +862,7 @@ function _optimize!(model::Optimizer) return end if !model.silent - rule = "-"^(7 + 13 * (MOI.output_dimension(model.f) + 1)) - println(rule) - println(" MultiObjectiveAlgorithms.jl") - println(rule) - println("Algorithm: ", _describe(model.algorithm)) - println(rule) - print("solve #") - for i in 1:MOI.output_dimension(model.f) - print(lpad("Obj. $i ", 13)) - end - println(" Time ") - println(rule) + _print_header(stdout, model) end # We need to clear the ideal point prior to starting the solve. Algorithms # may update this during the solve, otherwise we will update it at the end. @@ -857,10 +875,7 @@ function _optimize!(model::Optimizer) _sort!(model.solutions, MOI.get(model, MOI.ObjectiveSense())) end if !model.silent - println(rule) - println("Terminating with status: ", status) - println("Number of non-dominated solutions: ", length(model.solutions)) - println(rule) + _print_footer(stdout, model) end if MOI.get(model, ComputeIdealPoint()) _compute_ideal_point(model) diff --git a/src/algorithms/Chalmet.jl b/src/algorithms/Chalmet.jl index 4359a01..33e2c4b 100644 --- a/src/algorithms/Chalmet.jl +++ b/src/algorithms/Chalmet.jl @@ -25,8 +25,6 @@ This algorithm is restricted to problems with: """ struct Chalmet <: AbstractAlgorithm end -_describe(::Chalmet) = "Chalmet()" - function _solve_constrained_model( model::Optimizer, ::Chalmet, @@ -40,13 +38,13 @@ function _solve_constrained_model( optimize_inner!(model) status = MOI.get(model.inner, MOI.TerminationStatus()) if !_is_scalar_status_optimal(status) - _log_solution(model, "subproblem not optimal") + _log_subproblem_solve(model, "subproblem not optimal") MOI.delete.(model, c) return status, nothing end variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) X, Y = _compute_point(model, variables, model.f) - _log_solution(model, Y) + _log_subproblem_solve(model, Y) MOI.delete.(model, c) return status, SolutionPoint(X, Y) end @@ -69,7 +67,7 @@ function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer) return status, solutions end _, y1[2] = _compute_point(model, variables, f2) - _log_solution(model, variables) + _log_subproblem_solve(model, variables) MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f1)}(), f1) y1_constraint = MOI.Utilities.normalize_and_add_constraint( model.inner, @@ -82,7 +80,7 @@ function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer) return status, solutions end x1, y1[1] = _compute_point(model, variables, f1) - _log_solution(model, y1) + _log_subproblem_solve(model, y1) MOI.delete(model.inner, y1_constraint) push!(solutions, SolutionPoint(x1, y1)) MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f1)}(), f1) @@ -92,7 +90,7 @@ function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer) return status, solutions end _, y2[1] = _compute_point(model, variables, f1) - _log_solution(model, variables) + _log_subproblem_solve(model, variables) if y2[1] ≈ solutions[1].y[1] return MOI.OPTIMAL, solutions end @@ -108,7 +106,7 @@ function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer) return status, solutions end x2, y2[2] = _compute_point(model, variables, f2) - _log_solution(model, y2) + _log_subproblem_solve(model, y2) MOI.delete(model.inner, y2_constraint) push!(solutions, SolutionPoint(x2, y2)) push!(Q, (1, 2)) diff --git a/src/algorithms/Dichotomy.jl b/src/algorithms/Dichotomy.jl index 5b1c3a9..7332c86 100644 --- a/src/algorithms/Dichotomy.jl +++ b/src/algorithms/Dichotomy.jl @@ -30,8 +30,6 @@ mutable struct Dichotomy <: AbstractAlgorithm Dichotomy() = new(nothing) end -_describe(::Dichotomy) = "Dichotomy()" - """ NISE() @@ -76,7 +74,7 @@ function _solve_weighted_sum( end variables = MOI.get(model.inner, MOI.ListOfVariableIndices()) X, Y = _compute_point(model, variables, model.f) - _log_solution(model, Y) + _log_subproblem_solve(model, Y) return status, SolutionPoint(X, Y) end diff --git a/src/algorithms/DominguezRios.jl b/src/algorithms/DominguezRios.jl index b2f0c84..e5d8d4c 100644 --- a/src/algorithms/DominguezRios.jl +++ b/src/algorithms/DominguezRios.jl @@ -26,8 +26,6 @@ This algorithm is restricted to problems with: """ struct DominguezRios <: AbstractAlgorithm end -_describe(::DominguezRios) = "DominguezRios()" - mutable struct _DominguezRiosBox l::Vector{Float64} u::Vector{Float64} @@ -66,8 +64,8 @@ function _p_partition( ẑ = max.(z, B.l) ret = _DominguezRiosBox[] for i in 1:length(z) - new_l = vcat(B.l[1:i], ẑ[i+1:end]) - new_u = vcat(B.u[1:i-1], ẑ[i], B.u[i+1:end]) + new_l = vcat(B.l[1:i], ẑ[(i+1):end]) + new_u = vcat(B.u[1:(i-1)], ẑ[i], B.u[(i+1):end]) new_priority = _reduced_scaled_priority(new_l, new_u, i, ẑ, yI, yN) push!(ret, _DominguezRiosBox(new_l, new_u, new_priority)) end @@ -167,7 +165,7 @@ function minimize_multiobjective!(algorithm::DominguezRios, model::Optimizer) return status, nothing end _, Y = _compute_point(model, variables, f_i) - _log_solution(model, variables) + _log_subproblem_solve(model, variables) yI[i] = Y model.ideal_point[i] = Y MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) @@ -178,7 +176,7 @@ function minimize_multiobjective!(algorithm::DominguezRios, model::Optimizer) return status, nothing end _, Y = _compute_point(model, variables, f_i) - _log_solution(model, variables) + _log_subproblem_solve(model, variables) yN[i] = Y + 1 end MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) @@ -219,7 +217,7 @@ function minimize_multiobjective!(algorithm::DominguezRios, model::Optimizer) optimize_inner!(model) if _is_scalar_status_optimal(model) X, Y = _compute_point(model, variables, model.f) - _log_solution(model, Y) + _log_subproblem_solve(model, Y) obj = MOI.get(model.inner, MOI.ObjectiveValue()) # We need to undo the scaling of the scalar objective. There's no # need to unscale `Y` because we have evaluated this explicitly from @@ -237,7 +235,7 @@ function minimize_multiobjective!(algorithm::DominguezRios, model::Optimizer) # fail and return something like OTHER_ERROR (e.g., because the # numerics are challenging). Rather than error completely, let's # just skip this box. - _log_solution(model, "subproblem not optimal") + _log_subproblem_solve(model, "subproblem not optimal") deleteat!(L[k], i) end MOI.delete.(model.inner, constraints) diff --git a/src/algorithms/EpsilonConstraint.jl b/src/algorithms/EpsilonConstraint.jl index b2bd1ca..3bcdb94 100644 --- a/src/algorithms/EpsilonConstraint.jl +++ b/src/algorithms/EpsilonConstraint.jl @@ -36,8 +36,6 @@ mutable struct EpsilonConstraint <: AbstractAlgorithm EpsilonConstraint() = new(nothing, nothing) end -_describe(::EpsilonConstraint) = "EpsilonConstraint()" - MOI.supports(::EpsilonConstraint, ::SolutionLimit) = true function MOI.set(alg::EpsilonConstraint, ::SolutionLimit, value) @@ -128,7 +126,7 @@ function minimize_multiobjective!( break end X, Y = _compute_point(model, variables, model.f) - _log_solution(model, Y) + _log_subproblem_solve(model, Y) if isempty(solutions) || !(Y ≈ solutions[end].y) push!(solutions, SolutionPoint(X, Y)) end diff --git a/src/algorithms/Hierarchical.jl b/src/algorithms/Hierarchical.jl index 5bc4046..33aabc4 100644 --- a/src/algorithms/Hierarchical.jl +++ b/src/algorithms/Hierarchical.jl @@ -37,8 +37,6 @@ mutable struct Hierarchical <: AbstractAlgorithm Hierarchical() = new(Int[], Float64[], Float64[]) end -_describe(::Hierarchical) = "Hierarchical()" - MOI.supports(::Hierarchical, ::ObjectivePriority) = true function MOI.get(alg::Hierarchical, attr::ObjectivePriority) @@ -116,7 +114,7 @@ function minimize_multiobjective!(algorithm::Hierarchical, model::Optimizer) end if !model.silent X, Y = _compute_point(model, variables, model.f) - _log_solution(model, Y) + _log_subproblem_solve(model, Y) end # Add tolerance constraints X, Y = _compute_point(model, variables, new_vector_f) @@ -128,7 +126,7 @@ function minimize_multiobjective!(algorithm::Hierarchical, model::Optimizer) end end X, Y = _compute_point(model, variables, model.f) - _log_solution(model, Y) + _log_subproblem_solve(model, Y) # Remove tolerance constraints for c in constraints MOI.delete(model, c) diff --git a/src/algorithms/KirlikSayin.jl b/src/algorithms/KirlikSayin.jl index fbf3fbb..8f36040 100644 --- a/src/algorithms/KirlikSayin.jl +++ b/src/algorithms/KirlikSayin.jl @@ -31,8 +31,6 @@ This algorithm is restricted to problems with: """ struct KirlikSayin <: AbstractAlgorithm end -_describe(::KirlikSayin) = "KirlikSayin()" - struct _Rectangle l::Vector{Float64} u::Vector{Float64} @@ -107,7 +105,7 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) return status, nothing end _, Y = _compute_point(model, variables, f_i) - _log_solution(model, variables) + _log_subproblem_solve(model, variables) model.ideal_point[i] = yI[i] = Y # Nadir point MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) @@ -120,7 +118,7 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) return status, nothing end _, Y = _compute_point(model, variables, f_i) - _log_solution(model, variables) + _log_subproblem_solve(model, variables) yN[i] = Y + δ MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) end @@ -155,7 +153,7 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) push!(ε_constraints, ci) end optimize_inner!(model) - _log_solution(model, "auxillary subproblem") + _log_subproblem_solve(model, "auxillary subproblem") if !_is_scalar_status_optimal(model) # If this fails, it likely means that the solver experienced a # numerical error with this box. Just skip it. @@ -176,7 +174,7 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) ) optimize_inner!(model) if !_is_scalar_status_optimal(model) - _log_solution(model, "subproblem not optimal") + _log_subproblem_solve(model, "subproblem not optimal") # If this fails, it likely means that the solver experienced a # numerical error with this box. Just skip it. MOI.delete.(model, ε_constraints) @@ -185,7 +183,7 @@ function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer) continue end X, Y = _compute_point(model, variables, model.f) - _log_solution(model, Y) + _log_subproblem_solve(model, Y) Y_proj = _project(Y, k) if !(Y in YN) push!(solutions, SolutionPoint(X, Y)) diff --git a/src/algorithms/Lexicographic.jl b/src/algorithms/Lexicographic.jl index b667a25..7ca354d 100644 --- a/src/algorithms/Lexicographic.jl +++ b/src/algorithms/Lexicographic.jl @@ -43,8 +43,6 @@ mutable struct Lexicographic <: AbstractAlgorithm end end -_describe(::Lexicographic) = "Lexicographic()" - MOI.supports(::Lexicographic, ::ObjectiveRelativeTolerance) = true function MOI.get(alg::Lexicographic, attr::ObjectiveRelativeTolerance) @@ -138,7 +136,7 @@ function _solve_in_sequence( primal_status = MOI.get(model.inner, MOI.PrimalStatus()) if _is_scalar_status_feasible_point(primal_status) X, Y = _compute_point(model, variables, model.f) - _log_solution(model, Y) + _log_subproblem_solve(model, Y) solution = [SolutionPoint(X, Y)] end if !_is_scalar_status_optimal(status) diff --git a/src/algorithms/RandomWeighting.jl b/src/algorithms/RandomWeighting.jl index af1ed1a..10182c8 100644 --- a/src/algorithms/RandomWeighting.jl +++ b/src/algorithms/RandomWeighting.jl @@ -27,8 +27,6 @@ mutable struct RandomWeighting <: AbstractAlgorithm RandomWeighting() = new(nothing) end -_describe(::RandomWeighting) = "RandomWeighting()" - MOI.supports(::RandomWeighting, ::SolutionLimit) = true function MOI.set(alg::RandomWeighting, ::SolutionLimit, value) @@ -55,7 +53,7 @@ function optimize_multiobjective!(algorithm::RandomWeighting, model::Optimizer) status = MOI.get(model.inner, MOI.TerminationStatus()) if _is_scalar_status_optimal(status) X, Y = _compute_point(model, variables, model.f) - _log_solution(model, Y) + _log_subproblem_solve(model, Y) push!(solutions, SolutionPoint(X, Y)) else return status, nothing @@ -77,7 +75,7 @@ function optimize_multiobjective!(algorithm::RandomWeighting, model::Optimizer) status = MOI.get(model.inner, MOI.TerminationStatus()) if _is_scalar_status_optimal(status) X, Y = _compute_point(model, variables, model.f) - _log_solution(model, Y) + _log_subproblem_solve(model, Y) push!(solutions, SolutionPoint(X, Y)) end end diff --git a/src/algorithms/Sandwiching.jl b/src/algorithms/Sandwiching.jl index 6ec3002..0008e71 100644 --- a/src/algorithms/Sandwiching.jl +++ b/src/algorithms/Sandwiching.jl @@ -28,5 +28,3 @@ algorithm = MOA.Sandwiching(0.0) mutable struct Sandwiching <: AbstractAlgorithm precision::Float64 end - -_describe(alg::Sandwiching) = "Sandwiching($(alg.precision))" diff --git a/src/algorithms/TambyVanderpooten.jl b/src/algorithms/TambyVanderpooten.jl index b0debd0..1d4fdbd 100644 --- a/src/algorithms/TambyVanderpooten.jl +++ b/src/algorithms/TambyVanderpooten.jl @@ -32,8 +32,6 @@ This algorithm is restricted to problems with: """ struct TambyVanderpooten <: AbstractAlgorithm end -_describe(::TambyVanderpooten) = "TambyVanderpooten()" - function _update_search_region( U_N::Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}}, y::Vector{Float64}, @@ -113,7 +111,7 @@ function minimize_multiobjective!( return status, nothing end _, Y = _compute_point(model, variables, f_i) - _log_solution(model, variables) + _log_subproblem_solve(model, variables) yI[i] = Y model.ideal_point[i] = Y MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE) @@ -124,7 +122,7 @@ function minimize_multiobjective!( return status, nothing end _, Y = _compute_point(model, variables, f_i) - _log_solution(model, variables) + _log_subproblem_solve(model, variables) yN[i] = Y + 1 end MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE) @@ -168,7 +166,7 @@ function minimize_multiobjective!( end end optimize_inner!(model) - _log_solution(model, "auxillary subproblem") + _log_subproblem_solve(model, "auxillary subproblem") status = MOI.get(model.inner, MOI.TerminationStatus()) if !_is_scalar_status_optimal(status) MOI.delete.(model, ε_constraints) @@ -190,7 +188,7 @@ function minimize_multiobjective!( return status, nothing end X, Y = _compute_point(model, variables, model.f) - _log_solution(model, Y) + _log_subproblem_solve(model, Y) MOI.delete.(model, ε_constraints) MOI.delete(model, y_k_constraint) push!(V[k], (u, Y)) diff --git a/test/test_model.jl b/test/test_model.jl index db61d86..44302dd 100644 --- a/test/test_model.jl +++ b/test/test_model.jl @@ -234,6 +234,35 @@ function test_check_interrupt() return end +function _test_printing() + model = MOA.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOA.Algorithm(), MOA.KirlikSayin()) + x = MOI.add_variables(model, 2) + MOI.add_constraint.(model, x, MOI.GreaterThan(0.0)) + MOI.add_constraint(model, x[2], MOI.LessThan(3.0)) + MOI.add_constraint(model, 3.0 * x[1] - 1.0 * x[2], MOI.LessThan(6.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + f = MOI.Utilities.vectorize([3.0 * x[1] + x[2], -1.0 * x[1] - 2.0 * x[2]]) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + dir = mktempdir() + open(joinpath(dir, "log.txt"), "w") do io + redirect_stdout(() -> MOI.optimize!(model), io) + return + end + contents = read(joinpath(dir, "log.txt"), String) + for line in [ + "Algorithm: KirlikSayin", + "auxillary subproblem", + "1 0.00000e+00 0.00000e+00", + "----------------------------------------------", + "TerminationStatus: OPTIMAL", + "ResultCount: 10", + ] + @test occursin(line, contents) + end + return +end + end # module TestModel.run_tests() From 2d6590b811aea62c975f7e2cda661e3450753730 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 12 Dec 2025 15:39:40 +1300 Subject: [PATCH 5/6] Update --- test/test_model.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_model.jl b/test/test_model.jl index 44302dd..845132b 100644 --- a/test/test_model.jl +++ b/test/test_model.jl @@ -260,6 +260,14 @@ function _test_printing() ] @test occursin(line, contents) end + @test MOI.supports(model, MOI.Silent()) + @test MOI.get(model, MOI.Silent()) == false + MOI.set(model, MOI.Silent(), true) + open(joinpath(dir, "log2.txt"), "w") do io + redirect_stdout(() -> MOI.optimize!(model), io) + return + end + @test read(joinpath(dir, "log2.txt"), String) == "" return end From 54e202dc4a850dec3204cf3d01537c52a3728643 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 12 Dec 2025 16:17:28 +1300 Subject: [PATCH 6/6] Update --- src/MultiObjectiveAlgorithms.jl | 2 -- test/test_model.jl | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/MultiObjectiveAlgorithms.jl b/src/MultiObjectiveAlgorithms.jl index 7e87697..2882a60 100644 --- a/src/MultiObjectiveAlgorithms.jl +++ b/src/MultiObjectiveAlgorithms.jl @@ -849,8 +849,6 @@ _format(x::Int) = Printf.@sprintf("%5d", x) _format(x::Float64) = Printf.@sprintf("% .5e", x) -_format(::Nothing) = " " - function _optimize!(model::Optimizer) model.start_time = time() empty!(model.solutions) diff --git a/test/test_model.jl b/test/test_model.jl index 845132b..619c11a 100644 --- a/test/test_model.jl +++ b/test/test_model.jl @@ -234,7 +234,7 @@ function test_check_interrupt() return end -function _test_printing() +function test_printing() model = MOA.Optimizer(HiGHS.Optimizer) MOI.set(model, MOA.Algorithm(), MOA.KirlikSayin()) x = MOI.add_variables(model, 2)