1use oxiphysics_core::math::Vec3;
13
14use crate::assembly::{assemble_load_vector, assemble_stiffness};
15use crate::boundary::{DirichletBc, NeumannBc, apply_dirichlet, apply_neumann};
16use crate::constitutive::LinearElasticMaterial;
17use crate::element::LinearTetrahedron;
18use crate::mesh::TetrahedralMesh;
19use crate::solvers::{jacobi_preconditioner, preconditioned_cg};
20
21#[derive(Debug, Clone, PartialEq)]
27pub enum FemError {
28 SingularSystem,
30 InvalidInput(String),
32 SolverNotConverged,
34}
35
36impl std::fmt::Display for FemError {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 FemError::SingularSystem => write!(f, "singular or ill-conditioned system"),
40 FemError::InvalidInput(msg) => write!(f, "invalid input: {msg}"),
41 FemError::SolverNotConverged => write!(f, "iterative solver did not converge"),
42 }
43 }
44}
45
46impl std::error::Error for FemError {}
47
48#[derive(Debug, Clone)]
54pub struct FemResult {
55 pub displacements: Vec<Vec3>,
57 pub stress: Vec<[f64; 6]>,
59 pub strain: Vec<[f64; 6]>,
61}
62
63pub struct LinearStaticAnalysis {
72 pub max_iter: usize,
74 pub tolerance: f64,
76}
77
78impl Default for LinearStaticAnalysis {
79 fn default() -> Self {
80 Self {
81 max_iter: 10000,
82 tolerance: 1e-10,
83 }
84 }
85}
86
87impl LinearStaticAnalysis {
88 pub fn new() -> Self {
90 Self::default()
91 }
92
93 pub fn solve(
95 &self,
96 mesh: &TetrahedralMesh,
97 material: &LinearElasticMaterial,
98 dirichlet_bcs: &[DirichletBc],
99 neumann_bcs: &[NeumannBc],
100 body_force: &Vec3,
101 ) -> FemResult {
102 let ndof = mesh.num_nodes() * 3;
103
104 let mut k = assemble_stiffness(mesh, material);
105 let mut f = assemble_load_vector(mesh, body_force);
106
107 apply_neumann(&mut f, neumann_bcs);
108 apply_dirichlet(&mut k, &mut f, dirichlet_bcs);
109
110 let precond = jacobi_preconditioner(&k);
111 let u = preconditioned_cg(&k, &f, &precond, self.max_iter, self.tolerance);
112
113 let num_nodes = mesh.num_nodes();
114 let mut displacements = Vec::with_capacity(num_nodes);
115 for i in 0..num_nodes {
116 displacements.push(Vec3::new(u[i * 3], u[i * 3 + 1], u[i * 3 + 2]));
117 }
118
119 let d_matrix = material.constitutive_matrix();
120 let num_elements = mesh.num_elements();
121 let mut stress = Vec::with_capacity(num_elements);
122 let mut strain = Vec::with_capacity(num_elements);
123
124 for elem in &mesh.elements {
125 let nodes = [
126 mesh.nodes[elem[0]],
127 mesh.nodes[elem[1]],
128 mesh.nodes[elem[2]],
129 mesh.nodes[elem[3]],
130 ];
131
132 let (b_mat, _vol) = LinearTetrahedron::b_matrix(&nodes);
133
134 let mut u_e = [0.0f64; 12];
135 for (local, &global) in elem.iter().enumerate() {
136 u_e[local * 3] = u[global * 3];
137 u_e[local * 3 + 1] = u[global * 3 + 1];
138 u_e[local * 3 + 2] = u[global * 3 + 2];
139 }
140
141 let mut eps = [0.0f64; 6];
142 for i in 0..6 {
143 for j in 0..12 {
144 eps[i] += b_mat[i * 12 + j] * u_e[j];
145 }
146 }
147
148 let mut sig = [0.0f64; 6];
149 for i in 0..6 {
150 for j in 0..6 {
151 sig[i] += d_matrix[i][j] * eps[j];
152 }
153 }
154
155 strain.push(eps);
156 stress.push(sig);
157 }
158
159 let _ = ndof;
160
161 FemResult {
162 displacements,
163 stress,
164 strain,
165 }
166 }
167}
168
169pub struct StaticAnalysis {
177 pub n_dofs: usize,
179 pub stiffness_matrix: Vec<f64>,
181 pub force_vector: Vec<f64>,
183}
184
185impl StaticAnalysis {
186 pub fn new(n_dofs: usize) -> Self {
188 Self {
189 n_dofs,
190 stiffness_matrix: vec![0.0; n_dofs * n_dofs],
191 force_vector: vec![0.0; n_dofs],
192 }
193 }
194
195 pub fn assemble_global_stiffness(&mut self, elements: &[(Vec<usize>, Vec<f64>)]) {
200 let n = self.n_dofs;
201 for (dofs, ke) in elements {
202 let ndof_e = dofs.len();
203 for i in 0..ndof_e {
204 for j in 0..ndof_e {
205 let gi = dofs[i];
206 let gj = dofs[j];
207 self.stiffness_matrix[gi * n + gj] += ke[i * ndof_e + j];
208 }
209 }
210 }
211 }
212
213 pub fn solve(&self) -> Result<Vec<f64>, FemError> {
217 let n = self.n_dofs;
218 if n == 0 {
219 return Ok(Vec::new());
220 }
221 let mut aug: Vec<f64> = Vec::with_capacity(n * (n + 1));
223 for i in 0..n {
224 for j in 0..n {
225 aug.push(self.stiffness_matrix[i * n + j]);
226 }
227 aug.push(self.force_vector[i]);
228 }
229
230 let cols = n + 1;
231
232 for col in 0..n {
233 let mut max_val = aug[col * cols + col].abs();
235 let mut max_row = col;
236 for row in (col + 1)..n {
237 let v = aug[row * cols + col].abs();
238 if v > max_val {
239 max_val = v;
240 max_row = row;
241 }
242 }
243 if max_val < 1e-14 {
244 return Err(FemError::SingularSystem);
245 }
246 if max_row != col {
248 for k in 0..cols {
249 aug.swap(col * cols + k, max_row * cols + k);
250 }
251 }
252 let pivot = aug[col * cols + col];
253 for row in (col + 1)..n {
255 let factor = aug[row * cols + col] / pivot;
256 for k in col..cols {
257 let sub = factor * aug[col * cols + k];
258 aug[row * cols + k] -= sub;
259 }
260 }
261 }
262
263 let mut u = vec![0.0; n];
265 for i in (0..n).rev() {
266 let mut sum = aug[i * cols + n];
267 for j in (i + 1)..n {
268 sum -= aug[i * cols + j] * u[j];
269 }
270 u[i] = sum / aug[i * cols + i];
271 }
272 Ok(u)
273 }
274
275 pub fn compute_strains(
280 &self,
281 displacements: &[f64],
282 elements: &[(Vec<usize>, Vec<f64>)],
283 ) -> Vec<[f64; 6]> {
284 let mut strains = Vec::with_capacity(elements.len());
285 for (dofs, b_mat) in elements {
286 let ndof_e = dofs.len();
287 let mut u_e = vec![0.0; ndof_e];
288 for (k, &g) in dofs.iter().enumerate() {
289 u_e[k] = displacements[g];
290 }
291 let mut eps = [0.0f64; 6];
292 for i in 0..6 {
293 for j in 0..ndof_e {
294 eps[i] += b_mat[i * ndof_e + j] * u_e[j];
295 }
296 }
297 strains.push(eps);
298 }
299 strains
300 }
301
302 pub fn compute_stresses(strains: &[[f64; 6]], d_matrix: &[f64; 36]) -> Vec<[f64; 6]> {
306 strains
307 .iter()
308 .map(|eps| {
309 let mut sig = [0.0f64; 6];
310 for i in 0..6 {
311 for j in 0..6 {
312 sig[i] += d_matrix[i * 6 + j] * eps[j];
313 }
314 }
315 sig
316 })
317 .collect()
318 }
319}
320
321pub struct ModalAnalysis {
328 pub max_iter: usize,
330 pub tol: f64,
332}
333
334impl Default for ModalAnalysis {
335 fn default() -> Self {
336 Self {
337 max_iter: 500,
338 tol: 1e-10,
339 }
340 }
341}
342
343impl ModalAnalysis {
344 pub fn new() -> Self {
346 Self::default()
347 }
348
349 fn dot(a: &[f64], b: &[f64]) -> f64 {
351 a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
352 }
353
354 fn matvec_dense(a: &[f64], x: &[f64], n: usize) -> Vec<f64> {
356 let mut y = vec![0.0; n];
357 for i in 0..n {
358 for j in 0..n {
359 y[i] += a[i * n + j] * x[j];
360 }
361 }
362 y
363 }
364
365 fn solve_dense(a: &[f64], b: &[f64], n: usize) -> Option<Vec<f64>> {
368 let cols = n + 1;
369 let mut aug = vec![0.0; n * cols];
370 for i in 0..n {
371 for j in 0..n {
372 aug[i * cols + j] = a[i * n + j];
373 }
374 aug[i * cols + n] = b[i];
375 }
376 for col in 0..n {
377 let mut max_val = aug[col * cols + col].abs();
379 let mut max_row = col;
380 for row in (col + 1)..n {
381 let v = aug[row * cols + col].abs();
382 if v > max_val {
383 max_val = v;
384 max_row = row;
385 }
386 }
387 if max_val < 1e-15 {
388 return None;
389 }
390 if max_row != col {
391 for k in 0..cols {
392 aug.swap(col * cols + k, max_row * cols + k);
393 }
394 }
395 let pivot = aug[col * cols + col];
396 for row in (col + 1)..n {
397 let factor = aug[row * cols + col] / pivot;
398 for k in col..cols {
399 let sub = factor * aug[col * cols + k];
400 aug[row * cols + k] -= sub;
401 }
402 }
403 }
404 let mut x = vec![0.0; n];
405 for i in (0..n).rev() {
406 let mut s = aug[i * cols + n];
407 for j in (i + 1)..n {
408 s -= aug[i * cols + j] * x[j];
409 }
410 x[i] = s / aug[i * cols + i];
411 }
412 Some(x)
413 }
414
415 pub fn compute_natural_frequencies(
420 k: &[f64],
421 m: &[f64],
422 n: usize,
423 n_modes: usize,
424 max_iter: usize,
425 ) -> Vec<f64> {
426 let mut freqs = Vec::with_capacity(n_modes);
427 let mut found_modes: Vec<Vec<f64>> = Vec::new();
428
429 for mode_idx in 0..n_modes {
430 let mut q: Vec<f64> = (0..n)
432 .map(|i| {
433 let v = ((i + mode_idx * 13 + 1) as f64 * 0.7).sin();
434 if i == mode_idx % n { v + 1.0 } else { v }
435 })
436 .collect();
437
438 let mq = Self::matvec_dense(m, &q, n);
440 let nm = Self::dot(&q, &mq).max(0.0).sqrt();
441 if nm > 1e-60 {
442 for qi in q.iter_mut() {
443 *qi /= nm;
444 }
445 }
446
447 let mut omega_sq = 0.0_f64;
448
449 for _iter in 0..max_iter {
450 for phi in &found_modes {
452 let mphi = Self::matvec_dense(m, phi, n);
453 let c = Self::dot(&q, &mphi);
454 for i in 0..n {
455 q[i] -= c * phi[i];
456 }
457 }
458
459 let kq = Self::matvec_dense(k, &q, n);
461 let mq2 = Self::matvec_dense(m, &q, n);
462 let num = Self::dot(&q, &kq);
463 let den = Self::dot(&q, &mq2);
464 let rq = if den.abs() > 1e-60 { num / den } else { 0.0 };
465
466 let mut km = vec![0.0; n * n];
468 for i in 0..n {
469 for j in 0..n {
470 km[i * n + j] = k[i * n + j] - rq * m[i * n + j];
471 }
472 }
473 let rhs = Self::matvec_dense(m, &q, n);
474 let y_opt = Self::solve_dense(&km, &rhs, n);
475 let y = match y_opt {
476 Some(y) => y,
477 None => {
478 let mut rhs2 = Self::matvec_dense(m, &q, n);
480 if let Some(sol) = Self::solve_dense(k, &rhs2, n) {
482 rhs2 = sol;
483 }
484 rhs2
485 }
486 };
487
488 let mut yd = y.clone();
490 for phi in &found_modes {
491 let mphi = Self::matvec_dense(m, phi, n);
492 let c = Self::dot(&yd, &mphi);
493 for i in 0..n {
494 yd[i] -= c * phi[i];
495 }
496 }
497
498 let my = Self::matvec_dense(m, &yd, n);
500 let nm2 = Self::dot(&yd, &my).max(0.0).sqrt();
501 if nm2 < 1e-60 {
502 break;
503 }
504 let q_new: Vec<f64> = yd.iter().map(|yi| yi / nm2).collect();
505
506 let kqn = Self::matvec_dense(k, &q_new, n);
508 let mqn = Self::matvec_dense(m, &q_new, n);
509 let num_new = Self::dot(&q_new, &kqn);
510 let den_new = Self::dot(&q_new, &mqn);
511 let rq_new = if den_new.abs() > 1e-60 {
512 num_new / den_new
513 } else {
514 0.0
515 };
516
517 let change = (rq_new - omega_sq).abs() / (rq_new.abs() + 1e-60);
518 q = q_new;
519 omega_sq = rq_new;
520 if change < 1e-10 && _iter > 0 {
521 break;
522 }
523 }
524
525 freqs.push(omega_sq.max(0.0).sqrt());
526 found_modes.push(q);
527 }
528
529 freqs
530 }
531
532 pub fn compute_mode_shapes(
537 k: &[f64],
538 m: &[f64],
539 n: usize,
540 omega: f64,
541 max_iter: usize,
542 ) -> Vec<f64> {
543 let shift = omega * omega;
544 let mut km = vec![0.0; n * n];
546 for i in 0..n {
547 for j in 0..n {
548 km[i * n + j] = k[i * n + j] - shift * m[i * n + j];
549 }
550 }
551
552 let mut q: Vec<f64> = (0..n).map(|i| ((i + 1) as f64 * 0.5).sin()).collect();
553 let nrm: f64 = q.iter().map(|x| x * x).sum::<f64>().sqrt();
555 if nrm > 1e-60 {
556 for qi in q.iter_mut() {
557 *qi /= nrm;
558 }
559 }
560
561 for _ in 0..max_iter {
562 let rhs = Self::matvec_dense(m, &q, n);
563 let y = match Self::solve_dense(&km, &rhs, n) {
564 Some(y) => y,
565 None => break,
566 };
567 let ny: f64 = y.iter().map(|x| x * x).sum::<f64>().sqrt();
568 if ny < 1e-60 {
569 break;
570 }
571 let q_new: Vec<f64> = y.iter().map(|yi| yi / ny).collect();
572 let change: f64 = q
573 .iter()
574 .zip(q_new.iter())
575 .map(|(a, b)| (a - b).abs())
576 .sum::<f64>()
577 / (n as f64);
578 q = q_new;
579 if change < 1e-12 {
580 break;
581 }
582 }
583
584 let mq = Self::matvec_dense(m, &q, n);
586 let mn: f64 = Self::dot(&q, &mq).max(0.0).sqrt();
587 if mn > 1e-60 {
588 for qi in q.iter_mut() {
589 *qi /= mn;
590 }
591 }
592 q
593 }
594}
595
596pub struct DynamicAnalysis {
604 pub m: Vec<f64>,
606 pub c: Vec<f64>,
608 pub k: Vec<f64>,
610 pub n: usize,
612}
613
614impl DynamicAnalysis {
615 pub fn new(m: Vec<f64>, c: Vec<f64>, k: Vec<f64>, n: usize) -> Self {
617 Self { m, c, k, n }
618 }
619
620 fn mv(a: &[f64], x: &[f64], n: usize) -> Vec<f64> {
622 let mut y = vec![0.0; n];
623 for i in 0..n {
624 for j in 0..n {
625 y[i] += a[i * n + j] * x[j];
626 }
627 }
628 y
629 }
630
631 fn solve(a: &[f64], b: &[f64], n: usize) -> Vec<f64> {
633 let cols = n + 1;
634 let mut aug = vec![0.0; n * cols];
635 for i in 0..n {
636 for j in 0..n {
637 aug[i * cols + j] = a[i * n + j];
638 }
639 aug[i * cols + n] = b[i];
640 }
641 for col in 0..n {
642 let mut max_val = aug[col * cols + col].abs();
643 let mut max_row = col;
644 for row in (col + 1)..n {
645 let v = aug[row * cols + col].abs();
646 if v > max_val {
647 max_val = v;
648 max_row = row;
649 }
650 }
651 if max_val < 1e-15 {
652 continue;
653 }
654 if max_row != col {
655 for k in 0..cols {
656 aug.swap(col * cols + k, max_row * cols + k);
657 }
658 }
659 let piv = aug[col * cols + col];
660 for row in (col + 1)..n {
661 let fac = aug[row * cols + col] / piv;
662 for k in col..cols {
663 let sub = fac * aug[col * cols + k];
664 aug[row * cols + k] -= sub;
665 }
666 }
667 }
668 let mut x = vec![0.0; n];
669 for i in (0..n).rev() {
670 let mut s = aug[i * cols + n];
671 for j in (i + 1)..n {
672 s -= aug[i * cols + j] * x[j];
673 }
674 x[i] = s / aug[i * cols + i];
675 }
676 x
677 }
678
679 pub fn newmark_beta_step(
691 &self,
692 u: &[f64],
693 v: &[f64],
694 a: &[f64],
695 f_ext: &[f64],
696 dt: f64,
697 beta: f64,
698 gamma: f64,
699 ) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
700 let n = self.n;
701 let u_pred: Vec<f64> = (0..n)
703 .map(|i| u[i] + dt * v[i] + dt * dt * (0.5 - beta) * a[i])
704 .collect();
705 let v_pred: Vec<f64> = (0..n).map(|i| v[i] + dt * (1.0 - gamma) * a[i]).collect();
706
707 let c1 = 1.0 / (beta * dt * dt);
709 let c2 = gamma / (beta * dt);
710
711 let mut k_eff = vec![0.0; n * n];
712 for i in 0..n {
713 for j in 0..n {
714 k_eff[i * n + j] =
715 c1 * self.m[i * n + j] + c2 * self.c[i * n + j] + self.k[i * n + j];
716 }
717 }
718
719 let ku_pred = Self::mv(&self.k, &u_pred, n);
721 let cv_pred = Self::mv(&self.c, &v_pred, n);
722 let rhs: Vec<f64> = (0..n).map(|i| f_ext[i] - ku_pred[i] - cv_pred[i]).collect();
723
724 let a_new = Self::solve(&k_eff, &rhs, n);
725 let u_new: Vec<f64> = (0..n)
726 .map(|i| u_pred[i] + beta * dt * dt * a_new[i])
727 .collect();
728 let v_new: Vec<f64> = (0..n).map(|i| v_pred[i] + gamma * dt * a_new[i]).collect();
729
730 (u_new, v_new, a_new)
731 }
732
733 pub fn wilson_theta_step(
740 &self,
741 u: &[f64],
742 v: &[f64],
743 a: &[f64],
744 f_ext: &[f64],
745 dt: f64,
746 theta: f64,
747 ) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
748 let n = self.n;
749 let tau = theta * dt;
750
751 let c1 = 6.0 / (tau * tau);
753 let c2 = 3.0 / tau;
754
755 let mut k_eff = vec![0.0; n * n];
756 for i in 0..n {
757 for j in 0..n {
758 k_eff[i * n + j] =
759 c1 * self.m[i * n + j] + c2 * self.c[i * n + j] + self.k[i * n + j];
760 }
761 }
762
763 let f_tau = f_ext.to_vec();
767
768 let mu = Self::mv(&self.m, u, n);
770 let mv_curr = Self::mv(&self.m, v, n);
771 let ma = Self::mv(&self.m, a, n);
772 let cu = Self::mv(&self.c, u, n);
773 let cv_curr = Self::mv(&self.c, v, n);
774
775 let rhs: Vec<f64> = (0..n)
776 .map(|i| {
777 f_tau[i] + c1 * mu[i] + c2 * mv_curr[i] + 2.0 * ma[i]
778 - self.k[i * n..i * n + n]
779 .iter()
780 .zip(u.iter())
781 .map(|(kij, uj)| kij * uj)
782 .sum::<f64>()
783 + (c2 * cu[i] + cv_curr[i])
784 })
785 .collect();
786
787 let a_tau = Self::solve(&k_eff, &rhs, n);
789
790 let a_new: Vec<f64> = (0..n).map(|i| a[i] + (a_tau[i] - a[i]) / theta).collect();
792 let v_new: Vec<f64> = (0..n)
793 .map(|i| v[i] + dt / 2.0 * (a[i] + a_new[i]))
794 .collect();
795 let u_new: Vec<f64> = (0..n)
796 .map(|i| u[i] + dt * v[i] + dt * dt / 6.0 * (2.0 * a[i] + a_new[i]))
797 .collect();
798
799 (u_new, v_new, a_new)
800 }
801}
802
803#[derive(Debug, Clone, Copy)]
809pub struct StressState {
810 pub sigma: [f64; 6],
812}
813
814impl StressState {
815 pub fn from_voigt(sigma: [f64; 6]) -> Self {
817 Self { sigma }
818 }
819
820 pub fn new(sxx: f64, syy: f64, szz: f64, txy: f64, tyz: f64, txz: f64) -> Self {
822 Self {
823 sigma: [sxx, syy, szz, txy, tyz, txz],
824 }
825 }
826
827 pub fn hydrostatic(&self) -> f64 {
829 (self.sigma[0] + self.sigma[1] + self.sigma[2]) / 3.0
830 }
831
832 pub fn i1(&self) -> f64 {
834 self.sigma[0] + self.sigma[1] + self.sigma[2]
835 }
836
837 pub fn j2(&self) -> f64 {
841 let p = self.hydrostatic();
842 let sx = self.sigma[0] - p;
843 let sy = self.sigma[1] - p;
844 let sz = self.sigma[2] - p;
845 let txy = self.sigma[3];
846 let tyz = self.sigma[4];
847 let txz = self.sigma[5];
848 0.5 * (sx * sx + sy * sy + sz * sz + 2.0 * (txy * txy + tyz * tyz + txz * txz))
849 }
850
851 pub fn j3(&self) -> f64 {
853 let p = self.hydrostatic();
854 let sx = self.sigma[0] - p;
855 let sy = self.sigma[1] - p;
856 let sz = self.sigma[2] - p;
857 let txy = self.sigma[3];
858 let tyz = self.sigma[4];
859 let txz = self.sigma[5];
860 sx * (sy * sz - tyz * tyz) - txy * (txy * sz - tyz * txz) + txz * (txy * tyz - sy * txz)
861 }
862
863 pub fn von_mises(&self) -> f64 {
865 (3.0 * self.j2()).max(0.0).sqrt()
866 }
867
868 pub fn tresca_stress(&self) -> f64 {
870 let [s1, _s2, s3] = self.principal_stresses();
871 s1 - s3
872 }
873
874 pub fn principal_stresses(&self) -> [f64; 3] {
879 let p = self.hydrostatic();
880 let j2 = self.j2();
881 let j3 = self.j3();
882 let r = (j2 / 3.0).max(0.0).sqrt();
883 if r < 1e-30 {
884 return [p, p, p];
885 }
886 let cos_arg = (j3 / (2.0 * r * r * r)).clamp(-1.0, 1.0);
887 let theta = cos_arg.acos() / 3.0;
888 use std::f64::consts::PI;
889 let s1 = p + 2.0 * r * theta.cos();
890 let s2 = p + 2.0 * r * (theta - 2.0 * PI / 3.0).cos();
891 let s3 = p + 2.0 * r * (theta - 4.0 * PI / 3.0).cos();
892 let mut arr = [s1, s2, s3];
893 arr.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
894 arr
895 }
896
897 pub fn lode_angle_deg(&self) -> f64 {
901 let j2 = self.j2();
902 let j3 = self.j3();
903 if j2 < 1e-30 {
904 return 0.0;
905 }
906 let cos_arg = (3.0_f64.sqrt() * 3.0 * j3 / (2.0 * j2.powf(1.5))).clamp(-1.0, 1.0);
907 let theta_rad = cos_arg.acos() / 3.0;
908 theta_rad.to_degrees()
909 }
910
911 pub fn safety_factor_von_mises(&self, yield_stress: f64) -> f64 {
915 let vm = self.von_mises();
916 if vm < 1e-30 {
917 return f64::INFINITY;
918 }
919 yield_stress / vm
920 }
921
922 pub fn safety_factor_tresca(&self, yield_stress: f64) -> f64 {
924 let tresca = self.tresca_stress();
925 if tresca < 1e-30 {
926 return f64::INFINITY;
927 }
928 yield_stress / tresca
929 }
930
931 pub fn drucker_prager(&self, alpha: f64) -> f64 {
935 alpha * self.i1() + self.j2().max(0.0).sqrt()
936 }
937}
938
939#[derive(Debug, Clone)]
950pub struct FatigueLifeEstimator {
951 pub sigma_uts: f64,
953 pub n_ref: f64,
955 pub endurance_limit: f64,
957}
958
959impl FatigueLifeEstimator {
960 pub fn new(sigma_uts: f64, n_ref: f64, endurance_limit: f64) -> Self {
962 assert!(sigma_uts > 0.0);
963 assert!(n_ref > 0.0);
964 assert!(endurance_limit >= 0.0 && endurance_limit < sigma_uts);
965 Self {
966 sigma_uts,
967 n_ref,
968 endurance_limit,
969 }
970 }
971
972 pub fn cycles_to_failure(&self, sigma_a: f64) -> f64 {
976 if sigma_a <= self.endurance_limit {
977 return f64::INFINITY;
978 }
979 if sigma_a >= self.sigma_uts {
980 return 1.0;
981 }
982 let m = self.basquin_exponent();
985 self.n_ref * (self.sigma_uts / sigma_a).powf(m)
986 }
987
988 pub fn basquin_exponent(&self) -> f64 {
992 self.n_ref.log10() / 2.0_f64.log10()
995 }
996
997 pub fn goodman_corrected_amplitude(&self, sigma_a: f64, sigma_mean: f64) -> f64 {
1001 let denom = 1.0 - sigma_mean / self.sigma_uts;
1002 if denom <= 0.0 {
1003 return f64::INFINITY;
1004 }
1005 sigma_a / denom
1006 }
1007}
1008
1009pub fn miner_damage(cycle_ratios: &[f64]) -> f64 {
1014 cycle_ratios.iter().sum()
1015}
1016
1017pub fn compute_element_strains(
1025 displacements: &[f64],
1026 element_dofs: &[Vec<usize>],
1027 b_matrices: &[Vec<f64>],
1028) -> Vec<[f64; 6]> {
1029 element_dofs
1030 .iter()
1031 .zip(b_matrices.iter())
1032 .map(|(dofs, b)| {
1033 let mut eps = [0.0f64; 6];
1034 let n_dof = dofs.len();
1035 for row in 0..6 {
1036 let mut sum = 0.0;
1037 for col in 0..n_dof {
1038 if dofs[col] < displacements.len() {
1039 sum += b[row * n_dof + col] * displacements[dofs[col]];
1040 }
1041 }
1042 eps[row] = sum;
1043 }
1044 eps
1045 })
1046 .collect()
1047}
1048
1049pub fn von_mises_field(stresses: &[[f64; 6]]) -> Vec<f64> {
1051 stresses
1052 .iter()
1053 .map(|&s| StressState::from_voigt(s).von_mises())
1054 .collect()
1055}
1056
1057pub fn safety_factor_field(stresses: &[[f64; 6]], yield_stress: f64) -> Vec<f64> {
1059 stresses
1060 .iter()
1061 .map(|&s| StressState::from_voigt(s).safety_factor_von_mises(yield_stress))
1062 .collect()
1063}
1064
1065pub fn smooth_field(values: &[f64], neighbors: &[Vec<usize>]) -> Vec<f64> {
1070 values
1071 .iter()
1072 .enumerate()
1073 .map(|(i, &v)| {
1074 let nbrs = &neighbors[i];
1075 if nbrs.is_empty() {
1076 return v;
1077 }
1078 let sum: f64 = nbrs
1079 .iter()
1080 .map(|&j| if j < values.len() { values[j] } else { 0.0 })
1081 .sum();
1082 (v + sum) / (1 + nbrs.len()) as f64
1083 })
1084 .collect()
1085}
1086
1087#[derive(Debug, Clone, Copy)]
1095pub struct RayleighDamping {
1096 pub alpha: f64,
1098 pub beta: f64,
1100}
1101
1102impl RayleighDamping {
1103 pub fn from_two_modes(omega_i: f64, omega_j: f64, zeta_i: f64, zeta_j: f64) -> Self {
1108 let det = (omega_j * omega_j - omega_i * omega_i) / (2.0 * omega_i * omega_j);
1112 if det.abs() < 1e-30 {
1113 return Self {
1114 alpha: 0.0,
1115 beta: 0.0,
1116 };
1117 }
1118 let alpha = 2.0 * omega_i * omega_j * (zeta_i * omega_j - zeta_j * omega_i)
1120 / (omega_j * omega_j - omega_i * omega_i);
1121 let beta =
1122 2.0 * (zeta_j * omega_j - zeta_i * omega_i) / (omega_j * omega_j - omega_i * omega_i);
1123 Self { alpha, beta }
1124 }
1125
1126 pub fn damping_ratio(&self, omega: f64) -> f64 {
1128 if omega < 1e-30 {
1129 return 0.0;
1130 }
1131 self.alpha / (2.0 * omega) + self.beta * omega / 2.0
1132 }
1133
1134 pub fn assemble(&self, mass: &[f64], stiffness: &[f64], n: usize) -> Vec<f64> {
1136 assert_eq!(mass.len(), n * n);
1137 assert_eq!(stiffness.len(), n * n);
1138 (0..n * n)
1139 .map(|k| self.alpha * mass[k] + self.beta * stiffness[k])
1140 .collect()
1141 }
1142}
1143
1144#[cfg(test)]
1149mod tests {
1150 use super::*;
1151
1152 #[test]
1155 fn test_static_solve_2dof() {
1156 let mut sa = StaticAnalysis::new(2);
1159 sa.stiffness_matrix = vec![4.0, -1.0, -1.0, 3.0];
1160 sa.force_vector = vec![1.0, 2.0];
1161 let u = sa.solve().unwrap();
1162 let expected0 = 5.0 / 11.0;
1163 let expected1 = 9.0 / 11.0;
1164 assert!(
1165 (u[0] - expected0).abs() < 1e-12,
1166 "u[0] = {}, expected {expected0}",
1167 u[0]
1168 );
1169 assert!(
1170 (u[1] - expected1).abs() < 1e-12,
1171 "u[1] = {}, expected {expected1}",
1172 u[1]
1173 );
1174 }
1175
1176 #[test]
1177 fn test_static_solve_singular() {
1178 let mut sa = StaticAnalysis::new(2);
1179 sa.stiffness_matrix = vec![1.0, 1.0, 1.0, 1.0];
1180 sa.force_vector = vec![1.0, 1.0];
1181 let result = sa.solve();
1182 assert!(result.is_err(), "expected SingularSystem error");
1183 }
1184
1185 #[test]
1186 fn test_static_assemble_and_solve() {
1187 let mut sa = StaticAnalysis::new(2);
1190 let elements = vec![(vec![0usize, 1], vec![2.0, -2.0, -2.0, 2.0])];
1191 sa.assemble_global_stiffness(&elements);
1192 sa.stiffness_matrix[0] = 1.0;
1194 sa.stiffness_matrix[1] = 0.0;
1195 sa.stiffness_matrix[2] = 0.0;
1196 sa.force_vector[0] = 0.0;
1197 sa.force_vector[1] = 1.0; let u = sa.solve().unwrap();
1199 assert!((u[0]).abs() < 1e-12, "u[0] should be 0, got {}", u[0]);
1200 assert!(u[1] > 0.0, "u[1] should be positive, got {}", u[1]);
1201 }
1202
1203 #[test]
1204 fn test_compute_strains() {
1205 let sa = StaticAnalysis::new(1);
1207 let b_mat: Vec<f64> = {
1208 let mut v = vec![0.0; 6];
1209 v[0] = 1.0;
1210 v
1211 };
1212 let elements = vec![(vec![0usize], b_mat)];
1213 let u = vec![0.5_f64];
1214 let strains = sa.compute_strains(&u, &elements);
1215 assert_eq!(strains.len(), 1);
1216 assert!((strains[0][0] - 0.5).abs() < 1e-15);
1217 for (k, &val) in strains[0].iter().enumerate().skip(1) {
1218 assert_eq!(val, 0.0, "strains[0][{k}] should be 0");
1219 }
1220 }
1221
1222 #[test]
1223 fn test_compute_stresses_identity_d() {
1224 let mut d = [0.0f64; 36];
1226 for i in 0..6 {
1227 d[i * 6 + i] = 1.0;
1228 }
1229 let eps = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1230 let stresses = StaticAnalysis::compute_stresses(&[eps], &d);
1231 assert_eq!(stresses[0], eps);
1232 }
1233
1234 #[test]
1237 fn test_modal_1dof() {
1238 let k_val = 400.0f64;
1240 let m_val = 1.0f64;
1241 let k = vec![k_val];
1242 let m = vec![m_val];
1243 let _ma = ModalAnalysis::default();
1244 let freqs = ModalAnalysis::compute_natural_frequencies(&k, &m, 1, 1, 200);
1245 let expected = k_val.sqrt();
1246 assert!(
1247 (freqs[0] - expected).abs() / expected < 1e-5,
1248 "ω = {}, expected {expected}",
1249 freqs[0]
1250 );
1251 }
1252
1253 #[test]
1254 fn test_modal_2dof_diagonal() {
1255 let k = vec![1.0, 0.0, 0.0, 9.0];
1257 let m = vec![1.0, 0.0, 0.0, 1.0];
1258 let _ma = ModalAnalysis::default();
1259 let mut freqs = ModalAnalysis::compute_natural_frequencies(&k, &m, 2, 2, 500);
1260 freqs.sort_by(|a, b| a.partial_cmp(b).unwrap());
1261 assert!((freqs[0] - 1.0).abs() < 0.05, "ω₁ = {}", freqs[0]);
1262 assert!((freqs[1] - 3.0).abs() < 0.05, "ω₂ = {}", freqs[1]);
1263 }
1264
1265 #[test]
1266 fn test_mode_shape_1dof() {
1267 let k = vec![25.0_f64];
1268 let m = vec![1.0_f64];
1269 let phi = ModalAnalysis::compute_mode_shapes(&k, &m, 1, 5.0, 100);
1270 let norm: f64 = phi.iter().map(|x| x * x).sum::<f64>().sqrt();
1272 assert!((norm - 1.0).abs() < 1e-6, "mass norm = {norm}");
1273 }
1274
1275 #[test]
1280 fn test_newmark_free_vibration() {
1281 let m_val = 1.0_f64;
1282 let k_val = 100.0_f64;
1283 let dt = 0.001;
1284 let beta = 0.25;
1285 let gamma = 0.5;
1286
1287 let da = DynamicAnalysis::new(vec![m_val], vec![0.0], vec![k_val], 1);
1288 let mut u = vec![1.0_f64];
1289 let mut v = vec![0.0_f64];
1290 let mut a = vec![-k_val / m_val];
1291
1292 for _ in 0..100 {
1293 let (un, vn, an) = da.newmark_beta_step(&u, &v, &a, &[0.0], dt, beta, gamma);
1294 u = un;
1295 v = vn;
1296 a = an;
1297 }
1298 assert!(u[0].abs() <= 1.01, "u should be bounded, got {}", u[0]);
1300 }
1301
1302 #[test]
1303 fn test_newmark_static_equilibrium() {
1304 let da = DynamicAnalysis::new(vec![1.0], vec![0.1], vec![10.0], 1);
1306 let mut u = vec![0.0_f64];
1307 let mut v = vec![0.0_f64];
1308 let mut a = vec![0.0_f64];
1309 let f = vec![5.0_f64]; for _ in 0..5000 {
1311 let (un, vn, an) = da.newmark_beta_step(&u, &v, &a, &f, 0.01, 0.25, 0.5);
1312 u = un;
1313 v = vn;
1314 a = an;
1315 }
1316 assert!(u[0] > 0.0, "u should be positive, got {}", u[0]);
1318 }
1319
1320 #[test]
1321 fn test_wilson_theta_step_displacement_sign() {
1322 let da = DynamicAnalysis::new(vec![1.0], vec![0.0], vec![1.0], 1);
1324 let u = vec![0.0_f64];
1325 let v = vec![0.0_f64];
1326 let a = vec![0.0_f64];
1327 let (u_new, _, _) = da.wilson_theta_step(&u, &v, &a, &[1.0], 0.01, 1.4);
1328 assert!(
1329 u_new[0] >= 0.0,
1330 "displacement should be non-negative, got {}",
1331 u_new[0]
1332 );
1333 }
1334
1335 #[test]
1338 fn test_cantilever_deflection() {
1339 let length: f64 = 0.5;
1340 let width: f64 = 0.1;
1341 let height: f64 = 0.1;
1342 let e: f64 = 200.0e9;
1343 let nu: f64 = 0.3;
1344 let p: f64 = -500.0;
1345
1346 let mesh = TetrahedralMesh::generate_beam(length, width, height, 4, 1, 1);
1347 let material = LinearElasticMaterial::new(e, nu);
1348
1349 let mut dirichlet_bcs = Vec::new();
1350 for (i, node) in mesh.nodes.iter().enumerate() {
1351 if node.x.abs() < 1e-10 {
1352 dirichlet_bcs.push(DirichletBc::new(i, 0, 0.0));
1353 dirichlet_bcs.push(DirichletBc::new(i, 1, 0.0));
1354 dirichlet_bcs.push(DirichletBc::new(i, 2, 0.0));
1355 }
1356 }
1357
1358 let free_end_nodes: Vec<usize> = mesh
1359 .nodes
1360 .iter()
1361 .enumerate()
1362 .filter(|(_, n)| (n.x - length).abs() < 1e-10)
1363 .map(|(i, _)| i)
1364 .collect();
1365 let num_free = free_end_nodes.len() as f64;
1366 let neumann_bcs: Vec<NeumannBc> = free_end_nodes
1367 .iter()
1368 .map(|&i| NeumannBc::new(i, [0.0, p / num_free, 0.0]))
1369 .collect();
1370
1371 let analysis = LinearStaticAnalysis {
1372 max_iter: 20000,
1373 tolerance: 1e-10,
1374 };
1375 let result = analysis.solve(
1376 &mesh,
1377 &material,
1378 &dirichlet_bcs,
1379 &neumann_bcs,
1380 &Vec3::new(0.0, 0.0, 0.0),
1381 );
1382
1383 let max_y_disp = free_end_nodes
1384 .iter()
1385 .map(|&i| result.displacements[i].y)
1386 .fold(f64::INFINITY, f64::min);
1387
1388 assert!(
1389 max_y_disp < 0.0,
1390 "Tip deflection should be downward (negative y), got {max_y_disp:.6e}"
1391 );
1392 }
1393
1394 #[test]
1397 fn test_hydrostatic_pure_hydrostatic() {
1398 let sigma = [1.0, 1.0, 1.0, 0.0, 0.0, 0.0];
1399 let s = StressState::from_voigt(sigma);
1400 assert!(
1401 (s.hydrostatic() - 1.0).abs() < 1e-12,
1402 "hydrostatic={}",
1403 s.hydrostatic()
1404 );
1405 }
1406
1407 #[test]
1408 fn test_von_mises_zero_for_hydrostatic() {
1409 let sigma = [5.0, 5.0, 5.0, 0.0, 0.0, 0.0];
1410 let s = StressState::from_voigt(sigma);
1411 assert!(
1412 s.von_mises() < 1e-10,
1413 "von Mises should be 0 for hydrostatic: {}",
1414 s.von_mises()
1415 );
1416 }
1417
1418 #[test]
1419 fn test_von_mises_uniaxial() {
1420 let s0 = 300e6;
1421 let sigma = [s0, 0.0, 0.0, 0.0, 0.0, 0.0];
1422 let s = StressState::from_voigt(sigma);
1423 assert!(
1424 (s.von_mises() - s0).abs() < 1.0,
1425 "von Mises uniaxial: {}",
1426 s.von_mises()
1427 );
1428 }
1429
1430 #[test]
1431 fn test_principal_stresses_sorted() {
1432 let sigma = [200e6, 100e6, 50e6, 0.0, 0.0, 0.0];
1433 let s = StressState::from_voigt(sigma);
1434 let [s1, s2, s3] = s.principal_stresses();
1435 assert!(
1436 s1 >= s2 && s2 >= s3,
1437 "principal stresses not sorted: {s1} {s2} {s3}"
1438 );
1439 }
1440
1441 #[test]
1442 fn test_safety_factor_von_mises() {
1443 let sigma = [200e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1444 let s = StressState::from_voigt(sigma);
1445 let sf = s.safety_factor_von_mises(400e6);
1446 assert!((sf - 2.0).abs() < 1e-6, "safety factor={sf}");
1447 }
1448
1449 #[test]
1450 fn test_lode_angle_uniaxial_tension() {
1451 let sigma = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1452 let s = StressState::from_voigt(sigma);
1453 let theta = s.lode_angle_deg();
1455 assert!(theta.is_finite(), "Lode angle should be finite: {theta}");
1456 }
1457
1458 #[test]
1459 fn test_tresca_criterion_uniaxial() {
1460 let sigma = [200e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1461 let s = StressState::from_voigt(sigma);
1462 let tresca = s.tresca_stress();
1464 assert!((tresca - 200e6).abs() < 1.0, "Tresca={tresca}");
1465 }
1466
1467 #[test]
1470 fn test_fatigue_life_below_endurance() {
1471 let est = FatigueLifeEstimator::new(500e6, 1e6, 200e6);
1472 let life = est.cycles_to_failure(100e6);
1473 assert!(
1474 life == f64::INFINITY,
1475 "below endurance should return infinity: {life}"
1476 );
1477 }
1478
1479 #[test]
1480 fn test_fatigue_life_at_ultimate() {
1481 let est = FatigueLifeEstimator::new(500e6, 1e6, 200e6);
1482 let life = est.cycles_to_failure(500e6);
1483 assert!(life <= 1e6 + 1.0, "at UTS life should be <= N_ref: {life}");
1484 }
1485
1486 #[test]
1487 fn test_damage_accumulation_miner() {
1488 let d = miner_damage(&[0.3, 0.5, 0.2]);
1489 assert!((d - 1.0).abs() < 1e-12, "Miner sum = {d}");
1490 }
1491
1492 #[test]
1495 fn test_cantilever_beam() {
1496 let length: f64 = 1.0;
1497 let width: f64 = 0.1;
1498 let height: f64 = 0.1;
1499 let e: f64 = 200.0e9;
1500 let nu: f64 = 0.3;
1501 let p: f64 = -1000.0;
1502
1503 let inertia = width * height.powi(3) / 12.0;
1504 let analytical_deflection = p * length.powi(3) / (3.0 * e * inertia);
1505
1506 let nx = 10;
1507 let ny = 2;
1508 let nz = 2;
1509 let mesh = TetrahedralMesh::generate_beam(length, width, height, nx, ny, nz);
1510 let material = LinearElasticMaterial::new(e, nu);
1511
1512 let mut dirichlet_bcs = Vec::new();
1513 for (i, node) in mesh.nodes.iter().enumerate() {
1514 if node.x.abs() < 1e-10 {
1515 dirichlet_bcs.push(DirichletBc::new(i, 0, 0.0));
1516 dirichlet_bcs.push(DirichletBc::new(i, 1, 0.0));
1517 dirichlet_bcs.push(DirichletBc::new(i, 2, 0.0));
1518 }
1519 }
1520
1521 let free_end_nodes: Vec<usize> = mesh
1522 .nodes
1523 .iter()
1524 .enumerate()
1525 .filter(|(_, n)| (n.x - length).abs() < 1e-10)
1526 .map(|(i, _)| i)
1527 .collect();
1528 let num_free = free_end_nodes.len() as f64;
1529 let neumann_bcs: Vec<NeumannBc> = free_end_nodes
1530 .iter()
1531 .map(|&i| NeumannBc::new(i, [0.0, p / num_free, 0.0]))
1532 .collect();
1533
1534 let analysis = LinearStaticAnalysis {
1535 max_iter: 50000,
1536 tolerance: 1e-12,
1537 };
1538
1539 let result = analysis.solve(
1540 &mesh,
1541 &material,
1542 &dirichlet_bcs,
1543 &neumann_bcs,
1544 &Vec3::new(0.0, 0.0, 0.0),
1545 );
1546
1547 let max_y_disp = free_end_nodes
1548 .iter()
1549 .map(|&i| result.displacements[i].y)
1550 .fold(f64::INFINITY, f64::min);
1551
1552 let error = ((max_y_disp - analytical_deflection) / analytical_deflection).abs();
1553
1554 assert!(
1555 max_y_disp < 0.0,
1556 "Deflection should be negative (downward), got {max_y_disp:.6e}",
1557 );
1558 assert!(
1559 error < 1.0,
1560 "Cantilever deflection error too large: {:.1}%. \
1561 FEM = {max_y_disp:.6e}, analytical = {analytical_deflection:.6e}",
1562 error * 100.0,
1563 );
1564 }
1565}