Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,514 @@
//! Classical PDE solvers for comparison.
//!
//! Provides finite difference solvers to compare against neural operators.
use neuralop_studio_shared::{BCType, PDEDefinition, PDEType, SolutionField};
/// Classical PDE solver.
#[derive(Debug)]
pub struct ClassicalSolver {
/// Maximum iterations.
max_iterations: usize,
/// Convergence tolerance.
tolerance: f64,
}
impl Default for ClassicalSolver {
fn default() -> Self {
Self::new()
}
}
impl ClassicalSolver {
/// Create a new classical solver.
#[must_use]
pub fn new() -> Self {
Self {
max_iterations: 10000,
tolerance: 1e-6,
}
}
/// Set maximum iterations.
#[must_use]
pub fn with_max_iterations(mut self, max_iter: usize) -> Self {
self.max_iterations = max_iter;
self
}
/// Set convergence tolerance.
#[must_use]
pub fn with_tolerance(mut self, tol: f64) -> Self {
self.tolerance = tol;
self
}
/// Solve the PDE.
pub fn solve(&self, pde: &PDEDefinition) -> Result<SolutionField, String> {
match pde.pde_type {
PDEType::Poisson => self.solve_poisson(pde),
PDEType::Heat => self.solve_heat(pde),
PDEType::Wave => self.solve_wave(pde),
PDEType::Burgers => self.solve_burgers(pde),
_ => Err(format!("Unsupported PDE type: {:?}", pde.pde_type)),
}
}
/// Solve Poisson equation using Gauss-Seidel iteration.
fn solve_poisson(&self, pde: &PDEDefinition) -> Result<SolutionField, String> {
let nx = pde.domain.resolution[0];
let ny = if pde.domain.dimensions > 1 {
pde.domain.resolution.get(1).copied().unwrap_or(1)
} else {
1
};
let x_range = pde.domain.bounds[0];
let y_range = if pde.domain.dimensions > 1 {
pde.domain.bounds.get(1).copied().unwrap_or((0.0, 1.0))
} else {
(0.0, 1.0)
};
let dx = (x_range.1 - x_range.0) / (nx - 1) as f64;
let dy = if ny > 1 {
(y_range.1 - y_range.0) / (ny - 1) as f64
} else {
dx
};
let mut u = vec![0.0; nx * ny];
let mut u_new = vec![0.0; nx * ny];
// Apply boundary conditions
self.apply_boundary_conditions(&mut u, pde, nx, ny);
// Source term
let source: Vec<f64> = (0..nx * ny)
.map(|idx| {
let i = idx % nx;
let j = idx / nx;
let x = x_range.0 + i as f64 * dx;
let y = y_range.0 + j as f64 * dy;
self.evaluate_source(pde, x, y)
})
.collect();
// Gauss-Seidel iteration
for iteration in 0..self.max_iterations {
u_new.copy_from_slice(&u);
for j in 1..ny.saturating_sub(1).max(1) {
for i in 1..nx.saturating_sub(1) {
let idx = j * nx + i;
let idx_left = j * nx + i - 1;
let idx_right = j * nx + i + 1;
let idx_down = (j.saturating_sub(1)) * nx + i;
let idx_up = (j + 1).min(ny - 1) * nx + i;
if ny == 1 {
// 1D case
u_new[idx] = 0.5 * (u[idx_left] + u[idx_right] + dx * dx * source[idx]);
} else {
// 2D case
u_new[idx] = 0.25
* (u[idx_left]
+ u_new[idx_right.min(nx * ny - 1)]
+ u[idx_down]
+ u_new[idx_up.min(nx * ny - 1)]
+ dx * dy * source[idx]);
}
}
}
// Check convergence
let diff: f64 = u
.iter()
.zip(u_new.iter())
.map(|(a, b)| (a - b).abs())
.sum::<f64>()
/ (nx * ny) as f64;
u.copy_from_slice(&u_new);
if diff < self.tolerance {
break;
}
if iteration == self.max_iterations - 1 {
// Warning: did not converge, but continue
}
}
// Generate coordinates
let coordinates: Vec<Vec<f64>> = (0..nx * ny)
.map(|idx| {
let i = idx % nx;
let j = idx / nx;
let x = x_range.0 + i as f64 * dx;
let y = y_range.0 + j as f64 * dy;
vec![x, y]
})
.collect();
Ok(SolutionField {
coordinates,
values: u,
name: "u".to_string(),
time: None,
})
}
/// Solve heat equation using explicit finite differences.
fn solve_heat(&self, pde: &PDEDefinition) -> Result<SolutionField, String> {
let nx = pde.domain.resolution[0];
let (t_start, t_end) = pde.domain.time_bounds.unwrap_or((0.0, 1.0));
let nt = pde.domain.time_resolution.unwrap_or(100);
let x_range = pde.domain.bounds[0];
let dx = (x_range.1 - x_range.0) / (nx - 1) as f64;
let dt = (t_end - t_start) / nt as f64;
let alpha = pde.parameters.diffusion.unwrap_or(0.01);
// CFL condition check
let cfl = alpha * dt / (dx * dx);
if cfl > 0.5 {
// Adjust dt if unstable
let _dt = 0.4 * dx * dx / alpha;
}
let mut u = vec![0.0; nx];
let mut u_new = vec![0.0; nx];
// Apply initial condition
if let Some(ref ic) = pde.initial_condition {
for i in 0..nx {
let x = x_range.0 + i as f64 * dx;
u[i] = self.evaluate_initial(ic, x);
}
}
// Time stepping
for _ in 0..nt {
u_new.copy_from_slice(&u);
for i in 1..nx - 1 {
let laplacian = (u[i + 1] - 2.0 * u[i] + u[i - 1]) / (dx * dx);
u_new[i] = u[i] + dt * alpha * laplacian;
}
// Apply boundary conditions
u_new[0] = 0.0;
u_new[nx - 1] = 0.0;
u.copy_from_slice(&u_new);
}
let coordinates: Vec<Vec<f64>> = (0..nx)
.map(|i| {
let x = x_range.0 + i as f64 * dx;
vec![x]
})
.collect();
Ok(SolutionField {
coordinates,
values: u,
name: "u".to_string(),
time: Some(t_end),
})
}
/// Solve wave equation using explicit finite differences.
fn solve_wave(&self, pde: &PDEDefinition) -> Result<SolutionField, String> {
let nx = pde.domain.resolution[0];
let (t_start, t_end) = pde.domain.time_bounds.unwrap_or((0.0, 1.0));
let nt = pde.domain.time_resolution.unwrap_or(100);
let x_range = pde.domain.bounds[0];
let dx = (x_range.1 - x_range.0) / (nx - 1) as f64;
let dt = (t_end - t_start) / nt as f64;
let c = pde.parameters.wave_speed.unwrap_or(1.0);
let courant = c * dt / dx;
if courant > 1.0 {
// Warn about stability
}
let mut u = vec![0.0; nx];
let mut u_prev = vec![0.0; nx];
let mut u_new = vec![0.0; nx];
// Initial conditions
if let Some(ref ic) = pde.initial_condition {
for i in 0..nx {
let x = x_range.0 + i as f64 * dx;
u[i] = self.evaluate_initial(ic, x);
u_prev[i] = u[i]; // Zero initial velocity assumed
}
}
// Time stepping
for _ in 0..nt {
for i in 1..nx - 1 {
let laplacian = (u[i + 1] - 2.0 * u[i] + u[i - 1]) / (dx * dx);
u_new[i] = 2.0 * u[i] - u_prev[i] + dt * dt * c * c * laplacian;
}
u_new[0] = 0.0;
u_new[nx - 1] = 0.0;
u_prev.copy_from_slice(&u);
u.copy_from_slice(&u_new);
}
let coordinates: Vec<Vec<f64>> = (0..nx).map(|i| vec![x_range.0 + i as f64 * dx]).collect();
Ok(SolutionField {
coordinates,
values: u,
name: "u".to_string(),
time: Some(t_end),
})
}
/// Solve Burgers' equation using Lax-Wendroff scheme.
fn solve_burgers(&self, pde: &PDEDefinition) -> Result<SolutionField, String> {
let nx = pde.domain.resolution[0];
let (t_start, t_end) = pde.domain.time_bounds.unwrap_or((0.0, 1.0));
let nt = pde.domain.time_resolution.unwrap_or(100);
let x_range = pde.domain.bounds[0];
let dx = (x_range.1 - x_range.0) / (nx - 1) as f64;
let dt = (t_end - t_start) / nt as f64;
let nu = pde.parameters.diffusion.unwrap_or(0.01);
let mut u = vec![0.0; nx];
let mut u_new = vec![0.0; nx];
// Initial condition
if let Some(ref ic) = pde.initial_condition {
for i in 0..nx {
let x = x_range.0 + i as f64 * dx;
u[i] = self.evaluate_initial(ic, x);
}
}
// Time stepping
for _ in 0..nt {
u_new.copy_from_slice(&u);
for i in 1..nx - 1 {
let laplacian = (u[i + 1] - 2.0 * u[i] + u[i - 1]) / (dx * dx);
let du_dx = (u[i + 1] - u[i - 1]) / (2.0 * dx);
let convection = u[i] * du_dx;
u_new[i] = u[i] + dt * (nu * laplacian - convection);
}
// Periodic boundary conditions
u_new[0] = u_new[nx - 2];
u_new[nx - 1] = u_new[1];
u.copy_from_slice(&u_new);
}
let coordinates: Vec<Vec<f64>> = (0..nx).map(|i| vec![x_range.0 + i as f64 * dx]).collect();
Ok(SolutionField {
coordinates,
values: u,
name: "u".to_string(),
time: Some(t_end),
})
}
/// Apply boundary conditions.
fn apply_boundary_conditions(&self, u: &mut [f64], pde: &PDEDefinition, nx: usize, ny: usize) {
for bc in &pde.boundary_conditions {
match bc.bc_type {
BCType::Dirichlet => {
let value: f64 = bc.value.parse().unwrap_or(0.0);
match bc.boundary.as_str() {
"left" => {
for j in 0..ny {
u[j * nx] = value;
}
}
"right" => {
for j in 0..ny {
u[j * nx + nx - 1] = value;
}
}
"bottom" => {
for i in 0..nx {
u[i] = value;
}
}
"top" => {
for i in 0..nx {
u[(ny - 1) * nx + i] = value;
}
}
"all" | _ => {
// All boundaries
for i in 0..nx {
u[i] = value;
u[(ny - 1) * nx + i] = value;
}
for j in 0..ny {
u[j * nx] = value;
u[j * nx + nx - 1] = value;
}
}
}
}
BCType::Periodic => {
// Handled in time stepping
}
_ => {}
}
}
}
/// Evaluate source term.
fn evaluate_source(&self, pde: &PDEDefinition, x: f64, y: f64) -> f64 {
if let Some(ref source) = pde.parameters.source_term {
// Simple pattern matching for common sources
if source.contains("sin") {
(std::f64::consts::PI * x).sin() * (std::f64::consts::PI * y).sin()
} else {
source.parse().unwrap_or(0.0)
}
} else {
0.0
}
}
/// Evaluate initial condition.
fn evaluate_initial(&self, ic: &neuralop_studio_shared::InitialCondition, x: f64) -> f64 {
if ic.function.contains("sin") {
(std::f64::consts::PI * x).sin()
} else if ic.function.contains("gauss") {
(-((x - 0.5) * (x - 0.5)) / 0.01).exp()
} else {
ic.function.parse().unwrap_or(0.0)
}
}
}
/// Compare neural operator solution with classical solution.
#[must_use]
pub fn compare_solutions(neural_solution: &[f64], classical_solution: &[f64]) -> (f64, f64, f64) {
if neural_solution.is_empty() || classical_solution.is_empty() {
return (0.0, 0.0, 0.0);
}
let min_len = neural_solution.len().min(classical_solution.len());
let mse: f64 = neural_solution[..min_len]
.iter()
.zip(classical_solution[..min_len].iter())
.map(|(n, c)| (n - c).powi(2))
.sum::<f64>()
/ min_len as f64;
let classical_norm: f64 = classical_solution[..min_len]
.iter()
.map(|c| c.powi(2))
.sum::<f64>()
.sqrt();
let error_norm: f64 = neural_solution[..min_len]
.iter()
.zip(classical_solution[..min_len].iter())
.map(|(n, c)| (n - c).powi(2))
.sum::<f64>()
.sqrt();
let relative_l2 = if classical_norm > 1e-10 {
error_norm / classical_norm
} else {
error_norm
};
let max_error: f64 = neural_solution[..min_len]
.iter()
.zip(classical_solution[..min_len].iter())
.map(|(n, c)| (n - c).abs())
.fold(0.0, f64::max);
(mse, relative_l2, max_error)
}
#[cfg(test)]
mod tests {
use super::*;
use neuralop_studio_shared::{
sample_burgers_problem, sample_heat_problem, sample_poisson_problem,
};
#[test]
fn test_solver_creation() {
let solver = ClassicalSolver::new();
assert_eq!(solver.max_iterations, 10000);
}
#[test]
fn test_solve_poisson() {
let solver = ClassicalSolver::new().with_max_iterations(100);
let pde = sample_poisson_problem();
let result = solver.solve(&pde);
assert!(result.is_ok());
let solution = result.unwrap();
assert!(!solution.values.is_empty());
}
#[test]
fn test_solve_heat() {
let solver = ClassicalSolver::new();
let pde = sample_heat_problem();
let result = solver.solve(&pde);
assert!(result.is_ok());
let solution = result.unwrap();
assert!(solution.time.is_some());
}
#[test]
fn test_solve_burgers() {
let solver = ClassicalSolver::new();
let pde = sample_burgers_problem();
let result = solver.solve(&pde);
assert!(result.is_ok());
}
#[test]
fn test_compare_solutions() {
let neural = vec![0.0, 0.1, 0.2, 0.1, 0.0];
let classical = vec![0.0, 0.1, 0.2, 0.1, 0.0];
let (mse, rel_l2, max_err) = compare_solutions(&neural, &classical);
assert!(mse < 1e-10);
assert!(rel_l2 < 1e-10);
assert!(max_err < 1e-10);
}
#[test]
fn test_compare_solutions_with_error() {
let neural = vec![0.0, 0.11, 0.2, 0.1, 0.0]; // Small error
let classical = vec![0.0, 0.1, 0.2, 0.1, 0.0];
let (mse, _rel_l2, max_err) = compare_solutions(&neural, &classical);
assert!(mse > 0.0);
assert!((max_err - 0.01).abs() < 1e-10);
}
}