use super::*;
#[derive(Debug, Clone)]
pub struct SaeArrowVector {
pub t: Array1<f64>,
pub beta: Array1<f64>,
}
pub(crate) struct DeflatedArrowSolver<'a> {
pub(crate) cache: &'a ArrowFactorCache,
pub(crate) gauge_basis: Vec<Array1<f64>>,
pub(crate) gauge_response_physical: Vec<Array1<f64>>,
pub(crate) woodbury_factor: Option<FaerCholeskyFactor>,
pub(crate) gauge_stiffness: f64,
}
impl<'a> DeflatedArrowSolver<'a> {
pub(crate) fn plain(cache: &'a ArrowFactorCache) -> Self {
Self {
cache,
gauge_basis: Vec::new(),
gauge_response_physical: Vec::new(),
woodbury_factor: None,
gauge_stiffness: 0.0,
}
}
pub(crate) fn from_orthonormal_gauges(
cache: &'a ArrowFactorCache,
gauge_basis: Vec<Array1<f64>>,
stiffness: f64,
) -> Result<Self, String> {
if gauge_basis.is_empty() {
return Ok(Self::plain(cache));
}
if !(stiffness.is_finite() && stiffness > 0.0) {
return Err(format!(
"DeflatedArrowSolver: gauge stiffness must be finite and positive; got {stiffness}"
));
}
let full_len = cache.delta_t_len() + cache.k;
let mut gauge_responses = Vec::with_capacity(gauge_basis.len());
for gauge in &gauge_basis {
if gauge.len() != full_len {
return Err(format!(
"DeflatedArrowSolver: gauge length {} != cache full length {full_len}",
gauge.len()
));
}
let (sol_t, sol_beta) = cache
.full_inverse_apply(
gauge.slice(s![..cache.delta_t_len()]),
gauge.slice(s![cache.delta_t_len()..]),
)
.map_err(|err| format!("DeflatedArrowSolver: gauge back-solve: {err}"))?;
gauge_responses.push(flatten_arrow_parts(sol_t.view(), sol_beta.view()));
}
let rank = gauge_basis.len();
let stiffness_recip = stiffness.recip();
let mut gauge_metric = Array2::<f64>::zeros((rank, rank));
let mut woodbury = Array2::<f64>::eye(rank);
for i in 0..rank {
woodbury[[i, i]] *= stiffness_recip;
for j in 0..rank {
let value = gauge_basis[i].dot(&gauge_responses[j]);
gauge_metric[[i, j]] = value;
woodbury[[i, j]] += value;
}
}
let woodbury_factor = woodbury
.cholesky(Side::Lower)
.map_err(|err| format!("DeflatedArrowSolver: gauge Woodbury factor failed: {err}"))?;
let mut gauge_response_physical = gauge_responses;
for j in 0..rank {
for i in 0..rank {
let coeff = gauge_metric[[i, j]];
for row in 0..full_len {
gauge_response_physical[j][row] -= coeff * gauge_basis[i][row];
}
}
}
Ok(Self {
cache,
gauge_basis,
gauge_response_physical,
woodbury_factor: Some(woodbury_factor),
gauge_stiffness: stiffness,
})
}
pub(crate) fn solve(
&self,
rhs_t: ArrayView1<'_, f64>,
rhs_beta: ArrayView1<'_, f64>,
) -> Result<SaeArrowVector, String> {
let (sol_t, sol_beta) = self
.cache
.full_inverse_apply(rhs_t, rhs_beta)
.map_err(|err| format!("DeflatedArrowSolver: full inverse: {err}"))?;
let Some(factor) = self.woodbury_factor.as_ref() else {
return Ok(SaeArrowVector {
t: sol_t,
beta: sol_beta,
});
};
let full_len = self.cache.delta_t_len() + self.cache.k;
let mut flat = flatten_arrow_parts(sol_t.view(), sol_beta.view());
if flat.len() != full_len {
return Err(format!(
"DeflatedArrowSolver: solution length {} != cache full length {full_len}",
flat.len()
));
}
let mut gauge_coeffs = Array1::<f64>::zeros(self.gauge_basis.len());
for (idx, gauge) in self.gauge_basis.iter().enumerate() {
gauge_coeffs[idx] = gauge.dot(&flat);
}
let weights = factor.solvevec(&gauge_coeffs);
for (gauge, &coeff) in self.gauge_basis.iter().zip(gauge_coeffs.iter()) {
for i in 0..flat.len() {
flat[i] -= gauge[i] * coeff;
}
}
for (response, &weight) in self.gauge_response_physical.iter().zip(weights.iter()) {
for i in 0..flat.len() {
flat[i] -= response[i] * weight;
}
}
for (gauge, &weight) in self.gauge_basis.iter().zip(weights.iter()) {
let coeff = self.gauge_stiffness.recip() * weight;
for i in 0..flat.len() {
flat[i] += gauge[i] * coeff;
}
}
Ok(SaeArrowVector {
t: flat.slice(s![..self.cache.delta_t_len()]).to_owned(),
beta: flat.slice(s![self.cache.delta_t_len()..]).to_owned(),
})
}
pub(crate) fn latent_inverse_diagonal_kept(&self) -> Result<Array1<f64>, String> {
let mut out = self.latent_inverse_diagonal()?;
let cache = self.cache;
for (row, dirs) in cache.deflated_row_directions.iter().enumerate() {
if dirs.is_empty() {
continue;
}
let base = cache.row_offsets[row];
for v in dirs {
for s in 0..v.len() {
if base + s < out.len() {
out[base + s] -= v[s] * v[s];
}
}
}
}
Ok(out)
}
pub(crate) fn plain_selected_inverse_available(&self) -> bool {
self.woodbury_factor.is_none()
}
pub(crate) fn beta_inv(&self) -> Result<Array2<f64>, String> {
let k = self.cache.k;
if k == 0 {
return Ok(Array2::<f64>::zeros((0, 0)));
}
self.cache
.schur_inverse_block(0..k)
.map_err(|err| format!("DeflatedArrowSolver::beta_inv: {err}"))
}
pub(crate) fn selected_inverse_row_blocks(
&self,
row: usize,
beta_inv: &Array2<f64>,
) -> Result<(Array2<f64>, Array2<f64>), String> {
let cache = self.cache;
let q = cache.row_dims[row];
let k = cache.k;
let factor = cache.undamped_factor(row);
let mut a_inv = Array2::<f64>::zeros((q, q));
let mut e_j = Array1::<f64>::zeros(q);
for j in 0..q {
e_j.fill(0.0);
e_j[j] = 1.0;
let col = cholesky_solve_vector(factor, e_j.view());
for r in 0..q {
a_inv[[r, j]] = col[r];
}
}
if k == 0 {
return Ok((a_inv, Array2::<f64>::zeros((q, 0))));
}
let mut g = Array2::<f64>::zeros((q, k));
let mut e_c = Array1::<f64>::zeros(k);
let mut b_col = Array1::<f64>::zeros(q);
for c in 0..k {
e_c.fill(0.0);
e_c[c] = 1.0;
b_col.fill(0.0);
if !cache.apply_htbeta_row(row, e_c.view(), &mut b_col) {
return Err(format!(
"DeflatedArrowSolver::selected_inverse_row_blocks: H_tβ^({row}) apply failed"
));
}
let g_col = cholesky_solve_vector(factor, b_col.view());
for r in 0..q {
g[[r, c]] = g_col[r];
}
}
let gs = g.dot(beta_inv);
let inv_vbeta = -&gs;
let mut inv_vv = a_inv;
inv_vv += &gs.dot(&g.t());
Ok((inv_vv, inv_vbeta))
}
pub(crate) fn latent_inverse_diagonal(&self) -> Result<Array1<f64>, String> {
if self.woodbury_factor.is_none() {
return self
.cache
.latent_block_inverse_diagonal()
.map_err(|err| format!("DeflatedArrowSolver: latent inverse diagonal: {err}"));
}
let total_t = self.cache.delta_t_len();
let mut out = Array1::<f64>::zeros(total_t);
let rhs_beta = Array1::<f64>::zeros(self.cache.k);
let mut rhs_t = Array1::<f64>::zeros(total_t);
for idx in 0..total_t {
rhs_t[idx] = 1.0;
let solved = self.solve(rhs_t.view(), rhs_beta.view())?;
rhs_t[idx] = 0.0;
out[idx] = solved.t[idx];
}
Ok(out)
}
}
pub(crate) fn row_selected_inverse_from_probes(
cache: &ArrowFactorCache,
row: usize,
probes: &[Array1<f64>],
sinv_probes: &[Array1<f64>],
want_tbeta: bool,
context: &str,
) -> Result<(Array2<f64>, Array2<f64>), String> {
let q = cache.row_dims[row];
let k = cache.k;
let factor = cache.undamped_factor(row);
let mut inv_vv = Array2::<f64>::zeros((q, q));
let mut unit = Array1::<f64>::zeros(q);
for j in 0..q {
unit.fill(0.0);
unit[j] = 1.0;
let col = cholesky_solve_vector(factor, unit.view());
for r in 0..q {
inv_vv[[r, j]] = col[r];
}
}
let border_cols = if want_tbeta { k } else { 0 };
let mut inv_vbeta = Array2::<f64>::zeros((q, border_cols));
let m = probes.len();
if k == 0 || m == 0 {
return Ok((inv_vv, inv_vbeta));
}
let inv_m = 1.0 / m as f64;
let mut b_tmp = Array1::<f64>::zeros(q);
for l in 0..m {
b_tmp.fill(0.0);
if !cache.apply_htbeta_row(row, probes[l].view(), &mut b_tmp) {
return Err(format!("{context}: H_tβ^({row}) probe apply failed"));
}
let w = cholesky_solve_vector(factor, b_tmp.view());
b_tmp.fill(0.0);
if !cache.apply_htbeta_row(row, sinv_probes[l].view(), &mut b_tmp) {
return Err(format!("{context}: H_tβ^({row}) solve apply failed"));
}
let s = cholesky_solve_vector(factor, b_tmp.view());
for a in 0..q {
for b in 0..q {
inv_vv[[a, b]] += 0.5 * inv_m * (w[a] * s[b] + s[a] * w[b]);
}
}
if want_tbeta {
for a in 0..q {
inv_vbeta.row_mut(a).scaled_add(-inv_m * w[a], &sinv_probes[l]);
}
}
}
Ok((inv_vv, inv_vbeta))
}
#[cfg(test)]
mod selected_inverse_row_blocks_oracle_tests {
use super::*;
use gam_solve::arrow_schur::{
ArrowFactorSlab, ArrowHtbetaCache, ArrowPcgDiagnostics, ArrowSolverMode,
ArrowUndampedFactors,
};
use ndarray::array;
use std::sync::Arc;
fn coupled_arrow_cache() -> ArrowFactorCache {
let htt = ArrowFactorSlab::from_blocks(vec![
array![[1.3_f64, 0.0], [0.4, 1.1]],
array![[0.9_f64]],
]);
let schur = array![[1.2_f64, 0.0], [0.25, 0.95]];
ArrowFactorCache {
htt_factors: htt,
htt_factors_undamped: ArrowUndampedFactors::SameAsDamped,
schur_factor: Some(schur),
schur_factor_is_undamped: true,
beta_schur_conditioning: None,
joint_hessian_log_det: None,
solver_mode: ArrowSolverMode::Direct,
ridge_t: 0.0,
ridge_beta: 0.0,
htbeta: ArrowHtbetaCache::Dense {
blocks: Arc::from(
vec![array![[0.5_f64, -0.2], [0.1, 0.4]], array![[0.3_f64, 0.7]]]
.into_boxed_slice(),
),
estimated_bytes: 0,
},
d: 2,
row_dims: Arc::from(vec![2usize, 1usize].into_boxed_slice()),
row_offsets: Arc::from(vec![0usize, 2usize, 3usize].into_boxed_slice()),
k: 2,
manifold_mode_fingerprint: 0,
row_hessian_fingerprint: 0,
pcg_diagnostics: ArrowPcgDiagnostics::default(),
gauge_deflated_directions: 0,
deflated_row_directions: Arc::from(Vec::new()),
deflation_row_spectra: Arc::from(Vec::new()),
beta_gauge_quotient: None,
}
}
#[test]
fn row_local_blocks_match_per_row_solve() {
let cache = coupled_arrow_cache();
let solver = DeflatedArrowSolver::plain(&cache);
assert!(
solver.plain_selected_inverse_available(),
"plain cache must take the fast selected-inverse path"
);
let total_t = cache.delta_t_len();
let k = cache.k;
let beta_inv = solver.beta_inv().expect("beta_inv");
let rhs_t_zero = Array1::<f64>::zeros(total_t);
for col in 0..k {
let mut rhs_beta = Array1::<f64>::zeros(k);
rhs_beta[col] = 1.0;
let solved = solver
.solve(rhs_t_zero.view(), rhs_beta.view())
.expect("β solve");
for r in 0..k {
assert!(
(beta_inv[[r, col]] - solved.beta[r]).abs() <= 1e-9,
"beta_inv[{r},{col}] {} != solve {}",
beta_inv[[r, col]],
solved.beta[r]
);
}
}
let rhs_beta_zero = Array1::<f64>::zeros(k);
for row in 0..cache.n_rows() {
let q = cache.row_dims[row];
let base = cache.row_offsets[row];
let (inv_vv, inv_vbeta) = solver
.selected_inverse_row_blocks(row, &beta_inv)
.expect("row blocks");
for col in 0..q {
let mut rhs_t = Array1::<f64>::zeros(total_t);
rhs_t[base + col] = 1.0;
let solved = solver
.solve(rhs_t.view(), rhs_beta_zero.view())
.expect("t solve");
for r in 0..q {
assert!(
(inv_vv[[r, col]] - solved.t[base + r]).abs() <= 1e-9,
"inv_vv[{r},{col}] {} != solve {}",
inv_vv[[r, col]],
solved.t[base + r]
);
}
for b in 0..k {
assert!(
(inv_vbeta[[col, b]] - solved.beta[b]).abs() <= 1e-9,
"inv_vbeta[{col},{b}] {} != solve {}",
inv_vbeta[[col, b]],
solved.beta[b]
);
}
}
}
}
}
pub(crate) fn flatten_arrow_parts(
t: ArrayView1<'_, f64>,
beta: ArrayView1<'_, f64>,
) -> Array1<f64> {
let mut out = Array1::<f64>::zeros(t.len() + beta.len());
for i in 0..t.len() {
out[i] = t[i];
}
for i in 0..beta.len() {
out[t.len() + i] = beta[i];
}
out
}
pub(crate) fn apply_cached_arrow_hessian(
cache: &ArrowFactorCache,
v_t: ArrayView1<'_, f64>,
v_beta: ArrayView1<'_, f64>,
) -> Result<SaeArrowVector, String> {
let total_t = cache.delta_t_len();
if v_t.len() != total_t || v_beta.len() != cache.k {
return Err(format!(
"apply_cached_arrow_hessian: vector shapes (t={}, beta={}) != cache shapes \
(t={total_t}, beta={})",
v_t.len(),
v_beta.len(),
cache.k
));
}
let mut out_t = Array1::<f64>::zeros(total_t);
let mut out_beta = Array1::<f64>::zeros(cache.k);
for row in 0..cache.n_rows() {
let di = cache.row_dims[row];
let base = cache.row_offsets[row];
let row_v = v_t.slice(s![base..base + di]);
let factor = cache.undamped_factor(row);
let av = cholesky_factor_apply(factor, row_v);
for j in 0..di {
out_t[base + j] += av[j];
}
if cache.k > 0 {
let mut b_vbeta = Array1::<f64>::zeros(di);
if !cache.apply_htbeta_row(row, v_beta, &mut b_vbeta) {
return Err(format!(
"apply_cached_arrow_hessian: H_tβ^({row}) apply failed"
));
}
for j in 0..di {
out_t[base + j] += b_vbeta[j];
}
if !cache.apply_htbeta_row_transpose(row, row_v, &mut out_beta, None) {
return Err(format!(
"apply_cached_arrow_hessian: H_βt^({row}) apply failed"
));
}
}
}
if cache.k > 0 {
let Some(schur_factor) = cache.schur_factor.as_ref() else {
return Err(
"apply_cached_arrow_hessian: dense Schur factor is required for gauge probing"
.to_string(),
);
};
if !cache.schur_factor_is_undamped {
return Err(
"apply_cached_arrow_hessian: Schur factor was not built from the undamped evidence row factors"
.to_string(),
);
}
let schur_v = cholesky_factor_apply(schur_factor.view(), v_beta);
for i in 0..cache.k {
out_beta[i] += schur_v[i];
}
for row in 0..cache.n_rows() {
let di = cache.row_dims[row];
let mut b_vbeta = Array1::<f64>::zeros(di);
if !cache.apply_htbeta_row(row, v_beta, &mut b_vbeta) {
return Err(format!(
"apply_cached_arrow_hessian: H_tβ^({row}) Schur correction apply failed"
));
}
let a_inv_b_vbeta = cholesky_solve_vector(cache.undamped_factor(row), b_vbeta.view());
if !cache.apply_htbeta_row_transpose(row, a_inv_b_vbeta.view(), &mut out_beta, None) {
return Err(format!(
"apply_cached_arrow_hessian: H_βt^({row}) Schur correction apply failed"
));
}
}
}
Ok(SaeArrowVector {
t: out_t,
beta: out_beta,
})
}
pub(crate) fn apply_raw_cached_arrow_hessian(
cache: &ArrowFactorCache,
v_t: ArrayView1<'_, f64>,
v_beta: ArrayView1<'_, f64>,
) -> Result<SaeArrowVector, String> {
let mut out = apply_cached_arrow_hessian(cache, v_t, v_beta)?;
for row in 0..cache.n_rows() {
let Some(spectrum) = cache
.deflation_row_spectra
.get(row)
.and_then(Option::as_ref)
else {
continue;
};
let q = cache.row_dims[row];
if spectrum.evecs.dim() != (q, q)
|| spectrum.raw_evals.len() != q
|| spectrum.cond_evals.len() != q
{
return Err(format!(
"apply_raw_cached_arrow_hessian: row {row} has dimension {q}, but its \
spectral carrier is {:?} with {} raw and {} conditioned eigenvalues",
spectrum.evecs.dim(),
spectrum.raw_evals.len(),
spectrum.cond_evals.len(),
));
}
let base = cache.row_offsets[row];
let row_v = v_t.slice(s![base..base + q]);
let coefficients = spectrum.evecs.t().dot(&row_v);
let correction_coefficients = Array1::from_iter(
(0..q).map(|axis| {
(spectrum.raw_evals[axis] - spectrum.cond_evals[axis])
* coefficients[axis]
}),
);
let correction = spectrum.evecs.dot(&correction_coefficients);
out.t
.slice_mut(s![base..base + q])
.scaled_add(1.0, &correction);
}
Ok(out)
}
pub(crate) fn cholesky_factor_apply(
factor: ArrayView2<'_, f64>,
vector: ArrayView1<'_, f64>,
) -> Array1<f64> {
let n = factor.nrows();
let mut lt_v = Array1::<f64>::zeros(n);
for j in 0..n {
let vj = vector[j];
if vj == 0.0 {
continue;
}
for (i, &lji) in factor.row(j).iter().enumerate().take(j + 1) {
lt_v[i] += lji * vj;
}
}
let mut out = Array1::<f64>::zeros(n);
for row in 0..n {
let mut acc = 0.0_f64;
for (col, &lrc) in factor.row(row).iter().enumerate().take(row + 1) {
acc += lrc * lt_v[col];
}
out[row] = acc;
}
out
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum SaeLocalRowVar {
Logit { atom: usize },
Coord { atom: usize, axis: usize },
}
#[derive(Debug, Clone)]
pub(crate) struct SaeBorderChannel {
pub(crate) atom: usize,
pub(crate) basis_col: usize,
pub(crate) index: usize,
pub(crate) output: Vec<f64>,
}
#[derive(Debug, Clone)]
pub(crate) struct SaeRowJets {
pub(crate) vars: Vec<SaeLocalRowVar>,
pub(crate) channels: crate::row_jet_program::SaeScheduledRowJets,
}
impl SaeRowJets {
#[inline]
pub(crate) fn first(&self, primary: usize) -> &[f64] {
self.channels.first(primary)
}
#[inline]
pub(crate) fn second(&self, a: usize, b: usize) -> &[f64] {
self.channels.second(a, b)
}
#[inline]
pub(crate) fn beta(&self, border: usize) -> &[f64] {
self.channels.beta(border)
}
#[inline]
pub(crate) fn beta_deriv(&self, primary: usize, border: usize) -> &[f64] {
self.channels.beta_deriv(primary, border)
}
#[inline]
pub(crate) fn beta_l_deriv(&self, primary: usize, border: usize) -> &[f64] {
self.channels.beta_l_deriv(primary, border)
}
}
pub(crate) fn sae_dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()
}
pub(crate) fn sae_inner(a: &SaeArrowVector, b: &SaeArrowVector) -> f64 {
sae_dot(a.t.as_slice().unwrap_or(&[]), b.t.as_slice().unwrap_or(&[]))
+ sae_dot(
a.beta.as_slice().unwrap_or(&[]),
b.beta.as_slice().unwrap_or(&[]),
)
}
pub(crate) fn sae_norm(a: &SaeArrowVector) -> f64 {
sae_inner(a, a).max(0.0).sqrt()
}
fn admitted_gmres_restart_with_budget(dim: usize, budget: usize) -> Result<usize, String> {
let storage_bytes = |m: usize| -> Option<usize> {
let basis = m.checked_add(1)?.checked_mul(dim)?;
let preconditioned_directions = m.checked_mul(dim)?;
let hessenberg = m.checked_add(1)?.checked_mul(m)?;
let fixed = dim.checked_mul(6)?.checked_add(m.checked_mul(4)?)?;
basis
.checked_add(preconditioned_directions)?
.checked_add(hessenberg)?
.checked_add(fixed)?
.checked_mul(std::mem::size_of::<f64>())
};
let minimum = storage_bytes(1).ok_or_else(|| {
format!("solve_b_preconditioned_gmres: storage size overflow for dimension {dim}")
})?;
if minimum > budget {
return Err(format!(
"solve_b_preconditioned_gmres: even one Arnoldi direction needs {minimum} bytes, \
exceeding the cgroup-aware Krylov budget {budget}"
));
}
let mut low = 1usize;
let mut high = dim;
while low < high {
let mid = low + (high - low).div_ceil(2);
if storage_bytes(mid).is_some_and(|bytes| bytes <= budget) {
low = mid;
} else {
high = mid - 1;
}
}
Ok(low)
}
fn admitted_gmres_restart(dim: usize) -> Result<usize, String> {
let (budget, available) = sae_host_in_core_budget_bytes();
admitted_gmres_restart_with_budget(dim, budget)
.map_err(|reason| format!("{reason} (available {available})"))
}
fn reorthogonalize_arnoldi_column(
basis: &[Array1<f64>],
w: &mut Array1<f64>,
h: &mut Array2<f64>,
column: usize,
) {
for _ in 0..2 {
for i in 0..=column {
let correction = basis[i].dot(w);
h[[i, column]] += correction;
for slot in 0..w.len() {
w[slot] -= correction * basis[i][slot];
}
}
}
}
fn physical_krylov_least_squares(
residual: &Array1<f64>,
operator_images: &[Array1<f64>],
) -> Result<Array1<f64>, String> {
if operator_images.is_empty() {
return Err("solve_b_preconditioned_gmres: empty Krylov image space".to_string());
}
let dim = residual.len();
let directions = operator_images.len();
let mut design = Array2::<f64>::zeros((dim, directions));
for (column, image) in operator_images.iter().enumerate() {
if image.len() != dim || image.iter().any(|value| !value.is_finite()) {
return Err(format!(
"solve_b_preconditioned_gmres: invalid physical operator image {column}"
));
}
design.column_mut(column).assign(image);
}
let (u_opt, singular_values, vt_opt) = design
.svd(true, true)
.map_err(|error| format!("solve_b_preconditioned_gmres: physical SVD failed: {error}"))?;
let u = u_opt.ok_or_else(|| {
"solve_b_preconditioned_gmres: physical SVD omitted left vectors".to_string()
})?;
let vt = vt_opt.ok_or_else(|| {
"solve_b_preconditioned_gmres: physical SVD omitted right vectors".to_string()
})?;
let largest = singular_values
.iter()
.fold(0.0_f64, |current, &value| current.max(value));
if !(largest.is_finite() && largest > 0.0) {
return Err(
"solve_b_preconditioned_gmres: physical Krylov image has zero numerical rank"
.to_string(),
);
}
let rank_floor =
largest * f64::EPSILON * (design.nrows().max(design.ncols()) as f64);
let projected = u.t().dot(residual);
let mut scaled = Array1::<f64>::zeros(singular_values.len());
for index in 0..singular_values.len() {
if singular_values[index] > rank_floor {
scaled[index] = projected[index] / singular_values[index];
}
}
let coefficients = vt.t().dot(&scaled);
if coefficients.iter().any(|value| !value.is_finite()) {
return Err(
"solve_b_preconditioned_gmres: physical Krylov coefficients are non-finite"
.to_string(),
);
}
Ok(coefficients)
}
#[cfg(test)]
mod gmres_restart_budget_tests {
use super::admitted_gmres_restart_with_budget;
fn saturated_bytes(dim: usize) -> usize {
24 * dim * dim + 96 * dim
}
#[test]
fn gmres_restart_saturates_below_the_binding_dimension() {
for dim in [26usize, 200, 2_048] {
let budget = saturated_bytes(dim);
assert_eq!(
admitted_gmres_restart_with_budget(dim, budget),
Ok(dim),
"dim={dim} must take full unrestarted GMRES when its whole basis fits"
);
let restart = admitted_gmres_restart_with_budget(dim, budget - 1)
.expect("one Arnoldi direction still fits");
assert!(
restart < dim,
"dim={dim} must restart below dim once its full basis no longer fits, got {restart}"
);
}
}
#[test]
fn gmres_restart_refuses_when_a_single_direction_does_not_fit() {
let error = admitted_gmres_restart_with_budget(1_000, 8)
.expect_err("8 bytes cannot hold one Arnoldi direction at dim=1000");
assert!(
error.contains("even one Arnoldi direction"),
"the refusal must name the storage it could not afford, got {error}"
);
}
}
pub(crate) fn solve_b_preconditioned_gmres_with<F, P>(
rhs: &SaeArrowVector,
apply_a: F,
precondition: P,
) -> Result<SaeArrowVector, String>
where
F: Fn(&SaeArrowVector) -> Result<SaeArrowVector, String>,
P: Fn(&SaeArrowVector) -> Result<SaeArrowVector, String>,
{
let initial = SaeArrowVector {
t: Array1::zeros(rhs.t.len()),
beta: Array1::zeros(rhs.beta.len()),
};
solve_b_preconditioned_gmres_from(rhs, &initial, apply_a, precondition)
.map(|(solution, _iterations)| solution)
}
pub(crate) fn solve_b_preconditioned_gmres_from<F, P>(
rhs: &SaeArrowVector,
initial: &SaeArrowVector,
apply_a: F,
precondition: P,
) -> Result<(SaeArrowVector, usize), String>
where
F: Fn(&SaeArrowVector) -> Result<SaeArrowVector, String>,
P: Fn(&SaeArrowVector) -> Result<SaeArrowVector, String>,
{
let t_len = rhs.t.len();
let beta_len = rhs.beta.len();
if initial.t.len() != t_len || initial.beta.len() != beta_len {
return Err(format!(
"solve_b_preconditioned_gmres: initial dimensions ({}, {}) do not match rhs ({t_len}, {beta_len})",
initial.t.len(),
initial.beta.len(),
));
}
let dim = t_len + beta_len;
if dim == 0 {
return Ok((
SaeArrowVector {
t: Array1::zeros(0),
beta: Array1::zeros(0),
},
0,
));
}
let rhs_flat = flatten_arrow_parts(rhs.t.view(), rhs.beta.view());
let rhs_norm = rhs_flat.dot(&rhs_flat).sqrt();
if rhs_norm == 0.0 {
return Ok((
SaeArrowVector {
t: Array1::zeros(t_len),
beta: Array1::zeros(beta_len),
},
0,
));
}
if !rhs_norm.is_finite() {
return Err("solve_b_preconditioned_gmres: non-finite right-hand side".to_string());
}
let b = rhs_flat;
let b_norm = rhs_norm;
let relative_floor = f64::EPSILON.sqrt();
const RESTART_CYCLE_CEILING: usize = 256;
let restart = admitted_gmres_restart(dim)?;
let started = std::time::Instant::now();
let mut iterations = 0usize;
let mut cycles = 0usize;
let mut best_contraction = 1.0_f64;
let mut solution = flatten_arrow_parts(initial.t.view(), initial.beta.view());
if solution.iter().any(|value| !value.is_finite()) {
return Err("solve_b_preconditioned_gmres: non-finite initial solution".to_string());
}
let as_arrow = |flat: &Array1<f64>| SaeArrowVector {
t: flat.slice(s![..t_len]).to_owned(),
beta: flat.slice(s![t_len..]).to_owned(),
};
let apply_preconditioner = |flat: &Array1<f64>| -> Result<Array1<f64>, String> {
let direction = as_arrow(flat);
let preconditioned = precondition(&direction)
.map_err(|err| format!("solve_b_preconditioned_gmres: B inverse: {err}"))?;
Ok(flatten_arrow_parts(
preconditioned.t.view(),
preconditioned.beta.view(),
))
};
let apply_operator = |flat: &Array1<f64>| -> Result<Array1<f64>, String> {
let physical = as_arrow(flat);
let applied = apply_a(&physical)?;
Ok(flatten_arrow_parts(applied.t.view(), applied.beta.view()))
};
loop {
let ax = apply_operator(&solution)?;
let mut residual = &b - &ax;
let residual_norm = residual.dot(&residual).sqrt();
if residual_norm <= relative_floor * b_norm {
let candidate = as_arrow(&solution);
let ax = apply_a(&candidate)?;
let original = SaeArrowVector {
t: &rhs.t - &ax.t,
beta: &rhs.beta - &ax.beta,
};
let original_norm = sae_norm(&original);
if original_norm <= relative_floor * rhs_norm {
return Ok((candidate, iterations));
}
}
if !(residual_norm.is_finite() && residual_norm > 0.0) {
return Err("solve_b_preconditioned_gmres: non-finite original residual".to_string());
}
let cycle_residual = residual.clone();
residual.mapv_inplace(|value| value / residual_norm);
let cycle = restart;
let mut basis: Vec<Array1<f64>> = Vec::with_capacity(cycle + 1);
basis.push(residual);
let mut preconditioned_basis: Vec<Array1<f64>> = Vec::with_capacity(cycle);
let mut operator_images: Vec<Array1<f64>> = Vec::with_capacity(cycle);
let mut h = Array2::<f64>::zeros((cycle + 1, cycle));
let mut cosines = vec![0.0_f64; cycle];
let mut sines = vec![0.0_f64; cycle];
let mut g = Array1::<f64>::zeros(cycle + 1);
g[0] = residual_norm;
let mut used = 0usize;
for j in 0..cycle {
let preconditioned_direction = apply_preconditioner(&basis[j])?;
if !preconditioned_direction
.iter()
.all(|value| value.is_finite())
{
return Err(format!(
"solve_b_preconditioned_gmres: non-finite preconditioned direction at \
iteration {}",
iterations + j
));
}
let mut w = apply_operator(&preconditioned_direction)?;
operator_images.push(w.clone());
preconditioned_basis.push(preconditioned_direction);
reorthogonalize_arnoldi_column(&basis, &mut w, &mut h, j);
let next_norm = w.dot(&w).sqrt();
let arnoldi_space_closed = next_norm <= f64::EPSILON;
h[[j + 1, j]] = next_norm;
if !arnoldi_space_closed {
w.mapv_inplace(|value| value / next_norm);
basis.push(w);
} else {
basis.push(Array1::zeros(dim));
}
for i in 0..j {
let upper = cosines[i] * h[[i, j]] + sines[i] * h[[i + 1, j]];
let lower = -sines[i] * h[[i, j]] + cosines[i] * h[[i + 1, j]];
h[[i, j]] = upper;
h[[i + 1, j]] = lower;
}
let diagonal = h[[j, j]];
let below = h[[j + 1, j]];
let radius = diagonal.hypot(below);
if !(radius.is_finite() && radius > f64::EPSILON) {
return Err(format!(
"solve_b_preconditioned_gmres: Arnoldi breakdown after {} iterations",
iterations + j
));
}
cosines[j] = diagonal / radius;
sines[j] = below / radius;
h[[j, j]] = radius;
h[[j + 1, j]] = 0.0;
let gj = g[j];
g[j] = cosines[j] * gj;
g[j + 1] = -sines[j] * gj;
used = j + 1;
iterations = iterations.checked_add(1).ok_or_else(|| {
"solve_b_preconditioned_gmres: iteration counter overflow".to_string()
})?;
if iterations.is_power_of_two() {
log::info!(
"[SAE-GMRES] dim={dim} restart={restart} iter={iterations} \
rel_residual={:.3e} target={:.3e} elapsed={:.1}s",
g[j + 1].abs() / b_norm,
relative_floor,
started.elapsed().as_secs_f64(),
);
}
if arnoldi_space_closed {
break;
}
}
let y = physical_krylov_least_squares(&cycle_residual, &operator_images[..used])?;
let mut represented_residual = cycle_residual;
for i in 0..used {
for slot in 0..dim {
solution[slot] += y[i] * preconditioned_basis[i][slot];
represented_residual[slot] -= y[i] * operator_images[i][slot];
}
}
let represented_norm = represented_residual.dot(&represented_residual).sqrt();
let candidate = as_arrow(&solution);
let ax = apply_a(&candidate)?;
let original = SaeArrowVector {
t: &rhs.t - &ax.t,
beta: &rhs.beta - &ax.beta,
};
let original_norm = sae_norm(&original);
let roundoff_floor = relative_floor * rhs_norm;
cycles += 1;
log::info!(
"[SAE-GMRES] cycle {cycles} closed: dim={dim} restart={restart} iters={iterations} \
rel_original_residual={:.3e} floor={:.3e} elapsed={:.1}s",
original_norm / rhs_norm,
roundoff_floor / rhs_norm,
started.elapsed().as_secs_f64(),
);
if original_norm <= roundoff_floor {
return Ok((candidate, iterations));
}
let next_ax = apply_operator(&solution)?;
let next_residual = &b - &next_ax;
let next_norm = next_residual.dot(&next_residual).sqrt();
if !(next_norm.is_finite() && next_norm < residual_norm) {
return Err(format!(
"solve_b_preconditioned_gmres: no representable residual reduction after \
{iterations} iterations (restart {restart}, dimension {dim}); original \
residual {residual_norm:.3e} -> {next_norm:.3e}, relative residual \
{:.3e}, stored-operator residual {represented_norm:.3e}, round-off \
certification floor {:.3e}",
original_norm / rhs_norm,
roundoff_floor / rhs_norm,
));
}
best_contraction = best_contraction.min(next_norm / residual_norm);
if cycles >= RESTART_CYCLE_CEILING {
return Err(format!(
"solve_b_preconditioned_gmres: restart anti-runaway ceiling \
{RESTART_CYCLE_CEILING} cycles reached after {iterations} iterations \
(restart {restart}, dimension {dim}); relative original residual \
{:.3e} against round-off certification floor {:.3e}, best per-cycle \
contraction {best_contraction:.9}",
original_norm / rhs_norm,
roundoff_floor / rhs_norm,
));
}
if best_contraction > 0.0 && best_contraction < 1.0 && next_norm > roundoff_floor {
let projected = (roundoff_floor / next_norm).ln() / best_contraction.ln();
if projected.is_finite() && projected > 0.0 {
let remaining = (RESTART_CYCLE_CEILING - cycles) as f64;
if projected.ceil() > remaining {
return Err(format!(
"solve_b_preconditioned_gmres: restarted residual contracts too \
slowly to certify — best measured per-cycle contraction \
{best_contraction:.9} projects {projected:.1} further cycles to \
reach the round-off floor, against {remaining} left under the \
{RESTART_CYCLE_CEILING}-cycle anti-runaway ceiling (restart \
{restart}, dimension {dim}, {iterations} iterations); relative \
original residual {:.3e}, floor {:.3e}",
original_norm / rhs_norm,
roundoff_floor / rhs_norm,
));
}
}
}
}
}
#[cfg(test)]
mod right_preconditioned_gmres_tests {
use super::*;
use ndarray::array;
#[test]
fn reorthogonalizes_nearly_dependent_arnoldi_column_before_residual_projection_2653() {
let dim = 7usize;
let q = Array1::from_elem(dim, (dim as f64).sqrt().recip());
let mut transverse = Array1::from_iter((-3..=3).map(f64::from));
let transverse_norm = transverse.dot(&transverse).sqrt();
transverse.mapv_inplace(|value| value / transverse_norm);
let original = &q + &(1.0e-12 * &transverse);
let mut once = original.clone();
let once_projection = q.dot(&once);
for slot in 0..dim {
once[slot] -= once_projection * q[slot];
}
let once_norm = once.dot(&once).sqrt();
once.mapv_inplace(|value| value / once_norm);
let one_pass_overlap = q.dot(&once).abs();
assert!(
one_pass_overlap > 1.0e-5,
"fixture must expose the one-pass Arnoldi residual gap, overlap={one_pass_overlap:.3e}"
);
let basis = vec![q];
let mut twice = original;
let mut h = Array2::<f64>::zeros((2, 1));
reorthogonalize_arnoldi_column(&basis, &mut twice, &mut h, 0);
let twice_norm = twice.dot(&twice).sqrt();
twice.mapv_inplace(|value| value / twice_norm);
let two_pass_overlap = basis[0].dot(&twice).abs();
assert!(
two_pass_overlap <= 64.0 * f64::EPSILON,
"reorthogonalized Arnoldi residual must remain physical, overlap={two_pass_overlap:.3e}"
);
}
#[test]
fn exact_physical_warm_start_returns_without_an_arnoldi_step_2515() {
let rhs = SaeArrowVector {
t: array![5.0_f64, 5.0],
beta: Array1::zeros(0),
};
let exact = SaeArrowVector {
t: array![1.0_f64, 1.0],
beta: Array1::zeros(0),
};
let apply_a = |value: &SaeArrowVector| -> Result<SaeArrowVector, String> {
Ok(SaeArrowVector {
t: array![
3.0 * value.t[0] + 2.0 * value.t[1],
value.t[0] + 4.0 * value.t[1],
],
beta: Array1::zeros(0),
})
};
let identity =
|value: &SaeArrowVector| -> Result<SaeArrowVector, String> { Ok(value.clone()) };
let (solved, iterations) =
solve_b_preconditioned_gmres_from(&rhs, &exact, apply_a, identity)
.expect("exact warm start");
assert_eq!(iterations, 0, "an exact warm start must do no Arnoldi work");
assert_eq!(solved.t, exact.t);
assert_eq!(solved.beta, exact.beta);
}
#[test]
fn certifies_original_residual_under_ill_scaled_preconditioner_2258() {
let rhs = SaeArrowVector {
t: array![5.0_f64, 5.0],
beta: Array1::zeros(0),
};
let apply_a = |value: &SaeArrowVector| -> Result<SaeArrowVector, String> {
Ok(SaeArrowVector {
t: array![
3.0 * value.t[0] + 2.0 * value.t[1],
value.t[0] + 4.0 * value.t[1],
],
beta: Array1::zeros(0),
})
};
let precondition = |value: &SaeArrowVector| -> Result<SaeArrowVector, String> {
Ok(SaeArrowVector {
t: array![1.0e-3 * value.t[0], 1.0e3 * value.t[1]],
beta: Array1::zeros(0),
})
};
let solved = solve_b_preconditioned_gmres_with(&rhs, apply_a, precondition)
.expect("right-preconditioned solve");
let applied = apply_a(&solved).expect("physical operator");
let residual = SaeArrowVector {
t: &rhs.t - &applied.t,
beta: Array1::zeros(0),
};
assert!(
sae_norm(&residual) <= f64::EPSILON.sqrt() * sae_norm(&rhs),
"physical residual was not certified: relative={:.3e}",
sae_norm(&residual) / sae_norm(&rhs),
);
assert!((solved.t[0] - 1.0).abs() <= f64::EPSILON.sqrt());
assert!((solved.t[1] - 1.0).abs() <= f64::EPSILON.sqrt());
}
#[test]
fn supports_non_linear_adaptive_preconditioner_2258() {
let rhs = SaeArrowVector {
t: array![3.0_f64, 4.0],
beta: Array1::zeros(0),
};
let apply_a =
|value: &SaeArrowVector| -> Result<SaeArrowVector, String> { Ok(value.clone()) };
let precondition = |value: &SaeArrowVector| -> Result<SaeArrowVector, String> {
let norm = sae_norm(value);
if norm == 0.0 {
return Ok(value.clone());
}
Ok(SaeArrowVector {
t: &value.t / norm,
beta: &value.beta / norm,
})
};
let solved = solve_b_preconditioned_gmres_with(&rhs, apply_a, precondition)
.expect("flexible right-preconditioned solve");
let residual = SaeArrowVector {
t: &rhs.t - &solved.t,
beta: &rhs.beta - &solved.beta,
};
assert!(
sae_norm(&residual) <= f64::EPSILON.sqrt() * sae_norm(&rhs),
"adaptive inverse must certify the physical equation: relative={:.3e}",
sae_norm(&residual) / sae_norm(&rhs),
);
}
}