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