1use crate::kernels::Kernel;
12#[cfg(feature = "parallel")]
13#[allow(unused_imports)]
14use rayon::prelude::*;
15use scirs2_core::ndarray::{Array1, Array2};
16use sklears_core::{error::Result, types::Float};
17use std::collections::HashMap;
18
19#[derive(Debug, Clone)]
21pub struct DecompositionConfig {
22 pub max_working_set_size: usize,
24 pub min_working_set_size: usize,
26 pub decomposition_levels: usize,
28 pub working_set_overlap: usize,
30 pub selection_strategy: WorkingSetSelectionStrategy,
32 pub max_iterations_per_step: usize,
34 pub tolerance: Float,
36 pub use_hierarchical: bool,
38 pub kernel_cache_size: usize,
40}
41
42impl Default for DecompositionConfig {
43 fn default() -> Self {
44 Self {
45 max_working_set_size: 2000,
46 min_working_set_size: 100,
47 decomposition_levels: 3,
48 working_set_overlap: 50,
49 selection_strategy: WorkingSetSelectionStrategy::MaximalViolating,
50 max_iterations_per_step: 1000,
51 tolerance: 1e-6,
52 use_hierarchical: true,
53 kernel_cache_size: 256,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy)]
60pub enum WorkingSetSelectionStrategy {
61 MaximalViolating,
63 WeightedRandom,
65 SteepestFeasible,
67 Hybrid,
69 BlockWise,
71}
72
73pub struct DecompositionSolver {
75 config: DecompositionConfig,
76 kernel: Box<dyn Kernel>,
77 working_sets: Vec<WorkingSet>,
78 #[allow(dead_code)] kernel_cache: HashMap<(usize, usize), Float>,
80 convergence_history: Vec<Float>,
81}
82
83impl DecompositionSolver {
84 pub fn new(kernel: Box<dyn Kernel>, config: DecompositionConfig) -> Self {
86 let cache_capacity = config.kernel_cache_size * config.kernel_cache_size;
87 Self {
88 config,
89 kernel,
90 working_sets: Vec::new(),
91 kernel_cache: HashMap::with_capacity(cache_capacity),
92 convergence_history: Vec::new(),
93 }
94 }
95
96 pub fn solve(
98 &mut self,
99 x: &Array2<Float>,
100 y: &Array1<Float>,
101 c: Float,
102 initial_alpha: Option<&Array1<Float>>,
103 ) -> Result<(Array1<Float>, Float)> {
104 let n_samples = x.nrows();
105
106 let mut alpha = initial_alpha
108 .cloned()
109 .unwrap_or_else(|| Array1::zeros(n_samples));
110
111 self.create_initial_decomposition(n_samples)?;
113
114 let mut iteration = 0;
116 let mut converged = false;
117
118 while !converged
119 && iteration < self.config.max_iterations_per_step * self.config.decomposition_levels
120 {
121 let mut global_change = 0.0;
123
124 for i in 0..self.working_sets.len() {
126 if !self.working_sets[i].active || self.working_sets[i].indices.len() < 2 {
127 continue;
128 }
129
130 let ws_size = self.working_sets[i].indices.len();
132 let mut ws_x = Array2::zeros((ws_size, x.ncols()));
133 let mut ws_y = Array1::zeros(ws_size);
134 let mut ws_alpha = Array1::zeros(ws_size);
135
136 for (j, &idx) in self.working_sets[i].indices.iter().enumerate() {
137 ws_x.row_mut(j).assign(&x.row(idx));
138 ws_y[j] = y[idx];
139 ws_alpha[j] = alpha[idx];
140 }
141
142 let new_alpha = self.optimize_working_set(&ws_x, &ws_y, &ws_alpha, c);
145
146 let change = (&new_alpha - &ws_alpha).mapv(|x| x.abs()).sum();
148
149 for (j, &idx) in self.working_sets[i].indices.iter().enumerate() {
151 alpha[idx] = new_alpha[j];
152 }
153
154 self.working_sets[i].last_change = change;
155 global_change += change;
156 }
157
158 self.update_working_sets(&alpha, x, y, c)?;
160
161 self.convergence_history.push(global_change);
163 converged = self.check_convergence(global_change);
164
165 iteration += 1;
166 }
167
168 let bias = self.compute_bias(&alpha, x, y, c)?;
170
171 Ok((alpha, bias))
172 }
173
174 fn optimize_working_set(
189 &self,
190 ws_x: &Array2<Float>,
191 ws_y: &Array1<Float>,
192 ws_alpha: &Array1<Float>,
193 c: Float,
194 ) -> Array1<Float> {
195 let n = ws_alpha.len();
196 if n < 2 {
197 return ws_alpha.clone();
198 }
199
200 let mut k = Array2::<Float>::zeros((n, n));
202 for i in 0..n {
203 for j in i..n {
204 let val = self
205 .kernel
206 .compute(ws_x.row(i).to_owned().view(), ws_x.row(j).to_owned().view());
207 k[[i, j]] = val;
208 k[[j, i]] = val;
209 }
210 }
211
212 let mut alpha = ws_alpha.clone();
213 let tol = self.config.tolerance;
214
215 let mut f = Array1::<Float>::zeros(n);
220 for i in 0..n {
221 let mut acc = -ws_y[i];
222 for j in 0..n {
223 if alpha[j] != 0.0 {
224 acc += alpha[j] * ws_y[j] * k[[i, j]];
225 }
226 }
227 f[i] = acc;
228 }
229
230 let max_inner = self.config.max_iterations_per_step.max(1);
231
232 for _iter in 0..max_inner {
233 let mut i_up = None;
239 let mut g_max = Float::NEG_INFINITY;
240 let mut j_low = None;
241 let mut g_min = Float::INFINITY;
242
243 for t in 0..n {
244 let yt = ws_y[t];
245 let in_up = (yt > 0.0 && alpha[t] < c - tol) || (yt < 0.0 && alpha[t] > tol);
246 let in_low = (yt > 0.0 && alpha[t] > tol) || (yt < 0.0 && alpha[t] < c - tol);
247 let grad = -yt * f[t];
248 if in_up && grad > g_max {
249 g_max = grad;
250 i_up = Some(t);
251 }
252 if in_low && grad < g_min {
253 g_min = grad;
254 j_low = Some(t);
255 }
256 }
257
258 if g_max - g_min < tol {
259 break;
260 }
261
262 let (i, j) = match (i_up, j_low) {
263 (Some(i), Some(j)) if i != j => (i, j),
264 _ => break,
265 };
266
267 let yi = ws_y[i];
268 let yj = ws_y[j];
269 let ai_old = alpha[i];
270 let aj_old = alpha[j];
271
272 let eta = k[[i, i]] + k[[j, j]] - 2.0 * k[[i, j]];
274 if eta <= 1e-12 {
275 break;
276 }
277
278 let aj_unc = aj_old + yj * (f[i] - f[j]) / eta;
281
282 let (low, high) = if yi != yj {
284 let diff = aj_old - ai_old;
285 (diff.max(0.0), c + (aj_old - ai_old).min(0.0))
286 } else {
287 let sum = ai_old + aj_old;
288 ((sum - c).max(0.0), sum.min(c))
289 };
290
291 let aj_new = aj_unc.clamp(low, high);
292 let ai_new = ai_old + yi * yj * (aj_old - aj_new);
294
295 let d_ai = ai_new - ai_old;
296 let d_aj = aj_new - aj_old;
297
298 if d_ai.abs() < 1e-12 && d_aj.abs() < 1e-12 {
299 break;
300 }
301
302 alpha[i] = ai_new;
303 alpha[j] = aj_new;
304
305 for t in 0..n {
307 f[t] += yi * d_ai * k[[t, i]] + yj * d_aj * k[[t, j]];
308 }
309 }
310
311 alpha
312 }
313
314 fn create_initial_decomposition(&mut self, n_samples: usize) -> Result<()> {
316 self.working_sets.clear();
317
318 match self.config.selection_strategy {
319 WorkingSetSelectionStrategy::BlockWise => {
320 self.create_block_decomposition(n_samples)?;
321 }
322 _ => {
323 self.create_overlapping_decomposition(n_samples)?;
324 }
325 }
326
327 Ok(())
328 }
329
330 fn create_block_decomposition(&mut self, n_samples: usize) -> Result<()> {
332 let block_size = self.config.max_working_set_size;
333 let mut start = 0;
334
335 while start < n_samples {
336 let end = (start + block_size).min(n_samples);
337 let indices: Vec<usize> = (start..end).collect();
338
339 self.working_sets.push(WorkingSet {
340 indices,
341 active: true,
342 last_change: Float::INFINITY,
343 priority: 1.0,
344 });
345
346 start = end;
347 }
348
349 Ok(())
350 }
351
352 fn create_overlapping_decomposition(&mut self, n_samples: usize) -> Result<()> {
354 let step_size = self.config.max_working_set_size - self.config.working_set_overlap;
355 let mut start = 0;
356
357 while start < n_samples {
358 let end = (start + self.config.max_working_set_size).min(n_samples);
359 let indices: Vec<usize> = (start..end).collect();
360
361 if indices.len() >= self.config.min_working_set_size {
362 self.working_sets.push(WorkingSet {
363 indices,
364 active: true,
365 last_change: Float::INFINITY,
366 priority: 1.0,
367 });
368 }
369
370 start += step_size;
371 }
372
373 Ok(())
374 }
375
376 #[allow(dead_code)] fn solve_working_set(
379 &mut self,
380 working_set: &mut WorkingSet,
381 x: &Array2<Float>,
382 y: &Array1<Float>,
383 alpha: &mut Array1<Float>,
384 c: Float,
385 ) -> Result<Float> {
386 if !working_set.active || working_set.indices.len() < 2 {
387 return Ok(0.0);
388 }
389
390 let ws_size = working_set.indices.len();
392 let mut ws_x = Array2::zeros((ws_size, x.ncols()));
393 let mut ws_y = Array1::zeros(ws_size);
394 let mut ws_alpha = Array1::zeros(ws_size);
395
396 for (i, &idx) in working_set.indices.iter().enumerate() {
397 ws_x.row_mut(i).assign(&x.row(idx));
398 ws_y[i] = y[idx];
399 ws_alpha[i] = alpha[idx];
400 }
401
402 let new_alpha = self.optimize_working_set(&ws_x, &ws_y, &ws_alpha, c);
405
406 let change = (&new_alpha - &ws_alpha).mapv(|x| x.abs()).sum();
408
409 for (i, &idx) in working_set.indices.iter().enumerate() {
411 alpha[idx] = new_alpha[i];
412 }
413
414 working_set.last_change = change;
415 Ok(change)
416 }
417
418 #[allow(dead_code)] fn compute_cached_kernel_matrix(
421 &mut self,
422 x: &Array2<Float>,
423 indices: &[usize],
424 ) -> Array2<Float> {
425 let n = x.nrows();
426 let mut kernel_matrix = Array2::zeros((n, n));
427
428 for i in 0..n {
429 for j in i..n {
430 let key = (indices[i].min(indices[j]), indices[i].max(indices[j]));
431
432 let k_val = if let Some(&cached_val) = self.kernel_cache.get(&key) {
433 cached_val
434 } else {
435 let val = self
436 .kernel
437 .compute(x.row(i).to_owned().view(), x.row(j).to_owned().view());
438
439 if self.kernel_cache.len()
441 >= self.config.kernel_cache_size * self.config.kernel_cache_size
442 {
443 if let Some(key_to_remove) = self.kernel_cache.keys().next().copied() {
445 self.kernel_cache.remove(&key_to_remove);
446 }
447 }
448
449 self.kernel_cache.insert(key, val);
450 val
451 };
452
453 kernel_matrix[[i, j]] = k_val;
454 kernel_matrix[[j, i]] = k_val;
455 }
456 }
457
458 kernel_matrix
459 }
460
461 fn update_working_sets(
463 &mut self,
464 alpha: &Array1<Float>,
465 x: &Array2<Float>,
466 y: &Array1<Float>,
467 c: Float,
468 ) -> Result<()> {
469 let violations = self.compute_kkt_violations(alpha, x, y, c)?;
471
472 for working_set in &mut self.working_sets {
474 let avg_violation: Float = working_set
475 .indices
476 .iter()
477 .map(|&i| violations[i])
478 .sum::<Float>()
479 / working_set.indices.len() as Float;
480
481 working_set.priority = avg_violation;
482 working_set.active = working_set.last_change > self.config.tolerance * 0.1
483 || avg_violation > self.config.tolerance;
484 }
485
486 if self.config.use_hierarchical {
488 self.create_adaptive_working_sets(&violations)?;
489 }
490
491 Ok(())
492 }
493
494 fn compute_kkt_violations(
496 &self,
497 alpha: &Array1<Float>,
498 x: &Array2<Float>,
499 y: &Array1<Float>,
500 c: Float,
501 ) -> Result<Array1<Float>> {
502 let n_samples = x.nrows();
503 let mut violations = Array1::zeros(n_samples);
504
505 let mut decision_values: Array1<Float> = Array1::zeros(n_samples);
507 for i in 0..n_samples {
508 for j in 0..n_samples {
509 if alpha[j] > 0.0 {
510 let k_val = self
511 .kernel
512 .compute(x.row(i).to_owned().view(), x.row(j).to_owned().view());
513 decision_values[i] += alpha[j] * y[j] * k_val;
514 }
515 }
516 }
517
518 for i in 0..n_samples {
520 let yi_f: Float = y[i] * decision_values[i];
521
522 let one = 1.0 as Float;
523 let zero = 0.0 as Float;
524
525 violations[i] = if alpha[i] < 1e-8 {
526 (one - yi_f).max(zero)
528 } else if alpha[i] > c - 1e-8 {
529 (yi_f - one).max(zero)
531 } else {
532 (one - yi_f).abs()
534 };
535 }
536
537 Ok(violations)
538 }
539
540 fn create_adaptive_working_sets(&mut self, violations: &Array1<Float>) -> Result<()> {
542 let n_samples = violations.len();
543 let threshold = violations.iter().fold(0.0, |acc, &x| acc + x) / n_samples as Float;
544
545 let high_violation_indices: Vec<usize> = (0..n_samples)
547 .filter(|&i| violations[i] > threshold * 2.0)
548 .collect();
549
550 if high_violation_indices.len() >= self.config.min_working_set_size {
551 let mut new_working_set = WorkingSet {
553 indices: high_violation_indices,
554 active: true,
555 last_change: Float::INFINITY,
556 priority: threshold * 2.0,
557 };
558
559 if new_working_set.indices.len() > self.config.max_working_set_size {
561 new_working_set
562 .indices
563 .truncate(self.config.max_working_set_size);
564 }
565
566 self.working_sets.push(new_working_set);
567 }
568
569 Ok(())
570 }
571
572 fn check_convergence(&self, change: Float) -> bool {
574 if self.convergence_history.len() < 3 {
575 return false;
576 }
577
578 if change < self.config.tolerance {
580 return true;
581 }
582
583 let recent_changes: Float =
585 self.convergence_history.iter().rev().take(3).sum::<Float>() / 3.0;
586
587 recent_changes < self.config.tolerance * 10.0
588 }
589
590 fn compute_bias(
592 &self,
593 alpha: &Array1<Float>,
594 x: &Array2<Float>,
595 y: &Array1<Float>,
596 c: Float,
597 ) -> Result<Float> {
598 let n_samples = x.nrows();
599 let mut bias_sum = 0.0;
600 let mut n_free_sv = 0;
601
602 for i in 0..n_samples {
603 if alpha[i] > 1e-8 && alpha[i] < c - 1e-8 {
604 let mut decision_value = 0.0;
606 for j in 0..n_samples {
607 if alpha[j] > 1e-8 {
608 let k_val = self
609 .kernel
610 .compute(x.row(i).to_owned().view(), x.row(j).to_owned().view());
611 decision_value += alpha[j] * y[j] * k_val;
612 }
613 }
614 bias_sum += y[i] - decision_value;
615 n_free_sv += 1;
616 }
617 }
618
619 if n_free_sv > 0 {
620 Ok(bias_sum / n_free_sv as Float)
621 } else {
622 Ok(0.0)
623 }
624 }
625
626 pub fn get_convergence_history(&self) -> &[Float] {
628 &self.convergence_history
629 }
630
631 pub fn get_active_working_sets(&self) -> usize {
633 self.working_sets.iter().filter(|ws| ws.active).count()
634 }
635}
636
637#[derive(Debug, Clone)]
639struct WorkingSet {
640 indices: Vec<usize>,
642 active: bool,
644 last_change: Float,
646 priority: Float,
648}
649
650pub struct HierarchicalDecomposer {
652 levels: Vec<DecompositionLevel>,
653 config: DecompositionConfig,
654}
655
656impl HierarchicalDecomposer {
657 pub fn new(config: DecompositionConfig) -> Self {
659 Self {
660 levels: Vec::new(),
661 config,
662 }
663 }
664
665 pub fn decompose(&mut self, n_samples: usize) -> Result<()> {
667 self.levels.clear();
668
669 let mut current_size = n_samples;
670 let mut level = 0;
671
672 while current_size > self.config.max_working_set_size
673 && level < self.config.decomposition_levels
674 {
675 let reduction_factor =
676 (self.config.max_working_set_size as Float / current_size as Float).sqrt();
677 let new_size = (current_size as Float * reduction_factor).ceil() as usize;
678
679 self.levels.push(DecompositionLevel {
680 level,
681 original_size: current_size,
682 reduced_size: new_size,
683 reduction_factor,
684 mapping: self.create_level_mapping(current_size, new_size)?,
685 });
686
687 current_size = new_size;
688 level += 1;
689 }
690
691 Ok(())
692 }
693
694 fn create_level_mapping(&self, from_size: usize, to_size: usize) -> Result<Vec<Vec<usize>>> {
696 let cluster_size = (from_size as Float / to_size as Float).ceil() as usize;
697 let mut mapping = Vec::with_capacity(to_size);
698
699 for i in 0..to_size {
700 let start = i * cluster_size;
701 let end = ((i + 1) * cluster_size).min(from_size);
702 mapping.push((start..end).collect());
703 }
704
705 Ok(mapping)
706 }
707}
708
709#[derive(Debug, Clone)]
711struct DecompositionLevel {
712 #[allow(dead_code)] level: usize,
714 #[allow(dead_code)] original_size: usize,
716 #[allow(dead_code)] reduced_size: usize,
718 #[allow(dead_code)] reduction_factor: Float,
720 #[allow(dead_code)] mapping: Vec<Vec<usize>>,
722}
723
724#[allow(non_snake_case)]
725#[cfg(test)]
726mod tests {
727 use super::*;
728 use crate::kernels::{LinearKernel, RbfKernel};
729
730 #[test]
731 fn test_decomposition_solver_creation() {
732 let kernel = Box::new(LinearKernel);
733 let config = DecompositionConfig::default();
734 let solver = DecompositionSolver::new(kernel, config);
735 assert_eq!(solver.working_sets.len(), 0);
736 }
737
738 #[test]
739 fn test_block_decomposition() {
740 let kernel = Box::new(RbfKernel::new(1.0));
741 let config = DecompositionConfig {
742 max_working_set_size: 100,
743 selection_strategy: WorkingSetSelectionStrategy::BlockWise,
744 ..DecompositionConfig::default()
745 };
746
747 let mut solver = DecompositionSolver::new(kernel, config);
748 solver
749 .create_initial_decomposition(250)
750 .expect("operation should succeed");
751
752 assert_eq!(solver.working_sets.len(), 3); }
754
755 #[test]
756 fn test_hierarchical_decomposer() {
757 let config = DecompositionConfig::default();
758 let mut decomposer = HierarchicalDecomposer::new(config);
759
760 decomposer
761 .decompose(10000)
762 .expect("operation should succeed");
763 assert!(!decomposer.levels.is_empty());
764 }
765}