From d0e01bed55d97bce58a4b907de0b2eaa0b9045f6 Mon Sep 17 00:00:00 2001 From: Tinko Sebastian Bartels Date: Mon, 6 Jan 2025 23:24:51 +0800 Subject: [PATCH] Test for overlay operations on random grids with integer coordinates. --- .../overlay/areal_areal/CMakeLists.txt | 1 + test/robustness/overlay/areal_areal/Jamfile | 2 +- .../areal_areal/random_integer_grids.cpp | 274 ++++++++++++++++++ 3 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 test/robustness/overlay/areal_areal/random_integer_grids.cpp diff --git a/test/robustness/overlay/areal_areal/CMakeLists.txt b/test/robustness/overlay/areal_areal/CMakeLists.txt index f01e7c688..9f0a1b99f 100644 --- a/test/robustness/overlay/areal_areal/CMakeLists.txt +++ b/test/robustness/overlay/areal_areal/CMakeLists.txt @@ -15,6 +15,7 @@ foreach(item IN ITEMS recursive_polygons star_comb #ticket_9081 + #random_integer_grid ) boost_geometry_add_unit_test("robustness" ${item}) target_include_directories(${BOOST_GEOMETRY_UNIT_TEST_NAME} diff --git a/test/robustness/overlay/areal_areal/Jamfile b/test/robustness/overlay/areal_areal/Jamfile index 96e42656d..8e950e97d 100644 --- a/test/robustness/overlay/areal_areal/Jamfile +++ b/test/robustness/overlay/areal_areal/Jamfile @@ -25,4 +25,4 @@ exe recursive_polygons : recursive_polygons.cpp ; exe star_comb : star_comb.cpp ; exe ticket_9081 : ticket_9081.cpp ; - +exe random_integer_grids : random_integer_grids.cpp ; diff --git a/test/robustness/overlay/areal_areal/random_integer_grids.cpp b/test/robustness/overlay/areal_areal/random_integer_grids.cpp new file mode 100644 index 000000000..791f1bdb2 --- /dev/null +++ b/test/robustness/overlay/areal_areal/random_integer_grids.cpp @@ -0,0 +1,274 @@ +// Boost.Geometry (aka GGL, Generic Geometry Library) +// Robustness Test + +// Copyright (c) 2025 Tinko Bartels, Shenzhen, China. + +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +#define BOOST_GEOMETRY_NO_BOOST_TEST + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#define BOOST_GEOMETRY_TEST_ONLY_ONE_TYPE +#define BOOST_GEOMETRY_DEFAULT_TEST_TYPE int +#include + +constexpr int chunk_size = 64; +using bits = std::vector>; +using generator_t = std::mt19937_64; +static_assert(sizeof(generator_t::result_type) >= chunk_size / 8, "Generator output too narrow."); + +namespace bg = boost::geometry; + +using point = bg::model::d2::point_xy; +using box = bg::model::box; +using mp_t = bg::model::multi_polygon>; + +struct grid_settings +{ + int width = 5; + int height = 5; + int count = 1; + generator_t::result_type seed = generator_t::default_seed; +}; + +constexpr int cell_dimension = 2; + +std::vector grid_cells(grid_settings const& settings) +{ + std::vector out; + for (int y = 0; y < settings.height; ++y) + { + for (int x = 0; x < settings.width; ++x) + { + out.push_back(box{point{x * cell_dimension, y * cell_dimension}, + point{(x + 1) * cell_dimension, (y + 1) * cell_dimension}}); + } + } + return out; +} + +std::vector test_points(grid_settings const& settings) +{ + std::vector out; + for (int y = 0; y < settings.height; ++y) + { + for (int x = 0; x < settings.width; ++x) + { + out.push_back(point{x * cell_dimension + cell_dimension / 2, + y * cell_dimension + cell_dimension / 2}); + } + } + return out; +} + +std::ostream& operator<<(std::ostream& os, std::pair const& b_gs) +{ + os << '\n'; + for (int y = b_gs.second.height - 1; y >= 0; --y) + { + for (int x = 0; x < b_gs.second.width; ++x) + { + int index = y * b_gs.second.width + x; + os << b_gs.first[index / chunk_size][index % chunk_size]; + } + os << '\n'; + } + return os; +} + +bits geometry_to_bits(mp_t const& geometry, std::vector const& test_points) +{ + bits out((test_points.size() + chunk_size - 1) / chunk_size); + for (size_t i = 0; i < test_points.size(); ++i) + { + out[i / chunk_size][i % chunk_size] = bg::within(test_points[i], geometry); + } + return out; +} + +mp_t bits_to_geometry(bits const& b, std::vector const& grid, std::vector const& points, + grid_settings const& settings, bool& all_success) +{ + mp_t out; + for (size_t i = 0; i < grid.size(); ++i) + { + if( b[i / chunk_size][i % chunk_size] ) + { + mp_t temp; + bg::union_(out, grid[i], temp); + out = std::move(temp); + } + } + // Convenience lambda to pair bits with settings to use width/height in operator<<(os, ...) + const auto b_gs = [&settings](bits const& b) { return std::make_pair(b, settings); }; + std::string reason{}; + if (! bg::is_valid(out, reason)) + { + std::cout << bg::wkt(out) << "\ngenerated from" << b_gs(b) + << "is invalid: " << reason << ".\n\n"; + all_success = false; + } + if (geometry_to_bits(out, points) != b) + { + std::cout << "Generating grid from pattern" << b_gs(b) + << "results in mismatching geometry: " << bg::wkt(out) << ".\n\n"; + all_success = false; + } + return out; +} + +bits gen_bits(generator_t& generator, int bits_size) +{ + bits b((bits_size + chunk_size - 1) / chunk_size); + std::generate(b.begin(), b.end(), std::ref(generator)); + if (bits_size % chunk_size != 0) + { + std::bitset bm; + bm.set(); + bm >>= chunk_size - bits_size % chunk_size; + b.back() &= bm; + } + return b; +} + +template +bits apply_for_each(bits a, bits const& b, BitOp const& bit_op) +{ + for (size_t i = 0; i < a.size(); ++i) a[i] = bit_op(a[i], b[i]); + return a; +} + +template +void test_op(bits const& bits1, bits const& bits2, mp_t const& geo1, mp_t const& geo2, + std::string const& op_label, BitOp const& bit_op, GeoOp const& geo_op, + std::vector const& test_points, std::vector const& grid, + grid_settings const& settings, bool& success) +{ + auto test_geo = geo_op(geo1, geo2); + // Convenience lambda to pair bits with settings to use width/height in operator<<(os, ...) + const auto b_gs = [&settings](bits const& b) { return std::make_pair(b, settings); }; + std::string reason{}; + if (! bg::is_valid(test_geo, reason)) + { + std::cout << op_label << "(\n\t " << bg::wkt(geo1) << ",\n\t " << bg::wkt(geo2) << "\n),\n" + << "generated from" << b_gs(bits1) << "and" << b_gs(bits2) << "is invalid: " + << reason << ".\n\n"; + success = false; + } + const bits expected = apply_for_each(bits1, bits2, bit_op); + const bits obtained = geometry_to_bits(test_geo, test_points); + if (obtained != expected) + { + std::cout << op_label << "(\n\t" << bg::wkt(geo1) << ",\n\t" << bg::wkt(geo2) << "\n),\n" + << "generated from" << b_gs(bits1) << "and" << b_gs(bits2) + << "is incorrect.\nExpected: " + << bg::wkt(bits_to_geometry(expected, grid, test_points, settings, success)) + << "\ncorresponding to" << b_gs(expected) << "Obtained: " + << bg::wkt(test_geo) << "\ncorresponding to" << b_gs(obtained) << "\n"; + success = false; + } +} + +bool test_all(grid_settings const& settings) +{ + generator_t genenerator(settings.seed); + const auto grid = grid_cells(settings); + const auto points = test_points(settings); + bool all_success = true; + for (int i = 0; i < settings.count || settings.count == -1; i++) + { + const bits bits1 = gen_bits(genenerator, settings.width * settings.height); + const bits bits2 = gen_bits(genenerator, settings.width * settings.height); + const auto geo1 = bits_to_geometry(bits1, grid, points, settings, all_success); + const auto geo2 = bits_to_geometry(bits2, grid, points, settings, all_success); + test_op(bits1, bits2, geo1, geo2, "union", std::bit_or<>{}, + [](mp_t const& g1, mp_t const& g2) { mp_t g; bg::union_(g1, g2, g); return g; }, + points, grid, settings, all_success); + test_op(bits1, bits2, geo1, geo2, "intersection", std::bit_and<>{}, + [](mp_t const& g1, mp_t const& g2) { mp_t g; bg::intersection(g1, g2, g); return g; }, + points, grid, settings, all_success); + test_op(bits1, bits2, geo1, geo2, "sym_difference", std::bit_xor<>{}, + [](mp_t const& g1, mp_t const& g2) { mp_t g; bg::sym_difference(g1, g2, g); return g; }, + points, grid, settings, all_success); + test_op(bits1, bits2, geo1, geo2, "difference g1 \\ g2", + [](std::bitset b1, std::bitset b2) { return b1 & (~b2); }, + [](mp_t const& g1, mp_t const& g2) { mp_t g; bg::difference(g1, g2, g); return g; }, + points, grid, settings, all_success); + test_op(bits1, bits2, geo1, geo2, "difference g2 \\ g1", + [](std::bitset b1, std::bitset b2) { return b2 & (~b1); }, + [](mp_t const& g1, mp_t const& g2) { mp_t g; bg::difference(g2, g1, g); return g; }, + points, grid, settings, all_success); + } + return all_success; +} + +int main(int argc, char** argv) +{ + BoostGeometryWriteTestConfiguration(); + try + { + namespace po = boost::program_options; + po::options_description description("=== random_integer_grids ===\nAllowed options"); + + grid_settings settings; + + description.add_options() + ("help", "Help message") + ("seed", + po::value(&settings.seed)->default_value(settings.seed), + "Initialization seed for random generator") + ("count", + po::value(&settings.count)->default_value(settings.count), + "Number of tests (-1 for infinite loop)") + ("width", + po::value(&settings.width)->default_value(settings.width), + "Width of grid (>= 1)") + ("height", + po::value(&settings.height)->default_value(settings.height), + "Height of grid (>= 1)") + ; + + po::variables_map varmap; + po::store(po::parse_command_line(argc, argv, description), varmap); + po::notify(varmap); + + if(settings.height < 1 || settings.width < 1) + { + std::cout << "Invalid dimensions, height and width need to be positive.\n"; + return 1; + } + if (varmap.count("help")) + { + std::cout << description << std::endl; + return 1; + } + if (! test_all(settings)) return 1; + } + catch(std::exception const& e) + { + std::cout << "Exception " << e.what() << '\n'; + } + catch(...) + { + std::cout << "Other exception" << '\n'; + } + + return 0; +}