#![allow(non_camel_case_types, non_snake_case)]
#![allow(unsafe_op_in_unsafe_fn, dead_code)]
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
pub mod bounds;
pub mod errors;
pub mod fortran;
pub mod qp;
pub mod solver;
pub use bounds::*;
pub use errors::*;
pub use qp::*;
use bounds::{ComplementsLedger, ComplementsRow, ComplementsUse, DeclaredPairs};
use pounce_algorithm::application::{
IpoptApplication, default_backend_factory, feral_config_from_options, ma57_config_from_options,
};
use pounce_algorithm::intermediate as ip_intermediate;
use pounce_common::reg_options::OptionType;
use pounce_common::types::{NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF};
use pounce_nlp::mpcc::{ComplementarityPair, PairErrorKind, PairShape, classify_pair};
use pounce_nlp::return_codes::ApplicationReturnStatus;
use pounce_nlp::solve_statistics::SolveStatistics;
use pounce_nlp::tnlp::{
BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, ScalingRequest, Solution, SparsityRequest,
StartingPoint, TNLP,
};
use pounce_restoration::resto_alg_builder::RestoAlgorithmBuilder;
use pounce_restoration::resto_inner_solver::{
InnerBackendFactoryFactory, make_default_restoration_factory_provider,
};
use pounce_restoration::second_opinion_driver::run_second_opinion_ladder;
use std::cell::RefCell;
use std::ffi::{CStr, c_char, c_int, c_uint, c_void};
use std::rc::Rc;
pub type Number = f64;
pub type Index = c_int;
pub type Bool = u8;
const TRUE: Bool = 1;
const FALSE: Bool = 0;
const _: () = assert!(
core::mem::size_of::<Bool>() == 1,
"Bool must be one byte to match `typedef bool Bool` in pounce.h"
);
pub(crate) fn ffi_guard<R>(fallback: R, body: impl FnOnce() -> R) -> R {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)) {
Ok(r) => r,
Err(_) => fallback,
}
}
pub(crate) unsafe trait ExtensibleOut: Copy {}
pub(crate) const fn struct_size_of<T: ExtensibleOut>() -> Index {
std::mem::size_of::<T>() as Index
}
pub(crate) unsafe fn write_extensible<T: ExtensibleOut>(out: *mut T, value: &T) -> bool {
const HEAD: usize = std::mem::size_of::<Index>();
let size = std::mem::size_of::<T>();
let claimed = unsafe { std::ptr::read(out.cast::<Index>()) };
if usize::try_from(claimed).map_or(true, |c| c < size) {
return false;
}
unsafe {
std::ptr::copy_nonoverlapping(
(value as *const T).cast::<u8>().add(HEAD),
out.cast::<u8>().add(HEAD),
size - HEAD,
);
}
true
}
pub type IpoptBoundStatus = c_int;
pub type IpoptConsStatus = c_int;
const POUNCE_WS_INACTIVE: c_int = 0;
const POUNCE_WS_AT_LOWER: c_int = 1;
const POUNCE_WS_AT_UPPER: c_int = 2;
const POUNCE_WS_FIXED_OR_EQ: c_int = 3;
pub struct IpoptProblemInfo {
pub(crate) app: IpoptApplication,
pub(crate) n: Index,
pub(crate) m: Index,
pub(crate) nele_jac: Index,
pub(crate) nele_hess: Index,
pub(crate) index_style: Index,
pub(crate) x_l: Vec<Number>,
pub(crate) x_u: Vec<Number>,
pub(crate) g_l: Vec<Number>,
pub(crate) g_u: Vec<Number>,
pub(crate) eval_f: Option<Eval_F_CB>,
pub(crate) eval_g: Option<Eval_G_CB>,
pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
pub(crate) eval_h: Option<Eval_H_CB>,
pub(crate) intermediate_cb: Option<Intermediate_CB>,
pub(crate) user_scaling: Option<UserScaling>,
pub(crate) last_solve: Option<LastSolve>,
pub(crate) pending_working_set: Option<pounce_qp::WorkingSet>,
pub(crate) nonlinear_vars: Option<Vec<Index>>,
pub(crate) complementarity: DeclaredPairs,
pub(crate) complements_used: ComplementsLedger,
}
#[derive(Clone)]
pub(crate) struct UserScaling {
obj_scaling: Number,
x_scaling: Option<Vec<Number>>,
g_scaling: Option<Vec<Number>>,
}
#[derive(Clone)]
pub(crate) struct LastSolve {
pub(crate) stats: SolveStatistics,
pub(crate) status: ApplicationReturnStatus,
pub(crate) linear_solver: Option<pounce_linsol::summary::LinearSolverSummary>,
pub(crate) final_x: Vec<Number>,
pub(crate) final_lambda: Vec<Number>,
pub(crate) final_obj: Number,
pub(crate) mpcc: Option<pounce_restoration::mpcc_driver::MpccOutcome>,
pub(crate) second_opinion:
Option<pounce_restoration::second_opinion_driver::SecondOpinionOutcome>,
}
impl Default for LastSolve {
fn default() -> Self {
Self {
stats: SolveStatistics::default(),
status: ApplicationReturnStatus::InternalError,
linear_solver: None,
final_x: Vec::new(),
final_lambda: Vec::new(),
final_obj: 0.0,
mpcc: None,
second_opinion: None,
}
}
}
pub type IpoptProblem = *mut IpoptProblemInfo;
pub type Eval_F_CB = unsafe extern "C" fn(
n: Index,
x: *const Number,
new_x: Bool,
obj_value: *mut Number,
user_data: *mut c_void,
) -> Bool;
pub type Eval_Grad_F_CB = unsafe extern "C" fn(
n: Index,
x: *const Number,
new_x: Bool,
grad_f: *mut Number,
user_data: *mut c_void,
) -> Bool;
pub type Eval_G_CB = unsafe extern "C" fn(
n: Index,
x: *const Number,
new_x: Bool,
m: Index,
g: *mut Number,
user_data: *mut c_void,
) -> Bool;
pub type Eval_Jac_G_CB = unsafe extern "C" fn(
n: Index,
x: *const Number,
new_x: Bool,
m: Index,
nele_jac: Index,
iRow: *mut Index,
jCol: *mut Index,
values: *mut Number,
user_data: *mut c_void,
) -> Bool;
pub type Eval_H_CB = unsafe extern "C" fn(
n: Index,
x: *const Number,
new_x: Bool,
obj_factor: Number,
m: Index,
lambda: *const Number,
new_lambda: Bool,
nele_hess: Index,
iRow: *mut Index,
jCol: *mut Index,
values: *mut Number,
user_data: *mut c_void,
) -> Bool;
pub type Intermediate_CB = unsafe extern "C" fn(
alg_mod: Index,
iter_count: Index,
obj_value: Number,
inf_pr: Number,
inf_du: Number,
mu: Number,
d_norm: Number,
regularization_size: Number,
alpha_du: Number,
alpha_pr: Number,
ls_trials: Index,
user_data: *mut c_void,
) -> Bool;
#[unsafe(no_mangle)]
pub unsafe extern "C" fn CreateIpoptProblem(
n: Index,
x_L: *const Number,
x_U: *const Number,
m: Index,
g_L: *const Number,
g_U: *const Number,
nele_jac: Index,
nele_hess: Index,
index_style: Index,
eval_f: Option<Eval_F_CB>,
eval_g: Option<Eval_G_CB>,
eval_grad_f: Option<Eval_Grad_F_CB>,
eval_jac_g: Option<Eval_Jac_G_CB>,
eval_h: Option<Eval_H_CB>,
) -> IpoptProblem {
unsafe {
pounce_observability::init_subscriber();
if n < 0 || m < 0 || nele_jac < 0 || nele_hess < 0 {
return std::ptr::null_mut();
}
if !(0..=1).contains(&index_style) {
return std::ptr::null_mut();
}
if eval_f.is_none() || eval_grad_f.is_none() {
return std::ptr::null_mut();
}
if m > 0 && (eval_g.is_none() || eval_jac_g.is_none()) {
return std::ptr::null_mut();
}
if n > 0 && (x_L.is_null() || x_U.is_null()) {
return std::ptr::null_mut();
}
if m > 0 && (g_L.is_null() || g_U.is_null()) {
return std::ptr::null_mut();
}
let x_l = if n > 0 {
std::slice::from_raw_parts(x_L, n as usize).to_vec()
} else {
Vec::new()
};
let x_u = if n > 0 {
std::slice::from_raw_parts(x_U, n as usize).to_vec()
} else {
Vec::new()
};
let g_l_vec = if m > 0 {
std::slice::from_raw_parts(g_L, m as usize).to_vec()
} else {
Vec::new()
};
let g_u_vec = if m > 0 {
std::slice::from_raw_parts(g_U, m as usize).to_vec()
} else {
Vec::new()
};
let info = Box::new(IpoptProblemInfo {
app: IpoptApplication::new(),
n,
m,
nele_jac,
nele_hess,
index_style,
x_l,
x_u,
g_l: g_l_vec,
g_u: g_u_vec,
eval_f,
eval_g,
eval_grad_f,
eval_jac_g,
eval_h,
intermediate_cb: None,
user_scaling: None,
nonlinear_vars: None,
complementarity: DeclaredPairs::default(),
complements_used: ComplementsLedger::default(),
last_solve: None,
pending_working_set: None,
});
Box::into_raw(info)
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn FreeIpoptProblem(ipopt_problem: IpoptProblem) {
unsafe {
if ipopt_problem.is_null() {
return;
}
drop(Box::from_raw(ipopt_problem));
}
}
unsafe fn keyword_str<'a>(keyword: *const c_char) -> Option<&'a str> {
unsafe {
if keyword.is_null() {
return None;
}
CStr::from_ptr(keyword).to_str().ok()
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn AddIpoptStrOption(
ipopt_problem: IpoptProblem,
keyword: *const c_char,
val: *const c_char,
) -> Bool {
unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
let info = &mut *ipopt_problem;
let Some(k) = keyword_str(keyword) else {
return FALSE;
};
if val.is_null() {
return FALSE;
}
let Ok(v) = CStr::from_ptr(val).to_str() else {
return FALSE;
};
match info.app.options_mut().set_string_value(k, v, true, false) {
Ok(_) => TRUE,
Err(_) => FALSE,
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn AddIpoptNumOption(
ipopt_problem: IpoptProblem,
keyword: *const c_char,
val: Number,
) -> Bool {
unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
let info = &mut *ipopt_problem;
let Some(k) = keyword_str(keyword) else {
return FALSE;
};
match info
.app
.options_mut()
.set_numeric_value(k, val, true, false)
{
Ok(_) => TRUE,
Err(_) => FALSE,
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn AddIpoptIntOption(
ipopt_problem: IpoptProblem,
keyword: *const c_char,
val: Index,
) -> Bool {
unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
let info = &mut *ipopt_problem;
let Some(k) = keyword_str(keyword) else {
return FALSE;
};
match info.app.options_mut().set_integer_value(
k,
val as pounce_common::types::Index,
true,
false,
) {
Ok(_) => TRUE,
Err(_) => FALSE,
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn OpenIpoptOutputFile(
ipopt_problem: IpoptProblem,
file_name: *const c_char,
print_level: c_int,
) -> Bool {
unsafe {
if ipopt_problem.is_null() || file_name.is_null() {
return FALSE;
}
let info = &mut *ipopt_problem;
let Ok(fname) = CStr::from_ptr(file_name).to_str() else {
return FALSE;
};
if info.app.open_output_file(fname, print_level) {
TRUE
} else {
FALSE
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn SetIpoptProblemScaling(
ipopt_problem: IpoptProblem,
obj_scaling: Number,
x_scaling: *const Number,
g_scaling: *const Number,
) -> Bool {
unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
let info = &mut *ipopt_problem;
let n = info.n as usize;
let m = info.m as usize;
let x_vec = if !x_scaling.is_null() && n > 0 {
Some(std::slice::from_raw_parts(x_scaling, n).to_vec())
} else {
None
};
let g_vec = if !g_scaling.is_null() && m > 0 {
Some(std::slice::from_raw_parts(g_scaling, m).to_vec())
} else {
None
};
info.user_scaling = Some(UserScaling {
obj_scaling,
x_scaling: x_vec,
g_scaling: g_vec,
});
TRUE
}
}
#[allow(clippy::too_many_arguments)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn IpoptSolve(
ipopt_problem: IpoptProblem,
x: *mut Number,
g: *mut Number,
obj_val: *mut Number,
mult_g: *mut Number,
mult_x_L: *mut Number,
mult_x_U: *mut Number,
user_data: *mut c_void,
) -> Index {
unsafe {
if ipopt_problem.is_null() {
return ApplicationReturnStatus::InternalError as Index;
}
(*ipopt_problem).last_solve = None;
ffi_guard(ApplicationReturnStatus::InternalError as Index, || {
let info = &mut *ipopt_problem;
let _lowered = LoweredSolveGuard::set(!info.complementarity.is_empty());
if info.n < 0 || info.m < 0 {
return ApplicationReturnStatus::InvalidProblemDefinition as Index;
}
if info.n > 0 && x.is_null() {
return ApplicationReturnStatus::InvalidProblemDefinition as Index;
}
if !info.complementarity.is_empty() && info.pending_working_set.is_some() {
if pounce_algorithm::second_opinion::narration_is_wanted(info.app.options()) {
eprintln!(
"pounce: a warm-start working set cannot seed a solve with \
complementarity pairs ({} declared); clear one of them \
(IpoptClearWarmStartWorkingSet or ClearPounceComplementarityPairs).",
info.complementarity.len()
);
}
return ApplicationReturnStatus::InvalidProblemDefinition as Index;
}
let n_us = info.n as usize;
let m_us = info.m as usize;
let initial_x = if n_us > 0 {
std::slice::from_raw_parts(x, n_us).to_vec()
} else {
Vec::new()
};
if let Some(working) = info.pending_working_set.take() {
let seed_duals = matches!(
info.app
.options()
.get_bool_value("warm_start_init_point", ""),
Ok((true, true))
);
let read_in = |p: *const Number, len: usize| -> Vec<Number> {
if seed_duals && !p.is_null() && len > 0 {
std::slice::from_raw_parts(p, len).to_vec()
} else {
vec![0.0; len]
}
};
let lambda_g = read_in(mult_g as *const Number, m_us);
let z_l = read_in(mult_x_L as *const Number, n_us);
let z_u = read_in(mult_x_U as *const Number, n_us);
let lambda_x = z_l.iter().zip(&z_u).map(|(l, u)| l - u).collect();
info.app
.set_sqp_warm_start(pounce_algorithm::sqp::SqpIterates {
x: initial_x.clone(),
lambda_g,
lambda_x,
working: Some(working),
});
}
let (lower_inf, upper_inf, zero_fixed_grad_lag) =
bridge_violation_options(info.app.options());
let bridge = Rc::new(RefCell::new(CCallbackTnlp {
n: info.n,
m: info.m,
nele_jac: info.nele_jac,
nele_hess: info.nele_hess,
index_style: info.index_style,
x_l: info.x_l.clone(),
x_u: info.x_u.clone(),
g_l: info.g_l.clone(),
g_u: info.g_u.clone(),
initial_x,
start_lambda: mult_g as *const Number,
start_z_l: mult_x_L as *const Number,
start_z_u: mult_x_U as *const Number,
eval_f: info.eval_f,
eval_grad_f: info.eval_grad_f,
eval_g: info.eval_g,
eval_jac_g: info.eval_jac_g,
eval_h: info.eval_h,
user_data,
intermediate_cb: info.intermediate_cb,
user_scaling: info.user_scaling.clone(),
nonlinear_vars: info.nonlinear_vars.clone(),
lower_inf,
upper_inf,
zero_fixed_grad_lag,
final_status: None,
final_x: vec![0.0; n_us],
final_z_l: vec![0.0; n_us],
final_z_u: vec![0.0; n_us],
final_g: vec![0.0; m_us],
final_lambda: vec![0.0; m_us],
final_obj: 0.0,
}));
let feral_cfg = feral_config_from_options(info.app.options());
let ma57_cfg = ma57_config_from_options(info.app.options(), "resto.");
let bff_mint = move || -> InnerBackendFactoryFactory {
let feral_cfg = feral_cfg.clone();
let ma57_cfg = ma57_cfg.clone();
Box::new(move || default_backend_factory(feral_cfg.clone(), ma57_cfg.clone()))
};
let resto_provider = make_default_restoration_factory_provider(
RestoAlgorithmBuilder::new(),
info.app.algorithm_builder_from_options(),
bff_mint,
);
info.app.set_restoration_factory_provider(resto_provider);
let bridge_for_solve: Rc<RefCell<dyn TNLP>> = bridge.clone();
info.app.defer_end_verdict();
let (status, statistics, mpcc, second_opinion) = if !info.complementarity.is_empty() {
let narrate =
pounce_algorithm::second_opinion::narration_is_wanted(info.app.options());
let result = pounce_restoration::mpcc_driver::solve_mpcc(
&mut info.app,
bridge_for_solve,
info.complementarity.to_vec(),
&mut |line| {
if narrate {
eprintln!("{line}");
}
},
);
let out = match result {
Ok(out) => out,
Err(e) => {
if narrate {
eprintln!(
"pounce: cannot solve this model's complementarity constraints: {e}"
);
}
let status = ApplicationReturnStatus::InvalidProblemDefinition;
if info.app.release_end_verdict() {
info.app.print_end_verdict(status);
}
return status as Index;
}
};
if narrate {
eprintln!(
"{}",
pounce_algorithm::mpcc::describe_outcome(
out.reported,
out.solves.len(),
out.total_iterations,
out.max_abs_product,
out.certificate.as_ref(),
)
);
}
if info.app.release_end_verdict() {
info.app.print_end_verdict(out.status);
}
(out.status, out.statistics.clone(), Some(out), None)
} else {
let status = info.app.optimize_tnlp(bridge_for_solve);
let stats = info.app.statistics();
let narrate =
pounce_algorithm::second_opinion::narration_is_wanted(info.app.options());
let ladder = run_second_opinion_ladder(
&mut info.app,
bridge.clone() as Rc<RefCell<dyn TNLP>>,
status,
stats,
&mut |line| {
if narrate {
eprintln!("{line}");
}
},
);
(ladder.status, ladder.statistics.clone(), None, Some(ladder))
};
let bridge_ref = bridge.borrow();
info.last_solve = Some(LastSolve {
stats: statistics,
status,
linear_solver: info.app.linear_solver_summary(),
final_x: bridge_ref.final_x.clone(),
final_lambda: bridge_ref.final_lambda.clone(),
final_obj: bridge_ref.final_obj,
mpcc,
second_opinion,
});
if !x.is_null() && n_us > 0 {
std::ptr::copy_nonoverlapping(bridge_ref.final_x.as_ptr(), x, n_us);
}
if !g.is_null() && m_us > 0 {
std::ptr::copy_nonoverlapping(bridge_ref.final_g.as_ptr(), g, m_us);
}
if !obj_val.is_null() {
*obj_val = bridge_ref.final_obj;
}
if !mult_g.is_null() && m_us > 0 {
std::ptr::copy_nonoverlapping(bridge_ref.final_lambda.as_ptr(), mult_g, m_us);
}
if !mult_x_L.is_null() && n_us > 0 {
std::ptr::copy_nonoverlapping(bridge_ref.final_z_l.as_ptr(), mult_x_L, n_us);
}
if !mult_x_U.is_null() && n_us > 0 {
std::ptr::copy_nonoverlapping(bridge_ref.final_z_u.as_ptr(), mult_x_U, n_us);
}
status as Index
})
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn SetIntermediateCallback(
ipopt_problem: IpoptProblem,
intermediate_cb: Option<Intermediate_CB>,
) -> Bool {
unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
let info = &mut *ipopt_problem;
info.intermediate_cb = intermediate_cb;
TRUE
}
}
#[allow(clippy::too_many_arguments)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetIpoptCurrentIterate(
ipopt_problem: IpoptProblem,
scaled: Bool,
n: Index,
x: *mut Number,
z_l: *mut Number,
z_u: *mut Number,
m: Index,
g: *mut Number,
lambda: *mut Number,
) -> Bool {
unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
if in_lowered_solve() {
return FALSE;
}
let info = &*ipopt_problem;
if n != info.n || m != info.m {
return FALSE;
}
if let Some(r) = ip_intermediate::with_current_user_iterate(|it| {
let scaled_it;
let it = if scaled != FALSE {
scaled_it = pounce_nlp::live_iterate::scaled_iterate(it);
&scaled_it
} else {
it
};
let n_us = n as usize;
let m_us = m as usize;
let copy = |src: &[Number], dst: *mut Number, len: usize| -> bool {
if dst.is_null() || len == 0 {
return true;
}
if src.len() != len {
return false;
}
std::ptr::copy_nonoverlapping(src.as_ptr(), dst, len);
true
};
copy(&it.x, x, n_us)
&& copy(&it.z_l, z_l, n_us)
&& copy(&it.z_u, z_u, n_us)
&& copy(&it.g, g, m_us)
&& copy(&it.lambda, lambda, m_us)
}) {
return if r { TRUE } else { FALSE };
}
let result = ip_intermediate::with_current(|ctx| {
let curr = {
let data = ctx.data.borrow();
match data.curr.as_ref() {
Some(curr) => curr.clone(),
None => return false,
}
};
let n_us = n as usize;
let m_us = m as usize;
let d = ctx.nlp.borrow().variable_scaling();
let d = d.as_deref();
if !x.is_null() && n_us > 0 {
let mut full_x = ctx.nlp.borrow().lift_x_to_full(&*curr.x);
if full_x.len() != n_us {
return false;
}
pounce_nlp::scaling_tnlp::unscale_iterate(d, &mut full_x, &mut [], &mut []);
std::ptr::copy_nonoverlapping(full_x.as_ptr(), x, n_us);
}
if !z_l.is_null() && n_us > 0 {
let mut full = ctx.nlp.borrow().finalize_solution_z_l(&*curr.z_l);
if full.len() != n_us {
return false;
}
pounce_nlp::scaling_tnlp::unscale_iterate(d, &mut [], &mut full, &mut []);
std::ptr::copy_nonoverlapping(full.as_ptr(), z_l, n_us);
}
if !z_u.is_null() && n_us > 0 {
let mut full = ctx.nlp.borrow().finalize_solution_z_u(&*curr.z_u);
if full.len() != n_us {
return false;
}
pounce_nlp::scaling_tnlp::unscale_iterate(d, &mut [], &mut [], &mut full);
std::ptr::copy_nonoverlapping(full.as_ptr(), z_u, n_us);
}
if !g.is_null() && m_us > 0 {
let (c, d) = {
let cq = ctx.cq.borrow();
(cq.curr_c(), cq.curr_d())
};
let full = ctx.nlp.borrow().pack_g_for_user(&*c, &*d);
if full.len() != m_us {
return false;
}
std::ptr::copy_nonoverlapping(full.as_ptr(), g, m_us);
}
if !lambda.is_null() && m_us > 0 {
let full = ctx
.nlp
.borrow()
.finalize_solution_lambda(&*curr.y_c, &*curr.y_d);
if full.len() != m_us {
return false;
}
std::ptr::copy_nonoverlapping(full.as_ptr(), lambda, m_us);
}
true
});
if result.unwrap_or(false) { TRUE } else { FALSE }
}
}
#[allow(clippy::too_many_arguments)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetIpoptCurrentViolations(
ipopt_problem: IpoptProblem,
scaled: Bool,
n: Index,
x_l_violation: *mut Number,
x_u_violation: *mut Number,
compl_x_l: *mut Number,
compl_x_u: *mut Number,
grad_lag_x: *mut Number,
m: Index,
nlp_constraint_violation: *mut Number,
compl_g: *mut Number,
) -> Bool {
unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
if in_lowered_solve() {
return FALSE;
}
let info = &*ipopt_problem;
if n != info.n || m != info.m {
return FALSE;
}
let n_us = n as usize;
let m_us = m as usize;
let Some(it) = ip_intermediate::with_current_user_iterate(|it| it.clone()) else {
return FALSE;
};
let bridge = CURRENT_BRIDGE.with(|c| c.get());
if bridge.is_null() {
return FALSE;
}
let bridge = &*bridge;
if it.x.len() != n_us
|| it.z_l.len() != n_us
|| it.z_u.len() != n_us
|| it.g.len() != m_us
|| it.lambda.len() != m_us
{
return FALSE;
}
let grad_lag = if grad_lag_x.is_null() || n_us == 0 {
None
} else if it.grad_lag_x.len() == n_us {
Some(it.grad_lag_x.clone())
} else {
match bridge.user_grad_lag(&it) {
Some(g) => Some(g),
None => return FALSE,
}
};
let bounds = pounce_nlp::live_iterate::DeclaredBounds {
x_l: &bridge.x_l,
x_u: &bridge.x_u,
g_l: &bridge.g_l,
g_u: &bridge.g_u,
lower_inf: bridge.lower_inf,
upper_inf: bridge.upper_inf,
};
let violations = Some(pounce_nlp::live_iterate::current_violations(
&it,
&bounds,
grad_lag,
bridge.zero_fixed_grad_lag,
scaled != FALSE,
));
let Some(v) = violations else {
return FALSE;
};
let copy = |src: &[Number], dst: *mut Number, len: usize| -> bool {
if dst.is_null() || len == 0 {
return true;
}
if src.len() != len {
return false;
}
std::ptr::copy_nonoverlapping(src.as_ptr(), dst, len);
true
};
let ok = copy(&v.x_l_violation, x_l_violation, n_us)
&& copy(&v.x_u_violation, x_u_violation, n_us)
&& copy(&v.compl_x_l, compl_x_l, n_us)
&& copy(&v.compl_x_u, compl_x_u, n_us)
&& copy(&v.grad_lag_x, grad_lag_x, n_us)
&& copy(&v.constraint_violation, nlp_constraint_violation, m_us)
&& copy(&v.compl_g, compl_g, m_us);
if ok { TRUE } else { FALSE }
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetIpoptVersion(
major: *mut c_int,
minor: *mut c_int,
release: *mut c_int,
) {
unsafe {
let (mj, mn, pt) = parse_pkg_version(env!("CARGO_PKG_VERSION"));
if !major.is_null() {
*major = mj;
}
if !minor.is_null() {
*minor = mn;
}
if !release.is_null() {
*release = pt;
}
}
}
fn parse_pkg_version(v: &str) -> (c_int, c_int, c_int) {
let mut it = v.split('.').map(|s| s.parse::<c_int>().unwrap_or(0));
(
it.next().unwrap_or(0),
it.next().unwrap_or(0),
it.next().unwrap_or(0),
)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetIpoptIterCount(ipopt_problem: IpoptProblem) -> Index {
unsafe { last_stat(ipopt_problem, |s| s.iteration_count).unwrap_or(0) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetIpoptSolveTime(ipopt_problem: IpoptProblem) -> Number {
unsafe { last_stat(ipopt_problem, |s| s.total_wallclock_time_secs).unwrap_or(0.0) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetIpoptPrimalInf(ipopt_problem: IpoptProblem) -> Number {
unsafe { last_stat(ipopt_problem, |s| s.final_constr_viol).unwrap_or(0.0) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetIpoptDualInf(ipopt_problem: IpoptProblem) -> Number {
unsafe { last_stat(ipopt_problem, |s| s.final_dual_inf).unwrap_or(0.0) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetIpoptComplInf(ipopt_problem: IpoptProblem) -> Number {
unsafe { last_stat(ipopt_problem, |s| s.final_compl).unwrap_or(0.0) }
}
unsafe fn last_stat<T, F>(ipopt_problem: IpoptProblem, f: F) -> Option<T>
where
F: FnOnce(&SolveStatistics) -> T,
{
unsafe {
if ipopt_problem.is_null() {
return None;
}
(*ipopt_problem).last_solve.as_ref().map(|ls| f(&ls.stats))
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetPounceRestorationStats(
ipopt_problem: IpoptProblem,
calls: *mut Index,
inner_iters: *mut Index,
outer_iters: *mut Index,
wall_secs: *mut Number,
) {
unsafe {
let stats = last_stat(ipopt_problem, |s| {
(
s.restoration_calls,
s.restoration_inner_iters,
s.restoration_outer_iters,
s.restoration_wall_secs,
)
});
let (c, i, o, w) = stats.unwrap_or((0, 0, 0, 0.0));
if !calls.is_null() {
*calls = c;
}
if !inner_iters.is_null() {
*inner_iters = i;
}
if !outer_iters.is_null() {
*outer_iters = o;
}
if !wall_secs.is_null() {
*wall_secs = w;
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetPounceFdHessianStats(
ipopt_problem: IpoptProblem,
pattern_used: *mut Index,
nnz: *mut Index,
n: *mut Index,
groups: *mut Index,
rho_max: *mut Index,
coloring_fell_back: *mut Index,
objective_clique_widened: *mut Index,
) {
unsafe {
let stats = last_stat(ipopt_problem, |s| {
(
s.fd_hessian_pattern_used,
s.fd_hessian_nnz,
s.fd_hessian_n,
s.fd_hessian_groups,
s.fd_hessian_rho_max,
if s.fd_hessian_coloring_fell_back {
1
} else {
0
},
if s.fd_hessian_objective_clique_widened {
1
} else {
0
},
)
});
let (p, nz, cols, g, r, f, w) = stats.unwrap_or((-1, 0, 0, 0, 0, 0, 0));
if !pattern_used.is_null() {
*pattern_used = p;
}
if !nnz.is_null() {
*nnz = nz;
}
if !n.is_null() {
*n = cols;
}
if !groups.is_null() {
*groups = g;
}
if !rho_max.is_null() {
*rho_max = r;
}
if !coloring_fell_back.is_null() {
*coloring_fell_back = f;
}
if !objective_clique_widened.is_null() {
*objective_clique_widened = w;
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct PounceLinearSolverStats {
pub solver_name: [c_char; 32],
pub n_factors: Index,
pub n_pattern_reuse: Index,
pub n_pattern_changes: Index,
pub max_fill_ratio: Number,
pub min_abs_pivot: Number,
pub max_abs_pivot: Number,
pub last_inertia_positive: Index,
pub last_inertia_negative: Index,
pub last_inertia_zero: Index,
pub last_nnz_a: Index,
pub last_nnz_l: Index,
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetPounceLinearSolverStats(
ipopt_problem: IpoptProblem,
stats: *mut PounceLinearSolverStats,
) -> Bool {
unsafe {
if ipopt_problem.is_null() || stats.is_null() {
return FALSE;
}
let Some(summary) = (*ipopt_problem)
.last_solve
.as_ref()
.and_then(|ls| ls.linear_solver.as_ref())
else {
return FALSE;
};
let count = |v: u64| Index::try_from(v).unwrap_or(Index::MAX);
let size = |x: usize| Index::try_from(x).unwrap_or(Index::MAX);
let opt_size = |v: Option<usize>| v.map_or(-1, size);
let inertia = summary.last_inertia;
let mut out = PounceLinearSolverStats {
solver_name: [0; 32],
n_factors: count(summary.n_factors),
n_pattern_reuse: count(summary.n_pattern_reuse),
n_pattern_changes: count(summary.n_pattern_changes),
max_fill_ratio: summary.max_fill_ratio.unwrap_or(Number::NAN),
min_abs_pivot: summary.min_abs_pivot.unwrap_or(Number::NAN),
max_abs_pivot: summary.max_abs_pivot.unwrap_or(Number::NAN),
last_inertia_positive: inertia.map_or(-1, |(p, _, _)| size(p)),
last_inertia_negative: inertia.map_or(-1, |(_, n, _)| size(n)),
last_inertia_zero: inertia.map_or(-1, |(_, _, z)| size(z)),
last_nnz_a: opt_size(summary.last_nnz_a),
last_nnz_l: opt_size(summary.last_nnz_l),
};
let name = summary.solver_name.as_bytes();
let keep = name.len().min(out.solver_name.len() - 1);
for (slot, b) in out.solver_name.iter_mut().zip(&name[..keep]) {
*slot = *b as c_char;
}
*stats = out;
TRUE
}
}
thread_local! {
static DEFAULT_REGISTRY: Rc<pounce_common::reg_options::RegisteredOptions> =
Rc::clone(IpoptApplication::new().registered_options());
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetPounceOptionType(
ipopt_problem: IpoptProblem,
keyword: *const c_char,
) -> c_int {
unsafe {
if keyword.is_null() {
return 0;
}
let Ok(name) = CStr::from_ptr(keyword).to_str() else {
return 0;
};
let registered = if ipopt_problem.is_null() {
DEFAULT_REGISTRY.with(|r| r.get_option(name))
} else {
(*ipopt_problem).app.registered_options().get_option(name)
};
let Some(opt) = registered else {
return 0;
};
match opt.option_type {
OptionType::OT_Number => 1,
OptionType::OT_Integer => 2,
OptionType::OT_String => 3,
OptionType::OT_Unknown => 0,
}
}
}
fn bound_status_to_int(s: pounce_qp::BoundStatus) -> c_int {
use pounce_qp::BoundStatus::*;
match s {
Inactive => POUNCE_WS_INACTIVE,
AtLower => POUNCE_WS_AT_LOWER,
AtUpper => POUNCE_WS_AT_UPPER,
Fixed => POUNCE_WS_FIXED_OR_EQ,
}
}
pub(crate) fn bound_status_consistent(s: pounce_qp::BoundStatus, lo: Number, hi: Number) -> bool {
match s {
pounce_qp::BoundStatus::Fixed => lo == hi,
pounce_qp::BoundStatus::AtLower => lo > NLP_LOWER_BOUND_INF,
pounce_qp::BoundStatus::AtUpper => hi < NLP_UPPER_BOUND_INF,
pounce_qp::BoundStatus::Inactive => true,
}
}
pub(crate) fn cons_status_consistent(s: pounce_qp::ConsStatus, lo: Number, hi: Number) -> bool {
let lo_finite = lo > NLP_LOWER_BOUND_INF;
let hi_finite = hi < NLP_UPPER_BOUND_INF;
match s {
pounce_qp::ConsStatus::Equality => lo_finite && hi_finite && lo == hi,
pounce_qp::ConsStatus::AtLower => lo_finite,
pounce_qp::ConsStatus::AtUpper => hi_finite,
pounce_qp::ConsStatus::Inactive => true,
}
}
fn int_to_bound_status(v: c_int) -> Option<pounce_qp::BoundStatus> {
use pounce_qp::BoundStatus::*;
match v {
POUNCE_WS_INACTIVE => Some(Inactive),
POUNCE_WS_AT_LOWER => Some(AtLower),
POUNCE_WS_AT_UPPER => Some(AtUpper),
POUNCE_WS_FIXED_OR_EQ => Some(Fixed),
_ => None,
}
}
fn cons_status_to_int(s: pounce_qp::ConsStatus) -> c_int {
use pounce_qp::ConsStatus::*;
match s {
Inactive => POUNCE_WS_INACTIVE,
AtLower => POUNCE_WS_AT_LOWER,
AtUpper => POUNCE_WS_AT_UPPER,
Equality => POUNCE_WS_FIXED_OR_EQ,
}
}
fn int_to_cons_status(v: c_int) -> Option<pounce_qp::ConsStatus> {
use pounce_qp::ConsStatus::*;
match v {
POUNCE_WS_INACTIVE => Some(Inactive),
POUNCE_WS_AT_LOWER => Some(AtLower),
POUNCE_WS_AT_UPPER => Some(AtUpper),
POUNCE_WS_FIXED_OR_EQ => Some(Equality),
_ => None,
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn IpoptGetWorkingSet(
ipopt_problem: IpoptProblem,
bound_status_out: *mut IpoptBoundStatus,
cons_status_out: *mut IpoptConsStatus,
) -> Bool {
unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
let info = &*ipopt_problem;
let Some(last) = info.last_solve.as_ref() else {
return FALSE;
};
if last.mpcc.is_some() || !info.complementarity.is_empty() {
return FALSE;
}
let ws = match info.app.last_sqp_working_set() {
Some(w) => w,
None => return FALSE,
};
let n = info.x_l.len();
let m = info.g_l.len();
if ws.constraints.len() != m || ws.bounds.len() != n {
return FALSE;
}
if !bound_status_out.is_null() {
for (i, &s) in ws.bounds.iter().enumerate() {
*bound_status_out.add(i) = bound_status_to_int(s);
}
}
if !cons_status_out.is_null() {
for (i, &s) in ws.constraints.iter().enumerate() {
*cons_status_out.add(i) = cons_status_to_int(s);
}
}
TRUE
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn IpoptSetWarmStartWorkingSet(
ipopt_problem: IpoptProblem,
bound_status_in: *const IpoptBoundStatus,
cons_status_in: *const IpoptConsStatus,
) -> Bool {
unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
if bound_status_in.is_null() && cons_status_in.is_null() {
return FALSE;
}
let info = &mut *ipopt_problem;
let n = info.n.max(0) as usize;
let m = info.m.max(0) as usize;
let mut bounds = vec![pounce_qp::BoundStatus::Inactive; n];
if !bound_status_in.is_null() {
for i in 0..n {
let v = *bound_status_in.add(i);
let Some(s) = int_to_bound_status(v) else {
return FALSE;
};
if !bound_status_consistent(s, info.x_l[i], info.x_u[i]) {
return FALSE;
}
}
for (i, b) in bounds.iter_mut().enumerate() {
if let Some(s) = int_to_bound_status(*bound_status_in.add(i)) {
*b = s;
}
}
}
let mut constraints = vec![pounce_qp::ConsStatus::Inactive; m];
if !cons_status_in.is_null() {
for i in 0..m {
let v = *cons_status_in.add(i);
let Some(s) = int_to_cons_status(v) else {
return FALSE;
};
if !cons_status_consistent(s, info.g_l[i], info.g_u[i]) {
return FALSE;
}
}
for (i, c) in constraints.iter_mut().enumerate() {
if let Some(s) = int_to_cons_status(*cons_status_in.add(i)) {
*c = s;
}
}
}
info.pending_working_set = Some(pounce_qp::WorkingSet {
bounds,
constraints,
});
TRUE
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn IpoptSetNonlinearVariables(
ipopt_problem: IpoptProblem,
num_nonlin_vars: Index,
pos_nonlin_vars: *const Index,
) -> Bool {
unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
let info = &mut *ipopt_problem;
if num_nonlin_vars < 0 || num_nonlin_vars > info.n {
return FALSE;
}
if num_nonlin_vars > 0 && pos_nonlin_vars.is_null() {
return FALSE;
}
let offset = if info.index_style == 1 { 1 } else { 0 };
let raw = if num_nonlin_vars == 0 {
&[][..]
} else {
std::slice::from_raw_parts(pos_nonlin_vars, num_nonlin_vars as usize)
};
for &p in raw {
let zero_based = p - offset;
if zero_based < 0 || zero_based >= info.n {
return FALSE;
}
}
info.nonlinear_vars = Some(raw.to_vec());
TRUE
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn IpoptClearNonlinearVariables(ipopt_problem: IpoptProblem) -> Bool {
unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
(*ipopt_problem).nonlinear_vars = None;
TRUE
}
}
const KNOWN_PAIR_FLAGS: c_uint = 0;
fn pair_error_code(kind: &PairErrorKind) -> c_int {
match kind {
PairErrorKind::OutOfRange => POUNCE_ERR_OUT_OF_RANGE,
PairErrorKind::VarAlreadyPaired => POUNCE_ERR_VAR_ALREADY_PAIRED,
PairErrorKind::RowAlreadyPaired => POUNCE_ERR_ROW_ALREADY_PAIRED,
PairErrorKind::UnsupportedShape => POUNCE_ERR_SHAPE,
}
}
pub(crate) fn zero_based(p: Index, offset: Index, len: Index) -> usize {
match p.checked_sub(offset) {
Some(z) if z >= 0 && z < len => z as usize,
_ => usize::MAX,
}
}
fn replace_pairs(
info: &mut IpoptProblemInfo,
new: &[(Index, Index)],
) -> Result<(), (usize, PairErrorKind)> {
let offset = if info.index_style == 1 { 1 } else { 0 };
let candidate: Vec<ComplementarityPair> = new
.iter()
.map(|&(row, var)| ComplementarityPair {
row: zero_based(row, offset, info.m),
var: zero_based(var, offset, info.n),
})
.collect();
let mut seen_var = vec![false; info.n as usize];
let mut seen_row = vec![false; info.m as usize];
for (k, p) in candidate.iter().enumerate() {
if p.var == usize::MAX || p.row == usize::MAX {
return Err((k, PairErrorKind::OutOfRange));
}
if std::mem::replace(&mut seen_var[p.var], true) {
return Err((k, PairErrorKind::VarAlreadyPaired));
}
if std::mem::replace(&mut seen_row[p.row], true) {
return Err((k, PairErrorKind::RowAlreadyPaired));
}
}
for (k, p) in candidate.iter().enumerate() {
if classify_pair(
info.x_l[p.var],
info.x_u[p.var],
info.g_l[p.row],
info.g_u[p.row],
)
.is_none()
{
return Err((k, PairErrorKind::UnsupportedShape));
}
}
info.complementarity.replace(candidate);
Ok(())
}
fn append_pair(info: &mut IpoptProblemInfo, row: Index, var: Index) -> Result<(), PairErrorKind> {
let offset = if info.index_style == 1 { 1 } else { 0 };
let r = zero_based(row, offset, info.m);
let v = zero_based(var, offset, info.n);
if r == usize::MAX || v == usize::MAX {
return Err(PairErrorKind::OutOfRange);
}
if info.complementarity.of_var(v).is_some() {
return Err(PairErrorKind::VarAlreadyPaired);
}
if info.complementarity.of_row(r).is_some() {
return Err(PairErrorKind::RowAlreadyPaired);
}
if classify_pair(info.x_l[v], info.x_u[v], info.g_l[r], info.g_u[r]).is_none() {
return Err(PairErrorKind::UnsupportedShape);
}
info.complementarity
.push(ComplementarityPair { row: r, var: v });
Ok(())
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn AddPounceComplementarityPair(
ipopt_problem: IpoptProblem,
row: Index,
var: Index,
) -> c_int {
ffi_guard(POUNCE_ERR_INTERNAL, || unsafe {
if ipopt_problem.is_null() {
return POUNCE_ERR_NULL;
}
match append_pair(&mut *ipopt_problem, row, var) {
Ok(()) => POUNCE_OK,
Err(kind) => pair_error_code(&kind),
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn SetPounceComplementarityPairs(
ipopt_problem: IpoptProblem,
n_pairs: Index,
rows: *const Index,
vars: *const Index,
flags: c_uint,
bad_index: *mut Index,
) -> c_int {
ffi_guard(POUNCE_ERR_INTERNAL, || unsafe {
let report = |k: Index| {
if !bad_index.is_null() {
*bad_index = k;
}
};
report(-1);
if ipopt_problem.is_null() {
return POUNCE_ERR_NULL;
}
if flags & !KNOWN_PAIR_FLAGS != 0 {
return POUNCE_ERR_FLAGS;
}
let max_pairs = (*ipopt_problem).n.min((*ipopt_problem).m);
if n_pairs < 0 || n_pairs > max_pairs {
return POUNCE_ERR_COUNT;
}
if n_pairs > 0 && (rows.is_null() || vars.is_null()) {
return POUNCE_ERR_NULL;
}
let new: Vec<(Index, Index)> = if n_pairs == 0 {
Vec::new()
} else {
let r = std::slice::from_raw_parts(rows, n_pairs as usize);
let v = std::slice::from_raw_parts(vars, n_pairs as usize);
r.iter().copied().zip(v.iter().copied()).collect()
};
match replace_pairs(&mut *ipopt_problem, &new) {
Ok(()) => POUNCE_OK,
Err((k, kind)) => {
report(k as Index);
pair_error_code(&kind)
}
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ClearPounceComplementarityPairs(ipopt_problem: IpoptProblem) -> Bool {
ffi_guard(FALSE, || unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
(*ipopt_problem).complementarity.clear();
TRUE
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetPounceComplementarityPairCount(ipopt_problem: IpoptProblem) -> Index {
ffi_guard(-1, || unsafe {
if ipopt_problem.is_null() {
return -1;
}
(&*ipopt_problem).complementarity.len() as Index
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetPounceComplementarityPairs(
ipopt_problem: IpoptProblem,
rows: *mut Index,
vars: *mut Index,
) -> Bool {
ffi_guard(FALSE, || unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
let info = &*ipopt_problem;
let k = info.complementarity.len();
if k == 0 {
return TRUE;
}
if rows.is_null() || vars.is_null() {
return FALSE;
}
let offset = if info.index_style == 1 { 1 } else { 0 };
let rows = std::slice::from_raw_parts_mut(rows, k);
let vars = std::slice::from_raw_parts_mut(vars, k);
for (i, p) in info.complementarity.iter().enumerate() {
rows[i] = p.row as Index + offset;
vars[i] = p.var as Index + offset;
}
TRUE
})
}
pub const POUNCE_COMPLEMENTS_PAIR_MIXED: c_int = 0;
pub const POUNCE_COMPLEMENTS_PAIR_LOWER: c_int = 1;
pub const POUNCE_COMPLEMENTS_PAIR_UPPER: c_int = 2;
pub const POUNCE_COMPLEMENTS_ROW_FIXED_NO_PAIR: c_int = 3;
pub const POUNCE_COMPLEMENTS_VAR_FIXED_NO_PAIR: c_int = 4;
pub(crate) fn complements_row(xl: Number, xu: Number) -> Option<ComplementsRow> {
if xl.is_nan() || xu.is_nan() {
return None;
}
match (xl > NLP_LOWER_BOUND_INF, xu < NLP_UPPER_BOUND_INF) {
(true, true) if xl < xu => Some(ComplementsRow::Mixed),
(true, true) if xl == xu => Some(ComplementsRow::VarFixed),
(true, true) => None,
(true, false) => Some(ComplementsRow::Lower),
(false, true) => Some(ComplementsRow::Upper),
(false, false) => Some(ComplementsRow::RowFixed),
}
}
impl ComplementsRow {
pub(crate) fn row_bounds(self, gl: Number, gu: Number) -> (Number, Number, bool) {
match self {
ComplementsRow::Mixed => (gl, gu, true),
ComplementsRow::Lower => (0.0, gu, true),
ComplementsRow::Upper => (gl, 0.0, true),
ComplementsRow::RowFixed => (0.0, 0.0, false),
ComplementsRow::VarFixed => (gl, gu, false),
}
}
fn code(self) -> c_int {
match self {
ComplementsRow::Mixed => POUNCE_COMPLEMENTS_PAIR_MIXED,
ComplementsRow::Lower => POUNCE_COMPLEMENTS_PAIR_LOWER,
ComplementsRow::Upper => POUNCE_COMPLEMENTS_PAIR_UPPER,
ComplementsRow::RowFixed => POUNCE_COMPLEMENTS_ROW_FIXED_NO_PAIR,
ComplementsRow::VarFixed => POUNCE_COMPLEMENTS_VAR_FIXED_NO_PAIR,
}
}
fn shape_matches(self, shape: Option<PairShape>) -> bool {
matches!(
(self, shape),
(ComplementsRow::Mixed, Some(PairShape::Mixed { .. }))
| (ComplementsRow::Lower, Some(PairShape::OneSided { .. }))
| (ComplementsRow::Upper, Some(PairShape::OneSided { .. }))
)
}
}
fn add_complements(info: &mut IpoptProblemInfo, row: Index, var: Index) -> c_int {
let offset = if info.index_style == 1 { 1 } else { 0 };
let (r, v) = (
zero_based(row, offset, info.m),
zero_based(var, offset, info.n),
);
if r == usize::MAX || v == usize::MAX {
return POUNCE_ERR_OUT_OF_RANGE;
}
if info.complementarity.of_var(v).is_some() || info.complements_used.of_var(v).is_some() {
return POUNCE_ERR_VAR_ALREADY_PAIRED;
}
if info.complementarity.of_row(r).is_some() || info.complements_used.of_row(r).is_some() {
return POUNCE_ERR_ROW_ALREADY_PAIRED;
}
let (gl, gu) = (info.g_l[r], info.g_u[r]);
if !(gl <= NLP_LOWER_BOUND_INF && gu >= NLP_UPPER_BOUND_INF) {
return POUNCE_ERR_ROW_NOT_FREE;
}
let Some(table_row) = complements_row(info.x_l[v], info.x_u[v]) else {
return POUNCE_ERR_VAR_BOUNDS;
};
let (new_gl, new_gu, declares) = table_row.row_bounds(gl, gu);
debug_assert!(
!declares
|| table_row.shape_matches(classify_pair(info.x_l[v], info.x_u[v], new_gl, new_gu)),
"MOI table row {table_row:?} edits to a bound pair that is not its native shape"
);
info.g_l[r] = new_gl;
info.g_u[r] = new_gu;
if declares && append_pair(info, row, var).is_err() {
info.g_l[r] = gl;
info.g_u[r] = gu;
return POUNCE_ERR_INTERNAL;
}
info.complements_used.push(ComplementsUse {
row: r,
var: v,
table: table_row,
user_gl: gl,
user_gu: gu,
});
table_row.code()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn AddPounceComplementsConstraint(
ipopt_problem: IpoptProblem,
row: Index,
var: Index,
) -> c_int {
ffi_guard(POUNCE_ERR_INTERNAL, || unsafe {
if ipopt_problem.is_null() {
return POUNCE_ERR_NULL;
}
add_complements(&mut *ipopt_problem, row, var)
})
}
pub const POUNCE_CC_CLASS_S: c_int = 0;
pub const POUNCE_CC_CLASS_W: c_int = 1;
pub const POUNCE_CC_CLASS_NONE: c_int = 2;
pub const POUNCE_CC_CLASS_NOT_SEARCHED: c_int = 3;
pub const POUNCE_CC_CLASS_ABSENT: c_int = -1;
pub const POUNCE_CC_REPORTED_ANSWER: c_int = 0;
pub const POUNCE_CC_REPORTED_WEAK_FALLBACK: c_int = 1;
pub const POUNCE_CC_REPORTED_FEASIBLE_UNCERTIFIED: c_int = 2;
pub const POUNCE_CC_REPORTED_VERDICT: c_int = 3;
pub const POUNCE_CC_B_VERDICT_B: c_int = 0;
pub const POUNCE_CC_B_VERDICT_NOT_B: c_int = 1;
pub const POUNCE_CC_B_VERDICT_NOT_CHECKED: c_int = 2;
pub const POUNCE_CC_B_VERDICT_ABSENT: c_int = -1;
pub const POUNCE_CC_TEXT_SUMMARY: c_int = 0;
pub const POUNCE_CC_TEXT_POLISH_REASON: c_int = 1;
pub const POUNCE_CC_TEXT_B_REASON: c_int = 2;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct PounceComplementarityStats {
pub struct_size: Index,
pub strategy: [c_char; 16],
pub reported: Index,
pub accepted_by: [c_char; 32],
pub fallback: [c_char; 32],
pub n_solves: Index,
pub total_iterations: Index,
pub max_abs_product: Number,
pub certificate_class: Index,
pub n_pairs: Index,
pub n_biactive: Index,
pub n_neither: Index,
pub multipliers_unique: Index,
pub residual: Number,
pub resid_tol: Number,
pub tight: Index,
pub feasible: Index,
pub primal_violation: Number,
pub b_verdict: Index,
pub b_n_biactive: Index,
pub b_branches_solved: Index,
pub b_enumerated: Index,
pub escapes: Index,
pub lifted: Index,
pub natural_residual: Number,
pub polish_ran: Index,
pub polish_taken: Index,
pub polish_residual_before: Number,
pub polish_residual_after: Number,
pub polish_threshold: Number,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct PounceComplementaritySolve {
pub struct_size: Index,
pub stage: [c_char; 32],
pub relaxed: Index,
pub tau: Number,
pub status: Index,
pub iterations: Index,
pub max_abs_product: Number,
pub seconds: Number,
}
unsafe impl ExtensibleOut for PounceComplementarityStats {}
unsafe impl ExtensibleOut for PounceComplementaritySolve {}
fn copy_label<const N: usize>(dst: &mut [c_char; N], s: &str) {
dst.fill(0);
let keep = s.len().min(N.saturating_sub(1));
for (slot, b) in dst.iter_mut().zip(&s.as_bytes()[..keep]) {
*slot = *b as c_char;
}
}
fn cc_count(v: usize) -> Index {
Index::try_from(v).unwrap_or(Index::MAX)
}
fn cc_real(v: Number) -> Number {
if v.is_finite() { v } else { Number::NAN }
}
fn cc_flag(b: bool) -> Index {
Index::from(b)
}
pub(crate) fn complementarity_stats(
out: &pounce_restoration::mpcc_driver::MpccOutcome,
) -> PounceComplementarityStats {
use pounce_algorithm::mpcc::{BVerdict, Reported, StationarityClass};
let mut s = PounceComplementarityStats {
struct_size: struct_size_of::<PounceComplementarityStats>(),
strategy: [0; 16],
reported: match out.reported {
Reported::Answer(_) => POUNCE_CC_REPORTED_ANSWER,
Reported::WeakFallback(_) => POUNCE_CC_REPORTED_WEAK_FALLBACK,
Reported::FeasibleUncertified(_) => POUNCE_CC_REPORTED_FEASIBLE_UNCERTIFIED,
Reported::Verdict => POUNCE_CC_REPORTED_VERDICT,
},
accepted_by: [0; 32],
fallback: [0; 32],
n_solves: cc_count(out.solves.len()),
total_iterations: cc_count(out.total_iterations),
max_abs_product: cc_real(out.max_abs_product),
certificate_class: POUNCE_CC_CLASS_ABSENT,
n_pairs: -1,
n_biactive: -1,
n_neither: -1,
multipliers_unique: -1,
residual: Number::NAN,
resid_tol: Number::NAN,
tight: -1,
feasible: -1,
primal_violation: Number::NAN,
b_verdict: POUNCE_CC_B_VERDICT_ABSENT,
b_n_biactive: -1,
b_branches_solved: -1,
b_enumerated: -1,
escapes: cc_count(out.escapes),
lifted: cc_count(out.lifted),
natural_residual: out.natural_residual.map_or(Number::NAN, cc_real),
polish_ran: 0,
polish_taken: 0,
polish_residual_before: Number::NAN,
polish_residual_after: Number::NAN,
polish_threshold: cc_real(out.polish_threshold),
};
copy_label(&mut s.strategy, out.strategy.name());
if let Some(stage) = out.accepted_by {
copy_label(&mut s.accepted_by, stage.label());
}
if let Some(stage) = out.fallback {
copy_label(&mut s.fallback, stage.label());
}
if let Some(c) = &out.certificate {
s.certificate_class = match c.class {
StationarityClass::S => POUNCE_CC_CLASS_S,
StationarityClass::W => POUNCE_CC_CLASS_W,
StationarityClass::None => POUNCE_CC_CLASS_NONE,
StationarityClass::NotSearched => POUNCE_CC_CLASS_NOT_SEARCHED,
};
s.n_pairs = cc_count(c.n_pairs);
s.n_biactive = cc_count(c.n_biactive);
s.n_neither = cc_count(c.n_neither);
s.multipliers_unique = cc_flag(c.multipliers_unique);
s.residual = cc_real(c.residual);
s.resid_tol = cc_real(c.resid_tol);
s.tight = cc_flag(c.tight);
s.feasible = cc_flag(c.feasible);
s.primal_violation = cc_real(c.primal_violation);
}
if let Some(b) = &out.b_check {
s.b_verdict = match b.verdict {
BVerdict::B => POUNCE_CC_B_VERDICT_B,
BVerdict::NotB { .. } => POUNCE_CC_B_VERDICT_NOT_B,
BVerdict::NotChecked(_) => POUNCE_CC_B_VERDICT_NOT_CHECKED,
};
s.b_n_biactive = cc_count(b.n_biactive);
s.b_branches_solved = cc_count(b.branches_solved);
s.b_enumerated = cc_flag(b.enumerated);
}
if let Some(p) = &out.polish {
s.polish_ran = 1;
s.polish_taken = cc_flag(p.taken);
s.polish_residual_before = cc_real(p.residual_before);
s.polish_residual_after = cc_real(p.residual_after);
}
s
}
pub(crate) fn complementarity_solve(
r: &pounce_restoration::mpcc_driver::MpccSolveRecord,
) -> PounceComplementaritySolve {
let mut s = PounceComplementaritySolve {
struct_size: struct_size_of::<PounceComplementaritySolve>(),
stage: [0; 32],
relaxed: cc_flag(r.tau.is_some()),
tau: r.tau.unwrap_or(Number::NAN),
status: r.status as Index,
iterations: cc_count(r.iterations),
max_abs_product: cc_real(r.max_abs_product),
seconds: cc_real(r.seconds),
};
copy_label(&mut s.stage, r.stage.label());
s
}
pub(crate) fn complementarity_text(
out: &pounce_restoration::mpcc_driver::MpccOutcome,
which: c_int,
) -> Option<String> {
use pounce_algorithm::mpcc::BVerdict;
match which {
POUNCE_CC_TEXT_SUMMARY => Some(pounce_algorithm::mpcc::describe_outcome(
out.reported,
out.solves.len(),
out.total_iterations,
out.max_abs_product,
out.certificate.as_ref(),
)),
POUNCE_CC_TEXT_POLISH_REASON => out.polish.as_ref().and_then(|p| p.reason.clone()),
POUNCE_CC_TEXT_B_REASON => match out.b_check.as_ref().map(|b| &b.verdict) {
Some(BVerdict::NotChecked(why)) => Some((*why).to_string()),
_ => None,
},
_ => None,
}
}
unsafe fn last_mpcc<'a>(
ipopt_problem: IpoptProblem,
) -> Option<&'a pounce_restoration::mpcc_driver::MpccOutcome> {
unsafe {
if ipopt_problem.is_null() {
return None;
}
(*ipopt_problem)
.last_solve
.as_ref()
.and_then(|ls| ls.mpcc.as_ref())
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetPounceComplementarityStats(
ipopt_problem: IpoptProblem,
stats: *mut PounceComplementarityStats,
) -> Bool {
ffi_guard(FALSE, || unsafe {
if stats.is_null() {
return FALSE;
}
let Some(out) = last_mpcc(ipopt_problem) else {
return FALSE;
};
Bool::from(write_extensible(stats, &complementarity_stats(out)))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetPounceComplementaritySolve(
ipopt_problem: IpoptProblem,
k: Index,
solve: *mut PounceComplementaritySolve,
) -> Bool {
ffi_guard(FALSE, || unsafe {
if solve.is_null() {
return FALSE;
}
let Some(out) = last_mpcc(ipopt_problem) else {
return FALSE;
};
let Some(r) = usize::try_from(k).ok().and_then(|k| out.solves.get(k)) else {
return FALSE;
};
Bool::from(write_extensible(solve, &complementarity_solve(r)))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetPounceComplementarityText(
ipopt_problem: IpoptProblem,
which: c_int,
buf: *mut c_char,
buflen: Index,
) -> Index {
ffi_guard(-1, || unsafe {
let Some(out) = last_mpcc(ipopt_problem) else {
return -1;
};
let Some(text) = complementarity_text(out, which) else {
return -1;
};
let bytes = text.as_bytes();
if !buf.is_null() && buflen > 0 {
let keep = bytes.len().min(buflen as usize - 1);
let dst = std::slice::from_raw_parts_mut(buf, keep + 1);
for (slot, b) in dst.iter_mut().zip(&bytes[..keep]) {
*slot = *b as c_char;
}
dst[keep] = 0;
}
cc_count(bytes.len())
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn IpoptClearWarmStartWorkingSet(ipopt_problem: IpoptProblem) -> Bool {
unsafe {
if ipopt_problem.is_null() {
return FALSE;
}
(*ipopt_problem).pending_working_set = None;
(*ipopt_problem).app.clear_sqp_warm_start();
TRUE
}
}
#[allow(clippy::too_many_arguments)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn IpoptSolveWarmStart(
ipopt_problem: IpoptProblem,
x: *mut Number,
g: *mut Number,
obj_val: *mut Number,
mult_g: *mut Number,
mult_x_L: *mut Number,
mult_x_U: *mut Number,
bound_status_in: *const IpoptBoundStatus,
cons_status_in: *const IpoptConsStatus,
bound_status_out: *mut IpoptBoundStatus,
cons_status_out: *mut IpoptConsStatus,
user_data: *mut c_void,
) -> Index {
if ipopt_problem.is_null() {
return ApplicationReturnStatus::InternalError as Index;
}
ffi_guard(ApplicationReturnStatus::InternalError as Index, || unsafe {
if !bound_status_in.is_null() || !cons_status_in.is_null() {
let _ = IpoptSetWarmStartWorkingSet(ipopt_problem, bound_status_in, cons_status_in);
}
let status = IpoptSolve(
ipopt_problem,
x,
g,
obj_val,
mult_g,
mult_x_L,
mult_x_U,
user_data,
);
if (*ipopt_problem).last_solve.is_some() {
let _ = IpoptGetWorkingSet(ipopt_problem, bound_status_out, cons_status_out);
}
status
})
}
thread_local! {
static CURRENT_BRIDGE: std::cell::Cell<*const CCallbackTnlp> =
const { std::cell::Cell::new(std::ptr::null()) };
}
pub(crate) fn bridge_violation_options(
o: &pounce_common::options_list::OptionsList,
) -> (Number, Number, bool) {
let num = |k: &str, d: Number| {
o.get_numeric_value(k, "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(d)
};
let relax = matches!(
o.get_string_value("fixed_variable_treatment", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.as_deref(),
Some("relax_bounds")
);
(
num("nlp_lower_bound_inf", NLP_LOWER_BOUND_INF),
num("nlp_upper_bound_inf", NLP_UPPER_BOUND_INF),
!relax,
)
}
thread_local! {
static IN_LOWERED_SOLVE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
struct LoweredSolveGuard {
prev: bool,
}
impl LoweredSolveGuard {
fn set(lowered: bool) -> Self {
Self {
prev: IN_LOWERED_SOLVE.with(|c| c.replace(lowered)),
}
}
}
impl Drop for LoweredSolveGuard {
fn drop(&mut self) {
IN_LOWERED_SOLVE.with(|c| c.set(self.prev));
}
}
fn in_lowered_solve() -> bool {
IN_LOWERED_SOLVE.with(|c| c.get())
}
struct BridgeGuard {
prev: *const CCallbackTnlp,
}
impl BridgeGuard {
fn install(bridge: &CCallbackTnlp) -> Self {
let prev = CURRENT_BRIDGE.with(|c| c.replace(bridge as *const _));
Self { prev }
}
}
impl Drop for BridgeGuard {
fn drop(&mut self) {
CURRENT_BRIDGE.with(|c| c.set(self.prev));
}
}
impl CCallbackTnlp {
fn user_grad_lag(&self, it: &ip_intermediate::UserIterate) -> Option<Vec<Number>> {
let n = self.n as usize;
let cb = self.eval_grad_f?;
let mut grad = vec![0.0; n];
let x = it.x.as_ptr() as *mut Number;
if unsafe { cb(self.n, x, TRUE, grad.as_mut_ptr(), self.user_data) } == FALSE {
return None;
}
let nnz = self.nele_jac.max(0) as usize;
let (mut rows, mut cols, mut vals) = (vec![0; nnz], vec![0; nnz], vec![0.0; nnz]);
if self.m > 0 && nnz > 0 {
let jac = self.eval_jac_g?;
let structure = unsafe {
jac(
self.n,
x,
FALSE,
self.m,
self.nele_jac,
rows.as_mut_ptr(),
cols.as_mut_ptr(),
std::ptr::null_mut(),
self.user_data,
)
};
let values = unsafe {
jac(
self.n,
x,
FALSE,
self.m,
self.nele_jac,
std::ptr::null_mut(),
std::ptr::null_mut(),
vals.as_mut_ptr(),
self.user_data,
)
};
if structure == FALSE || values == FALSE {
return None;
}
}
let off = if self.index_style == 1 { 1 } else { 0 };
let to_usize = |v: &[Index]| -> Vec<usize> {
v.iter()
.map(|&k| usize::try_from(k - off).unwrap_or(usize::MAX))
.collect()
};
Some(pounce_nlp::live_iterate::lagrangian_gradient(
it,
&grad,
&to_usize(&rows),
&to_usize(&cols),
&vals,
))
}
}
pub(crate) struct CCallbackTnlp {
pub(crate) n: Index,
pub(crate) m: Index,
pub(crate) nele_jac: Index,
pub(crate) nele_hess: Index,
pub(crate) index_style: Index,
pub(crate) x_l: Vec<Number>,
pub(crate) x_u: Vec<Number>,
pub(crate) g_l: Vec<Number>,
pub(crate) g_u: Vec<Number>,
pub(crate) initial_x: Vec<Number>,
pub(crate) start_lambda: *const Number,
pub(crate) start_z_l: *const Number,
pub(crate) start_z_u: *const Number,
pub(crate) eval_f: Option<Eval_F_CB>,
pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
pub(crate) eval_g: Option<Eval_G_CB>,
pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
pub(crate) eval_h: Option<Eval_H_CB>,
pub(crate) user_data: *mut c_void,
pub(crate) intermediate_cb: Option<Intermediate_CB>,
pub(crate) user_scaling: Option<UserScaling>,
pub(crate) nonlinear_vars: Option<Vec<Index>>,
pub(crate) lower_inf: Number,
pub(crate) upper_inf: Number,
pub(crate) zero_fixed_grad_lag: bool,
pub(crate) final_status: Option<pounce_nlp::alg_types::SolverReturn>,
pub(crate) final_x: Vec<Number>,
pub(crate) final_z_l: Vec<Number>,
pub(crate) final_z_u: Vec<Number>,
pub(crate) final_g: Vec<Number>,
pub(crate) final_lambda: Vec<Number>,
pub(crate) final_obj: Number,
}
impl TNLP for CCallbackTnlp {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: self.n as pounce_common::types::Index,
m: self.m as pounce_common::types::Index,
nnz_jac_g: self.nele_jac as pounce_common::types::Index,
nnz_h_lag: self.nele_hess as pounce_common::types::Index,
index_style: if self.index_style == 1 {
IndexStyle::Fortran
} else {
IndexStyle::C
},
})
}
fn get_number_of_nonlinear_variables(&mut self) -> pounce_common::types::Index {
match &self.nonlinear_vars {
Some(v) => v.len() as pounce_common::types::Index,
None => -1,
}
}
fn get_list_of_nonlinear_variables(
&mut self,
pos_nonlin_vars: &mut [pounce_common::types::Index],
) -> bool {
let Some(v) = self.nonlinear_vars.as_ref() else {
return false;
};
if v.len() != pos_nonlin_vars.len() {
return false;
}
pos_nonlin_vars.copy_from_slice(v);
true
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
if !self.x_l.is_empty() {
b.x_l.copy_from_slice(&self.x_l);
}
if !self.x_u.is_empty() {
b.x_u.copy_from_slice(&self.x_u);
}
if !self.g_l.is_empty() {
b.g_l.copy_from_slice(&self.g_l);
}
if !self.g_u.is_empty() {
b.g_u.copy_from_slice(&self.g_u);
}
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
if !self.initial_x.is_empty() {
sp.x.copy_from_slice(&self.initial_x);
}
let copy_in = |dst: &mut [Number], src: *const Number| {
if !src.is_null() && !dst.is_empty() {
dst.copy_from_slice(unsafe { std::slice::from_raw_parts(src, dst.len()) });
}
};
if sp.init_z {
copy_in(sp.z_l, self.start_z_l);
copy_in(sp.z_u, self.start_z_u);
}
if sp.init_lambda {
copy_in(sp.lambda, self.start_lambda);
}
true
}
fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
let Some(s) = self.user_scaling.as_ref() else {
return false;
};
*req.obj_scaling = s.obj_scaling;
if let Some(x) = s.x_scaling.as_ref() {
if x.len() == req.x_scaling.len() {
req.x_scaling.copy_from_slice(x);
*req.use_x_scaling = true;
}
} else {
*req.use_x_scaling = false;
}
if let Some(g) = s.g_scaling.as_ref() {
if g.len() == req.g_scaling.len() {
req.g_scaling.copy_from_slice(g);
*req.use_g_scaling = true;
}
} else {
*req.use_g_scaling = false;
}
true
}
fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
let cb = self.eval_f?;
let mut obj = 0.0;
let ok = unsafe {
cb(
self.n,
x.as_ptr() as *mut Number,
if new_x { TRUE } else { FALSE },
&mut obj,
self.user_data,
)
};
if ok != FALSE { Some(obj) } else { None }
}
fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
let Some(cb) = self.eval_grad_f else {
return false;
};
let ok = unsafe {
cb(
self.n,
x.as_ptr() as *mut Number,
if new_x { TRUE } else { FALSE },
grad_f.as_mut_ptr(),
self.user_data,
)
};
ok != FALSE
}
fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
if self.m == 0 {
return true;
}
let Some(cb) = self.eval_g else {
return false;
};
let ok = unsafe {
cb(
self.n,
x.as_ptr() as *mut Number,
if new_x { TRUE } else { FALSE },
self.m,
g.as_mut_ptr(),
self.user_data,
)
};
ok != FALSE
}
fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
if self.m == 0 || self.nele_jac == 0 {
return true;
}
let Some(cb) = self.eval_jac_g else {
return false;
};
let x_ptr = x
.map(|s| s.as_ptr() as *mut Number)
.unwrap_or(std::ptr::null_mut());
let ok = match mode {
SparsityRequest::Structure { irow, jcol } => unsafe {
cb(
self.n,
x_ptr,
if new_x { TRUE } else { FALSE },
self.m,
self.nele_jac,
irow.as_mut_ptr(),
jcol.as_mut_ptr(),
std::ptr::null_mut(),
self.user_data,
)
},
SparsityRequest::Values { values } => unsafe {
cb(
self.n,
x_ptr,
if new_x { TRUE } else { FALSE },
self.m,
self.nele_jac,
std::ptr::null_mut(),
std::ptr::null_mut(),
values.as_mut_ptr(),
self.user_data,
)
},
};
ok != FALSE
}
fn eval_h(
&mut self,
x: Option<&[Number]>,
new_x: bool,
obj_factor: Number,
lambda: Option<&[Number]>,
new_lambda: bool,
mode: SparsityRequest<'_>,
) -> bool {
let Some(cb) = self.eval_h else {
return false;
};
if self.nele_hess == 0 {
return true;
}
let x_ptr = x
.map(|s| s.as_ptr() as *mut Number)
.unwrap_or(std::ptr::null_mut());
let lambda_ptr = lambda
.map(|s| s.as_ptr() as *mut Number)
.unwrap_or(std::ptr::null_mut());
let ok = match mode {
SparsityRequest::Structure { irow, jcol } => unsafe {
cb(
self.n,
x_ptr,
if new_x { TRUE } else { FALSE },
obj_factor,
self.m,
lambda_ptr,
if new_lambda { TRUE } else { FALSE },
self.nele_hess,
irow.as_mut_ptr(),
jcol.as_mut_ptr(),
std::ptr::null_mut(),
self.user_data,
)
},
SparsityRequest::Values { values } => unsafe {
cb(
self.n,
x_ptr,
if new_x { TRUE } else { FALSE },
obj_factor,
self.m,
lambda_ptr,
if new_lambda { TRUE } else { FALSE },
self.nele_hess,
std::ptr::null_mut(),
std::ptr::null_mut(),
values.as_mut_ptr(),
self.user_data,
)
},
};
ok != FALSE
}
fn wants_intermediate_callback(&self) -> bool {
self.intermediate_cb.is_some()
}
fn wants_current_iterate(&self) -> bool {
self.intermediate_cb.is_some()
}
fn intermediate_callback(
&mut self,
stats: pounce_nlp::tnlp::IterStats,
_ip_data: &IpoptData,
_ip_cq: &IpoptCq,
) -> bool {
let Some(cb) = self.intermediate_cb else {
return true;
};
let _bridge = BridgeGuard::install(self);
let ok = unsafe {
cb(
stats.mode as Index,
stats.iter as Index,
stats.obj_value,
stats.inf_pr,
stats.inf_du,
stats.mu,
stats.d_norm,
stats.regularization_size,
stats.alpha_du,
stats.alpha_pr,
stats.ls_trials as Index,
self.user_data,
)
};
ok != FALSE
}
fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
self.final_status = Some(sol.status);
if !sol.x.is_empty() {
self.final_x.copy_from_slice(sol.x);
}
if !sol.z_l.is_empty() {
self.final_z_l.copy_from_slice(sol.z_l);
}
if !sol.z_u.is_empty() {
self.final_z_u.copy_from_slice(sol.z_u);
}
if !sol.g.is_empty() {
self.final_g.copy_from_slice(sol.g);
}
if !sol.lambda.is_empty() {
self.final_lambda.copy_from_slice(sol.lambda);
}
self.final_obj = sol.obj_value;
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn IpoptEnableIterHistory(ipopt_problem: IpoptProblem) -> Bool {
if ipopt_problem.is_null() {
return FALSE;
}
let info = unsafe { &mut *ipopt_problem };
info.app.enable_iter_history();
TRUE
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn IpoptWriteSolveReport(
ipopt_problem: IpoptProblem,
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, || unsafe {
if ipopt_problem.is_null() || path.is_null() {
return FALSE;
}
let info = &*ipopt_problem;
let Some(last) = info.last_solve.as_ref() else {
return FALSE;
};
let Ok(path_str) = CStr::from_ptr(path).to_str() else {
return FALSE;
};
let detail_choice = if detail.is_null() {
ReportDetail::Summary
} else {
let Ok(detail_str) = CStr::from_ptr(detail).to_str() else {
return FALSE;
};
match ReportDetail::parse(detail_str) {
Ok(d) => d,
Err(_) => return FALSE,
}
};
let mut builder = ReportBuilder::new(detail_choice, InputDescriptor::TnlpDirect);
builder.problem.n_variables = info.n;
builder.problem.n_constraints = info.m;
builder.problem.n_objectives = 1;
builder.problem.nnz_jac_g = Some(info.nele_jac);
builder.problem.nnz_h_lag = Some(info.nele_hess);
builder.solution.status = last.status;
builder.solution.solve_result_num = status_to_solve_result_num(last.status);
builder.solution.objective = last.final_obj;
builder.solution.x = last.final_x.clone();
builder.solution.lambda = last.final_lambda.clone();
builder.ingest_stats(&last.stats);
if let Some(linsol) = last.linear_solver.clone() {
builder.set_linear_solver_summary(linsol);
}
if let Some(out) = &last.mpcc {
builder.set_complementarity(out.report_info());
}
if let Some(so) = &last.second_opinion
&& so.ran()
{
builder.set_second_opinion(so.report_info());
}
let report = builder.finish();
match write_report_file(std::path::Path::new(path_str), &report) {
Ok(_) => TRUE,
Err(_) => FALSE,
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
unsafe extern "C" fn dummy_eval_f(
_n: Index,
_x: *const Number,
_new_x: Bool,
_obj_value: *mut Number,
_user_data: *mut c_void,
) -> Bool {
TRUE
}
unsafe extern "C" fn dummy_eval_grad_f(
_n: Index,
_x: *const Number,
_new_x: Bool,
_grad_f: *mut Number,
_user_data: *mut c_void,
) -> Bool {
TRUE
}
fn create_unconstrained() -> IpoptProblem {
let xl = [-1.0; 4];
let xu = [1.0; 4];
unsafe {
CreateIpoptProblem(
4,
xl.as_ptr(),
xu.as_ptr(),
0,
std::ptr::null(),
std::ptr::null(),
0,
10,
0,
Some(dummy_eval_f),
None,
Some(dummy_eval_grad_f),
None,
None,
)
}
}
#[test]
fn create_succeeds_for_unconstrained_problem() {
let p = create_unconstrained();
assert!(!p.is_null());
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn create_returns_null_on_missing_required_callbacks() {
let xl = [-1.0; 4];
let xu = [1.0; 4];
let p = unsafe {
CreateIpoptProblem(
4,
xl.as_ptr(),
xu.as_ptr(),
0,
std::ptr::null(),
std::ptr::null(),
0,
10,
0,
None, None,
Some(dummy_eval_grad_f),
None,
None,
)
};
assert!(p.is_null());
}
#[test]
fn create_returns_null_on_negative_n() {
let p = unsafe {
CreateIpoptProblem(
-1,
std::ptr::null(),
std::ptr::null(),
0,
std::ptr::null(),
std::ptr::null(),
0,
10,
0,
Some(dummy_eval_f),
None,
Some(dummy_eval_grad_f),
None,
None,
)
};
assert!(p.is_null());
}
#[test]
fn create_returns_null_on_invalid_index_style() {
let xl = [0.0; 1];
let xu = [1.0; 1];
let p = unsafe {
CreateIpoptProblem(
1,
xl.as_ptr(),
xu.as_ptr(),
0,
std::ptr::null(),
std::ptr::null(),
0,
1,
2, Some(dummy_eval_f),
None,
Some(dummy_eval_grad_f),
None,
None,
)
};
assert!(p.is_null());
}
#[test]
fn add_int_option_forwards_to_application() {
let p = create_unconstrained();
let key = CString::new("print_level").unwrap();
let ok = unsafe { AddIpoptIntOption(p, key.as_ptr(), 5) };
assert_eq!(ok, TRUE);
let info = unsafe { &*p };
let (level, found) = info
.app
.options()
.get_integer_value("print_level", "")
.unwrap();
assert!(found);
assert_eq!(level, 5);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn add_str_option_with_invalid_key_returns_false() {
let p = create_unconstrained();
let key = CString::new("totally_unknown_option").unwrap();
let val = CString::new("yes").unwrap();
let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
assert_eq!(ok, FALSE);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn add_options_on_null_problem_returns_false() {
let key = CString::new("print_level").unwrap();
let v = CString::new("yes").unwrap();
unsafe {
assert_eq!(
AddIpoptIntOption(std::ptr::null_mut(), key.as_ptr(), 5),
FALSE
);
assert_eq!(
AddIpoptNumOption(std::ptr::null_mut(), key.as_ptr(), 1.0),
FALSE
);
assert_eq!(
AddIpoptStrOption(std::ptr::null_mut(), key.as_ptr(), v.as_ptr()),
FALSE
);
}
}
unsafe extern "C" fn dummy_intermediate(
_alg_mod: Index,
_iter_count: Index,
_obj_value: Number,
_inf_pr: Number,
_inf_du: Number,
_mu: Number,
_d_norm: Number,
_regularization_size: Number,
_alpha_du: Number,
_alpha_pr: Number,
_ls_trials: Index,
_user_data: *mut c_void,
) -> Bool {
TRUE
}
#[test]
fn set_intermediate_callback_stores_pointer() {
let p = create_unconstrained();
let ok = unsafe { SetIntermediateCallback(p, Some(dummy_intermediate)) };
assert_eq!(ok, TRUE);
let info = unsafe { &*p };
assert!(info.intermediate_cb.is_some());
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn solve_returns_internal_error_on_null_problem() {
let rc = unsafe {
IpoptSolve(
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(rc, -199);
}
#[test]
fn free_null_is_safe() {
unsafe { FreeIpoptProblem(std::ptr::null_mut()) };
}
unsafe extern "C" fn quad_eval_f(
_n: Index,
x: *const Number,
_new_x: Bool,
obj_value: *mut Number,
_user_data: *mut c_void,
) -> Bool {
unsafe {
let v = *x.offset(0);
*obj_value = (v - 2.0) * (v - 2.0);
TRUE
}
}
unsafe extern "C" fn quad_eval_grad_f(
_n: Index,
x: *const Number,
_new_x: Bool,
grad: *mut Number,
_user_data: *mut c_void,
) -> Bool {
unsafe {
let v = *x.offset(0);
*grad.offset(0) = 2.0 * (v - 2.0);
TRUE
}
}
unsafe extern "C" fn quad_eval_h(
_n: Index,
_x: *const Number,
_new_x: Bool,
obj_factor: Number,
_m: Index,
_lambda: *const Number,
_new_lambda: Bool,
_nele_hess: Index,
irow: *mut Index,
jcol: *mut Index,
values: *mut Number,
_user_data: *mut c_void,
) -> Bool {
unsafe {
if !irow.is_null() && !jcol.is_null() && values.is_null() {
*irow.offset(0) = 0;
*jcol.offset(0) = 0;
} else if irow.is_null() && jcol.is_null() && !values.is_null() {
*values.offset(0) = 2.0 * obj_factor;
} else {
return FALSE;
}
TRUE
}
}
#[test]
fn solve_drives_unconstrained_quadratic_through_bridge() {
let xl = [-1.0e20];
let xu = [1.0e20];
let p = unsafe {
CreateIpoptProblem(
1,
xl.as_ptr(),
xu.as_ptr(),
0,
std::ptr::null(),
std::ptr::null(),
0,
1,
0,
Some(quad_eval_f),
None,
Some(quad_eval_grad_f),
None,
Some(quad_eval_h),
)
};
assert!(!p.is_null());
let mut x = [0.0_f64];
let mut obj = 0.0_f64;
let rc = unsafe {
IpoptSolve(
p,
x.as_mut_ptr(),
std::ptr::null_mut(),
&mut obj,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
assert!((x[0] - 2.0).abs() < 1e-6, "x[0] = {}", x[0]);
assert!(obj.abs() < 1e-10, "obj = {}", obj);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn stale_stats_cleared_when_resolve_bails() {
let xl = [-1.0e20];
let xu = [1.0e20];
let p = unsafe {
CreateIpoptProblem(
1,
xl.as_ptr(),
xu.as_ptr(),
0,
std::ptr::null(),
std::ptr::null(),
0,
1,
0,
Some(quad_eval_f),
None,
Some(quad_eval_grad_f),
None,
Some(quad_eval_h),
)
};
assert!(!p.is_null());
let mut x = [0.0_f64];
let mut obj = 0.0_f64;
let rc = unsafe {
IpoptSolve(
p,
x.as_mut_ptr(),
std::ptr::null_mut(),
&mut obj,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
let iters_after_success = unsafe { GetIpoptIterCount(p) };
assert!(
iters_after_success >= 1,
"a converged solve should record >=1 iteration, got {iters_after_success}"
);
assert!(unsafe { (*p).last_solve.is_some() });
unsafe { (*p).n = -1 };
let mut x2 = [0.0_f64];
let rc2 = unsafe {
IpoptSolve(
p,
x2.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(
rc2,
ApplicationReturnStatus::InvalidProblemDefinition as Index
);
assert!(
unsafe { (*p).last_solve.is_none() },
"a bailed re-solve must clear stale last_solve (F5)"
);
assert_eq!(
unsafe { GetIpoptIterCount(p) },
0,
"stale iteration count must not survive a bailed re-solve (F5)"
);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn solve_invalid_problem_definition_when_x_null() {
let p = create_unconstrained();
let rc = unsafe {
IpoptSolve(
p,
std::ptr::null_mut(), std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(
rc,
ApplicationReturnStatus::InvalidProblemDefinition as Index
);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn get_version_writes_pkg_version() {
let (mut mj, mut mn, mut pt) = (-1, -1, -1);
unsafe { GetIpoptVersion(&mut mj, &mut mn, &mut pt) };
let expected = parse_pkg_version(env!("CARGO_PKG_VERSION"));
assert_eq!((mj, mn, pt), expected);
}
#[test]
fn get_version_tolerates_null_buffers() {
unsafe {
GetIpoptVersion(
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
}
#[test]
fn set_scaling_stores_user_supplied_arrays() {
let p = create_unconstrained();
let xs = [2.0, 3.0, 4.0, 5.0];
let ok = unsafe { SetIpoptProblemScaling(p, 7.0, xs.as_ptr(), std::ptr::null()) };
assert_eq!(ok, TRUE);
let info = unsafe { &*p };
let s = info.user_scaling.as_ref().unwrap();
assert_eq!(s.obj_scaling, 7.0);
assert_eq!(s.x_scaling.as_deref(), Some(&xs[..]));
assert!(s.g_scaling.is_none());
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn set_scaling_on_null_problem_returns_false() {
let ok = unsafe {
SetIpoptProblemScaling(
std::ptr::null_mut(),
1.0,
std::ptr::null(),
std::ptr::null(),
)
};
assert_eq!(ok, FALSE);
}
#[test]
fn open_output_file_writes_and_attaches_journal() {
let p = create_unconstrained();
let dir = std::env::temp_dir().join("pounce-cinterface-test");
let _ = std::fs::create_dir_all(&dir);
let path = dir.join("output.log");
let cstr = CString::new(path.to_string_lossy().as_bytes()).unwrap();
let ok = unsafe { OpenIpoptOutputFile(p, cstr.as_ptr(), 5) };
assert_eq!(ok, TRUE);
let info = unsafe { &*p };
let (level, found) = info
.app
.options()
.get_integer_value("file_print_level", "")
.unwrap();
assert!(found);
assert_eq!(level, 5);
unsafe { FreeIpoptProblem(p) };
let _ = std::fs::remove_file(&path);
}
#[test]
fn open_output_file_with_null_inputs_returns_false() {
let key = CString::new("nope").unwrap();
unsafe {
assert_eq!(
OpenIpoptOutputFile(std::ptr::null_mut(), key.as_ptr(), 0),
FALSE
);
}
let p = create_unconstrained();
unsafe {
assert_eq!(OpenIpoptOutputFile(p, std::ptr::null(), 0), FALSE);
FreeIpoptProblem(p);
}
}
#[test]
fn get_current_iterate_returns_false_outside_callback() {
let p = create_unconstrained();
let rc = unsafe {
GetIpoptCurrentIterate(
p,
FALSE,
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(rc, FALSE);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn get_current_violations_returns_false_outside_callback() {
let p = create_unconstrained();
let rc = unsafe {
GetIpoptCurrentViolations(
p,
FALSE,
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(rc, FALSE);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn post_solve_stats_zero_before_solve() {
let p = create_unconstrained();
unsafe {
assert_eq!(GetIpoptIterCount(p), 0);
assert_eq!(GetIpoptSolveTime(p), 0.0);
assert_eq!(GetIpoptPrimalInf(p), 0.0);
assert_eq!(GetIpoptDualInf(p), 0.0);
assert_eq!(GetIpoptComplInf(p), 0.0);
FreeIpoptProblem(p);
}
}
#[test]
fn post_solve_stats_populated_after_solve() {
let xl = [-1.0e20];
let xu = [1.0e20];
let p = unsafe {
CreateIpoptProblem(
1,
xl.as_ptr(),
xu.as_ptr(),
0,
std::ptr::null(),
std::ptr::null(),
0,
1,
0,
Some(quad_eval_f),
None,
Some(quad_eval_grad_f),
None,
Some(quad_eval_h),
)
};
let mut x = [0.0_f64];
let mut obj = 0.0_f64;
let rc = unsafe {
IpoptSolve(
p,
x.as_mut_ptr(),
std::ptr::null_mut(),
&mut obj,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
unsafe {
assert!(GetIpoptIterCount(p) >= 0);
assert!(GetIpoptSolveTime(p) >= 0.0);
assert!(GetIpoptPrimalInf(p).is_finite());
assert!(GetIpoptDualInf(p).is_finite());
assert!(GetIpoptComplInf(p).is_finite());
FreeIpoptProblem(p);
}
}
#[test]
fn linear_solver_stats_populated_after_solve() {
let xl = [-1.0e20];
let xu = [1.0e20];
let p = unsafe {
CreateIpoptProblem(
1,
xl.as_ptr(),
xu.as_ptr(),
0,
std::ptr::null(),
std::ptr::null(),
0,
1,
0,
Some(quad_eval_f),
None,
Some(quad_eval_grad_f),
None,
Some(quad_eval_h),
)
};
let mut stats = unsafe { std::mem::zeroed::<PounceLinearSolverStats>() };
assert_eq!(unsafe { GetPounceLinearSolverStats(p, &mut stats) }, FALSE);
let mut x = [0.0_f64];
let mut obj = 0.0_f64;
let rc = unsafe {
IpoptSolve(
p,
x.as_mut_ptr(),
std::ptr::null_mut(),
&mut obj,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
assert_eq!(unsafe { GetPounceLinearSolverStats(p, &mut stats) }, TRUE);
let name = unsafe { CStr::from_ptr(stats.solver_name.as_ptr()) }
.to_str()
.expect("solver name is ASCII");
assert_eq!(name, "feral", "default backend should report itself");
assert!(stats.n_factors > 0, "n_factors = {}", stats.n_factors);
assert_eq!(
stats.n_pattern_reuse + stats.n_pattern_changes,
stats.n_factors,
"every factor is either a pattern reuse or a pattern change"
);
assert!(stats.max_fill_ratio.is_nan() || stats.max_fill_ratio > 0.0);
assert!(stats.last_nnz_l == -1 || stats.last_nnz_l > 0);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn option_type_reports_the_setter_a_keyword_expects() {
let p = create_unconstrained();
let ty = |s: &str| {
let c = std::ffi::CString::new(s).unwrap();
unsafe { GetPounceOptionType(p, c.as_ptr()) }
};
assert_eq!(ty("tol"), 1, "tol is a number");
assert_eq!(ty("max_iter"), 2, "max_iter is an integer");
assert_eq!(ty("linear_solver"), 3, "linear_solver is a string");
assert_eq!(ty("hessian_approximation"), 3);
assert_eq!(ty("no_such_option_at_all"), 0);
assert_eq!(unsafe { GetPounceOptionType(p, std::ptr::null()) }, 0);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn option_type_answers_without_a_problem_handle() {
let ty = |s: &str| {
let c = std::ffi::CString::new(s).unwrap();
unsafe { GetPounceOptionType(std::ptr::null_mut(), c.as_ptr()) }
};
assert_eq!(ty("tol"), 1);
assert_eq!(ty("max_iter"), 2);
assert_eq!(ty("linear_solver"), 3);
assert_eq!(ty("no_such_option_at_all"), 0);
let p = create_unconstrained();
for name in [
"tol",
"max_iter",
"linear_solver",
"mu_strategy",
"print_level",
] {
let c = std::ffi::CString::new(name).unwrap();
assert_eq!(
unsafe { GetPounceOptionType(std::ptr::null_mut(), c.as_ptr()) },
unsafe { GetPounceOptionType(p, c.as_ptr()) },
"handle-free and problem-bound disagree on {name}"
);
}
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn write_solve_report_emits_v1_json_with_iter_history() {
let xl = [-1.0e20];
let xu = [1.0e20];
let p = unsafe {
CreateIpoptProblem(
1,
xl.as_ptr(),
xu.as_ptr(),
0,
std::ptr::null(),
std::ptr::null(),
0,
1,
0,
Some(quad_eval_f),
None,
Some(quad_eval_grad_f),
None,
Some(quad_eval_h),
)
};
let cpath = CString::new("/tmp/pounce_cinterface_no_solve.json").unwrap();
let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), std::ptr::null()) };
assert_eq!(bad, FALSE);
assert_eq!(unsafe { IpoptEnableIterHistory(p) }, TRUE);
let mut x = [0.0_f64];
let mut obj = 0.0_f64;
let rc = unsafe {
IpoptSolve(
p,
x.as_mut_ptr(),
std::ptr::null_mut(),
&mut obj,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
let dir = std::env::temp_dir();
let path = dir.join("pounce_cinterface_report.json");
let cpath = CString::new(path.to_str().unwrap()).unwrap();
let cdetail = CString::new("full").unwrap();
let ok = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), cdetail.as_ptr()) };
assert_eq!(ok, TRUE);
let txt = std::fs::read_to_string(&path).unwrap();
assert!(
txt.contains("\"schema\": \"pounce.solve-report/v1\""),
"{txt}"
);
assert!(txt.contains("\"kind\": \"tnlp-direct\""));
let parsed: pounce_solve_report::SolveReport = serde_json::from_str(&txt).unwrap();
assert_eq!(parsed.problem.n_variables, 1);
assert_eq!(parsed.problem.n_constraints, 0);
let bad_detail = CString::new("verbose").unwrap();
let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), bad_detail.as_ptr()) };
assert_eq!(bad, FALSE);
let _ = std::fs::remove_file(&path);
unsafe { FreeIpoptProblem(p) };
}
unsafe extern "C" fn cb_quad_eval_g(
_n: Index,
x: *const Number,
_new_x: Bool,
_m: Index,
g: *mut Number,
_user_data: *mut c_void,
) -> Bool {
unsafe {
*g.offset(0) = *x.offset(0);
TRUE
}
}
unsafe extern "C" fn cb_quad_eval_jac_g(
_n: Index,
_x: *const Number,
_new_x: Bool,
_m: Index,
nele_jac: Index,
irow: *mut Index,
jcol: *mut Index,
values: *mut Number,
_user_data: *mut c_void,
) -> Bool {
unsafe {
assert_eq!(nele_jac, 1);
if !irow.is_null() {
*irow.offset(0) = 0;
*jcol.offset(0) = 0;
}
if !values.is_null() {
*values.offset(0) = 1.0;
}
TRUE
}
}
unsafe extern "C" fn cb_quad_eval_h(
_n: Index,
_x: *const Number,
_new_x: Bool,
obj_factor: Number,
_m: Index,
_lambda: *const Number,
_new_lambda: Bool,
_nele_hess: Index,
irow: *mut Index,
jcol: *mut Index,
values: *mut Number,
_user_data: *mut c_void,
) -> Bool {
unsafe {
if !irow.is_null() {
*irow.offset(0) = 0;
*jcol.offset(0) = 0;
}
if !values.is_null() {
*values.offset(0) = 2.0 * obj_factor;
}
TRUE
}
}
fn create_callback_test_problem() -> IpoptProblem {
let xl = [-1.0e20];
let xu = [1.0e20];
let gl = [-10.0];
let gu = [10.0];
unsafe {
CreateIpoptProblem(
1,
xl.as_ptr(),
xu.as_ptr(),
1,
gl.as_ptr(),
gu.as_ptr(),
1,
1,
0,
Some(quad_eval_f),
Some(cb_quad_eval_g),
Some(quad_eval_grad_f),
Some(cb_quad_eval_jac_g),
Some(cb_quad_eval_h),
)
}
}
static CB_ITER_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
static CB_LAST_ITER: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
static CB_INSPECTOR_OK: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
unsafe extern "C" fn counting_cb(
_alg_mod: Index,
iter_count: Index,
_obj_value: Number,
_inf_pr: Number,
_inf_du: Number,
_mu: Number,
_d_norm: Number,
_regularization_size: Number,
_alpha_du: Number,
_alpha_pr: Number,
_ls_trials: Index,
user_data: *mut c_void,
) -> Bool {
unsafe {
CB_ITER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
CB_LAST_ITER.store(iter_count, std::sync::atomic::Ordering::SeqCst);
let problem = user_data as IpoptProblem;
let mut x = [0.0_f64];
let rc = GetIpoptCurrentIterate(
problem,
FALSE,
1,
x.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
1,
std::ptr::null_mut(),
std::ptr::null_mut(),
);
if rc == TRUE && x[0].is_finite() {
CB_INSPECTOR_OK.store(true, std::sync::atomic::Ordering::SeqCst);
}
TRUE
}
}
#[test]
fn intermediate_callback_fires_per_iteration_and_inspector_reads_x() {
CB_ITER_COUNTER.store(0, std::sync::atomic::Ordering::SeqCst);
CB_LAST_ITER.store(-1, std::sync::atomic::Ordering::SeqCst);
CB_INSPECTOR_OK.store(false, std::sync::atomic::Ordering::SeqCst);
let p = create_callback_test_problem();
assert!(!p.is_null());
let ok = unsafe { SetIntermediateCallback(p, Some(counting_cb)) };
assert_eq!(ok, TRUE);
let mut x = [0.0_f64];
let mut obj = 0.0_f64;
let rc = unsafe {
IpoptSolve(
p,
x.as_mut_ptr(),
std::ptr::null_mut(),
&mut obj,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
p as *mut c_void,
)
};
assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
let n_fires = CB_ITER_COUNTER.load(std::sync::atomic::Ordering::SeqCst);
assert!(n_fires >= 2, "callback fired {n_fires} times, want >=2");
assert!(
CB_LAST_ITER.load(std::sync::atomic::Ordering::SeqCst) >= 1,
"last iter should be >= 1 after at least one accepted step"
);
assert!(
CB_INSPECTOR_OK.load(std::sync::atomic::Ordering::SeqCst),
"GetIpoptCurrentIterate did not return a usable x"
);
unsafe { FreeIpoptProblem(p) };
}
static CB_VIOL_OK: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
fn create_bounded_callback_test_problem() -> IpoptProblem {
let xl = [0.0];
let xu = [10.0];
let gl = [-10.0];
let gu = [10.0];
unsafe {
CreateIpoptProblem(
1,
xl.as_ptr(),
xu.as_ptr(),
1,
gl.as_ptr(),
gu.as_ptr(),
1,
1,
0,
Some(quad_eval_f),
Some(cb_quad_eval_g),
Some(quad_eval_grad_f),
Some(cb_quad_eval_jac_g),
Some(cb_quad_eval_h),
)
}
}
unsafe extern "C" fn violations_inspecting_cb(
_alg_mod: Index,
_iter_count: Index,
_obj_value: Number,
_inf_pr: Number,
_inf_du: Number,
_mu: Number,
_d_norm: Number,
_regularization_size: Number,
_alpha_du: Number,
_alpha_pr: Number,
_ls_trials: Index,
user_data: *mut c_void,
) -> Bool {
unsafe {
let problem = user_data as IpoptProblem;
let mut x_l_viol = [f64::NAN];
let mut x_u_viol = [f64::NAN];
let rc = GetIpoptCurrentViolations(
problem,
FALSE,
1,
x_l_viol.as_mut_ptr(),
x_u_viol.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
1,
std::ptr::null_mut(),
std::ptr::null_mut(),
);
if rc == TRUE
&& x_l_viol[0].is_finite()
&& x_l_viol[0] >= 0.0
&& x_u_viol[0].is_finite()
&& x_u_viol[0] >= 0.0
{
CB_VIOL_OK.store(true, std::sync::atomic::Ordering::SeqCst);
}
TRUE
}
}
#[test]
fn get_current_violations_inside_callback_reports_finite_bounds() {
CB_VIOL_OK.store(false, std::sync::atomic::Ordering::SeqCst);
let p = create_bounded_callback_test_problem();
assert!(!p.is_null());
let ok = unsafe { SetIntermediateCallback(p, Some(violations_inspecting_cb)) };
assert_eq!(ok, TRUE);
let mut x = [5.0_f64];
let mut obj = 0.0_f64;
let rc = unsafe {
IpoptSolve(
p,
x.as_mut_ptr(),
std::ptr::null_mut(),
&mut obj,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
p as *mut c_void,
)
};
assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
assert!(
CB_VIOL_OK.load(std::sync::atomic::Ordering::SeqCst),
"GetIpoptCurrentViolations did not return finite, non-negative \
bound violations from inside the callback"
);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn bound_violation_scatter_rejects_oversized_pack_instead_of_panicking() {
let n_us = 1usize;
let packed = [0.5_f64, -0.3];
let unguarded = std::panic::catch_unwind(|| {
let mut v = vec![0.0; n_us];
for (i, s) in packed.iter().enumerate() {
v[i] = (-s).max(0.0);
}
v
});
assert!(
unguarded.is_err(),
"unguarded scatter should panic (→ abort across extern \"C\") on an oversized pack"
);
let guarded: Result<Vec<f64>, ()> = (|| {
if packed.len() != n_us {
return Err(());
}
let mut v = vec![0.0; n_us];
for (i, s) in packed.iter().enumerate() {
v[i] = (-s).max(0.0);
}
Ok(v)
})();
assert!(
guarded.is_err(),
"guarded scatter should reject the length mismatch (return FALSE), not panic"
);
}
unsafe extern "C" fn user_stop_cb(
_alg_mod: Index,
_iter_count: Index,
_obj_value: Number,
_inf_pr: Number,
_inf_du: Number,
_mu: Number,
_d_norm: Number,
_regularization_size: Number,
_alpha_du: Number,
_alpha_pr: Number,
_ls_trials: Index,
_user_data: *mut c_void,
) -> Bool {
FALSE
}
#[test]
fn intermediate_callback_false_surfaces_user_requested_stop() {
let p = create_callback_test_problem();
assert!(!p.is_null());
let ok = unsafe { SetIntermediateCallback(p, Some(user_stop_cb)) };
assert_eq!(ok, TRUE);
let mut x = [0.0_f64];
let rc = unsafe {
IpoptSolve(
p,
x.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(rc, ApplicationReturnStatus::UserRequestedStop as Index);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn ffi_guard_converts_panic_to_fallback() {
let fallback = ApplicationReturnStatus::InternalError as Index;
let got = ffi_guard(fallback, || -> Index {
panic!("boom inside solver core");
});
assert_eq!(got, fallback);
assert_eq!(got, ApplicationReturnStatus::InternalError as Index);
}
#[test]
fn ffi_guard_is_transparent_on_success() {
let got = ffi_guard(-99, || 7);
assert_eq!(got, 7);
}
#[test]
fn parse_pkg_version_handles_missing_components() {
assert_eq!(parse_pkg_version("1.2.3"), (1, 2, 3));
assert_eq!(parse_pkg_version("4.5"), (4, 5, 0));
assert_eq!(parse_pkg_version(""), (0, 0, 0));
assert_eq!(parse_pkg_version("1.x.3"), (1, 0, 3));
}
use crate::solver::{
IpoptCreateSolver, IpoptFreeSolver, IpoptSolverGetKktDim, IpoptSolverKktSolve,
IpoptSolverSolve,
};
#[test]
fn solver_create_consumes_problem_handle() {
let mut p = create_unconstrained();
assert!(!p.is_null());
let s = unsafe { IpoptCreateSolver(&mut p) };
assert!(!s.is_null());
assert!(
p.is_null(),
"IpoptCreateSolver should NULL out the caller's handle"
);
unsafe { IpoptFreeSolver(s) };
}
#[test]
fn solver_create_null_inputs_return_null() {
let s = unsafe { IpoptCreateSolver(std::ptr::null_mut()) };
assert!(s.is_null());
let mut p: IpoptProblem = std::ptr::null_mut();
let s = unsafe { IpoptCreateSolver(&mut p) };
assert!(s.is_null());
}
#[test]
fn solver_free_null_is_safe() {
unsafe { IpoptFreeSolver(std::ptr::null_mut()) };
}
#[test]
fn solver_solve_drives_quadratic_and_retains_factor() {
let xl = [-1.0e20];
let xu = [1.0e20];
let mut p = unsafe {
CreateIpoptProblem(
1,
xl.as_ptr(),
xu.as_ptr(),
0,
std::ptr::null(),
std::ptr::null(),
0,
1,
0,
Some(quad_eval_f),
None,
Some(quad_eval_grad_f),
None,
Some(quad_eval_h),
)
};
assert!(!p.is_null());
let s = unsafe { IpoptCreateSolver(&mut p) };
assert!(!s.is_null());
let mut x = [0.0_f64];
let mut obj = 0.0_f64;
let rc = unsafe {
IpoptSolverSolve(
s,
x.as_mut_ptr(),
std::ptr::null_mut(),
&mut obj,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
assert!((x[0] - 2.0).abs() < 1e-6);
assert!(obj.abs() < 1e-10);
let dim = unsafe { IpoptSolverGetKktDim(s) };
assert!(dim > 0, "expected positive KKT dim, got {dim}");
let rhs = vec![0.0_f64; dim as usize];
let mut lhs = vec![1.0_f64; dim as usize];
let ok = unsafe { IpoptSolverKktSolve(s, rhs.as_ptr(), lhs.as_mut_ptr()) };
assert_eq!(ok, TRUE);
for (i, v) in lhs.iter().enumerate() {
assert!(v.abs() < 1e-10, "lhs[{i}] = {v} not ~0");
}
unsafe { IpoptFreeSolver(s) };
}
#[test]
fn solver_kkt_dim_minus_one_before_solve() {
let mut p = create_unconstrained();
let s = unsafe { IpoptCreateSolver(&mut p) };
assert_eq!(unsafe { IpoptSolverGetKktDim(s) }, -1);
unsafe { IpoptFreeSolver(s) };
}
#[test]
fn c_get_working_set_returns_false_before_any_solve() {
let p = create_unconstrained();
let mut bound_buf = [0; 4];
let rc = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), std::ptr::null_mut()) };
assert_eq!(rc, FALSE);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn c_set_warm_start_with_both_null_returns_false() {
let p = create_unconstrained();
let rc = unsafe { IpoptSetWarmStartWorkingSet(p, std::ptr::null(), std::ptr::null()) };
assert_eq!(rc, FALSE);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn c_set_warm_start_with_bad_status_code_returns_false() {
let p = create_unconstrained();
let bogus = [
POUNCE_WS_INACTIVE,
7,
POUNCE_WS_AT_LOWER,
POUNCE_WS_INACTIVE,
];
let rc = unsafe { IpoptSetWarmStartWorkingSet(p, bogus.as_ptr(), std::ptr::null()) };
assert_eq!(rc, FALSE);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn c_set_warm_start_then_clear_succeeds() {
let p = create_unconstrained();
let in_buf = [POUNCE_WS_INACTIVE; 4];
let set_rc = unsafe { IpoptSetWarmStartWorkingSet(p, in_buf.as_ptr(), std::ptr::null()) };
assert_eq!(set_rc, TRUE);
let clr_rc = unsafe { IpoptClearWarmStartWorkingSet(p) };
assert_eq!(clr_rc, TRUE);
unsafe { FreeIpoptProblem(p) };
}
#[test]
fn c_set_warm_start_on_null_problem_returns_false() {
let in_buf = [POUNCE_WS_INACTIVE; 1];
let rc = unsafe {
IpoptSetWarmStartWorkingSet(std::ptr::null_mut(), in_buf.as_ptr(), std::ptr::null())
};
assert_eq!(rc, FALSE);
}
#[test]
fn c_solve_warm_start_round_trips_working_set_on_sqp_path() {
let p = create_callback_test_problem();
let key = CString::new("algorithm").unwrap();
let val = CString::new("active-set-sqp").unwrap();
let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
assert_eq!(ok, TRUE);
let mut x = [0.0_f64];
let mut obj = 0.0_f64;
let rc1 = unsafe {
IpoptSolve(
p,
x.as_mut_ptr(),
std::ptr::null_mut(),
&mut obj,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(rc1, ApplicationReturnStatus::SolveSucceeded as Index);
let mut bound_buf = [-1; 1];
let mut cons_buf = [-1; 1];
let got = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), cons_buf.as_mut_ptr()) };
assert_eq!(got, TRUE);
assert!((0..=3).contains(&bound_buf[0]));
assert!((0..=3).contains(&cons_buf[0]));
x[0] = 0.0;
let mut obj2 = 0.0_f64;
let mut bound_out = [-1; 1];
let mut cons_out = [-1; 1];
let rc2 = unsafe {
IpoptSolveWarmStart(
p,
x.as_mut_ptr(),
std::ptr::null_mut(),
&mut obj2,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
bound_buf.as_ptr(),
cons_buf.as_ptr(),
bound_out.as_mut_ptr(),
cons_out.as_mut_ptr(),
std::ptr::null_mut(),
)
};
assert_eq!(rc2, ApplicationReturnStatus::SolveSucceeded as Index);
assert!((0..=3).contains(&bound_out[0]));
assert!((0..=3).contains(&cons_out[0]));
unsafe { FreeIpoptProblem(p) };
}
fn synthetic_outcome() -> pounce_restoration::mpcc_driver::MpccOutcome {
use pounce_algorithm::mpcc::{
Certificate, MpccStage, Reported, StationarityClass, Strategy,
};
use pounce_restoration::mpcc_driver::{MpccOutcome, MpccSolveRecord};
MpccOutcome {
status: ApplicationReturnStatus::SolveSucceeded,
statistics: SolveStatistics::default(),
accepted_by: Some(MpccStage::Exact),
fallback: None,
reported: Reported::Answer(MpccStage::Exact),
strategy: Strategy::Continuation,
solves: vec![MpccSolveRecord {
stage: MpccStage::Branch,
tau: None,
status: ApplicationReturnStatus::MaximumIterationsExceeded,
iterations: 7,
budget_iterations: 7,
max_abs_product: Number::INFINITY,
seconds: Number::NAN,
}],
total_iterations: 7,
x: vec![0.0],
max_abs_product: Number::INFINITY,
certificate: Some(Certificate {
class: StationarityClass::S,
n_pairs: 1,
n_biactive: 0,
n_neither: 0,
multipliers_unique: true,
residual: 0.0,
resid_tol: Number::INFINITY,
tight: true,
primal_violation: 0.0,
feasible: true,
worst_biactive_sign: 0.0,
multipliers: None,
}),
b_check: None,
escapes: 3,
lifted: 2,
natural_residual: None,
polish: None,
polish_threshold: 1e-7,
}
}
fn c_label(chars: &[c_char]) -> String {
unsafe { CStr::from_ptr(chars.as_ptr()) }
.to_str()
.unwrap()
.to_string()
}
#[test]
fn complementarity_encodings_absent_certificate_and_non_finite_reals() {
let mut out = synthetic_outcome();
out.certificate = None;
let s = complementarity_stats(&out);
assert_eq!(s.certificate_class, POUNCE_CC_CLASS_ABSENT);
assert_eq!(
(
s.n_pairs,
s.n_biactive,
s.n_neither,
s.multipliers_unique,
s.tight,
s.feasible
),
(-1, -1, -1, -1, -1, -1)
);
assert!(s.residual.is_nan() && s.resid_tol.is_nan() && s.primal_violation.is_nan());
assert!(s.max_abs_product.is_nan() && s.natural_residual.is_nan());
assert_eq!(c_label(&s.strategy), "continuation");
assert_eq!(c_label(&s.accepted_by), "exact product");
assert_eq!(c_label(&s.fallback), "");
assert_eq!((s.escapes, s.lifted), (3, 2));
assert!(complementarity_text(&out, POUNCE_CC_TEXT_SUMMARY).is_some());
let s = complementarity_stats(&synthetic_outcome());
assert_eq!(s.certificate_class, POUNCE_CC_CLASS_S);
assert!(s.resid_tol.is_nan());
assert_eq!(
(s.n_pairs, s.multipliers_unique, s.tight, s.feasible),
(1, 1, 1, 1)
);
let r = complementarity_solve(&out.solves[0]);
assert_eq!(c_label(&r.stage), "branch NLP");
assert_eq!((r.relaxed, r.status, r.iterations), (0, -1, 7));
assert!(r.tau.is_nan() && r.max_abs_product.is_nan() && r.seconds.is_nan());
}
#[test]
fn complementarity_encodings_every_reported_and_class() {
use pounce_algorithm::mpcc::{MpccStage, Reported, StationarityClass};
let mut out = synthetic_outcome();
for (reported, code) in [
(
Reported::Answer(MpccStage::Exact),
POUNCE_CC_REPORTED_ANSWER,
),
(
Reported::WeakFallback(MpccStage::Penalty),
POUNCE_CC_REPORTED_WEAK_FALLBACK,
),
(
Reported::FeasibleUncertified(MpccStage::Continuation),
POUNCE_CC_REPORTED_FEASIBLE_UNCERTIFIED,
),
(Reported::Verdict, POUNCE_CC_REPORTED_VERDICT),
] {
out.reported = reported;
assert_eq!(complementarity_stats(&out).reported, code, "{reported:?}");
}
for (class, code) in [
(StationarityClass::S, POUNCE_CC_CLASS_S),
(StationarityClass::W, POUNCE_CC_CLASS_W),
(StationarityClass::None, POUNCE_CC_CLASS_NONE),
(StationarityClass::NotSearched, POUNCE_CC_CLASS_NOT_SEARCHED),
] {
out.certificate.as_mut().unwrap().class = class;
assert_eq!(
complementarity_stats(&out).certificate_class,
code,
"{class:?}"
);
}
}
#[test]
fn complementarity_encodings_b_verdicts_and_b_reason() {
use pounce_algorithm::mpcc::{BCheck, BVerdict};
let mut out = synthetic_outcome();
assert_eq!(
complementarity_stats(&out).b_verdict,
POUNCE_CC_B_VERDICT_ABSENT
);
assert_eq!(complementarity_text(&out, POUNCE_CC_TEXT_B_REASON), None);
for (verdict, code, reason) in [
(BVerdict::B, POUNCE_CC_B_VERDICT_B, None),
(
BVerdict::NotB {
held: vec![],
decrease: -1.0,
},
POUNCE_CC_B_VERDICT_NOT_B,
None,
),
(
BVerdict::NotChecked("a branch LP did not solve"),
POUNCE_CC_B_VERDICT_NOT_CHECKED,
Some("a branch LP did not solve"),
),
] {
out.b_check = Some(BCheck {
verdict,
n_biactive: 12,
branches_solved: 1,
enumerated: false,
});
let s = complementarity_stats(&out);
assert_eq!(s.b_verdict, code);
assert_eq!(
(s.b_n_biactive, s.b_branches_solved, s.b_enumerated),
(12, 1, 0)
);
assert_eq!(
complementarity_text(&out, POUNCE_CC_TEXT_B_REASON).as_deref(),
reason
);
}
}
#[test]
fn complementarity_encodings_polish_not_taken_has_a_reason() {
use pounce_restoration::mpcc_driver::PolishRecord;
let mut out = synthetic_outcome();
out.polish = Some(PolishRecord {
taken: false,
residual_before: 1e-5,
residual_after: Number::INFINITY,
reason: Some("the re-solve did not converge".to_string()),
});
let s = complementarity_stats(&out);
assert_eq!((s.polish_ran, s.polish_taken), (1, 0));
assert_eq!(s.polish_residual_before, 1e-5);
assert!(
s.polish_residual_after.is_nan(),
"unmeasured after reads NaN"
);
assert_eq!(
complementarity_text(&out, POUNCE_CC_TEXT_POLISH_REASON).as_deref(),
Some("the re-solve did not converge")
);
out.polish = Some(PolishRecord {
taken: true,
residual_before: 1e-5,
residual_after: 0.0,
reason: None,
});
assert_eq!(complementarity_stats(&out).polish_taken, 1);
assert_eq!(
complementarity_text(&out, POUNCE_CC_TEXT_POLISH_REASON),
None
);
assert_eq!(complementarity_text(&out, 3), None);
}
#[test]
fn complementarity_labels_truncate_and_terminate() {
let mut a = [0x7f as c_char; 4];
copy_label(&mut a, "abcdef");
assert_eq!(a, [b'a' as c_char, b'b' as c_char, b'c' as c_char, 0]);
copy_label(&mut a, "x");
assert_eq!(a, [b'x' as c_char, 0, 0, 0], "the tail is cleared");
use pounce_algorithm::mpcc::MpccStage::*;
for s in [Exact, Continuation, Penalty, Branch, Polish] {
assert!(s.label().len() < 32, "{}", s.label());
}
use pounce_algorithm::mpcc::Strategy;
for s in [Strategy::Both, Strategy::Exact, Strategy::Continuation] {
assert!(s.name().len() < 16, "{}", s.name());
}
}
#[test]
fn complements_row_reads_every_table_row_and_refuses_nan_and_crossed() {
use ComplementsRow::*;
let inf = 2e19;
assert_eq!(complements_row(0.0, 2.0), Some(Mixed));
assert_eq!(complements_row(-1.0, inf), Some(Lower));
assert_eq!(complements_row(-inf, 3.0), Some(Upper));
assert_eq!(complements_row(-inf, inf), Some(RowFixed));
assert_eq!(complements_row(1.0, 1.0), Some(VarFixed));
assert_eq!(complements_row(-1e19, 1e19), Some(RowFixed));
assert_eq!(complements_row(-9.9e18, 1e19), Some(Lower));
assert_eq!(complements_row(2.0, 1.0), None);
assert_eq!(complements_row(f64::NAN, 1.0), None);
assert_eq!(complements_row(0.0, f64::NAN), None);
}
#[test]
fn complements_edits_classify_as_the_native_shape_they_declare() {
let inf = 2e19;
for (xl, xu, want) in [
(0.0, 2.0, ComplementsRow::Mixed),
(-1.0, inf, ComplementsRow::Lower),
(-inf, 3.0, ComplementsRow::Upper),
] {
let row = complements_row(xl, xu).unwrap();
assert_eq!(row, want);
let (gl, gu, declares) = row.row_bounds(-inf, inf);
assert!(declares);
let shape = classify_pair(xl, xu, gl, gu);
assert!(row.shape_matches(shape), "{row:?}: {shape:?}");
}
assert_eq!(
ComplementsRow::RowFixed.row_bounds(-inf, inf),
(0.0, 0.0, false)
);
assert_eq!(
ComplementsRow::VarFixed.row_bounds(-inf, inf),
(-inf, inf, false)
);
assert!(!ComplementsRow::Mixed.shape_matches(None));
assert!(!ComplementsRow::Lower.shape_matches(classify_pair(0.0, 2.0, -inf, inf)));
}
fn create_two_by_two(x_l: [Number; 2], x_u: [Number; 2]) -> IpoptProblem {
let g_l = [-2e19, -2e19];
let g_u = [2e19, 2e19];
let p = unsafe {
CreateIpoptProblem(
2,
x_l.as_ptr(),
x_u.as_ptr(),
2,
g_l.as_ptr(),
g_u.as_ptr(),
0,
0,
0,
Some(quad_eval_f),
Some(cb_quad_eval_g),
Some(quad_eval_grad_f),
Some(cb_quad_eval_jac_g),
Some(cb_quad_eval_h),
)
};
assert!(!p.is_null());
p
}
fn assert_maps_match(p: IpoptProblem) {
let i = unsafe { &*p };
for (k, q) in i.complementarity.iter().enumerate() {
assert_eq!(i.complementarity.of_var(q.var), Some(k));
assert_eq!(i.complementarity.of_row(q.row), Some(k));
}
for (k, u) in i.complements_used.iter().enumerate() {
assert_eq!(i.complements_used.of_var(u.var), Some(k));
assert_eq!(i.complements_used.of_row(u.row), Some(k));
}
for j in 0..i.n.max(i.m) as usize {
assert_eq!(
i.complementarity.of_var(j).is_some(),
i.complementarity.iter().any(|q| q.var == j)
);
assert_eq!(
i.complementarity.of_row(j).is_some(),
i.complementarity.iter().any(|q| q.row == j)
);
assert_eq!(
i.complements_used.of_var(j).is_some(),
i.complements_used.iter().any(|u| u.var == j)
);
assert_eq!(
i.complements_used.of_row(j).is_some(),
i.complements_used.iter().any(|u| u.row == j)
);
}
}
#[test]
fn pair_maps_follow_every_declaration_path() {
let p = create_two_by_two([0.0, 0.0], [2.0, 2.0]);
unsafe {
assert_eq!(AddPounceComplementarityPair(p, 0, 0), POUNCE_OK);
assert_maps_match(p);
assert_eq!(
AddPounceComplementarityPair(p, 1, 0),
POUNCE_ERR_VAR_ALREADY_PAIRED
);
assert_eq!(
AddPounceComplementarityPair(p, 0, 1),
POUNCE_ERR_ROW_ALREADY_PAIRED
);
assert_maps_match(p);
let (rows, vars) = ([1, 0], [0, 1]);
assert_eq!(
SetPounceComplementarityPairs(
p,
2,
rows.as_ptr(),
vars.as_ptr(),
0,
std::ptr::null_mut()
),
POUNCE_OK
);
assert_maps_match(p);
assert_eq!((*p).complementarity.of_var(0), Some(0));
assert_eq!((*p).complementarity.of_row(1), Some(0));
let (rows, vars) = ([0, 0], [0, 1]);
assert_eq!(
SetPounceComplementarityPairs(
p,
2,
rows.as_ptr(),
vars.as_ptr(),
0,
std::ptr::null_mut()
),
POUNCE_ERR_ROW_ALREADY_PAIRED
);
assert_eq!((*p).complementarity.of_row(1), Some(0));
assert_maps_match(p);
assert_eq!(ClearPounceComplementarityPairs(p), TRUE);
assert_maps_match(p);
assert_eq!((&*p).complementarity.of_var(0), None);
assert_eq!(
AddPounceComplementsConstraint(p, 1, 1),
POUNCE_COMPLEMENTS_PAIR_MIXED
);
assert_maps_match(p);
assert_eq!(
AddPounceComplementsConstraint(p, 0, 1),
POUNCE_ERR_VAR_ALREADY_PAIRED
);
assert_maps_match(p);
FreeIpoptProblem(p);
}
}
#[test]
fn complements_records_the_table_row_and_the_callers_row_bounds() {
let p = create_two_by_two([0.0, -2e19], [2e19, 2e19]);
unsafe {
assert_eq!(
AddPounceComplementsConstraint(p, 0, 0),
POUNCE_COMPLEMENTS_PAIR_LOWER
);
assert_eq!(
AddPounceComplementsConstraint(p, 1, 1),
POUNCE_COMPLEMENTS_ROW_FIXED_NO_PAIR
);
let i = &*p;
assert_eq!(
i.complements_used[..],
[
ComplementsUse {
row: 0,
var: 0,
table: ComplementsRow::Lower,
user_gl: -2e19,
user_gu: 2e19
},
ComplementsUse {
row: 1,
var: 1,
table: ComplementsRow::RowFixed,
user_gl: -2e19,
user_gu: 2e19
},
]
);
assert_eq!((i.g_l[0], i.g_u[0]), (0.0, 2e19));
assert_eq!((i.g_l[1], i.g_u[1]), (0.0, 0.0));
FreeIpoptProblem(p);
}
}
}