diff --git a/README.md b/README.md index c28fd31..c068c43 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,132 @@ # kdtreepp -### A C++ k-d tree implementation +[![CI](https://github.com/jhurliman/kdtreepp/actions/workflows/ci.yml/badge.svg)](https://github.com/jhurliman/kdtreepp/actions/workflows/ci.yml) -Requires C++17 and Eigen. The library is header-only; tests and benchmarks are optional and are not consumer dependencies. +**Build spatial queries over your own data.** A header-only C++17 k-d tree with Eigen geometry, custom point and bounds accessors, and a visitor API for pruning the search space. + +Use it for nearest-neighbor searches, region queries, or spatial filtering of objects that have a position or bounding box. You supply the data and query logic; the tree supplies a hierarchy of bounds that lets you skip irrelevant regions. + +- **Keep your object type.** Accessor functions describe where each object is and which bounds contain it. +- **Control the query.** A bounds predicate prunes branches, and a visitor examines candidate items. +- **Integrate as a library.** CMake exports a target; Bzlmod declares Eigen transitively. Tests and benchmarks are optional. + +The tree partitions the input range **in place** and retains iterators into it. Keep the underlying storage alive and its iterators valid for the tree's lifetime. If positions or bounds change, rebuild the tree before querying. Nodes allocate memory; the tree does not make a separate owning copy of your objects. + +## Find the closest point + +This complete example builds a 3D tree and uses squared distances to prune nodes farther away than the best point found so far: + +```cpp +#include +#include +#include +#include +#include + +int main() { + using Point = Eigen::Vector3d; + std::vector> points{ + Point{0, 0, 0}, Point{4, 0, 0}, Point{0, 3, 0}}; + + const auto tree = kdtreepp::MakeEigenKdTreeNode( + points.begin(), points.end(), + [](const Point& p) { return p; }, // sort point + [](const Point& p) { return p; }); // bounds contributor + + const Point query{3, 0, 0}; + double bestDistanceSq = std::numeric_limits::infinity(); + Point closest = Point::Zero(); + bool found = false; + + tree.visit( + [&](const Eigen::AlignedBox3d& bounds) { + return bounds.squaredExteriorDistance(query) < bestDistanceSq; + }, + [&](const Point& point) { + const double distanceSq = (point - query).squaredNorm(); + if (distanceSq < bestDistanceSq) { + bestDistanceSq = distanceSq; + closest = point; + found = true; + } + }); + + if (found) std::cout << closest.transpose() << '\n'; // 4 0 0 +} +``` + +For custom objects, return an Eigen point from the sort accessor and a point or aligned box from the bounds accessor. A region query follows the same pattern: reject nonintersecting node bounds, then test each visited item against the exact region. Leaf visitors receive candidates, so the item-level test remains your responsibility. ## Bazel / Bzlmod -Until a release is registered in the Bazel Central Registry, use a checkout override: +The 2.0.0 build integration is prepared in this branch and is not yet registered in the Bazel Central Registry. Start with a local checkout in your monorepo: ```starlark -bazel_dep(name = "kdtreepp", version = "2.0.0") +# MODULE.bazel +bazel_dep(name = "kdtreepp", version = "2.0.0", repo_name = "spatial") local_path_override(module_name = "kdtreepp", path = "third_party/kdtreepp") ``` -Link `@kdtreepp//:kdtreepp` from your `cc_library`, `cc_binary`, or `cc_test`. Eigen is declared transitively. Set C++17 or newer in your monorepo's toolchain/configuration (for example `--cxxopt=-std=c++17`); dependency `.bazelrc` files do not set consumer compiler options. Repository renaming through `repo_name` is supported. +Add `@spatial//:kdtreepp` to your target's `deps`. Eigen is a transitive dependency, and the renamed repository is covered by the [independent consumer example](examples/bazel-consumer). -For a remote checkout, replace the local override with a `git_override` using a reviewed, full commit SHA. This repository is not yet registered in BCR, so `bazel_dep` alone is not sufficient. No dependency downloads occur during C++ compilation. +For a remote dependency, replace the local override with `git_override(module_name = "kdtreepp", remote = "https://github.com/jhurliman/kdtreepp.git", commit = "")`. Once BCR registration is accepted, the override can be removed. + +Configure C++17 or newer in your monorepo toolchain—for GCC/Clang, `--cxxopt=-std=c++17`. The repository tests use Bazel 9.2. See [RELEASING.md](RELEASING.md) for archive verification and BCR preparation. ## CMake -With Eigen installed, use `add_subdirectory` and link `kdtreepp::kdtreepp`, or install and consume the exported package: +Install Eigen's CMake package first. Then either use `add_subdirectory` and link `kdtreepp::kdtreepp`, or install the header package: ```sh -cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/your/prefix +cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/path/to/prefix cmake --install build ``` +In the consuming project: + ```cmake -find_package(kdtreepp CONFIG REQUIRED) +find_package(kdtreepp 2 CONFIG REQUIRED) target_link_libraries(my_application PRIVATE kdtreepp::kdtreepp) ``` -CMake no longer invokes Conan automatically or changes global compiler flags. The Conan 2 recipe declares Eigen transitively and is validated with `conan create . --build=missing -s compiler.cppstd=17`. Conan 1 is no longer supported. +Set `CMAKE_PREFIX_PATH` to the installation prefix. The target propagates Eigen and the C++17 requirement. Installation is relocatable and architecture-independent; the library does not set global compiler flags. A complete [installed consumer](examples/cmake-consumer) is included. -## Usage +### Conan 2 -### Search for the closest 3D point +The recipe packages the headers and declares Eigen as a dependency: -```cpp -#include -#include -#include -#include +```sh +conan profile detect +conan create . --build=missing -s compiler.cppstd=17 +``` -#include "kdtreepp.hpp" +This creates and tests a local Conan package; it does not publish one. CMake does not invoke Conan automatically. Conan 1 is no longer supported. -using Vector3 = Eigen::Vector3d; -using AlignedBox3 = Eigen::AlignedBox3d; +## Tree construction and traversal -int main() { - std::vector> points; - std::mt19937_64 randGen{size_t(42)}; - std::uniform_real_distribution dist{-1000.0, 1000.0}; - - // Make random points - points.resize(size_t(5000)); - for (auto& point : points) { - point << dist(randGen), dist(randGen), dist(randGen); - } - - // Construct a k-d tree from 3d points - const auto node = kdtreepp::MakeEigenKdTreeNode( - points.begin(), points.end(), [](const Vector3& p) { return p; }, - [](const Vector3& p) { return p; }); - - // Create a random query point - const Vector3 queryPoint{dist(randGen), dist(randGen), dist(randGen)}; - - // Find the closest point to the given query point - double minDistSq = std::numeric_limits::max(); - Vector3 closestPoint; - node.visit( - [&minDistSq, queryPoint](const AlignedBox3& bounds) { - return bounds.squaredExteriorDistance(queryPoint) < minDistSq; - }, - [&minDistSq, &closestPoint, queryPoint](const Vector3& point) { - const double rSq = (point - queryPoint).squaredNorm(); - if (rSq < minDistSq) { - minDistSq = rSq; - closestPoint = point; - } - }); +`MakeEigenKdTreeNode(begin, end, sortPointGetter, boundsGetter, maxPerLeaf = 8, maxSubDivs = 16)` builds a tree from a mutable random-access range. `T` is the coordinate scalar type and `N` is the dimension. - std::cout << "Closest point to " << queryPoint << " is " << closestPoint << "\n"; -} -``` +| Member | Use | +| --- | --- | +| `bounds()` | Inspect a node's surrounding aligned box. | +| `isLeaf()` / `isBranch()` | Inspect the node type. | +| `visit(boundsTest, visitor)` | Visit items under nodes whose bounds pass your predicate. | -## Test +Use finite coordinates, a positive `maxPerLeaf`, and a nonnegative `maxSubDivs`. Smaller leaves can enable tighter pruning at the cost of more nodes; the best configuration depends on your data and queries. The library does not provide dynamic insertion/removal or a dedicated nearest-neighbor method—the example implements the search through traversal. + +## Development ```sh -bazel test //:regression_test -cmake -S . -B build -DKDTREEPP_BUILD_TESTS=ON -cmake --build build +bazelisk test //:regression_test --cxxopt=-std=c++17 +cmake -S . -B build -DKDTREEPP_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel ctest --test-dir build --output-on-failure ``` -The regression checks compare nearest-neighbor results with brute force over empty, singleton, boundary-sized and larger trees, and check duplicate points. Enable `KDTREEPP_BUILD_LEGACY_TESTS` with Catch2 2.x installed to run the original tests, or `KDTREEPP_BUILD_BENCHMARKS` with Google Benchmark installed. - -For sanitizer checks, configure with `-DCMAKE_CXX_FLAGS=-fsanitize=address,undefined` on a supporting compiler. - -# License - -kdtreepp is licensed under [MIT](https://opensource.org/licenses/MIT). +Regression tests compare nearest-neighbor results with brute force, including empty, singleton, duplicate and boundary-sized inputs. CI also checks sanitizers, installed CMake consumers, Conan, Make targets and a checksum-verified archive consumed through a temporary Bazel registry. -Made with [hpp-skel](https://github.com/mapbox/hpp-skel). +Enable `KDTREEPP_BUILD_LEGACY_TESTS` with Catch2 2.x installed for the original suite, or `KDTREEPP_BUILD_BENCHMARKS` with Google Benchmark installed. [RELEASING.md](RELEASING.md) documents Make, coverage and release commands; [CHANGELOG.md](CHANGELOG.md) describes the version 2 build migration. No general performance improvement is claimed by that migration. -## Release preparation +## License -See [RELEASING.md](RELEASING.md) for the tested Conan consumer, restored Make commands and BCR archive/registry validation and submission procedure. Version 2.0.0 is prepared in this branch; it is not yet published or registered. +[MIT](LICENSE). Originally scaffolded with [hpp-skel](https://github.com/mapbox/hpp-skel).