diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64ed25c..2d52e6d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,8 @@ name: build on: - schedule: - - cron: '0 5 * * *' - push: + merge_group: pull_request: + push: env: CARGO_TERM_COLOR: always jobs: @@ -11,29 +10,22 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 + - name: msrv + run: | + msrv=$(cargo metadata --no-deps --format-version 1 | + jq --raw-output '.packages[] | select(.name=="miniball") | .rust_version') + echo "MSRV=$msrv" >> $GITHUB_ENV - name: toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - toolchain: nightly - profile: minimal + toolchain: ${{ env.MSRV }} components: rustfmt, rust-docs, clippy - override: true - name: test - uses: actions-rs/cargo@v1 - with: - command: test + run: cargo test - name: clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --tests -- --deny clippy::pedantic + run: cargo clippy --tests --examples -- -D clippy::all -D clippy::pedantic -D clippy::nursery - name: doc - uses: actions-rs/cargo@v1 - with: - command: doc + run: cargo doc - name: fmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --check + run: cargo fmt --check diff --git a/Cargo.toml b/Cargo.toml index 67f9761..be2e2c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,8 @@ name = "miniball" description = "Minimum enclosing ball" authors = ["Rouven Spreckels "] -version = "0.2.0" +version = "0.3.0" +rust-version = "1.61.0" edition = "2021" documentation = "https://docs.rs/miniball" repository = "https://github.com/qu1x/miniball" @@ -31,8 +32,7 @@ include = [ ] [dependencies] -arrayvec = "0.7.2" -nalgebra = { version = "0.32.2", features = ["rand"] } +nalgebra = { version = "0.32.4", features = ["rand"] } stacker = "0.1.15" [profile.test] diff --git a/README.md b/README.md index 69408d3..3356077 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,16 @@ Minimum enclosing ball. [![Documentation][]](https://docs.rs/miniball) [![Downloads][]](https://crates.io/crates/miniball) [![Version][]](https://crates.io/crates/miniball) -[![Rust][]](https://www.rust-lang.org) +[![Rust][]](https://blog.rust-lang.org/2022/05/19/Rust-1.61.0.html) [![License][]](https://mozilla.org/MPL) [Build]: https://github.com/qu1x/miniball/actions/workflows/build.yml/badge.svg [Documentation]: https://docs.rs/miniball/badge.svg [Downloads]: https://img.shields.io/crates/d/miniball.svg [Version]: https://img.shields.io/crates/v/miniball.svg -[Rust]: https://img.shields.io/badge/rust-nightly-orange.svg +[Rust]: https://img.shields.io/badge/rust-v1.61.0-brightgreen.svg [License]: https://img.shields.io/crates/l/miniball -**NOTE**: This crate requires nightly Rust. - * Finds circumscribed *n*-ball of set of bounds. * Finds minimum *n*-ball enclosing set of points. @@ -33,7 +31,7 @@ See the [release history] to keep track of the development. ## License -Copyright © 2022 Rouven Spreckels +Copyright © 2022-2024 Rouven Spreckels Licensed under the terms of the [`MPL-2.0`](LICENSES/MPL-2.0). diff --git a/RELEASES.md b/RELEASES.md index 8ad7533..587b791 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,8 @@ +# Version 0.3.0 (2024-03-17) + + * Replace `Point` with `OPoint` supporting static as well as dynamic dimensions. + * Replace `ArrayVec` with `OVec>` supporting stabe Rust. + # Version 0.2.0 (2023-04-01) * Update dependencies. diff --git a/rustfmt.toml b/rustfmt.toml index 0ab32d8..a37478d 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,2 +1,2 @@ hard_tabs = true -format_code_in_doc_comments = true +#format_code_in_doc_comments = true diff --git a/src/ball.rs b/src/ball.rs index f9a7df6..ceb386e 100644 --- a/src/ball.rs +++ b/src/ball.rs @@ -1,37 +1,55 @@ -// Copyright © 2022 Rouven Spreckels +// Copyright © 2022-2024 Rouven Spreckels // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. use super::Enclosing; -use nalgebra::{distance_squared, Point, RealField, SMatrix, SVector}; +use nalgebra::{ + base::allocator::Allocator, DefaultAllocator, DimNameAdd, DimNameSum, OMatrix, OPoint, OVector, + RealField, U1, +}; /// Ball over real field `R` of dimension `D` with center and radius squared. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Ball { +#[derive(Debug, Clone, PartialEq)] +pub struct Ball> +where + DefaultAllocator: Allocator, +{ /// Ball's center. - pub center: Point, + pub center: OPoint, /// Ball's radius squared. pub radius_squared: R, } -impl Enclosing for Ball { +impl> Copy for Ball +where + OPoint: Copy, + DefaultAllocator: Allocator, +{ +} + +impl> Enclosing for Ball +where + DefaultAllocator: + Allocator + Allocator + Allocator, DimNameSum>, + , DimNameSum>>::Buffer: Default, +{ #[inline] - fn contains(&self, point: &Point) -> bool { - distance_squared(&self.center, point) <= self.radius_squared + fn contains(&self, point: &OPoint) -> bool { + (point - &self.center).norm_squared() <= self.radius_squared } - fn with_bounds(bounds: &[Point]) -> Option { - let length = bounds.len().checked_sub(1).filter(|&length| length <= D)?; - let points = SMatrix::::from_fn(|row, column| { + fn with_bounds(bounds: &[OPoint]) -> Option { + let length = bounds.len().checked_sub(1).filter(|&len| len <= D::USIZE)?; + let points = OMatrix::::from_fn(|row, column| { if column < length { bounds[column + 1].coords[row].clone() - bounds[0].coords[row].clone() } else { R::zero() } }); - let points = points.view((0, 0), (D, length)); - let matrix = SMatrix::::from_fn(|row, column| { + let points = points.view((0, 0), (D::USIZE, length)); + let matrix = OMatrix::::from_fn(|row, column| { if row < length && column < length { points.column(row).dot(&points.column(column)) * (R::one() + R::one()) } else { @@ -39,7 +57,7 @@ impl Enclosing for Ball { } }); let matrix = matrix.view((0, 0), (length, length)); - let vector = SVector::::from_fn(|row, _column| { + let vector = OVector::::from_fn(|row, _column| { if row < length { points.column(row).norm_squared() } else { @@ -49,11 +67,11 @@ impl Enclosing for Ball { let vector = vector.view((0, 0), (length, 1)); matrix.try_inverse().map(|matrix| { let vector = matrix * vector; - let mut center = SVector::::zeros(); + let mut center = OVector::::zeros(); for point in 0..length { center += points.column(point) * vector[point].clone(); } - Ball { + Self { center: &bounds[0] + ¢er, radius_squared: center.norm_squared(), } diff --git a/src/deque.rs b/src/deque.rs index b938742..b7b7e60 100644 --- a/src/deque.rs +++ b/src/deque.rs @@ -1,4 +1,4 @@ -// Copyright © 2022 Rouven Spreckels +// Copyright © 2022-2024 Rouven Spreckels // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -32,49 +32,49 @@ pub trait Deque { impl Deque for VecDeque { #[inline] fn len(&self) -> usize { - VecDeque::len(self) + Self::len(self) } #[inline] fn pop_front(&mut self) -> Option { - VecDeque::pop_front(self) + Self::pop_front(self) } #[inline] fn pop_back(&mut self) -> Option { - VecDeque::pop_back(self) + Self::pop_back(self) } #[inline] fn push_front(&mut self, value: T) { - VecDeque::push_front(self, value); + Self::push_front(self, value); } #[inline] fn push_back(&mut self, value: T) { - VecDeque::push_back(self, value); + Self::push_back(self, value); } } impl Deque for LinkedList { #[inline] fn len(&self) -> usize { - LinkedList::len(self) + Self::len(self) } #[inline] fn pop_front(&mut self) -> Option { - LinkedList::pop_front(self) + Self::pop_front(self) } #[inline] fn pop_back(&mut self) -> Option { - LinkedList::pop_back(self) + Self::pop_back(self) } #[inline] fn push_front(&mut self, value: T) { - LinkedList::push_front(self, value); + Self::push_front(self, value); } #[inline] fn push_back(&mut self, value: T) { - LinkedList::push_back(self, value); + Self::push_back(self, value); } } diff --git a/src/enclosing.rs b/src/enclosing.rs index 1bd156a..46fd053 100644 --- a/src/enclosing.rs +++ b/src/enclosing.rs @@ -1,27 +1,34 @@ -// Copyright © 2022 Rouven Spreckels +// Copyright © 2022-2024 Rouven Spreckels // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use super::Deque; -use arrayvec::ArrayVec; -use nalgebra::{Point, RealField}; +use super::{Deque, OVec}; +use nalgebra::{ + base::allocator::Allocator, DefaultAllocator, DimNameAdd, DimNameSum, OPoint, RealField, U1, +}; use stacker::maybe_grow; use std::mem::size_of; /// Minimum enclosing ball. -pub trait Enclosing: Clone { +pub trait Enclosing> +where + Self: Clone, + DefaultAllocator: Allocator + Allocator, DimNameSum>, + , DimNameSum>>::Buffer: Default, +{ #[doc(hidden)] /// Guaranteed stack size per recursion step. - const RED_ZONE: usize = 32 * 1_024 + (8 * D + 2 * D.pow(2)) * size_of::>(); + const RED_ZONE: usize = + 32 * 1_024 + (8 * D::USIZE + 2 * D::USIZE.pow(2)) * size_of::>(); #[doc(hidden)] /// New stack space to allocate if within [`Self::RED_ZONE`]. const STACK_SIZE: usize = Self::RED_ZONE * 1_024; /// Whether ball contains `point`. #[must_use] - fn contains(&self, point: &Point) -> bool; + fn contains(&self, point: &OPoint) -> bool; /// Returns circumscribed ball with all `bounds` on surface or `None` if it does not exist. /// /// # Example @@ -52,7 +59,7 @@ pub trait Enclosing: Clone { /// assert_eq!(radius_squared, 3.0); /// ``` #[must_use] - fn with_bounds(bounds: &[Point]) -> Option; + fn with_bounds(bounds: &[OPoint]) -> Option; /// Returns minimum ball enclosing `points`. /// @@ -123,13 +130,13 @@ pub trait Enclosing: Clone { /// ``` #[must_use] #[inline] - fn enclosing_points(points: &mut impl Deque>) -> Self - where - ArrayVec, { D + 1 }>:, - { + fn enclosing_points(points: &mut impl Deque>) -> Self { maybe_grow(Self::RED_ZONE, Self::STACK_SIZE, || { - Self::enclosing_points_with_bounds(points, &mut ArrayVec::new()) - .expect("Empty point set") + Self::enclosing_points_with_bounds( + points, + &mut OVec::, DimNameSum>::new(), + ) + .expect("Empty point set") }) } /// Returns minimum ball enclosing `points` with `bounds`. @@ -138,20 +145,16 @@ pub trait Enclosing: Clone { #[doc(hidden)] #[must_use] fn enclosing_points_with_bounds( - points: &mut impl Deque>, - bounds: &mut ArrayVec, { D + 1 }>, + points: &mut impl Deque>, + bounds: &mut OVec, DimNameSum>, ) -> Option { // Take point from back. - if let Some(point) = points.pop_back() - && !bounds.is_full() - { + if let Some(point) = points.pop_back().filter(|_| !bounds.is_full()) { let ball = maybe_grow(Self::RED_ZONE, Self::STACK_SIZE, || { // Branch with one point less. Self::enclosing_points_with_bounds(points, bounds) }); - if let Some(ball) = ball - && ball.contains(&point) - { + if let Some(ball) = ball.filter(|ball| ball.contains(&point)) { // Move point to back. points.push_back(point); Some(ball) @@ -168,7 +171,7 @@ pub trait Enclosing: Clone { } } else { // Circumscribed ball with bounds. - Self::with_bounds(bounds) + Self::with_bounds(bounds.as_slice()) } } } diff --git a/src/lib.rs b/src/lib.rs index 168669f..98f383b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright © 2022 Rouven Spreckels +// Copyright © 2022-2024 Rouven Spreckels // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -6,8 +6,6 @@ //! Minimum enclosing ball. //! -//! **NOTE**: This crate requires nightly Rust. -//! //! * Finds circumscribed *n*-ball of set of bounds, see [`Enclosing::with_bounds()`]. //! * Finds minimum *n*-ball enclosing set of points, see [`Enclosing::enclosing_points()`]. //! @@ -20,15 +18,14 @@ #![forbid(missing_docs)] #![forbid(unsafe_code)] #![allow(clippy::tabs_in_doc_comments)] -#![allow(incomplete_features)] -#![feature(generic_const_exprs)] -#![feature(let_chains)] mod ball; mod deque; mod enclosing; +mod ovec; pub use ball::Ball; pub use deque::Deque; pub use enclosing::Enclosing; pub use nalgebra; +use ovec::OVec; diff --git a/src/ovec.rs b/src/ovec.rs new file mode 100644 index 0000000..cbc5a2c --- /dev/null +++ b/src/ovec.rs @@ -0,0 +1,93 @@ +// Copyright © 2022-2024 Rouven Spreckels +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use nalgebra::{base::allocator::Allocator, DefaultAllocator, DimName, OVector}; +use std::mem::take; + +/// Owned vector of item `T` and capacity `D`. +#[doc(hidden)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OVec +where + OVector: Default, + DefaultAllocator: Allocator, +{ + size: usize, + data: OVector, +} + +impl OVec +where + OVector: Default, + DefaultAllocator: Allocator, +{ + /// New empty vector. + #[must_use] + #[inline] + pub fn new() -> Self { + Self::default() + } + /// Number of items. + #[must_use] + #[inline] + pub const fn len(&self) -> usize { + self.size + } + /// Whether vector is empty. + #[must_use] + #[inline] + pub const fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Whether vector is full. + #[must_use] + #[inline] + pub fn is_full(&self) -> bool { + self.len() == self.data.len() + } + /// Immutable slice of items. + #[must_use] + #[inline] + pub fn as_slice(&self) -> &[T] { + &self.data.as_slice()[..self.len()] + } + /// Adds `item`. + /// + /// # Panics + /// + /// Panics if [`Self::is_full()`]. + #[inline] + pub fn push(&mut self, item: T) { + assert!(!self.is_full()); + self.data[self.size] = item; + self.size += 1; + } + /// Removes last item. + /// + /// Returns `Some(T)` or `None` if [`Self::is_empty()`]. + #[inline] + pub fn pop(&mut self) -> Option { + if self.is_empty() { + None + } else { + self.size -= 1; + Some(take(&mut self.data[self.size])) + } + } +} + +impl Default for OVec +where + OVector: Default, + DefaultAllocator: Allocator, +{ + fn default() -> Self { + Self { + size: 0, + data: OVector::default(), + } + } +} diff --git a/tests/ball_enclosing_points.rs b/tests/ball_enclosing_points.rs index 62081a0..3840100 100644 --- a/tests/ball_enclosing_points.rs +++ b/tests/ball_enclosing_points.rs @@ -10,7 +10,7 @@ use miniball::{Ball, Enclosing}; use nalgebra::{ distance, Point, Point1, Point2, Point3, Point6, Vector1, Vector2, Vector3, Vector6, }; -use std::collections::VecDeque; +use std::{collections::VecDeque, iter::once}; #[test] fn minimum_0_ball_enclosing_bounds() { @@ -18,7 +18,7 @@ fn minimum_0_ball_enclosing_bounds() { let Ball { center, radius_squared, - } = Ball::enclosing_points(&mut [a].into_iter().collect::>()); + } = Ball::enclosing_points(&mut once(a).collect::>()); assert_eq!(center, a); assert_eq!(radius_squared, 0.0); } diff --git a/tests/ball_with_bounds.rs b/tests/ball_with_bounds.rs index 9346614..dfa49c6 100644 --- a/tests/ball_with_bounds.rs +++ b/tests/ball_with_bounds.rs @@ -7,11 +7,11 @@ #![allow(clippy::float_cmp)] use miniball::{Ball, Enclosing}; -use nalgebra::{center, Point, Point1, Point2, Point3, Vector1, Vector2, Vector3}; +use nalgebra::{center, Point, Point1, Point2, Point3, Vector1, Vector2, Vector3, U0, U1, U2, U3}; #[test] fn circumscribed_0_ball_with_0_bounds() { - let ball = Ball::::with_bounds(&[]); + let ball = Ball::::with_bounds(&[]); assert_eq!(ball, None); } @@ -36,7 +36,7 @@ fn circumscribed_0_ball_with_2_bounds() { #[test] fn circumscribed_1_ball_with_0_bounds() { - let ball = Ball::::with_bounds(&[]); + let ball = Ball::::with_bounds(&[]); assert_eq!(ball, None); } @@ -66,7 +66,7 @@ fn circumscribed_1_ball_with_2_bounds() { #[test] fn circumscribed_2_ball_with_0_bounds() { - let ball = Ball::::with_bounds(&[]); + let ball = Ball::::with_bounds(&[]); assert_eq!(ball, None); } @@ -129,7 +129,7 @@ fn circumscribed_2_ball_with_3_points() { #[test] fn circumscribed_3_ball_with_0_bounds() { - let ball = Ball::::with_bounds(&[]); + let ball = Ball::::with_bounds(&[]); assert_eq!(ball, None); }