scirs2_interpolate/random_features/
feature_map.rs1use crate::error::InterpolateError;
12use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
13
14pub(super) struct Lcg64 {
19 state: u64,
20}
21
22impl Lcg64 {
23 pub(super) fn new(seed: u64) -> Self {
24 Self {
26 state: seed.wrapping_add(1).max(1),
27 }
28 }
29
30 pub(super) fn next_u64(&mut self) -> u64 {
31 self.state = self
32 .state
33 .wrapping_mul(6_364_136_223_846_793_005)
34 .wrapping_add(1_442_695_040_888_963_407);
35 self.state
36 }
37
38 pub(super) fn next_f64(&mut self) -> f64 {
40 (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
41 }
42
43 pub(super) fn next_normal(&mut self) -> f64 {
45 loop {
46 let u1 = self.next_f64();
47 if u1 > 1e-300 {
48 let u2 = self.next_f64();
49 return (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
50 }
51 }
52 }
53
54 pub(super) fn next_cauchy(&mut self) -> f64 {
56 loop {
57 let b = self.next_normal();
58 if b.abs() > 1e-15 {
59 return self.next_normal() / b;
60 }
61 }
62 }
63
64 pub(super) fn next_student_t(&mut self, nu: usize) -> f64 {
67 let z = self.next_normal();
68 let chi2: f64 = (0..nu).map(|_| self.next_normal().powi(2)).sum();
69 let chi = (chi2 / nu as f64).sqrt().max(1e-300);
70 z / chi
71 }
72}
73
74#[non_exhaustive]
78#[derive(Debug, Clone, PartialEq)]
79pub enum RffKernel {
80 Gaussian {
83 length_scale: f64,
85 },
86 Laplacian {
89 length_scale: f64,
91 },
92 Matern32 {
95 length_scale: f64,
97 },
98 Matern52 {
101 length_scale: f64,
103 },
104}
105
106impl RffKernel {
107 pub fn length_scale(&self) -> f64 {
109 match self {
110 RffKernel::Gaussian { length_scale }
111 | RffKernel::Laplacian { length_scale }
112 | RffKernel::Matern32 { length_scale }
113 | RffKernel::Matern52 { length_scale } => *length_scale,
114 }
115 }
116
117 pub(super) fn sample_omega_component(&self, rng: &mut Lcg64) -> f64 {
120 let ls = self.length_scale().max(1e-300);
121 match self {
122 RffKernel::Gaussian { .. } => rng.next_normal() / ls,
123 RffKernel::Laplacian { .. } => rng.next_cauchy() / ls,
124 RffKernel::Matern32 { .. } => rng.next_student_t(3) / ls,
125 RffKernel::Matern52 { .. } => rng.next_student_t(5) / ls,
126 }
127 }
128}
129
130#[derive(Debug, Clone)]
155pub struct FourierFeatureMap {
156 omega: Array2<f64>,
158 bias: Array1<f64>,
160 pub kernel: RffKernel,
162 scale: f64,
164 pub d_in: usize,
166 pub d_out: usize,
168}
169
170impl FourierFeatureMap {
171 pub(super) fn from_parts(
176 kernel: RffKernel,
177 d_in: usize,
178 d_out: usize,
179 omega: Array2<f64>,
180 bias: Array1<f64>,
181 scale: f64,
182 ) -> Self {
183 Self {
184 omega,
185 bias,
186 kernel,
187 scale,
188 d_in,
189 d_out,
190 }
191 }
192
193 pub fn new(kernel: RffKernel, d_in: usize, d_out: usize, seed: u64) -> Self {
204 assert!(d_in > 0, "d_in must be > 0");
205 assert!(d_out > 0, "d_out must be > 0");
206
207 let mut rng = Lcg64::new(seed);
208
209 let omega_data: Vec<f64> = (0..d_out * d_in)
211 .map(|_| kernel.sample_omega_component(&mut rng))
212 .collect();
213 let omega = Array2::from_shape_vec((d_out, d_in), omega_data)
214 .expect("shape is consistent by construction");
215
216 let bias_data: Vec<f64> = (0..d_out)
218 .map(|_| rng.next_f64() * 2.0 * std::f64::consts::PI)
219 .collect();
220 let bias = Array1::from_vec(bias_data);
221
222 let scale = (2.0 / d_out as f64).sqrt();
223
224 Self {
225 omega,
226 bias,
227 kernel,
228 scale,
229 d_in,
230 d_out,
231 }
232 }
233
234 pub fn transform(&self, x: &ArrayView2<f64>) -> Result<Array2<f64>, InterpolateError> {
241 let n = x.nrows();
242 let d = x.ncols();
243 if d != self.d_in {
244 return Err(InterpolateError::DimensionMismatch(format!(
245 "FourierFeatureMap expects {d_in} input dimensions, got {d}",
246 d_in = self.d_in,
247 )));
248 }
249
250 let mut z = Array2::<f64>::zeros((n, self.d_out));
251 for i in 0..n {
252 let xi = x.row(i);
253 for j in 0..self.d_out {
254 let omega_j = self.omega.row(j);
255 let dot: f64 = omega_j.iter().zip(xi.iter()).map(|(w, xv)| w * xv).sum();
256 z[(i, j)] = self.scale * (dot + self.bias[j]).cos();
257 }
258 }
259 Ok(z)
260 }
261
262 pub fn kernel_approx(&self, x1: &[f64], x2: &[f64]) -> Result<f64, InterpolateError> {
267 if x1.len() != self.d_in || x2.len() != self.d_in {
268 return Err(InterpolateError::DimensionMismatch(format!(
269 "kernel_approx expects {d_in} dimensions",
270 d_in = self.d_in,
271 )));
272 }
273 let mut result = 0.0f64;
274 for j in 0..self.d_out {
275 let omega_j = self.omega.row(j);
276 let dot1: f64 = omega_j.iter().zip(x1.iter()).map(|(w, v)| w * v).sum();
277 let dot2: f64 = omega_j.iter().zip(x2.iter()).map(|(w, v)| w * v).sum();
278 result += (dot1 + self.bias[j]).cos() * (dot2 + self.bias[j]).cos();
279 }
280 Ok(2.0 / self.d_out as f64 * result)
281 }
282
283 pub fn omega(&self) -> &Array2<f64> {
285 &self.omega
286 }
287
288 pub fn bias(&self) -> &Array1<f64> {
290 &self.bias
291 }
292}
293
294#[cfg(test)]
297mod tests {
298 use super::*;
299 use scirs2_core::ndarray::Array2;
300
301 #[test]
302 fn test_rff_output_shape() {
303 let map = FourierFeatureMap::new(RffKernel::Gaussian { length_scale: 1.0 }, 3, 64, 1);
304 let x = Array2::<f64>::zeros((5, 3));
305 let z = map.transform(&x.view()).expect("transform");
306 assert_eq!(z.shape(), &[5, 64]);
307 }
308
309 #[test]
310 fn test_gaussian_kernel_approx_close() {
311 let map = FourierFeatureMap::new(RffKernel::Gaussian { length_scale: 1.0 }, 2, 1000, 77);
313 let x1 = [1.0f64, 0.0];
314 let x2 = [0.0f64, 1.0];
315 let true_k = (-1.0f64).exp(); let approx_k = map.kernel_approx(&x1, &x2).expect("approx");
317 let err = (approx_k - true_k).abs();
318 assert!(err < 0.1, "error={err:.4}, expected < 0.1 for D=1000");
319 }
320
321 #[test]
322 fn test_all_kernel_types_output_finite() {
323 let x = Array2::from_shape_fn((4, 2), |(i, j)| (i + j) as f64 * 0.2);
324 for kernel in [
325 RffKernel::Gaussian { length_scale: 1.0 },
326 RffKernel::Laplacian { length_scale: 1.0 },
327 RffKernel::Matern32 { length_scale: 1.0 },
328 RffKernel::Matern52 { length_scale: 1.0 },
329 ] {
330 let map = FourierFeatureMap::new(kernel, 2, 32, 0);
331 let z = map.transform(&x.view()).expect("transform");
332 assert!(
333 z.iter().all(|v| v.is_finite()),
334 "all features must be finite"
335 );
336 }
337 }
338}