Skip to main content

wai_quantum/
quantum_kernel.rs

1//! Phasor-feature classifier — the quantum kernel, deployed (`wai.quantum.kernel`).
2//!
3//! A quantum kernel `K(x,x') = |⟨ψ(x)|ψ(x')⟩|²` is, by the Schuld–Sweke–Meyer
4//! identity, a shift-invariant kernel whose Fourier spectrum is set by the data
5//! encoding — and **random Fourier features** are its classical realization: draw
6//! random frequencies `ω`, map `x ↦ φ(x) = √(2/D)·cos(ω·x + b)` (a bank of phasors,
7//! the FHRR feature map), and `⟨φ(x),φ(x')⟩ ≈ K(x,x')` (Rahimi–Recht). The
8//! dequantization theorems say this matches the quantum kernel whenever the
9//! spectrum is compact. So the "quantum kernel" ships as an ordinary phasor layer:
10//! train a linear model on the phasor features and it separates data a linear model
11//! cannot — the supervised counterpart to the generative Born machine, deployable
12//! on a laptop, and at **qFHRR** phase resolution for the edge.
13//!
14//! Every phasor evaluation is a counted operation (`feature_ops`) — the hook for
15//! pricing interference computing in joules. Deterministic f64.
16
17// Indexed loops are deliberate in the ridge-fit / linear-solve matrix code.
18#![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}
33/// Standard normal via Box–Muller (deterministic f64).
34fn 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
40/// A random-Fourier-feature (phasor) map approximating a Gaussian kernel
41/// `exp(-γ·|x−x'|²)`.
42pub struct Rff {
43    /// `omegas[i]` is a `dim`-vector frequency; `phases[i]` a bias in `[0,2π)`.
44    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
55/// Sample a phasor feature map: `d_features` frequencies `ω ~ N(0, 2γ·I)` and
56/// uniform phase biases. Deterministic in `seed`.
57pub 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
67/// The phasor feature vector `φ(x)`.
68pub 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
79/// `φ(x)` with each phasor phase snapped to `bits`-bit qFHRR resolution.
80pub 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
94/// Solve `A w = b` for a symmetric positive-definite `A` (Gaussian elimination
95/// with partial pivoting). `a` is consumed.
96fn solve(mut a: Vec<Vec<f64>>, mut b: Vec<f64>) -> Vec<f64> {
97    let n = b.len();
98    for col in 0..n {
99        // partial pivot
100        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
133/// Fit a ridge classifier on phasor features: `w = (ΦᵀΦ + λI)⁻¹ Φᵀ y`. Labels `y`
134/// in `{−1,+1}`; predict `sign(w·φ(x))`.
135pub 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
157/// The raw decision score `w·φ(x)`.
158pub fn score(rff: &Rff, w: &[f64], x: &[f64]) -> f64 {
159    features(rff, x).iter().zip(w).map(|(f, wi)| f * wi).sum()
160}
161
162/// Classification accuracy on `(x, y)`.
163pub 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
173/// Accuracy using `bits`-bit qFHRR features (the edge-deployable path).
174pub 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
185/// Phasor operations to featurize `n_samples`: `D · (dim + 1)` per sample — the
186/// count a joule meter prices.
187pub 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
192// ---------------------------------------------------------------------------
193// Deterministic 2-D benchmark datasets (nonlinearly separable)
194// ---------------------------------------------------------------------------
195
196/// Concentric circles: inner disc (−1) vs outer ring (+1). Not linearly separable.
197pub 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
212/// XOR clusters at `(±1,±1)`, label `sign(x1·x2)`. Not linearly separable.
213pub 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        // a degenerate RFF with a single low-frequency feature ≈ linear — should fail
252        // on a problem the full phasor bank solves, showing the features are the point.
253        let (xtr, ytr) = circles(300, 1);
254        let (xte, yte) = circles(300, 2);
255        // near-linear: 1 feature, tiny gamma
256        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}