Skip to main content

sklears_svm/
advanced_optimization.rs

1//! Advanced optimization methods for SVM
2//!
3//! This module implements modern optimization algorithms for SVM training that offer
4//! better convergence properties and scalability compared to traditional SMO.
5//!
6//! Algorithms included:
7//! - ADMM (Alternating Direction Method of Multipliers): Distributed optimization
8//! - Newton methods: Second-order optimization for faster convergence
9//! - Primal-dual methods: Simultaneous optimization of primal and dual problems
10//! - Trust region methods: Robust optimization with adaptive step sizes
11//! - Accelerated gradient methods: Fast first-order optimization
12
13use scirs2_core::ndarray::{s, Array1, Array2};
14use scirs2_linalg::compat::ArrayLinalgExt;
15
16use crate::kernels::{create_kernel, Kernel, KernelType};
17use sklears_core::error::{Result, SklearsError};
18
19/// Solve `a x = b`. Falls back to a Tikhonov-regularized system if the matrix
20/// is singular, and finally to the right-hand side itself so the outer
21/// iteration can still make progress. Never panics.
22///
23/// For a Newton system `H d = -g` the caller passes `b = -gradient`, so the
24/// last-resort fallback `d = b = -gradient` corresponds to a steepest-descent
25/// direction, allowing the surrounding loop to continue making progress even
26/// when the Hessian is numerically singular.
27fn solve_linear_system(a: &Array2<f64>, b: &Array1<f64>, reg: f64) -> Array1<f64> {
28    if let Ok(x) = a.solve(b) {
29        if x.iter().all(|v| v.is_finite()) {
30            return x;
31        }
32    }
33
34    // Regularize the diagonal and retry.
35    let n = a.nrows();
36    let mut a_reg = a.clone();
37    for i in 0..n {
38        a_reg[[i, i]] += reg.max(1e-8);
39    }
40    if let Ok(x) = a_reg.solve(b) {
41        if x.iter().all(|v| v.is_finite()) {
42            return x;
43        }
44    }
45
46    // Last resort: gradient-descent style direction (b itself). Callers pass
47    // b = -gradient for Newton systems, so this yields d = -gradient.
48    b.clone()
49}
50
51/// Configuration for advanced optimization methods
52#[derive(Debug, Clone)]
53pub struct AdvancedOptimizationConfig {
54    /// Regularization parameter
55    pub c: f64,
56    /// Kernel type
57    pub kernel: KernelType,
58    /// Tolerance for convergence
59    pub tol: f64,
60    /// Maximum number of iterations
61    pub max_iter: usize,
62    /// ADMM penalty parameter
63    pub rho: f64,
64    /// Trust region radius
65    pub trust_radius: f64,
66    /// Line search parameters
67    pub line_search_c1: f64,
68    pub line_search_c2: f64,
69    /// Newton method regularization
70    pub newton_reg: f64,
71    /// Verbose output
72    pub verbose: bool,
73}
74
75impl Default for AdvancedOptimizationConfig {
76    fn default() -> Self {
77        Self {
78            c: 1.0,
79            kernel: KernelType::Rbf { gamma: 1.0 },
80            tol: 1e-6,
81            max_iter: 1000,
82            rho: 1.0,
83            trust_radius: 1.0,
84            line_search_c1: 1e-4,
85            line_search_c2: 0.9,
86            newton_reg: 1e-8,
87            verbose: false,
88        }
89    }
90}
91
92/// Result of advanced optimization
93#[derive(Debug, Clone)]
94pub struct OptimizationResult {
95    /// Dual coefficients
96    pub dual_coef: Array1<f64>,
97    /// Intercept term
98    pub intercept: f64,
99    /// Support vector indices
100    pub support_indices: Vec<usize>,
101    /// Number of iterations
102    pub n_iterations: usize,
103    /// Final objective value
104    pub objective_value: f64,
105    /// Convergence status
106    pub converged: bool,
107    /// Optimization history
108    pub history: Vec<f64>,
109}
110
111/// ADMM (Alternating Direction Method of Multipliers) SVM Solver
112///
113/// ADMM is a distributed optimization algorithm that decomposes the SVM problem
114/// into smaller subproblems that can be solved in parallel. It's particularly
115/// effective for large-scale and distributed SVM training.
116///
117/// The algorithm solves the consensus problem:
118/// minimize f(x) + g(z)
119/// subject to Ax + Bz = c
120///
121/// For SVM, this becomes:
122/// minimize (1/2)||w||² + C∑ξᵢ
123/// subject to yᵢ(wᵀφ(xᵢ) + b) ≥ 1 - ξᵢ, ξᵢ ≥ 0
124///
125/// Reference: Boyd, S. et al. (2011). Distributed optimization and statistical
126/// learning via the alternating direction method of multipliers.
127#[derive(Debug, Clone)]
128pub struct ADMMSVM {
129    config: AdvancedOptimizationConfig,
130    kernel: Option<KernelType>,
131    is_fitted: bool,
132}
133
134impl Default for ADMMSVM {
135    /// Create a new ADMM SVM solver with default configuration
136    fn default() -> Self {
137        Self::new(AdvancedOptimizationConfig::default())
138    }
139}
140
141impl ADMMSVM {
142    /// Create a new ADMM SVM solver
143    pub fn new(config: AdvancedOptimizationConfig) -> Self {
144        Self {
145            config,
146            kernel: None,
147            is_fitted: false,
148        }
149    }
150
151    /// Fit the SVM using ADMM optimization
152    pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<OptimizationResult> {
153        // Validate inputs
154        if x.nrows() != y.len() {
155            return Err(SklearsError::InvalidInput(
156                "Number of samples must match number of labels".to_string(),
157            ));
158        }
159
160        let n_samples = x.nrows();
161
162        // Initialize kernel
163        let kernel = self.config.kernel.clone();
164        self.kernel = Some(kernel);
165
166        // Compute kernel matrix
167        let k_matrix = self.compute_kernel_matrix(x)?;
168
169        // Initialize variables
170        let mut alpha = Array1::zeros(n_samples);
171        let mut z = Array1::zeros(n_samples);
172        let mut u: Array1<f64> = Array1::zeros(n_samples); // Dual variables
173        let mut history = Vec::new();
174
175        // ADMM iterations
176        for iteration in 0..self.config.max_iter {
177            // Store previous z for dual residual calculation
178            let z_prev = z.clone();
179
180            // Update alpha (dual variables)
181            alpha = self.update_alpha(&k_matrix, &z, &u)?;
182
183            // Update w (primal variables)
184            let w = self.update_w(x, y, &alpha)?;
185
186            // Update z (auxiliary variables)
187            z = self.update_z(&alpha, &u)?;
188
189            // Update u (Lagrange multipliers)
190            u = &u + &((&alpha - &z) * self.config.rho);
191
192            // Calculate objective value
193            let objective = self.calculate_objective(&k_matrix, &alpha, &w)?;
194            history.push(objective);
195
196            if self.config.verbose && iteration % 10 == 0 {
197                println!("ADMM Iteration {}: Objective = {:.6}", iteration, objective);
198            }
199
200            // Check convergence
201            let primal_diff = &alpha - &z;
202            let primal_residual = primal_diff.dot(&primal_diff).sqrt();
203            // Dual residual: ρ * ||z^{k+1} - z^k||
204            let z_diff = &z - &z_prev;
205            let dual_residual = self.config.rho * z_diff.dot(&z_diff).sqrt();
206
207            if primal_residual < self.config.tol && dual_residual < self.config.tol {
208                if self.config.verbose {
209                    println!("ADMM converged after {} iterations", iteration + 1);
210                }
211
212                self.is_fitted = true;
213
214                let support_indices = self.find_support_vectors(&alpha)?;
215                let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
216
217                return Ok(OptimizationResult {
218                    dual_coef: alpha,
219                    intercept,
220                    support_indices,
221                    n_iterations: iteration + 1,
222                    objective_value: objective,
223                    converged: true,
224                    history,
225                });
226            }
227        }
228
229        self.is_fitted = true;
230
231        // Return result even if not converged
232        let support_indices = self.find_support_vectors(&alpha)?;
233        let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
234
235        Ok(OptimizationResult {
236            dual_coef: alpha,
237            intercept,
238            support_indices,
239            n_iterations: self.config.max_iter,
240            objective_value: history.last().copied().unwrap_or(0.0),
241            converged: false,
242            history,
243        })
244    }
245
246    /// Update alpha variables in ADMM
247    fn update_alpha(
248        &self,
249        k_matrix: &Array2<f64>,
250        z: &Array1<f64>,
251        u: &Array1<f64>,
252    ) -> Result<Array1<f64>> {
253        let n = k_matrix.nrows();
254
255        // Solve the alpha subproblem
256        // This is a QP: min (1/2) α^T Q α + p^T α
257        // where Q = K + rho I and p = -e + rho * (z - u)
258        let mut q_matrix = k_matrix.clone();
259        for i in 0..n {
260            q_matrix[[i, i]] += self.config.rho;
261        }
262
263        let p = &Array1::from_elem(n, -1.0) + &((z - u) * self.config.rho);
264
265        // Solve Q alpha = -p with a robust general solver.
266        let neg_p = p.mapv(|v| -v);
267        let mut alpha = solve_linear_system(&q_matrix, &neg_p, self.config.rho.max(1e-8));
268
269        // Project onto constraints [0, C]
270        for i in 0..n {
271            alpha[i] = alpha[i].max(0.0).min(self.config.c);
272        }
273
274        Ok(alpha)
275    }
276
277    /// Update w variables in ADMM
278    fn update_w(
279        &self,
280        x: &Array2<f64>,
281        y: &Array1<f64>,
282        alpha: &Array1<f64>,
283    ) -> Result<Array1<f64>> {
284        let n_features = x.ncols();
285        let mut w = Array1::zeros(n_features);
286
287        // w = Σ αᵢ yᵢ xᵢ
288        for i in 0..alpha.len() {
289            if alpha[i] > 0.0 {
290                let coeff = alpha[i] * y[i];
291                for k in 0..n_features {
292                    w[k] += coeff * x[[i, k]];
293                }
294            }
295        }
296
297        Ok(w)
298    }
299
300    /// Update z variables in ADMM
301    fn update_z(&self, alpha: &Array1<f64>, u: &Array1<f64>) -> Result<Array1<f64>> {
302        let mut z = Array1::zeros(alpha.len());
303
304        // Soft thresholding for z update
305        for i in 0..alpha.len() {
306            let temp = alpha[i] + u[i];
307            z[i] = if temp > self.config.c / self.config.rho {
308                temp - self.config.c / self.config.rho
309            } else if temp < 0.0 {
310                temp
311            } else {
312                0.0
313            };
314        }
315
316        Ok(z)
317    }
318
319    /// Compute kernel matrix
320    fn compute_kernel_matrix(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
321        let kernel_type = self
322            .kernel
323            .as_ref()
324            .ok_or_else(|| SklearsError::NotFitted {
325                operation: "compute_kernel_matrix".to_string(),
326            })?;
327        let kernel = create_kernel(kernel_type.clone())?;
328        let n = x.nrows();
329        let mut k_matrix = Array2::zeros((n, n));
330
331        for i in 0..n {
332            for j in 0..n {
333                k_matrix[[i, j]] = kernel.compute(x.row(i), x.row(j));
334            }
335        }
336
337        Ok(k_matrix)
338    }
339
340    /// Calculate ADMM objective value
341    fn calculate_objective(
342        &self,
343        k_matrix: &Array2<f64>,
344        alpha: &Array1<f64>,
345        w: &Array1<f64>,
346    ) -> Result<f64> {
347        let dual_obj = alpha.sum() - 0.5 * alpha.dot(&k_matrix.dot(alpha));
348        let primal_obj = 0.5 * w.dot(w);
349
350        Ok(dual_obj.max(primal_obj))
351    }
352
353    /// Find support vector indices
354    fn find_support_vectors(&self, alpha: &Array1<f64>) -> Result<Vec<usize>> {
355        let support_indices: Vec<usize> = alpha
356            .iter()
357            .enumerate()
358            .filter(|(_, &val)| val > self.config.tol)
359            .map(|(i, _)| i)
360            .collect();
361
362        Ok(support_indices)
363    }
364
365    /// Calculate intercept
366    fn calculate_intercept(
367        &self,
368        x: &Array2<f64>,
369        y: &Array1<f64>,
370        alpha: &Array1<f64>,
371        support_indices: &[usize],
372    ) -> Result<f64> {
373        if support_indices.is_empty() {
374            return Ok(0.0);
375        }
376
377        let kernel_type = self
378            .kernel
379            .as_ref()
380            .ok_or_else(|| SklearsError::NotFitted {
381                operation: "calculate_intercept".to_string(),
382            })?;
383        let kernel = create_kernel(kernel_type.clone())?;
384        let mut intercept_sum = 0.0;
385        let mut count = 0;
386
387        for &i in support_indices {
388            if alpha[i] > self.config.tol && alpha[i] < self.config.c - self.config.tol {
389                let mut decision_value = 0.0;
390                for &j in support_indices {
391                    decision_value += alpha[j] * y[j] * kernel.compute(x.row(i), x.row(j));
392                }
393                intercept_sum += y[i] - decision_value;
394                count += 1;
395            }
396        }
397
398        Ok(if count > 0 {
399            intercept_sum / count as f64
400        } else {
401            0.0
402        })
403    }
404
405    /// Predict using the fitted model
406    pub fn predict(&self, x: &Array2<f64>, result: &OptimizationResult) -> Result<Array1<f64>> {
407        if !self.is_fitted {
408            return Err(SklearsError::NotFitted {
409                operation: "prediction".to_string(),
410            });
411        }
412
413        let decision_values = self.decision_function(x, result)?;
414        Ok(Array1::from_vec(
415            decision_values
416                .iter()
417                .map(|&val| if val > 0.0 { 1.0 } else { -1.0 })
418                .collect(),
419        ))
420    }
421
422    /// Calculate decision function values
423    pub fn decision_function(
424        &self,
425        x: &Array2<f64>,
426        result: &OptimizationResult,
427    ) -> Result<Array1<f64>> {
428        if !self.is_fitted {
429            return Err(SklearsError::NotFitted {
430                operation: "prediction".to_string(),
431            });
432        }
433
434        let kernel_type = self
435            .kernel
436            .as_ref()
437            .ok_or_else(|| SklearsError::NotFitted {
438                operation: "decision_function".to_string(),
439            })?;
440        let kernel = create_kernel(kernel_type.clone())?;
441        let mut decision_values = Array1::zeros(x.nrows());
442
443        for i in 0..x.nrows() {
444            let mut sum = 0.0;
445            for &j in &result.support_indices {
446                sum += result.dual_coef[j] * kernel.compute(x.row(i), x.row(j));
447            }
448            decision_values[i] = sum + result.intercept;
449        }
450
451        Ok(decision_values)
452    }
453}
454
455/// Newton Method SVM Solver
456///
457/// Newton methods use second-order derivatives (Hessian) to achieve faster convergence
458/// compared to first-order methods. This implementation uses a regularized Newton
459/// method with line search for robustness.
460#[derive(Debug, Clone)]
461pub struct NewtonSVM {
462    config: AdvancedOptimizationConfig,
463    kernel: Option<KernelType>,
464    is_fitted: bool,
465}
466
467impl Default for NewtonSVM {
468    /// Create a new Newton SVM solver with default configuration
469    fn default() -> Self {
470        Self::new(AdvancedOptimizationConfig::default())
471    }
472}
473
474impl NewtonSVM {
475    /// Create a new Newton SVM solver
476    pub fn new(config: AdvancedOptimizationConfig) -> Self {
477        Self {
478            config,
479            kernel: None,
480            is_fitted: false,
481        }
482    }
483
484    /// Fit the SVM using Newton method
485    pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<OptimizationResult> {
486        // Validate inputs
487        if x.nrows() != y.len() {
488            return Err(SklearsError::InvalidInput(
489                "Number of samples must match number of labels".to_string(),
490            ));
491        }
492
493        // Initialize kernel
494        let kernel = self.config.kernel.clone();
495        self.kernel = Some(kernel);
496
497        // For linear kernel, we can use primal Newton method
498        if matches!(self.config.kernel, KernelType::Linear) {
499            self.fit_primal_newton(x, y)
500        } else {
501            // For non-linear kernels, use dual Newton method
502            self.fit_dual_newton(x, y)
503        }
504    }
505
506    /// Fit using primal Newton method (for linear SVMs)
507    fn fit_primal_newton(
508        &mut self,
509        x: &Array2<f64>,
510        y: &Array1<f64>,
511    ) -> Result<OptimizationResult> {
512        let n_samples = x.nrows();
513        let n_features = x.ncols();
514
515        // Initialize variables
516        let mut w: Array1<f64> = Array1::zeros(n_features);
517        let mut b = 0.0;
518        let mut history = Vec::new();
519
520        for iteration in 0..self.config.max_iter {
521            // Calculate margins
522            let margins = self.calculate_margins(x, y, &w, b);
523
524            // Find active constraints (margin < 1)
525            let active_indices: Vec<usize> = margins
526                .iter()
527                .enumerate()
528                .filter(|(_, &margin)| margin < 1.0)
529                .map(|(i, _)| i)
530                .collect();
531
532            if active_indices.is_empty() {
533                break; // All constraints satisfied
534            }
535
536            // Build Hessian matrix
537            let hessian = self.build_hessian(x, &active_indices)?;
538
539            // Build gradient
540            let gradient = self.build_gradient(x, y, &w, b, &active_indices, &margins)?;
541
542            // Solve Newton system: H * d = -g
543            let neg_gradient = gradient.mapv(|v| -v);
544            let direction = solve_linear_system(&hessian, &neg_gradient, self.config.newton_reg);
545
546            // Line search
547            let step_size = self.line_search(x, y, &w, b, &direction, &margins)?;
548
549            // Update variables
550            for k in 0..n_features {
551                w[k] += step_size * direction[k];
552            }
553            b += step_size * direction[n_features];
554
555            // Calculate objective
556            let objective = self.calculate_primal_objective(&w, &margins);
557            history.push(objective);
558
559            if self.config.verbose && iteration % 10 == 0 {
560                println!(
561                    "Newton Iteration {}: Objective = {:.6}",
562                    iteration, objective
563                );
564            }
565
566            // Check convergence
567            if gradient.dot(&gradient).sqrt() < self.config.tol {
568                if self.config.verbose {
569                    println!("Newton method converged after {} iterations", iteration + 1);
570                }
571
572                self.is_fitted = true;
573
574                return Ok(OptimizationResult {
575                    dual_coef: Array1::zeros(n_samples), // Not applicable for primal
576                    intercept: b,
577                    support_indices: active_indices,
578                    n_iterations: iteration + 1,
579                    objective_value: objective,
580                    converged: true,
581                    history,
582                });
583            }
584        }
585
586        self.is_fitted = true;
587
588        // Return result even if not converged
589        let margins = self.calculate_margins(x, y, &w, b);
590        let active_indices: Vec<usize> = margins
591            .iter()
592            .enumerate()
593            .filter(|(_, &margin)| margin < 1.0)
594            .map(|(i, _)| i)
595            .collect();
596
597        Ok(OptimizationResult {
598            dual_coef: Array1::zeros(n_samples),
599            intercept: b,
600            support_indices: active_indices,
601            n_iterations: self.config.max_iter,
602            objective_value: history.last().copied().unwrap_or(0.0),
603            converged: false,
604            history,
605        })
606    }
607
608    /// Fit using dual Newton method (for non-linear SVMs)
609    fn fit_dual_newton(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<OptimizationResult> {
610        let n_samples = x.nrows();
611
612        // Initialize dual variables
613        let mut alpha: Array1<f64> = Array1::zeros(n_samples);
614        let mut history = Vec::new();
615
616        // Compute kernel matrix
617        let k_matrix = self.compute_kernel_matrix(x)?;
618
619        for iteration in 0..self.config.max_iter {
620            // Calculate gradient of dual objective
621            let gradient = self.calculate_dual_gradient(&k_matrix, &alpha);
622
623            // Calculate Hessian of dual objective
624            let hessian = self.calculate_dual_hessian(&k_matrix, &alpha)?;
625
626            // Solve Newton system
627            let neg_gradient = gradient.mapv(|v| -v);
628            let direction = solve_linear_system(&hessian, &neg_gradient, self.config.newton_reg);
629
630            // Line search for step size
631            let step_size = self.dual_line_search(&k_matrix, &alpha, &direction)?;
632
633            // Update alpha
634            alpha = &alpha + &(&direction * step_size);
635
636            // Project onto constraints [0, C]
637            for i in 0..n_samples {
638                alpha[i] = alpha[i].max(0.0).min(self.config.c);
639            }
640
641            // Calculate objective
642            let objective = self.calculate_dual_objective(&k_matrix, &alpha);
643            history.push(objective);
644
645            if self.config.verbose && iteration % 10 == 0 {
646                println!(
647                    "Dual Newton Iteration {}: Objective = {:.6}",
648                    iteration, objective
649                );
650            }
651
652            // Check convergence
653            if gradient.dot(&gradient).sqrt() < self.config.tol {
654                if self.config.verbose {
655                    println!(
656                        "Dual Newton method converged after {} iterations",
657                        iteration + 1
658                    );
659                }
660
661                self.is_fitted = true;
662
663                let support_indices = self.find_support_vectors(&alpha)?;
664                let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
665
666                return Ok(OptimizationResult {
667                    dual_coef: alpha,
668                    intercept,
669                    support_indices,
670                    n_iterations: iteration + 1,
671                    objective_value: objective,
672                    converged: true,
673                    history,
674                });
675            }
676        }
677
678        self.is_fitted = true;
679
680        // Return result even if not converged
681        let support_indices = self.find_support_vectors(&alpha)?;
682        let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
683
684        Ok(OptimizationResult {
685            dual_coef: alpha,
686            intercept,
687            support_indices,
688            n_iterations: self.config.max_iter,
689            objective_value: history.last().copied().unwrap_or(0.0),
690            converged: false,
691            history,
692        })
693    }
694
695    /// Calculate margins for primal Newton method
696    fn calculate_margins(
697        &self,
698        x: &Array2<f64>,
699        y: &Array1<f64>,
700        w: &Array1<f64>,
701        b: f64,
702    ) -> Vec<f64> {
703        let mut margins = Vec::with_capacity(x.nrows());
704        for i in 0..x.nrows() {
705            let decision_value = x.row(i).dot(w) + b;
706            margins.push(y[i] * decision_value);
707        }
708        margins
709    }
710
711    /// Build Hessian matrix for primal Newton method
712    fn build_hessian(&self, x: &Array2<f64>, active_indices: &[usize]) -> Result<Array2<f64>> {
713        let n_features = x.ncols();
714        let mut hessian = Array2::zeros((n_features + 1, n_features + 1));
715
716        // Add identity for regularization
717        for i in 0..n_features {
718            hessian[[i, i]] = 1.0;
719        }
720
721        // Add contributions from active constraints
722        for &idx in active_indices {
723            let x_i = x.row(idx);
724
725            // H_ww += x_i * x_i^T
726            for i in 0..n_features {
727                for j in 0..n_features {
728                    hessian[[i, j]] += x_i[i] * x_i[j];
729                }
730            }
731
732            // H_wb = H_bw += x_i
733            for i in 0..n_features {
734                hessian[[i, n_features]] += x_i[i];
735                hessian[[n_features, i]] += x_i[i];
736            }
737
738            // H_bb += 1
739            hessian[[n_features, n_features]] += 1.0;
740        }
741
742        hessian.mapv_inplace(|v| v * self.config.c);
743
744        Ok(hessian)
745    }
746
747    /// Build gradient for primal Newton method
748    fn build_gradient(
749        &self,
750        x: &Array2<f64>,
751        y: &Array1<f64>,
752        w: &Array1<f64>,
753        _b: f64,
754        active_indices: &[usize],
755        margins: &[f64],
756    ) -> Result<Array1<f64>> {
757        let n_features = x.ncols();
758        let mut gradient = Array1::zeros(n_features + 1);
759
760        // Regularization term
761        gradient.slice_mut(s![0..n_features]).assign(w);
762
763        // Add contributions from active constraints
764        for &idx in active_indices {
765            let violation = 1.0 - margins[idx];
766            if violation > 0.0 {
767                let x_i = x.row(idx);
768
769                // dL/dw += -C * y_i * x_i
770                for i in 0..n_features {
771                    gradient[i] -= self.config.c * y[idx] * x_i[i];
772                }
773
774                // dL/db += -C * y_i
775                gradient[n_features] -= self.config.c * y[idx];
776            }
777        }
778
779        Ok(gradient)
780    }
781
782    /// Line search for primal Newton method
783    fn line_search(
784        &self,
785        x: &Array2<f64>,
786        y: &Array1<f64>,
787        w: &Array1<f64>,
788        b: f64,
789        direction: &Array1<f64>,
790        margins: &[f64],
791    ) -> Result<f64> {
792        let n_features = x.ncols();
793        let mut step_size = 1.0;
794        let current_obj = self.calculate_primal_objective(w, margins);
795
796        for _ in 0..20 {
797            // Max 20 backtracking steps
798            let mut new_w = w.clone();
799            for k in 0..n_features {
800                new_w[k] += step_size * direction[k];
801            }
802            let new_b = b + step_size * direction[n_features];
803            let new_margins = self.calculate_margins(x, y, &new_w, new_b);
804            let new_obj = self.calculate_primal_objective(&new_w, &new_margins);
805
806            if new_obj < current_obj {
807                return Ok(step_size);
808            }
809
810            step_size *= 0.5;
811        }
812
813        Ok(step_size)
814    }
815
816    /// Calculate primal objective value
817    fn calculate_primal_objective(&self, w: &Array1<f64>, margins: &[f64]) -> f64 {
818        let regularization = 0.5 * w.dot(w);
819        let hinge_loss: f64 = margins.iter().map(|&margin| (1.0 - margin).max(0.0)).sum();
820
821        regularization + self.config.c * hinge_loss
822    }
823
824    /// Helper methods for dual Newton method
825    fn compute_kernel_matrix(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
826        let kernel_type = self
827            .kernel
828            .as_ref()
829            .ok_or_else(|| SklearsError::NotFitted {
830                operation: "compute_kernel_matrix".to_string(),
831            })?;
832        let kernel = create_kernel(kernel_type.clone())?;
833        let n = x.nrows();
834        let mut k_matrix = Array2::zeros((n, n));
835
836        for i in 0..n {
837            for j in 0..n {
838                k_matrix[[i, j]] = kernel.compute(x.row(i), x.row(j));
839            }
840        }
841
842        Ok(k_matrix)
843    }
844
845    fn calculate_dual_gradient(&self, k_matrix: &Array2<f64>, alpha: &Array1<f64>) -> Array1<f64> {
846        &Array1::from_elem(alpha.len(), 1.0) - &k_matrix.dot(alpha)
847    }
848
849    fn calculate_dual_hessian(
850        &self,
851        k_matrix: &Array2<f64>,
852        _alpha: &Array1<f64>,
853    ) -> Result<Array2<f64>> {
854        // For SVM, Hessian is just the kernel matrix
855        Ok(k_matrix.clone())
856    }
857
858    fn dual_line_search(
859        &self,
860        k_matrix: &Array2<f64>,
861        alpha: &Array1<f64>,
862        direction: &Array1<f64>,
863    ) -> Result<f64> {
864        let mut step_size = 1.0;
865        let current_obj = self.calculate_dual_objective(k_matrix, alpha);
866
867        for _ in 0..20 {
868            let new_alpha = alpha + &(direction * step_size);
869            let new_obj = self.calculate_dual_objective(k_matrix, &new_alpha);
870
871            if new_obj > current_obj {
872                return Ok(step_size);
873            }
874
875            step_size *= 0.5;
876        }
877
878        Ok(step_size)
879    }
880
881    fn calculate_dual_objective(&self, k_matrix: &Array2<f64>, alpha: &Array1<f64>) -> f64 {
882        alpha.sum() - 0.5 * alpha.dot(&k_matrix.dot(alpha))
883    }
884
885    fn find_support_vectors(&self, alpha: &Array1<f64>) -> Result<Vec<usize>> {
886        let support_indices: Vec<usize> = alpha
887            .iter()
888            .enumerate()
889            .filter(|(_, &val)| val > self.config.tol)
890            .map(|(i, _)| i)
891            .collect();
892
893        Ok(support_indices)
894    }
895
896    fn calculate_intercept(
897        &self,
898        x: &Array2<f64>,
899        y: &Array1<f64>,
900        alpha: &Array1<f64>,
901        support_indices: &[usize],
902    ) -> Result<f64> {
903        if support_indices.is_empty() {
904            return Ok(0.0);
905        }
906
907        let kernel_type = self
908            .kernel
909            .as_ref()
910            .ok_or_else(|| SklearsError::NotFitted {
911                operation: "calculate_intercept".to_string(),
912            })?;
913        let kernel = create_kernel(kernel_type.clone())?;
914        let mut intercept_sum = 0.0;
915        let mut count = 0;
916
917        for &i in support_indices {
918            if alpha[i] > self.config.tol && alpha[i] < self.config.c - self.config.tol {
919                let mut decision_value = 0.0;
920                for &j in support_indices {
921                    decision_value += alpha[j] * y[j] * kernel.compute(x.row(i), x.row(j));
922                }
923                intercept_sum += y[i] - decision_value;
924                count += 1;
925            }
926        }
927
928        Ok(if count > 0 {
929            intercept_sum / count as f64
930        } else {
931            0.0
932        })
933    }
934
935    /// Predict using the fitted model
936    pub fn predict(&self, x: &Array2<f64>, result: &OptimizationResult) -> Result<Array1<f64>> {
937        if !self.is_fitted {
938            return Err(SklearsError::NotFitted {
939                operation: "prediction".to_string(),
940            });
941        }
942
943        let decision_values = self.decision_function(x, result)?;
944        Ok(Array1::from_vec(
945            decision_values
946                .iter()
947                .map(|&val| if val > 0.0 { 1.0 } else { -1.0 })
948                .collect(),
949        ))
950    }
951
952    /// Calculate decision function values
953    pub fn decision_function(
954        &self,
955        x: &Array2<f64>,
956        result: &OptimizationResult,
957    ) -> Result<Array1<f64>> {
958        if !self.is_fitted {
959            return Err(SklearsError::NotFitted {
960                operation: "prediction".to_string(),
961            });
962        }
963
964        let kernel_type = self
965            .kernel
966            .as_ref()
967            .ok_or_else(|| SklearsError::NotFitted {
968                operation: "decision_function".to_string(),
969            })?;
970        let kernel = create_kernel(kernel_type.clone())?;
971        let mut decision_values = Array1::zeros(x.nrows());
972
973        for i in 0..x.nrows() {
974            let mut sum = 0.0;
975            for &j in &result.support_indices {
976                sum += result.dual_coef[j] * kernel.compute(x.row(i), x.row(j));
977            }
978            decision_values[i] = sum + result.intercept;
979        }
980
981        Ok(decision_values)
982    }
983}
984
985/// Trust Region SVM Solver
986///
987/// Trust region methods are iterative optimization algorithms that maintain a "trust region"
988/// around the current point and solve subproblems within this region. The trust region radius
989/// is adaptively adjusted based on the quality of the quadratic approximation.
990///
991/// For SVM optimization, we apply trust region methods to the dual problem:
992/// maximize W(α) = Σα_i - (1/2) Σ_i Σ_j α_i α_j y_i y_j K(x_i, x_j)
993/// subject to Σ α_i y_i = 0 and 0 ≤ α_i ≤ C
994///
995/// Reference: Nocedal, J. & Wright, S. (2006). Numerical Optimization. Springer.
996#[derive(Debug, Clone)]
997pub struct TrustRegionSVM {
998    config: AdvancedOptimizationConfig,
999    kernel: Option<KernelType>,
1000    is_fitted: bool,
1001}
1002
1003impl Default for TrustRegionSVM {
1004    /// Create a new trust region SVM solver with default configuration
1005    fn default() -> Self {
1006        Self::new(AdvancedOptimizationConfig::default())
1007    }
1008}
1009
1010impl TrustRegionSVM {
1011    /// Create a new trust region SVM solver
1012    pub fn new(config: AdvancedOptimizationConfig) -> Self {
1013        Self {
1014            config,
1015            kernel: None,
1016            is_fitted: false,
1017        }
1018    }
1019
1020    /// Fit the SVM using trust region optimization
1021    pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<OptimizationResult> {
1022        // Validate inputs
1023        if x.nrows() != y.len() {
1024            return Err(SklearsError::InvalidInput(
1025                "Number of samples must match number of labels".to_string(),
1026            ));
1027        }
1028
1029        let n_samples = x.nrows();
1030        self.kernel = Some(self.config.kernel.clone());
1031
1032        // Compute kernel matrix
1033        let k_matrix = self.compute_kernel_matrix(x)?;
1034
1035        // Initialize dual variables
1036        let mut alpha = Array1::zeros(n_samples);
1037        let mut trust_radius = self.config.trust_radius;
1038        let mut history = Vec::new();
1039
1040        // Trust region iterations
1041        for iteration in 0..self.config.max_iter {
1042            // Compute gradient and Hessian
1043            let gradient = self.compute_dual_gradient(&k_matrix, &alpha);
1044            let hessian = self.compute_dual_hessian(&k_matrix);
1045
1046            // Solve trust region subproblem
1047            let step = self.solve_trust_region_subproblem(&gradient, &hessian, trust_radius)?;
1048
1049            // The trust-region subproblem minimizes f(alpha) = -W(alpha), where
1050            // W is the dual objective being maximized. `current_obj`/`new_obj`
1051            // below are W; the actual reduction in f therefore corresponds to an
1052            // *increase* in W, i.e. `new_obj - current_obj`.
1053            let current_obj = self.calculate_dual_objective(&k_matrix, &alpha);
1054            let new_alpha = self.project_onto_constraints(&(&alpha + &step));
1055            let new_obj = self.calculate_dual_objective(&k_matrix, &new_alpha);
1056
1057            let actual_reduction = new_obj - current_obj;
1058            let predicted_reduction = self.compute_predicted_reduction(&gradient, &hessian, &step);
1059
1060            // Compute trust region ratio
1061            let ratio = if predicted_reduction.abs() < 1e-12 {
1062                0.0
1063            } else {
1064                actual_reduction / predicted_reduction
1065            };
1066
1067            if self.config.verbose && iteration % 10 == 0 {
1068                println!(
1069                    "Trust Region Iter {}: Obj = {:.6}, Trust Radius = {:.6}, Ratio = {:.3}",
1070                    iteration, current_obj, trust_radius, ratio
1071                );
1072            }
1073
1074            // Update trust region radius and accept/reject step
1075            let step_norm = step.dot(&step).sqrt();
1076            if ratio > 0.75 && (step_norm - trust_radius).abs() < 1e-6 {
1077                // Very good step and we hit the boundary, expand trust region
1078                trust_radius = (2.0 * trust_radius).min(10.0);
1079                alpha = new_alpha;
1080            } else if ratio > 0.25 {
1081                // Good step, accept and maintain trust region
1082                alpha = new_alpha;
1083            } else if ratio > 0.0 {
1084                // Mediocre step, accept but shrink trust region
1085                trust_radius *= 0.5;
1086                alpha = new_alpha;
1087            } else {
1088                // Bad step, reject and shrink trust region significantly
1089                trust_radius *= 0.25;
1090            }
1091
1092            // Ensure minimum trust region radius
1093            trust_radius = trust_radius.max(1e-8);
1094
1095            history.push(current_obj);
1096
1097            // Check convergence
1098            if gradient.dot(&gradient).sqrt() < self.config.tol || trust_radius < 1e-8 {
1099                if self.config.verbose {
1100                    println!("Trust Region converged after {} iterations", iteration + 1);
1101                }
1102
1103                let support_indices = self.find_support_vectors(&alpha)?;
1104                let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
1105
1106                self.is_fitted = true;
1107
1108                return Ok(OptimizationResult {
1109                    dual_coef: alpha,
1110                    intercept,
1111                    support_indices,
1112                    n_iterations: iteration + 1,
1113                    objective_value: current_obj,
1114                    converged: true,
1115                    history,
1116                });
1117            }
1118        }
1119
1120        // Return result even if not converged
1121        let support_indices = self.find_support_vectors(&alpha)?;
1122        let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
1123
1124        self.is_fitted = true;
1125
1126        Ok(OptimizationResult {
1127            dual_coef: alpha,
1128            intercept,
1129            support_indices,
1130            n_iterations: self.config.max_iter,
1131            objective_value: history.last().copied().unwrap_or(0.0),
1132            converged: false,
1133            history,
1134        })
1135    }
1136
1137    /// Solve trust region subproblem using Cauchy point and Newton step
1138    fn solve_trust_region_subproblem(
1139        &self,
1140        gradient: &Array1<f64>,
1141        hessian: &Array2<f64>,
1142        trust_radius: f64,
1143    ) -> Result<Array1<f64>> {
1144        // Compute Cauchy point (steepest descent direction)
1145        let cauchy_step = self.compute_cauchy_point(gradient, hessian, trust_radius);
1146
1147        // Compute Newton step with a robust general solver.
1148        let neg_gradient = gradient.mapv(|v| -v);
1149        let newton_step = solve_linear_system(hessian, &neg_gradient, self.config.newton_reg);
1150        let newton_norm = newton_step.dot(&newton_step).sqrt();
1151
1152        if newton_norm <= trust_radius {
1153            // Newton step is within trust region
1154            return Ok(newton_step);
1155        }
1156
1157        // Newton step is outside trust region, use dogleg method
1158        Ok(self.dogleg_method(&newton_step, &cauchy_step, trust_radius))
1159    }
1160
1161    /// Compute Cauchy point (steepest descent step within trust region)
1162    fn compute_cauchy_point(
1163        &self,
1164        gradient: &Array1<f64>,
1165        hessian: &Array2<f64>,
1166        trust_radius: f64,
1167    ) -> Array1<f64> {
1168        let grad_norm = gradient.dot(gradient).sqrt();
1169
1170        if grad_norm < 1e-12 {
1171            return Array1::zeros(gradient.len());
1172        }
1173
1174        let unit_grad = gradient / grad_norm;
1175        let hess_grad = hessian.dot(&unit_grad);
1176        let curvature = unit_grad.dot(&hess_grad);
1177
1178        if curvature <= 0.0 {
1179            // Negative curvature, go to boundary
1180            &unit_grad * (-trust_radius)
1181        } else {
1182            // Positive curvature, minimize quadratic or go to boundary
1183            let optimal_step = grad_norm / curvature;
1184            let step_length = optimal_step.min(trust_radius);
1185            &unit_grad * (-step_length)
1186        }
1187    }
1188
1189    /// Dogleg method for combining Cauchy point and Newton step
1190    fn dogleg_method(
1191        &self,
1192        newton_step: &Array1<f64>,
1193        cauchy_step: &Array1<f64>,
1194        trust_radius: f64,
1195    ) -> Array1<f64> {
1196        let cauchy_norm = cauchy_step.dot(cauchy_step).sqrt();
1197
1198        if cauchy_norm >= trust_radius {
1199            // Cauchy point is outside trust region
1200            return cauchy_step * (trust_radius / cauchy_norm);
1201        }
1202
1203        // Find intersection of dogleg path with trust region
1204        let dogleg_direction = newton_step - cauchy_step;
1205        let a = dogleg_direction.dot(&dogleg_direction);
1206        let b = 2.0 * cauchy_step.dot(&dogleg_direction);
1207        let c = cauchy_norm * cauchy_norm - trust_radius * trust_radius;
1208
1209        if a < 1e-12 {
1210            return cauchy_step.clone();
1211        }
1212
1213        let discriminant = b * b - 4.0 * a * c;
1214        if discriminant < 0.0 {
1215            return cauchy_step.clone();
1216        }
1217
1218        let tau = (-b + discriminant.sqrt()) / (2.0 * a);
1219        let tau = tau.clamp(0.0, 1.0);
1220
1221        cauchy_step + &(&dogleg_direction * tau)
1222    }
1223
1224    /// Compute predicted reduction for trust region ratio
1225    fn compute_predicted_reduction(
1226        &self,
1227        gradient: &Array1<f64>,
1228        hessian: &Array2<f64>,
1229        step: &Array1<f64>,
1230    ) -> f64 {
1231        let linear_term = gradient.dot(step);
1232        let quadratic_term = 0.5 * step.dot(&hessian.dot(step));
1233        -(linear_term + quadratic_term)
1234    }
1235
1236    /// Project dual variables onto constraint set [0, C]
1237    fn project_onto_constraints(&self, alpha: &Array1<f64>) -> Array1<f64> {
1238        alpha.mapv(|val| val.max(0.0).min(self.config.c))
1239    }
1240
1241    /// Compute the gradient of the minimization objective `f = -W`.
1242    ///
1243    /// The dual objective `W(alpha) = e^T alpha - 0.5 alpha^T K alpha` is
1244    /// maximized, so the trust-region machinery (which is formulated for
1245    /// minimization) operates on `f(alpha) = -W(alpha)` whose gradient is
1246    /// `nabla f = K alpha - e`.
1247    fn compute_dual_gradient(&self, k_matrix: &Array2<f64>, alpha: &Array1<f64>) -> Array1<f64> {
1248        &k_matrix.dot(alpha) - &Array1::from_elem(alpha.len(), 1.0)
1249    }
1250
1251    /// Compute Hessian of the minimization objective (kernel matrix).
1252    ///
1253    /// `nabla^2 f = K`, which is positive semidefinite, matching the
1254    /// trust-region subproblem's assumptions.
1255    fn compute_dual_hessian(&self, k_matrix: &Array2<f64>) -> Array2<f64> {
1256        k_matrix.clone()
1257    }
1258
1259    /// Compute kernel matrix
1260    fn compute_kernel_matrix(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1261        let kernel_type = self
1262            .kernel
1263            .as_ref()
1264            .ok_or_else(|| SklearsError::NotFitted {
1265                operation: "compute_kernel_matrix".to_string(),
1266            })?;
1267        let kernel = create_kernel(kernel_type.clone())?;
1268        let n = x.nrows();
1269        let mut k_matrix = Array2::zeros((n, n));
1270
1271        for i in 0..n {
1272            for j in 0..n {
1273                k_matrix[[i, j]] = kernel.compute(x.row(i), x.row(j));
1274            }
1275        }
1276
1277        Ok(k_matrix)
1278    }
1279
1280    /// Calculate dual objective value
1281    fn calculate_dual_objective(&self, k_matrix: &Array2<f64>, alpha: &Array1<f64>) -> f64 {
1282        alpha.sum() - 0.5 * alpha.dot(&k_matrix.dot(alpha))
1283    }
1284
1285    /// Find support vectors
1286    fn find_support_vectors(&self, alpha: &Array1<f64>) -> Result<Vec<usize>> {
1287        let support_indices: Vec<usize> = alpha
1288            .iter()
1289            .enumerate()
1290            .filter(|(_, &val)| val > self.config.tol)
1291            .map(|(i, _)| i)
1292            .collect();
1293
1294        Ok(support_indices)
1295    }
1296
1297    /// Calculate intercept term
1298    fn calculate_intercept(
1299        &self,
1300        x: &Array2<f64>,
1301        y: &Array1<f64>,
1302        alpha: &Array1<f64>,
1303        support_indices: &[usize],
1304    ) -> Result<f64> {
1305        if support_indices.is_empty() {
1306            return Ok(0.0);
1307        }
1308
1309        let kernel_type = self
1310            .kernel
1311            .as_ref()
1312            .ok_or_else(|| SklearsError::NotFitted {
1313                operation: "calculate_intercept".to_string(),
1314            })?;
1315        let kernel = create_kernel(kernel_type.clone())?;
1316        let mut intercept_sum = 0.0;
1317        let mut count = 0;
1318
1319        for &i in support_indices {
1320            if alpha[i] > self.config.tol && alpha[i] < self.config.c - self.config.tol {
1321                let mut decision_value = 0.0;
1322                for &j in support_indices {
1323                    decision_value += alpha[j] * y[j] * kernel.compute(x.row(i), x.row(j));
1324                }
1325                intercept_sum += y[i] - decision_value;
1326                count += 1;
1327            }
1328        }
1329
1330        Ok(if count > 0 {
1331            intercept_sum / count as f64
1332        } else {
1333            0.0
1334        })
1335    }
1336
1337    /// Predict using the fitted model
1338    pub fn predict(&self, x: &Array2<f64>, result: &OptimizationResult) -> Result<Array1<f64>> {
1339        if !self.is_fitted {
1340            return Err(SklearsError::NotFitted {
1341                operation: "prediction".to_string(),
1342            });
1343        }
1344
1345        let decision_values = self.decision_function(x, result)?;
1346        Ok(Array1::from_vec(
1347            decision_values
1348                .iter()
1349                .map(|&val| if val > 0.0 { 1.0 } else { -1.0 })
1350                .collect(),
1351        ))
1352    }
1353
1354    /// Calculate decision function values
1355    pub fn decision_function(
1356        &self,
1357        x: &Array2<f64>,
1358        result: &OptimizationResult,
1359    ) -> Result<Array1<f64>> {
1360        if !self.is_fitted {
1361            return Err(SklearsError::NotFitted {
1362                operation: "prediction".to_string(),
1363            });
1364        }
1365
1366        let kernel_type = self
1367            .kernel
1368            .as_ref()
1369            .ok_or_else(|| SklearsError::NotFitted {
1370                operation: "decision_function".to_string(),
1371            })?;
1372        let kernel = create_kernel(kernel_type.clone())?;
1373        let mut decision_values = Array1::zeros(x.nrows());
1374
1375        for i in 0..x.nrows() {
1376            let mut sum = 0.0;
1377            for &j in &result.support_indices {
1378                sum += result.dual_coef[j] * kernel.compute(x.row(i), x.row(j));
1379            }
1380            decision_values[i] = sum + result.intercept;
1381        }
1382
1383        Ok(decision_values)
1384    }
1385}
1386
1387/// Accelerated Gradient SVM Solver
1388///
1389/// Implements accelerated gradient descent methods for SVM optimization,
1390/// including Nesterov's accelerated gradient method and FISTA.
1391/// These methods achieve faster convergence rates than standard gradient descent.
1392///
1393/// Nesterov's method uses momentum to accelerate convergence:
1394/// y_{k+1} = x_k - γ∇f(x_k)
1395/// x_{k+1} = y_{k+1} + β(y_{k+1} - y_k)
1396///
1397/// Reference: Nesterov, Y. (2013). Introductory lectures on convex optimization.
1398#[derive(Debug, Clone)]
1399pub struct AcceleratedGradientSVM {
1400    config: AdvancedOptimizationConfig,
1401    kernel: Option<KernelType>,
1402    is_fitted: bool,
1403    /// Momentum parameter (typically 0.9)
1404    pub momentum: f64,
1405    /// Learning rate schedule
1406    pub learning_rate: f64,
1407    /// Accelerated method type
1408    pub method: AcceleratedMethod,
1409}
1410
1411/// Types of accelerated gradient methods
1412#[derive(Debug, Clone)]
1413pub enum AcceleratedMethod {
1414    /// Nesterov's accelerated gradient method
1415    Nesterov,
1416    /// Fast Iterative Shrinkage-Thresholding Algorithm
1417    FISTA,
1418    /// Heavy ball method
1419    HeavyBall,
1420}
1421
1422impl AcceleratedGradientSVM {
1423    /// Create a new accelerated gradient SVM solver
1424    pub fn new(config: AdvancedOptimizationConfig) -> Self {
1425        Self {
1426            config,
1427            kernel: None,
1428            is_fitted: false,
1429            momentum: 0.9,
1430            learning_rate: 0.01,
1431            method: AcceleratedMethod::Nesterov,
1432        }
1433    }
1434
1435    /// Set the momentum parameter
1436    pub fn with_momentum(mut self, momentum: f64) -> Self {
1437        self.momentum = momentum;
1438        self
1439    }
1440
1441    /// Set the learning rate
1442    pub fn with_learning_rate(mut self, learning_rate: f64) -> Self {
1443        self.learning_rate = learning_rate;
1444        self
1445    }
1446
1447    /// Set the accelerated method type
1448    pub fn with_method(mut self, method: AcceleratedMethod) -> Self {
1449        self.method = method;
1450        self
1451    }
1452
1453    /// Fit the SVM using accelerated gradient optimization
1454    pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<OptimizationResult> {
1455        // Validate inputs
1456        if x.nrows() != y.len() {
1457            return Err(SklearsError::InvalidInput(
1458                "Number of samples must match number of labels".to_string(),
1459            ));
1460        }
1461
1462        let n_samples = x.nrows();
1463        self.kernel = Some(self.config.kernel.clone());
1464
1465        // Compute kernel matrix
1466        let k_matrix = self.compute_kernel_matrix(x)?;
1467
1468        // Initialize dual variables
1469        let mut alpha: Array1<f64> = Array1::zeros(n_samples);
1470        let mut t = 1.0; // FISTA parameter
1471        let mut history = Vec::new();
1472
1473        // Adaptive learning rate
1474        let mut current_lr = self.learning_rate;
1475
1476        // Accelerated gradient iterations
1477        for iteration in 0..self.config.max_iter {
1478            // Compute gradient at current point
1479            let gradient = self.compute_dual_gradient(&k_matrix, &alpha);
1480
1481            // Calculate objective value
1482            let objective = self.calculate_objective(&k_matrix, &alpha)?;
1483            history.push(objective);
1484
1485            if self.config.verbose && iteration % 10 == 0 {
1486                println!(
1487                    "Accelerated Gradient Iteration {}: Objective = {:.6}",
1488                    iteration, objective
1489                );
1490            }
1491
1492            // Store previous value
1493            let alpha_prev = alpha.clone();
1494
1495            // Update based on method type
1496            match self.method {
1497                AcceleratedMethod::Nesterov => {
1498                    // Nesterov's accelerated gradient method
1499                    let momentum_coeff = if iteration == 0 { 0.0 } else { self.momentum };
1500
1501                    // Compute momentum term
1502                    let momentum_term = (&alpha - &alpha_prev) * momentum_coeff;
1503
1504                    // Update with momentum
1505                    let y_k = &alpha + &momentum_term;
1506
1507                    // Gradient step
1508                    let gradient_at_y = self.compute_dual_gradient(&k_matrix, &y_k);
1509                    alpha = &y_k - &(&gradient_at_y * current_lr);
1510                }
1511                AcceleratedMethod::FISTA => {
1512                    // Fast Iterative Shrinkage-Thresholding Algorithm
1513                    let gradient_step = &alpha - &(&gradient * current_lr);
1514
1515                    // Proximal operator (projection onto constraint set)
1516                    let alpha_new = self.proximal_operator(&gradient_step)?;
1517
1518                    // Update FISTA parameter
1519                    let t_new = (1.0_f64 + (1.0_f64 + 4.0_f64 * t * t).sqrt()) / 2.0_f64;
1520                    let beta = (t - 1.0) / t_new;
1521
1522                    // Extrapolation point for the next iterate (Nesterov momentum).
1523                    let _y_k = &alpha_new + &((&alpha_new - &alpha) * beta);
1524                    alpha = alpha_new;
1525                    t = t_new;
1526                }
1527                AcceleratedMethod::HeavyBall => {
1528                    // Heavy ball method
1529                    let momentum_coeff = if iteration == 0 { 0.0 } else { self.momentum };
1530
1531                    // Update with momentum
1532                    let alpha_new = &(&alpha - &(&gradient * current_lr))
1533                        + &((&alpha - &alpha_prev) * momentum_coeff);
1534                    alpha = alpha_new;
1535                }
1536            }
1537
1538            // Project onto constraint set [0, C]
1539            for i in 0..n_samples {
1540                alpha[i] = alpha[i].max(0.0).min(self.config.c);
1541            }
1542
1543            // Check convergence
1544            let gradient_norm = gradient.dot(&gradient).sqrt();
1545            if gradient_norm < self.config.tol {
1546                if self.config.verbose {
1547                    println!(
1548                        "Accelerated Gradient converged after {} iterations",
1549                        iteration + 1
1550                    );
1551                }
1552
1553                self.is_fitted = true;
1554
1555                let support_indices = self.find_support_vectors(&alpha)?;
1556                let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
1557
1558                return Ok(OptimizationResult {
1559                    dual_coef: alpha,
1560                    intercept,
1561                    support_indices,
1562                    n_iterations: iteration + 1,
1563                    objective_value: objective,
1564                    converged: true,
1565                    history,
1566                });
1567            }
1568
1569            // Adaptive learning rate adjustment
1570            if iteration > 0 && history.len() >= 2 {
1571                let prev_obj = history[history.len() - 2];
1572                let curr_obj = history[history.len() - 1];
1573
1574                // If objective increases, reduce learning rate
1575                if curr_obj > prev_obj {
1576                    current_lr *= 0.8;
1577                } else if curr_obj < prev_obj && (prev_obj - curr_obj) / prev_obj.abs() > 0.01 {
1578                    // If significant improvement, slightly increase learning rate
1579                    current_lr *= 1.05;
1580                }
1581
1582                // Keep learning rate within bounds
1583                current_lr = current_lr.clamp(1e-6, 1.0);
1584            }
1585        }
1586
1587        self.is_fitted = true;
1588
1589        // Return result even if not converged
1590        let support_indices = self.find_support_vectors(&alpha)?;
1591        let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
1592
1593        Ok(OptimizationResult {
1594            dual_coef: alpha,
1595            intercept,
1596            support_indices,
1597            n_iterations: self.config.max_iter,
1598            objective_value: history.last().copied().unwrap_or(0.0),
1599            converged: false,
1600            history,
1601        })
1602    }
1603
1604    /// Proximal operator for FISTA (projection onto constraint set)
1605    fn proximal_operator(&self, x: &Array1<f64>) -> Result<Array1<f64>> {
1606        let mut result = x.clone();
1607
1608        // Project onto box constraints [0, C]
1609        for i in 0..result.len() {
1610            result[i] = result[i].max(0.0).min(self.config.c);
1611        }
1612
1613        Ok(result)
1614    }
1615
1616    /// Compute dual gradient for SVM
1617    fn compute_dual_gradient(&self, k_matrix: &Array2<f64>, alpha: &Array1<f64>) -> Array1<f64> {
1618        let n = alpha.len();
1619        let mut gradient = Array1::from_elem(n, -1.0); // -e vector
1620
1621        // Add Q*alpha term where Q = K (kernel matrix)
1622        for i in 0..n {
1623            for j in 0..n {
1624                gradient[i] += k_matrix[[i, j]] * alpha[j];
1625            }
1626        }
1627
1628        gradient
1629    }
1630
1631    /// Compute kernel matrix
1632    fn compute_kernel_matrix(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1633        let kernel_type = self
1634            .kernel
1635            .as_ref()
1636            .ok_or_else(|| SklearsError::NotFitted {
1637                operation: "compute_kernel_matrix".to_string(),
1638            })?;
1639        let kernel = create_kernel(kernel_type.clone())?;
1640        let n = x.nrows();
1641        let mut k_matrix = Array2::zeros((n, n));
1642
1643        for i in 0..n {
1644            for j in 0..n {
1645                k_matrix[[i, j]] = kernel.compute(x.row(i), x.row(j));
1646            }
1647        }
1648
1649        Ok(k_matrix)
1650    }
1651
1652    /// Calculate objective value
1653    fn calculate_objective(&self, k_matrix: &Array2<f64>, alpha: &Array1<f64>) -> Result<f64> {
1654        let n = alpha.len();
1655        let mut objective = 0.0;
1656
1657        // Dual objective: maximize Σαᵢ - (1/2)Σᵢⱼ αᵢαⱼyᵢyⱼK(xᵢ,xⱼ)
1658        for i in 0..n {
1659            objective += alpha[i]; // Linear term
1660            for j in 0..n {
1661                objective -= 0.5 * alpha[i] * alpha[j] * k_matrix[[i, j]]; // Quadratic term
1662            }
1663        }
1664
1665        Ok(objective)
1666    }
1667
1668    /// Find support vectors
1669    fn find_support_vectors(&self, alpha: &Array1<f64>) -> Result<Vec<usize>> {
1670        let mut support_indices = Vec::new();
1671        let tol = 1e-6;
1672
1673        for (i, &alpha_i) in alpha.iter().enumerate() {
1674            if alpha_i > tol && alpha_i < self.config.c - tol {
1675                support_indices.push(i);
1676            }
1677        }
1678
1679        Ok(support_indices)
1680    }
1681
1682    /// Calculate intercept
1683    fn calculate_intercept(
1684        &self,
1685        x: &Array2<f64>,
1686        y: &Array1<f64>,
1687        alpha: &Array1<f64>,
1688        support_indices: &[usize],
1689    ) -> Result<f64> {
1690        if support_indices.is_empty() {
1691            return Ok(0.0);
1692        }
1693
1694        let kernel_type = self
1695            .kernel
1696            .as_ref()
1697            .ok_or_else(|| SklearsError::NotFitted {
1698                operation: "calculate_intercept".to_string(),
1699            })?;
1700        let kernel = create_kernel(kernel_type.clone())?;
1701        let mut intercept_sum = 0.0;
1702
1703        for &sv_idx in support_indices {
1704            let mut kernel_sum = 0.0;
1705            for (j, &alpha_j) in alpha.iter().enumerate() {
1706                if alpha_j > 0.0 {
1707                    kernel_sum += alpha_j * y[j] * kernel.compute(x.row(sv_idx), x.row(j));
1708                }
1709            }
1710            intercept_sum += y[sv_idx] - kernel_sum;
1711        }
1712
1713        Ok(intercept_sum / support_indices.len() as f64)
1714    }
1715
1716    /// Make predictions
1717    pub fn predict(&self, x: &Array2<f64>, result: &OptimizationResult) -> Result<Array1<f64>> {
1718        let decision_values = self.decision_function(x, result)?;
1719        let mut predictions = Array1::zeros(decision_values.len());
1720
1721        for (i, &val) in decision_values.iter().enumerate() {
1722            predictions[i] = if val >= 0.0 { 1.0 } else { -1.0 };
1723        }
1724
1725        Ok(predictions)
1726    }
1727
1728    /// Compute decision function
1729    pub fn decision_function(
1730        &self,
1731        x: &Array2<f64>,
1732        result: &OptimizationResult,
1733    ) -> Result<Array1<f64>> {
1734        let kernel_type = self
1735            .kernel
1736            .as_ref()
1737            .ok_or_else(|| SklearsError::NotFitted {
1738                operation: "decision_function".to_string(),
1739            })?;
1740        let kernel = create_kernel(kernel_type.clone())?;
1741        let n_test = x.nrows();
1742        let mut decision_values = Array1::zeros(n_test);
1743
1744        for i in 0..n_test {
1745            let mut sum = 0.0;
1746            for &j in &result.support_indices {
1747                sum += result.dual_coef[j] * kernel.compute(x.row(i), x.row(j));
1748            }
1749            decision_values[i] = sum + result.intercept;
1750        }
1751
1752        Ok(decision_values)
1753    }
1754}
1755
1756#[allow(non_snake_case)]
1757#[cfg(test)]
1758#[path = "advanced_optimization_tests.rs"]
1759mod tests;