use std::{
fmt,
time::{Duration, Instant},
};
use thiserror::Error;
use crate::{
certificate::Certificate,
kkt::{DualVariables, KktError, KktResiduals},
problem::{ProblemError, QpProblem},
workspace::Workspace,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SolveStatus {
Solved,
MaxIterations,
NumericalFailure,
PrimalInfeasible,
DualInfeasible,
}
impl fmt::Display for SolveStatus {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Solved => "solved",
Self::MaxIterations => "maximum iterations reached",
Self::NumericalFailure => "numerical failure",
Self::PrimalInfeasible => "primal infeasible",
Self::DualInfeasible => "dual infeasible",
})
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SolverSettings {
pub max_iterations: usize,
pub absolute_tolerance: f64,
pub relative_tolerance: f64,
pub rho: f64,
pub sigma: f64,
pub check_termination_every: usize,
pub adaptive_rho: bool,
pub adaptive_rho_interval: usize,
pub adaptive_rho_tolerance: f64,
pub adaptive_rho_multiplier: f64,
pub over_relaxation: f64,
pub minimum_rho: f64,
pub maximum_rho: f64,
pub scaling_iterations: usize,
pub infeasibility_tolerance: f64,
pub polish: bool,
pub polish_regularization: f64,
pub polish_refinement_iterations: usize,
}
impl Default for SolverSettings {
fn default() -> Self {
Self {
max_iterations: 10_000,
absolute_tolerance: 1.0e-6,
relative_tolerance: 1.0e-5,
rho: 1.0,
sigma: 1.0e-6,
check_termination_every: 10,
adaptive_rho: true,
adaptive_rho_interval: 25,
adaptive_rho_tolerance: 5.0,
adaptive_rho_multiplier: 2.0,
over_relaxation: 1.6,
minimum_rho: 1.0e-6,
maximum_rho: 1.0e6,
scaling_iterations: 10,
infeasibility_tolerance: 1.0e-5,
polish: true,
polish_regularization: 1.0e-6,
polish_refinement_iterations: 3,
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WarmStart {
pub x: Vec<f64>,
pub equality_dual: Option<Vec<f64>>,
pub inequality_dual: Option<Vec<f64>>,
pub bound_dual: Option<Vec<f64>>,
pub l1_dual: Option<Vec<f64>>,
}
impl WarmStart {
#[must_use]
pub fn from_primal(x: Vec<f64>) -> Self {
Self {
x,
..Self::default()
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ConvergenceDiagnostics {
pub primal_tolerance: f64,
pub dual_tolerance: f64,
pub coefficient_spread_decades: f64,
pub rho_at_limit: bool,
pub hints: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Solution {
pub status: SolveStatus,
pub x: Vec<f64>,
pub dual: DualVariables,
pub objective: f64,
pub residuals: KktResiduals,
pub iterations: usize,
pub solve_time: Duration,
pub final_rho: f64,
pub rho_updates: usize,
pub polished: bool,
pub diagnostics: Option<ConvergenceDiagnostics>,
pub certificate: Option<Certificate>,
}
impl Solution {
#[must_use]
pub fn warm_start(&self) -> WarmStart {
WarmStart {
x: self.x.clone(),
equality_dual: Some(self.dual.equalities.clone()),
inequality_dual: Some(self.dual.inequalities.clone()),
bound_dual: Some(self.dual.bounds.clone()),
l1_dual: Some(self.dual.l1.clone()),
}
}
}
#[derive(Debug, Error)]
pub enum SolverError {
#[error(transparent)]
InvalidProblem(#[from] ProblemError),
#[error("invalid solver setting: {0}")]
InvalidSettings(&'static str),
#[error("dense factor covariance is not positive semidefinite")]
NonPositiveSemidefiniteOmega,
#[error("failed to factor the reduced linear system")]
LinearSystem,
#[error(transparent)]
Kkt(#[from] KktError),
#[error("warm-start {field} has length {actual}; expected {expected}")]
WarmStartDimension {
field: &'static str,
expected: usize,
actual: usize,
},
#[error("warm-start {0} contains a non-finite value")]
WarmStartNonFinite(&'static str),
}
#[derive(Clone, Debug, Default)]
pub struct Solver {
settings: SolverSettings,
}
impl Solver {
#[must_use]
pub const fn new(settings: SolverSettings) -> Self {
Self { settings }
}
#[must_use]
pub const fn settings(&self) -> &SolverSettings {
&self.settings
}
pub fn solve(
&self,
problem: &QpProblem,
warm_start: Option<&WarmStart>,
) -> Result<Solution, SolverError> {
let started = Instant::now();
let mut workspace = Workspace::new(&self.settings, problem)?;
workspace.solve_from(started, warm_start)
}
pub fn workspace(&self, problem: &QpProblem) -> Result<Workspace, SolverError> {
Workspace::new(&self.settings, problem)
}
}
pub(crate) fn validate_settings(settings: &SolverSettings) -> Result<(), SolverError> {
if settings.max_iterations == 0 {
return Err(SolverError::InvalidSettings(
"max_iterations must be positive",
));
}
if !settings.absolute_tolerance.is_finite() || settings.absolute_tolerance <= 0.0 {
return Err(SolverError::InvalidSettings(
"absolute_tolerance must be finite and positive",
));
}
if !settings.relative_tolerance.is_finite() || settings.relative_tolerance < 0.0 {
return Err(SolverError::InvalidSettings(
"relative_tolerance must be finite and non-negative",
));
}
if !settings.rho.is_finite() || settings.rho <= 0.0 {
return Err(SolverError::InvalidSettings(
"rho must be finite and positive",
));
}
if !settings.sigma.is_finite() || settings.sigma <= 0.0 {
return Err(SolverError::InvalidSettings(
"sigma must be finite and positive",
));
}
if settings.check_termination_every == 0 {
return Err(SolverError::InvalidSettings(
"check_termination_every must be positive",
));
}
if settings.adaptive_rho_interval == 0 {
return Err(SolverError::InvalidSettings(
"adaptive_rho_interval must be positive",
));
}
if !settings.adaptive_rho_tolerance.is_finite() || settings.adaptive_rho_tolerance <= 1.0 {
return Err(SolverError::InvalidSettings(
"adaptive_rho_tolerance must be finite and greater than one",
));
}
if !settings.adaptive_rho_multiplier.is_finite() || settings.adaptive_rho_multiplier <= 1.0 {
return Err(SolverError::InvalidSettings(
"adaptive_rho_multiplier must be finite and greater than one",
));
}
if !settings.over_relaxation.is_finite()
|| settings.over_relaxation <= 0.0
|| settings.over_relaxation >= 2.0
{
return Err(SolverError::InvalidSettings(
"over_relaxation must lie strictly between zero and two",
));
}
if !settings.minimum_rho.is_finite() || settings.minimum_rho <= 0.0 {
return Err(SolverError::InvalidSettings(
"minimum_rho must be finite and positive",
));
}
if !settings.maximum_rho.is_finite() || settings.maximum_rho < settings.minimum_rho {
return Err(SolverError::InvalidSettings(
"maximum_rho must be finite and at least minimum_rho",
));
}
if settings.rho < settings.minimum_rho || settings.rho > settings.maximum_rho {
return Err(SolverError::InvalidSettings(
"rho must lie between minimum_rho and maximum_rho",
));
}
if !settings.infeasibility_tolerance.is_finite() || settings.infeasibility_tolerance < 0.0 {
return Err(SolverError::InvalidSettings(
"infeasibility_tolerance must be finite and non-negative",
));
}
if !settings.polish_regularization.is_finite() || settings.polish_regularization <= 0.0 {
return Err(SolverError::InvalidSettings(
"polish_regularization must be finite and positive",
));
}
Ok(())
}
#[cfg(feature = "bench-internals")]
#[doc(hidden)]
pub mod bench_internals {
use super::{QpProblem, SolverError};
use crate::workspace::FactorizedSystem;
pub struct ReducedSystem(FactorizedSystem);
pub fn factorize(
problem: &QpProblem,
rho: f64,
sigma: f64,
) -> Result<ReducedSystem, SolverError> {
FactorizedSystem::new(problem, rho, sigma).map(ReducedSystem)
}
pub fn x_update(system: &ReducedSystem, right_hand_side: &mut [f64]) {
system.0.solve_in_place(right_hand_side);
}
}