Merge Jet product optimization and verified timing results into dev

Integrate reviewed feature 01d004c385.
Single official TBB pair: 4134.657782 to 3747.788261 seconds (9.356748% lower).
Numeric bits and per-increment Newton counts match; no new regression.
Existing 16 FAIL / 1 SKIP, two missing references and physical-validation
limitations remain. Full validation and independent review are preserved in
.build/jet-product-20261021-01/. No remote push.
This commit is contained in:
KOKO\Mimi
2026-10-21 22:09:15 +09:00
9 changed files with 1278 additions and 16 deletions
+6
View File
@@ -14,6 +14,8 @@ Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "fesa_reference_tests", "msv
EndProject
Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "gtest", "msvc\gtest.vcxproj", "{03194BA4-3F3B-5399-BCAF-FB4FA47300CE}"
EndProject
Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "fesa_jet_baseline_tests", "msvc\fesa_jet_baseline_tests.vcxproj", "{5FD398DA-6058-4CE3-9440-C9E0FDD180C3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -44,5 +46,9 @@ Global
{03194BA4-3F3B-5399-BCAF-FB4FA47300CE}.Debug|x64.Build.0 = Debug|x64
{03194BA4-3F3B-5399-BCAF-FB4FA47300CE}.Release|x64.ActiveCfg = Release|x64
{03194BA4-3F3B-5399-BCAF-FB4FA47300CE}.Release|x64.Build.0 = Release|x64
{5FD398DA-6058-4CE3-9440-C9E0FDD180C3}.Debug|x64.ActiveCfg = Debug|x64
{5FD398DA-6058-4CE3-9440-C9E0FDD180C3}.Debug|x64.Build.0 = Debug|x64
{5FD398DA-6058-4CE3-9440-C9E0FDD180C3}.Release|x64.ActiveCfg = Release|x64
{5FD398DA-6058-4CE3-9440-C9E0FDD180C3}.Release|x64.Build.0 = Release|x64
EndGlobalSection
EndGlobal
+107 -3
View File
@@ -10,10 +10,114 @@ Debug·Release 네 빌드는 모두 종료0이며 CLI와 세 테스트 실행
Debug는 `mkl_tbb_threadd`·`tbb12_debug`, Release는 `mkl_tbb_thread`·`tbb12`를 사용하고
HDF5의 `libmmd.dll`을 유지한다. 근거는 `.build/mkl-tbb-20261020-01/`에 보관했다.
사용자 지시로 이번 설정 변경에서는 테스트·모델·수치 비교·성능 측정·VTune을 실행하지
않았다. 실제 DLL loading, 수치 정확성, 속도 향상과 OpenMP 대기 제거는 미검증이다.
사용자 지시로 당시 설정 변경에서는 테스트·모델·수치 비교·성능 측정·VTune을 실행하지
않았다. 실제 DLL loading, 수치 정확성, 속도 향상과 OpenMP 대기 제거는 당시 미검증이었다.
기존 OpenMP 전용 local-thread/TLS helper와 HHT cache 진단 코드는 유지했으며 TBB에서의
동등성도 확인하지 않았다. 아래 과거 시간·검증 기록을 새 TBB 구성의 결과로 승계하지 않는다.
동등성도 당시 확인하지 않았다. 이후 Jet 최적화의 새 TBB 검증·측정은 다음 절과 구분한다.
아래 과거 OpenMP 시간·검증 기록을 새 TBB 구성의 결과로 승계하지 않는다.
## Jet Product 최적화와 현재 검증
2026-10-21 TBB 기준 `9ae7e6d414bfe36c20433c9dfa9fa4095aa9f7bd`와 최종 제품 source
`685f5e6a826c7bb66b9cef90e086ef4b2685252d`를 새로 비교했다. 후속 `3ede11b`는
검증 근거만 기록한 빈 commit이며 제품 tree는 같다. 24자유도 Shell Jet과 private 누적
helper만 수정했고 물리·공개 API·schema·수렴 기준은 유지했다. 기본 직접 작은 drilling
스프링과 기존 상대 비틀림을 모두 보존했으며 공식 시간 모델은 직접 방식을 사용했다.
| 검토 후보 | 채택 결과와 이유 |
| --- | --- |
| 외적·Hessian 결합 | 모든 MXCSR 예외가 masked이고 첫 MKL 외적이 성공한 경우에만 그 전치를 재사용한다. 두 번째 외적 호출을 줄이되 각 성분의 덧셈 순서를 유지한다. Unmasked 또는 외적 거부 시 기존 순서로 계산한다. |
| 누적의 중간 저장 | 소비되는 임시 Jet이 할당된 Hessian을 단독 소유할 때만 재사용한다. 공유·동일 객체 alias·unmasked 예외는 기존 경로를 유지한다. |
| 상수 연산 특화 | 단순 scalar 대체는 원본 192개 경계 그룹 중 184개에서 signed zero 등의 결과가 달라 비채택했다. 기존 constant provenance와 implicit-zero는 유지한다. |
| 요청 없는 방향 채널 생략 | 비채택했다. 사용하지 않는 고차 채널도 overflow·예외·finite 실패를 결정하므로 단순 생략은 실패를 성공으로 바꿀 수 있다. 필요한 방향·혼합 도함수는 모두 유지한다. |
| 메모리 관리 | 서로 배타적인 빠른 경로와 기존 경로가 scratch 배열 하나를 공유한다. 단순 monotonic arena는 저장량 증가로 비채택했으며 범용 recycler는 구현·측정하지 않았다. |
대표 local 평가의 Hessian 할당은 직접 방식에서 방향 없음 5416→4047회, 방향 있음
6452→4911회로 줄었다. 기존 방식은 7456→5733회, 7636→5897회다. MSVC Release 곱셈의
예약 stack은 통합 후보의 7696→5488 bytes로 2208 bytes 감소했지만 원래 5472 bytes보다
16 bytes 크고 `__chkstk`도 유지한다. 단순 arena는 직접 평가의 payload를 331776에서
18648576 bytes로 늘려 부적합했다. 이 할당·stack 수치를 전체 시간 단축률로 환산하지 않는다.
부동소수점 예외의 첫 trap, 수명·공유·동시·중첩 평가와 기존 private helper의 관성 consumer를
독립 검토했고 FMA·재결합·전역 compiler 옵션은 추가하지 않았다.
### 정식 모델 시간과 증분 기록
원본 `reference/shellNL-dyn-meshfine/shellNL-dyn-meshfine.inp`의 hash가 같은 새 복사본에서
MSVC x64 Release·동일 MKL/TBB runtime DLL·기본 스레드 환경·전원 조건으로 각각 한 번
실행했다. Ryzen7 5800H의 8 cores/16 logical processors 환경이며 별도 워밍업·VTune·동시
빌드·시험·모델 실행은 없다. 타이머는 `Popen` 요청 직전부터 `wait` 반환 직후까지로,
읽기·계산·복구·로그·HDF5 close/finalize를 포함하고 준비·빌드·비교는 제외한다.
| 실행 | 전체 초 | 분·초 |
| --- | ---: | ---: |
| 새 TBB 기준 | 4134.657782 | 68분 54.66초 |
| 최종 개선본 | 3747.788261 | 62분 27.79초 |
이 단일 쌍에서 386.869521초(6분 26.87초), 9.356748% 감소·speedup 1.103226배를 확인해
`improved`로 판정했다. 변경하지 않은 Native Hex8 선형 사례에서도 약4.9% 시간 감소가
있었고, 긴 비선형·동일 CMake 경로의 Hex8/열 사례는 기준과 훨씬 가까웠다. 이는 실행 변동의
문맥이며 공식 시간을 보정하거나 분산·통계 신뢰도를 추정하는 데 사용하지 않았다. 현재
구체적인 환경·수치·반복 불일치가 없어 추가 쌍이나 profile은 실행하지 않았다. 한 쌍의
실측이며 반복 통계·다른 기계에서의 보장·특정 함수만의 기여를 입증한 것은 아니다.
기존 FEM-REQ-PERF-001..009와 Shell 100초 목표는 변경하지 않았고 여전히 미달이다.
두 실행 모두 증분0의 초기화와 양의 증분1..100을 구분했다. 모든 증분의 횟수가 같으며,
1..71은 Newton 보정 2회·로그 잔차 평가 3회, 72..100은 1회·2회다. 양의 증분 합은
171회·271회이고 초기화의 잔차 기록 1개는 별도다. 이는 고유 Newton 로그 기록 수이며
숨은 root·trial·assembly의 전체 호출 수가 아니다. 개선본의 초기화 9.044633초 +
스텝 합 3737.305479초 + 마무리 1.438149초는 전체 시간과 일치한다.
스텝 시간은 10ms 간격의 외부 accepted 로그 관측으로 얻은 경과시간이며 순수 요소 계산
시간이 아니다. 개선본의 최대 accepted 경계 관측 불확실성은 0.0329873초이고 누락·중복·
observer 오류·구간 합계 오차는 없다. 이 경계 불확실성은 스케줄링이나 실행 변동의 상한이
아니다. [기준 CSV](../../.build/jet-product-20261021-01/baseline/time_steps.csv)와
[개선본 CSV](../../.build/jet-product-20261021-01/candidate/time_steps.csv)에 101개 행의
시간·보정·관측 평가 수와 경계 간격을 보존했다.
### 정확성·회귀 결과와 남은 한계
정식 모델은 729절점·101프레임·schema2를 유지했다. HDF5의 2235개 dataset·27524개
attribute를 읽고 12358개 수치 leaf의 15066400개 성분을 대조해 finite·signed zero·
정수 identity를 포함한 수치 bytes가 모두 같음을 확인했다. 문자열 차이는 열거한 입력
경로 6개뿐이다. 전체729절점 내부 재현성과 외부9절점 참조의 U101 blocking family 통과를
구분하며, UR100 failed warning family·경고200개는 그대로다.
| 새 최종 검사 | 실제 실행 결과 |
| --- | --- |
| Native MSVC x64 Debug 전체 | 1035개 = 1018 PASS / 기존16 FAIL / 기존1 SKIP |
| 새 CMake Debug 전체 CTest | 1036개 = 1019 PASS / 기존16 FAIL / 기존1 SKIP |
| Python | 164 PASS |
| 세 구성의 독립·원본 Jet 대조 | 구성별 5종, 총15개 명령 PASS |
Native 단위915·통합74·참조46개와 CTest의 실제 등록·실행 identity를 대조했다. CTest에는
같은1035개 외 독립 Jet 검사1개가 포함된다. Native 단위 exit1·CTest exit8은 기준에서
재현된 실패 때문이며 전체 green이 아니다. 모든 기존 실패 assertion·수치·skip 진단은
명시한 source 경로와 GTest 시간 줄 외에 동일했고 신규 회귀는 없다. 원본 Hex8 동해석
101프레임 두 사례, 열 2차원 격자 수렴과 슬래브12개를 포함해 실행했다. 각 구성의 독립
항등식·원본42 private/64 public 사례·곱셈3072·누적192·alias8 대조도 통과했으며 기존
snapshot은 재생성하지 않았다. 결과 비교 도구는 동일 복사본/허용 경로 차이 2개와 값·
signed zero·ID·예상 밖 문자열·NaN 검출 5개 대조 사례를 통과했다.
참조46개는 물리 모델46개의 정확성 통과를 뜻하지 않는다. 실제 분류는 positive Shell
검사3개·기타2개·policy/error 계약40개·기존 열 NT11 불일치 진단1개다. NT11의 87 failed
rows·정규화 RMS0.049674084615673193은 기준과 동일한 수치 실패이며 진단 검사의 PASS를
열 정확성 PASS로 바꾸지 않았다. 정식 `reference/shellNL-meshfine`와
`reference/shellNL-meshfine_02`가 없고 후자의 전용 비교 계약도 없다. `reference/temp`는
사용하지 않았으며 정식 원본36개 파일의 전후 hash는 같다. 기존16 FAIL·1 SKIP·자료 누락,
HHT 물리·시간 정확도와 열 필수 참조의 미해결 범위가 남아 전체 물리 검증은 미완료다.
기존 `.build/cmake-debug` 캐시는 다른 source root를 가리켜 그대로 보존했다. 이번에는
동일 preset에 `-B .build/cmake-jet-debug`를 지정하고 그 경로를 build·CTest `--test-dir`에
사용했다. 실제231개 compile command와 전체 검사 등록이 명시한 source/build 경로 외에는
기준과 같음을 확인했다. Native Release 명령의 include 경로 따옴표 차이는 Windows argv로
대조했고 옵션·순서·값은 같다. 과거 compiler binary SHA는 확보되지 않았으므로 동일 hash를
주장하지 않으며 보존된 compiler/pass 버전·경로·실제 옵션·SDK/runtime와 현재 hash를 기록했다.
전체 근거는 [최종 비교](../../.build/jet-product-20261021-01/comparison.json),
[검사·실행 요약](../../.build/jet-product-20261021-01/t6/T6-summary.json),
[독립 검토](../../.build/jet-product-20261021-01/review/t6-final.json)에 있다. 각 실행의 source·
실행물·입력·결과 hash와 raw·즉시 exit를 보존했으며, 자료는 로컬 `.build`에 있고 Git에
포함하지 않는다. 문서 갱신 뒤 제품 source가 바뀌지 않았으므로 build/test를 반복하지 않았다.
## 채택한 계산 경로와 수명
+11 -2
View File
@@ -9,13 +9,22 @@ polar/swing/twist를 생략하고 physical 회전·관성·고정 null 기저·
`sum_e kd*gamma_ei*(n_i·d_i0)-n_i·Fext_rot`다. 그 같은 식의 gradient·Hessian·
필요한 3차 방향미분을 사용한다. 초기 director를 현재 null로 고정하지 않는다.
현재 source `6bbbb2b`를 전체 실행한 Debug Native/CTest는 각각1035개 중
2026-10-21 현재 Jet 최적화 제품 source는 `685f5e6`이다. 새 TBB 기준 `9ae7e6d`와의
전체 회귀에서 Native1035개는 1018 PASS·기존16 FAIL·1 SKIP, CTest1036개는
1019 PASS·같은16 FAIL·1 SKIP, Python164개는 PASS이며 신규 회귀는 없다.
직접 작은 스프링의 정식729절점·101프레임 수치 bytes와 모든 증분의 Newton 기록 수는
기준과 같고 외부9절점 U101 families는 PASS, UR 경고200개는 유지했다. 동일 Release 조건의
공식 한 쌍은 4134.657782→3747.788261초로 9.36% 짧게 측정됐다. 반복 통계나 전체 물리·
시간 정확도 통과를 뜻하지 않으며 기존16 FAIL·1 SKIP·정식 참조 누락은 남는다.
후보별 채택·비채택과 측정 범위는 [계산 성능 결과](../calculation-performance/README.md)에 있다.
2026-10-20 source `6bbbb2b`를 당시 전체 실행한 Debug Native/CTest는 각각1035개 중
1024 PASS·기존10 FAIL·1 INCONCLUSIVE SKIP이며 별도 Python164개는 PASS다.
정식729node·101frame Direct Release 해석과 외부9node U의101 families 비교는
CLI0/비교0, 최대 정규화 RMS0.2282326248391787%로 통과했고 UR 경고200개를 유지했다.
기존 방식은 별도 Debug의 관련 검사와 실제 소형3frame HHT 출력을 확인했다.
729node 기존 방식의 외부 U 재해석은 이번에 하지 않았다. 과거 결과를 현재 PASS로 승계하지 않는다.
기존10개 물리 실패·N7 미판정·정식 자료 누락이 남으므로 전체 검증 완료는 아니다.
당시10개 물리 실패·N7 미판정·정식 자료 누락으로 전체 검증 완료는 아니었다.
아래 2026-10-19 수용 기록은 이전 수렴 실험의 결정이며 현재 전체 PASS를 뜻하지 않는다.
2026-10-19 후속 사용자 승인으로 이번 수렴 규칙 실험은 **Abaqus 변위 검증 통과·실패10개 보류로 조건부 완료**한다.
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations"><ProjectConfiguration Include="Debug|x64"><Configuration>Debug</Configuration><Platform>x64</Platform></ProjectConfiguration><ProjectConfiguration Include="Release|x64"><Configuration>Release</Configuration><Platform>x64</Platform></ProjectConfiguration></ItemGroup>
<PropertyGroup Label="Globals"><ProjectGuid>{5FD398DA-6058-4CE3-9440-C9E0FDD180C3}</ProjectGuid><Keyword>Win32Proj</Keyword><WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion></PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration"><ConfigurationType>Application</ConfigurationType><PlatformToolset>v145</PlatformToolset><UseDebugLibraries Condition="'$(Configuration)'=='Debug'">true</UseDebugLibraries></PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<Import Project="Fesa.Common.props" />
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(FesaRoot)src\fesa;$(FESA_MKL_ROOT)\include;$(FESA_TBB_ROOT)\include;$(FESA_HDF5_ROOT)\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>MKL_ILP64;__TBB_NO_IMPLICIT_LINKAGE=1;H5_BUILT_AS_DYNAMIC_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup><ClCompile Include="..\tests\unit\elements\nonlinear_mitc4_shell_jet_baseline.cpp" /></ItemGroup>
<ItemGroup>
<ProjectReference Include="fesa_solver.vcxproj"><Project>{3AEC39D9-4641-5161-9107-265EB50CA882}</Project><LinkLibraryDependencies>true</LinkLibraryDependencies></ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="Fesa.Runtime.targets" />
</Project>
+72 -9
View File
@@ -1,5 +1,7 @@
#include "fesa/elements/nonlinear_mitc4_shell.h"
#include <xmmintrin.h>
#include <algorithm>
#include <array>
#include <cmath>
@@ -384,6 +386,9 @@ class Hessian {
/// @brief Identifies only the immutable zero Hessian, independently of seeds.
bool IsImplicitZero() const { return !values_; }
/// @brief Allows a consumed temporary to reuse only its own materialized data.
bool HasUniqueStorage() const { return values_ && values_.use_count() == 1; }
/// @brief Gets fresh output storage without redundant zero initialization.
/// @note Only fresh operator results use this pointer; all 576 entries must
/// be written before any read or copy of the result.
@@ -412,11 +417,14 @@ class Hessian {
/// @brief Forms an ordinary derivative outer product without a Matrix copy.
/// @note Rejected nonfinite operands propagate to the existing Jet finite gate.
void Outer(const std::array<double, kGlobalDofCount>& left,
bool Outer(const std::array<double, kGlobalDofCount>& left,
const std::array<double, kGlobalDofCount>& right, double* result) {
if (!dense_blas_internal::OuterProduct(left.data(), left.size(), right.data(),
right.size(), result))
right.size(), result)) {
std::fill_n(result, kHessianSize, std::numeric_limits<double>::quiet_NaN());
return false;
}
return true;
}
/// @brief Carries exact first and second trial-coordinate variations.
@@ -483,6 +491,30 @@ Jet operator+(const Jet& left, const Jet& right) {
return result;
}
/// @brief Adds to a consumed temporary without allocating another Hessian.
/// @note Shared/live aliases and unmasked exceptions retain the original path.
Jet operator+(Jet&& left, const Jet& right) {
if ((_mm_getcsr() & _MM_MASK_MASK) != _MM_MASK_MASK || &left == &right ||
!left.second.HasUniqueStorage())
return static_cast<const Jet&>(left) + right;
left.value = left.value + right.value;
left.directional_first = left.directional_first + right.directional_first;
left.directional_second = left.directional_second + right.directional_second;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
left.first[index] = left.first[index] + right.first[index];
left.mixed_first[index] = left.mixed_first[index] + right.mixed_first[index];
left.mixed_second[index] = left.mixed_second[index] + right.mixed_second[index];
}
left.constant_derivatives = left.constant_derivatives &&
right.constant_derivatives &&
std::isfinite(left.value);
const double* right_second = right.second.ReadData();
double* left_second = left.second.Data();
for (std::size_t index = 0U; index < kHessianSize; ++index)
left_second[index] = left_second[index] + right_second[index];
return std::move(left);
}
/// @brief Subtracts two second-order trial-coordinate quantities.
Jet operator-(const Jet& left, const Jet& right) {
Jet result{};
@@ -559,14 +591,45 @@ Jet operator*(const Jet& left, const Jet& right) {
left.value * right_second[index];
return result;
}
Outer(left.first, right.first, result_second);
std::array<double, kHessianSize> reverse;
Outer(right.first, left.first, reverse.data());
const bool masked_exceptions =
(_mm_getcsr() & _MM_MASK_MASK) == _MM_MASK_MASK;
const bool outer_accepted = Outer(left.first, right.first, result_second);
std::array<double, kHessianSize> scratch;
if (masked_exceptions && outer_accepted) {
// Retain earlier upper entries for later lower entries before overwriting
// them. Reuse the MKL k=1 transpose while preserving the original row-major
// arithmetic order as well as each entry's four-term association.
std::size_t saved = 0U;
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < row; ++column) {
const std::size_t index = row * kGlobalDofCount + column;
result_second[index] = left_second[index] * right.value +
result_second[index] + scratch[saved++] +
left.value * right_second[index];
}
const std::size_t diagonal = row * kGlobalDofCount + row;
const double product = result_second[diagonal];
result_second[diagonal] = left_second[diagonal] * right.value +
product + product + left.value * right_second[diagonal];
for (std::size_t column = row + 1U; column < kGlobalDofCount; ++column) {
const std::size_t index = row * kGlobalDofCount + column;
const std::size_t transpose = column * kGlobalDofCount + row;
const double forward = result_second[index];
const double reverse = result_second[transpose];
scratch[column * (column - 1U) / 2U + row] = forward;
result_second[index] = left_second[index] * right.value +
forward + reverse + left.value * right_second[index];
}
}
return result;
}
// Preserve the reverse input-validation order after a rejected first outer.
Outer(right.first, left.first, scratch.data());
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
const std::size_t index = row * kGlobalDofCount + column;
result_second[index] = left_second[index] * right.value +
result_second[index] + reverse[index] +
result_second[index] + scratch[index] +
left.value * right_second[index];
}
}
@@ -1068,7 +1131,7 @@ Jet FrameComponent(const JetVector3& left, const JetTensor& tensor,
Jet result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result = result + left[row] * tensor[row][column] * right[column];
result = std::move(result) + left[row] * tensor[row][column] * right[column];
}
}
return result;
@@ -1146,9 +1209,9 @@ Jet PhysicalEnergy(const std::array<Vector3, kNodeCount>& coordinates,
shear * strain[4U]};
Jet density{};
for (std::size_t component = 0U; component < strain.size(); ++component) {
density = density + strain[component] * stress[component];
density = std::move(density) + strain[component] * stress[component];
}
energy = energy + 0.5 * reference.reference_jacobian * density;
energy = std::move(energy) + 0.5 * reference.reference_jacobian * density;
}
}
if (!IsFinite(energy)) {
+3 -2
View File
@@ -3,6 +3,7 @@
#include <array>
#include <cstddef>
#include <utility>
namespace fesa::math_internal {
/// @brief Multiplies fixed Jet matrices in row/column/index order.
@@ -17,7 +18,7 @@ std::array<std::array<Jet, 3>, 3> MultiplyJetMatrix(
for (std::size_t column = 0; column < 3; ++column) {
result[row][column] = constant(0.0);
for (std::size_t index = 0; index < 3; ++index)
result[row][column] = result[row][column] +
result[row][column] = std::move(result[row][column]) +
left[row][index] * right[index][column];
}
return result;
@@ -31,7 +32,7 @@ std::array<Jet, 3> MultiplyJetMatrixVector(
std::array<Jet, 3> result{constant(0.0),constant(0.0),constant(0.0)};
for (std::size_t row = 0; row < 3; ++row)
for (std::size_t column = 0; column < 3; ++column)
result[row] = result[row] + matrix[row][column] * vector[column];
result[row] = std::move(result[row]) + matrix[row][column] * vector[column];
return result;
}
} // namespace fesa::math_internal
+19
View File
@@ -179,6 +179,17 @@ target_compile_definitions(
FESA_TEST_BINARY_DIR="${PROJECT_BINARY_DIR}"
)
add_executable(fesa_jet_baseline_tests
unit/elements/nonlinear_mitc4_shell_jet_baseline.cpp)
target_include_directories(fesa_jet_baseline_tests PRIVATE
"${PROJECT_SOURCE_DIR}/src/fesa")
target_compile_definitions(fesa_jet_baseline_tests PRIVATE
MKL_ILP64 __TBB_NO_IMPLICIT_LINKAGE=1 H5_BUILT_AS_DYNAMIC_LIB)
target_compile_options(fesa_jet_baseline_tests PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/fp:precise /EHsc>)
target_link_libraries(fesa_jet_baseline_tests PRIVATE
fesa_solver Fesa::MKL Fesa::TBB Fesa::HDF5)
if(WIN32)
if(NOT TARGET ${FESA_MKL_THREAD_TARGET} OR NOT TARGET MKL::mkl_core)
message(FATAL_ERROR "The oneMKL runtime files required by tests were not resolved")
@@ -226,6 +237,7 @@ if(WIN32)
fesa_stage_test_runtime(fesa_unit_tests)
fesa_stage_test_runtime(fesa_integration_tests)
fesa_stage_test_runtime(fesa_reference_tests)
fesa_stage_test_runtime(fesa_jet_baseline_tests)
unset(_fesa_mkl_dispatch_dll)
unset(_fesa_mkl_dispatch_dlls)
@@ -258,4 +270,11 @@ add_custom_target(
fesa_unit_tests
fesa_integration_tests
fesa_reference_tests
fesa_jet_baseline_tests
)
# Verification never creates or replaces an immutable baseline snapshot.
add_test(NAME NonlinearMitc4Jet.IndependentOracle
COMMAND fesa_jet_baseline_tests --verify)
set_tests_properties(NonlinearMitc4Jet.IndependentOracle PROPERTIES
LABELS "unit;jet-baseline")
@@ -0,0 +1,935 @@
// Standalone private baseline fixture: compile this translation unit instead of
// nonlinear_mitc4_shell.cpp, then link the ordinary solver archive. No
// production source, public interface, or official CLI instrumentation is
// needed. Build target: fesa_jet_baseline_tests (MSBuild/CMake). --verify runs
// independent checks without writing evidence. Compare immutable evidence with
// --compare PATH, Debug to Debug or Release to Release; never regenerate a
// snapshot during regression. See nonlinear_mitc4_shell_jet_baseline.md.
#include <float.h>
#include <xmmintrin.h>
#include <cfenv>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <future>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <vector>
#include "../../../src/fesa/elements/nonlinear_mitc4_shell.cpp"
namespace fesa {
namespace {
using Coefficients = std::array<double, 24>;
std::fenv_t initial_environment;
unsigned initial_mxcsr;
std::string FpState() {
unsigned control = 0;
_controlfp_s(&control, 0, 0);
std::ostringstream out;
out << std::fetestexcept(FE_ALL_EXCEPT) << ':' << std::fegetround() << ':'
<< _statusfp() << ':' << control << ':' << _mm_getcsr();
return out.str();
}
std::string ResetFp() {
std::fesetenv(&initial_environment);
std::feclearexcept(FE_ALL_EXCEPT);
_clearfp();
_mm_setcsr(initial_mxcsr & ~0x3fu);
return FpState();
}
struct Monomial {
double coefficient;
std::array<unsigned, 24> powers{};
};
// Independent oracle: differentiate monomial exponent lists, never Jet rules.
double Differentiate(const std::vector<Monomial>& polynomial,
const Coefficients& point,
const std::vector<std::size_t>& derivative) {
long double sum = 0;
for (auto term : polynomial) {
long double value = term.coefficient;
for (auto index : derivative) {
value *= term.powers[index];
if (term.powers[index] == 0) {
value = 0;
break;
}
--term.powers[index];
}
for (std::size_t i = 0; i < 24; ++i)
for (unsigned p = 0; p < term.powers[i]; ++p) value *= point[i];
sum += value;
}
return static_cast<double>(sum);
}
std::uint64_t Bits(double value) {
std::uint64_t result;
std::memcpy(&result, &value, sizeof(result));
return result;
}
void Check(double actual, double expected, const char* channel) {
if (!std::isfinite(actual) ||
std::abs(actual - expected) > 2e-12 * std::max(1.0, std::abs(expected)))
throw std::runtime_error(std::string("independent oracle: ") + channel);
}
Jet Polynomial(const std::vector<Monomial>& terms,
const std::array<Jet, 24>& variables) {
Jet result = Constant(0);
for (const auto& term : terms) {
Jet value = Constant(term.coefficient);
for (std::size_t i = 0; i < 24; ++i)
for (unsigned p = 0; p < term.powers[i]; ++p)
value = value * variables[i];
result = result + value;
}
return result;
}
std::string Snapshot(const char* name, const Jet& jet, int exceptions,
unsigned status, unsigned mxcsr) {
std::ostringstream out;
out << name << ' ' << jet.constant_derivatives << ' '
<< jet.second.IsImplicitZero() << ' ' << IsFinite(jet) << ' '
<< exceptions << ' ' << status << ' ' << mxcsr << std::hex;
auto put = [&](double x) {
if (std::isnan(x))
out << " nan";
else
out << ' ' << Bits(x);
};
put(jet.value);
for (auto x : jet.first) put(x);
for (auto x : jet.second) put(x);
put(jet.directional_first);
put(jet.directional_second);
for (auto x : jet.mixed_first) put(x);
for (auto x : jet.mixed_second) put(x);
return out.str() + "\n";
}
template <class F>
std::string Observe(const char* name, F expression, Jet* returned = nullptr) {
const auto before = ResetFp();
Jet value = expression();
const int exceptions = std::fetestexcept(FE_ALL_EXCEPT);
const unsigned status = _statusfp();
const unsigned mxcsr = _mm_getcsr();
const auto after = FpState();
if (returned != nullptr) *returned = value;
return std::string("initial-fp=") + before + " final-fp=" + after + " " +
Snapshot(name, value, exceptions, status, mxcsr);
}
std::string Verify(bool corrupt_oracle) {
Coefficients x{}, direction{};
std::array<Jet, 24> variables{};
std::vector<Monomial> terms;
for (std::size_t i = 0; i < 24; ++i) {
x[i] = (static_cast<double>(i % 7) - 3) / 8;
direction[i] = (static_cast<double>(i % 5) - 2) / 16;
variables[i] = Variable(x[i], i, direction[i]);
Monomial term{(i % 2 ? -1.0 : 1.0) / 8};
term.powers[i] = 3;
term.powers[(i + 1) % 24] += 2;
terms.push_back(term);
Monomial cross{0.5};
cross.powers[i] = 1;
cross.powers[(i + 11) % 24] += 1;
terms.push_back(cross);
}
Jet polynomial = Polynomial(terms, variables);
if (corrupt_oracle) polynomial.value += 1; // Deliberate fixture-only mutant.
Check(polynomial.value, Differentiate(terms, x, {}), "value");
double d1 = 0, d2 = 0;
for (std::size_t i = 0; i < 24; ++i) {
const double gradient = Differentiate(terms, x, {i});
Check(polynomial.first[i], gradient, "gradient");
d1 += gradient * direction[i];
double mixed1 = 0, mixed2 = 0;
for (std::size_t j = 0; j < 24; ++j) {
const double hessian = Differentiate(terms, x, {i, j});
Check(polynomial.second.ReadData()[24 * i + j], hessian, "Hessian");
d2 += hessian * direction[i] * direction[j];
mixed1 += hessian * direction[j];
for (std::size_t k = 0; k < 24; ++k)
mixed2 +=
Differentiate(terms, x, {i, j, k}) * direction[j] * direction[k];
}
Check(polynomial.mixed_first[i], mixed1, "mixed_first");
Check(polynomial.mixed_second[i], mixed2, "mixed_second");
}
Check(polynomial.directional_first, d1, "directional_first");
Check(polynomial.directional_second, d2, "directional_second");
// Affine composition has a rank-one Hessian; its third derivative contracts
// independently to the mixed-second channel.
Jet affine = Constant(2);
Coefficients weights{};
double slope = 0;
for (std::size_t i = 0; i < 24; ++i) {
weights[i] = (static_cast<double>(i % 3) - 1) / 64;
affine = affine + weights[i] * variables[i];
slope += weights[i] * direction[i];
}
auto unary = [&](const Jet& jet, double value, double a, double b, double c) {
Check(jet.value, value, "unary value");
Check(jet.directional_first, a * slope, "unary direction1");
Check(jet.directional_second, b * slope * slope, "unary direction2");
for (std::size_t i = 0; i < 24; ++i) {
Check(jet.first[i], a * weights[i], "unary gradient");
Check(jet.mixed_first[i], b * weights[i] * slope, "unary mixed1");
Check(jet.mixed_second[i], c * weights[i] * slope * slope,
"unary mixed2");
for (std::size_t j = 0; j < 24; ++j)
Check(jet.second.ReadData()[24 * i + j], b * weights[i] * weights[j],
"unary Hessian");
}
};
const double t = affine.value, r = std::sqrt(t);
unary(Sin(affine), std::sin(t), std::cos(t), -std::sin(t), -std::cos(t));
unary(Cos(affine), std::cos(t), -std::sin(t), -std::cos(t), std::sin(t));
unary(Reciprocal(affine), 1 / t, -1 / (t * t), 2 / (t * t * t),
-6 / (t * t * t * t));
unary(Sqrt(affine), r, 0.5 / r, -0.25 / (r * r * r),
0.375 / (r * r * r * r * r));
const double den = 1 + t * t;
unary(Atan2(affine, Constant(1)), std::atan(t), 1 / den, -2 * t / (den * den),
(6 * t * t - 2) / (den * den * den));
const Jet original = polynomial;
const Jet alias = original;
const auto before = Snapshot("alias", alias, 0, 0, 0);
const Jet transformed = alias * alias + original;
if (before != Snapshot("alias", original, 0, 0, 0))
throw std::runtime_error("alias modified");
std::string out;
std::vector<std::future<std::pair<Jet, std::string>>> futures;
for (int i = 0; i < 4; ++i)
futures.push_back(std::async(std::launch::async, [&] {
Jet value;
const auto evidence = Observe(
"shared-full", [&] { return original * original + original; },
&value);
return std::make_pair(std::move(value), evidence);
}));
for (std::size_t i = 0; i < futures.size(); ++i) {
const auto worker = futures[i].get();
if (Snapshot("shared", worker.first, 0, 0, 0) !=
Snapshot("shared", transformed, 0, 0, 0))
throw std::runtime_error("shared complete-channel snapshot");
out += "worker-" + std::to_string(i) + " " + worker.second;
}
auto add = [&](const char* name, auto f) { out += Observe(name, f); };
add("constant", [] { return Constant(-0.0); });
add("variable-zero-direction", [] { return Variable(-0.0, 23); });
add("variable-direction", [] { return Variable(0.25, 0, -0.5); });
add("polynomial", [&] { return Polynomial(terms, variables); });
add("polynomial-zero-direction", [&] {
auto zero_direction = variables;
for (auto& variable : zero_direction) variable.directional_first = 0;
return Polynomial(terms, zero_direction);
});
add("actual-zero-hessian", [&] { return original - original; });
add("implicit-zero-nonconstant",
[] { return Variable(1, 0) - Variable(1, 0); });
add("implicit-add", [] { return Variable(1, 0) + Constant(-1); });
add("implicit-subtract", [] { return Variable(1, 0) - Constant(1); });
for (int kind = 0; kind < 4; ++kind) {
const auto name = "surviving-copy-" + std::to_string(kind);
add(name.c_str(), [&] {
Jet survivor;
std::string expected;
{
Jet source = kind == 0 ? Constant(-0.0)
: kind == 1 ? Variable(0.5, 7, -0.25)
: Polynomial(terms, variables);
if (kind == 3) source = source - source;
const Jet another_alias = source;
expected = Snapshot("survivor", source, 0, 0, 0);
survivor = another_alias;
}
if (Snapshot("survivor", survivor, 0, 0, 0) != expected)
throw std::runtime_error("surviving copy complete-channel snapshot");
return survivor;
});
}
add("constant-multiply", [] { return Constant(-0.0) * Constant(3); });
add("constant-compose", [] { return Sin(Constant(0)); });
add("scale", [&] { return -0.5 * original; });
add("multiply", [&] { return original * original; });
add("compose-sin", [&] { return Sin(original); });
add("compose-cos", [&] { return Cos(original); });
add("divide", [&] { return original / affine; });
add("sqrt", [&] { return Sqrt(affine); });
add("atan2", [&] { return Atan2(original, affine); });
add("dot", [&] {
return Dot(JetVector3{original, affine, variables[0]},
JetVector3{affine, variables[1], original});
});
add("cancellation",
[] { return (Variable(1e16, 0, 1) + Constant(1)) - Constant(1e16); });
add("overflow", [] {
return Variable(std::numeric_limits<double>::max(), 0, 1) * Constant(2);
});
add("subnormal", [] {
return std::numeric_limits<double>::denorm_min() * Variable(0.5, 0, 0.5);
});
add("nan", [] {
return Constant(std::numeric_limits<double>::quiet_NaN()) * Variable(1, 0);
});
add("inf", [] {
return Constant(std::numeric_limits<double>::infinity()) * Constant(0);
});
add("singular", [] { return Reciprocal(Variable(0, 0, 1)); });
add("singular-zero-direction", [] { return Reciprocal(Variable(0, 0)); });
add("constant-singular-reciprocal", [] { return Reciprocal(Constant(0)); });
add("constant-singular-sqrt", [] { return Sqrt(Constant(0)); });
add("negative-sqrt", [] { return Sqrt(Variable(-1, 0, 1)); });
add("after-failure", [&] { return Polynomial(terms, variables); });
// Exercise the production MKL k=1 outer path directly, including entries
// whose rounded bits agree while their IEEE exception state can differ.
auto outer = [](double a, double b) {
Jet jet{};
Coefficients left{}, right{};
for (std::size_t i = 0; i < 24; ++i) {
left[i] = i % 2 ? -a : a;
right[i] = i % 3 ? -b : b;
}
Outer(left, right, jet.second.OverwriteData());
return jet;
};
add("outer-signed-zero", [&] { return outer(-0.0, 1); });
add("outer-underflow",
[&] { return outer(std::numeric_limits<double>::denorm_min(), 0.5); });
add("outer-overflow",
[&] { return outer(std::numeric_limits<double>::max(), 2); });
add("outer-invalid",
[&] { return outer(std::numeric_limits<double>::infinity(), 0); });
if (!Constant(1).constant_derivatives ||
!Constant(1).second.IsImplicitZero() ||
Variable(1, 0).constant_derivatives ||
!Variable(1, 0).second.IsImplicitZero() ||
polynomial.second.IsImplicitZero())
throw std::runtime_error("provenance");
return out;
}
// Separate supplemental evidence: never changes the original T1 snapshot.
// A changed Hessian association, a lost transpose term, or a changed MKL zero
// convention must fail the bit/classification and complete FP-state comparison.
std::string ProductSnapshots(bool omit_transpose) {
const std::array<double, 16> boundary{
0.0, -0.0, 1.0, -1.0, 0.1, -0.3,
std::numeric_limits<double>::denorm_min(),
-std::numeric_limits<double>::denorm_min(),
std::numeric_limits<double>::min(),
std::nextafter(std::numeric_limits<double>::min(), 0.0),
std::numeric_limits<double>::max(),
-std::numeric_limits<double>::max(),
std::numeric_limits<double>::infinity(),
-std::numeric_limits<double>::infinity(),
std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::signaling_NaN()};
std::string out;
for (const int rounding : {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD,
FE_TOWARDZERO}) {
for (const unsigned flush : {0u, 0x40u, 0x8000u, 0x8040u}) {
for (const bool sticky : {false, true}) {
auto reset = [&] {
ResetFp();
std::fesetround(rounding);
_mm_setcsr((_mm_getcsr() & ~0x8040u) | flush);
if (sticky) std::feraiseexcept(FE_ALL_EXCEPT);
return FpState();
};
for (std::size_t kind = 0; kind < 96; ++kind) {
ResetFp(); // Construct inputs outside the observed environment.
Jet left{}, right{};
left.value = 0.75;
right.value = -1.25;
left.directional_first = 0.25;
right.directional_first = -0.5;
left.directional_second = -0.125;
right.directional_second = 0.375;
std::uint64_t random = 0x719b245af51d6723ULL + kind;
auto coefficient = [&] {
random ^= random << 13;
random ^= random >> 7;
random ^= random << 17;
return (static_cast<double>(random % 2000001) - 1000000) /
1048576.0;
};
for (std::size_t i = 0; i < 24; ++i) {
left.first[i] = kind < 32 ? boundary[(kind + i) % 16]
: coefficient();
right.first[i] = kind < 32 ? boundary[(kind / 2 + 3 * i) % 16]
: coefficient();
left.mixed_first[i] = coefficient();
right.mixed_first[i] = coefficient();
left.mixed_second[i] = coefficient();
right.mixed_second[i] = coefficient();
}
// Include dense, asymmetric inputs, implicit zeros and aliased input.
if (kind % 3 != 0) {
double* l = left.second.OverwriteData();
double* r = right.second.OverwriteData();
for (std::size_t i = 0; i < 576; ++i) {
l[i] = coefficient();
r[i] = coefficient();
}
}
if (kind == 32) left = Constant(-0.0);
if (kind == 33) right = Constant(0.0);
if (kind == 34) left.value = boundary[10];
if (kind == 35) right.value = boundary[12];
if (kind == 36) left.second.Data()[17] = boundary[10];
if (kind == 37) right.second.Data()[24] = boundary[10];
if (kind == 38) left.second.Data()[17] = boundary[14];
if (kind == 39) right = left; // Shared immutable Hessian storage.
if (kind == 40) {
left.first[0] = boundary[12];
right.first[0] = boundary[15];
}
if (kind == 41) {
left.first[0] = boundary[15];
right.first[0] = boundary[12];
}
if (kind >= 42 && kind < 64) {
for (std::size_t i = 0; i < 24; ++i) {
left.first[i] = boundary[(kind + i) % 12];
right.first[i] = boundary[(kind / 2 + 3 * i) % 12];
if (kind == 42) {
left.first[i] = boundary[6];
right.first[i] = i % 2 ? -0.5 : 0.5;
}
if (kind == 43) {
left.first[i] = boundary[10];
right.first[i] = i % 2 ? -2.0 : 2.0;
}
if (kind == 44) {
left.first[i] = i % 2 ? -0.0 : 0.0;
right.first[i] = i % 3 ? -1.0 : 1.0;
}
}
}
const auto left_before = Snapshot("input", left, 0, 0, 0);
const auto right_before = Snapshot("input", right, 0, 0, 0);
const auto before = reset();
Jet product = left * right;
if (omit_transpose && kind >= 64) {
// Deliberately omit the reverse outer term in one finite entry.
// This mutant tests arithmetic detection, not just file integrity.
product.second.Data()[1] =
left.second.ReadData()[1] * right.value +
left.first[0] * right.first[1] +
left.value * right.second.ReadData()[1];
}
const int exceptions = std::fetestexcept(FE_ALL_EXCEPT);
const unsigned status = _statusfp(), mxcsr = _mm_getcsr();
const auto after = FpState();
out += "product " + std::to_string(kind) + " initial-fp=" + before +
" final-fp=" + after + " " +
Snapshot("result", product, exceptions, status, mxcsr);
if (left_before != Snapshot("input", left, 0, 0, 0) ||
right_before != Snapshot("input", right, 0, 0, 0))
throw std::runtime_error("supplemental product modified input");
// Independently pin the actual MKL operand-swap/transpose contract.
// Capture FP state before inspecting values (sNaN classification can
// itself raise INVALID). Each call starts from the same environment.
Jet forward{}, reverse{};
reset();
Outer(left.first, right.first, forward.second.OverwriteData());
const auto forward_fp = FpState();
reset();
Outer(right.first, left.first, reverse.second.OverwriteData());
const auto reverse_fp = FpState();
for (std::size_t row = 0; row < 24; ++row) {
for (std::size_t column = 0; column < 24; ++column) {
const double a = forward.second.ReadData()[row * 24 + column];
const double b = reverse.second.ReadData()[column * 24 + row];
if (!(std::isnan(a) && std::isnan(b)) && Bits(a) != Bits(b))
throw std::runtime_error("outer transpose coefficient mismatch");
}
}
out += "forward-outer-fp=" + forward_fp +
" reverse-outer-fp=" + reverse_fp + "\n";
}
}
}
}
ResetFp();
return out;
}
// Public request-mode evidence for later direction-channel separation. Existing
// tangent tests check nonzero-direction derivatives but do not freeze the full
// omitted-direction response inventory and invalid-direction diagnostics.
std::string PublicSnapshots() {
std::array<Node, 4> nodes;
const std::array<std::array<double, 3>, 4> coordinates{
{{{-1, -1, 0}}, {{1, -1, 0}}, {{1, 1, 0}}, {{-1, 1, 0}}}};
std::array<const Node*, 4> pointers{};
std::array<ShellNodeInitialFrame, 4> frames{};
NonlinearMitc4TrialDofs trial{};
Vector zero(24), direction(24), wrong(23), nan(24), inf(24);
for (std::size_t i = 0; i < 4; ++i) {
nodes[i] = {{"Jet-Baseline", static_cast<std::int64_t>(i + 1),
std::to_string(i + 1)},
coordinates[i],
{"jet-baseline.inp", i + 1}};
pointers[i] = &nodes[i];
frames[i] = {static_cast<EntityIndex>(i), {0, 0, 1}, {1, 0, 0}, {0, 1, 0}};
trial.displacement[i] = {0.01 * i, -0.02 * i, 0.015 * i};
trial.rotation_vector[i] = {0.02 * i, -0.01 * i, 0.005 * i};
}
for (std::size_t i = 0; i < 24; ++i)
direction[i] = (static_cast<double>(i % 5) - 2) / 8;
nan[7] = std::numeric_limits<double>::quiet_NaN();
inf[7] = std::numeric_limits<double>::infinity();
const auto created = NonlinearMitc4Shell::Create(
pointers, frames, {"Section", 0.2, 0, {"jet-baseline.inp", 20}},
{"Material", 120, 0.25, {"jet-baseline.inp", 21}});
if (!created.HasValue()) throw std::runtime_error("public fixture creation");
std::ostringstream out;
for (const auto mode : {Mitc4DrillingMode::kDirectSpring,
Mitc4DrillingMode::kLegacyCorotatedTwist}) {
const auto shell =
mitc4_drilling_internal::TestAccess::WithMode(created.Value(), mode);
auto observe = [&](const char* name, const NonlinearMitc4TrialDofs& input,
const Vector* request,
std::optional<bool> expected_success) {
const auto before = ResetFp();
const auto result =
EvaluateInternalResidualAndTangent(shell, input, request);
const int exceptions = std::fetestexcept(FE_ALL_EXCEPT);
const unsigned status = _statusfp(), mxcsr = _mm_getcsr();
if (expected_success && result.HasValue() != *expected_success)
throw std::runtime_error(std::string("public outcome: ") + name);
out << "public " << static_cast<int>(mode) << ' ' << name << ' '
<< result.HasValue() << ' ' << exceptions << ' ' << status << ' '
<< mxcsr << " initial-fp=" << before << " final-fp=" << FpState();
auto put = [&](double value) {
if (!std::isfinite(value))
throw std::runtime_error("public nonfinite field");
out << ' ' << std::hex << Bits(value) << std::dec;
};
auto vector = [&](const Vector& value) {
out << " v" << value.Size();
for (std::size_t i = 0; i < value.Size(); ++i) put(value[i]);
};
auto matrix = [&](const Matrix& value) {
out << " m" << value.Rows() << 'x' << value.Columns();
for (std::size_t i = 0; i < value.Rows() * value.Columns(); ++i)
put(value.Data()[i]);
};
if (result.HasValue()) {
const auto& r = result.Value();
vector(r.internal_residual);
matrix(r.internal_tangent);
vector(r.internal_force);
put(r.physical_strain_energy);
vector(r.physical_force);
vector(r.drilling_force);
matrix(r.drilling_tangent);
matrix(r.drilling_metric);
put(r.drilling_energy);
vector(r.force_directional_second);
vector(r.drilling_force_directional_second);
vector(r.drilling_constraint);
matrix(r.drilling_constraint_gradient);
for (const auto& hessian : r.drilling_constraint_hessian)
matrix(hessian);
for (const auto& third :
r.drilling_constraint_third_directional_gradient)
vector(third);
put(r.recovery.physical_strain_energy);
for (const auto& point : r.recovery.gauss_points) {
out << " location=" << static_cast<int>(point.location);
for (double value : point.natural_coordinates) put(value);
for (const auto& axis : {point.local_frame.e1, point.local_frame.e2,
point.local_frame.e3})
for (double value : axis) put(value);
for (double value : point.generalized_strain) put(value);
for (double value : point.section_resultant) put(value);
for (const auto& stress : point.stress) {
out << " section=" << static_cast<int>(stress.position);
put(stress.zeta);
for (double value : stress.components) put(value);
}
}
} else {
const auto& failure = result.GetStatus();
out << " category="
<< (failure.Category() ? static_cast<int>(*failure.Category())
: -1);
for (const auto& diagnostic : failure.Diagnostics())
out << ' ' << static_cast<int>(diagnostic.severity) << ' '
<< std::quoted(diagnostic.code) << ' '
<< std::quoted(diagnostic.location.file.generic_string()) << ' '
<< diagnostic.location.line << ' '
<< std::quoted(diagnostic.keyword) << ' '
<< std::quoted(diagnostic.entity_identity) << ' '
<< std::quoted(diagnostic.message);
}
out << '\n';
};
observe("none", trial, nullptr, true);
observe("zero", trial, &zero, true);
observe("nonzero", trial, &direction, true);
observe("invalid-dimension", trial, &wrong, false);
observe("nan-direction", trial, &nan, false);
observe("inf-direction", trial, &inf, false);
auto invalid = trial;
invalid.displacement[1] = {std::numeric_limits<double>::quiet_NaN(), 0, 0};
observe("nan-trial-none", invalid, nullptr, false);
observe("none-after-failure", trial, nullptr, true);
auto requests = [&](const std::string& name,
const NonlinearMitc4TrialDofs& input) {
observe((name + "-none").c_str(), input, nullptr, std::nullopt);
observe((name + "-zero").c_str(), input, &zero, std::nullopt);
observe((name + "-nonzero").c_str(), input, &direction, std::nullopt);
};
requests("zero-rotation", NonlinearMitc4TrialDofs{});
for (const double threshold : {1e-8, 0.25}) {
const double angle = std::sqrt(threshold);
const std::array<double, 3> angles{
std::nextafter(angle, 0.0), angle,
std::nextafter(angle, std::numeric_limits<double>::infinity())};
for (std::size_t i = 0; i < angles.size(); ++i) {
NonlinearMitc4TrialDofs boundary{};
for (auto& rotation : boundary.rotation_vector)
rotation = {angles[i], 0, 0};
requests((threshold == 0.25 ? "rotation-squared-quarter-"
: "rotation-squared-small-") +
std::to_string(i),
boundary);
}
}
auto translated = trial;
for (auto& displacement : translated.displacement)
displacement = displacement + Vector3{1e9, -2e9, 3e9};
requests("large-translation", translated);
}
return out.str();
}
__declspec(noinline) double Consume(const Jet& value) {
double result =
value.value + value.directional_first + value.directional_second;
for (double entry : value.first) result += entry;
for (double entry : value.second) result += entry;
for (double entry : value.mixed_first) result += entry;
for (double entry : value.mixed_second) result += entry;
return result + value.constant_derivatives + value.second.IsImplicitZero();
}
// T3 evidence is separate from both immutable earlier snapshot formats. These
// calls also exercise the original overload when built against the T1 source.
std::string AccumulationSnapshots(bool reassociate) {
const std::array<double, 12> boundary{
0.0, -0.0, 1.0, -1.0, 0.1, -0.3, 1e16,
std::numeric_limits<double>::denorm_min(),
std::numeric_limits<double>::max(),
std::numeric_limits<double>::infinity(),
std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::signaling_NaN()};
std::string out;
const std::array<int, 4> rounding{FE_TONEAREST, FE_DOWNWARD, FE_UPWARD,
FE_TOWARDZERO};
for (const int mode : rounding) {
for (const unsigned flush : {0u, 0x8040u}) {
for (const unsigned sticky : {0u, 0x21u}) {
for (std::size_t kind = 0; kind < boundary.size(); ++kind) {
ResetFp();
Jet left = Variable(boundary[kind], kind, -0.25);
Jet right = Variable(0.5, (kind + 1) % 24, 0.125);
if (kind % 3 == 0) left = Constant(boundary[kind]);
if (kind % 3 == 1) {
left.second.Data()[25] = boundary[kind];
right.second.Data()[48] = -0.5;
}
left.mixed_first[3] = boundary[kind];
left.mixed_second[4] = -0.0;
const Jet saved_left = left;
const Jet saved_right = right;
const auto before_left = Snapshot("input", left, 0, 0, 0);
const auto before_right = Snapshot("input", right, 0, 0, 0);
const auto environment = [&] {
ResetFp();
std::fesetround(mode);
_mm_setcsr((_mm_getcsr() & ~0x8040u) | flush | sticky);
};
const auto observe = [&](const char* name, auto expression) {
environment();
const auto before = FpState();
const Jet result = expression();
const int exceptions = std::fetestexcept(FE_ALL_EXCEPT);
const unsigned status = _statusfp(), mxcsr = _mm_getcsr();
out += std::to_string(mode) + ":" + std::to_string(flush) + ":" +
std::to_string(sticky) + ":" + std::to_string(kind) + " " +
before + " " + FpState() + " " +
Snapshot(name, result, exceptions, status, mxcsr);
};
const JetVector3 a{left, right, Constant(-1)};
const JetVector3 b{right, Constant(1), left};
observe("dot", [&] {
if (reassociate)
return a[0] * b[0] + (a[1] * b[1] + a[2] * b[2]);
return Dot(a, b);
});
for (std::size_t component = 0; component < 3; ++component)
observe("cross", [&] { return Cross(a, b)[component]; });
const JetMatrix3 matrix{{a, b, a}};
const JetMatrix3 other{{b, a, b}};
// Every matrix/vector result is consumed over the whole fixture;
// boundary groups rotate the selected entry to keep evidence bounded.
observe("matrix", [&] {
return Multiply(matrix, other)[kind % 3][(kind / 3) % 3];
});
observe("matrix-vector", [&] {
return Multiply(matrix, b)[kind % 3];
});
observe("frame", [&] { return FrameComponent(a, matrix, b); });
observe("fixed-frame", [&] {
return FrameComponent(ConstantVector(Vector3{0.5, -1, 0}), matrix,
ConstantVector(Vector3{-0.0, 0.25, 1}));
});
observe("energy-accumulation", [&] {
Jet energy{};
for (std::size_t i = 0; i < 3; ++i)
energy = std::move(energy) + a[i] * b[i];
return energy;
});
observe("shared-rvalue", [&] {
Jet value = left;
return std::move(value) + right;
});
observe("same-object-rvalue", [&] {
Jet value = right;
return std::move(value) + value;
});
observe("unique-rvalue", [&] {
Jet value = left * right;
return std::move(value) + right;
});
observe("constant-product", [&] { return Constant(-0.5) * left; });
observe("scalar-product", [&] { return -0.5 * left; });
if (before_left != Snapshot("input", saved_left, 0, 0, 0) ||
before_left != Snapshot("input", left, 0, 0, 0) ||
before_right != Snapshot("input", saved_right, 0, 0, 0) ||
before_right != Snapshot("input", right, 0, 0, 0))
throw std::runtime_error("accumulation input alias modified");
}
}
}
}
ResetFp();
return out;
}
std::string AccumulationAliasSnapshots(bool overwrite_input) {
std::string out;
for (int kind = 0; kind < 4; ++kind) {
Jet value = Variable(kind == 0 ? -0.0 : 0.5, 0, -0.25) *
Variable(0.75, 1, 0.5);
if (kind == 1) value = value - value;
if (kind == 2) value.second.Data()[0] =
std::numeric_limits<double>::max();
const auto before = Snapshot("live-input", value, 0, 0, 0);
out += Observe("unique-same-object", [&] {
if (overwrite_input) value.first[0] = 17; // Fixture-only negative control.
return std::move(value) + value;
});
if (before != Snapshot("live-input", value, 0, 0, 0))
throw std::runtime_error("same-object live operand modified");
Jet survivor = value;
out += Observe("shared-move", [&] {
return std::move(value) + Constant(-1);
});
if (before != Snapshot("live-input", survivor, 0, 0, 0))
throw std::runtime_error("surviving shared operand modified");
}
return out;
}
void AccumulationBench() {
const Jet left = Variable(0.5, 0, 0.25) * Variable(0.75, 1, -0.5);
const Jet right = Variable(0.25, 2, 0.125) * Variable(-0.5, 3, 0.25);
const JetVector3 a{left, right, left}, b{right, left, right};
const JetMatrix3 matrix{{a, b, a}}, other{{b, a, b}};
const auto e1 = ConstantVector(Vector3{0.5, -1, 0.25});
const auto e2 = ConstantVector(Vector3{-0.25, 0.5, 1});
volatile double consumed = 0;
for (int kind = 0; kind < 6; ++kind) {
const auto start = std::chrono::steady_clock::now();
for (int i = 0; i < 3000; ++i) {
if (kind == 0) consumed = consumed + Consume(Dot(a, b));
if (kind == 1) {
const auto result = Cross(a, b);
for (const auto& value : result) consumed = consumed + Consume(value);
}
if (kind == 2) {
const auto result = Multiply(matrix, other);
for (const auto& row : result)
for (const auto& value : row) consumed = consumed + Consume(value);
}
if (kind == 3) {
const auto result = Multiply(matrix, b);
for (const auto& value : result) consumed = consumed + Consume(value);
}
if (kind == 4)
consumed = consumed + Consume(FrameComponent(e1, matrix, e2));
if (kind == 5) {
Jet density{};
for (std::size_t component = 0; component < 3; ++component)
density = std::move(density) + a[component] * b[component];
consumed = consumed + Consume(0.5 * density);
}
}
std::cout << "accumulation-bench," << kind << ",3000,"
<< std::chrono::duration<double>(
std::chrono::steady_clock::now() - start).count()
<< ",allocation-included,setup-excluded," << consumed << '\n';
}
}
void Bench() {
volatile double consumed = 0;
for (int kind = 0; kind < 8; ++kind) {
Jet left =
kind == 0 ? Constant(0.5) : Variable(0.5, 0, kind % 2 ? 0.0 : 0.25);
Jet right = Variable(0.75, 1, kind == 0 || kind % 2 ? 0.0 : -0.5);
if (kind >= 3) {
left = left * left;
right = right * right;
}
if (kind == 5) left = left - left;
if (kind == 7) {
left = Constant(0.5);
right = Constant(0.75);
}
const auto start = std::chrono::steady_clock::now();
for (int i = 0; i < 20000; ++i) {
left.value += (i % 2 ? -1.0 : 1.0) / 1048576;
const Jet value = kind == 6 ? Dot(JetVector3{left, right, left},
JetVector3{right, left, right})
: left * right;
consumed = consumed + Consume(value);
}
std::cout << "bench," << kind << ",20000,"
<< std::chrono::duration<double>(
std::chrono::steady_clock::now() - start)
.count()
<< ",allocation-included," << consumed << '\n';
}
}
} // namespace
} // namespace fesa
int main(int argc, char** argv) {
try {
std::fegetenv(&fesa::initial_environment);
fesa::initial_mxcsr = _mm_getcsr();
if (argc == 2 && std::string(argv[1]) == "--bench") {
fesa::Bench();
return 0;
}
if (argc == 2 && std::string(argv[1]) == "--accumulation-bench") {
fesa::AccumulationBench();
return 0;
}
if (argc >= 3 && (std::string(argv[1]) == "--accumulation-write" ||
std::string(argv[1]) == "--accumulation-compare")) {
const bool alias = argc > 3 && std::string(argv[3]) == "--unique-alias";
auto actual = alias ? fesa::AccumulationAliasSnapshots(
argc > 4 && std::string(argv[4]) ==
"--overwrite-input")
: fesa::AccumulationSnapshots(
argc > 3 && std::string(argv[3]) ==
"--reassociate");
if (std::string(argv[1]) == "--accumulation-write") {
if (std::ifstream(argv[2], std::ios::binary))
throw std::runtime_error("accumulation snapshot already exists");
std::ofstream output(argv[2], std::ios::binary);
output << actual;
output.close();
if (!output) throw std::runtime_error("accumulation snapshot write failure");
} else {
std::ifstream input(argv[2], std::ios::binary);
const std::string expected((std::istreambuf_iterator<char>(input)), {});
if (expected != actual)
throw std::runtime_error("accumulation snapshot mismatch");
}
std::cout << argv[1]
<< (alias ? " passed; 8 unique/same-object/shared alias cases"
: " passed; 192 boundary/environment groups, Dot/"
"Cross/matrix/vector/frame/accumulation/constants")
<< ", 651 channels and FP state\n";
return 0;
}
if (argc >= 3 && (std::string(argv[1]) == "--product-write" ||
std::string(argv[1]) == "--product-compare")) {
auto actual = fesa::ProductSnapshots(
argc > 3 && std::string(argv[3]) == "--omit-transpose");
if (argc > 3 && std::string(argv[3]) == "--mutate")
actual[actual.find('\n') - 1] ^= 1;
if (std::string(argv[1]) == "--product-write") {
if (std::ifstream(argv[2], std::ios::binary))
throw std::runtime_error("supplemental snapshot already exists");
std::ofstream output(argv[2], std::ios::binary);
output << actual;
output.close();
if (!output) throw std::runtime_error("supplemental snapshot write failure");
} else {
std::ifstream input(argv[2], std::ios::binary);
const std::string expected((std::istreambuf_iterator<char>(input)), {});
if (expected != actual)
throw std::runtime_error("supplemental product snapshot mismatch");
}
std::cout << argv[1] << " passed; 3072 product cases and paired MKL outer "
"checks, 651 channels, input aliases, rounding/FTZ/DAZ/sticky\n";
return 0;
}
const bool verify_only = argc == 2 && std::string(argv[1]) == "--verify";
if (argc < 3 && !verify_only)
throw std::runtime_error(
"--verify OR --write|--compare PATH [--mutate|--corrupt-oracle]");
std::string actual =
fesa::Verify(argc > 3 && std::string(argv[3]) == "--corrupt-oracle");
actual += fesa::PublicSnapshots();
if (argc > 3 && std::string(argv[3]) == "--mutate")
actual[actual.find('\n') - 1] ^= 1;
if (verify_only) {
// CTest and ordinary regression do not create baseline files.
} else if (std::string(argv[1]) == "--write") {
if (std::ifstream(argv[2], std::ios::binary))
throw std::runtime_error("snapshot already exists");
std::ofstream output(argv[2], std::ios::binary);
output << actual;
output.close();
if (!output) throw std::runtime_error("snapshot write failure");
} else if (std::string(argv[1]) == "--compare") {
std::ifstream input(argv[2], std::ios::binary);
const std::string expected((std::istreambuf_iterator<char>(input)), {});
if (expected != actual)
throw std::runtime_error("snapshot mismatch (bits/provenance/fenv)");
} else {
throw std::runtime_error("unknown verification mode");
}
std::cout
<< argv[1]
<< " passed; independent oracle and 42 private cases "
"(651 floating channels each), 64 public request/error cases\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
@@ -0,0 +1,104 @@
# Nonlinear MITC4 Jet 기준 검사
`fesa_jet_baseline_tests`는 제품의 private 구현을 별도 번역 단위에 포함하고
일반 solver archive를 연결한다. 제품 source와 공개 API를 계측하지 않는다.
독립 다항식 지수 미분 및 미분 항등식과 원래 경로의 비트 snapshot은 별도 근거다.
VS x64 환경과 저장소의 승인된 dependency 환경변수에서 실행한다.
```bat
MSBuild msvc\fesa_jet_baseline_tests.vcxproj /m:2 /p:Configuration=Debug /p:Platform=x64
.build\native\Debug\tests\fesa_jet_baseline_tests.exe --verify
cmake --preset windows-msvc-ninja-debug
cmake --build --preset windows-msvc-ninja-debug --target fesa_jet_baseline_tests --parallel 2
ctest --preset windows-msvc-ninja-debug -R "^NonlinearMitc4Jet\.IndependentOracle$" --output-on-failure
```
Release는 Native의 `Configuration=Release` 또는 해당 CMake Release preset을 쓴다.
기존 unit/integration/reference 세 GTest 실행물의 발견 identity와 이 전용 검사의
선택 수는 구분한다. CTest의 `--verify`는 baseline 파일을 생성하지 않는다.
승인된 기준 revision에서만 새 경로에 `--write <snapshot>`을 한 번 실행한다.
기존 파일은 덮어쓰지 않는다. 이후에는 원래 파일과 hash를 보존하고 같은 빌드 경로와
구성끼리 비교한다. 일반 회귀나 후보 코드에서 `--write`로 기대값을 만들지 않는다.
```bat
.build\native\Debug\tests\fesa_jet_baseline_tests.exe --compare "<immutable-Debug-snapshot>"
.build\native\Release\tests\fesa_jet_baseline_tests.exe --compare "<immutable-Release-snapshot>"
.build\cmake-debug\tests\fesa_jet_baseline_tests.exe --compare "<immutable-CMake-Debug-snapshot>"
```
`--compare <snapshot> --mutate`와 `--corrupt-oracle`는 각각 snapshot 비교와 독립
oracle의 fixture 전용 오류 주입이며 exit 1이 기대값이다. `--bench`는 별도 micro
측정이다. 연산의 할당·해제와 교대 입력값 갱신을 포함하고 fixture 생성은 제외한다.
전체 채널을 읽는 noinline checksum과 volatile consume을 포함해 결과 제거를 막는다.
공식 모델 CLI의 시간과 혼합하지 않는다.
snapshot은 finite 값·signed zero·Inf 비트, NaN 분류, provenance와 implicit Hessian,
복원한 초기 FP 환경 및 연산 뒤 FE/rounding/control word/full MXCSR를 기록한다.
공개 반환의 방향 요청 유무·실패 진단·회복량 identity도 포함한다. 수식의 정확성은
독립 oracle로 검사하고, snapshot은 기준의 계산 순서와 관측 의미 보존을 검사한다.
T2 곱셈 경계는 기존 T1 snapshot과 별도 CLI로 보존한다. 원본 제품 source의 hash를
확인하고 보존한 원본 archive를 연결한 실행물에서만 새 경로에 한 번 생성한다.
후보는 `--product-compare`만 실행한다. 기존 파일을 덮어쓰거나 T1 기대값을 확장하지 않는다.
```bat
<original-fixture.exe> --product-write <new-immutable-product-snapshot>
<candidate-fixture.exe> --product-compare <immutable-product-snapshot>
<candidate-fixture.exe> --product-compare <immutable-product-snapshot> --mutate
<candidate-fixture.exe> --product-compare <immutable-product-snapshot> --omit-transpose
```
마지막 두 명령은 각각 출력 변조와 reverse 외적 항 누락의 fixture 전용 음성 대조로
exit 1이 기대값이다. 이 그룹은 96개 입력을 4 rounding × 4 FTZ/DAZ × 2 초기 sticky
환경에서 실행한 3072개 곱셈이다. 전체 651 채널, 비대칭·공유·implicit Hessian과
입력 불변성, provenance, 전체 FP 상태를 비교한다. finite 전용 ±0·subnormal·underflow·
overflow gradient와 비유한 입력 거부를 구분하며, 실제 MKL 두 외적의 operand 교환과
transpose 성분도 비교한다. 두 호출의 FP 상태는 각각 기록하므로 입력 검사 조기 종료로
차이가 있는 경우를 수치 동등성으로 숨기지 않는다. NaN payload는 비교하지 않는다.
이 그룹은 예외를 mask한 상태의 관측이다. Unmasked 환경의 최초 trap 순서는 별도
SEH/process 관측이나 생성 코드 근거가 필요하며, source의 row-major 순서만으로 보장하지 않는다.
T2의 무가드 단일 외적 후보는 unmasked 최초 INVALID/OVERFLOW 순서가 달라 비채택했다.
채택한 guarded 경로는 MXCSR의 여섯 예외가 모두 masked이고 첫 외적이 성공한 경우에만
전치 성분과 276개 상삼각 scratch를 재사용한다. Unmasked 환경과 외적 거부는 기존 두 호출,
576개 reverse 외적과 row-major 결합을 유지한다. Masked snapshot과 별도 원본/후보 SEH
관측을 함께 확인하며, 정상 경로의 임시 쓰기 감소를 전체 stack 예약 감소로 보고하지 않는다.
T5는 서로 배타적인 276개 상삼각 보관과 576개 reverse 외적에 같은 local
`std::array<double, 576>` scratch를 사용한다. 입력·공유 Hessian의 저장소와 수명은
바꾸지 않는다. 두 분기에서 쓰기 전 읽기가 없는지 검토하고 위 3구성 비교 및 별도
6개 unmasked 최초 trap을 확인한다. Stack 예약량은 실제 Release production object의
frame과 `__chkstk`를 확인하며, heap 할당 감소나 전체 모델 속도 향상으로 해석하지 않는다.
별도 복사 source의 Values 생성·소멸 계측은 생존 시작량을 포함한 peak payload와
증가량을 구분한다. 동시 호출 peak는 스케줄에 따라 달라지고, 계측 시간은 정상 Release
측정에서 제외한다. Hessian 복사 연산 횟수는 implicit/null 복사도 포함하며 strong-ref
증가 횟수나 CPU 비용을 뜻하지 않는다. Global pool이나 trial 간 cache는 도입하지 않는다.
T3 누적 검사는 기존 T1/T2 snapshot을 확장하지 않고 별도 파일을 쓴다. 원본 source와
보존 archive의 출처를 확인한 실행물에서만 새 경로에 생성한다. 실제 CMake 경로는
원본 source staging을 CMake로 configure/build한 실행물과 구분해 연결한다.
```bat
<original-fixture.exe> --accumulation-write <new-immutable-accumulation-snapshot>
<candidate-fixture.exe> --accumulation-compare <immutable-accumulation-snapshot>
<candidate-fixture.exe> --accumulation-compare <immutable-accumulation-snapshot> --reassociate
<original-fixture.exe> --accumulation-write <new-immutable-alias-snapshot> --unique-alias
<candidate-fixture.exe> --accumulation-compare <immutable-alias-snapshot> --unique-alias
<candidate-fixture.exe> --accumulation-compare <immutable-alias-snapshot> --unique-alias --overwrite-input
<candidate-fixture.exe> --accumulation-bench
```
일반 그룹은 12개 경계 × 4 rounding × 2 FTZ/DAZ × 2 초기 sticky의 192개 조합으로
Dot·Cross·3×3 matrix/vector·일반/고정 frame·energy 형태 누적·상수 Jet/scalar를
검사한다. 별도 8개 alias 그룹은 유일한 materialized Hessian을 가진 동일 객체,
surviving copy와 공유 rvalue를 검사한다. `--reassociate`와 `--overwrite-input`는
fixture 전용 음성 대조이며 exit 1이 기대값이다. 모든 비교는 651개 채널·provenance·
implicit Hessian·FP 상태를 포함한다. 상수 Jet와 scalar를 서로 같은 연산으로 가정하지 않는다.
`--accumulation-bench`는 여섯 연산 종류마다 3,000회, 전체 결과 채널 checksum을
소비한다. Fixture 준비는 제외하고 연산의 할당·해제와 checksum은 포함한다. 이 일반
실행물의 시간과 별도 복사 source의 Hessian materialization 계측을 구분한다.
Unmasked 최초 trap은 별도 SEH/process 관측으로, 곱셈 단계와 실제 덧셈 단계의
검증을 구분한다. 전체 모델 시간·stack 예약 감소를 뜻하지 않는다.