use gam_linalg::faer_ndarray::{FaerEigh, FaerSvd};
use gam_linalg::lanczos::{SymmetricExtremeLanczosOptions, symmetric_extreme_lanczos_eigenpairs};
use ndarray::{Array1, Array2, Array3, ArrayView1, ArrayViewMut2, s};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct FrameAtomSlot {
offset: usize,
axes: usize,
axis_start: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct LocalAxis {
offset: usize,
axes: usize,
axis: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FrameColumnLayout {
p: usize,
d_total: usize,
param_dim: usize,
atoms: Vec<FrameAtomSlot>,
locals: Vec<LocalAxis>,
}
impl FrameColumnLayout {
pub fn new(p: usize, axes_per_atom: &[usize]) -> Self {
let mut atoms = Vec::with_capacity(axes_per_atom.len());
let mut locals = Vec::new();
let mut offset = 0usize;
let mut axis_start = 0usize;
for &axes in axes_per_atom {
atoms.push(FrameAtomSlot {
offset,
axes,
axis_start,
});
for axis in 0..axes {
locals.push(LocalAxis { offset, axes, axis });
}
offset += p * axes;
axis_start += axes;
}
Self {
p,
d_total: axis_start,
param_dim: offset,
atoms,
locals,
}
}
pub fn for_frames<'a>(frames: impl IntoIterator<Item = &'a Array2<f64>>) -> Option<Self> {
let dims: Vec<(usize, usize)> = frames.into_iter().map(|f| f.dim()).collect();
let mut p: Option<usize> = None;
for &(rows, cols) in &dims {
if cols == 0 {
continue;
}
match p {
None => p = Some(rows),
Some(known) if known == rows => {}
Some(_) => return None,
}
}
let p = p.unwrap_or(0);
let axes: Vec<usize> = dims.iter().map(|&(_, cols)| cols).collect();
Some(Self::new(p, &axes))
}
#[inline]
pub fn output_dim(&self) -> usize {
self.p
}
#[inline]
pub fn block_dim(&self) -> usize {
self.d_total
}
#[inline]
pub fn param_dim(&self) -> usize {
self.param_dim
}
#[inline]
pub fn atom_count(&self) -> usize {
self.atoms.len()
}
#[inline]
pub fn local_axis_base(&self, atom: usize) -> usize {
self.atoms[atom].axis_start
}
#[inline]
pub fn column(&self, output: usize, local: usize) -> usize {
let slot = self.locals[local];
slot.offset + output * slot.axes + slot.axis
}
pub fn output_of(&self, column: usize) -> Option<usize> {
let slot = self
.atoms
.iter()
.rev()
.find(|slot| slot.axes > 0 && column >= slot.offset)?;
if column >= slot.offset + self.p * slot.axes {
return None;
}
Some((column - slot.offset) / slot.axes)
}
#[inline]
pub fn gather_output(&self, v: ArrayView1<'_, f64>, output: usize, out: &mut [f64]) {
for (local, slot) in self.locals.iter().enumerate() {
out[local] = v[slot.offset + output * slot.axes + slot.axis];
}
}
}
fn fold_row_into_triangular_factor(r: &mut ArrayViewMut2<'_, f64>, v: &mut [f64]) {
let d = v.len();
for j in 0..d {
let vj = v[j];
if vj == 0.0 {
continue;
}
let rjj = r[[j, j]];
let norm = rjj.hypot(vj);
if norm == 0.0 {
continue;
}
let (c, s) = (rjj / norm, vj / norm);
r[[j, j]] = norm;
v[j] = 0.0;
for k in (j + 1)..d {
let rjk = r[[j, k]];
let vk = v[k];
r[[j, k]] = c * rjk + s * vk;
v[k] = c * vk - s * rjk;
}
}
}
pub struct OutputBlockRootAccumulator {
roots: Array3<f64>,
layout: FrameColumnLayout,
scratch: Vec<f64>,
}
impl OutputBlockRootAccumulator {
pub fn new(layout: FrameColumnLayout) -> Self {
let d = layout.block_dim();
Self {
roots: Array3::<f64>::zeros((layout.output_dim(), d, d)),
layout,
scratch: vec![0.0_f64; d],
}
}
pub fn push_row_jacobian(&mut self, g: &Array2<f64>) {
let d = self.layout.block_dim();
for i in 0..self.layout.output_dim() {
let mut any = false;
for l in 0..d {
let v = g[[i, l]];
self.scratch[l] = v;
any |= v != 0.0;
}
if !any {
continue;
}
let mut block = self.roots.slice_mut(s![i, .., ..]);
fold_row_into_triangular_factor(&mut block, &mut self.scratch);
}
}
pub fn finish_with_rows(
self,
dense_rows: Array2<f64>,
root_rows: usize,
) -> Result<ResidualGaugeCurvature, String> {
if dense_rows.nrows() > 0 && dense_rows.ncols() != self.layout.param_dim() {
return Err(format!(
"residual gauge curvature: dense rows have {} columns but param_dim = {}",
dense_rows.ncols(),
self.layout.param_dim()
));
}
Ok(ResidualGaugeCurvature::OutputBlockRoots {
roots: self.roots,
dense_rows,
layout: self.layout,
root_rows,
})
}
pub fn finish(self, root_rows: usize) -> ResidualGaugeCurvature {
let param_dim = self.layout.param_dim();
self.finish_with_rows(Array2::<f64>::zeros((0, param_dim)), root_rows)
.expect("an empty dense-row block is conformable with any layout")
}
}
pub struct TriangularRootAccumulator {
factor: Array2<f64>,
}
impl TriangularRootAccumulator {
pub fn new(param_dim: usize) -> Self {
Self {
factor: Array2::<f64>::zeros((param_dim, param_dim)),
}
}
pub fn push_root_row(&mut self, row: &mut [f64]) -> Result<(), String> {
if row.len() != self.factor.ncols() {
return Err(format!(
"residual gauge curvature: root row has {} entries but the factor is over {} \
parameters",
row.len(),
self.factor.ncols()
));
}
let mut view = self.factor.view_mut();
fold_row_into_triangular_factor(&mut view, row);
Ok(())
}
pub fn finish(self, root_rows: usize) -> ResidualGaugeCurvature {
ResidualGaugeCurvature::DualRoot {
root: self.factor,
root_rows,
}
}
pub fn into_factor(self) -> Array2<f64> {
self.factor
}
pub fn merge(&mut self, other: Self) -> Result<(), String> {
if other.factor.ncols() != self.factor.ncols() {
return Err(format!(
"residual gauge curvature: cannot merge a factor over {} parameters into one \
over {}",
other.factor.ncols(),
self.factor.ncols()
));
}
let mut row = vec![0.0_f64; self.factor.ncols()];
for r in 0..other.factor.nrows() {
for (c, slot) in row.iter_mut().enumerate() {
*slot = other.factor[[r, c]];
}
self.push_root_row(&mut row)?;
}
Ok(())
}
}
pub struct BlockPlusRowsSpectrum {
block_eigenvalues: Array2<f64>,
projected: Array3<f64>,
update_rank: usize,
block_lambda_max: f64,
update_norm_sq: f64,
}
impl BlockPlusRowsSpectrum {
pub fn new(
roots: &Array3<f64>,
dense_rows: &Array2<f64>,
layout: &FrameColumnLayout,
) -> Result<Self, String> {
let p = layout.output_dim();
let d = layout.block_dim();
let k = dense_rows.nrows();
let mut block_eigenvalues = Array2::<f64>::zeros((p, d));
let mut projected = Array3::<f64>::zeros((p, k, d));
let mut gathered = vec![0.0_f64; d];
let mut block_lambda_max = 0.0_f64;
for i in 0..p {
let block = roots.slice(s![i, .., ..]);
let basis_t = if block.iter().all(|v| *v == 0.0) || d == 1 {
if d == 1 {
block_eigenvalues[[i, 0]] = block[[0, 0]] * block[[0, 0]];
block_lambda_max = block_lambda_max.max(block_eigenvalues[[i, 0]]);
}
None
} else {
let (_u, sv, vt) = block.to_owned().svd(false, true).map_err(|e| {
format!("residual gauge curvature: SVD of block {i} failed: {e}")
})?;
let vt = vt.ok_or_else(|| {
format!("residual gauge curvature: block {i} SVD returned no right factor")
})?;
for (t, sigma) in sv.iter().enumerate() {
block_eigenvalues[[i, t]] = sigma * sigma;
block_lambda_max = block_lambda_max.max(block_eigenvalues[[i, t]]);
}
Some(vt)
};
for j in 0..k {
layout.gather_output(dense_rows.row(j), i, &mut gathered);
match &basis_t {
Some(vt) => {
for t in 0..d {
let mut acc = 0.0_f64;
for a in 0..d {
acc += vt[[t, a]] * gathered[a];
}
projected[[i, j, t]] = acc;
}
}
None => {
for t in 0..d {
projected[[i, j, t]] = gathered[t];
}
}
}
}
}
let update_norm_sq = dense_rows.iter().map(|v| v * v).sum::<f64>();
Ok(Self {
block_eigenvalues,
projected,
update_rank: k,
block_lambda_max,
update_norm_sq,
})
}
pub fn count_above(&self, shift: f64) -> Result<usize, String> {
let (p, d) = self.block_eigenvalues.dim();
let mut shift = shift;
for _ in 0..8 {
let mut collided: Option<f64> = None;
for lambda in self.block_eigenvalues.iter() {
let tol = 8.0 * f64::EPSILON * lambda.abs().max(shift.abs());
if (lambda - shift).abs() <= tol {
collided = Some(collided.map_or(*lambda, |worst: f64| worst.max(*lambda)));
}
}
let Some(lambda) = collided else { break };
let step = (16.0 * f64::EPSILON * lambda.abs().max(shift.abs()))
.max(f64::MIN_POSITIVE * 16.0);
shift = lambda + step;
}
let mut count = 0usize;
for i in 0..p {
for t in 0..d {
if self.block_eigenvalues[[i, t]] > shift {
count += 1;
}
}
}
let k = self.update_rank;
if k == 0 {
return Ok(count);
}
let mut reduced = Array2::<f64>::zeros((k, k));
for j in 0..k {
reduced[[j, j]] = -1.0;
}
for i in 0..p {
for t in 0..d {
let denom = self.block_eigenvalues[[i, t]] - shift;
if denom == 0.0 {
return Err(
"residual gauge curvature: inertia shift collides with a block eigenvalue"
.to_string(),
);
}
let inv = 1.0 / denom;
for j in 0..k {
let pj = self.projected[[i, j, t]];
if pj == 0.0 {
continue;
}
for l in 0..=j {
reduced[[j, l]] -= pj * self.projected[[i, l, t]] * inv;
}
}
}
}
for j in 0..k {
for l in 0..j {
reduced[[l, j]] = reduced[[j, l]];
}
}
let (evals, _) = reduced.eigh(faer::Side::Lower).map_err(|e| {
format!("residual gauge curvature: inertia of the {k}x{k} reduced matrix failed: {e}")
})?;
count += evals.iter().filter(|v| **v > 0.0).count();
Ok(count)
}
pub fn lambda_max(&self) -> Result<f64, String> {
if self.update_rank == 0 || self.update_norm_sq == 0.0 {
return Ok(self.block_lambda_max);
}
let mut lo = self.block_lambda_max;
let mut hi = self.block_lambda_max + self.update_norm_sq;
if self.count_above(lo)? == 0 {
return Ok(lo);
}
for _ in 0..100 {
if hi - lo <= f64::EPSILON * hi.abs().max(1.0) {
break;
}
let mid = lo + 0.5 * (hi - lo);
if mid <= lo || mid >= hi {
break;
}
if self.count_above(mid)? > 0 {
lo = mid;
} else {
hi = mid;
}
}
Ok(hi)
}
pub fn block_lambda_max(&self) -> f64 {
self.block_lambda_max
}
pub fn update_rank(&self) -> usize {
self.update_rank
}
}
pub enum ResidualGaugeCurvature {
OutputBlockRoots {
roots: Array3<f64>,
dense_rows: Array2<f64>,
layout: FrameColumnLayout,
root_rows: usize,
},
DualRoot { root: Array2<f64>, root_rows: usize },
DenseGram { gram: Array2<f64>, root_rows: usize },
}
impl ResidualGaugeCurvature {
pub fn root_rows(&self) -> usize {
match self {
Self::OutputBlockRoots { root_rows, .. }
| Self::DualRoot { root_rows, .. }
| Self::DenseGram { root_rows, .. } => *root_rows,
}
}
pub fn stored_scalars(&self) -> usize {
match self {
Self::OutputBlockRoots {
roots, dense_rows, ..
} => roots.len() + dense_rows.len(),
Self::DualRoot { root, .. } => root.len(),
Self::DenseGram { gram, .. } => gram.len(),
}
}
pub fn is_finite(&self) -> bool {
match self {
Self::OutputBlockRoots {
roots, dense_rows, ..
} => roots.iter().all(|v| v.is_finite()) && dense_rows.iter().all(|v| v.is_finite()),
Self::DualRoot { root, .. } => root.iter().all(|v| v.is_finite()),
Self::DenseGram { gram, .. } => gram.iter().all(|v| v.is_finite()),
}
}
pub fn structure_tag(&self) -> &'static str {
match self {
Self::OutputBlockRoots { .. } => "output_block_roots",
Self::DualRoot { .. } => "dual_root",
Self::DenseGram { .. } => "dense_gram",
}
}
pub fn param_dim(&self) -> usize {
match self {
Self::OutputBlockRoots { layout, .. } => layout.param_dim(),
Self::DualRoot { root, .. } => root.ncols(),
Self::DenseGram { gram, .. } => gram.ncols(),
}
}
pub fn to_dense_gram(&self) -> Array2<f64> {
match self {
Self::OutputBlockRoots {
roots,
dense_rows,
layout,
..
} => {
let n = layout.param_dim();
let d = layout.block_dim();
let mut gram = Array2::<f64>::zeros((n, n));
for i in 0..layout.output_dim() {
let block = roots.slice(s![i, .., ..]);
let dense = block.t().dot(&block);
for a in 0..d {
let ca = layout.column(i, a);
for b in 0..d {
gram[[ca, layout.column(i, b)]] = dense[[a, b]];
}
}
}
if dense_rows.nrows() > 0 {
gram = gram + dense_rows.t().dot(dense_rows);
}
gram
}
Self::DualRoot { root, .. } => root.t().dot(root),
Self::DenseGram { gram, .. } => gram.clone(),
}
}
}
pub trait StreamedFrameCurvature: Sync {
fn param_dim(&self) -> usize;
fn root_rows(&self) -> usize;
fn apply(&self, x: &[f64], y: &mut [f64]) -> Result<(), String>;
fn diagonal(&self) -> Result<Array1<f64>, String>;
fn project_root(&self, directions: &[ArrayView1<'_, f64>]) -> Result<Array2<f64>, String>;
}
#[derive(Debug, Clone, Copy)]
pub struct StreamedLambdaMax {
pub lambda_max: f64,
pub relative_residual: f64,
pub trace: f64,
pub passes: usize,
}
fn streamed_lambda_max_relative_tol() -> f64 {
f64::EPSILON.sqrt()
}
fn streamed_lanczos_start(dim: usize) -> Vec<f64> {
let mut state = 1_u64;
let mut start = vec![0.0_f64; dim];
for value in &mut start {
state = (state * 106 + 1283) % 6075;
*value = state as f64 / 6075.0 - 0.5;
}
start
}
pub fn streamed_lambda_max(
operator: &dyn StreamedFrameCurvature,
) -> Result<StreamedLambdaMax, String> {
let dim = operator.param_dim();
if dim == 0 || operator.root_rows() == 0 {
return Ok(StreamedLambdaMax {
lambda_max: 0.0,
relative_residual: 0.0,
trace: 0.0,
passes: 0,
});
}
let diagonal = operator.diagonal()?;
if diagonal.len() != dim {
return Err(format!(
"streamed curvature: diagonal has {} entries but param_dim = {dim}",
diagonal.len()
));
}
if let Some(bad) = diagonal.iter().find(|v| !v.is_finite() || **v < 0.0) {
return Err(format!(
"streamed curvature: diag(H) must be finite and non-negative for a PSD \
curvature; found {bad:.6e}"
));
}
let trace = diagonal.iter().sum::<f64>();
if trace == 0.0 {
return Ok(StreamedLambdaMax {
lambda_max: 0.0,
relative_residual: 0.0,
trace: 0.0,
passes: 1,
});
}
let steps = dim.min(operator.root_rows()).max(1);
let check_every = 10usize.min((dim / 10).max(1));
let start = streamed_lanczos_start(dim);
let mut matvecs = 0usize;
let pairs = symmetric_extreme_lanczos_eigenpairs(
dim,
&start,
SymmetricExtremeLanczosOptions {
target_rank: 1,
max_steps: steps,
check_every,
relative_residual_tol: streamed_lambda_max_relative_tol(),
breakdown_tol: f64::EPSILON * trace,
},
|q, image| {
matvecs += 1;
operator.apply(q, image)
},
)
.map_err(|e| format!("streamed curvature: λ_max solve did not certify: {e}"))?;
let lambda_max = pairs.eigenvalues[0];
let residual = pairs.residual_bounds[0];
if !lambda_max.is_finite() {
return Err("streamed curvature: λ_max solve returned a non-finite Ritz value".to_string());
}
let slack = f64::EPSILON * (dim as f64) * trace;
if lambda_max < -slack || lambda_max > trace + slack {
return Err(format!(
"streamed curvature: λ_max = {lambda_max:.6e} is outside the PSD bracket \
[0, tr(H) = {trace:.6e}] its own diagonal gives; the operator's matvec and \
its diagonal disagree"
));
}
let lambda_max = lambda_max.clamp(0.0, trace);
Ok(StreamedLambdaMax {
lambda_max,
relative_residual: residual / lambda_max.max(1.0),
trace,
passes: matvecs + 1,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layout_column_and_output_are_inverse() {
let layout = FrameColumnLayout::new(5, &[2, 1, 3]);
assert_eq!(layout.param_dim(), 5 * 6);
assert_eq!(layout.block_dim(), 6);
for i in 0..5 {
for l in 0..6 {
let c = layout.column(i, l);
assert_eq!(layout.output_of(c), Some(i), "column {c} = (i={i}, l={l})");
}
}
let mut seen = vec![false; layout.param_dim()];
for i in 0..5 {
for l in 0..6 {
let c = layout.column(i, l);
assert!(!seen[c], "column {c} produced twice");
seen[c] = true;
}
}
assert!(seen.into_iter().all(|s| s));
}
#[test]
fn layout_matches_the_certificate_index_arithmetic() {
let axes = [2usize, 1, 3];
let p = 4;
let layout = FrameColumnLayout::new(p, &axes);
let mut offset = 0usize;
for (k, &d) in axes.iter().enumerate() {
for i in 0..p {
for a in 0..d {
let expected = offset + i * d + a;
let local = layout.local_axis_base(k) + a;
assert_eq!(layout.column(i, local), expected);
}
}
offset += p * d;
}
}
#[test]
fn layout_for_frames_rejects_disagreeing_frame_heights() {
let a = Array2::<f64>::zeros((4, 2));
let b = Array2::<f64>::zeros((3, 1));
assert!(FrameColumnLayout::for_frames([&a, &b]).is_none());
let c = Array2::<f64>::zeros((4, 1));
let layout = FrameColumnLayout::for_frames([&a, &c]).expect("agreeing heights");
assert_eq!(layout.output_dim(), 4);
assert_eq!(layout.block_dim(), 3);
assert_eq!(layout.param_dim(), 12);
}
#[test]
fn layout_for_frames_ignores_an_axisless_atom_height() {
let empty = Array2::<f64>::zeros((0, 0));
let real = Array2::<f64>::zeros((6, 2));
let layout = FrameColumnLayout::for_frames([&empty, &real]).expect("layout");
assert_eq!(layout.output_dim(), 6);
assert_eq!(layout.block_dim(), 2);
assert_eq!(layout.param_dim(), 12);
assert_eq!(layout.local_axis_base(1), 0);
}
#[test]
fn folding_rows_into_a_triangular_factor_reproduces_their_gram() {
let d = 5usize;
let mut seed = 0x2757_ACC0_0000_0001u64;
let mut next = || {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((seed >> 11) as f64) / ((1u64 << 53) as f64) - 0.5
};
let rows: Vec<Vec<f64>> = (0..17).map(|_| (0..d).map(|_| next()).collect()).collect();
let mut expected = Array2::<f64>::zeros((d, d));
for row in &rows {
for a in 0..d {
for b in 0..d {
expected[[a, b]] += row[a] * row[b];
}
}
}
let mut factor = Array2::<f64>::zeros((d, d));
for row in &rows {
let mut v = row.clone();
fold_row_into_triangular_factor(&mut factor.view_mut(), &mut v);
}
for a in 0..d {
for b in 0..a {
assert_eq!(factor[[a, b]], 0.0, "({a},{b}) below the diagonal");
}
}
let recovered = factor.t().dot(&factor);
let scale = expected.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
let worst = recovered
.iter()
.zip(expected.iter())
.fold(0.0_f64, |m, (a, b)| m.max((a - b).abs()));
assert!(
worst <= 1.0e-13 * scale,
"rᵀr must equal the accumulated Gram: worst |Δ| {worst:.3e} against {scale:.3e}"
);
}
#[test]
fn folding_survives_magnitudes_that_overflow_a_gram() {
let mut factor = Array2::<f64>::zeros((2, 2));
for scale in [1.0e200_f64, 2.0e200] {
let mut v = vec![scale, 0.0];
fold_row_into_triangular_factor(&mut factor.view_mut(), &mut v);
}
assert!(
factor.iter().all(|v| v.is_finite()),
"the triangular factor must stay finite where the Gram would overflow"
);
let expected = (1.0e200_f64).hypot(2.0e200);
assert!(
((factor[[0, 0]] - expected) / expected).abs() <= 1.0e-14,
"σ = {} against {expected}",
factor[[0, 0]]
);
assert!(
!(1.0e200_f64 * 1.0e200).is_finite(),
"the Gram entry does overflow"
);
}
#[test]
fn the_inertia_shift_guard_is_relative_to_the_eigenvalue_not_the_operator() {
let layout = FrameColumnLayout::new(4, &[1]);
let mut roots = Array3::<f64>::zeros((4, 1, 1));
roots[[0, 0, 0]] = 1.0;
roots[[3, 0, 0]] = 1.0e-7;
let dense_rows = Array2::<f64>::from_elem((1, layout.param_dim()), 1.0e-30);
let spectrum =
BlockPlusRowsSpectrum::new(&roots, &dense_rows, &layout).expect("inertia machinery");
assert_eq!(
spectrum.count_above(1.0e-20).expect("count"),
2,
"both the unit eigenvalue and the 1e-14 one are above 1e-20"
);
assert_eq!(spectrum.count_above(1.0e-16).expect("count"), 2);
assert_eq!(spectrum.count_above(1.0e-12).expect("count"), 1);
assert_eq!(spectrum.count_above(2.0).expect("count"), 0);
}
#[test]
fn a_degenerate_layout_yields_an_empty_curvature_rather_than_an_error() {
for axes in [vec![], vec![0usize], vec![1usize, 2]] {
for p in [0usize, 3] {
let layout = FrameColumnLayout::new(p, &axes);
let curvature = OutputBlockRootAccumulator::new(layout.clone()).finish(0);
assert_eq!(curvature.param_dim(), layout.param_dim());
assert!(curvature.is_finite());
let gram = curvature.to_dense_gram();
assert_eq!(gram.dim(), (layout.param_dim(), layout.param_dim()));
assert!(gram.iter().all(|v| *v == 0.0));
}
}
}
#[test]
fn is_finite_refuses_a_nan_in_any_representation() {
let layout = FrameColumnLayout::new(2, &[1]);
let mut roots = Array3::<f64>::zeros((2, 1, 1));
roots[[0, 0, 0]] = 1.0;
let clean = ResidualGaugeCurvature::OutputBlockRoots {
roots: roots.clone(),
dense_rows: Array2::<f64>::zeros((0, layout.param_dim())),
layout: layout.clone(),
root_rows: 3,
};
assert!(clean.is_finite());
roots[[1, 0, 0]] = f64::NAN;
let dirty = ResidualGaugeCurvature::OutputBlockRoots {
roots,
dense_rows: Array2::<f64>::zeros((0, layout.param_dim())),
layout,
root_rows: 3,
};
assert!(!dirty.is_finite());
assert!(
!ResidualGaugeCurvature::DualRoot {
root: Array2::<f64>::from_elem((1, 2), f64::INFINITY),
root_rows: 1,
}
.is_finite()
);
assert!(
!ResidualGaugeCurvature::DenseGram {
gram: Array2::<f64>::from_elem((2, 2), f64::NAN),
root_rows: 1,
}
.is_finite()
);
}
#[test]
fn output_blocks_densify_to_a_block_diagonal_gram() {
let layout = FrameColumnLayout::new(3, &[1, 2]);
let mut roots = Array3::<f64>::zeros((3, 3, 3));
for i in 0..3 {
for a in 0..3 {
for b in a..3 {
roots[[i, a, b]] = (i + 1) as f64 * ((a + 1) + (b + 1)) as f64;
}
}
}
let curvature = ResidualGaugeCurvature::OutputBlockRoots {
roots,
dense_rows: Array2::<f64>::zeros((0, layout.param_dim())),
layout: layout.clone(),
root_rows: 7,
};
let dense = curvature.to_dense_gram();
assert_eq!(dense.dim(), (9, 9));
for a in 0..9 {
for b in 0..9 {
let (ia, ib) = (
layout.output_of(a).expect("in range"),
layout.output_of(b).expect("in range"),
);
if ia != ib {
assert_eq!(dense[[a, b]], 0.0, "off-block ({a},{b}) must be zero");
} else {
assert!(dense[[a, b]] != 0.0, "in-block ({a},{b}) must be populated");
}
}
}
}
}