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