1use ndarray::{Array2, ArrayView2};
10use solow_core::{Error, Result};
11
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[derive(Clone, Debug, PartialEq)]
15pub struct GaussianRandomProjection {
16 pub components: Array2<f64>,
18 pub n_components: usize,
20 pub n_features_in: usize,
22 pub seed: u64,
24}
25
26impl GaussianRandomProjection {
27 pub fn fit(x: ArrayView2<'_, f64>, n_components: usize, seed: u64) -> Result<Self> {
29 let d = x.ncols();
30 if n_components == 0 {
31 return Err(Error::Value("GaussianRandomProjection: n_components must be ≥ 1".into()));
32 }
33 let mut state = seed.wrapping_add(0xF00D_C0DE);
34 let mut r = Array2::<f64>::zeros((n_components, d));
35 let scale = (1.0 / n_components as f64).sqrt();
36 for i in 0..n_components {
37 for j in 0..d {
38 r[[i, j]] = scale * standard_normal(&mut state);
39 }
40 }
41 Ok(Self {
42 components: r,
43 n_components,
44 n_features_in: d,
45 seed,
46 })
47 }
48
49 pub fn transform(&self, x: ArrayView2<'_, f64>) -> Result<Array2<f64>> {
51 if x.ncols() != self.n_features_in {
52 return Err(Error::Shape("GaussianRandomProjection::transform: shape mismatch".into()));
53 }
54 let n = x.nrows();
55 let k = self.n_components;
56 let d = self.n_features_in;
57 let mut out = Array2::<f64>::zeros((n, k));
58 for i in 0..n {
59 for c in 0..k {
60 let mut s = 0.0_f64;
61 for j in 0..d {
62 s += x[[i, j]] * self.components[[c, j]];
63 }
64 out[[i, c]] = s;
65 }
66 }
67 Ok(out)
68 }
69}
70
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
74#[derive(Clone, Debug, PartialEq)]
75pub struct SparseRandomProjection {
76 pub components: Array2<f64>,
78 pub n_components: usize,
80 pub n_features_in: usize,
82 pub density: f64,
84 pub seed: u64,
86}
87
88impl SparseRandomProjection {
89 pub fn fit(x: ArrayView2<'_, f64>, n_components: usize, seed: u64) -> Result<Self> {
91 let d = x.ncols();
92 let density = (1.0_f64 / (d as f64).sqrt()).max(1.0 / d as f64);
93 Self::fit_with(x, n_components, density, seed)
94 }
95
96 pub fn fit_with(
98 x: ArrayView2<'_, f64>,
99 n_components: usize,
100 density: f64,
101 seed: u64,
102 ) -> Result<Self> {
103 let d = x.ncols();
104 if n_components == 0 {
105 return Err(Error::Value("SparseRandomProjection: n_components must be ≥ 1".into()));
106 }
107 if !(0.0..=1.0).contains(&density) || density == 0.0 {
108 return Err(Error::Value(format!(
109 "SparseRandomProjection: density must be in (0, 1] (got {density})"
110 )));
111 }
112 let s = 1.0 / density;
113 let scale = (s / n_components as f64).sqrt();
114 let mut state = seed.wrapping_add(0xF00D_D00D);
115 let mut r = Array2::<f64>::zeros((n_components, d));
116 for i in 0..n_components {
117 for j in 0..d {
118 let u = uniform01(&mut state);
119 r[[i, j]] = if u < 1.0 / (2.0 * s) {
120 -scale
121 } else if u < 1.0 / s {
122 scale
123 } else {
124 0.0
125 };
126 }
127 }
128 Ok(Self {
129 components: r,
130 n_components,
131 n_features_in: d,
132 density,
133 seed,
134 })
135 }
136
137 pub fn transform(&self, x: ArrayView2<'_, f64>) -> Result<Array2<f64>> {
139 if x.ncols() != self.n_features_in {
140 return Err(Error::Shape("SparseRandomProjection::transform: shape mismatch".into()));
141 }
142 let n = x.nrows();
143 let k = self.n_components;
144 let d = self.n_features_in;
145 let mut out = Array2::<f64>::zeros((n, k));
146 for i in 0..n {
147 for c in 0..k {
148 let mut s = 0.0_f64;
149 for j in 0..d {
150 s += x[[i, j]] * self.components[[c, j]];
151 }
152 out[[i, c]] = s;
153 }
154 }
155 Ok(out)
156 }
157}
158
159pub fn johnson_lindenstrauss_min_dim(n_samples: usize, eps: f64) -> usize {
162 if eps <= 0.0 || eps >= 1.0 {
163 return 0;
164 }
165 let denom = eps * eps / 2.0 - eps * eps * eps / 3.0;
166 ((4.0 * (n_samples as f64).ln() / denom).ceil() as usize).max(1)
167}
168
169fn standard_normal(state: &mut u64) -> f64 {
170 let u1 = uniform01(state).max(1e-12);
171 let u2 = uniform01(state);
172 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
173}
174
175fn uniform01(state: &mut u64) -> f64 {
176 *state = state
177 .wrapping_mul(6_364_136_223_846_793_005)
178 .wrapping_add(1_442_695_040_888_963_407);
179 let r = *state >> 11;
180 (r as f64) * f64::from_bits(0x3CA0_0000_0000_0000)
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use ndarray::array;
187
188 #[test]
189 fn gaussian_random_projection_is_deterministic_at_a_seed() {
190 let x = array![[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0]];
191 let a = GaussianRandomProjection::fit(x.view(), 5, 42).unwrap();
192 let b = GaussianRandomProjection::fit(x.view(), 5, 42).unwrap();
193 for i in 0..5 {
194 for j in 0..3 {
195 assert_eq!(a.components[[i, j]], b.components[[i, j]]);
196 }
197 }
198 }
199
200 #[test]
201 fn sparse_random_projection_produces_a_projection_with_the_expected_shape() {
202 let x = array![
203 [1.0_f64, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0]
204 ];
205 let m = SparseRandomProjection::fit(x.view(), 3, 7).unwrap();
206 let z = m.transform(x.view()).unwrap();
207 assert_eq!(z.shape(), &[3, 3]);
208 }
209
210 #[test]
211 fn jl_min_dim_grows_logarithmically_with_n_samples() {
212 let a = johnson_lindenstrauss_min_dim(100, 0.1);
213 let b = johnson_lindenstrauss_min_dim(10_000, 0.1);
214 assert!(b > a);
216 assert!(b < 10 * a);
217 }
218}