fib_quant/
spherical_beta.rs1use rand::Rng;
2use rand_distr::{Distribution, Gamma, StandardNormal};
3
4use crate::{beta_inv::beta_inv, FibQuantError, Result};
5
6pub fn beta_d_k(d: usize, k: usize) -> Result<f64> {
8 validate_dk(d, k)?;
9 if k == d {
10 return Ok(1.0);
11 }
12 let dk_term = (d as f64 - k as f64 - 2.0) / 2.0;
13 let beta = (k as f64 / (k as f64 + 2.0)) * dk_term + 1.0;
14 if beta.is_finite() && beta > 0.0 {
15 Ok(beta)
16 } else {
17 Err(FibQuantError::NumericalFailure(format!(
18 "invalid beta_{{d,k}} for d={d}, k={k}: {beta}"
19 )))
20 }
21}
22
23pub fn radius_quantile(d: usize, k: usize, n: usize, n_total: usize) -> Result<f64> {
25 validate_dk(d, k)?;
26 if n == 0 || n > n_total || n_total == 0 {
27 return Err(FibQuantError::InvalidCodebookSize(n_total));
28 }
29 if k == d {
30 return Ok(1.0);
31 }
32 let q = (n as f64 - 0.5) / n_total as f64;
33 if k == 2 {
34 return radius_quantile_k2_closed_form(d, q);
35 }
36 let alpha = k as f64 / 2.0;
37 let beta = beta_d_k(d, k)?;
38 Ok(beta_inv(q, alpha, beta)?.sqrt())
39}
40
41pub fn radius_quantile_k2_closed_form(d: usize, q: f64) -> Result<f64> {
43 if d <= 2 {
44 return Err(FibQuantError::InvalidBlockDim {
45 ambient_dim: d,
46 block_dim: 2,
47 });
48 }
49 if !(0.0..=1.0).contains(&q) || !q.is_finite() {
50 return Err(FibQuantError::NumericalFailure(format!(
51 "invalid k=2 radius quantile q={q}"
52 )));
53 }
54 Ok((1.0 - (1.0 - q).powf(4.0 / d as f64)).sqrt())
55}
56
57pub fn sample_spherical_beta(d: usize, k: usize, rng: &mut impl Rng) -> Result<Vec<f64>> {
59 validate_dk(d, k)?;
60 if k == d {
61 return sample_unit_sphere(k, rng);
62 }
63 let r2 = sample_beta(k as f64 / 2.0, (d - k) as f64 / 2.0, rng)?;
64 let direction = sample_unit_sphere(k, rng)?;
65 let r = r2.sqrt();
66 Ok(direction.into_iter().map(|value| r * value).collect())
67}
68
69pub fn sample_reference_projection(d: usize, k: usize, rng: &mut impl Rng) -> Result<Vec<f64>> {
71 validate_dk(d, k)?;
72 let mut values = Vec::with_capacity(d);
73 let mut norm_sq = 0.0;
74 for _ in 0..d {
75 let value: f64 = StandardNormal.sample(rng);
76 norm_sq += value * value;
77 values.push(value);
78 }
79 if norm_sq == 0.0 || !norm_sq.is_finite() {
80 return Err(FibQuantError::NumericalFailure(
81 "zero/non-finite gaussian norm".into(),
82 ));
83 }
84 let norm = norm_sq.sqrt();
85 Ok(values
86 .into_iter()
87 .take(k)
88 .map(|value| value / norm)
89 .collect())
90}
91
92pub(crate) fn sample_unit_sphere(k: usize, rng: &mut impl Rng) -> Result<Vec<f64>> {
93 if k == 0 {
94 return Err(FibQuantError::InvalidBlockDim {
95 ambient_dim: 0,
96 block_dim: 0,
97 });
98 }
99 loop {
100 let mut values = Vec::with_capacity(k);
101 let mut norm_sq = 0.0;
102 for _ in 0..k {
103 let value: f64 = StandardNormal.sample(rng);
104 norm_sq += value * value;
105 values.push(value);
106 }
107 if norm_sq > 0.0 && norm_sq.is_finite() {
108 let norm = norm_sq.sqrt();
109 return Ok(values.into_iter().map(|value| value / norm).collect());
110 }
111 }
112}
113
114fn sample_beta(alpha: f64, beta: f64, rng: &mut impl Rng) -> Result<f64> {
115 let ga = Gamma::new(alpha, 1.0)
116 .map_err(|err| FibQuantError::NumericalFailure(format!("gamma alpha: {err}")))?;
117 let gb = Gamma::new(beta, 1.0)
118 .map_err(|err| FibQuantError::NumericalFailure(format!("gamma beta: {err}")))?;
119 let a: f64 = ga.sample(rng);
120 let b: f64 = gb.sample(rng);
121 let sum = a + b;
122 if sum <= 0.0 || !sum.is_finite() {
123 return Err(FibQuantError::NumericalFailure("beta sample sum".into()));
124 }
125 Ok(a / sum)
126}
127
128fn validate_dk(d: usize, k: usize) -> Result<()> {
129 if d == 0 {
130 return Err(FibQuantError::ZeroDimension);
131 }
132 if k == 0 || k > d {
133 return Err(FibQuantError::InvalidBlockDim {
134 ambient_dim: d,
135 block_dim: k,
136 });
137 }
138 Ok(())
139}