use pounce_algorithm::application::IpoptApplication;
use pounce_algorithm::sqp::{classify_working_set, SqpIterates};
use pounce_common::types::{Index, Number};
use pounce_nlp::return_codes::ApplicationReturnStatus;
use pounce_nlp::tnlp::{
BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest, StartingPoint,
TNLP,
};
struct SimplexProj {
p: std::rc::Rc<std::cell::RefCell<Vec<Number>>>,
finalize_sink: std::rc::Rc<std::cell::RefCell<Option<FinalizedIterate>>>,
}
#[derive(Clone)]
struct FinalizedIterate {
x: Vec<Number>,
z_l: Vec<Number>,
z_u: Vec<Number>,
lambda: Vec<Number>,
g: Vec<Number>,
}
impl SimplexProj {
fn new(p: Vec<Number>) -> Self {
Self {
p: std::rc::Rc::new(std::cell::RefCell::new(p)),
finalize_sink: std::rc::Rc::new(std::cell::RefCell::new(None)),
}
}
fn n(&self) -> usize {
self.p.borrow().len()
}
}
impl TNLP for SimplexProj {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
let n = self.n() as Index;
Some(NlpInfo {
n,
m: 1,
nnz_jac_g: n,
nnz_h_lag: n,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
for i in 0..self.n() {
b.x_l[i] = 0.0;
b.x_u[i] = 1.0e20;
}
b.g_l[0] = 1.0;
b.g_u[0] = 1.0;
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
let inv = 1.0 / self.n() as Number;
for v in sp.x.iter_mut() {
*v = inv;
}
true
}
fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
let p = self.p.borrow();
Some(
0.5 * x
.iter()
.zip(p.iter())
.map(|(xi, pi)| (xi - pi) * (xi - pi))
.sum::<Number>(),
)
}
fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
let p = self.p.borrow();
for (g, (xi, pi)) in grad.iter_mut().zip(x.iter().zip(p.iter())) {
*g = xi - pi;
}
true
}
fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g[0] = x.iter().sum();
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
mode: SparsityRequest<'_>,
) -> bool {
let n = self.n();
match mode {
SparsityRequest::Structure { irow, jcol } => {
for i in 0..n {
irow[i] = 0;
jcol[i] = i as Index;
}
}
SparsityRequest::Values { values, .. } => {
for v in values.iter_mut() {
*v = 1.0;
}
}
}
true
}
fn eval_h(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
obj_factor: Number,
_lambda: Option<&[Number]>,
_new_lambda: bool,
mode: SparsityRequest<'_>,
) -> bool {
let n = self.n();
match mode {
SparsityRequest::Structure { irow, jcol } => {
for i in 0..n {
irow[i] = i as Index;
jcol[i] = i as Index;
}
}
SparsityRequest::Values { values, .. } => {
for v in values.iter_mut() {
*v = obj_factor;
}
}
}
true
}
fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
*self.finalize_sink.borrow_mut() = Some(FinalizedIterate {
x: sol.x.to_vec(),
z_l: sol.z_l.to_vec(),
z_u: sol.z_u.to_vec(),
lambda: sol.lambda.to_vec(),
g: sol.g.to_vec(),
});
}
}
#[test]
fn ipm_then_sqp_corrector_recovers_exact_perturbed_optimum() {
let n = 3;
let p0 = vec![0.5, 0.4, -0.1];
let tnlp = std::rc::Rc::new(std::cell::RefCell::new(SimplexProj::new(p0.clone())));
let p_handle = tnlp.borrow().p.clone();
let finalize_handle = tnlp.borrow().finalize_sink.clone();
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("print_level 0\n").unwrap();
let status = app.optimize_tnlp(tnlp.clone());
assert_eq!(status, ApplicationReturnStatus::SolveSucceeded);
let ipm_iter = finalize_handle.borrow().clone().unwrap();
assert!((ipm_iter.x[0] - 0.55).abs() < 1e-6);
assert!((ipm_iter.x[1] - 0.45).abs() < 1e-6);
assert!(ipm_iter.x[2].abs() < 1e-6);
let lambda_x: Vec<Number> = ipm_iter
.z_l
.iter()
.zip(ipm_iter.z_u.iter())
.map(|(l, u)| l - u)
.collect();
let x_l = vec![0.0; n];
let x_u = vec![1.0e20; n];
let g_l = vec![1.0];
let g_u = vec![1.0];
let ws = classify_working_set(
&lambda_x,
&ipm_iter.lambda,
1, &ipm_iter.x,
&x_l,
&x_u,
&ipm_iter.g,
&g_l,
&g_u,
1e-8,
1e-6,
);
use pounce_qp::{BoundStatus, ConsStatus};
assert_eq!(ws.bounds[0], BoundStatus::Inactive);
assert_eq!(ws.bounds[1], BoundStatus::Inactive);
assert_eq!(ws.bounds[2], BoundStatus::AtLower);
assert_eq!(ws.constraints[0], ConsStatus::Equality);
let dp = vec![0.02, -0.01, 0.05];
*p_handle.borrow_mut() = p0.iter().zip(dp.iter()).map(|(a, b)| a + b).collect();
let p_new = p_handle.borrow().clone();
let lambda_eq = (p_new[0] + p_new[1] - 1.0) / 2.0;
let x_predictor = vec![p_new[0] - lambda_eq, p_new[1] - lambda_eq, 0.0];
let warm = SqpIterates {
x: x_predictor.clone(),
lambda_g: vec![-lambda_eq], lambda_x: lambda_x.clone(),
working: Some(ws),
};
app.set_sqp_warm_start(warm);
app.options_mut()
.set_string_value("algorithm", "active-set-sqp", true, false)
.unwrap();
let status2 = app.optimize_tnlp(tnlp.clone());
assert_eq!(status2, ApplicationReturnStatus::SolveSucceeded);
let sqp_iter = finalize_handle.borrow().clone().unwrap();
assert!(
(sqp_iter.x[0] - x_predictor[0]).abs() < 1e-8,
"x[0]: corrector={}, expected={}",
sqp_iter.x[0],
x_predictor[0]
);
assert!(
(sqp_iter.x[1] - x_predictor[1]).abs() < 1e-8,
"x[1]: corrector={}, expected={}",
sqp_iter.x[1],
x_predictor[1]
);
assert!(sqp_iter.x[2].abs() < 1e-8);
assert!(app.last_sqp_working_set().is_some());
}
#[test]
fn sqp_corrector_handles_active_set_flip_between_perturbations() {
let p0 = vec![0.5, 0.4, -0.1];
let tnlp = std::rc::Rc::new(std::cell::RefCell::new(SimplexProj::new(p0.clone())));
let p_handle = tnlp.borrow().p.clone();
let finalize_handle = tnlp.borrow().finalize_sink.clone();
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("print_level 0\n").unwrap();
let status = app.optimize_tnlp(tnlp.clone());
assert_eq!(status, ApplicationReturnStatus::SolveSucceeded);
let ipm = finalize_handle.borrow().clone().unwrap();
let n = 3;
let lambda_x: Vec<Number> = ipm
.z_l
.iter()
.zip(ipm.z_u.iter())
.map(|(l, u)| l - u)
.collect();
let x_l = vec![0.0; n];
let x_u = vec![1.0e20; n];
let g_l = vec![1.0];
let g_u = vec![1.0];
let ws = classify_working_set(
&lambda_x,
&ipm.lambda,
1,
&ipm.x,
&x_l,
&x_u,
&ipm.g,
&g_l,
&g_u,
1e-8,
1e-6,
);
use pounce_qp::BoundStatus;
assert_eq!(
ws.bounds[2],
BoundStatus::AtLower,
"WS must capture x₃ active"
);
*p_handle.borrow_mut() = vec![0.5, 0.4, 0.1];
let warm = SqpIterates {
x: ipm.x.clone(),
lambda_g: ipm.lambda.clone(),
lambda_x: lambda_x.clone(),
working: Some(ws),
};
app.set_sqp_warm_start(warm);
app.options_mut()
.set_string_value("algorithm", "active-set-sqp", true, false)
.unwrap();
let status2 = app.optimize_tnlp(tnlp.clone());
assert_eq!(status2, ApplicationReturnStatus::SolveSucceeded);
let sqp = finalize_handle.borrow().clone().unwrap();
assert!((sqp.x[0] - 0.5).abs() < 1e-7, "x[0] = {}", sqp.x[0]);
assert!((sqp.x[1] - 0.4).abs() < 1e-7, "x[1] = {}", sqp.x[1]);
assert!((sqp.x[2] - 0.1).abs() < 1e-7, "x[2] = {}", sqp.x[2]);
let new_ws = app.last_sqp_working_set().expect("ws produced");
assert_eq!(new_ws.bounds[2], BoundStatus::Inactive);
}