Skip to main content

scirs2_optimize/
mip.rs

1//! Mixed Integer Programming (MIP) — unified top-level interface.
2//!
3//! This module exposes a clean, high-level API for Mixed-Integer Linear
4//! Programming (MILP) problems.  It re-exports the core solver types from
5//! [`crate::integer`] and adds:
6//!
7//! - [`MilpProblem`] — unified problem description with a fluent builder.
8//! - [`MilpSolverConfig`] — algorithm selection and tuning.
9//! - [`MilpSolver`] — single entry-point that dispatches B&B or cutting planes.
10//! - [`IntegerConstraint`] — per-variable integrality annotations.
11//!
12//! # Problem form
13//!
14//! ```text
15//! minimise   c^T x
16//! subject to A  x ≤ b            (linear inequalities, optional)
17//!            Aeq x = beq          (linear equalities, optional)
18//!            lb ≤ x ≤ ub          (variable bounds)
19//!            x_i ∈ ℤ   ∀ i ∈ I   (integrality)
20//!            x_i ∈ {0,1} ∀ i ∈ B (binary subset)
21//! ```
22//!
23//! # Example
24//!
25//! ```rust
26//! use scirs2_optimize::mip::{MilpProblem, MilpSolverConfig, MilpSolver, IntegerConstraint};
27//! use scirs2_core::ndarray::{array, Array2};
28//!
29//! // Simple knapsack: max 4x₀ + 5x₁ + 3x₂  s.t. 2x₀+3x₁+x₂ ≤ 5, x ∈ {0,1}
30//! let c = array![-4.0, -5.0, -3.0]; // negate for min
31//! let a = Array2::from_shape_vec((1, 3), vec![2.0, 3.0, 1.0]).expect("valid input");
32//! let b = array![5.0];
33//!
34//! let prob = MilpProblem::builder(c)
35//!     .inequalities(a, b)
36//!     .bounds(array![0.0, 0.0, 0.0], array![1.0, 1.0, 1.0])
37//!     .all_binary()
38//!     .build()
39//!     .expect("valid input");
40//!
41//! let result = MilpSolver::default().solve(&prob).expect("valid input");
42//! assert!(result.success);
43//! assert!(result.objective <= -7.0 + 1e-4);
44//! ```
45
46use crate::error::{OptimizeError, OptimizeResult};
47use scirs2_core::ndarray::{Array1, Array2};
48
49// ─── Re-exports from integer submodule ───────────────────────────────────────
50
51pub use crate::integer::milp_branch_and_bound::{
52    branch_and_bound, BnbConfig, BranchingStrategy, MilpProblem as MilpProblemInner,
53    MilpResult as MilpResultInner,
54};
55pub use crate::integer::{
56    is_integer_valued, BranchAndBoundOptions, BranchAndBoundSolver, CuttingPlaneOptions,
57    CuttingPlaneSolver, IntegerKind, IntegerVariableSet, LinearProgram, MipResult,
58};
59
60// ─── IntegerConstraint ───────────────────────────────────────────────────────
61
62/// Per-variable integrality specification.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub enum IntegerConstraint {
65    /// Variable is continuous (real-valued).
66    Continuous,
67    /// Variable must take an integer value.
68    Integer,
69    /// Variable is binary: must be 0 or 1.
70    Binary,
71    /// Variable must be a non-negative integer.
72    NonNegativeInteger,
73}
74
75impl IntegerConstraint {
76    /// Convert to the lower-level [`IntegerKind`].
77    pub fn to_kind(self) -> IntegerKind {
78        match self {
79            IntegerConstraint::Continuous => IntegerKind::Continuous,
80            IntegerConstraint::Integer | IntegerConstraint::NonNegativeInteger => {
81                IntegerKind::Integer
82            }
83            IntegerConstraint::Binary => IntegerKind::Binary,
84        }
85    }
86}
87
88// ─── MilpProblem ─────────────────────────────────────────────────────────────
89
90/// A Mixed-Integer Linear Program.
91///
92/// Prefer constructing via [`MilpProblem::builder`].
93#[derive(Debug, Clone)]
94pub struct MilpProblem {
95    /// Objective coefficients (length n).
96    pub c: Array1<f64>,
97    /// Inequality LHS (m_ub × n), optional.
98    pub a_ub: Option<Array2<f64>>,
99    /// Inequality RHS (length m_ub), optional.
100    pub b_ub: Option<Array1<f64>>,
101    /// Equality LHS (m_eq × n), optional.
102    pub a_eq: Option<Array2<f64>>,
103    /// Equality RHS (length m_eq), optional.
104    pub b_eq: Option<Array1<f64>>,
105    /// Lower bounds (length n).
106    pub lb: Array1<f64>,
107    /// Upper bounds (length n).
108    pub ub: Array1<f64>,
109    /// Per-variable integrality specification (length n).
110    pub constraints: Vec<IntegerConstraint>,
111}
112
113impl MilpProblem {
114    /// Begin building a [`MilpProblem`].
115    pub fn builder(c: Array1<f64>) -> MilpProblemBuilder {
116        let n = c.len();
117        MilpProblemBuilder {
118            c,
119            a_ub: None,
120            b_ub: None,
121            a_eq: None,
122            b_eq: None,
123            lb: Array1::zeros(n),
124            ub: Array1::from_elem(n, f64::INFINITY),
125            constraints: vec![IntegerConstraint::Continuous; n],
126        }
127    }
128
129    /// Number of decision variables.
130    #[inline]
131    pub fn n_vars(&self) -> usize {
132        self.c.len()
133    }
134
135    /// Number of inequality constraints.
136    #[inline]
137    pub fn n_ineq(&self) -> usize {
138        self.a_ub.as_ref().map_or(0, |a| a.nrows())
139    }
140
141    /// Number of equality constraints.
142    #[inline]
143    pub fn n_eq(&self) -> usize {
144        self.a_eq.as_ref().map_or(0, |a| a.nrows())
145    }
146
147    /// Indices of integer / binary variables.
148    pub fn integer_indices(&self) -> Vec<usize> {
149        self.constraints
150            .iter()
151            .enumerate()
152            .filter(|(_, c)| **c != IntegerConstraint::Continuous)
153            .map(|(i, _)| i)
154            .collect()
155    }
156
157    /// Convert to the lower-level [`LinearProgram`] (ignoring integrality).
158    pub fn to_linear_program(&self) -> LinearProgram {
159        LinearProgram {
160            c: self.c.clone(),
161            a_ub: self.a_ub.clone(),
162            b_ub: self.b_ub.clone(),
163            a_eq: self.a_eq.clone(),
164            b_eq: self.b_eq.clone(),
165            lower: Some(self.lb.clone()),
166            upper: Some(self.ub.clone()),
167        }
168    }
169
170    /// Convert to the lower-level [`IntegerVariableSet`].
171    pub fn to_integer_variable_set(&self) -> IntegerVariableSet {
172        IntegerVariableSet {
173            kinds: self.constraints.iter().map(|c| c.to_kind()).collect(),
174        }
175    }
176
177    /// Convert to [`MilpProblemInner`] for use with [`branch_and_bound`].
178    ///
179    /// Equality constraints are expanded into two inequalities:
180    /// `Aeq x ≤ beq` and `-Aeq x ≤ -beq`.
181    pub fn to_inner(&self) -> OptimizeResult<MilpProblemInner> {
182        let (a_combined, b_combined) = self.build_combined_ineq()?;
183        MilpProblemInner::new(
184            self.c.clone(),
185            a_combined,
186            b_combined,
187            self.lb.clone(),
188            self.ub.clone(),
189            self.integer_indices(),
190        )
191    }
192
193    fn build_combined_ineq(&self) -> OptimizeResult<(Array2<f64>, Array1<f64>)> {
194        let n = self.n_vars();
195        let m_ub = self.n_ineq();
196        let m_eq = self.n_eq();
197        let total = m_ub + 2 * m_eq;
198
199        if total == 0 {
200            return Ok((Array2::zeros((0, n)), Array1::zeros(0)));
201        }
202
203        let mut a = Array2::<f64>::zeros((total, n));
204        let mut b = Array1::<f64>::zeros(total);
205
206        if let (Some(aub), Some(bub)) = (&self.a_ub, &self.b_ub) {
207            for i in 0..m_ub {
208                for j in 0..n {
209                    a[[i, j]] = aub[[i, j]];
210                }
211                b[i] = bub[i];
212            }
213        }
214
215        if let (Some(aeq), Some(beq)) = (&self.a_eq, &self.b_eq) {
216            for k in 0..m_eq {
217                let rp = m_ub + k;
218                let rm = m_ub + m_eq + k;
219                for j in 0..n {
220                    a[[rp, j]] = aeq[[k, j]];
221                    a[[rm, j]] = -aeq[[k, j]];
222                }
223                b[rp] = beq[k];
224                b[rm] = -beq[k];
225            }
226        }
227
228        Ok((a, b))
229    }
230}
231
232// ─── MilpProblemBuilder ───────────────────────────────────────────────────────
233
234/// Fluent builder for [`MilpProblem`].
235pub struct MilpProblemBuilder {
236    c: Array1<f64>,
237    a_ub: Option<Array2<f64>>,
238    b_ub: Option<Array1<f64>>,
239    a_eq: Option<Array2<f64>>,
240    b_eq: Option<Array1<f64>>,
241    lb: Array1<f64>,
242    ub: Array1<f64>,
243    constraints: Vec<IntegerConstraint>,
244}
245
246impl MilpProblemBuilder {
247    /// Add inequality constraints `A x ≤ b`.
248    pub fn inequalities(mut self, a: Array2<f64>, b: Array1<f64>) -> Self {
249        self.a_ub = Some(a);
250        self.b_ub = Some(b);
251        self
252    }
253
254    /// Add equality constraints `Aeq x = beq`.
255    pub fn equalities(mut self, a: Array2<f64>, b: Array1<f64>) -> Self {
256        self.a_eq = Some(a);
257        self.b_eq = Some(b);
258        self
259    }
260
261    /// Set variable bounds.
262    pub fn bounds(mut self, lb: Array1<f64>, ub: Array1<f64>) -> Self {
263        self.lb = lb;
264        self.ub = ub;
265        self
266    }
267
268    /// Set per-variable integrality.
269    pub fn integer_constraints(mut self, c: Vec<IntegerConstraint>) -> Self {
270        self.constraints = c;
271        self
272    }
273
274    /// Mark all variables as binary (sets ub=1, lb=0).
275    pub fn all_binary(mut self) -> Self {
276        let n = self.c.len();
277        self.constraints = vec![IntegerConstraint::Binary; n];
278        self.lb = Array1::zeros(n);
279        self.ub = Array1::ones(n);
280        self
281    }
282
283    /// Mark all variables as integer.
284    pub fn all_integer(mut self) -> Self {
285        let n = self.c.len();
286        self.constraints = vec![IntegerConstraint::Integer; n];
287        self
288    }
289
290    /// Validate and finalise the problem.
291    pub fn build(self) -> OptimizeResult<MilpProblem> {
292        let n = self.c.len();
293
294        if n == 0 {
295            return Err(OptimizeError::InvalidInput(
296                "Objective vector c is empty".to_string(),
297            ));
298        }
299        if self.lb.len() != n || self.ub.len() != n {
300            return Err(OptimizeError::InvalidInput(format!(
301                "lb/ub length ({}/{}) must equal n={}",
302                self.lb.len(),
303                self.ub.len(),
304                n
305            )));
306        }
307        if self.constraints.len() != n {
308            return Err(OptimizeError::InvalidInput(format!(
309                "constraints length ({}) must equal n={}",
310                self.constraints.len(),
311                n
312            )));
313        }
314        if let (Some(a), Some(b)) = (&self.a_ub, &self.b_ub) {
315            if a.ncols() != n {
316                return Err(OptimizeError::InvalidInput(format!(
317                    "A_ub has {} columns but n={}",
318                    a.ncols(),
319                    n
320                )));
321            }
322            if a.nrows() != b.len() {
323                return Err(OptimizeError::InvalidInput(format!(
324                    "A_ub rows ({}) ≠ b_ub len ({})",
325                    a.nrows(),
326                    b.len()
327                )));
328            }
329        }
330        if let (Some(a), Some(b)) = (&self.a_eq, &self.b_eq) {
331            if a.ncols() != n {
332                return Err(OptimizeError::InvalidInput(format!(
333                    "A_eq has {} columns but n={}",
334                    a.ncols(),
335                    n
336                )));
337            }
338            if a.nrows() != b.len() {
339                return Err(OptimizeError::InvalidInput(format!(
340                    "A_eq rows ({}) ≠ b_eq len ({})",
341                    a.nrows(),
342                    b.len()
343                )));
344            }
345        }
346
347        Ok(MilpProblem {
348            c: self.c,
349            a_ub: self.a_ub,
350            b_ub: self.b_ub,
351            a_eq: self.a_eq,
352            b_eq: self.b_eq,
353            lb: self.lb,
354            ub: self.ub,
355            constraints: self.constraints,
356        })
357    }
358}
359
360// ─── MilpSolveResult ─────────────────────────────────────────────────────────
361
362/// Result of solving a MILP.
363#[derive(Debug, Clone)]
364pub struct MilpSolveResult {
365    /// Optimal solution vector (length n).
366    pub x: Array1<f64>,
367    /// Optimal objective value (minimisation sense).
368    pub objective: f64,
369    /// `true` if an integer-feasible solution was found.
370    pub success: bool,
371    /// Human-readable status.
372    pub message: String,
373    /// Number of B&B nodes explored.
374    pub nodes_explored: usize,
375    /// Lower bound on the optimal objective at termination.
376    pub lower_bound: f64,
377    /// Total LP sub-problems solved.
378    pub lp_solves: usize,
379    /// Relative optimality gap: `|obj − lb| / (1 + |obj|)`.
380    pub gap: f64,
381}
382
383impl MilpSolveResult {
384    fn from_bnb(r: MilpResultInner) -> Self {
385        let gap = if r.success {
386            (r.obj - r.lower_bound).abs() / (1.0 + r.obj.abs())
387        } else {
388            f64::INFINITY
389        };
390        MilpSolveResult {
391            x: r.x,
392            objective: r.obj,
393            success: r.success,
394            message: r.message,
395            nodes_explored: r.nodes_explored,
396            lower_bound: r.lower_bound,
397            lp_solves: 0,
398            gap,
399        }
400    }
401
402    fn from_mip(r: MipResult) -> Self {
403        let gap = if r.success {
404            (r.fun - r.lower_bound).abs() / (1.0 + r.fun.abs())
405        } else {
406            f64::INFINITY
407        };
408        MilpSolveResult {
409            x: r.x,
410            objective: r.fun,
411            success: r.success,
412            message: r.message,
413            nodes_explored: r.nodes_explored,
414            lower_bound: r.lower_bound,
415            lp_solves: r.lp_solves,
416            gap,
417        }
418    }
419}
420
421// ─── MilpAlgorithm ────────────────────────────────────────────────────────────
422
423/// Algorithm selection for [`MilpSolver`].
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
425pub enum MilpAlgorithm {
426    /// Branch-and-bound with LP relaxation at each node (default).
427    BranchAndBound,
428    /// Gomory cutting planes followed by B&B fallback.
429    CuttingPlanes,
430}
431
432// ─── MilpSolverConfig ─────────────────────────────────────────────────────────
433
434/// Configuration for [`MilpSolver`].
435#[derive(Debug, Clone)]
436pub struct MilpSolverConfig {
437    /// Algorithm to use.
438    pub algorithm: MilpAlgorithm,
439    /// Maximum B&B nodes.
440    pub max_nodes: usize,
441    /// Integrality tolerance.
442    pub int_tol: f64,
443    /// Optimality gap tolerance (relative).
444    pub opt_tol: f64,
445    /// Branching strategy (B&B).
446    pub branching: BranchingStrategy,
447    /// Maximum Gomory cut rounds (`CuttingPlanes` only).
448    pub max_cut_rounds: usize,
449    /// Emit progress messages to stderr.
450    pub verbose: bool,
451}
452
453impl Default for MilpSolverConfig {
454    fn default() -> Self {
455        MilpSolverConfig {
456            algorithm: MilpAlgorithm::BranchAndBound,
457            max_nodes: 50_000,
458            int_tol: 1e-6,
459            opt_tol: 1e-6,
460            branching: BranchingStrategy::MostFractional,
461            max_cut_rounds: 50,
462            verbose: false,
463        }
464    }
465}
466
467// ─── MilpSolver ──────────────────────────────────────────────────────────────
468
469/// Unified MILP solver.
470///
471/// Dispatches to branch-and-bound or cutting-planes based on
472/// [`MilpSolverConfig::algorithm`].
473pub struct MilpSolver {
474    config: MilpSolverConfig,
475}
476
477impl MilpSolver {
478    /// Create with the given configuration.
479    pub fn new(config: MilpSolverConfig) -> Self {
480        MilpSolver { config }
481    }
482
483    /// Solve a [`MilpProblem`].
484    pub fn solve(&self, problem: &MilpProblem) -> OptimizeResult<MilpSolveResult> {
485        match self.config.algorithm {
486            MilpAlgorithm::BranchAndBound => self.solve_bnb(problem),
487            MilpAlgorithm::CuttingPlanes => self.solve_cutting_planes(problem),
488        }
489    }
490
491    fn bnb_config(&self) -> BnbConfig {
492        BnbConfig {
493            max_nodes: self.config.max_nodes,
494            int_tol: self.config.int_tol,
495            abs_gap: self.config.opt_tol,
496            rel_gap: self.config.opt_tol,
497            branching: self.config.branching,
498            ..Default::default()
499        }
500    }
501
502    fn solve_bnb(&self, problem: &MilpProblem) -> OptimizeResult<MilpSolveResult> {
503        let inner = problem.to_inner()?;
504        let r = branch_and_bound(&inner, &self.bnb_config())?;
505        Ok(MilpSolveResult::from_bnb(r))
506    }
507
508    fn solve_cutting_planes(&self, problem: &MilpProblem) -> OptimizeResult<MilpSolveResult> {
509        let lp = problem.to_linear_program();
510        let ivs = problem.to_integer_variable_set();
511        let opts = CuttingPlaneOptions {
512            max_iter: self.config.max_cut_rounds,
513            int_tol: self.config.int_tol,
514            fallback_bb: true,
515            ..Default::default()
516        };
517        let r = CuttingPlaneSolver::with_options(opts).solve(&lp, &ivs)?;
518        Ok(MilpSolveResult::from_mip(r))
519    }
520}
521
522impl Default for MilpSolver {
523    fn default() -> Self {
524        MilpSolver::new(MilpSolverConfig::default())
525    }
526}
527
528// ─── Convenience function ─────────────────────────────────────────────────────
529
530/// Solve a [`MilpProblem`] with default solver settings.
531pub fn solve_milp(problem: &MilpProblem) -> OptimizeResult<MilpSolveResult> {
532    MilpSolver::default().solve(problem)
533}
534
535// ─── Tests ────────────────────────────────────────────────────────────────────
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540    use approx::assert_abs_diff_eq;
541    use scirs2_core::ndarray::{array, Array2};
542
543    fn knapsack_problem(values: &[f64], weights: &[f64], cap: f64) -> MilpProblem {
544        let n = values.len();
545        let c = Array1::from_vec(values.iter().map(|&v| -v).collect());
546        let a = Array2::from_shape_vec((1, n), weights.to_vec()).expect("shape");
547        let b = Array1::from_vec(vec![cap]);
548        MilpProblem::builder(c)
549            .inequalities(a, b)
550            .bounds(Array1::zeros(n), Array1::ones(n))
551            .all_binary()
552            .build()
553            .expect("valid problem")
554    }
555
556    #[test]
557    fn test_builder_basic_shape() {
558        let c = array![-4.0, -5.0, -3.0];
559        let a = Array2::from_shape_vec((1, 3), vec![2.0, 3.0, 1.0]).expect("shape");
560        let b = array![5.0];
561        let p = MilpProblem::builder(c)
562            .inequalities(a, b)
563            .all_binary()
564            .build()
565            .expect("valid");
566        assert_eq!(p.n_vars(), 3);
567        assert_eq!(p.n_ineq(), 1);
568        assert_eq!(p.n_eq(), 0);
569    }
570
571    #[test]
572    fn test_builder_dimension_mismatch() {
573        let c = array![1.0, 2.0];
574        // A has 3 cols but c has 2
575        let a = Array2::from_shape_vec((1, 3), vec![1.0, 1.0, 1.0]).expect("shape");
576        let b = array![5.0];
577        let result = MilpProblem::builder(c)
578            .inequalities(a, b)
579            .all_binary()
580            .build();
581        assert!(result.is_err());
582    }
583
584    #[test]
585    fn test_solve_binary_knapsack() {
586        // max 4x+5y s.t. 2x+3y<=5, x,y in {0,1}
587        // opt: x=1,y=1 weight=5<=5, obj=-9
588        let p = knapsack_problem(&[4.0, 5.0], &[2.0, 3.0], 5.0);
589        let r = solve_milp(&p).expect("solve ok");
590        assert!(r.success);
591        assert!(r.objective <= -8.0 + 1e-4, "obj={}", r.objective);
592    }
593
594    #[test]
595    fn test_solve_pure_integer() {
596        // min x+y s.t. x+y >= 3.5, x,y>=0, integer -> opt=4
597        let c = array![1.0, 1.0];
598        let a = Array2::from_shape_vec((1, 2), vec![-1.0, -1.0]).expect("shape");
599        let b = array![-3.5];
600        let p = MilpProblem::builder(c)
601            .inequalities(a, b)
602            .bounds(array![0.0, 0.0], array![10.0, 10.0])
603            .all_integer()
604            .build()
605            .expect("valid");
606        let r = solve_milp(&p).expect("solve ok");
607        assert!(r.success);
608        assert_abs_diff_eq!(r.objective, 4.0, epsilon = 1e-3);
609    }
610
611    #[test]
612    fn test_solve_with_equalities() {
613        // min x+y s.t. x+y=5, x,y>=0, integer -> opt=5
614        let c = array![1.0, 1.0];
615        let aeq = Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).expect("shape");
616        let beq = array![5.0];
617        let p = MilpProblem::builder(c)
618            .equalities(aeq, beq)
619            .bounds(array![0.0, 0.0], array![10.0, 10.0])
620            .all_integer()
621            .build()
622            .expect("valid");
623        let r = solve_milp(&p).expect("solve ok");
624        assert!(r.success);
625        assert_abs_diff_eq!(r.objective, 5.0, epsilon = 1e-3);
626    }
627
628    #[test]
629    fn test_solve_infeasible() {
630        // x in {0,1}, x >= 2 -> infeasible
631        let c = array![-1.0];
632        let a = Array2::from_shape_vec((1, 1), vec![-1.0]).expect("shape");
633        let b = array![-2.0];
634        let p = MilpProblem::builder(c)
635            .inequalities(a, b)
636            .bounds(array![0.0], array![1.0])
637            .all_binary()
638            .build()
639            .expect("valid");
640        let r = solve_milp(&p).expect("runs");
641        assert!(!r.success);
642    }
643
644    #[test]
645    fn test_solve_cutting_planes() {
646        let c = array![1.0, 1.0];
647        let a = Array2::from_shape_vec((1, 2), vec![-1.0, -1.0]).expect("shape");
648        let b = array![-3.5];
649        let p = MilpProblem::builder(c)
650            .inequalities(a, b)
651            .bounds(array![0.0, 0.0], array![10.0, 10.0])
652            .all_integer()
653            .build()
654            .expect("valid");
655        let cfg = MilpSolverConfig {
656            algorithm: MilpAlgorithm::CuttingPlanes,
657            ..Default::default()
658        };
659        let r = MilpSolver::new(cfg).solve(&p).expect("ok");
660        assert!(r.success);
661        assert_abs_diff_eq!(r.objective, 4.0, epsilon = 1e-3);
662    }
663
664    #[test]
665    fn test_integer_constraint_to_kind() {
666        assert_eq!(
667            IntegerConstraint::Continuous.to_kind(),
668            IntegerKind::Continuous
669        );
670        assert_eq!(IntegerConstraint::Binary.to_kind(), IntegerKind::Binary);
671        assert_eq!(IntegerConstraint::Integer.to_kind(), IntegerKind::Integer);
672        assert_eq!(
673            IntegerConstraint::NonNegativeInteger.to_kind(),
674            IntegerKind::Integer
675        );
676    }
677
678    #[test]
679    fn test_gap_computation() {
680        let p = knapsack_problem(&[5.0, 3.0], &[3.0, 2.0], 4.0);
681        let r = solve_milp(&p).expect("ok");
682        if r.success {
683            assert!(r.gap >= 0.0);
684            assert!(r.gap < 1.0 + 1e-10);
685        }
686    }
687}