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 * crate::repro::ln(u1)).sqrt() * crate::repro::sin_cos(2.0 * PI * u2).1
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 * crate::repro::sin_cos(dot + b).1
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 * crate::repro::sin_cos(tq).1
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 let (sin, cos) = crate::repro::sin_cos(ang);
206 x.push(vec![r * cos + 0.03 * gauss(&mut st), r * sin + 0.03 * gauss(&mut st)]);
207 y.push(if inner { -1.0 } else { 1.0 });
208 }
209 (x, y)
210}
211
212pub fn xor_data(n: usize, seed: u64) -> (Vec<Vec<f64>>, Vec<f64>) {
214 let mut st = seed.wrapping_mul(0x9E37_79B9).wrapping_add(3);
215 let mut x = Vec::with_capacity(n);
216 let mut y = Vec::with_capacity(n);
217 for i in 0..n {
218 let sx = if (i & 1) == 0 { 1.0 } else { -1.0 };
219 let sy = if (i & 2) == 0 { 1.0 } else { -1.0 };
220 x.push(vec![sx + 0.28 * gauss(&mut st), sy + 0.28 * gauss(&mut st)]);
221 y.push(sx * sy);
222 }
223 (x, y)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn phasor_kernel_separates_circles() {
232 let (xtr, ytr) = circles(300, 1);
233 let (xte, yte) = circles(300, 2);
234 let rff = sample_rff(2, 200, 4.0, 7);
235 let w = fit_ridge(&rff, &xtr, &ytr, 1e-3);
236 let acc = accuracy(&rff, &w, &xte, &yte);
237 assert!(acc > 0.9, "phasor kernel must separate circles: {acc}");
238 }
239
240 #[test]
241 fn phasor_kernel_separates_xor() {
242 let (xtr, ytr) = xor_data(320, 5);
243 let (xte, yte) = xor_data(320, 9);
244 let rff = sample_rff(2, 160, 1.5, 3);
245 let w = fit_ridge(&rff, &xtr, &ytr, 1e-3);
246 assert!(accuracy(&rff, &w, &xte, &yte) > 0.9, "phasor kernel must solve XOR");
247 }
248
249 #[test]
250 fn a_linear_model_fails_on_circles() {
251 let (xtr, ytr) = circles(300, 1);
254 let (xte, yte) = circles(300, 2);
255 let rff = sample_rff(2, 2, 1e-4, 7);
257 let w = fit_ridge(&rff, &xtr, &ytr, 1e-3);
258 assert!(accuracy(&rff, &w, &xte, &yte) < 0.7, "a linear model can't separate circles");
259 }
260
261 #[test]
262 fn qfhrr_quantized_features_still_classify() {
263 let (xtr, ytr) = circles(300, 1);
264 let (xte, yte) = circles(300, 2);
265 let rff = sample_rff(2, 200, 4.0, 7);
266 let w = fit_ridge(&rff, &xtr, &ytr, 1e-3);
267 let full = accuracy(&rff, &w, &xte, &yte);
268 let q4 = accuracy_quantized(&rff, &w, &xte, &yte, 4);
269 assert!(q4 > 0.85, "4-bit qFHRR features still classify: {q4} (full {full})");
270 assert!(accuracy_quantized(&rff, &w, &xte, &yte, 8) >= q4 - 0.02, "more bits don't hurt");
271 }
272
273 #[test]
274 fn deterministic() {
275 let (x, y) = circles(50, 1);
276 let rff = sample_rff(2, 32, 3.0, 7);
277 let a = fit_ridge(&rff, &x, &y, 1e-3);
278 let b = fit_ridge(&rff, &x, &y, 1e-3);
279 assert!(a.iter().zip(&b).all(|(p, q)| p.to_bits() == q.to_bits()));
280 }
281}