use std::collections::{HashMap, HashSet};
use std::ffi::{c_int, c_uint};
use std::ops::Deref;
use pounce_common::types::{NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF, Number};
use pounce_nlp::mpcc::{ComplementarityPair, PairShape, classify_pair};
use crate::errors::*;
use crate::{
Index, IpoptProblem, IpoptProblemInfo, bound_status_consistent, complements_row,
cons_status_consistent, ffi_guard, zero_based,
};
#[derive(Debug, Clone, Default)]
pub(crate) struct DeclaredPairs {
list: Vec<ComplementarityPair>,
by_var: HashMap<usize, usize>,
by_row: HashMap<usize, usize>,
}
impl PartialEq for DeclaredPairs {
fn eq(&self, other: &Self) -> bool {
self.list == other.list
}
}
impl Deref for DeclaredPairs {
type Target = [ComplementarityPair];
fn deref(&self) -> &[ComplementarityPair] {
&self.list
}
}
impl DeclaredPairs {
pub(crate) fn replace(&mut self, list: Vec<ComplementarityPair>) {
self.by_var = list.iter().enumerate().map(|(k, p)| (p.var, k)).collect();
self.by_row = list.iter().enumerate().map(|(k, p)| (p.row, k)).collect();
debug_assert_eq!(self.by_var.len(), list.len(), "a variable in two pairs");
debug_assert_eq!(self.by_row.len(), list.len(), "a row in two pairs");
self.list = list;
}
pub(crate) fn push(&mut self, p: ComplementarityPair) {
debug_assert!(!self.by_var.contains_key(&p.var) && !self.by_row.contains_key(&p.row));
let k = self.list.len();
self.by_var.insert(p.var, k);
self.by_row.insert(p.row, k);
self.list.push(p);
}
pub(crate) fn clear(&mut self) {
self.list.clear();
self.by_var.clear();
self.by_row.clear();
}
pub(crate) fn of_var(&self, v: usize) -> Option<usize> {
self.by_var.get(&v).copied()
}
pub(crate) fn of_row(&self, r: usize) -> Option<usize> {
self.by_row.get(&r).copied()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ComplementsRow {
Mixed,
Lower,
Upper,
RowFixed,
VarFixed,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct ComplementsUse {
pub(crate) row: usize,
pub(crate) var: usize,
pub(crate) table: ComplementsRow,
pub(crate) user_gl: Number,
pub(crate) user_gu: Number,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ComplementsLedger {
list: Vec<ComplementsUse>,
by_var: HashMap<usize, usize>,
by_row: HashMap<usize, usize>,
}
impl PartialEq for ComplementsLedger {
fn eq(&self, other: &Self) -> bool {
self.list == other.list
}
}
impl Deref for ComplementsLedger {
type Target = [ComplementsUse];
fn deref(&self) -> &[ComplementsUse] {
&self.list
}
}
impl ComplementsLedger {
pub(crate) fn push(&mut self, u: ComplementsUse) {
debug_assert!(!self.by_var.contains_key(&u.var) && !self.by_row.contains_key(&u.row));
let k = self.list.len();
self.by_var.insert(u.var, k);
self.by_row.insert(u.row, k);
self.list.push(u);
}
pub(crate) fn of_var(&self, v: usize) -> Option<usize> {
self.by_var.get(&v).copied()
}
pub(crate) fn of_row(&self, r: usize) -> Option<usize> {
self.by_row.get(&r).copied()
}
pub(crate) fn set_user_row_bounds(&mut self, k: usize, gl: Number, gu: Number) {
self.list[k].user_gl = gl;
self.list[k].user_gu = gu;
}
}
const KNOWN_FLAGS: c_uint = 0;
pub const POUNCE_BOUNDS_VIEW_USER: c_int = 0;
pub const POUNCE_BOUNDS_VIEW_EFFECTIVE: c_int = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PairIdentity {
Sides { var_lower: bool, row_lower: bool },
Mixed,
}
fn identity(shape: PairShape) -> PairIdentity {
match shape {
PairShape::OneSided { g, h } | PairShape::OneSidedPlusBound { g, h } => {
PairIdentity::Sides {
var_lower: g.0 > 0.0,
row_lower: h.0 > 0.0,
}
}
PairShape::Mixed { .. } => PairIdentity::Mixed,
}
}
type Refusal = (c_int, Index);
struct Side {
entries: Vec<(usize, Number, Number)>,
at: HashMap<usize, usize>,
}
fn count_ok(count: Index, idx: *const Index, len: Index) -> bool {
(0..=len).contains(&count) && (!idx.is_null() || count == 0 || count == len)
}
#[allow(clippy::too_many_arguments)]
unsafe fn read_side(
count: Index,
idx: *const Index,
lo: *const Number,
hi: *const Number,
len: Index,
offset: Index,
base: Index,
cur: impl Fn(usize) -> (Number, Number),
) -> Result<Side, Refusal> {
if !count_ok(count, idx, len) {
return Err((POUNCE_ERR_COUNT, -1));
}
let k = count as usize;
let mut side = Side {
entries: Vec::with_capacity(k),
at: HashMap::with_capacity(k),
};
for p in 0..k {
let pos = base + p as Index;
let j = if idx.is_null() {
p
} else {
zero_based(unsafe { *idx.add(p) }, offset, len)
};
if j == usize::MAX {
return Err((POUNCE_ERR_OUT_OF_RANGE, pos));
}
if side.at.insert(j, p).is_some() {
return Err((POUNCE_ERR_DUPLICATE, pos));
}
let (cl, cu) = cur(j);
let l = if lo.is_null() {
cl
} else {
unsafe { *lo.add(p) }
};
let u = if hi.is_null() {
cu
} else {
unsafe { *hi.add(p) }
};
if l.is_nan() || u.is_nan() {
return Err((POUNCE_ERR_NAN, pos));
}
side.entries.push((j, l, u));
}
Ok(side)
}
fn user_row(info: &IpoptProblemInfo, r: usize) -> (Number, Number) {
match info.complements_used.of_row(r) {
Some(e) => {
let u = &info.complements_used[e];
(u.user_gl, u.user_gu)
}
None => (info.g_l[r], info.g_u[r]),
}
}
fn is_free(lo: Number, hi: Number) -> bool {
lo <= NLP_LOWER_BOUND_INF && hi >= NLP_UPPER_BOUND_INF
}
#[allow(clippy::too_many_arguments)]
unsafe fn set_bounds(
info: &mut IpoptProblemInfo,
nx: Index,
x_idx: *const Index,
x_l: *const Number,
x_u: *const Number,
ng: Index,
g_idx: *const Index,
g_l: *const Number,
g_u: *const Number,
) -> Result<(), Refusal> {
let offset = if info.index_style == 1 { 1 } else { 0 };
if !count_ok(nx, x_idx, info.n) || !count_ok(ng, g_idx, info.m) {
return Err((POUNCE_ERR_COUNT, -1));
}
let xs = unsafe {
read_side(nx, x_idx, x_l, x_u, info.n, offset, 0, |v| {
(info.x_l[v], info.x_u[v])
})?
};
let gs = unsafe {
read_side(ng, g_idx, g_l, g_u, info.m, offset, nx, |r| {
user_row(info, r)
})?
};
let var_new = |v: usize| match xs.at.get(&v) {
Some(&p) => (xs.entries[p].1, xs.entries[p].2),
None => (info.x_l[v], info.x_u[v]),
};
let row_new_effective = |r: usize| {
let user = match gs.at.get(&r) {
Some(&p) => (gs.entries[p].1, gs.entries[p].2),
None => user_row(info, r),
};
match info.complements_used.of_row(r) {
Some(e) => {
let (l, u, _) = info.complements_used[e].table.row_bounds(user.0, user.1);
(l, u)
}
None => user,
}
};
let mut pairs_checked: HashSet<usize> = HashSet::new();
let mut check_pair = |q: usize, pos: Index| -> Result<(), Refusal> {
if !pairs_checked.insert(q) {
return Ok(());
}
let pair = info.complementarity[q];
let (v, r) = (pair.var, pair.row);
let Some(old) = classify_pair(info.x_l[v], info.x_u[v], info.g_l[r], info.g_u[r]) else {
return Err((POUNCE_ERR_INTERNAL, -1));
};
let (xl, xu) = var_new(v);
let (gl, gu) = row_new_effective(r);
match classify_pair(xl, xu, gl, gu) {
Some(new) if identity(new) == identity(old) => Ok(()),
_ => Err((POUNCE_ERR_PAIR_CHANGED, pos)),
}
};
for (p, &(v, l, u)) in xs.entries.iter().enumerate() {
let pos = p as Index;
if let Some(e) = info.complements_used.of_var(v)
&& complements_row(l, u) != Some(info.complements_used[e].table)
{
return Err((POUNCE_ERR_COMPLEMENTS_CLASS, pos));
}
if let Some(q) = info.complementarity.of_var(v) {
check_pair(q, pos)?;
}
}
for (p, &(r, l, u)) in gs.entries.iter().enumerate() {
let pos = nx + p as Index;
if info.complements_used.of_row(r).is_some() && !is_free(l, u) {
return Err((POUNCE_ERR_ROW_NOT_FREE, pos));
}
if let Some(q) = info.complementarity.of_row(r) {
check_pair(q, pos)?;
}
}
if let Some(ws) = info.pending_working_set.as_ref() {
for (p, &(v, l, u)) in xs.entries.iter().enumerate() {
if !bound_status_consistent(ws.bounds[v], l, u) {
return Err((POUNCE_ERR_WORKING_SET, p as Index));
}
}
for (p, &(r, _, _)) in gs.entries.iter().enumerate() {
let (l, u) = row_new_effective(r);
if !cons_status_consistent(ws.constraints[r], l, u) {
return Err((POUNCE_ERR_WORKING_SET, nx + p as Index));
}
}
}
let effective_rows: Vec<(usize, Number, Number)> = gs
.entries
.iter()
.map(|&(r, _, _)| {
let (l, u) = row_new_effective(r);
(r, l, u)
})
.collect();
for &(v, l, u) in &xs.entries {
info.x_l[v] = l;
info.x_u[v] = u;
}
for (&(r, ul, uu), &(_, el, eu)) in gs.entries.iter().zip(&effective_rows) {
if let Some(e) = info.complements_used.of_row(r) {
info.complements_used.set_user_row_bounds(e, ul, uu);
}
info.g_l[r] = el;
info.g_u[r] = eu;
}
Ok(())
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn SetPounceBounds(
ipopt_problem: IpoptProblem,
nx: Index,
x_idx: *const Index,
x_L: *const Number,
x_U: *const Number,
ng: Index,
g_idx: *const Index,
g_L: *const Number,
g_U: *const Number,
flags: c_uint,
bad_index: *mut Index,
) -> c_int {
let report = |k: Index| {
if !bad_index.is_null() {
unsafe { *bad_index = k };
}
};
report(-1);
ffi_guard(POUNCE_ERR_INTERNAL, || unsafe {
if ipopt_problem.is_null() {
return POUNCE_ERR_NULL;
}
if flags & !KNOWN_FLAGS != 0 {
return POUNCE_ERR_FLAGS;
}
match set_bounds(
&mut *ipopt_problem,
nx,
x_idx,
x_L,
x_U,
ng,
g_idx,
g_L,
g_U,
) {
Ok(()) => POUNCE_OK,
Err((code, pos)) => {
report(pos);
code
}
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn GetPounceBounds(
ipopt_problem: IpoptProblem,
nx: Index,
x_idx: *const Index,
x_L: *mut Number,
x_U: *mut Number,
ng: Index,
g_idx: *const Index,
g_L: *mut Number,
g_U: *mut Number,
view: c_int,
flags: c_uint,
bad_index: *mut Index,
) -> c_int {
let report = |k: Index| {
if !bad_index.is_null() {
unsafe { *bad_index = k };
}
};
report(-1);
ffi_guard(POUNCE_ERR_INTERNAL, || unsafe {
if ipopt_problem.is_null() {
return POUNCE_ERR_NULL;
}
let info = &*ipopt_problem;
if flags & !KNOWN_FLAGS != 0 {
return POUNCE_ERR_FLAGS;
}
if view != POUNCE_BOUNDS_VIEW_USER && view != POUNCE_BOUNDS_VIEW_EFFECTIVE {
return POUNCE_ERR_ARGUMENT;
}
let offset = if info.index_style == 1 { 1 } else { 0 };
let null_ok = |count: Index, idx: *const Index, len: Index| {
count >= 0 && (!idx.is_null() || count == 0 || count == len)
};
if !null_ok(nx, x_idx, info.n) || !null_ok(ng, g_idx, info.m) {
return POUNCE_ERR_COUNT;
}
let resolve = |count: Index, idx: *const Index, len: Index, base: Index| {
(0..count as usize)
.map(|p| {
let j = if idx.is_null() {
p
} else {
zero_based(*idx.add(p), offset, len)
};
if j == usize::MAX {
Err((POUNCE_ERR_OUT_OF_RANGE, base + p as Index))
} else {
Ok(j)
}
})
.collect::<Result<Vec<usize>, Refusal>>()
};
let (vars, rows) = match resolve(nx, x_idx, info.n, 0)
.and_then(|v| resolve(ng, g_idx, info.m, nx).map(|r| (v, r)))
{
Ok(vr) => vr,
Err((code, pos)) => {
report(pos);
return code;
}
};
for (p, &v) in vars.iter().enumerate() {
if !x_L.is_null() {
*x_L.add(p) = info.x_l[v];
}
if !x_U.is_null() {
*x_U.add(p) = info.x_u[v];
}
}
for (p, &r) in rows.iter().enumerate() {
let (l, u) = if view == POUNCE_BOUNDS_VIEW_USER {
user_row(info, r)
} else {
(info.g_l[r], info.g_u[r])
};
if !g_L.is_null() {
*g_L.add(p) = l;
}
if !g_U.is_null() {
*g_U.add(p) = u;
}
}
POUNCE_OK
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AddPounceComplementarityPair, CreateIpoptProblem, FreeIpoptProblem};
unsafe extern "C" fn f(
_: Index,
_: *const Number,
_: crate::Bool,
_: *mut Number,
_: *mut std::ffi::c_void,
) -> crate::Bool {
0
}
unsafe extern "C" fn g(
_: Index,
_: *const Number,
_: crate::Bool,
_: Index,
_: *mut Number,
_: *mut std::ffi::c_void,
) -> crate::Bool {
0
}
#[allow(clippy::too_many_arguments)]
unsafe extern "C" fn jac(
_: Index,
_: *const Number,
_: crate::Bool,
_: Index,
_: Index,
_: *mut Index,
_: *mut Index,
_: *mut Number,
_: *mut std::ffi::c_void,
) -> crate::Bool {
0
}
#[test]
fn a_stored_pair_that_no_longer_classifies_is_an_internal_error() {
let (x_l, x_u) = ([0.0, 0.0], [2.0, 2.0]);
let (g_l, g_u) = ([-2e19, -2e19], [2e19, 2e19]);
unsafe {
let p = CreateIpoptProblem(
2,
x_l.as_ptr(),
x_u.as_ptr(),
2,
g_l.as_ptr(),
g_u.as_ptr(),
0,
0,
0,
Some(f),
Some(g),
Some(f_grad),
Some(jac),
None,
);
assert!(!p.is_null());
assert_eq!(AddPounceComplementarityPair(p, 0, 0), crate::POUNCE_OK);
(&mut (*p).x_u)[0] = 0.0;
let before = ((&*p).x_l.clone(), (&*p).x_u.clone(), (&*p).g_l.clone());
let (idx, lo, hi) = ([1], [-1.0], [3.0]);
let mut bad = -42;
let code = SetPounceBounds(
p,
1,
idx.as_ptr(),
lo.as_ptr(),
hi.as_ptr(),
1,
[0].as_ptr(),
[-2e19].as_ptr(),
[2e19].as_ptr(),
0,
&mut bad,
);
assert_eq!((code, bad), (POUNCE_ERR_INTERNAL, -1));
assert_eq!(
before,
((&*p).x_l.clone(), (&*p).x_u.clone(), (&*p).g_l.clone())
);
FreeIpoptProblem(p);
}
}
unsafe extern "C" fn f_grad(
_: Index,
_: *const Number,
_: crate::Bool,
_: *mut Number,
_: *mut std::ffi::c_void,
) -> crate::Bool {
0
}
}