From d7f6d0ce746018c30dbf0226863a34a1c844d823 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Sun, 18 Oct 2026 23:42:54 +0900 Subject: [PATCH 01/11] chore: preserve approved MITC4 tolerance experiment baseline --- docs/README.md | 1 + docs/abaqus-tolerance/README.md | 192 ++++++++++++++++++ docs/abaqus-tolerance/application-plan.md | 139 +++++++++++++ .../local-kernel-experiment.md | 132 ++++++++++++ docs/hht-implicit-dynamics/README.md | 8 + .../nonlinear-static-newton-raphson/README.md | 6 + docs/tracking/findings.md | 15 ++ docs/tracking/status.md | 7 + src/fesa/elements/nonlinear_mitc4_inertia.cpp | 15 +- src/fesa/elements/nonlinear_mitc4_shell.cpp | 5 +- .../unit/analysis/hht_shell_physics_test.cpp | 61 +++--- .../elements/nonlinear_mitc4_inertia_test.cpp | 110 +++++----- .../nonlinear_mitc4_shell_tangent_test.cpp | 49 ++--- 13 files changed, 630 insertions(+), 110 deletions(-) create mode 100644 docs/abaqus-tolerance/README.md create mode 100644 docs/abaqus-tolerance/application-plan.md create mode 100644 docs/abaqus-tolerance/local-kernel-experiment.md diff --git a/docs/README.md b/docs/README.md index 6813fe4..ef925cc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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) diff --git a/docs/abaqus-tolerance/README.md b/docs/abaqus-tolerance/README.md new file mode 100644 index 0000000..5779df8 --- /dev/null +++ b/docs/abaqus-tolerance/README.md @@ -0,0 +1,192 @@ +# Abaqus 수렴 허용치 조사와 FESA 대응 + +조사일: 2026-10-18. FESA 조사 기준: `43867d0`. +상태: 문헌·코드 조사이며, 허용치 변경이나 성능 실험 결과가 아니다. + +## 조사 결론 + +Abaqus/Standard의 일반 비선형 평형 기준은 잔차 비율 **0.005**, 해 보정 비율 +**0.01**이다. 병진과 회전을 각각 검사한다. FESA의 현재 MITC4 HHT는 힘·모멘트 +잔차와 q/v/a/A 보정량에 **1e-10**을 사용한다. 완화 실험을 검토할 근거는 있으나, +양쪽의 정규화와 검사 대상이 달라 숫자의 비로 정확도나 실행시간을 예측할 수 없다. +일반 기준값의 근거는 [Abaqus 공식 해석 지침의 제어값 설명][S1]이다. + +비선형 반복 종료, 선형방정식 풀이 정확도, 시간적분 정확도, 결과 비교 오차는 서로 +다른 기준이다. `1e-10`을 전역 검색해 `0.005`로 치환하는 접근은 부적절하다. +이 문서에서의 적용 권고는 **실험 제안**이며 현재 승인된 FESA 판정을 대체하지 않는다. + +## 출처와 버전의 한계 + +- 아래 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보다 엄격하다”로 일반화할 수 없다. + +## 7. 성능 가설과 적용 전 확인 + +확인된 사실은 엄격한 HHT 종료 기준과, 외부 Newton 외에도 비용이 큰 내부 root· +요소 도함수·조립 경로가 존재한다는 점이다. 허용치가 실제 장시간 실행의 주원인인지는 +현재 측정하지 않았다. [기존 성능 기록](../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" diff --git a/docs/abaqus-tolerance/application-plan.md b/docs/abaqus-tolerance/application-plan.md new file mode 100644 index 0000000..3898b46 --- /dev/null +++ b/docs/abaqus-tolerance/application-plan.md @@ -0,0 +1,139 @@ +# MITC4 HHT에 Abaqus 수렴 규칙을 적용하는 실행 계획 + +작성일: 2026-10-18. 조사 기준 source: 43867d0. +상태: 실제 구현·측정·검증을 위한 개정안. 이번 ready에서는 계획 문서만 수정한다. +수정안 승인 후 go는 **실제 코드 수정·빌드·FESA 실행·성능 측정·정확도 검증**을 수행한다. +이 계획의 Abaqus 외부 Newton 규칙 구현·측정은 아직 실행하지 않았다. + +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 기준 후보 구현을 보류하지 않는다. + +공개 규칙을 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, finest normalized error≤1e-4, +기존 N7 해상도 판정을 유지한다. 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//의 기준/후보별 위치에 보존한다. +추적 요약은 docs/abaqus-tolerance/results.md에 작성하고 HHT README·관련 contracts· +tracking/status와 findings 및 필요한 결정 기록을 실제 결과에 맞춘다. + +결론은 **개선 확인 / 속도 미개선 / 수치 부적합 / 검증 미완료**로 구분한다. +개선 확인에는 필수 수치 검증과 동일 조건 후보 elapsed<기준 elapsed가 모두 필요하다. +단일 표본의 개선을 통계적 보장으로 확대하지 않는다. 코드 구현 완료와 전체 검증 완료를 분리한다. + +승인 후 로컬 코드·검사·빌드·FESA 실행은 이 계획의 범위이며 단계별 승인을 다시 요구하지 않는다. +외부 Abaqus·라이선스 비용·원격 전송·release·최종 병합은 제외한다. +실험 revision의 후보 적용과 주 개발 branch의 정식 기본값 채택을 구분하며 최종 병합은 사용자가 결정한다. diff --git a/docs/abaqus-tolerance/local-kernel-experiment.md b/docs/abaqus-tolerance/local-kernel-experiment.md new file mode 100644 index 0000000..3878226 --- /dev/null +++ b/docs/abaqus-tolerance/local-kernel-experiment.md @@ -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 수식·종료 조건은 직전 실험본 그대로이며 이번에는 테스트와 문서만 수정했다. diff --git a/docs/hht-implicit-dynamics/README.md b/docs/hht-implicit-dynamics/README.md index 30bb178..66200b9 100644 --- a/docs/hht-implicit-dynamics/README.md +++ b/docs/hht-implicit-dynamics/README.md @@ -2,6 +2,14 @@ ## 범위와 현재 상태 +2026-10-18 후속 사용자 지시로 polar 반복 12→1회, 관성 회전/Jacobian 급수 32→3차, +질량 대칭·null/Gram·분해 복원 검사 1e-12→1e-3의 local 근사 실험을 적용했다. +최초 엄격한 기준의 관련 요소 46개 중 5개가 실패했다. 후속 사용자 지시로 관성·접선· +HHT 물리 테스트의 오차 기준을 1e-3으로 완화했으며 최신 선택 50개는 두 빌드 경로에서 +46 PASS·4 FAIL이다. 실제 해석 루틴의 수렴 조건은 이번 테스트 수정으로 바뀌지 않았다. +아래 정식화의 유한회전 항등식과 과거 검증을 이 근사본의 통과로 승계하지 않는다. +변경 범위와 실제 검증 한계는 [실험 기록](../abaqus-tolerance/local-kernel-experiment.md)을 따른다. + MITC4 shell의 유한회전 기하비선형 동해석이다. S4/S4R은 FESA-MITC4로 해석하며 외부 solver 내부 알고리즘과의 동등성을 뜻하지 않는다. 현재 채택 경로는 **nodal-lumped-kinetic 질량, reduced-coordinate HHT, 초기 null의 고정 직교 보완기저, diff --git a/docs/nonlinear-static-newton-raphson/README.md b/docs/nonlinear-static-newton-raphson/README.md index 771156a..e80d2ae 100644 --- a/docs/nonlinear-static-newton-raphson/README.md +++ b/docs/nonlinear-static-newton-raphson/README.md @@ -2,6 +2,12 @@ ## 목적과 경계 +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로 증가시킨다. diff --git a/docs/tracking/findings.md b/docs/tracking/findings.md index dcf7c06..4f942c4 100644 --- a/docs/tracking/findings.md +++ b/docs/tracking/findings.md @@ -1,5 +1,20 @@ # 해결되지 않은 문제 +## 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이다. 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)을 따른다. + ## 비정적 검증 공백 - 조건·관측: 2026-10-18 통합에서 장시간 HHT 검증이 사용자 요청으로 중단되고 정적·공통 diff --git a/docs/tracking/status.md b/docs/tracking/status.md index c8e4245..a425397 100644 --- a/docs/tracking/status.md +++ b/docs/tracking/status.md @@ -1,5 +1,12 @@ # 구현과 검증 상태 +2026-10-18 후속 [MITC4 local 근사 실험](../abaqus-tolerance/local-kernel-experiment.md)은 +polar 1회·관성 회전/Jacobian 3차·질량 검사 1e-3을 적용했다. 두 Debug build는 성공했으나 +최초 관련 요소 46개 중 5 FAIL이었다. 후속 사용자 지시로 관련 테스트의 오차 기준을 +1e-3으로 변경했고, 최신 Native/CTest 동일 선택 50개는 각각46 PASS·4 FAIL이다. +전체 965개 중 나머지 915개는 이번에 미실행이며 실제 해석 수렴 조건은 변경하지 않았다. +아래 과거 검증을 이 변경본에 승계하지 않는다. Abaqus 외부 Newton 정책은 미구현이다. + ## 기준 시점 2026-10-18 저장소 HEAD `945d899` 기준이다. 아래 통과 수치는 각 검증 당시 기록의 범위이며 diff --git a/src/fesa/elements/nonlinear_mitc4_inertia.cpp b/src/fesa/elements/nonlinear_mitc4_inertia.cpp index b0716d4..9a806b2 100644 --- a/src/fesa/elements/nonlinear_mitc4_inertia.cpp +++ b/src/fesa/elements/nonlinear_mitc4_inertia.cpp @@ -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(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(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."); } diff --git a/src/fesa/elements/nonlinear_mitc4_shell.cpp b/src/fesa/elements/nonlinear_mitc4_shell.cpp index d058f18..d6711cb 100644 --- a/src/fesa/elements/nonlinear_mitc4_shell.cpp +++ b/src/fesa/elements/nonlinear_mitc4_shell.cpp @@ -1155,11 +1155,10 @@ Jet PhysicalEnergy(const std::array& 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) diff --git a/tests/unit/analysis/hht_shell_physics_test.cpp b/tests/unit/analysis/hht_shell_physics_test.cpp index 13b49c9..abac0f0 100644 --- a/tests/unit/analysis/hht_shell_physics_test.cpp +++ b/tests/unit/analysis/hht_shell_physics_test.cpp @@ -33,6 +33,9 @@ namespace { +// User-approved physical/numerical comparison tolerance for the local experiment. +constexpr double kNumericalTolerance = 1.0e-3; + using Frame = fesa::DynamicAcceptedFrameState; using Assembly = fesa::hht_internal::DynamicAssembly; using L3 = std::array; @@ -520,7 +523,7 @@ void CompleteManufacturedConstraints( if (scale == 0.0) EXPECT_DOUBLE_EQ(value, 0.0); else - EXPECT_LE(value / scale, 1.0e-10); + EXPECT_LE(value / scale, kNumericalTolerance); } } } @@ -561,7 +564,7 @@ TEST(HhtShellPhysics, NodalRigidForceRatesMatchIndependentTimeDifferences) { scale = std::hypot(scale, difference); } ASSERT_GT(scale, 0.0); - EXPECT_LE(error / scale, 1.0e-6); + EXPECT_LE(error / scale, kNumericalTolerance); } } } @@ -580,10 +583,10 @@ TEST(HhtShellPhysics, ExternalDrillingHessianBalancesGradientAcceleration) { for (std::size_t component = 0U; component < 3U; ++component) gradient_acceleration += external.Value().gradient[component] * acceleration[component]; - EXPECT_NEAR(external.Value().directional_second, -1.0 / 6.0, 1.0e-14); - EXPECT_NEAR(gradient_acceleration, 1.0 / 6.0, 1.0e-14); + EXPECT_NEAR(external.Value().directional_second, -1.0 / 6.0, kNumericalTolerance); + EXPECT_NEAR(gradient_acceleration, 1.0 / 6.0, kNumericalTolerance); EXPECT_NEAR(external.Value().directional_second + gradient_acceleration, - 0.0, 1.0e-14); + 0.0, kNumericalTolerance); constexpr double step = 1.0e-4; const auto plus = fixture.shells[0U].internal->EvaluateExternalDrillingConstraint( 0U, {step, 0.0, step}, {}, force, {}, {}); @@ -597,7 +600,7 @@ TEST(HhtShellPhysics, ExternalDrillingHessianBalancesGradientAcceleration) { const double finite_difference = (plus.Value().value - 2.0 * middle.Value().value + minus.Value().value) / (step * step); - EXPECT_NEAR(finite_difference, external.Value().directional_second, 1.0e-8); + EXPECT_NEAR(finite_difference, external.Value().directional_second, kNumericalTolerance); } TEST(HhtShellPhysics, ManufacturedHiddenCountsExternalHessianOnce) { @@ -634,7 +637,7 @@ TEST(HhtShellPhysics, ManufacturedHiddenCountsExternalHessianOnce) { ASSERT_GT(scale, 0.0); EXPECT_LE(std::abs(balance.hidden_acceleration_defect[node] - expected) / scale, - 1.0e-12); + kNumericalTolerance); } TEST(HhtShellPhysics, @@ -656,17 +659,17 @@ TEST(HhtShellPhysics, Evaluate(fixture, exact, 1.0 / 16.0, parameters, true); auto difference = assembly.frame.inertia_force; difference.Axpy(-1.0, oracle.force); - EXPECT_LE(difference.Norm() / oracle.force.Norm(), 1.0e-12); + EXPECT_LE(difference.Norm() / oracle.force.Norm(), kNumericalTolerance); EXPECT_LE(std::abs(assembly.frame.kinetic_energy - static_cast(oracle.kinetic)) / static_cast(oracle.kinetic), - 1.0e-12); + kNumericalTolerance); EXPECT_LE(std::abs(oracle.force.Dot(exact.velocity) - static_cast(oracle.power)) / static_cast(oracle.power_scale), - 1.0e-10); - EXPECT_LE(assembly.frame.physical_strain_energy / 800.0, 1.0e-10); - EXPECT_LE(assembly.frame.drilling_energy / 800.0, 1.0e-10); + kNumericalTolerance); + EXPECT_LE(assembly.frame.physical_strain_energy / 800.0, kNumericalTolerance); + EXPECT_LE(assembly.frame.drilling_energy / 800.0, kNumericalTolerance); fesa::Vector scaled_q = exact.generalized_coordinates; fesa::Vector scaled_force = assembly.frame.internal_force; for (std::size_t i = 0; i < scaled_q.Size(); ++i) { @@ -679,7 +682,7 @@ TEST(HhtShellPhysics, if (force_scale == 0.0) EXPECT_DOUBLE_EQ(scaled_force.Norm(), 0.0); else - EXPECT_LE(scaled_force.Norm() / force_scale, 1.0e-10); + EXPECT_LE(scaled_force.Norm() / force_scale, kNumericalTolerance); double momentum_error = 0.0; double angular_error = 0.0; for (std::size_t c = 0; c < 3; ++c) { @@ -691,10 +694,10 @@ TEST(HhtShellPhysics, static_cast(oracle.angular_momentum[c])); } EXPECT_LE(momentum_error / static_cast(oracle.momentum_scale), - 1.0e-12); + kNumericalTolerance); EXPECT_LE( angular_error / static_cast(oracle.angular_momentum_scale), - 1.0e-12); + kNumericalTolerance); auto balance = assembly.frame; ManufacturedHidden(assembly, RigidForceRates(fixture, time), &balance); for (std::size_t node = 0; node < 4U; ++node) { @@ -710,7 +713,7 @@ TEST(HhtShellPhysics, if (scale == 0.0) EXPECT_DOUBLE_EQ(value, 0.0); else - EXPECT_LE(value / scale, 1.0e-10); + EXPECT_LE(value / scale, kNumericalTolerance); } } EXPECT_TRUE(fesa::hht_internal::CompleteEquilibrium( @@ -721,7 +724,7 @@ TEST(HhtShellPhysics, fesa::Vector null(24U); for (std::size_t c = 0; c < 3U; ++c) null[6U * node + 3U + c] = assembly.rotations[node].drill_null[c]; - EXPECT_LE(assembly.mass.Multiply(null).Norm() / mass_norm, 1.0e-12); + EXPECT_LE(assembly.mass.Multiply(null).Norm() / mass_norm, kNumericalTolerance); } maximum_rotation = std::max( maximum_rotation, @@ -2252,7 +2255,7 @@ GridEvidence RunPlate(const Fixture& fixture, const double h, if (!manufactured) { InitializeNullVelocity(fixture, assembly, &frame, &tangent); for (std::size_t node = 0; node < fixture.domain->Nodes().size(); ++node) - EXPECT_NEAR(frame.velocity[6U * node + 5U], 0.4, 1.0e-12); + EXPECT_NEAR(frame.velocity[6U * node + 5U], 0.4, kNumericalTolerance); assembly = Evaluate(fixture, frame, h, p, true); } const auto null_nodes = @@ -2270,7 +2273,7 @@ GridEvidence RunPlate(const Fixture& fixture, const double h, // it becomes frame-zero history; the initializer's private solve is separate. for (const double residual : frame.free_residual_metrics) { EXPECT_TRUE(std::isfinite(residual)); - EXPECT_LE(residual, 1.0e-10); + EXPECT_LE(residual, kNumericalTolerance); } if (manufactured) CompleteManufacturedConstraints(fixture, assembly, null_nodes, &positive, @@ -2287,7 +2290,7 @@ GridEvidence RunPlate(const Fixture& fixture, const double h, EXPECT_LE(std::abs(frame.kinetic_energy - static_cast(initial_oracle.kinetic)) / energy0, - 1.0e-12); + kNumericalTolerance); const auto initial_momentum = frame.linear_momentum; const auto initial_angular_momentum = frame.angular_momentum; evidence.history.push_back(frame); @@ -2356,7 +2359,7 @@ GridEvidence RunPlate(const Fixture& fixture, const double h, Require(fesa::hht_internal::CompleteEquilibrium(*fixture.domain, fixture.dofs, reconstructed_assembly, &reconstructed_zero)); for (const double residual : reconstructed_zero.free_residual_metrics) - EXPECT_LE(residual, 1.0e-10); + EXPECT_LE(residual, kNumericalTolerance); Require(reduced::CheckGraphRegularity(assembly, chart, root0.operators, tangent)); const auto increments = static_cast(evidence.end_time / h); for (std::uint32_t increment = 1U; increment <= increments; ++increment) { @@ -2837,15 +2840,15 @@ GridEvidence RunPlate(const Fixture& fixture, const double h, std::to_string(h) + ", increment=" + std::to_string(increment)}; const auto physical_oracle = Integrate(fixture, frame); EXPECT_LE(std::abs(frame.kinetic_energy - - static_cast(physical_oracle.kinetic)) / energy0, 1.0e-10); + static_cast(physical_oracle.kinetic)) / energy0, kNumericalTolerance); EXPECT_LE(static_cast(Norm(Add(physical_oracle.momentum, Scale(L3{frame.linear_momentum[0], frame.linear_momentum[1], frame.linear_momentum[2]}, -1.0L)))) / - static_cast(initial_oracle.momentum_scale), 1.0e-10); + static_cast(initial_oracle.momentum_scale), kNumericalTolerance); EXPECT_LE(static_cast(Norm(Add(physical_oracle.angular_momentum, Scale(L3{frame.angular_momentum[0], frame.angular_momentum[1], frame.angular_momentum[2]}, -1.0L)))) / - static_cast(initial_oracle.angular_momentum_scale), 1.0e-10); + static_cast(initial_oracle.angular_momentum_scale), kNumericalTolerance); const double energy = frame.kinetic_energy + frame.physical_strain_energy + frame.drilling_energy; if (!manufactured) @@ -2888,7 +2891,7 @@ GridEvidence RunPlate(const Fixture& fixture, const double h, else EXPECT_LE(std::abs(frame.hidden_constraint[i]) / frame.hidden_constraint_scale[i], - 1.0e-10); + kNumericalTolerance); const double vscale = frame.hidden_velocity_defect_scale[i]; const double ascale = frame.hidden_acceleration_defect_scale[i]; if (vscale > 0.0) @@ -3097,7 +3100,7 @@ TEST(HhtShellPhysics, ReducedManufacturedControllerKeepsExactInitialOrbit) { for (std::size_t i = 0U; i < channel->Size(); ++i) EXPECT_TRUE(std::isfinite((*channel)[i])); for (const double residual : frame.free_residual_metrics) - EXPECT_LE(residual, 1.0e-10); + EXPECT_LE(residual, kNumericalTolerance); for (const auto* defect : {&frame.hidden_constraint, &frame.hidden_velocity_defect, &frame.hidden_acceleration_defect}) @@ -3149,7 +3152,7 @@ TEST(HhtShellPhysics, C08aRigidManufacturedOrbitReducedCoordinateTimeConvergence if (values.second == 0.0) EXPECT_DOUBLE_EQ(values.first, 0.0); else - EXPECT_LE(std::abs(values.first) / values.second, 1.0e-10); + EXPECT_LE(std::abs(values.first) / values.second, kNumericalTolerance); } } const auto exact = RigidOrbit(fixture, frame.time); @@ -3244,7 +3247,7 @@ TEST(HhtShellPhysics, C08aRigidManufacturedOrbitReducedCoordinateTimeConvergence } for (std::size_t channel = 0; channel < 6U; ++channel) { SCOPED_TRACE(channel); - EXPECT_LE(errors.back()[channel], 1.0e-4); + EXPECT_LE(errors.back()[channel], kNumericalTolerance); for (std::size_t grid = grids.size() - 3U; grid + 1U < grids.size(); ++grid) { if (errors[grid][channel] == 0.0 && errors[grid + 1U][channel] == 0.0) continue; @@ -3358,7 +3361,7 @@ TEST(HhtShellPhysics, } for (std::size_t channel = 0; channel < 6; ++channel) { SCOPED_TRACE(channel); - EXPECT_LE(differences.back()[channel], 1.0e-4); + EXPECT_LE(differences.back()[channel], kNumericalTolerance); for (std::size_t pair = differences.size() - 3U; pair + 1U < differences.size(); ++pair) { const double first = differences[pair][channel]; diff --git a/tests/unit/elements/nonlinear_mitc4_inertia_test.cpp b/tests/unit/elements/nonlinear_mitc4_inertia_test.cpp index 79f5761..5002648 100644 --- a/tests/unit/elements/nonlinear_mitc4_inertia_test.cpp +++ b/tests/unit/elements/nonlinear_mitc4_inertia_test.cpp @@ -18,6 +18,9 @@ namespace { +// User-approved physical/numerical comparison tolerance for the local experiment. +constexpr double kNumericalTolerance = 1.0e-3; + fesa::Node Node(const std::int64_t label, const std::array& coordinates) { return {{"Shell-Instance", label, std::to_string(label)}, @@ -88,17 +91,17 @@ TEST(NonlinearMitc4Inertia, IntegratesTheRectangleNodalLumpedMassAndNull) { for (std::size_t row = 0U; row < 4U; ++row) { for (std::size_t column = 0U; column < 4U; ++column) { EXPECT_NEAR(mass(6U * row, 6U * column), row == column ? 0.2 : 0.0, - 1.0e-12); + kNumericalTolerance); EXPECT_NEAR(mass(6U * row + 3U, 6U * column + 3U), - row == column ? 0.2 * 0.2 * 0.2 / 12.0 : 0.0, 1.0e-12); + row == column ? 0.2 * 0.2 * 0.2 / 12.0 : 0.0, kNumericalTolerance); } - EXPECT_NEAR(mass(6U * row + 5U, 6U * row + 5U), 0.0, 1.0e-12); + EXPECT_NEAR(mass(6U * row + 5U, 6U * row + 5U), 0.0, kNumericalTolerance); } fesa::Vector drilling_null{24U}; for (std::size_t node = 0U; node < 4U; ++node) { drilling_null[6U * node + 5U] = 1.0; } - EXPECT_LE(mass.Multiply(drilling_null).Norm(), 1.0e-12); + EXPECT_LE(mass.Multiply(drilling_null).Norm(), kNumericalTolerance); } TEST(NonlinearMitc4Inertia, UsesPhysicalAccelerationAndExactCurrentForce) { @@ -117,8 +120,8 @@ TEST(NonlinearMitc4Inertia, UsesPhysicalAccelerationAndExactCurrentForce) { ASSERT_TRUE(evaluation.HasValue()); const fesa::Vector expected = evaluation.Value().mass.Multiply(acceleration); for (std::size_t index = 0U; index < expected.Size(); ++index) { - EXPECT_NEAR(evaluation.Value().force[index], expected[index], 1.0e-11); - EXPECT_NEAR(evaluation.Value().convective_force[index], 0.0, 1.0e-11); + EXPECT_NEAR(evaluation.Value().force[index], expected[index], kNumericalTolerance); + EXPECT_NEAR(evaluation.Value().convective_force[index], 0.0, kNumericalTolerance); } } @@ -170,7 +173,7 @@ TEST(NonlinearMitc4Inertia, EXPECT_DOUBLE_EQ(actual.Value().convective_force[full], 0.0); } ASSERT_GT(scale, 0.0); - EXPECT_LE(error / scale, 1e-12); + EXPECT_LE(error / scale, kNumericalTolerance); } } } @@ -234,8 +237,8 @@ TEST(NonlinearMitc4Inertia, MixedScaleRotationDerivativeAndAngularMomentum) { std::abs(actual.Value().angular_momentum_rate[c] - nodal_rotary_inertia * acceleration[3U + c])); } - EXPECT_LE(derivative_error / (nodal_rotary_inertia * epsilon), 1e-12); - EXPECT_LE(rate_error / (nodal_rotary_inertia * epsilon), 1e-12); + EXPECT_LE(derivative_error / (nodal_rotary_inertia * epsilon), kNumericalTolerance); + EXPECT_LE(rate_error / (nodal_rotary_inertia * epsilon), kNumericalTolerance); const auto moving = inertia.Value().Evaluate( {}, acceleration, fesa::Vector{24U}); ASSERT_TRUE(moving.HasValue()); @@ -244,7 +247,7 @@ TEST(NonlinearMitc4Inertia, MixedScaleRotationDerivativeAndAngularMomentum) { momentum_error = std::max(momentum_error, std::abs(moving.Value().angular_momentum[c] - nodal_rotary_inertia * acceleration[3U + c])); - EXPECT_LE(momentum_error / (nodal_rotary_inertia * epsilon), 1e-12); + EXPECT_LE(momentum_error / (nodal_rotary_inertia * epsilon), kNumericalTolerance); } } } @@ -490,6 +493,19 @@ fesa::Matrix ScaledMatrix(const fesa::Matrix& matrix, const double length) { return result; } +TEST(DynamicRotation, UsesCubicDirectorAndJacobianAtUnitAxisRotation) { + const auto value = fesa::EvaluateDynamicRotation( + {1.0, 0.0, 0.0}, {0.0, 0.0, 1.0}, {0.0, 1.0, 0.0}, {}); + ASSERT_TRUE(value.HasValue()); + // Independent unit-axis polynomial values; the full exponential differs. + EXPECT_NEAR(value.Value().director.X(), 0.0, kNumericalTolerance); + EXPECT_NEAR(value.Value().director.Y(), -5.0 / 6.0, kNumericalTolerance); + EXPECT_NEAR(value.Value().director.Z(), 0.5, kNumericalTolerance); + EXPECT_NEAR(value.Value().angular_velocity.X(), 0.0, kNumericalTolerance); + EXPECT_NEAR(value.Value().angular_velocity.Y(), 5.0 / 6.0, kNumericalTolerance); + EXPECT_NEAR(value.Value().angular_velocity.Z(), 11.0 / 24.0, kNumericalTolerance); +} + TEST(DynamicRotation, InverseIdentityAndCurrentAnalyticNullAtFiniteRotation) { for (const V3 phi : {V3{0.5, 0.0, 0.0}, V3{0.2, -0.3, 0.1}}) { const auto rotation = fesa::ExpSkew(phi); @@ -502,9 +518,9 @@ TEST(DynamicRotation, InverseIdentityAndCurrentAnalyticNullAtFiniteRotation) { ASSERT_TRUE(value.HasValue()); const V3 mapped = Multiply(jacobian, value.Value().drill_null); EXPECT_GT(mapped[column], 0.0); - EXPECT_NEAR(value.Value().drill_null.Norm(), 1.0, 1e-12); + EXPECT_NEAR(value.Value().drill_null.Norm(), 1.0, kNumericalTolerance); for (std::size_t row = 0; row < 3; ++row) - if (row != column) EXPECT_NEAR(mapped[row], 0.0, 1e-12); + if (row != column) EXPECT_NEAR(mapped[row], 0.0, kNumericalTolerance); } } for (const Fixture fixture : {Fixture{}, Warped()}) { @@ -527,9 +543,9 @@ TEST(DynamicRotation, InverseIdentityAndCurrentAnalyticNullAtFiniteRotation) { for (std::size_t j = 0; j < 3; ++j) { n[6U * node + 3U + j] = rotation.Value().drill_null[j]; } - EXPECT_LE(mass.Multiply(n).Norm() / MatrixNorm(mass), 1.0e-12); + EXPECT_LE(mass.Multiply(n).Norm() / MatrixNorm(mass), kNumericalTolerance); const auto scaled_force = ApplyD(value.Value().force, fixture.Length()); - EXPECT_LE(std::abs(n.Dot(scaled_force)) / scaled_force.Norm(), 1.0e-10); + EXPECT_LE(std::abs(n.Dot(scaled_force)) / scaled_force.Norm(), kNumericalTolerance); fesa::Vector fixed_drill{24U}; fixed_drill[6U * node + 5U] = 1.0; EXPECT_GT(mass.Multiply(fixed_drill).Norm() / MatrixNorm(mass), 1.0e-8); @@ -630,7 +646,7 @@ TEST(NonlinearMitc4Inertia, ? analytical.Norm() + fd.Norm() : MatrixNorm(scaled); const double metric = scale > 0.0 ? error / scale : error; - const double tolerance = analytical.Norm() > 0.0 ? 1.0e-6 : 1.0e-10; + const double tolerance = kNumericalTolerance; std::cout << "C06 tangent channel=" << channel << " mode=" << mode << " j=" << exponent << " raw=" << error << " metric=" << metric << '\n'; @@ -791,38 +807,38 @@ TEST(NonlinearMitc4Inertia, std::hypot(symmetry_error, actual_mass(i, j) - actual_mass(j, i)); } } - EXPECT_LE(mass_error / MatrixNorm(expected_mass), 1.0e-12); - EXPECT_LE(symmetry_error / MatrixNorm(expected_mass), 1.0e-12); + EXPECT_LE(mass_error / MatrixNorm(expected_mass), kNumericalTolerance); + EXPECT_LE(symmetry_error / MatrixNorm(expected_mass), kNumericalTolerance); EXPECT_LE( std::abs(value.Value().kinetic_energy - oracle.energy) / oracle.energy, - 1.0e-12); + kNumericalTolerance); EXPECT_LE(std::abs(0.5 * state.v.Dot(value.Value().mass.Multiply(state.v)) - oracle.energy) / oracle.energy, - 1.0e-12); + kNumericalTolerance); EXPECT_LE(std::abs(state.v.Dot(value.Value().force) - oracle.power) / std::abs(oracle.power), - 1.0e-10); + kNumericalTolerance); EXPECT_LE(Difference(ApplyD(value.Value().force, fixture.Length()), ApplyD(oracle.force, fixture.Length()), 1.0) .Norm() / ApplyD(oracle.force, fixture.Length()).Norm(), - 1.0e-12); + kNumericalTolerance); EXPECT_LE((value.Value().momentum - oracle.momentum).Norm() / oracle.momentum.Norm(), - 1.0e-12); + kNumericalTolerance); EXPECT_LE( (value.Value().angular_momentum - oracle.angular_momentum).Norm() / oracle.angular_momentum.Norm(), - 1.0e-12); + kNumericalTolerance); EXPECT_LE((value.Value().momentum_rate - oracle.momentum_rate).Norm() / oracle.momentum_rate.Norm(), - 1.0e-12); + kNumericalTolerance); EXPECT_LE( (value.Value().angular_momentum_rate - oracle.angular_momentum_rate) .Norm() / oracle.angular_momentum_rate.Norm(), - 1.0e-12); + kNumericalTolerance); // Recover C0/C1/C2 from their known tensor factors, with the prescribed // m_e*(t/2)^k scales even when a centered C1 is exactly zero. for (std::size_t i = 0; i < 4; ++i) { @@ -854,7 +870,7 @@ TEST(NonlinearMitc4Inertia, EXPECT_LE(std::abs(numerator[k] / denominator[k] - oracle.moments[k][i][j]) / scale, - 1.0e-12); + kNumericalTolerance); scale *= fixture.thickness / 2.0; } } @@ -868,7 +884,7 @@ TEST(NonlinearMitc4Inertia, const auto translation = inertia.Value().Evaluate({}, uniform.v, uniform.a); ASSERT_TRUE(translation.HasValue()); EXPECT_NEAR(translation.Value().kinetic_energy / oracle.total_mass, 0.25, - 1.0e-12); + kNumericalTolerance); } } @@ -893,7 +909,7 @@ TEST(NonlinearMitc4Inertia, for (std::size_t i = 0; i < 24; ++i) { for (std::size_t j = 0; j < 24; ++j) { EXPECT_NEAR(initial.Value().mass(i, j), - eigen.Value().lumped_global24(i, j), 1.0e-12); + eigen.Value().lumped_global24(i, j), kNumericalTolerance); if (i / 6U != j / 6U) EXPECT_EQ(initial.Value().mass(i, j), 0.0); } } @@ -901,7 +917,7 @@ TEST(NonlinearMitc4Inertia, const auto lumped = IntegrateFivePoints(fixture, state, true); const auto consistent = IntegrateFivePoints(fixture, state, false); EXPECT_GT(lumped.energy - consistent.energy, 0.0); - EXPECT_NEAR(lumped.total_mass, consistent.total_mass, 1.0e-12); + EXPECT_NEAR(lumped.total_mass, consistent.total_mass, kNumericalTolerance); } } @@ -940,16 +956,16 @@ TEST(DynamicRotation, ThirdDerivativesMatchAllPermutationsAndInteriorSweep) { const V3 oracle = BlockExponentialDerivative( states[state_index], initial, {directions[p], directions[r], directions[t]}); - EXPECT_LE((analytic - oracle).Norm(), 1.0e-12); + EXPECT_LE((analytic - oracle).Norm(), kNumericalTolerance); EXPECT_LE((analytic - Third(value.Value(), directions[t], directions[p], directions[r])) .Norm(), - 1.0e-12); + kNumericalTolerance); const double error = (analytic - fd).Norm(); const double scale = analytic.Norm() > 0.0 ? analytic.Norm() + fd.Norm() : 1.0; const double metric = error / scale; - const double tolerance = analytic.Norm() > 0.0 ? 1.0e-6 : 1.0e-10; + const double tolerance = kNumericalTolerance; std::cout << "C06 D3 state=" << state_index << " triple=" << p << r << t << " j=" << exponent << " raw=" << error << " metric=" << metric << '\n'; @@ -978,11 +994,11 @@ TEST(DynamicRotation, NormalizedNullDerivativesIncludeNormalizationCurvature) { fesa::EvaluateDynamicRotation(phi, initial, direction, {}); ASSERT_TRUE(value.HasValue()); const V3 first = Multiply(value.Value().null_gradient, direction); - EXPECT_NEAR(value.Value().drill_null.Norm(), 1.0, 1.0e-12); - EXPECT_NEAR(value.Value().drill_null.Dot(first), 0.0, 1.0e-12); + EXPECT_NEAR(value.Value().drill_null.Norm(), 1.0, kNumericalTolerance); + EXPECT_NEAR(value.Value().drill_null.Dot(first), 0.0, kNumericalTolerance); EXPECT_NEAR( value.Value().drill_null.Dot(value.Value().null_directional_second), - -first.Dot(first), 1.0e-12); + -first.Dot(first), kNumericalTolerance); bool previous = false; bool consecutive = false; for (const int exponent : {8, 10, 12, 14, 16, 18, 20}) { @@ -1004,7 +1020,7 @@ TEST(DynamicRotation, NormalizedNullDerivativesIncludeNormalizationCurvature) { (fd2.Norm() + value.Value().null_directional_second.Norm()); std::cout << "C06 null j=" << exponent << " D1=" << error1 << " D2=" << error2 << '\n'; - const bool pass = error1 <= 1.0e-6 && error2 <= 1.0e-6 && + const bool pass = error1 <= kNumericalTolerance && error2 <= kNumericalTolerance && exponent >= 10 && exponent <= 18; consecutive = consecutive || (previous && pass); previous = pass; @@ -1053,7 +1069,7 @@ TEST(NonlinearMitc4Inertia, UnitAndGlobalFrameChangesPreservePhysicalMetrics) { EXPECT_LE(std::abs(uv.Value().kinetic_energy / energy_scale - value.Value().kinetic_energy) / value.Value().kinetic_energy, - 1.0e-12); + kNumericalTolerance); const auto original_scaled = ScaledMatrix(value.Value().mass, fixture.Length()); const auto converted_scaled = ScaledMatrix(uv.Value().mass, units.Length()); @@ -1065,13 +1081,13 @@ TEST(NonlinearMitc4Inertia, UnitAndGlobalFrameChangesPreservePhysicalMetrics) { original_scaled(i, j)); } } - EXPECT_LE(error / MatrixNorm(original_scaled), 1.0e-12); + EXPECT_LE(error / MatrixNorm(original_scaled), kNumericalTolerance); const auto original_force = ApplyD(value.Value().force, fixture.Length()); auto converted_force = ApplyD(uv.Value().force, units.Length()); converted_force.Scale(1.0 / energy_scale); EXPECT_LE(Difference(original_force, converted_force, 1.0).Norm() / original_force.Norm(), - 1.0e-10); + kNumericalTolerance); const auto qx = fesa::ExpSkew(V3{0.4, 0, 0}); const auto qz = fesa::ExpSkew(V3{0, 0, 0.7}); @@ -1110,22 +1126,22 @@ TEST(NonlinearMitc4Inertia, UnitAndGlobalFrameChangesPreservePhysicalMetrics) { ASSERT_TRUE(fv.HasValue()); EXPECT_LE(std::abs(fv.Value().kinetic_energy - value.Value().kinetic_energy) / value.Value().kinetic_energy, - 1.0e-12); + kNumericalTolerance); EXPECT_LE((fv.Value().momentum - rotate(value.Value().momentum)).Norm() / value.Value().momentum.Norm(), - 1.0e-12); + kNumericalTolerance); EXPECT_LE( (fv.Value().angular_momentum - rotate(value.Value().angular_momentum)) .Norm() / value.Value().angular_momentum.Norm(), - 1.0e-12); + kNumericalTolerance); for (std::size_t base = 0; base < 24; base += 3) { const V3 expected = rotate(V3{value.Value().force[base], value.Value().force[base + 1U], value.Value().force[base + 2U]}); const V3 actual{fv.Value().force[base], fv.Value().force[base + 1U], fv.Value().force[base + 2U]}; - EXPECT_LE((actual - expected).Norm() / expected.Norm(), 1.0e-10); + EXPECT_LE((actual - expected).Norm() / expected.Norm(), kNumericalTolerance); } // Full matrix congruence checks every translation/rotation cross-block. double covariance_error = 0.0; @@ -1151,7 +1167,7 @@ TEST(NonlinearMitc4Inertia, UnitAndGlobalFrameChangesPreservePhysicalMetrics) { } } } - EXPECT_LE(covariance_error / MatrixNorm(value.Value().mass), 1.0e-12); + EXPECT_LE(covariance_error / MatrixNorm(value.Value().mass), kNumericalTolerance); } TEST(DynamicRotation, MatchesEightyDigitIndependentBlockExponentialAnchors) { @@ -1193,12 +1209,12 @@ TEST(DynamicRotation, MatchesEightyDigitIndependentBlockExponentialAnchors) { const auto value = fesa::EvaluateDynamicRotation(states[i], e3, {}, {}); ASSERT_TRUE(value.HasValue()); EXPECT_LE((Third(value.Value(), e1, e1, e1) - expected[i][0]).Norm(), - 1.0e-12); + kNumericalTolerance); EXPECT_LE((Third(value.Value(), e1, e2, e3) - expected[i][1]).Norm(), - 1.0e-12); + kNumericalTolerance); EXPECT_LE( (Third(value.Value(), mixed, mixed, mixed) - expected[i][2]).Norm(), - 1.0e-12); + kNumericalTolerance); } } diff --git a/tests/unit/elements/nonlinear_mitc4_shell_tangent_test.cpp b/tests/unit/elements/nonlinear_mitc4_shell_tangent_test.cpp index 331abee..5256a2b 100644 --- a/tests/unit/elements/nonlinear_mitc4_shell_tangent_test.cpp +++ b/tests/unit/elements/nonlinear_mitc4_shell_tangent_test.cpp @@ -16,6 +16,9 @@ namespace { +// User-approved physical/numerical comparison tolerance for the local experiment. +constexpr double kNumericalTolerance = 1.0e-3; + using Coordinates = std::array; fesa::Node Node(const std::int64_t label, const Coordinates coordinates) { @@ -188,10 +191,10 @@ fesa::Matrix WorkConjugateTangent(const fesa::Matrix& tangent, void ExpectRelative(const double actual, const double expected) { if (expected == 0.0) { - EXPECT_LE(std::abs(actual), 1.0e-12); + EXPECT_LE(std::abs(actual), kNumericalTolerance); return; } - EXPECT_LE(std::abs(actual - expected) / std::abs(expected), 1.0e-9); + EXPECT_LE(std::abs(actual - expected) / std::abs(expected), kNumericalTolerance); } // NSNR-ELEM-TAN-001 @@ -284,8 +287,8 @@ TEST(NonlinearMitc4Tangent, MatchesTheApprovedDirectionalSchedule) { } EXPECT_GE(force_order_count, 2U); EXPECT_GE(moment_order_count, 2U); - EXPECT_LE(force_errors[3U], 1.0e-6); - EXPECT_LE(moment_errors[3U], 1.0e-6); + EXPECT_LE(force_errors[3U], kNumericalTolerance); + EXPECT_LE(moment_errors[3U], kNumericalTolerance); } // NSNR-ELEM-TAN-004 and NSNR-ELEM-TAN-005 @@ -304,7 +307,7 @@ TEST(NonlinearMitc4Tangent, PreservesRigidTranslationNullspaceAndSymmetry) { WorkConjugate(equilibrium.Value().internal_tangent.Multiply( ToVector(rigid_translation)), length); - EXPECT_LE(null_action.Norm(), 1.0e-10); + EXPECT_LE(null_action.Norm(), kNumericalTolerance); const fesa::Matrix scaled_tangent = WorkConjugateTangent(equilibrium.Value().internal_tangent, length); @@ -316,7 +319,7 @@ TEST(NonlinearMitc4Tangent, PreservesRigidTranslationNullspaceAndSymmetry) { } ASSERT_GT(FrobeniusNorm(scaled_tangent), 0.0); EXPECT_LE(FrobeniusNorm(antisymmetric) / FrobeniusNorm(scaled_tangent), - 1.0e-12); + kNumericalTolerance); } // NSNR-ELEM-TAN-006 @@ -356,8 +359,8 @@ TEST(NonlinearMitc4Objectivity, FiniteRigidMotionHasZeroInternalForce) { const auto result = fesa::EvaluateInternalResidualAndTangent(Shell(nodes), trial); ASSERT_TRUE(result.HasValue()); - EXPECT_LE(result.Value().internal_residual.Norm(), 1.0e-10); - EXPECT_LE(result.Value().physical_strain_energy, 1.0e-12); + EXPECT_LE(result.Value().internal_residual.Norm(), kNumericalTolerance); + EXPECT_LE(result.Value().physical_strain_energy, kNumericalTolerance); } // NSNR-ELEM-TAN-008 @@ -462,7 +465,7 @@ void CheckThirdSweep(const fesa::NonlinearMitc4TrialDofs& trial) { fd_norm = std::hypot(fd_norm, d * fd); } if (power >= 10 && power <= 18 && - error / (analytic_norm + fd_norm) <= 1e-6) { + error / (analytic_norm + fd_norm) <= kNumericalTolerance) { ++consecutive; passed = passed || consecutive >= 2U; } else { @@ -540,9 +543,9 @@ TEST(NonlinearMitc4Residual, SmallRotationPhysicalForceRetainsDrillingNullIdenti } ASSERT_GT(physical_scale, 0.0); ASSERT_GT(total_scale, 0.0); - EXPECT_LE(std::abs(physical) / physical_scale, 1.0e-10); + EXPECT_LE(std::abs(physical) / physical_scale, kNumericalTolerance); EXPECT_LE(std::abs(projected - response.Value().drilling_constraint[node]) / total_scale, - 1.0e-10); + kNumericalTolerance); } } @@ -580,7 +583,7 @@ TEST(NonlinearMitc4Residual, for (std::size_t c = 0U; c < 3U; ++c) psi += rotation.Value().drill_null[c] * exact.Value().internal_force[6U * node + 3U + c]; - EXPECT_NEAR(exact.Value().drilling_constraint[node], psi, 1e-11); + EXPECT_NEAR(exact.Value().drilling_constraint[node], psi, kNumericalTolerance); for (std::size_t i = 0U; i < 24U; ++i) { double gradient = 0.; for (std::size_t c = 0U; c < 3U; ++c) { @@ -591,7 +594,7 @@ TEST(NonlinearMitc4Residual, exact.Value().internal_force[6U * node + 3U + c]; } EXPECT_NEAR(exact.Value().drilling_constraint_gradient(node, i), - gradient, 1e-10); + gradient, kNumericalTolerance); } } unsigned consecutive = 0U; @@ -620,7 +623,7 @@ TEST(NonlinearMitc4Residual, anorm = std::hypot(anorm, scale * analytic); fnorm = std::hypot(fnorm, scale * fd); } - if (power >= 10 && power <= 18 && error / (anorm + fnorm) <= 1e-6) { + if (power >= 10 && power <= 18 && error / (anorm + fnorm) <= kNumericalTolerance) { ++consecutive; passed = passed || consecutive >= 2U; } else @@ -668,7 +671,7 @@ TEST(NonlinearMitc4Residual, EXPECT_NEAR( exact.Value().drilling_constraint_third_directional_gradient[node] [row], - finite_difference, 2.0e-5); + finite_difference, kNumericalTolerance); } } } @@ -698,23 +701,23 @@ TEST(NonlinearMitc4Residual, ASSERT_TRUE(minus.HasValue()); EXPECT_NEAR(exact.Value().directional_first, (plus.Value().value - minus.Value().value) / (2.0 * h), - 2.0e-8); + kNumericalTolerance); EXPECT_NEAR(exact.Value().directional_second, (plus.Value().value - 2.0 * exact.Value().value + minus.Value().value) / (h * h), - 3.0e-7); + kNumericalTolerance); for (std::size_t row = 0U; row < 3U; ++row) { EXPECT_NEAR(exact.Value().gradient_directional_first[row], (plus.Value().gradient[row] - minus.Value().gradient[row]) / (2.0 * h), - 3.0e-8); + kNumericalTolerance); EXPECT_NEAR( exact.Value().gradient_directional_second[row], (plus.Value().gradient[row] - 2.0 * exact.Value().gradient[row] + minus.Value().gradient[row]) / (h * h), - 4.0e-7); + kNumericalTolerance); } for (std::size_t column = 0U; column < 3U; ++column) { fesa::Vector3 basis{}; @@ -732,7 +735,7 @@ TEST(NonlinearMitc4Residual, (positive.Value().gradient[row] - negative.Value().gradient[row]) / (2.0 * h), - 2.0e-7); + kNumericalTolerance); } } } @@ -819,17 +822,17 @@ TEST(NonlinearMitc4Residual, TinyRotationPreservesPhysicalForceAndRecoveryEnergy } ASSERT_GT(scale, 0.0); ASSERT_GT(quadratic_energy, 0.0); - EXPECT_LE(error / scale, 1e-12); + EXPECT_LE(error / scale, kNumericalTolerance); EXPECT_GT(actual.Value().physical_strain_energy, 0.0); EXPECT_LE(std::abs(actual.Value().physical_strain_energy - quadratic_energy) / quadratic_energy, - 1e-12); + kNumericalTolerance); if (!warped) { // Uniform director tilt has .5 * (5/6) * G * t * area * theta^2. constexpr double shear_energy = 1.6e-33; EXPECT_LE(std::abs(actual.Value().physical_strain_energy - shear_energy) / shear_energy, - 1e-12); + kNumericalTolerance); } } } From 78cb33920742a91b4801788fbcc953d3306b0884 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Sun, 18 Oct 2026 23:51:44 +0900 Subject: [PATCH 02/11] docs: define HHT field convergence policy and branch contract --- docs/abaqus-tolerance/README.md | 10 +- docs/abaqus-tolerance/application-plan.md | 5 + .../formulation-convergence.md | 357 ++++++++++++++++++ 3 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 docs/abaqus-tolerance/formulation-convergence.md diff --git a/docs/abaqus-tolerance/README.md b/docs/abaqus-tolerance/README.md index 5779df8..017ee6c 100644 --- a/docs/abaqus-tolerance/README.md +++ b/docs/abaqus-tolerance/README.md @@ -1,7 +1,12 @@ # Abaqus 수렴 허용치 조사와 FESA 대응 조사일: 2026-10-18. FESA 조사 기준: `43867d0`. -상태: 문헌·코드 조사이며, 허용치 변경이나 성능 실험 결과가 아니다. +상태: 문헌·코드 조사와 승인된 후보 정책의 수식 계약이다. 구현·성능 실험 통과 보고가 아니다. + +승인된 후보의 시험 가능한 수식·분기 우선순위·평균 이력·로그 계약은 +[외부 field 수렴 정식화](formulation-convergence.md)를 따른다. 아래는 조사 당시 코드와 +공개 기본값의 비교이며, 후보 구현 완료를 뜻하지 않는다. 정식화는 2025 공개 STRICT +규칙과 비공개 부분의 명시적 FESA 대응을 구분하며 실제 2024 설정과의 동일성을 주장하지 않는다. ## 조사 결론 @@ -13,7 +18,8 @@ Abaqus/Standard의 일반 비선형 평형 기준은 잔차 비율 **0.005**, 비선형 반복 종료, 선형방정식 풀이 정확도, 시간적분 정확도, 결과 비교 오차는 서로 다른 기준이다. `1e-10`을 전역 검색해 `0.005`로 치환하는 접근은 부적절하다. -이 문서에서의 적용 권고는 **실험 제안**이며 현재 승인된 FESA 판정을 대체하지 않는다. +아래 적용 권고는 조사 당시 **실험 제안**이다. 이후 승인된 후보의 정확한 판정은 +정식화에 고정하며, 실제 구현·검증 상태와 구분한다. ## 출처와 버전의 한계 diff --git a/docs/abaqus-tolerance/application-plan.md b/docs/abaqus-tolerance/application-plan.md index 3898b46..da63cd4 100644 --- a/docs/abaqus-tolerance/application-plan.md +++ b/docs/abaqus-tolerance/application-plan.md @@ -20,6 +20,11 @@ MITC4 implicit dynamics의 외부 Newton 종료에 **Abaqus/Standard 2025의 공 근거는 [조사 보고서](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을 평균에 사용하고 diff --git a/docs/abaqus-tolerance/formulation-convergence.md b/docs/abaqus-tolerance/formulation-convergence.md new file mode 100644 index 0000000..722c4c9 --- /dev/null +++ b/docs/abaqus-tolerance/formulation-convergence.md @@ -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 ` 경로가 이 후보를 실제 사용해야 한다. 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가 아니다. From adb809bc01c862f252a96121e2ee39059257ced6 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Mon, 19 Oct 2026 02:09:22 +0900 Subject: [PATCH 03/11] =?UTF-8?q?=EC=97=84=EA=B2=A9=20=EC=88=98=EB=A0=B4?= =?UTF-8?q?=20=EA=B8=B0=EC=A4=80=20=EC=8B=A4=ED=96=89=20=EB=B3=B4=EC=A1=B4?= =?UTF-8?q?=EA=B3=BC=20=ED=95=9C=EC=A0=95=20HHT=20=EA=B0=9C=EB=B0=9C=20?= =?UTF-8?q?=EA=B3=84=EC=B8=A1=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 1 + docs/abaqus-tolerance/results.md | 158 ++++++++++++++++++ msvc/Fesa.Common.props | 4 + msvc/fesa_solver.vcxproj | 1 + msvc/fesa_unit_tests.vcxproj | 1 + src/fesa/CMakeLists.txt | 4 + src/fesa/analysis/hht_development_probe.h | 141 ++++++++++++++++ src/fesa/analysis/hht_dynamic_assembly.cpp | 5 + src/fesa/analysis/hht_reduced_operators.cpp | 5 + src/fesa/analysis/hht_reduced_procedure.cpp | 12 ++ tests/CMakeLists.txt | 4 + .../analysis/hht_development_probe_test.cpp | 114 +++++++++++++ 12 files changed, 450 insertions(+) create mode 100644 docs/abaqus-tolerance/results.md create mode 100644 src/fesa/analysis/hht_development_probe.h create mode 100644 tests/unit/analysis/hht_development_probe_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cd78db..1dbc94b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,7 @@ 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$<$:Debug>DLL") # Pin the original backend ABI/link/thread choices rather than package defaults. set(MKL_INTERFACE ilp64) diff --git a/docs/abaqus-tolerance/results.md b/docs/abaqus-tolerance/results.md new file mode 100644 index 0000000..bfe029a --- /dev/null +++ b/docs/abaqus-tolerance/results.md @@ -0,0 +1,158 @@ +# 외부 수렴 정책 실험 결과 — T2 기준 보존 + +기록일: 2026-10-19. **정상 strict 기준은 확보했으나 전체 실험·물리 검증은 미완료**다. +후보 정책은 아직 구현·측정하지 않았다. 가속률이나 채택 가능성을 주장하지 않는다. + +## 기준과 원시 증거 + +측정 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 원인 분리와 필수 전체 물리/회귀 검증은 남았다. diff --git a/msvc/Fesa.Common.props b/msvc/Fesa.Common.props index d8e4e27..34bf8c1 100644 --- a/msvc/Fesa.Common.props +++ b/msvc/Fesa.Common.props @@ -3,6 +3,7 @@ + false $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\')) $(FesaRoot).build\native\$(Configuration)\ $(FesaRoot.Replace('\','/').TrimEnd('/')) @@ -20,6 +21,9 @@ $(FesaExternalOptions) /external:I"$(FESA_MKL_ROOT)\include" + + FESA_HHT_DEVELOPMENT_PROBE=1;%(PreprocessorDefinitions) + stdcpp17 false diff --git a/msvc/fesa_solver.vcxproj b/msvc/fesa_solver.vcxproj index 897f7f5..3ab073a 100644 --- a/msvc/fesa_solver.vcxproj +++ b/msvc/fesa_solver.vcxproj @@ -14,6 +14,7 @@ + diff --git a/msvc/fesa_unit_tests.vcxproj b/msvc/fesa_unit_tests.vcxproj index d0826de..80e95ee 100644 --- a/msvc/fesa_unit_tests.vcxproj +++ b/msvc/fesa_unit_tests.vcxproj @@ -20,6 +20,7 @@ + diff --git a/src/fesa/CMakeLists.txt b/src/fesa/CMakeLists.txt index 475748b..804c94a 100644 --- a/src/fesa/CMakeLists.txt +++ b/src/fesa/CMakeLists.txt @@ -7,6 +7,7 @@ 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_reduced_operators.cpp analysis/hht_reduced_procedure.cpp @@ -126,6 +127,9 @@ target_compile_options( PRIVATE $<$:/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 diff --git a/src/fesa/analysis/hht_development_probe.h b/src/fesa/analysis/hht_development_probe.h new file mode 100644 index 0000000..803203c --- /dev/null +++ b/src/fesa/analysis/hht_development_probe.h @@ -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 +#include +#include +#include +#include +#include +#include + +#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, 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 inclusive_ns{}; + std::array exclusive_ns{}; + std::array stage_calls{}; + std::array, 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::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_ diff --git a/src/fesa/analysis/hht_dynamic_assembly.cpp b/src/fesa/analysis/hht_dynamic_assembly.cpp index 17012ab..bec9a3f 100644 --- a/src/fesa/analysis/hht_dynamic_assembly.cpp +++ b/src/fesa/analysis/hht_dynamic_assembly.cpp @@ -12,6 +12,7 @@ #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" @@ -382,6 +383,7 @@ Result 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{ &frame.generalized_coordinates, &frame.velocity, &frame.acceleration, @@ -446,9 +448,11 @@ Result 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 +460,7 @@ Result 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(); diff --git a/src/fesa/analysis/hht_reduced_operators.cpp b/src/fesa/analysis/hht_reduced_operators.cpp index 4f0b94b..be4781f 100644 --- a/src/fesa/analysis/hht_reduced_operators.cpp +++ b/src/fesa/analysis/hht_reduced_operators.cpp @@ -1,4 +1,5 @@ #include "analysis/hht_reduced_internal.h" +#include "analysis/hht_development_probe.h" #include #include @@ -513,6 +514,7 @@ Result EvaluateConstraintOperators( shell_index++, shells.size(), q, v); std::optional fresh; if (saved == nullptr) { + FESA_HHT_PROBE_COUNT(kConstraintInternalEvaluation); auto internal = shell.internal->EvaluateTrial(Trial(q), &v); if (!internal.HasValue()) @@ -713,6 +715,7 @@ Result 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::Failure( @@ -830,6 +833,7 @@ Result SolveEmbeddingRoot(const FixedChart& chart, prior_q = q.Value(); has_prior_z = true; z = std::move(next_z); + FESA_HHT_PROBE_COUNT(kInnerCorrection); } return Result::Failure(Failure("Unreachable graph loop.")); } @@ -876,6 +880,7 @@ Result EvaluateEmbeddingFirstDerivatives( Result 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() || diff --git a/src/fesa/analysis/hht_reduced_procedure.cpp b/src/fesa/analysis/hht_reduced_procedure.cpp index efcd372..408b8e4 100644 --- a/src/fesa/analysis/hht_reduced_procedure.cpp +++ b/src/fesa/analysis/hht_reduced_procedure.cpp @@ -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" @@ -396,8 +397,16 @@ Result 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 ExecuteReducedHht( @@ -405,6 +414,7 @@ Result 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::Failure(Status::Failure( FailureCategory::kModel, {{Severity::kError, "invalid-dynamic-step", {}, @@ -1016,6 +1026,7 @@ Result 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); @@ -1091,6 +1102,7 @@ Result 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); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5845708..1e77eac 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,6 +14,7 @@ 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_reduced_shell_test.cpp unit/analysis/hht_dynamic_procedure_test.cpp unit/analysis/nonlinear_analysis_state_test.cpp @@ -99,6 +100,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 diff --git a/tests/unit/analysis/hht_development_probe_test.cpp b/tests/unit/analysis/hht_development_probe_test.cpp new file mode 100644 index 0000000..3fa5cea --- /dev/null +++ b/tests/unit/analysis/hht_development_probe_test.cpp @@ -0,0 +1,114 @@ +#include "analysis/hht_development_probe.h" + +#include + +#if defined(FESA_HHT_DEVELOPMENT_PROBE) +#include "fesa/math/sparse_matrix.h" +#include "fesa/math/vector.h" +#include "analysis/hht_reduced_internal.h" +namespace fesa::hht_development_probe { +class IdentityTangentSolver final : public TangentSolver { + public: + Status FactorizeFresh(const SparseMatrix&) override { return Status::Ok(); } + Status SolveCorrection(const Vector& rhs, Vector& result) const override { + result = rhs; return Status::Ok(); + } +}; +TEST(HhtDevelopmentProbe, CountsActualInnerUpdatesForLinearGraphAndZeroRoot) { + hht_reduced_internal::FixedChart chart{}; + chart.full_dof_count = 1U; + chart.z_dofs = {0U}; + chart.z_columns = {{0U, 1U, false, {{0U, 1.0}}, {0.0}}}; + IdentityTangentSolver solver; + const auto evaluate = [](const Vector& q) { + hht_reduced_internal::ConstraintOperators operators; + operators.rows.resize(1U); + operators.rows[0U].value = q[0U]; + operators.rows[0U].gradient = {{0U, 1.0}}; + return Result::Success( + std::move(operators)); + }; + Statistics statistics; + active = &statistics; + const auto first = hht_reduced_internal::SolveEmbeddingRoot( + chart, Vector{0U}, Vector{1U, 1.0}, evaluate, solver); + active = nullptr; + ASSERT_TRUE(first.HasValue()); + EXPECT_EQ(statistics.counts[kInnerCorrection].load(), 1U); + EXPECT_DOUBLE_EQ(first.Value().z[0U], 0.0); + Statistics zero_statistics; + active = &zero_statistics; + const auto zero = hht_reduced_internal::SolveEmbeddingRoot( + chart, Vector{0U}, Vector{1U, 0.0}, evaluate, solver); + active = nullptr; + ASSERT_TRUE(zero.HasValue()); + EXPECT_EQ(zero_statistics.counts[kInnerCorrection].load(), 0U); +} +class FailingSolver final : public LinearSolver, public TangentSolver { + public: + Status Factorize(const SparseMatrix&) override { + ++factor_calls; return Status::Failure(FailureCategory::kSolver, {}); + } + Status FactorizeFresh(const SparseMatrix& matrix) override { return Factorize(matrix); } + Status Solve(const Vector&, Vector&) const override { + ++solve_calls; return Status::Failure(FailureCategory::kSolver, {}); + } + Status SolveCorrection(const Vector& rhs, Vector& result) const override { + return Solve(rhs, result); + } + int factor_calls = 0; + mutable int solve_calls = 0; +}; +TEST(HhtDevelopmentProbe, ForwardsEveryLogicalCallAndPreservesFailure) { + Statistics statistics; + active = &statistics; + FailingSolver backend; + PositiveSolver positive{backend}; + CorrectionSolver tangent{backend}; + auto matrix = SparseMatrix::FromCoo(0U, 0U, {}, SparsePattern{{0U}, {}}); + ASSERT_TRUE(matrix.HasValue()); + const Vector rhs{0U}; + Vector result{0U}; + EXPECT_FALSE(positive.Factorize(matrix.Value()).IsOk()); + EXPECT_FALSE(positive.Solve(rhs, result).IsOk()); + EXPECT_FALSE(tangent.FactorizeFresh(matrix.Value()).IsOk()); + EXPECT_FALSE(tangent.SolveCorrection(rhs, result).IsOk()); + active = nullptr; + EXPECT_EQ(backend.factor_calls, 2); + EXPECT_EQ(backend.solve_calls, 2); + EXPECT_EQ(statistics.counts[kFactorCall].load(), 2U); + EXPECT_EQ(statistics.counts[kSolveCall].load(), 2U); + EXPECT_EQ(statistics.counts[kRhs].load(), 2U); +} +TEST(HhtDevelopmentProbe, CountsOnlyWhileActiveAndPreservesMultiplicity) { + Statistics statistics; + active = &statistics; + Count(kOuterCorrection); + Count(kInternalEvaluation, 7U); + active = nullptr; + Count(kOuterCorrection); + EXPECT_EQ(statistics.counts[kOuterCorrection].load(), 1U); + EXPECT_EQ(statistics.counts[kInternalEvaluation].load(), 7U); + EXPECT_EQ(statistics.counts[kInertiaEvaluation].load(), 0U); +} + +TEST(HhtDevelopmentProbe, NestedTimesHaveNoDoubleCounting) { + Statistics statistics; + active = &statistics; + { + Scope parent{kProcedure}; + { Scope child{kAssembly}; } + } + active = nullptr; + EXPECT_EQ(statistics.stage_calls[kProcedure], 1U); + EXPECT_EQ(statistics.stage_calls[kAssembly], 1U); + EXPECT_GE(statistics.inclusive_ns[kProcedure], 0); + EXPECT_GE(statistics.inclusive_ns[kAssembly], 0); + EXPECT_EQ(statistics.exclusive_ns[kAssembly], + statistics.inclusive_ns[kAssembly]); + EXPECT_EQ(statistics.exclusive_ns[kProcedure] + + statistics.exclusive_ns[kAssembly], + statistics.inclusive_ns[kProcedure]); +} +} // namespace fesa::hht_development_probe +#endif From ad06d7ad6cac5f8252611d5ef06f10fb404df952 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Mon, 19 Oct 2026 02:20:58 +0900 Subject: [PATCH 04/11] Implement private HHT field convergence policy and oracle tests --- msvc/fesa_solver.vcxproj | 1 + msvc/fesa_unit_tests.vcxproj | 1 + src/fesa/CMakeLists.txt | 1 + src/fesa/analysis/hht_field_convergence.cpp | 166 +++++++++++++ src/fesa/analysis/hht_field_convergence.h | 52 ++++ tests/CMakeLists.txt | 1 + .../analysis/hht_field_convergence_test.cpp | 232 ++++++++++++++++++ 7 files changed, 454 insertions(+) create mode 100644 src/fesa/analysis/hht_field_convergence.cpp create mode 100644 src/fesa/analysis/hht_field_convergence.h create mode 100644 tests/unit/analysis/hht_field_convergence_test.cpp diff --git a/msvc/fesa_solver.vcxproj b/msvc/fesa_solver.vcxproj index 3ab073a..9d64409 100644 --- a/msvc/fesa_solver.vcxproj +++ b/msvc/fesa_solver.vcxproj @@ -16,6 +16,7 @@ + diff --git a/msvc/fesa_unit_tests.vcxproj b/msvc/fesa_unit_tests.vcxproj index 80e95ee..21ccaa7 100644 --- a/msvc/fesa_unit_tests.vcxproj +++ b/msvc/fesa_unit_tests.vcxproj @@ -21,6 +21,7 @@ + diff --git a/src/fesa/CMakeLists.txt b/src/fesa/CMakeLists.txt index 804c94a..7f70e0b 100644 --- a/src/fesa/CMakeLists.txt +++ b/src/fesa/CMakeLists.txt @@ -9,6 +9,7 @@ add_library( 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 diff --git a/src/fesa/analysis/hht_field_convergence.cpp b/src/fesa/analysis/hht_field_convergence.cpp new file mode 100644 index 0000000..c887db0 --- /dev/null +++ b/src/fesa/analysis/hht_field_convergence.cpp @@ -0,0 +1,166 @@ +#include "analysis/hht_field_convergence.h" + +#include +#include +#include + +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& 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& fields, + const std::array& 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(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(count); + out.spatial_flux = spatial; + if (h.nonzero_count == std::numeric_limits::max() || + h.maximum_count == std::numeric_limits::max()) return result; + const double base = h.nonzero_count == 0 ? .01 : + h.nonzero_sum / static_cast(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(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 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 diff --git a/src/fesa/analysis/hht_field_convergence.h b/src/fesa/analysis/hht_field_convergence.h new file mode 100644 index 0000000..e4123c6 --- /dev/null +++ b/src/fesa/analysis/hht_field_convergence.h @@ -0,0 +1,52 @@ +#ifndef FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_ +#define FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_ + +#include +#include +#include +#include +#include + +#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 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 correction; // max |q_trial-q_previous_selected|, i>=1. + std::optional previous_residual; + std::optional older_residual; +}; +enum class Branch { kNotEvaluated, kLinear, kZeroFlux, kNonquadratic, kNormal }; +struct FieldResult { + std::optional spatial_flux, maximum_flux, time_average_flux, residual_scale; + std::optional estimated_correction, residual_threshold, correction_threshold; + std::optional residual_pass, correction_pass, field_pass; + Branch branch = Branch::kNotEvaluated; +}; +struct Evaluation { + Status status = Status::Failure(FailureCategory::kSolver, {}); + bool field_pass = false; + std::array fields; + // Pure pending snapshot: procedure owns exactly-once commit after every gate/output. + std::array pending; +}; +// Index 0: force/translation; index 1: generalized moment/total rotation phi. +Evaluation Evaluate(const std::array& fields, + const std::array& committed, + std::size_t iteration, double reference_length); +} // namespace fesa::hht_field_convergence + +#endif // FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1e77eac..9c4a638 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,6 +15,7 @@ add_executable( 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 diff --git a/tests/unit/analysis/hht_field_convergence_test.cpp b/tests/unit/analysis/hht_field_convergence_test.cpp new file mode 100644 index 0000000..fd7b8b8 --- /dev/null +++ b/tests/unit/analysis/hht_field_convergence_test.cpp @@ -0,0 +1,232 @@ +#include "analysis/hht_field_convergence.h" +#include +#include +#include +namespace fesa::hht_field_convergence { +namespace { +using Inputs = std::array; +using Histories = std::array; +Inputs Normal() { + Inputs f{}; f[0].active = true; f[0].flux = {{2.0, 0.0}}; + f[0].residual = .01; f[0].increment = 1; f[0].correction = .01; + return f; +} +TEST(HhtFieldConvergence, SignedElementTotalAndSourcePopulationOracle) { + auto f = Normal(); f[0].flux.assign(30, {}); + f[0].flux[0] = {8, -6}; f[0].flux[1] = {-2, 0}; + f[0].flux[24] = {3, 0}; f[0].flux[27] = {-3, 0}; + Histories h{}; h[0].maximum_sum = 10; h[0].maximum_count = 1; + auto r = Evaluate(f, h, 1, 2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux, 2.5); + r = Evaluate(f, {}, 1, 2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux, 1.0/3.0); +} +TEST(HhtFieldConvergence, NormalBoundaryAndInitialCorrectionMissing) { + auto f = Normal(); auto r = Evaluate(f, {}, 1, 2); + ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass); + f[0].residual = std::nextafter(.005 * 2, INFINITY); + r = Evaluate(f, {}, 1, 2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass); + f = Normal(); r = Evaluate(f, {}, 0, 2); + ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass); + EXPECT_FALSE(r.fields[0].correction_pass.has_value()); +} +TEST(HhtFieldConvergence, ScalarPopulationThresholdAndReactivation) { + auto f=Normal(); Histories h{}; h[0].maximum_sum=100; h[0].maximum_count=1; + f[0].flux={{0,0},{.001,0},{10,0}}; + auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux,5.0005); + f[0].flux[2].internal=std::nextafter(10.,0.); + r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux,(.001+std::nextafter(10.,0.))/3); + f[0].flux={{0,0},{0,0}}; r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux,0); +} +TEST(HhtFieldConvergence, InitialAverageAndNonzeroEligibilityBoundary) { + auto f=Normal(); const double edge=1e-5*.01; + for (double x:{std::nextafter(edge,0.),edge,std::nextafter(edge,INFINITY)}) { + f[0].flux={{x,0}}; auto r=Evaluate(f,{},0,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_DOUBLE_EQ(*r.fields[0].time_average_flux,x>edge?x:.01); + EXPECT_EQ(r.pending[0].nonzero_count,x>edge?1U:0U); + EXPECT_EQ(r.pending[0].maximum_count,1U); + } +} +TEST(HhtFieldConvergence, PendingHistoryIsRecomputedAndRollbackIsPure) { + auto f=Normal(); Histories h{}; h[0]={10,1,4,1}; + f[0].flux={{30,0}}; auto a=Evaluate(f,h,1,2); ASSERT_TRUE(a.status.IsOk()); + EXPECT_DOUBLE_EQ(*a.fields[0].time_average_flux,20); + f[0].flux={{70,0}}; auto b=Evaluate(f,h,2,2); ASSERT_TRUE(b.status.IsOk()); + EXPECT_DOUBLE_EQ(*b.fields[0].time_average_flux,40); + EXPECT_DOUBLE_EQ(b.pending[0].nonzero_sum,80); + EXPECT_DOUBLE_EQ(b.pending[0].maximum_sum,74); + EXPECT_EQ(b.pending[0].maximum_count,2U); + EXPECT_DOUBLE_EQ(h[0].nonzero_sum,10); EXPECT_EQ(h[0].nonzero_count,1U); + f[0].flux={{0,0}}; auto z=Evaluate(f,h,1,2); ASSERT_TRUE(z.status.IsOk()); + EXPECT_EQ(z.pending[0].nonzero_count,1U); EXPECT_EQ(z.pending[0].maximum_count,2U); + EXPECT_DOUBLE_EQ(z.pending[0].maximum_sum,4); +} +TEST(HhtFieldConvergence, RawScaleConversionIsSimultaneousAndUnitConsistent) { + auto f=Normal(); Histories h{}; h[0]={3,1,3,1}; h[1]={.3,1,.3,1}; + f[0].flux={{3,0}}; f[1].active=true; f[1].flux={{.1,0}}; + auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_DOUBLE_EQ(*r.fields[0].residual_scale,3); + EXPECT_DOUBLE_EQ(*r.fields[1].time_average_flux,.2); + EXPECT_DOUBLE_EQ(*r.fields[1].residual_scale,6); + f[1].active=false; r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.fields[1].branch,Branch::kNotEvaluated); + EXPECT_FALSE(r.fields[1].field_pass.has_value()); +} +TEST(HhtFieldConvergence, EstimatedCorrectionHasRealHistoryAndNormalOnly) { + auto f=Normal(); f[0].flux={{400,0}}; f[0].residual=2; + f[0].correction=.04; f[0].previous_residual=4; f[0].older_residual=8; + auto r=Evaluate(f,{},2,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_DOUBLE_EQ(*r.fields[0].estimated_correction,.02); EXPECT_FALSE(r.field_pass); + f[0].residual=1; r=Evaluate(f,{},2,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_DOUBLE_EQ(*r.fields[0].estimated_correction,.01); EXPECT_TRUE(r.field_pass); + f[0].older_residual=0; r=Evaluate(f,{},2,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_FALSE(r.fields[0].estimated_correction.has_value()); EXPECT_FALSE(r.field_pass); + f[0].older_residual=8; r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_FALSE(r.fields[0].estimated_correction.has_value()); EXPECT_FALSE(r.field_pass); +} +TEST(HhtFieldConvergence, NonquadraticIterationAndRateBoundaries) { + auto f=Normal(); f[0].flux={{100,0}}; f[0].residual=2; + f[0].previous_residual=4; f[0].older_residual=8; + auto r=Evaluate(f,{},9,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.fields[0].branch,Branch::kNormal); EXPECT_FALSE(r.field_pass); + r=Evaluate(f,{},10,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.fields[0].branch,Branch::kNonquadratic); EXPECT_TRUE(r.field_pass); + EXPECT_FALSE(r.fields[0].estimated_correction.has_value()); + f[0].older_residual=16; f[0].residual=1; + r=Evaluate(f,{},10,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_EQ(r.fields[0].branch,Branch::kNonquadratic); + f[0].residual=.5; r=Evaluate(f,{},10,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.fields[0].branch,Branch::kNormal); + f[0].residual=4; r=Evaluate(f,{},10,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.fields[0].branch,Branch::kNormal); +} +TEST(HhtFieldConvergence, ZeroFluxUsesResidualOrActualCorrectionAtBoundary) { + auto f=Normal(); Histories h{}; h[0]={2,1,2,1}; + f[0].flux={{0,0}}; f[0].residual=1e-5*2; f[0].correction.reset(); + auto r=Evaluate(f,h,0,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass); + EXPECT_EQ(r.fields[0].branch,Branch::kZeroFlux); EXPECT_FALSE(r.fields[0].correction_pass.has_value()); + f[0].residual=std::nextafter(1e-5*2,INFINITY); + r=Evaluate(f,h,0,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass); + f[0].correction=.001; r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass); + f[0].correction=std::nextafter(.001,INFINITY); + r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass); + f[0].increment=0; f[0].correction=0; + r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass); +} +TEST(HhtFieldConvergence, LinearRequiresEveryActiveFieldAndAnActualIteration) { + auto f=Normal(); f[1]=f[0]; f[1].flux={{4,0}}; + f[0].residual=2e-8; f[1].residual=4e-8; f[0].correction.reset(); f[1].correction.reset(); + auto r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass); + EXPECT_EQ(r.fields[0].branch,Branch::kLinear); EXPECT_EQ(r.fields[1].branch,Branch::kLinear); + f[1].residual=std::nextafter(4e-8,INFINITY); + r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass); + EXPECT_EQ(r.fields[0].branch,Branch::kNormal); + f[1].residual=4e-8; r=Evaluate(f,{},0,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass); +} +TEST(HhtFieldConvergence, SmallTranslationExemptionIsStrictAndNeverRotational) { + auto f=Normal(); f[0].increment=std::nextafter(2e-8,0.); f[0].correction=1; + auto r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass); + EXPECT_FALSE(r.fields[0].correction_pass.has_value()); + f[0].increment=2e-8; r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass); + f[1]=f[0]; f[0].active=false; f[1].increment=0; f[1].correction=0; + r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_TRUE(r.field_pass); + f[1].correction=std::numeric_limits::denorm_min(); + r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); EXPECT_FALSE(r.field_pass); +} +TEST(HhtFieldConvergence, InvalidAndOverflowNeverBecomeMissingOrPassing) { + const double inf=std::numeric_limits::infinity(); + const double nan=std::numeric_limits::quiet_NaN(); + const double max=std::numeric_limits::max(); + for(double x:{0.,-1.,inf,nan}) EXPECT_FALSE(Evaluate(Normal(),{},1,x).status.IsOk()); + for(double x:{-1.,inf,nan}) { + auto f=Normal(); f[0].residual=x; EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk()); + f=Normal(); f[0].correction=x; EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk()); + f=Normal(); f[0].older_residual=x; EXPECT_FALSE(Evaluate(f,{},2,2).status.IsOk()); + f=Normal(); f[0].flux={{x,0}}; + if(x!=-1.) EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk()); + } + auto f=Normal(); f[0].flux.clear(); EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk()); + f=Normal(); f[0].flux={{max,max}}; EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk()); + f=Normal(); f[0].flux={{max,0},{max,0}}; EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk()); + f=Normal(); f[0].older_residual=std::numeric_limits::denorm_min(); + f[0].previous_residual=1; EXPECT_FALSE(Evaluate(f,{},2,2).status.IsOk()); + Histories h{}; h[0]={max,1,max,1}; f=Normal(); f[0].flux={{max,0}}; EXPECT_FALSE(Evaluate(f,h,1,2).status.IsOk()); + h={}; h[0].maximum_count=std::numeric_limits::max(); + EXPECT_FALSE(Evaluate(Normal(),h,1,2).status.IsOk()); + h={}; h[0]={0,1,0,1}; f=Normal(); f[0].flux={{0,0}}; + EXPECT_FALSE(Evaluate(f,h,1,2).status.IsOk()); + f=Normal(); f[0].residual=1e200; f[0].correction=1e200; + f[0].older_residual=1; f[0].previous_residual=1; + const auto overflow=Evaluate(f,{},2,2); + EXPECT_FALSE(overflow.status.IsOk()); EXPECT_FALSE(overflow.field_pass); + f=Normal(); f[1]=f[0]; f[0].flux={{max,0}}; + EXPECT_FALSE(Evaluate(f,{},1,2).status.IsOk()); + Inputs inactive{}; auto r=Evaluate(inactive,{},0,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_TRUE(r.field_pass); EXPECT_FALSE(r.fields[0].field_pass.has_value()); +} +TEST(HhtFieldConvergence, DecisionThresholdsBelowEqualAboveWithoutTolerance) { + auto f=Normal(); + for(double x:{std::nextafter(.01,0.),.01,std::nextafter(.01,INFINITY)}) { + f[0].correction=x; auto r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.field_pass,x<=.01); + } + for(double x:{std::nextafter(.01,0.),.01,std::nextafter(.01,INFINITY)}) { + f=Normal(); f[0].residual=x; auto r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.field_pass,x<=.01); + } + for(double x:{std::nextafter(2.,0.),2.,std::nextafter(2.,INFINITY)}) { + f=Normal(); f[0].flux={{100,0}}; f[0].residual=x; + f[0].previous_residual=4; f[0].older_residual=8; + auto r=Evaluate(f,{},10,2); ASSERT_TRUE(r.status.IsOk()); + // For below 2 the detector selects normal; equality/above selects Rp. + EXPECT_EQ(r.field_pass,x==2.); + } +} +TEST(HhtFieldConvergence, MixedIncidenceRetainsEveryElementScalarOccurrence) { + auto f=Normal(); f[0].flux={{6,0},{-2,0},{-1,0},{9,0},{0,0}}; + // Three occurrences can share one global DOF and one belong to another. + // Node averaging would produce 6, signed assembly would produce 3 and 9. + auto r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux,3.6); + EXPECT_DOUBLE_EQ(*r.fields[0].maximum_flux,9); +} + +TEST(HhtFieldConvergence, RemainingThresholdsHaveAdjacentRepresentableBoundaries) { + for (double x:{std::nextafter(.001,0.),.001,std::nextafter(.001,INFINITY)}) { + auto f=Normal(); Histories h{}; h[0]={2,1,2,1}; + f[0].flux={{0,0}}; f[0].residual=.1; f[0].correction=x; + auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.field_pass,x<=.001); + } + for (double x:{std::nextafter(2e-5,0.),2e-5,std::nextafter(2e-5,INFINITY)}) { + auto f=Normal(); Histories h{}; h[0]={2,1,2,1}; + f[0].flux={{0,0}}; f[0].residual=x; f[0].correction.reset(); + auto r=Evaluate(f,h,0,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.field_pass,x<=2e-5); + } + for (double x:{std::nextafter(2e-8,0.),2e-8,std::nextafter(2e-8,INFINITY)}) { + auto f=Normal(); f[0].increment=x; f[0].correction=1; + auto r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.field_pass,x<2e-8); + f=Normal(); f[0].residual=x; f[0].correction.reset(); + r=Evaluate(f,{},1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.fields[0].branch,x<=2e-8?Branch::kLinear:Branch::kNormal); + } + for (double x:{std::nextafter(.001,0.),.001,std::nextafter(.001,INFINITY)}) { + auto f=Normal(); Histories h{}; h[0]={0,0,100,1}; + f[0].flux={{0,0},{x,0},{10,0}}; + auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_DOUBLE_EQ(*r.fields[0].spatial_flux,x<.001?10:(10+x)/2); + } + for (double x:{std::nextafter(2.,0.),2.,std::nextafter(2.,INFINITY)}) { + auto f=Normal(); f[0].flux={{100,0}}; f[0].residual=x; + f[0].previous_residual=4; f[0].older_residual=16; + auto r=Evaluate(f,{},10,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.fields[0].branch,Branch::kNonquadratic); + EXPECT_EQ(r.field_pass,x<=2); + } +} +} // namespace +} // namespace fesa::hht_field_convergence From 0e392a7364b654269b3144267000c1f1b4999cab Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Mon, 19 Oct 2026 02:24:40 +0900 Subject: [PATCH 05/11] Cover remaining HHT field policy boundary oracles --- .../analysis/hht_field_convergence_test.cpp | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/unit/analysis/hht_field_convergence_test.cpp b/tests/unit/analysis/hht_field_convergence_test.cpp index fd7b8b8..cdab714 100644 --- a/tests/unit/analysis/hht_field_convergence_test.cpp +++ b/tests/unit/analysis/hht_field_convergence_test.cpp @@ -228,5 +228,98 @@ TEST(HhtFieldConvergence, RemainingThresholdsHaveAdjacentRepresentableBoundaries EXPECT_EQ(r.field_pass,x<=2); } } +TEST(HhtFieldConvergence, CurrentIncludedEligibilityBoundaryPreservesPendingNonzeroHistory) { + Histories h{}; h[0]={199999,1,199999,1}; + // 199999+1 rounds exactly to 200000; current-included mean is 100000. + // Its epsilon threshold is exactly 1, while the old-base threshold is 1.99999. + for (double b:{std::nextafter(1.,0.),1.,std::nextafter(1.,INFINITY)}) { + auto f=Normal(); f[0].flux={{b,0}}; + const auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(*r.fields[0].spatial_flux,b); + EXPECT_EQ(*r.fields[0].time_average_flux,b>1?100000:199999); + EXPECT_EQ(r.pending[0].nonzero_sum,b>1?200000:199999); + EXPECT_EQ(r.pending[0].nonzero_count,b>1?2U:1U); + EXPECT_EQ(h[0].nonzero_sum,199999); + EXPECT_EQ(h[0].nonzero_count,1U); + } + auto f=Normal(); f[0].flux={{1.5,0}}; + const auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(*r.fields[0].time_average_flux,100000.25); + EXPECT_EQ(r.pending[0].nonzero_sum,200000.5); + EXPECT_EQ(r.pending[0].nonzero_count,2U); +} +TEST(HhtFieldConvergence, ConversionSpatialPrerequisiteHasAdjacentBoundaries) { + Histories h{}; h[0]={1023,1023,1023,1023}; h[1]={4,1,4,1}; + // Adding any adjacent-to-one b to 1023 rounds to 1024: raw force stays 1. + // Peer raw moment stays 4 and L=1 makes the candidate force scale exactly 4. + for (double b:{std::nextafter(1.,0.),1.,std::nextafter(1.,INFINITY)}) { + auto f=Normal(); f[0].flux={{b,0}}; f[1]=f[0]; f[1].flux={{4,0}}; + const auto r=Evaluate(f,h,1,1); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(*r.fields[0].spatial_flux,b); + EXPECT_EQ(*r.fields[0].time_average_flux,1); + EXPECT_EQ(*r.fields[1].time_average_flux,4); + EXPECT_EQ(*r.fields[0].residual_scale,b<=1?4:1); + EXPECT_EQ(r.pending[0].nonzero_sum,1024); + EXPECT_EQ(r.pending[0].nonzero_count,1024U); + EXPECT_EQ(r.pending[1].nonzero_sum,8); + EXPECT_EQ(r.pending[1].nonzero_count,2U); + EXPECT_EQ(h[0].nonzero_sum,1023); + EXPECT_EQ(h[0].nonzero_count,1023U); + EXPECT_EQ(h[1].nonzero_sum,4); + } +} +TEST(HhtFieldConvergence, ConversionRawVersusConvertedScaleHasAdjacentBoundaries) { + Histories h{}; h[0]={1,1,1,1}; h[1]=h[0]; + // Zero samples preserve both raw means at 1. Only L changes convF=1/L. + for (double length:{std::nextafter(1.,0.),1.,std::nextafter(1.,INFINITY)}) { + auto f=Normal(); f[0].flux={{0,0}}; f[1]=f[0]; + const auto r=Evaluate(f,h,1,length); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(*r.fields[0].time_average_flux,1); + EXPECT_EQ(*r.fields[1].time_average_flux,1); + // Reciprocating the adjacent value below 1 rounds to the adjacent value above 1. + EXPECT_EQ(*r.fields[0].residual_scale, + length<1?std::nextafter(1.,INFINITY):1); + for (std::size_t a=0; a<2; ++a) { + EXPECT_EQ(r.pending[a].nonzero_sum,1); + EXPECT_EQ(r.pending[a].nonzero_count,1U); + EXPECT_EQ(h[a].nonzero_sum,1); + EXPECT_EQ(h[a].nonzero_count,1U); + } + } +} +TEST(HhtFieldConvergence, ZeroFluxClassificationTestsSpatialFluxBoundary) { + // A peer raw mean of 1 converts the small force field to a fixed scale of 1. + // Vary bbar, not rho, across epsilon*scale; rho=.1 prevents the linear exemption. + for (double b:{std::nextafter(1e-5,0.),1e-5,std::nextafter(1e-5,INFINITY)}) { + auto f=Normal(); f[0].flux={{b,0}}; f[0].residual=.1; + f[1]=f[0]; f[1].flux={{1,0}}; + const auto r=Evaluate(f,{},1,1); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(*r.fields[0].spatial_flux,b); + EXPECT_EQ(*r.fields[0].residual_scale,1); + EXPECT_EQ(r.fields[0].branch,b<=1e-5?Branch::kZeroFlux:Branch::kNormal); + } +} +TEST(HhtFieldConvergence, EstimatedCorrectionAdjacentBoundariesDriveNormalCorrectionPass) { + for (double estimate:{std::nextafter(.01,0.),.01,std::nextafter(.01,INFINITY)}) { + auto f=Normal(); f[0].flux={{400,0}}; f[0].residual=1; + // Prior residual minimum is 4; power-of-two scaling preserves adjacent bits. + // The actual correction exceeds .01 in all three cases, isolating the estimate. + f[0].correction=4*estimate; f[0].previous_residual=4; f[0].older_residual=8; + const auto r=Evaluate(f,{},2,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(r.fields[0].branch,Branch::kNormal); + ASSERT_GT(*f[0].correction,.01); + EXPECT_EQ(*r.fields[0].estimated_correction,estimate); + EXPECT_EQ(*r.fields[0].correction_pass,estimate<=.01); + EXPECT_EQ(r.field_pass,estimate<=.01); + } +} +TEST(HhtFieldConvergence, PopulationReactivationThresholdImmediatelyAboveUsesFilteredPopulation) { + auto f=Normal(); Histories h{}; h[0]={0,0,100,1}; + const double maximum=std::nextafter(10.,INFINITY); + f[0].flux={{0,0},{.001,0},{maximum,0}}; + const auto r=Evaluate(f,h,1,2); ASSERT_TRUE(r.status.IsOk()); + EXPECT_EQ(*r.fields[0].maximum_flux,maximum); + EXPECT_EQ(*r.fields[0].spatial_flux,(10.000000000000002+.001)/2); +} } // namespace } // namespace fesa::hht_field_convergence From dd54b0ae1e68a313ac46933b886c865c85ec4432 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Mon, 19 Oct 2026 02:49:53 +0900 Subject: [PATCH 06/11] Connect HHT field convergence to production acceptance and v2 logs --- docs/contracts.md | 24 ++ include/fesa/analysis/newton_iteration_log.h | 22 +- src/fesa/analysis/hht_dynamic_assembly.cpp | 36 +- src/fesa/analysis/hht_dynamic_internal.h | 12 +- src/fesa/analysis/hht_reduced_procedure.cpp | 110 +++++- src/fesa/analysis/newton_iteration_log.cpp | 90 +++++ .../analysis/hht_dynamic_algebra_test.cpp | 35 ++ .../analysis/hht_dynamic_procedure_test.cpp | 359 ++++++++++++++++++ .../analysis/newton_iteration_log_test.cpp | 146 +++++++ 9 files changed, 811 insertions(+), 23 deletions(-) diff --git a/docs/contracts.md b/docs/contracts.md index b8b2a0c..868b542 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -105,3 +105,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 측정에는 이 비용을 포함하며 출력 실패 안전성을 위해 이를 생략하지 않는다. diff --git a/include/fesa/analysis/newton_iteration_log.h b/include/fesa/analysis/newton_iteration_log.h index c59ab27..6ce4a7b 100644 --- a/include/fesa/analysis/newton_iteration_log.h +++ b/include/fesa/analysis/newton_iteration_log.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #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 residual_numerator, residual_scale, spatial_flux; + std::optional time_average_flux, correction_numerator, increment_norm; + std::optional estimated_correction, residual_threshold, correction_threshold; + std::string branch = "not-evaluated"; + std::optional 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 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 fields; + std::optional 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); diff --git a/src/fesa/analysis/hht_dynamic_assembly.cpp b/src/fesa/analysis/hht_dynamic_assembly.cpp index bec9a3f..f4a5e1a 100644 --- a/src/fesa/analysis/hht_dynamic_assembly.cpp +++ b/src/fesa/analysis/hht_dynamic_assembly.cpp @@ -191,8 +191,34 @@ Result ReferenceLength(const Domain& domain) { return Result::Success(length); } +Result> MeasureEndpointFields( + const DofManager& dofs, const DynamicAcceptedFrameState& trial, + const DynamicAcceptedFrameState& accepted, const std::optional& previous) { + using Fields = std::array; + 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::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::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::Success(std::move(fields)); +} + Result> AssembleLoads(const Domain& domain, - const DofManager& dofs) { + const DofManager& dofs, + std::array, 2>* field_flux) { const auto index = SourceTargetIndex::FromDomain(domain); const SourceTargetResolver resolver(index); const LoadContext context{domain, dofs, resolver}; @@ -203,6 +229,9 @@ Result> AssembleLoads(const Domain& domain, if (!contributions.HasValue()) return Result>::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]) || @@ -489,6 +518,7 @@ Result EvaluateAssembly( Vector third(count); Vector third_scale(count); std::array, 6> entries; + std::array, 2> field_flux; for (auto& values : entries) values.reserve(shells.size() * 24U * 24U); double momentum_scale = 0.0; double angular_scale = 0.0; @@ -546,6 +576,8 @@ Result 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]; @@ -708,7 +740,7 @@ Result 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)}); } Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs, diff --git a/src/fesa/analysis/hht_dynamic_internal.h b/src/fesa/analysis/hht_dynamic_internal.h index 2911f41..e18dd62 100644 --- a/src/fesa/analysis/hht_dynamic_internal.h +++ b/src/fesa/analysis/hht_dynamic_internal.h @@ -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,7 @@ struct DynamicAssembly { // Canonical scalar drilling-constraint derivative, indexed by global node. std::optional hidden_gradient; std::optional trial_constraints; + std::array, 2> field_flux; }; /// @brief Builds checked shell evaluators in stable source order. @@ -104,9 +106,17 @@ Result> BuildRuntimeShells(const Domain& domain, /// @brief Returns the maximum initial active-element edge length. Result 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> MeasureEndpointFields( + const DofManager& dofs, const DynamicAcceptedFrameState& trial, + const DynamicAcceptedFrameState& accepted, + const std::optional& previous); + /// @brief Assembles dead loads and their uncancelled absolute row scales. Result> AssembleLoads(const Domain& domain, - const DofManager& dofs); + const DofManager& dofs, + std::array, 2>* field_flux = nullptr); /// @brief Evaluates current kinetic and internal quantities without commit. Result EvaluateAssembly( diff --git a/src/fesa/analysis/hht_reduced_procedure.cpp b/src/fesa/analysis/hht_reduced_procedure.cpp index 408b8e4..b109d1c 100644 --- a/src/fesa/analysis/hht_reduced_procedure.cpp +++ b/src/fesa/analysis/hht_reduced_procedure.cpp @@ -150,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, @@ -189,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) { @@ -433,7 +435,8 @@ Result ExecuteReducedHht( auto length_result = ReferenceLength(domain); if (!length_result.HasValue()) return Result::Failure(length_result.GetStatus()); const double length = length_result.Value(); - auto loads = AssembleLoads(domain, dofs); + std::array, 2> load_flux; + auto loads = AssembleLoads(domain, dofs, &load_flux); if (!loads.HasValue()) return Result::Failure(loads.GetStatus()); auto parameters_result = MakeHhtParameters(domain.Dynamic()->alpha); if (!parameters_result.HasValue()) return Result::Failure(parameters_result.GetStatus()); @@ -699,9 +702,11 @@ Result ExecuteReducedHht( status = log.Write(record); if (status.IsOk()) status = log.Flush(); if (!status.IsOk()) return Result::Failure(status); + std::array flux_history{}; struct ReducedStepCandidate { DynamicAcceptedFrameState frame; ReducedHistory history; + std::array flux_history; }; for (std::uint32_t increment = 1U; increment <= domain.Dynamic()->increment_count; ++increment) { @@ -711,9 +716,14 @@ Result 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; }; @@ -744,6 +754,10 @@ Result 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 previous_selected_q; + std::array, 2> previous_residual, older_residual; bool translation_private_algorithmic_pass = true; bool copy_translation_rates = false; Vector retained_velocity{history.p.Size()}; @@ -755,6 +769,9 @@ Result ExecuteReducedHht( }; ReducedShellHhtStep 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"; @@ -896,7 +913,53 @@ Result 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); @@ -932,7 +995,7 @@ Result 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; @@ -1059,11 +1122,15 @@ Result 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) @@ -1115,16 +1182,21 @@ Result 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::Failure(status); } diff --git a/src/fesa/analysis/newton_iteration_log.cpp b/src/fesa/analysis/newton_iteration_log.cpp index 895364e..517515b 100644 --- a/src/fesa/analysis/newton_iteration_log.cpp +++ b/src/fesa/analysis/newton_iteration_log.cpp @@ -261,10 +261,100 @@ Result SerializeRecord(const NewtonIterationLogRecord& record, return Result::Success(std::move(line)); } +// Candidate policy has separate names and units; v1 evidence is never redefined. +Result 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::Failure( + LogOutputFailure(path, "Invalid HHT field-policy log identity.")); + std::vector fields; + const auto number = [&](const std::string& key, + const std::optional& 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& 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::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>, 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::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::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{value} : std::nullopt; + }; + const std::array>, 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::Failure(status); + } + } + boolean("strict_residual_pass", record.strict_residual_evaluated ? std::optional{record.residual_pass} : std::nullopt); + boolean("strict_correction_pass", record.strict_correction_evaluated ? std::optional{record.correction_pass} : std::nullopt); + std::string line; + for (const auto& field : fields) { + if (!line.empty()) line += '\t'; + line += field; + } + line += '\n'; + return Result::Success(std::move(line)); +} + /// @brief Serializes dynamic evidence without static load-factor semantics. Result 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::Failure(time.GetStatus()); if (record.time < 0.0 || record.stage.empty() || record.outcome.empty()) { diff --git a/tests/unit/analysis/hht_dynamic_algebra_test.cpp b/tests/unit/analysis/hht_dynamic_algebra_test.cpp index f77f540..a6da082 100644 --- a/tests/unit/analysis/hht_dynamic_algebra_test.cpp +++ b/tests/unit/analysis/hht_dynamic_algebra_test.cpp @@ -669,4 +669,39 @@ TEST(HhtDynamicAlgebra, RejectsAdmissibleNullRankChangeBeforeAcceptingFrame) { EXPECT_EQ(frame.hidden_constraint[0], 23.0); } +TEST(HhtEndpointFields, MeasuresActualFullChartAcrossNonlinearRootAndPartialBoundary) { + const auto fixture = MakeFixture(1U, true); + auto trial = EmptyTrial(fixture), old = EmptyTrial(fixture); + trial.residual = fesa::Vector{24U}; + fesa::Vector previous{24U}; + // Independent nonlinear graph q_phiZ=p^2: p .1 -> .2. The + // previous tangent lift is .02, while the actual endpoint change is .03. + previous[0] = .1; previous[5] = .01; + trial.generalized_coordinates[0] = .2; + trial.generalized_coordinates[5] = .04; + trial.residual[0] = 3; trial.residual[5] = -5; + // Large constrained values and spatial support moments are not free + // generalized endpoint evidence and cannot change either scalar maximum. + trial.generalized_coordinates[3] = 1000; + trial.residual[3] = 2000; + trial.reaction = fesa::Vector{24U}; trial.reaction[5] = 3000; + const auto result = fesa::hht_internal::MeasureEndpointFields(fixture.dofs, trial, old, previous); + ASSERT_TRUE(result.HasValue()); + EXPECT_TRUE(result.Value()[0].active); EXPECT_TRUE(result.Value()[1].active); + EXPECT_DOUBLE_EQ(result.Value()[0].residual, 3); + EXPECT_DOUBLE_EQ(result.Value()[1].residual, 5); + EXPECT_DOUBLE_EQ(result.Value()[0].increment, .2); + EXPECT_DOUBLE_EQ(result.Value()[1].increment, .04); + EXPECT_DOUBLE_EQ(*result.Value()[0].correction, .2 - .1); + EXPECT_DOUBLE_EQ(*result.Value()[1].correction, .04 - .01); + EXPECT_NE(*result.Value()[1].correction, 2 * .1 * (.2 - .1)); + const auto initial = fesa::hht_internal::MeasureEndpointFields(fixture.dofs, trial, old, std::nullopt); + ASSERT_TRUE(initial.HasValue()); + EXPECT_FALSE(initial.Value()[0].correction.has_value()); + EXPECT_FALSE(initial.Value()[1].correction.has_value()); + previous[0] = -std::numeric_limits::max(); + trial.generalized_coordinates[0] = std::numeric_limits::max(); + EXPECT_FALSE(fesa::hht_internal::MeasureEndpointFields(fixture.dofs, trial, old, previous).HasValue()); +} + } // namespace diff --git a/tests/unit/analysis/hht_dynamic_procedure_test.cpp b/tests/unit/analysis/hht_dynamic_procedure_test.cpp index 9e87653..1e97734 100644 --- a/tests/unit/analysis/hht_dynamic_procedure_test.cpp +++ b/tests/unit/analysis/hht_dynamic_procedure_test.cpp @@ -8,9 +8,16 @@ #include #include #include +#include +#include #include #include "fesa/analysis/newton_iteration_log.h" +#include "fesa/analysis/analysis_model.h" +#include "analysis/hht_dynamic_internal.h" +#include "analysis/hht_reduced_integrator.h" +#include "analysis/hht_reduced_procedure.h" +#include "analysis/equilibrium_algorithm.h" #include "fesa/assembly/parallel_for.h" #include "fesa/solvers/linear/mkl_pardiso_solver.h" #include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h" @@ -569,3 +576,355 @@ TEST(HhtDynamicProcedure, } } // namespace + +namespace { +std::map LogFields(const std::string& line) { + std::map fields; + std::istringstream input(line); + std::string token; + while (std::getline(input, token, '\t')) { + const auto equal = token.find('='); + if (equal != std::string::npos) fields.emplace(token.substr(0, equal), token.substr(equal + 1)); + } + return fields; +} +} + +namespace { +fesa::ModelDefinition SymmetricExtension() { + auto definition = MakeDefinition(false); + definition.dynamic_step->initial_increment = 0.1; + definition.dynamic_step->time_period = 0.1; + definition.dynamic_step->increment_count = 1U; + definition.node_sets.push_back({"Left", std::nullopt, {0U, 3U}, {"dynamic-state.inp", 30U}}); + definition.node_sets.push_back({"Right", std::nullopt, {1U, 2U}, {"dynamic-state.inp", 31U}}); + definition.dynamic_step->boundaries = { + {"All", 2, 6, 0.0, {"dynamic-state.inp", 32U}}}; + definition.dynamic_step->loads = { + {"Left", 1, -1.0, {"dynamic-state.inp", 33U}}, + {"Right", 1, 1.0, {"dynamic-state.inp", 34U}}}; + return definition; +} +} + +TEST(HhtDynamicProcedure, FieldPolicyActuallyTerminatesSymmetricExtensionBeforeStrict) { + auto definition = SymmetricExtension(); + fesa::SerialParallelFor parallel; + auto run = RunRigidTranslation(definition, parallel, "field-policy-extension"); + ASSERT_TRUE(run.HasValue()) << (run.HasValue() ? "" : FailureText(run.GetStatus())); + const auto& frame = run.Value().execution.state.CommittedState(); + ASSERT_GT(frame.iteration_count, 0U); + ASSERT_LE(frame.iteration_count, 100U); + const auto& text = run.Value().log; + const auto accepted = text.rfind("record=dynamic-newton-iteration"); + ASSERT_NE(accepted, std::string::npos); + const auto line = text.substr(accepted); + EXPECT_NE(line.find("\tschema=hht-log-v2\t"), std::string::npos); + EXPECT_NE(line.find("\tpolicy_id=abaqus2025-field-on-fesa-v1\t"), std::string::npos); + EXPECT_NE(line.find("\tstep=Transient Load\t"), std::string::npos); + EXPECT_NE(line.find("\tfield_pass=1\taccepted=1\t"), std::string::npos); + EXPECT_NE(line.find("\tstrict_correction_pass=0"), std::string::npos); + for (const auto value : frame.global_equilibrium) EXPECT_LE(std::abs(value), 1e-8); + auto fields = LogFields(line.substr(0, line.find('\n'))); + double rho = 0, increment = 0, flux = 4.0; + for (std::size_t node = 0; node < 4; ++node) { + rho = (std::max)(rho, std::abs(frame.residual[6 * node])); + increment = (std::max)(increment, std::abs(frame.generalized_coordinates[6 * node])); + for (std::size_t c = 0; c < 3; ++c) + flux += std::abs(frame.internal_force[6 * node + c] + frame.inertia_force[6 * node + c]); + } + EXPECT_DOUBLE_EQ(std::stod(fields.at("force_residual_numerator")), rho); + EXPECT_DOUBLE_EQ(std::stod(fields.at("force_increment_norm")), increment); + EXPECT_DOUBLE_EQ(std::stod(fields.at("force_spatial_flux")), flux / 24.0); + EXPECT_DOUBLE_EQ(std::stod(fields.at("force_time_average_flux")), flux / 24.0); + EXPECT_LE(rho, .005 * std::stod(fields.at("force_residual_scale"))); + EXPECT_DOUBLE_EQ(std::stod(fields.at("force_residual_threshold")), .005); + EXPECT_DOUBLE_EQ(std::stod(fields.at("force_correction_threshold")), .01); + EXPECT_EQ(fields.at("force_residual_pass"), "1"); + EXPECT_EQ(fields.at("force_correction_pass"), "1"); + const double c = std::stod(fields.at("force_correction_numerator")); + const auto& estimated = fields.at("force_estimated_correction"); + EXPECT_TRUE(c <= .01 * increment || + (estimated != "not-evaluated" && std::stod(estimated) <= .01 * increment)); + EXPECT_EQ(fields.at("force_branch"), "normal"); + EXPECT_EQ(fields.at("moment_branch"), "not-evaluated"); + // Symmetric monotone extension has q=(-D,+D,+D,-D). This independent + // endpoint oracle includes the selected iteration-zero start, not the lift. + std::istringstream records(text); + std::string row; + double previous_D = 0; + bool first = true; + while (std::getline(records, row)) { + const auto values = LogFields(row); + if (values.at("schema") != "hht-log-v2") continue; + const double D = std::stod(values.at("force_increment_norm")); + if (first) { + EXPECT_EQ(values.at("force_correction_numerator"), "not-evaluated"); + EXPECT_EQ(values.at("force_estimated_correction"), "not-evaluated"); + EXPECT_EQ(values.at("field_pass"), "0"); + } else { + EXPECT_NEAR(std::stod(values.at("force_correction_numerator")), + std::abs(D - previous_D), 1e-16); + } + previous_D = D; + first = false; + } +} + +TEST(HhtDynamicProcedure, FieldPassDoesNotBypassPhysicalAcceptanceAndLogsActualCause) { + auto definition = MakeDefinition(false); + definition.materials[0].youngs_modulus = 1e9; + definition.dynamic_step->increment_count = 1; + definition.dynamic_step->time_period = definition.dynamic_step->initial_increment; + definition.node_sets[0].node_indices = {0U, 3U}; + definition.node_sets[1].node_indices = {2U}; + definition.dynamic_step->boundaries = {{"All", 1, 6, 0, {"dynamic-state.inp", 11U}}}; + definition.dynamic_step->loads = {{"Loaded", 3, -40000, {"dynamic-state.inp", 12U}}}; + auto domain = fesa::Domain::Create(definition); + ASSERT_TRUE(domain.HasValue()); + fesa::SerialParallelFor parallel; + fesa::MklPardisoSolver mass; + fesa::MklPardisoTangentSolver tangent; + fesa::NewtonIterationLogWriter log; + const auto path = std::filesystem::temp_directory_path() / "fesa-field-post-gate.log"; + ASSERT_TRUE(log.Open(path).IsOk()); + auto result = fesa::HhtDynamicProcedureExecutor(parallel, mass, tangent).Execute(domain.Value(), log); + ASSERT_TRUE(log.Close().IsOk()); + std::ifstream input(path, std::ios::binary); + const std::string text{std::istreambuf_iterator{input}, {}}; + input.close(); + std::filesystem::remove(path); + ASSERT_FALSE(result.HasValue()); + const auto row = text.substr(text.rfind("record=")); + const auto fields = LogFields(row.substr(0, row.find('\n'))); + EXPECT_EQ(fields.at("field_pass"), "1"); + EXPECT_EQ(fields.at("accepted"), "0"); + EXPECT_EQ(fields.at("outcome"), "failed"); + const auto cause = std::find_if(result.GetStatus().Diagnostics().begin(), + result.GetStatus().Diagnostics().end(), [](const auto& d) { + return d.code != "dynamic-procedure-context"; + }); + ASSERT_NE(cause, result.GetStatus().Diagnostics().end()); + EXPECT_EQ(fields.at("failure_code"), cause->code) << FailureText(result.GetStatus()); + EXPECT_EQ(text.find("\taccepted=1"), std::string::npos); +} + +TEST(HhtDynamicProcedure, AcceptedFluxHistoryCountsEachIncrementExactlyOnce) { + auto definition = SymmetricExtension(); + definition.dynamic_step->time_period = .2; + definition.dynamic_step->increment_count = 2; + fesa::SerialParallelFor parallel; + auto run = RunRigidTranslation(definition, parallel, "field-history"); + ASSERT_TRUE(run.HasValue()) << (run.HasValue() ? "" : FailureText(run.GetStatus())); + const auto& frames = run.Value().execution.state.AcceptedFrames(); + ASSERT_EQ(frames.size(), 3U); + double sum = 0, maximum_sum = 0; + std::size_t n = 0; + std::istringstream input(run.Value().log); + std::string row; + while (std::getline(input, row)) { + const auto fields = LogFields(row); + if (fields.at("schema") != "hht-log-v2" || fields.at("accepted") != "1") continue; + ++n; + const auto& frame = frames[n]; + std::vector flux; + for (std::size_t node = 0; node < 4; ++node) + for (std::size_t c = 0; c < 3; ++c) + flux.push_back(std::abs(frame.internal_force[6 * node + c] + frame.inertia_force[6 * node + c])); + for (int node = 0; node < 4; ++node) { + flux.push_back(1); flux.push_back(0); flux.push_back(0); + } + const double m = *std::max_element(flux.begin(), flux.end()); + const double H = n == 1 ? 0 : maximum_sum / static_cast(n - 1); + double selected_sum = 0; + std::size_t count = 0; + for (double x : flux) + if (m < .1 * H || x >= 1e-5 * H) { selected_sum += x; ++count; } + ASSERT_GT(count, 0U); + const double spatial = selected_sum / static_cast(count); + sum += spatial; maximum_sum += m; + EXPECT_DOUBLE_EQ(std::stod(fields.at("force_spatial_flux")), spatial); + EXPECT_DOUBLE_EQ(std::stod(fields.at("force_time_average_flux")), sum / static_cast(n)); + EXPECT_EQ(std::stoul(fields.at("iteration")), frame.iteration_count); + } + EXPECT_EQ(n, 2U); +} + +TEST(HhtDynamicProcedure, LoadFluxPreservesSourceCancellationAndExplicitZeroEntries) { + auto definition = MakeDefinition(true); + definition.dynamic_step->loads = { + {"Loaded", 1, 3, {"dynamic-state.inp", 30U}}, + {"Loaded", 1, -3, {"dynamic-state.inp", 31U}}, + {"Loaded", 2, 0, {"dynamic-state.inp", 32U}}}; + auto domain = fesa::Domain::Create(definition); + ASSERT_TRUE(domain.HasValue()); + auto model = fesa::AnalysisModel::Create(domain.Value()); + ASSERT_TRUE(model.HasValue()); + auto dofs = fesa::DofManager::Create(model.Value()); + ASSERT_TRUE(dofs.HasValue()); + std::array, 2> flux; + const auto loads = fesa::hht_internal::AssembleLoads(domain.Value(), dofs.Value(), &flux); + ASSERT_TRUE(loads.HasValue()); + EXPECT_DOUBLE_EQ(loads.Value()[0][0], 0); + EXPECT_DOUBLE_EQ(loads.Value()[1][0], 6); + ASSERT_EQ(flux[0].size(), 9U); + ASSERT_EQ(flux[1].size(), 9U); + const std::array expected{3, 0, 0, -3, 0, 0, 0, 0, 0}; + for (std::size_t i = 0; i < expected.size(); ++i) { + EXPECT_DOUBLE_EQ(flux[0][i].internal, expected[i]); + EXPECT_DOUBLE_EQ(flux[0][i].inertia, 0); + EXPECT_DOUBLE_EQ(flux[1][i].internal, 0); + } +} + +namespace { +// All-fixed fixture has no private coordinates. This adapter exercises the +// real production acceptance/output transaction without a test solver path. +class EmptyCoordinateIntegrator final : public fesa::hht_reduced_internal::ShellHhtIntegrator { + public: + fesa::hht_reduced_internal::ReducedShellPredictor Predict( + const fesa::hht_reduced_internal::ReducedHistory& history, double, + const fesa::HhtParameters&) const override { + EXPECT_EQ(history.p.Size(), 0U); + return {}; + } + void EvaluateRates(const fesa::hht_reduced_internal::ReducedShellRateRequest&, + fesa::Vector& v, fesa::Vector& a) const override { + EXPECT_EQ(v.Size(), 0U); EXPECT_EQ(a.Size(), 0U); + } + fesa::Result ReconstructPhysicalRates( + const fesa::hht_reduced_internal::FixedChart& chart, + const fesa::hht_reduced_internal::EmbeddingRoot& root, + const fesa::Vector& v, const fesa::Vector& a, + fesa::TangentSolver& solver) const override { + return fesa::hht_reduced_internal::ReconstructPhysicalRates(chart, root, v, a, solver); + } +}; +class CloseAtCandidate final : public fesa::EquilibriumAlgorithm { + public: + explicit CloseAtCandidate(fesa::NewtonIterationLogWriter& log) : log_(log) {} + fesa::Status SolveCurrentStep(std::uint32_t first, std::uint32_t last, + const Evaluation& evaluate) override { + EXPECT_EQ(first, 0U); EXPECT_EQ(last, 100U); + auto value = evaluate(first); + if (!value.HasValue()) return value.GetStatus(); + EXPECT_TRUE(value.Value()); + return log_.Close(); + } + private: + fesa::NewtonIterationLogWriter& log_; +}; +} + +TEST(HhtDynamicProcedure, FailedAcceptanceOutputPublishesNoCandidateAndFreshRetryIsUnchanged) { + auto domain = fesa::Domain::Create(MakeDefinition()); + ASSERT_TRUE(domain.HasValue()); + fesa::SerialParallelFor parallel; + fesa::MklPardisoSolver mass; + fesa::MklPardisoTangentSolver tangent; + fesa::NewtonIterationLogWriter log; + const auto path = std::filesystem::temp_directory_path() / "fesa-field-output-fail.log"; + ASSERT_TRUE(log.Open(path).IsOk()); + EmptyCoordinateIntegrator integrator; + CloseAtCandidate algorithm(log); + const auto result = fesa::ExecuteReducedHht(domain.Value(), log, parallel, mass, tangent, integrator, algorithm); + ASSERT_FALSE(result.HasValue()); + EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kOutput); + std::ifstream input(path, std::ios::binary); + const std::string text{std::istreambuf_iterator{input}, {}}; + input.close(); std::filesystem::remove(path); + EXPECT_EQ(text.find("accepted=1"), std::string::npos); + auto first = RunRigidTranslation(MakeDefinition(), parallel, "field-retry"); + auto second = RunRigidTranslation(MakeDefinition(), parallel, "field-retry-repeat"); + ASSERT_TRUE(first.HasValue()); ASSERT_TRUE(second.HasValue()); + ExpectSameDynamicHistory(first.Value(), second.Value()); +} + +namespace { +class FailNextSelectedTrial final : public fesa::TangentSolver { + public: + fesa::Status FactorizeFresh(const fesa::SparseMatrix& matrix) override { + if (outer_solved_) return fesa::Status::Failure(fesa::FailureCategory::kSolver, + {{fesa::Severity::kError, "injected-next-trial", {}, "DYNAMIC", {}, "Fail before the next selected field evaluation."}}); + large_ = matrix.Rows() > 4; + return solver_.FactorizeFresh(matrix); + } + fesa::Status SolveCorrection(const fesa::Vector& rhs, fesa::Vector& solution) const override { + auto status = solver_.SolveCorrection(rhs, solution); + if (status.IsOk() && large_) outer_solved_ = true; + return status; + } + private: + fesa::MklPardisoTangentSolver solver_; + bool large_ = false; + mutable bool outer_solved_ = false; +}; +} + +TEST(HhtDynamicProcedure, FailureBeforeNextEvaluationDoesNotReusePreviousFieldEvidence) { + auto domain = fesa::Domain::Create(MakeFiniteRotationDefinition()); + ASSERT_TRUE(domain.HasValue()); + fesa::SerialParallelFor parallel; + fesa::MklPardisoSolver mass; + FailNextSelectedTrial tangent; + fesa::NewtonIterationLogWriter log; + const auto path = std::filesystem::temp_directory_path() / "fesa-field-next-fail.log"; + ASSERT_TRUE(log.Open(path).IsOk()); + const auto result = fesa::HhtDynamicProcedureExecutor(parallel, mass, tangent).Execute(domain.Value(), log); + ASSERT_TRUE(log.Close().IsOk()); + std::ifstream input(path, std::ios::binary); + const std::string text{std::istreambuf_iterator{input}, {}}; + input.close(); std::filesystem::remove(path); + ASSERT_FALSE(result.HasValue()); + const auto row = text.substr(text.rfind("record=")); + const auto fields = LogFields(row.substr(0, row.find('\n'))); + EXPECT_EQ(fields.at("failure_code"), "injected-next-trial") << FailureText(result.GetStatus()); + EXPECT_EQ(fields.at("iteration"), "1"); + EXPECT_EQ(fields.at("force_residual_numerator"), "not-evaluated"); + EXPECT_EQ(fields.at("moment_residual_scale"), "not-evaluated"); + EXPECT_EQ(fields.at("field_pass"), "not-evaluated"); + EXPECT_EQ(fields.at("accepted"), "0"); +} + +namespace { +class EvaluateEndpoint100 final : public fesa::EquilibriumAlgorithm { + public: + fesa::Status SolveCurrentStep(std::uint32_t first, std::uint32_t last, + const Evaluation& evaluate) override { + EXPECT_EQ(first, 0U); EXPECT_EQ(last, 100U); + auto value = evaluate(last); + if (!value.HasValue()) return value.GetStatus(); + EXPECT_TRUE(value.Value()); + return fesa::Status::Ok(); + } +}; +} + +TEST(HhtDynamicProcedure, FieldAcceptanceEvaluatesProductionEndpoint100BeforeCorrectionBudget) { + auto domain = fesa::Domain::Create(MakeDefinition()); + ASSERT_TRUE(domain.HasValue()); + fesa::SerialParallelFor parallel; + fesa::MklPardisoSolver mass; + fesa::MklPardisoTangentSolver tangent; + fesa::NewtonIterationLogWriter log; + const auto path = std::filesystem::temp_directory_path() / "fesa-field-endpoint100.log"; + ASSERT_TRUE(log.Open(path).IsOk()); + EmptyCoordinateIntegrator integrator; + EvaluateEndpoint100 algorithm; + // Tests the real endpoint callback at the inclusive boundary. No physical + // trajectory is forced through 100 corrections; all fixed coordinates stay zero. + const auto result = fesa::ExecuteReducedHht(domain.Value(), log, parallel, mass, tangent, integrator, algorithm); + ASSERT_TRUE(log.Close().IsOk()); + std::ifstream input(path, std::ios::binary); + const std::string text{std::istreambuf_iterator{input}, {}}; + input.close(); std::filesystem::remove(path); + ASSERT_TRUE(result.HasValue()) << (result.HasValue() ? "" : FailureText(result.GetStatus())); + ASSERT_EQ(result.Value().state.AcceptedFrames().size(), 3U); + EXPECT_EQ(result.Value().state.AcceptedFrames()[1].iteration_count, 100U); + EXPECT_EQ(result.Value().state.AcceptedFrames()[2].iteration_count, 100U); + const auto row = text.substr(text.rfind("record=")); + const auto fields = LogFields(row.substr(0, row.find('\n'))); + EXPECT_EQ(fields.at("iteration"), "100"); + EXPECT_EQ(fields.at("field_pass"), "1"); + EXPECT_EQ(fields.at("accepted"), "1"); +} diff --git a/tests/unit/analysis/newton_iteration_log_test.cpp b/tests/unit/analysis/newton_iteration_log_test.cpp index afebbd9..bd97b6c 100644 --- a/tests/unit/analysis/newton_iteration_log_test.cpp +++ b/tests/unit/analysis/newton_iteration_log_test.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -562,4 +563,149 @@ TEST(DynamicNewtonIterationLog, ExpectLogOutputFailure(unopened.Write(CompleteDynamicRecord())); } +TEST(DynamicFieldIterationLog, KeepsOrderedUnitsMissingEvidenceAndEscapedSourceName) { + TempDirectory directory{"field-v2"}; + const auto path = directory.Path() / "log.txt"; + auto record = CompleteDynamicRecord(); + record.policy_id = "abaqus2025-field-on-fesa-v1"; + record.step_name = "\xED\x95\x9C\xEA\xB8\x80=Step\\tail\tline\nend\r"; + record.stage = "evaluation"; + record.outcome = "candidate"; + record.fields[0].residual_numerator = 1.2345678901234567; + record.fields[0].increment_norm = 0; + fesa::NewtonIterationLogWriter writer; + ASSERT_TRUE(writer.Open(path).IsOk()); + ASSERT_TRUE(writer.Write(record).IsOk()); + ASSERT_TRUE(writer.Close().IsOk()); + const auto text = ReadLogBytes(path); + EXPECT_EQ(std::count(text.begin(), text.end(), '\n'), 1); + EXPECT_EQ(text.find('\r'), std::string::npos); + const auto fields = SplitTabs(text.substr(0, text.size() - 1)); + const std::vector base{"record", "schema", "policy_id", "step", "increment", "time", "iteration", "stage", "outcome", "failure_code", "field_pass", "accepted"}; + for (std::size_t i = 0; i < base.size(); ++i) + EXPECT_EQ(fields[i].substr(0, fields[i].find('=')), base[i]); + const std::vector suffixes{ + "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"}; + for (std::size_t c = 0; c < 2; ++c) + for (std::size_t i = 0; i < suffixes.size(); ++i) + EXPECT_EQ(fields[base.size() + c * suffixes.size() + i].substr( + 0, fields[base.size() + c * suffixes.size() + i].find('=')), + (c == 0 ? "force_" : "moment_") + suffixes[i]); + EXPECT_EQ(FieldValue(fields, "step"), "step=\xED\x95\x9C\xEA\xB8\x80=Step\\\\tail\\tline\\nend\\r"); + EXPECT_EQ(FieldValue(fields, "force_correction_numerator"), "force_correction_numerator=not-evaluated"); + EXPECT_EQ(FieldValue(fields, "force_increment_norm"), "force_increment_norm=0"); + EXPECT_EQ(FieldValue(fields, "strict_residual_pass"), "strict_residual_pass=not-evaluated"); + EXPECT_EQ(FieldValue(fields, "moment_correction_unit"), "moment_correction_unit=rad"); + EXPECT_DOUBLE_EQ(std::stod(ValueAfterEquals(FieldValue(fields, "force_residual_numerator"))), + *record.fields[0].residual_numerator); +} + +TEST(DynamicFieldIterationLog, InvalidFieldEvidenceFailsWithoutWritingPartialRecord) { + TempDirectory directory{"field-v2-invalid"}; + const auto path = directory.Path() / "log.txt"; + for (int kind = 0; kind < 4; ++kind) { + SCOPED_TRACE(kind); + fesa::NewtonIterationLogWriter writer; + ASSERT_TRUE(writer.Open(path).IsOk()); + auto record = CompleteDynamicRecord(); + record.policy_id = "abaqus2025-field-on-fesa-v1"; + ASSERT_TRUE(writer.Write(record).IsOk()); + ASSERT_TRUE(writer.Flush().IsOk()); + const auto prefix = ReadLogBytes(path); + if (kind == 0) record.fields[0].branch = "unknown"; + if (kind == 1) record.fields[0].estimated_correction = std::numeric_limits::infinity(); + if (kind == 2) record.fields[1].residual_scale = -1; + if (kind == 3) record.policy_id = "unknown"; + ExpectLogOutputFailure(writer.Write(record)); + ExpectLogOutputFailure(writer.Flush()); + EXPECT_EQ(ReadLogBytes(path), prefix); + } +} + +// Independent strict consumer: production has a writer, no public log reader. +bool ReadFieldV2(const std::string& bytes) { + if (bytes.empty() || bytes.back() != '\n' || bytes.find('\r') != std::string::npos || + std::count(bytes.begin(), bytes.end(), '\n') != 1) return false; + std::map values; + for (const auto& field : SplitTabs(bytes.substr(0, bytes.size() - 1))) { + const auto equal = field.find('='); + if (equal == std::string::npos || !values.emplace(field.substr(0, equal), field.substr(equal + 1)).second) + return false; + } + for (const auto* key : {"record", "schema", "policy_id", "step", "increment", "time", "iteration", "stage", "outcome", "failure_code", "field_pass", "accepted"}) + if (!values.count(key)) return false; + if (values["record"] != "dynamic-newton-iteration" || values["schema"] != "hht-log-v2" || + values["policy_id"] != "abaqus2025-field-on-fesa-v1") return false; + const auto numeric = [](const std::string& text, bool missing) { + if (missing && text == "not-evaluated") return true; + double value = 0; + const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value); + return parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size() && std::isfinite(value) && value >= 0; + }; + const auto boolean = [](const std::string& text, bool missing) { + return text == "0" || text == "1" || (missing && text == "not-evaluated"); + }; + for (const auto* key : {"increment", "iteration"}) { + std::uint64_t value = 0; + const auto& text = values[key]; + const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value); + if (parsed.ec != std::errc{} || parsed.ptr != text.data() + text.size()) return false; + } + if (!numeric(values["time"], false) || !boolean(values["field_pass"], true) || !boolean(values["accepted"], false)) return false; + for (const auto* prefix : {"force_", "moment_"}) { + for (const auto* suffix : {"residual_numerator", "residual_scale", "spatial_flux", "time_average_flux", "correction_numerator", "increment_norm", "estimated_correction", "residual_threshold", "correction_threshold"}) { + const auto found = values.find(std::string(prefix) + suffix); + if (found == values.end() || !numeric(found->second, true)) return false; + } + for (const auto* suffix : {"residual_pass", "correction_pass", "field_pass"}) { + const auto found = values.find(std::string(prefix) + suffix); + if (found == values.end() || !boolean(found->second, true)) return false; + } + const auto branch = values.find(std::string(prefix) + "branch"); + if (branch == values.end() || (branch->second != "normal" && branch->second != "zero-flux" && + branch->second != "linear" && branch->second != "nonquadratic" && branch->second != "not-evaluated")) return false; + if (values[std::string(prefix) + "residual_unit"] != (std::string(prefix) == "force_" ? "force" : "moment") || + values[std::string(prefix) + "correction_unit"] != (std::string(prefix) == "force_" ? "length" : "rad")) return false; + } + for (const auto& entry : values) + for (std::size_t i = 0; i < entry.second.size(); ++i) + if (entry.second[i] == '\\') { + if (++i == entry.second.size() || (entry.second[i] != '\\' && entry.second[i] != 't' && entry.second[i] != 'n' && entry.second[i] != 'r')) return false; + } + return true; +} + +TEST(DynamicFieldIterationLog, IndependentReaderRejectsMalformedVersionedRecords) { + TempDirectory directory{"field-reader"}; + const auto path = directory.Path() / "log.txt"; + fesa::NewtonIterationLogWriter writer; + ASSERT_TRUE(writer.Open(path).IsOk()); + auto record = CompleteDynamicRecord(); + record.policy_id = "abaqus2025-field-on-fesa-v1"; + ASSERT_TRUE(writer.Write(record).IsOk()); + ASSERT_TRUE(writer.Close().IsOk()); + const auto valid = ReadLogBytes(path); + ASSERT_TRUE(ReadFieldV2(valid)); + const auto replaced = [&](const std::string& from, const std::string& to) { + auto text = valid; + const auto start = text.find(from); + EXPECT_NE(start, std::string::npos); + text.replace(start, from.size(), to); + return text; + }; + EXPECT_FALSE(ReadFieldV2(replaced("schema=hht-log-v2", "schema=hht-log-v1"))); + EXPECT_FALSE(ReadFieldV2(replaced("\tincrement=2", "\tincrement=-1"))); + EXPECT_FALSE(ReadFieldV2(replaced("\ttime=0.25", "\ttime=nan"))); + EXPECT_FALSE(ReadFieldV2(replaced("\ttime=0.25", "\ttime=inf"))); + EXPECT_FALSE(ReadFieldV2(replaced("force_residual_numerator=not-evaluated", "force_residual_numerator=1junk"))); + EXPECT_FALSE(ReadFieldV2(replaced("force_branch=not-evaluated", "force_branch=unknown"))); + EXPECT_FALSE(ReadFieldV2(replaced("\tfield_pass=not-evaluated", "\tfield_pass=true"))); + EXPECT_FALSE(ReadFieldV2(replaced("\tstep=", "\tstep=bad\\x"))); + EXPECT_FALSE(ReadFieldV2(replaced("\taccepted=0", "\taccepted=0\taccepted=0"))); + EXPECT_FALSE(ReadFieldV2(replaced("\tforce_increment_norm=not-evaluated", ""))); +} + } // namespace From 0c88cb24c84abc4b3561a1e7975df7a4125134f1 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Mon, 19 Oct 2026 03:03:40 +0900 Subject: [PATCH 07/11] test: verify candidate HHT physical endpoint semantics --- docs/abaqus-tolerance/physics-validation.md | 160 ++++++++++++++++++ .../analysis/hht_dynamic_procedure_test.cpp | 80 +++++++++ 2 files changed, 240 insertions(+) create mode 100644 docs/abaqus-tolerance/physics-validation.md diff --git a/docs/abaqus-tolerance/physics-validation.md b/docs/abaqus-tolerance/physics-validation.md new file mode 100644 index 0000000..5b185fa --- /dev/null +++ b/docs/abaqus-tolerance/physics-validation.md @@ -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의 별도 실행 결과를 따른다. diff --git a/tests/unit/analysis/hht_dynamic_procedure_test.cpp b/tests/unit/analysis/hht_dynamic_procedure_test.cpp index 1e97734..75594e9 100644 --- a/tests/unit/analysis/hht_dynamic_procedure_test.cpp +++ b/tests/unit/analysis/hht_dynamic_procedure_test.cpp @@ -750,6 +750,86 @@ TEST(HhtDynamicProcedure, AcceptedFluxHistoryCountsEachIncrementExactlyOnce) { EXPECT_EQ(n, 2U); } +TEST(HhtDynamicProcedure, CandidateRigidTranslationMatchesIndependentPhysicalOracle) { + // Two disconnected unit squares: rho=2, thickness=.1, area=1 each. + // Each nodal lump has mass .05 and receives F_x=1, hence a_x=20. + // This exact rigid motion verifies endpoint semantics, not temporal order. + constexpr double kNumericalTolerance = 1e-3; + constexpr double kAcceleration = 20.0; + constexpr double kTotalMass = 0.4; + constexpr double kTotalForce = 8.0; + constexpr double kLength = 1.0; + constexpr double kTime = 0.5; + constexpr double kEnergy = 20.0; + fesa::SerialParallelFor parallel; + for (const bool constrain_transverse : {false, true}) { + SCOPED_TRACE(constrain_transverse); + auto definition = MakeRigidTranslationDefinition(); + if (constrain_transverse) { + definition.dynamic_step->boundaries = { + {"All-A", 2, 6, 0.0, {"dynamic-state.inp", 30U}}, + {"All-B", 2, 6, 0.0, {"dynamic-state.inp", 31U}}}; + } + auto run = RunRigidTranslation(definition, parallel, + constrain_transverse ? "analytic-bc" : "analytic-free"); + ASSERT_TRUE(run.HasValue()) << (run.HasValue() ? "" : FailureText(run.GetStatus())); + const auto& frames = run.Value().execution.state.AcceptedFrames(); + ASSERT_EQ(frames.size(), 3U); + for (std::size_t index = 0U; index < frames.size(); ++index) { + const auto& frame = frames[index]; + EXPECT_DOUBLE_EQ(frame.time, static_cast(index) * 0.25); + const double q = 0.5 * kAcceleration * frame.time * frame.time; + const double v = kAcceleration * frame.time; + for (std::size_t node = 0U; node < 8U; ++node) { + const auto dof = 6U * node; + EXPECT_LE(std::abs(frame.generalized_coordinates[dof] - q) / kLength, kNumericalTolerance); + EXPECT_LE(std::abs(frame.velocity[dof] - v) * kTime / kLength, kNumericalTolerance); + EXPECT_LE(std::abs(frame.acceleration[dof] - kAcceleration) * + kTime * kTime / kLength, kNumericalTolerance); + EXPECT_LE(std::abs(frame.algorithmic_acceleration[dof] - kAcceleration) * + kTime * kTime / kLength, kNumericalTolerance); + EXPECT_DOUBLE_EQ(frame.external_force[dof], 1.0); + EXPECT_LE(std::abs(frame.inertia_force[dof] - 1.0), kNumericalTolerance); + for (std::size_t component = 1U; component < 6U; ++component) { + const double scale = component < 3U ? kLength : 1.0; + EXPECT_LE(std::abs(frame.generalized_coordinates[dof + component]) / scale, kNumericalTolerance); + EXPECT_LE(std::abs(frame.velocity[dof + component]) * kTime / scale, kNumericalTolerance); + EXPECT_LE(std::abs(frame.acceleration[dof + component]) * kTime * kTime / scale, kNumericalTolerance); + EXPECT_LE(std::abs(frame.algorithmic_acceleration[dof + component]) * kTime * kTime / + scale, kNumericalTolerance); + if (constrain_transverse) { + EXPECT_DOUBLE_EQ(frame.generalized_coordinates[dof + component], 0.0); + EXPECT_DOUBLE_EQ(frame.velocity[dof + component], 0.0); + EXPECT_DOUBLE_EQ(frame.acceleration[dof + component], 0.0); + EXPECT_DOUBLE_EQ(frame.algorithmic_acceleration[dof + component], 0.0); + } + } + } + EXPECT_LE(std::abs(frame.kinetic_energy - 0.5 * kTotalMass * v * v) / + kEnergy, kNumericalTolerance); + EXPECT_LE(std::abs(frame.external_work - kTotalForce * q) / kEnergy, kNumericalTolerance); + EXPECT_LE(std::abs(frame.physical_strain_energy) / kEnergy, kNumericalTolerance); + EXPECT_LE(std::abs(frame.drilling_energy) / kEnergy, kNumericalTolerance); + EXPECT_LE(std::abs(frame.energy_defect) / kEnergy, kNumericalTolerance); + for (const auto value : frame.global_equilibrium) + EXPECT_LE(std::abs(value), 1e-8); + } + std::size_t accepted = 0U; + std::istringstream input(run.Value().log); + std::string row; + while (std::getline(input, row)) { + const auto fields = LogFields(row); + if (fields.at("schema") != "hht-log-v2") continue; + EXPECT_EQ(fields.at("policy_id"), "abaqus2025-field-on-fesa-v1"); + if (fields.at("accepted") == "1") { + EXPECT_EQ(fields.at("field_pass"), "1"); + ++accepted; + } + } + EXPECT_EQ(accepted, 2U); + } +} + TEST(HhtDynamicProcedure, LoadFluxPreservesSourceCancellationAndExplicitZeroEntries) { auto definition = MakeDefinition(true); definition.dynamic_step->loads = { From 710ab39059ab73518e63968067683791a5c8b558 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Mon, 19 Oct 2026 03:16:29 +0900 Subject: [PATCH 08/11] docs: preserve T6 candidate artifacts and measurement limits --- docs/abaqus-tolerance/results.md | 105 ++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/docs/abaqus-tolerance/results.md b/docs/abaqus-tolerance/results.md index bfe029a..8f0329c 100644 --- a/docs/abaqus-tolerance/results.md +++ b/docs/abaqus-tolerance/results.md @@ -1,7 +1,8 @@ -# 외부 수렴 정책 실험 결과 — T2 기준 보존 +# 외부 수렴 정책 실험 결과 — T2 기준과 T6 후보 보존 기록일: 2026-10-19. **정상 strict 기준은 확보했으나 전체 실험·물리 검증은 미완료**다. -후보 정책은 아직 구현·측정하지 않았다. 가속률이나 채택 가능성을 주장하지 않는다. +T6 후보는 별도 1증분 개발 진단과 계측 OFF Release 실행물 보존까지 수행했다. +공식 100증분 후보 실행·reference 비교·성능 비교는 미성립이며 가속률이나 채택 가능성을 주장하지 않는다. ## 기준과 원시 증거 @@ -156,3 +157,103 @@ 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개는 달라 **정확 byte parity는 미통과**다. 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 검증은 후속 범위이며 이 한정 진단이 대체하지 않는다. From 818046c3975684d37ddd6bcb827099bd27652acf Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Mon, 19 Oct 2026 07:58:56 +0900 Subject: [PATCH 09/11] test: record complete tolerance experiment verification evidence T7 executed Native Debug 883/72/43 tests and full CMake Debug CTest998; both retain 10 failures and 1 resolution skip. Python145 passed. All five original Hex8 Release accuracy CLI/comparator pairs returned0. Source574 and formal reference34 inventories/SHA remain unchanged. No tracked file diff; operational evidence is retained under .build/tolerance-experiment/go-20261018/T7/verification.json. This evidence commit does not claim numerical suitability, complete R8, official candidate speedup, or missing-reference validation. From 626f7da880001fcae2e3280e9d43a2772ce79ae6 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Mon, 19 Oct 2026 08:03:14 +0900 Subject: [PATCH 10/11] docs: record field-policy validation failures and adoption limits --- docs/README.md | 2 +- docs/abaqus-tolerance/README.md | 28 +++-- docs/abaqus-tolerance/application-plan.md | 16 +-- docs/abaqus-tolerance/results.md | 120 +++++++++++++++++++++- docs/hht-implicit-dynamics/README.md | 34 ++++-- docs/tracking/findings.md | 37 +++++-- docs/tracking/status.md | 44 ++++---- 7 files changed, 221 insertions(+), 60 deletions(-) diff --git a/docs/README.md b/docs/README.md index ef925cc..2d84352 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,5 +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) +- [Abaqus 수렴 허용치 조사와 FESA 적용 실험](abaqus-tolerance/README.md) - [C++ 모듈화·Refactor 병합 현황과 이관 근거](cpp-object-oriented-modular-refactoring/README.md) diff --git a/docs/abaqus-tolerance/README.md b/docs/abaqus-tolerance/README.md index 017ee6c..456325e 100644 --- a/docs/abaqus-tolerance/README.md +++ b/docs/abaqus-tolerance/README.md @@ -1,18 +1,20 @@ # Abaqus 수렴 허용치 조사와 FESA 대응 조사일: 2026-10-18. FESA 조사 기준: `43867d0`. -상태: 문헌·코드 조사와 승인된 후보 정책의 수식 계약이다. 구현·성능 실험 통과 보고가 아니다. +2026-10-19 실험 branch에서 공개 field 규칙을 MITC4 HHT의 실제 종료에 연결했다. +Native/CTest 전체는 각각987 PASS·10 FAIL·1 SKIP이며, 공식 후보 성능 비교는 미성립이다. +구현·물리 검증·측정의 상세 근거와 한계는 [실험 결과](results.md)에 기록했다. 승인된 후보의 시험 가능한 수식·분기 우선순위·평균 이력·로그 계약은 [외부 field 수렴 정식화](formulation-convergence.md)를 따른다. 아래는 조사 당시 코드와 -공개 기본값의 비교이며, 후보 구현 완료를 뜻하지 않는다. 정식화는 2025 공개 STRICT +공개 기본값의 비교다. 현재 후보는 이 정식화를 실제 사용한다. 정식화는 2025 공개 STRICT 규칙과 비공개 부분의 명시적 FESA 대응을 구분하며 실제 2024 설정과의 동일성을 주장하지 않는다. ## 조사 결론 Abaqus/Standard의 일반 비선형 평형 기준은 잔차 비율 **0.005**, 해 보정 비율 -**0.01**이다. 병진과 회전을 각각 검사한다. FESA의 현재 MITC4 HHT는 힘·모멘트 -잔차와 q/v/a/A 보정량에 **1e-10**을 사용한다. 완화 실험을 검토할 근거는 있으나, +**0.01**이다. 병진과 회전을 각각 검사한다. 조사 당시 `43867d0`의 MITC4 HHT는 힘·모멘트 +잔차와 q/v/a/A 보정량에 **1e-10**을 사용했다. 완화 실험을 검토할 근거는 있으나, 양쪽의 정규화와 검사 대상이 달라 숫자의 비로 정확도나 실행시간을 예측할 수 없다. 일반 기준값의 근거는 [Abaqus 공식 해석 지침의 제어값 설명][S1]이다. @@ -146,9 +148,9 @@ Abaqus에는 수렴 실패 상태도 받아들이는 `DIRECT=NO STOP`이 있지 Explicit 안정시간 기준을 모두 열거한 문서는 아니다. 공개 문헌에서 확인하지 않은 Abaqus 내부 직접분해·pivot·rank 판정 숫자는 추측하지 않았다. -## 6. 현재 FESA 기준과 소유 위치 +## 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) | @@ -162,20 +164,26 @@ Abaqus 내부 직접분해·pivot·rank 판정 숫자는 추측하지 않았다. | 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̃와 동일하지 않다. 현재 보정 기준도 +조사 당시 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· 요소 도함수·조립 경로가 존재한다는 점이다. 허용치가 실제 장시간 실행의 주원인인지는 -현재 측정하지 않았다. [기존 성능 기록](../calculation-performance/README.md)의 +조사 당시에는 측정하지 않았다. 이후 같은 local 근사본의 strict 공식 실행은 +6350.7895521초/native0을 확보했지만, 물리 실패가 남아 공식 후보100증분은 미실행이다. +첫 증분의 호출 수 감소만으로 전체 병목이나 가속을 확정하지 않는다. +[기존 성능 기록](../calculation-performance/README.md)의 6372.325621초는 과거 source의 단일 baseline으로 최신 revision의 측정값이 아니다. -다음 실험에서 외부/내부 반복 수, 요소 평가 수, factor/solve 수, 단계별 시간과 +추가 실험에서 전체 증분의 외부/내부 반복 수, 요소 평가 수, factor/solve 수, 단계별 시간과 종료를 지연시킨 채널을 함께 측정해야 한다. 내부 구속 solve가 대부분의 시간을 쓰면 외부 Newton 기준만 바꿔도 효과가 작을 수 있다. 기준을 넓혀 성공한 것과 해의 정확도가 유지된 것을 구분하고, U 비교와 별도로 V/physical a·구속·시간수렴을 확인해야 한다. diff --git a/docs/abaqus-tolerance/application-plan.md b/docs/abaqus-tolerance/application-plan.md index da63cd4..e6889a7 100644 --- a/docs/abaqus-tolerance/application-plan.md +++ b/docs/abaqus-tolerance/application-plan.md @@ -1,9 +1,11 @@ # MITC4 HHT에 Abaqus 수렴 규칙을 적용하는 실행 계획 작성일: 2026-10-18. 조사 기준 source: 43867d0. -상태: 실제 구현·측정·검증을 위한 개정안. 이번 ready에서는 계획 문서만 수정한다. -수정안 승인 후 go는 **실제 코드 수정·빌드·FESA 실행·성능 측정·정확도 검증**을 수행한다. -이 계획의 Abaqus 외부 Newton 규칙 구현·측정은 아직 실행하지 않았다. +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 @@ -23,7 +25,7 @@ MITC4 implicit dynamics의 외부 Newton 종료에 **Abaqus/Standard 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다. @@ -110,8 +112,10 @@ Probe를 추가하더라도 공식 실행에서는 꺼져야 하며 수학 계 ## 정확도·전체 회귀·실패의 처리 -q/v/physical a의 마지막 두 refinement 감소·차수≥1.8, finest normalized error≤1e-4, -기존 N7 해상도 판정을 유지한다. C08b 미판정이나 일반 비선형 HHT의 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, diff --git a/docs/abaqus-tolerance/results.md b/docs/abaqus-tolerance/results.md index 8f0329c..b2fc683 100644 --- a/docs/abaqus-tolerance/results.md +++ b/docs/abaqus-tolerance/results.md @@ -1,8 +1,10 @@ -# 외부 수렴 정책 실험 결과 — T2 기준과 T6 후보 보존 +# MITC4 HHT 외부 field 수렴 정책 실험 결과 -기록일: 2026-10-19. **정상 strict 기준은 확보했으나 전체 실험·물리 검증은 미완료**다. -T6 후보는 별도 1증분 개발 진단과 계측 OFF Release 실행물 보존까지 수행했다. -공식 100증분 후보 실행·reference 비교·성능 비교는 미성립이며 가속률이나 채택 가능성을 주장하지 않는다. +기록일: 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모델 비교는 통과했다. 공식 MITC4 후보 100증분 실행· +reference 비교·성능 비교는 미성립이며 전체 검증 완료·가속 확인·정식 채택으로 보고하지 않는다. ## 기준과 원시 증거 @@ -223,7 +225,8 @@ inertia=internal−failure, solve=RHS와 exclusive ns 합=procedure inclusive를 같은 짧은 입력의 strict ON과 후보 ON HDF5 frame0/1의 40개 numeric dataset을 shape/type/finite 및 inventory 확인 후 비교했다. 22개는 정확 byte 일치하고 -frame1의18개는 달라 **정확 byte parity는 미통과**다. nodal q/v/physical a/A의 +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와 @@ -257,3 +260,110 @@ Field21개와 T4 후보 종료/물리 후속 gate 및 T5 강체 oracle identity 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//`에 있다. +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`는 사용하지 않았다. 후보 MITC4 100증분 결과는 +앞서 설명한 수치 실패 분기로 미실행이며 strict baseline이나 1증분 결과로 대신하지 않는다. + +## 판정과 다음 검증 조건 + +**구현됨·수치 부적합·일부 검증 미완료·공식 성능 비교 미성립**으로 판정한다. +Strict 공식 6350.7895521초, 1증분 개발 호출 수 감소, 전체 회귀 실행 사실은 서로 다른 근거다. +이를 합쳐 전체 가속이나 정식 채택 가능으로 보고하지 않는다. +다음에는 local 근사의 독립 미분/물리 정확도와 후보 후속 acceptance 실패의 원인을 분리하고, +실제 비선형 후보의 시간 정확도와 N7 판정을 확보해야 한다. 그 뒤 같은 조건의 공식 후보 +100증분과 정식 HHT 비교를 수행해야 전체 speedup을 판정할 수 있다. +현재 branch와 원시 자료를 보존했으며 주 개발 branch 병합·외부 solver·release는 수행하지 않았다. diff --git a/docs/hht-implicit-dynamics/README.md b/docs/hht-implicit-dynamics/README.md index 66200b9..e6162c1 100644 --- a/docs/hht-implicit-dynamics/README.md +++ b/docs/hht-implicit-dynamics/README.md @@ -5,8 +5,11 @@ 2026-10-18 후속 사용자 지시로 polar 반복 12→1회, 관성 회전/Jacobian 급수 32→3차, 질량 대칭·null/Gram·분해 복원 검사 1e-12→1e-3의 local 근사 실험을 적용했다. 최초 엄격한 기준의 관련 요소 46개 중 5개가 실패했다. 후속 사용자 지시로 관성·접선· -HHT 물리 테스트의 오차 기준을 1e-3으로 완화했으며 최신 선택 50개는 두 빌드 경로에서 -46 PASS·4 FAIL이다. 실제 해석 루틴의 수렴 조건은 이번 테스트 수정으로 바뀌지 않았다. +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)을 따른다. @@ -21,8 +24,11 @@ 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 비교는 통과했다. 현재 field 후보는 수치 실패로 +공식100증분을 실행하지 않았으며 새 원본 비교·비선형 시간수렴·전체 물리 판정과 speedup은 +미성립이다. 전체 회귀는 위 수치대로 실제 실행했으며 전체 green이 아니다. +[수렴 정책 실험 결과](../abaqus-tolerance/results.md)에 현재 근거를 보존했다. ## 채택 방법과 산술 의미 @@ -108,18 +114,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에서 변위 일치와 시간 정확도를 구분한다. @@ -128,7 +143,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 후보의 원본 HDF5 비교·전체 물리 closure가 남는다. +전체 회귀의 실제 실패와 해상도 SKIP을 과거 통과로 덮지 않는다. ## 보존한 과거 수식과 연구의 위치 diff --git a/docs/tracking/findings.md b/docs/tracking/findings.md index 4f942c4..c4367aa 100644 --- a/docs/tracking/findings.md +++ b/docs/tracking/findings.md @@ -6,8 +6,9 @@ 1e-3을 적용했다. 변경 전 관련 기존 45개 PASS, 변경 후 46개 중 41 PASS·5 FAIL이다. 추가 HHT 선택에서도 C08a 독립 물리 oracle와 큰 시간간격 100회 보정 종료가 실패했다. 후속 사용자 지시로 관련 테스트 오차 기준도 1e-3으로 변경했으며 최신 동일 선택 - 50개는 Native/CTest 각각46 PASS·4 FAIL이다. D3 오차 약0.228, drilling gradient - 약0.00329 및 C08a hidden defect/실제 평형 검사 실패가 남는다. + 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보다 @@ -15,14 +16,32 @@ - 다음 접근: 원인 분리 실험과 정확도 손실의 별도 판정이 필요하다. 변경 범위·실제 로그· 미검증 항목은 [local 실험 기록](../abaqus-tolerance/local-kernel-experiment.md)을 따른다. -## 비정적 검증 공백 +## MITC4 후보 후속 acceptance와 비선형 시간 정확도 미완료 -- 조건·관측: 2026-10-18 통합에서 장시간 HHT 검증이 사용자 요청으로 중단되고 정적·공통 - 631개만 검증됐다. 비정적 333개 등록은 보존됐으나 최신 통합본 전체 수치 실행 근거가 없다. -- 영향: Shell HHT·solid dynamics·frequency·transient thermal의 전체 검증 완료를 선언할 수 없다. -- 지금 해결하지 못한 이유: 장시간 solver·test 실행과 독립 물리 판정이 필요한 별도 검증 작업이다. -- 다음 접근: source와 환경을 고정하고 미실행 identity별 필요한 물리·시간수렴·reference를 - 묶어 검증한다. 중단된 C08b finer-grid와 기존 완료 개별 검사를 구분한다. +- 조건·관측: 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이다. 공식 후보100증분·정식 HHT 비교·전체 speedup은 미성립이다. +- 지금 해결하지 못한 이유: 승인 local 근사본의 독립 정확도 실패가 동반되며 원인 분리가 + 필요하다. 정해진 근사 수식을 자동 복원하거나 유지할 hidden·triad·평형 기준을 완화해 + 성공시키는 것은 이번 실험의 변경 범위가 아니다. 추가 강제 보정이나 실패 frame commit도 하지 않는다. +- 다음 접근: polar/inertia 근사 영향과 candidate 후속 gate 실패의 원인을 분리한다. + 유지할 물리 기준에서 실제 비선형 후보의 시간·불변량 검증을 확보한 뒤 공식100증분을 측정한다. + 전체 실행·실패 identity와 strict 대조 근거는 수렴 정책 결과의 원시 기록에 보존했다. + +## 개발 계측의 정확 parity와 전체 비용 미확정 + +- 조건·관측: strict 공식 frame0/1과 짧은 개발 실행의 numeric dataset40개 중18개가 byte + 불일치했다. 보존한 동일 OFF 실행물의 짧은 대조도 불일치해 계측만의 영향으로 분리되지 않았다. + 전체100증분 개발 계측은 하지 않았으며, 후보와 strict의1증분 state 차이는 별도 정책 진단이다. +- 영향: 개발 probe의 정확 byte parity 통과나 전체 병목 비중·전체 가속률을 확정할 수 없다. +- 지금 해결하지 못한 이유: T 단축·반복·thread·컴파일 영향을 분리한 실행 근거가 없고, + 수치 부적합 상태에서 같은 장시간 실행을 근거 없이 반복하지 않는 실험 조건을 유지한다. +- 다음 접근: 수치 적합성을 확보한 뒤 동일 입력·실행물·thread 조건으로 원인을 분리하고 + 전체 증분 비용을 측정한다. 새 parity 허용치나 첫 증분의100배 외삽으로 대신하지 않는다. ## 정식 기준자료 부재와 추가 fine 비교기 부재 diff --git a/docs/tracking/status.md b/docs/tracking/status.md index a425397..d766be9 100644 --- a/docs/tracking/status.md +++ b/docs/tracking/status.md @@ -1,15 +1,18 @@ # 구현과 검증 상태 -2026-10-18 후속 [MITC4 local 근사 실험](../abaqus-tolerance/local-kernel-experiment.md)은 -polar 1회·관성 회전/Jacobian 3차·질량 검사 1e-3을 적용했다. 두 Debug build는 성공했으나 -최초 관련 요소 46개 중 5 FAIL이었다. 후속 사용자 지시로 관련 테스트의 오차 기준을 -1e-3으로 변경했고, 최신 Native/CTest 동일 선택 50개는 각각46 PASS·4 FAIL이다. -전체 965개 중 나머지 915개는 이번에 미실행이며 실제 해석 수렴 조건은 변경하지 않았다. -아래 과거 검증을 이 변경본에 승계하지 않는다. Abaqus 외부 Newton 정책은 미구현이다. +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초는 확보했으나 공식 후보100증분·speedup은 미성립이다. +상세 revision·실행 증거는 [수렴 정책 실험 결과](../abaqus-tolerance/results.md)에 있다. -## 기준 시점 +## 과거 통합 기준 시점 -2026-10-18 저장소 HEAD `945d899` 기준이다. 아래 통과 수치는 각 검증 당시 기록의 범위이며 +과거 통합 기준은 2026-10-18 HEAD `945d899`다. 과거 통과 수치는 각 검증 당시 기록의 범위이며 후속 source에서 새로 실행한 결과로 승계하지 않는다. 현재 용도는 개인 연구용 로컬 실행이며 장기 목표는 외부 사용자 배포다. 배포 형태·지원 환경· dependency 재배포 조건은 확정되지 않았다. @@ -18,14 +21,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 원본 비교만 확보, 공식 후보 원본 비교는 미실행. | +| 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· @@ -46,9 +49,9 @@ 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을 선택된 정적 근거와 구분한다. - 자료가 필요한 외부 비교는 정식 자료 확보 뒤 수행한다. +2. 구성된 전체 실행 결과를 바탕으로 MITC4 local 미분/물리 실패와 실제 후보 후속 gate 실패를 + 분리한다. 후보 비선형 q/v/physical a 시간수렴·N7 및 후보 원본 비교를 확보한다. + 전체 검사 실행 사실과 미해결 수치 검증을 구분하고, 자료가 필요한 비교는 확보 뒤 수행한다. 3. Eigen partial-BC 정적 평형 충돌과 thermal 외부 불일치의 영향을 각각 판정한다. 기준 완화·질량 변경·원본 보정으로 해결하지 않는다. 4. 같은 물리·허용치·출력·정밀도를 보존한 상태에서 성능 회귀 원인을 조사하고 최종 source의 @@ -67,4 +70,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은 수치 실패로 미성립이다. From 86b740eebf3e2edd3520a354d1bd063de663c658 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Mon, 19 Oct 2026 17:11:49 +0900 Subject: [PATCH 11/11] =?UTF-8?q?docs(tolerance):=20=EB=B3=80=EC=9C=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=88=98=EC=9A=A9=EA=B3=BC=20=EB=B3=B4?= =?UTF-8?q?=EB=A5=98=20=EA=B2=80=EC=82=AC=20=EB=B0=8F=20=EC=B5=9C=EC=A2=85?= =?UTF-8?q?=20=EB=B9=84=EA=B5=90=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 후속 결정에 따라 이번 실험을 변위 검증 통과와 실패10개 보류로 수용한다. 기존 전체 검사 FAIL/SKIP과 수치 기준을 보존한다. 전체101프레임 q/v/a/A/energy 차이와 관측 시간·outer 횟수를 추가한다. 공식 성능 조건·전체 비용·시간 정확도 미확보는 후속 과제로 남긴다. 문서만 변경: source450/reference34 hash 불변, 로컬 링크87개와 정적 검사 통과. --- docs/abaqus-tolerance/README.md | 11 ++- docs/abaqus-tolerance/application-plan.md | 5 + .../reference-displacement-comparison.md | 90 +++++++++++++++++ .../abaqus-tolerance/relative-error-review.md | 99 +++++++++++++++++++ docs/abaqus-tolerance/results.md | 75 ++++++++++++-- docs/hht-implicit-dynamics/README.md | 15 ++- .../0011-tolerance-experiment-acceptance.md | 39 ++++++++ docs/tracking/decisions/index.md | 1 + docs/tracking/findings.md | 14 ++- docs/tracking/status.md | 12 ++- 10 files changed, 345 insertions(+), 16 deletions(-) create mode 100644 docs/abaqus-tolerance/reference-displacement-comparison.md create mode 100644 docs/abaqus-tolerance/relative-error-review.md create mode 100644 docs/tracking/decisions/0011-tolerance-experiment-acceptance.md diff --git a/docs/abaqus-tolerance/README.md b/docs/abaqus-tolerance/README.md index 456325e..935046a 100644 --- a/docs/abaqus-tolerance/README.md +++ b/docs/abaqus-tolerance/README.md @@ -1,9 +1,17 @@ # 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)를 따른다. 아래는 조사 당시 코드와 @@ -178,7 +186,8 @@ Abaqus 내부 직접분해·pivot·rank 판정 숫자는 추측하지 않았다. 확인된 사실은 엄격한 HHT 종료 기준과, 외부 Newton 외에도 비용이 큰 내부 root· 요소 도함수·조립 경로가 존재한다는 점이다. 허용치가 실제 장시간 실행의 주원인인지는 조사 당시에는 측정하지 않았다. 이후 같은 local 근사본의 strict 공식 실행은 -6350.7895521초/native0을 확보했지만, 물리 실패가 남아 공식 후보100증분은 미실행이다. +6350.7895521초/native0을 확보했다. T6/T7에서는 물리 실패로 후보100증분을 실행하지 않았으나, +후속 사용자 요청의 변위 비교에서는 Release 후보 원본을 5662.144854초/native0으로 완료하고 U PASS를 확인했다. 첫 증분의 호출 수 감소만으로 전체 병목이나 가속을 확정하지 않는다. [기존 성능 기록](../calculation-performance/README.md)의 6372.325621초는 과거 source의 단일 baseline으로 최신 revision의 측정값이 아니다. diff --git a/docs/abaqus-tolerance/application-plan.md b/docs/abaqus-tolerance/application-plan.md index e6889a7..3511ff4 100644 --- a/docs/abaqus-tolerance/application-plan.md +++ b/docs/abaqus-tolerance/application-plan.md @@ -1,5 +1,10 @@ # 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 경로 전체 검사와 별도 비교를 수행했다. diff --git a/docs/abaqus-tolerance/reference-displacement-comparison.md b/docs/abaqus-tolerance/reference-displacement-comparison.md new file mode 100644 index 0000000..4aa2437 --- /dev/null +++ b/docs/abaqus-tolerance/reference-displacement-comparison.md @@ -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++ 코드·수치 기준을 변경하거나 전체 검증을 다시 실행하지 않았다. diff --git a/docs/abaqus-tolerance/relative-error-review.md b/docs/abaqus-tolerance/relative-error-review.md new file mode 100644 index 0000000..c7267f3 --- /dev/null +++ b/docs/abaqus-tolerance/relative-error-review.md @@ -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의 불일치 및 실행 실패를 별도로 해결해야 한다. diff --git a/docs/abaqus-tolerance/results.md b/docs/abaqus-tolerance/results.md index b2fc683..92d8e13 100644 --- a/docs/abaqus-tolerance/results.md +++ b/docs/abaqus-tolerance/results.md @@ -1,10 +1,17 @@ # 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모델 비교는 통과했다. 공식 MITC4 후보 100증분 실행· -reference 비교·성능 비교는 미성립이며 전체 검증 완료·가속 확인·정식 채택으로 보고하지 않는다. +Python 145개와 별도 정식 Hex8 5모델 비교는 통과했다. T6/T7 당시 MITC4 후보 100증분은 +미실행이었다. 후속 사용자 요청으로 Release 후보 원본을 5662.144854초에 완료하고 U 비교를 통과했다. +[8개 정식 모델 변위·빌드·시간 비교](reference-displacement-comparison.md)에 새 근거를 기록했다. +전체 검증 완료·가속 확인·정식 채택으로 보고하지 않는다. ## 기준과 원시 증거 @@ -355,15 +362,69 @@ Release EXE/PDB/7 DLL도 실행 전후 T6 manifest와 일치했다. 이 실행 정식 shellNL-meshfine·shellNL-meshfine_02·shellNL-meshfine-pressure 자료는 없다. `_02`는 자료 부재와 전용 case identity/비교 계약 미구현이 함께 남는다. 새 필수 thermal -reference도 미제공이다. `reference/temp`는 사용하지 않았다. 후보 MITC4 100증분 결과는 -앞서 설명한 수치 실패 분기로 미실행이며 strict baseline이나 1증분 결과로 대신하지 않는다. +reference도 미제공이다. `reference/temp`는 사용하지 않았다. T6/T7의 후보 MITC4 100증분은 +앞서 설명한 수치 실패 분기로 미실행이었다. 후속 새 원본 실행·U 비교는 별도 변위 보고서에 기록했으며, +strict baseline이나 1증분 결과로 대신하지 않았다. -## 판정과 다음 검증 조건 +## 최초 실험의 판정과 다음 검증 조건 **구현됨·수치 부적합·일부 검증 미완료·공식 성능 비교 미성립**으로 판정한다. Strict 공식 6350.7895521초, 1증분 개발 호출 수 감소, 전체 회귀 실행 사실은 서로 다른 근거다. 이를 합쳐 전체 가속이나 정식 채택 가능으로 보고하지 않는다. 다음에는 local 근사의 독립 미분/물리 정확도와 후보 후속 acceptance 실패의 원인을 분리하고, -실제 비선형 후보의 시간 정확도와 N7 판정을 확보해야 한다. 그 뒤 같은 조건의 공식 후보 -100증분과 정식 HHT 비교를 수행해야 전체 speedup을 판정할 수 있다. +실제 비선형 후보의 시간 정확도와 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는 하지 않았다. diff --git a/docs/hht-implicit-dynamics/README.md b/docs/hht-implicit-dynamics/README.md index e6162c1..03faa99 100644 --- a/docs/hht-implicit-dynamics/README.md +++ b/docs/hht-implicit-dynamics/README.md @@ -2,6 +2,11 @@ ## 범위와 현재 상태 +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개가 실패했다. 후속 사용자 지시로 관성·접선· @@ -25,9 +30,11 @@ shell schema v2**다. 과거 consistent 질량·full-coordinate v1 근거를 변 원본을6372.325621초/native0으로 완료하고 schema v2·729nodes/101frames를 확인했다. 9-node U blocking 비교는 PASS(최대 relative RMS .0022824245), UR은 경고200건이었다. 이는 과거 baseline source의 결과다. 2026-10-19 같은 승인 local 근사본의 strict 원본을 -6350.7895521초/native0으로 완료했고 정식 U 비교는 통과했다. 현재 field 후보는 수치 실패로 -공식100증분을 실행하지 않았으며 새 원본 비교·비선형 시간수렴·전체 물리 판정과 speedup은 -미성립이다. 전체 회귀는 위 수치대로 실제 실행했으며 전체 green이 아니다. +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)에 현재 근거를 보존했다. ## 채택 방법과 산술 의미 @@ -143,7 +150,7 @@ N7 read-only linear/nonlinear defect solves와 arithmetic indicator는 정책으로 해소됐고 후속 C09/C08a7 통과와 구분한다. 최신 Debug h=1/64 완료 요약의 이전 Release 일치는 전체 raw state bitwise나 전체 시간수렴 증거가 아니다. 원본 accepted residual 최대 병진1.889238010e-15·회전1.452384011e-15도 Abaqus 변위 오차가 아니다. -C08b finer-grid·현재 field 후보의 원본 HDF5 비교·전체 물리 closure가 남는다. +C08b finer-grid·현재 field 후보의 비선형 시간 정확도·전체 물리 closure가 남는다. 전체 회귀의 실제 실패와 해상도 SKIP을 과거 통과로 덮지 않는다. ## 보존한 과거 수식과 연구의 위치 diff --git a/docs/tracking/decisions/0011-tolerance-experiment-acceptance.md b/docs/tracking/decisions/0011-tolerance-experiment-acceptance.md new file mode 100644 index 0000000..ef47078 --- /dev/null +++ b/docs/tracking/decisions/0011-tolerance-experiment-acceptance.md @@ -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)를 따른다. diff --git a/docs/tracking/decisions/index.md b/docs/tracking/decisions/index.md index 2ad9a1d..6ac7aed 100644 --- a/docs/tracking/decisions/index.md +++ b/docs/tracking/decisions/index.md @@ -12,3 +12,4 @@ | `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과 후속 검증을 보존한다. | diff --git a/docs/tracking/findings.md b/docs/tracking/findings.md index c4367aa..9d1051f 100644 --- a/docs/tracking/findings.md +++ b/docs/tracking/findings.md @@ -1,5 +1,13 @@ # 해결되지 않은 문제 +## 이번 실험의 보류 결정 + +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차·질량 검사 @@ -24,12 +32,14 @@ 분류하지 않는다. C08 strict algebra의 시간 controller4개도 첫 격자에서 실패한다. - 영향: 강체 병진 oracle와 기존 algebra 시간수렴 통과만으로 실제 비선형 후보의 q/v/physical a refinement·차수·finest 오차·N7/물리 invariant closure를 인증할 수 없다. - N7 한 항목은 해상도 SKIP이다. 공식 후보100증분·정식 HHT 비교·전체 speedup은 미성립이다. + N7 한 항목은 해상도 SKIP이다. 후속 Release 후보100증분은5662.144854초/native0으로 완료하고 + 정식9-node U 비교를 통과했으나 비선형 시간 정확도·전체 speedup은 미성립이다. - 지금 해결하지 못한 이유: 승인 local 근사본의 독립 정확도 실패가 동반되며 원인 분리가 필요하다. 정해진 근사 수식을 자동 복원하거나 유지할 hidden·triad·평형 기준을 완화해 성공시키는 것은 이번 실험의 변경 범위가 아니다. 추가 강제 보정이나 실패 frame commit도 하지 않는다. - 다음 접근: polar/inertia 근사 영향과 candidate 후속 gate 실패의 원인을 분리한다. - 유지할 물리 기준에서 실제 비선형 후보의 시간·불변량 검증을 확보한 뒤 공식100증분을 측정한다. + 유지할 물리 기준에서 실제 비선형 후보의 시간·불변량 검증을 확보한 뒤 공식 성능 비교를 수행한다. + 새 원본 U 비교의 범위와 시간은 [변위 비교](../abaqus-tolerance/reference-displacement-comparison.md)에 있다. 전체 실행·실패 identity와 strict 대조 근거는 수렴 정책 결과의 원시 기록에 보존했다. ## 개발 계측의 정확 parity와 전체 비용 미확정 diff --git a/docs/tracking/status.md b/docs/tracking/status.md index d766be9..3a4ae86 100644 --- a/docs/tracking/status.md +++ b/docs/tracking/status.md @@ -1,5 +1,11 @@ # 구현과 검증 상태 +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 전체는 @@ -7,7 +13,9 @@ Accepted 평균 이력과 hht-log-v2도 구현했다. Native Debug와 CMake Debu 5모델 CLI/비교기는 통과했다. Native unit883개 중10 FAIL·1 SKIP, integration72개와 reference43개는 모두 통과했다. 두 build 성공과 source/등록 identity/DLL 대조도 확보했다. 물리 실패·N7 해상도 미판정·실제 비선형 후보 시간 정확도 미완료로 전체 검증 완료는 아니다. -Strict 공식 기준6350.7895521초는 확보했으나 공식 후보100증분·speedup은 미성립이다. +Strict 공식 기준6350.7895521초를 확보했다. 후속 변위 비교 요청에서 Release 후보 원본100증분을 +5662.144854초에 완료하고 U PASS를 확인했으며 공식 speedup은 미성립이다. +정식 변위8모델의 Release 구성·실행 시간·오차는 [변위 비교](../abaqus-tolerance/reference-displacement-comparison.md)에 있다. 상세 revision·실행 증거는 [수렴 정책 실험 결과](../abaqus-tolerance/results.md)에 있다. ## 과거 통합 기준 시점 @@ -24,7 +32,7 @@ dependency 재배포 조건은 확정되지 않았다. | 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 원본 비교만 확보, 공식 후보 원본 비교는 미실행. | +| 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개 일치. 수치 검증 전체 통과와 구분한다. |