#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct ShapeMemoryAlloy {
pub ms: f64,
pub mf: f64,
pub as_: f64,
pub af: f64,
pub e_austenite: f64,
pub e_martensite: f64,
pub h_max: f64,
}
impl ShapeMemoryAlloy {
pub fn new(
ms: f64,
mf: f64,
as_: f64,
af: f64,
e_austenite: f64,
e_martensite: f64,
h_max: f64,
) -> Self {
Self {
ms,
mf,
as_,
af,
e_austenite,
e_martensite,
h_max,
}
}
pub fn new_nitinol() -> Self {
Self::new(
291.0, 273.0, 307.0, 325.0, 75.0e9, 28.0e9, 0.08, )
}
pub fn nitinol() -> Self {
Self::new_nitinol()
}
pub fn phase_fraction(&self, temp: f64, stress: f64) -> f64 {
let cc_slope = 10.0e6; let dt = stress / cc_slope;
let ms = self.ms + dt;
let mf = self.mf + dt;
let as_ = self.as_ + dt;
let af = self.af + dt;
if temp <= mf {
1.0_f64
} else if temp <= ms {
0.5 * (PI * (temp - mf) / (ms - mf)).cos() + 0.5
} else if temp < as_ {
1.0_f64
} else if temp <= af {
1.0 - (0.5 * (PI * (temp - as_) / (af - as_)).cos() + 0.5)
} else {
0.0_f64
}
}
pub fn elastic_modulus(&self, xi: f64) -> f64 {
self.e_austenite + xi.clamp(0.0, 1.0) * (self.e_martensite - self.e_austenite)
}
pub fn constitutive_response(&self, strain: f64, temp: f64) -> f64 {
let xi = self.phase_fraction(temp, 0.0);
let e = self.elastic_modulus(xi);
e * strain - e * self.h_max * xi
}
pub fn recovery_strain(&self, xi_start: f64, xi_end: f64) -> f64 {
self.h_max * (xi_start - xi_end)
}
pub fn critical_stress(&self, temp: f64) -> f64 {
let cm = 10.0e6; if temp > self.ms {
cm * (temp - self.ms)
} else {
0.0
}
}
}
#[derive(Debug, Clone)]
pub struct BraisbirdAuricchioModel {
pub ca: f64,
pub cm: f64,
pub t0: f64,
pub sigma0: f64,
}
impl BraisbirdAuricchioModel {
pub fn new(ca: f64, cm: f64, t0: f64, sigma0: f64) -> Self {
Self { ca, cm, t0, sigma0 }
}
pub fn nitinol() -> Self {
Self::new(13.0e6, 8.0e6, 291.0, 100.0e6)
}
pub fn forward_transformation_stress(&self) -> f64 {
self.sigma0
}
pub fn reverse_transformation_stress(&self) -> f64 {
if self.ca.abs() < 1e-15 {
return 0.0;
}
self.sigma0 * (self.cm / self.ca)
}
}
#[derive(Debug, Clone)]
pub struct ThermoelasticMartensite {
pub epsilon_l: f64,
pub delta_s: f64,
}
impl ThermoelasticMartensite {
pub fn new(epsilon_l: f64, delta_s: f64) -> Self {
Self { epsilon_l, delta_s }
}
pub fn nitinol() -> Self {
Self::new(0.08, -2.2e5)
}
pub fn clausius_clapeyron(&self) -> f64 {
if self.epsilon_l.abs() < 1e-15 {
return 0.0;
}
-self.delta_s / self.epsilon_l
}
}
#[derive(Debug, Clone)]
pub struct SMAPseudoelasticity {
pub sigma_f: f64,
pub sigma_r: f64,
pub epsilon_l: f64,
pub e_a: f64,
}
impl SMAPseudoelasticity {
pub fn new(sigma_f: f64, sigma_r: f64, epsilon_l: f64, e_a: f64) -> Self {
Self {
sigma_f,
sigma_r,
epsilon_l,
e_a,
}
}
pub fn nitinol_room_temp() -> Self {
Self::new(500.0e6, 200.0e6, 0.06, 75.0e9)
}
pub fn loading_plateau_stress(&self) -> f64 {
self.sigma_f
}
pub fn unloading_plateau_stress(&self) -> f64 {
self.sigma_r
}
pub fn hysteresis_area(&self) -> f64 {
(self.sigma_f - self.sigma_r) * self.epsilon_l
}
pub fn loading_response(&self, eps: f64) -> f64 {
let eps_onset = self.sigma_f / self.e_a;
if eps <= eps_onset {
self.e_a * eps
} else {
self.sigma_f
}
}
pub fn unloading_response(&self, eps: f64) -> f64 {
let eps_end = self.epsilon_l;
let eps_reverse_end = (self.sigma_f - self.sigma_r) / self.e_a;
let eps_elastic_end = eps_end - eps_reverse_end;
if eps >= eps_elastic_end {
self.sigma_f - self.e_a * (eps_end - eps)
} else {
self.sigma_r.max(0.0)
}
}
}
#[derive(Debug, Clone)]
pub struct TwoWaySMA {
pub training_cycles: usize,
pub max_trained_strain: f64,
}
impl TwoWaySMA {
pub fn new(training_cycles: usize, max_trained_strain: f64) -> Self {
Self {
training_cycles,
max_trained_strain,
}
}
pub fn nitinol() -> Self {
Self::new(0, 0.04) }
pub fn train(&mut self, cycles: usize) {
self.training_cycles += cycles;
}
pub fn trained_strain(&self) -> f64 {
let n = self.training_cycles as f64;
self.max_trained_strain * (1.0 - (-n / 10.0).exp())
}
pub fn saturation_fraction(&self) -> f64 {
if self.max_trained_strain.abs() < 1e-15 {
return 0.0;
}
self.trained_strain() / self.max_trained_strain
}
}
pub fn nitinol_phase_diagram() -> [(f64, f64); 4] {
[
(1.0, 291.0), (2.0, 273.0), (3.0, 307.0), (4.0, 325.0), ]
}
fn cosine_interpolate(x: f64) -> f64 {
0.5 * (1.0 - (PI * x).cos())
}
pub fn thermal_conductivity(xi: f64, k_martensite: f64, k_austenite: f64) -> f64 {
let xi = xi.clamp(0.0, 1.0);
xi * k_martensite + (1.0 - xi) * k_austenite
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nitinol_temperatures() {
let sma = ShapeMemoryAlloy::new_nitinol();
assert!(sma.ms > sma.mf, "Ms must be above Mf");
assert!(sma.af > sma.as_, "Af must be above As");
assert!(sma.as_ > sma.ms, "As must be above Ms for NiTi");
}
#[test]
fn test_phase_fraction_fully_martensitic() {
let sma = ShapeMemoryAlloy::new_nitinol();
let xi = sma.phase_fraction(250.0, 0.0);
assert!(
(xi - 1.0).abs() < 1e-10,
"should be xi=1 below Mf, got {xi}"
);
}
#[test]
fn test_phase_fraction_fully_austenitic() {
let sma = ShapeMemoryAlloy::new_nitinol();
let xi = sma.phase_fraction(340.0, 0.0);
assert!(xi < 0.01, "should be xi≈0 above Af, got {xi}");
}
#[test]
fn test_phase_fraction_transition_region() {
let sma = ShapeMemoryAlloy::new_nitinol();
let xi = sma.phase_fraction(282.0, 0.0);
assert!(
xi > 0.0 && xi < 1.0,
"xi should be in (0,1) in transformation region, got {xi}"
);
}
#[test]
fn test_elastic_modulus_pure_austenite() {
let sma = ShapeMemoryAlloy::new_nitinol();
let e = sma.elastic_modulus(0.0);
assert!(
(e - sma.e_austenite).abs() < 1.0,
"pure austenite modulus wrong"
);
}
#[test]
fn test_elastic_modulus_pure_martensite() {
let sma = ShapeMemoryAlloy::new_nitinol();
let e = sma.elastic_modulus(1.0);
assert!(
(e - sma.e_martensite).abs() < 1.0,
"pure martensite modulus wrong"
);
}
#[test]
fn test_elastic_modulus_mixed() {
let sma = ShapeMemoryAlloy::new_nitinol();
let e = sma.elastic_modulus(0.5);
let expected = 0.5 * (sma.e_austenite + sma.e_martensite);
assert!((e - expected).abs() < 1.0, "mixed modulus wrong");
}
#[test]
fn test_constitutive_response_sign() {
let sma = ShapeMemoryAlloy::new_nitinol();
let sigma = sma.constitutive_response(0.01, 350.0);
assert!(
sigma > 0.0,
"positive strain should give positive stress in austenite"
);
}
#[test]
fn test_recovery_strain_positive() {
let sma = ShapeMemoryAlloy::new_nitinol();
let eps = sma.recovery_strain(1.0, 0.0);
assert!(
(eps - sma.h_max).abs() < 1e-10,
"full phase recovery should give h_max"
);
}
#[test]
fn test_recovery_strain_zero_change() {
let sma = ShapeMemoryAlloy::new_nitinol();
let eps = sma.recovery_strain(0.5, 0.5);
assert!(eps.abs() < 1e-10, "no phase change → zero recovery strain");
}
#[test]
fn test_critical_stress_above_ms() {
let sma = ShapeMemoryAlloy::new_nitinol();
let sigma = sma.critical_stress(310.0);
assert!(sigma > 0.0, "critical stress should be positive above Ms");
}
#[test]
fn test_critical_stress_below_ms() {
let sma = ShapeMemoryAlloy::new_nitinol();
let sigma = sma.critical_stress(250.0);
assert!(sigma == 0.0, "critical stress should be zero below Ms");
}
#[test]
fn test_braisbird_forward_stress() {
let m = BraisbirdAuricchioModel::nitinol();
let sf = m.forward_transformation_stress();
assert!(sf > 0.0, "forward transformation stress must be positive");
}
#[test]
fn test_braisbird_reverse_stress() {
let m = BraisbirdAuricchioModel::nitinol();
let sr = m.reverse_transformation_stress();
let sf = m.forward_transformation_stress();
assert!(sr <= sf, "reverse stress should not exceed forward stress");
}
#[test]
fn test_braisbird_nitinol_ca_cm() {
let m = BraisbirdAuricchioModel::nitinol();
assert!(m.ca > 0.0);
assert!(m.cm > 0.0);
}
#[test]
fn test_clausius_clapeyron_sign() {
let tm = ThermoelasticMartensite::nitinol();
let ds_dt = tm.clausius_clapeyron();
assert!(
ds_dt > 0.0,
"Clausius-Clapeyron slope should be positive for NiTi, got {ds_dt}"
);
}
#[test]
fn test_clausius_clapeyron_magnitude() {
let tm = ThermoelasticMartensite::nitinol();
let ds_dt = tm.clausius_clapeyron();
assert!(
ds_dt > 1.0e6 && ds_dt < 50.0e6,
"CC slope should be in the MPa/K range, got {ds_dt}"
);
}
#[test]
fn test_clausius_clapeyron_zero_strain() {
let tm = ThermoelasticMartensite::new(0.0, -2.2e5);
assert_eq!(tm.clausius_clapeyron(), 0.0);
}
#[test]
fn test_pseudoelastic_loading_plateau() {
let pe = SMAPseudoelasticity::nitinol_room_temp();
assert!((pe.loading_plateau_stress() - 500.0e6).abs() < 1.0);
}
#[test]
fn test_pseudoelastic_unloading_plateau() {
let pe = SMAPseudoelasticity::nitinol_room_temp();
assert!((pe.unloading_plateau_stress() - 200.0e6).abs() < 1.0);
}
#[test]
fn test_pseudoelastic_hysteresis_positive() {
let pe = SMAPseudoelasticity::nitinol_room_temp();
let area = pe.hysteresis_area();
assert!(area > 0.0, "hysteresis area should be positive");
}
#[test]
fn test_pseudoelastic_hysteresis_magnitude() {
let pe = SMAPseudoelasticity::nitinol_room_temp();
let expected = (500.0e6 - 200.0e6) * 0.06;
assert!((pe.hysteresis_area() - expected).abs() < 1.0);
}
#[test]
fn test_pseudoelastic_loading_elastic() {
let pe = SMAPseudoelasticity::nitinol_room_temp();
let eps = 0.001;
let sigma = pe.loading_response(eps);
let expected = pe.e_a * eps;
assert!((sigma - expected).abs() / expected < 1e-9);
}
#[test]
fn test_pseudoelastic_loading_plateau_region() {
let pe = SMAPseudoelasticity::nitinol_room_temp();
let eps = 0.05;
let sigma = pe.loading_response(eps);
assert!((sigma - pe.sigma_f).abs() < 1.0);
}
#[test]
fn test_twsma_zero_cycles() {
let sma = TwoWaySMA::new(0, 0.04);
assert!(sma.trained_strain() < 1e-10, "no training → zero strain");
}
#[test]
fn test_twsma_increases_with_cycles() {
let sma1 = TwoWaySMA::new(10, 0.04);
let sma2 = TwoWaySMA::new(50, 0.04);
assert!(
sma2.trained_strain() > sma1.trained_strain(),
"more training cycles should give more strain"
);
}
#[test]
fn test_twsma_saturates() {
let sma = TwoWaySMA::new(1000, 0.04);
assert!(
sma.saturation_fraction() > 0.99,
"should saturate near 1.0 after many cycles"
);
}
#[test]
fn test_twsma_train_method() {
let mut sma = TwoWaySMA::new(0, 0.04);
sma.train(20);
assert_eq!(sma.training_cycles, 20);
let eps = sma.trained_strain();
assert!(eps > 0.0);
}
#[test]
fn test_nitinol_phase_diagram_temps() {
let pd = nitinol_phase_diagram();
let mf = pd[1].1;
let ms = pd[0].1;
let as_ = pd[2].1;
let af = pd[3].1;
assert!(mf < ms, "Mf should be below Ms");
assert!(ms < as_, "Ms should be below As");
assert!(as_ < af, "As should be below Af");
}
#[test]
fn test_nitinol_phase_diagram_length() {
assert_eq!(nitinol_phase_diagram().len(), 4);
}
#[test]
fn test_cosine_interpolate_bounds() {
assert!((cosine_interpolate(0.0) - 0.0).abs() < 1e-10);
assert!((cosine_interpolate(1.0) - 1.0).abs() < 1e-10);
assert!((cosine_interpolate(0.5) - 0.5).abs() < 1e-10);
}
#[test]
fn test_thermal_conductivity_pure_phases() {
let k = thermal_conductivity(0.0, 10.0, 20.0);
assert!((k - 20.0).abs() < 1e-10, "pure austenite should give k_A");
let k = thermal_conductivity(1.0, 10.0, 20.0);
assert!((k - 10.0).abs() < 1e-10, "pure martensite should give k_M");
}
#[test]
fn test_thermal_conductivity_mixed() {
let k = thermal_conductivity(0.5, 10.0, 20.0);
assert!((k - 15.0).abs() < 1e-10, "mixed should give average");
}
#[test]
fn test_thermal_conductivity_clamps() {
let k = thermal_conductivity(2.0, 10.0, 20.0);
assert!((k - 10.0).abs() < 1e-10);
let k2 = thermal_conductivity(-1.0, 10.0, 20.0);
assert!((k2 - 20.0).abs() < 1e-10);
}
}