Files
rustytorch/crates/specialized/rtx-fea/src/boundary/robin.rs
T
Omar SobhandClaude Fable 5 87cf392556
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
rtx-fea: re-enable the remaining CPU test modules; fix three real defects they caught
All 33 remaining #[cfg(disabled)] test modules outside the GPU cluster are
now enabled: assembly (dof_mapping, constraints, global assembly), boundary
(mod + dirichlet/neumann/robin/thermal/contact), analysis (mod + static),
materials (mod, linear_elastic, hyperelastic, plasticity), elements (mod,
element_matrices, isoparametric, jacobian, quadrature), mesh (element_types,
connectivity, topology, topology_repair), solvers (mod, direct, iterative,
nonlinear) and lib.rs. Lib tests 117 -> 335, stable across repeated runs.
Only gpu_solver_tests and the GpuMeshData fixture stay disabled — they need
CUDA hardware and belong to the GPU tranche.

Three real defects found by the newly-compiling tests, each fixed:

- Direct solvers reused factorizations keyed on matrix SIZE alone.
  In a Newton loop the Jacobian changes every iteration but never its
  dimension, so LuDirect/CholeskyDirect/LdltDirect silently solved with the
  first iteration's factorization forever — Newton on x^2-4 crawled to
  x=1.955 in 1000 iterations instead of converging in 5. Invisible in
  single-solve linear analysis, which is why every green test passed over
  it. solve() now factorizes the matrix it is given.

- AdaptiveQuadrature's refinement re-integrated the WHOLE domain once per
  subdomain, so each level multiplied the estimate by the subdomain count:
  integrating e^x over [-1,1] at tolerance 1e-10 returned ~75 instead of
  2.35. The recursion now descends into each sub-box with its share of the
  error budget.

- compute_skewness read Jacobian columns as coordinate-line tangents, but
  the trait's jacobian() stores tangents in ROWS: on a sheared
  parallelogram whose tangents meet at 14 degrees it reported skewness 0.43
  instead of 0.84 — measuring per-component gradients, not mesh skew.

Fixtures corrected rather than the code where the fixture was wrong:
sigma_yy ~ 0 asserted uniaxial-stress physics on a uniaxial-strain state
(exact Lame values now asserted); an "unstable" orthotropic parameter set
that satisfies the determinant stability condition (delta = 0.187 > 0); a
unit-cube hex Jacobian of 1.0 that assumed a unit reference element (it is
0.125 from [-1,1]^3); a "distorted" quad whose centre Jacobian is exactly
orthogonal, asserted as skewed (flattening and shearing now tested
separately); a quality score below the implementation's own calibration;
Rayleigh damping fed the scalar-field mass (now expanded via the Kronecker
identity, with C = alpha*M + beta*K asserted entry-wise); an element
factory required to construct Point/Line types that have no implementation;
and DOF counts that encoded the repaired 3-DOFs-per-node-on-2-D defect.

MaterialDatabase::add_material call sites updated to the (id, material,
name) signature; ConnectivityInfo::build takes elements only;
TopologyRepair::triangle_quality (normalized 4*sqrt(3)*A/sum(a^2)) added
for the repair tests; create_subdomain_rule_* widened to pub(super) for the
quadrature tests.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-19 18:52:50 -07:00

