#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
use std::f64::consts::PI;
fn normalise3(v: [f64; 3]) -> [f64; 3] {
let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if n < 1e-14 {
[0.0; 3]
} else {
[v[0] / n, v[1] / n, v[2] / n]
}
}
fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn double_contract(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> f64 {
let mut s = 0.0;
for i in 0..3 {
for j in 0..3 {
s += a[i][j] * b[i][j];
}
}
s
}
fn matmul3(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
let mut c = [[0.0f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
c[i][j] += a[i][k] * b[k][j];
}
}
}
c
}
fn transpose3(m: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
let mut t = [[0.0f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
t[i][j] = m[j][i];
}
}
t
}
#[derive(Debug, Clone)]
pub struct SlipSystem {
pub slip_direction: [f64; 3],
pub slip_normal: [f64; 3],
pub schmid_factor: f64,
}
impl SlipSystem {
pub fn new(direction: [f64; 3], normal: [f64; 3]) -> Self {
let d = normalise3(direction);
let n = normalise3(normal);
let cos_phi = n[0]; let cos_lam = d[0]; let schmid_factor = cos_phi * cos_lam;
Self {
slip_direction: d,
slip_normal: n,
schmid_factor,
}
}
pub fn schmid_tensor(&self) -> [[f64; 3]; 3] {
let d = self.slip_direction;
let n = self.slip_normal;
let mut p = [[0.0f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
p[i][j] = 0.5 * (d[i] * n[j] + n[i] * d[j]);
}
}
p
}
pub fn resolved_shear_stress(&self, cauchy_stress: &[[f64; 3]; 3]) -> f64 {
let p = self.schmid_tensor();
double_contract(cauchy_stress, &p)
}
}
pub fn fcc_slip_systems() -> Vec<SlipSystem> {
let normals: [[f64; 3]; 4] = [
[1.0, 1.0, 1.0],
[-1.0, 1.0, 1.0],
[1.0, -1.0, 1.0],
[1.0, 1.0, -1.0],
];
let directions: [[f64; 3]; 6] = [
[1.0, -1.0, 0.0],
[-1.0, 0.0, 1.0],
[0.0, 1.0, -1.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 1.0],
[1.0, 0.0, 1.0],
];
let pairs: [(usize, usize); 12] = [
(0, 0),
(0, 1),
(0, 2),
(1, 0),
(1, 3),
(1, 4),
(2, 1),
(2, 3),
(2, 5),
(3, 2),
(3, 4),
(3, 5),
];
pairs
.iter()
.map(|&(ni, di)| SlipSystem::new(directions[di], normals[ni]))
.collect()
}
pub fn bcc_slip_systems() -> Vec<SlipSystem> {
let normals: [[f64; 3]; 6] = [
[1.0, 1.0, 0.0],
[1.0, -1.0, 0.0],
[1.0, 0.0, 1.0],
[1.0, 0.0, -1.0],
[0.0, 1.0, 1.0],
[0.0, 1.0, -1.0],
];
let directions: [[f64; 3]; 4] = [
[1.0, 1.0, 1.0],
[-1.0, 1.0, 1.0],
[1.0, -1.0, 1.0],
[1.0, 1.0, -1.0],
];
let pairs: [(usize, usize); 12] = [
(0, 2),
(0, 3),
(1, 0),
(1, 1),
(2, 0),
(2, 1),
(3, 2),
(3, 3),
(4, 0),
(4, 3),
(5, 1),
(5, 2),
];
pairs
.iter()
.map(|&(ni, di)| SlipSystem::new(directions[di], normals[ni]))
.collect()
}
#[derive(Debug, Clone)]
pub struct CrystalOrientationMatrix {
pub r: [[f64; 3]; 3],
}
impl CrystalOrientationMatrix {
pub fn from_euler_angles(phi1: f64, phi: f64, phi2: f64) -> Self {
let (c1, s1) = (phi1.cos(), phi1.sin());
let (c, s) = (phi.cos(), phi.sin());
let (c2, s2) = (phi2.cos(), phi2.sin());
let r = [
[c2 * c1 - s2 * c * s1, -c2 * s1 - s2 * c * c1, s2 * s],
[s2 * c1 + c2 * c * s1, -s2 * s1 + c2 * c * c1, -c2 * s],
[s * s1, s * c1, c],
];
Self { r }
}
pub fn euler_angles(&self) -> (f64, f64, f64) {
let r = &self.r;
let phi = r[2][2].clamp(-1.0, 1.0).acos();
let (phi1, phi2);
if phi.sin().abs() < 1e-10 {
phi1 = r[0][1].atan2(r[0][0]);
phi2 = 0.0;
} else {
phi1 = r[2][0].atan2(r[2][1]);
phi2 = r[0][2].atan2(-r[1][2]);
}
(phi1.rem_euclid(2.0 * PI), phi, phi2.rem_euclid(2.0 * PI))
}
pub fn rotate_stress(&self, sigma: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
let rt = transpose3(&self.r);
let tmp = matmul3(&self.r, sigma);
matmul3(&tmp, &rt)
}
}
pub fn taylor_factor(slip_systems: &[SlipSystem], strain_rate: [f64; 6]) -> f64 {
let e11 = strain_rate[0];
if e11.abs() < 1e-15 {
return 0.0;
}
let sigma = [
[strain_rate[0], strain_rate[5], strain_rate[4]],
[strain_rate[5], strain_rate[1], strain_rate[3]],
[strain_rate[4], strain_rate[3], strain_rate[2]],
];
let total: f64 = slip_systems
.iter()
.map(|s| s.resolved_shear_stress(&sigma).abs())
.sum();
total / e11.abs()
}
pub fn hardening_matrix_latent(n_systems: usize, q: f64) -> Vec<Vec<f64>> {
let mut h = vec![vec![q; n_systems]; n_systems];
#[allow(clippy::needless_range_loop)]
for i in 0..n_systems {
h[i][i] = 1.0;
}
h
}
#[derive(Debug, Clone)]
pub struct PowerLawCreep {
pub a: f64,
pub n: f64,
pub q_activation: f64,
pub r_gas: f64,
}
impl PowerLawCreep {
pub fn new(a: f64, n: f64, q_activation: f64, r_gas: f64) -> Self {
Self {
a,
n,
q_activation,
r_gas,
}
}
pub fn strain_rate(&self, stress: f64, temperature: f64) -> f64 {
let arrhenius = (-self.q_activation / (self.r_gas * temperature)).exp();
self.a * stress.powf(self.n) * arrhenius
}
pub fn creep_exponent(&self) -> f64 {
self.n
}
}
pub fn voigt_average(elastic_constants: &[f64]) -> f64 {
if elastic_constants.is_empty() {
return 0.0;
}
elastic_constants.iter().sum::<f64>() / elastic_constants.len() as f64
}
pub fn reuss_average(elastic_constants: &[f64]) -> f64 {
if elastic_constants.is_empty() {
return 0.0;
}
let n = elastic_constants.len() as f64;
let sum_inv: f64 = elastic_constants.iter().map(|c| 1.0 / c).sum();
n / sum_inv
}
pub fn hill_average(voigt: f64, reuss: f64) -> f64 {
(voigt + reuss) / 2.0
}
#[derive(Debug, Clone)]
pub struct TextureEvolution {
pub orientations: Vec<CrystalOrientationMatrix>,
pub weights: Vec<f64>,
}
impl TextureEvolution {
pub fn new() -> Self {
Self {
orientations: Vec::new(),
weights: Vec::new(),
}
}
pub fn add_orientation(&mut self, ori: CrystalOrientationMatrix, weight: f64) {
self.orientations.push(ori);
self.weights.push(weight);
}
pub fn count(&self) -> usize {
self.orientations.len()
}
pub fn average_taylor_factor(&self, slip_systems: &[SlipSystem], strain: [f64; 6]) -> f64 {
if self.orientations.is_empty() {
return 0.0;
}
let total_weight: f64 = self.weights.iter().sum();
if total_weight < 1e-15 {
return 0.0;
}
let weighted_sum: f64 = self
.orientations
.iter()
.zip(self.weights.iter())
.map(|(ori, &w)| {
let rotated: Vec<SlipSystem> = slip_systems
.iter()
.map(|s| {
let rd = mat_vec3(&ori.r, s.slip_direction);
let rn = mat_vec3(&ori.r, s.slip_normal);
SlipSystem::new(rd, rn)
})
.collect();
w * taylor_factor(&rotated, strain)
})
.sum();
weighted_sum / total_weight
}
}
impl Default for TextureEvolution {
fn default() -> Self {
Self::new()
}
}
fn mat_vec3(m: &[[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
[dot3(m[0], v), dot3(m[1], v), dot3(m[2], v)]
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
#[test]
fn test_fcc_has_12_slip_systems() {
let sys = fcc_slip_systems();
assert_eq!(sys.len(), 12, "FCC should have 12 slip systems");
}
#[test]
fn test_fcc_slip_directions_are_unit_vectors() {
for s in fcc_slip_systems() {
let d = s.slip_direction;
let norm = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
assert!((norm - 1.0).abs() < 1e-10, "slip direction norm={norm}");
}
}
#[test]
fn test_fcc_slip_normals_are_unit_vectors() {
for s in fcc_slip_systems() {
let n = s.slip_normal;
let norm = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
assert!((norm - 1.0).abs() < 1e-10, "slip normal norm={norm}");
}
}
#[test]
fn test_fcc_schmid_factor_in_range() {
for s in fcc_slip_systems() {
assert!(
s.schmid_factor >= -0.5 - EPS && s.schmid_factor <= 0.5 + EPS,
"Schmid factor {} out of [-0.5, 0.5]",
s.schmid_factor
);
}
}
#[test]
fn test_bcc_has_12_slip_systems() {
let sys = bcc_slip_systems();
assert_eq!(sys.len(), 12, "BCC should have 12 slip systems");
}
#[test]
fn test_bcc_slip_directions_are_unit_vectors() {
for s in bcc_slip_systems() {
let d = s.slip_direction;
let norm = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
assert!((norm - 1.0).abs() < 1e-10, "BCC slip direction norm={norm}");
}
}
#[test]
fn test_bcc_schmid_factor_in_range() {
for s in bcc_slip_systems() {
assert!(
s.schmid_factor >= -0.5 - EPS && s.schmid_factor <= 0.5 + EPS,
"BCC Schmid factor {} out of [-0.5, 0.5]",
s.schmid_factor
);
}
}
#[test]
#[allow(clippy::needless_range_loop)]
fn test_schmid_tensor_symmetric() {
let s = SlipSystem::new([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
let p = s.schmid_tensor();
for i in 0..3 {
for j in 0..3 {
assert!(
(p[i][j] - p[j][i]).abs() < EPS,
"P not symmetric at ({i},{j})"
);
}
}
}
#[test]
fn test_resolved_shear_stress_uniaxial() {
let s = SlipSystem::new([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
let sigma = [[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
let tau = s.resolved_shear_stress(&sigma);
assert!((tau - 1.0).abs() < EPS, "tau={tau}");
}
#[test]
fn test_euler_identity() {
let ori = CrystalOrientationMatrix::from_euler_angles(0.0, 0.0, 0.0);
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(ori.r[i][j] - expected).abs() < 1e-10,
"R[{i}][{j}]={}",
ori.r[i][j]
);
}
}
}
#[test]
fn test_euler_angles_roundtrip() {
let phi1 = 0.5_f64;
let phi = 0.8_f64;
let phi2 = 1.2_f64;
let ori = CrystalOrientationMatrix::from_euler_angles(phi1, phi, phi2);
let (r1, r, r2) = ori.euler_angles();
let ori2 = CrystalOrientationMatrix::from_euler_angles(r1, r, r2);
for i in 0..3 {
for j in 0..3 {
assert!(
(ori.r[i][j] - ori2.r[i][j]).abs() < 1e-8,
"Euler roundtrip mismatch at [{i}][{j}]"
);
}
}
}
#[test]
fn test_rotate_stress_identity_orientation() {
let ori = CrystalOrientationMatrix::from_euler_angles(0.0, 0.0, 0.0);
let sigma = [[1.0, 2.0, 3.0], [2.0, 4.0, 5.0], [3.0, 5.0, 6.0]];
let rotated = ori.rotate_stress(&sigma);
for i in 0..3 {
for j in 0..3 {
assert!((rotated[i][j] - sigma[i][j]).abs() < 1e-10);
}
}
}
#[test]
fn test_voigt_greater_than_or_equal_reuss() {
let constants = vec![100.0, 200.0, 150.0, 180.0];
let v = voigt_average(&constants);
let r = reuss_average(&constants);
assert!(v >= r - EPS, "Voigt={v}, Reuss={r}");
}
#[test]
fn test_hill_between_voigt_and_reuss() {
let v = 180.0_f64;
let r = 120.0_f64;
let h = hill_average(v, r);
assert!(h >= r - EPS && h <= v + EPS, "Hill={h} not in [{r},{v}]");
}
#[test]
fn test_voigt_single_value() {
let v = voigt_average(&[200.0]);
assert!((v - 200.0).abs() < EPS);
}
#[test]
fn test_reuss_single_value() {
let r = reuss_average(&[200.0]);
assert!((r - 200.0).abs() < EPS);
}
#[test]
fn test_hill_average_formula() {
let h = hill_average(180.0, 120.0);
assert!((h - 150.0).abs() < EPS);
}
#[test]
fn test_voigt_empty_returns_zero() {
assert_eq!(voigt_average(&[]), 0.0);
}
#[test]
fn test_reuss_empty_returns_zero() {
assert_eq!(reuss_average(&[]), 0.0);
}
#[test]
fn test_power_law_strain_rate_positive() {
let creep = PowerLawCreep::new(1e-15, 5.0, 200_000.0, 8.314);
let rate = creep.strain_rate(100e6, 800.0);
assert!(rate > 0.0, "strain rate should be positive, got {rate}");
}
#[test]
fn test_power_law_strain_rate_increases_with_stress() {
let creep = PowerLawCreep::new(1e-15, 5.0, 200_000.0, 8.314);
let r1 = creep.strain_rate(100e6, 800.0);
let r2 = creep.strain_rate(200e6, 800.0);
assert!(r2 > r1, "strain rate should increase with stress");
}
#[test]
fn test_power_law_strain_rate_increases_with_temperature() {
let creep = PowerLawCreep::new(1e-15, 5.0, 200_000.0, 8.314);
let r1 = creep.strain_rate(100e6, 700.0);
let r2 = creep.strain_rate(100e6, 900.0);
assert!(r2 > r1, "strain rate should increase with temperature");
}
#[test]
fn test_creep_exponent_accessor() {
let creep = PowerLawCreep::new(1e-15, 5.0, 200_000.0, 8.314);
assert!((creep.creep_exponent() - 5.0).abs() < EPS);
}
#[test]
#[allow(clippy::needless_range_loop)]
fn test_hardening_matrix_diagonal_is_one() {
let h = hardening_matrix_latent(12, 1.4);
for i in 0..12 {
assert!((h[i][i] - 1.0).abs() < EPS);
}
}
#[test]
#[allow(clippy::needless_range_loop)]
fn test_hardening_matrix_off_diagonal_is_q() {
let q = 1.4_f64;
let h = hardening_matrix_latent(12, q);
for i in 0..12 {
for j in 0..12 {
if i != j {
assert!((h[i][j] - q).abs() < EPS);
}
}
}
}
#[test]
fn test_texture_add_orientation() {
let mut tex = TextureEvolution::new();
let ori = CrystalOrientationMatrix::from_euler_angles(0.0, 0.0, 0.0);
tex.add_orientation(ori, 1.0);
assert_eq!(tex.count(), 1);
}
#[test]
fn test_texture_empty_average_taylor_zero() {
let tex = TextureEvolution::new();
let sys = fcc_slip_systems();
let m = tex.average_taylor_factor(&sys, [1.0, -0.5, -0.5, 0.0, 0.0, 0.0]);
assert_eq!(m, 0.0);
}
#[test]
fn test_texture_single_grain_taylor_factor() {
let mut tex = TextureEvolution::new();
let ori = CrystalOrientationMatrix::from_euler_angles(0.0, 0.0, 0.0);
tex.add_orientation(ori, 1.0);
let sys = fcc_slip_systems();
let m = tex.average_taylor_factor(&sys, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
assert!(m >= 0.0, "Taylor factor should be non-negative, got {m}");
}
#[test]
fn test_taylor_factor_zero_strain_returns_zero() {
let sys = fcc_slip_systems();
let m = taylor_factor(&sys, [0.0; 6]);
assert_eq!(m, 0.0);
}
#[test]
fn test_taylor_factor_positive_for_nonzero_strain() {
let sys = fcc_slip_systems();
let m = taylor_factor(&sys, [1.0, -0.5, -0.5, 0.0, 0.0, 0.0]);
assert!(m >= 0.0);
}
}