Skip to main content

use_elasticity/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Small scalar elasticity helpers.
5
6use core::num::FpCategory;
7
8pub mod prelude;
9
10fn all_finite(values: &[f64]) -> bool {
11    values.iter().all(|value| value.is_finite())
12}
13
14fn finite(value: f64) -> Option<f64> {
15    value.is_finite().then_some(value)
16}
17
18const fn is_zero(value: f64) -> bool {
19    matches!(value.classify(), FpCategory::Zero)
20}
21
22/// Computes normal stress from applied force and cross-sectional area.
23///
24/// Formula: `σ = F / A`.
25///
26/// Returns `None` when `area` is less than or equal to zero or when any input or result is not
27/// finite.
28///
29/// # Examples
30///
31/// ```
32/// use use_elasticity::normal_stress;
33///
34/// assert_eq!(normal_stress(100.0, 2.0), Some(50.0));
35/// ```
36#[must_use]
37pub fn normal_stress(force: f64, area: f64) -> Option<f64> {
38    if !all_finite(&[force, area]) || area <= 0.0 {
39        return None;
40    }
41
42    finite(force / area)
43}
44
45/// Computes shear stress from applied force and loaded area.
46///
47/// Formula: `τ = F / A`.
48///
49/// Returns `None` when `area` is less than or equal to zero or when any input or result is not
50/// finite.
51#[must_use]
52pub fn shear_stress(force: f64, area: f64) -> Option<f64> {
53    if !all_finite(&[force, area]) || area <= 0.0 {
54        return None;
55    }
56
57    finite(force / area)
58}
59
60/// Computes force from stress and cross-sectional area.
61///
62/// Formula: `F = σA`.
63///
64/// Returns `None` when `area` is negative or when any input or result is not finite.
65#[must_use]
66pub fn force_from_stress(stress: f64, area: f64) -> Option<f64> {
67    if !all_finite(&[stress, area]) || area < 0.0 {
68        return None;
69    }
70
71    finite(stress * area)
72}
73
74/// Computes normal strain from change in length and original length.
75///
76/// Formula: `ε = ΔL / L0`.
77///
78/// Returns `None` when `original_length` is less than or equal to zero or when any input or
79/// result is not finite.
80///
81/// # Examples
82///
83/// ```
84/// use use_elasticity::normal_strain;
85///
86/// assert_eq!(normal_strain(2.0, 10.0), Some(0.2));
87/// ```
88#[must_use]
89pub fn normal_strain(change_in_length: f64, original_length: f64) -> Option<f64> {
90    if !all_finite(&[change_in_length, original_length]) || original_length <= 0.0 {
91        return None;
92    }
93
94    finite(change_in_length / original_length)
95}
96
97/// Computes engineering shear strain from lateral displacement and height.
98///
99/// Formula: `γ = x / h`.
100///
101/// Returns `None` when `height` is less than or equal to zero or when any input or result is not
102/// finite.
103#[must_use]
104pub fn shear_strain(displacement: f64, height: f64) -> Option<f64> {
105    if !all_finite(&[displacement, height]) || height <= 0.0 {
106        return None;
107    }
108
109    finite(displacement / height)
110}
111
112/// Computes change in length from strain and original length.
113///
114/// Formula: `ΔL = εL0`.
115///
116/// Returns `None` when `original_length` is negative or when any input or result is not finite.
117#[must_use]
118pub fn change_in_length(strain: f64, original_length: f64) -> Option<f64> {
119    if !all_finite(&[strain, original_length]) || original_length < 0.0 {
120        return None;
121    }
122
123    finite(strain * original_length)
124}
125
126/// Computes final length after elastic axial strain.
127///
128/// Formula: `L = L0 * (1 + ε)`.
129///
130/// Returns `None` when `original_length` is negative, when the result is negative, or when any
131/// input or result is not finite.
132#[must_use]
133pub fn final_length(original_length: f64, strain: f64) -> Option<f64> {
134    if !all_finite(&[original_length, strain]) || original_length < 0.0 {
135        return None;
136    }
137
138    let result = original_length * (1.0 + strain);
139    if result < 0.0 {
140        return None;
141    }
142
143    finite(result)
144}
145
146/// Computes Young's modulus from stress and strain.
147///
148/// Formula: `E = σ / ε`.
149///
150/// Returns `None` when `strain` is zero, when the result is negative, or when any input or result
151/// is not finite.
152///
153/// # Examples
154///
155/// ```
156/// use use_elasticity::youngs_modulus;
157///
158/// assert_eq!(youngs_modulus(100.0, 0.01), Some(10_000.0));
159/// ```
160#[must_use]
161pub fn youngs_modulus(stress: f64, strain: f64) -> Option<f64> {
162    if !all_finite(&[stress, strain]) || is_zero(strain) {
163        return None;
164    }
165
166    let result = stress / strain;
167    if result < 0.0 {
168        return None;
169    }
170
171    finite(result)
172}
173
174/// Computes stress from Young's modulus and axial strain.
175///
176/// Formula: `σ = Eε`.
177///
178/// Returns `None` when `youngs_modulus` is negative or when any input or result is not finite.
179#[must_use]
180pub fn stress_from_youngs_modulus(youngs_modulus: f64, strain: f64) -> Option<f64> {
181    if !all_finite(&[youngs_modulus, strain]) || youngs_modulus < 0.0 {
182        return None;
183    }
184
185    finite(youngs_modulus * strain)
186}
187
188/// Computes strain from stress and Young's modulus.
189///
190/// Formula: `ε = σ / E`.
191///
192/// Returns `None` when `youngs_modulus` is less than or equal to zero or when any input or result
193/// is not finite.
194#[must_use]
195pub fn strain_from_youngs_modulus(stress: f64, youngs_modulus: f64) -> Option<f64> {
196    if !all_finite(&[stress, youngs_modulus]) || youngs_modulus <= 0.0 {
197        return None;
198    }
199
200    finite(stress / youngs_modulus)
201}
202
203/// Computes shear modulus from shear stress and shear strain.
204///
205/// Formula: `G = τ / γ`.
206///
207/// Returns `None` when `shear_strain` is zero, when the result is negative, or when any input or
208/// result is not finite.
209#[must_use]
210pub fn shear_modulus(shear_stress: f64, shear_strain: f64) -> Option<f64> {
211    if !all_finite(&[shear_stress, shear_strain]) || is_zero(shear_strain) {
212        return None;
213    }
214
215    let result = shear_stress / shear_strain;
216    if result < 0.0 {
217        return None;
218    }
219
220    finite(result)
221}
222
223/// Computes shear stress from shear modulus and shear strain.
224///
225/// Formula: `τ = Gγ`.
226///
227/// Returns `None` when `shear_modulus` is negative or when any input or result is not finite.
228#[must_use]
229pub fn shear_stress_from_modulus(shear_modulus: f64, shear_strain: f64) -> Option<f64> {
230    if !all_finite(&[shear_modulus, shear_strain]) || shear_modulus < 0.0 {
231        return None;
232    }
233
234    finite(shear_modulus * shear_strain)
235}
236
237/// Computes shear strain from shear stress and shear modulus.
238///
239/// Formula: `γ = τ / G`.
240///
241/// Returns `None` when `shear_modulus` is less than or equal to zero or when any input or result
242/// is not finite.
243#[must_use]
244pub fn shear_strain_from_modulus(shear_stress: f64, shear_modulus: f64) -> Option<f64> {
245    if !all_finite(&[shear_stress, shear_modulus]) || shear_modulus <= 0.0 {
246        return None;
247    }
248
249    finite(shear_stress / shear_modulus)
250}
251
252/// Computes bulk modulus from pressure change and volumetric strain.
253///
254/// Formula: `K = -ΔP / (ΔV / V)`.
255///
256/// Returns `None` when `volume_strain` is zero, when the result is negative, or when any input or
257/// result is not finite.
258#[must_use]
259pub fn bulk_modulus(pressure_change: f64, volume_strain: f64) -> Option<f64> {
260    if !all_finite(&[pressure_change, volume_strain]) || is_zero(volume_strain) {
261        return None;
262    }
263
264    let result = -pressure_change / volume_strain;
265    if result < 0.0 {
266        return None;
267    }
268
269    finite(result)
270}
271
272/// Computes pressure change from bulk modulus and volumetric strain.
273///
274/// Formula: `ΔP = -K * volume_strain`.
275///
276/// Returns `None` when `bulk_modulus` is negative or when any input or result is not finite.
277#[must_use]
278pub fn pressure_change_from_bulk_modulus(bulk_modulus: f64, volume_strain: f64) -> Option<f64> {
279    if !all_finite(&[bulk_modulus, volume_strain]) || bulk_modulus < 0.0 {
280        return None;
281    }
282
283    finite(-bulk_modulus * volume_strain)
284}
285
286/// Computes volumetric strain from change in volume and original volume.
287///
288/// Formula: `ΔV / V0`.
289///
290/// Returns `None` when `original_volume` is less than or equal to zero or when any input or
291/// result is not finite.
292#[must_use]
293pub fn volume_strain(change_in_volume: f64, original_volume: f64) -> Option<f64> {
294    if !all_finite(&[change_in_volume, original_volume]) || original_volume <= 0.0 {
295        return None;
296    }
297
298    finite(change_in_volume / original_volume)
299}
300
301/// Computes change in volume from volumetric strain and original volume.
302///
303/// Formula: `ΔV = volume_strain * V0`.
304///
305/// Returns `None` when `original_volume` is negative or when any input or result is not finite.
306#[must_use]
307pub fn change_in_volume(volume_strain: f64, original_volume: f64) -> Option<f64> {
308    if !all_finite(&[volume_strain, original_volume]) || original_volume < 0.0 {
309        return None;
310    }
311
312    finite(volume_strain * original_volume)
313}
314
315/// Computes Poisson's ratio from transverse and axial strain.
316///
317/// Formula: `ν = -ε_transverse / ε_axial`.
318///
319/// Returns `None` when `axial_strain` is zero or when any input or result is not finite.
320/// Auxetic values are allowed; common stable isotropic materials often fall within a narrower
321/// non-negative range.
322#[must_use]
323pub fn poisson_ratio(transverse_strain: f64, axial_strain: f64) -> Option<f64> {
324    if !all_finite(&[transverse_strain, axial_strain]) || is_zero(axial_strain) {
325        return None;
326    }
327
328    finite(-transverse_strain / axial_strain)
329}
330
331/// Computes transverse strain from Poisson's ratio and axial strain.
332///
333/// Formula: `ε_transverse = -ν * ε_axial`.
334///
335/// Returns `None` when any input or result is not finite.
336#[must_use]
337pub fn transverse_strain_from_poisson_ratio(poisson_ratio: f64, axial_strain: f64) -> Option<f64> {
338    if !all_finite(&[poisson_ratio, axial_strain]) {
339        return None;
340    }
341
342    finite(-poisson_ratio * axial_strain)
343}
344
345/// Returns `true` when Poisson's ratio is finite and between `0.0` and `0.5`, inclusive.
346#[must_use]
347pub fn is_common_poisson_ratio(poisson_ratio: f64) -> bool {
348    poisson_ratio.is_finite() && (0.0..=0.5).contains(&poisson_ratio)
349}
350
351/// Computes shear modulus from Young's modulus and Poisson's ratio.
352///
353/// Formula: `G = E / (2 * (1 + ν))`.
354///
355/// Returns `None` when `youngs_modulus` is negative, when the denominator is zero, or when any
356/// input or result is not finite.
357///
358/// # Examples
359///
360/// ```
361/// use use_elasticity::shear_modulus_from_youngs_and_poisson;
362///
363/// assert_eq!(shear_modulus_from_youngs_and_poisson(260.0, 0.3), Some(100.0));
364/// ```
365#[must_use]
366pub fn shear_modulus_from_youngs_and_poisson(
367    youngs_modulus: f64,
368    poisson_ratio: f64,
369) -> Option<f64> {
370    if !all_finite(&[youngs_modulus, poisson_ratio]) || youngs_modulus < 0.0 {
371        return None;
372    }
373
374    let denominator = 2.0 * (1.0 + poisson_ratio);
375    if !denominator.is_finite() || is_zero(denominator) {
376        return None;
377    }
378
379    finite(youngs_modulus / denominator)
380}
381
382/// Computes bulk modulus from Young's modulus and Poisson's ratio.
383///
384/// Formula: `K = E / (3 * (1 - 2ν))`.
385///
386/// Returns `None` when `youngs_modulus` is negative, when the denominator is less than or equal
387/// to zero, or when any input or result is not finite.
388#[must_use]
389pub fn bulk_modulus_from_youngs_and_poisson(
390    youngs_modulus: f64,
391    poisson_ratio: f64,
392) -> Option<f64> {
393    if !all_finite(&[youngs_modulus, poisson_ratio]) || youngs_modulus < 0.0 {
394        return None;
395    }
396
397    let denominator = 3.0 * poisson_ratio.mul_add(-2.0, 1.0);
398    if !denominator.is_finite() || denominator <= 0.0 {
399        return None;
400    }
401
402    finite(youngs_modulus / denominator)
403}
404
405/// Computes Young's modulus from shear modulus and Poisson's ratio.
406///
407/// Formula: `E = 2G(1 + ν)`.
408///
409/// Returns `None` when `shear_modulus` is negative, when the result is negative, or when any
410/// input or result is not finite.
411#[must_use]
412pub fn youngs_modulus_from_shear_and_poisson(
413    shear_modulus: f64,
414    poisson_ratio: f64,
415) -> Option<f64> {
416    if !all_finite(&[shear_modulus, poisson_ratio]) || shear_modulus < 0.0 {
417        return None;
418    }
419
420    let result = 2.0 * shear_modulus * (1.0 + poisson_ratio);
421    if result < 0.0 {
422        return None;
423    }
424
425    finite(result)
426}
427
428/// Computes axial deformation of a prismatic bar under linear elastic loading.
429///
430/// Formula: `δ = FL / AE`.
431///
432/// Returns `None` when `length` is negative, `area` is less than or equal to zero,
433/// `youngs_modulus` is less than or equal to zero, or when any input or result is not finite.
434///
435/// # Examples
436///
437/// ```
438/// use use_elasticity::axial_deformation;
439///
440/// assert_eq!(axial_deformation(100.0, 10.0, 2.0, 1_000.0), Some(0.5));
441/// ```
442#[must_use]
443pub fn axial_deformation(force: f64, length: f64, area: f64, youngs_modulus: f64) -> Option<f64> {
444    if !all_finite(&[force, length, area, youngs_modulus])
445        || length < 0.0
446        || area <= 0.0
447        || youngs_modulus <= 0.0
448    {
449        return None;
450    }
451
452    finite(force * length / (area * youngs_modulus))
453}
454
455/// Computes axial stiffness of a uniform elastic bar.
456///
457/// Formula: `k = AE / L`.
458///
459/// Returns `None` when `area` is negative, `youngs_modulus` is negative, `length` is less than or
460/// equal to zero, or when any input or result is not finite.
461#[must_use]
462pub fn axial_stiffness(area: f64, youngs_modulus: f64, length: f64) -> Option<f64> {
463    if !all_finite(&[area, youngs_modulus, length])
464        || area < 0.0
465        || youngs_modulus < 0.0
466        || length <= 0.0
467    {
468        return None;
469    }
470
471    finite(area * youngs_modulus / length)
472}
473
474/// Computes force from axial deformation of a uniform elastic bar.
475///
476/// Formula: `F = δAE / L`.
477///
478/// Returns `None` when `length` is less than or equal to zero, `area` is negative,
479/// `youngs_modulus` is negative, or when any input or result is not finite.
480#[must_use]
481pub fn force_from_axial_deformation(
482    deformation: f64,
483    length: f64,
484    area: f64,
485    youngs_modulus: f64,
486) -> Option<f64> {
487    if !all_finite(&[deformation, length, area, youngs_modulus])
488        || length <= 0.0
489        || area < 0.0
490        || youngs_modulus < 0.0
491    {
492        return None;
493    }
494
495    finite(deformation * area * youngs_modulus / length)
496}
497
498/// Computes elastic strain-energy density.
499///
500/// Formula: `u = 0.5 * σ * ε`.
501///
502/// Returns `None` when the result is negative or when any input or result is not finite.
503///
504/// # Examples
505///
506/// ```
507/// use use_elasticity::elastic_energy_density;
508///
509/// assert_eq!(elastic_energy_density(100.0, 0.01), Some(0.5));
510/// ```
511#[must_use]
512pub fn elastic_energy_density(stress: f64, strain: f64) -> Option<f64> {
513    if !all_finite(&[stress, strain]) {
514        return None;
515    }
516
517    let result = 0.5 * stress * strain;
518    if result < 0.0 {
519        return None;
520    }
521
522    finite(result)
523}
524
525/// Computes elastic energy stored in a linear spring from stiffness and deformation.
526///
527/// Formula: `U = 0.5 * k * x²`.
528///
529/// Returns `None` when `spring_constant` is negative or when any input or result is not finite.
530#[must_use]
531pub fn elastic_energy_from_spring_constant(spring_constant: f64, deformation: f64) -> Option<f64> {
532    if !all_finite(&[spring_constant, deformation]) || spring_constant < 0.0 {
533        return None;
534    }
535
536    finite(0.5 * spring_constant * deformation * deformation)
537}
538
539/// Computes elastic energy from force and deformation for a linear loading path.
540///
541/// Formula: `U = 0.5 * F * x`.
542///
543/// Returns `None` when the result is negative or when any input or result is not finite.
544#[must_use]
545pub fn elastic_energy_from_force_deformation(force: f64, deformation: f64) -> Option<f64> {
546    if !all_finite(&[force, deformation]) {
547        return None;
548    }
549
550    let result = 0.5 * force * deformation;
551    if result < 0.0 {
552        return None;
553    }
554
555    finite(result)
556}
557
558/// Simple elastic material parameters for linear isotropic summaries.
559#[derive(Debug, Clone, Copy, PartialEq)]
560pub struct ElasticMaterial {
561    /// Young's modulus in pascals.
562    pub youngs_modulus: f64,
563    /// Poisson's ratio when known.
564    pub poisson_ratio: Option<f64>,
565}
566
567impl ElasticMaterial {
568    /// Creates a material summary with Young's modulus only.
569    #[must_use]
570    pub fn new(youngs_modulus: f64) -> Option<Self> {
571        if !youngs_modulus.is_finite() || youngs_modulus < 0.0 {
572            return None;
573        }
574
575        Some(Self {
576            youngs_modulus,
577            poisson_ratio: None,
578        })
579    }
580
581    /// Creates a material summary with Young's modulus and Poisson's ratio.
582    #[must_use]
583    pub fn with_poisson_ratio(youngs_modulus: f64, poisson_ratio: f64) -> Option<Self> {
584        if !poisson_ratio.is_finite() {
585            return None;
586        }
587
588        Self::new(youngs_modulus).map(|material| Self {
589            poisson_ratio: Some(poisson_ratio),
590            ..material
591        })
592    }
593
594    /// Computes stress from strain using this material's Young's modulus.
595    ///
596    /// # Examples
597    ///
598    /// ```
599    /// use use_elasticity::ElasticMaterial;
600    ///
601    /// let Some(material) = ElasticMaterial::new(200.0) else {
602    ///     unreachable!();
603    /// };
604    ///
605    /// assert_eq!(material.stress_from_strain(0.01), Some(2.0));
606    /// ```
607    #[must_use]
608    pub fn stress_from_strain(&self, strain: f64) -> Option<f64> {
609        stress_from_youngs_modulus(self.youngs_modulus, strain)
610    }
611
612    /// Computes strain from stress using this material's Young's modulus.
613    #[must_use]
614    pub fn strain_from_stress(&self, stress: f64) -> Option<f64> {
615        strain_from_youngs_modulus(stress, self.youngs_modulus)
616    }
617
618    /// Computes shear modulus when Poisson's ratio is available.
619    #[must_use]
620    pub fn shear_modulus(&self) -> Option<f64> {
621        self.poisson_ratio
622            .and_then(|ratio| shear_modulus_from_youngs_and_poisson(self.youngs_modulus, ratio))
623    }
624
625    /// Computes bulk modulus when Poisson's ratio is available.
626    #[must_use]
627    pub fn bulk_modulus(&self) -> Option<f64> {
628        self.poisson_ratio
629            .and_then(|ratio| bulk_modulus_from_youngs_and_poisson(self.youngs_modulus, ratio))
630    }
631}
632
633/// Simple uniform elastic bar properties for axial loading summaries.
634#[derive(Debug, Clone, Copy, PartialEq)]
635pub struct ElasticBar {
636    /// Original bar length in meters.
637    pub length: f64,
638    /// Cross-sectional area in square meters.
639    pub area: f64,
640    /// Young's modulus in pascals.
641    pub youngs_modulus: f64,
642}
643
644impl ElasticBar {
645    /// Creates a uniform elastic bar summary.
646    #[must_use]
647    pub fn new(length: f64, area: f64, youngs_modulus: f64) -> Option<Self> {
648        if !all_finite(&[length, area, youngs_modulus])
649            || length <= 0.0
650            || area <= 0.0
651            || youngs_modulus <= 0.0
652        {
653            return None;
654        }
655
656        Some(Self {
657            length,
658            area,
659            youngs_modulus,
660        })
661    }
662
663    /// Computes axial stiffness for this bar.
664    #[must_use]
665    pub fn axial_stiffness(&self) -> Option<f64> {
666        axial_stiffness(self.area, self.youngs_modulus, self.length)
667    }
668
669    /// Computes axial deformation under the given force.
670    ///
671    /// # Examples
672    ///
673    /// ```
674    /// use use_elasticity::ElasticBar;
675    ///
676    /// let Some(bar) = ElasticBar::new(10.0, 2.0, 1_000.0) else {
677    ///     unreachable!();
678    /// };
679    ///
680    /// assert_eq!(bar.deformation_under_force(100.0), Some(0.5));
681    /// ```
682    #[must_use]
683    pub fn deformation_under_force(&self, force: f64) -> Option<f64> {
684        axial_deformation(force, self.length, self.area, self.youngs_modulus)
685    }
686
687    /// Computes force required to cause the given deformation.
688    #[must_use]
689    pub fn force_for_deformation(&self, deformation: f64) -> Option<f64> {
690        force_from_axial_deformation(deformation, self.length, self.area, self.youngs_modulus)
691    }
692
693    /// Computes normal stress under the given force.
694    #[must_use]
695    pub fn stress_under_force(&self, force: f64) -> Option<f64> {
696        normal_stress(force, self.area)
697    }
698
699    /// Computes normal strain under the given force using Young's modulus.
700    #[must_use]
701    pub fn strain_under_force(&self, force: f64) -> Option<f64> {
702        self.stress_under_force(force)
703            .and_then(|stress| strain_from_youngs_modulus(stress, self.youngs_modulus))
704    }
705}
706
707#[cfg(test)]
708mod tests {
709    use super::{
710        ElasticBar, ElasticMaterial, axial_deformation, axial_stiffness, bulk_modulus,
711        bulk_modulus_from_youngs_and_poisson, change_in_length, change_in_volume,
712        elastic_energy_density, elastic_energy_from_force_deformation,
713        elastic_energy_from_spring_constant, final_length, force_from_axial_deformation,
714        force_from_stress, is_common_poisson_ratio, normal_strain, normal_stress, poisson_ratio,
715        pressure_change_from_bulk_modulus, shear_modulus, shear_modulus_from_youngs_and_poisson,
716        shear_strain, shear_strain_from_modulus, shear_stress, shear_stress_from_modulus,
717        strain_from_youngs_modulus, stress_from_youngs_modulus,
718        transverse_strain_from_poisson_ratio, volume_strain, youngs_modulus,
719        youngs_modulus_from_shear_and_poisson,
720    };
721
722    fn assert_option_approx_eq(actual: Option<f64>, expected: f64) {
723        let Some(actual) = actual else {
724            panic!("expected Some({expected}), got None");
725        };
726
727        assert!(
728            (actual - expected).abs() < 1.0e-12,
729            "expected {expected}, got {actual}"
730        );
731    }
732
733    #[test]
734    fn stress_helpers_cover_expected_cases() {
735        assert_eq!(normal_stress(100.0, 2.0), Some(50.0));
736        assert_eq!(normal_stress(100.0, 0.0), None);
737
738        assert_eq!(shear_stress(100.0, 2.0), Some(50.0));
739        assert_eq!(shear_stress(100.0, 0.0), None);
740
741        assert_eq!(force_from_stress(50.0, 2.0), Some(100.0));
742        assert_eq!(force_from_stress(50.0, -2.0), None);
743    }
744
745    #[test]
746    fn strain_helpers_cover_expected_cases() {
747        assert_option_approx_eq(normal_strain(2.0, 10.0), 0.2);
748        assert_option_approx_eq(normal_strain(-2.0, 10.0), -0.2);
749        assert_eq!(normal_strain(2.0, 0.0), None);
750
751        assert_option_approx_eq(shear_strain(2.0, 10.0), 0.2);
752        assert_eq!(shear_strain(2.0, 0.0), None);
753
754        assert_option_approx_eq(change_in_length(0.2, 10.0), 2.0);
755        assert_option_approx_eq(final_length(10.0, 0.2), 12.0);
756        assert_eq!(final_length(10.0, -1.2), None);
757    }
758
759    #[test]
760    fn youngs_modulus_helpers_cover_expected_cases() {
761        assert_option_approx_eq(youngs_modulus(100.0, 0.01), 10_000.0);
762        assert_eq!(youngs_modulus(100.0, 0.0), None);
763        assert_eq!(youngs_modulus(-100.0, 0.01), None);
764
765        assert_option_approx_eq(stress_from_youngs_modulus(10_000.0, 0.01), 100.0);
766        assert_option_approx_eq(strain_from_youngs_modulus(100.0, 10_000.0), 0.01);
767    }
768
769    #[test]
770    fn shear_helpers_cover_expected_cases() {
771        assert_option_approx_eq(shear_modulus(50.0, 0.01), 5_000.0);
772        assert_eq!(shear_modulus(50.0, 0.0), None);
773
774        assert_option_approx_eq(shear_stress_from_modulus(5_000.0, 0.01), 50.0);
775        assert_option_approx_eq(shear_strain_from_modulus(50.0, 5_000.0), 0.01);
776    }
777
778    #[test]
779    fn bulk_helpers_cover_expected_cases() {
780        assert_option_approx_eq(volume_strain(-2.0, 10.0), -0.2);
781        assert_eq!(volume_strain(-2.0, 0.0), None);
782
783        assert_option_approx_eq(bulk_modulus(100.0, -0.01), 10_000.0);
784        assert_eq!(bulk_modulus(100.0, 0.01), None);
785
786        assert_option_approx_eq(pressure_change_from_bulk_modulus(10_000.0, -0.01), 100.0);
787        assert_option_approx_eq(change_in_volume(-0.2, 10.0), -2.0);
788    }
789
790    #[test]
791    fn poisson_helpers_cover_expected_cases() {
792        assert_option_approx_eq(poisson_ratio(-0.003, 0.01), 0.3);
793        assert_eq!(poisson_ratio(-0.003, 0.0), None);
794
795        assert_option_approx_eq(transverse_strain_from_poisson_ratio(0.3, 0.01), -0.003);
796        assert!(is_common_poisson_ratio(0.3));
797        assert!(!is_common_poisson_ratio(-0.1));
798        assert!(!is_common_poisson_ratio(0.6));
799    }
800
801    #[test]
802    fn modulus_relationships_cover_expected_cases() {
803        assert_option_approx_eq(shear_modulus_from_youngs_and_poisson(260.0, 0.3), 100.0);
804        assert_option_approx_eq(bulk_modulus_from_youngs_and_poisson(300.0, 0.25), 200.0);
805        assert_option_approx_eq(youngs_modulus_from_shear_and_poisson(100.0, 0.3), 260.0);
806    }
807
808    #[test]
809    fn axial_helpers_cover_expected_cases() {
810        assert_option_approx_eq(axial_deformation(100.0, 10.0, 2.0, 1_000.0), 0.5);
811        assert_eq!(axial_deformation(100.0, 10.0, 0.0, 1_000.0), None);
812
813        assert_option_approx_eq(axial_stiffness(2.0, 1_000.0, 10.0), 200.0);
814        assert_eq!(axial_stiffness(2.0, 1_000.0, 0.0), None);
815
816        assert_option_approx_eq(force_from_axial_deformation(0.5, 10.0, 2.0, 1_000.0), 100.0);
817    }
818
819    #[test]
820    fn elastic_energy_helpers_cover_expected_cases() {
821        assert_option_approx_eq(elastic_energy_density(100.0, 0.01), 0.5);
822        assert_eq!(elastic_energy_density(-100.0, 0.01), None);
823
824        assert_option_approx_eq(elastic_energy_from_spring_constant(100.0, 0.5), 12.5);
825        assert_eq!(elastic_energy_from_spring_constant(-100.0, 0.5), None);
826
827        assert_option_approx_eq(elastic_energy_from_force_deformation(100.0, 0.5), 25.0);
828        assert_eq!(elastic_energy_from_force_deformation(-100.0, 0.5), None);
829    }
830
831    #[test]
832    fn elastic_material_methods_cover_expected_cases() {
833        let Some(material) = ElasticMaterial::with_poisson_ratio(260.0, 0.3) else {
834            panic!("expected valid ElasticMaterial");
835        };
836
837        assert_option_approx_eq(material.stress_from_strain(0.01), 2.6);
838        assert_option_approx_eq(material.strain_from_stress(2.6), 0.01);
839        assert_option_approx_eq(material.shear_modulus(), 100.0);
840
841        assert_eq!(ElasticMaterial::new(-1.0), None);
842        assert_eq!(ElasticMaterial::with_poisson_ratio(260.0, f64::NAN), None);
843    }
844
845    #[test]
846    fn elastic_bar_methods_cover_expected_cases() {
847        let Some(bar) = ElasticBar::new(10.0, 2.0, 1_000.0) else {
848            panic!("expected valid ElasticBar");
849        };
850
851        assert_option_approx_eq(bar.axial_stiffness(), 200.0);
852        assert_option_approx_eq(bar.deformation_under_force(100.0), 0.5);
853        assert_option_approx_eq(bar.force_for_deformation(0.5), 100.0);
854        assert_option_approx_eq(bar.stress_under_force(100.0), 50.0);
855        assert_option_approx_eq(bar.strain_under_force(100.0), 0.05);
856
857        assert_eq!(ElasticBar::new(0.0, 2.0, 1_000.0), None);
858        assert_eq!(ElasticBar::new(10.0, 0.0, 1_000.0), None);
859        assert_eq!(ElasticBar::new(10.0, 2.0, 0.0), None);
860    }
861}