1use crate::error::PhysicsError;
18use crate::fem::{ElementType, FemMesh, FemNode};
19use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ModalAnalysisConfig {
28 pub num_modes: usize,
30 pub max_iterations: usize,
32 pub tolerance: f64,
34 pub mass_type: MassMatrixType,
36 pub normalisation: NormalisationMethod,
38 pub shift: f64,
40}
41
42impl Default for ModalAnalysisConfig {
43 fn default() -> Self {
44 Self {
45 num_modes: 5,
46 max_iterations: 1000,
47 tolerance: 1e-8,
48 mass_type: MassMatrixType::Lumped,
49 normalisation: NormalisationMethod::MassNormalised,
50 shift: 0.0,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57pub enum MassMatrixType {
58 Lumped,
60 Consistent,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66pub enum NormalisationMethod {
67 MassNormalised,
69 MaxDisplacement,
71 None,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct VibrationalMode {
82 pub mode_number: usize,
84 pub frequency_hz: f64,
86 pub angular_frequency: f64,
88 pub period: f64,
90 pub mode_shape: Vec<f64>,
92 pub iterations: usize,
94 pub converged: bool,
96 pub participation_factors: ParticipationFactors,
98 pub effective_modal_mass: f64,
100}
101
102#[derive(Debug, Clone, Default, Serialize, Deserialize)]
104pub struct ParticipationFactors {
105 pub x: f64,
107 pub y: f64,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ModalAnalysisResult {
114 pub modes: Vec<VibrationalMode>,
116 pub total_dofs: usize,
118 pub constrained_dofs: usize,
120 pub total_mass: f64,
122 pub all_converged: bool,
124 pub cumulative_mass_fraction: f64,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct MacEntry {
135 pub mode_i: usize,
136 pub mode_j: usize,
137 pub value: f64,
138}
139
140pub fn compute_mac_matrix(
144 modes_a: &[Vec<f64>],
145 modes_b: &[Vec<f64>],
146) -> Result<Vec<Vec<f64>>, PhysicsError> {
147 if modes_a.is_empty() || modes_b.is_empty() {
148 return Err(PhysicsError::Simulation("Empty mode shape set".into()));
149 }
150
151 let n = modes_a[0].len();
152 for m in modes_a.iter().chain(modes_b.iter()) {
153 if m.len() != n {
154 return Err(PhysicsError::Simulation(
155 "Mode shapes must all have the same length".into(),
156 ));
157 }
158 }
159
160 let mut mac = vec![vec![0.0; modes_b.len()]; modes_a.len()];
161
162 for (i, phi) in modes_a.iter().enumerate() {
163 let phi_dot_phi = dot(phi, phi);
164 for (j, psi) in modes_b.iter().enumerate() {
165 let psi_dot_psi = dot(psi, psi);
166 let phi_dot_psi = dot(phi, psi);
167 let denom = phi_dot_phi * psi_dot_psi;
168 mac[i][j] = if denom > 1e-30 {
169 (phi_dot_psi * phi_dot_psi) / denom
170 } else {
171 0.0
172 };
173 }
174 }
175
176 Ok(mac)
177}
178
179pub struct ModalAnalysisSolver {
185 config: ModalAnalysisConfig,
186}
187
188impl ModalAnalysisSolver {
189 pub fn new(config: ModalAnalysisConfig) -> Self {
191 Self { config }
192 }
193
194 pub fn with_defaults() -> Self {
196 Self::new(ModalAnalysisConfig::default())
197 }
198
199 pub fn solve(&self, mesh: &FemMesh) -> Result<ModalAnalysisResult, PhysicsError> {
204 if mesh.nodes.is_empty() {
205 return Err(PhysicsError::Simulation("Empty mesh".into()));
206 }
207 if mesh.elements.is_empty() {
208 return Err(PhysicsError::Simulation("No elements in mesh".into()));
209 }
210
211 let n_nodes = mesh.nodes.len();
212 let dofs_per_node = 2; let total_dofs = n_nodes * dofs_per_node;
214
215 let mut k_global = vec![vec![0.0; total_dofs]; total_dofs];
217 let mut m_global = vec![vec![0.0; total_dofs]; total_dofs];
218
219 for elem in &mesh.elements {
220 self.assemble_element_stiffness(mesh, elem, &mut k_global)?;
221 self.assemble_element_mass(mesh, elem, &mut m_global)?;
222 }
223
224 let mut constrained = vec![false; total_dofs];
226 let mut constrained_count = 0;
227 for node in &mesh.nodes {
228 if node.boundary_condition.is_some() {
229 constrained[node.id * 2] = true;
230 constrained[node.id * 2 + 1] = true;
231 constrained_count += 2;
232 }
233 }
234
235 let free_dofs: Vec<usize> = (0..total_dofs).filter(|&d| !constrained[d]).collect();
237 let n_free = free_dofs.len();
238
239 if n_free == 0 {
240 return Err(PhysicsError::Simulation(
241 "All DOFs constrained — no free modes".into(),
242 ));
243 }
244
245 let mut k_red = vec![vec![0.0; n_free]; n_free];
246 let mut m_red = vec![vec![0.0; n_free]; n_free];
247
248 for (ri, &gi) in free_dofs.iter().enumerate() {
249 for (rj, &gj) in free_dofs.iter().enumerate() {
250 k_red[ri][rj] = k_global[gi][gj];
251 m_red[ri][rj] = m_global[gi][gj];
252 }
253 }
254
255 let max_k_diag = (0..n_free)
261 .map(|i| k_red[i][i].abs())
262 .fold(0.0f64, f64::max);
263 if max_k_diag > 1e-30 {
264 let regularisation = max_k_diag * 1e-10;
265 for (i, k_row) in k_red.iter_mut().enumerate().take(n_free) {
266 if k_row[i].abs() < 1e-30 {
267 k_row[i] = regularisation;
268 }
269 }
270 }
271
272 let total_mass: f64 = (0..n_free).map(|i| m_red[i][i]).sum();
274
275 let num_modes = self.config.num_modes.min(n_free);
277 let mut modes = Vec::with_capacity(num_modes);
278 let mut deflation_vectors: Vec<Vec<f64>> = Vec::new();
279 let mut cumulative_mass = 0.0;
280
281 for mode_idx in 0..num_modes {
282 let (eigenvalue, eigenvector, iterations, converged) =
283 self.inverse_power_iteration(&k_red, &m_red, &deflation_vectors)?;
284
285 let angular_freq = if eigenvalue > 0.0 {
286 eigenvalue.sqrt()
287 } else {
288 0.0
289 };
290 let freq_hz = angular_freq / (2.0 * std::f64::consts::PI);
291 let period = if freq_hz > 1e-30 { 1.0 / freq_hz } else { 0.0 };
292
293 let mut full_shape = vec![0.0; total_dofs];
295 for (ri, &gi) in free_dofs.iter().enumerate() {
296 full_shape[gi] = eigenvector[ri];
297 }
298
299 let normalised = self.normalise_mode(&full_shape, &m_global);
301
302 let pf = self.compute_participation_factors(&eigenvector, &m_red, n_nodes);
304
305 let eff_mass = if total_mass > 1e-30 {
307 (pf.x * pf.x + pf.y * pf.y) / total_mass
308 } else {
309 0.0
310 };
311 cumulative_mass += eff_mass;
312
313 deflation_vectors.push(eigenvector);
314
315 modes.push(VibrationalMode {
316 mode_number: mode_idx,
317 frequency_hz: freq_hz,
318 angular_frequency: angular_freq,
319 period,
320 mode_shape: normalised,
321 iterations,
322 converged,
323 participation_factors: pf,
324 effective_modal_mass: eff_mass,
325 });
326 }
327
328 let all_converged = modes.iter().all(|m| m.converged);
329
330 Ok(ModalAnalysisResult {
331 modes,
332 total_dofs,
333 constrained_dofs: constrained_count,
334 total_mass,
335 all_converged,
336 cumulative_mass_fraction: cumulative_mass,
337 })
338 }
339
340 fn assemble_element_stiffness(
342 &self,
343 mesh: &FemMesh,
344 elem: &crate::fem::FemElement,
345 k_global: &mut [Vec<f64>],
346 ) -> Result<(), PhysicsError> {
347 match elem.element_type {
348 ElementType::Bar1D => self.assemble_bar1d_stiffness(mesh, elem, k_global),
349 ElementType::Triangle2D => self.assemble_tri2d_stiffness(mesh, elem, k_global),
350 _ => {
351 self.assemble_bar1d_stiffness(mesh, elem, k_global)
353 }
354 }
355 }
356
357 fn assemble_bar1d_stiffness(
360 &self,
361 mesh: &FemMesh,
362 elem: &crate::fem::FemElement,
363 k_global: &mut [Vec<f64>],
364 ) -> Result<(), PhysicsError> {
365 if elem.node_ids.len() < 2 {
366 return Err(PhysicsError::Simulation("Bar1D needs 2 nodes".into()));
367 }
368 let n0 = &mesh.nodes[elem.node_ids[0]];
369 let n1 = &mesh.nodes[elem.node_ids[1]];
370
371 let dx = n1.x - n0.x;
372 let dy = n1.y - n0.y;
373 let length = (dx * dx + dy * dy).sqrt();
374 if length < 1e-30 {
375 return Err(PhysicsError::Simulation("Zero-length element".into()));
376 }
377
378 let c = dx / length; let s = dy / length; let ea_l = elem.material.youngs_modulus / length; let ke = [
384 [ea_l * c * c, ea_l * c * s, -ea_l * c * c, -ea_l * c * s],
385 [ea_l * c * s, ea_l * s * s, -ea_l * c * s, -ea_l * s * s],
386 [-ea_l * c * c, -ea_l * c * s, ea_l * c * c, ea_l * c * s],
387 [-ea_l * c * s, -ea_l * s * s, ea_l * c * s, ea_l * s * s],
388 ];
389
390 let dofs = [
391 elem.node_ids[0] * 2,
392 elem.node_ids[0] * 2 + 1,
393 elem.node_ids[1] * 2,
394 elem.node_ids[1] * 2 + 1,
395 ];
396
397 for (i, &di) in dofs.iter().enumerate() {
398 for (j, &dj) in dofs.iter().enumerate() {
399 k_global[di][dj] += ke[i][j];
400 }
401 }
402
403 Ok(())
404 }
405
406 fn assemble_tri2d_stiffness(
408 &self,
409 mesh: &FemMesh,
410 elem: &crate::fem::FemElement,
411 k_global: &mut [Vec<f64>],
412 ) -> Result<(), PhysicsError> {
413 if elem.node_ids.len() < 3 {
414 return Err(PhysicsError::Simulation("Triangle2D needs 3 nodes".into()));
415 }
416 let nodes: Vec<&FemNode> = elem.node_ids.iter().map(|&id| &mesh.nodes[id]).collect();
417
418 let x1 = nodes[0].x;
419 let y1 = nodes[0].y;
420 let x2 = nodes[1].x;
421 let y2 = nodes[1].y;
422 let x3 = nodes[2].x;
423 let y3 = nodes[2].y;
424
425 let area = 0.5 * ((x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1)).abs();
426 if area < 1e-30 {
427 return Err(PhysicsError::Simulation("Degenerate triangle".into()));
428 }
429
430 let b1 = y2 - y3;
432 let b2 = y3 - y1;
433 let b3 = y1 - y2;
434 let c1 = x3 - x2;
435 let c2 = x1 - x3;
436 let c3 = x2 - x1;
437
438 let e = elem.material.youngs_modulus;
439 let nu = elem.material.poissons_ratio;
440 let factor = e / (1.0 - nu * nu);
441
442 let d11 = factor;
444 let d12 = factor * nu;
445 let d33 = factor * (1.0 - nu) / 2.0;
446
447 let inv_4a = 1.0 / (4.0 * area);
449
450 let mut ke = [[0.0f64; 6]; 6];
452
453 let b_rows: [[f64; 6]; 3] = [
455 [b1, 0.0, b2, 0.0, b3, 0.0],
456 [0.0, c1, 0.0, c2, 0.0, c3],
457 [c1, b1, c2, b2, c3, b3],
458 ];
459 let d_mat = [[d11, d12, 0.0], [d12, d11, 0.0], [0.0, 0.0, d33]];
460
461 for i in 0..6 {
463 for j in 0..6 {
464 let mut val = 0.0;
465 for p in 0..3 {
466 for q in 0..3 {
467 val += b_rows[p][i] * d_mat[p][q] * b_rows[q][j];
468 }
469 }
470 ke[i][j] = val * inv_4a;
471 }
472 }
473
474 let dofs: Vec<usize> = elem
475 .node_ids
476 .iter()
477 .flat_map(|&id| [id * 2, id * 2 + 1])
478 .collect();
479
480 for (i, &di) in dofs.iter().enumerate() {
481 for (j, &dj) in dofs.iter().enumerate() {
482 k_global[di][dj] += ke[i][j];
483 }
484 }
485
486 Ok(())
487 }
488
489 fn assemble_element_mass(
491 &self,
492 mesh: &FemMesh,
493 elem: &crate::fem::FemElement,
494 m_global: &mut [Vec<f64>],
495 ) -> Result<(), PhysicsError> {
496 match self.config.mass_type {
497 MassMatrixType::Lumped => self.assemble_lumped_mass(mesh, elem, m_global),
498 MassMatrixType::Consistent => self.assemble_consistent_mass(mesh, elem, m_global),
499 }
500 }
501
502 fn assemble_lumped_mass(
504 &self,
505 mesh: &FemMesh,
506 elem: &crate::fem::FemElement,
507 m_global: &mut [Vec<f64>],
508 ) -> Result<(), PhysicsError> {
509 let element_mass = self.compute_element_mass(mesh, elem)?;
510 let n_nodes = elem.node_ids.len();
511 let mass_per_node = element_mass / n_nodes as f64;
512
513 for &nid in &elem.node_ids {
514 m_global[nid * 2][nid * 2] += mass_per_node;
515 m_global[nid * 2 + 1][nid * 2 + 1] += mass_per_node;
516 }
517
518 Ok(())
519 }
520
521 fn assemble_consistent_mass(
523 &self,
524 mesh: &FemMesh,
525 elem: &crate::fem::FemElement,
526 m_global: &mut [Vec<f64>],
527 ) -> Result<(), PhysicsError> {
528 if elem.element_type != ElementType::Bar1D || elem.node_ids.len() < 2 {
530 return self.assemble_lumped_mass(mesh, elem, m_global);
531 }
532
533 let n0 = &mesh.nodes[elem.node_ids[0]];
534 let n1 = &mesh.nodes[elem.node_ids[1]];
535 let dx = n1.x - n0.x;
536 let dy = n1.y - n0.y;
537 let length = (dx * dx + dy * dy).sqrt();
538 let rho = elem.material.density;
539 let m_total = rho * length; let m6 = m_total / 6.0;
541
542 let dofs = [
544 elem.node_ids[0] * 2,
545 elem.node_ids[0] * 2 + 1,
546 elem.node_ids[1] * 2,
547 elem.node_ids[1] * 2 + 1,
548 ];
549
550 m_global[dofs[0]][dofs[0]] += 2.0 * m6;
552 m_global[dofs[0]][dofs[2]] += m6;
553 m_global[dofs[2]][dofs[0]] += m6;
554 m_global[dofs[2]][dofs[2]] += 2.0 * m6;
555
556 m_global[dofs[1]][dofs[1]] += 2.0 * m6;
557 m_global[dofs[1]][dofs[3]] += m6;
558 m_global[dofs[3]][dofs[1]] += m6;
559 m_global[dofs[3]][dofs[3]] += 2.0 * m6;
560
561 Ok(())
562 }
563
564 fn compute_element_mass(
566 &self,
567 mesh: &FemMesh,
568 elem: &crate::fem::FemElement,
569 ) -> Result<f64, PhysicsError> {
570 let rho = elem.material.density;
571 match elem.element_type {
572 ElementType::Bar1D | ElementType::Beam1D => {
573 if elem.node_ids.len() < 2 {
574 return Err(PhysicsError::Simulation("Need 2 nodes for bar".into()));
575 }
576 let n0 = &mesh.nodes[elem.node_ids[0]];
577 let n1 = &mesh.nodes[elem.node_ids[1]];
578 let dx = n1.x - n0.x;
579 let dy = n1.y - n0.y;
580 let length = (dx * dx + dy * dy).sqrt();
581 Ok(rho * length) }
583 ElementType::Triangle2D => {
584 if elem.node_ids.len() < 3 {
585 return Err(PhysicsError::Simulation("Need 3 nodes for triangle".into()));
586 }
587 let nodes: Vec<&FemNode> =
588 elem.node_ids.iter().map(|&id| &mesh.nodes[id]).collect();
589 let area = 0.5
590 * ((nodes[1].x - nodes[0].x) * (nodes[2].y - nodes[0].y)
591 - (nodes[2].x - nodes[0].x) * (nodes[1].y - nodes[0].y))
592 .abs();
593 Ok(rho * area) }
595 ElementType::Quad2D => {
596 if elem.node_ids.len() < 4 {
597 return Err(PhysicsError::Simulation("Need 4 nodes for quad".into()));
598 }
599 let nodes: Vec<&FemNode> =
601 elem.node_ids.iter().map(|&id| &mesh.nodes[id]).collect();
602 let a1 = 0.5
603 * ((nodes[1].x - nodes[0].x) * (nodes[2].y - nodes[0].y)
604 - (nodes[2].x - nodes[0].x) * (nodes[1].y - nodes[0].y))
605 .abs();
606 let a2 = 0.5
607 * ((nodes[2].x - nodes[0].x) * (nodes[3].y - nodes[0].y)
608 - (nodes[3].x - nodes[0].x) * (nodes[2].y - nodes[0].y))
609 .abs();
610 Ok(rho * (a1 + a2))
611 }
612 }
613 }
614
615 fn inverse_power_iteration(
620 &self,
621 k: &[Vec<f64>],
622 m: &[Vec<f64>],
623 deflation_vecs: &[Vec<f64>],
624 ) -> Result<(f64, Vec<f64>, usize, bool), PhysicsError> {
625 let n = k.len();
626 if n == 0 {
627 return Err(PhysicsError::Simulation("Empty system".into()));
628 }
629
630 let mut a = vec![vec![0.0; n]; n];
632 for i in 0..n {
633 for j in 0..n {
634 a[i][j] = k[i][j] - self.config.shift * m[i][j];
635 }
636 }
637
638 let mut v: Vec<f64> = (0..n).map(|i| 1.0 + 0.1 * (i as f64)).collect();
640
641 for prev in deflation_vecs {
643 let dot_mv = mat_vec_dot(m, &v);
644 let dot_prev = dot(prev, &dot_mv);
645 let prev_m_prev = dot(prev, &mat_vec_dot(m, prev));
646 if prev_m_prev.abs() > 1e-30 {
647 let alpha = dot_prev / prev_m_prev;
648 for (vi, pi) in v.iter_mut().zip(prev.iter()) {
649 *vi -= alpha * pi;
650 }
651 }
652 }
653
654 let mut eigenvalue = 0.0;
655 let mut converged = false;
656
657 for iter in 0..self.config.max_iterations {
658 let w = mat_vec_dot(m, &v);
660
661 let mut a_copy = a.clone();
663 let mut w_copy = w.clone();
664 let x = gaussian_elimination_dense(&mut a_copy, &mut w_copy)
665 .ok_or_else(|| PhysicsError::Simulation("Singular matrix in modal solve".into()))?;
666
667 let mut x_deflated = x;
669 for prev in deflation_vecs {
670 let dot_mx = mat_vec_dot(m, &x_deflated);
671 let dot_prev = dot(prev, &dot_mx);
672 let prev_m_prev = dot(prev, &mat_vec_dot(m, prev));
673 if prev_m_prev.abs() > 1e-30 {
674 let alpha = dot_prev / prev_m_prev;
675 for (xi, pi) in x_deflated.iter_mut().zip(prev.iter()) {
676 *xi -= alpha * pi;
677 }
678 }
679 }
680
681 let kx = mat_vec_dot(k, &x_deflated);
683 let mx = mat_vec_dot(m, &x_deflated);
684 let xtk = dot(&x_deflated, &kx);
685 let xtm = dot(&x_deflated, &mx);
686
687 let new_eigenvalue = if xtm.abs() > 1e-30 { xtk / xtm } else { 0.0 };
688
689 let norm = dot(&x_deflated, &mx).abs().sqrt();
691 if norm > 1e-30 {
692 for xi in &mut x_deflated {
693 *xi /= norm;
694 }
695 }
696
697 if iter > 0
699 && (new_eigenvalue - eigenvalue).abs()
700 < self.config.tolerance * new_eigenvalue.abs().max(1.0)
701 {
702 eigenvalue = new_eigenvalue;
703 converged = true;
704 return Ok((eigenvalue, x_deflated, iter + 1, converged));
705 }
706
707 eigenvalue = new_eigenvalue;
708 v = x_deflated;
709 }
710
711 Ok((eigenvalue, v, self.config.max_iterations, converged))
712 }
713
714 fn normalise_mode(&self, shape: &[f64], m_global: &[Vec<f64>]) -> Vec<f64> {
716 let mut result = shape.to_vec();
717 match self.config.normalisation {
718 NormalisationMethod::MassNormalised => {
719 let mx = mat_vec_dot(m_global, &result);
720 let phi_m_phi = dot(&result, &mx);
721 if phi_m_phi.abs() > 1e-30 {
722 let scale = 1.0 / phi_m_phi.abs().sqrt();
723 for r in &mut result {
724 *r *= scale;
725 }
726 }
727 }
728 NormalisationMethod::MaxDisplacement => {
729 let max_abs = result.iter().map(|v| v.abs()).fold(0.0f64, f64::max);
730 if max_abs > 1e-30 {
731 for r in &mut result {
732 *r /= max_abs;
733 }
734 }
735 }
736 NormalisationMethod::None => {}
737 }
738 result
739 }
740
741 fn compute_participation_factors(
743 &self,
744 shape: &[f64],
745 m_red: &[Vec<f64>],
746 _n_nodes: usize,
747 ) -> ParticipationFactors {
748 let n = shape.len();
749 let mut px = 0.0;
750 let mut py = 0.0;
751
752 for (i, m_row) in m_red.iter().enumerate().take(n) {
754 let m_row_sum: f64 = (0..n)
755 .map(|j| m_row[j] * shape[j])
756 .collect::<Vec<f64>>()
757 .iter()
758 .sum();
759 if i % 2 == 0 {
760 px += m_row_sum;
761 } else {
762 py += m_row_sum;
763 }
764 }
765
766 ParticipationFactors { x: px, y: py }
767 }
768
769 pub fn config(&self) -> &ModalAnalysisConfig {
771 &self.config
772 }
773}
774
775fn dot(a: &[f64], b: &[f64]) -> f64 {
781 a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
782}
783
784fn mat_vec_dot(a: &[Vec<f64>], x: &[f64]) -> Vec<f64> {
786 a.iter()
787 .map(|row| row.iter().zip(x.iter()).map(|(a, b)| a * b).sum())
788 .collect()
789}
790
791fn gaussian_elimination_dense(a: &mut [Vec<f64>], b: &mut [f64]) -> Option<Vec<f64>> {
793 let n = b.len();
794 for col in 0..n {
795 let mut max_row = col;
796 let mut max_val = a[col][col].abs();
797 for (row, a_row) in a.iter().enumerate().skip(col + 1).take(n - col - 1) {
798 if a_row[col].abs() > max_val {
799 max_val = a_row[col].abs();
800 max_row = row;
801 }
802 }
803 if max_val < 1e-30 {
804 return None;
805 }
806 a.swap(col, max_row);
807 b.swap(col, max_row);
808
809 let pivot = a[col][col];
810 for row in (col + 1)..n {
811 let factor = a[row][col] / pivot;
812 let pivot_row: Vec<f64> = a[col][col..n].to_vec();
813 for (val, &pv) in a[row][col..n].iter_mut().zip(pivot_row.iter()) {
814 *val -= factor * pv;
815 }
816 b[row] -= factor * b[col];
817 }
818 }
819
820 let mut x = vec![0.0f64; n];
821 for i in (0..n).rev() {
822 x[i] = b[i];
823 for j in (i + 1)..n {
824 x[i] -= a[i][j] * x[j];
825 }
826 if a[i][i].abs() < 1e-30 {
827 return None;
828 }
829 x[i] /= a[i][i];
830 }
831 Some(x)
832}
833
834#[cfg(test)]
839mod tests {
840 use super::*;
841 use crate::fem::{DofType, ElementType, FemMaterial, FemMesh};
842
843 fn simple_bar_mesh() -> FemMesh {
845 let mat = FemMaterial {
846 youngs_modulus: 200e9,
847 poissons_ratio: 0.3,
848 thermal_conductivity: 50.0,
849 density: 7850.0,
850 };
851 let mut mesh = FemMesh::new();
852 let n0 = mesh.add_node(0.0, 0.0);
853 let n1 = mesh.add_node(1.0, 0.0);
854 let n2 = mesh.add_node(2.0, 0.0);
855
856 mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
857 mesh.add_element(vec![n0, n1], mat.clone(), ElementType::Bar1D);
858 mesh.add_element(vec![n1, n2], mat, ElementType::Bar1D);
859 mesh
860 }
861
862 fn single_bar_mesh() -> FemMesh {
864 let mat = FemMaterial {
865 youngs_modulus: 1e6,
866 poissons_ratio: 0.3,
867 thermal_conductivity: 50.0,
868 density: 1000.0,
869 };
870 let mut mesh = FemMesh::new();
871 let n0 = mesh.add_node(0.0, 0.0);
872 let n1 = mesh.add_node(1.0, 0.0);
873
874 mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
875 mesh.add_element(vec![n0, n1], mat, ElementType::Bar1D);
876 mesh
877 }
878
879 fn triangle_mesh() -> FemMesh {
881 let mat = FemMaterial {
882 youngs_modulus: 200e9,
883 poissons_ratio: 0.3,
884 thermal_conductivity: 50.0,
885 density: 7850.0,
886 };
887 let mut mesh = FemMesh::new();
888 let n0 = mesh.add_node(0.0, 0.0);
889 let n1 = mesh.add_node(1.0, 0.0);
890 let n2 = mesh.add_node(0.5, 1.0);
891
892 mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
893 mesh.set_boundary_condition(n1, DofType::Displacement, 0.0);
894 mesh.add_element(vec![n0, n1, n2], mat, ElementType::Triangle2D);
895 mesh
896 }
897
898 #[test]
899 fn test_default_config() {
900 let config = ModalAnalysisConfig::default();
901 assert_eq!(config.num_modes, 5);
902 assert_eq!(config.max_iterations, 1000);
903 assert_eq!(config.mass_type, MassMatrixType::Lumped);
904 }
905
906 #[test]
907 fn test_solver_creation() {
908 let solver = ModalAnalysisSolver::with_defaults();
909 assert_eq!(solver.config().num_modes, 5);
910 }
911
912 #[test]
913 fn test_empty_mesh_error() {
914 let solver = ModalAnalysisSolver::with_defaults();
915 let mesh = FemMesh::new();
916 let result = solver.solve(&mesh);
917 assert!(result.is_err());
918 }
919
920 #[test]
921 fn test_no_elements_error() {
922 let solver = ModalAnalysisSolver::with_defaults();
923 let mut mesh = FemMesh::new();
924 mesh.add_node(0.0, 0.0);
925 let result = solver.solve(&mesh);
926 assert!(result.is_err());
927 }
928
929 #[test]
930 fn test_single_bar_modal_analysis() {
931 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
932 num_modes: 1,
933 max_iterations: 500,
934 tolerance: 1e-6,
935 ..Default::default()
936 });
937 let mesh = single_bar_mesh();
938 let result = solver.solve(&mesh).expect("solve failed");
939
940 assert_eq!(result.modes.len(), 1);
941 assert!(
942 result.modes[0].frequency_hz > 0.0,
943 "Frequency should be positive"
944 );
945 assert!(result.modes[0].converged, "Mode should converge");
946 assert_eq!(result.total_dofs, 4); assert_eq!(result.constrained_dofs, 2); }
949
950 #[test]
951 fn test_two_bar_modal_analysis() {
952 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
953 num_modes: 2,
954 max_iterations: 500,
955 tolerance: 1e-6,
956 ..Default::default()
957 });
958 let mesh = simple_bar_mesh();
959 let result = solver.solve(&mesh).expect("solve failed");
960
961 assert_eq!(result.modes.len(), 2);
962 assert!(result.modes[0].frequency_hz <= result.modes[1].frequency_hz + 1e-3);
964 assert!(result.total_mass > 0.0);
965 }
966
967 #[test]
968 fn test_triangle_modal_analysis() {
969 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
970 num_modes: 1,
971 max_iterations: 500,
972 tolerance: 1e-4,
973 ..Default::default()
974 });
975 let mesh = triangle_mesh();
976 let result = solver.solve(&mesh).expect("solve failed");
977
978 assert!(!result.modes.is_empty());
979 assert!(result.modes[0].frequency_hz > 0.0);
980 }
981
982 #[test]
983 fn test_mode_shape_length() {
984 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
985 num_modes: 1,
986 ..Default::default()
987 });
988 let mesh = simple_bar_mesh();
989 let result = solver.solve(&mesh).expect("solve failed");
990
991 assert_eq!(result.modes[0].mode_shape.len(), result.total_dofs);
993 }
994
995 #[test]
996 fn test_constrained_dofs_count() {
997 let mesh = simple_bar_mesh();
998 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
999 num_modes: 1,
1000 ..Default::default()
1001 });
1002 let result = solver.solve(&mesh).expect("solve failed");
1003 assert_eq!(result.constrained_dofs, 2); }
1005
1006 #[test]
1007 fn test_angular_frequency_relation() {
1008 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
1009 num_modes: 1,
1010 ..Default::default()
1011 });
1012 let mesh = single_bar_mesh();
1013 let result = solver.solve(&mesh).expect("solve failed");
1014 let mode = &result.modes[0];
1015
1016 let expected_omega = mode.frequency_hz * 2.0 * std::f64::consts::PI;
1017 assert!((mode.angular_frequency - expected_omega).abs() < 1e-6);
1018 }
1019
1020 #[test]
1021 fn test_period_relation() {
1022 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
1023 num_modes: 1,
1024 ..Default::default()
1025 });
1026 let mesh = single_bar_mesh();
1027 let result = solver.solve(&mesh).expect("solve failed");
1028 let mode = &result.modes[0];
1029
1030 if mode.frequency_hz > 1e-10 {
1031 let expected_period = 1.0 / mode.frequency_hz;
1032 assert!((mode.period - expected_period).abs() / expected_period < 1e-6);
1033 }
1034 }
1035
1036 #[test]
1037 fn test_lumped_mass() {
1038 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
1039 num_modes: 1,
1040 mass_type: MassMatrixType::Lumped,
1041 ..Default::default()
1042 });
1043 let mesh = single_bar_mesh();
1044 let result = solver.solve(&mesh).expect("solve failed");
1045 assert!(result.total_mass > 0.0);
1046 }
1047
1048 #[test]
1049 fn test_consistent_mass() {
1050 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
1051 num_modes: 1,
1052 mass_type: MassMatrixType::Consistent,
1053 ..Default::default()
1054 });
1055 let mesh = single_bar_mesh();
1056 let result = solver.solve(&mesh).expect("solve failed");
1057 assert!(result.total_mass > 0.0);
1058 }
1059
1060 #[test]
1061 fn test_max_displacement_normalisation() {
1062 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
1063 num_modes: 1,
1064 normalisation: NormalisationMethod::MaxDisplacement,
1065 ..Default::default()
1066 });
1067 let mesh = single_bar_mesh();
1068 let result = solver.solve(&mesh).expect("solve failed");
1069 let max_abs = result.modes[0]
1070 .mode_shape
1071 .iter()
1072 .map(|v| v.abs())
1073 .fold(0.0f64, f64::max);
1074 assert!(max_abs <= 1.0 + 1e-10);
1076 }
1077
1078 #[test]
1079 fn test_no_normalisation() {
1080 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
1081 num_modes: 1,
1082 normalisation: NormalisationMethod::None,
1083 ..Default::default()
1084 });
1085 let mesh = single_bar_mesh();
1086 let result = solver.solve(&mesh).expect("solve failed");
1087 assert!(!result.modes.is_empty());
1088 }
1089
1090 #[test]
1091 fn test_participation_factors() {
1092 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
1093 num_modes: 1,
1094 ..Default::default()
1095 });
1096 let mesh = single_bar_mesh();
1097 let result = solver.solve(&mesh).expect("solve failed");
1098 assert!(result.modes[0].participation_factors.x.is_finite());
1100 assert!(result.modes[0].participation_factors.y.is_finite());
1101 }
1102
1103 #[test]
1104 fn test_effective_modal_mass() {
1105 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
1106 num_modes: 2,
1107 ..Default::default()
1108 });
1109 let mesh = simple_bar_mesh();
1110 let result = solver.solve(&mesh).expect("solve failed");
1111
1112 for mode in &result.modes {
1113 assert!(mode.effective_modal_mass.is_finite());
1114 assert!(mode.effective_modal_mass >= 0.0);
1115 }
1116 }
1117
1118 #[test]
1119 fn test_mac_identity() {
1120 let v1 = vec![1.0, 0.0, 0.0];
1122 let v2 = vec![0.0, 1.0, 0.0];
1123 let modes = vec![v1, v2];
1124
1125 let mac = compute_mac_matrix(&modes, &modes).expect("MAC failed");
1126 assert!((mac[0][0] - 1.0).abs() < 1e-10);
1127 assert!((mac[1][1] - 1.0).abs() < 1e-10);
1128 assert!(mac[0][1].abs() < 1e-10); }
1130
1131 #[test]
1132 fn test_mac_same_mode() {
1133 let v = vec![1.0, 2.0, 3.0];
1134 let v2 = v.clone();
1135 let mac = compute_mac_matrix(std::slice::from_ref(&v), &[v2]).expect("MAC failed");
1136 assert!((mac[0][0] - 1.0).abs() < 1e-10);
1137 }
1138
1139 #[test]
1140 fn test_mac_empty_error() {
1141 let result = compute_mac_matrix(&[], &[vec![1.0]]);
1142 assert!(result.is_err());
1143 }
1144
1145 #[test]
1146 fn test_mac_mismatched_lengths() {
1147 let result = compute_mac_matrix(&[vec![1.0, 2.0]], &[vec![1.0]]);
1148 assert!(result.is_err());
1149 }
1150
1151 #[test]
1152 fn test_mac_parallel_modes() {
1153 let v1 = vec![1.0, 2.0];
1154 let v2 = vec![2.0, 4.0]; let mac = compute_mac_matrix(&[v1], &[v2]).expect("MAC failed");
1156 assert!((mac[0][0] - 1.0).abs() < 1e-10);
1157 }
1158
1159 #[test]
1160 fn test_dot_product() {
1161 assert!((dot(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]) - 32.0).abs() < 1e-10);
1162 }
1163
1164 #[test]
1165 fn test_mat_vec_product() {
1166 let m = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
1167 let v = vec![1.0, 1.0];
1168 let result = mat_vec_dot(&m, &v);
1169 assert!((result[0] - 3.0).abs() < 1e-10);
1170 assert!((result[1] - 7.0).abs() < 1e-10);
1171 }
1172
1173 #[test]
1174 fn test_gaussian_elimination_simple() {
1175 let mut a = vec![vec![2.0, 3.0], vec![1.0, 1.0]];
1177 let mut b = vec![8.0, 3.0];
1178 let x = gaussian_elimination_dense(&mut a, &mut b).expect("solve failed");
1179 assert!((x[0] - 1.0).abs() < 1e-10);
1180 assert!((x[1] - 2.0).abs() < 1e-10);
1181 }
1182
1183 #[test]
1184 fn test_gaussian_elimination_singular() {
1185 let mut a = vec![vec![1.0, 2.0], vec![2.0, 4.0]];
1186 let mut b = vec![3.0, 6.0];
1187 let result = gaussian_elimination_dense(&mut a, &mut b);
1188 assert!(result.is_none());
1189 }
1190
1191 #[test]
1192 fn test_all_constrained_error() {
1193 let mat = FemMaterial::default();
1194 let mut mesh = FemMesh::new();
1195 let n0 = mesh.add_node(0.0, 0.0);
1196 let n1 = mesh.add_node(1.0, 0.0);
1197 mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
1198 mesh.set_boundary_condition(n1, DofType::Displacement, 0.0);
1199 mesh.add_element(vec![n0, n1], mat, ElementType::Bar1D);
1200
1201 let solver = ModalAnalysisSolver::with_defaults();
1202 let result = solver.solve(&mesh);
1203 assert!(result.is_err());
1204 }
1205
1206 #[test]
1207 fn test_multiple_modes_increasing_frequency() {
1208 let mat = FemMaterial {
1209 youngs_modulus: 200e9,
1210 density: 7850.0,
1211 ..Default::default()
1212 };
1213 let mut mesh = FemMesh::new();
1214 let mut nodes = Vec::new();
1216 for i in 0..5 {
1217 nodes.push(mesh.add_node(i as f64, 0.0));
1218 }
1219 mesh.set_boundary_condition(nodes[0], DofType::Displacement, 0.0);
1220 for i in 0..4 {
1221 mesh.add_element(
1222 vec![nodes[i], nodes[i + 1]],
1223 mat.clone(),
1224 ElementType::Bar1D,
1225 );
1226 }
1227
1228 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
1229 num_modes: 3,
1230 max_iterations: 2000,
1231 tolerance: 1e-4,
1232 ..Default::default()
1233 });
1234 let result = solver.solve(&mesh).expect("solve failed");
1235 assert_eq!(result.modes.len(), 3);
1236 }
1237
1238 #[test]
1239 fn test_result_serialization() {
1240 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
1241 num_modes: 1,
1242 ..Default::default()
1243 });
1244 let mesh = single_bar_mesh();
1245 let result = solver.solve(&mesh).expect("solve failed");
1246
1247 let json = serde_json::to_string(&result).expect("serialize failed");
1248 assert!(json.contains("frequency_hz"));
1249 assert!(json.contains("mode_shape"));
1250 }
1251
1252 #[test]
1253 fn test_config_serialization() {
1254 let config = ModalAnalysisConfig::default();
1255 let json = serde_json::to_string(&config).expect("serialize failed");
1256 let deser: ModalAnalysisConfig = serde_json::from_str(&json).expect("deser failed");
1257 assert_eq!(deser.num_modes, config.num_modes);
1258 }
1259
1260 #[test]
1261 fn test_vibrational_mode_serialization() {
1262 let mode = VibrationalMode {
1263 mode_number: 0,
1264 frequency_hz: 100.0,
1265 angular_frequency: 628.318,
1266 period: 0.01,
1267 mode_shape: vec![0.0, 0.0, 1.0, 0.5],
1268 iterations: 42,
1269 converged: true,
1270 participation_factors: ParticipationFactors { x: 0.8, y: 0.2 },
1271 effective_modal_mass: 0.65,
1272 };
1273 let json = serde_json::to_string(&mode).expect("serialize failed");
1274 assert!(json.contains("\"mode_number\":0"));
1275 }
1276
1277 #[test]
1278 fn test_shifted_iteration() {
1279 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
1280 num_modes: 1,
1281 shift: 100.0, ..Default::default()
1283 });
1284 let mesh = single_bar_mesh();
1285 let result = solver.solve(&mesh).expect("solve failed");
1286 assert!(!result.modes.is_empty());
1287 }
1288
1289 #[test]
1290 fn test_total_mass_positive() {
1291 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
1292 num_modes: 1,
1293 ..Default::default()
1294 });
1295 let mesh = simple_bar_mesh();
1296 let result = solver.solve(&mesh).expect("solve failed");
1297 assert!(result.total_mass > 0.0);
1298 }
1299
1300 #[test]
1301 fn test_cumulative_mass_fraction() {
1302 let solver = ModalAnalysisSolver::new(ModalAnalysisConfig {
1303 num_modes: 2,
1304 ..Default::default()
1305 });
1306 let mesh = simple_bar_mesh();
1307 let result = solver.solve(&mesh).expect("solve failed");
1308 assert!(result.cumulative_mass_fraction.is_finite());
1309 }
1310}