1use super::EngineeringError;
33use crate::solvers::linear_algebra::lu::lu_decompose;
34
35#[derive(Debug, Clone, Copy)]
39pub struct FeNode {
40 pub x: f64,
41 pub y: f64,
42}
43
44#[derive(Debug, Clone, Copy)]
47pub enum FeElement {
48 Truss {
51 ni: usize,
52 nj: usize,
53 e: f64,
54 area: f64,
55 rho: f64,
56 },
57 Frame {
60 ni: usize,
61 nj: usize,
62 e: f64,
63 area: f64,
64 inertia: f64,
65 rho: f64,
66 },
67}
68
69impl FeElement {
70 fn nodes(&self) -> (usize, usize) {
71 match *self {
72 FeElement::Truss { ni, nj, .. } | FeElement::Frame { ni, nj, .. } => (ni, nj),
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
80pub struct FeModel {
81 pub nodes: Vec<FeNode>,
82 pub elements: Vec<FeElement>,
83 pub constraints: Vec<(usize, f64)>,
85 pub loads: Vec<(usize, f64)>,
87}
88
89impl FeModel {
90 pub fn ndof(&self) -> usize {
92 self.nodes.len() * 3
93 }
94
95 fn geom(&self, el: &FeElement) -> Result<(f64, f64, f64, f64, f64), EngineeringError> {
98 let (ni, nj) = el.nodes();
99 let n = self.nodes.len();
100 if ni >= n || nj >= n {
101 return Err(EngineeringError::ValidationError(format!(
102 "element references node {ni}/{nj} but model has {n} nodes"
103 )));
104 }
105 let a = self.nodes[ni];
106 let b = self.nodes[nj];
107 let dx = b.x - a.x;
108 let dy = b.y - a.y;
109 let len = (dx * dx + dy * dy).sqrt();
110 if !(len > 0.0) {
111 return Err(EngineeringError::ValidationError(
112 "element has zero length".to_string(),
113 ));
114 }
115 Ok((dx, dy, len, dx / len, dy / len))
116 }
117}
118
119fn truss_stiffness(
125 model: &FeModel,
126 el: &FeElement,
127) -> Result<(Vec<usize>, Vec<f64>), EngineeringError> {
128 let (e, area) = match *el {
129 FeElement::Truss { e, area, .. } => (e, area),
130 _ => unreachable!(),
131 };
132 let (ni, nj) = el.nodes();
133 let (_dx, _dy, len, c, s) = model.geom(el)?;
134 let k = e * area / len;
135 let (cc, cs, ss) = (c * c, c * s, s * s);
136 #[rustfmt::skip]
137 let ke = vec![
138 k*cc, k*cs, -k*cc, -k*cs,
139 k*cs, k*ss, -k*cs, -k*ss,
140 -k*cc, -k*cs, k*cc, k*cs,
141 -k*cs, -k*ss, k*cs, k*ss,
142 ];
143 let dofs = vec![3 * ni, 3 * ni + 1, 3 * nj, 3 * nj + 1];
144 Ok((dofs, ke))
145}
146
147fn frame_dofs(el: &FeElement) -> Vec<usize> {
149 let (ni, nj) = el.nodes();
150 vec![
151 3 * ni,
152 3 * ni + 1,
153 3 * ni + 2,
154 3 * nj,
155 3 * nj + 1,
156 3 * nj + 2,
157 ]
158}
159
160fn frame_transform(c: f64, s: f64) -> Vec<f64> {
163 #[rustfmt::skip]
164 let t = vec![
165 c, s, 0.0, 0.0, 0.0, 0.0,
166 -s, c, 0.0, 0.0, 0.0, 0.0,
167 0.0,0.0,1.0, 0.0, 0.0, 0.0,
168 0.0,0.0,0.0, c, s, 0.0,
169 0.0,0.0,0.0, -s, c, 0.0,
170 0.0,0.0,0.0, 0.0, 0.0, 1.0,
171 ];
172 t
173}
174
175fn congruence_6(t: &[f64], a: &[f64]) -> Vec<f64> {
177 let mut tmp = vec![0.0_f64; 36];
179 for i in 0..6 {
180 for j in 0..6 {
181 let mut acc = 0.0;
182 for p in 0..6 {
183 acc += t[p * 6 + i] * a[p * 6 + j]; }
185 tmp[i * 6 + j] = acc;
186 }
187 }
188 let mut out = vec![0.0_f64; 36];
190 for i in 0..6 {
191 for j in 0..6 {
192 let mut acc = 0.0;
193 for p in 0..6 {
194 acc += tmp[i * 6 + p] * t[p * 6 + j];
195 }
196 out[i * 6 + j] = acc;
197 }
198 }
199 out
200}
201
202fn frame_stiffness(
205 model: &FeModel,
206 el: &FeElement,
207) -> Result<(Vec<usize>, Vec<f64>), EngineeringError> {
208 let (e, area, inertia) = match *el {
209 FeElement::Frame {
210 e, area, inertia, ..
211 } => (e, area, inertia),
212 _ => unreachable!(),
213 };
214 let (_dx, _dy, l, c, s) = model.geom(el)?;
215 let ea_l = e * area / l;
216 let ei = e * inertia;
217 let (l2, l3) = (l * l, l * l * l);
218 let (b12, b6, b4, b2) = (12.0 * ei / l3, 6.0 * ei / l2, 4.0 * ei / l, 2.0 * ei / l);
219 #[rustfmt::skip]
221 let kl = vec![
222 ea_l, 0.0, 0.0, -ea_l, 0.0, 0.0,
223 0.0, b12, b6, 0.0, -b12, b6,
224 0.0, b6, b4, 0.0, -b6, b2,
225 -ea_l, 0.0, 0.0, ea_l, 0.0, 0.0,
226 0.0, -b12, -b6, 0.0, b12, -b6,
227 0.0, b6, b2, 0.0, -b6, b4,
228 ];
229 let t = frame_transform(c, s);
230 let ke = congruence_6(&t, &kl);
231 Ok((frame_dofs(el), ke))
232}
233
234fn frame_mass_consistent(
237 model: &FeModel,
238 el: &FeElement,
239) -> Result<(Vec<usize>, Vec<f64>), EngineeringError> {
240 let (area, inertia, rho) = match *el {
241 FeElement::Frame {
242 area, inertia, rho, ..
243 } => (area, inertia, rho),
244 _ => unreachable!(),
245 };
246 let _ = inertia;
247 let (_dx, _dy, l, c, s) = model.geom(el)?;
248 let m = rho * area * l;
249 let ax = m / 6.0;
250 let (l2, mb) = (l * l, m / 420.0);
251 #[rustfmt::skip]
253 let ml = vec![
254 2.0*ax, 0.0, 0.0, 1.0*ax, 0.0, 0.0,
255 0.0, 156.0*mb, 22.0*l*mb, 0.0, 54.0*mb, -13.0*l*mb,
256 0.0, 22.0*l*mb, 4.0*l2*mb, 0.0, 13.0*l*mb, -3.0*l2*mb,
257 1.0*ax, 0.0, 0.0, 2.0*ax, 0.0, 0.0,
258 0.0, 54.0*mb, 13.0*l*mb, 0.0, 156.0*mb, -22.0*l*mb,
259 0.0, -13.0*l*mb, -3.0*l2*mb, 0.0, -22.0*l*mb, 4.0*l2*mb,
260 ];
261 let t = frame_transform(c, s);
262 let me = congruence_6(&t, &ml);
263 Ok((frame_dofs(el), me))
264}
265
266fn truss_mass_lumped(
269 model: &FeModel,
270 el: &FeElement,
271) -> Result<(Vec<usize>, Vec<f64>), EngineeringError> {
272 let (area, rho) = match *el {
273 FeElement::Truss { area, rho, .. } => (area, rho),
274 _ => unreachable!(),
275 };
276 let (ni, nj) = el.nodes();
277 let (_dx, _dy, len, _c, _s) = model.geom(el)?;
278 let half = rho * area * len / 2.0;
279 #[rustfmt::skip]
280 let me = vec![
281 half, 0.0, 0.0, 0.0,
282 0.0, half, 0.0, 0.0,
283 0.0, 0.0, half, 0.0,
284 0.0, 0.0, 0.0, half,
285 ];
286 let dofs = vec![3 * ni, 3 * ni + 1, 3 * nj, 3 * nj + 1];
287 Ok((dofs, me))
288}
289
290fn scatter(global: &mut [f64], n: usize, dofs: &[usize], ke: &[f64]) {
294 let d = dofs.len();
295 for a in 0..d {
296 for b in 0..d {
297 global[dofs[a] * n + dofs[b]] += ke[a * d + b];
298 }
299 }
300}
301
302pub fn assemble_stiffness(model: &FeModel) -> Result<Vec<f64>, EngineeringError> {
304 let n = model.ndof();
305 if n == 0 {
306 return Err(EngineeringError::InsufficientData(
307 "FE model has no nodes".to_string(),
308 ));
309 }
310 let mut k = vec![0.0_f64; n * n];
311 for el in &model.elements {
312 let (dofs, ke) = match el {
313 FeElement::Truss { .. } => truss_stiffness(model, el)?,
314 FeElement::Frame { .. } => frame_stiffness(model, el)?,
315 };
316 scatter(&mut k, n, &dofs, &ke);
317 }
318 Ok(k)
319}
320
321pub fn assemble_mass(model: &FeModel) -> Result<Vec<f64>, EngineeringError> {
324 let n = model.ndof();
325 if n == 0 {
326 return Err(EngineeringError::InsufficientData(
327 "FE model has no nodes".to_string(),
328 ));
329 }
330 let mut mm = vec![0.0_f64; n * n];
331 for el in &model.elements {
332 let (dofs, me) = match el {
333 FeElement::Truss { .. } => truss_mass_lumped(model, el)?,
334 FeElement::Frame { .. } => frame_mass_consistent(model, el)?,
335 };
336 scatter(&mut mm, n, &dofs, &me);
337 }
338 Ok(mm)
339}
340
341pub fn assemble_loads(model: &FeModel) -> Vec<f64> {
343 let n = model.ndof();
344 let mut f = vec![0.0_f64; n];
345 for &(dof, val) in &model.loads {
346 if dof < n {
347 f[dof] += val;
348 }
349 }
350 f
351}
352
353#[derive(Debug, Clone)]
357pub struct FeStaticResult {
358 pub displacements: Vec<f64>,
360 pub reactions: Vec<(usize, f64)>,
362 pub element_axial_force: Vec<f64>,
364}
365
366pub fn solve_static(model: &FeModel) -> Result<FeStaticResult, EngineeringError> {
370 let n = model.ndof();
371 let k = assemble_stiffness(model)?;
372 let f = assemble_loads(model);
373 let u = solve_with_bcs(&k, &f, n, &model.constraints)?;
374
375 let mut reactions = Vec::with_capacity(model.constraints.len());
377 for &(c, _) in &model.constraints {
378 let mut r = -f[c];
379 for j in 0..n {
380 r += k[c * n + j] * u[j];
381 }
382 reactions.push((c, r));
383 }
384
385 let mut element_axial_force = Vec::with_capacity(model.elements.len());
387 for el in &model.elements {
388 element_axial_force.push(element_axial_force_of(model, el, &u)?);
389 }
390
391 Ok(FeStaticResult {
392 displacements: u,
393 reactions,
394 element_axial_force,
395 })
396}
397
398fn solve_with_bcs(
401 k: &[f64],
402 f: &[f64],
403 n: usize,
404 constraints: &[(usize, f64)],
405) -> Result<Vec<f64>, EngineeringError> {
406 let mut is_fixed = vec![false; n];
407 let mut fixed_val = vec![0.0_f64; n];
408 for &(dof, val) in constraints {
409 if dof >= n {
410 return Err(EngineeringError::ValidationError(format!(
411 "constraint DOF {dof} out of range (ndof {n})"
412 )));
413 }
414 is_fixed[dof] = true;
415 fixed_val[dof] = val;
416 }
417 let free: Vec<usize> = (0..n).filter(|&d| !is_fixed[d]).collect();
418 let nf = free.len();
419 if nf == 0 {
420 return Ok(fixed_val);
422 }
423
424 let mut kff = vec![0.0_f64; nf * nf];
426 let mut rhs = vec![0.0_f64; nf];
427 for (ii, &fi) in free.iter().enumerate() {
428 rhs[ii] = f[fi];
429 for (jj, &fj) in free.iter().enumerate() {
430 kff[ii * nf + jj] = k[fi * n + fj];
431 }
432 for &(cd, cv) in constraints {
433 rhs[ii] -= k[fi * n + cd] * cv;
434 }
435 }
436
437 let lu = lu_decompose(nf, &kff)
438 .map_err(|e| EngineeringError::SolverError(format!("LU factorization failed: {e:?}")))?;
439 let uf = lu.solve(&rhs).ok_or_else(|| {
440 EngineeringError::SolverError(
441 "reduced stiffness matrix is singular (under-constrained model?)".to_string(),
442 )
443 })?;
444
445 let mut u = fixed_val;
446 for (ii, &fi) in free.iter().enumerate() {
447 u[fi] = uf[ii];
448 }
449 Ok(u)
450}
451
452fn element_axial_force_of(
456 model: &FeModel,
457 el: &FeElement,
458 u: &[f64],
459) -> Result<f64, EngineeringError> {
460 let (ni, nj) = el.nodes();
461 let (_dx, _dy, len, c, s) = model.geom(el)?;
462 let (e, area) = match *el {
463 FeElement::Truss { e, area, .. } => (e, area),
464 FeElement::Frame { e, area, .. } => (e, area),
465 };
466 let uix = u[3 * ni];
467 let uiy = u[3 * ni + 1];
468 let ujx = u[3 * nj];
469 let ujy = u[3 * nj + 1];
470 let elong = (ujx - uix) * c + (ujy - uiy) * s;
471 Ok(e * area / len * elong)
472}
473
474fn matvec(a: &[f64], x: &[f64], n: usize) -> Vec<f64> {
478 let mut y = vec![0.0_f64; n];
479 for i in 0..n {
480 let mut acc = 0.0;
481 let row = i * n;
482 for j in 0..n {
483 acc += a[row + j] * x[j];
484 }
485 y[i] = acc;
486 }
487 y
488}
489
490fn axpy_into(dst: &mut [f64], a: f64, x: &[f64]) {
491 for (d, &xi) in dst.iter_mut().zip(x.iter()) {
492 *d += a * xi;
493 }
494}
495
496fn norm2(v: &[f64]) -> f64 {
497 v.iter().map(|&x| x * x).sum::<f64>().sqrt()
498}
499
500#[derive(Debug, Clone)]
504pub struct NewmarkResult {
505 pub time: Vec<f64>,
507 pub disp: Vec<Vec<f64>>,
509 pub vel: Vec<Vec<f64>>,
511 pub acc: Vec<Vec<f64>>,
513}
514
515impl NewmarkResult {
516 pub fn peak_abs(&self, d: usize) -> f64 {
518 self.disp
519 .iter()
520 .map(|u| u.get(d).copied().unwrap_or(0.0).abs())
521 .fold(0.0_f64, f64::max)
522 }
523}
524
525#[allow(clippy::too_many_arguments)]
530pub fn newmark_linear(
531 m: &[f64],
532 c: &[f64],
533 k: &[f64],
534 n: usize,
535 force: impl Fn(f64) -> Vec<f64>,
536 u0: &[f64],
537 v0: &[f64],
538 dt: f64,
539 nsteps: usize,
540 beta: f64,
541 gamma: f64,
542) -> Result<NewmarkResult, EngineeringError> {
543 if n == 0 {
544 return Err(EngineeringError::InsufficientData("zero-DOF system".into()));
545 }
546 if !(dt > 0.0) || nsteps == 0 {
547 return Err(EngineeringError::ValidationError(
548 "dt and nsteps must be positive".into(),
549 ));
550 }
551 if !(beta > 0.0) {
552 return Err(EngineeringError::ValidationError(
553 "Newmark β must be > 0".into(),
554 ));
555 }
556 if m.len() != n * n || c.len() != n * n || k.len() != n * n {
557 return Err(EngineeringError::ValidationError(
558 "M, C, K must each be n×n".into(),
559 ));
560 }
561
562 let a0 = 1.0 / (beta * dt * dt);
564 let a1 = gamma / (beta * dt);
565 let a2 = 1.0 / (beta * dt);
566 let a3 = 1.0 / (2.0 * beta) - 1.0;
567 let a4 = gamma / beta - 1.0;
568 let a5 = dt / 2.0 * (gamma / beta - 2.0);
569 let a6 = dt * (1.0 - gamma);
570 let a7 = gamma * dt;
571
572 let mut keff = k.to_vec();
574 for i in 0..n * n {
575 keff[i] += a0 * m[i] + a1 * c[i];
576 }
577 let keff_lu = lu_decompose(n, &keff).map_err(|e| {
578 EngineeringError::SolverError(format!("Newmark effective-stiffness LU failed: {e:?}"))
579 })?;
580
581 let mut u = u0.to_vec();
582 let mut v = v0.to_vec();
583 let f0 = force(0.0);
585 let cv = matvec(c, &v, n);
586 let ku = matvec(k, &u, n);
587 let mut rhs0 = vec![0.0_f64; n];
588 for i in 0..n {
589 rhs0[i] = f0[i] - cv[i] - ku[i];
590 }
591 let m_lu = lu_decompose(n, m).map_err(|e| {
592 EngineeringError::SolverError(format!("Newmark mass-matrix LU failed: {e:?}"))
593 })?;
594 let mut a = m_lu
595 .solve(&rhs0)
596 .ok_or_else(|| EngineeringError::SolverError("mass matrix is singular".to_string()))?;
597
598 let mut out = NewmarkResult {
599 time: Vec::with_capacity(nsteps + 1),
600 disp: Vec::with_capacity(nsteps + 1),
601 vel: Vec::with_capacity(nsteps + 1),
602 acc: Vec::with_capacity(nsteps + 1),
603 };
604 out.time.push(0.0);
605 out.disp.push(u.clone());
606 out.vel.push(v.clone());
607 out.acc.push(a.clone());
608
609 for step in 0..nsteps {
610 let t_next = (step + 1) as f64 * dt;
611 let mut mvec = vec![0.0_f64; n];
613 let mut cvec = vec![0.0_f64; n];
614 for i in 0..n {
615 mvec[i] = a0 * u[i] + a2 * v[i] + a3 * a[i];
616 cvec[i] = a1 * u[i] + a4 * v[i] + a5 * a[i];
617 }
618 let mm = matvec(m, &mvec, n);
619 let cc = matvec(c, &cvec, n);
620 let fext = force(t_next);
621 let mut feff = vec![0.0_f64; n];
622 for i in 0..n {
623 feff[i] = fext[i] + mm[i] + cc[i];
624 }
625 let u_new = keff_lu.solve(&feff).ok_or_else(|| {
626 EngineeringError::SolverError("Newmark step solve failed (singular)".to_string())
627 })?;
628 let mut a_new = vec![0.0_f64; n];
630 for i in 0..n {
631 a_new[i] = a0 * (u_new[i] - u[i]) - a2 * v[i] - a3 * a[i];
632 }
633 let mut v_new = vec![0.0_f64; n];
634 for i in 0..n {
635 v_new[i] = v[i] + a6 * a[i] + a7 * a_new[i];
636 }
637 u = u_new;
638 v = v_new;
639 a = a_new;
640 out.time.push(t_next);
641 out.disp.push(u.clone());
642 out.vel.push(v.clone());
643 out.acc.push(a.clone());
644 }
645 Ok(out)
646}
647
648pub fn newton_raphson(
656 n: usize,
657 residual: impl Fn(&[f64]) -> Vec<f64>,
658 tangent: impl Fn(&[f64]) -> Vec<f64>,
659 u0: &[f64],
660 tol: f64,
661 max_iter: usize,
662) -> Result<(Vec<f64>, usize), EngineeringError> {
663 let mut u = u0.to_vec();
664 for it in 0..max_iter {
665 let r = residual(&u);
666 if norm2(&r) <= tol {
667 return Ok((u, it));
668 }
669 let kt = tangent(&u);
670 let lu = lu_decompose(n, &kt).map_err(|e| {
671 EngineeringError::SolverError(format!("Newton tangent LU failed: {e:?}"))
672 })?;
673 let neg_r: Vec<f64> = r.iter().map(|&x| -x).collect();
675 let du = lu.solve(&neg_r).ok_or_else(|| {
676 EngineeringError::SolverError("Newton tangent is singular".to_string())
677 })?;
678 axpy_into(&mut u, 1.0, &du);
679 if norm2(&du) <= tol {
680 if norm2(&residual(&u)) <= tol.max(1e-8) {
682 return Ok((u, it + 1));
683 }
684 }
685 }
686 Err(EngineeringError::ConvergenceError(format!(
687 "Newton–Raphson did not converge in {max_iter} iterations"
688 )))
689}
690
691#[allow(clippy::too_many_arguments)]
698pub fn newmark_nonlinear(
699 m: &[f64],
700 c: &[f64],
701 n: usize,
702 internal: impl Fn(&[f64]) -> Vec<f64>,
703 tangent: impl Fn(&[f64]) -> Vec<f64>,
704 force: impl Fn(f64) -> Vec<f64>,
705 u0: &[f64],
706 v0: &[f64],
707 dt: f64,
708 nsteps: usize,
709 beta: f64,
710 gamma: f64,
711 tol: f64,
712 max_iter: usize,
713) -> Result<NewmarkResult, EngineeringError> {
714 if n == 0 {
715 return Err(EngineeringError::InsufficientData("zero-DOF system".into()));
716 }
717 if !(dt > 0.0) || nsteps == 0 || !(beta > 0.0) {
718 return Err(EngineeringError::ValidationError(
719 "dt, nsteps, β must be positive".into(),
720 ));
721 }
722
723 let a0 = 1.0 / (beta * dt * dt);
724 let a1 = gamma / (beta * dt);
725 let a2 = 1.0 / (beta * dt);
726 let a3 = 1.0 / (2.0 * beta) - 1.0;
727 let a6 = dt * (1.0 - gamma);
728 let a7 = gamma * dt;
729
730 let mut u = u0.to_vec();
731 let mut v = v0.to_vec();
732 let f0 = force(0.0);
734 let cv = matvec(c, &v, n);
735 let fi = internal(&u);
736 let rhs0: Vec<f64> = (0..n).map(|i| f0[i] - cv[i] - fi[i]).collect();
737 let m_lu = lu_decompose(n, m)
738 .map_err(|e| EngineeringError::SolverError(format!("mass LU failed: {e:?}")))?;
739 let mut a = m_lu
740 .solve(&rhs0)
741 .ok_or_else(|| EngineeringError::SolverError("mass matrix singular".into()))?;
742
743 let mut out = NewmarkResult {
744 time: vec![0.0],
745 disp: vec![u.clone()],
746 vel: vec![v.clone()],
747 acc: vec![a.clone()],
748 };
749
750 for step in 0..nsteps {
751 let t_next = (step + 1) as f64 * dt;
752 let fext = force(t_next);
753 let u_n = u.clone();
755 let v_n = v.clone();
756 let a_n = a.clone();
757 let mut u_trial = u_n.clone();
758
759 let mut converged = false;
760 for _ in 0..max_iter {
761 let a_u: Vec<f64> = (0..n)
764 .map(|i| a0 * (u_trial[i] - u_n[i]) - a2 * v_n[i] - a3 * a_n[i])
765 .collect();
766 let v_u: Vec<f64> = (0..n).map(|i| v_n[i] + a6 * a_n[i] + a7 * a_u[i]).collect();
767 let ma = matvec(m, &a_u, n);
768 let cvv = matvec(c, &v_u, n);
769 let fi_u = internal(&u_trial);
770 let g: Vec<f64> = (0..n).map(|i| ma[i] + cvv[i] + fi_u[i] - fext[i]).collect();
771 if norm2(&g) <= tol {
772 converged = true;
773 break;
774 }
775 let kt = tangent(&u_trial);
777 let mut keff = kt;
778 for i in 0..n * n {
779 keff[i] += a0 * m[i] + a1 * c[i];
780 }
781 let lu = lu_decompose(n, &keff).map_err(|e| {
782 EngineeringError::SolverError(format!("nonlinear-dynamic tangent LU failed: {e:?}"))
783 })?;
784 let neg_g: Vec<f64> = g.iter().map(|&x| -x).collect();
785 let du = lu
786 .solve(&neg_g)
787 .ok_or_else(|| EngineeringError::SolverError("tangent singular".into()))?;
788 axpy_into(&mut u_trial, 1.0, &du);
789 if norm2(&du) <= tol {
790 converged = true;
791 break;
792 }
793 }
794 if !converged {
795 return Err(EngineeringError::ConvergenceError(format!(
796 "nonlinear-dynamic Newton did not converge at t = {t_next}"
797 )));
798 }
799 let a_new: Vec<f64> = (0..n)
801 .map(|i| a0 * (u_trial[i] - u_n[i]) - a2 * v_n[i] - a3 * a_n[i])
802 .collect();
803 let v_new: Vec<f64> = (0..n)
804 .map(|i| v_n[i] + a6 * a_n[i] + a7 * a_new[i])
805 .collect();
806 u = u_trial;
807 v = v_new;
808 a = a_new;
809 out.time.push(t_next);
810 out.disp.push(u.clone());
811 out.vel.push(v.clone());
812 out.acc.push(a.clone());
813 }
814 Ok(out)
815}
816
817#[derive(Debug, Clone, Copy)]
833pub struct GeoNonlinearBar {
834 pub ea: f64,
835 pub length: f64,
836}
837
838impl GeoNonlinearBar {
839 pub fn strain(&self, u: f64) -> f64 {
841 let r = u / self.length;
842 r + 0.5 * r * r
843 }
844 pub fn internal_force(&self, u: f64) -> f64 {
846 self.ea * self.strain(u) * (1.0 + u / self.length)
847 }
848 pub fn tangent(&self, u: f64) -> f64 {
850 let r = u / self.length;
851 self.ea / self.length * ((1.0 + r) * (1.0 + r) + self.strain(u))
852 }
853 pub fn solve_static(
855 &self,
856 f_ext: f64,
857 tol: f64,
858 max_iter: usize,
859 ) -> Result<f64, EngineeringError> {
860 let ea = self.ea;
861 let l = self.length;
862 let bar = *self;
863 let (u, _it) = newton_raphson(
864 1,
865 |u| vec![bar.internal_force(u[0]) - f_ext],
866 |u| vec![bar.tangent(u[0])],
867 &[f_ext * l / ea], tol,
869 max_iter,
870 )?;
871 Ok(u[0])
872 }
873}
874
875#[cfg(test)]
876mod tests {
877 use super::*;
878
879 const E_STEEL: f64 = 200.0e9; #[test]
882 fn axial_bar_single_element_displacement() {
883 let f = 50.0e3;
887 let (l, area) = (2.0, 0.01);
888 let model = FeModel {
889 nodes: vec![FeNode { x: 0.0, y: 0.0 }, FeNode { x: l, y: 0.0 }],
890 elements: vec![FeElement::Truss {
891 ni: 0,
892 nj: 1,
893 e: E_STEEL,
894 area,
895 rho: 7850.0,
896 }],
897 constraints: vec![(0, 0.0), (1, 0.0), (2, 0.0), (4, 0.0), (5, 0.0)],
899 loads: vec![(3, f)], };
901 let res = solve_static(&model).unwrap();
902 let expected = f * l / (area * E_STEEL);
903 let got = res.displacements[3];
904 assert!(
905 (got - expected).abs() / expected < 1e-9,
906 "axial δ = {got} (expected {expected})"
907 );
908 let rux = res.reactions.iter().find(|(d, _)| *d == 0).unwrap().1;
910 assert!(
911 (rux + f).abs() / f < 1e-9,
912 "reaction ux = {rux} (expected {})",
913 -f
914 );
915 assert!(
917 (res.element_axial_force[0] - f).abs() / f < 1e-9,
918 "N = {}",
919 res.element_axial_force[0]
920 );
921 }
922
923 #[test]
924 fn two_bar_truss_hand_computed_joint_displacement() {
925 let f = 20.0e3;
931 let (l, area) = (1.0, 0.01);
932 let k = E_STEEL * area / l;
933 let model = FeModel {
934 nodes: vec![
935 FeNode { x: 0.0, y: 0.0 },
936 FeNode { x: l, y: 0.0 },
937 FeNode { x: 2.0 * l, y: 0.0 },
938 ],
939 elements: vec![
940 FeElement::Truss {
941 ni: 0,
942 nj: 1,
943 e: E_STEEL,
944 area,
945 rho: 7850.0,
946 },
947 FeElement::Truss {
948 ni: 1,
949 nj: 2,
950 e: E_STEEL,
951 area,
952 rho: 7850.0,
953 },
954 ],
955 constraints: vec![
957 (0, 0.0),
958 (1, 0.0),
959 (2, 0.0),
960 (4, 0.0),
961 (5, 0.0),
962 (7, 0.0),
963 (8, 0.0),
964 ],
965 loads: vec![(6, f)], };
967 let res = solve_static(&model).unwrap();
968 let u1 = res.displacements[3];
969 let u2 = res.displacements[6];
970 assert!(
971 (u1 - f / k).abs() / (f / k) < 1e-9,
972 "u1 = {u1} (expected {})",
973 f / k
974 );
975 assert!((u2 - 2.0 * f / k).abs() / (2.0 * f / k) < 1e-9, "u2 = {u2}");
976 }
977
978 #[test]
979 fn cantilever_tip_deflection_matches_euler_bernoulli() {
980 let (l, inertia, area) = (3.0, 8.0e-6, 0.01);
984 let p = 5.0e3;
985 let model = FeModel {
986 nodes: vec![FeNode { x: 0.0, y: 0.0 }, FeNode { x: l, y: 0.0 }],
987 elements: vec![FeElement::Frame {
988 ni: 0,
989 nj: 1,
990 e: E_STEEL,
991 area,
992 inertia,
993 rho: 7850.0,
994 }],
995 constraints: vec![(0, 0.0), (1, 0.0), (2, 0.0)],
997 loads: vec![(4, -p)], };
999 let res = solve_static(&model).unwrap();
1000 let tip = res.displacements[4]; let expected = -p * l * l * l / (3.0 * E_STEEL * inertia);
1002 assert!(
1003 (tip - expected).abs() / expected.abs() < 1e-9,
1004 "tip δ = {tip} (expected {expected})"
1005 );
1006 let ruy = res.reactions.iter().find(|(d, _)| *d == 1).unwrap().1;
1008 assert!(
1009 (ruy - p).abs() / p < 1e-9,
1010 "root reaction = {ruy} (expected {p})"
1011 );
1012 let rrz = res.reactions.iter().find(|(d, _)| *d == 2).unwrap().1;
1016 assert!(
1017 (rrz - p * l).abs() / (p * l) < 1e-9,
1018 "root moment = {rrz} (expected {})",
1019 p * l
1020 );
1021 }
1022
1023 #[test]
1024 fn cantilever_two_elements_also_exact() {
1025 let (l, inertia, area) = (3.0, 8.0e-6, 0.01);
1027 let p = 5.0e3;
1028 let model = FeModel {
1029 nodes: vec![
1030 FeNode { x: 0.0, y: 0.0 },
1031 FeNode { x: l / 2.0, y: 0.0 },
1032 FeNode { x: l, y: 0.0 },
1033 ],
1034 elements: vec![
1035 FeElement::Frame {
1036 ni: 0,
1037 nj: 1,
1038 e: E_STEEL,
1039 area,
1040 inertia,
1041 rho: 7850.0,
1042 },
1043 FeElement::Frame {
1044 ni: 1,
1045 nj: 2,
1046 e: E_STEEL,
1047 area,
1048 inertia,
1049 rho: 7850.0,
1050 },
1051 ],
1052 constraints: vec![(0, 0.0), (1, 0.0), (2, 0.0)],
1053 loads: vec![(7, -p)], };
1055 let res = solve_static(&model).unwrap();
1056 let tip = res.displacements[7];
1057 let expected = -p * l * l * l / (3.0 * E_STEEL * inertia);
1058 assert!(
1059 (tip - expected).abs() / expected.abs() < 1e-8,
1060 "two-element tip δ = {tip} (expected {expected})"
1061 );
1062 }
1063
1064 #[test]
1065 fn newmark_sdof_undamped_tracks_cosine() {
1066 let (mass, stiff, u0): (f64, f64, f64) = (2.0, 200.0, 0.05);
1068 let omega = (stiff / mass).sqrt();
1069 let period = 2.0 * std::f64::consts::PI / omega;
1070 let dt = period / 400.0;
1071 let nsteps = 400; let res = newmark_linear(
1073 &[mass],
1074 &[0.0],
1075 &[stiff],
1076 1,
1077 |_t| vec![0.0],
1078 &[u0],
1079 &[0.0],
1080 dt,
1081 nsteps,
1082 0.25,
1083 0.5,
1084 )
1085 .unwrap();
1086 let mut max_err = 0.0_f64;
1088 for (i, u) in res.disp.iter().enumerate() {
1089 let t = i as f64 * dt;
1090 let analytic = u0 * (omega * t).cos();
1091 max_err = max_err.max((u[0] - analytic).abs());
1092 }
1093 assert!(
1094 max_err < 1e-3 * u0,
1095 "max deviation from u0·cos(ωt) = {max_err} (u0 = {u0})"
1096 );
1097 let e0 = 0.5 * stiff * u0 * u0;
1099 for (u, v) in res.disp.iter().zip(res.vel.iter()) {
1100 let e = 0.5 * mass * v[0] * v[0] + 0.5 * stiff * u[0] * u[0];
1101 assert!(e <= e0 * (1.0 + 1e-6), "energy grew: {e} > {e0}");
1102 }
1103 assert!((res.disp[nsteps][0] - u0).abs() < 1e-3 * u0);
1105 assert!(res.vel[nsteps][0].abs() < 1e-2 * omega * u0);
1106 }
1107
1108 #[test]
1109 fn newton_raphson_cubic_spring_hand_solved_root() {
1110 let (k, k3, f) = (100.0, 100.0, 200.0);
1113 let (u, iters) = newton_raphson(
1114 1,
1115 |u| vec![k * u[0] + k3 * u[0].powi(3) - f],
1116 |u| vec![k + 3.0 * k3 * u[0] * u[0]],
1117 &[0.0],
1118 1e-12,
1119 50,
1120 )
1121 .unwrap();
1122 assert!((u[0] - 1.0).abs() < 1e-10, "u = {} (expected 1.0)", u[0]);
1123 assert!(iters < 20, "took {iters} iterations");
1124
1125 let (u2, _) = newton_raphson(
1127 1,
1128 |u| vec![k * u[0] + k3 * u[0].powi(3) - 1000.0],
1129 |u| vec![k + 3.0 * k3 * u[0] * u[0]],
1130 &[0.0],
1131 1e-12,
1132 50,
1133 )
1134 .unwrap();
1135 assert!((u2[0] - 2.0).abs() < 1e-10, "u2 = {}", u2[0]);
1136 }
1137
1138 #[test]
1139 fn geometric_nonlinear_bar_stiffens_and_recovers_linear() {
1140 let ea = 1.0e6;
1142 let bar = GeoNonlinearBar { ea, length: 1.0 };
1143 let small = bar.solve_static(100.0, 1e-12, 100).unwrap();
1145 let lin_small = 100.0 / ea;
1146 assert!(
1147 (small - lin_small).abs() / lin_small < 1e-3,
1148 "small u = {small}"
1149 );
1150 let big = bar.solve_static(3.0e5, 1e-10, 100).unwrap();
1153 let lin_big = 3.0e5 / ea;
1154 assert!(
1155 big < lin_big,
1156 "expected stiffening: u = {big}, linear = {lin_big}"
1157 );
1158 assert!(
1159 (bar.internal_force(big) - 3.0e5).abs() < 1e-3,
1160 "residual not satisfied: f_int = {}",
1161 bar.internal_force(big)
1162 );
1163 }
1164
1165 #[test]
1166 fn newmark_nonlinear_duffing_energy_bounded() {
1167 let (mass, k, k3, u0): (f64, f64, f64, f64) = (1.0, 100.0, 500.0, 0.3);
1171 let e0 = 0.5 * k * u0 * u0 + 0.25 * k3 * u0.powi(4);
1172 let omega = (k / mass).sqrt();
1174 let dt = (2.0 * std::f64::consts::PI / omega) / 500.0;
1175 let res = newmark_nonlinear(
1176 &[mass],
1177 &[0.0],
1178 1,
1179 |u| vec![k * u[0] + k3 * u[0].powi(3)],
1180 |u| vec![k + 3.0 * k3 * u[0] * u[0]],
1181 |_t| vec![0.0],
1182 &[u0],
1183 &[0.0],
1184 dt,
1185 2000,
1186 0.25,
1187 0.5,
1188 1e-12,
1189 50,
1190 )
1191 .unwrap();
1192 let mut max_e = 0.0_f64;
1193 let mut min_e = f64::INFINITY;
1194 for (u, v) in res.disp.iter().zip(res.vel.iter()) {
1195 let e = 0.5 * mass * v[0] * v[0] + 0.5 * k * u[0] * u[0] + 0.25 * k3 * u[0].powi(4);
1196 max_e = max_e.max(e);
1197 min_e = min_e.min(e);
1198 }
1199 assert!(
1201 (max_e - e0).abs() / e0 < 1e-2 && (e0 - min_e).abs() / e0 < 1e-2,
1202 "energy drift: E0 = {e0}, min = {min_e}, max = {max_e}"
1203 );
1204 let peak = res.peak_abs(0);
1206 assert!(peak <= u0 * (1.0 + 1e-3), "amplitude grew: {peak} > {u0}");
1207 }
1208}