From c8fcf2114461c9034986c3380ff4663d243b0d70 Mon Sep 17 00:00:00 2001 From: HansBug Date: Mon, 31 Aug 2026 13:37:09 +0800 Subject: [PATCH 1/3] boxstacks: fix out-of-range fixed_items_solutions index in sequential_onedimensional_rectangle When the middle axle weight constraint fails, the repair loop rescans fixed_items_solutions for the position to fix next. On the last entry, 'pos + 1 < fixed_items_solutions.size()' is false, so the break was skipped and the counter was incremented once more, leaving fixed_items_solutions_pos equal to fixed_items_solutions.size() whenever no earlier break happened. The clamp that follows only raises the index to the lower bound and the loop's end condition only breaks when the lower bound passes the upper bound, so the next iteration indexed one past the end and copy-constructed a Solution from that memory, which threw std::bad_array_new_length (std::bad_alloc for some objectives). Break on the last entry instead of running past it, so the index stays within the vector. --- src/boxstacks/sequential_onedimensional_rectangle.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/boxstacks/sequential_onedimensional_rectangle.cpp b/src/boxstacks/sequential_onedimensional_rectangle.cpp index 5383e42a4..e7685e4da 100644 --- a/src/boxstacks/sequential_onedimensional_rectangle.cpp +++ b/src/boxstacks/sequential_onedimensional_rectangle.cpp @@ -1277,8 +1277,8 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension fixed_items_solutions_pos_lower_bound = fixed_items_solutions_pos + 1; fixed_items_solutions_pos = 0; for (ItemPos pos = 0; pos < (ItemPos)fixed_items_solutions.size(); ++pos) { - if (pos + 1 < (ItemPos)fixed_items_solutions.size() - && fixed_items_solutions[pos + 1].x_max() > xi - x_max) + if (pos + 1 >= (ItemPos)fixed_items_solutions.size() + || fixed_items_solutions[pos + 1].x_max() > xi - x_max) break; //std::cout << "pos " << pos << " x " << fixed_items_x[pos] << " " << fixed_items_x[pos + 1] << std::endl; fixed_items_solutions_pos++; From d580742f28d0b4c16f0631a1fab5faefabe3c469 Mon Sep 17 00:00:00 2001 From: HansBug Date: Mon, 31 Aug 2026 18:13:08 +0800 Subject: [PATCH 2/3] boxstacks: add a regression test for the axle weight repair loop Two instances that enter the repair loop of sequential_onedimensional_rectangle, in a new parametrized suite over instance files, following the other parametrized tests of the project. Both throw std::bad_array_new_length without the fix. The bin packing case covers the objective the crash was reported with; the repair returns an empty solution for it, so the case only pins down that optimize returns at all. The knapsack case is the one that also pins down a non-empty result. The expected number of items is a minimum rather than an exact count, so the test does not have to be updated if the repair later packs more items. boxstacks has no SolutionBuilder::read, so the solutions are checked this way rather than against reference certificates. --- .../bins.csv | 2 + .../items.csv | 2 + .../parameters.csv | 2 + .../bins.csv | 2 + .../items.csv | 2 + .../parameters.csv | 2 + test/boxstacks/CMakeLists.txt | 3 +- test/boxstacks/axle_weight_test.cpp | 67 +++++++++++++++++++ 8 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/bins.csv create mode 100644 data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/items.csv create mode 100644 data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/parameters.csv create mode 100644 data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/bins.csv create mode 100644 data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/items.csv create mode 100644 data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/parameters.csv create mode 100644 test/boxstacks/axle_weight_test.cpp diff --git a/data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/bins.csv b/data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/bins.csv new file mode 100644 index 000000000..6173b1e5e --- /dev/null +++ b/data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/bins.csv @@ -0,0 +1,2 @@ +ID,X,Y,Z,COST,COPIES,MAXIMUM_WEIGHT,MAXIMUM_STACK_DENSITY,IS_SEMI_TRAILER_TRUCK,TRACTOR_WEIGHT,FRONT_AXLE_MIDDLE_AXLE_DISTANCE,FRONT_AXLE_TRACTOR_GRAVITY_CENTER_DISTANCE,FRONT_AXLE_HARNESS_DISTANCE,EMPTY_TRAILER_WEIGHT,HARNESS_REAR_AXLE_DISTANCE,TRAILER_GRAVITY_CENTER_REAR_AXLE_DISTANCE,TRAILER_START_HARNESS_DISTANCE,REAR_AXLE_MAXIMUM_WEIGHT,MIDDLE_AXLE_MAXIMUM_WEIGHT +0,1360,240,260,1,1,24000,1000,1,8000,380,100,320,6000,800,400,100,20000,9300 diff --git a/data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/items.csv b/data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/items.csv new file mode 100644 index 000000000..12b02013a --- /dev/null +++ b/data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/items.csv @@ -0,0 +1,2 @@ +ID,X,Y,Z,ROTATION_XYZ,ROTATION_YXZ,ROTATION_ZYX,ROTATION_YZX,ROTATION_XZY,ROTATION_ZXY,WEIGHT,COPIES,GROUP_ID,STACKABILITY_ID,NESTING_HEIGHT,MAXIMUM_STACKABILITY,MAXIMUM_WEIGHT_ABOVE +0,100,200,200,1,0,0,0,0,0,2000,3,0,0,0,1,10000 diff --git a/data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/parameters.csv b/data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/parameters.csv new file mode 100644 index 000000000..e72e86bc5 --- /dev/null +++ b/data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/parameters.csv @@ -0,0 +1,2 @@ +NAME,VALUE +objective,bin-packing diff --git a/data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/bins.csv b/data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/bins.csv new file mode 100644 index 000000000..6173b1e5e --- /dev/null +++ b/data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/bins.csv @@ -0,0 +1,2 @@ +ID,X,Y,Z,COST,COPIES,MAXIMUM_WEIGHT,MAXIMUM_STACK_DENSITY,IS_SEMI_TRAILER_TRUCK,TRACTOR_WEIGHT,FRONT_AXLE_MIDDLE_AXLE_DISTANCE,FRONT_AXLE_TRACTOR_GRAVITY_CENTER_DISTANCE,FRONT_AXLE_HARNESS_DISTANCE,EMPTY_TRAILER_WEIGHT,HARNESS_REAR_AXLE_DISTANCE,TRAILER_GRAVITY_CENTER_REAR_AXLE_DISTANCE,TRAILER_START_HARNESS_DISTANCE,REAR_AXLE_MAXIMUM_WEIGHT,MIDDLE_AXLE_MAXIMUM_WEIGHT +0,1360,240,260,1,1,24000,1000,1,8000,380,100,320,6000,800,400,100,20000,9300 diff --git a/data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/items.csv b/data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/items.csv new file mode 100644 index 000000000..12b02013a --- /dev/null +++ b/data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/items.csv @@ -0,0 +1,2 @@ +ID,X,Y,Z,ROTATION_XYZ,ROTATION_YXZ,ROTATION_ZYX,ROTATION_YZX,ROTATION_XZY,ROTATION_ZXY,WEIGHT,COPIES,GROUP_ID,STACKABILITY_ID,NESTING_HEIGHT,MAXIMUM_STACKABILITY,MAXIMUM_WEIGHT_ABOVE +0,100,200,200,1,0,0,0,0,0,2000,3,0,0,0,1,10000 diff --git a/data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/parameters.csv b/data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/parameters.csv new file mode 100644 index 000000000..5e7f5aca4 --- /dev/null +++ b/data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/parameters.csv @@ -0,0 +1,2 @@ +NAME,VALUE +objective,knapsack diff --git a/test/boxstacks/CMakeLists.txt b/test/boxstacks/CMakeLists.txt index 680dda002..a377a9499 100644 --- a/test/boxstacks/CMakeLists.txt +++ b/test/boxstacks/CMakeLists.txt @@ -1,6 +1,7 @@ add_executable(PackingSolver_boxstacks_test) target_sources(PackingSolver_boxstacks_test PRIVATE - boxstacks_test.cpp) + boxstacks_test.cpp + axle_weight_test.cpp) target_include_directories(PackingSolver_boxstacks_test PRIVATE ${PROJECT_SOURCE_DIR}/src) target_link_libraries(PackingSolver_boxstacks_test diff --git a/test/boxstacks/axle_weight_test.cpp b/test/boxstacks/axle_weight_test.cpp new file mode 100644 index 000000000..dd5ef5808 --- /dev/null +++ b/test/boxstacks/axle_weight_test.cpp @@ -0,0 +1,67 @@ +#include "packingsolver/boxstacks/instance_builder.hpp" +#include "packingsolver/boxstacks/optimize.hpp" + +#include +#include + +using namespace packingsolver::boxstacks; +namespace fs = boost::filesystem; + +struct BoxStacksAxleWeightTestParams +{ + fs::path items_path; + fs::path bins_path; + fs::path parameters_path; + + /** Minimum number of items the returned solution must pack. */ + packingsolver::ItemPos minimum_number_of_items; +}; + +inline std::ostream& operator<<(std::ostream& os, const BoxStacksAxleWeightTestParams& test_params) +{ + os << test_params.items_path; + return os; +} + +class BoxStacksAxleWeightTest: public testing::TestWithParam { }; + +TEST_P(BoxStacksAxleWeightTest, BoxStacksAxleWeight) +{ + BoxStacksAxleWeightTestParams test_params = GetParam(); + InstanceBuilder instance_builder; + instance_builder.read_item_types(test_params.items_path.string()); + instance_builder.read_bin_types(test_params.bins_path.string()); + instance_builder.read_parameters(test_params.parameters_path.string()); + Instance instance = instance_builder.build(); + + OptimizeParameters optimize_parameters; + optimize_parameters.optimization_mode = packingsolver::OptimizationMode::NotAnytimeSequential; + Output output = optimize(instance, optimize_parameters); + + // These instances enter the axle weight repair loop of + // 'sequential_onedimensional_rectangle', which used to read + // 'fixed_items_solutions' one element past the end and throw. + // The bound is a minimum rather than an exact count, so that the test + // does not have to be updated when the repair packs more items. + EXPECT_GE( + output.solution_pool.best().number_of_items(), + test_params.minimum_number_of_items); +} + +INSTANTIATE_TEST_SUITE_P( + BoxStacksAxleWeight, + BoxStacksAxleWeightTest, + testing::ValuesIn(std::vector{ + { + // The repair returns an empty solution for this instance; the + // point of the case is that 'optimize' returns at all. + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_bin_packing" / "items.csv", + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_bin_packing" / "bins.csv", + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_bin_packing" / "parameters.csv", + 0, + }, { + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_knapsack" / "items.csv", + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_knapsack" / "bins.csv", + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_knapsack" / "parameters.csv", + 1, + }})); From ff4c400d0b8aee81c82235495f8b524559f78c51 Mon Sep 17 00:00:00 2001 From: Florian Fontan Date: Sun, 6 Sep 2026 21:13:55 +0200 Subject: [PATCH 3/3] boxstacks: test the axle weight repair loop directly, with a certificate Move the regression test for the axle weight repair loop's off-by-one fix from axle_weight_test.cpp to sequential_onedimensional_rectangle_test.cpp and call sequential_onedimensional_rectangle() directly instead of optimize(), matching how the other algorithm-specific tests in this project (e.g. rectangleguillotine's sequential_strips_onedimensional_test) exercise their algorithm and compare against a certificate rather than going through the full solver and checking an item count. boxstacks::SolutionBuilder had no read(), and Solution::write() did not serialize item rotations, unlike box/rectangle/rectangleguillotine. Add both: a ROTATION column in Solution::write(), and SolutionBuilder::read() mirroring box's, so the new test can load a reference solution.csv and compare via Solution::operator< equivalence. Calling sequential_onedimensional_rectangle() directly (rather than through optimize(), which always resolves a real logger first) also exposed a separate, pre-existing bug: the function dereferences parameters.logger directly in several FFOT_LOG* calls, which is nullptr by default, crashing any direct caller that does not explicitly set a logger. Resolve parameters.get_logger() once at the top of both sequential_onedimensional_rectangle() and its sequential_onedimensional_rectangle_subproblem() helper and use that throughout, matching optimize()'s own pattern. --- .../solution.csv | 1 + .../solution.csv | 1 + .../sequential_onedimensional_rectangle.cpp | 62 ++++++++-------- src/boxstacks/solution.cpp | 8 +- src/boxstacks/solution_builder.cpp | 68 +++++++++++++++++ src/boxstacks/solution_builder.hpp | 3 + test/boxstacks/CMakeLists.txt | 2 +- test/boxstacks/axle_weight_test.cpp | 67 ----------------- ...quential_onedimensional_rectangle_test.cpp | 73 +++++++++++++++++++ 9 files changed, 185 insertions(+), 100 deletions(-) create mode 100644 data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/solution.csv create mode 100644 data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/solution.csv delete mode 100644 test/boxstacks/axle_weight_test.cpp create mode 100644 test/boxstacks/sequential_onedimensional_rectangle_test.cpp diff --git a/data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/solution.csv b/data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/solution.csv new file mode 100644 index 000000000..0a20cca79 --- /dev/null +++ b/data/boxstacks/tests/semi_trailer_truck_middle_axle_bin_packing/solution.csv @@ -0,0 +1 @@ +TYPE,ID,COPIES,BIN,STACK,X,Y,Z,LX,LY,LZ,GROUP_ID,ROTATION diff --git a/data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/solution.csv b/data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/solution.csv new file mode 100644 index 000000000..0a20cca79 --- /dev/null +++ b/data/boxstacks/tests/semi_trailer_truck_middle_axle_knapsack/solution.csv @@ -0,0 +1 @@ +TYPE,ID,COPIES,BIN,STACK,X,Y,Z,LX,LY,LZ,GROUP_ID,ROTATION diff --git a/src/boxstacks/sequential_onedimensional_rectangle.cpp b/src/boxstacks/sequential_onedimensional_rectangle.cpp index e7685e4da..d5a7cc3f6 100644 --- a/src/boxstacks/sequential_onedimensional_rectangle.cpp +++ b/src/boxstacks/sequential_onedimensional_rectangle.cpp @@ -214,9 +214,10 @@ SequentialOneDimensionalRectangleSubproblemOutput sequential_onedimensional_rect const std::vector>& rectangle2boxstacks, const rectangle::BranchingScheme::Parameters rectangle_parameters) { + auto logger = parameters.get_logger(); SequentialOneDimensionalRectangleSubproblemOutput output(instance); FFOT_LOG_FOLD_START( - parameters.logger, + logger, "it " << sor_output.number_of_iterations << " guide " << rectangle_parameters.guide_id << std::endl); @@ -241,7 +242,7 @@ SequentialOneDimensionalRectangleSubproblemOutput sequential_onedimensional_rect sor_output.number_of_rectangle_calls++; auto rectangle_solution = rectangle_branching_scheme.to_solution(rectangle_output.solution_pool.best()); FFOT_LOG( - parameters.logger, + logger, "rectangle_solution.number_of_items " << rectangle_solution.number_of_items() << " / " << rectangle_instance.number_of_items() << std::endl); @@ -404,7 +405,7 @@ SequentialOneDimensionalRectangleSubproblemOutput sequential_onedimensional_rect number_of_items_before_repair); output.profit_before_repair = solution.profit(); FFOT_LOG( - parameters.logger, + logger, "number of items " << solution.number_of_items() << std::endl << "profit " << solution.profit() << std::endl << "middle axle weight constraints violation " << solution.compute_middle_axle_weight_constraints_violation() << std::endl @@ -422,7 +423,7 @@ SequentialOneDimensionalRectangleSubproblemOutput sequential_onedimensional_rect parameters.new_solution_callback(sor_output); } output.solution = solution; - FFOT_LOG_FOLD_END(parameters.logger, ""); + FFOT_LOG_FOLD_END(logger, ""); return output; } @@ -430,8 +431,9 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension const Instance& instance, const SequentialOneDimensionalRectangleParameters& parameters) { + auto logger = parameters.get_logger(); FFOT_LOG_FOLD_START( - parameters.logger, + logger, "sequential_onedimensional_rectangle" << std::endl); SequentialOneDimensionalRectangleOutput output(instance); @@ -548,7 +550,7 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension } } FFOT_LOG( - parameters.logger, + logger, "fixed_items_solutions.size() " << fixed_items_solutions.size() << std::endl); @@ -561,7 +563,7 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension // Part of solution which is fixed. Solution fixed_items = fixed_items_solutions[fixed_items_solutions_pos]; FFOT_LOG_FOLD_START( - parameters.logger, + logger, "iteration " << output.number_of_iterations << std::endl << "fixed_items.number_of_items() " << fixed_items.number_of_items() << std::endl << "fixed_items.x_max() " << fixed_items.x_max() << std::endl); @@ -750,7 +752,7 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension auto onedim_solution = onedim_output.solution_pool.best(); if (parameters.timer.needs_to_end()) { - FFOT_LOG_FOLD_END(parameters.logger, ""); + FFOT_LOG_FOLD_END(logger, ""); algorithm_formatter.end(); return output; } @@ -789,7 +791,7 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension for (output.number_of_stack_splits = 0; output.number_of_stack_splits < 7; ++output.number_of_stack_splits) { - FFOT_LOG(parameters.logger, "number of splitted stacks " << output.number_of_stack_splits << std::endl); + FFOT_LOG(logger, "number of splitted stacks " << output.number_of_stack_splits << std::endl); Area stack_area = 0; for (BinPos bin_pos = 0; bin_pos < fixed_items.number_of_different_bins(); ++bin_pos) { @@ -982,8 +984,8 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension failed_middle_axle_weight_constraint_cur |= (subproblem_output.solution.compute_middle_axle_weight_constraints_violation() > 0); failed_rear_axle_weight_constraint_cur |= (subproblem_output.solution.compute_rear_axle_weight_constraints_violation() > 0); if (output.solution_pool.best().full()) { - FFOT_LOG_FOLD_END(parameters.logger, ""); - FFOT_LOG_FOLD_END(parameters.logger, ""); + FFOT_LOG_FOLD_END(logger, ""); + FFOT_LOG_FOLD_END(logger, ""); algorithm_formatter.end(); return output; } @@ -1011,8 +1013,8 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension failed_middle_axle_weight_constraint_cur |= (subproblem_output.solution.compute_middle_axle_weight_constraints_violation() > 0); failed_rear_axle_weight_constraint_cur |= (subproblem_output.solution.compute_rear_axle_weight_constraints_violation() > 0); if (output.solution_pool.best().full()) { - FFOT_LOG_FOLD_END(parameters.logger, ""); - FFOT_LOG_FOLD_END(parameters.logger, ""); + FFOT_LOG_FOLD_END(logger, ""); + FFOT_LOG_FOLD_END(logger, ""); algorithm_formatter.end(); return output; } @@ -1037,8 +1039,8 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension rectangle2boxstacks, rectangle_parameters); if (output.solution_pool.best().full()) { - FFOT_LOG_FOLD_END(parameters.logger, ""); - FFOT_LOG_FOLD_END(parameters.logger, ""); + FFOT_LOG_FOLD_END(logger, ""); + FFOT_LOG_FOLD_END(logger, ""); algorithm_formatter.end(); return output; } @@ -1062,8 +1064,8 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension rectangle2boxstacks, rectangle_parameters); if (output.solution_pool.best().full()) { - FFOT_LOG_FOLD_END(parameters.logger, ""); - FFOT_LOG_FOLD_END(parameters.logger, ""); + FFOT_LOG_FOLD_END(logger, ""); + FFOT_LOG_FOLD_END(logger, ""); algorithm_formatter.end(); return output; } @@ -1101,8 +1103,8 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension failed_middle_axle_weight_constraint_cur |= (subproblem_output.solution.compute_middle_axle_weight_constraints_violation() > 0); failed_rear_axle_weight_constraint_cur |= (subproblem_output.solution.compute_rear_axle_weight_constraints_violation() > 0); if (output.solution_pool.best().full()) { - FFOT_LOG_FOLD_END(parameters.logger, ""); - FFOT_LOG_FOLD_END(parameters.logger, ""); + FFOT_LOG_FOLD_END(logger, ""); + FFOT_LOG_FOLD_END(logger, ""); algorithm_formatter.end(); return output; } @@ -1126,8 +1128,8 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension rectangle2boxstacks, rectangle_parameters); if (output.solution_pool.best().full()) { - FFOT_LOG_FOLD_END(parameters.logger, ""); - FFOT_LOG_FOLD_END(parameters.logger, ""); + FFOT_LOG_FOLD_END(logger, ""); + FFOT_LOG_FOLD_END(logger, ""); algorithm_formatter.end(); return output; } @@ -1139,8 +1141,8 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension && !failed_rear_axle_weight_constraint_cur && fixed_items_solutions_pos == 0 && output.number_of_stack_splits == 0) { - FFOT_LOG_FOLD_END(parameters.logger, ""); - FFOT_LOG_FOLD_END(parameters.logger, ""); + FFOT_LOG_FOLD_END(logger, ""); + FFOT_LOG_FOLD_END(logger, ""); algorithm_formatter.end(); return output; } @@ -1164,8 +1166,8 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension rectangle2boxstacks, rectangle_parameters); if (output.solution_pool.best().full()) { - FFOT_LOG_FOLD_END(parameters.logger, ""); - FFOT_LOG_FOLD_END(parameters.logger, ""); + FFOT_LOG_FOLD_END(logger, ""); + FFOT_LOG_FOLD_END(logger, ""); algorithm_formatter.end(); return output; } @@ -1189,7 +1191,7 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension rectangle2boxstacks, rectangle_parameters); if (output.solution_pool.best().full()) { - FFOT_LOG_FOLD_END(parameters.logger, ""); + FFOT_LOG_FOLD_END(logger, ""); algorithm_formatter.end(); return output; } @@ -1268,7 +1270,7 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension fixed_items = fixed_items_builder.build(); } FFOT_LOG( - parameters.logger, + logger, "failed_middle_axle_weight_constraint " << failed_middle_axle_weight_constraint << std::endl << "failed_rear_axle_weight_constraint " << failed_rear_axle_weight_constraint << std::endl << "x_max " << x_max << " / " << xi << std::endl); @@ -1296,17 +1298,17 @@ const SequentialOneDimensionalRectangleOutput boxstacks::sequential_onedimension //fixed_items.write("fixed_items.csv"); FFOT_LOG( - parameters.logger, + logger, "fixed_items_solutions_pos " << fixed_items_solutions_pos << std::endl << "fixed_items_solutions_pos_lower_bound " << fixed_items_solutions_pos_lower_bound << std::endl << "fixed_items_solutions_pos_upper_bound " << fixed_items_solutions_pos_upper_bound << std::endl); - FFOT_LOG_FOLD_END(parameters.logger, ""); + FFOT_LOG_FOLD_END(logger, ""); if (fixed_items_solutions_pos_lower_bound > fixed_items_solutions_pos_upper_bound) break; } - FFOT_LOG_FOLD_END(parameters.logger, ""); + FFOT_LOG_FOLD_END(logger, ""); algorithm_formatter.end(); return output; } diff --git a/src/boxstacks/solution.cpp b/src/boxstacks/solution.cpp index 97c6b874e..57c4155be 100644 --- a/src/boxstacks/solution.cpp +++ b/src/boxstacks/solution.cpp @@ -374,7 +374,7 @@ void Solution::write( "unable to open file \"" + certificate_path + "\"."); } - file << "TYPE,ID,COPIES,BIN,STACK,X,Y,Z,LX,LY,LZ,GROUP_ID" << std::endl; + file << "TYPE,ID,COPIES,BIN,STACK,X,Y,Z,LX,LY,LZ,GROUP_ID,ROTATION" << std::endl; for (BinPos bin_pos = 0; bin_pos < number_of_different_bins(); ++bin_pos) { @@ -393,6 +393,7 @@ void Solution::write( << instance().bin_type(bin_type_id).box.x << "," << instance().bin_type(bin_type_id).box.y << "," << instance().bin_type(bin_type_id).box.z << "," + << "," << std::endl; for (DefectId defect_id = 0; @@ -412,6 +413,7 @@ void Solution::write( << defect.rect.y << "," << "0," << defect.rect.x << "," + << "," << std::endl; } @@ -429,6 +431,7 @@ void Solution::write( << stack.x_end - stack.x_start << "," << stack.y_end - stack.y_start << "," << stack.z_end << "," + << "," << std::endl; for (const SolutionItem& item: stack.items) { @@ -445,7 +448,8 @@ void Solution::write( << item_type.x(item.rotation) << "," << item_type.y(item.rotation) << "," << item_type.z(item.rotation) << "," - << item_type.group_id << std::endl; + << item_type.group_id << "," + << to_string(item.rotation) << std::endl; } } diff --git a/src/boxstacks/solution_builder.cpp b/src/boxstacks/solution_builder.cpp index 40e2d16bf..42761338f 100644 --- a/src/boxstacks/solution_builder.cpp +++ b/src/boxstacks/solution_builder.cpp @@ -1,8 +1,76 @@ #include "boxstacks/solution_builder.hpp" +#include "optimizationtools/utils/utils.hpp" + +#include + using namespace packingsolver; using namespace packingsolver::boxstacks; +void SolutionBuilder::read( + const std::string& certificate_path) +{ + std::ifstream file(certificate_path); + if (!file.good()) { + throw std::runtime_error( + FUNC_SIGNATURE + ": " + "unable to open file \"" + certificate_path + "\"."); + } + + std::string tmp; + std::vector line; + std::vector labels; + + getline(file, tmp); + labels = optimizationtools::split(tmp, ','); + while (getline(file, tmp)) { + line = optimizationtools::split(tmp, ','); + + std::string type = ""; + ItemTypeId id = -1; + BinPos copies = -1; + BinPos bin_pos = -1; + StackId stack_id = -1; + Length x = -1; + Length y = -1; + Length lx = -1; + Length ly = -1; + Rotation rotation = Rotation::XYZ; + + for (Counter i = 0; i < (Counter)line.size(); ++i) { + if (labels[i] == "TYPE") { + type = line[i]; + } else if (labels[i] == "ID") { + id = (ItemTypeId)std::stol(line[i]); + } else if (labels[i] == "COPIES") { + copies = (BinPos)std::stol(line[i]); + } else if (labels[i] == "BIN") { + bin_pos = (BinPos)std::stol(line[i]); + } else if (labels[i] == "STACK") { + stack_id = (StackId)std::stol(line[i]); + } else if (labels[i] == "X") { + x = (Length)std::stol(line[i]); + } else if (labels[i] == "Y") { + y = (Length)std::stol(line[i]); + } else if (labels[i] == "LX") { + lx = (Length)std::stol(line[i]); + } else if (labels[i] == "LY") { + ly = (Length)std::stol(line[i]); + } else if (labels[i] == "ROTATION" && !line[i].empty()) { + rotation = rotation_from_string(line[i]); + } + } + + if (type == "BIN") { + add_bin(id, copies); + } else if (type == "STACK") { + add_stack(bin_pos, x, x + lx, y, y + ly); + } else if (type == "ITEM") { + add_item(bin_pos, stack_id, id, rotation); + } + } +} + BinPos SolutionBuilder::add_bin( BinTypeId bin_type_id, BinPos copies) diff --git a/src/boxstacks/solution_builder.hpp b/src/boxstacks/solution_builder.hpp index 0bee0c42c..7af8c2988 100644 --- a/src/boxstacks/solution_builder.hpp +++ b/src/boxstacks/solution_builder.hpp @@ -15,6 +15,9 @@ class SolutionBuilder /** Constructor. */ SolutionBuilder(const Instance& instance): solution_(instance) { } + /** Read a solution from a file. */ + void read(const std::string& certificate_path); + /** Add a bin at the end of the solution. */ BinPos add_bin( BinTypeId bin_type_id, diff --git a/test/boxstacks/CMakeLists.txt b/test/boxstacks/CMakeLists.txt index a377a9499..7c916e699 100644 --- a/test/boxstacks/CMakeLists.txt +++ b/test/boxstacks/CMakeLists.txt @@ -1,7 +1,7 @@ add_executable(PackingSolver_boxstacks_test) target_sources(PackingSolver_boxstacks_test PRIVATE boxstacks_test.cpp - axle_weight_test.cpp) + sequential_onedimensional_rectangle_test.cpp) target_include_directories(PackingSolver_boxstacks_test PRIVATE ${PROJECT_SOURCE_DIR}/src) target_link_libraries(PackingSolver_boxstacks_test diff --git a/test/boxstacks/axle_weight_test.cpp b/test/boxstacks/axle_weight_test.cpp deleted file mode 100644 index dd5ef5808..000000000 --- a/test/boxstacks/axle_weight_test.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include "packingsolver/boxstacks/instance_builder.hpp" -#include "packingsolver/boxstacks/optimize.hpp" - -#include -#include - -using namespace packingsolver::boxstacks; -namespace fs = boost::filesystem; - -struct BoxStacksAxleWeightTestParams -{ - fs::path items_path; - fs::path bins_path; - fs::path parameters_path; - - /** Minimum number of items the returned solution must pack. */ - packingsolver::ItemPos minimum_number_of_items; -}; - -inline std::ostream& operator<<(std::ostream& os, const BoxStacksAxleWeightTestParams& test_params) -{ - os << test_params.items_path; - return os; -} - -class BoxStacksAxleWeightTest: public testing::TestWithParam { }; - -TEST_P(BoxStacksAxleWeightTest, BoxStacksAxleWeight) -{ - BoxStacksAxleWeightTestParams test_params = GetParam(); - InstanceBuilder instance_builder; - instance_builder.read_item_types(test_params.items_path.string()); - instance_builder.read_bin_types(test_params.bins_path.string()); - instance_builder.read_parameters(test_params.parameters_path.string()); - Instance instance = instance_builder.build(); - - OptimizeParameters optimize_parameters; - optimize_parameters.optimization_mode = packingsolver::OptimizationMode::NotAnytimeSequential; - Output output = optimize(instance, optimize_parameters); - - // These instances enter the axle weight repair loop of - // 'sequential_onedimensional_rectangle', which used to read - // 'fixed_items_solutions' one element past the end and throw. - // The bound is a minimum rather than an exact count, so that the test - // does not have to be updated when the repair packs more items. - EXPECT_GE( - output.solution_pool.best().number_of_items(), - test_params.minimum_number_of_items); -} - -INSTANTIATE_TEST_SUITE_P( - BoxStacksAxleWeight, - BoxStacksAxleWeightTest, - testing::ValuesIn(std::vector{ - { - // The repair returns an empty solution for this instance; the - // point of the case is that 'optimize' returns at all. - fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_bin_packing" / "items.csv", - fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_bin_packing" / "bins.csv", - fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_bin_packing" / "parameters.csv", - 0, - }, { - fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_knapsack" / "items.csv", - fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_knapsack" / "bins.csv", - fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_knapsack" / "parameters.csv", - 1, - }})); diff --git a/test/boxstacks/sequential_onedimensional_rectangle_test.cpp b/test/boxstacks/sequential_onedimensional_rectangle_test.cpp new file mode 100644 index 000000000..674848f0b --- /dev/null +++ b/test/boxstacks/sequential_onedimensional_rectangle_test.cpp @@ -0,0 +1,73 @@ +#include "packingsolver/boxstacks/instance_builder.hpp" +#include "boxstacks/sequential_onedimensional_rectangle.hpp" +#include "boxstacks/solution_builder.hpp" + +#include +#include + +using namespace packingsolver::boxstacks; +namespace fs = boost::filesystem; + +struct BoxStacksSequentialOneDimensionalRectangleTestParams +{ + fs::path items_path; + fs::path bins_path; + fs::path parameters_path; + fs::path certificate_path; +}; + +inline std::ostream& operator<<(std::ostream& os, const BoxStacksSequentialOneDimensionalRectangleTestParams& test_params) +{ + os << test_params.items_path; + return os; +} + +class BoxStacksSequentialOneDimensionalRectangleTest: public testing::TestWithParam { }; + +TEST_P(BoxStacksSequentialOneDimensionalRectangleTest, BoxStacksSequentialOneDimensionalRectangle) +{ + BoxStacksSequentialOneDimensionalRectangleTestParams test_params = GetParam(); + InstanceBuilder instance_builder; + instance_builder.read_item_types(test_params.items_path.string()); + instance_builder.read_bin_types(test_params.bins_path.string()); + instance_builder.read_parameters(test_params.parameters_path.string()); + Instance instance = instance_builder.build(); + + // These instances enter the axle weight repair loop, which used to read + // 'fixed_items_solutions' one element past the end and throw + // 'std::bad_array_new_length'/'std::bad_alloc'. + SequentialOneDimensionalRectangleParameters sodr_parameters; + SequentialOneDimensionalRectangleOutput output = sequential_onedimensional_rectangle(instance, sodr_parameters); + + SolutionBuilder solution_builder(instance); + solution_builder.read(test_params.certificate_path.string()); + Solution solution = solution_builder.build(); + std::cout << std::endl + << "Reference solution" << std::endl + << "------------------" << std::endl; + solution.format(std::cout); + + EXPECT_EQ(!(output.solution_pool.best() < solution), true); + EXPECT_EQ(!(solution < output.solution_pool.best()), true); +} + +INSTANTIATE_TEST_SUITE_P( + BoxStacksSequentialOneDimensionalRectangle, + BoxStacksSequentialOneDimensionalRectangleTest, + testing::ValuesIn(std::vector{ + { + // The repair returns an empty solution for both instances: + // called directly (unlike through 'optimize()'), the + // algorithm has no later tree search pass to refine the + // result, so this only pins down that it returns at all + // instead of throwing. + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_bin_packing" / "items.csv", + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_bin_packing" / "bins.csv", + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_bin_packing" / "parameters.csv", + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_bin_packing" / "solution.csv", + }, { + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_knapsack" / "items.csv", + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_knapsack" / "bins.csv", + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_knapsack" / "parameters.csv", + fs::path("data") / "boxstacks" / "tests" / "semi_trailer_truck_middle_axle_knapsack" / "solution.csv", + }}));