diff --git a/include/packingsolver/irregular/optimize.hpp b/include/packingsolver/irregular/optimize.hpp index 70a5eafea..99107df58 100644 --- a/include/packingsolver/irregular/optimize.hpp +++ b/include/packingsolver/irregular/optimize.hpp @@ -246,6 +246,12 @@ struct OptimizeParameters: packingsolver::Parameters /** Number of iterations of the sequential value correction algorithm. */ Counter not_anytime_sequential_value_correction_number_of_iterations = 32; + /** + * Maximum number of iterations without improvement when the local search + * packs an item, after which the item is considered not to fit. + */ + Counter not_anytime_local_search_maximum_number_of_iterations_without_improvement = 100; + /** * Size of the queue in the bin packing subproblem of the dichotomic search * algorithm. diff --git a/python/src/irregular.cpp b/python/src/irregular.cpp index 9fc7c306d..c96c054dc 100644 --- a/python/src/irregular.cpp +++ b/python/src/irregular.cpp @@ -671,6 +671,7 @@ void bind_irregular(nb::module_& m) .def_rw("not_anytime_tree_search_periodic_packing_queue_size", &OptimizeParameters::not_anytime_tree_search_periodic_packing_queue_size) .def_rw("not_anytime_sequential_single_knapsack_subproblem_tree_search_queue_size", &OptimizeParameters::not_anytime_sequential_single_knapsack_subproblem_tree_search_queue_size) .def_rw("not_anytime_sequential_value_correction_number_of_iterations", &OptimizeParameters::not_anytime_sequential_value_correction_number_of_iterations) + .def_rw("not_anytime_local_search_maximum_number_of_iterations_without_improvement", &OptimizeParameters::not_anytime_local_search_maximum_number_of_iterations_without_improvement) .def_rw("not_anytime_dichotomic_search_subproblem_tree_search_queue_size", &OptimizeParameters::not_anytime_dichotomic_search_subproblem_tree_search_queue_size) .def_rw("reduction_parameters", &OptimizeParameters::reduction_parameters); diff --git a/python/tests/test_irregular.py b/python/tests/test_irregular.py index eeed7b206..c8cb918e4 100644 --- a/python/tests/test_irregular.py +++ b/python/tests/test_irregular.py @@ -529,6 +529,9 @@ def test_parameters(): parameters.use_tree_search = True parameters.not_anytime_tree_search_queue_size = 64 assert parameters.not_anytime_tree_search_queue_size == 64 + assert parameters.not_anytime_local_search_maximum_number_of_iterations_without_improvement == 100 + parameters.not_anytime_local_search_maximum_number_of_iterations_without_improvement = 10 + assert parameters.not_anytime_local_search_maximum_number_of_iterations_without_improvement == 10 parameters.initial_maximum_approximation_ratio = 0.1 assert parameters.initial_maximum_approximation_ratio == pytest.approx(0.1) parameters.reduction_parameters.reduce = False @@ -538,6 +541,37 @@ def test_parameters(): assert output.solution.number_of_bins() == 2 +@pytest.mark.parametrize("copies", [1, 4]) +@pytest.mark.parametrize("reduce", [True, False]) +def test_local_search_bin_packing_with_leftovers(copies, reduce, tmp_path, monkeypatch): + """The local search used to loop on the infeasible sub-instances of the + leftovers, until an LP interrupted by the time limit made it throw ('wrong + LP solution'). It also used to write files in the working directory.""" + monkeypatch.chdir(tmp_path) + instance_builder = psi.InstanceBuilder() + instance_builder.set_objective(psi.Objective.BinPackingWithLeftovers) + instance_builder.add_bin_type(square(10)) + instance_builder.add_item_type(item_shapes(square(5)), copies=copies) + parameters = quiet_parameters(use_local_search=True, use_tree_search=False) + parameters.reduction_parameters.reduce = reduce + output = psi.optimize(instance_builder.build(), parameters) + assert output.solution.feasible() + assert output.solution.full() + assert list(tmp_path.iterdir()) == [] + + +def test_local_search_full_bin(): + """An item assigned to a bin without free space used to be placed inside + another item, which made the local search throw ('violated separation + constraint').""" + instance_builder = psi.InstanceBuilder() + instance_builder.read(os.path.join(DATA_DIR, "multiple_bins.json")) + parameters = quiet_parameters(use_local_search=True, use_tree_search=False) + output = psi.optimize(instance_builder.build(), parameters) + assert output.solution.feasible() + assert output.solution.full() + + def test_lifetimes(): """Outputs and solutions stay valid after their instance is dropped.""" output = psi.optimize(bin_packing_instance(5), quiet_parameters()) diff --git a/src/irregular/linear_programming.cpp b/src/irregular/linear_programming.cpp index ea10476cf..5862e4f24 100644 --- a/src/irregular/linear_programming.cpp +++ b/src/irregular/linear_programming.cpp @@ -3,8 +3,6 @@ #include "irregular/utils.hpp" #include "irregular/solution_builder.hpp" -#include "shape/writer.hpp" - #ifdef CBC_FOUND #include "mathoptsolverscmake/mathopt_cbc.hpp" #endif @@ -165,25 +163,6 @@ EdgeSeparationConstraintParameters packingsolver::irregular::find_best_edge_sepa + output.coef_lambda2 * scale_2; //if (shape::strictly_lesser(value, 0.0)) { if (shape::strictly_lesser(value, -1e-6)) { - std::cout << "shift_1 " << shift_1.to_string() - << " shift_2 " << shift_2.to_string() << std::endl; - std::cout << "scale_1 " << scale_1 - << " scale_2 " << scale_2 - << std::endl; - Shape shape_1_shifted = shape_1; - Shape shape_2_shifted = shape_2; - shape_1_shifted.shift(shift_1.x, shift_1.y); - shape_2_shifted.shift(shift_2.x, shift_2.y); - Shape shape_1_scaled = scale_1 * shape_1; - Shape shape_2_scaled = scale_2 * shape_2; - shape_1_scaled.shift(shift_1.x, shift_1.y); - shape_2_scaled.shift(shift_2.x, shift_2.y); - Writer() - .add_shape(shape_1) - .add_shape(shape_1_scaled) - .add_shape(shape_2) - .add_shape(shape_2_scaled) - .write_json("tmp.json"); const ShapeElement& edge_element = ((output.edge_shape_pos == 0)? shape_1.elements[output.edge_element_pos]: shape_2.elements[output.edge_element_pos]); @@ -324,10 +303,10 @@ Solution linear_programming_anchor( item_var_pos < (ItemPos)unfixed_items.size(); ++item_var_pos) { const AxisAlignedBoundingBox& ia = unfixed_item_aabbs[item_var_pos].item_aabb; - LengthDbl x_min = (x_weight < 0)? solution.x_min(): bin_type.aabb_scaled.x_min; - LengthDbl x_max = (x_weight > 0)? solution.x_max(): bin_type.aabb_scaled.x_max; - LengthDbl y_min = (y_weight < 0)? solution.y_min(): bin_type.aabb_scaled.y_min; - LengthDbl y_max = (y_weight > 0)? solution.y_max(): bin_type.aabb_scaled.y_max; + LengthDbl x_min = (x_weight < 0)? solution.x_min(): bin_type.aabb_orig.x_min; + LengthDbl x_max = (x_weight > 0)? solution.x_max(): bin_type.aabb_orig.x_max; + LengthDbl y_min = (y_weight < 0)? solution.y_min(): bin_type.aabb_orig.y_min; + LengthDbl y_max = (y_weight > 0)? solution.y_max(): bin_type.aabb_orig.y_max; item_bin_bounds[item_var_pos].x_min = x_min * instance.parameters().scale_value - ia.x_min; item_bin_bounds[item_var_pos].x_max = x_max * instance.parameters().scale_value - ia.x_max; item_bin_bounds[item_var_pos].y_min = y_min * instance.parameters().scale_value - ia.y_min; @@ -575,6 +554,14 @@ Solution linear_programming_anchor( //std::cout << "LP solve start" << std::endl; mathoptsolverscmake::solve(highs); //std::cout << "LP solve end" << std::endl; + if (highs.getModelStatus() != HighsModelStatus::kOptimal) { + // If the time limit is reached, keep the current solution. + if (parameters.timer.needs_to_end()) + break; + throw std::runtime_error( + FUNC_SIGNATURE + ": LP not solved to optimality; " + "model status: " + highs.modelStatusToString(highs.getModelStatus()) + "."); + } lp_solution = mathoptsolverscmake::get_solution(highs); #else throw std::invalid_argument(FUNC_SIGNATURE); @@ -842,10 +829,10 @@ LinearProgrammingMinimizeShrinkageOutput packingsolver::irregular::linear_progra unfixed_item_aabbs[item_var_pos].part_movement_aabbs[shape_pos][part_pos]; // RHS: part movement AABB clamped to bin bounds. - const LengthDbl x_min = (std::max)(pm.x_min, bin_type.aabb_scaled.x_min * sv); - const LengthDbl x_max = (std::min)(pm.x_max, bin_type.aabb_scaled.x_max * sv); - const LengthDbl y_min = (std::max)(pm.y_min, bin_type.aabb_scaled.y_min * sv); - const LengthDbl y_max = (std::min)(pm.y_max, bin_type.aabb_scaled.y_max * sv); + const LengthDbl x_min = (std::max)(pm.x_min, bin_type.aabb_scaled.x_min); + const LengthDbl x_max = (std::min)(pm.x_max, bin_type.aabb_scaled.x_max); + const LengthDbl y_min = (std::max)(pm.y_min, bin_type.aabb_scaled.y_min); + const LengthDbl y_max = (std::min)(pm.y_max, bin_type.aabb_scaled.y_max); const std::string sid = std::to_string(item_pos) + "_s" + std::to_string(shape_pos) @@ -1122,13 +1109,14 @@ LinearProgrammingMinimizeShrinkageOutput packingsolver::irregular::linear_progra highs.setOptionValue("parallel", "off"); mathoptsolverscmake::load(highs, lp_model); //mathoptsolverscmake::write_mps(highs, "lp.mps"); - lp_model.write_solution(lp_initial_solution, "initial_solution.txt"); mathoptsolverscmake::solve(highs); - if (highs.getModelStatus() == HighsModelStatus::kInfeasible - || highs.getModelStatus() == HighsModelStatus::kUnboundedOrInfeasible) { - highs.writeModel("infeasible.mps"); - std::cerr << "linear_programming_minimize_shrinkage: LP infeasible, wrote infeasible.mps" << std::endl; - exit(1); + if (highs.getModelStatus() != HighsModelStatus::kOptimal) { + // If the time limit is reached, keep the current solution. + if (parameters.timer.needs_to_end()) + break; + throw std::runtime_error( + FUNC_SIGNATURE + ": LP not solved to optimality; " + "model status: " + highs.modelStatusToString(highs.getModelStatus()) + "."); } lp_solution = mathoptsolverscmake::get_solution(highs); #else @@ -1139,7 +1127,6 @@ LinearProgrammingMinimizeShrinkageOutput packingsolver::irregular::linear_progra } if (!lp_model.check_solution(lp_solution, 0)) { - lp_model.check_solution(lp_solution, 4); throw std::logic_error( FUNC_SIGNATURE + ": wrong LP solution."); } @@ -1237,86 +1224,13 @@ LinearProgrammingMinimizeShrinkageOutput packingsolver::irregular::linear_progra + "_i" + std::to_string(e.item_2_var_pos) + "_s" + std::to_string(e.item_shape_2_pos) + "_p" + std::to_string(e.item_part_2_pos); - std::cout << "constraint " << constraint_name << std::endl; - - std::cout << "item_1_pos " << item_1_pos - << " item_type_1 " << item_1_prev.item_type_id - << " item_1_shape_pos " << e.item_shape_1_pos - << " item_1_part_pos " << e.item_part_1_pos - << " angle " << item_1_prev.angle - << " mirror " << item_1_prev.mirror - << std::endl; - std::cout << " bl " << (sv * item_1_prev.bl_corner).to_string() - << " -> " << (sv * item_1_curr.bl_corner).to_string() << std::endl; - std::cout << " lambda " << current_lambda[item_1_pos] - << " -> " << new_lambda[item_1_pos] << std::endl; - std::cout << "item_2_pos " << item_2_pos - << " item_type_2 " << item_2_prev.item_type_id - << " item_2_shape_pos " << e.item_shape_2_pos - << " item_2_part_pos " << e.item_part_2_pos - << " angle " << item_2_prev.angle - << " mirror " << item_2_prev.mirror - << std::endl; - std::cout << " bl " << (sv * item_2_prev.bl_corner).to_string() - << " -> " << (sv * item_2_curr.bl_corner).to_string() << std::endl; - std::cout << " lambda " << current_lambda[item_2_pos] - << " -> " << new_lambda[item_2_pos] << std::endl; - - const EdgeSeparationConstraintParameters p = find_best_edge_separator( - convex_part_1, - sv * item_1_prev.bl_corner, - current_lambda[item_1_pos], - convex_part_2, - sv * item_2_prev.bl_corner, - current_lambda[item_2_pos]); - std::cout << "shape_pos " << p.edge_shape_pos - << " edge_element_pos " << p.edge_element_pos - << " point_element_pos " << p.point_element_pos - << " distance " << p.distance - << std::endl; - std::cout << "edge_element " << ((p.edge_shape_pos == 0)? - convex_part_1.elements[p.edge_element_pos].to_string(): - convex_part_2.elements[p.edge_element_pos].to_string()) << std::endl; - std::cout << "point_element " << ((p.edge_shape_pos == 0)? - convex_part_2.elements[p.point_element_pos].to_string(): - convex_part_1.elements[p.point_element_pos].to_string()) << std::endl; - std::cout << "point " << p.point.to_string() << std::endl; - std::cout << "coef_x1 " << p.coef_x1 << " coef_y1 " << p.coef_y1 << " coef_lambda1 " << p.coef_lambda1 << std::endl; - std::cout << "coef_x2 " << p.coef_x2 << " coef_y2 " << p.coef_y2 << " coef_lambda2 " << p.coef_lambda2 << std::endl; - - Writer() - .add_shape(convex_part_1, "Part 1") - .add_shape(convex_part_2, "Part 2") - .add_shape(convex_part_1_prev, "Part 1 (prev)") - .add_shape(convex_part_1_curr, "Part 1 (curr)") - .add_shape(convex_part_2_prev, "Part 2 (prev)") - .add_shape(convex_part_2_curr, "Part 2 (curr)") - .write_json("tmp.json"); throw std::logic_error( - FUNC_SIGNATURE + ": convex part intersection after LP."); + FUNC_SIGNATURE + ": convex part intersection after LP; " + "constraint: " + constraint_name + "."); } Solution::OverlappingItems overlapping_items = new_solution.compute_overlapping_items(0, &new_lambda); if (!overlapping_items.item_item_pairs.empty()) { - const SolutionBin& bin = new_solution.bin(bin_pos); - for (const auto& pair: overlapping_items.item_item_pairs) { - const SolutionItem& item_1 = bin.items[pair.first]; - const ItemType& item_type_1 = instance.item_type(item_1.item_type_id); - ShapeWithHoles shape_1 = new_lambda[pair.first] * new_solution.shape_scaled(0, pair.first, 0); - ShapeWithHoles shape_1_prev = current_lambda[pair.first] * solution.shape_scaled(0, pair.first, 0); - - const SolutionItem& item_2 = bin.items[pair.second]; - const ItemType& item_type_2 = instance.item_type(item_2.item_type_id); - ShapeWithHoles shape_2 = new_lambda[pair.second] * new_solution.shape_scaled(0, pair.second, 0); - ShapeWithHoles shape_2_prev = current_lambda[pair.second] * solution.shape_scaled(0, pair.second, 0); - - Writer() - .add_shape_with_holes(shape_1_prev, "Shape 1 (prev)") - .add_shape_with_holes(shape_1, "Shape 1 (curr)") - .add_shape_with_holes(shape_2_prev, "Shape 2 (prev)") - .add_shape_with_holes(shape_2, "Shape 2 (curr)") - .write_json("tmp.json"); - } throw std::logic_error( FUNC_SIGNATURE + ": infeasible new_solution after LP."); } diff --git a/src/irregular/local_search.cpp b/src/irregular/local_search.cpp index b7a8bb1bd..9701421be 100644 --- a/src/irregular/local_search.cpp +++ b/src/irregular/local_search.cpp @@ -36,7 +36,8 @@ struct LocalSearchBinData * difference of the bin AABB minus existing item shapes, defects, and * borders). The bin is the valid one (item AABB fits) with the most remaining * area; falls back to the bin with the most remaining area if none fits. - * Returns the bin position the item was placed in. + * Returns the bin position the item was placed in, or -1 if the selected bin + * has no free space left (the item is then not added). */ BinPos assign_item_to_bin( const Instance& instance, @@ -132,14 +133,12 @@ BinPos assign_item_to_bin( } } - // Fall back to the bin centre if no free region was found. - Point bl_corner; - if (best_region_pos != -1) { - bl_corner = free_regions[best_region_pos].find_point_strictly_inside(); - } else { - bl_corner.x = (instance.parameters().scale_value * bin_aabb.x_min + instance.parameters().scale_value * bin_aabb.x_max) / 2.0; - bl_corner.y = (instance.parameters().scale_value * bin_aabb.y_min + instance.parameters().scale_value * bin_aabb.y_max) / 2.0; - } + // If the bin has no free space left, the item can't be placed in it: a + // point inside another item can't be separated from it by the + // shrinkage LP. + if (best_region_pos == -1) + return -1; + Point bl_corner = free_regions[best_region_pos].find_point_strictly_inside(); //std::cout << "bl_corner " << bl_corner.to_string() << std::endl; bl_corner = 1.0 / instance.parameters().scale_value * bl_corner; //std::cout << "bl_corner " << bl_corner.to_string() << std::endl; @@ -166,8 +165,11 @@ BinPos assign_item_to_bin( /** * Assign an item to a bin, then run minimize_shrinkage on that bin until it * is feasible (all items at full scale). Updates solution and bin_data in - * place. Returns false if the timer or end-flag fired before feasibility was - * reached. + * place. Returns false if the item fits in no bin, if feasibility wasn't + * reached within + * 'LocalSearchParameters::maximum_number_of_iterations_without_improvement' + * iterations without improvement, or if the timer or end-flag fired before + * feasibility was reached. */ bool pack_item( const Instance& instance, @@ -181,10 +183,17 @@ bool pack_item( if (algorithm_formatter.end_boolean() || parameters.timer.needs_to_end()) return false; + // 'optimize_item_types_fit' only checks the original instance: the bins + // of the sub-instances built by 'sequential_feasibility' are narrower. + if (!instance.fits_some_bin(item_type_id)) + return false; + const ItemType& item_type = instance.item_type(item_type_id); const BinPos bin_pos = assign_item_to_bin( instance, rng, item_type_id, solution, bin_data); + if (bin_pos == -1) + return false; bin_data[bin_pos].remaining_area -= item_type.area_scaled; bin_data[bin_pos].item_penalties.push_back(1.0); @@ -194,9 +203,18 @@ bool pack_item( bin_solution.append_bin(solution, bin_pos, 1); bool bin_feasible = false; + // Sum of the scale factors of the items of the bin: it measures how far + // the bin is from being feasible. + double best_lambda_sum = -1; + Counter number_of_iterations_without_improvement = 0; while (!bin_feasible) { if (algorithm_formatter.end_boolean() || parameters.timer.needs_to_end()) return false; + if (parameters.maximum_number_of_iterations_without_improvement != -1 + && number_of_iterations_without_improvement + >= parameters.maximum_number_of_iterations_without_improvement) { + return false; + } LinearProgrammingMinimizeShrinkageParameters lp_params; lp_params.timer = parameters.timer; @@ -211,6 +229,16 @@ bool pack_item( bin_feasible = lp_output.feasible; bin_data[bin_pos].lambda = lp_output.final_lambda; + double lambda_sum = 0; + for (double lambda: lp_output.final_lambda) + lambda_sum += lambda; + if (shape::strictly_greater(lambda_sum, best_lambda_sum)) { + best_lambda_sum = lambda_sum; + number_of_iterations_without_improvement = 0; + } else { + number_of_iterations_without_improvement++; + } + for (ItemPos item_pos = 0; item_pos < (ItemPos)lp_output.items_shrunken.size(); ++item_pos) { @@ -254,6 +282,8 @@ LocalSearchOutput packingsolver::irregular::local_search( inner_parameters.timer = parameters.timer; inner_parameters.timer.add_end_boolean(&algorithm_formatter.end_boolean()); inner_parameters.seed = parameters.seed; + inner_parameters.maximum_number_of_iterations_without_improvement + = parameters.maximum_number_of_iterations_without_improvement; return local_search(sub_instance, inner_parameters).solution_pool; }; diff --git a/src/irregular/local_search.hpp b/src/irregular/local_search.hpp index 1169dd0c0..f990bc6af 100644 --- a/src/irregular/local_search.hpp +++ b/src/irregular/local_search.hpp @@ -25,6 +25,14 @@ struct LocalSearchParameters: packingsolver::Parameters(), "") ("not-anytime-sequential-single-knapsack-subproblem-tree-search-queue-size,", po::value(), "") ("not-anytime-sequential-value-correction-number-of-iterations,", po::value(), "") + ("not-anytime-local-search-maximum-number-of-iterations-without-improvement,", po::value(), "") ("not-anytime-dichotomic-search-subproblem-tree-search-queue-size,", po::value(), "") ("group-identical-bins,", po::value(), "") @@ -193,6 +194,8 @@ int main(int argc, char *argv[]) parameters.not_anytime_sequential_single_knapsack_subproblem_tree_search_queue_size = vm["not-anytime-sequential-single-knapsack-subproblem-tree-search-queue-size"].as(); if (vm.count("not-anytime-sequential-value-correction-number-of-iterations")) parameters.not_anytime_sequential_value_correction_number_of_iterations = vm["not-anytime-sequential-value-correction-number-of-iterations"].as(); + if (vm.count("not-anytime-local-search-maximum-number-of-iterations-without-improvement")) + parameters.not_anytime_local_search_maximum_number_of_iterations_without_improvement = vm["not-anytime-local-search-maximum-number-of-iterations-without-improvement"].as(); if (vm.count("not-anytime-dichotomic-search-subproblem-tree-search-queue-size")) parameters.not_anytime_dichotomic_search_subproblem_tree_search_queue_size = vm["not-anytime-dichotomic-search-subproblem-tree-search-queue-size"].as(); const irregular::Output output = optimize(instance, parameters); diff --git a/src/irregular/optimize.cpp b/src/irregular/optimize.cpp index 1e1ec9d5e..5914ec4ee 100644 --- a/src/irregular/optimize.cpp +++ b/src/irregular/optimize.cpp @@ -319,6 +319,8 @@ void optimize_tree_search( last_bin_parameters.optimization_mode = parameters.optimization_mode; last_bin_parameters.not_anytime_maximum_approximation_ratio = parameters.not_anytime_maximum_approximation_ratio; last_bin_parameters.not_anytime_tree_search_queue_size = parameters.not_anytime_tree_search_queue_size; + last_bin_parameters.not_anytime_local_search_maximum_number_of_iterations_without_improvement + = parameters.not_anytime_local_search_maximum_number_of_iterations_without_improvement; last_bin_parameters.tree_search_guides = {2, 3}; last_bin_parameters.linear_programming_solver_name = parameters.linear_programming_solver_name; // Respect the caller's explicit algorithm selection (if any), rather @@ -370,6 +372,10 @@ void optimize_local_search( LocalSearchParameters ls_parameters; ls_parameters.verbosity_level = 0; ls_parameters.timer = parameters.timer; + if (parameters.optimization_mode != OptimizationMode::Anytime) { + ls_parameters.maximum_number_of_iterations_without_improvement + = parameters.not_anytime_local_search_maximum_number_of_iterations_without_improvement; + } ls_parameters.new_solution_callback = [&algorithm_formatter, local_output]( const irregular::Output& ps_output) {