#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::num::FpCategory;
pub mod prelude;
fn all_finite(values: &[f64]) -> bool {
values.iter().all(|value| value.is_finite())
}
fn finite(value: f64) -> Option<f64> {
value.is_finite().then_some(value)
}
const fn is_zero(value: f64) -> bool {
matches!(value.classify(), FpCategory::Zero)
}
#[must_use]
pub fn normal_stress(force: f64, area: f64) -> Option<f64> {
if !all_finite(&[force, area]) || area <= 0.0 {
return None;
}
finite(force / area)
}
#[must_use]
pub fn shear_stress(force: f64, area: f64) -> Option<f64> {
if !all_finite(&[force, area]) || area <= 0.0 {
return None;
}
finite(force / area)
}
#[must_use]
pub fn force_from_stress(stress: f64, area: f64) -> Option<f64> {
if !all_finite(&[stress, area]) || area < 0.0 {
return None;
}
finite(stress * area)
}
#[must_use]
pub fn normal_strain(change_in_length: f64, original_length: f64) -> Option<f64> {
if !all_finite(&[change_in_length, original_length]) || original_length <= 0.0 {
return None;
}
finite(change_in_length / original_length)
}
#[must_use]
pub fn shear_strain(displacement: f64, height: f64) -> Option<f64> {
if !all_finite(&[displacement, height]) || height <= 0.0 {
return None;
}
finite(displacement / height)
}
#[must_use]
pub fn change_in_length(strain: f64, original_length: f64) -> Option<f64> {
if !all_finite(&[strain, original_length]) || original_length < 0.0 {
return None;
}
finite(strain * original_length)
}
#[must_use]
pub fn final_length(original_length: f64, strain: f64) -> Option<f64> {
if !all_finite(&[original_length, strain]) || original_length < 0.0 {
return None;
}
let result = original_length * (1.0 + strain);
if result < 0.0 {
return None;
}
finite(result)
}
#[must_use]
pub fn youngs_modulus(stress: f64, strain: f64) -> Option<f64> {
if !all_finite(&[stress, strain]) || is_zero(strain) {
return None;
}
let result = stress / strain;
if result < 0.0 {
return None;
}
finite(result)
}
#[must_use]
pub fn stress_from_youngs_modulus(youngs_modulus: f64, strain: f64) -> Option<f64> {
if !all_finite(&[youngs_modulus, strain]) || youngs_modulus < 0.0 {
return None;
}
finite(youngs_modulus * strain)
}
#[must_use]
pub fn strain_from_youngs_modulus(stress: f64, youngs_modulus: f64) -> Option<f64> {
if !all_finite(&[stress, youngs_modulus]) || youngs_modulus <= 0.0 {
return None;
}
finite(stress / youngs_modulus)
}
#[must_use]
pub fn shear_modulus(shear_stress: f64, shear_strain: f64) -> Option<f64> {
if !all_finite(&[shear_stress, shear_strain]) || is_zero(shear_strain) {
return None;
}
let result = shear_stress / shear_strain;
if result < 0.0 {
return None;
}
finite(result)
}
#[must_use]
pub fn shear_stress_from_modulus(shear_modulus: f64, shear_strain: f64) -> Option<f64> {
if !all_finite(&[shear_modulus, shear_strain]) || shear_modulus < 0.0 {
return None;
}
finite(shear_modulus * shear_strain)
}
#[must_use]
pub fn shear_strain_from_modulus(shear_stress: f64, shear_modulus: f64) -> Option<f64> {
if !all_finite(&[shear_stress, shear_modulus]) || shear_modulus <= 0.0 {
return None;
}
finite(shear_stress / shear_modulus)
}
#[must_use]
pub fn bulk_modulus(pressure_change: f64, volume_strain: f64) -> Option<f64> {
if !all_finite(&[pressure_change, volume_strain]) || is_zero(volume_strain) {
return None;
}
let result = -pressure_change / volume_strain;
if result < 0.0 {
return None;
}
finite(result)
}
#[must_use]
pub fn pressure_change_from_bulk_modulus(bulk_modulus: f64, volume_strain: f64) -> Option<f64> {
if !all_finite(&[bulk_modulus, volume_strain]) || bulk_modulus < 0.0 {
return None;
}
finite(-bulk_modulus * volume_strain)
}
#[must_use]
pub fn volume_strain(change_in_volume: f64, original_volume: f64) -> Option<f64> {
if !all_finite(&[change_in_volume, original_volume]) || original_volume <= 0.0 {
return None;
}
finite(change_in_volume / original_volume)
}
#[must_use]
pub fn change_in_volume(volume_strain: f64, original_volume: f64) -> Option<f64> {
if !all_finite(&[volume_strain, original_volume]) || original_volume < 0.0 {
return None;
}
finite(volume_strain * original_volume)
}
#[must_use]
pub fn poisson_ratio(transverse_strain: f64, axial_strain: f64) -> Option<f64> {
if !all_finite(&[transverse_strain, axial_strain]) || is_zero(axial_strain) {
return None;
}
finite(-transverse_strain / axial_strain)
}
#[must_use]
pub fn transverse_strain_from_poisson_ratio(poisson_ratio: f64, axial_strain: f64) -> Option<f64> {
if !all_finite(&[poisson_ratio, axial_strain]) {
return None;
}
finite(-poisson_ratio * axial_strain)
}
#[must_use]
pub fn is_common_poisson_ratio(poisson_ratio: f64) -> bool {
poisson_ratio.is_finite() && (0.0..=0.5).contains(&poisson_ratio)
}
#[must_use]
pub fn shear_modulus_from_youngs_and_poisson(
youngs_modulus: f64,
poisson_ratio: f64,
) -> Option<f64> {
if !all_finite(&[youngs_modulus, poisson_ratio]) || youngs_modulus < 0.0 {
return None;
}
let denominator = 2.0 * (1.0 + poisson_ratio);
if !denominator.is_finite() || is_zero(denominator) {
return None;
}
finite(youngs_modulus / denominator)
}
#[must_use]
pub fn bulk_modulus_from_youngs_and_poisson(
youngs_modulus: f64,
poisson_ratio: f64,
) -> Option<f64> {
if !all_finite(&[youngs_modulus, poisson_ratio]) || youngs_modulus < 0.0 {
return None;
}
let denominator = 3.0 * poisson_ratio.mul_add(-2.0, 1.0);
if !denominator.is_finite() || denominator <= 0.0 {
return None;
}
finite(youngs_modulus / denominator)
}
#[must_use]
pub fn youngs_modulus_from_shear_and_poisson(
shear_modulus: f64,
poisson_ratio: f64,
) -> Option<f64> {
if !all_finite(&[shear_modulus, poisson_ratio]) || shear_modulus < 0.0 {
return None;
}
let result = 2.0 * shear_modulus * (1.0 + poisson_ratio);
if result < 0.0 {
return None;
}
finite(result)
}
#[must_use]
pub fn axial_deformation(force: f64, length: f64, area: f64, youngs_modulus: f64) -> Option<f64> {
if !all_finite(&[force, length, area, youngs_modulus])
|| length < 0.0
|| area <= 0.0
|| youngs_modulus <= 0.0
{
return None;
}
finite(force * length / (area * youngs_modulus))
}
#[must_use]
pub fn axial_stiffness(area: f64, youngs_modulus: f64, length: f64) -> Option<f64> {
if !all_finite(&[area, youngs_modulus, length])
|| area < 0.0
|| youngs_modulus < 0.0
|| length <= 0.0
{
return None;
}
finite(area * youngs_modulus / length)
}
#[must_use]
pub fn force_from_axial_deformation(
deformation: f64,
length: f64,
area: f64,
youngs_modulus: f64,
) -> Option<f64> {
if !all_finite(&[deformation, length, area, youngs_modulus])
|| length <= 0.0
|| area < 0.0
|| youngs_modulus < 0.0
{
return None;
}
finite(deformation * area * youngs_modulus / length)
}
#[must_use]
pub fn elastic_energy_density(stress: f64, strain: f64) -> Option<f64> {
if !all_finite(&[stress, strain]) {
return None;
}
let result = 0.5 * stress * strain;
if result < 0.0 {
return None;
}
finite(result)
}
#[must_use]
pub fn elastic_energy_from_spring_constant(spring_constant: f64, deformation: f64) -> Option<f64> {
if !all_finite(&[spring_constant, deformation]) || spring_constant < 0.0 {
return None;
}
finite(0.5 * spring_constant * deformation * deformation)
}
#[must_use]
pub fn elastic_energy_from_force_deformation(force: f64, deformation: f64) -> Option<f64> {
if !all_finite(&[force, deformation]) {
return None;
}
let result = 0.5 * force * deformation;
if result < 0.0 {
return None;
}
finite(result)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ElasticMaterial {
pub youngs_modulus: f64,
pub poisson_ratio: Option<f64>,
}
impl ElasticMaterial {
#[must_use]
pub fn new(youngs_modulus: f64) -> Option<Self> {
if !youngs_modulus.is_finite() || youngs_modulus < 0.0 {
return None;
}
Some(Self {
youngs_modulus,
poisson_ratio: None,
})
}
#[must_use]
pub fn with_poisson_ratio(youngs_modulus: f64, poisson_ratio: f64) -> Option<Self> {
if !poisson_ratio.is_finite() {
return None;
}
Self::new(youngs_modulus).map(|material| Self {
poisson_ratio: Some(poisson_ratio),
..material
})
}
#[must_use]
pub fn stress_from_strain(&self, strain: f64) -> Option<f64> {
stress_from_youngs_modulus(self.youngs_modulus, strain)
}
#[must_use]
pub fn strain_from_stress(&self, stress: f64) -> Option<f64> {
strain_from_youngs_modulus(stress, self.youngs_modulus)
}
#[must_use]
pub fn shear_modulus(&self) -> Option<f64> {
self.poisson_ratio
.and_then(|ratio| shear_modulus_from_youngs_and_poisson(self.youngs_modulus, ratio))
}
#[must_use]
pub fn bulk_modulus(&self) -> Option<f64> {
self.poisson_ratio
.and_then(|ratio| bulk_modulus_from_youngs_and_poisson(self.youngs_modulus, ratio))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ElasticBar {
pub length: f64,
pub area: f64,
pub youngs_modulus: f64,
}
impl ElasticBar {
#[must_use]
pub fn new(length: f64, area: f64, youngs_modulus: f64) -> Option<Self> {
if !all_finite(&[length, area, youngs_modulus])
|| length <= 0.0
|| area <= 0.0
|| youngs_modulus <= 0.0
{
return None;
}
Some(Self {
length,
area,
youngs_modulus,
})
}
#[must_use]
pub fn axial_stiffness(&self) -> Option<f64> {
axial_stiffness(self.area, self.youngs_modulus, self.length)
}
#[must_use]
pub fn deformation_under_force(&self, force: f64) -> Option<f64> {
axial_deformation(force, self.length, self.area, self.youngs_modulus)
}
#[must_use]
pub fn force_for_deformation(&self, deformation: f64) -> Option<f64> {
force_from_axial_deformation(deformation, self.length, self.area, self.youngs_modulus)
}
#[must_use]
pub fn stress_under_force(&self, force: f64) -> Option<f64> {
normal_stress(force, self.area)
}
#[must_use]
pub fn strain_under_force(&self, force: f64) -> Option<f64> {
self.stress_under_force(force)
.and_then(|stress| strain_from_youngs_modulus(stress, self.youngs_modulus))
}
}
#[cfg(test)]
mod tests {
use super::{
ElasticBar, ElasticMaterial, axial_deformation, axial_stiffness, bulk_modulus,
bulk_modulus_from_youngs_and_poisson, change_in_length, change_in_volume,
elastic_energy_density, elastic_energy_from_force_deformation,
elastic_energy_from_spring_constant, final_length, force_from_axial_deformation,
force_from_stress, is_common_poisson_ratio, normal_strain, normal_stress, poisson_ratio,
pressure_change_from_bulk_modulus, shear_modulus, shear_modulus_from_youngs_and_poisson,
shear_strain, shear_strain_from_modulus, shear_stress, shear_stress_from_modulus,
strain_from_youngs_modulus, stress_from_youngs_modulus,
transverse_strain_from_poisson_ratio, volume_strain, youngs_modulus,
youngs_modulus_from_shear_and_poisson,
};
fn assert_option_approx_eq(actual: Option<f64>, expected: f64) {
let Some(actual) = actual else {
panic!("expected Some({expected}), got None");
};
assert!(
(actual - expected).abs() < 1.0e-12,
"expected {expected}, got {actual}"
);
}
#[test]
fn stress_helpers_cover_expected_cases() {
assert_eq!(normal_stress(100.0, 2.0), Some(50.0));
assert_eq!(normal_stress(100.0, 0.0), None);
assert_eq!(shear_stress(100.0, 2.0), Some(50.0));
assert_eq!(shear_stress(100.0, 0.0), None);
assert_eq!(force_from_stress(50.0, 2.0), Some(100.0));
assert_eq!(force_from_stress(50.0, -2.0), None);
}
#[test]
fn strain_helpers_cover_expected_cases() {
assert_option_approx_eq(normal_strain(2.0, 10.0), 0.2);
assert_option_approx_eq(normal_strain(-2.0, 10.0), -0.2);
assert_eq!(normal_strain(2.0, 0.0), None);
assert_option_approx_eq(shear_strain(2.0, 10.0), 0.2);
assert_eq!(shear_strain(2.0, 0.0), None);
assert_option_approx_eq(change_in_length(0.2, 10.0), 2.0);
assert_option_approx_eq(final_length(10.0, 0.2), 12.0);
assert_eq!(final_length(10.0, -1.2), None);
}
#[test]
fn youngs_modulus_helpers_cover_expected_cases() {
assert_option_approx_eq(youngs_modulus(100.0, 0.01), 10_000.0);
assert_eq!(youngs_modulus(100.0, 0.0), None);
assert_eq!(youngs_modulus(-100.0, 0.01), None);
assert_option_approx_eq(stress_from_youngs_modulus(10_000.0, 0.01), 100.0);
assert_option_approx_eq(strain_from_youngs_modulus(100.0, 10_000.0), 0.01);
}
#[test]
fn shear_helpers_cover_expected_cases() {
assert_option_approx_eq(shear_modulus(50.0, 0.01), 5_000.0);
assert_eq!(shear_modulus(50.0, 0.0), None);
assert_option_approx_eq(shear_stress_from_modulus(5_000.0, 0.01), 50.0);
assert_option_approx_eq(shear_strain_from_modulus(50.0, 5_000.0), 0.01);
}
#[test]
fn bulk_helpers_cover_expected_cases() {
assert_option_approx_eq(volume_strain(-2.0, 10.0), -0.2);
assert_eq!(volume_strain(-2.0, 0.0), None);
assert_option_approx_eq(bulk_modulus(100.0, -0.01), 10_000.0);
assert_eq!(bulk_modulus(100.0, 0.01), None);
assert_option_approx_eq(pressure_change_from_bulk_modulus(10_000.0, -0.01), 100.0);
assert_option_approx_eq(change_in_volume(-0.2, 10.0), -2.0);
}
#[test]
fn poisson_helpers_cover_expected_cases() {
assert_option_approx_eq(poisson_ratio(-0.003, 0.01), 0.3);
assert_eq!(poisson_ratio(-0.003, 0.0), None);
assert_option_approx_eq(transverse_strain_from_poisson_ratio(0.3, 0.01), -0.003);
assert!(is_common_poisson_ratio(0.3));
assert!(!is_common_poisson_ratio(-0.1));
assert!(!is_common_poisson_ratio(0.6));
}
#[test]
fn modulus_relationships_cover_expected_cases() {
assert_option_approx_eq(shear_modulus_from_youngs_and_poisson(260.0, 0.3), 100.0);
assert_option_approx_eq(bulk_modulus_from_youngs_and_poisson(300.0, 0.25), 200.0);
assert_option_approx_eq(youngs_modulus_from_shear_and_poisson(100.0, 0.3), 260.0);
}
#[test]
fn axial_helpers_cover_expected_cases() {
assert_option_approx_eq(axial_deformation(100.0, 10.0, 2.0, 1_000.0), 0.5);
assert_eq!(axial_deformation(100.0, 10.0, 0.0, 1_000.0), None);
assert_option_approx_eq(axial_stiffness(2.0, 1_000.0, 10.0), 200.0);
assert_eq!(axial_stiffness(2.0, 1_000.0, 0.0), None);
assert_option_approx_eq(force_from_axial_deformation(0.5, 10.0, 2.0, 1_000.0), 100.0);
}
#[test]
fn elastic_energy_helpers_cover_expected_cases() {
assert_option_approx_eq(elastic_energy_density(100.0, 0.01), 0.5);
assert_eq!(elastic_energy_density(-100.0, 0.01), None);
assert_option_approx_eq(elastic_energy_from_spring_constant(100.0, 0.5), 12.5);
assert_eq!(elastic_energy_from_spring_constant(-100.0, 0.5), None);
assert_option_approx_eq(elastic_energy_from_force_deformation(100.0, 0.5), 25.0);
assert_eq!(elastic_energy_from_force_deformation(-100.0, 0.5), None);
}
#[test]
fn elastic_material_methods_cover_expected_cases() {
let Some(material) = ElasticMaterial::with_poisson_ratio(260.0, 0.3) else {
panic!("expected valid ElasticMaterial");
};
assert_option_approx_eq(material.stress_from_strain(0.01), 2.6);
assert_option_approx_eq(material.strain_from_stress(2.6), 0.01);
assert_option_approx_eq(material.shear_modulus(), 100.0);
assert_eq!(ElasticMaterial::new(-1.0), None);
assert_eq!(ElasticMaterial::with_poisson_ratio(260.0, f64::NAN), None);
}
#[test]
fn elastic_bar_methods_cover_expected_cases() {
let Some(bar) = ElasticBar::new(10.0, 2.0, 1_000.0) else {
panic!("expected valid ElasticBar");
};
assert_option_approx_eq(bar.axial_stiffness(), 200.0);
assert_option_approx_eq(bar.deformation_under_force(100.0), 0.5);
assert_option_approx_eq(bar.force_for_deformation(0.5), 100.0);
assert_option_approx_eq(bar.stress_under_force(100.0), 50.0);
assert_option_approx_eq(bar.strain_under_force(100.0), 0.05);
assert_eq!(ElasticBar::new(0.0, 2.0, 1_000.0), None);
assert_eq!(ElasticBar::new(10.0, 0.0, 1_000.0), None);
assert_eq!(ElasticBar::new(10.0, 2.0, 0.0), None);
}
}