use crate::error::{NumRs2Error, Result};
use num_traits::Float;
use std::fmt::Debug;
#[derive(Debug, Clone, Copy)]
pub enum BoundaryCondition<T> {
Dirichlet(T),
Neumann(T),
Periodic,
}
#[derive(Debug, Clone)]
pub struct PdeResult<T> {
pub u: Vec<Vec<T>>,
pub t: Vec<T>,
pub x: Vec<T>,
pub nx: usize,
pub nt: usize,
pub success: bool,
pub message: String,
}
#[derive(Debug, Clone)]
pub struct Pde2dResult<T> {
pub u: Vec<Vec<T>>,
pub t: Vec<T>,
pub x: Vec<T>,
pub y: Vec<T>,
pub nx: usize,
pub ny: usize,
pub nt: usize,
pub success: bool,
}
pub fn solve_heat_1d<T>(
initial: &[T],
alpha: T,
dx: T,
dt: T,
nt: usize,
bc_left: BoundaryCondition<T>,
bc_right: BoundaryCondition<T>,
) -> Result<PdeResult<T>>
where
T: Float + Debug + std::iter::Sum,
{
let nx = initial.len();
if nx < 3 {
return Err(NumRs2Error::ValueError(
"Need at least 3 spatial points".to_string(),
));
}
let r = alpha * dt / (dx * dx);
let half = T::from(0.5).expect("0.5 is representable as Float");
if r > half {
return Err(NumRs2Error::ValueError(format!(
"Unstable: r = {:?} > 0.5. Reduce dt or increase dx.",
r
)));
}
let mut u = vec![initial.to_vec()];
let mut current = initial.to_vec();
let mut next = vec![T::zero(); nx];
let mut t = vec![T::zero()];
let x: Vec<T> = (0..nx)
.map(|i| T::from(i).expect("i is representable as Float") * dx)
.collect();
for n in 0..nt {
apply_bc_1d(&mut current, bc_left, bc_right, dx);
for i in 1..nx - 1 {
let d2u = current[i + 1]
- T::from(2.0).expect("2.0 is representable as Float") * current[i]
+ current[i - 1];
next[i] = current[i] + r * d2u;
}
apply_bc_to_next(&mut next, bc_left, bc_right, ¤t, dx);
current = next.clone();
u.push(current.clone());
t.push(T::from(n + 1).expect("n+1 is representable as Float") * dt);
}
Ok(PdeResult {
u,
t,
x,
nx,
nt: nt + 1,
success: true,
message: "Heat equation solved successfully".to_string(),
})
}
fn apply_bc_1d<T: Float + Debug>(
u: &mut [T],
bc_left: BoundaryCondition<T>,
bc_right: BoundaryCondition<T>,
dx: T,
) {
let n = u.len();
match bc_left {
BoundaryCondition::Dirichlet(val) => u[0] = val,
BoundaryCondition::Neumann(val) => u[0] = u[1] - dx * val,
BoundaryCondition::Periodic => u[0] = u[n - 2],
}
match bc_right {
BoundaryCondition::Dirichlet(val) => u[n - 1] = val,
BoundaryCondition::Neumann(val) => u[n - 1] = u[n - 2] + dx * val,
BoundaryCondition::Periodic => u[n - 1] = u[1],
}
}
fn apply_bc_to_next<T: Float + Debug>(
next: &mut [T],
bc_left: BoundaryCondition<T>,
bc_right: BoundaryCondition<T>,
current: &[T],
dx: T,
) {
let n = next.len();
match bc_left {
BoundaryCondition::Dirichlet(val) => next[0] = val,
BoundaryCondition::Neumann(val) => next[0] = next[1] - dx * val,
BoundaryCondition::Periodic => next[0] = next[n - 2],
}
match bc_right {
BoundaryCondition::Dirichlet(val) => next[n - 1] = val,
BoundaryCondition::Neumann(val) => next[n - 1] = next[n - 2] + dx * val,
BoundaryCondition::Periodic => next[n - 1] = next[1],
}
}
pub fn solve_heat_1d_crank_nicolson<T>(
initial: &[T],
alpha: T,
dx: T,
dt: T,
nt: usize,
bc_left: BoundaryCondition<T>,
bc_right: BoundaryCondition<T>,
) -> Result<PdeResult<T>>
where
T: Float + Debug + std::iter::Sum,
{
let nx = initial.len();
if nx < 3 {
return Err(NumRs2Error::ValueError(
"Need at least 3 spatial points".to_string(),
));
}
let r = alpha * dt / (dx * dx);
let half = T::from(0.5).expect("0.5 is representable as Float");
let two = T::from(2.0).expect("2.0 is representable as Float");
let one = T::one();
let a_diag = one + r; let a_off = -half * r; let b_diag = one - r; let b_off = half * r;
let mut u = vec![initial.to_vec()];
let mut current = initial.to_vec();
let mut t = vec![T::zero()];
let x: Vec<T> = (0..nx)
.map(|i| T::from(i).expect("i is representable as Float") * dx)
.collect();
for n in 0..nt {
apply_bc_1d(&mut current, bc_left, bc_right, dx);
let mut rhs = vec![T::zero(); nx - 2];
for i in 1..nx - 1 {
rhs[i - 1] = b_off * current[i - 1] + b_diag * current[i] + b_off * current[i + 1];
}
if let BoundaryCondition::Dirichlet(val) = bc_left {
rhs[0] = rhs[0] - a_off * val;
}
if let BoundaryCondition::Dirichlet(val) = bc_right {
rhs[nx - 3] = rhs[nx - 3] - a_off * val;
}
let solution = thomas_algorithm(
&vec![a_off; nx - 3],
&vec![a_diag; nx - 2],
&vec![a_off; nx - 3],
&rhs,
)?;
current[1..(nx - 1)].copy_from_slice(&solution[..((nx - 1) - 1)]);
apply_bc_1d(&mut current, bc_left, bc_right, dx);
u.push(current.clone());
t.push(T::from(n + 1).expect("n+1 is representable as Float") * dt);
}
Ok(PdeResult {
u,
t,
x,
nx,
nt: nt + 1,
success: true,
message: "Crank-Nicolson heat equation solved successfully".to_string(),
})
}
fn thomas_algorithm<T>(a: &[T], b: &[T], c: &[T], d: &[T]) -> Result<Vec<T>>
where
T: Float + Debug,
{
let n = b.len();
if a.len() != n - 1 || c.len() != n - 1 || d.len() != n {
return Err(NumRs2Error::ValueError(
"Invalid sizes for tridiagonal system".to_string(),
));
}
let mut c_prime = vec![T::zero(); n - 1];
let mut d_prime = vec![T::zero(); n];
c_prime[0] = c[0] / b[0];
d_prime[0] = d[0] / b[0];
for i in 1..n - 1 {
let denom = b[i] - a[i - 1] * c_prime[i - 1];
if denom.abs() < T::epsilon() {
return Err(NumRs2Error::ValueError(
"Division by zero in Thomas algorithm".to_string(),
));
}
c_prime[i] = c[i] / denom;
d_prime[i] = (d[i] - a[i - 1] * d_prime[i - 1]) / denom;
}
let denom = b[n - 1] - a[n - 2] * c_prime[n - 2];
d_prime[n - 1] = (d[n - 1] - a[n - 2] * d_prime[n - 2]) / denom;
let mut x = vec![T::zero(); n];
x[n - 1] = d_prime[n - 1];
for i in (0..n - 1).rev() {
x[i] = d_prime[i] - c_prime[i] * x[i + 1];
}
Ok(x)
}
pub fn solve_wave_1d<T>(
initial_u: &[T],
initial_v: &[T],
c: T,
dx: T,
dt: T,
nt: usize,
bc_left: BoundaryCondition<T>,
bc_right: BoundaryCondition<T>,
) -> Result<PdeResult<T>>
where
T: Float + Debug + std::iter::Sum,
{
let nx = initial_u.len();
if nx < 3 {
return Err(NumRs2Error::ValueError(
"Need at least 3 spatial points".to_string(),
));
}
if initial_v.len() != nx {
return Err(NumRs2Error::DimensionMismatch(
"Initial u and v must have same length".to_string(),
));
}
let courant = c * dt / dx;
if courant > T::one() {
return Err(NumRs2Error::ValueError(format!(
"Unstable: Courant number {:?} > 1. Reduce dt or increase dx.",
courant
)));
}
let courant_sq = courant * courant;
let two = T::from(2.0).expect("2.0 is representable as Float");
let mut u_curr = initial_u.to_vec();
let mut u_prev;
let mut u_next = vec![T::zero(); nx];
for i in 1..nx - 1 {
let d2u = u_curr[i + 1] - two * u_curr[i] + u_curr[i - 1];
u_next[i] = u_curr[i] + dt * initial_v[i] + courant_sq * d2u / two;
}
apply_bc_1d(&mut u_next, bc_left, bc_right, dx);
let mut result_u = vec![u_curr.clone(), u_next.clone()];
let mut t = vec![T::zero(), dt];
let x: Vec<T> = (0..nx)
.map(|i| T::from(i).expect("i is representable as Float") * dx)
.collect();
u_prev = u_curr;
u_curr = u_next.clone();
for n in 1..nt {
for i in 1..nx - 1 {
let d2u = u_curr[i + 1] - two * u_curr[i] + u_curr[i - 1];
u_next[i] = two * u_curr[i] - u_prev[i] + courant_sq * d2u;
}
apply_bc_1d(&mut u_next, bc_left, bc_right, dx);
u_prev = u_curr.clone();
u_curr = u_next.clone();
result_u.push(u_curr.clone());
t.push(T::from(n + 1).expect("n+1 is representable as Float") * dt);
}
Ok(PdeResult {
u: result_u,
t,
x,
nx,
nt: nt + 1,
success: true,
message: "Wave equation solved successfully".to_string(),
})
}
pub fn solve_heat_2d<T>(
initial: &[T],
nx: usize,
ny: usize,
alpha: T,
dx: T,
dy: T,
dt: T,
nt: usize,
bc: BoundaryCondition<T>,
) -> Result<Pde2dResult<T>>
where
T: Float + Debug + std::iter::Sum,
{
if initial.len() != nx * ny {
return Err(NumRs2Error::DimensionMismatch(format!(
"Initial condition size {} doesn't match nx*ny={}",
initial.len(),
nx * ny
)));
}
if nx < 3 || ny < 3 {
return Err(NumRs2Error::ValueError(
"Need at least 3 points in each dimension".to_string(),
));
}
let rx = alpha * dt / (dx * dx);
let ry = alpha * dt / (dy * dy);
let r_total = rx + ry;
let half = T::from(0.5).expect("0.5 is representable as Float");
if r_total > half {
return Err(NumRs2Error::ValueError(format!(
"Unstable: r = {:?} > 0.5. Reduce dt or increase dx/dy.",
r_total
)));
}
let two = T::from(2.0).expect("2.0 is representable as Float");
let mut u_curr = initial.to_vec();
let mut u_next = vec![T::zero(); nx * ny];
let mut result_u = vec![u_curr.clone()];
let mut t = vec![T::zero()];
let x: Vec<T> = (0..nx)
.map(|i| T::from(i).expect("i is representable as Float") * dx)
.collect();
let y: Vec<T> = (0..ny)
.map(|j| T::from(j).expect("j is representable as Float") * dy)
.collect();
for n in 0..nt {
apply_bc_2d(&mut u_curr, nx, ny, bc);
for j in 1..ny - 1 {
for i in 1..nx - 1 {
let idx = j * nx + i;
let d2ux = u_curr[idx + 1] - two * u_curr[idx] + u_curr[idx - 1];
let d2uy = u_curr[idx + nx] - two * u_curr[idx] + u_curr[idx - nx];
u_next[idx] = u_curr[idx] + rx * d2ux + ry * d2uy;
}
}
apply_bc_2d(&mut u_next, nx, ny, bc);
u_curr = u_next.clone();
result_u.push(u_curr.clone());
t.push(T::from(n + 1).expect("n+1 is representable as Float") * dt);
}
Ok(Pde2dResult {
u: result_u,
t,
x,
y,
nx,
ny,
nt: nt + 1,
success: true,
})
}
fn apply_bc_2d<T: Float>(u: &mut [T], nx: usize, ny: usize, bc: BoundaryCondition<T>) {
match bc {
BoundaryCondition::Dirichlet(val) => {
for i in 0..nx {
u[i] = val; u[(ny - 1) * nx + i] = val; }
for j in 0..ny {
u[j * nx] = val; u[j * nx + nx - 1] = val; }
}
BoundaryCondition::Neumann(_) => {
for i in 1..nx - 1 {
u[i] = u[nx + i]; u[(ny - 1) * nx + i] = u[(ny - 2) * nx + i]; }
for j in 1..ny - 1 {
u[j * nx] = u[j * nx + 1]; u[j * nx + nx - 1] = u[j * nx + nx - 2]; }
u[0] = u[nx + 1];
u[nx - 1] = u[nx + nx - 2];
u[(ny - 1) * nx] = u[(ny - 2) * nx + 1];
u[ny * nx - 1] = u[(ny - 1) * nx - 2];
}
BoundaryCondition::Periodic => {
for i in 0..nx {
u[i] = u[(ny - 2) * nx + i]; u[(ny - 1) * nx + i] = u[nx + i]; }
for j in 0..ny {
u[j * nx] = u[j * nx + nx - 2]; u[j * nx + nx - 1] = u[j * nx + 1]; }
}
}
}
pub fn solve_poisson_2d<T>(
f: &[T],
nx: usize,
ny: usize,
dx: T,
dy: T,
bc: BoundaryCondition<T>,
max_iter: usize,
tol: T,
) -> Result<(Vec<T>, usize, T)>
where
T: Float + Debug + std::iter::Sum,
{
if f.len() != nx * ny {
return Err(NumRs2Error::DimensionMismatch(format!(
"Source size {} doesn't match nx*ny={}",
f.len(),
nx * ny
)));
}
let dx2 = dx * dx;
let dy2 = dy * dy;
let two = T::from(2.0).expect("2.0 is representable as Float");
let denom = two * (dx2 + dy2);
let mut u = vec![T::zero(); nx * ny];
let mut u_new = vec![T::zero(); nx * ny];
for iter in 0..max_iter {
apply_bc_2d(&mut u, nx, ny, bc);
let mut max_diff = T::zero();
for j in 1..ny - 1 {
for i in 1..nx - 1 {
let idx = j * nx + i;
let u_xx = u[idx + 1] + u[idx - 1];
let u_yy = u[idx + nx] + u[idx - nx];
u_new[idx] = (dy2 * u_xx + dx2 * u_yy - dx2 * dy2 * f[idx]) / denom;
let diff = (u_new[idx] - u[idx]).abs();
if diff > max_diff {
max_diff = diff;
}
}
}
if max_diff < tol {
return Ok((u_new, iter + 1, max_diff));
}
std::mem::swap(&mut u, &mut u_new);
}
Ok((u, max_iter, T::infinity()))
}
pub fn solve_poisson_gauss_seidel<T>(
f: &[T],
nx: usize,
ny: usize,
dx: T,
dy: T,
bc: BoundaryCondition<T>,
max_iter: usize,
tol: T,
) -> Result<(Vec<T>, usize, T)>
where
T: Float + Debug + std::iter::Sum,
{
if f.len() != nx * ny {
return Err(NumRs2Error::DimensionMismatch(format!(
"Source size {} doesn't match nx*ny={}",
f.len(),
nx * ny
)));
}
let dx2 = dx * dx;
let dy2 = dy * dy;
let two = T::from(2.0).expect("2.0 is representable as Float");
let denom = two * (dx2 + dy2);
let mut u = vec![T::zero(); nx * ny];
for iter in 0..max_iter {
apply_bc_2d(&mut u, nx, ny, bc);
let mut max_diff = T::zero();
for j in 1..ny - 1 {
for i in 1..nx - 1 {
let idx = j * nx + i;
let u_xx = u[idx + 1] + u[idx - 1];
let u_yy = u[idx + nx] + u[idx - nx];
let u_old = u[idx];
u[idx] = (dy2 * u_xx + dx2 * u_yy - dx2 * dy2 * f[idx]) / denom;
let diff = (u[idx] - u_old).abs();
if diff > max_diff {
max_diff = diff;
}
}
}
if max_diff < tol {
return Ok((u, iter + 1, max_diff));
}
}
Ok((u, max_iter, T::infinity()))
}
pub fn solve_poisson_sor<T>(
f: &[T],
nx: usize,
ny: usize,
dx: T,
dy: T,
bc: BoundaryCondition<T>,
omega: T,
max_iter: usize,
tol: T,
) -> Result<(Vec<T>, usize, T)>
where
T: Float + Debug + std::iter::Sum,
{
if f.len() != nx * ny {
return Err(NumRs2Error::DimensionMismatch(format!(
"Source size {} doesn't match nx*ny={}",
f.len(),
nx * ny
)));
}
if omega <= T::zero() || omega >= T::from(2.0).expect("2.0 is representable as Float") {
return Err(NumRs2Error::ValueError(
"Omega must be in (0, 2) for SOR convergence".to_string(),
));
}
let dx2 = dx * dx;
let dy2 = dy * dy;
let two = T::from(2.0).expect("2.0 is representable as Float");
let denom = two * (dx2 + dy2);
let one_minus_omega = T::one() - omega;
let mut u = vec![T::zero(); nx * ny];
for iter in 0..max_iter {
apply_bc_2d(&mut u, nx, ny, bc);
let mut max_diff = T::zero();
for j in 1..ny - 1 {
for i in 1..nx - 1 {
let idx = j * nx + i;
let u_xx = u[idx + 1] + u[idx - 1];
let u_yy = u[idx + nx] + u[idx - nx];
let u_old = u[idx];
let u_gs = (dy2 * u_xx + dx2 * u_yy - dx2 * dy2 * f[idx]) / denom;
u[idx] = one_minus_omega * u_old + omega * u_gs;
let diff = (u[idx] - u_old).abs();
if diff > max_diff {
max_diff = diff;
}
}
}
if max_diff < tol {
return Ok((u, iter + 1, max_diff));
}
}
Ok((u, max_iter, T::infinity()))
}
pub fn optimal_omega<T>(nx: usize, ny: usize) -> T
where
T: Float,
{
let pi = T::from(std::f64::consts::PI).expect("PI is representable as Float");
let n = T::from(nx.min(ny)).expect("nx.min(ny) is representable as Float");
let rho = (pi / (n + T::one())).cos();
T::from(2.0).expect("2.0 is representable as Float")
/ (T::one() + (T::one() - rho * rho).sqrt())
}
pub fn method_of_lines_heat<T>(
nx: usize,
alpha: T,
dx: T,
bc_left: BoundaryCondition<T>,
bc_right: BoundaryCondition<T>,
) -> impl Fn(T, &[T]) -> Vec<T>
where
T: Float + Debug,
{
let coef = alpha / (dx * dx);
let two = T::from(2.0).expect("2.0 is representable as Float");
move |_t: T, u: &[T]| -> Vec<T> {
let mut dudt = vec![T::zero(); nx];
for i in 1..nx - 1 {
dudt[i] = coef * (u[i + 1] - two * u[i] + u[i - 1]);
}
match bc_left {
BoundaryCondition::Dirichlet(_) => dudt[0] = T::zero(),
BoundaryCondition::Neumann(val) => {
let u_ghost =
u[1] - T::from(2.0).expect("2.0 is representable as Float") * dx * val;
dudt[0] = coef * (u[1] - two * u[0] + u_ghost);
}
BoundaryCondition::Periodic => {
dudt[0] = coef * (u[1] - two * u[0] + u[nx - 2]);
}
}
match bc_right {
BoundaryCondition::Dirichlet(_) => dudt[nx - 1] = T::zero(),
BoundaryCondition::Neumann(val) => {
let u_ghost =
u[nx - 2] + T::from(2.0).expect("2.0 is representable as Float") * dx * val;
dudt[nx - 1] = coef * (u_ghost - two * u[nx - 1] + u[nx - 2]);
}
BoundaryCondition::Periodic => {
dudt[nx - 1] = coef * (u[1] - two * u[nx - 1] + u[nx - 2]);
}
}
dudt
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_heat_1d_basic() {
let mut initial = vec![0.0f64; 50];
for i in 20..30 {
initial[i] = 1.0;
}
let result = solve_heat_1d(
&initial,
0.1,
0.02,
0.001,
100,
BoundaryCondition::Dirichlet(0.0),
BoundaryCondition::Dirichlet(0.0),
)
.expect("solve_heat_1d should succeed with valid parameters");
assert!(result.success);
assert_eq!(result.u.len(), 101);
assert_eq!(result.nx, 50);
}
#[test]
fn test_heat_1d_stability() {
let initial = vec![0.0f64; 50];
let result = solve_heat_1d(
&initial,
1.0, 0.01, 0.1, 10,
BoundaryCondition::Dirichlet(0.0),
BoundaryCondition::Dirichlet(0.0),
);
assert!(result.is_err());
}
#[test]
fn test_heat_1d_crank_nicolson() {
let initial: Vec<f64> = (0..50).map(|i| (i as f64 * 0.1).sin()).collect();
let result = solve_heat_1d_crank_nicolson(
&initial,
0.1,
0.02,
0.01, 50,
BoundaryCondition::Dirichlet(0.0),
BoundaryCondition::Dirichlet(0.0),
)
.expect("Crank-Nicolson should succeed with valid parameters");
assert!(result.success);
}
#[test]
fn test_wave_1d_basic() {
let nx = 100;
let initial_u: Vec<f64> = (0..nx)
.map(|i| {
let x = i as f64 - 50.0;
(-x * x / 100.0).exp()
})
.collect();
let initial_v = vec![0.0f64; nx];
let result = solve_wave_1d(
&initial_u,
&initial_v,
1.0, 0.1, 0.05, 100,
BoundaryCondition::Dirichlet(0.0),
BoundaryCondition::Dirichlet(0.0),
)
.expect("solve_wave_1d should succeed with valid parameters");
assert!(result.success);
assert_eq!(result.u.len(), 101);
}
#[test]
fn test_wave_1d_courant() {
let initial_u = vec![0.0f64; 50];
let initial_v = vec![0.0f64; 50];
let result = solve_wave_1d(
&initial_u,
&initial_v,
1.0,
0.1,
0.2, 10,
BoundaryCondition::Dirichlet(0.0),
BoundaryCondition::Dirichlet(0.0),
);
assert!(result.is_err());
}
#[test]
fn test_heat_2d_basic() {
let nx = 20;
let ny = 20;
let mut initial = vec![0.0f64; nx * ny];
for j in 8..12 {
for i in 8..12 {
initial[j * nx + i] = 1.0;
}
}
let result = solve_heat_2d(
&initial,
nx,
ny,
0.1,
0.1,
0.1,
0.01,
50,
BoundaryCondition::Dirichlet(0.0),
)
.expect("solve_heat_2d should succeed with valid parameters");
assert!(result.success);
assert_eq!(result.u.len(), 51);
}
#[test]
fn test_poisson_jacobi() {
let nx = 20;
let ny = 20;
let f = vec![0.0f64; nx * ny];
let (solution, _iters, _error) = solve_poisson_2d(
&f,
nx,
ny,
0.1,
0.1,
BoundaryCondition::Dirichlet(0.0),
1000,
1e-6,
)
.expect("solve_poisson_2d should succeed with valid parameters");
assert_eq!(solution.len(), nx * ny);
assert!(solution.iter().all(|&x| x.abs() < 1e-10));
}
#[test]
fn test_poisson_gauss_seidel() {
let nx = 20;
let ny = 20;
let f = vec![1.0f64; nx * ny];
let (solution, _iters, _) = solve_poisson_gauss_seidel(
&f,
nx,
ny,
0.1,
0.1,
BoundaryCondition::Dirichlet(0.0),
500,
1e-6,
)
.expect("solve_poisson_gauss_seidel should succeed with valid parameters");
assert_eq!(solution.len(), nx * ny);
let center = solution[ny / 2 * nx + nx / 2];
assert!(center.abs() > 0.0);
}
#[test]
fn test_poisson_sor() {
let nx = 30;
let ny = 30;
let f = vec![0.0f64; nx * ny];
let omega = optimal_omega::<f64>(nx, ny);
assert!(omega > 1.0 && omega < 2.0);
let (_, iters_sor, _) = solve_poisson_sor(
&f,
nx,
ny,
0.1,
0.1,
BoundaryCondition::Dirichlet(0.0),
omega,
500,
1e-6,
)
.expect("solve_poisson_sor should succeed with valid parameters");
assert!(iters_sor > 0);
}
#[test]
fn test_thomas_algorithm() {
let a = vec![-1.0f64, -1.0];
let b = vec![2.0, 2.0, 2.0];
let c = vec![-1.0f64, -1.0];
let d = vec![1.0, 0.0, 1.0];
let x = thomas_algorithm(&a, &b, &c, &d)
.expect("thomas_algorithm should succeed with valid input");
assert_eq!(x.len(), 3);
let tol = 1e-10;
assert!((2.0 * x[0] - x[1] - 1.0).abs() < tol);
assert!((-x[0] + 2.0 * x[1] - x[2]).abs() < tol);
assert!((-x[1] + 2.0 * x[2] - 1.0).abs() < tol);
}
#[test]
fn test_method_of_lines() {
let nx = 10;
let alpha = 0.1;
let dx = 0.1;
let f = method_of_lines_heat(
nx,
alpha,
dx,
BoundaryCondition::Dirichlet(0.0),
BoundaryCondition::Dirichlet(0.0),
);
let u = vec![1.0f64; nx];
let dudt = f(0.0, &u);
assert_eq!(dudt.len(), nx);
assert_eq!(dudt[0], 0.0);
assert_eq!(dudt[nx - 1], 0.0);
}
#[test]
fn test_periodic_boundary() {
let initial: Vec<f64> = (0..50)
.map(|i| (2.0 * std::f64::consts::PI * i as f64 / 50.0).sin())
.collect();
let result = solve_heat_1d(
&initial,
0.1,
0.02,
0.001,
100,
BoundaryCondition::Periodic,
BoundaryCondition::Periodic,
)
.expect("solve_heat_1d with periodic BC should succeed");
assert!(result.success);
}
#[test]
fn test_neumann_boundary() {
let initial = vec![1.0f64; 50];
let result = solve_heat_1d(
&initial,
0.1,
0.02,
0.001,
100,
BoundaryCondition::Neumann(0.0),
BoundaryCondition::Neumann(0.0),
)
.expect("solve_heat_1d with Neumann BC should succeed");
assert!(result.success);
let last = result.u.last().expect("solution should have elements");
let mean: f64 = last.iter().sum::<f64>() / last.len() as f64;
assert!((mean - 1.0).abs() < 0.1);
}
}