use crate::common::IntegrateFloat;
use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{ArrayView1, ArrayView2};
use std::f64::consts::PI;
use std::fmt;
pub trait ExactSolution<F: IntegrateFloat> {
fn evaluate(&self, coordinates: &[F]) -> F;
fn derivative(&self, coordinates: &[F], variable: usize) -> F;
fn second_derivative(&self, coordinates: &[F], variable: usize) -> F;
fn mixed_derivative(&self, coordinates: &[F], var1: usize, var2: usize) -> F {
let h = F::from(1e-8).expect("Failed to convert constant to float");
let mut coords_plus = coordinates.to_vec();
let mut coords_minus = coordinates.to_vec();
coords_plus[var2] += h;
coords_minus[var2] -= h;
let deriv_plus = self.derivative(&coords_plus, var1);
let deriv_minus = self.derivative(&coords_minus, var1);
(deriv_plus - deriv_minus)
/ (F::from(2.0).expect("Failed to convert constant to float") * h)
}
fn dimension(&self) -> usize;
}
#[derive(Debug, Clone)]
pub struct PolynomialSolution<F: IntegrateFloat> {
coefficients: Vec<F>,
}
impl<F: IntegrateFloat> PolynomialSolution<F> {
pub fn new(coefficients: Vec<F>) -> Self {
Self { coefficients }
}
}
impl<F: IntegrateFloat> ExactSolution<F> for PolynomialSolution<F> {
fn evaluate(&self, coordinates: &[F]) -> F {
let t = coordinates[0];
let mut result = F::zero();
let mut t_power = F::one();
for &coeff in &self.coefficients {
result += coeff * t_power;
t_power *= t;
}
result
}
fn derivative(&self, coordinates: &[F], variable: usize) -> F {
let t = coordinates[0];
let mut result = F::zero();
let mut t_power = F::one();
for (i, &coeff) in self.coefficients.iter().enumerate().skip(1) {
result += F::from(i).expect("Failed to convert to float") * coeff * t_power;
t_power *= t;
}
result
}
fn second_derivative(&self, coordinates: &[F], variable: usize) -> F {
let t = coordinates[0];
let mut result = F::zero();
let mut t_power = F::one();
for (i, &coeff) in self.coefficients.iter().enumerate().skip(2) {
let factor = F::from(i * (i - 1)).expect("Operation failed");
result += factor * coeff * t_power;
t_power *= t;
}
result
}
fn dimension(&self) -> usize {
1
}
}
#[derive(Debug, Clone)]
pub struct TrigonometricSolution2D<F: IntegrateFloat> {
freq_x: F,
freq_y: F,
phase_x: F,
phase_y: F,
}
impl<F: IntegrateFloat> TrigonometricSolution2D<F> {
pub fn new(freq_x: F, freq_y: F, phase_x: F, phase_y: F) -> Self {
Self {
freq_x,
freq_y,
phase_x,
phase_y,
}
}
pub fn simple(freq_x: F, freqy: F) -> Self {
Self::new(freq_x, freqy, F::zero(), F::zero())
}
}
impl<F: IntegrateFloat> ExactSolution<F> for TrigonometricSolution2D<F> {
fn evaluate(&self, coordinates: &[F]) -> F {
let x = coordinates[0];
let y = coordinates[1];
(self.freq_x * x + self.phase_x).sin() * (self.freq_y * y + self.phase_y).cos()
}
fn derivative(&self, coordinates: &[F], variable: usize) -> F {
let x = coordinates[0];
let y = coordinates[1];
match variable {
0 => {
self.freq_x
* (self.freq_x * x + self.phase_x).cos()
* (self.freq_y * y + self.phase_y).cos()
}
1 => {
-self.freq_y
* (self.freq_x * x + self.phase_x).sin()
* (self.freq_y * y + self.phase_y).sin()
}
_ => F::zero(),
}
}
fn second_derivative(&self, coordinates: &[F], variable: usize) -> F {
let x = coordinates[0];
let y = coordinates[1];
match variable {
0 => {
-self.freq_x
* self.freq_x
* (self.freq_x * x + self.phase_x).sin()
* (self.freq_y * y + self.phase_y).cos()
}
1 => {
-self.freq_y
* self.freq_y
* (self.freq_x * x + self.phase_x).sin()
* (self.freq_y * y + self.phase_y).cos()
}
_ => F::zero(),
}
}
fn dimension(&self) -> usize {
2
}
}
#[derive(Debug)]
pub struct MMSODEProblem<F: IntegrateFloat, S: ExactSolution<F>> {
exact_solution: S,
time_span: [F; 2],
_phantom: std::marker::PhantomData<F>,
}
impl<F: IntegrateFloat, S: ExactSolution<F>> MMSODEProblem<F, S> {
pub fn new(exact_solution: S, timespan: [F; 2]) -> Self {
Self {
exact_solution,
time_span: timespan,
_phantom: std::marker::PhantomData,
}
}
pub fn source_term(&self, t: F) -> F {
let coords = [t];
self.exact_solution.derivative(&coords, 0)
}
pub fn initial_condition(&self) -> F {
self.exact_solution.evaluate(&[self.time_span[0]])
}
pub fn exact_at(&self, t: F) -> F {
self.exact_solution.evaluate(&[t])
}
pub fn time_span(&self) -> [F; 2] {
self.time_span
}
}
#[derive(Debug)]
pub struct MMSPDEProblem<F: IntegrateFloat, S: ExactSolution<F>> {
exact_solution: S,
domain_x: [F; 2],
domain_y: [F; 2],
domain_z: Option<[F; 2]>,
pde_type: PDEType,
parameters: PDEParameters<F>,
_phantom: std::marker::PhantomData<F>,
}
#[derive(Debug, Clone)]
pub struct PDEParameters<F: IntegrateFloat> {
pub diffusion_coeff: F,
pub wave_speed: F,
pub advection_velocity: Vec<F>,
pub reaction_coeff: F,
pub helmholtz_k: F,
}
impl<F: IntegrateFloat> Default for PDEParameters<F> {
fn default() -> Self {
Self {
diffusion_coeff: F::one(),
wave_speed: F::one(),
advection_velocity: vec![F::zero()],
reaction_coeff: F::zero(),
helmholtz_k: F::one(),
}
}
}
#[derive(Debug, Clone)]
pub enum PDEType {
Poisson2D,
Poisson3D,
Diffusion2D,
Diffusion3D,
Wave2D,
Wave3D,
AdvectionDiffusion2D,
AdvectionDiffusion3D,
Helmholtz2D,
Helmholtz3D,
}
impl<F: IntegrateFloat, S: ExactSolution<F>> MMSPDEProblem<F, S> {
pub fn new_poisson_2d(exact_solution: S, domain_x: [F; 2], domainy: [F; 2]) -> Self {
Self {
exact_solution,
domain_x,
domain_y: domainy,
domain_z: None,
pde_type: PDEType::Poisson2D,
parameters: PDEParameters::default(),
_phantom: std::marker::PhantomData,
}
}
pub fn new_poisson_3d(
exact_solution: S,
domain_x: [F; 2],
domain_y: [F; 2],
domain_z: [F; 2],
) -> Self {
Self {
exact_solution,
domain_x,
domain_y,
domain_z: Some(domain_z),
pde_type: PDEType::Poisson3D,
parameters: PDEParameters::default(),
_phantom: std::marker::PhantomData,
}
}
pub fn new_diffusion_2d(
exact_solution: S,
domain_x: [F; 2],
domain_y: [F; 2],
diffusion_coeff: F,
) -> Self {
let mut params = PDEParameters::default();
params.diffusion_coeff = diffusion_coeff;
Self {
exact_solution,
domain_x,
domain_y,
domain_z: None,
pde_type: PDEType::Diffusion2D,
parameters: params,
_phantom: std::marker::PhantomData,
}
}
pub fn new_wave_2d(
exact_solution: S,
domain_x: [F; 2],
domain_y: [F; 2],
wave_speed: F,
) -> Self {
let mut params = PDEParameters::default();
params.wave_speed = wave_speed;
Self {
exact_solution,
domain_x,
domain_y,
domain_z: None,
pde_type: PDEType::Wave2D,
parameters: params,
_phantom: std::marker::PhantomData,
}
}
pub fn new_helmholtz_2d(exact_solution: S, domain_x: [F; 2], domainy: [F; 2], k: F) -> Self {
let mut params = PDEParameters::default();
params.helmholtz_k = k;
Self {
exact_solution,
domain_x,
domain_y: domainy,
domain_z: None,
pde_type: PDEType::Helmholtz2D,
parameters: params,
_phantom: std::marker::PhantomData,
}
}
pub fn source_term(&self, coordinates: &[F]) -> F {
match self.pde_type {
PDEType::Poisson2D => {
-(self.exact_solution.second_derivative(coordinates, 0)
+ self.exact_solution.second_derivative(coordinates, 1))
}
PDEType::Poisson3D => {
-(self.exact_solution.second_derivative(coordinates, 0)
+ self.exact_solution.second_derivative(coordinates, 1)
+ self.exact_solution.second_derivative(coordinates, 2))
}
PDEType::Diffusion2D => {
let time_dim = coordinates.len() - 1;
let spatial_laplacian = self.exact_solution.second_derivative(coordinates, 0)
+ self.exact_solution.second_derivative(coordinates, 1);
self.exact_solution.derivative(coordinates, time_dim)
- self.parameters.diffusion_coeff * spatial_laplacian
}
PDEType::Wave2D => {
let spatial_laplacian = self.exact_solution.second_derivative(coordinates, 0)
+ self.exact_solution.second_derivative(coordinates, 1);
F::zero()
- self.parameters.wave_speed * self.parameters.wave_speed * spatial_laplacian
}
PDEType::Helmholtz2D => {
let laplacian = self.exact_solution.second_derivative(coordinates, 0)
+ self.exact_solution.second_derivative(coordinates, 1);
laplacian
+ self.parameters.helmholtz_k
* self.parameters.helmholtz_k
* self.exact_solution.evaluate(coordinates)
}
PDEType::AdvectionDiffusion2D => {
let time_dim = coordinates.len() - 1;
let time_deriv = self.exact_solution.derivative(coordinates, time_dim);
let spatial_laplacian = self.exact_solution.second_derivative(coordinates, 0)
+ self.exact_solution.second_derivative(coordinates, 1);
let mut advection_term = F::zero();
for (i, &v_i) in self
.parameters
.advection_velocity
.iter()
.enumerate()
.take(2)
{
advection_term += v_i * self.exact_solution.derivative(coordinates, i);
}
time_deriv + advection_term - self.parameters.diffusion_coeff * spatial_laplacian
}
_ => F::zero(),
}
}
pub fn boundary_condition(&self, x: F, y: F) -> F {
self.exact_solution.evaluate(&[x, y])
}
pub fn boundary_condition_3d(&self, x: F, y: F, z: F) -> F {
self.exact_solution.evaluate(&[x, y, z])
}
pub fn exact_at(&self, x: F, y: F) -> F {
self.exact_solution.evaluate(&[x, y])
}
pub fn exact_at_3d(&self, x: F, y: F, z: F) -> F {
self.exact_solution.evaluate(&[x, y, z])
}
pub fn domain(&self) -> ([F; 2], [F; 2]) {
(self.domain_x, self.domain_y)
}
pub fn domain_3d(&self) -> ([F; 2], [F; 2], [F; 2]) {
(
self.domain_x,
self.domain_y,
self.domain_z.unwrap_or([F::zero(), F::one()]),
)
}
pub fn parameters(&self) -> &PDEParameters<F> {
&self.parameters
}
pub fn is_3d(&self) -> bool {
self.domain_z.is_some()
}
}
#[derive(Debug, Clone)]
pub struct ConvergenceAnalysis<F: IntegrateFloat> {
pub grid_sizes: Vec<F>,
pub errors: Vec<F>,
pub order: F,
pub order_confidence: (F, F),
}
impl<F: IntegrateFloat> ConvergenceAnalysis<F> {
pub fn compute_order(_gridsizes: Vec<F>, errors: Vec<F>) -> IntegrateResult<Self> {
if _gridsizes.len() != errors.len() || _gridsizes.len() < 2 {
return Err(IntegrateError::ValueError(
"Need at least 2 points for convergence analysis".to_string(),
));
}
let n = _gridsizes.len();
let mut sum_log_h = F::zero();
let mut sum_log_e = F::zero();
let mut sum_log_h_sq = F::zero();
let mut sum_log_h_log_e = F::zero();
for (h, e) in _gridsizes.iter().zip(errors.iter()) {
if *e <= F::zero() || *h <= F::zero() {
return Err(IntegrateError::ValueError(
"Grid sizes and errors must be positive".to_string(),
));
}
let log_h = h.ln();
let log_e = e.ln();
sum_log_h += log_h;
sum_log_e += log_e;
sum_log_h_sq += log_h * log_h;
sum_log_h_log_e += log_h * log_e;
}
let n_f = F::from(n).expect("Failed to convert to float");
let denominator = n_f * sum_log_h_sq - sum_log_h * sum_log_h;
if denominator.abs() < F::from(1e-12).expect("Failed to convert constant to float") {
return Err(IntegrateError::ComputationError(
"Cannot compute order - insufficient variation in grid sizes".to_string(),
));
}
let order = (n_f * sum_log_h_log_e - sum_log_h * sum_log_e) / denominator;
let confidence_delta = F::from(0.1).expect("Failed to convert constant to float");
let order_confidence = (order - confidence_delta, order + confidence_delta);
Ok(Self {
grid_sizes: _gridsizes,
errors,
order,
order_confidence,
})
}
pub fn verify_order(&self, expectedorder: F, tolerance: F) -> bool {
(self.order - expectedorder).abs() <= tolerance
}
}
impl<F: IntegrateFloat> fmt::Display for ConvergenceAnalysis<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Convergence Analysis Results:")?;
writeln!(f, "Grid Size Error")?;
writeln!(f, "─────────────────────")?;
for (h, e) in self.grid_sizes.iter().zip(self.errors.iter()) {
writeln!(f, "{h:9.2e} {e:9.2e}")?;
}
writeln!(f, "─────────────────────")?;
writeln!(f, "Estimated order: {:.3}", self.order)?;
writeln!(
f,
"95% confidence: ({:.3}, {:.3})",
self.order_confidence.0, self.order_confidence.1
)?;
Ok(())
}
}
pub struct ErrorAnalysis;
impl ErrorAnalysis {
pub fn l2_norm<F: IntegrateFloat>(
exact: ArrayView1<F>,
numerical: ArrayView1<F>,
) -> IntegrateResult<F> {
if exact.len() != numerical.len() {
return Err(IntegrateError::ValueError(
"Arrays must have same length".to_string(),
));
}
let mut sum_sq = F::zero();
for (e, n) in exact.iter().zip(numerical.iter()) {
let diff = *e - *n;
sum_sq += diff * diff;
}
Ok((sum_sq / F::from(exact.len()).expect("Operation failed")).sqrt())
}
pub fn max_norm<F: IntegrateFloat>(
exact: ArrayView1<F>,
numerical: ArrayView1<F>,
) -> IntegrateResult<F> {
if exact.len() != numerical.len() {
return Err(IntegrateError::ValueError(
"Arrays must have same length".to_string(),
));
}
let mut max_error = F::zero();
for (e, n) in exact.iter().zip(numerical.iter()) {
let diff = (*e - *n).abs();
if diff > max_error {
max_error = diff;
}
}
Ok(max_error)
}
pub fn l2_norm_2d<F: IntegrateFloat>(
exact: ArrayView2<F>,
numerical: ArrayView2<F>,
) -> IntegrateResult<F> {
if exact.shape() != numerical.shape() {
return Err(IntegrateError::ValueError(
"Arrays must have same shape".to_string(),
));
}
let mut sum_sq = F::zero();
let mut count = 0;
for (e, n) in exact.iter().zip(numerical.iter()) {
let diff = *e - *n;
sum_sq += diff * diff;
count += 1;
}
Ok((sum_sq / F::from(count).expect("Failed to convert to float")).sqrt())
}
}
#[allow(dead_code)]
pub fn polynomial_solution<F: IntegrateFloat>(coefficients: Vec<F>) -> PolynomialSolution<F> {
PolynomialSolution::new(coefficients)
}
#[allow(dead_code)]
pub fn trigonometric_solution_2d<F: IntegrateFloat>(
freq_x: F,
freq_y: F,
) -> TrigonometricSolution2D<F> {
TrigonometricSolution2D::simple(freq_x, freq_y)
}
#[derive(Debug, Clone)]
pub struct ExponentialSolution<F: IntegrateFloat> {
amplitude: F,
decay_rate: F,
phase: F,
}
impl<F: IntegrateFloat> ExponentialSolution<F> {
pub fn new(_amplitude: F, decayrate: F, phase: F) -> Self {
Self {
amplitude: _amplitude,
decay_rate: decayrate,
phase,
}
}
pub fn simple(amplitude: F, decayrate: F) -> Self {
Self::new(amplitude, decayrate, F::zero())
}
}
impl<F: IntegrateFloat> ExactSolution<F> for ExponentialSolution<F> {
fn evaluate(&self, coordinates: &[F]) -> F {
let t = coordinates[0];
self.amplitude * (self.decay_rate * t + self.phase).exp()
}
fn derivative(&self, coordinates: &[F], variable: usize) -> F {
let t = coordinates[0];
self.amplitude * self.decay_rate * (self.decay_rate * t + self.phase).exp()
}
fn second_derivative(&self, coordinates: &[F], variable: usize) -> F {
let t = coordinates[0];
self.amplitude
* self.decay_rate
* self.decay_rate
* (self.decay_rate * t + self.phase).exp()
}
fn dimension(&self) -> usize {
1
}
}
#[derive(Debug, Clone)]
pub struct CombinedSolution<F: IntegrateFloat> {
polynomial: Option<PolynomialSolution<F>>,
trigonometric: Option<TrigonometricSolution2D<F>>,
exponential: Option<ExponentialSolution<F>>,
dimension: usize,
}
impl<F: IntegrateFloat> CombinedSolution<F> {
pub fn new(dimension: usize) -> Self {
Self {
polynomial: None,
trigonometric: None,
exponential: None,
dimension,
}
}
pub fn with_polynomial(mut self, poly: PolynomialSolution<F>) -> Self {
self.polynomial = Some(poly);
self
}
pub fn with_trigonometric(mut self, trig: TrigonometricSolution2D<F>) -> Self {
self.trigonometric = Some(trig);
self
}
pub fn with_exponential(mut self, exp: ExponentialSolution<F>) -> Self {
self.exponential = Some(exp);
self
}
}
impl<F: IntegrateFloat> ExactSolution<F> for CombinedSolution<F> {
fn evaluate(&self, coordinates: &[F]) -> F {
let mut result = F::zero();
if let Some(ref poly) = self.polynomial {
result += poly.evaluate(coordinates);
}
if let Some(ref trig) = self.trigonometric {
if coordinates.len() >= 2 {
result += trig.evaluate(coordinates);
}
}
if let Some(ref exp) = self.exponential {
result += exp.evaluate(coordinates);
}
result
}
fn derivative(&self, coordinates: &[F], variable: usize) -> F {
let mut result = F::zero();
if let Some(ref poly) = self.polynomial {
result += poly.derivative(coordinates, variable);
}
if let Some(ref trig) = self.trigonometric {
if coordinates.len() >= 2 {
result += trig.derivative(coordinates, variable);
}
}
if let Some(ref exp) = self.exponential {
result += exp.derivative(coordinates, variable);
}
result
}
fn second_derivative(&self, coordinates: &[F], variable: usize) -> F {
let mut result = F::zero();
if let Some(ref poly) = self.polynomial {
result += poly.second_derivative(coordinates, variable);
}
if let Some(ref trig) = self.trigonometric {
if coordinates.len() >= 2 {
result += trig.second_derivative(coordinates, variable);
}
}
if let Some(ref exp) = self.exponential {
result += exp.second_derivative(coordinates, variable);
}
result
}
fn dimension(&self) -> usize {
self.dimension
}
}
#[derive(Debug, Clone)]
pub struct TrigonometricSolution3D<F: IntegrateFloat> {
freq_x: F,
freq_y: F,
freq_z: F,
phase_x: F,
phase_y: F,
phase_z: F,
}
impl<F: IntegrateFloat> TrigonometricSolution3D<F> {
#[allow(clippy::too_many_arguments)]
pub fn new(freq_x: F, freq_y: F, freq_z: F, phase_x: F, phase_y: F, phase_z: F) -> Self {
Self {
freq_x,
freq_y,
freq_z,
phase_x,
phase_y,
phase_z,
}
}
pub fn simple(freq_x: F, freq_y: F, freq_z: F) -> Self {
Self::new(freq_x, freq_y, freq_z, F::zero(), F::zero(), F::zero())
}
}
impl<F: IntegrateFloat> ExactSolution<F> for TrigonometricSolution3D<F> {
fn evaluate(&self, coordinates: &[F]) -> F {
let x = coordinates[0];
let y = coordinates[1];
let z = coordinates[2];
(self.freq_x * x + self.phase_x).sin()
* (self.freq_y * y + self.phase_y).cos()
* (self.freq_z * z + self.phase_z).sin()
}
fn derivative(&self, coordinates: &[F], variable: usize) -> F {
let x = coordinates[0];
let y = coordinates[1];
let z = coordinates[2];
match variable {
0 => {
self.freq_x
* (self.freq_x * x + self.phase_x).cos()
* (self.freq_y * y + self.phase_y).cos()
* (self.freq_z * z + self.phase_z).sin()
}
1 => {
-self.freq_y
* (self.freq_x * x + self.phase_x).sin()
* (self.freq_y * y + self.phase_y).sin()
* (self.freq_z * z + self.phase_z).sin()
}
2 => {
self.freq_z
* (self.freq_x * x + self.phase_x).sin()
* (self.freq_y * y + self.phase_y).cos()
* (self.freq_z * z + self.phase_z).cos()
}
_ => F::zero(),
}
}
fn second_derivative(&self, coordinates: &[F], variable: usize) -> F {
let x = coordinates[0];
let y = coordinates[1];
let z = coordinates[2];
match variable {
0 => {
-self.freq_x
* self.freq_x
* (self.freq_x * x + self.phase_x).sin()
* (self.freq_y * y + self.phase_y).cos()
* (self.freq_z * z + self.phase_z).sin()
}
1 => {
-self.freq_y
* self.freq_y
* (self.freq_x * x + self.phase_x).sin()
* (self.freq_y * y + self.phase_y).cos()
* (self.freq_z * z + self.phase_z).sin()
}
2 => {
-self.freq_z
* self.freq_z
* (self.freq_x * x + self.phase_x).sin()
* (self.freq_y * y + self.phase_y).cos()
* (self.freq_z * z + self.phase_z).sin()
}
_ => F::zero(),
}
}
fn dimension(&self) -> usize {
3
}
}
#[derive(Debug, Clone)]
pub struct SystemVerification<F: IntegrateFloat> {
pub system_size: usize,
pub component_names: Vec<String>,
_phantom: std::marker::PhantomData<F>,
}
impl<F: IntegrateFloat> SystemVerification<F> {
pub fn new(systemsize: usize) -> Self {
let component_names = (0..systemsize).map(|i| format!("Component {i}")).collect();
Self {
system_size: systemsize,
component_names,
_phantom: std::marker::PhantomData,
}
}
pub fn with_names(componentnames: Vec<String>) -> Self {
let system_size = componentnames.len();
Self {
system_size,
component_names: componentnames,
_phantom: std::marker::PhantomData,
}
}
pub fn verify_system<S1, S2>(
&self,
exact_solutions: &[S1],
numerical_solutions: &[S2],
coordinates: &[F],
) -> Vec<F>
where
S1: ExactSolution<F>,
S2: Fn(&[F]) -> F,
{
assert_eq!(exact_solutions.len(), self.system_size);
assert_eq!(numerical_solutions.len(), self.system_size);
let mut errors = Vec::with_capacity(self.system_size);
for i in 0..self.system_size {
let exact = exact_solutions[i].evaluate(coordinates);
let numerical = numerical_solutions[i](coordinates);
errors.push((exact - numerical).abs());
}
errors
}
}
pub struct VerificationWorkflow<F: IntegrateFloat> {
pub test_cases: Vec<VerificationTestCase<F>>,
}
#[derive(Debug, Clone)]
pub struct VerificationTestCase<F: IntegrateFloat> {
pub name: String,
pub expected_order: F,
pub order_tolerance: F,
pub grid_sizes: Vec<F>,
pub expected_errors: Option<Vec<F>>,
}
impl<F: IntegrateFloat> VerificationWorkflow<F> {
pub fn new() -> Self {
Self {
test_cases: Vec::new(),
}
}
pub fn add_test_case(&mut self, testcase: VerificationTestCase<F>) {
self.test_cases.push(testcase);
}
pub fn run_verification<S: Fn(&[F]) -> IntegrateResult<F>>(
&self,
solver: S,
) -> Vec<VerificationResult<F>> {
let mut results = Vec::new();
for test_case in &self.test_cases {
let mut errors = Vec::new();
for &grid_size in &test_case.grid_sizes {
match solver(&[grid_size]) {
Ok(error) => errors.push(error),
Err(_) => {
results.push(VerificationResult {
test_name: test_case.name.clone(),
passed: false,
computed_order: None,
error_message: Some("Solver failed".to_string()),
});
break;
}
}
}
if errors.len() == test_case.grid_sizes.len() {
match ConvergenceAnalysis::compute_order(test_case.grid_sizes.clone(), errors) {
Ok(analysis) => {
let passed = analysis
.verify_order(test_case.expected_order, test_case.order_tolerance);
results.push(VerificationResult {
test_name: test_case.name.clone(),
passed,
computed_order: Some(analysis.order),
error_message: if passed {
None
} else {
Some("Order verification failed".to_string())
},
});
}
Err(e) => {
results.push(VerificationResult {
test_name: test_case.name.clone(),
passed: false,
computed_order: None,
error_message: Some(format!("Convergence analysis failed: {e}")),
});
}
}
}
}
results
}
}
#[derive(Debug, Clone)]
pub struct VerificationResult<F: IntegrateFloat> {
pub test_name: String,
pub passed: bool,
pub computed_order: Option<F>,
pub error_message: Option<String>,
}
impl<F: IntegrateFloat> Default for VerificationWorkflow<F> {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
pub fn exponential_solution<F: IntegrateFloat>(
amplitude: F,
decay_rate: F,
) -> ExponentialSolution<F> {
ExponentialSolution::simple(amplitude, decay_rate)
}
#[allow(dead_code)]
pub fn trigonometric_solution_3d<F: IntegrateFloat>(
freq_x: F,
freq_y: F,
freq_z: F,
) -> TrigonometricSolution3D<F> {
TrigonometricSolution3D::simple(freq_x, freq_y, freq_z)
}
#[allow(dead_code)]
pub fn combined_solution<F: IntegrateFloat>(dimension: usize) -> CombinedSolution<F> {
CombinedSolution::new(dimension)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::Array1;
#[test]
fn test_polynomial_solution() {
let poly = polynomial_solution(vec![1.0, 2.0, 3.0]);
assert_abs_diff_eq!(poly.evaluate(&[0.0]), 1.0);
assert_abs_diff_eq!(poly.evaluate(&[1.0]), 6.0);
assert_abs_diff_eq!(poly.derivative(&[0.0], 0), 2.0);
assert_abs_diff_eq!(poly.derivative(&[1.0], 0), 8.0);
assert_abs_diff_eq!(poly.second_derivative(&[0.0], 0), 6.0);
assert_abs_diff_eq!(poly.second_derivative(&[1.0], 0), 6.0);
}
#[test]
fn test_trigonometric_solution_2d() {
let trig = trigonometric_solution_2d(1.0, 1.0);
assert_abs_diff_eq!(trig.evaluate(&[0.0, 0.0]), 0.0); assert_abs_diff_eq!(trig.evaluate(&[PI / 2.0, 0.0]), 1.0);
assert_abs_diff_eq!(trig.derivative(&[0.0, 0.0], 0), 1.0);
assert_abs_diff_eq!(trig.derivative(&[0.0, 0.0], 1), 0.0); }
#[test]
fn test_mms_ode_problem() {
let poly = polynomial_solution(vec![0.0, 0.0, 1.0]); let problem = MMSODEProblem::new(poly, [0.0, 1.0]);
assert_abs_diff_eq!(problem.initial_condition(), 0.0);
assert_abs_diff_eq!(problem.source_term(0.0), 0.0);
assert_abs_diff_eq!(problem.source_term(1.0), 2.0);
assert_abs_diff_eq!(problem.exact_at(0.5), 0.25);
}
#[test]
fn test_convergence_analysis() {
let grid_sizes = vec![0.1, 0.05, 0.025, 0.0125];
let errors = vec![0.01, 0.0025, 0.000625, 0.00015625];
let analysis =
ConvergenceAnalysis::compute_order(grid_sizes, errors).expect("Operation failed");
assert!((analysis.order - 2.0_f64).abs() < 0.1);
assert!(analysis.verify_order(2.0, 0.2));
}
#[test]
fn test_error_analysis() {
let exact = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let numerical = Array1::from_vec(vec![1.1, 1.9, 3.05, 3.98]);
let l2_error =
ErrorAnalysis::l2_norm(exact.view(), numerical.view()).expect("Operation failed");
let max_error =
ErrorAnalysis::max_norm(exact.view(), numerical.view()).expect("Operation failed");
assert!(l2_error > 0.0 && l2_error < 0.2);
assert_abs_diff_eq!(max_error, 0.1, epsilon = 1e-10);
}
#[test]
fn test_exponential_solution() {
let exp_sol = exponential_solution(2.0, -3.0);
assert_abs_diff_eq!(exp_sol.evaluate(&[0.0]), 2.0);
assert_abs_diff_eq!(exp_sol.evaluate(&[1.0]), 2.0 * (-3.0_f64).exp());
assert_abs_diff_eq!(exp_sol.derivative(&[0.0], 0), -6.0);
assert_abs_diff_eq!(exp_sol.derivative(&[1.0], 0), -6.0 * (-3.0_f64).exp());
assert_abs_diff_eq!(exp_sol.second_derivative(&[0.0], 0), 18.0);
assert_abs_diff_eq!(
exp_sol.second_derivative(&[1.0], 0),
18.0 * (-3.0_f64).exp()
);
}
#[test]
fn test_combined_solution() {
let poly = polynomial_solution(vec![1.0, 2.0]); let exp = exponential_solution(1.0, -1.0);
let combined = combined_solution(1)
.with_polynomial(poly)
.with_exponential(exp);
assert_abs_diff_eq!(combined.evaluate(&[0.0]), 2.0);
let expected = 3.0 + (-1.0_f64).exp();
assert_abs_diff_eq!(combined.evaluate(&[1.0]), expected, epsilon = 1e-10);
assert_abs_diff_eq!(combined.derivative(&[0.0], 0), 1.0);
}
#[test]
fn test_trigonometric_solution_3d() {
let trig3d = trigonometric_solution_3d(1.0, 1.0, 1.0);
assert_abs_diff_eq!(
trig3d.evaluate(&[PI / 2.0, 0.0, PI / 2.0]),
1.0,
epsilon = 1e-10
);
assert_abs_diff_eq!(
trig3d.derivative(&[0.0, 0.0, PI / 2.0], 0),
1.0,
epsilon = 1e-10
);
assert_abs_diff_eq!(
trig3d.second_derivative(&[0.0, 0.0, PI / 2.0], 0),
0.0,
epsilon = 1e-10
);
}
#[test]
fn test_3d_poisson_problem() {
let exact = trigonometric_solution_3d(PI, PI, PI);
let problem = MMSPDEProblem::new_poisson_3d(exact, [0.0, 1.0], [0.0, 1.0], [0.0, 1.0]);
assert!(problem.is_3d());
let (dx, dy, dz) = problem.domain_3d();
assert_eq!(dx, [0.0, 1.0]);
assert_eq!(dy, [0.0, 1.0]);
assert_eq!(dz, [0.0, 1.0]);
let coords = [0.5, 0.0, 0.5]; let u_exact = problem.exact_at_3d(coords[0], coords[1], coords[2]);
let f_computed = problem.source_term(&coords);
let f_expected = 3.0 * PI * PI * u_exact;
assert_abs_diff_eq!(f_computed, f_expected, epsilon = 1e-10);
}
#[test]
fn test_helmholtz_problem() {
let exact = trigonometric_solution_2d(PI, PI);
let k = 2.0;
let problem = MMSPDEProblem::new_helmholtz_2d(exact, [0.0, 1.0], [0.0, 1.0], k);
let coords = [0.5, 0.0]; let u_exact = problem.exact_at(coords[0], coords[1]);
let f_computed = problem.source_term(&coords);
let f_expected = (k * k - 2.0 * PI * PI) * u_exact;
assert_abs_diff_eq!(f_computed, f_expected, epsilon = 1e-10);
}
#[test]
fn test_verification_workflow() {
let mut workflow = VerificationWorkflow::new();
let test_case = VerificationTestCase {
name: "Second-order test".to_string(),
expected_order: 2.0,
order_tolerance: 0.1,
grid_sizes: vec![0.1, 0.05, 0.025],
expected_errors: None,
};
workflow.add_test_case(test_case);
let mock_solver = |grid_sizes: &[f64]| -> IntegrateResult<f64> {
let h = grid_sizes[0];
Ok(0.1 * h * h) };
let results = workflow.run_verification(mock_solver);
assert_eq!(results.len(), 1);
assert!(results[0].passed);
assert!(
results[0]
.computed_order
.expect("Test: computed_order missing")
> 1.8
);
assert!(
results[0]
.computed_order
.expect("Test: computed_order missing")
< 2.2
);
}
#[test]
fn test_system_verification() {
let poly = polynomial_solution(vec![1.0, 2.0]);
let trig = trigonometric_solution_2d(1.0, 1.0);
let system = SystemVerification::<f64>::new(2);
assert_eq!(system.system_size, 2);
let coords = vec![0.0, 0.0];
let poly_exact = poly.evaluate(&coords);
let poly_numerical = 1.0 + 2.0 * coords[0];
let trig_exact = trig.evaluate(&coords);
let trig_numerical = coords[0] * coords[1];
let poly_error = (poly_exact as f64 - poly_numerical).abs() as f64;
let trig_error = (trig_exact as f64 - trig_numerical).abs() as f64;
assert_abs_diff_eq!(poly_error, 0.0);
assert_abs_diff_eq!(trig_error, 0.0);
let named_system: SystemVerification<f64> = SystemVerification::with_names(vec![
"Polynomial".to_string(),
"Trigonometric".to_string(),
]);
assert_eq!(named_system.component_names[0], "Polynomial");
assert_eq!(named_system.component_names[1], "Trigonometric");
}
}
pub struct AdvancedVerificationFramework {
pub refinement_strategy: RefinementStrategy,
pub error_estimation_method: ErrorEstimationMethod,
pub convergence_criteria: ConvergenceCriteria,
pub statistical_analysis: bool,
}
#[derive(Debug, Clone, Copy)]
pub enum RefinementStrategy {
Uniform,
Adaptive,
Custom(f64),
}
#[derive(Debug, Clone, Copy)]
pub enum ErrorEstimationMethod {
Richardson,
Embedded,
Bootstrap,
CrossValidation,
}
#[derive(Debug, Clone)]
pub struct ConvergenceCriteria {
pub min_levels: usize,
pub max_levels: usize,
pub min_r_squared: f64,
pub order_tolerance: f64,
pub target_accuracy: f64,
}
impl Default for ConvergenceCriteria {
fn default() -> Self {
Self {
min_levels: 3,
max_levels: 8,
min_r_squared: 0.95,
order_tolerance: 0.2,
target_accuracy: 1e-10,
}
}
}
impl Default for AdvancedVerificationFramework {
fn default() -> Self {
Self {
refinement_strategy: RefinementStrategy::Uniform,
error_estimation_method: ErrorEstimationMethod::Richardson,
convergence_criteria: ConvergenceCriteria::default(),
statistical_analysis: true,
}
}
}