Skip to main content

libsvm_rs/
solver.rs

1//! SMO solver for the SVM dual problem.
2//!
3//! Implements the Fan et al. (JMLR 2005) algorithm with WSS3 working-set
4//! selection, shrinking heuristic, and both Standard and Nu variants.
5//!
6//! This is a faithful translation of `Solver` and `Solver_NU` from
7//! LIBSVM's `svm.cpp` (lines 362–1265).
8
9use crate::qmatrix::QMatrix;
10
11const TAU: f64 = 1e-12;
12const INF: f64 = f64::INFINITY;
13
14/// Result of the solver.
15#[derive(Debug, Clone)]
16pub struct SolutionInfo {
17    /// Final dual objective value reported like LIBSVM's `SolutionInfo::obj`.
18    pub obj: f64,
19    /// Bias term (`rho`) for the solved decision function.
20    pub rho: f64,
21    /// Positive-class box constraint used by the solved sub-problem.
22    pub upper_bound_p: f64,
23    /// Negative-class box constraint used by the solved sub-problem.
24    pub upper_bound_n: f64,
25    /// Extra value for Nu solver: `(r1 + r2) / 2`.
26    pub r: f64,
27}
28
29/// Alpha variable status relative to its box constraint.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31enum AlphaStatus {
32    LowerBound,
33    UpperBound,
34    Free,
35}
36
37/// Standard vs Nu solver variant.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum SolverVariant {
40    /// Standard LIBSVM `Solver` variant for C-SVC, one-class SVM, and ε-SVR.
41    Standard,
42    /// LIBSVM `Solver_NU` variant for ν-SVC and ν-SVR.
43    Nu,
44}
45
46/// SMO solver.
47pub struct Solver<'a> {
48    l: usize,
49    active_size: usize,
50    variant: SolverVariant,
51
52    y: Vec<i8>,
53    g: Vec<f64>,
54    g_bar: Vec<f64>,
55    alpha: Vec<f64>,
56    alpha_status: Vec<AlphaStatus>,
57    p: Vec<f64>,
58    active_set: Vec<usize>,
59    unshrink: bool,
60
61    q: Box<dyn QMatrix + 'a>,
62    qd: Vec<f64>,
63    cp: f64,
64    cn: f64,
65    eps: f64,
66}
67
68#[allow(clippy::needless_range_loop)]
69impl<'a> Solver<'a> {
70    /// Run the SMO solver.
71    ///
72    /// # Arguments
73    /// * `variant` — Standard or Nu
74    /// * `l` — problem size
75    /// * `q` — Q matrix (ownership transferred)
76    /// * `p_` — linear term
77    /// * `y_` — labels (+1/-1)
78    /// * `alpha_` — initial alpha (modified in place with solution)
79    /// * `cp`, `cn` — box constraints for positive/negative classes
80    /// * `eps` — stopping tolerance
81    /// * `shrinking` — whether to use the shrinking heuristic
82    #[allow(clippy::too_many_arguments)]
83    pub fn solve(
84        variant: SolverVariant,
85        l: usize,
86        q: Box<dyn QMatrix + 'a>,
87        p_: &[f64],
88        y_: &[i8],
89        alpha_: &mut [f64],
90        cp: f64,
91        cn: f64,
92        eps: f64,
93        shrinking: bool,
94    ) -> SolutionInfo {
95        let qd = q.get_qd().to_vec();
96        let p = p_.to_vec();
97        let y = y_.to_vec();
98        let alpha = alpha_.to_vec();
99
100        let mut solver = Solver {
101            l,
102            active_size: l,
103            variant,
104            y,
105            g: vec![0.0; l],
106            g_bar: vec![0.0; l],
107            alpha,
108            alpha_status: vec![AlphaStatus::LowerBound; l],
109            p,
110            active_set: (0..l).collect(),
111            unshrink: false,
112            q,
113            qd,
114            cp,
115            cn,
116            eps,
117        };
118
119        // Initialize alpha_status
120        for i in 0..l {
121            solver.update_alpha_status(i);
122        }
123
124        // Initialize gradient
125        for i in 0..l {
126            solver.g[i] = solver.p[i];
127        }
128
129        for i in 0..l {
130            if !solver.is_lower_bound(i) {
131                let alpha_i = solver.alpha[i];
132                let q_i = solver.q.get_q(i, l).to_vec();
133                for j in 0..l {
134                    solver.g[j] += alpha_i * q_i[j] as f64;
135                }
136                if solver.is_upper_bound(i) {
137                    let c_i = solver.get_c(i);
138                    for j in 0..l {
139                        solver.g_bar[j] += c_i * q_i[j] as f64;
140                    }
141                }
142            }
143        }
144
145        // Main SMO loop
146        let max_iter = 10_000_000usize.max(if l > i32::MAX as usize / 100 {
147            usize::MAX
148        } else {
149            100 * l
150        });
151        let mut counter = l.min(1000) + 1;
152        let mut iter = 0usize;
153
154        while iter < max_iter {
155            // Show progress and do shrinking
156            counter -= 1;
157            if counter == 0 {
158                counter = l.min(1000);
159                if shrinking {
160                    solver.do_shrinking();
161                }
162            }
163
164            let (wi, wj) = match solver.select_working_set() {
165                Some(pair) => pair,
166                None => {
167                    // Reconstruct gradient and retry
168                    solver.reconstruct_gradient();
169                    solver.active_size = l;
170                    match solver.select_working_set() {
171                        Some(pair) => {
172                            counter = 1; // do shrinking next iteration
173                            pair
174                        }
175                        None => break, // optimal
176                    }
177                }
178            };
179
180            iter += 1;
181
182            // Update alpha[i] and alpha[j]
183            solver.update_alpha_pair(wi, wj);
184        }
185
186        if iter >= max_iter {
187            if solver.active_size < l {
188                solver.reconstruct_gradient();
189                solver.active_size = l;
190            }
191            crate::info("WARNING: reaching max number of iterations\n");
192        }
193
194        // Calculate rho
195        let (rho, r) = solver.calculate_rho();
196
197        // Calculate objective value
198        let obj = {
199            let mut v = 0.0;
200            for i in 0..l {
201                v += solver.alpha[i] * (solver.g[i] + solver.p[i]);
202            }
203            v / 2.0
204        };
205
206        // Put back the solution via active_set mapping
207        for i in 0..l {
208            alpha_[solver.active_set[i]] = solver.alpha[i];
209        }
210
211        let si = SolutionInfo {
212            obj,
213            rho,
214            upper_bound_p: cp,
215            upper_bound_n: cn,
216            r,
217        };
218
219        crate::info(&format!("optimization finished, #iter = {}\n", iter));
220
221        si
222    }
223
224    // ─── Helper methods ─────────────────────────────────────────────
225
226    #[inline]
227    fn get_c(&self, i: usize) -> f64 {
228        if self.y[i] > 0 {
229            self.cp
230        } else {
231            self.cn
232        }
233    }
234
235    #[inline]
236    fn update_alpha_status(&mut self, i: usize) {
237        self.alpha_status[i] = if self.alpha[i] >= self.get_c(i) {
238            AlphaStatus::UpperBound
239        } else if self.alpha[i] <= 0.0 {
240            AlphaStatus::LowerBound
241        } else {
242            AlphaStatus::Free
243        };
244    }
245
246    #[inline]
247    fn is_upper_bound(&self, i: usize) -> bool {
248        self.alpha_status[i] == AlphaStatus::UpperBound
249    }
250
251    #[inline]
252    fn is_lower_bound(&self, i: usize) -> bool {
253        self.alpha_status[i] == AlphaStatus::LowerBound
254    }
255
256    #[inline]
257    fn is_free(&self, i: usize) -> bool {
258        self.alpha_status[i] == AlphaStatus::Free
259    }
260
261    fn swap_index(&mut self, i: usize, j: usize) {
262        self.q.swap_index(i, j);
263        self.y.swap(i, j);
264        self.g.swap(i, j);
265        self.alpha_status.swap(i, j);
266        self.alpha.swap(i, j);
267        self.p.swap(i, j);
268        self.active_set.swap(i, j);
269        self.g_bar.swap(i, j);
270        self.qd.swap(i, j);
271    }
272
273    fn reconstruct_gradient(&mut self) {
274        if self.active_size == self.l {
275            return;
276        }
277
278        for j in self.active_size..self.l {
279            self.g[j] = self.g_bar[j] + self.p[j];
280        }
281
282        let mut nr_free = 0;
283        for j in 0..self.active_size {
284            if self.is_free(j) {
285                nr_free += 1;
286            }
287        }
288
289        if 2 * nr_free < self.active_size {
290            crate::info("WARNING: using -h 0 may be faster\n");
291        }
292
293        let active_size = self.active_size;
294        let l = self.l;
295
296        if nr_free * l > 2 * active_size * (l - active_size) {
297            for i in active_size..l {
298                let q_i = self.q.get_q(i, active_size).to_vec();
299                for j in 0..active_size {
300                    if self.is_free(j) {
301                        self.g[i] += self.alpha[j] * q_i[j] as f64;
302                    }
303                }
304            }
305        } else {
306            for i in 0..active_size {
307                if self.is_free(i) {
308                    let q_i = self.q.get_q(i, l).to_vec();
309                    let alpha_i = self.alpha[i];
310                    for j in active_size..l {
311                        self.g[j] += alpha_i * q_i[j] as f64;
312                    }
313                }
314            }
315        }
316    }
317
318    // ─── Working set selection ──────────────────────────────────────
319
320    /// Select working set (i, j). Returns None if already optimal.
321    fn select_working_set(&mut self) -> Option<(usize, usize)> {
322        match self.variant {
323            SolverVariant::Standard => self.select_working_set_standard(),
324            SolverVariant::Nu => self.select_working_set_nu(),
325        }
326    }
327
328    fn select_working_set_standard(&mut self) -> Option<(usize, usize)> {
329        let mut gmax = -INF;
330        let mut gmax2 = -INF;
331        let mut gmax_idx: Option<usize> = None;
332        let mut gmin_idx: Option<usize> = None;
333        let mut obj_diff_min = INF;
334
335        // First pass: find i (maximizes -y_i * grad(f)_i in I_up)
336        // Slice to active_size so the compiler can drop per-index bounds checks.
337        let n = self.active_size;
338        let (y, g, status) = (&self.y[..n], &self.g[..n], &self.alpha_status[..n]);
339        for t in 0..n {
340            if y[t] == 1 {
341                if status[t] != AlphaStatus::UpperBound && -g[t] >= gmax {
342                    gmax = -g[t];
343                    gmax_idx = Some(t);
344                }
345            } else if status[t] != AlphaStatus::LowerBound && g[t] >= gmax {
346                gmax = g[t];
347                gmax_idx = Some(t);
348            }
349        }
350
351        let i = gmax_idx?;
352        let q_i = self.q.get_q(i, self.active_size).to_vec();
353
354        // Second pass: find j (minimizes objective decrease)
355        // Reuse `y`/`g`/`status` sliced to `n`; index values are < n.
356        let qd_i = self.qd[i];
357        let yi = y[i] as f64;
358        for j in 0..n {
359            if y[j] == 1 {
360                if status[j] != AlphaStatus::LowerBound {
361                    let grad_diff = gmax + g[j];
362                    if g[j] >= gmax2 {
363                        gmax2 = g[j];
364                    }
365                    if grad_diff > 0.0 {
366                        let quad_coef = qd_i + self.qd[j] - 2.0 * yi * q_i[j] as f64;
367                        let obj_diff = if quad_coef > 0.0 {
368                            -(grad_diff * grad_diff) / quad_coef
369                        } else {
370                            -(grad_diff * grad_diff) / TAU
371                        };
372                        if obj_diff <= obj_diff_min {
373                            gmin_idx = Some(j);
374                            obj_diff_min = obj_diff;
375                        }
376                    }
377                }
378            } else if status[j] != AlphaStatus::UpperBound {
379                let grad_diff = gmax - g[j];
380                if -g[j] >= gmax2 {
381                    gmax2 = -g[j];
382                }
383                if grad_diff > 0.0 {
384                    let quad_coef = qd_i + self.qd[j] + 2.0 * yi * q_i[j] as f64;
385                    let obj_diff = if quad_coef > 0.0 {
386                        -(grad_diff * grad_diff) / quad_coef
387                    } else {
388                        -(grad_diff * grad_diff) / TAU
389                    };
390                    if obj_diff <= obj_diff_min {
391                        gmin_idx = Some(j);
392                        obj_diff_min = obj_diff;
393                    }
394                }
395            }
396        }
397
398        if gmax + gmax2 < self.eps || gmin_idx.is_none() {
399            return None;
400        }
401
402        Some((i, gmin_idx?))
403    }
404
405    fn select_working_set_nu(&mut self) -> Option<(usize, usize)> {
406        let mut gmaxp = -INF;
407        let mut gmaxp2 = -INF;
408        let mut gmaxp_idx: Option<usize> = None;
409        let mut gmaxn = -INF;
410        let mut gmaxn2 = -INF;
411        let mut gmaxn_idx: Option<usize> = None;
412        let mut gmin_idx: Option<usize> = None;
413        let mut obj_diff_min = INF;
414
415        // Slice to active_size so index-based loops drop bounds checks.
416        let n = self.active_size;
417        let (y, g, status) = (&self.y[..n], &self.g[..n], &self.alpha_status[..n]);
418        for t in 0..n {
419            if y[t] == 1 {
420                if status[t] != AlphaStatus::UpperBound && -g[t] >= gmaxp {
421                    gmaxp = -g[t];
422                    gmaxp_idx = Some(t);
423                }
424            } else if status[t] != AlphaStatus::LowerBound && g[t] >= gmaxn {
425                gmaxn = g[t];
426                gmaxn_idx = Some(t);
427            }
428        }
429
430        let ip = gmaxp_idx;
431        let in_ = gmaxn_idx;
432
433        let q_ip = if let Some(ip) = ip {
434            Some(self.q.get_q(ip, self.active_size).to_vec())
435        } else {
436            None
437        };
438        let q_in = if let Some(in_) = in_ {
439            Some(self.q.get_q(in_, self.active_size).to_vec())
440        } else {
441            None
442        };
443
444        for j in 0..n {
445            if y[j] == 1 {
446                if status[j] != AlphaStatus::LowerBound {
447                    let grad_diff = gmaxp + g[j];
448                    if g[j] >= gmaxp2 {
449                        gmaxp2 = g[j];
450                    }
451                    if grad_diff > 0.0 {
452                        if let (Some(ip), Some(q_ip)) = (ip, q_ip.as_ref()) {
453                            let quad_coef = self.qd[ip] + self.qd[j] - 2.0 * q_ip[j] as f64;
454                            let obj_diff = if quad_coef > 0.0 {
455                                -(grad_diff * grad_diff) / quad_coef
456                            } else {
457                                -(grad_diff * grad_diff) / TAU
458                            };
459                            if obj_diff <= obj_diff_min {
460                                gmin_idx = Some(j);
461                                obj_diff_min = obj_diff;
462                            }
463                        }
464                    }
465                }
466            } else if status[j] != AlphaStatus::UpperBound {
467                let grad_diff = gmaxn - g[j];
468                if -g[j] >= gmaxn2 {
469                    gmaxn2 = -g[j];
470                }
471                if grad_diff > 0.0 {
472                    if let (Some(in_), Some(q_in)) = (in_, q_in.as_ref()) {
473                        let quad_coef = self.qd[in_] + self.qd[j] - 2.0 * q_in[j] as f64;
474                        let obj_diff = if quad_coef > 0.0 {
475                            -(grad_diff * grad_diff) / quad_coef
476                        } else {
477                            -(grad_diff * grad_diff) / TAU
478                        };
479                        if obj_diff <= obj_diff_min {
480                            gmin_idx = Some(j);
481                            obj_diff_min = obj_diff;
482                        }
483                    }
484                }
485            }
486        }
487
488        if f64::max(gmaxp + gmaxp2, gmaxn + gmaxn2) < self.eps || gmin_idx.is_none() {
489            return None;
490        }
491
492        let out_j = gmin_idx?;
493        let out_i = if self.y[out_j] == 1 {
494            gmaxp_idx?
495        } else {
496            gmaxn_idx?
497        };
498
499        Some((out_i, out_j))
500    }
501
502    // ─── Alpha pair update ──────────────────────────────────────────
503
504    fn update_alpha_pair(&mut self, i: usize, j: usize) {
505        let active_size = self.active_size;
506        let q_i = self.q.get_q(i, active_size).to_vec();
507        let q_j = self.q.get_q(j, active_size).to_vec();
508
509        let c_i = self.get_c(i);
510        let c_j = self.get_c(j);
511
512        let old_alpha_i = self.alpha[i];
513        let old_alpha_j = self.alpha[j];
514
515        if self.y[i] != self.y[j] {
516            let mut quad_coef = self.qd[i] + self.qd[j] + 2.0 * q_i[j] as f64;
517            if quad_coef <= 0.0 {
518                quad_coef = TAU;
519            }
520            let delta = (-self.g[i] - self.g[j]) / quad_coef;
521            let diff = self.alpha[i] - self.alpha[j];
522            self.alpha[i] += delta;
523            self.alpha[j] += delta;
524
525            if diff > 0.0 {
526                if self.alpha[j] < 0.0 {
527                    self.alpha[j] = 0.0;
528                    self.alpha[i] = diff;
529                }
530            } else if self.alpha[i] < 0.0 {
531                self.alpha[i] = 0.0;
532                self.alpha[j] = -diff;
533            }
534            if diff > c_i - c_j {
535                if self.alpha[i] > c_i {
536                    self.alpha[i] = c_i;
537                    self.alpha[j] = c_i - diff;
538                }
539            } else if self.alpha[j] > c_j {
540                self.alpha[j] = c_j;
541                self.alpha[i] = c_j + diff;
542            }
543        } else {
544            let mut quad_coef = self.qd[i] + self.qd[j] - 2.0 * q_i[j] as f64;
545            if quad_coef <= 0.0 {
546                quad_coef = TAU;
547            }
548            let delta = (self.g[i] - self.g[j]) / quad_coef;
549            let sum = self.alpha[i] + self.alpha[j];
550            self.alpha[i] -= delta;
551            self.alpha[j] += delta;
552
553            if sum > c_i {
554                if self.alpha[i] > c_i {
555                    self.alpha[i] = c_i;
556                    self.alpha[j] = sum - c_i;
557                }
558            } else if self.alpha[j] < 0.0 {
559                self.alpha[j] = 0.0;
560                self.alpha[i] = sum;
561            }
562            if sum > c_j {
563                if self.alpha[j] > c_j {
564                    self.alpha[j] = c_j;
565                    self.alpha[i] = sum - c_j;
566                }
567            } else if self.alpha[i] < 0.0 {
568                self.alpha[i] = 0.0;
569                self.alpha[j] = sum;
570            }
571        }
572
573        // Update gradient G
574        let delta_alpha_i = self.alpha[i] - old_alpha_i;
575        let delta_alpha_j = self.alpha[j] - old_alpha_j;
576
577        // Update gradient G (sliced so `k < active_size` proves in-bounds)
578        let g = &mut self.g[..active_size];
579        for k in 0..active_size {
580            g[k] += q_i[k] as f64 * delta_alpha_i + q_j[k] as f64 * delta_alpha_j;
581        }
582
583        // Update alpha_status and G_bar
584        let ui = self.is_upper_bound(i);
585        let uj = self.is_upper_bound(j);
586        self.update_alpha_status(i);
587        self.update_alpha_status(j);
588
589        let l = self.l;
590
591        if ui != self.is_upper_bound(i) {
592            let q_i_full = self.q.get_q(i, l).to_vec();
593            if ui {
594                for k in 0..l {
595                    self.g_bar[k] -= c_i * q_i_full[k] as f64;
596                }
597            } else {
598                for k in 0..l {
599                    self.g_bar[k] += c_i * q_i_full[k] as f64;
600                }
601            }
602        }
603
604        if uj != self.is_upper_bound(j) {
605            let q_j_full = self.q.get_q(j, l).to_vec();
606            if uj {
607                for k in 0..l {
608                    self.g_bar[k] -= c_j * q_j_full[k] as f64;
609                }
610            } else {
611                for k in 0..l {
612                    self.g_bar[k] += c_j * q_j_full[k] as f64;
613                }
614            }
615        }
616    }
617
618    // ─── Shrinking ──────────────────────────────────────────────────
619
620    fn do_shrinking(&mut self) {
621        match self.variant {
622            SolverVariant::Standard => self.do_shrinking_standard(),
623            SolverVariant::Nu => self.do_shrinking_nu(),
624        }
625    }
626
627    fn be_shrunk_standard(&self, i: usize, gmax1: f64, gmax2: f64) -> bool {
628        if self.is_upper_bound(i) {
629            if self.y[i] == 1 {
630                -self.g[i] > gmax1
631            } else {
632                -self.g[i] > gmax2
633            }
634        } else if self.is_lower_bound(i) {
635            if self.y[i] == 1 {
636                self.g[i] > gmax2
637            } else {
638                self.g[i] > gmax1
639            }
640        } else {
641            false
642        }
643    }
644
645    fn do_shrinking_standard(&mut self) {
646        let mut gmax1 = -INF;
647        let mut gmax2 = -INF;
648
649        for i in 0..self.active_size {
650            if self.y[i] == 1 {
651                if !self.is_upper_bound(i) && -self.g[i] >= gmax1 {
652                    gmax1 = -self.g[i];
653                }
654                if !self.is_lower_bound(i) && self.g[i] >= gmax2 {
655                    gmax2 = self.g[i];
656                }
657            } else {
658                if !self.is_upper_bound(i) && -self.g[i] >= gmax2 {
659                    gmax2 = -self.g[i];
660                }
661                if !self.is_lower_bound(i) && self.g[i] >= gmax1 {
662                    gmax1 = self.g[i];
663                }
664            }
665        }
666
667        if !self.unshrink && gmax1 + gmax2 <= self.eps * 10.0 {
668            self.unshrink = true;
669            self.reconstruct_gradient();
670            self.active_size = self.l;
671        }
672
673        let mut i = 0;
674        while i < self.active_size {
675            if self.be_shrunk_standard(i, gmax1, gmax2) {
676                self.active_size -= 1;
677                while self.active_size > i {
678                    if !self.be_shrunk_standard(self.active_size, gmax1, gmax2) {
679                        self.swap_index(i, self.active_size);
680                        break;
681                    }
682                    self.active_size -= 1;
683                }
684            }
685            i += 1;
686        }
687    }
688
689    fn be_shrunk_nu(&self, i: usize, gmax1: f64, gmax2: f64, gmax3: f64, gmax4: f64) -> bool {
690        if self.is_upper_bound(i) {
691            if self.y[i] == 1 {
692                -self.g[i] > gmax1
693            } else {
694                -self.g[i] > gmax4
695            }
696        } else if self.is_lower_bound(i) {
697            if self.y[i] == 1 {
698                self.g[i] > gmax2
699            } else {
700                self.g[i] > gmax3
701            }
702        } else {
703            false
704        }
705    }
706
707    fn do_shrinking_nu(&mut self) {
708        let mut gmax1 = -INF;
709        let mut gmax2 = -INF;
710        let mut gmax3 = -INF;
711        let mut gmax4 = -INF;
712
713        for i in 0..self.active_size {
714            if !self.is_upper_bound(i) {
715                if self.y[i] == 1 {
716                    if -self.g[i] > gmax1 {
717                        gmax1 = -self.g[i];
718                    }
719                } else if -self.g[i] > gmax4 {
720                    gmax4 = -self.g[i];
721                }
722            }
723            if !self.is_lower_bound(i) {
724                if self.y[i] == 1 {
725                    if self.g[i] > gmax2 {
726                        gmax2 = self.g[i];
727                    }
728                } else if self.g[i] > gmax3 {
729                    gmax3 = self.g[i];
730                }
731            }
732        }
733
734        if !self.unshrink && f64::max(gmax1 + gmax2, gmax3 + gmax4) <= self.eps * 10.0 {
735            self.unshrink = true;
736            self.reconstruct_gradient();
737            self.active_size = self.l;
738        }
739
740        let mut i = 0;
741        while i < self.active_size {
742            if self.be_shrunk_nu(i, gmax1, gmax2, gmax3, gmax4) {
743                self.active_size -= 1;
744                while self.active_size > i {
745                    if !self.be_shrunk_nu(self.active_size, gmax1, gmax2, gmax3, gmax4) {
746                        self.swap_index(i, self.active_size);
747                        break;
748                    }
749                    self.active_size -= 1;
750                }
751            }
752            i += 1;
753        }
754    }
755
756    // ─── Rho calculation ────────────────────────────────────────────
757
758    fn calculate_rho(&self) -> (f64, f64) {
759        match self.variant {
760            SolverVariant::Standard => (self.calculate_rho_standard(), 0.0),
761            SolverVariant::Nu => self.calculate_rho_nu(),
762        }
763    }
764
765    fn calculate_rho_standard(&self) -> f64 {
766        let mut nr_free = 0;
767        let mut ub = INF;
768        let mut lb = -INF;
769        let mut sum_free = 0.0;
770
771        for i in 0..self.active_size {
772            let yg = self.y[i] as f64 * self.g[i];
773
774            if self.is_upper_bound(i) {
775                if self.y[i] == -1 {
776                    ub = ub.min(yg);
777                } else {
778                    lb = lb.max(yg);
779                }
780            } else if self.is_lower_bound(i) {
781                if self.y[i] == 1 {
782                    ub = ub.min(yg);
783                } else {
784                    lb = lb.max(yg);
785                }
786            } else {
787                nr_free += 1;
788                sum_free += yg;
789            }
790        }
791
792        if nr_free > 0 {
793            sum_free / nr_free as f64
794        } else {
795            (ub + lb) / 2.0
796        }
797    }
798
799    fn calculate_rho_nu(&self) -> (f64, f64) {
800        let mut nr_free1 = 0;
801        let mut nr_free2 = 0;
802        let mut ub1 = INF;
803        let mut ub2 = INF;
804        let mut lb1 = -INF;
805        let mut lb2 = -INF;
806        let mut sum_free1 = 0.0;
807        let mut sum_free2 = 0.0;
808
809        for i in 0..self.active_size {
810            if self.y[i] == 1 {
811                if self.is_upper_bound(i) {
812                    lb1 = lb1.max(self.g[i]);
813                } else if self.is_lower_bound(i) {
814                    ub1 = ub1.min(self.g[i]);
815                } else {
816                    nr_free1 += 1;
817                    sum_free1 += self.g[i];
818                }
819            } else if self.is_upper_bound(i) {
820                lb2 = lb2.max(self.g[i]);
821            } else if self.is_lower_bound(i) {
822                ub2 = ub2.min(self.g[i]);
823            } else {
824                nr_free2 += 1;
825                sum_free2 += self.g[i];
826            }
827        }
828
829        let r1 = if nr_free1 > 0 {
830            sum_free1 / nr_free1 as f64
831        } else {
832            (ub1 + lb1) / 2.0
833        };
834
835        let r2 = if nr_free2 > 0 {
836            sum_free2 / nr_free2 as f64
837        } else {
838            (ub2 + lb2) / 2.0
839        };
840
841        let rho = (r1 - r2) / 2.0;
842        let r = (r1 + r2) / 2.0;
843        (rho, r)
844    }
845}