pub(crate) mod common;
pub(crate) mod kkt;
pub(crate) mod ippmm;
pub(crate) mod solver_loop;
pub(crate) mod scaling;
use crate::options::SolverOptions;
use crate::problem::SolverResult;
use crate::qp::problem::QpProblem;
#[cfg(test)]
pub(crate) use scaling::check_dfeas_status;
#[cfg(test)]
pub(crate) use scaling::check_dfeas_status_relative;
pub(crate) const TAU: f64 = 0.995;
pub(crate) const BETA_GONDZIO: f64 = 1.0;
pub(crate) const GAMMA_L: f64 = 0.1;
pub(crate) const GAMMA_U: f64 = 10.0;
pub(crate) const ALPHA_IMPROVE_THRESHOLD: f64 = 1e-3;
pub(crate) const IPM_EPS_NOISE_FLOOR: f64 = 100.0 * f64::EPSILON;
pub fn solve_qp_ippmm(problem: &QpProblem, options: &SolverOptions) -> SolverResult {
scaling::solve_with_ruiz_scaling(problem, options, ippmm::solve_ippmm_inner)
}
#[cfg(test)]
mod tests {
use super::*;
use super::scaling::{unscale_ipm_result, compute_amplification, EPS_FLOOR, post_verify_solution};
use crate::linalg::ruiz::RuizScaler;
use crate::options::SolverOptions;
use crate::problem::SolveStatus;
use crate::sparse::CscMatrix;
const EPS: f64 = 1e-5;
fn close(a: f64, b: f64, name: &str) {
assert!(
(a - b).abs() < EPS,
"{}: expected {:.8}, got {:.8} (diff={:.2e})",
name,
b,
a,
(a - b).abs()
);
}
fn default_opts() -> SolverOptions {
SolverOptions::default()
}
#[test]
fn test_ipm_basic_2d() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[-1.0, -1.0], 1, 2).unwrap();
let b = vec![-1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let result = crate::qp::solve_qp_with(&problem, &default_opts());
assert_eq!(result.status, SolveStatus::Optimal);
close(result.solution[0], 0.5, "x[0]");
close(result.solution[1], 0.5, "x[1]");
close(result.objective, 0.5, "objective");
}
#[test]
fn test_ipm_unconstrained() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![-6.0, -8.0];
let a = CscMatrix::new(0, 2);
let b = vec![];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let result = crate::qp::solve_qp_with(&problem, &default_opts());
assert_eq!(result.status, SolveStatus::Optimal);
close(result.solution[0], 3.0, "x[0]");
close(result.solution[1], 4.0, "x[1]");
close(result.objective, -25.0, "objective");
}
#[test]
fn test_ipm_equality_constraint() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(
&[0, 0, 1, 1],
&[0, 1, 0, 1],
&[1.0, 1.0, -1.0, -1.0],
2,
2,
)
.unwrap();
let b = vec![1.0, -1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let opts = SolverOptions { timeout_secs: Some(10.0), ..default_opts() };
let result = crate::qp::solve_qp_with(&problem, &opts);
assert_eq!(result.status, SolveStatus::Optimal);
close(result.solution[0], 0.5, "x[0]");
close(result.solution[1], 0.5, "x[1]");
close(result.objective, 0.5, "objective");
}
#[test]
fn test_ipm_box_constrained() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![-4.0, -4.0];
let a = CscMatrix::new(0, 2);
let b = vec![];
let bounds = vec![(0.0_f64, 1.0_f64); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let mut opts = SolverOptions { timeout_secs: Some(10.0), ..default_opts() };
opts.ipm.eps = 1e-7;
let result = crate::qp::solve_qp_with(&problem, &opts);
assert_eq!(result.status, SolveStatus::Optimal);
close(result.solution[0], 1.0, "x[0]");
close(result.solution[1], 1.0, "x[1]");
close(result.objective, -6.0, "objective");
}
#[test]
fn test_ipm_portfolio() {
let q = CscMatrix::from_triplets(
&[0, 1, 2],
&[0, 1, 2],
&[2.0, 2.0, 2.0],
3,
3,
)
.unwrap();
let c = vec![0.0, 0.0, 0.0];
let a = CscMatrix::from_triplets(
&[0, 0, 0, 1, 1, 1, 2, 3, 4],
&[0, 1, 2, 0, 1, 2, 0, 1, 2],
&[1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0],
5,
3,
)
.unwrap();
let b = vec![1.0, -1.0, 0.0, 0.0, 0.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 3];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let result = crate::qp::solve_qp_with(&problem, &default_opts());
assert_eq!(result.status, SolveStatus::Optimal);
close(result.solution[0], 1.0 / 3.0, "w[0]");
close(result.solution[1], 1.0 / 3.0, "w[1]");
close(result.solution[2], 1.0 / 3.0, "w[2]");
close(result.objective, 1.0 / 3.0, "objective");
}
#[test]
fn test_ipm_timeout() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[-1.0, -1.0], 1, 2).unwrap();
let b = vec![-1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let mut opts = SolverOptions { timeout_secs: Some(0.0001), ..Default::default() };
opts.use_ruiz_scaling = false;
let result = crate::qp::solve_qp_with(&problem, &opts);
assert!(
result.status == SolveStatus::Timeout || result.status == SolveStatus::Optimal,
"got {:?}", result.status
);
}
#[test]
fn test_ipm_post_unscaling_false_optimal_detection() {
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let c_vec = vec![0.0];
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let b = vec![0.5];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY)];
let problem = QpProblem::new_all_le(q, c_vec, a, b, bounds).unwrap();
let mut scaler = RuizScaler::new(1, 1);
scaler.d = vec![2.0];
scaler.e = vec![1.0];
scaler.c = 1.0;
let mock_result = SolverResult {
status: SolveStatus::Optimal,
solution: vec![1.0],
dual_solution: vec![0.0],
objective: 1.0,
..SolverResult::default()
};
let eps = 1e-6_f64;
let result = unscale_ipm_result(mock_result, &scaler, &problem, eps);
assert_ne!(result.status, SolveStatus::Optimal, "got {:?}", result.status);
assert_eq!(result.status, SolveStatus::SuboptimalSolution);
close(result.solution[0], 2.0, "x[0] unscaled");
}
#[test]
fn test_ipm_post_unscaling_genuine_optimal_preserved() {
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let c_vec = vec![0.0];
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let b = vec![1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY)];
let problem = QpProblem::new_all_le(q, c_vec, a, b, bounds).unwrap();
let scaler = RuizScaler::new(1, 1);
let mock_result = SolverResult {
status: SolveStatus::Optimal,
solution: vec![0.0],
dual_solution: vec![0.0],
objective: 0.0,
..SolverResult::default()
};
let eps = 1e-6_f64;
let result = unscale_ipm_result(mock_result, &scaler, &problem, eps);
assert_eq!(result.status, SolveStatus::Optimal);
}
#[test]
fn test_ipm_bfeas_violation_detected() {
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let c_vec = vec![0.0];
let a = CscMatrix::new(0, 1);
let b = vec![];
let bounds = vec![(0.0_f64, 0.5_f64)];
let problem = QpProblem::new_all_le(q, c_vec, a, b, bounds).unwrap();
let scaler = RuizScaler::new(1, 0);
let mock_result = SolverResult {
status: SolveStatus::Optimal,
solution: vec![1.0],
dual_solution: vec![],
objective: 1.0,
..SolverResult::default()
};
let eps = 1e-6_f64;
let result = unscale_ipm_result(mock_result, &scaler, &problem, eps);
assert_ne!(result.status, SolveStatus::Optimal, "got {:?}", result.status);
assert_eq!(result.status, SolveStatus::SuboptimalSolution);
}
#[test]
fn test_ipm_bfeas_within_bounds_preserved() {
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let c_vec = vec![0.0];
let a = CscMatrix::new(0, 1);
let b = vec![];
let bounds = vec![(-1.0_f64, 1.0_f64)];
let problem = QpProblem::new_all_le(q, c_vec, a, b, bounds).unwrap();
let scaler = RuizScaler::new(1, 0);
let mock_result = SolverResult {
status: SolveStatus::Optimal,
solution: vec![0.0],
dual_solution: vec![],
objective: 0.0,
..SolverResult::default()
};
let eps = 1e-6_f64;
let result = unscale_ipm_result(mock_result, &scaler, &problem, eps);
assert_eq!(result.status, SolveStatus::Optimal);
}
#[test]
fn test_ipm_dfeas_violation_detected() {
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let c_vec = vec![1.0];
let a = CscMatrix::new(0, 1);
let b = vec![];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY)];
let problem = QpProblem::new_all_le(q, c_vec, a, b, bounds).unwrap();
let scaler = RuizScaler::new(1, 0);
let mock_result = SolverResult {
status: SolveStatus::Optimal,
solution: vec![10.0],
dual_solution: vec![],
objective: 110.0,
..SolverResult::default()
};
let eps = 1e-6_f64;
let result = unscale_ipm_result(mock_result, &scaler, &problem, eps);
assert_ne!(result.status, SolveStatus::Optimal, "got {:?}", result.status);
assert_eq!(result.status, SolveStatus::SuboptimalSolution);
}
#[test]
fn test_ipm_dfeas_within_tolerance_preserved() {
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let c_vec = vec![1.0];
let a = CscMatrix::new(0, 1);
let b = vec![];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY)];
let problem = QpProblem::new_all_le(q, c_vec, a, b, bounds).unwrap();
let scaler = RuizScaler::new(1, 0);
let mock_result = SolverResult {
status: SolveStatus::Optimal,
solution: vec![-0.5],
dual_solution: vec![],
objective: -0.25,
..SolverResult::default()
};
let eps = 1e-6_f64;
let result = unscale_ipm_result(mock_result, &scaler, &problem, eps);
assert_eq!(result.status, SolveStatus::Optimal);
}
#[test]
fn test_compute_amplification_calculation() {
let mut scaler = RuizScaler::new(2, 2);
scaler.d = vec![0.01, 1.0];
scaler.e = vec![0.1, 1.0];
scaler.c = 1.0;
let amp = compute_amplification(&scaler);
assert!((amp - 100.0).abs() < 1e-10, "got {:.6}", amp);
}
#[test]
fn test_adjusted_eps_less_than_user_eps() {
let mut scaler = RuizScaler::new(2, 1);
scaler.d = vec![0.1, 0.5];
scaler.e = vec![0.5];
scaler.c = 1.0;
let amp = compute_amplification(&scaler);
assert!(amp > 1.0, "got {:.6}", amp);
let user_eps = 1e-6_f64;
let adjusted_eps = (user_eps / amp).max(EPS_FLOOR);
assert!(adjusted_eps < user_eps);
}
#[test]
fn test_ipm_bound_duals_lb_only_active() {
let q = CscMatrix::from_triplets(&[0], &[0], &[0.001], 1, 1).unwrap();
let c_vec = vec![1.0];
let a = CscMatrix::new(0, 1);
let b = vec![];
let bounds = vec![(1.0_f64, f64::INFINITY)];
let problem = QpProblem::new_all_le(q, c_vec, a, b, bounds).unwrap();
let opts = SolverOptions { use_ruiz_scaling: false, ..Default::default() };
let result = crate::qp::solve_qp_with(&problem, &opts);
assert_eq!(result.status, SolveStatus::Optimal);
assert!((result.solution[0] - 1.0).abs() < 1e-4, "got {:.6}", result.solution[0]);
assert!(!result.bound_duals.is_empty());
assert!(
(result.bound_duals[0] - 1.001).abs() < 0.1,
"got {:.6}", result.bound_duals[0]
);
}
#[test]
fn test_ipm_bound_duals_ub_only_active() {
let q = CscMatrix::from_triplets(&[0], &[0], &[0.001], 1, 1).unwrap();
let c_vec = vec![-1.0];
let a = CscMatrix::new(0, 1);
let b = vec![];
let bounds = vec![(f64::NEG_INFINITY, 0.5_f64)];
let problem = QpProblem::new_all_le(q, c_vec, a, b, bounds).unwrap();
let opts = SolverOptions { use_ruiz_scaling: false, ..Default::default() };
let result = crate::qp::solve_qp_with(&problem, &opts);
assert_eq!(result.status, SolveStatus::Optimal);
assert!((result.solution[0] - 0.5).abs() < 1e-4, "got {:.6}", result.solution[0]);
assert!(!result.bound_duals.is_empty());
assert!((result.bound_duals[0] - 1.0).abs() < 0.1, "got {:.6}", result.bound_duals[0]);
}
#[test]
fn test_ipm_bound_duals_box_constrained() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c_vec = vec![-4.0, -4.0];
let a = CscMatrix::new(0, 2);
let b = vec![];
let bounds = vec![(0.0_f64, 1.0_f64), (0.0_f64, 1.0_f64)];
let problem = QpProblem::new_all_le(q, c_vec, a, b, bounds).unwrap();
let opts = SolverOptions { use_ruiz_scaling: false, ..Default::default() };
let result = crate::qp::solve_qp_with(&problem, &opts);
assert_eq!(result.status, SolveStatus::Optimal);
assert!((result.solution[0] - 1.0).abs() < 0.01);
assert!((result.solution[1] - 1.0).abs() < 0.01);
assert_eq!(result.bound_duals.len(), 4);
assert!(result.bound_duals[2] > 0.5, "got {:.6}", result.bound_duals[2]);
assert!(result.bound_duals[3] > 0.5, "got {:.6}", result.bound_duals[3]);
}
#[test]
fn test_ipm_post_verify_timeout_stays_within_budget() {
let q = CscMatrix::from_triplets(&[0, 1, 2], &[0, 1, 2], &[2.0, 2.0, 2.0], 3, 3).unwrap();
let c = vec![0.0; 3];
let a = CscMatrix::from_triplets(&[0, 0, 0], &[0, 1, 2], &[-1.0, -1.0, -1.0], 1, 3).unwrap();
let b = vec![-1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 3];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let timeout_secs = 0.01;
let mut opts = SolverOptions { timeout_secs: Some(timeout_secs), ..Default::default() };
opts.use_ruiz_scaling = true;
let start = std::time::Instant::now();
let result = crate::qp::solve_qp_with(&problem, &opts);
let elapsed = start.elapsed().as_secs_f64();
assert!(
result.status == SolveStatus::Timeout || result.status == SolveStatus::Optimal,
"got {:?}", result.status
);
assert!(elapsed < timeout_secs * 2.0, "elapsed={:.3}s", elapsed);
}
#[test]
fn test_ipm_ruiz_scaling_consistency() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[-1.0, -1.0], 1, 2).unwrap();
let b = vec![-1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let result_ruiz = crate::qp::solve_qp_with(&problem, &SolverOptions::default());
let opts_no_ruiz = SolverOptions { use_ruiz_scaling: false, ..Default::default() };
let result_no_ruiz = crate::qp::solve_qp_with(&problem, &opts_no_ruiz);
assert_eq!(result_ruiz.status, SolveStatus::Optimal);
assert_eq!(result_no_ruiz.status, SolveStatus::Optimal);
close(result_ruiz.solution[0], result_no_ruiz.solution[0], "x[0]");
close(result_ruiz.solution[1], result_no_ruiz.solution[1], "x[1]");
close(result_ruiz.objective, result_no_ruiz.objective, "objective");
}
#[test]
fn test_a2t02_ipm_timeout_zero_returns_immediately() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[-1.0, -1.0], 1, 2).unwrap();
let b = vec![-1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let opts = SolverOptions { timeout_secs: Some(0.0), ..SolverOptions::default() };
let start = std::time::Instant::now();
let result = crate::qp::solve_qp_with(&problem, &opts);
let elapsed = start.elapsed().as_secs_f64();
assert!(
result.status == SolveStatus::Timeout || result.status == SolveStatus::Optimal,
"got: {:?}", result.status
);
assert!(elapsed < 0.5, "elapsed={:.3}s", elapsed);
}
#[test]
fn test_a5s01_scaling_solution_equivalence_constrained() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(
&[0, 0, 1, 1], &[0, 1, 0, 1], &[1.0, 1.0, -1.0, -1.0], 2, 2,
).unwrap();
let b = vec![1.0, -1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let result_ruiz = crate::qp::solve_qp_with(&problem, &SolverOptions::default());
let opts_no_ruiz = SolverOptions { use_ruiz_scaling: false, ..SolverOptions::default() };
let result_no_ruiz = crate::qp::solve_qp_with(&problem, &opts_no_ruiz);
assert_eq!(result_ruiz.status, SolveStatus::Optimal);
assert_eq!(result_no_ruiz.status, SolveStatus::Optimal);
assert!((result_ruiz.solution[0] - result_no_ruiz.solution[0]).abs() < 1e-4);
assert!((result_ruiz.solution[1] - result_no_ruiz.solution[1]).abs() < 1e-4);
}
#[test]
fn test_a5s02_post_verify_no_false_optimal() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[-1.0, -1.0], 1, 2).unwrap();
let b = vec![-1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let opts = SolverOptions { use_ruiz_scaling: true, ..SolverOptions::default() };
let result = crate::qp::solve_qp_with(&problem, &opts);
assert_ne!(result.status, SolveStatus::SuboptimalSolution);
assert_eq!(result.status, SolveStatus::Optimal);
}
#[test]
fn test_post_verify_row_norm_rejects_small_row_violation() {
use crate::problem::ConstraintType;
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[1e6_f64, 1.0], 2, 2).unwrap();
let b = vec![1e6_f64, 1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new(
q, c, a, b, bounds,
vec![ConstraintType::Eq, ConstraintType::Eq],
).unwrap();
let delta = 1e-5_f64;
let mock_result = SolverResult {
status: SolveStatus::SuboptimalSolution,
solution: vec![1.0, 1.0 + delta],
dual_solution: vec![0.0, 0.0],
..SolverResult::default()
};
let eps = 1e-6_f64;
let result = post_verify_solution(mock_result, &problem, eps);
assert_eq!(result.status, SolveStatus::SuboptimalSolution, "got {:?}", result.status);
}
#[test]
fn test_post_verify_row_norm_accepts_exact_solution() {
use crate::problem::ConstraintType;
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[1e6_f64, 1.0], 2, 2).unwrap();
let b = vec![1e6_f64, 1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new(
q, c, a, b, bounds,
vec![ConstraintType::Eq, ConstraintType::Eq],
).unwrap();
let mock_result = SolverResult {
status: SolveStatus::SuboptimalSolution,
solution: vec![1.0, 1.0],
dual_solution: vec![-2e-6_f64, -2.0],
..SolverResult::default()
};
let eps = 1e-6_f64;
let result = post_verify_solution(mock_result, &problem, eps);
assert_eq!(result.status, SolveStatus::Optimal, "got {:?}", result.status);
}
#[test]
fn test_ipm_ge_defensive() {
use crate::problem::ConstraintType;
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let b = vec![1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new(q, c, a, b, bounds, vec![ConstraintType::Ge]).unwrap();
let opts = SolverOptions { timeout_secs: Some(5.0), ..SolverOptions::default() };
let start = std::time::Instant::now();
let result = crate::qp::solve_qp_with(&problem, &opts);
let elapsed = start.elapsed().as_secs_f64();
assert_eq!(result.status, SolveStatus::Optimal);
close(result.solution[0], 0.5, "x[0]");
close(result.solution[1], 0.5, "x[1]");
assert!(elapsed < 6.0, "elapsed={:.3}s", elapsed);
}
#[test]
fn test_ipm_empty_constraints() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[1.0, 1.0], 2, 2).unwrap();
let c = vec![-1.0, -1.0];
let a = CscMatrix::new(0, 2);
let b = vec![];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let opts = SolverOptions { timeout_secs: Some(5.0), ..SolverOptions::default() };
let result = crate::qp::solve_qp_with(&problem, &opts);
assert_eq!(result.status, SolveStatus::Optimal);
}
}