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        let buf_idx = self.next_buffer;
213        self.next_buffer = 1 - self.next_buffer;
214        let si = self.sign[i] as f32;
215        let buf = &mut self.buffer[buf_idx];
216        for j in 0..len {
217            buf[j] = si * (self.sign[j] as f32) * data[self.index[j]];
218        }
219        &self.buffer[buf_idx][..len]
220    }
221
222    fn get_qd(&self) -> &[f64] {
223        &self.qd
224    }
225
226    fn swap_index(&mut self, i: usize, j: usize) {
227        self.sign.swap(i, j);
228        self.index.swap(i, j);
229        self.qd.swap(i, j);
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use crate::types::{KernelType, SvmNode, SvmParameter};
237
238    fn make_nodes(pairs: &[(i32, f64)]) -> Vec<SvmNode> {
239        pairs
240            .iter()
241            .map(|&(i, v)| SvmNode { index: i, value: v })
242            .collect()
243    }
244
245    fn default_rbf_param() -> SvmParameter {
246        SvmParameter {
247            kernel_type: KernelType::Rbf,
248            gamma: 0.5,
249            cache_size: 1.0,
250            ..Default::default()
251        }
252    }
253
254    #[test]
255    fn svc_q_diagonal_equals_one_for_rbf() {
256        let data = vec![
257            make_nodes(&[(1, 1.0), (2, 0.0)]),
258            make_nodes(&[(1, 0.0), (2, 1.0)]),
259        ];
260        let y = vec![1i8, -1i8];
261        let param = default_rbf_param();
262        let q = SvcQ::new(&data, &param, &y);
263        // K(x,x) = 1 for RBF, QD[i] = y[i]*y[i]*1 = 1
264        for &d in q.get_qd() {
265            assert!((d - 1.0).abs() < 1e-15);
266        }
267    }
268
269    #[test]
270    #[allow(clippy::needless_range_loop)]
271    fn svc_q_symmetry_and_sign() {
272        let data = vec![
273            make_nodes(&[(1, 1.0)]),
274            make_nodes(&[(1, 2.0)]),
275            make_nodes(&[(1, 3.0)]),
276        ];
277        let y = vec![1i8, -1i8, 1i8];
278        let param = default_rbf_param();
279        let mut q = SvcQ::new(&data, &param, &y);
280        let l = data.len();
281
282        // Collect full matrix
283        let mut matrix = vec![vec![0.0f32; l]; l];
284        for i in 0..l {
285            let row = q.get_q(i, l).to_vec();
286            matrix[i][..l].copy_from_slice(&row[..l]);
287        }
288
289        // Check symmetry
290        for i in 0..l {
291            for j in 0..l {
292                assert!(
293                    (matrix[i][j] - matrix[j][i]).abs() < 1e-6,
294                    "Q[{},{}]={} != Q[{},{}]={}",
295                    i,
296                    j,
297                    matrix[i][j],
298                    j,
299                    i,
300                    matrix[j][i]
301                );
302            }
303        }
304
305        // Check sign: Q[0][1] should be negative (y[0]*y[1] = -1)
306        assert!(matrix[0][1] < 0.0);
307        // Q[0][2] should be positive (y[0]*y[2] = +1)
308        assert!(matrix[0][2] > 0.0);
309    }
310
311    #[test]
312    fn one_class_q_no_sign_scaling() {
313        let data = vec![make_nodes(&[(1, 1.0)]), make_nodes(&[(1, 2.0)])];
314        let param = default_rbf_param();
315        let mut q = OneClassQ::new(&data, &param);
316
317        let row = q.get_q(0, 2);
318        // All values should be positive (kernel values are always positive for RBF)
319        assert!(row[0] > 0.0);
320        assert!(row[1] > 0.0);
321        // Diagonal should be 1.0
322        assert!((row[0] - 1.0).abs() < 1e-6);
323    }
324
325    #[test]
326    fn svr_q_double_buffer() {
327        let data = vec![make_nodes(&[(1, 1.0)]), make_nodes(&[(1, 2.0)])];
328        let param = default_rbf_param();
329        let mut q = SvrQ::new(&data, &param);
330        let l2 = 2 * data.len(); // 4
331
332        // Get two rows — they use different buffers
333        let row0 = q.get_q(0, l2).to_vec();
334        let row1 = q.get_q(1, l2).to_vec();
335
336        // Row 0: sign[0]=+1, index[0]=0 → K(0, index[j]) * sign[0] * sign[j]
337        // Row 1: sign[1]=+1, index[1]=1 → K(1, index[j]) * sign[1] * sign[j]
338        // Both should have non-zero entries
339        assert!(row0.iter().any(|&v| v != 0.0));
340        assert!(row1.iter().any(|&v| v != 0.0));
341
342        // For index 0 (sign +1) and index 2 (sign -1, real_idx 0):
343        // Q[0][2] = sign[0]*sign[2]*K(0,0) = 1*(-1)*1 = -1
344        assert!((row0[2] - (-1.0)).abs() < 1e-6, "Q[0][2] = {}", row0[2]);
345    }
346}