use pounce_algorithm::application::IpoptApplication;
use pounce_common::types::Number;
use pounce_nlp::return_codes::ApplicationReturnStatus;
use pounce_nlp::tnlp::{
BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest, StartingPoint,
TNLP,
};
use std::cell::RefCell;
use std::rc::Rc;
struct DenseLp {
n: usize,
m: usize,
c: Vec<Number>,
a: Vec<Number>, g_l: Vec<Number>,
g_u: Vec<Number>,
x_l: Vec<Number>,
x_u: Vec<Number>,
x0: Vec<Number>,
final_obj: Option<Number>,
}
impl DenseLp {
fn a(&self, i: usize, j: usize) -> Number {
self.a[i * self.n + j]
}
}
impl TNLP for DenseLp {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: self.n as i32,
m: self.m as i32,
nnz_jac_g: (self.m * self.n) as i32,
nnz_h_lag: 0, index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&self.x_l);
b.x_u.copy_from_slice(&self.x_u);
b.g_l.copy_from_slice(&self.g_l);
b.g_u.copy_from_slice(&self.g_u);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&self.x0);
true
}
fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
Some((0..self.n).map(|j| self.c[j] * x[j]).sum())
}
fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g.copy_from_slice(&self.c);
true
}
fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
for i in 0..self.m {
g[i] = (0..self.n).map(|j| self.a(i, j) * x[j]).sum();
}
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
let mut k = 0;
for i in 0..self.m {
for j in 0..self.n {
irow[k] = i as i32;
jcol[k] = j as i32;
k += 1;
}
}
}
SparsityRequest::Values { values } => {
values.copy_from_slice(&self.a);
}
}
true
}
fn eval_h(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
_obj_factor: Number,
_lambda: Option<&[Number]>,
_new_lambda: bool,
_mode: SparsityRequest<'_>,
) -> bool {
true
}
fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
self.final_obj = Some(sol.obj_value);
}
}
fn solve(inst: Rc<RefCell<DenseLp>>, max_iter: i32) -> (ApplicationReturnStatus, Number) {
let mut app = IpoptApplication::new();
app.options_mut()
.set_integer_value("max_iter", max_iter, true, false)
.unwrap();
app.initialize().unwrap();
let tnlp: Rc<RefCell<dyn TNLP>> = inst;
let status = app.optimize_tnlp(tnlp);
let obj = app.statistics().final_objective;
(status, obj)
}
const INF: Number = 2e19;
#[test]
fn null_aeq_free_var_unbounded_reports_diverging() {
for &max_iter in &[300, 3000] {
let inst = Rc::new(RefCell::new(DenseLp {
n: 2,
m: 1,
c: vec![-1.0, 0.0],
a: vec![1.0, -1.0],
g_l: vec![0.0],
g_u: vec![0.0],
x_l: vec![-INF, -INF],
x_u: vec![INF, INF],
x0: vec![0.0, 0.0],
final_obj: None,
}));
let (status, obj) = solve(Rc::clone(&inst), max_iter);
assert!(
matches!(status, ApplicationReturnStatus::DivergingIterates),
"max_iter={max_iter}: a recession ray in null(A_eq) over free \
variables must report DivergingIterates (issue #285); got \
{status:?} obj={obj}",
);
}
}
#[test]
fn null_aeq_bounded_by_var_upper_is_optimal() {
let inst = Rc::new(RefCell::new(DenseLp {
n: 2,
m: 1,
c: vec![-1.0, 0.0],
a: vec![1.0, -1.0],
g_l: vec![0.0],
g_u: vec![0.0],
x_l: vec![-INF, -INF],
x_u: vec![10.0, INF],
x0: vec![0.0, 0.0],
final_obj: None,
}));
let (status, obj) = solve(Rc::clone(&inst), 3000);
assert!(
!matches!(status, ApplicationReturnStatus::DivergingIterates),
"a bounded variant (x0 ≤ 10) must not report DivergingIterates \
(issue #285); got {status:?} obj={obj}",
);
assert!(
(obj - (-10.0)).abs() < 1e-5,
"bounded variant objective {obj} is not the finite optimum -10",
);
}
#[test]
fn free_vars_with_pinning_equalities_is_optimal() {
let inst = Rc::new(RefCell::new(DenseLp {
n: 2,
m: 2,
c: vec![-1.0, 0.0],
a: vec![1.0, -1.0, 1.0, 1.0],
g_l: vec![0.0, 4.0],
g_u: vec![0.0, 4.0],
x_l: vec![-INF, -INF],
x_u: vec![INF, INF],
x0: vec![0.0, 0.0],
final_obj: None,
}));
let (status, obj) = solve(Rc::clone(&inst), 3000);
assert!(
!matches!(status, ApplicationReturnStatus::DivergingIterates),
"a bounded, fully-pinned variant must not report DivergingIterates \
(issue #285); got {status:?} obj={obj}",
);
assert!(
(obj - (-2.0)).abs() < 1e-5,
"pinned variant objective {obj} is not the finite optimum -2",
);
}
#[test]
fn free_var_capped_by_inequality_is_optimal() {
let inst = Rc::new(RefCell::new(DenseLp {
n: 1,
m: 1,
c: vec![-1.0],
a: vec![1.0],
g_l: vec![-INF],
g_u: vec![1e8],
x_l: vec![-INF],
x_u: vec![INF],
x0: vec![0.0],
final_obj: None,
}));
let (status, obj) = solve(Rc::clone(&inst), 3000);
assert!(
!matches!(status, ApplicationReturnStatus::DivergingIterates),
"a free variable capped by an inequality must not report \
DivergingIterates (issue #285); got {status:?} obj={obj}",
);
assert!(
(obj - (-1e8)).abs() / 1e8 < 1e-4,
"inequality-capped variant objective {obj} is not the finite optimum -1e8",
);
}