use scirs2_core::ndarray::{Array1, Array2};
use std::f64;
use crate::pde::{PDEError, PDEResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FEM1DElementType {
Linear,
Quadratic,
}
#[derive(Debug, Clone)]
pub enum FEM1DBoundaryCondition {
Dirichlet(f64),
Neumann(f64),
}
#[derive(Debug, Clone)]
pub struct Mesh1D {
pub nodes: Array1<f64>,
pub elements: Vec<Vec<usize>>,
pub element_type: FEM1DElementType,
}
impl Mesh1D {
pub fn uniform(
a: f64,
b: f64,
num_elements: usize,
element_type: FEM1DElementType,
) -> PDEResult<Self> {
if num_elements < 1 {
return Err(PDEError::InvalidParameter(
"Need at least 1 element".to_string(),
));
}
if a >= b {
return Err(PDEError::DomainError("a must be < b".to_string()));
}
match element_type {
FEM1DElementType::Linear => {
let n_nodes = num_elements + 1;
let h = (b - a) / num_elements as f64;
let nodes = Array1::from_shape_fn(n_nodes, |i| a + i as f64 * h);
let elements: Vec<Vec<usize>> = (0..num_elements).map(|e| vec![e, e + 1]).collect();
Ok(Mesh1D {
nodes,
elements,
element_type,
})
}
FEM1DElementType::Quadratic => {
let n_nodes = 2 * num_elements + 1;
let h = (b - a) / num_elements as f64;
let nodes = Array1::from_shape_fn(n_nodes, |i| a + (i as f64) * h / 2.0);
let elements: Vec<Vec<usize>> = (0..num_elements)
.map(|e| vec![2 * e, 2 * e + 1, 2 * e + 2])
.collect();
Ok(Mesh1D {
nodes,
elements,
element_type,
})
}
}
}
pub fn num_nodes(&self) -> usize {
self.nodes.len()
}
pub fn num_elements(&self) -> usize {
self.elements.len()
}
pub fn element_size(&self, e: usize) -> f64 {
let el = &self.elements[e];
let first = *el.first().unwrap_or(&0);
let last = *el.last().unwrap_or(&0);
(self.nodes[last] - self.nodes[first]).abs()
}
}
#[derive(Debug, Clone)]
pub struct FEM1DSteadyOptions {
pub element_type: FEM1DElementType,
pub num_quad_points: usize,
}
impl Default for FEM1DSteadyOptions {
fn default() -> Self {
FEM1DSteadyOptions {
element_type: FEM1DElementType::Linear,
num_quad_points: 3,
}
}
}
#[derive(Debug, Clone)]
pub struct FEM1DSteadyResult {
pub x: Array1<f64>,
pub u: Array1<f64>,
pub stiffness: Array2<f64>,
pub load: Array1<f64>,
pub error_indicators: Array1<f64>,
}
pub fn solve_steady_1d(
a_coeff: &dyn Fn(f64) -> f64,
c_coeff: &dyn Fn(f64) -> f64,
f_source: &dyn Fn(f64) -> f64,
x_left: f64,
x_right: f64,
num_elements: usize,
left_bc: &FEM1DBoundaryCondition,
right_bc: &FEM1DBoundaryCondition,
options: &FEM1DSteadyOptions,
) -> PDEResult<FEM1DSteadyResult> {
let mesh = Mesh1D::uniform(x_left, x_right, num_elements, options.element_type)?;
let n = mesh.num_nodes();
let (mut k_global, mut f_global) =
assemble_system(&mesh, a_coeff, c_coeff, f_source, options.num_quad_points)?;
if let FEM1DBoundaryCondition::Neumann(flux) = right_bc {
f_global[n - 1] += *flux;
}
if let FEM1DBoundaryCondition::Neumann(flux) = left_bc {
f_global[0] -= *flux; }
let k_orig = k_global.clone();
let f_orig = f_global.clone();
apply_dirichlet_bc(&mut k_global, &mut f_global, left_bc, right_bc, n);
let u = solve_linear_system_1d(&k_global, &f_global)?;
let error_indicators = compute_error_indicators(&mesh, &u, &k_orig, &f_orig)?;
Ok(FEM1DSteadyResult {
x: mesh.nodes,
u,
stiffness: k_global,
load: f_global,
error_indicators,
})
}
#[derive(Debug, Clone)]
pub struct FEM1DTransientOptions {
pub element_type: FEM1DElementType,
pub num_quad_points: usize,
pub theta: f64,
pub dt: f64,
pub num_steps: usize,
pub save_every: usize,
}
impl Default for FEM1DTransientOptions {
fn default() -> Self {
FEM1DTransientOptions {
element_type: FEM1DElementType::Linear,
num_quad_points: 3,
theta: 0.5, dt: 0.01,
num_steps: 100,
save_every: 1,
}
}
}
#[derive(Debug, Clone)]
pub struct FEM1DTransientResult {
pub x: Array1<f64>,
pub t: Array1<f64>,
pub u: Vec<Array1<f64>>,
pub error_indicators: Array1<f64>,
}
pub fn solve_transient_1d(
a_coeff: &dyn Fn(f64) -> f64,
c_coeff: &dyn Fn(f64) -> f64,
f_source: &dyn Fn(f64, f64) -> f64,
x_left: f64,
x_right: f64,
num_elements: usize,
initial_condition: &dyn Fn(f64) -> f64,
left_bc: &FEM1DBoundaryCondition,
right_bc: &FEM1DBoundaryCondition,
options: &FEM1DTransientOptions,
) -> PDEResult<FEM1DTransientResult> {
if options.theta < 0.0 || options.theta > 1.0 {
return Err(PDEError::InvalidParameter(
"Theta must be in [0, 1]".to_string(),
));
}
let mesh = Mesh1D::uniform(x_left, x_right, num_elements, options.element_type)?;
let n = mesh.num_nodes();
let dt = options.dt;
let theta = options.theta;
let (k_global, _f_placeholder) =
assemble_system(&mesh, a_coeff, c_coeff, &|_| 0.0, options.num_quad_points)?;
let m_global = assemble_mass_matrix(&mesh, options.num_quad_points)?;
let mut a_mat = m_global.clone();
for i in 0..n {
for j in 0..n {
a_mat[[i, j]] += theta * dt * k_global[[i, j]];
}
}
let mut b_mat = m_global.clone();
for i in 0..n {
for j in 0..n {
b_mat[[i, j]] -= (1.0 - theta) * dt * k_global[[i, j]];
}
}
let mut u_curr = Array1::from_shape_fn(n, |i| initial_condition(mesh.nodes[i]));
apply_dirichlet_to_vec(&mut u_curr, left_bc, right_bc, n);
let save_every = if options.save_every == 0 {
1
} else {
options.save_every
};
let mut solutions = vec![u_curr.clone()];
let mut times = vec![0.0];
for step in 0..options.num_steps {
let t_curr = step as f64 * dt;
let t_next = (step + 1) as f64 * dt;
let f_curr =
assemble_load_vector(&mesh, &|x| f_source(x, t_curr), options.num_quad_points)?;
let f_next =
assemble_load_vector(&mesh, &|x| f_source(x, t_next), options.num_quad_points)?;
let mut rhs = Array1::zeros(n);
for i in 0..n {
let mut sum = 0.0;
for j in 0..n {
sum += b_mat[[i, j]] * u_curr[j];
}
rhs[i] = sum + dt * ((1.0 - theta) * f_curr[i] + theta * f_next[i]);
}
if let FEM1DBoundaryCondition::Neumann(flux) = right_bc {
rhs[n - 1] += dt * theta * flux;
}
if let FEM1DBoundaryCondition::Neumann(flux) = left_bc {
rhs[0] -= dt * theta * flux;
}
let mut a_mod = a_mat.clone();
apply_dirichlet_bc_system(&mut a_mod, &mut rhs, left_bc, right_bc, n);
u_curr = solve_linear_system_1d(&a_mod, &rhs)?;
if (step + 1) % save_every == 0 || step + 1 == options.num_steps {
solutions.push(u_curr.clone());
times.push(t_next);
}
}
let (k_for_err, f_for_err) = assemble_system(
&mesh,
a_coeff,
c_coeff,
&|x| f_source(x, options.num_steps as f64 * dt),
options.num_quad_points,
)?;
let error_indicators = compute_error_indicators(&mesh, &u_curr, &k_for_err, &f_for_err)?;
Ok(FEM1DTransientResult {
x: mesh.nodes,
t: Array1::from_vec(times),
u: solutions,
error_indicators,
})
}
#[derive(Debug, Clone)]
pub struct RefinementIndicator {
pub element: usize,
pub indicator: f64,
pub refine: bool,
}
pub fn mark_for_refinement(
error_indicators: &Array1<f64>,
theta_fraction: f64,
) -> Vec<RefinementIndicator> {
let n_elem = error_indicators.len();
let total_error: f64 = error_indicators.iter().sum();
let threshold = theta_fraction * total_error;
let mut indices: Vec<usize> = (0..n_elem).collect();
indices.sort_by(|&a, &b| {
error_indicators[b]
.partial_cmp(&error_indicators[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut cumulative = 0.0;
let mut marked = vec![false; n_elem];
for &idx in &indices {
if cumulative >= threshold {
break;
}
marked[idx] = true;
cumulative += error_indicators[idx];
}
(0..n_elem)
.map(|e| RefinementIndicator {
element: e,
indicator: error_indicators[e],
refine: marked[e],
})
.collect()
}
fn assemble_system(
mesh: &Mesh1D,
a_coeff: &dyn Fn(f64) -> f64,
c_coeff: &dyn Fn(f64) -> f64,
f_source: &dyn Fn(f64) -> f64,
num_quad: usize,
) -> PDEResult<(Array2<f64>, Array1<f64>)> {
let n = mesh.num_nodes();
let mut k_global = Array2::zeros((n, n));
let mut f_global = Array1::zeros(n);
let (quad_pts, quad_wts) = gauss_legendre_1d(num_quad);
for e in 0..mesh.num_elements() {
let el = &mesh.elements[e];
let n_local = el.len();
let x_left = mesh.nodes[el[0]];
let x_right = mesh.nodes[el[n_local - 1]];
let h = x_right - x_left;
if h.abs() < 1e-15 {
continue;
}
let mut k_local: Array2<f64> = Array2::zeros((n_local, n_local));
let mut f_local: Array1<f64> = Array1::zeros(n_local);
for q in 0..num_quad {
let xi = quad_pts[q]; let w = quad_wts[q];
let x_phys = x_left + 0.5 * (1.0 + xi) * h;
let jac = h / 2.0;
let (phi, dphi_dxi) = eval_basis(xi, mesh.element_type);
let dphi_dx: Vec<f64> = dphi_dxi.iter().map(|&d| d / jac).collect();
let a_val = a_coeff(x_phys);
let c_val = c_coeff(x_phys);
let f_val = f_source(x_phys);
for i in 0..n_local {
for j in 0..n_local {
k_local[[i, j]] +=
w * jac * (a_val * dphi_dx[i] * dphi_dx[j] + c_val * phi[i] * phi[j]);
}
f_local[i] += w * jac * f_val * phi[i];
}
}
for i in 0..n_local {
let gi = el[i];
f_global[gi] += f_local[i];
for j in 0..n_local {
let gj = el[j];
k_global[[gi, gj]] += k_local[[i, j]];
}
}
}
Ok((k_global, f_global))
}
fn assemble_mass_matrix(mesh: &Mesh1D, num_quad: usize) -> PDEResult<Array2<f64>> {
let n = mesh.num_nodes();
let mut m_global = Array2::zeros((n, n));
let (quad_pts, quad_wts) = gauss_legendre_1d(num_quad);
for e in 0..mesh.num_elements() {
let el = &mesh.elements[e];
let n_local = el.len();
let x_left = mesh.nodes[el[0]];
let x_right = mesh.nodes[el[n_local - 1]];
let h = x_right - x_left;
if h.abs() < 1e-15 {
continue;
}
let mut m_local: Array2<f64> = Array2::zeros((n_local, n_local));
for q in 0..num_quad {
let xi = quad_pts[q];
let w = quad_wts[q];
let jac = h / 2.0;
let (phi, _) = eval_basis(xi, mesh.element_type);
for i in 0..n_local {
for j in 0..n_local {
m_local[[i, j]] += w * jac * phi[i] * phi[j];
}
}
}
for i in 0..n_local {
let gi = el[i];
for j in 0..n_local {
let gj = el[j];
m_global[[gi, gj]] += m_local[[i, j]];
}
}
}
Ok(m_global)
}
fn assemble_load_vector(
mesh: &Mesh1D,
f_source: &dyn Fn(f64) -> f64,
num_quad: usize,
) -> PDEResult<Array1<f64>> {
let n = mesh.num_nodes();
let mut f_global = Array1::zeros(n);
let (quad_pts, quad_wts) = gauss_legendre_1d(num_quad);
for e in 0..mesh.num_elements() {
let el = &mesh.elements[e];
let n_local = el.len();
let x_left = mesh.nodes[el[0]];
let x_right = mesh.nodes[el[n_local - 1]];
let h = x_right - x_left;
if h.abs() < 1e-15 {
continue;
}
let mut f_local: Array1<f64> = Array1::zeros(n_local);
for q in 0..num_quad {
let xi = quad_pts[q];
let w = quad_wts[q];
let x_phys = x_left + 0.5 * (1.0 + xi) * h;
let jac = h / 2.0;
let (phi, _) = eval_basis(xi, mesh.element_type);
let f_val = f_source(x_phys);
for i in 0..n_local {
f_local[i] += w * jac * f_val * phi[i];
}
}
for i in 0..n_local {
f_global[el[i]] += f_local[i];
}
}
Ok(f_global)
}
fn eval_basis(xi: f64, element_type: FEM1DElementType) -> (Vec<f64>, Vec<f64>) {
match element_type {
FEM1DElementType::Linear => {
let phi = vec![0.5 * (1.0 - xi), 0.5 * (1.0 + xi)];
let dphi = vec![-0.5, 0.5];
(phi, dphi)
}
FEM1DElementType::Quadratic => {
let phi = vec![0.5 * xi * (xi - 1.0), 1.0 - xi * xi, 0.5 * xi * (xi + 1.0)];
let dphi = vec![xi - 0.5, -2.0 * xi, xi + 0.5];
(phi, dphi)
}
}
}
fn gauss_legendre_1d(n: usize) -> (Vec<f64>, Vec<f64>) {
match n {
1 => (vec![0.0], vec![2.0]),
2 => {
let p = 1.0 / 3.0_f64.sqrt();
(vec![-p, p], vec![1.0, 1.0])
}
3 => {
let p = (3.0 / 5.0_f64).sqrt();
(vec![-p, 0.0, p], vec![5.0 / 9.0, 8.0 / 9.0, 5.0 / 9.0])
}
4 => {
let a = (3.0 / 7.0 - 2.0 / 7.0 * (6.0 / 5.0_f64).sqrt()).sqrt();
let b = (3.0 / 7.0 + 2.0 / 7.0 * (6.0 / 5.0_f64).sqrt()).sqrt();
let wa = (18.0 + 30.0_f64.sqrt()) / 36.0;
let wb = (18.0 - 30.0_f64.sqrt()) / 36.0;
(vec![-b, -a, a, b], vec![wb, wa, wa, wb])
}
_ => {
let p = (3.0 / 5.0_f64).sqrt();
(vec![-p, 0.0, p], vec![5.0 / 9.0, 8.0 / 9.0, 5.0 / 9.0])
}
}
}
fn apply_dirichlet_bc(
k: &mut Array2<f64>,
f: &mut Array1<f64>,
left_bc: &FEM1DBoundaryCondition,
right_bc: &FEM1DBoundaryCondition,
n: usize,
) {
let penalty = 1e30;
if let FEM1DBoundaryCondition::Dirichlet(val) = left_bc {
k[[0, 0]] += penalty;
f[0] += penalty * val;
}
if let FEM1DBoundaryCondition::Dirichlet(val) = right_bc {
k[[n - 1, n - 1]] += penalty;
f[n - 1] += penalty * val;
}
}
fn apply_dirichlet_bc_system(
a: &mut Array2<f64>,
rhs: &mut Array1<f64>,
left_bc: &FEM1DBoundaryCondition,
right_bc: &FEM1DBoundaryCondition,
n: usize,
) {
if let FEM1DBoundaryCondition::Dirichlet(val) = left_bc {
for j in 0..n {
a[[0, j]] = 0.0;
}
a[[0, 0]] = 1.0;
rhs[0] = *val;
}
if let FEM1DBoundaryCondition::Dirichlet(val) = right_bc {
for j in 0..n {
a[[n - 1, j]] = 0.0;
}
a[[n - 1, n - 1]] = 1.0;
rhs[n - 1] = *val;
}
}
fn apply_dirichlet_to_vec(
u: &mut Array1<f64>,
left_bc: &FEM1DBoundaryCondition,
right_bc: &FEM1DBoundaryCondition,
n: usize,
) {
if let FEM1DBoundaryCondition::Dirichlet(val) = left_bc {
u[0] = *val;
}
if let FEM1DBoundaryCondition::Dirichlet(val) = right_bc {
u[n - 1] = *val;
}
}
fn solve_linear_system_1d(a: &Array2<f64>, b: &Array1<f64>) -> PDEResult<Array1<f64>> {
let n = b.len();
if n == 0 {
return Ok(Array1::zeros(0));
}
let mut a_copy = a.clone();
let mut b_copy = b.clone();
for k in 0..n {
let mut max_val = a_copy[[k, k]].abs();
let mut max_row = k;
for i in k + 1..n {
let val = a_copy[[i, k]].abs();
if val > max_val {
max_val = val;
max_row = i;
}
}
if max_val < 1e-14 {
return Err(PDEError::ComputationError(
"Singular or near-singular FEM system matrix".to_string(),
));
}
if max_row != k {
for j in k..n {
let tmp = a_copy[[k, j]];
a_copy[[k, j]] = a_copy[[max_row, j]];
a_copy[[max_row, j]] = tmp;
}
let tmp = b_copy[k];
b_copy[k] = b_copy[max_row];
b_copy[max_row] = tmp;
}
for i in k + 1..n {
let factor = a_copy[[i, k]] / a_copy[[k, k]];
for j in k + 1..n {
a_copy[[i, j]] -= factor * a_copy[[k, j]];
}
b_copy[i] -= factor * b_copy[k];
}
}
let mut x = Array1::zeros(n);
for i in (0..n).rev() {
let mut sum = 0.0;
for j in i + 1..n {
sum += a_copy[[i, j]] * x[j];
}
x[i] = (b_copy[i] - sum) / a_copy[[i, i]];
}
Ok(x)
}
fn compute_error_indicators(
mesh: &Mesh1D,
u: &Array1<f64>,
k_global: &Array2<f64>,
f_global: &Array1<f64>,
) -> PDEResult<Array1<f64>> {
let n = mesh.num_nodes();
let ne = mesh.num_elements();
let mut residual = f_global.clone();
for i in 0..n {
for j in 0..n {
residual[i] -= k_global[[i, j]] * u[j];
}
}
let mut indicators = Array1::zeros(ne);
for e in 0..ne {
let el = &mesh.elements[e];
let h = mesh.element_size(e);
let mut r_sq = 0.0;
for &node in el {
r_sq += residual[node] * residual[node];
}
indicators[e] = h * h * r_sq;
}
Ok(indicators)
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_mesh_uniform_linear() {
let mesh =
Mesh1D::uniform(0.0, 1.0, 10, FEM1DElementType::Linear).expect("Should create mesh");
assert_eq!(mesh.num_nodes(), 11);
assert_eq!(mesh.num_elements(), 10);
assert!((mesh.nodes[0] - 0.0).abs() < 1e-15);
assert!((mesh.nodes[10] - 1.0).abs() < 1e-15);
}
#[test]
fn test_mesh_uniform_quadratic() {
let mesh =
Mesh1D::uniform(0.0, 1.0, 5, FEM1DElementType::Quadratic).expect("Should create mesh");
assert_eq!(mesh.num_nodes(), 11);
assert_eq!(mesh.num_elements(), 5);
}
#[test]
fn test_steady_constant_solution() {
let result = solve_steady_1d(
&|_| 1.0,
&|_| 0.0,
&|_| 0.0,
0.0,
1.0,
10,
&FEM1DBoundaryCondition::Dirichlet(1.0),
&FEM1DBoundaryCondition::Dirichlet(1.0),
&FEM1DSteadyOptions::default(),
)
.expect("Should succeed");
for i in 0..result.x.len() {
assert!(
(result.u[i] - 1.0).abs() < 1e-8,
"Node {i}: u={}, expected 1.0",
result.u[i]
);
}
}
#[test]
fn test_steady_linear_solution() {
let result = solve_steady_1d(
&|_| 1.0,
&|_| 0.0,
&|_| 0.0,
0.0,
1.0,
10,
&FEM1DBoundaryCondition::Dirichlet(0.0),
&FEM1DBoundaryCondition::Dirichlet(1.0),
&FEM1DSteadyOptions::default(),
)
.expect("Should succeed");
for i in 0..result.x.len() {
assert!(
(result.u[i] - result.x[i]).abs() < 1e-6,
"Node {i}: u={}, x={}, expected linear",
result.u[i],
result.x[i]
);
}
}
#[test]
fn test_steady_poisson_1d() {
let result = solve_steady_1d(
&|_| 1.0,
&|_| 0.0,
&|_| 2.0,
0.0,
1.0,
20,
&FEM1DBoundaryCondition::Dirichlet(0.0),
&FEM1DBoundaryCondition::Dirichlet(0.0),
&FEM1DSteadyOptions::default(),
)
.expect("Should succeed");
for i in 0..result.x.len() {
let x = result.x[i];
let exact = x * (1.0 - x);
assert!(
(result.u[i] - exact).abs() < 1e-4,
"Node {i}: u={}, exact={exact}",
result.u[i]
);
}
}
#[test]
fn test_steady_quadratic_elements() {
let opts = FEM1DSteadyOptions {
element_type: FEM1DElementType::Quadratic,
num_quad_points: 3,
};
let result = solve_steady_1d(
&|_| 1.0,
&|_| 0.0,
&|_| 2.0,
0.0,
1.0,
10,
&FEM1DBoundaryCondition::Dirichlet(0.0),
&FEM1DBoundaryCondition::Dirichlet(0.0),
&opts,
)
.expect("Should succeed");
for i in 0..result.x.len() {
let x = result.x[i];
let exact = x * (1.0 - x);
assert!(
(result.u[i] - exact).abs() < 1e-6,
"Quadratic node {i}: u={}, exact={exact}",
result.u[i]
);
}
}
#[test]
fn test_steady_neumann_bc() {
let result = solve_steady_1d(
&|_| 1.0,
&|_| 0.0,
&|_| 1.0,
0.0,
1.0,
20,
&FEM1DBoundaryCondition::Dirichlet(0.0),
&FEM1DBoundaryCondition::Neumann(0.0),
&FEM1DSteadyOptions::default(),
)
.expect("Should succeed");
for i in 0..result.x.len() {
let x = result.x[i];
let exact = x - x * x / 2.0;
assert!(
(result.u[i] - exact).abs() < 0.02,
"Neumann node {i}: u={}, exact={exact}",
result.u[i]
);
}
}
#[test]
fn test_steady_variable_coefficient() {
let result = solve_steady_1d(
&|x| 1.0 + x,
&|_| 0.0,
&|_| 1.0,
0.0,
1.0,
40,
&FEM1DBoundaryCondition::Dirichlet(0.0),
&FEM1DBoundaryCondition::Dirichlet(0.0),
&FEM1DSteadyOptions::default(),
)
.expect("Should succeed");
for i in 1..result.x.len() - 1 {
assert!(
result.u[i] > 0.0,
"Interior node {i} should be positive: u={}",
result.u[i]
);
}
}
#[test]
fn test_transient_decay() {
let opts = FEM1DTransientOptions {
element_type: FEM1DElementType::Linear,
num_quad_points: 3,
theta: 0.5,
dt: 0.001,
num_steps: 100,
save_every: 100,
};
let result = solve_transient_1d(
&|_| 1.0,
&|_| 0.0,
&|_, _| 0.0,
0.0,
1.0,
20,
&|x| (PI * x).sin(),
&FEM1DBoundaryCondition::Dirichlet(0.0),
&FEM1DBoundaryCondition::Dirichlet(0.0),
&opts,
)
.expect("Should succeed");
let t_final = opts.dt * opts.num_steps as f64;
let u_final = &result.u[result.u.len() - 1];
let mid = result.x.len() / 2;
let exact = (PI * 0.5).sin() * (-PI * PI * t_final).exp();
assert!(
(u_final[mid] - exact).abs() < 0.02,
"Transient: u={}, exact={exact} (t={t_final})",
u_final[mid]
);
}
#[test]
fn test_transient_steady_state() {
let opts = FEM1DTransientOptions {
dt: 0.01,
num_steps: 50,
save_every: 50,
..Default::default()
};
let result = solve_transient_1d(
&|_| 1.0,
&|_| 0.0,
&|_, _| 0.0,
0.0,
1.0,
10,
&|_| 1.0,
&FEM1DBoundaryCondition::Dirichlet(1.0),
&FEM1DBoundaryCondition::Dirichlet(1.0),
&opts,
)
.expect("Should succeed");
let u_final = &result.u[result.u.len() - 1];
for i in 0..u_final.len() {
assert!(
(u_final[i] - 1.0).abs() < 1e-6,
"Steady: node {i}, u={}",
u_final[i]
);
}
}
#[test]
fn test_error_indicators_nonneg() {
let result = solve_steady_1d(
&|_| 1.0,
&|_| 0.0,
&|_| 2.0,
0.0,
1.0,
10,
&FEM1DBoundaryCondition::Dirichlet(0.0),
&FEM1DBoundaryCondition::Dirichlet(0.0),
&FEM1DSteadyOptions::default(),
)
.expect("Should succeed");
for &val in result.error_indicators.iter() {
assert!(val >= 0.0, "Error indicator must be non-negative");
}
}
#[test]
fn test_refinement_marking() {
let indicators = Array1::from_vec(vec![1.0, 0.1, 0.5, 0.2, 0.05]);
let marks = mark_for_refinement(&indicators, 0.5);
assert!(marks[0].refine, "Largest element should be marked");
assert_eq!(marks.len(), 5);
}
#[test]
fn test_eval_basis_linear_partition() {
for &xi in &[-1.0, -0.5, 0.0, 0.5, 1.0] {
let (phi, _) = eval_basis(xi, FEM1DElementType::Linear);
let sum: f64 = phi.iter().sum();
assert!(
(sum - 1.0).abs() < 1e-14,
"Linear partition of unity at xi={xi}: sum={sum}"
);
}
}
#[test]
fn test_eval_basis_quadratic_partition() {
for &xi in &[-1.0, -0.5, 0.0, 0.5, 1.0] {
let (phi, _) = eval_basis(xi, FEM1DElementType::Quadratic);
let sum: f64 = phi.iter().sum();
assert!(
(sum - 1.0).abs() < 1e-14,
"Quadratic partition of unity at xi={xi}: sum={sum}"
);
}
}
#[test]
fn test_mass_matrix_symmetry() {
let mesh = Mesh1D::uniform(0.0, 1.0, 5, FEM1DElementType::Linear).expect("mesh");
let m = assemble_mass_matrix(&mesh, 3).expect("mass matrix");
let n = m.shape()[0];
for i in 0..n {
for j in 0..n {
assert!(
(m[[i, j]] - m[[j, i]]).abs() < 1e-14,
"Mass matrix not symmetric at [{i},{j}]"
);
}
}
}
#[test]
fn test_stiffness_matrix_symmetry() {
let mesh = Mesh1D::uniform(0.0, 1.0, 5, FEM1DElementType::Linear).expect("mesh");
let (k, _) = assemble_system(&mesh, &|_| 1.0, &|_| 0.0, &|_| 0.0, 3).expect("assembly");
let n = k.shape()[0];
for i in 0..n {
for j in 0..n {
assert!(
(k[[i, j]] - k[[j, i]]).abs() < 1e-14,
"Stiffness matrix not symmetric at [{i},{j}]"
);
}
}
}
}