Path: blob/main/crates/bevy_math/src/sampling/shape_sampling.rs
6596 views
//! The [`ShapeSample`] trait, allowing random sampling from geometric shapes.1//!2//! At the most basic level, this allows sampling random points from the interior and boundary of3//! geometric primitives. For example:4//! ```5//! # use bevy_math::primitives::*;6//! # use bevy_math::ShapeSample;7//! # use rand::SeedableRng;8//! # use rand::rngs::StdRng;9//! // Get some `Rng`:10//! let rng = &mut StdRng::from_os_rng();11//! // Make a circle of radius 2:12//! let circle = Circle::new(2.0);13//! // Get a point inside this circle uniformly at random:14//! let interior_pt = circle.sample_interior(rng);15//! // Get a point on the circle's boundary uniformly at random:16//! let boundary_pt = circle.sample_boundary(rng);17//! ```18//!19//! For repeated sampling, `ShapeSample` also includes methods for accessing a [`Distribution`]:20//! ```21//! # use bevy_math::primitives::*;22//! # use bevy_math::{Vec2, ShapeSample};23//! # use rand::SeedableRng;24//! # use rand::rngs::StdRng;25//! # use rand::distr::Distribution;26//! # let rng1 = StdRng::from_os_rng();27//! # let rng2 = StdRng::from_os_rng();28//! // Use a rectangle this time:29//! let rectangle = Rectangle::new(1.0, 2.0);30//! // Get an iterator that spits out random interior points:31//! let interior_iter = rectangle.interior_dist().sample_iter(rng1);32//! // Collect random interior points from the iterator:33//! let interior_pts: Vec<Vec2> = interior_iter.take(1000).collect();34//! // Similarly, get an iterator over many random boundary points and collect them:35//! let boundary_pts: Vec<Vec2> = rectangle.boundary_dist().sample_iter(rng2).take(1000).collect();36//! ```37//!38//! In any case, the [`Rng`] used as the source of randomness must be provided explicitly.3940use core::f32::consts::{FRAC_PI_2, PI, TAU};4142use crate::{ops, primitives::*, NormedVectorSpace, ScalarField, Vec2, Vec3};43use rand::{44distr::{45uniform::SampleUniform,46weighted::{Weight, WeightedIndex},47Distribution,48},49Rng,50};5152/// Exposes methods to uniformly sample a variety of primitive shapes.53pub trait ShapeSample {54/// The type of vector returned by the sample methods, [`Vec2`] for 2D shapes and [`Vec3`] for 3D shapes.55type Output;5657/// Uniformly sample a point from inside the area/volume of this shape, centered on 0.58///59/// Shapes like [`Cylinder`], [`Capsule2d`] and [`Capsule3d`] are oriented along the y-axis.60///61/// # Example62/// ```63/// # use bevy_math::prelude::*;64/// let square = Rectangle::new(2.0, 2.0);65///66/// // Returns a Vec2 with both x and y between -1 and 1.67/// println!("{}", square.sample_interior(&mut rand::rng()));68/// ```69fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output;7071/// Uniformly sample a point from the surface of this shape, centered on 0.72///73/// Shapes like [`Cylinder`], [`Capsule2d`] and [`Capsule3d`] are oriented along the y-axis.74///75/// # Example76/// ```77/// # use bevy_math::prelude::*;78/// let square = Rectangle::new(2.0, 2.0);79///80/// // Returns a Vec2 where one of the coordinates is at ±1,81/// // and the other is somewhere between -1 and 1.82/// println!("{}", square.sample_boundary(&mut rand::rng()));83/// ```84fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output;8586/// Extract a [`Distribution`] whose samples are points of this shape's interior, taken uniformly.87///88/// # Example89///90/// ```91/// # use bevy_math::prelude::*;92/// # use rand::distr::Distribution;93/// let square = Rectangle::new(2.0, 2.0);94/// let rng = rand::rng();95///96/// // Iterate over points randomly drawn from `square`'s interior:97/// for random_val in square.interior_dist().sample_iter(rng).take(5) {98/// println!("{}", random_val);99/// }100/// ```101fn interior_dist(self) -> impl Distribution<Self::Output>102where103Self: Sized,104{105InteriorOf(self)106}107108/// Extract a [`Distribution`] whose samples are points of this shape's boundary, taken uniformly.109///110/// # Example111///112/// ```113/// # use bevy_math::prelude::*;114/// # use rand::distr::Distribution;115/// let square = Rectangle::new(2.0, 2.0);116/// let rng = rand::rng();117///118/// // Iterate over points randomly drawn from `square`'s boundary:119/// for random_val in square.boundary_dist().sample_iter(rng).take(5) {120/// println!("{}", random_val);121/// }122/// ```123fn boundary_dist(self) -> impl Distribution<Self::Output>124where125Self: Sized,126{127BoundaryOf(self)128}129}130131#[derive(Clone, Copy)]132/// A wrapper struct that allows interior sampling from a [`ShapeSample`] type directly as133/// a [`Distribution`].134pub struct InteriorOf<T: ShapeSample>(pub T);135136#[derive(Clone, Copy)]137/// A wrapper struct that allows boundary sampling from a [`ShapeSample`] type directly as138/// a [`Distribution`].139pub struct BoundaryOf<T: ShapeSample>(pub T);140141impl<T: ShapeSample> Distribution<<T as ShapeSample>::Output> for InteriorOf<T> {142fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> <T as ShapeSample>::Output {143self.0.sample_interior(rng)144}145}146147impl<T: ShapeSample> Distribution<<T as ShapeSample>::Output> for BoundaryOf<T> {148fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> <T as ShapeSample>::Output {149self.0.sample_boundary(rng)150}151}152153impl ShapeSample for Circle {154type Output = Vec2;155156fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec2 {157// https://mathworld.wolfram.com/DiskPointPicking.html158let theta = rng.random_range(0.0..TAU);159let r_squared = rng.random_range(0.0..=(self.radius * self.radius));160let r = ops::sqrt(r_squared);161let (sin, cos) = ops::sin_cos(theta);162Vec2::new(r * cos, r * sin)163}164165fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec2 {166let theta = rng.random_range(0.0..TAU);167let (sin, cos) = ops::sin_cos(theta);168Vec2::new(self.radius * cos, self.radius * sin)169}170}171172impl ShapeSample for CircularSector {173type Output = Vec2;174175fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec2 {176let theta = rng.random_range(-self.half_angle()..=self.half_angle());177let r_squared = rng.random_range(0.0..=(self.radius() * self.radius()));178let r = ops::sqrt(r_squared);179let (sin, cos) = ops::sin_cos(theta);180Vec2::new(r * sin, r * cos)181}182183fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec2 {184if rng.random_range(0.0..=1.0) <= self.arc_length() / self.perimeter() {185// Sample on the arc186let theta = FRAC_PI_2 + rng.random_range(-self.half_angle()..self.half_angle());187Vec2::from_angle(theta) * self.radius()188} else {189// Sample on the "inner" straight lines190let dir = self.radius() * Vec2::from_angle(FRAC_PI_2 + self.half_angle());191let r: f32 = rng.random_range(-1.0..1.0);192(-r).clamp(0.0, 1.0) * dir + r.clamp(0.0, 1.0) * dir * Vec2::new(-1.0, 1.0)193}194}195}196197/// Boundary sampling for unit-spheres198#[inline]199fn sample_unit_sphere_boundary<R: Rng + ?Sized>(rng: &mut R) -> Vec3 {200let z = rng.random_range(-1f32..=1f32);201let (a_sin, a_cos) = ops::sin_cos(rng.random_range(-PI..=PI));202let c = ops::sqrt(1f32 - z * z);203let x = a_sin * c;204let y = a_cos * c;205206Vec3::new(x, y, z)207}208209impl ShapeSample for Sphere {210type Output = Vec3;211212fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {213let r_cubed = rng.random_range(0.0..=(self.radius * self.radius * self.radius));214let r = ops::cbrt(r_cubed);215216r * sample_unit_sphere_boundary(rng)217}218219fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {220self.radius * sample_unit_sphere_boundary(rng)221}222}223224impl ShapeSample for Annulus {225type Output = Vec2;226227fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {228let inner_radius = self.inner_circle.radius;229let outer_radius = self.outer_circle.radius;230231// Like random sampling for a circle, radius is weighted by the square.232let r_squared =233rng.random_range((inner_radius * inner_radius)..(outer_radius * outer_radius));234let r = ops::sqrt(r_squared);235let theta = rng.random_range(0.0..TAU);236let (sin, cos) = ops::sin_cos(theta);237238Vec2::new(r * cos, r * sin)239}240241fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {242let total_perimeter = self.inner_circle.perimeter() + self.outer_circle.perimeter();243let inner_prob = (self.inner_circle.perimeter() / total_perimeter) as f64;244245// Sample from boundary circles, choosing which one by weighting by perimeter:246let inner = rng.random_bool(inner_prob);247if inner {248self.inner_circle.sample_boundary(rng)249} else {250self.outer_circle.sample_boundary(rng)251}252}253}254255impl ShapeSample for Rhombus {256type Output = Vec2;257258fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec2 {259let x: f32 = rng.random_range(0.0..=1.0);260let y: f32 = rng.random_range(0.0..=1.0);261262let unit_p = Vec2::NEG_X + x * Vec2::ONE + Vec2::new(y, -y);263unit_p * self.half_diagonals264}265266fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec2 {267let x: f32 = rng.random_range(-1.0..=1.0);268let y_sign = if rng.random() { -1.0 } else { 1.0 };269270let y = (1.0 - ops::abs(x)) * y_sign;271Vec2::new(x, y) * self.half_diagonals272}273}274275impl ShapeSample for Rectangle {276type Output = Vec2;277278fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec2 {279let x = rng.random_range(-self.half_size.x..=self.half_size.x);280let y = rng.random_range(-self.half_size.y..=self.half_size.y);281Vec2::new(x, y)282}283284fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec2 {285let primary_side = rng.random_range(-1.0..1.0);286let other_side = if rng.random() { -1.0 } else { 1.0 };287288if self.half_size.x + self.half_size.y > 0.0 {289if rng.random_bool((self.half_size.x / (self.half_size.x + self.half_size.y)) as f64) {290Vec2::new(primary_side, other_side) * self.half_size291} else {292Vec2::new(other_side, primary_side) * self.half_size293}294} else {295Vec2::ZERO296}297}298}299300impl ShapeSample for Cuboid {301type Output = Vec3;302303fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {304let x = rng.random_range(-self.half_size.x..=self.half_size.x);305let y = rng.random_range(-self.half_size.y..=self.half_size.y);306let z = rng.random_range(-self.half_size.z..=self.half_size.z);307Vec3::new(x, y, z)308}309310fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {311let primary_side1 = rng.random_range(-1.0..1.0);312let primary_side2 = rng.random_range(-1.0..1.0);313let other_side = if rng.random() { -1.0 } else { 1.0 };314315if let Ok(dist) = WeightedIndex::new([316self.half_size.y * self.half_size.z,317self.half_size.x * self.half_size.z,318self.half_size.x * self.half_size.y,319]) {320match dist.sample(rng) {3210 => Vec3::new(other_side, primary_side1, primary_side2) * self.half_size,3221 => Vec3::new(primary_side1, other_side, primary_side2) * self.half_size,3232 => Vec3::new(primary_side1, primary_side2, other_side) * self.half_size,324_ => unreachable!(),325}326} else {327Vec3::ZERO328}329}330}331332/// Interior sampling for triangles which doesn't depend on the ambient dimension.333fn sample_triangle_interior<P, R>(vertices: [P; 3], rng: &mut R) -> P334where335P: NormedVectorSpace,336P::Scalar: SampleUniform + PartialOrd,337R: Rng + ?Sized,338{339let [a, b, c] = vertices;340let ab = b - a;341let ac = c - a;342343// Generate random points on a parallelepiped and reflect so that344// we can use the points that lie outside the triangle345let u = rng.random_range(P::Scalar::ZERO..=P::Scalar::ONE);346let v = rng.random_range(P::Scalar::ZERO..=P::Scalar::ONE);347348if u + v > P::Scalar::ONE {349let u1 = P::Scalar::ONE - v;350let v1 = P::Scalar::ONE - u;351a + (ab * u1 + ac * v1)352} else {353a + (ab * u + ac * v)354}355}356357/// Boundary sampling for triangles which doesn't depend on the ambient dimension.358fn sample_triangle_boundary<P, R>(vertices: [P; 3], rng: &mut R) -> P359where360P: NormedVectorSpace,361P::Scalar: Weight + SampleUniform + PartialOrd + for<'a> ::core::ops::AddAssign<&'a P::Scalar>,362R: Rng + ?Sized,363{364let [a, b, c] = vertices;365let ab = b - a;366let ac = c - a;367let bc = c - b;368369let t = rng.random_range(<P::Scalar as ScalarField>::ZERO..=P::Scalar::ONE);370371if let Ok(dist) = WeightedIndex::new([ab.norm(), ac.norm(), bc.norm()]) {372match dist.sample(rng) {3730 => a.lerp(b, t),3741 => a.lerp(c, t),3752 => b.lerp(c, t),376_ => unreachable!(),377}378} else {379// This should only occur when the triangle is 0-dimensional degenerate380// so this is actually the correct result.381a382}383}384385impl ShapeSample for Triangle2d {386type Output = Vec2;387388fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {389sample_triangle_interior(self.vertices, rng)390}391392fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {393sample_triangle_boundary(self.vertices, rng)394}395}396397impl ShapeSample for Triangle3d {398type Output = Vec3;399400fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {401sample_triangle_interior(self.vertices, rng)402}403404fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {405sample_triangle_boundary(self.vertices, rng)406}407}408409impl ShapeSample for Tetrahedron {410type Output = Vec3;411412fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {413let [v0, v1, v2, v3] = self.vertices;414415// Generate a random point in a cube:416let mut coords: [f32; 3] = [417rng.random_range(0.0..1.0),418rng.random_range(0.0..1.0),419rng.random_range(0.0..1.0),420];421422// The cube is broken into six tetrahedra of the form 0 <= c_0 <= c_1 <= c_2 <= 1,423// where c_i are the three euclidean coordinates in some permutation. (Since 3! = 6,424// there are six of them). Sorting the coordinates folds these six tetrahedra into the425// tetrahedron 0 <= x <= y <= z <= 1 (i.e. a fundamental domain of the permutation action).426coords.sort_by(|x, y| x.partial_cmp(y).unwrap());427428// Now, convert a point from the fundamental tetrahedron into barycentric coordinates by429// taking the four successive differences of coordinates; note that these telescope to sum430// to 1, and this transformation is linear, hence preserves the probability density, since431// the latter comes from the Lebesgue measure.432//433// (See https://en.wikipedia.org/wiki/Lebesgue_measure#Properties — specifically, that434// Lebesgue measure of a linearly transformed set is its original measure times the435// determinant.)436let (a, b, c, d) = (437coords[0],438coords[1] - coords[0],439coords[2] - coords[1],4401. - coords[2],441);442443// This is also a linear mapping, so probability density is still preserved.444v0 * a + v1 * b + v2 * c + v3 * d445}446447fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {448let triangles = self.faces();449let areas = triangles.iter().map(Measured2d::area);450451if areas.clone().sum::<f32>() > 0.0 {452// There is at least one triangle with nonzero area, so this unwrap succeeds.453let dist = WeightedIndex::new(areas).unwrap();454455// Get a random index, then sample the interior of the associated triangle.456let idx = dist.sample(rng);457triangles[idx].sample_interior(rng)458} else {459// In this branch the tetrahedron has zero surface area; just return a point that's on460// the tetrahedron.461self.vertices[0]462}463}464}465466impl ShapeSample for Cylinder {467type Output = Vec3;468469fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {470let Vec2 { x, y: z } = self.base().sample_interior(rng);471let y = rng.random_range(-self.half_height..=self.half_height);472Vec3::new(x, y, z)473}474475fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {476// This uses the area of the ends divided by the overall surface area (optimized)477// [2 (\pi r^2)]/[2 (\pi r^2) + 2 \pi r h] = r/(r + h)478if self.radius + 2.0 * self.half_height > 0.0 {479if rng.random_bool((self.radius / (self.radius + 2.0 * self.half_height)) as f64) {480let Vec2 { x, y: z } = self.base().sample_interior(rng);481if rng.random() {482Vec3::new(x, self.half_height, z)483} else {484Vec3::new(x, -self.half_height, z)485}486} else {487let Vec2 { x, y: z } = self.base().sample_boundary(rng);488let y = rng.random_range(-self.half_height..=self.half_height);489Vec3::new(x, y, z)490}491} else {492Vec3::ZERO493}494}495}496497impl ShapeSample for Capsule2d {498type Output = Vec2;499500fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec2 {501let rectangle_area = self.half_length * self.radius * 4.0;502let capsule_area = rectangle_area + PI * self.radius * self.radius;503if capsule_area > 0.0 {504// Check if the random point should be inside the rectangle505if rng.random_bool((rectangle_area / capsule_area) as f64) {506self.to_inner_rectangle().sample_interior(rng)507} else {508let circle = Circle::new(self.radius);509let point = circle.sample_interior(rng);510// Add half length if it is the top semi-circle, otherwise subtract half511if point.y > 0.0 {512point + Vec2::Y * self.half_length513} else {514point - Vec2::Y * self.half_length515}516}517} else {518Vec2::ZERO519}520}521522fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec2 {523let rectangle_surface = 4.0 * self.half_length;524let capsule_surface = rectangle_surface + TAU * self.radius;525if capsule_surface > 0.0 {526if rng.random_bool((rectangle_surface / capsule_surface) as f64) {527let side_distance =528rng.random_range((-2.0 * self.half_length)..=(2.0 * self.half_length));529if side_distance < 0.0 {530Vec2::new(self.radius, side_distance + self.half_length)531} else {532Vec2::new(-self.radius, side_distance - self.half_length)533}534} else {535let circle = Circle::new(self.radius);536let point = circle.sample_boundary(rng);537// Add half length if it is the top semi-circle, otherwise subtract half538if point.y > 0.0 {539point + Vec2::Y * self.half_length540} else {541point - Vec2::Y * self.half_length542}543}544} else {545Vec2::ZERO546}547}548}549550impl ShapeSample for Capsule3d {551type Output = Vec3;552553fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {554let cylinder_vol = PI * self.radius * self.radius * 2.0 * self.half_length;555// Add 4/3 pi r^3556let capsule_vol = cylinder_vol + 4.0 / 3.0 * PI * self.radius * self.radius * self.radius;557if capsule_vol > 0.0 {558// Check if the random point should be inside the cylinder559if rng.random_bool((cylinder_vol / capsule_vol) as f64) {560self.to_cylinder().sample_interior(rng)561} else {562let sphere = Sphere::new(self.radius);563let point = sphere.sample_interior(rng);564// Add half length if it is the top semi-sphere, otherwise subtract half565if point.y > 0.0 {566point + Vec3::Y * self.half_length567} else {568point - Vec3::Y * self.half_length569}570}571} else {572Vec3::ZERO573}574}575576fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {577let cylinder_surface = TAU * self.radius * 2.0 * self.half_length;578let capsule_surface = cylinder_surface + 4.0 * PI * self.radius * self.radius;579if capsule_surface > 0.0 {580if rng.random_bool((cylinder_surface / capsule_surface) as f64) {581let Vec2 { x, y: z } = Circle::new(self.radius).sample_boundary(rng);582let y = rng.random_range(-self.half_length..=self.half_length);583Vec3::new(x, y, z)584} else {585let sphere = Sphere::new(self.radius);586let point = sphere.sample_boundary(rng);587// Add half length if it is the top semi-sphere, otherwise subtract half588if point.y > 0.0 {589point + Vec3::Y * self.half_length590} else {591point - Vec3::Y * self.half_length592}593}594} else {595Vec3::ZERO596}597}598}599600impl<P: Primitive2d + Measured2d + ShapeSample<Output = Vec2>> ShapeSample for Extrusion<P> {601type Output = Vec3;602603fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {604let base_point = self.base_shape.sample_interior(rng);605let depth = rng.random_range(-self.half_depth..self.half_depth);606base_point.extend(depth)607}608609fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {610let base_area = self.base_shape.area();611let total_area = self.area();612613let random = rng.random_range(0.0..total_area);614match random {615x if x < base_area => self.base_shape.sample_interior(rng).extend(self.half_depth),616x if x < 2. * base_area => self617.base_shape618.sample_interior(rng)619.extend(-self.half_depth),620_ => self621.base_shape622.sample_boundary(rng)623.extend(rng.random_range(-self.half_depth..self.half_depth)),624}625}626}627628#[cfg(test)]629mod tests {630use super::*;631use rand::SeedableRng;632use rand_chacha::ChaCha8Rng;633634#[test]635fn circle_interior_sampling() {636let mut rng = ChaCha8Rng::from_seed(Default::default());637let circle = Circle::new(8.0);638639let boxes = [640(-3.0, 3.0),641(1.0, 2.0),642(-1.0, -2.0),643(3.0, -2.0),644(1.0, -6.0),645(-3.0, -7.0),646(-7.0, -3.0),647(-6.0, 1.0),648];649let mut box_hits = [0; 8];650651// Checks which boxes (if any) the sampled points are in652for _ in 0..5000 {653let point = circle.sample_interior(&mut rng);654655for (i, box_) in boxes.iter().enumerate() {656if (point.x > box_.0 && point.x < box_.0 + 4.0)657&& (point.y > box_.1 && point.y < box_.1 + 4.0)658{659box_hits[i] += 1;660}661}662}663664assert_eq!(665box_hits,666[396, 377, 415, 404, 366, 408, 408, 430],667"samples will occur across all array items at statistically equal chance"668);669}670671#[test]672fn circle_boundary_sampling() {673let mut rng = ChaCha8Rng::from_seed(Default::default());674let circle = Circle::new(1.0);675676let mut wedge_hits = [0; 8];677678// Checks in which eighth of the circle each sampled point is in679for _ in 0..5000 {680let point = circle.sample_boundary(&mut rng);681682let angle = ops::atan(point.y / point.x) + PI / 2.0;683let wedge = ops::floor(angle * 8.0 / PI) as usize;684wedge_hits[wedge] += 1;685}686687assert_eq!(688wedge_hits,689[636, 608, 639, 603, 614, 650, 640, 610],690"samples will occur across all array items at statistically equal chance"691);692}693}694695696