use nalgebra::{SVector, U1};
use num::FromPrimitive;
use num_dual::{Derivative, DualNum, DualVec, DualVec64, HyperDualVec, HyperDualVec64};
use std::cell::RefCell;
use std::convert::Infallible;
#[cfg(feature = "ipopt")]
pub mod ipopt;
#[cfg(feature = "ripopt")]
pub mod ripopt;
pub trait BasicADProblem<const X: usize> {
fn bounds(&self) -> ([f64; X], [f64; X]);
fn initial_point(&self) -> [f64; X];
fn constraint_bounds(&self) -> (Vec<f64>, Vec<f64>);
}
pub trait SimpleADProblem<const X: usize>: BasicADProblem<X> {
fn objective<D: DualNum<f64> + Copy>(&self, x: [D; X]) -> D;
fn constraints<D: DualNum<f64> + Copy>(&self, x: [D; X]) -> Vec<D>;
}
pub trait CachedADProblem<const X: usize>: BasicADProblem<X> {
type Error;
fn evaluate<D: DualNum<f64> + Copy>(&self, x: [D; X]) -> Result<(D, Vec<D>), Self::Error>;
}
#[expect(clippy::type_complexity)]
#[allow(unused)]
pub struct ADProblem<T, I, const X: usize, const CACHE: bool> {
problem: T,
con_jac_row_vec: Vec<I>,
con_jac_col_vec: Vec<I>,
hess_row_vec: Vec<I>,
hess_col_vec: Vec<I>,
cache: RefCell<Option<Option<(f64, Vec<f64>)>>>,
grad_cache: RefCell<Option<Option<([f64; X], Vec<[f64; X]>)>>>,
}
impl<T: BasicADProblem<X>, I: FromPrimitive, const X: usize, const CACHE: bool>
ADProblem<T, I, X, CACHE>
{
fn new_impl<
Err,
G: Fn(&T, [DualVec64<U1>; X]) -> Result<Vec<DualVec64<U1>>, Err>,
E: Fn(
&T,
[HyperDualVec64<U1, U1>; X],
) -> Result<(HyperDualVec64<U1, U1>, Vec<HyperDualVec64<U1, U1>>), Err>,
>(
problem: T,
constraints: G,
evaluate: E,
) -> Result<Self, Err> {
let x = problem.initial_point();
let mut con_jac_row_vec = Vec::new();
let mut con_jac_col_vec = Vec::new();
for i in 0..x.len() {
let mut x_dual: [DualVec64<U1>; X] = x.map(DualVec::from_re);
x_dual[i].eps = Derivative::derivative_generic(U1, U1, 0);
let con = constraints(&problem, x_dual)?;
for (j, c) in con.into_iter().enumerate() {
if c.eps != Derivative::none() {
con_jac_row_vec.push(I::from_usize(j).unwrap());
con_jac_col_vec.push(I::from_usize(i).unwrap());
}
}
}
let mut hess_row_vec = Vec::new();
let mut hess_col_vec = Vec::new();
for row in 0..x.len() {
for col in 0..=row {
let mut x_dual: [HyperDualVec64<U1, U1>; X] = x.map(HyperDualVec::from_re);
x_dual[row].eps1 = Derivative::derivative_generic(U1, U1, 0);
x_dual[col].eps2 = Derivative::derivative_generic(U1, U1, 0);
let (mut f, con) = evaluate(&problem, x_dual)?;
for g in con {
f += g;
}
if f.eps1eps2 != Derivative::none() {
hess_row_vec.push(I::from_usize(row).unwrap());
hess_col_vec.push(I::from_usize(col).unwrap());
}
}
}
Ok(Self {
problem,
con_jac_row_vec,
con_jac_col_vec,
hess_row_vec,
hess_col_vec,
cache: RefCell::new(None),
grad_cache: RefCell::new(None),
})
}
#[allow(unused)]
fn update_cache(&self, new_x: bool) {
if new_x {
*self.cache.borrow_mut() = None;
*self.grad_cache.borrow_mut() = None;
}
}
}
impl<T: SimpleADProblem<X>, I: FromPrimitive, const X: usize> ADProblem<T, I, X, false> {
pub fn new(problem: T) -> Self {
Self::new_impl(
problem,
|problem, x| Ok::<_, Infallible>(problem.constraints(x)),
|problem, x| Ok((problem.objective(x), problem.constraints(x))),
)
.unwrap()
}
}
impl<T: CachedADProblem<X>, I: FromPrimitive, const X: usize> ADProblem<T, I, X, true> {
pub fn new_cached(problem: T) -> Result<Self, T::Error> {
Self::new_impl(
problem,
|problem, x| problem.evaluate(x).map(|(_, g)| g),
|problem, x| problem.evaluate(x),
)
}
#[expect(clippy::type_complexity)]
#[allow(unused)]
fn evaluate_gradients(&self, x: [f64; X]) -> Result<([f64; X], Vec<[f64; X]>), T::Error> {
let mut x = SVector::from(x).map(DualVec::from_re);
let (r, c) = x.shape_generic();
for (i, xi) in x.iter_mut().enumerate() {
xi.eps = Derivative::derivative_generic(r, c, i);
}
let (f, g) = self.problem.evaluate(x.data.0[0])?;
Ok((
f.eps.unwrap_generic(r, c).data.0[0],
g.into_iter()
.map(|g| g.eps.unwrap_generic(r, c).data.0[0])
.collect(),
))
}
}