1#![allow(dead_code)]
10#![allow(clippy::too_many_arguments)]
11
12use std::f64::consts::PI;
13
14fn normalise3(v: [f64; 3]) -> [f64; 3] {
20 let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
21 if n < 1e-14 {
22 [0.0; 3]
23 } else {
24 [v[0] / n, v[1] / n, v[2] / n]
25 }
26}
27
28fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
30 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
31}
32
33fn double_contract(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> f64 {
35 let mut s = 0.0;
36 for i in 0..3 {
37 for j in 0..3 {
38 s += a[i][j] * b[i][j];
39 }
40 }
41 s
42}
43
44fn matmul3(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
46 let mut c = [[0.0f64; 3]; 3];
47 for i in 0..3 {
48 for j in 0..3 {
49 for k in 0..3 {
50 c[i][j] += a[i][k] * b[k][j];
51 }
52 }
53 }
54 c
55}
56
57fn transpose3(m: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
59 let mut t = [[0.0f64; 3]; 3];
60 for i in 0..3 {
61 for j in 0..3 {
62 t[i][j] = m[j][i];
63 }
64 }
65 t
66}
67
68#[derive(Debug, Clone)]
78pub struct SlipSystem {
79 pub slip_direction: [f64; 3],
81 pub slip_normal: [f64; 3],
83 pub schmid_factor: f64,
85}
86
87impl SlipSystem {
88 pub fn new(direction: [f64; 3], normal: [f64; 3]) -> Self {
93 let d = normalise3(direction);
94 let n = normalise3(normal);
95 let cos_phi = n[0]; let cos_lam = d[0]; let schmid_factor = cos_phi * cos_lam;
99 Self {
100 slip_direction: d,
101 slip_normal: n,
102 schmid_factor,
103 }
104 }
105
106 pub fn schmid_tensor(&self) -> [[f64; 3]; 3] {
108 let d = self.slip_direction;
109 let n = self.slip_normal;
110 let mut p = [[0.0f64; 3]; 3];
111 for i in 0..3 {
112 for j in 0..3 {
113 p[i][j] = 0.5 * (d[i] * n[j] + n[i] * d[j]);
114 }
115 }
116 p
117 }
118
119 pub fn resolved_shear_stress(&self, cauchy_stress: &[[f64; 3]; 3]) -> f64 {
121 let p = self.schmid_tensor();
122 double_contract(cauchy_stress, &p)
123 }
124}
125
126pub fn fcc_slip_systems() -> Vec<SlipSystem> {
132 let normals: [[f64; 3]; 4] = [
134 [1.0, 1.0, 1.0],
135 [-1.0, 1.0, 1.0],
136 [1.0, -1.0, 1.0],
137 [1.0, 1.0, -1.0],
138 ];
139 let directions: [[f64; 3]; 6] = [
141 [1.0, -1.0, 0.0],
142 [-1.0, 0.0, 1.0],
143 [0.0, 1.0, -1.0],
144 [1.0, 1.0, 0.0],
145 [0.0, 1.0, 1.0],
146 [1.0, 0.0, 1.0],
147 ];
148
149 let pairs: [(usize, usize); 12] = [
151 (0, 0),
152 (0, 1),
153 (0, 2),
154 (1, 0),
155 (1, 3),
156 (1, 4),
157 (2, 1),
158 (2, 3),
159 (2, 5),
160 (3, 2),
161 (3, 4),
162 (3, 5),
163 ];
164
165 pairs
166 .iter()
167 .map(|&(ni, di)| SlipSystem::new(directions[di], normals[ni]))
168 .collect()
169}
170
171pub fn bcc_slip_systems() -> Vec<SlipSystem> {
173 let normals: [[f64; 3]; 6] = [
175 [1.0, 1.0, 0.0],
176 [1.0, -1.0, 0.0],
177 [1.0, 0.0, 1.0],
178 [1.0, 0.0, -1.0],
179 [0.0, 1.0, 1.0],
180 [0.0, 1.0, -1.0],
181 ];
182 let directions: [[f64; 3]; 4] = [
184 [1.0, 1.0, 1.0],
185 [-1.0, 1.0, 1.0],
186 [1.0, -1.0, 1.0],
187 [1.0, 1.0, -1.0],
188 ];
189
190 let pairs: [(usize, usize); 12] = [
192 (0, 2),
193 (0, 3),
194 (1, 0),
195 (1, 1),
196 (2, 0),
197 (2, 1),
198 (3, 2),
199 (3, 3),
200 (4, 0),
201 (4, 3),
202 (5, 1),
203 (5, 2),
204 ];
205
206 pairs
207 .iter()
208 .map(|&(ni, di)| SlipSystem::new(directions[di], normals[ni]))
209 .collect()
210}
211
212#[derive(Debug, Clone)]
220pub struct CrystalOrientationMatrix {
221 pub r: [[f64; 3]; 3],
223}
224
225impl CrystalOrientationMatrix {
226 pub fn from_euler_angles(phi1: f64, phi: f64, phi2: f64) -> Self {
228 let (c1, s1) = (phi1.cos(), phi1.sin());
229 let (c, s) = (phi.cos(), phi.sin());
230 let (c2, s2) = (phi2.cos(), phi2.sin());
231
232 let r = [
234 [c2 * c1 - s2 * c * s1, -c2 * s1 - s2 * c * c1, s2 * s],
235 [s2 * c1 + c2 * c * s1, -s2 * s1 + c2 * c * c1, -c2 * s],
236 [s * s1, s * c1, c],
237 ];
238
239 Self { r }
240 }
241
242 pub fn euler_angles(&self) -> (f64, f64, f64) {
246 let r = &self.r;
247 let phi = r[2][2].clamp(-1.0, 1.0).acos();
248 let (phi1, phi2);
249 if phi.sin().abs() < 1e-10 {
250 phi1 = r[0][1].atan2(r[0][0]);
252 phi2 = 0.0;
253 } else {
254 phi1 = r[2][0].atan2(r[2][1]);
255 phi2 = r[0][2].atan2(-r[1][2]);
256 }
257 (phi1.rem_euclid(2.0 * PI), phi, phi2.rem_euclid(2.0 * PI))
258 }
259
260 pub fn rotate_stress(&self, sigma: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
264 let rt = transpose3(&self.r);
265 let tmp = matmul3(&self.r, sigma);
266 matmul3(&tmp, &rt)
267 }
268}
269
270pub fn taylor_factor(slip_systems: &[SlipSystem], strain_rate: [f64; 6]) -> f64 {
284 let e11 = strain_rate[0];
285 if e11.abs() < 1e-15 {
286 return 0.0;
287 }
288 let sigma = [
290 [strain_rate[0], strain_rate[5], strain_rate[4]],
291 [strain_rate[5], strain_rate[1], strain_rate[3]],
292 [strain_rate[4], strain_rate[3], strain_rate[2]],
293 ];
294 let total: f64 = slip_systems
295 .iter()
296 .map(|s| s.resolved_shear_stress(&sigma).abs())
297 .sum();
298 total / e11.abs()
299}
300
301pub fn hardening_matrix_latent(n_systems: usize, q: f64) -> Vec<Vec<f64>> {
311 let mut h = vec![vec![q; n_systems]; n_systems];
312 #[allow(clippy::needless_range_loop)]
313 for i in 0..n_systems {
314 h[i][i] = 1.0;
315 }
316 h
317}
318
319#[derive(Debug, Clone)]
325pub struct PowerLawCreep {
326 pub a: f64,
328 pub n: f64,
330 pub q_activation: f64,
332 pub r_gas: f64,
334}
335
336impl PowerLawCreep {
337 pub fn new(a: f64, n: f64, q_activation: f64, r_gas: f64) -> Self {
339 Self {
340 a,
341 n,
342 q_activation,
343 r_gas,
344 }
345 }
346
347 pub fn strain_rate(&self, stress: f64, temperature: f64) -> f64 {
353 let arrhenius = (-self.q_activation / (self.r_gas * temperature)).exp();
354 self.a * stress.powf(self.n) * arrhenius
355 }
356
357 pub fn creep_exponent(&self) -> f64 {
359 self.n
360 }
361}
362
363pub fn voigt_average(elastic_constants: &[f64]) -> f64 {
369 if elastic_constants.is_empty() {
370 return 0.0;
371 }
372 elastic_constants.iter().sum::<f64>() / elastic_constants.len() as f64
373}
374
375pub fn reuss_average(elastic_constants: &[f64]) -> f64 {
377 if elastic_constants.is_empty() {
378 return 0.0;
379 }
380 let n = elastic_constants.len() as f64;
381 let sum_inv: f64 = elastic_constants.iter().map(|c| 1.0 / c).sum();
382 n / sum_inv
383}
384
385pub fn hill_average(voigt: f64, reuss: f64) -> f64 {
387 (voigt + reuss) / 2.0
388}
389
390#[derive(Debug, Clone)]
396pub struct TextureEvolution {
397 pub orientations: Vec<CrystalOrientationMatrix>,
399 pub weights: Vec<f64>,
401}
402
403impl TextureEvolution {
404 pub fn new() -> Self {
406 Self {
407 orientations: Vec::new(),
408 weights: Vec::new(),
409 }
410 }
411
412 pub fn add_orientation(&mut self, ori: CrystalOrientationMatrix, weight: f64) {
414 self.orientations.push(ori);
415 self.weights.push(weight);
416 }
417
418 pub fn count(&self) -> usize {
420 self.orientations.len()
421 }
422
423 pub fn average_taylor_factor(&self, slip_systems: &[SlipSystem], strain: [f64; 6]) -> f64 {
429 if self.orientations.is_empty() {
430 return 0.0;
431 }
432 let total_weight: f64 = self.weights.iter().sum();
433 if total_weight < 1e-15 {
434 return 0.0;
435 }
436 let weighted_sum: f64 = self
437 .orientations
438 .iter()
439 .zip(self.weights.iter())
440 .map(|(ori, &w)| {
441 let rotated: Vec<SlipSystem> = slip_systems
443 .iter()
444 .map(|s| {
445 let rd = mat_vec3(&ori.r, s.slip_direction);
447 let rn = mat_vec3(&ori.r, s.slip_normal);
448 SlipSystem::new(rd, rn)
449 })
450 .collect();
451 w * taylor_factor(&rotated, strain)
452 })
453 .sum();
454 weighted_sum / total_weight
455 }
456}
457
458impl Default for TextureEvolution {
459 fn default() -> Self {
460 Self::new()
461 }
462}
463
464fn mat_vec3(m: &[[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
466 [dot3(m[0], v), dot3(m[1], v), dot3(m[2], v)]
467}
468
469#[cfg(test)]
474mod tests {
475 use super::*;
476
477 const EPS: f64 = 1e-9;
478
479 #[test]
482 fn test_fcc_has_12_slip_systems() {
483 let sys = fcc_slip_systems();
484 assert_eq!(sys.len(), 12, "FCC should have 12 slip systems");
485 }
486
487 #[test]
488 fn test_fcc_slip_directions_are_unit_vectors() {
489 for s in fcc_slip_systems() {
490 let d = s.slip_direction;
491 let norm = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
492 assert!((norm - 1.0).abs() < 1e-10, "slip direction norm={norm}");
493 }
494 }
495
496 #[test]
497 fn test_fcc_slip_normals_are_unit_vectors() {
498 for s in fcc_slip_systems() {
499 let n = s.slip_normal;
500 let norm = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
501 assert!((norm - 1.0).abs() < 1e-10, "slip normal norm={norm}");
502 }
503 }
504
505 #[test]
506 fn test_fcc_schmid_factor_in_range() {
507 for s in fcc_slip_systems() {
508 assert!(
509 s.schmid_factor >= -0.5 - EPS && s.schmid_factor <= 0.5 + EPS,
510 "Schmid factor {} out of [-0.5, 0.5]",
511 s.schmid_factor
512 );
513 }
514 }
515
516 #[test]
519 fn test_bcc_has_12_slip_systems() {
520 let sys = bcc_slip_systems();
521 assert_eq!(sys.len(), 12, "BCC should have 12 slip systems");
522 }
523
524 #[test]
525 fn test_bcc_slip_directions_are_unit_vectors() {
526 for s in bcc_slip_systems() {
527 let d = s.slip_direction;
528 let norm = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
529 assert!((norm - 1.0).abs() < 1e-10, "BCC slip direction norm={norm}");
530 }
531 }
532
533 #[test]
534 fn test_bcc_schmid_factor_in_range() {
535 for s in bcc_slip_systems() {
536 assert!(
537 s.schmid_factor >= -0.5 - EPS && s.schmid_factor <= 0.5 + EPS,
538 "BCC Schmid factor {} out of [-0.5, 0.5]",
539 s.schmid_factor
540 );
541 }
542 }
543
544 #[test]
547 #[allow(clippy::needless_range_loop)]
548 fn test_schmid_tensor_symmetric() {
549 let s = SlipSystem::new([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
550 let p = s.schmid_tensor();
551 for i in 0..3 {
552 for j in 0..3 {
553 assert!(
554 (p[i][j] - p[j][i]).abs() < EPS,
555 "P not symmetric at ({i},{j})"
556 );
557 }
558 }
559 }
560
561 #[test]
562 fn test_resolved_shear_stress_uniaxial() {
563 let s = SlipSystem::new([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
566 let sigma = [[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
567 let tau = s.resolved_shear_stress(&sigma);
568 assert!((tau - 1.0).abs() < EPS, "tau={tau}");
570 }
571
572 #[test]
575 fn test_euler_identity() {
576 let ori = CrystalOrientationMatrix::from_euler_angles(0.0, 0.0, 0.0);
578 for i in 0..3 {
579 for j in 0..3 {
580 let expected = if i == j { 1.0 } else { 0.0 };
581 assert!(
582 (ori.r[i][j] - expected).abs() < 1e-10,
583 "R[{i}][{j}]={}",
584 ori.r[i][j]
585 );
586 }
587 }
588 }
589
590 #[test]
591 fn test_euler_angles_roundtrip() {
592 let phi1 = 0.5_f64;
593 let phi = 0.8_f64;
594 let phi2 = 1.2_f64;
595 let ori = CrystalOrientationMatrix::from_euler_angles(phi1, phi, phi2);
596 let (r1, r, r2) = ori.euler_angles();
597 let ori2 = CrystalOrientationMatrix::from_euler_angles(r1, r, r2);
599 for i in 0..3 {
600 for j in 0..3 {
601 assert!(
602 (ori.r[i][j] - ori2.r[i][j]).abs() < 1e-8,
603 "Euler roundtrip mismatch at [{i}][{j}]"
604 );
605 }
606 }
607 }
608
609 #[test]
610 fn test_rotate_stress_identity_orientation() {
611 let ori = CrystalOrientationMatrix::from_euler_angles(0.0, 0.0, 0.0);
612 let sigma = [[1.0, 2.0, 3.0], [2.0, 4.0, 5.0], [3.0, 5.0, 6.0]];
613 let rotated = ori.rotate_stress(&sigma);
614 for i in 0..3 {
615 for j in 0..3 {
616 assert!((rotated[i][j] - sigma[i][j]).abs() < 1e-10);
617 }
618 }
619 }
620
621 #[test]
624 fn test_voigt_greater_than_or_equal_reuss() {
625 let constants = vec![100.0, 200.0, 150.0, 180.0];
626 let v = voigt_average(&constants);
627 let r = reuss_average(&constants);
628 assert!(v >= r - EPS, "Voigt={v}, Reuss={r}");
629 }
630
631 #[test]
632 fn test_hill_between_voigt_and_reuss() {
633 let v = 180.0_f64;
634 let r = 120.0_f64;
635 let h = hill_average(v, r);
636 assert!(h >= r - EPS && h <= v + EPS, "Hill={h} not in [{r},{v}]");
637 }
638
639 #[test]
640 fn test_voigt_single_value() {
641 let v = voigt_average(&[200.0]);
642 assert!((v - 200.0).abs() < EPS);
643 }
644
645 #[test]
646 fn test_reuss_single_value() {
647 let r = reuss_average(&[200.0]);
648 assert!((r - 200.0).abs() < EPS);
649 }
650
651 #[test]
652 fn test_hill_average_formula() {
653 let h = hill_average(180.0, 120.0);
654 assert!((h - 150.0).abs() < EPS);
655 }
656
657 #[test]
658 fn test_voigt_empty_returns_zero() {
659 assert_eq!(voigt_average(&[]), 0.0);
660 }
661
662 #[test]
663 fn test_reuss_empty_returns_zero() {
664 assert_eq!(reuss_average(&[]), 0.0);
665 }
666
667 #[test]
670 fn test_power_law_strain_rate_positive() {
671 let creep = PowerLawCreep::new(1e-15, 5.0, 200_000.0, 8.314);
672 let rate = creep.strain_rate(100e6, 800.0);
673 assert!(rate > 0.0, "strain rate should be positive, got {rate}");
674 }
675
676 #[test]
677 fn test_power_law_strain_rate_increases_with_stress() {
678 let creep = PowerLawCreep::new(1e-15, 5.0, 200_000.0, 8.314);
679 let r1 = creep.strain_rate(100e6, 800.0);
680 let r2 = creep.strain_rate(200e6, 800.0);
681 assert!(r2 > r1, "strain rate should increase with stress");
682 }
683
684 #[test]
685 fn test_power_law_strain_rate_increases_with_temperature() {
686 let creep = PowerLawCreep::new(1e-15, 5.0, 200_000.0, 8.314);
687 let r1 = creep.strain_rate(100e6, 700.0);
688 let r2 = creep.strain_rate(100e6, 900.0);
689 assert!(r2 > r1, "strain rate should increase with temperature");
690 }
691
692 #[test]
693 fn test_creep_exponent_accessor() {
694 let creep = PowerLawCreep::new(1e-15, 5.0, 200_000.0, 8.314);
695 assert!((creep.creep_exponent() - 5.0).abs() < EPS);
696 }
697
698 #[test]
701 #[allow(clippy::needless_range_loop)]
702 fn test_hardening_matrix_diagonal_is_one() {
703 let h = hardening_matrix_latent(12, 1.4);
704 for i in 0..12 {
705 assert!((h[i][i] - 1.0).abs() < EPS);
706 }
707 }
708
709 #[test]
710 #[allow(clippy::needless_range_loop)]
711 fn test_hardening_matrix_off_diagonal_is_q() {
712 let q = 1.4_f64;
713 let h = hardening_matrix_latent(12, q);
714 for i in 0..12 {
715 for j in 0..12 {
716 if i != j {
717 assert!((h[i][j] - q).abs() < EPS);
718 }
719 }
720 }
721 }
722
723 #[test]
726 fn test_texture_add_orientation() {
727 let mut tex = TextureEvolution::new();
728 let ori = CrystalOrientationMatrix::from_euler_angles(0.0, 0.0, 0.0);
729 tex.add_orientation(ori, 1.0);
730 assert_eq!(tex.count(), 1);
731 }
732
733 #[test]
734 fn test_texture_empty_average_taylor_zero() {
735 let tex = TextureEvolution::new();
736 let sys = fcc_slip_systems();
737 let m = tex.average_taylor_factor(&sys, [1.0, -0.5, -0.5, 0.0, 0.0, 0.0]);
738 assert_eq!(m, 0.0);
739 }
740
741 #[test]
742 fn test_texture_single_grain_taylor_factor() {
743 let mut tex = TextureEvolution::new();
744 let ori = CrystalOrientationMatrix::from_euler_angles(0.0, 0.0, 0.0);
745 tex.add_orientation(ori, 1.0);
746 let sys = fcc_slip_systems();
747 let m = tex.average_taylor_factor(&sys, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
748 assert!(m >= 0.0, "Taylor factor should be non-negative, got {m}");
749 }
750
751 #[test]
754 fn test_taylor_factor_zero_strain_returns_zero() {
755 let sys = fcc_slip_systems();
756 let m = taylor_factor(&sys, [0.0; 6]);
757 assert_eq!(m, 0.0);
758 }
759
760 #[test]
761 fn test_taylor_factor_positive_for_nonzero_strain() {
762 let sys = fcc_slip_systems();
763 let m = taylor_factor(&sys, [1.0, -0.5, -0.5, 0.0, 0.0, 0.0]);
764 assert!(m >= 0.0);
765 }
766}