1#![allow(clippy::needless_range_loop)]
19
20use std::f64::consts::PI;
21
22fn splitmix64(s: &mut u64) -> u64 {
23 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
24 let mut z = *s;
25 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
26 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
27 z ^ (z >> 31)
28}
29#[inline]
30fn u01(s: &mut u64) -> f64 {
31 (splitmix64(s) >> 11) as f64 / (1u64 << 53) as f64
32}
33fn gauss(s: &mut u64) -> f64 {
35 let u1 = u01(s).max(1e-300);
36 let u2 = u01(s);
37 (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos()
38}
39
40pub struct Rff {
43 omegas: Vec<Vec<f64>>,
45 phases: Vec<f64>,
46 scale: f64,
47}
48
49impl Rff {
50 pub fn dim_features(&self) -> usize {
51 self.phases.len()
52 }
53}
54
55pub fn sample_rff(dim: usize, d_features: usize, gamma: f64, seed: u64) -> Rff {
58 let mut st = seed.wrapping_mul(0xA24B_AED4).wrapping_add(1);
59 let sd = (2.0 * gamma).sqrt();
60 let omegas = (0..d_features)
61 .map(|_| (0..dim).map(|_| gauss(&mut st) * sd).collect())
62 .collect();
63 let phases = (0..d_features).map(|_| u01(&mut st) * 2.0 * PI).collect();
64 Rff { omegas, phases, scale: (2.0 / d_features as f64).sqrt() }
65}
66
67pub fn features(rff: &Rff, x: &[f64]) -> Vec<f64> {
69 rff.omegas
70 .iter()
71 .zip(&rff.phases)
72 .map(|(w, &b)| {
73 let dot: f64 = w.iter().zip(x).map(|(wi, xi)| wi * xi).sum();
74 rff.scale * (dot + b).cos()
75 })
76 .collect()
77}
78
79pub fn features_quantized(rff: &Rff, x: &[f64], bits: u32) -> Vec<f64> {
81 let levels = (1u64 << bits) as f64;
82 rff.omegas
83 .iter()
84 .zip(&rff.phases)
85 .map(|(w, &b)| {
86 let dot: f64 = w.iter().zip(x).map(|(wi, xi)| wi * xi).sum();
87 let t = dot + b;
88 let tq = (t / (2.0 * PI) * levels).round() / levels * 2.0 * PI;
89 rff.scale * tq.cos()
90 })
91 .collect()
92}
93
94fn solve(mut a: Vec<Vec<f64>>, mut b: Vec<f64>) -> Vec<f64> {
97 let n = b.len();
98 for col in 0..n {
99 let mut piv = col;
101 for r in (col + 1)..n {
102 if a[r][col].abs() > a[piv][col].abs() {
103 piv = r;
104 }
105 }
106 a.swap(col, piv);
107 b.swap(col, piv);
108 let d = a[col][col];
109 if d.abs() < 1e-15 {
110 continue;
111 }
112 for r in (col + 1)..n {
113 let f = a[r][col] / d;
114 if f != 0.0 {
115 for c in col..n {
116 a[r][c] -= f * a[col][c];
117 }
118 b[r] -= f * b[col];
119 }
120 }
121 }
122 let mut w = vec![0.0; n];
123 for i in (0..n).rev() {
124 let mut s = b[i];
125 for c in (i + 1)..n {
126 s -= a[i][c] * w[c];
127 }
128 w[i] = if a[i][i].abs() < 1e-15 { 0.0 } else { s / a[i][i] };
129 }
130 w
131}
132
133pub fn fit_ridge(rff: &Rff, x: &[Vec<f64>], y: &[f64], lambda: f64) -> Vec<f64> {
136 let d = rff.dim_features();
137 let mut a = vec![vec![0.0; d]; d];
138 let mut b = vec![0.0; d];
139 for (xi, &yi) in x.iter().zip(y) {
140 let phi = features(rff, xi);
141 for i in 0..d {
142 b[i] += phi[i] * yi;
143 for j in i..d {
144 a[i][j] += phi[i] * phi[j];
145 }
146 }
147 }
148 for i in 0..d {
149 for j in 0..i {
150 a[i][j] = a[j][i];
151 }
152 a[i][i] += lambda;
153 }
154 solve(a, b)
155}
156
157pub fn score(rff: &Rff, w: &[f64], x: &[f64]) -> f64 {
159 features(rff, x).iter().zip(w).map(|(f, wi)| f * wi).sum()
160}
161
162pub fn accuracy(rff: &Rff, w: &[f64], x: &[Vec<f64>], y: &[f64]) -> f64 {
164 let mut correct = 0usize;
165 for (xi, yi) in x.iter().zip(y) {
166 if score(rff, w, xi).signum() == yi.signum() {
167 correct += 1;
168 }
169 }
170 correct as f64 / x.len() as f64
171}
172
173pub fn accuracy_quantized(rff: &Rff, w: &[f64], x: &[Vec<f64>], y: &[f64], bits: u32) -> f64 {
175 let mut correct = 0usize;
176 for (xi, yi) in x.iter().zip(y) {
177 let s: f64 = features_quantized(rff, xi, bits).iter().zip(w).map(|(f, wi)| f * wi).sum();
178 if s.signum() == yi.signum() {
179 correct += 1;
180 }
181 }
182 correct as f64 / x.len() as f64
183}
184
185pub fn feature_ops(rff: &Rff, n_samples: usize) -> u64 {
188 let dim = rff.omegas.first().map(|w| w.len()).unwrap_or(0);
189 (rff.dim_features() as u64) * (dim as u64 + 1) * n_samples as u64
190}
191
192pub fn circles(n: usize, seed: u64) -> (Vec<Vec<f64>>, Vec<f64>) {
198 let mut st = seed.wrapping_mul(0x2545_F491).wrapping_add(1);
199 let mut x = Vec::with_capacity(n);
200 let mut y = Vec::with_capacity(n);
201 for i in 0..n {
202 let inner = i % 2 == 0;
203 let ang = u01(&mut st) * 2.0 * PI;
204 let r = if inner { 0.35 * u01(&mut st) } else { 0.9 + 0.35 * u01(&mut st) };
205 x.push(vec![r * ang.cos() + 0.03 * gauss(&mut st), r * ang.sin() + 0.03 * gauss(&mut st)]);
206 y.push(if inner { -1.0 } else { 1.0 });
207 }
208 (x, y)
209}
210
211pub fn xor_data(n: usize, seed: u64) -> (Vec<Vec<f64>>, Vec<f64>) {
213 let mut st = seed.wrapping_mul(0x9E37_79B9).wrapping_add(3);
214 let mut x = Vec::with_capacity(n);
215 let mut y = Vec::with_capacity(n);
216 for i in 0..n {
217 let sx = if (i & 1) == 0 { 1.0 } else { -1.0 };
218 let sy = if (i & 2) == 0 { 1.0 } else { -1.0 };
219 x.push(vec![sx + 0.28 * gauss(&mut st), sy + 0.28 * gauss(&mut st)]);
220 y.push(sx * sy);
221 }
222 (x, y)
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn phasor_kernel_separates_circles() {
231 let (xtr, ytr) = circles(300, 1);
232 let (xte, yte) = circles(300, 2);
233 let rff = sample_rff(2, 200, 4.0, 7);
234 let w = fit_ridge(&rff, &xtr, &ytr, 1e-3);
235 let acc = accuracy(&rff, &w, &xte, &yte);
236 assert!(acc > 0.9, "phasor kernel must separate circles: {acc}");
237 }
238
239 #[test]
240 fn phasor_kernel_separates_xor() {
241 let (xtr, ytr) = xor_data(320, 5);
242 let (xte, yte) = xor_data(320, 9);
243 let rff = sample_rff(2, 160, 1.5, 3);
244 let w = fit_ridge(&rff, &xtr, &ytr, 1e-3);
245 assert!(accuracy(&rff, &w, &xte, &yte) > 0.9, "phasor kernel must solve XOR");
246 }
247
248 #[test]
249 fn a_linear_model_fails_on_circles() {
250 let (xtr, ytr) = circles(300, 1);
253 let (xte, yte) = circles(300, 2);
254 let rff = sample_rff(2, 2, 1e-4, 7);
256 let w = fit_ridge(&rff, &xtr, &ytr, 1e-3);
257 assert!(accuracy(&rff, &w, &xte, &yte) < 0.7, "a linear model can't separate circles");
258 }
259
260 #[test]
261 fn qfhrr_quantized_features_still_classify() {
262 let (xtr, ytr) = circles(300, 1);
263 let (xte, yte) = circles(300, 2);
264 let rff = sample_rff(2, 200, 4.0, 7);
265 let w = fit_ridge(&rff, &xtr, &ytr, 1e-3);
266 let full = accuracy(&rff, &w, &xte, &yte);
267 let q4 = accuracy_quantized(&rff, &w, &xte, &yte, 4);
268 assert!(q4 > 0.85, "4-bit qFHRR features still classify: {q4} (full {full})");
269 assert!(accuracy_quantized(&rff, &w, &xte, &yte, 8) >= q4 - 0.02, "more bits don't hurt");
270 }
271
272 #[test]
273 fn deterministic() {
274 let (x, y) = circles(50, 1);
275 let rff = sample_rff(2, 32, 3.0, 7);
276 let a = fit_ridge(&rff, &x, &y, 1e-3);
277 let b = fit_ridge(&rff, &x, &y, 1e-3);
278 assert!(a.iter().zip(&b).all(|(p, q)| p.to_bits() == q.to_bits()));
279 }
280}