feat: implement consistent-mass HHT shell dynamics

This commit is contained in:
KOKO\Mimi
2026-09-22 21:38:42 +09:00
parent fec8cf09c2
commit f0dc42a280
41 changed files with 9294 additions and 125 deletions
+2
View File
@@ -214,3 +214,5 @@ PowerShell helper의 `-B`는 named parameter로 잘못 bind될 수 있어 위 ar
- 2026-09-22 初回記録: 本文のsnapshot作成。Reference16/100受理、最新Debug focused実行中。Comparator41testsのmain再実行PASSを確認。今後も作業を継続する。
- 2026-09-22 후속 확인: 독립 physics reviewer가 `physics-evaluation.md` 초안을 작성했다. 최신 Debug focused는 68 tests 중67 PASS/1 C04a INCONCLUSIVE SKIP으로 종료했다. Main은 다음 명령에서 종료코드와 summary를 직접 확인한다. C08 및 최종 reference 검증은 계속 PENDING이다.
- 2026-09-22 후속 작업: main이 Debug focused session81971 종료코드0을 직접 확인했다. 68 tests/67 PASS/1 INCONCLUSIVE SKIP, 총117.526s. Comparator41tests도 main 확인 완료. 구현 묶음을 로컬 커밋한 뒤 C08 파일을 등록한다. 신규 private test는 아직 이 Debug 실행에 포함되지 않았다.
+10
View File
@@ -78,3 +78,13 @@ Feature 범위·상태는 [PRD](PRD.md), 상세 acceptance intent는 [requiremen
- 승인 근거: 사용자의 “I/O definition, implementation, physics evaluation gate 모두 진행하세요” 및 후속 “깃에 커밋 하면서 진행하세요”.
- 결정: HHT-ADR-006의 검토된 physical a/auxiliary A 분리, 0+ full STEP 초기화, HHT-ADR-007의 consistent mass를 실제 구현 대상으로 채택한다. 공개 입력은 [I/O 계약](io-contract.md)의 단일 NLGEOM shell DIRECT·기본 계수·zero 초기조건/고정 BC·constant force CLOAD subset이다. 별도 dynamic state와 dynamic-only HDF5 schema v1을 추가하고 기존 static/eigen 계약은 보존한다.
- 영향: [구현 계획](implementation-plan.md)에 따라 MSVC Debug build/CTest, 독립 수치·물리 검증과 지정 원본 reference 비교를 수행한다. 확인된 문서·검증된 구현 묶음을 현 branch에서 순차 커밋한다. 외부 solver 실행·reference 보정·push/release는 포함하지 않는다. 실패·미실행 검증을 통과로 표시하지 않는다.
## HHT-ADR-010 — Newton 시작 형상과 영변형 산술
- 상태: `accepted-implementation-detail`; reference 통과를 의미하지 않는다.
- Owner: 메인 assistant; 독립 수치 검토는 numerical-review-agent.
- 근거: 승인된 전체 구현 범위 안의 수치 시작값 선택. `.harness/build/hht-numerical/zero-trial-red.log`와 original reference 실행에서 먼저 실패를 재현했다.
- 결정: 모든 increment의 Newton 초기 좌표를 `q_n`으로 둔다. Constant-A 외삽은 지정 DIRECT h=.1에서 최초 trial geometry를 무효화했으므로 N2의 시작값을 명시적으로 갱신한다. HHT predictor/rate/접선, alpha/beta/gamma, 고정 dt, Newton100 및 모든 tolerance는 유지한다. 유효한 시작점에서 나온 correction도 chart/geometry 밖이면 즉시 실패하며 retry·clamp·cutback은 없다.
- 추가 수정: 초기 형상에서 차감한 큰 covariant metric 대신 `delta g`로 Green strain과 `F=I+delta g⊗Gdual`을 계산한다. 수학적으로 같은 식이며 영변형에서 생긴 artificial drilling force를 제거한다. 강성·정적 결과 영향은 기존 regression으로 확인한다.
- 검증: decimal 좌표 영변형 exact-zero 회귀, finite/warped scalar drilling constraint·gradient·Hessian, 작은각 D3 threshold 검사가 통과했다. 상세 실행/물리 acceptance는 최종 implementation evidence에서 구분한다.
@@ -0,0 +1,49 @@
# HHT 관성 구현 독립 수치 검토
- Owner: `numerical-review-agent`; date: `2026-09-22`.
- 상태: `source-delta-reviewed; runtime-verification-pending`. 수식/API 중간 검토이며 전체 구현 gate 통과 판정이 아니다.
- 범위: [관성 API](../../include/fesa/elements/nonlinear_mitc4_inertia.h), [관성 구현](../../src/fesa/elements/nonlinear_mitc4_inertia.cpp), [관성 tests](../../tests/unit/elements/nonlinear_mitc4_inertia_test.cpp), [비선형 내력](../../src/fesa/elements/nonlinear_mitc4_shell.cpp)의 directional third-derivative, [초기화·null algebra](../../src/fesa/analysis/hht_dynamic_algebra.cpp), [동적 조립](../../src/fesa/analysis/hht_dynamic_assembly.cpp), [HHT procedure](../../src/fesa/analysis/hht_dynamic_procedure.cpp).
- 기준: [정식화](formulation.md), [관성 상세](formulation-inertia.md), [N1–N7](numerical-controls.md), [C01–C12](verification-cases.md). Source/tests/상위 계약은 읽기 전용이다.
## 첫 검토 findings
아래는 작업 중 최초로 관찰한 내용이다. Owner 수정 뒤의 대조·실행 evidence는 별도로 갱신한다.
| ID | 심각도·상태 | 관찰·수정과 필요한 검증 |
| --- | --- | --- |
| HHT-IMP-NR-001 | **P1; source corrected, execution pending** | `Inverse(JetMatrix)`의 cyclic minor에 이미 포함된 cofactor 부호를 다시 적용하던 오류가 제거됐다. 새 test가 독립 `Jl^-T`와 현재 null, finite rotation의 `M n=0`을 대조한다. 실행 evidence 확인 전이다. |
| HHT-IMP-NR-002 | **P2; corrected, focused regression passed** | `convective_force=force-M*a` cancellation을 직접 `∫rho Hᵀ B_x[v,v]dV0` 누적으로 수정했다. 같은 q/v에서 a를 `1e15`배 변경해도 c가 동일한지 검사하는 test의 통과 로그를 확인했다. |
| HHT-IMP-NR-003 | **P2; corrected, focused sweep passed** | 최초 raw mixed-unit direction/`1e-5` test를 D scaling/단위 direction, N5 `1e-6`·연속한 두 내부 sweep 점으로 수정했다. Position/velocity/acceleration partial 및 두 h의 dependent-rate effective inertia tangent, pure translation/rotation/mixed 방향과 해당 실행 통과를 확인했다. |
| HHT-IMP-NR-004 | **미완료 coverage; 실행 대기** | 새 N4 guard는 analytic null의 20차원 직교 여공간에서 scaled symmetry/null, positive Cholesky와 `rcond_inf>=2^-32`를 검사한다. 물리 DOF를 numeric rank cutoff로 삭제하지 않는다. C05/C06의 finite rotation·warped/oracle·D3·단위변환 test와 실행 evidence는 owner 완료 후 확인한다. |
| HHT-IMP-NR-005 | **P2; corrected, threshold regression passed** | 작은 각에서 기존 직접 삼각함수 D3의 cancellation 위험을 안정 series로 수정했다. `|phi|<.5`의 squared-norm 9차 coefficient recurrence와 최종 회전행렬 extras 복사를 대조했으며 작은각 경계 양쪽을 포함한 4개 C++ sweep의 통과를 확인했다. |
| HHT-IMP-NR-006 | **P2; closed, exact scale restored** | 기존 `Σe |D²f_e[v,v]|`는 element 내부 coordinate-pair 상쇄를 잃어 N3와 달랐다. 수정본은 아래 exact scalar ψ Hessian을 global `(q_j,q_k)` identity로 합친 **뒤** `Σjk |ψ_qq,jk v_j v_k|`를 누적하고 algebra가 이를 소비한다. N3/[I/O 계약](io-contract.md) 의미와 일치한다. Finite/warped ψ·gradient·Hessian 독립 test 통과도 확인했다. |
| HHT-IMP-NR-007 | **P2; source corrected, execution pending** | 최초 `BuildPhysicalSpace`에 없던 N4 `PᵀZ` 및 `[P Z]` Gram guard가 추가됐다. Nodal sparse block의 실제 free dimension과 global Frobenius 오차, normalized null Gram을 검사하며 `1e-12` 기준을 유지한다. 수정본 실행 evidence는 대기다. |
| HHT-IMP-NR-008 | **수치 절차 변경; 문서 정합성 필요** | Reference의 큰 DIRECT step에서 constant-A 초기 추정이 geometry를 벗어나, Newton 시작 좌표를 `q_n`으로 변경했다. HHT predictor/종속 rates/endpoint 식은 그대로이며 cutback/retry도 없어 수식 변경은 아니다. 다만 N2가 기존 시작값을 명시하므로 새 deterministic 시작값과 실패 동작을 N2에 명시해야 한다. 기존 문구 상태로 계약 일치를 주장하지 않는다. |
NR-001의 독립 scalar 반례: `phi=(.5,0,0)`, `d0=eZ`, Jl의 yz block은 `[[sin(.5)/.5,−(1−cos(.5))/.5],[(1−cos(.5))/.5,sin(.5)/.5]]`다. 관찰한 inverse 식을 적용하면 unit null의 yz 성분이 `(-.6816387600,.7316888689)`이고 `||B n||=||(Jl n)×d||=.4744471057`이다. Python 표준 산술로 실제 계산했다. 이는 zero rotation에서 숨는 부호 오류이며 같은 잘못된 n을 미분한 finite-difference test만으로는 검출되지 않는다.
## 일치한 수식과 남은 대조
최초 snapshot의 `H`, `B_x`, `C_x` 조립과 `I=∫rho Hᵀ(Ha+B_x[v,v])`, `DqI`, `DvI=2∫rho HᵀB_x[v,p]`, `M`의 표시된 index contraction은 정식화와 일치했다. `P/L` 및 `Pdot/Ldot`는 같은 현재 x와 물리적 acceleration을 사용하고 `Ldot=∫rho x×xddot`의 `v×v=0` 항등식과 맞는다. Referenced density/measure, 3³ mass quadrature, current geometry 검사와 fixed-global rate 의미도 대조했다. 이것은 runtime 통과 판정이 아니다.
Internal Jet의 product 6항 및 Compose의 `f''' x_i x_v²+f''(2x_iv x_v+x_i x_vv)+f' x_ivv`를 독립 대조했다. Atan2의 bounded-ratio 미분은 quadrant 상수의 미분이 0인 점과 맞으며 기존 first/second reduction을 유지한다. 반환 `force_directional_second=D²f_int[v,v]`, `drilling_metric=Σkd gradγ⊗gradγ` 및 physical/drilling force·energy 분리는 정식화와 일치한다. 실제 procedure의 hidden-constraint 연결은 이 kernel 범위 밖이며 별도 검증이 필요하다.
NR-005의 독립 scalar 검산은 `phi=(θ,0,0), d0=eZ`에 대해 `D³d_x[eX,eX,eZ]=cosθ/θ−2sinθ/θ²+2(1−cosθ)/θ³`와 안정 전개 `−θ/4+θ³/36−θ⁵/960+θ⁷/50400`를 Python binary64로 비교했다. `θ=1e-4`에서 각각 `−7.7441382018e-5`, `−2.4999999972e-5`, 대칭 상대오차 `0.512`이고 `θ=1e-4*(1+2^-10)`에서 `0.339`다. 이는 algebraic scalar cancellation의 재현이며 수정 전 C++ Jet 출력 자체를 실행했다고 주장하지 않는다. 관성 rotation kernel의 32항 matrix exponential series에는 같은 직접 뺄셈이 없다. `|phi|<pi`에서 그 D3 truncation tail의 exact-arithmetic 상계는 `pi^30/30!/(1−pi/31)<3.45e-18`이며 binary64 roundoff 검증은 별개다.
NR-006의 최소 exact 구현 경로는 공개 moment-load가 0이고 동일 nodal initial director를 공유하는 현 범위에서 `κ_i=||Jl^-1d_i||`, `σ_i=1/κ_i`, `ψ_i=σ_i Σ_(e∋i)kd_e γ_ei`를 2차 미분하는 것이다. `δphi_i=n_i`는 `δtheta_i=σ_i d_i`를 만들어 모든 director와 element polar rotation을 고정하고 nodal R을 오른쪽 drilling 회전시킨다. 따라서 `Dγ_ej[n_i]=δij σ_i`, `n_iᵀf_phys=0`이다. σ는 현재 phi의 함수이므로 Hessian에서 동결하지 않는다. `Jl^-1d=Jr^-1d0=d0+.5 phi×d0+a(phi²)phi×(phi×d0)`로 σ를 안정 계산할 수 있다. 이 scalar ψ의 element-local Hessian을 source identity로 합친 뒤 `Σjk|ψ_i,qjqk||v_j||v_k|`를 계산하면 full energy D3 tensor 없이 N3 분모를 얻는다. 이는 승인 정식화의 항등식이며 scale 계약 변경이 아니다. 실제 code에서는 finite/warped 상태의 `nᵀf` 및 ψ gradient/Hessian과 독립 대조하고 수치 polar가 proper rotation으로 수렴하는 범위를 확인해야 한다.
## 초기화·procedure delta와 실행 evidence
`a_P=D P (Pᵀ DᵀM D P)^-1 PᵀDᵀ(F−f−c)` 뒤 `W^-1 ψ_q N W^-1` solve와 `N W^-1` 복원으로 null acceleration을 구하는 구현은 부호·단위와 맞는다. `ψ_q=DNᵀg+NᵀK`, raw hidden2의 `D²N[v,v]ᵀg+2DN[v]ᵀKv+NᵀD²f[v,v]+ψ_q a`도 일치한다. Null은 현재 `Jl^-1d`와 실제 free BC의 exact intersection으로 결정하며 작은 비영 constrained 성분을 버리지 않는다. Accepted null inventory 변경은 실패한다. Scaled physical mass factor와 W-scaled algebraic rcond 검사에는 epsilon mass/numeric rank deletion이 없다.
Newmark의 q/v는 A를 사용하고 물리 a는 `(A+alpha*a_old)/(1+alpha)`로 회복한다. Effective tangent의 rate 계수, force/moment 별 uncancelled Newton scale, 네 correction gate, 100번째 correction 뒤 재평가, fixed DIRECT `n*h`, endpoint reaction 및 현재 arm/`Jl^-T` spatial moment balance를 대조했다. Constant STEP load의 work `F·q`와 `T+Uphys+Udrill−E0−W`는 현 하중 범위와 맞으며 energy 감소를 강제하지 않는다. 이것은 실제 moving-shell 시간 정확도 인증이 아니다.
메인 실행 로그를 독립 열람했다. [focused.log](../../.harness/build/hht-numerical/focused.log)는 관성 8 tests와 일반 finite-rotation internal D3 sweep, HHT helper 3 tests의 통과를 보인다. 같은 실행 전체는 당시 mapper/writer 7 failures가 있어 전체 PASS가 아니다. `DynamicRotation.*`의 inverse/null/D3 oracle suite는 이 로그에 없어 별도 실행 확인이 필요하다. 당시 없었던 internal 작은각 threshold fixture는 아래 후속 로그에서 통과했다.
[focused-procedure.log](../../.harness/build/hht-numerical/focused-procedure.log)는 algebra 7/8, all-fixed procedure, state/writer/log 통과와 총 37개 중 2개 실패를 보인다. Algebra 경계 fixture는 이후 `W=I`, `diag(1,2^-k)`로 binary-exact threshold를 검사하도록 고쳤으나 이 보고서 시점 재실행 evidence는 대기다. 나머지 실패는 mapper diagnostic fixture다. 기존 로그를 새 소스 전체 통과로 소급하지 않는다.
후속 [focused-exact-constraint.log](../../.harness/build/hht-numerical/focused-exact-constraint.log)는 internal residual 7 tests의 통과를 보이며, NR-005의 작은각 4 fixtures와 NR-006의 finite/warped exact ψ·gradient·Hessian 검사를 포함한다. [zero-trial-red.log](../../.harness/build/hht-numerical/zero-trial-red.log)의 기존 영변형 spurious force 실패도 확인했다. 수정 `CovariantIncrement`의 `2E=G·δg+δg·G+δg·δg`, drilling `F=I+δg⊗Gdual`는 큰 기준값의 뺄셈을 피하는 동일 수식이다. `q=0`의 exact-zero 회복은 force clamp나 tolerance 완화가 아니다. 새 zero-trial test의 통과를 확인했다. Recovery·기존 정적 영향은 전체 회귀 검사 대상이다.
C04a는 판정을 분리한다. 위 로그에서 finest binary64 full/reduced acceleration 차이 약 `3.079e-10`이 `1e-10`을 넘었지만 N7 해상도 약 `6.296e-10` 이하다. q/v/physical-a의 시간차수·오차/noise 검사와 coarse reduction은 별개다. N7에 따라 finest reduction은 **INCONCLUSIVE**로 남길 수 있으며 threshold를 바꾸지 않는다. [decimal80 독립 검산](../../.harness/build/hht-numerical/c04a-decimal-reduction.json)의 full 2×2/scalar recurrence 차이 `<=1.70e-73`은 수학적 reduction을 지지하지만 FESA binary64의 `1e-10` 통과를 대신하지 않는다. 새 test는 시간수렴/coarse gate와 finest `GTEST_SKIP`을 분리했으며 이 보고서에서는 새 바이너리 재실행을 아직 확인하지 않았다. C04a 전체를 무조건 PASS로 합치지 않는다.
Build/CTest는 메인/구현 owner가 담당하며 reviewer가 별도 C++ 실행을 했다고 주장하지 않는다. 문서 `git diff --check`는 exit 0이다. NR-008 문서 정합성, 새 algebra/rotation oracle 실행, 실제 shell의 시간수렴·energy·moving-null·reference 검증이 남는다. 본 보고서는 전체 기능/물리 gate 통과 판정이 아니다.
+1 -1
View File
@@ -122,7 +122,7 @@ Generalized force 계열 component_names=`F1,F2,F3,MPHI1,MPHI2,MPHI3`, units=`fo
Energy의 W=F*ᵀ(qn-q0), defect=T+Uphys+Udrill-E0-W다. Initial physical energy E0는 frame0의 첫 세 성분 합이며 W0/defect0=0. A를 kinetic energy나 physical inertia에 쓰지 않는다. Algorithmic dissipation을 physical damping energy로 이름 붙이지 않는다. Raw momentum/hidden diagnostics는 위 internal frame의 필수 evidence이며 이번 persistent schema에는 별도 필수 dataset을 추가하지 않는다. HDF5에서 계산할 수 없는 상세 검증은 실행 보고서가 internal evidence를 기록한다.
`/diagnostics`는 기존 diagnostic compound의 severity/code/source_file/source_line/keyword/entity_identity/message와 정렬을 재사용한다. Writer는 전체 frames/node/element inventory, dtype/rank/component/attrs/time/finite/constraint, complete final frame를 검사하고 write/flush/close/reopen/self-check 후 원자 replace한다. 기존 final file은 실패 시 보존한다. Live log의 이미 flush된 prefix는 rollback하지 않는다. No-op warning이 있어도 필수 dataset을 생략할 수 없다.
`/diagnostics`는 기존 diagnostic의 의미·dtype·정렬을 재사용하되 dynamic compound field 이름을 severity/code/source_file/source_line/keyword/entity_identity/message로 명시한다. 기존 static writer의 file/line 이름은 이 dynamic schema에 적용하지 않는다. Writer는 전체 frames/node/element inventory, dtype/rank/component/attrs/time/finite/constraint, complete final frame를 검사하고 write/flush/close/reopen/self-check 후 원자 replace한다. 기존 final file은 실패 시 보존한다. Live log의 이미 flush된 prefix는 rollback하지 않는다. No-op warning이 있어도 필수 dataset을 생략할 수 없다.
## 4. 진단과 비교 binding
@@ -29,7 +29,7 @@
모든 적용 가능한 gate의 **AND**다. a와 A의 서로 다른 chain-rule 계수를 반영한다. h-based rate gate는 증가량의 운동학 일관성을 검사하며 physical acceleration의 시간 정확도를 인증하지 않는다. 해당 정확도는 N7의 독립 검증으로 확인한다.
Newton 초기 추정은 constant-acceleration `q_trial=q_p+beta h² A_n`으로 둔다. 이는 Newton 시작값이며 시간 scheme 변경이 아니다. 모든 trial에서 고정 q/v/a/A 성분은 exact 0이다. Residual이 이미 통과하는 초기 추정은 correction=0으로 명시하여 동일 gate를 평가한다. 그 외에는 `Keff`를 매회 새로 조립하고 correction을 수정 없이 적용한 후 residual을 다시 검사한다. Initial guess가 chart/geometry 밖이면 실패이며 clamp·line search·cutback으로 바꾸지 않는다.
Newton 초기 추정은 직전 accepted geometry `q_trial=q_n`으로 둔다([HHT-ADR-010](ADR.md)). `q_p`, `v_p` 및 물리 a/보조 A의 관계는 정식화 그대로 유지한다. 이는 Newton 시작값이며 시간 scheme 변경이 아니다. 모든 trial에서 고정 q/v/a/A 성분은 exact 0이다. Residual이 이미 통과하는 초기 추정은 correction=0으로 명시하여 동일 gate를 평가한다. 그 외에는 `Keff`를 매회 새로 조립하고 correction을 수정 없이 적용한 후 residual을 다시 검사한다. Initial guess가 chart/geometry 밖이면 실패이며 clamp·line search·cutback으로 바꾸지 않는다.
최대 **100개의 Newton correction**을 허용한다. 100번째 correction 뒤의 candidate가 모든 gate를 만족하면 통과, 아니면 실패하며 101번째 solve는 없다. 기존 정적 100회/no-cutback 제한을 동일 실패 상한으로 채택한 것이고, 동적 문제가 100회 안에 수렴한다는 보장은 아니다. Backend success나 작은 correction 하나만으로 성공하지 않는다. Singular/invalid/nonfinite/linear backward-error failure는 즉시 실패다.
@@ -0,0 +1,47 @@
# HHT 동해석 물리 평가
- Owner: `physics-evaluation-agent`; 검토일: `2026-09-22`.
- 판정: **PENDING — 실제 shell C08 및 완료된 원본 reference/HDF5 evidence 대기**. 제한된 oscillator·관성 kernel 결과는 물리적으로 일관되지만 전체 physics gate 통과를 뜻하지 않는다.
- 적용 계약: [정식화](formulation.md), [관성 상세](formulation-inertia.md), [N1–N7](numerical-controls.md), [C01–C12](verification-cases.md), [reference mapping](reference-model.md), [I/O](io-contract.md). [독립 구현 수치 검토](implementation-numerical-review.md)와 구분한다.
## 우선순위별 findings
| ID | 상태 | 근거·영향·후속 owner |
| --- | --- | --- |
| HHT-PHY-001 | **필수 evidence 미완료** | C08a의 독립 volume forcing/전체 shell 좌표 시간수렴과 C08b의 자유 변형 shell 에너지·운동량·hidden defect 결과가 아직 없다. C01–C07과 작은 reference 변위로 유한회전 shell DAE의 물리적 적절성을 대신 입증할 수 없다. 구현 owner의 C08 결과를 검토해야 한다. |
| HHT-PHY-002 | **필수 reference 대기** | 원본 계산의 부분 Newton 로그만 존재한다. 완료된 101-frame 공식 HDF5와 C12 202-family 비교 결과를 아직 검토하지 못했다. 부분 accepted increment는 원본 전체 해석·평형·비교 통과가 아니다. |
| HHT-PHY-003 | **Debug 후속 실행 확인** | 기존 `focused-optimized.log`는 42 PASS/1 SKIP/1 FAIL이었다. 후속 `focused-debug-complete.log`는 68개 중 67 PASS/1 SKIP이며 `LargeDirectStepStartsFromAcceptedGeometry`도 PASS다. 이전 로그를 소급해 통과로 바꾸지 않는다. 구성된 전체 CTest 결과와는 구별한다. |
| HHT-PHY-004 | **C04a finest reduction INCONCLUSIVE** | Binary64 full/reduced physical acceleration 차이 `3.0790209097630861e-10`은 기존 `1e-10`보다 크고 N7 산술/solve 해상도 `6.2961354117696263e-10`보다 작다. 시간수렴 통과와 구별한다. Decimal80 최대 차이 `1.6934145088608964e-73`은 수학적 reduction 근거이며 FESA binary64 통과 증거가 아니다. |
## 확인한 물리 근거
실행 주체는 구현 owner다. Reviewer는 아래 로그와 원본 CSV를 읽고 표시된 수치로 독립 계산했으며 C++ solver를 별도 재실행하지 않았다. 단위는 일관된 입력 단위계다.
| 검사 | 관찰·계산 | 해석과 한계 |
| --- | --- | --- |
| C01/C03/C04a/C04b 시간수렴 | `focused-stable-kernel.log`의 `h=1/128→1/256` 전체 시점 최대오차로 `log2(e_h/e_h2)`를 재계산: q/v/a 각각 C01 `1.99978/1.99931/1.99978`, C03 `1.99994/1.99768/1.99944`, C04a `1.99993/1.99746/1.99992`, C04b `1.99968/1.99982/2.00237`. C03 naive q order `0.899645` | Smooth variable mass와 moving-null 제한 모델에서 승인 확장의 약 2차 수렴 및 잘못된 질량 확대의 구별을 지지한다. Shell의 증명은 아니다. |
| C04b hidden defects | Finest `max|q·v|=6.72614e-10`, `max|q·a+v²|=3.16003e-6`; 이전 두 grid에서 각각 감소 | C04b `1e-6/1e-4` 이내. Position-level 평형만으로 rate consistency를 주장하지 않는 계약과 맞는다. |
| C07 alpha=0 | `max|E−E0|/E0=1.14353e-13`, 한계 `1e-10` | 선형 무감쇠 평균가속도 한계의 에너지 보존 지지. |
| C07 기본 alpha | 최대 energy defect `2.72922e-5`, 종단 signed defect `−2.45134e-5`, displacement error `0.0016895`, phase error `−0.00185687` | 관측된 소산·위상 변화는 algorithmic 효과다. 물리 damping이나 비선형 매-step 단조 감소로 해석하지 않는다. |
| C07 spectrum | `omega*h=10/100/1000`의 spectral radius `0.913168/0.904853/0.904763`; 해석적 limit `.95/1.05=0.9047619048` | 기본 음수 alpha의 고주파 소산 방향과 한계에 일치. 마지막 finite frequency와 limit의 exact equality를 요구하지 않는다. |
| C07 관성 반력 | `SupportReactionUsesPhysicalAccelerationAndCoupling` PASS. 계약 모델에서 constrained acceleration=0이어도 `R2=M21*a1=.5*a1` | Support-on-structure 부호와 physical endpoint 반력의 근거다. `A` 또는 weighted constrained residual을 반력으로 대체할 수 없다. |
| C05/C06 관성·회전 | Optimized 로그에서 관성 8 tests, 회전 4 tests PASS: rectangle 질량/null, warped 독립 volume energy/power, 단위·global-frame 변환, finite-rotation inverse/null, 독립 D3 anchors, 누락 mutation 검출 | Current consistent mass, 양의 물리 질량과 무질량 drill, 관성 power의 국소 근거다. Test 이름의 PASS를 전역 shell 운동량·시간수렴 통과로 확대하지 않는다. |
| 대수 초기화·rigid limits | Algebra 8 tests와 all-fixed procedure PASS. Internal zero-trial 및 rigid translation nullspace/symmetry PASS | 초기 mass-null 가속도 결합, admissible null rank, 영변형 spurious force 방지의 제한 근거다. Free translating shell 전체 이력과 C11 반복 실행 coverage는 별도 확인 대상이다. |
## 원본 모델과 coverage
원본 INP는 729 nodes/676 elements의 평판, 외곽 104 nodes의 6 DOF 고정, 중심 node `2`의 global force `(-100000,-100000,-100000)`이다. 입력 수치로 총 질량은 `rho*area*t=2700*100*.5=135000` mass units다. `E=2.1e11`, `nu=.3`, `rho=2700`, 두께 `.5`를 SI로 단정하지 않는다.
원본 CSV 909행을 직접 집계했다. `max|U1|=max|U2|=2.31251943e-6`, `max|U3|=4.91895989e-5` length units, 따라서 `max|U3|/t=9.83791978e-5`다. 모든 표본 U는 0 이하이고 중심의 U1=U2로, 적용 하중 방향 및 x/y 교환 대칭과 일관된다. 최대 회전 성분은 `6.88441514e-6 rad`다. 이는 외부 reference 자체의 관찰이며 FESA 응답 검증이 아니다. 동역학에서 개별 자유도의 가속도·반력·변위 부호를 하중 부호만으로 일률 강제하지 않는다.
Reference는 중심 주변 9 nodes만 제공한다. [계약](reference-model.md)에 기록된 `dt/T1≈7.4567`과 위 작은 회전 때문에 transient phase 정확도·전판 모드·큰 회전 관성·stress/V/A/반력/에너지에는 coverage가 없다. 외부 일치는 Abaqus 내부 요소·질량·시간적분 동등성의 근거가 아니다.
완료 HDF5에서는 기존 N2/N6에 따라 `I+f_int−F`, constrained selector 반력, `Σ(F+R)−Pdot`, current-arm spatial moment 평형을 확인해야 한다. `T/Uphys/Udrill`의 비음성·규모, `W=F·(q−q0)`, energy defect 정의, constrained exact zero, 물리 strain/stress 회복과 대칭도 확인 대상이다. `Udrill`을 물리 stress에 합치거나 `A`를 물리 관성/운동에너지에 쓰면 안 된다. 현재 이 전판 결과는 미검토 상태다.
## 열람 evidence와 잔여 위험
- [stable kernel 로그](../../.harness/build/hht-numerical/focused-stable-kernel.log), [exact-constraint 로그](../../.harness/build/hht-numerical/focused-exact-constraint.log), [optimized 로그](../../.harness/build/hht-numerical/focused-optimized.log), [Debug 후속 로그](../../.harness/build/hht-numerical/focused-debug-complete.log).
- [C04a Decimal80](../../.harness/build/hht-numerical/c04a-decimal-reduction.json).
- [원본 INP](../../reference/shellNL-dyn-meshfine/shellNL-dyn-meshfine.inp), [원본 CSV](<../../reference/shellNL-dyn-meshfine/shellNL-dyn-meshfine displacements.csv>), [진행 중 reference 로그](../../reference/shellNL-dyn-meshfine/shellNL-dyn-meshfine_log.txt): 초기 검토 snapshot은 increment 8 accepted까지이며 이후 실행 완료를 뜻하지 않는다.
잔여 위험은 실제 shell의 유한회전 DAE 정확도, nonlinear energy/momentum 수렴, 전판 물리 평형·recovery 및 필수 reference 미완료다. C09–C11의 전체 실패 보존/결정론/rigid-motion coverage와 구성된 build/CTest도 해당 실행 보고서에서 확인해야 한다. 기존 기준을 완화하거나 새 acceptance gate를 만들지 않았다. 본 문서는 release 승인 문서가 아니다.
@@ -0,0 +1,113 @@
#ifndef FESA_ANALYSIS_DYNAMIC_ANALYSIS_STATE_H_
#define FESA_ANALYSIS_DYNAMIC_ANALYSIS_STATE_H_
#include <array>
#include <cstdint>
#include <vector>
#include "fesa/core/source_identity.h"
#include "fesa/core/status.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/math/vector.h"
#include "fesa/model/domain.h"
#include "fesa/results/result_records.h"
namespace fesa {
/// @brief Stores one accepted endpoint of an HHT dynamic step.
struct DynamicAcceptedFrameState {
std::uint32_t frame_index = 0U;
std::uint32_t increment_index = 0U;
std::uint32_t iteration_count = 0U;
double time = 0.0;
bool is_initial = false;
std::vector<EntityIndex> nodal_internal_order;
std::vector<SourceEntityId> nodal_source_ids;
Vector generalized_coordinates{0U};
Vector velocity{0U};
Vector acceleration{0U};
Vector algorithmic_acceleration{0U};
std::vector<std::array<std::array<double, 3>, 3>> nodal_triad;
std::vector<std::array<double, 3>> nodal_director;
Vector angular_velocity{0U};
Vector angular_acceleration{0U};
Vector inertia_force{0U};
Vector internal_force{0U};
Vector external_force{0U};
Vector residual{0U};
Vector reaction{0U};
Vector residual_scale{0U};
std::vector<ShellResultRow> shell_results;
double kinetic_energy = 0.0;
double physical_strain_energy = 0.0;
double drilling_energy = 0.0;
double external_work = 0.0;
double energy_defect = 0.0;
std::array<double, 6> global_equilibrium{};
std::array<double, 3> linear_momentum{};
std::array<double, 3> angular_momentum{};
std::array<double, 3> linear_momentum_rate{};
std::array<double, 3> angular_momentum_rate{};
std::array<double, 2> free_residual_metrics{};
Vector hidden_constraint{0U};
Vector hidden_velocity_defect{0U};
Vector hidden_acceleration_defect{0U};
Vector hidden_constraint_scale{0U};
Vector hidden_velocity_defect_scale{0U};
Vector hidden_acceleration_defect_scale{0U};
};
/// @brief Owns accepted HHT frame history and the current committed endpoint.
class DynamicAnalysisState {
public:
/// @brief Creates an uninitialized state that cannot be written or committed.
DynamicAnalysisState() = default;
/// @brief Validates the immutable dynamic inventory and schedule.
/// @return A state ready to accept the required frame zero.
static Result<DynamicAnalysisState> Create(const Domain& domain,
const DofManager& dofs);
/// @brief Returns an independent copy of the committed frame.
/// @pre At least one frame has been committed.
Result<DynamicAcceptedFrameState> BeginTrialFromCommitted() const;
/// @brief Atomically accepts one complete finite dynamic frame.
Status CommitAcceptedFrame(const DynamicAcceptedFrameState& frame);
/// @brief Discards caller-owned trial data without mutating this state.
void DiscardTrial() noexcept;
/// @brief Returns accepted frames in frame-index order.
const std::vector<DynamicAcceptedFrameState>& AcceptedFrames() const noexcept;
/// @brief Returns the current committed frame.
/// @pre At least one frame has been committed.
const DynamicAcceptedFrameState& CommittedState() const;
/// @brief Reports whether Create has validated this state inventory.
bool IsInitialized() const noexcept;
private:
DynamicAnalysisState(std::size_t full_dof_count,
std::uint32_t increment_count, double initial_increment,
std::vector<EntityIndex> shell_element_order,
std::vector<std::size_t> constrained_dofs,
std::vector<SourceEntityId> nodal_source_ids,
std::vector<ShellNodeInitialFrame> initial_frames);
std::size_t full_dof_count_ = 0U;
std::uint32_t increment_count_ = 0U;
double initial_increment_ = 0.0;
std::vector<EntityIndex> shell_element_order_;
std::vector<std::size_t> constrained_dofs_;
std::vector<SourceEntityId> nodal_source_ids_;
std::vector<ShellNodeInitialFrame> initial_frames_;
std::vector<DynamicAcceptedFrameState> accepted_frames_;
std::size_t hidden_constraint_count_ = 0U;
bool initialized_ = false;
};
} // namespace fesa
#endif // FESA_ANALYSIS_DYNAMIC_ANALYSIS_STATE_H_
@@ -0,0 +1,29 @@
#ifndef FESA_ANALYSIS_HHT_DYNAMIC_PROCEDURE_H_
#define FESA_ANALYSIS_HHT_DYNAMIC_PROCEDURE_H_
#include "fesa/analysis/prepared_analysis.h"
namespace fesa {
class LinearSolver;
class ParallelFor;
class TangentSolver;
/// @brief Executes endpoint-equilibrium HHT with consistent shell inertia.
class HhtDynamicProcedureExecutor {
public:
/// @brief Retains caller-owned deterministic execution and solve adapters.
HhtDynamicProcedureExecutor(const ParallelFor& parallel_for,
LinearSolver& positive_solver,
TangentSolver& tangent_solver);
/// @brief Returns complete accepted history without opening result output.
Result<DynamicExecutionResult> Execute(const Domain& domain,
NewtonIterationLogWriter& log) const;
private:
const ParallelFor& parallel_for_;
LinearSolver& positive_solver_;
TangentSolver& tangent_solver_;
};
} // namespace fesa
#endif // FESA_ANALYSIS_HHT_DYNAMIC_PROCEDURE_H_
@@ -0,0 +1,50 @@
#ifndef FESA_ANALYSIS_HHT_TIME_INTEGRATION_H_
#define FESA_ANALYSIS_HHT_TIME_INTEGRATION_H_
#include "fesa/core/status.h"
#include "fesa/math/vector.h"
namespace fesa {
/// @brief Stores the coupled original negative-alpha HHT coefficients.
struct HhtParameters {
double alpha;
double beta;
double gamma;
};
/// @brief Stores the displacement and velocity Newmark predictors.
struct HhtPredictor {
Vector coordinates;
Vector velocity;
};
/// @brief Separates physical chart rates from the auxiliary acceleration.
struct HhtRates {
Vector velocity;
Vector acceleration;
Vector algorithmic_acceleration;
};
/// @brief Builds coupled coefficients for alpha in [-1/3, 0].
/// @note The public INP subset uses only alpha=-0.05.
Result<HhtParameters> MakeHhtParameters(double alpha);
/// @brief Predicts q and v using the old auxiliary acceleration.
/// @param time_increment Finite positive duration in input time units.
Result<HhtPredictor> PredictHht(const Vector& coordinates,
const Vector& velocity,
const Vector& algorithmic_acceleration,
double time_increment,
const HhtParameters& parameters);
/// @brief Recovers physical and auxiliary rates from one trial coordinate.
/// @param old_acceleration Physical endpoint acceleration at the old time.
Result<HhtRates> EvaluateHhtRates(const Vector& coordinates,
const HhtPredictor& predictor,
const Vector& old_acceleration,
double time_increment,
const HhtParameters& parameters);
} // namespace fesa
#endif // FESA_ANALYSIS_HHT_TIME_INTEGRATION_H_
@@ -1,6 +1,7 @@
#ifndef FESA_ANALYSIS_NEWTON_ITERATION_LOG_H_
#define FESA_ANALYSIS_NEWTON_ITERATION_LOG_H_
#include <array>
#include <cstdint>
#include <filesystem>
#include <fstream>
@@ -49,6 +50,27 @@ struct NewtonIterationLogRecord {
bool combined_passed;
};
/// @brief Carries two-channel HHT endpoint convergence evidence.
/// @note Array entries are translation and rotation channels, respectively.
/// Corrections are dimensionless N2 metrics; rate corrections already include
/// h or h squared. Residual numerator and scale retain force/moment units.
struct DynamicNewtonIterationLogRecord {
std::uint32_t increment_index = 0U;
std::uint32_t iteration_index = 0U;
double time = 0.0;
std::string stage;
std::string outcome;
std::string failure_code;
std::array<double, 2U> residual_numerator{};
std::array<double, 2U> residual_scale{};
std::array<double, 2U> coordinate_correction{};
std::array<double, 2U> velocity_correction{};
std::array<double, 2U> acceleration_correction{};
std::array<double, 2U> algorithmic_correction{};
bool residual_pass = false;
bool correction_pass = false;
};
/// @brief Writes deterministic, line-oriented Newton iteration diagnostics.
/// @note A write, flush, or close failure permanently prevents a successful
/// log lifecycle for this writer instance.
@@ -66,6 +88,11 @@ class NewtonIterationLogWriter {
/// failure with code `log-output-failure`.
Status Write(const NewtonIterationLogRecord& record);
/// @brief Writes one complete HHT record using schema `hht-log-v1`.
/// @return Success after the complete line is written; invalid numeric
/// evidence or stream failure permanently fails this writer instance.
Status Write(const DynamicNewtonIterationLogRecord& record);
/// @brief Flushes every complete record written so far to the log.
/// @return Success after the stream flush, otherwise an output failure with
/// code `log-output-failure`.
+14 -3
View File
@@ -6,6 +6,7 @@
#include <vector>
#include "fesa/analysis/analysis_state.h"
#include "fesa/analysis/dynamic_analysis_state.h"
#include "fesa/analysis/eigen_analysis_state.h"
#include "fesa/analysis/nonlinear_analysis_state.h"
#include "fesa/core/diagnostic.h"
@@ -21,6 +22,8 @@ class NewtonIterationLogWriter;
class NonlinearResultsWriter;
class NonlinearStaticProcedureExecutor;
class ResultsWriter;
class HhtDynamicProcedureExecutor;
class DynamicResultsWriter;
struct DerivedOutputPaths;
/// @brief Owns one parsed and semantically mapped analysis input.
@@ -49,9 +52,15 @@ struct NonlinearStaticExecutionResult {
std::vector<Diagnostic> diagnostics;
};
/// @brief Stores a complete HHT history before atomic result finalization.
struct DynamicExecutionResult {
DynamicAnalysisState state;
std::vector<Diagnostic> diagnostics;
};
using PreparedAnalysisOutcome =
std::variant<LinearStaticExecutionResult, NonlinearStaticExecutionResult,
EigenAnalysisState>;
EigenAnalysisState, DynamicExecutionResult>;
/// @brief Selects the approved procedure from a prepared Domain.
/// @pre The executors and log outlive this call.
@@ -60,7 +69,8 @@ Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
NonlinearStaticProcedureExecutor& nonlinear_executor,
const LinearFrequencyProcedureExecutor& frequency_executor,
NewtonIterationLogWriter& log);
NewtonIterationLogWriter& log,
const HhtDynamicProcedureExecutor* dynamic_executor = nullptr);
/// @brief Writes an already computed outcome to its authoritative result file.
/// @pre The Newton log has closed successfully before this function is called.
@@ -69,7 +79,8 @@ Status FinalizePreparedAnalysisResult(
const DerivedOutputPaths& paths, const Domain& domain,
const PreparedAnalysisOutcome& outcome,
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer);
NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer,
DynamicResultsWriter* dynamic_writer = nullptr);
} // namespace fesa
@@ -0,0 +1,106 @@
#ifndef FESA_ELEMENTS_NONLINEAR_MITC4_INERTIA_H_
#define FESA_ELEMENTS_NONLINEAR_MITC4_INERTIA_H_
#include <array>
#include <string>
#include "fesa/core/status.h"
#include "fesa/elements/nonlinear_mitc4_shell.h"
#include "fesa/math/matrix.h"
#include "fesa/math/vector.h"
#include "fesa/math/vector3.h"
#include "fesa/model/model_types.h"
namespace fesa {
/// @brief Stores exact physical rotation-rate and drilling-null quantities.
struct DynamicRotationKinematics {
Vector3 director;
Vector3 drill_null;
Matrix null_gradient{3U, 3U};
/// @brief Stores D2 drill_null[velocity, velocity], without Dn[a].
Vector3 null_directional_second;
Vector3 angular_velocity;
Vector3 angular_acceleration;
/// @brief Stores director derivatives in fixed-global chart directions.
std::array<Vector3, 3> director_first;
std::array<std::array<Vector3, 3>, 3> director_second;
std::array<std::array<std::array<Vector3, 3>, 3>, 3> director_third;
};
/// @brief Evaluates exact finite-rotation rates and the unit drilling null.
/// @param phi Fixed-global principal-chart rotation coordinate.
/// @param initial_director Unit director at zero rotation.
/// @param velocity Fixed-chart rotation-coordinate rate.
/// @param acceleration Fixed-chart rotation-coordinate acceleration.
/// @return Finite director/rates/null derivatives or a structured failure.
[[nodiscard]] Result<DynamicRotationKinematics> EvaluateDynamicRotation(
const Vector3& phi, const Vector3& initial_director,
const Vector3& velocity, const Vector3& acceleration);
/// @brief Stores the consistent kinetic-inertia quantities at one trial.
struct NonlinearMitc4InertiaEvaluation {
Matrix mass;
Matrix position_tangent;
Matrix velocity_tangent;
Vector force;
Vector convective_force;
double kinetic_energy;
Vector3 momentum;
Vector3 angular_momentum;
Vector3 momentum_rate;
Vector3 angular_momentum_rate;
/// @brief Stores uncancelled quadrature norm sums for balance checks.
double momentum_rate_scale = 0.0;
double angular_momentum_rate_scale = 0.0;
};
/// @brief Evaluates finite-rotation consistent inertia for one MITC4 shell.
/// @note This object owns immutable reference geometry only, never analysis
/// trial state.
class NonlinearMitc4Inertia {
public:
/// @brief Creates a checked consistent kinetic-mass evaluator.
/// @param nodes Four non-owning nodes in source order.
/// @param initial_frames Fixed proper node frames in source order.
/// @param section Centered shell section with positive thickness.
/// @param material Shell material used to retain the nonlinear-shell input
/// contract.
/// @param density Reference mass density, strictly positive and finite.
/// @return A checked immutable evaluator or a structured model failure.
static Result<NonlinearMitc4Inertia> Create(
std::array<const Node*, 4> nodes,
std::array<ShellNodeInitialFrame, 4> initial_frames,
const ShellSection& section, const LinearElasticMaterial& material,
double density);
/// @brief Evaluates physical consistent inertia at a complete trial.
/// @param trial Fixed-global translation and rotation coordinates.
/// @param velocity24 Fixed-chart generalized physical velocity.
/// @param physical_acceleration24 Fixed-chart generalized physical
/// acceleration.
/// @return Current mass, inertia force and exact partial tangents, or a
/// fail-closed diagnostic.
[[nodiscard]] Result<NonlinearMitc4InertiaEvaluation> Evaluate(
const NonlinearMitc4TrialDofs& trial, const Vector& velocity24,
const Vector& physical_acceleration24) const;
private:
NonlinearMitc4Inertia(std::array<Vector3, 4> coordinates,
std::array<Matrix, 4> initial_frames,
std::array<Vector3, 4> initial_directors,
double thickness, double density,
SourceLocation source_location, std::string identity);
std::array<Vector3, 4> coordinates_;
std::array<Matrix, 4> initial_frames_;
std::array<Vector3, 4> initial_directors_;
double thickness_;
double density_;
SourceLocation source_location_;
std::string identity_;
};
} // namespace fesa
#endif // FESA_ELEMENTS_NONLINEAR_MITC4_INERTIA_H_
+15 -2
View File
@@ -43,6 +43,16 @@ struct NonlinearMitc4InternalEquilibrium {
Vector internal_force;
NonlinearMitc4Recovery recovery;
double physical_strain_energy;
Vector physical_force{0U};
Vector drilling_force{0U};
Matrix drilling_tangent{0U, 0U};
Matrix drilling_metric{0U, 0U};
double drilling_energy = 0.0;
Vector force_directional_second{0U};
Vector drilling_constraint{0U};
Matrix drilling_constraint_gradient{0U, 0U};
std::array<Matrix, 4> drilling_constraint_hessian{
Matrix{0U, 0U}, Matrix{0U, 0U}, Matrix{0U, 0U}, Matrix{0U, 0U}};
};
class NonlinearMitc4Shell;
@@ -50,11 +60,13 @@ class NonlinearMitc4Shell;
/// @brief Evaluates the internal residual and tangent at one complete trial.
/// @param element Immutable nonlinear MITC4 reference evaluator.
/// @param trial Complete global translation and rotation trial coordinates.
/// @param coordinate_direction Optional 24-vector for D2(f_int)[v,v].
/// @return Internal force, consistent tangent and physical recovery, or
/// failure.
/// @note External load vectors and their tangents are intentionally excluded.
Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
const NonlinearMitc4Shell& element, const NonlinearMitc4TrialDofs& trial);
const NonlinearMitc4Shell& element, const NonlinearMitc4TrialDofs& trial,
const Vector* coordinate_direction = nullptr);
/// @brief Evaluates fixed-reference Total Lagrangian MITC4 kinematics.
/// @note This class owns no mutable trial or accepted analysis state.
@@ -94,7 +106,8 @@ class NonlinearMitc4Shell {
friend Result<NonlinearMitc4InternalEquilibrium>
EvaluateInternalResidualAndTangent(const NonlinearMitc4Shell& element,
const NonlinearMitc4TrialDofs& trial);
const NonlinearMitc4TrialDofs& trial,
const Vector* coordinate_direction);
std::array<Vector3, 4> coordinates_;
std::array<Matrix, 4> initial_frames_;
@@ -0,0 +1,20 @@
#ifndef FESA_IO_HDF5_DYNAMIC_HDF5_RESULTS_WRITER_H_
#define FESA_IO_HDF5_DYNAMIC_HDF5_RESULTS_WRITER_H_
#include "fesa/results/dynamic_results_writer.h"
namespace fesa {
/// @brief Writes schema-v1 HHT dynamic HDF5 results with atomic finalization.
class DynamicHdf5ResultsWriter final : public DynamicResultsWriter {
public:
/// @brief Writes a validated dynamic result file and preserves a prior final
/// file on failure.
Status Write(const std::filesystem::path& output_path, const Domain& domain,
const DynamicAnalysisState& state,
const std::vector<Diagnostic>& diagnostics) override;
};
} // namespace fesa
#endif // FESA_IO_HDF5_DYNAMIC_HDF5_RESULTS_WRITER_H_
@@ -0,0 +1,28 @@
#ifndef FESA_RESULTS_DYNAMIC_RESULTS_WRITER_H_
#define FESA_RESULTS_DYNAMIC_RESULTS_WRITER_H_
#include <filesystem>
#include <vector>
#include "fesa/analysis/dynamic_analysis_state.h"
#include "fesa/core/diagnostic.h"
#include "fesa/core/status.h"
namespace fesa {
class Domain;
/// @brief Writes complete HHT dynamic results to their official output file.
class DynamicResultsWriter {
public:
virtual ~DynamicResultsWriter() = default;
/// @brief Persists every accepted dynamic frame and diagnostic atomically.
virtual Status Write(const std::filesystem::path& output_path,
const Domain& domain, const DynamicAnalysisState& state,
const std::vector<Diagnostic>& diagnostics) = 0;
};
} // namespace fesa
#endif // FESA_RESULTS_DYNAMIC_RESULTS_WRITER_H_
+406
View File
@@ -0,0 +1,406 @@
"""Compare the approved 101-frame HHT reference without modifying artifacts.
Run with ``uv run --with h5py python -B scripts/verification/compare_hht_dynamic.py``.
Only the documented displacement/rotation reference quantities are compared;
this is not an independent validation of inertia, energy, or convergence.
"""
from __future__ import annotations
import argparse
import csv
from decimal import Decimal, InvalidOperation
import hashlib
import json
import math
from pathlib import Path
import re
from typing import Any
import h5py
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
CASE = "shellNL-dyn-meshfine"
REFERENCE_DIRECTORY = ROOT / "reference" / CASE
NODE_LABELS = (2, 21, 22, 69, 70, 165, 309, 442, 586)
INSTANCE = "PART-1-1"
STEP = "Step-1"
NODE_COUNT = 729
FRAME_COUNT = 101
INCREMENT = 0.1
COMPONENTS = ("U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3")
CSV_COLUMNS = ("Frame", "Part Instance Name", "Node Label", *COMPONENTS)
FRAME_PATTERN = re.compile(
r"Increment\s+(\d+):\s*Step Time\s*=\s*"
r"([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)"
)
POLICY = {
"policy_id": "common-reference-tolerance",
"near_zero_ratio": 0.01,
"relative_tolerance": 0.05,
"relative_rms_tolerance": 0.01,
}
class ComparisonError(ValueError):
"""An identity/schema/finite failure that must precede tolerance evaluation."""
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
def require(condition: bool, code: str, message: str) -> None:
if not condition:
raise ComparisonError(code, message)
def positive_label(text: str) -> int:
require(bool(re.fullmatch(r"\d+", text)), "invalid-node-label", repr(text))
label = int(text)
require(0 < label <= 2**63 - 1, "invalid-node-label", repr(text))
return label
def canonical_instance(text: str) -> str:
return text.translate(str.maketrans("abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
def input_inventory(path: Path) -> tuple[dict[int, tuple[float, ...]], str]:
"""Read identity and schedule authority from this specific approved INP."""
raw = path.read_bytes()
nodes: dict[int, tuple[float, ...]] = {}
parts: list[str] = []
instances: list[tuple[str, str]] = []
steps: list[dict[str, str]] = []
dynamic_rows: list[tuple[float, float]] = []
keyword = ""
in_part = False
for line_number, line in enumerate(raw.decode("utf-8-sig").splitlines(), 1):
text = line.strip()
if not text or text.startswith("**"):
continue
fields = [field.strip() for field in text.split(",")]
if text.startswith("*"):
keyword = fields[0].upper()
parameters = dict(
(name.upper(), value.strip())
for field in fields[1:] if "=" in field
for name, value in [field.split("=", 1)]
)
if keyword == "*PART":
in_part = True
parts.append(canonical_instance(parameters.get("NAME", "")))
elif keyword == "*END PART":
in_part = False
elif keyword == "*INSTANCE":
instances.append((canonical_instance(parameters.get("NAME", "")),
canonical_instance(parameters.get("PART", ""))))
elif keyword == "*STEP":
steps.append(parameters)
elif keyword == "*DYNAMIC":
require([field.upper() for field in fields] == ["*DYNAMIC", "DIRECT"],
"wrong-input-procedure", f"line {line_number}")
continue
if keyword == "*NODE":
require(in_part and len(fields) == 4, "invalid-input-node", f"line {line_number}")
label = positive_label(fields[0])
require(label not in nodes, "duplicate-input-node", str(label))
coordinates = tuple(float(value) for value in fields[1:])
require(all(math.isfinite(value) for value in coordinates),
"nonfinite-input-node", str(label))
nodes[label] = coordinates
elif keyword == "*DYNAMIC":
if fields[-1] == "":
fields.pop()
require(len(fields) == 2, "wrong-input-schedule", f"line {line_number}")
dynamic_rows.append((float(fields[0]), float(fields[1])))
require(parts == ["PART-1"] and instances == [(INSTANCE, "PART-1")],
"wrong-input-instance", "Expected the original single identity instance.")
require(len(nodes) == NODE_COUNT and set(NODE_LABELS) <= nodes.keys(),
"wrong-input-node-inventory", f"Expected {NODE_COUNT} source nodes.")
require(len(steps) == 1 and steps[0].get("NAME") == STEP
and steps[0].get("NLGEOM", "").upper() == "YES"
and steps[0].get("INC") == "1000", "wrong-input-step", str(steps))
require(dynamic_rows == [(INCREMENT, 10.0)], "wrong-input-schedule", str(dynamic_rows))
value = 14695981039346656037
for byte in raw:
value = ((value ^ byte) * 1099511628211) & (2**64 - 1)
return nodes, f"fnv1a64:{value:016x}"
def read_reference(path: Path) -> dict[tuple[int, int], tuple[float, ...]]:
rows: dict[tuple[int, int], tuple[float, ...]] = {}
with path.open(encoding="utf-8-sig", newline="") as stream:
reader = csv.reader(stream)
header = [field.strip() for field in next(reader, [])]
require(len(header) == len(set(header)), "duplicate-reference-column", str(header))
require(set(CSV_COLUMNS) <= set(header), "missing-reference-column", str(header))
for line_number, fields in enumerate(reader, 2):
if not fields or all(not field.strip() for field in fields):
continue
require(len(fields) == len(header), "invalid-reference-row", f"line {line_number}")
row = dict(zip(header, (field.strip() for field in fields)))
match = FRAME_PATTERN.fullmatch(row["Frame"])
require(match is not None, "invalid-reference-frame", f"line {line_number}")
increment = int(match.group(1))
time = Decimal(match.group(2))
require(time.is_finite() and time == Decimal(increment) / Decimal(10),
"wrong-reference-time", f"line {line_number}: {row['Frame']}")
require(canonical_instance(row["Part Instance Name"]) == INSTANCE,
"wrong-reference-instance", f"line {line_number}")
label = positive_label(row["Node Label"])
key = (increment, label)
require(key not in rows, "duplicate-reference-row", str(key))
values = tuple(float(row[component]) for component in COMPONENTS)
require(all(math.isfinite(value) for value in values),
"nonfinite-reference-value", str(key))
rows[key] = values
expected = {(frame, label) for frame in range(FRAME_COUNT) for label in NODE_LABELS}
require(rows.keys() == expected, "reference-row-inventory",
f"missing={sorted(expected - rows.keys())}; extra={sorted(rows.keys() - expected)}")
return rows
def text_value(value: Any) -> str:
return value.decode("utf-8") if isinstance(value, bytes) else str(value)
def string_attribute(obj: Any, name: str, expected: str | None = None) -> str:
require(name in obj.attrs, "missing-hdf5-attribute", f"{obj.name}@{name}")
attribute = obj.attrs.get_id(name)
string = h5py.check_string_dtype(attribute.dtype)
require(attribute.shape == () and string is not None and string.length is None
and string.encoding == "utf-8", "wrong-hdf5-attribute-type", f"{obj.name}@{name}")
value = text_value(obj.attrs[name])
require(bool(value) and (expected is None or value == expected),
"wrong-hdf5-attribute-value", f"{obj.name}@{name}: {value!r}")
return value
def numeric_attribute(obj: Any, name: str, dtype: str, expected: int | float) -> None:
require(name in obj.attrs, "missing-hdf5-attribute", f"{obj.name}@{name}")
attribute = obj.attrs.get_id(name)
require(attribute.shape == () and attribute.dtype == np.dtype(dtype),
"wrong-hdf5-attribute-type", f"{obj.name}@{name}")
value = obj.attrs[name]
require(math.isfinite(value) and value == expected, "wrong-hdf5-attribute-value",
f"{obj.name}@{name}: {value!r}")
def dataset(file: h5py.File, path: str, shape: tuple[int, ...]) -> h5py.Dataset:
require(path in file and isinstance(file[path], h5py.Dataset), "missing-hdf5-dataset", path)
result = file[path]
require(result.shape == shape and result.chunks is None and result.compression is None,
"wrong-hdf5-dataset-layout", path)
return result
def read_results(path: Path, nodes: dict[int, tuple[float, ...]],
content_identity: str) -> tuple[list[np.ndarray], dict[str, Any]]:
"""Precheck every result node/frame/component, then project by source key."""
projected: list[np.ndarray] = []
with h5py.File(path, "r") as file:
require("metadata" in file and isinstance(file["metadata"], h5py.Group),
"missing-hdf5-metadata", "/metadata")
metadata = file["metadata"]
for name, expected in {
"feature_id": "hht-implicit-dynamics", "analysis_procedure": "hht-implicit-dynamics",
"mass_formulation": "consistent-kinetic", "internal_formulation": "FESA-MITC4-total-lagrangian",
"initial_state": "zero-q-v-step-load-0+",
"acceleration_convention": "physical-endpoint-and-auxiliary-separated",
"source_step_name": STEP, "step_name": STEP, "unit_system_label": "consistent-input-units",
}.items():
string_attribute(metadata, name, expected)
string_attribute(metadata, "solver_version")
source_identity = string_attribute(metadata, "source_input_identity")
require(source_identity.endswith(";content_identity=" + content_identity),
"wrong-input-content-identity", source_identity)
for name, dtype, value in (
("schema_version", "<u8", 1), ("increment_count", "<u8", 100),
("maximum_increments", "<u8", 1000), ("alpha", "<f8", -0.05),
("beta", "<f8", 0.275625), ("gamma", "<f8", 0.55),
("initial_increment", "<f8", INCREMENT), ("time_period", "<f8", 10.0),
):
numeric_attribute(metadata, name, dtype, value)
table = dataset(file, "model/nodes", (NODE_COUNT,))
require(table.dtype.names == ("internal_node_id", "instance_name", "source_label", "coordinates"),
"wrong-node-schema", str(table.dtype))
require(table.dtype["internal_node_id"] == np.dtype("<u8")
and table.dtype["coordinates"] == np.dtype(("<f8", (3,))),
"wrong-node-schema", str(table.dtype))
for name in ("instance_name", "source_label"):
string = h5py.check_string_dtype(table.dtype[name])
require(string is not None and string.length is None and string.encoding == "utf-8",
"wrong-node-schema", name)
node_indices: dict[int, int] = {}
for index, node in enumerate(table[:]):
require(int(node["internal_node_id"]) == index, "wrong-internal-node-identity", str(index))
instance = canonical_instance(text_value(node["instance_name"]))
label = positive_label(text_value(node["source_label"]))
require(instance == INSTANCE, "wrong-result-instance", instance)
require(label not in node_indices, "duplicate-result-node", str(label))
require(label in nodes, "extra-result-node", str(label))
require(np.isfinite(node["coordinates"]).all(), "nonfinite-result-node", str(label))
require(tuple(node["coordinates"]) == nodes[label], "wrong-result-coordinate", str(label))
node_indices[label] = index
require(node_indices.keys() == nodes.keys(), "result-node-inventory", "Source node inventories differ.")
require("steps" in file and isinstance(file["steps"], h5py.Group)
and set(file["steps"]) == {STEP}, "wrong-result-step", "Expected one accepted step.")
accepted = dataset(file, f"steps/{STEP}/accepted_frames", (FRAME_COUNT,))
expected_fields = (("frame_index", "<u8"), ("increment_index", "<u8"),
("time", "<f8"), ("iteration_count", "<u8"), ("is_initial", "u1"))
require(accepted.dtype.names == tuple(name for name, _ in expected_fields)
and all(accepted.dtype[name] == np.dtype(dtype) for name, dtype in expected_fields),
"wrong-frame-table-schema", str(accepted.dtype))
frame_path = f"steps/{STEP}/frames"
require(frame_path in file and isinstance(file[frame_path], h5py.Group)
and set(file[frame_path]) == {str(i) for i in range(FRAME_COUNT)},
"result-frame-inventory", "Missing, extra, or noncanonical frame group.")
selection = [node_indices[label] for label in NODE_LABELS]
for frame, row in enumerate(accepted[:]):
scheduled_time = float(frame) * INCREMENT
require(int(row["frame_index"]) == frame and int(row["increment_index"]) == frame
and math.isfinite(row["time"]) and float(row["time"]) == scheduled_time
and int(row["is_initial"]) == int(frame == 0)
and 0 <= int(row["iteration_count"]) <= 100
and (frame != 0 or (int(row["iteration_count"]) == 0 and not np.signbit(row["time"]))),
"wrong-result-frame-time", str(frame))
displacement = dataset(file, f"{frame_path}/{frame}/nodal/displacement", (NODE_COUNT, 6))
require(displacement.dtype == np.dtype("<f8"), "wrong-displacement-type", str(frame))
for name, value in {
"component_names": "UX,UY,UZ,URX,URY,URZ",
"component_unit_dimensions": "length,length,length,1,1,1",
"coordinate_system": "global-cartesian", "location": "nodal",
"step_name": STEP, "source_step_name": STEP,
"rotation_representation": "total-rotation-vector-chart",
"quantity_semantics": "global-translation-and-total-rotation-vector",
}.items():
string_attribute(displacement, name, value)
for name, dtype, value in (
("schema_version", "<u8", 1), ("frame_index", "<u8", frame),
("increment_index", "<u8", frame), ("time", "<f8", scheduled_time),
("is_initial", "u1", int(frame == 0)),
):
numeric_attribute(displacement, name, dtype, value)
values = displacement[:]
require(np.isfinite(values).all(), "nonfinite-result-value", str(frame))
if frame == 0:
require((values == 0.0).all(), "nonzero-initial-displacement", "Frame zero must have q0=0.")
projected.append(values[selection])
return projected, {"source_input_identity": source_identity, "source_node_count": NODE_COUNT,
"accepted_frame_count": FRAME_COUNT, "checked_result_scalars": NODE_COUNT * FRAME_COUNT * 6,
"projected_node_labels": list(NODE_LABELS)}
def evaluate_family(increment: int, quantity: str, result: list[float],
reference: list[float]) -> dict[str, Any]:
"""Apply the common policy exactly, including zero-scale and family RMS."""
require(len(result) == len(reference) and len(result) > 0, "scalar-inventory", quantity)
require(all(math.isfinite(value) for value in (*result, *reference)),
"nonfinite-comparison-value", quantity)
scale = max(abs(value) for value in reference)
band = POLICY["near_zero_ratio"] * scale
rows = []
error_norm = 0.0
for index, (actual, expected) in enumerate(zip(result, reference)):
error = abs(actual - expected)
require(math.isfinite(error), "nonfinite-comparison-value", quantity)
error_norm = math.hypot(error_norm, error)
require(math.isfinite(error_norm), "nonfinite-comparison-value", quantity)
relative_error = None
if scale == 0.0:
branch, threshold, passed = "zero-scale-exact", 0.0, error == 0.0
elif abs(expected) <= band:
branch, threshold, passed = "near-zero", band, error <= band
else:
branch = "relative"
threshold = POLICY["relative_tolerance"] * abs(expected)
relative_error = error / abs(expected)
require(math.isfinite(relative_error), "nonfinite-comparison-value", quantity)
passed = relative_error <= POLICY["relative_tolerance"]
rows.append({"instance": INSTANCE, "node_label": NODE_LABELS[index // 3],
"component": f"{quantity}{index % 3 + 1}", "fesa_value": actual,
"reference_value": expected, "absolute_error": error,
"relative_error": relative_error, "branch": branch,
"threshold": threshold, "passed": passed})
rows_passed = all(row["passed"] for row in rows)
rms = error_norm / math.sqrt(len(rows)) / scale if scale else None
require(rms is None or math.isfinite(rms), "nonfinite-comparison-value", quantity)
rms_passed = rows_passed if scale == 0.0 else rms <= POLICY["relative_rms_tolerance"]
return {"identity": f"{quantity}-global:{STEP}:increment-{increment}", "case": CASE,
"step": STEP, "increment": increment, "hdf5_time": float(increment) * INCREMENT,
"reference_decimal_time": str(Decimal(increment) / Decimal(10)),
"quantity": quantity, "components": [f"{quantity}{i}" for i in (1, 2, 3)],
"unit_dimension": "length" if quantity == "U" else "rad",
"coordinate_system": "global-cartesian" if quantity == "U" else "fixed-initial-global-total-rotation-vector",
"impact": "blocking" if quantity == "U" else "warning-only", "row_count": len(rows),
"reference_scale": scale, "near_zero_band": band, "rows": rows,
"rows_passed": rows_passed, "relative_rms": rms, "rms_passed": rms_passed,
"diagnostic_code": "zero-reference-scale-nonzero-error" if scale == 0.0 and not rows_passed else None,
"passed": rows_passed and rms_passed}
def compare(input_path: Path, reference_path: Path, results_path: Path) -> dict[str, Any]:
report: dict[str, Any] = {
"schema": "hht-reference-comparison-v1", "case": CASE, "policy": dict(POLICY),
"identities": {}, "precheck": {"passed": False}, "families": [],
"blocking_failures": [], "warnings": [], "blocking_passed": False, "verdict": "failed",
}
try:
for name, path in (("input", input_path), ("reference", reference_path), ("results", results_path)):
report["identities"][name] = {"path": str(path.absolute())}
with path.open("rb") as stream:
report["identities"][name]["sha256"] = hashlib.file_digest(stream, "sha256").hexdigest()
nodes, content_identity = input_inventory(input_path)
reference = read_reference(reference_path)
result, evidence = read_results(results_path, nodes, content_identity)
families = []
for frame in range(FRAME_COUNT):
for quantity, first in (("U", 0), ("UR", 3)):
actual = [float(value) for row in result[frame] for value in row[first:first + 3]]
expected = [value for label in NODE_LABELS for value in reference[(frame, label)][first:first + 3]]
families.append(evaluate_family(frame, quantity, actual, expected))
report["precheck"] = {"passed": True, "reference_row_count": len(reference),
"reference_frame_count": FRAME_COUNT, **evidence}
report["families"] = families
for family in families:
if family["passed"]:
continue
if family["impact"] == "blocking":
report["blocking_failures"].append(family["identity"])
else:
if not family["rows_passed"]:
report["warnings"].append({"family": family["identity"], "code": "row-tolerance-failure"})
if not family["rms_passed"]:
report["warnings"].append({"family": family["identity"], "code": "relative-rms-failure"})
report["blocking_passed"] = not report["blocking_failures"]
report["verdict"] = "passed" if report["blocking_passed"] else "failed"
except (ComparisonError, OSError, ValueError, InvalidOperation, UnicodeError) as error:
report["precheck"] = {"passed": False, "code": getattr(error, "code", "invalid-comparison-input"),
"message": str(error)}
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--input", type=Path, default=REFERENCE_DIRECTORY / f"{CASE}.inp")
parser.add_argument("--reference", type=Path, default=REFERENCE_DIRECTORY / f"{CASE} displacements.csv")
parser.add_argument("--results", type=Path)
parser.add_argument("--report", type=Path, required=True)
args = parser.parse_args()
results = args.results or args.input.with_name(args.input.stem + "_results.h5")
if args.report.resolve() in {args.input.resolve(), args.reference.resolve(), results.resolve()}:
parser.error("The report path must not overwrite an input, reference, or solver result.")
report = compare(args.input, args.reference, results)
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(report, indent=2, sort_keys=True, allow_nan=False) + "\n", encoding="utf-8")
print(json.dumps({"verdict": report["verdict"], "precheck": report["precheck"],
"blocking_failure_count": len(report["blocking_failures"]),
"warning_count": len(report["warnings"]), "report": str(args.report)}, allow_nan=False))
return 2 if not report["precheck"]["passed"] else (0 if report["blocking_passed"] else 1)
if __name__ == "__main__":
raise SystemExit(main())
+7
View File
@@ -3,7 +3,12 @@ add_library(
STATIC
analysis/analysis_model.cpp
analysis/analysis_state.cpp
analysis/dynamic_analysis_state.cpp
analysis/eigen_reduction.cpp
analysis/hht_time_integration.cpp
analysis/hht_dynamic_algebra.cpp
analysis/hht_dynamic_assembly.cpp
analysis/hht_dynamic_procedure.cpp
analysis/linear_frequency_procedure.cpp
analysis/linear_static_analysis.cpp
analysis/linear_static_procedure.cpp
@@ -29,6 +34,7 @@ add_library(
elements/finite_rotation.cpp
elements/mitc4_shell.cpp
elements/nonlinear_mitc4_shell.cpp
elements/nonlinear_mitc4_inertia.cpp
fem/dof_manager.cpp
io/abaqus/domain_builder.cpp
io/abaqus/domain_mapping_context.cpp
@@ -39,6 +45,7 @@ add_library(
io/abaqus/surface_definition_mapper.cpp
io/abaqus/topology_mapper.cpp
io/hdf5/hdf5_atomic_file.cpp
io/hdf5/dynamic_hdf5_results_writer.cpp
io/hdf5/hdf5_model_writer.cpp
io/hdf5/nonlinear_hdf5_results_writer.cpp
io/hdf5/hdf5_primitives.cpp
@@ -0,0 +1,406 @@
#include "fesa/analysis/dynamic_analysis_state.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <string>
#include <utility>
#include "fesa/elements/element_definition.h"
#include "fesa/elements/finite_rotation.h"
namespace fesa {
namespace {
constexpr std::size_t kDofsPerNode = 6U;
constexpr std::size_t kAngularDofsPerNode = 3U;
constexpr std::size_t kShellLocationsPerElement = 4U;
constexpr std::uint32_t kMaximumNewtonIterations = 100U;
/// @brief Reports a rejected state without changing committed history.
Status Failure(const FailureCategory category, const std::string& message) {
return Status::Failure(category, {{Severity::kError,
"dynamic-invalid-state",
{},
"DYNAMIC_ANALYSIS_STATE",
{},
message}});
}
/// @brief Compares canonical and raw source identity exactly.
bool SameId(const SourceEntityId& left, const SourceEntityId& right) {
return left.instance_name == right.instance_name &&
left.source_label == right.source_label &&
left.source_label_text == right.source_label_text;
}
/// @brief Checks all components of one fixed-size physical record.
template <std::size_t Size>
bool IsFinite(const std::array<double, Size>& values) {
return std::all_of(values.begin(), values.end(),
[](double value) { return std::isfinite(value); });
}
/// @brief Checks every vector component before state acceptance.
bool IsFinite(const Vector& values) {
for (std::size_t index = 0U; index < values.Size(); ++index) {
if (!std::isfinite(values[index])) {
return false;
}
}
return true;
}
/// @brief Checks all physical recovery quantities for finite values.
bool IsFinite(const ShellResultRow& row) {
if (!IsFinite(row.natural_coordinates) || !IsFinite(row.generalized_strain) ||
!IsFinite(row.section_resultant)) {
return false;
}
for (const auto& axis : row.local_frame) {
if (!IsFinite(axis)) {
return false;
}
}
return std::all_of(row.stress.begin(), row.stress.end(),
[](const ShellSectionStressRow& stress) {
return std::isfinite(stress.zeta) &&
IsFinite(stress.components);
});
}
/// @brief Collects stable Domain indices for recovery identity.
std::vector<EntityIndex> ShellElementOrder(const Domain& domain) {
std::vector<EntityIndex> order;
for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) {
if (domain.Elements()[index].Kind() == ElementDefinitionKind::kMitc4Shell) {
order.push_back(static_cast<EntityIndex>(index));
}
}
return order;
}
/// @brief Requires complete ordered Gauss and section-position rows.
Status ValidateShellRows(const DynamicAcceptedFrameState& frame,
const std::vector<EntityIndex>& shell_order) {
if (shell_order.size() > (std::numeric_limits<std::size_t>::max)() /
kShellLocationsPerElement ||
frame.shell_results.size() !=
shell_order.size() * kShellLocationsPerElement) {
return Failure(FailureCategory::kSolver,
"Shell recovery inventory does not match the Domain.");
}
constexpr std::array<ShellMidsurfaceLocation, kShellLocationsPerElement>
kLocations{ShellMidsurfaceLocation::kGp1, ShellMidsurfaceLocation::kGp2,
ShellMidsurfaceLocation::kGp3, ShellMidsurfaceLocation::kGp4};
constexpr std::array<ShellSectionPosition, 3U> kPositions{
ShellSectionPosition::kBottom, ShellSectionPosition::kMiddle,
ShellSectionPosition::kTop};
constexpr std::array<double, 3U> kZeta{-1.0, 0.0, 1.0};
const double gauss = 1.0 / std::sqrt(3.0);
const std::array<std::array<double, 2U>, 4U> natural{
std::array<double, 2U>{-gauss, -gauss},
{gauss, -gauss},
{gauss, gauss},
{-gauss, gauss}};
for (std::size_t element = 0U; element < shell_order.size(); ++element) {
for (std::size_t point = 0U; point < kShellLocationsPerElement; ++point) {
const auto& row =
frame.shell_results[element * kShellLocationsPerElement + point];
if (row.element != shell_order[element] ||
row.location != kLocations[point] ||
row.natural_coordinates != natural[point] || !IsFinite(row)) {
return Failure(FailureCategory::kSolver,
"Shell recovery rows have invalid identity or values.");
}
for (std::size_t position = 0U; position < kPositions.size();
++position) {
if (row.stress[position].position != kPositions[position] ||
row.stress[position].zeta != kZeta[position]) {
return Failure(FailureCategory::kSolver,
"Shell stress rows have invalid section identity.");
}
}
}
}
return Status::Ok();
}
} // namespace
Result<DynamicAnalysisState> DynamicAnalysisState::Create(
const Domain& domain, const DofManager& dofs) {
const DynamicStepDefinition* const dynamic = domain.Dynamic();
const std::size_t node_count = domain.Nodes().size();
if (dynamic == nullptr || dynamic->increment_count == 0U ||
!std::isfinite(dynamic->initial_increment) ||
!std::isfinite(dynamic->time_period) ||
dynamic->initial_increment <= 0.0 ||
dynamic->increment_count > dynamic->maximum_increments ||
!std::isfinite(dynamic->alpha) || dynamic->alpha < -1.0 / 3.0 ||
dynamic->alpha > 0.0 ||
dynamic->beta != 0.25 * (1.0 - dynamic->alpha) * (1.0 - dynamic->alpha) ||
dynamic->gamma != 0.5 - dynamic->alpha || dynamic->time_period <= 0.0 ||
static_cast<double>(dynamic->increment_count) *
dynamic->initial_increment !=
dynamic->time_period ||
node_count == 0U || domain.ShellElements().Empty() ||
domain.Elements().Size() != domain.ShellElements().Size() ||
domain.ShellNodeInitialFrames().size() != node_count ||
node_count > (std::numeric_limits<std::size_t>::max)() / kDofsPerNode ||
dofs.FullDofCount() != node_count * kDofsPerNode ||
!dofs.ValidateInvariants().IsOk()) {
return Result<DynamicAnalysisState>::Failure(
Failure(FailureCategory::kModel,
"Dynamic schedule and full-DOF inventory must be valid."));
}
for (std::size_t index = 0U; index < dofs.PrescribedValues().Size();
++index) {
if (dofs.PrescribedValues()[index] != 0.0) {
return Result<DynamicAnalysisState>::Failure(
Failure(FailureCategory::kModel,
"Dynamic prescribed components must be exactly zero."));
}
}
std::vector<SourceEntityId> source_ids;
source_ids.reserve(node_count);
for (const Node& node : domain.Nodes()) {
source_ids.push_back(node.source_id);
}
return Result<DynamicAnalysisState>::Success(DynamicAnalysisState{
dofs.FullDofCount(), dynamic->increment_count, dynamic->initial_increment,
ShellElementOrder(domain), dofs.ConstrainedDofs(), std::move(source_ids),
domain.ShellNodeInitialFrames()});
}
Result<DynamicAcceptedFrameState>
DynamicAnalysisState::BeginTrialFromCommitted() const {
if (accepted_frames_.empty()) {
return Result<DynamicAcceptedFrameState>::Failure(
Failure(FailureCategory::kSolver,
"A dynamic trial requires an accepted frame zero."));
}
return Result<DynamicAcceptedFrameState>::Success(accepted_frames_.back());
}
Status DynamicAnalysisState::CommitAcceptedFrame(
const DynamicAcceptedFrameState& frame) {
if (!initialized_) {
return Failure(FailureCategory::kSolver,
"A default dynamic state cannot accept frames.");
}
const std::size_t expected_frame = accepted_frames_.size();
if (expected_frame > (std::numeric_limits<std::uint32_t>::max)() ||
frame.frame_index != expected_frame ||
frame.increment_index != expected_frame ||
frame.increment_index > increment_count_ || !std::isfinite(frame.time) ||
frame.time !=
static_cast<double>(frame.increment_index) * initial_increment_ ||
frame.is_initial != (frame.frame_index == 0U) ||
(frame.is_initial && frame.iteration_count != 0U) ||
frame.iteration_count > kMaximumNewtonIterations ||
(frame.is_initial && std::signbit(frame.time))) {
return Failure(FailureCategory::kSolver,
"Frame schedule or iteration identity is invalid.");
}
if (!accepted_frames_.empty() && frame.time <= accepted_frames_.back().time) {
return Failure(FailureCategory::kSolver,
"Accepted dynamic frame times must strictly increase.");
}
const std::size_t node_count = nodal_source_ids_.size();
if (frame.nodal_internal_order.size() != node_count ||
frame.nodal_source_ids.size() != node_count ||
frame.nodal_triad.size() != node_count ||
frame.nodal_director.size() != node_count) {
return Failure(FailureCategory::kSolver,
"Frame node inventory does not match the Domain.");
}
for (std::size_t node = 0U; node < node_count; ++node) {
if (frame.nodal_internal_order[node] != node ||
!SameId(frame.nodal_source_ids[node], nodal_source_ids_[node]) ||
!IsFinite(frame.nodal_triad[node][0U]) ||
!IsFinite(frame.nodal_triad[node][1U]) ||
!IsFinite(frame.nodal_triad[node][2U]) ||
!IsFinite(frame.nodal_director[node])) {
return Failure(FailureCategory::kSolver,
"Frame nodal identity or orientation is invalid.");
}
}
const std::array<const Vector*, 10U> full_vectors{
&frame.generalized_coordinates,
&frame.velocity,
&frame.acceleration,
&frame.algorithmic_acceleration,
&frame.inertia_force,
&frame.internal_force,
&frame.external_force,
&frame.residual,
&frame.reaction,
&frame.residual_scale};
for (const Vector* values : full_vectors) {
if (values->Size() != full_dof_count_ || !IsFinite(*values)) {
return Failure(FailureCategory::kSolver,
"Frame generalized vector inventory is invalid.");
}
}
// N5 preserves the existing chart and 1e-12 orientation consistency checks.
for (std::size_t node = 0U; node < node_count; ++node) {
Matrix initial{3U, 3U};
for (std::size_t component = 0U; component < 3U; ++component) {
initial(component, 0U) = initial_frames_[node].tangent_a[component];
initial(component, 1U) = initial_frames_[node].tangent_b[component];
initial(component, 2U) = initial_frames_[node].director[component];
}
const std::size_t offset = node * kDofsPerNode;
const Vector3 phi{frame.generalized_coordinates[offset + 3U],
frame.generalized_coordinates[offset + 4U],
frame.generalized_coordinates[offset + 5U]};
const auto rotation = MakeFiniteRotationState(phi, initial);
if (!rotation.HasValue()) {
return Failure(FailureCategory::kSolver,
"Frame rotation is outside the valid initial chart.");
}
for (std::size_t axis = 0U; axis < 3U; ++axis) {
for (std::size_t component = 0U; component < 3U; ++component) {
if (std::abs(frame.nodal_triad[node][axis][component] -
rotation.Value().q(component, axis)) > 1.0e-12) {
return Failure(FailureCategory::kSolver,
"Frame triad does not match its rotation coordinate.");
}
}
if (std::abs(frame.nodal_director[node][axis] -
rotation.Value().q(axis, 2U)) > 1.0e-12) {
return Failure(
FailureCategory::kSolver,
"Frame director does not match its rotation coordinate.");
}
}
}
const std::array<const Vector*, 6U> hidden_vectors{
&frame.hidden_constraint,
&frame.hidden_velocity_defect,
&frame.hidden_acceleration_defect,
&frame.hidden_constraint_scale,
&frame.hidden_velocity_defect_scale,
&frame.hidden_acceleration_defect_scale};
for (const Vector* values : hidden_vectors) {
if (values->Size() != frame.hidden_constraint.Size() ||
!IsFinite(*values)) {
return Failure(FailureCategory::kSolver,
"Frame hidden-constraint evidence has invalid size.");
}
}
if ((!accepted_frames_.empty() &&
frame.hidden_constraint.Size() != hidden_constraint_count_) ||
frame.angular_velocity.Size() != node_count * kAngularDofsPerNode ||
frame.angular_acceleration.Size() != node_count * kAngularDofsPerNode ||
!IsFinite(frame.angular_velocity) ||
!IsFinite(frame.angular_acceleration) ||
!std::isfinite(frame.kinetic_energy) ||
!std::isfinite(frame.physical_strain_energy) ||
!std::isfinite(frame.drilling_energy) ||
!std::isfinite(frame.external_work) ||
!std::isfinite(frame.energy_defect) ||
!IsFinite(frame.global_equilibrium) || !IsFinite(frame.linear_momentum) ||
!IsFinite(frame.angular_momentum) ||
!IsFinite(frame.linear_momentum_rate) ||
!IsFinite(frame.angular_momentum_rate) ||
!IsFinite(frame.free_residual_metrics)) {
return Failure(FailureCategory::kSolver,
"Frame physical evidence is nonfinite or has invalid size.");
}
for (const std::size_t dof : constrained_dofs_) {
if (frame.generalized_coordinates[dof] != 0.0 ||
frame.velocity[dof] != 0.0 || frame.acceleration[dof] != 0.0 ||
frame.algorithmic_acceleration[dof] != 0.0) {
return Failure(FailureCategory::kSolver,
"Fixed boundary components must remain exactly zero.");
}
}
for (const Vector* scales :
{&frame.hidden_constraint_scale, &frame.hidden_velocity_defect_scale,
&frame.hidden_acceleration_defect_scale}) {
for (std::size_t index = 0U; index < scales->Size(); ++index) {
if ((*scales)[index] < 0.0) {
return Failure(FailureCategory::kSolver,
"Hidden evidence scales must be nonnegative.");
}
}
}
if (frame.free_residual_metrics[0U] < 0.0 ||
frame.free_residual_metrics[1U] < 0.0 ||
(frame.is_initial &&
(frame.external_work != 0.0 || frame.energy_defect != 0.0))) {
return Failure(FailureCategory::kSolver,
"Frame metric or initial energy evidence is invalid.");
}
std::size_t constraint = 0U;
for (std::size_t dof = 0U; dof < full_dof_count_; ++dof) {
const bool constrained = constraint < constrained_dofs_.size() &&
constrained_dofs_[constraint] == dof;
if (constrained) ++constraint;
if (frame.residual[dof] != frame.inertia_force[dof] +
frame.internal_force[dof] -
frame.external_force[dof] ||
frame.reaction[dof] != (constrained ? frame.residual[dof] : 0.0) ||
frame.residual_scale[dof] < 0.0 ||
(!accepted_frames_.empty() &&
frame.external_force[dof] !=
accepted_frames_.front().external_force[dof]) ||
(frame.is_initial &&
(frame.generalized_coordinates[dof] != 0.0 ||
frame.velocity[dof] != 0.0 ||
frame.acceleration[dof] != frame.algorithmic_acceleration[dof]))) {
return Failure(
FailureCategory::kSolver,
"Frame force, reaction, or initial-state identity is invalid.");
}
}
const Status shell_status = ValidateShellRows(frame, shell_element_order_);
if (!shell_status.IsOk()) {
return shell_status;
}
const std::size_t hidden_constraint_count = frame.hidden_constraint.Size();
// Copy insertion preserves the previous vector when allocation/copy fails.
accepted_frames_.push_back(frame);
hidden_constraint_count_ = hidden_constraint_count;
return Status::Ok();
}
void DynamicAnalysisState::DiscardTrial() noexcept {}
const std::vector<DynamicAcceptedFrameState>&
DynamicAnalysisState::AcceptedFrames() const noexcept {
return accepted_frames_;
}
const DynamicAcceptedFrameState& DynamicAnalysisState::CommittedState() const {
if (accepted_frames_.empty()) {
throw std::logic_error{
"Committed dynamic state is unavailable before frame zero."};
}
return accepted_frames_.back();
}
bool DynamicAnalysisState::IsInitialized() const noexcept {
return initialized_;
}
DynamicAnalysisState::DynamicAnalysisState(
const std::size_t full_dof_count, const std::uint32_t increment_count,
const double initial_increment,
std::vector<EntityIndex> shell_element_order,
std::vector<std::size_t> constrained_dofs,
std::vector<SourceEntityId> nodal_source_ids,
std::vector<ShellNodeInitialFrame> initial_frames)
: full_dof_count_{full_dof_count},
increment_count_{increment_count},
initial_increment_{initial_increment},
shell_element_order_{std::move(shell_element_order)},
constrained_dofs_{std::move(constrained_dofs)},
nodal_source_ids_{std::move(nodal_source_ids)},
initial_frames_{std::move(initial_frames)},
initialized_{true} {}
} // namespace fesa
+732
View File
@@ -0,0 +1,732 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <iomanip>
#include <limits>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include "analysis/hht_dynamic_internal.h"
#include "fesa/math/matrix.h"
#include "fesa/solvers/linear/linear_solver.h"
#include "fesa/solvers/linear/tangent_solver.h"
namespace fesa {
namespace hht_internal {
namespace {
/// @brief Attaches a fail-closed dynamic diagnostic to the source model.
Status Failure(const Domain& domain, const std::string& code,
const std::string& message) {
return Status::Failure(FailureCategory::kSolver, {{Severity::kError,
code,
{domain.SourcePath(), 0U},
"*DYNAMIC",
{},
message}});
}
/// @brief Reads one structural entry, including an implicit exact zero.
double Entry(const SparseMatrix& matrix, const std::size_t row,
const std::size_t column) {
const auto first = matrix.ColumnIndices().begin() +
static_cast<std::ptrdiff_t>(matrix.RowOffsets()[row]);
const auto last = matrix.ColumnIndices().begin() +
static_cast<std::ptrdiff_t>(matrix.RowOffsets()[row + 1U]);
const auto found = std::lower_bound(first, last, column);
return found != last && *found == column
? matrix.Values()[static_cast<std::size_t>(
found - matrix.ColumnIndices().begin())]
: 0.0;
}
/// @brief Applies the fixed translation length scale by full-space row.
double Scale(const std::size_t full_dof, const double length) {
return full_dof % 6U < 3U ? length : 1.0;
}
/// @brief Checks a dimensionless ratio without inventing a zero-scale floor.
bool Within(const double value, const double scale, const double tolerance) {
return std::isfinite(value) && std::isfinite(scale) && scale >= 0.0 &&
(scale == 0.0 ? value == 0.0 : std::abs(value) / scale <= tolerance);
}
/// @brief Verifies dimensions and finite values before algebraic contractions.
Status Validate(const Domain& domain, const DofManager& dofs,
const DynamicAssembly& assembly, const double length) {
const std::size_t count = dofs.FullDofCount();
if (!std::isfinite(length) || !(length > 0.0) || count % 6U != 0U ||
assembly.rotations.size() != count / 6U) {
return Failure(domain, "dynamic-invalid-algebra-state",
"Dynamic algebra requires complete rotations and a positive "
"reference length.");
}
for (const Vector* vector :
{&assembly.frame.velocity, &assembly.frame.acceleration,
&assembly.frame.internal_force, &assembly.frame.external_force,
&assembly.frame.inertia_force, &assembly.frame.residual_scale,
&assembly.force_directional_second,
&assembly.force_directional_second_scale}) {
if (vector->Size() != count) {
return Failure(
domain, "dynamic-invalid-algebra-state",
"Dynamic algebra vectors must match the full equation space.");
}
for (std::size_t i = 0; i < count; ++i) {
if (!std::isfinite((*vector)[i])) {
return Failure(domain, "dynamic-invalid-algebra-state",
"Dynamic algebra vectors must remain finite.");
}
}
}
for (const Vector* vector :
{&assembly.hidden_second, &assembly.hidden_second_scale}) {
if (vector->Size() != assembly.rotations.size()) {
return Failure(
domain, "dynamic-invalid-algebra-state",
"Exact scalar hidden Hessian evidence must cover every node.");
}
for (std::size_t i = 0; i < vector->Size(); ++i) {
if (!std::isfinite((*vector)[i]) ||
(vector == &assembly.hidden_second_scale && (*vector)[i] < 0.0)) {
return Failure(domain, "dynamic-invalid-algebra-state",
"Scalar hidden Hessian evidence must be finite with "
"nonnegative scale.");
}
}
}
for (const SparseMatrix* matrix : {&assembly.mass, &assembly.internal_tangent,
&assembly.drilling_metric}) {
if (matrix->Rows() != count || matrix->Columns() != count ||
!matrix->Validate().IsOk()) {
return Failure(
domain, "dynamic-invalid-algebra-state",
"Dynamic algebra operators must match the full equation space.");
}
}
for (const auto& rotation : assembly.rotations) {
if (!rotation.drill_null.IsFinite() ||
!(std::abs(rotation.drill_null.Dot(rotation.drill_null) - 1.0) <=
1.0e-12)) {
return Failure(
domain, "dynamic-invalid-analytic-null",
"Every analytic drilling null must remain finite and unit length.");
}
}
return Status::Ok();
}
using BasisRow = std::vector<std::pair<std::size_t, double>>;
/// @brief Holds sparse nodal physical-complement columns and their mass.
struct PhysicalSpace {
std::vector<BasisRow> rows;
SparseMatrix mass;
};
/// @brief Checks a nonempty nodal physical block without removing small pivots.
Status CheckLocalMass(const Domain& domain, const SparseMatrix& matrix,
const std::size_t begin, const std::size_t end) {
if (begin == end) return Status::Ok();
const std::size_t size = end - begin;
std::array<std::array<double, 6>, 6> block{};
std::array<std::array<double, 6>, 6> lower{};
double scale = 0.0;
for (std::size_t i = 0; i < size; ++i) {
for (std::size_t j = 0; j < size; ++j) {
block[i][j] = Entry(matrix, begin + i, begin + j);
scale = std::max(scale, std::abs(block[i][j]));
}
}
if (!std::isfinite(scale) || !(scale > 0.0)) {
return Failure(domain, "dynamic-positive-mass",
"A nonempty physical mass block has no positive scale.");
}
double norm = 0.0;
for (std::size_t i = 0; i < size; ++i) {
double sum = 0.0;
for (std::size_t j = 0; j < size; ++j) {
block[i][j] /= scale;
sum += std::abs(block[i][j]);
}
norm = std::max(norm, sum);
for (std::size_t j = 0; j <= i; ++j) {
double value = block[i][j];
for (std::size_t k = 0; k < j; ++k) value -= lower[i][k] * lower[j][k];
if (!std::isfinite(value) || (i == j && !(value > 0.0))) {
return Failure(
domain, "dynamic-positive-mass",
"Physical mass requires strictly positive Cholesky pivots.");
}
lower[i][j] = i == j ? std::sqrt(value) : value / lower[j][j];
}
}
std::array<double, 6> sums{};
for (std::size_t column = 0; column < size; ++column) {
std::array<double, 6> solution{};
for (std::size_t i = 0; i < size; ++i) {
double value = i == column ? 1.0 : 0.0;
for (std::size_t k = 0; k < i; ++k) value -= lower[i][k] * solution[k];
solution[i] = value / lower[i][i];
}
for (std::size_t reverse = size; reverse > 0U; --reverse) {
const std::size_t i = reverse - 1U;
double value = solution[i];
for (std::size_t k = i + 1U; k < size; ++k)
value -= lower[k][i] * solution[k];
solution[i] = value / lower[i][i];
sums[i] += std::abs(solution[i]);
}
}
const double inverse_norm = *std::max_element(sums.begin(), sums.end());
const double rcond = 1.0 / norm / inverse_norm;
if (!std::isfinite(inverse_norm) || !std::isfinite(rcond) ||
!(rcond >= std::ldexp(1.0, -32))) {
return Failure(
domain, "dynamic-positive-mass-conditioning",
"A nodal free physical block reciprocal condition is below 2^-32.");
}
return Status::Ok();
}
/// @brief Builds the actual free-space analytic complement, without rank cuts.
Result<PhysicalSpace> BuildPhysicalSpace(
const Domain& domain, const DofManager& dofs,
const DynamicAssembly& assembly, const double length,
const std::vector<std::size_t>& null_nodes, LinearSolver& solver) {
const auto scaled = ScaledFree(assembly.mass, dofs, length);
if (!scaled.HasValue())
return Result<PhysicalSpace>::Failure(scaled.GetStatus());
const SparseMatrix& mass = scaled.Value();
double mass_scale = 0.0;
for (const double entry : mass.Values())
mass_scale = std::max(mass_scale, std::abs(entry));
const std::size_t no_null = std::numeric_limits<std::size_t>::max();
std::vector<std::size_t> node_null(assembly.rotations.size(), no_null);
for (std::size_t i = 0; i < null_nodes.size(); ++i)
node_null[null_nodes[i]] = i;
double mass_norm = 0.0;
double symmetry_error = 0.0;
double null_error = 0.0;
if (mass_scale > 0.0) {
for (std::size_t row = 0; row < mass.Rows(); ++row) {
std::vector<double> action(null_nodes.size(), 0.0);
for (std::size_t index = mass.RowOffsets()[row];
index < mass.RowOffsets()[row + 1U]; ++index) {
const std::size_t column = mass.ColumnIndices()[index];
const double entry = mass.Values()[index] / mass_scale;
mass_norm = std::hypot(mass_norm, entry);
symmetry_error = std::hypot(
symmetry_error, entry - Entry(mass, column, row) / mass_scale);
const std::size_t full = dofs.FreeDofs()[column];
if (full % 6U >= 3U && node_null[full / 6U] != no_null) {
action[node_null[full / 6U]] +=
entry * assembly.rotations[full / 6U].drill_null[full % 6U - 3U];
}
}
for (const double value : action)
null_error = std::hypot(null_error, value);
}
}
if ((mass_norm > 0.0 && symmetry_error / mass_norm > 1.0e-12) ||
(!null_nodes.empty() && mass_norm > 0.0 &&
null_error / mass_norm /
std::sqrt(static_cast<double>(null_nodes.size())) >
1.0e-12)) {
return Result<PhysicalSpace>::Failure(
Failure(domain, "dynamic-invalid-mass-null",
"Scaled free mass symmetry or analytic null action failed."));
}
std::vector<BasisRow> rows(dofs.FullDofCount());
std::vector<std::size_t> node_starts;
std::size_t dimension = 0U;
for (std::size_t node = 0; node < assembly.rotations.size(); ++node) {
node_starts.push_back(dimension);
for (std::size_t component = 0; component < 3; ++component) {
if (dofs.FreeEquation(6U * node + component).has_value()) {
rows[6U * node + component].push_back({dimension++, 1.0});
}
}
std::vector<std::size_t> free_rotations;
for (std::size_t component = 0; component < 3; ++component) {
if (dofs.FreeEquation(6U * node + 3U + component).has_value()) {
free_rotations.push_back(component);
}
}
if (node_null[node] == no_null) {
for (const std::size_t component : free_rotations) {
rows[6U * node + 3U + component].push_back({dimension++, 1.0});
}
continue;
}
const Vector3 n = assembly.rotations[node].drill_null;
std::stable_sort(free_rotations.begin(), free_rotations.end(),
[&](const std::size_t a, const std::size_t b) {
return std::abs(n[a]) < std::abs(n[b]);
});
std::vector<Vector3> basis;
for (std::size_t column = 0; column + 1U < free_rotations.size();
++column) {
std::array<double, 3> axis{};
axis[free_rotations[column]] = 1.0;
Vector3 tangent = Vector3{axis} - n[free_rotations[column]] * n;
for (const Vector3& previous : basis)
tangent = tangent - tangent.Dot(previous) * previous;
const double norm = tangent.Norm();
if (!std::isfinite(norm) || !(norm > 0.0)) {
return Result<PhysicalSpace>::Failure(Failure(
domain, "dynamic-invalid-analytic-null",
"The free physical complement lost its analytic dimension."));
}
tangent = tangent / norm;
basis.push_back(tangent);
for (const std::size_t component : free_rotations) {
rows[6U * node + 3U + component].push_back(
{dimension, tangent[component]});
}
++dimension;
}
}
std::vector<CooContribution> entries;
node_starts.push_back(dimension);
double complement_gram_error = 0.0;
double null_gram_error = 0.0;
double cross_gram_error = 0.0;
for (std::size_t node = 0; node < assembly.rotations.size(); ++node) {
const std::size_t first = node_starts[node];
const std::size_t last = node_starts[node + 1U];
std::size_t free_count = 0U;
for (std::size_t c = 0; c < 6; ++c) {
if (dofs.FreeEquation(6U * node + c).has_value()) ++free_count;
}
const bool has_null = node_null[node] != no_null;
if (last - first + (has_null ? 1U : 0U) != free_count) {
return Result<PhysicalSpace>::Failure(Failure(
domain, "dynamic-invalid-analytic-null",
"The free physical and null bases do not span the free space."));
}
for (std::size_t a = first; a < last; ++a) {
for (std::size_t b = first; b < last; ++b) {
double gram = a == b ? -1.0 : 0.0;
for (std::size_t c = 0; c < 6; ++c) {
double pa = 0.0;
double pb = 0.0;
for (const auto& entry : rows[6U * node + c]) {
if (entry.first == a) pa = entry.second;
if (entry.first == b) pb = entry.second;
}
gram += pa * pb;
}
complement_gram_error = std::hypot(complement_gram_error, gram);
}
if (has_null) {
double gram = 0.0;
for (std::size_t c = 0; c < 3; ++c) {
for (const auto& entry : rows[6U * node + 3U + c]) {
if (entry.first == a)
gram += entry.second * assembly.rotations[node].drill_null[c];
}
}
cross_gram_error = std::hypot(cross_gram_error, gram);
}
}
if (has_null)
null_gram_error =
std::hypot(null_gram_error, assembly.rotations[node].drill_null.Dot(
assembly.rotations[node].drill_null) -
1.0);
}
const double full_gram_error = std::hypot(
complement_gram_error,
std::hypot(null_gram_error, std::sqrt(2.0) * cross_gram_error));
if (!(cross_gram_error <= 1.0e-12) || !(full_gram_error <= 1.0e-12) ||
(!null_nodes.empty() &&
!(null_gram_error / std::sqrt(static_cast<double>(null_nodes.size())) <=
1.0e-12))) {
return Result<PhysicalSpace>::Failure(Failure(
domain, "dynamic-invalid-analytic-null",
"The scaled physical and analytic null Gram identities failed."));
}
for (std::size_t i = 0; i < dimension; ++i)
entries.push_back({i, i, 0.0, 0U, entries.size()});
for (std::size_t row = 0; row < mass.Rows(); ++row) {
const auto& left = rows[dofs.FreeDofs()[row]];
for (std::size_t index = mass.RowOffsets()[row];
index < mass.RowOffsets()[row + 1U]; ++index) {
const auto& right = rows[dofs.FreeDofs()[mass.ColumnIndices()[index]]];
for (const auto& a : left) {
for (const auto& b : right) {
entries.push_back({a.first, b.first,
a.second * mass.Values()[index] * b.second, 0U,
entries.size()});
}
}
}
}
auto block = MakeSparse(dimension, dimension, std::move(entries));
if (!block.HasValue())
return Result<PhysicalSpace>::Failure(block.GetStatus());
for (std::size_t node = 0; node < assembly.rotations.size(); ++node) {
const Status local = CheckLocalMass(
domain, block.Value(), node_starts[node], node_starts[node + 1U]);
if (!local.IsOk()) return Result<PhysicalSpace>::Failure(local);
}
if (dimension > 0U) {
const Status factor = solver.Factorize(block.Value());
if (!factor.IsOk()) return Result<PhysicalSpace>::Failure(factor);
}
return Result<PhysicalSpace>::Success(
{std::move(rows), std::move(block.Value())});
}
/// @brief Builds psi_q from the current unit-null derivatives and force
/// Hessian.
Result<SparseMatrix> ConstraintGradient(const DynamicAssembly& assembly,
const std::vector<std::size_t>& nodes) {
std::vector<CooContribution> entries;
const auto& stiffness = assembly.internal_tangent;
for (std::size_t row = 0; row < nodes.size(); ++row) {
const std::size_t node = nodes[row];
const auto& rotation = assembly.rotations[node];
for (std::size_t c = 0; c < 3; ++c) {
const std::size_t full = 6U * node + 3U + c;
const double g = assembly.frame.internal_force[full] -
assembly.frame.external_force[full];
for (std::size_t d = 0; d < 3; ++d) {
entries.push_back({row, 6U * node + 3U + d,
rotation.null_gradient(c, d) * g, 0U,
entries.size()});
}
for (std::size_t index = stiffness.RowOffsets()[full];
index < stiffness.RowOffsets()[full + 1U]; ++index) {
entries.push_back({row, stiffness.ColumnIndices()[index],
rotation.drill_null[c] * stiffness.Values()[index],
0U, entries.size()});
}
}
}
return MakeSparse(nodes.size(), stiffness.Columns(), std::move(entries));
}
/// @brief Stores W-scaled regularity with exact inverse-action infinity norms.
struct Regularity {
SparseMatrix matrix;
Vector weights;
};
/// @brief Factors W^-1 psi_q N W^-1 and checks the prescribed condition bound.
Result<Regularity> CheckRegularity(const Domain& domain,
const DynamicAssembly& assembly,
const std::vector<std::size_t>& nodes,
const SparseMatrix& gradient,
TangentSolver& solver) {
const std::size_t no_null = std::numeric_limits<std::size_t>::max();
std::vector<std::size_t> node_null(assembly.rotations.size(), no_null);
Vector weights{nodes.size()};
for (std::size_t i = 0; i < nodes.size(); ++i) {
node_null[nodes[i]] = i;
const auto& n = assembly.rotations[nodes[i]].drill_null;
double stiffness = 0.0;
for (std::size_t a = 0; a < 3; ++a) {
for (std::size_t b = 0; b < 3; ++b) {
stiffness += n[a] *
Entry(assembly.drilling_metric, 6U * nodes[i] + 3U + a,
6U * nodes[i] + 3U + b) *
n[b];
}
}
if (!std::isfinite(stiffness) || !(stiffness > 0.0)) {
return Result<Regularity>::Failure(
Failure(domain, "dynamic-algebraic-regularity",
"Every admissible drill null requires positive finite "
"drilling metric."));
}
weights[i] = std::sqrt(stiffness);
}
std::vector<CooContribution> entries;
for (std::size_t row = 0; row < nodes.size(); ++row) {
entries.push_back({row, row, 0.0, 0U, entries.size()});
for (std::size_t index = gradient.RowOffsets()[row];
index < gradient.RowOffsets()[row + 1U]; ++index) {
const std::size_t full = gradient.ColumnIndices()[index];
if (full % 6U >= 3U && node_null[full / 6U] != no_null) {
const std::size_t column = node_null[full / 6U];
entries.push_back(
{row, column,
gradient.Values()[index] *
assembly.rotations[full / 6U].drill_null[full % 6U - 3U] /
weights[row] / weights[column],
0U, entries.size()});
}
}
}
auto matrix = MakeSparse(nodes.size(), nodes.size(), std::move(entries));
if (!matrix.HasValue())
return Result<Regularity>::Failure(matrix.GetStatus());
if (nodes.empty())
return Result<Regularity>::Success(
{std::move(matrix.Value()), std::move(weights)});
const Status factor = solver.FactorizeFresh(matrix.Value());
if (!factor.IsOk()) return Result<Regularity>::Failure(factor);
double norm = 0.0;
for (std::size_t row = 0; row < nodes.size(); ++row) {
double sum = 0.0;
for (std::size_t index = matrix.Value().RowOffsets()[row];
index < matrix.Value().RowOffsets()[row + 1U]; ++index)
sum += std::abs(matrix.Value().Values()[index]);
norm = std::max(norm, sum);
}
Vector inverse_sums{nodes.size()};
// Columns are discarded after their absolute row sums are accumulated;
// a global dense inverse is never retained.
for (std::size_t column = 0; column < nodes.size(); ++column) {
Vector rhs{nodes.size()};
rhs[column] = 1.0;
Vector solution{nodes.size()};
const Status solved = solver.SolveCorrection(rhs, solution);
if (!solved.IsOk()) return Result<Regularity>::Failure(solved);
const Status checked = CheckLinearSolve(matrix.Value(), rhs, solution);
if (!checked.IsOk()) return Result<Regularity>::Failure(checked);
for (std::size_t row = 0; row < nodes.size(); ++row)
inverse_sums[row] += std::abs(solution[row]);
}
double inverse_norm = 0.0;
for (std::size_t row = 0; row < nodes.size(); ++row)
inverse_norm = std::max(inverse_norm, inverse_sums[row]);
const double condition = 1.0 / norm / inverse_norm;
if (!std::isfinite(norm) || !std::isfinite(inverse_norm) ||
!std::isfinite(condition) || !(condition >= std::ldexp(1.0, -32))) {
std::ostringstream message;
message << std::setprecision(17)
<< "Scaled null regularity reciprocal infinity condition "
<< condition << " is below 2^-32; norm=" << norm
<< ", inverse-action norm=" << inverse_norm << ".";
return Result<Regularity>::Failure(
Failure(domain, "dynamic-algebraic-conditioning", message.str()));
}
return Result<Regularity>::Success(
{std::move(matrix.Value()), std::move(weights)});
}
/// @brief Recovers raw hidden equations and uncancelled derivative term scales.
Status HiddenEvidence(const Domain& domain, const DynamicAssembly& assembly,
const std::vector<std::size_t>& nodes,
const SparseMatrix& gradient,
DynamicAcceptedFrameState* frame,
const bool require_acceleration) {
Vector psi{nodes.size()};
Vector psi_scale{nodes.size()};
Vector velocity_defect = gradient.Multiply(frame->velocity);
Vector acceleration_defect = gradient.Multiply(frame->acceleration);
Vector velocity_scale{nodes.size()};
Vector acceleration_scale{nodes.size()};
for (std::size_t row = 0; row < nodes.size(); ++row) {
const std::size_t node = nodes[row];
const auto& rotation = assembly.rotations[node];
for (std::size_t index = gradient.RowOffsets()[row];
index < gradient.RowOffsets()[row + 1U]; ++index) {
const std::size_t column = gradient.ColumnIndices()[index];
velocity_scale[row] +=
std::abs(gradient.Values()[index] * frame->velocity[column]);
acceleration_scale[row] +=
std::abs(gradient.Values()[index] * frame->acceleration[column]);
}
for (std::size_t c = 0; c < 3; ++c) {
const std::size_t full = 6U * node + 3U + c;
const double g =
frame->internal_force[full] - frame->external_force[full];
const double n = rotation.drill_null[c];
psi[row] += n * g;
psi_scale[row] += std::abs(n) * frame->residual_scale[full];
}
acceleration_defect[row] += assembly.hidden_second[node];
acceleration_scale[row] += assembly.hidden_second_scale[node];
if (!Within(psi[row], psi_scale[row], 1.0e-10) ||
(frame->is_initial &&
!Within(velocity_defect[row], velocity_scale[row], 1.0e-10)) ||
(frame->is_initial && require_acceleration &&
!Within(acceleration_defect[row], acceleration_scale[row], 1.0e-10)) ||
!std::isfinite(velocity_defect[row]) ||
!std::isfinite(acceleration_defect[row]) ||
!std::isfinite(velocity_scale[row]) ||
!std::isfinite(acceleration_scale[row])) {
std::ostringstream message;
message << std::setprecision(17)
<< "Dynamic null consistency failed at node "
<< domain.Nodes()[node].source_id.instance_name << ":"
<< domain.Nodes()[node].source_id.source_label_text
<< "; psi=" << psi[row] << ", psi_scale=" << psi_scale[row]
<< ", velocity=" << velocity_defect[row]
<< ", velocity_scale=" << velocity_scale[row]
<< ", acceleration=" << acceleration_defect[row]
<< ", acceleration_scale=" << acceleration_scale[row]
<< ", initial=" << frame->is_initial
<< ", enforce_acceleration=" << require_acceleration << ".";
return Failure(domain, "dynamic-hidden-constraint", message.str());
}
}
frame->hidden_constraint = std::move(psi);
frame->hidden_constraint_scale = std::move(psi_scale);
frame->hidden_velocity_defect = std::move(velocity_defect);
frame->hidden_velocity_defect_scale = std::move(velocity_scale);
frame->hidden_acceleration_defect = std::move(acceleration_defect);
frame->hidden_acceleration_defect_scale = std::move(acceleration_scale);
return Status::Ok();
}
} // namespace
std::vector<std::size_t> NullNodes(
const DofManager& dofs,
const std::vector<DynamicRotationKinematics>& rotations) {
std::vector<std::size_t> nodes;
for (std::size_t node = 0; node < rotations.size(); ++node) {
bool admissible = true;
for (std::size_t component = 0; component < 3; ++component) {
if (!dofs.FreeEquation(6U * node + 3U + component).has_value() &&
rotations[node].drill_null[component] != 0.0)
admissible = false;
}
if (admissible) nodes.push_back(node);
}
return nodes;
}
Result<Vector> InitializeAcceleration(const Domain& domain,
const DofManager& dofs,
const DynamicAssembly& initial,
const double length,
LinearSolver& positive_solver,
TangentSolver& tangent_solver) {
const Status valid = Validate(domain, dofs, initial, length);
if (!valid.IsOk()) return Result<Vector>::Failure(valid);
const auto nodes = NullNodes(dofs, initial.rotations);
auto physical =
BuildPhysicalSpace(domain, dofs, initial, length, nodes, positive_solver);
if (!physical.HasValue())
return Result<Vector>::Failure(physical.GetStatus());
auto gradient = ConstraintGradient(initial, nodes);
if (!gradient.HasValue())
return Result<Vector>::Failure(gradient.GetStatus());
auto regularity =
CheckRegularity(domain, initial, nodes, gradient.Value(), tangent_solver);
if (!regularity.HasValue())
return Result<Vector>::Failure(regularity.GetStatus());
DynamicAcceptedFrameState candidate = initial.frame;
candidate.is_initial = true;
candidate.acceleration = Vector{dofs.FullDofCount()};
const Status consistent = HiddenEvidence(domain, initial, nodes,
gradient.Value(), &candidate, false);
if (!consistent.IsOk()) return Result<Vector>::Failure(consistent);
Vector rhs{physical.Value().mass.Rows()};
// The initialization evaluation has zero acceleration, so I(q,v,0)=c(q,v).
for (std::size_t full = 0; full < dofs.FullDofCount(); ++full) {
if (initial.frame.acceleration[full] != 0.0) {
return Result<Vector>::Failure(Failure(
domain, "dynamic-invalid-initial-acceleration",
"Initial inertia must be evaluated at zero unknown acceleration."));
}
const double force = initial.frame.external_force[full] -
initial.frame.internal_force[full] -
initial.frame.inertia_force[full];
for (const auto& entry : physical.Value().rows[full]) {
rhs[entry.first] += entry.second * Scale(full, length) * force;
}
}
Vector solution{rhs.Size()};
if (rhs.Size() > 0U) {
const Status solved = positive_solver.Solve(rhs, solution);
if (!solved.IsOk()) return Result<Vector>::Failure(solved);
const Status checked =
CheckLinearSolve(physical.Value().mass, rhs, solution);
if (!checked.IsOk()) return Result<Vector>::Failure(checked);
}
for (std::size_t full = 0; full < dofs.FullDofCount(); ++full) {
for (const auto& entry : physical.Value().rows[full]) {
candidate.acceleration[full] +=
Scale(full, length) * entry.second * solution[entry.first];
}
}
const Status physical_hidden = HiddenEvidence(
domain, initial, nodes, gradient.Value(), &candidate, false);
if (!physical_hidden.IsOk()) return Result<Vector>::Failure(physical_hidden);
if (!nodes.empty()) {
Vector null_rhs{nodes.size()};
for (std::size_t row = 0; row < nodes.size(); ++row) {
null_rhs[row] = -candidate.hidden_acceleration_defect[row] /
regularity.Value().weights[row];
}
Vector correction{nodes.size()};
const Status solved = tangent_solver.SolveCorrection(null_rhs, correction);
if (!solved.IsOk()) return Result<Vector>::Failure(solved);
const Status checked =
CheckLinearSolve(regularity.Value().matrix, null_rhs, correction);
if (!checked.IsOk()) return Result<Vector>::Failure(checked);
for (std::size_t i = 0; i < nodes.size(); ++i) {
for (std::size_t c = 0; c < 3; ++c) {
candidate.acceleration[6U * nodes[i] + 3U + c] +=
initial.rotations[nodes[i]].drill_null[c] * correction[i] /
regularity.Value().weights[i];
}
}
}
const Status hidden = HiddenEvidence(domain, initial, nodes, gradient.Value(),
&candidate, true);
if (!hidden.IsOk()) return Result<Vector>::Failure(hidden);
const Vector ma = initial.mass.Multiply(candidate.acceleration);
for (const std::size_t full : dofs.FreeDofs()) {
const double residual = ma[full] + initial.frame.inertia_force[full] +
initial.frame.internal_force[full] -
initial.frame.external_force[full];
double scale = std::abs(initial.frame.inertia_force[full]) +
std::abs(initial.frame.internal_force[full]) +
std::abs(initial.frame.external_force[full]);
for (std::size_t index = initial.mass.RowOffsets()[full];
index < initial.mass.RowOffsets()[full + 1U]; ++index) {
scale +=
std::abs(initial.mass.Values()[index] *
candidate.acceleration[initial.mass.ColumnIndices()[index]]);
}
if (!Within(residual, scale, 1.0e-10)) {
return Result<Vector>::Failure(Failure(
domain, "dynamic-initial-equilibrium",
"Initial physical acceleration failed the complete free equation."));
}
}
return Result<Vector>::Success(std::move(candidate.acceleration));
}
Status CompleteConstraintEvidence(
const Domain& domain, const DofManager& dofs,
const DynamicAssembly& assembly, const double length,
const std::vector<std::size_t>& initial_null_nodes,
LinearSolver& positive_solver, TangentSolver& tangent_solver,
DynamicAcceptedFrameState* frame) {
const Status valid = Validate(domain, dofs, assembly, length);
if (!valid.IsOk()) return valid;
const auto nodes = NullNodes(dofs, assembly.rotations);
if (nodes != initial_null_nodes) {
return Failure(domain, "dynamic-mass-null-rank-change",
"The admissible analytic drilling-null inventory changed.");
}
const auto physical = BuildPhysicalSpace(domain, dofs, assembly, length,
nodes, positive_solver);
if (!physical.HasValue()) return physical.GetStatus();
const auto gradient = ConstraintGradient(assembly, nodes);
if (!gradient.HasValue()) return gradient.GetStatus();
const auto regularity = CheckRegularity(domain, assembly, nodes,
gradient.Value(), tangent_solver);
if (!regularity.HasValue()) return regularity.GetStatus();
DynamicAcceptedFrameState candidate = *frame;
const Status hidden = HiddenEvidence(domain, assembly, nodes,
gradient.Value(), &candidate, true);
if (!hidden.IsOk()) return hidden;
*frame = std::move(candidate);
return Status::Ok();
}
} // namespace hht_internal
} // namespace fesa
+483
View File
@@ -0,0 +1,483 @@
#include <algorithm>
#include <cmath>
#include <limits>
#include <map>
#include <optional>
#include <string>
#include <utility>
#include "analysis/hht_dynamic_internal.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/elements/finite_rotation.h"
#include "fesa/loads/load.h"
#include "fesa/model/source_target_resolver.h"
namespace fesa::hht_internal {
namespace {
/// @brief Reports a dynamic assembly or equilibrium failure.
Status Failure(const std::string& message) {
return Status::Failure(FailureCategory::kSolver, {{Severity::kError,
"dynamic-invalid-state",
{},
"DYNAMIC",
{},
message}});
}
/// @brief Uses exact zero semantics for a dimensionally consistent ratio.
double Ratio(const double numerator, const double denominator) {
return denominator > 0.0 ? numerator / denominator
: numerator == 0.0 ? 0.0
: (std::numeric_limits<double>::infinity)();
}
/// @brief Gathers one source-node Cartesian component triple.
Vector3 Triple(const Vector& vector, const std::size_t first) {
return {vector[first], vector[first + 1U], vector[first + 2U]};
}
/// @brief Computes one matrix-vector spatial transform.
Vector3 Transform(const Matrix& matrix, const Vector3& vector) {
return {matrix(0U, 0U) * vector[0U] + matrix(0U, 1U) * vector[1U] +
matrix(0U, 2U) * vector[2U],
matrix(1U, 0U) * vector[0U] + matrix(1U, 1U) * vector[1U] +
matrix(1U, 2U) * vector[2U],
matrix(2U, 0U) * vector[0U] + matrix(2U, 1U) * vector[1U] +
matrix(2U, 2U) * vector[2U]};
}
/// @brief Gathers a local 24-vector in the DOF-owner scatter order.
Vector Gather(const Vector& full, const RuntimeShell& shell) {
Vector local(24U);
for (std::size_t i = 0U; i < 24U; ++i) local[i] = full[shell.scatter[i]];
return local;
}
} // namespace
Result<std::vector<RuntimeShell>> BuildRuntimeShells(const Domain& domain,
const DofManager& dofs) {
std::vector<RuntimeShell> shells;
for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) {
const auto* definition =
dynamic_cast<const Mitc4ShellDefinition*>(&domain.Elements()[index]);
if (definition == nullptr ||
definition->section_index >= domain.Properties().Size() ||
definition->material_index >= domain.Materials().Size()) {
return Result<std::vector<RuntimeShell>>::Failure(
Failure("HHT requires valid MITC4 shell definitions."));
}
const auto* section = dynamic_cast<const ShellSection*>(
&domain.Properties()[definition->section_index]);
const auto* material = dynamic_cast<const LinearElasticMaterial*>(
&domain.Materials()[definition->material_index]);
if (section == nullptr || material == nullptr) {
return Result<std::vector<RuntimeShell>>::Failure(
Failure("HHT requires homogeneous elastic shell properties."));
}
double density = 0.0;
for (const auto& record : domain.Densities()) {
if (record.material_index == definition->material_index)
density = record.rho;
}
std::array<const Node*, 4> nodes{};
std::array<ShellNodeInitialFrame, 4> frames{};
for (std::size_t local = 0U; local < 4U; ++local) {
const auto node = definition->node_indices[local];
if (node >= domain.Nodes().size() ||
node >= domain.ShellNodeInitialFrames().size()) {
return Result<std::vector<RuntimeShell>>::Failure(
Failure("A dynamic shell node has no initial frame."));
}
nodes[local] = &domain.Nodes()[node];
frames[local] = domain.ShellNodeInitialFrames()[node];
}
auto internal =
NonlinearMitc4Shell::Create(nodes, frames, *section, *material);
if (!internal.HasValue())
return Result<std::vector<RuntimeShell>>::Failure(internal.GetStatus());
auto inertia = NonlinearMitc4Inertia::Create(nodes, frames, *section,
*material, density);
if (!inertia.HasValue())
return Result<std::vector<RuntimeShell>>::Failure(inertia.GetStatus());
shells.push_back({static_cast<EntityIndex>(index),
dofs.ShellElementScatter(static_cast<EntityIndex>(index)),
std::move(internal.Value()), std::move(inertia.Value())});
}
return Result<std::vector<RuntimeShell>>::Success(std::move(shells));
}
Result<double> ReferenceLength(const Domain& domain) {
double length = 0.0;
for (std::size_t i = 0U; i < domain.ShellElements().Size(); ++i) {
const auto& element = domain.ShellElements()[i];
for (std::size_t node = 0U; node < 4U; ++node) {
const Vector3 a(domain.Nodes()[element.node_indices[node]].coordinates);
const Vector3 b(
domain.Nodes()[element.node_indices[(node + 1U) % 4U]].coordinates);
length = (std::max)(length, (a - b).Norm());
}
}
if (!std::isfinite(length) || length <= 0.0)
return Result<double>::Failure(
Failure("The dynamic reference length must be positive."));
return Result<double>::Success(length);
}
Result<std::array<Vector, 2>> AssembleLoads(const Domain& domain,
const DofManager& dofs) {
const auto index = SourceTargetIndex::FromDomain(domain);
const SourceTargetResolver resolver(index);
const LoadContext context{domain, dofs, resolver};
std::array<Vector, 2> result{Vector(dofs.FullDofCount()),
Vector(dofs.FullDofCount())};
for (const auto& load : domain.Steps()[0U].Loads()) {
const auto contributions = load.get().ComputeContributions(context);
if (!contributions.HasValue())
return Result<std::array<Vector, 2>>::Failure(contributions.GetStatus());
for (const auto& entry : contributions.Value()) {
result[0U][entry.full_dof_index] += entry.value;
result[1U][entry.full_dof_index] += std::abs(entry.value);
if (!std::isfinite(result[0U][entry.full_dof_index]) ||
!std::isfinite(result[1U][entry.full_dof_index]))
return Result<std::array<Vector, 2>>::Failure(
Failure("Dynamic load reduction overflowed."));
}
}
return Result<std::array<Vector, 2>>::Success(std::move(result));
}
Result<SparseMatrix> MakeSparse(const std::size_t rows,
const std::size_t columns,
std::vector<CooContribution> contributions) {
std::vector<std::vector<std::size_t>> row_columns(rows);
for (const auto& entry : contributions) {
if (entry.row >= rows || entry.column >= columns)
return Result<SparseMatrix>::Failure(
Failure("Projected sparse index is invalid."));
row_columns[entry.row].push_back(entry.column);
}
SparsePattern pattern;
pattern.row_offsets.push_back(0U);
for (auto& row : row_columns) {
std::sort(row.begin(), row.end());
row.erase(std::unique(row.begin(), row.end()), row.end());
pattern.column_indices.insert(pattern.column_indices.end(), row.begin(),
row.end());
pattern.row_offsets.push_back(pattern.column_indices.size());
}
return SparseMatrix::FromCoo(rows, columns, std::move(contributions),
pattern);
}
Result<SparseMatrix> ScaledFree(const SparseMatrix& matrix,
const DofManager& dofs, const double length) {
std::vector<CooContribution> entries;
for (std::size_t i = 0U; i < dofs.FreeDofCount(); ++i) {
const auto row = dofs.FreeDofs()[i];
for (auto offset = matrix.RowOffsets()[row];
offset < matrix.RowOffsets()[row + 1U]; ++offset) {
const auto column = matrix.ColumnIndices()[offset];
const auto free_column = dofs.FreeEquation(column);
if (free_column.has_value()) {
const double factor =
(row % 6U < 3U ? length : 1.0) * (column % 6U < 3U ? length : 1.0);
entries.push_back(
{i, *free_column, factor * matrix.Values()[offset], row, offset});
}
}
}
return MakeSparse(dofs.FreeDofCount(), dofs.FreeDofCount(),
std::move(entries));
}
Status CheckLinearSolve(const SparseMatrix& matrix, const Vector& rhs,
const Vector& solution) {
if (matrix.Rows() != rhs.Size() || matrix.Columns() != solution.Size())
return Failure("Dynamic linear solve dimensions differ.");
double numerator = 0.0;
double denominator = 0.0;
for (std::size_t row = 0U; row < matrix.Rows(); ++row) {
double residual = -rhs[row];
double scale = std::abs(rhs[row]);
for (auto offset = matrix.RowOffsets()[row];
offset < matrix.RowOffsets()[row + 1U]; ++offset) {
const double term =
matrix.Values()[offset] * solution[matrix.ColumnIndices()[offset]];
residual += term;
scale += std::abs(term);
}
if (!std::isfinite(residual) || !std::isfinite(scale))
return Failure("Dynamic linear solve returned nonfinite data.");
numerator = (std::max)(numerator, std::abs(residual));
denominator = (std::max)(denominator, scale);
}
if (Ratio(numerator, denominator) > 1e-12) {
return Status::Failure(FailureCategory::kSolver,
{{Severity::kError,
"dynamic-linear-solve",
{},
"DYNAMIC",
{},
"Dynamic linear backward error exceeds 1e-12."}});
}
return Status::Ok();
}
Result<DynamicAssembly> EvaluateAssembly(
const Domain& domain, const DofManager& dofs,
const std::vector<RuntimeShell>& shells, DynamicAcceptedFrameState frame,
const Vector& load_scale, const double time_increment,
const HhtParameters& parameters, const ParallelFor& parallel_for,
const bool directional_third) {
const std::size_t count = dofs.FullDofCount();
for (const auto* values : std::array<const Vector*, 6>{
&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
&frame.algorithmic_acceleration, &frame.external_force,
&load_scale}) {
if (values->Size() != count)
return Result<DynamicAssembly>::Failure(
Failure("Dynamic trial vector size is invalid."));
for (std::size_t i = 0U; i < count; ++i)
if (!std::isfinite((*values)[i]))
return Result<DynamicAssembly>::Failure(
Failure("Dynamic trial contains nonfinite values."));
}
frame.nodal_internal_order.clear();
frame.nodal_source_ids.clear();
frame.nodal_triad.clear();
frame.nodal_director.clear();
frame.angular_velocity = Vector(3U * domain.Nodes().size());
frame.angular_acceleration = Vector(3U * domain.Nodes().size());
std::vector<DynamicRotationKinematics> rotations;
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
const auto phi = Triple(frame.generalized_coordinates, 6U * node + 3U);
const auto& baseline = domain.ShellNodeInitialFrames()[node];
auto rotation = EvaluateDynamicRotation(
phi, Vector3(baseline.director), Triple(frame.velocity, 6U * node + 3U),
Triple(frame.acceleration, 6U * node + 3U));
if (!rotation.HasValue())
return Result<DynamicAssembly>::Failure(rotation.GetStatus());
const Matrix r = ExpSkew(phi);
frame.nodal_internal_order.push_back(static_cast<EntityIndex>(node));
frame.nodal_source_ids.push_back(domain.Nodes()[node].source_id);
frame.nodal_director.push_back(rotation.Value().director.Components());
frame.nodal_triad.push_back(
{Transform(r, Vector3(baseline.tangent_a)).Components(),
Transform(r, Vector3(baseline.tangent_b)).Components(),
rotation.Value().director.Components()});
for (std::size_t c = 0U; c < 3U; ++c) {
frame.angular_velocity[3U * node + c] =
rotation.Value().angular_velocity[c];
frame.angular_acceleration[3U * node + c] =
rotation.Value().angular_acceleration[c];
}
rotations.push_back(std::move(rotation.Value()));
}
std::vector<std::optional<NonlinearMitc4InternalEquilibrium>> internal(
shells.size());
std::vector<std::optional<NonlinearMitc4InertiaEvaluation>> inertia(
shells.size());
std::vector<std::optional<Status>> failures(shells.size());
parallel_for.Execute(shells.size(), [&](const std::size_t e) {
const auto q = Gather(frame.generalized_coordinates, shells[e]);
const auto v = Gather(frame.velocity, shells[e]);
const auto a = Gather(frame.acceleration, shells[e]);
NonlinearMitc4TrialDofs trial{};
for (std::size_t node = 0U; node < 4U; ++node) {
trial.displacement[node] = Triple(q, 6U * node);
trial.rotation_vector[node] = Triple(q, 6U * node + 3U);
}
auto f = EvaluateInternalResidualAndTangent(
shells[e].internal, trial, directional_third ? &v : nullptr);
if (!f.HasValue()) {
failures[e] = f.GetStatus();
return;
}
auto m = shells[e].inertia.Evaluate(trial, v, a);
if (!m.HasValue()) {
failures[e] = m.GetStatus();
return;
}
internal[e] = std::move(f.Value());
inertia[e] = std::move(m.Value());
});
frame.inertia_force = Vector(count);
frame.internal_force = Vector(count);
frame.residual = Vector(count);
frame.reaction = Vector(count);
frame.residual_scale = load_scale;
frame.shell_results.clear();
frame.kinetic_energy = frame.physical_strain_energy = frame.drilling_energy =
0.0;
frame.linear_momentum = {};
frame.angular_momentum = {};
frame.linear_momentum_rate = {};
frame.angular_momentum_rate = {};
std::vector<std::map<std::pair<std::size_t, std::size_t>, double>>
constraint_hessians(domain.Nodes().size());
Vector third(count);
Vector third_scale(count);
std::array<std::vector<CooContribution>, 4> entries;
for (auto& values : entries) values.reserve(shells.size() * 24U * 24U);
double momentum_scale = 0.0;
double angular_scale = 0.0;
const double a_coefficient =
1.0 / ((1.0 + parameters.alpha) * parameters.beta * time_increment *
time_increment);
const double v_coefficient =
parameters.gamma / (parameters.beta * time_increment);
for (std::size_t e = 0U; e < shells.size(); ++e) {
if (failures[e].has_value())
return Result<DynamicAssembly>::Failure(*failures[e]);
const auto& f = *internal[e];
const auto& m = *inertia[e];
if (directional_third) {
for (std::size_t local = 0U; local < 4U; ++local) {
auto& hessian = constraint_hessians[shells[e].scatter[6U * local] / 6U];
for (std::size_t i = 0U; i < 24U; ++i)
for (std::size_t j = 0U; j < 24U; ++j) {
hessian[{shells[e].scatter[i], shells[e].scatter[j]}] +=
f.drilling_constraint_hessian[local](i, j);
}
}
}
const auto a = Gather(frame.acceleration, shells[e]);
for (std::size_t row = 0U; row < 24U; ++row) {
const auto global = shells[e].scatter[row];
frame.internal_force[global] += f.internal_force[row];
frame.inertia_force[global] += m.force[row];
third[global] += f.force_directional_second[row];
third_scale[global] += std::abs(f.force_directional_second[row]);
frame.residual_scale[global] += std::abs(m.convective_force[row]) +
std::abs(f.physical_force[row]) +
std::abs(f.drilling_force[row]);
for (std::size_t column = 0U; column < 24U; ++column) {
frame.residual_scale[global] +=
std::abs(m.mass(row, column) * a[column]);
const std::array<double, 4> values{
m.mass(row, column), f.internal_tangent(row, column),
f.internal_tangent(row, column) + m.position_tangent(row, column) +
v_coefficient * m.velocity_tangent(row, column) +
a_coefficient * m.mass(row, column),
f.drilling_metric(row, column)};
for (std::size_t k = 0U; k < 4U; ++k)
entries[k].push_back({global, shells[e].scatter[column], values[k], e,
row * 24U + column});
}
}
for (const auto& point : f.recovery.gauss_points) {
frame.shell_results.push_back(
{shells[e].element_index,
point.location,
point.natural_coordinates,
{point.local_frame.e1, point.local_frame.e2, point.local_frame.e3},
point.generalized_strain,
point.section_resultant,
point.stress});
}
frame.kinetic_energy += m.kinetic_energy;
frame.physical_strain_energy += f.physical_strain_energy;
frame.drilling_energy += f.drilling_energy;
momentum_scale += m.momentum_rate_scale;
angular_scale += m.angular_momentum_rate_scale;
for (std::size_t c = 0U; c < 3U; ++c) {
frame.linear_momentum[c] += m.momentum[c];
frame.angular_momentum[c] += m.angular_momentum[c];
frame.linear_momentum_rate[c] += m.momentum_rate[c];
frame.angular_momentum_rate[c] += m.angular_momentum_rate[c];
}
}
for (std::size_t i = 0U; i < count; ++i) {
frame.residual[i] = frame.inertia_force[i] + frame.internal_force[i] -
frame.external_force[i];
if (!std::isfinite(frame.residual[i]) ||
!std::isfinite(frame.residual_scale[i]) || !std::isfinite(third[i]) ||
!std::isfinite(third_scale[i])) {
return Result<DynamicAssembly>::Failure(
Failure("Dynamic force reduction overflowed."));
}
}
for (const auto i : dofs.ConstrainedDofs())
frame.reaction[i] = frame.residual[i];
std::array<double, 2> residual_norm{};
std::array<double, 2> scale_norm{};
for (const auto i : dofs.FreeDofs()) {
const auto channel = i % 6U < 3U ? 0U : 1U;
residual_norm[channel] =
(std::max)(residual_norm[channel], std::abs(frame.residual[i]));
scale_norm[channel] =
(std::max)(scale_norm[channel], frame.residual_scale[i]);
}
for (std::size_t c = 0U; c < 2U; ++c)
frame.free_residual_metrics[c] = Ratio(residual_norm[c], scale_norm[c]);
auto mass = SparseMatrix::FromCoo(count, count, std::move(entries[0U]),
dofs.GetSparsePattern());
auto stiffness = SparseMatrix::FromCoo(count, count, std::move(entries[1U]),
dofs.GetSparsePattern());
auto effective = SparseMatrix::FromCoo(count, count, std::move(entries[2U]),
dofs.GetSparsePattern());
auto metric = SparseMatrix::FromCoo(count, count, std::move(entries[3U]),
dofs.GetSparsePattern());
for (const auto* result : {&mass, &stiffness, &effective, &metric})
if (!result->HasValue())
return Result<DynamicAssembly>::Failure(result->GetStatus());
Vector hidden_second(domain.Nodes().size());
Vector hidden_second_scale(domain.Nodes().size());
for (std::size_t node = 0U; node < constraint_hessians.size(); ++node) {
for (const auto& entry : constraint_hessians[node]) {
const double term = entry.second * frame.velocity[entry.first.first] *
frame.velocity[entry.first.second];
hidden_second[node] += term;
hidden_second_scale[node] += std::abs(term);
}
}
return Result<DynamicAssembly>::Success(
{std::move(frame), std::move(mass.Value()), std::move(stiffness.Value()),
std::move(effective.Value()), std::move(metric.Value()),
std::move(third), std::move(third_scale), std::move(rotations),
momentum_scale, angular_scale, std::move(hidden_second),
std::move(hidden_second_scale)});
}
Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs,
const DynamicAssembly& assembly,
DynamicAcceptedFrameState* frame) {
static_cast<void>(dofs);
Vector3 total_force;
Vector3 total_moment;
double force_scale = assembly.momentum_rate_scale;
double moment_scale = assembly.angular_momentum_rate_scale;
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
const auto first = 6U * node;
const Vector3 arm = Vector3(domain.Nodes()[node].coordinates) +
Triple(frame->generalized_coordinates, first);
const Matrix transform = InverseTransposeLeftJacobianSO3(
Triple(frame->generalized_coordinates, first + 3U));
for (const Vector* values : {&frame->external_force, &frame->reaction}) {
const Vector3 force = Triple(*values, first);
const Vector3 moment = Transform(transform, Triple(*values, first + 3U));
total_force = total_force + force;
total_moment = total_moment + arm.Cross(force) + moment;
force_scale += force.Norm();
moment_scale += arm.Cross(force).Norm() + moment.Norm();
}
}
const Vector3 force_error =
total_force - Vector3(frame->linear_momentum_rate);
const Vector3 moment_error =
total_moment - Vector3(frame->angular_momentum_rate);
for (std::size_t c = 0U; c < 3U; ++c) {
frame->global_equilibrium[c] = force_error[c];
frame->global_equilibrium[3U + c] = moment_error[c];
}
if (!force_error.IsFinite() || !moment_error.IsFinite() ||
!std::isfinite(force_scale) || !std::isfinite(moment_scale) ||
Ratio(force_error.Norm(), force_scale) > 1e-8 ||
Ratio(moment_error.Norm(), moment_scale) > 1e-8)
return Failure("Dynamic physical force or moment balance exceeds 1e-8.");
return Status::Ok();
}
} // namespace fesa::hht_internal
+102
View File
@@ -0,0 +1,102 @@
#ifndef FESA_ANALYSIS_HHT_DYNAMIC_INTERNAL_H_
#define FESA_ANALYSIS_HHT_DYNAMIC_INTERNAL_H_
#include <array>
#include <vector>
#include "fesa/analysis/dynamic_analysis_state.h"
#include "fesa/analysis/hht_time_integration.h"
#include "fesa/elements/nonlinear_mitc4_inertia.h"
#include "fesa/elements/nonlinear_mitc4_shell.h"
#include "fesa/math/sparse_matrix.h"
namespace fesa {
class LinearSolver;
class ParallelFor;
class TangentSolver;
namespace hht_internal {
/// @brief Keeps immutable local evaluators with the DOF-owner scatter.
struct RuntimeShell {
EntityIndex element_index;
std::array<std::size_t, 24> scatter;
NonlinearMitc4Shell internal;
NonlinearMitc4Inertia inertia;
};
/// @brief Stores a complete trial with independently assembled operators.
struct DynamicAssembly {
DynamicAcceptedFrameState frame;
SparseMatrix mass;
SparseMatrix internal_tangent;
SparseMatrix effective_tangent;
SparseMatrix drilling_metric;
Vector force_directional_second;
Vector force_directional_second_scale;
std::vector<DynamicRotationKinematics> rotations;
double momentum_rate_scale;
double angular_momentum_rate_scale;
Vector hidden_second{0U};
Vector hidden_second_scale{0U};
};
/// @brief Builds checked shell evaluators in stable source order.
Result<std::vector<RuntimeShell>> BuildRuntimeShells(const Domain& domain,
const DofManager& dofs);
/// @brief Returns the maximum initial active-element edge length.
Result<double> ReferenceLength(const Domain& domain);
/// @brief Assembles dead loads and their uncancelled absolute row scales.
Result<std::array<Vector, 2>> AssembleLoads(const Domain& domain,
const DofManager& dofs);
/// @brief Evaluates current kinetic and internal quantities without commit.
Result<DynamicAssembly> EvaluateAssembly(
const Domain& domain, const DofManager& dofs,
const std::vector<RuntimeShell>& shells, DynamicAcceptedFrameState frame,
const Vector& load_scale, double time_increment,
const HhtParameters& parameters, const ParallelFor& parallel_for,
bool directional_third);
/// @brief Builds deterministic CSR structure from an ordered local list.
Result<SparseMatrix> MakeSparse(std::size_t rows, std::size_t columns,
std::vector<CooContribution> contributions);
/// @brief Builds the scaled free-space operator D^T K D.
Result<SparseMatrix> ScaledFree(const SparseMatrix& matrix,
const DofManager& dofs, double length);
/// @brief Checks normwise backward error in the supplied equation space.
Status CheckLinearSolve(const SparseMatrix& matrix, const Vector& rhs,
const Vector& solution);
/// @brief Finds analytic admissible node null directions without rank cuts.
std::vector<std::size_t> NullNodes(
const DofManager& dofs,
const std::vector<DynamicRotationKinematics>& rotations);
/// @brief Solves the positive-mass and differentiated-null initial equations.
Result<Vector> InitializeAcceleration(const Domain& domain,
const DofManager& dofs,
const DynamicAssembly& initial,
double length,
LinearSolver& positive_solver,
TangentSolver& tangent_solver);
/// @brief Checks mass/regularity and recovers all exact hidden defects.
Status CompleteConstraintEvidence(
const Domain& domain, const DofManager& dofs,
const DynamicAssembly& assembly, double length,
const std::vector<std::size_t>& initial_null_nodes,
LinearSolver& positive_solver, TangentSolver& tangent_solver,
DynamicAcceptedFrameState* frame);
/// @brief Recovers physical support balance using volume momentum rates.
Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs,
const DynamicAssembly& assembly,
DynamicAcceptedFrameState* frame);
} // namespace hht_internal
} // namespace fesa
#endif // FESA_ANALYSIS_HHT_DYNAMIC_INTERNAL_H_
+322
View File
@@ -0,0 +1,322 @@
#include "fesa/analysis/hht_dynamic_procedure.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <string>
#include <utility>
#include "analysis/hht_dynamic_internal.h"
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/newton_iteration_log.h"
#include "fesa/solvers/linear/tangent_solver.h"
namespace fesa {
namespace {
using namespace hht_internal;
/// @brief Creates one procedure-owned diagnostic with the source step.
Status Failure(const Domain& domain, const std::string& code,
const std::string& message) {
return Status::Failure(
FailureCategory::kSolver,
{{Severity::kError, code, domain.Dynamic()->dynamic_location, "DYNAMIC",
domain.Dynamic()->source_step_name_raw, message}});
}
/// @brief Checks one update against its uncancelled, dimensionally equal terms.
bool UpdateEqual(const double left, const std::initializer_list<double> terms) {
double right = 0.0;
double scale = 0.0;
for (const double term : terms) {
right += term;
scale += std::abs(term);
}
const double defect = std::abs(left - right);
return std::isfinite(defect) && std::isfinite(scale) &&
(scale == 0.0 ? defect == 0.0 : defect / scale <= 1e-12);
}
/// @brief Verifies both absolute and increment forms before accepting history.
Status CheckUpdates(const Domain& domain, const DynamicAcceptedFrameState& old,
const DynamicAcceptedFrameState& frame,
const HhtPredictor& predictor, const HhtParameters& p,
const double h) {
for (std::size_t i = 0U; i < frame.generalized_coordinates.Size(); ++i) {
const double increment =
frame.generalized_coordinates[i] - old.generalized_coordinates[i];
const double old_term =
h * h * (0.5 - p.beta) * old.algorithmic_acceleration[i];
const double new_term = h * h * p.beta * frame.algorithmic_acceleration[i];
if (!UpdateEqual(frame.generalized_coordinates[i],
{old.generalized_coordinates[i], h * old.velocity[i],
old_term, new_term}) ||
!UpdateEqual(increment, {h * old.velocity[i], old_term, new_term}) ||
!UpdateEqual(
frame.generalized_coordinates[i] - predictor.coordinates[i],
{new_term}) ||
!UpdateEqual(frame.velocity[i],
{old.velocity[i],
h * (1 - p.gamma) * old.algorithmic_acceleration[i],
h * p.gamma * frame.algorithmic_acceleration[i]}) ||
!UpdateEqual(frame.algorithmic_acceleration[i],
{(1 + p.alpha) * frame.acceleration[i],
-p.alpha * old.acceleration[i]})) {
return Failure(
domain, "dynamic-update-inconsistency",
"HHT endpoint update or resolved increment exceeds 1e-12.");
}
}
return Status::Ok();
}
/// @brief Fills the raw channel evidence without hiding a zero denominator.
void ResidualRecord(const DofManager& dofs,
const DynamicAcceptedFrameState& frame,
DynamicNewtonIterationLogRecord* record) {
record->residual_numerator = {};
record->residual_scale = {};
for (const auto full : dofs.FreeDofs()) {
const auto c = full % 6U < 3U ? 0U : 1U;
record->residual_numerator[c] = (std::max)(record->residual_numerator[c],
std::abs(frame.residual[full]));
record->residual_scale[c] =
(std::max)(record->residual_scale[c], frame.residual_scale[full]);
}
record->residual_pass = frame.free_residual_metrics[0U] <= 1e-10 &&
frame.free_residual_metrics[1U] <= 1e-10;
}
/// @brief Forms the four coordinate/rate correction checks in scaled units.
void CorrectionRecord(const Vector& correction, const DofManager& dofs,
const double length, const HhtParameters& p,
DynamicNewtonIterationLogRecord* record) {
record->coordinate_correction = {};
for (const auto full : dofs.FreeDofs()) {
const auto c = full % 6U < 3U ? 0U : 1U;
record->coordinate_correction[c] =
(std::max)(record->coordinate_correction[c],
std::abs(correction[full]) / (c == 0U ? length : 1.0));
}
record->correction_pass = true;
for (std::size_t c = 0; c < 2U; ++c) {
const double dq = record->coordinate_correction[c];
record->velocity_correction[c] = p.gamma / p.beta * dq;
record->acceleration_correction[c] = dq / ((1 + p.alpha) * p.beta);
record->algorithmic_correction[c] = dq / p.beta;
for (const auto* channel :
{&record->coordinate_correction, &record->velocity_correction,
&record->acceleration_correction, &record->algorithmic_correction})
record->correction_pass =
record->correction_pass && (*channel)[c] <= 1e-10;
}
}
} // namespace
HhtDynamicProcedureExecutor::HhtDynamicProcedureExecutor(
const ParallelFor& parallel_for, LinearSolver& positive_solver,
TangentSolver& tangent_solver)
: parallel_for_(parallel_for),
positive_solver_(positive_solver),
tangent_solver_(tangent_solver) {}
Result<DynamicExecutionResult> HhtDynamicProcedureExecutor::Execute(
const Domain& domain, NewtonIterationLogWriter& log) const {
if (domain.Dynamic() == nullptr) {
return Result<DynamicExecutionResult>::Failure(Status::Failure(
FailureCategory::kModel, {{Severity::kError,
"invalid-dynamic-step",
{},
"DYNAMIC",
{},
"HHT requires exactly one dynamic step."}}));
}
auto model = AnalysisModel::Create(domain);
if (!model.HasValue())
return Result<DynamicExecutionResult>::Failure(model.GetStatus());
auto dofs_result = DofManager::Create(model.Value());
if (!dofs_result.HasValue())
return Result<DynamicExecutionResult>::Failure(dofs_result.GetStatus());
const auto& dofs = dofs_result.Value();
auto state_result = DynamicAnalysisState::Create(domain, dofs);
if (!state_result.HasValue())
return Result<DynamicExecutionResult>::Failure(state_result.GetStatus());
auto state = std::move(state_result.Value());
auto shells = BuildRuntimeShells(domain, dofs);
if (!shells.HasValue())
return Result<DynamicExecutionResult>::Failure(shells.GetStatus());
auto length_result = ReferenceLength(domain);
if (!length_result.HasValue())
return Result<DynamicExecutionResult>::Failure(length_result.GetStatus());
const double length = length_result.Value();
auto loads = AssembleLoads(domain, dofs);
if (!loads.HasValue())
return Result<DynamicExecutionResult>::Failure(loads.GetStatus());
auto parameters = MakeHhtParameters(domain.Dynamic()->alpha);
if (!parameters.HasValue())
return Result<DynamicExecutionResult>::Failure(parameters.GetStatus());
const auto p = parameters.Value();
const double h = domain.Dynamic()->initial_increment;
DynamicAcceptedFrameState frame{};
frame.is_initial = true;
for (auto* vector : {&frame.generalized_coordinates, &frame.velocity,
&frame.acceleration, &frame.algorithmic_acceleration})
*vector = Vector(dofs.FullDofCount());
frame.external_force = loads.Value()[0U];
auto initial = EvaluateAssembly(domain, dofs, shells.Value(), frame,
loads.Value()[1U], h, p, parallel_for_, true);
if (!initial.HasValue())
return Result<DynamicExecutionResult>::Failure(initial.GetStatus());
const auto null_nodes = NullNodes(dofs, initial.Value().rotations);
auto acceleration = InitializeAcceleration(
domain, dofs, initial.Value(), length, positive_solver_, tangent_solver_);
if (!acceleration.HasValue())
return Result<DynamicExecutionResult>::Failure(acceleration.GetStatus());
frame.acceleration = acceleration.Value();
frame.algorithmic_acceleration = acceleration.Value();
initial = EvaluateAssembly(domain, dofs, shells.Value(), frame,
loads.Value()[1U], h, p, parallel_for_, true);
if (!initial.HasValue())
return Result<DynamicExecutionResult>::Failure(initial.GetStatus());
frame = initial.Value().frame;
Status status = CompleteConstraintEvidence(
domain, dofs, initial.Value(), length, null_nodes, positive_solver_,
tangent_solver_, &frame);
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
status = CompleteEquilibrium(domain, dofs, initial.Value(), &frame);
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
const double initial_energy = frame.kinetic_energy +
frame.physical_strain_energy +
frame.drilling_energy;
status = state.CommitAcceptedFrame(frame);
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
DynamicNewtonIterationLogRecord record{};
record.stage = "accepted";
record.outcome = "initial";
record.correction_pass = true;
ResidualRecord(dofs, frame, &record);
status = log.Write(record);
if (status.IsOk()) status = log.Flush();
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
for (std::uint32_t increment = 1U;
increment <= domain.Dynamic()->increment_count; ++increment) {
const auto& old = state.CommittedState();
auto predictor = PredictHht(old.generalized_coordinates, old.velocity,
old.algorithmic_acceleration, h, p);
if (!predictor.HasValue())
return Result<DynamicExecutionResult>::Failure(predictor.GetStatus());
frame = old;
frame.is_initial = false;
frame.frame_index = increment;
frame.increment_index = increment;
frame.time = static_cast<double>(increment) * h;
// A large DIRECT step may put a kinematic predictor outside the shell
// chart before Newton starts. The last accepted geometry is a valid
// initial iterate; q_p still defines the unchanged HHT rate equations.
frame.generalized_coordinates = old.generalized_coordinates;
record = {};
record.increment_index = increment;
record.time = frame.time;
record.correction_pass = true;
bool accepted = false;
for (std::uint32_t iteration = 0U; iteration <= 100U; ++iteration) {
record.iteration_index = iteration;
record.stage = "evaluation";
record.outcome = "candidate";
auto fail = [&](const Status& failure) -> Result<DynamicExecutionResult> {
record.stage = "failed";
record.outcome = "failed";
record.failure_code = failure.Diagnostics().empty()
? "dynamic-solver-failure"
: failure.Diagnostics().front().code;
const Status written = log.Write(record);
return Result<DynamicExecutionResult>::Failure(
written.IsOk() ? failure : written);
};
auto rates = EvaluateHhtRates(frame.generalized_coordinates,
predictor.Value(), old.acceleration, h, p);
if (!rates.HasValue()) return fail(rates.GetStatus());
frame.velocity = std::move(rates.Value().velocity);
frame.acceleration = std::move(rates.Value().acceleration);
frame.algorithmic_acceleration =
std::move(rates.Value().algorithmic_acceleration);
frame.iteration_count = iteration;
auto assembly =
EvaluateAssembly(domain, dofs, shells.Value(), frame,
loads.Value()[1U], h, p, parallel_for_, false);
if (!assembly.HasValue()) return fail(assembly.GetStatus());
frame = assembly.Value().frame;
ResidualRecord(dofs, frame, &record);
if (record.residual_pass && record.correction_pass) {
assembly =
EvaluateAssembly(domain, dofs, shells.Value(), frame,
loads.Value()[1U], h, p, parallel_for_, true);
if (!assembly.HasValue()) return fail(assembly.GetStatus());
frame = assembly.Value().frame;
status = CompleteConstraintEvidence(
domain, dofs, assembly.Value(), length, null_nodes,
positive_solver_, tangent_solver_, &frame);
if (!status.IsOk()) return fail(status);
status = CompleteEquilibrium(domain, dofs, assembly.Value(), &frame);
if (!status.IsOk()) return fail(status);
status = CheckUpdates(domain, old, frame, predictor.Value(), p, h);
if (!status.IsOk()) return fail(status);
frame.external_work = 0.0;
for (std::size_t i = 0; i < dofs.FullDofCount(); ++i)
frame.external_work +=
frame.external_force[i] * frame.generalized_coordinates[i];
frame.energy_defect =
frame.kinetic_energy + frame.physical_strain_energy +
frame.drilling_energy - initial_energy - frame.external_work;
status = state.CommitAcceptedFrame(frame);
if (!status.IsOk()) return fail(status);
record.stage = "accepted";
record.outcome = "accepted";
status = log.Write(record);
if (status.IsOk()) status = log.Flush();
if (!status.IsOk())
return Result<DynamicExecutionResult>::Failure(status);
accepted = true;
break;
}
if (iteration == 100U)
return fail(Failure(domain, "dynamic-nonconvergence",
"HHT exhausted 100 corrections without satisfying "
"every endpoint check."));
status = log.Write(record);
if (!status.IsOk())
return Result<DynamicExecutionResult>::Failure(status);
auto tangent =
ScaledFree(assembly.Value().effective_tangent, dofs, length);
if (!tangent.HasValue()) return fail(tangent.GetStatus());
Vector rhs(dofs.FreeDofCount());
Vector solution(dofs.FreeDofCount());
Vector correction(dofs.FullDofCount());
for (std::size_t i = 0; i < dofs.FreeDofCount(); ++i) {
const auto full = dofs.FreeDofs()[i];
rhs[i] = -frame.residual[full] * (full % 6U < 3U ? length : 1.0);
}
if (dofs.FreeDofCount() > 0U) {
status = tangent_solver_.FactorizeFresh(tangent.Value());
if (!status.IsOk()) return fail(status);
status = tangent_solver_.SolveCorrection(rhs, solution);
if (!status.IsOk()) return fail(status);
status = CheckLinearSolve(tangent.Value(), rhs, solution);
if (!status.IsOk()) return fail(status);
}
for (std::size_t i = 0; i < dofs.FreeDofCount(); ++i) {
const auto full = dofs.FreeDofs()[i];
correction[full] = solution[i] * (full % 6U < 3U ? length : 1.0);
frame.generalized_coordinates[full] += correction[full];
}
CorrectionRecord(correction, dofs, length, p, &record);
}
if (!accepted)
return Result<DynamicExecutionResult>::Failure(
Failure(domain, "dynamic-nonconvergence",
"Dynamic increment was not accepted."));
}
return Result<DynamicExecutionResult>::Success(
{std::move(state), domain.Warnings()});
}
} // namespace fesa
@@ -0,0 +1,97 @@
#include "fesa/analysis/hht_time_integration.h"
#include <cmath>
#include <utility>
namespace fesa {
namespace {
/// @brief Reports invalid or unrepresentable HHT kinematics.
Status Failure() {
return Status::Failure(
FailureCategory::kSolver,
{{Severity::kError, "dynamic-invalid-state", {}, "DYNAMIC", {},
"HHT requires finite compatible rates, positive time, and coupled "
"coefficients in the supported range."}});
}
/// @brief Validates the coupled coefficient and time denominators.
bool Valid(const double h, const HhtParameters& p) {
return std::isfinite(h) && h > 0.0 && std::isfinite(h * h) &&
h * h > 0.0 && std::isfinite(p.alpha) &&
p.alpha >= -1.0 / 3.0 && p.alpha <= 0.0 &&
p.beta == 0.25 * (1.0 - p.alpha) * (1.0 - p.alpha) &&
p.gamma == 0.5 - p.alpha && p.beta * h * h > 0.0;
}
} // namespace
Result<HhtParameters> MakeHhtParameters(const double alpha) {
if (!std::isfinite(alpha) || alpha < -1.0 / 3.0 || alpha > 0.0) {
return Result<HhtParameters>::Failure(Failure());
}
return Result<HhtParameters>::Success(
{alpha, 0.25 * (1.0 - alpha) * (1.0 - alpha), 0.5 - alpha});
}
Result<HhtPredictor> PredictHht(const Vector& coordinates,
const Vector& velocity,
const Vector& algorithmic_acceleration,
const double time_increment,
const HhtParameters& parameters) {
if (!Valid(time_increment, parameters) ||
coordinates.Size() != velocity.Size() ||
coordinates.Size() != algorithmic_acceleration.Size()) {
return Result<HhtPredictor>::Failure(Failure());
}
HhtPredictor predictor{Vector(coordinates.Size()), Vector(coordinates.Size())};
for (std::size_t i = 0U; i < coordinates.Size(); ++i) {
predictor.coordinates[i] =
coordinates[i] + time_increment * velocity[i] +
time_increment * time_increment * (0.5 - parameters.beta) *
algorithmic_acceleration[i];
predictor.velocity[i] =
velocity[i] + time_increment * (1.0 - parameters.gamma) *
algorithmic_acceleration[i];
if (!std::isfinite(predictor.coordinates[i]) ||
!std::isfinite(predictor.velocity[i])) {
return Result<HhtPredictor>::Failure(Failure());
}
}
return Result<HhtPredictor>::Success(std::move(predictor));
}
Result<HhtRates> EvaluateHhtRates(const Vector& coordinates,
const HhtPredictor& predictor,
const Vector& old_acceleration,
const double time_increment,
const HhtParameters& parameters) {
if (!Valid(time_increment, parameters) ||
coordinates.Size() != predictor.coordinates.Size() ||
coordinates.Size() != predictor.velocity.Size() ||
coordinates.Size() != old_acceleration.Size()) {
return Result<HhtRates>::Failure(Failure());
}
HhtRates rates{Vector(coordinates.Size()), Vector(coordinates.Size()),
Vector(coordinates.Size())};
for (std::size_t i = 0U; i < coordinates.Size(); ++i) {
const double increment = coordinates[i] - predictor.coordinates[i];
rates.algorithmic_acceleration[i] =
increment / (parameters.beta * time_increment * time_increment);
rates.velocity[i] = predictor.velocity[i] +
parameters.gamma * increment /
(parameters.beta * time_increment);
rates.acceleration[i] =
(rates.algorithmic_acceleration[i] +
parameters.alpha * old_acceleration[i]) /
(1.0 + parameters.alpha);
if (!std::isfinite(rates.algorithmic_acceleration[i]) ||
!std::isfinite(rates.acceleration[i]) ||
!std::isfinite(rates.velocity[i])) {
return Result<HhtRates>::Failure(Failure());
}
}
return Result<HhtRates>::Success(std::move(rates));
}
} // namespace fesa
@@ -258,6 +258,68 @@ Result<std::string> SerializeRecord(const NewtonIterationLogRecord& record,
return Result<std::string>::Success(std::move(line));
}
/// @brief Serializes dynamic evidence without static load-factor semantics.
Result<std::string> SerializeRecord(
const DynamicNewtonIterationLogRecord& record,
const std::filesystem::path& log_path) {
const auto time = FormatFiniteNumber(record.time, log_path);
if (!time.HasValue()) return Result<std::string>::Failure(time.GetStatus());
if (record.time < 0.0 || record.stage.empty() || record.outcome.empty()) {
return Result<std::string>::Failure(LogOutputFailure(
log_path,
"A dynamic Newton log requires nonnegative time, stage, and outcome."));
}
std::vector<std::string> fields;
fields.reserve(24U);
AppendField(&fields, "record", "dynamic-newton-iteration");
AppendField(&fields, "schema", "hht-log-v1");
AppendField(&fields, "increment", std::to_string(record.increment_index));
AppendField(&fields, "time", time.Value());
AppendField(&fields, "iteration", std::to_string(record.iteration_index));
AppendField(&fields, "stage", EscapeText(record.stage));
const std::array<const char*, 2U> channels{"u", "phi"};
for (std::size_t channel = 0U; channel < channels.size(); ++channel) {
const std::string suffix{channels[channel]};
const std::array<std::pair<std::string, double>, 6U> metrics{{
{"residual_" + suffix + "_numerator",
record.residual_numerator[channel]},
{"residual_" + suffix + "_scale", record.residual_scale[channel]},
{"coordinate_correction_" + suffix,
record.coordinate_correction[channel]},
{"velocity_correction_" + suffix, record.velocity_correction[channel]},
{"acceleration_correction_" + suffix,
record.acceleration_correction[channel]},
{"algorithmic_correction_" + suffix,
record.algorithmic_correction[channel]},
}};
for (const auto& metric : metrics) {
const auto formatted = FormatFiniteNumber(metric.second, log_path);
if (!formatted.HasValue()) {
return Result<std::string>::Failure(formatted.GetStatus());
}
if (metric.second < 0.0) {
return Result<std::string>::Failure(LogOutputFailure(
log_path, "A dynamic Newton log norm or scale is negative."));
}
AppendField(&fields, metric.first, formatted.Value());
}
AppendField(
&fields, "residual_" + suffix + "_branch",
record.residual_scale[channel] == 0.0 ? "zero-scale" : "relative");
}
AppendField(&fields, "residual_pass", record.residual_pass ? "1" : "0");
AppendField(&fields, "correction_pass", record.correction_pass ? "1" : "0");
AppendField(&fields, "outcome", EscapeText(record.outcome));
AppendField(&fields, "failure_code", EscapeText(record.failure_code));
std::string line;
for (std::size_t index = 0U; index < fields.size(); ++index) {
if (index != 0U) line += '\t';
line += fields[index];
}
line += '\n';
return Result<std::string>::Success(std::move(line));
}
} // namespace
Status NewtonIterationLogWriter::Open(const std::filesystem::path& log_path) {
@@ -296,6 +358,27 @@ Status NewtonIterationLogWriter::Write(const NewtonIterationLogRecord& record) {
return Status::Ok();
}
Status NewtonIterationLogWriter::Write(
const DynamicNewtonIterationLogRecord& record) {
if (state_ != State::kOpen) {
state_ = State::kFailed;
return LogOutputFailure(log_path_, "The Newton log writer is not open.");
}
const auto line = SerializeRecord(record, log_path_);
if (!line.HasValue()) {
state_ = State::kFailed;
return line.GetStatus();
}
stream_.write(line.Value().data(),
static_cast<std::streamsize>(line.Value().size()));
if (!stream_) {
state_ = State::kFailed;
return LogOutputFailure(log_path_,
"The dynamic Newton log record could not write.");
}
return Status::Ok();
}
Status NewtonIterationLogWriter::Flush() {
if (state_ != State::kOpen) {
state_ = State::kFailed;
+35 -3
View File
@@ -2,6 +2,7 @@
#include <utility>
#include "fesa/analysis/hht_dynamic_procedure.h"
#include "fesa/analysis/linear_frequency_procedure.h"
#include "fesa/analysis/linear_static_procedure.h"
#include "fesa/analysis/newton_iteration_log.h"
@@ -9,6 +10,7 @@
#include "fesa/app/output_paths.h"
#include "fesa/io/abaqus/domain_mapper.h"
#include "fesa/io/abaqus/input_reader.h"
#include "fesa/results/dynamic_results_writer.h"
#include "fesa/results/eigen_results_writer.h"
#include "fesa/results/nonlinear_results_writer.h"
#include "fesa/results/results_writer.h"
@@ -37,7 +39,8 @@ Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
NonlinearStaticProcedureExecutor& nonlinear_executor,
const LinearFrequencyProcedureExecutor& frequency_executor,
NewtonIterationLogWriter& log) {
NewtonIterationLogWriter& log,
const HhtDynamicProcedureExecutor* dynamic_executor) {
if (domain.Steps().Size() != 1U) {
return Result<PreparedAnalysisOutcome>::Failure(Status::Failure(
FailureCategory::kInput,
@@ -49,6 +52,22 @@ Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
"Prepared analysis requires exactly one analysis step."}}));
}
if (domain.Dynamic() != nullptr) {
if (dynamic_executor == nullptr)
return Result<PreparedAnalysisOutcome>::Failure(
Status::Failure(FailureCategory::kSolver,
{{Severity::kError,
"missing-dynamic-executor",
{},
"DYNAMIC",
{},
"Dynamic execution adapter is required."}}));
auto dynamic = dynamic_executor->Execute(domain, log);
if (!dynamic.HasValue())
return Result<PreparedAnalysisOutcome>::Failure(dynamic.GetStatus());
return Result<PreparedAnalysisOutcome>::Success(
PreparedAnalysisOutcome{std::move(dynamic.Value())});
}
if (domain.Steps()[0U].IsFrequency()) {
auto eigen = frequency_executor.Execute(domain);
if (!eigen.HasValue()) {
@@ -80,8 +99,21 @@ Status FinalizePreparedAnalysisResult(
const DerivedOutputPaths& paths, const Domain& domain,
const PreparedAnalysisOutcome& outcome,
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
NonlinearResultsWriter& nonlinear_writer,
EigenResultsWriter& eigen_writer) {
NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer,
DynamicResultsWriter* dynamic_writer) {
if (std::holds_alternative<DynamicExecutionResult>(outcome)) {
if (dynamic_writer == nullptr)
return Status::Failure(FailureCategory::kOutput,
{{Severity::kError,
"missing-dynamic-writer",
{},
"DYNAMIC",
{},
"Dynamic result writer is required."}});
return dynamic_writer->Write(
paths.results_path, domain,
std::get<DynamicExecutionResult>(outcome).state, diagnostics);
}
if (std::holds_alternative<EigenAnalysisState>(outcome)) {
return eigen_writer.Write(paths.results_path, domain,
std::get<EigenAnalysisState>(outcome),
+9 -4
View File
@@ -5,6 +5,7 @@
#include <string>
#include <vector>
#include "fesa/analysis/hht_dynamic_procedure.h"
#include "fesa/analysis/linear_frequency_procedure.h"
#include "fesa/analysis/linear_static_procedure.h"
#include "fesa/analysis/newton_iteration_log.h"
@@ -13,6 +14,7 @@
#include "fesa/app/output_paths.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/core/diagnostic.h"
#include "fesa/io/hdf5/dynamic_hdf5_results_writer.h"
#include "fesa/io/hdf5/eigen_hdf5_results_writer.h"
#include "fesa/io/hdf5/hdf5_results_writer.h"
#include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h"
@@ -84,6 +86,7 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
Hdf5ResultsWriter linear_writer;
NonlinearHdf5ResultsWriter nonlinear_writer;
EigenHdf5ResultsWriter eigen_writer;
DynamicHdf5ResultsWriter dynamic_writer;
NewtonIterationLogWriter log;
Status status = log.Open(paths.Value().log_path);
@@ -101,9 +104,11 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
NonlinearStaticProcedureExecutor nonlinear_executor(parallel_for,
tangent_solver);
const LinearFrequencyProcedureExecutor frequency_executor;
Result<PreparedAnalysisOutcome> outcome =
ExecutePreparedAnalysis(prepared.Value().domain, linear_executor,
nonlinear_executor, frequency_executor, log);
const HhtDynamicProcedureExecutor dynamic_executor(
parallel_for, linear_solver, tangent_solver);
Result<PreparedAnalysisOutcome> outcome = ExecutePreparedAnalysis(
prepared.Value().domain, linear_executor, nonlinear_executor,
frequency_executor, log, &dynamic_executor);
if (!outcome.HasValue()) {
status = outcome.GetStatus();
} else {
@@ -115,7 +120,7 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
status = FinalizePreparedAnalysisResult(
paths.Value(), prepared.Value().domain, outcome.Value(),
prepared.Value().diagnostics, linear_writer, nonlinear_writer,
eigen_writer);
eigen_writer, &dynamic_writer);
if (status.IsOk()) {
return kSuccessExitCode;
}
@@ -0,0 +1,871 @@
#include "fesa/elements/nonlinear_mitc4_inertia.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <limits>
#include <string>
#include <utility>
#include "fesa/elements/finite_rotation.h"
namespace fesa {
namespace {
constexpr std::size_t kNodes = 4U;
constexpr std::size_t kDofs = 24U;
const std::array<double, 3> kGauss{-std::sqrt(3.0 / 5.0), 0.0,
std::sqrt(3.0 / 5.0)};
constexpr std::array<double, 3> kWeights{5.0 / 9.0, 8.0 / 9.0, 5.0 / 9.0};
template <class T>
Result<T> Failure(const std::string& code, const SourceLocation& location,
const std::string& identity, std::string message) {
return Result<T>::Failure(Status::Failure(
FailureCategory::kModel, {{Severity::kError, code, location, "*ELEMENT",
identity, std::move(message)}}));
}
std::string Identity(const std::array<const Node*, kNodes>& nodes) {
std::string identity;
for (const Node* node : nodes) {
if (!identity.empty()) {
identity += "-";
}
identity += node == nullptr ? "?" : node->source_id.source_label_text;
}
return identity;
}
Matrix FrameMatrix(const ShellNodeInitialFrame& frame) {
Matrix result{3U, 3U};
for (std::size_t row = 0U; row < 3U; ++row) {
result(row, 0U) = frame.tangent_a[row];
result(row, 1U) = frame.tangent_b[row];
result(row, 2U) = frame.director[row];
}
return result;
}
bool IsFinite(const Matrix& matrix) {
for (std::size_t row = 0U; row < matrix.Rows(); ++row) {
for (std::size_t column = 0U; column < matrix.Columns(); ++column) {
if (!std::isfinite(matrix(row, column))) {
return false;
}
}
}
return true;
}
bool IsFinite(const Vector& vector) {
for (std::size_t index = 0U; index < vector.Size(); ++index) {
if (!std::isfinite(vector[index])) {
return false;
}
}
return true;
}
/// @brief Carries all derivatives through order three in one nodal rotation.
struct Jet {
double value{};
std::array<double, 3> d{};
std::array<std::array<double, 3>, 3> dd{};
std::array<std::array<std::array<double, 3>, 3>, 3> ddd{};
};
Jet Constant(const double value) { return Jet{value, {}, {}, {}}; }
Jet Variable(const double value, const std::size_t component) {
Jet result = Constant(value);
result.d[component] = 1.0;
return result;
}
Jet operator+(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value + right.value;
for (std::size_t i = 0U; i < 3U; ++i) {
result.d[i] = left.d[i] + right.d[i];
for (std::size_t j = 0U; j < 3U; ++j) {
result.dd[i][j] = left.dd[i][j] + right.dd[i][j];
for (std::size_t k = 0U; k < 3U; ++k) {
result.ddd[i][j][k] = left.ddd[i][j][k] + right.ddd[i][j][k];
}
}
}
return result;
}
Jet operator-(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value - right.value;
for (std::size_t i = 0U; i < 3U; ++i) {
result.d[i] = left.d[i] - right.d[i];
for (std::size_t j = 0U; j < 3U; ++j) {
result.dd[i][j] = left.dd[i][j] - right.dd[i][j];
for (std::size_t k = 0U; k < 3U; ++k) {
result.ddd[i][j][k] = left.ddd[i][j][k] - right.ddd[i][j][k];
}
}
}
return result;
}
Jet operator*(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value * right.value;
for (std::size_t i = 0U; i < 3U; ++i) {
result.d[i] = left.d[i] * right.value + left.value * right.d[i];
for (std::size_t j = 0U; j < 3U; ++j) {
result.dd[i][j] = left.dd[i][j] * right.value + left.d[i] * right.d[j] +
left.d[j] * right.d[i] + left.value * right.dd[i][j];
for (std::size_t k = 0U; k < 3U; ++k) {
result.ddd[i][j][k] =
left.ddd[i][j][k] * right.value + left.dd[i][j] * right.d[k] +
left.dd[i][k] * right.d[j] + left.dd[j][k] * right.d[i] +
left.d[i] * right.dd[j][k] + left.d[j] * right.dd[i][k] +
left.d[k] * right.dd[i][j] + left.value * right.ddd[i][j][k];
}
}
}
return result;
}
Jet Reciprocal(const Jet& value) {
Jet result{};
result.value = 1.0 / value.value;
const double squared = result.value * result.value;
const double cubed = squared * result.value;
const double fourth = cubed * result.value;
for (std::size_t i = 0U; i < 3U; ++i) {
result.d[i] = -value.d[i] * squared;
for (std::size_t j = 0U; j < 3U; ++j) {
result.dd[i][j] =
2.0 * value.d[i] * value.d[j] * cubed - value.dd[i][j] * squared;
for (std::size_t k = 0U; k < 3U; ++k) {
result.ddd[i][j][k] =
-6.0 * value.d[i] * value.d[j] * value.d[k] * fourth +
2.0 *
(value.dd[i][j] * value.d[k] + value.dd[i][k] * value.d[j] +
value.dd[j][k] * value.d[i]) *
cubed -
value.ddd[i][j][k] * squared;
}
}
}
return result;
}
Jet InverseSqrt(const Jet& value) {
Jet result{};
result.value = 1.0 / std::sqrt(value.value);
const double cubed = result.value * result.value * result.value;
const double fifth = cubed * result.value * result.value;
const double seventh = fifth * result.value * result.value;
for (std::size_t i = 0U; i < 3U; ++i) {
result.d[i] = -0.5 * cubed * value.d[i];
for (std::size_t j = 0U; j < 3U; ++j) {
result.dd[i][j] =
0.75 * fifth * value.d[i] * value.d[j] - 0.5 * cubed * value.dd[i][j];
for (std::size_t k = 0U; k < 3U; ++k) {
result.ddd[i][j][k] =
-1.875 * seventh * value.d[i] * value.d[j] * value.d[k] +
0.75 * fifth *
(value.dd[i][j] * value.d[k] + value.dd[i][k] * value.d[j] +
value.dd[j][k] * value.d[i]) -
0.5 * cubed * value.ddd[i][j][k];
}
}
}
return result;
}
Jet Scale(const double scalar, const Jet& value) {
return Constant(scalar) * value;
}
using JetVector = std::array<Jet, 3>;
using JetMatrix = std::array<std::array<Jet, 3>, 3>;
JetVector Multiply(const JetMatrix& matrix, const JetVector& vector) {
JetVector result{Constant(0.0), Constant(0.0), Constant(0.0)};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row] = result[row] + matrix[row][column] * vector[column];
}
}
return result;
}
JetMatrix Multiply(const JetMatrix& left, const JetMatrix& right) {
JetMatrix result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] = Constant(0.0);
for (std::size_t index = 0U; index < 3U; ++index) {
result[row][column] =
result[row][column] + left[row][index] * right[index][column];
}
}
}
return result;
}
JetMatrix IdentityMatrix() {
JetMatrix result{};
for (std::size_t index = 0U; index < 3U; ++index) {
result[index][index] = Constant(1.0);
}
return result;
}
JetMatrix Skew(const JetVector& value) {
JetMatrix result{};
result[0U][1U] = Scale(-1.0, value[2U]);
result[0U][2U] = value[1U];
result[1U][0U] = value[2U];
result[1U][2U] = Scale(-1.0, value[0U]);
result[2U][0U] = Scale(-1.0, value[1U]);
result[2U][1U] = value[0U];
return result;
}
Jet Dot(const JetVector& left, const JetVector& right) {
Jet result = Constant(0.0);
for (std::size_t component = 0U; component < 3U; ++component) {
result = result + left[component] * right[component];
}
return result;
}
JetMatrix Inverse(const JetMatrix& matrix) {
JetMatrix cofactor{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
const std::size_t a = (row + 1U) % 3U;
const std::size_t b = (row + 2U) % 3U;
const std::size_t c = (column + 1U) % 3U;
const std::size_t d = (column + 2U) % 3U;
cofactor[row][column] =
matrix[a][c] * matrix[b][d] - matrix[a][d] * matrix[b][c];
// Cyclic minor ordering already includes the cofactor sign.
}
}
Jet determinant = Constant(0.0);
for (std::size_t column = 0U; column < 3U; ++column) {
determinant = determinant + matrix[0U][column] * cofactor[0U][column];
}
const Jet inverse_determinant = Reciprocal(determinant);
JetMatrix result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] = cofactor[column][row] * inverse_determinant;
}
}
return result;
}
JetMatrix LeftJacobian(const JetVector& phi) {
const JetMatrix skew = Skew(phi);
JetMatrix result = IdentityMatrix();
JetMatrix term = IdentityMatrix();
double factorial = 1.0;
for (std::size_t order = 1U; order <= 32U; ++order) {
term = Multiply(term, skew);
factorial *= static_cast<double>(order + 1U);
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] =
result[row][column] + Scale(1.0 / factorial, term[row][column]);
}
}
}
return result;
}
/// @brief Uses a converged exponential series so D1/D2/D3 share one analytic
/// map.
JetMatrix ExpSkew(const JetVector& phi) {
const JetMatrix skew = Skew(phi);
JetMatrix result = IdentityMatrix();
JetMatrix term = IdentityMatrix();
double factorial = 1.0;
for (std::size_t order = 1U; order <= 32U; ++order) {
term = Multiply(term, skew);
factorial *= static_cast<double>(order);
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] =
result[row][column] + Scale(1.0 / factorial, term[row][column]);
}
}
}
return result;
}
double Determinant(const Vector3& a, const Vector3& b, const Vector3& c) {
return a.Dot(b.Cross(c));
}
/// @brief Checks the scaled physical twenty-dimensional mass without a rank
/// cut.
Status CheckPhysicalMass(const Matrix& mass,
const std::array<Vector3, kNodes>& nulls,
const double length, const SourceLocation& location,
const std::string& identity) {
const auto fail = [&](const std::string& code, const std::string& message) {
return Status::Failure(
FailureCategory::kModel,
{{Severity::kError, code, location, "*ELEMENT", identity, message}});
};
constexpr std::size_t kPhysical = 20U;
Matrix scaled{kDofs, kDofs};
double scale = 0.0;
for (std::size_t row = 0U; row < kDofs; ++row) {
for (std::size_t column = 0U; column < kDofs; ++column) {
scaled(row, column) = mass(row, column) * (row % 6U < 3U ? length : 1.0) *
(column % 6U < 3U ? length : 1.0);
if (mass(row, column) != 0.0 && scaled(row, column) == 0.0) {
return fail("invalid-nonlinear-inertia-positive-mass",
"Physical mass scaling underflowed a nonzero entry.");
}
scale = std::max(scale, std::abs(scaled(row, column)));
}
}
if (!IsFinite(scaled) || !std::isfinite(scale) || !(scale > 0.0)) {
return fail("invalid-nonlinear-inertia-positive-mass",
"Scaled physical mass must have a finite positive scale.");
}
// Scaling by a positive entry preserves conditioning and avoids density
// overflow in the Cholesky and inverse-action norms.
for (std::size_t row = 0U; row < kDofs; ++row) {
for (std::size_t column = 0U; column < kDofs; ++column) {
scaled(row, column) /= scale;
}
}
Matrix complement{kDofs, kPhysical};
for (std::size_t node = 0U; node < kNodes; ++node) {
const Vector3& normal = nulls[node];
std::array<std::size_t, 3> axes{0U, 1U, 2U};
std::stable_sort(axes.begin(), axes.end(),
[&](const std::size_t a, const std::size_t b) {
return std::abs(normal[a]) < std::abs(normal[b]);
});
std::array<Vector3, 2> tangent{};
for (std::size_t column = 0U; column < 2U; ++column) {
std::array<double, 3> axis{};
axis[axes[column]] = 1.0;
Vector3 direction{axis};
direction = direction - direction.Dot(normal) * normal;
for (std::size_t previous = 0U; previous < column; ++previous) {
direction =
direction - direction.Dot(tangent[previous]) * tangent[previous];
}
const double norm = direction.Norm();
if (!std::isfinite(norm) || !(norm > 0.0)) {
return fail("invalid-nonlinear-inertia-null",
"The analytic physical complement must remain finite.");
}
tangent[column] = direction / norm;
}
for (std::size_t row = 0U; row < 3U; ++row) {
complement(6U * node + row, 5U * node + row) = 1.0;
for (std::size_t column = 0U; column < 2U; ++column) {
complement(6U * node + 3U + row, 5U * node + 3U + column) =
tangent[column][row];
}
}
}
double mass_norm = 0.0;
double symmetry_norm = 0.0;
double null_action_norm = 0.0;
for (std::size_t row = 0U; row < kDofs; ++row) {
for (std::size_t column = 0U; column < kDofs; ++column) {
mass_norm = std::hypot(mass_norm, scaled(row, column));
symmetry_norm =
std::hypot(symmetry_norm, scaled(row, column) - scaled(column, row));
}
for (std::size_t node = 0U; node < kNodes; ++node) {
double action = 0.0;
for (std::size_t component = 0U; component < 3U; ++component) {
action +=
scaled(row, 6U * node + 3U + component) * nulls[node][component];
}
null_action_norm = std::hypot(null_action_norm, action);
}
}
double gram_error = 0.0;
double orthogonality_error = 0.0;
for (std::size_t row = 0U; row < kPhysical; ++row) {
for (std::size_t column = 0U; column < kPhysical; ++column) {
double entry = row == column ? -1.0 : 0.0;
for (std::size_t inner = 0U; inner < kDofs; ++inner) {
entry += complement(inner, row) * complement(inner, column);
}
gram_error = std::hypot(gram_error, entry);
}
for (std::size_t node = 0U; node < kNodes; ++node) {
double entry = 0.0;
for (std::size_t component = 0U; component < 3U; ++component) {
entry += complement(6U * node + 3U + component, row) *
nulls[node][component];
}
orthogonality_error = std::hypot(orthogonality_error, entry);
}
}
double null_gram_error = 0.0;
for (const Vector3& normal : nulls) {
null_gram_error = std::hypot(null_gram_error, normal.Dot(normal) - 1.0);
}
const double full_gram_error = std::hypot(
gram_error,
std::hypot(null_gram_error, std::sqrt(2.0) * orthogonality_error));
if (!(symmetry_norm / mass_norm <= 1.0e-12) ||
!(null_action_norm / mass_norm / 2.0 <= 1.0e-12) ||
!(full_gram_error <= 1.0e-12)) {
return fail("invalid-nonlinear-inertia-null",
"Scaled mass symmetry and analytic null identities failed.");
}
Matrix block{kPhysical, kPhysical};
Matrix mass_complement{kDofs, kPhysical};
for (std::size_t row = 0U; row < kDofs; ++row) {
for (std::size_t column = 0U; column < kPhysical; ++column) {
for (std::size_t inner = 0U; inner < kDofs; ++inner) {
mass_complement(row, column) +=
scaled(row, inner) * complement(inner, column);
}
}
}
double block_norm = 0.0;
for (std::size_t row = 0U; row < kPhysical; ++row) {
double row_sum = 0.0;
for (std::size_t column = 0U; column < kPhysical; ++column) {
for (std::size_t inner = 0U; inner < kDofs; ++inner) {
block(row, column) +=
complement(inner, row) * mass_complement(inner, column);
}
row_sum += std::abs(block(row, column));
}
block_norm = std::max(block_norm, row_sum);
}
Matrix lower{kPhysical, kPhysical};
for (std::size_t row = 0U; row < kPhysical; ++row) {
for (std::size_t column = 0U; column <= row; ++column) {
double entry = block(row, column);
for (std::size_t inner = 0U; inner < column; ++inner) {
entry -= lower(row, inner) * lower(column, inner);
}
if (!std::isfinite(entry) || (row == column && !(entry > 0.0))) {
return fail(
"invalid-nonlinear-inertia-positive-mass",
"Physical mass requires strictly positive Cholesky pivots.");
}
lower(row, column) =
row == column ? std::sqrt(entry) : entry / lower(column, column);
}
}
std::array<double, kPhysical> inverse_row_sums{};
double reconstruction_error = 0.0;
double block_frobenius = 0.0;
for (std::size_t row = 0U; row < kPhysical; ++row) {
for (std::size_t column = 0U; column < kPhysical; ++column) {
double reconstructed = 0.0;
for (std::size_t inner = 0U; inner <= std::min(row, column); ++inner) {
reconstructed += lower(row, inner) * lower(column, inner);
}
reconstruction_error =
std::hypot(reconstruction_error, reconstructed - block(row, column));
block_frobenius = std::hypot(block_frobenius, block(row, column));
}
}
if (!(reconstruction_error / block_frobenius <= 1.0e-12)) {
return fail("invalid-nonlinear-inertia-positive-mass",
"Physical mass Cholesky reconstruction failed.");
}
for (std::size_t column = 0U; column < kPhysical; ++column) {
std::array<double, kPhysical> solution{};
for (std::size_t row = 0U; row < kPhysical; ++row) {
double rhs = row == column ? 1.0 : 0.0;
for (std::size_t inner = 0U; inner < row; ++inner) {
rhs -= lower(row, inner) * solution[inner];
}
solution[row] = rhs / lower(row, row);
}
for (std::size_t reverse = kPhysical; reverse > 0U; --reverse) {
const std::size_t row = reverse - 1U;
double rhs = solution[row];
for (std::size_t inner = row + 1U; inner < kPhysical; ++inner) {
rhs -= lower(inner, row) * solution[inner];
}
solution[row] = rhs / lower(row, row);
inverse_row_sums[row] += std::abs(solution[row]);
}
}
const double inverse_norm =
*std::max_element(inverse_row_sums.begin(), inverse_row_sums.end());
const double reciprocal_condition = 1.0 / block_norm / inverse_norm;
if (!std::isfinite(inverse_norm) || !std::isfinite(reciprocal_condition) ||
!(reciprocal_condition >= std::ldexp(1.0, -32))) {
return fail("ill-conditioned-nonlinear-inertia-positive-mass",
"Physical mass reciprocal infinity condition is below 2^-32.");
}
return Status::Ok();
}
} // namespace
Result<DynamicRotationKinematics> EvaluateDynamicRotation(
const Vector3& phi_value, const Vector3& initial_director,
const Vector3& velocity, const Vector3& acceleration) {
if (!phi_value.IsFinite() || !initial_director.IsFinite() ||
!velocity.IsFinite() || !acceleration.IsFinite() ||
!(std::abs(initial_director.Norm() - 1.0) <= 1.0e-12) ||
!(std::acos(-1.0) - phi_value.Norm() >
std::sqrt(std::numeric_limits<double>::epsilon()))) {
return Failure<DynamicRotationKinematics>(
"invalid-dynamic-rotation", {{}, 0U}, "DYNAMIC_ROTATION",
"Dynamic rotation requires finite principal coordinates and a unit "
"initial director.");
}
JetVector phi{};
for (std::size_t component = 0U; component < 3U; ++component) {
phi[component] = Variable(phi_value[component], component);
}
const JetVector initial{Constant(initial_director.X()),
Constant(initial_director.Y()),
Constant(initial_director.Z())};
const JetVector director_jet = Multiply(ExpSkew(phi), initial);
const JetMatrix left_jacobian = LeftJacobian(phi);
JetVector null_jet = Multiply(Inverse(left_jacobian), director_jet);
const Jet null_norm_squared = Dot(null_jet, null_jet);
if (!std::isfinite(null_norm_squared.value) ||
!(null_norm_squared.value > 0.0)) {
return Failure<DynamicRotationKinematics>(
"invalid-dynamic-rotation", {{}, 0U}, "DYNAMIC_ROTATION",
"Dynamic rotation produced a nonpositive drilling-null norm.");
}
const Jet inverse_norm = InverseSqrt(null_norm_squared);
for (Jet& component : null_jet) {
component = component * inverse_norm;
}
std::array<double, 3> directional_second{};
std::array<double, 3> angular_velocity{};
std::array<double, 3> angular_acceleration{};
Matrix gradient{3U, 3U};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
gradient(row, column) = null_jet[row].d[column];
}
}
for (std::size_t row = 0U; row < 3U; ++row) {
double second = 0.0;
double omega = 0.0;
double omega_dot = 0.0;
for (std::size_t column = 0U; column < 3U; ++column) {
omega += left_jacobian[row][column].value * velocity[column];
omega_dot += left_jacobian[row][column].value * acceleration[column];
for (std::size_t other = 0U; other < 3U; ++other) {
second += null_jet[row].dd[column][other] * velocity[column] *
velocity[other];
omega_dot += left_jacobian[row][column].d[other] * velocity[other] *
velocity[column];
}
}
directional_second[row] = second;
angular_velocity[row] = omega;
angular_acceleration[row] = omega_dot;
}
const Vector3 director{director_jet[0U].value, director_jet[1U].value,
director_jet[2U].value};
const Vector3 drill_null{null_jet[0U].value, null_jet[1U].value,
null_jet[2U].value};
const Vector3 directional_second_value{directional_second};
const Vector3 angular_velocity_value{angular_velocity};
const Vector3 angular_acceleration_value{angular_acceleration};
if (!director.IsFinite() || !drill_null.IsFinite() ||
!directional_second_value.IsFinite() ||
!angular_velocity_value.IsFinite() ||
!angular_acceleration_value.IsFinite() || !IsFinite(gradient)) {
return Failure<DynamicRotationKinematics>(
"invalid-dynamic-rotation", {{}, 0U}, "DYNAMIC_ROTATION",
"Dynamic rotation evaluation must remain finite.");
}
DynamicRotationKinematics result{director,
drill_null,
std::move(gradient),
directional_second_value,
angular_velocity_value,
angular_acceleration_value,
{},
{},
{}};
for (std::size_t i = 0U; i < 3U; ++i) {
result.director_first[i] = {director_jet[0].d[i], director_jet[1].d[i],
director_jet[2].d[i]};
for (std::size_t j = 0U; j < 3U; ++j) {
result.director_second[i][j] = {director_jet[0].dd[i][j],
director_jet[1].dd[i][j],
director_jet[2].dd[i][j]};
for (std::size_t k = 0U; k < 3U; ++k) {
result.director_third[i][j][k] = {director_jet[0].ddd[i][j][k],
director_jet[1].ddd[i][j][k],
director_jet[2].ddd[i][j][k]};
}
}
}
return Result<DynamicRotationKinematics>::Success(std::move(result));
}
NonlinearMitc4Inertia::NonlinearMitc4Inertia(
std::array<Vector3, 4> coordinates, std::array<Matrix, 4> initial_frames,
std::array<Vector3, 4> initial_directors, const double thickness,
const double density, SourceLocation source_location, std::string identity)
: coordinates_(std::move(coordinates)),
initial_frames_(std::move(initial_frames)),
initial_directors_(std::move(initial_directors)),
thickness_(thickness),
density_(density),
source_location_(std::move(source_location)),
identity_(std::move(identity)) {}
Result<NonlinearMitc4Inertia> NonlinearMitc4Inertia::Create(
std::array<const Node*, 4> nodes,
std::array<ShellNodeInitialFrame, 4> initial_frames,
const ShellSection& section, const LinearElasticMaterial& material,
const double density) {
const std::string identity = Identity(nodes);
if (!std::isfinite(density) || !(density > 0.0)) {
return Failure<NonlinearMitc4Inertia>(
"invalid-nonlinear-inertia-density", section.location, identity,
"Nonlinear MITC4 inertia requires a finite positive density.");
}
const auto shell =
NonlinearMitc4Shell::Create(nodes, initial_frames, section, material);
if (!shell.HasValue()) {
return Result<NonlinearMitc4Inertia>::Failure(shell.GetStatus());
}
std::array<Vector3, kNodes> coordinates{};
std::array<Matrix, kNodes> frames{Matrix{3U, 3U}, Matrix{3U, 3U},
Matrix{3U, 3U}, Matrix{3U, 3U}};
std::array<Vector3, kNodes> directors{};
for (std::size_t node = 0U; node < kNodes; ++node) {
coordinates[node] = Vector3{nodes[node]->coordinates};
frames[node] = FrameMatrix(initial_frames[node]);
directors[node] = Vector3{initial_frames[node].director};
}
NonlinearMitc4Inertia inertia{
coordinates, std::move(frames), directors, section.thickness,
density, nodes[0U]->location, identity};
const auto initial = inertia.Evaluate({}, Vector{kDofs}, Vector{kDofs});
if (!initial.HasValue()) {
return Result<NonlinearMitc4Inertia>::Failure(initial.GetStatus());
}
return Result<NonlinearMitc4Inertia>::Success(std::move(inertia));
}
Result<NonlinearMitc4InertiaEvaluation> NonlinearMitc4Inertia::Evaluate(
const NonlinearMitc4TrialDofs& trial, const Vector& velocity,
const Vector& acceleration) const {
if (velocity.Size() != kDofs || acceleration.Size() != kDofs ||
!IsFinite(velocity) || !IsFinite(acceleration)) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-rate", source_location_, identity_,
"Nonlinear MITC4 inertia requires 24 finite rates.");
}
std::array<DynamicRotationKinematics, kNodes> rotations{};
std::array<Vector3, kNodes> nulls{};
double length = 0.0;
for (std::size_t node = 0U; node < kNodes; ++node) {
length = std::max(
length,
(coordinates_[(node + 1U) % kNodes] - coordinates_[node]).Norm());
if (!trial.displacement[node].IsFinite() ||
!MakeFiniteRotationState(trial.rotation_vector[node],
initial_frames_[node])
.HasValue()) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-trial", source_location_, identity_,
"Nonlinear MITC4 inertia trial must be in the principal chart.");
}
const auto rotation = EvaluateDynamicRotation(
trial.rotation_vector[node], initial_directors_[node], {}, {});
if (!rotation.HasValue()) {
return Result<NonlinearMitc4InertiaEvaluation>::Failure(
rotation.GetStatus());
}
rotations[node] = rotation.Value();
nulls[node] = rotation.Value().drill_null;
for (std::size_t previous = 0U; previous < node; ++previous) {
if (!(rotations[node].director.Dot(rotations[previous].director) >
-1.0 + 1.0e-8)) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-director", source_location_, identity_,
"Nonlinear MITC4 inertia trial directors must not be opposed.");
}
}
}
if (!std::isfinite(length) || !(length > 0.0)) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-geometry", source_location_, identity_,
"Nonlinear MITC4 inertia requires a finite positive initial edge.");
}
NonlinearMitc4InertiaEvaluation result{Matrix{kDofs, kDofs},
Matrix{kDofs, kDofs},
Matrix{kDofs, kDofs},
Vector{kDofs},
Vector{kDofs},
0.0,
{},
{},
{},
{}};
for (std::size_t xi = 0U; xi < kGauss.size(); ++xi) {
for (std::size_t eta = 0U; eta < kGauss.size(); ++eta) {
const Mitc4ShapeFunctions shape =
Mitc4Shell::ShapeFunctions(kGauss[xi], kGauss[eta]);
for (std::size_t zeta = 0U; zeta < kGauss.size(); ++zeta) {
const double z = 0.5 * thickness_ * kGauss[zeta];
const double weight = kWeights[xi] * kWeights[eta] * kWeights[zeta];
Vector3 ref_xi{};
Vector3 ref_eta{};
Vector3 ref_zeta{};
Vector3 current_xi{};
Vector3 current_eta{};
Vector3 current_zeta{};
Vector3 position{};
Vector3 velocity_point{};
Vector3 acceleration_point{};
Vector3 convective_point{};
std::array<Vector3, kDofs> h{};
std::array<Vector3, kDofs> velocity_derivative{};
std::array<Vector3, kDofs> position_derivative{};
for (std::size_t node = 0U; node < kNodes; ++node) {
const std::size_t base = 6U * node;
const auto& rotation = rotations[node];
const Vector3 reference =
coordinates_[node] + z * initial_directors_[node];
const Vector3 current = coordinates_[node] +
trial.displacement[node] +
z * rotation.director;
ref_xi = ref_xi + shape.xi_derivatives[node] * reference;
ref_eta = ref_eta + shape.eta_derivatives[node] * reference;
ref_zeta = ref_zeta + 0.5 * thickness_ * shape.values[node] *
initial_directors_[node];
current_xi = current_xi + shape.xi_derivatives[node] * current;
current_eta = current_eta + shape.eta_derivatives[node] * current;
current_zeta = current_zeta + 0.5 * thickness_ * shape.values[node] *
rotation.director;
position = position + shape.values[node] * current;
for (std::size_t component = 0U; component < 3U; ++component) {
std::array<double, 3> translation{};
translation[component] = shape.values[node];
h[base + component] = Vector3{translation};
h[base + 3U + component] =
z * shape.values[node] * rotation.director_first[component];
for (std::size_t other = 0U; other < 3U; ++other) {
const Vector3 second = z * shape.values[node] *
rotation.director_second[component][other];
convective_point =
convective_point + second * velocity[base + 3U + component] *
velocity[base + 3U + other];
velocity_derivative[base + 3U + component] =
velocity_derivative[base + 3U + component] +
2.0 * second * velocity[base + 3U + other];
position_derivative[base + 3U + component] =
position_derivative[base + 3U + component] +
second * acceleration[base + 3U + other];
for (std::size_t third = 0U; third < 3U; ++third) {
position_derivative[base + 3U + component] =
position_derivative[base + 3U + component] +
z * shape.values[node] *
rotation.director_third[component][other][third] *
velocity[base + 3U + other] *
velocity[base + 3U + third];
}
}
}
}
const double jacobian = Determinant(ref_xi, ref_eta, ref_zeta);
const double current_jacobian =
Determinant(current_xi, current_eta, current_zeta);
if (!std::isfinite(jacobian) || !(jacobian > 0.0) ||
!std::isfinite(current_jacobian) || !(current_jacobian > 0.0)) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-geometry", source_location_, identity_,
"Inertia requires positive reference and current mass-point "
"Jacobians.");
}
for (std::size_t row = 0U; row < kDofs; ++row) {
velocity_point = velocity_point + h[row] * velocity[row];
acceleration_point = acceleration_point + h[row] * acceleration[row];
}
acceleration_point = acceleration_point + convective_point;
const double scale = density_ * weight * jacobian;
if (!std::isfinite(scale) || !(scale > 0.0)) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-positive-mass", source_location_,
identity_,
"Mass-point integration weight must be finite and positive.");
}
result.kinetic_energy +=
0.5 * scale * velocity_point.Dot(velocity_point);
result.momentum = result.momentum + scale * velocity_point;
result.momentum_rate =
result.momentum_rate + scale * acceleration_point;
result.angular_momentum =
result.angular_momentum + scale * position.Cross(velocity_point);
result.angular_momentum_rate =
result.angular_momentum_rate +
scale * position.Cross(acceleration_point);
result.momentum_rate_scale += scale * acceleration_point.Norm();
result.angular_momentum_rate_scale +=
scale * position.Cross(acceleration_point).Norm();
for (std::size_t row = 0U; row < kDofs; ++row) {
result.force[row] += scale * h[row].Dot(acceleration_point);
// Keep c independent of a; subtracting M*a destroys small c values.
result.convective_force[row] += scale * h[row].Dot(convective_point);
for (std::size_t column = 0U; column < kDofs; ++column) {
result.mass(row, column) += scale * h[row].Dot(h[column]);
result.velocity_tangent(row, column) +=
scale * h[row].Dot(velocity_derivative[column]);
double position_entry = h[row].Dot(position_derivative[column]);
if (row / 6U == column / 6U && row % 6U >= 3U &&
column % 6U >= 3U) {
const std::size_t node = row / 6U;
position_entry +=
z * shape.values[node] *
rotations[node]
.director_second[row % 6U - 3U][column % 6U - 3U]
.Dot(acceleration_point);
}
result.position_tangent(row, column) += scale * position_entry;
}
}
}
}
}
if (!IsFinite(result.mass) || !IsFinite(result.position_tangent) ||
!IsFinite(result.velocity_tangent) || !IsFinite(result.force) ||
!IsFinite(result.convective_force) ||
!std::isfinite(result.kinetic_energy) || !result.momentum.IsFinite() ||
!result.angular_momentum.IsFinite() || !result.momentum_rate.IsFinite() ||
!result.angular_momentum_rate.IsFinite() ||
!std::isfinite(result.momentum_rate_scale) ||
!std::isfinite(result.angular_momentum_rate_scale)) {
return Failure<NonlinearMitc4InertiaEvaluation>(
"invalid-nonlinear-inertia-evaluation", source_location_, identity_,
"Nonlinear MITC4 inertia evaluation must remain finite.");
}
const Status physical_mass = CheckPhysicalMass(result.mass, nulls, length,
source_location_, identity_);
if (!physical_mass.IsOk()) {
return Result<NonlinearMitc4InertiaEvaluation>::Failure(physical_mass);
}
return Result<NonlinearMitc4InertiaEvaluation>::Success(std::move(result));
}
} // namespace fesa
+297 -92
View File
@@ -42,9 +42,8 @@ template <class T>
Result<T> Failure(const std::string& code, const SourceLocation& location,
const std::string& identity, std::string message) {
return Result<T>::Failure(Status::Failure(
FailureCategory::kModel,
{{Severity::kError, code, location, "*ELEMENT", identity,
std::move(message)}}));
FailureCategory::kModel, {{Severity::kError, code, location, "*ELEMENT",
identity, std::move(message)}}));
}
/// @brief Builds the stable source label concatenation used by diagnostics.
@@ -134,8 +133,7 @@ bool MakeLocalFrame(const Vector3& midsurface_xi, const Vector3& midsurface_eta,
}
const Vector3 e1_candidate = midsurface_xi - midsurface_xi.Dot(e3) * e3;
const double e1_norm = e1_candidate.Norm();
if (!e1_candidate.IsFinite() || !std::isfinite(e1_norm) ||
!(e1_norm > 0.0)) {
if (!e1_candidate.IsFinite() || !std::isfinite(e1_norm) || !(e1_norm > 0.0)) {
return false;
}
const Vector3 e1 = e1_candidate / e1_norm;
@@ -155,8 +153,7 @@ bool EvaluateGeometry(const std::array<Vector3, kNodeCount>& coordinates,
const double thickness, const double xi, const double eta,
const double zeta, Geometry& geometry) {
const Mitc4ShapeFunctions shape = Mitc4Shell::ShapeFunctions(xi, eta);
const Vector3 reference_xi =
DerivativeSum(shape.xi_derivatives, coordinates);
const Vector3 reference_xi = DerivativeSum(shape.xi_derivatives, coordinates);
const Vector3 reference_eta =
DerivativeSum(shape.eta_derivatives, coordinates);
const Vector3 initial_director_xi =
@@ -218,8 +215,7 @@ Tensor DirectStrain(const Geometry& geometry) {
Tensor strain{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
strain[row][column] =
0.5 * (geometry.current_covariant[row].Dot(
strain[row][column] = 0.5 * (geometry.current_covariant[row].Dot(
geometry.current_covariant[column]) -
geometry.reference_covariant[row].Dot(
geometry.reference_covariant[column]));
@@ -278,17 +274,17 @@ EngineeringStress Stress(const EngineeringStrain& strain, const double youngs,
const double shear = kShearCorrection * youngs / (2.0 * (1.0 + poisson));
return {factor * (strain[0U] + poisson * strain[1U]),
factor * (poisson * strain[0U] + strain[1U]),
factor * (1.0 - poisson) * 0.5 * strain[2U],
shear * strain[3U], shear * strain[4U]};
factor * (1.0 - poisson) * 0.5 * strain[2U], shear * strain[3U],
shear * strain[4U]};
}
/// @brief Evaluates direct strain at one mandatory edge-midpoint tying point.
bool EvaluateTyingSample(const std::array<Vector3, kNodeCount>& coordinates,
bool EvaluateTyingSample(
const std::array<Vector3, kNodeCount>& coordinates,
const std::array<Vector3, kNodeCount>& initial_directors,
const std::array<Vector3, kNodeCount>& directors,
const NonlinearMitc4TrialDofs& trial,
const double thickness, const double xi,
const double eta, Tensor& strain) {
const NonlinearMitc4TrialDofs& trial, const double thickness,
const double xi, const double eta, Tensor& strain) {
Geometry geometry{};
if (!EvaluateGeometry(coordinates, initial_directors, directors, trial,
thickness, xi, eta, 0.0, geometry)) {
@@ -319,7 +315,8 @@ bool EvaluatePoint(const std::array<Vector3, kNodeCount>& coordinates,
covariant[1U][2U] = weights.eta_zeta[0U] * tying_samples[2U][1U][2U] +
weights.eta_zeta[1U] * tying_samples[3U][1U][2U];
covariant[2U][1U] = covariant[1U][2U];
const Tensor cartesian = CartesianStrain(covariant, point.geometry.reciprocal);
const Tensor cartesian =
CartesianStrain(covariant, point.geometry.reciprocal);
point.strain = LocalEngineering(cartesian, point.geometry.local_frame);
point.stress = Stress(point.strain, youngs, poisson);
return IsFinite(covariant) && IsFinite(cartesian) && IsFinite(point.strain) &&
@@ -372,6 +369,10 @@ struct Jet {
double value{};
std::array<double, kGlobalDofCount> first{};
Hessian second;
double directional_first{};
double directional_second{};
std::array<double, kGlobalDofCount> mixed_first{};
std::array<double, kGlobalDofCount> mixed_second{};
};
using JetVector3 = std::array<Jet, 3>;
@@ -386,9 +387,11 @@ Jet Constant(const double value) {
}
/// @brief Returns one independent global trial coordinate.
Jet Variable(const double value, const std::size_t index) {
Jet Variable(const double value, const std::size_t index,
const double direction = 0.0) {
Jet result = Constant(value);
result.first[index] = 1.0;
result.directional_first = direction;
return result;
}
@@ -396,8 +399,15 @@ Jet Variable(const double value, const std::size_t index) {
Jet operator+(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value + right.value;
result.directional_first = left.directional_first + right.directional_first;
result.directional_second =
left.directional_second + right.directional_second;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] = left.first[index] + right.first[index];
result.mixed_first[index] =
left.mixed_first[index] + right.mixed_first[index];
result.mixed_second[index] =
left.mixed_second[index] + right.mixed_second[index];
}
for (std::size_t index = 0U; index < kHessianSize; ++index) {
result.second[index] = left.second[index] + right.second[index];
@@ -409,8 +419,15 @@ Jet operator+(const Jet& left, const Jet& right) {
Jet operator-(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value - right.value;
result.directional_first = left.directional_first - right.directional_first;
result.directional_second =
left.directional_second - right.directional_second;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] = left.first[index] - right.first[index];
result.mixed_first[index] =
left.mixed_first[index] - right.mixed_first[index];
result.mixed_second[index] =
left.mixed_second[index] - right.mixed_second[index];
}
for (std::size_t index = 0U; index < kHessianSize; ++index) {
result.second[index] = left.second[index] - right.second[index];
@@ -422,9 +439,26 @@ Jet operator-(const Jet& left, const Jet& right) {
Jet operator*(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value * right.value;
result.directional_first = left.directional_first * right.value +
left.value * right.directional_first;
result.directional_second =
left.directional_second * right.value +
2.0 * left.directional_first * right.directional_first +
left.value * right.directional_second;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] =
left.first[index] * right.value + left.value * right.first[index];
result.mixed_first[index] = left.mixed_first[index] * right.value +
left.first[index] * right.directional_first +
left.directional_first * right.first[index] +
left.value * right.mixed_first[index];
result.mixed_second[index] =
left.mixed_second[index] * right.value +
2.0 * left.mixed_first[index] * right.directional_first +
left.first[index] * right.directional_second +
left.directional_second * right.first[index] +
2.0 * left.directional_first * right.mixed_first[index] +
left.value * right.mixed_second[index];
}
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
@@ -442,8 +476,12 @@ Jet operator*(const Jet& left, const Jet& right) {
Jet operator*(const double scalar, const Jet& value) {
Jet result{};
result.value = scalar * value.value;
result.directional_first = scalar * value.directional_first;
result.directional_second = scalar * value.directional_second;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] = scalar * value.first[index];
result.mixed_first[index] = scalar * value.mixed_first[index];
result.mixed_second[index] = scalar * value.mixed_second[index];
}
for (std::size_t index = 0U; index < kHessianSize; ++index) {
result.second[index] = scalar * value.second[index];
@@ -456,11 +494,24 @@ Jet operator*(const Jet& value, const double scalar) { return scalar * value; }
/// @brief Applies a scalar unary function through its first two derivatives.
Jet Compose(const Jet& input, const double value, const double first,
const double second) {
const double second, const double third) {
Jet result{};
result.value = value;
result.directional_first = first * input.directional_first;
result.directional_second =
second * input.directional_first * input.directional_first +
first * input.directional_second;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] = first * input.first[index];
result.mixed_first[index] =
second * input.first[index] * input.directional_first +
first * input.mixed_first[index];
result.mixed_second[index] =
third * input.first[index] * input.directional_first *
input.directional_first +
second * (2.0 * input.mixed_first[index] * input.directional_first +
input.first[index] * input.directional_second) +
first * input.mixed_second[index];
}
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
@@ -476,7 +527,8 @@ Jet Compose(const Jet& input, const double value, const double first,
Jet Reciprocal(const Jet& value) {
const double inverse = 1.0 / value.value;
return Compose(value, inverse, -inverse * inverse,
2.0 * inverse * inverse * inverse);
2.0 * inverse * inverse * inverse,
-6.0 * inverse * inverse * inverse * inverse);
}
/// @brief Divides two second-order trial-coordinate quantities.
@@ -487,19 +539,20 @@ Jet operator/(const Jet& left, const Jet& right) {
/// @brief Computes the analytic square root variation.
Jet Sqrt(const Jet& value) {
const double root = std::sqrt(value.value);
return Compose(value, root, 0.5 / root, -0.25 / (root * root * root));
return Compose(value, root, 0.5 / root, -0.25 / (root * root * root),
0.375 / (root * root * root * root * root));
}
/// @brief Computes the analytic sine variation.
Jet Sin(const Jet& value) {
return Compose(value, std::sin(value.value), std::cos(value.value),
-std::sin(value.value));
-std::sin(value.value), -std::cos(value.value));
}
/// @brief Computes the analytic cosine variation.
Jet Cos(const Jet& value) {
return Compose(value, std::cos(value.value), -std::sin(value.value),
-std::cos(value.value));
-std::cos(value.value), std::sin(value.value));
}
/// @brief Computes the two-argument angle and its analytic first/second map.
@@ -529,15 +582,40 @@ Jet Atan2(const Jet& y, const Jet& x) {
derivative_xx * x.first[row] * x.first[column];
}
}
// Use a bounded ratio for the directional third derivative only. Retain
// the established value/gradient/Hessian reduction above for static runs.
const bool use_x = std::abs(x.value) >= std::abs(y.value);
const Jet ratio = use_x ? y / x : x / y;
const double denominator = 1.0 + ratio.value * ratio.value;
const Jet angle = Compose(ratio, std::atan(ratio.value), 1.0 / denominator,
-2.0 * ratio.value / (denominator * denominator),
(6.0 * ratio.value * ratio.value - 2.0) /
(denominator * denominator * denominator));
const double sign = use_x ? 1.0 : -1.0;
result.directional_first = sign * angle.directional_first;
result.directional_second = sign * angle.directional_second;
for (std::size_t i = 0U; i < kGlobalDofCount; ++i) {
result.mixed_first[i] = sign * angle.mixed_first[i];
result.mixed_second[i] = sign * angle.mixed_second[i];
}
return result;
}
/// @brief Checks every coefficient in an automatic-differentiation quantity.
bool IsFinite(const Jet& value) {
if (!std::isfinite(value.value)) {
if (!std::isfinite(value.value) || !std::isfinite(value.directional_first) ||
!std::isfinite(value.directional_second)) {
return false;
}
return std::all_of(value.first.begin(), value.first.end(),
return std::all_of(value.mixed_first.begin(), value.mixed_first.end(),
[](const double coefficient) {
return std::isfinite(coefficient);
}) &&
std::all_of(value.mixed_second.begin(), value.mixed_second.end(),
[](const double coefficient) {
return std::isfinite(coefficient);
}) &&
std::all_of(value.first.begin(), value.first.end(),
[](const double coefficient) {
return std::isfinite(coefficient);
}) &&
@@ -728,8 +806,39 @@ JetMatrix3 ExpSkew(const JetVector3& rotation) {
(Constant(1.0) - Cos(norm)) / squared_norm;
}
const JetMatrix3 skew = Skew(rotation);
return Add(Add(Identity(), Scale(sine_over_norm, skew)),
JetMatrix3 result =
Add(Add(Identity(), Scale(sine_over_norm, skew)),
Scale(one_minus_cosine_over_norm_squared, Multiply(skew, skew)));
if (squared_norm.value < 0.25) {
// Third derivatives amplify cancellation in 1-cos(theta). Preserve the
// established static coefficients and use a stable series for the new
// directional derivatives of the complete rotation tensor.
Jet sine_series = Constant(1.0);
Jet cosine_series = Constant(0.5);
Jet power = Constant(1.0);
double sine_coefficient = 1.0;
double cosine_coefficient = 0.5;
for (unsigned int k = 1U; k <= 9U; ++k) {
power = power * squared_norm;
sine_coefficient /= -static_cast<double>((2U * k) * (2U * k + 1U));
cosine_coefficient /= -static_cast<double>((2U * k + 1U) * (2U * k + 2U));
sine_series = sine_series + sine_coefficient * power;
cosine_series = cosine_series + cosine_coefficient * power;
}
const JetMatrix3 stable = Add(Add(Identity(), Scale(sine_series, skew)),
Scale(cosine_series, Multiply(skew, skew)));
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column].directional_first =
stable[row][column].directional_first;
result[row][column].directional_second =
stable[row][column].directional_second;
result[row][column].mixed_first = stable[row][column].mixed_first;
result[row][column].mixed_second = stable[row][column].mixed_second;
}
}
}
return result;
}
/// @brief Stores the complete Jet-valued global element trial coordinates.
@@ -741,14 +850,18 @@ struct JetTrial {
/// @brief Lifts one validated nonlinear trial into independent Jet variables.
JetTrial MakeJetTrial(const NonlinearMitc4TrialDofs& trial,
const std::array<Matrix, kNodeCount>& initial_frames) {
const std::array<Matrix, kNodeCount>& initial_frames,
const Vector* direction) {
JetTrial result{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
for (std::size_t component = 0U; component < 3U; ++component) {
result.displacement[node][component] =
Variable(trial.displacement[node][component], 6U * node + component);
result.displacement[node][component] = Variable(
trial.displacement[node][component], 6U * node + component,
direction == nullptr ? 0.0 : (*direction)[6U * node + component]);
result.rotation[node][component] = Variable(
trial.rotation_vector[node][component], 6U * node + 3U + component);
trial.rotation_vector[node][component], 6U * node + 3U + component,
direction == nullptr ? 0.0
: (*direction)[6U * node + 3U + component]);
}
JetMatrix3 frame{};
for (std::size_t row = 0U; row < 3U; ++row) {
@@ -764,52 +877,42 @@ JetTrial MakeJetTrial(const NonlinearMitc4TrialDofs& trial,
return result;
}
/// @brief Forms one current covariant basis from the exact trial variables.
std::array<JetVector3, 3> CurrentCovariant(
const std::array<Vector3, kNodeCount>& coordinates, const JetTrial& trial,
const double thickness, const double xi, const double eta,
const double zeta) {
const Mitc4ShapeFunctions shape = Mitc4Shell::ShapeFunctions(xi, eta);
JetVector3 displacement_xi{};
JetVector3 displacement_eta{};
JetVector3 director{};
JetVector3 director_xi{};
JetVector3 director_eta{};
Vector3 reference_xi{};
Vector3 reference_eta{};
/// @brief Forms covariant increments directly, preserving exact zero motion.
std::array<JetVector3, 3> CovariantIncrement(
const std::array<Vector3, kNodeCount>& initial_directors,
const JetTrial& trial, const double thickness, const double xi,
const double eta, const double zeta) {
const auto shape = Mitc4Shell::ShapeFunctions(xi, eta);
std::array<JetVector3, 3> increment{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
reference_xi =
reference_xi + shape.xi_derivatives[node] * coordinates[node];
reference_eta =
reference_eta + shape.eta_derivatives[node] * coordinates[node];
displacement_xi = Add(displacement_xi, Scale(shape.xi_derivatives[node],
trial.displacement[node]));
displacement_eta = Add(displacement_eta, Scale(shape.eta_derivatives[node],
trial.displacement[node]));
director = Add(director, Scale(shape.values[node], trial.director[node]));
director_xi = Add(director_xi,
Scale(shape.xi_derivatives[node], trial.director[node]));
director_eta = Add(
director_eta, Scale(shape.eta_derivatives[node], trial.director[node]));
const auto displacement =
Subtract(trial.displacement[node], trial.displacement[0U]);
const auto director =
Subtract(trial.director[node], ConstantVector(initial_directors[node]));
const auto layer =
Add(displacement, Scale(0.5 * thickness * zeta, director));
increment[0U] =
Add(increment[0U], Scale(shape.xi_derivatives[node], layer));
increment[1U] =
Add(increment[1U], Scale(shape.eta_derivatives[node], layer));
increment[2U] = Add(increment[2U],
Scale(0.5 * thickness * shape.values[node], director));
}
const double half_thickness = 0.5 * thickness;
return {Add(Add(ConstantVector(reference_xi), displacement_xi),
Scale(half_thickness * zeta, director_xi)),
Add(Add(ConstantVector(reference_eta), displacement_eta),
Scale(half_thickness * zeta, director_eta)),
Scale(half_thickness, director)};
return increment;
}
/// @brief Forms the direct covariant Green--Lagrange strain tensor.
JetTensor DirectStrain(const std::array<JetVector3, 3>& current,
JetTensor DirectStrain(const std::array<JetVector3, 3>& increment,
const Geometry& reference) {
JetTensor result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] =
0.5 * (Dot(current[row], current[column]) -
Constant(reference.reference_covariant[row].Dot(
reference.reference_covariant[column])));
0.5 * (Dot(ConstantVector(reference.reference_covariant[row]),
increment[column]) +
Dot(increment[row],
ConstantVector(reference.reference_covariant[column])) +
Dot(increment[row], increment[column]));
}
}
result[2U][2U] = Constant(0.0);
@@ -867,7 +970,7 @@ Jet PhysicalEnergy(const std::array<Vector3, kNodeCount>& coordinates,
return {};
}
tying[point] =
DirectStrain(CurrentCovariant(coordinates, trial, thickness,
DirectStrain(CovariantIncrement(initial_directors, trial, thickness,
tying_locations[point][0U],
tying_locations[point][1U], 0.0),
reference);
@@ -892,7 +995,7 @@ Jet PhysicalEnergy(const std::array<Vector3, kNodeCount>& coordinates,
return {};
}
JetTensor covariant =
DirectStrain(CurrentCovariant(coordinates, trial, thickness,
DirectStrain(CovariantIncrement(initial_directors, trial, thickness,
surface[0U], surface[1U], zeta),
reference);
const Mitc4TyingWeights weights =
@@ -940,11 +1043,41 @@ JetMatrix3 PolarRotation(const JetMatrix3& deformation) {
return result;
}
/// @brief Returns the speed of physical spin for a unit chart null vector.
Jet DrillSpinRate(const JetVector3& phi, const Vector3& director) {
const Jet square = Dot(phi, phi);
Jet coefficient{};
if (square.value < 0.25) {
constexpr std::array<double, 9> series{1.0 / 12.0,
1.0 / 720.0,
1.0 / 30240.0,
1.0 / 1209600.0,
1.0 / 47900160.0,
691.0 / 1307674368000.0,
1.0 / 74724249600.0,
3617.0 / 10670622842880000.0,
43867.0 / 5109094217170944000.0};
coefficient = Constant(series.back());
for (std::size_t i = series.size() - 1U; i > 0U; --i)
coefficient = Constant(series[i - 1U]) + square * coefficient;
} else {
const Jet half = 0.5 * Sqrt(square);
coefficient = (Constant(1.0) - half * Cos(half) * Reciprocal(Sin(half))) *
Reciprocal(square);
}
const auto d0 = ConstantVector(director);
const auto cross = Cross(phi, d0);
const auto inverse_action =
Add(Add(d0, Scale(0.5, cross)), Scale(coefficient, Cross(phi, cross)));
return Reciprocal(Sqrt(Dot(inverse_action, inverse_action)));
}
/// @brief Evaluates the objective numerical drilling potential and variations.
Jet DrillingEnergy(const std::array<Vector3, kNodeCount>& coordinates,
const std::array<Vector3, kNodeCount>& initial_directors,
const JetTrial& trial, const double thickness,
const double drilling_stiffness, bool& valid) {
const double drilling_stiffness, bool& valid, Matrix* metric,
std::array<Jet, 4>* constraints) {
const NonlinearMitc4TrialDofs reference_trial{};
Geometry reference{};
if (!EvaluateGeometry(coordinates, initial_directors, initial_directors,
@@ -953,8 +1086,8 @@ Jet DrillingEnergy(const std::array<Vector3, kNodeCount>& coordinates,
return {};
}
const auto current =
CurrentCovariant(coordinates, trial, thickness, 0.0, 0.0, 0.0);
JetMatrix3 deformation{};
CovariantIncrement(initial_directors, trial, thickness, 0.0, 0.0, 0.0);
JetMatrix3 deformation = Identity();
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
for (std::size_t alpha = 0U; alpha < 3U; ++alpha) {
@@ -991,7 +1124,19 @@ Jet DrillingEnergy(const std::array<Vector3, kNodeCount>& coordinates,
const Jet cosine =
0.5 * (twist[0U][0U] + twist[1U][1U] + twist[2U][2U] - Constant(1.0));
const Jet gamma = Atan2(sine, cosine);
if (constraints != nullptr) {
// n^T grad(Uphys)=0 and Dgamma_j[n_i]=delta_ij/||Jl^-1 d||.
(*constraints)[node] =
drilling_stiffness *
DrillSpinRate(trial.rotation[node], initial_directors[node]) * gamma;
}
energy = energy + 0.5 * drilling_stiffness * gamma * gamma;
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
(*metric)(row, column) +=
drilling_stiffness * gamma.first[row] * gamma.first[column];
}
}
}
if (!IsFinite(energy)) {
valid = false;
@@ -1040,13 +1185,15 @@ Result<NonlinearMitc4Shell> NonlinearMitc4Shell::Create(
for (std::size_t previous = 0U; previous < node; ++previous) {
if (coordinates[node] == coordinates[previous]) {
return Failure<NonlinearMitc4Shell>(
"invalid-nonlinear-shell-geometry", nodes[node]->location,
identity, "Nonlinear MITC4 nodes must have distinct coordinates.");
"invalid-nonlinear-shell-geometry", nodes[node]->location, identity,
"Nonlinear MITC4 nodes must have distinct coordinates.");
}
if (initial_frames[node].node_index == initial_frames[previous].node_index) {
if (initial_frames[node].node_index ==
initial_frames[previous].node_index) {
return Failure<NonlinearMitc4Shell>(
"invalid-nonlinear-shell-frame", nodes[node]->location, identity,
"Nonlinear MITC4 initial frames must identify four distinct nodes.");
"Nonlinear MITC4 initial frames must identify four distinct "
"nodes.");
}
}
}
@@ -1089,7 +1236,8 @@ Result<NonlinearMitc4Shell> NonlinearMitc4Shell::Create(
})) {
return Failure<NonlinearMitc4Shell>(
"invalid-nonlinear-shell-frame", nodes[0U]->location, identity,
"Nonlinear MITC4 initial directors must follow the source positive face.");
"Nonlinear MITC4 initial directors must follow the source positive "
"face.");
}
if (!std::isfinite(section.thickness) || !(section.thickness > 0.0)) {
@@ -1150,12 +1298,13 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::EvaluateKinematics(
"invalid-nonlinear-shell-trial", source_location_, identity_,
"Nonlinear MITC4 trial displacements and rotations must be finite.");
}
const auto state =
MakeFiniteRotationState(trial.rotation_vector[node], initial_frames_[node]);
const auto state = MakeFiniteRotationState(trial.rotation_vector[node],
initial_frames_[node]);
if (!state.HasValue()) {
return Failure<NonlinearMitc4Recovery>(
"invalid-nonlinear-shell-rotation", source_location_, identity_,
"Nonlinear MITC4 trial rotations must remain in the principal chart.");
"Nonlinear MITC4 trial rotations must remain in the principal "
"chart.");
}
directors[node] = state.Value().director;
}
@@ -1185,10 +1334,12 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::EvaluateKinematics(
for (std::size_t point = 0U; point < tying_locations.size(); ++point) {
if (!EvaluateTyingSample(coordinates_, initial_directors_, directors, trial,
thickness_, tying_locations[point][0U],
tying_locations[point][1U], tying_samples[point])) {
tying_locations[point][1U],
tying_samples[point])) {
return Failure<NonlinearMitc4Recovery>(
"invalid-nonlinear-shell-geometry", source_location_, identity_,
"Nonlinear MITC4 tying-point reference or trial geometry is invalid.");
"Nonlinear MITC4 tying-point reference or trial geometry is "
"invalid.");
}
}
@@ -1198,8 +1349,7 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::EvaluateKinematics(
ShellMidsurfaceLocation::kGp3, ShellMidsurfaceLocation::kGp4};
const std::array<std::array<double, 2>, 4> surface_points{
std::array<double, 2>{-gauss, -gauss},
std::array<double, 2>{gauss, -gauss},
std::array<double, 2>{gauss, gauss},
std::array<double, 2>{gauss, -gauss}, std::array<double, 2>{gauss, gauss},
std::array<double, 2>{-gauss, gauss}};
constexpr std::array<double, 2> thickness_gauss{-1.0, 1.0};
constexpr std::array<double, 3> stress_zeta{-1.0, 0.0, 1.0};
@@ -1236,12 +1386,12 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::EvaluateKinematics(
for (std::size_t component = 0U; component < 3U; ++component) {
row.generalized_strain[component] += 0.5 * point.strain[component];
row.generalized_strain[3U + component] +=
3.0 * thickness_sign * gauss * point.strain[component] /
thickness_;
3.0 * thickness_sign * gauss * point.strain[component] / thickness_;
row.section_resultant[component] +=
0.5 * thickness_ * point.stress[component];
row.section_resultant[3U + component] +=
0.25 * thickness_ * thickness_sign * gauss * point.stress[component];
row.section_resultant[3U + component] += 0.25 * thickness_ *
thickness_sign * gauss *
point.stress[component];
}
for (std::size_t component = 0U; component < 2U; ++component) {
row.generalized_strain[6U + component] +=
@@ -1269,9 +1419,10 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::EvaluateKinematics(
"invalid-nonlinear-shell-geometry", source_location_, identity_,
"Nonlinear MITC4 section-position recovery geometry is invalid.");
}
row.stress[position] = {stress_position[position], stress_zeta[position],
{point.stress[0U], point.stress[1U],
point.stress[2U]}};
row.stress[position] = {
stress_position[position],
stress_zeta[position],
{point.stress[0U], point.stress[1U], point.stress[2U]}};
}
}
@@ -1289,7 +1440,22 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::RecoverState(
}
Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
const NonlinearMitc4Shell& element, const NonlinearMitc4TrialDofs& trial) {
const NonlinearMitc4Shell& element, const NonlinearMitc4TrialDofs& trial,
const Vector* coordinate_direction) {
if (coordinate_direction != nullptr) {
if (coordinate_direction->Size() != kGlobalDofCount) {
return Failure<NonlinearMitc4InternalEquilibrium>(
"invalid-nonlinear-shell-equilibrium", element.source_location_,
element.identity_, "The directional derivative requires 24 DOFs.");
}
for (std::size_t i = 0U; i < kGlobalDofCount; ++i) {
if (!std::isfinite((*coordinate_direction)[i])) {
return Failure<NonlinearMitc4InternalEquilibrium>(
"invalid-nonlinear-shell-equilibrium", element.source_location_,
element.identity_, "The directional derivative must be finite.");
}
}
}
const auto recovery = element.EvaluateKinematics(trial);
if (!recovery.HasValue()) {
return Result<NonlinearMitc4InternalEquilibrium>::Failure(
@@ -1297,14 +1463,18 @@ Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
}
bool valid = true;
const JetTrial jet_trial = MakeJetTrial(trial, element.initial_frames_);
const JetTrial jet_trial =
MakeJetTrial(trial, element.initial_frames_, coordinate_direction);
const Jet physical =
PhysicalEnergy(element.coordinates_, element.initial_directors_,
jet_trial, element.thickness_, element.youngs_modulus_,
element.poisson_ratio_, valid);
Matrix drilling_metric{kGlobalDofCount, kGlobalDofCount};
std::array<Jet, 4> constraints{};
const Jet drilling = DrillingEnergy(
element.coordinates_, element.initial_directors_, jet_trial,
element.thickness_, element.drilling_stiffness_, valid);
element.thickness_, element.drilling_stiffness_, valid, &drilling_metric,
coordinate_direction != nullptr ? &constraints : nullptr);
const Jet internal_energy = physical + drilling;
if (!valid || !IsFinite(internal_energy)) {
return Failure<NonlinearMitc4InternalEquilibrium>(
@@ -1344,8 +1514,43 @@ Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
std::move(internal_residual), std::move(internal_tangent),
Vector{kGlobalDofCount}, recovery.Value(),
recovery.Value().physical_strain_energy};
if (coordinate_direction != nullptr) {
equilibrium.drilling_constraint = Vector{4U};
equilibrium.drilling_constraint_gradient = Matrix{4U, kGlobalDofCount};
for (std::size_t node = 0U; node < 4U; ++node) {
if (!IsFinite(constraints[node]))
return Failure<NonlinearMitc4InternalEquilibrium>(
"invalid-nonlinear-shell-equilibrium", element.source_location_,
element.identity_,
"Drilling constraint derivatives must remain finite.");
equilibrium.drilling_constraint[node] = constraints[node].value;
equilibrium.drilling_constraint_hessian[node] =
Matrix{kGlobalDofCount, kGlobalDofCount};
for (std::size_t i = 0U; i < kGlobalDofCount; ++i) {
equilibrium.drilling_constraint_gradient(node, i) =
constraints[node].first[i];
for (std::size_t j = 0U; j < kGlobalDofCount; ++j)
equilibrium.drilling_constraint_hessian[node](i, j) =
constraints[node].second[i * kGlobalDofCount + j];
}
}
}
equilibrium.physical_force = Vector{kGlobalDofCount};
equilibrium.drilling_force = Vector{kGlobalDofCount};
equilibrium.drilling_tangent = Matrix{kGlobalDofCount, kGlobalDofCount};
equilibrium.drilling_metric = std::move(drilling_metric);
equilibrium.drilling_energy = drilling.value;
equilibrium.force_directional_second = Vector{kGlobalDofCount};
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
equilibrium.internal_force[index] = equilibrium.internal_residual[index];
equilibrium.physical_force[index] = physical.first[index];
equilibrium.drilling_force[index] = drilling.first[index];
equilibrium.force_directional_second[index] =
internal_energy.mixed_second[index];
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
equilibrium.drilling_tangent(index, column) =
drilling.second[index * kGlobalDofCount + column];
}
}
return Result<NonlinearMitc4InternalEquilibrium>::Success(
std::move(equilibrium));
File diff suppressed because it is too large Load Diff
+8
View File
@@ -6,8 +6,14 @@ add_executable(
unit/analysis/analysis_test.cpp
unit/analysis/analysis_model_test.cpp
unit/analysis/analysis_state_test.cpp
unit/analysis/dynamic_analysis_state_test.cpp
unit/analysis/eigen_reduction_test.cpp
unit/analysis/eigen_mass_assembly_test.cpp
unit/analysis/hht_time_integration_test.cpp
unit/analysis/hht_energy_verification_test.cpp
unit/analysis/hht_temporal_verification_test.cpp
unit/analysis/hht_dynamic_algebra_test.cpp
unit/analysis/hht_dynamic_procedure_test.cpp
unit/analysis/nonlinear_analysis_state_test.cpp
unit/analysis/nonlinear_static_control_test.cpp
unit/analysis/newton_iteration_log_test.cpp
@@ -28,6 +34,7 @@ add_executable(
unit/elements/mitc4_shell_test.cpp
unit/elements/mitc4_shell_mass_contract_test.cpp
unit/elements/nonlinear_mitc4_shell_test.cpp
unit/elements/nonlinear_mitc4_inertia_test.cpp
unit/elements/nonlinear_mitc4_shell_tangent_test.cpp
unit/fem/dof_manager_test.cpp
unit/math/dense_blas_internal_test.cpp
@@ -43,6 +50,7 @@ add_executable(
unit/io/abaqus/input_syntax_test.cpp
unit/io/abaqus/shell_surface_pressure_domain_test.cpp
unit/io/hdf5/hdf5_results_writer_test.cpp
unit/io/hdf5/dynamic_hdf5_results_writer_test.cpp
unit/io/hdf5/eigen_hdf5_results_writer_test.cpp
unit/io/hdf5/nonlinear_hdf5_results_writer_test.cpp
unit/io/hdf5/hdf5_writer_components_test.cpp
+302
View File
@@ -0,0 +1,302 @@
"""HHT reference identity, time, projection and common-tolerance regressions."""
import csv
import json
import math
from pathlib import Path
import shutil
import subprocess
import sys
import pytest
h5py = pytest.importorskip("h5py", reason="Run with uv --with h5py for HHT comparison tests.")
np = pytest.importorskip("numpy")
from scripts.verification import compare_hht_dynamic as comparator
@pytest.fixture(scope="module")
def artifacts(tmp_path_factory):
directory = tmp_path_factory.mktemp("hht-comparison")
input_path = comparator.REFERENCE_DIRECTORY / f"{comparator.CASE}.inp"
reference_path = comparator.REFERENCE_DIRECTORY / f"{comparator.CASE} displacements.csv"
nodes, content_identity = comparator.input_inventory(input_path)
reference = comparator.read_reference(reference_path)
result_path = directory / "matching_results.h5"
string_type = h5py.string_dtype("utf-8")
node_type = np.dtype([("internal_node_id", "<u8"), ("instance_name", string_type),
("source_label", string_type), ("coordinates", "<f8", (3,))])
node_rows = np.empty(729, dtype=node_type)
labels = list(nodes)
for index, label in enumerate(labels):
node_rows[index] = (index, "PART-1-1", str(label), nodes[label])
frame_type = np.dtype([("frame_index", "<u8"), ("increment_index", "<u8"),
("time", "<f8"), ("iteration_count", "<u8"), ("is_initial", "u1")])
frames = np.empty(101, dtype=frame_type)
with h5py.File(result_path, "w") as file:
metadata = file.create_group("metadata")
metadata.attrs.update({
"feature_id": "hht-implicit-dynamics", "analysis_procedure": "hht-implicit-dynamics",
"mass_formulation": "consistent-kinetic", "internal_formulation": "FESA-MITC4-total-lagrangian",
"initial_state": "zero-q-v-step-load-0+",
"acceleration_convention": "physical-endpoint-and-auxiliary-separated",
"source_step_name": "Step-1", "step_name": "Step-1", "unit_system_label": "consistent-input-units",
"solver_version": "test-fixture", "source_input_identity": f"path={input_path};content_identity={content_identity}",
"schema_version": np.uint64(1), "increment_count": np.uint64(100), "maximum_increments": np.uint64(1000),
"alpha": -0.05, "beta": 0.275625, "gamma": 0.55, "initial_increment": 0.1, "time_period": 10.0,
})
file.create_dataset("model/nodes", data=node_rows)
for increment in range(101):
time = float(increment) * 0.1
frames[increment] = (increment, increment, time, 0 if increment == 0 else 2, int(increment == 0))
values = np.zeros((729, 6), dtype="<f8")
for label in (2, 21, 22, 69, 70, 165, 309, 442, 586):
values[labels.index(label)] = reference[(increment, label)]
dataset = file.create_dataset(f"steps/Step-1/frames/{increment}/nodal/displacement", data=values)
dataset.attrs.update({
"component_names": "UX,UY,UZ,URX,URY,URZ", "component_unit_dimensions": "length,length,length,1,1,1",
"coordinate_system": "global-cartesian", "location": "nodal", "step_name": "Step-1", "source_step_name": "Step-1",
"rotation_representation": "total-rotation-vector-chart",
"quantity_semantics": "global-translation-and-total-rotation-vector",
"schema_version": np.uint64(1), "frame_index": np.uint64(increment), "increment_index": np.uint64(increment),
"time": time, "is_initial": np.uint8(increment == 0),
})
file.create_dataset("steps/Step-1/accepted_frames", data=frames)
return input_path, reference_path, result_path
def changed_result(tmp_path, artifacts):
target = tmp_path / "changed.h5"
shutil.copyfile(artifacts[2], target)
return target
def test_original_csv_exact_matching_history_and_deterministic_report(artifacts):
first = comparator.compare(*artifacts)
second = comparator.compare(*artifacts)
assert first == second
assert first["precheck"]["passed"], first["precheck"]
assert first["precheck"]["reference_row_count"] == 909
assert first["precheck"]["checked_result_scalars"] == 441774
assert first["verdict"] == "passed"
assert first["warnings"] == []
assert len(first["families"]) == 202
assert sum(family["row_count"] for family in first["families"]) == 5454
assert [family["quantity"] for family in first["families"][:4]] == ["U", "UR", "U", "UR"]
assert all(family["passed"] for family in first["families"])
assert first["families"][0]["reference_scale"] == 0.0
assert first["families"][6]["hdf5_time"] == 3 * 0.1
assert first["families"][6]["reference_decimal_time"] == "0.3"
assert 3 * 0.1 != float("0.3") # Both legitimate representations were validated independently.
json.dumps(first, allow_nan=False)
@pytest.mark.parametrize("mutation,code", [
("procedure", "wrong-hdf5-attribute-value"),
("mass", "wrong-hdf5-attribute-value"),
("coefficient", "wrong-hdf5-attribute-value"),
("initial_convention", "wrong-hdf5-attribute-value"),
("input_content", "wrong-input-content-identity"),
("component", "wrong-hdf5-attribute-value"),
("rotation", "wrong-hdf5-attribute-value"),
("missing_component_attribute", "missing-hdf5-attribute"),
("nonprojected_nan", "nonfinite-result-value"),
("rotation_inf", "nonfinite-result-value"),
("initial_motion", "nonzero-initial-displacement"),
("duplicate_node", "duplicate-result-node"),
("extra_node_label", "extra-result-node"),
("wrong_instance", "wrong-result-instance"),
("coordinate", "wrong-result-coordinate"),
("wrong_shape", "wrong-hdf5-dataset-layout"),
("float32", "wrong-displacement-type"),
("missing_frame", "result-frame-inventory"),
("extra_frame", "result-frame-inventory"),
("leading_zero_frame", "result-frame-inventory"),
("duplicate_increment", "wrong-result-frame-time"),
("decimal_binary_time", "wrong-result-frame-time"),
("frame_attribute_time", "wrong-hdf5-attribute-value"),
("trial_iteration", "wrong-result-frame-time"),
("initial_iteration", "wrong-result-frame-time"),
])
def test_hdf5_precheck_rejects_before_tolerance(tmp_path, artifacts, mutation, code):
path = changed_result(tmp_path, artifacts)
with h5py.File(path, "r+") as file:
metadata = file["metadata"]
displacement = file["steps/Step-1/frames/3/nodal/displacement"]
nodes = file["model/nodes"]
table = file["steps/Step-1/accepted_frames"]
if mutation in {"procedure", "mass", "coefficient", "initial_convention", "input_content"}:
key, value = {
"procedure": ("analysis_procedure", "linear-static"),
"mass": ("mass_formulation", "lumped"),
"coefficient": ("alpha", 0.0),
"initial_convention": ("initial_state", "unloaded"),
"input_content": ("source_input_identity", "wrong-content"),
}[mutation]
metadata.attrs.modify(key, value)
elif mutation in {"component", "rotation", "frame_attribute_time"}:
key, value = {"component": ("component_names", "U1,U2,U3,UR1,UR2,UR3"),
"rotation": ("rotation_representation", "incremental-rotation"),
"frame_attribute_time": ("time", 0.3)}[mutation]
displacement.attrs.modify(key, value)
elif mutation == "missing_component_attribute":
del displacement.attrs["component_names"]
elif mutation in {"nonprojected_nan", "rotation_inf"}:
displacement[0, 0 if mutation == "nonprojected_nan" else 3] = float("nan") if mutation == "nonprojected_nan" else float("inf")
elif mutation == "initial_motion":
file["steps/Step-1/frames/0/nodal/displacement"][0, 0] = 1.0e-100
elif mutation in {"duplicate_node", "extra_node_label", "wrong_instance", "coordinate"}:
rows = nodes[:]
if mutation == "duplicate_node":
rows[1]["source_label"] = rows[0]["source_label"]
elif mutation == "extra_node_label":
rows[0]["source_label"] = "99999"
elif mutation == "wrong_instance":
rows[0]["instance_name"] = "Other"
else:
rows[0]["coordinates"] = [1.0, 2.0, 3.0]
nodes[:] = rows
elif mutation in {"wrong_shape", "float32"}:
name = displacement.name
values = displacement[:]
del file[name]
file.create_dataset(name, data=values[:-1] if mutation == "wrong_shape" else values.astype("<f4"))
elif mutation == "missing_frame":
del file["steps/Step-1/frames/100"]
elif mutation == "extra_frame":
file.create_group("steps/Step-1/frames/101")
elif mutation == "leading_zero_frame":
file.move("steps/Step-1/frames/1", "steps/Step-1/frames/01")
else:
rows = table[:]
if mutation == "duplicate_increment":
rows[3]["increment_index"] = 2
elif mutation == "decimal_binary_time":
rows[3]["time"] = 0.3
elif mutation == "trial_iteration":
rows[3]["iteration_count"] = 101
elif mutation == "initial_iteration":
rows[0]["iteration_count"] = 1
table[:] = rows
report = comparator.compare(artifacts[0], artifacts[1], path)
assert report["precheck"]["passed"] is False
assert report["precheck"]["code"] == code, report["precheck"]
assert report["families"] == []
assert report["verdict"] == "failed"
@pytest.mark.parametrize("mutation,code", [
("duplicate_header", "duplicate-reference-column"),
("missing_header", "missing-reference-column"),
("duplicate_row", "duplicate-reference-row"),
("missing_row", "reference-row-inventory"),
("extra_node", "reference-row-inventory"),
("extra_frame", "reference-row-inventory"),
("time", "wrong-reference-time"),
("instance", "wrong-reference-instance"),
("label", "invalid-node-label"),
("nonfinite_rotation", "nonfinite-reference-value"),
])
def test_reference_precheck_rejects_before_tolerance(tmp_path, artifacts, mutation, code):
with artifacts[1].open(newline="", encoding="utf-8") as stream:
rows = list(csv.reader(stream))
if mutation == "duplicate_header":
rows[0][-1] = rows[0][-2]
elif mutation == "missing_header":
rows[0][-1] = "Not a required component"
elif mutation == "duplicate_row":
rows.append(rows[1])
elif mutation == "missing_row":
rows.pop()
elif mutation in {"extra_node", "extra_frame"}:
row = rows[1].copy()
if mutation == "extra_node":
row[2] = "1"
else:
row[0] = "Increment 101: Step Time = 10.1"
rows.append(row)
elif mutation == "time":
rows[1][0] = "Increment 0: Step Time = 0.0000000001"
elif mutation == "instance":
rows[1][1] = "WRONG-INSTANCE"
elif mutation == "label":
rows[1][2] = "2.0"
else:
rows[1][-1] = "nan"
reference = tmp_path / "changed.csv"
with reference.open("w", newline="", encoding="utf-8") as stream:
csv.writer(stream).writerows(rows)
report = comparator.compare(artifacts[0], reference, artifacts[2])
assert not report["precheck"]["passed"]
assert report["precheck"]["code"] == code, report["precheck"]
assert report["families"] == []
def test_projection_joins_source_identity_and_csv_order_is_irrelevant(tmp_path, artifacts):
path = changed_result(tmp_path, artifacts)
with h5py.File(path, "r+") as file:
nodes = file["model/nodes"][:][::-1]
nodes["internal_node_id"] = np.arange(729, dtype="<u8")
file["model/nodes"][:] = nodes
for frame in range(101):
values = file[f"steps/Step-1/frames/{frame}/nodal/displacement"]
values[:] = values[:][::-1]
with artifacts[1].open(newline="", encoding="utf-8") as stream:
rows = list(csv.reader(stream))
reference = tmp_path / "reordered.csv"
with reference.open("w", newline="", encoding="utf-8") as stream:
csv.writer(stream).writerows([rows[0], *reversed(rows[1:])])
report = comparator.compare(artifacts[0], reference, path)
assert report["precheck"]["passed"], report["precheck"]
assert report["verdict"] == "passed"
@pytest.mark.parametrize("component,passed", [(0, False), (3, True)])
def test_translation_blocks_rotation_warns(tmp_path, artifacts, component, passed):
path = changed_result(tmp_path, artifacts)
with h5py.File(path, "r+") as file:
values = file["steps/Step-1/frames/1/nodal/displacement"]
rows = values[:]
rows[:, component:component + 3] *= 2.0
values[:] = rows
report = comparator.compare(artifacts[0], artifacts[1], path)
assert report["precheck"]["passed"]
assert report["blocking_passed"] is passed
assert len(report["warnings"]) == (2 if passed else 0)
if passed:
assert [warning["code"] for warning in report["warnings"]] == ["row-tolerance-failure", "relative-rms-failure"]
else:
assert report["blocking_failures"] == ["U-global:Step-1:increment-1"]
def test_common_policy_boundaries_and_zero_scale():
near = comparator.evaluate_family(1, "U", [100.0, 2.0, 1.0], [100.0, 1.0, 0.0])
assert near["passed"]
assert [row["branch"] for row in near["rows"]] == ["relative", "near-zero", "near-zero"]
relative = comparator.evaluate_family(1, "U", [105.0], [100.0])
assert relative["rows_passed"] and not relative["rms_passed"]
boundary = comparator.evaluate_family(1, "U", [101.0], [100.0])
assert boundary["passed"]
assert boundary["relative_rms"] == 0.01
outside = comparator.evaluate_family(1, "U", [math.nextafter(101.0, math.inf)], [100.0])
assert outside["rows_passed"] and not outside["rms_passed"]
zero = comparator.evaluate_family(0, "U", [0.0], [0.0])
assert zero["passed"] and zero["relative_rms"] is None
nonzero = comparator.evaluate_family(0, "U", [1.0e-300], [0.0])
assert not nonzero["passed"]
assert nonzero["diagnostic_code"] == "zero-reference-scale-nonzero-error"
json.dumps(nonzero, allow_nan=False)
with pytest.raises(comparator.ComparisonError, match="U"):
comparator.evaluate_family(1, "U", [1.0e308], [-1.0e308])
def test_cli_writes_json_and_exit_two_for_missing_results(tmp_path, artifacts):
report = tmp_path / "evidence.json"
completed = subprocess.run([sys.executable, "-B", str(Path(comparator.__file__)),
"--input", str(artifacts[0]), "--reference", str(artifacts[1]),
"--results", str(tmp_path / "absent.h5"), "--report", str(report)],
check=False, capture_output=True, text=True)
assert completed.returncode == 2, completed.stderr
evidence = json.loads(report.read_text())
assert not evidence["precheck"]["passed"]
assert evidence["families"] == []
@@ -0,0 +1,305 @@
#include "fesa/analysis/dynamic_analysis_state.h"
#include <gtest/gtest.h>
#include <array>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <functional>
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
#include <utility>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/fem/dof_manager.h"
namespace {
constexpr std::array<std::array<double, 3>, 3> kIdentityTriad{
std::array<double, 3>{1.0, 0.0, 0.0}, std::array<double, 3>{0.0, 1.0, 0.0},
std::array<double, 3>{0.0, 0.0, 1.0}};
fesa::ModelDefinition MakeDefinition(const bool fixed = true) {
const std::filesystem::path source{"dynamic-state.inp"};
fesa::ModelDefinition definition{};
definition.source_path = source;
definition.source_content_identity = "hht-output-unit-fixture";
definition.nodes = {{{"Shell-A", 10, "0010"}, {0.0, 0.0, 0.0}, {source, 1U}},
{{"Shell-A", 20, "20"}, {1.0, 0.0, 0.0}, {source, 2U}},
{{"Shell-A", 30, "30"}, {1.0, 1.0, 0.0}, {source, 3U}},
{{"Shell-A", 40, "40"}, {0.0, 1.0, 0.0}, {source, 4U}}};
definition.materials = {{"Material", 1000.0, 0.25, {source, 5U}}};
definition.densities = {{0U, 2.0, {source, 6U}}};
definition.shell_sections = {{"Shell", 0.1, 0U, {source, 7U}}};
definition.shell_elements = {{{"Shell-A", 9, "0009"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
0U,
0U,
{source, 8U}}};
for (std::size_t node = 0U; node < 4U; ++node) {
definition.shell_node_initial_frames.push_back(
{static_cast<fesa::EntityIndex>(node),
{0.0, 0.0, 1.0},
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0}});
}
definition.node_sets = {{"All", std::nullopt, {0U, 1U, 2U, 3U}, {source, 9U}},
{"Loaded", std::nullopt, {0U}, {source, 10U}}};
fesa::DynamicStepDefinition step{};
step.source_step_name_raw = "Transient Load";
step.source_step_name_canonical = "TRANSIENT LOAD";
step.initial_increment = 0.25;
step.time_period = 0.5;
step.increment_count = 2U;
step.maximum_increments = 3U;
if (fixed) {
step.boundaries = {{"All", 1, 6, 0.0, {source, 11U}}};
step.loads = {{"Loaded", 3, 4.0, {source, 12U}}};
}
definition.dynamic_step = step;
return definition;
}
struct Fixture {
std::unique_ptr<fesa::Domain> domain;
fesa::DynamicAnalysisState state;
};
Fixture MakeFixture(fesa::ModelDefinition definition = MakeDefinition()) {
auto domain_result = fesa::Domain::Create(std::move(definition));
if (!domain_result.HasValue())
throw std::runtime_error{"Invalid fixture Domain."};
auto domain =
std::make_unique<fesa::Domain>(std::move(domain_result.Value()));
auto model = fesa::AnalysisModel::Create(*domain);
if (!model.HasValue())
throw std::runtime_error{"Invalid fixture AnalysisModel."};
auto dofs = fesa::DofManager::Create(model.Value());
if (!dofs.HasValue()) throw std::runtime_error{"Invalid fixture DofManager."};
auto state = fesa::DynamicAnalysisState::Create(*domain, dofs.Value());
if (!state.HasValue()) throw std::runtime_error{"Invalid fixture state."};
return {std::move(domain), std::move(state.Value())};
}
// A fully restrained, initially undeformed shell under a dead nodal force has
// zero motion and strain, and nonzero support force at every accepted endpoint.
fesa::DynamicAcceptedFrameState MakeFrame(const fesa::Domain& domain,
const std::uint32_t index) {
fesa::DynamicAcceptedFrameState frame{};
frame.frame_index = index;
frame.increment_index = index;
frame.time = static_cast<double>(index) * domain.Dynamic()->initial_increment;
frame.is_initial = index == 0U;
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
frame.nodal_internal_order.push_back(static_cast<fesa::EntityIndex>(node));
frame.nodal_source_ids.push_back(domain.Nodes()[node].source_id);
frame.nodal_triad.push_back(kIdentityTriad);
frame.nodal_director.push_back({0.0, 0.0, 1.0});
}
for (auto* vector :
{&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
&frame.algorithmic_acceleration, &frame.inertia_force,
&frame.internal_force, &frame.external_force, &frame.residual,
&frame.reaction, &frame.residual_scale}) {
*vector = fesa::Vector{24U};
}
frame.angular_velocity = fesa::Vector{12U};
frame.angular_acceleration = fesa::Vector{12U};
if (!domain.Dynamic()->loads.empty()) {
frame.external_force[2U] = 4.0;
frame.residual[2U] = -4.0;
frame.reaction[2U] = -4.0;
frame.residual_scale[2U] = 4.0;
}
const double gauss = 1.0 / std::sqrt(3.0);
const std::array<std::array<double, 2>, 4> natural{
std::array<double, 2>{-gauss, -gauss},
{gauss, -gauss},
{gauss, gauss},
{-gauss, gauss}};
const std::array<fesa::ShellMidsurfaceLocation, 4> locations{
fesa::ShellMidsurfaceLocation::kGp1, fesa::ShellMidsurfaceLocation::kGp2,
fesa::ShellMidsurfaceLocation::kGp3, fesa::ShellMidsurfaceLocation::kGp4};
for (std::size_t point = 0U; point < 4U; ++point) {
frame.shell_results.push_back(
{0U,
locations[point],
natural[point],
kIdentityTriad,
{},
{},
{{{fesa::ShellSectionPosition::kBottom, -1.0, {}},
{fesa::ShellSectionPosition::kMiddle, 0.0, {}},
{fesa::ShellSectionPosition::kTop, 1.0, {}}}}});
}
return frame;
}
TEST(DynamicAnalysisState, DefaultStateRejectsTrialAndCommit) {
fesa::DynamicAnalysisState state;
EXPECT_FALSE(state.IsInitialized());
EXPECT_TRUE(state.AcceptedFrames().empty());
EXPECT_FALSE(state.BeginTrialFromCommitted().HasValue());
EXPECT_THROW((void)state.CommittedState(), std::logic_error);
const auto status = state.CommitAcceptedFrame({});
EXPECT_FALSE(status.IsOk());
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, "dynamic-invalid-state");
}
TEST(DynamicAnalysisState,
CommitsCompleteZeroCorrectionHistoryAndCopiesTrials) {
auto fixture = MakeFixture();
EXPECT_TRUE(fixture.state.IsInitialized());
EXPECT_FALSE(fixture.state.BeginTrialFromCommitted().HasValue());
for (std::uint32_t index = 0U; index <= 2U; ++index) {
auto frame = MakeFrame(*fixture.domain, index);
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(frame).IsOk());
frame.reaction[2U] = 123.0;
EXPECT_DOUBLE_EQ(fixture.state.CommittedState().reaction[2U], -4.0);
EXPECT_EQ(fixture.state.AcceptedFrames().size(), index + 1U);
}
auto trial = fixture.state.BeginTrialFromCommitted();
ASSERT_TRUE(trial.HasValue());
trial.Value().generalized_coordinates[0U] = 1.0;
trial.Value().shell_results[0U].stress[0U].components[0U] = 1.0;
fixture.state.DiscardTrial();
EXPECT_DOUBLE_EQ(fixture.state.CommittedState().generalized_coordinates[0U],
0.0);
EXPECT_DOUBLE_EQ(fixture.state.CommittedState()
.shell_results[0U]
.stress[0U]
.components[0U],
0.0);
EXPECT_DOUBLE_EQ(fixture.state.CommittedState().time, 0.5);
EXPECT_FALSE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 3U)).IsOk());
EXPECT_EQ(fixture.state.AcceptedFrames().size(), 3U);
}
TEST(DynamicAnalysisState,
RejectsMalformedInventoryScheduleAndPreservesEndpoint) {
auto fixture = MakeFixture();
ASSERT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 0U)).IsOk());
using Frame = fesa::DynamicAcceptedFrameState;
const std::vector<std::function<void(Frame&)>> mutations{
[](Frame& frame) { frame.frame_index = 2U; },
[](Frame& frame) { frame.increment_index = 2U; },
[](Frame& frame) { frame.time = 0.26; },
[](Frame& frame) {
frame.time = std::numeric_limits<double>::quiet_NaN();
},
[](Frame& frame) { frame.is_initial = true; },
[](Frame& frame) { frame.iteration_count = 101U; },
[](Frame& frame) { frame.nodal_internal_order[1U] = 0U; },
[](Frame& frame) {
frame.nodal_source_ids[1U].source_label_text = "wrong";
},
[](Frame& frame) { frame.nodal_triad.pop_back(); },
[](Frame& frame) {
frame.nodal_director[0U][2U] = std::numeric_limits<double>::infinity();
},
[](Frame& frame) { frame.acceleration = fesa::Vector{23U}; },
[](Frame& frame) { frame.velocity[0U] = 1.0; },
[](Frame& frame) { frame.acceleration[0U] = 1.0; },
[](Frame& frame) { frame.algorithmic_acceleration[0U] = 1.0; },
[](Frame& frame) { frame.generalized_coordinates[0U] = 1.0; },
[](Frame& frame) { frame.reaction[2U] = 0.0; },
[](Frame& frame) { frame.residual[2U] = 0.0; },
[](Frame& frame) { frame.residual_scale[0U] = -1.0; },
[](Frame& frame) {
frame.external_force[0U] = 1.0;
frame.residual[0U] = -1.0;
frame.reaction[0U] = -1.0;
},
[](Frame& frame) { frame.shell_results.pop_back(); },
[](Frame& frame) { frame.shell_results[0U].element = 1U; },
[](Frame& frame) {
frame.shell_results[0U].natural_coordinates[0U] = 0.0;
},
[](Frame& frame) { frame.shell_results[0U].stress[0U].zeta = 0.0; },
[](Frame& frame) {
frame.shell_results[0U].stress[0U].components[0U] =
std::numeric_limits<double>::quiet_NaN();
},
[](Frame& frame) {
frame.kinetic_energy = std::numeric_limits<double>::infinity();
},
[](Frame& frame) {
frame.angular_momentum_rate[0U] =
std::numeric_limits<double>::quiet_NaN();
},
[](Frame& frame) { frame.hidden_constraint = fesa::Vector{1U}; },
[](Frame& frame) { frame.free_residual_metrics[0U] = -1.0; }};
for (std::size_t index = 0U; index < mutations.size(); ++index) {
SCOPED_TRACE(index);
auto frame = MakeFrame(*fixture.domain, 1U);
mutations[index](frame);
const auto status = fixture.state.CommitAcceptedFrame(frame);
ASSERT_FALSE(status.IsOk());
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, "dynamic-invalid-state");
ASSERT_EQ(fixture.state.AcceptedFrames().size(), 1U);
EXPECT_DOUBLE_EQ(fixture.state.CommittedState().time, 0.0);
EXPECT_DOUBLE_EQ(fixture.state.CommittedState().reaction[2U], -4.0);
}
EXPECT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 1U)).IsOk());
}
TEST(DynamicAnalysisState, RejectsInvalidInitialStateAndFreeReaction) {
auto fixture = MakeFixture(MakeDefinition(false));
auto initial = MakeFrame(*fixture.domain, 0U);
initial.generalized_coordinates[0U] = 1.0;
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(initial).IsOk());
initial = MakeFrame(*fixture.domain, 0U);
initial.acceleration[0U] = 1.0;
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(initial).IsOk());
initial = MakeFrame(*fixture.domain, 0U);
initial.time = -0.0;
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(initial).IsOk());
initial = MakeFrame(*fixture.domain, 0U);
initial.reaction[0U] = 1.0;
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(initial).IsOk());
EXPECT_TRUE(fixture.state.AcceptedFrames().empty());
EXPECT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 0U)).IsOk());
}
TEST(DynamicAnalysisState, CreateRejectsInvalidScheduleAndNonzeroBoundary) {
const std::vector<std::function<void(fesa::ModelDefinition&)>> mutations{
[](auto& definition) { definition.dynamic_step->increment_count = 0U; },
[](auto& definition) { definition.dynamic_step->time_period = 0.6; },
[](auto& definition) {
definition.dynamic_step->maximum_increments = 1U;
},
[](auto& definition) {
definition.dynamic_step->initial_increment =
std::numeric_limits<double>::quiet_NaN();
},
[](auto& definition) {
definition.dynamic_step->boundaries[0U].value = 1.0;
}};
for (std::size_t index = 0U; index < mutations.size(); ++index) {
SCOPED_TRACE(index);
auto definition = MakeDefinition();
mutations[index](definition);
auto domain = fesa::Domain::Create(std::move(definition));
ASSERT_TRUE(domain.HasValue());
auto model = fesa::AnalysisModel::Create(domain.Value());
ASSERT_TRUE(model.HasValue());
auto dofs = fesa::DofManager::Create(model.Value());
ASSERT_TRUE(dofs.HasValue());
EXPECT_FALSE(
fesa::DynamicAnalysisState::Create(domain.Value(), dofs.Value())
.HasValue());
}
}
} // namespace
@@ -0,0 +1,330 @@
#include <gtest/gtest.h>
#include <array>
#include <cmath>
#include <cstddef>
#include <memory>
#include <optional>
#include <stdexcept>
#include <utility>
#include <vector>
#include "analysis/hht_dynamic_internal.h"
#include "fesa/analysis/analysis_model.h"
#include "fesa/math/matrix.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
namespace {
struct Fixture {
std::unique_ptr<fesa::Domain> domain;
fesa::DofManager dofs;
};
Fixture MakeFixture(const std::size_t free_nodes = 1U,
const bool partial_rotation = false,
const bool translations_free = true) {
fesa::ModelDefinition definition{};
definition.source_path = "hht-algebra.inp";
definition.nodes = {
{{"Plate", 1, "1"}, {-1, -1, 0}, {definition.source_path, 1U}},
{{"Plate", 2, "2"}, {1, -1, 0}, {definition.source_path, 2U}},
{{"Plate", 3, "3"}, {1, 1, 0}, {definition.source_path, 3U}},
{{"Plate", 4, "4"}, {-1, 1, 0}, {definition.source_path, 4U}}};
definition.materials = {{"Elastic", 1000, 0.3, {definition.source_path, 5U}}};
definition.densities = {{0U, 1.0, {definition.source_path, 6U}}};
definition.shell_sections = {
{"Plate", 0.2, 0U, {definition.source_path, 7U}}};
definition.shell_elements = {{{"Plate", 1, "1"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
0U,
0U,
{definition.source_path, 8U}}};
fesa::DynamicStepDefinition step{};
step.source_step_name_raw = "Dynamic";
step.source_step_name_canonical = "DYNAMIC";
step.initial_increment = 0.1;
step.time_period = 0.1;
step.increment_count = 1U;
step.maximum_increments = 1U;
for (std::size_t i = 0; i < 4; ++i) {
definition.shell_node_initial_frames.push_back(
{static_cast<fesa::EntityIndex>(i), {0, 0, 1}, {1, 0, 0}, {0, 1, 0}});
const std::string name = "Node" + std::to_string(i);
definition.node_sets.push_back({name,
std::nullopt,
{static_cast<fesa::EntityIndex>(i)},
{definition.source_path, 9U}});
if (i >= free_nodes) {
step.boundaries.push_back(
{name, 1, 6, 0.0, {definition.source_path, 10U}});
} else {
if (partial_rotation)
step.boundaries.push_back(
{name, 4, 5, 0.0, {definition.source_path, 10U}});
if (!translations_free)
step.boundaries.push_back(
{name, 1, 3, 0.0, {definition.source_path, 10U}});
}
}
definition.dynamic_step = step;
auto result = fesa::Domain::Create(std::move(definition));
if (!result.HasValue()) throw std::runtime_error{"Invalid algebra domain."};
auto domain = std::make_unique<fesa::Domain>(std::move(result.Value()));
auto model = fesa::AnalysisModel::Create(*domain);
if (!model.HasValue()) throw std::runtime_error{"Invalid algebra model."};
auto dofs = fesa::DofManager::Create(model.Value());
if (!dofs.HasValue()) throw std::runtime_error{"Invalid algebra DOFs."};
return {std::move(domain), std::move(dofs.Value())};
}
fesa::SparseMatrix Sparse(const fesa::Matrix& matrix) {
std::vector<fesa::CooContribution> entries;
for (std::size_t i = 0; i < matrix.Rows(); ++i) {
for (std::size_t j = 0; j < matrix.Columns(); ++j) {
entries.push_back({i, j, matrix(i, j), 0U, entries.size()});
}
}
auto result = fesa::hht_internal::MakeSparse(matrix.Rows(), matrix.Columns(),
std::move(entries));
EXPECT_TRUE(result.HasValue());
return std::move(result.Value());
}
fesa::hht_internal::DynamicAssembly Assembly(
const double second_stiffness = 3.0, const double mass_scale = 1.0) {
fesa::DynamicAcceptedFrameState frame;
frame.is_initial = true;
for (auto* vector :
{&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
&frame.algorithmic_acceleration, &frame.inertia_force,
&frame.internal_force, &frame.external_force, &frame.residual,
&frame.residual_scale})
*vector = fesa::Vector{24U};
frame.external_force[0] = 2.0;
frame.residual_scale[0] = 2.0;
fesa::Matrix mass{24U, 24U};
fesa::Matrix stiffness{24U, 24U};
fesa::Matrix metric{24U, 24U};
std::vector<fesa::DynamicRotationKinematics> rotations;
for (std::size_t i = 0; i < 4; ++i) {
const std::size_t base = 6U * i;
for (std::size_t j = 0; j < 5; ++j)
mass(base + j, base + j) = mass_scale * (j < 3U ? 2.0 : 1.0);
const double k = i == 1U ? second_stiffness : 3.0;
stiffness(base, base) = 8.0 + k * 0.7 * 0.7;
stiffness(base, base + 5U) = -0.7 * k;
stiffness(base + 5U, base) = -0.7 * k;
stiffness(base + 5U, base + 5U) = k;
metric(base + 5U, base + 5U) = 3.0;
auto rotation = fesa::EvaluateDynamicRotation({}, {0, 0, 1}, {}, {});
EXPECT_TRUE(rotation.HasValue());
rotations.push_back(std::move(rotation.Value()));
}
return {std::move(frame),
Sparse(mass),
Sparse(stiffness),
Sparse(stiffness),
Sparse(metric),
fesa::Vector{24U},
fesa::Vector{24U},
std::move(rotations),
0.0,
0.0,
fesa::Vector{4U},
fesa::Vector{4U}};
}
TEST(HhtDynamicAlgebra,
InitializesCoupledMasslessAccelerationFromHiddenEquation) {
const auto fixture = MakeFixture();
auto assembly = Assembly();
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
const auto acceleration = fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, assembly, 2.0, positive, tangent);
ASSERT_TRUE(acceleration.HasValue());
EXPECT_NEAR(acceleration.Value()[0], 1.0, 1.0e-12);
EXPECT_NEAR(acceleration.Value()[5], 0.7, 1.0e-12);
for (const auto full : fixture.dofs.ConstrainedDofs())
EXPECT_EQ(acceleration.Value()[full], 0.0);
const auto nodes =
fesa::hht_internal::NullNodes(fixture.dofs, assembly.rotations);
ASSERT_EQ(nodes, std::vector<std::size_t>{0U});
assembly.frame.acceleration = acceleration.Value();
auto frame = assembly.frame;
EXPECT_TRUE(fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly, 2.0, nodes, positive,
tangent, &frame)
.IsOk());
ASSERT_EQ(frame.hidden_acceleration_defect.Size(), 1U);
EXPECT_NEAR(frame.hidden_acceleration_defect[0], 0.0, 1.0e-12);
EXPECT_GT(frame.hidden_acceleration_defect_scale[0], 0.0);
frame.acceleration[5] = 0.0;
const auto pseudoinverse = fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly, 2.0, nodes, positive, tangent,
&frame);
EXPECT_FALSE(pseudoinverse.IsOk());
}
TEST(HhtDynamicAlgebra, ChecksInitialConstraintAndVelocityWithoutRepair) {
const auto fixture = MakeFixture();
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
auto inconsistent = Assembly();
inconsistent.frame.internal_force[5] = 0.3;
inconsistent.frame.residual_scale[5] = 0.3;
EXPECT_FALSE(
fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, inconsistent, 2.0, positive, tangent)
.HasValue());
inconsistent = Assembly();
inconsistent.frame.velocity[5] = 0.1;
EXPECT_FALSE(
fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, inconsistent, 2.0, positive, tangent)
.HasValue());
}
TEST(HhtDynamicAlgebra, KeepsAcceptedHiddenRateDefectsAsDiagnostics) {
const auto fixture = MakeFixture();
auto assembly = Assembly();
assembly.frame.is_initial = false;
assembly.frame.velocity[5] = 0.1;
assembly.frame.acceleration[5] = -0.2;
auto frame = assembly.frame;
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
const auto nodes =
fesa::hht_internal::NullNodes(fixture.dofs, assembly.rotations);
const auto result = fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly, 2.0, nodes, positive, tangent,
&frame);
ASSERT_TRUE(result.IsOk());
EXPECT_NEAR(frame.hidden_velocity_defect[0], 0.3, 1.0e-12);
EXPECT_NEAR(frame.hidden_acceleration_defect[0], -0.6, 1.0e-12);
}
TEST(HhtDynamicAlgebra, RejectsNegativePhysicalMassAndAlgebraicRankLoss) {
const auto fixture = MakeFixture(2U);
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
const auto negative = Assembly(3.0, -1.0);
EXPECT_FALSE(
fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, negative, 2.0, positive, tangent)
.HasValue());
const auto singular = Assembly(0.0);
EXPECT_FALSE(
fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, singular, 2.0, positive, tangent)
.HasValue());
}
TEST(HhtDynamicAlgebra, AppliesTheExactRegularityConditionBoundary) {
const auto fixture = MakeFixture(2U);
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
for (const int exponent : {31, 32, 33}) {
auto assembly = Assembly();
// The boundary oracle is exactly diag(1,c), with W=I. Avoid a rounded
// sqrt(3) congruence when testing a binary-exact inclusive threshold.
fesa::Matrix stiffness{24U, 24U};
fesa::Matrix metric{24U, 24U};
for (std::size_t node = 0; node < 4; ++node) {
const std::size_t drill = 6U * node + 5U;
stiffness(drill, drill) = node == 1U ? std::ldexp(1.0, -exponent) : 1.0;
metric(drill, drill) = 1.0;
}
assembly.internal_tangent = Sparse(stiffness);
assembly.drilling_metric = Sparse(metric);
const auto result = fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, assembly, 2.0, positive, tangent);
EXPECT_EQ(result.HasValue(), exponent <= 32)
<< exponent
<< (result.HasValue() ? ""
: result.GetStatus().Diagnostics()[0].message);
if (exponent == 33) {
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Diagnostics()[0].code,
"dynamic-algebraic-conditioning");
}
}
}
TEST(HhtDynamicAlgebra, AllFixedModelHasAnEmptyValidAccelerationSolve) {
const auto fixture = MakeFixture(0U);
auto assembly = Assembly();
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
const auto result = fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, assembly, 2.0, positive, tangent);
ASSERT_TRUE(result.HasValue());
EXPECT_EQ(result.Value().Norm(), 0.0);
auto frame = assembly.frame;
EXPECT_TRUE(fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly, 2.0, {}, positive,
tangent, &frame)
.IsOk());
EXPECT_EQ(frame.hidden_constraint.Size(), 0U);
}
TEST(HhtDynamicAlgebra, PartialBoundaryRetainsEveryNonzeroPhysicalDirection) {
for (const bool translations_free : {false, true}) {
const auto fixture = MakeFixture(1U, true, translations_free);
for (const double p : {0.0, 1.0 / 2000.0, 1.0e-8}) {
auto assembly = Assembly();
assembly.frame.external_force[0] = 0.0;
fesa::Matrix mass{24U, 24U};
const double nz = std::sqrt(1.0 - p * p);
for (std::size_t node = 0; node < 4; ++node) {
for (std::size_t j = 0; j < 3; ++j)
mass(6U * node + j, 6U * node + j) = 2.0;
mass(6U * node + 3U, 6U * node + 3U) = nz * nz;
mass(6U * node + 4U, 6U * node + 4U) = 1.0;
mass(6U * node + 5U, 6U * node + 5U) = p * p;
mass(6U * node + 3U, 6U * node + 5U) = -p * nz;
mass(6U * node + 5U, 6U * node + 3U) = -p * nz;
assembly.rotations[node].drill_null = {p, 0, nz};
}
assembly.mass = Sparse(mass);
const auto nodes =
fesa::hht_internal::NullNodes(fixture.dofs, assembly.rotations);
EXPECT_EQ(nodes.size(), p == 0.0 ? 1U : 0U);
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
const auto result = fesa::hht_internal::InitializeAcceleration(
*fixture.domain, fixture.dofs, assembly, 2.0, positive, tangent);
const bool expected = !(translations_free && p == 1.0e-8);
EXPECT_EQ(result.HasValue(), expected) << "p=" << p;
if (!expected) {
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Diagnostics()[0].code,
"dynamic-positive-mass-conditioning");
}
}
}
}
TEST(HhtDynamicAlgebra, RejectsAdmissibleNullRankChangeBeforeAcceptingFrame) {
const auto fixture = MakeFixture(1U, true);
auto assembly = Assembly();
const auto initial_nodes =
fesa::hht_internal::NullNodes(fixture.dofs, assembly.rotations);
ASSERT_EQ(initial_nodes.size(), 1U);
assembly.rotations[0].drill_null = {1.0e-8, 0, std::sqrt(1.0 - 1.0e-16)};
auto frame = assembly.frame;
frame.hidden_constraint = fesa::Vector{1U, 23.0};
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
const auto result = fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly, 2.0, initial_nodes, positive,
tangent, &frame);
ASSERT_FALSE(result.IsOk());
EXPECT_EQ(result.Diagnostics()[0].code, "dynamic-mass-null-rank-change");
EXPECT_EQ(frame.hidden_constraint[0], 23.0);
}
} // namespace
@@ -0,0 +1,125 @@
#include "fesa/analysis/hht_dynamic_procedure.h"
#include <gtest/gtest.h>
#include <filesystem>
#include <optional>
#include "fesa/analysis/newton_iteration_log.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
namespace {
fesa::ModelDefinition MakeDefinition(const bool fixed = true) {
const std::filesystem::path source{"dynamic-state.inp"};
fesa::ModelDefinition definition{};
definition.source_path = source;
definition.source_content_identity = "hht-output-unit-fixture";
definition.nodes = {{{"Shell-A", 10, "0010"}, {0.0, 0.0, 0.0}, {source, 1U}},
{{"Shell-A", 20, "20"}, {1.0, 0.0, 0.0}, {source, 2U}},
{{"Shell-A", 30, "30"}, {1.0, 1.0, 0.0}, {source, 3U}},
{{"Shell-A", 40, "40"}, {0.0, 1.0, 0.0}, {source, 4U}}};
definition.materials = {{"Material", 1000.0, 0.25, {source, 5U}}};
definition.densities = {{0U, 2.0, {source, 6U}}};
definition.shell_sections = {{"Shell", 0.1, 0U, {source, 7U}}};
definition.shell_elements = {{{"Shell-A", 9, "0009"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
0U,
0U,
{source, 8U}}};
for (std::size_t node = 0U; node < 4U; ++node) {
definition.shell_node_initial_frames.push_back(
{static_cast<fesa::EntityIndex>(node),
{0.0, 0.0, 1.0},
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0}});
}
definition.node_sets = {{"All", std::nullopt, {0U, 1U, 2U, 3U}, {source, 9U}},
{"Loaded", std::nullopt, {0U}, {source, 10U}}};
fesa::DynamicStepDefinition step{};
step.source_step_name_raw = "Transient Load";
step.source_step_name_canonical = "TRANSIENT LOAD";
step.initial_increment = 0.25;
step.time_period = 0.5;
step.increment_count = 2U;
step.maximum_increments = 3U;
if (fixed) {
step.boundaries = {{"All", 1, 6, 0.0, {source, 11U}}};
step.loads = {{"Loaded", 3, 4.0, {source, 12U}}};
}
definition.dynamic_step = step;
return definition;
}
TEST(HhtDynamicProcedure, ClampedStepLoadHasPhysicalReactionAndZeroMotion) {
auto domain = fesa::Domain::Create(MakeDefinition());
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
fesa::NewtonIterationLogWriter log;
const auto path =
std::filesystem::temp_directory_path() / "fesa-hht-clamped.log";
ASSERT_TRUE(log.Open(path).IsOk());
const fesa::HhtDynamicProcedureExecutor executor(parallel, mass, tangent);
auto result = executor.Execute(domain.Value(), log);
ASSERT_TRUE(log.Close().IsOk());
std::filesystem::remove(path);
ASSERT_TRUE(result.HasValue())
<< (result.HasValue() ? ""
: result.GetStatus().Diagnostics().front().message);
ASSERT_EQ(result.Value().state.AcceptedFrames().size(), 3U);
for (const auto& frame : result.Value().state.AcceptedFrames()) {
EXPECT_EQ(frame.iteration_count, 0U);
EXPECT_DOUBLE_EQ(frame.reaction[2U], -4.0);
EXPECT_DOUBLE_EQ(frame.external_force[2U], 4.0);
EXPECT_DOUBLE_EQ(frame.kinetic_energy, 0.0);
EXPECT_DOUBLE_EQ(frame.energy_defect, 0.0);
for (std::size_t i = 0; i < 24U; ++i) {
EXPECT_DOUBLE_EQ(frame.generalized_coordinates[i], 0.0);
EXPECT_DOUBLE_EQ(frame.acceleration[i], 0.0);
}
}
}
TEST(HhtDynamicProcedure, LargeDirectStepStartsFromAcceptedGeometry) {
auto definition = MakeDefinition(false);
definition.materials[0U].youngs_modulus = 1e9;
definition.dynamic_step->increment_count = 1U;
definition.dynamic_step->time_period =
definition.dynamic_step->initial_increment;
definition.node_sets[0U].node_indices = {0U, 3U};
definition.node_sets[1U].node_indices = {2U};
definition.dynamic_step->boundaries = {
{"All", 1, 6, 0.0, {"dynamic-state.inp", 11U}}};
definition.dynamic_step->loads = {
{"Loaded", 3, -40000.0, {"dynamic-state.inp", 12U}}};
auto domain = fesa::Domain::Create(std::move(definition));
ASSERT_TRUE(domain.HasValue());
fesa::SerialParallelFor parallel;
fesa::MklPardisoSolver mass;
fesa::MklPardisoTangentSolver tangent;
fesa::NewtonIterationLogWriter log;
const auto path =
std::filesystem::temp_directory_path() / "fesa-hht-large-direct.log";
ASSERT_TRUE(log.Open(path).IsOk());
auto result = fesa::HhtDynamicProcedureExecutor(parallel, mass, tangent)
.Execute(domain.Value(), log);
ASSERT_TRUE(log.Close().IsOk());
ASSERT_TRUE(result.HasValue())
<< (result.HasValue() ? ""
: result.GetStatus().Diagnostics().front().message);
EXPECT_GT(
std::abs(result.Value().state.AcceptedFrames()[0U].acceleration[14U]),
1e5);
EXPECT_LT(result.Value().state.CommittedState().generalized_coordinates[14U],
0.0);
for (double metric :
result.Value().state.CommittedState().free_residual_metrics)
EXPECT_LE(metric, 1e-10);
EXPECT_GT(result.Value().state.CommittedState().iteration_count, 0U);
std::filesystem::remove(path);
}
} // namespace
@@ -0,0 +1,223 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <complex>
#include <cstddef>
#include <iostream>
#include <stdexcept>
#include "fesa/analysis/hht_time_integration.h"
namespace {
struct Oscillator {
fesa::Vector q{1U};
fesa::Vector v{1U};
fesa::Vector a{1U};
fesa::Vector auxiliary{1U};
};
void Advance(Oscillator* state, const double mass, const double stiffness,
const double force, const double h,
const fesa::HhtParameters& parameters) {
const auto predictor =
fesa::PredictHht(state->q, state->v, state->auxiliary, h, parameters);
if (!predictor.HasValue())
throw std::runtime_error{"Invalid oscillator predictor."};
fesa::Vector q{1U};
q[0] =
(mass * predictor.Value().coordinates[0] +
parameters.beta * h * h *
((1.0 + parameters.alpha) * force -
mass * parameters.alpha * state->a[0])) /
(mass + (1.0 + parameters.alpha) * stiffness * parameters.beta * h * h);
const auto rates =
fesa::EvaluateHhtRates(q, predictor.Value(), state->a, h, parameters);
if (!rates.HasValue()) throw std::runtime_error{"Invalid oscillator rates."};
state->q = q;
state->v = rates.Value().velocity;
state->a = rates.Value().acceleration;
state->auxiliary = rates.Value().algorithmic_acceleration;
}
using Matrix3 = std::array<std::array<double, 3>, 3>;
Matrix3 Amplification(const double frequency_step, const double alpha) {
// Independent dimensionless recurrence in (q,h*v,h^2*A), with m=1.
const double beta = (1.0 - alpha) * (1.0 - alpha) / 4.0;
const double gamma = 0.5 - alpha;
const double stiffness = frequency_step * frequency_step;
Matrix3 matrix{};
for (std::size_t column = 0; column < 3; ++column) {
const double q = column == 0U ? 1.0 : 0.0;
const double velocity = column == 1U ? 1.0 : 0.0;
const double auxiliary = column == 2U ? 1.0 : 0.0;
const double predictor = q + velocity + (0.5 - beta) * auxiliary;
const double next_auxiliary =
(-(1.0 + alpha) * stiffness * predictor + alpha * stiffness * q) /
(1.0 + (1.0 + alpha) * stiffness * beta);
matrix[0][column] = predictor + beta * next_auxiliary;
matrix[1][column] =
velocity + (1.0 - gamma) * auxiliary + gamma * next_auxiliary;
matrix[2][column] = next_auxiliary;
}
return matrix;
}
std::array<std::complex<double>, 3> Eigenvalues(const Matrix3& matrix) {
const double trace = matrix[0][0] + matrix[1][1] + matrix[2][2];
const double second =
matrix[0][0] * matrix[1][1] + matrix[0][0] * matrix[2][2] +
matrix[1][1] * matrix[2][2] - matrix[0][1] * matrix[1][0] -
matrix[0][2] * matrix[2][0] - matrix[1][2] * matrix[2][1];
const double determinant =
matrix[0][0] *
(matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1]) -
matrix[0][1] *
(matrix[1][0] * matrix[2][2] - matrix[1][2] * matrix[2][0]) +
matrix[0][2] *
(matrix[1][0] * matrix[2][1] - matrix[1][1] * matrix[2][0]);
if (determinant == 0.0) {
const auto discriminant =
std::sqrt(std::complex<double>{trace * trace - 4.0 * second, 0.0});
return {std::complex<double>{0.0, 0.0}, 0.5 * (trace + discriminant),
0.5 * (trace - discriminant)};
}
std::array<std::complex<double>, 3> roots{
std::complex<double>{1.1, 0.0}, {-0.6, 0.9}, {-0.6, -0.9}};
bool converged = false;
for (std::size_t iteration = 0; iteration < 500U; ++iteration) {
const auto previous = roots;
double change = 0.0;
for (std::size_t i = 0; i < 3; ++i) {
const auto value =
((previous[i] - trace) * previous[i] + second) * previous[i] -
determinant;
const auto denominator = (previous[i] - previous[(i + 1U) % 3U]) *
(previous[i] - previous[(i + 2U) % 3U]);
roots[i] = previous[i] - value / denominator;
change = std::max(change, std::abs(roots[i] - previous[i]));
}
if (change <= 1.0e-13) {
converged = true;
break;
}
}
EXPECT_TRUE(converged);
for (const auto root : roots) {
const double numerator =
std::abs(((root - trace) * root + second) * root - determinant);
const double scale =
std::pow(std::abs(root), 3) + std::abs(trace) * std::norm(root) +
std::abs(second) * std::abs(root) + std::abs(determinant);
EXPECT_LE(scale > 0.0 ? numerator / scale : numerator, 1.0e-12);
}
return roots;
}
TEST(HhtEnergyVerification, C07AlphaZeroConservesLinearOscillatorEnergy) {
const auto parameters = fesa::MakeHhtParameters(0.0);
ASSERT_TRUE(parameters.HasValue());
Oscillator state;
state.q[0] = 1.0;
state.a[0] = -4.0;
state.auxiliary = state.a;
double maximum = 0.0;
for (std::size_t step = 0; step < 640U; ++step) {
Advance(&state, 1.0, 4.0, 0.0, 1.0 / 64.0, parameters.Value());
const double energy =
0.5 * state.v[0] * state.v[0] + 2.0 * state.q[0] * state.q[0];
maximum = std::max(maximum, std::abs(energy - 2.0) / 2.0);
}
std::cout << "C07 alpha0 max energy defect=" << maximum << '\n';
EXPECT_LE(maximum, 1.0e-10);
}
TEST(HhtEnergyVerification, C07DefaultAlphaReportsEnergyAndPhaseChanges) {
const auto parameters = fesa::MakeHhtParameters(-0.05);
ASSERT_TRUE(parameters.HasValue());
Oscillator state;
state.q[0] = 1.0;
state.a[0] = -4.0;
state.auxiliary = state.a;
double maximum = 0.0;
for (std::size_t step = 0; step < 640U; ++step) {
Advance(&state, 1.0, 4.0, 0.0, 1.0 / 64.0, parameters.Value());
const double energy =
0.5 * state.v[0] * state.v[0] + 2.0 * state.q[0] * state.q[0];
maximum = std::max(maximum, std::abs(energy - 2.0) / 2.0);
}
const double energy =
0.5 * state.v[0] * state.v[0] + 2.0 * state.q[0] * state.q[0];
std::cout << "C07 default alpha max energy defect=" << maximum
<< " signed endpoint=" << (energy - 2.0) / 2.0
<< " displacement error=" << state.q[0] - std::cos(20.0)
<< " phase error="
<< std::remainder(std::atan2(-state.v[0] / 2.0, state.q[0]) - 20.0,
2.0 * std::acos(-1.0))
<< '\n';
EXPECT_TRUE(std::isfinite(energy));
EXPECT_LT(energy, 2.0);
}
TEST(HhtEnergyVerification,
C07IndependentAmplificationHasThePrescribedSpectrum) {
for (const double alpha : {0.0, -0.05}) {
double previous_limit_error = 1.0;
for (const double frequency_step : {0.01, 0.1, 1.0, 10.0, 100.0, 1000.0}) {
auto roots = Eigenvalues(Amplification(frequency_step, alpha));
std::sort(roots.begin(), roots.end(), [](const auto& a, const auto& b) {
return std::abs(a) < std::abs(b);
});
const double radius = std::abs(roots.back());
std::cout << "C07 alpha=" << alpha << " omega*h=" << frequency_step
<< " spectral radius=" << radius << '\n';
if (alpha == 0.0) {
EXPECT_NEAR(std::abs(roots[1]), 1.0, 1.0e-10);
EXPECT_NEAR(std::abs(roots[2]), 1.0, 1.0e-10);
} else {
EXPECT_LE(radius, 1.0 + 1.0e-10);
const double limit = (1.0 + alpha) / (1.0 - alpha);
if (frequency_step >= 100.0) {
const double error = std::abs(radius - limit);
EXPECT_LT(error, previous_limit_error);
previous_limit_error = error;
std::cout << "C07 high-frequency limit=" << limit
<< " error=" << error << '\n';
}
}
}
}
}
TEST(HhtEnergyVerification,
C07SupportReactionUsesPhysicalAccelerationAndCoupling) {
const auto parameters = fesa::MakeHhtParameters(-0.05);
ASSERT_TRUE(parameters.HasValue());
Oscillator state;
state.a[0] = 1.0;
state.auxiliary = state.a;
double mutation_difference = 0.0;
for (std::size_t step = 1; step <= 64U; ++step) {
Advance(&state, 2.0, 8.0, 2.0, 1.0 / 64.0, parameters.Value());
const std::array<double, 2> displacement{state.q[0], 0.0};
const std::array<double, 2> acceleration{state.a[0], 0.0};
const std::array<std::array<double, 2>, 2> mass{{{2.0, 0.5}, {0.5, 1.0}}};
const double reaction = mass[1][0] * acceleration[0] +
mass[1][1] * acceleration[1] +
3.0 * displacement[1];
EXPECT_EQ(reaction, 0.5 * state.a[0]);
mutation_difference = std::max(
mutation_difference, std::abs(reaction - 0.5 * state.auxiliary[0]));
const double momentum_rate = 2.5 * state.a[0];
const double balance = 2.0 + reaction - 8.0 * state.q[0] - momentum_rate;
const double scale = 2.0 + std::abs(reaction) + std::abs(8.0 * state.q[0]) +
std::abs(momentum_rate);
EXPECT_LE(std::abs(balance) / scale, 1.0e-10);
}
EXPECT_GT(mutation_difference, 1.0e-6);
}
} // namespace
@@ -0,0 +1,507 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <vector>
#include "fesa/analysis/hht_time_integration.h"
namespace {
enum class Case { kC01, kC03, kC04a, kC04b };
using Dense = std::array<std::array<double, 2>, 2>;
struct ExactState {
fesa::Vector q;
fesa::Vector v;
fesa::Vector a;
};
std::size_t Dimension(const Case model) {
return model == Case::kC01 || model == Case::kC03 ? 1U : 2U;
}
ExactState Exact(const Case model, const double time) {
const std::size_t size = Dimension(model);
ExactState result{fesa::Vector{size}, fesa::Vector{size}, fesa::Vector{size}};
if (model == Case::kC01) {
result.q[0] = std::cos(2.0 * time) + 0.15 * std::sin(2.0 * time);
result.v[0] = -2.0 * std::sin(2.0 * time) + 0.3 * std::cos(2.0 * time);
result.a[0] = -4.0 * result.q[0];
} else if (model == Case::kC03) {
result.q[0] = 0.4 + std::sin(time);
result.v[0] = std::cos(time);
result.a[0] = -std::sin(time);
} else if (model == Case::kC04a) {
result.q[0] = 0.25 * (1.0 - std::cos(2.0 * time));
result.v[0] = 0.5 * std::sin(2.0 * time);
result.a[0] = std::cos(2.0 * time);
result.q[1] = 0.7 * result.q[0];
result.v[1] = 0.7 * result.v[0];
result.a[1] = 0.7 * result.a[0];
} else {
result.q[0] = std::cos(time);
result.q[1] = std::sin(time);
result.v[0] = -std::sin(time);
result.v[1] = std::cos(time);
result.a[0] = -result.q[0];
result.a[1] = -result.q[1];
}
return result;
}
std::array<double, 3> Amplitudes(const Case model) {
if (model == Case::kC01) {
const double q = std::sqrt(1.0 + 0.15 * 0.15);
return {q, 2.0 * q, 4.0 * q};
}
if (model == Case::kC04a) {
const double direction = std::sqrt(1.0 + 0.7 * 0.7);
return {0.5 * direction, 0.5 * direction, direction};
}
return {1.0, 1.0, 1.0};
}
struct Equation {
Dense mass{};
Dense dq{};
Dense dv{};
fesa::Vector residual;
fesa::Vector scale;
};
Equation Evaluate(const Case model, const fesa::Vector& q,
const fesa::Vector& v, const fesa::Vector& a,
const double time) {
const std::size_t size = q.Size();
Equation result{{}, {}, {}, fesa::Vector{size}, fesa::Vector{size}};
fesa::Vector c{size};
fesa::Vector internal{size};
fesa::Vector force{size};
if (model == Case::kC01) {
result.mass[0][0] = 1.0;
result.dq[0][0] = 4.0;
internal[0] = 4.0 * q[0];
} else if (model == Case::kC03) {
result.mass[0][0] = 1.0 + q[0] * q[0];
c[0] = q[0] * v[0] * v[0];
const auto exact = Exact(model, time);
force[0] = (1.0 + exact.q[0] * exact.q[0]) * exact.a[0] +
exact.q[0] * exact.v[0] * exact.v[0];
result.dq[0][0] = 2.0 * q[0] * a[0] + v[0] * v[0];
result.dv[0][0] = 2.0 * q[0] * v[0];
} else if (model == Case::kC04a) {
result.mass[0][0] = 2.0;
result.dq = {{{8.0 + 3.0 * 0.7 * 0.7, -3.0 * 0.7}, {-3.0 * 0.7, 3.0}}};
internal[0] = result.dq[0][0] * q[0] + result.dq[0][1] * q[1];
internal[1] = result.dq[1][0] * q[0] + result.dq[1][1] * q[1];
force[0] = 2.0;
} else {
const double radius = q.Norm();
if (!(radius > 0.0)) throw std::runtime_error{"Invalid circle chart."};
const double r2 = radius * radius;
const double r4 = r2 * r2;
const double r6 = r4 * r2;
const double s = q.Dot(v);
std::array<double, 2> mv{};
for (std::size_t i = 0; i < 2; ++i) {
for (std::size_t j = 0; j < 2; ++j) {
result.mass[i][j] = (i == j ? 1.0 / r2 : 0.0) - q[i] * q[j] / r4;
mv[i] += result.mass[i][j] * v[j];
}
c[i] = -2.0 * s / r2 * mv[i];
internal[i] = (1.0 - 1.0 / radius) * q[i];
}
for (std::size_t column = 0; column < 2; ++column) {
for (std::size_t row = 0; row < 2; ++row) {
double dma = 0.0;
double dmv = 0.0;
for (std::size_t j = 0; j < 2; ++j) {
const double derivative =
(row == j ? -2.0 * q[column] / r4 : 0.0) +
4.0 * q[column] * q[row] * q[j] / r6 -
((row == column ? q[j] : 0.0) + (j == column ? q[row] : 0.0)) /
r4;
dma += derivative * a[j];
dmv += derivative * v[j];
}
result.dq[row][column] =
dma + (-2.0 * v[column] / r2 + 4.0 * s * q[column] / r4) * mv[row] -
2.0 * s / r2 * dmv + (row == column ? 1.0 - 1.0 / radius : 0.0) +
q[row] * q[column] / (r2 * radius);
result.dv[row][column] = -2.0 * q[column] / r2 * mv[row] -
2.0 * s / r2 * result.mass[row][column];
}
}
}
for (std::size_t i = 0; i < size; ++i) {
result.residual[i] = c[i] + internal[i] - force[i];
result.scale[i] =
std::abs(c[i]) + std::abs(internal[i]) + std::abs(force[i]);
for (std::size_t j = 0; j < size; ++j) {
result.residual[i] += result.mass[i][j] * a[j];
result.scale[i] += std::abs(result.mass[i][j] * a[j]);
}
}
return result;
}
fesa::Vector Multiply(const Dense& matrix, const fesa::Vector& x) {
fesa::Vector result{x.Size()};
for (std::size_t i = 0; i < x.Size(); ++i) {
for (std::size_t j = 0; j < x.Size(); ++j) result[i] += matrix[i][j] * x[j];
}
return result;
}
fesa::Vector Solve(const Dense& matrix, const fesa::Vector& rhs) {
fesa::Vector result{rhs.Size()};
if (rhs.Size() == 1U) {
if (matrix[0][0] == 0.0)
throw std::runtime_error{"Singular scalar tangent."};
result[0] = rhs[0] / matrix[0][0];
} else {
Dense lu = matrix;
std::array<double, 2> b{rhs[0], rhs[1]};
if (std::abs(lu[1][0]) > std::abs(lu[0][0])) {
std::swap(lu[0], lu[1]);
std::swap(b[0], b[1]);
}
if (lu[0][0] == 0.0) throw std::runtime_error{"Singular Cartesian pivot."};
const double multiplier = lu[1][0] / lu[0][0];
lu[1][1] -= multiplier * lu[0][1];
b[1] -= multiplier * b[0];
if (lu[1][1] == 0.0) throw std::runtime_error{"Singular Cartesian pivot."};
result[1] = b[1] / lu[1][1];
result[0] = (b[0] - lu[0][1] * result[1]) / lu[0][0];
}
double residual_norm = 0.0;
double scale_norm = 0.0;
for (std::size_t i = 0; i < rhs.Size(); ++i) {
double residual = -rhs[i];
double scale = std::abs(rhs[i]);
for (std::size_t j = 0; j < rhs.Size(); ++j) {
residual += matrix[i][j] * result[j];
scale += std::abs(matrix[i][j] * result[j]);
}
EXPECT_TRUE(std::isfinite(result[i]));
residual_norm = std::max(residual_norm, std::abs(residual));
scale_norm = std::max(scale_norm, scale);
}
EXPECT_LE(scale_norm > 0.0 ? residual_norm / scale_norm : residual_norm,
1.0e-12);
return result;
}
Dense Effective(const Equation& equation, const double cv, const double ca) {
Dense tangent{};
for (std::size_t i = 0; i < 2; ++i) {
for (std::size_t j = 0; j < 2; ++j) {
tangent[i][j] =
equation.dq[i][j] + cv * equation.dv[i][j] + ca * equation.mass[i][j];
}
}
return tangent;
}
struct Errors {
std::array<double, 3> maximum{};
std::array<double, 3> first{};
std::array<double, 3> tail{};
std::array<double, 3> endpoint{};
std::array<double, 3> noise{};
double position_constraint = 0.0;
double hidden_velocity = 0.0;
double hidden_acceleration = 0.0;
double auxiliary_difference = 0.0;
double constant_mass_recurrence_difference = 0.0;
double reduced_difference = 0.0;
};
Errors Integrate(const Case model, const std::size_t increments,
const bool naive_variable_mass = false) {
const double h = 1.0 / static_cast<double>(increments);
const auto parameter_result = fesa::MakeHhtParameters(-0.05);
if (!parameter_result.HasValue())
throw std::runtime_error{"Invalid HHT parameters."};
const auto parameters = parameter_result.Value();
const double cv = parameters.gamma / (parameters.beta * h);
const double ca = 1.0 / ((1.0 + parameters.alpha) * parameters.beta * h * h);
ExactState state = Exact(model, 0.0);
fesa::Vector auxiliary = state.a;
const auto amplitudes = Amplitudes(model);
Errors errors;
double old_naive_b = 0.0;
double old_naive_scale = 2.0 * std::abs(state.q[0] * state.v[0] * state.v[0]);
if (naive_variable_mass)
old_naive_b = -(1.0 + state.q[0] * state.q[0]) * state.a[0];
// Independent scalar constant-M HHT recurrence, used for C01 and the exact
// algebraic reduction of C04a; it does not call the production rate helpers.
double rq = state.q[0];
double rv = state.v[0];
double ra = state.a[0];
for (std::size_t step = 1; step <= increments; ++step) {
const double time = static_cast<double>(step) * h;
const auto predictor_result =
fesa::PredictHht(state.q, state.v, auxiliary, h, parameters);
if (!predictor_result.HasValue())
throw std::runtime_error{"Invalid predictor."};
const auto predictor = predictor_result.Value();
fesa::Vector q = predictor.coordinates;
q.Axpy(parameters.beta * h * h, auxiliary);
fesa::Vector last_delta{q.Size()};
fesa::Vector last_residual{q.Size()};
Dense last_tangent{};
std::size_t corrections = 0U;
for (;;) {
const auto rates =
fesa::EvaluateHhtRates(q, predictor, state.a, h, parameters);
if (!rates.HasValue()) throw std::runtime_error{"Invalid trial rates."};
auto equation = Evaluate(model, q, rates.Value().velocity,
rates.Value().acceleration, time);
Dense tangent = Effective(equation, cv, ca);
if (naive_variable_mass) {
const auto exact = Exact(model, time);
const double force = (1.0 + exact.q[0] * exact.q[0]) * exact.a[0] +
exact.q[0] * exact.v[0] * exact.v[0];
const double v = rates.Value().velocity[0];
const double a = rates.Value().algorithmic_acceleration[0];
const double b = q[0] * v * v - force;
equation.residual[0] = (1.0 + q[0] * q[0]) * a +
(1.0 + parameters.alpha) * b -
parameters.alpha * old_naive_b;
equation.scale[0] = std::abs((1.0 + q[0] * q[0]) * a) +
(1.0 + parameters.alpha) *
(std::abs(q[0] * v * v) + std::abs(force)) +
std::abs(parameters.alpha) * old_naive_scale;
tangent[0][0] =
2.0 * q[0] * a + (1.0 + q[0] * q[0]) / (parameters.beta * h * h) +
(1.0 + parameters.alpha) * (v * v + 2.0 * q[0] * v * cv);
}
double residual = 0.0;
double scale = 0.0;
double correction = 0.0;
for (std::size_t i = 0; i < q.Size(); ++i) {
residual = std::max(residual, std::abs(equation.residual[i]));
scale = std::max(scale, equation.scale[i]);
correction = std::max(correction, std::abs(last_delta[i]));
}
const bool force_pass =
scale > 0.0 ? residual / scale <= 1.0e-10 : residual == 0.0;
const bool correction_pass = correction <= 1.0e-10 &&
h * cv * correction <= 1.0e-10 &&
h * h * ca * correction <= 1.0e-10 &&
correction / parameters.beta <= 1.0e-10;
if (force_pass && correction_pass) {
if (corrections == 0U) {
last_tangent = tangent;
last_residual = equation.residual;
}
break;
}
if (corrections >= 100U)
throw std::runtime_error{"Newton correction limit."};
auto rhs = equation.residual;
rhs.Scale(-1.0);
last_delta = Solve(tangent, rhs);
last_tangent = tangent;
last_residual = equation.residual;
q.Axpy(1.0, last_delta);
++corrections;
}
const auto rates_result =
fesa::EvaluateHhtRates(q, predictor, state.a, h, parameters);
if (!rates_result.HasValue())
throw std::runtime_error{"Invalid accepted rates."};
const auto rates = rates_result.Value();
const auto endpoint_equation =
Evaluate(model, q, rates.velocity, rates.acceleration, time);
const Dense endpoint_tangent = Effective(endpoint_equation, cv, ca);
if (!naive_variable_mass) {
auto linear_defect = Multiply(last_tangent, last_delta);
linear_defect.Axpy(1.0, last_residual);
linear_defect.Scale(-1.0);
const auto z_linear = Solve(last_tangent, linear_defect);
auto endpoint_rhs = endpoint_equation.residual;
endpoint_rhs.Scale(-1.0);
const auto z_nonlinear = Solve(endpoint_tangent, endpoint_rhs);
const double eta_q =
last_delta.Norm() + z_linear.Norm() + z_nonlinear.Norm();
const double unit_roundoff = std::ldexp(1.0, -53);
const double gamma32 =
32.0 * unit_roundoff / (1.0 - 32.0 * unit_roundoff);
const double eta_round =
gamma32 * (q.Norm() + predictor.coordinates.Norm());
const std::array<double, 3> factors{1.0, cv, ca};
for (std::size_t channel = 0; channel < 3; ++channel) {
errors.noise[channel] =
std::max(errors.noise[channel], factors[channel] *
std::max(eta_q, eta_round) /
amplitudes[channel]);
}
}
const auto exact = Exact(model, time);
const std::array<const fesa::Vector*, 3> actual{&q, &rates.velocity,
&rates.acceleration};
const std::array<const fesa::Vector*, 3> reference{&exact.q, &exact.v,
&exact.a};
for (std::size_t channel = 0; channel < 3; ++channel) {
auto difference = *actual[channel];
difference.Axpy(-1.0, *reference[channel]);
const double error = difference.Norm() / amplitudes[channel];
errors.maximum[channel] = std::max(errors.maximum[channel], error);
if (step == 1U) errors.first[channel] = error;
if (time >= 0.25)
errors.tail[channel] = std::max(errors.tail[channel], error);
if (step == increments) errors.endpoint[channel] = error;
}
auto auxiliary_difference = rates.algorithmic_acceleration;
auxiliary_difference.Axpy(-1.0, rates.acceleration);
errors.auxiliary_difference =
std::max(errors.auxiliary_difference, auxiliary_difference.Norm());
if (model == Case::kC04b) {
errors.position_constraint =
std::max(errors.position_constraint, std::abs(q.Norm() - 1.0));
errors.hidden_velocity =
std::max(errors.hidden_velocity, std::abs(q.Dot(rates.velocity)));
errors.hidden_acceleration =
std::max(errors.hidden_acceleration,
std::abs(q.Dot(rates.acceleration) +
rates.velocity.Dot(rates.velocity)));
}
if (model == Case::kC01 || model == Case::kC04a) {
const double m = model == Case::kC01 ? 1.0 : 2.0;
const double k = model == Case::kC01 ? 4.0 : 8.0;
const double force = model == Case::kC01 ? 0.0 : 2.0;
const double qp = rq + h * rv + h * h * (0.5 - parameters.beta) * ra;
const double next_a =
((1.0 + parameters.alpha) * (force - k * qp) -
parameters.alpha * (force - k * rq)) /
(m + (1.0 + parameters.alpha) * k * parameters.beta * h * h);
const double next_q = qp + parameters.beta * h * h * next_a;
const double next_v =
rv + h * ((1.0 - parameters.gamma) * ra + parameters.gamma * next_a);
errors.constant_mass_recurrence_difference = std::max(
errors.constant_mass_recurrence_difference,
std::max(std::abs(q[0] - next_q) / amplitudes[0],
std::abs(rates.velocity[0] - next_v) / amplitudes[1]));
if (model == Case::kC04a) {
errors.reduced_difference = std::max(
errors.reduced_difference,
std::max({std::abs(q[1] - 0.7 * q[0]),
std::abs(rates.velocity[1] - 0.7 * rates.velocity[0]),
std::abs(rates.acceleration[1] -
0.7 * rates.acceleration[0])}));
}
rq = next_q;
rv = next_v;
ra = next_a;
}
if (naive_variable_mass) {
const double force = (1.0 + exact.q[0] * exact.q[0]) * exact.a[0] +
exact.q[0] * exact.v[0] * exact.v[0];
old_naive_b = q[0] * rates.velocity[0] * rates.velocity[0] - force;
old_naive_scale = std::abs(q[0] * rates.velocity[0] * rates.velocity[0]) +
std::abs(force);
}
state = {q, rates.velocity, rates.acceleration};
auxiliary = rates.algorithmic_acceleration;
}
return errors;
}
std::vector<Errors> Sweep(const Case model) {
std::vector<Errors> errors;
for (const std::size_t increments : {16U, 32U, 64U, 128U, 256U}) {
errors.push_back(Integrate(model, increments));
const auto& current = errors.back();
for (std::size_t channel = 0; channel < 3; ++channel) {
std::cout << "N7 model=" << static_cast<int>(model)
<< " increments=" << increments << " channel=" << channel
<< " maximum=" << current.maximum[channel]
<< " first=" << current.first[channel]
<< " tail=" << current.tail[channel]
<< " endpoint=" << current.endpoint[channel]
<< " noise=" << current.noise[channel] << '\n';
EXPECT_GT(current.maximum[channel], 100.0 * current.noise[channel])
<< "N7 inconclusive: iteration/roundoff dominates channel "
<< channel;
}
std::cout << "N7 hidden v=" << current.hidden_velocity
<< " a=" << current.hidden_acceleration
<< " A-a=" << current.auxiliary_difference << '\n';
}
for (std::size_t channel = 0; channel < 3; ++channel) {
for (std::size_t grid = 3; grid < 5; ++grid) {
EXPECT_LT(errors[grid].maximum[channel],
errors[grid - 1U].maximum[channel]);
EXPECT_GE(std::log2(errors[grid - 1U].maximum[channel] /
errors[grid].maximum[channel]),
1.8);
}
EXPECT_LE(errors.back().maximum[channel], 1.0e-4);
}
return errors;
}
TEST(HhtTemporalVerification, C01MatchesConstantMassRecurrenceAndSecondOrder) {
const auto errors = Sweep(Case::kC01);
for (const auto& error : errors)
EXPECT_LE(error.constant_mass_recurrence_difference, 1.0e-12);
}
TEST(HhtTemporalVerification,
C03VariableMassIsSecondOrderAndNaiveWeightingIsNot) {
const auto errors = Sweep(Case::kC03);
EXPECT_GT(errors.back().auxiliary_difference, 0.0);
const auto coarse = Integrate(Case::kC03, 128U, true);
const auto fine = Integrate(Case::kC03, 256U, true);
const double order = std::log2(coarse.maximum[0] / fine.maximum[0]);
std::cout << "C03 naive q-order=" << order << '\n';
EXPECT_LT(order, 1.5);
}
TEST(HhtTemporalVerification, C04aCoupledMasslessCoordinatesHaveSecondOrder) {
const auto errors = Sweep(Case::kC04a);
for (std::size_t grid = 0; grid < errors.size(); ++grid) {
EXPECT_LE(errors[grid].constant_mass_recurrence_difference, 1.0e-12);
if (grid + 1U < errors.size())
EXPECT_LE(errors[grid].reduced_difference, 1.0e-10);
}
}
TEST(HhtTemporalVerification,
C04aFinestReductionComparisonIsNumericallyResolved) {
const auto error = Integrate(Case::kC04a, 256U);
if (error.reduced_difference > 1.0e-10 &&
error.reduced_difference <= error.noise[2]) {
GTEST_SKIP() << "N7 INCONCLUSIVE: h=1/256 full/reduced acceleration defect="
<< error.reduced_difference
<< ", arithmetic/solve resolution=" << error.noise[2]
<< "; coarse grids remain blocking. Independent decimal80 "
"full2x2/reduced"
" recurrence evidence is recorded separately; this "
"binary64 comparison is not passed.";
}
EXPECT_LE(error.reduced_difference, 1.0e-10);
}
TEST(HhtTemporalVerification, C04bFullCartesianMovingNullConverges) {
const auto errors = Sweep(Case::kC04b);
for (const auto& error : errors)
EXPECT_LE(error.position_constraint, 1.0e-10);
for (std::size_t grid = 3; grid < 5; ++grid) {
EXPECT_LT(errors[grid].hidden_velocity, errors[grid - 1U].hidden_velocity);
EXPECT_LT(errors[grid].hidden_acceleration,
errors[grid - 1U].hidden_acceleration);
}
EXPECT_LE(errors.back().hidden_velocity, 1.0e-6);
EXPECT_LE(errors.back().hidden_acceleration, 1.0e-4);
}
} // namespace
@@ -0,0 +1,97 @@
#include "fesa/analysis/hht_time_integration.h"
#include <cmath>
#include <gtest/gtest.h>
namespace fesa {
namespace {
TEST(HhtTimeIntegrationTest, KeepsPhysicalAndAlgorithmicAccelerationDistinct) {
const auto parameters = MakeHhtParameters(-0.05);
ASSERT_TRUE(parameters.HasValue());
Vector q(1U);
Vector v(1U);
Vector old_a(1U);
old_a[0U] = 1.0;
const auto predictor = PredictHht(q, v, old_a, 0.1, parameters.Value());
ASSERT_TRUE(predictor.HasValue());
q[0U] = predictor.Value().coordinates[0U] +
parameters.Value().beta * 0.01 * 2.0;
const auto rates = EvaluateHhtRates(q, predictor.Value(), old_a, 0.1,
parameters.Value());
ASSERT_TRUE(rates.HasValue());
EXPECT_NEAR(rates.Value().algorithmic_acceleration[0U], 2.0, 1e-14);
EXPECT_NEAR(rates.Value().acceleration[0U], 1.95 / 0.95, 1e-14);
EXPECT_NEAR(rates.Value().velocity[0U], 0.155, 1e-14);
}
TEST(HhtTimeIntegrationTest, RejectsInvalidParametersAndTime) {
EXPECT_FALSE(MakeHhtParameters(0.01).HasValue());
EXPECT_FALSE(MakeHhtParameters(-0.34).HasValue());
ASSERT_TRUE(MakeHhtParameters(-1.0 / 3.0).HasValue());
const auto parameters = MakeHhtParameters(0.0);
ASSERT_TRUE(parameters.HasValue());
EXPECT_EQ(parameters.Value().beta, 0.25);
EXPECT_EQ(parameters.Value().gamma, 0.5);
const Vector zero(1U);
EXPECT_FALSE(PredictHht(zero, zero, zero, 0.0, parameters.Value()).HasValue());
}
// C02 is a right-continuous STEP load with a nonzero physical a0.
TEST(HhtTimeIntegrationTest, StepOscillatorHasSecondOrderMaximumError) {
const auto parameters = MakeHhtParameters(-0.05);
ASSERT_TRUE(parameters.HasValue());
double previous_q = 0.0;
double previous_v = 0.0;
double previous_a = 0.0;
for (unsigned int power = 4U; power <= 8U; ++power) {
const unsigned int count = 1U << power;
const double h = 1.0 / static_cast<double>(count);
Vector q(1U);
Vector v(1U);
Vector a(1U);
Vector auxiliary(1U);
a[0U] = auxiliary[0U] = 1.0;
double error_q = 0.0;
double error_v = 0.0;
double error_a = 0.0;
for (unsigned int n = 1U; n <= count; ++n) {
const auto predictor = PredictHht(q, v, auxiliary, h, parameters.Value());
ASSERT_TRUE(predictor.HasValue());
const double coefficient =
1.0 / ((1.0 + parameters.Value().alpha) *
parameters.Value().beta * h * h);
q[0U] = (2.0 + 2.0 * coefficient * predictor.Value().coordinates[0U] -
2.0 * parameters.Value().alpha * a[0U] /
(1.0 + parameters.Value().alpha)) /
(8.0 + 2.0 * coefficient);
const auto rates = EvaluateHhtRates(q, predictor.Value(), a, h,
parameters.Value());
ASSERT_TRUE(rates.HasValue());
v = rates.Value().velocity;
a = rates.Value().acceleration;
auxiliary = rates.Value().algorithmic_acceleration;
const double time = static_cast<double>(n) * h;
error_q = (std::max)(error_q, std::abs(q[0U] - 0.25 * (1.0 - std::cos(2.0 * time))) / 0.5);
error_v = (std::max)(error_v, std::abs(v[0U] - 0.5 * std::sin(2.0 * time)) / 0.5);
error_a = (std::max)(error_a, std::abs(a[0U] - std::cos(2.0 * time)));
}
if (power >= 7U) {
EXPECT_GE(std::log2(previous_q / error_q), 1.8);
EXPECT_GE(std::log2(previous_v / error_v), 1.8);
EXPECT_GE(std::log2(previous_a / error_a), 1.8);
}
if (power == 8U) {
EXPECT_LT(error_q, 1e-4);
EXPECT_LT(error_v, 1e-4);
EXPECT_LT(error_a, 1e-4);
}
previous_q = error_q;
previous_v = error_v;
previous_a = error_a;
}
}
} // namespace
} // namespace fesa
@@ -6,6 +6,8 @@
#include <cmath>
#include <filesystem>
#include <fstream>
#include <functional>
#include <limits>
#include <iterator>
#include <stdexcept>
#include <string>
@@ -412,4 +414,152 @@ TEST(NewtonIterationLog,
"residual_moment_threshold_unit=1");
}
fesa::DynamicNewtonIterationLogRecord CompleteDynamicRecord() {
return {2U,
3U,
0.25,
"convergence",
"accepted",
"",
{2.0e-12, 0.0},
{2.0, 0.0},
{1.0e-11, 2.0e-11},
{3.0e-11, 4.0e-11},
{5.0e-11, 6.0e-11},
{7.0e-11, 8.0e-11},
true,
true};
}
std::string ReadLogBytes(const std::filesystem::path& path) {
std::ifstream input{path, std::ios::binary};
return std::string{std::istreambuf_iterator<char>{input},
std::istreambuf_iterator<char>{}};
}
TEST(DynamicNewtonIterationLog, WritesTimeAndAllTwoChannelMetrics) {
TempDirectory directory{"dynamic-complete"};
const auto path = directory.Path() / "dynamic_log.txt";
fesa::NewtonIterationLogWriter writer;
const auto record = CompleteDynamicRecord();
ASSERT_TRUE(writer.Open(path).IsOk());
ASSERT_TRUE(writer.Write(record).IsOk());
ASSERT_TRUE(writer.Flush().IsOk());
ASSERT_TRUE(writer.Close().IsOk());
const std::string bytes = ReadLogBytes(path);
ASSERT_FALSE(bytes.empty());
EXPECT_EQ(bytes.back(), '\n');
EXPECT_EQ(std::count(bytes.begin(), bytes.end(), '\n'), 1);
EXPECT_EQ(bytes.find('\r'), std::string::npos);
EXPECT_EQ(bytes.find("target_factor"), std::string::npos);
EXPECT_EQ(bytes.find("nsnr-log-v1"), std::string::npos);
const auto fields = SplitTabs(bytes.substr(0U, bytes.size() - 1U));
EXPECT_EQ(fields.size(), 24U);
EXPECT_EQ(FieldValue(fields, "record"), "record=dynamic-newton-iteration");
EXPECT_EQ(FieldValue(fields, "schema"), "schema=hht-log-v1");
EXPECT_EQ(FieldValue(fields, "increment"), "increment=2");
EXPECT_EQ(FieldValue(fields, "iteration"), "iteration=3");
EXPECT_EQ(FieldValue(fields, "time"), "time=0.25");
EXPECT_EQ(FieldValue(fields, "stage"), "stage=convergence");
EXPECT_EQ(FieldValue(fields, "residual_u_branch"),
"residual_u_branch=relative");
EXPECT_EQ(FieldValue(fields, "residual_phi_branch"),
"residual_phi_branch=zero-scale");
EXPECT_EQ(FieldValue(fields, "residual_pass"), "residual_pass=1");
EXPECT_EQ(FieldValue(fields, "correction_pass"), "correction_pass=1");
const std::vector<std::pair<std::string, double>> expected{
{"residual_u_numerator", 2.0e-12},
{"residual_u_scale", 2.0},
{"residual_phi_numerator", 0.0},
{"residual_phi_scale", 0.0},
{"coordinate_correction_u", 1.0e-11},
{"coordinate_correction_phi", 2.0e-11},
{"velocity_correction_u", 3.0e-11},
{"velocity_correction_phi", 4.0e-11},
{"acceleration_correction_u", 5.0e-11},
{"acceleration_correction_phi", 6.0e-11},
{"algorithmic_correction_u", 7.0e-11},
{"algorithmic_correction_phi", 8.0e-11}};
for (const auto& metric : expected) {
const auto text = ValueAfterEquals(FieldValue(fields, metric.first));
double value = 0.0;
const auto parsed =
std::from_chars(text.data(), text.data() + text.size(), value);
EXPECT_EQ(parsed.ec, std::errc{});
EXPECT_EQ(parsed.ptr, text.data() + text.size());
EXPECT_DOUBLE_EQ(value, metric.second);
}
}
TEST(DynamicNewtonIterationLog, EscapesFailureContextAndKeepsZeroScaleFailure) {
TempDirectory directory{"dynamic-failure"};
const auto path = directory.Path() / "dynamic_log.txt";
auto record = CompleteDynamicRecord();
record.stage = "mass\tcheck";
record.outcome = "failed\ntrial";
record.failure_code = "dynamic\\mass\rregularity";
record.residual_numerator = {1.0, 0.0};
record.residual_scale = {0.0, 0.0};
record.residual_pass = false;
record.correction_pass = false;
fesa::NewtonIterationLogWriter writer;
ASSERT_TRUE(writer.Open(path).IsOk());
ASSERT_TRUE(writer.Write(record).IsOk());
ASSERT_TRUE(writer.Close().IsOk());
const auto bytes = ReadLogBytes(path);
EXPECT_EQ(std::count(bytes.begin(), bytes.end(), '\n'), 1);
const auto fields = SplitTabs(bytes.substr(0U, bytes.size() - 1U));
EXPECT_EQ(FieldValue(fields, "stage"), "stage=mass\\tcheck");
EXPECT_EQ(FieldValue(fields, "outcome"), "outcome=failed\\ntrial");
EXPECT_EQ(FieldValue(fields, "failure_code"),
"failure_code=dynamic\\\\mass\\rregularity");
EXPECT_EQ(FieldValue(fields, "residual_u_numerator"),
"residual_u_numerator=1");
EXPECT_EQ(FieldValue(fields, "residual_u_scale"), "residual_u_scale=0");
EXPECT_EQ(FieldValue(fields, "residual_u_branch"),
"residual_u_branch=zero-scale");
EXPECT_EQ(FieldValue(fields, "residual_pass"), "residual_pass=0");
}
TEST(DynamicNewtonIterationLog,
InvalidEvidencePreservesFlushedPrefixAndFailsLifecycle) {
using Record = fesa::DynamicNewtonIterationLogRecord;
const double nan = std::numeric_limits<double>::quiet_NaN();
const double infinity = std::numeric_limits<double>::infinity();
const std::vector<std::function<void(Record&)>> mutations{
[nan](Record& record) { record.time = nan; },
[nan](Record& record) { record.residual_numerator[0U] = nan; },
[infinity](Record& record) { record.residual_scale[1U] = infinity; },
[nan](Record& record) { record.coordinate_correction[0U] = nan; },
[infinity](Record& record) { record.velocity_correction[1U] = infinity; },
[nan](Record& record) { record.acceleration_correction[0U] = nan; },
[infinity](Record& record) {
record.algorithmic_correction[1U] = infinity;
},
[](Record& record) { record.time = -0.1; },
[](Record& record) { record.residual_scale[0U] = -1.0; },
[](Record& record) { record.coordinate_correction[0U] = -1.0; },
[](Record& record) { record.stage.clear(); },
[](Record& record) { record.outcome.clear(); }};
TempDirectory directory{"dynamic-invalid"};
const auto path = directory.Path() / "dynamic_log.txt";
for (std::size_t index = 0U; index < mutations.size(); ++index) {
SCOPED_TRACE(index);
fesa::NewtonIterationLogWriter writer;
ASSERT_TRUE(writer.Open(path).IsOk());
ASSERT_TRUE(writer.Write(CompleteDynamicRecord()).IsOk());
ASSERT_TRUE(writer.Flush().IsOk());
const std::string prefix = ReadLogBytes(path);
auto invalid = CompleteDynamicRecord();
mutations[index](invalid);
ExpectLogOutputFailure(writer.Write(invalid));
ExpectLogOutputFailure(writer.Write(CompleteDynamicRecord()));
ExpectLogOutputFailure(writer.Flush());
ExpectLogOutputFailure(writer.Close());
EXPECT_EQ(ReadLogBytes(path), prefix);
}
fesa::NewtonIterationLogWriter unopened;
ExpectLogOutputFailure(unopened.Write(CompleteDynamicRecord()));
}
} // namespace
File diff suppressed because it is too large Load Diff
@@ -10,6 +10,7 @@
#include <string>
#include "fesa/elements/finite_rotation.h"
#include "fesa/elements/nonlinear_mitc4_inertia.h"
#include "fesa/elements/nonlinear_mitc4_shell.h"
#include "fesa/model/model_types.h"
@@ -417,4 +418,165 @@ TEST(NonlinearMitc4Residual, RejectsNonfiniteTrialData) {
EXPECT_FALSE(overflow_result.HasValue());
}
void CheckThirdSweep(const fesa::NonlinearMitc4TrialDofs& trial) {
const auto nodes = Nodes();
const auto shell = Shell(nodes);
auto direction = Direction();
double norm = 0.0;
const auto unscaled = ToVector(direction);
for (std::size_t i = 0U; i < 24U; ++i) {
norm = std::hypot(norm, unscaled[i]);
}
for (std::size_t node = 0U; node < 4U; ++node) {
direction.displacement[node] = 2.0 * direction.displacement[node] / norm;
direction.rotation_vector[node] = direction.rotation_vector[node] / norm;
}
const auto velocity = ToVector(direction);
const auto exact =
fesa::EvaluateInternalResidualAndTangent(shell, trial, &velocity);
ASSERT_TRUE(exact.HasValue());
unsigned int consecutive = 0U;
bool passed = false;
for (int power = 8; power <= 20; power += 2) {
const double h = std::ldexp(1.0, -power);
const auto plus = fesa::EvaluateInternalResidualAndTangent(
shell, AddScaled(trial, direction, h));
const auto minus = fesa::EvaluateInternalResidualAndTangent(
shell, AddScaled(trial, direction, -h));
ASSERT_TRUE(plus.HasValue());
ASSERT_TRUE(minus.HasValue());
double error = 0.0;
double analytic_norm = 0.0;
double fd_norm = 0.0;
for (std::size_t i = 0U; i < 24U; ++i) {
double fd = 0.0;
for (std::size_t j = 0U; j < 24U; ++j) {
fd += (plus.Value().internal_tangent(i, j) -
minus.Value().internal_tangent(i, j)) *
velocity[j] / (2.0 * h);
}
const double d = i % 6U < 3U ? 2.0 : 1.0;
const double analytic = exact.Value().force_directional_second[i];
error = std::hypot(error, d * (analytic - fd));
analytic_norm = std::hypot(analytic_norm, d * analytic);
fd_norm = std::hypot(fd_norm, d * fd);
}
if (power >= 10 && power <= 18 &&
error / (analytic_norm + fd_norm) <= 1e-6) {
++consecutive;
passed = passed || consecutive >= 2U;
} else {
consecutive = 0U;
}
}
EXPECT_TRUE(passed);
EXPECT_GE(exact.Value().drilling_energy, 0.0);
for (std::size_t i = 0U; i < 24U; ++i) {
EXPECT_DOUBLE_EQ(
exact.Value().internal_force[i],
exact.Value().physical_force[i] + exact.Value().drilling_force[i]);
}
}
TEST(NonlinearMitc4Residual,
ZeroTrialOnTranslatedDecimalMeshHasNoArtificialForce) {
auto nodes = Nodes();
nodes[0].coordinates = {3.84615385, 2.30769231, 0.0};
nodes[1].coordinates = {4.23076923, 2.30769231, 0.0};
nodes[2].coordinates = {4.23076923, 2.69230769, 0.0};
nodes[3].coordinates = {3.84615385, 2.69230769, 0.0};
const auto shell = Shell(nodes);
const auto evaluated = fesa::EvaluateInternalResidualAndTangent(shell, {});
ASSERT_TRUE(evaluated.HasValue());
for (std::size_t i = 0; i < 24U; ++i)
EXPECT_DOUBLE_EQ(evaluated.Value().internal_force[i], 0.0);
EXPECT_DOUBLE_EQ(evaluated.Value().drilling_energy, 0.0);
}
TEST(NonlinearMitc4Residual, ExactDirectionalThirdGradientMatchesTangentSweep) {
CheckThirdSweep(Trial());
}
TEST(NonlinearMitc4Residual, ThirdGradientIsStableAcrossSmallAngleBranches) {
for (const double angle : {1e-4 * (1 - std::ldexp(1., -10)),
1e-4 * (1 + std::ldexp(1., -10)), 2e-4, 1e-3}) {
SCOPED_TRACE(angle);
auto trial = Trial();
for (auto& phi : trial.rotation_vector) phi = {angle, 0., 0.};
CheckThirdSweep(trial);
}
}
TEST(NonlinearMitc4Residual,
ExactDrillingConstraintHasIndependentForceGradientAndHessian) {
for (bool warped : {false, true}) {
auto nodes = Nodes();
if (warped) nodes[2U].coordinates[2U] = 0.1;
const auto shell = Shell(nodes);
auto trial = Trial();
trial.rotation_vector[0U] = {.6, -.2, .1};
const auto direction = Direction();
const auto velocity = ToVector(direction);
const auto exact =
fesa::EvaluateInternalResidualAndTangent(shell, trial, &velocity);
ASSERT_TRUE(exact.HasValue());
for (std::size_t node = 0U; node < 4U; ++node) {
const auto rotation = fesa::EvaluateDynamicRotation(
trial.rotation_vector[node], {0., 0., 1.},
direction.rotation_vector[node], {});
ASSERT_TRUE(rotation.HasValue());
double psi = 0.;
for (std::size_t c = 0U; c < 3U; ++c)
psi += rotation.Value().drill_null[c] *
exact.Value().internal_force[6U * node + 3U + c];
EXPECT_NEAR(exact.Value().drilling_constraint[node], psi, 1e-11);
for (std::size_t i = 0U; i < 24U; ++i) {
double gradient = 0.;
for (std::size_t c = 0U; c < 3U; ++c) {
gradient += rotation.Value().drill_null[c] *
exact.Value().internal_tangent(6U * node + 3U + c, i);
if (i / 6U == node && i % 6U >= 3U)
gradient += rotation.Value().null_gradient(c, i % 6U - 3U) *
exact.Value().internal_force[6U * node + 3U + c];
}
EXPECT_NEAR(exact.Value().drilling_constraint_gradient(node, i),
gradient, 1e-10);
}
}
unsigned consecutive = 0U;
bool passed = false;
for (int power = 8; power <= 20; power += 2) {
const double h = std::ldexp(1., -power);
const auto plus = fesa::EvaluateInternalResidualAndTangent(
shell, AddScaled(trial, direction, h), &velocity);
const auto minus = fesa::EvaluateInternalResidualAndTangent(
shell, AddScaled(trial, direction, -h), &velocity);
ASSERT_TRUE(plus.HasValue());
ASSERT_TRUE(minus.HasValue());
double error = 0., anorm = 0., fnorm = 0.;
for (std::size_t node = 0U; node < 4U; ++node)
for (std::size_t i = 0U; i < 24U; ++i) {
double analytic = 0.;
for (std::size_t j = 0U; j < 24U; ++j)
analytic += exact.Value().drilling_constraint_hessian[node](i, j) *
velocity[j];
const double fd =
(plus.Value().drilling_constraint_gradient(node, i) -
minus.Value().drilling_constraint_gradient(node, i)) /
(2 * h);
const double scale = i % 6U < 3U ? 2. : 1.;
error = std::hypot(error, scale * (analytic - fd));
anorm = std::hypot(anorm, scale * analytic);
fnorm = std::hypot(fnorm, scale * fd);
}
if (power >= 10 && power <= 18 && error / (anorm + fnorm) <= 1e-6) {
++consecutive;
passed = passed || consecutive >= 2U;
} else
consecutive = 0U;
}
EXPECT_TRUE(passed);
}
}
} // namespace
@@ -0,0 +1,636 @@
#include "fesa/io/hdf5/dynamic_hdf5_results_writer.h"
#include <Windows.h>
#include <gtest/gtest.h>
#include <hdf5.h>
#include <array>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/dynamic_analysis_state.h"
#include "fesa/elements/finite_rotation.h"
#include "fesa/fem/dof_manager.h"
#include "io/hdf5/hdf5_raii.h"
namespace {
using fesa::hdf5_internal::Hdf5Handle;
constexpr std::array<std::array<double, 3>, 3> kIdentityTriad{
std::array<double, 3>{1.0, 0.0, 0.0}, std::array<double, 3>{0.0, 1.0, 0.0},
std::array<double, 3>{0.0, 0.0, 1.0}};
fesa::ModelDefinition MakeDefinition(const bool fixed = true) {
const std::filesystem::path source{"dynamic-state.inp"};
fesa::ModelDefinition definition{};
definition.source_path = source;
definition.source_content_identity = "hht-output-unit-fixture";
definition.nodes = {{{"Shell-A", 10, "0010"}, {0.0, 0.0, 0.0}, {source, 1U}},
{{"Shell-A", 20, "20"}, {1.0, 0.0, 0.0}, {source, 2U}},
{{"Shell-A", 30, "30"}, {1.0, 1.0, 0.0}, {source, 3U}},
{{"Shell-A", 40, "40"}, {0.0, 1.0, 0.0}, {source, 4U}}};
definition.materials = {{"Material", 1000.0, 0.25, {source, 5U}}};
definition.densities = {{0U, 2.0, {source, 6U}}};
definition.shell_sections = {{"Shell", 0.1, 0U, {source, 7U}}};
definition.shell_elements = {{{"Shell-A", 9, "0009"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
0U,
0U,
{source, 8U}}};
for (std::size_t node = 0U; node < 4U; ++node) {
definition.shell_node_initial_frames.push_back(
{static_cast<fesa::EntityIndex>(node),
{0.0, 0.0, 1.0},
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0}});
}
definition.node_sets = {{"All", std::nullopt, {0U, 1U, 2U, 3U}, {source, 9U}},
{"Loaded", std::nullopt, {0U}, {source, 10U}}};
fesa::DynamicStepDefinition step{};
step.source_step_name_raw = "Transient Load";
step.source_step_name_canonical = "TRANSIENT LOAD";
step.initial_increment = 0.25;
step.time_period = 0.5;
step.increment_count = 2U;
step.maximum_increments = 3U;
if (fixed) {
step.boundaries = {{"All", 1, 6, 0.0, {source, 11U}}};
step.loads = {{"Loaded", 3, 4.0, {source, 12U}}};
}
definition.dynamic_step = step;
return definition;
}
struct Fixture {
std::unique_ptr<fesa::Domain> domain;
fesa::DynamicAnalysisState state;
};
Fixture MakeFixture(fesa::ModelDefinition definition = MakeDefinition()) {
auto domain_result = fesa::Domain::Create(std::move(definition));
if (!domain_result.HasValue())
throw std::runtime_error{"Invalid fixture Domain."};
auto domain =
std::make_unique<fesa::Domain>(std::move(domain_result.Value()));
auto model = fesa::AnalysisModel::Create(*domain);
if (!model.HasValue())
throw std::runtime_error{"Invalid fixture AnalysisModel."};
auto dofs = fesa::DofManager::Create(model.Value());
if (!dofs.HasValue()) throw std::runtime_error{"Invalid fixture DofManager."};
auto state = fesa::DynamicAnalysisState::Create(*domain, dofs.Value());
if (!state.HasValue()) throw std::runtime_error{"Invalid fixture state."};
return {std::move(domain), std::move(state.Value())};
}
// A fully restrained, initially undeformed shell under a dead nodal force has
// zero motion and strain, and nonzero support force at every accepted endpoint.
fesa::DynamicAcceptedFrameState MakeFrame(const fesa::Domain& domain,
const std::uint32_t index) {
fesa::DynamicAcceptedFrameState frame{};
frame.frame_index = index;
frame.increment_index = index;
frame.time = static_cast<double>(index) * domain.Dynamic()->initial_increment;
frame.is_initial = index == 0U;
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
frame.nodal_internal_order.push_back(static_cast<fesa::EntityIndex>(node));
frame.nodal_source_ids.push_back(domain.Nodes()[node].source_id);
frame.nodal_triad.push_back(kIdentityTriad);
frame.nodal_director.push_back({0.0, 0.0, 1.0});
}
for (auto* vector :
{&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
&frame.algorithmic_acceleration, &frame.inertia_force,
&frame.internal_force, &frame.external_force, &frame.residual,
&frame.reaction, &frame.residual_scale}) {
*vector = fesa::Vector{24U};
}
frame.angular_velocity = fesa::Vector{12U};
frame.angular_acceleration = fesa::Vector{12U};
if (!domain.Dynamic()->loads.empty()) {
frame.external_force[2U] = 4.0;
frame.residual[2U] = -4.0;
frame.reaction[2U] = -4.0;
frame.residual_scale[2U] = 4.0;
}
const double gauss = 1.0 / std::sqrt(3.0);
const std::array<std::array<double, 2>, 4> natural{
std::array<double, 2>{-gauss, -gauss},
{gauss, -gauss},
{gauss, gauss},
{-gauss, gauss}};
const std::array<fesa::ShellMidsurfaceLocation, 4> locations{
fesa::ShellMidsurfaceLocation::kGp1, fesa::ShellMidsurfaceLocation::kGp2,
fesa::ShellMidsurfaceLocation::kGp3, fesa::ShellMidsurfaceLocation::kGp4};
for (std::size_t point = 0U; point < 4U; ++point) {
frame.shell_results.push_back(
{0U,
locations[point],
natural[point],
kIdentityTriad,
{},
{},
{{{fesa::ShellSectionPosition::kBottom, -1.0, {}},
{fesa::ShellSectionPosition::kMiddle, 0.0, {}},
{fesa::ShellSectionPosition::kTop, 1.0, {}}}}});
}
return frame;
}
class TempFile {
public:
explicit TempFile(const std::string& name)
: path_{std::filesystem::temp_directory_path() /
("fesa-dynamic-writer-" + name + ".h5")} {
std::error_code error;
std::filesystem::remove(path_, error);
}
~TempFile() {
std::error_code error;
std::filesystem::remove(path_, error);
}
const std::filesystem::path& Path() const { return path_; }
private:
std::filesystem::path path_;
};
void CommitHistory(Fixture& fixture) {
for (std::uint32_t index = 0U; index <= 2U; ++index) {
ASSERT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, index))
.IsOk());
}
}
std::string ReadBytes(const std::filesystem::path& path) {
std::ifstream input{path, std::ios::binary};
return std::string{std::istreambuf_iterator<char>{input}, {}};
}
std::string ReadString(const hid_t object, const char* name) {
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
EXPECT_EQ(H5Tget_class(type.Get()), H5T_STRING);
EXPECT_GT(H5Tis_variable_str(type.Get()), 0);
EXPECT_EQ(H5Tget_cset(type.Get()), H5T_CSET_UTF8);
char* raw = nullptr;
EXPECT_GE(H5Aread(attribute.Get(), type.Get(), &raw), 0);
const std::string value = raw == nullptr ? "" : raw;
(void)H5free_memory(raw);
return value;
}
template <typename Value>
Value ReadAttribute(const hid_t object, const char* name, const hid_t file_type,
const hid_t memory_type) {
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
Hdf5Handle space{H5Aget_space(attribute.Get()), H5Sclose};
EXPECT_GT(H5Tequal(type.Get(), file_type), 0);
EXPECT_EQ(H5Sget_simple_extent_type(space.Get()), H5S_SCALAR);
Value result{};
EXPECT_GE(H5Aread(attribute.Get(), memory_type, &result), 0);
return result;
}
std::vector<double> ReadDoubles(const hid_t file, const std::string& path,
const std::vector<hsize_t>& dimensions) {
Hdf5Handle dataset{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose};
EXPECT_GE(dataset.Get(), 0);
Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose};
EXPECT_GT(H5Tequal(type.Get(), H5T_IEEE_F64LE), 0);
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
EXPECT_EQ(H5Sget_simple_extent_ndims(space.Get()), dimensions.size());
std::vector<hsize_t> actual(dimensions.size());
EXPECT_GE(H5Sget_simple_extent_dims(space.Get(), actual.data(), nullptr), 0);
EXPECT_EQ(actual, dimensions);
Hdf5Handle properties{H5Dget_create_plist(dataset.Get()), H5Pclose};
EXPECT_EQ(H5Pget_layout(properties.Get()), H5D_CONTIGUOUS);
EXPECT_EQ(H5Pget_nfilters(properties.Get()), 0);
std::size_t count = 1U;
for (const auto size : dimensions) count *= static_cast<std::size_t>(size);
std::vector<double> values(count);
EXPECT_GE(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
H5P_DEFAULT, values.data()),
0);
for (const double value : values) EXPECT_TRUE(std::isfinite(value));
return values;
}
void ExpectCompound(const hid_t file, const char* path,
const std::vector<std::pair<const char*, hid_t>>& fields,
const hsize_t rows) {
Hdf5Handle dataset{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose};
ASSERT_GE(dataset.Get(), 0);
Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose};
EXPECT_EQ(H5Tget_nmembers(type.Get()), fields.size());
for (std::size_t index = 0U; index < fields.size(); ++index) {
char* name = H5Tget_member_name(type.Get(), static_cast<unsigned>(index));
ASSERT_NE(name, nullptr);
EXPECT_STREQ(name, fields[index].first);
(void)H5free_memory(name);
Hdf5Handle member{
H5Tget_member_type(type.Get(), static_cast<unsigned>(index)), H5Tclose};
EXPECT_GT(H5Tequal(member.Get(), fields[index].second), 0);
}
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
EXPECT_EQ(H5Sget_simple_extent_ndims(space.Get()), 1);
hsize_t actual = 0U;
EXPECT_GE(H5Sget_simple_extent_dims(space.Get(), &actual, nullptr), 0);
EXPECT_EQ(actual, rows);
}
TEST(DynamicHdf5ResultsWriter, RejectsDefaultStateBeforeCreatingOutput) {
auto domain = fesa::Domain::Create(fesa::ModelDefinition{});
ASSERT_TRUE(domain.HasValue());
TempFile output{"default"};
fesa::DynamicHdf5ResultsWriter writer;
const auto status = writer.Write(output.Path(), domain.Value(), {}, {});
EXPECT_FALSE(status.IsOk());
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, "dynamic-output-schema");
EXPECT_FALSE(std::filesystem::exists(output.Path()));
}
TEST(DynamicHdf5ResultsWriter, WritesCompletePhysicalClampedHistoryAndSchema) {
auto fixture = MakeFixture();
CommitHistory(fixture);
TempFile output{"complete"};
fesa::DynamicHdf5ResultsWriter writer;
const std::vector<fesa::Diagnostic> diagnostics{{fesa::Severity::kWarning,
"ignored-output-request",
{"dynamic-state.inp", 22U},
"OUTPUT",
"",
"Field request ignored."},
{fesa::Severity::kWarning,
"ignored-output-request",
{"dynamic-state.inp", 21U},
"RESTART",
"",
"Restart request ignored."}};
const auto status =
writer.Write(output.Path(), *fixture.domain, fixture.state, diagnostics);
ASSERT_TRUE(status.IsOk())
<< (status.Diagnostics().empty() ? "" : status.Diagnostics()[0U].message);
Hdf5Handle file{
H5Fopen(output.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
H5Fclose};
ASSERT_GE(file.Get(), 0);
Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
EXPECT_EQ(ReadString(metadata.Get(), "feature_id"), "hht-implicit-dynamics");
EXPECT_EQ(ReadString(metadata.Get(), "analysis_procedure"),
"hht-implicit-dynamics");
EXPECT_EQ(ReadString(metadata.Get(), "source_step_name"), "Transient Load");
EXPECT_EQ(ReadString(metadata.Get(), "mass_formulation"),
"consistent-kinetic");
EXPECT_EQ(ReadString(metadata.Get(), "acceleration_convention"),
"physical-endpoint-and-auxiliary-separated");
EXPECT_EQ(ReadString(metadata.Get(), "initial_state"),
"zero-q-v-step-load-0+");
EXPECT_EQ(ReadString(metadata.Get(), "unit_system_label"),
"consistent-input-units");
EXPECT_EQ(ReadAttribute<std::uint64_t>(metadata.Get(), "schema_version",
H5T_STD_U64LE, H5T_NATIVE_UINT64),
1U);
EXPECT_DOUBLE_EQ(ReadAttribute<double>(metadata.Get(), "alpha",
H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE),
-0.05);
EXPECT_DOUBLE_EQ(ReadAttribute<double>(metadata.Get(), "beta", H5T_IEEE_F64LE,
H5T_NATIVE_DOUBLE),
0.275625);
EXPECT_DOUBLE_EQ(ReadAttribute<double>(metadata.Get(), "gamma",
H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE),
0.55);
ExpectCompound(file.Get(), "/steps/Step-1/accepted_frames",
{{"frame_index", H5T_STD_U64LE},
{"increment_index", H5T_STD_U64LE},
{"time", H5T_IEEE_F64LE},
{"iteration_count", H5T_STD_U64LE},
{"is_initial", H5T_STD_U8LE}},
3U);
ExpectCompound(
file.Get(), "/model/material_density",
{{"internal_material_id", H5T_STD_U64LE}, {"rho", H5T_IEEE_F64LE}}, 1U);
struct Density {
std::uint64_t id;
double rho;
};
Hdf5Handle density{
H5Dopen2(file.Get(), "/model/material_density", H5P_DEFAULT), H5Dclose};
Hdf5Handle density_type{H5Dget_type(density.Get()), H5Tclose};
Density density_row{};
ASSERT_GE(H5Dread(density.Get(), density_type.Get(), H5S_ALL, H5S_ALL,
H5P_DEFAULT, &density_row),
0);
EXPECT_EQ(density_row.id, 0U);
EXPECT_DOUBLE_EQ(density_row.rho, 2.0);
Hdf5Handle mask{
H5Dopen2(file.Get(), "/model/nodal_constraint_mask", H5P_DEFAULT),
H5Dclose};
Hdf5Handle mask_type{H5Dget_type(mask.Get()), H5Tclose};
EXPECT_GT(H5Tequal(mask_type.Get(), H5T_STD_U8LE), 0);
std::array<std::uint8_t, 24U> constraints{};
ASSERT_GE(H5Dread(mask.Get(), H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL, H5P_DEFAULT,
constraints.data()),
0);
for (const auto value : constraints) EXPECT_EQ(value, 1U);
EXPECT_EQ(ReadDoubles(file.Get(), "/model/prescribed_displacement", {4U, 6U}),
std::vector<double>(24U, 0.0));
EXPECT_EQ(H5Lexists(file.Get(), "/model/prescribed_displacement_start",
H5P_DEFAULT),
0);
EXPECT_EQ(H5Lexists(file.Get(), "/model/prescribed_displacement_target",
H5P_DEFAULT),
0);
struct Dataset {
const char* path;
std::vector<hsize_t> dimensions;
const char* components;
};
const std::vector<Dataset> datasets{
{"nodal/displacement", {4U, 6U}, "UX,UY,UZ,URX,URY,URZ"},
{"nodal/velocity", {4U, 6U}, "V1,V2,V3,PHIDOT1,PHIDOT2,PHIDOT3"},
{"nodal/acceleration", {4U, 6U}, "A1,A2,A3,PHIDDOT1,PHIDDOT2,PHIDDOT3"},
{"nodal/algorithmic_acceleration",
{4U, 6U},
"AUX1,AUX2,AUX3,AUX4,AUX5,AUX6"},
{"nodal/angular_velocity", {4U, 3U}, "OMEGA1,OMEGA2,OMEGA3"},
{"nodal/angular_acceleration", {4U, 3U}, "OMEGADOT1,OMEGADOT2,OMEGADOT3"},
{"nodal/triad", {4U, 3U, 3U}, "X,Y,Z"},
{"nodal/director", {4U, 3U}, "D1,D2,D3"},
{"nodal/generalized_inertia_force",
{4U, 6U},
"F1,F2,F3,MPHI1,MPHI2,MPHI3"},
{"nodal/generalized_internal_force",
{4U, 6U},
"F1,F2,F3,MPHI1,MPHI2,MPHI3"},
{"nodal/generalized_external_force",
{4U, 6U},
"F1,F2,F3,MPHI1,MPHI2,MPHI3"},
{"nodal/generalized_residual", {4U, 6U}, "F1,F2,F3,MPHI1,MPHI2,MPHI3"},
{"nodal/generalized_reaction", {4U, 6U}, "F1,F2,F3,MPHI1,MPHI2,MPHI3"},
{"nodal/reaction", {4U, 6U}, "RF1,RF2,RF3,RM1,RM2,RM3"},
{"element/shell/local_frame", {1U, 4U, 3U, 3U}, "X,Y,Z"},
{"element/shell/generalized_strain",
{1U, 4U, 8U},
"E11,E22,G12,K11,K22,K12,G13,G23"},
{"element/shell/section_resultant",
{1U, 4U, 8U},
"N11,N22,N12,M11,M22,M12,Q13,Q23"},
{"element/shell/stress", {1U, 4U, 3U, 3U}, "S11,S22,S12"},
{"global/energy",
{5U},
"KINETIC_ENERGY,PHYSICAL_STRAIN_ENERGY,DRILLING_ENERGY,EXTERNAL_WORK,"
"ENERGY_DEFECT"},
{"global/equilibrium",
{6U},
"FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3"}};
for (std::uint64_t index = 0U; index <= 2U; ++index) {
const std::string root =
"/steps/Step-1/frames/" + std::to_string(index) + "/";
for (const auto& expected : datasets) {
const std::string path = root + expected.path;
SCOPED_TRACE(path);
const auto values = ReadDoubles(file.Get(), path, expected.dimensions);
Hdf5Handle dataset{H5Dopen2(file.Get(), path.c_str(), H5P_DEFAULT),
H5Dclose};
EXPECT_EQ(ReadString(dataset.Get(), "component_names"),
expected.components);
EXPECT_FALSE(
ReadString(dataset.Get(), "component_unit_dimensions").empty());
EXPECT_FALSE(ReadString(dataset.Get(), "coordinate_system").empty());
EXPECT_FALSE(ReadString(dataset.Get(), "location").empty());
EXPECT_EQ(ReadString(dataset.Get(), "step_name"), "Step-1");
EXPECT_EQ(ReadString(dataset.Get(), "source_step_name"),
"Transient Load");
EXPECT_EQ(ReadAttribute<std::uint64_t>(dataset.Get(), "frame_index",
H5T_STD_U64LE, H5T_NATIVE_UINT64),
index);
EXPECT_EQ(ReadAttribute<std::uint64_t>(dataset.Get(), "increment_index",
H5T_STD_U64LE, H5T_NATIVE_UINT64),
index);
EXPECT_EQ(ReadAttribute<std::uint8_t>(dataset.Get(), "is_initial",
H5T_STD_U8LE, H5T_NATIVE_UINT8),
index == 0U ? 1U : 0U);
EXPECT_DOUBLE_EQ(ReadAttribute<double>(dataset.Get(), "time",
H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE),
static_cast<double>(index) * 0.25);
EXPECT_EQ(H5Aexists(dataset.Get(), "load_factor"), 0);
const std::string leaf{expected.path};
if (leaf == "nodal/triad" || leaf == "element/shell/local_frame") {
for (std::size_t entry = 0U; entry < values.size(); ++entry) {
EXPECT_DOUBLE_EQ(
values[entry],
entry % 9U == 0U || entry % 9U == 4U || entry % 9U == 8U ? 1.0
: 0.0);
}
} else if (leaf == "nodal/director") {
for (std::size_t entry = 0U; entry < values.size(); ++entry)
EXPECT_DOUBLE_EQ(values[entry], entry % 3U == 2U ? 1.0 : 0.0);
} else {
for (std::size_t entry = 0U; entry < values.size(); ++entry) {
const bool force =
entry == 2U && (leaf == "nodal/generalized_external_force" ||
leaf == "nodal/generalized_residual" ||
leaf == "nodal/generalized_reaction" ||
leaf == "nodal/reaction");
EXPECT_DOUBLE_EQ(
values[entry],
force ? (leaf == "nodal/generalized_external_force" ? 4.0 : -4.0)
: 0.0);
}
}
}
}
Hdf5Handle stress{
H5Dopen2(file.Get(), "/steps/Step-1/frames/2/element/shell/stress",
H5P_DEFAULT),
H5Dclose};
EXPECT_EQ(ReadString(stress.Get(), "source_element_type_dataset"),
"/model/elements.source_element_type");
EXPECT_EQ(ReadString(stress.Get(), "midsurface_location_dataset"),
"/model/shell/midsurface_locations");
EXPECT_EQ(ReadString(stress.Get(), "section_position_dataset"),
"/model/shell/section_positions");
EXPECT_EQ(ReadString(stress.Get(), "local_frame_dataset"),
"/steps/Step-1/frames/2/element/shell/local_frame");
Hdf5Handle diagnostic_table{H5Dopen2(file.Get(), "/diagnostics", H5P_DEFAULT),
H5Dclose};
Hdf5Handle diagnostic_type{H5Dget_type(diagnostic_table.Get()), H5Tclose};
EXPECT_GE(H5Tget_member_index(diagnostic_type.Get(), "source_file"), 0);
EXPECT_GE(H5Tget_member_index(diagnostic_type.Get(), "source_line"), 0);
}
TEST(DynamicHdf5ResultsWriter,
PreservesSeparateSuppliedEndpointAndAuxiliaryEvidence) {
auto fixture = MakeFixture(MakeDefinition(false));
ASSERT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 0U)).IsOk());
// This checks serialization of supplied endpoint evidence; it does not assert
// that these records are a converged HHT trajectory or independent physics
// proof.
for (std::uint32_t index = 1U; index <= 2U; ++index) {
auto frame = MakeFrame(*fixture.domain, index);
frame.acceleration[0U] = 2.0;
frame.algorithmic_acceleration[0U] = 3.0;
frame.velocity[0U] = 4.0;
frame.inertia_force[0U] = 5.0;
frame.residual[0U] = 5.0;
frame.residual_scale[0U] = 5.0;
frame.kinetic_energy = 6.0;
frame.physical_strain_energy = 7.0;
frame.drilling_energy = 8.0;
frame.external_work = 9.0;
frame.energy_defect = 12.0;
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(frame).IsOk());
}
TempFile output{"separate-accelerations"};
fesa::DynamicHdf5ResultsWriter writer;
ASSERT_TRUE(
writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk());
Hdf5Handle file{
H5Fopen(output.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
H5Fclose};
const std::string root = "/steps/Step-1/frames/2/";
EXPECT_DOUBLE_EQ(
ReadDoubles(file.Get(), root + "nodal/acceleration", {4U, 6U})[0U], 2.0);
EXPECT_DOUBLE_EQ(
ReadDoubles(file.Get(), root + "nodal/algorithmic_acceleration",
{4U, 6U})[0U],
3.0);
EXPECT_DOUBLE_EQ(
ReadDoubles(file.Get(), root + "nodal/generalized_inertia_force",
{4U, 6U})[0U],
5.0);
EXPECT_EQ(ReadDoubles(file.Get(), root + "global/energy", {5U}),
(std::vector<double>{6.0, 7.0, 8.0, 9.0, 12.0}));
Hdf5Handle acceleration{
H5Dopen2(file.Get(), (root + "nodal/acceleration").c_str(), H5P_DEFAULT),
H5Dclose};
EXPECT_EQ(ReadString(acceleration.Get(), "rotation_representation"),
"total-rotation-vector-chart");
EXPECT_EQ(ReadString(acceleration.Get(), "quantity_semantics"),
"physical-endpoint-chart-acceleration");
}
TEST(DynamicHdf5ResultsWriter,
ConvertsPartialConstraintMomentToSpatialReaction) {
auto definition = MakeDefinition(false);
definition.dynamic_step->boundaries = {{"Loaded", 4, 4, 0.0, {}}};
auto fixture = MakeFixture(std::move(definition));
ASSERT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 0U)).IsOk());
// A supplied finite-chart endpoint tests the output coordinate transform,
// independently of time-integration acceptance.
for (std::uint32_t index = 1U; index <= 2U; ++index) {
auto frame = MakeFrame(*fixture.domain, index);
frame.generalized_coordinates[4U] = 0.2;
frame.generalized_coordinates[5U] = 0.3;
const auto rotation = fesa::ExpSkew(fesa::Vector3{0.0, 0.2, 0.3});
for (std::size_t axis = 0U; axis < 3U; ++axis) {
for (std::size_t component = 0U; component < 3U; ++component) {
frame.nodal_triad[0U][axis][component] = rotation(component, axis);
}
frame.nodal_director[0U][axis] = rotation(axis, 2U);
}
frame.internal_force[3U] = 5.0;
frame.residual[3U] = 5.0;
frame.reaction[3U] = 5.0;
frame.residual_scale[3U] = 5.0;
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(frame).IsOk());
}
TempFile output{"spatial-reaction"};
fesa::DynamicHdf5ResultsWriter writer;
ASSERT_TRUE(
writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk());
Hdf5Handle file{
H5Fopen(output.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
H5Fclose};
const std::string root = "/steps/Step-1/frames/2/nodal/";
const auto generalized =
ReadDoubles(file.Get(), root + "generalized_reaction", {4U, 6U});
const auto spatial = ReadDoubles(file.Get(), root + "reaction", {4U, 6U});
EXPECT_DOUBLE_EQ(generalized[3U], 5.0);
EXPECT_DOUBLE_EQ(generalized[4U], 0.0);
EXPECT_DOUBLE_EQ(generalized[5U], 0.0);
const double angle = std::sqrt(0.13);
const double coefficient =
1.0 / 0.13 - (1.0 + std::cos(angle)) / (2.0 * angle * std::sin(angle));
EXPECT_NEAR(spatial[3U], 5.0 * (1.0 - coefficient * 0.13), 1.0e-14);
EXPECT_NEAR(spatial[4U], 0.75, 1.0e-14);
EXPECT_NEAR(spatial[5U], -0.5, 1.0e-14);
}
TEST(DynamicHdf5ResultsWriter,
IncompleteOrMismatchedHistoryPreservesExistingFile) {
auto fixture = MakeFixture();
ASSERT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 0U)).IsOk());
TempFile output{"preservation"};
{
std::ofstream existing{output.Path(), std::ios::binary};
existing << "previous-result";
}
fesa::DynamicHdf5ResultsWriter writer;
EXPECT_FALSE(
writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk());
EXPECT_EQ(ReadBytes(output.Path()), "previous-result");
for (std::uint32_t index = 1U; index <= 2U; ++index)
ASSERT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, index))
.IsOk());
auto different_definition = MakeDefinition();
different_definition.nodes[0U].source_id.source_label_text = "10";
auto other = MakeFixture(std::move(different_definition));
EXPECT_FALSE(
writer.Write(output.Path(), *other.domain, fixture.state, {}).IsOk());
EXPECT_EQ(ReadBytes(output.Path()), "previous-result");
ASSERT_TRUE(
writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk());
const auto valid_bytes = ReadBytes(output.Path());
// Deliberate post-commit corruption proves writer revalidation is independent
// of the state API that normally prevents these mutations.
auto& corrupt = const_cast<fesa::DynamicAcceptedFrameState&>(
fixture.state.CommittedState());
corrupt.reaction[2U] = 0.0;
EXPECT_FALSE(
writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk());
EXPECT_EQ(ReadBytes(output.Path()), valid_bytes);
}
TEST(DynamicHdf5ResultsWriter, FailedAtomicReplacementPreservesPreviousBytes) {
auto fixture = MakeFixture();
CommitHistory(fixture);
TempFile output{"locked"};
{
std::ofstream existing{output.Path(), std::ios::binary};
existing << "locked-previous-result";
}
const HANDLE locked =
CreateFileW(output.Path().c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
ASSERT_NE(locked, INVALID_HANDLE_VALUE);
fesa::DynamicHdf5ResultsWriter writer;
const auto status =
writer.Write(output.Path(), *fixture.domain, fixture.state, {});
EXPECT_FALSE(status.IsOk());
EXPECT_TRUE(CloseHandle(locked));
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, "hdf5-finalization-failure");
EXPECT_EQ(ReadBytes(output.Path()), "locked-previous-result");
}
} // namespace