use crate::{DriftDerivResult, HyperOperator};
use ndarray::{Array1, Array2};
use std::sync::Arc;
#[derive(Clone)]
pub struct ExactNewtonJointPsiTerms {
pub objective_psi: f64,
pub score_psi: Array1<f64>,
pub hessian_psi: Array2<f64>,
pub hessian_psi_operator: Option<Arc<dyn HyperOperator>>,
}
impl std::fmt::Debug for ExactNewtonJointPsiTerms {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExactNewtonJointPsiTerms")
.field("objective_psi", &self.objective_psi)
.field("score_psi", &self.score_psi)
.field("hessian_psi", &self.hessian_psi)
.field(
"hessian_psi_operator",
&self.hessian_psi_operator.as_ref().map(|_| "<operator>"),
)
.finish()
}
}
impl ExactNewtonJointPsiTerms {
pub fn zeros(total: usize) -> Self {
Self {
objective_psi: 0.0,
score_psi: Array1::zeros(total),
hessian_psi: Array2::zeros((total, total)),
hessian_psi_operator: None,
}
}
}
#[derive(Clone)]
pub struct ExactNewtonJointPsiSecondOrderTerms {
pub objective_psi_psi: f64,
pub score_psi_psi: Array1<f64>,
pub hessian_psi_psi: Array2<f64>,
pub hessian_psi_psi_operator: Option<Arc<dyn HyperOperator>>,
}
pub struct ExactNewtonJointPsiSecondOrderContracted {
pub objective: Array1<f64>,
pub score: Array2<f64>,
pub hessian: Vec<DriftDerivResult>,
}
pub trait ExactNewtonJointPsiWorkspace: Send + Sync {
fn first_order_terms(
&self,
psi_index: usize,
) -> Result<Option<ExactNewtonJointPsiTerms>, String> {
let Some(all) = self.first_order_terms_all()? else {
return Ok(None);
};
let materialized = all.len();
match all.into_iter().nth(psi_index) {
Some(terms) => Ok(Some(terms)),
None => Err(format!(
"ExactNewtonJointPsiWorkspace: psi index {psi_index} is out of range for the \
{materialized} axes this workspace materialized"
)),
}
}
fn first_order_terms_all(&self) -> Result<Option<Vec<ExactNewtonJointPsiTerms>>, String> {
Ok(None)
}
fn second_order_terms(
&self,
psi_i: usize,
psi_j: usize,
) -> Result<Option<ExactNewtonJointPsiSecondOrderTerms>, String>;
fn second_order_terms_contracted(
&self,
_: &[f64],
) -> Result<Option<ExactNewtonJointPsiSecondOrderContracted>, String> {
Ok(None)
}
fn hessian_directional_derivative(
&self,
psi_index: usize,
d_beta_flat: &Array1<f64>,
) -> Result<Option<DriftDerivResult>, String>;
fn hessian_directional_derivatives_all_beta_axes(
&self,
psi_index: usize,
total: usize,
) -> Result<Option<Vec<DriftDerivResult>>, String> {
per_axis_psi_hessian_directional_derivatives(self, psi_index, total)
}
fn hessian_second_directional_derivative_all_beta_axes(
&self,
psi_index: usize,
d_beta_flat: &Array1<f64>,
) -> Result<Option<Vec<Array2<f64>>>, String> {
Err(format!("exact third information derivatives are unavailable for psi axis {psi_index} and coefficient direction of length {}", d_beta_flat.len()))
}
fn second_order_hessian_directional_derivative_all_beta_axes(
&self,
psi_i: usize,
psi_j: usize,
) -> Result<Option<Vec<Array2<f64>>>, String> {
Err(format!("exact third information derivatives are unavailable for psi pair ({psi_i}, {psi_j})"))
}
}
pub fn per_axis_psi_hessian_directional_derivatives(
workspace: &(impl ExactNewtonJointPsiWorkspace + ?Sized),
psi_index: usize,
total: usize,
) -> Result<Option<Vec<DriftDerivResult>>, String> {
let mut axes = Vec::with_capacity(total);
let mut direction = Array1::<f64>::zeros(total);
for axis in 0..total {
direction[axis] = 1.0;
let Some(derivative) = workspace.hessian_directional_derivative(psi_index, &direction)?
else {
return Ok(None);
};
axes.push(derivative);
direction[axis] = 0.0;
}
Ok(Some(axes))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zeros_has_zero_objective() {
let t = ExactNewtonJointPsiTerms::zeros(3);
assert_eq!(t.objective_psi, 0.0);
}
#[test]
fn zeros_has_correct_score_dimension() {
let t = ExactNewtonJointPsiTerms::zeros(5);
assert_eq!(t.score_psi.len(), 5);
assert!(t.score_psi.iter().all(|&v| v == 0.0));
}
#[test]
fn zeros_has_square_hessian_of_correct_size() {
let t = ExactNewtonJointPsiTerms::zeros(4);
assert_eq!(t.hessian_psi.nrows(), 4);
assert_eq!(t.hessian_psi.ncols(), 4);
assert!(t.hessian_psi.iter().all(|&v| v == 0.0));
}
#[test]
fn zeros_has_no_operator() {
let t = ExactNewtonJointPsiTerms::zeros(2);
assert!(t.hessian_psi_operator.is_none());
}
#[test]
fn zeros_with_dimension_zero_does_not_panic() {
let t = ExactNewtonJointPsiTerms::zeros(0);
assert_eq!(t.score_psi.len(), 0);
assert_eq!(t.hessian_psi.nrows(), 0);
}
}