diff --git a/example/Jamfile.v2 b/example/Jamfile.v2 index ed34f78d7..8d9093b2f 100644 --- a/example/Jamfile.v2 +++ b/example/Jamfile.v2 @@ -71,6 +71,7 @@ run edge_property.cpp ; run edge-function.cpp : $(TEST_DIR)/makefile-dependencies.dat $(TEST_DIR)/makefile-target-names.dat ; run edge-iter-constructor.cpp : $(TEST_DIR)/makefile-dependencies.dat ; exe edmonds-karp-eg : edmonds-karp-eg.cpp ; +run geometric_graph_generator_example.cpp ; run exterior_properties.cpp ; run exterior_property_map.cpp ; run family_tree.cpp ; diff --git a/example/geometric_graph_generator_example.cpp b/example/geometric_graph_generator_example.cpp new file mode 100644 index 000000000..4e3978175 --- /dev/null +++ b/example/geometric_graph_generator_example.cpp @@ -0,0 +1,264 @@ +//======================================================================= +// Copyright 2026 +// Author: Matyas W Egyhazy +// +// Distributed under 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) +//======================================================================= + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// Utility function to write graph to a GraphML stream +template < typename Graph > +void write_graph_to_graphml(std::ostream& os, Graph& g, + const std::vector< boost::simple_point< double > >& points) +{ + const std::size_t num_verts = boost::num_vertices(g); + + std::vector< double > x_coords(num_verts); + std::vector< double > y_coords(num_verts); + + auto vertex_idx_map = boost::get(boost::vertex_index, g); + + for (auto v : boost::make_iterator_range(boost::vertices(g))) + { + std::size_t idx = boost::get(vertex_idx_map, v); + x_coords[idx] = points[idx].x; + y_coords[idx] = points[idx].y; + } + + auto x_pmap = boost::make_iterator_property_map< double* >( + x_coords.data(), vertex_idx_map); + auto y_pmap = boost::make_iterator_property_map< double* >( + y_coords.data(), vertex_idx_map); + + boost::dynamic_properties dp; + dp.property("x", x_pmap); + dp.property("y", y_pmap); + dp.property("weight", boost::get(boost::edge_weight, g)); + + boost::write_graphml(os, g, dp, true); +} + +void example_basic_random_graph() +{ + std::cout + << "\n[Example 2: Basic Random Complete Graph (adjacency_matrix)]\n"; + + using Graph = boost::adjacency_matrix< boost::undirectedS, + boost::no_property, boost::property< boost::edge_weight_t, double > >; + using Point = boost::simple_point< double >; + + const std::size_t num_vertices = 25; + Graph g(num_vertices); + + std::vector< Point > points; + points.reserve(num_vertices); + boost::generate_unique_random_points< Point >( + num_vertices, 500, std::back_inserter(points)); + + auto weight_map = boost::get(boost::edge_weight, g); + auto vertex_index_map = boost::get(boost::vertex_index, g); + + boost::connect_all_geometric(g, points, weight_map, vertex_index_map); + + std::cout << " Generated " << boost::num_vertices(g) << " vertices, " + << boost::num_edges(g) << " edges.\n"; +} + +void example_custom_distribution() +{ + std::cout + << "\n[Example 3: Custom Gaussian Distribution (adjacency_list)]\n"; + + using Graph = boost::adjacency_list< boost::vecS, boost::vecS, + boost::undirectedS, boost::no_property, + boost::property< boost::edge_weight_t, double > >; + using Point = boost::simple_point< double >; + + const std::size_t num_vertices = 15; + Graph g(num_vertices); + + std::mt19937 rng(42); + std::normal_distribution< double > normal_dist(50.0, 10.0); + + std::vector< Point > points; + points.reserve(num_vertices); + boost::generate_unique_random_points< Point >(num_vertices, normal_dist, + normal_dist, std::back_inserter(points), rng); + + boost::connect_all_geometric(g, points, boost::get(boost::edge_weight, g), + boost::get(boost::vertex_index, g)); + + std::cout << " Generated " << boost::num_vertices(g) << " vertices, " + << boost::num_edges(g) << " edges.\n"; +} + +void example_mst_on_euclidean_graph(std::ostream* graphml_out) +{ + std::cout << "\n[Example 4: Minimum Spanning Tree Execution]\n"; + + using Graph = boost::adjacency_matrix< boost::undirectedS, + boost::no_property, boost::property< boost::edge_weight_t, double > >; + using Point = boost::simple_point< double >; + using Edge = typename boost::graph_traits< Graph >::edge_descriptor; + + const std::size_t num_vertices = 20; + Graph g(num_vertices); + + std::vector< Point > points; + points.reserve(num_vertices); + boost::generate_unique_random_points< Point >( + num_vertices, 500, std::back_inserter(points)); + + boost::connect_all_geometric(g, points, boost::get(boost::edge_weight, g), + boost::get(boost::vertex_index, g)); + + // Compute Kruskal's MST + std::vector< Edge > mst_edges; + boost::kruskal_minimum_spanning_tree(g, std::back_inserter(mst_edges)); + + Graph mst_graph(num_vertices); + auto weight_map_g = boost::get(boost::edge_weight, g); + auto weight_map_mst = boost::get(boost::edge_weight, mst_graph); + + for (const auto& e : mst_edges) + { + auto src = boost::source(e, g); + auto tgt = boost::target(e, g); + double w = boost::get(weight_map_g, e); + + std::pair< Edge, bool > result = boost::add_edge(src, tgt, mst_graph); + if (result.second) + { + boost::put(weight_map_mst, result.first, w); + } + } + + std::cout << " Extracted MST (" << boost::num_edges(mst_graph) + << " edges).\n"; + + // Export GraphML ONLY if an explicit output stream was passed + if (graphml_out) + { + write_graph_to_graphml(*graphml_out, mst_graph, points); + std::cout << " GraphML payload successfully exported to designated " + "stream.\n"; + } + else + { + std::cout << " GraphML export skipped (no output flag specified).\n"; + } +} + +void example_make_convenient_euclidean_graph() +{ + std::cout << "\n[Example 1: High-Level Geometric Generator Convenience " + "Function]\n"; + + using Graph = boost::adjacency_matrix< boost::undirectedS, + boost::no_property, boost::property< boost::edge_weight_t, double > >; + using Point = boost::simple_point< double >; + + const std::size_t num_vertices = 10; + const std::size_t coord_max = 100; + Graph g(num_vertices); + + boost::make_random_geometric_graph< Point >(g, num_vertices, coord_max, + boost::get(boost::edge_weight, g), boost::get(boost::vertex_index, g)); + + std::cout << " Convenience generator created " << boost::num_vertices(g) + << " vertices, " << boost::num_edges(g) << " edges.\n"; +} + +int main(int argc, char* argv[]) +{ + try + { + bool use_console = false; + std::string output_path; + + for (int i = 1; i < argc; ++i) + { + std::string arg = argv[i]; + if (arg == "-c" || arg == "--console") + { + use_console = true; + } + else if ((arg == "-o" || arg == "--output") && i + 1 < argc) + { + output_path = argv[++i]; + } + else if (arg == "-h" || arg == "--help") + { + std::cout << "Usage: " << argv[0] << " [options]\n" + << " -c, --console Emit GraphML XML output " + "directly to std::cout\n" + << " -o, --output Write GraphML XML output " + "to specified file path\n" + << " -h, --help Show this help message\n"; + return EXIT_SUCCESS; + } + } + + if (use_console && !output_path.empty()) + { + throw std::runtime_error( + "Cannot specify both --console (-c) and --output (-o)."); + } + + std::ostream* graphml_out = nullptr; + std::ofstream file_stream; + + if (use_console) + { + graphml_out = &std::cout; + } + else if (!output_path.empty()) + { + file_stream.exceptions( + std::ofstream::failbit | std::ofstream::badbit); + file_stream.open(output_path, std::ios::out | std::ios::trunc); + graphml_out = &file_stream; + } + + std::cout << "====================================================\n"; + std::cout << " Boost.Graph Geometric Graph Generator Example Suite \n"; + std::cout << "====================================================\n"; + + example_make_convenient_euclidean_graph(); + example_basic_random_graph(); + example_custom_distribution(); + example_mst_on_euclidean_graph(graphml_out); + + std::cout << "\nAll examples executed successfully.\n"; + return EXIT_SUCCESS; + } + catch (const std::exception& e) + { + std::cerr << "\nError during execution: " << e.what() << std::endl; + return EXIT_FAILURE; + } + catch (...) + { + std::cerr << "\nUnknown error occurred during execution." << std::endl; + return EXIT_FAILURE; + } +} \ No newline at end of file diff --git a/include/boost/graph/geometric_graph_generator.hpp b/include/boost/graph/geometric_graph_generator.hpp new file mode 100644 index 000000000..7bbe8116b --- /dev/null +++ b/include/boost/graph/geometric_graph_generator.hpp @@ -0,0 +1,342 @@ +//======================================================================= +// Copyright 2026 +// Author: Matyas W Egyhazy +// +// Distributed under 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) +//======================================================================= + +#ifndef BOOST_GRAPH_GEOMETRIC_GRAPH_GENERATOR_HPP +#define BOOST_GRAPH_GEOMETRIC_GRAPH_GENERATOR_HPP + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + + +namespace boost +{ + + namespace geometric_graph_generator_detail + { + // Detection trait for boost::hash support + template < typename T, typename = void > + struct is_boost_hashable : std::false_type + { + }; + + template < typename T > + struct is_boost_hashable< T, + boost::void_t< decltype(hash_value(std::declval< const T& >())) > > + : std::true_type + { + }; + } + +// connect_all_geometric +// +// Creates a complete graph with geometric distance edge weights. +// Connects all vertices in the graph with edges weighted by the +// distance between their corresponding points in the point container. +// This is a common preprocessing step for TSP algorithms. +// Relies on on a templated distance function (e.g. boost::geometry::distance) +// for flexibility and compatibility with various point types, including +// Boost.Geometry points. +// +// Preconditions: g must have num_vertices(g) == points.size() and no edges +// Postconditions: g will be a complete graph with geometric distance weights +// Complexity: O(V^2) where V is the number of vertices + +template < typename VertexListGraph, typename PointContainer, + typename WeightMap, typename VertexIndexMap , typename BinaryFunction> +void connect_all_geometric(VertexListGraph& g, const PointContainer& points, + WeightMap wmap, VertexIndexMap vmap, BinaryFunction distance) +{ + BOOST_CONCEPT_ASSERT((ReadablePropertyMapConcept< VertexIndexMap, + typename graph_traits< VertexListGraph >::vertex_descriptor >)); + BOOST_CONCEPT_ASSERT((RandomAccessContainerConcept< PointContainer >)); + BOOST_CONCEPT_ASSERT((VertexListGraphConcept< VertexListGraph >)); + BOOST_CONCEPT_ASSERT((MutableGraphConcept< VertexListGraph >)); + BOOST_CONCEPT_ASSERT((WritablePropertyMapConcept< WeightMap, + typename graph_traits< VertexListGraph >::edge_descriptor >)); + BOOST_CONCEPT_ASSERT((ReadablePropertyMapConcept< VertexIndexMap, + typename graph_traits< VertexListGraph >::vertex_descriptor >)); + // Inside connect_all_geometric: + using DirectedCategory = + typename boost::graph_traits< VertexListGraph >::directed_category; + BOOST_STATIC_ASSERT_MSG( + (!std::is_convertible< DirectedCategory, boost::directed_tag >::value), + "connect_all_geometric requires an undirected graph type. " + "Directed graphs are not supported because geometric complete graphs " + "require symmetric edges."); + BOOST_CONCEPT_ASSERT((boost::MutableGraphConcept< VertexListGraph >)); + + // Precondition: Graph should have no edges and size of points should match + // num_vertices(g) + BOOST_ASSERT_MSG(boost::num_edges(g) == 0, + "connect_all_geometric requires a graph with no edges)"); + BOOST_ASSERT_MSG(boost::num_vertices(g) == points.size(), + "connect_all_geometric requires num_vertices(g) == points.size()"); + + using Edge = typename graph_traits< VertexListGraph >::edge_descriptor; + using VItr = typename graph_traits< VertexListGraph >::vertex_iterator; + + // Deduce the weight type from the WeightMap's value type + using WeightType = typename boost::property_traits< WeightMap >::value_type; + + using IndexType = typename boost::property_traits::value_type; + + // Compile-time assertion: Prevent integer weight types + BOOST_STATIC_ASSERT_MSG( + std::is_floating_point::value, + "connect_all_geometric requires floating-point weight types (float, double, or long double). " + "Integer types cause truncation and produce non-useful edge lengths. " + "e.g. Use property instead of property." + ); + + std::pair< VItr, VItr > verts(vertices(g)); + + for (VItr src(verts.first); src != verts.second; ++src) + { + const IndexType src_idx = boost::get(vmap, *src); // Cache source index lookup + + VItr dest(src); + ++dest; // Skip self-edge + + for (; dest != verts.second; ++dest) + { + const IndexType dest_idx + = boost::get(vmap, *dest); // Cache destination index lookup + + const auto weight = static_cast< WeightType >( + distance(points[src_idx], points[dest_idx])); + + // No need to check 'inserted' - building fresh complete graph + Edge e = boost::add_edge(*src, *dest, g).first; + boost::put(wmap, e, weight); + } + } +} + + +template < typename VertexListGraph, typename PointContainer, + typename WeightMap, typename VertexIndexMap> +void connect_all_geometric(VertexListGraph& g, const PointContainer& points, + WeightMap wmap, VertexIndexMap vmap) +{ + auto adl_distance = [](auto const &a, auto const &b){ return distance(a, b); }; + connect_all_geometric(g, points, wmap, vmap, adl_distance); +} + +// generate_unique_random_points +// +// Generates a set of random unique 2D points . +// Uses unordered_set to ensure uniqueness and avoid duplicate points. +// This involves copying points into the output iterator... +// but simplifies uniqueness handling. +// Supports custom random distributions for flexible point generation patterns. +// +// Returns: The number of unique points generated. +// +// Parameters: +// num_points - Number of unique points to generate +// x_dist - Distribution for x-coordinates (e.g., uniform, normal) +// y_dist - Distribution for y-coordinates (can differ from x) +// out - Output iterator for storing generated points +// rng - Random number generator (default: std::mt19937 with random seed) +// +// Postconditions: Exactly num_points unique points written to out +// Complexity: O(N) average case, O(N^2) worst case due to collision handling + +// Generic version: PointType must be constructible from (CoordType, CoordType) +template < typename PointType, typename OutputIterator, typename XDistribution, + typename YDistribution, typename RandomEngine > +std::size_t generate_unique_random_points( + std::size_t num_points, + XDistribution x_dist, + YDistribution y_dist, + OutputIterator out, + RandomEngine& rng, + std::size_t max_attempts = 0) +{ + using CoordType = typename XDistribution::result_type; + BOOST_STATIC_ASSERT_MSG( + (std::is_same< CoordType, typename YDistribution::result_type >::value), + "X and Y distributions must have the same result type"); + BOOST_STATIC_ASSERT_MSG(geometric_graph_generator_detail::is_boost_hashable< PointType >::value, + "PointType must be hashable via boost::hash. " + "Provide a hash_value overload or specialize boost::hash."); + + + // This avoids the rare case in which the while loop runs indefinitely due to collisions + // when num_points is large and the distribution is narrow. + if (max_attempts == 0) + max_attempts = std::max(10 * num_points, 100); + + using PointSet = boost::unordered_flat_set; + PointSet point_set(0); + point_set.reserve(num_points); + + std::size_t attempts = 0; + while (point_set.size() < num_points && attempts < max_attempts) + { + point_set.insert(PointType{ x_dist(rng), y_dist(rng) }); + ++attempts; + } + + std::copy(std::make_move_iterator(point_set.begin()), + std::make_move_iterator(point_set.end()), out); + + return point_set.size(); +} + +// Overload with default RNG and max_attempts parameter +template < typename PointType, typename OutputIterator, typename XDistribution, + typename YDistribution > +std::size_t generate_unique_random_points( + std::size_t num_points, + XDistribution x_dist, + YDistribution y_dist, + OutputIterator out, + std::size_t max_attempts = 0) +{ + std::random_device rd; + std::seed_seq seq { rd(), rd(), rd(), rd(), rd(), rd(), rd(), rd() }; + std::mt19937 rng(seq); + return generate_unique_random_points(num_points, x_dist, y_dist, out, rng, max_attempts); +} + +// Overload for uniform distribution, with max_attempts parameter +template < typename PointType, typename OutputIterator, typename CoordType = double > +std::size_t generate_unique_random_points( + std::size_t num_points, + std::size_t coordinate_max, + OutputIterator out, + std::size_t max_attempts = 0) +{ + std::uniform_real_distribution< CoordType > dist( + static_cast< CoordType >(0), static_cast< CoordType >(coordinate_max)); + return generate_unique_random_points(num_points, dist, dist, out, max_attempts); +} + +// make_random_euclidean_graph +// +// Creates a complete graph with random points and Euclidean distance weights. +// This is a convenience function that combines random point generation with +// complete graph construction for TSP testing and benchmarking. +// +// Parameters: +// g - Graph to populate (must have num_vertices(g) == num_points) +// num_points - Number of vertices/points +// coordinate_max - Maximum coordinate value for random points +// weight_map - Property map for storing edge weights +// vertex_index_map - Property map for vertex indices +// +// Postconditions: g is a complete graph with Euclidean distance weights +// Complexity: O(V^2) where V is the number of vertices +template < typename VertexListGraph, typename WeightMap, + typename VertexIndexMap, typename BinaryFunction, typename CoordType = double > +void make_random_euclidean_graph(VertexListGraph& g, std::size_t num_points, + std::size_t coordinate_max, WeightMap weight_map, + VertexIndexMap vertex_index_map, BinaryFunction distance) +{ + std::vector< simple_point< CoordType > > points; + points.reserve(num_points); + generate_unique_random_points< simple_point< CoordType > >( + num_points, coordinate_max, std::back_inserter(points)); + connect_all_geometric(g, points, weight_map, vertex_index_map, distance); +} + +// make_random_euclidean_graph (parameterized distribution version for simple points) +// +// Version with custom distribution support for flexible point generation. +template < typename VertexListGraph, typename WeightMap, + typename VertexIndexMap, typename XDistribution, typename YDistribution , typename BinaryFunction> +void make_random_euclidean_graph(VertexListGraph& g, std::size_t num_points, + XDistribution x_dist, YDistribution y_dist, WeightMap weight_map, + VertexIndexMap vertex_index_map, BinaryFunction distance) +{ + using CoordType = typename XDistribution::result_type; + std::vector< simple_point< CoordType > > points; + points.reserve(num_points); + generate_unique_random_points< simple_point< CoordType > >( + num_points, x_dist, y_dist, std::back_inserter(points)); + connect_all_geometric(g, points, weight_map, vertex_index_map, distance); +} + + +// make_random_geometric_graph (parameterized for custom points) +// +// Creates a complete graph with random points of arbitrary PointType and geometric distance weights. +// This function allows the user to specify the point type and random distributions for each coordinate. +// +// Parameters: +// g - Graph to populate (must have num_vertices(g) == num_points) +// num_points - Number of vertices/points +// x_dist, y_dist - Distributions for x and y coordinates +// weight_map - Property map for storing edge weights +// vertex_index_map - Property map for vertex indices +// distance - Binary function to compute distance between two points (e.g., +// boost::geometry::distance) +// Postconditions: g is a complete graph with geometric distance weights +// Complexity: O(V^2) where V is the number of vertices +template < typename PointType, typename VertexListGraph, typename WeightMap, + typename VertexIndexMap, typename XDistribution, typename YDistribution, typename BinaryFunction > +void make_random_geometric_graph(VertexListGraph& g, std::size_t num_points, + XDistribution x_dist, YDistribution y_dist, WeightMap weight_map, + VertexIndexMap vertex_index_map, BinaryFunction distance) +{ + std::vector< PointType > points; + points.reserve(num_points); + generate_unique_random_points(num_points, x_dist, y_dist, std::back_inserter(points)); + connect_all_geometric(g, points, weight_map, vertex_index_map, distance); +} + + +// make_random_geometric_graph +// +// Creates a complete graph with random points of arbitrary PointType and geometric distance weights. +// This overload uses a uniform real distribution for both coordinates in the range [0, coordinate_max]. +// +// Parameters: +// g - Graph to populate (must have num_vertices(g) == num_points) +// num_points - Number of vertices/points +// coordinate_max - Maximum coordinate value for random points +// weight_map - Property map for storing edge weights +// vertex_index_map - Property map for vertex indices +// +// Postconditions: g is a complete graph with geometric distance weights +// Complexity: O(V^2) where V is the number of vertices +template < typename PointType, typename VertexListGraph, typename WeightMap, + typename VertexIndexMap, typename CoordType = double > +void make_random_geometric_graph(VertexListGraph& g, std::size_t num_points, + std::size_t coordinate_max, WeightMap weight_map, + VertexIndexMap vertex_index_map) +{ + std::vector< PointType > points; + points.reserve(num_points); + generate_unique_random_points(num_points, coordinate_max, std::back_inserter(points)); + connect_all_geometric(g, points, weight_map, vertex_index_map); +} + +} // namespace boost + +#endif // BOOST_GRAPH_GEOMETRIC_GRAPH_GENERATOR_HPP diff --git a/include/boost/graph/simple_point.hpp b/include/boost/graph/simple_point.hpp index 0e3dffca6..bb0cc23d1 100644 --- a/include/boost/graph/simple_point.hpp +++ b/include/boost/graph/simple_point.hpp @@ -9,6 +9,10 @@ #ifndef BOOST_GRAPH_SIMPLE_POINT_HPP #define BOOST_GRAPH_SIMPLE_POINT_HPP +#include + +#include + namespace boost { @@ -16,8 +20,53 @@ template < typename T > struct simple_point { T x; T y; + + // Deduce return type: float for float, double for double/ints, long double + // for long double, to avoid silent and dangerous truncation of floating to int in distance + using distance_type = + typename std::conditional< std::is_same< T, long double >::value, + long double, + typename std::conditional< std::is_same< T, float >::value, float, + double >::type >::type; + + constexpr friend distance_type distance( + const simple_point& a, const simple_point& b) + { + return std::hypot(static_cast< distance_type >(a.x) + - static_cast< distance_type >(b.x), + static_cast< distance_type >(a.y) + - static_cast< distance_type >(b.y)); + } + + constexpr friend + bool operator==(simple_point const &a, simple_point const &b) noexcept + { + return a.x == b.x && a.y == b.y; + } + + constexpr friend + bool operator!=(simple_point const &a, simple_point const &b) noexcept + { + return !(a == b); + } + + friend constexpr + std::size_t hash_value(simple_point const& p) + { + std::size_t seed = 0; + + // Normalize zero values to avoid -0.0 and +0.0 hash collisions + T x_norm = p.x == T(0) ? T(0) : p.x; + T y_norm = p.y == T(0) ? T(0) : p.y; + + boost::hash_combine(seed, x_norm); + boost::hash_combine(seed, y_norm); + + return seed; + } }; + } // end namespace boost #endif // BOOST_GRAPH_SIMPLE_POINT_HPP diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index 43669da80..232c7f040 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -66,6 +66,7 @@ alias graph_test_regular : [ compile filtered_graph_cc.cpp ] [ run filter_graph_vp_test.cpp ] [ run generator_test.cpp ] + [ run geometric_graph_generator_test.cpp ] [ run graph.cpp : : : TEST=1 : graph_1 ] [ run graph.cpp : : : TEST=2 : graph_2 ] [ run graph.cpp : : : TEST=3 : graph_3 ] diff --git a/test/geometric_graph_generator_test.cpp b/test/geometric_graph_generator_test.cpp new file mode 100644 index 000000000..26a35e59e --- /dev/null +++ b/test/geometric_graph_generator_test.cpp @@ -0,0 +1,479 @@ +//======================================================================= +// Copyright 2026 +// Author: Matyas W Egyhazy +// +// Distributed under 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_TEST_MODULE geometric_graph_generator_test +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace +{ + +// Type aliases for common graph types used in tests +using UndirectedListGraph + = boost::adjacency_list< boost::vecS, boost::vecS, boost::undirectedS, + boost::no_property, boost::property< boost::edge_weight_t, double > >; + +using UndirectedMatrixGraph = boost::adjacency_matrix< boost::undirectedS, + boost::no_property, boost::property< boost::edge_weight_t, double > >; + +using PointDbl = boost::simple_point< double >; + +// Helper: Calculate Euclidean distance between two points +template < typename CoordType > +double euclidean_distance(const boost::simple_point< CoordType >& p1, + const boost::simple_point< CoordType >& p2) +{ + return std::hypot(p1.x - p2.x, p1.y - p2.y); +} + +// Helper: Verify that a graph is complete +template < typename Graph > +bool is_complete_graph(const Graph& g) +{ + const std::size_t n = boost::num_vertices(g); + const std::size_t expected_edges = (n * (n - 1)) / 2; + return boost::num_edges(g) == expected_edges; +} + +//======================================================================= +// Test Fixtures +//======================================================================= + +// Fixture for tests using random points with matrix graph +template < typename Graph > +struct RandomGraphFixture +{ + static constexpr std::size_t num_vertices = 10; + Graph g; + std::vector< PointDbl > points; + + RandomGraphFixture() : g(num_vertices) + { + boost::generate_unique_random_points< PointDbl >( + num_vertices, 100, std::back_inserter(points)); + boost::connect_all_geometric(g, points, + boost::get(boost::edge_weight, g), + boost::get(boost::vertex_index, g)); + } +}; + +using MatrixGraphFixture = RandomGraphFixture< UndirectedMatrixGraph >; +using ListGraphFixture = RandomGraphFixture< UndirectedListGraph >; + +// Fixture for tests using known 3-4-5 triangle points +template < typename Graph, typename PointType > +struct KnownPointsFixture +{ + Graph g; + std::vector< PointType > points; + decltype(boost::get(boost::edge_weight, g)) weight_map; + + KnownPointsFixture() : g(5), weight_map(boost::get(boost::edge_weight, g)) + { + points = { { 0.0, 0.0 }, { 3.0, 0.0 }, { 0.0, 4.0 }, { 3.0, 4.0 }, + { 1.5, 2.0 } }; + boost::connect_all_geometric( + g, points, weight_map, boost::get(boost::vertex_index, g)); + } +}; + +using MatrixKnownPointsFixture + = KnownPointsFixture< UndirectedMatrixGraph, PointDbl >; + +struct adl_distance_fn +{ + template < typename A, typename B > + auto operator()(const A& a, const B& b) const -> decltype(distance(a, b)) + { + return distance(a, b); + } +}; + +} // anonymous namespace + +//======================================================================= +// Test Suite: generate_unique_random_points +//======================================================================= + +BOOST_AUTO_TEST_SUITE(generate_random_points_tests) + +BOOST_AUTO_TEST_CASE(test_uniqueness) +{ + const std::size_t num_points = 100; + std::vector< PointDbl > points; + + boost::generate_unique_random_points< PointDbl >( + num_points, 10000, std::back_inserter(points)); + + using Pair = std::pair< double, double >; + boost::unordered_flat_set< Pair, boost::hash< Pair > > unique_points; + for (const auto& p : points) + { + auto result = unique_points.emplace(p.x, p.y); + BOOST_TEST(result.second); // Assert immediately if duplicate found + } +} + +BOOST_AUTO_TEST_CASE(test_custom_distributions) +{ + const std::size_t num_points = 30; + std::vector< PointDbl > points; + + std::uniform_real_distribution< double > x_dist(0.0, 100.0); + std::uniform_real_distribution< double > y_dist(0.0, 200.0); + + std::size_t generated = boost::generate_unique_random_points< PointDbl >( + num_points, x_dist, y_dist, std::back_inserter(points)); + + BOOST_TEST(generated == num_points); + BOOST_TEST(points.size() == num_points); + + for (const auto& p : points) + { + BOOST_TEST(p.x >= 0.0); + BOOST_TEST(p.x <= 100.0); + BOOST_TEST(p.y >= 0.0); + BOOST_TEST(p.y <= 200.0); + } +} + +BOOST_AUTO_TEST_CASE(test_max_attempts) +{ + const std::size_t num_points = 100; + std::uniform_int_distribution< int > narrow_dist(0, 9); + std::mt19937 rng(12345); + std::vector< boost::simple_point< int > > points; + + std::size_t generated + = boost::generate_unique_random_points < boost::simple_point< int > >(num_points, + narrow_dist, narrow_dist, std::back_inserter(points), rng, 200); + + BOOST_TEST(generated < num_points); + BOOST_TEST(points.size() == generated); +} + +BOOST_AUTO_TEST_CASE(test_empty) +{ + std::vector< PointDbl > points; + std::size_t generated + = boost::generate_unique_random_points< PointDbl >(0, 100, std::back_inserter(points)); + + BOOST_TEST(generated == 0u); + BOOST_TEST(points.empty()); +} + +BOOST_AUTO_TEST_SUITE_END() + +// Not perfectly elegant but enables boost::geometry points to work with boost::connect_all_geometric and +// boost::make_random_euclidean_graph, and does not require linking against +// boost::geometry library +namespace boost { +namespace geometry { + namespace model { + namespace d2 { + +template +std::size_t hash_value(point_xy const &p) +{ + std::size_t seed = 0; + + // Normalize zero values to avoid -0.0 and +0.0 hash collisions + Float x_norm = p.x() == Float(0) ? Float(0) : p.x(); + Float y_norm = p.y() == Float(0) ? Float(0) : p.y(); + + boost::hash_combine(seed, x_norm); + boost::hash_combine(seed, y_norm); + + return seed; +} + +template +constexpr +bool operator==(point_xy const &a, point_xy const &b) +{ + return a.x() == b.x() && a.y() == b.y(); +} + +}}}} + +struct geom_distance_fn +{ + template < typename P1, typename P2 > + auto operator()(const P1& a, const P2& b) const + -> decltype(boost::geometry::distance(a, b)) + { + return boost::geometry::distance(a, b); + } +}; + +BOOST_AUTO_TEST_CASE(test_boost_geometry_point_compatibility) +{ + using BoostGeomPoint = boost::geometry::model::d2::point_xy; + using Graph = boost::adjacency_matrix< boost::undirectedS, boost::no_property, boost::property< boost::edge_weight_t, double > >; + + std::vector points = { + BoostGeomPoint(0.0, 0.0), + BoostGeomPoint(3.0, 0.0), + BoostGeomPoint(0.0, 4.0) + }; + Graph g(points.size()); + auto weight_map = boost::get(boost::edge_weight, g); + auto vertex_index_map = boost::get(boost::vertex_index, g); + + boost::connect_all_geometric( + g, points, weight_map, vertex_index_map, geom_distance_fn { }); + + // Check edge weights using Boost.Geometry distance + auto e01 = boost::edge(0, 1, g); + BOOST_REQUIRE(e01.second); + double expected01 = boost::geometry::distance(points[0], points[1]); + BOOST_TEST(boost::get(weight_map, e01.first) == expected01, boost::test_tools::tolerance(1e-10)); + + auto e02 = boost::edge(0, 2, g); + BOOST_REQUIRE(e02.second); + double expected02 = boost::geometry::distance(points[0], points[2]); + BOOST_TEST(boost::get(weight_map, e02.first) == expected02, boost::test_tools::tolerance(1e-10)); + + auto e12 = boost::edge(1, 2, g); + BOOST_REQUIRE(e12.second); + double expected12 = boost::geometry::distance(points[1], points[2]); + BOOST_TEST(boost::get(weight_map, e12.first) == expected12, boost::test_tools::tolerance(1e-10)); +} + +//======================================================================= +// Test Suite: connect_all_geometric (using fixtures) +//======================================================================= + +BOOST_AUTO_TEST_SUITE(connect_all_euclidean_tests) + +BOOST_FIXTURE_TEST_CASE(test_adjacency_matrix, MatrixGraphFixture) +{ + BOOST_TEST(is_complete_graph(g)); +} + +BOOST_FIXTURE_TEST_CASE(test_adjacency_list, ListGraphFixture) +{ + BOOST_TEST(is_complete_graph(g)); +} + +BOOST_FIXTURE_TEST_CASE(test_edge_weights_matrix, MatrixKnownPointsFixture) +{ + auto e01 = boost::edge(0, 1, g); + BOOST_REQUIRE(e01.second); + BOOST_TEST(boost::get(weight_map, e01.first) + == euclidean_distance(points[0], points[1]), + boost::test_tools::tolerance(1e-10)); + + auto e02 = boost::edge(0, 2, g); + BOOST_REQUIRE(e02.second); + BOOST_TEST(boost::get(weight_map, e02.first) + == euclidean_distance(points[0], points[2]), + boost::test_tools::tolerance(1e-10)); + + auto e03 = boost::edge(0, 3, g); + BOOST_REQUIRE(e03.second); + BOOST_TEST(boost::get(weight_map, e03.first) + == euclidean_distance(points[0], points[3]), + boost::test_tools::tolerance(1e-10)); +} + +BOOST_AUTO_TEST_CASE(test_single_vertex_matrix) +{ + UndirectedMatrixGraph g(1); + std::vector< PointDbl > points = { { 0.0, 0.0 } }; + + boost::connect_all_geometric(g, points, boost::get(boost::edge_weight, g), + boost::get(boost::vertex_index, g)); + + BOOST_TEST(boost::num_edges(g) == 0u); +} + +BOOST_AUTO_TEST_SUITE_END() + +//======================================================================= +// Test Suite: make_random_euclidean_graph +//======================================================================= + +BOOST_AUTO_TEST_SUITE(make_random_euclidean_graph_tests) + +BOOST_AUTO_TEST_CASE(test_with_distributions_matrix) +{ + const std::size_t num_vertices = 20; + UndirectedMatrixGraph g(num_vertices); + + std::uniform_real_distribution< double > x_dist(0.0, 500.0); + std::normal_distribution< double > y_dist(250.0, 50.0); + + + boost::make_random_euclidean_graph(g, num_vertices, x_dist, y_dist, + boost::get(boost::edge_weight, g), boost::get(boost::vertex_index, g), + adl_distance_fn {}); + + BOOST_TEST(is_complete_graph(g)); +} + +BOOST_AUTO_TEST_SUITE_END() + +//======================================================================= +// Test Suite: float_precision +//======================================================================= + +BOOST_AUTO_TEST_SUITE(float_precision_tests) + +BOOST_AUTO_TEST_CASE(test_float_generation_and_math) +{ + const std::size_t num_points = 20; + std::vector< boost::simple_point< float > > points; + std::uniform_real_distribution< float > dist(0.0f, 100.0f); + + std::size_t generated = boost::generate_unique_random_points< boost::simple_point< float > >( + num_points, dist, dist, std::back_inserter(points)); + + BOOST_TEST(generated == num_points); + + using FloatGraph = boost::adjacency_matrix< boost::undirectedS, + boost::no_property, boost::property< boost::edge_weight_t, float > >; + + FloatGraph g(3); + std::vector< boost::simple_point< float > > known_points + = { { 0.0f, 0.0f }, { 3.0f, 0.0f }, { 0.0f, 4.0f } }; + + boost::connect_all_geometric(g, known_points, + boost::get(boost::edge_weight, g), boost::get(boost::vertex_index, g)); + + auto weight_map = boost::get(boost::edge_weight, g); + + auto e01 = boost::edge(0, 1, g); + BOOST_REQUIRE(e01.second); + BOOST_TEST(boost::get(weight_map, e01.first) + == euclidean_distance(known_points[0], known_points[1]), + boost::test_tools::tolerance(1e-5f)); + + auto e02 = boost::edge(0, 2, g); + BOOST_REQUIRE(e02.second); + BOOST_TEST(boost::get(weight_map, e02.first) + == euclidean_distance(known_points[0], known_points[2]), + boost::test_tools::tolerance(1e-5f)); + + auto e12 = boost::edge(1, 2, g); + BOOST_REQUIRE(e12.second); + BOOST_TEST(boost::get(weight_map, e12.first) + == euclidean_distance(known_points[1], known_points[2]), + boost::test_tools::tolerance(1e-5f)); +} + +BOOST_AUTO_TEST_SUITE_END() + +//======================================================================= +// Test Suite: triangle_inequality +//======================================================================= + +BOOST_AUTO_TEST_SUITE(triangle_inequality_tests) + +BOOST_FIXTURE_TEST_CASE( + test_euclidean_weights_satisfy_triangle_inequality_matrix, + MatrixGraphFixture) +{ + auto weight_map = boost::get(boost::edge_weight, g); + + for (std::size_t i = 0; i < num_vertices; ++i) + { + for (std::size_t j = 0; j < num_vertices; ++j) + { + if (i == j) + continue; + for (std::size_t k = 0; k < num_vertices; ++k) + { + if (k == i || k == j) + continue; + + auto e_ik = boost::edge(i, k, g); + auto e_ij = boost::edge(i, j, g); + auto e_jk = boost::edge(j, k, g); + + double d_ik = boost::get(weight_map, e_ik.first); + double d_ij = boost::get(weight_map, e_ij.first); + double d_jk = boost::get(weight_map, e_jk.first); + + BOOST_TEST(d_ik <= d_ij + d_jk + 1e-10); + } + } + } +} + +BOOST_AUTO_TEST_SUITE_END() + +//======================================================================= +// Test Suite: make_random_geometric_graph +//======================================================================= + +BOOST_AUTO_TEST_SUITE(make_random_geometric_graph_tests) + +BOOST_AUTO_TEST_CASE(test_with_uniform_distribution_simple_point) +{ + using Graph = UndirectedMatrixGraph; + using Point = boost::simple_point; + const std::size_t num_vertices = 15; + const std::size_t coordinate_max = 1000; + Graph g(num_vertices); + boost::make_random_geometric_graph< Point >( + g, num_vertices, coordinate_max, + boost::get(boost::edge_weight, g), + boost::get(boost::vertex_index, g)); + BOOST_TEST(is_complete_graph(g)); +} + + /* Note: This fails due to ADL issues with boost::geometry::distance and the +point_xy type. + BOOST_AUTO_TEST_CASE(test_with_uniform_distribution_boost_geometry_point) + { + using Graph = UndirectedMatrixGraph; + using Point = boost::geometry::model::d2::point_xy; + const std::size_t num_vertices = 12; + const std::size_t coordinate_max = 500; + Graph g(num_vertices); + boost::make_random_geometric_graph< Point >( + g, num_vertices, coordinate_max, + boost::get(boost::edge_weight, g), + boost::get(boost::vertex_index, g)); + BOOST_TEST(is_complete_graph(g)); +} +*/ + +BOOST_AUTO_TEST_CASE(test_with_custom_distribution_boost_geometry_point) +{ + using Graph = UndirectedMatrixGraph; + using Point = boost::geometry::model::d2::point_xy; + const std::size_t num_vertices = 10; + std::uniform_real_distribution x_dist(0.0, 100.0); + std::normal_distribution y_dist(50.0, 10.0); + Graph g(num_vertices); + boost::make_random_geometric_graph< Point >( + g, num_vertices, x_dist, y_dist, + boost::get(boost::edge_weight, g), boost::get(boost::vertex_index, g), + geom_distance_fn { }); + BOOST_TEST(is_complete_graph(g)); +} + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file