fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn norm(v: &[f64]) -> f64 {
dot(v, v).sqrt()
}
fn vec_sub(a: &[f64], b: &[f64]) -> Vec<f64> {
a.iter().zip(b.iter()).map(|(x, y)| x - y).collect()
}
fn vec_add(a: &[f64], b: &[f64]) -> Vec<f64> {
a.iter().zip(b.iter()).map(|(x, y)| x + y).collect()
}
fn vec_scale(v: &[f64], s: f64) -> Vec<f64> {
v.iter().map(|x| x * s).collect()
}
fn mat_vec(a: &[Vec<f64>], x: &[f64]) -> Vec<f64> {
a.iter().map(|row| dot(row, x)).collect()
}
fn mat_transpose(a: &[Vec<f64>]) -> Vec<Vec<f64>> {
if a.is_empty() {
return vec![];
}
let m = a.len();
let n = a[0].len();
let mut t = vec![vec![0.0; m]; n];
for i in 0..m {
for j in 0..n {
t[j][i] = a[i][j];
}
}
t
}
fn mat_mul(a: &[Vec<f64>], b: &[Vec<f64>]) -> Vec<Vec<f64>> {
if a.is_empty() || b.is_empty() {
return vec![];
}
let m = a.len();
let n = b[0].len();
let bt = mat_transpose(b);
let mut c = vec![vec![0.0; n]; m];
for i in 0..m {
for j in 0..n {
c[i][j] = dot(&a[i], &bt[j]);
}
}
c
}
#[cfg(test)]
fn back_substitute(u: &[Vec<f64>], b: &[f64]) -> Vec<f64> {
let n = b.len();
let mut x = vec![0.0; n];
for i in (0..n).rev() {
let mut s = b[i];
for j in (i + 1)..n {
s -= u[i][j] * x[j];
}
x[i] = if u[i][i].abs() < 1e-14 {
0.0
} else {
s / u[i][i]
};
}
x
}
fn forward_substitute(l: &[Vec<f64>], b: &[f64]) -> Vec<f64> {
let n = b.len();
let mut x = vec![0.0; n];
for i in 0..n {
let mut s = b[i];
for j in 0..i {
s -= l[i][j] * x[j];
}
x[i] = if l[i][i].abs() < 1e-14 {
0.0
} else {
s / l[i][i]
};
}
x
}
fn cg_solve(a: &[Vec<f64>], b: &[f64], max_iter: usize, tol: f64) -> Vec<f64> {
let n = b.len();
let mut x = vec![0.0; n];
let mut r = b.to_vec();
let mut p = r.clone();
let mut rr = dot(&r, &r);
for _ in 0..max_iter {
if rr.sqrt() < tol {
break;
}
let ap = mat_vec(a, &p);
let alpha = rr / dot(&p, &ap);
x = vec_add(&x, &vec_scale(&p, alpha));
r = vec_sub(&r, &vec_scale(&ap, alpha));
let rr_new = dot(&r, &r);
let beta = rr_new / rr;
p = vec_add(&r, &vec_scale(&p, beta));
rr = rr_new;
}
x
}
fn eye(n: usize) -> Vec<Vec<f64>> {
let mut m = vec![vec![0.0; n]; n];
for (i, row) in m.iter_mut().enumerate().take(n) {
row[i] = 1.0;
}
m
}
#[derive(Clone, Debug)]
pub struct PodBasis {
pub modes: Vec<Vec<f64>>,
pub singular_values: Vec<f64>,
pub full_dim: usize,
pub reduced_dim: usize,
}
impl PodBasis {
pub fn new(full_dim: usize) -> Self {
Self {
modes: vec![],
singular_values: vec![],
full_dim,
reduced_dim: 0,
}
}
pub fn rank(&self) -> usize {
self.modes.len()
}
pub fn project(&self, q: &[f64]) -> Vec<f64> {
self.modes.iter().map(|phi| dot(phi, q)).collect()
}
pub fn reconstruct(&self, q_r: &[f64]) -> Vec<f64> {
let n = self.full_dim;
let mut q = vec![0.0; n];
for (phi, &coeff) in self.modes.iter().zip(q_r.iter()) {
for (qi, &phi_i) in q.iter_mut().zip(phi.iter()) {
*qi += coeff * phi_i;
}
}
q
}
pub fn energy_content(&self) -> f64 {
let total: f64 = self.singular_values.iter().map(|s| s * s).sum();
if total < 1e-300 {
return 0.0;
}
let retained: f64 = self.singular_values[..self.reduced_dim]
.iter()
.map(|s| s * s)
.sum();
retained / total
}
}
pub fn build_pod_basis(snapshots: &[Vec<f64>], max_modes: usize) -> PodBasis {
let n_snap = snapshots.len();
if n_snap == 0 || snapshots[0].is_empty() {
return PodBasis::new(0);
}
let n = snapshots[0].len();
let mut gram = vec![vec![0.0; n_snap]; n_snap];
for i in 0..n_snap {
for j in i..n_snap {
let v = dot(&snapshots[i], &snapshots[j]);
gram[i][j] = v;
gram[j][i] = v;
}
}
let r = max_modes.min(n_snap).min(n);
let mut eigenvecs: Vec<Vec<f64>> = Vec::with_capacity(r);
let mut eigenvals: Vec<f64> = Vec::with_capacity(r);
let mut deflated = gram.clone();
for _k in 0..r {
let pivot = (0..n_snap)
.max_by(|&a, &b| {
deflated[a][a]
.abs()
.partial_cmp(&deflated[b][b].abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(0);
let mut v: Vec<f64> = deflated.iter().map(|row| row[pivot]).collect();
let vn = norm(&v);
if vn < 1e-14 {
break;
}
for x in v.iter_mut() {
*x /= vn;
}
for _iter in 0..200 {
let av = mat_vec(&deflated, &v);
let lambda = dot(&v, &av);
let vn2 = norm(&av);
if vn2 < 1e-14 {
break;
}
let v_new: Vec<f64> = av.iter().map(|x| x / vn2).collect();
let diff: f64 = vec_sub(&v_new, &v)
.iter()
.map(|x| x.abs())
.fold(0.0_f64, f64::max);
v = v_new;
if diff < 1e-12 {
let av2 = mat_vec(&deflated, &v);
let _lam = dot(&v, &av2);
let _ = lambda;
break;
}
}
let av = mat_vec(&deflated, &v);
let lambda = dot(&v, &av).max(0.0);
let sigma = lambda.sqrt();
if sigma < 1e-14 {
break;
}
eigenvals.push(sigma);
eigenvecs.push(v.clone());
for i in 0..n_snap {
for j in 0..n_snap {
deflated[i][j] -= lambda * v[i] * v[j];
}
}
}
let mut modes: Vec<Vec<f64>> = Vec::with_capacity(eigenvals.len());
let mut singular_values: Vec<f64> = Vec::with_capacity(eigenvals.len());
for (v, sigma) in eigenvecs.iter().zip(eigenvals.iter()) {
let mut phi = vec![0.0; n];
for (snap, &coeff) in snapshots.iter().zip(v.iter()) {
for (phi_i, &s_i) in phi.iter_mut().zip(snap.iter()) {
*phi_i += coeff * s_i;
}
}
let pn = norm(&phi);
if pn < 1e-14 {
continue;
}
for x in phi.iter_mut() {
*x /= pn;
}
modes.push(phi);
singular_values.push(*sigma);
}
let reduced_dim = modes.len();
PodBasis {
modes,
singular_values,
full_dim: n,
reduced_dim,
}
}
#[derive(Clone, Debug)]
pub struct GalerkinSystem {
pub k_reduced: Vec<Vec<f64>>,
pub f_reduced: Vec<f64>,
pub reduced_dim: usize,
}
impl GalerkinSystem {
pub fn solve(&self) -> Vec<f64> {
cg_solve(&self.k_reduced, &self.f_reduced, 500, 1e-10)
}
}
pub fn galerkin_projection(
k_full: &[Vec<f64>],
f_full: &[f64],
basis: &PodBasis,
) -> GalerkinSystem {
let r = basis.rank();
let k_phi: Vec<Vec<f64>> = basis.modes.iter().map(|phi| mat_vec(k_full, phi)).collect();
let mut k_reduced = vec![vec![0.0; r]; r];
for (i, row) in k_reduced.iter_mut().enumerate().take(r) {
for (j, cell) in row.iter_mut().enumerate().take(r) {
*cell = dot(&basis.modes[i], &k_phi[j]);
}
}
let f_reduced: Vec<f64> = basis.modes.iter().map(|phi| dot(phi, f_full)).collect();
GalerkinSystem {
k_reduced,
f_reduced,
reduced_dim: r,
}
}
#[derive(Clone, Debug)]
pub struct ReducedBasis {
pub basis: Vec<Vec<f64>>,
pub selected_params: Vec<f64>,
pub full_dim: usize,
}
impl ReducedBasis {
pub fn new(full_dim: usize) -> Self {
Self {
basis: vec![],
selected_params: vec![],
full_dim,
}
}
pub fn dim(&self) -> usize {
self.basis.len()
}
pub fn add_snapshot(&mut self, snap: &[f64], param: f64) {
let mut v = snap.to_vec();
for phi in &self.basis {
let c = dot(&v, phi);
v = vec_sub(&v, &vec_scale(phi, c));
}
let n = norm(&v);
if n < 1e-12 {
return;
}
self.basis.push(vec_scale(&v, 1.0 / n));
self.selected_params.push(param);
}
pub fn project(&self, q: &[f64]) -> Vec<f64> {
self.basis.iter().map(|phi| dot(phi, q)).collect()
}
pub fn reconstruct(&self, q_r: &[f64]) -> Vec<f64> {
let mut q = vec![0.0; self.full_dim];
for (phi, &coeff) in self.basis.iter().zip(q_r.iter()) {
for (qi, &phi_i) in q.iter_mut().zip(phi.iter()) {
*qi += coeff * phi_i;
}
}
q
}
}
pub fn greedy_reduced_basis(
params: &[f64],
snapshots: &[Vec<f64>],
max_basis: usize,
tol: f64,
) -> ReducedBasis {
if snapshots.is_empty() {
return ReducedBasis::new(0);
}
let n = snapshots[0].len();
let mut rb = ReducedBasis::new(n);
let (best_idx, _) = snapshots
.iter()
.enumerate()
.map(|(i, s)| (i, norm(s)))
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or((0, 0.0));
rb.add_snapshot(&snapshots[best_idx], params[best_idx]);
for _k in 1..max_basis {
let mut max_err = 0.0_f64;
let mut max_i = 0;
for (i, snap) in snapshots.iter().enumerate() {
let q_r = rb.project(snap);
let recon = rb.reconstruct(&q_r);
let err = norm(&vec_sub(snap, &recon));
if err > max_err {
max_err = err;
max_i = i;
}
}
if max_err < tol {
break;
}
rb.add_snapshot(&snapshots[max_i], params[max_i]);
}
rb
}
#[derive(Clone, Debug)]
pub struct DeimInterpolation {
pub indices: Vec<usize>,
pub p_matrix: Vec<Vec<f64>>,
pub modes: Vec<Vec<f64>>,
}
impl DeimInterpolation {
pub fn approximate(&self, f_full: &[f64]) -> Vec<f64> {
let f_s: Vec<f64> = self.indices.iter().map(|&i| f_full[i]).collect();
let c = mat_vec(&self.p_matrix, &f_s);
let n = if self.modes.is_empty() {
0
} else {
self.modes[0].len()
};
let mut approx = vec![0.0; n];
for (phi, &coeff) in self.modes.iter().zip(c.iter()) {
for (a, &b) in approx.iter_mut().zip(phi.iter()) {
*a += coeff * b;
}
}
approx
}
}
pub fn deim(modes: &[Vec<f64>]) -> DeimInterpolation {
if modes.is_empty() {
return DeimInterpolation {
indices: vec![],
p_matrix: vec![],
modes: vec![],
};
}
let _n = modes[0].len();
let r = modes.len();
let mut indices: Vec<usize> = Vec::with_capacity(r);
let first_idx = modes[0]
.iter()
.enumerate()
.max_by(|a, b| {
a.1.abs()
.partial_cmp(&b.1.abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.unwrap_or(0);
indices.push(first_idx);
for l in 1..r {
let u_l = &modes[l];
let b: Vec<f64> = indices.iter().map(|&i| u_l[i]).collect();
let mut u_s = vec![vec![0.0; l]; l];
for (row, &idx) in indices.iter().enumerate() {
for (col, mode) in modes[..l].iter().enumerate() {
u_s[row][col] = mode[idx];
}
}
let c = forward_substitute(&u_s, &b);
let mut residual = u_l.clone();
for (mode, &coeff) in modes[..l].iter().zip(c.iter()) {
for (ri, &mi) in residual.iter_mut().zip(mode.iter()) {
*ri -= coeff * mi;
}
}
let new_idx = residual
.iter()
.enumerate()
.max_by(|a, b| {
a.1.abs()
.partial_cmp(&b.1.abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.unwrap_or(0);
indices.push(new_idx);
}
let m = indices.len();
let u_s: Vec<Vec<f64>> = indices
.iter()
.map(|&i| modes.iter().map(|mode| mode[i]).collect::<Vec<f64>>())
.collect();
let mut p_matrix = vec![vec![0.0; m]; r]; let eye_m = eye(m);
for k in 0..m {
let col = forward_substitute(&u_s, &eye_m[k]);
for i in 0..r.min(col.len()) {
p_matrix[i][k] = col[i];
}
}
DeimInterpolation {
indices,
p_matrix,
modes: modes.to_vec(),
}
}
#[derive(Clone, Debug)]
pub struct PetrovGalerkinSystem {
pub k_reduced: Vec<Vec<f64>>,
pub f_reduced: Vec<f64>,
pub reduced_dim: usize,
}
impl PetrovGalerkinSystem {
pub fn solve(&self) -> Vec<f64> {
cg_solve(&self.k_reduced, &self.f_reduced, 500, 1e-10)
}
}
pub fn petrov_galerkin_projection(
k_full: &[Vec<f64>],
f_full: &[f64],
phi: &[Vec<f64>],
psi: &[Vec<f64>],
) -> PetrovGalerkinSystem {
let r = phi.len().min(psi.len());
let k_phi: Vec<Vec<f64>> = phi.iter().map(|p| mat_vec(k_full, p)).collect();
let mut k_reduced = vec![vec![0.0; r]; r];
for i in 0..r {
for j in 0..r {
k_reduced[i][j] = dot(&psi[i], &k_phi[j]);
}
}
let f_reduced: Vec<f64> = psi.iter().map(|p| dot(p, f_full)).collect();
PetrovGalerkinSystem {
k_reduced,
f_reduced,
reduced_dim: r,
}
}
#[derive(Clone, Debug)]
pub struct BalancedTruncation {
pub hankel_singular_values: Vec<f64>,
pub transform_left: Vec<Vec<f64>>,
pub transform_right: Vec<Vec<f64>>,
pub reduced_dim: usize,
}
impl BalancedTruncation {
pub fn project_matrix(&self, a: &[Vec<f64>]) -> Vec<Vec<f64>> {
let at_r = mat_mul(a, &mat_transpose(&self.transform_left));
let t_r = &self.transform_right;
let m = self.transform_left.len();
let _p = t_r[0].len();
let mut out = vec![vec![0.0; self.reduced_dim]; m];
for (i, out_row) in out.iter_mut().enumerate().take(m) {
for (j, cell) in out_row.iter_mut().enumerate().take(self.reduced_dim) {
*cell = dot(
&self.transform_left[i],
&(0..a.len()).map(|k| at_r[k][j]).collect::<Vec<_>>(),
);
}
}
out
}
pub fn project_vector(&self, b: &[f64]) -> Vec<f64> {
mat_vec(&self.transform_left, b)
}
}
pub fn balanced_truncation(
a: &[Vec<f64>],
b_mat: &[Vec<f64>],
c_mat: &[Vec<f64>],
max_modes: usize,
) -> BalancedTruncation {
let n = a.len();
let bt = mat_transpose(b_mat);
let w_c = mat_mul(b_mat, &bt);
let ct = mat_transpose(c_mat);
let w_o = mat_mul(&ct, c_mat);
let h = mat_mul(&w_c, &w_o);
let r = max_modes.min(n);
let mut t_l: Vec<Vec<f64>> = Vec::with_capacity(r);
let mut hsv: Vec<f64> = Vec::with_capacity(r);
let mut deflated = h.clone();
for _k in 0..r {
let mut v: Vec<f64> = (0..n).map(|i| if i == 0 { 1.0 } else { 0.0 }).collect();
let mut lambda = 0.0_f64;
for _iter in 0..300 {
let av = mat_vec(&deflated, &v);
let lambda_new = dot(&v, &av);
let vn = norm(&av);
if vn < 1e-14 {
break;
}
v = vec_scale(&av, 1.0 / vn);
let converged = (lambda_new - lambda).abs() < 1e-12;
lambda = lambda_new;
if converged {
break;
}
}
let _ = lambda;
let av = mat_vec(&deflated, &v);
let lam = dot(&v, &av).max(0.0);
let sigma = lam.sqrt();
if sigma < 1e-14 {
break;
}
hsv.push(sigma);
t_l.push(v.clone());
for i in 0..n {
for j in 0..n {
deflated[i][j] -= lam * v[i] * v[j];
}
}
}
let t_r = mat_transpose(&t_l);
let reduced_dim = t_l.len();
BalancedTruncation {
hankel_singular_values: hsv,
transform_left: t_l,
transform_right: t_r,
reduced_dim,
}
}
#[derive(Clone, Debug)]
pub struct RomErrorBound {
pub residual_norm: f64,
pub coercivity_lb: f64,
pub error_bound: f64,
pub effectivity: Option<f64>,
}
impl RomErrorBound {
pub fn is_sharp(&self, tol: f64) -> bool {
self.effectivity.is_some_and(|e| (e - 1.0).abs() < tol)
}
}
pub fn compute_error_bound(
k_full: &[Vec<f64>],
f_full: &[f64],
u_rom: &[f64],
coercivity_lb: f64,
true_error: Option<f64>,
) -> RomErrorBound {
let ku = mat_vec(k_full, u_rom);
let residual = vec_sub(f_full, &ku);
let residual_norm = norm(&residual);
let alpha = coercivity_lb.max(1e-14);
let error_bound = residual_norm / alpha;
let effectivity = true_error.map(|e| if e < 1e-14 { 1.0 } else { error_bound / e });
RomErrorBound {
residual_norm,
coercivity_lb: alpha,
error_bound,
effectivity,
}
}
#[derive(Clone, Debug)]
pub struct EimApproximant {
pub magic_points: Vec<usize>,
pub basis_functions: Vec<Vec<f64>>,
pub interp_matrix: Vec<Vec<f64>>,
}
impl EimApproximant {
pub fn evaluate(&self, g_magic: &[f64]) -> Vec<f64> {
let c = cg_solve(&self.interp_matrix, g_magic, 100, 1e-12);
let n = if self.basis_functions.is_empty() {
0
} else {
self.basis_functions[0].len()
};
let mut g_approx = vec![0.0; n];
for (phi, &coeff) in self.basis_functions.iter().zip(c.iter()) {
for (g, &q) in g_approx.iter_mut().zip(phi.iter()) {
*g += coeff * q;
}
}
g_approx
}
}
pub fn build_eim(snapshots: &[Vec<f64>]) -> EimApproximant {
if snapshots.is_empty() {
return EimApproximant {
magic_points: vec![],
basis_functions: vec![],
interp_matrix: vec![],
};
}
let m = snapshots.len();
let n = snapshots[0].len();
let mut magic_points: Vec<usize> = Vec::with_capacity(m);
let mut basis: Vec<Vec<f64>> = Vec::with_capacity(m);
let mut interp_mat: Vec<Vec<f64>> = Vec::with_capacity(m);
for (k, snap) in snapshots.iter().enumerate() {
let residual = if k == 0 {
snap.clone()
} else {
let g_magic: Vec<f64> = magic_points.iter().map(|&i| snap[i]).collect();
let bk: Vec<Vec<f64>> = interp_mat[..k].to_vec();
let c = cg_solve(&bk, &g_magic[..k], 100, 1e-12);
let approx_k = {
let mut a = vec![0.0; n];
for (phi, &coeff) in basis.iter().zip(c.iter()) {
for (ai, &bi) in a.iter_mut().zip(phi.iter()) {
*ai += coeff * bi;
}
}
a
};
vec_sub(snap, &approx_k)
};
let new_pt = residual
.iter()
.enumerate()
.max_by(|a, b| {
a.1.abs()
.partial_cmp(&b.1.abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.unwrap_or(0);
let rn = norm(&residual);
if rn < 1e-14 {
break;
}
let q_k: Vec<f64> = vec_scale(&residual, 1.0 / rn);
magic_points.push(new_pt);
basis.push(q_k.clone());
let mut new_row = vec![0.0; k + 1];
for (j, phi) in basis.iter().enumerate() {
new_row[j] = phi[new_pt];
}
interp_mat.push(new_row);
}
EimApproximant {
magic_points,
basis_functions: basis,
interp_matrix: interp_mat,
}
}
#[derive(Clone, Debug)]
pub struct AffineDecomposition {
pub k_reduced_components: Vec<Vec<Vec<f64>>>,
pub f_reduced_components: Vec<Vec<f64>>,
pub reduced_dim: usize,
}
impl AffineDecomposition {
pub fn assemble(&self, theta_k: &[f64], theta_f: &[f64]) -> GalerkinSystem {
let r = self.reduced_dim;
let mut k_r = vec![vec![0.0; r]; r];
let mut f_r = vec![0.0; r];
for (q, &th) in theta_k.iter().enumerate() {
if q >= self.k_reduced_components.len() {
break;
}
for (i, kr_row) in k_r.iter_mut().enumerate().take(r) {
for (j, kr_ij) in kr_row.iter_mut().enumerate().take(r) {
*kr_ij += th * self.k_reduced_components[q][i][j];
}
}
}
for (q, &th) in theta_f.iter().enumerate() {
if q >= self.f_reduced_components.len() {
break;
}
for (i, fr_i) in f_r.iter_mut().enumerate().take(r) {
*fr_i += th * self.f_reduced_components[q][i];
}
}
GalerkinSystem {
k_reduced: k_r,
f_reduced: f_r,
reduced_dim: r,
}
}
}
pub fn offline_affine_decomposition(
k_components: &[Vec<Vec<f64>>],
f_components: &[Vec<f64>],
basis: &PodBasis,
) -> AffineDecomposition {
let r = basis.rank();
let k_reduced_components: Vec<Vec<Vec<f64>>> = k_components
.iter()
.map(|kq| {
let k_phi: Vec<Vec<f64>> = basis.modes.iter().map(|phi| mat_vec(kq, phi)).collect();
let mut kq_r = vec![vec![0.0; r]; r];
for (i, kqr_row) in kq_r.iter_mut().enumerate().take(r) {
for (j, kqr_ij) in kqr_row.iter_mut().enumerate().take(r) {
*kqr_ij = dot(&basis.modes[i], &k_phi[j]);
}
}
kq_r
})
.collect();
let f_reduced_components: Vec<Vec<f64>> = f_components
.iter()
.map(|fq| basis.modes.iter().map(|phi| dot(phi, fq)).collect())
.collect();
AffineDecomposition {
k_reduced_components,
f_reduced_components,
reduced_dim: r,
}
}
#[derive(Clone, Debug)]
pub struct ManifoldInterpolator {
pub params: Vec<f64>,
pub solutions: Vec<Vec<f64>>,
pub rbf_width: f64,
}
impl ManifoldInterpolator {
pub fn new(rbf_width: f64) -> Self {
Self {
params: vec![],
solutions: vec![],
rbf_width,
}
}
pub fn add_training_point(&mut self, mu: f64, q_r: Vec<f64>) {
self.params.push(mu);
self.solutions.push(q_r);
}
pub fn interpolate(&self, mu: f64) -> Vec<f64> {
let n = self.params.len();
if n == 0 {
return vec![];
}
let dim = self.solutions[0].len();
let rbf = |r: f64| -> f64 { (-(r * r) / (2.0 * self.rbf_width * self.rbf_width)).exp() };
let mut phi_mat = vec![vec![0.0; n]; n];
for (i, row) in phi_mat.iter_mut().enumerate().take(n) {
for (j, cell) in row.iter_mut().enumerate().take(n) {
*cell = rbf((self.params[i] - self.params[j]).abs());
}
}
let phi_mu: Vec<f64> = self.params.iter().map(|&p| rbf((p - mu).abs())).collect();
let mut q_interp = vec![0.0; dim];
for d in 0..dim {
let rhs: Vec<f64> = self.solutions.iter().map(|s| s[d]).collect();
let coeffs = cg_solve(&phi_mat, &rhs, 200, 1e-10);
q_interp[d] = dot(&phi_mu, &coeffs);
}
q_interp
}
}
pub fn select_pod_modes_by_energy(singular_values: &[f64], energy_fraction: f64) -> usize {
let total: f64 = singular_values.iter().map(|s| s * s).sum();
if total < 1e-300 {
return 0;
}
let mut cumulative = 0.0;
for (k, &s) in singular_values.iter().enumerate() {
cumulative += s * s;
if cumulative / total >= energy_fraction {
return k + 1;
}
}
singular_values.len()
}
#[derive(Clone, Debug)]
pub struct ReducedDynamicState {
pub q: Vec<f64>,
pub dq: Vec<f64>,
pub time: f64,
pub k_r: Vec<Vec<f64>>,
pub m_r: Vec<Vec<f64>>,
pub c_r: Vec<Vec<f64>>,
}
impl ReducedDynamicState {
pub fn new(k_r: Vec<Vec<f64>>, m_r: Vec<Vec<f64>>, c_r: Vec<Vec<f64>>) -> Self {
let r = k_r.len();
Self {
q: vec![0.0; r],
dq: vec![0.0; r],
time: 0.0,
k_r,
m_r,
c_r,
}
}
pub fn step_explicit(&mut self, f_r: &[f64], dt: f64) {
let r = self.q.len();
let kq = mat_vec(&self.k_r, &self.q);
let cdq = mat_vec(&self.c_r, &self.dq);
let rhs: Vec<f64> = (0..r).map(|i| f_r[i] - kq[i] - cdq[i]).collect();
let a: Vec<f64> = (0..r)
.map(|i| {
let mii = self.m_r[i][i];
if mii.abs() < 1e-14 { 0.0 } else { rhs[i] / mii }
})
.collect();
for (i, &ai) in a.iter().enumerate().take(r) {
self.dq[i] += ai * dt;
self.q[i] += self.dq[i] * dt;
}
self.time += dt;
}
}
pub fn projection_errors(snapshots: &[Vec<f64>], basis: &PodBasis) -> Vec<f64> {
snapshots
.iter()
.map(|snap| {
let q_r = basis.project(snap);
let recon = basis.reconstruct(&q_r);
norm(&vec_sub(snap, &recon))
})
.collect()
}
pub fn relative_projection_error(snap: &[f64], basis: &PodBasis) -> f64 {
let q_r = basis.project(snap);
let recon = basis.reconstruct(&q_r);
let err = norm(&vec_sub(snap, &recon));
let sn = norm(snap);
if sn < 1e-14 {
return 0.0;
}
err / sn
}
pub fn gram_schmidt(vectors: &[Vec<f64>]) -> Vec<Vec<f64>> {
let mut basis: Vec<Vec<f64>> = Vec::with_capacity(vectors.len());
for v in vectors {
let mut w = v.clone();
for phi in &basis {
let c = dot(&w, phi);
w = vec_sub(&w, &vec_scale(phi, c));
}
let n = norm(&w);
if n > 1e-12 {
basis.push(vec_scale(&w, 1.0 / n));
}
}
basis
}
pub fn is_orthonormal(basis: &[Vec<f64>], tol: f64) -> bool {
for i in 0..basis.len() {
for j in 0..basis.len() {
let inner = dot(&basis[i], &basis[j]);
let expected = if i == j { 1.0 } else { 0.0 };
if (inner - expected).abs() > tol {
return false;
}
}
}
true
}
pub fn qdeim_indices(modes: &[Vec<f64>], m: usize) -> Vec<usize> {
if modes.is_empty() || m == 0 {
return vec![];
}
let n = modes[0].len();
let mut selected: Vec<usize> = Vec::with_capacity(m);
let _r = modes.len();
let mut residual: Vec<Vec<f64>> = (0..n)
.map(|i| modes.iter().map(|mode| mode[i]).collect())
.collect();
for _step in 0..m {
let best_i = residual
.iter()
.enumerate()
.max_by(|a, b| {
let na: f64 = a.1.iter().map(|x| x * x).sum::<f64>();
let nb: f64 = b.1.iter().map(|x| x * x).sum::<f64>();
na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.unwrap_or(0);
selected.push(best_i);
let row = residual[best_i].clone();
let rn: f64 = row.iter().map(|x| x * x).sum::<f64>().sqrt();
if rn < 1e-14 {
break;
}
let row_unit: Vec<f64> = row.iter().map(|x| x / rn).collect();
for res_row in residual.iter_mut().take(n) {
let c: f64 = res_row
.iter()
.zip(row_unit.iter())
.map(|(&ri, &ru)| ri * ru)
.sum();
for (res_k, &ru_k) in res_row.iter_mut().zip(row_unit.iter()) {
*res_k -= c * ru_k;
}
}
}
selected
}
pub fn rom_output_functional(s_full: &[f64], u_r: &[f64], basis: &PodBasis) -> f64 {
let u_recon = basis.reconstruct(u_r);
dot(s_full, &u_recon)
}
pub fn rom_output_sensitivity<F>(mu: f64, h: f64, eval_rom: F) -> f64
where
F: Fn(f64) -> f64,
{
(eval_rom(mu + h) - eval_rom(mu - h)) / (2.0 * h)
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn identity_snapshots(n: usize, m: usize) -> Vec<Vec<f64>> {
(0..m)
.map(|k| {
let mut v = vec![0.0; n];
if k < n {
v[k] = 1.0;
}
v
})
.collect()
}
fn linspace_snapshots(n: usize, m: usize) -> Vec<Vec<f64>> {
(0..m)
.map(|k| {
let t = k as f64 / (m as f64);
(0..n)
.map(|i| (PI * t * (i + 1) as f64 / n as f64).sin())
.collect()
})
.collect()
}
fn identity_matrix(n: usize) -> Vec<Vec<f64>> {
eye(n)
}
#[test]
fn test_dot_product() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![4.0, 5.0, 6.0];
assert!((dot(&a, &b) - 32.0).abs() < 1e-12);
}
#[test]
fn test_norm_unit_vector() {
let v = vec![1.0, 0.0, 0.0];
assert!((norm(&v) - 1.0).abs() < 1e-12);
}
#[test]
fn test_gram_schmidt_orthonormal() {
let vecs = vec![
vec![1.0, 1.0, 0.0],
vec![1.0, 0.0, 1.0],
vec![0.0, 1.0, 1.0],
];
let basis = gram_schmidt(&vecs);
assert!(is_orthonormal(&basis, 1e-10), "Should be orthonormal");
}
#[test]
fn test_gram_schmidt_single_vector() {
let vecs = vec![vec![3.0, 4.0]];
let basis = gram_schmidt(&vecs);
assert_eq!(basis.len(), 1);
assert!((norm(&basis[0]) - 1.0).abs() < 1e-12);
}
#[test]
fn test_mat_vec_identity() {
let id = identity_matrix(3);
let x = vec![1.0, 2.0, 3.0];
let y = mat_vec(&id, &x);
assert!((y[0] - 1.0).abs() < 1e-12);
assert!((y[1] - 2.0).abs() < 1e-12);
assert!((y[2] - 3.0).abs() < 1e-12);
}
#[test]
fn test_back_substitute_upper_triangular() {
let u = vec![vec![2.0, 1.0], vec![0.0, 3.0]];
let b = vec![5.0, 6.0];
let x = back_substitute(&u, &b);
assert!((x[0] - 1.5).abs() < 1e-10, "x[0]={}", x[0]);
assert!((x[1] - 2.0).abs() < 1e-10, "x[1]={}", x[1]);
}
#[test]
fn test_forward_substitute_lower_triangular() {
let l = vec![vec![1.0, 0.0], vec![2.0, 1.0]];
let b = vec![3.0, 7.0];
let x = forward_substitute(&l, &b);
assert!((x[0] - 3.0).abs() < 1e-10);
assert!((x[1] - 1.0).abs() < 1e-10);
}
#[test]
fn test_pod_basis_new() {
let basis = PodBasis::new(100);
assert_eq!(basis.full_dim, 100);
assert_eq!(basis.rank(), 0);
}
#[test]
fn test_build_pod_basis_from_identity_snapshots() {
let snaps = identity_snapshots(5, 3);
let basis = build_pod_basis(&snaps, 3);
assert!(basis.rank() <= 3);
assert_eq!(basis.full_dim, 5);
}
#[test]
fn test_pod_basis_project_reconstruct_roundtrip() {
let q = vec![1.0, 2.0, 3.0, 4.0];
let snaps = vec![q.clone()];
let basis = build_pod_basis(&snaps, 1);
assert_eq!(basis.rank(), 1);
let q_r = basis.project(&q);
let recon = basis.reconstruct(&q_r);
let err = norm(&vec_sub(&q, &recon));
assert!(
err < 1e-10,
"Reconstruction error must be ~0 for single-snapshot basis: {err:.6}"
);
}
#[test]
fn test_pod_basis_singular_values_positive() {
let snaps = linspace_snapshots(8, 4);
let basis = build_pod_basis(&snaps, 4);
for &sv in &basis.singular_values {
assert!(sv >= 0.0, "Singular value must be ≥ 0: {sv}");
}
}
#[test]
fn test_pod_energy_content_full_rank() {
let snaps = identity_snapshots(3, 3);
let basis = build_pod_basis(&snaps, 3);
let e = basis.energy_content();
assert!(e > 0.0 && e <= 1.0 + 1e-10, "Energy content: {e}");
}
#[test]
fn test_select_pod_modes_by_energy() {
let svs = vec![10.0, 5.0, 1.0, 0.1];
let r = select_pod_modes_by_energy(&svs, 0.99);
assert!(r <= 3, "Need at most 3 modes for 99% energy: r={r}");
assert!(r >= 2, "Need at least 2 modes: r={r}");
}
#[test]
fn test_galerkin_system_solve_identity() {
let k = identity_matrix(3);
let f = vec![1.0, 2.0, 3.0];
let snaps = identity_snapshots(3, 3);
let basis = build_pod_basis(&snaps, 3);
let gs = galerkin_projection(&k, &f, &basis);
let q_r = gs.solve();
assert_eq!(q_r.len(), gs.reduced_dim);
}
#[test]
fn test_galerkin_system_reduced_dim() {
let k = identity_matrix(5);
let f = vec![1.0; 5];
let snaps = identity_snapshots(5, 3);
let basis = build_pod_basis(&snaps, 3);
let gs = galerkin_projection(&k, &f, &basis);
assert_eq!(gs.reduced_dim, gs.k_reduced.len());
assert_eq!(gs.reduced_dim, gs.f_reduced.len());
}
#[test]
fn test_reduced_basis_add_snapshot() {
let mut rb = ReducedBasis::new(4);
rb.add_snapshot(&[1.0, 0.0, 0.0, 0.0], 1.0);
rb.add_snapshot(&[0.0, 1.0, 0.0, 0.0], 2.0);
assert_eq!(rb.dim(), 2);
}
#[test]
fn test_reduced_basis_orthonormal() {
let mut rb = ReducedBasis::new(4);
rb.add_snapshot(&[1.0, 1.0, 0.0, 0.0], 1.0);
rb.add_snapshot(&[0.0, 1.0, 1.0, 0.0], 2.0);
rb.add_snapshot(&[0.0, 0.0, 1.0, 1.0], 3.0);
assert!(is_orthonormal(&rb.basis, 1e-10));
}
#[test]
fn test_greedy_reduced_basis_convergence() {
let params: Vec<f64> = (0..5).map(|i| i as f64).collect();
let snaps: Vec<Vec<f64>> = params
.iter()
.map(|&mu| vec![mu.cos(), mu.sin(), mu * mu * 0.1, (-mu).exp()])
.collect();
let rb = greedy_reduced_basis(¶ms, &snaps, 5, 1e-8);
assert!(rb.dim() >= 1);
assert!(rb.dim() <= 5);
}
#[test]
fn test_greedy_reduced_basis_empty_snapshots() {
let rb = greedy_reduced_basis(&[], &[], 5, 1e-6);
assert_eq!(rb.dim(), 0);
}
#[test]
fn test_deim_indices_count() {
let modes: Vec<Vec<f64>> = (0..3)
.map(|k| {
(0..8)
.map(|i| (PI * (k + 1) as f64 * i as f64 / 8.0).sin())
.collect()
})
.collect();
let interp = deim(&modes);
assert_eq!(interp.indices.len(), 3);
}
#[test]
fn test_deim_empty_modes() {
let interp = deim(&[]);
assert!(interp.indices.is_empty());
}
#[test]
fn test_deim_approximate_identity_modes() {
let modes: Vec<Vec<f64>> = (0..3)
.map(|k| {
let mut v = vec![0.0; 6];
v[k] = 1.0;
v
})
.collect();
let interp = deim(&modes);
let f_full = vec![1.0, 2.0, 3.0, 0.0, 0.0, 0.0];
let approx = interp.approximate(&f_full);
assert_eq!(approx.len(), 6);
}
#[test]
fn test_qdeim_indices_count() {
let modes: Vec<Vec<f64>> = (0..2)
.map(|k| {
(0..6)
.map(|i| (PI * (k + 1) as f64 * i as f64 / 6.0).sin())
.collect()
})
.collect();
let indices = qdeim_indices(&modes, 2);
assert_eq!(indices.len(), 2);
}
#[test]
fn test_qdeim_empty() {
let indices = qdeim_indices(&[], 3);
assert!(indices.is_empty());
}
#[test]
fn test_petrov_galerkin_same_as_galerkin_for_symmetric() {
let k = identity_matrix(4);
let f = vec![1.0, 2.0, 3.0, 4.0];
let snaps = identity_snapshots(4, 3);
let basis = build_pod_basis(&snaps, 3);
let phi = &basis.modes;
let psi = phi;
let pg = petrov_galerkin_projection(&k, &f, phi, psi);
let g = galerkin_projection(&k, &f, &basis);
assert_eq!(pg.reduced_dim, g.reduced_dim);
}
#[test]
fn test_petrov_galerkin_solve_returns_correct_length() {
let k = identity_matrix(4);
let f = vec![1.0; 4];
let snaps = identity_snapshots(4, 2);
let basis = build_pod_basis(&snaps, 2);
let phi = &basis.modes;
let pg = petrov_galerkin_projection(&k, &f, phi, phi);
let sol = pg.solve();
assert_eq!(sol.len(), pg.reduced_dim);
}
#[test]
fn test_error_bound_zero_residual() {
let k = identity_matrix(3);
let f = vec![1.0, 2.0, 3.0];
let u_rom = f.clone(); let eb = compute_error_bound(&k, &f, &u_rom, 1.0, None);
assert!(
eb.residual_norm < 1e-12,
"Residual should be 0: {}",
eb.residual_norm
);
assert!(eb.error_bound < 1e-12);
}
#[test]
fn test_error_bound_positive_residual() {
let k = identity_matrix(3);
let f = vec![1.0, 2.0, 3.0];
let u_approx = vec![0.5, 1.0, 1.5]; let eb = compute_error_bound(&k, &f, &u_approx, 1.0, None);
assert!(eb.error_bound > 0.0);
}
#[test]
fn test_error_bound_effectivity() {
let k = identity_matrix(2);
let f = vec![2.0, 4.0];
let u_true = vec![2.0, 4.0];
let u_rom = vec![1.0, 3.0];
let true_err = norm(&vec_sub(&u_true, &u_rom));
let eb = compute_error_bound(&k, &f, &u_rom, 1.0, Some(true_err));
assert!(eb.effectivity.is_some());
}
#[test]
fn test_eim_magic_points_count() {
let snaps: Vec<Vec<f64>> = (0..3)
.map(|k| {
(0..8)
.map(|i| (PI * (k + 1) as f64 * i as f64 / 8.0).sin())
.collect()
})
.collect();
let eim = build_eim(&snaps);
assert!(eim.magic_points.len() <= 3);
}
#[test]
fn test_eim_empty_snapshots() {
let eim = build_eim(&[]);
assert!(eim.magic_points.is_empty());
}
#[test]
fn test_offline_affine_assemble_recovers_full() {
let k_components = vec![identity_matrix(4)];
let f_components = vec![vec![1.0, 2.0, 3.0, 4.0]];
let snaps = identity_snapshots(4, 2);
let basis = build_pod_basis(&snaps, 2);
let ad = offline_affine_decomposition(&k_components, &f_components, &basis);
let system = ad.assemble(&[2.0], &[1.0]);
assert_eq!(system.reduced_dim, basis.rank());
for i in 0..system.reduced_dim {
for j in 0..system.reduced_dim {
assert!(
(system.k_reduced[i][j] - 2.0 * ad.k_reduced_components[0][i][j]).abs() < 1e-12
);
}
}
}
#[test]
fn test_offline_affine_decomposition_empty() {
let basis = PodBasis::new(4);
let ad = offline_affine_decomposition(&[], &[], &basis);
assert!(ad.k_reduced_components.is_empty());
}
#[test]
fn test_manifold_interpolator_at_training_point() {
let mut interp = ManifoldInterpolator::new(1.0);
interp.add_training_point(0.0, vec![1.0, 0.0]);
interp.add_training_point(1.0, vec![0.0, 1.0]);
let q = interp.interpolate(0.0);
assert_eq!(q.len(), 2);
assert!(
q[0] > 0.4,
"Expected q[0] ≈ 1 at training point, got {}",
q[0]
);
}
#[test]
fn test_manifold_interpolator_empty() {
let interp = ManifoldInterpolator::new(1.0);
let q = interp.interpolate(0.5);
assert!(q.is_empty());
}
#[test]
fn test_balanced_truncation_returns_modes() {
let a = identity_matrix(4);
let b_mat = vec![vec![1.0], vec![0.5], vec![0.0], vec![0.0]];
let c_mat = vec![vec![1.0, 0.0, 0.5, 0.0]];
let bt = balanced_truncation(&a, &b_mat, &c_mat, 2);
assert!(bt.reduced_dim <= 2);
assert_eq!(bt.hankel_singular_values.len(), bt.reduced_dim);
}
#[test]
fn test_balanced_truncation_project_vector() {
let a = identity_matrix(3);
let b_mat = vec![vec![1.0], vec![1.0], vec![1.0]];
let c_mat = vec![vec![1.0, 1.0, 1.0]];
let bt = balanced_truncation(&a, &b_mat, &c_mat, 2);
let b_vec = vec![1.0, 0.0, 0.0];
let proj = bt.project_vector(&b_vec);
assert_eq!(proj.len(), bt.reduced_dim);
}
#[test]
fn test_projection_errors_zero_for_full_basis() {
let snaps: Vec<Vec<f64>> = vec![vec![1.0, 0.0, 0.0, 0.0], vec![0.0, 0.0, 0.0, 5.0]];
let basis = build_pod_basis(&snaps, 2);
let errors = projection_errors(&snaps, &basis);
for (i, &e) in errors.iter().enumerate() {
assert!(e < 0.5, "Error for snap {i} too large: {e:.6}");
}
}
#[test]
fn test_relative_projection_error_zero_snap() {
let snap = vec![0.0, 0.0, 0.0];
let basis = PodBasis::new(3);
let rel_err = relative_projection_error(&snap, &basis);
assert_eq!(rel_err, 0.0);
}
#[test]
fn test_reduced_dynamic_state_advances_time() {
let k_r = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let m_r = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let c_r = vec![vec![0.1, 0.0], vec![0.0, 0.1]];
let mut state = ReducedDynamicState::new(k_r, m_r, c_r);
let f_r = vec![0.0, 0.0];
state.step_explicit(&f_r, 0.01);
assert!((state.time - 0.01).abs() < 1e-12);
}
#[test]
fn test_reduced_dynamic_state_displacement_grows_under_load() {
let k_r = vec![vec![1.0]];
let m_r = vec![vec![1.0]];
let c_r = vec![vec![0.0]];
let mut state = ReducedDynamicState::new(k_r, m_r, c_r);
let f_r = vec![1.0]; for _ in 0..100 {
state.step_explicit(&f_r, 0.01);
}
assert!(state.q[0] > 0.0, "Displacement should grow: {}", state.q[0]);
}
#[test]
fn test_rom_output_functional() {
let snaps = identity_snapshots(3, 3);
let basis = build_pod_basis(&snaps, 2);
let s = vec![1.0, 1.0, 1.0];
let u_r = vec![1.0, 0.5];
let out = rom_output_functional(&s, &u_r, &basis);
assert!(out.is_finite(), "Output should be finite: {out}");
}
#[test]
fn test_rom_output_sensitivity() {
let sens = rom_output_sensitivity(3.0, 1e-5, |mu| mu * mu);
assert!((sens - 6.0).abs() < 1e-6, "Sensitivity = {sens:.6}");
}
}