use scirs2_core::ndarray::{Array1, Array2};
use crate::pde::{PDEError, PDEResult};
#[derive(Debug, Clone)]
pub struct TimeFemConfig {
pub theta: f64,
pub lumped_mass: bool,
pub adaptive_tol: Option<f64>,
}
impl Default for TimeFemConfig {
fn default() -> Self {
TimeFemConfig {
theta: 0.5,
lumped_mass: false,
adaptive_tol: None,
}
}
}
pub fn assemble_mass_matrix(n_elements: usize, dx: f64, lumped: bool) -> PDEResult<Array2<f64>> {
if n_elements < 1 {
return Err(PDEError::InvalidParameter(
"Need at least 1 element".to_string(),
));
}
if dx <= 0.0 {
return Err(PDEError::InvalidParameter("dx must be positive".to_string()));
}
let n = n_elements + 1;
let mut m = Array2::<f64>::zeros((n, n));
if lumped {
m[[0, 0]] = dx * 0.5;
for i in 1..(n - 1) {
m[[i, i]] = dx;
}
m[[n - 1, n - 1]] = dx * 0.5;
} else {
for e in 0..n_elements {
m[[e, e]] += dx / 3.0;
m[[e, e + 1]] += dx / 6.0;
m[[e + 1, e]] += dx / 6.0;
m[[e + 1, e + 1]] += dx / 3.0;
}
}
Ok(m)
}
pub fn assemble_stiffness_matrix(n_elements: usize, dx: f64) -> PDEResult<Array2<f64>> {
if n_elements < 1 {
return Err(PDEError::InvalidParameter(
"Need at least 1 element".to_string(),
));
}
if dx <= 0.0 {
return Err(PDEError::InvalidParameter("dx must be positive".to_string()));
}
let n = n_elements + 1;
let mut k = Array2::<f64>::zeros((n, n));
for e in 0..n_elements {
let inv_dx = 1.0 / dx;
k[[e, e]] += inv_dx;
k[[e, e + 1]] -= inv_dx;
k[[e + 1, e]] -= inv_dx;
k[[e + 1, e + 1]] += inv_dx;
}
Ok(k)
}
pub fn time_step_theta(
u_n: &Array1<f64>,
k_mat: &Array2<f64>,
m_mat: &Array2<f64>,
f_n: &Array1<f64>,
f_np1: &Array1<f64>,
dt: f64,
theta: f64,
bc: Option<(f64, f64)>,
) -> PDEResult<Array1<f64>> {
let n = u_n.len();
if k_mat.nrows() != n
|| k_mat.ncols() != n
|| m_mat.nrows() != n
|| m_mat.ncols() != n
|| f_n.len() != n
|| f_np1.len() != n
{
return Err(PDEError::ComputationError(
"time_step_theta: inconsistent dimensions".to_string(),
));
}
if dt <= 0.0 {
return Err(PDEError::InvalidParameter("dt must be positive".to_string()));
}
if !(0.0..=1.0).contains(&theta) {
return Err(PDEError::InvalidParameter(
"theta must be in [0, 1]".to_string(),
));
}
let mut a_mat = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
a_mat[[i, j]] = m_mat[[i, j]] + theta * dt * k_mat[[i, j]];
}
}
let mut rhs = Array1::<f64>::zeros(n);
for i in 0..n {
let mut row_val = 0.0_f64;
for j in 0..n {
row_val += (m_mat[[i, j]] - (1.0 - theta) * dt * k_mat[[i, j]]) * u_n[j];
}
rhs[i] = row_val + dt * (theta * f_np1[i] + (1.0 - theta) * f_n[i]);
}
if let Some((left_val, right_val)) = bc {
apply_dirichlet_penalty(&mut a_mat, &mut rhs, 0, left_val);
apply_dirichlet_penalty(&mut a_mat, &mut rhs, n - 1, right_val);
}
let u_np1 = gauss_solve(&a_mat, &rhs)?;
Ok(u_np1)
}
pub fn solve_heat_equation_fem(
initial_cond: &dyn Fn(f64) -> f64,
alpha: f64,
x_left: f64,
x_right: f64,
n_elements: usize,
bc: (f64, f64),
dt: f64,
n_steps: usize,
theta: f64,
) -> PDEResult<Vec<Array1<f64>>> {
if n_elements < 1 {
return Err(PDEError::InvalidParameter(
"n_elements must be >= 1".to_string(),
));
}
if x_right <= x_left {
return Err(PDEError::DomainError(
"x_right must be > x_left".to_string(),
));
}
if alpha <= 0.0 {
return Err(PDEError::InvalidParameter("alpha must be positive".to_string()));
}
if dt <= 0.0 {
return Err(PDEError::InvalidParameter("dt must be positive".to_string()));
}
if !(0.0..=1.0).contains(&theta) {
return Err(PDEError::InvalidParameter(
"theta must be in [0, 1]".to_string(),
));
}
let n = n_elements + 1;
let dx = (x_right - x_left) / n_elements as f64;
let mut u = Array1::<f64>::zeros(n);
for i in 0..n {
let xi = x_left + i as f64 * dx;
u[i] = initial_cond(xi);
}
u[0] = bc.0;
u[n - 1] = bc.1;
let m_mat = assemble_mass_matrix(n_elements, dx, false)?;
let k_raw = assemble_stiffness_matrix(n_elements, dx)?;
let mut k_mat = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
k_mat[[i, j]] = alpha * k_raw[[i, j]];
}
}
let f_zero = Array1::<f64>::zeros(n);
let mut history = Vec::with_capacity(n_steps + 1);
history.push(u.clone());
for _ in 0..n_steps {
let u_new = time_step_theta(
&u, &k_mat, &m_mat, &f_zero, &f_zero, dt, theta, Some(bc),
)?;
u = u_new;
history.push(u.clone());
}
Ok(history)
}
pub fn solve_wave_equation_fem(
u0: &dyn Fn(f64) -> f64,
v0: &dyn Fn(f64) -> f64,
c_wave: f64,
x_left: f64,
x_right: f64,
n_elements: usize,
bc: (f64, f64),
dt: f64,
n_steps: usize,
) -> PDEResult<Vec<Array1<f64>>> {
if n_elements < 1 {
return Err(PDEError::InvalidParameter(
"n_elements must be >= 1".to_string(),
));
}
if x_right <= x_left {
return Err(PDEError::DomainError(
"x_right must be > x_left".to_string(),
));
}
if c_wave <= 0.0 {
return Err(PDEError::InvalidParameter(
"c_wave must be positive".to_string(),
));
}
if dt <= 0.0 {
return Err(PDEError::InvalidParameter("dt must be positive".to_string()));
}
let n = n_elements + 1;
let dx = (x_right - x_left) / n_elements as f64;
let mut u = Array1::<f64>::zeros(n);
let mut v = Array1::<f64>::zeros(n);
for i in 0..n {
let xi = x_left + i as f64 * dx;
u[i] = u0(xi);
v[i] = v0(xi);
}
u[0] = bc.0;
u[n - 1] = bc.1;
v[0] = 0.0;
v[n - 1] = 0.0;
let m_mat = assemble_mass_matrix(n_elements, dx, false)?;
let k_raw = assemble_stiffness_matrix(n_elements, dx)?;
let mut k_mat = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
k_mat[[i, j]] = c_wave * c_wave * k_raw[[i, j]];
}
}
let f_zero = Array1::<f64>::zeros(n);
let rhs0 = compute_newmark_rhs(&f_zero, &k_mat, &u);
let mut a = newmark_accel_solve(&m_mat, &rhs0, bc)?;
let beta = 0.25_f64;
let gamma = 0.5_f64;
let k_eff = build_effective_stiffness(&m_mat, &k_mat, beta, dt);
let mut history = Vec::with_capacity(n_steps + 1);
history.push(u.clone());
for _ in 0..n_steps {
let u_pred = newmark_predict_u(&u, &v, &a, beta, dt);
let v_pred = newmark_predict_v(&v, &a, gamma, dt);
let rhs = compute_newmark_rhs(&f_zero, &k_mat, &u_pred);
let a_new = newmark_accel_solve_eff(&k_eff, &rhs, bc)?;
let mut u_new = Array1::<f64>::zeros(n);
let mut v_new = Array1::<f64>::zeros(n);
for i in 0..n {
u_new[i] = u_pred[i] + beta * dt * dt * a_new[i];
v_new[i] = v_pred[i] + gamma * dt * a_new[i];
}
u_new[0] = bc.0;
u_new[n - 1] = bc.1;
v_new[0] = 0.0;
v_new[n - 1] = 0.0;
u = u_new;
v = v_new;
a = a_new;
history.push(u.clone());
}
Ok(history)
}
pub fn adaptive_time_stepping(
u: &Array1<f64>,
k_mat: &Array2<f64>,
m_mat: &Array2<f64>,
dt: f64,
tol: f64,
) -> PDEResult<f64> {
if dt <= 0.0 {
return Err(PDEError::InvalidParameter("dt must be positive".to_string()));
}
if tol <= 0.0 {
return Err(PDEError::InvalidParameter("tol must be positive".to_string()));
}
let n = u.len();
let f_zero = Array1::<f64>::zeros(n);
let u_cn = time_step_theta(u, k_mat, m_mat, &f_zero, &f_zero, dt, 0.5, None)?;
let u_be = time_step_theta(u, k_mat, m_mat, &f_zero, &f_zero, dt, 1.0, None)?;
let mut err = 0.0_f64;
let mut norm_ref = 0.0_f64;
for i in 0..n {
err = err.max((u_cn[i] - u_be[i]).abs());
norm_ref = norm_ref.max(u_cn[i].abs().max(1.0));
}
let rel_err = err / norm_ref;
if rel_err < f64::EPSILON {
return Ok(dt * 4.0);
}
let factor = 0.9 * (tol / rel_err).sqrt();
let factor_clamped = factor.clamp(0.1, 4.0);
Ok(dt * factor_clamped)
}
fn compute_newmark_rhs(f: &Array1<f64>, k: &Array2<f64>, u: &Array1<f64>) -> Array1<f64> {
let n = u.len();
let mut rhs = Array1::<f64>::zeros(n);
for i in 0..n {
let mut ku = 0.0_f64;
for j in 0..n {
ku += k[[i, j]] * u[j];
}
rhs[i] = f[i] - ku;
}
rhs
}
fn newmark_predict_u(
u: &Array1<f64>,
v: &Array1<f64>,
a: &Array1<f64>,
beta: f64,
dt: f64,
) -> Array1<f64> {
let n = u.len();
let mut u_pred = Array1::<f64>::zeros(n);
for i in 0..n {
u_pred[i] = u[i] + dt * v[i] + dt * dt * (0.5 - beta) * a[i];
}
u_pred
}
fn newmark_predict_v(
v: &Array1<f64>,
a: &Array1<f64>,
gamma: f64,
dt: f64,
) -> Array1<f64> {
let n = v.len();
let mut v_pred = Array1::<f64>::zeros(n);
for i in 0..n {
v_pred[i] = v[i] + dt * (1.0 - gamma) * a[i];
}
v_pred
}
fn build_effective_stiffness(
m: &Array2<f64>,
k: &Array2<f64>,
beta: f64,
dt: f64,
) -> Array2<f64> {
let n = m.nrows();
let mut k_eff = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
k_eff[[i, j]] = m[[i, j]] + beta * dt * dt * k[[i, j]];
}
}
k_eff
}
fn newmark_accel_solve(
m: &Array2<f64>,
rhs: &Array1<f64>,
bc: (f64, f64),
) -> PDEResult<Array1<f64>> {
let n = rhs.len();
let mut m_bc = m.clone();
let mut r_bc = rhs.clone();
apply_dirichlet_penalty(&mut m_bc, &mut r_bc, 0, 0.0);
apply_dirichlet_penalty(&mut m_bc, &mut r_bc, n - 1, 0.0);
let _ = bc; gauss_solve(&m_bc, &r_bc)
}
fn newmark_accel_solve_eff(
k_eff: &Array2<f64>,
rhs: &Array1<f64>,
bc: (f64, f64),
) -> PDEResult<Array1<f64>> {
let n = rhs.len();
let mut k_bc = k_eff.clone();
let mut r_bc = rhs.clone();
apply_dirichlet_penalty(&mut k_bc, &mut r_bc, 0, 0.0);
apply_dirichlet_penalty(&mut k_bc, &mut r_bc, n - 1, 0.0);
let _ = bc;
gauss_solve(&k_bc, &r_bc)
}
fn apply_dirichlet_penalty(a: &mut Array2<f64>, rhs: &mut Array1<f64>, dof: usize, value: f64) {
let n = a.nrows();
for i in 0..n {
if i != dof {
rhs[i] -= a[[i, dof]] * value;
a[[i, dof]] = 0.0;
}
}
for j in 0..n {
a[[dof, j]] = 0.0;
}
a[[dof, dof]] = 1.0;
rhs[dof] = value;
}
fn gauss_solve(a: &Array2<f64>, b: &Array1<f64>) -> PDEResult<Array1<f64>> {
let n = a.nrows();
if n != a.ncols() || n != b.len() {
return Err(PDEError::ComputationError(
"gauss_solve: dimension mismatch".to_string(),
));
}
let mut aug = Array2::<f64>::zeros((n, n + 1));
for i in 0..n {
for j in 0..n {
aug[[i, j]] = a[[i, j]];
}
aug[[i, n]] = b[i];
}
for col in 0..n {
let mut pivot_row = col;
let mut max_val = aug[[col, col]].abs();
for row in (col + 1)..n {
let v = aug[[row, col]].abs();
if v > max_val {
max_val = v;
pivot_row = row;
}
}
if max_val < 1e-14 {
return Err(PDEError::ComputationError(
"gauss_solve: matrix is singular".to_string(),
));
}
if pivot_row != col {
for k in 0..=n {
let tmp = aug[[col, k]];
aug[[col, k]] = aug[[pivot_row, k]];
aug[[pivot_row, k]] = tmp;
}
}
let pivot = aug[[col, col]];
for row in (col + 1)..n {
let fac = aug[[row, col]] / pivot;
for k in col..=n {
let delta = fac * aug[[col, k]];
aug[[row, k]] -= delta;
}
}
}
let mut x = Array1::<f64>::zeros(n);
for i in (0..n).rev() {
let mut s = aug[[i, n]];
for j in (i + 1)..n {
s -= aug[[i, j]] * x[j];
}
x[i] = s / aug[[i, i]];
}
Ok(x)
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_mass_matrix_consistent_shape() {
let m = assemble_mass_matrix(4, 0.25, false).expect("mass matrix");
assert_eq!(m.nrows(), 5);
assert_eq!(m.ncols(), 5);
}
#[test]
fn test_mass_matrix_row_sum_uniform_mesh() {
let n_elem = 10;
let dx = 1.0 / n_elem as f64;
let m = assemble_mass_matrix(n_elem, dx, false).expect("mass");
let total: f64 = m.iter().sum();
assert!((total - 1.0).abs() < 1e-12, "total mass = {total}");
}
#[test]
fn test_mass_matrix_lumped() {
let n_elem = 4;
let dx = 0.25;
let m = assemble_mass_matrix(n_elem, dx, true).expect("lumped mass");
let n = n_elem + 1;
for i in 0..n {
for j in 0..n {
if i != j {
assert!(
m[[i, j]].abs() < 1e-15,
"off-diagonal [{i},{j}] = {}",
m[[i, j]]
);
}
}
}
let total: f64 = m.iter().sum();
assert!((total - 1.0).abs() < 1e-12, "lumped total = {total}");
}
#[test]
fn test_stiffness_matrix_shape() {
let k = assemble_stiffness_matrix(5, 0.2).expect("stiffness");
assert_eq!(k.nrows(), 6);
assert_eq!(k.ncols(), 6);
}
#[test]
fn test_stiffness_matrix_null_space() {
let n_elem = 5;
let dx = 1.0 / n_elem as f64;
let k = assemble_stiffness_matrix(n_elem, dx).expect("stiffness");
let n = n_elem + 1;
let ones = Array1::<f64>::ones(n);
let mut ku = Array1::<f64>::zeros(n);
for i in 0..n {
for j in 0..n {
ku[i] += k[[i, j]] * ones[j];
}
}
for i in 0..n {
assert!(
ku[i].abs() < 1e-12,
"K * 1 has non-zero entry [{i}] = {}",
ku[i]
);
}
}
#[test]
fn test_time_step_theta_steady_state() {
let n_elem = 4;
let dx = 0.25;
let n = n_elem + 1;
let m = assemble_mass_matrix(n_elem, dx, false).expect("M");
let k = assemble_stiffness_matrix(n_elem, dx).expect("K");
let u_n = Array1::from_shape_fn(n, |i| i as f64 * dx);
let f_zero = Array1::<f64>::zeros(n);
let u_new = time_step_theta(&u_n, &k, &m, &f_zero, &f_zero, 0.01, 0.5, Some((0.0, 1.0)))
.expect("theta step");
for i in 0..n {
assert!(
(u_new[i] - u_n[i]).abs() < 1e-10,
"u[{i}] changed: {} -> {}",
u_n[i],
u_new[i]
);
}
}
#[test]
fn test_heat_equation_decay() {
let n_elem = 20;
let alpha = 1.0;
let dt = 0.001;
let n_steps = 100;
let t_final = dt * n_steps as f64;
let history = solve_heat_equation_fem(
&|x| (PI * x).sin(),
alpha,
0.0,
1.0,
n_elem,
(0.0, 0.0),
dt,
n_steps,
0.5,
)
.expect("heat solve");
assert_eq!(history.len(), n_steps + 1);
let mid = n_elem / 2;
let u_final = history.last().expect("history non-empty")[mid];
let expected = (PI * (mid as f64) / n_elem as f64).sin() * (-PI * PI * alpha * t_final).exp();
let rel_err = (u_final - expected).abs() / (expected.abs() + 1e-12);
assert!(
rel_err < 0.02,
"heat decay: got {u_final}, expected {expected}, rel_err={rel_err}"
);
}
#[test]
fn test_heat_equation_constant_bc() {
let history = solve_heat_equation_fem(
&|_| 0.0,
1.0,
0.0,
1.0,
10,
(0.0, 0.0),
0.01,
50,
0.5,
)
.expect("heat solve zero");
for snap in &history {
for &v in snap.iter() {
assert!(v.abs() < 1e-12, "non-zero value {v} in zero solution");
}
}
}
#[test]
fn test_wave_equation_returns_snapshots() {
let n_steps = 20;
let history = solve_wave_equation_fem(
&|x| (PI * x).sin(),
&|_| 0.0,
1.0,
0.0,
1.0,
10,
(0.0, 0.0),
0.01,
n_steps,
)
.expect("wave solve");
assert_eq!(history.len(), n_steps + 1);
}
#[test]
fn test_wave_equation_zero_ic() {
let n_steps = 10;
let history = solve_wave_equation_fem(
&|_| 0.0,
&|_| 0.0,
1.0,
0.0,
1.0,
8,
(0.0, 0.0),
0.01,
n_steps,
)
.expect("wave solve zero");
for snap in &history {
for &v in snap.iter() {
assert!(v.abs() < 1e-10, "non-zero value {v} in zero wave solution");
}
}
}
#[test]
fn test_adaptive_time_stepping_increases_step() {
let n_elem = 10;
let dx = 0.1;
let m = assemble_mass_matrix(n_elem, dx, false).expect("M");
let k = assemble_stiffness_matrix(n_elem, dx).expect("K");
let u = Array1::<f64>::zeros(n_elem + 1);
let dt = 0.01;
let tol = 1e-3;
let new_dt = adaptive_time_stepping(&u, &k, &m, dt, tol).expect("adaptive");
assert!(new_dt >= dt, "new_dt {new_dt} < old dt {dt}");
}
#[test]
fn test_adaptive_time_stepping_reduces_step() {
let n_elem = 10;
let dx = 0.1;
let m = assemble_mass_matrix(n_elem, dx, false).expect("M");
let k_raw = assemble_stiffness_matrix(n_elem, dx).expect("K");
let n = n_elem + 1;
let mut k = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
k[[i, j]] = 1000.0 * k_raw[[i, j]];
}
}
let u = Array1::from_shape_fn(n, |i| (PI * i as f64 * dx).sin());
let dt = 0.1;
let tol = 1e-6;
let new_dt = adaptive_time_stepping(&u, &k, &m, dt, tol).expect("adaptive");
assert!(
new_dt < dt,
"step should decrease for stiff problem: new_dt={new_dt}, dt={dt}"
);
}
#[test]
fn test_time_fem_config_default() {
let cfg = TimeFemConfig::default();
assert!((cfg.theta - 0.5).abs() < 1e-12);
assert!(!cfg.lumped_mass);
assert!(cfg.adaptive_tol.is_none());
}
#[test]
fn test_gauss_solve() {
let mut a = Array2::<f64>::zeros((3, 3));
a[[0, 0]] = 1.0;
a[[0, 1]] = 2.0;
a[[0, 2]] = 0.0;
a[[1, 0]] = 3.0;
a[[1, 1]] = 4.0;
a[[1, 2]] = 5.0;
a[[2, 0]] = 0.0;
a[[2, 1]] = 6.0;
a[[2, 2]] = 7.0;
let b = Array1::from_vec(vec![5.0, 26.0, 45.0]);
let x = gauss_solve(&a, &b).expect("solve");
for i in 0..3 {
let mut ax_i = 0.0_f64;
for j in 0..3 {
ax_i += a[[i, j]] * x[j];
}
assert!((ax_i - b[i]).abs() < 1e-9, "residual[{i}] = {}", ax_i - b[i]);
}
}
}