1use crate::kernels::Kernel;
4use scirs2_core::ndarray::{Array1, Array2};
5use sklears_core::{
6 error::{Result, SklearsError},
7 types::Float,
8};
9use std::cell::RefCell;
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum WorkingSetStrategy {
15 FirstOrder,
17 SecondOrder,
19 Mixed,
21}
22
23#[derive(Debug, Clone)]
25pub struct SmoConfig {
26 pub c: Float,
28 pub tol: Float,
30 pub max_iter: usize,
32 pub cache_size: usize,
34 pub shrinking: bool,
36 pub working_set_strategy: WorkingSetStrategy,
38 pub early_stopping_tol: Float,
40 pub convergence_check_interval: usize,
42}
43
44impl Default for SmoConfig {
45 fn default() -> Self {
46 Self {
47 c: 1.0,
48 tol: 1e-3,
49 max_iter: 1000,
50 cache_size: 200,
51 shrinking: true,
52 working_set_strategy: WorkingSetStrategy::SecondOrder,
53 early_stopping_tol: 1e-4,
54 convergence_check_interval: 10,
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct SmoResult {
62 pub alpha: Array1<Float>,
64 pub b: Float,
66 pub support_indices: Vec<usize>,
68 pub n_iter: usize,
70 pub converged: bool,
72 pub objective_value: Float,
74 pub cache_hit_ratio: Float,
76}
77
78#[derive(Debug)]
80struct KernelCache {
81 cache: HashMap<(usize, usize), Float>,
82 max_size: usize,
83 hits: usize,
84 total_requests: usize,
85}
86
87impl KernelCache {
88 fn new(max_size: usize) -> Self {
89 Self {
90 cache: HashMap::with_capacity(max_size),
91 max_size,
92 hits: 0,
93 total_requests: 0,
94 }
95 }
96
97 fn get(&mut self, i: usize, j: usize) -> Option<Float> {
98 self.total_requests += 1;
99 let key = if i <= j { (i, j) } else { (j, i) };
100 if let Some(&value) = self.cache.get(&key) {
101 self.hits += 1;
102 Some(value)
103 } else {
104 None
105 }
106 }
107
108 fn insert(&mut self, i: usize, j: usize, value: Float) {
109 let key = if i <= j { (i, j) } else { (j, i) };
110
111 if self.cache.len() >= self.max_size {
112 if let Some(first_key) = self.cache.keys().next().copied() {
114 self.cache.remove(&first_key);
115 }
116 }
117
118 self.cache.insert(key, value);
119 }
120
121 fn hit_ratio(&self) -> Float {
122 if self.total_requests == 0 {
123 0.0
124 } else {
125 self.hits as Float / self.total_requests as Float
126 }
127 }
128}
129
130pub struct SmoSolver<K: Kernel> {
132 config: SmoConfig,
133 kernel: K,
134 x: Array2<Float>,
136 y: Array1<Float>,
137 alpha: Array1<Float>,
139 f: Array1<Float>, b: Float,
141 active_set: Vec<usize>,
143 inactive_set: Vec<usize>,
144 kernel_cache: RefCell<KernelCache>,
146 objective_values: Vec<Float>,
148 convergence_history: Vec<Float>,
149}
150
151impl<K: Kernel> SmoSolver<K> {
152 pub fn new(config: SmoConfig, kernel: K) -> Self {
154 let cache_size = (config.cache_size * 1024 * 1024) / (std::mem::size_of::<Float>() * 2); Self {
156 kernel_cache: RefCell::new(KernelCache::new(cache_size)),
157 config,
158 kernel,
159 x: Array2::zeros((0, 0)),
160 y: Array1::zeros(0),
161 alpha: Array1::zeros(0),
162 f: Array1::zeros(0),
163 b: 0.0,
164 active_set: Vec::new(),
165 inactive_set: Vec::new(),
166 objective_values: Vec::new(),
167 convergence_history: Vec::new(),
168 }
169 }
170
171 pub fn solve(&mut self, x: &Array2<Float>, y: &Array1<Float>) -> Result<SmoResult> {
173 self.solve_with_warm_start(x, y, None)
174 }
175
176 pub fn solve_with_warm_start(
178 &mut self,
179 x: &Array2<Float>,
180 y: &Array1<Float>,
181 warm_start_alpha: Option<&Array1<Float>>,
182 ) -> Result<SmoResult> {
183 let (n_samples, _n_features) = x.dim();
184
185 if n_samples != y.len() {
186 return Err(SklearsError::InvalidInput(
187 "Number of samples in X and y must match".to_string(),
188 ));
189 }
190
191 if n_samples == 0 {
192 return Err(SklearsError::InvalidInput(
193 "Cannot solve SVM with empty dataset".to_string(),
194 ));
195 }
196
197 if self.x.dim() == x.dim() {
199 self.x.assign(x); } else {
201 self.x = x.clone(); }
203
204 if self.y.dim() == y.dim() {
205 self.y.assign(y); } else {
207 self.y = y.clone(); }
209
210 if let Some(alpha_init) = warm_start_alpha {
212 if alpha_init.len() != n_samples {
213 return Err(SklearsError::InvalidInput(format!(
214 "Warm start alpha length {} does not match number of samples {}",
215 alpha_init.len(),
216 n_samples
217 )));
218 }
219
220 for &alpha in alpha_init.iter() {
222 if alpha < 0.0 || alpha > self.config.c + 1e-10 {
223 return Err(SklearsError::InvalidInput(format!(
224 "Invalid alpha value {} in warm start (must be between 0 and C={})",
225 alpha, self.config.c
226 )));
227 }
228 }
229
230 self.alpha = alpha_init.clone();
231
232 self.f = Array1::zeros(n_samples);
234 for i in 0..n_samples {
235 let mut f_i = 0.0;
236 for j in 0..n_samples {
237 if self.alpha[j] > 1e-10 {
238 let k_ij = self.get_kernel_value(i, j);
239 f_i += self.alpha[j] * self.y[j] * k_ij;
240 }
241 }
242 self.f[i] = f_i - self.y[i];
243 }
244
245 let support_indices = self.find_support_vectors();
247 self.update_bias(&support_indices)?;
248 } else {
249 self.alpha = Array1::zeros(n_samples);
251 self.f = -y.clone(); self.b = 0.0;
253 }
254
255 self.active_set = (0..n_samples).collect();
256 self.inactive_set.clear();
257 self.objective_values.clear();
258 self.convergence_history.clear();
259
260 let mut n_iter = 0; let mut loop_iter = 0; let mut converged = false;
263 let mut no_change_count = 0; while n_iter < self.config.max_iter && loop_iter < self.config.max_iter * 10 {
267 loop_iter += 1;
268
269 let (i, j) = match self.select_working_set() {
270 Some(pair) => pair,
271 None => {
272 converged = true;
273 break;
274 }
275 };
276
277 if self.take_step(i, j)? {
278 n_iter += 1;
279 no_change_count = 0; if n_iter % 5 == 0 {
283 let obj_val = self.compute_objective();
284 self.objective_values.push(obj_val);
285 }
286 } else {
287 no_change_count += 1;
288 if no_change_count > 100 {
290 converged = true;
291 break;
292 }
293 }
294
295 if self.config.shrinking && n_iter > 0 && n_iter % 100 == 0 {
297 self.apply_shrinking();
298 }
299
300 if n_iter > 0 && n_iter % self.config.convergence_check_interval == 0 {
302 let convergence_measure = self.compute_convergence_measure();
303 self.convergence_history.push(convergence_measure);
304
305 if self.check_convergence() {
306 converged = true;
307 break;
308 }
309
310 if self.should_early_stop() {
312 converged = true;
313 break;
314 }
315 }
316 }
317
318 let support_indices = self.find_support_vectors();
320
321 self.update_bias(&support_indices)?;
323
324 let final_objective = self.compute_objective();
325 let cache_hit_ratio = self.kernel_cache.borrow().hit_ratio();
326
327 Ok(SmoResult {
328 alpha: self.alpha.clone(),
329 b: self.b,
330 support_indices,
331 n_iter,
332 converged,
333 objective_value: final_objective,
334 cache_hit_ratio,
335 })
336 }
337
338 fn select_working_set(&self) -> Option<(usize, usize)> {
340 match self.config.working_set_strategy {
341 WorkingSetStrategy::FirstOrder => self.select_working_set_first_order(),
342 WorkingSetStrategy::SecondOrder => self.select_working_set_second_order(),
343 WorkingSetStrategy::Mixed => {
344 if self.objective_values.len().is_multiple_of(2) {
346 self.select_working_set_second_order()
347 } else {
348 self.select_working_set_first_order()
349 }
350 }
351 }
352 }
353
354 fn select_working_set_first_order(&self) -> Option<(usize, usize)> {
357 let mut i_up = None;
358 let mut i_low = None;
359 let mut g_min_up = Float::INFINITY; let mut g_max_low = Float::NEG_INFINITY; for &t in &self.active_set {
363 let alpha_t = self.alpha[t];
364 let y_t = self.y[t];
365 let f_t = self.f[t];
366 let g_t = y_t * f_t; if (y_t > 0.0 && alpha_t < self.config.c - self.config.tol)
372 || (y_t < 0.0 && alpha_t > self.config.tol)
373 {
374 if g_t < g_min_up {
376 g_min_up = g_t;
377 i_up = Some(t);
378 }
379 }
380
381 if (y_t < 0.0 && alpha_t < self.config.c - self.config.tol)
385 || (y_t > 0.0 && alpha_t > self.config.tol)
386 {
387 if g_t > g_max_low {
389 g_max_low = g_t;
390 i_low = Some(t);
391 }
392 }
393 }
394
395 match (i_up, i_low) {
397 (Some(i), Some(j)) => {
398 let gap = g_max_low - g_min_up;
401 if gap > self.config.tol
402 || (gap.abs() <= self.config.tol && g_min_up < 1.0 - self.config.tol)
403 {
404 Some((i, j))
405 } else {
406 None
407 }
408 }
409 _ => None,
410 }
411 }
412
413 fn select_working_set_second_order(&self) -> Option<(usize, usize)> {
416 let mut i_up = None;
418 let mut g_min_up = Float::INFINITY;
419
420 for &t in &self.active_set {
421 let alpha_t = self.alpha[t];
422 let y_t = self.y[t];
423 let f_t = self.f[t];
424 let g_t = y_t * f_t;
425
426 if ((y_t > 0.0 && alpha_t < self.config.c - self.config.tol)
428 || (y_t < 0.0 && alpha_t > self.config.tol))
429 && g_t < g_min_up
430 {
431 g_min_up = g_t;
432 i_up = Some(t);
433 }
434 }
435
436 let i = i_up?;
437
438 let mut best_j = None;
440 let mut best_decrease = -1e-10; let k_ii = self.get_kernel_value(i, i);
443 let g_i = g_min_up;
444
445 for &j in &self.active_set {
446 if i == j {
447 continue;
448 }
449
450 let alpha_j = self.alpha[j];
451 let y_j = self.y[j];
452 let f_j = self.f[j];
453 let g_j = y_j * f_j;
454
455 if !((y_j < 0.0 && alpha_j < self.config.c - self.config.tol)
457 || (y_j > 0.0 && alpha_j > self.config.tol))
458 {
459 continue;
460 }
461
462 let k_jj = self.get_kernel_value(j, j);
464 let k_ij = self.get_kernel_value(i, j);
465 let eta = k_ii + k_jj - 2.0 * k_ij;
466
467 if eta <= 0.0 {
468 continue; }
470
471 let grad_diff = g_j - g_i; let decrease = (grad_diff * grad_diff) / eta;
474
475 if decrease > best_decrease {
476 best_decrease = decrease;
477 best_j = Some(j);
478 }
479 }
480
481 if best_j.is_none() {
484 self.select_working_set_first_order()
485 } else {
486 best_j.map(|j| (i, j))
487 }
488 }
489
490 #[allow(dead_code)] fn estimate_objective_decrease(&self, i: usize, j: usize) -> Float {
493 let y_i = self.y[i];
494 let y_j = self.y[j];
495 let f_i = self.f[i];
496 let f_j = self.f[j];
497
498 let grad_diff = (y_i * f_i - 1.0).abs() + (y_j * f_j - 1.0).abs();
500 let f_diff = (f_i - f_j).abs();
501
502 grad_diff * f_diff
503 }
504
505 fn violates_kkt(&self, i: usize) -> bool {
507 let alpha_i = self.alpha[i];
508 let y_i = self.y[i];
509 let f_i = self.f[i];
510
511 let tol = self.config.tol;
512
513 if alpha_i < tol {
514 y_i * f_i < 1.0 - tol
516 } else if alpha_i > self.config.c - tol {
517 y_i * f_i > 1.0 + tol
519 } else {
520 (y_i * f_i - 1.0).abs() > tol
522 }
523 }
524
525 fn take_step(&mut self, i: usize, j: usize) -> Result<bool> {
527 if i == j {
528 return Ok(false);
529 }
530
531 let alpha_i_old = self.alpha[i];
532 let alpha_j_old = self.alpha[j];
533 let y_i = self.y[i];
534 let y_j = self.y[j];
535
536 let (l, h) = if y_i == y_j {
538 let gamma = alpha_i_old + alpha_j_old;
539 ((gamma - self.config.c).max(0.0), gamma.min(self.config.c))
540 } else {
541 let gamma = alpha_i_old - alpha_j_old;
542 (
543 (-gamma).max(0.0),
544 (self.config.c - gamma).min(self.config.c),
545 )
546 };
547
548 if (l - h).abs() < 1e-10 {
549 return Ok(false);
550 }
551
552 let k_ii = self.get_kernel_value(i, i);
554 let k_jj = self.get_kernel_value(j, j);
555 let k_ij = self.get_kernel_value(i, j);
556
557 let eta = k_ii + k_jj - 2.0 * k_ij;
559
560 let alpha_j_new = if eta > 0.0 {
561 let alpha_j_unc = alpha_j_old + y_j * (self.f[i] - self.f[j]) / eta;
563 alpha_j_unc.max(l).min(h)
564 } else {
565 let f1 = self.objective_at_endpoint(i, j, l)?;
567 let f2 = self.objective_at_endpoint(i, j, h)?;
568
569 if f1 < f2 - 1e-10 {
570 l
571 } else if f2 < f1 - 1e-10 {
572 h
573 } else {
574 alpha_j_old
575 }
576 };
577
578 if (alpha_j_new - alpha_j_old).abs() < 1e-10 {
579 return Ok(false);
580 }
581
582 let alpha_i_new = alpha_i_old + y_i * y_j * (alpha_j_old - alpha_j_new);
584
585 self.alpha[i] = alpha_i_new;
587 self.alpha[j] = alpha_j_new;
588
589 self.update_f_values(i, j, alpha_i_old, alpha_j_old)?;
591
592 Ok(true)
593 }
594
595 fn objective_at_endpoint(&self, i: usize, j: usize, alpha_j: Float) -> Result<Float> {
597 let y_i = self.y[i];
598 let y_j = self.y[j];
599 let alpha_i_old = self.alpha[i];
600 let alpha_j_old = self.alpha[j];
601
602 let alpha_i = alpha_i_old + y_i * y_j * (alpha_j_old - alpha_j);
603
604 let k_ii = self.get_kernel_value(i, i);
607 let k_jj = self.get_kernel_value(j, j);
608 let k_ij = self.get_kernel_value(i, j);
609
610 Ok(alpha_i * self.f[i] + alpha_j * self.f[j]
611 - 0.5
612 * (alpha_i * alpha_i * k_ii
613 + alpha_j * alpha_j * k_jj
614 + 2.0 * alpha_i * alpha_j * k_ij))
615 }
616
617 fn update_f_values(
619 &mut self,
620 i: usize,
621 j: usize,
622 alpha_i_old: Float,
623 alpha_j_old: Float,
624 ) -> Result<()> {
625 let delta_alpha_i = self.alpha[i] - alpha_i_old;
626 let delta_alpha_j = self.alpha[j] - alpha_j_old;
627
628 if delta_alpha_i.abs() < 1e-10 && delta_alpha_j.abs() < 1e-10 {
630 return Ok(());
631 }
632
633 for &k in &self.active_set {
635 let k_ik = self.get_kernel_value(i, k);
636 let k_jk = self.get_kernel_value(j, k);
637 self.f[k] += self.y[i] * delta_alpha_i * k_ik + self.y[j] * delta_alpha_j * k_jk;
638 }
639
640 if !self.config.shrinking {
642 for &k in &self.inactive_set {
643 let k_ik = self.get_kernel_value(i, k);
644 let k_jk = self.get_kernel_value(j, k);
645 self.f[k] += self.y[i] * delta_alpha_i * k_ik + self.y[j] * delta_alpha_j * k_jk;
646 }
647 }
648
649 Ok(())
650 }
651
652 fn check_convergence(&self) -> bool {
654 for &i in &self.active_set {
655 if self.violates_kkt(i) {
656 return false;
657 }
658 }
659 true
660 }
661
662 fn find_support_vectors(&self) -> Vec<usize> {
664 let mut support_indices = Vec::new();
665
666 for i in 0..self.alpha.len() {
667 if self.alpha[i] > 1e-10 {
668 support_indices.push(i);
669 }
670 }
671
672 support_indices
673 }
674
675 fn update_bias(&mut self, support_indices: &[usize]) -> Result<()> {
677 if support_indices.is_empty() {
678 self.b = 0.0;
679 return Ok(());
680 }
681
682 let mut bias_sum = 0.0;
684 let mut n_free = 0;
685
686 for &i in support_indices {
687 let alpha_i = self.alpha[i];
688 if alpha_i > 1e-10 && alpha_i < self.config.c - 1e-10 {
689 bias_sum += -self.f[i];
698 n_free += 1;
699 }
700 }
701
702 if n_free > 0 {
703 self.b = bias_sum / n_free as Float;
704 } else {
705 bias_sum = 0.0;
707 for &i in support_indices {
708 bias_sum += -self.f[i];
709 }
710 self.b = bias_sum / support_indices.len() as Float;
711 }
712
713 Ok(())
714 }
715
716 fn get_kernel_value(&self, i: usize, j: usize) -> Float {
718 let mut cache = self.kernel_cache.borrow_mut();
719 if let Some(value) = cache.get(i, j) {
720 value
721 } else {
722 let value = self.kernel.compute(self.x.row(i), self.x.row(j));
724 cache.insert(i, j, value);
725 value
726 }
727 }
728
729 fn apply_shrinking(&mut self) {
731 if !self.config.shrinking {
732 return;
733 }
734
735 let mut new_active = Vec::new();
736 let tol = self.config.tol;
737
738 for &i in &self.active_set {
739 let alpha_i = self.alpha[i];
740 let y_i = self.y[i];
741 let f_i = self.f[i];
742
743 let should_keep = if alpha_i < tol {
745 y_i * f_i < 1.0 + tol
747 } else if alpha_i > self.config.c - tol {
748 y_i * f_i > 1.0 - tol
750 } else {
751 true
753 };
754
755 if should_keep {
756 new_active.push(i);
757 } else {
758 self.inactive_set.push(i);
759 }
760 }
761
762 self.active_set = new_active;
763 }
764
765 fn compute_convergence_measure(&self) -> Float {
767 let mut max_violation: Float = 0.0;
768
769 for &i in &self.active_set {
770 let alpha_i = self.alpha[i];
771 let y_i = self.y[i];
772 let f_i = self.f[i];
773
774 let violation = if alpha_i < self.config.tol {
775 (1.0 - y_i * f_i).max(0.0)
776 } else if alpha_i > self.config.c - self.config.tol {
777 (y_i * f_i - 1.0).max(0.0)
778 } else {
779 (y_i * f_i - 1.0).abs()
780 };
781
782 max_violation = max_violation.max(violation);
783 }
784
785 max_violation
786 }
787
788 fn should_early_stop(&self) -> bool {
790 if self.convergence_history.len() < 3 {
791 return false;
792 }
793
794 let recent = &self.convergence_history[self.convergence_history.len() - 3..];
795 let improvement = recent[0] - recent[2];
796
797 improvement < self.config.early_stopping_tol
798 }
799
800 fn compute_objective(&mut self) -> Float {
802 let mut objective = 0.0;
803
804 for &alpha in self.alpha.iter() {
806 objective += alpha;
807 }
808
809 for i in 0..self.alpha.len() {
811 for j in 0..self.alpha.len() {
812 if self.alpha[i] > 1e-10 && self.alpha[j] > 1e-10 {
813 let k_ij = self.get_kernel_value(i, j);
814 objective -= 0.5 * self.alpha[i] * self.alpha[j] * self.y[i] * self.y[j] * k_ij;
815 }
816 }
817 }
818
819 objective
820 }
821}
822
823#[allow(non_snake_case)]
824#[cfg(test)]
825mod tests {
826 use super::*;
827 use crate::kernels::LinearKernel;
828 use approx::assert_abs_diff_eq;
829 use scirs2_core::ndarray::array;
830
831 #[test]
832 fn test_smo_linear_separable() {
833 let x = array![[1.0, 1.0], [2.0, 2.0], [-1.0, -1.0], [-2.0, -2.0],];
834 let y = array![1.0, 1.0, -1.0, -1.0];
835
836 let config = SmoConfig {
837 c: 1.0,
838 tol: 1e-3,
839 max_iter: 200,
840 working_set_strategy: WorkingSetStrategy::SecondOrder,
841 ..Default::default()
842 };
843
844 let mut solver = SmoSolver::new(config, LinearKernel::new());
845 let result = solver.solve(&x, &y).expect("solver should succeed");
846
847 assert!(result.converged || result.n_iter < 1000);
849 assert!(!result.support_indices.is_empty());
850 assert!(result.cache_hit_ratio >= 0.0);
851
852 for &alpha in result.alpha.iter() {
854 assert!(alpha >= -1e-10);
855 }
856 }
857
858 #[test]
859 fn test_working_set_strategies() {
860 let x = array![[1.0, 1.0], [2.0, 2.0], [-1.0, -1.0], [-2.0, -2.0],];
861 let y = array![1.0, 1.0, -1.0, -1.0];
862
863 for strategy in [
864 WorkingSetStrategy::FirstOrder,
865 WorkingSetStrategy::SecondOrder,
866 WorkingSetStrategy::Mixed,
867 ] {
868 let config = SmoConfig {
869 c: 1.0,
870 tol: 1e-2, max_iter: 500, working_set_strategy: strategy,
873 ..Default::default()
874 };
875
876 let mut solver = SmoSolver::new(config, LinearKernel::new());
877 let result = solver.solve(&x, &y).expect("solver should succeed");
878
879 assert!(
882 result.converged || !result.support_indices.is_empty(),
883 "Strategy {:?} should either converge or find support vectors",
884 strategy
885 );
886 }
887 }
888
889 #[test]
890 fn test_kernel_cache() {
891 let mut cache = KernelCache::new(10);
892
893 assert_eq!(cache.get(0, 1), None);
895 assert_eq!(cache.hit_ratio(), 0.0);
896
897 cache.insert(0, 1, 5.0);
899 assert_eq!(cache.get(0, 1), Some(5.0));
900 assert_eq!(cache.get(1, 0), Some(5.0)); assert!(cache.hit_ratio() > 0.0);
903 }
904
905 #[test]
906 fn test_smo_config_default() {
907 let config = SmoConfig::default();
908 assert_eq!(config.c, 1.0);
909 assert_eq!(config.tol, 1e-3);
910 assert_eq!(config.max_iter, 1000);
911 assert_eq!(config.working_set_strategy, WorkingSetStrategy::SecondOrder);
912 assert!(config.shrinking);
913 }
914
915 #[test]
916 fn test_smo_warm_start() {
917 let x = array![[1.0, 1.0], [2.0, 2.0], [-1.0, -1.0], [-2.0, -2.0],];
918 let y = array![1.0, 1.0, -1.0, -1.0];
919
920 let config = SmoConfig {
921 c: 1.0,
922 tol: 1e-2,
923 max_iter: 50,
924 ..Default::default()
925 };
926
927 let mut solver1 = SmoSolver::new(config.clone(), LinearKernel::new());
929 let result1 = solver1.solve(&x, &y).expect("solver should succeed");
930
931 let mut solver2 = SmoSolver::new(config, LinearKernel::new());
933 let result2 = solver2
934 .solve_with_warm_start(&x, &y, Some(&result1.alpha))
935 .expect("operation should succeed");
936
937 assert!(result2.n_iter <= result1.n_iter);
939
940 for (a1, a2) in result1.alpha.iter().zip(result2.alpha.iter()) {
942 assert_abs_diff_eq!(a1, a2, epsilon = 1e-1);
943 }
944 }
945
946 #[test]
947 fn test_smo_warm_start_invalid_alpha() {
948 let x = array![[1.0, 1.0], [2.0, 2.0]];
949 let y = array![1.0, -1.0];
950
951 let config = SmoConfig::default();
952 let mut solver = SmoSolver::new(config, LinearKernel::new());
953
954 let invalid_alpha = array![-0.1, 0.5];
956 let result = solver.solve_with_warm_start(&x, &y, Some(&invalid_alpha));
957 assert!(result.is_err());
958
959 let invalid_alpha = array![0.5, 2.0]; let result = solver.solve_with_warm_start(&x, &y, Some(&invalid_alpha));
962 assert!(result.is_err());
963
964 let invalid_alpha = array![0.1]; let result = solver.solve_with_warm_start(&x, &y, Some(&invalid_alpha));
967 assert!(result.is_err());
968 }
969}