Skip to main content

libsvm_rs/
qmatrix.rs

1//! Q matrix implementations for the SMO solver.
2//!
3//! The Q matrix encodes the quadratic form in the SVM dual problem:
4//! `Q[i][j] = y[i] * y[j] * K(x[i], x[j])` for classification,
5//! or just `K(i,j)` for one-class, or a signed/indexed variant for SVR.
6//!
7//! Each implementation wraps a `Kernel` and a `Cache`, providing the
8//! `QMatrix` trait that the solver consumes.
9
10use crate::cache::{Cache, Qfloat};
11use crate::kernel::Kernel;
12use crate::types::{SvmNode, SvmParameter};
13
14/// Trait for Q matrix access used by the SMO solver.
15///
16/// Takes `&mut self` because `get_q` mutates the internal cache.
17/// The solver owns `Box<dyn QMatrix>` and copies row data into its
18/// own buffers to avoid lifetime issues.
19pub trait QMatrix {
20    /// Get column `i` of the Q matrix, with at least `len` elements.
21    fn get_q(&mut self, i: usize, len: usize) -> &[Qfloat];
22
23    /// Get the diagonal of Q: `QD[i] = Q[i][i]`.
24    fn get_qd(&self) -> &[f64];
25
26    /// Swap indices i and j in all internal data structures.
27    fn swap_index(&mut self, i: usize, j: usize);
28}
29
30// ─── SVC_Q ──────────────────────────────────────────────────────────
31
32/// Q matrix for C-SVC and ν-SVC classification.
33///
34/// `Q[i][j] = y[i] * y[j] * K(x[i], x[j])` stored as `Qfloat` (f32).
35pub struct SvcQ<'a> {
36    kernel: Kernel<'a>,
37    cache: Cache,
38    y: Vec<i8>,
39    qd: Vec<f64>,
40}
41
42impl<'a> SvcQ<'a> {
43    /// Build the classification Q matrix used by LIBSVM's `SVC_Q` helper.
44    pub fn new(x: &'a [Vec<SvmNode>], param: &SvmParameter, y: &[i8]) -> Self {
45        let l = x.len();
46        let kernel = Kernel::new(x, param);
47        // `cache_size` is in MB; multiply by 1048576 to convert to bytes.
48        // The float→usize cast saturates on overflow; Cache::new clamps the
49        // resulting byte count to a safe internal limit. check_parameter is
50        // expected to have validated cache_size > 0 before we get here, but
51        // even if bypassed the cast and Cache::new prevent any panic.
52        let cache = Cache::new(l, (param.cache_size * 1048576.0) as usize);
53        let qd: Vec<f64> = (0..l).map(|i| kernel.evaluate(i, i)).collect();
54        let y = y.to_vec();
55        Self {
56            kernel,
57            cache,
58            y,
59            qd,
60        }
61    }
62}
63
64#[allow(clippy::needless_range_loop)]
65impl<'a> QMatrix for SvcQ<'a> {
66    fn get_q(&mut self, i: usize, len: usize) -> &[Qfloat] {
67        let (data, start) = self.cache.get_data(i, len);
68        if start < len {
69            let yi = self.y[i] as f64;
70            for j in start..len {
71                let kval = self.kernel.evaluate(i, j);
72                data[j] = (yi * self.y[j] as f64 * kval) as Qfloat;
73            }
74        }
75        &data[..len]
76    }
77
78    fn get_qd(&self) -> &[f64] {
79        &self.qd
80    }
81
82    fn swap_index(&mut self, i: usize, j: usize) {
83        self.cache.swap_index(i, j);
84        self.kernel.swap_index(i, j);
85        self.y.swap(i, j);
86        self.qd.swap(i, j);
87    }
88}
89
90// ─── ONE_CLASS_Q ────────────────────────────────────────────────────
91
92/// Q matrix for one-class SVM.
93///
94/// `Q[i][j] = K(x[i], x[j])` — no label scaling.
95pub struct OneClassQ<'a> {
96    kernel: Kernel<'a>,
97    cache: Cache,
98    qd: Vec<f64>,
99}
100
101impl<'a> OneClassQ<'a> {
102    /// Build the one-class Q matrix used by LIBSVM's `ONE_CLASS_Q` helper.
103    pub fn new(x: &'a [Vec<SvmNode>], param: &SvmParameter) -> Self {
104        let l = x.len();
105        let kernel = Kernel::new(x, param);
106        // See SvcQ::new for the cache_size cast note (saturating float→usize, Cache clamps).
107        let cache = Cache::new(l, (param.cache_size * 1048576.0) as usize);
108        let qd: Vec<f64> = (0..l).map(|i| kernel.evaluate(i, i)).collect();
109        Self { kernel, cache, qd }
110    }
111}
112
113#[allow(clippy::needless_range_loop)]
114impl<'a> QMatrix for OneClassQ<'a> {
115    fn get_q(&mut self, i: usize, len: usize) -> &[Qfloat] {
116        let (data, start) = self.cache.get_data(i, len);
117        if start < len {
118            for j in start..len {
119                data[j] = self.kernel.evaluate(i, j) as Qfloat;
120            }
121        }
122        &data[..len]
123    }
124
125    fn get_qd(&self) -> &[f64] {
126        &self.qd
127    }
128
129    fn swap_index(&mut self, i: usize, j: usize) {
130        self.cache.swap_index(i, j);
131        self.kernel.swap_index(i, j);
132        self.qd.swap(i, j);
133    }
134}
135
136// ─── SVR_Q ──────────────────────────────────────────────────────────
137
138/// Q matrix for ε-SVR and ν-SVR regression.
139///
140/// The regression dual has 2l variables (α_i^+ and α_i^-).
141/// The underlying kernel cache stores only l rows of actual kernel
142/// evaluations; `get_q` reorders/signs them into a double-buffered output.
143pub struct SvrQ<'a> {
144    kernel: Kernel<'a>,
145    cache: Cache,
146    /// Number of original data points.
147    l: usize,
148    /// Sign of each of the 2l variables: +1 for first l, -1 for second l.
149    sign: Vec<i8>,
150    /// Maps each of the 2l indices to the original data index [0..l).
151    index: Vec<usize>,
152    /// Diagonal of the 2l×2l Q matrix.
153    qd: Vec<f64>,
154    /// Double buffer for returning Q rows (solver may hold two simultaneously).
155    buffer: [Vec<Qfloat>; 2],
156    /// Toggle between the two buffers.
157    next_buffer: usize,
158}
159
160impl<'a> SvrQ<'a> {
161    /// Build the regression Q matrix used by LIBSVM's `SVR_Q` helper.
162    pub fn new(x: &'a [Vec<SvmNode>], param: &SvmParameter) -> Self {
163        let l = x.len();
164        let kernel = Kernel::new(x, param);
165        // See SvcQ::new for the cache_size cast note (saturating float→usize, Cache clamps).
166        let cache = Cache::new(l, (param.cache_size * 1048576.0) as usize);
167
168        let mut sign = vec![0i8; 2 * l];
169        let mut index = vec![0usize; 2 * l];
170        let mut qd = vec![0.0f64; 2 * l];
171
172        for k in 0..l {
173            sign[k] = 1;
174            sign[k + l] = -1;
175            index[k] = k;
176            index[k + l] = k;
177            let kk = kernel.evaluate(k, k);
178            qd[k] = kk;
179            qd[k + l] = kk;
180        }
181
182        let buffer = [vec![0.0 as Qfloat; 2 * l], vec![0.0 as Qfloat; 2 * l]];
183
184        Self {
185            kernel,
186            cache,
187            l,
188            sign,
189            index,
190            qd,
191            buffer,
192            next_buffer: 0,
193        }
194    }
195}
196
197#[allow(clippy::needless_range_loop)]
198impl<'a> QMatrix for SvrQ<'a> {
199    fn get_q(&mut self, i: usize, len: usize) -> &[Qfloat] {
200        let real_i = self.index[i];
201        let l = self.l;
202
203        // Fetch (or fill) the full kernel row for the real data index
204        let (data, start) = self.cache.get_data(real_i, l);
205        if start < l {
206            for j in start..l {
207                data[j] = self.kernel.evaluate(real_i, j) as Qfloat;
208            }
209        }
210
211        // Reorder and apply signs into the output buffer.
212        // Slice buf/sign/index to `len` so the compiler drops the
213        // per-element bounds checks; `data` is an l-length cache row and
214        // `index[j] < l` is a data-dependent lookup that stays checked.
215        let buf_idx = self.next_buffer;
216        self.next_buffer = 1 - self.next_buffer;
217        let si = self.sign[i] as f32;
218        let buf = &mut self.buffer[buf_idx][..len];
219        let sign = &self.sign[..len];
220        let index = &self.index[..len];
221        for j in 0..len {
222            buf[j] = si * (sign[j] as f32) * data[index[j]];
223        }
224        &self.buffer[buf_idx][..len]
225    }
226
227    fn get_qd(&self) -> &[f64] {
228        &self.qd
229    }
230
231    fn swap_index(&mut self, i: usize, j: usize) {
232        self.sign.swap(i, j);
233        self.index.swap(i, j);
234        self.qd.swap(i, j);
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::types::{KernelType, SvmNode, SvmParameter};
242
243    fn make_nodes(pairs: &[(i32, f64)]) -> Vec<SvmNode> {
244        pairs
245            .iter()
246            .map(|&(i, v)| SvmNode { index: i, value: v })
247            .collect()
248    }
249
250    fn default_rbf_param() -> SvmParameter {
251        SvmParameter {
252            kernel_type: KernelType::Rbf,
253            gamma: 0.5,
254            cache_size: 1.0,
255            ..Default::default()
256        }
257    }
258
259    #[test]
260    fn svc_q_diagonal_equals_one_for_rbf() {
261        let data = vec![
262            make_nodes(&[(1, 1.0), (2, 0.0)]),
263            make_nodes(&[(1, 0.0), (2, 1.0)]),
264        ];
265        let y = vec![1i8, -1i8];
266        let param = default_rbf_param();
267        let q = SvcQ::new(&data, &param, &y);
268        // K(x,x) = 1 for RBF, QD[i] = y[i]*y[i]*1 = 1
269        for &d in q.get_qd() {
270            assert!((d - 1.0).abs() < 1e-15);
271        }
272    }
273
274    #[test]
275    #[allow(clippy::needless_range_loop)]
276    fn svc_q_symmetry_and_sign() {
277        let data = vec![
278            make_nodes(&[(1, 1.0)]),
279            make_nodes(&[(1, 2.0)]),
280            make_nodes(&[(1, 3.0)]),
281        ];
282        let y = vec![1i8, -1i8, 1i8];
283        let param = default_rbf_param();
284        let mut q = SvcQ::new(&data, &param, &y);
285        let l = data.len();
286
287        // Collect full matrix
288        let mut matrix = vec![vec![0.0f32; l]; l];
289        for i in 0..l {
290            let row = q.get_q(i, l).to_vec();
291            matrix[i][..l].copy_from_slice(&row[..l]);
292        }
293
294        // Check symmetry
295        for i in 0..l {
296            for j in 0..l {
297                assert!(
298                    (matrix[i][j] - matrix[j][i]).abs() < 1e-6,
299                    "Q[{},{}]={} != Q[{},{}]={}",
300                    i,
301                    j,
302                    matrix[i][j],
303                    j,
304                    i,
305                    matrix[j][i]
306                );
307            }
308        }
309
310        // Check sign: Q[0][1] should be negative (y[0]*y[1] = -1)
311        assert!(matrix[0][1] < 0.0);
312        // Q[0][2] should be positive (y[0]*y[2] = +1)
313        assert!(matrix[0][2] > 0.0);
314    }
315
316    #[test]
317    fn one_class_q_no_sign_scaling() {
318        let data = vec![make_nodes(&[(1, 1.0)]), make_nodes(&[(1, 2.0)])];
319        let param = default_rbf_param();
320        let mut q = OneClassQ::new(&data, &param);
321
322        let row = q.get_q(0, 2);
323        // All values should be positive (kernel values are always positive for RBF)
324        assert!(row[0] > 0.0);
325        assert!(row[1] > 0.0);
326        // Diagonal should be 1.0
327        assert!((row[0] - 1.0).abs() < 1e-6);
328    }
329
330    #[test]
331    fn svr_q_double_buffer() {
332        let data = vec![make_nodes(&[(1, 1.0)]), make_nodes(&[(1, 2.0)])];
333        let param = default_rbf_param();
334        let mut q = SvrQ::new(&data, &param);
335        let l2 = 2 * data.len(); // 4
336
337        // Get two rows — they use different buffers
338        let row0 = q.get_q(0, l2).to_vec();
339        let row1 = q.get_q(1, l2).to_vec();
340
341        // Row 0: sign[0]=+1, index[0]=0 → K(0, index[j]) * sign[0] * sign[j]
342        // Row 1: sign[1]=+1, index[1]=1 → K(1, index[j]) * sign[1] * sign[j]
343        // Both should have non-zero entries
344        assert!(row0.iter().any(|&v| v != 0.0));
345        assert!(row1.iter().any(|&v| v != 0.0));
346
347        // For index 0 (sign +1) and index 2 (sign -1, real_idx 0):
348        // Q[0][2] = sign[0]*sign[2]*K(0,0) = 1*(-1)*1 = -1
349        assert!((row0[2] - (-1.0)).abs() < 1e-6, "Q[0][2] = {}", row0[2]);
350    }
351}