From 132c0a76029dd1f357c5d5d0f345a5f10104a7ff Mon Sep 17 00:00:00 2001 From: Przemyslaw Olszewski Date: Wed, 12 Aug 2026 10:27:40 +0200 Subject: [PATCH] feat: Support KDE in no_std --- .github/workflows/test.yml | 3 +++ Cargo.lock.MSRV | 4 ++-- Cargo.toml | 7 +++++-- README.md | 7 +++++++ src/density/kde.rs | 2 ++ src/density/knn.rs | 2 ++ src/density/mod.rs | 2 ++ tests/no_std/Cargo.toml | 2 +- tests/no_std/src/lib.rs | 5 +++++ 9 files changed, 29 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f90b470e..bd0ef9bd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -87,6 +87,9 @@ jobs: - name: Check no_std with nalgebra and rand run: cargo check --target riscv32imac-unknown-none-elf --no-default-features --features nalgebra,rand --lib + - name: Check no_std with KDE + run: cargo check --target riscv32imac-unknown-none-elf --no-default-features --features kde --lib + features: needs: [clippy, fmt] runs-on: ubuntu-latest diff --git a/Cargo.lock.MSRV b/Cargo.lock.MSRV index 9faa9327..435d8e08 100644 --- a/Cargo.lock.MSRV +++ b/Cargo.lock.MSRV @@ -348,10 +348,10 @@ dependencies = [ [[package]] name = "kdtree" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd666c2aae5dde60d2f9bfa4e1f8c17698fce13d599c0f9b76725641fec13d9" +source = "git+https://github.com/mrhooray/kdtree-rs?rev=db9af2f8e987c4f1c21251998f032b89a8bd2d52#db9af2f8e987c4f1c21251998f032b89a8bd2d52" dependencies = [ "num-traits", + "serde", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 6e3fd32e..61039884 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,11 +31,11 @@ required-features = ["rand", "std", "kde"] [features] default = ["std", "nalgebra", "rand"] -std = ["approx/std", "num-traits/std", "nalgebra?/std", "rand?/std"] +std = ["approx/std", "num-traits/std", "nalgebra?/std", "rand?/std", "kdtree?/std"] nalgebra = ["dep:nalgebra", "nalgebra/alloc", "nalgebra/libm"] rand = ["dep:rand", "nalgebra?/rand-no-std", "rand?/std_rng"] # kd-tree backed density estimation (src/density), implemented in terms of nalgebra vectors -kde = ["dep:kdtree", "nalgebra", "std"] +kde = ["dep:kdtree", "kdtree/libm", "nalgebra"] [dependencies] approx = { version = "0.5.0", default-features = false } @@ -54,7 +54,10 @@ default-features = false [dependencies.kdtree] version = "0.8.1" +git = "https://github.com/mrhooray/kdtree-rs" +rev = "db9af2f8e987c4f1c21251998f032b89a8bd2d52" optional = true +default-features = false [dev-dependencies] criterion = "0.8" diff --git a/README.md b/README.md index 540f2642..a63ff5e8 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,13 @@ matrix-backed distributions without `std`, enable `nalgebra` directly: statrs = { version = "*", default-features = false, features = ["nalgebra"] } ``` +To enable kernel density estimation without `std`, enable `kde`: + +```toml +[dependencies] +statrs = { version = "*", default-features = false, features = ["kde"] } +``` + Heap-backed APIs use Rust's `alloc` crate. A `no_std` application that calls these APIs must provide and initialize a global allocator suitable for its target: diff --git a/src/density/kde.rs b/src/density/kde.rs index 3d561d24..50587900 100644 --- a/src/density/kde.rs +++ b/src/density/kde.rs @@ -1,4 +1,6 @@ use kdtree::distance::squared_euclidean; +#[cfg(not(feature = "std"))] +use num_traits::Float as _; use crate::{ density::{Container, DensityError, nearest_neighbors, neighborhood_radius}, diff --git a/src/density/knn.rs b/src/density/knn.rs index 95d44dd4..1d6b476a 100644 --- a/src/density/knn.rs +++ b/src/density/knn.rs @@ -4,6 +4,8 @@ use crate::{ function::gamma::gamma, }; use core::f64::consts::PI; +#[cfg(not(feature = "std"))] +use num_traits::Float as _; /// Computes the `k`-nearest neighbor density estimate for a given point `x` /// using the samples provided. diff --git a/src/density/mod.rs b/src/density/mod.rs index 0e18eca9..9bad855a 100644 --- a/src/density/mod.rs +++ b/src/density/mod.rs @@ -16,6 +16,8 @@ pub mod kde; pub mod knn; use alloc::vec::Vec; use kdtree::{ErrorKind, KdTree, distance::squared_euclidean}; +#[cfg(not(feature = "std"))] +use num_traits::Float as _; use thiserror::Error; /// Errors that can occur when estimating a density from a sample. diff --git a/tests/no_std/Cargo.toml b/tests/no_std/Cargo.toml index a8ea7b67..322dd108 100644 --- a/tests/no_std/Cargo.toml +++ b/tests/no_std/Cargo.toml @@ -12,7 +12,7 @@ crate-type = ["cdylib"] [dependencies] dlmalloc = { version = "0.2.14", features = ["global"] } -statrs = { path = "../..", default-features = false, features = ["nalgebra"] } +statrs = { path = "../..", default-features = false, features = ["kde"] } [profile.dev] panic = "abort" diff --git a/tests/no_std/src/lib.rs b/tests/no_std/src/lib.rs index 2b83b8bb..ed602602 100644 --- a/tests/no_std/src/lib.rs +++ b/tests/no_std/src/lib.rs @@ -3,6 +3,7 @@ extern crate alloc; use alloc::vec; +use statrs::density::{kde::kde_pdf, knn::knn_pdf}; use statrs::distribution::{Categorical, Continuous, Empirical, Multinomial, MultivariateNormal}; use statrs::generate::log_spaced; use statrs::statistics::{Data, Distribution, MeanN, OrderStatistics, RankTieBreaker}; @@ -22,6 +23,10 @@ fn assert_close(actual: f64, expected: f64, tolerance: f64) { #[unsafe(no_mangle)] pub extern "C" fn verify() { + let kde_samples = vec![[-1.0], [0.0], [1.0]]; + assert!(kde_pdf(&[0.0], &kde_samples, Some(1.0)).unwrap() > 0.0); + assert!(knn_pdf(&[0.0], &kde_samples, Some(1.0)).unwrap() > 0.0); + let categorical = Categorical::new(&[1.0, 2.0, 3.0]).unwrap(); assert_close(categorical.mean().unwrap(), 4.0 / 3.0, 1e-12);