feat: implement consistent-mass HHT shell dynamics
This commit is contained in:
@@ -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 실행에 포함되지 않았다.
|
||||
|
||||
@@ -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 통과 판정이 아니다.
|
||||
@@ -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`.
|
||||
|
||||
@@ -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_
|
||||
@@ -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_
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user