use crate::{Error, Result};
pub fn fisher_orthogonal_project(
gradient: &[f64],
past_gradients: &[Vec<f64>],
fisher_diag: &[f64],
) -> Result<Vec<f64>> {
let d = gradient.len();
validate_fisher_diag(fisher_diag, d)?;
validate_past_gradients(past_gradients, d)?;
if past_gradients.is_empty() {
return Ok(gradient.to_vec());
}
let k = past_gradients.len();
let gt_f_g: Vec<f64> = past_gradients
.iter()
.map(|gi| dot_diag_weighted(gi, fisher_diag, gradient))
.collect();
let mut gt_f_g_mat = vec![0.0; k * k];
for i in 0..k {
for j in i..k {
let val = dot_diag_weighted(&past_gradients[i], fisher_diag, &past_gradients[j]);
gt_f_g_mat[i * k + j] = val;
gt_f_g_mat[j * k + i] = val;
}
}
let x = solve_symmetric_positive(>_f_g_mat, >_f_g, k)?;
let mut result = gradient.to_vec();
for (j, xj) in x.iter().enumerate() {
for i in 0..d {
result[i] -= past_gradients[j][i] * xj;
}
}
Ok(result)
}
pub fn fisher_orthogonal_project_full(
gradient: &[f64],
past_gradients: &[Vec<f64>],
fisher: &[f64],
) -> Result<Vec<f64>> {
let d = gradient.len();
if fisher.len() != d * d {
return Err(Error::LengthMismatch {
a_name: "fisher",
a_len: fisher.len(),
b_name: "gradient (d*d)",
b_len: d * d,
});
}
validate_past_gradients(past_gradients, d)?;
if past_gradients.is_empty() {
return Ok(gradient.to_vec());
}
let k = past_gradients.len();
let f_g = matvec(fisher, gradient, d);
let gt_f_g: Vec<f64> = past_gradients.iter().map(|gi| dot(gi, &f_g)).collect();
let f_g_cols: Vec<Vec<f64>> = past_gradients
.iter()
.map(|gj| matvec(fisher, gj, d))
.collect();
let mut gt_f_g_mat = vec![0.0; k * k];
for i in 0..k {
for j in i..k {
let val = dot(&past_gradients[i], &f_g_cols[j]);
gt_f_g_mat[i * k + j] = val;
gt_f_g_mat[j * k + i] = val;
}
}
let x = solve_symmetric_positive(>_f_g_mat, >_f_g, k)?;
let mut result = gradient.to_vec();
for (j, xj) in x.iter().enumerate() {
for i in 0..d {
result[i] -= past_gradients[j][i] * xj;
}
}
Ok(result)
}
fn dot_diag_weighted(a: &[f64], diag: &[f64], b: &[f64]) -> f64 {
a.iter()
.zip(diag.iter())
.zip(b.iter())
.map(|((&ai, &di), &bi)| ai * di * bi)
.sum()
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(&ai, &bi)| ai * bi).sum()
}
fn matvec(mat: &[f64], v: &[f64], d: usize) -> Vec<f64> {
(0..d)
.map(|i| {
let row = &mat[i * d..(i + 1) * d];
dot(row, v)
})
.collect()
}
fn solve_symmetric_positive(a: &[f64], b: &[f64], n: usize) -> Result<Vec<f64>> {
let mut l = vec![0.0; n * n];
let eps = 1e-12;
for i in 0..n {
for j in 0..=i {
let mut sum = 0.0;
for k in 0..j {
sum += l[i * n + k] * l[j * n + k];
}
if i == j {
let diag = a[i * n + i] - sum + eps; if diag <= 0.0 {
return Ok(vec![0.0; n]);
}
l[i * n + j] = diag.sqrt();
} else {
l[i * n + j] = (a[i * n + j] - sum) / l[j * n + j];
}
}
}
let mut y = vec![0.0; n];
for i in 0..n {
let mut sum = 0.0;
for j in 0..i {
sum += l[i * n + j] * y[j];
}
y[i] = (b[i] - sum) / l[i * n + i];
}
let mut x = vec![0.0; n];
for i in (0..n).rev() {
let mut sum = 0.0;
for j in (i + 1)..n {
sum += l[j * n + i] * x[j];
}
x[i] = (y[i] - sum) / l[i * n + i];
}
Ok(x)
}
fn validate_fisher_diag(fisher_diag: &[f64], d: usize) -> Result<()> {
if fisher_diag.len() != d {
return Err(Error::LengthMismatch {
a_name: "fisher_diag",
a_len: fisher_diag.len(),
b_name: "gradient",
b_len: d,
});
}
Ok(())
}
fn validate_past_gradients(past_gradients: &[Vec<f64>], d: usize) -> Result<()> {
for (idx, g) in past_gradients.iter().enumerate() {
if g.len() != d {
return Err(Error::LengthMismatch {
a_name: "past_gradient",
a_len: g.len(),
b_name: "gradient",
b_len: d,
});
}
let _ = idx; }
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-10;
#[test]
fn empty_past_returns_original() {
let g = vec![1.0, 2.0, 3.0];
let f = vec![1.0, 1.0, 1.0];
let result = fisher_orthogonal_project(&g, &[], &f).unwrap();
assert_eq!(result, g);
}
#[test]
fn empty_past_returns_original_full() {
let g = vec![1.0, 2.0];
let f = vec![1.0, 0.0, 0.0, 1.0];
let result = fisher_orthogonal_project_full(&g, &[], &f).unwrap();
assert_eq!(result, g);
}
#[test]
fn identity_fisher_reduces_to_standard_projection() {
let g = vec![3.0, 4.0];
let past = vec![vec![1.0, 0.0]]; let f_diag = vec![1.0, 1.0];
let proj = fisher_orthogonal_project(&g, &past, &f_diag).unwrap();
assert!((proj[0]).abs() < EPS, "x should be 0, got {}", proj[0]);
assert!(
(proj[1] - 4.0).abs() < EPS,
"y should be 4, got {}",
proj[1]
);
}
#[test]
fn parallel_gradient_projects_to_zero() {
let past = vec![vec![1.0, 2.0, 3.0]];
let g = vec![2.0, 4.0, 6.0]; let f_diag = vec![1.0, 1.0, 1.0];
let proj = fisher_orthogonal_project(&g, &past, &f_diag).unwrap();
for (i, &v) in proj.iter().enumerate() {
assert!(v.abs() < EPS, "proj[{i}] = {v}, expected ~0");
}
}
#[test]
fn already_orthogonal_returns_unchanged() {
let g = vec![0.0, 1.0];
let past = vec![vec![1.0, 0.0]];
let f_diag = vec![1.0, 1.0];
let proj = fisher_orthogonal_project(&g, &past, &f_diag).unwrap();
assert!(
(proj[0] - 0.0).abs() < EPS,
"proj[0] = {}, expected 0",
proj[0]
);
assert!(
(proj[1] - 1.0).abs() < EPS,
"proj[1] = {}, expected 1",
proj[1]
);
}
#[test]
fn projected_is_fisher_orthogonal_to_past() {
let g = vec![1.0, 2.0, 3.0, 4.0];
let past = vec![vec![1.0, 0.0, 1.0, 0.0], vec![0.0, 1.0, 0.0, 1.0]];
let f_diag = vec![2.0, 3.0, 1.0, 4.0];
let proj = fisher_orthogonal_project(&g, &past, &f_diag).unwrap();
for (j, gj) in past.iter().enumerate() {
let inner: f64 = proj
.iter()
.zip(f_diag.iter())
.zip(gj.iter())
.map(|((&pi, &fi), &gji)| pi * fi * gji)
.sum();
assert!(
inner.abs() < EPS,
"proj^T F g_past[{j}] = {inner}, expected ~0"
);
}
}
#[test]
fn full_matrix_matches_diagonal_for_diagonal_fisher() {
let g = vec![1.0, 2.0, 3.0];
let past = vec![vec![1.0, 0.5, 0.0], vec![0.0, 0.5, 1.0]];
let f_diag = vec![2.0, 3.0, 1.0];
let mut f_full = vec![0.0; 9];
for i in 0..3 {
f_full[i * 3 + i] = f_diag[i];
}
let proj_diag = fisher_orthogonal_project(&g, &past, &f_diag).unwrap();
let proj_full = fisher_orthogonal_project_full(&g, &past, &f_full).unwrap();
for i in 0..3 {
assert!(
(proj_diag[i] - proj_full[i]).abs() < EPS,
"diag[{i}]={} != full[{i}]={}",
proj_diag[i],
proj_full[i]
);
}
}
#[test]
fn full_fisher_nondiagonal() {
let f = vec![2.0, 1.0, 1.0, 2.0];
let g = vec![1.0, 1.0];
let past = vec![vec![1.0, -1.0]];
let proj = fisher_orthogonal_project_full(&g, &past, &f).unwrap();
let f_past: Vec<f64> = vec![
f[0] * past[0][0] + f[1] * past[0][1],
f[2] * past[0][0] + f[3] * past[0][1],
];
let inner: f64 = proj[0] * f_past[0] + proj[1] * f_past[1];
assert!(inner.abs() < EPS, "proj^T F past = {inner}, expected ~0");
}
#[test]
fn length_mismatch_errors() {
let g = vec![1.0, 2.0];
let past = vec![vec![1.0, 2.0, 3.0]]; let f_diag = vec![1.0, 1.0];
assert!(fisher_orthogonal_project(&g, &past, &f_diag).is_err());
let f_diag_wrong = vec![1.0, 1.0, 1.0]; assert!(fisher_orthogonal_project(&g, &[], &f_diag_wrong).is_err());
}
#[test]
fn multiple_past_gradients() {
let g = vec![1.0, 1.0, 5.0];
let past = vec![vec![1.0, 0.0, 0.0], vec![0.0, 1.0, 0.0]];
let f_diag = vec![1.0, 1.0, 1.0];
let proj = fisher_orthogonal_project(&g, &past, &f_diag).unwrap();
assert!(proj[0].abs() < EPS, "x should be 0, got {}", proj[0]);
assert!(proj[1].abs() < EPS, "y should be 0, got {}", proj[1]);
assert!(
(proj[2] - 5.0).abs() < EPS,
"z should be 5, got {}",
proj[2]
);
}
#[test]
fn weighted_fisher_changes_projection() {
let g = vec![1.0, 1.0];
let past = vec![vec![1.0, 1.0]];
let f_uniform = vec![1.0, 1.0];
let proj_uniform = fisher_orthogonal_project(&g, &past, &f_uniform).unwrap();
for &v in &proj_uniform {
assert!(v.abs() < EPS);
}
let f_weighted = vec![2.0, 1.0];
let proj_weighted = fisher_orthogonal_project(&g, &past, &f_weighted).unwrap();
for &v in &proj_weighted {
assert!(v.abs() < EPS);
}
let g2 = vec![1.0, 0.0];
let proj2 = fisher_orthogonal_project(&g2, &past, &f_weighted).unwrap();
let inner: f64 = proj2
.iter()
.zip(f_weighted.iter())
.zip(past[0].iter())
.map(|((&pi, &fi), &gi)| pi * fi * gi)
.sum();
assert!(inner.abs() < EPS, "not Fisher-orthogonal: {inner}");
}
}