diff --git a/example/Jamfile.v2 b/example/Jamfile.v2 index ed34f78d7..a70a53de9 100644 --- a/example/Jamfile.v2 +++ b/example/Jamfile.v2 @@ -81,6 +81,7 @@ run filtered_graph_edge_range.cpp ; run filtered_vec_as_graph.cpp ; run filtered-copy-example.cpp ; exe fr_layout : fr_layout.cpp /boost/timer//boost_timer : [ requires cxx11_noexcept cxx11_rvalue_references sfinae_expr cxx11_auto_declarations cxx11_lambdas cxx11_unified_initialization_syntax cxx11_hdr_tuple cxx11_hdr_initializer_list cxx11_hdr_chrono cxx11_thread_local cxx11_constexpr cxx11_nullptr cxx11_numeric_limits cxx11_decltype cxx11_hdr_array cxx11_hdr_atomic cxx11_hdr_type_traits cxx11_allocator cxx11_explicit_conversion_operators ] ; +run geometric_graph_generator_example.cpp ; run gerdemann.cpp ; run graph.cpp ; run graph_as_tree.cpp ; diff --git a/example/geometric_graph_generator_example.cpp b/example/geometric_graph_generator_example.cpp new file mode 100644 index 000000000..fc2662f8d --- /dev/null +++ b/example/geometric_graph_generator_example.cpp @@ -0,0 +1,63 @@ +//======================================================================= +// Copyright 2026 Matyas W Egyhazy +// Copyright (C) 2026 Arnaud Becheler +// 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 + +namespace +{ + +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 = ::boost::graph_traits< Graph >::edge_descriptor; + +struct euclidean +{ + template < typename P > auto operator()(P const& a, P const& b) const -> decltype(distance(a, b)) + { + return distance(a, b); + } +}; + +} // end anonymous namespace + +int main() +{ + constexpr std::size_t num_points = 20; + + Graph g(num_points); + std::mt19937 gen(42); + std::uniform_real_distribution< double > coordinates(0.0, 500.0); + auto weight_map = ::boost::get(::boost::edge_weight, g); + + ::boost::graph::make_random_geometric_graph< Point >(g, num_points, coordinates, coordinates, weight_map, ::boost::get(::boost::vertex_index, g), gen, euclidean {}); + + std::cout << "Complete geometric graph: " << ::boost::num_vertices(g) << " vertices, " << ::boost::num_edges(g) << " edges\n"; + + std::vector< Edge > mst; + ::boost::kruskal_minimum_spanning_tree(g, std::back_inserter(mst)); + + double total = 0.0; + + for (const Edge& e : mst) + total += ::boost::get(weight_map, e); + + std::cout << "Minimum spanning tree: " << mst.size() << " edges, total weight " << total << "\n"; + + return 0; +} diff --git a/include/boost/graph/geometric_graph_generator.hpp b/include/boost/graph/geometric_graph_generator.hpp new file mode 100644 index 000000000..2dc4c1f54 --- /dev/null +++ b/include/boost/graph/geometric_graph_generator.hpp @@ -0,0 +1,146 @@ +//======================================================================= +// Copyright 2026 Matyas W Egyhazy +// Copyright (C) 2026 Arnaud Becheler +// 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 graph +{ + +// Connects every pair of vertices with an edge weighted by the distance between +// the corresponding points. A common preprocessing step for TSP algorithms. +// +// The distance functor must be supplied by the caller. Writing an ADL wrapper +// inside namespace boost is not possible because unqualified lookup there also +// finds boost::distance from Boost.Range. +// +// Preconditions: num_vertices(g) == points.size() and g has no edges. +// Complexity: O(V^2). +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) +{ + using Traits = ::boost::graph_traits< VertexListGraph >; + + BOOST_CONCEPT_ASSERT((::boost::VertexListGraphConcept< VertexListGraph >)); + BOOST_CONCEPT_ASSERT((::boost::MutableGraphConcept< VertexListGraph >)); + BOOST_CONCEPT_ASSERT((::boost::RandomAccessContainerConcept< PointContainer >)); + BOOST_CONCEPT_ASSERT((::boost::ReadablePropertyMapConcept< VertexIndexMap, typename Traits::vertex_descriptor >)); + BOOST_CONCEPT_ASSERT((::boost::WritablePropertyMapConcept< WeightMap, typename Traits::edge_descriptor >)); + + BOOST_STATIC_ASSERT_MSG((!std::is_convertible< typename Traits::directed_category, ::boost::directed_tag >::value), "connect_all_geometric requires an undirected graph type."); + + using WeightType = typename ::boost::property_traits< WeightMap >::value_type; + using IndexType = typename ::boost::property_traits< VertexIndexMap >::value_type; + + BOOST_STATIC_ASSERT_MSG(!std::is_integral< WeightType >::value, "connect_all_geometric requires a non-integral weight type. Integer weights truncate distances."); + + BOOST_ASSERT_MSG(num_vertices(g) == points.size(), "connect_all_geometric requires num_vertices(g) == points.size()"); + + using VertexIterator = typename Traits::vertex_iterator; + std::pair< VertexIterator, VertexIterator > verts(vertices(g)); + + for (VertexIterator src(verts.first); src != verts.second; ++src) + { + const IndexType src_index = get(vmap, *src); + VertexIterator dest(src); + ++dest; + + for (; dest != verts.second; ++dest) + { + const IndexType dest_index = get(vmap, *dest); + const WeightType weight = static_cast< WeightType >(distance(points[src_index], points[dest_index])); + put(wmap, add_edge(*src, *dest, g).first, weight); + } + } +} + +// Writes num_points distinct points to out, in generation order. +// +// Uniqueness is decided on the drawn coordinate pair rather than on PointType, so +// PointType needs no hash and no equality of its own. That is equivalent as long +// as PointType{x, y} is injective. A point type that normalises or snaps on +// construction needs its own filter. +// +// Returns the number of points actually written, which is less than num_points +// when the distributions cannot supply enough distinct values within +// max_attempts draws. A max_attempts of zero selects a default budget. +template < typename PointType, typename OutputIterator, typename XDistribution, typename YDistribution, typename URBG > +std::size_t generate_unique_random_points(std::size_t num_points, XDistribution x_dist, YDistribution y_dist, OutputIterator out, URBG&& gen, 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"); + + if (max_attempts == 0) + max_attempts = std::max< std::size_t >(10 * num_points, 100); + + ::boost::unordered_flat_set< std::pair< CoordType, CoordType > > seen; + seen.reserve(num_points); + + std::size_t attempts = 0; + + while (seen.size() < num_points && attempts < max_attempts) + { + const CoordType x = x_dist(gen); + const CoordType y = y_dist(gen); + + if (seen.insert(std::make_pair(x, y)).second) + *out++ = PointType { x, y }; + + ++attempts; + } + + return seen.size(); +} + +// Populates g with num_points random points and complete geometric weights. +// +// Throws std::runtime_error when the distributions cannot supply num_points +// distinct points, because a shorter point set would leave connect_all_geometric +// indexing past the end. +template < typename PointType, typename VertexListGraph, typename WeightMap, typename VertexIndexMap, typename XDistribution, typename YDistribution, typename URBG, 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, URBG&& gen, BinaryFunction distance) +{ + std::vector< PointType > points; + points.reserve(num_points); + + const std::size_t generated = generate_unique_random_points< PointType >(num_points, x_dist, y_dist, std::back_inserter(points), gen); + + if (generated != num_points) + throw std::runtime_error("make_random_geometric_graph: the given distributions cannot supply num_points distinct points"); + + connect_all_geometric(g, points, weight_map, vertex_index_map, distance); +} + +} // end namespace graph +} // end 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..889ff1dff 100644 --- a/include/boost/graph/simple_point.hpp +++ b/include/boost/graph/simple_point.hpp @@ -1,5 +1,6 @@ //======================================================================= // Copyright 2005 Trustees of Indiana University +// Copyright (C) 2026 Arnaud Becheler // Authors: Andrew Lumsdaine, Douglas Gregor // // Distributed under the Boost Software License, Version 1.0. (See @@ -9,6 +10,9 @@ #ifndef BOOST_GRAPH_SIMPLE_POINT_HPP #define BOOST_GRAPH_SIMPLE_POINT_HPP +#include +#include + namespace boost { @@ -16,6 +20,14 @@ template < typename T > struct simple_point { T x; T y; + + // Integral coordinates widen to double so that distances are not truncated. + 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; + + 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)); + } }; } // end namespace boost diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index b068df84b..34fbbae77 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -68,6 +68,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..9f120751e --- /dev/null +++ b/test/geometric_graph_generator_test.cpp @@ -0,0 +1,171 @@ +//======================================================================= +// Copyright 2026 Matyas W Egyhazy +// Copyright (C) 2026 Arnaud Becheler +// 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 + +namespace +{ + +using Graph = ::boost::adjacency_matrix< ::boost::undirectedS, ::boost::no_property, ::boost::property< ::boost::edge_weight_t, double > >; +using Point = ::boost::simple_point< double >; + +// ADL on simple_point resolves to its hidden friend. Safe outside namespace boost. +struct euclidean +{ + template < typename P > auto operator()(P const& a, P const& b) const -> decltype(distance(a, b)) + { + return distance(a, b); + } +}; + +// Stands in for a third party point type such as boost::geometry point_xy. +// No hash, no equality, no default constructor. +struct opaque_point +{ + double a; + double b; + + opaque_point(double x, double y) : a(x), b(y) { } +}; + +struct opaque_distance +{ + double operator()(opaque_point const& p, opaque_point const& q) const + { + return std::hypot(p.a - q.a, p.b - q.b); + } +}; + +// A 3-4-5 triangle gives exact weights with no tolerance needed. +void test_known_weights() +{ + Graph g(3); + const std::vector< Point > points = { { 0.0, 0.0 }, { 3.0, 0.0 }, { 0.0, 4.0 } }; + auto weight_map = ::boost::get(::boost::edge_weight, g); + + ::boost::graph::connect_all_geometric(g, points, weight_map, ::boost::get(::boost::vertex_index, g), euclidean {}); + + BOOST_TEST_EQ(::boost::num_edges(g), 3u); + BOOST_TEST_EQ(::boost::get(weight_map, ::boost::edge(0, 1, g).first), 3.0); + BOOST_TEST_EQ(::boost::get(weight_map, ::boost::edge(0, 2, g).first), 4.0); + BOOST_TEST_EQ(::boost::get(weight_map, ::boost::edge(1, 2, g).first), 5.0); +} + +void test_single_vertex_has_no_edges() +{ + Graph g(1); + const std::vector< Point > points = { { 0.0, 0.0 } }; + + ::boost::graph::connect_all_geometric(g, points, ::boost::get(::boost::edge_weight, g), ::boost::get(::boost::vertex_index, g), euclidean {}); + + BOOST_TEST_EQ(::boost::num_edges(g), 0u); +} + +// The same seed must give the same weights, and points come out in generation order. +void test_reproducible() +{ + constexpr std::size_t num_points = 12; + std::uniform_real_distribution< double > dist(0.0, 100.0); + std::vector< double > weights[2]; + + for (int run = 0; run < 2; ++run) + { + Graph g(num_points); + std::mt19937 gen(42); + auto weight_map = ::boost::get(::boost::edge_weight, g); + + ::boost::graph::make_random_geometric_graph< Point >(g, num_points, dist, dist, weight_map, ::boost::get(::boost::vertex_index, g), gen, euclidean {}); + + BOOST_TEST_EQ(::boost::num_edges(g), num_points * (num_points - 1) / 2); + + const auto edge_range = ::boost::edges(g); + + for (auto it = edge_range.first; it != edge_range.second; ++it) + weights[run].push_back(::boost::get(weight_map, *it)); + } + + BOOST_TEST(weights[0] == weights[1]); +} + +void test_requested_count_is_met() +{ + constexpr std::size_t num_points = 20; + std::uniform_real_distribution< double > dist(0.0, 1000.0); + std::mt19937 gen(1); + std::vector< Point > points; + + const std::size_t generated = ::boost::graph::generate_unique_random_points< Point >(num_points, dist, dist, std::back_inserter(points), gen); + + BOOST_TEST_EQ(generated, num_points); + BOOST_TEST_EQ(points.size(), num_points); +} + +// Nine lattice points cannot fill fifty vertices, so the shortfall must be reported +// rather than left for connect_all_geometric to index past the end. +void test_shortfall_throws() +{ + using IntPoint = ::boost::simple_point< int >; + Graph g(50); + std::uniform_int_distribution< int > narrow(0, 2); + std::mt19937 gen(7); + + BOOST_TEST_THROWS((::boost::graph::make_random_geometric_graph< IntPoint >(g, 50, narrow, narrow, ::boost::get(::boost::edge_weight, g), ::boost::get(::boost::vertex_index, g), gen, euclidean {})), std::runtime_error); +} + +void test_shortfall_is_reported_by_return_value() +{ + using IntPoint = ::boost::simple_point< int >; + std::uniform_int_distribution< int > narrow(0, 2); + std::mt19937 gen(7); + std::vector< IntPoint > points; + + const std::size_t generated = ::boost::graph::generate_unique_random_points< IntPoint >(50, narrow, narrow, std::back_inserter(points), gen, 200); + + BOOST_TEST_LT(generated, 50u); + BOOST_TEST_EQ(points.size(), generated); +} + +// The generator must not require anything of PointType beyond construction from +// two coordinates, otherwise third party point types stop working. +void test_point_type_needs_no_hash_or_equality() +{ + constexpr std::size_t num_points = 10; + Graph g(num_points); + std::mt19937 gen(42); + std::uniform_real_distribution< double > dist(0.0, 100.0); + + ::boost::graph::make_random_geometric_graph< opaque_point >(g, num_points, dist, dist, ::boost::get(::boost::edge_weight, g), ::boost::get(::boost::vertex_index, g), gen, opaque_distance {}); + + BOOST_TEST_EQ(::boost::num_edges(g), num_points * (num_points - 1) / 2); +} + +} // end anonymous namespace + +int main() +{ + test_known_weights(); + test_single_vertex_has_no_edges(); + test_reproducible(); + test_requested_count_is_met(); + test_shortfall_throws(); + test_shortfall_is_reported_by_return_value(); + test_point_type_needs_no_hash_or_equality(); + return ::boost::report_errors(); +}