From 10fa4bae8895ca1a3c3379920e7529004c76f5de Mon Sep 17 00:00:00 2001 From: HansBug Date: Mon, 31 Aug 2026 13:49:10 +0800 Subject: [PATCH 1/2] Don't compute axle weights from a degenerate truck geometry compute_axle_weights divides by harness_rear_axle_distance and by front_axle_middle_axle_distance. A bin file that gives axle weight limits but no truck geometry leaves both at 0, and boxstacks' read_bin_types flags every bin read from a CSV file as a semi-trailer truck, so the divisions produced an infinite rear axle weight and a NaN middle axle weight. The infinite weight exceeds any finite REAR_AXLE_MAXIMUM_WEIGHT, so Solution::feasible_axle_weights rejected every candidate and the solver returned an empty solution with exit code 0. Skip the axle weights that the geometry cannot produce instead: without harness_rear_axle_distance neither weight can be computed, and without front_axle_middle_axle_distance only the middle axle weight is lost while the rear axle stays constrained. A bin type that provides both distances is unaffected, and a bin type that provides only one of them keeps the constraint that is still computable. Added unit tests for the complete geometry and for both degenerate cases. --- include/packingsolver/algorithms/truck.hpp | 22 +++++--- test/algorithms/CMakeLists.txt | 3 +- test/algorithms/truck_test.cpp | 60 ++++++++++++++++++++++ 3 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 test/algorithms/truck_test.cpp diff --git a/include/packingsolver/algorithms/truck.hpp b/include/packingsolver/algorithms/truck.hpp index 36b73736e..0999327b1 100644 --- a/include/packingsolver/algorithms/truck.hpp +++ b/include/packingsolver/algorithms/truck.hpp @@ -54,6 +54,11 @@ struct SemiTrailerTruckData { if (!is) return {0, 0}; + // Both axle weights are computed from 'harness_weight', which divides + // by 'harness_rear_axle_distance', so without that distance neither + // axle weight can be computed and neither axle is constrained. + if (harness_rear_axle_distance <= 0) + return {0, 0}; double stacks_gravity_center_trailer_start_distance // eje = weight_weighted_sum / weight; // tmt @@ -78,12 +83,17 @@ struct SemiTrailerTruckData = weight // tmt + empty_trailer_weight // EM - harness_weight; // emh - double middle_axle_weight // emm - = (tractor_weight // CM - * front_axle_tractor_gravity_center_distance // CJfc - + harness_weight // emh - * front_axle_harness_distance) // CJfh - / front_axle_middle_axle_distance; // CJfm + // Likewise, without 'front_axle_middle_axle_distance' the middle axle + // weight cannot be computed, so only the rear axle is constrained. + double middle_axle_weight = 0; // emm + if (front_axle_middle_axle_distance > 0) { + middle_axle_weight + = (tractor_weight // CM + * front_axle_tractor_gravity_center_distance // CJfc + + harness_weight // emh + * front_axle_harness_distance) // CJfh + / front_axle_middle_axle_distance; // CJfm + } return {middle_axle_weight, rear_axle_weight}; } diff --git a/test/algorithms/CMakeLists.txt b/test/algorithms/CMakeLists.txt index d178b8e1f..4daecd8bc 100644 --- a/test/algorithms/CMakeLists.txt +++ b/test/algorithms/CMakeLists.txt @@ -1,6 +1,7 @@ add_executable(PackingSolver_algorithms_test) target_sources(PackingSolver_algorithms_test PRIVATE - meet_in_the_middle_test.cpp) + meet_in_the_middle_test.cpp + truck_test.cpp) target_include_directories(PackingSolver_algorithms_test PRIVATE ${PROJECT_SOURCE_DIR}/src) target_link_libraries(PackingSolver_algorithms_test diff --git a/test/algorithms/truck_test.cpp b/test/algorithms/truck_test.cpp new file mode 100644 index 000000000..eb542ae3c --- /dev/null +++ b/test/algorithms/truck_test.cpp @@ -0,0 +1,60 @@ +#include "packingsolver/algorithms/truck.hpp" + +#include + +using namespace packingsolver; + +namespace +{ + +/** Semi-trailer truck with a complete geometry. */ +SemiTrailerTruckData complete_truck() +{ + SemiTrailerTruckData semi_trailer_truck_data; + semi_trailer_truck_data.is = true; + semi_trailer_truck_data.tractor_weight = 8000; + semi_trailer_truck_data.front_axle_middle_axle_distance = 380; + semi_trailer_truck_data.front_axle_tractor_gravity_center_distance = 100; + semi_trailer_truck_data.front_axle_harness_distance = 320; + semi_trailer_truck_data.empty_trailer_weight = 6000; + semi_trailer_truck_data.harness_rear_axle_distance = 800; + semi_trailer_truck_data.trailer_gravity_center_rear_axle_distance = 400; + semi_trailer_truck_data.trailer_start_harness_distance = 100; + return semi_trailer_truck_data; +} + +} + +TEST(Truck, ComputeAxleWeights) +{ + SemiTrailerTruckData semi_trailer_truck_data = complete_truck(); + std::pair axle_weights + = semi_trailer_truck_data.compute_axle_weights(200000, 2000); + EXPECT_NEAR(axle_weights.first, 6315.789473684211, 1e-6); + EXPECT_NEAR(axle_weights.second, 3000.0, 1e-6); +} + +TEST(Truck, ComputeAxleWeightsWithoutHarnessRearAxleDistance) +{ + // Every axle weight is computed from 'harness_weight', which divides by + // 'harness_rear_axle_distance'. + SemiTrailerTruckData semi_trailer_truck_data = complete_truck(); + semi_trailer_truck_data.harness_rear_axle_distance = 0; + std::pair axle_weights + = semi_trailer_truck_data.compute_axle_weights(200000, 2000); + EXPECT_EQ(axle_weights.first, 0); + EXPECT_EQ(axle_weights.second, 0); +} + +TEST(Truck, ComputeAxleWeightsWithoutFrontAxleMiddleAxleDistance) +{ + // Only the middle axle weight divides by + // 'front_axle_middle_axle_distance', so the rear axle weight is still + // computed. + SemiTrailerTruckData semi_trailer_truck_data = complete_truck(); + semi_trailer_truck_data.front_axle_middle_axle_distance = 0; + std::pair axle_weights + = semi_trailer_truck_data.compute_axle_weights(200000, 2000); + EXPECT_EQ(axle_weights.first, 0); + EXPECT_NEAR(axle_weights.second, 3000.0, 1e-6); +} From bdf95c86ba62e07e1852da6ffe66aca8ada1fefc Mon Sep 17 00:00:00 2001 From: Florian Fontan Date: Sun, 6 Sep 2026 19:59:09 +0200 Subject: [PATCH 2/2] truck: validate semi-trailer truck data instead of guarding against zero distances boxstacks::InstanceBuilder::read_bin_types forced 'is' to true for every bin read from a CSV file, so an ordinary bin file with no truck geometry left harness_rear_axle_distance and front_axle_middle_axle_distance at 0, and compute_axle_weights divided by them. Stop forcing 'is' to true and instead only set it when the CSV explicitly provides IS_SEMI_TRAILER_TRUCK. Add SemiTrailerTruckData::check(), called from both rectangle's and boxstacks' InstanceBuilder::build(), to reject inconsistent truck data up front: a semi-trailer truck without harness_rear_axle_distance or front_axle_middle_axle_distance. A bin type that isn't a semi-trailer truck needs no truck data at all, since compute_axle_weights returns {0, 0} without looking at any other field. This turns the degenerate cases compute_axle_weights previously guarded against into invalid_argument at build time, so read_bin_types no longer needs to force 'is' and compute_axle_weights no longer needs to special-case either distance. --- include/packingsolver/algorithms/truck.hpp | 50 +++++++++++++++------- src/boxstacks/instance_builder.cpp | 3 +- src/rectangle/instance_builder.cpp | 2 + test/algorithms/truck_test.cpp | 32 ++++++++------ 4 files changed, 56 insertions(+), 31 deletions(-) diff --git a/include/packingsolver/algorithms/truck.hpp b/include/packingsolver/algorithms/truck.hpp index 0999327b1..3ce843cb6 100644 --- a/include/packingsolver/algorithms/truck.hpp +++ b/include/packingsolver/algorithms/truck.hpp @@ -4,6 +4,7 @@ #include #include +#include namespace packingsolver { @@ -54,11 +55,6 @@ struct SemiTrailerTruckData { if (!is) return {0, 0}; - // Both axle weights are computed from 'harness_weight', which divides - // by 'harness_rear_axle_distance', so without that distance neither - // axle weight can be computed and neither axle is constrained. - if (harness_rear_axle_distance <= 0) - return {0, 0}; double stacks_gravity_center_trailer_start_distance // eje = weight_weighted_sum / weight; // tmt @@ -83,20 +79,42 @@ struct SemiTrailerTruckData = weight // tmt + empty_trailer_weight // EM - harness_weight; // emh - // Likewise, without 'front_axle_middle_axle_distance' the middle axle - // weight cannot be computed, so only the rear axle is constrained. - double middle_axle_weight = 0; // emm - if (front_axle_middle_axle_distance > 0) { - middle_axle_weight - = (tractor_weight // CM - * front_axle_tractor_gravity_center_distance // CJfc - + harness_weight // emh - * front_axle_harness_distance) // CJfh - / front_axle_middle_axle_distance; // CJfm - } + double middle_axle_weight // emm + = (tractor_weight // CM + * front_axle_tractor_gravity_center_distance // CJfc + + harness_weight // emh + * front_axle_harness_distance) // CJfh + / front_axle_middle_axle_distance; // CJfm return {middle_axle_weight, rear_axle_weight}; } + /** + * Check that the provided truck data is consistent. + * + * A bin type that is not a semi-trailer truck needs no truck data: + * compute_axle_weights returns {0, 0} without looking at any other + * field, so the other fields can hold any value. A semi-trailer truck + * needs 'harness_rear_axle_distance' and 'front_axle_middle_axle_distance' + * to compute the rear and middle axle weights. + */ + void check() const + { + if (!is) + return; + if (harness_rear_axle_distance <= 0) { + throw std::invalid_argument( + FUNC_SIGNATURE + ": " + "a semi-trailer truck bin type must have a strictly " + "positive 'harness_rear_axle_distance'."); + } + if (front_axle_middle_axle_distance <= 0) { + throw std::invalid_argument( + FUNC_SIGNATURE + ": " + "a semi-trailer truck bin type must have a strictly " + "positive 'front_axle_middle_axle_distance'."); + } + } + void read(std::string label, std::string value) { if (label == "IS_SEMI_TRAILER_TRUCK") { diff --git a/src/boxstacks/instance_builder.cpp b/src/boxstacks/instance_builder.cpp index 70fa6d2bb..8e26f44d7 100644 --- a/src/boxstacks/instance_builder.cpp +++ b/src/boxstacks/instance_builder.cpp @@ -684,7 +684,6 @@ void InstanceBuilder::read_bin_types( double maximum_stack_density = std::numeric_limits::max(); SemiTrailerTruckData semi_trailer_truck_data; - semi_trailer_truck_data.is = true; for (Counter i = 0; i < (Counter)line.size(); ++i) { if (labels[i] == "X") { @@ -1001,6 +1000,8 @@ Instance InstanceBuilder::build() bin_type_id < instance_.number_of_bin_types(); ++bin_type_id) { const BinType& bin_type = instance_.bin_type(bin_type_id); + // Check truck data consistency. + bin_type.semi_trailer_truck_data.check(); // Update bin_type.copies. if (bin_type.copies == -1) instance_.bin_types_[bin_type_id].copies = instance_.number_of_items(); diff --git a/src/rectangle/instance_builder.cpp b/src/rectangle/instance_builder.cpp index 4696c884e..33a75c2b4 100644 --- a/src/rectangle/instance_builder.cpp +++ b/src/rectangle/instance_builder.cpp @@ -1131,6 +1131,8 @@ Instance InstanceBuilder::build() bin_type_id < instance_.number_of_bin_types(); ++bin_type_id) { const BinType& bin_type = instance_.bin_type(bin_type_id); + // Check truck data consistency. + bin_type.semi_trailer_truck_data.check(); // Update bin_type.copies. if (bin_type.copies == -1) instance_.bin_types_[bin_type_id].copies = instance_.number_of_items(); diff --git a/test/algorithms/truck_test.cpp b/test/algorithms/truck_test.cpp index eb542ae3c..0d9495690 100644 --- a/test/algorithms/truck_test.cpp +++ b/test/algorithms/truck_test.cpp @@ -2,6 +2,8 @@ #include +#include + using namespace packingsolver; namespace @@ -34,27 +36,29 @@ TEST(Truck, ComputeAxleWeights) EXPECT_NEAR(axle_weights.second, 3000.0, 1e-6); } -TEST(Truck, ComputeAxleWeightsWithoutHarnessRearAxleDistance) +TEST(Truck, CheckNotATruck) +{ + // A bin type that isn't a semi-trailer truck needs no truck data. + SemiTrailerTruckData semi_trailer_truck_data; + EXPECT_NO_THROW(semi_trailer_truck_data.check()); +} + +TEST(Truck, CheckWithoutHarnessRearAxleDistance) { // Every axle weight is computed from 'harness_weight', which divides by - // 'harness_rear_axle_distance'. + // 'harness_rear_axle_distance', so a semi-trailer truck without it is an + // inconsistent input, not a case to silently skip. SemiTrailerTruckData semi_trailer_truck_data = complete_truck(); semi_trailer_truck_data.harness_rear_axle_distance = 0; - std::pair axle_weights - = semi_trailer_truck_data.compute_axle_weights(200000, 2000); - EXPECT_EQ(axle_weights.first, 0); - EXPECT_EQ(axle_weights.second, 0); + EXPECT_THROW(semi_trailer_truck_data.check(), std::invalid_argument); } -TEST(Truck, ComputeAxleWeightsWithoutFrontAxleMiddleAxleDistance) +TEST(Truck, CheckWithoutFrontAxleMiddleAxleDistance) { - // Only the middle axle weight divides by - // 'front_axle_middle_axle_distance', so the rear axle weight is still - // computed. + // The middle axle weight is computed from 'harness_weight', which + // divides by 'front_axle_middle_axle_distance', so a semi-trailer truck + // without it is an inconsistent input, not a case to silently skip. SemiTrailerTruckData semi_trailer_truck_data = complete_truck(); semi_trailer_truck_data.front_axle_middle_axle_distance = 0; - std::pair axle_weights - = semi_trailer_truck_data.compute_axle_weights(200000, 2000); - EXPECT_EQ(axle_weights.first, 0); - EXPECT_NEAR(axle_weights.second, 3000.0, 1e-6); + EXPECT_THROW(semi_trailer_truck_data.check(), std::invalid_argument); }