#[derive(Debug, Clone)]
pub struct Distribution1D {
func: Vec<f32>,
cdf: Vec<f32>,
func_int: f32,
}
impl Distribution1D {
pub fn new(func: Vec<f32>) -> Self {
let n = func.len().max(1);
let mut cdf = vec![0.0f32; n + 1];
for i in 1..=n {
let w = func.get(i - 1).copied().unwrap_or(0.0).max(0.0);
cdf[i] = w.mul_add(1.0 / n as f32, cdf[i - 1]);
}
let func_int = cdf[n];
if func_int > 0.0 {
for c in &mut cdf[1..=n] {
*c /= func_int;
}
cdf[n] = 1.0;
} else {
for (i, c) in cdf.iter_mut().enumerate().take(n + 1).skip(1) {
*c = i as f32 / n as f32;
}
}
Self {
func,
cdf,
func_int,
}
}
pub(crate) fn n(&self) -> usize {
self.func.len().max(1)
}
pub(crate) fn find_bucket(&self, u: f32) -> usize {
let cdf = &self.cdf;
let mut first = 0usize;
let mut len = cdf.len();
while len > 0 {
let half = len / 2;
let middle = first + half;
if cdf[middle] <= u {
first = middle + 1;
len -= half + 1;
} else {
len = half;
}
}
first.saturating_sub(1).min(self.n() - 1)
}
fn bucket_pdf(&self, offset: usize) -> f32 {
if self.func_int > 0.0 {
self.func[offset].max(0.0) / self.func_int
} else {
1.0
}
}
pub fn sample_continuous(&self, u: f32) -> (f32, f32, usize) {
let u = u.clamp(0.0, 0.999_999_94);
let offset = self.find_bucket(u);
let span = self.cdf[offset + 1] - self.cdf[offset];
let du = if span > 0.0 {
(u - self.cdf[offset]) / span
} else {
0.0
};
let n = self.n();
let sample = ((offset as f32 + du) / n as f32).clamp(0.0, 0.999_999_94);
(sample, self.bucket_pdf(offset), offset)
}
pub fn pdf(&self, x: f32) -> f32 {
let n = self.n();
let offset = ((x.clamp(0.0, 0.999_999_94)) * n as f32) as usize;
self.bucket_pdf(offset.min(n - 1))
}
#[cfg(feature = "gpu")]
#[must_use]
pub(crate) fn cdf(&self) -> &[f32] {
&self.cdf
}
#[cfg(feature = "gpu")]
#[must_use]
pub(crate) fn func(&self) -> &[f32] {
&self.func
}
#[cfg(feature = "gpu")]
#[must_use]
pub(crate) const fn func_int(&self) -> f32 {
self.func_int
}
}
#[derive(Debug, Clone)]
pub struct Distribution2D {
conditional: Vec<Distribution1D>,
marginal: Distribution1D,
height: usize,
}
impl Distribution2D {
pub fn new(func: &[f32], width: usize, height: usize) -> Self {
debug_assert_eq!(func.len(), width * height);
let height = height.max(1);
let width = width.max(1);
let mut conditional = Vec::with_capacity(height);
let mut marginal_func = Vec::with_capacity(height);
for y in 0..height {
let start = y * width;
let row: Vec<f32> = func
.get(start..start + width)
.map_or_else(|| vec![0.0; width], <[f32]>::to_vec);
let dist = Distribution1D::new(row);
marginal_func.push(dist.func_int);
conditional.push(dist);
}
let marginal = Distribution1D::new(marginal_func);
Self {
conditional,
marginal,
height,
}
}
pub fn sample(&self, u0: f32, u1: f32) -> (f32, f32, f32) {
let (v, pdf_v, row) = self.marginal.sample_continuous(u1);
let (u, pdf_u, _col) = self.conditional[row].sample_continuous(u0);
(u, v, pdf_u * pdf_v)
}
pub fn pdf(&self, u: f32, v: f32) -> f32 {
let row = ((v.clamp(0.0, 0.999_999_94)) * self.height as f32) as usize;
let row = row.min(self.height - 1);
self.marginal.pdf(v) * self.conditional[row].pdf(u)
}
#[cfg(feature = "gpu")]
#[must_use]
pub(crate) const fn marginal(&self) -> &Distribution1D {
&self.marginal
}
#[cfg(feature = "gpu")]
#[must_use]
pub(crate) fn conditional(&self) -> &[Distribution1D] {
&self.conditional
}
#[cfg(feature = "gpu")]
#[must_use]
pub(crate) const fn height(&self) -> usize {
self.height
}
#[cfg(feature = "gpu")]
#[must_use]
pub(crate) fn width(&self) -> usize {
self.conditional[0].n()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uniform_weights_give_uniform_pdf_and_samples() {
let dist = Distribution1D::new(vec![1.0; 8]);
for i in 0..=20 {
let u = i as f32 / 20.0;
let (_, pdf, _) = dist.sample_continuous(u.min(0.999_999));
assert!(
(pdf - 1.0).abs() < 1e-5,
"uniform bucket weights must give pdf==1, got {pdf}"
);
}
}
#[test]
fn all_zero_weights_fall_back_to_uniform_without_nan() {
let dist = Distribution1D::new(vec![0.0; 4]);
for i in 0..10 {
let u = i as f32 / 10.0;
let (sample, pdf, _) = dist.sample_continuous(u);
assert!(sample.is_finite() && !sample.is_nan());
assert!((pdf - 1.0).abs() < 1e-6);
}
}
#[test]
fn single_bucket_distribution_does_not_panic() {
let dist = Distribution1D::new(vec![3.5]);
let (sample, pdf, offset) = dist.sample_continuous(0.5);
assert_eq!(offset, 0);
assert!((0.0..1.0).contains(&sample));
assert!((pdf - 1.0).abs() < 1e-6);
}
#[test]
fn dominant_bucket_is_sampled_almost_always() {
let mut weights = vec![0.0f32; 16];
weights[10] = 1000.0;
let dist = Distribution1D::new(weights);
let hits = (0..1000)
.filter(|&i| {
let u = (i as f32 + 0.5) / 1000.0;
dist.sample_continuous(u).2 == 10
})
.count();
assert!(
hits >= 990,
"expected the dominant bucket to be hit almost every time, got {hits}/1000"
);
}
#[test]
fn distribution_2d_pdf_matches_sample_pdf() {
let width = 8;
let height = 4;
let func: Vec<f32> = (0..width * height).map(|i| i as f32 + 1.0).collect();
let dist2d = Distribution2D::new(&func, width, height);
let (u, v, pdf_sampled) = dist2d.sample(0.37, 0.81);
let pdf_looked_up = dist2d.pdf(u, v);
assert!(
(pdf_sampled - pdf_looked_up).abs() < 1e-4,
"sample()'s pdf and pdf() must agree at the sampled point"
);
}
}