#![allow(non_upper_case_globals)]
use std::ffi::CString;
use std::os::raw::{c_char, c_int};
use crate::opf::nlp::{NlpSolution, NonlinearProblem};
use crate::opf::{OpfError, OptStatus};
#[allow(
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
dead_code,
clippy::all
)]
mod bindings {
include!(concat!(env!("OUT_DIR"), "/ipopt_bindings.rs"));
}
#[derive(Clone, Debug)]
pub struct IpoptOptions {
pub tolerance: f64,
pub max_iterations: i32,
pub print_level: i32,
pub string_options: Vec<(String, String)>,
}
impl Default for IpoptOptions {
fn default() -> Self {
Self {
tolerance: 1e-8,
max_iterations: 3000,
print_level: 0,
string_options: Vec::new(),
}
}
}
struct Context<'a> {
problem: &'a dyn NonlinearProblem,
jacobian_pattern: Vec<(usize, usize)>,
hessian_pattern: Vec<(usize, usize)>,
pattern_mismatch: bool,
}
fn lower_triangle(triplets: &[(usize, usize, f64)]) -> Vec<(usize, usize)> {
let mut seen: Vec<(usize, usize)> = triplets
.iter()
.filter(|(r, c, _)| r >= c)
.map(|(r, c, _)| (*r, *c))
.collect();
seen.sort_unstable();
seen.dedup();
seen
}
fn scaled_hessian(
problem: &dyn NonlinearProblem,
x: &[f64],
lambda: &[f64],
sigma: f64,
pattern: &[(usize, usize)],
) -> Vec<f64> {
let negated: Vec<f64> = lambda.iter().map(|v| -v).collect();
let mut dense: std::collections::HashMap<(usize, usize), f64> =
std::collections::HashMap::new();
for (r, c, v) in problem.lagrangian_hessian(x, &negated) {
if r >= c {
*dense.entry((r, c)).or_insert(0.0) += v;
}
}
if sigma != 1.0 {
let zero = vec![0.0; lambda.len()];
for (r, c, v) in problem.lagrangian_hessian(x, &zero) {
if r >= c {
*dense.entry((r, c)).or_insert(0.0) += (sigma - 1.0) * v;
}
}
}
pattern
.iter()
.map(|key| dense.get(key).copied().unwrap_or(0.0))
.collect()
}
unsafe extern "C" fn eval_f(
n: bindings::ipindex,
x: *mut bindings::ipnumber,
_new_x: bool,
obj_value: *mut bindings::ipnumber,
user_data: bindings::UserDataPtr,
) -> bool {
let context = unsafe { &mut *(user_data as *mut Context) };
let x = unsafe { std::slice::from_raw_parts(x, n as usize) };
let value = context.problem.objective(x);
unsafe { *obj_value = value };
value.is_finite()
}
unsafe extern "C" fn eval_grad_f(
n: bindings::ipindex,
x: *mut bindings::ipnumber,
_new_x: bool,
grad_f: *mut bindings::ipnumber,
user_data: bindings::UserDataPtr,
) -> bool {
let context = unsafe { &mut *(user_data as *mut Context) };
let x = unsafe { std::slice::from_raw_parts(x, n as usize) };
let gradient = context.problem.gradient(x);
let out = unsafe { std::slice::from_raw_parts_mut(grad_f, n as usize) };
out.copy_from_slice(&gradient);
true
}
unsafe extern "C" fn eval_g(
n: bindings::ipindex,
x: *mut bindings::ipnumber,
_new_x: bool,
m: bindings::ipindex,
g: *mut bindings::ipnumber,
user_data: bindings::UserDataPtr,
) -> bool {
let context = unsafe { &mut *(user_data as *mut Context) };
let x = unsafe { std::slice::from_raw_parts(x, n as usize) };
let values = context.problem.constraints(x);
if m > 0 {
let out = unsafe { std::slice::from_raw_parts_mut(g, m as usize) };
out.copy_from_slice(&values);
}
true
}
unsafe extern "C" fn eval_jac_g(
n: bindings::ipindex,
x: *mut bindings::ipnumber,
_new_x: bool,
_m: bindings::ipindex,
nele_jac: bindings::ipindex,
i_row: *mut bindings::ipindex,
j_col: *mut bindings::ipindex,
values: *mut bindings::ipnumber,
user_data: bindings::UserDataPtr,
) -> bool {
let context = unsafe { &mut *(user_data as *mut Context) };
let count = nele_jac as usize;
if values.is_null() {
let rows = unsafe { std::slice::from_raw_parts_mut(i_row, count) };
let cols = unsafe { std::slice::from_raw_parts_mut(j_col, count) };
for (k, (r, c)) in context.jacobian_pattern.iter().enumerate() {
rows[k] = *r as bindings::ipindex;
cols[k] = *c as bindings::ipindex;
}
return true;
}
let x = unsafe { std::slice::from_raw_parts(x, n as usize) };
let mut folded = vec![0.0; count];
let mut index: std::collections::HashMap<(usize, usize), usize> =
std::collections::HashMap::with_capacity(count);
for (k, key) in context.jacobian_pattern.iter().enumerate() {
index.insert(*key, k);
}
for (r, c, v) in context.problem.jacobian(x) {
match index.get(&(r, c)) {
Some(&k) => folded[k] += v,
None => {
context.pattern_mismatch = true;
return false;
}
}
}
let out = unsafe { std::slice::from_raw_parts_mut(values, count) };
out.copy_from_slice(&folded);
true
}
#[allow(clippy::too_many_arguments)]
unsafe extern "C" fn eval_h(
n: bindings::ipindex,
x: *mut bindings::ipnumber,
_new_x: bool,
obj_factor: bindings::ipnumber,
m: bindings::ipindex,
lambda: *mut bindings::ipnumber,
_new_lambda: bool,
nele_hess: bindings::ipindex,
i_row: *mut bindings::ipindex,
j_col: *mut bindings::ipindex,
values: *mut bindings::ipnumber,
user_data: bindings::UserDataPtr,
) -> bool {
let context = unsafe { &mut *(user_data as *mut Context) };
let count = nele_hess as usize;
if values.is_null() {
let rows = unsafe { std::slice::from_raw_parts_mut(i_row, count) };
let cols = unsafe { std::slice::from_raw_parts_mut(j_col, count) };
for (k, (r, c)) in context.hessian_pattern.iter().enumerate() {
rows[k] = *r as bindings::ipindex;
cols[k] = *c as bindings::ipindex;
}
return true;
}
let x = unsafe { std::slice::from_raw_parts(x, n as usize) };
let lambda = if m > 0 {
unsafe { std::slice::from_raw_parts(lambda, m as usize) }.to_vec()
} else {
Vec::new()
};
let folded = scaled_hessian(
context.problem,
x,
&lambda,
obj_factor,
&context.hessian_pattern,
);
let out = unsafe { std::slice::from_raw_parts_mut(values, count) };
out.copy_from_slice(&folded);
true
}
fn map_status(status: bindings::ApplicationReturnStatus) -> OptStatus {
use bindings::*;
match status {
ApplicationReturnStatus_Solve_Succeeded
| ApplicationReturnStatus_Solved_To_Acceptable_Level => OptStatus::Optimal,
ApplicationReturnStatus_Infeasible_Problem_Detected => OptStatus::Infeasible,
ApplicationReturnStatus_Diverging_Iterates => OptStatus::Unbounded,
ApplicationReturnStatus_Maximum_Iterations_Exceeded => OptStatus::Other("iteration limit"),
ApplicationReturnStatus_Maximum_CpuTime_Exceeded => OptStatus::Other("time limit"),
ApplicationReturnStatus_Restoration_Failed => OptStatus::Other("restoration failed"),
ApplicationReturnStatus_Search_Direction_Becomes_Too_Small => {
OptStatus::Other("search direction collapsed")
}
ApplicationReturnStatus_User_Requested_Stop => OptStatus::Other("stopped by user"),
ApplicationReturnStatus_Invalid_Number_Detected => {
OptStatus::Other("invalid number in a callback")
}
_ => OptStatus::Other("IPOPT reported a failure"),
}
}
fn set_string_option(
problem: bindings::IpoptProblem,
name: &str,
value: &str,
) -> Result<(), OpfError> {
let name = CString::new(name).map_err(|e| OpfError::Backend(e.to_string()))?;
let value = CString::new(value).map_err(|e| OpfError::Backend(e.to_string()))?;
let ok = unsafe {
bindings::AddIpoptStrOption(
problem,
name.as_ptr() as *mut c_char,
value.as_ptr() as *mut c_char,
)
};
if ok {
Ok(())
} else {
Err(OpfError::Backend(format!(
"IPOPT rejected option {name:?} = {value:?}"
)))
}
}
pub fn solve(
problem: &dyn NonlinearProblem,
options: &IpoptOptions,
) -> Result<NlpSolution, OpfError> {
let n = problem.n_vars();
let m = problem.n_constraints();
let (mut x_lower, mut x_upper) = problem.var_bounds();
let (mut g_lower, mut g_upper) = problem.constraint_bounds();
let start = problem.initial_point();
const IPOPT_INFINITY: f64 = 2.0e19;
for value in x_lower.iter_mut().chain(g_lower.iter_mut()) {
if *value == f64::NEG_INFINITY {
*value = -IPOPT_INFINITY;
}
}
for value in x_upper.iter_mut().chain(g_upper.iter_mut()) {
if *value == f64::INFINITY {
*value = IPOPT_INFINITY;
}
}
let jacobian_pattern = {
let mut keys: Vec<(usize, usize)> = problem
.jacobian(&start)
.into_iter()
.map(|(r, c, _)| (r, c))
.collect();
keys.sort_unstable();
keys.dedup();
keys
};
let hessian_pattern = {
let zero = vec![0.0; m];
let mut both = problem.lagrangian_hessian(&start, &zero);
let ones = vec![1.0; m];
both.extend(problem.lagrangian_hessian(&start, &ones));
lower_triangle(&both)
};
let mut context = Context {
problem,
jacobian_pattern,
hessian_pattern,
pattern_mismatch: false,
};
let handle = unsafe {
bindings::CreateIpoptProblem(
n as bindings::ipindex,
x_lower.as_mut_ptr(),
x_upper.as_mut_ptr(),
m as bindings::ipindex,
g_lower.as_mut_ptr(),
g_upper.as_mut_ptr(),
context.jacobian_pattern.len() as bindings::ipindex,
context.hessian_pattern.len() as bindings::ipindex,
0, Some(eval_f),
Some(eval_g),
Some(eval_grad_f),
Some(eval_jac_g),
Some(eval_h),
)
};
if handle.is_null() {
return Err(OpfError::Backend("CreateIpoptProblem returned null".into()));
}
struct Handle(bindings::IpoptProblem);
impl Drop for Handle {
fn drop(&mut self) {
unsafe { bindings::FreeIpoptProblem(self.0) };
}
}
let guard = Handle(handle);
set_string_option(handle, "sb", "yes")?;
let tol = CString::new("tol").unwrap();
let max_iter = CString::new("max_iter").unwrap();
let print_level = CString::new("print_level").unwrap();
unsafe {
bindings::AddIpoptNumOption(handle, tol.as_ptr() as *mut c_char, options.tolerance);
bindings::AddIpoptIntOption(
handle,
max_iter.as_ptr() as *mut c_char,
options.max_iterations as c_int,
);
bindings::AddIpoptIntOption(
handle,
print_level.as_ptr() as *mut c_char,
options.print_level as c_int,
);
}
for (name, value) in &options.string_options {
set_string_option(handle, name, value)?;
}
let mut x = start.clone();
let mut g = vec![0.0; m.max(1)];
let mut objective = 0.0f64;
let mut mult_g = vec![0.0; m.max(1)];
let mut mult_x_lower = vec![0.0; n.max(1)];
let mut mult_x_upper = vec![0.0; n.max(1)];
let status = unsafe {
bindings::IpoptSolve(
handle,
x.as_mut_ptr(),
g.as_mut_ptr(),
&mut objective,
mult_g.as_mut_ptr(),
mult_x_lower.as_mut_ptr(),
mult_x_upper.as_mut_ptr(),
&mut context as *mut Context as bindings::UserDataPtr,
)
};
drop(guard);
if context.pattern_mismatch {
return Err(OpfError::Backend(
"the problem's Jacobian sparsity pattern changed during the solve; IPOPT \
requires it to be fixed"
.into(),
));
}
let status = map_status(status);
let constraints = problem.constraints(&x);
let (c_lower, c_upper) = problem.constraint_bounds();
let mut violation = 0.0f64;
for r in 0..m {
violation = violation
.max((c_lower[r] - constraints[r]).max(0.0))
.max((constraints[r] - c_upper[r]).max(0.0));
}
Ok(NlpSolution {
status,
objective,
x,
y: mult_g[..m].iter().map(|v| -v).collect(),
z: (0..n).map(|k| mult_x_lower[k] - mult_x_upper[k]).collect(),
iterations: 0,
violation,
})
}
pub fn version() -> String {
format!(
"{}.{}.{}",
bindings::IPOPT_VERSION_MAJOR,
bindings::IPOPT_VERSION_MINOR,
bindings::IPOPT_VERSION_RELEASE
)
}