use std::cell::RefCell;
use std::rc::Rc;
use pounce_algorithm::application::IpoptApplication;
use pounce_common::types::Number;
use pounce_nlp::tnlp::{
BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, ScalingRequest, Solution, SparsityRequest,
StartingPoint, TNLP,
};
struct Skewed {
factors: Option<Vec<Number>>,
solution: Rc<RefCell<Option<Vec<Number>>>>,
bound_mult: Rc<RefCell<Option<(Vec<Number>, Vec<Number>)>>>,
}
impl Skewed {
fn new(factors: Option<Vec<Number>>) -> Self {
Self {
factors,
solution: Rc::new(RefCell::new(None)),
bound_mult: Rc::new(RefCell::new(None)),
}
}
}
impl TNLP for Skewed {
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.0e20, -1.0e20]);
b.x_u.copy_from_slice(&[1.0e20, 1.0e20]);
b.g_l[0] = 7.0;
b.g_u[0] = 7.0;
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
if sp.init_x {
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] - 3.0).powi(2) + (x[1] - 2.0e6).powi(2))
}
fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad_f: &mut [Number]) -> bool {
grad_f[0] = 2.0 * (x[0] - 3.0);
grad_f[1] = 2.0 * (x[1] - 2.0e6);
true
}
fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g[0] = x[0] + x[1] / 1.0e6;
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]);
true
}
SparsityRequest::Values { values } => {
values[0] = 1.0;
values[1] = 1.0e-6;
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, 1]);
jcol.copy_from_slice(&[0, 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<'_>, _d: &IpoptData, _c: &IpoptCq) {
*self.solution.borrow_mut() = Some(sol.x.to_vec());
*self.bound_mult.borrow_mut() = Some((sol.z_l.to_vec(), sol.z_u.to_vec()));
}
fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
match &self.factors {
Some(f) => {
*req.obj_scaling = 1.0;
*req.use_x_scaling = true;
req.x_scaling.copy_from_slice(f);
*req.use_g_scaling = false;
true
}
None => false,
}
}
}
fn solve(factors: Option<Vec<Number>>, user_scaling: bool) -> Option<Vec<Number>> {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
let _ = app
.options_mut()
.set_integer_value("print_level", 0, true, false);
if user_scaling {
let _ =
app.options_mut()
.set_string_value("nlp_scaling_method", "user-scaling", true, false);
}
let concrete = Rc::new(RefCell::new(Skewed::new(factors)));
let seen = concrete.borrow().solution.clone();
let tnlp: Rc<RefCell<dyn TNLP>> = concrete;
let _ = app.optimize_tnlp(tnlp);
let got = seen.borrow().clone();
got
}
#[test]
fn variable_factors_do_not_move_the_solution() {
let plain = solve(None, false).expect("unscaled solve finalizes");
let scaled = solve(Some(vec![1.0, 1.0e-6]), true).expect("scaled solve finalizes");
assert!(
(plain[0] - 5.0).abs() < 1e-4 && (plain[1] - 2.0e6).abs() < 1e2,
"unscaled solve landed at {plain:?}"
);
assert!(
(scaled[0] - plain[0]).abs() < 1e-4,
"x0: scaled {} vs unscaled {}",
scaled[0],
plain[0]
);
assert!(
(scaled[1] - plain[1]).abs() < 1e2,
"x1: scaled {} vs unscaled {}",
scaled[1],
plain[1]
);
}
#[test]
fn a_scaled_solve_is_no_longer_refused() {
assert!(
solve(Some(vec![1.0, 1.0e-6]), true).is_some(),
"the solve was refused before reaching finalize_solution"
);
}
#[test]
fn factors_are_ignored_under_other_scaling_methods() {
let with_factors_but_gradient_based =
solve(Some(vec![1.0, 1.0e-6]), false).expect("solve finalizes");
let plain = solve(None, false).expect("solve finalizes");
assert!(
(with_factors_but_gradient_based[0] - plain[0]).abs() < 1e-6,
"the default scaling method must not consult the factors"
);
}
#[test]
fn the_reported_factors_do_not_survive_into_the_next_solve() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
let _ = app
.options_mut()
.set_integer_value("print_level", 0, true, false);
let _ = app
.options_mut()
.set_string_value("nlp_scaling_method", "user-scaling", true, false);
let scaled: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Skewed::new(Some(vec![1.0, 1.0e-6]))));
let _ = app.optimize_tnlp(scaled);
assert_eq!(
app.variable_scaling(),
Some(vec![1.0, 1.0e-6]),
"the scaled solve should report the factors it applied"
);
let plain: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Skewed::new(None)));
let _ = app.optimize_tnlp(plain);
assert_eq!(
app.variable_scaling(),
None,
"a solve with no factors must not report the previous solve's"
);
}
#[test]
fn a_free_variable_gains_no_bound_multipliers_from_scaling() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
let _ = app
.options_mut()
.set_integer_value("print_level", 0, true, false);
let _ = app
.options_mut()
.set_string_value("nlp_scaling_method", "user-scaling", true, false);
let concrete = Rc::new(RefCell::new(Skewed::new(Some(vec![1.0, 1.0e-6]))));
let seen = concrete.borrow().bound_mult.clone();
let tnlp: Rc<RefCell<dyn TNLP>> = concrete;
let _ = app.optimize_tnlp(tnlp);
let (z_l, z_u) = seen.borrow().clone().expect("the solve finalizes");
assert!(
z_l.iter().chain(z_u.iter()).all(|v| v.abs() < 1e-8),
"free variables picked up bound multipliers: z_L = {z_l:?}, z_U = {z_u:?}"
);
}