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        for t in 0..self.active_size {
337            if self.y[t] == 1 {
338                if !self.is_upper_bound(t) && -self.g[t] >= gmax {
339                    gmax = -self.g[t];
340                    gmax_idx = Some(t);
341                }
342            } else if !self.is_lower_bound(t) && self.g[t] >= gmax {
343                gmax = self.g[t];
344                gmax_idx = Some(t);
345            }
346        }
347
348        let i = gmax_idx?;
349        let q_i = self.q.get_q(i, self.active_size).to_vec();
350
351        // Second pass: find j (minimizes objective decrease)
352        for j in 0..self.active_size {
353            if self.y[j] == 1 {
354                if !self.is_lower_bound(j) {
355                    let grad_diff = gmax + self.g[j];
356                    if self.g[j] >= gmax2 {
357                        gmax2 = self.g[j];
358                    }
359                    if grad_diff > 0.0 {
360                        let quad_coef =
361                            self.qd[i] + self.qd[j] - 2.0 * (self.y[i] as f64) * q_i[j] as f64;
362                        let obj_diff = if quad_coef > 0.0 {
363                            -(grad_diff * grad_diff) / quad_coef
364                        } else {
365                            -(grad_diff * grad_diff) / TAU
366                        };
367                        if obj_diff <= obj_diff_min {
368                            gmin_idx = Some(j);
369                            obj_diff_min = obj_diff;
370                        }
371                    }
372                }
373            } else if !self.is_upper_bound(j) {
374                let grad_diff = gmax - self.g[j];
375                if -self.g[j] >= gmax2 {
376                    gmax2 = -self.g[j];
377                }
378                if grad_diff > 0.0 {
379                    let quad_coef =
380                        self.qd[i] + self.qd[j] + 2.0 * (self.y[i] as f64) * q_i[j] as f64;
381                    let obj_diff = if quad_coef > 0.0 {
382                        -(grad_diff * grad_diff) / quad_coef
383                    } else {
384                        -(grad_diff * grad_diff) / TAU
385                    };
386                    if obj_diff <= obj_diff_min {
387                        gmin_idx = Some(j);
388                        obj_diff_min = obj_diff;
389                    }
390                }
391            }
392        }
393
394        if gmax + gmax2 < self.eps || gmin_idx.is_none() {
395            return None;
396        }
397
398        Some((i, gmin_idx?))
399    }
400
401    fn select_working_set_nu(&mut self) -> Option<(usize, usize)> {
402        let mut gmaxp = -INF;
403        let mut gmaxp2 = -INF;
404        let mut gmaxp_idx: Option<usize> = None;
405        let mut gmaxn = -INF;
406        let mut gmaxn2 = -INF;
407        let mut gmaxn_idx: Option<usize> = None;
408        let mut gmin_idx: Option<usize> = None;
409        let mut obj_diff_min = INF;
410
411        for t in 0..self.active_size {
412            if self.y[t] == 1 {
413                if !self.is_upper_bound(t) && -self.g[t] >= gmaxp {
414                    gmaxp = -self.g[t];
415                    gmaxp_idx = Some(t);
416                }
417            } else if !self.is_lower_bound(t) && self.g[t] >= gmaxn {
418                gmaxn = self.g[t];
419                gmaxn_idx = Some(t);
420            }
421        }
422
423        let ip = gmaxp_idx;
424        let in_ = gmaxn_idx;
425
426        let q_ip = if let Some(ip) = ip {
427            Some(self.q.get_q(ip, self.active_size).to_vec())
428        } else {
429            None
430        };
431        let q_in = if let Some(in_) = in_ {
432            Some(self.q.get_q(in_, self.active_size).to_vec())
433        } else {
434            None
435        };
436
437        for j in 0..self.active_size {
438            if self.y[j] == 1 {
439                if !self.is_lower_bound(j) {
440                    let grad_diff = gmaxp + self.g[j];
441                    if self.g[j] >= gmaxp2 {
442                        gmaxp2 = self.g[j];
443                    }
444                    if grad_diff > 0.0 {
445                        if let (Some(ip), Some(ref q_ip)) = (ip, &q_ip) {
446                            let quad_coef = self.qd[ip] + self.qd[j] - 2.0 * q_ip[j] as f64;
447                            let obj_diff = if quad_coef > 0.0 {
448                                -(grad_diff * grad_diff) / quad_coef
449                            } else {
450                                -(grad_diff * grad_diff) / TAU
451                            };
452                            if obj_diff <= obj_diff_min {
453                                gmin_idx = Some(j);
454                                obj_diff_min = obj_diff;
455                            }
456                        }
457                    }
458                }
459            } else if !self.is_upper_bound(j) {
460                let grad_diff = gmaxn - self.g[j];
461                if -self.g[j] >= gmaxn2 {
462                    gmaxn2 = -self.g[j];
463                }
464                if grad_diff > 0.0 {
465                    if let (Some(in_), Some(ref q_in)) = (in_, &q_in) {
466                        let quad_coef = self.qd[in_] + self.qd[j] - 2.0 * q_in[j] as f64;
467                        let obj_diff = if quad_coef > 0.0 {
468                            -(grad_diff * grad_diff) / quad_coef
469                        } else {
470                            -(grad_diff * grad_diff) / TAU
471                        };
472                        if obj_diff <= obj_diff_min {
473                            gmin_idx = Some(j);
474                            obj_diff_min = obj_diff;
475                        }
476                    }
477                }
478            }
479        }
480
481        if f64::max(gmaxp + gmaxp2, gmaxn + gmaxn2) < self.eps || gmin_idx.is_none() {
482            return None;
483        }
484
485        let out_j = gmin_idx?;
486        let out_i = if self.y[out_j] == 1 {
487            gmaxp_idx?
488        } else {
489            gmaxn_idx?
490        };
491
492        Some((out_i, out_j))
493    }
494
495    // ─── Alpha pair update ──────────────────────────────────────────
496
497    fn update_alpha_pair(&mut self, i: usize, j: usize) {
498        let active_size = self.active_size;
499        let q_i = self.q.get_q(i, active_size).to_vec();
500        let q_j = self.q.get_q(j, active_size).to_vec();
501
502        let c_i = self.get_c(i);
503        let c_j = self.get_c(j);
504
505        let old_alpha_i = self.alpha[i];
506        let old_alpha_j = self.alpha[j];
507
508        if self.y[i] != self.y[j] {
509            let mut quad_coef = self.qd[i] + self.qd[j] + 2.0 * q_i[j] as f64;
510            if quad_coef <= 0.0 {
511                quad_coef = TAU;
512            }
513            let delta = (-self.g[i] - self.g[j]) / quad_coef;
514            let diff = self.alpha[i] - self.alpha[j];
515            self.alpha[i] += delta;
516            self.alpha[j] += delta;
517
518            if diff > 0.0 {
519                if self.alpha[j] < 0.0 {
520                    self.alpha[j] = 0.0;
521                    self.alpha[i] = diff;
522                }
523            } else if self.alpha[i] < 0.0 {
524                self.alpha[i] = 0.0;
525                self.alpha[j] = -diff;
526            }
527            if diff > c_i - c_j {
528                if self.alpha[i] > c_i {
529                    self.alpha[i] = c_i;
530                    self.alpha[j] = c_i - diff;
531                }
532            } else if self.alpha[j] > c_j {
533                self.alpha[j] = c_j;
534                self.alpha[i] = c_j + diff;
535            }
536        } else {
537            let mut quad_coef = self.qd[i] + self.qd[j] - 2.0 * q_i[j] as f64;
538            if quad_coef <= 0.0 {
539                quad_coef = TAU;
540            }
541            let delta = (self.g[i] - self.g[j]) / quad_coef;
542            let sum = self.alpha[i] + self.alpha[j];
543            self.alpha[i] -= delta;
544            self.alpha[j] += delta;
545
546            if sum > c_i {
547                if self.alpha[i] > c_i {
548                    self.alpha[i] = c_i;
549                    self.alpha[j] = sum - c_i;
550                }
551            } else if self.alpha[j] < 0.0 {
552                self.alpha[j] = 0.0;
553                self.alpha[i] = sum;
554            }
555            if sum > c_j {
556                if self.alpha[j] > c_j {
557                    self.alpha[j] = c_j;
558                    self.alpha[i] = sum - c_j;
559                }
560            } else if self.alpha[i] < 0.0 {
561                self.alpha[i] = 0.0;
562                self.alpha[j] = sum;
563            }
564        }
565
566        // Update gradient G
567        let delta_alpha_i = self.alpha[i] - old_alpha_i;
568        let delta_alpha_j = self.alpha[j] - old_alpha_j;
569
570        for k in 0..active_size {
571            self.g[k] += q_i[k] as f64 * delta_alpha_i + q_j[k] as f64 * delta_alpha_j;
572        }
573
574        // Update alpha_status and G_bar
575        let ui = self.is_upper_bound(i);
576        let uj = self.is_upper_bound(j);
577        self.update_alpha_status(i);
578        self.update_alpha_status(j);
579
580        let l = self.l;
581
582        if ui != self.is_upper_bound(i) {
583            let q_i_full = self.q.get_q(i, l).to_vec();
584            if ui {
585                for k in 0..l {
586                    self.g_bar[k] -= c_i * q_i_full[k] as f64;
587                }
588            } else {
589                for k in 0..l {
590                    self.g_bar[k] += c_i * q_i_full[k] as f64;
591                }
592            }
593        }
594
595        if uj != self.is_upper_bound(j) {
596            let q_j_full = self.q.get_q(j, l).to_vec();
597            if uj {
598                for k in 0..l {
599                    self.g_bar[k] -= c_j * q_j_full[k] as f64;
600                }
601            } else {
602                for k in 0..l {
603                    self.g_bar[k] += c_j * q_j_full[k] as f64;
604                }
605            }
606        }
607    }
608
609    // ─── Shrinking ──────────────────────────────────────────────────
610
611    fn do_shrinking(&mut self) {
612        match self.variant {
613            SolverVariant::Standard => self.do_shrinking_standard(),
614            SolverVariant::Nu => self.do_shrinking_nu(),
615        }
616    }
617
618    fn be_shrunk_standard(&self, i: usize, gmax1: f64, gmax2: f64) -> bool {
619        if self.is_upper_bound(i) {
620            if self.y[i] == 1 {
621                -self.g[i] > gmax1
622            } else {
623                -self.g[i] > gmax2
624            }
625        } else if self.is_lower_bound(i) {
626            if self.y[i] == 1 {
627                self.g[i] > gmax2
628            } else {
629                self.g[i] > gmax1
630            }
631        } else {
632            false
633        }
634    }
635
636    fn do_shrinking_standard(&mut self) {
637        let mut gmax1 = -INF;
638        let mut gmax2 = -INF;
639
640        for i in 0..self.active_size {
641            if self.y[i] == 1 {
642                if !self.is_upper_bound(i) && -self.g[i] >= gmax1 {
643                    gmax1 = -self.g[i];
644                }
645                if !self.is_lower_bound(i) && self.g[i] >= gmax2 {
646                    gmax2 = self.g[i];
647                }
648            } else {
649                if !self.is_upper_bound(i) && -self.g[i] >= gmax2 {
650                    gmax2 = -self.g[i];
651                }
652                if !self.is_lower_bound(i) && self.g[i] >= gmax1 {
653                    gmax1 = self.g[i];
654                }
655            }
656        }
657
658        if !self.unshrink && gmax1 + gmax2 <= self.eps * 10.0 {
659            self.unshrink = true;
660            self.reconstruct_gradient();
661            self.active_size = self.l;
662        }
663
664        let mut i = 0;
665        while i < self.active_size {
666            if self.be_shrunk_standard(i, gmax1, gmax2) {
667                self.active_size -= 1;
668                while self.active_size > i {
669                    if !self.be_shrunk_standard(self.active_size, gmax1, gmax2) {
670                        self.swap_index(i, self.active_size);
671                        break;
672                    }
673                    self.active_size -= 1;
674                }
675            }
676            i += 1;
677        }
678    }
679
680    fn be_shrunk_nu(&self, i: usize, gmax1: f64, gmax2: f64, gmax3: f64, gmax4: f64) -> bool {
681        if self.is_upper_bound(i) {
682            if self.y[i] == 1 {
683                -self.g[i] > gmax1
684            } else {
685                -self.g[i] > gmax4
686            }
687        } else if self.is_lower_bound(i) {
688            if self.y[i] == 1 {
689                self.g[i] > gmax2
690            } else {
691                self.g[i] > gmax3
692            }
693        } else {
694            false
695        }
696    }
697
698    fn do_shrinking_nu(&mut self) {
699        let mut gmax1 = -INF;
700        let mut gmax2 = -INF;
701        let mut gmax3 = -INF;
702        let mut gmax4 = -INF;
703
704        for i in 0..self.active_size {
705            if !self.is_upper_bound(i) {
706                if self.y[i] == 1 {
707                    if -self.g[i] > gmax1 {
708                        gmax1 = -self.g[i];
709                    }
710                } else if -self.g[i] > gmax4 {
711                    gmax4 = -self.g[i];
712                }
713            }
714            if !self.is_lower_bound(i) {
715                if self.y[i] == 1 {
716                    if self.g[i] > gmax2 {
717                        gmax2 = self.g[i];
718                    }
719                } else if self.g[i] > gmax3 {
720                    gmax3 = self.g[i];
721                }
722            }
723        }
724
725        if !self.unshrink && f64::max(gmax1 + gmax2, gmax3 + gmax4) <= self.eps * 10.0 {
726            self.unshrink = true;
727            self.reconstruct_gradient();
728            self.active_size = self.l;
729        }
730
731        let mut i = 0;
732        while i < self.active_size {
733            if self.be_shrunk_nu(i, gmax1, gmax2, gmax3, gmax4) {
734                self.active_size -= 1;
735                while self.active_size > i {
736                    if !self.be_shrunk_nu(self.active_size, gmax1, gmax2, gmax3, gmax4) {
737                        self.swap_index(i, self.active_size);
738                        break;
739                    }
740                    self.active_size -= 1;
741                }
742            }
743            i += 1;
744        }
745    }
746
747    // ─── Rho calculation ────────────────────────────────────────────
748
749    fn calculate_rho(&self) -> (f64, f64) {
750        match self.variant {
751            SolverVariant::Standard => (self.calculate_rho_standard(), 0.0),
752            SolverVariant::Nu => self.calculate_rho_nu(),
753        }
754    }
755
756    fn calculate_rho_standard(&self) -> f64 {
757        let mut nr_free = 0;
758        let mut ub = INF;
759        let mut lb = -INF;
760        let mut sum_free = 0.0;
761
762        for i in 0..self.active_size {
763            let yg = self.y[i] as f64 * self.g[i];
764
765            if self.is_upper_bound(i) {
766                if self.y[i] == -1 {
767                    ub = ub.min(yg);
768                } else {
769                    lb = lb.max(yg);
770                }
771            } else if self.is_lower_bound(i) {
772                if self.y[i] == 1 {
773                    ub = ub.min(yg);
774                } else {
775                    lb = lb.max(yg);
776                }
777            } else {
778                nr_free += 1;
779                sum_free += yg;
780            }
781        }
782
783        if nr_free > 0 {
784            sum_free / nr_free as f64
785        } else {
786            (ub + lb) / 2.0
787        }
788    }
789
790    fn calculate_rho_nu(&self) -> (f64, f64) {
791        let mut nr_free1 = 0;
792        let mut nr_free2 = 0;
793        let mut ub1 = INF;
794        let mut ub2 = INF;
795        let mut lb1 = -INF;
796        let mut lb2 = -INF;
797        let mut sum_free1 = 0.0;
798        let mut sum_free2 = 0.0;
799
800        for i in 0..self.active_size {
801            if self.y[i] == 1 {
802                if self.is_upper_bound(i) {
803                    lb1 = lb1.max(self.g[i]);
804                } else if self.is_lower_bound(i) {
805                    ub1 = ub1.min(self.g[i]);
806                } else {
807                    nr_free1 += 1;
808                    sum_free1 += self.g[i];
809                }
810            } else if self.is_upper_bound(i) {
811                lb2 = lb2.max(self.g[i]);
812            } else if self.is_lower_bound(i) {
813                ub2 = ub2.min(self.g[i]);
814            } else {
815                nr_free2 += 1;
816                sum_free2 += self.g[i];
817            }
818        }
819
820        let r1 = if nr_free1 > 0 {
821            sum_free1 / nr_free1 as f64
822        } else {
823            (ub1 + lb1) / 2.0
824        };
825
826        let r2 = if nr_free2 > 0 {
827            sum_free2 / nr_free2 as f64
828        } else {
829            (ub2 + lb2) / 2.0
830        };
831
832        let rho = (r1 - r2) / 2.0;
833        let r = (r1 + r2) / 2.0;
834        (rho, r)
835    }
836}