use crate::errors::*;
use crate::{
Bool, ExtensibleOut, FALSE, Index, Number, TRUE, ffi_guard, struct_size_of, write_extensible,
};
use pounce_convex::hsde_scalars::{self, HsdeScalars};
use pounce_convex::pipeline::{
EngineSwitches, check_ipm_inertia, demote_unverified_optimal, solve_presolved,
};
use pounce_convex::{
ActiveSetOverrides, ConeSpec, HessianInertia, NEG_INF, POS_INF, QpOptions, QpProblem,
QpSolution, QpStatus, Triplet, certificate_scale, certify_psd_lower_triangle_in_band,
qp_status_to_ars, reported_kkt_residuals, solve_qp_active_set_inertia, solve_qp_ipm,
solve_socp_ipm,
};
use pounce_feral::FeralSolverInterface;
use pounce_linsol::SparseSymLinearSolverInterface;
use std::ffi::{CStr, c_char, c_int};
use std::time::Duration;
pub const POUNCE_QP_STATUS_OPTIMAL: c_int = 0;
pub const POUNCE_QP_STATUS_OPTIMAL_INACCURATE: c_int = 1;
pub const POUNCE_QP_STATUS_PRIMAL_INFEASIBLE: c_int = 2;
pub const POUNCE_QP_STATUS_DUAL_INFEASIBLE: c_int = 3;
pub const POUNCE_QP_STATUS_ITERATION_LIMIT: c_int = 4;
pub const POUNCE_QP_STATUS_TIME_LIMIT: c_int = 5;
pub const POUNCE_QP_STATUS_NUMERICAL_FAILURE: c_int = 6;
pub const POUNCE_CONE_NONNEG: c_int = 0;
pub const POUNCE_CONE_SOC: c_int = 1;
pub const POUNCE_CONE_EXP: c_int = 2;
pub const POUNCE_CONE_POW: c_int = 3;
pub const POUNCE_CONE_PSD: c_int = 4;
pub const POUNCE_QP_CERT_NONE: c_int = 0;
pub const POUNCE_QP_CERT_PRIMAL_INFEASIBLE: c_int = 1;
pub const POUNCE_QP_CERT_DUAL_INFEASIBLE: c_int = 2;
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct PounceQpResiduals {
pub struct_size: Index,
pub primal_infeasibility: Number,
pub dual_infeasibility: Number,
pub complementarity: Number,
pub kkt_error: Number,
pub primal_infeasibility_raw: Number,
pub dual_infeasibility_raw: Number,
pub complementarity_raw: Number,
pub kkt_error_raw: Number,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct PounceQpCertificate {
pub struct_size: Index,
pub kind: Index,
pub scale: Number,
pub tau: Number,
pub kappa: Number,
}
unsafe impl ExtensibleOut for PounceQpResiduals {}
unsafe impl ExtensibleOut for PounceQpCertificate {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Method {
Ipm,
ActiveSet,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InertiaChoice {
Auto,
Claim(HessianInertia),
}
#[derive(Debug, Clone, Copy)]
struct Settings {
tol: Option<f64>,
max_iter: Option<usize>,
time_limit: Option<Duration>,
tau: Option<f64>,
tau_max: Option<f64>,
method: Method,
inertia: InertiaChoice,
switches: EngineSwitches,
}
impl Default for Settings {
fn default() -> Self {
Self {
tol: None,
max_iter: None,
time_limit: None,
tau: None,
tau_max: None,
method: Method::Ipm,
inertia: InertiaChoice::Auto,
switches: EngineSwitches::default(),
}
}
}
impl Settings {
fn qp_options(&self) -> QpOptions {
let mut o = pounce_convex::qp_options_from_overrides(
self.tol,
self.max_iter,
self.tau,
self.tau_max,
self.time_limit,
);
self.switches.apply(&mut o);
o
}
}
struct QpLast {
sol: QpSolution,
engine: &'static str,
residuals: PounceQpResiduals,
hsde: Option<HsdeScalars>,
seconds: f64,
}
pub struct PounceQpInfo {
n: usize,
m_eq: usize,
m_ineq: usize,
index_style: Index,
p: Vec<Triplet>,
c: Option<Vec<f64>>,
a: Vec<Triplet>,
b: Option<Vec<f64>>,
g: Vec<Triplet>,
h: Option<Vec<f64>>,
lb: Vec<f64>,
ub: Vec<f64>,
cones: Vec<ConeSpec>,
settings: Settings,
psd_verdict: Option<Result<bool, ()>>,
last: Option<QpLast>,
}
pub type PounceQp = *mut PounceQpInfo;
fn backend() -> Box<dyn SparseSymLinearSolverInterface> {
Box::new(FeralSolverInterface::new())
}
unsafe fn slice<'a, T>(ptr: *const T, len: usize) -> Option<&'a [T]> {
if len == 0 {
Some(&[])
} else if ptr.is_null() {
None
} else {
Some(std::slice::from_raw_parts(ptr, len))
}
}
impl PounceQpInfo {
unsafe fn triplets(
&self,
nnz: Index,
rows: usize,
r: *const Index,
c: *const Index,
v: *const Number,
lower: bool,
) -> Option<Vec<Triplet>> {
let nnz = usize::try_from(nnz).ok()?;
let (r, c, v) = (slice(r, nnz)?, slice(c, nnz)?, slice(v, nnz)?);
let shift = self.index_style;
let mut out = Vec::with_capacity(nnz);
for k in 0..nnz {
let row = usize::try_from(r[k].checked_sub(shift)?).ok()?;
let col = usize::try_from(c[k].checked_sub(shift)?).ok()?;
if row >= rows || col >= self.n || !v[k].is_finite() || (lower && row < col) {
return None;
}
out.push(Triplet::new(row, col, v[k]));
}
Some(out)
}
unsafe fn finite_vec(ptr: *const Number, len: usize) -> Option<Vec<f64>> {
let s = slice(ptr, len)?;
s.iter().all(|x| x.is_finite()).then(|| s.to_vec())
}
fn problem(&self) -> QpProblem {
QpProblem {
n: self.n,
p_lower: self.p.clone(),
c: self.c.clone().unwrap_or_default(),
a: self.a.clone(),
b: self.b.clone().unwrap_or_default(),
g: self.g.clone(),
h: self.h.clone().unwrap_or_default(),
lb: self.lb.clone(),
ub: self.ub.clone(),
}
}
}
#[cfg(test)]
thread_local! {
static CERTIFICATE_RUNS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
fn psd_inertia(qp: &mut PounceQpInfo) -> Result<HessianInertia, ()> {
if let InertiaChoice::Claim(h) = qp.settings.inertia {
return Ok(h);
}
if qp.p.is_empty() {
return Ok(HessianInertia::Psd);
}
let verdict = match qp.psd_verdict {
Some(v) => v,
None => {
#[cfg(test)]
CERTIFICATE_RUNS.with(|c| c.set(c.get() + 1));
let v = certify_psd_lower_triangle_in_band(qp.n, &qp.p, backend).map_err(|_| ());
qp.psd_verdict = Some(v);
v
}
};
verdict.map(|certified| {
if certified {
HessianInertia::Psd
} else {
HessianInertia::Indefinite
}
})
}
fn status_code(s: QpStatus) -> c_int {
match s {
QpStatus::Optimal => POUNCE_QP_STATUS_OPTIMAL,
QpStatus::OptimalInaccurate => POUNCE_QP_STATUS_OPTIMAL_INACCURATE,
QpStatus::PrimalInfeasible => POUNCE_QP_STATUS_PRIMAL_INFEASIBLE,
QpStatus::DualInfeasible => POUNCE_QP_STATUS_DUAL_INFEASIBLE,
QpStatus::IterationLimit => POUNCE_QP_STATUS_ITERATION_LIMIT,
QpStatus::TimeLimit => POUNCE_QP_STATUS_TIME_LIMIT,
QpStatus::NumericalFailure => POUNCE_QP_STATUS_NUMERICAL_FAILURE,
}
}
fn residuals(sol: &QpSolution, prob: &QpProblem, cones: &[ConeSpec]) -> PounceQpResiduals {
let (r, raw) = reported_kkt_residuals(sol, prob, cones);
PounceQpResiduals {
struct_size: struct_size_of::<PounceQpResiduals>(),
primal_infeasibility: r.primal_infeasibility,
dual_infeasibility: r.dual_infeasibility,
complementarity: r.complementarity,
kkt_error: r.kkt_error(),
primal_infeasibility_raw: raw.primal_infeasibility,
dual_infeasibility_raw: raw.dual_infeasibility,
complementarity_raw: raw.complementarity,
kkt_error_raw: raw.kkt_error(),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceCreateQp(
n: Index,
m_eq: Index,
m_ineq: Index,
index_style: Index,
) -> PounceQp {
ffi_guard(std::ptr::null_mut(), || {
let (Ok(n), Ok(m_eq), Ok(m_ineq)) = (
usize::try_from(n),
usize::try_from(m_eq),
usize::try_from(m_ineq),
) else {
return std::ptr::null_mut();
};
if n == 0 || !(index_style == 0 || index_style == 1) {
return std::ptr::null_mut();
}
Box::into_raw(Box::new(PounceQpInfo {
n,
m_eq,
m_ineq,
index_style,
p: Vec::new(),
c: None,
a: Vec::new(),
b: (m_eq == 0).then(Vec::new),
g: Vec::new(),
h: (m_ineq == 0).then(Vec::new),
lb: vec![NEG_INF; n],
ub: vec![POS_INF; n],
cones: Vec::new(),
settings: Settings::default(),
psd_verdict: None,
last: None,
}))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceFreeQp(qp: PounceQp) {
if !qp.is_null() {
drop(Box::from_raw(qp));
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceQpSetObjective(
qp: PounceQp,
nnz_p: Index,
p_row: *const Index,
p_col: *const Index,
p_val: *const Number,
c: *const Number,
) -> Bool {
ffi_guard(FALSE, || {
let Some(info) = qp.as_mut() else {
return FALSE;
};
let Some(p) = info.triplets(nnz_p, info.n, p_row, p_col, p_val, true) else {
return FALSE;
};
let Some(c) = PounceQpInfo::finite_vec(c, info.n) else {
return FALSE;
};
info.p = p;
info.c = Some(c);
info.psd_verdict = None;
info.last = None;
TRUE
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceQpSetEqualities(
qp: PounceQp,
nnz_a: Index,
a_row: *const Index,
a_col: *const Index,
a_val: *const Number,
b: *const Number,
) -> Bool {
ffi_guard(FALSE, || {
let Some(info) = qp.as_mut() else {
return FALSE;
};
let Some(a) = info.triplets(nnz_a, info.m_eq, a_row, a_col, a_val, false) else {
return FALSE;
};
let Some(b) = PounceQpInfo::finite_vec(b, info.m_eq) else {
return FALSE;
};
info.a = a;
info.b = Some(b);
info.last = None;
TRUE
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceQpSetInequalities(
qp: PounceQp,
nnz_g: Index,
g_row: *const Index,
g_col: *const Index,
g_val: *const Number,
h: *const Number,
) -> Bool {
ffi_guard(FALSE, || {
let Some(info) = qp.as_mut() else {
return FALSE;
};
let Some(g) = info.triplets(nnz_g, info.m_ineq, g_row, g_col, g_val, false) else {
return FALSE;
};
let Some(h) = PounceQpInfo::finite_vec(h, info.m_ineq) else {
return FALSE;
};
info.g = g;
info.h = Some(h);
info.last = None;
TRUE
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceQpSetBounds(
qp: PounceQp,
lb: *const Number,
ub: *const Number,
) -> Bool {
ffi_guard(FALSE, || {
let Some(info) = qp.as_mut() else {
return FALSE;
};
let n = info.n;
let read = |p: *const Number, absent: f64, is_absent: fn(f64) -> bool| {
if p.is_null() {
return Some(vec![absent; n]);
}
let s = std::slice::from_raw_parts(p, n);
s.iter()
.map(|&v| {
if v.is_nan() {
None
} else if is_absent(v) {
Some(absent)
} else {
Some(v)
}
})
.collect::<Option<Vec<f64>>>()
};
let Some(l) = read(lb, NEG_INF, |v| v <= -1e19) else {
return FALSE;
};
let Some(u) = read(ub, POS_INF, |v| v >= 1e19) else {
return FALSE;
};
if l.iter()
.zip(&u)
.any(|(&lo, &hi)| lo >= 1e19 || hi <= -1e19 || lo > hi)
{
return FALSE;
}
info.lb = l;
info.ub = u;
info.last = None;
TRUE
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceQpAddCone(
qp: PounceQp,
kind: c_int,
dim: Index,
alpha: Number,
) -> c_int {
ffi_guard(POUNCE_ERR_INTERNAL, || {
let Some(info) = qp.as_mut() else {
return POUNCE_ERR_NULL;
};
let Ok(d) = usize::try_from(dim) else {
return POUNCE_ERR_ARGUMENT;
};
let spec = match kind {
POUNCE_CONE_NONNEG => ConeSpec::Nonneg(d),
POUNCE_CONE_SOC if d >= 1 => ConeSpec::SecondOrder(d),
POUNCE_CONE_EXP if d == 3 => ConeSpec::Exponential,
POUNCE_CONE_POW if d == 3 && alpha > 0.0 && alpha < 1.0 => ConeSpec::Power(alpha),
POUNCE_CONE_PSD if d >= 1 && d.checked_mul(d + 1).is_some() => ConeSpec::Psd(d),
_ => return POUNCE_ERR_ARGUMENT,
};
let Ok(pos) = c_int::try_from(info.cones.len()) else {
return POUNCE_ERR_ARGUMENT;
};
info.cones.push(spec);
info.last = None;
pos
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceQpSetNumOption(
qp: PounceQp,
key: *const c_char,
value: Number,
) -> Bool {
ffi_guard(FALSE, || {
let Some(info) = qp.as_mut() else {
return FALSE;
};
if key.is_null() {
return FALSE;
}
let Ok(key) = CStr::from_ptr(key).to_str() else {
return FALSE;
};
let unit = value.is_finite() && value > 0.0 && value < 1.0;
let s = &mut info.settings;
match key {
"tol" if unit => s.tol = Some(value),
"max_iter" if value >= 1.0 && value.fract() == 0.0 && value <= 1e15 => {
s.max_iter = Some(value as usize)
}
"time_limit" => match Duration::try_from_secs_f64(value) {
Ok(d) => s.time_limit = Some(d),
Err(_) => return FALSE,
},
"tau" if unit => s.tau = Some(value),
"tau_max" if unit => s.tau_max = Some(value),
"qp_reg" if value.is_finite() && value >= 0.0 => s.switches.reg = Some(value),
_ => return FALSE,
}
TRUE
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceQpSetStrOption(
qp: PounceQp,
key: *const c_char,
value: *const c_char,
) -> Bool {
ffi_guard(FALSE, || {
let Some(info) = qp.as_mut() else {
return FALSE;
};
if key.is_null() || value.is_null() {
return FALSE;
}
let (Ok(key), Ok(value)) = (CStr::from_ptr(key).to_str(), CStr::from_ptr(value).to_str())
else {
return FALSE;
};
let yes_no = match value {
"yes" => Some(true),
"no" => Some(false),
_ => None,
};
let s = &mut info.settings;
match (key, value, yes_no) {
("method", "ipm", _) => s.method = Method::Ipm,
("method", "active-set", _) => s.method = Method::ActiveSet,
("hessian_inertia", "auto", _) => s.inertia = InertiaChoice::Auto,
("hessian_inertia", "psd", _) => s.inertia = InertiaChoice::Claim(HessianInertia::Psd),
("hessian_inertia", "indefinite", _) => {
s.inertia = InertiaChoice::Claim(HessianInertia::Indefinite)
}
("hessian_inertia", "unknown", _) => {
s.inertia = InertiaChoice::Claim(HessianInertia::Unknown)
}
("qp_presolve", _, Some(v)) => s.switches.presolve = Some(v),
("qp_hsde", _, Some(v)) => s.switches.hsde = Some(v),
("qp_equilibrate", _, Some(v)) => s.switches.equilibrate = Some(v),
("qp_crossover", _, Some(v)) => s.switches.crossover = Some(v),
_ => return FALSE,
}
if key == "hessian_inertia" {
info.psd_verdict = None;
}
TRUE
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceQpSolve(qp: PounceQp) -> c_int {
let Some(info) = qp.as_mut() else {
return POUNCE_ERR_NULL;
};
info.last = None;
ffi_guard(POUNCE_ERR_INTERNAL, || solve(info))
}
fn solve(info: &mut PounceQpInfo) -> c_int {
if info.c.is_none() || info.b.is_none() || info.h.is_none() {
return POUNCE_ERR_INCOMPLETE;
}
let s = info.settings;
let conic = !info.cones.is_empty();
if conic {
let nonsym = info
.cones
.iter()
.any(|c| matches!(c, ConeSpec::Exponential | ConeSpec::Power(_)));
let psd = info.cones.iter().any(|c| matches!(c, ConeSpec::Psd(_)));
let rows = info
.cones
.iter()
.try_fold(0usize, |acc, c| acc.checked_add(c.dim()));
if (nonsym && psd) || rows != Some(info.m_ineq) {
return POUNCE_ERR_CONES;
}
if s.method != Method::Ipm
|| s.tau.is_some()
|| s.tau_max.is_some()
|| s.switches != EngineSwitches::default()
|| s.inertia != InertiaChoice::Auto
{
return POUNCE_ERR_OPTIONS;
}
} else if s
.switches
.check(s.method == Method::ActiveSet, false)
.is_err()
{
return POUNCE_ERR_OPTIONS;
}
let Ok(inertia) = psd_inertia(info) else {
return POUNCE_ERR_ARGUMENT;
};
if s.method == Method::Ipm && check_ipm_inertia(inertia).is_err() {
return POUNCE_ERR_INDEFINITE;
}
let prob = info.problem();
let o = s.qp_options();
let presolve_on = s.switches.presolve.unwrap_or(false);
let t0 = std::time::Instant::now();
hsde_scalars::clear();
pounce_convex::crossover::clear_report();
let (sol, engine) = if conic {
(solve_socp_ipm(&prob, &info.cones, &o, backend), "cvx-qcqp")
} else if s.method == Method::Ipm {
let sol = if presolve_on {
let (sol, _) = solve_presolved(&prob, &o, |p, po| solve_qp_ipm(p, po, backend));
demote_unverified_optimal(&prob, &o, sol)
} else {
solve_qp_ipm(&prob, &o, backend)
};
(sol, "cvx-qp")
} else {
let ov = ActiveSetOverrides::default();
let sol = if presolve_on {
solve_presolved(&prob, &o, |p, po| {
solve_qp_active_set_inertia(p, po, &ov, inertia, &mut backend)
})
.0
} else {
solve_qp_active_set_inertia(&prob, &o, &ov, inertia, &mut backend)
};
(sol, "qp-active-set")
};
let seconds = t0.elapsed().as_secs_f64();
let hsde = hsde_scalars::take();
pounce_convex::crossover::clear_report();
let code = status_code(sol.status);
info.last = Some(QpLast {
residuals: residuals(&sol, &prob, &info.cones),
sol,
engine,
hsde,
seconds,
});
code
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceQpGetSolution(
qp: PounceQp,
x: *mut Number,
y: *mut Number,
z: *mut Number,
z_lb: *mut Number,
z_ub: *mut Number,
obj: *mut Number,
iters: *mut Index,
) -> Bool {
ffi_guard(FALSE, || {
let Some(last) = qp.as_ref().and_then(|i| i.last.as_ref()) else {
return FALSE;
};
let info = &*qp;
let sol = &last.sol;
let outs = [
(x, &sol.x, info.n),
(y, &sol.y, info.m_eq),
(z, &sol.z, info.m_ineq),
(z_lb, &sol.z_lb, info.n),
(z_ub, &sol.z_ub, info.n),
];
if outs.iter().any(|(_, src, len)| src.len() != *len) {
return FALSE;
}
for (dst, src, len) in outs {
if !dst.is_null() {
std::ptr::copy_nonoverlapping(src.as_ptr(), dst, len);
}
}
if !obj.is_null() {
*obj = sol.obj;
}
if !iters.is_null() {
*iters = Index::try_from(sol.iters).unwrap_or(Index::MAX);
}
TRUE
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceQpGetResiduals(qp: PounceQp, out: *mut PounceQpResiduals) -> Bool {
ffi_guard(FALSE, || {
let Some(last) = qp.as_ref().and_then(|i| i.last.as_ref()) else {
return FALSE;
};
if out.is_null() {
return FALSE;
}
Bool::from(write_extensible(out, &last.residuals))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceQpGetCertificate(
qp: PounceQp,
out: *mut PounceQpCertificate,
) -> Bool {
ffi_guard(FALSE, || {
let Some(last) = qp.as_ref().and_then(|i| i.last.as_ref()) else {
return FALSE;
};
if out.is_null() {
return FALSE;
}
let sol = &last.sol;
let kind = match sol.status {
QpStatus::PrimalInfeasible => POUNCE_QP_CERT_PRIMAL_INFEASIBLE,
QpStatus::DualInfeasible => POUNCE_QP_CERT_DUAL_INFEASIBLE,
_ => POUNCE_QP_CERT_NONE,
};
let scale = certificate_scale(sol).unwrap_or(f64::NAN);
let cert = PounceQpCertificate {
struct_size: struct_size_of::<PounceQpCertificate>(),
kind,
scale,
tau: last.hsde.map_or(f64::NAN, |h| h.tau),
kappa: last.hsde.map_or(f64::NAN, |h| h.kappa),
};
Bool::from(write_extensible(out, &cert))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PounceQpWriteSolveReport(
qp: PounceQp,
path: *const c_char,
detail: *const c_char,
) -> Bool {
use pounce_solve_report::{
InputDescriptor, ReportBuilder, ReportDetail, status_to_solve_result_num, write_report_file,
};
ffi_guard(FALSE, || {
let Some(info) = qp.as_ref() else {
return FALSE;
};
let Some(last) = info.last.as_ref() else {
return FALSE;
};
if path.is_null() {
return FALSE;
}
let Ok(path) = CStr::from_ptr(path).to_str() else {
return FALSE;
};
let detail = if detail.is_null() {
ReportDetail::Summary
} else {
match CStr::from_ptr(detail).to_str().map(ReportDetail::parse) {
Ok(Ok(d)) => d,
_ => return FALSE,
}
};
let idx = |v: usize| Index::try_from(v).unwrap_or(Index::MAX);
let sol = &last.sol;
let status = qp_status_to_ars(sol.status);
let mut b = ReportBuilder::new(detail, InputDescriptor::TnlpDirect);
b.problem.n_variables = idx(info.n);
b.problem.n_constraints = idx(info.m_eq + info.m_ineq);
b.problem.n_objectives = 1;
b.problem.nnz_jac_g = Some(idx(info.a.len() + info.g.len()));
b.problem.nnz_h_lag = Some(idx(info.p.len()));
b.solution.engine = last.engine.to_string();
b.solution.status = status;
b.solution.solve_result_num = status_to_solve_result_num(status);
b.solution.objective = sol.obj;
b.solution.x = sol.x.clone();
b.solution.lambda = sol.y.iter().chain(&sol.z).copied().collect();
b.stats.iteration_count = idx(sol.iters);
b.stats.final_objective = sol.obj;
b.stats.final_constr_viol = last.residuals.primal_infeasibility;
b.stats.final_dual_inf = last.residuals.dual_infeasibility;
b.stats.final_compl = last.residuals.complementarity;
b.stats.final_kkt_error = last.residuals.kkt_error;
b.stats.total_wallclock_time_secs = last.seconds;
match write_report_file(std::path::Path::new(path), &b.finish()) {
Ok(_) => TRUE,
Err(_) => FALSE,
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::ptr::null;
#[test]
fn free_null_is_a_no_op_and_create_refuses_bad_dimensions() {
unsafe {
PounceFreeQp(std::ptr::null_mut());
assert!(PounceCreateQp(0, 0, 0, 0).is_null());
assert!(PounceCreateQp(-1, 0, 0, 0).is_null());
assert!(PounceCreateQp(2, -1, 0, 0).is_null());
assert!(PounceCreateQp(2, 0, -1, 0).is_null());
assert!(PounceCreateQp(2, 0, 0, 2).is_null());
let qp = PounceCreateQp(2, 0, 0, 1);
assert!(!qp.is_null());
PounceFreeQp(qp);
}
}
unsafe fn coupled_qp(b: f64, h: f64) -> PounceQp {
unsafe {
let qp = PounceCreateQp(2, 1, 1, 0);
assert!(!qp.is_null());
let (pr, pc, pv) = ([0, 1, 1], [0, 0, 1], [2.0, 1.0, 2.0]);
let c = [-1.0, 0.0];
assert_eq!(
PounceQpSetObjective(qp, 3, pr.as_ptr(), pc.as_ptr(), pv.as_ptr(), c.as_ptr()),
TRUE
);
set_rows(qp, b, h);
qp
}
}
unsafe fn set_rows(qp: PounceQp, b: f64, h: f64) {
unsafe {
let (r, cc, v) = ([0, 0], [0, 1], [1.0, 1.0]);
assert_eq!(
PounceQpSetEqualities(qp, 2, r.as_ptr(), cc.as_ptr(), v.as_ptr(), [b].as_ptr()),
TRUE
);
let (r, cc, v) = ([0], [1], [1.0]);
assert_eq!(
PounceQpSetInequalities(qp, 1, r.as_ptr(), cc.as_ptr(), v.as_ptr(), [h].as_ptr()),
TRUE
);
}
}
fn runs() -> usize {
CERTIFICATE_RUNS.with(|c| c.get())
}
#[test]
fn the_psd_certificate_runs_once_per_objective() {
unsafe {
let qp = coupled_qp(1.0, 5.0);
assert_eq!(runs(), 0);
assert_eq!(PounceQpSolve(qp), POUNCE_QP_STATUS_OPTIMAL);
assert_eq!(runs(), 1, "the first solve certifies P");
set_rows(qp, 2.0, 5.0);
assert_eq!(PounceQpSolve(qp), POUNCE_QP_STATUS_OPTIMAL);
assert_eq!(PounceQpSetBounds(qp, [-10.0, -10.0].as_ptr(), null()), TRUE);
assert_eq!(PounceQpSetNumOption(qp, c"tol".as_ptr(), 1e-9), TRUE);
assert_eq!(PounceQpSolve(qp), POUNCE_QP_STATUS_OPTIMAL);
assert_eq!(runs(), 1, "b, bounds, options: the cached verdict");
let (pr, pc, pv) = ([0, 1, 1], [0, 0, 1], [3.0, 1.0, 3.0]);
let c = [-1.0, 0.0];
assert_eq!(
PounceQpSetObjective(qp, 3, pr.as_ptr(), pc.as_ptr(), pv.as_ptr(), c.as_ptr()),
TRUE
);
assert_eq!(PounceQpSolve(qp), POUNCE_QP_STATUS_OPTIMAL);
assert_eq!(runs(), 2, "a new P is certified");
assert_eq!(
PounceQpSetStrOption(qp, c"hessian_inertia".as_ptr(), c"psd".as_ptr()),
TRUE
);
assert_eq!(PounceQpSolve(qp), POUNCE_QP_STATUS_OPTIMAL);
assert_eq!(runs(), 2, "a claim is used as given");
assert_eq!(
PounceQpSetStrOption(qp, c"hessian_inertia".as_ptr(), c"auto".as_ptr()),
TRUE
);
assert_eq!(PounceQpSolve(qp), POUNCE_QP_STATUS_OPTIMAL);
assert_eq!(runs(), 3, "a hessian_inertia change drops the verdict");
PounceFreeQp(qp);
}
}
#[test]
fn a_cached_verdict_never_outlives_its_objective() {
unsafe {
let qp = coupled_qp(1.0, 5.0);
let (pr, pc, pv) = ([0, 1, 1], [0, 0, 1], [1.0, 2.0, 1.0]); let c = [-1.0, 0.0];
assert_eq!(
PounceQpSetObjective(qp, 3, pr.as_ptr(), pc.as_ptr(), pv.as_ptr(), c.as_ptr()),
TRUE
);
assert_eq!(PounceQpSolve(qp), POUNCE_ERR_INDEFINITE);
set_rows(qp, 2.0, 5.0);
assert_eq!(PounceQpSolve(qp), POUNCE_ERR_INDEFINITE);
assert_eq!(runs(), 1);
let pv = [2.0, 1.0, 2.0];
assert_eq!(
PounceQpSetObjective(qp, 3, pr.as_ptr(), pc.as_ptr(), pv.as_ptr(), c.as_ptr()),
TRUE
);
assert_eq!(PounceQpSolve(qp), POUNCE_QP_STATUS_OPTIMAL);
assert_eq!(runs(), 2);
PounceFreeQp(qp);
}
}
#[test]
fn get_solution_refuses_a_result_of_the_wrong_shape() {
unsafe {
let qp = coupled_qp(1.0, 5.0);
assert_eq!(PounceQpSolve(qp), POUNCE_QP_STATUS_OPTIMAL);
let read = |qp: PounceQp| {
let (mut x, mut y, mut z) = ([7.0; 2], [7.0; 1], [7.0; 1]);
let (mut zl, mut zu, mut obj, mut it) = ([7.0; 2], [7.0; 2], 7.0, 7);
let ok = PounceQpGetSolution(
qp,
x.as_mut_ptr(),
y.as_mut_ptr(),
z.as_mut_ptr(),
zl.as_mut_ptr(),
zu.as_mut_ptr(),
&mut obj,
&mut it,
);
let untouched = x == [7.0; 2]
&& y == [7.0]
&& z == [7.0]
&& zl == [7.0; 2]
&& zu == [7.0; 2]
&& obj == 7.0
&& it == 7;
(ok, untouched)
};
assert_eq!(read(qp), (TRUE, false), "the control: a true shape");
for k in 0..5 {
let sol = &mut (*qp).last.as_mut().unwrap().sol;
let v = [
&mut sol.x,
&mut sol.y,
&mut sol.z,
&mut sol.z_lb,
&mut sol.z_ub,
];
let target = v.into_iter().nth(k).unwrap();
target.push(0.0);
assert_eq!(read(qp), (FALSE, true), "vector {k} one long");
let sol = &mut (*qp).last.as_mut().unwrap().sol;
[
&mut sol.x,
&mut sol.y,
&mut sol.z,
&mut sol.z_lb,
&mut sol.z_ub,
]
.into_iter()
.nth(k)
.unwrap()
.pop();
assert_eq!(read(qp).0, TRUE, "vector {k} restored");
}
PounceFreeQp(qp);
}
}
#[test]
fn status_mappings_cover_every_variant() {
use pounce_solve_report::status_to_solve_result_num;
let all = [
(QpStatus::Optimal, 0, 0),
(QpStatus::OptimalInaccurate, 1, 1),
(QpStatus::PrimalInfeasible, 2, 200),
(QpStatus::DualInfeasible, 3, 300),
(QpStatus::IterationLimit, 4, 400),
(QpStatus::TimeLimit, 5, 400),
(QpStatus::NumericalFailure, 6, 500),
];
for (s, code, srn) in all {
assert_eq!(status_code(s), code, "{s:?}");
assert_eq!(
status_to_solve_result_num(qp_status_to_ars(s)),
srn,
"{s:?}"
);
}
}
}