682 lines
21 KiB
Rust
682 lines
21 KiB
Rust
//! Conservation Laws for Physics-Informed Neural Networks
|
||
//!
|
||
//! This module implements various conservation laws that can be enforced
|
||
//! in PINNs to ensure physical consistency of the learned solutions.
|
||
|
||
use crate::Tensor;
|
||
use crate::error::{ConservationLaw, Result, ScienceError};
|
||
use crate::variable_extensions::VariableExt;
|
||
use async_trait::async_trait;
|
||
use rtx_autograd::Variable;
|
||
use serde::{Deserialize, Serialize};
|
||
use std::collections::HashMap;
|
||
|
||
/// Trait for conservation law constraints in PINNs
|
||
#[async_trait]
|
||
pub trait ConservationLoss: Send + Sync {
|
||
/// Compute the conservation law violation
|
||
async fn compute_loss(
|
||
&self,
|
||
coordinates: &Tensor, // Input coordinates [x, t, ...]
|
||
solution: &Variable, // Network output u(x,t)
|
||
du_dx: &Variable, // ∂u/∂x
|
||
du_dt: &Variable, // ∂u/∂t
|
||
) -> Result<f32>;
|
||
|
||
/// Get the name of the conservation law
|
||
fn name(&self) -> &str;
|
||
|
||
/// Get the conservation law type
|
||
fn law_type(&self) -> ConservationLaw;
|
||
|
||
/// Get tolerance for violation detection
|
||
fn tolerance(&self) -> f64;
|
||
|
||
/// Validate conservation at given points
|
||
async fn validate_conservation(
|
||
&self,
|
||
coordinates: &Tensor,
|
||
solution: &Variable,
|
||
du_dx: &Variable,
|
||
du_dt: &Variable,
|
||
) -> Result<ValidationResult>;
|
||
}
|
||
|
||
/// Conservation law validation result
|
||
#[derive(Debug, Clone)]
|
||
pub struct ValidationResult {
|
||
/// Maximum violation magnitude
|
||
pub max_violation: f64,
|
||
/// Mean violation magnitude
|
||
pub mean_violation: f64,
|
||
/// Points where tolerance is exceeded
|
||
pub violation_points: Vec<usize>,
|
||
/// Whether conservation is satisfied within tolerance
|
||
pub is_satisfied: bool,
|
||
/// Detailed diagnostics
|
||
pub diagnostics: HashMap<String, f64>,
|
||
}
|
||
|
||
/// Mass conservation law: ∂ρ/∂t + ∇·(ρv) = 0
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct MassConservation {
|
||
/// Density field identifier
|
||
pub density_field: String,
|
||
/// Velocity field identifiers [u, v, w]
|
||
pub velocity_fields: Vec<String>,
|
||
/// Tolerance for conservation violation
|
||
pub tolerance: f64,
|
||
/// Weight in loss function
|
||
pub weight: f64,
|
||
}
|
||
|
||
/// Momentum conservation law: ∂(ρv)/∂t + ∇·(ρv⊗v) = -∇p + μ∇²v + f
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct MomentumConservation {
|
||
/// Density field
|
||
pub density_field: String,
|
||
/// Velocity components
|
||
pub velocity_fields: Vec<String>,
|
||
/// Pressure field
|
||
pub pressure_field: String,
|
||
/// Viscosity coefficient
|
||
pub viscosity: f64,
|
||
/// External force fields
|
||
pub force_fields: Vec<String>,
|
||
/// Tolerance for conservation violation
|
||
pub tolerance: f64,
|
||
/// Weight in loss function
|
||
pub weight: f64,
|
||
}
|
||
|
||
/// Energy conservation law: ∂E/∂t + ∇·(Ev + pv - k∇T) = Q
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct EnergyConservation {
|
||
/// Total energy field
|
||
pub energy_field: String,
|
||
/// Velocity fields
|
||
pub velocity_fields: Vec<String>,
|
||
/// Pressure field
|
||
pub pressure_field: String,
|
||
/// Temperature field
|
||
pub temperature_field: String,
|
||
/// Thermal conductivity
|
||
pub thermal_conductivity: f64,
|
||
/// Heat source term
|
||
pub heat_source: f64,
|
||
/// Tolerance for conservation violation
|
||
pub tolerance: f64,
|
||
/// Weight in loss function
|
||
pub weight: f64,
|
||
}
|
||
|
||
/// Angular momentum conservation: L = r × p = constant
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct AngularMomentumConservation {
|
||
/// Position fields [x, y, z]
|
||
pub position_fields: Vec<String>,
|
||
/// Momentum fields [px, py, pz]
|
||
pub momentum_fields: Vec<String>,
|
||
/// Reference point for angular momentum calculation
|
||
pub reference_point: Vec<f64>,
|
||
/// Tolerance for conservation violation
|
||
pub tolerance: f64,
|
||
/// Weight in loss function
|
||
pub weight: f64,
|
||
}
|
||
|
||
/// Charge conservation: ∂ρ/∂t + ∇·J = 0
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct ChargeConservation {
|
||
/// Charge density field
|
||
pub charge_density_field: String,
|
||
/// Current density fields [Jx, Jy, Jz]
|
||
pub current_density_fields: Vec<String>,
|
||
/// Tolerance for conservation violation
|
||
pub tolerance: f64,
|
||
/// Weight in loss function
|
||
pub weight: f64,
|
||
}
|
||
|
||
/// Probability conservation (quantum mechanics): ∫|ψ|²dx = 1
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct ProbabilityConservation {
|
||
/// Wavefunction field (complex)
|
||
pub wavefunction_field: String,
|
||
/// Integration domain bounds
|
||
pub domain_bounds: Vec<(f64, f64)>,
|
||
/// Tolerance for conservation violation
|
||
pub tolerance: f64,
|
||
/// Weight in loss function
|
||
pub weight: f64,
|
||
}
|
||
|
||
/// Conservation validator for comprehensive checking
|
||
pub struct ConservationValidator {
|
||
/// List of conservation laws to check
|
||
conservation_laws: Vec<Box<dyn ConservationLoss + Send + Sync>>,
|
||
/// Global tolerance settings
|
||
global_tolerance: f64,
|
||
/// Validation frequency (every N training steps)
|
||
validation_frequency: usize,
|
||
/// Current validation step
|
||
current_step: usize,
|
||
}
|
||
|
||
impl std::fmt::Debug for ConservationValidator {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
f.debug_struct("ConservationValidator")
|
||
.field("num_conservation_laws", &self.conservation_laws.len())
|
||
.field("global_tolerance", &self.global_tolerance)
|
||
.field("validation_frequency", &self.validation_frequency)
|
||
.field("current_step", &self.current_step)
|
||
.finish()
|
||
}
|
||
}
|
||
|
||
impl MassConservation {
|
||
/// Create a new mass conservation law
|
||
#[must_use]
|
||
pub fn new(tolerance: f64) -> Self {
|
||
Self {
|
||
density_field: "rho".to_string(),
|
||
velocity_fields: vec!["u".to_string(), "v".to_string()],
|
||
tolerance,
|
||
weight: 1.0,
|
||
}
|
||
}
|
||
|
||
/// Set velocity field names
|
||
#[must_use]
|
||
pub fn with_velocity_fields(mut self, fields: Vec<String>) -> Self {
|
||
self.velocity_fields = fields;
|
||
self
|
||
}
|
||
|
||
/// Set loss weight
|
||
#[must_use]
|
||
pub fn with_weight(mut self, weight: f64) -> Self {
|
||
self.weight = weight;
|
||
self
|
||
}
|
||
}
|
||
|
||
#[async_trait]
|
||
impl ConservationLoss for MassConservation {
|
||
async fn compute_loss(
|
||
&self,
|
||
_coordinates: &Tensor,
|
||
_solution: &Variable,
|
||
du_dx: &Variable,
|
||
du_dt: &Variable,
|
||
) -> Result<f32> {
|
||
// For simplified mass conservation: ∂ρ/∂t + ∂(ρu)/∂x = 0
|
||
// Assuming solution represents density ρ and we have velocity field
|
||
|
||
// ∂ρ/∂t term
|
||
let drho_dt = du_dt.clone();
|
||
|
||
// ∂(ρu)/∂x term - simplified to ρ∂u/∂x + u∂ρ/∂x
|
||
// For this example, assume constant velocity or treat solution as ρu
|
||
let drhou_dx = du_dx.clone();
|
||
|
||
// Mass conservation residual: ∂ρ/∂t + ∂(ρu)/∂x = 0
|
||
let residual = drho_dt.add(&drhou_dx)?;
|
||
|
||
// Return mean squared residual as f32
|
||
let squared_residual = residual.multiply(&residual)?;
|
||
let loss_var = squared_residual.mean_square()?;
|
||
let loss_value = loss_var.value().to_scalar::<f32>()?;
|
||
|
||
Ok(loss_value)
|
||
}
|
||
|
||
fn name(&self) -> &'static str {
|
||
"Mass Conservation"
|
||
}
|
||
|
||
fn law_type(&self) -> ConservationLaw {
|
||
ConservationLaw::Mass
|
||
}
|
||
|
||
fn tolerance(&self) -> f64 {
|
||
self.tolerance
|
||
}
|
||
|
||
async fn validate_conservation(
|
||
&self,
|
||
coordinates: &Tensor,
|
||
solution: &Variable,
|
||
du_dx: &Variable,
|
||
du_dt: &Variable,
|
||
) -> Result<ValidationResult> {
|
||
// Compute conservation residual
|
||
let residual = self
|
||
.compute_loss(coordinates, solution, du_dx, du_dt)
|
||
.await?;
|
||
let residual_values = [residual]; // f32 scalar converted to vec
|
||
|
||
let violations: Vec<f64> = residual_values
|
||
.iter()
|
||
.map(|&x| f64::from(x.abs()))
|
||
.collect();
|
||
|
||
let max_violation = violations.iter().copied().fold(0.0, f64::max);
|
||
let mean_violation = violations.iter().sum::<f64>() / violations.len() as f64;
|
||
|
||
let violation_points: Vec<usize> = violations
|
||
.iter()
|
||
.enumerate()
|
||
.filter_map(|(i, &v)| if v > self.tolerance { Some(i) } else { None })
|
||
.collect();
|
||
|
||
let is_satisfied = max_violation <= self.tolerance;
|
||
|
||
let mut diagnostics = HashMap::new();
|
||
diagnostics.insert("max_violation".to_string(), max_violation);
|
||
diagnostics.insert("mean_violation".to_string(), mean_violation);
|
||
diagnostics.insert(
|
||
"violation_rate".to_string(),
|
||
violation_points.len() as f64 / violations.len() as f64,
|
||
);
|
||
|
||
Ok(ValidationResult {
|
||
max_violation,
|
||
mean_violation,
|
||
violation_points,
|
||
is_satisfied,
|
||
diagnostics,
|
||
})
|
||
}
|
||
}
|
||
|
||
impl MomentumConservation {
|
||
/// Create a new momentum conservation law
|
||
#[must_use]
|
||
pub fn new(viscosity: f64, tolerance: f64) -> Self {
|
||
Self {
|
||
density_field: "rho".to_string(),
|
||
velocity_fields: vec!["u".to_string(), "v".to_string()],
|
||
pressure_field: "p".to_string(),
|
||
viscosity,
|
||
force_fields: Vec::new(),
|
||
tolerance,
|
||
weight: 1.0,
|
||
}
|
||
}
|
||
|
||
/// Add external force field
|
||
#[must_use]
|
||
pub fn with_force_field(mut self, force_field: String) -> Self {
|
||
self.force_fields.push(force_field);
|
||
self
|
||
}
|
||
}
|
||
|
||
#[async_trait]
|
||
impl ConservationLoss for MomentumConservation {
|
||
async fn compute_loss(
|
||
&self,
|
||
_coordinates: &Tensor,
|
||
_solution: &Variable,
|
||
du_dx: &Variable,
|
||
du_dt: &Variable,
|
||
) -> Result<f32> {
|
||
// Simplified momentum conservation: ∂(ρu)/∂t + ∂p/∂x = μ∂²u/∂x²
|
||
// Assuming solution represents momentum ρu
|
||
|
||
let momentum_time_derivative = du_dt.clone();
|
||
|
||
// For now, assume pressure gradient is represented by some component
|
||
// In a full implementation, this would be computed from pressure field
|
||
let pressure_gradient = du_dx.multiply_scalar(0.1)?; // Placeholder
|
||
|
||
// Viscous term (simplified)
|
||
let viscous_term = du_dx.multiply_scalar(self.viscosity as f32)?;
|
||
|
||
// Momentum conservation residual
|
||
let residual = momentum_time_derivative
|
||
.add(&pressure_gradient)?
|
||
.subtract(&viscous_term)?;
|
||
|
||
let squared_residual = residual.multiply(&residual)?;
|
||
let loss_var = squared_residual.mean_square()?;
|
||
let loss_value = loss_var.value().to_scalar::<f32>()?;
|
||
|
||
Ok(loss_value)
|
||
}
|
||
|
||
fn name(&self) -> &'static str {
|
||
"Momentum Conservation"
|
||
}
|
||
|
||
fn law_type(&self) -> ConservationLaw {
|
||
ConservationLaw::Momentum
|
||
}
|
||
|
||
fn tolerance(&self) -> f64 {
|
||
self.tolerance
|
||
}
|
||
|
||
async fn validate_conservation(
|
||
&self,
|
||
coordinates: &Tensor,
|
||
solution: &Variable,
|
||
du_dx: &Variable,
|
||
du_dt: &Variable,
|
||
) -> Result<ValidationResult> {
|
||
let residual = self
|
||
.compute_loss(coordinates, solution, du_dx, du_dt)
|
||
.await?;
|
||
let residual_values = [residual]; // f32 scalar converted to vec
|
||
|
||
let violations: Vec<f64> = residual_values
|
||
.iter()
|
||
.map(|&x| f64::from(x.abs()))
|
||
.collect();
|
||
|
||
let max_violation = violations.iter().copied().fold(0.0, f64::max);
|
||
let mean_violation = violations.iter().sum::<f64>() / violations.len() as f64;
|
||
|
||
let violation_points: Vec<usize> = violations
|
||
.iter()
|
||
.enumerate()
|
||
.filter_map(|(i, &v)| if v > self.tolerance { Some(i) } else { None })
|
||
.collect();
|
||
|
||
let is_satisfied = max_violation <= self.tolerance;
|
||
|
||
let mut diagnostics = HashMap::new();
|
||
diagnostics.insert("max_violation".to_string(), max_violation);
|
||
diagnostics.insert("mean_violation".to_string(), mean_violation);
|
||
diagnostics.insert("viscosity".to_string(), self.viscosity);
|
||
|
||
Ok(ValidationResult {
|
||
max_violation,
|
||
mean_violation,
|
||
violation_points,
|
||
is_satisfied,
|
||
diagnostics,
|
||
})
|
||
}
|
||
}
|
||
|
||
impl EnergyConservation {
|
||
/// Create a new energy conservation law
|
||
#[must_use]
|
||
pub fn new(thermal_conductivity: f64, tolerance: f64) -> Self {
|
||
Self {
|
||
energy_field: "E".to_string(),
|
||
velocity_fields: vec!["u".to_string(), "v".to_string()],
|
||
pressure_field: "p".to_string(),
|
||
temperature_field: "T".to_string(),
|
||
thermal_conductivity,
|
||
heat_source: 0.0,
|
||
tolerance,
|
||
weight: 1.0,
|
||
}
|
||
}
|
||
|
||
/// Set heat source term
|
||
#[must_use]
|
||
pub fn with_heat_source(mut self, heat_source: f64) -> Self {
|
||
self.heat_source = heat_source;
|
||
self
|
||
}
|
||
}
|
||
|
||
#[async_trait]
|
||
impl ConservationLoss for EnergyConservation {
|
||
async fn compute_loss(
|
||
&self,
|
||
_coordinates: &Tensor,
|
||
_solution: &Variable,
|
||
du_dx: &Variable,
|
||
du_dt: &Variable,
|
||
) -> Result<f32> {
|
||
// Simplified energy conservation: ∂E/∂t = -∂q/∂x + Q
|
||
// where q is heat flux and Q is heat source
|
||
|
||
let energy_time_derivative = du_dt.clone();
|
||
|
||
// Heat flux term (simplified as thermal conductivity * temperature gradient)
|
||
let heat_flux_gradient = du_dx.multiply_scalar(self.thermal_conductivity as f32)?;
|
||
|
||
// Heat source term
|
||
let du_dt_tensor = du_dt.value();
|
||
let source_shape = &[du_dt_tensor.shape().dims()[0]];
|
||
let source_tensor =
|
||
Tensor::full(source_shape, self.heat_source as f32, du_dt_tensor.device())?;
|
||
let source_term = Variable::constant(source_tensor);
|
||
|
||
// Energy conservation residual
|
||
let residual = energy_time_derivative
|
||
.add(&heat_flux_gradient)?
|
||
.subtract(&source_term)?;
|
||
|
||
let squared_residual = residual.multiply(&residual)?;
|
||
let loss_var = squared_residual.mean_square()?;
|
||
let loss_value = loss_var.value().to_scalar::<f32>()?;
|
||
|
||
Ok(loss_value)
|
||
}
|
||
|
||
fn name(&self) -> &'static str {
|
||
"Energy Conservation"
|
||
}
|
||
|
||
fn law_type(&self) -> ConservationLaw {
|
||
ConservationLaw::Energy
|
||
}
|
||
|
||
fn tolerance(&self) -> f64 {
|
||
self.tolerance
|
||
}
|
||
|
||
async fn validate_conservation(
|
||
&self,
|
||
coordinates: &Tensor,
|
||
solution: &Variable,
|
||
du_dx: &Variable,
|
||
du_dt: &Variable,
|
||
) -> Result<ValidationResult> {
|
||
let residual = self
|
||
.compute_loss(coordinates, solution, du_dx, du_dt)
|
||
.await?;
|
||
let residual_values = [residual]; // f32 scalar converted to vec
|
||
|
||
let violations: Vec<f64> = residual_values
|
||
.iter()
|
||
.map(|&x| f64::from(x.abs()))
|
||
.collect();
|
||
|
||
let max_violation = violations.iter().copied().fold(0.0, f64::max);
|
||
let mean_violation = violations.iter().sum::<f64>() / violations.len() as f64;
|
||
|
||
let violation_points: Vec<usize> = violations
|
||
.iter()
|
||
.enumerate()
|
||
.filter_map(|(i, &v)| if v > self.tolerance { Some(i) } else { None })
|
||
.collect();
|
||
|
||
let is_satisfied = max_violation <= self.tolerance;
|
||
|
||
let mut diagnostics = HashMap::new();
|
||
diagnostics.insert("max_violation".to_string(), max_violation);
|
||
diagnostics.insert("mean_violation".to_string(), mean_violation);
|
||
diagnostics.insert(
|
||
"thermal_conductivity".to_string(),
|
||
self.thermal_conductivity,
|
||
);
|
||
diagnostics.insert("heat_source".to_string(), self.heat_source);
|
||
|
||
Ok(ValidationResult {
|
||
max_violation,
|
||
mean_violation,
|
||
violation_points,
|
||
is_satisfied,
|
||
diagnostics,
|
||
})
|
||
}
|
||
}
|
||
|
||
impl ConservationValidator {
|
||
/// Create a new conservation validator
|
||
#[must_use]
|
||
pub fn new(global_tolerance: f64, validation_frequency: usize) -> Self {
|
||
Self {
|
||
conservation_laws: Vec::new(),
|
||
global_tolerance,
|
||
validation_frequency,
|
||
current_step: 0,
|
||
}
|
||
}
|
||
|
||
/// Add a conservation law to validate
|
||
#[must_use]
|
||
pub fn add_conservation_law(mut self, law: Box<dyn ConservationLoss + Send + Sync>) -> Self {
|
||
self.conservation_laws.push(law);
|
||
self
|
||
}
|
||
|
||
/// Validate all conservation laws
|
||
pub async fn validate_all(
|
||
&mut self,
|
||
coordinates: &Tensor,
|
||
solution: &Variable,
|
||
du_dx: &Variable,
|
||
du_dt: &Variable,
|
||
) -> Result<Vec<ValidationResult>> {
|
||
self.current_step += 1;
|
||
|
||
// Only validate at specified frequency
|
||
if !self.current_step.is_multiple_of(self.validation_frequency) {
|
||
return Ok(Vec::new());
|
||
}
|
||
|
||
let mut results = Vec::new();
|
||
|
||
for law in &self.conservation_laws {
|
||
let result = law
|
||
.validate_conservation(coordinates, solution, du_dx, du_dt)
|
||
.await?;
|
||
|
||
// Log critical violations
|
||
if !result.is_satisfied && result.max_violation > self.global_tolerance * 10.0 {
|
||
tracing::warn!(
|
||
"Critical conservation violation in {}: max = {:.2e}, tolerance = {:.2e}",
|
||
law.name(),
|
||
result.max_violation,
|
||
law.tolerance()
|
||
);
|
||
}
|
||
|
||
results.push(result);
|
||
}
|
||
|
||
Ok(results)
|
||
}
|
||
|
||
/// Get summary of all conservation laws
|
||
#[must_use]
|
||
pub fn summary(&self) -> HashMap<String, String> {
|
||
let mut summary = HashMap::new();
|
||
|
||
summary.insert(
|
||
"num_laws".to_string(),
|
||
self.conservation_laws.len().to_string(),
|
||
);
|
||
summary.insert(
|
||
"global_tolerance".to_string(),
|
||
self.global_tolerance.to_string(),
|
||
);
|
||
summary.insert(
|
||
"validation_frequency".to_string(),
|
||
self.validation_frequency.to_string(),
|
||
);
|
||
summary.insert("current_step".to_string(), self.current_step.to_string());
|
||
|
||
for (i, law) in self.conservation_laws.iter().enumerate() {
|
||
summary.insert(format!("law_{i}_name"), law.name().to_string());
|
||
summary.insert(format!("law_{i}_tolerance"), law.tolerance().to_string());
|
||
}
|
||
|
||
summary
|
||
}
|
||
}
|
||
|
||
/// Factory function for creating conservation laws
|
||
pub fn create_conservation_law(
|
||
law_type: ConservationLaw,
|
||
tolerance: f64,
|
||
parameters: &HashMap<String, f64>,
|
||
) -> Result<Box<dyn ConservationLoss + Send + Sync>> {
|
||
match law_type {
|
||
ConservationLaw::Mass => Ok(Box::new(MassConservation::new(tolerance))),
|
||
ConservationLaw::Momentum => {
|
||
let viscosity = parameters.get("viscosity").copied().unwrap_or(1e-3);
|
||
Ok(Box::new(MomentumConservation::new(viscosity, tolerance)))
|
||
}
|
||
ConservationLaw::Energy => {
|
||
let thermal_conductivity = parameters
|
||
.get("thermal_conductivity")
|
||
.copied()
|
||
.unwrap_or(0.1);
|
||
let mut energy_law = EnergyConservation::new(thermal_conductivity, tolerance);
|
||
if let Some(&heat_source) = parameters.get("heat_source") {
|
||
energy_law = energy_law.with_heat_source(heat_source);
|
||
}
|
||
Ok(Box::new(energy_law))
|
||
}
|
||
_ => Err(ScienceError::physics(
|
||
format!("Unsupported conservation law: {law_type:?}"),
|
||
crate::error::PhysicsDomain::FluidDynamics,
|
||
)),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::Device;
|
||
|
||
#[tokio::test]
|
||
async fn test_mass_conservation() -> Result<()> {
|
||
let device = Device::cpu();
|
||
let conservation = MassConservation::new(1e-6);
|
||
|
||
// Create dummy data
|
||
let coords = Tensor::zeros(&[10, 2], &device)?;
|
||
let solution = Variable::new(Tensor::ones(&[10], &device)?, true);
|
||
let du_dx = Variable::new(Tensor::zeros(&[10], &device)?, false);
|
||
let du_dt = Variable::new(Tensor::zeros(&[10], &device)?, false);
|
||
|
||
let loss = conservation
|
||
.compute_loss(&coords, &solution, &du_dx, &du_dt)
|
||
.await?;
|
||
assert!(loss >= 0.0);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn test_conservation_validator() {
|
||
let validator = ConservationValidator::new(1e-6, 10);
|
||
assert_eq!(validator.conservation_laws.len(), 0);
|
||
|
||
let summary = validator.summary();
|
||
assert_eq!(summary["num_laws"], "0");
|
||
}
|
||
|
||
#[test]
|
||
fn test_conservation_law_factory() -> Result<()> {
|
||
let mut params = HashMap::new();
|
||
params.insert("viscosity".to_string(), 1e-3);
|
||
|
||
let law = create_conservation_law(ConservationLaw::Momentum, 1e-6, ¶ms)?;
|
||
assert_eq!(law.name(), "Momentum Conservation");
|
||
|
||
Ok(())
|
||
}
|
||
}
|