use std::cell::RefCell;
use std::rc::Rc;
use pounce_algorithm::application::IpoptApplication;
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,
};
#[derive(Default, Clone)]
struct CapturedSolution {
x: Vec<Number>,
g: Vec<Number>,
lambda: Vec<Number>,
obj_value: Number,
}
struct EqOnly {
captured: Rc<RefCell<Option<CapturedSolution>>>,
}
impl EqOnly {
fn new() -> (Self, Rc<RefCell<Option<CapturedSolution>>>) {
let captured = Rc::new(RefCell::new(None));
(
Self {
captured: Rc::clone(&captured),
},
captured,
)
}
}
impl TNLP for EqOnly {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 2,
m: 1,
nnz_jac_g: 2,
nnz_h_lag: 2,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[-1.0e19, -1.0e19]);
b.x_u.copy_from_slice(&[1.0e19, 1.0e19]);
b.g_l[0] = 1.0;
b.g_u[0] = 1.0;
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&[0.0, 0.0]);
true
}
fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
Some(x[0] * x[0] + x[1] * x[1])
}
fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g[0] = 2.0 * x[0];
g[1] = 2.0 * x[1];
true
}
fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g[0] = x[0] + 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[0] = 0;
jcol[0] = 0;
irow[1] = 0;
jcol[1] = 1;
true
}
SparsityRequest::Values { values } => {
values[0] = 1.0;
values[1] = 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 {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow[0] = 0;
jcol[0] = 0;
irow[1] = 1;
jcol[1] = 1;
true
}
SparsityRequest::Values { values } => {
values[0] = 2.0 * obj_factor;
values[1] = 2.0 * obj_factor;
true
}
}
}
fn finalize_solution(&mut self, sol: Solution<'_>, _ip_data: &IpoptData, _ip_cq: &IpoptCq) {
*self.captured.borrow_mut() = Some(CapturedSolution {
x: sol.x.to_vec(),
g: sol.g.to_vec(),
lambda: sol.lambda.to_vec(),
obj_value: sol.obj_value,
});
}
}
fn build_app(l1_enabled: bool, rho: Number) -> IpoptApplication {
let mut app = IpoptApplication::new();
{
let opts = app.options_mut();
let _ = opts.set_string_value("sb", "yes", true, false);
let _ = opts.set_integer_value("print_level", 0, true, false);
let _ = opts.set_numeric_value("tol", 1e-10, true, false);
let _ = opts.set_integer_value("max_iter", 200, true, false);
if l1_enabled {
let _ = opts.set_string_value("l1_exact_penalty_barrier", "yes", true, false);
let _ = opts.set_numeric_value("l1_penalty_init", rho, true, false);
}
}
app.initialize().expect("initialize");
app
}
#[test]
fn flag_off_solves_eq_only_to_known_optimum() {
let (tnlp, captured) = EqOnly::new();
let tnlp_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(tnlp));
let mut app = build_app(false, 1.0);
let status = app.optimize_tnlp(Rc::clone(&tnlp_rc));
assert!(
matches!(
status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
),
"flag-off status = {:?}",
status
);
let cap = captured.borrow().clone().expect("finalize_solution called");
assert_eq!(cap.x.len(), 2, "flag-off x length");
assert!(
(cap.x[0] - 0.5).abs() < 1e-6,
"flag-off x[0] = {}",
cap.x[0]
);
assert!(
(cap.x[1] - 0.5).abs() < 1e-6,
"flag-off x[1] = {}",
cap.x[1]
);
assert!(
(cap.obj_value - 0.5).abs() < 1e-8,
"flag-off obj = {}",
cap.obj_value
);
eprintln!("flag-off captured lambda = {:?}", cap.lambda);
assert!(
cap.lambda.iter().any(|v| v.abs() > 0.1),
"post-#11: bare equality solve must report non-zero λ; got {:?}",
cap.lambda,
);
}
#[test]
fn flag_on_solution_x_truncated_to_n_orig() {
let (tnlp, captured) = EqOnly::new();
let tnlp_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(tnlp));
let mut app = build_app(true, 1.0);
let _ = app.optimize_tnlp(Rc::clone(&tnlp_rc));
let cap = captured.borrow().clone().expect("finalize_solution called");
assert_eq!(
cap.x.len(),
2,
"x must be truncated to n_orig (got {} entries)",
cap.x.len()
);
}
#[test]
fn flag_on_objective_excludes_penalty_term() {
let (tnlp, captured) = EqOnly::new();
let tnlp_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(tnlp));
let rho = 2.5; let mut app = build_app(true, rho);
let _ = app.optimize_tnlp(Rc::clone(&tnlp_rc));
let cap = captured.borrow().clone().expect("finalize_solution called");
assert!(
(cap.obj_value - 0.5).abs() < 1e-6,
"reported obj must be original f(x*) = 0.5, got {}",
cap.obj_value
);
}
#[test]
fn flag_on_constraint_value_excludes_slack_contribution() {
let (tnlp, captured) = EqOnly::new();
let tnlp_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(tnlp));
let mut app = build_app(true, 1.0);
let _ = app.optimize_tnlp(Rc::clone(&tnlp_rc));
let cap = captured.borrow().clone().expect("finalize_solution called");
assert_eq!(cap.g.len(), 1);
assert!(
(cap.g[0] - 1.0).abs() < 1e-3,
"reported g[0] = {} (expected ≈ 1.0; gap = {:.2e})",
cap.g[0],
(cap.g[0] - 1.0).abs()
);
}
#[test]
fn flag_on_lambda_length_and_passthrough() {
let (tnlp_off, captured_off) = EqOnly::new();
let tnlp_off_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(tnlp_off));
let mut app_off = build_app(false, 1.0);
let _ = app_off.optimize_tnlp(Rc::clone(&tnlp_off_rc));
let cap_off = captured_off.borrow().clone().expect("flag-off finalize");
let (tnlp_on, captured_on) = EqOnly::new();
let tnlp_on_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(tnlp_on));
let mut app_on = build_app(true, 1.0);
let _ = app_on.optimize_tnlp(Rc::clone(&tnlp_on_rc));
let cap_on = captured_on.borrow().clone().expect("flag-on finalize");
assert_eq!(
cap_on.lambda.len(),
1,
"wrapper must not add constraint rows"
);
assert_eq!(
cap_on.lambda.len(),
cap_off.lambda.len(),
"lambda length must match flag-off"
);
for i in 0..cap_on.lambda.len() {
assert!(
(cap_on.lambda[i] - cap_off.lambda[i]).abs() < 1e-6,
"lambda[{}] must pass through (flag-off {}, flag-on {})",
i,
cap_off.lambda[i],
cap_on.lambda[i],
);
}
}
#[test]
fn flag_on_no_op_when_no_equality_rows() {
struct IneqOnly {
captured: Rc<RefCell<Option<CapturedSolution>>>,
}
impl TNLP for IneqOnly {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 1,
m: 1,
nnz_jac_g: 1,
nnz_h_lag: 1,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l[0] = -1e19;
b.x_u[0] = 1e19;
b.g_l[0] = -1e19;
b.g_u[0] = 10.0;
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x[0] = 0.0;
true
}
fn eval_f(&mut self, x: &[Number], _: bool) -> Option<Number> {
Some((x[0] - 3.0).powi(2))
}
fn eval_grad_f(&mut self, x: &[Number], _: bool, g: &mut [Number]) -> bool {
g[0] = 2.0 * (x[0] - 3.0);
true
}
fn eval_g(&mut self, x: &[Number], _: bool, g: &mut [Number]) -> bool {
g[0] = x[0];
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow[0] = 0;
jcol[0] = 0;
true
}
SparsityRequest::Values { values } => {
values[0] = 1.0;
true
}
}
}
fn eval_h(
&mut self,
_x: Option<&[Number]>,
_: bool,
obj_factor: Number,
_lambda: Option<&[Number]>,
_: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow[0] = 0;
jcol[0] = 0;
true
}
SparsityRequest::Values { values } => {
values[0] = 2.0 * obj_factor;
true
}
}
}
fn finalize_solution(&mut self, sol: Solution<'_>, _: &IpoptData, _: &IpoptCq) {
*self.captured.borrow_mut() = Some(CapturedSolution {
x: sol.x.to_vec(),
g: sol.g.to_vec(),
lambda: sol.lambda.to_vec(),
obj_value: sol.obj_value,
});
}
}
let captured = Rc::new(RefCell::new(None));
let tnlp_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(IneqOnly {
captured: Rc::clone(&captured),
}));
let mut app = build_app(true, 1.0);
let _ = app.optimize_tnlp(Rc::clone(&tnlp_rc));
let cap = captured.borrow().clone().expect("finalize_solution called");
assert_eq!(cap.x.len(), 1);
assert!(
(cap.x[0] - 3.0).abs() < 1e-4,
"x* should be ~3, got {}",
cap.x[0]
);
}
struct BurkeHanLike {
captured: Rc<RefCell<Option<CapturedSolution>>>,
}
impl BurkeHanLike {
fn new() -> (Self, Rc<RefCell<Option<CapturedSolution>>>) {
let captured = Rc::new(RefCell::new(None));
(
Self {
captured: Rc::clone(&captured),
},
captured,
)
}
}
impl TNLP for BurkeHanLike {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 2,
m: 2,
nnz_jac_g: 4,
nnz_h_lag: 2,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[-1.0e19, -1.0e19]);
b.x_u.copy_from_slice(&[1.0e19, 1.0e19]);
b.g_l.copy_from_slice(&[1.0, 0.0]);
b.g_u.copy_from_slice(&[1.0, 0.0]);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&[0.5, 0.5]);
true
}
fn eval_f(&mut self, _x: &[Number], _: bool) -> Option<Number> {
Some(0.0)
}
fn eval_grad_f(&mut self, _x: &[Number], _: bool, g: &mut [Number]) -> bool {
g[0] = 0.0;
g[1] = 0.0;
true
}
fn eval_g(&mut self, x: &[Number], _: bool, g: &mut [Number]) -> bool {
g[0] = x[0] + x[1];
g[1] = x[0] * x[0] + x[1] * x[1];
true
}
fn eval_jac_g(&mut self, x: Option<&[Number]>, _: bool, mode: SparsityRequest<'_>) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow[0] = 0;
jcol[0] = 0;
irow[1] = 0;
jcol[1] = 1;
irow[2] = 1;
jcol[2] = 0;
irow[3] = 1;
jcol[3] = 1;
true
}
SparsityRequest::Values { values } => {
let x = x.expect("values call needs x");
values[0] = 1.0;
values[1] = 1.0;
values[2] = 2.0 * x[0];
values[3] = 2.0 * x[1];
true
}
}
}
fn eval_h(
&mut self,
_x: Option<&[Number]>,
_: bool,
_obj_factor: Number,
lambda: Option<&[Number]>,
_: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow[0] = 0;
jcol[0] = 0;
irow[1] = 1;
jcol[1] = 1;
true
}
SparsityRequest::Values { values } => {
let lam = lambda.expect("values call needs lambda");
values[0] = 2.0 * lam[1];
values[1] = 2.0 * lam[1];
true
}
}
}
fn finalize_solution(&mut self, sol: Solution<'_>, _: &IpoptData, _: &IpoptCq) {
*self.captured.borrow_mut() = Some(CapturedSolution {
x: sol.x.to_vec(),
g: sol.g.to_vec(),
lambda: sol.lambda.to_vec(),
obj_value: sol.obj_value,
});
}
}
#[test]
fn bnw_outer_loop_runs_to_completion() {
let (tnlp, captured) = EqOnly::new();
let tnlp_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(tnlp));
let mut app = build_app(true, 1.0);
let status = app.optimize_tnlp(Rc::clone(&tnlp_rc));
assert!(
matches!(
status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
),
"BNW outer-loop status = {:?}",
status
);
let cap = captured.borrow().clone().expect("finalize_solution called");
assert_eq!(cap.x.len(), 2);
assert!((cap.x[0] - 0.5).abs() < 1e-4);
assert!((cap.x[1] - 0.5).abs() < 1e-4);
assert!((cap.obj_value - 0.5).abs() < 1e-4);
}
#[test]
fn infeasible_problem_upgrades_to_infeasibility_detected() {
let (tnlp, _captured) = BurkeHanLike::new();
let tnlp_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(tnlp));
let mut app = build_app(true, 1.0);
{
let opts = app.options_mut();
let _ = opts.set_numeric_value("l1_penalty_max", 1.0e4, true, false);
let _ = opts.set_integer_value("l1_penalty_max_outer_iter", 5, true, false);
}
let status = app.optimize_tnlp(Rc::clone(&tnlp_rc));
assert!(
matches!(status, ApplicationReturnStatus::InfeasibleProblemDetected),
"expected InfeasibleProblemDetected, got {:?}",
status,
);
}
#[test]
fn flag_on_does_not_regress_well_conditioned_problem() {
let (tnlp_off, captured_off) = EqOnly::new();
let tnlp_off_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(tnlp_off));
let mut app_off = build_app(false, 1.0);
let _ = app_off.optimize_tnlp(Rc::clone(&tnlp_off_rc));
let cap_off = captured_off.borrow().clone().expect("off");
let (tnlp_on, captured_on) = EqOnly::new();
let tnlp_on_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(tnlp_on));
let mut app_on = build_app(true, 1.0);
let _ = app_on.optimize_tnlp(Rc::clone(&tnlp_on_rc));
let cap_on = captured_on.borrow().clone().expect("on");
for i in 0..cap_off.x.len() {
assert!(
(cap_off.x[i] - cap_on.x[i]).abs() < 1e-3,
"x[{}] differs: off {} vs on {}",
i,
cap_off.x[i],
cap_on.x[i]
);
}
assert!(
(cap_off.obj_value - cap_on.obj_value).abs() < 1e-3,
"obj differs: off {} vs on {}",
cap_off.obj_value,
cap_on.obj_value
);
}
fn build_app_with_fallback(rho_init: Number) -> IpoptApplication {
let mut app = IpoptApplication::new();
{
let opts = app.options_mut();
let _ = opts.set_string_value("sb", "yes", true, false);
let _ = opts.set_integer_value("print_level", 0, true, false);
let _ = opts.set_numeric_value("tol", 1e-10, true, false);
let _ = opts.set_integer_value("max_iter", 200, true, false);
let _ = opts.set_string_value("l1_fallback_on_restoration_failure", "yes", true, false);
let _ = opts.set_numeric_value("l1_penalty_init", rho_init, true, false);
let _ = opts.set_integer_value("l1_penalty_max_outer_iter", 5, true, false);
let _ = opts.set_numeric_value("l1_penalty_max", 1.0e4, true, false);
}
app.initialize().expect("initialize");
app
}
#[test]
fn auto_fallback_no_op_when_first_attempt_succeeds() {
let (tnlp_off, captured_off) = EqOnly::new();
let tnlp_off_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(tnlp_off));
let mut app_off = build_app(false, 1.0);
let status_off = app_off.optimize_tnlp(Rc::clone(&tnlp_off_rc));
let (tnlp_fb, captured_fb) = EqOnly::new();
let tnlp_fb_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(tnlp_fb));
let mut app_fb = build_app_with_fallback(1.0);
let status_fb = app_fb.optimize_tnlp(Rc::clone(&tnlp_fb_rc));
assert_eq!(
std::mem::discriminant(&status_off),
std::mem::discriminant(&status_fb),
"fallback should not change status on a success path: off {:?} vs fb {:?}",
status_off,
status_fb
);
let cap_off = captured_off.borrow().clone().expect("off finalize");
let cap_fb = captured_fb.borrow().clone().expect("fb finalize");
for i in 0..cap_off.x.len() {
assert!(
(cap_off.x[i] - cap_fb.x[i]).abs() < 1e-6,
"x[{}] should be identical: off {} vs fb {}",
i,
cap_off.x[i],
cap_fb.x[i]
);
}
}
#[test]
fn auto_fallback_preserves_status_on_truly_infeasible_problem() {
let (tnlp, _captured) = BurkeHanLike::new();
let tnlp_rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(tnlp));
let mut app = build_app_with_fallback(1.0);
let status = app.optimize_tnlp(Rc::clone(&tnlp_rc));
assert!(
!matches!(status, ApplicationReturnStatus::SolveSucceeded),
"fallback must not promote when retry didn't succeed; got {:?}",
status,
);
}
#[allow(dead_code)]
fn _index_marker(_i: Index) {}