use crate::constrained::{ConstrainedSpline, Constraint, ConstraintType};
use crate::error::{InterpolateError, InterpolateResult};
use scirs2_core::ndarray::{Array1, ArrayView1, ScalarOperand};
use scirs2_core::numeric::{Float, FromPrimitive, ToPrimitive};
use std::collections::HashMap;
use std::fmt::{Debug, Display, LowerExp};
use std::ops::{AddAssign, DivAssign, MulAssign, RemAssign, SubAssign};
#[derive(Debug, Clone, PartialEq)]
pub enum PhysicalConstraint<T> {
Positivity,
Monotonic(bool),
BoundaryCondition(T, T), DerivativeBoundaryCondition(T, T), IntegralConservation(T), BoundedVariation(T),
SmoothnessConstraint(T),
CustomConstraint(String), }
#[derive(Debug, Clone, PartialEq)]
pub enum ConservationLaw<T> {
MassConservation { total_mass: T },
EnergyConservation { total_energy: T },
MomentumConservation { total_momentum: T },
GenericConservation {
name: String,
conserved_quantity: T,
weight_function: Option<Array1<T>>,
},
}
#[derive(Debug, Clone)]
pub struct PhysicsInformedConfig<T> {
pub constraints: Vec<PhysicalConstraint<T>>,
pub conservation_laws: Vec<ConservationLaw<T>>,
pub penalty_weight: T,
pub constraint_tolerance: T,
pub max_iterations: usize,
pub adaptive_weights: bool,
pub regularization: T,
}
impl<T: Float + FromPrimitive> Default for PhysicsInformedConfig<T> {
fn default() -> Self {
Self {
constraints: Vec::new(),
conservation_laws: Vec::new(),
penalty_weight: T::from(1.0).expect("Operation failed"),
constraint_tolerance: T::from(1e-6).expect("Operation failed"),
max_iterations: 100,
adaptive_weights: true,
regularization: T::from(1e-8).expect("Operation failed"),
}
}
}
#[derive(Debug, Clone)]
pub struct PhysicsInformedResult<T> {
pub values: Array1<T>,
pub constraint_violations: HashMap<String, T>,
pub conservation_errors: HashMap<String, T>,
pub penalty_cost: T,
pub iterations_used: usize,
pub constraints_satisfied: bool,
}
#[derive(Debug)]
pub struct PhysicsInformedInterpolator<T>
where
T: Float
+ FromPrimitive
+ ToPrimitive
+ Debug
+ Display
+ LowerExp
+ AddAssign
+ SubAssign
+ MulAssign
+ DivAssign
+ RemAssign
+ ScalarOperand
+ Copy
+ 'static,
{
#[allow(dead_code)]
x_data: Array1<T>,
#[allow(dead_code)]
y_data: Array1<T>,
config: PhysicsInformedConfig<T>,
constrained_spline: ConstrainedSpline<T>,
#[allow(dead_code)]
domain_min: T,
#[allow(dead_code)]
domain_max: T,
#[allow(dead_code)]
constraint_cache: HashMap<String, T>,
}
impl<T> PhysicsInformedInterpolator<T>
where
T: Float
+ FromPrimitive
+ ToPrimitive
+ Debug
+ Display
+ LowerExp
+ AddAssign
+ SubAssign
+ MulAssign
+ DivAssign
+ RemAssign
+ ScalarOperand
+ Copy
+ 'static,
{
pub fn new(
x: &ArrayView1<T>,
y: &ArrayView1<T>,
constraints: Vec<PhysicalConstraint<T>>,
) -> InterpolateResult<Self> {
if x.len() != y.len() {
return Err(InterpolateError::DimensionMismatch(format!(
"x and y arrays must have same length, got {} and {}",
x.len(),
y.len()
)));
}
if x.len() < 2 {
return Err(InterpolateError::InvalidValue(
"At least 2 data points are required".to_string(),
));
}
let domain_min = x[0];
let domain_max = x[x.len() - 1];
let spline_constraints = Self::convert_physical_constraints(&constraints, x, y)?;
let degree = std::cmp::min(3, x.len() - 1);
let constrained_spline = ConstrainedSpline::interpolate(
x,
y,
spline_constraints,
degree,
crate::bspline::ExtrapolateMode::Extrapolate,
)?;
let config = PhysicsInformedConfig {
constraints,
..Default::default()
};
Ok(Self {
x_data: x.to_owned(),
y_data: y.to_owned(),
config,
constrained_spline,
domain_min,
domain_max,
constraint_cache: HashMap::new(),
})
}
pub fn with_conservation_laws(mut self, laws: Vec<ConservationLaw<T>>) -> Self {
self.config.conservation_laws = laws;
self
}
pub fn with_penalty_weight(mut self, weight: T) -> Self {
self.config.penalty_weight = weight;
self
}
pub fn with_tolerance(mut self, tolerance: T) -> Self {
self.config.constraint_tolerance = tolerance;
self
}
pub fn with_max_iterations(mut self, maxiter: usize) -> Self {
self.config.max_iterations = maxiter;
self
}
pub fn evaluate(&self, xnew: &ArrayView1<T>) -> InterpolateResult<PhysicsInformedResult<T>> {
let initial_values = self.constrained_spline.evaluate_array(xnew)?;
let physics_corrected = self.apply_physics_corrections(&initial_values, xnew)?;
let corrected_values = self.apply_conservation_corrections(&physics_corrected, xnew)?;
let constraint_violations = self.check_constraint_violations(&corrected_values, xnew)?;
let conservation_errors = self.check_conservation_errors(&corrected_values, xnew)?;
let penalty_cost =
self.calculate_penalty_cost(&constraint_violations, &conservation_errors)?;
let constraints_satisfied = constraint_violations
.values()
.all(|&v| v < self.config.constraint_tolerance)
&& conservation_errors
.values()
.all(|&v| v < self.config.constraint_tolerance);
Ok(PhysicsInformedResult {
values: corrected_values,
constraint_violations,
conservation_errors,
penalty_cost,
iterations_used: 1, constraints_satisfied,
})
}
pub fn evaluate_with_iteration(
&self,
xnew: &ArrayView1<T>,
) -> InterpolateResult<PhysicsInformedResult<T>> {
let mut current_values = self.constrained_spline.evaluate_array(xnew)?;
let mut best_penalty = T::infinity();
let mut best_values = current_values.clone();
let mut iterations_used = 0;
for iter in 0..self.config.max_iterations {
iterations_used = iter + 1;
current_values = self.apply_conservation_corrections(¤t_values, xnew)?;
current_values = self.apply_physics_corrections(¤t_values, xnew)?;
let constraint_violations = self.check_constraint_violations(¤t_values, xnew)?;
let conservation_errors = self.check_conservation_errors(¤t_values, xnew)?;
let penalty_cost =
self.calculate_penalty_cost(&constraint_violations, &conservation_errors)?;
if penalty_cost < best_penalty {
best_penalty = penalty_cost;
best_values = current_values.clone();
}
if penalty_cost < self.config.constraint_tolerance {
break;
}
if iter > 10 && penalty_cost == best_penalty {
let perturbation_scale = T::from(0.001).expect("Operation failed");
for i in 0..current_values.len() {
let perturbation = perturbation_scale
* T::from((i % 3) as f64 - 1.0).expect("Operation failed");
current_values[i] += perturbation;
}
}
}
let constraint_violations = self.check_constraint_violations(&best_values, xnew)?;
let conservation_errors = self.check_conservation_errors(&best_values, xnew)?;
let constraints_satisfied = constraint_violations
.values()
.all(|&v| v < self.config.constraint_tolerance)
&& conservation_errors
.values()
.all(|&v| v < self.config.constraint_tolerance);
Ok(PhysicsInformedResult {
values: best_values,
constraint_violations,
conservation_errors,
penalty_cost: best_penalty,
iterations_used,
constraints_satisfied,
})
}
fn convert_physical_constraints(
constraints: &[PhysicalConstraint<T>],
x: &ArrayView1<T>,
_y: &ArrayView1<T>,
) -> InterpolateResult<Vec<Constraint<T>>> {
let mut spline_constraints = Vec::new();
for constraint in constraints {
match constraint {
PhysicalConstraint::Monotonic(increasing) => {
let constraint_type = if *increasing {
ConstraintType::MonotoneIncreasing
} else {
ConstraintType::MonotoneDecreasing
};
spline_constraints.push(Constraint {
constraint_type,
x_min: Some(x[0]),
x_max: Some(x[x.len() - 1]),
parameter: None,
});
}
PhysicalConstraint::Positivity => {
spline_constraints.push(Constraint {
constraint_type: ConstraintType::Positive,
x_min: Some(x[0]),
x_max: Some(x[x.len() - 1]),
parameter: None,
});
}
PhysicalConstraint::BoundedVariation(_) => {
continue;
}
_ => {
continue;
}
}
}
Ok(spline_constraints)
}
fn apply_conservation_corrections(
&self,
values: &Array1<T>,
xnew: &ArrayView1<T>,
) -> InterpolateResult<Array1<T>> {
let mut corrected_values = values.clone();
for conservation_law in &self.config.conservation_laws {
match conservation_law {
ConservationLaw::MassConservation { total_mass } => {
corrected_values =
self.apply_mass_conservation(&corrected_values, xnew, *total_mass)?;
}
ConservationLaw::EnergyConservation { total_energy } => {
corrected_values =
self.apply_energy_conservation(&corrected_values, xnew, *total_energy)?;
}
ConservationLaw::GenericConservation {
conserved_quantity,
weight_function,
..
} => {
corrected_values = self.apply_generic_conservation(
&corrected_values,
xnew,
*conserved_quantity,
weight_function.as_ref(),
)?;
}
_ => {
continue;
}
}
}
Ok(corrected_values)
}
fn apply_mass_conservation(
&self,
values: &Array1<T>,
xnew: &ArrayView1<T>,
target_mass: T,
) -> InterpolateResult<Array1<T>> {
if xnew.len() < 2 {
return Ok(values.clone());
}
let mut current_mass = T::zero();
for i in 0..xnew.len() - 1 {
let dx = xnew[i + 1] - xnew[i];
let avg_value = (values[i] + values[i + 1]) / T::from(2.0).expect("Operation failed");
current_mass += avg_value * dx;
}
if current_mass.abs() < T::from(1e-12).expect("Operation failed") {
let domain_width = xnew[xnew.len() - 1] - xnew[0];
let uniform_value = target_mass / domain_width;
Ok(Array1::from_elem(values.len(), uniform_value))
} else if current_mass > T::zero() {
let scaling_factor = target_mass / current_mass;
let max_scaling = T::from(100.0).expect("Operation failed");
let min_scaling = T::from(0.01).expect("Operation failed");
let bounded_scaling = scaling_factor.min(max_scaling).max(min_scaling);
Ok(values * bounded_scaling)
} else {
let domain_width = xnew[xnew.len() - 1] - xnew[0];
let uniform_value = target_mass / domain_width;
Ok(Array1::from_elem(
values.len(),
uniform_value.max(T::zero()),
))
}
}
fn apply_energy_conservation(
&self,
values: &Array1<T>,
xnew: &ArrayView1<T>,
target_energy: T,
) -> InterpolateResult<Array1<T>> {
if xnew.len() < 2 {
return Ok(values.clone());
}
let mut current_energy = T::zero();
for i in 0..xnew.len() - 1 {
let dx = xnew[i + 1] - xnew[i];
let avg_energy = (values[i] * values[i] + values[i + 1] * values[i + 1])
/ T::from(2.0).expect("Operation failed");
current_energy += avg_energy * dx;
}
if current_energy.abs() < T::from(1e-12).expect("Operation failed") {
let domain_width = xnew[xnew.len() - 1] - xnew[0];
let uniform_magnitude = (target_energy / domain_width).sqrt();
Ok(Array1::from_elem(values.len(), uniform_magnitude))
} else if current_energy > T::zero() {
let energy_scaling = (target_energy / current_energy).sqrt();
let max_scaling = T::from(10.0).expect("Operation failed");
let min_scaling = T::from(0.1).expect("Operation failed");
let bounded_scaling = energy_scaling.min(max_scaling).max(min_scaling);
Ok(values * bounded_scaling)
} else {
let domain_width = xnew[xnew.len() - 1] - xnew[0];
let uniform_magnitude = (target_energy / domain_width).sqrt();
Ok(Array1::from_elem(values.len(), uniform_magnitude))
}
}
fn apply_generic_conservation(
&self,
values: &Array1<T>,
xnew: &ArrayView1<T>,
target_quantity: T,
weight_function: Option<&Array1<T>>,
) -> InterpolateResult<Array1<T>> {
if xnew.len() < 2 {
return Ok(values.clone());
}
let weighted_values = if let Some(weights) = weight_function {
if weights.len() != values.len() {
return Err(InterpolateError::DimensionMismatch(
"Weight _function length must match values length".to_string(),
));
}
values * weights
} else {
values.clone()
};
self.apply_mass_conservation(&weighted_values, xnew, target_quantity)
}
fn apply_physics_corrections(
&self,
values: &Array1<T>,
_xnew: &ArrayView1<T>,
) -> InterpolateResult<Array1<T>> {
let mut corrected_values = values.clone();
for constraint in &self.config.constraints {
match constraint {
PhysicalConstraint::Positivity => {
let min_positive = T::from(1e-6).expect("Operation failed"); corrected_values.mapv_inplace(|v| {
if v <= T::zero() {
min_positive
} else {
v.max(min_positive) }
});
}
PhysicalConstraint::BoundedVariation(max_variation) => {
corrected_values = self.limit_variation(&corrected_values, *max_variation)?;
}
_ => {
continue;
}
}
}
Ok(corrected_values)
}
fn limit_variation(
&self,
values: &Array1<T>,
max_variation: T,
) -> InterpolateResult<Array1<T>> {
if values.len() < 2 {
return Ok(values.clone());
}
let mut corrected_values = values.clone();
let mut current_variation = T::zero();
for i in 1..values.len() {
current_variation += (values[i] - values[i - 1]).abs();
}
if current_variation > max_variation {
let smoothing_factor = max_variation / current_variation;
for i in 1..corrected_values.len() {
let diff = corrected_values[i] - corrected_values[i - 1];
corrected_values[i] = corrected_values[i - 1] + diff * smoothing_factor;
}
}
Ok(corrected_values)
}
fn check_constraint_violations(
&self,
values: &Array1<T>,
xnew: &ArrayView1<T>,
) -> InterpolateResult<HashMap<String, T>> {
let mut violations = HashMap::new();
for (i, constraint) in self.config.constraints.iter().enumerate() {
let violation_key = format!("constraint_{}", i);
let violation = match constraint {
PhysicalConstraint::Positivity => values
.iter()
.map(|&v| {
if v < T::zero() {
(-v).max(T::zero())
} else {
T::zero()
}
})
.fold(T::zero(), |acc, v| acc + v),
PhysicalConstraint::Monotonic(increasing) => {
let mut monotonicity_violation = T::zero();
for i in 1..values.len() {
let diff = values[i] - values[i - 1];
if *increasing && diff < T::zero() {
monotonicity_violation += -diff;
} else if !*increasing && diff > T::zero() {
monotonicity_violation += diff;
}
}
monotonicity_violation
}
PhysicalConstraint::BoundaryCondition(x_loc, target_value) => {
let mut min_distance = T::infinity();
let mut closest_value = values[0];
for (j, &x_val) in xnew.iter().enumerate() {
let distance = (x_val - *x_loc).abs();
if distance < min_distance {
min_distance = distance;
closest_value = values[j];
}
}
(closest_value - *target_value).abs()
}
PhysicalConstraint::BoundedVariation(max_variation) => {
let mut total_variation = T::zero();
for i in 1..values.len() {
total_variation += (values[i] - values[i - 1]).abs();
}
if total_variation > *max_variation {
total_variation - *max_variation
} else {
T::zero()
}
}
_ => T::zero(), };
violations.insert(violation_key, violation);
}
Ok(violations)
}
fn check_conservation_errors(
&self,
values: &Array1<T>,
xnew: &ArrayView1<T>,
) -> InterpolateResult<HashMap<String, T>> {
let mut errors = HashMap::new();
for (i, conservation_law) in self.config.conservation_laws.iter().enumerate() {
let error_key = format!("conservation_{}", i);
let error = match conservation_law {
ConservationLaw::MassConservation { total_mass } => {
let current_mass = self.calculate_integral(values, xnew)?;
(current_mass - *total_mass).abs()
}
ConservationLaw::EnergyConservation { total_energy } => {
let energy_values = values.mapv(|v| v * v);
let current_energy = self.calculate_integral(&energy_values, xnew)?;
(current_energy - *total_energy).abs()
}
ConservationLaw::GenericConservation {
conserved_quantity,
weight_function,
..
} => {
let weighted_values = if let Some(weights) = weight_function {
values * weights
} else {
values.clone()
};
let current_quantity = self.calculate_integral(&weighted_values, xnew)?;
(current_quantity - *conserved_quantity).abs()
}
_ => T::zero(),
};
errors.insert(error_key, error);
}
Ok(errors)
}
fn calculate_integral(&self, values: &Array1<T>, x: &ArrayView1<T>) -> InterpolateResult<T> {
if values.len() != x.len() || x.len() < 2 {
return Ok(T::zero());
}
let mut integral = T::zero();
for i in 0..x.len() - 1 {
let dx = x[i + 1] - x[i];
let avg_value = (values[i] + values[i + 1]) / T::from(2.0).expect("Operation failed");
integral += avg_value * dx;
}
Ok(integral)
}
fn calculate_penalty_cost(
&self,
constraint_violations: &HashMap<String, T>,
conservation_errors: &HashMap<String, T>,
) -> InterpolateResult<T> {
let mut total_cost = T::zero();
for &violation in constraint_violations.values() {
total_cost += violation * self.config.penalty_weight;
}
for &error in conservation_errors.values() {
total_cost += error * self.config.penalty_weight;
}
Ok(total_cost)
}
pub fn get_constrained_spline(&self) -> &ConstrainedSpline<T> {
&self.constrained_spline
}
pub fn get_config(&self) -> &PhysicsInformedConfig<T> {
&self.config
}
}
#[allow(dead_code)]
pub fn make_mass_conserving_interpolator<T>(
x: &ArrayView1<T>,
y: &ArrayView1<T>,
total_mass: T,
) -> InterpolateResult<PhysicsInformedInterpolator<T>>
where
T: Float
+ FromPrimitive
+ ToPrimitive
+ Debug
+ Display
+ LowerExp
+ AddAssign
+ SubAssign
+ MulAssign
+ DivAssign
+ RemAssign
+ ScalarOperand
+ Copy
+ 'static,
{
let constraints = vec![PhysicalConstraint::Positivity];
let conservation_laws = vec![ConservationLaw::MassConservation { total_mass }];
let interpolator = PhysicsInformedInterpolator::new(x, y, constraints)?
.with_conservation_laws(conservation_laws);
Ok(interpolator)
}
#[allow(dead_code)]
pub fn make_monotonic_physics_interpolator<T>(
x: &ArrayView1<T>,
y: &ArrayView1<T>,
increasing: bool,
enforce_positivity: bool,
) -> InterpolateResult<PhysicsInformedInterpolator<T>>
where
T: Float
+ FromPrimitive
+ ToPrimitive
+ Debug
+ Display
+ LowerExp
+ AddAssign
+ SubAssign
+ MulAssign
+ DivAssign
+ RemAssign
+ ScalarOperand
+ Copy
+ 'static,
{
let mut constraints = vec![PhysicalConstraint::Monotonic(increasing)];
if enforce_positivity {
constraints.push(PhysicalConstraint::Positivity);
}
let interpolator = PhysicsInformedInterpolator::new(x, y, constraints)?;
Ok(interpolator)
}
#[allow(dead_code)]
pub fn make_smooth_physics_interpolator<T>(
x: &ArrayView1<T>,
y: &ArrayView1<T>,
max_curvature: T,
boundary_conditions: Vec<(T, T)>, ) -> InterpolateResult<PhysicsInformedInterpolator<T>>
where
T: Float
+ FromPrimitive
+ ToPrimitive
+ Debug
+ Display
+ LowerExp
+ AddAssign
+ SubAssign
+ MulAssign
+ DivAssign
+ RemAssign
+ ScalarOperand
+ Copy
+ 'static,
{
let mut constraints = vec![PhysicalConstraint::SmoothnessConstraint(max_curvature)];
for (x_loc, value) in boundary_conditions {
constraints.push(PhysicalConstraint::BoundaryCondition(x_loc, value));
}
let interpolator = PhysicsInformedInterpolator::new(x, y, constraints)?;
Ok(interpolator)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array1;
#[test]
fn test_physics_informed_creation() {
let x = Array1::linspace(0.0, 10.0, 11);
let y = Array1::linspace(1.0, 11.0, 11);
let constraints = vec![
PhysicalConstraint::Positivity,
PhysicalConstraint::Monotonic(true),
];
let interpolator = PhysicsInformedInterpolator::new(&x.view(), &y.view(), constraints);
assert!(interpolator.is_ok());
}
#[test]
fn test_mass_conservation() {
let x = Array1::from_vec(vec![0.0, 0.5, 1.0]);
let y = Array1::from_vec(vec![2.0, 1.0, 2.0]);
let total_mass = 1.5; let interpolator = make_mass_conserving_interpolator(&x.view(), &y.view(), total_mass)
.expect("Operation failed");
let xnew = Array1::linspace(0.0, 1.0, 11); let result = interpolator
.evaluate_with_iteration(&xnew.view())
.expect("Operation failed");
let calculated_mass = interpolator
.calculate_integral(&result.values, &xnew.view())
.expect("Operation failed");
let relative_error = (calculated_mass - total_mass).abs() / total_mass;
assert!(
relative_error < 1.0,
"Mass conservation failed: calculated {calculated_mass:.6}, target {total_mass:.6}, relative error {relative_error:.3}"
);
assert!(
result.values.iter().all(|&v| v.is_finite() && v > 0.0),
"Result contains invalid values"
);
}
#[test]
fn test_positivity_constraint() {
let x = Array1::from_vec(vec![0.0, 1.0, 2.0, 3.0]);
let y = Array1::from_vec(vec![1.0, -0.5, 2.0, 0.5]);
let constraints = vec![PhysicalConstraint::Positivity];
let interpolator = PhysicsInformedInterpolator::new(&x.view(), &y.view(), constraints)
.expect("Operation failed");
let xnew = Array1::linspace(0.0, 3.0, 31);
let result = interpolator
.evaluate_with_iteration(&xnew.view())
.expect("Operation failed");
for &value in result.values.iter() {
assert!(value > 0.0, "Value {value} should be positive");
}
}
#[test]
fn test_monotonic_constraint() {
let x = Array1::linspace(0.0, 5.0, 6);
let y = Array1::from_vec(vec![1.0, 3.0, 2.0, 4.0, 6.0, 5.0]);
let interpolator = make_monotonic_physics_interpolator(&x.view(), &y.view(), true, false)
.expect("Operation failed");
let xnew = Array1::linspace(0.0, 5.0, 51);
let result = interpolator
.evaluate_with_iteration(&xnew.view())
.expect("Operation failed");
let mut violations = 0;
for i in 1..result.values.len() {
if result.values[i] < result.values[i - 1] {
violations += 1;
}
}
assert!(
violations < 5,
"Too many monotonicity violations: {violations}"
);
}
#[test]
#[cfg(feature = "linalg")]
fn test_boundary_conditions() {
let x = Array1::from_vec(vec![0.0, 1.0, 2.0, 3.0, 4.0]);
let y = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let boundary_conditions = vec![(0.0, 1.0)]; let interpolator =
match make_smooth_physics_interpolator(&x.view(), &y.view(), 10.0, boundary_conditions)
{
Ok(interp) => interp,
Err(_) => {
println!("Test case is ill-conditioned, skipping detailed assertions");
return;
}
};
let xnew = Array1::from_vec(vec![0.0, 2.0]); let result = interpolator
.evaluate(&xnew.view())
.expect("Operation failed");
assert!(
(result.values[0] - 1.0).abs() < 2.0,
"Boundary condition not satisfied: got {:.3}, expected ~1.0",
result.values[0]
);
assert!(
result.values[1] > 1.0 && result.values[1] < 6.0,
"Middle point result unreasonable: {:.3}",
result.values[1]
);
}
#[test]
fn test_conservation_laws() {
let x = Array1::linspace(0.0, 1.0, 5);
let y = Array1::from_vec(vec![2.0, 3.0, 1.0, 4.0, 2.0]);
let constraints = vec![PhysicalConstraint::Positivity];
let total_energy = 8.0;
let conservation_laws = vec![ConservationLaw::EnergyConservation { total_energy }];
let interpolator = PhysicsInformedInterpolator::new(&x.view(), &y.view(), constraints)
.expect("Operation failed")
.with_conservation_laws(conservation_laws);
let xnew = Array1::linspace(0.0, 1.0, 21);
let result = interpolator
.evaluate(&xnew.view())
.expect("Operation failed");
assert!(result.conservation_errors.contains_key("conservation_0"));
let energy_error = result.conservation_errors["conservation_0"];
assert!(
energy_error < 10.0,
"Energy conservation error too large: {energy_error}"
);
}
#[test]
fn test_combined_constraints() {
let x = Array1::linspace(0.0, 2.0, 5);
let y = Array1::from_vec(vec![1.0, 2.0, 1.5, 3.0, 2.5]);
let constraints = vec![
PhysicalConstraint::Positivity,
PhysicalConstraint::Monotonic(true),
PhysicalConstraint::BoundedVariation(5.0),
];
let total_mass = 4.0;
let conservation_laws = vec![ConservationLaw::MassConservation { total_mass }];
let interpolator = PhysicsInformedInterpolator::new(&x.view(), &y.view(), constraints)
.expect("Operation failed")
.with_conservation_laws(conservation_laws)
.with_max_iterations(50);
let xnew = Array1::linspace(0.0, 2.0, 21);
let result = interpolator
.evaluate_with_iteration(&xnew.view())
.expect("Operation failed");
let total_violations: f64 = result
.constraint_violations
.values()
.map(|&v| v.to_f64().unwrap_or(0.0))
.sum();
let total_errors: f64 = result
.conservation_errors
.values()
.map(|&v| v.to_f64().unwrap_or(0.0))
.sum();
assert!(
total_violations < 1.0,
"Total constraint violations too large: {total_violations}"
);
assert!(
total_errors < 1.0,
"Total conservation errors too large: {total_errors}"
);
}
}