pub mod assembly;
pub mod boundary;
pub mod elements;
pub mod mesh;
pub mod solvers;
pub use assembly::{assemble_global_system, DofNumbering};
pub use boundary::{
apply_boundary_conditions, BoundaryCondition, BoundaryConditionSet, DirichletBc, NeumannBc,
};
pub use elements::{
ElementStiffness, ElementType, GaussQuadrature, LineElement, QuadElement, ShapeFunction,
TriElement,
};
pub use mesh::{Element, Mesh, MeshQuality, Node};
pub use solvers::{
solve_elasticity_1d, solve_heat_equation_1d, ConjugateGradientSolver, DirectSolver, FemSolver,
PostProcessor, SolverConfig,
};
use crate::error::NumRs2Error;
use std::fmt;
use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum FemError {
#[error("Mesh error: {0}")]
MeshError(String),
#[error("Element error: {0}")]
ElementError(String),
#[error("Assembly error: {0}")]
AssemblyError(String),
#[error("Solver error: {0}")]
SolverError(String),
#[error("Boundary condition error: {0}")]
BoundaryError(String),
#[error("Invalid parameter: {0}")]
InvalidParameter(String),
#[error("Singular system: {0}")]
SingularSystem(String),
#[error("Numerical error: {0}")]
NumericalError(String),
}
pub type FemResult<T> = std::result::Result<T, FemError>;
impl From<FemError> for NumRs2Error {
fn from(err: FemError) -> Self {
NumRs2Error::ComputationError(format!("FEM: {}", err))
}
}
impl From<NumRs2Error> for FemError {
fn from(err: NumRs2Error) -> Self {
FemError::NumericalError(format!("NumRS2: {}", err))
}
}
pub trait FemProblem {
fn dimension(&self) -> usize;
fn conductivity(&self, point: &[f64]) -> f64;
fn source_term(&self, point: &[f64]) -> f64;
fn dofs_per_node(&self) -> usize {
1
}
fn name(&self) -> &str;
}
pub struct HeatConduction1D {
pub conductivity_val: f64,
source_fn: Box<dyn Fn(f64) -> f64 + Send + Sync>,
problem_name: String,
}
impl HeatConduction1D {
pub fn new(
conductivity: f64,
source: impl Fn(f64) -> f64 + Send + Sync + 'static,
) -> FemResult<Self> {
if conductivity <= 0.0 {
return Err(FemError::InvalidParameter(
"Conductivity must be positive".to_string(),
));
}
Ok(Self {
conductivity_val: conductivity,
source_fn: Box::new(source),
problem_name: "1D Heat Conduction".to_string(),
})
}
pub fn eval_source(&self, x: f64) -> f64 {
(self.source_fn)(x)
}
}
impl FemProblem for HeatConduction1D {
fn dimension(&self) -> usize {
1
}
fn conductivity(&self, _point: &[f64]) -> f64 {
self.conductivity_val
}
fn source_term(&self, point: &[f64]) -> f64 {
if point.is_empty() {
return 0.0;
}
(self.source_fn)(point[0])
}
fn name(&self) -> &str {
&self.problem_name
}
}
impl fmt::Debug for HeatConduction1D {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HeatConduction1D")
.field("conductivity", &self.conductivity_val)
.field("name", &self.problem_name)
.finish()
}
}
pub struct BarElasticity1D {
pub youngs_modulus: f64,
pub cross_section_area: f64,
source_fn: Box<dyn Fn(f64) -> f64 + Send + Sync>,
problem_name: String,
}
impl BarElasticity1D {
pub fn new(
youngs_modulus: f64,
cross_section_area: f64,
source: impl Fn(f64) -> f64 + Send + Sync + 'static,
) -> FemResult<Self> {
if youngs_modulus <= 0.0 {
return Err(FemError::InvalidParameter(
"Young's modulus must be positive".to_string(),
));
}
if cross_section_area <= 0.0 {
return Err(FemError::InvalidParameter(
"Cross-section area must be positive".to_string(),
));
}
Ok(Self {
youngs_modulus,
cross_section_area,
source_fn: Box::new(source),
problem_name: "1D Bar Elasticity".to_string(),
})
}
pub fn axial_stiffness(&self) -> f64 {
self.youngs_modulus * self.cross_section_area
}
pub fn eval_source(&self, x: f64) -> f64 {
(self.source_fn)(x)
}
}
impl FemProblem for BarElasticity1D {
fn dimension(&self) -> usize {
1
}
fn conductivity(&self, _point: &[f64]) -> f64 {
self.youngs_modulus * self.cross_section_area
}
fn source_term(&self, point: &[f64]) -> f64 {
if point.is_empty() {
return 0.0;
}
(self.source_fn)(point[0])
}
fn name(&self) -> &str {
&self.problem_name
}
}
impl fmt::Debug for BarElasticity1D {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BarElasticity1D")
.field("youngs_modulus", &self.youngs_modulus)
.field("cross_section_area", &self.cross_section_area)
.field("name", &self.problem_name)
.finish()
}
}
pub struct PlaneStress2D {
pub youngs_modulus: f64,
pub poisson_ratio: f64,
pub thickness: f64,
problem_name: String,
}
impl PlaneStress2D {
pub fn new(youngs_modulus: f64, poisson_ratio: f64, thickness: f64) -> FemResult<Self> {
if youngs_modulus <= 0.0 {
return Err(FemError::InvalidParameter(
"Young's modulus must be positive".to_string(),
));
}
if poisson_ratio <= -1.0 || poisson_ratio >= 0.5 {
return Err(FemError::InvalidParameter(
"Poisson's ratio must be in (-1, 0.5)".to_string(),
));
}
if thickness <= 0.0 {
return Err(FemError::InvalidParameter(
"Thickness must be positive".to_string(),
));
}
Ok(Self {
youngs_modulus,
poisson_ratio,
thickness,
problem_name: "2D Plane Stress".to_string(),
})
}
pub fn elasticity_matrix(&self) -> [[f64; 3]; 3] {
let e = self.youngs_modulus;
let nu = self.poisson_ratio;
let factor = e / (1.0 - nu * nu);
[
[factor, factor * nu, 0.0],
[factor * nu, factor, 0.0],
[0.0, 0.0, factor * (1.0 - nu) / 2.0],
]
}
}
impl FemProblem for PlaneStress2D {
fn dimension(&self) -> usize {
2
}
fn conductivity(&self, _point: &[f64]) -> f64 {
self.youngs_modulus
}
fn source_term(&self, _point: &[f64]) -> f64 {
0.0
}
fn dofs_per_node(&self) -> usize {
2
}
fn name(&self) -> &str {
&self.problem_name
}
}
impl fmt::Debug for PlaneStress2D {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PlaneStress2D")
.field("youngs_modulus", &self.youngs_modulus)
.field("poisson_ratio", &self.poisson_ratio)
.field("thickness", &self.thickness)
.field("name", &self.problem_name)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fem_error_creation() {
let err = FemError::MeshError("bad mesh".to_string());
assert!(format!("{}", err).contains("bad mesh"));
}
#[test]
fn test_fem_error_conversion() {
let fem_err = FemError::SolverError("did not converge".to_string());
let numrs_err: NumRs2Error = fem_err.into();
assert!(format!("{}", numrs_err).contains("FEM"));
}
#[test]
fn test_heat_conduction_1d() {
let problem = HeatConduction1D::new(1.0, |x| x * x).expect("valid problem");
assert_eq!(problem.dimension(), 1);
assert_eq!(problem.conductivity(&[0.5]), 1.0);
assert!((problem.source_term(&[2.0]) - 4.0).abs() < 1e-12);
assert_eq!(problem.dofs_per_node(), 1);
}
#[test]
fn test_heat_conduction_invalid() {
let result = HeatConduction1D::new(-1.0, |_| 0.0);
assert!(result.is_err());
}
#[test]
fn test_bar_elasticity_1d() {
let problem = BarElasticity1D::new(200e9, 0.01, |_| 1000.0).expect("valid problem");
assert_eq!(problem.dimension(), 1);
assert!((problem.axial_stiffness() - 2e9).abs() < 1.0);
assert!((problem.source_term(&[0.5]) - 1000.0).abs() < 1e-12);
}
#[test]
fn test_plane_stress_2d() {
let problem = PlaneStress2D::new(200e9, 0.3, 0.01).expect("valid problem");
assert_eq!(problem.dimension(), 2);
assert_eq!(problem.dofs_per_node(), 2);
let d = problem.elasticity_matrix();
assert!((d[0][1] - d[1][0]).abs() < 1e-6);
let expected_d33 = 200e9 * (1.0 - 0.3) / (2.0 * (1.0 - 0.3 * 0.3));
assert!((d[2][2] - expected_d33).abs() / expected_d33 < 1e-10);
}
#[test]
fn test_plane_stress_invalid_poisson() {
let result = PlaneStress2D::new(200e9, 0.5, 0.01);
assert!(result.is_err());
}
}