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
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]>
611 lines
19 KiB
Rust
611 lines
19 KiB
Rust
// Copyright (c) 2024 RustyTorch++ Team
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
//! Boundary condition definitions and application.
|
|
//!
|
|
//! This module provides comprehensive boundary condition types and efficient
|
|
//! application methods for finite element analysis.
|
|
|
|
pub mod contact;
|
|
pub mod dirichlet;
|
|
pub mod neumann;
|
|
pub mod robin;
|
|
pub mod thermal;
|
|
|
|
use crate::assembly::{AdvancedDofNumbering, DofComponent};
|
|
use crate::error::{BoundaryError, FeaResult};
|
|
use crate::mesh::{ElementId, Mesh, NodeId};
|
|
use nalgebra::{DVector, Vector3};
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
pub use contact::*;
|
|
pub use dirichlet::*;
|
|
pub use neumann::*;
|
|
pub use robin::*;
|
|
pub use thermal::*;
|
|
|
|
/// Time-dependent boundary condition function.
|
|
pub struct TimeFunction(pub Box<dyn Fn(f64) -> f64 + Send + Sync>);
|
|
|
|
impl std::fmt::Debug for TimeFunction {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str("TimeFunction")
|
|
}
|
|
}
|
|
|
|
impl std::ops::Deref for TimeFunction {
|
|
type Target = dyn Fn(f64) -> f64 + Send + Sync;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&*self.0
|
|
}
|
|
}
|
|
|
|
/// Spatial boundary condition function.
|
|
pub struct SpatialFunction(pub Box<dyn Fn(&Vector3<f64>) -> f64 + Send + Sync>);
|
|
|
|
impl std::fmt::Debug for SpatialFunction {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str("SpatialFunction")
|
|
}
|
|
}
|
|
|
|
impl std::ops::Deref for SpatialFunction {
|
|
type Target = dyn Fn(&Vector3<f64>) -> f64 + Send + Sync;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&*self.0
|
|
}
|
|
}
|
|
|
|
/// Combined time-spatial boundary condition function.
|
|
pub struct TimeSpatialFunction(pub Box<dyn Fn(f64, &Vector3<f64>) -> f64 + Send + Sync>);
|
|
|
|
impl std::fmt::Debug for TimeSpatialFunction {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str("TimeSpatialFunction")
|
|
}
|
|
}
|
|
|
|
impl std::ops::Deref for TimeSpatialFunction {
|
|
type Target = dyn Fn(f64, &Vector3<f64>) -> f64 + Send + Sync;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&*self.0
|
|
}
|
|
}
|
|
|
|
/// Boundary condition types supported by the FEA solver.
|
|
#[derive(Debug)]
|
|
pub enum BoundaryCondition {
|
|
/// Dirichlet (essential) boundary condition
|
|
Dirichlet(DirichletBC),
|
|
/// Neumann (natural) boundary condition
|
|
Neumann(NeumannBC),
|
|
/// Robin (mixed) boundary condition
|
|
Robin(RobinBC),
|
|
/// Contact boundary condition
|
|
Contact(ContactBC),
|
|
/// Thermal boundary condition
|
|
Thermal(ThermalBC),
|
|
}
|
|
|
|
impl BoundaryCondition {
|
|
/// Get the nodes affected by this boundary condition.
|
|
pub fn affected_nodes(&self) -> Vec<NodeId> {
|
|
match self {
|
|
Self::Dirichlet(bc) => bc.nodes.clone(),
|
|
Self::Neumann(bc) => bc.nodes.clone(),
|
|
Self::Robin(bc) => bc.nodes.clone(),
|
|
Self::Contact(bc) => bc.slave_nodes.clone(),
|
|
Self::Thermal(bc) => bc.nodes.clone(),
|
|
}
|
|
}
|
|
|
|
/// Check if this boundary condition applies at the given time.
|
|
pub fn is_active(&self, time: f64) -> bool {
|
|
match self {
|
|
Self::Dirichlet(bc) => bc.is_active(time),
|
|
Self::Neumann(bc) => bc.is_active(time),
|
|
Self::Robin(bc) => bc.is_active(time),
|
|
Self::Contact(bc) => bc.is_active(time),
|
|
Self::Thermal(bc) => bc.is_active(time),
|
|
}
|
|
}
|
|
|
|
/// Apply this boundary condition to the global system.
|
|
pub fn apply(
|
|
&self,
|
|
mesh: &Mesh,
|
|
dof_numbering: &mut AdvancedDofNumbering,
|
|
global_stiffness: &mut crate::assembly::SparseMatrix,
|
|
global_force: &mut DVector<f64>,
|
|
time: f64,
|
|
) -> FeaResult<()> {
|
|
if !self.is_active(time) {
|
|
return Ok(());
|
|
}
|
|
|
|
match self {
|
|
Self::Dirichlet(bc) => {
|
|
bc.apply(mesh, dof_numbering, global_stiffness, global_force, time)
|
|
}
|
|
Self::Neumann(bc) => {
|
|
bc.apply(mesh, dof_numbering, global_stiffness, global_force, time)
|
|
}
|
|
Self::Robin(bc) => bc.apply(mesh, dof_numbering, global_stiffness, global_force, time),
|
|
Self::Contact(bc) => {
|
|
bc.apply(mesh, dof_numbering, global_stiffness, global_force, time)
|
|
}
|
|
Self::Thermal(bc) => {
|
|
bc.apply(mesh, dof_numbering, global_stiffness, global_force, time)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Collection of boundary conditions with management utilities.
|
|
#[derive(Debug)]
|
|
pub struct BoundaryConditionSet {
|
|
/// All boundary conditions
|
|
conditions: Vec<BoundaryCondition>,
|
|
/// Node sets for organized boundary condition application
|
|
node_sets: HashMap<String, HashSet<NodeId>>,
|
|
/// Element sets for surface-based boundary conditions
|
|
element_sets: HashMap<String, HashSet<ElementId>>,
|
|
/// Time step information for time-dependent conditions
|
|
time_info: TimeInfo,
|
|
}
|
|
|
|
/// Time stepping information for boundary conditions.
|
|
#[derive(Debug, Clone)]
|
|
pub struct TimeInfo {
|
|
pub current_time: f64,
|
|
pub time_step: f64,
|
|
pub start_time: f64,
|
|
pub end_time: f64,
|
|
}
|
|
|
|
impl Default for TimeInfo {
|
|
fn default() -> Self {
|
|
Self {
|
|
current_time: 0.0,
|
|
time_step: 0.1,
|
|
start_time: 0.0,
|
|
end_time: 1.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BoundaryConditionSet {
|
|
/// Create a new boundary condition set.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
conditions: Vec::new(),
|
|
node_sets: HashMap::new(),
|
|
element_sets: HashMap::new(),
|
|
time_info: TimeInfo::default(),
|
|
}
|
|
}
|
|
|
|
/// Add a boundary condition to the set.
|
|
pub fn add_condition(&mut self, condition: BoundaryCondition) {
|
|
self.conditions.push(condition);
|
|
}
|
|
|
|
/// Add multiple boundary conditions.
|
|
pub fn add_conditions(&mut self, conditions: Vec<BoundaryCondition>) {
|
|
self.conditions.extend(conditions);
|
|
}
|
|
|
|
/// Get a reference to the conditions.
|
|
pub fn conditions(&self) -> &[BoundaryCondition] {
|
|
&self.conditions
|
|
}
|
|
|
|
/// Create a node set with a name.
|
|
pub fn create_node_set(&mut self, name: String, nodes: HashSet<NodeId>) {
|
|
self.node_sets.insert(name, nodes);
|
|
}
|
|
|
|
/// Create an element set with a name.
|
|
pub fn create_element_set(&mut self, name: String, elements: HashSet<ElementId>) {
|
|
self.element_sets.insert(name, elements);
|
|
}
|
|
|
|
/// Get nodes in a named set.
|
|
pub fn get_node_set(&self, name: &str) -> Option<&HashSet<NodeId>> {
|
|
self.node_sets.get(name)
|
|
}
|
|
|
|
/// Get elements in a named set.
|
|
pub fn get_element_set(&self, name: &str) -> Option<&HashSet<ElementId>> {
|
|
self.element_sets.get(name)
|
|
}
|
|
|
|
/// Apply all boundary conditions at the current time.
|
|
pub fn apply_all(
|
|
&self,
|
|
mesh: &Mesh,
|
|
dof_numbering: &mut AdvancedDofNumbering,
|
|
global_stiffness: &mut crate::assembly::SparseMatrix,
|
|
global_force: &mut DVector<f64>,
|
|
) -> FeaResult<()> {
|
|
for condition in &self.conditions {
|
|
condition.apply(
|
|
mesh,
|
|
dof_numbering,
|
|
global_stiffness,
|
|
global_force,
|
|
self.time_info.current_time,
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Apply boundary conditions at a specific time.
|
|
pub fn apply_at_time(
|
|
&self,
|
|
mesh: &Mesh,
|
|
dof_numbering: &mut AdvancedDofNumbering,
|
|
global_stiffness: &mut crate::assembly::SparseMatrix,
|
|
global_force: &mut DVector<f64>,
|
|
time: f64,
|
|
) -> FeaResult<()> {
|
|
for condition in &self.conditions {
|
|
condition.apply(mesh, dof_numbering, global_stiffness, global_force, time)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Update time information.
|
|
pub fn set_time_info(&mut self, time_info: TimeInfo) {
|
|
self.time_info = time_info;
|
|
}
|
|
|
|
/// Advance time by one step.
|
|
pub fn advance_time(&mut self) {
|
|
self.time_info.current_time += self.time_info.time_step;
|
|
}
|
|
|
|
/// Get current time.
|
|
pub fn current_time(&self) -> f64 {
|
|
self.time_info.current_time
|
|
}
|
|
|
|
/// Get all nodes affected by boundary conditions.
|
|
pub fn affected_nodes(&self) -> HashSet<NodeId> {
|
|
let mut nodes = HashSet::new();
|
|
for condition in &self.conditions {
|
|
nodes.extend(condition.affected_nodes());
|
|
}
|
|
nodes
|
|
}
|
|
|
|
/// Count active boundary conditions at current time.
|
|
pub fn count_active_conditions(&self) -> usize {
|
|
self.conditions
|
|
.iter()
|
|
.filter(|bc| bc.is_active(self.time_info.current_time))
|
|
.count()
|
|
}
|
|
|
|
/// Get statistics about the boundary condition set.
|
|
pub fn statistics(&self) -> BoundaryConditionStatistics {
|
|
let mut dirichlet_count = 0;
|
|
let mut neumann_count = 0;
|
|
let mut robin_count = 0;
|
|
let mut contact_count = 0;
|
|
let mut thermal_count = 0;
|
|
|
|
for condition in &self.conditions {
|
|
match condition {
|
|
BoundaryCondition::Dirichlet(_) => dirichlet_count += 1,
|
|
BoundaryCondition::Neumann(_) => neumann_count += 1,
|
|
BoundaryCondition::Robin(_) => robin_count += 1,
|
|
BoundaryCondition::Contact(_) => contact_count += 1,
|
|
BoundaryCondition::Thermal(_) => thermal_count += 1,
|
|
}
|
|
}
|
|
|
|
BoundaryConditionStatistics {
|
|
total_conditions: self.conditions.len(),
|
|
active_conditions: self.count_active_conditions(),
|
|
dirichlet_count,
|
|
neumann_count,
|
|
robin_count,
|
|
contact_count,
|
|
thermal_count,
|
|
node_sets: self.node_sets.len(),
|
|
element_sets: self.element_sets.len(),
|
|
affected_nodes: self.affected_nodes().len(),
|
|
}
|
|
}
|
|
|
|
/// Validate all boundary conditions.
|
|
pub fn validate(&self, mesh: &Mesh) -> FeaResult<ValidationReport> {
|
|
let mut errors = Vec::new();
|
|
let mut warnings = Vec::new();
|
|
|
|
// Check that all referenced nodes exist in the mesh
|
|
for condition in &self.conditions {
|
|
for node_id in condition.affected_nodes() {
|
|
if !mesh.nodes.contains_key(&node_id) {
|
|
errors.push(ValidationError::NodeNotFound(node_id));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for conflicting Dirichlet conditions
|
|
let mut dirichlet_dofs = HashMap::new();
|
|
for (index, condition) in self.conditions.iter().enumerate() {
|
|
if let BoundaryCondition::Dirichlet(dirichlet_bc) = condition {
|
|
for &node_id in &dirichlet_bc.nodes {
|
|
for &component in &dirichlet_bc.components {
|
|
let key = (node_id, component);
|
|
if let Some(existing_index) = dirichlet_dofs.get(&key) {
|
|
warnings.push(ValidationWarning::ConflictingDirichlet {
|
|
node_id,
|
|
component,
|
|
condition1: *existing_index,
|
|
condition2: index,
|
|
});
|
|
} else {
|
|
dirichlet_dofs.insert(key, index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check node sets reference valid nodes
|
|
for (set_name, nodes) in &self.node_sets {
|
|
for &node_id in nodes {
|
|
if !mesh.nodes.contains_key(&node_id) {
|
|
errors.push(ValidationError::NodeSetInvalidNode {
|
|
set_name: set_name.clone(),
|
|
node_id,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check element sets reference valid elements
|
|
for (set_name, elements) in &self.element_sets {
|
|
for &element_id in elements {
|
|
if !mesh.elements.contains_key(&element_id) {
|
|
errors.push(ValidationError::ElementSetInvalidElement {
|
|
set_name: set_name.clone(),
|
|
element_id,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(ValidationReport {
|
|
is_valid: errors.is_empty(),
|
|
errors,
|
|
warnings,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Default for BoundaryConditionSet {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Statistics about boundary conditions.
|
|
#[derive(Debug, Clone)]
|
|
pub struct BoundaryConditionStatistics {
|
|
pub total_conditions: usize,
|
|
pub active_conditions: usize,
|
|
pub dirichlet_count: usize,
|
|
pub neumann_count: usize,
|
|
pub robin_count: usize,
|
|
pub contact_count: usize,
|
|
pub thermal_count: usize,
|
|
pub node_sets: usize,
|
|
pub element_sets: usize,
|
|
pub affected_nodes: usize,
|
|
}
|
|
|
|
impl std::fmt::Display for BoundaryConditionStatistics {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
writeln!(f, "Boundary Condition Statistics:")?;
|
|
writeln!(f, " Total conditions: {}", self.total_conditions)?;
|
|
writeln!(f, " Active conditions: {}", self.active_conditions)?;
|
|
writeln!(f, " Dirichlet conditions: {}", self.dirichlet_count)?;
|
|
writeln!(f, " Neumann conditions: {}", self.neumann_count)?;
|
|
writeln!(f, " Robin conditions: {}", self.robin_count)?;
|
|
writeln!(f, " Contact conditions: {}", self.contact_count)?;
|
|
writeln!(f, " Thermal conditions: {}", self.thermal_count)?;
|
|
writeln!(f, " Node sets: {}", self.node_sets)?;
|
|
writeln!(f, " Element sets: {}", self.element_sets)?;
|
|
writeln!(f, " Affected nodes: {}", self.affected_nodes)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Validation errors for boundary conditions.
|
|
#[derive(Debug, Clone)]
|
|
pub enum ValidationError {
|
|
NodeNotFound(NodeId),
|
|
NodeSetInvalidNode {
|
|
set_name: String,
|
|
node_id: NodeId,
|
|
},
|
|
ElementSetInvalidElement {
|
|
set_name: String,
|
|
element_id: ElementId,
|
|
},
|
|
}
|
|
|
|
/// Validation warnings for boundary conditions.
|
|
#[derive(Debug, Clone)]
|
|
pub enum ValidationWarning {
|
|
ConflictingDirichlet {
|
|
node_id: NodeId,
|
|
component: DofComponent,
|
|
condition1: usize,
|
|
condition2: usize,
|
|
},
|
|
}
|
|
|
|
/// Validation report for boundary conditions.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ValidationReport {
|
|
pub is_valid: bool,
|
|
pub errors: Vec<ValidationError>,
|
|
pub warnings: Vec<ValidationWarning>,
|
|
}
|
|
|
|
impl std::fmt::Display for ValidationReport {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
writeln!(f, "Boundary Condition Validation Report:")?;
|
|
writeln!(f, " Valid: {}", self.is_valid)?;
|
|
writeln!(f, " Errors: {}", self.errors.len())?;
|
|
writeln!(f, " Warnings: {}", self.warnings.len())?;
|
|
|
|
if !self.errors.is_empty() {
|
|
writeln!(f, "\nErrors:")?;
|
|
for error in &self.errors {
|
|
writeln!(f, " {error:?}")?;
|
|
}
|
|
}
|
|
|
|
if !self.warnings.is_empty() {
|
|
writeln!(f, "\nWarnings:")?;
|
|
for warning in &self.warnings {
|
|
writeln!(f, " {warning:?}")?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Boundary condition application utilities.
|
|
pub struct BoundaryConditionApplicator;
|
|
|
|
impl BoundaryConditionApplicator {
|
|
/// Apply Dirichlet boundary condition using elimination method.
|
|
pub fn apply_dirichlet_elimination(
|
|
dof: usize,
|
|
value: f64,
|
|
global_stiffness: &mut crate::assembly::SparseMatrix,
|
|
global_force: &mut DVector<f64>,
|
|
) -> FeaResult<()> {
|
|
global_stiffness.set_dirichlet(dof, value)?;
|
|
global_force[dof] = value;
|
|
Ok(())
|
|
}
|
|
|
|
/// Apply Neumann boundary condition (add to force vector).
|
|
pub fn apply_neumann_force(
|
|
dof: usize,
|
|
force: f64,
|
|
global_force: &mut DVector<f64>,
|
|
) -> FeaResult<()> {
|
|
if dof < global_force.len() {
|
|
global_force[dof] += force;
|
|
Ok(())
|
|
} else {
|
|
Err(BoundaryError::DofOutOfBounds {
|
|
dof,
|
|
max_dof: global_force.len().saturating_sub(1),
|
|
}
|
|
.into())
|
|
}
|
|
}
|
|
|
|
/// Apply Robin boundary condition (add to both stiffness and force).
|
|
pub fn apply_robin(
|
|
dof: usize,
|
|
stiffness_contribution: f64,
|
|
force_contribution: f64,
|
|
global_stiffness: &mut crate::assembly::SparseMatrix,
|
|
global_force: &mut DVector<f64>,
|
|
) -> FeaResult<()> {
|
|
global_stiffness.add_entry(dof, dof, stiffness_contribution)?;
|
|
global_force[dof] += force_contribution;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::mesh::{MaterialId, geometry::Rectangle};
|
|
|
|
#[test]
|
|
fn test_boundary_condition_set_creation() {
|
|
let bc_set = BoundaryConditionSet::new();
|
|
assert_eq!(bc_set.conditions.len(), 0);
|
|
assert_eq!(bc_set.node_sets.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_time_info() {
|
|
let time_info = TimeInfo::default();
|
|
assert_eq!(time_info.current_time, 0.0);
|
|
assert_eq!(time_info.time_step, 0.1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_node_set_creation() {
|
|
let mut bc_set = BoundaryConditionSet::new();
|
|
let mut nodes = HashSet::new();
|
|
nodes.insert(NodeId(0));
|
|
nodes.insert(NodeId(1));
|
|
|
|
bc_set.create_node_set("fixed_nodes".to_string(), nodes.clone());
|
|
assert_eq!(bc_set.get_node_set("fixed_nodes"), Some(&nodes));
|
|
}
|
|
|
|
#[test]
|
|
fn test_boundary_condition_statistics() {
|
|
let bc_set = BoundaryConditionSet::new();
|
|
let stats = bc_set.statistics();
|
|
|
|
assert_eq!(stats.total_conditions, 0);
|
|
assert_eq!(stats.active_conditions, 0);
|
|
assert_eq!(stats.dirichlet_count, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_validation_empty_set() {
|
|
let rect = Rectangle::new(1.0, 1.0);
|
|
let mesh = rect.generate_quad_mesh(1, 1, MaterialId(0)).unwrap();
|
|
let bc_set = BoundaryConditionSet::new();
|
|
|
|
let report = bc_set.validate(&mesh).unwrap();
|
|
assert!(report.is_valid);
|
|
assert_eq!(report.errors.len(), 0);
|
|
assert_eq!(report.warnings.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_boundary_condition_applicator() {
|
|
use crate::assembly::SparseMatrix;
|
|
|
|
let mut stiffness = SparseMatrix::new(3, 3);
|
|
let mut force = DVector::zeros(3);
|
|
|
|
// Test Dirichlet application
|
|
BoundaryConditionApplicator::apply_dirichlet_elimination(
|
|
0,
|
|
1.0,
|
|
&mut stiffness,
|
|
&mut force,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(force[0], 1.0);
|
|
|
|
// Test Neumann application
|
|
BoundaryConditionApplicator::apply_neumann_force(1, 10.0, &mut force).unwrap();
|
|
assert_eq!(force[1], 10.0);
|
|
}
|
|
}
|