Compare commits
26 Commits
0d826f835b
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ae7e6d414 | |||
| d1c0e36566 | |||
| 89ac4f27a2 | |||
| 3ecb8655a2 | |||
| 649ffe4085 | |||
| f09b3e1249 | |||
| 45ac118d68 | |||
| 6bbbb2ba8c | |||
| a73d3fc9ca | |||
| 3d5c296651 | |||
| df1bf94369 | |||
| c6188b3ad7 | |||
| 294ffbf8a1 | |||
| 8bac3ebf49 | |||
| 52e321e15c | |||
| 86b740eebf | |||
| 626f7da880 | |||
| 818046c397 | |||
| 710ab39059 | |||
| 0c88cb24c8 | |||
| dd54b0ae1e | |||
| 0e392a7364 | |||
| ad06d7ad6c | |||
| adb809bc01 | |||
| 78cb339207 | |||
| d7f6d0ce74 |
+2
-1
@@ -3,11 +3,12 @@ project(FESA VERSION 0.1.0 LANGUAGES CXX)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
option(FESA_HHT_DEVELOPMENT_PROBE "Build private HHT development counters" OFF)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
# Pin the original backend ABI/link/thread choices rather than package defaults.
|
||||
set(MKL_INTERFACE ilp64)
|
||||
set(MKL_LINK dynamic)
|
||||
set(MKL_THREADING intel_thread)
|
||||
set(MKL_THREADING tbb_thread)
|
||||
include(cmake/FesaDependencies.cmake)
|
||||
find_package(Doxygen QUIET)
|
||||
if(Doxygen_FOUND)
|
||||
|
||||
@@ -44,6 +44,15 @@ if(NOT TARGET TBB::tbb)
|
||||
message(FATAL_ERROR "The oneTBB CONFIG package did not define TBB::tbb")
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
set(FESA_MKL_THREAD_TARGET MKL::mkl_tbb_thread)
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set(FESA_MKL_THREAD_TARGET MKL::mkl_tbb_threadd)
|
||||
endif()
|
||||
# HDF5 requires the Intel math runtime independently of MKL threading.
|
||||
set(FESA_INTEL_MATH_RUNTIME "$ENV{FESA_OMP_ROOT}/bin/libmmd.dll")
|
||||
endif()
|
||||
|
||||
# Prefer the packaged shared C runtime when both variants exist. The Windows
|
||||
# static archive may carry compiler-runtime requirements that are not part of
|
||||
# the HDF5 imported target's transitive link interface.
|
||||
|
||||
@@ -38,4 +38,5 @@ Formulation의 수식·가정은 원문으로 보존한다. 문서 안의 ready/
|
||||
- [Hex8 구조해석](hex8-structural-analysis/README.md)
|
||||
- [Shell 열전달](shell-transient-heat-transfer/README.md)
|
||||
- [계산 성능](calculation-performance/README.md)
|
||||
- [Abaqus 수렴 허용치 조사와 FESA 적용 실험](abaqus-tolerance/README.md)
|
||||
- [C++ 모듈화·Refactor 병합 현황과 이관 근거](cpp-object-oriented-modular-refactoring/README.md)
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
# Abaqus 수렴 허용치 조사와 FESA 대응
|
||||
|
||||
2026-10-19 후속 사용자 승인으로 이번 실험은 **Abaqus 변위 검증 통과·실패10개 보류로 조건부 완료**한다.
|
||||
[결정 기록](../tracking/decisions/0011-tolerance-experiment-acceptance.md)과 [최종 정리](results.md#후속-승인과-남은-단계의-완료)를 따른다.
|
||||
기존 FAIL/SKIP·수치 기준은 보존하며 공식 speedup·전체 물리/시간 정확도 완료로 확대하지 않는다.
|
||||
|
||||
조사일: 2026-10-18. FESA 조사 기준: `43867d0`.
|
||||
2026-10-19 실험 branch에서 공개 field 규칙을 MITC4 HHT의 실제 종료에 연결했다.
|
||||
Native/CTest 전체는 각각987 PASS·10 FAIL·1 SKIP이며, 공식 후보 성능 비교는 미성립이다.
|
||||
구현·물리 검증·측정의 상세 근거와 한계는 [실험 결과](results.md)에 기록했다.
|
||||
후속 [정식 모델 변위 비교](reference-displacement-comparison.md)는 8개 모델 모두 U PASS다.
|
||||
모두 Release 결과이며, 새 field 후보 100증분은 5662.144854초로 완료했다. 공식 speedup 판정과는 구분한다.
|
||||
실패 10개의 [상대오차 전환 검토](relative-error-review.md)에서는 D3·drilling이 상대 `1e-3`도
|
||||
넘는 것을 확인했다. 나머지 항목의 기존 정규화 검사와 실행 실패도 구분했으며 기준을 변경하지 않았다.
|
||||
|
||||
승인된 후보의 시험 가능한 수식·분기 우선순위·평균 이력·로그 계약은
|
||||
[외부 field 수렴 정식화](formulation-convergence.md)를 따른다. 아래는 조사 당시 코드와
|
||||
공개 기본값의 비교다. 현재 후보는 이 정식화를 실제 사용한다. 정식화는 2025 공개 STRICT
|
||||
규칙과 비공개 부분의 명시적 FESA 대응을 구분하며 실제 2024 설정과의 동일성을 주장하지 않는다.
|
||||
|
||||
## 조사 결론
|
||||
|
||||
Abaqus/Standard의 일반 비선형 평형 기준은 잔차 비율 **0.005**, 해 보정 비율
|
||||
**0.01**이다. 병진과 회전을 각각 검사한다. 조사 당시 `43867d0`의 MITC4 HHT는 힘·모멘트
|
||||
잔차와 q/v/a/A 보정량에 **1e-10**을 사용했다. 완화 실험을 검토할 근거는 있으나,
|
||||
양쪽의 정규화와 검사 대상이 달라 숫자의 비로 정확도나 실행시간을 예측할 수 없다.
|
||||
일반 기준값의 근거는 [Abaqus 공식 해석 지침의 제어값 설명][S1]이다.
|
||||
|
||||
비선형 반복 종료, 선형방정식 풀이 정확도, 시간적분 정확도, 결과 비교 오차는 서로
|
||||
다른 기준이다. `1e-10`을 전역 검색해 `0.005`로 치환하는 접근은 부적절하다.
|
||||
아래 적용 권고는 조사 당시 **실험 제안**이다. 이후 승인된 후보의 정확한 판정은
|
||||
정식화에 고정하며, 실제 구현·검증 상태와 구분한다.
|
||||
|
||||
## 출처와 버전의 한계
|
||||
|
||||
- 아래 Abaqus 수치는 Dassault Systèmes가 작성한 **Abaqus 2025 공식 문서**의 공개
|
||||
호스팅본에서 확인했다. 각 링크는 사용한 문서의 버전을 포함한다.
|
||||
- 저장소의 [동해석 기준 입력](../../reference/shellNL-dyn-meshfine/shellNL-dyn-meshfine.inp)
|
||||
3행은 `Abaqus/CAE Learning Edition 2024` 생성 표기다. 이는 실제 solver 실행 버전이나
|
||||
적용된 모든 제어값을 증명하지 않는다.
|
||||
- 2024판의 대응 문서 주소는 조사 환경에서 HTTP 403으로 열리지 않았다. 2025값을
|
||||
2024에서 확인한 값으로 보고하지 않는다.
|
||||
- 해당 기준 폴더에는 INP와 변위 CSV만 있고 `.msg`/`.dat`는 없다. 공식 지침에 따르면
|
||||
실제 적용 제어값은 이 두 로그에서 확인할 수 있다.[S1]
|
||||
- 따라서 현재 확정 가능한 것은 **2025 문서 기본값과 FESA 코드의 차이**다.
|
||||
기준 해석과 완전히 같은 설정이라는 주장은 실제 실행 로그 또는 해당 버전 원문을
|
||||
확보한 뒤에만 가능하다. 이번 조사에서는 Abaqus를 실행하지 않았다.
|
||||
|
||||
## 1. 비선형 field 방정식의 기본값
|
||||
|
||||
다음은 `*CONTROLS, PARAMETERS=FIELD` 기본값이다. α는 field 종류다.
|
||||
q̃0/q̃u 외에는 무차원 비율이며 절대 힘·길이로 해석하지 않는다.[S2]
|
||||
|
||||
| 기호 | 기본값 | 역할 |
|
||||
| --- | ---: | --- |
|
||||
| Rn | 5e-3 | 최대 scaled 잔차 / 평균 flux |
|
||||
| Cn | 1e-2 | 최대 Newton 보정 / 해당 증분의 최대 변화 |
|
||||
| q̃0 | 첫 step 1e-2; 이후 이전 step 평균 | 평균 flux 초기값, 해당 flux 단위 |
|
||||
| q̃u | 미지정 | 사용자가 지정하는 고정 평균 flux, 해당 flux 단위 |
|
||||
| Rp | 2e-2 | 비이차 수렴에서 사용하는 대체 잔차 비율 |
|
||||
| ε | 1e-5 | zero-flux 분류 기준 |
|
||||
| Cε | 1e-3 | zero-flux 시 보정 비율 |
|
||||
| Rl | 1e-8 | 선형 증분 판정용 잔차 비율 |
|
||||
| Cf | 1 | 미소 field의 물리 차원 변환 비율 |
|
||||
| εl | 1e-5 | 평균 계산의 비활성 flux 구분 |
|
||||
| εd | 1e-8 | 특성길이 대비 미소 병진 증분 구분 |
|
||||
|
||||
첫 데이터 줄 순서는 `Rn, Cn, q̃0, q̃u, Rp, ε, Cε, Rl`,
|
||||
둘째 줄은 `Cf, εl, εd`다.[S2]
|
||||
|
||||
2025 문서의 `CONVERGENCE CHECK` 기본 모드는 `STRICT`다. `MODERATE`, `RELAXED`,
|
||||
`CONCEPT DESIGN`은 분모나 잔차/보정의 결합이 달라지는 별도 선택이며, 이번 기본값
|
||||
비교에서 이 모드들을 섞지 않는다.[S2]
|
||||
|
||||
## 2. 같은 숫자가 같은 판정이 아닌 이유
|
||||
|
||||
Abaqus의 보통 잔차 검사는 `max_i |r_i/(f_i q̃)| <= Rn`, 보정 검사는
|
||||
`cmax <= Cn Δumax`다. 여기서 q̃는 요소가 절점에 주는 flux와 외부 flux의 크기로
|
||||
계산한 공간 평균을 다시 증분 이력에서 평균한 값이다. 단순한 최대 외력이나
|
||||
조립 후 잔차 평균이 아니다. 종속 절점이 없는 경우 fi=1이다.[S3]
|
||||
|
||||
기본값 재현에는 다음 분기가 함께 필요하다.[S3]
|
||||
|
||||
| 상황 | 의미 |
|
||||
| --- | --- |
|
||||
| 보통 비선형 | 각 활성 field에서 잔차와 실제 보정 또는 추정 다음 보정 검사 |
|
||||
| zero flux | 공간 평균이 εq̃ 이하일 때 별도 잔차/보정 분기 |
|
||||
| 선형 증분 | 모든 field의 잔차가 Rl 이하면 보정 검사 없이 수렴 가능 |
|
||||
| 비이차 수렴 | IP 이후, 해당 조건에서 Rp와 보정 검사를 함께 적용; quasi-Newton 제외 |
|
||||
| 비활성 영역 | 작은 flux를 평균에서 제외하는 규칙으로 평균 희석 방지 |
|
||||
| 미소 field/증분 | 힘↔모멘트 특성길이 변환과 미소 병진 증분 예외 |
|
||||
|
||||
이 분기들을 생략하면 “Abaqus 기본 수치를 사용한 부분 정책”으로 표현해야 한다.
|
||||
특히 zero-flux는 **외력이 정확히 0인 경우**와 같지 않다. 평균 이력은 실패한 trial이나
|
||||
중간 Newton 반복마다 누적해서는 안 된다. FESA 도입 시 동일 trial의 평균 후보를 계산하고,
|
||||
accepted increment에서만 이력을 반영하는 설계가 필요하다.
|
||||
|
||||
## 3. MITC4 동해석에 관계되는 시간적분 기준
|
||||
|
||||
Abaqus의 half-increment residual은 증분 중간의 평형 불균형으로 시간간격의 정확도를
|
||||
판단하는 양이다. 증분 끝의 Newton 잔차 허용치가 아니다. 공식 `*DYNAMIC` 문서의 구분은
|
||||
다음과 같다.[S4]
|
||||
|
||||
| 항목 | 문서 기본값 또는 지침 | FESA와의 관계 |
|
||||
| --- | --- | --- |
|
||||
| HALFINC SCALE FACTOR | transient fidelity: 비접촉 1000, 접촉 10000 | 시간 평균 힘·모멘트에 곱하는 비율 |
|
||||
| HAFTOL | 사용자가 지정하는 힘 차원의 값 | DIRECT와 함께 지정 불가 |
|
||||
| HAFTOL 선택 지침 | 저감쇠 탄성: 보통 정확도 대표 힘의 1–10배, 높은 정확도 0.1–1배 | 기본값이 아닌 선택 지침 |
|
||||
| HAFTOL 선택 지침 | 소성 등 소산이 큰 경우: 보통 10–100배, 높은 정확도 1–10배 | 현재 FESA 탄성 shell의 직접 대응 아님 |
|
||||
| NOHAF | half-increment residual 계산 생략 | DIRECT에서도 계산은 기본 수행되며 생략 가능 |
|
||||
| DIRECT | 고정 시간간격 | HAFTOL·HALFINC SCALE FACTOR와 동시 지정 불가 |
|
||||
|
||||
Moderate dissipation과 quasi-static의 half-increment 검사는 기본적으로 억제된다.
|
||||
1000/10000은 매우 큰 Newton 허용치가 아니라 별도 시간 정확도 지표의 배율이다.[S5]
|
||||
|
||||
저장소 기준 입력 1475–1477행은 `*Dynamic,direct`와 `0.1,10.,`다.
|
||||
따라서 100개 고정 증분이며, 자동 시간간격 조절 배율을 바꾸어 이 모델의 증분 수를
|
||||
줄이는 계획은 현재 입력 계약과 다르다. `INC=1000`은 실제 증분 수가 아닌 상한이다.
|
||||
|
||||
Abaqus에는 수렴 실패 상태도 받아들이는 `DIRECT=NO STOP`이 있지만, 고정 간격을 유지하는
|
||||
`DIRECT`와 다르다. FESA의 실패 candidate 보존 계약에 반하므로 제안하지 않는다.[S6]
|
||||
|
||||
## 4. 반복·시간간격 제어값
|
||||
|
||||
아래는 허용오차 자체보다 재시도·반복 횟수에 관계되는 보조 제어값이다.
|
||||
자동 시간간격과 고정 시간간격에서 적용 방식이 다르다. 이를 FESA의 100회 상한과
|
||||
그대로 교환하면 같은 해석 절차가 되지 않는다.[S1]
|
||||
|
||||
| 제어값 | 기본값 | 의미 |
|
||||
| --- | ---: | --- |
|
||||
| I0 / IR | 4 / 8 | 발산 검사 / 수렴률 검사 시작 |
|
||||
| IP | 9 | 대체 잔차 기준을 검토하는 반복 수 |
|
||||
| IC | 16 | 연속 평형 반복 제어 상한; 세부 조건은 원문 참조 |
|
||||
| IA | 5 | 증분 시도 횟수 |
|
||||
| Df / DC | 0.25 / 0.5 | 발산 / 느린 수렴 시 자동 cutback |
|
||||
| DB / DD | 0.75 / 1.5 | 다음 증분 축소 / 빠른 수렴 시 증가 |
|
||||
| 동해석 DM | 1.25 | 자동 증분 증가의 최대 배율.[S2] |
|
||||
|
||||
이번 FESA 실험 제안에서는 먼저 기존 고정 시간간격·반복 상한을 유지하여,
|
||||
종료 판정 변경의 효과를 분리하는 편이 타당하다. 이는 조사에 따른 권고다.
|
||||
|
||||
## 5. 다른 종류의 tolerance
|
||||
|
||||
| 종류 | 확인값과 범위 | FESA 적용 판단 |
|
||||
| --- | --- | --- |
|
||||
| 반복 선형방정식 solver | 2025 기본 상대잔차 1e-6, 최대 500회.[S7] | PARDISO direct solve의 backward error와 교환 불가 |
|
||||
| 선형 잔차의 정의 | 반복 solver는 L2 잔차 `||Ku-f||/||f||`; Newton 종료와 독립.[S8] | norm·분모가 다르므로 숫자만 비교 불가 |
|
||||
| 구속 적합성 | hybrid 체적/축/전단 1e-5; distributing coupling 변위/회전 1e-5.[S2] | FESA hidden mass-null constraint와 다른 방정식 |
|
||||
| 접촉 | 적합성 5e-3, 저압 soft-contact 0.1, CONVERT SDI=YES force error 1.[S2] | 현재 지원 subset 밖; MITC4 shear/drill 기준으로 사용 금지 |
|
||||
| 열전달 | field 기준 외에 DELTMX는 사용자가 정하는 최대 온도 변화.[S9] | 현재 선형 열전달 잔차 인증의 완화 근거가 아님 |
|
||||
| 고유치 | Lanczos/AMS/Subspace별 절차·설정 구분.[S10] | FEAST의 잔차·직교성 1e-10 대응값으로 임의 채택 금지 |
|
||||
| 외부 결과 비교 | Abaqus Newton 기준과 다른 목적 | 기존 FESA U/RF/S 등의 비교 허용치는 유지 |
|
||||
|
||||
조사 범위는 FESA 구조해석, 특히 MITC4 implicit dynamics에 관련되는 기준이다.
|
||||
접촉·hybrid는 경계 설명용이며, Abaqus 전체 제품의 재료별 적분·최적화·손상·확산·
|
||||
Explicit 안정시간 기준을 모두 열거한 문서는 아니다. 공개 문헌에서 확인하지 않은
|
||||
Abaqus 내부 직접분해·pivot·rank 판정 숫자는 추측하지 않았다.
|
||||
|
||||
## 6. 조사 시점 FESA 기준과 소유 위치
|
||||
|
||||
| 대상 | 조사 시점 판정 | 근거/실험 시 취급 |
|
||||
| --- | --- | --- |
|
||||
| MITC4 비선형 정적 | 잔차·보정 상대 1e-2; 외력 또는 증분 scale=0에서 typed absolute 1e-3 | [공개 기준](../../include/fesa/analysis/nonlinear_static_control.h), [실제 분기](../../src/fesa/analysis/nonlinear_static_control.cpp); 동해석과 별도 |
|
||||
| Shell HHT 외부 Newton 잔차 | 병진/회전 별 `max|r|/max(scale) <=1e-10` | [조립과 scale](../../src/fesa/analysis/hht_dynamic_assembly.cpp), [ResidualRecord](../../src/fesa/analysis/hht_reduced_procedure.cpp) |
|
||||
| Shell HHT 외부 Newton 보정 | 병진은 L, 회전은 1로 나눈 δq·hδv·h²δa·h²δA 모두 1e-10 | 같은 파일 `CorrectionRecord`; Abaqus의 증분 대비 보정과 다름 |
|
||||
| Shell HHT 내부 위치 root | hidden residual 1e-10, 회전 보정 1e-10 등; 최대 100 correction | [SolveEmbeddingRoot](../../src/fesa/analysis/hht_reduced_operators.cpp); 별도 구속 정밀도 |
|
||||
| 회전만 추가 보정하는 solve 경로 | 병진 통과/회전 미통과와 병진 보정량의 1e-10에 의존 | 같은 파일 `SolveRotationOnlyCorrection`; 종료 정책과 결합됨 |
|
||||
| 초기 frame0 일관성 | 초기·재구성 full 잔차 1e-10 | [procedure](../../src/fesa/analysis/hht_reduced_procedure.cpp); 양의 증분 종료와 분리 |
|
||||
| HHT 선형 solve | scaled backward error 1e-12 | [CheckLinearSolve](../../src/fesa/analysis/hht_dynamic_assembly.cpp); 유지 대상 |
|
||||
| HHT 전역 힘·모멘트 평형 | 각각 normalized 1e-8 | 같은 파일 `CompleteEquilibrium`; 외부 Newton 완화 후에도 남는 필수 acceptance |
|
||||
| 이력 update·basis·pair 산술 | same-unit update 1e-12 및 exact-zero/정확 반올림 검사 | [procedure](../../src/fesa/analysis/hht_reduced_procedure.cpp), [pair 검사](../../src/fesa/analysis/hht_reduced_increment.h); 유지 대상 |
|
||||
| hidden 위치/rate·rank·null | 별도 1e-10/1e-12 등의 물리·대수 검사 | [algebra](../../src/fesa/analysis/hht_dynamic_algebra.cpp), [기능 요약](../hht-implicit-dynamics/README.md); 일괄 완화 제외 |
|
||||
| 선형 정적·고유치·열 | 각 기능별 독립 기준 | 이번 MITC4 동해석 실험의 자동 변경 대상 아님 |
|
||||
|
||||
조사 당시 HHT의 scale은 외력과 요소별 physical/drilling force, convective inertia 및 질량×가속도
|
||||
항들의 **상쇄 전 크기**를 누적한다. Abaqus의 q̃와 동일하지 않다. 당시 보정 기준도
|
||||
`δq/Δq`가 아니라 특성길이·시간간격에 기반하므로 `1e-10 → 0.01`은 단순 완화 이상의
|
||||
의미 변경이다. 정적은 이미 상대 1e-2이므로 Abaqus의 Rn=5e-3 채택이 숫자상 더 엄격할
|
||||
수도 있다. “FESA 전체가 Abaqus보다 엄격하다”로 일반화할 수 없다.
|
||||
|
||||
현재 실험 후보는 이 표의 외부 종료를 field 평균·증분 대비 보정으로 교체했고 기존 strict
|
||||
잔차·q/v/a/A 크기는 진단으로 보존했다. 내부 산술·frame0·전역 평형은 별도로 유지한다.
|
||||
|
||||
## 7. 성능 가설과 적용 전 확인
|
||||
|
||||
확인된 사실은 엄격한 HHT 종료 기준과, 외부 Newton 외에도 비용이 큰 내부 root·
|
||||
요소 도함수·조립 경로가 존재한다는 점이다. 허용치가 실제 장시간 실행의 주원인인지는
|
||||
조사 당시에는 측정하지 않았다. 이후 같은 local 근사본의 strict 공식 실행은
|
||||
6350.7895521초/native0을 확보했다. T6/T7에서는 물리 실패로 후보100증분을 실행하지 않았으나,
|
||||
후속 사용자 요청의 변위 비교에서는 Release 후보 원본을 5662.144854초/native0으로 완료하고 U PASS를 확인했다.
|
||||
첫 증분의 호출 수 감소만으로 전체 병목이나 가속을 확정하지 않는다.
|
||||
[기존 성능 기록](../calculation-performance/README.md)의
|
||||
6372.325621초는 과거 source의 단일 baseline으로 최신 revision의 측정값이 아니다.
|
||||
|
||||
추가 실험에서 전체 증분의 외부/내부 반복 수, 요소 평가 수, factor/solve 수, 단계별 시간과
|
||||
종료를 지연시킨 채널을 함께 측정해야 한다. 내부 구속 solve가 대부분의 시간을 쓰면
|
||||
외부 Newton 기준만 바꿔도 효과가 작을 수 있다. 기준을 넓혀 성공한 것과 해의 정확도가
|
||||
유지된 것을 구분하고, U 비교와 별도로 V/physical a·구속·시간수렴을 확인해야 한다.
|
||||
|
||||
적용 단계와 승인 경계는 [적용 계획](application-plan.md)에 정리한다.
|
||||
|
||||
## 출처
|
||||
|
||||
아래는 모두 Abaqus 2025 공식 문서의 공개 호스팅본이다. 실제 2024 실행값 확인은 남아 있다.
|
||||
|
||||
[S1]: https://docs.software.vt.edu/abaqusv2025/English/SIMACAEANLRefMap/simaanl-c-convergecontrol.htm "Commonly Used Control Parameters"
|
||||
[S2]: https://docs.software.vt.edu/abaqusv2025/English/SIMACAEKEYRefMap/simakey-r-controls.htm "*CONTROLS"
|
||||
[S3]: https://docs.software.vt.edu/abaqusv2025/English/SIMACAEANLRefMap/simaanl-c-convergcriteria.htm "Convergence Criteria for Nonlinear Problems"
|
||||
[S4]: https://docs.software.vt.edu/abaqusv2025/English/SIMACAEKEYRefMap/simakey-r-dynamic.htm "*DYNAMIC"
|
||||
[S5]: https://docs.software.vt.edu/abaqusv2025/English/SIMACAECAERefMap/simacae-t-simconfiguredynamicimplicit.htm "Configuring a dynamic, implicit procedure"
|
||||
[S6]: https://docs.software.vt.edu/abaqusv2025/English/SIMACAEANLRefMap/simaanl-c-dynamic.htm "Implicit Dynamic Analysis Using Direct Integration"
|
||||
[S7]: https://docs.software.vt.edu/abaqusv2025/English/SIMACAEKEYRefMap/simakey-r-solvercontrols.htm "*SOLVER CONTROLS"
|
||||
[S8]: https://docs.software.vt.edu/abaqusv2025/English/SIMACAEANLRefMap/simaanl-c-itrsolveroverview.htm "Iterative Linear Equation Solver"
|
||||
[S9]: https://docs.software.vt.edu/abaqusv2025/English/SIMACAEKEYRefMap/simakey-r-heattransfer.htm "*HEAT TRANSFER"
|
||||
[S10]: https://docs.software.vt.edu/abaqusv2025/English/SIMACAEKEYRefMap/simakey-r-frequency.htm "*FREQUENCY"
|
||||
@@ -0,0 +1,153 @@
|
||||
# MITC4 HHT에 Abaqus 수렴 규칙을 적용하는 실행 계획
|
||||
|
||||
2026-10-19 후속 사용자 승인으로 이번 실험은 변위 검증 통과·실패10개 보류로 조건부 완료한다.
|
||||
실제 기준·FAIL/SKIP을 변경하지 않았으며, 아래는 최초 계획과 재현 조건을 보존한 기록이다.
|
||||
후속 후보100증분 U 비교와 전체 상태·관측 시간 정리는 [최종 결과](results.md#후속-승인과-남은-단계의-완료),
|
||||
수용 범위는 [결정 기록](../tracking/decisions/0011-tolerance-experiment-acceptance.md)을 따른다.
|
||||
|
||||
작성일: 2026-10-18. 조사 기준 source: 43867d0.
|
||||
2026-10-19 승인된 실행 결과: 외부 field 규칙을 실제 HHT 종료에 연결하고 strict 기준 실행,
|
||||
1증분 후보 진단, 두 Debug 경로 전체 검사와 별도 비교를 수행했다.
|
||||
Native/CTest는 각각987 PASS·10 FAIL·1 SKIP이다. 물리 실패 분기에 따라 공식 후보100증분은
|
||||
미실행이며 전체 가속·정식 채택은 미확정이다. 상세 실행 증거는 [실험 결과](results.md)에 있다.
|
||||
아래는 재현할 실행 순서와 유지 조건이며, 미충족 검증을 완료로 바꾸지 않는다.
|
||||
|
||||
2026-10-18 후속 사용자 지시로 별도 [요소 local 근사 실험](local-kernel-experiment.md)을
|
||||
먼저 적용했다. 아래 요소 수식·질량 보존 조건의 명시적 예외는 polar 1회, 회전/Jacobian
|
||||
3차 급수, 질량 검사 1e-3이다. 후속 지시로 관련 관성·접선·HHT 물리 테스트 오차도
|
||||
1e-3으로 완화했지만 기존 시간수렴 차수·실제 해석 controller는 유지했다.
|
||||
이는 Abaqus 기본값이 아니며 정적 비선형 MITC4에도
|
||||
polar 변경이 공유된다. 외부 수렴 규칙의 효과를 분리하려면 strict/candidate 양쪽에 같은
|
||||
local kernel을 사용하고 원래 43867d0 기준과의 차이도 별도로 기록해야 한다.
|
||||
|
||||
## 목표와 확정한 방식
|
||||
|
||||
MITC4 implicit dynamics의 외부 Newton 종료에 **Abaqus/Standard 2025의 공개 STRICT field
|
||||
수렴 규칙**을 적용한다. 숫자만 바꾸거나 기존 strict 종료를 계속 사용하는 방식으로 축소하지 않는다.
|
||||
근거는 [조사 보고서](README.md)다. 2024 생성 INP의 실제 solver 설정 미확인은 별도 한계로
|
||||
기록하며, 그 로그가 없다는 이유로 2025 기준 후보 구현을 보류하지 않는다.
|
||||
|
||||
T1의 [외부 field 수렴 정식화](formulation-convergence.md)가 T3/T4의 수치·분기·로그 계약이다.
|
||||
요소 signed total과 source occurrence 개수, current trial 평균 후보와 accepted commit,
|
||||
보정/증분 chart, 비이차 검출의 명시적 FESA 대응 및 독립 oracle 경계를 이 문서에 고정했다.
|
||||
이 수식 계약은 실제 후보에 연결했다. 후보의 비선형 시간 정확도와 전체 물리 검증은 미완료다.
|
||||
|
||||
공개 규칙을 FESA의 기존 물리량에 대응하는 실험이다. 잔차는 기존 full free endpoint
|
||||
E=I+fint-F, field는 병진 u와 total rotation-vector φ, 회전 flux는 generalized moment다.
|
||||
요소의 total internal+physical inertia flux와 외부 source contribution을 평균에 사용하고
|
||||
상쇄 전 개별 항 합인 기존 residual_scale과 혼동하지 않는다. 미소 field 변환의 특성길이는
|
||||
기존 ReferenceLength의 초기 shell 최대 edge 길이에 대응한다.
|
||||
Abaqus의 비공개 요소·관성·회전 처리와 내부 특성길이까지 같다고 보장하지 않는다.
|
||||
|
||||
대상은 MITC4 HHT 양의 증분이다. 정적·Hex8·열·고유치의 tolerance, 요소 수식·질량·HHT 적분식,
|
||||
시간간격 dt=0.1/T=10/100증분 및 기존 최대100 correction은 유지한다.
|
||||
|
||||
## 변경하는 판정과 유지하는 검사
|
||||
|
||||
| 변경하는 부분 | 유지하는 부분 |
|
||||
| --- | --- |
|
||||
| 외부 Newton 잔차·보정의 종료 판정 | frame0 초기 일관성 |
|
||||
| 힘·모멘트 평균과 accepted 증분 이력 | 내부 graph/hidden constraint·rank/null |
|
||||
| 일반·zero·linear·추정 보정·비이차 수렴 분기 | 선형 backward error1e-12·update1e-12·pair/rounding |
|
||||
| q/φ의 증분 대비 보정으로 종료 | PrivateCorrectionEqual·physical rate 복원 |
|
||||
| 기존 q/v/a/A1e-10 크기는 strict 비교 진단 | 전역 힘·모멘트 평형 각각1e-8·finite·영 구속 |
|
||||
| 의미가 달라지는 후보 로그 hht-log-v2 | CLI·지원 INP·HDF5 schema2·최종 결과 보호 |
|
||||
|
||||
Rn=.005, Cn=.01, Rp=.02, ε=1e-5, Cε=.001, Rl=1e-8,
|
||||
Cf=1, εl=1e-5, εd=1e-8, IP=9와 첫 step q̃0=.01을 사용한다.
|
||||
힘/모멘트별 일반 규칙과 예외를 함께 구현한다. 보정의 분모는 predictor 차이가 아니라
|
||||
직전 accepted endpoint부터 현재 trial까지의 증분이다.
|
||||
|
||||
평균 후보는 현재 trial에서 다시 계산하고, 모든 acceptance가 끝난 증분에서 한 번 commit한다.
|
||||
외부 field 수렴 후에도 유지 검사가 실패하면 정상 결과를 공개하지 않는다. 자동 완화,
|
||||
숨은 strict 재시도·polishing, 기존 HDF5 재사용으로 성공을 만들지 않는다.
|
||||
회전 전용 correction의 기존 strict 선택 자격은 유지하고 후보 field pass와 혼용하지 않는다.
|
||||
|
||||
## 실제 실행 순서와 완료 증거
|
||||
|
||||
| 순서 | 실제 작업과 주요 대상 | 완료 증거 |
|
||||
| --- | --- | --- |
|
||||
| T1 | 공개 규칙·FESA 대응·분기 시점 고정. formulation-convergence.md 작성 | 수식/분기/단위/차이와 독립 수치 검토 |
|
||||
| T2 | strict source·Release 실행물·DLL 보존, 기준 실행과 개발 계측 | hash·native exit·새 결과·U비교·elapsed·비용 기록 |
|
||||
| T3 | analysis-private field 수렴 계산과 평균 이력 구현 | 독립 oracle의 경계·예외·rollback 단위 검사 |
|
||||
| T4 | HHT 실제 종료·accepted 이력·로그 연결 | 실제 조기 종료·후속 검사·최종 파일 보호·v2 reader 검사 |
|
||||
| T5 | 실제 후보 경로의 물리·시간 정확도 검사 | q/v/physical a·hidden·회전·C08a/C08b·기존 N7 판정 |
|
||||
| T6 | 후보 Release 공식 측정·개발 계측·정식 HHT 비교 | 기준과 같은 조건의 elapsed/반복/비용·새 HDF5·비교 결과 |
|
||||
| T7 | 전체 직접 MSVC Debug·CMake/CTest·Python·필수 reference | 전체 discovery/실행 identity·exit·raw/XML·비교 증거 |
|
||||
| T8 | 결과와 한계·채택 판단 문서 갱신 | results.md와 관련 README/contracts/status/findings의 근거 일치 |
|
||||
|
||||
순서는 T1→T2→T3→T4→T5→T6→T7→T8이다. 코드 의미 변경 전에 기준을 보존하고,
|
||||
측정 중 다른 build/test를 실행하지 않도록 직렬 수행한다. 새 source/test는 직접 MSVC와
|
||||
CMake의 명시적 목록에 함께 등록한다. 같은 source·build tree를 여러 담당자가 동시에 수정하지 않는다.
|
||||
|
||||
## 코드와 검사 범위
|
||||
|
||||
핵심 변경 위치는 src/fesa/analysis의 hht_dynamic_assembly.cpp,
|
||||
hht_reduced_procedure.cpp와 관련 private 선언, 필요 시 hht_reduced_operators.cpp다.
|
||||
새 field 계산은 hht_field_convergence.h/.cpp 등의 최소 private 단위로 둔다.
|
||||
로그 변경은 공개 newton_iteration_log.h와 대응 구현·검사·contracts에 함께 반영한다.
|
||||
범용 runtime tolerance 설정이나 새 CLI option, *CONTROLS parser는 추가하지 않는다.
|
||||
|
||||
단위 검사는 상쇄되는 요소 flux, 비활성 영역, 비균일 절점 연결, 첫 평균,
|
||||
분모0/NaN/Inf, 경계의 아래/같음/위, zero-flux·선형·추정·비이차 분기, 이력 commit/rollback을 포함한다.
|
||||
통합 검사는 판정량 출력만 달라진 구현을 걸러낼 수 있도록 실제 후보 종료를 확인한다.
|
||||
CorrectionRecord에서 strict 크기 gate를 분리하면서 PrivateCorrectionEqual까지 없애지 않도록 검사한다.
|
||||
|
||||
hht_reduced_shell_test.cpp, hht_dynamic_procedure_test.cpp,
|
||||
hht_time_integration_test.cpp, hht_temporal_verification_test.cpp,
|
||||
hht_shell_physics_test.cpp, hht_energy_verification_test.cpp와 state/writer 검사를 수행한다.
|
||||
기존 strict 상수를 사용하는 algebra fixture와 실제 후보 production 경로의 검증을 구분한다.
|
||||
|
||||
## 성능 측정 방법
|
||||
|
||||
공식 elapsed는 계측 없는 baseline/candidate Release 실행을 같은 host·최적화/산술 옵션·
|
||||
runtime·thread·전원 조건에서 각각1회 측정한다. Process 시작 직전부터 정상 종료·최종 HDF5
|
||||
공개까지를 포함하며 build·입력 복사·비교는 제외한다. 동일한 개발 probe를 적용한 별도 실행으로
|
||||
outer/inner correction, 요소 평가, factor/solve/RHS 수와 중복 없는 단계 시간을 수집한다.
|
||||
Probe를 추가하더라도 공식 실행에서는 꺼져야 하며 수학 계산·평가 횟수를 바꾸지 않는다.
|
||||
|
||||
결과는 elapsed, Δ초, 변화율, baseline/candidate speedup과 정확도를 함께 제시한다.
|
||||
과거6372초·계측 CPU sample 합·여러 실행 중 최솟값을 공식 baseline으로 사용하지 않는다.
|
||||
기존100초 목표는 참고이며 이번 실험이 그 달성을 약속하는 것은 아니다.
|
||||
|
||||
정식 reference/shellNL-dyn-meshfine의 hash 확인 새 작업 복사본에서 실행한다.
|
||||
전체729-node/101-frame schema·identity를 먼저 확인한 뒤 기존9-node U blocking/UR warning-only
|
||||
비교를 수행한다. Near-zero1%·일반 행5%·RMS1%·zero-scale exact-zero를 유지한다.
|
||||
후보/기준 전체 q/v/a/energy 차이는 별도 진단이며, CSV에 없는 V/a/energy의 외부 검증을 대신하지 않는다.
|
||||
|
||||
## 정확도·전체 회귀·실패의 처리
|
||||
|
||||
q/v/physical a의 마지막 두 refinement 감소·차수≥1.8과 기존 N7 해상도 판정을 유지한다.
|
||||
후속 승인에 따른 관련 테스트 오차는1e-3이다. 기존 strict algebra의 finest1e-4 검사도
|
||||
그대로 통과했으나 이를 실제 비선형 후보의 정확도 증거로 대신하지 않는다.
|
||||
C08b 미판정이나 일반 비선형 HHT의 energy 의미를
|
||||
결과에 맞춰 다시 정의하지 않는다. 기존 물리 기준을 통과하지 못하면 수치 부적합으로 보고한다.
|
||||
|
||||
관련 검사 후 전체 MSVC x64 Debug build와 unit/integration/reference 세 executable,
|
||||
보조 CMake/CTest 전체 및 별도 Python suite를 실제 실행한다. 환경·명령은
|
||||
[solver-core](../solver-core/README.md#빌드와-직접-검사)를 따른다. 짧고 고유한 TMP/TEMP,
|
||||
raw/XML과 discovery를 보존하고 상속 filter/sharding을 제거한다.
|
||||
|
||||
정식 fine·추가 fine·pressure 등 자료가 없으면 미완료다. _02의 전용 비교 계약 미구현도 구분한다.
|
||||
reference/temp로 대체하거나 fixture 기대값을 완화·삭제하지 않는다. Skip·중단·발견0건은 PASS가 아니다.
|
||||
기준에서도 재현되는 실패와 후보가 만든 실패를 구분한다. 범위 안 결함은 수정·재검증하고,
|
||||
유지하기로 한 물리·acceptance 변경이 필요하면 근거와 영향을 보고한다.
|
||||
|
||||
실패 때문에 완결 HDF5가 없으면 성공 실행시간 비교나 reference PASS는 성립하지 않는다.
|
||||
입증된 같은 수치 실패의 긴 실행을 새 근거 없이 반복하지 않는다. 가능한 독립 검사와
|
||||
결과/한계 기록은 수행하되 실행하지 못한 단계를 전체 완료로 표시하지 않는다.
|
||||
|
||||
## 산출물·판정과 실행 권한
|
||||
|
||||
원시 실행물·DLL·stdout/stderr·XML·HDF5·비교 JSON·환경/입력 hash·elapsed는
|
||||
.build/tolerance-experiment/<run-id>/의 기준/후보별 위치에 보존한다.
|
||||
추적 요약은 docs/abaqus-tolerance/results.md에 작성하고 HHT README·관련 contracts·
|
||||
tracking/status와 findings 및 필요한 결정 기록을 실제 결과에 맞춘다.
|
||||
|
||||
결론은 **개선 확인 / 속도 미개선 / 수치 부적합 / 검증 미완료**로 구분한다.
|
||||
개선 확인에는 필수 수치 검증과 동일 조건 후보 elapsed<기준 elapsed가 모두 필요하다.
|
||||
단일 표본의 개선을 통계적 보장으로 확대하지 않는다. 코드 구현 완료와 전체 검증 완료를 분리한다.
|
||||
|
||||
승인 후 로컬 코드·검사·빌드·FESA 실행은 이 계획의 범위이며 단계별 승인을 다시 요구하지 않는다.
|
||||
외부 Abaqus·라이선스 비용·원격 전송·release·최종 병합은 제외한다.
|
||||
실험 revision의 후보 적용과 주 개발 branch의 정식 기본값 채택을 구분하며 최종 병합은 사용자가 결정한다.
|
||||
@@ -0,0 +1,357 @@
|
||||
# MITC4 HHT 외부 field 수렴 정식화
|
||||
|
||||
작성일: 2026-10-18. 코드 대조 revision: `d7f6d0c`.
|
||||
정책 ID: `abaqus2025-field-on-fesa-v1`. 상태: 구현 계약이며 구현·수치 통과 보고가 아니다.
|
||||
|
||||
## 적용 범위와 근거
|
||||
|
||||
이 문서는 [적용 계획](application-plan.md)의 T1 산출물이다. 양의 증분의
|
||||
nodal-lumped-kinetic / reduced-coordinate / fixed-null MITC4 HHT 외부 Newton 종료만
|
||||
교체한다. 통상 `fesa.exe <input>` 경로가 이 후보를 실제 사용해야 한다. runtime option,
|
||||
`*CONTROLS` parser, 정적·Hex8·열·eigen 변경은 없다. dt=.1, T=10, 100증분과
|
||||
최대100 outer correction은 유지한다. frame0은 기존 strict 절차다.
|
||||
|
||||
2026-10-18 아래 공식 2025 페이지를 직접 열고 확인했다.
|
||||
|
||||
- [S1 Commonly Used Control Parameters](https://docs.software.vt.edu/abaqusv2025/English/SIMACAEANLRefMap/simaanl-c-convergecontrol.htm):
|
||||
field 제어와 반복 제어의 구분 및 IP.
|
||||
- [S2 *CONTROLS](https://docs.software.vt.edu/abaqusv2025/English/SIMACAEKEYRefMap/simakey-r-controls.htm):
|
||||
STRICT 기본 모드와 FIELD 두 데이터 줄.
|
||||
- [S3 Convergence Criteria for Nonlinear Problems](https://docs.software.vt.edu/abaqusv2025/English/SIMACAEANLRefMap/simaanl-c-convergcriteria.htm):
|
||||
Terminology, Average Flux, Residuals, Zero Flux, Negligible Response,
|
||||
Linear Increments, Nonquadratic Convergence.
|
||||
|
||||
S2의 기본값은 Rn=.005, Cn=.01, Rp=.02, ε=1e-5, Cε=.001, Rl=1e-8,
|
||||
Cf=1, εl=1e-5, εd=1e-8이고 첫 step의 초기 평균은 각 flux 단위로 .01이다.
|
||||
IP=9이며 평균 override는 없다. 이하 식은 공개 규칙을 FESA 물리량에
|
||||
대응한 것이다. 특히 **비이차 검출·평균 초기화·occurrence 활성화·분기 우선순위는
|
||||
아래의 명시적 FESA 선택**이다. Abaqus의 비공개 구현과 동일하다는 주장이 아니다.
|
||||
2024 생성 INP의 실제 solver 설정은 .msg/.dat 부재로 미확인이다. 이를 기다리며
|
||||
2025 후보 구현을 중단하지 않는다.
|
||||
|
||||
## 물리량과 소유권
|
||||
|
||||
field α=F는 global 병진 u, α=M은 global total rotation-vector φ다.
|
||||
둘 다 모델에 존재하면 모든 활성 field가 통과해야 한다.
|
||||
활성 field는 해당 종류의 **free full DOF가 하나 이상**인 field다.
|
||||
all-constrained field는 검사 대상 없음으로 기록하고 전체 AND의 항등원으로만 취급한다.
|
||||
모델 flux 모집단에서는 constrained DOF도 제거하지 않는다.
|
||||
|
||||
같은 trial (q,v,physical a)의 기존 endpoint 잔차를 그대로 쓴다.
|
||||
|
||||
```text
|
||||
E = I(q,v,a) + fint(q) - F
|
||||
Jα = 해당 field의 full free DOF 집합
|
||||
ρα(i) = max(j∈Jα) |E_j(i)| [force 또는 moment]
|
||||
```
|
||||
|
||||
종속 절점이 없는 지원 subset이므로 S3의 fi=1이다. fixed-null chart의 reduced
|
||||
좌표/hidden 변수는 종속 절점 수가 아니다. reduced projected residual만으로 통과시키지
|
||||
않는다. 회전의 E·flux·보정은 φ와 켤레인 generalized moment/rotation chart다.
|
||||
`Jl^-T`로 변환한 spatial reaction moment나 lever-arm global moment를 섞지 않는다.
|
||||
|
||||
증분 n의 iteration i는 완료된 outer correction 수다. i=0은 선택된 시작 trial이며
|
||||
predictor와 old-position 시작점의 비교 평가 중 **선택된 하나만** 이력 표본이다.
|
||||
후속 trial 재구성까지 마친 full q(i)를 사용한다.
|
||||
|
||||
```text
|
||||
Dα(i) = max(j∈Jα) |q_j(i) - q_j(n-1,accepted)|
|
||||
cα(i) = max(j∈Jα) |q_j(i) - q_j(i-1)| (i>=1만 정의)
|
||||
```
|
||||
|
||||
q의 회전 성분은 total φ다. 두 차분 모두 같은 고정 full chart에서 계산한다.
|
||||
i-1은 같은 증분의 직전 **선택된 평가 trial**이며 accepted 이전 증분이나 predictor가
|
||||
아니다. full 재구성 후 차분을 써서 nonlinear graph root 변화도 포함한다.
|
||||
`MapAugmentedCorrection.coordinate`의 접선 lift만을 실제 endpoint 차분으로
|
||||
간주하지 않는다. 병진 단위는 입력 길이, 회전은 rad다. V/a/A의 차분은 이 c가 아니다.
|
||||
i=0의 c 및 추정 보정은 미평가이며 0으로 채워 통과시키지 않는다.
|
||||
|
||||
기존 [ReferenceLength](../../src/fesa/analysis/hht_dynamic_assembly.cpp)는 초기 shell의
|
||||
네 edge 길이 중 모델 최대 L을 계산하고 유한 양수인지 검사한다.
|
||||
L을 아래 차원 변환과 미소 병진 예외에 그대로 사용한다. Abaqus 내부 특성길이와
|
||||
같다고 가정하지 않는다. 단위계 변환 때 L·힘·모멘트가 함께 변환되어야 한다.
|
||||
|
||||
## 공간 평균: 조립 전 signed total과 source 개수
|
||||
|
||||
같은 trial의 각 요소 e, local node a, field component k에서 다음 scalar occurrence를 만든다.
|
||||
|
||||
```text
|
||||
x(e,a,k) = fint_e(a,k) + I_e(a,k)
|
||||
= f.internal_force[row] + m.force[row]
|
||||
b(e,a,k) = |x(e,a,k)|
|
||||
```
|
||||
|
||||
`f.internal_force`에는 기존 physical+drilling 성분이 포함되고,
|
||||
`m.force`는 같은 physical a의 M a+convective inertia다.
|
||||
**요소 내부에서 signed total을 먼저 더하고 그 후 abs**를 취한다.
|
||||
`|fint|+|I|`, 전역 조립 후 `|Σx|`, 기존 `residual_scale`의
|
||||
`|physical|+|drill|+|convective|+Σ|M_rc*a_c|`는 대체물이 아니다.
|
||||
요소 연결 수가 다른 절점도 node 평균으로 압축하지 않는다.
|
||||
|
||||
외부 occurrence는 `Load::ComputeContributions`가 반환하는 source별 scalar contribution
|
||||
하나마다 `b=|entry.value|` 하나를 만든다. 같은 full DOF의 서로 반대 load도
|
||||
두 개로 남긴다. source load 순서, 그 안의 target/contribution 순서를 보존한다.
|
||||
반환된 명시적 0 contribution도 모집단에 포함한다. 반환되지 않은 방향의 0 load를
|
||||
인위적으로 추가하지 않는다. 현 지원 subset은 일정 global 병진 CLOAD뿐이다.
|
||||
|
||||
field별 목록 Bα는 요소 source 순서→local node→component 순서, 그 뒤 외부 source
|
||||
순서다. 각 요소는 해당 field의 12개 scalar를 제공한다. constrained 포함이며
|
||||
없는 field의 entry는 만들지 않는다. 병렬 worker는 index별 응답만 만들고 이 순서대로
|
||||
serial reduction한다. source identity를 내부 index 값으로 바꾸지 않는다.
|
||||
|
||||
```text
|
||||
mα(i) = max Bα(i)
|
||||
Hmaxα = 이전 accepted 증분들의 mα 산술평균; 표본 없으면 0
|
||||
Aα(i) = {b∈Bα(i) : b >= εl*Hmaxα}
|
||||
Busedα = Aα(i), if mα(i) >= .1*Hmaxα
|
||||
Bα(i), otherwise
|
||||
bbarα(i) = sum(Busedα)/count(Busedα)
|
||||
```
|
||||
|
||||
비활성화는 **scalar flux occurrence 단위**로 한다. 한 global DOF에 붙은 서로 다른
|
||||
요소 occurrence의 활성 상태를 합치지 않는다. 공개 문헌은 mixed incidence의 세부
|
||||
marking을 제공하지 않으므로 이것은 element-node-component 모집단을 보존하는
|
||||
FESA 선택이다. 같은 값/threshold에서는 활성이다. Hmax=0이면 0도 활성이라
|
||||
첫 trial은 전체 모집단이다. m<.1Hmax에서는 전체를 다시 활성화한다.
|
||||
count=0이면 평균을 나누지 않고 invalid-flux-population 실패다.
|
||||
m=0,Hmax>0은 전체 재활성화로 bbar=0이다. 활성 DOF 없는 field도 가능한 한
|
||||
모집단 진단을 계산하되 그 성공으로 free residual 검사를 대신하지 않는다.
|
||||
|
||||
## 시간 평균 후보와 field 차원 변환
|
||||
|
||||
field별 committed 이력은 (Sα,Nα,Kα,Mα)다.
|
||||
S,N은 비zero accepted bbar의 합/개수, K,M은 모든 accepted 증분의 m 합/개수다.
|
||||
Hmax=K/M (M>0)이며 M=0이면 0이다. frame0은 표본이 아니다.
|
||||
합·count 및 아래 산술은 유한/overflow를 검사한다.
|
||||
|
||||
각 iteration에서 **같은 committed 이력**으로 다음 후보를 다시 만든다.
|
||||
|
||||
```text
|
||||
baseα = Sα/Nα (Nα>0), else .01
|
||||
Nα=0: eligibleα = (bbarα > ε*baseα)
|
||||
Nα>0: tα = (Sα+bbarα)/(Nα+1)
|
||||
eligibleα = (bbarα > ε*tα)
|
||||
qtildeα = (Sα+bbarα)/(Nα+1), if eligibleα
|
||||
baseα, otherwise
|
||||
```
|
||||
|
||||
N=0에서 initial .01을 보존하는 조건과 N>0에서 current-included 평균을 먼저 시험하는
|
||||
조건을 구별한다. 이는 S3의 초기값 유지 및 자기참조적인 nonzero 표본 정의를
|
||||
결정적으로 풀기 위한 FESA 규칙이다. qtilde는 iteration마다 달라질 수 있지만
|
||||
S,N,K,M은 증분 도중 불변이다. 과거 eligible 여부는 재분류하지 않는다.
|
||||
비zero 증분은 시간폭 가중이 아닌 **증분 표본 산술평균**이다.
|
||||
첫 step만 지원하므로 이전 step 전달은 이번 구현 대상이 아니다.
|
||||
|
||||
최종 accepted trial의 eligible에 대해 pending 이력은 다음과 같다.
|
||||
|
||||
```text
|
||||
S' = S+bbar, N' = N+1, if eligible
|
||||
S, N, otherwise
|
||||
K' = K+m, M' = M+1
|
||||
```
|
||||
|
||||
따라서 nonzero 평균만 거르는 S/N과 모든 accepted 최대 flux를 평균하는 K/M은
|
||||
서로 다른 모집단이다. 실패 또는 미완료 증분에서는 네 값 모두 이전 값 그대로다.
|
||||
|
||||
먼저 두 field의 raw qtilde를 독립 계산하고 다음 치환을 **동시에 한 번만** 한다.
|
||||
|
||||
```text
|
||||
convF = Cf*qtildeM/L
|
||||
convM = Cf*L*qtildeF
|
||||
sα = convα, if bbarα <= qtildeα < convα and 다른 field 활성
|
||||
qtildeα, otherwise
|
||||
zα = (bbarα <= ε*sα)
|
||||
να = ρα/sα
|
||||
```
|
||||
|
||||
s는 해당 iteration의 판정 scale이다. 치환된 s를 상대 field 변환이나 committed
|
||||
평균에 되먹이지 않는다. 이 동시 평가 순서와 모든 잔차 분기에 같은 s를 쓰는 것은
|
||||
FESA의 명시적 대응이다. `time_average_flux`는 raw qtilde,
|
||||
`residual_scale`은 치환 후 s를 기록해 차이를 숨기지 않는다.
|
||||
εl 활성화는 Hmax에만 의존하며 s로 다시 계산하지 않는다.
|
||||
|
||||
필수 s는 유한 양수여야 한다. 음수·NaN·Inf·overflow는 즉시 실패이며
|
||||
0/0을 0으로 만들지 않는다. ratio 대신 양수 scale에 threshold를 곱해 비교해도
|
||||
곱셈 유효성을 확인한다. absolute floor, `max(1,scale)`, clamp는 없다.
|
||||
|
||||
## 보정 추정과 비이차 검출
|
||||
|
||||
정상 분기에서 i>=2이고 같은 증분의 ρα(i-1),ρα(i-2)가 모두 유효할 때만
|
||||
다음을 정의한다. qtilde*normalized residual은 fi=1에서 원래 ρα다.
|
||||
|
||||
```text
|
||||
denα = min(ρα(i-1), ρα(i-2))
|
||||
cestα(i) = [ρα(i)/denα] * cα(i) (denα>0)
|
||||
```
|
||||
|
||||
이는 S3의 다음 Newton correction 추정식에 대응한다. missing history 또는 den=0이면
|
||||
추정은 미평가다. 이 경우 실제 보정으로 통과할 수 있지만 추정 unavailable을
|
||||
면제/통과로 간주하지 않는다. 입력·중간 연산·결과의 NaN/Inf/overflow는 추정 미평가로
|
||||
숨기지 않고 전체 invalid-state 실패다. i=0의 ρ는 첫 잔차 이력 표본이며 c는 없다.
|
||||
추정값 0은 분모 양수·분자0·유효 c가 실제 존재할 때만 가능하다.
|
||||
|
||||
S3는 IP 이후 선형 속도 수렴에서 Rp 사용을 설명하지만 검출 세부 알고리즘은
|
||||
공개하지 않는다. 다음은 **Abaqus 검출식이 아닌 보수적 FESA 검출식**이다.
|
||||
|
||||
```text
|
||||
nonquadraticα(i) =
|
||||
i>9 AND ρα(i-2)>ρα(i-1)>ρα(i)>0
|
||||
AND ρα(i)/ρα(i-1) >= ρα(i-1)/ρα(i-2)
|
||||
```
|
||||
|
||||
두 번 연속 감소하면서 감소 비율이 개선되지 않는 경우만 인정한다.
|
||||
엄밀한 등비 선형 감소는 포함하고 quadratic 감소·증가·정체·zero는 제외한다.
|
||||
정규화가 변해도 ρ 이력을 써 거짓 수렴률 변화를 피한다.
|
||||
반복9는 불가, 반복10부터 가능하다. history 부족/0 분모는 false이며
|
||||
비율 산술 실패는 전체 invalid-state 실패다. quasi-Newton을 새로 추가하지 않는다.
|
||||
이 식은 일부 비이차 수렴을 더 늦게 받아들일 수 있으며 Abaqus 내부 분기 재현을
|
||||
보장하지 않는다. 검출 threshold를 임의로 추가하거나 9회 뒤 무조건 Rp로 바꾸지 않는다.
|
||||
|
||||
## 분기 우선순위와 판정
|
||||
|
||||
모든 비교는 아래의 <=, <, > 경계를 그대로 사용한다.
|
||||
candidate 상태가 유효하고 모든 활성 field의 s를 구한 뒤 다음 순서로 평가한다.
|
||||
|
||||
1. i>=1이고 **모든 활성 field**가 ν<=Rl이면 전부 `linear`:
|
||||
residual_pass=1, correction_pass=not-evaluated, field_pass=1.
|
||||
i=0에서는 한 번도 Newton correction을 하지 않았으므로 linear 면제를 쓰지 않는다.
|
||||
2. 남은 경우 각 field에서 z가 true이면 `zero-flux`:
|
||||
residual_pass=(ν<=ε). 통과하면 correction은 미평가이고 field_pass=1.
|
||||
아니면 유효 실제 c에 대해 correction_pass=(c<=Cε*D),
|
||||
field_pass=correction_pass다. **OR 규칙**이며 Cn·Rp·추정 보정을 섞지 않는다.
|
||||
3. z=false이고 nonquadratic 검출이 true이면 `nonquadratic`:
|
||||
residual_pass=(ν<=Rp), correction_pass=(유효 실제 c<=Cn*D),
|
||||
field_pass=두 pass의 AND다. 이 분기에는 cest를 사용하지 않는다.
|
||||
4. 나머지는 `normal`:
|
||||
residual_pass=(ν<=Rn),
|
||||
correction_pass=(유효 실제 c<=Cn*D OR 유효 cest<=Cn*D),
|
||||
field_pass=두 pass의 AND다.
|
||||
|
||||
S3의 미소 병진 예외는 normal/nonquadratic의 **Cn 보정 요구**에만 적용한다.
|
||||
force field에서 D<εd*L이면 correction_pass=not-evaluated로 면제하고,
|
||||
field_pass=residual_pass다. D=εd*L에서는 면제가 아니다.
|
||||
moment에는 길이 예외가 없다. zero-flux의 Cε 요구는 면제하지 않는다.
|
||||
i=0에서는 이 예외로 가짜 correction을 만들지 않으며, Cn 기반 통과는 허용하지 않는다.
|
||||
단 zero-flux의 ν<=ε 잔차 단독 통과는 i=0에서도 실제 잔차 검사이므로 가능하다.
|
||||
normal/nonquadratic의 D=0에서 면제가 없으면 c=0만 cross-product 기준을 만족하며
|
||||
c/D division은 하지 않는다.
|
||||
|
||||
분기 순서는 공개 원문의 설명 순서를 실제 구현 가능한 형태로 고정한 FESA 선택이다.
|
||||
예를 들어 하나만 Rl을 통과하면 linear로 개별 면제하지 않고 각자 zero/normal/
|
||||
nonquadratic을 검사한다. 미활성 field는 `branch=not-evaluated`,
|
||||
field_pass=not-evaluated이며 전체 field AND에서 제외한다.
|
||||
전체 `field_pass`는 활성 field들의 pass AND이며 후속 acceptance와 다르다.
|
||||
필수 보정이 missing이면 field_pass=0이고 correction_pass=not-evaluated다.
|
||||
두 비교 중 하나라도 유효 true인 정상 보정 OR만 통과한다.
|
||||
|
||||
## 실행 연결과 transaction 경계
|
||||
|
||||
현재 source를 직접 확인한 결과와 필요한 연결점은 다음과 같다.
|
||||
|
||||
| 소유 위치 | 실제 현재 동작과 후보에 필요한 작업 |
|
||||
| --- | --- |
|
||||
| [hht_dynamic_assembly.cpp](../../src/fesa/analysis/hht_dynamic_assembly.cpp) / EvaluateAssembly | indexed local 결과를 e,row 순서로 serial reduce. f.internal_force[row], m.force[row]의 signed total을 global scatter 직전에 수집해야 한다. 기존 residual_scale은 strict 진단을 위해 보존한다. |
|
||||
| 같은 파일 / AssembleLoads | 외력 signed 합과 absolute row 합만 반환한다. 이 두 벡터로 source 개수를 복원할 수 없다. ComputeContributions 순회 중 별도 occurrence를 보존해야 한다. |
|
||||
| [hht_reduced_procedure.cpp](../../src/fesa/analysis/hht_reduced_procedure.cpp) / evaluate_trial | graph root·physical rate 재구성 후 assembly. 시작점 비교·inner root용 보조 평가를 outer 이력에 넣지 않는다. |
|
||||
| 같은 파일 / ResidualRecord·CorrectionRecord | 현재 strict residual_pass AND correction_pass가 종료 조건이고 i=0 correction_pass=true 초기화가 있다. 후보 종료와 strict 로그를 분리하며 이 초기 true를 후보에 전용하지 않는다. |
|
||||
| 같은 파일 / accepted 후보 생성 | CompleteConstraintEvidence, CompleteEquilibrium, CheckGraphRegularity, CheckReducedUpdates 뒤 frame/history 반환. 후보 field pass는 이 검사들을 건너뛰지 않는다. |
|
||||
| 같은 파일 / RunReducedHhtStep commit callback | state.CommitAcceptedFrame 성공 뒤 private history 교체. flux history candidate도 이 transaction에 묶어 한 번만 commit한다. |
|
||||
| [hht_dynamic_procedure.cpp](../../src/fesa/analysis/hht_dynamic_procedure.cpp) | 실제 executor가 ExecuteReducedHht에 연결된다. 별도 시험용 판정만 추가해서는 CLI 동작 변경이 아니다. |
|
||||
|
||||
flux snapshot, 직전 selected full q, 최근 세 ρ와 validity, pending S/N/K/M은
|
||||
analysis-private procedure가 소유한다. Element·Domain·integrator에 accepted 이력을
|
||||
옮기지 않는다. 매 trial은 immutable committed snapshot에서 pending 후보를 만든다.
|
||||
field 수렴 및 **모든** 후속 gate 뒤에 frame·private history·flux history를
|
||||
같은 acceptance로 commit한다. log write/flush 실패도 정상 accepted 공개를 만들지 않는다.
|
||||
실패가 commit 도중 발생하면 해당 증분의 state/private/flux history를 함께 이전 snapshot으로
|
||||
유지하거나 rollback한다. current frame만 바뀐 부분 commit을 호출자에게 반환하지 않는다.
|
||||
실패 trial, inner 평가, 재평가, 시작점 대안, 실패 출력/재시도는 committed 이력에
|
||||
표본을 추가하지 않는다. retry가 있다면 local residual/correction 이력도 새로 시작한다.
|
||||
이 정책은 기존 고정 dt 실패를 자동 cutback이나 strict 재시도로 바꾸지 않는다.
|
||||
|
||||
`CorrectionRecord`의 δq/L,hδv/L,h²δa/L,h²δA/L(회전 scale=1) <=1e-10은
|
||||
strict 비교 진단이다. 후보에 AND로 숨겨 종료를 지연시키지 않는다.
|
||||
반면 `PrivateCorrectionEqual`의 full/private acceleration 산술 일치는 별도 유지
|
||||
검사다. 기존 angular-only 선택의 strict 병진 residual·보정·private 산술 자격도
|
||||
유지한다. 후보 force field_pass를 angular-only 선택 자격으로 쓰지 않는다.
|
||||
|
||||
frame0, inner graph/hidden/rank/null/regularity, physical rate 복원,
|
||||
linear backward1e-12, update1e-12, pair/RN-even/finite/영 BC,
|
||||
global force/moment1e-8, HHT 계수와 schema2/source identity/final 보호는 유지한다.
|
||||
field pass 뒤 이 gate가 실패하면 정상 결과를 공개하지 않는다.
|
||||
자동 완화·polishing·숨은 strict 재시도로 성공을 만들지 않는다.
|
||||
|
||||
현재 local kernel은 사용자 승인 실험인 polar1회, ExpSkew/Jacobian3차,
|
||||
mass4gate1e-3과 관련 inertia/tangent/HHT physics test 오차1e-3이다.
|
||||
기존 exact equality·negative control·실제 controller는 유지한다.
|
||||
최신 선택50개 중4FAIL이라는 한계를 보존하고 strict/candidate에 같은 kernel을 쓴다.
|
||||
이 근사나 test 오차를 Abaqus 기본값 또는 후보 field 검증 통과로 보고하지 않는다.
|
||||
|
||||
## 로그 소비 계약
|
||||
|
||||
후보 record는 `record=dynamic-newton-iteration`, `schema=hht-log-v2`,
|
||||
`policy_id=abaqus2025-field-on-fesa-v1`이다.
|
||||
기본 키는 다음 순서다.
|
||||
|
||||
```text
|
||||
record schema policy_id step increment time iteration stage outcome failure_code field_pass accepted
|
||||
```
|
||||
|
||||
뒤이어 force_,moment_ 각각 아래 suffix 순서로 기록한다.
|
||||
|
||||
```text
|
||||
residual_numerator residual_scale spatial_flux time_average_flux
|
||||
correction_numerator increment_norm estimated_correction
|
||||
residual_threshold correction_threshold residual_unit correction_unit
|
||||
branch residual_pass correction_pass field_pass
|
||||
```
|
||||
|
||||
residual_numerator=ρ, residual_scale=s, spatial_flux=bbar,
|
||||
time_average_flux=raw qtilde, correction_numerator=실제 c,
|
||||
increment_norm=D, estimated_correction=cest다.
|
||||
residual_threshold는 무차원 Rl/ε/Rp/Rn, correction_threshold는 Cε/Cn이며
|
||||
면제이면 not-evaluated다. 단위는 force의 residual_unit=force,
|
||||
correction_unit=length, moment의 residual_unit=moment, correction_unit=rad다.
|
||||
힘·길이는 INP의 일관 단위계를 따른다. branch는 위 5개 enum만 허용한다.
|
||||
|
||||
미평가 numeric/bool은 literal `not-evaluated`, 평가 bool은 `0` 또는 `1`이다.
|
||||
실패 전 이미 평가한 값은 유지하고 이후 미평가값은 이전 record 값으로 채우지 않는다.
|
||||
strict_ prefix는 기존 residual numerator/scale와 q/v/a/A norm·pass 진단에 붙인다.
|
||||
strict 결과는 후보 field_pass와 분리한다.
|
||||
|
||||
각 record는 UTF-8 한 줄, LF 종결, 실제 tab으로 key=value를 구분한다.
|
||||
value의 backslash/tab/LF/CR는 각각 `\\`, `\t`, `\n`, `\r`로 escape한다.
|
||||
첫 '='만 key/value 경계이며 value 안 '='는 보존한다. 중복 key·없는 필수 key·
|
||||
알 수 없는 enum·numeric NaN/Inf는 reader 오류다. finite numeric은 locale-independent
|
||||
round-trip binary64 표현, index는 10진 비음수 정수다.
|
||||
step은 raw source step 이름을 escape하고 increment/time은 기존 schedule identity다.
|
||||
`stage=evaluation,outcome=candidate,accepted=0`은 field 통과 전후에도 가능하다.
|
||||
모든 유지 gate와 state/history commit 성공 뒤에만
|
||||
`stage=accepted,outcome=accepted,accepted=1` record를 쓴다.
|
||||
실패는 outcome=failed, accepted=0 및 실제 failure_code다.
|
||||
완결 accepted 로그도 final HDF5 저장 성공을 대신하지 않는다.
|
||||
다른 procedure의 로그·CLI·INP·HDF5 schema는 바꾸지 않는다.
|
||||
|
||||
## 독립 oracle에 필요한 경계 예
|
||||
|
||||
다음은 source fixture를 복제하는 대신 손으로 계산할 수 있는 입력/기대값이다.
|
||||
T3에서 각 threshold의 바로 아래·같음·바로 위와 nonfinite 실패를 추가한다.
|
||||
|
||||
| 입력 | 기대값 |
|
||||
| --- | --- |
|
||||
| Hmax=10. 두 요소에서 같은 global DOF total은 각각 8-6=2와 -2; 외부 source 두 개는 같은 DOF에 +3,-3. 나머지 요소/외부 성분은0 | 전체30개(요소24+외부6) 중 비활성0 26개를 제외한 목록 [2,2,3,3], 평균2.5. m=3>=.1Hmax. 조립 residual=0이어도 평균0 아님. Hmax=0이면 전체 평균10/30이다. |
|
||||
| Hmax=100, 목록 [0,.001,10] | εlHmax=.001, m=10=.1Hmax: 평균(.001+10)/2=5.0005. 0은 제외, .001은 포함. m<10이면 전체 재활성. |
|
||||
| N=0, base=.01, bbar=1e-7 | εbase와 같으므로 initial .01 유지, zero-flux. 바로 위는 첫 표본 후보 qtilde=bbar. |
|
||||
| S=10,N=1,bbar=30 | pending qtilde=20. 같은 증분 재평가 bbar=70이면40; (10+30+70)/3으로 중복 누적하지 않음. |
|
||||
| L=2,qtildeF=3,qtildeM=.2,bbarM=.1 | sM=6, raw qtildeM=.2 유지. sF=3. force에서 변환된6을 역변환하지 않음. |
|
||||
| ρ 이력 [8,4,2], c=.04,D=1 | cest=.02. 실제와 추정 모두 Cn=.01을 넘음. 이력 [8,4,1]이면 cest=.01로 경계 통과. |
|
||||
| 앞선 ρ 중 하나0 또는 i=1 | 추정 미평가. 실제 보정 실패를 추정 미평가로 통과시키지 않음. |
|
||||
| i=9 또는10, ρ=[8,4,2] | i9는 normal, i10은 nonquadratic. [16,4,1]도 일정 비율로 검출; [16,4,.5]는 비율 개선으로 normal. |
|
||||
| s=2,ρ=2e-5,bbar<=2e-5 | zero-flux 잔차 경계 통과. ρ가 바로 위면 실제 c<=.001D를 요구. |
|
||||
| νF=1e-8,νM=1.1e-8 | 모든 field linear 아님. 각 field를 개별 분기로 검사. |
|
||||
| DF=εdL,DM=0 | force 길이 면제 없음. moment에도 길이 면제 없음. 유효 cM=0이면 cross-product 보정 통과 가능. |
|
||||
| i0,ρ>εs,c가 없음 | 가짜 c=0 통과 금지. zero residual 단독 통과 외에는 실제 correction 후 재평가. |
|
||||
| field pass 뒤 global equilibrium 실패 | accepted=0, flux/history commit 없음, 정상 final 교체 없음. |
|
||||
|
||||
문서 단계에서는 링크·source 식·분기·단위·transaction을 정적으로 검토한다.
|
||||
빌드·solver 실행이나 과거 2024 결과에 대한 동등성 검증은 이 산출물의 evidence가 아니다.
|
||||
@@ -0,0 +1,132 @@
|
||||
# MITC4 local 근사 실험
|
||||
|
||||
2026-10-18 사용자 명시 지시에 따라 적용한 실험이다. Abaqus의 공식 tolerance나
|
||||
외부 Newton 수렴 규칙을 구현한 변경이 아니다. 기준 revision은 `43867d0`이며
|
||||
기존 문서와 reference 원본은 보존했다.
|
||||
|
||||
후속 사용자 지시로 테스트 오차 허용치도 `1e-3`으로 변경했다. 아래 1차 결과는 그 전의
|
||||
엄격한 테스트 기준으로 실행한 이력이며, 최신 결과는 마지막 절을 따른다.
|
||||
|
||||
## 적용 범위
|
||||
|
||||
| 위치 | 변경 |
|
||||
| --- | --- |
|
||||
| nonlinear_mitc4_shell.cpp / PolarRotation | 고정 12회 → 고정 1회 |
|
||||
| nonlinear_mitc4_inertia.cpp / ExpSkew, LeftJacobian | order 1..32 → 1..3; 항등항은 별도 유지 |
|
||||
| 같은 파일 / CheckPhysicalMass | 대칭성·analytic null action·full Gram·Cholesky 복원 네 비교 1e-12 → 1e-3 |
|
||||
|
||||
회전 급수는 `E3=I+P+P²/2+P³/6`, Jacobian은
|
||||
`J3=I+P/2+P²/6+P³/24`다. 자동미분은 이 다항식의 D1/D2/D3을 계산한다.
|
||||
질량의 양의 Cholesky pivot, reciprocal condition `2^-32`, 초기 director 단위길이
|
||||
검사는 그대로다. 외부 Newton·inner root·선형 solve·update·전역 평형과 시간수렴,
|
||||
외부 reference 비교의 기존 허용치는 변경하지 않았다. Hex8도 변경하지 않았다.
|
||||
공유 polar 변경은 비선형 정적 MITC4와 HHT에 모두 영향을 준다.
|
||||
|
||||
## 수치적 의미와 독립 검토
|
||||
|
||||
`F=RU`일 때 한 번의 polar 반복 결과는 `R(U+U^-1)/2`이므로 일반 변형에서
|
||||
직교 회전행렬이 아니다. Drilling potential과 그 미분값이 바뀐다. 순수 강체회전은
|
||||
별개이며 실제 관련 강체운동 검사는 통과했다.
|
||||
|
||||
Cubic exponential은 정확한 SO(3) 회전과 director/Jacobian 미분·null 항등식을
|
||||
일반적으로 만족하지 않는다. D3 오차도 작은 회전에서 나타날 수 있다. 변경된 식의
|
||||
AD/finite-difference 접선 일치가 원래 유한회전 물리의 정확도를 보장하지 않는다.
|
||||
질량 검사 완화는 검사값을 통과시키는 범위를 10^9배 넓히며 행렬 오차를 교정하지 않는다.
|
||||
독립 수치 검토에서도 요청과 production diff가 일치함을 확인했지만 정확도 통과로
|
||||
판정하지 않았다.
|
||||
|
||||
## 1차 실행 증거 — 테스트 허용치 변경 전
|
||||
|
||||
실행 로그·XML·discovery는 ignored `.build/local-numerics-check/`에 있다.
|
||||
MSVC x64 Debug 전체 build와 CMake/Ninja Debug 전체 build는 각각 exit 0이다.
|
||||
동일 source의 unit 850·integration 72·reference 43, 총 965개가 두 경로에서 등록됐다.
|
||||
CTest 표시 이름의 parameter pretty-print를 제외하고 실제 GoogleTest filter identity를
|
||||
대조해 동일함을 확인했다. 등록과 실제 실행은 다르다.
|
||||
|
||||
| 실행 | 결과 | 로그 |
|
||||
| --- | --- | --- |
|
||||
| 변경 전 새 cubic 검사 | 1 FAIL: 기존 32차와 구분됨 | baseline-requested-test.log/.xml |
|
||||
| 변경 전 관련 기존 검사 | 45 PASS, 0 FAIL | baseline-elements.log/.xml |
|
||||
| 변경 후 같은 범위와 새 검사 | 46개 중 41 PASS, 5 FAIL | modified-elements.log/.xml |
|
||||
| CTest 전체 선택·첫 실패 시 정지 | 43개 실행: 42 PASS, 1 FAIL; 나머지 922개 미실행, exit 8 | ctest.log/.xml |
|
||||
|
||||
실패한 기존 검사와 관측 예는 다음과 같다. 기존 assertion은 완화하거나 삭제하지 않았다.
|
||||
|
||||
| 검사 | 관측 |
|
||||
| --- | --- |
|
||||
| NonlinearMitc4Inertia.MatchesIndependentWarpedNodalMomentsEnergyAndPower | 질량 독립 oracle 상대오차 3.653585e-7, 기준 1e-12 |
|
||||
| DynamicRotation.InverseIdentityAndCurrentAnalyticNullAtFiniteRotation | null 변환의 불필요 성분 9.165932e-4, 기준 1e-12 |
|
||||
| DynamicRotation.ThirdDerivativesMatchAllPermutationsAndInteriorSweep | D3 oracle 오차 예 1.250000e-6, 기준 1e-12 |
|
||||
| DynamicRotation.MatchesEightyDigitIndependentBlockExponentialAnchors | D3 oracle 오차 예 1.250000e-6, 기준 1e-12 |
|
||||
| NonlinearMitc4Residual.ExactDrillingConstraintHasIndependentForceGradientAndHessian | constraint 차이 예 6.755203e-4, 기준 1e-11 |
|
||||
|
||||
새 cubic 동작 검사, 기존 접선 유한차분 검사와 강체운동 검사는 통과했다.
|
||||
이 통과로 위 실패를 상쇄하지 않는다.
|
||||
|
||||
HHT 추가 선택 실행의 raw 기록은 `modified-hht.log`다. C08a 독립 volume-force/
|
||||
power/momentum oracle 검사가 실패했으며 힘 상대오차 예는 9.105479e-7(기준 1e-12)다.
|
||||
`HhtDynamicProcedure.LargeDirectStepStartsFromAcceptedGeometry`는 outer correction
|
||||
100회 후 `HHT exhausted 100 corrections`로 실패했다. 이 추가 HHT 선택의 변경 전
|
||||
동일 실행은 하지 않았으므로 새 회귀로 단정하는 비교 증거와 구분한다.
|
||||
|
||||
이 HHT 선택은 7개 중 6개 완료(4 PASS·2 FAIL) 후 물리 실패가 확인된 상태에서
|
||||
마지막 `UniformTranslationHasNoShellStrainAndIsDeterministicAcrossPartitions` 실행을
|
||||
agent가 중단했다(native exit -1). 마지막 1개는 미판정이다. 완결 XML은 없으며 raw 로그와
|
||||
`hht-interruption.txt`를 남겼다. CTest도 전체 통과가 아니며 첫 실패 C08a에서 중단했다.
|
||||
전체 Native unit/integration/reference 실행, 정식 외부 reference 비교는 수행하지 않았다.
|
||||
물리 정확도 실패를 남긴 채 장시간 검증·공식 성능 측정으로 채택을 주장하지 않는다.
|
||||
선택 범위·exit·source/executable hash 요약은 `verification-summary.json`에 있다.
|
||||
|
||||
Python suite는 현재 Python 환경의 `No module named pytest`로 실행되지 않았다.
|
||||
자동 설치하지 않았다. 전체 검사와 정식 reference 비교·Release 성능·C08a/C08b
|
||||
시간수렴 통과는 확보하지 않았다. 검사 실행시간 차이는 조건을 통제한 성능 측정이 아니므로
|
||||
속도 향상률로 보고하지 않는다.
|
||||
|
||||
## 1차 판정과 다음 접근
|
||||
|
||||
요청한 수치 변경은 구현됐지만 기존 정확도 검증은 실패했다. 승인된 실험값과 기존
|
||||
검증 기준을 그대로 남긴다. 정확도 손실을 수용한 solver 채택이나 성능 목표 달성을
|
||||
뜻하지 않는다. 후속 원인 분리가 필요하면 polar, cubic, mass gate를 하나씩 바꾸는
|
||||
동일 조건 비교로 각각의 영향을 측정한다. 실제 성능은 동일 local kernel을 사용한
|
||||
strict/Abaqus 외부 정책 비교와 원래 kernel 대비 비교를 분리해 측정해야 한다.
|
||||
|
||||
## 후속 변경 — 테스트 오차 허용치 1e-3
|
||||
|
||||
사용자가 처음 제안한 `1e-4`를 최종 `1e-3`으로 대체했다. 다음 세 테스트 파일에
|
||||
`kNumericalTolerance=1e-3`을 두고 기존 숫자 기준이 이보다 작은 직접 오차 비교 100개
|
||||
(관성 50·접선 21·HHT 29)와 요소 유한차분 sweep의 오차 판정을 변경했다.
|
||||
|
||||
- tests/unit/elements/nonlinear_mitc4_inertia_test.cpp
|
||||
- tests/unit/elements/nonlinear_mitc4_shell_tangent_test.cpp
|
||||
- tests/unit/analysis/hht_shell_physics_test.cpp
|
||||
|
||||
오차 계산식과 oracle·fixture·유한차분 간격은 그대로다. 절대오차/정규화오차 구분도
|
||||
그대로이므로 모든 `1e-3`을 상대 0.1%로 해석할 수 없다. 일부 작은 회전질량의 절대
|
||||
기대값 약 `6.67e-4`보다 허용치가 커서 해당 개별 비교는 회전질량 0도 허용한다.
|
||||
정확한 equality, nonfinite/실패 검사, 누락항 negative control, 순수 산술 bookkeeping
|
||||
두 검사, 시간수렴의 감소/차수 조건, 테스트 내부 해석 controller와 production 종료
|
||||
조건은 유지했다. HHT 시간수렴의 finest 오차 assertion만 `1e-4`에서 `1e-3`으로 바뀌었다.
|
||||
외부 Abaqus 결과 비교나 다른 요소의 테스트는 바꾸지 않았다.
|
||||
|
||||
최신 로그는 ignored `.build/test-tolerance-1e-3/`에 별도 보관했다. 두 Debug 전체 target
|
||||
build는 exit 0이다. Native 및 CTest에서 같은 50개 identity를 선택해 모두 실행했으며
|
||||
각각 **46 PASS·4 FAIL·0 SKIP**(exit 1/8)이다. 전체 965개 중 나머지 915개는 이번에
|
||||
실행하지 않았다. 관련 요소 46개에 HHT 네 물리 검사를 추가한 선택이다.
|
||||
|
||||
| 남은 실패 | 새 기준에서도 실패한 이유/관측 예 |
|
||||
| --- | --- |
|
||||
| DynamicRotation.ThirdDerivativesMatchAllPermutationsAndInteriorSweep | 독립 D3 oracle 절대오차 0.2276159 > 0.001 |
|
||||
| DynamicRotation.MatchesEightyDigitIndependentBlockExponentialAnchors | 독립 D3 oracle 절대오차 0.2276159 > 0.001 |
|
||||
| NonlinearMitc4Residual.ExactDrillingConstraintHasIndependentForceGradientAndHessian | gradient 절대오차 0.00329135 > 0.001 |
|
||||
| HhtShellPhysics.C08aRigidOrbitMatchesIndependentVolumeForcePowerAndMomentum | 정규화 hidden defect 예 0.00123371 > 0.001 및 실제 CompleteEquilibrium 실패 |
|
||||
|
||||
기존 실패 중 질량/운동량 독립 oracle와 inverse/null 검사는 완화된 기준에서 통과했다.
|
||||
이는 계산값의 정확도가 개선됐다는 뜻이 아니다. D3·drilling·HHT 실패는 남겼으며
|
||||
추가 완화나 production 변경으로 숨기지 않았다. 이전 큰 시간간격 수렴 실패와 중단된
|
||||
HHT 실행은 이번에 재실행하지 않았고 이전 미판정 상태를 유지한다.
|
||||
|
||||
`native-selected.log/.xml`, `ctest-selected.log/.xml`, `changed-comparisons.json`과
|
||||
`verification-summary.json`에 실행·변경 근거가 있다. 최초 CTest wrapper 호출의 shell
|
||||
인용 오류(exit 255)는 `ctest-invocation-error.log`로 보존하고 CTest 직접 호출로 위
|
||||
50개를 재실행했다. 독립 검토에서도 변경 범위와 보존 조건이 일치함을 확인했다.
|
||||
Production 수식·종료 조건은 직전 실험본 그대로이며 이번에는 테스트와 문서만 수정했다.
|
||||
@@ -0,0 +1,160 @@
|
||||
# T5 후보 물리·시간 정확도 검증
|
||||
|
||||
검사일: 2026-10-19. pinned parent `dd54b0ae1e68a313ac46933b886c865c85ec4432`,
|
||||
branch `dryforge/tolerance-t5`. 이 기록은 T5의 선택 검사다. **물리 검증 closure는
|
||||
FAIL이며, 후보 production 경로의 비선형 시간 정확도 R8은 미완료다.** 새 강체 병진
|
||||
oracle의 통과와 기존 strict algebra 검사의 통과로 전체 정확도·채택 가능성을 주장하지 않는다.
|
||||
|
||||
## 변경·독립 검토
|
||||
|
||||
변경은 기존 `hht_dynamic_procedure_test.cpp`의
|
||||
`HhtDynamicProcedure.CandidateRigidTranslationMatchesIndependentPhysicalOracle` 한 검사와
|
||||
이 기록뿐이다. Production·local kernel·T3 helper·probe·기존 판정 기준은 수정하지 않았다.
|
||||
기존 검사 파일은 `tests/CMakeLists.txt`와 `msvc/fesa_unit_tests.vcxproj` 양쪽에 등록되어
|
||||
있어 추가 source 등록은 필요 없다. 전체 CMake 실행은 T7 범위다.
|
||||
|
||||
새 검사는 `HhtDynamicProcedureExecutor::Execute` → `ExecuteReducedHht`의 실제 후보
|
||||
경로를 호출한다. Fixture는 기존 두 disconnected unit-square shell의 density=2,
|
||||
thickness=.1, area=1을 그대로 사용한다. Nodal-lumped translational mass는 각 node
|
||||
`.05`, 총 질량은 `.4`다. 여덟 node 각각에 일정 global x CLOAD=1이므로 총 힘은8,
|
||||
초기 및 이후 물리 가속도는20이다. 시간은 `h=.25,T=.5,N=2,INC=3`이며 source의
|
||||
최대 증분을 변경하지 않았다. 독립 연속체 강체 해는 다음과 같다.
|
||||
|
||||
```text
|
||||
q_x(t) = 10 t^2, v_x(t) = 20 t, physical a_x(t) = 20
|
||||
public A_x(t) = (1+alpha)20-alpha*20 = 20
|
||||
T(t) = .5*.4*(20t)^2 = 80t^2
|
||||
W(t) = 8*(10t^2) = 80t^2
|
||||
Uphysical = Udrill = energy defect = 0
|
||||
```
|
||||
|
||||
Frame0 및 두 endpoint를 모두 비교한다. All-free와 y/z/rotation을 영구속한 두 경우를
|
||||
각각 실행하여 full q/v/a/A의 나머지 성분과 영 BC를 검사한다. 입력 힘, nodal inertia,
|
||||
전역 force/moment balance, kinetic/physical/drilling energy, 일정 외력 work/defect와
|
||||
두 positive increment의 `hht-log-v2`, policy ID, field_pass/accepted를 확인한다.
|
||||
일반 수치오차 상한은 사용자 승인 실험의 `1e-3`이다. 단위 scale은 L=1,T=.5,E=20이며,
|
||||
정확한 입력 힘·schedule·영 BC와 필수 global equilibrium `1e-8`은 그대로 검사한다.
|
||||
이 값은 Abaqus 기본값이나 production 내부 acceptance 완화를 뜻하지 않는다.
|
||||
|
||||
Root의 독립 검토는 source의 두 단위면적·rho·두께·CLOAD에서 M=.4/F=8/a=20을
|
||||
재계산하고 q/v/a·energy 식, scale, 양 BC 및 로그 의미를 대조했다. 처음 실행한 더
|
||||
엄격한 진단 버전도 통과했으나 최종 검사의 일반 오차 상한은 승인된 `1e-3`이다.
|
||||
정확한 강체 해는 시간 오차가 roundoff 수준이고 a와 A도 같으므로 **차수 >=1.8,
|
||||
N7 separation 또는 비상수 physical a/A 구분의 증거가 아니다.** 해당 구분은 아래의
|
||||
기존 algebra/rate/writer 검사 범위와 함께 해석해야 한다.
|
||||
|
||||
## 현재 실행 evidence
|
||||
|
||||
공유 evidence root는 workspace root 기준
|
||||
`.build/tolerance-experiment/go-20261018/T5/`다. 실제 cwd는
|
||||
`.dryforge/worktrees/T2`이며 다른 workspace·main·dev는 수정하지 않았다.
|
||||
원본 reference·`reference/temp`·외부 Abaqus 실행·다운로드는 사용하지 않았다.
|
||||
|
||||
| 실행 | 실제 선택 | PASS | FAIL | SKIP | native exit |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| 최종 Native Release probe ON |153|144|8|1|1|
|
||||
| 최종 Native Debug probe ON |28|27|0|1|0|
|
||||
| 보존 strict Release C08 대조 |5|0|5|0|1|
|
||||
|
||||
Release/Debug unit discovery는 각각887개로 identity가 일치했다. 최종 filter의 선택
|
||||
identity와 XML을 대조하여 missing/extra/duplicate=0을 확인했다. 범위 밖 identity는
|
||||
`verification.json`의 `excluded`에 보존했다. 위 행은 서로 중복되는 검사를 포함하므로
|
||||
합쳐서 unique 통과 수로 보고하지 않는다. Release153의 elapsed69.325초와
|
||||
Debug28의25.638초는 일부 동시 검사의 verification timing이며 공식 성능 측정이 아니다.
|
||||
|
||||
- `final-selected-release.command.txt`, `.raw.txt`, `.xml`, `.exit.txt`:
|
||||
물리·time/algebra·reduced/rate/partial-BC·energy·state/writer·요소 정확도153개.
|
||||
- `final-selected-debug`의 같은 네 파일: 새 oracle, 기존 후보 extension/history,
|
||||
time/energy/algebra·state/writer28개. `.exit.txt=0`에도 N7 SKIP1개가 있다.
|
||||
- `strict-physics`의 같은 네 파일: 보존 strict 바이너리 C08 실패 대조5개.
|
||||
초기 상대 baseline 경로의 launch 오류는 `strict-physics-initial-launch-error.txt`에
|
||||
별도 기록했다. 이 launch는 native 실행이 아니며 검사 수에 넣지 않았다.
|
||||
- `build-approved-release.raw.txt/.exit.txt`, `build-approved-debug.raw.txt/.exit.txt`:
|
||||
직접 MSBuild 재빌드 둘 다 exit0. 실제 VS 환경·target·probe 옵션은
|
||||
`build-final-release.cmd`, `build-final-debug.cmd`에 있다.
|
||||
- `final-run-binary-hashes.json`, `verification.json`: 실행 전후 binary/source/DLL
|
||||
SHA256, cwd/branch, 각 test identity·실패·skip, literal command와 제외 목록.
|
||||
`summarize.py`와 `summary.raw.txt/.exit.txt`는 XML/filter/discovery 대조를 재현한다.
|
||||
|
||||
최종 unit binary SHA256은 Release
|
||||
`1dbb49aaacbb48ea483c4cba3b98805fa6b89f65cf4c61b428fc5e60f98b4492`, Debug
|
||||
`248b23c0d2a1e3441424b6051fe5d54f648104e04fa22e3a403a41b680ded3d2`다.
|
||||
보존 strict binary
|
||||
`272a1829d6440dac32011dc30943bd8213b8bce5cbd4239243fb448f237be922`는
|
||||
`baseline/test-runtime-hashes.json`과 일치한다. inherited GTEST filter/shard를 제거하고
|
||||
`FESA_HHT_PROBE_OUTPUT`을 unset했으며 짧고 분리된 TMP/TEMP `C:/ft5f`,
|
||||
`C:/ft5g`, `C:/ft5b`를 사용했다.
|
||||
|
||||
## 정확도 실패와 strict 대조
|
||||
|
||||
최종 Release의 다음8개 identity는 FAIL이다. 수치 gate를 변경하여 합격시키지 않았다.
|
||||
|
||||
| identity (suite 생략 표기) | 현재 실패 evidence |
|
||||
| --- | --- |
|
||||
| `HhtShellPhysics.C08aRigidOrbitMatchesIndependentVolumeForcePowerAndMomentum` | pointwise hidden 최대 `.0012337114403665182 > .001`; t=.0625 이후 여러 `CompleteEquilibrium` 실패 |
|
||||
| `HhtShellPhysics.ReducedManufacturedControllerKeepsExactInitialOrbit` | h=1/64, increment1에서 strict outer100 소진 |
|
||||
| `HhtShellPhysics.C08aRigidManufacturedOrbitReducedCoordinateTimeConvergence` | 첫 grid h=1/16, increment1에서 strict outer100 소진 |
|
||||
| `HhtShellPhysics.PredictorStartCompletesPreviouslyFailingLumpedFreePlateGrid` | h=1/256, increment1에서 strict outer100 소진 |
|
||||
| `HhtShellPhysics.C08bFreeFiniteRotationPlateSelfConvergenceAndPhysicalInvariants` | 첫 grid h=1/64, increment1에서 strict outer100 소진 |
|
||||
| `DynamicRotation.ThirdDerivativesMatchAllPermutationsAndInteriorSweep` | 독립 D3 norm error `.22761592213997162 > .001` |
|
||||
| `DynamicRotation.MatchesEightyDigitIndependentBlockExponentialAnchors` | 독립 anchor error `.22761592213997159 > .001` |
|
||||
| `NonlinearMitc4Residual.ExactDrillingConstraintHasIndependentForceGradientAndHessian` | gradient error `.0032913506131779374 > .001` |
|
||||
|
||||
보존 strict 바이너리에서도 C08의 위5개가 모두 같은 hidden/equilibrium 및 같은
|
||||
첫 증분·grid의 correction budget 실패로 끝났다. `RunPlate`는 source상 자체
|
||||
strict residual/correction `1e-10`을 사용한다. 이 대조는 후보 field 정책 때문에
|
||||
C08 harness의 새 실패가 생겼다는 주장을 뒷받침하지 않으며, **후보 production의
|
||||
시간 정확도가 통과했다는 뜻도 아니다.** Local D3/drilling의 세 실패는 T2에서 이미
|
||||
기록한 local 근사의 부적합과 같고 현재 raw 결과도 남겼다. 본 태스크는 local 근사를
|
||||
수정하거나 physics 기준·kernel·HHT 계수·outer/controller를 바꿀 권한이 없다.
|
||||
|
||||
T4의 동일 production SHA evidence는 추가로 재사용한다:
|
||||
`../T4/final-native-{release,debug}-selected.xml` 각각86개 중84PASS/2FAIL 및
|
||||
`strict-baseline-related.xml`의2FAIL이다. T5의 수정은 test-only라 production SHA는
|
||||
불변이다. LargeDirect는 후보 field-pass 뒤 `dynamic-invalid-mass-null`, Uniform은
|
||||
첫 강체 병진 이후 finite-rotation increment2의 triad gate에서 실패했다. 보존 strict는
|
||||
두 검사에서 outer100 소진, 특히 Uniform의 첫 병진에서 막혀 후속 finite rotation에
|
||||
도달하지 않았다. 따라서 후속 triad 실패의 원인이 strict와 같다고 단정하지 않는다.
|
||||
T4의 후보 output-failure rollback/accepted-once와 CLI old-final 보호 evidence도
|
||||
그 범위로만 사용하며 이를 새 시간수렴 또는 T7 전체 검사 통과로 세지 않는다.
|
||||
|
||||
## 통과 범위와 남은 R8
|
||||
|
||||
현재 실행에서 partial rotation BC/null 교집합·exact-zero, physical rate 복원,
|
||||
full/augmented tangent와 restricted defect, fixed chart/transpose·rank/regularity,
|
||||
pair/RN-even·zero-scale·nonfinite 산술, lifecycle rollback, state/source identity,
|
||||
schema2/writer의 endpoint/A 분리·부분 moment reaction·old-final 보호 검사가 통과했다.
|
||||
실제 후보 연결은 새 강체 oracle 및 기존 extension/history/lifecycle 검사로,
|
||||
각 operator·합성 writer fixture의 검증과 구분한다.
|
||||
|
||||
C01/C03/C04a/C04b는 생산 시간 helper를 쓰지만 자체 strict Newton/algebra harness다.
|
||||
아래는 최종 Release raw에서 추출한 q/v/physical-a의 finest normalized error 및
|
||||
마지막 두 refinement 차수의 channel별 최솟값이다. 모든 해당 channel은 감소했고
|
||||
차수 >=1.8, 기존 더 엄격한 finest1e-4도 만족했다. 이를 후보 field path의 R8로
|
||||
승격하지 않는다. 사용자 승인 finest1e-3을 적용하기 위해 기존 검사를 완화할 필요도 없었다.
|
||||
|
||||
| algebra model | finest q / v / physical a | 최소 마지막 차수 q / v / a |
|
||||
| --- | --- | --- |
|
||||
| C01 |1.10353e-5 / 4.26133e-6 / 1.10353e-5|1.99948 / 1.99851 / 1.99948|
|
||||
| C03 variable mass |1.10242e-6 / 3.42816e-7 / 9.26995e-7|1.99989 / 1.99477 / 1.99886|
|
||||
| C04a coupled massless |5.26648e-6 / 4.20639e-6 / 1.05330e-5|1.99977 / 1.99472 / 1.99977|
|
||||
| C04b moving null |1.62408e-6 / 1.66011e-6 / 3.16757e-6|1.99932 / 1.99962 / 2.00237|
|
||||
|
||||
별도의 `HhtTemporalVerification.C04aFinestReductionComparisonIsNumericallyResolved`는
|
||||
Release/Debug 모두 SKIP이다. h=1/256 full/reduced acceleration defect
|
||||
`3.0790209097630861e-10`이 arithmetic/solve resolution `6.2961354117696263e-10`보다
|
||||
작아 판정 불가다. C04a의 위 analytic 시간수렴 통과와 이 comparison SKIP을 섞지 않는다.
|
||||
C07 energy·spectrum·reaction, C02 STEP oscillator, 비상수 a/A helper 검사는 통과했으나
|
||||
이는 실제 후보 nonlinear shell의 energy/시간수렴 closure를 대신하지 않는다.
|
||||
|
||||
R8의 **실제 후보 nonlinear shell q/v/physical-a 마지막 두 refinement·order>=1.8·
|
||||
finest<=1e-3·기존 N7 separation**은 아직 충족하지 못했다. C08 시간 검사는 첫 grid에서
|
||||
실패하여 마지막 refinement와 invariant 감소 판정까지 도달하지 않았다. 별도의
|
||||
유효한 비선형 production analytic oracle도 구성하지 않았으며, 미세 grid를 외부 truth로
|
||||
삼거나 선형 근사를 exact nonlinear truth로 대입하지 않았다. 실패/미평가를 PASS로
|
||||
세거나 hidden retry/polishing·failed-frame commit으로 성공을 만들지 않는다.
|
||||
|
||||
현재 결과는 T6 장시간 성능 측정의 수치 승인 근거가 될 수 없다. 독립적으로 남은
|
||||
범위는 local 근사의 독립 정확도 부적합, candidate finite-rotation acceptance 실패,
|
||||
candidate 비선형 시간수렴·N7/physical invariant closure다. 전체 Native Debug,
|
||||
전체 CMake CTest, 별도 Python 및 필수 reference 검증은 T7의 별도 실행 결과를 따른다.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Reference 모델 변위 비교
|
||||
|
||||
비교일: 2026-10-19. 대상 source HEAD: `626f7da880001fcae2e3280e9d43a2772ce79ae6`.
|
||||
정식 변위 reference가 있는 8개 모델 중 **8개 U 비교 통과**다.
|
||||
이는 변위 reference 비교 결과이며, 기존 물리 검사 10개 실패의 해결이나 후보 채택을 뜻하지 않는다.
|
||||
후속 사용자 승인으로 이 변위 결과를 이번 실험의 검증 통과로 수용하고 실패10개는 보류했다.
|
||||
[결정과 적용 범위](../tracking/decisions/0011-tolerance-experiment-acceptance.md)를 따른다.
|
||||
|
||||
## 빌드와 실행 시간
|
||||
|
||||
최종 표는 모두 **MSVC x64 Release, 개발용 HHT probe OFF** 실행 결과다.
|
||||
기존 Debug 정적 결과는 사전 확인에만 사용했고, beam·shell·shellNL은 이번에 같은 Release EXE로 새로 실행했다.
|
||||
MITC4 HHT도 이번에 정식 입력의 100증분 전체를 새 작업 복사본에서 실행했다.
|
||||
CLI와 정식 HHT 비교기 모두 exit 0이다. 양의 100증분 중 71개는 Newton 2회,
|
||||
29개는 1회로 수렴했으며 로그의 실제 정책은 `abaqus2025-field-on-fesa-v1`이다.
|
||||
Hex8 네 모델은 T7에서 실행한 Release 결과를 재사용했다. 실행 당시 report·HDF5·입력·reference hash와
|
||||
현재 source/build/test/script 450개 및 runtime 9개 hash를 대조했으며 변경이 없었다.
|
||||
|
||||
Release 빌드 명령은 `/p:Configuration=Release /p:Platform=x64 /p:FesaHhtDevelopmentProbe=false`다.
|
||||
실제 compiler 기록 102개 소스의 `/O2`, `NDEBUG`, `/MD`를 확인했다.
|
||||
링커의 `/DEBUG`는 PDB 생성 옵션이며 Debug 컴파일 구성을 의미하지 않는다.
|
||||
EXE SHA-256: `449196eb79efab3c62b8dac4b73c3c76e97ec76f6a77f9d80ce1ee5c8c1f0cfa`.
|
||||
|
||||
시간은 `perf_counter()`로 측정한 FESA CLI 프로세스의 wall time이다. 입력 읽기·조립·풀이·결과 저장을
|
||||
포함하고, 빌드·비교 스크립트 실행 시간은 제외한다. 모델별 1회 실행이며 반복 측정·기준 실행과의
|
||||
통제된 성능 비교가 아니므로 speedup으로 해석하지 않는다. 서로 다른 시점의 Hex8 시간도 구분했다.
|
||||
|
||||
## 변위 결과
|
||||
|
||||
RMS는 각 프레임의 U1·U2·U3 전체 비교 scalar 오차 RMS를 같은 프레임의 최대 절대 reference 변위로
|
||||
나눈 값이다. 아래는 프레임별 정규화 RMS의 최댓값이며, 프레임을 합친 RMS가 아니다.
|
||||
최대 절대오차는 모든 비교 프레임·절점·병진 성분에서 찾았다. 길이는 입력의 일관 단위이며
|
||||
HDF5가 `user-consistent-unspecified`이므로 임의로 mm 또는 m를 붙이지 않았다.
|
||||
|
||||
| 모델 | 빌드 | 절점 × 프레임 | 최악 정규화 RMS (%) | 최대 절대오차 | CLI 시간 (초) | U 판정 |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | --- |
|
||||
| cantilever beam (B33) | Release | 11 × 1 | 8.4596232e-07 | 5.33322917e-10 | 0.148 | PASS |
|
||||
| shell (MITC4, 선형 정적) | Release | 49 × 1 | 0.10332757 | 1.90378535e-07 | 0.182 | PASS |
|
||||
| shellNL (MITC4, 비선형 정적) | Release | 49 × 1 | 0.10332757 | 1.90378553e-07 | 1.000 | PASS |
|
||||
| Hex8 선형 정적 | Release | 442 × 1 | 9.3179458e-07 | 6.09626121e-11 | 0.674 | PASS |
|
||||
| Hex8 비선형 정적 | Release | 442 × 1 | 2.1699132e-05 | 9.83246600e-10 | 1.570 | PASS |
|
||||
| Hex8 선형 동적 | Release | 442 × 101 | 3.6188213e-05 | 1.17034536e-09 | 57.675 | PASS |
|
||||
| Hex8 비선형 동적 | Release | 442 × 101 | 0.0002155586 | 7.77901169e-09 | 114.651 | PASS |
|
||||
| MITC4 HHT 비선형 동적 | Release | 9 × 101 | 0.22824245 | 3.09203705e-07 | 5662.145 | PASS |
|
||||
|
||||
## 최대 절대오차의 위치
|
||||
|
||||
Abaqus와 FESA 값은 같은 source instance·node label·성분·증분에 해당한다.
|
||||
|
||||
| 모델 | 증분 | 절점 | 성분 | Abaqus | FESA | 절대오차 |
|
||||
| --- | ---: | ---: | --- | ---: | ---: | ---: |
|
||||
| cantilever beam (B33) | 1 | 11 | U3 | -1.9047627200e-02 | -1.9047626667e-02 | 5.33322917e-10 |
|
||||
| shell (MITC4, 선형 정적) | 1 | 2 | U3 | -2.3740820300e-05 | -2.3550441765e-05 | 1.90378535e-07 |
|
||||
| shellNL (MITC4, 비선형 정적) | 10 | 2 | U3 | -2.3740820300e-05 | -2.3550441747e-05 | 1.90378553e-07 |
|
||||
| Hex8 선형 정적 | 10 | 43 | U2 | 1.2031230100e-03 | 1.2031230710e-03 | 6.09626121e-11 |
|
||||
| Hex8 비선형 정적 | 10 | 305 | U3 | 1.3131889300e-06 | 1.3122056834e-06 | 9.83246600e-10 |
|
||||
| Hex8 선형 동적 | 7 | 143 | U1 | 2.0765191900e-03 | 2.0765203603e-03 | 1.17034536e-09 |
|
||||
| Hex8 비선형 동적 | 30 | 8 | U2 | 1.8255165800e-03 | 1.8255088010e-03 | 7.77901169e-09 |
|
||||
| MITC4 HHT 비선형 동적 | 1 | 2 | U3 | -4.9189598900e-05 | -4.8880395195e-05 | 3.09203705e-07 |
|
||||
|
||||
## 판정 기준과 비교 범위
|
||||
|
||||
- 기존 공통 reference 정책을 유지했다. 프레임별 `S=max(abs(U_reference))`, near-zero 경계 `0.01*S`다.
|
||||
Near-zero 행은 절대오차 `<=0.01*S`, 그 밖의 행은 상대오차 `<=5%`, family 정규화 RMS는 `<=1%`를 모두 만족해야 한다.
|
||||
`S=0`이면 정확한 0을 요구한다. Local 검사에 승인한 `1e-3`을 외부 비교 허용치로 사용하지 않았다.
|
||||
- Source instance·node label·프레임을 고정해 대응하고 누락·중복·nonfinite·입력 identity를 먼저 확인했다.
|
||||
beam 11절점, shell/shellNL 49절점, Hex8 442절점의 전체 U1·U2·U3를 비교했다.
|
||||
- MITC4 HHT는 HDF5의 전체 729절점·101프레임과 finite/inventory를 사전 검사했지만, Abaqus CSV가 제공하는
|
||||
9절점(2, 21, 22, 69, 70, 165, 309, 442, 586)의 101프레임, 총 2,727개 U scalar만 외부 값과 비교한다.
|
||||
729절점 전체에 대한 Abaqus 변위 일치를 주장하지 않는다.
|
||||
- shellNL 정적은 load factor 1의 accepted frame을 찾아 Abaqus 증분 10과 대응했다. HDF5 frame index 9를 증분 9로 오인하지 않았다.
|
||||
- Hex8 동적과 MITC4 HHT는 초기 프레임을 포함한 101프레임이다. 정적 CSV는 최종 상태 1프레임 비교다.
|
||||
- 이번 표는 병진 U만 다룬다. Shell 회전 UR은 기존 계약의 warning-only 분류를 유지한다.
|
||||
MITC4 HHT의 UR 실패 family는 100개 프레임이며 warning은 200건이다.
|
||||
- 고유치 모델(shell 및 Hex8)은 모드 정규화에 따라 변위 크기가 달라 절대 물리 변위 비교 대상에 넣지 않았다.
|
||||
shell-thermal은 NT11 reference만 있어 변위 비교를 수행하지 않았다.
|
||||
- `reference/temp`는 사용하지 않았다. 정식 자료가 없는 추가 mesh/pressure 변형을 통과로 세지 않았다.
|
||||
- 원본 reference 34개 파일의 실행 전후 SHA-256이 동일했다. 원본 입력·CSV·결과를 수정하지 않았다.
|
||||
|
||||
## 근거 파일
|
||||
|
||||
- 집계 JSON: [`displacement-summary.json`](../../.build/reference-displacement/run-20261019-090831/displacement-summary.json)
|
||||
- 집계 CSV: [`displacement-summary.csv`](../../.build/reference-displacement/run-20261019-090831/displacement-summary.csv)
|
||||
- 성분별 상세 CSV: [`displacement-components.csv`](../../.build/reference-displacement/run-20261019-090831/displacement-components.csv)
|
||||
- 빌드 확인: [`build-verification.json`](../../.build/reference-displacement/run-20261019-090831/build-verification.json)
|
||||
- 새 실행 근거: [`.build/reference-displacement/run-20261019-090831`](../../.build/reference-displacement/run-20261019-090831/)
|
||||
- 재사용한 Hex8 실행 근거: [T7 hex8-reference](../../.build/tolerance-experiment/go-20261018/T7/hex8-reference/)
|
||||
- 기존 전체 검증과 미해결 항목: [실험 결과](results.md)
|
||||
|
||||
Raw evidence는 로컬 ignored `.build`에 보존했다. 이 문서 추가로 C++ 코드·수치 기준을 변경하거나 전체 검증을 다시 실행하지 않았다.
|
||||
@@ -0,0 +1,99 @@
|
||||
# 실패 검사 10개의 상대오차 전환 검토
|
||||
|
||||
검토일: 2026-10-19. 대상 제품 source는 변위 비교 당시 `626f7da`와 동일하다.
|
||||
**검사 오차만 상대오차로 바꿔 통과를 확인할 수 있는 항목은 없다.**
|
||||
D3 두 검사와 drilling 한 검사는 상대 기준에서도 실패하고, C08a pointwise 한 검사는
|
||||
이미 정규화 오차에서 실패한다. 나머지 6개는 수치 비교 assertion에 도달하기 전에 실패한다.
|
||||
테스트·production·reference 기준을 변경하지 않았다.
|
||||
이후 사용자는 [변위 기준으로 이번 실험을 수용하고10개 실패를 보류](../tracking/decisions/0011-tolerance-experiment-acceptance.md)했다.
|
||||
아래 상대오차 계산과 실패 사실은 보존한다.
|
||||
|
||||
## 검토한 상대오차 정의
|
||||
|
||||
사용자가 승인한 검사 tolerance `1e-3`을 상대오차에도 적용한다고 가정했다.
|
||||
이는 **0.1%**이며, 외부 Abaqus 변위 비교의 family RMS 1%와 다른 기준이다.
|
||||
|
||||
```text
|
||||
reference 상대오차 = ||actual - oracle|| / ||oracle||
|
||||
대칭 정규화 오차 = ||actual - oracle|| / (||actual|| + ||oracle||)
|
||||
검토 상한 = 0.001
|
||||
```
|
||||
|
||||
벡터는 Euclidean norm, 텐서는 Frobenius norm을 사용했다. 분모 0은 별도 exact-zero
|
||||
사례로 다루며 `max(1, norm)` 같은 floor로 오차를 줄이지 않았다.
|
||||
Drilling gradient는 병진 좌표 미분과 회전 좌표 미분의 차원을 맞추기 위해 병진 열에
|
||||
특성길이 2, 회전 열에 1을 곱했다. 이는 기존 같은 테스트의 Hessian 비교에 쓰는 scaling이다.
|
||||
이 검토 정의는 기존 검사에 채택한 새 정책이 아니다.
|
||||
|
||||
## 10개 검사별 결론
|
||||
|
||||
| 기존 실패 identity | 현재 실패 위치 | 상대오차 전환 검토 |
|
||||
| --- | --- | --- |
|
||||
| `DynamicRotation.ThirdDerivativesMatchAllPermutationsAndInteriorSweep` | 독립 D3 oracle와의 norm 차이 | 여전히 FAIL. 유한회전 상태의 전체 D3 텐서 상대오차 31.7922%. |
|
||||
| `DynamicRotation.MatchesEightyDigitIndependentBlockExponentialAnchors` | 독립 고정밀 D3 anchor 3개 | 여전히 FAIL. 해당 방향의 상대오차 23.2126%, 100%, 31.2134%. |
|
||||
| `NonlinearMitc4Residual.ExactDrillingConstraintHasIndependentForceGradientAndHessian` | drilling gradient 성분 차이 | 여전히 FAIL. 전체 gradient 상대오차 평면 93.7766%, warped 93.8607%. |
|
||||
| `HhtShellPhysics.C08aRigidOrbitMatchesIndependentVolumeForcePowerAndMomentum` | hidden defect와 `CompleteEquilibrium` | 이미 `abs(defect)/scale` 검사. 최대 0.123371% > 0.1%. 별도 내부 평형 실패도 남는다. |
|
||||
| `HhtShellPhysics.ReducedManufacturedControllerKeepsExactInitialOrbit` | h=1/64, increment 1, strict Newton 100회 소진 | 비교값을 얻기 전 예외. 사후 상대오차 assertion 전환으로 해결되지 않는다. |
|
||||
| `HhtShellPhysics.C08aRigidManufacturedOrbitReducedCoordinateTimeConvergence` | h=1/16, increment 1, strict Newton 100회 소진 | 동일. 시간수렴의 오차·차수 판정에 도달하지 못한다. |
|
||||
| `HhtShellPhysics.PredictorStartCompletesPreviouslyFailingLumpedFreePlateGrid` | h=1/256, increment 1, strict Newton 100회 소진 | 동일. |
|
||||
| `HhtShellPhysics.C08bFreeFiniteRotationPlateSelfConvergenceAndPhysicalInvariants` | h=1/64, increment 1, strict Newton 100회 소진 | 동일. |
|
||||
| `HhtDynamicProcedure.LargeDirectStepStartsFromAcceptedGeometry` | 후보 field 통과 뒤 mass/null acceptance, 결과 없음 | 내부 mass 검사는 이미 norm으로 정규화된 `1e-12`. 테스트의 `HasValue()`를 상대오차로 바꿀 수 없다. |
|
||||
| `HhtDynamicProcedure.UniformTranslationHasNoShellStrainAndIsDeterministicAcrossPartitions` | finite-rotation increment 2, triad/회전좌표 불일치, 결과 없음 | 내부 무차원 triad 성분 차이 `1e-12`에서 실패. 사후 테스트 비교의 전환 대상이 아니다. |
|
||||
|
||||
마지막 두 검사의 gate 자체를 바꾸는 것은 production acceptance 변경이다. 이번 검토는
|
||||
그 변경의 통과 여부를 측정하지 않았으며, 저장된 실패 상태에 numerator가 없는 항목의
|
||||
새 상대오차나 통과를 추정하지 않았다. C08 네 controller도 이미 길이·시간 scale로
|
||||
정규화한 보정과 잔차에 `1e-10`을 사용한다. 이를 `1e-3`으로 바꾸는 것은 반복 종료
|
||||
규칙의 변경이며, 절대오차 비교를 상대오차로 재표현하는 작업과 구분해야 한다.
|
||||
|
||||
## D3의 실제 분자와 분모
|
||||
|
||||
기존 두 D3 검사의 유한회전 상태 `phi=(0.2,-0.3,0.4)`에서 재계산했다.
|
||||
`e1,e2,e3`는 단위 좌표 방향, `m=(1,2,-1)/sqrt(6)`이다.
|
||||
|
||||
| 방향 | 절대 norm 차이 | oracle norm | reference 상대오차 | 대칭 정규화 오차 |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| e1,e1,e1 | 0.227615922140 | 0.980571766913 | 23.2126% | 11.4924% |
|
||||
| e1,e2,e3 | 0.030126523605 | 0.030126523605 | 100% | 100% |
|
||||
| m,m,m | 0.270116450986 | 0.865384981666 | 31.2134% | 15.1900% |
|
||||
| 전체 D3 텐서, 좌표 27방향 | 0.575746413774 | 1.810965975332 | 31.7922% | 15.8315% |
|
||||
|
||||
e1,e2,e3 방향의 cubic D3는 0이지만 독립 oracle는 0이 아니므로 상대오차는 100%다.
|
||||
전체 텐서 norm으로 묶어도 0.1%를 넘는다. 따라서 작은 성분 한 개를 나눈 결과만으로
|
||||
실패한다고 결론 낸 것이 아니다. 위 anchor 절대오차는 보존 T7 XML의 실제 실패값과 일치했다.
|
||||
|
||||
기존 5상태×4×4×4방향의 중복 없는 320개 D3 비교에서도 절대 `1e-3` 실패는 64개,
|
||||
순수 상대 `1e-3` 실패는 103개였다. 이는 기존 finite-difference 간격 7회의 반복 assertion을
|
||||
제외한 진단 개수다. 작지만 0이 아닌 oracle 값에서 순수 상대오차는 오히려 더 엄격해진다.
|
||||
기존 테스트의 모든 절대 검사를 일괄 상대 검사로 바꾸는 근거가 되지 않는다.
|
||||
|
||||
## Drilling의 실제 분자와 분모
|
||||
|
||||
기존 테스트와 같은 flat/warped fixture에서 4절점×24열 전체 gradient를 계산했다.
|
||||
|
||||
| fixture | scaled 차이 norm | scaled oracle norm | reference 상대오차 | 대칭 정규화 오차 |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| flat | 0.030299801962 | 0.032310614847 | 93.7766% | 77.8392% |
|
||||
| warped | 0.030708542429 | 0.032717144436 | 93.8607% | 78.0369% |
|
||||
|
||||
대표 성분은 actual=-0.000323196746, oracle=-0.003614547359,
|
||||
차이=0.003291350613으로 성분 상대오차도 91.0584%다. 전체 norm과 대칭 분모로
|
||||
계산해도 실패한다. 절대값의 단위·크기 때문에 실패한 것으로 해석할 수 없다.
|
||||
|
||||
## 근거와 실행 범위
|
||||
|
||||
- 기존 10개 실패는 [T7 unit XML](../../.build/tolerance-experiment/go-20261018/T7/fesa_unit_tests.xml)과 현재 테스트 본문에서 확인했다.
|
||||
- 독립 작업 폴더의 진단 C++는 기존 두 요소 테스트 파일의 fixture/oracle를 include하고
|
||||
실제 `EvaluateDynamicRotation` 및 `EvaluateInternalResidualAndTangent`를 호출했다.
|
||||
기존 MSVC Debug solver/gtest library에 연결했으며 production library를 다시 빌드하거나 수정하지 않았다.
|
||||
- 실행 filter는 `RelativeReview.*`의 진단 2개뿐이다. Exit 0은 진단값 수집의 성공이며
|
||||
기존 실패 테스트 10개의 통과를 의미하지 않는다. 10개 전체·전체 회귀·reference 해석을 재실행하지 않았다.
|
||||
- Source/build/test/script 450개와 solver/gtest library hash 불변을 확인했다.
|
||||
- Raw/수치: [진단 로그](../../.build/relative-review/run-20261019-134221/diagnostic.raw.txt),
|
||||
[계산값·검증 JSON](../../.build/relative-review/run-20261019-134221/numeric-review.json),
|
||||
[진단 XML](../../.build/relative-review/run-20261019-134221/diagnostic.xml).
|
||||
첫 standalone link의 main 누락 실패와 main 추가 후 build 성공도 같은 폴더에 보존했다.
|
||||
|
||||
상대오차는 단위·규모가 다른 값을 비교할 때 유용하지만 이번 세 미분 검사의 오차를
|
||||
0.1% 이내로 만들지는 못한다. 상대 정책을 도입한다면 물리량별 차원·norm·near-zero
|
||||
처리를 먼저 정하고, 근사식과 독립 oracle의 불일치 및 실행 실패를 별도로 해결해야 한다.
|
||||
@@ -0,0 +1,430 @@
|
||||
# MITC4 HHT 외부 field 수렴 정책 실험 결과
|
||||
|
||||
**후속 사용자 승인: 이번 실험은 변위 검증 통과·실패10개 보류로 조건부 완료한다.**
|
||||
마지막 [최종 정리](#후속-승인과-남은-단계의-완료)가 현재 판정이며,
|
||||
아래 최초 실험의 FAIL/SKIP·미측정 기록과 수치 기준은 그대로 보존한다.
|
||||
[수용 범위와 후속 의무](../tracking/decisions/0011-tolerance-experiment-acceptance.md)를 함께 따른다.
|
||||
|
||||
기록일: 2026-10-19. **실제 종료 정책은 구현했으나 수치 부적합과 검증 공백이 남아 있다.**
|
||||
MITC4 HHT 양의 증분은 `abaqus2025-field-on-fesa-v1`로 판정하며 `hht-log-v2`를 기록한다.
|
||||
Native Debug와 CMake Debug/CTest 전체는 각각 998개 중 **987 PASS·10 FAIL·1 SKIP**이다.
|
||||
Python 145개와 별도 정식 Hex8 5모델 비교는 통과했다. T6/T7 당시 MITC4 후보 100증분은
|
||||
미실행이었다. 후속 사용자 요청으로 Release 후보 원본을 5662.144854초에 완료하고 U 비교를 통과했다.
|
||||
[8개 정식 모델 변위·빌드·시간 비교](reference-displacement-comparison.md)에 새 근거를 기록했다.
|
||||
전체 검증 완료·가속 확인·정식 채택으로 보고하지 않는다.
|
||||
|
||||
## 기준과 원시 증거
|
||||
|
||||
측정 source는 `78cb33920742a91b4801788fbcc953d3306b0884`다.
|
||||
`d7f6d0c`와 제품 source·두 빌드 설정의 diff는 exit 0이다. 원래 조사 기준
|
||||
`43867d0`와 달리 사용자 승인 local kernel인 polar 1회, ExpSkew/Jacobian 3차,
|
||||
질량 gate와 관련 테스트 오차 `1e-3`을 사용했다. 외부 Newton은 잔차와
|
||||
q/v/a/A 보정 `1e-10`, 최대 100회인 기존 strict 경로다.
|
||||
|
||||
원시 자료는 **부모 작업공간**의 다음 ignored 위치에 보존했다. T2 worktree의
|
||||
`.build`를 정리해도 아래 자료는 별도로 남는다.
|
||||
|
||||
```text
|
||||
.build/tolerance-experiment/go-20261018/baseline/
|
||||
source/strict-source-78cb339.zip, source-hashes.json, revision.txt
|
||||
runtime/ # 공식 EXE·DLL·PDB
|
||||
runtime/tests/ # 같은 Release 검사 EXE·DLL·PDB
|
||||
build/ # 실제 명령·전체 로그·각 exit·선택 CMake compile 명령
|
||||
run/ # 새 INP·CSV 복사본·로그·새 HDF5
|
||||
execution.json, runtime-hashes.json, test-runtime-hashes.json
|
||||
input-hashes-before.json, input-hashes-after.json, environment-before.json
|
||||
reference-comparison.json, official-log-summary.json
|
||||
development-one-increment/ # 최초 ON 진단, 보완 전
|
||||
development-off-control/ # 보존된 공식 OFF EXE의 1증분 대조
|
||||
development-final/ # 보완 후 최종 ON 진단·카운터 관계 검증
|
||||
```
|
||||
|
||||
| 보존 대상 | SHA256 |
|
||||
| --- | --- |
|
||||
| strict source ZIP | `0B655E0257720AD9CEF31274931578FE17C4456B9988C158A61DF88D93792BED` |
|
||||
| 공식 fesa.exe | `3CDB5D9D35F6AD95781044BD311ECB35C9FB121780EF81FC535434BF3515172C` |
|
||||
| 정식 INP 및 공식 작업 복사 | `B8922243CA8DDFE20B1DD39F3123207F789A1A75F6B10DBB6B0BC0B5A40DFFF5` |
|
||||
| 정식 변위 CSV 및 복사 | `422ECF17609E14FD119D60A3D5AF7A0D1BC4A95E0F556608A7D791E2718C56C5` |
|
||||
| 새 공식 HDF5 | `55EF1FFAAE061311F3C1AE705824658B668A818D5F1F7D27BB538CA66EE4792C` |
|
||||
|
||||
원본 reference는 실행·수정하지 않았고 전후 hash가 같다. 기존 결과를 재사용하지 않았다.
|
||||
|
||||
## 계측 없는 공식 실행
|
||||
|
||||
`MSBuild FESA.sln /m:2 /p:Configuration=Release /p:Platform=x64`는 native exit 0이다.
|
||||
VS 18.11/MSVC 14.51 x64, `/O2 /Ob2 /fp:precise /MD`, staged MKL 2026.1,
|
||||
TBB 2023.1, HDF5 2.1.1, Intel runtime 2026.1을 사용했다.
|
||||
기존 Hex8 검사 source의 C4334 경고 1개가 있었고 오류는 없었다.
|
||||
|
||||
호스트는 KOKO, Ryzen 7 5800H(8코어/16논리), 전원은 고성능이다.
|
||||
관련 thread 환경변수와 PATH를 `environment-before.json`에 기록했고 변경하지 않았다.
|
||||
실제 worker 수를 고정·계측한 결과는 아니다. 후보 공식 측정도 같은 환경을 사용해야 한다.
|
||||
측정 중 다른 build/test/solver를 실행하지 않았다. 워밍업 없이 정확히 1회 실행했다.
|
||||
|
||||
| 항목 | 실제 결과 |
|
||||
| --- | --- |
|
||||
| 명령 | 보존된 `runtime/fesa.exe run/shellNL-dyn-meshfine.inp` |
|
||||
| 입력 | dt=.1, T=10, 100증분, 729 nodes, 676 elements |
|
||||
| 시작/종료 UTC | 2026-10-18 15:01:20.6895159 / 16:47:11.4199462 |
|
||||
| native / wrapper exit | 0 / 0 |
|
||||
| 공식 elapsed | **6350.7895521초** |
|
||||
| 프로세스 CPU | 18021.3125초; elapsed와 다른 값 |
|
||||
| 새 HDF5 | 126254676 bytes, schema2, 729 nodes / 101 frames |
|
||||
| peak working set | 종료 후 API가 null을 반환하여 미확보 |
|
||||
| 정식 비교 | comparator exit 0, 사전검사 통과, U blocking 실패 0, UR 경고 200 |
|
||||
| U 최대 family relative RMS | 0.002282424502275574 |
|
||||
|
||||
정식 비교는 기존 9-node U blocking/UR warning 정책 그대로 수행했다.
|
||||
전체 441774 변위 scalar의 identity/finite 확인 후 202 families를 평가했고
|
||||
near-zero 1%, 일반 상대 5%, RMS 1%, zero-scale exact-zero를 유지했다.
|
||||
Python의 로컬 uv cache를 child `PYTHONPATH`로 읽어 numpy 2.4.6/h5py 3.16.0을 사용했다.
|
||||
설치·다운로드·캐시 쓰기를 하지 않았다. 실제 argv와 exit는 원시 로그에 있다.
|
||||
|
||||
공식 hht-log-v1에서 양의 accepted 100건, 평가 204건이며 accepted iteration 합은
|
||||
**outer applied correction 204회**다. 96증분은 2회, 4증분은 3회다.
|
||||
잔차가 통과했지만 보정이 실패한 평가는 27건이다. 보정 채널별 실패 평가 건수는
|
||||
u의 q/v/a/A가 102/103/104/104, 회전은 100/100/102/102다.
|
||||
이는 중복을 허용한 로그 채널 진단이며 배타적 실행시간이나 후보의 절약 가능 회수가 아니다.
|
||||
|
||||
## 별도 1증분 개발 계측
|
||||
|
||||
기존 4개 물리·미분 검사 실패가 남은 상태에서 동일 100증분을 장시간 반복하지 않았다.
|
||||
승인된 별도 개발 입력 복사에서 T만 `.1`로 줄였고 dt=.1과 local kernel·strict 종료는 유지했다.
|
||||
이 입력 SHA256은 `79D1C09FC5AFB792312F1908E13D71ABC228D98D10B438B91412A9BFA4C95A50`다.
|
||||
|
||||
계측은 기본 컴파일 OFF다. Native는 `/p:FesaHhtDevelopmentProbe=true`, CMake는
|
||||
`-DFESA_HHT_DEVELOPMENT_PROBE=ON`으로 켜고, ON 실행에서만
|
||||
`FESA_HHT_PROBE_OUTPUT=<새 sidecar 경로>`를 설정한다. 정상 OFF 객체에는 해당 환경 키가 없다.
|
||||
추가 요소 평가·수치 보정·판정 완화 없이 기존 호출 앞의 계수와 serial scope의 정수 ns 타이머만 쓴다.
|
||||
단일 procedure 개발 실행용이며 동시 다중 procedure 계측을 지원한다는 주장은 없다.
|
||||
병렬 요소 callback은 atomic 계수만 쓰고 타이머/단계 배열은 serial thread만 수정한다.
|
||||
|
||||
최종 개발 실행 native exit 0, 개발 elapsed 77.5268139초다. 이는 공식 시간이 아니다.
|
||||
최초 ON, OFF 대조, 보완 후 최종 ON은 모두 별도 파일로 보존했다.
|
||||
|
||||
| 최종 sidecar 계수 | 값과 범위 |
|
||||
| --- | --- |
|
||||
| outer_applied_correction / inner_applied_correction | 3 / 1; 실제 좌표 갱신만 포함 |
|
||||
| assembly_calls | 14 |
|
||||
| assembly_internal_evaluation | 9464 = 14×676 |
|
||||
| assembly_internal_failure | 3; 대체 시작 trial 평가의 실패이며 최종 해석 실패가 아님 |
|
||||
| assembly_inertia_evaluation | 9461 = 9464−3; internal 실패 뒤 생략된 관성 호출 제외 |
|
||||
| constraint_internal_evaluation | 6760; cache miss의 실제 추가 EvaluateTrial만 포함 |
|
||||
| logical_factor_call / logical_solve_call / logical_rhs | 1274 / 7516 / 7516 |
|
||||
|
||||
Solver 수는 HHT 경계 forwarding wrapper의 논리 호출이다. 실패 시도도 세며 vendor의
|
||||
phase11/22/33 전체 호출수로 해석하지 않는다. inner correction도 stagnation 때문에
|
||||
계산만 하고 적용하지 않은 solve는 제외하지만 logical solve에는 포함한다.
|
||||
계수 관계와 공식 첫 증분의 outer 3회 일치는 `counter-check.json`에서 assert했다.
|
||||
|
||||
| 중복 없는 최종 단계 시간 | 초 |
|
||||
| --- | ---: |
|
||||
| procedure_other | 30.6919325 |
|
||||
| inner_graph | 26.6134860 |
|
||||
| assembly | 13.8891082 |
|
||||
| outer_linear | 0.1445448 |
|
||||
| physical_rates | 0.2650750 |
|
||||
| factor | 5.0312963 |
|
||||
| solve | 0.5715257 |
|
||||
| 합계 = procedure inclusive | 77.2069685 |
|
||||
|
||||
각 `_exclusive_ns`는 자식 시간을 뺀 값이고 합계가 `procedure_inclusive_ns`와 정확히 같음을
|
||||
assert했다. ExecuteReducedHht 경계이므로 CLI 입력·최종 HDF5 finalize·sidecar I/O를 제외한다.
|
||||
요소 kernel 시간은 병렬 wall-time을 포함한 assembly 범위이며 각 요소 CPU 시간의 합이 아니다.
|
||||
전체 100증분의 inner/요소/factor/solve 비용과 후반 채널 분포는 **계측하지 않았다**.
|
||||
첫 증분 계수·시간을 100배하거나 후보 전체 비용으로 외삽하지 않는다.
|
||||
|
||||
## 정확 byte parity와 검증 한계
|
||||
|
||||
공식 frame0/1과 개발 결과의 40개 numeric dataset을 identity/shape/type/finite 확인 후 비교했다.
|
||||
ON 최초·ON 최종·보존된 공식 OFF EXE의 짧은 입력 대조 모두 22개 dataset이 byte 일치하고
|
||||
frame1의 18개 dataset은 불일치했다. **정확 byte parity는 미통과**다.
|
||||
최종 ON의 q/v/physical a 최대 절대차는 각각 6.776263578034403e-21,
|
||||
2.168404344971009e-19, 6.938893903907228e-18이고 energy는 8.881784197001252e-16이다.
|
||||
거의 영인 값의 scalar 상대오차가 클 수 있으므로 최대 절대차만으로 통과 판정하지 않는다.
|
||||
|
||||
동일 보존 OFF EXE의 대조에서도 q/v/a 최대 절대차는 1.3552527156068805e-20,
|
||||
3.552713678800501e-15, 5.684341886080802e-14였다. T 단축·반복 실행·기본 thread 조건·
|
||||
컴파일/계측 영향을 완전히 분리하지 못했으며 원인을 하나로 단정하지 않는다.
|
||||
새 parity tolerance나 floor를 도입하지 않았다. dataset별 차이 수·최대 절대/상대차·첫 위치와
|
||||
원본/실제 bytes는 각 `parity-details.json`에 보존했다. NaN을 동일 처리하지 않고 finite를 요구했다.
|
||||
독립 source 검토에서 산술·평가·진단 순서 변경이 없다고 확인한 사실은 실행 parity 통과와 별개다.
|
||||
|
||||
계측 검사는 no-op에서 2FAIL/native1을 확인한 뒤 카운트·중첩 배타 시간·solver forwarding/
|
||||
실패 보존·실제 inner 1회/0회 갱신의 4개 검사를 native0으로 통과했다.
|
||||
실제 inner 계수 검사는 CMake OFF operators 객체를 연결하면 1FAIL/native1, ON이면 4PASS/native0다.
|
||||
Standalone 준비 중 link/include/MKL runtime 오류와 짧은 clock tick assertion 실패도 별도 보존했다.
|
||||
짧은 tick은 0ns를 허용하고 scope 진입 수와 시간 분할의 exact equality를 검사한다.
|
||||
Standalone에 필요한 기존 설치의 sequential DLL은 테스트 전용이며 공식 runtime에 추가하지 않았다.
|
||||
|
||||
Native ON CLI 최종 빌드는 exit0(경고0), CMake Release 기본 OFF의 변경 production 3개와
|
||||
새 검사 객체 compile은 exit0, ON 새 검사 객체 compile도 exit0이다. source 등록을 양쪽에 맞췄다.
|
||||
OFF에서는 새 검사 identity가 없고 ON에서는 4개다. 전체 CMake link/CTest나 전체 Debug 검사는
|
||||
이번 T2에서 실행하지 않았고 후속 전체 회귀를 대신하지 않는다.
|
||||
기존 local-kernel의 4FAIL은 [기록](local-kernel-experiment.md)을 유지하며 재실행/해결한 것으로 보고하지 않는다.
|
||||
U 통과는 V/a/energy의 외부 검증이나 기존 물리 실패의 해결을 뜻하지 않는다.
|
||||
|
||||
T2 판정은 **기준 보존 완료·개발 진단 한정 확보·전체 검증 미완료**다.
|
||||
후보 측정, 전체 100증분 개발 비용, 정확 parity 원인 분리와 필수 전체 물리/회귀 검증은 남았다.
|
||||
|
||||
## T6 후보 실행물과 공식 측정 미실행
|
||||
|
||||
T6 source는 `0c88cb24c84abc4b3561a1e7975df7a4125134f1`, branch는
|
||||
`dryforge/tolerance-t6`다. 제품 source와 probe·판정 기준은 수정하지 않았다.
|
||||
T4 최종 parent `dd54b0a`와 src/include/msvc 및 CMake 설정의 Git identity가 같고,
|
||||
T4 manifest의 제품 source 5개와 cached probe ON CLI SHA256
|
||||
`6C9264282B117A35A46C3550CAE215B096F60B46F0105B929BA9A1979EBF547F`가 일치한다.
|
||||
T5는 검사와 문서만 변경했다. Source ZIP, 파일 hash, 실행물과 DLL, 실제 명령,
|
||||
환경·전원 및 raw 자료는 부모 작업공간의 다음 위치에 별도 보존했다.
|
||||
|
||||
```text
|
||||
.build/tolerance-experiment/go-20261018/
|
||||
candidate/source/ # pinned source ZIP·SHA256
|
||||
candidate/development-runtime/ # cached probe ON EXE·7 DLL·PDB
|
||||
candidate/development-one-increment/ # 새 짧은 입력·native exit·로그·HDF5·probe
|
||||
candidate/runtime/ # 새 full Native Release probe OFF 실행물
|
||||
candidate/build/ # ON/OFF compile 기록·OFF 명령·raw·exit
|
||||
candidate/official-input/ # 정식 INP·CSV의 새 hash 확인 복사, 미실행
|
||||
T6/ # preflight·진단 script·metadata·검토 기록
|
||||
```
|
||||
|
||||
[T5 물리 검증](physics-validation.md)의 Release153 결과는 144PASS/8FAIL/1SKIP,
|
||||
native exit1이다. 독립 D3 두 검사, drilling gradient, C08 pointwise 및 strict algebra
|
||||
시간 controller 네 실패와 후보 비선형 시간 정확도 R8 미충족이 남는다.
|
||||
보존 strict C08 대조도 5FAIL이며 이를 후보 정확도 통과로 해석하지 않는다.
|
||||
계획의 명시된 실패 분기에 따라 같은 긴 실행을 근거 없이 반복하지 않았고,
|
||||
**공식 dt=.1/T=10/100증분 후보 실행은 시작하지 않았다.**
|
||||
따라서 후보 공식 elapsed, strict 대비 Δ·speedup, 729 nodes/101 frames 완결 결과와
|
||||
정식 9-node U blocking/UR warning reference 판정은 모두 **not-established**다.
|
||||
T2 strict 공식 6350.7895521초와 아래 개발 시간을 비교하지 않는다.
|
||||
|
||||
## T6 초기화와 1증분 개발 진단
|
||||
|
||||
T2 `baseline/development-final`의 T=.1/dt=.1 입력을 새 경로에 복사했고 SHA256
|
||||
`79D1C09FC5AFB792312F1908E13D71ABC228D98D10B438B91412A9BFA4C95A50`가 같다.
|
||||
공식 100증분 입력과 별개다. KOKO/Ryzen 7 5800H, 고성능 전원, 기존 thread 환경과
|
||||
동일 local polar1/inertia3/mass gate1e-3 및 cached MSVC Release probe ON을 사용했다.
|
||||
짧은 고유 TMP/TEMP `C:/ft6d`와 새 `FESA_HHT_PROBE_OUTPUT=.../probe.txt`를 설정했다.
|
||||
상속 GoogleTest 선택 환경은 제거했다. 실행 전 guard는 오래된 MSBuild PID5112를
|
||||
발견해 native launch 전에 중단했다. CPU2.640625초 불변·cl/link/solver/test 부재와
|
||||
T4/T5 종료 확인 후 idle worker만 제외했고 프로세스는 종료하지 않았다.
|
||||
두 preflight와 근거를 T6에 보존했으며 guard 대기는 elapsed에 포함하지 않는다.
|
||||
|
||||
실제 native 실행은 1회다. UTC `2026-10-18T18:07:36.7511886Z`부터
|
||||
`18:08:41.2302276Z`까지 **64.4754692초**, native exit0이며 새 HDF5는
|
||||
2851989 bytes, frame0/1 두 frame이다. 이는 계측 ON 초기화+1증분 개발 시간이고
|
||||
공식 시간·전체 비용·후반 증분 비용으로 외삽하지 않는다.
|
||||
|
||||
| 동일 계수 정의의 1증분 진단 | T2 strict ON | T6 candidate ON |
|
||||
| --- | ---: | ---: |
|
||||
| outer / inner applied correction | 3 / 1 | 2 / 1 |
|
||||
| assembly calls | 14 | 12 |
|
||||
| internal evaluation / failure | 9464 / 3 | 8112 / 3 |
|
||||
| inertia evaluation | 9461 | 8109 |
|
||||
| constraint internal evaluation | 6760 | 5408 |
|
||||
| logical factor | 1274 | 1271 |
|
||||
| logical solve / RHS | 7516 / 7516 | 7513 / 7513 |
|
||||
|
||||
실제 호출 경계와 forwarding 의미는 T2와 같다. Internal=assembly×676,
|
||||
inertia=internal−failure, solve=RHS와 exclusive ns 합=procedure inclusive를
|
||||
`counter-comparison.json`에서 확인했다. 내부 실패3은 대체 시작 trial의 실패이며
|
||||
최종 해석 실패가 아니다. 카운터 감소를 공식 가속률로 변환하지 않는다.
|
||||
|
||||
같은 짧은 입력의 strict ON과 후보 ON HDF5 frame0/1의 40개 numeric dataset을
|
||||
shape/type/finite 및 inventory 확인 후 비교했다. 22개는 정확 byte 일치하고
|
||||
frame1의18개는 달랐다. 후보와 strict의 동일 bytes를 요구하는 합격 검사는 아니며,
|
||||
정책 변경에 따른 전체 state 차이 진단이다. nodal q/v/physical a/A의
|
||||
최대 절대차는 각각 `2.0328790734103208e-20`, `3.552713678800501e-15`,
|
||||
`5.684341886080802e-14`, `5.684341886080802e-14`이며 energy는
|
||||
`8.171241461241152e-14`다. Angular velocity/acceleration·triad·director와
|
||||
force/reaction/residual·element 결과도 전체 dataset별 차이를
|
||||
`state-differences.json`에 보존했다. 이는 정책 차이의 진단 숫자이며 외부 truth,
|
||||
정확도 승인 또는 새 parity tolerance가 아니다. T2 ON/OFF의 기존 ULP 차이도 미해결이다.
|
||||
|
||||
## T6 계측 OFF Release 보존과 정적 확인
|
||||
|
||||
개발 진단 종료 후 VS18.11.0/MSVC14.51.36231 x64에서 전체
|
||||
`MSBuild FESA.sln /m:2 /p:Configuration=Release /p:Platform=x64 /p:FesaHhtDevelopmentProbe=false`
|
||||
를 실행했다. 실제 명령에는 `/verbosity:minimal`만 추가했고 native exit0이다.
|
||||
기존 Hex8 검사 C4334 경고1개를 보존했으며 검사 실행을 빌드 통과로 세지 않는다.
|
||||
새 OFF CLI의 SHA256은
|
||||
`449196EB79EFAB3C62B8DAC4B73C3C76E97EC76F6A77F9D80CE1EE5C8C1F0CFA`,
|
||||
pinned source ZIP은
|
||||
`BF2D12688F778A8F9010C3FE086AA9D5E226209AAF1893AE84EBE841BF0F9C9B`다.
|
||||
EXE/PDB/7개 DLL을 `candidate/runtime`에 보존했으며 DLL SHA는 T2 strict와 모두 같다.
|
||||
ON/OFF solver·CLI compile tlog를 각각 보존했다. OFF solver compile에 probe define이
|
||||
없고 `/O2 /Ob2 /MD /fp:precise`를 확인했으며 102개 solver 객체에
|
||||
`FESA_HHT_PROBE_OUTPUT` 문자열이 없음을 검사했다.
|
||||
|
||||
OFF unit discovery native exit0,883개이며 ON887개에서 probe4개가 빠진 목록이다.
|
||||
Field21개와 T4 후보 종료/물리 후속 gate 및 T5 강체 oracle identity가 존재한다.
|
||||
이는 discovery 확인이며 unit 실행 통과가 아니다. `T6/verification.json`,
|
||||
`verify-artifacts.raw.txt`, `commands.txt`에 실제 범위·exit·미실행 상태를 남겼다.
|
||||
정식 INP/CSV 새 복사 hash는 각각 기존 T2의 `B8922243...DFFF5`,
|
||||
`422ECF17...8C56C5`와 같고 원본 전후 hash도 같다. 공식 복사본은 실행하지 않았다.
|
||||
문서 diff/링크/기록 숫자를 정적으로 검토했으며 tracked 변경은 이 문서뿐이다.
|
||||
|
||||
T6은 공식 장시간 성능과 reference 통과를 확정하지 못한 **DONE_WITH_CONCERNS**다.
|
||||
수치 부적합 해결·기준 완화는 수행하지 않았다. T7의 전체 Native Debug·CMake CTest·
|
||||
별도 Python·필수 reference 검증은 후속 범위이며 이 한정 진단이 대체하지 않는다.
|
||||
|
||||
## 실제 구현과 유지한 경계
|
||||
|
||||
수치 계약은 [field 정식화](formulation-convergence.md), 실제 물리 검증은
|
||||
[물리 검증 기록](physics-validation.md)에 보존했다. Private field 계산의 독립 경계 검사
|
||||
21개, 실제 종료·rollback·로그 연결 검사 11개와 강체 병진의 독립 물리 oracle 1개를 추가했다.
|
||||
개발 probe 검사 4개는 별도 ON 구성에서 검증했으며 기본 OFF의 998개 등록에는 포함하지 않는다.
|
||||
|
||||
힘/병진과 generalized moment/total rotation-vector를 분리해 일반·zero-flux·linear·
|
||||
estimated correction·nonquadratic 규칙을 실제 종료에 연결했다. 평균은 요소 signed
|
||||
internal+physical inertia와 외부 source occurrence로 계산하고 accepted 증분에서 한 번 갱신한다.
|
||||
실제 full endpoint 보정/증분을 사용하며 기존 q/v/a/A `1e-10` 크기는 strict 진단으로 남겼다.
|
||||
`PrivateCorrectionEqual`, frame0, inner/hidden/rank, 선형·update `1e-12`, 전역 평형 `1e-8`,
|
||||
기존 HDF5 schema2와 최종 파일 보호는 유지했다. 수렴 뒤 후속 검사 실패를 자동 완화하지 않는다.
|
||||
|
||||
출력 성공 전 accepted state를 복사해 commit을 검증하고 로그 flush 뒤 state·private history·
|
||||
flux history를 함께 공개한다. 누적 frame 이력 복사 비용이 있으며 공식 후보 시간 측정 시
|
||||
포함해야 한다. 이번에는 그 전체 비용을 측정하지 않았다.
|
||||
2025 공개 규칙의 FESA 대응이며 비공개 Abaqus 요소·관성·회전·특성길이까지 같다는 뜻은 아니다.
|
||||
2024 모델 생성 당시 실제 solver 설정도 미확인이다.
|
||||
|
||||
## 구성된 전체 회귀 실행
|
||||
|
||||
실행 revision은 `710ab39059ab73518e63968067683791a5c8b558`, 실행 증거 commit은
|
||||
`818046c3975684d37ddd6bcb827099bd27652acf`다. 후자는 tracked 파일 변경 없는 실행 기록이다.
|
||||
제품 source는 실제 종료 연결 revision `dd54b0a` 이후 바뀌지 않았다.
|
||||
원시 자료의 기준 경로는 `.build/tolerance-experiment/go-20261018/T7/`다.
|
||||
|
||||
| 실행 | 실제 수 | PASS | FAIL | SKIP | native exit |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| MSVC x64 Debug unit | 883 | 872 | 10 | 1 | 1 |
|
||||
| MSVC x64 Debug integration | 72 | 72 | 0 | 0 | 0 |
|
||||
| MSVC x64 Debug reference | 43 | 43 | 0 | 0 | 0 |
|
||||
| CMake Debug 전체 CTest | 998 | 987 | 10 | 1 | 8 |
|
||||
| 별도 Python suite | 145 | 145 | 0 | 0 | 0 |
|
||||
|
||||
Native 전체도 합계 998개, 987 PASS·10 FAIL·1 SKIP이다. Native build와 CMake
|
||||
configure/build/discovery는 각각 exit0이다. 실제 실행 identity 998개씩을 discovery와
|
||||
대조했고 누락·중복은 없었다. 실패 10개와 N7 skip의 identity도 두 경로에서 같다.
|
||||
Timeout·abort·필터 제외는 없었다. 제품·검사 compile source 223개와 실행 파일 옆 DLL 7개의
|
||||
SHA가 양쪽에서 일치했다. C++17·Debug CRT와 전용 Hex8 source만 AVX2인 범위도 확인했다.
|
||||
Native 명시 `/fp:Precise`, CMake 명시 fp 옵션 없음은 실제 설정 관측값으로 남겼다.
|
||||
|
||||
실제 명령은 `commands.json`, Native 세 `fesa_*_tests.xml`, `ctest.xml`, 각
|
||||
`*.raw.txt`/`*.exit.txt`, `registration-and-timeout.json`, `build-configuration-comparison.json`,
|
||||
`toolchain-environment.json`으로 추적한다. Python은 기존 CP311 캐시 패키지를 쓰는
|
||||
`run_cached_python.py -m pytest tests -q`로 실행했으며 `python-suite.raw.txt`에 결과가 있다.
|
||||
패키지를 다운로드하거나 기본 Python의 import 실패를 전체 환경 부재로 처리하지 않았다.
|
||||
|
||||
Native unit 뒤 실행 보조 도구가 TMP 이름을 잘라 만든 충돌로 integration 시작 전에 중단됐다.
|
||||
제품 executable은 시작되지 않았고 고유 TMP 생성만 수정해 남은 단계부터 재개했다.
|
||||
이미 끝난 build/unit을 반복하지 않았으며 `driver-interruption.json`에 원인·해결을 보존했다.
|
||||
검사 중 source 574개와 정식 reference 34개의 전체 inventory·SHA는 전후 동일하다.
|
||||
|
||||
## 실패·건너뜀과 후보 정확도 한계
|
||||
|
||||
| 실제 실패 묶음 | 수 | 관측과 분류 |
|
||||
| --- | ---: | --- |
|
||||
| DynamicRotation D3 독립 미분·80자리 anchor | 2 | 대표 오차 약0.228로 승인된 1e-3 초과 |
|
||||
| ExactDrillingConstraint force gradient/Hessian | 1 | 대표 오차 약0.00329로 1e-3 초과 |
|
||||
| C08a pointwise·시간 controller·C08b | 5 | hidden defect 약0.00123 및 첫 격자에서 100회 보정 소진. 보존 strict도 같은 5개 실패 |
|
||||
| 실제 HHT LargeDirect·UniformTranslation | 2 | 후보 field 통과 뒤 각각 mass-null·유한회전 increment2의 triad 일관성 실패 |
|
||||
|
||||
마지막 두 identity의 strict 대조는 외부 보정 100회 소진이었다. 특히 strict UniformTranslation은
|
||||
첫 병진에서 실패해 후보의 후속 유한회전에 도달하지 않았다. 원인이 같다고 단정하지 않는다.
|
||||
전체 검사명·실패 메시지·build별 실행 identity는 `verification.json`과 XML에 있다.
|
||||
사용자 승인 local polar1/inertia3/mass gate1e-3와 관련 테스트 오차1e-3을 유지했으며,
|
||||
통과시키기 위한 추가 허용치·기대값 변경이나 실패 검사 삭제는 하지 않았다.
|
||||
|
||||
유일한 SKIP은 `HhtTemporalVerification.C04aFinestReductionComparisonIsNumericallyResolved`다.
|
||||
이는 binary64/solve 해상도 때문에 비교가 미판정인 N7 항목이며 PASS가 아니다.
|
||||
독립 강체 병진의 실제 후보 q/v/physical a/A·energy 검사는 통과했지만 상수 가속도 해다.
|
||||
기존 strict algebra의 시간수렴 통과와 함께 실제 비선형 후보의 q/v/physical a refinement,
|
||||
차수≥1.8·finest 오차·N7/물리 invariant closure를 대체하지 않는다. 이 범위는 여전히 미충족이다.
|
||||
|
||||
## 별도 정식 Hex8 비교와 미확보 자료
|
||||
|
||||
같은 최종 제품 source의 보존된 계측 OFF Release로 정식 원본의 hash 확인 새 복사본을 실행했다.
|
||||
아래 5개는 모두 새 HDF5, CLI exit0, 기존 comparator exit0, precheck·실행 검증·blocking PASS다.
|
||||
|
||||
| 모델 | CLI exit | comparator exit |
|
||||
| --- | ---: | ---: |
|
||||
| hexa_linearstatic | 0 | 0 |
|
||||
| hexa_nonlinearstatic | 0 | 0 |
|
||||
| hexa_freq | 0 | 0 |
|
||||
| hexa_dynlinear | 0 | 0 |
|
||||
| hexa_dynnonlinear | 0 | 0 |
|
||||
|
||||
보고서·입력·HDF5 SHA는 `objective-gate.json`과 `hex8-reference/<case>/`에 있다.
|
||||
Release EXE/PDB/7 DLL도 실행 전후 T6 manifest와 일치했다. 이 실행은 정확도 비교이며,
|
||||
개별 elapsed를 공식 성능이나 과거 baseline 대비 speedup으로 해석하지 않는다.
|
||||
기존 NT11 reference 검사는 불일치를 diagnostic으로 남기는 동작의 통과다. 외부 열 수치 일치가 아니다.
|
||||
|
||||
정식 shellNL-meshfine·shellNL-meshfine_02·shellNL-meshfine-pressure 자료는 없다.
|
||||
`_02`는 자료 부재와 전용 case identity/비교 계약 미구현이 함께 남는다. 새 필수 thermal
|
||||
reference도 미제공이다. `reference/temp`는 사용하지 않았다. T6/T7의 후보 MITC4 100증분은
|
||||
앞서 설명한 수치 실패 분기로 미실행이었다. 후속 새 원본 실행·U 비교는 별도 변위 보고서에 기록했으며,
|
||||
strict baseline이나 1증분 결과로 대신하지 않았다.
|
||||
|
||||
## 최초 실험의 판정과 다음 검증 조건
|
||||
|
||||
**구현됨·수치 부적합·일부 검증 미완료·공식 성능 비교 미성립**으로 판정한다.
|
||||
Strict 공식 6350.7895521초, 1증분 개발 호출 수 감소, 전체 회귀 실행 사실은 서로 다른 근거다.
|
||||
이를 합쳐 전체 가속이나 정식 채택 가능으로 보고하지 않는다.
|
||||
다음에는 local 근사의 독립 미분/물리 정확도와 후보 후속 acceptance 실패의 원인을 분리하고,
|
||||
실제 비선형 후보의 시간 정확도와 N7 판정을 확보해야 한다. 새 원본 U 비교와 단일 CLI 시간은
|
||||
확보했지만, 같은 조건의 공식 성능 비교가 추가로 필요하며 현재 전체 speedup을 확정하지 않는다.
|
||||
현재 branch와 원시 자료를 보존했으며 주 개발 branch 병합·외부 solver·release는 수행하지 않았다.
|
||||
|
||||
## 후속 승인과 남은 단계의 완료
|
||||
|
||||
2026-10-19 사용자는 Abaqus 변위 일치를 이번 실험의 검증 통과 근거로 수용하고 실패10개는
|
||||
보류한 채 남은 단계를 진행하도록 승인했다. 이번 실험의 판정은 **변위 검증 통과·알려진
|
||||
실패 보류로 조건부 완료**다. 실제 Native/CTest 각각987 PASS·10 FAIL·1 SKIP과 Python145 PASS를
|
||||
바꾸지 않았다. Assertion·production acceptance·HHT 계수·local 근사·reference 기준도 그대로다.
|
||||
|
||||
앞선 [변위 비교](reference-displacement-comparison.md)는8개 모델 U PASS다. MITC4 HHT의
|
||||
최악 프레임 정규화 RMS0.22824245%가1% 이내라는 근거이며, 모든 개별 scalar의 상대오차가
|
||||
1% 이내라는 주장은 아니다. 9절점×101프레임 외부 비교 범위, UR100-frame/200-warning을 유지한다.
|
||||
|
||||
기존 두100증분 HDF5를 읽기 전용으로 비교하여 남은 전체 상태 진단을 확보했다.
|
||||
729절점·676요소 identity·101프레임 schedule·dtype/shape/attribute/finite를 대조하고,
|
||||
프레임당20개, 총2020개 numeric dataset을 비교했다. q/v/a/A의 병진과 회전은 단위를 분리했다.
|
||||
|
||||
| strict/candidate 채널 | 전체101프레임 최대 절대차 |
|
||||
| --- | ---: |
|
||||
| 병진 q | 2.18527724e-15 |
|
||||
| 회전 q | 1.52349857e-15 |
|
||||
| 병진 v | 5.05292422e-14 |
|
||||
| 회전 chart rate | 3.51910590e-14 |
|
||||
| 병진 physical a | 7.59479632e-12 |
|
||||
| 회전 chart acceleration | 5.32315680e-12 |
|
||||
| 병진 public A | 6.83806137e-12 |
|
||||
| 회전 public A | 4.79310284e-12 |
|
||||
| kinetic energy | 4.90274488e-13 |
|
||||
| physical strain energy | 2.22044605e-14 |
|
||||
| drilling energy | 1.86347248e-20 |
|
||||
| external work | 2.13162821e-14 |
|
||||
| energy defect | 4.83169060e-13 |
|
||||
|
||||
절대차의 단위는 해당 채널의 입력 일관 단위다. 이 비교는 strict/candidate 차이 진단이며
|
||||
새 tolerance에 대한 합격 판정·정확 byte parity·Abaqus V/a/energy 일치가 아니다.
|
||||
실제 비선형 시간 정확도·N7·물리 검사의 미해결 사실을 대신하지 않는다.
|
||||
|
||||
| 항목 | strict | field 후보 |
|
||||
| --- | ---: | ---: |
|
||||
| 기록된 Release CLI 시간(초) | 6350.7895521 | 5662.1448538 |
|
||||
| 양의100증분 outer 보정 합 | 204 | 171 |
|
||||
| accepted 반복 횟수 분포 | 2회96증분·3회4증분 | 1회29증분·2회71증분 |
|
||||
|
||||
관측 시간은688.6446983초,10.8434501% 감소했고 strict/candidate 비율은1.1216226이다.
|
||||
후보 실행은 정확도 비교 목적이었고 전원·측정 격리 조건 인증이 없어 **공식 speedup으로
|
||||
확정하지 않는다**. Full100 inner/요소 평가/factor/solve/RHS·단계 비용은 미측정이다.
|
||||
첫 증분 개발 계수와 시간을 전체 비용으로 외삽하지 않았다. 알려진 수치 실패의 장시간 반복을
|
||||
피하는 기존 계획에 따라 추가 solver 실행 없이 완료된 결과를 재사용했다.
|
||||
|
||||
원시 진단은 [full-state-comparison.json](../../.build/tolerance-experiment/resume-20261019-170632/full-state-comparison.json)과
|
||||
[프레임 상세](../../.build/tolerance-experiment/resume-20261019-170632/full-state-frame-details.json)에 있다.
|
||||
문서·읽기 전용 진단만 추가했으므로 전체 build/test를 반복하지 않았으며, 변경 없는 source와
|
||||
보존된 전체 검증 evidence를 hash로 확인하고 수치·링크·정적 검사를 수행했다.
|
||||
보류10개·N7·자료 부재·공식 성능 조건은 후속 과제다. 주 개발 branch 병합·push·release는 하지 않았다.
|
||||
@@ -53,6 +53,32 @@ procedure의 검증을 모두 통과한 candidate만 accepted state와 history
|
||||
시점부터 적용한다. 반력은 열저장까지 포함한 full residual의 constrained projection이다.
|
||||
중앙면 양쪽 flux trace는 서로 다른 위치이며 평균하지 않는다.
|
||||
|
||||
## MITC4 drilling의 선택과 인공 모멘트
|
||||
|
||||
현재 모델 전체는 한 코드 설정으로 직접 초기 director 스프링 또는 기존 상대 비틀림을
|
||||
선택한다. 기본값은 직접 스프링이며 선형·비선형 정적·정적 pressure·frequency·HHT에
|
||||
같이 적용한다. Beam·Hex8·DS4에는 적용하지 않는다. Runtime·section별 선택은 현재 없다.
|
||||
|
||||
직접 방식에서 `d0`는 초기 global 단위 director, `phi`는 전체 global rotation vector다.
|
||||
`gamma=d0·phi`, `Udrill=0.5*kd*sum(gamma²)`, `frot=kd*gamma*d0`,
|
||||
`Krot=kd*d0*d0^T`다. 병진·절점 간 coupling·drilling force의2차 방향미분은 exact zero다.
|
||||
Kd는 BC 전 초기 physical rotary 대각8개의 overflow 방지 평균에 `1e-3`을 곱해 고정한다.
|
||||
Zero 대각도 평균에 포함하며 음수/nonfinite·전부 zero·nonpositive kd는 실패다.
|
||||
Floor·density 치환·trial 재조정은 없다. 유한 강체 회전의 인공 에너지·모멘트는 허용한다.
|
||||
|
||||
직접 HHT hidden 식은 실제 관성의 normalized null `n(phi)`에 대해
|
||||
`sum_e kd*gamma_ei*(n·d_ei0)-n·Fext_rot`이며 같은 trial의 고차 미분을 사용한다.
|
||||
공유 node의 외력 항은 한 번만 뺀다. 일정 drilling 접선이 null의 고차 미분을 없애지는 않는다.
|
||||
기존 방식의 계산은 polar1·관성3차 근사와 기존 판정을 보존하며 정확도 실패를 자동 수정하지 않는다.
|
||||
|
||||
내부 spatial `Mdrill=sum[x cross fdrill_trans+Jl(phi)^(-T)*fdrill_rot]`를 source 순서로
|
||||
누적한다. 선형은 reference 위치/작은 회전, 비선형/HHT는 현재 위치/기존 SO(3) 변환이다.
|
||||
기존 raw 평형은 그대로이고 보정 평형은 `[raw_force,raw_moment-Mdrill]`다.
|
||||
직접 방식의 기존 전역 gate만 보정값을 사용하고 기존 방식은 raw를 사용한다.
|
||||
기존 scale·exact-zero·허용치는 유지하며 인공 모멘트를 분모에 더하지 않는다.
|
||||
비선형 정적의 전역 수치 gate는 기존 reference 비교기에 있으며 procedure에 새 gate를 만들지 않는다.
|
||||
Marker 없는 옛 결과의 방식은 미상이다. 선형/HHT 변위 비교기에는 새 평형 수치 family를 만들지 않는다.
|
||||
|
||||
## 외부 수치 비교
|
||||
|
||||
먼저 source identity·row 집합·component·schema·단위·좌표·위치와 finite 여부를 검사한다.
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
|
||||
Hex8와 reduced-coordinate shell HHT의 실제 병목을 VTune으로 조사하고, 승인된 물리·입력·정밀도·결과 의미를 유지하며 실행 시간을 줄이는 작업이다. 새 요소·하중·적분법·공용 cache framework 추가는 범위 밖이다. 요구사항 `FEM-REQ-PERF-001..009`의 시간 목표와 검증은 별개이며 현재 최종 완료 상태는 아니다.
|
||||
|
||||
## 현재 MKL 연결과 확인 범위
|
||||
|
||||
2026-10-20 MKL threading layer를 TBB로 변경했다. 직접 MSBuild와 CMake의 x64
|
||||
Debug·Release 네 빌드는 모두 종료0이며 CLI와 세 테스트 실행 파일 총16개의 링크 및
|
||||
인접 DLL48개의 일반·지연 PE 의존성과 설치 원본 해시를 정적으로 확인했다.
|
||||
Debug는 `mkl_tbb_threadd`·`tbb12_debug`, Release는 `mkl_tbb_thread`·`tbb12`를 사용하고
|
||||
HDF5의 `libmmd.dll`을 유지한다. 근거는 `.build/mkl-tbb-20261020-01/`에 보관했다.
|
||||
|
||||
사용자 지시로 이번 설정 변경에서는 테스트·모델·수치 비교·성능 측정·VTune을 실행하지
|
||||
않았다. 실제 DLL loading, 수치 정확성, 속도 향상과 OpenMP 대기 제거는 미검증이다.
|
||||
기존 OpenMP 전용 local-thread/TLS helper와 HHT cache 진단 코드는 유지했으며 TBB에서의
|
||||
동등성도 확인하지 않았다. 아래 과거 시간·검증 기록을 새 TBB 구성의 결과로 승계하지 않는다.
|
||||
|
||||
## 채택한 계산 경로와 수명
|
||||
|
||||
[Matrix::Multiply](../../src/fesa/math/matrix.cpp)는 dense GEMV/GEMM, [SparseMatrix::Multiply](../../src/fesa/math/sparse_matrix.cpp)는 CSR validate·finite/RHS/checked MKL_INT 확인 후 `mkl_sparse_d_create_csr → mkl_sparse_d_mv → destroy`를 사용한다. Const 호출마다 변환 배열·values 복사·handle을 소유하므로 copy/move·동시 호출에 backend pointer를 공유하지 않는다. Empty shape도 입력 검증 뒤 zero vector를 반환한다. 지속 symbolic cache나 scalar fallback은 없다. 호출마다 CSR 배열 복사 비용이 있으므로 MKL 연결만으로 전체 성능 향상을 주장하지 않는다.
|
||||
|
||||
@@ -95,6 +95,40 @@ Schema·필수 field·dtype·shape·inventory·finite 불일치는 읽기나 비
|
||||
누락을 0·NaN·기본 marker로 채워 이용하지 않는다. Mode shape는 응답 변위가 아니며
|
||||
frequency와 raw lambda도 다른 양이다. 작은 음의 lambda를 abs/clamp하여 raw 값을 바꾸지 않는다.
|
||||
|
||||
## MITC4 drilling 추가 진단
|
||||
|
||||
기존 feature/procedure/schema 번호를 유지한다. 새 MITC4 구조 결과는 실제 계산한 state의
|
||||
방식을 저장하며 writer가 현재 코드 기본값으로 추정하지 않는다. Beam·Hex8·thermal의
|
||||
metadata·dataset·report 계약에는 아래 필드를 추가하지 않는다.
|
||||
|
||||
| 위치 | 필드 | 형식과 의미 |
|
||||
| --- | --- | --- |
|
||||
| `/metadata` attribute | `mitc4_drilling_formulation` | UTF-8 scalar; `direct-initial-director-spring` 또는 `legacy-corotated-twist` |
|
||||
| `/metadata` attribute | `drilling_diagnostics_version` | U64LE scalar, 1 |
|
||||
| `/steps/Step-1/frames/<k>/global` | `drilling_moment` | F64LE `[3]`, global spatial 내부 복원모멘트 XYZ, force×length |
|
||||
| 같은 위치 | `equilibrium_with_drilling` | F64LE `[6]`, raw force XYZ와 raw moment에서 drilling moment를 차감한 XYZ |
|
||||
|
||||
기존 `global/equilibrium`이 있는 선형/비선형 정적·HHT의 모든 frame에 두 dataset을
|
||||
추가하며 HHT frame0도 포함한다. Eigen에는 두 metadata attribute만 추가하고 가상 평형
|
||||
frame을 만들지 않는다. Raw equilibrium의 기존 값·단위·의미는 유지한다.
|
||||
|
||||
일반 reader의 두 marker가 모두 없는 옛 파일은 옛 계약으로 읽고 진단은 미상/null이다.
|
||||
한 marker만 있거나 알 수 없는 방식/version·필수 dataset 누락·dtype/shape/finite 오류는
|
||||
실패다. 모든 applicable frame에서 보정 force는 raw와 같고 보정 moment는 binary64의
|
||||
`raw-Mdrill`과 정확히 같아야 한다. 작은 값에 absolute tolerance를 더해 허용하지 않는다.
|
||||
새 candidate self-check는 actual state로 marker 필요 여부를 판단하므로 두 marker를
|
||||
모두 삭제해도 옛 파일로 오인해 통과시키지 않는다. Write·close·readback·최종 교체 실패는
|
||||
이전 최종 HDF5를 보존한다.
|
||||
|
||||
기존 MITC4 JSON report는 schema/version·기존 키를 유지하고 `drilling_diagnostics`를
|
||||
추가한다. 옛 markerless 결과는 `null`이다. 새 값은 `formulation` 문자열, `version:1`,
|
||||
`frames` 배열이다. 각 frame은 `storage_step:"Step-1"`, 정수 `frame_index`,
|
||||
숫자 배열 `raw_equilibrium[6]`, `drilling_moment[3]`, `equilibrium_with_drilling[6]`을
|
||||
가지며 storage frame 번호 순서로 나열한다. 기존 C++ typed report도 같은 정보를 보존한다.
|
||||
Eigen typed 진단의 frames는 빈 배열이며 별도 eigen JSON API를 새로 만들지 않는다.
|
||||
비선형 정적의 기존 moment family만 marker에 따라 판정값을 고른다. 선형/HHT 비교기는
|
||||
진단을 사전검사하며 기존 family·U blocking/UR warning·수치 verdict를 유지한다.
|
||||
|
||||
## C++ consumer와 비교 도구
|
||||
|
||||
공개 interface는 `include/fesa`의 backend-neutral 값과 `Status`/`Result<T>`를 사용한다.
|
||||
@@ -105,3 +139,27 @@ Procedure는 family별 completed outcome을 반환하며 로그 close와 최종
|
||||
Hex8/HHT Python 비교기는 입력·reference·HDF5를 읽고 지정한 JSON report만 쓴다.
|
||||
Solver를 기동하거나 원본을 수정하지 않는다. 입력 부재·hash 충돌·schema/identity/finite
|
||||
불일치는 실패이며 합성 fixture 통과는 실제 모델 성공을 뜻하지 않는다.
|
||||
|
||||
## Shell HHT iteration 로그 독자
|
||||
|
||||
Frame0과 직접 v1 writer는 기존 `hht-log-v1` 의미를 유지한다. 양의 reduced-coordinate
|
||||
MITC4 HHT 증분은 `hht-log-v2`, `policy_id=abaqus2025-field-on-fesa-v1`을 기록하므로
|
||||
한 파일 안의 각 record에서 schema를 읽는다. v1 숫자를 v2 field 값으로 재해석하지 않는다.
|
||||
V2의 고정 키 순서·분기·단위·평균·threshold는
|
||||
[field 수렴 정식화](abaqus-tolerance/formulation-convergence.md#로그-소비-계약)를 따른다.
|
||||
`strict_` 진단은 기존 힘/모멘트 residual과 무차원 q/v/a/A correction이며 종료 기준이 아니다.
|
||||
|
||||
UTF-8 LF 한 줄과 tab 구분 key=value를 사용한다. 첫 `=`만 구분자이며 값의 backslash,
|
||||
tab, LF, CR은 각각 `\\`, `\t`, `\n`, `\r`로 escape한다. 미평가 numeric/bool은
|
||||
literal `not-evaluated`, 평가 bool은 `0`/`1`이다. 독자는 중복·필수키 누락·알 수 없는
|
||||
branch/policy/schema·NaN/Inf를 오류로 처리한다. 숫자는 locale-independent binary64
|
||||
round-trip 표현이며 step은 raw source 이름이다. 실패 전 평가된 부분값만 보존한다.
|
||||
`field_pass=1`은 후속 acceptance 통과를 뜻하지 않는다. `accepted=1`도 최종 HDF5 성공을
|
||||
뜻하지 않으며 CLI exit와 완결 결과 identity를 함께 확인한다. 실패 증분은 accepted
|
||||
frame/private/flux history에 반영되지 않는다.
|
||||
|
||||
현재 acceptance transaction은 기존 accepted frame 전체를 가진 state를 지역 staged copy로
|
||||
검증·commit한 뒤 로그 write/flush 성공 시 move로 공개한다. private/flux history도 같은
|
||||
성공 경계에서만 교체한다. 이 최소 구현은 증분마다 누적 frame 수에 비례하는 copy 시간과
|
||||
추가 메모리를 사용하므로 전체 실행의 복사 비용은 frame 수에 대해 이차로 증가할 수 있다.
|
||||
후보의 실제 elapsed 측정에는 이 비용을 포함하며 출력 실패 안전성을 위해 이를 생략하지 않는다.
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
# 재현된 함정과 대응
|
||||
|
||||
## 작은 직접 스프링에서 중간 곱이 먼저 사라진 경우
|
||||
|
||||
양의 subnormal kd에 먼저 `0.5*kd`를 계산하면 energy의 AD 미분에서 force/tangent가
|
||||
사라질 수 있다. 직접 force와 일정 tangent는 명시적 선형식으로 계산하고 energy는
|
||||
`0.5*gamma*(kd*gamma)`로 평가한다. 양의 작은 scale을 floor로 올리는 대응은 하지 않는다.
|
||||
독립 tiny-scale oracle로 energy·force·tangent를 각각 검사했다.
|
||||
|
||||
## 관성 null과 drilling primitive가 달라진 경우
|
||||
|
||||
Cubic 관성의 실제 null은 정확 SO(3)의 closed null과 다르다. 직접 스프링의 hidden
|
||||
미분에 후자를 사용하면 실제 관성과 다른 제약을 풀게 된다. 실제 cubic Exp/Jacobian의
|
||||
normalized solve와 그 미분을 사용한다. 별도 physical force의 null 투영도 정확히0이라고
|
||||
가정할 수 없어 `n^T(full force-external)`로 canonical drilling scalar를 재구성하지 않는다.
|
||||
Full mass/null·triad·physical residual gate는 그대로 남으며 이 선택이 기존 D3 실패를 해결하지는 않는다.
|
||||
|
||||
## 빈 null과 미평가 scalar를 혼동한 경우
|
||||
|
||||
방향 응답을 요청하지 않은 assembly의 canonical scalar가 비어 있다는 이유만으로
|
||||
all-rotation-fixed/all-fixed 초기화를 거부했던 회귀를 재현했다. Scalar는 실제 admissible
|
||||
null이 소비할 때 요구한다. 필요한 scalar의 누락/Inf는 계속 실패이며 미평가를0으로 채우지 않는다.
|
||||
방식이 다른 cache도 typed configuration identity로 구별한다.
|
||||
|
||||
## 새 candidate를 옛 markerless 파일로 오인한 경우
|
||||
|
||||
호환 reader만으로 새 파일을 검사하면 marker 두 개를 모두 지운 candidate가 옛 파일로
|
||||
읽힐 수 있다. Writer self-check는 actual state의 진단 존재를 별도로 요구한다.
|
||||
양 marker/양 dataset 삭제와 write/readback close 실패 주입에서 이전 final bytes 보존을 확인했다.
|
||||
일반 옛 파일 reader의 null 진단은 유지하며 형식 검사를 위한 tolerance floor를 추가하지 않는다.
|
||||
|
||||
## 긴 임시 경로가 출력 충돌처럼 보인 경우
|
||||
|
||||
Integration 검증에서 TMP 아래 lease 경로가 260/261자가 되어 `output-busy`가 발생했다.
|
||||
|
||||
@@ -2,6 +2,38 @@
|
||||
|
||||
## 범위와 현재 상태
|
||||
|
||||
2026-10-20 현재 drilling 기본값은 `gamma_i=d_i0·phi_i`의 직접 작은 스프링이며
|
||||
기존 상대 비틀림도 모델 전체의 코드 설정으로 선택한다. 직접 경로는 drilling용
|
||||
polar/swing/twist를 생략하고 physical 회전·관성·고정 null 기저·시간적분은 유지한다.
|
||||
직접 hidden scalar는 실제 cubic 관성이 계산한 normalized null `n_i(phi)`에 대해
|
||||
`sum_e kd*gamma_ei*(n_i·d_i0)-n_i·Fext_rot`다. 그 같은 식의 gradient·Hessian·
|
||||
필요한 3차 방향미분을 사용한다. 초기 director를 현재 null로 고정하지 않는다.
|
||||
|
||||
현재 source `6bbbb2b`를 전체 실행한 Debug Native/CTest는 각각1035개 중
|
||||
1024 PASS·기존10 FAIL·1 INCONCLUSIVE SKIP이며 별도 Python164개는 PASS다.
|
||||
정식729node·101frame Direct Release 해석과 외부9node U의101 families 비교는
|
||||
CLI0/비교0, 최대 정규화 RMS0.2282326248391787%로 통과했고 UR 경고200개를 유지했다.
|
||||
기존 방식은 별도 Debug의 관련 검사와 실제 소형3frame HHT 출력을 확인했다.
|
||||
729node 기존 방식의 외부 U 재해석은 이번에 하지 않았다. 과거 결과를 현재 PASS로 승계하지 않는다.
|
||||
기존10개 물리 실패·N7 미판정·정식 자료 누락이 남으므로 전체 검증 완료는 아니다.
|
||||
아래 2026-10-19 수용 기록은 이전 수렴 실험의 결정이며 현재 전체 PASS를 뜻하지 않는다.
|
||||
|
||||
2026-10-19 후속 사용자 승인으로 이번 수렴 규칙 실험은 **Abaqus 변위 검증 통과·실패10개 보류로 조건부 완료**한다.
|
||||
[수용 범위](../tracking/decisions/0011-tolerance-experiment-acceptance.md)를 따르며, 아래 미해결 물리 검사·
|
||||
N7와 실제 FAIL/SKIP은 보존한다. 전체 상태729절점·101프레임 진단과 관측 시간 비교는
|
||||
[최종 결과](../abaqus-tolerance/results.md#후속-승인과-남은-단계의-완료)에 있다. 공식 speedup은 미확정이다.
|
||||
|
||||
2026-10-18 후속 사용자 지시로 polar 반복 12→1회, 관성 회전/Jacobian 급수 32→3차,
|
||||
질량 대칭·null/Gram·분해 복원 검사 1e-12→1e-3의 local 근사 실험을 적용했다.
|
||||
최초 엄격한 기준의 관련 요소 46개 중 5개가 실패했다. 후속 사용자 지시로 관성·접선·
|
||||
HHT 물리 테스트의 오차 기준을 1e-3으로 완화했다. 이후 Abaqus2025 공개 field 규칙을
|
||||
양의 증분의 실제 종료에 연결하고 accepted 평균 이력·hht-log-v2를 구현했다.
|
||||
2026-10-19 Native Debug/CTest 전체는 각각998개 중987 PASS·10 FAIL·1 SKIP이다.
|
||||
새 정책의 실제 조기 종료·rollback·강체 병진 oracle는 통과했으나 D3·drilling·C08와
|
||||
후속 mass-null/triad 검사가 실패한다. 실제 비선형 후보 시간 정확도는 미검증이다.
|
||||
아래 정식화의 유한회전 항등식과 과거 검증을 이 근사본의 통과로 승계하지 않는다.
|
||||
변경 범위와 실제 검증 한계는 [실험 기록](../abaqus-tolerance/local-kernel-experiment.md)을 따른다.
|
||||
|
||||
MITC4 shell의 유한회전 기하비선형 동해석이다. S4/S4R은 FESA-MITC4로 해석하며
|
||||
외부 solver 내부 알고리즘과의 동등성을 뜻하지 않는다. 현재 채택 경로는
|
||||
**nodal-lumped-kinetic 질량, reduced-coordinate HHT, 초기 null의 고정 직교 보완기저,
|
||||
@@ -13,8 +45,13 @@ shell schema v2**다. 과거 consistent 질량·full-coordinate v1 근거를 변
|
||||
사용자 중단 exit -1은 수치 FAIL이나 PASS가 아니다. 이후 2026-10-14 성능 baseline은
|
||||
원본을6372.325621초/native0으로 완료하고 schema v2·729nodes/101frames를 확인했다.
|
||||
9-node U blocking 비교는 PASS(최대 relative RMS .0022824245), UR은 경고200건이었다.
|
||||
이는 baseline source의 결과다. 최신 최적화 source의 새 원본 비교·C08b·전체 회귀·독립
|
||||
전체 물리 판정은 남아 있으며 전체 green이 아니다. [성능 요약](../calculation-performance/README.md)을 함께 본다.
|
||||
이는 과거 baseline source의 결과다. 2026-10-19 같은 승인 local 근사본의 strict 원본을
|
||||
6350.7895521초/native0으로 완료했고 정식 U 비교는 통과했다. T6/T7에서는 수치 실패로
|
||||
field 후보100증분을 실행하지 않았다. 후속 사용자 요청으로 Release 후보 원본을
|
||||
5662.144854초/native0으로 완료했고9-node/101-frame U 비교는 PASS다(최대 정규화 RMS0.22824245%).
|
||||
[변위 비교 보고서](../abaqus-tolerance/reference-displacement-comparison.md)에 빌드·시간·coverage를 기록했다.
|
||||
비선형 시간수렴·전체 물리 판정과 공식 speedup은 미성립이다. 전체 회귀는 위 수치대로 실제 실행했으며 전체 green이 아니다.
|
||||
[수렴 정책 실험 결과](../abaqus-tolerance/results.md)에 현재 근거를 보존했다.
|
||||
|
||||
## 채택 방법과 산술 의미
|
||||
|
||||
@@ -55,7 +92,7 @@ Schema identity는 `(hht-implicit-dynamics,hht-implicit-dynamics,2)`다.
|
||||
`reduced-coordinate-HHT-with-physical-rate-reconstruction`,
|
||||
`mass_formulation=nodal-lumped-kinetic`,
|
||||
`acceleration_convention=physical-endpoint-and-auxiliary-separated`다.
|
||||
모든 result dataset version도 2이며 A의 quantity_semantics는
|
||||
기존 schema2 result dataset의 version은2이며 A의 quantity_semantics는
|
||||
`full-chart-HHT-filter-combination-not-predictor-driver`다. 잘못된 marker·혼합 version·mass·
|
||||
inventory·nonfinite는 dynamic-output-schema 실패다. Historical v1 reader branch는
|
||||
과거 full-coordinate 결과만 읽으며 marker를 보충하거나 v2로 재표시하지 않는다.
|
||||
@@ -71,6 +108,13 @@ inventory·nonfinite는 dynamic-output-schema 실패다. Historical v1 reader br
|
||||
| nodal/reaction | [N,6]; support-on-structure spatial RF/RM, moment는 Jl^-T 변환 |
|
||||
| element/shell/local_frame, generalized_strain, section_resultant, stress | [E,4,3,3], [E,4,8], [E,4,8], [E,4,3,3]; stress는 bottom/middle/top × S11/S22/S12 |
|
||||
| global/energy, global/equilibrium | [5] kinetic/physical/drill/work/defect, [6] 관성 포함 force/moment balance |
|
||||
| global/drilling_moment, global/equilibrium_with_drilling | [3] 내부 spatial drilling moment, [6] raw force와 raw moment에서 drilling moment를 차감한 진단; frame0 포함 |
|
||||
|
||||
새 MITC4 출력에는 실제 방식의 UTF-8 `mitc4_drilling_formulation`과 U64LE
|
||||
`drilling_diagnostics_version=1`을 metadata에 추가한다. 기존 schema2와 raw equilibrium은
|
||||
유지한다. 직접 방식의 기존 전역 gate만 보정 moment를 사용하고 기존 방식은 raw 판정이다.
|
||||
Marker 없는 옛 결과는 방식 미상이며 새 진단을 추정하지 않는다. 변위 비교기는 새 진단의
|
||||
모든 frame을 사전검사하지만 새 평형 수치 family나 verdict를 만들지 않는다.
|
||||
|
||||
Row는 model source inventory와 결합한다. Generalized moment와 spatial moment, physical a와
|
||||
A, drilling force와 physical stress를 혼합하지 않는다. 영 BC는 q/v/a/A exact zero다.
|
||||
@@ -100,18 +144,27 @@ full accepted frame은 같은 acceptance 뒤 commit한다. Full residual·recove
|
||||
검증 전에 일부 이력을 commit하지 않는다. [DynamicHdf5ResultsWriter](../../src/fesa/io/hdf5/dynamic_hdf5_results_writer.cpp)는
|
||||
완결 이력을 write/close/reopen/self-check한 뒤 final을 교체한다. 실패 candidate는 기존 final을
|
||||
보존한다. Accepted history는 실행 중 메모리에 있어 강제 종료 로그만으로 복구되지 않는다.
|
||||
Field 평균 후보는 반복마다 재계산하고 accepted 증분에서 한 번 갱신한다. State의 staged
|
||||
copy에서 commit을 검증하고 로그 flush 성공 뒤 state·private/flux history를 공개한다.
|
||||
누적 frame 이력 복사 비용이 있으며 공식 후보 elapsed에 포함할 대상이다.
|
||||
|
||||
## 검증 기준·교훈·남은 한계
|
||||
|
||||
N2 full force/moment·q/v/a/A correction은 채널별 1e-10, 실제 scaled solve backward error는
|
||||
1e-12다. N3 update는 상쇄 전 same-unit scale의 1e-12와 exact-zero branch를 유지한다.
|
||||
현재 양의 증분은 `abaqus2025-field-on-fesa-v1`로 종료한다. 힘/모멘트 flux 평균과
|
||||
full endpoint u/total-φ의 실제 보정·증분을 사용하며 normal/zero-flux/linear/nonquadratic과
|
||||
추정 보정을 구분한다. 기존 N2 full force/moment·q/v/a/A의1e-10은 strict 진단이다.
|
||||
PrivateCorrectionEqual과 회전 전용 보정의 기존 strict 선택 자격은 유지했다.
|
||||
Frame0·후속 hidden/rank/regularity·전역 평형1e-8도 별도 gate다.
|
||||
실제 scaled solve backward error는1e-12다. N3 update는 상쇄 전 same-unit scale의1e-12와 exact-zero branch를 유지한다.
|
||||
Pair increment의 exact expansion 검사와 RN-even 저장 검사는 별도다. Hidden 위치 조건·rank/
|
||||
regularity를 유지하되 hidden rate 진단을 임의 Newton gate로 바꾸지 않는다. 일반 dynamic
|
||||
operator에 대칭성을 강제하지 않는다. 일반 nonlinear HHT energy에 매 frame 보존·감소를
|
||||
요구하거나 numerical dissipation을 physical damping으로 기록하지 않는다.
|
||||
|
||||
시간 검증은 q/v/physical a의 마지막 두 refinement 감소·차수 >=1.8 및 finest normalized
|
||||
error <=1e-4를 요구한다. N7 read-only linear/nonlinear defect solves와 arithmetic indicator는
|
||||
시간 검증은 q/v/physical a의 마지막 두 refinement 감소·차수 >=1.8을 요구한다.
|
||||
이번 승인 근사 실험의 관련 테스트 오차는1e-3이다. 기존 strict algebra의 finest1e-4
|
||||
검사도 그대로 통과했지만 실제 비선형 후보의 refinement·finest 오차 판정은 미완료다.
|
||||
N7 read-only linear/nonlinear defect solves와 arithmetic indicator는
|
||||
100배 separation을 확인하며 state polishing이 아니다. 원본의 성긴 dt/T1≈7.4567에서
|
||||
변위 일치와 시간 정확도를 구분한다.
|
||||
|
||||
@@ -120,7 +173,8 @@ error <=1e-4를 요구한다. N7 read-only linear/nonlinear defect solves와 ari
|
||||
정책으로 해소됐고 후속 C09/C08a7 통과와 구분한다. 최신 Debug h=1/64 완료 요약의 이전
|
||||
Release 일치는 전체 raw state bitwise나 전체 시간수렴 증거가 아니다. 원본 accepted residual
|
||||
최대 병진1.889238010e-15·회전1.452384011e-15도 Abaqus 변위 오차가 아니다.
|
||||
C08b finer-grid·최신 최적화 source의 원본 HDF5 비교·전체 회귀·전체 물리 closure가 남는다.
|
||||
C08b finer-grid·현재 field 후보의 비선형 시간 정확도·전체 물리 closure가 남는다.
|
||||
전체 회귀의 실제 실패와 해상도 SKIP을 과거 통과로 덮지 않는다.
|
||||
|
||||
## 보존한 과거 수식과 연구의 위치
|
||||
|
||||
|
||||
@@ -16,12 +16,18 @@ component selector를 아래 고정 ON 기저로 대체한다. 기존
|
||||
## 1. 적용 가정과 물리 계약
|
||||
|
||||
Total Lagrangian MITC4, 큰 변위·유한회전·작은 탄성 변형률, 등방 선형 탄성,
|
||||
objective drilling potential, `nodal-lumped-kinetic`, 기존 공간 적분·두께 적분과
|
||||
선택한 drilling potential, `nodal-lumped-kinetic`, 기존 공간 적분·두께 적분과
|
||||
fixed-initial total rotation vector를 유지한다. 재료 history update는 없다.
|
||||
관성·내력·공간 각속도·단면력은 [기존 물리 정식화](formulation.md)와
|
||||
[관성 상세](formulation-inertia.md)가 소유한다. 좌표 변경은 constitutive law,
|
||||
mass measure, drilling stiffness 또는 recovery formula를 바꾸지 않는다.
|
||||
|
||||
2026-10-20 기본 직접 스프링은 별도로 승인한 numerical potential 변경이다.
|
||||
고정 기저·partial-BC 교집합·graph/rate 복원·regularity 계약은 유지하며, hidden primitive만
|
||||
직접 힘과 실제 cubic 관성 null에서 일관되게 계산한다. 기존 objective twist를 선택하면
|
||||
그 현행 계산과 판정을 보존한다. 현재 null을 초기 director로 고정하거나 partial BC를
|
||||
full clamp로 바꾸어 새 스프링의 rank/regularity 실패를 피하지 않는다.
|
||||
|
||||
Full node-major ordering은 `q_i=(u_x,u_y,u_z,phi_X,phi_Y,phi_Z)`다.
|
||||
`u`: length, `phi`: rad, `v=dot q`, `a=ddot q`이며 `v_phi`와 `a_phi`는
|
||||
공간 각속도·각가속도가 아니다. Zero fixed BC를 제거한 공간에서
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
|
||||
## 1. 회전과 관성의 미분
|
||||
|
||||
2026-10-20 drilling 선택 변경은 아래 관성의 수학 유도나 현행 cubic 근사 차수를 바꾸지
|
||||
않는다. 기본 직접 스프링의 hidden 미분은 실제 cubic 관성 null을 사용한다. 아래 정확
|
||||
SO(3) 유도와 현재 근사 구현의 D3/null 항등식 검증 상태를 구분한다.
|
||||
직접 스프링은 유한 강체 회전에서 인공 내부 spatial moment `Mdrill`을 허용하므로,
|
||||
아래 objective total energy의 무일 가정은 직접 drilling에 적용하지 않는다.
|
||||
Raw 관성 포함 평형을 보존하고 직접 방식의 기존 전역 gate에는 moment에서 `Mdrill`을
|
||||
차감한다. 기존 방식은 raw 판정이며 physical mass·momentum·a/A·history 정의는 그대로다.
|
||||
|
||||
`P=[phi]_×`, `theta=|phi|`에 대해
|
||||
|
||||
\[
|
||||
|
||||
@@ -15,7 +15,16 @@ physics와 graph/rate 계약은 유지하되, identity-coordinate 전용 설명
|
||||
그대로 적용하지 않는다. 실제 augmented scaling·초기화·CA/N7의 새 basis 경계는
|
||||
해당 상세 문서를 따른다.
|
||||
|
||||
기존 Total Lagrangian MITC4, objective numerical drilling potential, isotropic linear elasticity, nodal lumped kinetic measure, 공간 적분과 fixed-initial global chart를 유지한다. 좌표는 node-major `q=(u_x,u_y,u_z,phi_X,phi_Y,phi_Z)`, `v=dot q`, `a=ddot q`; 병진 단위는 length, 회전은 rad다. 질량 추가, damping, stiffness modification, stress averaging은 없다. 공개 load subset은 기존 constant global force, 구속은 fixed zero다. 아래 smooth F(t)의 시간미분은 C08a 내부 manufactured oracle에만 해당한다.
|
||||
기존 Total Lagrangian MITC4, 선택한 numerical drilling potential, isotropic linear elasticity, nodal lumped kinetic measure, 공간 적분과 fixed-initial global chart를 유지한다. 좌표는 node-major `q=(u_x,u_y,u_z,phi_X,phi_Y,phi_Z)`, `v=dot q`, `a=ddot q`; 병진 단위는 length, 회전은 rad다. 질량 추가, damping, stress averaging은 없다. 공개 load subset은 기존 constant global force, 구속은 fixed zero다. 아래 smooth F(t)의 시간미분은 C08a 내부 manufactured oracle에만 해당한다.
|
||||
|
||||
2026-10-20 기본 직접 스프링의 primitive는
|
||||
`psi_ei=kd*(d_i0·phi_i)*(n_actual(phi_i)·d_i0)`다. `n_actual`은 기존
|
||||
`EvaluateDynamicRotation`의 cubic Exp/Jacobian에서 계산한 `normalize(J_cubic^-1*d_cubic)`를
|
||||
사용하며, 아래 정확 SO(3)의 closed 식으로 대체하지 않는다. Shared node에서 요소별
|
||||
primitive를 source 순서로 합하고 `n_actual·Fext_rot`을 한 번만 뺀다.
|
||||
Gradient·Hessian·3차 방향미분도 같은 primitive와 실제 null에서 유도한다.
|
||||
Metric은 `sum_e kd*grad(gamma_ei)*grad(gamma_ei)^T`다. Force의 일정 접선을 이유로
|
||||
null의 고차 미분을 지우지 않는다. 다음 `k*S*gamma`는 기존 상대 비틀림 방식의 유도다.
|
||||
|
||||
Zero constrained 성분을 정확히 제거한 free 공간에서
|
||||
|
||||
@@ -32,7 +41,14 @@ S_i=1/\|J_l^{-1}d_i\|.
|
||||
\tag{RC-02}
|
||||
\]
|
||||
|
||||
이는 기존 `drilling_constraint`의 scalar이며 raw twist gamma가 아니다. Physical force의 null work가 0인 정확 identity를 사용하므로 energy의 4차 tensor는 필요 없다. Translation force의 null work는 0이다. Shared node external term은 한번만 추가한다.
|
||||
이는 기존 `drilling_constraint`의 scalar이며 raw twist gamma가 아니다. 이 유도는 physical force의 null work가 0인 정확 identity를 사용하므로 energy의 4차 tensor는 필요 없다. Translation force의 null work는 0이다. Shared node external term은 한번만 추가한다.
|
||||
|
||||
현재 cubic 관성에서는 별도로 계산한 physical force의 null 투영이 이 정확 identity를
|
||||
항상 만족하지 않는다. 직접 방식의 hidden scalar를 `n^T(fphysical+fdrill-Fext)`로
|
||||
재구성하면 primitive 미분과 다른 식이 되므로 canonical scalar를 그대로 사용한다.
|
||||
Full mass/null·physical residual·triad acceptance는 별도로 유지하며 근사 오차를 숨기지 않는다.
|
||||
빈 admissible null inventory에는 scalar를 요구하지 않고, 필요한 null이 있을 때 미평가
|
||||
scalar를0으로 치환하거나 사용할 수 없는 directional 응답을 재사용하지 않는다.
|
||||
|
||||
## 2. Fixed graph와 부분 구속
|
||||
|
||||
|
||||
@@ -11,6 +11,13 @@ Composite·offset·material nonlinearity·mixed beam-shell은 이 기능 범위
|
||||
|
||||
## 채택한 수식과 이유
|
||||
|
||||
2026-10-20부터 모델 전체의 drilling 방식은 코드의 단일 설정으로 선택한다.
|
||||
기본 `kDirectSpring`은 초기 global 단위 director `d0`와 전체 회전 `phi`에 대해
|
||||
`gamma=d0·phi`, `Udrill=0.5*kd*sum(gamma²)`를 사용한다.
|
||||
`kLegacyCorotatedTwist`는 기존 상대 비틀림 방식이다. 선형 정적과 원형상 고유치의
|
||||
drilling block은 두 선택에서 같으며, 차이를 만들기 위해 physical 강성을 바꾸지 않는다.
|
||||
직접 방식은 유한 강체 회전의 인공 에너지·모멘트를 허용한다.
|
||||
|
||||
각 절점의 6 global DOF에서 director 접평면의 5 physical DOF를 분리한다.
|
||||
Bilinear geometry와 canonical edge-midpoint tying으로 공변 횡전단장을 보간한다.
|
||||
이는 직접 shear interpolation의 locking을 줄이기 위한 MITC 선택이다.
|
||||
@@ -48,6 +55,11 @@ Key는 frame·instance·source node·component이며 row-set·finite·unit/locat
|
||||
그 밖은 relative error `<=5%`, family `RMS(error)/S<=1%`를 동시에 요구한다.
|
||||
`S=0`은 exact zero다. UR 수치 실패는 warning이며 잘못된 identity를 허용하는 의미는 아니다.
|
||||
내부 free residual·전역 force/moment balance는 normalized `1e-10` 한계다.
|
||||
직접 방식의 기존 전역 gate는 raw moment에서 별도 내부 spatial drilling moment를
|
||||
차감한 값을 사용한다. 기존 방식은 raw 판정을 유지하며 분모·허용치는 같다.
|
||||
기존 `global/equilibrium`은 raw 값이다. 새 파일에는 실제 방식과 진단 version,
|
||||
`global/drilling_moment[3]`, `global/equilibrium_with_drilling[6]`을 추가한다.
|
||||
선형 변위 비교기에 새 평형 수치 family를 추가하지 않고 진단의 형식·관계만 사전검사한다.
|
||||
|
||||
## 구현과 데이터 흐름
|
||||
|
||||
@@ -61,6 +73,11 @@ Procedure는 해석 상태, writer는 serialization을 소유하며 element에 e
|
||||
|
||||
## 검증 의미와 한계
|
||||
|
||||
2026-10-20 source `6bbbb2b`의 두 방식 별도 실행에서 정식 S4 입력의 생성·U 비교를
|
||||
통과했고 실제 선형 변위도 정확히 같았다. 직접 방식의 인공 torque·보정 부호·실제
|
||||
불평형 거부 검사를 포함했다. 전체 Debug 검사는 Native/CTest 각각
|
||||
1035개 중1024 PASS·기존10 FAIL·1 INCONCLUSIVE SKIP으로 전체 수치 검증 통과는 아니다.
|
||||
|
||||
과거 S4 reference는 147 blocking U rows와 평형·대칭·constitutive·physical energy 검사를 통과했다.
|
||||
그때 중심 U3는 약 `-2.35504418e-5`, normalized free residual은 약 `1.08e-14`였다.
|
||||
이는 당시 실행 evidence이며 이번 문서 정리에 따른 현재 build/test 통과 선언이 아니다.
|
||||
|
||||
@@ -5,7 +5,14 @@
|
||||
>
|
||||
> 적용 경계: 물리 정식화와 당시 수치 stabilization 유도를 구분한다. 현재 공변 trace-average
|
||||
> scale·frozen lifecycle은 [고유치 질량·stabilization](../mitc4-shell-feast-eigen2/formulation-mass-stabilization.md)을
|
||||
> 함께 따른다. 이전 minimum-diagonal 식을 현재 기준으로 재도입하지 않으며 S3 partial-BC 보류를 유지한다.
|
||||
> 함께 따른다. 이전 minimum-diagonal 식을 현재 기준으로 재도입하지 않는다.
|
||||
>
|
||||
> 2026-10-20 채택: 단일 코드 설정의 기본값은 초기 director 직접 스프링이며 기존 상대
|
||||
> 비틀림도 선택한다. 선형/eigen의 drilling block과 trace-average scale은 두 방식에서 같다.
|
||||
> 직접 방식의 기존 전역 moment gate는 raw에서 별도 내부 drilling moment를 차감한다.
|
||||
> 아래 초기 문서의 drilling 전용 출력 금지와 objective nonlinear 후보 설명은 당시 범위다.
|
||||
> 현재는 실제 방식 metadata와 raw/인공/보정 평형을 별도로 저장하며, 직접 방식의 유한
|
||||
> 강체 회전 인공 에너지를 허용한다. 물리 MITC strain·constitutive·적분 유도는 유지한다.
|
||||
|
||||
## Metadata
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ Mass는 `3×3×3` 적분의 scalar thickness moments를 row-sum하여 nodal `6×
|
||||
3 병진·2 director 접평면 회전 관성과 warped geometry cross coupling을 보존한다.
|
||||
Numerical drill은 analytic mass null이며 인공 drilling inertia를 넣지 않는다.
|
||||
Stabilization scale은 초기 physical rotary diagonal 8개 trace 평균의 `10^-3`이다.
|
||||
2026-10-20부터 모델 전체의 코드 설정은 직접 초기 director 스프링을 기본으로 하며
|
||||
기존 상대 비틀림도 선택할 수 있다. 원형상 eigen의 K/M과 drilling block은 두 방식에서
|
||||
같다. Artificial drilling mass를 추가하거나 서로 다른 spectrum을 만들지 않는다.
|
||||
Global 실제 BC를 먼저 적용하고 analytic null과 free 공간의 교집합만 제거한다.
|
||||
Partial rotational BC를 full clamp로 대체하지 않고 finite constrained spectrum을 유지한다.
|
||||
Positive nodal mass space를 whitening하고 sparse symmetric 문제를 FEAST에 전달한다.
|
||||
@@ -41,6 +44,9 @@ HDF5는 `schema_version=0`, `eigen_schema_version=1`, procedure `linear-frequenc
|
||||
Constrained 성분은 exact zero이고 sign은 최대 scaled global component의 stable rule로 정한다.
|
||||
Raw 작은 음수 lambda를 clamp하지 않는다. Frequency는 lambda>=0 mode subset만 생성한다.
|
||||
`verification`에는 전체 c개 residual·Gram·null 검사와 backend call/control evidence를 보존한다.
|
||||
실제 선택은 `/metadata`의 `mitc4_drilling_formulation`과 U64LE
|
||||
`drilling_diagnostics_version=1`에 기록한다. Eigen에는 평형 frame이나 가상 drilling
|
||||
moment dataset을 만들지 않는다. Typed 비교 report의 진단 frames도 빈 배열이다.
|
||||
|
||||
| 원본 | 비교 계약 |
|
||||
| --- | --- |
|
||||
@@ -69,10 +75,16 @@ Domain→local K/M→ordered assembly→reduced solve→global phi 복원→all-
|
||||
|
||||
## 실제 evidence와 미해결 범위
|
||||
|
||||
2026-10-20 source `6bbbb2b`의 두 방식 별도 실행에서 정식10-mode 생성·비교를 통과했고
|
||||
실제 eigen table은 정확히 같았다. 전체 Debug Native/CTest는 각각1035개 중
|
||||
1024 PASS·기존10 FAIL·1 INCONCLUSIVE SKIP이다. 해당 Value 비교 통과와 전체 물리 검증을 구분한다.
|
||||
|
||||
2026-09-10 사용자 승인 첫 구현에서 전체 417/417 PASS와 원본 10 Value의 모드별5% 통과 기록이 있다.
|
||||
최대 외부 상대차는 약 `4.261331%`다. 이전 RMS1% 불일치 이력은 소급 PASS로 고치지 않는다.
|
||||
이 기록은 당시 revision의 evidence이며 이번 문서 정리는 새 build/test를 실행하지 않았다.
|
||||
Rational mass oracle·Jensen energy·좌표 재계산 공변성·density scaling·all-c 실패 주입이 보완 근거다.
|
||||
S3 public static 평형 충돌과 Abaqus Value 정확도 개선은 후속 보류다.
|
||||
S3 partial-BC의 public static은 직접 방식에서 인공 모멘트 차감 후 기존 gate를 통과한다.
|
||||
기존 방식의 raw gate 실패는 명시적 검사로 보존했다. 이는 partial BC를 full clamp로
|
||||
바꾸거나 Value 허용치를 완화한 해결이 아니다. Abaqus Value 정확도 개선은 후속 보류다.
|
||||
실제 512회 vendor 호출 또는 실제 HDF5 close-error의 모든 조합을 주입한 것은 아니다.
|
||||
Retained shell/shellNL 회귀와 내부 검증 의무를 유지하며 `reference/temp` 제외 예제를 재사용하지 않는다.
|
||||
|
||||
@@ -136,7 +136,14 @@ spectrum·static response의 공변성을 검증한다. 다른 BC를 같은 문
|
||||
여섯 물리 rigid modes는 양의 mass를 갖는다. Pure numerical drill `[0;di]`는 mass=0,
|
||||
stiffness>0이다. Linear UR에 full spatial Omega를 넣는 field와 이 physical 대표값을 구분한다.
|
||||
|
||||
## S2. 비선형 objective potential과 고정 scale
|
||||
## S2. 비선형 drilling 선택과 고정 scale
|
||||
|
||||
2026-10-20 현재 기본값은 초기 global 단위 director `d_i0`를 사용한 직접 스프링이다.
|
||||
`gamma_i=d_i0·phi_i`, `Pi_d=0.5*kd*sum(gamma_i²)`,
|
||||
`r_i=kd*gamma_i*d_i0`, `K_ii=kd*d_i0*d_i0^T`이며 병진·절점 간 coupling은0이다.
|
||||
Drilling polar/swing/atan2를 평가하지 않고 유한 강체 회전의 인공 모멘트를 허용한다.
|
||||
S1의 초기 scale과 mass null은 유지한다. 원형상 선형/eigen block은 두 방식에서 같다.
|
||||
다음 objective 유도는 선택 가능한 기존 상대 비틀림 방식에 한정한다.
|
||||
|
||||
기존 [nonlinear 정식화 §10](../nonlinear-static-newton-raphson/formulation.md)의 polar
|
||||
corotation, total nodal rotation, minimal swing 및 objective `gamma_i=atan2(s_i,c_i)`를
|
||||
@@ -148,7 +155,9 @@ corotation, total nodal rotation, minimal swing 및 objective `gamma_i=atan2(s_i
|
||||
K_d=k_d\left(D_\gamma^TD_\gamma+\sum_i\gamma_iD^2\gamma_i\right). \tag{S2}
|
||||
\]
|
||||
|
||||
Polar/swing/atan2의 미분과 branch validation을 생략하지 않는다. Trial마다 scale을 바꾸지 않는다.
|
||||
기존 방식에서는 polar/swing/atan2의 미분과 branch validation을 생략하지 않는다.
|
||||
현행 구현의 polar1과 미분 정확도 실패를 보존하며 과거 polar12로 복원하지 않는다.
|
||||
두 방식 모두 trial마다 scale을 바꾸지 않는다.
|
||||
일반 nonlinear Hessian 전체의 SPD는 주장하지 않는다. 초기에도 Dgamma는 corotation coupling을
|
||||
포함하므로 linear Td와 동일시하지 않는다. 기존 physical 선형극한, directional residual/Hessian,
|
||||
objective rigid orbit·state rollback을 검증한다. 새 eigen에는 이 nonlinear state를 전달하지 않는다.
|
||||
@@ -173,3 +182,7 @@ Static 대응 load는 node1의 tangent moment `m=(5/3)M0 b`, 다른 load=0이다
|
||||
`d·m=0,mZ=M0`; `theta1Z=M0/Kff`, full residual `r=Kq-F`다.
|
||||
회전 뒤 `m'=Qm`을 사용한다. Pure global eZ moment는 drilling 성분 때문에 기존 load
|
||||
계약에 부합하지 않는다. Eigen의 constrained residual을 static reaction으로 출력하지 않는다.
|
||||
|
||||
2026-10-20 현재 이 static 사례의 직접 방식은 별도 인공 spatial 모멘트를 차감한 기존
|
||||
gate를 통과하고, 기존 방식은 raw gate에서 계속 거부된다. S3의 K/M/lambda target이나
|
||||
partial BC를 바꾼 결과가 아니며 Value 비교 예외를 정적 평형에 적용하지 않는다.
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
|
||||
## 목적과 경계
|
||||
|
||||
2026-10-20 현재 drilling 기본값은 초기 director의 직접 작은 스프링이다.
|
||||
모델 전체의 한 코드 설정으로 기존 상대 비틀림도 선택하며 정적 pressure에도 같은
|
||||
선택을 적용한다. 직접 방식은 drilling의 polar/swing/twist 계산을 생략한다.
|
||||
아래 polar 근사 실험의 실패는 보존한 기존 방식의 근거이며 직접 스프링 식을 뜻하지 않는다.
|
||||
|
||||
2026-10-18 후속 사용자 지시의 [local 근사 실험](../abaqus-tolerance/local-kernel-experiment.md)에서
|
||||
공유 nonlinear MITC4의 polar 반복을 12→1회로 변경했다. 후속 사용자 지시로 관련 접선
|
||||
테스트 오차를 1e-3으로 완화한 뒤에도 drilling constraint의 gradient 항등식 검사가
|
||||
실패했으므로 과거 정적 검증을 변경본에 승계하지 않는다.
|
||||
정적 Newton tolerance 자체는 변경하지 않았다.
|
||||
|
||||
등방 선형탄성 MITC4의 기하 비선형 정적 응답을 full Newton으로 계산한다.
|
||||
재료 비선형·contact·arc length·line search·자동 cutback은 이 기능에 포함하지 않는다.
|
||||
고정 global dead nodal force/moment와 factor-one prescribed target을 공통 load factor로 증가시킨다.
|
||||
@@ -14,8 +25,12 @@
|
||||
초기 director/frame에서 같은 trial의 기하·strain·internal force·consistent tangent를 계산한다.
|
||||
Rotation-vector generalized moment와 global spatial moment는 일의 짝이 다르다.
|
||||
반력의 공개 moment 복구에서 chart 변환을 적용하며 성분명 변경으로 대체하지 않는다.
|
||||
Physical MITC strain·constitutive response와 objective numerical drilling potential을 분리한다.
|
||||
Physical MITC strain·constitutive response와 선택한 numerical drilling potential을 분리한다.
|
||||
Drilling scale은 초기 상태에서 정하고 trial마다 다시 tuning하지 않는다.
|
||||
직접 방식은 `gamma_i=d_i0·phi_i`, `f_i=kd*gamma_i*d_i0`,
|
||||
`K_ii=kd*d_i0*d_i0^T`다. 병진과 절점 간 drilling coupling은 exact zero다.
|
||||
유한 강체 회전에도 인공 복원모멘트가 생기는 것을 허용한다. 기존 방식의 objective
|
||||
유도와 polar1 구현을 이 직접 방식에 적용하지 않는다.
|
||||
평형 residual은 internal minus external이며 follower load가 있으면 tangent는
|
||||
`Kint-dFext/dq`다. Unsymmetric tangent를 대칭화하지 않는다.
|
||||
|
||||
@@ -52,6 +67,11 @@ Accepted free residual은 matching external scale이 nonzero이면 infinity-norm
|
||||
exact-zero이면 typed absolute `<=1e-3` force 또는 force-length다. Factor-one 전역 force/moment
|
||||
balance는 각각 normalized `<=1e-8`이다. 이는 기존 comparator의 승인 계약이며 두 양을
|
||||
하나의 `1e-10` 기준으로 요약하지 않는다. Linear 정적·HHT·eigen의 별도 gate는 바꾸지 않는다.
|
||||
이 전역 수치 판정은 기존 reference 비교기의 책임이다. Procedure에 새 전역 gate를
|
||||
만들지 않는다. 비교기는 직접 방식 marker일 때 기존 moment family에
|
||||
`raw_moment-Mdrill`을, 기존 방식·marker 없는 옛 파일에는 raw 값을 사용한다.
|
||||
`Mdrill`은 현재 위치의 병진 모멘트팔과 `Jl(phi)^(-T)`로 변환한 내부 drilling
|
||||
모멘트의 source 순서 합이다. 분모에 인공 모멘트를 더하지 않는다.
|
||||
|
||||
## 구현과 상태 수명
|
||||
|
||||
@@ -67,8 +87,14 @@ Domain→non-owning model→stable full/free DOF→local trial response→ordere
|
||||
|
||||
## 검증에서 배운 점과 남은 경계
|
||||
|
||||
2026-10-20 source `6bbbb2b`의 기본 직접 방식과 별도 기존 방식에서 정식 coarse
|
||||
nonlinear 입력의 생성·기존 비교를 통과했다. 두 방식의 저장·reader·원자적 실패 보존도
|
||||
검사했다. 보존한 기존 방식의 gradient 항등식은 차이 약0.00329135로 계속 실패하며,
|
||||
전체 Native/CTest 각각1035개는1024 PASS·10 FAIL·1 INCONCLUSIVE SKIP이다.
|
||||
이는 현재 실행 결과이며 아래 과거 기록이나 coarse U 통과로 모든 비선형 검증을 대체하지 않는다.
|
||||
|
||||
기존 nonlinear reference와 rollback·finite rotation·tangent·equilibrium 검증 기록이 있다.
|
||||
이는 새 revision의 build/test 통과를 의미하지 않으며 이번 작업은 문서 정리만 수행한다.
|
||||
과거 기록만으로 새 revision의 build/test 통과를 의미하지 않는다.
|
||||
Reaction은 단일 global 합만 맞춰 local reaction 정확성을 대체할 수 없다.
|
||||
큰 자세 변화와 원점 이동에서 spatial moment balance를 함께 확인해야 한다.
|
||||
외부 U 통과는 warning UR 또는 review RF/RM의 수치 일치를 뜻하지 않는다.
|
||||
|
||||
@@ -5,7 +5,40 @@
|
||||
>
|
||||
> 적용 경계: 물리 정식화와 당시 수치 stabilization 유도를 구분한다. 현재 공변 trace-average
|
||||
> scale·frozen lifecycle은 [고유치 질량·stabilization](../mitc4-shell-feast-eigen2/formulation-mass-stabilization.md)을
|
||||
> 함께 따른다. 이전 minimum-diagonal 식을 현재 기준으로 재도입하지 않으며 S3 partial-BC 보류를 유지한다.
|
||||
> 함께 따른다. 이전 minimum-diagonal 식을 현재 기준으로 재도입하지 않는다.
|
||||
> S3 partial-BC는 직접 방식의 보정 gate와 기존 방식의 raw gate를 구분한다.
|
||||
|
||||
## 2026-10-20 drilling 채택 범위
|
||||
|
||||
기본값은 초기 global 단위 director `d_i0`와 total global rotation vector `phi_i`의
|
||||
직접 스프링이다. 아래 §10의 corotation·minimal swing·atan2 유도는 선택 가능한
|
||||
기존 상대 비틀림 방식의 원문으로 보존한다. 기존 구현은 기준 source
|
||||
`52e321e15c70377d4318aace3f3bc64b3ea1a65c`의 polar1을 유지하며, 원문의 정확한
|
||||
객관성·미분 항등식이 이 근사 구현에서 모두 통과했다는 뜻은 아니다.
|
||||
|
||||
```text
|
||||
gamma_i = d_i0 · phi_i
|
||||
kd = 1e-3 * average(initial physical K20 tangent-rotation diagonals, 8 entries)
|
||||
Udrill = 0.5 * kd * sum_i gamma_i^2
|
||||
f_drill,i,rot = kd * gamma_i * d_i0
|
||||
K_drill,ii,rot,rot = kd * d_i0 * transpose(d_i0)
|
||||
D² f_drill[v,v] = 0
|
||||
```
|
||||
|
||||
병진 force/tangent와 서로 다른 절점의 coupling은 exact zero다. Global URZ 또는
|
||||
현재 director를 `d_i0` 대신 사용하지 않는다. Scale은 BC 전 초기 physical K20에서
|
||||
기존 결정적 overflow 방지 평균으로 한 번 계산하며 zero 대각도 8개 평균에 포함한다.
|
||||
음수/nonfinite 대각, 모두 zero인 대각, nonfinite/nonpositive kd는 실패다.
|
||||
Clamp·floor·density 치환이나 trial별 재조정은 없다.
|
||||
|
||||
직접 경로는 drilling의 polar/swing/twist/atan2를 평가하지 않는다. Physical strain·
|
||||
geometry·관성 계산은 유지한다. 초기 director와 전체 회전 기준이므로 유한 강체 회전의
|
||||
인공 에너지·복원모멘트를 허용하며 아래 기존 방식의 self-equilibrated 성질을 요구하지 않는다.
|
||||
내부 generalized drilling 힘은 현재 위치의 모멘트팔과 `Jl(phi)^(-T)` 변환으로 spatial
|
||||
`Mdrill`을 만들고 source 순서로 누적한다. Raw 평형은 보존하고 직접 방식의 기존 moment
|
||||
판정에만 `raw_moment-Mdrill`을 사용한다. 기존 방식은 raw 판정을 유지한다.
|
||||
비선형 정적 procedure에는 원래 진단만 있었으며 수치 gate는 기존 reference 비교기에 있다.
|
||||
새 procedure gate나 인공 모멘트를 포함한 정규화 분모를 만들지 않는다.
|
||||
|
||||
## 1. Metadata
|
||||
|
||||
|
||||
+19
-3
@@ -3,7 +3,7 @@
|
||||
## 준비 순서
|
||||
|
||||
저장소 root에서 VS x64 개발 환경을 사용한다. 직접 프로젝트는 v145 toolset과 Windows SDK를
|
||||
요구한다. 필요한 로컬 설치는 MKL·TBB·HDF5·OpenMP/Intel runtime과 승인된 GoogleTest
|
||||
요구한다. 필요한 로컬 설치는 MKL·TBB·HDF5·Intel 수학 runtime과 승인된 GoogleTest
|
||||
source checkout이다. CMake 경로에는 추가로 CMake 3.25 이상과 Ninja가 필요하다.
|
||||
Dependency 설치·다운로드·외부 source 수정은 아래 명령에 포함되지 않는다.
|
||||
|
||||
@@ -15,7 +15,7 @@ Dependency 설치·다운로드·외부 source 수정은 아래 명령에 포함
|
||||
| `FESA_MKL_ROOT` | MKL의 include·lib·runtime을 포함한 설치 root |
|
||||
| `FESA_TBB_ROOT` | TBB 설치 root; Debug와 Release runtime을 구분한다. |
|
||||
| `FESA_HDF5_ROOT` | HDF5 C library의 include·lib·runtime 설치 root |
|
||||
| `FESA_OMP_ROOT` | Intel OpenMP library와 runtime 설치 root |
|
||||
| `FESA_OMP_ROOT` | HDF5가 요구하는 `bin/libmmd.dll`의 Intel compiler runtime 설치 root; 기존 변수명을 유지한다. |
|
||||
| `MKL_DIR`·`TBB_DIR`·`HDF5_DIR` | CMake에서만 사용하는 각 CONFIG package 디렉터리; 설치 root와 혼동하지 않는다. |
|
||||
|
||||
VS x64 Developer PowerShell에서 다음을 확인한다. 누락되면 해당 로컬 설치 경로를 설정한
|
||||
@@ -33,7 +33,13 @@ foreach ($root_name in $required_roots) {
|
||||
```
|
||||
|
||||
위 확인은 환경변수 방식의 준비 절차다. `Fesa.Local.props`를 사용하는 경우에는 그 파일의
|
||||
동일 MSBuild property 값을 확인한다. CMake는 `Fesa.Local.props`를 읽지 않는다.
|
||||
동일 MSBuild property 값을 확인한다. CMake는 `Fesa.Local.props`를 읽지 않으며,
|
||||
HDF5용 `libmmd.dll`을 찾을 때 `FESA_OMP_ROOT` 환경변수를 사용한다.
|
||||
|
||||
MKL은 ILP64·dynamic·TBB threading으로 연결한다. Debug는 `mkl_tbb_threadd`와
|
||||
`tbb12_debug.dll`, Release는 `mkl_tbb_thread`와 `tbb12.dll`을 사용한다. CLI와 세
|
||||
테스트 실행 파일 곁에 같은 구성의 DLL을 준비한다. MKL용 `mkl_intel_thread`와
|
||||
`libiomp5md.dll`은 연결·복사하지 않으며 HDF5의 `libmmd.dll`은 별도로 유지한다.
|
||||
|
||||
## 주 개발 빌드와 직접 검사
|
||||
|
||||
@@ -72,6 +78,16 @@ foreach ($test_name in 'fesa_unit_tests','fesa_integration_tests','fesa_referenc
|
||||
|
||||
생성된 경로에서도 실제 최장 lease 경로를 확인한다. 너무 길면 승인된 짧은 작업 위치를 사용한다.
|
||||
|
||||
## MITC4 drilling 방식 변경
|
||||
|
||||
`src/fesa/elements/mitc4_drilling_internal.h`의 `kProductionDrillingMode` 한 곳을 바꾼다.
|
||||
기본값은 `Mitc4DrillingMode::kDirectSpring`, 기존 방식은
|
||||
`Mitc4DrillingMode::kLegacyCorotatedTwist`다. 변경 후 solver·CLI·검사를 모두 rebuild한다.
|
||||
같은 모델의 모든 MITC4 정적·pressure·frequency·HHT에 적용되며 CLI/INP/환경변수나
|
||||
section별 override는 현재 없다. 시험용 명시적 alternate mode 경로는 production 설정이 아니다.
|
||||
서로 다른 mode의 실행물·DLL·입력 sibling 결과를 별도 위치에 두고 실제 metadata marker로
|
||||
선택을 확인한다. 선형 변위와 원형상 eigen값이 두 방식에서 같아도 정상이다.
|
||||
|
||||
## CMake 보조 빌드
|
||||
|
||||
같은 VS x64 환경에서 로컬 CONFIG package 환경변수를 설정한 뒤 실행한다.
|
||||
|
||||
@@ -41,6 +41,11 @@ Follower 여부는 procedure로 결정하며 keyword 옵션을 조용히 무시
|
||||
결과는 기존 linear schema0 또는 nonlinear schema1의 displacement/reaction·shell recovery다.
|
||||
별도의 가상 pressure stress 또는 nodal averaged stress를 추가하지 않는다.
|
||||
Nonlinear 비교는 exact factor-one accepted frame만 사용한다.
|
||||
MITC4의 직접 스프링 기본값과 기존 상대 비틀림 선택은 pressure에도 모델 전체로 적용한다.
|
||||
Pressure force/tangent 정의는 그대로다. 새 방식 marker가 있는 nonlinear 비교의 기존
|
||||
moment family만 직접 방식의 `raw-Mdrill` 또는 기존 방식의 raw를 선택한다.
|
||||
기존 scale·`1e-8` 기준과 U/UR 정책은 유지한다. 정식 pressure bundle 부재로
|
||||
실제 외부 pressure 비교는 아직 미완료이며 내부 pressure 검사가 이를 대신하지 않는다.
|
||||
|
||||
| 원본 bundle | 비교 의미 |
|
||||
| --- | --- |
|
||||
|
||||
@@ -45,15 +45,17 @@ Named exception **`mitc4-eigen-first-implementation`**(2026-09-10 승인)은 eig
|
||||
|
||||
Configure/build/test의 compiler·구성·build tree를 함께 맞추고 discovery0건·다른 compiler를 실패로 다룬다. MSVC runtime DLL staging을 확인하며 개발자 PATH의 우연한 성공을 배포 검증으로 삼지 않는다. Dense allocation 크기 곱 overflow는 할당 전에 검사한다. Header는 self-contained `.h`/guard, 파일·namespace snake_case, 타입·함수 PascalCase, 변수 snake_case, class member trailing `_`, Google 기반2-space format을 따른다. Vendor target에 FESA warning 정책을 강제하지 않는다.
|
||||
|
||||
현재 공백은 shell HHT의 최신 전체 검증·시간수렴, thermal 새 필수 reference, eigen S3 partial-BC 정적 충돌, 성능 목표 미달이다. 각 기능에 구체 한계를 보존한다. 기존1×1 PARDISO 반복의 heap failure는 SpMV 없는 baseline에서도 재현됐고 vendor 원인은 미확정이다. 기본 정책의 해당 크기에만 local1-thread/TLS 복원을 적용했으며 보조100회 성공을 라이브러리 전체 해결로 확대하지 않는다.
|
||||
현재 공백은 shell HHT의 전체 물리·시간정확도, thermal 새 필수 reference, 기존 drilling 방식의 S3 partial-BC raw 정적 gate 한계, 성능 목표 미달이다. S3의 기본 직접 방식은 별도 인공 모멘트 차감으로 기존 gate를 통과하며 기존 방식의 raw 거부를 유지한다. 각 기능에 구체 한계를 보존한다. 기존1×1 PARDISO 반복의 heap failure는 SpMV 없는 baseline에서도 재현됐고 vendor 원인은 미확정이다. 기본 정책의 해당 크기에만 local1-thread/TLS 복원을 적용했으며 보조100회 성공을 라이브러리 전체 해결로 확대하지 않는다.
|
||||
|
||||
## 빌드와 직접 검사
|
||||
|
||||
직접 관리하는 [FESA.sln](../../FESA.sln)과 [msvc 지침](../../msvc/AGENTS.md)이 주 개발 빌드다.
|
||||
VS x64 개발 환경에서 저장소 root를 작업 디렉터리로 사용한다. v145 toolset과 로컬
|
||||
GoogleTest source checkout, MKL ILP64/dynamic/Intel threading·TBB·HDF5·OpenMP가 필요하다.
|
||||
GoogleTest source checkout, MKL ILP64/dynamic/TBB threading·oneTBB·HDF5와
|
||||
HDF5용 Intel 수학 runtime(`libmmd.dll`)이 필요하다.
|
||||
`FESA_GTEST_SOURCE_DIR`, `FESA_MKL_ROOT`, `FESA_TBB_ROOT`, `FESA_HDF5_ROOT`,
|
||||
`FESA_OMP_ROOT`에 설치 root를 지정하거나 ignored `msvc/Fesa.Local.props`로 주입한다.
|
||||
`FESA_OMP_ROOT`는 기존 변수명을 유지하며 현재 HDF5의 `libmmd.dll` 준비에 사용한다.
|
||||
개인 설치 경로를 추적 파일에 고정하거나 dependency를 자동 다운로드하지 않는다.
|
||||
|
||||
```bat
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# MITC4 HHT 실험의 변위 기준 수용과 실패 검사 보류
|
||||
|
||||
결정일: 2026-10-19. 사용자 후속 지시로 확정했다.
|
||||
|
||||
## 배경과 선택
|
||||
|
||||
사용자는 Abaqus 변위 결과의 일치를 이번 실험의 검증 통과 근거로 수용하고,
|
||||
실패한 테스트는 보류한 채 남은 실행 계획을 진행하라고 지시했다.
|
||||
Release 변위8모델은 기존 reference 비교 정책을 통과했고, MITC4 HHT의 최악
|
||||
프레임 정규화 RMS는0.22824245%로1% 이내다. 해당 동적 CSV의 외부 비교 범위는
|
||||
9절점×101프레임이다. 모든 성분의 개별 상대오차가1% 이내라는 의미는 아니다.
|
||||
|
||||
이번 실험은 **변위 검증 통과·실패10개 보류로 조건부 완료**한다. 기존 assertion이나
|
||||
production acceptance를 완화하거나 테스트를 삭제·skip하지 않는다. 원래 전체 Native와
|
||||
CTest의987 PASS·10 FAIL·1 SKIP, 별도 Python145 PASS 기록은 그대로 보존한다.
|
||||
보류할10개 identity와 상대오차 재계산 근거는 [검토 보고서](../../abaqus-tolerance/relative-error-review.md)에 있다.
|
||||
|
||||
## 대안과 영향
|
||||
|
||||
기존의 전체 물리 검증 통과까지 이번 실험 종료를 막는 대안 대신, 사용자 결정에 따라
|
||||
실험을 마무리하고 정확도 문제를 후속 과제로 남긴다. D3·drilling·C08·mass/null·triad
|
||||
문제가 해결됐다는 뜻은 아니다. N7 해상도 SKIP1개와 정식 fine/pressure/thermal 자료
|
||||
미확보도 별도 미완료로 남긴다. 이 결정은 이후 일반 솔버 기능의 전체 검증 의무를 바꾸지 않는다.
|
||||
|
||||
전체729절점·101프레임의 strict/candidate q/v/physical a/A/energy 차이는 진단으로 확보했다.
|
||||
이는 Abaqus V/a/energy 검증이나 실제 비선형 시간수렴 증거를 대신하지 않는다.
|
||||
기록된 시간6350.7895521초와5662.1448538초의 차이688.6446983초(10.8434501%),
|
||||
비율1.1216226은 관측 비교다. 후보 실행의 전원·측정 격리 조건 인증이 없어 공식 speedup은
|
||||
확정하지 않는다. Full100 inner/요소/factor/solve 비용도 측정하지 않았으며1증분 probe를 외삽하지 않는다.
|
||||
|
||||
## 변경 경계와 후속 과제
|
||||
|
||||
제품·검사 수식과 수치 기준은 그대로 유지한다. 주 개발 branch 병합·정식 기본값 채택·
|
||||
원격 push·release는 이번 결정에 포함되지 않는다. 실험 branch와 raw evidence를 보존한다.
|
||||
후속 수치 수정 시에는 보류10개와 N7·시간 정확도를 다시 검증하고, 성능 인증이 필요하면
|
||||
동일 조건의 공식 측정과 전체 비용 측정을 별도 수행한다.
|
||||
|
||||
실행 결과와 한계는 [결과 보고서](../../abaqus-tolerance/results.md),
|
||||
변위·빌드·시간은 [변위 비교](../../abaqus-tolerance/reference-displacement-comparison.md)를 따른다.
|
||||
@@ -0,0 +1,39 @@
|
||||
# MITC4 drilling의 직접 스프링 기본값과 기존 방식 선택
|
||||
|
||||
## 상황
|
||||
|
||||
기존 비선형 상대 비틀림은 drilling의 polar·minimal swing·atan2와 고차 미분을 계산한다.
|
||||
사용자는 이 계산을 생략하는 작은 직접 스프링을 기본값으로 요청했고, 초기 director와
|
||||
전체 회전 기준에서 유한 강체 회전의 인공 에너지·복원모멘트가 생기는 것을 허용했다.
|
||||
기존 polar1·관성3차 근사와 정확도 실패는 별도 보존 대상이다.
|
||||
|
||||
## 결정
|
||||
|
||||
모델 전체의 compile-time 설정 하나로 직접 초기 director 스프링 또는 기존 상대 비틀림을
|
||||
선택하며 기본값은 직접 방식이다. `gamma=d0·phi`, `U=0.5*kd*sum(gamma²)`를 사용하고
|
||||
초기 physical rotary 대각8개 평균의 `1e-3` scale을 고정한다.
|
||||
선형·비선형 정적·pressure·frequency·HHT에 같이 적용한다.
|
||||
직접 HHT hidden 미분은 실제 cubic 관성이 계산한 null과 같은 scalar에서 도출한다.
|
||||
|
||||
인공 내부 spatial 모멘트를 별도로 기록한다. 기존 raw 평형을 보존하고 직접 방식의 기존
|
||||
moment gate는 raw에서 인공 모멘트를 차감한다. 기존 방식은 raw 판정·현재 계산을 유지한다.
|
||||
기존 feature/procedure/schema와 외부 U/UR 기준을 바꾸지 않고 추가 marker와 진단으로 구분한다.
|
||||
|
||||
## 검토한 대안
|
||||
|
||||
기존 상대 비틀림만 유지하면 drilling 회전 계산 생략 요구를 충족하지 못한다.
|
||||
직접 스프링에 상대 회전 보정을 다시 넣으면 허용한 비객관성을 제거하는 대신 그 계산을
|
||||
다시 도입하게 된다. 현재는 모델·section별 runtime 입력을 늘리는 대신 한 코드 설정을 택했다.
|
||||
Global URZ에만 스프링을 놓으면 기울어진 초기 director의 drilling을 나타내지 못하므로 쓰지 않는다.
|
||||
|
||||
## 결과와 한계
|
||||
|
||||
직접 force의 병진·절점 간 coupling과2차 방향미분은0이지만 HHT null의 고차 미분은 필요하다.
|
||||
인공 에너지를 physical stress/resultant/energy에 혼합하거나 spring 질량을 추가하지 않는다.
|
||||
Raw 평형만 보고 직접 방식의 유한 강체 회전을 불평형으로 거부해서는 안 되며, 보정 분모에
|
||||
인공 모멘트를 더해 실제 불평형을 숨겨서도 안 된다. 옛 markerless 파일의 방식은 미상이다.
|
||||
현재 선형/eigen block이 두 방식에서 같은 것은 정상이다.
|
||||
|
||||
이 선택은 기존 근사 실험의 D3·Legacy constraint/C08·mass-null/triad 실패나 비선형 시간정확도를
|
||||
해결하는 결정이 아니다. 정식 Direct HHT U 통과와 전체 Debug의10 FAIL/1 SKIP은 함께 남는다.
|
||||
공식 성능 개선이나 전체 solver correctness의 인증도 포함하지 않는다.
|
||||
@@ -12,3 +12,5 @@
|
||||
| `0008-shell-inertia.md` | Shell eigen/HHT의 질량 영공간을 보존하고 물리 rate를 복원한다. |
|
||||
| `0009-hex8-numerics.md` | Mean-volume 강성·reference row-sum mass·high+low 상태를 사용한다. |
|
||||
| `0010-thermal-discretization.md` | 다섯 두께 온도점·consistent capacity·Backward Euler를 사용한다. |
|
||||
| `0011-tolerance-experiment-acceptance.md` | 이번 MITC4 HHT 실험은 변위 검증 통과·실패10개 보류로 수용하며 실제 FAIL/SKIP과 후속 검증을 보존한다. |
|
||||
| `0012-mitc4-drilling-options.md` | 초기 director 직접 스프링을 기본으로 하고 기존 상대 비틀림을 코드로 선택한다. 인공 spatial 모멘트와 raw/보정 평형을 구별한다. |
|
||||
|
||||
+84
-14
@@ -1,13 +1,80 @@
|
||||
# 해결되지 않은 문제
|
||||
|
||||
## 비정적 검증 공백
|
||||
## 2026-10-20 현재 실행과 미완료 경계
|
||||
|
||||
- 조건·관측: 2026-10-18 통합에서 장시간 HHT 검증이 사용자 요청으로 중단되고 정적·공통
|
||||
631개만 검증됐다. 비정적 333개 등록은 보존됐으나 최신 통합본 전체 수치 실행 근거가 없다.
|
||||
- 영향: Shell HHT·solid dynamics·frequency·transient thermal의 전체 검증 완료를 선언할 수 없다.
|
||||
- 지금 해결하지 못한 이유: 장시간 solver·test 실행과 독립 물리 판정이 필요한 별도 검증 작업이다.
|
||||
- 다음 접근: source와 환경을 고정하고 미실행 identity별 필요한 물리·시간수렴·reference를
|
||||
묶어 검증한다. 중단된 C08b finer-grid와 기존 완료 개별 검사를 구분한다.
|
||||
직접 스프링 기본 source `6bbbb2b`의 전체 Native/CTest는 각각1035개 중
|
||||
1024 PASS·10 FAIL·1 INCONCLUSIVE SKIP이다. 기존 실패를 ID뿐 아니라 현재 raw 수치와
|
||||
발생 단계로 대조했다. 새 직접 스프링 수식·저장 연결 검사는 통과했으며 아래 근사 문제를
|
||||
이번 변경에서 자동 복원하거나 허용치를 더 완화하지 않았다.
|
||||
|
||||
- 관성 D3 두 검사의 첫 차이는 약0.22761592214, 명시 Legacy drilling gradient 차이는
|
||||
약0.00329135061318이다. Legacy C08a hidden ratio는0.00123371144037로1e-3을 초과한다.
|
||||
- C08 controller4개는 첫 격자(h=1/64,1/16,1/256,1/64)의 inc1에서100회 correction을
|
||||
소진했다. 시간수렴·차수 판정 이전 실패이며 이전 통과로 대신하지 않는다.
|
||||
- LargeDirect는 accepted inc1/t=.25/iteration5의 mass-null 실패, UniformTranslation의
|
||||
유한회전 subrun은 inc2/t=.02의 triad/rotation mismatch가 유지됐다.
|
||||
- N7 finest 비교 하나는 arithmetic/solve resolution의 INCONCLUSIVE SKIP이다.
|
||||
|
||||
정식 직접 HHT의729nodes·101frames와9node U101 families는 이번에 실제 통과했지만
|
||||
비선형 q/v/physical a의 시간정확도와 위 독립 물리 실패를 해결하지는 않는다.
|
||||
기존 방식의 대형 HHT 외부 U 재해석은 미실행이다. 별도 관련162검사 중7 FAIL은 기존5개와
|
||||
Direct 기본값 전용 검사를 잘못 포함한2개로 분리했다. 두 선택 오류도 실제 FAIL로 보존했다.
|
||||
정식 fine/fine_02/pressure·새 thermal 자료는 없으며 `reference/temp`를 쓰지 않았다.
|
||||
이 미완료는 승인된 근사 정확도 조사와 외부 자료 제공이 필요한 범위이므로, 이번 drilling
|
||||
선택 구현을 전체 solver 검증 통과로 확대하지 않는다.
|
||||
|
||||
## 이번 실험의 보류 결정
|
||||
|
||||
2026-10-19 사용자는 Abaqus U 비교를 이번 실험의 검증 통과로 수용하고 아래 실패10개를
|
||||
보류하도록 승인했다. [결정 기록](decisions/0011-tolerance-experiment-acceptance.md)을 따른다.
|
||||
문제 해결이나 raw FAIL의 PASS 전환은 아니며 N7 SKIP·자료 미확보도 그대로 남는다.
|
||||
전체 strict/candidate 상태 차이와 관측 시간은 추가로 확보했지만 비선형 시간 정확도·공식 성능 조건은
|
||||
후속 과제다. 이후 일반 solver 기능의 전체 검증 의무를 면제하지 않는다.
|
||||
|
||||
## MITC4 local 근사 실험의 정확도 실패
|
||||
|
||||
- 조건·관측: 2026-10-18 사용자 지시로 polar 1회·관성 회전/Jacobian 3차·질량 검사
|
||||
1e-3을 적용했다. 변경 전 관련 기존 45개 PASS, 변경 후 46개 중 41 PASS·5 FAIL이다.
|
||||
추가 HHT 선택에서도 C08a 독립 물리 oracle와 큰 시간간격 100회 보정 종료가 실패했다.
|
||||
후속 사용자 지시로 관련 테스트 오차 기준도 1e-3으로 변경했으며 최신 동일 선택
|
||||
50개는 당시 Native/CTest 각각46 PASS·4 FAIL이었다. 2026-10-19 외부 field 정책 연결 뒤
|
||||
전체998개를 양쪽에서 실행했고 동일10 FAIL·1 SKIP이 남는다. D3 오차 약0.228,
|
||||
drilling gradient 약0.00329 및 C08a hidden defect/실제 평형 검사 실패도 재현했다.
|
||||
- 영향: 회전 미분·analytic null·질량/에너지·drilling 항등식의 기존 정확도 검증을 만족하지
|
||||
못한다. 구현 완료를 solver correctness·시간수렴·성능 향상으로 보고할 수 없다.
|
||||
- 지금 해결하지 않는 이유: 요청한 근사값의 수치 영향이며 승인된 테스트 허용치1e-3보다
|
||||
더 완화하거나 요청을 자동 취소해 실패를 숨기지 않는다. 외부 Newton 기준 변경만으로 해당 항등식은 복원되지 않는다.
|
||||
- 다음 접근: 원인 분리 실험과 정확도 손실의 별도 판정이 필요하다. 변경 범위·실제 로그·
|
||||
미검증 항목은 [local 실험 기록](../abaqus-tolerance/local-kernel-experiment.md)을 따른다.
|
||||
|
||||
## MITC4 후보 후속 acceptance와 비선형 시간 정확도 미완료
|
||||
|
||||
- 조건·관측: 2026-10-19 구성된 전체 Native/CTest998개는 각각987 PASS·10 FAIL·1 SKIP이다.
|
||||
실제 후보 LargeDirect는 field 통과 뒤 mass-null, UniformTranslation은 유한회전 증분2에서
|
||||
triad 일관성이 실패한다. 같은 identity의 보존 strict는 외부100회 소진이므로 원인 동일로
|
||||
분류하지 않는다. C08 strict algebra의 시간 controller4개도 첫 격자에서 실패한다.
|
||||
- 영향: 강체 병진 oracle와 기존 algebra 시간수렴 통과만으로 실제 비선형 후보의
|
||||
q/v/physical a refinement·차수·finest 오차·N7/물리 invariant closure를 인증할 수 없다.
|
||||
N7 한 항목은 해상도 SKIP이다. 후속 Release 후보100증분은5662.144854초/native0으로 완료하고
|
||||
정식9-node U 비교를 통과했으나 비선형 시간 정확도·전체 speedup은 미성립이다.
|
||||
- 지금 해결하지 못한 이유: 승인 local 근사본의 독립 정확도 실패가 동반되며 원인 분리가
|
||||
필요하다. 정해진 근사 수식을 자동 복원하거나 유지할 hidden·triad·평형 기준을 완화해
|
||||
성공시키는 것은 이번 실험의 변경 범위가 아니다. 추가 강제 보정이나 실패 frame commit도 하지 않는다.
|
||||
- 다음 접근: polar/inertia 근사 영향과 candidate 후속 gate 실패의 원인을 분리한다.
|
||||
유지할 물리 기준에서 실제 비선형 후보의 시간·불변량 검증을 확보한 뒤 공식 성능 비교를 수행한다.
|
||||
새 원본 U 비교의 범위와 시간은 [변위 비교](../abaqus-tolerance/reference-displacement-comparison.md)에 있다.
|
||||
전체 실행·실패 identity와 strict 대조 근거는 수렴 정책 결과의 원시 기록에 보존했다.
|
||||
|
||||
## 개발 계측의 정확 parity와 전체 비용 미확정
|
||||
|
||||
- 조건·관측: strict 공식 frame0/1과 짧은 개발 실행의 numeric dataset40개 중18개가 byte
|
||||
불일치했다. 보존한 동일 OFF 실행물의 짧은 대조도 불일치해 계측만의 영향으로 분리되지 않았다.
|
||||
전체100증분 개발 계측은 하지 않았으며, 후보와 strict의1증분 state 차이는 별도 정책 진단이다.
|
||||
- 영향: 개발 probe의 정확 byte parity 통과나 전체 병목 비중·전체 가속률을 확정할 수 없다.
|
||||
- 지금 해결하지 못한 이유: T 단축·반복·thread·컴파일 영향을 분리한 실행 근거가 없고,
|
||||
수치 부적합 상태에서 같은 장시간 실행을 근거 없이 반복하지 않는 실험 조건을 유지한다.
|
||||
- 다음 접근: 수치 적합성을 확보한 뒤 동일 입력·실행물·thread 조건으로 원인을 분리하고
|
||||
전체 증분 비용을 측정한다. 새 parity 허용치나 첫 증분의100배 외삽으로 대신하지 않는다.
|
||||
|
||||
## 정식 기준자료 부재와 추가 fine 비교기 부재
|
||||
|
||||
@@ -30,14 +97,17 @@
|
||||
- 다음 접근: 새 reference의 identity·시점·hash와 blocking 계약을 확정해 별도로 검증한다.
|
||||
음수 온도 clamp·원본 보정·RMS 완화로 기존 기록을 통과로 바꾸지 않는다.
|
||||
|
||||
## Eigen partial-BC 정적 평형 충돌
|
||||
## 기존 drilling 방식의 partial-BC raw 정적 gate 한계
|
||||
|
||||
- 조건·관측: MITC4 eigen의 S3 partial rotational BC 사례와 public static 평형 사이의
|
||||
충돌이 기능 기록에 보류 상태로 남아 있다.
|
||||
- 영향: 모드별 Value 5% 승인 예외가 이 정적 평형 문제나 Abaqus 알고리즘 동등성을 해결하지 않는다.
|
||||
- 지금 해결하지 않는 이유: 수식·공개 정적 평형 계약의 별도 조사이며 문서만으로 고칠 수 없다.
|
||||
- 다음 접근: 동일 source BC에서 static과 eigen의 null/finite 공간·힘/모멘트 의미를 독립 대조한다.
|
||||
부분 회전구속을 full clamp로 대체하지 않는다.
|
||||
- 조건·관측: 보존한 S3 partial rotational BC의 public static 사례는 현재 기본 직접 방식에서
|
||||
raw 인공 모멘트를 별도로 복구·차감한 뒤 기존1e-10 gate를 통과한다. 기존 방식의 raw
|
||||
gate는 같은 사례에서 계속 거부되며 명시 Legacy 검사와 별도 build의 실제 진단에 보존했다.
|
||||
- 영향: 직접 방식의 해당 충돌은 해결했지만 기존 방식의 raw 수용 범위를 넓힌 것은 아니다.
|
||||
Eigen Value5% 예외나 모든 partial-BC 비선형 모델의 성공을 뜻하지 않는다.
|
||||
- 지금 남겨 두는 이유: 기존 방식의 수치 판정 보존이 승인된 계약이다. 그 gate를 자동으로
|
||||
보정값으로 바꾸거나 부분 회전구속을 full clamp로 대체하지 않는다.
|
||||
- 다음 접근: 기존 방식의 수용 범위를 변경하려면 같은 BC에서 null/finite 공간과
|
||||
generalized/spatial 힘·모멘트의 계약을 별도로 판정한다.
|
||||
|
||||
## 성능 목표 미달과 과거 회귀
|
||||
|
||||
|
||||
+67
-15
@@ -1,8 +1,59 @@
|
||||
# 구현과 검증 상태
|
||||
|
||||
## 기준 시점
|
||||
## 2026-10-20 MITC4 drilling 선택
|
||||
|
||||
2026-10-18 저장소 HEAD `945d899` 기준이다. 아래 통과 수치는 각 검증 당시 기록의 범위이며
|
||||
초기 director 직접 스프링을 기본으로 하고 기존 상대 비틀림을 코드 한 곳에서 선택하는
|
||||
구현을 선형·비선형 정적·pressure·frequency·HHT와 저장·reader·비교기에 연결했다.
|
||||
직접 방식의 비객관성은 허용하며 raw/인공/보정 spatial 평형을 구별한다.
|
||||
아래 실행 근거의 제품 source는 `6bbbb2ba8c8078c7a637886201750753da0af5c0`이다.
|
||||
|
||||
| 현재 실제 실행 | 결과와 captured exit |
|
||||
| --- | --- |
|
||||
| Native x64 Debug 전체 | unit915:904 PASS/10 FAIL/1 SKIP, exit1; integration74와 reference46은 모두 PASS, exit0/0 |
|
||||
| CMake x64 Debug 전체 CTest | 같은1035 identity,1024 PASS/10 FAIL/1 SKIP, exit8 |
|
||||
| 별도 Python 전체 |164 PASS, exit0; pytest9.1.1/NumPy2.4.6/h5py3.16.0의 격리 Python3.11 환경 |
|
||||
| 기존 방식 별도 Debug 관련 검사 |162 unique=155 PASS/7 FAIL. 기존 물리5개와 Direct 기본값 전용 선택 오류2개를 분리하며 실제 FAIL은 보존 |
|
||||
| 두 방식의 정식 linear/coarse-NL/eigen |실제 생성·기존 비교 모두 PASS. 현재 선형 변위와 eigen table은 두 방식에서 정확히 동일 |
|
||||
| 직접 방식 정식 HHT |Release CLI0/비교0,729nodes·101frames, 외부9node U101 families PASS; 최대 정규화 RMS0.2282326248391787%, UR 경고200개 |
|
||||
| 기존 방식 실제 HHT |별도 Debug 소형3frame CLI/readback PASS.729node 외부 U 재해석은 미실행 |
|
||||
|
||||
두 Debug build와 별도 기존 방식 Debug/공식 Direct Release build는 exit0이며 실제
|
||||
MSVC19.51.36260.0 x64/v145, toolset directory14.51.36231과 probe false/OFF를 확인했다.
|
||||
Compile source·등록/실행 identity·CL/link·staged/loaded DLL·원본 hash를 대조했다.
|
||||
정식 원본34파일은 불변이고 실제10 HDF5/128평형 frame의 추가 marker·스키마·대수 관계를 검사했다.
|
||||
실행 명령/exit·raw/XML·현재 실패 원인·선별/제외 목록과 hash는
|
||||
`.build/drilling-options-20261020/T4/`에 보존했다. 이 자료가 없는 checkout의 현재 실행 근거로 간주하지 않는다.
|
||||
|
||||
Native/CTest의10 FAIL은 기존 D3·명시 Legacy constraint/C08·후속 mass-null/triad 원인과
|
||||
현재 수치/실패 단계를 대조했다. N7 하나는 해상도 INCONCLUSIVE SKIP이다. Legacy의 선택 오류2개는
|
||||
Direct mode literal 또는 production Direct 수용을 전제한 검사이며 기본 Direct 전체에서는 PASS했다.
|
||||
반력·영운동·실제 Legacy marker는 별도 HHT 출력으로 확인했고, 오류 선택의 FAIL을 PASS로 바꾸지 않았다.
|
||||
정식 fine/fine_02/pressure·새 thermal 자료 누락과 기존 NT11 수치 불일치도 남는다.
|
||||
구현과 관련 실행은 확보했으나 전체 물리/시간정확도 검증 완료는 아니다. 성능 인증은 하지 않았다.
|
||||
|
||||
## 2026-10-19 수렴 실험 기록
|
||||
|
||||
2026-10-19 후속 사용자 결정으로 이번 MITC4 HHT 실험은 **변위 검증 통과·실패10개 보류로 조건부 완료**한다.
|
||||
[수용 결정](decisions/0011-tolerance-experiment-acceptance.md)을 따르며 아래 전체 검사 raw 통계와
|
||||
N7·자료 부재·미해결 물리 문제는 보존한다. 전체 물리/시간정확도 검증 통과로 확대하지 않는다.
|
||||
남은729절점·101프레임 전체 상태 진단과 outer204→171회·관측 시간688.6446983초 감소를
|
||||
[최종 결과](../abaqus-tolerance/results.md#후속-승인과-남은-단계의-완료)에 기록했다. 공식 speedup은 미확정이다.
|
||||
|
||||
2026-10-19 현재 실험 branch는 polar1회·관성 회전/Jacobian3차·질량 gate와 관련 테스트
|
||||
오차1e-3을 유지하며, Abaqus2025 공개 field 규칙을 MITC4 HHT의 실제 종료에 연결했다.
|
||||
Accepted 평균 이력과 hht-log-v2도 구현했다. Native Debug와 CMake Debug/CTest 전체는
|
||||
각각998개 중987 PASS·10 FAIL·1 SKIP이다. Python145개와 별도 Release 정식 Hex8
|
||||
5모델 CLI/비교기는 통과했다. Native unit883개 중10 FAIL·1 SKIP, integration72개와
|
||||
reference43개는 모두 통과했다. 두 build 성공과 source/등록 identity/DLL 대조도 확보했다.
|
||||
물리 실패·N7 해상도 미판정·실제 비선형 후보 시간 정확도 미완료로 전체 검증 완료는 아니다.
|
||||
Strict 공식 기준6350.7895521초를 확보했다. 후속 변위 비교 요청에서 Release 후보 원본100증분을
|
||||
5662.144854초에 완료하고 U PASS를 확인했으며 공식 speedup은 미성립이다.
|
||||
정식 변위8모델의 Release 구성·실행 시간·오차는 [변위 비교](../abaqus-tolerance/reference-displacement-comparison.md)에 있다.
|
||||
상세 revision·실행 증거는 [수렴 정책 실험 결과](../abaqus-tolerance/results.md)에 있다.
|
||||
|
||||
## 과거 통합 기준 시점
|
||||
|
||||
과거 통합 기준은 2026-10-18 HEAD `945d899`다. 과거 통과 수치는 각 검증 당시 기록의 범위이며
|
||||
후속 source에서 새로 실행한 결과로 승계하지 않는다.
|
||||
현재 용도는 개인 연구용 로컬 실행이며 장기 목표는 외부 사용자 배포다. 배포 형태·지원 환경·
|
||||
dependency 재배포 조건은 확정되지 않았다.
|
||||
@@ -11,14 +62,14 @@ dependency 재배포 조건은 확정되지 않았다.
|
||||
|
||||
| 영역 | 구현된 범위 | 확보된 검증과 한계 |
|
||||
| --- | --- | --- |
|
||||
| B33·MITC4 선형 정적 | 입력 → solve → 복구 → HDF5 | 2026-10-18 정식 정적 모델 실행·비교 범위에 포함. 일반 곡선 beam·광범위 distorted/curved shell 정확도를 뜻하지 않는다. |
|
||||
| MITC4 비선형 정적·압력 | Full Newton·trial/commit·정적 follower pressure | ShellNL 정식 기본 모델의 U blocking·UR warning·내부 평형 통과 기록. RF/RM은 review-only. Fine·추가 fine·pressure의 정식 자료가 없고 추가 fine 비교기 연결도 없다. |
|
||||
| Shell frequency | Lumped mass·실제 FEAST·null/finite 공간·전체 반환 pair 검증 | 2026-09-10 첫 구현의 417개 통과와 Value 10개 모드별 5% 기록. 최신 통합본의 frequency 수치 실행을 완료한 근거로 승계하지 않는다. |
|
||||
| Shell HHT | Nodal-lumped-kinetic·reduced-coordinate·fixed-null·schema2 | 과거 C09와 C08a 7-grid, baseline 원본 U 비교 기록. 최신 전체 회귀·C08b finer-grid·최신 원본 비교·전체 물리 판정은 미완료. 중단을 수치 실패나 통과로 세지 않는다. |
|
||||
| Hex8 | Mean-volume 강성·reference row-sum mass·정적/고유치/HHT·high+low | 2026-10-13 관련 C++ 91개·Python 96개와 보존 결과 5개 비교 기록. 2026-10-18에는 정적 2개를 재실행. 최신 고유치·동적 전체 검증은 별도다. |
|
||||
| DS4 thermal | 5 온도점·consistent capacity·BE·flux/reaction·원자적 출력 | 당시 추가 139개 검사 기록. 기존 NT11은 diagnostic 불일치이며 새 필수 외부 reference 검증은 미완료. 최신 통합본의 전체 열 수치 실행도 미완료다. |
|
||||
| 공통 구조·빌드 | Typed procedure/capability/outcome·private backend·Native/CMake | 2026-10-18 두 경로 전체 target build·공개 header 111개 독립 compile·runtime DLL 7개 대조 기록. 물리 기능 전체 검증과 구분한다. |
|
||||
| 성능 | MKL 행렬 연산·제한된 불변 cache·Hex8 SIMD·shell 산술 최적화 | 세 모델의 목표 달성은 미완료. 단일 개발 표본과 복원된 후보의 통과를 최종 성능으로 승계하지 않는다. |
|
||||
| B33·MITC4 선형 정적 | 입력 → solve → 복구 → HDF5 | 2026-10-19 두 Debug 경로의 구성된 reference 실행·비교 포함. 일반 곡선 beam·광범위 distorted/curved shell 정확도를 뜻하지 않는다. |
|
||||
| MITC4 비선형 정적·압력 | Full Newton·trial/commit·정적 follower pressure | 2026-10-19 정식 coarse 모델의 기존 비교를 포함한 구성 검사 실행. RF/RM은 review-only. Fine·추가 fine·pressure 자료 부재와 추가 fine 비교 계약 부재는 남는다. |
|
||||
| Shell frequency | Lumped mass·실제 FEAST·null/finite 공간·전체 반환 pair 검증 | 2026-10-19 구성된 두 Debug 경로 reference 실행에 포함. 한정된 Value 10개 모드별5% 계약을 다른 고유치 비교로 확대하지 않는다. |
|
||||
| Shell HHT | Reduced-coordinate·fixed-null·schema2·Abaqus2025 field 종료·log-v2 | 두 전체 경로에서 동일10 FAIL·N7 SKIP1. 실제 조기 종료·강체 oracle는 통과하나 비선형 시간 정확도와 후속 acceptance 실패는 미해결. Strict와 후속 Release field 후보 원본의9-node U 비교를 확보했다. |
|
||||
| Hex8 | Mean-volume 강성·reference row-sum mass·정적/고유치/HHT·high+low | 2026-10-19 구성된 두 Debug 경로 전체 실행과 별도 Release 정식5모델의 새 HDF5·기존 비교 통과. 공식 성능 비교가 아니다. |
|
||||
| DS4 thermal | 5 온도점·consistent capacity·BE·flux/reaction·원자적 출력 | 2026-10-19 구성된 unit/integration/reference 실행 완료. 기존 NT11 diagnostic 불일치와 새 필수 외부 reference 부재는 남는다. |
|
||||
| 공통 구조·빌드 | Typed procedure/capability/outcome·private backend·Native/CMake | 2026-10-19 두 Debug build 성공, compile source223개·실행 identity998개·runtime DLL7개 일치. 수치 검증 전체 통과와 구분한다. |
|
||||
| 성능 | MKL 행렬 연산·제한된 불변 cache·Hex8 SIMD·shell 산술 최적화 | Strict HHT 공식6350.7895521초 확보. 후보의1증분 호출 수 감소는 한정 진단이며 공식100증분 가속·목표 달성은 미확정. |
|
||||
|
||||
2026-10-18 통합 기록은 두 빌드 경로에서 unit 849·integration 72·reference 43, 총 964개
|
||||
등록 identity가 일치한다고 보고한다. 사용자 지시로 정적·공통 631개(unit 555·integration 35·
|
||||
@@ -39,10 +90,10 @@ Fresh Refactor·통합 Native·통합 CMake 실행물에서 같은 scratch 입
|
||||
|
||||
1. 정식 fine·추가 fine·pressure·새 thermal 기준자료의 제공 여부와 source/frame/hash 계약을
|
||||
확정한다. 추가 fine는 자료뿐 아니라 전용 case identity와 comparator 연결이 필요하다.
|
||||
2. 현재 source·compiler·build tree를 묶어 비정적 수치 검증을 수행한다. Shell HHT 시간수렴·
|
||||
원본 비교, solid dynamics, frequency, transient thermal을 선택된 정적 근거와 구분한다.
|
||||
자료가 필요한 외부 비교는 정식 자료 확보 뒤 수행한다.
|
||||
3. Eigen partial-BC 정적 평형 충돌과 thermal 외부 불일치의 영향을 각각 판정한다.
|
||||
2. 구성된 전체 실행 결과를 바탕으로 MITC4 local 미분/물리 실패와 실제 후보 후속 gate 실패를
|
||||
분리한다. 후보 비선형 q/v/physical a 시간수렴·N7 및 후보 원본 비교를 확보한다.
|
||||
전체 검사 실행 사실과 미해결 수치 검증을 구분하고, 자료가 필요한 비교는 확보 뒤 수행한다.
|
||||
3. 기존 drilling 방식의 partial-BC raw 정적 gate 한계와 thermal 외부 불일치의 영향을 각각 판정한다.
|
||||
기준 완화·질량 변경·원본 보정으로 해결하지 않는다.
|
||||
4. 같은 물리·허용치·출력·정밀도를 보존한 상태에서 성능 회귀 원인을 조사하고 최종 source의
|
||||
Release 시간과 수치 결과를 다시 측정한다. 검증되지 않은 가속을 완료로 기록하지 않는다.
|
||||
@@ -60,4 +111,5 @@ Fresh Refactor·통합 Native·통합 CMake 실행물에서 같은 scratch 입
|
||||
|
||||
Hex8 동적 개발 표본 117.7986225초는 native 0과 U 비교 통과였으나 100초 목표를 넘었다.
|
||||
Refactor 당시 ShellNL elapsed 회귀 Native +86.52%·CMake +120.91%는 과거 단일 기록이며
|
||||
원인·변산이 미확정이다. 최종 통합 source의 새 Release 성능 측정은 미완료다.
|
||||
원인·변산이 미확정이다. 2026-10-19 승인 local 근사본의 strict HHT는6350.7895521초였다.
|
||||
현재 field 후보의 공식 Release 시간과 speedup은 수치 실패로 미성립이다.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
@@ -72,6 +73,8 @@ class AnalysisState {
|
||||
double PhysicalStrainEnergy() const noexcept;
|
||||
/// @brief Returns global force and moment equilibrium components.
|
||||
const std::array<double, 6>& Equilibrium() const noexcept;
|
||||
/// @brief Returns actual MITC4 mode and artificial spatial moment evidence.
|
||||
const std::optional<Mitc4DrillingEvidence>& Mitc4Drilling() const noexcept;
|
||||
/// @brief Returns normalized shell verification metrics.
|
||||
const std::array<double, 3>& VerificationMetrics() const noexcept;
|
||||
|
||||
@@ -96,6 +99,7 @@ class AnalysisState {
|
||||
double physical_strain_energy_{0.0};
|
||||
std::array<double, 6> equilibrium_{};
|
||||
std::array<double, 3> verification_metrics_{};
|
||||
std::optional<Mitc4DrillingEvidence> mitc4_drilling_;
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/source_identity.h"
|
||||
@@ -54,6 +55,7 @@ struct DynamicAcceptedFrameState {
|
||||
double external_work = 0.0;
|
||||
double energy_defect = 0.0;
|
||||
std::array<double, 6> global_equilibrium{};
|
||||
std::optional<Mitc4DrillingEvidence> mitc4_drilling;
|
||||
std::array<double, 3> linear_momentum{};
|
||||
std::array<double, 3> angular_momentum{};
|
||||
std::array<double, 3> linear_momentum_rate{};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/math/matrix.h"
|
||||
#include "fesa/elements/mitc4_drilling_mode.h"
|
||||
#include "fesa/solvers/eigen/eigen_solver.h"
|
||||
|
||||
namespace fesa {
|
||||
@@ -53,6 +54,7 @@ struct EigenAnalysisState {
|
||||
explicit EigenAnalysisState(FeastEigenResult backend_result)
|
||||
: backend{std::move(backend_result)} {}
|
||||
|
||||
std::optional<Mitc4DrillingMode> drilling_mode;
|
||||
std::size_t full_dof_count{0U};
|
||||
std::size_t free_dof_count{0U};
|
||||
std::size_t mass_null_dimension{0U};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/analysis/nonlinear_static_control.h"
|
||||
@@ -52,6 +53,17 @@ struct NewtonIterationLogRecord {
|
||||
bool combined_passed;
|
||||
};
|
||||
|
||||
/// @brief Vendor-neutral field evidence in full global u/total-phi coordinates.
|
||||
/// @note Missing quantities serialize as literal not-evaluated. Force uses
|
||||
/// force/length units; generalized moment uses moment/rad units.
|
||||
struct DynamicFieldLogEvidence {
|
||||
std::optional<double> residual_numerator, residual_scale, spatial_flux;
|
||||
std::optional<double> time_average_flux, correction_numerator, increment_norm;
|
||||
std::optional<double> estimated_correction, residual_threshold, correction_threshold;
|
||||
std::string branch = "not-evaluated";
|
||||
std::optional<bool> residual_pass, correction_pass, field_pass;
|
||||
};
|
||||
|
||||
/// @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
|
||||
@@ -71,6 +83,14 @@ struct DynamicNewtonIterationLogRecord {
|
||||
std::array<double, 2U> algorithmic_correction{};
|
||||
bool residual_pass = false;
|
||||
bool correction_pass = false;
|
||||
// Absent policy preserves the historical v1 direct-writer/frame-zero contract.
|
||||
std::string policy_id;
|
||||
std::string step_name;
|
||||
std::array<DynamicFieldLogEvidence, 2U> fields;
|
||||
std::optional<bool> field_pass;
|
||||
bool accepted = false;
|
||||
bool strict_residual_evaluated = false;
|
||||
bool strict_correction_evaluated = false;
|
||||
};
|
||||
|
||||
/// @brief Writes deterministic, line-oriented Newton iteration diagnostics.
|
||||
@@ -90,7 +110,7 @@ class NewtonIterationLogWriter {
|
||||
/// failure with code `log-output-failure`.
|
||||
Status Write(const NewtonIterationLogRecord& record);
|
||||
|
||||
/// @brief Writes one complete HHT record using schema `hht-log-v1`.
|
||||
/// @brief Writes HHT v1 evidence, or v2 when the field policy is identified.
|
||||
/// @return Success after the complete line is written; invalid numeric
|
||||
/// evidence or stream failure permanently fails this writer instance.
|
||||
Status Write(const DynamicNewtonIterationLogRecord& record);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/source_identity.h"
|
||||
@@ -46,6 +47,7 @@ struct NonlinearTrialState {
|
||||
std::vector<ShellResultRow> shell_results;
|
||||
double physical_strain_energy;
|
||||
std::array<double, 6> global_equilibrium;
|
||||
std::optional<Mitc4DrillingEvidence> mitc4_drilling;
|
||||
};
|
||||
|
||||
/// @brief Stores one authoritative nonlinear increment frame.
|
||||
@@ -69,6 +71,7 @@ struct AcceptedFrameState {
|
||||
std::vector<ShellResultRow> shell_results;
|
||||
double physical_strain_energy;
|
||||
std::array<double, 6> global_equilibrium;
|
||||
std::optional<Mitc4DrillingEvidence> mitc4_drilling;
|
||||
};
|
||||
|
||||
/// @brief Owns committed nonlinear state and validated accepted frames.
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#ifndef FESA_ELEMENTS_ELEMENT_H_
|
||||
#define FESA_ELEMENTS_ELEMENT_H_
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
#include "fesa/elements/element_dof_layout.h"
|
||||
#include "fesa/elements/mitc4_drilling_mode.h"
|
||||
#include "fesa/math/matrix.h"
|
||||
#include "fesa/math/vector.h"
|
||||
#include "fesa/results/result_records.h"
|
||||
@@ -30,6 +33,8 @@ struct BeamElementResultRows {
|
||||
struct ShellElementResultRows {
|
||||
std::vector<ShellResultRow> rows;
|
||||
double physical_strain_energy{0.0};
|
||||
std::optional<Mitc4DrillingMode> drilling_mode;
|
||||
std::array<double, 3> drilling_moment{};
|
||||
};
|
||||
|
||||
/// @brief Selects the physical recovery shape of one runtime element.
|
||||
|
||||
@@ -178,6 +178,8 @@ class ShellReferenceElement : public Element {
|
||||
virtual Result<Mitc4Stiffness> Stiffness() const = 0;
|
||||
/// @copydoc Mitc4Shell::Mass
|
||||
virtual Result<Mitc4Mass> Mass(double density) const = 0;
|
||||
/// @brief Returns the actual immutable reference-kernel drilling formulation.
|
||||
virtual Mitc4DrillingMode DrillingMode() const noexcept = 0;
|
||||
};
|
||||
|
||||
/// @brief Borrows the same linear MITC4 kernel for stiffness and recovery.
|
||||
@@ -190,6 +192,7 @@ class Mitc4ReferenceAdapter final : public ShellReferenceElement {
|
||||
Result<ElementResultBundle> Recover(const Vector& u) const override;
|
||||
Result<Mitc4Stiffness> Stiffness() const override;
|
||||
Result<Mitc4Mass> Mass(double density) const override;
|
||||
Mitc4DrillingMode DrillingMode() const noexcept override;
|
||||
|
||||
private:
|
||||
const Mitc4Shell& kernel_;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef FESA_ELEMENTS_MITC4_DRILLING_MODE_H_
|
||||
#define FESA_ELEMENTS_MITC4_DRILLING_MODE_H_
|
||||
|
||||
namespace fesa {
|
||||
|
||||
/// @brief Identifies the immutable numerical MITC4 drilling formulation.
|
||||
enum class Mitc4DrillingMode { kDirectSpring, kLegacyCorotatedTwist };
|
||||
|
||||
/// @brief Returns the stable storage name of a supported drilling formulation.
|
||||
constexpr const char* Mitc4DrillingModeName(Mitc4DrillingMode mode) noexcept {
|
||||
switch (mode) {
|
||||
case Mitc4DrillingMode::kDirectSpring:
|
||||
return "direct-initial-director-spring";
|
||||
case Mitc4DrillingMode::kLegacyCorotatedTwist:
|
||||
return "legacy-corotated-twist";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
namespace mitc4_drilling_internal {
|
||||
struct TestAccess;
|
||||
} // namespace mitc4_drilling_internal
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
#endif // FESA_ELEMENTS_MITC4_DRILLING_MODE_H_
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "fesa/core/status.h"
|
||||
#include "fesa/elements/element.h"
|
||||
#include "fesa/elements/mitc4_shell_definition.h"
|
||||
#include "fesa/elements/mitc4_drilling_mode.h"
|
||||
#include "fesa/materials/isotropic_linear_elastic_material.h"
|
||||
#include "fesa/math/matrix.h"
|
||||
#include "fesa/math/vector.h"
|
||||
@@ -92,6 +93,11 @@ class Mitc4Shell final : public Element {
|
||||
std::array<std::array<double, 3>, 4> initial_directors,
|
||||
const ShellSection& section, const LinearElasticMaterial& material);
|
||||
|
||||
/// @brief Returns this immutable kernel's production drilling formulation.
|
||||
[[nodiscard]] Mitc4DrillingMode DrillingMode() const noexcept {
|
||||
return drilling_mode_;
|
||||
}
|
||||
|
||||
/// @brief Returns the factory-bound MITC4 DOF order.
|
||||
const ElementDofLayout& DofLayout() const noexcept override;
|
||||
|
||||
@@ -163,6 +169,7 @@ class Mitc4Shell final : public Element {
|
||||
|
||||
private:
|
||||
friend class ElementFactory;
|
||||
friend struct mitc4_drilling_internal::TestAccess;
|
||||
|
||||
/// @brief Binds Domain identity after the numerical candidate is valid.
|
||||
void BindRuntime(ElementDofLayout layout, EntityIndex element_index,
|
||||
@@ -203,6 +210,7 @@ class Mitc4Shell final : public Element {
|
||||
double thickness_;
|
||||
double youngs_modulus_;
|
||||
double poisson_ratio_;
|
||||
Mitc4DrillingMode drilling_mode_;
|
||||
SourceLocation source_location_;
|
||||
std::string identity_;
|
||||
ElementDofLayout dof_layout_;
|
||||
|
||||
@@ -54,6 +54,8 @@ struct NonlinearMitc4InternalEquilibrium {
|
||||
Matrix drilling_metric{0U, 0U};
|
||||
double drilling_energy = 0.0;
|
||||
Vector force_directional_second{0U};
|
||||
/// @brief Stores D2(f_drill)[v,v] separately; direct springs give exact zero.
|
||||
Vector drilling_force_directional_second{0U};
|
||||
Vector drilling_constraint{0U};
|
||||
Matrix drilling_constraint_gradient{0U, 0U};
|
||||
std::array<Matrix, 4> drilling_constraint_hessian{
|
||||
@@ -104,6 +106,11 @@ class NonlinearMitc4Shell {
|
||||
std::array<ShellNodeInitialFrame, 4> initial_frames,
|
||||
const ShellSection& section, const LinearElasticMaterial& material);
|
||||
|
||||
/// @brief Returns this immutable kernel's numerical drilling formulation.
|
||||
[[nodiscard]] Mitc4DrillingMode DrillingMode() const noexcept {
|
||||
return drilling_mode_;
|
||||
}
|
||||
|
||||
/// @brief Evaluates trial physical kinematics and fixed recovery rows.
|
||||
/// @param trial Global nodal translation and fixed-global rotation vectors.
|
||||
/// @return Physical recovery or a fail-closed trial diagnostic.
|
||||
@@ -134,6 +141,7 @@ class NonlinearMitc4Shell {
|
||||
|
||||
private:
|
||||
friend struct nonlinear_mitc4_internal::ConfigurationSnapshot;
|
||||
friend struct mitc4_drilling_internal::TestAccess;
|
||||
|
||||
/// @brief Retains validated fixed-reference data without analysis ownership.
|
||||
NonlinearMitc4Shell(std::array<Vector3, 4> coordinates,
|
||||
@@ -155,6 +163,7 @@ class NonlinearMitc4Shell {
|
||||
double youngs_modulus_;
|
||||
double poisson_ratio_;
|
||||
double drilling_stiffness_;
|
||||
Mitc4DrillingMode drilling_mode_;
|
||||
SourceLocation source_location_;
|
||||
std::string identity_;
|
||||
};
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/source_identity.h"
|
||||
#include "fesa/elements/mitc4_drilling_mode.h"
|
||||
|
||||
namespace fesa {
|
||||
|
||||
@@ -70,6 +72,15 @@ struct ShellResultRow {
|
||||
std::array<ShellSectionStressRow, 3> stress;
|
||||
};
|
||||
|
||||
/// @brief Separates artificial internal spatial moment from raw equilibrium.
|
||||
/// @note The actual immutable kernel mode accompanies every production frame.
|
||||
struct Mitc4DrillingEvidence {
|
||||
Mitc4DrillingMode mode;
|
||||
std::array<double, 3> drilling_moment{};
|
||||
// Raw force balance and raw moment balance minus positive internal drilling.
|
||||
std::array<double, 6> equilibrium_with_drilling{};
|
||||
};
|
||||
|
||||
/// @brief Carries a complete shell result candidate for atomic validation.
|
||||
struct ShellStateCandidate {
|
||||
std::vector<ShellResultRow> rows;
|
||||
@@ -79,6 +90,7 @@ struct ShellStateCandidate {
|
||||
// [FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED,
|
||||
// MOMENT_BALANCE_NORMALIZED].
|
||||
std::array<double, 3> verification_metrics{};
|
||||
std::optional<Mitc4DrillingEvidence> mitc4_drilling;
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<!-- Override installation roots with environment variables or this ignored sheet. -->
|
||||
<Import Project="Fesa.Local.props" Condition="Exists('Fesa.Local.props')" />
|
||||
<PropertyGroup>
|
||||
<FesaHhtDevelopmentProbe Condition="'$(FesaHhtDevelopmentProbe)'==''">false</FesaHhtDevelopmentProbe>
|
||||
<FesaRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\'))</FesaRoot>
|
||||
<FesaBuildRoot>$(FesaRoot).build\native\$(Configuration)\</FesaBuildRoot>
|
||||
<FesaRootForward>$(FesaRoot.Replace('\','/').TrimEnd('/'))</FesaRootForward>
|
||||
@@ -13,6 +14,8 @@
|
||||
<IntDir>$(FesaBuildRoot)obj\$(MSBuildProjectName)\</IntDir>
|
||||
<TargetName Condition="'$(MSBuildProjectName)'=='fesa_cli'">fesa</TargetName>
|
||||
<TbbSuffix Condition="'$(Configuration)'=='Debug'">_debug</TbbSuffix>
|
||||
<FesaMklThreadName>mkl_tbb_thread</FesaMklThreadName>
|
||||
<FesaMklThreadName Condition="'$(Configuration)'=='Debug'">mkl_tbb_threadd</FesaMklThreadName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<FesaExternalOptions Condition="'$(MSBuildProjectName)'=='fesa_solver'">/external:I"$(FESA_MKL_ROOT)\include" /external:I"$(FESA_TBB_ROOT)\include" /external:I"$(FESA_HDF5_ROOT)\include" /external:W0</FesaExternalOptions>
|
||||
@@ -20,6 +23,9 @@
|
||||
<FesaExternalOptions Condition="'$(MSBuildProjectName)'=='fesa_unit_tests'">$(FesaExternalOptions) /external:I"$(FESA_MKL_ROOT)\include"</FesaExternalOptions>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile Condition="'$(FesaHhtDevelopmentProbe)'=='true' And ('$(MSBuildProjectName)'=='fesa_solver' Or '$(MSBuildProjectName)'=='fesa_unit_tests')">
|
||||
<PreprocessorDefinitions>FESA_HHT_DEVELOPMENT_PROBE=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<ClCompile>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<SupportJustMyCode>false</SupportJustMyCode>
|
||||
@@ -41,7 +47,7 @@
|
||||
<PreprocessorDefinitions>WIN32;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions Condition="'$(Configuration)'=='Release'">NDEBUG;WIN32;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>$(FesaRoot)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories Condition="'$(MSBuildProjectName)'=='fesa_solver' Or '$(MSBuildProjectName)'=='fesa_unit_tests' Or '$(MSBuildProjectName)'=='fesa_integration_tests'">$(FesaRoot)include;$(FesaRoot)src\fesa;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories Condition="'$(MSBuildProjectName)'=='fesa_solver' Or '$(MSBuildProjectName)'=='fesa_unit_tests' Or '$(MSBuildProjectName)'=='fesa_integration_tests' Or '$(MSBuildProjectName)'=='fesa_reference_tests'">$(FesaRoot)include;$(FesaRoot)src\fesa;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalOptions>$(FesaExternalOptions) /MP2 %(AdditionalOptions)</AdditionalOptions>
|
||||
<ExternalWarningLevel>TurnOffAllWarnings</ExternalWarningLevel>
|
||||
</ClCompile>
|
||||
@@ -52,8 +58,8 @@
|
||||
<OptimizeReferences Condition="'$(Configuration)'=='Release' And '$(MSBuildProjectName)'=='fesa_cli'">true</OptimizeReferences>
|
||||
<EnableCOMDATFolding Condition="'$(Configuration)'=='Release' And '$(MSBuildProjectName)'=='fesa_cli'">true</EnableCOMDATFolding>
|
||||
<GenerateDebugInformation Condition="'$(Configuration)'=='Debug'">true</GenerateDebugInformation>
|
||||
<AdditionalLibraryDirectories>$(FESA_MKL_ROOT)\lib;$(FESA_OMP_ROOT)\lib;$(FESA_TBB_ROOT)\lib;$(FESA_HDF5_ROOT)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>mkl_intel_ilp64_dll.lib;mkl_intel_thread_dll.lib;mkl_core_dll.lib;libiomp5md.lib;tbb12$(TbbSuffix).lib;hdf5.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(FESA_MKL_ROOT)\lib;$(FESA_TBB_ROOT)\lib;$(FESA_HDF5_ROOT)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>mkl_intel_ilp64_dll.lib;$(FesaMklThreadName)_dll.lib;mkl_core_dll.lib;tbb12$(TbbSuffix).lib;hdf5.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(MSBuildProjectName)'=='fesa_solver'">
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
<Error Condition="'$(Platform)'!='x64'" Text="FESA requires MSVC x64." />
|
||||
<Error Condition="!Exists('$(FESA_GTEST_SOURCE_DIR)\googletest\src\gtest-all.cc')" Text="Set FESA_GTEST_SOURCE_DIR to the approved local GoogleTest source checkout." />
|
||||
<Error Condition="!Exists('$(FESA_MKL_ROOT)\lib\mkl_intel_ilp64_dll.lib')" Text="Set FESA_MKL_ROOT to the oneMKL installation root." />
|
||||
<Error Condition="!Exists('$(FESA_MKL_ROOT)\lib\$(FesaMklThreadName)_dll.lib')" Text="The selected oneMKL TBB-thread import library was not resolved." />
|
||||
<Error Condition="!Exists('$(FESA_TBB_ROOT)\lib\tbb12$(TbbSuffix).lib')" Text="Set FESA_TBB_ROOT to the oneTBB installation root." />
|
||||
<Error Condition="!Exists('$(FESA_HDF5_ROOT)\lib\hdf5.lib')" Text="Set FESA_HDF5_ROOT to the shared HDF5 installation root." />
|
||||
<Error Condition="!Exists('$(FESA_OMP_ROOT)\bin\libiomp5md.dll') Or !Exists('$(FESA_OMP_ROOT)\bin\libmmd.dll')" Text="Set FESA_OMP_ROOT to the Intel compiler runtime installation root." />
|
||||
<Error Condition="!Exists('$(FESA_OMP_ROOT)\bin\libmmd.dll')" Text="Set FESA_OMP_ROOT to the Intel compiler runtime installation root required by HDF5." />
|
||||
</Target>
|
||||
<Target Name="StageFesaRuntime" AfterTargets="Build" Condition="'$(ConfigurationType)'=='Application'">
|
||||
<ItemGroup>
|
||||
<FesaMklThread Include="$(FESA_MKL_ROOT)\bin\mkl_intel_thread.*.dll" />
|
||||
<FesaMklThread Include="$(FESA_MKL_ROOT)\bin\$(FesaMklThreadName).*.dll" />
|
||||
<FesaMklCore Include="$(FESA_MKL_ROOT)\bin\mkl_core.*.dll" />
|
||||
<FesaMklDispatch Include="$(FESA_MKL_ROOT)\bin\mkl_def.*.dll" />
|
||||
<FesaRuntime Include="@(FesaMklThread);@(FesaMklCore);@(FesaMklDispatch);$(FESA_TBB_ROOT)\bin\tbb12$(TbbSuffix).dll;$(FESA_OMP_ROOT)\bin\libiomp5md.dll;$(FESA_OMP_ROOT)\bin\libmmd.dll;$(FESA_HDF5_ROOT)\bin\hdf5.dll" />
|
||||
<FesaRuntime Include="@(FesaMklThread);@(FesaMklCore);@(FesaMklDispatch);$(FESA_TBB_ROOT)\bin\tbb12$(TbbSuffix).dll;$(FESA_OMP_ROOT)\bin\libmmd.dll;$(FESA_HDF5_ROOT)\bin\hdf5.dll" />
|
||||
</ItemGroup>
|
||||
<Error Condition="'@(FesaMklThread)'==''" Text="The oneMKL Intel-thread runtime DLL was not resolved." />
|
||||
<Error Condition="'@(FesaMklThread)'==''" Text="The selected oneMKL TBB-thread runtime DLL was not resolved." />
|
||||
<Error Condition="'@(FesaMklCore)'==''" Text="The oneMKL core runtime DLL was not resolved." />
|
||||
<Error Condition="'@(FesaMklDispatch)'==''" Text="The oneMKL default-dispatch runtime DLL was not resolved." />
|
||||
<Copy SourceFiles="@(FesaRuntime)" DestinationFolder="$(OutDir)" SkipUnchangedFiles="true" />
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
<ClCompile Include="..\src\fesa\analysis\eigen_reduction.cpp" />
|
||||
<ClCompile Include="..\src\fesa\analysis\hht_time_integration.cpp" />
|
||||
<ClCompile Include="..\src\fesa\analysis\hht_dynamic_algebra.cpp" />
|
||||
<ClInclude Include="..\src\fesa\analysis\hht_development_probe.h" />
|
||||
<ClCompile Include="..\src\fesa\analysis\hht_dynamic_assembly.cpp" />
|
||||
<ClCompile Include="..\src\fesa\analysis\hht_field_convergence.cpp" />
|
||||
<ClCompile Include="..\src\fesa\analysis\hht_reduced_operators.cpp" />
|
||||
<ClCompile Include="..\src\fesa\analysis\hht_reduced_procedure.cpp" />
|
||||
<ClCompile Include="..\src\fesa\analysis\hht_dynamic_procedure.cpp" />
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
<ClCompile Include="..\tests\unit\analysis\hht_energy_verification_test.cpp" />
|
||||
<ClCompile Include="..\tests\unit\analysis\hht_temporal_verification_test.cpp" />
|
||||
<ClCompile Include="..\tests\unit\analysis\hht_dynamic_algebra_test.cpp" />
|
||||
<ClCompile Include="..\tests\unit\analysis\hht_development_probe_test.cpp" />
|
||||
<ClCompile Include="..\tests\unit\analysis\hht_field_convergence_test.cpp" />
|
||||
<ClCompile Include="..\tests\unit\analysis\hht_reduced_shell_test.cpp" />
|
||||
<ClCompile Include="..\tests\unit\analysis\hht_dynamic_procedure_test.cpp" />
|
||||
<ClCompile Include="..\tests\unit\analysis\nonlinear_analysis_state_test.cpp" />
|
||||
|
||||
@@ -197,6 +197,38 @@ def dataset(file: h5py.File, path: str, shape: tuple[int, ...]) -> h5py.Dataset:
|
||||
return result
|
||||
|
||||
|
||||
def drilling_diagnostics(file: h5py.File) -> dict[str, Any] | None:
|
||||
"""Validate additive diagnostics without introducing a balance verdict."""
|
||||
metadata = file["metadata"]
|
||||
mode_present = "mitc4_drilling_formulation" in metadata.attrs
|
||||
version_present = "drilling_diagnostics_version" in metadata.attrs
|
||||
require(mode_present == version_present, "partial-drilling-markers", "Both drilling markers are required.")
|
||||
if not mode_present:
|
||||
return None
|
||||
mode = string_attribute(metadata, "mitc4_drilling_formulation")
|
||||
require(mode in ("direct-initial-director-spring", "legacy-corotated-twist"),
|
||||
"unknown-drilling-formulation", mode)
|
||||
numeric_attribute(metadata, "drilling_diagnostics_version", "<u8", 1)
|
||||
frames = []
|
||||
for frame in range(FRAME_COUNT):
|
||||
root = f"steps/{STEP}/frames/{frame}/global"
|
||||
values = {}
|
||||
for name, size in (("equilibrium", 6), ("drilling_moment", 3), ("equilibrium_with_drilling", 6)):
|
||||
stored = dataset(file, f"{root}/{name}", (size,))
|
||||
require(stored.dtype == np.dtype("<f8"), "wrong-drilling-dataset-type", stored.name)
|
||||
values[name] = stored[:]
|
||||
require(np.isfinite(values[name]).all(), "nonfinite-drilling-value", stored.name)
|
||||
expected = values["equilibrium"].copy()
|
||||
expected[3:] -= values["drilling_moment"]
|
||||
require(np.array_equal(expected, values["equilibrium_with_drilling"]),
|
||||
"inconsistent-drilling-equilibrium", str(frame))
|
||||
frames.append({"storage_step": STEP, "frame_index": frame,
|
||||
"raw_equilibrium": values["equilibrium"].tolist(),
|
||||
"drilling_moment": values["drilling_moment"].tolist(),
|
||||
"equilibrium_with_drilling": values["equilibrium_with_drilling"].tolist()})
|
||||
return {"formulation": mode, "version": 1, "frames": frames}
|
||||
|
||||
|
||||
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."""
|
||||
@@ -319,9 +351,10 @@ def read_results(path: Path, nodes: dict[int, tuple[float, ...]],
|
||||
if frame == 0:
|
||||
require((values == 0.0).all(), "nonzero-initial-displacement", "Frame zero must have q0=0.")
|
||||
projected.append(values[selection])
|
||||
drilling = drilling_diagnostics(file)
|
||||
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)}
|
||||
"projected_node_labels": list(NODE_LABELS), "drilling_diagnostics": drilling}
|
||||
|
||||
|
||||
def evaluate_family(increment: int, quantity: str, result: list[float],
|
||||
@@ -374,6 +407,7 @@ def evaluate_family(increment: int, quantity: str, result: list[float],
|
||||
|
||||
def compare(input_path: Path, reference_path: Path, results_path: Path) -> dict[str, Any]:
|
||||
report: dict[str, Any] = {
|
||||
"drilling_diagnostics": None,
|
||||
"schema": "hht-reference-comparison-v1", "case": CASE, "policy": dict(POLICY),
|
||||
"identities": {}, "precheck": {"passed": False}, "families": [],
|
||||
"blocking_failures": [], "warnings": [], "blocking_passed": False, "verdict": "failed",
|
||||
@@ -386,6 +420,7 @@ def compare(input_path: Path, reference_path: Path, results_path: Path) -> dict[
|
||||
nodes, content_identity = input_inventory(input_path)
|
||||
reference = read_reference(reference_path)
|
||||
result, evidence = read_results(results_path, nodes, content_identity)
|
||||
report["drilling_diagnostics"] = evidence.pop("drilling_diagnostics")
|
||||
families = []
|
||||
for frame in range(FRAME_COUNT):
|
||||
for quantity, first in (("U", 0), ("UR", 3)):
|
||||
|
||||
@@ -7,7 +7,9 @@ add_library(
|
||||
analysis/eigen_reduction.cpp
|
||||
analysis/hht_time_integration.cpp
|
||||
analysis/hht_dynamic_algebra.cpp
|
||||
analysis/hht_development_probe.h
|
||||
analysis/hht_dynamic_assembly.cpp
|
||||
analysis/hht_field_convergence.cpp
|
||||
analysis/hht_reduced_operators.cpp
|
||||
analysis/hht_reduced_procedure.cpp
|
||||
analysis/hht_dynamic_procedure.cpp
|
||||
@@ -126,6 +128,9 @@ target_compile_options(
|
||||
PRIVATE
|
||||
$<$<CXX_COMPILER_ID:MSVC>:/W4 /WX /EHsc>
|
||||
)
|
||||
if(FESA_HHT_DEVELOPMENT_PROBE)
|
||||
target_compile_definitions(fesa_solver PRIVATE FESA_HHT_DEVELOPMENT_PROBE=1)
|
||||
endif()
|
||||
|
||||
# Keep the CPU/OS dispatch on the baseline ISA; only its private kernel uses AVX2.
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND
|
||||
@@ -154,11 +159,10 @@ target_link_options(fesa_cli PRIVATE
|
||||
unset(_fesa_target)
|
||||
|
||||
if(WIN32)
|
||||
if(NOT TARGET MKL::mkl_intel_thread OR NOT TARGET MKL::mkl_core OR
|
||||
NOT OMP_DLL_DIR OR NOT OMP_DLLNAME)
|
||||
if(NOT TARGET ${FESA_MKL_THREAD_TARGET} OR NOT TARGET MKL::mkl_core)
|
||||
message(FATAL_ERROR "The oneMKL runtime files required by fesa.exe were not resolved")
|
||||
endif()
|
||||
if(NOT EXISTS "${OMP_DLL_DIR}/libmmd.dll")
|
||||
if(NOT EXISTS "${FESA_INTEL_MATH_RUNTIME}")
|
||||
message(FATAL_ERROR "The Intel runtime required by the selected HDF5 DLL was not resolved")
|
||||
endif()
|
||||
|
||||
@@ -177,16 +181,13 @@ if(WIN32)
|
||||
"$<TARGET_FILE:TBB::tbb>"
|
||||
"$<TARGET_FILE_DIR:fesa_cli>"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"$<TARGET_FILE:MKL::mkl_intel_thread>"
|
||||
"$<TARGET_FILE:${FESA_MKL_THREAD_TARGET}>"
|
||||
"$<TARGET_FILE_DIR:fesa_cli>"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"$<TARGET_FILE:MKL::mkl_core>"
|
||||
"$<TARGET_FILE_DIR:fesa_cli>"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${OMP_DLL_DIR}/${OMP_DLLNAME}"
|
||||
"$<TARGET_FILE_DIR:fesa_cli>"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${OMP_DLL_DIR}/libmmd.dll"
|
||||
"${FESA_INTEL_MATH_RUNTIME}"
|
||||
"$<TARGET_FILE_DIR:fesa_cli>"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${_fesa_cli_mkl_dispatch_dll}"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "fesa/analysis/analysis_state.h"
|
||||
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
@@ -167,9 +169,18 @@ Status AnalysisState::CommitShellResults(
|
||||
"Shell energy, equilibrium, and normalized metrics must be finite.");
|
||||
}
|
||||
|
||||
if (!results_internal::ValidDrillingEvidence(candidate.mitc4_drilling,
|
||||
candidate.equilibrium) ||
|
||||
(mitc4_drilling_ && !results_internal::SameDrillingInventory(
|
||||
mitc4_drilling_, candidate.mitc4_drilling)))
|
||||
return ShellCandidateFailure("invalid-shell-drilling-evidence",
|
||||
identity_.step_name,
|
||||
"Drilling evidence must be finite, consistent, and retain its mode.");
|
||||
|
||||
shell_results_ = std::move(candidate.rows);
|
||||
physical_strain_energy_ = candidate.physical_strain_energy;
|
||||
equilibrium_ = candidate.equilibrium;
|
||||
mitc4_drilling_ = candidate.mitc4_drilling;
|
||||
verification_metrics_ = candidate.verification_metrics;
|
||||
return Status::Ok();
|
||||
}
|
||||
@@ -187,6 +198,11 @@ const std::array<double, 6>& AnalysisState::Equilibrium() const noexcept {
|
||||
return equilibrium_;
|
||||
}
|
||||
|
||||
const std::optional<Mitc4DrillingEvidence>& AnalysisState::Mitc4Drilling()
|
||||
const noexcept {
|
||||
return mitc4_drilling_;
|
||||
}
|
||||
|
||||
const std::array<double, 3>& AnalysisState::VerificationMetrics()
|
||||
const noexcept {
|
||||
return verification_metrics_;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "fesa/analysis/dynamic_analysis_state.h"
|
||||
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
@@ -273,6 +275,12 @@ Status DynamicAnalysisState::CommitAcceptedFrame(
|
||||
return Failure(FailureCategory::kSolver,
|
||||
"Frame physical evidence is nonfinite or has invalid size.");
|
||||
}
|
||||
if (!results_internal::ValidDrillingEvidence(frame.mitc4_drilling,
|
||||
frame.global_equilibrium) ||
|
||||
(!accepted_frames_.empty() && !results_internal::SameDrillingInventory(
|
||||
frame.mitc4_drilling, accepted_frames_.back().mitc4_drilling)))
|
||||
return Failure(FailureCategory::kSolver,
|
||||
"Accepted drilling evidence must be finite, consistent, and retain its mode.");
|
||||
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 ||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#ifndef FESA_ANALYSIS_HHT_DEVELOPMENT_PROBE_H_
|
||||
#define FESA_ANALYSIS_HHT_DEVELOPMENT_PROBE_H_
|
||||
|
||||
// This header is compiled out of normal builds, including environment/file I/O.
|
||||
#if defined(FESA_HHT_DEVELOPMENT_PROBE)
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/solvers/linear/linear_solver.h"
|
||||
#include "fesa/solvers/linear/tangent_solver.h"
|
||||
|
||||
namespace fesa::hht_development_probe {
|
||||
enum Stage { kProcedure, kInner, kAssembly, kOuterSolve, kRates,
|
||||
kFactor, kSolve, kStageCount };
|
||||
enum Counter { kOuterCorrection, kInnerCorrection, kInternalEvaluation,
|
||||
kInertiaEvaluation, kFactorCall, kSolveCall, kRhs,
|
||||
kConstraintInternalEvaluation, kInternalFailure, kCounterCount };
|
||||
struct Statistics {
|
||||
std::array<std::atomic<std::uint64_t>, kCounterCount> counts{};
|
||||
// Timers/stage attribution are serial procedure-thread data. Only the three
|
||||
// element counters below and internal failures may be touched by workers.
|
||||
std::array<std::int64_t, kStageCount> inclusive_ns{};
|
||||
std::array<std::int64_t, kStageCount> exclusive_ns{};
|
||||
std::array<std::uint64_t, kStageCount> stage_calls{};
|
||||
std::array<std::array<std::uint64_t, kCounterCount>, kStageCount> by_stage{};
|
||||
};
|
||||
inline Statistics* active = nullptr;
|
||||
class Scope {
|
||||
public:
|
||||
explicit Scope(Stage stage) : stage_(stage), parent_(current_),
|
||||
start_(std::chrono::steady_clock::now()) {
|
||||
current_ = this;
|
||||
if (active != nullptr) ++active->stage_calls[stage_];
|
||||
}
|
||||
~Scope() {
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now() - start_).count();
|
||||
if (active != nullptr) {
|
||||
active->inclusive_ns[stage_] += elapsed;
|
||||
active->exclusive_ns[stage_] += elapsed - children_;
|
||||
}
|
||||
if (parent_ != nullptr) parent_->children_ += elapsed;
|
||||
current_ = parent_;
|
||||
}
|
||||
static Stage Current() { return current_ == nullptr ? kProcedure : current_->stage_; }
|
||||
private:
|
||||
Stage stage_;
|
||||
Scope* parent_;
|
||||
std::chrono::steady_clock::time_point start_;
|
||||
std::int64_t children_ = 0;
|
||||
inline static thread_local Scope* current_ = nullptr;
|
||||
};
|
||||
inline void Count(Counter counter, std::uint64_t count = 1U) {
|
||||
if (active == nullptr) return;
|
||||
active->counts[counter].fetch_add(count, std::memory_order_relaxed);
|
||||
// Element callbacks are parallel; their totals use only the atomics above.
|
||||
if (counter != kInternalEvaluation && counter != kInertiaEvaluation &&
|
||||
counter != kConstraintInternalEvaluation && counter != kInternalFailure)
|
||||
active->by_stage[Scope::Current()][counter] += count;
|
||||
}
|
||||
class Session {
|
||||
public:
|
||||
Session() : previous_(active) { active = &statistics_; }
|
||||
~Session() {
|
||||
active = previous_;
|
||||
#if defined(_WIN32)
|
||||
char* environment = nullptr;
|
||||
std::size_t size = 0U;
|
||||
if (_dupenv_s(&environment, &size, "FESA_HHT_PROBE_OUTPUT") != 0) return;
|
||||
const std::string path = environment == nullptr ? "" : environment;
|
||||
std::free(environment);
|
||||
#else
|
||||
const char* environment = std::getenv("FESA_HHT_PROBE_OUTPUT");
|
||||
const std::string path = environment == nullptr ? "" : environment;
|
||||
#endif
|
||||
if (path.empty()) return;
|
||||
std::ofstream output(path, std::ios::out | std::ios::binary);
|
||||
constexpr const char* counters[] = {"outer_applied_correction", "inner_applied_correction",
|
||||
"assembly_internal_evaluation", "assembly_inertia_evaluation",
|
||||
"logical_factor_call", "logical_solve_call", "logical_rhs",
|
||||
"constraint_internal_evaluation", "assembly_internal_failure"};
|
||||
constexpr const char* stages[] = {"procedure_other", "inner_graph", "assembly",
|
||||
"outer_linear", "physical_rates", "factor", "solve"};
|
||||
for (int c = 0; c < kCounterCount; ++c)
|
||||
output << counters[c] << '=' << statistics_.counts[c].load() << '\n';
|
||||
output << "procedure_inclusive_ns=" << statistics_.inclusive_ns[kProcedure] << '\n';
|
||||
for (int s = 0; s < kStageCount; ++s) {
|
||||
output << stages[s] << "_calls=" << statistics_.stage_calls[s] << '\n';
|
||||
output << stages[s] << "_exclusive_ns=" << statistics_.exclusive_ns[s] << '\n';
|
||||
for (int c = 0; c < kCounterCount; ++c)
|
||||
if (statistics_.by_stage[s][c] != 0U)
|
||||
output << stages[s] << '_' << counters[c] << '='
|
||||
<< statistics_.by_stage[s][c] << '\n';
|
||||
}
|
||||
}
|
||||
private:
|
||||
Statistics statistics_;
|
||||
Statistics* previous_;
|
||||
};
|
||||
class PositiveSolver final : public LinearSolver {
|
||||
public:
|
||||
explicit PositiveSolver(LinearSolver& solver) : solver_(solver) {}
|
||||
Status Factorize(const SparseMatrix& matrix) override {
|
||||
Count(kFactorCall); Scope scope{kFactor}; return solver_.Factorize(matrix);
|
||||
}
|
||||
Status Solve(const Vector& rhs, Vector& solution) const override {
|
||||
Count(kSolveCall); Count(kRhs); Scope scope{kSolve};
|
||||
return solver_.Solve(rhs, solution);
|
||||
}
|
||||
private:
|
||||
LinearSolver& solver_;
|
||||
};
|
||||
class CorrectionSolver final : public TangentSolver {
|
||||
public:
|
||||
explicit CorrectionSolver(TangentSolver& solver) : solver_(solver) {}
|
||||
Status FactorizeFresh(const SparseMatrix& matrix) override {
|
||||
Count(kFactorCall); Scope scope{kFactor}; return solver_.FactorizeFresh(matrix);
|
||||
}
|
||||
Status SolveCorrection(const Vector& rhs, Vector& solution) const override {
|
||||
Count(kSolveCall); Count(kRhs); Scope scope{kSolve};
|
||||
return solver_.SolveCorrection(rhs, solution);
|
||||
}
|
||||
private:
|
||||
TangentSolver& solver_;
|
||||
};
|
||||
} // namespace fesa::hht_development_probe
|
||||
#define FESA_HHT_PROBE_COUNT(counter) \
|
||||
::fesa::hht_development_probe::Count(::fesa::hht_development_probe::counter)
|
||||
#define FESA_HHT_PROBE_SCOPE(stage) \
|
||||
::fesa::hht_development_probe::Scope probe_scope{ \
|
||||
::fesa::hht_development_probe::stage}
|
||||
#else
|
||||
#define FESA_HHT_PROBE_COUNT(counter) ((void)0)
|
||||
#define FESA_HHT_PROBE_SCOPE(stage) ((void)0)
|
||||
#endif
|
||||
#endif // FESA_ANALYSIS_HHT_DEVELOPMENT_PROBE_H_
|
||||
@@ -547,6 +547,18 @@ Status HiddenEvidence(const Domain& domain, const DynamicAssembly& assembly,
|
||||
const SparseMatrix& gradient,
|
||||
DynamicAcceptedFrameState* frame,
|
||||
const bool require_acceleration) {
|
||||
const bool direct = frame->mitc4_drilling &&
|
||||
frame->mitc4_drilling->mode == Mitc4DrillingMode::kDirectSpring;
|
||||
if (direct) {
|
||||
for (const std::size_t node : nodes) {
|
||||
if (node >= assembly.hidden_constraint.Size())
|
||||
return Failure(domain, "dynamic-invalid-algebra-state",
|
||||
"Canonical hidden values must cover every admissible null node.");
|
||||
if (!std::isfinite(assembly.hidden_constraint[node]))
|
||||
return Failure(domain, "dynamic-invalid-algebra-state",
|
||||
"Canonical hidden values must remain finite.");
|
||||
}
|
||||
}
|
||||
Vector psi{nodes.size()};
|
||||
Vector psi_scale{nodes.size()};
|
||||
Vector velocity_defect{nodes.size()};
|
||||
@@ -575,9 +587,10 @@ Status HiddenEvidence(const Domain& domain, const DynamicAssembly& assembly,
|
||||
const double g =
|
||||
frame->internal_force[full] - frame->external_force[full];
|
||||
const double n = rotation.drill_null[c];
|
||||
psi[row] += n * g;
|
||||
if (!direct) psi[row] += n * g;
|
||||
psi_scale[row] += std::abs(n) * frame->residual_scale[full];
|
||||
}
|
||||
if (direct) psi[row] = assembly.hidden_constraint[node];
|
||||
velocity_defect[row] += velocity_correction;
|
||||
AddCompensated(assembly.hidden_second[node], &acceleration_defect[row],
|
||||
&acceleration_correction);
|
||||
|
||||
@@ -12,11 +12,13 @@
|
||||
#endif
|
||||
|
||||
#include "analysis/hht_dynamic_internal.h"
|
||||
#include "analysis/hht_development_probe.h"
|
||||
#include "math/dense_blas_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"
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
|
||||
namespace fesa::hht_internal {
|
||||
|
||||
@@ -190,8 +192,34 @@ Result<double> ReferenceLength(const Domain& domain) {
|
||||
return Result<double>::Success(length);
|
||||
}
|
||||
|
||||
Result<std::array<hht_field_convergence::FieldInput, 2>> MeasureEndpointFields(
|
||||
const DofManager& dofs, const DynamicAcceptedFrameState& trial,
|
||||
const DynamicAcceptedFrameState& accepted, const std::optional<Vector>& previous) {
|
||||
using Fields = std::array<hht_field_convergence::FieldInput, 2>;
|
||||
const auto count = dofs.FullDofCount();
|
||||
if (trial.generalized_coordinates.Size() != count || trial.residual.Size() != count ||
|
||||
accepted.generalized_coordinates.Size() != count || (previous && previous->Size() != count))
|
||||
return Result<Fields>::Failure(Failure("Invalid full endpoint field inventory."));
|
||||
Fields fields;
|
||||
for (auto& field : fields) if (previous) field.correction = 0.0;
|
||||
for (const auto full : dofs.FreeDofs()) {
|
||||
auto& field = fields[full % 6U < 3U ? 0U : 1U];
|
||||
const double residual = std::abs(trial.residual[full]);
|
||||
const double increment = std::abs(trial.generalized_coordinates[full] - accepted.generalized_coordinates[full]);
|
||||
const double correction = previous ? std::abs(trial.generalized_coordinates[full] - (*previous)[full]) : 0.0;
|
||||
if (!std::isfinite(residual) || !std::isfinite(increment) || !std::isfinite(correction))
|
||||
return Result<Fields>::Failure(Failure("Nonfinite full endpoint field arithmetic."));
|
||||
field.active = true;
|
||||
field.residual = (std::max)(field.residual, residual);
|
||||
field.increment = (std::max)(field.increment, increment);
|
||||
if (previous) field.correction = (std::max)(*field.correction, correction);
|
||||
}
|
||||
return Result<Fields>::Success(std::move(fields));
|
||||
}
|
||||
|
||||
Result<std::array<Vector, 2>> AssembleLoads(const Domain& domain,
|
||||
const DofManager& dofs) {
|
||||
const DofManager& dofs,
|
||||
std::array<std::vector<hht_field_convergence::FluxOccurrence>, 2>* field_flux) {
|
||||
const auto index = SourceTargetIndex::FromDomain(domain);
|
||||
const SourceTargetResolver resolver(index);
|
||||
const LoadContext context{domain, dofs, resolver};
|
||||
@@ -202,6 +230,9 @@ Result<std::array<Vector, 2>> AssembleLoads(const Domain& domain,
|
||||
if (!contributions.HasValue())
|
||||
return Result<std::array<Vector, 2>>::Failure(contributions.GetStatus());
|
||||
for (const auto& entry : contributions.Value()) {
|
||||
if (field_flux != nullptr)
|
||||
(*field_flux)[entry.full_dof_index % 6U < 3U ? 0U : 1U].push_back(
|
||||
{entry.value, 0.0});
|
||||
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]) ||
|
||||
@@ -382,6 +413,7 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
const Vector& load_scale, const double time_increment,
|
||||
const HhtParameters& parameters, const ParallelFor& parallel_for,
|
||||
const bool directional_third) {
|
||||
FESA_HHT_PROBE_SCOPE(kAssembly);
|
||||
const std::size_t count = dofs.FullDofCount();
|
||||
for (const auto* values : std::array<const Vector*, 6>{
|
||||
&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
|
||||
@@ -446,9 +478,11 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
}
|
||||
const auto before = retain_constraints ? ReadTrialEvaluationContext()
|
||||
: TrialEvaluationContext{};
|
||||
FESA_HHT_PROBE_COUNT(kInternalEvaluation);
|
||||
auto f = shells[e].internal->EvaluateTrial(
|
||||
trial, directional_third ? &v : nullptr);
|
||||
if (!f.HasValue()) {
|
||||
FESA_HHT_PROBE_COUNT(kInternalFailure);
|
||||
failures[e] = f.GetStatus();
|
||||
return;
|
||||
}
|
||||
@@ -456,6 +490,7 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
const auto after = ReadTrialEvaluationContext();
|
||||
if (ReusableTrialContext(before, after)) evaluation_contexts[e] = after;
|
||||
}
|
||||
FESA_HHT_PROBE_COUNT(kInertiaEvaluation);
|
||||
auto m = shells[e].inertia->Evaluate(trial, v, a);
|
||||
if (!m.HasValue()) {
|
||||
failures[e] = m.GetStatus();
|
||||
@@ -464,6 +499,10 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
internal[e] = std::move(f.Value());
|
||||
inertia[e] = std::move(m.Value());
|
||||
});
|
||||
if (shells.empty())
|
||||
return Result<DynamicAssembly>::Failure(Failure("HHT requires a shell kernel."));
|
||||
frame.mitc4_drilling =
|
||||
Mitc4DrillingEvidence{shells.front().internal_kernel->DrillingMode()};
|
||||
frame.inertia_force = Vector(count);
|
||||
frame.internal_force = Vector(count);
|
||||
frame.residual = Vector(count);
|
||||
@@ -481,9 +520,11 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
std::vector<std::map<std::size_t, double>>
|
||||
constraint_gradients(domain.Nodes().size());
|
||||
std::vector<bool> external_done(domain.Nodes().size(), false);
|
||||
Vector hidden_constraint(directional_third ? domain.Nodes().size() : 0U);
|
||||
Vector third(count);
|
||||
Vector third_scale(count);
|
||||
std::array<std::vector<CooContribution>, 6> entries;
|
||||
std::array<std::vector<hht_field_convergence::FluxOccurrence>, 2> field_flux;
|
||||
for (auto& values : entries) values.reserve(shells.size() * 24U * 24U);
|
||||
double momentum_scale = 0.0;
|
||||
double angular_scale = 0.0;
|
||||
@@ -497,9 +538,31 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
return Result<DynamicAssembly>::Failure(*failures[e]);
|
||||
const auto& f = *internal[e];
|
||||
const auto& m = *inertia[e];
|
||||
if (shells[e].internal_kernel->DrillingMode() != frame.mitc4_drilling->mode ||
|
||||
f.drilling_force.Size() != 24U)
|
||||
return Result<DynamicAssembly>::Failure(Failure("Invalid actual drilling mode or force."));
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
const auto local = 6U * node;
|
||||
const auto global = shells[e].scatter[local];
|
||||
const Vector3 force = Triple(f.drilling_force, local);
|
||||
const auto spatial = SpatialMomentSO3(
|
||||
Triple(frame.generalized_coordinates, global + 3U),
|
||||
Triple(f.drilling_force, local + 3U));
|
||||
if (!spatial.HasValue())
|
||||
return Result<DynamicAssembly>::Failure(spatial.GetStatus());
|
||||
const Vector3 position = Vector3(domain.Nodes()[global / 6U].coordinates) +
|
||||
Triple(frame.generalized_coordinates, global);
|
||||
const Vector3 moment = position.Cross(force) + spatial.Value();
|
||||
for (std::size_t c = 0U; c < 3U; ++c)
|
||||
frame.mitc4_drilling->drilling_moment[c] += moment[c];
|
||||
}
|
||||
for (const double value : frame.mitc4_drilling->drilling_moment)
|
||||
if (!std::isfinite(value))
|
||||
return Result<DynamicAssembly>::Failure(Failure("Drilling spatial moment overflowed."));
|
||||
if (directional_third) {
|
||||
for (std::size_t local = 0U; local < 4U; ++local) {
|
||||
const auto node = shells[e].scatter[6U * local] / 6U;
|
||||
hidden_constraint[node] += f.drilling_constraint[local];
|
||||
auto& gradient = constraint_gradients[node];
|
||||
auto& hessian = constraint_hessians[node];
|
||||
for (std::size_t i = 0U; i < 24U; ++i)
|
||||
@@ -520,6 +583,7 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
Triple(frame.external_force, first), {}, {});
|
||||
if (!external.HasValue())
|
||||
return Result<DynamicAssembly>::Failure(external.GetStatus());
|
||||
hidden_constraint[node] += external.Value().value;
|
||||
for (std::size_t i = 0U; i < 3U; ++i) {
|
||||
gradient[first + i] += external.Value().gradient[i];
|
||||
for (std::size_t j = 0U; j < 3U; ++j)
|
||||
@@ -541,6 +605,8 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
const Vector mass_action = EntryProducts(mass_values, acceleration_values);
|
||||
for (std::size_t row = 0U; row < 24U; ++row) {
|
||||
const auto global = shells[e].scatter[row];
|
||||
field_flux[row % 6U < 3U ? 0U : 1U].push_back(
|
||||
{f.internal_force[row], m.force[row]});
|
||||
frame.internal_force[global] += f.internal_force[row];
|
||||
frame.inertia_force[global] += m.force[row];
|
||||
third[global] += f.force_directional_second[row];
|
||||
@@ -703,7 +769,8 @@ Result<DynamicAssembly> EvaluateAssembly(
|
||||
std::move(third), std::move(third_scale), std::move(rotations),
|
||||
momentum_scale, angular_scale, std::move(hidden_second),
|
||||
std::move(hidden_second_scale), std::move(hidden_gradient),
|
||||
std::move(trial_constraints)});
|
||||
std::move(trial_constraints), std::move(field_flux),
|
||||
std::move(hidden_constraint)});
|
||||
}
|
||||
|
||||
Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs,
|
||||
@@ -739,10 +806,20 @@ Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs,
|
||||
frame->global_equilibrium[c] = force_error[c];
|
||||
frame->global_equilibrium[3U + c] = moment_error[c];
|
||||
}
|
||||
Vector3 acceptance_moment = moment_error;
|
||||
if (frame->mitc4_drilling) {
|
||||
results_internal::CompleteDrillingEvidence(frame->global_equilibrium,
|
||||
&*frame->mitc4_drilling);
|
||||
if (!results_internal::ValidDrillingEvidence(frame->mitc4_drilling,
|
||||
frame->global_equilibrium))
|
||||
return Failure("Invalid dynamic drilling equilibrium evidence.");
|
||||
if (frame->mitc4_drilling->mode == Mitc4DrillingMode::kDirectSpring)
|
||||
acceptance_moment = moment_error - Vector3(frame->mitc4_drilling->drilling_moment);
|
||||
}
|
||||
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)
|
||||
Ratio(acceptance_moment.Norm(), moment_scale) > 1e-8)
|
||||
return Failure("Dynamic physical force or moment balance exceeds 1e-8.");
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "fesa/math/sparse_matrix.h"
|
||||
#include "elements/nonlinear_mitc4_configuration_internal.h"
|
||||
#include "math/dense_blas_internal.h"
|
||||
#include "analysis/hht_field_convergence.h"
|
||||
|
||||
namespace fesa {
|
||||
class LinearSolver;
|
||||
@@ -95,6 +96,10 @@ struct DynamicAssembly {
|
||||
// Canonical scalar drilling-constraint derivative, indexed by global node.
|
||||
std::optional<SparseMatrix> hidden_gradient;
|
||||
std::optional<TrialConstraintData> trial_constraints;
|
||||
std::array<std::vector<hht_field_convergence::FluxOccurrence>, 2> field_flux;
|
||||
// Source-ordered scalar primitive, including the external term exactly once.
|
||||
// Empty when directional primitives were not evaluated; never a zero fallback.
|
||||
Vector hidden_constraint{0U};
|
||||
};
|
||||
|
||||
/// @brief Builds checked shell evaluators in stable source order.
|
||||
@@ -104,9 +109,17 @@ Result<std::vector<RuntimeShell>> BuildRuntimeShells(const Domain& domain,
|
||||
/// @brief Returns the maximum initial active-element edge length.
|
||||
Result<double> ReferenceLength(const Domain& domain);
|
||||
|
||||
/// @brief Measures free full endpoint residuals and actual u/total-phi changes.
|
||||
/// @note Previous is the selected trial in this increment, not a tangent lift.
|
||||
Result<std::array<hht_field_convergence::FieldInput, 2>> MeasureEndpointFields(
|
||||
const DofManager& dofs, const DynamicAcceptedFrameState& trial,
|
||||
const DynamicAcceptedFrameState& accepted,
|
||||
const std::optional<Vector>& previous);
|
||||
|
||||
/// @brief Assembles dead loads and their uncancelled absolute row scales.
|
||||
Result<std::array<Vector, 2>> AssembleLoads(const Domain& domain,
|
||||
const DofManager& dofs);
|
||||
const DofManager& dofs,
|
||||
std::array<std::vector<hht_field_convergence::FluxOccurrence>, 2>* field_flux = nullptr);
|
||||
|
||||
/// @brief Evaluates current kinetic and internal quantities without commit.
|
||||
Result<DynamicAssembly> EvaluateAssembly(
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
#include "analysis/hht_field_convergence.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace fesa::hht_field_convergence {
|
||||
namespace {
|
||||
constexpr double kRn = .005, kCn = .01, kRp = .02, kEpsilon = 1e-5;
|
||||
constexpr double kZeroCorrection = .001, kLinear = 1e-8;
|
||||
constexpr double kInactive = 1e-5, kSmallTranslation = 1e-8;
|
||||
bool Nonnegative(double x) { return std::isfinite(x) && x >= 0; }
|
||||
bool ValidOptional(const std::optional<double>& x) {
|
||||
return !x || Nonnegative(*x);
|
||||
}
|
||||
bool ValidHistory(const History& h) {
|
||||
return Nonnegative(h.nonzero_sum) && Nonnegative(h.maximum_sum) &&
|
||||
h.nonzero_count <= h.maximum_count &&
|
||||
(h.nonzero_count != 0 || h.nonzero_sum == 0) &&
|
||||
(h.maximum_count != 0 || h.maximum_sum == 0);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Evaluation Evaluate(const std::array<FieldInput, 2>& fields,
|
||||
const std::array<History, 2>& committed,
|
||||
std::size_t iteration, double reference_length) {
|
||||
Evaluation result;
|
||||
result.status = Status::Failure(FailureCategory::kSolver,
|
||||
{{Severity::kError, "invalid-state", {}, {}, {}, "Invalid field convergence arithmetic or input."}});
|
||||
result.pending = committed;
|
||||
if (!std::isfinite(reference_length) || reference_length <= 0) return result;
|
||||
for (std::size_t a = 0; a < 2; ++a) {
|
||||
const auto& f = fields[a];
|
||||
const auto& h = committed[a];
|
||||
auto& out = result.fields[a];
|
||||
if (!ValidHistory(h) || !Nonnegative(f.residual) || !Nonnegative(f.increment) ||
|
||||
!ValidOptional(f.correction) || !ValidOptional(f.previous_residual) ||
|
||||
!ValidOptional(f.older_residual)) return result;
|
||||
if (f.flux.empty()) {
|
||||
if (f.active) { result.status = Status::Failure(FailureCategory::kSolver,
|
||||
{{Severity::kError, "invalid-flux-population", {}, {}, {}, "Empty active flux population."}}); return result; }
|
||||
continue;
|
||||
}
|
||||
const double historic_maximum = h.maximum_count == 0 ? 0 :
|
||||
h.maximum_sum / static_cast<double>(h.maximum_count);
|
||||
const double cutoff = kInactive * historic_maximum;
|
||||
double maximum = 0;
|
||||
for (const auto& x : f.flux) {
|
||||
if (!std::isfinite(x.internal) || !std::isfinite(x.inertia)) return result;
|
||||
const double total = x.internal + x.inertia;
|
||||
if (!std::isfinite(total)) return result;
|
||||
maximum = std::max(maximum, std::abs(total));
|
||||
}
|
||||
out.maximum_flux = maximum;
|
||||
const bool filter = maximum >= .1 * historic_maximum;
|
||||
double sum = 0;
|
||||
std::size_t count = 0;
|
||||
for (const auto& x : f.flux) {
|
||||
const double b = std::abs(x.internal + x.inertia);
|
||||
if (!filter || b >= cutoff) {
|
||||
sum += b;
|
||||
if (!std::isfinite(sum)) return result;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
if (count == 0) { result.status = Status::Failure(FailureCategory::kSolver,
|
||||
{{Severity::kError, "invalid-flux-population", {}, {}, {}, "Empty active flux population."}}); return result; }
|
||||
const double spatial = sum / static_cast<double>(count);
|
||||
out.spatial_flux = spatial;
|
||||
if (h.nonzero_count == std::numeric_limits<std::uint64_t>::max() ||
|
||||
h.maximum_count == std::numeric_limits<std::uint64_t>::max()) return result;
|
||||
const double base = h.nonzero_count == 0 ? .01 :
|
||||
h.nonzero_sum / static_cast<double>(h.nonzero_count);
|
||||
const double next_sum = h.nonzero_sum + spatial;
|
||||
const double next_maximum = h.maximum_sum + maximum;
|
||||
if (!Nonnegative(next_sum) || !Nonnegative(next_maximum)) return result;
|
||||
const double included = next_sum / static_cast<double>(h.nonzero_count + 1);
|
||||
const bool eligible = spatial > kEpsilon * (h.nonzero_count == 0 ? base : included);
|
||||
const double raw = eligible ? included : base;
|
||||
if (!std::isfinite(raw) || raw <= 0) return result;
|
||||
out.time_average_flux = raw;
|
||||
out.residual_scale = raw;
|
||||
auto& pending = result.pending[a];
|
||||
if (eligible) { pending.nonzero_sum = next_sum; ++pending.nonzero_count; }
|
||||
pending.maximum_sum = next_maximum;
|
||||
++pending.maximum_count;
|
||||
}
|
||||
// Both conversions read raw means, never a converted peer scale.
|
||||
if (fields[0].active && fields[1].active) {
|
||||
const double force = *result.fields[0].time_average_flux;
|
||||
const double moment = *result.fields[1].time_average_flux;
|
||||
const std::array<double, 2> converted{moment / reference_length, reference_length * force};
|
||||
for (std::size_t a = 0; a < 2; ++a) {
|
||||
if (!Nonnegative(converted[a])) return result;
|
||||
auto& out = result.fields[a];
|
||||
if (*out.spatial_flux <= *out.time_average_flux &&
|
||||
*out.time_average_flux < converted[a]) out.residual_scale = converted[a];
|
||||
}
|
||||
}
|
||||
bool all_linear = iteration >= 1;
|
||||
for (std::size_t a = 0; a < 2; ++a) {
|
||||
if (!fields[a].active) continue;
|
||||
const double scale = *result.fields[a].residual_scale;
|
||||
if (!std::isfinite(scale) || scale <= 0) return result;
|
||||
const double ratio = fields[a].residual / scale;
|
||||
if (!Nonnegative(ratio)) return result;
|
||||
all_linear = all_linear && ratio <= kLinear;
|
||||
}
|
||||
bool all_pass = true;
|
||||
for (std::size_t a = 0; a < 2; ++a) {
|
||||
const auto& f = fields[a];
|
||||
if (!f.active) continue;
|
||||
auto& out = result.fields[a];
|
||||
const double scale = *out.residual_scale;
|
||||
const double ratio = f.residual / scale;
|
||||
if (all_linear) {
|
||||
out.branch = Branch::kLinear;
|
||||
out.residual_threshold = kLinear;
|
||||
out.residual_pass = true; out.field_pass = true;
|
||||
continue;
|
||||
}
|
||||
const bool zero = *out.spatial_flux <= kEpsilon * scale;
|
||||
bool nonquadratic = false;
|
||||
if (!zero && iteration > 9 && f.previous_residual && f.older_residual &&
|
||||
*f.older_residual > *f.previous_residual && *f.previous_residual > f.residual && f.residual > 0) {
|
||||
const double recent = f.residual / *f.previous_residual;
|
||||
const double older = *f.previous_residual / *f.older_residual;
|
||||
if (!Nonnegative(recent) || !Nonnegative(older)) return result;
|
||||
nonquadratic = recent >= older;
|
||||
}
|
||||
out.branch = zero ? Branch::kZeroFlux :
|
||||
(nonquadratic ? Branch::kNonquadratic : Branch::kNormal);
|
||||
const double residual_threshold = zero ? kEpsilon : (nonquadratic ? kRp : kRn);
|
||||
out.residual_threshold = residual_threshold;
|
||||
out.residual_pass = ratio <= residual_threshold;
|
||||
if (zero && *out.residual_pass) { out.field_pass = true; continue; }
|
||||
if (!zero && iteration >= 1 && a == 0 && f.increment < kSmallTranslation * reference_length) {
|
||||
out.field_pass = out.residual_pass;
|
||||
} else {
|
||||
const double correction_threshold = zero ? kZeroCorrection : kCn;
|
||||
out.correction_threshold = correction_threshold;
|
||||
const double bound = correction_threshold * f.increment;
|
||||
if (!Nonnegative(bound)) return result;
|
||||
if (iteration >= 1 && f.correction) out.correction_pass = *f.correction <= bound;
|
||||
if (!zero && !nonquadratic && iteration >= 2 && f.correction &&
|
||||
f.previous_residual && f.older_residual) {
|
||||
const double denominator = std::min(*f.previous_residual, *f.older_residual);
|
||||
if (denominator > 0) {
|
||||
const double quotient = f.residual / denominator;
|
||||
if (!Nonnegative(quotient)) return result;
|
||||
const double estimate = quotient * *f.correction;
|
||||
if (!Nonnegative(estimate)) return result;
|
||||
out.estimated_correction = estimate;
|
||||
out.correction_pass = out.correction_pass.value_or(false) || estimate <= bound;
|
||||
}
|
||||
}
|
||||
out.field_pass = zero ? out.correction_pass.value_or(false) :
|
||||
(*out.residual_pass && out.correction_pass.value_or(false));
|
||||
}
|
||||
all_pass = all_pass && *out.field_pass;
|
||||
}
|
||||
result.field_pass = all_pass;
|
||||
result.status = Status::Ok();
|
||||
return result;
|
||||
}
|
||||
} // namespace fesa::hht_field_convergence
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_
|
||||
#define FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
|
||||
namespace fesa::hht_field_convergence {
|
||||
inline constexpr const char* kPolicyId = "abaqus2025-field-on-fesa-v1";
|
||||
// Ordered element rows first, then external source entries (including explicit zero).
|
||||
// External entries use internal=value, inertia=0. No assembled cancellation here.
|
||||
struct FluxOccurrence { double internal = 0.0; double inertia = 0.0; };
|
||||
struct History {
|
||||
double nonzero_sum = 0.0;
|
||||
std::uint64_t nonzero_count = 0;
|
||||
double maximum_sum = 0.0;
|
||||
std::uint64_t maximum_count = 0;
|
||||
};
|
||||
struct FieldInput {
|
||||
bool active = false; // At least one free full DOF of this field.
|
||||
std::vector<FluxOccurrence> flux;
|
||||
double residual = 0.0; // max |I+fint-F| on full free endpoint DOFs, fi=1.
|
||||
double increment = 0.0; // max |q_trial-q_last_accepted| in fixed full u/phi chart.
|
||||
std::optional<double> correction; // max |q_trial-q_previous_selected|, i>=1.
|
||||
std::optional<double> previous_residual;
|
||||
std::optional<double> older_residual;
|
||||
};
|
||||
enum class Branch { kNotEvaluated, kLinear, kZeroFlux, kNonquadratic, kNormal };
|
||||
struct FieldResult {
|
||||
std::optional<double> spatial_flux, maximum_flux, time_average_flux, residual_scale;
|
||||
std::optional<double> estimated_correction, residual_threshold, correction_threshold;
|
||||
std::optional<bool> residual_pass, correction_pass, field_pass;
|
||||
Branch branch = Branch::kNotEvaluated;
|
||||
};
|
||||
struct Evaluation {
|
||||
Status status = Status::Failure(FailureCategory::kSolver, {});
|
||||
bool field_pass = false;
|
||||
std::array<FieldResult, 2> fields;
|
||||
// Pure pending snapshot: procedure owns exactly-once commit after every gate/output.
|
||||
std::array<History, 2> pending;
|
||||
};
|
||||
// Index 0: force/translation; index 1: generalized moment/total rotation phi.
|
||||
Evaluation Evaluate(const std::array<FieldInput, 2>& fields,
|
||||
const std::array<History, 2>& committed,
|
||||
std::size_t iteration, double reference_length);
|
||||
} // namespace fesa::hht_field_convergence
|
||||
|
||||
#endif // FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "analysis/hht_reduced_internal.h"
|
||||
#include "analysis/hht_development_probe.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -81,7 +82,8 @@ const hht_internal::DirectionalShellConstraintData* MatchingConstraints(
|
||||
nonlinear_mitc4_internal::ConfigurationSnapshot::Read(*shell.internal_kernel);
|
||||
if (record.configuration.source.file != configuration.source.file ||
|
||||
record.configuration.source.line != configuration.source.line ||
|
||||
record.configuration.identity != configuration.identity)
|
||||
record.configuration.identity != configuration.identity ||
|
||||
record.configuration.drilling_mode != configuration.drilling_mode)
|
||||
return nullptr;
|
||||
for (std::size_t i = 0U; i < configuration.values.size(); ++i)
|
||||
if (!std::isfinite(record.configuration.values[i]) ||
|
||||
@@ -513,6 +515,7 @@ Result<ConstraintOperators> EvaluateConstraintOperators(
|
||||
shell_index++, shells.size(), q, v);
|
||||
std::optional<NonlinearMitc4InternalEquilibrium> fresh;
|
||||
if (saved == nullptr) {
|
||||
FESA_HHT_PROBE_COUNT(kConstraintInternalEvaluation);
|
||||
auto internal =
|
||||
shell.internal->EvaluateTrial(Trial(q), &v);
|
||||
if (!internal.HasValue())
|
||||
@@ -713,6 +716,7 @@ Result<EmbeddingRoot> SolveEmbeddingRoot(const FixedChart& chart,
|
||||
const Vector& p, const Vector& seed,
|
||||
const ConstraintEvaluator& evaluate,
|
||||
TangentSolver& solver) {
|
||||
FESA_HHT_PROBE_SCOPE(kInner);
|
||||
if (!evaluate || p.Size() != chart.p_dofs.size() ||
|
||||
seed.Size() != chart.z_dofs.size())
|
||||
return Result<EmbeddingRoot>::Failure(
|
||||
@@ -830,6 +834,7 @@ Result<EmbeddingRoot> SolveEmbeddingRoot(const FixedChart& chart,
|
||||
prior_q = q.Value();
|
||||
has_prior_z = true;
|
||||
z = std::move(next_z);
|
||||
FESA_HHT_PROBE_COUNT(kInnerCorrection);
|
||||
}
|
||||
return Result<EmbeddingRoot>::Failure(Failure("Unreachable graph loop."));
|
||||
}
|
||||
@@ -876,6 +881,7 @@ Result<EmbeddingFirstDerivatives> EvaluateEmbeddingFirstDerivatives(
|
||||
Result<ReconstructedRates> ReconstructPhysicalRates(
|
||||
const FixedChart& chart, const EmbeddingRoot& root, const Vector& vp,
|
||||
const Vector& ap, TangentSolver& solver) {
|
||||
FESA_HHT_PROBE_SCOPE(kRates);
|
||||
if (!ValidBasis(chart) || root.q.Size() != chart.full_dof_count ||
|
||||
root.operators.rows.size() != chart.z_dofs.size() ||
|
||||
vp.Size() != chart.p_dofs.size() || ap.Size() != chart.p_dofs.size() ||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "analysis/hht_reduced_procedure.h"
|
||||
#include "analysis/hht_development_probe.h"
|
||||
#include "analysis/hht_reduced_integrator.h"
|
||||
|
||||
#include "equilibrium_algorithm.h"
|
||||
@@ -149,7 +150,7 @@ void ResidualRecord(const DofManager& dofs,
|
||||
frame.free_residual_metrics[1U] <= 1e-10;
|
||||
}
|
||||
|
||||
void CorrectionRecord(const Vector& coordinate, const Vector& velocity,
|
||||
bool CorrectionRecord(const Vector& coordinate, const Vector& velocity,
|
||||
const Vector& acceleration,
|
||||
const Vector& full_algorithmic,
|
||||
const Vector& reduced_algorithmic,
|
||||
@@ -188,12 +189,14 @@ void CorrectionRecord(const Vector& coordinate, const Vector& velocity,
|
||||
if (reduced_algorithmic.Size() != chart.p_columns.size() ||
|
||||
reduced_physical.Size() != chart.p_columns.size()) {
|
||||
record->correction_pass = false;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
bool private_pass = true;
|
||||
for (std::size_t i = 0U; i < chart.p_dofs.size(); ++i)
|
||||
record->correction_pass = record->correction_pass &&
|
||||
PrivateCorrectionEqual(chart, i, full_algorithmic,
|
||||
reduced_algorithmic, reduced_physical, alpha);
|
||||
private_pass = private_pass && PrivateCorrectionEqual(
|
||||
chart, i, full_algorithmic, reduced_algorithmic, reduced_physical, alpha);
|
||||
record->correction_pass = record->correction_pass && private_pass;
|
||||
return private_pass;
|
||||
}
|
||||
|
||||
bool ExactlyEqual(const Vector& left, const Vector& right) {
|
||||
@@ -396,8 +399,16 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
TangentSolver& tangent_solver) {
|
||||
const ReducedCoordinateShellHhtIntegrator integrator;
|
||||
NewtonStepAlgorithm algorithm;
|
||||
#if defined(FESA_HHT_DEVELOPMENT_PROBE)
|
||||
hht_development_probe::Session probe_session;
|
||||
hht_development_probe::PositiveSolver measured_positive{positive_solver};
|
||||
hht_development_probe::CorrectionSolver measured_tangent{tangent_solver};
|
||||
return ExecuteReducedHht(domain, log, parallel_for, measured_positive,
|
||||
measured_tangent, integrator, algorithm);
|
||||
#else
|
||||
return ExecuteReducedHht(domain, log, parallel_for, positive_solver,
|
||||
tangent_solver, integrator, algorithm);
|
||||
#endif
|
||||
}
|
||||
|
||||
Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
@@ -405,6 +416,7 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
const ParallelFor& parallel_for, LinearSolver& positive_solver,
|
||||
TangentSolver& tangent_solver, const ShellHhtIntegrator& integrator,
|
||||
EquilibriumAlgorithm& algorithm) {
|
||||
FESA_HHT_PROBE_SCOPE(kProcedure);
|
||||
if (domain.Dynamic() == nullptr)
|
||||
return Result<DynamicExecutionResult>::Failure(Status::Failure(
|
||||
FailureCategory::kModel, {{Severity::kError, "invalid-dynamic-step", {},
|
||||
@@ -423,7 +435,8 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
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);
|
||||
std::array<std::vector<hht_field_convergence::FluxOccurrence>, 2> load_flux;
|
||||
auto loads = AssembleLoads(domain, dofs, &load_flux);
|
||||
if (!loads.HasValue()) return Result<DynamicExecutionResult>::Failure(loads.GetStatus());
|
||||
auto parameters_result = MakeHhtParameters(domain.Dynamic()->alpha);
|
||||
if (!parameters_result.HasValue()) return Result<DynamicExecutionResult>::Failure(parameters_result.GetStatus());
|
||||
@@ -689,9 +702,11 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
status = log.Write(record); if (status.IsOk()) status = log.Flush();
|
||||
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
|
||||
|
||||
std::array<hht_field_convergence::History, 2> flux_history{};
|
||||
struct ReducedStepCandidate {
|
||||
DynamicAcceptedFrameState frame;
|
||||
ReducedHistory history;
|
||||
std::array<hht_field_convergence::History, 2> flux_history;
|
||||
};
|
||||
for (std::uint32_t increment = 1U;
|
||||
increment <= domain.Dynamic()->increment_count; ++increment) {
|
||||
@@ -701,9 +716,14 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
domain, failure, stage, increment, record.time);
|
||||
record.stage = stage;
|
||||
record.outcome = "failed";
|
||||
record.failure_code = contextual.Diagnostics().empty()
|
||||
? "dynamic-solver-failure"
|
||||
: contextual.Diagnostics().front().code;
|
||||
record.accepted = false;
|
||||
const auto cause = std::find_if(
|
||||
failure.Diagnostics().begin(), failure.Diagnostics().end(),
|
||||
[](const Diagnostic& diagnostic) {
|
||||
return diagnostic.code != "dynamic-procedure-context";
|
||||
});
|
||||
record.failure_code = cause == failure.Diagnostics().end()
|
||||
? "dynamic-solver-failure" : cause->code;
|
||||
const Status written = log.Write(record);
|
||||
return written.IsOk() ? contextual : written;
|
||||
};
|
||||
@@ -734,6 +754,10 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
record.increment_index = increment;
|
||||
record.time = trial_frame.time;
|
||||
record.correction_pass = true;
|
||||
record.policy_id = hht_field_convergence::kPolicyId;
|
||||
record.step_name = domain.Dynamic()->source_step_name_raw;
|
||||
std::optional<Vector> previous_selected_q;
|
||||
std::array<std::optional<double>, 2> previous_residual, older_residual;
|
||||
bool translation_private_algorithmic_pass = true;
|
||||
bool copy_translation_rates = false;
|
||||
Vector retained_velocity{history.p.Size()};
|
||||
@@ -745,6 +769,9 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
};
|
||||
ReducedShellHhtStep<ReducedStepCandidate> iteration_step(
|
||||
[&](std::uint32_t iteration) -> StepResult {
|
||||
record.fields = {};
|
||||
record.field_pass.reset();
|
||||
record.strict_residual_evaluated = false;
|
||||
record.iteration_index = iteration;
|
||||
record.stage = "evaluation";
|
||||
record.outcome = "candidate";
|
||||
@@ -886,7 +913,53 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
if (!operators.HasValue())
|
||||
return fail(operators.GetStatus());
|
||||
ResidualRecord(dofs, candidate, &record);
|
||||
if (record.residual_pass && record.correction_pass) {
|
||||
record.strict_residual_evaluated = true;
|
||||
auto measured_fields = MeasureEndpointFields(dofs, candidate, old, previous_selected_q);
|
||||
if (!measured_fields.HasValue()) return fail(measured_fields.GetStatus());
|
||||
auto field_inputs = std::move(measured_fields.Value());
|
||||
for (std::size_t c = 0; c < 2; ++c) {
|
||||
field_inputs[c].flux = trial.Value().assembly.field_flux[c];
|
||||
field_inputs[c].flux.insert(field_inputs[c].flux.end(),
|
||||
load_flux[c].begin(), load_flux[c].end());
|
||||
field_inputs[c].previous_residual = previous_residual[c];
|
||||
field_inputs[c].older_residual = older_residual[c];
|
||||
}
|
||||
const auto field_evaluation = hht_field_convergence::Evaluate(
|
||||
field_inputs, flux_history, iteration, length);
|
||||
for (std::size_t c = 0; c < 2; ++c) {
|
||||
auto& e = record.fields[c];
|
||||
const auto& in = field_inputs[c];
|
||||
const auto& out = field_evaluation.fields[c];
|
||||
if (in.active) {
|
||||
e.residual_numerator = in.residual;
|
||||
e.increment_norm = in.increment;
|
||||
e.correction_numerator = in.correction;
|
||||
}
|
||||
e.residual_scale = out.residual_scale;
|
||||
e.spatial_flux = out.spatial_flux;
|
||||
e.time_average_flux = out.time_average_flux;
|
||||
e.estimated_correction = out.estimated_correction;
|
||||
e.residual_threshold = out.residual_threshold;
|
||||
e.correction_threshold = out.correction_threshold;
|
||||
e.residual_pass = out.residual_pass;
|
||||
e.correction_pass = out.correction_pass;
|
||||
e.field_pass = out.field_pass;
|
||||
using Branch = hht_field_convergence::Branch;
|
||||
switch (out.branch) {
|
||||
case Branch::kNotEvaluated: e.branch = "not-evaluated"; break;
|
||||
case Branch::kLinear: e.branch = "linear"; break;
|
||||
case Branch::kZeroFlux: e.branch = "zero-flux"; break;
|
||||
case Branch::kNonquadratic: e.branch = "nonquadratic"; break;
|
||||
case Branch::kNormal: e.branch = "normal"; break;
|
||||
}
|
||||
}
|
||||
if (!field_evaluation.status.IsOk()) return fail(field_evaluation.status);
|
||||
record.field_pass = field_evaluation.field_pass;
|
||||
previous_selected_q = candidate.generalized_coordinates;
|
||||
older_residual = previous_residual;
|
||||
for (std::size_t c = 0; c < 2; ++c)
|
||||
previous_residual[c] = field_inputs[c].residual;
|
||||
if (field_evaluation.field_pass) {
|
||||
status = CompleteConstraintEvidence(
|
||||
domain, dofs, trial.Value().assembly, length, null_nodes,
|
||||
positive_solver, tangent_solver, &candidate);
|
||||
@@ -922,7 +995,7 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
return StepResult::Success(ReducedStepCandidate{
|
||||
std::move(candidate),
|
||||
{trial_p, trial_v, trial_a, trial_algorithmic,
|
||||
trial.Value().root.z}});
|
||||
trial.Value().root.z}, field_evaluation.pending});
|
||||
}
|
||||
if (!CanApplyOuterCorrection(iteration)) {
|
||||
double full_hidden = 0.0;
|
||||
@@ -1016,6 +1089,7 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
};
|
||||
Vector selected_solution{selected.Value().rhs.Size()};
|
||||
if (selected_solution.Size() > 0U) {
|
||||
FESA_HHT_PROBE_SCOPE(kOuterSolve);
|
||||
status = tangent_solver.FactorizeFresh(selected.Value().tangent);
|
||||
if (!status.IsOk())
|
||||
return linear_failure(status);
|
||||
@@ -1048,11 +1122,15 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
correction.Value().p[i] / (parameters.beta * h * h);
|
||||
reduced_physical[i] = acceleration_rate * correction.Value().p[i];
|
||||
}
|
||||
CorrectionRecord(correction.Value().coordinate,
|
||||
const bool private_correction_pass = CorrectionRecord(correction.Value().coordinate,
|
||||
correction.Value().velocity,
|
||||
correction.Value().acceleration, full_algorithmic,
|
||||
reduced_algorithmic, reduced_physical, chart.Value(),
|
||||
dofs, length, h, parameters.alpha, &record);
|
||||
record.strict_correction_evaluated = true;
|
||||
if (!private_correction_pass)
|
||||
return fail(Failure(domain, "reduced-dynamic-update-inconsistency",
|
||||
"The full/private correction acceleration arithmetic is inconsistent."));
|
||||
translation_private_algorithmic_pass = true;
|
||||
for (std::size_t i = 0U; i < chart.Value().p_dofs.size(); ++i)
|
||||
if (PhysicalCoordinateChannel(chart.Value(), i) == 0U)
|
||||
@@ -1091,6 +1169,7 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
seed = trial.Value().root.z;
|
||||
for (std::size_t i = 0U; i < seed.Size(); ++i)
|
||||
seed[i] += correction.Value().z[i];
|
||||
FESA_HHT_PROBE_COUNT(kOuterCorrection);
|
||||
return StepResult::Success(std::nullopt);
|
||||
},
|
||||
kMaximumOuterCorrections);
|
||||
@@ -1103,16 +1182,21 @@ Result<DynamicExecutionResult> ExecuteReducedHht(
|
||||
iteration_step.TakeCandidate());
|
||||
},
|
||||
[&](ReducedStepCandidate&& accepted_candidate) -> Status {
|
||||
status = state.CommitAcceptedFrame(accepted_candidate.frame);
|
||||
if (!status.IsOk())
|
||||
return report_failure(status, "accepted");
|
||||
history = std::move(accepted_candidate.history);
|
||||
// Stage the complete accepted history. Output failure discards all
|
||||
// three snapshots; publication below consists only of noexcept moves.
|
||||
auto staged_state = state;
|
||||
status = staged_state.CommitAcceptedFrame(accepted_candidate.frame);
|
||||
if (!status.IsOk()) return report_failure(status, "accepted");
|
||||
record.stage = "accepted";
|
||||
record.outcome = "accepted";
|
||||
record.accepted = true;
|
||||
status = log.Write(record);
|
||||
if (status.IsOk())
|
||||
status = log.Flush();
|
||||
return status;
|
||||
if (status.IsOk()) status = log.Flush();
|
||||
if (!status.IsOk()) return status;
|
||||
state = std::move(staged_state);
|
||||
history = std::move(accepted_candidate.history);
|
||||
flux_history = accepted_candidate.flux_history;
|
||||
return Status::Ok();
|
||||
});
|
||||
if (!status.IsOk()) return Result<DynamicExecutionResult>::Failure(status);
|
||||
}
|
||||
|
||||
@@ -217,6 +217,13 @@ Result<EigenAnalysisState> ExecuteCandidate(const Domain& domain,
|
||||
if (!backend.HasValue())
|
||||
return Result<EigenAnalysisState>::Failure(backend.GetStatus());
|
||||
EigenAnalysisState state(std::move(backend.Value()));
|
||||
for (const auto& reference : references) {
|
||||
if (state.drilling_mode && *state.drilling_mode != reference->DrillingMode())
|
||||
return Result<EigenAnalysisState>::Failure(Failure(
|
||||
"invalid-shell-drilling-evidence", "mode",
|
||||
"All frequency kernels must retain one actual drilling mode."));
|
||||
state.drilling_mode = reference->DrillingMode();
|
||||
}
|
||||
state.full_dof_count = dofs.FullDofCount();
|
||||
state.free_dof_count = reduction.Value().free_count;
|
||||
state.mass_null_dimension = reduction.Value().null_count;
|
||||
|
||||
@@ -261,10 +261,100 @@ Result<std::string> SerializeRecord(const NewtonIterationLogRecord& record,
|
||||
return Result<std::string>::Success(std::move(line));
|
||||
}
|
||||
|
||||
// Candidate policy has separate names and units; v1 evidence is never redefined.
|
||||
Result<std::string> SerializeFieldRecord(
|
||||
const DynamicNewtonIterationLogRecord& record,
|
||||
const std::filesystem::path& path) {
|
||||
if (record.policy_id != "abaqus2025-field-on-fesa-v1" ||
|
||||
record.time < 0.0 || record.stage.empty() || record.outcome.empty())
|
||||
return Result<std::string>::Failure(
|
||||
LogOutputFailure(path, "Invalid HHT field-policy log identity."));
|
||||
std::vector<std::string> fields;
|
||||
const auto number = [&](const std::string& key,
|
||||
const std::optional<double>& value) -> Status {
|
||||
if (!value.has_value()) {
|
||||
AppendField(&fields, key, "not-evaluated");
|
||||
return Status::Ok();
|
||||
}
|
||||
auto formatted = FormatFiniteNumber(*value, path);
|
||||
if (!formatted.HasValue()) return formatted.GetStatus();
|
||||
if (*value < 0.0)
|
||||
return LogOutputFailure(path, "Negative HHT field norm or scale.");
|
||||
AppendField(&fields, key, formatted.Value());
|
||||
return Status::Ok();
|
||||
};
|
||||
const auto boolean = [&](const std::string& key, const std::optional<bool>& value) {
|
||||
AppendField(&fields, key, value.has_value() ? (*value ? "1" : "0") : "not-evaluated");
|
||||
};
|
||||
AppendField(&fields, "record", "dynamic-newton-iteration");
|
||||
AppendField(&fields, "schema", "hht-log-v2");
|
||||
AppendField(&fields, "policy_id", record.policy_id);
|
||||
AppendField(&fields, "step", EscapeText(record.step_name));
|
||||
AppendField(&fields, "increment", std::to_string(record.increment_index));
|
||||
auto status = number("time", record.time);
|
||||
if (!status.IsOk()) return Result<std::string>::Failure(status);
|
||||
AppendField(&fields, "iteration", std::to_string(record.iteration_index));
|
||||
AppendField(&fields, "stage", EscapeText(record.stage));
|
||||
AppendField(&fields, "outcome", EscapeText(record.outcome));
|
||||
AppendField(&fields, "failure_code", EscapeText(record.failure_code));
|
||||
boolean("field_pass", record.field_pass);
|
||||
boolean("accepted", record.accepted);
|
||||
for (std::size_t c = 0; c < 2; ++c) {
|
||||
const auto& e = record.fields[c];
|
||||
const std::string prefix = c == 0 ? "force_" : "moment_";
|
||||
const std::array<std::pair<const char*, std::optional<double>>, 9> numbers{{
|
||||
{"residual_numerator", e.residual_numerator}, {"residual_scale", e.residual_scale},
|
||||
{"spatial_flux", e.spatial_flux}, {"time_average_flux", e.time_average_flux},
|
||||
{"correction_numerator", e.correction_numerator}, {"increment_norm", e.increment_norm},
|
||||
{"estimated_correction", e.estimated_correction}, {"residual_threshold", e.residual_threshold},
|
||||
{"correction_threshold", e.correction_threshold}}};
|
||||
for (const auto& entry : numbers) {
|
||||
status = number(prefix + entry.first, entry.second);
|
||||
if (!status.IsOk()) return Result<std::string>::Failure(status);
|
||||
}
|
||||
AppendField(&fields, prefix + "residual_unit", c == 0 ? "force" : "moment");
|
||||
AppendField(&fields, prefix + "correction_unit", c == 0 ? "length" : "rad");
|
||||
if (e.branch != "normal" && e.branch != "zero-flux" && e.branch != "linear" &&
|
||||
e.branch != "nonquadratic" && e.branch != "not-evaluated")
|
||||
return Result<std::string>::Failure(LogOutputFailure(path, "Invalid HHT field branch."));
|
||||
AppendField(&fields, prefix + "branch", e.branch);
|
||||
boolean(prefix + "residual_pass", e.residual_pass);
|
||||
boolean(prefix + "correction_pass", e.correction_pass);
|
||||
boolean(prefix + "field_pass", e.field_pass);
|
||||
}
|
||||
for (std::size_t c = 0; c < 2; ++c) {
|
||||
const std::string suffix = c == 0 ? "u" : "phi";
|
||||
const auto strict = [&](double value, bool evaluated) {
|
||||
return evaluated ? std::optional<double>{value} : std::nullopt;
|
||||
};
|
||||
const std::array<std::pair<std::string, std::optional<double>>, 6> numbers{{
|
||||
{"residual_" + suffix + "_numerator", strict(record.residual_numerator[c], record.strict_residual_evaluated)},
|
||||
{"residual_" + suffix + "_scale", strict(record.residual_scale[c], record.strict_residual_evaluated)},
|
||||
{"coordinate_correction_" + suffix, strict(record.coordinate_correction[c], record.strict_correction_evaluated)},
|
||||
{"velocity_correction_" + suffix, strict(record.velocity_correction[c], record.strict_correction_evaluated)},
|
||||
{"acceleration_correction_" + suffix, strict(record.acceleration_correction[c], record.strict_correction_evaluated)},
|
||||
{"algorithmic_correction_" + suffix, strict(record.algorithmic_correction[c], record.strict_correction_evaluated)}}};
|
||||
for (const auto& entry : numbers) {
|
||||
status = number("strict_" + entry.first, entry.second);
|
||||
if (!status.IsOk()) return Result<std::string>::Failure(status);
|
||||
}
|
||||
}
|
||||
boolean("strict_residual_pass", record.strict_residual_evaluated ? std::optional<bool>{record.residual_pass} : std::nullopt);
|
||||
boolean("strict_correction_pass", record.strict_correction_evaluated ? std::optional<bool>{record.correction_pass} : std::nullopt);
|
||||
std::string line;
|
||||
for (const auto& field : fields) {
|
||||
if (!line.empty()) line += '\t';
|
||||
line += field;
|
||||
}
|
||||
line += '\n';
|
||||
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) {
|
||||
if (!record.policy_id.empty()) return SerializeFieldRecord(record, 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()) {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "fesa/elements/element_definition.h"
|
||||
#include "math/finite_values.h"
|
||||
#include "results/shell_result_recovery.h"
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
#include "analysis/shell_state_inventory.h"
|
||||
|
||||
namespace fesa {
|
||||
@@ -110,6 +111,13 @@ Status ValidateAcceptedFrame(const AcceptedFrameState& frame,
|
||||
"Accepted frame physical result inventory is invalid.");
|
||||
}
|
||||
|
||||
if (!results_internal::ValidDrillingEvidence(frame.mitc4_drilling,
|
||||
frame.global_equilibrium) ||
|
||||
(accepted_frame_count != 0U && !results_internal::SameDrillingInventory(
|
||||
frame.mitc4_drilling, committed.mitc4_drilling)))
|
||||
return StateFailure(FailureCategory::kSolver, "invalid-shell-drilling-evidence", {},
|
||||
"Accepted drilling evidence must be finite and retain its mode.");
|
||||
|
||||
const double gauss = 1.0 / std::sqrt(3.0);
|
||||
const std::array<ShellMidsurfaceLocation, kShellLocationsPerElement>
|
||||
expected_locations{ShellMidsurfaceLocation::kGp1,
|
||||
@@ -243,7 +251,7 @@ Status NonlinearAnalysisState::CommitAcceptedFrame(
|
||||
frame.reaction,
|
||||
frame.shell_results,
|
||||
frame.physical_strain_energy,
|
||||
frame.global_equilibrium};
|
||||
frame.global_equilibrium, frame.mitc4_drilling};
|
||||
std::vector<AcceptedFrameState> accepted_candidate = accepted_frames_;
|
||||
accepted_candidate.push_back(frame);
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "assembly/ordered_contribution_internal.h"
|
||||
#include "math/finite_values.h"
|
||||
#include "elements/shell_kernel_values.h"
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
@@ -57,6 +58,7 @@ struct InternalAssembly {
|
||||
SparseMatrix tangent;
|
||||
std::vector<ShellResultRow> shell_rows;
|
||||
double physical_strain_energy;
|
||||
Mitc4DrillingEvidence mitc4_drilling;
|
||||
};
|
||||
|
||||
Status ProcedureFailure(const FailureCategory category, const std::string& code,
|
||||
@@ -175,6 +177,10 @@ Result<NonlinearStateBaselineInput> BuildBaseline(const Domain& domain) {
|
||||
Result<std::vector<RuntimeShell>> BuildRuntimeShells(const Domain& domain,
|
||||
const DofManager& dofs) {
|
||||
std::vector<RuntimeShell> shells;
|
||||
if (domain.Elements().Empty())
|
||||
return Result<std::vector<RuntimeShell>>::Failure(ProcedureFailure(
|
||||
FailureCategory::kModel, "unsupported-nonlinear-element", {}, "ELEMENT",
|
||||
{}, "The nonlinear static procedure requires a MITC4 shell element."));
|
||||
shells.reserve(domain.Elements().Size());
|
||||
for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) {
|
||||
const auto* definition =
|
||||
@@ -320,7 +326,7 @@ NonlinearMitc4TrialDofs ExtractElementTrial(
|
||||
|
||||
Result<InternalAssembly> AssembleInternal(
|
||||
const std::vector<RuntimeShell>& shells, const DofManager& dofs,
|
||||
const Vector& coordinates, const ParallelFor& parallel_for) {
|
||||
const Domain& domain, const Vector& coordinates, const ParallelFor& parallel_for) {
|
||||
std::vector<std::optional<NonlinearMitc4InternalEquilibrium>> local(
|
||||
shells.size());
|
||||
std::vector<std::optional<Status>> failures(shells.size());
|
||||
@@ -341,6 +347,7 @@ Result<InternalAssembly> AssembleInternal(
|
||||
std::vector<ShellResultRow> rows;
|
||||
rows.reserve(shells.size() * 4U);
|
||||
double energy = 0.0;
|
||||
Mitc4DrillingEvidence drilling{shells.front().kernel->DrillingMode()};
|
||||
for (std::size_t index = 0U; index < shells.size(); ++index) {
|
||||
if (failures[index].has_value()) {
|
||||
return Result<InternalAssembly>::Failure(*failures[index]);
|
||||
@@ -354,6 +361,28 @@ Result<InternalAssembly> AssembleInternal(
|
||||
"NONLINEAR_STATIC_PROCEDURE", std::to_string(index),
|
||||
"A nonlinear shell internal residual or tangent is nonfinite."));
|
||||
}
|
||||
if (shells[index].kernel->DrillingMode() != drilling.mode ||
|
||||
local[index]->drilling_force.Size() != kShellDofCount ||
|
||||
!IsFinite(local[index]->drilling_force))
|
||||
return Result<InternalAssembly>::Failure(ProcedureFailure(
|
||||
FailureCategory::kSolver, "invalid-shell-drilling-evidence", {},
|
||||
"NONLINEAR_STATIC_PROCEDURE", {}, "Drilling mode or force is invalid."));
|
||||
const auto element_trial = ExtractElementTrial(shells[index], coordinates);
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
const auto first = 6U * node;
|
||||
const auto global_node = shells[index].scatter[first] / 6U;
|
||||
const auto& values = local[index]->drilling_force;
|
||||
const Vector3 drilling_force{values[first], values[first + 1U], values[first + 2U]};
|
||||
const auto spatial = SpatialMomentSO3(element_trial.rotation_vector[node],
|
||||
{values[first + 3U], values[first + 4U], values[first + 5U]});
|
||||
if (!spatial.HasValue())
|
||||
return Result<InternalAssembly>::Failure(spatial.GetStatus());
|
||||
const Vector3 position = Vector3(domain.Nodes()[global_node].coordinates) +
|
||||
element_trial.displacement[node];
|
||||
const Vector3 moment = position.Cross(drilling_force) + spatial.Value();
|
||||
for (std::size_t c = 0U; c < 3U; ++c)
|
||||
drilling.drilling_moment[c] += moment[c];
|
||||
}
|
||||
for (std::size_t row = 0U; row < kShellDofCount; ++row) {
|
||||
force[shells[index].scatter[row]] += local[index]->internal_residual[row];
|
||||
assembly_internal::AppendLocalMatrixRow(
|
||||
@@ -373,7 +402,8 @@ Result<InternalAssembly> AssembleInternal(
|
||||
}
|
||||
energy += local[index]->physical_strain_energy;
|
||||
}
|
||||
if (!IsFinite(force) || !std::isfinite(energy)) {
|
||||
if (!IsFinite(force) || !std::isfinite(energy) ||
|
||||
!IsFinite(drilling.drilling_moment)) {
|
||||
return Result<InternalAssembly>::Failure(ProcedureFailure(
|
||||
FailureCategory::kSolver, "nonfinite-newton-data", {},
|
||||
"NONLINEAR_STATIC_PROCEDURE", {},
|
||||
@@ -387,7 +417,7 @@ Result<InternalAssembly> AssembleInternal(
|
||||
return Result<InternalAssembly>::Failure(tangent.GetStatus());
|
||||
}
|
||||
return Result<InternalAssembly>::Success(
|
||||
{std::move(force), std::move(tangent.Value()), std::move(rows), energy});
|
||||
{std::move(force), std::move(tangent.Value()), std::move(rows), energy, drilling});
|
||||
}
|
||||
|
||||
Result<Vector> Difference(const Vector& left, const Vector& right) {
|
||||
@@ -699,7 +729,7 @@ AcceptedFrameState AcceptedFrame(const std::uint32_t frame_index,
|
||||
trial.reaction,
|
||||
trial.shell_results,
|
||||
trial.physical_strain_energy,
|
||||
trial.global_equilibrium};
|
||||
trial.global_equilibrium, trial.mitc4_drilling};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -807,7 +837,7 @@ Result<NonlinearStaticExecutionResult> ExecuteNonlinearStaticCore(
|
||||
NewtonIterationStage::kKinematicsEvaluated;
|
||||
|
||||
Result<InternalAssembly> internal = AssembleInternal(
|
||||
shells.Value(), dofs.Value(),
|
||||
shells.Value(), dofs.Value(), domain,
|
||||
trial.Value().generalized_coordinates, parallel_for);
|
||||
if (!internal.HasValue()) {
|
||||
return Result<std::optional<ShellStepCandidate>>::Failure(
|
||||
@@ -879,6 +909,9 @@ Result<NonlinearStaticExecutionResult> ExecuteNonlinearStaticCore(
|
||||
NewtonIterationStage::kExternalAssembled));
|
||||
}
|
||||
trial.Value().global_equilibrium = global_equilibrium.Value();
|
||||
trial.Value().mitc4_drilling = internal.Value().mitc4_drilling;
|
||||
results_internal::CompleteDrillingEvidence(
|
||||
trial.Value().global_equilibrium, &*trial.Value().mitc4_drilling);
|
||||
|
||||
Result<PartitionedStiffness> partition =
|
||||
constraints.Partition(tangent.Value(), dofs.Value());
|
||||
|
||||
@@ -95,6 +95,10 @@ Result<ThermalShellRecovery> Ds4ThermalAdapter::Recover(
|
||||
|
||||
Mitc4ReferenceAdapter::Mitc4ReferenceAdapter(const Mitc4Shell& kernel)
|
||||
: kernel_{kernel} {}
|
||||
Mitc4DrillingMode Mitc4ReferenceAdapter::DrillingMode() const noexcept {
|
||||
return kernel_.DrillingMode();
|
||||
}
|
||||
|
||||
const ElementDofLayout& Mitc4ReferenceAdapter::DofLayout() const noexcept {
|
||||
return kernel_.DofLayout();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef FESA_SRC_FESA_ELEMENTS_MITC4_DRILLING_INTERNAL_H_
|
||||
#define FESA_SRC_FESA_ELEMENTS_MITC4_DRILLING_INTERNAL_H_
|
||||
|
||||
#include "fesa/elements/nonlinear_mitc4_shell.h"
|
||||
|
||||
namespace fesa::mitc4_drilling_internal {
|
||||
|
||||
// The sole production selection. Change this constant and rebuild all targets.
|
||||
inline constexpr Mitc4DrillingMode kProductionDrillingMode =
|
||||
Mitc4DrillingMode::kDirectSpring;
|
||||
|
||||
/// @brief Provides explicit alternate formulations only for internal tests.
|
||||
/// @note Production factories use the single compile-time selection above.
|
||||
struct TestAccess {
|
||||
static Mitc4Shell WithMode(Mitc4Shell shell, Mitc4DrillingMode mode) {
|
||||
shell.drilling_mode_ = mode;
|
||||
return shell;
|
||||
}
|
||||
static NonlinearMitc4Shell WithMode(NonlinearMitc4Shell shell,
|
||||
Mitc4DrillingMode mode) {
|
||||
shell.drilling_mode_ = mode;
|
||||
return shell;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace fesa::mitc4_drilling_internal
|
||||
|
||||
#endif // FESA_SRC_FESA_ELEMENTS_MITC4_DRILLING_INTERNAL_H_
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "math/finite_values.h"
|
||||
#include "math/vector3_operations.h"
|
||||
#include "elements/shell_kernel_values.h"
|
||||
#include "elements/mitc4_drilling_internal.h"
|
||||
|
||||
namespace fesa {
|
||||
|
||||
@@ -957,6 +958,22 @@ Result<ElementResultBundle> Mitc4Shell::Recover(
|
||||
ShellElementResultRows rows{};
|
||||
rows.rows.reserve(kNodeCount);
|
||||
rows.physical_strain_energy = recovered.Value().strain_energy;
|
||||
const auto stiffness = Stiffness();
|
||||
if (!stiffness.HasValue())
|
||||
return Result<ElementResultBundle>::Failure(stiffness.GetStatus());
|
||||
const Vector drilling_force =
|
||||
stiffness.Value().drilling_global24.Multiply(element_displacement);
|
||||
rows.drilling_mode = DrillingMode();
|
||||
Vector3 drilling_moment;
|
||||
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
||||
const std::size_t first = 6U * node;
|
||||
const Vector3 force{drilling_force[first], drilling_force[first + 1U],
|
||||
drilling_force[first + 2U]};
|
||||
const Vector3 moment{drilling_force[first + 3U], drilling_force[first + 4U],
|
||||
drilling_force[first + 5U]};
|
||||
drilling_moment = drilling_moment + coordinates_[node].Cross(force) + moment;
|
||||
}
|
||||
rows.drilling_moment = drilling_moment.Components();
|
||||
for (std::size_t point = 0U; point < recovered.Value().points.size();
|
||||
++point) {
|
||||
const auto& physical_point = recovered.Value().points[point];
|
||||
@@ -1002,6 +1019,7 @@ Mitc4Shell::Mitc4Shell(std::array<Vector3, 4> coordinates,
|
||||
thickness_{thickness},
|
||||
youngs_modulus_{youngs_modulus},
|
||||
poisson_ratio_{poisson_ratio},
|
||||
drilling_mode_{mitc4_drilling_internal::kProductionDrillingMode},
|
||||
source_location_{std::move(source_location)},
|
||||
identity_{std::move(identity)} {}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace fesa::nonlinear_mitc4_internal {
|
||||
/// key.
|
||||
struct ConfigurationSnapshot {
|
||||
std::array<double, 64> values{};
|
||||
Mitc4DrillingMode drilling_mode = Mitc4DrillingMode::kDirectSpring;
|
||||
SourceLocation source{};
|
||||
std::string identity;
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ JetMatrix LeftJacobian(const JetVector& phi) {
|
||||
JetMatrix result = IdentityMatrix();
|
||||
JetMatrix term = IdentityMatrix();
|
||||
double factorial = 1.0;
|
||||
for (std::size_t order = 1U; order <= 32U; ++order) {
|
||||
for (std::size_t order = 1U; order <= 3U; ++order) {
|
||||
term = Multiply(term, skew);
|
||||
factorial *= static_cast<double>(order + 1U);
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
@@ -242,14 +242,13 @@ JetMatrix LeftJacobian(const JetVector& phi) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// @brief Uses a converged exponential series so D1/D2/D3 share one analytic
|
||||
/// map.
|
||||
/// @brief Uses a cubic exponential polynomial with matching D1/D2/D3.
|
||||
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) {
|
||||
for (std::size_t order = 1U; order <= 3U; ++order) {
|
||||
term = Multiply(term, skew);
|
||||
factorial *= static_cast<double>(order);
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
@@ -378,9 +377,9 @@ Status CheckPhysicalMass(const Matrix& mass,
|
||||
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)) {
|
||||
if (!(symmetry_norm / mass_norm <= 1.0e-3) ||
|
||||
!(null_action_norm / mass_norm / 2.0 <= 1.0e-3) ||
|
||||
!(full_gram_error <= 1.0e-3)) {
|
||||
return fail("invalid-nonlinear-inertia-null",
|
||||
"Scaled mass symmetry and analytic null identities failed.");
|
||||
}
|
||||
@@ -408,7 +407,7 @@ Status CheckPhysicalMass(const Matrix& mass,
|
||||
block_frobenius = std::hypot(block_frobenius, block(row, column));
|
||||
}
|
||||
}
|
||||
if (!(reconstruction_error / block_frobenius <= 1.0e-12)) {
|
||||
if (!(reconstruction_error / block_frobenius <= 1.0e-3)) {
|
||||
return fail("invalid-nonlinear-inertia-positive-mass",
|
||||
"Physical mass Cholesky reconstruction failed.");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "fesa/elements/finite_rotation.h"
|
||||
#include "elements/nonlinear_mitc4_configuration_internal.h"
|
||||
#include "elements/mitc4_drilling_internal.h"
|
||||
#include "math/dense_blas_internal.h"
|
||||
#include "math/jet_arithmetic.h"
|
||||
#include "math/jet_linear_solve.h"
|
||||
@@ -39,6 +40,7 @@ nonlinear_mitc4_internal::ConfigurationSnapshot::Read(
|
||||
result.values[index++] = element.youngs_modulus_;
|
||||
result.values[index++] = element.poisson_ratio_;
|
||||
result.values[index] = element.drilling_stiffness_;
|
||||
result.drilling_mode = element.drilling_mode_;
|
||||
result.source = element.source_location_;
|
||||
result.identity = element.identity_;
|
||||
return result;
|
||||
@@ -1155,11 +1157,10 @@ Jet PhysicalEnergy(const std::array<Vector3, kNodeCount>& coordinates,
|
||||
return energy;
|
||||
}
|
||||
|
||||
/// @brief Computes a fixed-iteration analytic polar factor for drill
|
||||
/// corotation.
|
||||
/// @brief Applies one polar iteration for drill corotation.
|
||||
JetMatrix3 PolarRotation(const JetMatrix3& deformation) {
|
||||
JetMatrix3 result = deformation;
|
||||
for (std::size_t iteration = 0U; iteration < 12U; ++iteration) {
|
||||
for (std::size_t iteration = 0U; iteration < 1U; ++iteration) {
|
||||
const JetMatrix3 coefficients = Transpose(result);
|
||||
Matrix primal{3, 3};
|
||||
for (std::size_t row = 0; row < 3; ++row)
|
||||
@@ -1228,8 +1229,91 @@ Jet DrillSpinRate(const JetVector3& phi, const Vector3& director) {
|
||||
return Reciprocal(Sqrt(Dot(preimage, preimage)));
|
||||
}
|
||||
|
||||
/// @brief Uses precisely the current inertia kernel's cubic rotation maps.
|
||||
/// @note This is deliberately distinct from the legacy analytic null. Keep the
|
||||
/// cubic Exp/Jacobian order synchronized with EvaluateDynamicRotation; it is
|
||||
/// not an approximation to be silently replaced by the exact exponential
|
||||
/// inverse.
|
||||
JetVector3 InertiaDrillNull(const JetVector3& phi, const Vector3& director) {
|
||||
const JetMatrix3 skew = Skew(phi);
|
||||
JetMatrix3 rotation = Identity();
|
||||
JetMatrix3 jacobian = Identity();
|
||||
JetMatrix3 term = Identity();
|
||||
double rotation_factorial = 1.0;
|
||||
double jacobian_factorial = 1.0;
|
||||
for (std::size_t order = 1U; order <= 3U; ++order) {
|
||||
term = Multiply(term, skew);
|
||||
rotation_factorial *= static_cast<double>(order);
|
||||
jacobian_factorial *= static_cast<double>(order + 1U);
|
||||
rotation = Add(rotation, Scale(1.0 / rotation_factorial, term));
|
||||
jacobian = Add(jacobian, Scale(1.0 / jacobian_factorial, term));
|
||||
}
|
||||
const JetVector3 rhs = Multiply(rotation, ConstantVector(director));
|
||||
Matrix primal{3U, 3U};
|
||||
for (std::size_t row = 0U; row < 3U; ++row)
|
||||
for (std::size_t column = 0U; column < 3U; ++column)
|
||||
primal(row, column) = jacobian[row][column].value;
|
||||
const auto factor = dense_blas_internal::DenseFactor::Lu(primal);
|
||||
const std::array<std::size_t, 4> channels{1U, kGlobalDofCount + 1U,
|
||||
kHessianSize + 1U + kGlobalDofCount,
|
||||
kGlobalDofCount};
|
||||
JetVector3 preimage{};
|
||||
if (!factor.HasValue() ||
|
||||
!math_internal::SolveJetAction(jacobian, rhs, factor.Value(), channels,
|
||||
Constant, ReadChannel, WriteChannel,
|
||||
preimage)) {
|
||||
preimage.fill(Constant(std::numeric_limits<double>::quiet_NaN()));
|
||||
return preimage;
|
||||
}
|
||||
return Scale(Reciprocal(Sqrt(Dot(preimage, preimage))), preimage);
|
||||
}
|
||||
|
||||
/// @brief Evaluates the fixed initial-director spring without corotation.
|
||||
Jet DirectDrillingEnergy(
|
||||
const std::array<Vector3, kNodeCount>& initial_directors,
|
||||
const JetTrial& trial, double drilling_stiffness, Matrix* metric,
|
||||
std::array<Jet, 4>* constraints) {
|
||||
Jet energy{};
|
||||
double* tangent = energy.second.Data();
|
||||
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
||||
const auto d0 = ConstantVector(initial_directors[node]);
|
||||
const Jet gamma = Dot(d0, trial.rotation[node]);
|
||||
// Retain the validated spring scale through subnormal intermediates.
|
||||
// Fill its linear derivatives explicitly, without differentiating a
|
||||
// floating half-energy that can round kd before doubling it again.
|
||||
energy.value += 0.5 * gamma.value * (drilling_stiffness * gamma.value);
|
||||
energy.directional_first +=
|
||||
drilling_stiffness * gamma.value * gamma.directional_first;
|
||||
energy.directional_second +=
|
||||
drilling_stiffness * gamma.directional_first * gamma.directional_first;
|
||||
for (std::size_t i = 0U; i < 3U; ++i) {
|
||||
const auto row = 6U * node + 3U + i;
|
||||
energy.first[row] =
|
||||
drilling_stiffness * gamma.value * initial_directors[node][i];
|
||||
energy.mixed_first[row] = drilling_stiffness * gamma.directional_first *
|
||||
initial_directors[node][i];
|
||||
}
|
||||
// D2 f_drill[v,v] is identically zero; mixed_second retains exact zeros.
|
||||
if (constraints != nullptr) {
|
||||
const auto null =
|
||||
InertiaDrillNull(trial.rotation[node], initial_directors[node]);
|
||||
(*constraints)[node] = drilling_stiffness * gamma * Dot(null, d0);
|
||||
}
|
||||
for (std::size_t i = 0U; i < 3U; ++i)
|
||||
for (std::size_t j = 0U; j < 3U; ++j) {
|
||||
const auto row = 6U * node + 3U + i;
|
||||
const auto column = 6U * node + 3U + j;
|
||||
const double value = drilling_stiffness * initial_directors[node][i] *
|
||||
initial_directors[node][j];
|
||||
tangent[row * kGlobalDofCount + column] = value;
|
||||
(*metric)(row, column) = value;
|
||||
}
|
||||
}
|
||||
return energy;
|
||||
}
|
||||
|
||||
/// @brief Evaluates the objective numerical drilling potential and variations.
|
||||
Jet DrillingEnergy(const std::array<Vector3, kNodeCount>& coordinates,
|
||||
Jet LegacyDrillingEnergy(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, Matrix* metric,
|
||||
@@ -1324,6 +1408,7 @@ NonlinearMitc4Shell::NonlinearMitc4Shell(
|
||||
youngs_modulus_{youngs_modulus},
|
||||
poisson_ratio_{poisson_ratio},
|
||||
drilling_stiffness_{drilling_stiffness},
|
||||
drilling_mode_{mitc4_drilling_internal::kProductionDrillingMode},
|
||||
source_location_{std::move(source_location)},
|
||||
identity_{std::move(identity)} {}
|
||||
|
||||
@@ -1641,7 +1726,10 @@ NonlinearMitc4Shell::EvaluateExternalDrillingConstraint(
|
||||
jet_phi[component] = Variable(phi[component], component,
|
||||
phi_dot[component]);
|
||||
}
|
||||
const JetVector3 null_direction = DrillNull(jet_phi, initial_directors_[node]);
|
||||
const JetVector3 null_direction =
|
||||
drilling_mode_ == Mitc4DrillingMode::kDirectSpring
|
||||
? InertiaDrillNull(jet_phi, initial_directors_[node])
|
||||
: DrillNull(jet_phi, initial_directors_[node]);
|
||||
const Jet scalar = -1.0 * Dot(null_direction, ConstantVector(force));
|
||||
const Jet scalar_time =
|
||||
-1.0 * Dot(null_direction, ConstantVector(force_rate));
|
||||
@@ -1742,10 +1830,17 @@ Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
|
||||
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, &drilling_metric,
|
||||
coordinate_direction != nullptr ? &constraints : nullptr);
|
||||
const Jet drilling =
|
||||
element.drilling_mode_ == Mitc4DrillingMode::kDirectSpring
|
||||
? DirectDrillingEnergy(
|
||||
element.initial_directors_, jet_trial,
|
||||
element.drilling_stiffness_, &drilling_metric,
|
||||
coordinate_direction != nullptr ? &constraints : nullptr)
|
||||
: LegacyDrillingEnergy(
|
||||
element.coordinates_, element.initial_directors_, jet_trial,
|
||||
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>(
|
||||
@@ -1816,10 +1911,13 @@ Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
|
||||
equilibrium.drilling_metric = std::move(drilling_metric);
|
||||
equilibrium.drilling_energy = drilling.value;
|
||||
equilibrium.force_directional_second = Vector{kGlobalDofCount};
|
||||
equilibrium.drilling_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.drilling_force_directional_second[index] =
|
||||
drilling.mixed_second[index];
|
||||
equilibrium.force_directional_second[index] =
|
||||
internal_energy.mixed_second[index];
|
||||
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
#include "io/hdf5/hdf5_primitives.h"
|
||||
#include "io/hdf5/hdf5_raii.h"
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
#include "io/hdf5/mitc4_file_hooks.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
@@ -849,13 +853,13 @@ void CheckTables(const hid_t file, const DynamicAnalysisState& state,
|
||||
Status SelfCheck(const std::filesystem::path& path, const Domain& domain,
|
||||
const DynamicAnalysisState& state,
|
||||
const hdf5_internal::WriterModelData& model,
|
||||
const std::vector<Diagnostic>& diagnostics) {
|
||||
const std::vector<Diagnostic>& diagnostics, herr_t (*close_reader)(hid_t)) {
|
||||
try {
|
||||
Hdf5Handle file{
|
||||
hdf5_internal::RequireHdf5Id(
|
||||
H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
|
||||
"Unable to reopen dynamic candidate."),
|
||||
H5Fclose};
|
||||
close_reader};
|
||||
Hdf5Handle metadata{hdf5_internal::RequireHdf5Id(
|
||||
H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT),
|
||||
"The dynamic metadata group is missing."),
|
||||
@@ -902,9 +906,16 @@ Status SelfCheck(const std::filesystem::path& path, const Domain& domain,
|
||||
"Unable to inspect accepted frame group.");
|
||||
Check(information.nlinks == state.AcceptedFrames().size(),
|
||||
"Unexpected frame group inventory.");
|
||||
const auto drilling = hdf5_internal::RequireDrillingDiagnostics(
|
||||
file.Get(), state.AcceptedFrames().front().mitc4_drilling->mode);
|
||||
hdf5_internal::RequireDrilling(drilling.frames.size() == state.AcceptedFrames().size());
|
||||
std::size_t drilling_index = 0U;
|
||||
for (const auto& frame : state.AcceptedFrames()) {
|
||||
hdf5_internal::CheckDrillingFrame(drilling.frames[drilling_index++], frame.frame_index,
|
||||
frame.global_equilibrium, *frame.mitc4_drilling);
|
||||
WriteFrame(file.Get(), domain, frame, true);
|
||||
}
|
||||
hdf5_internal::RequireHdf5(file.CloseChecked(), "Unable to close dynamic readback.");
|
||||
return Status::Ok();
|
||||
} catch (const std::exception& error) {
|
||||
return Failure("dynamic-output-schema", error.what());
|
||||
@@ -913,10 +924,10 @@ Status SelfCheck(const std::filesystem::path& path, const Domain& domain,
|
||||
|
||||
} // namespace
|
||||
|
||||
Status DynamicHdf5ResultsWriter::Write(
|
||||
Status hdf5_internal::WriteDynamicFile(
|
||||
const std::filesystem::path& output_path, const Domain& domain,
|
||||
const DynamicAnalysisState& state,
|
||||
const std::vector<Diagnostic>& diagnostics) {
|
||||
const std::vector<Diagnostic>& diagnostics, const Mitc4FileHooks& hooks) {
|
||||
if (output_path.empty() || output_path.filename().empty() ||
|
||||
!state.IsInitialized() || state.AcceptedFrames().empty() ||
|
||||
domain.Dynamic() == nullptr)
|
||||
@@ -924,6 +935,11 @@ Status DynamicHdf5ResultsWriter::Write(
|
||||
"Dynamic output requires a named path, initialized state, "
|
||||
"and accepted frames.");
|
||||
try {
|
||||
for (const auto& frame : state.AcceptedFrames()) {
|
||||
hdf5_internal::ValidateDrillingEvidence(frame.global_equilibrium, frame.mitc4_drilling);
|
||||
hdf5_internal::RequireDrilling(frame.mitc4_drilling->mode ==
|
||||
state.AcceptedFrames().front().mitc4_drilling->mode);
|
||||
}
|
||||
hdf5_internal::Hdf5ErrorSilencer silence;
|
||||
const auto& step = *domain.Dynamic();
|
||||
Check(state.AcceptedFrames().size() ==
|
||||
@@ -965,24 +981,28 @@ Status DynamicHdf5ResultsWriter::Write(
|
||||
H5Fcreate(candidate_path.string().c_str(), H5F_ACC_EXCL,
|
||||
H5P_DEFAULT, H5P_DEFAULT),
|
||||
"Unable to create dynamic candidate."),
|
||||
H5Fclose};
|
||||
hooks.candidate_close};
|
||||
(void)hdf5_internal::CreateGroup(file.Get(), "/model/shell");
|
||||
hdf5_internal::WriteModel(file.Get(), domain, model);
|
||||
WriteMetadata(file.Get(), domain, state.AcceptedFrames().front());
|
||||
hdf5_internal::WriteDrillingMetadata(file.Get(), state.AcceptedFrames().front().mitc4_drilling->mode);
|
||||
WriteDensity(file.Get(), domain);
|
||||
(void)hdf5_internal::CreateGroup(file.Get(), "/steps");
|
||||
(void)hdf5_internal::CreateGroup(file.Get(), "/steps/Step-1");
|
||||
WriteAcceptedFrames(file.Get(), state.AcceptedFrames());
|
||||
for (const auto& frame : state.AcceptedFrames()) {
|
||||
WriteFrame(file.Get(), domain, frame);
|
||||
hdf5_internal::WriteDrillingFrame(file.Get(),
|
||||
"/steps/Step-1/frames/" + std::to_string(frame.frame_index), *frame.mitc4_drilling);
|
||||
}
|
||||
WriteDiagnostics(file.Get(), diagnostics);
|
||||
hdf5_internal::RequireHdf5(H5Fflush(file.Get(), H5F_SCOPE_GLOBAL),
|
||||
"Unable to flush dynamic candidate.");
|
||||
hdf5_internal::RequireHdf5(file.CloseChecked(),
|
||||
"Unable to close dynamic candidate.");
|
||||
if (hooks.before_self_check) hooks.before_self_check(candidate_path);
|
||||
const Status check =
|
||||
SelfCheck(candidate_path, domain, state, model, diagnostics);
|
||||
SelfCheck(candidate_path, domain, state, model, diagnostics, hooks.reader_close);
|
||||
const Status finalization = hdf5_internal::AtomicFinalizeValidatedCandidate(
|
||||
candidate_path, output_path, check);
|
||||
if (finalization.IsOk()) cleanup.Release();
|
||||
@@ -994,4 +1014,10 @@ Status DynamicHdf5ResultsWriter::Write(
|
||||
}
|
||||
}
|
||||
|
||||
Status DynamicHdf5ResultsWriter::Write(
|
||||
const std::filesystem::path& output_path, const Domain& domain,
|
||||
const DynamicAnalysisState& state, const std::vector<Diagnostic>& diagnostics) {
|
||||
return hdf5_internal::WriteDynamicFile(output_path, domain, state, diagnostics);
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
#include "io/hdf5/hdf5_raii.h"
|
||||
#include "math/finite_values.h"
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
#include "io/hdf5/mitc4_file_hooks.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
@@ -875,13 +879,14 @@ void WriteDiagnostics(hid_t file, const std::vector<Diagnostic>& diagnostics) {
|
||||
|
||||
} // namespace
|
||||
|
||||
Status EigenHdf5ResultsWriter::Write(
|
||||
Status hdf5_internal::WriteEigenFile(
|
||||
const std::filesystem::path& output_path, const Domain& domain,
|
||||
const EigenAnalysisState& state,
|
||||
const std::vector<Diagnostic>& diagnostics) {
|
||||
const std::vector<Diagnostic>& diagnostics, const Mitc4FileHooks& hooks) {
|
||||
const Status valid = Validate(domain, state, diagnostics);
|
||||
if (!valid.IsOk()) return valid;
|
||||
try {
|
||||
hdf5_internal::RequireDrilling(state.drilling_mode.has_value());
|
||||
hdf5_internal::Hdf5ErrorSilencer silence;
|
||||
const auto candidate_path = hdf5_internal::MakeCandidatePath(output_path);
|
||||
hdf5_internal::CandidateFileGuard cleanup{candidate_path};
|
||||
@@ -890,7 +895,7 @@ Status EigenHdf5ResultsWriter::Write(
|
||||
H5Fcreate(candidate_path.string().c_str(), H5F_ACC_EXCL,
|
||||
H5P_DEFAULT, H5P_DEFAULT),
|
||||
"Unable to create eigen candidate."),
|
||||
H5Fclose};
|
||||
hooks.candidate_close};
|
||||
hdf5_internal::WriterModelData model{
|
||||
{}, state.constraint_mask, state.prescribed_displacement};
|
||||
(void)hdf5_internal::CreateGroup(file.Get(), "/model");
|
||||
@@ -899,6 +904,7 @@ Status EigenHdf5ResultsWriter::Write(
|
||||
WriteDensities(file.Get(), domain);
|
||||
WriteDiagnostics(file.Get(), diagnostics);
|
||||
WriteEigenMetadata(file.Get());
|
||||
hdf5_internal::WriteDrillingMetadata(file.Get(), *state.drilling_mode);
|
||||
WriteStep(file.Get(), domain, state);
|
||||
(void)hdf5_internal::CreateGroup(file.Get(), kEigenRoot);
|
||||
WriteModes(file.Get(), state, domain.Frequency()->source_step_name_raw);
|
||||
@@ -940,8 +946,9 @@ Status EigenHdf5ResultsWriter::Write(
|
||||
"Unable to flush eigen candidate.");
|
||||
hdf5_internal::RequireHdf5(file.CloseChecked(),
|
||||
"Unable to close eigen candidate.");
|
||||
if (hooks.before_self_check) hooks.before_self_check(candidate_path);
|
||||
const Status checked = hdf5_internal::SelfCheckEigenFile(
|
||||
candidate_path, domain, state, diagnostics);
|
||||
candidate_path, domain, state, diagnostics, hooks.reader_close);
|
||||
const Status result = hdf5_internal::AtomicFinalizeValidatedCandidate(
|
||||
candidate_path, output_path, checked);
|
||||
if (result.IsOk()) cleanup.Release();
|
||||
@@ -953,4 +960,10 @@ Status EigenHdf5ResultsWriter::Write(
|
||||
}
|
||||
}
|
||||
|
||||
Status EigenHdf5ResultsWriter::Write(
|
||||
const std::filesystem::path& output_path, const Domain& domain,
|
||||
const EigenAnalysisState& state, const std::vector<Diagnostic>& diagnostics) {
|
||||
return hdf5_internal::WriteEigenFile(output_path, domain, state, diagnostics);
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#include "io/hdf5/hdf5_primitives.h"
|
||||
#include "io/hdf5/hdf5_raii.h"
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
namespace fesa::hdf5_internal {
|
||||
namespace {
|
||||
|
||||
@@ -726,13 +728,21 @@ bool CheckModalRows(hid_t file, const Domain& domain,
|
||||
|
||||
Status SelfCheckEigenFile(const std::filesystem::path& path,
|
||||
const Domain& domain, const EigenAnalysisState& state,
|
||||
const std::vector<Diagnostic>& diagnostics) {
|
||||
const std::vector<Diagnostic>& diagnostics,
|
||||
herr_t (*close_reader)(hid_t)) {
|
||||
try {
|
||||
Hdf5ErrorSilencer silence;
|
||||
Hdf5Handle file{RequireHdf5Id(H5Fopen(path.string().c_str(), H5F_ACC_RDONLY,
|
||||
H5P_DEFAULT),
|
||||
"Unable to reopen eigen candidate."),
|
||||
H5Fclose};
|
||||
close_reader};
|
||||
const auto drilling = ReadDrillingDiagnostics(file.Get());
|
||||
if (state.drilling_mode) {
|
||||
RequireDrilling(drilling.has_value() && drilling->formulation ==
|
||||
Mitc4DrillingModeName(*state.drilling_mode) && drilling->frames.empty());
|
||||
} else {
|
||||
RequireDrilling(!drilling.has_value());
|
||||
}
|
||||
for (const char* required :
|
||||
{"/metadata", "/model/nodes", "/model/elements",
|
||||
"/model/shell/densities", "/diagnostics", "/steps/Step-1/eigen/modes",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef FESA_IO_HDF5_EIGEN_HDF5_SELF_CHECK_H_
|
||||
#define FESA_IO_HDF5_EIGEN_HDF5_SELF_CHECK_H_
|
||||
|
||||
#include <hdf5.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
@@ -17,7 +19,8 @@ namespace fesa::hdf5_internal {
|
||||
/// @brief Reopens and validates a closed authoritative eigen HDF5 candidate.
|
||||
Status SelfCheckEigenFile(const std::filesystem::path& path,
|
||||
const Domain& domain, const EigenAnalysisState& state,
|
||||
const std::vector<Diagnostic>& diagnostics);
|
||||
const std::vector<Diagnostic>& diagnostics,
|
||||
herr_t (*close_reader)(hid_t) = H5Fclose);
|
||||
|
||||
} // namespace fesa::hdf5_internal
|
||||
|
||||
|
||||
@@ -12,12 +12,16 @@
|
||||
#include "io/hdf5/hdf5_result_writer.h"
|
||||
#include "io/hdf5/hdf5_self_check.h"
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
#include "io/hdf5/mitc4_file_hooks.h"
|
||||
|
||||
namespace fesa {
|
||||
|
||||
Status Hdf5ResultsWriter::Write(const std::filesystem::path& output_path,
|
||||
Status hdf5_internal::WriteStaticFile(const std::filesystem::path& output_path,
|
||||
const Domain& domain,
|
||||
const AnalysisState& state,
|
||||
const std::vector<Diagnostic>& diagnostics) {
|
||||
const std::vector<Diagnostic>& diagnostics, const Mitc4FileHooks& hooks) {
|
||||
auto plan_result =
|
||||
hdf5_internal::BuildWriterPlan(output_path, domain, state, diagnostics);
|
||||
if (!plan_result.HasValue()) {
|
||||
@@ -26,6 +30,8 @@ Status Hdf5ResultsWriter::Write(const std::filesystem::path& output_path,
|
||||
hdf5_internal::WriterPlan plan = std::move(plan_result.Value());
|
||||
|
||||
try {
|
||||
if (hdf5_internal::IsShellDomain(domain))
|
||||
hdf5_internal::ValidateDrillingEvidence(state.Equilibrium(), state.Mitc4Drilling());
|
||||
hdf5_internal::Hdf5ErrorSilencer silence_backend_errors;
|
||||
const std::filesystem::path candidate_path =
|
||||
hdf5_internal::MakeCandidatePath(output_path);
|
||||
@@ -35,7 +41,7 @@ Status Hdf5ResultsWriter::Write(const std::filesystem::path& output_path,
|
||||
H5Fcreate(candidate_path.string().c_str(), H5F_ACC_EXCL,
|
||||
H5P_DEFAULT, H5P_DEFAULT),
|
||||
"Unable to create the temporary HDF5 file."),
|
||||
H5Fclose};
|
||||
hooks.candidate_close};
|
||||
(void)hdf5_internal::CreateGroup(file.Get(), "/model");
|
||||
(void)hdf5_internal::CreateGroup(file.Get(),
|
||||
"/steps/Step-1/frames/0/nodal");
|
||||
@@ -50,14 +56,19 @@ Status Hdf5ResultsWriter::Write(const std::filesystem::path& output_path,
|
||||
}
|
||||
hdf5_internal::WriteModel(file.Get(), domain, plan.model_data);
|
||||
hdf5_internal::WriteResults(file.Get(), domain, state, diagnostics, plan);
|
||||
if (hdf5_internal::IsShellDomain(domain)) {
|
||||
hdf5_internal::WriteDrillingMetadata(file.Get(), state.Mitc4Drilling()->mode);
|
||||
hdf5_internal::WriteDrillingFrame(file.Get(), hdf5_internal::kStepRoot, *state.Mitc4Drilling());
|
||||
}
|
||||
hdf5_internal::RequireHdf5(H5Fflush(file.Get(), H5F_SCOPE_GLOBAL),
|
||||
"Unable to flush the temporary HDF5 file.");
|
||||
hdf5_internal::RequireHdf5(
|
||||
file.CloseChecked(),
|
||||
"Unable to close the temporary HDF5 file after writing.");
|
||||
|
||||
if (hooks.before_self_check) hooks.before_self_check(candidate_path);
|
||||
const Status self_check = hdf5_internal::SelfCheck(
|
||||
candidate_path, domain, state, diagnostics.size(), plan.model_data);
|
||||
candidate_path, domain, state, diagnostics.size(), plan.model_data, hooks.reader_close);
|
||||
const Status atomic = hdf5_internal::AtomicFinalizeValidatedCandidate(
|
||||
candidate_path, output_path, self_check);
|
||||
if (atomic.IsOk()) {
|
||||
@@ -71,4 +82,10 @@ Status Hdf5ResultsWriter::Write(const std::filesystem::path& output_path,
|
||||
}
|
||||
}
|
||||
|
||||
Status Hdf5ResultsWriter::Write(
|
||||
const std::filesystem::path& output_path, const Domain& domain,
|
||||
const AnalysisState& state, const std::vector<Diagnostic>& diagnostics) {
|
||||
return hdf5_internal::WriteStaticFile(output_path, domain, state, diagnostics);
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include "io/hdf5/hdf5_raii.h"
|
||||
#include "io/hdf5/hdf5_primitives.h"
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
namespace fesa::hdf5_internal {
|
||||
|
||||
Hdf5Handle OpenDatasetForCheck(const hid_t file, const char* path) {
|
||||
@@ -516,18 +518,30 @@ void RequireSchemaV1ShellIdentity(const hid_t file, const char* path,
|
||||
void SelfCheckFile(const std::filesystem::path& path, const Domain& domain,
|
||||
const AnalysisState& state,
|
||||
const std::size_t diagnostic_count,
|
||||
const WriterModelData& model_data) {
|
||||
const WriterModelData& model_data,
|
||||
herr_t (*close_reader)(hid_t)) {
|
||||
if (H5Fis_hdf5(path.string().c_str()) <= 0) {
|
||||
throw Hdf5Failure{"The temporary output is not an HDF5 file."};
|
||||
}
|
||||
Hdf5Handle file{
|
||||
RequireHdf5Id(H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
|
||||
"Unable to reopen the temporary HDF5 file read-only."),
|
||||
H5Fclose};
|
||||
close_reader};
|
||||
Hdf5Handle metadata{
|
||||
RequireHdf5Id(H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT),
|
||||
"The HDF5 metadata group is missing."),
|
||||
H5Gclose};
|
||||
if (IsShellDomain(domain)) {
|
||||
const auto drilling = ReadDrillingDiagnostics(file.Get());
|
||||
if (state.Mitc4Drilling()) {
|
||||
ValidateDrillingEvidence(state.Equilibrium(), state.Mitc4Drilling());
|
||||
RequireDrilling(drilling.has_value() && drilling->formulation ==
|
||||
Mitc4DrillingModeName(state.Mitc4Drilling()->mode) && drilling->frames.size() == 1U);
|
||||
CheckDrillingFrame(drilling->frames.front(), 0U, state.Equilibrium(), *state.Mitc4Drilling());
|
||||
} else {
|
||||
RequireDrilling(!drilling.has_value());
|
||||
}
|
||||
}
|
||||
RequireUint64Attribute(metadata.Get(), "schema_version", 0U);
|
||||
RequireStringAttribute(metadata.Get(), "solver_version",
|
||||
std::string{SolverVersion()});
|
||||
@@ -751,18 +765,36 @@ void SelfCheckFile(const std::filesystem::path& path, const Domain& domain,
|
||||
void NonlinearSelfCheckFile(const std::filesystem::path& path,
|
||||
const Domain& domain,
|
||||
const NonlinearAnalysisState& state,
|
||||
const std::size_t diagnostic_count) {
|
||||
const std::size_t diagnostic_count,
|
||||
herr_t (*close_reader)(hid_t)) {
|
||||
if (H5Fis_hdf5(path.string().c_str()) <= 0) {
|
||||
throw Hdf5Failure{"The nonlinear candidate is not an HDF5 file."};
|
||||
}
|
||||
Hdf5Handle file{
|
||||
RequireHdf5Id(H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
|
||||
"Unable to reopen nonlinear candidate read-only."),
|
||||
H5Fclose};
|
||||
close_reader};
|
||||
Hdf5Handle metadata{
|
||||
RequireHdf5Id(H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT),
|
||||
"The nonlinear metadata group is missing."),
|
||||
H5Gclose};
|
||||
RequireDrilling(!state.AcceptedFrames().empty());
|
||||
const auto drilling = ReadDrillingDiagnostics(file.Get());
|
||||
const auto& expected_drilling = state.AcceptedFrames().front().mitc4_drilling;
|
||||
if (expected_drilling) {
|
||||
RequireDrilling(drilling.has_value() && drilling->formulation ==
|
||||
Mitc4DrillingModeName(expected_drilling->mode) &&
|
||||
drilling->frames.size() == state.AcceptedFrames().size());
|
||||
for (std::size_t i = 0; i < drilling->frames.size(); ++i) {
|
||||
const auto& frame = state.AcceptedFrames()[i];
|
||||
ValidateDrillingEvidence(frame.global_equilibrium, frame.mitc4_drilling);
|
||||
RequireDrilling(frame.mitc4_drilling->mode == expected_drilling->mode);
|
||||
CheckDrillingFrame(drilling->frames[i], frame.frame_index, frame.global_equilibrium, *frame.mitc4_drilling);
|
||||
}
|
||||
} else {
|
||||
RequireDrilling(!drilling.has_value());
|
||||
for (const auto& frame : state.AcceptedFrames()) RequireDrilling(!frame.mitc4_drilling);
|
||||
}
|
||||
RequireUint64Attribute(metadata.Get(), "schema_version", 1U);
|
||||
RequireStringAttribute(metadata.Get(), "feature_id",
|
||||
"nonlinear-static-newton-raphson");
|
||||
@@ -919,9 +951,10 @@ void NonlinearSelfCheckFile(const std::filesystem::path& path,
|
||||
|
||||
Status SelfCheck(const std::filesystem::path& path, const Domain& domain,
|
||||
const AnalysisState& state, const std::size_t diagnostic_count,
|
||||
const WriterModelData& model_data) {
|
||||
const WriterModelData& model_data,
|
||||
herr_t (*close_reader)(hid_t)) {
|
||||
try {
|
||||
SelfCheckFile(path, domain, state, diagnostic_count, model_data);
|
||||
SelfCheckFile(path, domain, state, diagnostic_count, model_data, close_reader);
|
||||
return Status::Ok();
|
||||
} catch (const Hdf5Failure& failure) {
|
||||
return OutputFailure("hdf5-write-failure", failure.what());
|
||||
@@ -933,9 +966,10 @@ Status SelfCheck(const std::filesystem::path& path, const Domain& domain,
|
||||
Status NonlinearSelfCheck(const std::filesystem::path& path,
|
||||
const Domain& domain,
|
||||
const NonlinearAnalysisState& state,
|
||||
const std::size_t diagnostic_count) {
|
||||
const std::size_t diagnostic_count,
|
||||
herr_t (*close_reader)(hid_t)) {
|
||||
try {
|
||||
NonlinearSelfCheckFile(path, domain, state, diagnostic_count);
|
||||
NonlinearSelfCheckFile(path, domain, state, diagnostic_count, close_reader);
|
||||
return Status::Ok();
|
||||
} catch (const Hdf5Failure& failure) {
|
||||
return OutputFailure("hdf5-write-failure", failure.what());
|
||||
|
||||
@@ -15,13 +15,15 @@ namespace fesa::hdf5_internal {
|
||||
/// @brief Reopens and validates the complete closed schema-v0 candidate.
|
||||
Status SelfCheck(const std::filesystem::path& path, const Domain& domain,
|
||||
const AnalysisState& state, std::size_t diagnostic_count,
|
||||
const WriterModelData& model_data);
|
||||
const WriterModelData& model_data,
|
||||
herr_t (*close_reader)(hid_t) = H5Fclose);
|
||||
|
||||
/// @brief Reopens and validates the complete closed schema-v1 candidate.
|
||||
Status NonlinearSelfCheck(const std::filesystem::path& path,
|
||||
const Domain& domain,
|
||||
const NonlinearAnalysisState& state,
|
||||
std::size_t diagnostic_count);
|
||||
std::size_t diagnostic_count,
|
||||
herr_t (*close_reader)(hid_t) = H5Fclose);
|
||||
|
||||
} // namespace fesa::hdf5_internal
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#ifndef FESA_IO_HDF5_MITC4_DRILLING_DIAGNOSTICS_H_
|
||||
#define FESA_IO_HDF5_MITC4_DRILLING_DIAGNOSTICS_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/results/result_records.h"
|
||||
#include "io/hdf5/hdf5_primitives.h"
|
||||
|
||||
namespace fesa::hdf5_internal {
|
||||
|
||||
struct StoredDrillingFrame {
|
||||
std::string storage_step;
|
||||
std::uint64_t frame_index;
|
||||
std::array<double, 6> raw_equilibrium;
|
||||
std::array<double, 3> drilling_moment;
|
||||
std::array<double, 6> equilibrium_with_drilling;
|
||||
};
|
||||
|
||||
struct StoredDrillingDiagnostics {
|
||||
std::string formulation;
|
||||
std::uint64_t version;
|
||||
std::vector<StoredDrillingFrame> frames;
|
||||
};
|
||||
|
||||
inline void RequireDrilling(bool valid) {
|
||||
if (!valid) throw Hdf5Failure{"Invalid MITC4 drilling diagnostics contract."};
|
||||
}
|
||||
|
||||
inline void ValidateDrillingEvidence(
|
||||
const std::array<double, 6>& raw,
|
||||
const std::optional<Mitc4DrillingEvidence>& evidence) {
|
||||
RequireDrilling(evidence.has_value());
|
||||
RequireDrilling(Mitc4DrillingModeName(evidence->mode)[0] != '\0' &&
|
||||
IsFinite(raw) && IsFinite(evidence->drilling_moment) &&
|
||||
IsFinite(evidence->equilibrium_with_drilling));
|
||||
for (std::size_t i = 0; i < 6U; ++i) {
|
||||
RequireDrilling(evidence->equilibrium_with_drilling[i] ==
|
||||
(i < 3U ? raw[i] : raw[i] - evidence->drilling_moment[i - 3U]));
|
||||
}
|
||||
}
|
||||
|
||||
inline void WriteDrillingMetadata(hid_t file, Mitc4DrillingMode mode) {
|
||||
RequireDrilling(Mitc4DrillingModeName(mode)[0] != '\0');
|
||||
Hdf5Handle metadata{RequireHdf5Id(H5Gopen2(file, "/metadata", H5P_DEFAULT),
|
||||
"Missing drilling metadata group."), H5Gclose};
|
||||
WriteStringAttribute(metadata.Get(), "mitc4_drilling_formulation",
|
||||
Mitc4DrillingModeName(mode));
|
||||
WriteUint64Attribute(metadata.Get(), "drilling_diagnostics_version", 1U);
|
||||
}
|
||||
|
||||
inline void WriteDrillingFrame(hid_t file, const std::string& root,
|
||||
const Mitc4DrillingEvidence& evidence) {
|
||||
WriteModelDoubleDataset(file, root + "/global/drilling_moment", {3U},
|
||||
evidence.drilling_moment.data(), 3U, "MDRILL_X,MDRILL_Y,MDRILL_Z",
|
||||
"force*length,force*length,force*length", "global-cartesian", "global-origin");
|
||||
WriteModelDoubleDataset(file, root + "/global/equilibrium_with_drilling", {6U},
|
||||
evidence.equilibrium_with_drilling.data(), 6U,
|
||||
"FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3",
|
||||
"force,force,force,force*length,force*length,force*length",
|
||||
"global-cartesian", "global-origin");
|
||||
}
|
||||
|
||||
inline std::string ReadDrillingString(hid_t object, const char* name) {
|
||||
Hdf5Handle attribute{RequireHdf5Id(H5Aopen(object, name, H5P_DEFAULT),
|
||||
"Missing drilling string."), H5Aclose};
|
||||
Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
|
||||
Hdf5Handle space{H5Aget_space(attribute.Get()), H5Sclose};
|
||||
RequireDrilling(H5Sget_simple_extent_type(space.Get()) == H5S_SCALAR &&
|
||||
H5Tget_class(type.Get()) == H5T_STRING &&
|
||||
H5Tis_variable_str(type.Get()) > 0 &&
|
||||
H5Tget_cset(type.Get()) == H5T_CSET_UTF8);
|
||||
char* value = nullptr;
|
||||
RequireHdf5(H5Aread(attribute.Get(), type.Get(), &value), "Cannot read drilling string.");
|
||||
const std::string result = value == nullptr ? "" : value;
|
||||
if (value != nullptr) H5free_memory(value);
|
||||
RequireDrilling(IsValidUtf8(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
template <std::size_t N>
|
||||
inline std::array<double, N> ReadDrillingValues(hid_t file, const std::string& path) {
|
||||
Hdf5Handle dataset{RequireHdf5Id(H5Dopen2(file, path.c_str(), H5P_DEFAULT),
|
||||
"Missing drilling dataset."), H5Dclose};
|
||||
Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose};
|
||||
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
|
||||
hsize_t dimension = 0U;
|
||||
RequireDrilling(H5Tequal(type.Get(), H5T_IEEE_F64LE) > 0 &&
|
||||
H5Sget_simple_extent_ndims(space.Get()) == 1 &&
|
||||
H5Sget_simple_extent_dims(space.Get(), &dimension, nullptr) >= 0 &&
|
||||
dimension == N);
|
||||
std::array<double, N> values{};
|
||||
RequireHdf5(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
|
||||
H5P_DEFAULT, values.data()), "Cannot read drilling dataset.");
|
||||
RequireDrilling(IsFinite(values));
|
||||
return values;
|
||||
}
|
||||
|
||||
// Markerless historical files retain their old contract; no mode is inferred.
|
||||
inline std::optional<StoredDrillingDiagnostics> ReadDrillingDiagnostics(hid_t file) {
|
||||
Hdf5Handle metadata{RequireHdf5Id(H5Gopen2(file, "/metadata", H5P_DEFAULT),
|
||||
"Missing drilling metadata group."), H5Gclose};
|
||||
const htri_t mode = H5Aexists(metadata.Get(), "mitc4_drilling_formulation");
|
||||
const htri_t version = H5Aexists(metadata.Get(), "drilling_diagnostics_version");
|
||||
RequireDrilling(mode >= 0 && version >= 0 && (mode > 0) == (version > 0));
|
||||
if (mode == 0) return std::nullopt;
|
||||
StoredDrillingDiagnostics result{};
|
||||
result.formulation = ReadDrillingString(metadata.Get(), "mitc4_drilling_formulation");
|
||||
RequireDrilling(result.formulation == Mitc4DrillingModeName(Mitc4DrillingMode::kDirectSpring) ||
|
||||
result.formulation == Mitc4DrillingModeName(Mitc4DrillingMode::kLegacyCorotatedTwist));
|
||||
Hdf5Handle attribute{H5Aopen(metadata.Get(), "drilling_diagnostics_version", H5P_DEFAULT), H5Aclose};
|
||||
Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
|
||||
Hdf5Handle space{H5Aget_space(attribute.Get()), H5Sclose};
|
||||
RequireDrilling(H5Tequal(type.Get(), H5T_STD_U64LE) > 0 &&
|
||||
H5Sget_simple_extent_type(space.Get()) == H5S_SCALAR);
|
||||
RequireHdf5(H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &result.version), "Cannot read drilling version.");
|
||||
RequireDrilling(result.version == 1U);
|
||||
if (H5Lexists(file, "/steps/Step-1/frames", H5P_DEFAULT) > 0) {
|
||||
Hdf5Handle frames{H5Gopen2(file, "/steps/Step-1/frames", H5P_DEFAULT), H5Gclose};
|
||||
H5G_info_t info{};
|
||||
RequireHdf5(H5Gget_info(frames.Get(), &info), "Cannot inspect drilling frames.");
|
||||
for (hsize_t i = 0; i < info.nlinks; ++i) {
|
||||
const auto size = H5Lget_name_by_idx(frames.Get(), ".", H5_INDEX_NAME, H5_ITER_INC,
|
||||
i, nullptr, 0U, H5P_DEFAULT);
|
||||
RequireDrilling(size > 0);
|
||||
std::vector<char> name(static_cast<std::size_t>(size) + 1U);
|
||||
RequireDrilling(H5Lget_name_by_idx(frames.Get(), ".", H5_INDEX_NAME, H5_ITER_INC,
|
||||
i, name.data(), name.size(), H5P_DEFAULT) == size);
|
||||
const std::string token{name.data()};
|
||||
const std::string root = "/steps/Step-1/frames/" + token + "/global/";
|
||||
// Each new MITC4 static/dynamic frame has an existing raw equilibrium.
|
||||
StoredDrillingFrame frame{};
|
||||
frame.storage_step = "Step-1";
|
||||
RequireDrilling(!token.empty() && token.find_first_not_of("0123456789") == std::string::npos);
|
||||
frame.frame_index = std::stoull(token);
|
||||
RequireDrilling(std::to_string(frame.frame_index) == token);
|
||||
frame.raw_equilibrium = ReadDrillingValues<6>(file, root + "equilibrium");
|
||||
frame.drilling_moment = ReadDrillingValues<3>(file, root + "drilling_moment");
|
||||
frame.equilibrium_with_drilling = ReadDrillingValues<6>(file, root + "equilibrium_with_drilling");
|
||||
for (std::size_t j = 0; j < 6U; ++j)
|
||||
RequireDrilling(frame.equilibrium_with_drilling[j] == (j < 3U ? frame.raw_equilibrium[j] :
|
||||
frame.raw_equilibrium[j] - frame.drilling_moment[j - 3U]));
|
||||
result.frames.push_back(frame);
|
||||
}
|
||||
std::sort(result.frames.begin(), result.frames.end(),
|
||||
[](const auto& left, const auto& right) { return left.frame_index < right.frame_index; });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inline StoredDrillingDiagnostics RequireDrillingDiagnostics(hid_t file, Mitc4DrillingMode mode) {
|
||||
const auto result = ReadDrillingDiagnostics(file);
|
||||
RequireDrilling(result.has_value() && result->formulation == Mitc4DrillingModeName(mode));
|
||||
return *result;
|
||||
}
|
||||
|
||||
inline void CheckDrillingFrame(const StoredDrillingFrame& stored,
|
||||
std::uint64_t index,
|
||||
const std::array<double, 6>& raw,
|
||||
const Mitc4DrillingEvidence& evidence) {
|
||||
RequireDrilling(stored.frame_index == index && stored.raw_equilibrium == raw &&
|
||||
stored.drilling_moment == evidence.drilling_moment &&
|
||||
stored.equilibrium_with_drilling == evidence.equilibrium_with_drilling);
|
||||
}
|
||||
|
||||
} // namespace fesa::hdf5_internal
|
||||
|
||||
#endif // FESA_IO_HDF5_MITC4_DRILLING_DIAGNOSTICS_H_
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef FESA_IO_HDF5_MITC4_FILE_HOOKS_H_
|
||||
#define FESA_IO_HDF5_MITC4_FILE_HOOKS_H_
|
||||
|
||||
#include <hdf5.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
|
||||
namespace fesa {
|
||||
class Domain;
|
||||
class AnalysisState;
|
||||
class NonlinearAnalysisState;
|
||||
class DynamicAnalysisState;
|
||||
struct EigenAnalysisState;
|
||||
struct Diagnostic;
|
||||
|
||||
namespace hdf5_internal {
|
||||
|
||||
// Private lifecycle fault seams. Production uses real closers and no callback.
|
||||
// Injected closers must release the handle even when reporting failure.
|
||||
struct Mitc4FileHooks {
|
||||
herr_t (*candidate_close)(hid_t) = H5Fclose;
|
||||
herr_t (*reader_close)(hid_t) = H5Fclose;
|
||||
std::function<void(const std::filesystem::path&)> before_self_check;
|
||||
};
|
||||
|
||||
Status WriteStaticFile(const std::filesystem::path&, const Domain&,
|
||||
const AnalysisState&, const std::vector<Diagnostic>&, const Mitc4FileHooks& = {});
|
||||
Status WriteNonlinearFile(const std::filesystem::path&, const Domain&,
|
||||
const NonlinearAnalysisState&, const std::vector<Diagnostic>&, const Mitc4FileHooks& = {});
|
||||
Status WriteDynamicFile(const std::filesystem::path&, const Domain&,
|
||||
const DynamicAnalysisState&, const std::vector<Diagnostic>&, const Mitc4FileHooks& = {});
|
||||
Status WriteEigenFile(const std::filesystem::path&, const Domain&,
|
||||
const EigenAnalysisState&, const std::vector<Diagnostic>&, const Mitc4FileHooks& = {});
|
||||
|
||||
} // namespace hdf5_internal
|
||||
} // namespace fesa
|
||||
|
||||
#endif // FESA_IO_HDF5_MITC4_FILE_HOOKS_H_
|
||||
@@ -22,6 +22,10 @@
|
||||
#include "math/finite_values.h"
|
||||
#include "results/shell_result_recovery.h"
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
#include "io/hdf5/mitc4_file_hooks.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
@@ -955,10 +959,10 @@ Status ValidateDiagnostics(const std::vector<Diagnostic>& diagnostics) {
|
||||
|
||||
} // namespace
|
||||
|
||||
Status NonlinearHdf5ResultsWriter::Write(
|
||||
Status hdf5_internal::WriteNonlinearFile(
|
||||
const std::filesystem::path& output_path, const Domain& domain,
|
||||
const NonlinearAnalysisState& state,
|
||||
const std::vector<Diagnostic>& diagnostics) {
|
||||
const std::vector<Diagnostic>& diagnostics, const Mitc4FileHooks& hooks) {
|
||||
if (output_path.empty() || output_path.filename().empty()) {
|
||||
return Failure("invalid-output-path", "The HDF5 output path must name a file.");
|
||||
}
|
||||
@@ -981,6 +985,11 @@ Status NonlinearHdf5ResultsWriter::Write(
|
||||
}
|
||||
|
||||
try {
|
||||
for (const auto& frame : state.AcceptedFrames()) {
|
||||
hdf5_internal::ValidateDrillingEvidence(frame.global_equilibrium, frame.mitc4_drilling);
|
||||
hdf5_internal::RequireDrilling(frame.mitc4_drilling->mode ==
|
||||
state.AcceptedFrames().front().mitc4_drilling->mode);
|
||||
}
|
||||
hdf5_internal::Hdf5ErrorSilencer silence_backend_errors;
|
||||
const std::filesystem::path candidate_path =
|
||||
hdf5_internal::MakeCandidatePath(output_path);
|
||||
@@ -989,25 +998,29 @@ Status NonlinearHdf5ResultsWriter::Write(
|
||||
H5Fcreate(candidate_path.string().c_str(), H5F_ACC_EXCL,
|
||||
H5P_DEFAULT, H5P_DEFAULT),
|
||||
"Unable to create nonlinear temporary HDF5 file."),
|
||||
H5Fclose};
|
||||
hooks.candidate_close};
|
||||
(void)hdf5_internal::CreateGroup(file.Get(), "/model");
|
||||
(void)hdf5_internal::CreateGroup(file.Get(), "/model/shell");
|
||||
(void)hdf5_internal::CreateGroup(file.Get(), "/steps/Step-1");
|
||||
WriteMetadata(file.Get(), domain);
|
||||
hdf5_internal::WriteDrillingMetadata(file.Get(), state.AcceptedFrames().front().mitc4_drilling->mode);
|
||||
WriteNodes(file.Get(), domain);
|
||||
WriteElements(file.Get(), domain);
|
||||
WriteModelData(file.Get(), domain, model_data);
|
||||
WriteAcceptedFrameTable(file.Get(), state.AcceptedFrames());
|
||||
for (const auto& frame : state.AcceptedFrames()) {
|
||||
WriteFrame(file.Get(), domain, frame);
|
||||
hdf5_internal::WriteDrillingFrame(file.Get(),
|
||||
"/steps/Step-1/frames/" + std::to_string(frame.frame_index), *frame.mitc4_drilling);
|
||||
}
|
||||
WriteDiagnostics(file.Get(), diagnostics);
|
||||
hdf5_internal::RequireHdf5(H5Fflush(file.Get(), H5F_SCOPE_GLOBAL),
|
||||
"Unable to flush nonlinear temporary HDF5 file.");
|
||||
hdf5_internal::RequireHdf5(
|
||||
file.CloseChecked(), "Unable to close nonlinear temporary HDF5 file.");
|
||||
if (hooks.before_self_check) hooks.before_self_check(candidate_path);
|
||||
const Status self_check = hdf5_internal::NonlinearSelfCheck(
|
||||
candidate_path, domain, state, diagnostics.size());
|
||||
candidate_path, domain, state, diagnostics.size(), hooks.reader_close);
|
||||
const Status atomic = hdf5_internal::AtomicFinalizeValidatedCandidate(
|
||||
candidate_path, output_path, self_check);
|
||||
if (atomic.IsOk()) {
|
||||
@@ -1021,4 +1034,10 @@ Status NonlinearHdf5ResultsWriter::Write(
|
||||
}
|
||||
}
|
||||
|
||||
Status NonlinearHdf5ResultsWriter::Write(
|
||||
const std::filesystem::path& output_path, const Domain& domain,
|
||||
const NonlinearAnalysisState& state, const std::vector<Diagnostic>& diagnostics) {
|
||||
return hdf5_internal::WriteNonlinearFile(output_path, domain, state, diagnostics);
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
@@ -48,7 +48,8 @@ Status CommitRecoveryCandidate(RecoveryCandidate candidate,
|
||||
!candidate.shell.rows.empty() ||
|
||||
candidate.shell.physical_strain_energy != 0.0 ||
|
||||
candidate.shell.equilibrium != zero_equilibrium ||
|
||||
candidate.shell.verification_metrics != zero_metrics;
|
||||
candidate.shell.verification_metrics != zero_metrics ||
|
||||
candidate.shell.mitc4_drilling.has_value();
|
||||
const bool has_complete_shell_inventory =
|
||||
!candidate.expected_shell_elements.empty() &&
|
||||
!candidate.shell.rows.empty();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/math/vector3.h"
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
|
||||
namespace fesa::results_internal {
|
||||
namespace {
|
||||
@@ -243,11 +244,24 @@ Status RecoverShellGlobalEvidence(const Domain& domain, const DofManager& dofs,
|
||||
candidate.shell.equilibrium[component] = force_balance[component];
|
||||
candidate.shell.equilibrium[3U + component] = moment_balance[component];
|
||||
}
|
||||
Vector3 acceptance_moment = moment_balance;
|
||||
if (candidate.shell.mitc4_drilling) {
|
||||
CompleteDrillingEvidence(candidate.shell.equilibrium,
|
||||
&*candidate.shell.mitc4_drilling);
|
||||
if (!ValidDrillingEvidence(candidate.shell.mitc4_drilling,
|
||||
candidate.shell.equilibrium))
|
||||
return RecoveryFailure("invalid-shell-drilling-evidence",
|
||||
{domain.SourcePath(), 0U}, "global-equilibrium",
|
||||
"Shell drilling diagnostics must be finite and consistent.");
|
||||
if (candidate.shell.mitc4_drilling->mode == Mitc4DrillingMode::kDirectSpring)
|
||||
acceptance_moment = moment_balance -
|
||||
Vector3(candidate.shell.mitc4_drilling->drilling_moment);
|
||||
}
|
||||
const double force_metric =
|
||||
NormalizedBalance(force_balance.Components(),
|
||||
(std::max)(applied_force_scale, reaction_force_scale));
|
||||
const double moment_metric = NormalizedBalance(
|
||||
moment_balance.Components(),
|
||||
acceptance_moment.Components(),
|
||||
(std::max)(applied_moment_scale, reaction_moment_scale));
|
||||
candidate.shell.verification_metrics = {global.normalized_free_residual,
|
||||
force_metric, moment_metric};
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef FESA_RESULTS_MITC4_DRILLING_EVIDENCE_H_
|
||||
#define FESA_RESULTS_MITC4_DRILLING_EVIDENCE_H_
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
|
||||
#include "fesa/results/result_records.h"
|
||||
|
||||
namespace fesa::results_internal {
|
||||
|
||||
inline bool ValidDrillingMode(const Mitc4DrillingMode mode) {
|
||||
return mode == Mitc4DrillingMode::kDirectSpring ||
|
||||
mode == Mitc4DrillingMode::kLegacyCorotatedTwist;
|
||||
}
|
||||
|
||||
/// @brief Validates exact raw-minus-internal diagnostics without a tolerance.
|
||||
inline bool ValidDrillingEvidence(
|
||||
const std::optional<Mitc4DrillingEvidence>& evidence,
|
||||
const std::array<double, 6>& raw) {
|
||||
if (!evidence) return true; // Older manually constructed fixtures.
|
||||
if (!ValidDrillingMode(evidence->mode)) return false;
|
||||
for (std::size_t i = 0U; i < 6U; ++i) {
|
||||
const double expected = i < 3U ? raw[i]
|
||||
: raw[i] - evidence->drilling_moment[i - 3U];
|
||||
if (!std::isfinite(raw[i]) || !std::isfinite(expected) ||
|
||||
!std::isfinite(evidence->equilibrium_with_drilling[i]) ||
|
||||
evidence->equilibrium_with_drilling[i] != expected)
|
||||
return false;
|
||||
}
|
||||
for (const double value : evidence->drilling_moment)
|
||||
if (!std::isfinite(value)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool SameDrillingInventory(
|
||||
const std::optional<Mitc4DrillingEvidence>& left,
|
||||
const std::optional<Mitc4DrillingEvidence>& right) {
|
||||
return left.has_value() == right.has_value() &&
|
||||
(!left || left->mode == right->mode);
|
||||
}
|
||||
|
||||
/// @brief Keeps force balance raw and subtracts positive artificial moment once.
|
||||
inline void CompleteDrillingEvidence(
|
||||
const std::array<double, 6>& raw, Mitc4DrillingEvidence* evidence) {
|
||||
evidence->equilibrium_with_drilling = raw;
|
||||
for (std::size_t i = 0U; i < 3U; ++i)
|
||||
evidence->equilibrium_with_drilling[3U + i] -= evidence->drilling_moment[i];
|
||||
}
|
||||
|
||||
} // namespace fesa::results_internal
|
||||
|
||||
#endif // FESA_RESULTS_MITC4_DRILLING_EVIDENCE_H_
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
|
||||
#include "results/mitc4_drilling_evidence.h"
|
||||
|
||||
namespace fesa::results_internal {
|
||||
|
||||
bool IsFiniteShellRow(const ShellResultRow& row) {
|
||||
@@ -29,6 +31,27 @@ Status ShellResultRecovery(const SourceLocation& location,
|
||||
const EntityIndex expected_element,
|
||||
const ShellElementResultRows& rows,
|
||||
RecoveryCandidate& candidate) {
|
||||
if ((rows.drilling_mode && !ValidDrillingMode(*rows.drilling_mode)) ||
|
||||
!IsFiniteArray(rows.drilling_moment) ||
|
||||
(candidate.has_shell_results &&
|
||||
(candidate.shell.mitc4_drilling.has_value() != rows.drilling_mode.has_value() ||
|
||||
(rows.drilling_mode &&
|
||||
candidate.shell.mitc4_drilling->mode != *rows.drilling_mode)))) {
|
||||
return RecoveryFailure("invalid-shell-drilling-evidence", location,
|
||||
source_id.source_label_text,
|
||||
"Shell recovery requires one finite actual drilling mode.");
|
||||
}
|
||||
if (rows.drilling_mode) {
|
||||
if (!candidate.shell.mitc4_drilling)
|
||||
candidate.shell.mitc4_drilling = Mitc4DrillingEvidence{*rows.drilling_mode};
|
||||
for (std::size_t i = 0U; i < 3U; ++i) {
|
||||
candidate.shell.mitc4_drilling->drilling_moment[i] += rows.drilling_moment[i];
|
||||
if (!std::isfinite(candidate.shell.mitc4_drilling->drilling_moment[i]))
|
||||
return RecoveryFailure("nonfinite-recovery-value", location,
|
||||
source_id.source_label_text,
|
||||
"Source-ordered drilling moment overflowed.");
|
||||
}
|
||||
}
|
||||
const double accumulated_energy =
|
||||
candidate.shell.physical_strain_energy + rows.physical_strain_energy;
|
||||
if (!std::isfinite(rows.physical_strain_energy) ||
|
||||
|
||||
+11
-8
@@ -14,6 +14,8 @@ add_executable(
|
||||
unit/analysis/hht_energy_verification_test.cpp
|
||||
unit/analysis/hht_temporal_verification_test.cpp
|
||||
unit/analysis/hht_dynamic_algebra_test.cpp
|
||||
unit/analysis/hht_development_probe_test.cpp
|
||||
unit/analysis/hht_field_convergence_test.cpp
|
||||
unit/analysis/hht_reduced_shell_test.cpp
|
||||
unit/analysis/hht_dynamic_procedure_test.cpp
|
||||
unit/analysis/nonlinear_analysis_state_test.cpp
|
||||
@@ -99,6 +101,9 @@ target_include_directories(
|
||||
PRIVATE
|
||||
"${PROJECT_SOURCE_DIR}/src/fesa"
|
||||
)
|
||||
if(FESA_HHT_DEVELOPMENT_PROBE)
|
||||
target_compile_definitions(fesa_unit_tests PRIVATE FESA_HHT_DEVELOPMENT_PROBE=1)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
fesa_unit_tests
|
||||
@@ -157,6 +162,8 @@ add_executable(
|
||||
reference/thermal_reference_cases_test.cpp
|
||||
)
|
||||
|
||||
target_include_directories(fesa_reference_tests PRIVATE ${PROJECT_SOURCE_DIR}/src/fesa)
|
||||
|
||||
target_link_libraries(
|
||||
fesa_reference_tests
|
||||
PRIVATE
|
||||
@@ -173,11 +180,10 @@ target_compile_definitions(
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
if(NOT TARGET MKL::mkl_intel_thread OR NOT TARGET MKL::mkl_core OR
|
||||
NOT OMP_DLL_DIR OR NOT OMP_DLLNAME)
|
||||
if(NOT TARGET ${FESA_MKL_THREAD_TARGET} OR NOT TARGET MKL::mkl_core)
|
||||
message(FATAL_ERROR "The oneMKL runtime files required by tests were not resolved")
|
||||
endif()
|
||||
if(NOT EXISTS "${OMP_DLL_DIR}/libmmd.dll")
|
||||
if(NOT EXISTS "${FESA_INTEL_MATH_RUNTIME}")
|
||||
message(FATAL_ERROR "The Intel runtime required by the selected HDF5 DLL was not resolved")
|
||||
endif()
|
||||
|
||||
@@ -198,16 +204,13 @@ if(WIN32)
|
||||
"$<TARGET_FILE:TBB::tbb>"
|
||||
"$<TARGET_FILE_DIR:${target_name}>"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"$<TARGET_FILE:MKL::mkl_intel_thread>"
|
||||
"$<TARGET_FILE:${FESA_MKL_THREAD_TARGET}>"
|
||||
"$<TARGET_FILE_DIR:${target_name}>"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"$<TARGET_FILE:MKL::mkl_core>"
|
||||
"$<TARGET_FILE_DIR:${target_name}>"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${OMP_DLL_DIR}/${OMP_DLLNAME}"
|
||||
"$<TARGET_FILE_DIR:${target_name}>"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${OMP_DLL_DIR}/libmmd.dll"
|
||||
"${FESA_INTEL_MATH_RUNTIME}"
|
||||
"$<TARGET_FILE_DIR:${target_name}>"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${_fesa_mkl_dispatch_dll}"
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
#include "fesa/analysis/prepared_analysis.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/assembly/sparse_assembler.h"
|
||||
#include "fesa/elements/element_factory.h"
|
||||
#include "fesa/results/result_recovery.h"
|
||||
#include "elements/mitc4_drilling_internal.h"
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
#include "fesa/model/domain.h"
|
||||
#include "fesa/model/model_types.h"
|
||||
@@ -132,6 +135,8 @@ TEST(LinearFrequencyProcedureExecutor,
|
||||
fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value());
|
||||
ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus());
|
||||
const auto& state = result.Value();
|
||||
ASSERT_TRUE(state.drilling_mode);
|
||||
EXPECT_EQ(*state.drilling_mode, fesa::Mitc4DrillingMode::kDirectSpring);
|
||||
ASSERT_EQ(state.modes.size(), 1U);
|
||||
EXPECT_EQ(state.mass_null_dimension, 0U);
|
||||
EXPECT_EQ(state.finite_dimension, 1U);
|
||||
@@ -189,10 +194,10 @@ TEST(LinearFrequencyProcedureExecutor,
|
||||
}
|
||||
}
|
||||
|
||||
// E2-C09 is a pencil oracle. The public static contract conflict is recorded
|
||||
// separately; direct covariance does not certify spatial moment equilibrium.
|
||||
// E2-C09 keeps raw spatial imbalance visible while the direct-spring gate
|
||||
// subtracts only the separately recovered artificial internal moment.
|
||||
TEST(LinearFrequencyProcedureExecutor,
|
||||
PartialBoundaryStaticRejectsUnbalancedDrillingMoment) {
|
||||
PartialBoundaryStaticSeparatesArtificialMomentAndPreservesLegacyGate) {
|
||||
const auto domain = PanelDomain(false, false, true);
|
||||
ASSERT_TRUE(domain.HasValue());
|
||||
const fesa::SerialParallelFor serial;
|
||||
@@ -200,9 +205,32 @@ TEST(LinearFrequencyProcedureExecutor,
|
||||
const auto result =
|
||||
fesa::LinearStaticProcedureExecutor{serial, solver}.Execute(
|
||||
domain.Value());
|
||||
ASSERT_FALSE(result.HasValue());
|
||||
EXPECT_NE(StatusText(result.GetStatus())
|
||||
.find("global-equilibrium-tolerance-failure"),
|
||||
ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus());
|
||||
const auto& state = result.Value().state;
|
||||
ASSERT_TRUE(state.Mitc4Drilling());
|
||||
EXPECT_NE(state.Equilibrium()[5U], 0.0);
|
||||
for (std::size_t c = 0U; c < 3U; ++c) {
|
||||
EXPECT_NEAR(state.Equilibrium()[3U + c], state.Mitc4Drilling()->drilling_moment[c], 1e-14);
|
||||
EXPECT_NEAR(state.Mitc4Drilling()->equilibrium_with_drilling[3U + c], 0.0, 1e-14);
|
||||
}
|
||||
auto model = fesa::AnalysisModel::Create(domain.Value());
|
||||
ASSERT_TRUE(model.HasValue());
|
||||
auto dofs = fesa::DofManager::Create(model.Value());
|
||||
ASSERT_TRUE(dofs.HasValue());
|
||||
auto element = fesa::ElementFactory{}.Create(domain.Value().Elements()[0U], domain.Value());
|
||||
ASSERT_TRUE(element.HasValue());
|
||||
auto* shell = dynamic_cast<fesa::Mitc4Shell*>(element.Value().get());
|
||||
ASSERT_NE(shell, nullptr);
|
||||
*shell = fesa::mitc4_drilling_internal::TestAccess::WithMode(
|
||||
*shell, fesa::Mitc4DrillingMode::kLegacyCorotatedTwist);
|
||||
fesa::ElementView view{std::cref(*shell)};
|
||||
auto stiffness = fesa::SparseAssembler::Assemble(view, dofs.Value(), serial);
|
||||
ASSERT_TRUE(stiffness.HasValue());
|
||||
auto legacy_state = fesa::AnalysisState::Create(dofs.Value(), {"Step-1", 0U});
|
||||
const auto legacy = fesa::ResultRecovery::Recover(model.Value(), view, dofs.Value(),
|
||||
stiffness.Value(), state.Displacement(), state.ExternalForce(), legacy_state);
|
||||
ASSERT_FALSE(legacy.IsOk());
|
||||
EXPECT_NE(StatusText(legacy).find("global-equilibrium-tolerance-failure"),
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/elements/finite_rotation.h"
|
||||
#include "fesa/analysis/prepared_analysis.h"
|
||||
#include "fesa/analysis/nonlinear_static_procedure.h"
|
||||
#include "fesa/analysis/newton_iteration_log.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/elements/element_factory.h"
|
||||
#include "fesa/elements/mitc4_shell.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
|
||||
#include "fesa/math/vector3.h"
|
||||
|
||||
namespace {
|
||||
@@ -343,3 +350,57 @@ TEST(NonlinearStaticProcedure,
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(Mitc4DrillingEvidence, NonlinearAndPressureFramesRetainSignedTorqueOracle) {
|
||||
for (const bool pressure : {false, true}) {
|
||||
SCOPED_TRACE(pressure);
|
||||
TempDirectory directory{"drilling"};
|
||||
auto deck = AllConstrainedNonlinearShellDeck();
|
||||
const auto boundary = deck.find("All, 1, 6");
|
||||
ASSERT_NE(boundary, std::string::npos);
|
||||
deck.replace(boundary, std::string{"All, 1, 6"}.size(), "All, 1, 5\nAll, 6, 6, 0.125");
|
||||
if (pressure) {
|
||||
const auto assembly_end = deck.find("*End Assembly");
|
||||
deck.insert(assembly_end, "*Surface, TYPE=ELEMENT, NAME=Pressure\nShell-1.1, SPOS\n");
|
||||
deck.insert(deck.find("*End Step"), "*Dsload\nPressure, P, 4.0\n");
|
||||
}
|
||||
const auto path = directory.Path() / "drilling.inp";
|
||||
WriteText(path, deck);
|
||||
auto prepared = fesa::PrepareAnalysisInput(path);
|
||||
ASSERT_TRUE(prepared.HasValue());
|
||||
const auto& domain = prepared.Value().domain;
|
||||
auto element = fesa::ElementFactory{}.Create(domain.Elements()[0U], domain);
|
||||
ASSERT_TRUE(element.HasValue());
|
||||
const auto* shell = dynamic_cast<const fesa::Mitc4Shell*>(element.Value().get());
|
||||
ASSERT_NE(shell, nullptr);
|
||||
const auto stiffness = shell->Stiffness();
|
||||
ASSERT_TRUE(stiffness.HasValue());
|
||||
double trace = 0.0;
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
for (std::size_t c = 3U; c < 5U; ++c)
|
||||
trace += stiffness.Value().physical_local20(5U * node + c, 5U * node + c);
|
||||
const double kd = 1e-3 * trace / 8.0;
|
||||
fesa::SerialParallelFor serial;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
fesa::NewtonIterationLogWriter log;
|
||||
ASSERT_TRUE(log.Open(directory.Path() / "drilling.log").IsOk());
|
||||
auto result = fesa::NonlinearStaticProcedureExecutor{serial, tangent}.Execute(domain, log);
|
||||
ASSERT_TRUE(log.Close().IsOk());
|
||||
ASSERT_TRUE(result.HasValue()) << (result.GetStatus().Diagnostics().empty() ? "" : result.GetStatus().Diagnostics()[0U].message);
|
||||
ASSERT_EQ(result.Value().state.AcceptedFrames().size(), 10U);
|
||||
for (const auto& frame : result.Value().state.AcceptedFrames()) {
|
||||
ASSERT_TRUE(frame.mitc4_drilling);
|
||||
EXPECT_EQ(frame.mitc4_drilling->mode, fesa::Mitc4DrillingMode::kDirectSpring);
|
||||
const double torque = 4.0 * kd * 0.125 * frame.load_factor;
|
||||
EXPECT_NEAR(frame.global_equilibrium[5U], torque, 1e-13);
|
||||
EXPECT_NEAR(frame.mitc4_drilling->drilling_moment[2U], torque, 1e-13);
|
||||
for (const double value : frame.mitc4_drilling->equilibrium_with_drilling)
|
||||
EXPECT_NEAR(value, 0.0, 1e-13);
|
||||
EXPECT_NEAR(frame.physical_strain_energy, 0.0, 1e-20);
|
||||
if (pressure) EXPECT_NE(frame.external_force[2U], 0.0);
|
||||
}
|
||||
ASSERT_TRUE(result.Value().state.CommittedState().mitc4_drilling);
|
||||
EXPECT_EQ(result.Value().state.CommittedState().mitc4_drilling->drilling_moment,
|
||||
result.Value().state.AcceptedFrames().back().mitc4_drilling->drilling_moment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kStepRoot = "/steps/Step-1/frames/0";
|
||||
@@ -525,4 +527,40 @@ TEST(Mitc4ShellCli, WritesShellHdf5ThroughTheExistingApplicationRoute) {
|
||||
const auto result = RunApplication({input.string()});
|
||||
ASSERT_EQ(result.exit_code, 0) << result.standard_error;
|
||||
ExpectShellHdf5Identity(output, input);
|
||||
const auto file = OpenFile(output);
|
||||
const auto drilling = fesa::hdf5_internal::ReadDrillingDiagnostics(file.Get());
|
||||
ASSERT_TRUE(drilling);
|
||||
EXPECT_EQ(drilling->formulation, "direct-initial-director-spring");
|
||||
EXPECT_EQ(drilling->version, 1U);
|
||||
ASSERT_EQ(drilling->frames.size(), 1U);
|
||||
EXPECT_EQ(drilling->frames.front().frame_index, 0U);
|
||||
}
|
||||
|
||||
|
||||
TEST(Mitc4ShellCli, DrillingActualHhtPipelineWritesInitialAndAcceptedDiagnostics) {
|
||||
TempDirectory directory{"shell-drilling-hht"};
|
||||
const auto input = directory.Path() / "shell.inp";
|
||||
const auto output = directory.Path() / "shell_results.h5";
|
||||
std::string deck = AllConstrainedShellDeck();
|
||||
const auto material = deck.find("*Material, name=Steel\n");
|
||||
ASSERT_NE(material, std::string::npos);
|
||||
deck.insert(material + std::string{"*Material, name=Steel\n"}.size(), "*Density\n2.\n");
|
||||
const auto step = deck.find("*Step,");
|
||||
ASSERT_NE(step, std::string::npos);
|
||||
deck.replace(step, std::string::npos,
|
||||
"*Step, name=Load, nlgeom=YES\n*Dynamic, Direct\n0.25, 0.5\n*Cload\nAll, 3, 1.\n*End Step\n");
|
||||
WriteText(input, deck);
|
||||
const auto result = RunApplication({input.string()});
|
||||
ASSERT_EQ(result.exit_code, 0) << result.standard_error;
|
||||
const auto file = OpenFile(output);
|
||||
const auto drilling = fesa::hdf5_internal::ReadDrillingDiagnostics(file.Get());
|
||||
ASSERT_TRUE(drilling);
|
||||
EXPECT_EQ(drilling->formulation, "direct-initial-director-spring");
|
||||
ASSERT_EQ(drilling->frames.size(), 3U);
|
||||
for (std::size_t i = 0; i < 3U; ++i) {
|
||||
EXPECT_EQ(drilling->frames[i].frame_index, i);
|
||||
EXPECT_EQ(drilling->frames[i].raw_equilibrium, (std::array<double, 6>{}));
|
||||
EXPECT_EQ(drilling->frames[i].drilling_moment, (std::array<double, 3>{}));
|
||||
EXPECT_EQ(drilling->frames[i].equilibrium_with_drilling, (std::array<double, 6>{}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include "fesa/io/abaqus/domain_mapper.h"
|
||||
#include "fesa/io/abaqus/input_reader.h"
|
||||
|
||||
#include "mitc4_drilling_reader.h"
|
||||
|
||||
namespace fesa::test {
|
||||
namespace {
|
||||
|
||||
@@ -385,6 +387,9 @@ Result<EigenReferenceComparisonReport> EigenReferenceComparison::Compare(
|
||||
}
|
||||
|
||||
EigenReferenceComparisonReport report{};
|
||||
report.drilling_diagnostics = ReadMitc4DrillingReport(file.Get());
|
||||
if (report.drilling_diagnostics && !report.drilling_diagnostics->frames.empty())
|
||||
Fail("schema-mismatch", "Eigen drilling diagnostics must have no equilibrium frames.");
|
||||
report.case_id = reference_case.case_id;
|
||||
report.source_step_name = frequency.source_step_name_raw;
|
||||
report.requested_mode_count = requested;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
#include "mitc4_drilling_report.h"
|
||||
#include "reference_tolerance_policy.h"
|
||||
|
||||
namespace fesa::test {
|
||||
@@ -36,6 +37,7 @@ struct EigenReferenceComparisonReport {
|
||||
bool passed;
|
||||
std::string policy_id;
|
||||
double relative_rms_tolerance;
|
||||
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
|
||||
};
|
||||
|
||||
/// @brief Compares modal eigenvalues without changing the approved input or
|
||||
|
||||
@@ -123,6 +123,25 @@ TEST(EigenReferenceComparison,
|
||||
EXPECT_EQ(ok.Value().policy_id, "mitc4-eigen-first-implementation");
|
||||
EXPECT_DOUBLE_EQ(ok.Value().relative_rms_tolerance, 0.05);
|
||||
EXPECT_EQ(ok.Value().families.size(), 2U);
|
||||
EXPECT_FALSE(ok.Value().drilling_diagnostics);
|
||||
for (const std::string mode : {"direct-initial-director-spring", "legacy-corotated-twist"}) {
|
||||
f = H5Fopen(h5.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
|
||||
m = H5Gopen2(f, "/metadata", H5P_DEFAULT);
|
||||
if (H5Aexists(m, "mitc4_drilling_formulation") > 0) H5Adelete(m, "mitc4_drilling_formulation");
|
||||
if (H5Aexists(m, "drilling_diagnostics_version") > 0) H5Adelete(m, "drilling_diagnostics_version");
|
||||
Str(m, "mitc4_drilling_formulation", mode);
|
||||
U(m, "drilling_diagnostics_version", 1U);
|
||||
H5Gclose(m);
|
||||
H5Fclose(f);
|
||||
const auto compared = fesa::test::EigenReferenceComparison::Compare(reference_case);
|
||||
ASSERT_TRUE(compared.HasValue());
|
||||
ASSERT_TRUE(compared.Value().drilling_diagnostics);
|
||||
EXPECT_EQ(compared.Value().drilling_diagnostics->formulation, mode);
|
||||
EXPECT_EQ(compared.Value().drilling_diagnostics->version, 1U);
|
||||
EXPECT_TRUE(compared.Value().drilling_diagnostics->frames.empty());
|
||||
EXPECT_EQ(compared.Value().families.size(), 2U);
|
||||
}
|
||||
|
||||
// Only synthetic FESA output is changed; each TXT mode remains an independent
|
||||
// scalar family, including at both inclusive five-percent boundaries.
|
||||
for (const auto& sample :
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef FESA_TESTS_REFERENCE_MITC4_DRILLING_READER_H_
|
||||
#define FESA_TESTS_REFERENCE_MITC4_DRILLING_READER_H_
|
||||
|
||||
#include "mitc4_drilling_report.h"
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
namespace fesa::test {
|
||||
|
||||
inline std::optional<Mitc4DrillingDiagnosticsReport> ReadMitc4DrillingReport(hid_t file) {
|
||||
const auto stored = hdf5_internal::ReadDrillingDiagnostics(file);
|
||||
if (!stored) return std::nullopt;
|
||||
Mitc4DrillingDiagnosticsReport result{stored->formulation, stored->version, {}};
|
||||
for (const auto& frame : stored->frames)
|
||||
result.frames.push_back({frame.storage_step, frame.frame_index, frame.raw_equilibrium,
|
||||
frame.drilling_moment, frame.equilibrium_with_drilling});
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace fesa::test
|
||||
|
||||
#endif // FESA_TESTS_REFERENCE_MITC4_DRILLING_READER_H_
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef FESA_TESTS_REFERENCE_MITC4_DRILLING_REPORT_H_
|
||||
#define FESA_TESTS_REFERENCE_MITC4_DRILLING_REPORT_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa::test {
|
||||
|
||||
struct Mitc4DrillingFrameReport {
|
||||
std::string storage_step;
|
||||
std::uint64_t frame_index;
|
||||
std::array<double, 6> raw_equilibrium;
|
||||
std::array<double, 3> drilling_moment;
|
||||
std::array<double, 6> equilibrium_with_drilling;
|
||||
};
|
||||
|
||||
struct Mitc4DrillingDiagnosticsReport {
|
||||
std::string formulation;
|
||||
std::uint64_t version;
|
||||
std::vector<Mitc4DrillingFrameReport> frames;
|
||||
};
|
||||
|
||||
inline void WriteDrillingDiagnosticsJson(
|
||||
std::ostream& stream, const std::optional<Mitc4DrillingDiagnosticsReport>& report) {
|
||||
if (!report) { stream << "null"; return; }
|
||||
stream << "{\"formulation\":\"" << report->formulation
|
||||
<< "\",\"version\":" << report->version << ",\"frames\":[";
|
||||
const auto numbers = [&](const auto& values) {
|
||||
stream << '[';
|
||||
for (std::size_t i = 0; i < values.size(); ++i) {
|
||||
if (i != 0U) stream << ',';
|
||||
stream << values[i];
|
||||
}
|
||||
stream << ']';
|
||||
};
|
||||
for (std::size_t i = 0; i < report->frames.size(); ++i) {
|
||||
if (i != 0U) stream << ',';
|
||||
const auto& frame = report->frames[i];
|
||||
stream << "{\"storage_step\":\"Step-1\",\"frame_index\":" << frame.frame_index
|
||||
<< ",\"raw_equilibrium\":";
|
||||
numbers(frame.raw_equilibrium);
|
||||
stream << ",\"drilling_moment\":";
|
||||
numbers(frame.drilling_moment);
|
||||
stream << ",\"equilibrium_with_drilling\":";
|
||||
numbers(frame.equilibrium_with_drilling);
|
||||
stream << '}';
|
||||
}
|
||||
stream << "]}";
|
||||
}
|
||||
|
||||
} // namespace fesa::test
|
||||
|
||||
#endif // FESA_TESTS_REFERENCE_MITC4_DRILLING_REPORT_H_
|
||||
@@ -26,6 +26,8 @@
|
||||
|
||||
#include "reference_tolerance_policy.h"
|
||||
|
||||
#include "mitc4_drilling_reader.h"
|
||||
|
||||
namespace fesa::test {
|
||||
namespace {
|
||||
|
||||
@@ -573,6 +575,7 @@ std::vector<double> ReadDisplacement(const hid_t file,
|
||||
}
|
||||
|
||||
struct Hdf5Projection {
|
||||
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
|
||||
std::vector<WideRow> rows;
|
||||
std::string source_element_type;
|
||||
std::string internal_formulation;
|
||||
@@ -619,7 +622,7 @@ Hdf5Projection ReadHdf5(const Mitc4ReferenceCase& reference_case) {
|
||||
values.begin() + static_cast<std::ptrdiff_t>(row * kComponents.size()),
|
||||
kComponents.size(), rows[row].values.begin());
|
||||
}
|
||||
return {std::move(rows), reference_case.expected_source_element_type,
|
||||
return {ReadMitc4DrillingReport(file.Get()), std::move(rows), reference_case.expected_source_element_type,
|
||||
kInternalFormulation, kIntegrationRule};
|
||||
}
|
||||
|
||||
@@ -739,6 +742,7 @@ Result<Mitc4ComparisonReport> Mitc4ReferenceComparison::Compare(
|
||||
}
|
||||
|
||||
Mitc4ComparisonReport report{};
|
||||
report.drilling_diagnostics = std::move(hdf5.drilling_diagnostics);
|
||||
report.case_id = reference_case.case_id;
|
||||
report.source_element_type = std::move(hdf5.source_element_type);
|
||||
report.internal_formulation = std::move(hdf5.internal_formulation);
|
||||
@@ -889,7 +893,9 @@ Status Mitc4ReferenceComparison::WriteDeterministicJson(
|
||||
}
|
||||
stream.imbue(std::locale::classic());
|
||||
stream << std::setprecision(std::numeric_limits<double>::max_digits10);
|
||||
stream << "{\"case_id\":\"" << JsonEscape(report.case_id)
|
||||
stream << "{\"drilling_diagnostics\":";
|
||||
WriteDrillingDiagnosticsJson(stream, report.drilling_diagnostics);
|
||||
stream << ",\"case_id\":\"" << JsonEscape(report.case_id)
|
||||
<< "\",\"source_element_type\":\""
|
||||
<< JsonEscape(report.source_element_type)
|
||||
<< "\",\"internal_formulation\":\""
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
#include "mitc4_drilling_report.h"
|
||||
|
||||
namespace fesa::test {
|
||||
|
||||
@@ -77,6 +78,7 @@ struct Mitc4ComparisonReport {
|
||||
std::vector<Mitc4Warning> warnings;
|
||||
std::size_t worst_row;
|
||||
bool passed;
|
||||
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
|
||||
};
|
||||
|
||||
class Mitc4ReferenceComparison {
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
|
||||
#endif
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kInstanceName = "PART-1-1";
|
||||
@@ -771,3 +773,56 @@ TEST(Mitc4ReferenceComparison, RequiresOnlyDeclaredInputCsvAndHdf5) {
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
TEST(Mitc4ReferenceComparison, DrillingOldMarkerlessReportIsNull) {
|
||||
ContractFixture fixture{"drilling-old"};
|
||||
const auto compared = fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase());
|
||||
ASSERT_TRUE(compared.HasValue());
|
||||
EXPECT_FALSE(compared.Value().drilling_diagnostics);
|
||||
const auto json = fixture.Root() / "drilling-old.json";
|
||||
ASSERT_TRUE(fesa::test::Mitc4ReferenceComparison::WriteDeterministicJson(compared.Value(), json).IsOk());
|
||||
EXPECT_NE(ReadBytes(json).find("\"drilling_diagnostics\":null"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(Mitc4ReferenceComparison, DrillingNewMarkersPreserveAllFramesWithoutNewVerdict) {
|
||||
for (const auto mode : {fesa::Mitc4DrillingMode::kDirectSpring,
|
||||
fesa::Mitc4DrillingMode::kLegacyCorotatedTwist}) {
|
||||
ContractFixture fixture{"drilling-new"};
|
||||
{
|
||||
Hdf5Handle file{H5Fopen(fixture.Results().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
|
||||
fesa::hdf5_internal::WriteDrillingMetadata(file.Get(), mode);
|
||||
// Nonzero raw balance is diagnostic only for the linear displacement comparator.
|
||||
for (const int index : {0, 2, 10}) {
|
||||
const std::string root = "/steps/Step-1/frames/" + std::to_string(index);
|
||||
(void)fesa::hdf5_internal::CreateGroup(file.Get(), (root + "/global").c_str());
|
||||
const std::array<double, 6> raw{1., 2., 3., 4., 5., 6.};
|
||||
const fesa::Mitc4DrillingEvidence evidence{mode, {4., 5., 6.}, {1., 2., 3., 0., 0., 0.}};
|
||||
fesa::hdf5_internal::WriteModelDoubleDataset(file.Get(), root + "/global/equilibrium", {6U},
|
||||
raw.data(), 6U, "", "", "", "");
|
||||
fesa::hdf5_internal::WriteDrillingFrame(file.Get(), root, evidence);
|
||||
}
|
||||
}
|
||||
const auto compared = fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase());
|
||||
ASSERT_TRUE(compared.HasValue());
|
||||
EXPECT_TRUE(compared.Value().passed);
|
||||
EXPECT_EQ(compared.Value().metrics.size(), 2U);
|
||||
const auto& diagnostics = compared.Value().drilling_diagnostics;
|
||||
ASSERT_TRUE(diagnostics);
|
||||
EXPECT_EQ(diagnostics->formulation, fesa::Mitc4DrillingModeName(mode));
|
||||
ASSERT_EQ(diagnostics->frames.size(), 3U);
|
||||
EXPECT_EQ(diagnostics->frames[0].frame_index, 0U);
|
||||
EXPECT_EQ(diagnostics->frames[1].frame_index, 2U);
|
||||
EXPECT_EQ(diagnostics->frames[2].frame_index, 10U);
|
||||
const auto json = fixture.Root() / "drilling-new.json";
|
||||
ASSERT_TRUE(fesa::test::Mitc4ReferenceComparison::WriteDeterministicJson(compared.Value(), json).IsOk());
|
||||
const auto bytes = ReadBytes(json);
|
||||
EXPECT_NE(bytes.find("\"drilling_diagnostics\":{\"formulation\":\"" +
|
||||
diagnostics->formulation + "\",\"version\":1,\"frames\":["), std::string::npos);
|
||||
{
|
||||
Hdf5Handle file{H5Fopen(fixture.Results().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
|
||||
ASSERT_GE(H5Ldelete(file.Get(), "/steps/Step-1/frames/2/global/drilling_moment", H5P_DEFAULT), 0);
|
||||
}
|
||||
EXPECT_FALSE(fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()).HasValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
|
||||
#endif
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ReadBytes(const std::filesystem::path& path) {
|
||||
@@ -409,3 +411,75 @@ TEST(NonlinearStaticReference,
|
||||
EXPECT_FALSE(nonzero.normalized_metric.has_value()) << identity;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST(NonlinearStaticReference, DrillingSyntheticMomentSelectsOnlyExistingInternalFamily) {
|
||||
const auto reference_case = MakeCase("shellnl-s4-final");
|
||||
const auto copied = fesa::test::CopyInputToExecutionDirectory(reference_case);
|
||||
ASSERT_TRUE(copied.HasValue());
|
||||
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(reference_case, copied.Value());
|
||||
ASSERT_TRUE(generated.HasValue());
|
||||
const auto& path = generated.Value().generated_results_hdf5;
|
||||
const auto baseline = fesa::test::CompareHdf5ToReferenceCsv(reference_case, path);
|
||||
ASSERT_TRUE(baseline.HasValue());
|
||||
ASSERT_TRUE(baseline.Value().drilling_diagnostics);
|
||||
const auto final_index = baseline.Value().drilling_diagnostics->frames.back().frame_index;
|
||||
const std::string root = "/steps/Step-1/frames/" + std::to_string(final_index) + "/global/";
|
||||
const auto moment_gate = [](const auto& report) -> const auto& {
|
||||
const auto found = std::find_if(report.internal_equilibrium.begin(), report.internal_equilibrium.end(),
|
||||
[](const auto& family) { return family.family_identity == "coarse.global_moment_balance"; });
|
||||
if (found == report.internal_equilibrium.end()) throw std::runtime_error{"Missing existing moment family."};
|
||||
return *found;
|
||||
};
|
||||
// This mutates only a generated working output into an algebra fixture.
|
||||
// Its comparator verdict is not physics reference evidence.
|
||||
for (const std::string mode : {"direct-initial-director-spring", "legacy-corotated-twist", "markerless"}) {
|
||||
{
|
||||
fesa::hdf5_internal::Hdf5Handle file{H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
|
||||
fesa::hdf5_internal::Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
|
||||
ASSERT_GE(H5Adelete(metadata.Get(), "mitc4_drilling_formulation"), 0);
|
||||
ASSERT_GE(H5Adelete(metadata.Get(), "drilling_diagnostics_version"), 0);
|
||||
if (mode != "markerless") {
|
||||
fesa::hdf5_internal::WriteStringAttribute(metadata.Get(), "mitc4_drilling_formulation", mode);
|
||||
fesa::hdf5_internal::WriteUint64Attribute(metadata.Get(), "drilling_diagnostics_version", 1U);
|
||||
}
|
||||
for (const auto& entry : std::vector<std::pair<std::string, std::vector<double>>>{
|
||||
{"equilibrium", {0.,0.,0.,1e6,0.,0.}}, {"drilling_moment", {1e6,0.,0.}},
|
||||
{"equilibrium_with_drilling", {0.,0.,0.,0.,0.,0.}}}) {
|
||||
fesa::hdf5_internal::Hdf5Handle dataset{H5Dopen2(file.Get(), (root + entry.first).c_str(), H5P_DEFAULT), H5Dclose};
|
||||
ASSERT_GE(H5Dwrite(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
|
||||
H5P_DEFAULT, entry.second.data()), 0);
|
||||
}
|
||||
}
|
||||
const auto compared = fesa::test::CompareHdf5ToReferenceCsv(reference_case, path);
|
||||
ASSERT_TRUE(compared.HasValue());
|
||||
EXPECT_TRUE(compared.Value().external_blocking_passed);
|
||||
EXPECT_EQ(compared.Value().families.size(), baseline.Value().families.size());
|
||||
EXPECT_EQ(compared.Value().internal_equilibrium.size(), baseline.Value().internal_equilibrium.size());
|
||||
const auto& gate = moment_gate(compared.Value());
|
||||
EXPECT_EQ(gate.scale, moment_gate(baseline.Value()).scale);
|
||||
EXPECT_EQ(gate.numerator, mode == "direct-initial-director-spring" ? 0.0 : 1e6);
|
||||
EXPECT_EQ(gate.passed, mode == "direct-initial-director-spring");
|
||||
EXPECT_EQ(compared.Value().drilling_diagnostics.has_value(), mode != "markerless");
|
||||
}
|
||||
// Restore new metadata, retain exact raw-minus-M algebra, and expose a real imbalance.
|
||||
{
|
||||
fesa::hdf5_internal::Hdf5Handle file{H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
|
||||
fesa::hdf5_internal::WriteDrillingMetadata(file.Get(), fesa::Mitc4DrillingMode::kDirectSpring);
|
||||
for (const auto& entry : std::vector<std::pair<std::string, std::vector<double>>>{
|
||||
{"equilibrium", {0.,0.,0.,2e6,0.,0.}}, {"equilibrium_with_drilling", {0.,0.,0.,1e6,0.,0.}}}) {
|
||||
fesa::hdf5_internal::Hdf5Handle dataset{H5Dopen2(file.Get(), (root + entry.first).c_str(), H5P_DEFAULT), H5Dclose};
|
||||
ASSERT_GE(H5Dwrite(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
|
||||
H5P_DEFAULT, entry.second.data()), 0);
|
||||
}
|
||||
}
|
||||
const auto imbalanced = fesa::test::CompareHdf5ToReferenceCsv(reference_case, path);
|
||||
ASSERT_TRUE(imbalanced.HasValue());
|
||||
EXPECT_FALSE(moment_gate(imbalanced.Value()).passed);
|
||||
EXPECT_EQ(moment_gate(imbalanced.Value()).scale, moment_gate(baseline.Value()).scale);
|
||||
{
|
||||
fesa::hdf5_internal::Hdf5Handle file{H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
|
||||
ASSERT_GE(H5Ldelete(file.Get(), "/steps/Step-1/frames/0/global/drilling_moment", H5P_DEFAULT), 0);
|
||||
}
|
||||
EXPECT_FALSE(fesa::test::CompareHdf5ToReferenceCsv(reference_case, path).HasValue());
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
|
||||
#endif
|
||||
|
||||
#include "mitc4_drilling_reader.h"
|
||||
|
||||
namespace fesa::test {
|
||||
namespace {
|
||||
|
||||
@@ -896,6 +898,8 @@ std::vector<std::uint8_t> ReadConstraintMask(const hid_t file,
|
||||
}
|
||||
|
||||
struct Hdf5Projection {
|
||||
std::uint64_t selected_frame_index;
|
||||
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
|
||||
std::filesystem::path copied_input_path;
|
||||
std::vector<Hdf5Node> nodes;
|
||||
std::vector<double> displacement;
|
||||
@@ -972,7 +976,7 @@ Hdf5Projection ReadNonlinearHdf5(
|
||||
const std::uint64_t frame_index = ReadFactorOneFrame(file.Get());
|
||||
const std::string root = "/steps/Step-1/frames/" +
|
||||
std::to_string(frame_index) + "/nodal/";
|
||||
return {copied_input,
|
||||
return {frame_index, ReadMitc4DrillingReport(file.Get()), copied_input,
|
||||
nodes,
|
||||
ReadNodalDataset(file.Get(), root + "displacement",
|
||||
"UX,UY,UZ,URX,URY,URZ", contract.node_count),
|
||||
@@ -1320,6 +1324,16 @@ void AppendInternalEquilibrium(NonlinearReferenceComparisonReport* report,
|
||||
applied_moment_scale += applied_resultant.Norm();
|
||||
reaction_moment_scale += constrained_resultant.Norm();
|
||||
}
|
||||
const std::array<double, 6>* moment_balance = &projection.global_equilibrium;
|
||||
if (projection.drilling_diagnostics &&
|
||||
projection.drilling_diagnostics->formulation == "direct-initial-director-spring") {
|
||||
const auto& frames = projection.drilling_diagnostics->frames;
|
||||
const auto selected = std::find_if(frames.begin(), frames.end(), [&](const auto& frame) {
|
||||
return frame.frame_index == projection.selected_frame_index;
|
||||
});
|
||||
if (selected == frames.end()) Fail("schema-mismatch", "Missing factor-one drilling diagnostics.");
|
||||
moment_balance = &selected->equilibrium_with_drilling;
|
||||
}
|
||||
report->constrained_reaction_resultant = {
|
||||
reaction_force.Components(), reaction_moment.Components()};
|
||||
const std::string prefix = ContractFor(reference_case).family_prefix;
|
||||
@@ -1329,9 +1343,9 @@ void AppendInternalEquilibrium(NonlinearReferenceComparisonReport* report,
|
||||
projection.global_equilibrium[2U]},
|
||||
applied_force_scale, reaction_force_scale);
|
||||
AppendGlobalBalance(report, prefix + "global_moment_balance",
|
||||
{projection.global_equilibrium[3U],
|
||||
projection.global_equilibrium[4U],
|
||||
projection.global_equilibrium[5U]},
|
||||
{(*moment_balance)[3U],
|
||||
(*moment_balance)[4U],
|
||||
(*moment_balance)[5U]},
|
||||
applied_moment_scale, reaction_moment_scale);
|
||||
AppendFreeResidual(report, prefix + "accepted_free_force_residual",
|
||||
projection, false);
|
||||
@@ -1385,6 +1399,7 @@ void CompareLinearCase(NonlinearReferenceComparisonReport& report,
|
||||
: diagnostics.front().message;
|
||||
Fail("schema-mismatch", message);
|
||||
}
|
||||
report.drilling_diagnostics = legacy.Value().drilling_diagnostics;
|
||||
for (const auto& metric : legacy.Value().metrics) {
|
||||
ReferenceToleranceFamilyDecision tolerance{};
|
||||
tolerance.identity = metric.blocking ? "linear.translation" : "linear.rotation";
|
||||
@@ -1418,6 +1433,7 @@ void CompareNonlinearCase(NonlinearReferenceComparisonReport& report,
|
||||
report.generated_results_hdf5);
|
||||
report.copied_input_path = hdf5.copied_input_path;
|
||||
report.generated_log_path = DerivedLogPath(hdf5.copied_input_path);
|
||||
report.drilling_diagnostics = hdf5.drilling_diagnostics;
|
||||
RequireExactNodeLabels(hdf5.nodes, contract.node_count);
|
||||
const auto displacement_rows = ReadCsvWideRows(
|
||||
reference_case.displacement_csv_path, true, kDisplacementComponents,
|
||||
@@ -1475,6 +1491,7 @@ void ComparePressureCase(NonlinearReferenceComparisonReport& report,
|
||||
ReadNonlinearHdf5(reference_case, report.generated_results_hdf5);
|
||||
report.copied_input_path = hdf5.copied_input_path;
|
||||
report.generated_log_path = DerivedLogPath(hdf5.copied_input_path);
|
||||
report.drilling_diagnostics = hdf5.drilling_diagnostics;
|
||||
RequireExactNodeLabels(hdf5.nodes, contract.node_count);
|
||||
RequirePressureInstance(hdf5.nodes);
|
||||
const auto displacement_rows =
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
#include "mitc4_drilling_report.h"
|
||||
#include "reference_tolerance_policy.h"
|
||||
|
||||
namespace fesa::test {
|
||||
@@ -114,6 +115,7 @@ struct NonlinearReferenceComparisonReport {
|
||||
ConstrainedReactionResultantReport constrained_reaction_resultant;
|
||||
bool external_blocking_passed;
|
||||
bool internal_blocking_passed;
|
||||
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
|
||||
};
|
||||
|
||||
enum class NonlinearReferenceClassification {
|
||||
|
||||
@@ -349,3 +349,88 @@ def test_cli_writes_json_and_exit_two_for_missing_results(tmp_path, artifacts):
|
||||
evidence = json.loads(report.read_text())
|
||||
assert not evidence["precheck"]["passed"]
|
||||
assert evidence["families"] == []
|
||||
|
||||
|
||||
DRILLING_MODES = ("direct-initial-director-spring", "legacy-corotated-twist")
|
||||
|
||||
|
||||
def drilling_result(tmp_path, artifacts, mode=DRILLING_MODES[0]):
|
||||
path = changed_result(tmp_path, artifacts)
|
||||
with h5py.File(path, "r+") as file:
|
||||
file["metadata"].attrs["mitc4_drilling_formulation"] = mode
|
||||
file["metadata"].attrs["drilling_diagnostics_version"] = np.uint64(1)
|
||||
for frame in range(101):
|
||||
root = file.require_group(f"steps/Step-1/frames/{frame}/global")
|
||||
raw = np.array([1., 2., 3., 4., 5., 6.], dtype="<f8")
|
||||
moment = np.array([4., 5., 6.], dtype="<f8")
|
||||
corrected = raw.copy()
|
||||
corrected[3:] -= moment
|
||||
root.create_dataset("equilibrium", data=raw)
|
||||
root.create_dataset("drilling_moment", data=moment)
|
||||
root.create_dataset("equilibrium_with_drilling", data=corrected)
|
||||
return path
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", DRILLING_MODES)
|
||||
def test_drilling_diagnostics_are_precheck_and_report_all_frames(tmp_path, artifacts, mode):
|
||||
result = comparator.compare(artifacts[0], artifacts[1], drilling_result(tmp_path, artifacts, mode))
|
||||
assert result["precheck"]["passed"], result["precheck"]
|
||||
assert result["verdict"] == "passed"
|
||||
assert len(result["families"]) == 202
|
||||
diagnostics = result["drilling_diagnostics"]
|
||||
assert diagnostics["formulation"] == mode
|
||||
assert diagnostics["version"] == 1
|
||||
assert [frame["frame_index"] for frame in diagnostics["frames"]] == list(range(101))
|
||||
assert diagnostics["frames"][0] == {
|
||||
"storage_step": "Step-1", "frame_index": 0,
|
||||
"raw_equilibrium": [1., 2., 3., 4., 5., 6.],
|
||||
"drilling_moment": [4., 5., 6.],
|
||||
"equilibrium_with_drilling": [1., 2., 3., 0., 0., 0.]}
|
||||
|
||||
|
||||
def test_markerless_drilling_report_is_null(artifacts):
|
||||
assert comparator.compare(*artifacts)["drilling_diagnostics"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mutation", ["mode_missing", "version_missing", "unknown_mode", "unknown_version",
|
||||
"version_type", "mode_type", "initial_missing", "intermediate_missing", "both_missing",
|
||||
"wrong_shape", "wrong_type", "big_endian", "nan", "inf", "wrong_algebra", "wrong_force"])
|
||||
def test_drilling_diagnostics_rejected_before_displacement_tolerance(tmp_path, artifacts, mutation):
|
||||
path = drilling_result(tmp_path, artifacts)
|
||||
with h5py.File(path, "r+") as file:
|
||||
metadata = file["metadata"]
|
||||
root = file["steps/Step-1/frames/50/global"]
|
||||
if mutation == "mode_missing":
|
||||
del metadata.attrs["mitc4_drilling_formulation"]
|
||||
elif mutation == "version_missing":
|
||||
del metadata.attrs["drilling_diagnostics_version"]
|
||||
elif mutation == "unknown_mode":
|
||||
metadata.attrs["mitc4_drilling_formulation"] = "unknown"
|
||||
elif mutation == "unknown_version":
|
||||
metadata.attrs["drilling_diagnostics_version"] = np.uint64(2)
|
||||
elif mutation == "version_type":
|
||||
del metadata.attrs["drilling_diagnostics_version"]
|
||||
metadata.attrs["drilling_diagnostics_version"] = np.uint32(1)
|
||||
elif mutation == "mode_type":
|
||||
del metadata.attrs["mitc4_drilling_formulation"]
|
||||
metadata.attrs["mitc4_drilling_formulation"] = np.bytes_(DRILLING_MODES[0])
|
||||
elif mutation == "initial_missing":
|
||||
del file["steps/Step-1/frames/0/global/drilling_moment"]
|
||||
elif mutation == "intermediate_missing":
|
||||
del root["equilibrium_with_drilling"]
|
||||
elif mutation == "both_missing":
|
||||
del root["drilling_moment"]
|
||||
del root["equilibrium_with_drilling"]
|
||||
elif mutation in {"wrong_shape", "wrong_type", "big_endian"}:
|
||||
del root["drilling_moment"]
|
||||
root.create_dataset("drilling_moment", data=np.zeros(2 if mutation == "wrong_shape" else 3,
|
||||
dtype=">f8" if mutation == "big_endian" else ("<f4" if mutation == "wrong_type" else "<f8")))
|
||||
elif mutation == "nan":
|
||||
root["drilling_moment"][0] = np.nan
|
||||
elif mutation == "inf":
|
||||
root["equilibrium"][0] = np.inf
|
||||
else:
|
||||
root["equilibrium_with_drilling"][0 if mutation == "wrong_force" else 3] = 1e-100
|
||||
result = comparator.compare(artifacts[0], artifacts[1], path)
|
||||
assert not result["precheck"]["passed"]
|
||||
assert result["families"] == []
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user