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!("optimization finished, #iter = {}\n", iter));
214
215        si
216    }
217
218    // ─── Helper methods ─────────────────────────────────────────────
219
220    #[inline]
221    fn get_c(&self, i: usize) -> f64 {
222        if self.y[i] > 0 {
223            self.cp
224        } else {
225            self.cn
226        }
227    }
228
229    #[inline]
230    fn update_alpha_status(&mut self, i: usize) {
231        self.alpha_status[i] = if self.alpha[i] >= self.get_c(i) {
232            AlphaStatus::UpperBound
233        } else if self.alpha[i] <= 0.0 {
234            AlphaStatus::LowerBound
235        } else {
236            AlphaStatus::Free
237        };
238    }
239
240    #[inline]
241    fn is_upper_bound(&self, i: usize) -> bool {
242        self.alpha_status[i] == AlphaStatus::UpperBound
243    }
244
245    #[inline]
246    fn is_lower_bound(&self, i: usize) -> bool {
247        self.alpha_status[i] == AlphaStatus::LowerBound
248    }
249
250    #[inline]
251    fn is_free(&self, i: usize) -> bool {
252        self.alpha_status[i] == AlphaStatus::Free
253    }
254
255    fn swap_index(&mut self, i: usize, j: usize) {
256        self.q.swap_index(i, j);
257        self.y.swap(i, j);
258        self.g.swap(i, j);
259        self.alpha_status.swap(i, j);
260        self.alpha.swap(i, j);
261        self.p.swap(i, j);
262        self.active_set.swap(i, j);
263        self.g_bar.swap(i, j);
264        self.qd.swap(i, j);
265    }
266
267    fn reconstruct_gradient(&mut self) {
268        if self.active_size == self.l {
269            return;
270        }
271
272        for j in self.active_size..self.l {
273            self.g[j] = self.g_bar[j] + self.p[j];
274        }
275
276        let mut nr_free = 0;
277        for j in 0..self.active_size {
278            if self.is_free(j) {
279                nr_free += 1;
280            }
281        }
282
283        if 2 * nr_free < self.active_size {
284            crate::info("WARNING: using -h 0 may be faster\n");
285        }
286
287        let active_size = self.active_size;
288        let l = self.l;
289
290        if nr_free * l > 2 * active_size * (l - active_size) {
291            for i in active_size..l {
292                let q_i = self.q.get_q(i, active_size).to_vec();
293                for j in 0..active_size {
294                    if self.is_free(j) {
295                        self.g[i] += self.alpha[j] * q_i[j] as f64;
296                    }
297                }
298            }
299        } else {
300            for i in 0..active_size {
301                if self.is_free(i) {
302                    let q_i = self.q.get_q(i, l).to_vec();
303                    let alpha_i = self.alpha[i];
304                    for j in active_size..l {
305                        self.g[j] += alpha_i * q_i[j] as f64;
306                    }
307                }
308            }
309        }
310    }
311
312    // ─── Working set selection ──────────────────────────────────────
313
314    /// Select working set (i, j). Returns None if already optimal.
315    fn select_working_set(&mut self) -> Option<(usize, usize)> {
316        match self.variant {
317            SolverVariant::Standard => self.select_working_set_standard(),
318            SolverVariant::Nu => self.select_working_set_nu(),
319        }
320    }
321
322    fn select_working_set_standard(&mut self) -> Option<(usize, usize)> {
323        let mut gmax = -INF;
324        let mut gmax2 = -INF;
325        let mut gmax_idx: Option<usize> = None;
326        let mut gmin_idx: Option<usize> = None;
327        let mut obj_diff_min = INF;
328
329        // First pass: find i (maximizes -y_i * grad(f)_i in I_up)
330        for t in 0..self.active_size {
331            if self.y[t] == 1 {
332                if !self.is_upper_bound(t) && -self.g[t] >= gmax {
333                    gmax = -self.g[t];
334                    gmax_idx = Some(t);
335                }
336            } else if !self.is_lower_bound(t) && self.g[t] >= gmax {
337                gmax = self.g[t];
338                gmax_idx = Some(t);
339            }
340        }
341
342        let i = gmax_idx?;
343        let q_i = self.q.get_q(i, self.active_size).to_vec();
344
345        // Second pass: find j (minimizes objective decrease)
346        for j in 0..self.active_size {
347            if self.y[j] == 1 {
348                if !self.is_lower_bound(j) {
349                    let grad_diff = gmax + self.g[j];
350                    if self.g[j] >= gmax2 {
351                        gmax2 = self.g[j];
352                    }
353                    if grad_diff > 0.0 {
354                        let quad_coef =
355                            self.qd[i] + self.qd[j] - 2.0 * (self.y[i] as f64) * q_i[j] as f64;
356                        let obj_diff = if quad_coef > 0.0 {
357                            -(grad_diff * grad_diff) / quad_coef
358                        } else {
359                            -(grad_diff * grad_diff) / TAU
360                        };
361                        if obj_diff <= obj_diff_min {
362                            gmin_idx = Some(j);
363                            obj_diff_min = obj_diff;
364                        }
365                    }
366                }
367            } else 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 =
374                        self.qd[i] + self.qd[j] + 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        if gmax + gmax2 < self.eps || gmin_idx.is_none() {
389            return None;
390        }
391
392        Some((i, gmin_idx.unwrap()))
393    }
394
395    fn select_working_set_nu(&mut self) -> Option<(usize, usize)> {
396        let mut gmaxp = -INF;
397        let mut gmaxp2 = -INF;
398        let mut gmaxp_idx: Option<usize> = None;
399        let mut gmaxn = -INF;
400        let mut gmaxn2 = -INF;
401        let mut gmaxn_idx: Option<usize> = None;
402        let mut gmin_idx: Option<usize> = None;
403        let mut obj_diff_min = INF;
404
405        for t in 0..self.active_size {
406            if self.y[t] == 1 {
407                if !self.is_upper_bound(t) && -self.g[t] >= gmaxp {
408                    gmaxp = -self.g[t];
409                    gmaxp_idx = Some(t);
410                }
411            } else if !self.is_lower_bound(t) && self.g[t] >= gmaxn {
412                gmaxn = self.g[t];
413                gmaxn_idx = Some(t);
414            }
415        }
416
417        let ip = gmaxp_idx;
418        let in_ = gmaxn_idx;
419
420        let q_ip = if let Some(ip) = ip {
421            Some(self.q.get_q(ip, self.active_size).to_vec())
422        } else {
423            None
424        };
425        let q_in = if let Some(in_) = in_ {
426            Some(self.q.get_q(in_, self.active_size).to_vec())
427        } else {
428            None
429        };
430
431        for j in 0..self.active_size {
432            if self.y[j] == 1 {
433                if !self.is_lower_bound(j) {
434                    let grad_diff = gmaxp + self.g[j];
435                    if self.g[j] >= gmaxp2 {
436                        gmaxp2 = self.g[j];
437                    }
438                    if grad_diff > 0.0 {
439                        if let (Some(ip), Some(ref q_ip)) = (ip, &q_ip) {
440                            let quad_coef = self.qd[ip] + self.qd[j] - 2.0 * q_ip[j] as f64;
441                            let obj_diff = if quad_coef > 0.0 {
442                                -(grad_diff * grad_diff) / quad_coef
443                            } else {
444                                -(grad_diff * grad_diff) / TAU
445                            };
446                            if obj_diff <= obj_diff_min {
447                                gmin_idx = Some(j);
448                                obj_diff_min = obj_diff;
449                            }
450                        }
451                    }
452                }
453            } else if !self.is_upper_bound(j) {
454                let grad_diff = gmaxn - self.g[j];
455                if -self.g[j] >= gmaxn2 {
456                    gmaxn2 = -self.g[j];
457                }
458                if grad_diff > 0.0 {
459                    if let (Some(in_), Some(ref q_in)) = (in_, &q_in) {
460                        let quad_coef = self.qd[in_] + self.qd[j] - 2.0 * q_in[j] as f64;
461                        let obj_diff = if quad_coef > 0.0 {
462                            -(grad_diff * grad_diff) / quad_coef
463                        } else {
464                            -(grad_diff * grad_diff) / TAU
465                        };
466                        if obj_diff <= obj_diff_min {
467                            gmin_idx = Some(j);
468                            obj_diff_min = obj_diff;
469                        }
470                    }
471                }
472            }
473        }
474
475        if f64::max(gmaxp + gmaxp2, gmaxn + gmaxn2) < self.eps || gmin_idx.is_none() {
476            return None;
477        }
478
479        let out_j = gmin_idx.unwrap();
480        let out_i = if self.y[out_j] == 1 {
481            gmaxp_idx?
482        } else {
483            gmaxn_idx?
484        };
485
486        Some((out_i, out_j))
487    }
488
489    // ─── Alpha pair update ──────────────────────────────────────────
490
491    fn update_alpha_pair(&mut self, i: usize, j: usize) {
492        let active_size = self.active_size;
493        let q_i = self.q.get_q(i, active_size).to_vec();
494        let q_j = self.q.get_q(j, active_size).to_vec();
495
496        let c_i = self.get_c(i);
497        let c_j = self.get_c(j);
498
499        let old_alpha_i = self.alpha[i];
500        let old_alpha_j = self.alpha[j];
501
502        if self.y[i] != self.y[j] {
503            let mut quad_coef = self.qd[i] + self.qd[j] + 2.0 * q_i[j] as f64;
504            if quad_coef <= 0.0 {
505                quad_coef = TAU;
506            }
507            let delta = (-self.g[i] - self.g[j]) / quad_coef;
508            let diff = self.alpha[i] - self.alpha[j];
509            self.alpha[i] += delta;
510            self.alpha[j] += delta;
511
512            if diff > 0.0 {
513                if self.alpha[j] < 0.0 {
514                    self.alpha[j] = 0.0;
515                    self.alpha[i] = diff;
516                }
517            } else if self.alpha[i] < 0.0 {
518                self.alpha[i] = 0.0;
519                self.alpha[j] = -diff;
520            }
521            if diff > c_i - c_j {
522                if self.alpha[i] > c_i {
523                    self.alpha[i] = c_i;
524                    self.alpha[j] = c_i - diff;
525                }
526            } else if self.alpha[j] > c_j {
527                self.alpha[j] = c_j;
528                self.alpha[i] = c_j + diff;
529            }
530        } else {
531            let mut quad_coef = self.qd[i] + self.qd[j] - 2.0 * q_i[j] as f64;
532            if quad_coef <= 0.0 {
533                quad_coef = TAU;
534            }
535            let delta = (self.g[i] - self.g[j]) / quad_coef;
536            let sum = self.alpha[i] + self.alpha[j];
537            self.alpha[i] -= delta;
538            self.alpha[j] += delta;
539
540            if sum > c_i {
541                if self.alpha[i] > c_i {
542                    self.alpha[i] = c_i;
543                    self.alpha[j] = sum - c_i;
544                }
545            } else if self.alpha[j] < 0.0 {
546                self.alpha[j] = 0.0;
547                self.alpha[i] = sum;
548            }
549            if sum > c_j {
550                if self.alpha[j] > c_j {
551                    self.alpha[j] = c_j;
552                    self.alpha[i] = sum - c_j;
553                }
554            } else if self.alpha[i] < 0.0 {
555                self.alpha[i] = 0.0;
556                self.alpha[j] = sum;
557            }
558        }
559
560        // Update gradient G
561        let delta_alpha_i = self.alpha[i] - old_alpha_i;
562        let delta_alpha_j = self.alpha[j] - old_alpha_j;
563
564        for k in 0..active_size {
565            self.g[k] += q_i[k] as f64 * delta_alpha_i + q_j[k] as f64 * delta_alpha_j;
566        }
567
568        // Update alpha_status and G_bar
569        let ui = self.is_upper_bound(i);
570        let uj = self.is_upper_bound(j);
571        self.update_alpha_status(i);
572        self.update_alpha_status(j);
573
574        let l = self.l;
575
576        if ui != self.is_upper_bound(i) {
577            let q_i_full = self.q.get_q(i, l).to_vec();
578            if ui {
579                for k in 0..l {
580                    self.g_bar[k] -= c_i * q_i_full[k] as f64;
581                }
582            } else {
583                for k in 0..l {
584                    self.g_bar[k] += c_i * q_i_full[k] as f64;
585                }
586            }
587        }
588
589        if uj != self.is_upper_bound(j) {
590            let q_j_full = self.q.get_q(j, l).to_vec();
591            if uj {
592                for k in 0..l {
593                    self.g_bar[k] -= c_j * q_j_full[k] as f64;
594                }
595            } else {
596                for k in 0..l {
597                    self.g_bar[k] += c_j * q_j_full[k] as f64;
598                }
599            }
600        }
601    }
602
603    // ─── Shrinking ──────────────────────────────────────────────────
604
605    fn do_shrinking(&mut self) {
606        match self.variant {
607            SolverVariant::Standard => self.do_shrinking_standard(),
608            SolverVariant::Nu => self.do_shrinking_nu(),
609        }
610    }
611
612    fn be_shrunk_standard(&self, i: usize, gmax1: f64, gmax2: f64) -> bool {
613        if self.is_upper_bound(i) {
614            if self.y[i] == 1 {
615                -self.g[i] > gmax1
616            } else {
617                -self.g[i] > gmax2
618            }
619        } else if self.is_lower_bound(i) {
620            if self.y[i] == 1 {
621                self.g[i] > gmax2
622            } else {
623                self.g[i] > gmax1
624            }
625        } else {
626            false
627        }
628    }
629
630    fn do_shrinking_standard(&mut self) {
631        let mut gmax1 = -INF;
632        let mut gmax2 = -INF;
633
634        for i in 0..self.active_size {
635            if self.y[i] == 1 {
636                if !self.is_upper_bound(i) && -self.g[i] >= gmax1 {
637                    gmax1 = -self.g[i];
638                }
639                if !self.is_lower_bound(i) && self.g[i] >= gmax2 {
640                    gmax2 = self.g[i];
641                }
642            } else {
643                if !self.is_upper_bound(i) && -self.g[i] >= gmax2 {
644                    gmax2 = -self.g[i];
645                }
646                if !self.is_lower_bound(i) && self.g[i] >= gmax1 {
647                    gmax1 = self.g[i];
648                }
649            }
650        }
651
652        if !self.unshrink && gmax1 + gmax2 <= self.eps * 10.0 {
653            self.unshrink = true;
654            self.reconstruct_gradient();
655            self.active_size = self.l;
656        }
657
658        let mut i = 0;
659        while i < self.active_size {
660            if self.be_shrunk_standard(i, gmax1, gmax2) {
661                self.active_size -= 1;
662                while self.active_size > i {
663                    if !self.be_shrunk_standard(self.active_size, gmax1, gmax2) {
664                        self.swap_index(i, self.active_size);
665                        break;
666                    }
667                    self.active_size -= 1;
668                }
669            }
670            i += 1;
671        }
672    }
673
674    fn be_shrunk_nu(&self, i: usize, gmax1: f64, gmax2: f64, gmax3: f64, gmax4: f64) -> bool {
675        if self.is_upper_bound(i) {
676            if self.y[i] == 1 {
677                -self.g[i] > gmax1
678            } else {
679                -self.g[i] > gmax4
680            }
681        } else if self.is_lower_bound(i) {
682            if self.y[i] == 1 {
683                self.g[i] > gmax2
684            } else {
685                self.g[i] > gmax3
686            }
687        } else {
688            false
689        }
690    }
691
692    fn do_shrinking_nu(&mut self) {
693        let mut gmax1 = -INF;
694        let mut gmax2 = -INF;
695        let mut gmax3 = -INF;
696        let mut gmax4 = -INF;
697
698        for i in 0..self.active_size {
699            if !self.is_upper_bound(i) {
700                if self.y[i] == 1 {
701                    if -self.g[i] > gmax1 {
702                        gmax1 = -self.g[i];
703                    }
704                } else if -self.g[i] > gmax4 {
705                    gmax4 = -self.g[i];
706                }
707            }
708            if !self.is_lower_bound(i) {
709                if self.y[i] == 1 {
710                    if self.g[i] > gmax2 {
711                        gmax2 = self.g[i];
712                    }
713                } else if self.g[i] > gmax3 {
714                    gmax3 = self.g[i];
715                }
716            }
717        }
718
719        if !self.unshrink && f64::max(gmax1 + gmax2, gmax3 + gmax4) <= self.eps * 10.0 {
720            self.unshrink = true;
721            self.reconstruct_gradient();
722            self.active_size = self.l;
723        }
724
725        let mut i = 0;
726        while i < self.active_size {
727            if self.be_shrunk_nu(i, gmax1, gmax2, gmax3, gmax4) {
728                self.active_size -= 1;
729                while self.active_size > i {
730                    if !self.be_shrunk_nu(self.active_size, gmax1, gmax2, gmax3, gmax4) {
731                        self.swap_index(i, self.active_size);
732                        break;
733                    }
734                    self.active_size -= 1;
735                }
736            }
737            i += 1;
738        }
739    }
740
741    // ─── Rho calculation ────────────────────────────────────────────
742
743    fn calculate_rho(&self) -> (f64, f64) {
744        match self.variant {
745            SolverVariant::Standard => (self.calculate_rho_standard(), 0.0),
746            SolverVariant::Nu => self.calculate_rho_nu(),
747        }
748    }
749
750    fn calculate_rho_standard(&self) -> f64 {
751        let mut nr_free = 0;
752        let mut ub = INF;
753        let mut lb = -INF;
754        let mut sum_free = 0.0;
755
756        for i in 0..self.active_size {
757            let yg = self.y[i] as f64 * self.g[i];
758
759            if self.is_upper_bound(i) {
760                if self.y[i] == -1 {
761                    ub = ub.min(yg);
762                } else {
763                    lb = lb.max(yg);
764                }
765            } else if self.is_lower_bound(i) {
766                if self.y[i] == 1 {
767                    ub = ub.min(yg);
768                } else {
769                    lb = lb.max(yg);
770                }
771            } else {
772                nr_free += 1;
773                sum_free += yg;
774            }
775        }
776
777        if nr_free > 0 {
778            sum_free / nr_free as f64
779        } else {
780            (ub + lb) / 2.0
781        }
782    }
783
784    fn calculate_rho_nu(&self) -> (f64, f64) {
785        let mut nr_free1 = 0;
786        let mut nr_free2 = 0;
787        let mut ub1 = INF;
788        let mut ub2 = INF;
789        let mut lb1 = -INF;
790        let mut lb2 = -INF;
791        let mut sum_free1 = 0.0;
792        let mut sum_free2 = 0.0;
793
794        for i in 0..self.active_size {
795            if self.y[i] == 1 {
796                if self.is_upper_bound(i) {
797                    lb1 = lb1.max(self.g[i]);
798                } else if self.is_lower_bound(i) {
799                    ub1 = ub1.min(self.g[i]);
800                } else {
801                    nr_free1 += 1;
802                    sum_free1 += self.g[i];
803                }
804            } else if self.is_upper_bound(i) {
805                lb2 = lb2.max(self.g[i]);
806            } else if self.is_lower_bound(i) {
807                ub2 = ub2.min(self.g[i]);
808            } else {
809                nr_free2 += 1;
810                sum_free2 += self.g[i];
811            }
812        }
813
814        let r1 = if nr_free1 > 0 {
815            sum_free1 / nr_free1 as f64
816        } else {
817            (ub1 + lb1) / 2.0
818        };
819
820        let r2 = if nr_free2 > 0 {
821            sum_free2 / nr_free2 as f64
822        } else {
823            (ub2 + lb2) / 2.0
824        };
825
826        let rho = (r1 - r2) / 2.0;
827        let r = (r1 + r2) / 2.0;
828        (rho, r)
829    }
830}