Skip to main content

sklears_svm/
kernels.rs

1//! Kernel functions for Support Vector Machines
2//!
3//! This module provides comprehensive kernel implementations for SVM including basic kernels,
4//! composite kernels, graph kernels, and advanced kernel methods.
5
6use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
7use sklears_core::error::{Result, SklearsError};
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// Kernel trait for all kernel functions
12pub trait Kernel: Send + Sync + std::fmt::Debug {
13    /// Compute kernel value between two vectors
14    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64;
15
16    /// Compute kernel matrix for two datasets
17    fn compute_matrix(&self, x: &Array2<f64>, y: &Array2<f64>) -> Array2<f64> {
18        let (n_x, _) = x.dim();
19        let (n_y, _) = y.dim();
20        let mut kernel_matrix = Array2::zeros((n_x, n_y));
21
22        for i in 0..n_x {
23            for j in 0..n_y {
24                kernel_matrix[[i, j]] = self.compute(x.row(i), y.row(j));
25            }
26        }
27
28        kernel_matrix
29    }
30
31    /// Get kernel parameters for serialization
32    fn parameters(&self) -> HashMap<String, f64>;
33}
34
35/// Main kernel type enumeration
36#[derive(Debug, Clone, PartialEq)]
37pub enum KernelType {
38    /// Linear kernel: K(x,y) = x^T y
39    Linear,
40    /// RBF/Gaussian kernel: K(x,y) = exp(-γ||x-y||²)
41    Rbf { gamma: f64 },
42    /// Polynomial kernel: K(x,y) = (γ x^T y + r)^d
43    Polynomial { gamma: f64, coef0: f64, degree: f64 },
44    /// Sigmoid kernel: K(x,y) = tanh(γ x^T y + r)
45    Sigmoid { gamma: f64, coef0: f64 },
46    /// Precomputed kernel matrix
47    Precomputed,
48    /// Custom user-defined kernel
49    Custom(String),
50    /// Cosine similarity kernel
51    Cosine,
52    /// Chi-squared kernel
53    ChiSquared { gamma: f64 },
54    /// Histogram intersection kernel
55    Intersection,
56    /// Hellinger kernel
57    Hellinger,
58    /// Jensen-Shannon kernel
59    JensenShannon,
60    /// Periodic kernel
61    Periodic { length_scale: f64, period: f64 },
62}
63
64/// Create a kernel instance from a KernelType
65pub fn create_kernel(kernel_type: KernelType) -> Result<Box<dyn Kernel>> {
66    match kernel_type {
67        KernelType::Linear => Ok(Box::new(LinearKernel)),
68        KernelType::Rbf { gamma } => Ok(Box::new(RbfKernel { gamma })),
69        KernelType::Polynomial {
70            gamma,
71            coef0,
72            degree,
73        } => Ok(Box::new(PolynomialKernel {
74            gamma,
75            coef0,
76            degree,
77        })),
78        KernelType::Sigmoid { gamma, coef0 } => Ok(Box::new(SigmoidKernel { gamma, coef0 })),
79        KernelType::Cosine => Ok(Box::new(CosineKernel)),
80        KernelType::ChiSquared { gamma } => Ok(Box::new(ChiSquaredKernel { gamma })),
81        KernelType::Intersection => Ok(Box::new(IntersectionKernel)),
82        KernelType::Hellinger => Ok(Box::new(HellingerKernel)),
83        KernelType::JensenShannon => Ok(Box::new(JensenShannonKernel)),
84        KernelType::Periodic {
85            length_scale,
86            period,
87        } => Ok(Box::new(PeriodicKernel {
88            length_scale,
89            period,
90        })),
91        KernelType::Precomputed => Err(SklearsError::InvalidParameter {
92            name: "kernel_type".to_string(),
93            reason: "precomputed kernels must be created with data".to_string(),
94        }),
95        KernelType::Custom(name) => Err(SklearsError::InvalidParameter {
96            name: "kernel_type".to_string(),
97            reason: format!("custom kernel '{}' not implemented", name),
98        }),
99    }
100}
101
102impl<K: Kernel> Kernel for Arc<K> {
103    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
104        (**self).compute(x, y)
105    }
106
107    fn compute_matrix(&self, x: &Array2<f64>, y: &Array2<f64>) -> Array2<f64> {
108        (**self).compute_matrix(x, y)
109    }
110
111    fn parameters(&self) -> HashMap<String, f64> {
112        (**self).parameters()
113    }
114}
115
116/// Linear kernel implementation
117#[derive(Debug, Clone)]
118pub struct LinearKernel;
119
120impl Default for LinearKernel {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126impl LinearKernel {
127    pub fn new() -> Self {
128        Self
129    }
130}
131
132impl Kernel for LinearKernel {
133    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
134        x.dot(&y)
135    }
136
137    fn parameters(&self) -> HashMap<String, f64> {
138        HashMap::new()
139    }
140}
141
142/// RBF (Gaussian) kernel implementation
143#[derive(Debug, Clone)]
144pub struct RbfKernel {
145    pub gamma: f64,
146}
147
148impl RbfKernel {
149    pub fn new(gamma: f64) -> Self {
150        Self { gamma }
151    }
152}
153
154impl Kernel for RbfKernel {
155    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
156        // Compute squared distance inline without allocating temporary arrays
157        let squared_distance: f64 = x
158            .iter()
159            .zip(y.iter())
160            .map(|(xi, yi)| {
161                let diff = xi - yi;
162                diff * diff
163            })
164            .sum();
165
166        (-self.gamma * squared_distance).exp()
167    }
168
169    fn parameters(&self) -> HashMap<String, f64> {
170        let mut params = HashMap::new();
171        params.insert("gamma".to_string(), self.gamma);
172        params
173    }
174}
175
176/// Polynomial kernel implementation
177#[derive(Debug, Clone)]
178pub struct PolynomialKernel {
179    pub gamma: f64,
180    pub coef0: f64,
181    pub degree: f64,
182}
183
184impl PolynomialKernel {
185    pub fn new(gamma: f64, coef0: f64, degree: f64) -> Self {
186        Self {
187            gamma,
188            coef0,
189            degree,
190        }
191    }
192}
193
194impl Kernel for PolynomialKernel {
195    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
196        let dot_product = x.dot(&y);
197        (self.gamma * dot_product + self.coef0).powf(self.degree)
198    }
199
200    fn parameters(&self) -> HashMap<String, f64> {
201        let mut params = HashMap::new();
202        params.insert("gamma".to_string(), self.gamma);
203        params.insert("coef0".to_string(), self.coef0);
204        params.insert("degree".to_string(), self.degree);
205        params
206    }
207}
208
209/// Sigmoid kernel implementation
210#[derive(Debug, Clone)]
211pub struct SigmoidKernel {
212    pub gamma: f64,
213    pub coef0: f64,
214}
215
216impl SigmoidKernel {
217    pub fn new(gamma: f64, coef0: f64) -> Self {
218        Self { gamma, coef0 }
219    }
220}
221
222impl Kernel for SigmoidKernel {
223    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
224        let dot_product = x.dot(&y);
225        (self.gamma * dot_product + self.coef0).tanh()
226    }
227
228    fn parameters(&self) -> HashMap<String, f64> {
229        let mut params = HashMap::new();
230        params.insert("gamma".to_string(), self.gamma);
231        params.insert("coef0".to_string(), self.coef0);
232        params
233    }
234}
235
236/// Cosine similarity kernel implementation
237#[derive(Debug, Clone)]
238pub struct CosineKernel;
239
240impl Kernel for CosineKernel {
241    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
242        let dot_product = x.dot(&y);
243        let x_norm = x.dot(&x).sqrt();
244        let y_norm = y.dot(&y).sqrt();
245
246        if x_norm == 0.0 || y_norm == 0.0 {
247            0.0
248        } else {
249            dot_product / (x_norm * y_norm)
250        }
251    }
252
253    fn parameters(&self) -> HashMap<String, f64> {
254        HashMap::new()
255    }
256}
257
258/// Chi-squared kernel implementation
259#[derive(Debug, Clone)]
260pub struct ChiSquaredKernel {
261    pub gamma: f64,
262}
263
264impl ChiSquaredKernel {
265    pub fn new(gamma: f64) -> Self {
266        Self { gamma }
267    }
268}
269
270impl Kernel for ChiSquaredKernel {
271    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
272        let chi_squared_distance = x
273            .iter()
274            .zip(y.iter())
275            .map(|(a, b)| {
276                if a + b > 0.0 {
277                    (a - b).powi(2) / (a + b)
278                } else {
279                    0.0
280                }
281            })
282            .sum::<f64>();
283
284        (-self.gamma * chi_squared_distance).exp()
285    }
286
287    fn parameters(&self) -> HashMap<String, f64> {
288        let mut params = HashMap::new();
289        params.insert("gamma".to_string(), self.gamma);
290        params
291    }
292}
293
294/// Histogram intersection kernel implementation
295#[derive(Debug, Clone)]
296pub struct IntersectionKernel;
297
298impl Kernel for IntersectionKernel {
299    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
300        x.iter().zip(y.iter()).map(|(a, b)| a.min(*b)).sum()
301    }
302
303    fn parameters(&self) -> HashMap<String, f64> {
304        HashMap::new()
305    }
306}
307
308/// Periodic kernel implementation
309#[derive(Debug, Clone)]
310pub struct PeriodicKernel {
311    pub length_scale: f64,
312    pub period: f64,
313}
314
315impl PeriodicKernel {
316    pub fn new(length_scale: f64, period: f64) -> Self {
317        Self {
318            length_scale,
319            period,
320        }
321    }
322}
323
324impl Kernel for PeriodicKernel {
325    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
326        // Compute inline without allocating temporary arrays
327        let sin_squared: f64 = x
328            .iter()
329            .zip(y.iter())
330            .map(|(xi, yi)| {
331                let diff = xi - yi;
332                let sin_val = (std::f64::consts::PI * diff / self.period).sin();
333                sin_val * sin_val
334            })
335            .sum();
336
337        (-2.0 * sin_squared / (self.length_scale * self.length_scale)).exp()
338    }
339
340    fn parameters(&self) -> HashMap<String, f64> {
341        let mut params = HashMap::new();
342        params.insert("length_scale".to_string(), self.length_scale);
343        params.insert("period".to_string(), self.period);
344        params
345    }
346}
347
348/// Custom kernel implementation
349#[derive(Debug, Clone)]
350pub struct CustomKernel {
351    pub name: String,
352    pub function: fn(ArrayView1<f64>, ArrayView1<f64>) -> f64,
353}
354
355impl CustomKernel {
356    pub fn new(name: String, function: fn(ArrayView1<f64>, ArrayView1<f64>) -> f64) -> Self {
357        Self { name, function }
358    }
359}
360
361impl Kernel for CustomKernel {
362    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
363        (self.function)(x, y)
364    }
365
366    fn parameters(&self) -> HashMap<String, f64> {
367        HashMap::new()
368    }
369}
370
371/// Main kernel function wrapper
372#[derive(Debug, Clone)]
373pub struct KernelFunction {
374    kernel_type: KernelType,
375}
376
377impl KernelFunction {
378    pub fn new(kernel_type: KernelType) -> Self {
379        Self { kernel_type }
380    }
381
382    pub fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
383        match &self.kernel_type {
384            KernelType::Linear => LinearKernel.compute(x, y),
385            KernelType::Rbf { gamma } => RbfKernel::new(*gamma).compute(x, y),
386            KernelType::Polynomial {
387                gamma,
388                coef0,
389                degree,
390            } => PolynomialKernel::new(*gamma, *coef0, *degree).compute(x, y),
391            KernelType::Sigmoid { gamma, coef0 } => {
392                SigmoidKernel::new(*gamma, *coef0).compute(x, y)
393            }
394            KernelType::Cosine => CosineKernel.compute(x, y),
395            KernelType::ChiSquared { gamma } => ChiSquaredKernel::new(*gamma).compute(x, y),
396            KernelType::Intersection => IntersectionKernel.compute(x, y),
397            KernelType::Periodic {
398                length_scale,
399                period,
400            } => PeriodicKernel::new(*length_scale, *period).compute(x, y),
401            KernelType::Precomputed => {
402                // A precomputed kernel has no closed-form value for two arbitrary
403                // feature vectors: the kernel matrix must be supplied directly and
404                // indexed by sample position. The pointwise `compute` API cannot
405                // express this, so refuse rather than fabricate a 0.0 entry.
406                // `create_kernel` rejects `Precomputed` up front, so reaching here
407                // indicates misuse of the pointwise API.
408                panic!(
409                    "precomputed kernel cannot be evaluated pointwise; supply the \
410                     kernel matrix and index it by sample position instead"
411                )
412            }
413            KernelType::Custom(_name) => {
414                // Default custom implementation
415                x.dot(&y)
416            }
417            KernelType::Hellinger => {
418                // Hellinger kernel (Bhattacharyya coefficient)
419                let x_normalized = normalize_vector(&x.to_owned());
420                let y_normalized = normalize_vector(&y.to_owned());
421                x_normalized
422                    .iter()
423                    .zip(y_normalized.iter())
424                    .map(|(a, b)| (a * b).sqrt())
425                    .sum::<f64>()
426                    .sqrt()
427            }
428            KernelType::JensenShannon => {
429                // Jensen-Shannon kernel
430                let x_normalized = normalize_vector(&x.to_owned());
431                let y_normalized = normalize_vector(&y.to_owned());
432
433                let mut js_divergence = 0.0;
434                for i in 0..x_normalized.len() {
435                    let p = x_normalized[i];
436                    let q = y_normalized[i];
437                    let m = (p + q) / 2.0;
438
439                    if p > 0.0 && m > 0.0 {
440                        js_divergence += p * (p / m).ln();
441                    }
442                    if q > 0.0 && m > 0.0 {
443                        js_divergence += q * (q / m).ln();
444                    }
445                }
446                js_divergence /= 2.0;
447
448                (-js_divergence).exp()
449            }
450        }
451    }
452
453    pub fn compute_matrix(&self, x: &Array2<f64>, y: &Array2<f64>) -> Array2<f64> {
454        let (n_x, _) = x.dim();
455        let (n_y, _) = y.dim();
456        let mut kernel_matrix = Array2::zeros((n_x, n_y));
457
458        for i in 0..n_x {
459            for j in 0..n_y {
460                kernel_matrix[[i, j]] = self.compute(x.row(i), y.row(j));
461            }
462        }
463
464        kernel_matrix
465    }
466
467    pub fn kernel_type(&self) -> &KernelType {
468        &self.kernel_type
469    }
470}
471
472/// Utility function to normalize a vector
473fn normalize_vector(vec: &Array1<f64>) -> Array1<f64> {
474    let sum: f64 = vec.iter().sum();
475    if sum == 0.0 {
476        vec.clone()
477    } else {
478        vec / sum
479    }
480}
481
482/// Graph structure for graph kernels
483#[derive(Debug, Clone)]
484pub struct Graph {
485    pub adjacency_matrix: Array2<f64>,
486    pub node_labels: Option<Array1<usize>>,
487    pub edge_labels: Option<Array2<usize>>,
488}
489
490impl Graph {
491    pub fn new(adjacency_matrix: Array2<f64>) -> Self {
492        Self {
493            adjacency_matrix,
494            node_labels: None,
495            edge_labels: None,
496        }
497    }
498
499    pub fn with_node_labels(mut self, labels: Array1<usize>) -> Self {
500        self.node_labels = Some(labels);
501        self
502    }
503
504    pub fn with_edge_labels(mut self, labels: Array2<usize>) -> Self {
505        self.edge_labels = Some(labels);
506        self
507    }
508}
509
510/// Random Walk kernel for graphs
511#[derive(Debug, Clone)]
512pub struct RandomWalkKernel {
513    pub lambda: f64, // decay parameter
514    pub max_steps: usize,
515}
516
517impl RandomWalkKernel {
518    pub fn new(lambda: f64, max_steps: usize) -> Self {
519        Self { lambda, max_steps }
520    }
521
522    /// Compute the random-walk graph kernel (Gärtner / Kashima et al.).
523    ///
524    /// The geometric random-walk kernel between graphs `G1` and `G2` is
525    /// ```text
526    /// K(G1, G2) = Σ_{i,j} [ (I - λ A_×)^{-1} ]_{ij}
527    /// ```
528    /// where `A_×` is the adjacency matrix of the direct (tensor) product graph
529    /// `G1 × G2`, whose nodes are pairs `(u, v)` with `u ∈ G1`, `v ∈ G2`, and
530    /// `A_×[(u,v),(u',v')] = A1[u,u'] · A2[v,v']`. The closed form sums the
531    /// geometric series `Σ_t λ^t A_×^t = (I - λ A_×)^{-1}` over all start/end
532    /// pairs (uniform start/stop probabilities).
533    ///
534    /// We form `A_×` of size `(n1·n2) × (n1·n2)`, then solve the linear system
535    /// `(I - λ A_×) s = 1` for `s` and return `Σ_i s_i`. The kernel is
536    /// well-defined when `λ < 1 / ρ(A_×)`; if the system is singular or the
537    /// product graph is empty we fall back to `0.0`.
538    pub fn compute_graph_kernel(&self, g1: &Graph, g2: &Graph) -> f64 {
539        let n1 = g1.adjacency_matrix.nrows();
540        let n2 = g2.adjacency_matrix.nrows();
541        let n = n1 * n2;
542
543        if n == 0 {
544            return 0.0;
545        }
546
547        // Build M = I - λ A_×, where A_×[(a*n2+b), (c*n2+d)] = A1[a,c] * A2[b,d].
548        let a1 = &g1.adjacency_matrix;
549        let a2 = &g2.adjacency_matrix;
550        let mut m = Array2::<f64>::zeros((n, n));
551        for a in 0..n1 {
552            for c in 0..n1 {
553                let a1_ac = a1[[a, c]];
554                if a1_ac == 0.0 {
555                    continue;
556                }
557                for b in 0..n2 {
558                    for d in 0..n2 {
559                        let a2_bd = a2[[b, d]];
560                        if a2_bd == 0.0 {
561                            continue;
562                        }
563                        let row = a * n2 + b;
564                        let col = c * n2 + d;
565                        m[[row, col]] = -self.lambda * a1_ac * a2_bd;
566                    }
567                }
568            }
569        }
570        for i in 0..n {
571            m[[i, i]] += 1.0;
572        }
573
574        // Solve (I - λ A_×) s = 1 via Gaussian elimination with partial pivoting.
575        let mut rhs = Array1::<f64>::from_elem(n, 1.0);
576        match gaussian_solve(&mut m, &mut rhs) {
577            Some(s) => s.sum(),
578            None => 0.0,
579        }
580    }
581}
582
583/// Solve a dense linear system `A x = b` via Gaussian elimination with partial
584/// pivoting. Returns `None` if the matrix is (numerically) singular. `a` and
585/// `b` are consumed/overwritten as scratch space.
586fn gaussian_solve(a: &mut Array2<f64>, b: &mut Array1<f64>) -> Option<Array1<f64>> {
587    let n = a.nrows();
588    if n == 0 || a.ncols() != n || b.len() != n {
589        return None;
590    }
591
592    for col in 0..n {
593        // Partial pivoting: find the row with the largest magnitude in this column.
594        let mut pivot_row = col;
595        let mut pivot_val = a[[col, col]].abs();
596        for row in (col + 1)..n {
597            let val = a[[row, col]].abs();
598            if val > pivot_val {
599                pivot_val = val;
600                pivot_row = row;
601            }
602        }
603
604        if pivot_val < 1e-12 {
605            return None; // Singular.
606        }
607
608        if pivot_row != col {
609            for k in 0..n {
610                let tmp = a[[col, k]];
611                a[[col, k]] = a[[pivot_row, k]];
612                a[[pivot_row, k]] = tmp;
613            }
614            b.swap(col, pivot_row);
615        }
616
617        // Eliminate below the pivot.
618        let pivot = a[[col, col]];
619        for row in (col + 1)..n {
620            let factor = a[[row, col]] / pivot;
621            if factor == 0.0 {
622                continue;
623            }
624            for k in col..n {
625                let sub = factor * a[[col, k]];
626                a[[row, k]] -= sub;
627            }
628            b[row] -= factor * b[col];
629        }
630    }
631
632    // Back-substitution.
633    let mut x = Array1::<f64>::zeros(n);
634    for row in (0..n).rev() {
635        let mut sum = b[row];
636        for k in (row + 1)..n {
637            sum -= a[[row, k]] * x[k];
638        }
639        let diag = a[[row, row]];
640        if diag.abs() < 1e-12 {
641            return None;
642        }
643        x[row] = sum / diag;
644    }
645
646    Some(x)
647}
648
649/// Hellinger kernel implementation
650#[derive(Debug)]
651pub struct HellingerKernel;
652
653impl Kernel for HellingerKernel {
654    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
655        // Hellinger kernel: K(x,y) = sum(sqrt(x_i * y_i))
656        x.iter()
657            .zip(y.iter())
658            .map(|(xi, yi)| (xi * yi).sqrt())
659            .sum()
660    }
661
662    fn parameters(&self) -> HashMap<String, f64> {
663        HashMap::new()
664    }
665}
666
667/// Jensen-Shannon kernel implementation
668#[derive(Debug)]
669pub struct JensenShannonKernel;
670
671impl JensenShannonKernel {
672    fn jensen_shannon_divergence(&self, p: ArrayView1<f64>, q: ArrayView1<f64>) -> f64 {
673        // Jensen-Shannon divergence
674        let m: Vec<f64> = p
675            .iter()
676            .zip(q.iter())
677            .map(|(pi, qi)| 0.5 * (pi + qi))
678            .collect();
679        let m = Array1::from_vec(m);
680
681        let kl_pm = self.kl_divergence(p, m.view());
682        let kl_qm = self.kl_divergence(q, m.view());
683
684        0.5 * kl_pm + 0.5 * kl_qm
685    }
686
687    fn kl_divergence(&self, p: ArrayView1<f64>, q: ArrayView1<f64>) -> f64 {
688        // Kullback-Leibler divergence
689        p.iter()
690            .zip(q.iter())
691            .map(|(pi, qi)| {
692                if *pi > 0.0 && *qi > 0.0 {
693                    pi * (pi / qi).ln()
694                } else {
695                    0.0
696                }
697            })
698            .sum()
699    }
700}
701
702impl Kernel for JensenShannonKernel {
703    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
704        // Jensen-Shannon kernel: K(x,y) = exp(-JS(x,y))
705        let js_div = self.jensen_shannon_divergence(x, y);
706        (-js_div).exp()
707    }
708
709    fn parameters(&self) -> HashMap<String, f64> {
710        HashMap::new()
711    }
712}
713
714/// Implement Kernel trait for KernelType to enable direct usage
715impl Kernel for KernelType {
716    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
717        match self {
718            KernelType::Linear => LinearKernel.compute(x, y),
719            KernelType::Rbf { gamma } => RbfKernel::new(*gamma).compute(x, y),
720            KernelType::Polynomial {
721                gamma,
722                coef0,
723                degree,
724            } => PolynomialKernel::new(*gamma, *coef0, *degree).compute(x, y),
725            KernelType::Sigmoid { gamma, coef0 } => {
726                SigmoidKernel::new(*gamma, *coef0).compute(x, y)
727            }
728            KernelType::Cosine => CosineKernel.compute(x, y),
729            KernelType::ChiSquared { gamma } => ChiSquaredKernel::new(*gamma).compute(x, y),
730            KernelType::Intersection => IntersectionKernel.compute(x, y),
731            KernelType::Hellinger => HellingerKernel.compute(x, y),
732            KernelType::JensenShannon => JensenShannonKernel.compute(x, y),
733            KernelType::Periodic {
734                length_scale,
735                period,
736            } => PeriodicKernel::new(*length_scale, *period).compute(x, y),
737            KernelType::Precomputed => {
738                // See the note in `KernelFunction::compute`: a precomputed kernel
739                // must be indexed directly from a supplied matrix and has no
740                // pointwise value. Refuse rather than fabricate a 0.0 entry.
741                panic!(
742                    "precomputed kernel cannot be evaluated pointwise; supply the \
743                     kernel matrix and index it by sample position instead"
744                )
745            }
746            KernelType::Custom(_name) => {
747                // Default custom implementation
748                x.dot(&y)
749            }
750        }
751    }
752
753    fn parameters(&self) -> HashMap<String, f64> {
754        match self {
755            KernelType::Linear => HashMap::new(),
756            KernelType::Rbf { gamma } => {
757                let mut params = HashMap::new();
758                params.insert("gamma".to_string(), *gamma);
759                params
760            }
761            KernelType::Polynomial {
762                gamma,
763                coef0,
764                degree,
765            } => {
766                let mut params = HashMap::new();
767                params.insert("gamma".to_string(), *gamma);
768                params.insert("coef0".to_string(), *coef0);
769                params.insert("degree".to_string(), *degree);
770                params
771            }
772            KernelType::Sigmoid { gamma, coef0 } => {
773                let mut params = HashMap::new();
774                params.insert("gamma".to_string(), *gamma);
775                params.insert("coef0".to_string(), *coef0);
776                params
777            }
778            KernelType::Cosine => HashMap::new(),
779            KernelType::ChiSquared { gamma } => {
780                let mut params = HashMap::new();
781                params.insert("gamma".to_string(), *gamma);
782                params
783            }
784            KernelType::Intersection => HashMap::new(),
785            KernelType::Hellinger => HashMap::new(),
786            KernelType::JensenShannon => HashMap::new(),
787            KernelType::Periodic {
788                length_scale,
789                period,
790            } => {
791                let mut params = HashMap::new();
792                params.insert("length_scale".to_string(), *length_scale);
793                params.insert("period".to_string(), *period);
794                params
795            }
796            KernelType::Precomputed => HashMap::new(),
797            KernelType::Custom(_name) => HashMap::new(),
798        }
799    }
800}
801
802/// Implement Kernel trait for `Box<dyn Kernel>` to enable polymorphic usage
803impl Kernel for Box<dyn Kernel> {
804    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
805        self.as_ref().compute(x, y)
806    }
807
808    fn compute_matrix(&self, x: &Array2<f64>, y: &Array2<f64>) -> Array2<f64> {
809        self.as_ref().compute_matrix(x, y)
810    }
811
812    fn parameters(&self) -> HashMap<String, f64> {
813        self.as_ref().parameters()
814    }
815}
816
817#[allow(non_snake_case)]
818#[cfg(test)]
819mod tests {
820    use super::*;
821    use approx::assert_abs_diff_eq;
822
823    #[test]
824    fn test_linear_kernel() {
825        let kernel = LinearKernel;
826        let x = Array1::from_vec(vec![1.0, 2.0, 3.0]);
827        let y = Array1::from_vec(vec![4.0, 5.0, 6.0]);
828
829        let result = kernel.compute(x.view(), y.view());
830        assert_abs_diff_eq!(result, 32.0, epsilon = 1e-10);
831    }
832
833    #[test]
834    fn test_rbf_kernel() {
835        let kernel = RbfKernel::new(1.0);
836        let x = Array1::from_vec(vec![1.0, 2.0]);
837        let y = Array1::from_vec(vec![1.0, 2.0]);
838
839        let result = kernel.compute(x.view(), y.view());
840        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
841    }
842
843    #[test]
844    fn test_polynomial_kernel() {
845        let kernel = PolynomialKernel::new(1.0, 1.0, 2.0);
846        let x = Array1::from_vec(vec![1.0, 2.0]);
847        let y = Array1::from_vec(vec![3.0, 4.0]);
848
849        let result = kernel.compute(x.view(), y.view());
850        let expected = (1.0_f64 * (1.0 * 3.0 + 2.0 * 4.0) + 1.0).powf(2.0);
851        assert_abs_diff_eq!(result, expected, epsilon = 1e-10);
852    }
853
854    #[test]
855    fn test_cosine_kernel() {
856        let kernel = CosineKernel;
857        let x = Array1::from_vec(vec![1.0, 0.0]);
858        let y = Array1::from_vec(vec![0.0, 1.0]);
859
860        let result = kernel.compute(x.view(), y.view());
861        assert_abs_diff_eq!(result, 0.0, epsilon = 1e-10);
862    }
863
864    #[test]
865    fn test_kernel_function() {
866        let kernel_fn = KernelFunction::new(KernelType::Rbf { gamma: 0.5 });
867        let x = Array1::from_vec(vec![1.0, 2.0]);
868        let y = Array1::from_vec(vec![1.0, 2.0]);
869
870        let result = kernel_fn.compute(x.view(), y.view());
871        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
872    }
873
874    #[test]
875    fn test_kernel_matrix() {
876        let kernel_fn = KernelFunction::new(KernelType::Linear);
877        let x =
878            Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0]).expect("array shape mismatch");
879        let y =
880            Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0]).expect("array shape mismatch");
881
882        let kernel_matrix = kernel_fn.compute_matrix(&x, &y);
883
884        assert_eq!(kernel_matrix.dim(), (2, 2));
885        assert_abs_diff_eq!(kernel_matrix[[0, 0]], 5.0, epsilon = 1e-10); // [1,2] · [1,2] = 5
886        assert_abs_diff_eq!(kernel_matrix[[1, 1]], 25.0, epsilon = 1e-10); // [3,4] · [3,4] = 25
887    }
888}