use pounce_algorithm::application::IpoptApplication;
use pounce_common::types::Number;
use pounce_nlp::tnlp::{
BoundsInfo, IndexStyle, IpoptCq, IpoptData, Linearity, NlpInfo, Solution, SparsityRequest,
StartingPoint, TNLP,
};
use pounce_presolve::{LinearEqElimTnlp, PresolveOptions, PresolveTnlp};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Clone, Default)]
struct Captured {
x: Vec<Number>,
z_l: Vec<Number>,
z_u: Vec<Number>,
lambda: Vec<Number>,
}
struct Fixture {
alpha: Number,
x1_lo: Number,
x1_hi: Number,
captured: Option<Captured>,
}
impl Fixture {
fn new(alpha: Number, x1_lo: Number, x1_hi: Number) -> Self {
Self {
alpha,
x1_lo,
x1_hi,
captured: None,
}
}
}
impl TNLP for Fixture {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 2,
m: 1,
nnz_jac_g: 2,
nnz_h_lag: 1,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[-1e19, self.x1_lo]);
b.x_u.copy_from_slice(&[1.0, self.x1_hi]);
b.g_l.copy_from_slice(&[0.0]);
b.g_u.copy_from_slice(&[0.0]);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&[0.0, 0.0]);
true
}
fn get_constraints_linearity(&mut self, types: &mut [Linearity]) -> bool {
types.copy_from_slice(&[Linearity::Linear]);
true
}
fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
Some((x[0] - 4.0).powi(2))
}
fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g[0] = 2.0 * (x[0] - 4.0);
g[1] = 0.0;
true
}
fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g[0] = x[0] - self.alpha * x[1];
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow.copy_from_slice(&[0, 0]);
jcol.copy_from_slice(&[0, 1]);
}
SparsityRequest::Values { values } => {
values[0] = 1.0;
values[1] = -self.alpha;
}
}
true
}
fn eval_h(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
obj_factor: Number,
_lambda: Option<&[Number]>,
_new_lambda: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow.copy_from_slice(&[0]);
jcol.copy_from_slice(&[0]);
}
SparsityRequest::Values { values } => {
values[0] = obj_factor * 2.0;
}
}
true
}
fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
self.captured = Some(Captured {
x: sol.x.to_vec(),
z_l: sol.z_l.to_vec(),
z_u: sol.z_u.to_vec(),
lambda: sol.lambda.to_vec(),
});
}
}
fn opts(linear_eq_reduction: bool) -> PresolveOptions {
PresolveOptions {
enabled: true,
linear_eq_reduction,
bound_tightening: false,
redundant_constraint_removal: false,
licq_check: false,
warm_z_bounds: false,
..PresolveOptions::defaults()
}
}
struct Outcome {
captured: Captured,
reduced_n: i32,
}
fn solve(alpha: Number, x1_lo: Number, x1_hi: Number, linear_eq_reduction: bool) -> Outcome {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.options_mut()
.set_string_value("nlp_scaling_method", "none", true, false)
.unwrap();
let concrete = Rc::new(RefCell::new(Fixture::new(alpha, x1_lo, x1_hi)));
let presolve = Rc::new(RefCell::new(PresolveTnlp::new(
Rc::clone(&concrete) as Rc<RefCell<dyn TNLP>>,
opts(linear_eq_reduction),
)));
let elim = Rc::new(RefCell::new(LinearEqElimTnlp::new(
Rc::clone(&presolve) as Rc<RefCell<dyn TNLP>>,
opts(linear_eq_reduction),
)));
let info = elim.borrow_mut().get_nlp_info().expect("dims");
let _ = app.optimize_tnlp(Rc::clone(&elim) as Rc<RefCell<dyn TNLP>>);
Outcome {
captured: concrete.borrow().captured.clone().expect("finalized"),
reduced_n: info.n,
}
}
fn stationarity(alpha: Number, c: &Captured) -> [Number; 2] {
let grad = [2.0 * (c.x[0] - 4.0), 0.0];
let jac = [1.0, -alpha];
[0, 1].map(|j| grad[j] + jac[j] * c.lambda[0] - c.z_l[j] + c.z_u[j])
}
#[test]
fn a_transferred_upper_bounds_multiplier_lands_on_the_column_that_owns_it() {
let on = solve(2.0, -100.0, 100.0, true);
assert_eq!(on.reduced_n, 1, "x0 should be gone");
let off = solve(2.0, -100.0, 100.0, false);
assert!((on.captured.x[0] - 1.0).abs() < 1e-6, "{:?}", on.captured.x);
assert!((on.captured.x[1] - 0.5).abs() < 1e-6, "{:?}", on.captured.x);
assert!(
(on.captured.z_u[0] - 6.0).abs() < 1e-4,
"z_u[0] = {}, expected 6 (the bare solve reports {})",
on.captured.z_u[0],
off.captured.z_u[0]
);
assert!(
on.captured.z_u[1].abs() < 1e-4,
"x1 never declared that bound: z_u[1] = {}",
on.captured.z_u[1]
);
assert!(
(on.captured.lambda[0] - off.captured.lambda[0]).abs() < 1e-4,
"λ = {} vs bare {}",
on.captured.lambda[0],
off.captured.lambda[0]
);
for j in 0..2 {
assert!((on.captured.z_u[j] - off.captured.z_u[j]).abs() < 1e-4);
assert!((on.captured.z_l[j] - off.captured.z_l[j]).abs() < 1e-4);
}
}
#[test]
fn a_negative_coefficient_sends_the_multiplier_to_the_other_side_of_the_box() {
let on = solve(-2.0, -100.0, 100.0, true);
assert_eq!(on.reduced_n, 1);
let off = solve(-2.0, -100.0, 100.0, false);
assert!((on.captured.x[0] - 1.0).abs() < 1e-6, "{:?}", on.captured.x);
assert!((on.captured.x[1] + 0.5).abs() < 1e-6, "{:?}", on.captured.x);
assert!(
(on.captured.z_u[0] - 6.0).abs() < 1e-4,
"z_u[0] = {}, expected 6 (the bare solve reports {})",
on.captured.z_u[0],
off.captured.z_u[0]
);
assert!(
on.captured.z_l[0].abs() < 1e-4,
"x0 has no lower bound to be active: z_l[0] = {}",
on.captured.z_l[0]
);
assert!(on.captured.z_l[1].abs() < 1e-4 && on.captured.z_u[1].abs() < 1e-4);
assert!((on.captured.lambda[0] - off.captured.lambda[0]).abs() < 1e-4);
}
#[test]
fn both_signs_close_full_space_stationarity() {
for alpha in [2.0, -2.0, 0.5, -0.5, 3.0] {
let on = solve(alpha, -100.0, 100.0, true);
for (j, r) in stationarity(alpha, &on.captured).iter().enumerate() {
assert!(
r.abs() < 1e-5,
"α = {alpha}: stationarity at column {j} = {r}, duals = {:?}",
on.captured
);
}
for j in 0..2 {
assert!(on.captured.z_l[j] >= -1e-8 && on.captured.z_u[j] >= -1e-8);
}
}
}
#[test]
fn the_degenerate_both_active_case_is_still_a_valid_kkt_point() {
let alpha = 2.0;
let on = solve(alpha, -100.0, 0.5, true);
assert_eq!(on.reduced_n, 1);
let c = &on.captured;
assert!((c.x[0] - 1.0).abs() < 1e-6, "{:?}", c.x);
assert!((c.x[1] - 0.5).abs() < 1e-6, "{:?}", c.x);
for (j, r) in stationarity(alpha, c).iter().enumerate() {
assert!(
r.abs() < 1e-5,
"stationarity at column {j} = {r}, duals = {c:?}"
);
}
for j in 0..2 {
assert!(c.z_l[j] >= -1e-8, "z_l[{j}] = {} is negative", c.z_l[j]);
assert!(c.z_u[j] >= -1e-8, "z_u[{j}] = {} is negative", c.z_u[j]);
assert!(c.z_l[j] < 1e-4, "z_l[{j}] on an inactive bound");
}
assert!(
c.z_u[1] > 1e-3,
"expected the survivor to keep it: z_u = {:?}",
c.z_u
);
}