use crate::tnlp::{BoundsInfo, NlpInfo, TNLP};
use pounce_common::exception::{ExceptionKind, SolverException};
use pounce_common::types::{Index, Number};
use std::cell::RefCell;
use std::rc::Rc;
pub const DEFAULT_NLP_LOWER_BOUND_INF: Number = -1.0e19;
pub const DEFAULT_NLP_UPPER_BOUND_INF: Number = 1.0e19;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FixedVarTreatment {
MakeParameter,
RelaxBounds,
}
impl Default for FixedVarTreatment {
fn default() -> Self {
Self::MakeParameter
}
}
#[derive(Debug, Clone)]
pub struct BoundClassification {
pub n_full_x: Index,
pub n_full_g: Index,
pub n_x_fixed: Index,
pub x_not_fixed_map: Vec<Index>,
pub x_fixed_map: Vec<Index>,
pub x_fixed_vals: Vec<Number>,
pub full_to_var: Vec<Index>,
pub x_l_map: Vec<Index>,
pub x_u_map: Vec<Index>,
pub n_c: Index,
pub c_map: Vec<Index>,
pub n_d: Index,
pub d_map: Vec<Index>,
pub d_l_map: Vec<Index>,
pub d_u_map: Vec<Index>,
}
impl BoundClassification {
pub fn n_x_var(&self) -> Index {
self.x_not_fixed_map.len() as Index
}
pub fn n_x_l(&self) -> Index {
self.x_l_map.len() as Index
}
pub fn n_x_u(&self) -> Index {
self.x_u_map.len() as Index
}
pub fn n_d_l(&self) -> Index {
self.d_l_map.len() as Index
}
pub fn n_d_u(&self) -> Index {
self.d_u_map.len() as Index
}
}
pub struct TNLPAdapter {
tnlp: Rc<RefCell<dyn TNLP>>,
info: NlpInfo,
classification: BoundClassification,
nlp_lower_bound_inf: Number,
nlp_upper_bound_inf: Number,
}
impl std::fmt::Debug for TNLPAdapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TNLPAdapter")
.field("info", &self.info)
.field("classification", &self.classification)
.field("nlp_lower_bound_inf", &self.nlp_lower_bound_inf)
.field("nlp_upper_bound_inf", &self.nlp_upper_bound_inf)
.finish_non_exhaustive()
}
}
impl TNLPAdapter {
pub fn new(tnlp: Rc<RefCell<dyn TNLP>>) -> Result<Self, SolverException> {
Self::new_with_options(
tnlp,
DEFAULT_NLP_LOWER_BOUND_INF,
DEFAULT_NLP_UPPER_BOUND_INF,
FixedVarTreatment::MakeParameter,
)
}
pub fn new_with_inf(
tnlp: Rc<RefCell<dyn TNLP>>,
nlp_lower_bound_inf: Number,
nlp_upper_bound_inf: Number,
) -> Result<Self, SolverException> {
Self::new_with_options(
tnlp,
nlp_lower_bound_inf,
nlp_upper_bound_inf,
FixedVarTreatment::MakeParameter,
)
}
pub fn new_with_options(
tnlp: Rc<RefCell<dyn TNLP>>,
nlp_lower_bound_inf: Number,
nlp_upper_bound_inf: Number,
fixed_var_treatment: FixedVarTreatment,
) -> Result<Self, SolverException> {
if nlp_lower_bound_inf >= nlp_upper_bound_inf {
return Err(SolverException::new(
ExceptionKind::OPTION_INVALID,
"Option \"nlp_lower_bound_inf\" must be smaller than \
\"nlp_upper_bound_inf\".",
file!(),
line!() as Index,
));
}
let info = {
let mut t = tnlp.borrow_mut();
t.get_nlp_info().ok_or_else(|| {
SolverException::new(
ExceptionKind::INVALID_TNLP,
"TNLP::get_nlp_info returned None.",
file!(),
line!() as Index,
)
})?
};
if info.n <= 0 {
return Err(SolverException::new(
ExceptionKind::INVALID_TNLP,
format!("TNLP::get_nlp_info reported n = {} (must be > 0).", info.n),
file!(),
line!() as Index,
));
}
if info.m < 0 {
return Err(SolverException::new(
ExceptionKind::INVALID_TNLP,
format!("TNLP::get_nlp_info reported m = {} (must be ≥ 0).", info.m),
file!(),
line!() as Index,
));
}
let n_full_x = info.n;
let n_full_g = info.m;
let mut x_l = vec![0.0; n_full_x as usize];
let mut x_u = vec![0.0; n_full_x as usize];
let mut g_l = vec![0.0; n_full_g as usize];
let mut g_u = vec![0.0; n_full_g as usize];
{
let mut t = tnlp.borrow_mut();
let ok = t.get_bounds_info(BoundsInfo {
x_l: &mut x_l,
x_u: &mut x_u,
g_l: &mut g_l,
g_u: &mut g_u,
});
if !ok {
return Err(SolverException::new(
ExceptionKind::INVALID_TNLP,
"TNLP::get_bounds_info returned false.",
file!(),
line!() as Index,
));
}
}
let mut treatment = fixed_var_treatment;
let mut classification = classify_bounds(
n_full_x,
n_full_g,
&x_l,
&x_u,
&g_l,
&g_u,
nlp_lower_bound_inf,
nlp_upper_bound_inf,
treatment,
)?;
if treatment == FixedVarTreatment::MakeParameter
&& classification.n_x_fixed > 0
&& classification.n_x_var() > 0
&& classification.n_x_var() < classification.n_c
{
treatment = FixedVarTreatment::RelaxBounds;
classification = classify_bounds(
n_full_x,
n_full_g,
&x_l,
&x_u,
&g_l,
&g_u,
nlp_lower_bound_inf,
nlp_upper_bound_inf,
treatment,
)?;
}
Ok(Self {
tnlp,
info,
classification,
nlp_lower_bound_inf,
nlp_upper_bound_inf,
})
}
pub fn nlp_info(&self) -> &NlpInfo {
&self.info
}
pub fn classification(&self) -> &BoundClassification {
&self.classification
}
pub fn nlp_lower_bound_inf(&self) -> Number {
self.nlp_lower_bound_inf
}
pub fn nlp_upper_bound_inf(&self) -> Number {
self.nlp_upper_bound_inf
}
pub fn tnlp(&self) -> &Rc<RefCell<dyn TNLP>> {
&self.tnlp
}
}
#[allow(clippy::too_many_arguments)]
fn classify_bounds(
n_full_x: Index,
n_full_g: Index,
x_l: &[Number],
x_u: &[Number],
g_l: &[Number],
g_u: &[Number],
lo_inf: Number,
up_inf: Number,
treatment: FixedVarTreatment,
) -> Result<BoundClassification, SolverException> {
let nx = n_full_x as usize;
let ng = n_full_g as usize;
let mut x_not_fixed_map: Vec<Index> = Vec::with_capacity(nx);
let mut x_fixed_map: Vec<Index> = Vec::new();
let mut x_fixed_vals: Vec<Number> = Vec::new();
let mut full_to_var: Vec<Index> = vec![-1; nx];
let mut x_l_map: Vec<Index> = Vec::new();
let mut x_u_map: Vec<Index> = Vec::new();
let mut n_x_fixed: Index = 0;
for i in 0..nx {
let lo = x_l[i];
let hi = x_u[i];
if lo > hi {
return Err(SolverException::new(
ExceptionKind::INCONSISTENT_BOUNDS,
format!(
"There are inconsistent bounds on variable {i}: lower = {lo:25.16e} \
and upper = {hi:25.16e}."
),
file!(),
line!() as Index,
));
}
if lo == hi {
match treatment {
FixedVarTreatment::MakeParameter => {
n_x_fixed += 1;
x_fixed_map.push(i as Index);
x_fixed_vals.push(lo);
continue;
}
FixedVarTreatment::RelaxBounds => {
let var_idx = x_not_fixed_map.len() as Index;
x_not_fixed_map.push(i as Index);
full_to_var[i] = var_idx;
x_l_map.push(var_idx);
x_u_map.push(var_idx);
continue;
}
}
}
let var_idx = x_not_fixed_map.len() as Index;
x_not_fixed_map.push(i as Index);
full_to_var[i] = var_idx;
if lo > lo_inf {
x_l_map.push(var_idx);
}
if hi < up_inf {
x_u_map.push(var_idx);
}
}
let mut c_map: Vec<Index> = Vec::new();
let mut d_map: Vec<Index> = Vec::new();
let mut d_l_map: Vec<Index> = Vec::new();
let mut d_u_map: Vec<Index> = Vec::new();
for i in 0..ng {
let lo = g_l[i];
let hi = g_u[i];
if lo == hi {
c_map.push(i as Index);
} else if lo > hi {
return Err(SolverException::new(
ExceptionKind::INCONSISTENT_BOUNDS,
format!(
"There are inconsistent bounds on constraint function {i}: \
lower = {lo:25.16e} and upper = {hi:25.16e}."
),
file!(),
line!() as Index,
));
} else {
let d_idx = d_map.len() as Index;
d_map.push(i as Index);
if lo > lo_inf {
d_l_map.push(d_idx);
}
if hi < up_inf {
d_u_map.push(d_idx);
}
}
}
let n_c = c_map.len() as Index;
let n_d = d_map.len() as Index;
Ok(BoundClassification {
n_full_x,
n_full_g,
n_x_fixed,
x_not_fixed_map,
x_fixed_map,
x_fixed_vals,
full_to_var,
x_l_map,
x_u_map,
n_c,
c_map,
n_d,
d_map,
d_l_map,
d_u_map,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tnlp::{IndexStyle, IpoptCq, IpoptData, Solution, SparsityRequest, StartingPoint};
struct Hs071;
impl TNLP for Hs071 {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 4,
m: 2,
nnz_jac_g: 8,
nnz_h_lag: 10,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[1.0; 4]);
b.x_u.copy_from_slice(&[5.0; 4]);
b.g_l.copy_from_slice(&[25.0, 40.0]);
b.g_u.copy_from_slice(&[2.0e19, 40.0]);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&[1.0, 5.0, 5.0, 1.0]);
true
}
fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
Some(x[0] * x[3] * (x[0] + x[1] + x[2]) + x[2])
}
fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g.fill(0.0);
true
}
fn eval_g(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g.fill(0.0);
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
mode: SparsityRequest<'_>,
) -> bool {
if let SparsityRequest::Structure { irow, jcol } = mode {
irow.copy_from_slice(&[0, 0, 0, 0, 1, 1, 1, 1]);
jcol.copy_from_slice(&[0, 1, 2, 3, 0, 1, 2, 3]);
}
true
}
fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
#[test]
fn hs071_decomposes_to_one_eq_one_ineq() {
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Hs071));
let adapter = TNLPAdapter::new(tnlp).unwrap();
let c = adapter.classification();
assert_eq!(c.n_full_x, 4);
assert_eq!(c.n_full_g, 2);
assert_eq!(c.n_x_fixed, 0);
assert_eq!(c.n_x_var(), 4);
assert!(c.x_fixed_map.is_empty());
assert_eq!(c.full_to_var, vec![0, 1, 2, 3]);
assert_eq!(c.x_l_map, vec![0, 1, 2, 3]);
assert_eq!(c.x_u_map, vec![0, 1, 2, 3]);
assert_eq!(c.n_c, 1);
assert_eq!(c.c_map, vec![1]);
assert_eq!(c.n_d, 1);
assert_eq!(c.d_map, vec![0]);
assert_eq!(c.d_l_map, vec![0]);
assert!(c.d_u_map.is_empty());
assert_eq!(adapter.nlp_info().nnz_jac_g, 8);
}
struct Mixed;
impl TNLP for Mixed {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 3,
m: 2,
nnz_jac_g: 6,
nnz_h_lag: 0,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[3.0, -2.0e19, -2.0e19]);
b.x_u.copy_from_slice(&[3.0, 2.0e19, 7.0]);
b.g_l.copy_from_slice(&[0.0, -2.0e19]);
b.g_u.copy_from_slice(&[1.0, 2.0e19]);
true
}
fn get_starting_point(&mut self, _sp: StartingPoint<'_>) -> bool {
true
}
fn eval_f(&mut self, _x: &[Number], _new_x: bool) -> Option<Number> {
Some(0.0)
}
fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g.fill(0.0);
true
}
fn eval_g(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g.fill(0.0);
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
_m: SparsityRequest<'_>,
) -> bool {
true
}
fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
#[test]
fn mixed_bounds_classifies_correctly() {
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Mixed));
let adapter = TNLPAdapter::new(tnlp).unwrap();
let c = adapter.classification();
assert_eq!(c.n_full_x, 3);
assert_eq!(c.n_x_fixed, 1);
assert_eq!(c.n_x_var(), 2);
assert_eq!(c.x_not_fixed_map, vec![1, 2]);
assert_eq!(c.x_fixed_map, vec![0]);
assert_eq!(c.x_fixed_vals, vec![3.0]);
assert_eq!(c.full_to_var, vec![-1, 0, 1]);
assert!(c.x_l_map.is_empty());
assert_eq!(c.x_u_map, vec![1]);
assert_eq!(c.n_c, 0);
assert_eq!(c.n_d, 2);
assert_eq!(c.d_map, vec![0, 1]);
assert_eq!(c.d_l_map, vec![0]);
assert_eq!(c.d_u_map, vec![0]);
}
struct Bad;
impl TNLP for Bad {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 1,
m: 0,
nnz_jac_g: 0,
nnz_h_lag: 0,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l[0] = 5.0;
b.x_u[0] = 1.0;
true
}
fn get_starting_point(&mut self, _sp: StartingPoint<'_>) -> bool {
true
}
fn eval_f(&mut self, _x: &[Number], _new_x: bool) -> Option<Number> {
Some(0.0)
}
fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g.fill(0.0);
true
}
fn eval_g(&mut self, _x: &[Number], _new_x: bool, _g: &mut [Number]) -> bool {
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
_m: SparsityRequest<'_>,
) -> bool {
true
}
fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
struct OneFixedTwoEq;
impl TNLP for OneFixedTwoEq {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 3,
m: 2,
nnz_jac_g: 6,
nnz_h_lag: 0,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[2.5, -2.0e19, -2.0e19]);
b.x_u.copy_from_slice(&[2.5, 2.0e19, 2.0e19]);
b.g_l.copy_from_slice(&[0.0, 0.0]);
b.g_u.copy_from_slice(&[0.0, 0.0]);
true
}
fn get_starting_point(&mut self, _sp: StartingPoint<'_>) -> bool {
true
}
fn eval_f(&mut self, _x: &[Number], _new_x: bool) -> Option<Number> {
Some(0.0)
}
fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g.fill(0.0);
true
}
fn eval_g(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g.fill(0.0);
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
_m: SparsityRequest<'_>,
) -> bool {
true
}
fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
#[test]
fn relax_bounds_keeps_fixed_var_in_active_set() {
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(OneFixedTwoEq));
let adapter = TNLPAdapter::new_with_options(
tnlp,
DEFAULT_NLP_LOWER_BOUND_INF,
DEFAULT_NLP_UPPER_BOUND_INF,
FixedVarTreatment::RelaxBounds,
)
.unwrap();
let c = adapter.classification();
assert_eq!(c.n_full_x, 3);
assert_eq!(c.n_x_fixed, 0, "relax_bounds keeps fixed var in x_var");
assert_eq!(c.n_x_var(), 3);
assert_eq!(c.x_not_fixed_map, vec![0, 1, 2]);
assert!(c.x_fixed_map.is_empty());
assert!(c.x_fixed_vals.is_empty());
assert_eq!(c.full_to_var, vec![0, 1, 2]);
assert_eq!(c.x_l_map, vec![0]);
assert_eq!(c.x_u_map, vec![0]);
assert_eq!(c.n_c, 2);
}
#[test]
fn make_parameter_no_retry_when_boundary_dof() {
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(OneFixedTwoEq));
let adapter = TNLPAdapter::new(tnlp).unwrap();
let c = adapter.classification();
assert_eq!(c.n_x_fixed, 1);
assert_eq!(c.n_x_var(), 2);
assert_eq!(c.n_c, 2);
}
struct DofRescue;
impl TNLP for DofRescue {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 3,
m: 2,
nnz_jac_g: 6,
nnz_h_lag: 0,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[2.5, 1.0, -2.0e19]);
b.x_u.copy_from_slice(&[2.5, 1.0, 2.0e19]);
b.g_l.copy_from_slice(&[0.0, 0.0]);
b.g_u.copy_from_slice(&[0.0, 0.0]);
true
}
fn get_starting_point(&mut self, _sp: StartingPoint<'_>) -> bool {
true
}
fn eval_f(&mut self, _x: &[Number], _new_x: bool) -> Option<Number> {
Some(0.0)
}
fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g.fill(0.0);
true
}
fn eval_g(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g.fill(0.0);
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
_m: SparsityRequest<'_>,
) -> bool {
true
}
fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
#[test]
fn make_parameter_auto_retries_with_relax_bounds() {
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(DofRescue));
let adapter = TNLPAdapter::new(tnlp).unwrap();
let c = adapter.classification();
assert_eq!(c.n_x_fixed, 0);
assert_eq!(c.n_x_var(), 3);
assert_eq!(c.x_not_fixed_map, vec![0, 1, 2]);
assert_eq!(c.x_l_map, vec![0, 1]);
assert_eq!(c.x_u_map, vec![0, 1]);
assert_eq!(c.n_c, 2);
}
#[test]
fn inconsistent_variable_bounds_is_rejected() {
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Bad));
let err = TNLPAdapter::new(tnlp).unwrap_err();
assert_eq!(err.kind, ExceptionKind::INCONSISTENT_BOUNDS);
}
}