685 lines
21 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2024 RustyTorch++ Team
// Licensed under the Apache License, Version 2.0
//! Robin (mixed) boundary conditions combining Dirichlet and Neumann effects.
use super::{BoundaryConditionApplicator, TimeFunction};
use crate::assembly::{AdvancedDofNumbering, DofComponent, SparseMatrix};
use crate::error::{BoundaryError, FeaResult};
use crate::mesh::{Mesh, NodeId};
use nalgebra::DVector;
/// Robin boundary condition types for different physics.
#[derive(Debug)]
pub enum RobinType {
/// Linear spring: k * u + c = force
LinearSpring {
spring_constant: f64,
reference_displacement: f64,
},
/// Convective heat transfer: h * (T - T_ambient) = q
ConvectiveHeatTransfer {
heat_transfer_coefficient: f64,
ambient_temperature: f64,
},
/// Radiation heat transfer: σε * (T^4 - T_ambient^4) = q
RadiationHeatTransfer {
stefan_boltzmann: f64,
emissivity: f64,
ambient_temperature: f64,
},
/// Damped spring: c * du/dt + k * u = force
DampedSpring {
spring_constant: f64,
damping_coefficient: f64,
reference_displacement: f64,
},
/// Custom Robin condition: α * u + β * du/dn = γ
Custom {
alpha: f64,
beta: f64,
gamma: f64,
time_function: Option<TimeFunction>,
},
}
impl RobinType {
/// Get stiffness contribution for this Robin condition.
pub fn get_stiffness_contribution(&self, time: f64, current_value: f64) -> f64 {
match self {
Self::LinearSpring {
spring_constant, ..
} => *spring_constant,
Self::ConvectiveHeatTransfer {
heat_transfer_coefficient,
..
} => *heat_transfer_coefficient,
Self::RadiationHeatTransfer {
stefan_boltzmann,
emissivity,
ambient_temperature: _,
} => {
// Linearized radiation: d/dT(σε(T^4 - T_amb^4)) ≈ 4σεT^3
4.0 * stefan_boltzmann * emissivity * current_value.powi(3)
}
Self::DampedSpring {
spring_constant, ..
} => *spring_constant,
Self::Custom {
alpha,
time_function,
..
} => {
if let Some(func) = time_function {
alpha * func(time)
} else {
*alpha
}
}
}
}
/// Get force contribution for this Robin condition.
pub fn get_force_contribution(&self, time: f64, current_value: f64) -> f64 {
match self {
Self::LinearSpring {
spring_constant,
reference_displacement,
} => spring_constant * reference_displacement,
Self::ConvectiveHeatTransfer {
heat_transfer_coefficient,
ambient_temperature,
} => heat_transfer_coefficient * ambient_temperature,
Self::RadiationHeatTransfer {
stefan_boltzmann,
emissivity,
ambient_temperature,
} => {
// Linearized: σε * T_amb^4 + 3σεT^3 * T_amb
stefan_boltzmann
* emissivity
* (ambient_temperature.powi(4)
+ 3.0 * current_value.powi(3) * ambient_temperature)
}
Self::DampedSpring {
spring_constant,
reference_displacement,
..
} => spring_constant * reference_displacement,
Self::Custom {
beta,
gamma,
time_function,
..
} => {
if let Some(func) = time_function {
beta * func(time) + gamma
} else {
*gamma
}
}
}
}
/// Create a simple spring condition.
pub fn spring(spring_constant: f64, reference_displacement: f64) -> Self {
Self::LinearSpring {
spring_constant,
reference_displacement,
}
}
/// Create a convective heat transfer condition.
pub fn convection(heat_transfer_coefficient: f64, ambient_temperature: f64) -> Self {
Self::ConvectiveHeatTransfer {
heat_transfer_coefficient,
ambient_temperature,
}
}
/// Create a radiation heat transfer condition.
pub fn radiation(emissivity: f64, ambient_temperature: f64) -> Self {
Self::RadiationHeatTransfer {
stefan_boltzmann: 5.67e-8, // Stefan-Boltzmann constant
emissivity,
ambient_temperature,
}
}
/// Create a damped spring condition.
pub fn damped_spring(
spring_constant: f64,
damping_coefficient: f64,
reference_displacement: f64,
) -> Self {
Self::DampedSpring {
spring_constant,
damping_coefficient,
reference_displacement,
}
}
}
/// Robin boundary condition.
#[derive(Debug)]
pub struct RobinBC {
/// Nodes where this condition applies
pub nodes: Vec<NodeId>,
/// DOF components affected
pub components: Vec<DofComponent>,
/// Robin condition type
pub condition_type: RobinType,
/// Time range when this condition is active
pub time_range: Option<(f64, f64)>,
/// Scaling factor
pub scaling_factor: f64,
/// Previous time step values for time derivatives
pub previous_values: Option<Vec<f64>>,
/// Previous time for derivative calculation
pub previous_time: Option<f64>,
}
impl RobinBC {
/// Create a new Robin boundary condition.
pub fn new(
nodes: Vec<NodeId>,
components: Vec<DofComponent>,
condition_type: RobinType,
) -> Self {
Self {
nodes,
components,
condition_type,
time_range: None,
scaling_factor: 1.0,
previous_values: None,
previous_time: None,
}
}
/// Create a spring boundary condition.
pub fn spring_support(
nodes: Vec<NodeId>,
components: Vec<DofComponent>,
spring_constant: f64,
reference_displacement: f64,
) -> Self {
Self::new(
nodes,
components,
RobinType::spring(spring_constant, reference_displacement),
)
}
/// Create a convective heat transfer boundary condition.
pub fn convective_boundary(
_nodes: Vec<NodeId>,
heat_transfer_coefficient: f64,
ambient_temperature: f64,
) -> Self {
Self::new(
vec![],
vec![DofComponent::Temperature],
RobinType::convection(heat_transfer_coefficient, ambient_temperature),
)
}
/// Set time range for this boundary condition.
pub fn with_time_range(mut self, start_time: f64, end_time: f64) -> Self {
self.time_range = Some((start_time, end_time));
self
}
/// Set scaling factor.
pub fn with_scaling(mut self, factor: f64) -> Self {
self.scaling_factor = factor;
self
}
/// Check if this boundary condition is active at the given time.
pub fn is_active(&self, time: f64) -> bool {
if let Some((start_time, end_time)) = self.time_range {
time >= start_time && time <= end_time
} else {
true
}
}
/// Update previous values for time derivative calculation.
pub fn update_previous_values(&mut self, values: Vec<f64>, time: f64) {
self.previous_values = Some(values);
self.previous_time = Some(time);
}
/// Get time derivative for damped conditions.
fn get_time_derivative(&self, current_values: &[f64], dt: f64) -> Vec<f64> {
if let (Some(prev_values), Some(_)) = (&self.previous_values, self.previous_time) {
current_values
.iter()
.zip(prev_values.iter())
.map(|(curr, prev)| (curr - prev) / dt)
.collect()
} else {
vec![0.0; current_values.len()]
}
}
/// Apply this Robin boundary condition to the global system.
pub fn apply(
&self,
mesh: &Mesh,
dof_numbering: &mut AdvancedDofNumbering,
global_stiffness: &mut SparseMatrix,
global_force: &mut DVector<f64>,
time: f64,
) -> FeaResult<()> {
self.apply_with_solution(
mesh,
dof_numbering,
global_stiffness,
global_force,
time,
None,
)
}
/// Apply Robin boundary condition with current solution values.
pub fn apply_with_solution(
&self,
mesh: &Mesh,
dof_numbering: &mut AdvancedDofNumbering,
global_stiffness: &mut SparseMatrix,
global_force: &mut DVector<f64>,
time: f64,
current_solution: Option<&DVector<f64>>,
) -> FeaResult<()> {
if !self.is_active(time) {
return Ok(());
}
for &node_id in &self.nodes {
// Get node position
let node = mesh
.nodes
.get(&node_id)
.ok_or(BoundaryError::NodeNotFound { node_id: node_id.0 })?;
let _node_position = node.position();
// Apply to each component
for &component in &self.components {
if let Some(dof) = dof_numbering.get_dof(node_id, component) {
// Get current value for nonlinear Robin conditions
let current_value = if let Some(solution) = current_solution {
if dof < solution.len() {
solution[dof]
} else {
0.0
}
} else {
0.0
};
// Get stiffness and force contributions
let stiffness_contribution = self
.condition_type
.get_stiffness_contribution(time, current_value)
* self.scaling_factor;
let force_contribution = self
.condition_type
.get_force_contribution(time, current_value)
* self.scaling_factor;
// Apply Robin condition
BoundaryConditionApplicator::apply_robin(
dof,
stiffness_contribution,
force_contribution,
global_stiffness,
global_force,
)?;
}
}
}
Ok(())
}
/// Apply damped Robin condition (requires previous time step data).
pub fn apply_damped(
&mut self,
mesh: &Mesh,
dof_numbering: &mut AdvancedDofNumbering,
global_stiffness: &mut SparseMatrix,
global_force: &mut DVector<f64>,
time: f64,
current_solution: &DVector<f64>,
dt: f64,
) -> FeaResult<()> {
if !self.is_active(time) {
return Ok(());
}
// Handle damped spring conditions
if let RobinType::DampedSpring {
spring_constant,
damping_coefficient,
reference_displacement,
} = &self.condition_type
{
// Collect current values
let mut current_values = Vec::new();
let mut dof_indices = Vec::new();
for &node_id in &self.nodes {
for &component in &self.components {
if let Some(dof) = dof_numbering.get_dof(node_id, component)
&& dof < current_solution.len()
{
current_values.push(current_solution[dof]);
dof_indices.push(dof);
}
}
}
// Get time derivatives
let time_derivatives = self.get_time_derivative(&current_values, dt);
// Apply damped Robin condition
for (i, &dof) in dof_indices.iter().enumerate() {
let velocity = if i < time_derivatives.len() {
time_derivatives[i]
} else {
0.0
};
let _current_displacement = if i < current_values.len() {
current_values[i]
} else {
0.0
};
// Stiffness: k + c/dt (implicit time integration)
let stiffness_contribution =
(spring_constant + damping_coefficient / dt) * self.scaling_factor;
// Force: k * u_ref + c * v_prev / dt
let force_contribution = (spring_constant * reference_displacement
+ damping_coefficient * velocity / dt)
* self.scaling_factor;
BoundaryConditionApplicator::apply_robin(
dof,
stiffness_contribution,
force_contribution,
global_stiffness,
global_force,
)?;
}
// Update previous values
self.update_previous_values(current_values, time);
} else {
// For non-damped conditions, use regular apply
self.apply_with_solution(
mesh,
dof_numbering,
global_stiffness,
global_force,
time,
Some(current_solution),
)?;
}
Ok(())
}
/// Get all DOFs affected by this boundary condition.
pub fn get_affected_dofs(&self, dof_numbering: &AdvancedDofNumbering) -> Vec<usize> {
let mut dofs = Vec::new();
for &node_id in &self.nodes {
for &component in &self.components {
if let Some(dof) = dof_numbering.get_dof(node_id, component) {
dofs.push(dof);
}
}
}
dofs.sort_unstable();
dofs
}
}
/// Collection of common Robin boundary condition patterns.
pub struct RobinPatterns;
impl RobinPatterns {
/// Create elastic foundation (Winkler foundation).
pub fn elastic_foundation(nodes: Vec<NodeId>, foundation_modulus: f64) -> RobinBC {
RobinBC::spring_support(
nodes,
vec![DofComponent::DisplacementY], // Assuming Y is vertical
foundation_modulus,
0.0,
)
}
/// Create viscous damper.
pub fn viscous_damper(
nodes: Vec<NodeId>,
components: Vec<DofComponent>,
damping_coefficient: f64,
) -> RobinBC {
RobinBC::new(
nodes,
components,
RobinType::damped_spring(0.0, damping_coefficient, 0.0),
)
}
/// Create spring-damper system.
pub fn spring_damper(
nodes: Vec<NodeId>,
components: Vec<DofComponent>,
spring_constant: f64,
damping_coefficient: f64,
reference_displacement: f64,
) -> RobinBC {
RobinBC::new(
nodes,
components,
RobinType::damped_spring(spring_constant, damping_coefficient, reference_displacement),
)
}
/// Create thermal convection boundary.
pub fn thermal_convection(
nodes: Vec<NodeId>,
heat_transfer_coefficient: f64,
ambient_temperature: f64,
) -> RobinBC {
RobinBC::convective_boundary(nodes, heat_transfer_coefficient, ambient_temperature)
}
/// Create thermal radiation boundary.
pub fn thermal_radiation(
nodes: Vec<NodeId>,
emissivity: f64,
ambient_temperature: f64,
) -> RobinBC {
RobinBC::new(
nodes,
vec![DofComponent::Temperature],
RobinType::radiation(emissivity, ambient_temperature),
)
}
/// Create acoustic impedance boundary.
pub fn acoustic_impedance(nodes: Vec<NodeId>, impedance: f64) -> RobinBC {
RobinBC::new(
nodes,
vec![DofComponent::Pressure],
RobinType::Custom {
alpha: impedance,
beta: 1.0,
gamma: 0.0,
time_function: None,
},
)
}
/// Create contact spring (penalty method).
pub fn contact_spring(nodes: Vec<NodeId>, contact_stiffness: f64, gap: f64) -> RobinBC {
RobinBC::spring_support(
nodes,
vec![DofComponent::DisplacementZ], // Assuming Z is contact direction
contact_stiffness,
gap,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::{MaterialId, geometry::Rectangle};
#[test]
fn test_robin_type_spring() {
let robin = RobinType::spring(1000.0, 0.1);
let stiffness = robin.get_stiffness_contribution(0.0, 0.0);
let force = robin.get_force_contribution(0.0, 0.0);
assert_eq!(stiffness, 1000.0);
assert_eq!(force, 100.0); // 1000.0 * 0.1
}
#[test]
fn test_robin_type_convection() {
let robin = RobinType::convection(25.0, 300.0);
let stiffness = robin.get_stiffness_contribution(0.0, 350.0);
let force = robin.get_force_contribution(0.0, 350.0);
assert_eq!(stiffness, 25.0);
assert_eq!(force, 7500.0); // 25.0 * 300.0
}
#[test]
fn test_robin_type_radiation() {
let robin = RobinType::radiation(0.8, 300.0);
let stiffness = robin.get_stiffness_contribution(0.0, 350.0);
assert!(stiffness > 0.0); // Should be positive
let force = robin.get_force_contribution(0.0, 350.0);
assert!(force > 0.0); // Should be positive for radiation
}
#[test]
fn test_robin_bc_creation() {
let bc = RobinBC::spring_support(
vec![NodeId(0), NodeId(1)],
vec![DofComponent::DisplacementY],
1000.0,
0.0,
);
assert_eq!(bc.nodes, vec![NodeId(0), NodeId(1)]);
assert_eq!(bc.components, vec![DofComponent::DisplacementY]);
assert_eq!(bc.scaling_factor, 1.0);
}
#[test]
fn test_robin_bc_with_time_range() {
let bc = RobinBC::spring_support(
vec![NodeId(0)],
vec![DofComponent::DisplacementX],
500.0,
0.05,
)
.with_time_range(1.0, 5.0);
assert!(!bc.is_active(0.5));
assert!(bc.is_active(3.0));
assert!(!bc.is_active(6.0));
}
#[test]
fn test_robin_patterns_elastic_foundation() {
let bc = RobinPatterns::elastic_foundation(vec![NodeId(0), NodeId(1)], 50000.0);
assert_eq!(bc.nodes, vec![NodeId(0), NodeId(1)]);
assert_eq!(bc.components, vec![DofComponent::DisplacementY]);
}
#[test]
fn test_robin_patterns_spring_damper() {
let bc = RobinPatterns::spring_damper(
vec![NodeId(0)],
vec![DofComponent::DisplacementX],
1000.0,
50.0,
0.0,
);
if let RobinType::DampedSpring {
spring_constant,
damping_coefficient,
reference_displacement,
} = bc.condition_type
{
assert_eq!(spring_constant, 1000.0);
assert_eq!(damping_coefficient, 50.0);
assert_eq!(reference_displacement, 0.0);
} else {
panic!("Expected DampedSpring type");
}
}
#[test]
fn test_robin_patterns_thermal_convection() {
let bc = RobinPatterns::thermal_convection(vec![NodeId(0)], 15.0, 293.15);
assert_eq!(bc.components, vec![DofComponent::Temperature]);
}
#[test]
fn test_time_derivative_calculation() {
let mut bc = RobinBC::spring_support(
vec![NodeId(0)],
vec![DofComponent::DisplacementX],
1000.0,
0.0,
);
// Set previous values
bc.update_previous_values(vec![0.1], 1.0);
// Calculate derivatives
let current_values = vec![0.15];
let dt = 0.1;
let derivatives = bc.get_time_derivative(&current_values, dt);
assert_eq!(derivatives.len(), 1);
assert!((derivatives[0] - 0.5).abs() < 1e-10); // (0.15 - 0.1) / 0.1 = 0.5
}
#[test]
fn test_robin_type_custom() {
let robin = RobinType::Custom {
alpha: 2.0,
beta: 3.0,
gamma: 5.0,
time_function: None,
};
let stiffness = robin.get_stiffness_contribution(0.0, 0.0);
let force = robin.get_force_contribution(0.0, 0.0);
assert_eq!(stiffness, 2.0);
assert_eq!(force, 5.0);
}
}