1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq)]
560pub struct ElasticMaterial {
561 pub youngs_modulus: f64,
563 pub poisson_ratio: Option<f64>,
565}
566
567impl ElasticMaterial {
568 #[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 #[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 #[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 #[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 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq)]
635pub struct ElasticBar {
636 pub length: f64,
638 pub area: f64,
640 pub youngs_modulus: f64,
642}
643
644impl ElasticBar {
645 #[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 #[must_use]
665 pub fn axial_stiffness(&self) -> Option<f64> {
666 axial_stiffness(self.area, self.youngs_modulus, self.length)
667 }
668
669 #[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 #[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 #[must_use]
695 pub fn stress_under_force(&self, force: f64) -> Option<f64> {
696 normal_stress(force, self.area)
697 }
698
699 #[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}