From 9ecb4fa7dd59882f1f737d7de0619b0e761b7358 Mon Sep 17 00:00:00 2001 From: Guillaume Marques Date: Wed, 12 Jul 2023 11:15:49 +0200 Subject: [PATCH] restore some unit tests of Algorithms (#986) --- test/unit/Algorithm/explore.jl | 26 +- test/unit/Algorithm/optimizationstate.jl | 585 ++++++++++---------- test/unit/Algorithm/presolve.jl | 100 ++-- test/unit/Algorithm/record_mastercolumns.jl | 5 +- test/unit/Algorithm/subsolvers.jl | 87 ++- test/unit/Algorithm/treesearch_interface.jl | 353 ------------ test/unit/run.jl | 2 +- 7 files changed, 405 insertions(+), 753 deletions(-) delete mode 100644 test/unit/Algorithm/treesearch_interface.jl diff --git a/test/unit/Algorithm/explore.jl b/test/unit/Algorithm/explore.jl index 4fbd7b108..be48a5659 100644 --- a/test/unit/Algorithm/explore.jl +++ b/test/unit/Algorithm/explore.jl @@ -1,4 +1,4 @@ -struct NodeAe1 <: ClA.AbstractNode +struct NodeAe1 <: TreeSearch.AbstractNode id::Int depth::Int parent::Union{Nothing, NodeAe1} @@ -51,16 +51,18 @@ end TreeSearch.tree_search_output(space::CustomSearchSpaceAe1, _) = space.visit_order -@testset "Algorithm - treesearch exploration" begin - @testset "Depth-First Search" begin - search_space = CustomSearchSpaceAe1(2, 3, 11) - visit_order = ClA.tree_search(ClA.DepthFirstStrategy(), search_space, nothing, nothing) - @test visit_order == [1, 3, 5, 7, 6, 4, 9, 8, 2, 11, 10] - end +function test_dfs() + search_space = CustomSearchSpaceAe1(2, 3, 11) + visit_order = ClA.tree_search(ClA.DepthFirstStrategy(), search_space, nothing, nothing) + @test visit_order == [1, 3, 5, 7, 6, 4, 9, 8, 2, 11, 10] + return +end +register!(unit_tests, "explore", test_dfs) - @testset "Best-First Search" begin - search_space = CustomSearchSpaceAe1(2, 3, 11) - visit_order = ClA.tree_search(CustomBestFirstSearch(), search_space, nothing, nothing) - @test visit_order == [1, 3, 5, 7, 6, 4, 9, 8, 2, 11, 10] - end + +function test_bfs() + search_space = CustomSearchSpaceAe1(2, 3, 11) + visit_order = ClA.tree_search(CustomBestFirstSearch(), search_space, nothing, nothing) + @test visit_order == [1, 3, 5, 7, 6, 4, 9, 8, 2, 11, 10] end +register!(unit_tests, "explore", test_bfs) diff --git a/test/unit/Algorithm/optimizationstate.jl b/test/unit/Algorithm/optimizationstate.jl index 9e63d281c..b02cd7d41 100644 --- a/test/unit/Algorithm/optimizationstate.jl +++ b/test/unit/Algorithm/optimizationstate.jl @@ -5,300 +5,303 @@ function formulation_for_optimizationstate(sense = Coluna.MathProg.MinSense) return form, var, constr end -@testset "Algorithm - optimization state" begin - @testset "update solution with min objective" begin - form, var, constr = formulation_for_optimizationstate() - state = ClA.OptimizationState( - form, max_length_ip_primal_sols = 2.0, - max_length_lp_primal_sols = 2.0, max_length_lp_dual_sols = 2.0 - ) - primalsol = ClMP.PrimalSolution(form, [ClMP.getid(var)], [2.0], 2.0, ClB.UNKNOWN_FEASIBILITY) - dualsol = ClMP.DualSolution(form, [ClMP.getid(constr)], [1.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 1.0, ClB.UNKNOWN_FEASIBILITY) - - ### ip primal - ClA.update_ip_primal_sol!(state, primalsol) - # check that `primalsol` is added to `state.ip_primal_sols` - @test length(ClA.get_ip_primal_sols(state)) == 1 - @test ClA.get_ip_primal_sols(state)[1] == primalsol - # check that incumbent bound is updated - @test ClA.get_ip_primal_bound(state) == 2.0 - ClA.update_ip_primal_sol!(state, ClMP.PrimalSolution( - form, [ClMP.getid(var)], [3.0], 3.0, ClB.UNKNOWN_FEASIBILITY - )) - # check that solution worse than `primalsol` is NOT added to `state.ip_primal_sols` - @test length(ClA.get_ip_primal_sols(state)) == 1 - @test ClA.get_ip_primal_sols(state)[1] == primalsol - ### - - ### lp primal - ClA.update_lp_primal_sol!(state, primalsol) - # check that `primalsol` is added to `state.lp_primal_sols` - @test length(ClA.get_lp_primal_sols(state)) == 1 - @test ClA.get_lp_primal_sols(state)[1] == primalsol - # check that incumbent bound is updated - @test ClA.get_lp_primal_bound(state) == 2.0 - ClA.update_lp_primal_sol!( - state, ClMP.PrimalSolution(form, [ClMP.getid(var)], [3.0], 3.0, ClB.UNKNOWN_FEASIBILITY - )) - # check that solution worse than `primalsol` is NOT added to `state.lp_primal_sols` - @test length(ClA.get_lp_primal_sols(state)) == 1 - @test ClA.get_lp_primal_sols(state)[1] == primalsol - ### - - ### lp dual - ClA.update_lp_dual_sol!(state, dualsol) - # check that `dualsol` is added to `state.lp_dual_sols` - @test length(ClA.get_lp_dual_sols(state)) == 1 - @test ClA.get_lp_dual_sols(state)[1] == dualsol - # check that incumbent bound is updated - @test ClA.get_lp_dual_bound(state) == 1.0 - ClA.update_lp_dual_sol!(state, ClMP.DualSolution( - form, [ClMP.getid(constr)], [0.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 0.0, ClB.UNKNOWN_FEASIBILITY - )) - # check that solution worse than `dualsol` is NOT added to `state.lp_dual_sols` - @test length(ClA.get_lp_dual_sols(state)) == 1 - @test ClA.get_lp_dual_sols(state)[1] == dualsol - ### - end +function update_solution_with_min_objective() + form, var, constr = formulation_for_optimizationstate() + state = ClA.OptimizationState( + form, max_length_ip_primal_sols = 2.0, + max_length_lp_primal_sols = 2.0, max_length_lp_dual_sols = 2.0 + ) + primalsol = ClMP.PrimalSolution(form, [ClMP.getid(var)], [2.0], 2.0, ClB.UNKNOWN_FEASIBILITY) + dualsol = ClMP.DualSolution(form, [ClMP.getid(constr)], [1.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 1.0, ClB.UNKNOWN_FEASIBILITY) + ### ip primal + ClA.update_ip_primal_sol!(state, primalsol) + # check that `primalsol` is added to `state.ip_primal_sols` + @test length(ClA.get_ip_primal_sols(state)) == 1 + @test ClA.get_ip_primal_sols(state)[1] == primalsol + # check that incumbent bound is updated + @test ClA.get_ip_primal_bound(state) == 2.0 + ClA.update_ip_primal_sol!(state, ClMP.PrimalSolution( + form, [ClMP.getid(var)], [3.0], 3.0, ClB.UNKNOWN_FEASIBILITY + )) + # check that solution worse than `primalsol` is NOT added to `state.ip_primal_sols` + @test length(ClA.get_ip_primal_sols(state)) == 1 + @test ClA.get_ip_primal_sols(state)[1] == primalsol + ### - @testset "update solution with max objective" begin - form, var, constr = formulation_for_optimizationstate(ClMP.MaxSense) - state = ClA.OptimizationState( - form, max_length_ip_primal_sols = 2.0, - max_length_lp_primal_sols = 2.0, max_length_lp_dual_sols = 2.0 - ) - primalsol = ClMP.PrimalSolution(form, [ClMP.getid(var)], [1.0], 1.0, ClB.UNKNOWN_FEASIBILITY) - dualsol = ClMP.DualSolution(form, [ClMP.getid(constr)], [2.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 2.0, ClB.UNKNOWN_FEASIBILITY) - - ### ip primal - ClA.update_ip_primal_sol!(state, primalsol) - # check that `primalsol` is added to `state.ip_primal_sols` - @test length(ClA.get_ip_primal_sols(state)) == 1 - @test ClA.get_ip_primal_sols(state)[1] == primalsol - # check that incumbent bound is updated - @test ClA.get_ip_primal_bound(state) == 1.0 - ClA.update_ip_primal_sol!(state, ClMP.PrimalSolution( - form, [ClMP.getid(var)], [0.0], 0.0, ClB.UNKNOWN_FEASIBILITY - )) - # check that solution worse than `primalsol` is NOT added to `state.ip_primal_sols` - @test length(ClA.get_ip_primal_sols(state)) == 1 - @test ClA.get_ip_primal_sols(state)[1] == primalsol - ### - - ### lp primal - ClA.update_lp_primal_sol!(state, primalsol) - # check that `primalsol` is added to `state.lp_primal_sols` - @test length(ClA.get_lp_primal_sols(state)) == 1 - @test ClA.get_lp_primal_sols(state)[1] == primalsol - # check that incumbent bound is updated - @test ClA.get_lp_primal_bound(state) == 1.0 - ClA.update_lp_primal_sol!( - state, ClMP.PrimalSolution(form, [ClMP.getid(var)], [0.0], 0.0, ClB.UNKNOWN_FEASIBILITY - )) - # check that solution worse than `primalsol` is NOT added to `state.lp_primal_sols` - @test length(ClA.get_lp_primal_sols(state)) == 1 - @test ClA.get_lp_primal_sols(state)[1] == primalsol - ### - - ### lp dual - ClA.update_lp_dual_sol!(state, dualsol) - # check that `dualsol` is added to `state.lp_dual_sols` - @test length(ClA.get_lp_dual_sols(state)) == 1 - @test ClA.get_lp_dual_sols(state)[1] == dualsol - # check that incumbent bound is updated - @test ClA.get_lp_dual_bound(state) == 2.0 - ClA.update_lp_dual_sol!(state, ClMP.DualSolution( - form, [ClMP.getid(constr)], [3.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 3.0, ClB.UNKNOWN_FEASIBILITY - )) - # check that solution worse than `dualsol` is NOT added to `state.lp_dual_sols` - @test length(ClA.get_lp_dual_sols(state)) == 1 - @test ClA.get_lp_dual_sols(state)[1] == dualsol - ### - end + ### lp primal + ClA.update_lp_primal_sol!(state, primalsol) + # check that `primalsol` is added to `state.lp_primal_sols` + @test length(ClA.get_lp_primal_sols(state)) == 1 + @test ClA.get_lp_primal_sols(state)[1] == primalsol + # check that incumbent bound is updated + @test ClA.get_lp_primal_bound(state) == 2.0 + ClA.update_lp_primal_sol!( + state, ClMP.PrimalSolution(form, [ClMP.getid(var)], [3.0], 3.0, ClB.UNKNOWN_FEASIBILITY + )) + # check that solution worse than `primalsol` is NOT added to `state.lp_primal_sols` + @test length(ClA.get_lp_primal_sols(state)) == 1 + @test ClA.get_lp_primal_sols(state)[1] == primalsol + ### - @testset "add solution with min objective" begin - form, var, constr = formulation_for_optimizationstate() - state = ClA.OptimizationState(form) - primalsol = ClMP.PrimalSolution(form, [ClMP.getid(var)], [2.0], 2.0, ClB.UNKNOWN_FEASIBILITY) - dualsol = ClMP.DualSolution(form, [ClMP.getid(constr)], [1.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 1.0, ClB.UNKNOWN_FEASIBILITY) - - ### ip primal - ClA.add_ip_primal_sols!( - state, - ClMP.PrimalSolution(form, [ClMP.getid(var)], [3.0], 3.0, ClB.UNKNOWN_FEASIBILITY), - primalsol - ) - # check that `primalsol` is added to `state.ip_primal_sols` and worst solution is removed - @test length(ClA.get_ip_primal_sols(state)) == 1 - @test ClA.get_ip_primal_sols(state)[1] == primalsol - # check that incumbent bound is updated - @test ClA.get_ip_primal_bound(state) == 2.0 - ### - - ### lp primal - ClA.add_lp_primal_sol!(state, ClMP.PrimalSolution( - form, [ClMP.getid(var)], [3.0], 3.0, ClB.UNKNOWN_FEASIBILITY - )) - # check that incumbent bound is updated - @test ClA.get_lp_primal_bound(state) == 3.0 - ClA.add_lp_primal_sol!(state, primalsol) - # check that `primalsol` is added to `state.lp_primal_sols` and worst solution is removed - @test length(ClA.get_lp_primal_sols(state)) == 1 - @test ClA.get_lp_primal_sols(state)[1] == primalsol - # check that incumbent bound is updated - @test ClA.get_lp_primal_bound(state) == 2.0 - ### - - ### lp dual - ClA.add_lp_dual_sol!(state, ClMP.DualSolution( - form, [ClMP.getid(constr)], [0.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 0.0, ClB.UNKNOWN_FEASIBILITY - )) - # check that incumbent bound is updated - @test ClA.get_lp_dual_bound(state) == 0.0 - ClA.add_lp_dual_sol!(state, dualsol) - # check that `dualsol` is added to `state.lp_dual_sols` and worst solution is removed - @test length(ClA.get_lp_dual_sols(state)) == 1 - @test ClA.get_lp_dual_sols(state)[1] == dualsol - # check that incumbent bound is updated - @test ClA.get_lp_dual_bound(state) == 1.0 - ### - end + ### lp dual + ClA.update_lp_dual_sol!(state, dualsol) + # check that `dualsol` is added to `state.lp_dual_sols` + @test length(ClA.get_lp_dual_sols(state)) == 1 + @test ClA.get_lp_dual_sols(state)[1] == dualsol + # check that incumbent bound is updated + @test ClA.get_lp_dual_bound(state) == 1.0 + ClA.update_lp_dual_sol!(state, ClMP.DualSolution( + form, [ClMP.getid(constr)], [0.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 0.0, ClB.UNKNOWN_FEASIBILITY + )) + # check that solution worse than `dualsol` is NOT added to `state.lp_dual_sols` + @test length(ClA.get_lp_dual_sols(state)) == 1 + @test ClA.get_lp_dual_sols(state)[1] == dualsol + ### +end +register!(unit_tests, "optimization_state", update_solution_with_min_objective) + +function update_solution_with_max_objective() + form, var, constr = formulation_for_optimizationstate(ClMP.MaxSense) + state = ClA.OptimizationState( + form, max_length_ip_primal_sols = 2.0, + max_length_lp_primal_sols = 2.0, max_length_lp_dual_sols = 2.0 + ) + primalsol = ClMP.PrimalSolution(form, [ClMP.getid(var)], [1.0], 1.0, ClB.UNKNOWN_FEASIBILITY) + dualsol = ClMP.DualSolution(form, [ClMP.getid(constr)], [2.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 2.0, ClB.UNKNOWN_FEASIBILITY) + + ### ip primal + ClA.update_ip_primal_sol!(state, primalsol) + # check that `primalsol` is added to `state.ip_primal_sols` + @test length(ClA.get_ip_primal_sols(state)) == 1 + @test ClA.get_ip_primal_sols(state)[1] == primalsol + # check that incumbent bound is updated + @test ClA.get_ip_primal_bound(state) == 1.0 + ClA.update_ip_primal_sol!(state, ClMP.PrimalSolution( + form, [ClMP.getid(var)], [0.0], 0.0, ClB.UNKNOWN_FEASIBILITY + )) + # check that solution worse than `primalsol` is NOT added to `state.ip_primal_sols` + @test length(ClA.get_ip_primal_sols(state)) == 1 + @test ClA.get_ip_primal_sols(state)[1] == primalsol + ### + + ### lp primal + ClA.update_lp_primal_sol!(state, primalsol) + # check that `primalsol` is added to `state.lp_primal_sols` + @test length(ClA.get_lp_primal_sols(state)) == 1 + @test ClA.get_lp_primal_sols(state)[1] == primalsol + # check that incumbent bound is updated + @test ClA.get_lp_primal_bound(state) == 1.0 + ClA.update_lp_primal_sol!( + state, ClMP.PrimalSolution(form, [ClMP.getid(var)], [0.0], 0.0, ClB.UNKNOWN_FEASIBILITY + )) + # check that solution worse than `primalsol` is NOT added to `state.lp_primal_sols` + @test length(ClA.get_lp_primal_sols(state)) == 1 + @test ClA.get_lp_primal_sols(state)[1] == primalsol + ### + + ### lp dual + ClA.update_lp_dual_sol!(state, dualsol) + # check that `dualsol` is added to `state.lp_dual_sols` + @test length(ClA.get_lp_dual_sols(state)) == 1 + @test ClA.get_lp_dual_sols(state)[1] == dualsol + # check that incumbent bound is updated + @test ClA.get_lp_dual_bound(state) == 2.0 + ClA.update_lp_dual_sol!(state, ClMP.DualSolution( + form, [ClMP.getid(constr)], [3.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 3.0, ClB.UNKNOWN_FEASIBILITY + )) + # check that solution worse than `dualsol` is NOT added to `state.lp_dual_sols` + @test length(ClA.get_lp_dual_sols(state)) == 1 + @test ClA.get_lp_dual_sols(state)[1] == dualsol + ### +end +register!(unit_tests, "optimization_state", update_solution_with_max_objective) + +function add_solution_with_min_objective() + form, var, constr = formulation_for_optimizationstate() + state = ClA.OptimizationState(form) + primalsol = ClMP.PrimalSolution(form, [ClMP.getid(var)], [2.0], 2.0, ClB.UNKNOWN_FEASIBILITY) + dualsol = ClMP.DualSolution(form, [ClMP.getid(constr)], [1.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 1.0, ClB.UNKNOWN_FEASIBILITY) + + ### ip primal + ClA.add_ip_primal_sols!( + state, + ClMP.PrimalSolution(form, [ClMP.getid(var)], [3.0], 3.0, ClB.UNKNOWN_FEASIBILITY), + primalsol + ) + # check that `primalsol` is added to `state.ip_primal_sols` and worst solution is removed + @test length(ClA.get_ip_primal_sols(state)) == 1 + @test ClA.get_ip_primal_sols(state)[1] == primalsol + # check that incumbent bound is updated + @test ClA.get_ip_primal_bound(state) == 2.0 + ### + + ### lp primal + ClA.add_lp_primal_sol!(state, ClMP.PrimalSolution( + form, [ClMP.getid(var)], [3.0], 3.0, ClB.UNKNOWN_FEASIBILITY + )) + # check that incumbent bound is updated + @test ClA.get_lp_primal_bound(state) == 3.0 + ClA.add_lp_primal_sol!(state, primalsol) + # check that `primalsol` is added to `state.lp_primal_sols` and worst solution is removed + @test length(ClA.get_lp_primal_sols(state)) == 1 + @test ClA.get_lp_primal_sols(state)[1] == primalsol + # check that incumbent bound is updated + @test ClA.get_lp_primal_bound(state) == 2.0 + ### + + ### lp dual + ClA.add_lp_dual_sol!(state, ClMP.DualSolution( + form, [ClMP.getid(constr)], [0.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 0.0, ClB.UNKNOWN_FEASIBILITY + )) + # check that incumbent bound is updated + @test ClA.get_lp_dual_bound(state) == 0.0 + ClA.add_lp_dual_sol!(state, dualsol) + # check that `dualsol` is added to `state.lp_dual_sols` and worst solution is removed + @test length(ClA.get_lp_dual_sols(state)) == 1 + @test ClA.get_lp_dual_sols(state)[1] == dualsol + # check that incumbent bound is updated + @test ClA.get_lp_dual_bound(state) == 1.0 + ### +end +register!(unit_tests, "optimization_state", add_solution_with_min_objective) + +function add_solution_with_max_objective() + form, var, constr = formulation_for_optimizationstate(ClMP.MaxSense) + state = ClA.OptimizationState(form) + primalsol = ClMP.PrimalSolution(form, [ClMP.getid(var)], [1.0], 1.0, ClB.UNKNOWN_FEASIBILITY) + dualsol = ClMP.DualSolution(form, [ClMP.getid(constr)], [2.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 2.0, ClB.UNKNOWN_FEASIBILITY) - @testset "add solution with max objective" begin - form, var, constr = formulation_for_optimizationstate(ClMP.MaxSense) - state = ClA.OptimizationState(form) - primalsol = ClMP.PrimalSolution(form, [ClMP.getid(var)], [1.0], 1.0, ClB.UNKNOWN_FEASIBILITY) - dualsol = ClMP.DualSolution(form, [ClMP.getid(constr)], [2.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 2.0, ClB.UNKNOWN_FEASIBILITY) - - ### ip primal - ClA.add_ip_primal_sols!( - state, - ClMP.PrimalSolution(form, [ClMP.getid(var)], [0.0], 0.0, ClB.UNKNOWN_FEASIBILITY), - primalsol - ) - # check that `primalsol` is added to `state.ip_primal_sols` and worst solution is removed - @test length(ClA.get_ip_primal_sols(state)) == 1 - @test ClA.get_ip_primal_sols(state)[1] == primalsol - # check that incumbent bound is updated - @test ClA.get_ip_primal_bound(state) == 1.0 - ### - - ### lp primal - ClA.add_lp_primal_sol!(state, ClMP.PrimalSolution( - form, [ClMP.getid(var)], [0.0], 0.0, ClB.UNKNOWN_FEASIBILITY - )) - # check that incumbent bound is updated - @test ClA.get_lp_primal_bound(state) == 0.0 - ClA.add_lp_primal_sol!(state, primalsol) - # check that `primalsol` is added to `state.lp_primal_sols` and worst solution is removed - @test length(ClA.get_lp_primal_sols(state)) == 1 - @test ClA.get_lp_primal_sols(state)[1] == primalsol - # check that incumbent bound is updated - @test ClA.get_lp_primal_bound(state) == 1.0 - ### - - ### lp dual - ClA.add_lp_dual_sol!(state, ClMP.DualSolution( - form, [ClMP.getid(constr)], [3.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 3.0, ClB.UNKNOWN_FEASIBILITY - )) - # check that incumbent bound is updated - @test ClA.get_lp_dual_bound(state) == 3.0 - ClA.add_lp_dual_sol!(state, dualsol) - # check that `dualsol` is added to `state.lp_dual_sols` and worst solution is removed - @test length(ClA.get_lp_dual_sols(state)) == 1 - @test ClA.get_lp_dual_sols(state)[1] == dualsol - # check that incumbent bound is updated - @test ClA.get_lp_dual_bound(state) == 2.0 - ### - end + ### ip primal + ClA.add_ip_primal_sols!( + state, + ClMP.PrimalSolution(form, [ClMP.getid(var)], [0.0], 0.0, ClB.UNKNOWN_FEASIBILITY), + primalsol + ) + # check that `primalsol` is added to `state.ip_primal_sols` and worst solution is removed + @test length(ClA.get_ip_primal_sols(state)) == 1 + @test ClA.get_ip_primal_sols(state)[1] == primalsol + # check that incumbent bound is updated + @test ClA.get_ip_primal_bound(state) == 1.0 + ### - @testset "set solution with min objective" begin - form, var, constr = formulation_for_optimizationstate() - state = ClA.OptimizationState( - form, ip_primal_bound = 3.0, lp_primal_bound = 3.0, lp_dual_bound = -1.0 - ) - primalsol = ClMP.PrimalSolution(form, [ClMP.getid(var)], [2.0], 2.0, ClB.UNKNOWN_FEASIBILITY) - dualsol = ClMP.DualSolution(form, [ClMP.getid(constr)], [0.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 0.0, ClB.UNKNOWN_FEASIBILITY) - - ### ip primal - ClA.set_ip_primal_sol!(state, ClMP.PrimalSolution( - form, [ClMP.getid(var)], [1.0], 1.0, ClB.UNKNOWN_FEASIBILITY - )) - ClA.set_ip_primal_sol!(state, primalsol) - # check that only the solution which was set last is in `state.ip_primal_sols` - @test length(ClA.get_ip_primal_sols(state)) == 1 - @test ClA.get_ip_primal_sols(state)[1] == primalsol - # check that incumbent bound is NOT updated - @test ClA.get_ip_primal_bound(state) == 3.0 - ### - - ### lp primal - ClA.set_lp_primal_sol!(state, ClMP.PrimalSolution( - form, [ClMP.getid(var)], [1.0], 1.0, ClB.UNKNOWN_FEASIBILITY - )) - ClA.set_lp_primal_sol!(state, primalsol) - # check that only the solution which was set last is in `state.lp_primal_sols` - @test length(ClA.get_lp_primal_sols(state)) == 1 - @test ClA.get_lp_primal_sols(state)[1] == primalsol - # check that incumbent bound is NOT updated - @test ClA.get_lp_primal_bound(state) == 3.0 - ### - - ### lp dual - ClA.set_lp_dual_sol!(state, ClMP.DualSolution( - form, [ClMP.getid(constr)], [1.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 1.0, ClB.UNKNOWN_FEASIBILITY - )) - ClA.set_lp_dual_sol!(state, dualsol) - # check that only the solution which was set last is in `state.lp_dual_sols` - @test length(ClA.get_lp_dual_sols(state)) == 1 - @test ClA.get_lp_dual_sols(state)[1] == dualsol - # check that incumbent bound is NOT updated - @test ClA.get_lp_dual_bound(state) == -1.0 - ### - end + ### lp primal + ClA.add_lp_primal_sol!(state, ClMP.PrimalSolution( + form, [ClMP.getid(var)], [0.0], 0.0, ClB.UNKNOWN_FEASIBILITY + )) + # check that incumbent bound is updated + @test ClA.get_lp_primal_bound(state) == 0.0 + ClA.add_lp_primal_sol!(state, primalsol) + # check that `primalsol` is added to `state.lp_primal_sols` and worst solution is removed + @test length(ClA.get_lp_primal_sols(state)) == 1 + @test ClA.get_lp_primal_sols(state)[1] == primalsol + # check that incumbent bound is updated + @test ClA.get_lp_primal_bound(state) == 1.0 + ### - @testset "set solution with max objective" begin - form, var, constr = formulation_for_optimizationstate(ClMP.MaxSense) - state = ClA.OptimizationState( - form, ip_primal_bound = -1.0, lp_primal_bound = -1.0, lp_dual_bound = 3.0 - ) - primalsol = ClMP.PrimalSolution(form, [ClMP.getid(var)], [0.0], 0.0, ClB.UNKNOWN_FEASIBILITY) - dualsol = ClMP.DualSolution(form, [ClMP.getid(constr)], [2.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 2.0, ClB.UNKNOWN_FEASIBILITY) - - ### ip primal - ClA.set_ip_primal_sol!(state, ClMP.PrimalSolution( - form, [ClMP.getid(var)], [1.0], 1.0, ClB.UNKNOWN_FEASIBILITY - )) - ClA.set_ip_primal_sol!(state, primalsol) - # check that only the solution which was set last is in `state.ip_primal_sols` - @test length(ClA.get_ip_primal_sols(state)) == 1 - @test ClA.get_ip_primal_sols(state)[1] == primalsol - # check that incumbent bound is NOT updated - @test ClA.get_ip_primal_bound(state) == -1.0 - ### - - ### lp primal - ClA.set_lp_primal_sol!(state, ClMP.PrimalSolution( - form, [ClMP.getid(var)], [1.0], 1.0, ClB.UNKNOWN_FEASIBILITY - )) - ClA.set_lp_primal_sol!(state, primalsol) - # check that only the solution which was set last is in `state.lp_primal_sols` - @test length(ClA.get_lp_primal_sols(state)) == 1 - @test ClA.get_lp_primal_sols(state)[1] == primalsol - # check that incumbent bound is NOT updated - @test ClA.get_lp_primal_bound(state) == -1.0 - ### - - ### lp dual - ClA.set_lp_dual_sol!(state, ClMP.DualSolution( - form, [ClMP.getid(constr)], [1.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 1.0, ClB.UNKNOWN_FEASIBILITY - )) - ClA.set_lp_dual_sol!(state, dualsol) - # check that only the solution which was set last is in `state.lp_dual_sols` - @test length(ClA.get_lp_dual_sols(state)) == 1 - @test ClA.get_lp_dual_sols(state)[1] == dualsol - # check that incumbent bound is NOT updated - @test ClA.get_lp_dual_bound(state) == 3.0 - end -end \ No newline at end of file + ### lp dual + ClA.add_lp_dual_sol!(state, ClMP.DualSolution( + form, [ClMP.getid(constr)], [3.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 3.0, ClB.UNKNOWN_FEASIBILITY + )) + # check that incumbent bound is updated + @test ClA.get_lp_dual_bound(state) == 3.0 + ClA.add_lp_dual_sol!(state, dualsol) + # check that `dualsol` is added to `state.lp_dual_sols` and worst solution is removed + @test length(ClA.get_lp_dual_sols(state)) == 1 + @test ClA.get_lp_dual_sols(state)[1] == dualsol + # check that incumbent bound is updated + @test ClA.get_lp_dual_bound(state) == 2.0 + ### +end +register!(unit_tests, "optimization_state", add_solution_with_max_objective) + +function set_solution_with_min_objective() + form, var, constr = formulation_for_optimizationstate() + state = ClA.OptimizationState( + form, ip_primal_bound = 3.0, lp_primal_bound = 3.0, lp_dual_bound = -1.0 + ) + primalsol = ClMP.PrimalSolution(form, [ClMP.getid(var)], [2.0], 2.0, ClB.UNKNOWN_FEASIBILITY) + dualsol = ClMP.DualSolution(form, [ClMP.getid(constr)], [0.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 0.0, ClB.UNKNOWN_FEASIBILITY) + + ### ip primal + ClA.set_ip_primal_sol!(state, ClMP.PrimalSolution( + form, [ClMP.getid(var)], [1.0], 1.0, ClB.UNKNOWN_FEASIBILITY + )) + ClA.set_ip_primal_sol!(state, primalsol) + # check that only the solution which was set last is in `state.ip_primal_sols` + @test length(ClA.get_ip_primal_sols(state)) == 1 + @test ClA.get_ip_primal_sols(state)[1] == primalsol + # check that incumbent bound is NOT updated + @test ClA.get_ip_primal_bound(state) == 3.0 + ### + + ### lp primal + ClA.set_lp_primal_sol!(state, ClMP.PrimalSolution( + form, [ClMP.getid(var)], [1.0], 1.0, ClB.UNKNOWN_FEASIBILITY + )) + ClA.set_lp_primal_sol!(state, primalsol) + # check that only the solution which was set last is in `state.lp_primal_sols` + @test length(ClA.get_lp_primal_sols(state)) == 1 + @test ClA.get_lp_primal_sols(state)[1] == primalsol + # check that incumbent bound is NOT updated + @test ClA.get_lp_primal_bound(state) == 3.0 + ### + + ### lp dual + ClA.set_lp_dual_sol!(state, ClMP.DualSolution( + form, [ClMP.getid(constr)], [1.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 1.0, ClB.UNKNOWN_FEASIBILITY + )) + ClA.set_lp_dual_sol!(state, dualsol) + # check that only the solution which was set last is in `state.lp_dual_sols` + @test length(ClA.get_lp_dual_sols(state)) == 1 + @test ClA.get_lp_dual_sols(state)[1] == dualsol + # check that incumbent bound is NOT updated + @test ClA.get_lp_dual_bound(state) == -1.0 + ### +end +register!(unit_tests, "optimization_state", set_solution_with_min_objective) + +function set_solution_with_max_objective() + form, var, constr = formulation_for_optimizationstate(ClMP.MaxSense) + state = ClA.OptimizationState( + form, ip_primal_bound = -1.0, lp_primal_bound = -1.0, lp_dual_bound = 3.0 + ) + primalsol = ClMP.PrimalSolution(form, [ClMP.getid(var)], [0.0], 0.0, ClB.UNKNOWN_FEASIBILITY) + dualsol = ClMP.DualSolution(form, [ClMP.getid(constr)], [2.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 2.0, ClB.UNKNOWN_FEASIBILITY) + + ### ip primal + ClA.set_ip_primal_sol!(state, ClMP.PrimalSolution( + form, [ClMP.getid(var)], [1.0], 1.0, ClB.UNKNOWN_FEASIBILITY + )) + ClA.set_ip_primal_sol!(state, primalsol) + # check that only the solution which was set last is in `state.ip_primal_sols` + @test length(ClA.get_ip_primal_sols(state)) == 1 + @test ClA.get_ip_primal_sols(state)[1] == primalsol + # check that incumbent bound is NOT updated + @test ClA.get_ip_primal_bound(state) == -1.0 + ### + + ### lp primal + ClA.set_lp_primal_sol!(state, ClMP.PrimalSolution( + form, [ClMP.getid(var)], [1.0], 1.0, ClB.UNKNOWN_FEASIBILITY + )) + ClA.set_lp_primal_sol!(state, primalsol) + # check that only the solution which was set last is in `state.lp_primal_sols` + @test length(ClA.get_lp_primal_sols(state)) == 1 + @test ClA.get_lp_primal_sols(state)[1] == primalsol + # check that incumbent bound is NOT updated + @test ClA.get_lp_primal_bound(state) == -1.0 + ### + + ### lp dual + ClA.set_lp_dual_sol!(state, ClMP.DualSolution( + form, [ClMP.getid(constr)], [1.0], ClMP.VarId[], Float64[], ClMP.ActiveBound[], 1.0, ClB.UNKNOWN_FEASIBILITY + )) + ClA.set_lp_dual_sol!(state, dualsol) + # check that only the solution which was set last is in `state.lp_dual_sols` + @test length(ClA.get_lp_dual_sols(state)) == 1 + @test ClA.get_lp_dual_sols(state)[1] == dualsol + # check that incumbent bound is NOT updated + @test ClA.get_lp_dual_bound(state) == 3.0 +end +register!(unit_tests, "optimization_state", set_solution_with_max_objective) \ No newline at end of file diff --git a/test/unit/Algorithm/presolve.jl b/test/unit/Algorithm/presolve.jl index 931453284..d2907c03f 100644 --- a/test/unit/Algorithm/presolve.jl +++ b/test/unit/Algorithm/presolve.jl @@ -1,57 +1,57 @@ -@testset "Algorithm - Presolve" begin - @testset "RemovalOfFixedVariables" begin - @test ClA._fix_var(0.99, 1.01, 0.1) - @test !ClA._fix_var(0.98, 1.1, 0.1) - @test ClA._fix_var(1.5, 1.5, 0.1) - - @test_broken !ClA._infeasible_var(1.09, 0.91, 0.1) - @test ClA._infeasible_var(1.2, 0.9, 0.1) - - # Create the following formulation: - # min x1 + 2x2 + 3x3 - # st. x1 >= 1 - # x2 >= 2 - # x3 >= 3 - # x1 + 2x2 + 3x3 >= 10 - env = CL.Env{ClMP.VarId}(CL.Params()) - form = ClMP.create_formulation!(env, ClMP.DwMaster()) - vars = Dict{String, ClMP.Variable}() - for i in 1:3 - x = ClMP.setvar!(form, "x$i", ClMP.OriginalVar; cost = i, lb = i) - vars["x$i"] = x - end - - members = Dict{ClMP.VarId,Float64}( - ClMP.getid(vars["x1"]) => 1, - ClMP.getid(vars["x2"]) => 2, - ClMP.getid(vars["x3"]) => 3 - ) - c = ClMP.setconstr!(form, "c", ClMP.OriginalConstr; - rhs = 10, sense = ClMP.Greater, members = members - ) - DynamicSparseArrays.closefillmode!(ClMP.getcoefmatrix(form)) - - @test ClMP.getcurrhs(form, c) == 10 +# @testset "Algorithm - Presolve" begin +# @testset "RemovalOfFixedVariables" begin +# @test ClA._fix_var(0.99, 1.01, 0.1) +# @test !ClA._fix_var(0.98, 1.1, 0.1) +# @test ClA._fix_var(1.5, 1.5, 0.1) + +# @test_broken !ClA._infeasible_var(1.09, 0.91, 0.1) +# @test ClA._infeasible_var(1.2, 0.9, 0.1) + +# # Create the following formulation: +# # min x1 + 2x2 + 3x3 +# # st. x1 >= 1 +# # x2 >= 2 +# # x3 >= 3 +# # x1 + 2x2 + 3x3 >= 10 +# env = CL.Env{ClMP.VarId}(CL.Params()) +# form = ClMP.create_formulation!(env, ClMP.DwMaster()) +# vars = Dict{String, ClMP.Variable}() +# for i in 1:3 +# x = ClMP.setvar!(form, "x$i", ClMP.OriginalVar; cost = i, lb = i) +# vars["x$i"] = x +# end + +# members = Dict{ClMP.VarId,Float64}( +# ClMP.getid(vars["x1"]) => 1, +# ClMP.getid(vars["x2"]) => 2, +# ClMP.getid(vars["x3"]) => 3 +# ) +# c = ClMP.setconstr!(form, "c", ClMP.OriginalConstr; +# rhs = 10, sense = ClMP.Greater, members = members +# ) +# DynamicSparseArrays.closefillmode!(ClMP.getcoefmatrix(form)) + +# @test ClMP.getcurrhs(form, c) == 10 - ClMP.setcurlb!(form, vars["x1"], 2) - ClMP.setcurub!(form, vars["x1"], 2) - @test ClMP.getcurrhs(form, c) == 10 +# ClMP.setcurlb!(form, vars["x1"], 2) +# ClMP.setcurub!(form, vars["x1"], 2) +# @test ClMP.getcurrhs(form, c) == 10 - ClA.treat!(form, ClA.RemovalOfFixedVariables(1e-6)) +# ClA.treat!(form, ClA.RemovalOfFixedVariables(1e-6)) - @test ClMP.getcurrhs(form, c) == 10 - 2 +# @test ClMP.getcurrhs(form, c) == 10 - 2 - ClMP.setcurlb!(form, vars["x2"], 3) - ClMP.setcurub!(form, vars["x2"], 3) +# ClMP.setcurlb!(form, vars["x2"], 3) +# ClMP.setcurub!(form, vars["x2"], 3) - ClA.treat!(form, ClA.RemovalOfFixedVariables(1e-6)) +# ClA.treat!(form, ClA.RemovalOfFixedVariables(1e-6)) - @test ClMP.getcurrhs(form, c) == 10 - 2 - 2*3 +# @test ClMP.getcurrhs(form, c) == 10 - 2 - 2*3 - ClMP.unfix!(form, vars["x1"]) - ClMP.setcurlb!(form, vars["x1"], 1) - ClA.treat!(form, ClA.RemovalOfFixedVariables(1e-6)) - @test ClMP.getcurrhs(form, c) == 10 - 2*3 - return - end -end \ No newline at end of file +# ClMP.unfix!(form, vars["x1"]) +# ClMP.setcurlb!(form, vars["x1"], 1) +# ClA.treat!(form, ClA.RemovalOfFixedVariables(1e-6)) +# @test ClMP.getcurrhs(form, c) == 10 - 2*3 +# return +# end +# end \ No newline at end of file diff --git a/test/unit/Algorithm/record_mastercolumns.jl b/test/unit/Algorithm/record_mastercolumns.jl index ff758a169..a67c22ea5 100644 --- a/test/unit/Algorithm/record_mastercolumns.jl +++ b/test/unit/Algorithm/record_mastercolumns.jl @@ -1,4 +1,4 @@ -@testset "Unit - MasterColumnsRecord" begin +function unit_master_columns_record() function test_record(state, cost, lb, ub, fixed) @test state.cost == cost @test state.lb == lb @@ -75,4 +75,5 @@ test_var(form, vars["v1"], 1, 5, Inf, false) test_var(form, vars["v2"], 2, 0, 12, false) test_var(form, vars["v3"], 4.6, 3.5, 3.5, true) -end \ No newline at end of file +end +register!(unit_tests, "master_columns_record", unit_master_columns_record) \ No newline at end of file diff --git a/test/unit/Algorithm/subsolvers.jl b/test/unit/Algorithm/subsolvers.jl index 42234f826..80482d4f9 100644 --- a/test/unit/Algorithm/subsolvers.jl +++ b/test/unit/Algorithm/subsolvers.jl @@ -1,54 +1,53 @@ -@testset "Algorithm - Subsolvers" begin - @testset "MoiOptimize - Parameters reset after optimize_with_moi!" begin - # Create the formulation: - # Min x - # s.t. x >= 1 - - env = Env{ClMP.VarId}(Coluna.Params()) - form = ClMP.create_formulation!(env, ClMP.Original(), obj_sense = ClMP.MinSense) - ClMP.setvar!(form, "x", ClMP.OriginalVar; cost = 1, lb = 1) - closefillmode!(ClMP.getcoefmatrix(form)) - - algo1 = ClA.MoiOptimize( - time_limit = 1200, - silent = false, - custom_parameters = Dict( - "it_lim" => 60 - ) +function reset_parameters_after_optimize_with_moi() + # Create the formulation: + # Min x + # s.t. x >= 1 + + env = Env{ClMP.VarId}(Coluna.Params()) + form = ClMP.create_formulation!(env, ClMP.Original(), obj_sense = ClMP.MinSense) + ClMP.setvar!(form, "x", ClMP.OriginalVar; cost = 1, lb = 1) + closefillmode!(ClMP.getcoefmatrix(form)) + + algo1 = ClA.MoiOptimize( + time_limit = 1200, + silent = false, + custom_parameters = Dict( + "it_lim" => 60 ) + ) - algo2 = ClA.MoiOptimize( - silent = false, - custom_parameters = Dict( - "mip_gap" => 0.03 - ) + algo2 = ClA.MoiOptimize( + silent = false, + custom_parameters = Dict( + "mip_gap" => 0.03 ) + ) - optimizer = ClMP.MoiOptimizer(MOI._instantiate_and_check(GLPK.Optimizer)) + optimizer = ClMP.MoiOptimizer(MOI._instantiate_and_check(GLPK.Optimizer)) - get_time_lim() = MOI.get(optimizer.inner, MOI.TimeLimitSec()) - get_silent() = MOI.get(optimizer.inner, MOI.Silent()) - get_it_lim() = MOI.get(optimizer.inner, MOI.RawOptimizerAttribute("it_lim")) - get_mip_gap() = MOI.get(optimizer.inner, MOI.RawOptimizerAttribute("mip_gap")) + get_time_lim() = MOI.get(optimizer.inner, MOI.TimeLimitSec()) + get_silent() = MOI.get(optimizer.inner, MOI.Silent()) + get_it_lim() = MOI.get(optimizer.inner, MOI.RawOptimizerAttribute("it_lim")) + get_mip_gap() = MOI.get(optimizer.inner, MOI.RawOptimizerAttribute("mip_gap")) - default_time_lim = get_time_lim() - default_silent = get_silent() - default_it_lim = get_it_lim() - default_mip_gap = get_mip_gap() + default_time_lim = get_time_lim() + default_silent = get_silent() + default_it_lim = get_it_lim() + default_mip_gap = get_mip_gap() - ClA.optimize_with_moi!(optimizer, form, algo1, ClA.OptimizationState(form)) + ClA.optimize_with_moi!(optimizer, form, algo1, ClA.OptimizationState(form)) - @test get_time_lim() == default_time_lim - @test get_silent() == default_silent - @test get_it_lim() == default_it_lim - @test get_mip_gap() == default_mip_gap + @test get_time_lim() == default_time_lim + @test get_silent() == default_silent + @test get_it_lim() == default_it_lim + @test get_mip_gap() == default_mip_gap - ClA.optimize_with_moi!(optimizer, form, algo2, ClA.OptimizationState(form)) + ClA.optimize_with_moi!(optimizer, form, algo2, ClA.OptimizationState(form)) - @test get_time_lim() == default_time_lim - @test get_silent() == default_silent - @test get_it_lim() == default_it_lim - @test get_mip_gap() == default_mip_gap - return - end -end \ No newline at end of file + @test get_time_lim() == default_time_lim + @test get_silent() == default_silent + @test get_it_lim() == default_it_lim + @test get_mip_gap() == default_mip_gap + return +end +register!(unit_tests, "subsolvers", reset_parameters_after_optimize_with_moi) \ No newline at end of file diff --git a/test/unit/Algorithm/treesearch_interface.jl b/test/unit/Algorithm/treesearch_interface.jl deleted file mode 100644 index fc29bb6af..000000000 --- a/test/unit/Algorithm/treesearch_interface.jl +++ /dev/null @@ -1,353 +0,0 @@ -using Parameters - -# # Tutorial for the tree search interface -# ## Introduction - -# This is a test (and also a tutorial) on how to use the tree search interface together with -# algorithms. -# -# We consider a minimization problem with four binary variables -# Costs of the variable are `[-1, 1, 1, 1]``. -# The problem has no additional constraints. -# Therefore, the optimal solution is `[1, 0, 0, 0]`. - -const LOG_ATI1 = true -const NB_VARIABLES_ATI1 = 4 - -struct FormulationAti1 <: ClB.AbstractModel - var_cost::Vector{Int} - var_domains::Vector{Tuple{Int,Int}} - FormulationAti1() = new([-1, 1, 1, 1], fill((0,1), NB_VARIABLES_ATI1)) -end - -# We are going to enumerate all possible values for the two first variables using -# a binary tree : -# - depth 0: branch on first variable -# - depth 1: branch on second variable -# -# When the two first variables are fixed, we dive to fix the third and the fourth -# variables to zero. -# -# At the end, the tree will look like: -# -# ```mermaid -# graph TD -# A0(root node) -->|x1 >= 1| A1 -# A0 -->|x1 <= 0| B1 -# A1 -->|x2 <= 0| A2 -# A1 -->|x2 >= 1| B2 -# B1 -->|x2 <= 0| C2 -# B1 -->|x2 >= 1| D2 -# A2 -->|x3 == 0| A3 -# B2 -->|x3 == 0| B3 -# C2 -->|x3 == 0| C3 -# D2 -->|x3 == 0| D3 -# A3 -->|x4 == 0| A4 -# B3 -->|x4 == 0| B4 -# C3 -->|x4 == 0| C4 -# D3 -->|x4 == 0| D4 -# style A3 stroke:red -# style A4 stroke:red -# style B3 stroke:red -# style B4 stroke:red -# style C3 stroke:red -# style C4 stroke:red -# style D3 stroke:red -# style D4 stroke:red -# ``` - -# ## Tree search data structures - -# Now, we define the two concepts we'll use in the tree search algorithms. -# The third concept is the explore strategy and implemented in Coluna (see explore.jl). -# We start by defining the search space of the binary tree and the diving algorithms. - -mutable struct BtSearchSpaceAti1 <: AbstractColunaSearchSpace - formulation::FormulationAti1 - cost_of_best_solution::Float64 - conquer_alg - divide_alg - previous - BtSearchSpaceAti1(form, conquer, divide) = new(form, Inf, conquer, divide, nothing) -end - -ClA.get_reformulation(sp::BtSearchSpaceAti1) = sp.formulation -ClA.get_conquer(sp::BtSearchSpaceAti1) = sp.conquer_alg -ClA.get_divide(sp::BtSearchSpaceAti1) = sp.divide_alg -ClA.get_previous(sp::BtSearchSpaceAti1) = sp.previous -ClA.set_previous!(sp::BtSearchSpaceAti1, previous) = sp.previous = previous -TreeSearch.stop(sp::BtSearchSpaceAti1, _) = false - -mutable struct DivingSearchSpaceAti1 <: AbstractColunaSearchSpace - formulation::FormulationAti1 - starting_node_in_bt::ClA.AbstractNode # change node - cost_of_best_solution::Float64 - conquer_alg - divide_alg - previous - DivingSearchSpaceAti1(form, node, conquer, divide) = new(form, node, Inf, conquer, divide, nothing) -end - -ClA.get_reformulation(sp::DivingSearchSpaceAti1) = sp.formulation -ClA.get_conquer(sp::DivingSearchSpaceAti1) = sp.conquer_alg -ClA.get_divide(sp::DivingSearchSpaceAti1) = sp.divide_alg -ClA.get_previous(sp::DivingSearchSpaceAti1) = sp.previous -ClA.set_previous!(sp::DivingSearchSpaceAti1, previous) = sp.previous = previous -TreeSearch.stop(sp::DivingSearchSpaceAti1, _) = false - -# At last, we define the data contained in a node. -struct NodeAti1 <: ClA.AbstractNode - depth::Int - fixed_var_index::Union{Nothing, Int} - fixed_var_value::Union{Nothing, Float64} - solution::Vector{Float64} - var_lbs::Vector{Int} - var_ubs::Vector{Int} - parent::Union{Nothing, NodeAti1} - function NodeAti1( - parent::Union{Nothing, NodeAti1} = nothing, - var_index::Union{Nothing,Int} = nothing, - var_value::Union{Nothing,Real} = 0 - ) - @assert isnothing(var_index) || 1 <= var_index <= NB_VARIABLES_ATI1 - depth = isnothing(parent) ? 0 : parent.depth + 1 - # Store the solution at this node. - solution = if isnothing(parent) - fill(0.5, NB_VARIABLES_ATI1) - else - sol = copy(parent.solution) - if !isnothing(var_index) - sol[var_index] = var_value - end - sol - end - # Store the state of the formulation. - var_lbs = map(var_val -> var_val == 0.5 ? 0 : var_val, solution) - var_ubs = map(var_val -> var_val == 0.5 ? 1 : var_val, solution) - return new( - depth, - var_index, - var_value, - solution, - var_lbs, - var_ubs, - parent - ) - end -end - -TreeSearch.get_root(node::NodeAti1) = isnothing(node.parent) ? node : ClA.root(node.parent) -TreeSearch.get_parent(node::NodeAti1) = node.parent - -# ## Algorithms - -# Let's define the algorithm that we will use: -# At each node, we define an algorithm `ComputeSolCostAti1` that compute the cost of the -# solution and returns its value. - -@with_kw struct ComputeSolCostAti1 <: ClA.AbstractAlgorithm - log::String = "compute solution cost" -end - -struct ComputeSolCostInputAti1 - current_node::NodeAti1 -end - -function ClA.run!(algo::ComputeSolCostAti1, env, model::FormulationAti1, input::ComputeSolCostInputAti1) - sol_cost = 0.0 - for (cost, (ub, lb)) in Iterators.zip(model.var_cost, model.var_domains) - var_val = ub == lb ? ub : 0.5 - sol_cost += var_val * cost - end - return sol_cost -end - -# To generate, the children, we create an algorithm named `DivideAti1` that will create, -# for a given variable x, both branches x <= 0 & x >= 1 or only branch x = 0 depending on -# parameters chosen. -@with_kw struct DivideAti1 <: ClA.AbstractAlgorithm - log::String = "classic divide" - create_both_branches::Bool = true -end - -struct DivideInputAti1 - current_node::NodeAti1 -end - -function ClA.run!(algo::DivideAti1, env, ::FormulationAti1, input::DivideInputAti1) - LOG_ATI1 && println(algo.log) - parent = input.current_node - if algo.create_both_branches && parent.depth < 2 - var_pos_to_branch_in = parent.depth + 1 - var_pos_to_branch_in > 4 && return [] - LOG_ATI1 && println("** branch on x$(var_pos_to_branch_in) == 0 & x$(var_pos_to_branch_in) == 1") - return [(var_pos_to_branch_in, 0), (var_pos_to_branch_in, 1)] - elseif !algo.create_both_branches - var_pos_to_branch_in = parent.depth - var_pos_to_branch_in > 4 && return [] - LOG_ATI1 && println("** branch on x$(var_pos_to_branch_in) == 0") - return [(var_pos_to_branch_in, 0)] - end - return [] -end - -# The diving is a tree search algorithm that uses: -# - `ComputeSolCostAti1` as conquer strategy -# - `DivideAti1` with parameter `create_both_branches` equals to `false` as divide strategy -# - `Coluna.Algorithm.DepthFirstStrategy` as explore strategy -@with_kw struct DivingAti1 <: ClA.AbstractAlgorithm - conqueralg = ComputeSolCostAti1(log="compute solution cost of Diving tree") - dividealg = DivideAti1( - log = "divide for diving", - create_both_branches = false - ) - explore = ClA.DepthFirstStrategy() -end - -struct DivingInputAti1 - starting_node_in_parent_algorithm -end - -function ClA.run!(algo::DivingAti1, env, model::FormulationAti1, input::DivingInputAti1) - diving_space = TreeSearch.new_space(TreeSearch.search_space_type(algo), algo, model, input) - output = ClA.tree_search(algo.explore, diving_space, env, input) - return output -end - -# At last, we define the algorithm that will conquer each node of the binary tree algorithm. -# It runs the `ComputeSolCostAti1` algorithm and then the diving algorithm if the two first -# variables have been fixed (i.e. if depth == 2). -@with_kw struct BtConquerAti1 <: ClA.AbstractAlgorithm - compute = ComputeSolCostAti1(log = "compute solution cost for Binary tree") - heuristic = DivingAti1() -end - -function ClA.run!(algo::BtConquerAti1, env, model, input) - output = ClA.run!(algo.compute, env, model, input) - diving_output = Inf - if input.current_node.depth == 2 - diving_input = DivingInputAti1(input.current_node) # TODO: needs an interface or specific to the algorithm ? - diving_output = ClA.run!(algo.heuristic, env, model, diving_input) - end - return min(output, diving_output) -end - -# ## Interface implementation - -@with_kw struct TreeSearchAlgorithmAti1 - conqueralg = ClA.ColCutGenConquer() - dividealg = ClA.Branching() - explorestrategy = ClA.DepthFirstStrategy() -end - -function ClA.run!(algo::TreeSearchAlgorithmAti1, env, reform, input) - search_space = TreeSearch.new_space(TreeSearch.search_space_type(algo), algo, reform, input) - return ClA.tree_search(algo.explorestrategy, search_space, env, input) -end - -# We start by implementing methods that create the search space and the root node for each -# tree search algorithm that will be run. - -# First, we must indicate the type of search space used by our algorithms. -# We need such a method because the type may depends from the algorithms called by the -# tree-search algorithm. -TreeSearch.search_space_type(::TreeSearchAlgorithmAti1) = BtSearchSpaceAti1 -TreeSearch.search_space_type(::DivingAti1) = DivingSearchSpaceAti1 - -# The type of the search space is known from above method. -# A search space may receive information from the tree-search algorithm. -# The `model`, and `input` arguments are those received by the tree search algorithm. -TreeSearch.new_space(::Type{BtSearchSpaceAti1}, alg, model, input) = - BtSearchSpaceAti1(model, alg.conqueralg, alg.dividealg) -TreeSearch.new_space(::Type{DivingSearchSpaceAti1}, alg, model, input) = - DivingSearchSpaceAti1(model, input.starting_node_in_parent_algorithm, alg.conqueralg, alg.dividealg) - -# The definition of the root node depends on the search space. -TreeSearch.new_root(::BtSearchSpaceAti1, input) = NodeAti1() -TreeSearch.new_root(space::DivingSearchSpaceAti1, input) = - NodeAti1(space.starting_node_in_bt) - -# Then, we implement the method that converts the branching rules into nodes for the tree -# search algorithm. -function ClA.new_children(::ClA.AbstractColunaSearchSpace, branches, node::NodeAti1) - children = NodeAti1[] - for (var_pos, var_val_fixed) in branches - child = NodeAti1(node, var_pos, var_val_fixed) - push!(children, child) - end - return children -end - -struct CustomBestFirstSearchAti1 <: ClA.AbstractBestFirstSearch end - -# We implement the priority method for the `CustomBestFirstSearchAti1` strategy. -# The tree search algorithm will evaluate the node with highest priority. -TreeSearch.get_priority(::CustomBestFirstSearchAti1, node::NodeAti1) = -node.depth - -# We implement the `node_change` method to update the search space when the tree search -# just after the algorithm finishes to evaluate a node and chooses the next one. - -# There are two ways to store the state of a formulation at a given node. -# We can distribute information across the nodes or store the whole state at each node. -# We follow the second way (so we don't need `previous`). -function ClA.node_change!(::NodeAti1, next::NodeAti1, space::ClA.AbstractColunaSearchSpace, _) - for (var_pos, bounds) in enumerate(Iterators.zip(next.var_lbs, next.var_ubs)) - space.formulation.var_domains[var_pos] = bounds - end - return -end - -# We implement methods that update the best solution found after the conquer algorithm. -# One method for each search space. -function ClA.after_conquer!(space::BtSearchSpaceAti1, current, output) - if output < space.cost_of_best_solution - space.cost_of_best_solution = output - end - return -end - -function ClA.after_conquer!(space::DivingSearchSpaceAti1, current, output) - if output < space.cost_of_best_solution - space.cost_of_best_solution = output - end - return -end - -# We implement getters to retrieve the input from the search space and the node. -# The input is passed to the conquer and the divide algorithms. -ClA.get_input(::BtConquerAti1, space::BtSearchSpaceAti1, node::NodeAti1) = - ComputeSolCostInputAti1(node) -ClA.get_input(::DivideAti1, space::BtSearchSpaceAti1, node::NodeAti1) = - DivideInputAti1(node) -ClA.get_input(::ComputeSolCostAti1, space::DivingSearchSpaceAti1, node::NodeAti1) = - ComputeSolCostInputAti1(node) -ClA.get_input(::DivideAti1, space::DivingSearchSpaceAti1, node::NodeAti1) = - DivideInputAti1(node) - -# At last, we implement methods that will return the output of the tree search algorithms. -# One method for each search space. -TreeSearch.tree_search_output(space::BtSearchSpaceAti1, _) = space.cost_of_best_solution -TreeSearch.tree_search_output(space::DivingSearchSpaceAti1, _) = space.cost_of_best_solution - -@testset "Algorithm - treesearch interface" begin - env = nothing - model = FormulationAti1() - input = nothing - - treesearch = TreeSearchAlgorithmAti1( - conqueralg = BtConquerAti1(), - dividealg = DivideAti1(), - explorestrategy = ClA.DepthFirstStrategy() - ) - - output = ClA.run!(treesearch, env, model, input) - @test output == -1 - - treesearch = TreeSearchAlgorithmAti1( - conqueralg = BtConquerAti1(), - dividealg = DivideAti1(), - explorestrategy = CustomBestFirstSearchAti1() - ) - output = ClA.run!(treesearch, env, model, input) - @test output == -1 -end diff --git a/test/unit/run.jl b/test/unit/run.jl index db9f8aa46..a310f2691 100644 --- a/test/unit/run.jl +++ b/test/unit/run.jl @@ -1,4 +1,4 @@ -for dir in ["MustImplement", "ColunaBase", "MathProg", "ColGen", "Benders", "Branching"] +for dir in ["MustImplement", "ColunaBase", "MathProg", "ColGen", "Benders", "Branching", "Algorithm"] dirpath = joinpath(@__DIR__, dir) for filename in readdir(dirpath) includet(joinpath(dirpath, filename))